原文:
wesmckinney.com/book/
译者:飞龙
协议:CC BY-NC-SA 4.0
八、数据整理:连接、合并和重塑
原文:
wesmckinney.com/book/data-wrangling
译者:飞龙
协议:CC BY-NC-SA 4.0
此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。
如果您发现本书的在线版本有用,请考虑订购纸质版或无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。
在许多应用程序中,数据可能分布在许多文件或数据库中,或者以不便于分析的形式排列。本章重点介绍帮助组合、连接和重新排列数据的工具。
首先,我介绍了 pandas 中层次索引的概念,这在某些操作中被广泛使用。然后我深入研究了特定的数据操作。您可以在第十三章:数据分析示例中看到这些工具的各种应用用法。
8.1 层次索引
层次索引是 pandas 的一个重要特性,它使您能够在轴上具有多个(两个或更多)索引级别。另一种思考方式是,它为您提供了一种以较低维度形式处理较高维度数据的方法。让我们从一个简单的示例开始:创建一个 Series,其索引为列表的列表(或数组):
In [11]: data = pd.Series(np.random.uniform(size=9),
....: index=[["a", "a", "a", "b", "b", "c", "c", "d", "d"],
....: [1, 2, 3, 1, 3, 1, 2, 2, 3]])
In [12]: data
Out[12]:
a 1 0.929616
2 0.316376
3 0.183919
b 1 0.204560
3 0.567725
c 1 0.595545
2 0.964515
d 2 0.653177
3 0.748907
dtype: float64
您看到的是一个带有MultiIndex
作为索引的 Series 的美化视图。索引显示中的“间隙”表示“使用直接上面的标签”:
In [13]: data.index
Out[13]:
MultiIndex([('a', 1),
('a', 2),
('a', 3),
('b', 1),
('b', 3),
('c', 1),
('c', 2),
('d', 2),
('d', 3)],
)
对于具有层次索引的对象,可以进行所谓的部分索引,使您能够简洁地选择数据的子集:
In [14]: data["b"]
Out[14]:
1 0.204560
3 0.567725
dtype: float64
In [15]: data["b":"c"]
Out[15]:
b 1 0.204560
3 0.567725
c 1 0.595545
2 0.964515
dtype: float64
In [16]: data.loc[["b", "d"]]
Out[16]:
b 1 0.204560
3 0.567725
d 2 0.653177
3 0.748907
dtype: float64
甚至可以从“内部”级别进行选择。在这里,我从第二个索引级别选择所有具有值2
的值:
In [17]: data.loc[:, 2]
Out[17]:
a 0.316376
c 0.964515
d 0.653177
dtype: float64
层次索引在重塑数据和基于组的操作(如形成数据透视表)中发挥着重要作用。例如,您可以使用其unstack
方法将这些数据重新排列为 DataFrame:
In [18]: data.unstack()
Out[18]:
1 2 3
a 0.929616 0.316376 0.183919
b 0.204560 NaN 0.567725
c 0.595545 0.964515 NaN
d NaN 0.653177 0.748907
unstack
的逆操作是stack
:
In [19]: data.unstack().stack()
Out[19]:
a 1 0.929616
2 0.316376
3 0.183919
b 1 0.204560
3 0.567725
c 1 0.595545
2 0.964515
d 2 0.653177
3 0.748907
dtype: float64
stack
和unstack
将在重塑和透视中更详细地探讨。
对于 DataFrame,任一轴都可以具有分层索引:
In [20]: frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
....: index=[["a", "a", "b", "b"], [1, 2, 1, 2]],
....: columns=[["Ohio", "Ohio", "Colorado"],
....: ["Green", "Red", "Green"]])
In [21]: frame
Out[21]:
Ohio Colorado
Green Red Green
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
层次级别可以有名称(作为字符串或任何 Python 对象)。如果有的话,这些名称将显示在控制台输出中:
In [22]: frame.index.names = ["key1", "key2"]
In [23]: frame.columns.names = ["state", "color"]
In [24]: frame
Out[24]:
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
这些名称取代了仅用于单级索引的name
属性。
注意
请注意,索引名称"state"
和"color"
不是行标签(frame.index
值)的一部分。
您可以通过访问其nlevels
属性来查看索引具有多少级别:
In [25]: frame.index.nlevels
Out[25]: 2
通过部分列索引,您也可以类似地选择列组:
In [26]: frame["Ohio"]
Out[26]:
color Green Red
key1 key2
a 1 0 1
2 3 4
b 1 6 7
2 9 10
MultiIndex
可以单独创建,然后重复使用;具有级别名称的前述 DataFrame 中的列也可以这样创建:
pd.MultiIndex.from_arrays([["Ohio", "Ohio", "Colorado"],
["Green", "Red", "Green"]],
names=["state", "color"])
重新排序和排序级别
有时您可能需要重新排列轴上级别的顺序或按特定级别的值对数据进行排序。swaplevel
方法接受两个级别编号或名称,并返回一个级别互换的新对象(但数据本身不变):
In [27]: frame.swaplevel("key1", "key2")
Out[27]:
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
2 a 3 4 5
1 b 6 7 8
2 b 9 10 11
sort_index
默认按所有索引级别词典顺序对数据进行排序,但您可以选择通过传递level
参数仅使用单个级别或一组级别进行排序。例如:
In [28]: frame.sort_index(level=1)
Out[28]:
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
b 1 6 7 8
a 2 3 4 5
b 2 9 10 11
In [29]: frame.swaplevel(0, 1).sort_index(level=0)
Out[29]:
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
b 6 7 8
2 a 3 4 5
b 9 10 11
注意
如果索引按字典顺序排序,从最外层级别开始,那么在具有分层索引的对象上进行数据选择性能要好得多——也就是说,调用sort_index(level=0)
或sort_index()
的结果。
按级别汇总统计
DataFrame 和 Series 上的许多描述性和汇总统计信息具有level
选项,您可以在特定轴上指定要按级别聚合的级别。考虑上面的 DataFrame;我们可以按行或列的级别进行聚合,如下所示:
In [30]: frame.groupby(level="key2").sum()
Out[30]:
state Ohio Colorado
color Green Red Green
key2
1 6 8 10
2 12 14 16
In [31]: frame.groupby(level="color", axis="columns").sum()
Out[31]:
color Green Red
key1 key2
a 1 2 1
2 8 4
b 1 14 7
2 20 10
我们将在第十章:数据聚合和分组操作中更详细地讨论groupby
。
使用 DataFrame 的列进行索引
希望使用一个或多个 DataFrame 列作为行索引并不罕见;或者,您可能希望将行索引移入 DataFrame 的列中。这是一个示例 DataFrame:
In [32]: frame = pd.DataFrame({"a": range(7), "b": range(7, 0, -1),
....: "c": ["one", "one", "one", "two", "two",
....: "two", "two"],
....: "d": [0, 1, 2, 0, 1, 2, 3]})
In [33]: frame
Out[33]:
a b c d
0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
3 3 4 two 0
4 4 3 two 1
5 5 2 two 2
6 6 1 two 3
DataFrame 的set_index
函数将使用一个或多个列作为索引创建一个新的 DataFrame:
In [34]: frame2 = frame.set_index(["c", "d"])
In [35]: frame2
Out[35]:
a b
c d
one 0 0 7
1 1 6
2 2 5
two 0 3 4
1 4 3
2 5 2
3 6 1
默认情况下,列会从 DataFrame 中移除,但您可以通过向set_index
传递drop=False
来保留它们:
In [36]: frame.set_index(["c", "d"], drop=False)
Out[36]:
a b c d
c d
one 0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
two 0 3 4 two 0
1 4 3 two 1
2 5 2 two 2
3 6 1 two 3
另一方面,reset_index
的作用与set_index
相反;层次化索引级别被移动到列中:
In [37]: frame2.reset_index()
Out[37]:
c d a b
0 one 0 0 7
1 one 1 1 6
2 one 2 2 5
3 two 0 3 4
4 two 1 4 3
5 two 2 5 2
6 two 3 6 1
8.2 合并和组合数据集
pandas 对象中包含的数据可以以多种方式组合:
pandas.merge
基于一个或多个键连接 DataFrame 中的行。这将为使用 SQL 或其他关系数据库的用户提供熟悉的操作,因为它实现了数据库join操作。
pandas.concat
沿轴连接或“堆叠”对象。
combine_first
将重叠数据拼接在一起,用另一个对象中的值填充另一个对象中的缺失值。
我将逐个讨论这些并给出一些示例。它们将在本书的其余部分的示例中使用。
数据库风格的 DataFrame 连接
合并或连接操作通过使用一个或多个键链接行来合并数据集。这些操作在关系数据库(例如基于 SQL 的数据库)中尤为重要。pandas 中的pandas.merge
函数是使用这些算法在您的数据上的主要入口点。
让我们从一个简单的例子开始:
In [38]: df1 = pd.DataFrame({"key": ["b", "b", "a", "c", "a", "a", "b"],
....: "data1": pd.Series(range(7), dtype="Int64")})
In [39]: df2 = pd.DataFrame({"key": ["a", "b", "d"],
....: "data2": pd.Series(range(3), dtype="Int64")})
In [40]: df1
Out[40]:
key data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 a 5
6 b 6
In [41]: df2
Out[41]:
key data2
0 a 0
1 b 1
2 d 2
在这里,我使用 pandas 的Int64
扩展类型来表示可空整数,详细讨论请参见第 7.3 章:扩展数据类型。
这是一个多对一连接的示例;df1
中的数据有多行标记为a
和b
,而df2
中的每个值在key
列中只有一行。使用这些对象调用pandas.merge
,我们得到:
In [42]: pd.merge(df1, df2)
Out[42]:
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0
请注意,我没有指定要连接的列。如果没有指定该信息,pandas.merge
将使用重叠的列名作为键。不过,最好明确指定:
In [43]: pd.merge(df1, df2, on="key")
Out[43]:
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0
一般来说,在pandas.merge
操作中,列的输出顺序是不确定的。
如果每个对象中的列名不同,您可以分别指定它们:
In [44]: df3 = pd.DataFrame({"lkey": ["b", "b", "a", "c", "a", "a", "b"],
....: "data1": pd.Series(range(7), dtype="Int64")})
In [45]: df4 = pd.DataFrame({"rkey": ["a", "b", "d"],
....: "data2": pd.Series(range(3), dtype="Int64")})
In [46]: pd.merge(df3, df4, left_on="lkey", right_on="rkey")
Out[46]:
lkey data1 rkey data2
0 b 0 b 1
1 b 1 b 1
2 b 6 b 1
3 a 2 a 0
4 a 4 a 0
5 a 5 a 0
您可能会注意到结果中缺少"c"
和"d"
值及其相关数据。默认情况下,pandas.merge
执行的是"inner"
连接;结果中的键是交集,或者是在两个表中都找到的公共集合。其他可能的选项是"left"
、"right"
和"outer"
。外连接取键的并集,结合了应用左连接和右连接的效果:
In [47]: pd.merge(df1, df2, how="outer")
Out[47]:
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0
6 c 3 <NA>
7 d <NA> 2
In [48]: pd.merge(df3, df4, left_on="lkey", right_on="rkey", how="outer")
Out[48]:
lkey data1 rkey data2
0 b 0 b 1
1 b 1 b 1
2 b 6 b 1
3 a 2 a 0
4 a 4 a 0
5 a 5 a 0
6 c 3 NaN <NA>
7 NaN <NA> d 2
在外连接中,左侧或右侧 DataFrame 对象中与另一个 DataFrame 中的键不匹配的行将在另一个 DataFrame 的列中出现 NA 值。
请参阅表 8.1 以获取how
选项的摘要。
表 8.1:使用how
参数的不同连接类型
选项 | 行为 |
---|---|
how="inner" | 仅使用在两个表中观察到的键组合 |
how="left" | 使用在左表中找到的所有键组合 |
how="right" | 使用在右表中找到的所有键组合 |
how="outer" | 使用两个表中观察到的所有键组合 |
多对多 合并形成匹配键的笛卡尔积。以下是一个示例:
In [49]: df1 = pd.DataFrame({"key": ["b", "b", "a", "c", "a", "b"],
....: "data1": pd.Series(range(6), dtype="Int64")})
In [50]: df2 = pd.DataFrame({"key": ["a", "b", "a", "b", "d"],
....: "data2": pd.Series(range(5), dtype="Int64")})
In [51]: df1
Out[51]:
key data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 b 5
In [52]: df2
Out[52]:
key data2
0 a 0
1 b 1
2 a 2
3 b 3
4 d 4
In [53]: pd.merge(df1, df2, on="key", how="left")
Out[53]:
key data1 data2
0 b 0 1
1 b 0 3
2 b 1 1
3 b 1 3
4 a 2 0
5 a 2 2
6 c 3 <NA>
7 a 4 0
8 a 4 2
9 b 5 1
10 b 5 3
由于左侧 DataFrame 中有三行"b"
,右侧 DataFrame 中有两行"b"
,因此结果中有六行"b"
。传递给how
关键字参数的连接方法仅影响结果中出现的不同键值:
In [54]: pd.merge(df1, df2, how="inner")
Out[54]:
key data1 data2
0 b 0 1
1 b 0 3
2 b 1 1
3 b 1 3
4 b 5 1
5 b 5 3
6 a 2 0
7 a 2 2
8 a 4 0
9 a 4 2
要使用多个键进行合并,请传递列名列表:
In [55]: left = pd.DataFrame({"key1": ["foo", "foo", "bar"],
....: "key2": ["one", "two", "one"],
....: "lval": pd.Series([1, 2, 3], dtype='Int64')})
In [56]: right = pd.DataFrame({"key1": ["foo", "foo", "bar", "bar"],
....: "key2": ["one", "one", "one", "two"],
....: "rval": pd.Series([4, 5, 6, 7], dtype='Int64')})
In [57]: pd.merge(left, right, on=["key1", "key2"], how="outer")
Out[57]:
key1 key2 lval rval
0 foo one 1 4
1 foo one 1 5
2 foo two 2 <NA>
3 bar one 3 6
4 bar two <NA> 7
要确定根据合并方法的选择将出现在结果中的哪些键组合,请将多个键视为形成元组数组,用作单个连接键。
注意
当您在列上进行列连接时,传递的 DataFrame 对象的索引会被丢弃。如果需要保留索引值,可以使用reset_index
将索引附加到列中。
合并操作中要考虑的最后一个问题是处理重叠列名的方式。例如:
In [58]: pd.merge(left, right, on="key1")
Out[58]:
key1 key2_x lval key2_y rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
虽然您可以手动处理重叠(请参阅 Ch 7.2.4:重命名轴索引部分以重命名轴标签),pandas.merge
具有一个suffixes
选项,用于指定要附加到左侧和右侧 DataFrame 对象中重叠名称的字符串:
In [59]: pd.merge(left, right, on="key1", suffixes=("_left", "_right"))
Out[59]:
key1 key2_left lval key2_right rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
请参阅 pandas.merge 中的表 8.2,了解有关参数的参考。下一节将介绍使用 DataFrame 的行索引进行连接。
表 8.2:pandas.merge
函数参数
参数 | 描述 |
---|---|
left | 要在左侧合并的 DataFrame。 |
right | 要在右侧合并的 DataFrame。 |
how | 要应用的连接类型:"inner" 、"outer" 、"left" 或"right" 之一;默认为"inner" 。 |
on | 要连接的列名。必须在两个 DataFrame 对象中找到。如果未指定并且没有给出其他连接键,则将使用left 和right 中的列名的交集作为连接键。 |
left_on | 用作连接键的left DataFrame 中的列。可以是单个列名或列名列表。 |
right_on | 与right DataFrame 的left_on 类似。 |
left_index | 使用left 中的行索引作为其连接键(或键,如果是MultiIndex )。 |
right_index | 与left_index 类似。 |
sort | 按连接键按字典顺序对合并数据进行排序;默认为False 。 |
suffixes | 字符串元组值,用于在重叠的列名后追加(默认为("_x", "_y") ,例如,如果两个 DataFrame 对象中都有"data" ,则在结果中会显示为"data_x" 和"data_y" 。 |
copy | 如果为False ,则在某些特殊情况下避免将数据复制到结果数据结构中;默认情况下始终复制。 |
validate | 验证合并是否是指定类型,一对一、一对多或多对多。有关选项的完整详细信息,请参阅文档字符串。 |
| indicator
| 添加一个特殊列_merge
,指示每行的来源;值将根据每行中连接数据的来源为"left_only"
、"right_only"
或"both"
。
在索引上合并
在某些情况下,DataFrame 中的合并键会在其索引(行标签)中找到。在这种情况下,您可以传递left_index=True
或right_index=True
(或两者都传递)来指示索引应该用作合并键:
In [60]: left1 = pd.DataFrame({"key": ["a", "b", "a", "a", "b", "c"],
....: "value": pd.Series(range(6), dtype="Int64")})
In [61]: right1 = pd.DataFrame({"group_val": [3.5, 7]}, index=["a", "b"])
In [62]: left1
Out[62]:
key value
0 a 0
1 b 1
2 a 2
3 a 3
4 b 4
5 c 5
In [63]: right1
Out[63]:
group_val
a 3.5
b 7.0
In [64]: pd.merge(left1, right1, left_on="key", right_index=True)
Out[64]:
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0
注意
如果您仔细观察这里,您会发现left1
的索引值已被保留,而在上面的其他示例中,输入 DataFrame 对象的索引已被丢弃。由于right1
的索引是唯一的,这种“一对多”合并(使用默认的how="inner"
方法)可以保留与输出中的行对应的left1
的索引值。
由于默认合并方法是交集连接键,您可以使用外连接来形成它们的并集:
In [65]: pd.merge(left1, right1, left_on="key", right_index=True, how="outer")
Out[65]:
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0
5 c 5 NaN
对于具有分层索引的数据,情况会更加复杂,因为在索引上进行连接等效于多键合并:
In [66]: lefth = pd.DataFrame({"key1": ["Ohio", "Ohio", "Ohio",
....: "Nevada", "Nevada"],
....: "key2": [2000, 2001, 2002, 2001, 2002],
....: "data": pd.Series(range(5), dtype="Int64")})
In [67]: righth_index = pd.MultiIndex.from_arrays(
....: [
....: ["Nevada", "Nevada", "Ohio", "Ohio", "Ohio", "Ohio"],
....: [2001, 2000, 2000, 2000, 2001, 2002]
....: ]
....: )
In [68]: righth = pd.DataFrame({"event1": pd.Series([0, 2, 4, 6, 8, 10], dtype="I
nt64",
....: index=righth_index),
....: "event2": pd.Series([1, 3, 5, 7, 9, 11], dtype="I
nt64",
....: index=righth_index)})
In [69]: lefth
Out[69]:
key1 key2 data
0 Ohio 2000 0
1 Ohio 2001 1
2 Ohio 2002 2
3 Nevada 2001 3
4 Nevada 2002 4
In [70]: righth
Out[70]:
event1 event2
Nevada 2001 0 1
2000 2 3
Ohio 2000 4 5
2000 6 7
2001 8 9
2002 10 11
在这种情况下,您必须指示要合并的多个列作为列表(注意使用how="outer"
处理重复索引值):
In [71]: pd.merge(lefth, righth, left_on=["key1", "key2"], right_index=True)
Out[71]:
key1 key2 data event1 event2
0 Ohio 2000 0 4 5
0 Ohio 2000 0 6 7
1 Ohio 2001 1 8 9
2 Ohio 2002 2 10 11
3 Nevada 2001 3 0 1
In [72]: pd.merge(lefth, righth, left_on=["key1", "key2"],
....: right_index=True, how="outer")
Out[72]:
key1 key2 data event1 event2
0 Ohio 2000 0 4 5
0 Ohio 2000 0 6 7
1 Ohio 2001 1 8 9
2 Ohio 2002 2 10 11
3 Nevada 2001 3 0 1
4 Nevada 2002 4 <NA> <NA>
4 Nevada 2000 <NA> 2 3
使用合并的两侧的索引也是可能的:
In [73]: left2 = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]],
....: index=["a", "c", "e"],
....: columns=["Ohio", "Nevada"]).astype("Int64")
In [74]: right2 = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]],
....: index=["b", "c", "d", "e"],
....: columns=["Missouri", "Alabama"]).astype("Int64")
In [75]: left2
Out[75]:
Ohio Nevada
a 1 2
c 3 4
e 5 6
In [76]: right2
Out[76]:
Missouri Alabama
b 7 8
c 9 10
d 11 12
e 13 14
In [77]: pd.merge(left2, right2, how="outer", left_index=True, right_index=True)
Out[77]:
Ohio Nevada Missouri Alabama
a 1 2 <NA> <NA>
b <NA> <NA> 7 8
c 3 4 9 10
d <NA> <NA> 11 12
e 5 6 13 14
DataFrame 有一个join
实例方法,可以简化按索引合并。它还可以用于合并许多具有相同或类似索引但列不重叠的 DataFrame 对象。在前面的例子中,我们可以这样写:
In [78]: left2.join(right2, how="outer")
Out[78]:
Ohio Nevada Missouri Alabama
a 1 2 <NA> <NA>
b <NA> <NA> 7 8
c 3 4 9 10
d <NA> <NA> 11 12
e 5 6 13 14
与pandas.merge
相比,DataFrame 的join
方法默认在连接键上执行左连接。它还支持将传递的 DataFrame 的索引与调用 DataFrame 的某一列进行连接:
In [79]: left1.join(right1, on="key")
Out[79]:
key value group_val
0 a 0 3.5
1 b 1 7.0
2 a 2 3.5
3 a 3 3.5
4 b 4 7.0
5 c 5 NaN
您可以将此方法视为将数据“合并”到调用其join
方法的对象中。
最后,对于简单的索引对索引合并,您可以将 DataFrame 的列表传递给join
,作为使用下一节中描述的更一般的pandas.concat
函数的替代方法:
In [80]: another = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [16., 17.]],
....: index=["a", "c", "e", "f"],
....: columns=["New York", "Oregon"])
In [81]: another
Out[81]:
New York Oregon
a 7.0 8.0
c 9.0 10.0
e 11.0 12.0
f 16.0 17.0
In [82]: left2.join([right2, another])
Out[82]:
Ohio Nevada Missouri Alabama New York Oregon
a 1 2 <NA> <NA> 7.0 8.0
c 3 4 9 10 9.0 10.0
e 5 6 13 14 11.0 12.0
In [83]: left2.join([right2, another], how="outer")
Out[83]:
Ohio Nevada Missouri Alabama New York Oregon
a 1 2 <NA> <NA> 7.0 8.0
c 3 4 9 10 9.0 10.0
e 5 6 13 14 11.0 12.0
b <NA> <NA> 7 8 NaN NaN
d <NA> <NA> 11 12 NaN NaN
f <NA> <NA> <NA> <NA> 16.0 17.0
沿轴连接
另一种数据组合操作被称为连接或堆叠。NumPy 的concatenate
函数可以使用 NumPy 数组来执行此操作:
In [84]: arr = np.arange(12).reshape((3, 4))
In [85]: arr
Out[85]:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
In [86]: np.concatenate([arr, arr], axis=1)
Out[86]:
array([[ 0, 1, 2, 3, 0, 1, 2, 3],
[ 4, 5, 6, 7, 4, 5, 6, 7],
[ 8, 9, 10, 11, 8, 9, 10, 11]])
在 pandas 对象(如 Series 和 DataFrame)的上下文中,具有标记轴使您能够进一步推广数组连接。特别是,您有许多额外的考虑:
-
如果对象在其他轴上的索引不同,我们应该合并这些轴中的不同元素还是仅使用共同的值?
-
连接的数据块在结果对象中需要被识别吗?
-
“连接轴”中包含需要保留的数据吗?在许多情况下,DataFrame 中的默认整数标签在连接时最好被丢弃。
pandas 中的concat
函数提供了一种一致的方法来解决这些问题。我将给出一些示例来说明它是如何工作的。假设我们有三个没有索引重叠的 Series:
In [87]: s1 = pd.Series([0, 1], index=["a", "b"], dtype="Int64")
In [88]: s2 = pd.Series([2, 3, 4], index=["c", "d", "e"], dtype="Int64")
In [89]: s3 = pd.Series([5, 6], index=["f", "g"], dtype="Int64")
使用这些对象的列表调用pandas.concat
会将值和索引粘合在一起:
In [90]: s1
Out[90]:
a 0
b 1
dtype: Int64
In [91]: s2
Out[91]:
c 2
d 3
e 4
dtype: Int64
In [92]: s3
Out[92]:
f 5
g 6
dtype: Int64
In [93]: pd.concat([s1, s2, s3])
Out[93]:
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: Int64
默认情况下,pandas.concat
沿着axis="index"
工作,产生另一个 Series。如果传递axis="columns"
,结果将是一个 DataFrame:
In [94]: pd.concat([s1, s2, s3], axis="columns")
Out[94]:
0 1 2
a 0 <NA> <NA>
b 1 <NA> <NA>
c <NA> 2 <NA>
d <NA> 3 <NA>
e <NA> 4 <NA>
f <NA> <NA> 5
g <NA> <NA> 6
在这种情况下,另一个轴上没有重叠,您可以看到这是索引的并集("outer"
连接)。您可以通过传递join="inner"
来取交集:
In [95]: s4 = pd.concat([s1, s3])
In [96]: s4
Out[96]:
a 0
b 1
f 5
g 6
dtype: Int64
In [97]: pd.concat([s1, s4], axis="columns")
Out[97]:
0 1
a 0 0
b 1 1
f <NA> 5
g <NA> 6
In [98]: pd.concat([s1, s4], axis="columns", join="inner")
Out[98]:
0 1
a 0 0
b 1 1
在这个最后的例子中,"f"
和"g"
标签消失了,因为使用了join="inner"
选项。
一个潜在的问题是结果中无法识别连接的片段。假设您希望在连接轴上创建一个分层索引。为此,请使用keys
参数:
In [99]: result = pd.concat([s1, s1, s3], keys=["one", "two", "three"])
In [100]: result
Out[100]:
one a 0
b 1
two a 0
b 1
three f 5
g 6
dtype: Int64
In [101]: result.unstack()
Out[101]:
a b f g
one 0 1 <NA> <NA>
two 0 1 <NA> <NA>
three <NA> <NA> 5 6
在沿axis="columns"
组合 Series 的情况下,keys
变成了 DataFrame 的列标题:
In [102]: pd.concat([s1, s2, s3], axis="columns", keys=["one", "two", "three"])
Out[102]:
one two three
a 0 <NA> <NA>
b 1 <NA> <NA>
c <NA> 2 <NA>
d <NA> 3 <NA>
e <NA> 4 <NA>
f <NA> <NA> 5
g <NA> <NA> 6
相同的逻辑也适用于 DataFrame 对象:
In [103]: df1 = pd.DataFrame(np.arange(6).reshape(3, 2), index=["a", "b", "c"],
.....: columns=["one", "two"])
In [104]: df2 = pd.DataFrame(5 + np.arange(4).reshape(2, 2), index=["a", "c"],
.....: columns=["three", "four"])
In [105]: df1
Out[105]:
one two
a 0 1
b 2 3
c 4 5
In [106]: df2
Out[106]:
three four
a 5 6
c 7 8
In [107]: pd.concat([df1, df2], axis="columns", keys=["level1", "level2"])
Out[107]:
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
在这里,keys
参数用于创建一个分层索引,其中第一级可以用于标识每个连接的 DataFrame 对象。
如果您传递的是对象字典而不是列表,那么字典的键将用于keys
选项:
In [108]: pd.concat({"level1": df1, "level2": df2}, axis="columns")
Out[108]:
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
有一些额外的参数控制如何创建分层索引(参见表 8.3)。例如,我们可以使用names
参数为创建的轴级别命名:
In [109]: pd.concat([df1, df2], axis="columns", keys=["level1", "level2"],
.....: names=["upper", "lower"])
Out[109]:
upper level1 level2
lower one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
最后一个考虑因素涉及行索引不包含任何相关数据的 DataFrame:
In [110]: df1 = pd.DataFrame(np.random.standard_normal((3, 4)),
.....: columns=["a", "b", "c", "d"])
In [111]: df2 = pd.DataFrame(np.random.standard_normal((2, 3)),
.....: columns=["b", "d", "a"])
In [112]: df1
Out[112]:
a b c d
0 1.248804 0.774191 -0.319657 -0.624964
1 1.078814 0.544647 0.855588 1.343268
2 -0.267175 1.793095 -0.652929 -1.886837
In [113]: df2
Out[113]:
b d a
0 1.059626 0.644448 -0.007799
1 -0.449204 2.448963 0.667226
在这种情况下,您可以传递ignore_index=True
,这将丢弃每个 DataFrame 的索引并仅连接列中的数据,分配一个新的默认索引:
In [114]: pd.concat([df1, df2], ignore_index=True)
Out[114]:
a b c d
0 1.248804 0.774191 -0.319657 -0.624964
1 1.078814 0.544647 0.855588 1.343268
2 -0.267175 1.793095 -0.652929 -1.886837
3 -0.007799 1.059626 NaN 0.644448
4 0.667226 -0.449204 NaN 2.448963
表 8.3 描述了pandas.concat
函数的参数。
表 8.3:pandas.concat
函数参数
参数 | 描述 |
---|---|
objs | 要连接的 pandas 对象的列表或字典;这是唯一必需的参数 |
axis | 要沿着连接的轴;默认为沿着行连接(axis="index" ) |
join | 要么是"inner" 要么是"outer" (默认为"outer" );是否沿着其他轴相交(inner)或联合(outer)索引 |
keys | 与要连接的对象关联的值,形成沿着连接轴的分层索引;可以是任意值的列表或数组,元组的数组,或数组的列表(如果在levels 中传递了多级数组) |
levels | 用作分层索引级别的特定索引,如果传递了键 |
names | 如果传递了keys 和/或levels ,则为创建的分层级别命名 |
verify_integrity | 检查连接对象中的新轴是否存在重复项,如果存在则引发异常;默认情况下(False )允许重复项 |
ignore_index | 不保留沿着连接axis 的索引,而是生成一个新的range(total_length) 索引 |
组合具有重叠部分的数据
还有另一种数据组合情况,既不能表示为合并操作也不能表示为连接操作。您可能有两个具有完全或部分重叠索引的数据集。作为一个激励性的例子,考虑 NumPy 的where
函数,它执行数组导向的 if-else 表达式的等效操作:
In [115]: a = pd.Series([np.nan, 2.5, 0.0, 3.5, 4.5, np.nan],
.....: index=["f", "e", "d", "c", "b", "a"])
In [116]: b = pd.Series([0., np.nan, 2., np.nan, np.nan, 5.],
.....: index=["a", "b", "c", "d", "e", "f"])
In [117]: a
Out[117]:
f NaN
e 2.5
d 0.0
c 3.5
b 4.5
a NaN
dtype: float64
In [118]: b
Out[118]:
a 0.0
b NaN
c 2.0
d NaN
e NaN
f 5.0
dtype: float64
In [119]: np.where(pd.isna(a), b, a)
Out[119]: array([0. , 2.5, 0. , 3.5, 4.5, 5. ])
在这里,每当a
中的值为空时,将选择b
中的值,否则将选择a
中的非空值。使用numpy.where
不会检查索引标签是否对齐(甚至不需要对象具有相同的长度),因此如果要按索引对齐值,请使用 Seriescombine_first
方法:
In [120]: a.combine_first(b)
Out[120]:
a 0.0
b 4.5
c 3.5
d 0.0
e 2.5
f 5.0
dtype: float64
对于 DataFrame,combine_first
按列执行相同的操作,因此您可以将其视为使用传递的对象中的数据“修补”调用对象中的缺失数据:
In [121]: df1 = pd.DataFrame({"a": [1., np.nan, 5., np.nan],
.....: "b": [np.nan, 2., np.nan, 6.],
.....: "c": range(2, 18, 4)})
In [122]: df2 = pd.DataFrame({"a": [5., 4., np.nan, 3., 7.],
.....: "b": [np.nan, 3., 4., 6., 8.]})
In [123]: df1
Out[123]:
a b c
0 1.0 NaN 2
1 NaN 2.0 6
2 5.0 NaN 10
3 NaN 6.0 14
In [124]: df2
Out[124]:
a b
0 5.0 NaN
1 4.0 3.0
2 NaN 4.0
3 3.0 6.0
4 7.0 8.0
In [125]: df1.combine_first(df2)
Out[125]:
a b c
0 1.0 NaN 2.0
1 4.0 2.0 6.0
2 5.0 4.0 10.0
3 3.0 6.0 14.0
4 7.0 8.0 NaN
使用 DataFrame 对象的combine_first
的输出将具有所有列名称的并集。
8.3 重塑和旋转
有许多用于重新排列表格数据的基本操作。这些操作被称为重塑或旋转操作。
使用分层索引进行重塑
分层索引提供了在 DataFrame 中重新排列数据的一致方法。有两个主要操作:
stack
这将从数据中的列旋转或旋转到行。
unstack
这将从行旋转到列。
我将通过一系列示例来说明这些操作。考虑一个具有字符串数组作为行和列索引的小 DataFrame:
In [126]: data = pd.DataFrame(np.arange(6).reshape((2, 3)),
.....: index=pd.Index(["Ohio", "Colorado"], name="state"),
.....: columns=pd.Index(["one", "two", "three"],
.....: name="number"))
In [127]: data
Out[127]:
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
在这些数据上使用stack
方法将列旋转为行,生成一个 Series:
In [128]: result = data.stack()
In [129]: result
Out[129]:
state number
Ohio one 0
two 1
three 2
Colorado one 3
two 4
three 5
dtype: int64
从具有分层索引的 Series 中,您可以使用unstack
将数据重新排列回 DataFrame:
In [130]: result.unstack()
Out[130]:
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
默认情况下,最内层级别被取消堆叠(与stack
相同)。您可以通过传递级别编号或名称来取消堆叠不同的级别:
In [131]: result.unstack(level=0)
Out[131]:
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
In [132]: result.unstack(level="state")
Out[132]:
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
如果在每个子组中未找到级别中的所有值,则取消堆叠可能会引入缺失数据:
In [133]: s1 = pd.Series([0, 1, 2, 3], index=["a", "b", "c", "d"], dtype="Int64")
In [134]: s2 = pd.Series([4, 5, 6], index=["c", "d", "e"], dtype="Int64")
In [135]: data2 = pd.concat([s1, s2], keys=["one", "two"])
In [136]: data2
Out[136]:
one a 0
b 1
c 2
d 3
two c 4
d 5
e 6
dtype: Int64
堆叠默认会过滤掉缺失数据,因此该操作更容易反转:
In [137]: data2.unstack()
Out[137]:
a b c d e
one 0 1 2 3 <NA>
two <NA> <NA> 4 5 6
In [138]: data2.unstack().stack()
Out[138]:
one a 0
b 1
c 2
d 3
two c 4
d 5
e 6
dtype: Int64
In [139]: data2.unstack().stack(dropna=False)
Out[139]:
one a 0
b 1
c 2
d 3
e <NA>
two a <NA>
b <NA>
c 4
d 5
e 6
dtype: Int64
当您在 DataFrame 中取消堆叠时,取消堆叠的级别将成为结果中的最低级别:
In [140]: df = pd.DataFrame({"left": result, "right": result + 5},
.....: columns=pd.Index(["left", "right"], name="side"))
In [141]: df
Out[141]:
side left right
state number
Ohio one 0 5
two 1 6
three 2 7
Colorado one 3 8
two 4 9
three 5 10
In [142]: df.unstack(level="state")
Out[142]:
side left right
state Ohio Colorado Ohio Colorado
number
one 0 3 5 8
two 1 4 6 9
three 2 5 7 10
与unstack
一样,调用stack
时,我们可以指定要堆叠的轴的名称:
In [143]: df.unstack(level="state").stack(level="side")
Out[143]:
state Colorado Ohio
number side
one left 3 0
right 8 5
two left 4 1
right 9 6
three left 5 2
right 10 7
将“长”格式旋转为“宽”格式
在数据库和 CSV 文件中存储多个时间序列的常见方法有时被称为长或堆叠格式。在此格式中,单个值由表中的一行表示,而不是每行多个值。
让我们加载一些示例数据,并进行少量时间序列整理和其他数据清理:
In [144]: data = pd.read_csv("examples/macrodata.csv")
In [145]: data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
In [146]: data.head()
Out[146]:
year quarter realgdp infl unemp
0 1959 1 2710.349 0.00 5.8
1 1959 2 2778.801 2.34 5.1
2 1959 3 2775.488 2.74 5.3
3 1959 4 2785.204 0.27 5.6
4 1960 1 2847.699 2.31 5.2
首先,我使用pandas.PeriodIndex
(表示时间间隔而不是时间点),在 Ch 11: Time Series 中更详细地讨论,将year
和quarter
列组合起来,将索引设置为每个季度末的datetime
值:
In [147]: periods = pd.PeriodIndex(year=data.pop("year"),
.....: quarter=data.pop("quarter"),
.....: name="date")
In [148]: periods
Out[148]:
PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
'1960Q3', '1960Q4', '1961Q1', '1961Q2',
...
'2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
'2008Q4', '2009Q1', '2009Q2', '2009Q3'],
dtype='period[Q-DEC]', name='date', length=203)
In [149]: data.index = periods.to_timestamp("D")
In [150]: data.head()
Out[150]:
realgdp infl unemp
date
1959-01-01 2710.349 0.00 5.8
1959-04-01 2778.801 2.34 5.1
1959-07-01 2775.488 2.74 5.3
1959-10-01 2785.204 0.27 5.6
1960-01-01 2847.699 2.31 5.2
在这里,我在 DataFrame 上使用了pop
方法,该方法返回一个列,同时从 DataFrame 中删除它。
然后,我选择一部分列,并给columns
索引命名为"item"
:
In [151]: data = data.reindex(columns=["realgdp", "infl", "unemp"])
In [152]: data.columns.name = "item"
In [153]: data.head()
Out[153]:
item realgdp infl unemp
date
1959-01-01 2710.349 0.00 5.8
1959-04-01 2778.801 2.34 5.1
1959-07-01 2775.488 2.74 5.3
1959-10-01 2785.204 0.27 5.6
1960-01-01 2847.699 2.31 5.2
最后,我使用stack
重新塑造,使用reset_index
将新的索引级别转换为列,最后给包含数据值的列命名为"value"
:
In [154]: long_data = (data.stack()
.....: .reset_index()
.....: .rename(columns={0: "value"}))
现在,ldata
看起来像这样:
In [155]: long_data[:10]
Out[155]:
date item value
0 1959-01-01 realgdp 2710.349
1 1959-01-01 infl 0.000
2 1959-01-01 unemp 5.800
3 1959-04-01 realgdp 2778.801
4 1959-04-01 infl 2.340
5 1959-04-01 unemp 5.100
6 1959-07-01 realgdp 2775.488
7 1959-07-01 infl 2.740
8 1959-07-01 unemp 5.300
9 1959-10-01 realgdp 2785.204
在这种所谓的长格式中,每个时间序列的每一行在表中代表一个单独的观察。
数据经常以这种方式存储在关系型 SQL 数据库中,因为固定的模式(列名和数据类型)允许item
列中的不同值的数量随着数据添加到表中而改变。在前面的例子中,date
和item
通常会成为主键(在关系数据库术语中),提供关系完整性和更容易的连接。在某些情况下,以这种格式处理数据可能更加困难;您可能更喜欢拥有一个 DataFrame,其中包含一个以date
列中的时间戳为索引的每个不同item
值的列。DataFrame 的pivot
方法正好执行这种转换:
In [156]: pivoted = long_data.pivot(index="date", columns="item",
.....: values="value")
In [157]: pivoted.head()
Out[157]:
item infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8
1959-04-01 2.34 2778.801 5.1
1959-07-01 2.74 2775.488 5.3
1959-10-01 0.27 2785.204 5.6
1960-01-01 2.31 2847.699 5.2
传递的前两个值分别是要使用的列,作为行和列索引,最后是一个可选的值列,用于填充 DataFrame。假设您有两个值列,希望同时重塑:
In [159]: long_data["value2"] = np.random.standard_normal(len(long_data))
In [160]: long_data[:10]
Out[160]:
date item value value2
0 1959-01-01 realgdp 2710.349 0.802926
1 1959-01-01 infl 0.000 0.575721
2 1959-01-01 unemp 5.800 1.381918
3 1959-04-01 realgdp 2778.801 0.000992
4 1959-04-01 infl 2.340 -0.143492
5 1959-04-01 unemp 5.100 -0.206282
6 1959-07-01 realgdp 2775.488 -0.222392
7 1959-07-01 infl 2.740 -1.682403
8 1959-07-01 unemp 5.300 1.811659
9 1959-10-01 realgdp 2785.204 -0.351305
通过省略最后一个参数,您可以获得一个具有分层列的 DataFrame:
In [161]: pivoted = long_data.pivot(index="date", columns="item")
In [162]: pivoted.head()
Out[162]:
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8 0.575721 0.802926 1.381918
1959-04-01 2.34 2778.801 5.1 -0.143492 0.000992 -0.206282
1959-07-01 2.74 2775.488 5.3 -1.682403 -0.222392 1.811659
1959-10-01 0.27 2785.204 5.6 0.128317 -0.351305 -1.313554
1960-01-01 2.31 2847.699 5.2 -0.615939 0.498327 0.174072
In [163]: pivoted["value"].head()
Out[163]:
item infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8
1959-04-01 2.34 2778.801 5.1
1959-07-01 2.74 2775.488 5.3
1959-10-01 0.27 2785.204 5.6
1960-01-01 2.31 2847.699 5.2
请注意,pivot
等同于使用set_index
创建一个分层索引,然后调用unstack
:
In [164]: unstacked = long_data.set_index(["date", "item"]).unstack(level="item")
In [165]: unstacked.head()
Out[165]:
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8 0.575721 0.802926 1.381918
1959-04-01 2.34 2778.801 5.1 -0.143492 0.000992 -0.206282
1959-07-01 2.74 2775.488 5.3 -1.682403 -0.222392 1.811659
1959-10-01 0.27 2785.204 5.6 0.128317 -0.351305 -1.313554
1960-01-01 2.31 2847.699 5.2 -0.615939 0.498327 0.174072
从“宽”格式到“长”格式的旋转
DataFrame 的pivot
的逆操作是pandas.melt
。与在新的 DataFrame 中将一个列转换为多个不同,它将多个列合并为一个,生成一个比输入更长的 DataFrame。让我们看一个例子:
In [167]: df = pd.DataFrame({"key": ["foo", "bar", "baz"],
.....: "A": [1, 2, 3],
.....: "B": [4, 5, 6],
.....: "C": [7, 8, 9]})
In [168]: df
Out[168]:
key A B C
0 foo 1 4 7
1 bar 2 5 8
2 baz 3 6 9
"key"
列可以是一个组指示器,其他列是数据值。在使用pandas.melt
时,我们必须指示哪些列(如果有的话)是组指示器。让我们在这里只使用"key"
作为唯一的组指示器:
In [169]: melted = pd.melt(df, id_vars="key")
In [170]: melted
Out[170]:
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6
6 foo C 7
7 bar C 8
8 baz C 9
使用pivot
,我们可以重新塑造回原始布局:
In [171]: reshaped = melted.pivot(index="key", columns="variable",
.....: values="value")
In [172]: reshaped
Out[172]:
variable A B C
key
bar 2 5 8
baz 3 6 9
foo 1 4 7
由于pivot
的结果从用作行标签的列创建索引,我们可能希望使用reset_index
将数据移回到列中:
In [173]: reshaped.reset_index()
Out[173]:
variable key A B C
0 bar 2 5 8
1 baz 3 6 9
2 foo 1 4 7
您还可以指定要用作“值”列的列的子集:
In [174]: pd.melt(df, id_vars="key", value_vars=["A", "B"])
Out[174]:
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6
pandas.melt
也可以在没有任何组标识符的情况下使用:
In [175]: pd.melt(df, value_vars=["A", "B", "C"])
Out[175]:
variable value
0 A 1
1 A 2
2 A 3
3 B 4
4 B 5
5 B 6
6 C 7
7 C 8
8 C 9
In [176]: pd.melt(df, value_vars=["key", "A", "B"])
Out[176]:
variable value
0 key foo
1 key bar
2 key baz
3 A 1
4 A 2
5 A 3
6 B 4
7 B 5
8 B 6
8.4 结论
现在您已经掌握了一些关于 pandas 的基础知识,用于数据导入、清理和重新组织,我们准备继续使用 matplotlib 进行数据可视化。当我们讨论更高级的分析时,我们将回到书中的其他领域来探索 pandas 的更多功能。
九、绘图和可视化
原文:
wesmckinney.com/book/plotting-and-visualization
译者:飞龙
协议:CC BY-NC-SA 4.0
此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。
如果您发现本书的在线版本有用,请考虑订购纸质版或无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。
制作信息丰富的可视化(有时称为*图)是数据分析中最重要的任务之一。它可能是探索过程的一部分,例如,帮助识别异常值或所需的数据转换,或者作为生成模型想法的一种方式。对于其他人,构建用于网络的交互式可视化可能是最终目标。Python 有许多附加库用于制作静态或动态可视化,但我主要关注matplotlib和构建在其之上的库。
matplotlib 是一个桌面绘图包,旨在创建适合出版的图形和图表。该项目由 John Hunter 于 2002 年发起,旨在在 Python 中实现类似 MATLAB 的绘图界面。matplotlib 和 IPython 社区合作简化了从 IPython shell(现在是 Jupyter 笔记本)进行交互式绘图。matplotlib 支持所有操作系统上的各种 GUI 后端,并且可以将可视化导出为所有常见的矢量和光栅图形格式(PDF、SVG、JPG、PNG、BMP、GIF 等)。除了一些图表外,本书中几乎所有的图形都是使用 matplotlib 生成的。
随着时间的推移,matplotlib 衍生出了许多用于数据可视化的附加工具包,这些工具包使用 matplotlib 进行底层绘图。其中之一是seaborn,我们将在本章后面探讨。
在本章中跟随代码示例的最简单方法是在 Jupyter 笔记本中输出图形。要设置这个,可以在 Jupyter 笔记本中执行以下语句:
%matplotlib inline
注意
自 2012 年第一版以来,已经创建了许多新的数据可视化库,其中一些(如 Bokeh 和 Altair)利用现代网络技术创建交互式可视化,与 Jupyter 笔记本很好地集成。与在本书中使用多个可视化工具不同,我决定坚持使用 matplotlib 来教授基础知识,特别是因为 pandas 与 matplotlib 有很好的集成。您可以根据本章的原则学习如何使用其他可视化库。
9.1 简要的 matplotlib API 入门
使用 matplotlib 时,我们使用以下导入约定:
In [13]: import matplotlib.pyplot as plt
在 Jupyter 中运行%matplotlib notebook
(或在 IPython 中运行%matplotlib
),我们可以尝试创建一个简单的图。如果一切设置正确,应该会出现一个类似 Simple line plot 的线图:
In [14]: data = np.arange(10)
In [15]: data
Out[15]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [16]: plt.plot(data)
图 9.1:简单线图
虽然像 seaborn 和 pandas 内置绘图函数将处理许多制作图形的琐碎细节,但如果您希望自定义超出提供的函数选项之外的内容,您需要了解一些关于 matplotlib API 的知识。
注意
本书中没有足够的空间来全面介绍 matplotlib 的功能广度和深度。它应该足以教会您如何上手。matplotlib 图库和文档是学习高级功能的最佳资源。
图和子图
matplotlib 中的绘图位于 Figure
对象中。您可以使用 plt.figure
创建一个新的图:
In [17]: fig = plt.figure()
在 IPython 中,如果您首先运行 %matplotlib
来设置 matplotlib 集成,将会出现一个空白绘图窗口,但在 Jupyter 中,直到我们使用更多命令之前,什么都不会显示。
plt.figure
有许多选项;特别是,如果保存到磁盘,figsize
将保证图的特定大小和纵横比。
您不能在空白图中制作绘图。您必须使用 add_subplot
创建一个或多个 subplots
:
In [18]: ax1 = fig.add_subplot(2, 2, 1)
这意味着图应该是 2 × 2(因此总共最多四个绘图),我们选择了四个子图中的第一个(从 1 编号)。如果您创建下两个子图,您将得到一个看起来像 一个空的 matplotlib 图,带有三个子图 的可视化:
In [19]: ax2 = fig.add_subplot(2, 2, 2)
In [20]: ax3 = fig.add_subplot(2, 2, 3)
图 9.2:一个空的 matplotlib 图,带有三个子图
提示:
使用 Jupyter 笔记本的一个细微之处是,每次评估单元格后绘图都会重置,因此您必须将所有绘图命令放在一个单独的笔记本单元格中。
在这里,我们在同一个单元格中运行所有这些命令:
fig = plt.figure()
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 3)
这些绘图轴对象有各种方法,可以创建不同类型的绘图,最好使用轴方法而不是像 plt.plot
这样的顶级绘图函数。例如,我们可以使用 plot
方法制作一条线图(参见单个绘图后的数据可视化):
In [21]: ax3.plot(np.random.standard_normal(50).cumsum(), color="black",
....: linestyle="dashed")
图 9.3:单个绘图后的数据可视化
当您运行此命令时,您可能会注意到类似 <matplotlib.lines.Line2D at ...>
的输出。matplotlib 返回引用刚刚添加的绘图子组件的对象。大多数情况下,您可以安全地忽略此输出,或者您可以在行末加上分号以抑制输出。
附加选项指示 matplotlib 绘制一条黑色虚线。这里由 fig.add_subplot
返回的对象是 AxesSubplot
对象,您可以通过调用每个实例方法直接在其他空子图上绘制(参见添加额外绘图后的数据可视化):
In [22]: ax1.hist(np.random.standard_normal(100), bins=20, color="black", alpha=0
.3);
In [23]: ax2.scatter(np.arange(30), np.arange(30) + 3 * np.random.standard_normal
(30));
图 9.4:添加额外绘图后的数据可视化
alpha=0.3
样式选项设置了叠加绘图的透明度。
您可以在 matplotlib 文档 中找到绘图类型的全面目录。
为了更方便地创建子图网格,matplotlib 包括一个 plt.subplots
方法,它创建一个新图并返回一个包含创建的子图对象的 NumPy 数组:
In [25]: fig, axes = plt.subplots(2, 3)
In [26]: axes
Out[26]:
array([[<Axes: >, <Axes: >, <Axes: >],
[<Axes: >, <Axes: >, <Axes: >]], dtype=object)
然后,axes
数组可以像二维数组一样索引;例如,axes[0, 1]
指的是顶部行中心的子图。您还可以使用 sharex
和 sharey
指示子图应具有相同的 x 或 y 轴。当您在相同比例上比较数据时,这可能很有用;否则,matplotlib 会独立自动缩放绘图限制。有关此方法的更多信息,请参见 表 9.1。
表 9.1:matplotlib.pyplot.subplots
选项
参数 | 描述 |
---|---|
nrows | 子图的行数 |
ncols | 子图的列数 |
sharex | 所有子图应使用相同的 x 轴刻度(调整 xlim 将影响所有子图) |
sharey | 所有子图应使用相同的 y 轴刻度(调整 ylim 将影响所有子图) |
subplot_kw | 传递给 add_subplot 调用的关键字字典,用于创建每个子图 |
**fig_kw | 创建图时使用subplots 的附加关键字,例如plt.subplots(2, 2, figsize=(8, 6)) |
调整子图周围的间距
默认情况下,matplotlib 在子图周围留有一定量的填充和子图之间的间距。这些间距都是相对于绘图的高度和宽度指定的,因此如果您通过编程或使用 GUI 窗口手动调整绘图大小,绘图将动态调整自身。您可以使用Figure
对象上的subplots_adjust
方法更改间距:
subplots_adjust(left=None, bottom=None, right=None, top=None,
wspace=None, hspace=None)
wspace
和hspace
控制子图之间使用的百分比图宽度和图高度的间距。这里是一个您可以在 Jupyter 中执行的小例子,我将间距缩小到零(参见没有子图间距的数据可视化):
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)
for i in range(2):
for j in range(2):
axes[i, j].hist(np.random.standard_normal(500), bins=50,
color="black", alpha=0.5)
fig.subplots_adjust(wspace=0, hspace=0)
图 9.5:没有子图间距的数据可视化
您可能会注意到轴标签重叠。matplotlib 不会检查标签是否重叠,因此在这种情况下,您需要通过指定显式刻度位置和刻度标签自行修复标签(我们将在后面的部分刻度、标签和图例中看到如何做到这一点)。
颜色、标记和线型
matplotlib 的线plot
函数接受 x 和 y 坐标数组以及可选的颜色样式选项。例如,要用绿色虚线绘制x
与y
,您可以执行:
ax.plot(x, y, linestyle="--", color="green")
提供了许多常用颜色的颜色名称,但您可以通过指定其十六进制代码(例如,"#CECECE"
)来使用光谱上的任何颜色。您可以查看plt.plot
的文档字符串以查看一些支持的线型。在线文档中提供了更全面的参考资料。
线图还可以具有标记来突出实际数据点。由于 matplotlib 的plot
函数创建连续线图,插值点之间的插值,有时可能不清楚点位于何处。标记可以作为附加样式选项提供(参见带有标记的线图):
In [31]: ax = fig.add_subplot()
In [32]: ax.plot(np.random.standard_normal(30).cumsum(), color="black",
....: linestyle="dashed", marker="o");
图 9.6:带有标记的线图
对于线图,您会注意到默认情况下后续点是线性插值的。这可以通过drawstyle
选项进行更改(参见带有不同 drawstyle 选项的线图):
In [34]: fig = plt.figure()
In [35]: ax = fig.add_subplot()
In [36]: data = np.random.standard_normal(30).cumsum()
In [37]: ax.plot(data, color="black", linestyle="dashed", label="Default");
In [38]: ax.plot(data, color="black", linestyle="dashed",
....: drawstyle="steps-post", label="steps-post");
In [39]: ax.legend()
图 9.7:带有不同 drawstyle 选项的线图
在这里,由于我们将label
参数传递给plot
,我们能够使用ax.legend
创建一个图例,以标识每条线。我在刻度、标签和图例中更多地讨论图例。
注意
无论您在绘制数据时是否传递了label
选项,都必须调用ax.legend
来创建图例。
刻度、标签和图例
大多数类型的绘图装饰都可以通过 matplotlib 轴对象上的方法访问。这包括xlim
、xticks
和xticklabels
等方法。它们分别控制绘图范围、刻度位置和刻度标签。它们可以以两种方式使用:
-
不带参数调用返回当前参数值(例如,
ax.xlim()
返回当前 x 轴绘图范围) -
带参数调用设置参数值(例如,
ax.xlim([0, 10])
将 x 轴范围设置为 0 到 10)
所有这些方法都作用于活动或最近创建的AxesSubplot
。每个对应于 subplot 对象本身的两种方法;在xlim
的情况下,这些方法是ax.get_xlim
和ax.set_xlim
。
设置标题、轴标签、刻度和刻度标签
为了说明如何自定义坐标轴,我将创建一个简单的图和一个随机漫步的绘图(参见用于说明 xticks 的简单绘图(带有默认标签)):
In [40]: fig, ax = plt.subplots()
In [41]: ax.plot(np.random.standard_normal(1000).cumsum());
图 9.8:用于说明 xticks 的简单图表(带有默认标签)
要更改 x 轴刻度,最简单的方法是使用set_xticks
和set_xticklabels
。前者指示 matplotlib 在数据范围内放置刻度的位置;默认情况下,这些位置也将是标签。但是我们可以使用set_xticklabels
设置任何其他值作为标签:
In [42]: ticks = ax.set_xticks([0, 250, 500, 750, 1000])
In [43]: labels = ax.set_xticklabels(["one", "two", "three", "four", "five"],
....: rotation=30, fontsize=8)
rotation
选项将 x 轴刻度标签设置为 30 度旋转。最后,set_xlabel
为 x 轴命名,set_title
为子图标题(请参见用于说明自定义 xticks 的简单图表以查看生成的图):
In [44]: ax.set_xlabel("Stages")
Out[44]: Text(0.5, 6.666666666666652, 'Stages')
In [45]: ax.set_title("My first matplotlib plot")
图 9.9:用于说明自定义 xticks 的简单图表
修改 y 轴的过程与此示例中的x
替换为y
相同。axes 类有一个set
方法,允许批量设置绘图属性。从前面的示例中,我们也可以这样写:
ax.set(title="My first matplotlib plot", xlabel="Stages")
添加图例
图例是识别图表元素的另一个关键元素。有几种方法可以添加图例。最简单的方法是在添加每个图表元素时传递label
参数:
In [46]: fig, ax = plt.subplots()
In [47]: ax.plot(np.random.randn(1000).cumsum(), color="black", label="one");
In [48]: ax.plot(np.random.randn(1000).cumsum(), color="black", linestyle="dashed
",
....: label="two");
In [49]: ax.plot(np.random.randn(1000).cumsum(), color="black", linestyle="dotted
",
....: label="three");
一旦您完成了这一步,您可以调用ax.legend()
来自动创建图例。生成的图表在带有三条线和图例的简单图表中:
In [50]: ax.legend()
图 9.10:带有三条线和图例的简单图表
legend
方法有几个其他选项可用于位置loc
参数。有关更多信息,请参阅文档字符串(使用ax.legend?
)。
loc
图例选项告诉 matplotlib 在哪里放置图例。默认值是"best"
,它会尝试选择一个最不起眼的位置。要从图例中排除一个或多个元素,请不传递标签或传递label="_nolegend_"
。
注释和在子图上绘制
除了标准的绘图类型,您可能希望绘制自己的绘图注释,这可能包括文本、箭头或其他形状。您可以使用text
、arrow
和annotate
函数添加注释和文本。text
在给定坐标(x, y)
处绘制文本,可选的自定义样式:
ax.text(x, y, "Hello world!",
family="monospace", fontsize=10)
注释可以绘制文本和箭头,并适当排列。例如,让我们绘制自 2007 年以来的标准普尔 500 指数收盘价(从 Yahoo! Finance 获取),并用 2008-2009 年金融危机的一些重要日期进行注释。您可以在 Jupyter 笔记本中的单个单元格中运行此代码示例。查看 2008-2009 年金融危机中的重要日期以查看结果:
from datetime import datetime
fig, ax = plt.subplots()
data = pd.read_csv("examples/spx.csv", index_col=0, parse_dates=True)
spx = data["SPX"]
spx.plot(ax=ax, color="black")
crisis_data = [
(datetime(2007, 10, 11), "Peak of bull market"),
(datetime(2008, 3, 12), "Bear Stearns Fails"),
(datetime(2008, 9, 15), "Lehman Bankruptcy")
]
for date, label in crisis_data:
ax.annotate(label, xy=(date, spx.asof(date) + 75),
xytext=(date, spx.asof(date) + 225),
arrowprops=dict(facecolor="black", headwidth=4, width=2,
headlength=4),
horizontalalignment="left", verticalalignment="top")
# Zoom in on 2007-2010
ax.set_xlim(["1/1/2007", "1/1/2011"])
ax.set_ylim([600, 1800])
ax.set_title("Important dates in the 2008–2009 financial crisis")
图 9.11:2008-2009 年金融危机中的重要日期
在这个图表中有几个重要的要点需要强调。ax.annotate
方法可以在指定的 x 和 y 坐标处绘制标签。我们使用set_xlim
和set_ylim
方法手动设置绘图的起始和结束边界,而不是使用 matplotlib 的默认值。最后,ax.set_title
为绘图添加了一个主标题。
请查看在线 matplotlib 画廊,了解更多注释示例以供学习。
绘制形状需要更多的注意。matplotlib 有代表许多常见形状的对象,称为patches。其中一些,如Rectangle
和Circle
,可以在matplotlib.pyplot
中找到,但完整的集合位于matplotlib.patches
中。
要向图表添加形状,您需要创建补丁对象,并通过将补丁传递给ax.add_patch
将其添加到子图ax
中(请参见由三个不同补丁组成的数据可视化):
fig, ax = plt.subplots()
rect = plt.Rectangle((0.2, 0.75), 0.4, 0.15, color="black", alpha=0.3)
circ = plt.Circle((0.7, 0.2), 0.15, color="blue", alpha=0.3)
pgon = plt.Polygon([[0.15, 0.15], [0.35, 0.4], [0.2, 0.6]],
color="green", alpha=0.5)
ax.add_patch(rect)
ax.add_patch(circ)
ax.add_patch(pgon)
图 9.12:由三个不同补丁组成的数据可视化
如果您查看许多熟悉的绘图类型的实现,您会发现它们是由补丁组装而成的。
保存图表到文件
您可以使用图形对象的savefig
实例方法将活动图形保存到文件。例如,要保存图形的 SVG 版本,您只需输入:
fig.savefig("figpath.svg")
文件类型是从文件扩展名中推断的。因此,如果您使用.pdf
,您将得到一个 PDF。我经常用于发布图形的一个重要选项是dpi
,它控制每英寸的分辨率。要获得相同的图形作为 400 DPI 的 PNG,您可以执行:
fig.savefig("figpath.png", dpi=400)
有关savefig
的一些其他选项,请参见表 9.2。要获取全面的列表,请参考 IPython 或 Jupyter 中的文档字符串。
表 9.2:一些fig.savefig
选项
参数 | 描述 |
---|---|
fname | 包含文件路径或 Python 文件对象的字符串。图形格式从文件扩展名中推断(例如,.pdf 表示 PDF,.png 表示 PNG)。 |
dpi | 每英寸点数的图形分辨率;在 IPython 中默认为 100,在 Jupyter 中默认为 72,但可以进行配置。 |
facecolor, edgecolor | 子图外部的图形背景颜色;默认为"w" (白色)。 |
format | 要使用的显式文件格式("png" 、"pdf" 、"svg" 、"ps" 、"eps" 等)。 |
matplotlib 配置
matplotlib 预先配置了色彩方案和默认设置,主要用于准备出版图。幸运的是,几乎所有默认行为都可以通过全局参数进行自定义,这些参数控制图形大小、子图间距、颜色、字体大小、网格样式等。从 Python 编程方式修改配置的一种方法是使用rc
方法;例如,要将全局默认图形大小设置为 10×10,可以输入:
plt.rc("figure", figsize=(10, 10))
所有当前的配置设置都可以在plt.rcParams
字典中找到,并且可以通过调用plt.rcdefaults()
函数将其恢复为默认值。
rc
的第一个参数是您希望自定义的组件,例如"figure"
、"axes"
、"xtick"
、"ytick"
、"grid"
、"legend"
或其他许多选项。之后可以跟随一系列关键字参数,指示新的参数。在程序中写下选项的便捷方式是作为一个字典:
plt.rc("font", family="monospace", weight="bold", size=8)
要进行更广泛的自定义并查看所有选项列表,matplotlib 附带了一个配置文件matplotlibrc,位于matplotlib/mpl-data目录中。如果您自定义此文件并将其放在名为*.matplotlibrc*的主目录中,每次使用 matplotlib 时都会加载它。
正如我们将在下一节中看到的,seaborn 包具有几个内置的绘图主题或样式,这些主题或样式在内部使用 matplotlib 的配置系统。
9.2 使用 pandas 和 seaborn 绘图
matplotlib 可以是一个相当低级的工具。您可以从其基本组件中组装图表:数据显示(即绘图类型:线条、柱状图、箱线图、散点图、等高线图等)、图例、标题、刻度标签和其他注释。
在 pandas 中,我们可能有多列数据,以及行和列标签。pandas 本身具有内置方法,简化了从 DataFrame 和 Series 对象创建可视化的过程。另一个库是seaborn
,这是一个建立在 matplotlib 之上的高级统计图形库。seaborn 简化了创建许多常见可视化类型的过程。
线图
Series 和 DataFrame 具有plot
属性,用于创建一些基本的绘图类型。默认情况下,plot()
生成线图(参见简单 Series 绘图):
In [61]: s = pd.Series(np.random.standard_normal(10).cumsum(), index=np.arange(0,
100, 10))
In [62]: s.plot()
图 9.13:简单 Series 绘图
Series 对象的索引被传递给 matplotlib 以在 x 轴上绘制,尽管您可以通过传递 use_index=False
来禁用此功能。x 轴刻度和限制可以通过 xticks
和 xlim
选项进行调整,y 轴分别通过 yticks
和 ylim
进行调整。请参见 表 9.3 以获取 plot
选项的部分列表。我将在本节中评论其中一些,并留下其余的供您探索。
表 9.3:Series.plot
方法参数
参数 | 描述 |
---|---|
label | 图例标签 |
ax | 要绘制的 matplotlib 子图对象;如果未传递任何内容,则使用活动的 matplotlib 子图 |
style | 样式字符串,如 "ko--" ,传递给 matplotlib |
alpha | 图形填充不透明度(从 0 到 1) |
kind | 可以是 "area" , "bar" , "barh" , "density" , "hist" , "kde" , "line" , 或 "pie" ;默认为 "line" |
figsize | 要创建的图形对象的大小 |
logx | 在 x 轴上进行对数缩放,传递 True ;传递 "sym" 以进行允许负值的对称对数缩放 |
logy | 在 y 轴上进行对数缩放,传递 True ;传递 "sym" 以进行允许负值的对称对数缩放 |
title | 用于图的标题 |
use_index | 使用对象索引作为刻度标签 |
rot | 刻度标签的旋转(0 到 360) |
xticks | 用于 x 轴刻度的值 |
yticks | 用于 y 轴刻度的值 |
xlim | x 轴限制(例如,[0, 10] ) |
ylim | y 轴限制 |
grid | 显示坐标轴网格(默认关闭) |
大多数 pandas 的绘图方法都接受一个可选的 ax
参数,可以是一个 matplotlib 子图对象。这样可以在网格布局中更灵活地放置子图。
DataFrame 的 plot
方法将其每列作为不同的线绘制在同一个子图上,自动创建图例(请参见 简单的 DataFrame 绘图):
In [63]: df = pd.DataFrame(np.random.standard_normal((10, 4)).cumsum(0),
....: columns=["A", "B", "C", "D"],
....: index=np.arange(0, 100, 10))
In [64]: plt.style.use('grayscale')
In [65]: df.plot()
图 9.14:简单的 DataFrame 绘图
注意
这里我使用了 plt.style.use('grayscale')
来切换到更适合黑白出版的颜色方案,因为一些读者可能无法看到完整的彩色图。
plot
属性包含不同绘图类型的方法“家族”。例如,df.plot()
等同于 df.plot.line()
。我们将在接下来探索其中一些方法。
注意
plot
的其他关键字参数会传递给相应的 matplotlib 绘图函数,因此您可以通过学习更多关于 matplotlib API 的知识来进一步自定义这些图。
DataFrame 有许多选项,允许对列的处理方式进行一定的灵活性,例如,是否将它们全部绘制在同一个子图上,还是创建单独的子图。更多信息请参见 表 9.4。
表 9.4:DataFrame 特定的绘图参数
参数 | 描述 |
---|---|
subplots | 在单独的子图中绘制每个 DataFrame 列 |
layouts | 2 元组(行数,列数),提供子图的布局 |
sharex | 如果 subplots=True ,共享相同的 x 轴,链接刻度和限制 |
sharey | 如果 subplots=True ,共享相同的 y 轴 |
legend | 添加子图图例(默认为 True ) |
sort_columns | 按字母顺序绘制列;默认使用现有列顺序 |
注意
有关时间序列绘图,请参见 第十一章:时间序列。
条形图
plot.bar()
和 plot.barh()
分别绘制垂直和水平条形图。在这种情况下,Series 或 DataFrame 的索引将用作 x(bar
)或 y(barh
)刻度(请参见 水平和垂直条形图):
In [66]: fig, axes = plt.subplots(2, 1)
In [67]: data = pd.Series(np.random.uniform(size=16), index=list("abcdefghijklmno
p"))
In [68]: data.plot.bar(ax=axes[0], color="black", alpha=0.7)
Out[68]: <Axes: >
In [69]: data.plot.barh(ax=axes[1], color="black", alpha=0.7)
图 9.15:水平和垂直条形图
使用 DataFrame,条形图将每行中的值分组在条形图中,侧边显示,每个值一个条形图。请参见 DataFrame 条形图:
In [71]: df = pd.DataFrame(np.random.uniform(size=(6, 4)),
....: index=["one", "two", "three", "four", "five", "six"],
....: columns=pd.Index(["A", "B", "C", "D"], name="Genus"))
In [72]: df
Out[72]:
Genus A B C D
one 0.370670 0.602792 0.229159 0.486744
two 0.420082 0.571653 0.049024 0.880592
three 0.814568 0.277160 0.880316 0.431326
four 0.374020 0.899420 0.460304 0.100843
five 0.433270 0.125107 0.494675 0.961825
six 0.601648 0.478576 0.205690 0.560547
In [73]: df.plot.bar()
图 9.16:DataFrame 条形图
请注意,DataFrame 列上的“种属”名称用于标题图例。
我们通过传递stacked=True
从 DataFrame 创建堆叠条形图,导致每行中的值水平堆叠在一起(参见 DataFrame 堆叠条形图):
In [75]: df.plot.barh(stacked=True, alpha=0.5)
图 9.17:DataFrame 堆叠条形图
注意
一个有用的条形图的制作方法是使用value_counts
来可视化 Series 的值频率:s.value_counts().plot.bar()
。
让我们看一个关于餐厅小费的示例数据集。假设我们想要制作一个堆叠条形图,显示每天每个派对规模的数据点的百分比。我使用read_csv
加载数据,并通过日期和派对规模进行交叉制表。pandas.crosstab
函数是从两个 DataFrame 列计算简单频率表的便捷方法:
In [77]: tips = pd.read_csv("examples/tips.csv")
In [78]: tips.head()
Out[78]:
total_bill tip smoker day time size
0 16.99 1.01 No Sun Dinner 2
1 10.34 1.66 No Sun Dinner 3
2 21.01 3.50 No Sun Dinner 3
3 23.68 3.31 No Sun Dinner 2
4 24.59 3.61 No Sun Dinner 4
In [79]: party_counts = pd.crosstab(tips["day"], tips["size"])
In [80]: party_counts = party_counts.reindex(index=["Thur", "Fri", "Sat", "Sun"])
In [81]: party_counts
Out[81]:
size 1 2 3 4 5 6
day
Thur 1 48 4 5 1 3
Fri 1 16 1 1 0 0
Sat 2 53 18 13 1 0
Sun 0 39 15 18 3 1
由于没有很多一人和六人的派对,我在这里删除它们:
In [82]: party_counts = party_counts.loc[:, 2:5]
然后,对每一行进行归一化,使总和为 1,并绘制图表(参见每天各尺寸派对的比例):
# Normalize to sum to 1
In [83]: party_pcts = party_counts.div(party_counts.sum(axis="columns"),
....: axis="index")
In [84]: party_pcts
Out[84]:
size 2 3 4 5
day
Thur 0.827586 0.068966 0.086207 0.017241
Fri 0.888889 0.055556 0.055556 0.000000
Sat 0.623529 0.211765 0.152941 0.011765
Sun 0.520000 0.200000 0.240000 0.040000
In [85]: party_pcts.plot.bar(stacked=True)
图 9.18:每天各尺寸派对的比例
因此,您可以看到在这个数据集中,派对规模似乎在周末增加。
对于需要在制作图表之前进行聚合或总结的数据,使用seaborn
包可以使事情变得更简单(使用conda install seaborn
进行安装)。现在让我们用 seaborn 查看小费百分比按天的情况(查看带误差条的每日小费百分比以查看结果图):
In [87]: import seaborn as sns
In [88]: tips["tip_pct"] = tips["tip"] / (tips["total_bill"] - tips["tip"])
In [89]: tips.head()
Out[89]:
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.063204
1 10.34 1.66 No Sun Dinner 3 0.191244
2 21.01 3.50 No Sun Dinner 3 0.199886
3 23.68 3.31 No Sun Dinner 2 0.162494
4 24.59 3.61 No Sun Dinner 4 0.172069
In [90]: sns.barplot(x="tip_pct", y="day", data=tips, orient="h")
图 9.19:每日小费百分比带误差条
seaborn 中的绘图函数接受一个data
参数,它可以是一个 pandas DataFrame。其他参数是指列名。因为在day
的每个值中有多个观察值,所以条形图是tip_pct
的平均值。在条形图上画的黑线代表 95%的置信区间(可以通过可选参数进行配置)。
seaborn.barplot
有一个hue
选项,可以使我们按照额外的分类值进行拆分(参见每日和时间的小费百分比):
In [92]: sns.barplot(x="tip_pct", y="day", hue="time", data=tips, orient="h")
图 9.20:每日和时间的小费百分比
请注意,seaborn 自动更改了图表的美学特征:默认颜色调色板、图表背景和网格线颜色。您可以使用seaborn.set_style
在不同的图表外观之间切换:
In [94]: sns.set_style("whitegrid")
在为黑白打印媒介制作图表时,您可能会发现设置灰度调色板很有用,如下所示:
sns.set_palette("Greys_r")
直方图和密度图
直方图是一种显示值频率的离散化条形图。数据点被分成离散的、均匀间隔的箱子,并绘制每个箱子中的数据点数。使用之前的小费数据,我们可以使用 Series 的plot.hist
方法制作总账单的小费百分比的直方图(参见小费百分比的直方图):
In [96]: tips["tip_pct"].plot.hist(bins=50)
图 9.21:小费百分比的直方图
一个相关的图表类型是密度图,它是通过计算可能生成观察数据的连续概率分布的估计而形成的。通常的做法是将这个分布近似为“核”混合——即,像正态分布这样的简单分布。因此,密度图也被称为核密度估计(KDE)图。使用plot.density
可以使用传统的正态混合估计制作密度图(参见小费百分比的密度图):
In [98]: tips["tip_pct"].plot.density()
图 9.22:小费百分比的密度图
这种情节需要 SciPy,所以如果您还没有安装它,可以暂停一下然后安装:
conda install scipy
通过其histplot
方法,seaborn 使直方图和密度图更加容易,可以同时绘制直方图和连续密度估计。例如,考虑一个由两个不同标准正态分布的抽样组成的双峰分布(请参见 Normalized histogram of normal mixture):
In [100]: comp1 = np.random.standard_normal(200)
In [101]: comp2 = 10 + 2 * np.random.standard_normal(200)
In [102]: values = pd.Series(np.concatenate([comp1, comp2]))
In [103]: sns.histplot(values, bins=100, color="black")
图 9.23:正态混合的归一化直方图
散点图或点图
点图或散点图可以是检查两个一维数据系列之间关系的有用方法。例如,这里我们从 statsmodels 项目加载macrodata
数据集,选择几个变量,然后计算对数差异:
In [104]: macro = pd.read_csv("examples/macrodata.csv")
In [105]: data = macro[["cpi", "m1", "tbilrate", "unemp"]]
In [106]: trans_data = np.log(data).diff().dropna()
In [107]: trans_data.tail()
Out[107]:
cpi m1 tbilrate unemp
198 -0.007904 0.045361 -0.396881 0.105361
199 -0.021979 0.066753 -2.277267 0.139762
200 0.002340 0.010286 0.606136 0.160343
201 0.008419 0.037461 -0.200671 0.127339
202 0.008894 0.012202 -0.405465 0.042560
然后我们可以使用 seaborn 的regplot
方法,它可以制作散点图并拟合线性回归线(参见 A seaborn regression/scatter plot):
In [109]: ax = sns.regplot(x="m1", y="unemp", data=trans_data)
In [110]: ax.set_title("Changes in log(m1) versus log(unemp)")
图 9.24:一个 seaborn 回归/散点图
在探索性数据分析中,查看一组变量之间的所有散点图是有帮助的;这被称为pairs图或scatter plot matrix。从头开始制作这样的图需要一些工作,因此 seaborn 有一个方便的pairplot
函数,支持将每个变量的直方图或密度估计放在对角线上(请参见 Pair plot matrix of statsmodels macro data 以查看生成的图):
In [111]: sns.pairplot(trans_data, diag_kind="kde", plot_kws={"alpha": 0.2})
图 9.25:statsmodels 宏数据的 pairs 图矩阵
您可能会注意到plot_kws
参数。这使我们能够将配置选项传递给对角线元素上的各个绘图调用。查看seaborn.pairplot
文档字符串以获取更详细的配置选项。
Facet Grids 和分类数据
那么对于具有额外分组维度的数据集呢?一种可视化具有许多分类变量的数据的方法是使用facet grid,这是一个二维布局的图,其中数据根据某个变量的不同值在每个轴上分割到各个图中。seaborn 有一个有用的内置函数catplot
,简化了根据分类变量拆分的许多种 facet 图的制作(请参见 Tipping percentage by day/time/smoker 以查看生成的图):
In [112]: sns.catplot(x="day", y="tip_pct", hue="time", col="smoker",
.....: kind="bar", data=tips[tips.tip_pct < 1])
图 9.26:按天/时间/吸烟者的小费百分比
与在 facet 内通过不同的条形颜色对“时间”进行分组不同,我们还可以通过为每个time
值添加一行来扩展 facet grid(请参见 Tipping percentage by day split by time/smoker):
In [113]: sns.catplot(x="day", y="tip_pct", row="time",
.....: col="smoker",
.....: kind="bar", data=tips[tips.tip_pct < 1])
图 9.27:按天分割的小费百分比按时间/吸烟者
catplot
支持其他可能有用的绘图类型,具体取决于您要显示的内容。例如,箱线图(显示中位数、四分位数和异常值)可以是一种有效的可视化类型(请参见 Box plot of tipping percentage by day):
In [114]: sns.catplot(x="tip_pct", y="day", kind="box",
.....: data=tips[tips.tip_pct < 0.5])
图 9.28:按天的小费百分比箱线图
您可以使用更通用的seaborn.FacetGrid
类创建自己的 facet grid 图。有关更多信息,请参阅seaborn 文档。
9.3 其他 Python 可视化工具
与开源软件一样,Python 中有许多用于创建图形的选项(太多了无法列出)。自 2010 年以来,许多开发工作都集中在为在网页上发布的交互式图形创建工具上。使用诸如Altair、Bokeh和Plotly等工具,现在可以在 Python 中指定动态、交互式图形,用于与 Web 浏览器一起使用。
对于为印刷品或网络创建静态图形,我建议使用 matplotlib 以及构建在 matplotlib 基础上的库,如 pandas 和 seaborn,以满足您的需求。对于其他数据可视化需求,学习如何使用其他可用工具可能会有所帮助。我鼓励您探索这个生态系统,因为它将继续发展和创新。
数据可视化方面的一本优秀书籍是 Claus O. Wilke 的《数据可视化基础》(O’Reilly),可以在印刷版或 Claus 的网站clauswilke.com/dataviz
上找到。
9.4 结论
本章的目标是通过使用 pandas、matplotlib 和 seaborn 进行一些基本数据可视化,让您初步了解。如果在您的工作中视觉传达数据分析结果很重要,我鼓励您寻找资源,了解更多关于有效数据可视化的知识。这是一个活跃的研究领域,您可以通过在线和印刷的许多优秀学习资源进行实践。
在下一章中,我们将关注使用 pandas 进行数据聚合和分组操作。