-
Notifications
You must be signed in to change notification settings - Fork 172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Returning functions in place #76
Comments
🤔 🤔 🤔 This one is a tough one for me to respond to. On one hand, I can see why that might be desirable by some users who are used to the On the other hand, I think adding it in as an option means an additional thing to check for when any new function is submitted in. Conceptually, it's "just one less thing" to maintain if we just leave that argument out. With a fluent API, I don't think in-place modifications are a thing. That said, I'm willing to be persuaded by a counter-example to my general slant against an "in-place" interface, if one can provide an example where in-place modification accomplishes something substantial that a method chaining interface cannot accomplish. |
I’m actually more inclined have the default be to return functions in place for a few reasons:
|
@szuckerman, let me see if I can clarify my understanding of the definition of in-place vs. method chaining. Taking the example from the README, regular df = pd.DataFrame(...) # create a pandas DataFrame somehow.
del df['column1'] # delete a column from the dataframe.
df = df.dropna(subset=['column2', 'column3']) # drop rows that have empty values in column 2 and 3.
df = df.rename({'column2': 'unicorns', 'column3': 'dragons'}) # rename column2 and column3
df['newcolumn'] = ['iterable', 'of', 'items'] # add a new column. First off, with in-place modifications at each line, we do: df = pd.DataFrame(...)
df.remove_column('column1', inplace=True)
df.dropna(subset=['column2', 'column3'], inplace=True)
df.add_column('newcolumn', ['iterable', 'of', 'items'], inplace=True)
df.rename({'column2': 'unicorns', 'column3': 'dragons'}, inplace=True) To clarify, def function_name(df, kwargs, inplace=False):
# code above
if not inplace:
return df With method chaining, the syntax looks as such: df = (
pd.DataFrame(...)
.remove_column('column1')
.dropna(subset=['column2', 'column3'])
.add_column('newcolumn', ['iterable', 'of', 'items'])
.rename({'column2': 'unicorns', 'column3': 'dragons'})
) With method chaining, each function must return the modified dataframe. Without Also, without an |
Ok, I hear what you’re saying, and agree that things should be kept simple to allow for more contributions. That being the case, some of the functions already work in place. For example, limit_column_characters does, and I think add_column does too (but I’m not near a computer at the moment to check). Should functions be modified so they specifically do not work in place? |
@szuckerman yup, You can see here that |
@szuckerman yes, that's a valid concern, especially for consistency purposes. I think the hack that we've implicitly used, but not explicitly stated, is to piggy back off pandas operations. If modification takes place in-place anyways (to prevent memory copying), then it happens in-place; if not, then we don't really care. For a pareto-distributed set of users, 80% will have datasets that fit in-memory, while 20% will have @zbarry's kind of needs, where dataframes are too large for a single computer's RAM. At the moment, I don't think we're necessarily at a point where too many people will complain about the memory-usage architecture of |
Should mention that it's not a fit in memory problem or usage really, though that's definitely relevant... it's more performance-wise. Copying all that data every time you hit an op in a chain is really inefficient in time. Since the whole concept of chaining here means that can't get the intermediate dataframes out for each step anyway, having them mutate the original frame each time doesn't actually matter (provided you copy the original one before you begin to chain if it's necessary to keep it). Also thinking that after a decision is made w.r.t in place / not, it would be difficult to change it (in the case of going from not in place to in place, impossible to not be very disruptive to users' code, I think). |
@zbarry makes a good point that "since you can't get the intermediate dataframes out for each step anyway" returning in place is more performant. However, after doing some research it doesn't look like returning objects in place makes too much of a difference. I think the method-chaining syntax saves most people from the concern of making mistakes (i.e. by accidentally running a method that works in place by default), as given as one of the examples on stackoverflow. As for future considerations of the package, I think that to really determine whether methods should default to being in place or not depends on "usability". Will users be confused when dataframes are mutated in place? Later we can run tests, profile memory, etc. to determine if there's a difference between a method and its in place counterpart. |
Yeah, usability is probably core to the idea of pyjanitor even. That being said, I think having a (short) paragraph in the readme + in the notebook example I will (eventually) create as well as having a clear boilerplate message in all the function docs would convey this pretty well. If you make it such that all functions that are relevant w.r.t. this discussion operate on the same principle of in place, I think this gives a clear expection to users as to what approach pyjanitor takes for operations on Because I'm procrastinating, I ran a simple speed test for adding a column to a The slowdown with the data copy in this particular operation is a pretty consistent 1.5x. This could definetely add up when users chain multiple operations (I think it's possible that some ops could have even worse slowdown multipliers). |
@zbarry those are great tests. Yes, I think it's worth adding to the docstrings (but without user warnings popping up - those are too easily abused) that for the relevant functions, underneath the hood, we do change the original dataframe in place for performance reasons. |
Hmm... I was thinking of adding a pyqt5 dependency & popping up a message box warning you upon pyjanitor import of the mutation operations. Shame. |
Not sure if anyone is following Pandas development, but there is currently a plan to deprecate the inplace parameter. That may be worth taking into consideration for this discussion. |
After seeing issue #67, was curious as to what people think as to adding this capability to all functions?
Some of them, like
df.limit_column_characters()
, already return in place. I don't think it will be hard to extend to others.The text was updated successfully, but these errors were encountered: