前言
我们所说的 group by
主要涉及以下一个或多个步骤:
- 拆分:根据指定的标准对数据进行切割,并分为不同的组别
- 应用:分别在每个组中应用函数
- 组合:将所有的结果组合为数据结构
在这些步骤中,拆分是最直接的。而事实上,多数情况下,我们可能希望将数据集分成若干组,并对这些分组进行一些操作
在应用函数的步骤中,我们可能希望进行以下操作
- 聚合:为每个分组应用一个或多个汇总函数,例如:
- 计算分组的和或均值
- 计算分组的
sizes/counts
- 转换:为不同的分组执行不同的计算,并返回类似索引的对象,例如:
- 在组内进行标准化(
zscore
) - 填充每个分组中的
NA
值
- 在组内进行标准化(
- 筛选:过滤掉一些分组,例如:
- 丢弃元素数目较少的分组
- 根据组内的和或均值进行过滤
pandas
对象的 groupby
方法相较于 SQL
SELECT Column1, Column2, mean(Column3), sum(Column4)
FROM SomeTable
GROUP BY Column1, Column2
会更加简洁易用
1 将对象拆分为不同的组
pandas
对象可以在它的任何轴上进行分割。例如,使用如下代码创建 groupby
对象
In [1]: df = pd.DataFrame(
...: [
...: ("bird", "Falconiformes", 389.0),
...: ("bird", "Psittaciformes", 24.0),
...: ("mammal", "Carnivora", 80.2),
...: ("mammal", "Primates", np.nan),
...: ("mammal", "Carnivora", 58),
...: ],
...: index=["falcon", "parrot", "lion", "monkey", "leopard"],
...: columns=("class", "order", "max_speed"),
...: )
...:
In [2]: df
Out[2]:
class order max_speed
falcon bird Falconiformes 389.0
parrot bird Psittaciformes 24.0
lion mammal Carnivora 80.2
monkey mammal Primates NaN
leopard mammal Carnivora 58.0
# default is axis=0
In [3]: grouped = df.groupby("class")
In [4]: grouped = df.groupby("order", axis="columns")
In [5]: grouped = df.groupby(["class", "order"])
可以使用如下方法进行拆分:
- 函数,可以对轴标签进行调用
- 列表或数组,长度与选择的轴一致
- 字典或
Series
,存在label-> group name
映射 - 对于
DataFrame
对象,传入列名或索引级别名字符串 df.groupby('A')
是df.groupby(df['A'])
的语法糖- 上面任意组合的列表
注意:如果传入的字符串既匹配列名,又匹配索引级别名,会引发异常
In [6]: df = pd.DataFrame(
...: {
...: "A": ["foo", "bar", "foo", "bar", "foo", "bar", "foo", "foo"],
...: "B": ["one", "one", "two", "three", "two", "two", "one", "three"],
...: "C": np.random.randn(8),
...: "D": np.random.randn(8),
...: }
...: )
...:
In [7]: df
Out[7]:
A B C D
0 foo one 0.469112 -0.861849
1 bar one -0.282863 -2.104569
2 foo two -1.509059 -0.494929
3 bar three -1.135632 1.071804
4 foo two 1.212112 0.721555
5 bar two -0.173215 -0.706771
6 foo one 0.119209 -1.039575
7 foo three -1.044236 0.271860
对于 DataFrame
对象,可以使用 groupby()
获取一个 GroupBy
对象。我们可以根据 A
或 B
列进行分组
In [8]: grouped = df.groupby("A")
In [9]: grouped = df.groupby(["A", "B"])
如果我们把 A
、B
作为层次索引,则可以选择相应的 level
进行分组
In [10]: df2 = df.set_index(["A", "B"])
In [11]: grouped = df2.groupby(level=df2.index.names.difference(["B"]))
In [12]: grouped.sum()
Out[12]:
C D
A
bar -1.591710 -1.739537
foo -0.752861 -1.402938
我们也可以根据列来拆分数据
In [13]: def get_letter_type(letter):
....: if letter.lower() in 'aeiou':
....: return 'vowel'
....: else:
....: return 'consonant'
....:
In [14]: grouped = df.groupby(get_letter_type, axis=1)
pandas
的 Index
对象支持重复的索引。因此,可以对包含重复值的索引进行分组,相同的索引会被分为同一组
In [15]: lst = [1, 2, 3, 1, 2, 3]
In [16]: s = pd.Series([1, 2, 3, 10, 20, 30], lst)
In [17]: grouped = s.groupby(level=0)
In [18]: grouped.first()
Out[18]:
1 1
2 2
3 3
dtype: int64
In [19]: grouped.last()
Out[19]:
1 10
2 20
3 30
dtype: int64
In [20]: grouped.sum()
Out[20]:
1 11
2 22
3 33
dtype: int64
注意:只有在需要的时候,才会对数据进行拆分
1.1 排序
默认情况下,groupby
会对分组键进行排序,可以使用 sort=False
来加速该操作
In [21]: df2 = pd.DataFrame({"X": ["B", "B", "A", "A"], "Y": [1, 2, 3, 4]})
In [22]: df2.groupby(["X"]).sum()
Out[22]:
Y
X
A 7
B 3
In [23]: df2.groupby(["X"], sort=False).sum()
Out[23]:
Y
X
B 3
A 7
注意:设置不排序之后,groupby
将会按照每个分组在原始数据中的出现顺序排序
In [24]: df3 = pd.DataFrame({"X": ["A", "B", "A", "B"], "Y": [1, 4, 3, 2]})
In [25]: df3.groupby(["X"]).get_group("A")
Out[25]:
X Y
0 A 1
2 A 3
In [26]: df3.groupby(["X"]).get_group("B")
Out[26]:
X Y
1 B 4
3 B 2
dropna
默认情况下,groupby
操作会忽略 NA
值,可以使用 dropna=False
来保留 NA
值
In [27]: df_list = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]]
In [28]: df_dropna = pd.DataFrame(df_list, columns=["a", "b", "c"])
In [29]: df_dropna
Out[29]:
a b c
0 1 2.0 3
1 1 NaN 4
2 2 1.0 3
3 1 2.0 2
# 默认忽略 NA 值
In [30]: df_dropna.groupby(by=["b"], dropna=True).sum()
Out[30]:
a c
b
1.0 2 3
2.0 2 5
# dropna=False,保留 NA 值
In [31]: df_dropna.groupby(by=["b"], dropna=False).sum()
Out[31]:
a c
b
1.0 2 3
2.0 2 5
NaN 1 4
1.2 对象属性
groups
的属性是一个字典,键为每个分组的名称,值为每个组的轴标签。例如
In [32]: df.groupby("A").groups
Out[32]: {'bar': [1, 3, 5], 'foo': [0, 2, 4, 6, 7]}
In [33]: df.groupby(get_letter_type, axis=1).groups
Out[33]: {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}
对 group
对象使用 len
函数,将返回 groups
对象字典的长度
In [34]: grouped = df.groupby(["A", "B"])
In [35]: grouped.groups
Out[35]: {('bar', 'one'): [1], ('bar', 'three'): [3], ('bar', 'two'): [5], ('foo', 'one'): [0, 6], ('foo', 'three'): [7], ('foo', 'two'): [2, 4]}
In [36]: len(grouped)
Out[36]: 6
1.3 MultiIndex
对于层次索引,可以按照索引的某一 level
进行分组
我们先创建一个 MultiIndex
In [40]: arrays = [
....: ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
....: ["one", "two", "one", "two", "one", "two", "one", "two"],
....: ]
....:
In [41]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
In [42]: s = pd.Series(np.random.randn(8), index=index)
In [43]: s
Out[43]:
first second
bar one -0.919854
two -0.042379
baz one 1.247642
two -0.009920
foo one 0.290213
two 0.495767
qux one 0.362949
two 1.548106
dtype: float64
可以对 s
的某一个 level
进行分组,如 level=0
In [44]: grouped = s.groupby(level=0)
In [45]: grouped.sum()
Out[45]:
first
bar -0.962232
baz 1.237723
foo 0.785980
qux 1.911055
dtype: float64
如果 MultiIndex
指定了层级的名称,可以用这些来代替数字编号
In [46]: s.groupby(level="second").sum()
Out[46]:
second
one 0.980950
two 1.991575
dtype: float64
向 sum
这种聚合函数,可以直接传入 level
参数,其返回结果中的索引将是相应 level
的分组
In [47]: s.sum(level="second")
Out[47]:
second
one 0.980950
two 1.991575
dtype: float64
也可以传入多个 level
进行分组
In [48]: s
Out[48]:
first second third
bar doo one -1.131345
two -0.089329
baz bee one 0.337863
two -0.945867
foo bop one -0.932132
two 1.956030
qux bop one 0.017587
two -0.016692
dtype: float64
In [49]: s.groupby(level=["first", "second"]).sum()
Out[49]:
first second
bar doo -1.220674
baz bee -0.608004
foo bop 1.023898
qux bop 0.000895
dtype: float64
也可以直接作为键传入
In [50]: s.groupby(["first", "second"]).sum()
Out[50]:
first second
bar doo -1.220674
baz bee -0.608004
foo bop 1.023898
qux bop 0.000895
dtype: float64
1.4 根据索引 level 和列进行分组
DataFrame
可以通过同时指定列名和索引级别进行分组,其中列名传入的是字符串,索引级别传入的是 pd.Grouper
对象
例如,有如下数据
In [51]: arrays = [
....: ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
....: ["one", "two", "one", "two", "one", "two", "one", "two"],
....: ]
....:
In [52]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
In [53]: df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 3, 3], "B": np.arange(8)}, index=index)
In [54]: df
Out[54]:
A B
first second
bar one 1 0
two 1 1
baz one 1 2
two 1 3
foo one 2 4
two 2 5
qux one 3 6
two 3 7
我们可以根据 level=1
和 A
列进行分组
In [55]: df.groupby([pd.Grouper(level=1), "A"]).sum()
Out[55]:
B
second A
one 1 2
2 4
3 6
two 1 4
2 5
3 7
也可以直接传入层级名称
In [56]: df.groupby([pd.Grouper(level="second"), "A"]).sum()
Out[56]:
B
second A
one 1 2
2 4
3 6
two 1 4
2 5
3 7
也可以用更简洁的方式
In [57]: df.groupby(["second", "A"]).sum()
Out[57]:
B
second A
one 1 2
2 4
3 6
two 1 4
2 5
3 7
1.5 选择分组的列
在创建了 GroupBy
对象之后,可能需要对不同的列进行不同的操作,可以使用 []
类似从 DataFrame
中获取列的方式来进行操作
In [58]: grouped = df.groupby(["A"])
In [59]: grouped_C = grouped["C"]
In [60]: grouped_D = grouped["D"]
这种语法糖主要是为了替换下面这样冗长的代码
In [61]: df["C"].groupby(df["A"])
Out[61]: <pandas.core.groupby.generic.SeriesGroupBy object at 0x7fd2f6794610>
2 遍历分组
创建了 GroupBy
对象之后,可以很容易对其进行遍历
In [62]: grouped = df.groupby('A')
In [63]: for name, group in grouped:
....: print(name)
....: print(group)
....:
bar
A B C D
1 bar one 0.254161 1.511763
3 bar three 0.215897 -0.990582
5 bar two -0.077118 1.211526
foo
A B C D
0 foo one -0.575247 1.346061
2 foo two -1.143704 1.627081
4 foo two 1.193555 -0.441652
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
如果是对多个键进行分组,那么组名将是一个元组
In [64]: for name, group in df.groupby(['A', 'B']):
....: print(name)
....: print(group)
....:
('bar', 'one')
A B C D
1 bar one 0.254161 1.511763
('bar', 'three')
A B C D
3 bar three 0.215897 -0.990582
('bar', 'two')
A B C D
5 bar two -0.077118 1.211526
('foo', 'one')
A B C D
0 foo one -0.575247 1.346061
6 foo one -0.408530 0.268520
('foo', 'three')
A B C D
7 foo three -0.862495 0.02458
('foo', 'two')
A B C D
2 foo two -1.143704 1.627081
4 foo two 1.193555 -0.441652
3 选择分组
可以使用 get_group()
选择一个分组
In [65]: grouped.get_group("bar")
Out[65]:
A B C D
1 bar one 0.254161 1.511763
3 bar three 0.215897 -0.990582
5 bar two -0.077118 1.211526
对于多列的分组,需要传递元组
In [66]: df.groupby(["A", "B"]).get_group(("bar", "one"))
Out[66]:
A B C D
1 bar one 0.254161 1.511763
4 聚合
对数据分组完后,可以使用一些函数对分组数据进行计算
最常用的就是 aggregate()
(等于 agg()
) 方法
In [67]: grouped = df.groupby("A")
In [68]: grouped.aggregate(np.sum)
Out[68]:
C D
A
bar 0.392940 1.732707
foo -1.796421 2.824590
In [69]: grouped = df.groupby(["A", "B"])
In [70]: grouped.aggregate(np.sum)
Out[70]:
C D
A B
bar one 0.254161 1.511763
three 0.215897 -0.990582
two -0.077118 1.211526
foo one -0.983776 1.614581
three -0.862495 0.024580
two 0.049851 1.185429
如你所见,聚合的结果将以组名作为分组轴的新索引。
在有多个分组键的情况下,结果默认是一个 MultiIndex
,可以使用 as_index
参数来改变这一行为
In [71]: grouped = df.groupby(["A", "B"], as_index=False)
In [72]: grouped.aggregate(np.sum)
Out[72]:
A B C D
0 bar one 0.254161 1.511763
1 bar three 0.215897 -0.990582
2 bar two -0.077118 1.211526
3 foo one -0.983776 1.614581
4 foo three -0.862495 0.024580
5 foo two 0.049851 1.185429
In [73]: df.groupby("A", as_index=False).sum()
Out[73]:
A C D
0 bar 0.392940 1.732707
1 foo -1.796421 2.824590
当然,也可以使用 reset_index
达到相同的效果
In [74]: df.groupby(["A", "B"]).sum().reset_index()
Out[74]:
A B C D
0 bar one 0.254161 1.511763
1 bar three 0.215897 -0.990582
2 bar two -0.077118 1.211526
3 foo one -0.983776 1.614581
4 foo three -0.862495 0.024580
5 foo two 0.049851 1.185429
另一个简单的例子是,计算每个分组的大小。
In [75]: grouped.size()
Out[75]:
A B size
0 bar one 1
1 bar three 1
2 bar two 1
3 foo one 2
4 foo three 1
5 foo two 2
计算每个分组的基本统计信息
In [76]: grouped.describe()
Out[76]:
C ... D
count mean std min 25% 50% ... std min 25% 50% 75% max
0 1.0 0.254161 NaN 0.254161 0.254161 0.254161 ... NaN 1.511763 1.511763 1.511763 1.511763 1.511763
1 1.0 0.215897 NaN 0.215897 0.215897 0.215897 ... NaN -0.990582 -0.990582 -0.990582 -0.990582 -0.990582
2 1.0 -0.077118 NaN -0.077118 -0.077118 -0.077118 ... NaN 1.211526 1.211526 1.211526 1.211526 1.211526
3 2.0 -0.491888 0.117887 -0.575247 -0.533567 -0.491888 ... 0.761937 0.268520 0.537905 0.807291 1.076676 1.346061
4 1.0 -0.862495 NaN -0.862495 -0.862495 -0.862495 ... NaN 0.024580 0.024580 0.024580 0.024580 0.024580
5 2.0 0.024925 1.652692 -1.143704 -0.559389 0.024925 ... 1.462816 -0.441652 0.075531 0.592714 1.109898 1.627081
[6 rows x 16 columns]
可以使用 nunique
计算每个分组中唯一值的数量,与 value_counts
类似,但是它只计算唯一值
In [77]: ll = [['foo', 1], ['foo', 2], ['foo', 2], ['bar', 1], ['bar', 1]]
In [78]: df4 = pd.DataFrame(ll, columns=["A", "B"])
In [79]: df4
Out[79]:
A B
0 foo 1
1 foo 2
2 foo 2
3 bar 1
4 bar 1
In [80]: df4.groupby("A")["B"].nunique()
Out[80]:
A
bar 1
foo 2
Name: B, dtype: int64
聚合函数可以对数据进行降维,下面是一些常用的聚合函数:
上面的函数都会忽略 NA
值。任何一个能够将 Series
转化为标量值的函数都可以作为聚合函数
4.1 同时应用多个函数
对于一个分组的 Series
,可以传入一个函数列表或者字典,并输出一个 DataFrame
In [81]: grouped = df.groupby("A")
In [82]: grouped["C"].agg([np.sum, np.mean, np.std])
Out[82]:
sum mean std
A
bar 0.392940 0.130980 0.181231
foo -1.796421 -0.359284 0.912265
如果分组是 DataFrame
,传入一个函数列表或字典,将会得到一个层次索引
In [83]: grouped.agg([np.sum, np.mean, np.std])
Out[83]:
C D
sum mean std sum mean std
A
bar 0.392940 0.130980 0.181231 1.732707 0.577569 1.366330
foo -1.796421 -0.359284 0.912265 2.824590 0.564918 0.884785
对于 Series
分组返回结果的列名默认是函数名,可以使用链式操作修改列名
In [84]: (
....: grouped["C"]
....: .agg([np.sum, np.mean, np.std])
....: .rename(columns={"sum": "foo", "mean": "bar", "std": "baz"})
....: )
....:
Out[84]:
foo bar baz
A
bar 0.392940 0.130980 0.181231
foo -1.796421 -0.359284 0.912265
对于 DataFrame
分组,操作类似
In [85]: (
....: grouped.agg([np.sum, np.mean, np.std]).rename(
....: columns={"sum": "foo", "mean": "bar", "std": "baz"}
....: )
....: )
....:
Out[85]:
C D
foo bar baz foo bar baz
A
bar 0.392940 0.130980 0.181231 1.732707 0.577569 1.366330
foo -1.796421 -0.359284 0.912265 2.824590 0.564918 0.884785
注意:
通常,输出的列名是唯一的,不能将同一个函数或两个同名函数应用于同一列
In [86]: grouped["C"].agg(["sum", "sum"])
Out[86]:
sum sum
A
bar 0.392940 0.392940
foo -1.796421 -1.796421
如果传入的是多个 lambda
函数,pandas
会自动为这些函数重命名为 <lambda_i>
In [87]: grouped["C"].agg([lambda x: x.max() - x.min(), lambda x: x.median() - x.mean()])
Out[87]:
<lambda_0> <lambda_1>
A
bar 0.331279 0.084917
foo 2.337259 -0.215962
4.2 命名聚合
GroupBy.agg()
中接受一种特殊的语法,用于控制输出的列名以及特定列的聚合操作,即命名聚合
- 关键字就是输出的列名
- 值是元组的形式,第一个元素是要选择的列,第二个元素为对该列执行的操作。
pandas
提供了pandas.NamedAgg
命名元组,其字段为['column', 'aggfunc']
,是参数设置更加清晰
通常,聚合函数可以是可调用函数或字符串函数名
In [88]: animals = pd.DataFrame(
....: {
....: "kind": ["cat", "dog", "cat", "dog"],
....: "height": [9.1, 6.0, 9.5, 34.0],
....: "weight": [7.9, 7.5, 9.9, 198.0],
....: }
....: )
....:
In [89]: animals
Out[89]:
kind height weight
0 cat 9.1 7.9
1 dog 6.0 7.5
2 cat 9.5 9.9
3 dog 34.0 198.0
In [90]: animals.groupby("kind").agg(
....: min_height=pd.NamedAgg(column="height", aggfunc="min"),
....: max_height=pd.NamedAgg(column="height", aggfunc="max"),
....: average_weight=pd.NamedAgg(column="weight", aggfunc=np.mean),
....: )
....:
Out[90]:
min_height max_height average_weight
kind
cat 9.1 9.5 8.90
dog 6.0 34.0 102.75
pandas.NamedAgg
只是 namedtuple
,与直接传入元组等价
In [91]: animals.groupby("kind").agg(
....: min_height=("height", "min"),
....: max_height=("height", "max"),
....: average_weight=("weight", np.mean),
....: )
....:
Out[91]:
min_height max_height average_weight
kind
cat 9.1 9.5 8.90
dog 6.0 34.0 102.75
如果你的列名不是有效的 Python
关键字,可以构建一个字典并解包
In [92]: animals.groupby("kind").agg(
....: **{
....: "total weight": pd.NamedAgg(column="weight", aggfunc=sum)
....: }
....: )
....:
Out[92]:
total weight
kind
cat 17.8
dog 205.5
注意:在 Python 3.5
或更早的版本,**kwargs
不会保留键的顺序
Series
也可以使用命名聚合,因为 Series
不需要选择列,所以值就只是函数或字符串函数名
In [93]: animals.groupby("kind").height.agg(
....: min_height="min",
....: max_height="max",
....: )
....:
Out[93]:
min_height max_height
kind
cat 9.1 9.5
dog 6.0 34.0
4.3 为列应用不同的函数
通过对 aggregate
传递一个字典,你可以为不同的列应用不同的函数
In [94]: grouped.agg({"C": np.sum, "D": lambda x: np.std(x, ddof=1)})
Out[94]:
C D
A
bar 0.392940 1.366330
foo -1.796421 0.884785
函数名也可以是字符串,但是使用前必须定义了该函数
In [95]: grouped.agg({"C": "sum", "D": "std"})
Out[95]:
C D
A
bar 0.392940 1.366330
foo -1.796421 0.884785
5 转换
transform
方法返回一个与分组对象索引相同(大小相同)的对象,该函数必须:
- 返回一个与分组大小相同或可广播到分组大小的结果。例如,一个标量,
grouped.transform(lambda x: x.iloc[-1])
- 在组上逐列操作
- 不能执行原地修改操作
例如,对数据进行分组标准化
In [98]: index = pd.date_range("10/1/1999", periods=1100)
In [99]: ts = pd.Series(np.random.normal(0.5, 2, 1100), index)
In [100]: ts = ts.rolling(window=100, min_periods=100).mean().dropna()
In [101]: ts.head()
Out[101]:
2000-01-08 0.779333
2000-01-09 0.778852
2000-01-10 0.786476
2000-01-11 0.782797
2000-01-12 0.798110
Freq: D, dtype: float64
In [102]: ts.tail()
Out[102]:
2002-09-30 0.660294
2002-10-01 0.631095
2002-10-02 0.673601
2002-10-03 0.709213
2002-10-04 0.719369
Freq: D, dtype: float64
In [103]: transformed = ts.groupby(lambda x: x.year).transform(
.....: lambda x: (x - x.mean()) / x.std()
.....: )
.....:
标准化之后,均值为 0
,方差为 1
# 原始数据
In [104]: grouped = ts.groupby(lambda x: x.year)
In [105]: grouped.mean()
Out[105]:
2000 0.442441
2001 0.526246
2002 0.459365
dtype: float64
In [106]: grouped.std()
Out[106]:
2000 0.131752
2001 0.210945
2002 0.128753
dtype: float64
# 转换后的数据
In [107]: grouped_trans = transformed.groupby(lambda x: x.year)
In [108]: grouped_trans.mean()
Out[108]:
2000 1.167126e-15
2001 2.190637e-15
2002 1.088580e-15
dtype: float64
In [109]: grouped_trans.std()
Out[109]:
2000 1.0
2001 1.0
2002 1.0
dtype: float64
我们可以对比一下转换前后的数据分布
In [110]: compare = pd.DataFrame({"Original": ts, "Transformed": transformed})
In [111]: compare.plot()
如果转换函数返回的是维度更低的结果,则将会把值进行广播,使其与输入数组大小一样
In [112]: ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
Out[112]:
2000-01-08 0.623893
2000-01-09 0.623893
2000-01-10 0.623893
2000-01-11 0.623893
2000-01-12 0.623893
...
2002-09-30 0.558275
2002-10-01 0.558275
2002-10-02 0.558275
2002-10-03 0.558275
2002-10-04 0.558275
Freq: D, Length: 1001, dtype: float64
也可以使用内置函数,生成一样的结果
In [113]: max = ts.groupby(lambda x: x.year).transform("max")
In [114]: min = ts.groupby(lambda x: x.year).transform("min")
In [115]: max - min
Out[115]:
2000-01-08 0.623893
2000-01-09 0.623893
2000-01-10 0.623893
2000-01-11 0.623893
2000-01-12 0.623893
...
2002-09-30 0.558275
2002-10-01 0.558275
2002-10-02 0.558275
2002-10-03 0.558275
2002-10-04 0.558275
Freq: D, Length: 1001, dtype: float64
另一种常用的数据转换是,使用组内均值对缺失值进行填补。
例如,有如下数据
>>> data_df = pd.DataFrame(np.random.normal(0.5, 2, size=(1000, 3)), columns=list('ABC'))
>>> data_df.loc[np.random.randint(0, 1000, 100), 'C'] = np.NaN
>>> data_df
A B C
0 -1.807563 0.742651 0.582211
1 -0.004608 -0.252184 -0.599312
2 -0.682971 2.702668 1.314856
3 1.074685 0.203833 -2.223385
4 1.296123 2.436668 2.844688
.. ... ... ...
995 -2.413651 3.576030 0.209219
996 -0.501723 -0.510921 0.247469
997 -0.944480 -0.244293 -1.765085
998 -0.121340 -0.633210 -2.152916
999 -0.699248 -3.046279 1.562404
[1000 rows x 3 columns]
补缺失值
In [117]: countries = np.array(["US", "UK", "GR", "JP"])
In [118]: key = countries[np.random.randint(0, 4, 1000)]
In [119]: grouped = data_df.groupby(key)
# Non-NA count in each group
In [120]: grouped.count()
Out[120]:
A B C
GR 248 248 236
JP 243 243 213
UK 269 269 234
US 240 240 220
In [121]: transformed = grouped.transform(lambda x: x.fillna(x.mean()))
我们可以进行验证,转换前后均值并没有发生变化,但是,转换后已经不包含缺失值了
In [122]: grouped_trans = transformed.groupby(key)
In [123]: grouped.mean() # 原始数据的分组均值
Out[123]:
A B C
GR 0.744238 0.682563 0.671818
JP 0.637438 0.406635 0.476871
UK 0.343826 0.649000 0.489756
US 0.430899 0.276287 0.624809
In [124]: grouped_trans.mean() # 转换后均值不变
Out[124]:
A B C
GR 0.744238 0.682563 0.671818
JP 0.637438 0.406635 0.476871
UK 0.343826 0.649000 0.489756
US 0.430899 0.276287 0.624809
In [125]: grouped.count() # 转换前,行数不一致
Out[125]:
A B C
GR 248 248 236
JP 243 243 213
UK 269 269 234
US 240 240 220
In [126]: grouped_trans.count() # 转换后,行数一致
Out[126]:
A B C
GR 248 248 248
JP 243 243 243
UK 269 269 269
US 240 240 240
In [127]: grouped_trans.size() # count 与 size 的结果一致,不存在缺失值
Out[127]:
GR 248
JP 243
UK 269
US 240
dtype: int64
6 过滤
filter
方法可以返回原始对象的子集.
例如,我们想提取分组内的和大于 3
的所有分组的元素
In [136]: sf = pd.Series([1, 1, 2, 3, 3, 3])
In [137]: sf.groupby(sf).filter(lambda x: x.sum() > 2)
Out[137]:
3 3
4 3
5 3
dtype: int64
filter
的参数必须是一个函数,函数参数是每个分组,并且返回 True
或 False
例如,提取元素个数大于 2
的分组
In [138]: dff = pd.DataFrame({"A": np.arange(8), "B": list("aabbbbcc")})
In [139]: dff.groupby("B").filter(lambda x: len(x) > 2)
Out[139]:
A B
2 2 b
3 3 b
4 4 b
5 5 b
另外,我们也可以过滤掉不满足条件的组,而是返回一个类似索引对象。在这个对象中,没有通过的分组的元素被 NaN
填充
In [140]: dff.groupby("B").filter(lambda x: len(x) > 2, dropna=False)
Out[140]:
A B
0 NaN NaN
1 NaN NaN
2 2.0 b
3 3.0 b
4 4.0 b
5 5.0 b
6 NaN NaN
7 NaN NaN
对于具有多列的 DataFrames
,过滤器应明确指定一列作为过滤条件
In [141]: dff["C"] = np.arange(8)
In [142]: dff.groupby("B").filter(lambda x: len(x["C"]) > 2)
Out[142]:
A B C
2 2 b 2
3 3 b 3
4 4 b 4
5 5 b 5
7 分派实例方法
在进行聚合或转换时,你可能想对每个分组调用一个实例方法,例如
In [144]: grouped = df.groupby("A")
In [145]: grouped.agg(lambda x: x.std())
Out[145]:
C D
A
bar 0.181231 1.366330
foo 0.912265 0.884785
但是,如果需要传递额外的参数时,它会变得很冗长。我们可以直接使用分派到组对象上的方法
In [146]: grouped.std()
Out[146]:
C D
A
bar 0.181231 1.366330
foo 0.912265 0.884785
实际上这生成了一个函数包装器,在调用时,它接受所有传递的参数,并在每个分组上进行调用。
然后,这个结果可以和 agg
和 transform
结合在一起使用
In [147]: tsdf = pd.DataFrame(
.....: np.random.randn(1000, 3),
.....: index=pd.date_range("1/1/2000", periods=1000),
.....: columns=["A", "B", "C"],
.....: )
.....:
In [148]: tsdf.iloc[::2] = np.nan
In [149]: grouped = tsdf.groupby(lambda x: x.year)
In [150]: grouped.fillna(method="pad")
Out[150]:
A B C
2000-01-01 NaN NaN NaN
2000-01-02 -0.353501 -0.080957 -0.876864
2000-01-03 -0.353501 -0.080957 -0.876864
2000-01-04 0.050976 0.044273 -0.559849
2000-01-05 0.050976 0.044273 -0.559849
... ... ... ...
2002-09-22 0.005011 0.053897 -1.026922
2002-09-23 0.005011 0.053897 -1.026922
2002-09-24 -0.456542 -1.849051 1.559856
2002-09-25 -0.456542 -1.849051 1.559856
2002-09-26 1.123162 0.354660 1.128135
[1000 rows x 3 columns]
在上面的例子中,我们按照年份分组,然后对每个分组中使用 fillna
补缺失值
nlargest
和 nsmallest
可以在 Series
类型的 groupby
上使用
In [151]: s = pd.Series([9, 8, 7, 5, 19, 1, 4.2, 3.3])
In [152]: g = pd.Series(list("abababab"))
In [153]: gb = s.groupby(g)
In [154]: gb.nlargest(3)
Out[154]:
a 4 19.0
0 9.0
2 7.0
b 1 8.0
3 5.0
7 3.3
dtype: float64
In [155]: gb.nsmallest(3)
Out[155]:
a 6 4.2
2 7.0
0 9.0
b 5 1.0
7 3.3
3 5.0
dtype: float64
8 灵活的 apply
对分组数据的某些操作可能并不适合聚合或转换。或者说,你可能只是想让 GroupBy
来推断如何合并结果
我们可以使用 apply
函数,例如
In [156]: df
Out[156]:
A B C D
0 foo one -0.575247 1.346061
1 bar one 0.254161 1.511763
2 foo two -1.143704 1.627081
3 bar three 0.215897 -0.990582
4 foo two 1.193555 -0.441652
5 bar two -0.077118 1.211526
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
In [157]: grouped = df.groupby("A")
# 也可以直接使用 .describe()
In [158]: grouped["C"].apply(lambda x: x.describe())
Out[158]:
A
bar count 3.000000
mean 0.130980
std 0.181231
min -0.077118
25% 0.069390
...
foo min -1.143704
25% -0.862495
50% -0.575247
75% -0.408530
max 1.193555
Name: C, Length: 16, dtype: float64
改变返回结果的维度
In [159]: grouped = df.groupby('A')['C']
In [160]: def f(group):
.....: return pd.DataFrame({'original': group,
.....: 'demeaned': group - group.mean()})
.....:
In [161]: grouped.apply(f)
Out[161]:
original demeaned
0 -0.575247 -0.215962
1 0.254161 0.123181
2 -1.143704 -0.784420
3 0.215897 0.084917
4 1.193555 1.552839
5 -0.077118 -0.208098
6 -0.408530 -0.049245
7 -0.862495 -0.503211
在 Series
上使用 apply
类似
In [162]: def f(x):
.....: return pd.Series([x, x ** 2], index=["x", "x^2"])
.....:
In [163]: s = pd.Series(np.random.rand(5))
In [164]: s
Out[164]:
0 0.321438
1 0.493496
2 0.139505
3 0.910103
4 0.194158
dtype: float64
In [165]: s.apply(f)
Out[165]:
x x^2
0 0.321438 0.103323
1 0.493496 0.243538
2 0.139505 0.019462
3 0.910103 0.828287
4 0.194158 0.037697
9 其他有用的特征
9.1 自动排除某些列
对于之前的示例数据
In [166]: df
Out[166]:
A B C D
0 foo one -0.575247 1.346061
1 bar one 0.254161 1.511763
2 foo two -1.143704 1.627081
3 bar three 0.215897 -0.990582
4 foo two 1.193555 -0.441652
5 bar two -0.077118 1.211526
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
假设,我们想按 A
分组并计算组内的标准差,但是 B
列的数据我们并不关心。
如果我们的函数不能应用于某些列,则会隐式的删除这些列,所以
In [167]: df.groupby("A").std()
Out[167]:
C D
A
bar 0.181231 1.366330
foo 0.912265 0.884785
直接计算标准差并不会报错
9.2 使用有序因子进行分组
可以使用分类变量进行分组,分组的顺序会按照分类变量的顺序
In [177]: data = pd.Series(np.random.randn(100))
In [178]: factor = pd.qcut(data, [0, 0.25, 0.5, 0.75, 1.0])
In [179]: data.groupby(factor).mean()
Out[179]:
(-2.645, -0.523] -1.362896
(-0.523, 0.0296] -0.260266
(0.0296, 0.654] 0.361802
(0.654, 2.21] 1.073801
dtype: float64
9.3 使用 grouper 分组
可以使用 pd.Grouper
控制分组,对于如下数据
In [180]: import datetime
In [181]: df = pd.DataFrame(
.....: {
.....: "Branch": "A A A A A A A B".split(),
.....: "Buyer": "Carl Mark Carl Carl Joe Joe Joe Carl".split(),
.....: "Quantity": [1, 3, 5, 1, 8, 1, 9, 3],
.....: "Date": [
.....: datetime.datetime(2013, 1, 1, 13, 0),
.....: datetime.datetime(2013, 1, 1, 13, 5),
.....: datetime.datetime(2013, 10, 1, 20, 0),
.....: datetime.datetime(2013, 10, 2, 10, 0),
.....: datetime.datetime(2013, 10, 1, 20, 0),
.....: datetime.datetime(2013, 10, 2, 10, 0),
.....: datetime.datetime(2013, 12, 2, 12, 0),
.....: datetime.datetime(2013, 12, 2, 14, 0),
.....: ],
.....: }
.....: )
.....:
In [182]: df
Out[182]:
Branch Buyer Quantity Date
0 A Carl 1 2013-01-01 13:00:00
1 A Mark 3 2013-01-01 13:05:00
2 A Carl 5 2013-10-01 20:00:00
3 A Carl 1 2013-10-02 10:00:00
4 A Joe 8 2013-10-01 20:00:00
5 A Joe 1 2013-10-02 10:00:00
6 A Joe 9 2013-12-02 12:00:00
7 B Carl 3 2013-12-02 14:00:00
可以按照一定的频率对特定列进行分组,就像重抽样一样
In [183]: df.groupby([pd.Grouper(freq="1M", key="Date"), "Buyer"]).sum()
Out[183]:
Quantity
Date Buyer
2013-01-31 Carl 1
Mark 3
2013-10-31 Carl 6
Joe 9
2013-12-31 Carl 3
Joe 9
可以分别对列或索引进行分组
In [184]: df = df.set_index("Date")
In [185]: df["Date"] = df.index + pd.offsets.MonthEnd(2)
In [186]: df.groupby([pd.Grouper(freq="6M", key="Date"), "Buyer"]).sum()
Out[186]:
Quantity
Date Buyer
2013-02-28 Carl 1
Mark 3
2014-02-28 Carl 9
Joe 18
In [187]: df.groupby([pd.Grouper(freq="6M", level="Date"), "Buyer"]).sum()
Out[187]:
Quantity
Date Buyer
2013-01-31 Carl 1
Mark 3
2014-01-31 Carl 9
Joe 18
9.4 获取分组的第一行
类似于 Series
和 DataFrame
,可以使用 head
和 tail
获取分组前后几行
In [188]: df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=["A", "B"])
In [189]: df
Out[189]:
A B
0 1 2
1 1 4
2 5 6
In [190]: g = df.groupby("A")
In [191]: g.head(1)
Out[191]:
A B
0 1 2
2 5 6
In [192]: g.tail(1)
Out[192]:
A B
1 1 4
2 5 6
9.5 获取每组的第 n 行
在 Series
或 DataFrame
中可以使用 nth()
来获取第 n
个元素,也可以用于获取每个分组的某一行
In [193]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])
In [194]: g = df.groupby("A")
In [195]: g.nth(0)
Out[195]:
B
A
1 NaN
5 6.0
In [196]: g.nth(-1)
Out[196]:
B
A
1 4.0
5 6.0
In [197]: g.nth(1)
Out[197]:
B
A
1 4.0
如果你要选择非空项,可以使用关键字参数 dropna
,如果是 DataFrame
,需要指定为 any
或 all
(类似于 DataFrame.dropna(how='any|all')
)
# nth(0) 与 g.first() 等价
In [198]: g.nth(0, dropna="any")
Out[198]:
B
A
1 4.0
5 6.0
In [199]: g.first()
Out[199]:
B
A
1 4.0
5 6.0
# nth(-1) 与 g.last() 等价
In [200]: g.nth(-1, dropna="any") # NaNs denote group exhausted when using dropna
Out[200]:
B
A
1 4.0
5 6.0
In [201]: g.last()
Out[201]:
B
A
1 4.0
5 6.0
In [202]: g.B.nth(0, dropna="all")
Out[202]:
A
1 4.0
5 6.0
Name: B, dtype: float64
与其他方法一样,使用 as_index=False
分组名将不会作为索引
In [203]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])
In [204]: g = df.groupby("A", as_index=False)
In [205]: g.nth(0)
Out[205]:
A B
0 1 NaN
2 5 6.0
In [206]: g.nth(-1)
Out[206]:
A B
1 1 4.0
2 5 6.0
你也可以传入一个整数列表,一次性选取多行
In [207]: business_dates = pd.date_range(start="4/1/2014", end="6/30/2014", freq="B")
In [208]: df = pd.DataFrame(1, index=business_dates, columns=["a", "b"])
# 选取每月的第 1、4 和最后一天
In [209]: df.groupby([df.index.year, df.index.month]).nth([0, 3, -1])
Out[209]:
a b
2014 4 1 1
4 1 1
4 1 1
5 1 1
5 1 1
5 1 1
6 1 1
6 1 1
6 1 1
9.6 枚举分组项
使用 cumcount
方法,可以查看每行在分组中出现的顺序
In [210]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])
In [211]: dfg
Out[211]:
A
0 a
1 a
2 a
3 b
4 b
5 a
In [212]: dfg.groupby("A").cumcount()
Out[212]:
0 0
1 1
2 2
3 0
4 1
5 3
dtype: int64
In [213]: dfg.groupby("A").cumcount(ascending=False)
Out[213]:
0 3
1 2
2 1
3 1
4 0
5 0
dtype: int64
9.7 枚举分组
可以使用 ngroup()
查看分组的顺序,该顺序与 cumcount
的顺序相反。
注意:该顺序与迭代时的分组顺序一样,并不是第一次观测到的顺序
In [214]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])
In [215]: dfg
Out[215]:
A
0 a
1 a
2 a
3 b
4 b
5 a
In [216]: dfg.groupby("A").ngroup()
Out[216]:
0 0
1 0
2 0
3 1
4 1
5 0
dtype: int64
In [217]: dfg.groupby("A").ngroup(ascending=False)
Out[217]:
0 1
1 1
2 1
3 0
4 0
5 1
dtype: int64