Pandas 2 使用指南:写时复制(Copy-on-Write,CoW)

本文介绍了Pandas3.0中的写时复制模式,强调了其对代码行为的影响,包括链式赋值的弃用、只读NumPy数组和一次更新一个对象的规则。同时提供了如何迁移代码和解决常见问题的指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


注意

写时复制将在 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对象

以下代码片段在没有写时复制的情况下同时更新dfsubset

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。如果需要此行为,可以使用lociloc将此语句重写为一条语句。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在写时复制下返回一个惰性复制,而在不使用写时复制时复制数据。由于dfdf2两个对象共享相同的数据,因此在修改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=1DataFrame.rename()

当启用写时复制时,这些方法返回视图,与常规执行相比,提供了显著的性能提升。

如何启用写时复制

可以通过配置选项 copy_on_write 来启用写时复制。可以通过以下任一方式在全局范围内启用该选项:

pd.set_option("mode.copy_on_write", True)

pd.options.mode.copy_on_write = True

Pandas 2 使用指南导读

Pandas 2 使用指南:1、十分钟入门Pandas

Pandas 2 使用指南:2、数据结构简介

Pandas 2 使用指南:3、基本功能

Pandas 2 使用指南:4、IO工具(文本、CSV、HDF5等)

Pandas 2 使用指南:5、PyArrow 功能介绍

Pandas 2 使用指南: 6、索引和选择数据

Pandas 2 使用指南:7、多级索引 / 高级索引

Pandas 2 使用指南:8、写时复制(Copy-on-Write,CoW)

Pandas 2 使用指南:9、合并、连接、串联和比较

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 使用指南:16、图表可视化

Pandas 2 使用指南:17、表格可视化

Pandas 2 使用指南:18、Groupby:拆分-应用-合并 split-apply-combine

Pandas 2 使用指南:19、窗口操作 Windowing operations

Pandas 2 使用指南:20、时间序列/日期功能
Pandas 2 使用指南:21、时间差 Timedelta

Pandas 2 使用指南:22、选项和设置

Pandas 2 使用指南:23、提升性能 Enhancing performance

Pandas 2 使用指南:24、大规模数据集的扩展

Pandas 2 使用指南:25、稀疏数据结构 Sparse data structures

Pandas 2 使用指南:26、常见问题解答 (FAQ)

Pandas 2 使用指南:27、Cookbook

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数智笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值