与公众号同步更新,详细内容及相关ipynb文件在公众号中,公众号:AI入门小白
文章目录
import numpy as np
import pandas as pd
PREVIOUS_MAX_ROWS = pd.options.display.max_rows
pd.options.display.max_rows = 20
np.random.seed(12345)
import matplotlib.pyplot as plt
plt.rc('figure', figsize=(10, 6))
np.set_printoptions(precision=4, suppress=True)
GroupBy机制
Hadley Wickham(许多热⻔R语⾔包的作者)创造了⼀个⽤于表示分组运算的术语"split-apply-combine"(拆分-应⽤-合并)。第⼀个阶段,pandas对象(⽆论是Series、DataFrame还是其他的)中的数据会根据你所提供的⼀个或多个键被拆分(split)为多组。拆分操作是在对象的特定轴上执⾏的。例如,DataFrame可以在其⾏(axis=0)或列(axis=1)上进⾏分组。然后,将⼀个函数应⽤(apply)到各个分组并产⽣⼀个新值。最后,所有这些函数的执⾏结果会被合并(combine)到最终的结果对象中。结果对象的形式⼀般取决于数据上所执⾏的操作。
图10-1⼤致说明了⼀个简单的分组聚合过程。
分组键可以有多种形式,且类型不必相同:
- 列表或数组,其⻓度与待分组的轴⼀样。
- 表示DataFrame某个列名的值。
- 字典或Series,给出待分组轴上的值与分组名之间的对应关系。
- 函数,⽤于处理轴索引或索引中的各个标签。
注意,后三种都只是快捷⽅式⽽已,其最终⽬的仍然是产⽣⼀组⽤于拆分对象的值。如果觉得这些东⻄看起来很抽象,不⽤担⼼,我将在本章中给出⼤量有关于此的示例。⾸先来看看下⾯这个⾮常简单的表格型数据集(以DataFrame的形式):
df = pd.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列的平均值。实现该功能的⽅式有很多,⽽我们这⾥要⽤的是:访问data1,并根据key1调⽤groupby:
grouped = df['data1'].groupby(df['key1'])
grouped
变量grouped是⼀个GroupBy对象。它实际上还没有进⾏任何计算,只是含有⼀些有关分组键df['key1']
的中间数据⽽已。换句话说,该对象已经有了接下来对各分组执⾏运算所需的⼀切信息。例如,我们可以调⽤GroupBy的mean⽅法来计算分组平均值:
grouped.mean()
数据(Series)根据分组键进⾏了聚合,产⽣了⼀个新的Series,其索引为key1列中的唯⼀值。之所以结果中索引的名称为key1,是因为原始DataFrame的列df['key1']
就叫这个名字。
如果我们⼀次传⼊多个数组的列表,就会得到不同的结果:
means = df['data1'].groupby([df['key1'], df['key2']]).mean()
means
这⾥,我通过两个键对数据进⾏了分组,得到的Series具有⼀个层次化索引(由唯⼀的键对组成):
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()
通常,分组信息就位于相同的要处理DataFrame中。这⾥,你还可以将列名(可以是字符串、数字或其他Python对象)⽤作分组键:
df.groupby('key1').mean()
df.groupby(['key1', 'key2']).mean()
第⼀个例⼦在执⾏df.groupby('key1').mean()
时,结果中没有key2列。这是因为df['key2']
不是数值数据(俗称“麻烦列”),所以被从结果中排除了。默认情况下,所有数值列都会被聚合,虽然有时可能会被过滤为⼀个⼦集。
⽆论你准备拿groupby做什么,都有可能会⽤到GroupBy的size⽅法,它可以返回⼀个含有分组⼤⼩的Series:
df.groupby(['key1', 'key2']).size()
注意,任何分组关键词中的缺失值,都会被从结果中除去。
对分组进⾏迭代
GroupBy对象⽀持迭代,可以产⽣⼀组⼆元元组(由分组名和数据块组成)。看下⾯的例⼦:
for name, group in df.groupby('key1'):
print(name)
print(group)
对于多重键的情况,元组的第⼀个元素将会是由键值组成的元组:
for (k1, k2), group in df.groupby(['key1', 'key2']):
print((k1, k2))
print(group)
可以对这些数据⽚段做任何操作。有⼀个你可能会觉得有⽤的运算:将这些数据⽚段做成⼀个字典:
pieces = dict(list(df.groupby('key1')))
pieces['b']
groupby默认是在axis=0上进⾏分组的,通过设置也可以在其他任何轴上进⾏分组。拿上⾯例⼦中的df来说,我们可以根据dtype对列进⾏分组:
df.dtypes
可以如下打印分组:
grouped = df.groupby(df.dtypes, axis=1)
for dtype, group in grouped:
print(dtype)
print(group)
选取⼀列或列的⼦集
对于由DataFrame产⽣的GroupBy对象,如果⽤⼀个(单个字符串)或⼀组(字符串数组)列名对其进⾏索引,就能实现选取部分列进⾏聚合的⽬的。也就是说:
df.groupby('key1')['data1']
df.groupby('key1')[['data2']]
是以下代码的语法糖:
df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])
尤其对于⼤数据集,很可能只需要对部分列进⾏聚合。例如,在前⾯那个数据集中,如果只需计算data2列的平均值并以DataFrame形式得到结果,可以这样写:
df.groupby(['key1', 'key2'])[['data2']].mean()
这种索引操作所返回的对象是⼀个已分组的DataFrame(如果传⼊的是列表或数组)或已分组的Series(如果传⼊的是标量形式的单个列名):
s_grouped = df.groupby(['key1', 'key2'])['data2']
s_grouped.mean()
通过字典或Series进⾏分组
除数组以外,分组信息还可以其他形式存在。来看另⼀个示例DataFrame:
people = pd.DataFrame(np.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people.iloc[2:3, [1, 2]] = np.nan # 添加一些NA值
people
现在,假设已知列的分组关系,并希望根据分组计算列的和:
mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
'd': 'blue', 'e': 'red', 'f' : 'orange'}
可以将这个字典传给groupby,来构造数组,但我们可以直接传递字典(我包含了键“f”来强调,存在未使⽤的分组键是可以的):
by_column = people.groupby(mapping, axis=1)
by_column.sum()
Series也有同样的功能,它可以被看做⼀个固定⼤⼩的映射:
map_series = pd.Series(mapping)
map_series
people.groupby(map_series, axis=1).count()
通过函数进⾏分组
⽐起使⽤字典或Series,使⽤Python函数是⼀种更原⽣的⽅法定义分组映射。任何被当做分组键的函数都会在各个索引值上被调⽤⼀次,其返回值就会被⽤作分组名称。具体点说,以上⼀⼩节的示例DataFrame为例,其索引值为⼈的名字。你可以计算⼀个字符串⻓度的数组,更简单的⽅法是传⼊len函数:
people.groupby(len).sum() # 人名长度(索引长度)分别为3,5,6
将函数跟数组、列表、字典、Series混合使⽤也不是问题,因为任何东⻄在内部都会被转换为数组:
key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len, key_list]).min()
根据索引级别分组
层次化索引数据集最⽅便的地⽅就在于它能够根据轴索引的⼀个级别进⾏聚合:
columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
[1, 3, 5, 1, 3]],
names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
hier_df
要根据级别分组,使⽤level关键字传递级别序号或名字:
hier_df.groupby(level='cty', axis=1).count()
数据聚合
聚合指的是任何能够从数组产⽣标量值的数据转换过程。之前的例⼦已经⽤过⼀些,⽐如mean、count、min以及sum等。你可能想知道在GroupBy对象上调⽤mean()时究竟发⽣了什么。许多常⻅的聚合运算(如表10-1所示)都有进⾏优化。然⽽,除了这些⽅法,你还可以使⽤其它的。
你可以使⽤⾃⼰发明的聚合运算,还可以调⽤分组对象上已经定义好的任何⽅法。例如,quantile可以计算Series或DataFrame列的样本分位数。
虽然quantile并没有明确地实现于GroupBy,但它是⼀个Series⽅法,所以这⾥是能⽤的。实际上,GroupBy会⾼效地对Series进⾏切⽚,然后对各⽚调⽤piece.quantile(0.9)
,最后将这些结果组装成最终结果:
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)
注意,有些⽅法(如describe)也是可以⽤在这⾥的,即使严格来讲,它们并⾮聚合运算:
grouped.describe() # 生成描述性统计,总结数据集分布的中心趋势,分散和形状,不包括NaN值。
注意:⾃定义聚合函数要⽐表10-1中那些经过优化的函数慢得多。这是因为在构造中间分组数据块时存在⾮常⼤的开销(函数调⽤、数据重排等)。
⾯向列的多函数应⽤
回到前⾯⼩费的例⼦。使⽤read_csv导⼊数据之后,我们添加了⼀个⼩费百分⽐的列tip_pct:
tips = pd.read_csv('./tips.csv')
# 加上小费占账单总额的百分比
tips['tip_pct'] = tips['tip'] / tips['total_bill']
tips[:6]
你已经看到,对Series或DataFrame列的聚合运算其实就是使⽤aggregate(使⽤⾃定义函数)或调⽤诸如mean、std之类的⽅法。然⽽,你可能希望对不同的列使⽤不同的聚合函数,或⼀次应⽤多个函数。其实这也好办,我将通过⼀些示例来进⾏讲解。
⾸先,我根据天和smoker对tips进⾏分组:
grouped = tips.groupby(['day', 'smoker'])
注意,对于表10-1中的那些描述统计,可以将函数名以字符串的形式传⼊:
grouped_pct = grouped['tip_pct']
grouped_pct.agg('mean')
如果传⼊⼀组函数或函数名,得到的DataFrame的列就会以相应的函数命名:
grouped_pct.agg(['mean', 'std', peak_to_peak]) # 直接调用函数不加引号
这⾥,我们传递了⼀组聚合函数进⾏聚合,独⽴对数据分组进⾏评估。
你并⾮⼀定要接受GroupBy⾃动给出的那些列名,特别是lambda函数,它们的名称是<lambda>
,这样的辨识度就很低了(通过函数的name属性看看就知道了)。因此,如果传⼊的是⼀个由(name,function)元组组成的列表,则各元组的第⼀个元素就会被⽤作DataFrame的列名(可以将这种⼆元元组列表看做⼀个有序映射):
grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
对于DataFrame,还有更多选择,可以定义⼀组应⽤于全部列的⼀组函数,或不同的列应⽤不同的函数。假设想要对tip_pct和total_bill列计算三个统计信息:
functions = ['count', 'mean', 'max']
result = grouped[['tip_pct', 'total_bill']].agg(functions)
result
如你所⻅,结果DataFrame拥有层次化的列,这相当于分别对各列进⾏聚合,然后⽤concat将结果组装到⼀起。
跟前⾯⼀样,这⾥也可以传⼊带有⾃定义名称的⼀组元组:
ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
grouped[['tip_pct', 'total_bill']].agg(ftuples)
想要对⼀个列或不同的列应⽤不同的函数。具体的办法是向agg传⼊⼀个从列名映射到函数的字典:
grouped.agg({'tip' : np.max, 'size' : 'sum'})
grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
'size' : 'sum'})
只有将多个函数应⽤到⾄少⼀列时,DataFrame才会拥有层次化的列。
以“没有⾏索引”的形式返回聚合数据
到⽬前为⽌,所有示例中的聚合数据都有由唯⼀的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向groupby传⼊as_index=False
以禁⽤该功能:
tips.groupby(['day', 'smoker'], as_index=False).mean()
当然,对结果调⽤reset_index也能得到这种形式的结果。使⽤as_index=False
⽅法可以避免⼀些不必要的计算。
apply:⼀般性的“拆分-应⽤-合并”
最通⽤的GroupBy⽅法是apply。如图所示,apply会将待处理的对象拆分成多个⽚段,然后对各⽚段调⽤传⼊的函数,最后尝试将各⽚段组合到⼀起。
回到之前那个⼩费数据集,假设你想要根据分组选出最⾼的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)
top函数在DataFrame的各个⽚段上调⽤,然后结果由pandas.concat组装到⼀起,并以分组名称进⾏了标记。于是,最终结果就有了⼀个层次化索引,其内层索引值来⾃原DataFrame。
如果传给apply的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后⾯⼀并传⼊:
tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')
之前在GroupBy对象上调⽤过describe:
result = tips.groupby('smoker')['tip_pct'].describe()
result
result.unstack('smoker')
在GroupBy中,当你调⽤诸如describe之类的⽅法时,实际上只是应⽤了下⾯两条代码的快捷⽅式⽽已:
f = lambda x: x.describe()
grouped.apply(f)
禁⽌分组键
从上⾯的例⼦中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False
传⼊groupby即可禁⽌该效果:
tips.groupby('smoker', group_keys=False).apply(top)
分位数和桶分析
在第7章(数据清洗和准备)中讲过,pandas有⼀些能根据指定⾯元或样本分位数将数据拆分成多块的⼯具(⽐如cut和qcut)。将这些函数跟groupby结合起来,就能⾮常轻松地实现对数据集的桶(bucket)或分位数(quantile)分析了。以下⾯这个简单的随机数据集为例,我们利⽤cut将其装⼊⻓度相等的桶中:
frame = pd.DataFrame({'data1': np.random.randn(1000),
'data2': np.random.randn(1000)})
quartiles = pd.cut(frame.data1, 4)
quartiles[:10]
由cut返回的Categorical对象可直接传递到groupby。因此,我们可以像下⾯这样对data2列做⼀些统计计算:
def get_stats(group):
return {'min': group.min(), 'max': group.max(),
'count': group.count(), 'mean': group.mean()}
grouped = frame.data2.groupby(quartiles) # 传入的是Categorical对象
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()
示例:⽤特定于分组的值填充缺失值
对于缺失数据的清理⼯作,有时你会⽤dropna将其替换掉,⽽有时则可能会希望⽤⼀个固定值或由数据集本身所衍⽣出来的值去填充NA值。这时就得使⽤fillna这个⼯具了。在下⾯这个例⼦中,我⽤平均值去填充NA值:
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
s
s.fillna(s.mean())
假设你需要对不同的分组填充不同的值。⼀种⽅法是将数据分组,并使⽤apply和⼀个能够对各数据块调⽤fillna的函数即可。下⾯是⼀些有关美国⼏个州的示例数据,这些州⼜被分为东部和⻄部:
states = ['Ohio', 'New York', 'Vermont', 'Florida',
'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4
data = pd.Series(np.random.randn(8), index=states)
data
['East'] * 4
产⽣了⼀个列表,包括了['East']
中元素的四个拷⻉,将这些列表串联起来。
将⼀些值设为缺失:
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)
也可以在代码中预定义各组的填充值。由于分组具有⼀个name属性,所以我们可以拿来⽤⼀下:
fill_values = {'East': 0.5, 'West': -1}
fill_func = lambda g: g.fillna(fill_values[g.name])
data.groupby(group_key).apply(fill_func)
示例:随机采样和排列
假设你想要从⼀个⼤数据集中随机抽取(进⾏替换或不替换)样本以进⾏蒙特卡罗模拟(Monte Carlo simulation)或其他分析⼯作。“抽取”的⽅式有很多,这⾥使⽤的⽅法是对Series使⽤sample⽅法:
# 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 ['H', 'S', 'C', 'D']:
cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val, index=cards)
现在我有了⼀个⻓度为52的Series,其索引包括牌名,值则是21点或其他游戏中⽤于计分的点数(为了简单起⻅,我当A的点数为1):
deck[:13]
现在,根据上⾯所讲的,从整副牌中抽出5张,代码如下:
def draw(deck, n=5):
return deck.sample(n) # 截取列表的指定长度的随机数,但是不会改变列表本身的排序
draw(deck)
假设你想要从每种花⾊中随机抽取两张牌。由于花⾊是牌名的最后⼀个字符,所以我们可以据此进⾏分组,并使⽤apply:
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)
示例:分组加权平均数和相关系数
根据groupby的“拆分-应⽤-合并”范式,可以进⾏DataFrame的列与列之间或两个Series之间的运算(⽐如分组加权平均)。以下⾯这个数据集为例,它含有分组键、值以及⼀些权重值:
df = pd.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)
另⼀个例⼦,考虑⼀个来⾃Yahoo!Finance的数据集,其中含有⼏只股票和标准普尔500指数(符号SPX)的收盘价:
close_px = pd.read_csv('./stock_px_2.csv', parse_dates=True,
index_col=0)
close_px.info() # 返回一个格式化的多行字符串,其中包含有关代码对象的详细信息
close_px[-4:]
来做⼀个⽐较有趣的任务:计算⼀个由⽇收益率(通过百分数变化计算)与SPX之间的年度相关系数组成的DataFrame。下⾯是⼀个实现办法,我们先创建⼀个函数,⽤它计算每列和SPX列的成对相关系数:
spx_corr = lambda x: x.corrwith(x['SPX'])
接下来,我们使⽤pct_change计算close_px的百分⽐变化:
rets = close_px.pct_change().dropna() # 当前元素与先前元素之间的百分比变化。默认情况下,计算与前一行的百分比变化。
最后,我们⽤年对百分⽐变化进⾏分组,可以⽤⼀个⼀⾏的函数,从每⾏的标签返回每个datetime标签的year属性:
get_year = lambda x: x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
当然,你还可以计算列与列之间的相关系数。这⾥,我们计算Apple和Microsoft的年相关系数:
by_year.apply(lambda g: g['AAPL'].corr(g['MSFT'])) # 计算列的成对关联,不包括NA /空值
示例:组级别的线性回归
顺着上⼀个例⼦继续,你可以⽤groupby执⾏更为复杂的分组统计分析,只要函数返回的是pandas对象或标量值即可。例如,我可以定义下⾯这个regress函数(利⽤statsmodels计量经济学库)对各数据块执⾏普通最⼩⼆乘法(Ordinary Least Squares,OLS)回归:
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
现在,为了按年计算AAPL对SPX收益率的线性回归,执⾏:
by_year.apply(regress, 'AAPL', ['SPX'])
透视表和交叉表
透视表(pivot table)是各种电⼦表格程序和其他数据分析软件中⼀种常⻅的数据汇总⼯具。它根据⼀个或多个键对数据进⾏聚合,并根据⾏和列上的分组键将数据分配到各个矩形区域中。在Python和pandas中,可以通过本章所介绍的groupby功能以及(能够利⽤层次化索引的)重塑运算制作透视表。DataFrame有⼀个pivot_table⽅法,此外还有⼀个顶级的pandas.pivot_table函数。除能为groupby提供便利之外,pivot_table还可以添加分项⼩计,也叫做margins。
回到⼩费数据集,假设我想要根据day和smoker计算分组平均数(pivot_table的默认聚合类型),并将day和smoker放到⾏上:
tips.pivot_table(index=['day', 'smoker'])
可以⽤groupby直接来做。现在,假设我们只想聚合tip_pct和size,⽽且想根据time进⾏分组。我将smoker放到列上,把day放到⾏上:
tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
columns='smoker')
还可以对这个表作进⼀步的处理,传⼊margins=True
添加分项⼩计。这将会添加标签为All的⾏和列,其值对应于单个等级中所有数据的分组统计:
tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
columns='smoker', margins=True)
这⾥,All值为平均数:不单独考虑烟⺠与⾮烟⺠(All列),不单
独考虑⾏分组两个级别中的任何单项(All⾏)。
要使⽤其他的聚合函数,将其传给aggfunc即可。例如,使⽤
count或len可以得到有关分组⼤⼩的交叉表(计数或频率):
tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day',
aggfunc=len, margins=True)
如果存在空的组合(也就是NA),你可能会希望设置⼀个fill_value:
tips.pivot_table('tip_pct', index=['time', 'size', 'smoker'],
columns='day', aggfunc='mean', fill_value=0)
表10-2 pivot_table的选项
交叉表:crosstab
交叉表(cross-tabulation,简称crosstab)是⼀种⽤于计算分组频率的特殊透视表。看下⾯的例⼦:
from io import StringIO
data = """\
Sample Nationality Handedness
1 USA Right-handed
2 Japan Left-handed
3 USA Right-handed
4 Japan Right-handed
5 Japan Left-handed
6 Japan Right-handed
7 USA Right-handed
8 USA Left-handed
9 Japan Right-handed
10 USA Right-handed"""
data = pd.read_table(StringIO(data), sep='\s+')
data
作为调查分析的⼀部分,我们可能想要根据国籍和⽤⼿习惯对这段数据进⾏统计汇总。虽然可以⽤pivot_table实现该功能,但是pandas.crosstab函数会更⽅便:
pd.crosstab(data.Nationality, data.Handedness, margins=True)
crosstab的前两个参数可以是数组或Series,或是数组列表。就像⼩费数据:
pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)