老齐教室

Pandas和Numpy的视图和拷贝

编译:老齐

与本文相关的图书推荐:《跟老齐学Python:数据分析》

本书主要介绍Numpy、Pandas和数据可视化的有关知识,并且用实际案例贯穿于全书始终。本书适合于数据科学、机器学习、人工智能等方向的初学者。


在Numpy和Pandas中,有两个重要概念,容易混淆,一个是浅拷贝,也称为视图,另外一个是深拷贝,或者就称为拷贝。如果操作不当,Pandas会爆出SettingWithCopyWarning的异常。

本文我将就视图和拷贝问题,结合异常进行总结。

本文的操作,是基于Python3.7及其以上版本,并且Numpy使用的是1.18版本,Pandas的版本号是1.0,其他在此之上的版本一般都能兼容。

至于Pandas和Numpy的安装方法,请参阅《跟老齐学Python:数据分析》一书,书中有详细的说明。

异常的示例

首先,来看一看刚才说的Pandas中有可能爆出的SettingWithCopyWarning异常。按照下述方式创建DataFrame对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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

DataFrame对象就好像一个二维表格一样,如下图所示,最上面一行中的x、y、z是列标签(Column labels),左侧的a/b/c/d/e是行标签(Row labels),中间的就是数据了。

mmst-pandas-vc-01

通过DataFrame对象的索引,可以很容易地实现各种操作,比如筛选出z列中所有小于50的记录,可以这样操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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 是一个有布尔值构成的Series对象,然后用它作为df的索引,就得到了按照筛选条件返回的记录。本来返回的也是一个DataFrame对象,即df[mast],但是,如果你要对这个对象进行操作,比如试图将所有的z列的值修改为0,按照一般的理解就应该是df[mask]["z"]=0,如果这样做了,就会爆出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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列数值修改为0。这是为什么?

还是用图示的方式展现一下上面的操作——虽然失败了,目的是与后面的操作进行对比:

mmst-pandas-vc-02

其实,一般情况下,你不用这么做,只需要按照下面的方式做就能够达到目的了:

1
2
3
4
5
6
7
8
9
10
>>> 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

还有别的方式,也能实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> 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

是不是感觉有点奇怪了。还是看看上面的操作流程:

mmst-pandas-vc-03

这张图和前面的图对比一下,似乎也只是下标的顺序不同罢了。是不是感觉有点复杂?还有呢,继续看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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["z"][mask] = 0,不管是不是会报异常,都要避免,因为把握不好就容易出问题。
  • 使用单个的下表,比如df.loc[mask, 'z'] = 0,这样不仅意义明确,而且简单可行。

当然,对于上面问题的理解,就涉及到下面要说的视图(浅拷贝)和拷贝(深拷贝)问题了。

视图和拷贝

理解Numpy和Pandas中的视图和拷贝,是非常有必要的。因为我们有时候需要从内存中的数据中拷贝一份,有时候则需要把数据的一部分连同原数据集同时保存。

Numpy中的视图和拷贝

创建一个Numpy数组:

1
2
3
>>> arr = np.array([1, 2, 4, 8, 16, 32])
>>> arr
array([ 1, 2, 4, 8, 16, 32])

arr再创建一个数组,但是这里采切片和以列表作为索引两种方法创建,比如:

1
2
3
4
5
>>> arr[1:4:2]
array([2, 8])

>>> arr[[1, 3]]
array([2, 8]))

如果你还不了解数组的索引,也不用担心,可以参考《跟老齐学Python:数据分析》,书里有非常详细的讲解。

然而,用上面两种方法所得到的数组,还是有差别的。

1
2
3
4
5
6
7
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]]则是深拷贝(即常说的拷贝)。这就两种操作的差异。

Numpy中的浅拷贝或者视图,意思是它本身并没有数据,看起来像它的哪些数据,其实是原始数组中的数据,或者说,与原始数据共享内存(也称为共享视图)。如果更直观地,用数组的.view()可以创建数组的视图:

1
2
3
4
5
6
7
8
9
>>> 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的一个视图,或者浅拷贝,view_of_arr的属性.base就是指原始数组arr,或者说,view_of_arr没有自己的数据,它的数据都是arr的。你可以通过属性.flags来验证:

1
2
>>> view_of_arr.flags.owndata
False

如你所见,view_of_arr.flags.owndata返回了False,这意味着它没有自己的数据,它使用的是.base返回的对象的数据。

mmst-pandas-vc-04

上图所说明的就是arrview_of_arr指向了同样的数据对象。

Numpy数组的深拷贝,简称拷贝,就是要单独再创建一个拥有自己数据的数组。相对于原数组,经过深拷贝之后所得到的数组,虽然数据内容来自原数组,但它相对于原数组是独立的。我们可以使用.copy()方法来演示这种深拷贝:

1
2
3
4
5
6
7
8
9
>>> 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属性,如果判断其该熟悉的的布尔值,跟None一样。而属性.flags.owndata的返回值是True

mmst-pandas-vc-05

图中显示,两个数组各有一套数据。

那么,视图和拷贝有什么区别呢?其实,前面的演示你已经看出来了。

  • 视图没有自己的单独存储的数据,但是拷贝有
  • 如果修改原始数组,会影响视图,但是不影响拷贝

数组的属性.nbytes能返回该数组的字节数,下面就用它比较arr, view_of_arr, copy_of_arr三个数组。

1
2
3
4
5
6
>>> arr.nbytes
48
>>> view_of_arr.nbytes
48
>>> copy_of_arr.nbytes
48

目前,它们具有相同的字节:48字节。注意,看起来好像每个数组应该是8字节(64比特),其实不然,是48字节。

然而,如果使用sys.getsizeof()函数,则能够直接得到每个数组所占用的内存空间大小,这就能看出它们的区别了:

1
2
3
4
5
6
7
8
>>> from sys import getsizeof

>>> getsizeof(arr)
144
>>> getsizeof(view_of_arr)
96
>>> getsizeof(copy_of_arr)
144

arr and copy_of_arr hold 144 bytes each. As you’ve seen previously, 48 bytes out of the 144 total are for the data elements. The remaining 96 bytes are for other attributes. view_of_arr holds only those 96 bytes because it doesn’t have its own data elements.

arrcopy_of_arr占用了144字节,注意,其中的48字节用于保存数据元素,而view_of_arr只占用了96字节,因为它自己没有数据元素,所以就少了48字节。

此外,原数组修改,对视图和拷贝的数组影响各异,请看下面操作:

1
2
3
4
5
6
7
8
9
>>> 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

Pandas中的视图和拷贝

Pandas中也有视图和拷贝,用DataFrame对象的.copy()方法,可以分别创建视图和拷贝,区别在于参数的配置,如果deep=False,则为视图,如果deep=True则为拷贝,并且这种设置是默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> 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

视图和拷贝,看起来是一样的,如果把它们分别转化为Numpy的数组,就会发现差别了:

1
2
3
4
>>> 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()返回一个数组,dfview_of_df.base属性值相同,它们共享相同的数据。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> 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

dfz列数组设置为0,view_of_df也跟着变化,但是copy_of_df中的元素没有修改。

此外,行标签和列标签也都如此:

1
2
3
4
5
6
7
8
9
>>> 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

索引和切片

Numpy中的一维数组的切片方法,与Python中的列表、元组的操作一样。但是,对Numpy数组进行切片,得到的是一个视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> 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是原始数组,ab是通过切片操作得到的数组,这两个数组与与arr共享同样的数据,它们没有自己的独立数据,如下图所示:

mmst-pandas-vc-07

当你有一个很大的原始数组,但只需要其中的一小部分时,你可以在切片后调用’ .copy() ‘,并用’ del ‘语句删除指向原始数组的变量。通过这种方式,您保留了副本,并从内存中删除了原始数组。

注意:如果原始数组很大,但是你只需要其中的一小部分时,可以先用切片得到一个小数组,然后它的.copy(),并用del删除引用原始数组的变量。通过这种方式,您保留了副本,并从内存中删除了原始数组,可以尽可能节省内存。

切片返回的是视图,但是,索引则不同了。下面演示,使用列表作为索引,得到了原始数组的拷贝。

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

数组c中包含了原始数组中索引是13的两个元素,并且它是原始数组的一个拷贝。

mmst-pandas-vc-08

拷贝之后,carr是两个相互独立的数组。下面的例子中,列表中是布尔值,还是以这个列表为下标,获得True所对应的索引的值。所返回的值,还是原数组的拷贝。

1
2
3
4
5
6
7
8
>>> mask = [False, True, False, True, False, False]
>>> d = arr[mask]
>>> d
array([2, 8])
>>> d.base is None
True
>>> d.flags.owndata
True

以上代码,可以用下图演示:

mmst-pandas-vc-09

除了可以用列表作为下标,也可以使用Numpy的数组,但是不能用元组。

1
2
3
4
5
6
7
8
9
10
# `arr` 是原始数组:
arr = np.array([1, 2, 4, 8, 16, 32])

# 切片获得了`a` 和 `b`视图:
a = arr[1:3]
b = arr[1:4:2]

# 以列表为下标得到了`c` and `d`拷贝:
c = arr[[1, 3]]
d = arr[[False, True, False, True, False, False]]

跟前面对视图和拷贝的说明一样,视图都会受到原始数组变化的影响,拷贝不会:

1
2
3
4
5
6
7
8
9
10
11
>>> 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])

如果用链式操作,比如下面的,注意比较视图和拷贝的不同结果:

1
2
3
4
5
6
7
8
9
>>> 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])

This example illustrates the difference between copies and views when using chained indexing in NumPy.

arr[1:4:2]返回了视图,它引用了arr中的数据元素28,语句arr[1:4:2][0] = 64的意思是要将索引为1的元素的值设置为64,这个操作对arr和视图都会产生作用。

arr[[1, 3]]返回了拷贝,其中也包括28两个元素,但是,它们已经不是arr中的元素了,而是两个新的。arr[[1, 3]][0] = 64就不会影响arr了。

以上以一维数组为例,说明了切片和通过索引(下标)返回的不同类型对象,前者是试图,后者是拷贝。那么,如果是多维数组会如何?与一维的情况一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
>>> 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

Pandas中的拷贝和视图与Numpy类似。但是,要注意Pandas中的这样一种操作符: .loc[], .iloc[], .at[], and .iat

还是列举几个示例,从中看看Pandas的拷贝和视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> 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

拷贝和视图的应用

在前面我们已经看到,Pandas有时候会抛出SettingWithCopyWarning异常。下面我们就看看如何避免这种现象。

还是用前面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> df = pd.DataFrame(data=data, index=index)
>>> mask = df["z"] < 50
>>> mask
a True
b False
c True
d True
e False
Name: z, dtype: bool

>>> 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没有影响。从异常信息中,可以看到修改提示。但是,你不要认为这样就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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]返回的还是一个拷贝,跟前面的意思一样。

如果这样做,就能成功:

1
2
3
4
5
6
7
8
>>> 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

有的时候Pandas可能不会针对拷贝报错,比如:

1
2
3
4
5
6
7
8
9
>>> df = pd.DataFrame(data=data, index=index)
>>> df.loc[["a", "c", "e"]]["z"] = 0 # 这么写也不能实现修改,但不抛出异常
>>> df
x y z
a 1 1 45
b 2 3 98
c 4 9 24
d 8 27 11
e 16 81 64

另外,下面的操作是成立的,也会抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
>>> df = pd.DataFrame(data=data, index=index)
>>> df[:3]["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 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 # 操作成功,但是有异常抛出
__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

通常,对于上述示例中的操作意图,使用.loc实现,并且是按照下面的方式:

1
2
3
4
5
6
7
8
9
>>> 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只是提示或者警告,不是错误,你的代码并不会因为它而中断,如果你看着它不爽,可以修改,利用下面的配置方法:

  • pd.set_option("mode.chained_assignment", "raise") 抛出 SettingWithCopyException.
  • pd.set_option("mode.chained_assignment", "warn") 抛出 SettingWithCopyWarning. 这是默认设置
  • pd.set_option("mode.chained_assignment", None) 包括警告和错误信息。

例如,用下面的方式,就可以将SettingWithCopyWarning替换为SettingWithCopyException

1
2
3
4
5
6
7
8
9
10
>>> 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()函数查看当前的警告等级。

1
2
>>> pd.get_option("mode.chained_assignment")
'raise'

结论

本文讨论了Numpy和Pandas中的视图和拷贝,并且了解了SettingWithCopyWarning异常的有关问题。

参考文献:https://realpython.com/pandas-settingwithcopywarning/

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能