Pandas中数据规整化:清理、转换、合并、重塑
数据分析和建模方面的大量编程工作都是用在数据准备上的:加载、清理、转换以及重塑。有时候,存放在文件或数据库中的数据并不能满足你的数据处理应用的要求,这个时候通过python和pandas所提供的一系列的高效的、灵活的、高级的核心函数和算法能使你更轻松地将数据规整化为正确的形式。
import pandas as pd
import numpy as np
一、合并数据集:
pandas对象中的数据可以通过一些内置的方式进行合并:
pandas.merge()可根据一个或多个键将不同的DataFrame中的行连接起来,类似于数据库的连接操作;
pandas.concat()可以沿着一条轴将多个对象堆叠到一起;
实例方法combine_first()可以将重复的数据连接到一起,用一个对象中的值填充另一个对象中的缺失值;
数据库风格的DataFrame合并:
数据集的合并(merge)或连接(join)运算都是通过一个或多个键将行链接起来的,这些运算是关系型数据库的核心。pandas的merge()函数主要完成的就是该功能,具体的参数如下所示:
我们可以通过几个例子来查看merge()函数的用法:
例1:合并两个DataFrame,且没有指定要用哪个列进行连接,这种情况下merge()会将列名相同的列作为连接的键。
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)
不过最好使用参数on显示指定一下:
pd.merge(df1, df2, on='key')
若要根据多个键进行合并,则可以在参数on上传入一个列名组成的列表:
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')
例2:对两个不同列名的对象进行合并,要使用参数left_on、参数right_on分别指定这两个对象要链接的列名:
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')
例3:通过参数how设置不同的连接方式,在默认情况下merge做的是"inner"连接,此外还有"outer"、"left"、"right",依次表示的是内连接(交集)、外连接(并集)、左连接、右连接:
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, how='outer')
df1 = pd.DataFrame({'key':['b','b','a','c','a','b'], 'data1':range(6)})
df2 = pd.DataFrame({'key':['a','b','a','b','d'], 'data2':range(5)})
pd.merge(df1, df2, on='key', how='left')
pd.merge(df1, df2, on='key', how='right' )
例4:利用参数suffixes处理重复列名,可以用于指定附加到左右两个DataFrame对象的重叠列名上的字符串:
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', suffixes=('_left', '_right'))
索引上的合并:
有的时候,要参与连接的键可能位于DataFrame的索引中,在这种情况下,我们可以通过将参数left_index、right_index设置为True来表示索引被用作连接键:
例1:
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)
例2:对于层次化索引的数据,我们必须以列表的形式指明用作合并键的多个列的列名:
lefth = pd.DataFrame({'key1':['Ohio','Ohio','Ohio','Nevada','Nevada'],'key2':[2000,2001,2002,2001,2002],'data':np.arange(5.)})
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)
例3:DataFrame还有一个join()实例方法,它能更加方便地实现按索引合并,还可以用于合并多个带有相同或相似索引的DataFrame对象,而不管它们之间有没有重叠的列:
left2 = pd.DataFrame([[1,2],[3,4].[5,6]], index=['a','c','e'], columns=['Ohio','Nevada'])
right2 = pd.DataFrame([[7,8],[9,10],[11,12],[13,14]], index=['b','c','d','e'], columns=['Missouri','Alabama'])
left2.join(right2, how='outer')
DataFrame的join()函数在默认情况下是做左连接的,还支持参数DataFrame跟调用者DataFrame的某个列之间的连接:
left1 = pd.DataFrame({'key':['a','b','a','a','b','c'], 'value':range(6)})
right1 = pd.DataFrame({'group_val':[3.5,7]}, index=['a','b'])
left1.join(right1,on='key') # 表示的是,left1的'key'列和right1的索引做左连接
轴向连接:
还有一种数据合并运算也被称作连接(concatenation)、绑定(binding)或堆叠(stacking)。
对于numpy数组,在numpy中有一个合并numpy数组的concatenate()函数:
arr = np.arange(12).reshape((3,4))
np.concatenate([arr,arr], axis=1)
对于pandas对象(如DataFrame和Series)来说,pandas的concat()函数提供了一种高效地解决合并运算的可靠方式。concat()函数的参数列表如下所示:
我们将通过一些例子理解其具体的使用方式:
例1:把3个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])
例2:默认情况下,concat是在axis=0上工作的,也可以通过参数axis设置其值为1。在axis=1的情况下,很容易就能看出在轴上没有重合:
pd.concat([s1,s2,s3], axis=1)
例3:可以通过参数join来设置合并的方式,可选值有inner、outer,默认的是outer:
s4 = pd.concat([s1*5,s3])
pd.concat([s1,s4], axis=1, join='outer')
pd.concat([s1,s4], axis=1, join='inner')
例4:可以通过参数join_axes来指定要在其他轴上使用的索引,即通过参数join_axes指出新对象的索引结构,具体的数据与要合并的对象相对应:
pd.concat([s1,s4], axis=1, join_axes=[['a','f','b','e']])
例5:在合并的过程中会有一个问题,就是参与连接的片段在结果中区分不开,通过参数keys可以在连接轴上创建一个层次化索引来解决这个问题:
result = pd.concat([s1,s2,s3], keys=['one','two','three'])
如果将参数axis=1对Series进行合并,那么参数keys的值将会成为DataFrame的列头(列索引):
pd.concat([s1,s2,s3], axis=1, keys=['one','two','three'])
例6:对DataFrame使用concat()函数:
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'])
如果传入的不是列表而是一个字典,那么字典的键就会被当作参数keys的值:
pd.concat({'level':df1, 'level2':df2}, axis=1)
例7:可以通过参数names来设置分成级别的名称:
pd.concat([df1,df2], axis=1, keys=['level1','level2'], names=['upper','lower'])
例8:通过参数ignore_index可以忽略合并时的索引问题:
df1 = pd.DataFrame(np.random.randn(3,4), columns=['a','b','c','d'])
df2 = pd.DataFrame(np.random.randn(2,3), columns=['b','d','a'])
pd.concat([df1,df2], ignore_index=False)
pd.concat([df1,df2], ignore_index=True)
合并重叠数据:
还有一种数据组合问题,比如说,你可能有的数据集其索引全部或部分重叠,这个时候我们可以使用numpy中的where()函数,它用于表达一种矢量化的if-else:
例1:
a = pd.Series([np.nan,2.5,np.nan,3.5,4.5,np.nan], index=['f','e','d','c','b','a'])
b = pd.Series(np.arange(len(a), dtype=np.float64), index=['f','e','d','c','b','a'])
b[-1] = np.nan
np.where(pd.isnull(a),b,a)
例2:还可以通过Series、DataFrame的combine_first()函数实现这个功能,而且还会进行数据对齐:
二、重塑和轴向旋转:
那些重新排列表格型数据的基础运算,这些函数也称作重塑(reshape)或轴向旋转(pivot)运算。
重塑层次化索引:
层次化索引为DataFrame数据的重排任务提供和了一种具有良好一致性的方式:
例1:通过使用stack()函数,能将DataFrame的行转换为列:
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()
例2:通过使用unstack()函数,可以将一个层次化索引的Series的列转换为行从而变成一个DataFrame:
result.unstack()
例3: 在默认情况下,unstack()函数和stack()函数操作的是最内层,但是我们可以给参数level传入分层级别的编号或名称即可对其他级别进行unstack或stack操作
result.unstack(level=0)
result.unstack(level='state')
例4:如果有些级别值在各分组中找不到的话,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设置为False改变:
data2.unstack().stack()
data2.unstack().stack(dropna=False)
例5:对DataFrame进行unstack()、stack()操作时,作为旋转轴的级别将会成为结果中的最低级别:
df = pd.DataFrame({'left':result,'right':result+5}, columns=pd.Index(['left','right'], name='side'))
df.unstack('state')
df.unstack('state').stack('side')
将“长格式”旋转为“宽格式”:
时间序列数据通常是以所谓的“长格式”(long)或“堆叠格式”(stacked)存储在数据库和CSV中的,例如:
但是长格式的数据操作起来不是那么轻松,假设我们想要把以上数据处理成:不同的item值分别形成一列,date列中的时间值则用作索引。这个时候DataFrame的pivot()函数可以实现这个转换:
pivoted = ldata.pivot('data', 'item', 'value')
pivoted.head()
pivot()函数的前两个参数值表示用作行索引和列索引的列名,最后一个参数则是DataFrame里的数据值。
三、数据转换:
数据转换主要完成的是过滤、清理以及其他的转换工作。
移除重复数据:
DataFrame中常常会出现重复行,如何处理这些重复数据,pandas为我们提供了许多有用的函数:
例1:通过duplicated()函数,可以返回一个布尔型的Series,表示各行是否是重复行:
data = pd.DataFrame({'k1':['one']*3+['two']*4,'k2':[1,1,2,3,3,4,4]})
data.duplicated()
例2:通过drop_duplicates()函数,能返回一个移除了重复行的DataFrame:
data.drop_duplicates()
例3:DataFrame对象的duplicated()、drop_duplicates()函数在默认情况下是对所有列进行判断的,我们也可以指定某几列进行操作:
data['v1'] = range(7)
data.drop_duplicates(['k1'])
date.drop_duplicates(['k1','k2'])
例4:在默认情况下,duplicated()和drop_duplicates()保留的是第一个出现的值组合,可以通过参数task_last=True来设置成最后一个:
data.drop_duplicates(['k1','k2'], take_last=True)
利用函数或映射进行数据转换:
在对数据集进行转换时,可能希望根据数组、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]})
1.假设我们想要添加一列表示该肉类食物来源的动物类型,首先我们应该先编写一个肉类到动物的映射,然后使用Series中的map()函数进行元素级操作:
meat_to_animal = {'bacon':'pig','pulled pork':'pig','pastrami':'cow','corned beef':'cow','honey ham':'pig','nova lox':'salmon'}
data['animal'] = data['food'].map(str.lower).map(meat_to_animal)
或者我们可以通过lambda表达式完成这个工作:
data['food'].map(lambda x: meat_to_animal[x.lower()])
替换值:
利用fillna()函数能对缺失值进行处理,可以看成是值替换的一种特殊情况,更普通的方法是使用replace()函数来实现,而且该函数更加简单、灵活。
例1:我们有一个Series其中某些值为-999,而且该值表示的是一个缺失数据的标记值,现在想要将其替换为NaN:
data = pd.Series([1,-999,2,-999,-1000,3])
data.replace(-999, np.nan)
例2:如果你希望一次替换多个值,可以传入一个由待替换值组成的列表以及一个替换值:
data.replace([-999,-1000], np.nan)
例3:如果希望对不同的值进行不同的替换,则传入一个由替换关系组成的列表即可(或者是字典):
data.replace([-999,-1000], [np.nan,0])
data.replace({-999:np.nan,-1000:0})
重命名轴索引:
轴标签也可以通过函数或映射进行转换,从而得到一个新对象;轴还可以被就地修改:
例1:将DataFrame的index标签转换为相应的大写字母的形式:
data = pd.DataFrame(np.arange(12).reshape((3,4)), index=['Ohio','Colorado','New York'], columns=['one','two','three','four'])
data.index = data.index.map(str.upper)
例2:利用rename()函数可以实现该功能,但是它返回的是以及修改过索引新的DataFrame:
data.rename(index=str.title, columns=str.upper)
data.rename(index={'OHIO':'INDIANA'}, columns={'three':'peekaboo'})
离散化和面元划分:
为了方便分析,连续数据常常会被离散化或拆分为"面元"(bin)。我们通过几个例子来详细的说明面元在数据分析中的作用:
例1:假设我们有一组人员年龄数据,我们希望将它们划分为不同的年龄组,例如:划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元,这个时候就可以使用pandas的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)
cut()函数返回的是一个特殊的Categorical对象,可以将它看成一组表示面元名称的字符串,它含有一个表示不同分类名称的levels数组以及一个为年龄数据进行标号的labels属性:
cats.labels
cats.levels
pd.value_counts(cats)
例2:默认情况下,面元的区间是前开后闭的,也可以通过参数right=False进行修改:
pd.cut(ages, [18,26,36,61,100], right=False)
例3:通过给参数labels传入一个列表或数组可以设置自定义的面元名称:
group_names = ['Youth','YoungAdult','MiddleAged','Senior']
pd.cut(ages,bins,labels=group_names)
例4:还可以向cut()传入面元的数量,则cut()会根据数据的最小值和最大值计算出等长面元:
data = np.random.rand(20)
pd.cut(data, 4, precision=2)
例5:qcut()函数是一个类似于cut()的函数,它可以根据样本分位数对数据进行面元划分,即qcut()可以使得每个面元内的数据点数量相同:
data = np.random.randn(1000)
cats = pd.qcut(data, 4)
检测和过滤异常值:
异常值(outlier)的过滤或变换运算在很大程度上其实就是数组运算。比如我们对一个含有正太分布数据的DataFrame进行操作:
np.random.seed(12345)
data = pd.DataFrame(np.random.randn(1000,4))
data.describe()
假设我们想要找出第4列中绝对值大小超过3的值:
col = data[3]
col[np.abs(col)>3]
如果我们要选出全部含有“超过3或-3的值”的行,可以利用布尔型DataFrame以及any()函数:
data[(np.abs(data)>3).any(1)]
排列和随机采样:
利用numpy.random.permutation函数可以实现对Series或DataFrame的列的排列工作。通过需要排列的轴的长度调用permutation,可以产生一个表示新顺序的整数数组:
df = pd.DataFrame(np.arange(5*4).reshape(5,4))
sampler = np.random.permutation(5)
然后我们可以通过基于iloc的索引操作或take()函数中使用该数组了:
df.take(sampler)
计算指标/哑变量:
另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量矩阵(dummy)”或“指示矩阵(indicator matrix)”,即DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)。通过pandas的get_dummies()函数可以实现该功能:
df = pd.DataFrame({'key':['b','b','a','c','a','b'],'data1':range(6)})
pd.get_dummies(df['key'])
还可以给指示的DataFrame的列加上一个指定的前缀,以便能够跟其他数据进行合并,通过get_dummies()函数的参数prefix可以实现:
dummies = pd.get_dummies(df['key'], prefix='key')
df_with_dummy = df[['data1']].join(dummies)
四、字符串操作:
python具有强大的处理字符串和文本的功能,大部分的操作都直接通过字符串对象的内置方法就能完成,对于更复杂的模式匹配和文本操作,则可能会用到正则表达式。
字符串对象方法:
python中的内置的字符串常用的函数如下所示:
我们可以通过几个例子来进一步加深对于字符串对象函数的理解:
例1:以逗号分隔的字符串数组可以用split拆分:
val = 'a,b guido'
val.split(',')
例2:使用join()函数将字符串用作其他字符串的分隔符:
val = 'a,b, guido'
pieces = [x.strip() for x in val.split(',')]
'::'.join(pieces)
例3:使用python的in关键字检测子串,也可以使用inex()、find()函数:
'guido' in val
val.index(',')
val.find(':')
find()和index()函数都会返回第一次出现子串的位置,区别在于:当找不到时,index()将会引发一个异常;而find()将会返回-1。
例4:使用count()函数,可以返回指定子串的出现次数:
val.count(',')
例5:使用replace()函数能将指定模式替换为另一个模式:
val.replace(',','::')
val.replace(',','')
正则表达式:
正则表达式(regex)提供了一种灵活的在文本中搜索或匹配字符串模式的方式。正则表达式是根据正则表达式语言编写的字符串,python内置的re模块负责对字符串应用正则表达式。re模块的函数可以分成三个大类:模式匹配、替换以及拆分,一个regex描述了需要在文本中定位的一个模式。如图所示是,python的re模块的基本方法,下面我们通过几个例子来说明:
例1:拆分一个分隔符为数量不一定的一组空白符(制表符、空格、换行符等)的字符串,描述一个或多个空白符的regex是\s+:
import re
text = 'foo bar\t baz \tqux'\
re.split('\s+',text)
调用re.split('\s+'.text)时,正则表达式先会被编译,然后再在text上调用其split()函数。我们还可以用re.compile()自己编译regex以得到一个可重用的regex对象,如果打算对许多字符串应用同一条正则表达式,强烈建议通过re.compile()函数创建regex对象从而节省大量的CPU时间:
regex = re.compile('\s+')
regex.split(text)
例2:如果只希望得到匹配regex的所有模式,则可以使用findall()函数:
regex.findall(text)
例3:match()函数和search()函数跟findall()的功能相类似,findall返回的是字符串中所有的匹配项;search则是只返回第一个匹配项;match只匹配字符串的首部:
text = """
Dave aaa@qq.com
Steve aaa@qq.com
Rob aaa@qq.com
Ryan aaa@qq.com
"""
pattern = r'[A-Z0-9._%+-]aaa@qq.com[A-Z0-9.-]+\.[A-Z]{2,4}'
regex = re.compile(pattern, flags=re.IGNORECASE)
regex.findall(text)
regex.search(text)
regex.match(text)
pandas中矢量化的字符串函数:
清理待分析的散乱数据时,常常需要做一些字符串规整化的工作。这个时候使用矢量化操作数据的方式是十分高效的,具体的函数如下图所示: