本文的Pandas知识点包括:
1、合并数据集
2、重塑和轴向旋转
3、数据转换
4、数据聚合
1、合并数据集
Pandas中合并数据集有多种方式,这里我们来逐一介绍
1.1 数据库风格合并
数据库风格的合并指根据索引或某一列的值是否相等进行合并的方式,在pandas中,这种合并使用merge以及join函数实现。
先来看下面的例子:
df1 = pd.DataFrame({'key':['b','b','a','c','a','a','b'],'data1':range(7)})
df2 = pd.DataFrame({'key':['a','b','d'],'data2':range(3)})
pd.merge(df1,df2)
如果merge函数只指定了两个DataFrame,它会自动搜索两个DataFrame中相同的列索引,即key,当然,这可以进行指定,下面的语句和上面是等价的:
pd.merge(df1,df2,on='key')
当两个DataFrame没有相同的列索引时,我们可以指定链接的列:
#如果两个DataFrame的列名不同,可以分别指定
df3 = pd.DataFrame({'lkey':['b','b','a','c','a','a','b'],'data1':range(7)})
df4 = pd.DataFrame({'rkey':['a','b','d'],'data2':range(3)})
pd.merge(df3,df4,left_on='lkey',right_on='rkey')
同时我们可以看到,merge默认情况下采用的是内链接方式,当然我们可以通过how参数指定链接方式:
pd.merge(df1,df2,how='outer')
要根据多个键进行合并,传入一组由列名组成的列表即可:
left = pd.DataFrame({'key1':['foo','foo','bar'],'key2':['one','two','one'],'lval':[1,2,3]})
right = pd.DataFrame({'key1':['foo','foo','bar','bar'],'key2':['one','one','one','two'],'rval':[4,5,6,7]})
pd.merge(left,right,on=['key1','key2'],how='outer')
上面两个表有两列重复的列,如果只根据一列进行合并,则会多出一列重复列,重复列名的处理我们一般使用merge的suffixes属性,可以帮我们指定重复列合并后的列名:
pd.merge(left,right,on='key1',suffixes=('_left','_right'))
上面的on、left_on、right_on都是根据列值进行合并的,如果我们想用索引进行合并,使用left_index 或者 right_index属性:
left1 = pd.DataFrame({'key':['a','b','a','a','b','c'],'value':range(6)})
right1 = pd.DataFrame({'group_val':[3.5,7]},index=['a','b'])
pd.merge(left1,right1,left_on='key',right_index=True)
对于层次化索引的数据,我们必须以列表的形式指明用作合并键的多个列:
lefth = pd.DataFrame({'key1':['Ohio','Ohio','Ohio','Nevada','Nevada'],
'key2':[2000,2001,2002,2001,2002],
'data':np.arange(5.0)})
righth = pd.DataFrame(np.arange(12).reshape((6,2)),
index=[['Nevada','Nevada','Ohio','Ohio','Ohio','Ohio'],[2001,2000,2000,2000,2001,2002]],
columns=['event1','event2'])
pd.merge(lefth,righth,left_on=['key1','key2'],right_index=True)
如果单纯想根据索引进行合并,使用join方法会更加简单:
left2 = pd.DataFrame([[1.0,2.0],[3.0,4.0],[5.0,6.0]],index = ['a','c','e'],columns=['Ohio','Nevada'])
right2 = pd.DataFrame([[7.0,8.0],[9.0,10.0],[11.0,12.0],[13.0,14.0]],index = ['b','c','d','e'],columns=['Missouri','Alabama'])
left2.join(right2,how='outer')
1.2 轴向链接
pandas的轴向链接指的是根据某一个轴向来拼接数据,类似于列表的合并。concat函数,默认在轴0上工作,我们先来看一个Series的例子:
s1 = pd.Series([0,1],index=['a','b'])
s2 = pd.Series([2,3,4],index=['c','d','e'])
s3 = pd.Series([5,6],index=['f','g'])
pd.concat([s1,s2,s3])
#输出
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: int64
pd.concat([s1,s2,s3],axis=1)
#输出
0 1 2
a 0.0 NaN NaN
b 1.0 NaN NaN
c NaN 2.0 NaN
d NaN 3.0 NaN
e NaN 4.0 NaN
f NaN NaN 5.0
g NaN NaN 6.0
在上面的情况下,参与连接的片段在结果中区分不开,假设你想要在连接轴上创建一个层次化索引,我们可以额使用keys参数:
result = pd.concat([s1,s1,s3],keys=['one','two','three'])
result
#输出
one a 0
b 1
two a 0
b 1
three f 5
g 6
dtype: int64
如果是沿着axis=1进行轴向合并,keys会变为列索引:
pd.concat([s1,s1,s3],keys=['one','two','three'],axis=1)
上面的逻辑同样适用于DataFrame的轴向合并:
df1 = pd.DataFrame(np.arange(6).reshape((3,2)),index=['a','b','c'],columns=['one','two'])
df2 = pd.DataFrame(5 + np.arange(4).reshape((2,2)),index=['a','c'],columns=['three','four'])
pd.concat([df1,df2],axis=1,keys=['level1','level2'])
#下面的操作会得到与上面同样的效果
pd.concat({"level1":df1,'level2':df2},axis=1)
使用ignore_index参数可以不保留轴上的索引,产生一组新的索引:
df1 = pd.DataFrame(np.arange(6).reshape((3,2)),index=[1,2,3],columns=['one','two'])
df2 = pd.DataFrame(5 + np.arange(4).reshape((2,2)),index=[1,2],columns=['three','four'])
pd.concat([df1,df2],ignore_index = True)
2、重塑和轴向旋转
在重塑和轴向旋转中,有两个重要的函数,二者互为逆操作:
stack:将数据的列旋转为行
unstack:将数据的行旋转为列
先来看下面的例子:
data = pd.DataFrame(np.arange(6).reshape((2,3)),index=pd.Index(['Ohio','Colorado'],name='state'),
columns=pd.Index(['one','two','three'],name='number'))
result = data.stack()
result
我们使用unstack()将数据的列旋转为行,默认是最里层的行索引:
result.unstack()
默认unstack是将最里层的行索引旋转为列索引,不过我们可以指定unstack的层级,unstack之后作为旋转轴的级别将会成为结果中的最低级别,当然,我们也可以根据名字指定要旋转的索引,下面两句代码是等价的:
result.unstack(0)
result.unstack('state')
如果不是所有的级别都能在分组中找到的话,unstack操作可能会产生缺失数据:
s1 = pd.Series([0,1,2,3],index=['a','b','c','d'])
s2 = pd.Series([4,5,6],index=['c','d','e'])
data2 = pd.concat([s1,s2],keys=['one','two'])
data2.unstack()
stack操作默认会过滤掉缺失值,不过可以使用dropna参数选择不过滤缺失值:
data2.unstack().stack()
#输出
one a 0.0
b 1.0
c 2.0
d 3.0
two c 4.0
d 5.0
e 6.0
dtype: float64
data2.unstack().stack(dropna=False)
#输出
one a 0.0
b 1.0
c 2.0
d 3.0
e NaN
two a NaN
b NaN
c 4.0
d 5.0
e 6.0
dtype: float64
3、数据转换
3.1 移除重复数据
移除重复数据,使用drop_duplicates方法,该方法默认判断全部列,不过我们也可以根据指定列进行去重.
data = pd.DataFrame({'k1':['one']*3 + ['two'] * 4,'k2':[1,1,2,3,3,4,4]})
data.drop_duplicates()
#输出
<bound method DataFrame.drop_duplicates of k1 k2
0 one 1
1 one 1
2 one 2
3 two 3
4 two 3
5 two 4
6 two 4>
data.drop_duplicates(['k2'])
#输出
k1 k2
0 one 1
2 one 2
3 two 3
5 two 4
默认对于重复数据,系统会保留第一项,即keep参数的默认值为first,不过我们也可以保留最后一项,只需将keep参数设置为last即可:
data.drop_duplicates(['k2'],keep='last')
#输出
k1 k2
1 one 1
2 one 2
4 two 3
6 two 4
3.2 map函数
在对数据集进行转换时,你可能希望根据数组、Series或者DataFrame列中的值来实现该转换工作,我们来看看下面的肉类数据的处理:
data = pd.DataFrame({'food':['bacon','pulled pork','bacon',
'Pastrami','corned beef','Bacon','pastrami','honey ham','nova lox'],
'ounces':[4,3,12,6,7.5,8,3,5,6]})
meat_to_animal = {
'bacon':'pig',
'pulled pork':'pig',
'pastrami':'cow',
'corned beef':'cow',
'honey ham':'pig',
'nova lox':'salmon'
}
#Series的map方法接受一个函数或含有映射关系的字典对象,对元素进行相应的转换
data['animal']=data['food'].map(str.lower).map(meat_to_animal)
data
使用下面的代码是等价的:
data['animal'] = data['food'].map(lambda x: meat_to_animal[x.lower()])
3.3 replace值替换
使用replace方法进行值替换,返回一个新的对象。如果希望对不同的值进行不同的替换,传入一个由替换关系组成的列表或者字典即可:
data = pd.Series([1,-999,2,-999,-1000,3])
data.replace(-999,np.nan)
#输出
0 1
1 -999
2 2
3 -999
4 -1000
5 3
dtype: int64
data.replace([-999,-1000],[np.nan,0])
#输出
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
3.4 离散化和面元划分
根据区间对数据进行划分,使用cut函数,比如我们想根据年龄区间对人群进行划分,从而得到不同年龄段的人数统计:
ages = [20,22,25,27,21,23,37,31,61,45,41,32]
bins = [18,25,35,60,100]
cats = pd.cut(ages,bins)
cats
#输出
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
cut函数返回一个特殊的Categorical对象,可以通过codes来查看个数据的组别编号:
cats.codes
#输出
array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
使用value_counts可以实现分组计数:
pd.value_counts(cats)
#输出
(18, 25] 5
(35, 60] 3
(25, 35] 3
(60, 100] 1
dtype: int64
上面是前开后闭区间,如果想要变为前闭后开区间,只需要设置right=False参数:
cats = pd.cut(ages,bins,right=False)
pd.value_counts(cats)
#输出
[25, 35) 4
[18, 25) 4
[35, 60) 3
[60, 100) 1
dtype: int64
也可以设置自己的面元名称,将labels选项设为一个列表或者数组即可:
group_names = ['Youth','YoungAdult','MiddleAged','Senior']
pd.cut(ages,bins,labels=group_names)
#输出
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult]
Length: 12
Categories (4, object): [MiddleAged < Senior < YoungAdult < Youth]
如果向cut传入的不是面元边界而是面元的数量,则会根据数据的最大值和最小值自动计算等长面元,比如下面的例子将均匀分布的数据分为四组:
data = np.random.rand(20)
pd.cut(data,4,precision=2)
pandas还提供了一个对数据进行划分的函数:qcut。qcut基于样本分位数对数据进行面元划分,可以自定义分位数,也可以传入一个数量(会自动计算分位数):
data = np.random.randn(1000)
cats = pd.qcut(data,4)
pd.value_counts(cats)
#输出
(0.701, 3.451] 250
(0.0727, 0.701] 250
(-0.57, 0.0727] 250
(-3.84, -0.57] 250
dtype: int64
pd.value_counts(pd.qcut(data,[0,0.1,0.5,0.9,1]))
#输出
(0.0727, 1.338] 400
(-1.247, 0.0727] 400
(1.338, 3.451] 100
(-3.84, -1.247] 100
dtype: int64
3.5 排列和随机采样
利用numpy.random.permutation函数可以轻松实现对Series或者DataFrame的列的排列工作,通过需要排列的轴的长度调用permutation,可产生一个表示新顺序的整数数组,最后使用pandas的take函数返回指定大小的数据即可实现采样。
df = pd.DataFrame(np.arange(5*4).reshape(5,4))
sampler = np.random.permutation(5)
df.take(sampler[:3])
4、数据聚合
4.1 数据分组
pandas中的数据分组使用groupby方法,返回的是一个GroupBy对象,对分组之后的数据,我们可以使用一些聚合函数进行聚合,比如求平均值mean:
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)
})
groupd = df['data1'].groupby(df['key1'])
groupd
#<pandas.core.groupby.SeriesGroupBy object at 0x118814dd8>
groupd.mean()
#输出
key1
a 0.697500
b -0.068161
Name: data1, dtype: float64
上面是进行分组的一个简单的例子,我们可以根据多种数据格式进行数据分组,下面就一一整理一下:
Series
means = df['data1'].groupby([df['key1'],df['key2']]).mean()
means
#输出
key1 key2
a one 0.543936
two 1.004630
b one 0.219453
two -0.355776
Name: data1, dtype: float64
合适长度的数组
分组键可以是任何适当长度的数组,数组中每一个元素的值代表相应下标的记录的分组键:
states = np.array(['Ohio','Nevada','Nevada','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').mean()的结果中,结果并没有key2这一列,这是因为key2这一列不是数值数据,所以从结果中排除了,默认情况下,所有的数值列都会被聚合,虽然有时可能会被过滤为一个子集。
map
#我们还可以根据map来进行分组
people = pd.DataFrame(np.random.randn(5,5),columns=['a','b','c','d','e'],index=['Joe','Steve','Wes','Jim','Travis'])
mapping = {'a':'red','b':'red','c':'blue','d':'blue','e':'red','f':'orange'}
people.groupby(mapping,axis=1).sum()
Python函数
假如你想根据人名的长度进行分组,虽然可以求取一个字符串长度数组,其实仅仅传入len函数就可以了:
people.groupby(len).sum()
索引级别
columns = pd.MultiIndex.from_arrays([['US','US','US','JP','JP'],[1,3,5,1,3]],names=['city','tenor'])
hier_df = pd.DataFrame(np.random.randn(4,5),columns=columns)
hier_df.groupby(level='city',axis=1).count()
分组之后产生一个GroupBy对象,这个对象支持迭代,是一个由(分组名,数据块)组成的二元组:
for name,group in df.groupby('key1'):
print(name)
print(group)
groupby默认是在axis=0上分组的,不过我们也可以在axis=1上分组,比如根据列的数据类型进行分组:
for name,group in df.groupby(df.dtypes,axis=1):
print(name)
print(group)
4.2 数据聚合操作
特定聚合函数
我们可以像之前一样使用一些特定的聚合函数,比如sum,mean等等,但是同时也可以使用自定义的聚合函数,只需将其传入agg方法中即可:
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)
})
def peak_to_peak(arr):
return arr.max()-arr.min()
grouped = df.groupby('key1')
grouped.agg(peak_to_peak)
关于agg还有更多的功能,我们使用小费数据(下载地址:http://pan.baidu.com/s/1bpGW3Av 密码:2p9v),我们读入数据,并计算小费率一列:
tips = pd.read_csv('data/ch08/tips.csv')
tips['tip_pct'] = tips['tip'] / tips['total_bill']
tips.head(10)
可以同时使用多个聚合函数,此时得到的DataFrame的列就会以相应的函数命名:
grouped = tips.groupby(['sex','smoker'])
grouped_pct = grouped['tip_pct']
grouped_pct.agg(['mean','std',peak_to_peak])
你如果不想接受这些自动给出的列名,你可以用(name,function)的方法指定你的列名:
grouped_pct.agg([('foo','mean'),('bar',np.std)])
假如你想要对不同的列应用不同的函数,具体的办法是向agg传入一个从列名映射到函数的字典:
grouped.agg({'tip':[np.max,'min'],'size':'sum'})
如果你想以无索引的方式返回聚合数据,可是设置as_index=False:
tips.groupby(['sex','smoker'],as_index=False).mean()
transform函数
transform会将一个函数运用到各个分组,然后将结果放置到适当的位置上。如果个分组产生的是一个标量值,则该值将会被广播出去,如果分组产生的是一个相同大小的数组,则会根据下标放置到适当的位置上。
people = pd.DataFrame(np.random.randn(5,5),columns=['a','b','c','d','e'],index=['Joe','Steve','Wes','Jim','Travis'])
key = ['one','two','one','two','one']
people.groupby(key).transform(np.mean)
可以看到,在上面的例子中,分组产生了一个标量,即分组的平均值,然后transform将这个值映射到对应的位置上,现在DataFrame中每个位置上的数据都是对应组别的平均值。假设我们希望从各组中减去平均值,可以用下面的方法实现:
def demean(arr):
return arr - arr.mean()
demeaned = people.groupby(key).transform(demean)
demeaned
apply函数
同agg一样,transform也是有严格条件的函数,传入的函数只能产生两种结果:要么产生一个可以广播的标量值,如np.mean,要么产生一个相同大小的结果数组.最一般化的GroupBy方法是apply,apply将会待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试将各片段组合到一起.
def top(df,n=5,column='tip_pct'):
return df.sort_values(by=column)[-n:]
tips.groupby('smoker').apply(top)
如果传入apply的方法里有可变参数的话,我们可以自定义这些参数的值:
tips.groupby(['smoker','day']).apply(top,n=1,column='total_bill')
从上面的例子可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果:
tips.groupby(['smoker'],group_keys=False).apply(top)
4.3 数据透视表
透视表是各种电子表格程序和其他数据分析软件中一种常见的数据汇总工具,它根据一个或多个键对数据进行聚合,并根据行和列伤的分组键将数据分配到各个矩形区域中。
考虑我们的小费数据集,我们想聚合tip_pct和size,想根据day进行分组,将smoker放到列上,将day放到行上:
tips.pivot_table(['tip_pct','size'],index=['sex','day'],columns='smoker')
如果想增加汇总统计列,可以增加margins=True参数:
tips.pivot_table(['tip_pct','size'],index=['sex','day'],columns='smoker',margins=True)
如果想使用其他聚合函数,将其传入aggfunc即可,例如使用count或len可以得到有关分组大小的交叉表:
tips.pivot_table('tip_pct',index=['sex','smoker'],columns='day',aggfunc=len,margins=True)
可以使用fill_value填充缺失值:
tips.pivot_table('size',index=['time','sex','smoker'],columns='day',aggfunc=sum,fill_value=0)