注意
写时复制将在 pandas 3.0 中成为默认设置。我们建议立即启用以享受所有改进。
写时复制首次引入是在版本1.5.0中。从版本2.0开始,大部分通过写时复制实现的优化都已经实施和支持。从pandas 2.1开始,支持所有可能的优化。
写时复制将在版本3.0中默认启用。
写时复制将导致更可预测的行为,因为不可能用一条语句更新多个对象,例如索引操作或方法不会产生副作用。此外,通过尽可能延迟复制,平均性能和内存使用将得到改善。
以前的行为
pandas的索引行为很难理解。某些操作返回视图,而其他操作返回副本。根据操作的结果,更改一个对象可能会意外地更改另一个对象:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
subset = df["foo"]
subset.iloc[0] = 100
df
Out[4]:
foo bar
0 100 4
1 2 5
2 3 6
更改subset
,例如更新其值,也会更新df
。确切的行为很难预测。写时复制解决了意外修改多个对象的问题,它明确禁止这样做。启用写时复制后,df
保持不变:
pd.options.mode.copy_on_write = True
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
subset = df["foo"]
subset.iloc[0] = 100
df
Out[9]:
foo bar
0 1 4
1 2 5
2 3 6
下面的部分将解释这意味着什么以及它如何影响现有应用程序。
迁移到写时复制
写时复制将成为pandas 3.0的默认和唯一模式。这意味着用户需要将他们的代码迁移到符合写时复制规则的代码。
pandas的默认模式将对某些情况引发警告,这些情况将主动更改行为,从而更改用户预期的行为。
我们添加了另一种模式,例如
pd.options.mode.copy_on_write = "warn"
它将对每个将改变写时复制行为的操作发出警告。我们预计这种模式会非常嘈杂,因为我们不希望它们会影响用户的许多情况也会发出警告。我们建议检查此模式并分析警告,但不需要解决所有这些警告。以下列表的前两个项目是需要解决的唯一情况,以使现有代码与写时复制一起工作。
以下几个项目描述了用户可见的更改:
链式赋值将永远不起作用
应该使用loc
作为替代。有关更多详细信息,请查看链式赋值部分。
访问pandas对象的底层数组将返回只读视图
ser = pd.Series([1, 2, 3])
ser.to_numpy()
Out[11]: array([1, 2, 3])
此示例返回一个NumPy数组,该数组是Series对象的视图。此视图可以修改,从而也可以修改pandas对象。这与写时复制规则不符。返回的数组设置为不可写,以防止此行为。如果不再关心pandas对象,可以创建此数组的副本进行修改。如果不再关心pandas对象,还可以将数组设置为可写。
有关更多详细信息,请参见只读NumPy数组部分。
一次只更新一个pandas对象
以下代码片段在没有写时复制的情况下同时更新df
和subset
:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
subset = df["foo"]
subset.iloc[0] = 100
df
Out[15]:
foo bar
0 1 4
1 2 5
2 3 6
在写时复制中,这将不再可能,因为写时复制规则明确禁止这样做。这包括将单个列作为Series
更新,并依赖于更改传播回父DataFrame
。如果需要此行为,可以使用loc
或iloc
将此语句重写为一条语句。DataFrame.where()
是此情况的另一种合适的替代方法。
使用就地方法更新从DataFrame
选择的列也将不再起作用。
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df["foo"].replace(1, 5, inplace=True)
df
Out[18]:
foo bar
0 1 4
1 2 5
2 3 6
这是链式赋值的另一种形式。通常可以用两种不同的形式重写此操作:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df.replace({"foo": {1: 5}}, inplace=True)
df
Out[21]:
foo bar
0 5 4
1 2 5
2 3 6
另一种替代方法是不使用inplace
:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df["foo"] = df["foo"].replace(1, 5)
df
Out[24]:
foo bar
0 5 4
1 2 5
2 3 6
构造函数现在默认复制NumPy数组
当未另行指定时,Series和DataFrame构造函数现在默认复制NumPy数组。这样更改是为了避免在pandas之外就地更改NumPy数组时更改pandas对象。您可以设置copy=False
以避免进行此复制。
描述
写时复制意味着以任何方式从另一个DataFrame或Series派生的任何DataFrame或Series始终表现为副本。因此,我们只能通过修改对象本身来更改对象的值。写时复制不允许就地更新与另一个DataFrame或Series对象共享数据的DataFrame或Series。
这样可以避免在修改值时产生副作用,因此,大多数方法可以避免实际复制数据,并且仅在必要时触发复制。
以下示例将在写时复制中就地操作:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df.iloc[0, 0] = 100
df
Out[27]:
foo bar
0 100 4
1 2 5
2 3 6
对象df
不与任何其他对象共享数据,因此在更新值时不会触发复制。相反,在写时复制下,以下操作将触发数据的复制:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df2 = df.reset_index(drop=True)
df2.iloc[0, 0] = 100
df
Out[31]:
foo bar
0 1 4
1 2 5
2 3 6
df2
Out[32]:
foo bar
0 100 4
1 2 5
2 3 6
reset_index
在写时复制下返回一个惰性复制,而在不使用写时复制时复制数据。由于df
和df2
两个对象共享相同的数据,因此在修改df2
时会触发复制。对象df
仍然具有最初的值,而df2
已被修改。
如果在执行reset_index
操作后不再需要对象df
,则可以通过将reset_index
的输出分配给同一变量来模拟类似就地操作:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df = df.reset_index(drop=True)
df.iloc[0, 0] = 100
df
Out[36]:
foo bar
0 100 4
1 2 5
2 3 6
当reset_index
的结果被重新分配时,初始对象会超出范围,因此df
不与任何其他对象共享数据。在修改对象时不需要复制。对于写时复制优化中列出的所有方法来说,这通常是正确的。
以前,在操作视图时,会修改视图和父对象:
with pd.option_context("mode.copy_on_write", False):
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
view = df[:]
df.iloc[0, 0] = 100
df
Out[38]:
foo bar
0 100 4
1 2 5
2 3 6
view
Out[39]:
foo bar
0 100 4
1 2 5
2 3 6
写时复制在更改df
时会触发复制,以避免同时更改view
:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
view = df[:]
df.iloc[0, 0] = 100
df
Out[43]:
foo bar
0 100 4
1 2 5
2 3 6
view
Out[44]:
foo bar
0 1 4
1 2 5
2 3 6
链式赋值
链式赋值是指通过两个连续的索引操作来更新对象的技术,例如
with pd.option_context("mode.copy_on_write", False):
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df["foo"][df["bar"] > 5] = 100
df
当bar
列大于5时,更新foo
列。然而,这违反了写时复制的原则,因为它必须在一步中修改视图df["foo"]
和df
。因此,启用写时复制后,链式赋值将始终不起作用,并引发ChainedAssignmentError
警告:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df["foo"][df["bar"] > 5] = 100
使用写时复制,可以使用loc
来完成此操作。
df.loc[df["bar"] > 5, "foo"] = 100
只读NumPy数组
如果数组与初始DataFrame共享数据,则访问DataFrame的底层NumPy数组将返回只读数组:
如果初始DataFrame由多个数组组成,则数组是副本:
df = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})
df.to_numpy()
Out[50]:
array([[1. , 1.5],
[2. , 2.5]])
如果DataFrame只包含一个NumPy数组,则数组与DataFrame共享数据:
df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
df.to_numpy()
Out[52]:
array([[1, 3],
[2, 4]])
此数组是只读的,这意味着它无法就地修改:
arr = df.to_numpy()
arr[0, 0] = 100
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[54], line 1
----> 1 arr[0, 0] = 100
ValueError: assignment destination is read-only
对于 Series 来说也是一样的,因为 Series 总是由一个单独的数组组成。
有两种潜在的解决方案:
- 如果你想避免更新与数组共享内存的 DataFrame,可以手动触发复制。
- 将数组设置为可写。这是一种更高效的解决方案,但会绕过写时复制规则,因此应谨慎使用。
```python
arr = df.to_numpy()
arr.flags.writeable = True
arr[0, 0] = 100
arr
Out[58]:
array([[100, 3],
[ 2, 4]])
需要避免的模式
如果两个对象在您修改一个对象时共享相同的数据,则不会执行防御性复制。
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
df2 = df.reset_index(drop=True)
df2.iloc[0, 0] = 100
这将创建两个共享数据的对象,因此 setitem 操作将触发复制。如果不再需要初始对象 df
,则不需要这样做。只需将其重新赋值给同一变量即可使持有对象的引用失效。
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
df = df.reset_index(drop=True)
df.iloc[0, 0] = 100
在这个例子中不需要复制。创建多个引用会保持不必要的引用,并因此会对写时复制的性能产生影响。
写时复制优化
引入了一种新的延迟复制机制,直到修改相关对象时才进行复制,且仅在该对象与另一个对象共享数据时才进行复制。这个机制被添加到不需要复制底层数据的方法中。常见的例子是 DataFrame.drop()
的 axis=1
和 DataFrame.rename()
。
当启用写时复制时,这些方法返回视图,与常规执行相比,提供了显著的性能提升。
如何启用写时复制
可以通过配置选项 copy_on_write
来启用写时复制。可以通过以下任一方式在全局范围内启用该选项:
pd.set_option("mode.copy_on_write", True)
pd.options.mode.copy_on_write = True
Pandas 2 使用指南导读
Pandas 2 使用指南:4、IO工具(文本、CSV、HDF5等)
Pandas 2 使用指南:8、写时复制(Copy-on-Write,CoW)
Pandas 2 使用指南:10、重塑和透视表ReShapingand Pivot Tables
Pandas 2 使用指南:11、处理文本数据 Working with text data
Pandas 2 使用指南:12、处理缺失数据Working with missing data
Pandas 2 使用指南: 13、重复标签 Duplicate Labels
Pandas 2 使用指南:14、分类数据 Categorical data
Pandas 2 使用指南:15、可空整数数据类型、可空布尔数据类型
Pandas 2 使用指南:18、Groupby:拆分-应用-合并 split-apply-combine
Pandas 2 使用指南:19、窗口操作 Windowing operations
Pandas 2 使用指南:20、时间序列/日期功能
Pandas 2 使用指南:21、时间差 Timedelta
Pandas 2 使用指南:23、提升性能 Enhancing performance