Pandas 3 将强制使用写时复制来改善内存和性能

Pandas 3.0 版将默认实现写时复制 (CoW:Copy-on-Write:写时复制)。
  • 从 Pandas 2.0 版本开始,CoW开始生效但尚未完全实现。
  • Pandas 3将强制通过CoW 实现的大多数优化。

COW写时复制
顾名思义,数据修改的时候也会被复制,什么意思呢?

Pandas 支持写时复制,这是一种有助于提高内存使用率的优化技术,尤其是在处理大型数据集时。

  • 当 DataFrame 或 Series 共享与原始数据相同的数据时,它最初将为数据共享相同的内存,而不是创建副本。
  • 当原始或新 DataFrame 的数据被修改时,将为正在修改的 DataFrame 创建数据的新副本。

这将有效节省内存使用量并提高处理大型数据集时的性能。

在 Pandas 中启用 CoW
它默认未启用,所以我们需要使用copy_on_writePandas 中的配置选项来启用它。


import pandas as pd
 
# Option1
pd.options.mode.copy_on_write = True
# Option2
pd.set_option("mode.copy_on_write" : True)

您可以使用任意选项在您的环境中全局启用 CoW。

注意:CoW 将在 Pandas 3.0 中默认启用,因此请尽早习惯它。

CoW 对 Pandas 的影响
CoW 将禁止同时更新多个 pandas 对象。以下是具体操作方式。


import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
subset = df[
"A"]
subset.iloc[0] = 10
df

使用 CoW,上述代码片段将不会修改df,而只会修改数据subset。

TeX
# df
    A    B
0    1    4
1    2    5
2    3    6
 
# subset
    A
0    10
1    2
2    3

就地操作将不起作用
类似地,inplace启用 CoW 后操作将无法进行,CoW 会直接修改原始的df。

Python
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df[
"A"].replace(1, 5, inplace=True)
df
<hr>
    A    B
0    1    4
1    2    5
2    3    6

我们可以看到它df保持不变,此外,我们还会看到一个ChainedAssignmentError警告。

上述操作可以用两种不同的方式执行。

  •  一种方法是避免 inplace,
  • 另一种方法是使用 inplace 在 DataFrame 层直接修改原始 df。

# Avoid inplace
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df[
"A"] = df["A"].replace(1, 5)
df
<hr>
    A    B
0    5    4
1    2    5
2    3    6

# Using inplace at DataFrame level
df = pd.DataFrame({
"A": [1, 2, 3], "B": [4, 5, 6]})
df.replace({
"A": {2: 34}}, inplace=True)
df
<hr>
    A    B
0    1    4
1    34    5
2    3    6

链式赋值永远不会起作用

当我们在一行代码中使用多个索引操作修改 DataFrame 或 Series 时,这就是我们所说的链式分配技术。


# CoW disabled
with pd.option_context("mode.copy_on_write", False):
    df = pd.DataFrame({
"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]})
    df[
"B"][df['A'] > 2] = 10
df

上述代码片段试图更改原来列大于 2 的列。B这df意味着A列中第 2 和第 3 个索引处的值B将被修改。

由于CoW被禁用,因此允许此操作,并且原文df将被修改。

TeX
    A    B
0    1    5
1    2    6
2    3    10
3    4    10

但是,如果在 Pandas 中启用了 CoW,那么这将永远无法起作用。


# CoW enabled
df = pd.DataFrame({"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]})
df[
"B"][df["A"] > 2] = 10
df
<hr>
    A    B
0    1    5
1    2    6
2    3    7
3    4    8

相反,通过写时复制,我们可以用来.loc修改df使用多个索引条件。

# CoW enabled
df = pd.DataFrame({"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]})
df.loc[(df[
"A"] == 1) | (df["A"] > 3), "B"] = 100
df

这将修改列为1 或大于 3 的列。原始内容B将如下所示。Adf

TeX
    A    B
0    1    100
1    2    6
2    3    7
3    4    100


只读数组
当将 Series 或 DataFrame 作为 NumPy 数组访问时,如果该数组与初始 DataFrame 或 Series 共享相同的数据,则该数组将是只读的。


df = pd.DataFrame({"A": [1, 2, 3, 4], "B": ['5', '6', '7', '8']})
arr = df.to_numpy()
arr
<hr>
array([[1, '5'],
       [2, '6'],
       [3, '7'],
       [4, '8']], dtype=object)

在上面的代码片段中,arr将是一份副本,因为 df 包含两种不同类型的数组(int和str)。我们可以对 进行修改arr。


arr[1, 0] = 10
arr
<hr>
array([[1, '5'],
       [10, '6'],
       [3, '7'],
       [4, '8']], dtype=object)

看一下这个案例:

df = pd.DataFrame({"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]})
arr = df.to_numpy()
arr

DataFramedf只有一个 NumPy 数组(相同数据类型的数组),因此arr与 共享数据df。这意味着arr将是只读的,无法就地修改。

print(arr.flags.writeable)
arr[0,0] = 10
arr
<hr>
False
ValueError: assignment destination is read-only

惰性复制机制
当两个或多个 DataFrame 共享相同的数据时,不会立即创建副本。


df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df2 = df.reset_index(drop=True)

由于和共享相同的数据,因此df它们df2在内存中共享相同的引用。仅当任何 DataFrame 被修改时,才会触发复制机制。


df2.iloc[0, 0] = 10
print(df2)
print(df)
<hr>
    A  B
0  10  4
1   2  5
2   3  6
   A  B
0  1  4
1  2  5
2  3  6

但这不是必需的,如果我们不想要 initial df,我们可以简单地将其重新分配给同一个变量(df),这个过程将创建一个新的引用。这将避免写时复制过程。

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
print(
"Initial reference: ",id(df))
df = df.reset_index(drop=True)
print(
"New reference: ",id(df))
df.iloc[0, 0] = 10
print(df)
<hr>
Initial reference:  138400246865760
New reference:      138400246860336
    A  B
0  10  4
1   2  5
2   3  6

同样的优化(惰性复制机制)也被添加到不需要复制原始数据的方法中。

DataFrame.rename()


df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df.rename(columns={
"A": "X", "B": "Y"})
<hr>
    X    Y
0    1    4
1    2    5
2    3    6

当启用 CoW 时,此方法将返回原始 DataFrame,而不是创建数据的完整副本,这与常规执行不同。

DataFrame.drop() 用于axis=1

DataFrame.drop()类似地,对于for axis=1( axis='columns')也实现了同样的机制。


df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})
df.drop([
"A"], axis=1)
<hr>
    B    C
0    4    7
1    5    8
2    6    9

结论
Pandas 3.0 版将默认实现写时复制 (CoW)。

所有这些符合 CoW 的优化将在处理大型数据集时实现高效的内存和资源管理。

这将减少不可预测或不一致的行为并大大提高性能。