写时复制 (CoW) # 笔记 Copy-on-Write 将成为 pandas 3.0 中的默认设置。我们建议 立即将其打开, 以便从所有改进中受益。 写入时复制在 1.5.0 版本中首次引入。从版本 2.0 开始,通过 CoW 实现的大部分优化都得到了实现和支持。从 pandas 2.1 开始支持所有可能的优化。 CoW 在 3.0 版本中将默认启用。 CoW 将导致更可预测的行为,因为不可能用一条语句更新多个对象,例如索引操作或方法不会产生副作用。此外,通过尽可能延迟复制,平均性能和内存使用率将会提高。 之前的行为# pandas 的索引行为很难理解。某些操作返回视图,而其他操作则返回副本。根据操作的结果,改变一个对象可能会意外地改变另一个对象: In [1]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [2]: subset = df["foo"] In [3]: subset.iloc[0] = 100 In [4]: df Out[4]: foo bar 0 100 4 1 2 5 2 3 6 改变subset,例如更新其值,也会更新df。确切的行为很难预测。写入时复制解决了意外修改多个对象的问题,它明确禁止这样做。启用 CoW 后,df不变: In [5]: pd.options.mode.copy_on_write = True In [6]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [7]: subset = df["foo"] In [8]: subset.iloc[0] = 100 In [9]: df Out[9]: foo bar 0 1 4 1 2 5 2 3 6 以下部分将解释这意味着什么以及它如何影响现有应用程序。 迁移到写入时复制# Copy-on-Write 将是 pandas 3.0 中默认且唯一的模式。这意味着用户需要迁移其代码以符合 CoW 规则。 pandas 中的默认模式将在某些情况下发出警告,这些情况将主动改变行为,从而改变用户预期的行为。 我们添加了另一种模式,例如 pd.options.mode.copy_on_write = "warn" 这将对每一个会改变 CoW 行为的操作发出警告。我们预计这种模式会非常嘈杂,因为在很多情况下我们预计它们不会影响用户,但也会发出警告。我们建议检查此模式并分析警告,但没有必要解决所有这些警告。以下列表中的前两项是使现有代码与 CoW 兼容需要解决的唯一情况。 以下几项描述了用户可见的更改: 链式分配永远不会起作用 loc应用作替代方案。检查 链接分配部分以获取更多详细信息。 访问 pandas 对象的底层数组将返回只读视图 In [10]: ser = pd.Series([1, 2, 3]) In [11]: ser.to_numpy() Out[11]: array([1, 2, 3]) 此示例返回一个 NumPy 数组,它是 Series 对象的视图。可以修改此视图,从而也可以修改 pandas 对象。这不符合 CoW 规则。返回的数组被设置为不可写以防止这种行为。创建该数组的副本允许修改。如果您不再关心 pandas 对象,也可以使数组再次可写。 有关更多详细信息,请参阅有关只读 NumPy 数组的部分。 一次仅更新一个 pandas 对象 以下代码片段df在subset不使用 CoW 的情况下进行更新: In [12]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [13]: subset = df["foo"] In [14]: subset.iloc[0] = 100 In [15]: df Out[15]: foo bar 0 1 4 1 2 5 2 3 6 对于 CoW,这将不再可能,因为 CoW 规则明确禁止这样做。这包括将单个列更新为 aSeries并依赖于传播回父级的更改DataFrame。如果需要此行为,可以使用loc或将此语句重写为单个语句。对于这种情况,是另一种合适的选择。ilocDataFrame.where() DataFrame使用就地方法更新从 a 中选择的列也将不再起作用。 In [16]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [17]: df["foo"].replace(1, 5, inplace=True) In [18]: df Out[18]: foo bar 0 1 4 1 2 5 2 3 6 这是链式赋值的另一种形式。这通常可以重写为两种不同的形式: In [19]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [20]: df.replace({"foo": {1: 5}}, inplace=True) In [21]: df Out[21]: foo bar 0 5 4 1 2 5 2 3 6 另一种选择是不使用inplace: In [22]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [23]: df["foo"] = df["foo"].replace(1, 5) In [24]: df Out[24]: foo bar 0 5 4 1 2 5 2 3 6 构造函数现在默认复制 NumPy 数组 如果没有另外指定,Series 和 DataFrame 构造函数现在将默认复制 NumPy 数组。对此进行更改是为了避免在 Pandas 外部更改 NumPy 数组时改变 pandas 对象。您可以设置copy=False避免此副本。 描述# CoW 意味着任何以任何方式从另一个派生的 DataFrame 或 Series 始终表现为副本。因此,我们只能通过修改对象本身来改变对象的值。 CoW 不允许更新与另一个 DataFrame 或 Series 对象共享数据的 DataFrame 或 Series。 这避免了修改值时的副作用,因此,大多数方法可以避免实际复制数据,而仅在必要时触发复制。 以下示例将在 CoW 中就地运行: In [25]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [26]: df.iloc[0, 0] = 100 In [27]: df Out[27]: foo bar 0 100 4 1 2 5 2 3 6 该对象df不与任何其他对象共享任何数据,因此更新值时不会触发任何副本。相比之下,以下操作会触发 CoW 下的数据副本: In [28]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [29]: df2 = df.reset_index(drop=True) In [30]: df2.iloc[0, 0] = 100 In [31]: df Out[31]: foo bar 0 1 4 1 2 5 2 3 6 In [32]: df2 Out[32]: foo bar 0 100 4 1 2 5 2 3 6 reset_index使用 CoW 返回惰性副本,同时复制不使用 CoW 的数据。由于两个对象df和df2共享相同的数据,因此在修改 时会触发副本df2。该对象仍然具有与修改df时最初相同的值。df2 df如果执行操作后不再需要该对象reset_index,您可以通过将 的输出分配reset_index 给同一变量来模拟类似就地的操作: In [33]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [34]: df = df.reset_index(drop=True) In [35]: df.iloc[0, 0] = 100 In [36]: df Out[36]: foo bar 0 100 4 1 2 5 2 3 6 一旦reset_index重新分配结果,初始对象就会超出范围,因此df不会与任何其他对象共享数据。修改对象时不需要复制。对于写入时复制优化中列出的所有方法来说通常都是如此。 以前,在操作视图时,视图和父对象都会被修改: In [37]: 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 ....: In [38]: df Out[38]: foo bar 0 100 4 1 2 5 2 3 6 In [39]: view Out[39]: foo bar 0 100 4 1 2 5 2 3 6 dfCoW 在更改时触发副本以避免变异view: In [40]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [41]: view = df[:] In [42]: df.iloc[0, 0] = 100 In [43]: df Out[43]: foo bar 0 100 4 1 2 5 2 3 6 In [44]: view Out[44]: foo bar 0 1 4 1 2 5 2 3 6 链式分配# 链式赋值引用了一种通过两个后续索引操作更新对象的技术,例如 In [45]: 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 ....: foo当列bar大于 5 时,该列就会被更新。但这违反了 CoW 原则,因为它必须df["foo"]一步修改视图df。因此,链式分配将始终无法工作,并ChainedAssignmentError在启用 CoW 的情况下发出警告: In [46]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]}) In [47]: df["foo"][df["bar"] > 5] = 100 通过写时复制,可以使用loc. In [48]: df.loc[df["bar"] > 5, "foo"] = 100 只读 NumPy 数组# 如果数组与初始 DataFrame 共享数据,则访问 DataFrame 的底层 NumPy 数组将返回一个只读数组: 如果初始 DataFrame 由多个数组组成,则该数组是一个副本: In [49]: df = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]}) In [50]: df.to_numpy() Out[50]: array([[1. , 1.5], [2. , 2.5]]) 如果 DataFrame 仅包含一个 NumPy 数组,则该数组与 DataFrame 共享数据: In [51]: df = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) In [52]: df.to_numpy() Out[52]: array([[1, 3], [2, 4]]) 该数组是只读的,这意味着它不能就地修改: In [53]: arr = df.to_numpy() In [54]: 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,请手动触发副本。 使数组可写。这是一种性能更高的解决方案,但规避了写入时复制规则,因此应谨慎使用。 In [55]: arr = df.to_numpy() In [56]: arr.flags.writeable = True In [57]: arr[0, 0] = 100 In [58]: arr Out[58]: array([[100, 3], [ 2, 4]]) 要避免的模式# 如果在就地修改一个对象时两个对象共享相同的数据,则不会执行防御性复制。 In [59]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) In [60]: df2 = df.reset_index(drop=True) In [61]: df2.iloc[0, 0] = 100 这将创建两个共享数据的对象,因此 setitem 操作将触发复制。如果df不再需要初始对象,则不需要这样做。简单地重新分配给同一变量将使对象所持有的引用无效。 In [62]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) In [63]: df = df.reset_index(drop=True) In [64]: df.iloc[0, 0] = 100 在此示例中不需要复制。创建多个引用会使不必要的引用保持活动状态,因此会损害写入时复制的性能。 写时复制优化# 一种新的惰性复制机制,可以推迟复制,直到修改相关对象,并且仅当该对象与另一个对象共享数据时。此机制已添加到不需要底层数据副本的方法中。流行的例子是DataFrame.drop()和axis=1 。DataFrame.rename() 这些方法在启用写入时复制时返回视图,与常规执行相比,这提供了显着的性能改进。 如何启用 CoW # 可以通过配置选项启用写入时复制copy_on_write。可以通过以下任一方式打开 __globally__ 选项: In [65]: pd.set_option("mode.copy_on_write", True) In [66]: pd.options.mode.copy_on_write = True