Python 数据处理 —— pandas 分组聚合

前言

我们所说的 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 对象。我们可以根据 AB 列进行分组

In [8]: grouped = df.groupby("A")

In [9]: grouped = df.groupby(["A", "B"])

如果我们把 AB 作为层次索引,则可以选择相应的 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)

pandasIndex 对象支持重复的索引。因此,可以对包含重复值的索引进行分组,相同的索引会被分为同一组

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=1A 列进行分组

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 的参数必须是一个函数,函数参数是每个分组,并且返回 TrueFalse

例如,提取元素个数大于 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

实际上这生成了一个函数包装器,在调用时,它接受所有传递的参数,并在每个分组上进行调用。

然后,这个结果可以和 aggtransform 结合在一起使用

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 补缺失值

nlargestnsmallest 可以在 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 获取分组的第一行

类似于 SeriesDataFrame,可以使用 headtail 获取分组前后几行

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 行

SeriesDataFrame 中可以使用 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,需要指定为 anyall(类似于 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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值