数据规整化:清理、转换、合并、重塑
合并数据集
pandas对象中的数据可以通过一些内置的方式进行合并:
- pandas.merge可根据一个或多个键将不同DataFrame中的行连接起来。SQL或其他关系型数据库的用户对此应该会比较熟悉,因为它实现的就是数据库的连接操作。
- pandas.concat可以沿着一条轴将多个对象堆叠到一起。
- 实例方法combine_first可以将重复数据编接在一起,用一个对象中的值填充另一个对象中的缺失值。
数据库风格的DataFrame合并
数据集的合并(merge)或连接(join)运算是通过一个或多个键将行连接起来的。这些运算是关系型数据库的核心。pandas的merge函数是对数据应用这些算法的主要切入点。
以一个简单的例子开始:
In [176]: df1 = pd.DataFrame({'key':['b','b','a','c','a','a','b'],
...: 'data1':range(7)})
In [177]: df2 = pd.DataFrame({'key':['a','b','d'],
...: 'data2':range(3)})
In [178]: df1
Out[178]:
data1 key
0 0 b
1 1 b
2 2 a
3 3 c
4 4 a
5 5 a
6 6 b
In [179]: df2
Out[179]:
data2 key
0 0 a
1 1 b
2 2 d
In [180]: pd.merge(df1,df2)
Out[180]:
data1 key data2
0 0 b 1
1 1 b 1
2 6 b 1
3 2 a 0
4 4 a 0
5 5 a 0
注意,我们并没有指明要用哪个列进行连接。如果没有指明,merge就会将重叠列的列名当作键。不过,最好显式指定一下:
In [181]: pd.merge(df1,df2,on='key')
Out[181]:
data1 key data2
0 0 b 1
1 1 b 1
2 6 b 1
3 2 a 0
4 4 a 0
5 5 a 0
如果两个对象的列名不同,也可以分别进行指定:
In [182]: df3 = pd.DataFrame({'lkey':['b','b','a','c','a','a','b'],
...: 'data1':range(7)})
In [183]: df4 = pd.DataFrame({'rkey':['a','b','d'],
...: 'data2':range(3)})
In [184]: pd.merge(df3,df4,left_on='lkey',right_on='rkey')
Out[184]:
data1 lkey data2 rkey
0 0 b 1 b
1 1 b 1 b
2 6 b 1 b
3 2 a 0 a
4 4 a 0 a
5 5 a 0 a
可能你已经注意到,结果里面c和d以及与之相关的数据消失了。默认情况下,merge做的是“inner”连接;结果中的键是交集。其他方式还有“merge”、“right”以及“outer”。外连接求取的是键的并集,组合了左连接和右连接的效果:
In [185]: pd.merge(df1,df2,how='outer')
Out[185]:
data1 key data2
0 0.0 b 1.0
1 1.0 b 1.0
2 6.0 b 1.0
3 2.0 a 0.0
4 4.0 a 0.0
5 5.0 a 0.0
6 3.0 c NaN
7 NaN d 2.0
要根据多个键进行合并,传入一个由列名组成的列表即可:
In [186]: left = pd.DataFrame({'key1':['foo','foo','bar'],
...: 'key2':['one','two','one'],
...: 'lval':[1,2,3]})
In [187]: right = pd.DataFrame({'key1':['foo','foo','bar','bar'],
...: 'key2':['one','one','one','two'],
...: 'rval':[4,5,6,7]})
In [188]: pd.merge(left,right,on=['key1','key2'],how='outer')
Out[188]:
key1 key2 lval rval
0 foo one 1.0 4.0
1 foo one 1.0 5.0
2 foo two 2.0 NaN
3 bar one 3.0 6.0
4 bar two NaN 7.0
结果中会出现哪些键组合取决于所选的合并方式,你可以这样理解:多个键形成一系列元组,并将其当作单个连接键(当然,实际上不是这么回事)。
对于合并运算需要考虑的最后一个问题是对重复列名的处理,merge有一个实用的suffixes选项,用于指定附加到左右两个DataFrame对象的重叠列名上的字符串:
In [191]: pd.merge(left,right,on='key1')
Out[191]:
key1 key2_x lval key2_y rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
In [192]: pd.merge(left,right,on='key1',suffixes=('_left','_right'))
Out[192]:
key1 key2_left lval key2_right rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
merge函数的参数
参数 | 说明 |
---|---|
left | 参与合并的左侧DataFrame |
right | 参与合并的右侧DataFrame |
how | inner、outer、left、right其中之一。默认为inner |
on | 用于连接的列名。必须存在于左右两个DataFrame中。如果未指定,且其他连接键也未指定,则以left和right列名的交集作为连接键 |
left_on | 左侧DataFrame中用作连接键的列 |
right_on | 右侧DataFrame中用作连接键的列 |
left_index | 将左侧的行索引用作器连接键 |
right_index | 类似于left_inex
|
sort | 根据连接键对合并后的数据进行排序,默认为True |
suffixes | 字符串值元组,用于追加到重叠列名的末尾,默认为(_x,_y) |
copy | 设置为False,可以在某些特殊情况下避免将数据复制到结果数据结构中。默认总是复制。 |
索引上的合并
DataFrame的连接键也可能位于其索引中。在这种情况下,你可以传入left_index=True或right_index=True
(或两个都传)以说明索引应该被用作连接键:
In [7]: left1 = pd.DataFrame({'key':['a','b','a','a','b','c'],
...: 'value':range(6)})
In [8]: right1 = pd.DataFrame({'group_val':[3.5,7]},index=['a','b'])
In [9]: left1
Out[9]:
key value
0 a 0
1 b 1
2 a 2
3 a 3
4 b 4
5 c 5
In [10]: right1
Out[10]:
group_val
a 3.5
b 7.0
In [11]: pd.merge(left1,right1,left_on='key',right_index=True)
Out[11]:
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0
由于merge方法默认的是求连接键的交集,也可以通过外连接的方式得到它们的并集:
In [12]: pd.merge(left1,right1,left_on='key',right_index=True,how="outer")
Out[12]:
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0
5 c 5 NaN
对于层次化索引的数据,就更复杂点:
In [13]: lefth = pd.DataFrame({'key1':['Ohio','Ohio','Ohio','Nevada','Nevada'],
...: 'key2':[2000,2001,2002,2001,2002],
...: 'data':np.arange(5)})
In [14]: righth = pd.DataFrame(np.arange(12).reshape(6,2),index=[['Neveda','Nevada','Ohio','Ohio','Ohio','Ohio'],[2001,2000,2000,2000,2001,2002]],columns=['event1','event2'])
In [15]: lefth
Out[15]:
data key1 key2
0 0 Ohio 2000
1 1 Ohio 2001
2 2 Ohio 2002
3 3 Nevada 2001
4 4 Nevada 2002
In [16]: righth
Out[16]:
event1 event2
Neveda 2001 0 1
Nevada 2000 2 3
Ohio 2000 4 5
2000 6 7
2001 8 9
2002 10 11
这种情况下,必须以列表的形式指明用作合并键的多个列(注意对重复索引值的处理):
In [17]: pd.merge(lefth,righth,left_on=['key1','key2'],right_index=True)
Out[17]:
data key1 key2 event1 event2
0 0 Ohio 2000 4 5
0 0 Ohio 2000 6 7
1 1 Ohio 2001 8 9
2 2 Ohio 2002 10 11
In [18]: pd.merge(lefth,righth,left_on=['key1','key2'],right_index=True,how="outer")
Out[18]:
data key1 key2 event1 event2
0 0.0 Ohio 2000 4.0 5.0
0 0.0 Ohio 2000 6.0 7.0
1 1.0 Ohio 2001 8.0 9.0
2 2.0 Ohio 2002 10.0 11.0
3 3.0 Nevada 2001 NaN NaN
4 4.0 Nevada 2002 NaN NaN
4 NaN Neveda 2001 0.0 1.0
4 NaN Nevada 2000 2.0 3.0
同时使用合并双方的索引也没问题:
In [19]: left2 = pd.DataFrame([[1,2],[3,4],[5,6]],index=['a','c','e'],
...: columns=['Ohio','Nevada'])
In [20]: right2 = pd.DataFrame([[7,8],[9,10],[11,12],[13,14]],index=['b','c','d','e'],columns=['Missouri','Alabama'])
In [21]: left2
Out[21]:
Ohio Nevada
a 1 2
c 3 4
e 5 6
In [22]: right2
Out[22]:
Missouri Alabama
b 7 8
c 9 10
d 11 12
e 13 14
In [23]: pd.merge(left2,right2,left_index=True,right_index=True,how="outer")
Out[23]:
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0
DataFrame还有一个join实例方法,它能更为方便地实现按索引合并。它还可用于合并多个带有相同或相似索引地DataFrame对象,而不管它们之间地没有重叠的列。在上面的例子,我们可以编写:
In [24]: left2.join(right2,how="outer")
Out[24]:
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0
DataFrame的join方法是在连接键上做左连接。它还支持参数DataFrame的索引跟调用者DataFrame的某个列之间的连接:
In [26]: left1.join(right1,on="key")
Out[26]:
key value group_val
0 a 0 3.5
1 b 1 7.0
2 a 2 3.5
3 a 3 3.5
4 b 4 7.0
5 c 5 NaN
最后,对于简单的索引合并,你还可以向join传入一组DataFrame(后面我们会介绍更为通用的concat函数,它也能实现此功能):
In [27]: another = pd.DataFrame([[7,8],[9,10],[11,12],[16,17]],
...: index=['a','c','e','f'],columns=['New York','Oregon'])
In [28]: left2.join([right2,another])
Out[28]:
Ohio Nevada Missouri Alabama New York Oregon
a 1 2 NaN NaN 7 8
c 3 4 9.0 10.0 9 10
e 5 6 13.0 14.0 11 12
In [29]: left2.join([right2,another],how='outer')
Out[29]:
Ohio Nevada Missouri Alabama New York Oregon
a 1.0 2.0 NaN NaN 7.0 8.0
b NaN NaN 7.0 8.0 NaN NaN
c 3.0 4.0 9.0 10.0 9.0 10.0
d NaN NaN 11.0 12.0 NaN NaN
e 5.0 6.0 13.0 14.0 11.0 12.0
f NaN NaN NaN NaN 16.0 17.0
轴向连接
另一种数据合并运算也被称作连接(concatenation)、绑定(binding)或堆叠(stacking)。NumPy有一个用于合并原始NumPy数组的concatenation函数:
In [33]: arr = np.arange(12).reshape(3,4)
In [34]: arr
Out[34]:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
In [35]: np.concatenate([arr,arr],axis=1)
Out[35]:
array([[ 0, 1, 2, 3, 0, 1, 2, 3],
[ 4, 5, 6, 7, 4, 5, 6, 7],
[ 8, 9, 10, 11, 8, 9, 10, 11]])
对于pandas对象(如Series和DataFrame),带有标签的轴使你能够进一步推广数组的连接运算。具体点说,你还需要考虑以下这些东西:
- 如果各对象其他轴上的索引不同,那些轴应该是做并集还是交集?
- 结果对象中的分组需要各不相同吗?
- 用于连接的轴重要吗?
pandas的concat函数提供了一种能够解决这些问题的可靠方式。假设有三个没有重叠索引的Series:
In [36]: s1 = pd.Series([0,1],index=['a','b'])
In [37]: s2 = pd.Series([2,3,4],index=['c','d','e'])
In [38]: s3 = pd.Series([5,6],index=['f','g'])
In [39]: pd.concat([s1,s2,s3])
Out[39]:
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: int64
默认情况下,concat是在axis=0上工作的,最终产生一个新的Series。如果传入axis=1,则结果就会变成DataFrame(axis=1是列):
In [40]: pd.concat([s1,s2,s3],axis=1)
Out[40]:
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
这种情况下,另外一条轴上没有重叠,从索引的有序并集(外连接)上就可以看出来。传入join='inner'即可得到它们的交集:
In [47]: s4 = pd.concat([s1*5,s3])
In [48]: pd.concat([s1,s4],axis=1)
Out[48]:
0 1
a 0.0 0
b 1.0 5
f NaN 5
g NaN 6
In [50]: pd.concat([s1,s4],axis=1,join='inner')
Out[50]:
0 1
a 0 0
b 1 5
可以通过join_axes指定要在其他轴上使用的索引:
In [51]: pd.concat([s1,s4],axis=1,join_axes=[['a','c','b','e']])
Out[51]:
0 1
a 0.0 0.0
c NaN NaN
b 1.0 5.0
e NaN NaN
不过有个问题,参与连接的片段在结果中区分不开。假设你想要在连接轴上创建一个层次化索引。使用keys参数即可达到这个目的:
In [52]: result = pd.concat([s1,s1,s3],keys=['one','two','three'])
In [53]: result
Out[53]:
one a 0
b 1
two a 0
b 1
three f 5
g 6
dtype: int64
result.unstack()
Out[54]:
a b f g
one 0.0 1.0 NaN NaN
two 0.0 1.0 NaN NaN
three NaN NaN 5.0 6.0
如果沿着axis=1对Series进行合并,则keys就会成为DataFrame的列头:
In [60]: pd.concat([s1,s2,s3],axis=1,keys=['one','two','three'])
Out[60]:
one two three
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
同样的逻辑对DataFrame对象也是一样:
In [61]: df1 = pd.DataFrame(np.arange(6).reshape(3,2),index=['a','b','c'],
...: columns=['one','two'])
In [62]: df2 = pd.DataFrame(5+np.arange(4).reshape(2,2),index=['a','c'],columns=['three','four'])
In [63]: pd.concat([df1,df2],axis=1,keys=['level1','level2'])
Out[63]:
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0
最后一个需要考虑的问题是,和当前分析工作无关的DataFrame行索引:
In [69]: df1 = pd.DataFrame(np.random.randn(3,4),columns=['a','b','c','d'])
In [70]: df2 = pd.DataFrame(np.random.randn(2,3),columns=['b','d','a'])
In [71]: df1
Out[71]:
a b c d
0 2.240595 -0.978214 -2.582510 1.166495
1 -1.069589 1.448100 -0.511939 -0.047077
2 0.744933 1.829291 0.782055 -1.012039
In [72]: df2
Out[72]:
b d a
0 -1.259407 -1.117981 1.036470
1 2.338679 -0.140818 -0.167361
In [73]: pd.concat([df1,df2],ignore_index=True)
Out[73]:
a b c d
0 2.240595 -0.978214 -2.582510 1.166495
1 -1.069589 1.448100 -0.511939 -0.047077
2 0.744933 1.829291 0.782055 -1.012039
3 1.036470 -1.259407 NaN -1.117981
4 -0.167361 2.338679 NaN -0.140818
In [74]: pd.concat([df1,df2])
Out[74]:
a b c d
0 2.240595 -0.978214 -2.582510 1.166495
1 -1.069589 1.448100 -0.511939 -0.047077
2 0.744933 1.829291 0.782055 -1.012039
0 1.036470 -1.259407 NaN -1.117981
1 -0.167361 2.338679 NaN -0.140818
concat函数的参数
参数 | 说明 |
---|---|
objs | 参与连接的pandas对象的列表或字典。唯一必需的参数 |
axis | 指明连接的轴向,默认为0 |
join | inner、outer其中之一,默认为outer |
join_axes | 指明用于其他n-1条轴的索引,不执行并集/交集运算 |
keys | 与连接对象有关的值,用于形成连接轴向上的层次化索引。可以是任意值的列表或数组、元组数组、数组列表 |
levels | 指定用作层次化索引各级别上的索引,如果设置了keys的话 |
names | 用于创建分层级别的名称,如果设置了keys和(或)levels的话 |
verify_integrity | 检查结果对象新轴上的重复情况,如果发现则引发异常。默认False允许重复 |
ignore_index | 不保留连接轴上的索引,产生一组新索引range(total_length) |
合并重叠数据
还有一种数据组合问题不能用简单的合并(merge)或连接(concatenation)运算来处理。比如说,你可能有索引全部或部分重叠的两个数据集。给这个例子增加一点启发性,我们使用NumPy的where函数,它用于表达一种矢量化的if-else:
In [75]: a = pd.Series([np.nan,2.5,np.nan,3.5,4.5,np.nan],
...: index=['f','e','d','c','b','a'])
In [76]: b = pd.Series(np.arange(len(a),dtype=np.float64),index=['f','e','d','c','b','a'])
In [77]: b[-1] = np.nan
In [78]: a
Out[78]:
f NaN
e 2.5
d NaN
c 3.5
b 4.5
a NaN
dtype: float64
In [79]: b
Out[79]:
f 0.0
e 1.0
d 2.0
c 3.0
b 4.0
a NaN
dtype: float64
In [80]: np.where(pd.isnull(a),b,a)
Out[80]: array([ 0. , 2.5, 2. , 3.5, 4.5, nan])
Series有一个combine_first方法,实现的也是一样的功能,而且会进行数据对齐:
In [83]: b[:-2].combine_first(a[:2])
Out[83]:
c 3.0
d 2.0
e 1.0
f 0.0
dtype: float64
对于DataFrame,combine_first自然也会在列上做同样的事情,因此可以将其看作:用参数对象中的数据为调用者对象的缺失数据“打补丁”:
In [86]: df1 = pd.DataFrame({'a':[1,np.nan,5,np.nan],'b':[np.nan,2,np.nan,6],
...: 'c':range(2,18,4)})
In [87]: df2 = pd.DataFrame({'a':[5,4,np.nan,3,7],
...: 'b':[np.nan,3,4,6,8]})
In [88]: df1.combine_first(df2)
Out[88]:
a b c
0 1.0 NaN 2.0
1 4.0 2.0 6.0
2 5.0 4.0 10.0
3 3.0 6.0 14.0
4 7.0 8.0 NaN
重塑和轴向转换
有许多用于重新排列表格型数据的基础运算。这些函数也称作重塑(reshape)或轴向旋转(pivot)运算。
重塑层次化索引
层次化索引为DataFrame数据的重排任务提供了一种具有良好一致性的方式。主要功能有二:
- stack:将数据的列“旋转”为行
- unstack:将数据的行“旋转”为列
将通过一系列的范例来讲解这些操作。
In [92]: data = pd.DataFrame(np.arange(6).reshape(2,3),index=pd.Index(['Ohio','Colorado'],name='state'),columns=pd.Index(['one','two','three'],name='number'))
In [93]: data
Out[93]:
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
使用该数据的stack方法即可将列转换为行,得到一个Series:
In [94]: result = data.stack()
In [95]: result
Out[95]:
state number
Ohio one 0
two 1
three 2
Colorado one 3
two 4
three 5
dtype: int32
对于一个层次化索引的Series,你可以用unstack将其重排为一个DataFrame:
In [96]: result.unstack()
Out[96]:
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
默认情况下,unstack操作的是最内层(stack也是如此)。传入分层级别的编号或名称即可对其他级别进行unstack操作:
In [97]: result.unstack(0)
Out[97]:
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
In [98]: result.unstack('state')
Out[98]:
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
如果不是所有的级别值都能在各分组中找到的话,则unstack操作可能会引入缺失数据:
In [99]: s1 = pd.Series([0,1,2,3],index=['a','b','c','d'])
In [100]: s2 = pd.Series([4,5,6],index=['c','d','e'])
In [101]: data2 = pd.concat([s1,s2],keys=['one','two'])
In [102]: data2
Out[102]:
one a 0
b 1
c 2
d 3
two c 4
d 5
e 6
dtype: int64
In [103]: data2.unstack()
Out[103]:
a b c d e
one 0.0 1.0 2.0 3.0 NaN
two NaN NaN 4.0 5.0 6.0
stack默认会滤除缺失数据,因此该运算是可逆的:
In [104]: data2.unstack().stack()
Out[104]:
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
In [105]: data2.unstack().stack(dropna=False)
Out[105]:
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
在对DataFrame进行unstack操作时,作为旋转轴的级别将会成为结果中的最低级别:
In [110]: df.unstack('state')
Out[110]:
side left right
state Ohio Colorado Ohio Colorado
number
one 0 3 5 8
two 1 4 6 9
three 2 5 7 10
In [111]: df.unstack('state').stack('side')
Out[111]:
state Colorado Ohio
number side
one left 3 0
right 8 5
two left 4 1
right 9 6
three left 5 2
right 10 7
pivot函数是一个快捷方式:用set_index创建层次化索引,再用unstack重塑。
数据转换
移除重复数据
DataFrame中常常会出现重复行。
In [122]: data = pd.DataFrame({'k1':['one']*3 + ['two']*4,
...: 'k2':[1,1,2,3,3,4,4]})
In [123]: data
Out[123]:
k1 k2
0 one 1
1 one 1
2 one 2
3 two 3
4 two 3
5 two 4
6 two 4
In [124]: data.duplicated()
Out[124]:
0 False
1 True
2 False
3 False
4 True
5 False
6 True
dtype: bool
DataFrame的duplicated方法返回一个布尔型Series,表示各行是否是重复行。
drop_duplicates方法可以返回一个移除了重复行的DataFrame:
In [125]: data.drop_duplicates()
Out[125]:
k1 k2
0 one 1
2 one 2
3 two 3
5 two 4
以上的两个方法会默认判断全部列,你也可以指定部分列进行重复项判断。假设你还有一列,且只希望根据k1过滤重复项:
In [128]: data['v1'] = range(7)
In [129]: data
Out[129]:
k1 k2 v1
0 one 1 0
1 one 1 1
2 one 2 2
3 two 3 3
4 two 3 4
5 two 4 5
6 two 4 6
In [130]: data.drop_duplicates(['k1'])
Out[130]:
k1 k2 v1
0 one 1 0
3 two 3 3
duplicated和drop_duplicates默认保留第一个出现的值组合。
利用函数或映射进行数据转换
在对数据集进行转换时,你可能希望根据数组、Series或DataFrame列中的值来实现该转换工作。来看一个例子:
In [133]: data = pd.DataFrame({'food':['bacon','pulled pork','bacon','Pastrami','conrned beef','Bacon','pastrami','honey ham','nova lox'],
...: 'ounces':[4,3,12,6,7.5,8,3,5,6]})
In [134]: data
Out[134]:
food ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 Pastrami 6.0
4 conrned beef 7.5
5 Bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova lox 6.0
假设你想要添加一列表示该肉类食物来源的动物类型。先编写一个肉类到动物的映射:
In [135]: meat_to_animal = {
...: 'bacon':'pig',
...: 'pulled pork':'pig',
...: 'pastrami':'cow',
...: 'corned beef':'cow',
...: 'honey ham':'pig',
...: 'nova lox':'salmon'
...: }
Series的map方法可以接受一个函数或含有映射关系的字典型对象。
In [136]: data['animal'] = data['food'].map(str.lower).map(meat_to_animal)
In [137]: data
Out[137]:
food ounces animal
0 bacon 4.0 pig
1 pulled pork 3.0 pig
2 bacon 12.0 pig
3 Pastrami 6.0 cow
4 corned beef 7.5 cow
5 Bacon 8.0 pig
6 pastrami 3.0 cow
7 honey ham 5.0 pig
8 nova lox 6.0 salmon
也可以传入一个能够完成全部这些工作的函数:
In [145]: data['food'].map(lambda x:meat_to_animal[x.lower()])
Out[145]:
0 pig
1 pig
2 pig
3 cow
4 cow
5 pig
6 cow
7 pig
8 salmon
Name: food, dtype: object
使用map
是一种实现元素级转换以及其他数据清理工作的便捷方式。
替换值
利用fillna方法填充缺失数据可以看作值替换的一种特殊情况。虽然前面提到的map可用于修改对象的数据子集,而replace则提供了一种实现该功能的更简单、更灵活的方式。
In [146]: data = pd.Series([1,-999,2,-999,-1000,3])
In [147]: data
Out[147]:
0 1
1 -999
2 2
3 -999
4 -1000
5 3
dtype: int64
In [148]: data.replace(-999,np.nan)
Out[148]:
0 1.0
1 NaN
2 2.0
3 NaN
4 -1000.0
5 3.0
dtype: float64
如果你希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值:
In [149]: data.replace([-999,-1000],np.nan)
Out[149]:
0 1.0
1 NaN
2 2.0
3 NaN
4 NaN
5 3.0
dtype: float64
如果你希望对不同的值进行不同的替换,则传入一个由替换关系组成的列表或字典即可:
In [150]: data.replace([-999,-1000],[np.nan,0])
Out[150]:
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
In [151]: data.replace({-999:np.nan,-1000:0})
Out[151]:
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
重命名轴索引
和Series中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新对象。轴还可以被就地修改,而无需新建一个数据结构。
In [152]: data = pd.DataFrame(np.arange(12).reshape(3,4),index=['Ohio','Colorado','New York'],
...: columns=['one','two','three','four'])
In [153]: data
Out[153]:
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
In [154]: data.index.map(str.upper)
Out[154]: Index(['OHIO', 'COLORADO', 'NEW YORK'], dtype='object')
In [155]: data.index = data.index.map(str.upper)
In [156]: data
Out[156]:
one two three four
OHIO 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename:
In [157]: data.rename(index=str.title,columns=str.upper)
Out[157]:
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
rename可以结合字典型对象实现对部分轴标签的更新:
In [159]: data.rename(index={'OHIO':'INDIANA'},
...: columns={'three':'peekaboo'})
Out[159]:
one two peekaboo four
INDIANA 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
rename帮我们实现了:复制DataFrame并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入inplace=True即可。
离散化和面元划分
为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:
In [3]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,你需要使用pandas的cut函数:
In [4]: bins = [18, 25, 35, 60, 100]
In [5]: cats = pd.cut(ages,bins)
In [6]: cats
Out[6]:
[(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]]
pandas返回的是一个特殊的Categorical对象。结果展示了pandas.cut划分的面元。你可以将其看做一组表示面元名称的字符串。它的底层含有一个表示不同分类名称的类型数组,以及一个codes属性中的年龄数据的标签:
In [9]: cats.codes
Out[9]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
In [10]: cats.categories
Out[10]:
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
closed='right',
dtype='interval[int64]')
In [11]: pd.value_counts(cats)
Out[11]:
(18, 25] 5
(35, 60] 3
(25, 35] 3
(60, 100] 1
dtype: int64
跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False进行修改:
In [12]: pd.cut(ages, [18, 26, 36, 61, 100], right=False)
Out[12]:
[[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36, 61), [36, 61), [26, 36)]
Length: 12
Categories (4, interval[int64]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)]
可以通过传递一个列表或数组到labels,设置自己的面元名称:
In [13]: group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
In [14]: pd.cut(ages, bins, labels=group_names)
Out[14]:
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult]
Length: 12
Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。下面这个例子中,我们将一些均匀分布的数据分成四组:
In [19]: data = np.random.rand(20)
# precision=2,限定小数只有两位
In [20]: pd.cut(data, 4, precision=2)
Out[20]:
[(0.15, 0.36], (0.15, 0.36], (0.36, 0.57], (0.79, 1.0], (0.36, 0.57], ..., (0.57, 0.79], (0.79, 1.0], (0.79, 1.0], (0.57, 0.79], (0.57, 0.79]]
Length: 20
Categories (4, interval[float64]): [(0.15, 0.36] < (0.36, 0.57] < (0.57, 0.79] < (0.79, 1.0]]
qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分。根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点。而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元:
In [21]: data = np.random.randn(1000)
In [22]: cats = pd.qcut(data, 4)
In [23]: cats
Out[23]:
[(0.608, 2.892], (0.608, 2.892], (-2.985, -0.695], (-0.0327, 0.608], (0.608, 2.892], ..., (0.608, 2.892], (-0.0327, 0.608], (-0.0327, 0.608], (-0.695, -0.0327], (0.608, 2.892]]
Length: 1000
Categories (4, interval[float64]): [(-2.985, -0.695] < (-0.695, -0.0327] < (-0.0327, 0.608] < (0.608, 2.892]]
In [24]: pd.value_counts(cats)
Out[24]:
(0.608, 2.892] 250
(-0.0327, 0.608] 250
(-0.695, -0.0327] 250
(-2.985, -0.695] 250
dtype: int64
与cut类似,你也可以传递自定义的分位数(0到1之间的数值,包含端点):
In [25]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
Out[25]:
[(0.147, 0.288], (0.288, 0.694], (0.288, 0.694], (0.694, 0.98], (0.288, 0.694], ..., (0.288, 0.694], (0.694, 0.98], (0.98, 0.998], (0.694, 0.98], (0.288, 0.694]]
Length: 20
Categories (4, interval[float64]): [(0.147, 0.288] < (0.288, 0.694] < (0.694, 0.98] < (0.98, 0.998]]
检测和过滤异常值
异常值的过滤或变换运算在很大程度上其实就是数组运算。看一个例子:
In [177]: np.random.seed(12345)
In [178]: data = pd.DataFrame(np.random.randn(1000,4))
In [179]: data.describe()
Out[179]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.067684 0.067924 0.025598 -0.002298
std 0.998035 0.992106 1.006835 0.996794
min -3.428254 -3.548824 -3.184377 -3.745356
25% -0.774890 -0.591841 -0.641675 -0.644144
50% -0.116401 0.101143 0.002073 -0.013611
75% 0.616366 0.780282 0.680391 0.654328
max 3.366626 2.653656 3.260383 3.927528
假设你想找出某列中绝对值大于3的值:
In [181]: col[np.abs(col) > 3]
Out[181]:
97 3.927528
305 -3.399312
400 -3.745356
Name: 3, dtype: float64
要选出全部含有“超过3或-3的值”的行,可以利用布尔型DataFrame以及any方法:
In [184]: data[(np.abs(data)>3).any(1)]
Out[184]:
0 1 2 3
5 -0.539741 0.476985 3.248944 -1.021228
97 -0.774363 0.552936 0.106061 3.927528
102 -0.655054 -0.565230 3.176873 0.959533
305 -2.315555 0.457246 -0.025907 -3.399312
324 0.050188 1.951312 3.260383 0.963301
400 0.146326 0.508391 -0.196713 -3.745356
499 -0.293333 -0.242459 -3.056990 1.918403
523 -3.428254 -0.296336 -0.439938 -0.867165
586 0.275144 1.179227 -3.184377 1.369891
808 -0.362528 -3.548824 1.553205 -2.186301
900 3.366626 -2.372214 0.851010 1.332846
根据这些条件,可以轻松地对值进行设置。下面地代码可以将值限制在区间-3到3以内:
In [190]: data[np.abs(data)>3] = np.sign(data) * 3
In [191]: data.describe()
Out[191]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.067623 0.068473 0.025153 -0.002081
std 0.995485 0.990253 1.003977 0.989736
min -3.000000 -3.000000 -3.000000 -3.000000
25% -0.774890 -0.591841 -0.641675 -0.644144
50% -0.116401 0.101143 0.002073 -0.013611
75% 0.616366 0.780282 0.680391 0.654328
max 3.000000 2.653656 3.000000 3.000000
排列和随机采样
利用numpy.random.permutation函数可以轻松实现对Series或DataFrame的列的排列工作。通过需要排列的轴的长度调用permutation,可产生一个表示新顺序的整数数组:
In [196]: df = pd.DataFrame(np.arange(5*4).reshape(5,4))
In [197]: sampler = np.random.permutation(5)
In [198]: sampler
Out[198]: array([1, 0, 2, 3, 4])
In [199]: df
Out[199]:
0 1 2 3
0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
3 12 13 14 15
4 16 17 18 19
In [200]: df.take(sampler)
Out[200]:
0 1 2 3
1 4 5 6 7
0 0 1 2 3
2 8 9 10 11
3 12 13 14 15
4 16 17 18 19
要通过替换的方式产生样本,最快的方式是通过np.random.randint得到一组随机整数:
In [201]: bag = np.array([5,7,-1,6,4])
In [202]: sampler = np.random.randint(0,len(bag),size=10)
In [203]: sampler
Out[203]: array([2, 4, 4, 4, 4, 2, 2, 2, 0, 3])
In [204]: draws = bag.take(sampler)
In [205]: draws
Out[205]: array([-1, 4, 4, 4, 4, -1, -1, -1, 5, 6])
计算指标/哑变量
另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。
如果DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)。pandas有一个get_dummies函数可以实现该功能(其实自己动手做一个也不难)。使用之前的一个DataFrame例子:
In [27]: df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],'data1': range(6)})
In [28]: df
Out[28]:
data1 key
0 0 b
1 1 b
2 2 a
3 3 c
4 4 a
5 5 b
In [29]: pd.get_dummies(df['key'])
Out[29]:
a b c
0 0 1 0
1 0 1 0
2 1 0 0
3 0 0 1
4 1 0 0
5 0 1 0
有时候,你可能想给指标DataFrame的列加上一个前缀,以便能够跟其他数据进行合并。get_dummies的prefix参数可以实现该功能:
In [30]: dummies = pd.get_dummies(df['key'], prefix='key')
In [31]: df_with_dummy = df[['data1']].join(dummies)
In [32]: df_with_dummy
Out[32]:
data1 key_a key_b key_c
0 0 0 1 0
1 1 0 1 0
2 2 1 0 0
3 3 0 0 1
4 4 1 0 0
5 5 0 1 0
一个对统计应用有用的秘诀是:结合get_dummies和诸如cut之类的离散化函数:
In [33]: np.random.seed(12345)
In [34]: values = np.random.rand(10)
In [35]: values
Out[35]:
array([ 0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
In [36]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
In [37]: pd.get_dummies(pd.cut(values, bins))
Out[37]:
(0.0, 0.2] (0.2, 0.4] (0.4, 0.6] (0.6, 0.8] (0.8, 1.0]
0 0 0 0 0 1
1 0 1 0 0 0
2 1 0 0 0 0
3 0 1 0 0 0
4 0 0 1 0 0
5 0 0 1 0 0
6 0 0 0 0 1
7 0 0 0 1 0
8 0 0 0 1 0
9 0 0 0 1 0
字符串操作
大部分文本运算都直接做成了字符串对象的内置方法。对于更为复杂的模式匹配和文本操作,则可能需要用到正则表达式。。pandas对此进行了加强,它能使你能够对整组数据应用字符串表达式和正则表达式,而且能处理烦人的缺失数据。
字符串对象方法
Python内置的字符串方法
方法 | 说明 |
---|---|
count | 返回字串在字符串中出现的次数(非重叠) |
endswith、startswith | 如果字符串以某个后缀结尾(以某个前缀开头),则返回True |
join | 将字符串用作连接其他字符串序列的分隔符 |
index | 如果在字符串中找到子串,则返回子串第一个字符所在的位置。如果没有找到,则引发ValuleError |
find | 如果在字符串中找到子串,则返回第一个发现的子串的第一个字符所在的位置。如果没有找到,则返回-1 |
rfind | 如果在字符串中找到子串,则返回最后一个发现的子串的第一个字符所在的位置。如果没有找到,则返回-1 |
replace | 用另一字符串替换指定子串 |
strip、rstrip、lstrip | 去除空白符(包括换行符) |
split | 通过指定的分隔符将字符串拆分为一组子串 |
lower、upper | 分别将字母字符转换为小写或大写 |
ljust、rjust | 用空格(或其他字符)填充字符串的空白侧以返回符合最低宽度的字符串 |
正则表达式
正则表达式方法
方法 | 说明 |
---|---|
findall、finditer | 返回字符串中所有的非重叠匹配模式,findall返回的是由所有模式组成的列表,而finditer则返回一个迭代器逐个返回 |
match | 从字符串起始位置匹配模式,还可以对模式各部分进行分组。如果匹配到模式,则返回一个匹配项对象,否则返回None |
search | 扫描整个字符串以匹配模式。如果找到则返回一个匹配项对象。和match不同,其匹配项可以位于字符串的任意位置,而不仅仅是起始处。 |
split | 根据找到的模式将字符串拆分为数段 |
sub、subn | 将字符串中所有的(sub)或前n各(subn)模式替换为指定表达式。 |
pandas中矢量化的字符串函数
通过map函数,所有字符串和正则表达式方法都能被应用于(传入lambda表达式或其他函数)各个值,但是如果存在NA就会报错。为了解决这个问题,Series有一些能够跳过NA值得字符串操作方法。通过Series的str属性即可访问这些方法。例如,我们可以通过str.contains检查各个电子邮件地址是否含有“gmail”:
In [160]: data = {'Dava':'dave@google.com','Steve':'steve@gmail.com',
...: 'Rob':'rob@gmail.com','Wes':np.nan}
In [161]: data = pd.Series(data)
In [162]: data.str.contains('gmail')
Out[162]:
Dava False
Rob True
Steve True
Wes NaN
dtype: object
也可以使用正则表达式,还可以加上任意re选项(如IGNORECASE):
In [163]: pattern = '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
In [164]: import re
In [165]: data.str.findall(pattern,flags=re.IGNORECASE)
Out[165]:
Dava [(dave, google, com)]
Rob [(rob, gmail, com)]
Steve [(steve, gmail, com)]
Wes NaN
dtype: object
有两个办法可以实现矢量化的元素获取操作:要么使用str.get,要么在str属性上使用索引。
In [171]: matches = data.str.match(pattern,flags=re.IGNORECASE)
In [172]: matches
Out[172]:
Dava True
Rob True
Steve True
Wes NaN
dtype: object
In [173]: matches.str.get(1)
Out[173]:
Dava NaN
Rob NaN
Steve NaN
Wes NaN
dtype: float64
In [176]: data.str[:5]
Out[176]:
Dava dave@
Rob rob@g
Steve steve
Wes NaN
dtype: object
矢量化字符串方法
以上。