Pandas 中的 SettingWithCopyWarning:视图与副本
目录
NumPy和Pandas是用于数据操作的非常全面、高效和灵活的 Python 工具。这两个库的熟练用户要理解的一个重要概念是数据如何被引用为浅拷贝(视图)和深拷贝(或只是副本)。Pandas 有时会发出SettingWithCopyWarning
警告警告用户可能不恰当地使用视图和副本。
在本文中,您将了解:
- NumPy 和 Pandas 中的视图和副本是什么
- 如何在 NumPy 和 Pandas 中正确处理视图和副本
- 为什么
SettingWithCopyWarning
会在 Pandas 中发生 - 如何避免
SettingWithCopyWarning
在 Pandas 中出现
您首先会看到关于它是什么SettingWithCopyWarning
以及如何避免它的简短说明。您可能会发现这足以满足您的需求,但您还可以更深入地了解 NumPy 和 Pandas 的细节,以了解有关副本和视图的更多信息。
先决条件
要遵循本文中的示例,您需要Python 3.7或3.8以及库NumPy和Pandas。本文是为 NumPy 1.18.1 版和 Pandas 1.0.3 版编写的。您可以使用以下命令安装它们pip
:
$ python -m pip install -U "numpy==1.18.*" "pandas==1.0.*"
如果你喜欢蟒蛇或Miniconda分布,可以使用畅达的包管理系统。要了解有关此方法的更多信息,请查看在 Windows 上设置 Python 进行机器学习。现在,在您的环境中安装 NumPy 和 Pandas 就足够了:
$ conda install numpy=1.18.* pandas=1.0.*
现在您已经安装了 NumPy 和 Pandas,您可以导入它们并检查它们的版本:
>>> import numpy as np
>>> import pandas as pd
>>> np.__version__
'1.18.1'
>>> pd.__version__
'1.0.3'
而已。您已具备本文的所有先决条件。您的版本可能略有不同,但以下信息仍然适用。
注意:本文要求你有一定的 Pandas 知识。在后面的部分中,您还需要一些 NumPy 知识。
要刷新您的 NumPy 技能,您可以查看以下资源:
要提醒自己有关 Pandas 的信息,您可以阅读以下内容:
现在您已准备好开始了解视图、副本和SettingWithCopyWarning
!
一个例子 SettingWithCopyWarning
如果您使用过 Pandas,很可能您已经看到了SettingWithCopyWarning
它的实际应用。这可能很烦人,有时很难理解。然而,它的发布是有原因的。
你应该了解的第一件事SettingWithCopyWarning
是,它是不是一个错误。这是一个警告。它警告您,您可能已经做了一些会导致您的代码中出现不需要的行为的事情。
让我们看一个例子。您将首先创建一个 Pandas DataFrame:
>>> data = {"x": 2**np.arange(5),
... "y": 3**np.arange(5),
... "z": np.array([45, 98, 24, 11, 64])}
>>> index = ["a", "b", "c", "d", "e"]
>>> df = pd.DataFrame(data=data, index=index)
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
- 键
"x"
,"y"
, 和"z"
,这将是 DataFrame 的列标签 - 三个保存 DataFrame 数据的NumPy 数组
您使用例程创建前两个数组,numpy.arange()
使用numpy.array()
. 要了解有关 的更多信息arange()
,请查看NumPy arange():如何使用 np.arange()。
该表连接到变量index
包含字符串 "a"
,"b"
,"c"
,"d"
,和"e"
,这将是数据框的行标签。
最后,初始化包含来自和信息的DataFrame 。你可以像这样想象它:df
data
index
以下是 DataFrame 中包含的主要信息的细分:
- 紫色框:数据
- 蓝框:列标签
- 红框:行标签
DataFrame 存储附加信息或元数据,包括其形状、数据类型等。
现在您有了一个 DataFrame 可以使用,让我们尝试获取一个SettingWithCopyWarning
. 您将从列z
中取出所有小于 50 的值并将它们替换为零。您可以从使用Pandas 布尔运算符创建掩码或过滤器开始:
>>> mask = df["z"] < 50
>>> mask
a True
b False
c True
d True
e False
Name: z, dtype: bool
>>> df[mask]
x y z
a 1 1 45
c 4 9 24
d 8 27 11
mask
是Pandas 系列的一个实例,带有布尔数据和来自 的索引df
:
True
表示 的df
值z
小于的行50
。False
指示的行df
中的值z
是不小于50
。
df[mask]
返回与从行的数据帧df
为哪个mask
是True
。在这种情况下,你行a
,c
和d
。
如果您尝试df
通过提取行a
、c
和d
使用进行更改mask
,您将获得SettingWithCopyWarning
,df
并将保持不变:
>>> df[mask]["z"] = 0
__main__:1: 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
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
如您所见,向列分配零z
失败。此图说明了整个过程:
下面是上面代码示例中发生的事情:
df[mask]
返回一个全新的 DataFrame(用紫色标出)。此 DataFrame 保存df
与True
来自mask
(以绿色突出显示)的值相对应的数据副本。df[mask]["z"] = 0
将z
新 DataFrame的列修改为零,保持df
不变。
通常,你不想要这个!您想要修改df
而不是一些未被任何变量引用的中间数据结构。这就是 Pandas 发出 aSettingWithCopyWarning
并警告你这个可能的错误的原因。
在这种情况下,要修改的正确方法df
是应用的一个存取 .loc[]
,.iloc[]
,.at[]
,或.iat[]
:
>>> df = pd.DataFrame(data=data, index=index)
>>> df.loc[mask, "z"] = 0
>>> df
x y z
a 1 1 0
b 2 3 98
c 4 9 0
d 8 27 0
e 16 81 64
这种方法使您能够为将值分配给 DataFrame 的单个方法提供两个参数mask
和"z"
。
解决此问题的另一种方法是更改评估顺序:
>>> df = pd.DataFrame(data=data, index=index)
>>> df["z"]
a 45
b 98
c 24
d 11
e 64
Name: z, dtype: int64
>>> df["z"][mask] = 0
>>> df
x y z
a 1 1 0
b 2 3 98
c 4 9 0
d 8 27 0
e 16 81 64
这有效!你已经修改了df
. 这是这个过程的样子:
这是图像的细分::
df["z"]
返回一个Series
对象(在紫色概述),该指向相同的数据作为列z
中df
,而不是它的副本。df["z"][mask] = 0
Series
通过使用链式赋值将掩码值(以绿色突出显示)设置为零来修改此对象。df
也被修改,因为Series
对象df["z"]
与df
.
您已经看到df[mask]
包含数据的副本,而df["z"]
指向与df
. Pandas 用来确定您是否制作副本的规则非常复杂。幸运的是,有一些简单的方法可以为 DataFrame 赋值并避免SettingWithCopyWarning
.
由于以下原因,调用访问器通常被认为是比链式赋值更好的做法:
df
当您使用单一方法时,修改的意图对Pandas 来说更加清晰。- 代码对读者来说更清晰。
- 访问器往往具有更好的性能,即使在大多数情况下您不会注意到这一点。
然而,有时使用访问器是不够的。他们也可能会返回副本,在这种情况下,您可以获得SettingWithCopyWarning
:
>>> df = pd.DataFrame(data=data, index=index)
>>> df.loc[mask]["z"] = 0
__main__:1: 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
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
在此示例中,与前一个示例一样,您使用访问器.loc[]
。分配失败,因为df.loc[mask]
返回了一个新的 DataFrame,其中包含来自df
. 然后df.loc[mask]["z"] = 0
修改新的 DataFrame,而不是df
.
通常,要避免SettingWithCopyWarning
在 Pandas 中出现 a,您应该执行以下操作:
- 避免组合两个或多个索引操作(如
df["z"][mask] = 0
和 )的链式赋值df.loc[mask]["z"] = 0
。 - 仅使用一个索引操作来应用单个分配,例如
df.loc[mask, "z"] = 0
. 这可能(也可能不)涉及访问器的使用,但它们肯定非常有用并且通常更可取。
有了这些知识,您可以SettingWithCopyWarning
在大多数情况下成功地避免这种和任何不需要的行为。但是,如果您想更深入地了解 NumPy、Pandas、视图、副本以及与 相关的问题SettingWithCopyWarning
,请继续阅读本文的其余部分。
NumPy 和 Pandas 中的视图和副本
理解视图和副本是了解 NumPy 和 Pandas 如何操作数据的重要部分。它还可以帮助您避免错误和性能瓶颈。有时数据从内存的一个部分复制到另一部分,但在其他情况下,两个或多个对象可以共享相同的数据,既节省时间又节省内存。
了解 NumPy 中的视图和副本
让我们从创建一个NumPy 数组开始:
>>> arr = np.array([1, 2, 4, 8, 16, 32])
>>> arr
array([ 1, 2, 4, 8, 16, 32])
现在您有了arr
,您可以使用它来创建其他数组。我们首先将arr
( 2
and 8
)的第二个和第四个元素提取为一个新数组。做这件事有很多种方法:
>>> arr[1:4:2]
array([2, 8])
>>> arr[[1, 3]]
array([2, 8]))
如果您不熟悉数组索引,请不要担心。你会了解更多关于这些和其他的语句后面。现在,重要的是要注意这两个语句都返回array([2, 8])
。然而,它们在表面之下有不同的行为:
>>> arr[1:4:2].base
array([ 1, 2, 4, 8, 16, 32])
>>> arr[1:4:2].flags.owndata
False
>>> arr[[1, 3]].base
>>> arr[[1, 3]].flags.owndata
True
乍一看,这可能看起来很奇怪。不同之处在于arr[1:4:2]
返回浅拷贝,而arr[[1, 3]]
返回深拷贝。理解这种差异不仅对于处理SettingWithCopyWarning
NumPy 和 Pandas 的大数据至关重要,而且对于处理大数据也很重要。
在下面的部分中,您将详细了解 NumPy 和 Pandas 中的浅拷贝和深拷贝。
NumPy 中的视图
一个浅拷贝或视图是一个NumPy的阵列,无需拥有自己的数据。它查看或“查看”包含在原始数组中的数据。您可以使用以下命令创建数组视图.view()
:
>>> view_of_arr = arr.view()
>>> view_of_arr
array([ 1, 2, 4, 8, 16, 32])
>>> view_of_arr.base
array([ 1, 2, 4, 8, 16, 32])
>>> view_of_arr.base is arr
True
您已获得数组view_of_arr
,它是原始数组的视图或浅表副本arr
。该属性.base
的view_of_arr
是arr
自己。换句话说,view_of_arr
不拥有任何数据——它使用属于 的数据arr
。您还可以使用属性验证这一点.flags
:
>>> view_of_arr.flags.owndata
False
如您所见,view_of_arr.flags.owndata
是False
。这意味着它view_of_arr
不拥有数据并使用它.base
来获取数据:
上图显示了arr
和view_of_arr
指向相同的数据值。
NumPy 中的副本
一个深拷贝一个NumPy的阵列,有时也被称为只是一个副本,是一个独立的NumPy的阵列,有它自己的数据。深拷贝的数据是通过将原数组的元素拷贝到新数组中得到的。原件和副本是两个独立的实例。您可以使用以下命令创建数组的副本.copy()
:
>>> copy_of_arr = arr.copy()
>>> copy_of_arr
array([ 1, 2, 4, 8, 16, 32])
>>> copy_of_arr.base is None
True
>>> copy_of_arr.flags.owndata
True
如您所见,copy_of_arr
没有.base
. 为了更精确,的值copy_of_arr.base
是None
。属性.flags.owndata
是True
。这意味着copy_of_arr
拥有数据:
上图显示了arr
和copy_of_arr
包含数据值的不同实例。
视图和副本之间的差异
视图和副本之间有两个非常重要的区别:
- 视图不需要额外的数据存储,但副本需要。
- 修改原始数组会影响其视图,反之亦然。但是,修改原始数组不会影响其副本。
为了说明观点和副本之间的第一个区别,让我们比较的大小arr
,view_of_arr
和copy_of_arr
。该属性.nbytes
返回数组元素消耗的内存:
>>> arr.nbytes
48
>>> view_of_arr.nbytes
48
>>> copy_of_arr.nbytes
48
所有数组的内存量都相同:48 字节。每个数组查看 6 个 8 字节(64 位)的整数元素。总共 48 个字节。
但是,如果您使用sys.getsizeof()
获取直接归因于每个数组的内存量,那么您将看到不同之处:
>>> from sys import getsizeof
>>> getsizeof(arr)
144
>>> getsizeof(view_of_arr)
96
>>> getsizeof(copy_of_arr)
144
arr
和copy_of_arr
保持144字节的每个。正如您之前所见,总共 144 个字节中有 48 个字节用于数据元素。剩余的 96 个字节用于其他属性。view_of_arr
只保存那 96 个字节,因为它没有自己的数据元素。
为了说明视图和副本之间的第二个区别,您可以修改原始数组的任何元素:
>>> arr[1] = 64
>>> arr
array([ 1, 64, 4, 8, 16, 32])
>>> view_of_arr
array([ 1, 64, 4, 8, 16, 32])
>>> copy_of_arr
array([ 1, 2, 4, 8, 16, 32])
如您所见,视图也发生了变化,但副本保持不变。代码如下图所示:
视图被修改,因为它查看 的元素arr
,并且它.base
是原始数组。副本未更改,因为它不与原始数据共享数据,因此对原始数据的更改根本不会影响它。
了解 Pandas 中的视图和副本
Pandas 还区分了视图和副本。您可以使用.copy()
. 该参数deep
确定您需要视图 ( deep=False
) 还是副本 ( deep=True
)。deep
是True
默认,这样你就可以忽略它得到一个副本:
>>> df = pd.DataFrame(data=data, index=index)
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
>>> view_of_df = df.copy(deep=False)
>>> view_of_df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
>>> copy_of_df = df.copy()
>>> copy_of_df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
起初,视图和副本df
看起来是一样的。但是,如果您比较它们的 NumPy 表示,那么您可能会注意到这种细微的差异:
>>> view_of_df.to_numpy().base is df.to_numpy().base
True
>>> copy_of_df.to_numpy().base is df.to_numpy().base
False
在这里,.to_numpy()
返回保存 DataFrame 数据的 NumPy 数组。您可以看到df
并view_of_df
拥有.base
相同的数据并共享相同的数据。另一方面,copy_of_df
包含不同的数据。
您可以通过修改来验证这一点df
:
>>> df["z"] = 0
>>> df
x y z
a 1 1 0
b 2 3 0
c 4 9 0
d 8 27 0
e 16 81 0
>>> view_of_df
x y z
a 1 1 0
b 2 3 0
c 4 9 0
d 8 27 0
e 16 81 0
>>> copy_of_df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
您分配零,以列的所有元素z
在df
。这会导致 发生变化view_of_df
,但copy_of_df
保持不变。
行和列标签也表现出相同的行为:
>>> view_of_df.index is df.index
True
>>> view_of_df.columns is df.columns
True
>>> copy_of_df.index is df.index
False
>>> copy_of_df.columns is df.columns
False
df
并view_of_df
共享相同的行和列标签,同时copy_of_df
具有单独的索引实例。请记住,你不能修改的具体要素.index
和.columns
。它们是不可变的对象。
NumPy 和 Pandas 中的索引和切片
基本在NumPy的索引和切片类似于索引和切片的列表和元组。但是,NumPy 和 Pandas 都提供了额外的选项来引用对象及其部件并为其分配值。
NumPy 数组和Pandas 对象(DataFrame
和Series
)实现了特殊的方法,可以以类似于容器的风格引用、分配和删除值:
.__getitem__()
参考值。.__setitem__()
赋值。.__delitem__()
删除值。
当您在 Python 容器类对象中引用、分配或删除数据时,您通常会调用这些方法:
var = obj[key]
相当于var = obj.__getitem__(key)
。obj[key] = value
相当于obj.__setitem__(key, value)
。del obj[key]
相当于obj.__delitem__(key)
。
参数key
表示索引,可以是整数、切片、元组、列表、NumPy 数组等。
NumPy 中的索引:副本和视图
NumPy 在索引数组时有一套严格的与副本和视图相关的规则。您是否获得原始数据的视图或副本取决于您用于索引数组的方法:切片、整数索引或布尔索引。
一维数组
切片是 Python 中众所周知的操作,用于从数组、列表或元组中获取特定数据。当您对 NumPy 数组进行切片时,您将获得该数组的视图:
>>> arr = np.array([1, 2, 4, 8, 16, 32])
>>> a = arr[1:3]
>>> a
array([2, 4])
>>> a.base
array([ 1, 2, 4, 8, 16, 32])
>>> a.base is arr
True
>>> a.flags.owndata
False
>>> b = arr[1:4:2]
>>> b
array([2, 8])
>>> b.base
array([ 1, 2, 4, 8, 16, 32])
>>> b.base is arr
True
>>> b.flags.owndata
False
您已经创建了原始数组arr
并将其切片以获得两个较小的数组,a
并且b
. 双方a
并b
使用arr
他们的基地,并且都没有自己的数据。相反,他们查看以下数据arr
:
上图中的绿色指数是通过切片获得的。两者a
并b
查看arr
绿色矩形中的相应元素。
注意:当你有一个很大的原始数组并且只需要其中的一小部分时,你可以.copy()
在切片后调用并用del
语句删除指向原始数组的变量。这样,您可以保留副本并从内存中删除原始数组。
尽管切片会返回一个视图,但在其他情况下,从另一个数组创建一个数组实际上会生成一个副本。
使用整数列表索引数组会返回原始数组的副本。该副本包含来自原始数组的元素,其索引存在于列表中:
>>> c = arr[[1, 3]]
>>> c
array([2, 8])
>>> c.base is None
True
>>> c.flags.owndata
True
结果数组c
包含来自arr
索引1
和的元素3
。这些元素具有值2
和8
。在这种情况下,c
是 的副本arr
,它.base
是None
,并且它有自己的数据:
arr
具有所选索引1
和的元素3
被复制到新数组中c
。之后,完成了复制,arr
并且c
是独立的。
您还可以使用掩码数组或列表索引 NumPy 数组。掩码是与原始形状相同的布尔数组或列表。您将获得原始数组的副本,其中仅包含True
与掩码值对应的元素:
>>> mask = [False, True, False, True, False, False]
>>> d = arr[mask]
>>> d
array([2, 8])
>>> d.base is None
True
>>> d.flags.owndata
True
该列表在第二个和第四个位置mask
具有True
值。这就是为什么数组d
只包含 的第二个和第四个位置的元素arr
。在 的情况下c
,d
是一个副本,它.base
是None
,并且它有自己的数据:
arr
绿色矩形中的元素对应于 中的True
值mask
。这些元素被复制到新数组中d
。复制后,arr
又d
是独立的。
注意:您可以使用另一个 NumPy 整数数组代替列表,但不能使用 tuple。
回顾一下,以下是您迄今为止创建的引用变量arr
:
# `arr` is the original array:
arr = np.array([1, 2, 4, 8, 16, 32])
# `a` and `b` are views created through slicing:
a = arr[1:3]
b = arr[1:4:2]
# `c` and `d` are copies created through integer and Boolean indexing:
c = arr[[1, 3]]
d = arr[[False, True, False, True, False, False]]
请记住,这些示例展示了如何引用数组中的数据。使用索引和掩码数组时,在切片数组和副本时引用数据会返回视图。另一方面,赋值总是修改数组的原始数据。
现在你有了所有这些数组,让我们看看当你改变原始数组时会发生什么:
>>> arr[1] = 64
>>> arr
array([ 1, 64, 4, 8, 16, 32])
>>> a
array([64, 4])
>>> b
array([64, 8])
>>> c
array([2, 8])
>>> d
array([2, 8])
您已将arr
from的第二个值更改2
为64
。该值2
也存在于所导出的阵列a
,b
,c
,和d
。但是,只有视图a
和b
被修改:
视图a
和b
查看 的数据arr
,包括其第二个元素。这就是为什么你会看到变化。副本c
和d
保持不变,因为它们没有与arr
. 它们独立于arr
.
NumPy 中的链式索引
这是否与行为a
以及b
类似于早期的熊猫例子看看呢?它可能,因为链式索引的概念也适用于 NumPy:
>>> arr = np.array([1, 2, 4, 8, 16, 32])
>>> arr[1:4:2][0] = 64
>>> arr
array([ 1, 64, 4, 8, 16, 32])
>>> arr = np.array([1, 2, 4, 8, 16, 32])
>>> arr[[1, 3]][0] = 64
>>> arr
array([ 1, 2, 4, 8, 16, 32])
此示例说明了在 NumPy 中使用链式索引时副本和视图之间的区别。
在第一种情况下,arr[1:4:2]
返回一个视图,该视图引用arr
和包含元素2
and的数据8
。该语句arr[1:4:2][0] = 64
将这些元素中的第一个修改为64
。更改在arr
和 返回的视图中都可见arr[1:4:2]
。
在第二种情况下,arr[[1, 3]]
返回一个副本,其中还包含元素2
和8
。但这些元素与arr
. 他们是新来的。arr[[1, 3]][0] = 64
修改由返回的副本arr[[1, 3]]
并arr
保持不变。
这本质上与SettingWithCopyWarning
在 Pandas中产生 a 的行为相同,但在 NumPy 中不存在该警告。
多维数组
引用多维数组遵循相同的原则:
- 切片数组返回视图。
- 使用索引和掩码数组返回副本。
将索引和掩码数组与切片组合也是可能的。在这种情况下,您会得到副本。
这里有一些例子:
>>> arr = np.array([[ 1, 2, 4, 8],
... [ 16, 32, 64, 128],
... [256, 512, 1024, 2048]])
>>> arr
array([[ 1, 2, 4, 8],
[ 16, 32, 64, 128],
[ 256, 512, 1024, 2048]])
>>> a = arr[:, 1:3] # Take columns 1 and 2
>>> a
array([[ 2, 4],
[ 32, 64],
[ 512, 1024]])
>>> a.base
array([[ 1, 2, 4, 8],
[ 16, 32, 64, 128],
[ 256, 512, 1024, 2048]])
>>> a.base is arr
True
>>> b = arr[:, 1:4:2] # Take columns 1 and 3
>>> b
array([[ 2, 8],
[ 32, 128],
[ 512, 2048]])
>>> b.base
array([[ 1, 2, 4, 8],
[ 16, 32, 64, 128],
[ 256, 512, 1024, 2048]])
>>> b.base is arr
True
>>> c = arr[:, [1, 3]] # Take columns 1 and 3
>>> c
array([[ 2, 8],
[ 32, 128],
[ 512, 2048]])
>>> c.base
array([[ 2, 32, 512],
[ 8, 128, 2048]])
>>> c.base is arr
False
>>> d = arr[:, [False, True, False, True]] # Take columns 1 and 3
>>> d
array([[ 2, 8],
[ 32, 128],
[ 512, 2048]])
>>> d.base
array([[ 2, 32, 512],
[ 8, 128, 2048]])
>>> d.base is arr
False
在本例中,您从二维数组开始arr
。您为行应用切片。使用:
等效于的冒号语法 ( )slice(None)
意味着您要获取所有行。
当您处理切片1:3
和1:4:2
列时,将返回视图a
和b
。但是,当您应用 list[1, 3]
和 mask 时[False, True, False, True]
,您将获得副本c
和d
。
在.base
两者的a
和b
是arr
本身。双方c
并d
有无关自己的基地arr
。
与一维数组一样,当您修改原始数组时,视图会发生变化,因为它们看到相同的数据,但副本保持不变:
>>> arr[0, 1] = 100
>>> arr
array([[ 1, 100, 4, 8],
[ 16, 32, 64, 128],
[ 256, 512, 1024, 2048]])
>>> a
array([[ 100, 4],
[ 32, 64],
[ 512, 1024]])
>>> b
array([[ 100, 8],
[ 32, 128],
[ 512, 2048]])
>>> c
array([[ 2, 8],
[ 32, 128],
[ 512, 2048]])
>>> d
array([[ 2, 8],
[ 32, 128],
[ 512, 2048]])
您更改了该值2
在arr
以100
和改变从欣赏到相应的元素a
和b
。副本c
和d
不能以这种方式修改。
Pandas 中的索引:副本和视图
您已经了解了如何在 NumPy 中使用不同的索引选项来引用实际数据(视图或浅拷贝)或新复制的数据(深拷贝或仅复制)。NumPy 对此有一套严格的规则。
Pandas 严重依赖 NumPy 数组,但提供了额外的功能和灵活性。因此,返回视图和副本的规则更加复杂且不那么直接。它们取决于数据的布局、数据类型和其他细节。事实上,Pandas 通常不保证是否会引用视图或副本。
注意: Pandas 中的索引是一个非常广泛的话题。正确使用Pandas 数据结构至关重要。您可以使用多种技术:
- 类似字典的符号
- 类属性(点)表示法
- 访问器
.loc[]
,.iloc[]
,.at[]
, 和.iat
有关更多信息,请查看官方文档和Pandas DataFrame:使使用数据令人愉快。
在本节中,您将看到两个示例,说明 Pandas 的行为与 NumPy 的相似之处。首先,您可以看到使用df
切片访问前三行返回一个视图:
>>> df = pd.DataFrame(data=data, index=index)
>>> df["a":"c"]
x y z
a 1 1 45
b 2 3 98
c 4 9 24
>>> df["a":"c"].to_numpy().base
array([[ 1, 2, 4, 8, 16],
[ 1, 3, 9, 27, 81],
[45, 98, 24, 11, 64]])
>>> df["a":"c"].to_numpy().base is df.to_numpy().base
True
此视图查看与 相同的数据df
。
另一方面,访问df
带有标签列表的前两列会返回一个副本:
>>> df = pd.DataFrame(data=data, index=index)
>>> df[["x", "y"]]
x y
a 1 1
b 2 3
c 4 9
d 8 27
e 16 81
>>> df[["x", "y"]].to_numpy().base
array([[ 1, 2, 4, 8, 16],
[ 1, 3, 9, 27, 81]])
>>> df[["x", "y"]].to_numpy().base is df.to_numpy().base
False
副本有不同的.base
比df
。
在下一节中,您将找到与索引 DataFrame 以及返回视图和副本相关的更多详细信息。在某些情况下,您会看到 Pandas 的行为变得更加复杂并且与 NumPy 不同。
在 Pandas 中使用视图和副本
正如您已经了解到的,SettingWithCopyWarning
当您尝试修改数据副本而不是原始数据时,Pandas 会发出 a 。这通常遵循链式索引。
在本节中,您将看到一些产生SettingWithCopyWarning
. 您将确定原因并了解如何通过正确使用视图、副本和访问器来避免它们。
链式索引和 SettingWithCopyWarning
您已经SettingWithCopyWarning
在第一个示例中看到了如何使用链式索引。让我们详细说明一下。
您已经创建了 DataFrame 和Series
对应于的掩码对象df["z"] < 50
:
>>> df = pd.DataFrame(data=data, index=index)
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
>>> mask = df["z"] < 50
>>> mask
a True
b False
c True
d True
e False
Name: z, dtype: bool
你已经知道任务df[mask]["z"] = 0
失败了。在这种情况下,您会得到一个SettingWithCopyWarning
:
>>> df[mask]["z"] = 0
__main__:1: 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
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
分配失败,因为df[mask]
返回了一个副本。更准确地说,分配是在副本上进行的,df
不受影响。
您还看到,在 Pandas 中,评估顺序很重要。在某些情况下,您可以切换操作顺序以使代码工作:
>>> df["z"][mask] = 0
>>> df
x y z
a 1 1 0
b 2 3 98
c 4 9 0
d 8 27 0
e 16 81 64
df["z"][mask] = 0
成功,您将在df
没有SettingWithCopyWarning
.
建议使用访问器,但您也可能会遇到问题:
>>> df = pd.DataFrame(data=data, index=index)
>>> df.loc[mask]["z"] = 0
__main__:1: 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
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
在这种情况下,df.loc[mask]
返回一个副本,分配失败,Pandas 正确发出警告。
在某些情况下,Pandas 无法检测到问题,并且副本上的分配通过而没有SettingWithCopyWarning
:
>>> df = pd.DataFrame(data=data, index=index)
>>> df.loc[["a", "c", "e"]]["z"] = 0 # Assignment fails, no warning
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
在这里,您不会收到 aSettingWithCopyWarning
并且df
不会更改,因为df.loc[["a", "c", "e"]]
使用索引列表并返回副本,而不是视图。
在某些情况下代码可以工作,但 Pandas 无论如何都会发出警告:
>>> df = pd.DataFrame(data=data, index=index)
>>> df[:3]["z"] = 0 # Assignment succeeds, with warning
__main__:1: 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
>>> df
x y z
a 1 1 0
b 2 3 0
c 4 9 0
d 8 27 11
e 16 81 64
>>> df = pd.DataFrame(data=data, index=index)
>>> df.loc["a":"c"]["z"] = 0 # Assignment succeeds, with warning
__main__:1: 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
>>> df
x y z
a 1 1 0
b 2 3 0
c 4 9 0
d 8 27 11
e 16 81 64
在这两种情况下,您选择带有切片的前三行并获得视图。分配在视图和 上均成功df
。但是您仍然会收到一个SettingWithCopyWarning
.
执行此类操作的推荐方法是避免链接索引。访问器可以提供很大帮助:
>>> df = pd.DataFrame(data=data, index=index)
>>> df.loc[mask, "z"] = 0
>>> df
x y z
a 1 1 0
b 2 3 98
c 4 9 0
d 8 27 0
e 16 81 64
这种方法使用一个方法调用,没有链式索引,代码和你的意图都更清晰。作为奖励,这是一种更有效的数据分配方式。
数据类型对视图、副本和 SettingWithCopyWarning
在 Pandas 中,创建视图和创建副本的区别还取决于使用的数据类型。在决定是返回视图还是副本时,Pandas 处理具有单一数据类型的 DataFrame 与具有多种类型的 DataFrame 不同。
让我们关注本示例中的数据类型:
>>> df = pd.DataFrame(data=data, index=index)
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
>>> df.dtypes
x int64
y int64
z int64
dtype: object
您已经创建了包含所有整数列的 DataFrame。所有三列都具有相同的数据类型这一事实在这里很重要!在这种情况下,您可以选择带有切片的行并获取视图:
>>> df["b":"d"]["z"] = 0
__main__:1: 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
>>> df
x y z
a 1 1 45
b 2 3 0
c 4 9 0
d 8 27 0
e 16 81 64
这反映了您目前在文章中看到的行为。df["b":"d"]
返回一个视图并允许您修改原始数据。这就是任务df["b":"d"]["z"] = 0
成功的原因。请注意,在这种情况下,SettingWithCopyWarning
无论成功更改为 ,您都会得到df
。
如果您的 DataFrame 包含不同类型的列,那么您可能会得到一个副本而不是视图,在这种情况下,相同的分配将失败:
>>> df = pd.DataFrame(data=data, index=index).astype(dtype={"z": float})
>>> df
x y z
a 1 1 45.0
b 2 3 98.0
c 4 9 24.0
d 8 27 11.0
e 16 81 64.0
>>> df.dtypes
x int64
y int64
z float64
dtype: object
>>> df["b":"d"]["z"] = 0
__main__:1: 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
>>> df
x y z
a 1 1 45.0
b 2 3 98.0
c 4 9 24.0
d 8 27 11.0
e 16 81 64.0
在本例中,您使用.astype()创建了一个具有两个整数列和一个浮点列的 DataFrame。与前面的示例相反,df["b":"d"]
现在返回一个副本,因此赋值df["b":"d"]["z"] = 0
失败并df
保持不变。
如果有疑问,避免混乱和使用.loc[]
,.iloc[]
,.at[]
,和.iat[]
访问方法在你的代码!
分层索引和 SettingWithCopyWarning
分层索引或MultiIndex是 Pandas 的一项功能,可让您根据层次结构在多个级别上组织行或列索引。这是一项强大的功能,可提高 Pandas 的灵活性,并支持处理二维以上的数据。
使用元组作为行或列标签创建分层索引:
>>> df = pd.DataFrame(
... data={("powers", "x"): 2**np.arange(5),
... ("powers", "y"): 3**np.arange(5),
... ("random", "z"): np.array([45, 98, 24, 11, 64])},
... index=["a", "b", "c", "d", "e"]
... )
>>> df
powers random
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
现在您拥有df
带有两级列索引的 DataFrame :
- 第一级包含标签
powers
和random
。 - 第二层有标签
x
andy
,属于powers
,和z
,属于random
。
该表达式df["powers"]
将返回一个包含下面所有列的 DataFrame powers
,即列x
和y
。如果您只想获得该列x
,则可以同时通过powers
和x
。正确的方法是使用表达式df["powers", "x"]
:
>>> df["powers"]
x y
a 1 1
b 2 3
c 4 9
d 8 27
e 16 81
>>> df["powers", "x"]
a 1
b 2
c 4
d 8
e 16
Name: (powers, x), dtype: int64
>>> df["powers", "x"] = 0
>>> df
powers random
x y z
a 0 1 45
b 0 3 98
c 0 9 24
d 0 27 11
e 0 81 64
这是在多级列索引的情况下获取和设置列的一种方法。您还可以使用带有多索引 DataFrame 的访问器来获取或修改数据:
>>> df = pd.DataFrame(
... data={("powers", "x"): 2**np.arange(5),
... ("powers", "y"): 3**np.arange(5),
... ("random", "z"): np.array([45, 98, 24, 11, 64])},
... index=["a", "b", "c", "d", "e"]
... )
>>> df.loc[["a", "b"], "powers"]
x y
a 1 1
b 2 3
上述用途的例子中.loc[]
,返回一个数据帧与所述行a
和b
及列x
和y
,这是下面powers
。您可以类似地获得特定的列(或行):
>>> df.loc[["a", "b"], ("powers", "x")]
a 1
b 2
Name: (powers, x), dtype: int64
在这个例子中,您可以指定所需的行的交集a
,并b
与柱x
,这就是下面powers
。要获得单列,您可以传递索引元组("powers", "x")
并获得一个Series
对象作为结果。
您可以使用这种方法来修改具有分层索引的 DataFrames 的元素:
>>> df.loc[["a", "b"], ("powers", "x")] = 0
>>> df
powers random
x y z
a 0 1 45
b 0 3 98
c 4 9 24
d 8 27 11
e 16 81 64
在上面的示例中,您避免使用访问器 ( df.loc[["a", "b"], ("powers", "x")]
) 和不使用访问器 ( ) 进行链式索引df["powers", "x"]
。
正如您之前看到的,链式索引可能导致SettingWithCopyWarning
:
>>> df = pd.DataFrame(
... data={("powers", "x"): 2**np.arange(5),
... ("powers", "y"): 3**np.arange(5),
... ("random", "z"): np.array([45, 98, 24, 11, 64])},
... index=["a", "b", "c", "d", "e"]
... )
>>> df
powers random
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64
>>> df["powers"]
x y
a 1 1
b 2 3
c 4 9
d 8 27
e 16 81
>>> df["powers"]["x"] = 0
__main__:1: 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
>>> df
powers random
x y z
a 0 1 45
b 0 3 98
c 0 9 24
d 0 27 11
e 0 81 64
在这里,df["powers"]
返回一个包含列x
和的 DataFrame y
。这只是一个从 中指向数据的视图df
,所以赋值成功并被df
修改。但是 Pandas 仍然发出一个SettingWithCopyWarning
.
如果您重复相同的代码,但在 的列中使用不同的数据类型df
,那么您将获得不同的行为:
>>> df = pd.DataFrame(
... data={("powers", "x"): 2**np.arange(5),
... ("powers", "y"): 3**np.arange(5),
... ("random", "z"): np.array([45, 98, 24, 11, 64], dtype=float)},
... index=["a", "b", "c", "d", "e"]
... )
>>> df
powers random
x y z
a 1 1 45.0
b 2 3 98.0
c 4 9 24.0
d 8 27 11.0
e 16 81 64.0
>>> df["powers"]
x y
a 1 1
b 2 3
c 4 9
d 8 27
e 16 81
>>> df["powers"]["x"] = 0
__main__:1: 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
>>> df
powers random
x y z
a 1 1 45.0
b 2 3 98.0
c 4 9 24.0
d 8 27 11.0
e 16 81 64.0
这一次,df
有多个数据类型,因此df["powers"]
返回一个副本,df["powers"]["x"] = 0
对该副本进行更改,并df
保持不变,为您提供一个SettingWithCopyWarning
.
推荐的修改方式df
是避免链式赋值。您已经了解到访问器可能非常方便,但并不总是需要它们:
>>> df = pd.DataFrame(
... data={("powers", "x"): 2**np.arange(5),
... ("powers", "y"): 3**np.arange(5),
... ("random", "z"): np.array([45, 98, 24, 11, 64], dtype=float)},
... index=["a", "b", "c", "d", "e"]
... )
>>> df["powers", "x"] = 0
>>> df
powers random
x y z
a 0 1 45
b 0 3 98
c 0 9 24
d 0 27 11
e 0 81 64
>>> df = pd.DataFrame(
... data={("powers", "x"): 2**np.arange(5),
... ("powers", "y"): 3**np.arange(5),
... ("random", "z"): np.array([45, 98, 24, 11, 64], dtype=float)},
... index=["a", "b", "c", "d", "e"]
... )
>>> df.loc[:, ("powers", "x")] = 0
>>> df
powers random
x y z
a 0 1 45.0
b 0 3 98.0
c 0 9 24.0
d 0 27 11.0
e 0 81 64.0
在这两种情况下,您都可以获得df
没有SettingWithCopyWarning
.
更改默认SettingWithCopyWarning
行为
这SettingWithCopyWarning
是一个警告,而不是一个错误。您的代码在发布时仍会执行,即使它可能无法按预期工作。
要更改此行为,您可以mode.chained_assignment
使用pandas.set_option()
. 您可以使用以下设置:
pd.set_option("mode.chained_assignment", "raise")
提出一个SettingWithCopyException
.pd.set_option("mode.chained_assignment", "warn")
问题 aSettingWithCopyWarning
. 这是默认行为。pd.set_option("mode.chained_assignment", None)
抑制警告和错误。
例如,此代码将引发 aSettingWithCopyException
而不是发出 a SettingWithCopyWarning
:
>>> df = pd.DataFrame(
... data={("powers", "x"): 2**np.arange(5),
... ("powers", "y"): 3**np.arange(5),
... ("random", "z"): np.array([45, 98, 24, 11, 64], dtype=float)},
... index=["a", "b", "c", "d", "e"]
... )
>>> pd.set_option("mode.chained_assignment", "raise")
>>> df["powers"]["x"] = 0
除了修改默认行为外,您还可以使用get_option()
检索与以下相关的当前设置mode.chained_assignment
:
>>> pd.get_option("mode.chained_assignment")
'raise'
您"raise"
之所以会遇到这种情况,是因为您使用set_option()
. 通常,pd.get_option("mode.chained_assignment")
返回"warn"
.
尽管您可以取消它,但请记住,它SettingWithCopyWarning
对于通知您不正确的代码非常有用。
结论
在本文中,您了解了 NumPy 和 Pandas 中的视图和副本以及它们的行为有何不同。您还看到了 aSettingWithCopyWarning
是什么以及如何避免它指向的细微错误。
特别是,您已经了解了以下内容:
- NumPy 和 Pandas 中基于索引的赋值可以返回视图或副本。
- 视图和副本都有用,但它们有不同的行为。
- 必须特别注意避免在副本上设置不需要的值。
- 访问者在大熊猫是非常有用的对象进行适当分配和引用数据。
理解视图和副本是正确使用 NumPy 和 Pandas 的重要要求,尤其是在处理大数据时。现在您已经对这些概念有了扎实的掌握,您已经准备好深入了解令人兴奋的数据科学世界!
- 点赞
- 收藏
- 关注作者
评论(0)