《Python数据分析实战》
1.数据转换
现在,该进行数据处理的第二步了:数据转换。调整过数据的形式和结构之后,接下来很重要的一步是对元素进行转换。
在数据转换过程中,有些操作会涉及重复机票无效元素,可能需要将其删除或替换为别的元素;而其他一些操作则跟删除索引相关;此外还有些步骤会涉及对数值或字符串类型的数据进行处理。
删除重复元素:
dframe
>>
color value
0 white 2
1 white 1
2 red 3
3 red 3
4 white 2
DataFrame对象的duplicated()函数可用来检测重复的行,返回元素为布尔类型的Series对象。每个元素对应一行,如果该行与其他行重复(也就是说该行不是第一次出现),则元素为True;如果跟前面不重复,则元素就为False。
dframe.duplicated()
>>
0 False
1 False
2 False
3 True
4 True
dtype:float64
返回元素为布尔值的Series对象用处很大,特别适用于过滤操作。如果要寻找重复的行,输入以下命令即可:
#寻找重复的行
dframe[dframe.duplicated()]
>>
color value
3 red 3
4 white 2
通常,所有重复的行都需要从DataFrame对象中删除。pandas库的drop_duplicated()函数实现了删除功能,该函数返回的是删除重复行后的DataFrame对象。
#删除重复行
dframe.drop_duplicated<>
>>
color value
0 white 2
1 white 1
2 red 3
映射:
映射关系创建一个映射关系列表,把元素跟一个特定的标签或字符串绑定起来。
要定义映射关系,最好的对象莫过于dict。
map = {
'label1':'value1',
'label2':'value2'
...
}
这一节要讲的几个函数虽然执行的操作各不相同,但它们都以表示映射关系的dict对象作为参数。
用映射替换元素:
replace()函数
frame
>>
color item price
0 white ball 5.56
1 rosso mug 4.20
2 verde pen 1.30
3 balck pencil 0.56
4 yellow ashtray 2.75
要用新元素替换不正确的元素,需要定义一组映射关系。在映射关系中,旧元素作为键,新元素作为值。
newcolors = {
'rosso':'red',
'verde':'green'
}
frame.replace(newcolors)
>>
color item price
0 white ball 5.56
1 red mug 4.20
2 green pen 1.30
3 balck pencil 0.56
4 yellow ashtray 2.75
还有一种常见情况,是把NaN替换为其他值,比如0。这种情况下,仍能可以用replace()函数,它能优雅地完成该项操作。
ser
>>
0 1
1 3
2 NaN
3 4
4 6
5 NaN
6 3
ser.replace(np.nan,0)
>>
0 1
1 3
2 0
3 4
4 6
5 0
6 3
用映射添加元素:
我们将利用映射关系从另外一个数据结构获取元素,将其添加到目标数据结构地列中。
映射对象总是要单独定义地。
frame
>>
color item
0 white ball
1 red mug
2 green pen
3 balck pencil
4 yellow ashtray
#定义一个dict对象,它里面时一列商品及其价格信息。
price = {
'ball':5.56,
'mug':4.20,
'bottle':1.30,
'scissors':3.41,
'pen':1.30,
'pecil':0.56,
'ashtray':2.75
}
#map()函数可应用于Series对象或DataFrame对象的一列,它接收一个函数或表示映射关系的字典作为参数。这里,在DataFrame的item这一列应用映射关系,用字典price作为参数,为DataFrame对象添加price列。
frame3['pric'] = frame3['item'].map(price)
color item pri
0 white ball 5.56
1 red mug 4.20
2 green pen 1.30
3 black pecil 0.56
4 yellow ashtray 2.75
#即是,给frame添加pric列,通过item这一列进行映射关联,用定义的dict对象作为参数,从而添加新的一列信息。
重命名轴索引:
我们可以采用跟操作Series和DataFrame对象的元素类似的方法,使用映射关系转换轴标签。
pandas的rename()函数,以表示映射关系的字典对象作为参数,替换轴的索引标签。
color item pri
0 white ball 5.56
1 red mug 4.20
2 green pen 1.30
3 black pecil 0.56
4 yellow ashtray 2.75
reprice = {
0:1,
1:2,
2:3,
3:4,
4:5
}
frame3.rename(reprice)
>>
color item pri
1 white ball 5.56
2 red mug 4.20
3 green pen 1.30
4 black pecil 0.56
5 yellow ashtray 2.75
如上所见,索引被重命名。若要重命名各列,必须使用columns选项。
接下来我们把两个映射对象分别赋给index和columns选项。
recolumn = {
'item':'object',
'price':'value'
}
frame.rename(index=reindex,columns=recolumn)
>>
color object value
1 white ball 5.56
2 red mug 4.20
3 green pen 1.30
4 black pecil 0.56
5 yellow ashtray 2.75
对于只有单个元素要替换的最简单情况,可以对传入的参数做进一步限定,而无需把多个变量都写出来,也避免产生多次赋值操作。
frame.rename(index={1:'first'},columns={'item':'object'})
>>
color object pri
0 white ball 5.56
first red mug 4.20
2 green pen 1.30
3 black pecil 0.56
4 yellow ashtray 2.75
前面这几个例子,rename()函数返回一个经过改动的新DataFrame对象,但原DataFrame对象仍保持不变。如果要改变调用函数的对象本身,可使用inplace选项,并将其值置为True。
frame.rename(index={1:'first'},columns={'item':'object'},inplace=True)
>>
color object pri
0 white ball 5.56
first red mug 4.20
2 green pen 1.30
3 black pecil 0.56
4 yellow ashtray 2.75
2.离散化和面元划分
有时,尤其是在实验中,我们要处理的大量数据为连续型的。然而为了便于分析它们,我们需要把数据打散为几个类别,例如把读数的取值范围划分为一个个小区间,统计每个区间的元素数量或其他统计量。
另外一种情况是,对总体做出精确的测量,得到了大量个体。这种情况下,为了便于数据分析,也需要把元素分成几个类别,然后分布分析每个类别的个体数量及其他统计量。
results = [12,34,67,55,28,90,99,12,3,56,74,44,87,23,49,89,87]
#那我把数据范围分为四个面元(bin),0-25,25-50,50-75,75-100
bins = [0,25,50,75,100]
cat = pd.cut(results,bins)
>>
[(0, 25], (25, 50], (50, 75], (50, 75], (25, 50], ..., (75, 100], (0, 25], (25, 50], (75, 100], (75, 100]]
Length: 17
Categories (4, interval[int64]): [(0, 25] < (25, 50] < (50, 75] < (75, 100]]
cut()函数返回的对象为Categorical(类别型)类型,可以将其看作一个字符串数组,其元素为面元的名称。
该对象内部的levels数组为不同内部类别的名称,labels数组的元素数量跟results数组(也就是说,划分成各面元的数据)相同,labels数组的各数字表示results元素所属的面元。
cat.levels
>>
Index([u'(0,25]',u'(25,50]',u'(50,75]',u'(75,100]'],dtype='object')
cat.labels
>>
array([0, 1, 2, 2, 1, 3, 3, 0, 0, 2, 2, 1, 3, 0, 1, 3, 3], dtype=int8)
如果你想知道每个面元的出现次数,即每个类别有多少个元素,可使用value_counts()函数。
pd.value_counts(cat)
>>
(75,100] 5
(0,25] 4
(25,50] 4
(50,75] 4
dtype:int64
可以用字符串数组指定面元的名称,把它赋给cut()函数的labels选项,然后用该函数创建Categorical对象。
bin_names = ['unlikely','less likely','likely','highly likely']
pd.cut(results,bins,labels=bin_names)
>>
[unlikely, less likely, likely, likely, less likely, ..., highly likely, unlikely, less likely, highly likely, highly likely]
Length: 17
Categories (4, object): [unlikely < less likely < likely < highly likely]
若不指定面元的各界限,而只传入一个整数作为参数,cut()函数就会按照指定的数字,把数组元素的取值范围划分为相应的几部分。
pd.cut(results,5)
>>
[(2.904, 22.2], (22.2, 41.4], (60.6, 79.8], (41.4, 60.6], (22.2, 41.4], ..., (79.8, 99.0], (22.2, 41.4], (41.4, 60.6], (79.8, 99.0], (79.8, 99.0]]
Length: 17
Categories (5, interval[float64]): [(2.904, 22.2] < (22.2, 41.4] < (41.4, 60.6] < (60.6, 79.8] < (79.8, 99.0]]
除了cut()函数,pandas还有另外一个划分面元的函数:qcut()函数。这个函数直接把样本分成五个面元。cut()是等距划分,每个面元的个体数量不同。qcut()函数是非等距划分,但每个面元包含的个体数量一样。
pd.qcut(results,5)
>>
[(2.999, 24.0], (24.0, 46.0], (62.6, 87.0], (46.0, 62.6], (24.0, 46.0], ..., (62.6, 87.0], (2.999, 24.0], (46.0, 62.6], (87.0, 99.0], (62.6, 87.0]]
Length: 17
Categories (5, interval[float64]): [(2.999, 24.0] < (24.0, 46.0] < (46.0, 62.6] < (62.6, 87.0] < (87.0, 99.0]]
异常值检测和过滤:
randframe = pd.DataFrame(np.random.randn(1000,3))
randframe.describe()
>>
0 1 2
count 1000.000000 1000.000000 1000.000000
mean -0.015680 -0.025656 -0.044529
std 0.993903 1.029393 0.976945
min -3.309426 -3.573099 -3.027491
25% -0.628534 -0.697366 -0.700416
50% -0.017854 -0.047359 -0.079736
75% 0.637814 0.677233 0.606627
max 2.977097 3.008050 4.105961
用std()函数就可以求得DataFrame对象每一列的标准差。
ranframe.std()
>>
0 1.064746
1 0.920449
2 0.875716
dtype: float64
借助any()函数,就可以对每一列应用筛选条件。
randframe[(np.abs(randframe) > (3*randframe.std())).any(1)]
>>
0 1 2
69 -0.442 -1.099 3.206
576 -0.154 -1.108 3.458
907 2.296 1.129 -3.735
3.排序
可以用numpy.random.permutation()函数产生的随机顺序,调整Series对象或DataFrame对象各行的顺序(随机排序)。
#1.准备要进行排序的数据
nframe
>>
0 1 2 3 4
0 0 1 2 3 4
1 5 6 7 8 9
2 10 11 12 13 14
3 15 16 17 18 19
4 20 21 22 23 24
#2.permutation()函数产生随机顺序,我们按照这个数组元素的顺序为DataFrame对象的行排序
new_order = np.random.permutation(5)
new_order
>>
array([2,3,0,1,4])
#3.对DataFrame对象的所有行应用take()函数,根据给定的索引重新排列数据
nframe.take(new_order)
>>
0 1 2 3 4
2 10 11 12 13 14
3 15 16 17 18 19
0 0 1 2 3 4
1 5 6 7 8 9
4 20 21 22 23 24
如你所见,DataFrame对象各行的位置已经发生改变。新索引的顺序跟new_order数组的元素顺序保持一致。
你甚至还可以只对DataFrame对象的一部分进行排序操作。它将生成一个数组,只包含特定索引范围的数据。例如,我们这里的2-4.
new_order = [3,4,2]
nframe.take(new_order)
>>
0 1 2 3 4
3 15 16 17 18 19
4 20 21 22 23 24
2 10 11 12 13 14
随机取样:
若DataFrame规模很大,有时可能需要从中随机取样,最快的方法莫过于使用np.random.randint()函数。
sample = np..random.randint(0,len(nframe),size=3)
sample
>> array([1,4,4])
nframe.take(sample)
>>
0 1 2 3 4
1 5 6 7 8 9
4 20 21 22 23 23
5 20 21 22 23 23
附注:np.random.randint()使用
low、high、size三个参数。默认high是None,如果只有low,那范围就是[0,low)。如果有high,范围就是[low,high)。
>>> np.random.randint(2, size=10)
array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0])
>>> np.random.randint(1, size=10)
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
>>> np.random.randint(5, size=(2, 4))
array([[4, 0, 2, 1],
[3, 2, 2, 0]])
从随机取样这个例子可知,你可以多次获取相同的样本。
4.字符串处理
内置的字符串处理方法:
你常常需要将复合字符串分成几部分,分别赋予不同的变量。
split()函数以参考点为分隔符,比如逗号,将文本分为几部分。
text = '16 Bolton Avenue , Boston'
text.split(',')
>> ['16 Bolton Avenue ','Boston']
如上所见,切分后得到的第一个元素以空白字符结尾。为了解决这个问题,使用split()函数切分后,还要再用strip()函数删除多余的空白字符(包括换行符)。
tokens = [s.strip() for s in text.split('.')]
#多重赋值
adress,city = [s.strip() for s in text.split('.')]
adress
>> '16 Bolton Avenue'
city
>> 'Boston'
除了文本的切分方法,我们还可以把多个字符串拼接在一起形成一段长文本。
adress + ',' + city
>> '16 Bolton Avenue , Boston'
若只有两三个字符串,这种拼接方法很好用。若要拼接很多字符串,更为实用的方法则是,在作为连接符的字符串上调用join()函数。
string = ['A+','A','A-','B','BB','BBB','C+']
';'.join(string)
>> 'A+;A;A-;B;BB;BBB;C+'
另一类字符串操作是查找子串。Python的in关键字是检测子串的最好方法。
'Boston' in text
>> True
而这两个函数能够实现字符串查找:index()和find()。
text.index('Boston')
>> 19
text.find('Boston')
>> 19
这两个函数均返回子串在字符串中的索引。但是,如若没能找到子串,这两个函数的表现有所不同。若子串找不到,index()函数会报错,而find()函数会返回-1.
获知字符串或字符串组合在文本中的出现次数,用count()函数即可。
text.count('e')
>> 2
text.count('Avenue')
>> 1
针对字符串的另外一种操作是替换或删除字符串中的子串(或单个字符)。这两种操作都可以用replace()函数实现,如用空字符替换子串,效果等同于删除子串。
text.replace('Avenue','Street')
>> '16 Bolton Street, Boston'
text.replace('1','')
>> '6 Bolton Avenue, Boston'
正则表达式:
用正则表达式在文本中查找和匹配字符串模式很灵活。Python内置的re模块用于操作regex对象。
re模块所提供的函数可以分为以下几个类别:
text = 'This is an\t odd \n text!'
re.split('\s+',text)
>> ['This','is','an','odd','text!']
其实调用re.split()函数时,首先编译正则表达式,然后再作为参数传入的文本上调用split()函数。
因此你可以用re.compile()函数编译正则表达式,得到一个可以重用的正则表达式对象,从而节省CPU周期。
#1.在字符串组合或数组中,迭代查找子串时,预先编译正则表达式,能显著提升效率。
regex = re.compile('\s+')
#2.用compile()创建regex对象后,可直接像下面这样调用它的split()方法。
regex.split(text)
>> ['This','is','an','odd','text!']
findall()函数可匹配文本中所有符合正则表达式的子串。该函数返回一个列表,元素为文本中所有符合正则表达式的子串。
text = 'This is my address: 16 Bolton Avenue, Boston'
re. findall('A\w+',text)
['Avenue']
re. findall('[A,a]\w+',text)
['address','Avenue']
跟findall()函数相关的另外两个函数时:match()和search()。
findall()函数返回一列所有符合模式的子串,而search()函数仅返回第一处符合模式的子串。而且返回的时一个特殊类型的对象,只记录了该子串在字符串中的开始和结束位置。
re.search('[A,a]\w+',text)
>> <_sre.SRE_Match object at 0x000000007D7ECC8>
re.start()
>> 11
re.end()
>> 18
text[search.start():search.end()]
>> 'address'
match()函数从字符串开头开始匹配;如果第一个字符就不匹配,它不会再搜索字符串内部。如果没能找到任何匹配的子串,它不会返回任何对象。
如果match()有返回内容,则它返回的对象与search()函数返回的相同。
match = re.match('T\w+',text)
text[match.start():match.end()]
>> 'This'