pandas中的SettingWithCopyWarning和Copy-on-Write

本文仅探讨 pandas 版本 <3.0.0 的情况。

事情的起因

事情的起因是一段简单的代码:

1
2
3
4
5
6
7
8
9
import pandas as pd

df = pd.DataFrame([[1, 2, 3, 4],
[5, 6, 7, 8]],
columns=pd.MultiIndex.from_product([["foo", "bar"],
["odd", "even"]]))
subset = df.xs("odd", axis="columns", level=1)
subset["baz"] = subset["foo"] + subset["bar"]
print(subset)
1
2
3
4
5
6
7
8
9
10
11
   foo  bar  baz
0 1 3 4
1 5 7 12

/var/folders/68/cck66_g12kq8sg83lbjw6nf40000gn/T/ipykernel_31292/4030278016.py:8: 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

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
subset["baz"] = subset["foo"] + subset["bar"]

这段代码构造了一个带多层索引(MultiIndex)的 DataFrame,从中选取了一个子集,并且在该子集上新增了一列。虽然代码返回了正确的结果,但同时也发出了 SettingWithCopyWarning 警告。这是什么?该如何解决?

视图(View)与副本(Copy)

要解释 SettingWithCopyWarning 警告,首先需要了解 pandas 中视图(View)和副本(Copy)的概念。

在 pandas 中,大多数操作都是以一个原始对象(如 DataFrame 或 Series)为输入,并返回一个新的对象。这个返回的新对象有两种存在形式:

  • 视图(View):类似于 MySQL 中的视图(VIEW),它是以不同形式展示同一数据的“窗口”。它直接引用了原始对象的底层数据,共享了内存中的数据。因此,对视图的修改会直接作用于原始对象,反之亦然。
  • 副本(Copy):复制了原始对象的底层数据,在内存中独立存在。因此,对副本的修改不会影响原始对象,反之亦然。

至于某个操作会返回视图还是副本,主要取决于底层对象的内存布局。由于 pandas 无法对底层数据的内存布局做出严格保证,这一行为往往难以预测(参见官方文档)。

SettingWithCopyWarning

SettingWithCopyWarning 警告就是用来提示这种不确定性带来的潜在问题的。它相当于在提醒你:连续操作的中间返回值是副本而不是视图。这意味着你的修改不会作用于原始对象——如果你本来是想改原始对象,那结果就会和预期不符。

以文章开篇的代码为例,df.xs() 所返回的 subsetdf 的副本,因此任何对于 subset 的操作是无法作用于 df 的。虽然在这里,我并不需要改变 df,但 pandas 对此显然无从得知,还是发出了 SettingWithCopyWarning 警告。

即便 subsetdf 的视图,给 subset 增加一列的操作能否同步到 df 上也是存在疑问的。

如果不想看到 SettingWithCopyWarning 警告,有以下两个选项:

  1. 可以更改 pandas 的 mode.chained_assignment 选项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import pandas as pd

    pd.options.mode.chained_assignment = None

    df = pd.DataFrame([[1, 2, 3, 4],
    [5, 6, 7, 8]],
    columns=pd.MultiIndex.from_product([["foo", "bar"],
    ["odd", "even"]]))
    subset = df.xs("odd", axis="columns", level=1)
    subset["baz"] = subset["foo"] + subset["bar"]
    print(subset)
    1
    2
    3
      foo  bar  baz
    0 1 3 4
    1 5 7 12

    可以看到这并不影响代码的结果。

    pandas 的 mode.chained_assignment 选项可以取以下值:

    • None:既不发出警告,也不抛出错误
    • 'warn':默认值,pandas 会发出 SettingWithCopyWarning 警告
    • 'raise':pandas 会抛出 SettingWithCopyError 错误

    虽然这个选项名字里提到了“链式赋值”(下文会介绍),但它实际上控制的范围更广,并不只有链式赋值,也包括了文章开头的示例中的情况。

  2. 可以在 df.xs() 后面增加一个显式的 .copy()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import pandas as pd

    pd.options.mode.chained_assignment = None

    df = pd.DataFrame([[1, 2, 3, 4],
    [5, 6, 7, 8]],
    columns=pd.MultiIndex.from_product([["foo", "bar"],
    ["odd", "even"]]))
    subset = df.xs("odd", axis="columns", level=1).copy()
    subset["baz"] = subset["foo"] + subset["bar"]
    print(subset)
    1
    2
    3
       foo  bar  baz
    0 1 3 4
    1 5 7 12

    可以看到这并不影响代码的结果。

    新增的 .copy() 相当于告诉 pandas:我不希望 subset 成为 df 的视图,也不希望通过修改 subset 来修改 df。pandas 也因此不会发出 SettingWithCopyWarning 警告了。

    不过,这么做可能会带来不必要的性能浪费。

链式赋值(Chained Assignment)

文章开头的示例中的连续操作可能并非最常见,一种更为常见的连续操作是“链式赋值”。

所谓链式赋值,指的是通过连续索引的方式对 pandas 对象进行修改,例如:

1
2
3
4
5
6
7
import pandas as pd

df = pd.DataFrame([[1, 2],
[3, 4]],
columns=["foo", "bar"])
df["foo"][0] = 100
print(df)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   foo  bar
0 100 2
1 3 4

/var/folders/68/cck66_g12kq8sg83lbjw6nf40000gn/T/ipykernel_21717/2421035552.py:9: FutureWarning: ChainedAssignmentError: behaviour will change in pandas 3.0!
You are setting values through chained assignment. Currently this works in certain cases, but when using Copy-on-Write (which will become the default behaviour in pandas 3.0) this will never work to update the original DataFrame or Series, because the intermediate object on which we are setting values will behave as a copy.
A typical example is when you are setting values in a column of a DataFrame, like:

df["col"][row_indexer] = value

Use `df.loc[row_indexer, "col"] = values` instead, to perform the assignment in a single step and ensure this keeps updating the original `df`.

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

df["foo"][0] = 100

可以看到代码对 df 进行了修改,但也发出了 ChainAssignmentError 警告。

ChainedAssignmentError

有了前面的铺垫,就很好理解 ChainedAssignmentError 警告了。

由于 df["foo"] 返回了一个 df 的视图,对于 df["foo"] 的修改得以作用在 df 上。但是 df["foo"] 返回的是 df 的视图还是副本,取决于底层对象的内存布局,是不确定的。假如,df["foo"] 返回的是 df 的副本,那代码就根本无法产生预期的结果。ChainedAssignmentError 警告就是在提醒这种“危险”的用法。

如果不想看到 ChainedAssignmentError 警告,pandas 官方文档建议将链式赋值改成 .loc/.iloc。由于 .loc/.iloc 是单个操作,不依赖于连续操作的中间返回值是视图还是副本,使用 .loc/.iloc 不仅可以避免 ChainedAssignmentError 警告,还可以防止代码产生预料之外的结果。例如:

1
2
3
4
5
6
7
import pandas as pd

df = pd.DataFrame([[1, 2],
[3, 4]],
columns=["foo", "bar"])
df.loc[0, "foo"] = 100
print(df)
1
2
3
   foo  bar
0 100 2
1 3 4

可以看到这并不影响代码的结果。

写时复制(Copy-on-Write)

最后,我们来讲讲“写时复制(Copy-on-Write)”。这是一个默认关闭的选项(在 pandas >= 3.0.0 中将默认开启)。开启“写时复制”后,那些通过返回视图也能达成目的、其实不需要复制底层数据的操作(如 DataFrame.drop(axis=1))将启用“懒复制”机制——先是返回一个“视图”,等到返回值要被写入时,再执行底层数据的复制,将返回值转变成一个“副本”。这么做的好处有:

  1. 提升性能:原本总是进行的底层数据的复制,现在只会在必要时发生了。
  2. 减少错误:由于在写入时,返回值总是会转变成“副本”,每次写入最多只会影响一个对象,不需要担心不小心修改了多个对象了。
  3. 减少心智负担:之前各种操作的返回值可能是“视图”也有可能是“副本”。现在它们要么直接返回“副本”,要么先返回“视图”、再在写入时将其转变成“副本”——即,你所写入的永远是“副本”。

启用“写时复制”选项会对本文中的示例产生怎样的影响呢?

  1. 第一个示例由于并不依赖中间返回值来修改原始对象,可以看到这并不影响代码的结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import pandas as pd

    pd.options.mode.copy_on_write = True

    df = pd.DataFrame([[1, 2, 3, 4],
    [5, 6, 7, 8]],
    columns=pd.MultiIndex.from_product([["foo", "bar"],
    ["odd", "even"]]))
    subset = df.xs("odd", axis="columns", level=1).copy()
    subset["baz"] = subset["foo"] + subset["bar"]
    print(subset)
    1
    2
    3
       foo  bar  baz
    0 1 3 4
    1 5 7 12
  2. 第二个示例由于需要中间返回值来修改原始对象,启用“写时复制”直接改变了代码原有的行为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import pandas as pd

    pd.options.mode.copy_on_write = True

    df = pd.DataFrame([[1, 2],
    [3, 4]],
    columns=["foo", "bar"])
    df["foo"][0] = 100
    print(df)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
       foo  bar
    0 1 2
    1 3 4

    /var/folders/68/cck66_g12kq8sg83lbjw6nf40000gn/T/ipykernel_25093/1198405199.py:8: ChainedAssignmentError: A value is trying to be set on a copy of a DataFrame or Series through chained assignment.
    When using the Copy-on-Write mode, such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy.

    Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.

    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
    df["foo"][0] = 100

pandas 的 mode.copy_on_write 选项可以取以下值:

  • False:不开启“写时复制”(pandas < 3.0.0 的默认值)
  • 'warn':不开启“写时复制”,但对于每一个会因“写时复制”的启用而改变行为的操作,pandas 都会发出警告
  • True:开启“写时复制”(pandas >= 3.0.0 的默认值)

参考资料


pandas中的SettingWithCopyWarning和Copy-on-Write
https://tomzhu.site/2025/08/23/pandas中的SettingWithCopyWarning和Copy-on-Write/
作者
Tom Zhu
发布于
2025年8月23日
许可协议