最常用的 GroupBy 方法是 apply,apply 将正在操作的对象拆分为多个片段,在每个片段上调用传递给它函数,然后尝试连接这些片段。
还是用前面的小费数据集tips.csv,它的内容如下图:
假设我们想按smoker进行分组并选择前五个tip_pct值:
import numpy as np
import pandas as pdtips = pd.read_csv("examples/tips.csv")
tips["tip_pct"] = tips["tip"] / tips["total_bill"]# 自定义函数top,根据选择列tip_pct值最大的前n行
def top(df, n=5, column="tip_pct"):return df.sort_values(column, ascending=False)[:n]# 然后按 smoker 进行分组,并使用top函数调用 apply:
result = tips.groupby("smoker").apply(top)
print(result)
输出结果:
total_bill | tip | smoker | day | time | size | tip_pct | ||
---|---|---|---|---|---|---|---|---|
smoker | ||||||||
No | 232 | 11.61 | 3.39 | No | Sat | Dinner | 2 | 0.291990 |
149 | 7.51 | 2.00 | No | Thur | Lunch | 2 | 0.266312 | |
51 | 10.29 | 2.60 | No | Sun | Dinner | 2 | 0.252672 | |
185 | 20.69 | 5.00 | No | Sun | Dinner | 5 | 0.241663 | |
88 | 24.71 | 5.85 | No | Thur | Lunch | 2 | 0.236746 | |
Yes | 172 | 7.25 | 5.15 | Yes | Sun | Dinner | 2 | 0.710345 |
178 | 9.60 | 4.00 | Yes | Sun | Dinner | 2 | 0.416667 | |
67 | 3.07 | 1.00 | Yes | Sat | Dinner | 1 | 0.325733 | |
183 | 23.17 | 6.50 | Yes | Sun | Dinner | 4 | 0.280535 | |
109 | 14.31 | 4.00 | Yes | Sat | Dinner | 2 | 0.279525 |
这里发生了什么?首先,根据 smoker 的值将 tips DataFrame 分成几组。然后,在每个组上调用 top 函数,并使用 pandas.concat 将每个调用的结果粘合在一起,也同时使用组名称标记各个部分。因此,结果具有一个分层索引,其内部级别包含来自原始 DataFrame 的索引值。
除了向apply方法传递函数名外,还可以在后面传递其他参数:
import numpy as np
import pandas as pdtips = pd.read_csv("examples/tips.csv")
tips["tip_pct"] = tips["tip"] / tips["total_bill"]# 自定义函数top,根据选择列tip_pct值最大的前n行
def top(df, n=5, column="tip_pct"):return df.sort_values(column, ascending=False)[:n]# 然后按 smoker 进行分组,并使用top函数调用 apply:
#result = tips.groupby("smoker").apply(top)
result = tips.groupby(["smoker", "day"]).apply(top, n=1, column="total_bill")
print(result)
执行上述代码会产生一个警告:DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
result = tips.groupby(["smoker", "day"]).apply(top, n=1, column="total_bill")
那我们按照提示修改下代码:
import numpy as np
import pandas as pdtips = pd.read_csv("examples/tips.csv")
tips["tip_pct"] = tips["tip"] / tips["total_bill"]# 自定义函数top,根据选择列tip_pct值最大的前n行
def top(df, n=5, column="tip_pct"):return df.sort_values(column, ascending=False)[:n]# 然后按 smoker 进行分组,并使用top函数调用 apply:
#result = tips.groupby("smoker").apply(top)
result = tips.groupby(["smoker", "day"]).apply(top, n=1, column="total_bill", include_groups=False)
print(result)
输出结果如下:
total_bill | tip | time | size | tip_pct | |||
---|---|---|---|---|---|---|---|
smoker | day | ||||||
No | Fri | 94 | 22.75 | 3.25 | Dinner | 2 | 0.142857 |
Sat | 212 | 48.33 | 9.00 | Dinner | 4 | 0.186220 | |
Sun | 156 | 48.17 | 5.00 | Dinner | 6 | 0.103799 | |
Thur | 142 | 41.19 | 5.00 | Lunch | 5 | 0.121389 | |
Yes | Fri | 95 | 40.17 | 4.73 | Dinner | 4 | 0.117750 |
Sat | 170 | 50.81 | 10.00 | Dinner | 3 | 0.196812 | |
Sun | 182 | 45.35 | 3.50 | Dinner | 3 | 0.077178 | |
Thur | 197 | 43.11 | 5.00 | Lunch | 4 | 0.115982 |
除了这些基本的使用功能之外,要充分利用 apply 需要一些创造力。传递给apply的函数内部实现什么功能由我们自己决定,但它必须返回 pandas 对象或标量值。下面主要是通过代码示例展示如何使用 groupby 解决各种问题。
例如:对 GroupBy 对象调用了描述性统计函数 describe():
import numpy as np
import pandas as pdtips = pd.read_csv("examples/tips.csv")
tips["tip_pct"] = tips["tip"] / tips["total_bill"]result = tips.groupby("smoker")["tip_pct"].describe()print(result)
print(result.unstack("smoker"))
以上代码对tips按smoker进行分组,然后用describe()方法对tip_pct列进行统计分析,输出该列的均值、最大值、最小值等描述统计标量,输出结果如下:
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
smoker | ||||||||
No | 151.0 | 0.159328 | 0.039910 | 0.056797 | 0.136906 | 0.155625 | 0.185014 | 0.291990 |
Yes | 93.0 | 0.163196 | 0.085119 | 0.035638 | 0.106771 | 0.153846 | 0.195059 | 0.710345 |
然后调用result.unstack("smoker")输出:
隐藏分组用的键
在前面的示例中,生成的对象具有由组键以及原始对象的每个部分的索引组成的分层索引。要隐藏分组的键,可以设置group_keys=False,例如:
import numpy as np
import pandas as pddef top(df, n=5, column="tip_pct"):return df.sort_values(column, ascending=False)[:n]tips = pd.read_csv("examples/tips.csv")
tips["tip_pct"] = tips["tip"] / tips["total_bill"]result = tips.groupby("smoker", group_keys=False).apply(top, include_groups=False)print(result)
输出结果:
total_bill | tip | day | time | size | tip_pct | |
---|---|---|---|---|---|---|
232 | 11.61 | 3.39 | Sat | Dinner | 2 | 0.291990 |
149 | 7.51 | 2.00 | Thur | Lunch | 2 | 0.266312 |
51 | 10.29 | 2.60 | Sun | Dinner | 2 | 0.252672 |
185 | 20.69 | 5.00 | Sun | Dinner | 5 | 0.241663 |
88 | 24.71 | 5.85 | Thur | Lunch | 2 | 0.236746 |
172 | 7.25 | 5.15 | Sun | Dinner | 2 | 0.710345 |
178 | 9.60 | 4.00 | Sun | Dinner | 2 | 0.416667 |
67 | 3.07 | 1.00 | Sat | Dinner | 1 | 0.325733 |
183 | 23.17 | 6.50 | Sun | Dinner | 4 | 0.280535 |
109 | 14.31 | 4.00 | Sat | Dinner | 2 | 0.279525 |
分位数和存储桶分析(Quantile and Bucket Analysis)
之前的学习中,我们学习过 pandas.cut 和 pandas.qcut,使用选择的 bin 或样本分位数将数据切成桶。将这些函数与 groupby 结合使用,可以方便地对数据集执行存储桶或分位数分析。例如:
import numpy as np
import pandas as pdnp.random.seed(12345)
frame = pd.DataFrame({"data1": np.random.standard_normal(1000), "data2": np.random.standard_normal(1000)})
print(frame.head())quartiles = pd.cut(frame["data1"], 4)
print(quartiles.head(10))def get_stats(group):return pd.DataFrame({"min": group.min(), "max": group.max(), "count": group.count(), "mean": group.mean()})grouped = frame.groupby(quartiles, observed=False)
print(grouped.apply(get_stats))
frame.head()输出前5行:
data1 | data2 | |
---|---|---|
0 | -0.578828 | 0.517431 |
1 | 1.847446 | -0.756552 |
2 | 0.453739 | 1.171381 |
3 | -0.302450 | -1.212491 |
4 | 1.402558 | -0.399609 |
quartiles.head(10)输出按4个bin划分后的前10行:
从上面的输出可以看出4个bin分组分别是:[(-2.956, -1.23] < (-1.23, 0.489] < (0.489, 2.208] <
(2.208, 3.928]]
cut 返回的 Categorical 对象可以直接传递给 groupby。因此,我们可以计算四分位数的一组组统计数据,如下所示,grouped.apply(get_stats)输出:
min | max | count | mean | ||
---|---|---|---|---|---|
data1 | |||||
(-2.956, -1.23] | data1 | -2.261761 | 2.505056 | 95 | 0.100124 |
data2 | -2.186301 | 2.615416 | 95 | 0.173283 | |
(-1.23, 0.489] | data1 | -3.428254 | 2.653656 | 595 | 0.023041 |
data2 | -2.909373 | 2.492224 | 595 | 0.004728 | |
(0.489, 2.208] | data1 | -3.184377 | 2.423712 | 299 | -0.043678 |
data2 | -3.548824 | 3.366626 | 299 | -0.061474 | |
(2.208, 3.928] | data1 | -1.471248 | 2.036981 | 11 | -0.172572 |
data2 | -1.093112 | 1.776327 | 11 | 0.082027 |
其实,要输出相同结果还可以使用grouped.agg(["min", "max", "count", "mean"]),这种方法更简便:
import numpy as np
import pandas as pdnp.random.seed(12345)
frame = pd.DataFrame({"data1": np.random.standard_normal(1000), "data2": np.random.standard_normal(1000)})
print(frame.head())quartiles = pd.cut(frame["data1"], 4)
print(quartiles.head(10))#def get_stats(group):
# return pd.DataFrame({"min": group.min(), "max": group.max(),
# "count": group.count(), "mean": group.mean()})grouped = frame.groupby(quartiles, observed=False)
result = grouped.agg(["min", "max", "count", "mean"])
print(result)
输出:
data1 | data2 | |||||||
---|---|---|---|---|---|---|---|---|
min | max | count | mean | min | max | count | mean | |
data1 | ||||||||
(-2.956, -1.23] | -2.261761 | 2.505056 | 95 | 0.100124 | -2.186301 | 2.615416 | 95 | 0.173283 |
(-1.23, 0.489] | -3.428254 | 2.653656 | 595 | 0.023041 | -2.909373 | 2.492224 | 595 | 0.004728 |
(0.489, 2.208] | -3.184377 | 2.423712 | 299 | -0.043678 | -3.548824 | 3.366626 | 299 | -0.061474 |
(2.208, 3.928] | -1.471248 | 2.036981 | 11 | -0.172572 | -1.093112 | 1.776327 | 11 | 0.082027 |
注意:这些是等长的桶(bin)。要根据样本分位数计算等长的存储桶,可以使用 pandas.qcut。我们可以传递 4 作为存储桶计算样本四分位数,并传递 labels=False 仅获取四分位数索引而不是区间:
import numpy as np
import pandas as pdnp.random.seed(12345)
frame = pd.DataFrame({"data1": np.random.standard_normal(1000), "data2": np.random.standard_normal(1000)})
print(frame.head())#quartiles = pd.cut(frame["data1"], 4)
quartiles_samp = pd.qcut(frame["data1"], 4, labels=False)
print(quartiles_samp.head())def get_stats(group):return pd.DataFrame({"min": group.min(), "max": group.max(), "count": group.count(), "mean": group.mean()})
grouped = frame.groupby(quartiles_samp)
print(grouped.apply(get_stats))
quartiles_samp.head() 输出:
grouped.apply(get_stats)输出:
min | max | count | mean | ||
---|---|---|---|---|---|
data1 | |||||
0 | data1 | -3.428254 | -0.710197 | 250 | -1.272969 |
data2 | -2.909373 | 2.531127 | 250 | -0.076649 | |
1 | data1 | -0.705960 | 0.014029 | 250 | -0.338060 |
data2 | -3.548824 | 2.419003 | 250 | -0.007728 | |
2 | data1 | 0.016520 | 0.715103 | 250 | 0.360921 |
data2 | -2.611124 | 3.366626 | 250 | 0.046946 | |
3 | data1 | 0.718557 | 2.653656 | 250 | 1.283159 |
data2 | -2.748685 | 2.615416 | 250 | 0.044617 |
下面我们使用四个代码示例学习之前学过的内容。
Example 1:使用特定于组的值 填充 缺失值
在清理缺失数据时,可以使用 dropna 删除,但在其他情况下,我们希望使用固定值或从数据派生的某些值来填充空值 (NA)。那可以使用 Fillna 方法。这里我们用平均值填充 null 值:
import numpy as np
import pandas as pdnp.random.seed(12345)
s = pd.Series(np.random.standard_normal(6))
s[::2] = np.nan
print(s)
result = s.fillna(s.mean())
print(result)
填充前s输出:
用均值填充后输出:
假设我们需要 fill 的值因组而异。一种方法是对数据进行分组,并将 apply 与在每个数据块上调用 fillna 的函数一起使用:
import numpy as np
import pandas as pdnp.random.seed(12345)
states = ["Ohio", "New York", "Vermont", "Florida","Oregon", "Nevada", "California", "Idaho"]
group_key = ["East", "East", "East", "East","West", "West", "West", "West"]
data = pd.Series(np.random.standard_normal(8), index=states)
print(data)# 给data设置一些缺失值
data[["Vermont", "Nevada", "Idaho"]] = np.nan
print(data)# 按分组键列表中的键进行分组,返回每组的大小
data.groupby(group_key).size()# 按分组键列表中的键进行分组,返回每组的值的计数
data.groupby(group_key).count()# 按分组键列表中的键进行分组,返回每组的平均值
data.groupby(group_key).mean()# 使用组均值填充 NA 值,如下,先自定义一个填充函数:
def fill_mean(group):return group.fillna(group.mean())# 传递自定义函数fill_mean给apply,用每组的均值填充各组的缺失值
result = data.groupby(group_key).apply(fill_mean)
print(result)# 另一种情况:在代码中预定义每组的填充值,并用这些值填充每组的缺失值。
# 由于groups 内部有一个 name 属性,我们可以使用name属性,来指定为每组填充
fill_values = {"East": 0.5, "West": -1}
def fill_func(group):return group.fillna(fill_values[group.name])
result = data.groupby(group_key).apply(fill_func)
print(result)
data设置缺失值后输出:
用每组的平均值填充后输出:
用指定的每组填充值填充后输出:
Example 2 :随机采样和排列
假设我们想从大型数据集中随机抽取样本,那有多种方法可以执行;这里我们使用 Series 的 sample 方法。下面我们用代码来演示,代码中会构建一副扑克牌。
import numpy as np
import pandas as pd# Hearts, Spades, Clubs, Diamonds
# 红心、黑桃、梅花、方块
suits = ["H", "S", "C", "D"]
card_val = (list(range(1, 11)) + [10] * 3) * 4
base_names = ["A"] + list(range(2, 11)) + ["J", "K", "Q"]
cards = []
for suit in suits:cards.extend(str(num) + suit for num in base_names)deck = pd.Series(card_val, index=cards)# 现在我们有一个长度为 52 的 Series,
# 其索引包含牌名,
# 值是二十一点和其他游戏中使用的值
print(deck.head(13))# 从牌堆中抽一手五张牌可以写成:
def draw(deck, n=5):return deck.sample(n)
print(draw(deck))# 假设要随机抽取每种花色的两张牌,
# 因为花色是每张牌名的最后一个字符,所以我们可以基于此进行分组并使用 apply:
# 自定义一个函数get_suit返回牌的花色
def get_suit(card):# 最后一个字母是花色return card[-1]# 随机抽每种花色的2张牌
result = deck.groupby(get_suit).apply(draw, n=2)
print(result)
代码中首先构建了一副扑克牌,deck.head(13)输出13张:
draw(deck)随机抽5张,输出:
最后按花色分组,每种花色抽两张输出:
Example 3 :组加权平均值和相关性
在 groupby 的 split-apply-combine 范式下,可以在 DataFrame 或两个 Series 中的列之间进行操作,例如求组加权平均。示例:
import numpy as np
import pandas as pdnp.random.seed(12345)df = pd.DataFrame({"category": ["a", "a", "a", "a", "b", "b", "b", "b"], "data": np.random.standard_normal(8), "weights": np.random.uniform(size=8)})
print(df)# 然后按类别(category)划分,求加权平均值
grouped = df.groupby("category")
# 自定义函数get_wavg求加权平均
def get_wavg(group):return np.average(group["data"], weights=group["weights"])
result = grouped.apply(get_wavg)
print(result)
df输出:
category | data | weights | |
---|---|---|---|
0 | a | 0.006151 | 0.149751 |
1 | a | -0.500004 | 0.240539 |
2 | a | -1.310766 | 0.630025 |
3 | a | -0.832910 | 0.763095 |
4 | b | -0.055174 | 0.918371 |
5 | b | -1.444944 | 0.054725 |
6 | b | 0.944028 | 0.028265 |
7 | b | 0.064720 | 0.572253 |
按类别分组求加权平均输出:
再举一个例子,用一个金融数据集来展示,其中包含一些股票的收盘价和标准普尔 500 指数(SPX 代码),部分内容如下截图:
import numpy as np
import pandas as pdclose_px = pd.read_csv("examples/stock_px.csv", parse_dates=True, index_col=0)# 输出数据集DataFrame对象close_px的相关概览信息
print(close_px.info())
# 输出数据集前4行看看
print(close_px.tail(4))# 下面我们设置一个任务:生成一个 DataFrame,该 DataFrame 由每日回报的年度相关性(根据百分比变化计算)与 SPX 组成。
# 为此,1.首先创建一个函数,用于计算每列与 “SPX” 列的成对相关性:
def spx_corr(group):return group.corrwith(group["SPX"])# 接下来,2.使用 pct_change() 计算close_px的百分比变化:
rets = close_px.pct_change().dropna()# 最后,按年份对这些百分比变化进行分组,
# 使用返回每个日期时间标签的 year 属性的单行函数从每个行标签中提取这些变化
def get_year(x):return x.year
by_year = rets.groupby(get_year)
result = by_year.apply(spx_corr)# 输出结果
print(result)# 另一个任务:计算列间相关性。比如:计算 Apple 和 Microsoft 之间的年度相关性:
def corr_aapl_msft(group):return group["AAPL"].corr(group["MSFT"])
result2 = by_year.apply(corr_aapl_msft)
# 输出结果
print(result2)
数据集的概览信息输出:
数据集的后4行输出:
AAPL | MSFT | XOM | SPX | |
---|---|---|---|---|
2011-10-11 | 400.29 | 27.00 | 76.27 | 1195.54 |
2011-10-12 | 402.19 | 26.96 | 77.16 | 1207.25 |
2011-10-13 | 408.43 | 27.18 | 76.37 | 1203.66 |
2011-10-14 | 422.00 | 27.27 | 78.11 | 1224.58 |
第一个任务输出:
AAPL | MSFT | XOM | SPX | |
---|---|---|---|---|
2003 | 0.541124 | 0.745174 | 0.661265 | 1.0 |
2004 | 0.374283 | 0.588531 | 0.557742 | 1.0 |
2005 | 0.467540 | 0.562374 | 0.631010 | 1.0 |
2006 | 0.428267 | 0.406126 | 0.518514 | 1.0 |
2007 | 0.508118 | 0.658770 | 0.786264 | 1.0 |
2008 | 0.681434 | 0.804626 | 0.828303 | 1.0 |
2009 | 0.707103 | 0.654902 | 0.797921 | 1.0 |
2010 | 0.710105 | 0.730118 | 0.839057 | 1.0 |
2011 | 0.691931 | 0.800996 | 0.859975 | 1.0 |
第二个任务输出:
Example4:分组线性回归(Group-Wise Linear Regression)
对于Example3中的数据集,还可以使用 groupby 执行更复杂的分组统计分析,只要该函数返回 pandas 对象或标量值即可。例如,我们使用 statsmodels 计量经济学库自定义一个回归函数,该函数对每个数据块执行普通最小二乘法 (OLS) 回归。先pip install statsmodels。
import numpy as np
import pandas as pd
import statsmodels.api as smclose_px = pd.read_csv("examples/stock_px.csv", parse_dates=True, index_col=0)# 下面我们设置一个任务:生成一个 DataFrame,该 DataFrame 由每日回报的年度相关性(根据百分比变化计算)与 SPX 组成。
# 为此,1.首先创建一个函数,用于计算每列与 “SPX” 列的成对相关性:
def spx_corr(group):return group.corrwith(group["SPX"])# 接下来,2.使用 pct_change() 计算close_px的百分比变化:
rets = close_px.pct_change().dropna()# 最后,按年份对这些百分比变化进行分组,
# 使用返回每个日期时间标签的 year 属性的单行函数从每个行标签中提取这些变化
def get_year(x):return x.year
by_year = rets.groupby(get_year)# 自定义 普通最小二乘法 (OLS) 回归
def regress(data, yvar=None, xvars=None):Y = data[yvar]X = data[xvars]X["intercept"] = 1.result = sm.OLS(Y, X).fit()return result.params# 计算APPL SPX回报的年度线性回归
result = by_year.apply(regress, yvar="AAPL", xvars=["SPX"])
print(result)
输出结果: