pandas GroupBy:你的Python数据分组指南
无论你是刚刚开始使用pandas,想掌握它的一个核心功能,还是想填补你对.groupby()
的一些空白,本教程都将帮助你从头到尾地分解和可视化一个pandas GroupBy操作。
本教程是对官方pandas文档和pandas Cookbook的补充,在那里你会看到自成一体的、一口气的例子。然而,在这里,你将专注于三个使用真实世界数据集的更复杂的演练。
在本教程中,你将了解到。
- 如何在真实世界的数据上使用pandasGroupBy操作
- 拆分-应用-合并的操作链如何运作
- 如何将拆分-应用-合并链分解成步骤
- 如何根据pandas GroupBy对象的意图和结果对其方法进行分类
前提条件
在你继续之前,请确保你在一个新的虚拟环境中拥有最新版本的pandas。
PS> python -m venv venv
PS> venv\Scripts\Activate.ps1
(venv) PS> python -m pip install pandas
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ python -m pip install pandas
在本教程中,你将专注于三个数据集。
- 美国国会数据集包含了关于历史上的国会议员的公开信息,并说明了
.groupby()
。 - 空气质量数据集包含定期的气体传感器读数。这将使你能够处理浮点和时间序列数据。
- 新闻聚合器数据集拥有几十万篇新闻文章的元数据。你将与字符串打交道,并用
.groupby()
进行文本混合。
一旦你下载了.zip
,就把该文件解压到你当前目录下一个叫做groupby-data/
的文件夹中。在你继续阅读之前,确保你的目录树看起来像这样。
./
│
└── groupby-data/
│
├── legislators-historical.csv
├── airqual.csv
└── news.csv
安装了pandas
,激活了你的虚拟环境,并下载了数据集,你就可以开始行动了!
例1:美国国会数据集
你将通过剖析历史上的国会议员的数据集来直接进入状态。你可以用read_csv()
,把CSV文件读到pandasDataFrame
。
# pandas_legislators.py
import pandas as pd
dtypes = {
"first_name": "category",
"gender": "category",
"type": "category",
"state": "category",
"party": "category",
}
df = pd.read_csv(
"groupby-data/legislators-historical.csv",
dtype=dtypes,
usecols=list(dtypes) + ["birthday", "last_name"],
parse_dates=["birthday"]
)
该数据集包含成员的姓名、生日、性别、类型("rep"
代表众议院或"sen"
代表参议院)、美国各州和政党。你可以使用df.tail()
来查看数据集的最后几行。
>>>
>>> from pandas_legislators import df
>>> df.tail()
last_name first_name birthday gender type state party
11970 Garrett Thomas 1972-03-27 M rep VA Republican
11971 Handel Karen 1962-04-18 F rep GA Republican
11972 Jones Brenda 1959-10-24 F rep MI Democrat
11973 Marino Tom 1952-08-15 M rep PA Republican
11974 Jones Walter 1943-02-10 M rep NC Republican
DataFrame
为了节省空间,使用了分类的dtypes。
>>>
>>> df.dtypes
last_name object
first_name category
birthday datetime64[ns]
gender category
type category
state category
party category
dtype: object
你可以看到,数据集的大部分列都有category
,这可以减少机器上的内存负荷。
pandas GroupBy的Hello, World!
现在你已经熟悉了这个数据集,你将从pandas GroupBy操作的Hello, World!
。在数据集的整个历史中,以州为单位,国会成员的数量是多少?在SQL中,你可以通过一个SELECT
语句找到这个答案。
SELECT state, count(name)
FROM df
GROUP BY state
ORDER BY state;
下面是在pandas中的近似方法。
>>>
>>> n_by_state = df.groupby("state")["last_name"].count()
>>> n_by_state.head(10)
state
AK 16
AL 206
AR 117
AS 2
AZ 48
CA 361
CO 90
CT 240
DC 2
DE 97
Name: last_name, dtype: int64
你调用.groupby()
,并传递你想分组的列的名称,即"state"
。然后,你用["last_name"]
来指定你想进行实际聚合的列。
你可以向.groupby()
传递很多东西,而不仅仅是一个列名作为第一个参数。你也可以指定以下任何一项。
下面是一个在两列上联合分组的例子,它找到了按州和按性别划分的国会成员的数量。
>>>
>>> df.groupby(["state", "gender"])["last_name"].count()
state gender
AK F 0
M 16
AL F 3
M 203
AR F 5
...
WI M 196
WV F 1
M 119
WY F 2
M 38
Name: last_name, Length: 116, dtype: int64
类似的SQL查询会是这样的。
SELECT state, gender, count(name)
FROM df
GROUP BY state, gender
ORDER BY state, gender;
接下来你会看到,.groupby()
和类似的SQL语句是近亲,但它们在功能上往往不尽相同。
pandas GroupBy vs SQL
现在是介绍pandas GroupBy操作和上述SQL查询之间一个突出区别的好时机。SQL查询的结果集包含三列。
state
gender
count
在pandas版本中,分组后的列被默认推送到 MultiIndex
的,默认情况下,产生的Series
。
>>>
>>> n_by_state_gender = df.groupby(["state", "gender"])["last_name"].count()
>>> type(n_by_state_gender)
<class 'pandas.core.series.Series'>
>>> n_by_state_gender.index[:5]
MultiIndex([('AK', 'M'),
('AL', 'F'),
('AL', 'M'),
('AR', 'F'),
('AR', 'M')],
names=['state', 'gender'])
为了更接近于模仿SQL的结果,并将分组后的列推回结果中的列,你可以使用as_index=False
。
>>>
>>> df.groupby(["state", "gender"], as_index=False)["last_name"].count()
state gender last_name
0 AK F 0
1 AK M 16
2 AL F 3
3 AL M 203
4 AR F 5
.. ... ... ...
111 WI M 196
112 WV F 1
113 WV M 119
114 WY F 2
115 WY M 38
[116 rows x 3 columns]
这就产生了一个有三列和一个的DataFrame
,而不是一个有一个的 。 RangeIndex
Series
MultiIndex
简而言之,使用as_index=False
会使你的结果更接近于类似操作的默认SQL输出。
注意:在df.groupby(["state", "gender"])["last_name"].count()
,你也可以用.size()
,而不是.count()
,因为你知道没有NaN
的姓。使用.count()
不包括NaN
的值,而.size()
包括所有的东西,不管是不是NaN
。
还要注意的是,上面的SQL查询明确使用了ORDER BY
,而.groupby()
则没有。这是因为.groupby()
默认通过其参数sort
,除非你另外告诉它,否则它就是True
。
>>>
>>> # Don't sort results by the sort keys
>>> df.groupby("state", sort=False)["last_name"].count()
state
DE 97
VA 432
SC 251
MD 305
PA 1053
...
AK 16
PI 13
VI 4
GU 4
AS 2
Name: last_name, dtype: int64
接下来,你将深入了解.groupby()
实际产生的对象。
pandas GroupBy如何工作
在你进一步了解这些细节之前,先退一步看看.groupby()
本身。
>>>
>>> by_state = df.groupby("state")
>>> print(by_state)
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x107293278>
什么是DataFrameGroupBy
?print函数显示的它的.__str__()
值并没有给你提供多少关于它实际是什么或它如何工作的信息。DataFrameGroupBy
对象之所以让你难以理解,是因为它的本质是懒惰的。在你告诉它之前,它不会做任何操作来产生一个有用的结果。
注意:在本教程中,通用术语pandas GroupBy对象是指DataFrameGroupBy
和SeriesGroupBy
对象,它们有很多共同点。
一个经常与.groupby()
一起使用的术语是split-apply-combine。这指的是一个由三个步骤组成的链条。
- 将一个表分成若干组。
- 对每个小表应用一些操作。
- 合并结果。
检查df.groupby("state")
可能很困难,因为在你对结果对象进行操作之前,它几乎不做这些事情。一个pandas GroupBy对象几乎推迟了分割-应用-合并过程中的每一个部分,直到你对它调用一个方法。
那么,如果你不能看到它们中的任何一个单独发生,你怎么能在精神上把分裂、应用和组合阶段分开呢?检查pandas GroupBy对象并看到拆分过程的一个有效方法是对它进行迭代。
>>>
>>> for state, frame in by_state:
... print(f"First 2 entries for {state!r}")
... print("------------------------")
... print(frame.head(2), end="\n\n")
...
First 2 entries for 'AK'
------------------------
last_name first_name birthday gender type state party
6619 Waskey Frank 1875-04-20 M rep AK Democrat
6647 Cale Thomas 1848-09-17 M rep AK Independent
First 2 entries for 'AL'
------------------------
last_name first_name birthday gender type state party
912 Crowell John 1780-09-18 M rep AL Republican
991 Walker John 1783-08-12 M sen AL Republican
如果你正在处理一个具有挑战性的聚合问题,那么在pandas GroupBy对象上进行迭代是一个很好的方法,可以直观地看到split-apply-combine的分割部分。
还有一些其他的方法和属性可以让你查看各个组和它们的分割情况。.groups
属性会给你一个{group name: group label}
对的字典。例如,by_state.groups
是一个以状态为键的dict
。这里是"PA"
的键的值。
>>>
>>> by_state.groups["PA"]
Int64Index([ 4, 19, 21, 27, 38, 57, 69, 76, 84,
88,
...
11842, 11866, 11875, 11877, 11887, 11891, 11932, 11945, 11959,
11973],
dtype='int64', length=1053)
每个值都是属于该特定组的行的索引位置的序列。在上面的输出中,4、19和21是df
中的第一个索引,在这些索引中,状态等于"PA"
。
你也可以使用.get_group()
,作为从单一组中钻取子表的一种方式。
>>>
>>> by_state.get_group("PA")
last_name first_name birthday gender type state party
4 Clymer George 1739-03-16 M rep PA NaN
19 Maclay William 1737-07-20 M sen PA Anti-Administration
21 Morris Robert 1734-01-20 M sen PA Pro-Administration
27 Wynkoop Henry 1737-03-02 M rep PA NaN
38 Jacobs Israel 1726-06-09 M rep PA NaN
... ... ... ... ... ... ... ...
11891 Brady Robert 1945-04-07 M rep PA Democrat
11932 Shuster Bill 1961-01-10 M rep PA Republican
11945 Rothfus Keith 1962-04-25 M rep PA Republican
11959 Costello Ryan 1976-09-07 M rep PA Republican
11973 Marino Tom 1952-08-15 M rep PA Republican
这实际上等同于使用.loc[]
。你可以用类似于df.loc[df["state"] == "PA"]
的方式得到同样的输出。
还值得一提的是,.groupby()
确实通过为你传递的每个键建立一个Grouping
类实例来做一些,但不是全部的拆分工作。然而,持有这些分组的BaseGrouper
类的许多方法是被懒散地调用的,而不是在.__init__()
,而且许多方法还使用了缓存的属性设计。
接下来,应用部分呢?你可以把这一步看作是对拆分阶段产生的每一个子表应用相同的操作(或可调用)。
从pandas GroupBy对象by_state
,你可以用next()
,抓取初始美国状态和DataFrame
。当你遍历一个pandas GroupBy对象时,你会得到可以解包为两个变量的对。
>>>
>>> state, frame = next(iter(by_state)) # First tuple from iterator
>>> state
'AK'
>>> frame.head(3)
last_name first_name birthday gender type state party
6619 Waskey Frank 1875-04-20 M rep AK Democrat
6647 Cale Thomas 1848-09-17 M rep AK Independent
7442 Grigsby George 1874-12-02 M rep AK NaN
现在,回想一下你最初的、完整的操作。
>>>
>>> df.groupby("state")["last_name"].count()
state
AK 16
AL 206
AR 117
AS 2
AZ 48
...
应用阶段,当应用于你的单一的、子集的DataFrame
,将看起来像这样。
>>>
>>> frame["last_name"].count() # Count for state == 'AK'
16
你可以看到结果,16,与组合结果中的AK
的值一致。
最后一步,结合,将所有子表上应用的操作结果,以一种直观的方式结合在一起。
继续阅读,探索分割-应用-合并过程的更多例子。
例2:空气质量数据集
空气质量数据集包含来自意大利的气体传感器设备的每小时读数。CSV文件中的缺失值用*-200*来表示。你可以使用read_csv()
,将两列合并为一个时间戳,同时使用其他列的一个子集。
# pandas_airqual.py
import pandas as pd
df = pd.read_csv(
"groupby-data/airqual.csv",
parse_dates=[["Date", "Time"]],
na_values=[-200],
usecols=["Date", "Time", "CO(GT)", "T", "RH", "AH"]
).rename(
columns={
"CO(GT)": "co",
"Date_Time": "tstamp",
"T": "temp_c",
"RH": "rel_hum",
"AH": "abs_hum",
}
).set_index("tstamp")
这就产生了一个DataFrame
,其中有一个DatetimeIndex
和四个float
列。
>>>
>>> from pandas_airqual import df
>>> df.head()
co temp_c rel_hum abs_hum
tstamp
2004-03-10 18:00:00 2.6 13.6 48.9 0.758
2004-03-10 19:00:00 2.0 13.3 47.7 0.726
2004-03-10 20:00:00 2.2 11.9 54.0 0.750
2004-03-10 21:00:00 2.2 11.0 60.0 0.787
2004-03-10 22:00:00 1.6 11.2 59.6 0.789
这里,co
是该小时的平均一氧化碳读数,而temp_c
,rel_hum
, 和abs_hum
分别是该小时的平均摄氏温度、相对湿度和绝对湿度。观测时间为2004年3月至2005年4月。
>>>
>>> df.index.min()
Timestamp('2004-03-10 18:00:00')
>>> df.index.max()
Timestamp('2005-04-04 14:00:00')
到目前为止,你已经通过指定它们的名称为str
,如df.groupby("state")
,对列进行分组。但是.groupby()
,比这要灵活得多!接下来你会看到如何做。
对派生数组进行分组
前面你看到,.groupby()
的第一个参数可以接受几个不同的参数。
- 一个列或列的列表
- 一个
dict
或 pandasSeries
- 一个NumPy数组或pandas
Index
,或者一个类似于数组的可迭代参数
你可以利用最后一个选项,以便按星期分组。使用索引的.day_name()
,产生一个字符串的pandasIndex
。这里是前十个观察值。
>>>
>>> day_names = df.index.day_name()
>>> type(day_names)
<class 'pandas.core.indexes.base.Index'>
>>> day_names[:10]
Index(['Wednesday', 'Wednesday', 'Wednesday', 'Wednesday', 'Wednesday',
'Wednesday', 'Thursday', 'Thursday', 'Thursday', 'Thursday'],
dtype='object', name='tstamp')
然后你可以把这个对象作为.groupby()
的关键。在pandas中,day_names
是类似数组的。它是一个标签的一维序列。
注意:对于一个pandasSeries
,而不是一个Index
,你需要使用.dt
访问器来访问像.day_name()
这样的方法。如果ser
是你的Series
,那么你就需要ser.dt.day_name()
。
现在,将该对象传递给.groupby()
,以找到一周内各天的平均一氧化碳(co
)读数。
>>>
>>> df.groupby(day_names)["co"].mean()
tstamp
Friday 2.543
Monday 2.017
Saturday 1.861
Sunday 1.438
Thursday 2.456
Tuesday 2.382
Wednesday 2.401
Name: co, dtype: float64
拆分-应用-合并过程的表现与之前的大体相同,只是这次的拆分是在一个人为创建的列上进行的。这个列并不存在于DataFrame本身,而是从它派生出来的。
如果你不仅想按星期分组,还想按小时分组呢?这个结果应该有7 * 24 = 168
观测值。为了达到这个目的,你可以传递一个类似数组的对象列表。在这种情况下,你将传递pandasInt64Index
对象。
>>>
>>> hr = df.index.hour
>>> df.groupby([day_names, hr])["co"].mean().rename_axis(["dow", "hr"])
dow hr
Friday 0 1.936
1 1.609
2 1.172
3 0.887
4 0.823
...
Wednesday 19 4.147
20 3.845
21 2.898
22 2.102
23 1.938
Name: co, Length: 168, dtype: float64
下面是一个更类似的例子,它使用 .cut()
来把温度值分成离散的区间。
>>>
>>> import pandas as pd
>>> bins = pd.cut(df["temp_c"], bins=3, labels=("cool", "warm", "hot"))
>>> df[["rel_hum", "abs_hum"]].groupby(bins).agg(["mean", "median"])
rel_hum abs_hum
mean median mean median
temp_c
cool 57.651 59.2 0.666 0.658
warm 49.383 49.3 1.183 1.145P
hot 24.994 24.1 1.293 1.274
在这种情况下,bins
实际上是一个Series
。
>>>
>>> type(bins)
<class 'pandas.core.series.Series'>
>>> bins.head()
tstamp
2004-03-10 18:00:00 cool
2004-03-10 19:00:00 cool
2004-03-10 20:00:00 cool
2004-03-10 21:00:00 cool
2004-03-10 22:00:00 cool
Name: temp_c, dtype: category
Categories (3, object): [cool < warm < hot]
不管它是一个Series
,NumPy数组,还是列表都不重要。重要的是bins
仍然作为一个标签序列,包括cool
,warm
,和hot
。如果你真的想,那么你也可以使用一个Categorical
数组,甚至是一个普通的list
。
- 本土 Python 列表。
df.groupby(bins.tolist())
- pandas
Categorical
数组。df.groupby(bins.values)
正如你所看到的,.groupby()
很聪明,可以处理很多不同的输入类型。任何一个都会产生相同的结果,因为它们都是作为一个标签序列来执行分组和分割的功能。
重新取样
你已经用df.groupby(day_names)["co"].mean()
,按星期分组了df
。现在考虑一些不同的东西。如果你想按观察值的年份和季度分组呢?这里有一个方法可以实现这个目的。
>>>
>>> # See an easier alternative below
>>> df.groupby([df.index.year, df.index.quarter])["co"].agg(
... ["max", "min"]
... ).rename_axis(["year", "quarter"])
max min
year quarter
2004 1 8.1 0.3
2 7.3 0.1
3 7.5 0.1
4 11.9 0.1
2005 1 8.7 0.1
2 5.0 0.3
这整个操作可以通过重新取样来表达。重新取样的用途之一是作为一个基于时间的分组。你所需要做的就是传递一个频率字符串,比如"Q"
用于"quarterly"
,pandas会完成剩下的工作。
>>>
>>> df.resample("Q")["co"].agg(["max", "min"])
max min
tstamp
2004-03-31 8.1 0.3
2004-06-30 7.3 0.1
2004-09-30 7.5 0.1
2004-12-31 11.9 0.1
2005-03-31 8.7 0.1
2005-06-30 5.0 0.3
通常,当你使用.resample()
,你可以用一种更简洁的方式来表达基于时间的分组操作。其结果可能会与更冗长的.groupby()
等价物有一点点不同,但你经常会发现,.resample()
给你带来的正是你所要的。
例3:新闻聚合器数据集
现在你将处理第三个也是最后一个数据集,它拥有几十万篇新闻文章的元数据,并将它们分组为主题集群。
# pandas_news.py
import pandas as pd
def parse_millisecond_timestamp(ts):
"""Convert ms since Unix epoch to UTC datetime instance."""
return pd.to_datetime(ts, unit="ms")
df = pd.read_csv(
"groupby-data/news.csv",
sep="\t",
header=None,
index_col=0,
names=["title", "url", "outlet", "category", "cluster", "host", "tstamp"],
parse_dates=["tstamp"],
date_parser=parse_millisecond_timestamp,
dtype={
"outlet": "category",
"category": "category",
"cluster": "category",
"host": "category",
},
)
为了以适当的dtype
,将数据读入内存,你需要一个辅助函数来解析时间戳列。这是因为它表示为自Unix纪元以来的毫秒数,而不是小数秒。
与你之前所做的类似,你可以使用分类dtype
来有效地编码那些相对于列的长度来说具有相对较少数量的唯一值的列。
数据集的每一行都包含标题、URL、出版机构的名称和域名,以及出版时间戳。cluster
是文章所属的主题集群的随机ID。category
是新闻类别,包含以下选项。
b
用于商业t
科学和技术e
娱乐m
为健康
这里是第一行。
>>>
>>> from pandas_news import df
>>> df.iloc[0]
title Fed official says weak data caused by weather,...
url http://www.latimes.com/business/money/la-fi-mo...
outlet Los Angeles Times
category b
cluster ddUyU0VZz0BRneMioxUPQVP6sIxvM
host www.latimes.com
tstamp 2014-03-10 16:52:50.698000
Name: 1, dtype: object
现在你已经对数据有了一丝了解,你可以开始对它提出更复杂的问题。
使用Lambda函数在.groupby()
这个数据集引来了很多更多潜在的问题。这里有一个随机但有意义的问题:哪些网点谈论美联储最多?为简单起见,假设这需要搜索对大小写敏感的"Fed"
。请记住,这可能会产生一些假阳性的术语,如"Federal government"
。
为了按出口计算提及次数,你可以在出口上调用.groupby()
,然后用Python的lambda函数在每组上调用一个函数.apply()
。
>>>
>>> df.groupby("outlet", sort=False)["title"].apply(
... lambda ser: ser.str.contains("Fed").sum()
... ).nlargest(10)
outlet
Reuters 161
NASDAQ 103
Businessweek 93
Investing.com 66
Wall Street Journal \(blog\) 61
MarketWatch 56
Moneynews 55
Bloomberg 53
GlobalPost 51
Economic Times 44
Name: title, dtype: int64
因为有几个方法的调用是连续进行的,所以我们把它分解一下。像以前一样,你可以通过从pandas GroupBy迭代器中提取第一个tuple
,来拉出第一个组和它相应的pandas对象。
>>>
>>> title, ser = next(iter(df.groupby("outlet", sort=False)["title"]))
>>> title
'Los Angeles Times'
>>> ser.head()
1 Fed official says weak data caused by weather,...
486 Stocks fall on discouraging news from Asia
1124 Clues to Genghis Khan's rise, written in the r...
1146 Elephants distinguish human voices by sex, age...
1237 Honda splits Acura into its own division to re...
Name: title, dtype: object
在这种情况下,ser
是一个pandasSeries
,而不是一个DataFrame
。这是因为你在调用.groupby()
之后又调用了["title"]
。这就有效地选择了每个子表中的单列。
接下来是.str.contains("Fed")
。这将返回一个布尔值 Series
,当一个文章标题在搜索中登记为匹配时,就会返回True
。当然,第一行以"Fed official says weak data caused by weather,..."
开始,并显示为True
。
>>>
>>> ser.str.contains("Fed")
1 True
486 False
1124 False
1146 False
1237 False
...
421547 False
421584 False
421972 False
422226 False
422905 False
Name: title, Length: 1976, dtype: bool
下一步是将.sum()
这个Series
。由于bool
在技术上只是int
的一个专门类型,你可以将Series
的True
和False
相加,就像你将1
和0
的序列相加一样。
>>>
>>> ser.str.contains("Fed").sum()
17
结果是数据集中《洛杉矶时报》提及"Fed"
的次数。同样的程序也适用于路透社、纳斯达克、《商业周刊》以及其他的一些媒体。
改善《洛杉矶时报》的性能.groupby()
现在再次回溯到.groupby().apply()
,看看为什么这种模式会是次优的。.apply()
可能发生的情况是,它将有效地在每个组上执行一个 Python 循环。虽然.groupby().apply()
模式可以提供一些灵活性,但它也会抑制pandas在其他方面使用其基于Cython的优化。
所有这一切都意味着,当你发现自己在考虑使用.apply()
时,问问自己是否有办法以矢量的方式来表达这个操作。在这种情况下,你可以利用.groupby()
,它不仅接受一个或多个列名,而且还接受许多类似数组的结构。
- 一个一维的NumPy数组
- 一个列表
- 一个pandas
Series
或Index
还要注意,.groupby()
是Series
的有效实例方法,而不仅仅是DataFrame
,所以你基本上可以颠倒分割逻辑。考虑到这一点,你可以首先构建一个Series
,该布尔值表示标题是否包含"Fed"
。
>>>
>>> mentions_fed = df["title"].str.contains("Fed")
>>> type(mentions_fed)
<class 'pandas.core.series.Series'>
现在,.groupby()
也是Series
的一种方法,所以你可以把一个Series
分到另一个上。
>>>
>>> import numpy as np
>>> mentions_fed.groupby(
... df["outlet"], sort=False
... ).sum().nlargest(10).astype(np.uintc)
outlet
Reuters 161
NASDAQ 103
Businessweek 93
Investing.com 66
Wall Street Journal \(blog\) 61
MarketWatch 56
Moneynews 55
Bloomberg 53
GlobalPost 51
Economic Times 44
Name: title, dtype: uint32
这两个Series
,不需要是同一个DataFrame
对象的列。它们只需要具有相同的形状。
>>>
>>> mentions_fed.shape
(422419,)
>>> df["outlet"].shape
(422419,)
最后,如果你决心要得到最紧凑的结果,你可以用np.uintc
,将结果投回无符号整数。下面是两个版本的正面比较,它们会产生相同的结果。
# pandas_news_performance.py
import timeit
import numpy as np
from pandas_news import df
def test_apply():
"""Version 1: using `.apply()`"""
df.groupby("outlet", sort=False)["title"].apply(
lambda ser: ser.str.contains("Fed").sum()
).nlargest(10)
def test_vectorization():
"""Version 2: using vectorization"""
mentions_fed = df["title"].str.contains("Fed")
mentions_fed.groupby(
df["outlet"], sort=False
).sum().nlargest(10).astype(np.uintc)
print(f"Version 1: {timeit.timeit(test_apply, number=3)}")
print(f"Version 2: {timeit.timeit(test_vectorization, number=3)}")
你使用timeit
模块来估计两个版本的运行时间。
现在,运行该脚本,看看两个版本的性能如何。
(venv) $ python pandas_news_performance.py
Version 1: 2.5422707499965327
Version 2: 0.3260433749965159
当运行三次时,test_apply()
函数需要2.54秒,而test_vectorization()
只需要0.33秒。对于几十万条记录来说,这是一个令人印象深刻的CPU时间差异。想想看,当你的数据集增长到几百万行的时候,这种差异会变得多么巨大啊
注意:为了简单起见,这个例子忽略了数据中的一些细节。也就是说,搜索词"Fed"
可能也会找到类似"Federal government"
的内容。
Series.str.contains()
如果你想花哨一点,使用涉及负数前瞻的表达式,那么 "搜索 "也需要一个编译的正则表达式作为参数。
你可能还想计算的不仅仅是提及的原始数量,而是提及的比例相对于一个新闻机构制作的所有文章。
pandas GroupBy。把它放在一起
如果你在pandas GroupBy对象上调用dir()
,那么你会看到有足够多的方法让你头晕目眩!这时,你就会发现,有很多方法都是不一样的。要记住pandas GroupBy对象的所有功能是很困难的。拨开迷雾的方法之一是将不同的方法划分为它们的作用和行为方式。
大体上,pandas GroupBy 对象的方法可以分为几个类别。
-
聚合方法(也叫还原方法)将许多数据点组合成一个关于这些数据点的聚合统计。一个例子是取十个数字的总和、平均数或中位数,其结果只是一个数字。
-
滤波方法返回给你的是原始
DataFrame
的一个子集。这最常见的意思是使用.filter()
,根据关于该组和其子表的一些比较统计数字,放弃整个组。在这个定义下,包括一些从每组中排除特定行的方法也是有意义的。 -
转换方法返回一个
DataFrame
,其形状和索引与原始的相同,但数值不同。通过聚合和过滤方法,得到的DataFrame
通常会比输入的DataFrame
小。而转换方法则不然,它对单个数值本身进行转换,但保留了原始DataFrame
的形状。 -
元方法不太关心你调用
.groupby()
的原始对象,而更注重于给你提供高层次的信息,如组的数量和这些组的索引。 -
绘图方法模仿了pandas
Series
或DataFrame
的绘图API,但通常将输出分成多个子图。
pandas GroupBy 对象的一些方法并不能很好地归入上述类别。这些方法通常会产生一个不是DataFrame
或Series
的中间对象。例如,df.groupby().rolling()
产生一个RollingGroupby
对象,然后你可以对其调用聚合、过滤或转换方法。
如果你想更深入地了解,那么,以下的API文档将为你提供帮助 DataFrame.groupby()
, DataFrame.resample()
,和 pandas.Grouper
的API文档是探索方法和对象的资源。
总结
在本教程中,你已经涵盖了大量关于 .groupby()
,包括它的设计、它的API,以及如何将方法串联起来以获得适合你的目的的数据结构。
你已经学会了。
- 如何在真实世界的数据上使用pandasGroupBy操作
- 拆分-应用-合并的操作链是如何运作的,以及如何将其分解为若干步骤
- pandas GroupBy对象的方法是如何根据其意图和结果进行分类的
.groupby()
,还有很多东西是你在一个教程中无法涵盖的。但希望本教程是进一步探索的一个好起点