diff --git a/web/pandas/pdeps/0007-copy-on-write.md b/web/pandas/pdeps/0007-copy-on-write.md new file mode 100644 index 0000000000000..e45fbaf555bc1 --- /dev/null +++ b/web/pandas/pdeps/0007-copy-on-write.md @@ -0,0 +1,589 @@ +# PDEP-7: Consistent copy/view semantics in pandas with Copy-on-Write + +- Created: July 2021 +- Status: Accepted +- Discussion: [#36195](https://github.com/pandas-dev/pandas/issues/36195) +- Author: [Joris Van den Bossche](https://github.com/jorisvandenbossche) +- Revision: 1 + +## Abstract + +Short summary of the proposal: + +1. The result of _any_ indexing operation (subsetting a DataFrame or Series in any way, + i.e. including accessing a DataFrame column as a Series) or any method returning a + new DataFrame or Series, always _behaves as if_ it were a copy in terms of user + API. +2. We implement Copy-on-Write (as implementation detail). This way, we can actually use + views as much as possible under the hood, while ensuring the user API behaves as a + copy. +3. As a consequence, if you want to modify an object (DataFrame or Series), the only way + to do this is to directly modify that object itself . + +This addresses multiple aspects: 1) a clear and consistent user API (a clear rule: _any_ +subset or returned series/dataframe **always** behaves as a copy of the original, and +thus never modifies the original) and 2) improving performance by avoiding excessive +copies (e.g. a chained method workflow would no longer return an actual data copy at each +step). + +Because every single indexing step behaves as a copy, this also means that with this +proposal, "chained assignment" (with multiple setitem steps) will _never_ work and +the `SettingWithCopyWarning` can be removed. + +## Background + +pandas' current behavior on whether indexing returns a view or copy is confusing. Even +for experienced users, it's hard to tell whether a view or copy will be returned (see +below for a summary). We'd like to provide an API that is consistent and sensible about +returning views vs. copies. + +We also care about performance. Returning views from indexing operations is faster and +reduces memory usage. The same is true for several methods that don't modify the data +such as setting/resetting the index, renaming columns, etc. that can be used in a method +chaining workflow and currently return a new copy at each step. + +Finally, there are API / usability issues around views. It can be challenging to know +the user's intent in operations that modify a subset of a DataFrame (column and/or row +selection), like: + +```python +>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) +>>> df2 = df[["A", "B"]] +>>> df2.loc[df2["A"] > 1, "A"] = 1 +``` + +Did the user intend to modify `df` when they modified `df2` (setting aside issues with +the current implementation)? In other words, if we had a perfectly consistent world +where indexing the columns always returned views or always returned a copy, does the +code above imply that the user wants to mutate `df`? + +There are two possible behaviours the user might intend: + +1. Case 1: I know my subset might be a view of the original and I want to modify the + original as well. +2. Case 2: I just want to modify the subset without modifying the original. + +Today, pandas' inconsistency means _neither_ of these workflows is really possible. The +first is difficult, because indexing operations often (though not always) return copies, +and even when a view is returned you sometimes get a `SettingWithCopyWarning` when +mutating. The second is somewhat possible, but requires many defensive copies (to avoid +`SettingWithCopyWarning`, or to ensure that you have a copy when a view _was_ returned). + +## Proposal + +For these reasons (consistency, performance, code clarity), this PDEP proposes the +following changes: + +1. The result of _any_ indexing operation (subsetting a DataFrame or Series in any way, + i.e. including accessing a DataFrame column as a Series) or any method returning a + new DataFrame or Series, always _behaves as if_ it were a copy in terms of user + API. +2. We implement Copy-on-Write. This way, we can actually use views as much as possible + under the hood, while ensuring the user API behaves as a copy. + +The intent is to capture the performance benefits of views as much as possible, while +providing consistent and clear behaviour to the user. This essentially makes returning +views an internal optimization, without the user needing to know if the specific +indexing operation would return a view or a copy. The new rule would be simple: any +series/dataframe derived from another series/dataframe, through an indexing operation or +a method, always behaves as a copy of the original series/dataframe. + +The mechanism to ensure this consistent behaviour, Copy-on-Write, would entail the +following: the setitem operation (i.e. `df[..] = ..` or `df.loc[..] = ..` or +`df.iloc[..] = ..`, or equivalent for Series) would check if the data that is being +modified is a view on another dataframe (or is being viewed by another dataframe). If it +is, then we would copy the data before mutating. + +Taking the example from above, if the user wishes to not mutate the parent, we no longer +require a defensive copy just to avoid a `SettingWithCopyWarning`. + +```python +# Case 2: The user does not want mutating df2 to mutate the parent df, via CoW +>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) +>>> df2 = df[["A", "B"]] +>>> df2.loc[df2["A"] > 1, "A"] = 1 +>>> df.iloc[1, 0] # df was not mutated +2 +``` + +On the other hand, if the user actually wants to modify the original df, they can no +longer rely on the fact that `df2` could be a view, as mutating a subset would now never +mutate the parent. The only way to modify the original df is by combining all indexing +steps in a single indexing operation on the original (no "chained" setitem): + +```python +# Case 1: user wants mutations of df2 to be reflected in df -> no longer possible +>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) +>>> df2 = df[["A", "B"]] +>>> df2.loc[df2["A"] > 1, "A"] = 1 # mutating df2 will not mutate df +>>> df.loc[df["A"] > 1, "A"] = 1 # need to directly mutate df instead +``` + +### This proposal also extends to methods + +In principle, there's nothing special about indexing when it comes to defensive copying. +_Any_ method that returns a new series/dataframe without altering existing data (rename, +set_index, assign, dropping columns, etc.) currently returns a copy by default and is a +candidate for returning a view: + +```python +>>> df2 = df.rename(columns=str.lower) +>>> df3 = df2.set_index("a") +``` + +Now, generally, pandas users won't expect `df2` or `df3` to be a view such that mutating +`df2` or `df3` would mutate `df`. Copy-on-Write allows us to also avoid +unnecessary copies in methods such as the above (or in the variant using method chaining +like `df.rename(columns=str.lower).set_index("a")`). + +### Propagating mutation forwards + +Thus far we have considered the (more common) case of taking a subset, mutating the +subset, and how that should affect the parent. What about the other direction, where the +parent is mutated? + +```python +>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4]}) +>>> df2 = df[["A"]] +>>> df.iloc[0, 0] = 10 +>>> df2.iloc[0, 0] # what is this value? +``` + +Given that `df2` is _considered_ as a copy of df under this proposal (i.e. behaves as a +copy), also mutating the parent `df` will not mutate the subset `df2`. + +### When do mutations propagate to other objects and when not? + +This proposal basically means that mutations _never_ propagate to _other_ objects (as +would happen with views). The only way to modify a DataFrame or Series is to modify the +object itself directly. + +But let's illustrate this in Python terms. Consider that we have a DataFrame `df1`, and we +assign that to another name `df2`: + +```python +>>> df1 = pd.DataFrame({"A": [1, 2], "B": [3, 4]}) +>>> df2 = df1 +``` + +Although we have now two variables (`df1` and `df2`), this assignment follows the standard +python semantics, and both names are pointing to the same object ("df1 and df2 are +_identical_"): + +```python +>>> id(df1) == id(df2) # or: df1 is df2 +True +``` + +Thus, if you modify DataFrame `df2`, this is also reflected in the other variable `df1`, and +the other way around (since it's the same object): + +```python +>>> df1.iloc[0, 0] +1 +>>> df2.iloc[0, 0] = 10 +>>> df1.iloc[0, 0] +10 +``` + +In summary, modifications are only "propagated" between _identical_ objects (not just +equal (`==`), but identical (`is`) in python terms, see +[docs](https://docs.python.org/3/reference/expressions.html#is)). Propagation is not +really the proper term, since there is only one object that was modified. + +However, when in some way creating a new object (even though it might be a DataFrame +with the same data, and thus be an "equal" DataFrame): + +```python +>>> df1 = pd.DataFrame({"A": [1, 2], "B": [3, 4]}) +>>> df2 = df1[:] # or df1.loc[...] with some indexer +``` + +Those objects are no longer identical: + +```python +>>> id(df1) == id(df2) # or df1 is df2 +False +``` + +And thus modifications to one will not propagate to the other: + +```python +>>> df1.iloc[0, 0] +1 +>>> df2.iloc[0, 0] = 10 +>>> df1.iloc[0, 0] # not changed +1 +``` + +Currently, any getitem indexing operation returns _new_ objects, and also almost all +DataFrame/Series methods return a _new_ object (except with `inplace=True` in some +cases), and thus follow the above logic of never modifying its parent/child DataFrame or +Series (using the lazy Copy-on-Write mechanism where possible). + +## Copy / view behaviour in NumPy versus pandas + +NumPy has the concept of "views" (an array that shares data with another array, viewing +the same memory, see e.g. +[this explanation](https://scipy-cookbook.readthedocs.io/items/ViewsVsCopies.html) for +more details). Typically you create views as a slice of another array. But other +indexing methods, often called "fancy indexing", do not return views but copies: using a +list of indices or a boolean mask. + +Pandas, being built on NumPy, uses those concepts, and also exposes the behaviour +consequences to its users. This basically means that pandas users, to understand the +details of how indexing works, also need to understand those view / fancy indexing +concepts of numpy. + +However, because DataFrames are not an array, the copy/view rules still differ from +NumPy's rules with current pandas. Slicing rows generally gives a view (following +NumPy), but slicing columns doesn't always give a view (this could be changed to match +NumPy however, see "Alternatives" 1b below). Fancy indexing rows (e.g. with a list of +(positional) labels) gives a copy, but fancy indexing columns _could_ give a view +(currently this gives a copy as well, but one of the "Alternatives" (1b) is to have this +always return a view). + +The proposal in this document is to decouple the pandas user-facing behaviour from those +NumPy concepts. Creating a subset of a DataFrame with a slice or with a mask would +behave in a similar way for the user (both return a new object and behave as a copy of +the original). We still use the concept of views internally in pandas to optimize the +implementation, but this becomes hidden from the user. + +## Alternatives + +The [original document](https://docs.google.com/document/d/1csGE4qigPR2vzmU2--jwURn3sK5k5eVewinxd8OUPk0/edit) and GitHub issue ([Proposal for future copy / view semantics in indexing operations - #36195](https://github.com/pandas-dev/pandas/issues/36195)) discussed several options for making the copy/view situation more consistent and clear: + +1. **Well-Defined copy/view rules:** ensure we have more consistent rules about which + operations result in a copy and which in a view, and then views result in mutating + the parent, copies not. + a. A minimal change would be to officialize the current behaviour. This comes down to + fixing some bugs and clearly documenting and testing which operations are views, + and which are copies. + b. An alternative would be to simplify the set of rules. For example: selecting + columns is always a view, subsetting rows is always a copy. Or: selecting columns + is always a view, subsetting rows as a slice is a view otherwise always a copy. + +2. **Copy-on-Write**: The setitem operation would check if it's a view on another + dataframe. If it is, then we would copy our data before mutating. (i.e. this + proposal) + +3. **Error-on-Write**: The setitem operation would check if it's a subset of another + dataframe (both view of copy). Only rather than copying in case of a view we would + raise an exception telling the user to either copy the data with + ``.copy_if_needed()`` (name TBD) or mark the frame as "a mutable view" with + ``.as_mutable_view()`` (name TBD). + +This document basically proposes an extended version of option 2 (Copy-on-Write). Some +arguments in favor of Copy-on-Write compared to the other options: + +* Copy-on-Write will improve the copy/view efficiency of _methods_ (e.g. rename, + (re)set_index, drop columns, etc. See section above). This will result in + lower memory usage and better performance. + +* This proposal can also be seen as a clear "well-defined rule". Using Copy-on-Write + under the hood is an implementation detail to delay the actual copy until it is + needed. The rule of "always copy" is the simplest "well-defined rule" we can get. + + Other "well-defined rule" ideas above would always include some specific cases (and + deviations from the NumPy rules). And even with clear rules a user still needs to know + the details of those rules to understand that `df['a'][df['b'] < 0] = 0` or + `df[df['b'] < 0]['a'] = 0` does something differently (switched order of column/row + indexing: the first mutates df (if selecting a column is a view) and the second + doesn't). While with the "always copy" rule with Copy-on-Write, neither of those + examples will work to update `df`. + +On the other hand, the proposal in this document does not give the user control over +whether a subset should be a view (when possible) that mutates the parent when being +mutated. The only way to modify the parent dataframe is with a direct indexing operation +on this dataframe itself. + +See the GitHub comment with some more detailed argumentation: +[https://github.com/pandas-dev/pandas/issues/36195#issuecomment-786654449](https://github.com/pandas-dev/pandas/issues/36195#issuecomment-786654449) + +## Disadvantages + +Other than the fact that this proposal would result in a backwards incompatible, +breaking change in behaviour (see next section), there are some other potential +disadvantages: + +* Deviation from NumPy: NumPy uses the copy and view concepts, while in this proposal + views would basically not exist anymore in pandas (for the user, at least; we would + still use it internally as an implementation detail) + * But as a counter argument: many pandas users are probably not familiar with those + concepts, and pandas already deviates from the exact rules in NumPy. +* Performance cost of indexing and methods becomes harder to predict: because the copy + of the data doesn't happen at the moment when actually creating the new object, but + can happen at a later stage when modifying either the parent or child object, it + becomes less transparent about when pandas copies data (but in general we should copy + less often). This is somewhat mitigated because Copy-on-Write will only copy the columns + that are mutated. Unrelated columns won't get copied. +* Increased memory usage for some use cases: while the majority of use cases will + see an improvement in memory usage with this proposal, there are a few use + cases where this might not be the case. Specifically in cases where pandas currently + does return a view (e.g. slicing rows) and in the case you are fine with (or don't care + about) the current behaviour of it being a view when mutating that subset (i.e. + mutating the sliced subset also mutates the parent dataframe), in such a case the + proposal would introduce a new copy compared to the current behaviour. There is a + workaround for this though: the copy is not needed if the previous object goes out + of scope, e.g. the variable is reassigned to something else. + +## Backward compatibility + +The proposal in this document is clearly a backwards incompatible change that breaks +existing behaviour. Because of the current inconsistencies and subtleties around views +vs. copies and mutation, it would be difficult to change anything without breaking +changes. The current proposal is not the proposal with the minimal changes, though. A +change like this will in any case need to be accompanied with a major version bump (for +example pandas 3.0). + +Doing a traditional deprecation cycle that lives in several minor feature releases will +be too noisy. Indexing is too common an operation to include a warning (even if we limit +it to just those operations that previously returned views). However, this proposal is +already implemented and thus available. Users can opt-in and test their code (this is +possible starting with version 1.5 with `pd.options.mode.copy_on_write = True`). + +Further we will add a warning mode for pandas 2.2 that raises warnings for all cases that +will change behaviour under the Copy-on-Write proposal. We can +provide a clearly documented upgrade path to first enable the warnings, fix all +warnings, and then enable the Copy-on-Write mode and ensure your code is still working, +and then finally upgrade to the new major release. + +## Implementation + +The implementation is available since pandas 1.5 (and significantly improved starting +with pandas 2.0). It uses weakrefs to keep track of whether the +data of a Dataframe/Series are viewing the data of another (pandas) object or are being +viewed by another object. This way, whenever the series/dataframe gets modified, we can +check if its data first needs to be copied before mutating it +(see [here](https://pandas.pydata.org/docs/development/copy_on_write.html)). + +To test the implementation and experiment with the new behaviour, you can +enable it with the following option: + +```python +>>> pd.options.mode.copy_on_write = True +``` + +after importing pandas (or setting the `PANDAS_COPY_ON_WRITE=1` environment variable +before importing pandas). + +## Concrete examples + +### Chained assignment + +Consider a "classic" case of chained indexing, which was the original motivation for the SettingWithCopy warning: + +```python +>>> df[df['B'] > 3]['B'] = 10 +``` + +That is roughly equivalent to + +```python +>>> df2 = df[df['B'] > 3] # Copy under NumPy's rules +>>> df2['B'] = 10 # Update (the copy) df2, df not changed +>>> del df2 # All references to df2 are lost, goes out of scope +``` + +And so `df` is not modified. For this reason, the SettingWithCopyWarning was introduced. + +_With this proposal_, any result of an indexing operation behaves as a copy +(Copy-on-Write), and thus chained assignment will _never_ work. Given that there is then +no ambiguity, the idea is to drop the warning. + +The above example is a case where chained assignment doesn't work with current pandas. +But there are of course also patterns with chained assignment that currently _do_ work +and are used. _With this proposal_, any chained assignment will not work, and so those +cases will stop working (e.g. the case above but switching the order): + +```python +>>> df['B'][df['B'] > 3] = 10 +# or +>>> df['B'][0:5] = 10 +``` + +These cases will raise a warning ``ChainedAssignmentError``, because they can never +accomplish what the user intended. There will be false-positive cases when these +operations are triggered from Cython, because Cython uses a different reference counting +mechanism. These cases should be rare, since calling pandas code from Cython does not +have any performance benefits. + +### Filtered dataframe + +A typical example where the current SettingWithCopyWarning becomes annoying is when +filtering a DataFrame (which always already returns a copy): + +```python +>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) +>>> df_filtered = df[df["A"] > 1] +>>> df_filtered["new_column"] = 1 +SettingWithCopyWarning: +A value is trying to be set on a copy of a slice from a DataFrame. +Try using .loc[row_indexer,col_indexer] = value instead +``` + +If you then modify your filtered dataframe (e.g. adding a column), you get the +unnecessary SettingWithCopyWarning (with confusing message). The only way to get rid of +the warning is by doing a defensive copy (`df_filtered = df[df["A"] > 1].copy()`, which +results in copying the data twice in the current implementation, Copy-on-Write would +not require ``.copy()`` anymore). + +_With this proposal_, the filtered dataframe is never a view and the above +workflow would work as expected without warning (and thus without needing the extra +copy). + +### Modifying a Series (from DataFrame column) + +_Currently_, accessing a column of a DataFrame as a Series is one of the few cases that +is actually guaranteed to always be a view: + +```python +>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) +>>> s = df["A"] +>>> s.loc[0] = 0 # will also modify df (but no longer with this proposal) +``` + +_With this proposal_, any indexing operation results in a copy, so also accessing a +column as a Series (in practice, it will still be a view of course, but behave as a copy +through Copy-on-Write). In the above example, mutating `s` will no longer modify the +parent `df`. + +This situation is similar as the "chained assignment" case above, except with +an explicit intermediate variable. To actually change the original DataFrame, +the solution is the same: mutate directly the DataFrame in a single step. +For example: + +```python +>>> df.loc[0, "A"] = 0 +``` + +### "Shallow" copies + +_Currently_, it is possible to create a "shallow" copy of a DataFrame with +`copy(deep=False)`. This creates a new DataFrame object but without copying the +underlying index and data. Any changes to the data of the original will be reflected in +the shallow copy (and vice versa). See the +[docs](https://pandas.pydata.org/pandas-docs/version/1.5/reference/api/pandas.DataFrame.copy.html). + +```python +>>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) +>>> df2 = df.copy(deep=False) +>>> df2.iloc[0, 0] = 0 # will also modify df (but no longer with this proposal) +``` + +_With this proposal_, this kind of shallow copy is no longer possible. Only "identical" +objects (in Python terms: `df2 is df`) can share data without triggering Copy-on-Write. +A shallow copy will rather become a "delayed" copy through Copy-on-Write. + +See +[#36195 (comment)](https://github.com/pandas-dev/pandas/issues/36195#issuecomment-830579242) +for a more detailed comment on this. + +### Methods returning a new DataFrame with the same data + +This example is already shown above as well, but so _currently_ almost all methods on a +Series/DataFrame by default return a new object that is a copy of the original data: + +```python +>>> df2 = df.rename(columns=str.lower) +>>> df3 = df2.set_index("a") +``` + +In the above example, df2 holds a copy of the data of df, and df3 holds a copy of the +data of df2. Mutating any of those DataFrames would not modify the parent dataframe. + +_With this proposal_, those methods would continue to return new objects, but would use +the shallow copy mechanism with Copy-on-Write so that in practice, those methods don't +need to copy the data at each step, while preserving the current behaviour. + +### Series and DataFrame constructors + +_Currently_, the Series and DataFrame constructors don't always copy the input +(depending on the type of the input). For example: + +```python +>>> s = pd.Series([1, 2, 3]) +>>> s2 = pd.Series(s) +>>> s2.iloc[0] = 0 # will also modify the parent Series s +>>> s +0 0 # <-- modified +1 2 +2 3 +dtype: int64 +``` + +_With this proposal_, we can also use the shallow copy with Copy-on-Write approach _by +default_ in the constructors. This would mean that by default, a new Series or DataFrame +(like `s2` in the above example) would not modify the data from which it is being +constructed (when being modified itself), honoring the proposed rules. + +## More background: Current behaviour of views vs copy + +To the best of our knowledge, indexing operations currently return views in the +following cases: + +* Selecting a single column (as a Series) out of a DataFrame is always a view + (``df['a']``) +* Slicing columns from a DataFrame creating a subset DataFrame (``df[['a':'b']]`` or + ``df.loc[:, 'a': 'b']``) is a view _if_ the the original DataFrame consists of a + single block (single dtype, consolidated) and _if_ you are slicing (so not a list + selection). In all other cases, getting a subset is always a copy. +* Selecting rows _can_ return a view, when the row indexer is a `slice` object. + +Remaining operations (subsetting rows with a list indexer or boolean mask) in practice +return a copy, and we will raise a SettingWithCopyWarning when the user tries to modify +the subset. + +## More background: Previous attempts + +We've discussed this general issue before. [https://github.com/pandas-dev/pandas/issues/10954](https://github.com/pandas-dev/pandas/issues/10954) and a few pull requests ([https://github.com/pandas-dev/pandas/pull/12036](https://github.com/pandas-dev/pandas/pull/12036), [https://github.com/pandas-dev/pandas/pull/11207](https://github.com/pandas-dev/pandas/pull/11207), [https://github.com/pandas-dev/pandas/pull/11500](https://github.com/pandas-dev/pandas/pull/11500)). + +## Comparison with other languages / libraries + +### R + +For the user, R has somewhat similar behaviour. Most R objects can be considered +immutable, through "copy-on-modify" +([https://adv-r.hadley.nz/names-values.html#copy-on-modify](https://adv-r.hadley.nz/names-values.html#copy-on-modify)). +But in contrast to Python, in R this is a language feature, and any assignment (binding +a variable to a new name) or passing as function argument will essentially create a +"copy" (when mutating such an object, at that point the actual data get copied and +rebind to the name): + +```r +x <- c(1, 2, 3) +y <- x +y[[1]] <- 10 # does not modify x +``` + +While if you would do the above example in Python with a list, x and y are "identical" +and mutating one will also mutate the other. + +As a consequence of this language behaviour, modifying a `data.frame` will not modify +other data.frames that might share memory (before being copied with "copy-on-modify"). + +### Polars + +Polars ([https://github.com/pola-rs/polars](https://github.com/pola-rs/polars)) is a +DataFrame library with a Python interface, mainly written in Rust on top of Arrow. It +explicitly +[mentions](https://pola-rs.github.io/polars-book/user-guide/introduction.html#current-status) +"Copy-on-Write" semantics as one its features. + +Based on some experiments, the user-facing behaviour of Polars seems similar to the behaviour +described in this proposal (mutating a DataFrame/Series never mutates a parent/child +object, and so chained assignment also doesn't work) + + +## PDEP-7 History + +- July 2021: Initial version +- February 2023: Converted into a PDEP + +Note: this proposal has been discussed before it was turned into a PDEP. The main +discussion happened in [GH-36195](https://github.com/pandas-dev/pandas/issues/36195). +This document is modified from the original document discussing different options for +clear copy/view semantics started by Tom Augspurger +([google doc](https://docs.google.com/document/d/1csGE4qigPR2vzmU2--jwURn3sK5k5eVewinxd8OUPk0/edit)). + +Related mailing list discussion: [https://mail.python.org/pipermail/pandas-dev/2021-July/001358.html](https://t.co/vT5dOMhNjV?amp=1)