群組依據:拆分-套用-合併#

我們所謂的「群組依據」是指包含下列一個或多個步驟的程序

  • 拆分資料成依據特定條件的群組。

  • 套用一個函式至每個獨立的群組。

  • 合併結果至資料結構中。

其中,拆分步驟是最直接的。在套用步驟中,我們可能會想執行下列其中一項

  • 聚合:針對每個群組計算摘要統計資料(或統計資料)。以下是一些範例

    • 計算群組總和或平均值。

    • 計算群組大小/數量。

  • 轉換:執行一些群組特定運算,並傳回一個索引相同的物件。以下是一些範例

    • 在群組內標準化資料(z 分數)。

    • 使用從每個群組衍生的值,填補群組內的 NA 值。

  • 篩選:根據群組計算,評估為 True 或 False,捨棄一些群組。以下是一些範例

    • 捨棄僅有少數成員的群組所屬的資料。

    • 根據群組總和或平均值篩選資料。

這些運算中的許多都定義在 GroupBy 物件上。這些運算類似於 聚合 API視窗 API重新取樣 API 的運算。

給定的運算有可能不屬於這些類別中的任何一個,或為它們的某種組合。在這種情況下,可以使用 GroupBy 的 apply 方法計算運算。此方法將檢查套用步驟的結果,並嘗試合理地將它們組合成單一結果,如果它不符合上述三類別中的任何一類別。

注意

使用內建 GroupBy 運算拆分為多個步驟的運算,將比使用 apply 方法搭配使用者定義的 Python 函數更有效率。

對於使用過基於 SQL 的工具(或 itertools)的人來說,GroupBy 這個名稱應該相當熟悉,您可以在其中撰寫類似以下的程式碼

SELECT Column1, Column2, mean(Column3), sum(Column4)
FROM SomeTable
GROUP BY Column1, Column2

我們的目標是讓此類運算自然且容易使用 pandas 來表達。我們將探討 GroupBy 功能的每個領域,然後提供一些非平凡的範例/使用案例。

請參閱 食譜,以了解一些進階策略。

將物件拆分為群組#

群組的抽象定義是提供標籤對群組名稱的對應。若要建立 GroupBy 物件(稍後會進一步說明 GroupBy 物件),您可以執行下列動作

In [1]: speeds = 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]: speeds
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

In [3]: grouped = speeds.groupby("class")

In [4]: grouped = speeds.groupby(["class", "order"])

對應可以透過許多不同的方式指定

  • Python 函式,針對每個索引標籤呼叫。

  • 與索引長度相同的清單或 NumPy 陣列。

  • dict 或 Series,提供 label -> group name 對應。

  • 對於 DataFrame 物件,字串表示要使用來進行群組的欄位名稱或索引層級名稱。

  • 以上任何項目清單。

我們將群組物件統稱為金鑰。例如,請考慮下列 DataFrame

注意

傳遞給 groupby 的字串可以指欄位或索引層級。如果字串同時符合欄位名稱和索引層級名稱,將會引發 ValueError

In [5]: 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 [6]: df
Out[6]: 
     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 物件。此方法會傳回 pandas.api.typing.DataFrameGroupBy 實例。我們可以自然地依據 AB 欄位進行群組,或同時依據兩者

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

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

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

注意

df.groupby('A') 只是 df.groupby(df['A']) 的語法糖。

如果我們在欄位 AB 上也有 MultiIndex,我們可以依據我們指定的欄位以外的所有欄位進行群組

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

上述 GroupBy 會在索引(列)上分割 DataFrame。若要依據欄位分割,請先進行轉置

In [13]: def get_letter_type(letter):
   ....:     if letter.lower() in 'aeiou':
   ....:         return 'vowel'
   ....:     else:
   ....:         return 'consonant'
   ....: 

In [14]: grouped = df.T.groupby(get_letter_type)

pandas Index 物件支援重複值。如果在 groupby 作業中將非唯一索引用作群組金鑰,則相同索引值的所有值都會被視為在一個群組中,因此聚合函數的輸出只會包含唯一的索引值

In [15]: index = [1, 2, 3, 1, 2, 3]

In [16]: s = pd.Series([1, 2, 3, 10, 20, 30], index=index)

In [17]: s
Out[17]: 
1     1
2     2
3     3
1    10
2    20
3    30
dtype: int64

In [18]: grouped = s.groupby(level=0)

In [19]: grouped.first()
Out[19]: 
1    1
2    2
3    3
dtype: int64

In [20]: grouped.last()
Out[20]: 
1    10
2    20
3    30
dtype: int64

In [21]: grouped.sum()
Out[21]: 
1    11
2    22
3    33
dtype: int64

請注意,不會進行分割,直到需要為止。建立 GroupBy 物件只會驗證您已傳遞有效的對應。

注意

許多種類的複雜資料處理都可以用 GroupBy 作業來表示(儘管無法保證是最有效的實作)。您可以對標籤對應函數發揮創意。

GroupBy 排序#

預設情況下,群組鍵會在 groupby 作業期間進行排序。不過,你可以傳遞 sort=False 以進行潛在加速。使用 sort=False 時,群組鍵之間的順序會遵循原始資料框中鍵出現的順序

In [22]: df2 = pd.DataFrame({"X": ["B", "B", "A", "A"], "Y": [1, 2, 3, 4]})

In [23]: df2.groupby(["X"]).sum()
Out[23]: 
   Y
X   
A  7
B  3

In [24]: df2.groupby(["X"], sort=False).sum()
Out[24]: 
   Y
X   
B  3
A  7

請注意,groupby 會保留每個群組觀測值排序的順序。例如,下列 groupby() 所建立的群組會按照其在原始 DataFrame 中出現的順序

In [25]: df3 = pd.DataFrame({"X": ["A", "B", "A", "B"], "Y": [1, 4, 3, 2]})

In [26]: df3.groupby("X").get_group("A")
Out[26]: 
   X  Y
0  A  1
2  A  3

In [27]: df3.groupby(["X"]).get_group(("B",))
Out[27]: 
   X  Y
1  B  4
3  B  2

GroupBy dropna#

預設情況下,NA 值會在 groupby 作業期間從群組鍵中排除。不過,如果你想在群組鍵中包含 NA 值,你可以傳遞 dropna=False 來達成此目的。

In [28]: df_list = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]]

In [29]: df_dropna = pd.DataFrame(df_list, columns=["a", "b", "c"])

In [30]: df_dropna
Out[30]: 
   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2
# Default ``dropna`` is set to True, which will exclude NaNs in keys
In [31]: df_dropna.groupby(by=["b"], dropna=True).sum()
Out[31]: 
     a  c
b        
1.0  2  3
2.0  2  5

# In order to allow NaN in keys, set ``dropna`` to False
In [32]: df_dropna.groupby(by=["b"], dropna=False).sum()
Out[32]: 
     a  c
b        
1.0  2  3
2.0  2  5
NaN  1  4

預設的 dropna 引數設定為 True,這表示 NA 沒有包含在群組鍵中。

GroupBy 物件屬性#

groups 屬性是一個字典,其鍵為計算出的唯一群組,對應的值為屬於每個群組的軸標籤。在上述範例中,我們有

In [33]: df.groupby("A").groups
Out[33]: {'bar': [1, 3, 5], 'foo': [0, 2, 4, 6, 7]}

In [34]: df.T.groupby(get_letter_type).groups
Out[34]: {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}

在 GroupBy 物件上呼叫標準 Python len 函數會傳回群組數量,這與 groups 字典的長度相同

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

In [36]: grouped.groups
Out[36]: {('bar', 'one'): [1], ('bar', 'three'): [3], ('bar', 'two'): [5], ('foo', 'one'): [0, 6], ('foo', 'three'): [7], ('foo', 'two'): [2, 4]}

In [37]: len(grouped)
Out[37]: 6

GroupBy 會自動完成欄位名稱、GroupBy 作業和其他屬性

In [38]: n = 10

In [39]: weight = np.random.normal(166, 20, size=n)

In [40]: height = np.random.normal(60, 10, size=n)

In [41]: time = pd.date_range("1/1/2000", periods=n)

In [42]: gender = np.random.choice(["male", "female"], size=n)

In [43]: df = pd.DataFrame(
   ....:     {"height": height, "weight": weight, "gender": gender}, index=time
   ....: )
   ....: 

In [44]: df
Out[44]: 
               height      weight  gender
2000-01-01  42.849980  157.500553    male
2000-01-02  49.607315  177.340407    male
2000-01-03  56.293531  171.524640    male
2000-01-04  48.421077  144.251986  female
2000-01-05  46.556882  152.526206    male
2000-01-06  68.448851  168.272968  female
2000-01-07  70.757698  136.431469    male
2000-01-08  58.909500  176.499753  female
2000-01-09  76.435631  174.094104  female
2000-01-10  45.306120  177.540920    male

In [45]: gb = df.groupby("gender")
In [46]: gb.<TAB>  # noqa: E225, E999
gb.agg        gb.boxplot    gb.cummin     gb.describe   gb.filter     gb.get_group  gb.height     gb.last       gb.median     gb.ngroups    gb.plot       gb.rank       gb.std        gb.transform
gb.aggregate  gb.count      gb.cumprod    gb.dtype      gb.first      gb.groups     gb.hist       gb.max        gb.min        gb.nth        gb.prod       gb.resample   gb.sum        gb.var
gb.apply      gb.cummax     gb.cumsum     gb.fillna     gb.gender     gb.head       gb.indices    gb.mean       gb.name       gb.ohlc       gb.quantile   gb.size       gb.tail       gb.weight

使用 MultiIndex 進行 GroupBy#

使用 階層索引資料 時,依據階層中的某個層級進行分組是很自然的。

讓我們建立一個具有二層級 MultiIndex 的 Series。

In [47]: arrays = [
   ....:     ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
   ....:     ["one", "two", "one", "two", "one", "two", "one", "two"],
   ....: ]
   ....: 

In [48]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])

In [49]: s = pd.Series(np.random.randn(8), index=index)

In [50]: s
Out[50]: 
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 中依據某個層級進行分組。

In [51]: grouped = s.groupby(level=0)

In [52]: grouped.sum()
Out[52]: 
first
bar   -0.962232
baz    1.237723
foo    0.785980
qux    1.911055
dtype: float64

如果 MultiIndex 有指定名稱,則可以使用名稱代替層級編號

In [53]: s.groupby(level="second").sum()
Out[53]: 
second
one    0.980950
two    1.991575
dtype: float64

支援使用多個層級進行分組。

In [54]: arrays = [
   ....:     ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
   ....:     ["doo", "doo", "bee", "bee", "bop", "bop", "bop", "bop"],
   ....:     ["one", "two", "one", "two", "one", "two", "one", "two"],
   ....: ]
   ....: 

In [55]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second", "third"])

In [56]: s = pd.Series(np.random.randn(8), index=index)

In [57]: s
Out[57]: 
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 [58]: s.groupby(level=["first", "second"]).sum()
Out[58]: 
first  second
bar    doo      -1.220674
baz    bee      -0.608004
foo    bop       1.023898
qux    bop       0.000895
dtype: float64

索引層級名稱可以作為鍵值提供。

In [59]: s.groupby(["first", "second"]).sum()
Out[59]: 
first  second
bar    doo      -1.220674
baz    bee      -0.608004
foo    bop       1.023898
qux    bop       0.000895
dtype: float64

稍後會進一步說明 sum 函式和聚合。

使用索引層級和欄位對 DataFrame 進行分組#

可以依據欄位和索引層級的組合對 DataFrame 進行分組。您可以同時指定欄位和索引名稱,或使用 Grouper

讓我們先建立一個具有 MultiIndex 的 DataFrame

In [60]: arrays = [
   ....:     ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
   ....:     ["one", "two", "one", "two", "one", "two", "one", "two"],
   ....: ]
   ....: 

In [61]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])

In [62]: df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 3, 3], "B": np.arange(8)}, index=index)

In [63]: df
Out[63]: 
              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

然後我們依據 second 索引層級和 A 欄位對 df 進行分組。

In [64]: df.groupby([pd.Grouper(level=1), "A"]).sum()
Out[64]: 
          B
second A   
one    1  2
       2  4
       3  6
two    1  4
       2  5
       3  7

索引層級也可以依據名稱指定。

In [65]: df.groupby([pd.Grouper(level="second"), "A"]).sum()
Out[65]: 
          B
second A   
one    1  2
       2  4
       3  6
two    1  4
       2  5
       3  7

索引層級名稱可以作為鍵值直接指定給 groupby

In [66]: df.groupby(["second", "A"]).sum()
Out[66]: 
          B
second A   
one    1  2
       2  4
       3  6
two    1  4
       2  5
       3  7

在 GroupBy 中選取 DataFrame 欄位#

從 DataFrame 建立 GroupBy 物件後,您可能想要對每個欄位執行不同的操作。因此,您可以使用 GroupBy 物件上的 [],其用法類似於從 DataFrame 取得欄位,您可以執行下列操作

In [67]: 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 [68]: df
Out[68]: 
     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 [69]: grouped = df.groupby(["A"])

In [70]: grouped_C = grouped["C"]

In [71]: grouped_D = grouped["D"]

這主要是語法糖,用於替代冗長的語法

In [72]: df["C"].groupby(df["A"])
Out[72]: <pandas.core.groupby.generic.SeriesGroupBy object at 0x7fac68da1630>

此外,此方法避免重新計算從傳遞的鍵衍生的內部分組資訊。

您也可以包含分組欄,如果您想對它們進行操作。

In [73]: grouped[["A", "B"]].sum()
Out[73]: 
                   A                  B
A                                      
bar        barbarbar        onethreetwo
foo  foofoofoofoofoo  onetwotwoonethree

遍歷群組#

使用 GroupBy 物件,遍歷分組資料非常自然,其功能類似於 itertools.groupby()

In [74]: grouped = df.groupby('A')

In [75]: 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 [76]: 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

請參閱 遍歷群組

選取群組#

可以使用 DataFrameGroupBy.get_group() 選取單一群組

In [77]: grouped.get_group("bar")
Out[77]: 
     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 [78]: df.groupby(["A", "B"]).get_group(("bar", "one"))
Out[78]: 
     A    B         C         D
1  bar  one  0.254161  1.511763

聚合#

聚合是 GroupBy 作業,會減少分組物件的維度。聚合的結果是,或至少被視為,群組中每個欄的純量值。例如,產生群組中每個欄的總和。

In [79]: 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 [80]: animals
Out[80]: 
  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 [81]: animals.groupby("kind").sum()
Out[81]: 
      height  weight
kind                
cat     18.6    17.8
dog     40.0   205.5

在結果中,群組的鍵會預設顯示在索引中。它們也可以透過傳遞 as_index=False 來包含在欄位中。

In [82]: animals.groupby("kind", as_index=False).sum()
Out[82]: 
  kind  height  weight
0  cat    18.6    17.8
1  dog    40.0   205.5

內建聚合方法#

許多常見的聚合都內建在 GroupBy 物件中作為方法。在以下列出的方法中,帶有 * 的方法沒有有效率的 GroupBy 特定實作。

方法

說明

any()

計算群組中是否有任何值為真

all()

計算群組中是否所有值都為真

count()

計算群組中非 NA 值的數量

cov() *

計算群組的共變異數

first()

計算每個群組中第一個出現的值

idxmax()

計算每個群組中最大值所在的索引

idxmin()

計算每個群組中最小值所在的索引

last()

計算每個群組中最後一個出現的值

max()

計算每個群組中的最大值

mean()

計算每個群組的平均值

median()

計算每個群組的中位數

min()

計算每個群組中的最小值

nunique()

計算每個群組中唯一值的數量

prod()

計算每個群組中值的乘積

quantile()

計算每個群組中值的給定分位數

sem()

計算每個群組中值的平均值的標準誤差

size()

計算每個群組中的值數量

skew() *

計算每個群組中數值的偏度

std()

計算每個群組中數值的標準差

sum()

計算每個群組中數值的總和

var()

計算每個群組中數值的變異數

一些範例

In [83]: df.groupby("A")[["C", "D"]].max()
Out[83]: 
            C         D
A                      
bar  0.254161  1.511763
foo  1.193555  1.627081

In [84]: df.groupby(["A", "B"]).mean()
Out[84]: 
                  C         D
A   B                        
bar one    0.254161  1.511763
    three  0.215897 -0.990582
    two   -0.077118  1.211526
foo one   -0.491888  0.807291
    three -0.862495  0.024580
    two    0.024925  0.592714

另一個聚合範例是計算每個群組的大小。這包含在 GroupBy 中,作為 size 方法。它會傳回一個 Series,其索引包含群組名稱,而數值則是每個群組的大小。

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

In [86]: grouped.size()
Out[86]: 
A    B    
bar  one      1
     three    1
     two      1
foo  one      2
     three    1
     two      2
dtype: int64

雖然 DataFrameGroupBy.describe() 方法本身不是一個還原器,但它可以用來方便地產生每個群組的摘要統計資料集合。

In [87]: grouped.describe()
Out[87]: 
              C                      ...         D                    
          count      mean       std  ...       50%       75%       max
A   B                                ...                              
bar one     1.0  0.254161       NaN  ...  1.511763  1.511763  1.511763
    three   1.0  0.215897       NaN  ... -0.990582 -0.990582 -0.990582
    two     1.0 -0.077118       NaN  ...  1.211526  1.211526  1.211526
foo one     2.0 -0.491888  0.117887  ...  0.807291  1.076676  1.346061
    three   1.0 -0.862495       NaN  ...  0.024580  0.024580  0.024580
    two     2.0  0.024925  1.652692  ...  0.592714  1.109898  1.627081

[6 rows x 16 columns]

另一個聚合範例是計算每個群組的唯一數值數量。這類似於 DataFrameGroupBy.value_counts() 函數,只不過它只計算唯一數值的數量。

In [88]: ll = [['foo', 1], ['foo', 2], ['foo', 2], ['bar', 1], ['bar', 1]]

In [89]: df4 = pd.DataFrame(ll, columns=["A", "B"])

In [90]: df4
Out[90]: 
     A  B
0  foo  1
1  foo  2
2  foo  2
3  bar  1
4  bar  1

In [91]: df4.groupby("A")["B"].nunique()
Out[91]: 
A
bar    1
foo    2
Name: B, dtype: int64

注意

聚合函數不會傳回您在預設情況下作為已命名欄位聚合的群組,即 as_index=True。已分組的欄位將會是傳回物件的索引

傳遞 as_index=False 傳回您在作為已命名欄位聚合的群組,無論它們在輸入中是否已命名為索引欄位

方法 aggregate()#

注意

方法 aggregate() 可以接受許多不同類型的輸入。本節詳細說明使用字串別名來表示各種 GroupBy 方法;其他輸入則在以下各節中詳細說明。

pandas 實作的任何簡化方法都可以傳遞為字串至 aggregate()。建議使用者使用簡寫 agg。它將會像呼叫對應的方法一樣運作。

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

In [93]: grouped[["C", "D"]].aggregate("sum")
Out[93]: 
            C         D
A                      
bar  0.392940  1.732707
foo -1.796421  2.824590

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

In [95]: grouped.agg("sum")
Out[95]: 
                  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 [96]: grouped = df.groupby(["A", "B"], as_index=False)

In [97]: grouped.agg("sum")
Out[97]: 
     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 [98]: df.groupby("A", as_index=False)[["C", "D"]].agg("sum")
Out[98]: 
     A         C         D
0  bar  0.392940  1.732707
1  foo -1.796421  2.824590

請注意,您可以使用 DataFrame.reset_index() DataFrame 函數來達成與欄位名稱儲存在結果 MultiIndex 中相同的結果,儘管這會建立一個額外的副本。

In [99]: df.groupby(["A", "B"]).agg("sum").reset_index()
Out[99]: 
     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

使用自訂函數進行聚合#

使用者也可以提供他們自己的自訂函數 (UDF) 來進行自訂聚合。

警告

使用 UDF 進行聚合時,UDF 不應變異所提供的 Series。請參閱 使用自訂函數 (UDF) 方法進行變異 以取得更多資訊。

注意

使用 UDF 進行聚合通常比在 GroupBy 上使用內建的 pandas 方法效能較差。請考慮將複雜操作分解為利用內建方法的運算鏈。

In [100]: animals
Out[100]: 
  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 [101]: animals.groupby("kind")[["height"]].agg(lambda x: set(x))
Out[101]: 
           height
kind             
cat    {9.1, 9.5}
dog   {34.0, 6.0}

產生的 dtype 會反映聚合函數的 dtype。如果不同群組的結果有不同的 dtype,則會以與 DataFrame 建構相同的方式決定共同的 dtype。

In [102]: animals.groupby("kind")[["height"]].agg(lambda x: x.astype(int).sum())
Out[102]: 
      height
kind        
cat       18
dog       40

一次套用多個函數#

在已分組的 Series 上,您可以傳遞函數的清單或字典至 SeriesGroupBy.agg(),輸出 DataFrame

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

In [104]: grouped["C"].agg(["sum", "mean", "std"])
Out[104]: 
          sum      mean       std
A                                
bar  0.392940  0.130980  0.181231
foo -1.796421 -0.359284  0.912265

在已分組的 DataFrame 上,您可以傳遞函數的清單至 DataFrameGroupBy.agg() 以聚合每個欄位,這會產生具有階層式欄位索引的聚合結果

In [105]: grouped[["C", "D"]].agg(["sum", "mean", "std"])
Out[105]: 
            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 [106]: (
   .....:     grouped["C"]
   .....:     .agg(["sum", "mean", "std"])
   .....:     .rename(columns={"sum": "foo", "mean": "bar", "std": "baz"})
   .....: )
   .....: 
Out[106]: 
          foo       bar       baz
A                                
bar  0.392940  0.130980  0.181231
foo -1.796421 -0.359284  0.912265

對於分組的 DataFrame,您可以用類似的方式重新命名

In [107]: (
   .....:     grouped[["C", "D"]].agg(["sum", "mean", "std"]).rename(
   .....:         columns={"sum": "foo", "mean": "bar", "std": "baz"}
   .....:     )
   .....: )
   .....: 
Out[107]: 
            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

注意

一般來說,輸出的欄位名稱應該要唯一,但 pandas 會允許您將相同的函式(或兩個名稱相同的函式)套用至同一個欄位。

In [108]: grouped["C"].agg(["sum", "sum"])
Out[108]: 
          sum       sum
A                      
bar  0.392940  0.392940
foo -1.796421 -1.796421

pandas 也允許您提供多個 lambda。在這種情況下,pandas 會將(無名稱的)lambda 函式的名稱弄亂,並將 _<i> 附加到每個後續的 lambda。

In [109]: grouped["C"].agg([lambda x: x.max() - x.min(), lambda x: x.median() - x.mean()])
Out[109]: 
     <lambda_0>  <lambda_1>
A                          
bar    0.331279    0.084917
foo    2.337259   -0.215962

具名稱的聚合#

為了支援具有特定欄位聚合功能(並控制輸出欄位名稱),pandas 接受 DataFrameGroupBy.agg()SeriesGroupBy.agg() 中的特殊語法,稱為「具名稱的聚合」,其中

  • 關鍵字為輸出欄位名稱

  • 值為元組,其第一個元素為要選取的欄位,第二個元素為要套用至該欄位的聚合。pandas 提供 NamedAgg namedtuple,其欄位為 ['column', 'aggfunc'],以更清楚說明參數為何。一如往常,聚合可以是可呼叫物件或字串別名。

In [110]: animals
Out[110]: 
  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 [111]: 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="mean"),
   .....: )
   .....: 
Out[111]: 
      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

NamedAgg 僅為 namedtuple。純粹的 tuple 也可使用。

In [112]: animals.groupby("kind").agg(
   .....:     min_height=("height", "min"),
   .....:     max_height=("height", "max"),
   .....:     average_weight=("weight", "mean"),
   .....: )
   .....: 
Out[112]: 
      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

如果所要的欄位名稱不是有效的 Python 關鍵字,請建構一個字典,並解開關鍵字參數

In [113]: animals.groupby("kind").agg(
   .....:     **{
   .....:         "total weight": pd.NamedAgg(column="weight", aggfunc="sum")
   .....:     }
   .....: )
   .....: 
Out[113]: 
      total weight
kind              
cat           17.8
dog          205.5

使用命名聚合時,額外的關鍵字參數不會傳遞到聚合函數;只有 (column, aggfunc) 的配對應該傳遞為 **kwargs。如果聚合函數需要額外的參數,請使用 functools.partial() 部分套用。

命名聚合也適用於 Series groupby 聚合。在這種情況下沒有欄位選取,因此值僅為函數。

In [114]: animals.groupby("kind").height.agg(
   .....:     min_height="min",
   .....:     max_height="max",
   .....: )
   .....: 
Out[114]: 
      min_height  max_height
kind                        
cat          9.1         9.5
dog          6.0        34.0

對 DataFrame 欄位套用不同的函數#

透過傳遞字典給 aggregate,您可以對 DataFrame 的欄位套用不同的聚合

In [115]: grouped.agg({"C": "sum", "D": lambda x: np.std(x, ddof=1)})
Out[115]: 
            C         D
A                      
bar  0.392940  1.366330
foo -1.796421  0.884785

函數名稱也可以是字串。若要讓字串有效,它必須在 GroupBy 中實作

In [116]: grouped.agg({"C": "sum", "D": "std"})
Out[116]: 
            C         D
A                      
bar  0.392940  1.366330
foo -1.796421  0.884785

轉換#

轉換是一種 GroupBy 運算,其結果與被分組的結果索引相同。常見範例包括 cumsum()diff()

In [117]: speeds
Out[117]: 
          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

In [118]: grouped = speeds.groupby("class")["max_speed"]

In [119]: grouped.cumsum()
Out[119]: 
falcon     389.0
parrot     413.0
lion        80.2
monkey       NaN
leopard    138.2
Name: max_speed, dtype: float64

In [120]: grouped.diff()
Out[120]: 
falcon       NaN
parrot    -365.0
lion         NaN
monkey       NaN
leopard      NaN
Name: max_speed, dtype: float64

與聚合不同,用於分割原始物件的分組不會包含在結果中。

注意

由於轉換不包括用於分割結果的分組,因此 as_indexsort 引數在 DataFrame.groupby()Series.groupby() 中沒有作用。

轉換的常見用途是將結果新增回原始 DataFrame。

In [121]: result = speeds.copy()

In [122]: result["cumsum"] = grouped.cumsum()

In [123]: result["diff"] = grouped.diff()

In [124]: result
Out[124]: 
          class           order  max_speed  cumsum   diff
falcon     bird   Falconiformes      389.0   389.0    NaN
parrot     bird  Psittaciformes       24.0   413.0 -365.0
lion     mammal       Carnivora       80.2    80.2    NaN
monkey   mammal        Primates        NaN     NaN    NaN
leopard  mammal       Carnivora       58.0   138.2    NaN

內建轉換方法#

GroupBy 上的下列方法會作為轉換。

方法

說明

bfill()

在每個群組內回填 NA 值

cumcount()

計算每個群組內的累計次數

cummax()

計算每個群組內的累計最大值

cummin()

計算每個群組內的累計最小值

cumprod()

計算每一組內的累積乘積

cumsum()

計算每一組內的累積和

diff()

計算每一組內相鄰值的差

ffill()

向前填補每一組內的 NA 值

pct_change()

計算每一組內相鄰值的百分比變化

rank()

計算每一組內每個值的排名

shift()

在每一組內向上或向下移動值

此外,將任何內建聚合方法作為字串傳遞給 transform()(請參閱下一節),將會在組中廣播結果,產生轉換後的結果。如果聚合方法有高效的實作,這也會很有效能。

transform() 方法#

類似於 聚合方法transform() 方法可以接受前一節中內建轉換方法的字串別名。它也可以接受內建聚合方法的字串別名。當提供聚合方法時,結果會在組中廣播。

In [125]: speeds
Out[125]: 
          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

In [126]: grouped = speeds.groupby("class")[["max_speed"]]

In [127]: grouped.transform("cumsum")
Out[127]: 
         max_speed
falcon       389.0
parrot       413.0
lion          80.2
monkey         NaN
leopard      138.2

In [128]: grouped.transform("sum")
Out[128]: 
         max_speed
falcon       413.0
parrot       413.0
lion         138.2
monkey       138.2
leopard      138.2

除了字串別名外,transform() 方法也可以接受使用者定義函數 (UDF)。UDF 必須

  • 傳回一個結果,其大小與群組區塊相同,或可廣播至群組區塊的大小(例如,一個純量,grouped.transform(lambda x: x.iloc[-1]))。

  • 在群組區塊上逐欄操作。轉換套用至第一個群組區塊,使用 chunk.apply。

  • 不對群組區塊執行原地操作。群組區塊應視為不可變,而對群組區塊所做的變更可能會產生意外的結果。請參閱 使用使用者定義函數 (UDF) 方法進行變異 以取得更多資訊。

  • (選擇性地)一次對整個群組區塊的所有欄位進行操作。如果支援此功能,則會從第二個區塊開始使用快速路徑。

注意

透過提供 UDF 來進行轉換,通常比在 GroupBy 上使用內建方法的效能較差。請考慮將複雜操作分解成一連串使用內建方法的操作。

本節中的所有範例都可以透過呼叫內建方法(而非使用 UDF)來提升效能。請參閱 下方範例

在 2.0.0 版中變更: 在群組 DataFrame 上使用 .transform 時,如果轉換函式傳回 DataFrame,pandas 現在會將結果的索引與輸入的索引對齊。您可以在轉換函式中呼叫 .to_numpy() 以避免對齊。

aggregate() 方法 類似,產生的 dtype 會反映轉換函式的 dtype。如果不同群組的結果具有不同的 dtype,則會以與 DataFrame 建構相同的方式來決定共同的 dtype。

假設我們希望標準化每個群組中的資料

In [129]: index = pd.date_range("10/1/1999", periods=1100)

In [130]: ts = pd.Series(np.random.normal(0.5, 2, 1100), index)

In [131]: ts = ts.rolling(window=100, min_periods=100).mean().dropna()

In [132]: ts.head()
Out[132]: 
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 [133]: ts.tail()
Out[133]: 
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 [134]: transformed = ts.groupby(lambda x: x.year).transform(
   .....:     lambda x: (x - x.mean()) / x.std()
   .....: )
   .....: 

我們預期結果現在在每個群組中具有平均值 0 和標準差 1(浮點數誤差範圍內),我們可以輕鬆檢查

# Original Data
In [135]: grouped = ts.groupby(lambda x: x.year)

In [136]: grouped.mean()
Out[136]: 
2000    0.442441
2001    0.526246
2002    0.459365
dtype: float64

In [137]: grouped.std()
Out[137]: 
2000    0.131752
2001    0.210945
2002    0.128753
dtype: float64

# Transformed Data
In [138]: grouped_trans = transformed.groupby(lambda x: x.year)

In [139]: grouped_trans.mean()
Out[139]: 
2000   -4.870756e-16
2001   -1.545187e-16
2002    4.136282e-16
dtype: float64

In [140]: grouped_trans.std()
Out[140]: 
2000    1.0
2001    1.0
2002    1.0
dtype: float64

我們也可以視覺上比較原始和轉換後的資料集。

In [141]: compare = pd.DataFrame({"Original": ts, "Transformed": transformed})

In [142]: compare.plot()
Out[142]: <Axes: >
../_images/groupby_transform_plot.png

具有較低維度輸出的轉換函式會廣播以符合輸入陣列的形狀。

In [143]: ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
Out[143]: 
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 [144]: cols = ["A", "B", "C"]

In [145]: values = np.random.randn(1000, 3)

In [146]: values[np.random.randint(0, 1000, 100), 0] = np.nan

In [147]: values[np.random.randint(0, 1000, 50), 1] = np.nan

In [148]: values[np.random.randint(0, 1000, 200), 2] = np.nan

In [149]: data_df = pd.DataFrame(values, columns=cols)

In [150]: data_df
Out[150]: 
            A         B         C
0    1.539708 -1.166480  0.533026
1    1.302092 -0.505754       NaN
2   -0.371983  1.104803 -0.651520
3   -1.309622  1.118697 -1.161657
4   -1.924296  0.396437  0.812436
..        ...       ...       ...
995 -0.093110  0.683847 -0.774753
996 -0.185043  1.438572       NaN
997 -0.394469 -0.642343  0.011374
998 -1.174126  1.857148       NaN
999  0.234564  0.517098  0.393534

[1000 rows x 3 columns]

In [151]: countries = np.array(["US", "UK", "GR", "JP"])

In [152]: key = countries[np.random.randint(0, 4, 1000)]

In [153]: grouped = data_df.groupby(key)

# Non-NA count in each group
In [154]: grouped.count()
Out[154]: 
      A    B    C
GR  209  217  189
JP  240  255  217
UK  216  231  193
US  239  250  217

In [155]: transformed = grouped.transform(lambda x: x.fillna(x.mean()))

我們可以驗證群組平均值在轉換後的資料中沒有變更,而且轉換後的資料不包含任何 NA。

In [156]: grouped_trans = transformed.groupby(key)

In [157]: grouped.mean()  # original group means
Out[157]: 
           A         B         C
GR -0.098371 -0.015420  0.068053
JP  0.069025  0.023100 -0.077324
UK  0.034069 -0.052580 -0.116525
US  0.058664 -0.020399  0.028603

In [158]: grouped_trans.mean()  # transformation did not change group means
Out[158]: 
           A         B         C
GR -0.098371 -0.015420  0.068053
JP  0.069025  0.023100 -0.077324
UK  0.034069 -0.052580 -0.116525
US  0.058664 -0.020399  0.028603

In [159]: grouped.count()  # original has some missing data points
Out[159]: 
      A    B    C
GR  209  217  189
JP  240  255  217
UK  216  231  193
US  239  250  217

In [160]: grouped_trans.count()  # counts after transformation
Out[160]: 
      A    B    C
GR  228  228  228
JP  267  267  267
UK  247  247  247
US  258  258  258

In [161]: grouped_trans.size()  # Verify non-NA count equals group size
Out[161]: 
GR    228
JP    267
UK    247
US    258
dtype: int64

如上方的註解所述,本節中的每個範例都可以使用內建方法更有效率地計算。在以下程式碼中,使用 UDF 的低效率方式已註解掉,而較快的替代方式出現在下方。

# result = ts.groupby(lambda x: x.year).transform(
#     lambda x: (x - x.mean()) / x.std()
# )
In [162]: grouped = ts.groupby(lambda x: x.year)

In [163]: result = (ts - grouped.transform("mean")) / grouped.transform("std")

# result = ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
In [164]: grouped = ts.groupby(lambda x: x.year)

In [165]: result = grouped.transform("max") - grouped.transform("min")

# grouped = data_df.groupby(key)
# result = grouped.transform(lambda x: x.fillna(x.mean()))
In [166]: grouped = data_df.groupby(key)

In [167]: result = data_df.fillna(grouped.transform("mean"))

視窗和重新取樣操作#

可以將 resample()expanding()rolling() 用作 groupbys 上的方法。

以下範例會在欄位 B 的範例上套用 rolling() 方法,根據欄位 A 的群組。

In [168]: df_re = pd.DataFrame({"A": [1] * 10 + [5] * 10, "B": np.arange(20)})

In [169]: df_re
Out[169]: 
    A   B
0   1   0
1   1   1
2   1   2
3   1   3
4   1   4
.. ..  ..
15  5  15
16  5  16
17  5  17
18  5  18
19  5  19

[20 rows x 2 columns]

In [170]: df_re.groupby("A").rolling(4).B.mean()
Out[170]: 
A    
1  0      NaN
   1      NaN
   2      NaN
   3      1.5
   4      2.5
         ... 
5  15    13.5
   16    14.5
   17    15.5
   18    16.5
   19    17.5
Name: B, Length: 20, dtype: float64

expanding() 方法會累積每個特定群組所有成員的特定操作(範例中的 sum())。

In [171]: df_re.groupby("A").expanding().sum()
Out[171]: 
          B
A          
1 0     0.0
  1     1.0
  2     3.0
  3     6.0
  4    10.0
...     ...
5 15   75.0
  16   91.0
  17  108.0
  18  126.0
  19  145.0

[20 rows x 1 columns]

假設您想要使用 resample() 方法來取得資料框中每個群組的每日頻率,並希望使用 ffill() 方法填補遺失值。

In [172]: df_re = pd.DataFrame(
   .....:     {
   .....:         "date": pd.date_range(start="2016-01-01", periods=4, freq="W"),
   .....:         "group": [1, 1, 2, 2],
   .....:         "val": [5, 6, 7, 8],
   .....:     }
   .....: ).set_index("date")
   .....: 

In [173]: df_re
Out[173]: 
            group  val
date                  
2016-01-03      1    5
2016-01-10      1    6
2016-01-17      2    7
2016-01-24      2    8

In [174]: df_re.groupby("group").resample("1D", include_groups=False).ffill()
Out[174]: 
                  val
group date           
1     2016-01-03    5
      2016-01-04    5
      2016-01-05    5
      2016-01-06    5
      2016-01-07    5
...               ...
2     2016-01-20    7
      2016-01-21    7
      2016-01-22    7
      2016-01-23    7
      2016-01-24    8

[16 rows x 1 columns]

過濾#

過濾是 GroupBy 操作,會對原始分組物件進行子集化。它可以過濾掉整個群組、部分群組或兩者。過濾會傳回呼叫物件的過濾版本,包括提供的分組欄位。在以下範例中,class 會包含在結果中。

In [175]: speeds
Out[175]: 
          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

In [176]: speeds.groupby("class").nth(1)
Out[176]: 
         class           order  max_speed
parrot    bird  Psittaciformes       24.0
monkey  mammal        Primates        NaN

注意

與聚合不同,過濾不會將群組鍵新增到結果的索引中。因此,傳遞 as_index=Falsesort=True 這些方法不會受到影響。

過濾會尊重對 GroupBy 物件的欄位進行子集化。

In [177]: speeds.groupby("class")[["order", "max_speed"]].nth(1)
Out[177]: 
                 order  max_speed
parrot  Psittaciformes       24.0
monkey        Primates        NaN

內建過濾#

GroupBy 上的以下方法會作為過濾。所有這些方法都有有效率的 GroupBy 專屬實作。

方法

說明

head()

選取每組最上方的列

nth()

選取每組第 n 個列

tail()

選取每組最下方的列

使用者也可以使用轉換與布林索引來建構組內複雜的過濾。例如,假設我們給定產品組及其數量,我們希望將資料子集化,只保留每組中最大且合計數量不超過總數 90% 的產品。

In [178]: product_volumes = pd.DataFrame(
   .....:     {
   .....:         "group": list("xxxxyyy"),
   .....:         "product": list("abcdefg"),
   .....:         "volume": [10, 30, 20, 15, 40, 10, 20],
   .....:     }
   .....: )
   .....: 

In [179]: product_volumes
Out[179]: 
  group product  volume
0     x       a      10
1     x       b      30
2     x       c      20
3     x       d      15
4     y       e      40
5     y       f      10
6     y       g      20

# Sort by volume to select the largest products first
In [180]: product_volumes = product_volumes.sort_values("volume", ascending=False)

In [181]: grouped = product_volumes.groupby("group")["volume"]

In [182]: cumpct = grouped.cumsum() / grouped.transform("sum")

In [183]: cumpct
Out[183]: 
4    0.571429
1    0.400000
2    0.666667
6    0.857143
3    0.866667
0    1.000000
5    1.000000
Name: volume, dtype: float64

In [184]: significant_products = product_volumes[cumpct <= 0.9]

In [185]: significant_products.sort_values(["group", "product"])
Out[185]: 
  group product  volume
1     x       b      30
2     x       c      20
3     x       d      15
4     y       e      40
6     y       g      20

filter 方法#

注意

使用使用者自訂函數 (UDF) 提供 filter 進行過濾通常比使用 GroupBy 的內建方法效能較差。考慮將複雜的運算分解成利用內建方法的運算鏈。

filter 方法採用使用者自訂函數 (UDF),當應用於整個組時,會傳回 TrueFalse。然後,filter 方法的結果是 UDF 傳回 True 的組子集。

假設我們只想取組總和超過 2 的組的元素。

In [186]: sf = pd.Series([1, 1, 2, 3, 3, 3])

In [187]: sf.groupby(sf).filter(lambda x: x.sum() > 2)
Out[187]: 
3    3
4    3
5    3
dtype: int64

另一個有用的運算會過濾掉只包含幾個成員的組的元素。

In [188]: dff = pd.DataFrame({"A": np.arange(8), "B": list("aabbbbcc")})

In [189]: dff.groupby("B").filter(lambda x: len(x) > 2)
Out[189]: 
   A  B
2  2  b
3  3  b
4  4  b
5  5  b

或者,我們可以傳回一個類似索引的物件,其中未通過過濾的組會填入 NaN,而不是刪除有問題的組。

In [190]: dff.groupby("B").filter(lambda x: len(x) > 2, dropna=False)
Out[190]: 
     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

對於有數個欄位的資料框,過濾器應明確指定一個欄位作為過濾準則。

In [191]: dff["C"] = np.arange(8)

In [192]: dff.groupby("B").filter(lambda x: len(x["C"]) > 2)
Out[192]: 
   A  B  C
2  2  b  2
3  3  b  3
4  4  b  4
5  5  b  5

彈性的 apply#

某些群組資料上的運算可能不符合彙總、轉換或篩選類別。對於這些運算,您可以使用 apply 函數。

警告

apply 必須嘗試從結果推斷它應扮演還原器、轉換器篩選器的角色,具體取決於傳遞給它的內容。因此,群組欄位可能會包含在輸出中,也可能不會。雖然它會嘗試智慧地猜測如何執行,但有時可能會猜錯。

注意

本節中的所有範例都可以使用其他 pandas 功能更可靠、更有效率地計算。

In [193]: df
Out[193]: 
     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 [194]: grouped = df.groupby("A")

# could also just call .describe()
In [195]: grouped["C"].apply(lambda x: x.describe())
Out[195]: 
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 [196]: grouped = df.groupby('A')['C']

In [197]: def f(group):
   .....:     return pd.DataFrame({'original': group,
   .....:                          'demeaned': group - group.mean()})
   .....: 

In [198]: grouped.apply(f)
Out[198]: 
       original  demeaned
A                        
bar 1  0.254161  0.123181
    3  0.215897  0.084917
    5 -0.077118 -0.208098
foo 0 -0.575247 -0.215962
    2 -1.143704 -0.784420
    4  1.193555  1.552839
    6 -0.408530 -0.049245
    7 -0.862495 -0.503211

在 Series 上的 apply 可以對應用函數傳回的值進行運算,而該值本身是一個 series,並可能將結果上轉型為 DataFrame

In [199]: def f(x):
   .....:     return pd.Series([x, x ** 2], index=["x", "x^2"])
   .....: 

In [200]: s = pd.Series(np.random.rand(5))

In [201]: s
Out[201]: 
0    0.582898
1    0.098352
2    0.001438
3    0.009420
4    0.815826
dtype: float64

In [202]: s.apply(f)
Out[202]: 
          x       x^2
0  0.582898  0.339770
1  0.098352  0.009673
2  0.001438  0.000002
3  0.009420  0.000089
4  0.815826  0.665572

類似於 aggregate() 方法,產生的 dtype 會反映 apply 函數的 dtype。如果不同群組的結果有不同的 dtype,則會以與 DataFrame 建構相同的方式來決定一個常見的 dtype。

使用 group_keys 控制群組欄位的配置#

若要控制是否將群組欄位包含在索引中,您可以使用預設為 True 的引數 group_keys。比較

In [203]: df.groupby("A", group_keys=True).apply(lambda x: x, include_groups=False)
Out[203]: 
           B         C         D
A                               
bar 1    one  0.254161  1.511763
    3  three  0.215897 -0.990582
    5    two -0.077118  1.211526
foo 0    one -0.575247  1.346061
    2    two -1.143704  1.627081
    4    two  1.193555 -0.441652
    6    one -0.408530  0.268520
    7  three -0.862495  0.024580

In [204]: df.groupby("A", group_keys=False).apply(lambda x: x, include_groups=False)
Out[204]: 
       B         C         D
0    one -0.575247  1.346061
1    one  0.254161  1.511763
2    two -1.143704  1.627081
3  three  0.215897 -0.990582
4    two  1.193555 -0.441652
5    two -0.077118  1.211526
6    one -0.408530  0.268520
7  three -0.862495  0.024580

Numba 加速常式#

1.1 版中的新增功能。

如果 Numba 安裝為選用相依模組,則 transformaggregate 方法支援 engine='numba'engine_kwargs 參數。有關參數的一般用法和效能考量,請參閱 使用 Numba 提升效能

函式簽章必須以 values, index 開頭,完全符合傳遞至 values 的每個群組所屬資料,且群組索引會傳遞至 index

警告

使用 engine='numba' 時,內部不會有「備援」行為。群組資料和群組索引會以 NumPy 陣列傳遞至 JITed 使用者定義函式,且不會嘗試其他執行嘗試。

其他有用功能#

排除非數字欄#

再次考慮我們一直在檢視的範例 DataFrame

In [205]: df
Out[205]: 
     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 中的資料,因為它不是數字。您可以透過指定 numeric_only=True 來避免非數字欄。

In [206]: df.groupby("A").std(numeric_only=True)
Out[206]: 
            C         D
A                      
bar  0.181231  1.366330
foo  0.912265  0.884785

請注意,df.groupby('A').colname.std().df.groupby('A').std().colname 更有效率。因此,如果聚合函數的結果僅需要一個欄位(此處為 colname),則可以在套用聚合函數之前先進行篩選。

In [207]: from decimal import Decimal

In [208]: df_dec = pd.DataFrame(
   .....:     {
   .....:         "id": [1, 2, 1, 2],
   .....:         "int_column": [1, 2, 3, 4],
   .....:         "dec_column": [
   .....:             Decimal("0.50"),
   .....:             Decimal("0.15"),
   .....:             Decimal("0.25"),
   .....:             Decimal("0.40"),
   .....:         ],
   .....:     }
   .....: )
   .....: 

In [209]: df_dec.groupby(["id"])[["dec_column"]].sum()
Out[209]: 
   dec_column
id           
1        0.75
2        0.55

處理(未)觀察到的類別值#

使用 Categorical 分組器(作為單一的分組器,或作為多個分組器的一部分)時,observed 關鍵字會控制是否傳回所有可能的分組器值的笛卡兒積(observed=False)或僅傳回那些被觀察到的分組器(observed=True)。

顯示所有值

In [210]: pd.Series([1, 1, 1]).groupby(
   .....:     pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=False
   .....: ).count()
   .....: 
Out[210]: 
a    3
b    0
dtype: int64

僅顯示觀察到的值

In [211]: pd.Series([1, 1, 1]).groupby(
   .....:     pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=True
   .....: ).count()
   .....: 
Out[211]: 
a    3
dtype: int64

分組後傳回的 dtype 永遠會包含所有已分組的類別。

In [212]: s = (
   .....:     pd.Series([1, 1, 1])
   .....:     .groupby(pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=True)
   .....:     .count()
   .....: )
   .....: 

In [213]: s.index.dtype
Out[213]: CategoricalDtype(categories=['a', 'b'], ordered=False, categories_dtype=object)

NA 群組處理#

NA 中,我們指的是任何 NA 值,包括 NANaNNaTNone。如果分組金鑰中有任何 NA 值,預設會將這些值排除在外。換句話說,任何「NA 群組」都會被捨棄。您可以透過指定 dropna=False 來包含 NA 群組。

In [214]: df = pd.DataFrame({"key": [1.0, 1.0, np.nan, 2.0, np.nan], "A": [1, 2, 3, 4, 5]})

In [215]: df
Out[215]: 
   key  A
0  1.0  1
1  1.0  2
2  NaN  3
3  2.0  4
4  NaN  5

In [216]: df.groupby("key", dropna=True).sum()
Out[216]: 
     A
key   
1.0  3
2.0  4

In [217]: df.groupby("key", dropna=False).sum()
Out[217]: 
     A
key   
1.0  3
2.0  4
NaN  8

使用已排序的因子進行分組#

表示為 pandas Categorical 類別實例的分類變數可用作群組鍵。如果是這樣,層級順序將會保留。當 observed=Falsesort=False 時,任何未觀察到的類別將會依序出現在結果的最後。

In [218]: days = pd.Categorical(
   .....:     values=["Wed", "Mon", "Thu", "Mon", "Wed", "Sat"],
   .....:     categories=["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
   .....: )
   .....: 

In [219]: data = pd.DataFrame(
   .....:    {
   .....:        "day": days,
   .....:        "workers": [3, 4, 1, 4, 2, 2],
   .....:    }
   .....: )
   .....: 

In [220]: data
Out[220]: 
   day  workers
0  Wed        3
1  Mon        4
2  Thu        1
3  Mon        4
4  Wed        2
5  Sat        2

In [221]: data.groupby("day", observed=False, sort=True).sum()
Out[221]: 
     workers
day         
Mon        8
Tue        0
Wed        5
Thu        1
Fri        0
Sat        2
Sun        0

In [222]: data.groupby("day", observed=False, sort=False).sum()
Out[222]: 
     workers
day         
Wed        5
Mon        8
Thu        1
Sat        2
Tue        0
Fri        0
Sun        0

使用群組器規格進行分組#

您可能需要指定更多資料才能適當地進行分組。您可以使用 pd.Grouper 來提供此區域控制。

In [223]: import datetime

In [224]: 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 [225]: df
Out[225]: 
  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 [226]: df.groupby([pd.Grouper(freq="1ME", key="Date"), "Buyer"])[["Quantity"]].sum()
Out[226]: 
                  Quantity
Date       Buyer          
2013-01-31 Carl          1
           Mark          3
2013-10-31 Carl          6
           Joe           9
2013-12-31 Carl          3
           Joe           9

當指定 freq 時,pd.Grouper 回傳的物件將會是 pandas.api.typing.TimeGrouper 的實例。當有欄位和索引具有相同名稱時,您可以使用 key 依欄位進行分組,並使用 level 依索引進行分組。

In [227]: df = df.set_index("Date")

In [228]: df["Date"] = df.index + pd.offsets.MonthEnd(2)

In [229]: df.groupby([pd.Grouper(freq="6ME", key="Date"), "Buyer"])[["Quantity"]].sum()
Out[229]: 
                  Quantity
Date       Buyer          
2013-02-28 Carl          1
           Mark          3
2014-02-28 Carl          9
           Joe          18

In [230]: df.groupby([pd.Grouper(freq="6ME", level="Date"), "Buyer"])[["Quantity"]].sum()
Out[230]: 
                  Quantity
Date       Buyer          
2013-01-31 Carl          1
           Mark          3
2014-01-31 Carl          9
           Joe          18

取得每個群組的第一列#

就像 DataFrame 或 Series 一樣,您可以在 groupby 上呼叫 head 和 tail

In [231]: df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=["A", "B"])

In [232]: df
Out[232]: 
   A  B
0  1  2
1  1  4
2  5  6

In [233]: g = df.groupby("A")

In [234]: g.head(1)
Out[234]: 
   A  B
0  1  2
2  5  6

In [235]: g.tail(1)
Out[235]: 
   A  B
1  1  4
2  5  6

這會顯示每個群組的前 n 列或最後 n 列。

取得每個群組的第 n 列#

若要從每個群組中選取第 n 個項目,請使用 DataFrameGroupBy.nth()SeriesGroupBy.nth()。提供的引數可以是任何整數、整數清單、切片或切片清單;請參閱以下範例。當群組的第 n 個元素不存在時,不會引發錯誤;而是不傳回對應的行。

一般來說,此操作會作為過濾。在某些情況下,它也會傳回每組一行,使其也成為簡約。但是,由於一般而言它可以傳回每組零行或多行,因此 pandas 在所有情況下都將其視為過濾。

In [236]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])

In [237]: g = df.groupby("A")

In [238]: g.nth(0)
Out[238]: 
   A    B
0  1  NaN
2  5  6.0

In [239]: g.nth(-1)
Out[239]: 
   A    B
1  1  4.0
2  5  6.0

In [240]: g.nth(1)
Out[240]: 
   A    B
1  1  4.0

如果群組的第 n 個元素不存在,則結果中不會包含對應的行。特別是,如果指定的 n 大於任何群組,則結果將會是空的 DataFrame。

In [241]: g.nth(5)
Out[241]: 
Empty DataFrame
Columns: [A, B]
Index: []

如果您想要選取第 n 個非 Null 項目,請使用 dropna 關鍵字引數。對於 DataFrame,這應該是 'any''all',就像您傳遞給 dropna 一樣

# nth(0) is the same as g.first()
In [242]: g.nth(0, dropna="any")
Out[242]: 
   A    B
1  1  4.0
2  5  6.0

In [243]: g.first()
Out[243]: 
     B
A     
1  4.0
5  6.0

# nth(-1) is the same as g.last()
In [244]: g.nth(-1, dropna="any")
Out[244]: 
   A    B
1  1  4.0
2  5  6.0

In [245]: g.last()
Out[245]: 
     B
A     
1  4.0
5  6.0

In [246]: g.B.nth(0, dropna="all")
Out[246]: 
1    4.0
2    6.0
Name: B, dtype: float64

您也可以指定多個第 n 個值作為整數清單,從每個群組選取多行。

In [247]: business_dates = pd.date_range(start="4/1/2014", end="6/30/2014", freq="B")

In [248]: df = pd.DataFrame(1, index=business_dates, columns=["a", "b"])

# get the first, 4th, and last date index for each month
In [249]: df.groupby([df.index.year, df.index.month]).nth([0, 3, -1])
Out[249]: 
            a  b
2014-04-01  1  1
2014-04-04  1  1
2014-04-30  1  1
2014-05-01  1  1
2014-05-06  1  1
2014-05-30  1  1
2014-06-02  1  1
2014-06-05  1  1
2014-06-30  1  1

您也可以使用切片或切片清單。

In [250]: df.groupby([df.index.year, df.index.month]).nth[1:]
Out[250]: 
            a  b
2014-04-02  1  1
2014-04-03  1  1
2014-04-04  1  1
2014-04-07  1  1
2014-04-08  1  1
...        .. ..
2014-06-24  1  1
2014-06-25  1  1
2014-06-26  1  1
2014-06-27  1  1
2014-06-30  1  1

[62 rows x 2 columns]

In [251]: df.groupby([df.index.year, df.index.month]).nth[1:, :-1]
Out[251]: 
            a  b
2014-04-01  1  1
2014-04-02  1  1
2014-04-03  1  1
2014-04-04  1  1
2014-04-07  1  1
...        .. ..
2014-06-24  1  1
2014-06-25  1  1
2014-06-26  1  1
2014-06-27  1  1
2014-06-30  1  1

[65 rows x 2 columns]

列舉群組項目#

若要查看每行在其群組中出現的順序,請使用 cumcount 方法

In [252]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])

In [253]: dfg
Out[253]: 
   A
0  a
1  a
2  a
3  b
4  b
5  a

In [254]: dfg.groupby("A").cumcount()
Out[254]: 
0    0
1    1
2    2
3    0
4    1
5    3
dtype: int64

In [255]: dfg.groupby("A").cumcount(ascending=False)
Out[255]: 
0    3
1    2
2    1
3    1
4    0
5    0
dtype: int64

列舉群組#

若要查看群組的排序(與群組內列的順序不同,由 cumcount 給出),可以使用 DataFrameGroupBy.ngroup()

請注意,給予群組的數字與在迭代群組物件時看到的群組順序相符,而不是第一次觀察到的順序。

In [256]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])

In [257]: dfg
Out[257]: 
   A
0  a
1  a
2  a
3  b
4  b
5  a

In [258]: dfg.groupby("A").ngroup()
Out[258]: 
0    0
1    0
2    0
3    1
4    1
5    0
dtype: int64

In [259]: dfg.groupby("A").ngroup(ascending=False)
Out[259]: 
0    1
1    1
2    1
3    0
4    0
5    1
dtype: int64

繪製#

Groupby 也可搭配一些繪製方法使用。在此情況下,假設我們懷疑群組「B」中第 1 欄的平均值高出 3 倍。

In [260]: np.random.seed(1234)

In [261]: df = pd.DataFrame(np.random.randn(50, 2))

In [262]: df["g"] = np.random.choice(["A", "B"], size=50)

In [263]: df.loc[df["g"] == "B", 1] += 3

我們可以用箱型圖輕鬆視覺化這一點

In [264]: df.groupby("g").boxplot()
Out[264]: 
A         Axes(0.1,0.15;0.363636x0.75)
B    Axes(0.536364,0.15;0.363636x0.75)
dtype: object
../_images/groupby_boxplot.png

呼叫 boxplot 的結果是一個字典,其金鑰為我們的群組欄 g 的值(「A」和「B」)。結果字典的值可由 boxplotreturn_type 關鍵字控制。請參閱 視覺化文件 以進一步了解。

警告

基於歷史原因,df.groupby("g").boxplot()df.boxplot(by="g") 不同。請參閱 這裡 以取得說明。

串接函數呼叫#

類似於 DataFrameSeries 所提供的功能,採用 GroupBy 物件的函數可以使用 pipe 方法串連在一起,以允許更簡潔、更易讀的語法。若要了解 .pipe 的一般條款,請參閱 這裡

組合 .groupby.pipe 通常在您需要重複使用 GroupBy 物件時很有用。

舉例來說,想像有一個包含商店、產品、營收和銷售數量欄位的 DataFrame。我們想要對每個商店和每個產品進行價格(即營收/數量)的群組計算。我們可以在多步驟操作中執行此操作,但以管道方式表達它可以使程式碼更易讀。首先,我們設定資料

In [265]: n = 1000

In [266]: df = pd.DataFrame(
   .....:     {
   .....:         "Store": np.random.choice(["Store_1", "Store_2"], n),
   .....:         "Product": np.random.choice(["Product_1", "Product_2"], n),
   .....:         "Revenue": (np.random.random(n) * 50 + 10).round(2),
   .....:         "Quantity": np.random.randint(1, 10, size=n),
   .....:     }
   .....: )
   .....: 

In [267]: df.head(2)
Out[267]: 
     Store    Product  Revenue  Quantity
0  Store_2  Product_1    26.12         1
1  Store_2  Product_1    28.86         1

我們現在找出每個商店/產品的價格。

In [268]: (
   .....:     df.groupby(["Store", "Product"])
   .....:     .pipe(lambda grp: grp.Revenue.sum() / grp.Quantity.sum())
   .....:     .unstack()
   .....:     .round(2)
   .....: )
   .....: 
Out[268]: 
Product  Product_1  Product_2
Store                        
Store_1       6.82       7.05
Store_2       6.30       6.64

當您想傳遞群組物件給一些任意函數時,管道也可以表達,例如

In [269]: def mean(groupby):
   .....:     return groupby.mean()
   .....: 

In [270]: df.groupby(["Store", "Product"]).pipe(mean)
Out[270]: 
                     Revenue  Quantity
Store   Product                       
Store_1 Product_1  34.622727  5.075758
        Product_2  35.482815  5.029630
Store_2 Product_1  32.972837  5.237589
        Product_2  34.684360  5.224000

在此,mean 會採用 GroupBy 物件,並分別找出每個商店產品組合的營收和數量欄位的平均值。mean 函數可以是任何採用 GroupBy 物件的函數;.pipe 會將 GroupBy 物件傳遞為您指定的函數中的參數。

範例#

多欄位分解#

透過使用 DataFrameGroupBy.ngroup(),我們可以提取有關群組的資訊,類似於 factorize()(如 重塑 API 中進一步說明),但它自然適用於多個混合類型和不同來源的欄位。這在處理過程中可以作為一個中間類別步驟,當群組列之間的關係比其內容更重要時,或者作為輸入僅接受整數編碼的演算法時,這會很有用。(有關 pandas 對完整類別資料支援的更多資訊,請參閱 類別簡介API 文件。)

In [271]: dfg = pd.DataFrame({"A": [1, 1, 2, 3, 2], "B": list("aaaba")})

In [272]: dfg
Out[272]: 
   A  B
0  1  a
1  1  a
2  2  a
3  3  b
4  2  a

In [273]: dfg.groupby(["A", "B"]).ngroup()
Out[273]: 
0    0
1    0
2    1
3    2
4    1
dtype: int64

In [274]: dfg.groupby(["A", [0, 0, 0, 1, 1]]).ngroup()
Out[274]: 
0    0
1    0
2    1
3    3
4    2
dtype: int64

依索引值進行 Groupby 以「重新取樣」資料#

重新抽樣會從現有的觀察資料或產生資料的模型產生新的假設樣本(重新抽樣)。這些新樣本類似於現有的樣本。

為了讓重新抽樣在非日期時間類型的索引上運作,可以使用下列程序。

在以下範例中,df.index // 5 會傳回一個整數陣列,用於決定在 groupby 運算中選取哪些項目。

注意

以下範例顯示我們如何透過將樣本合併為較少的樣本來進行降採樣。在此,透過使用 df.index // 5,我們將樣本彙總到區段中。透過套用 std() 函數,我們將許多樣本中的資訊彙總成一個較小的值子集,也就是其標準差,進而減少樣本數。

In [275]: df = pd.DataFrame(np.random.randn(10, 2))

In [276]: df
Out[276]: 
          0         1
0 -0.793893  0.321153
1  0.342250  1.618906
2 -0.975807  1.918201
3 -0.810847 -1.405919
4 -1.977759  0.461659
5  0.730057 -1.316938
6 -0.751328  0.528290
7 -0.257759 -1.081009
8  0.505895 -1.701948
9 -1.006349  0.020208

In [277]: df.index // 5
Out[277]: Index([0, 0, 0, 0, 0, 1, 1, 1, 1, 1], dtype='int64')

In [278]: df.groupby(df.index // 5).std()
Out[278]: 
          0         1
0  0.823647  1.312912
1  0.760109  0.942941

傳回 Series 以傳播名稱#

將 DataFrame 欄位分組,計算一組指標,並傳回一個已命名的 Series。Series 名稱用作欄位索引的名稱。這在與堆疊等重塑運算結合使用時特別有用,其中欄位索引名稱將用作插入欄位的名稱

In [279]: df = pd.DataFrame(
   .....:     {
   .....:         "a": [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2],
   .....:         "b": [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1],
   .....:         "c": [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
   .....:         "d": [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
   .....:     }
   .....: )
   .....: 

In [280]: def compute_metrics(x):
   .....:     result = {"b_sum": x["b"].sum(), "c_mean": x["c"].mean()}
   .....:     return pd.Series(result, name="metrics")
   .....: 

In [281]: result = df.groupby("a").apply(compute_metrics, include_groups=False)

In [282]: result
Out[282]: 
metrics  b_sum  c_mean
a                     
0          2.0     0.5
1          2.0     0.5
2          2.0     0.5

In [283]: result.stack(future_stack=True)
Out[283]: 
a  metrics
0  b_sum      2.0
   c_mean     0.5
1  b_sum      2.0
   c_mean     0.5
2  b_sum      2.0
   c_mean     0.5
dtype: float64