Pandas 中的 SettingWithCopyWarning:视图与副本

举报
Yuchuan 发表于 2021/12/17 18:20:15 2021/12/17
【摘要】 在本文中,您了解了 NumPy 和 Pandas 中的视图和副本以及它们的行为有何不同。您还看到了 aSettingWithCopyWarning是什么以及如何避免它指向的细微错误。 特别是,您已经了解了以下内容: NumPy 和 Pandas 中基于索引的赋值可以返回视图或副本。 视图和副本都有用,但它们有不同的行为。 必须特别注意避免在副本上设置不需要的值。 访问者在大熊猫是非常有用的对象

目录

NumPyPandas是用于数据操作的非常全面、高效和灵活的 Python 工具。这两个库的熟练用户要理解的一个重要概念是数据如何被引用为浅拷贝视图)和深拷贝(或只是副本)。Pandas 有时会发出SettingWithCopyWarning警告警告用户可能不恰当地使用视图和副本。

在本文中,您将了解:

  • NumPy 和 Pandas 中的视图副本是什么
  • 如何在 NumPy 和 Pandas 中正确处理视图和副本
  • 为什么SettingWithCopyWarning会在 Pandas 中发生
  • 如何避免SettingWithCopyWarning在 Pandas 中出现

您首先会看到关于它是什么SettingWithCopyWarning以及如何避免它的简短说明。您可能会发现这足以满足您的需求,但您还可以更深入地了解 NumPy 和 Pandas 的细节,以了解有关副本和视图的更多信息。

先决条件

要遵循本文中的示例,您需要Python 3.73.8以及库NumPyPandas。本文是为 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

本示例创建一个由变量引用的字典,其中包含: data

  • "x""y", 和"z",这将是 DataFrame 的列标签
  • 三个保存 DataFrame 数据的NumPy 数组

您使用例程创建前两个数组,numpy.arange()使用numpy.array(). 要了解有关 的更多信息arange(),请查看NumPy arange():如何使用 np.arange()

连接到变量index包含字符串 "a""b""c""d",和"e",这将是数据框的行标签。

最后,初始化包含来自和信息的DataFrame 。你可以像这样想象它:dfdataindex

mmst-pandas-vc-01

以下是 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

maskPandas 系列的一个实例,带有布尔数据和来自 的索引df

  • True表示 的dfz小于的行50
  • False指示的行df中的值z小于50

df[mask]返回与从行的数据帧df为哪个maskTrue。在这种情况下,你行acd

如果您尝试df通过提取行acd使用进行更改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失败。此图说明了整个过程:

mmst-pandas-vc-02

下面是上面代码示例中发生的事情:

  • df[mask]返回一个全新的 DataFrame(用紫色标出)。此 DataFrame 保存dfTrue来自mask(以绿色突出显示)的值相对应的数据副本。
  • df[mask]["z"] = 0z新 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. 这是这个过程的样子:

mmst-pandas-vc-03

这是图像的细分::

  • df["z"]返回一个Series对象(在紫色概述),该指向相同的数据作为列zdf,而不是它的副本。
  • df["z"][mask] = 0Series通过使用链式赋值将掩码值(以绿色突出显示)设置为零来修改此对象。
  • df也被修改,因为Series对象df["z"]df.

您已经看到df[mask]包含数据的副本,而df["z"]指向与df. Pandas 用来确定您是否制作副本的规则非常复杂。幸运的是,有一些简单的方法可以为 DataFrame 赋值并避免SettingWithCopyWarning.

由于以下原因,调用访问器通常被认为是比链式赋值更好的做法:

  1. df当您使用单一方法时,修改的意图对Pandas 来说更加清晰。
  2. 代码对读者来说更清晰。
  3. 访问器往往具有更好的性能,即使在大多数情况下您不会注意到这一点。

然而,有时使用访问器是不够的。他们也可能会返回副本,在这种情况下,您可以获得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,您可以使用它来创建其他数组。我们首先将arr2and 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]]返回深拷贝。理解这种差异不仅对于处理SettingWithCopyWarningNumPy 和 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。该属性.baseview_of_arrarr自己。换句话说,view_of_arr不拥有任何数据——它使用属于 的数据arr。您还可以使用属性验证这一点.flags

>>>
>>> view_of_arr.flags.owndata
False

如您所见,view_of_arr.flags.owndataFalse。这意味着它view_of_arr不拥有数据并使用它.base来获取数据:

mmst-pandas-vc-04

上图显示了arrview_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.baseNone。属性.flags.owndataTrue。这意味着copy_of_arr拥有数据:

mmst-pandas-vc-05

上图显示了arrcopy_of_arr包含数据值的不同实例。

视图和副本之间的差异

视图和副本之间有两个非常重要的区别:

  1. 视图不需要额外的数据存储,但副本需要。
  2. 修改原始数组会影响其视图,反之亦然。但是,修改原始数组不会影响其副本。

为了说明观点和副本之间的第一个区别,让我们比较的大小arrview_of_arrcopy_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

arrcopy_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])

如您所见,视图也发生了变化,但副本保持不变。代码如下图所示:

mmst-pandas-vc-06

视图被修改,因为它查看 的元素arr,并且它.base是原始数组。副本未更改,因为它不与原始数据共享数据,因此对原始数据的更改根本不会影响它。

了解 Pandas 中的视图和副本

Pandas 还区分了视图和副本。您可以使用.copy(). 该参数deep确定您需要视图 ( deep=False) 还是副本 ( deep=True)。deepTrue默认,这样你就可以忽略它得到一个副本:

>>>
>>> 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 数组。您可以看到dfview_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

您分配零,以列的所有元素zdf。这会导致 发生变化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

dfview_of_df共享相同的行和列标签,同时copy_of_df具有单独的索引实例。请记住,你不能修改的具体要素.index.columns。它们是不可变的对象。

NumPy 和 Pandas 中的索引和切片

基本在NumPy的索引和切片类似于索引和切片列表和元组。但是,NumPy 和 Pandas 都提供了额外的选项来引用对象及其部件并为其分配值。

NumPy 数组和Pandas 对象DataFrameSeries)实现了特殊的方法,可以以类似于容器的风格引用、分配和删除值:

当您在 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. 双方ab使用arr他们的基地,并且都没有自己的数据。相反,他们查看以下数据arr

mmst-pandas-vc-07

上图中的绿色指数是通过切片获得的。两者ab查看arr绿色矩形中的相应元素。

注意:当你有一个很大的原始数组并且只需要其中的一小部分时,你可以.copy()在切片后调用并用del语句删除指向原始数组的变量。这样,您可以保留副本并从内存中删除原始数组。

尽管切片会返回一个视图,但在其他情况下,从另一个数组创建一个数组实际上会生成一个副本。

使用整数列表索引数组会返回原始数组的副本。该副本包含来自原始数组的元素,其索引存在于列表中:

>>>
>>> c = arr[[1, 3]]
>>> c
array([2, 8])
>>> c.base is None
True
>>> c.flags.owndata
True

结果数组c包含来自arr索引1和的元素3。这些元素具有值28。在这种情况下,c是 的副本arr,它.baseNone,并且它有自己的数据:

mmst-pandas-vc-08

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。在 的情况下cd是一个副本,它.baseNone,并且它有自己的数据:

mmst-pandas-vc-09

arr绿色矩形中的元素对应于 中的Truemask。这些元素被复制到新数组中d。复制后,arrd是独立的。

注意:您可以使用另一个 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])

您已将arrfrom的第二个值更改264。该值2也存在于所导出的阵列abc,和d。但是,只有视图ab被修改:

mmst-pandas-vc-10

视图ab查看 的数据arr,包括其第二个元素。这就是为什么你会看到变化。副本cd保持不变,因为它们没有与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和包含元素2and的数据8。该语句arr[1:4:2][0] = 64将这些元素中的第一个修改为64。更改在arr和 返回的视图中都可见arr[1:4:2]

在第二种情况下,arr[[1, 3]]返回一个副本,其中还包含元素28。但这些元素与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:31:4:2列时,将返回视图ab。但是,当您应用 list[1, 3]和 mask 时[False, True, False, True],您将获得副本cd

.base两者的abarr本身。双方cd有无关自己的基地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]])

您更改了该值2arr100和改变从欣赏到相应的元素ab。副本cd不能以这种方式修改。

要了解有关索引 NumPy 数组的更多信息,您可以查看官方快速入门教程索引教程

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

副本有不同的.basedf

在下一节中,您将找到与索引 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 :

  1. 第一级包含标签powersrandom
  2. 第二层有标签xand y,属于powers,和z,属于random

该表达式df["powers"]将返回一个包含下面所有列的 DataFrame powers,即列xy。如果您只想获得该列x,则可以同时通过powersx。正确的方法是使用表达式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[],返回一个数据帧与所述行ab及列xy,这是下面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")问题 a SettingWithCopyWarning. 这是默认行为。
  • 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 的重要要求,尤其是在处理大数据时。现在您已经对这些概念有了扎实的掌握,您已经准备好深入了解令人兴奋的数据科学世界!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。