《利用Python进行数据分析》读书笔记。
第 9 章:数据聚合和分组运算。
准备好数据集后,通常的任务是进行分组统计,或生成透视表。
pandas提供了 groupby 和 pivot, 可以方便的进行这些操作。
groupby
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
分组运算的典型过程为:split-apply-combine (拆分-应用-合并),如下图:
用pandas进行分组很灵活:
在维度上可以任意选择。例如, DataFrame可以在行(axis=0)或列(axis=1)上进行分组。
在分组键上,可以有多种形式,比如列名,关于名/值的数组、列表、字典、Series等,甚至可以使用函数。
一个简单的例子:
df = DataFrame({
'key1': ['a','a','b','b','a'],
'key2': ['one','two','one','two','one'],
'data1': np.random.randn(5),
'data2': np.random.randn(5)
})
df
# 根据key1 进行分组,并计算data1列的平均值
# 结果是一个Series
grouped = df['data1'].groupby(df['key1'])
grouped.mean()
#两个维度(key1,key2)上的分组
# 结果是一个Series
means = df['data1'].groupby([df['key1'], df['key2']]).mean()
means
# 转换成DataFrame
means.unstack()
# 分组键不仅可以是Series
# 比如,可以是数组(需要长度适当)
states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
years = np.array([2005, 2005, 2006, 2005, 2006])
df['data1'].groupby([states, years]).mean()
# 可以将列名(字符串、数组或其他对象)用作分组键
df.groupby('key1').mean()
df.groupby(['key1', 'key2']).mean()
df.groupby(['key1', 'key2']).size()
对分组进行迭代¶
# groupby 的结果是 GroupBy 对象。
# 可以进行迭代:
for name, group in df.groupby('key1'):
print('=================================')
print(name)
print('----')
print(group)
# 多重键时, 元组的第一个元素是 键值的组合
for (k1, k2), group in df.groupby(['key1', 'key2']):
print('=================================')
print((k1, k2))
print('----')
print(group)
# 可以利用这些数据片段。比如,做成一个字段
pieces = dict(list(df.groupby('key1')))
pieces
pieces['b']
# groupby默认是在 axis=0 上分组,其实可以在任何轴上进行分组
# 比如,根据 dtype对列进行分组
df.dtypes
grouped = df.groupby(df.dtypes, axis=1)
dict(list(grouped))
选取一个或一组列¶
对groupby产生的 GroupBy 进行索引,能实现选取部分列进行聚合的目的。索引可以是一个或一组字符串。
对于大数据集,可能只需要对部分列进行聚合,这种方法就很有用。比如:只计算data2列的平均值:
# 等价于 df['data2'].groupby(df['key1'])
df.groupby('key1')['data2'].mean()
# 直接转换为 DataFrame
# 等价于 df[['data2']].groupby(df['key1'])
df.groupby('key1')[['data2']].mean()
# 多个键值
df.groupby(['key1', 'key2'])[['data2']].mean()
通过字典或Series进行分组¶
people = DataFrame(np.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people.ix[2:3, ['b', 'c']] = np.nan # Add a few NA values
people
# 使用字典,根据列的分组关系计算总和
mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
'd': 'orange', 'e': 'red', 'f' : 'blue'}
by_column = people.groupby(mapping, axis=1)
by_column.sum()
# 使用 Series作为分组键, pandas会检查 Series以确保其索引与分组轴是对其的
map_series = Series(mapping)
map_series
people.groupby(map_series, axis=1).count()
通过函数进行分组¶
函数作为分组键时,会在各个索引值上被调用一次,起返回值作为分组名称。
people.groupby(len).sum()
# 数组,列表,字典,Series,函数可以混合分组
key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len, key_list]).min()
根据索引级别分组¶
层次化的索引,可以根据索引级别进行聚合。通过level关键字传入级别编号或名称即可。
columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
[1, 3, 5, 1, 3]], names=['cty', 'tenor'])
hier_df = DataFrame(np.random.randn(4, 5), columns=columns)
hier_df
hier_df.groupby(level='cty', axis=1).count()
hier_df.groupby(level=1, axis=1).count()
数据聚合
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
GroupBy对象上面优化了一些方法,可以快速进行统计计算,比如:
- count
- sum
- mean
- median(中位数)
- std, var(标准差、方差)
- min, max
- prod (积)
- first, last
但是不仅如此,可以自定义聚合运算。比如, quantile可以计算Series或 DataFrame列的样本分位数:
df = DataFrame({
'key1': ['a','a','b','b','a'],
'key2': ['one','two','one','two','one'],
'data1': np.random.randn(5),
'data2': np.random.randn(5)
})
df
grouped = df.groupby('key1')
grouped['data1'].quantile(0.9)
如果需要自定义聚合函数,将其传入 aggregate 或 agg 方法即可:
def peak_to_peak(arr):
return arr.max() - arr.min()
grouped.agg(peak_to_peak)
# 数据准备
tips = pd.read_csv('data/ch08/tips.csv')
# 增加小费占比(tip_pct)
tips['tip_pct'] = tips['tip']/tips['total_bill']
tips.head()
# 根据sex和smoker对tips进行分组
grouped = tips.groupby(['sex', 'smoker'])
grouped_pct = grouped['tip_pct']
# 对于优化过的描述统计,可以直接传入函数名
grouped_pct.agg('mean').unstack()
# 传入一组函数/函数名,得到的DataFrame列会自动命名
grouped_pct.agg(['mean', 'std', peak_to_peak])
# 通过 (name, function)元组指定列名
grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
# 对所有列应用一组函数
functions = ['count', 'mean', 'max']
result = grouped['tip_pct', 'total_bill'].agg(functions)
result
# 可以指定一组函数的名称
ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
grouped['tip_pct', 'total_bill'].agg(ftuples)
# 对不同的列应用不同的函数
grouped.agg({'tip' : np.max, 'size' : 'sum'})
# 更复杂的
grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
'size' : 'sum'})
以“无索引”的形式返回聚合数据¶
参数: as_index=False
tips.groupby(['sex', 'smoker'], as_index=False).mean()
# 对比
tips.groupby(['sex', 'smoker']).mean()
分组运算和转换
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
聚合,只是分组运算中的一种,是数据转换的一种方法:将一维数组简化为标量值。
更多的分组运算,可以通过 transform和apply方法指定。
df = DataFrame({
'key1': ['a','a','b','b','a'],
'key2': ['one','two','one','two','one'],
'data1': np.random.randn(5),
'data2': np.random.randn(5)
})
df
k1_means = df.groupby('key1').mean().add_prefix('mean_')
k1_means
pd.merge(df, k1_means, left_on='key1', right_index=True)
# 通过transform完成
df.groupby('key1').transform(np.mean)
transform会将一个函数应用到各个分组,然后将结果放置到适当的位置。
如果各分组产生的是一个标量值,则该值会被广播出去。
下面实现从各组中减去平均值。
# 创建一个距平化函数(demeaning function)
def demean(arr):
return arr - arr.mean()
demeaned = df.groupby('key1').transform(demean)
demeaned
# 检查一下,此时平均值应该为0:
demeaned.mean()
apply: 一般性的“拆分-应用-合并”¶
transform与aggregate一样,对函数有严格条件:其结果要么产生一个可以广播的标量值,如np.mean, 要么产生一个相同大小的结果数组。
最一般化的groupby方法是apply。apply会将待处理的对象拆分成多个片段,然后对个片段调用传入的函数,最后尝试将各个片段组合到一起。
# 数据准备
tips = pd.read_csv('data/ch08/tips.csv')
# 增加小费占比(tip_pct)
tips['tip_pct'] = tips['tip']/tips['total_bill']
tips.head()
# 假设要根据分组选出最高的5个tip_pct值
# 编写一个选取某个列具有最大值的行的函数
def top(df, n=5, column='tip_pct'):
return df.sort_values(by=column)[-n:]
top(tips,n=6)
# 现在,对smoker进行分组,并apply该函数
tips.groupby('smoker').apply(top)
# 传入apply函数的参数
tips.groupby(['smoker','day']).apply(top, n=1, column='total_bill')
# 禁用分组键
tips.groupby(['smoker','day'], group_keys=False).apply(top, n=1, column='total_bill')
分位数和桶分析¶
将分块工具(比如cut,qcut)与groupby结合起来,能非常轻松实现分位数(quantile)或桶(bucket)分析。
frame = DataFrame({'data1': np.random.randn(1000),
'data2': np.random.randn(1000)})
# 使用cut,将数据装入长度相等的桶中
factor = pd.cut(frame.data1, 4)
factor[:10]
# cut返回的Factor对象,可以直接用于groupby
def get_stats(group):
return {'min': group.min(), 'max': group.max(), 'count': group.count(), 'mean': group.mean()}
grouped = frame.data2.groupby(factor)
grouped.apply(get_stats).unstack()
# 使用qcut,根据样本分位数得到大小相等的桶。
# 传入labels=False可以只获取分位数的编号
grouping = pd.qcut(frame.data1, 10, labels=False)
grouped = frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()
示例:用特定于分组的值填充缺失值¶
s = Series(np.random.randn(6))
s[::2] = np.nan
s
s.fillna(s.mean())
states = ['Ohio', 'New York', 'Vermont', 'Florida',
'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4
data = Series(np.random.randn(8), index=states)
data[['Vermont', 'Nevada', 'Idaho']] = np.nan
data
data.groupby(group_key).mean()
# 用分组平均值填充 NA 值
fill_mean = lambda g: g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
# 指定填充
fill_values = {'East': 0.5, 'West': -1}
fill_func = lambda g: g.fillna(fill_values[g.name])
data.groupby(group_key).apply(fill_func)
示例:随机采样和排列¶
一个随机采样的方法:选取np.random.permutation(N)的前K个元素。其中,N为总体个数,K为期望的样本大小。 比如,一个扑克牌。
# suite: 花色: 红桃 Hearts, 黑桃 Spades, 梅花 Clubs, 方块 Diamonds
suits = ['H', 'S','C','D']
# 点数: 在21点中的取值 1,2,3,...9,10,10,10,10
card_val = (list(range(1, 11)) + [10] * 3) * 4
# 牌面
base_names = ['A'] + list(range(2,11)) + ['J','Q','K']
cards = []
for suit in ['H', 'S', 'C', 'D']:
cards.extend(str(num) + suit for num in base_names)
# 一副扑克牌(52张)
deck = Series(card_val, index=cards)
deck.head()
# 随机抽五张
def draw(deck,n=5):
return deck.take(np.random.permutation(len(deck))[:n])
draw(deck)
# 每种花色抽2张
get_suit = lambda card: card[-1]
deck.groupby(get_suit).apply(draw,n=2)
# 去掉键值
deck.groupby(get_suit, group_keys=False).apply(draw,n=2)
实例:分组加权平均数和相关系数¶
df = DataFrame({'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
'data': np.random.randn(8),
'weights': np.random.rand(8)})
df
# 利用category 计算分组加权平均数
grouped = df.groupby('category')
get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
grouped.apply(get_wavg)
# 标普500指数和几只股票的收盘价数据
close_px = pd.read_csv('data/ch09/stock_px.csv', parse_dates=True, index_col=0)
close_px.info()
# 任务:计算日收益率与SPX之间的年度相关系数
rets = close_px.pct_change().dropna()
spx_corr = lambda x: x.corrwith(x['SPX'])
by_year = rets.groupby(lambda x: x.year)
by_year.apply(spx_corr)
# 苹果与微软的年度相关系数
by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
示例:面向分组的线性回归¶
import statsmodels.api as sm
def regress(data, yvar, xvars):
Y = data[yvar]
X = data[xvars]
X['intercept'] = 1.
result = sm.OLS(Y, X).fit()
return result.params
by_year.apply(regress, 'AAPL', ['SPX'])
透视表和交叉表
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
透视表(pivot table), 一种常见的数据汇总工具。根据一个或多个键对数据进行聚合,并根据行和列上的分组键将数据分配到各个矩形区域中。
pandas中,groupby功能可以实现透视表。
更方便的,提供了 pandas.pivot_table函数和 DataFrame的pivot_table 方法。
pivot_table 方法的主要参数包括:
- values: 待聚合的列名。默认为所有列
- rows: 用于分组的列名或其他键,出现在结果透视表的行
- cols: 用于分组的列名或其他键,出现在结果透视表的列
- aggfunc: 聚合函数/函数列表,默认为'mean'。可以是任何对groupby有效的函数
- fill_value: 填充 NA的值
- margins: 是否显示行列小计和总计,默认为False
# 数据准备
tips = pd.read_csv('data/ch08/tips.csv')
# 增加小费占比(tip_pct)
tips['tip_pct'] = tips['tip']/tips['total_bill']
tips.head()
# 根据sex和smoker计算分组平均数
tips.pivot_table(index=['sex', 'smoker'])
# 聚合tip_pct和size, 根据day进行分组
tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
columns='smoker')
# 添加分项小计
tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
columns='smoker', margins=True)
# 通过aggfunc传入其他聚合函数
tips.pivot_table('tip_pct', index=['sex', 'smoker'], columns='day',
aggfunc=len, margins=True)
# 填充空值
tips.pivot_table('size', index=['time', 'sex', 'smoker'],
columns='day', aggfunc='sum', fill_value=0)
交叉表 crosstab¶
交叉表,是用于计算分组频率的特殊透视表。
import io
#from io.StringIO import StringIO
data = """\
Sample Gender Handedness
1 Female Right-handed
2 Male Left-handed
3 Female Right-handed
4 Male Right-handed
5 Male Left-handed
6 Male Right-handed
7 Female Right-handed
8 Female Left-handed
9 Male Right-handed
10 Female Right-handed"""
data = pd.read_table(io.StringIO(data), sep='\s+')
data
pd.crosstab(data.Gender,data.Handedness, margins=True)
pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)
示例:2012联邦选举委员会数据库
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
# 数据准备
fec = pd.read_csv('data/ch09/P00000001-ALL.csv')
fec.info()
# 一行的数据记录如下
fec.ix[123456]
# 通过unique,获取候选人名单
unique_cands = fec.cand_nm.unique()
unique_cands
# 党派关系数据
parties = {'Bachmann, Michelle': 'Republican',
'Cain, Herman': 'Republican',
'Gingrich, Newt': 'Republican',
'Huntsman, Jon': 'Republican',
'Johnson, Gary Earl': 'Republican',
'McCotter, Thaddeus G': 'Republican',
'Obama, Barack': 'Democrat',
'Paul, Ron': 'Republican',
'Pawlenty, Timothy': 'Republican',
'Perry, Rick': 'Republican',
"Roemer, Charles E. 'Buddy' III": 'Republican',
'Romney, Mitt': 'Republican',
'Santorum, Rick': 'Republican'}
fec.cand_nm[123456:123461]
fec.cand_nm[123456:123461].map(parties)
# 添加党派信息列
fec['party'] = fec.cand_nm.map(parties)
fec['party'].value_counts()
# 剔除退款数据
fec = fec[fec.contb_receipt_amt > 0]
# 只查看两位主要候选人
fec_mrbo = fec[fec.cand_nm.isin(['Obama, Barack', 'Romney, Mitt'])]
fec_mrbo.head()
根据职业和雇主统计赞助信息¶
# 根据职业计算出资总额
fec.contbr_occupation.value_counts()[:10]
# 清理一些类似职业(将其映射到另一个职业)
# 这里巧妙利用了dict.get, 运行没有映射关系的职业也能“通过”
occ_mapping = {
'INFORMATION REQUESTED PER BEST EFFORTS' : 'NOT PROVIDED',
'INFORMATION REQUESTED' : 'NOT PROVIDED',
'INFORMATION REQUESTED (BEST EFFORTS)' : 'NOT PROVIDED',
'C.E.O.': 'CEO'
}
# 如果没有提供相关映射,则返回x
f = lambda x: occ_mapping.get(x, x)
fec.contbr_occupation = fec.contbr_occupation.map(f)
# 对雇主信息进行同样的处理
emp_mapping = {
'INFORMATION REQUESTED PER BEST EFFORTS' : 'NOT PROVIDED',
'INFORMATION REQUESTED' : 'NOT PROVIDED',
'SELF' : 'SELF-EMPLOYED',
'SELF EMPLOYED' : 'SELF-EMPLOYED',
}
# If no mapping provided, return x
f = lambda x: emp_mapping.get(x, x)
fec.contbr_employer = fec.contbr_employer.map(f)
# 根据党派和职业进行聚合,得到 对个党派总出资最高的职业
by_occupation = fec.pivot_table('contb_receipt_amt',
index='contbr_occupation',
columns='party', aggfunc='sum')
# 过滤掉小于200万的赞助
over_2mm = by_occupation[by_occupation.sum(1) > 2000000]
over_2mm
# 绘制柱状图
over_2mm.plot(kind='barh')
# 计算对两位候选人总出资最高的职业和企业
def get_top_amounts(group, key, n=5):
totals = group.groupby(key)['contb_receipt_amt'].sum()
# Order totals by key in descending order
return totals.order(ascending=False)[-n:]
# 根据职业和雇主进行聚合
grouped = fec_mrbo.groupby('cand_nm')
grouped.apply(get_top_amounts, 'contbr_occupation', n=7)
grouped.apply(get_top_amounts, 'contbr_employer', n=10)
对出资额分组¶
bins = np.array([0, 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000])
# 用cut函数根据出资额大小将数据离散化到多个面元中
labels = pd.cut(fec_mrbo.contb_receipt_amt, bins)
labels
# 根据候选人和面元标签对数据进行分组
grouped = fec_mrbo.groupby(['cand_nm', labels])
grouped.size().unstack(0)
# 规整,以便画图
bucket_sums = grouped.contb_receipt_amt.sum().unstack(0)
bucket_sums
normed_sums = bucket_sums.div(bucket_sums.sum(axis=1), axis=0)
normed_sums
normed_sums[:-2].plot(kind='barh', stacked=True)
根据州统计赞助信息¶
# 根据候选人和州进行聚合
grouped = fec_mrbo.groupby(['cand_nm', 'contbr_st'])
totals = grouped.contb_receipt_amt.sum().unstack(0).fillna(0)
totals = totals[totals.sum(1) > 100000]
totals[:10]
percent = totals.div(totals.sum(1), axis=0)
percent[:10]
# 绘制地图
from mpl_toolkits.basemap import Basemap, cm
# 略