《利用python进行数据分析》读书笔记--第七章 数据规整化:清理、转换、合并、重塑(二)
3、数据转换
介绍完数据的重排之后,下面介绍数据的过滤、清理、以及其他转换工作。
-
去重
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame #DataFrame去重 data = DataFrame({'k1':['one']*3 + ['two'] * 4, 'k2':[1,1,2,3,3,4,4,]}) #print data print data.duplicated() #返回一个布尔型Series,重复的为True,不重复的为False #得到去重之后的DataFrame,应该意识到这是非常常用的 print data.drop_duplicates().reset_index(drop = True) #可以选定需要去重的列 print data.drop_duplicates(['k1']) #默认保留第一次出现的行 print data.drop_duplicates(['k1'],take_last = True) #设定保留最后一个出现的行
>>>
0 False
1 True
2 False
3 False
4 True
5 False
6 True
k1 k2
0 one 1
1 one 2
2 two 3
3 two 4
k1 k2
0 one 1
3 two 3
k1 k2
2 one 2
6 two 4
[Finished in 0.7s]
-
利用函数或者映射进行数据转换
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame data = 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]}) print data #假如你想添加一列表示该肉类食物来源的动物类型,我们先编写一个肉类到动物的映射。 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) print data #下面看一下map用来执行函数,即将data['food']的每个元素应用到隐含函数 print data['food'].map(lambda x:meat_to_animal[x.lower()])
>>>
food ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 Pastrami 6.0
4 corned beef 7.5
5 Bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova lox 6.0
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
0 pig
1 pig
2 pig
3 cow
4 cow
5 pig
6 cow
7 pig
8 salmon
Name: food
[Finished in 0.8s]
-
替换值
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame #下面看replace函数 data = Series([1.,-999.,2.,-999.,-1000.,3.]) print data #用replace替换-999、-1000,注意Series可以直接用,相当于矢量化了 print data.replace([-999,-1000],np.nan) #下面看一下numpy,不能直接用replace和map #data1 = np.arange(10) #print data1.replace(0,np.nan) #print data1.map(lambda x: x + 1) print data.replace([-999,-1000],[np.nan,0]) print data.replace({-999:np.nan,-1000:0})
>>>
0 1
1 -999
2 2
3 -999
4 -1000
5 3
0 1
1 NaN
2 2
3 NaN
4 NaN
5 3
0 1
1 NaN
2 2
3 NaN
4 0
5 3
0 1
1 NaN
2 2
3 NaN
4 0
5 3
[Finished in 0.8s]
-
重命名轴索引
跟Series的值一样,轴标签可以通过函数或者映射进行转换,从而得到一个新对象,轴还可以被就地修改,而无需新建一个数据结构。
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame data = DataFrame(np.arange(12).reshape((3,4)),index = ['Ohio','Colorado','New York'], columns = ['one','two','three','four']) print data #轴标签的map方法 print data.index.map(str.upper) #就地修改 data.index = data.index.map(str.upper) print data #下面用rename得到一个副本 print data.rename(index = str.title,columns = str.upper) #rename可以结合字典对象进行更新 print data.rename(index = {'OHIO':'INDIANA'},columns = {'three':'peekaboo'}) #rename可以将DataFrame的索引和标签进行复制和赋值 #就地修改 _ = data.rename(index = {'OHIO':'INDIANA'},inplace = True) print data print '\n',type(_) print _
>>>
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
[OHIO COLORADO NEW YORK]
one two three four
OHIO 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
one two peekaboo four
INDIANA 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
one two three four
INDIANA 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
<class 'pandas.core.frame.DataFrame'>
one two three four
INDIANA 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
[Finished in 0.8s]
-
离散化和面元划分
为了便于分析,连续数据常常被离散化或拆分为面元(bin),即分组。
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame ages = [20,22,25,27,21,23,37,31,61,45,41,32] bins = [18,25,35,60,100] #用的是cut函数 cats = pd.cut(ages,bins) print cats #返回的是一个特殊的Categorical对象,可以看作是表示面元名称的字符串。 #它含有一个表示不同分类名称的levels数组以及一个labels属性: print cats.labels #是分组的序号,标示为第几组 print cats.levels print pd.value_counts(cats) #得到的是几个“区间”,不包括左,包括右,可用right = False包括左,不包括右 print pd.cut(ages,[18,26,36,61,100],right = False) #可以设置自己的面元名称,设置label是即可 group_names = ['Youth','YoungAdult','MiddleAged','Senior'] print pd.cut(ages,bins,labels = group_names) #当然可以为cut传入面元的数量而不是具体的分界点,会自动均匀分布 data = np.random.randn(20) print data #下面标识分为4组,精度为2位 print pd.cut(data,4,precision = 2) #qcut函数是一个类似于cut的函数,可以根据样本分位数对数据进行面元划分。根据数据,cut可能无法 #是各个面元数量数据点相同,qcut使用的是样本分位数,因此可以得大小基本相等的面元。 data = np.random.randn(1000) cats = pd.qcut(data,4) #四份位数进行切割 print cats print pd.value_counts(cats) #当然可以设置自定义的分位数(0到1的值) print pd.qcut(data,[0,0.1,0.5,0.9,1.])
>>>
Categorical:
array([(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], (18, 25],
(35, 60], (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]], dtype=object)
Levels (4): Index([(18, 25], (25, 35], (35, 60], (60, 100]], dtype=object)
[0 0 0 1 0 0 2 1 3 2 2 1]
array([(18, 25], (25, 35], (35, 60], (60, 100]], dtype=object)
(18, 25] 5
(35, 60] 3
(25, 35] 3
(60, 100] 1
Categorical:
array([[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), [18, 26),
[36, 61), [26, 36), [61, 100), [36, 61), [36, 61), [26, 36)], dtype=object)
Levels (4): Index([[18, 26), [26, 36), [36, 61), [61, 100)], dtype=object)
Categorical:
array([Youth, Youth, Youth, YoungAdult, Youth, Youth, MiddleAged,
YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult], dtype=object)
Levels (4): Index([Youth, YoungAdult, MiddleAged, Senior], dtype=object)
Categorical:
array([(-0.5, 0.66], (0.66, 1.82], (0.66, 1.82], (-0.5, 0.66],
(-1.67, -0.5], (0.66, 1.82], (-0.5, 0.66], (-1.67, -0.5],
(0.66, 1.82], (-1.67, -0.5], (-1.67, -0.5], (-1.67, -0.5],
(-1.67, -0.5], (-0.5, 0.66], (-0.5, 0.66], (-0.5, 0.66],
(-0.5, 0.66], (1.82, 2.98], (-0.5, 0.66], (-0.5, 0.66]], dtype=object)
Levels (4): Index([(-1.67, -0.5], (-0.5, 0.66], (0.66, 1.82],
(1.82, 2.98]], dtype=object)
[-3.161, -0.624] 250
(0.69, 2.982] 250
(0.0578, 0.69] 250
(-0.624, 0.0578] 250
[Finished in 0.7s]
-
检测和过滤异常值
异常值(outlier)的过滤或变换运算在很大程度上其实就是数组运算。
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame np.random.seed(12345) data = DataFrame(np.random.randn(1000,4)) print data.describe() #假设想要找出某些列中绝对值大小超过3的值 col = data[3] #print col print col[np.abs(col) > 3] #找出全部含有超过3或-3的值的行 print data[(np.abs(data) > 3).any(1)] #对上面的这样的值限制在-3到3内 data[np.abs(data) > 3] = np.sign(data) * 3 print data.describe()
>>>
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
97 3.927528
305 -3.399312
400 -3.745356
Name: 3
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
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
[Finished in 0.8s]
-
排列和随机采样
下面是随机选取一个DataFrame的一些行,做法就是随机产生行号,然后进行选取即可。
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame df = DataFrame(np.arange(5 * 4).reshape(5,4)) sampler = np.random.permutation(5) #返回一个随机排列 print df print sampler #然后可以在基于ix的索引操作或者take函数中使用该数组 print df.take(sampler) #作者这里说了非替换式采样,我理解就是不重复采样吧! #下面是进行截取 print df.take(np.random.permutation(len(df))[:3]) bag = np.array([5,7,-1,6,4]) sampler = np.random.randint(0,len(bag),size = 10) print sampler draws = bag.take(sampler) print draws
>>>
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
[3 2 1 0 4]
0 1 2 3
3 12 13 14 15
2 8 9 10 11
1 4 5 6 7
0 0 1 2 3
4 16 17 18 19
0 1 2 3
4 16 17 18 19
0 0 1 2 3
3 12 13 14 15
[1 0 1 3 4 3 3 2 0 2]
[ 7 5 7 6 4 6 6 -1 5 -1]
[Finished in 0.7s]
-
计算指标/哑变量
另一种常用的用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量矩阵”(dummy matrix)或“指标矩阵”(indicator matrix)。如果DataFrame的某一列有k各不同的值,可以派生出一个k列的矩阵或者DataFrame(值为1和0)。这样的做法在下一章(第八章)的地图的例子中有体现(谁让我先看的第八章,当时还在想这个办法好,原来根源在这里)。
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame df = DataFrame({'key':['b','b','a','c','a','b'],'data1' : range(6)}) print df print pd.get_dummies(df['key']) #得到哑变量DataFrame #有时候,你想给指标DataFrame的列加上一个前缀,一边进行合并。 #这个功能好,但是注意是给指标DataFrame的列名加的前缀 dummies = pd.get_dummies(df['key'],prefix = 'key') print dummies df_with_dummy = df[['data1']].join(dummies) #按行索引合并 print df_with_dummy #这里说一个隐藏的trick,df['data1']得到一个Series,而df[['data1']]得到一个DataFrame print type(df['data1']) #Series而已,列名丢掉 print type(df[['data1']]) #DataFrame 是有列名的 #下面看如果DataFrame的某行属于多个分类怎么办,利用ch02中的MovieLens数据。 names = ['movie_id','title','genres'] movies = pd.read_table('E:\\movies.dat',sep = '::',header = None,names = names) print movies[:10] #要为genre添加指标变量的时候需要先进性数据规整。 #首先把所有genres提取出来 genre_iter = (set(x.split('|')) for x in movies.genres) genres = sorted(set.union(*genre_iter)) dummies = DataFrame(np.zeros((len(movies),len(genres))),columns = genres) #接下来,迭代每一部电影并将dummies各行的项设置为1 for i,gen in enumerate(movies.genres): dummies.ix[i,gen.split('|')] = 1 #然后与movies合并起来 movies_windic = movies.join(dummies.add_prefix('Genre_')) print movies_windic.ix[0] #但是对于河大的数据,这种方法构建指标非常慢。肯定需要编写一个能够利用DataFrame内部机制的更低级的函数才行 #一个对统计应用的秘诀是:结合get_dummies和诸如cut之类的离散化函数 values = np.random.rand(10) print values bins = [0,0.2,0.4,0.6,0.8,1] print pd.get_dummies(pd.cut(values,bins))
4、字符串操作
Python有简单易用的字符串和文本处理功能。大部分文本运算直接做成了字符串对象的内置方法。当然还能用正则表达式。pandas对此进行了加强,能够对数组数据应用字符串表达式和正则表达式,而且能处理烦人的缺失数据。
-
字符串对象方法
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame #字符串对象方法 #对于大部分的字符串而言,内置的方法已经能够满足要求了 val = 'a,b, guido' print val.split(',') #返回的是一个列表 pieces = [x.strip() for x in val.split(',')] #strip函数修剪空白字符 print pieces #利用加法可以把字符串连接起来,注意下面的赋值方式 first,second,third = pieces print first +'::' + second +'::'+ third #上面的不实用,下面是一种更快的风格 print '::'.join(pieces) #另一种方法是字串定位,常用的有 in、index、find print 'guido' in val #返回布尔型,是否在字符串中 print val.index(',') #返回第一次出现的位置,找不到返回异常 print val.find(':') #返回第一次出现字符的位置,找不到返回-1,可以指定从哪个位置开始和结束 print val.count(',') #返回个数 print val.replace(',','::') print val.replace(',','') #传入''用来删除字符
>>>
['a', 'b', ' guido']
['a', 'b', 'guido']
a::b::guido
a::b::guido
True
1
-1
2
a::b:: guido
ab guido
[Finished in 0.6s]
#上面这些都能用正则表达式实现
Python内置的字符串方法有:
-
正则表达式
正则表达式(regex)提供了一种灵活的在文本中搜索、匹配字符串的模式。用的是re模块。re模块的函数分为3类:模式匹配、替换、拆分。关于正则表达式的总结,参考一下:http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html (谢谢作者)。
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame import re text = "foo bar\t baz \tqux" print re.split('\s+',text) #这条语句是先编译正则表达式 \s+ (多个空白字符),然后再调用split regex = re.compile('\s+') print regex.split(text) #下面是找到匹配regex的所有模式 print regex.findall(text) #注意:想转义字符\不起作用,即作为一个单独字符,可以直接在前面加r,原生字符串 text1 = r'foo \t' print text1 #如果想对许多字符串都应用同一条正则表达式,应该先compile节省时间 #findall 返回字符串中所有匹配项,而search则只返回第一个匹配项。match更加严格,它只匹配字符串的首部 text = """Dave dave@google.com Steve steve@gmail.com Rob rob@gmail.com Ryan ryan@yahoo.com """ pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}' #下面第二个参数的作用是正则对大小写不敏感 regex = re.compile(pattern,flags = re.IGNORECASE) print regex.findall(text) #返回一个list #search返回第一个邮件地址,返回的是一种特殊特殊对象,这个对象只能告诉我们模式在原始字符串中的起始和结束位置 m = regex.search(text) print m print text[m.start():m.end()] #regex.match返回None,因为它只匹配出现在字符串开头的模式,也就是说,无法指定要开始和结束的匹配位置 print regex.match(text) #还有一个sub方法,会将匹配到的模式替换为指定字符串,并返回新字符串 print regex.sub('REDACTED',text) #另外,如果想将找出的模式分段,用圆括号括起来即可 pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})' regex = re.compile(pattern,flags = re.IGNORECASE) m = regex.match('wesm@bright.net') #返回一个match对象 print m print m.groups() #弄成元组的形式输出 print regex.findall(text) #返回一个列表,每一项都是一个元组 print regex.sub(r'Username: \1, Domain: \2, Suffix: \3',text) #sub可以利用\1 \2 \3访问被替换的字符串 #看下面的小例子 regex = re.compile(r""" (?P<username>[A-Z0-9._%+-]+) @ (?P<domain>[A-Z0-9.-]+) \. (?P<suffix>[A-Z]{2,4})""",flags = re.IGNORECASE|re.VERBOSE) #这样可以得到一个简单的字典 m = regex.match('wesm@bright.net') print m.groupdict()
>>>
['foo', 'bar', 'baz', 'qux']
['foo', 'bar', 'baz', 'qux']
[' ', '\t ', ' \t']
foo \t
['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']
<_sre.SRE_Match object at 0x03343758>
dave@google.com
None
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED
<_sre.SRE_Match object at 0x03342A70>
('wesm', 'bright', 'net')
[('dave', 'google', 'com'), ('steve', 'gmail', 'com'), ('rob', 'gmail', 'com'), ('ryan', 'yahoo', 'com')]
Dave Username: dave, Domain: google, Suffix: com
Steve Username: steve, Domain: gmail, Suffix: com
Rob Username: rob, Domain: gmail, Suffix: com
Ryan Username: ryan, Domain: yahoo, Suffix: com
{'username': 'wesm', 'domain': 'bright', 'suffix': 'net'}
[Finished in 0.8s]
正则表达式的方法有:
-
pandas中矢量化字符串函数
#-*- encoding: utf-8 -*- import numpy as np import pandas as pd import matplotlib.pyplot as plt from pandas import Series,DataFrame import re data = {'Dave':'dave@google.com','Steve':'steve@gmail.com','Rob':'rob@gmail.com','Web':np.nan} data = Series(data) print data print data.isnull() #通过map,所有字符串和正则都能传入各个值(通过lambda或者其他函数),但是如果存在NA就会报错。 #然而,Series有些跳过NA的方法。通过Series的str属性可以访问这些方法。 print '\n',data.str.contains('gmail'),'\n' #查看是否每个包含gmail pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})' print data.str.findall(pattern,flags = re.IGNORECASE),'\n' #print data.str.replace('@','') #这里的replace可以矢量化应用到每个元素 #有两个办法可以实现矢量化的元素获取操作:要么使用str.get,要么在str属性上用索引 matches = data.str.match(pattern,flags = re.IGNORECASE) print matches,'\n' print matches.str.get(1),'\n' print matches.str[0],'\n' #可以这样进行截取 print data.str[:5],'\n' #下面这样只是选取前两个 print data[:2]
>>>
Dave dave@google.com
Rob rob@gmail.com
Steve steve@gmail.com
Web NaN
Dave False
Rob False
Steve False
Web True
Dave False
Rob True
Steve True
Web NaN
Dave [('dave', 'google', 'com')]
Rob [('rob', 'gmail', 'com')]
Steve [('steve', 'gmail', 'com')]
Web NaN
Dave ('dave', 'google', 'com')
Rob ('rob', 'gmail', 'com')
Steve ('steve', 'gmail', 'com')
Web NaN
Dave google
Rob gmail
Steve gmail
Web NaN
Dave dave
Rob rob
Steve steve
Web NaN
Dave dave@
Rob rob@g
Steve steve
Web NaN
Dave dave@google.com
Rob rob@gmail.com
[Finished in 0.7s]
下面矢量化的字符串方法,比较重要。