7.1 处理缺失值
缺失数据会在很多数据分析应用中出现。pandas的目标之一就是尽可能无痛地处理缺失值。例如,pandas使用浮点值NaN(Not a Number来表示缺失值)。我们称NaN为容易检测到的标识值:
In [4]: string_data = pd.Series(['aardvark','artichoke',np.nan,'avocado'])
In [5]: string_data
Out[5]:
0 aardvark
1 artichoke
2 NaN
3 avocado
dtype: object
在pandas中,我们采用了R语言中的编程惯例,将缺失值称为NA,意思是not available(不可用)。在统计学应用中,NA数据可以是不存在的数据或者是存在但不可观察的数据(例如在数据收集过程中出现了问题)。当清晰数据用于分析时,对缺失数据本身进行分析以确定数据收集问题或数据丢失导致的数据偏差通常很重要。
Python内建的None值在对象数组中也被当做NA处理:
In [6]: string_data[0] = None
In [7]: string_data.isnull()
Out[7]:
0 True
1 False
2 True
3 False
dtype: bool
pandas项目持续改善处理缺失值的内部细节,但是用户API函数,比如pandas.isnull,抽象掉了很多令人厌烦的细节。
7.1.1 过滤缺失值
有多重过滤缺失值的方法。虽然你可以使用pandas.isnull和布尔值索引手动地过滤缺失值,但dropna在过滤缺失值时是非常有用的。在Series上使用dropna,它会返回Series中所有的非空数据及其索引值:
In [1]: import numpy as np
In [2]: import pandas as pd
In [3]: from numpy import nan as NA
In [4]: data = pd.Series([1,NA,3.5,NA,7])
In [5]: data.dropna()
Out[5]:
0 1.0
2 3.5
4 7.0
dtype: float64
上面的例子与下面的代码是等价的:
In [6]: data[data.notnull()]
Out[6]:
0 1.0
2 3.5
4 7.0
dtype: float64
当处理DataFrame对象时,事情会稍微更复杂一点。你可能想要删除全部为NA或包含有NA的行或列。dropna默认情况下会删除包含缺失值的行:
In [8]: data = pd.DataFrame([[1.,6.5,3.],[1.,NA,NA],[NA,NA,NA],[NA,6.5,3.]])
In [9]: cleaned = data.dropna()
In [10]: data
Out[10]:
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
传入how=‘all’时,将删除所有值均为NA的行:
In [11]: data.dropna(how='all')
Out[11]:
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0
如果要用同样的方式去删除列,传入参数axis=1
In [12]: data[4] = NA
In [13]: data
Out[13]:
0 1 2 4
0 1.0 6.5 3.0 NaN
1 1.0 NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN 6.5 3.0 NaN
In [15]: data.dropna(axis = 1,how="all")
Out[15]:
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
过滤DataFrame的行的相关方法往往涉及时间序列数据。假设你指向保留包含一定数量的观察值的行。你可以用thresh参数来表示:
In [20]: df = pd.DataFrame(np.random.randn(7,3))
In [21]: df.iloc[:4,1] = NA
In [22]: df.iloc[:2,2] = NA
In [23]: df
Out[23]:
0 1 2
0 -2.542647 NaN NaN
1 0.950216 NaN NaN
2 0.590236 NaN 1.727509
3 0.324402 NaN 0.188453
4 -1.901340 -1.285691 -0.527262
5 0.350042 -1.448348 0.864309
6 -0.542675 0.407457 -1.017582
In [24]: df.dropna()
Out[24]:
0 1 2
4 -1.901340 -1.285691 -0.527262
5 0.350042 -1.448348 0.864309
6 -0.542675 0.407457 -1.017582
In [25]: df.dropna(thresh=1)
Out[25]:
0 1 2
0 -2.542647 NaN NaN
1 0.950216 NaN NaN
2 0.590236 NaN 1.727509
3 0.324402 NaN 0.188453
4 -1.901340 -1.285691 -0.527262
5 0.350042 -1.448348 0.864309
6 -0.542675 0.407457 -1.017582
In [26]: df.dropna(thresh=2)
Out[26]:
0 1 2
2 0.590236 NaN 1.727509
3 0.324402 NaN 0.188453
4 -1.901340 -1.285691 -0.527262
5 0.350042 -1.448348 0.864309
6 -0.542675 0.407457 -1.017582
thresh参数是指:保留下的每一行,其非NA的数目>=N。举个例子,上面的代码,thresh为1时,保留下来的0行和1行,都有两个NaN(因为要求非NA的值有1个或1个以上就行)。但是为2的时候,就要求必须保留2个或2个以上非NaN的值,所以0和1不符合要求,被删除了。
7.1.2 补全缺失值
你有时可能需要以多种方式补全“漏洞”,而不是过滤缺失值(也可能丢弃其他数据)。大多数情况下,主要使用fillna方式来补全缺失值。调用fillna时,可以使用一个常数来代替缺失值:
In [27]: df.fillna(0)
Out[27]:
0 1 2
0 -2.542647 0.000000 0.000000
1 0.950216 0.000000 0.000000
2 0.590236 0.000000 1.727509
3 0.324402 0.000000 0.188453
4 -1.901340 -1.285691 -0.527262
5 0.350042 -1.448348 0.864309
6 -0.542675 0.407457 -1.017582
在调用fillna时使用字典,你可以为不同设定不同的填充值:
In [28]: df.fillna({1:0.5,2:0})
Out[28]:
0 1 2
0 -2.542647 0.500000 0.000000
1 0.950216 0.500000 0.000000
2 0.590236 0.500000 1.727509
3 0.324402 0.500000 0.188453
4 -1.901340 -1.285691 -0.527262
5 0.350042 -1.448348 0.864309
6 -0.542675 0.407457 -1.017582
fillna返回是一个新对象,但你也可以修改已经存在的对象:
In [30]: _ = df.fillna(0,inplace=True)
In [31]: df
Out[31]:
0 1 2
0 -2.542647 0.000000 0.000000
1 0.950216 0.000000 0.000000
2 0.590236 0.000000 1.727509
3 0.324402 0.000000 0.188453
4 -1.901340 -1.285691 -0.527262
5 0.350042 -1.448348 0.864309
6 -0.542675 0.407457 -1.017582
inplace=True的意思是不创建新的对象,直接对原始对象进行修改。反之inplace=False:对数据进行修改,创建并返回新的对象承载其修改结果。
回顾一下:
loc函数:通过行索引 “Index” 中的具体值来取行数据。
iloc函数:通过行号来取行数据(如取第二行的数据)。
用于重建索引的相同的插值方法也可以用于fillna:
In [33]: df = pd.DataFrame(np.random.randn(6,3))
In [34]: df.iloc[2:,1]= NA
In [36]: df.iloc[4:,2] = NA
In [37]: df
Out[37]:
0 1 2
0 1.022014 1.740698 1.232054
1 0.937588 0.620767 0.188563
2 -0.583284 NaN 2.839281
3 0.295677 NaN 0.009839
4 0.972779 NaN NaN
5 1.039146 NaN NaN
In [38]: df.fillna(method = 'ffill') # 取前一个值填充缺失值
Out[38]:
0 1 2
0 1.022014 1.740698 1.232054
1 0.937588 0.620767 0.188563
2 -0.583284 0.620767 2.839281
3 0.295677 0.620767 0.009839
4 0.972779 0.620767 0.009839
5 1.039146 0.620767 0.009839
In [39]: df.fillna(method='ffill',limit=2)
Out[39]:
0 1 2
0 1.022014 1.740698 1.232054
1 0.937588 0.620767 0.188563
2 -0.583284 0.620767 2.839281
3 0.295677 0.620767 0.009839
4 0.972779 NaN 0.009839
5 1.039146 NaN 0.009839
使用fillna你可以完成很多带有一点创造性的工作。例如,你可以将Series的平均值或中位数用于填充缺失值:
In [40]: data = pd.Series([1.,NA,3.5,NA,7])
In [41]: data.fillna(data.mean())
Out[41]:
0 1.000000
1 3.833333
2 3.500000
3 3.833333
4 7.000000
dtype: float64
7.2 数据转换
7.2.1 删除重复值
由于各种原因,DataFrame中会出现重复行。请看如下例子:
In [42]: data = pd.DataFrame({'k1':['one','two'] * 3 + ['two'],'k2':[1,1,2,3,3,4,4,]})
In [43]: data
Out[43]:
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4
DataFrame的duplicated方法返回的是一个布尔值Series,这个Series反映的是每一行是否存在重复(与之前出现过的行相同)情况:
In [44]: data.duplicated()
Out[44]:
0 False
1 False
2 False
3 False
4 False
5 False
6 True
dtype: bool
drop_duplicates返回的是DataFrame,内容是duplicated返回数组中为False的部分:
In [45]: data.drop_duplicates()
Out[45]:
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
这些方法默认都是对列进行操作。你可以指定数据的任何子集来检测是否有重复。假设我们有一个额外的列,并想基于‘k1’列去除重复值:
In [46]: data['v1'] = range(7)
In [47]: data.drop_duplicates(['k1'])
Out[47]:
k1 k2 v1
0 one 1 0
1 two 1 1
这里我猜测的意思是,按照k1的重复值去除多余内容。简单来说,k1有两个值,one和two,OK,其他的都是重复值(都是one和two)。那么就把其他行都删除。因为是基于它的逻辑,所以其他两列也都只剩下0和1两个位置的数据。
如果我们选择基于v1去除重复值:
In [51]: data.drop_duplicates(['v1'])
Out[51]:
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
5 two 4 5
6 two 4 6
因为v1没有重复,所以以它为基准,所有都会放出来。
duplicated 和duplicates 默认都是保留第一个观测的值。传入参数keep=‘last’将会返回最后一个:
In [52]: data.drop_duplicates(['k1','k2'],keep='last')
Out[52]:
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
6 two 4 6
上段代码中5跟6的数据内容是一样的,按照默认值,就会保留第5行的内容。如果设置为keep=‘last’则会保留最后一行重复的内容,即第6行。
7.2.2 使用函数或映射进行数据转换
对于许多数据集,你可能希望基于DataFrame的数组、列或列中的数值进行一些转换。考虑下面这些手机到的关于肉类的假设数据:
In [54]: 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]})
In [55]: data
Out[55]:
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
假设你想要添加一列用于表明每种食物的动物肉类型。让我们先写下一个食物和肉类的映射:
In [56]: meat_to_animal = {'bacon':'pig','pulled pork':'pig','pastrami':'cow','corned beef':'cow','honey ham':'pig','nova lox':'sa
...: lmon'}
Series 的map方法接收一个函数或一个包含映射关系的字典型对象,但是这里我们有一个小的问题在于一些肉类大写了,而另一部分肉类没有。因此,我们需要使用Series的str.lower方法将每个值都转换为小写:
In [57]: lowercased = data['food'].str.lower()
In [58]: lowercased
Out[58]:
0 bacon
1 pulled pork
2 bacon
3 pastrami
4 corned beef
5 bacon
6 pastrami
7 honey ham
8 nova lox
Name: food, dtype: object
In [59]: data['animal'] = lowercased.map(meat_to_animal)
In [60]: data
Out[60]:
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 [61]: data['food'].map(lambda x:meat_to_animal[x.lower()])
Out[61]:
0 pig
1 pig
2 pig
3 cow
4 cow
5 pig
6 cow
7 pig
8 salmon
Name: food, dtype: object
使用map是一种可以便携执行按元素转换及其他清晰相关操作的方法。
感觉这个超方便,超有用的。
7.2.3 替代值
使用fillna填充缺失值是通用值替换的特殊案例。前面你已经看到,map可以用来修改一个对象中的子集的值,但是replace提供了更为简单的实现。让我们考虑下面的Series:
In [62]: data = pd.Series([1.,-999.,2.,-999.,-1000.,3.])
In [63]: data
Out[63]:
0 1.0
1 -999.0
2 2.0
3 -999.0
4 -1000.0
5 3.0
dtype: float64
-999可能是缺失值的标识。如果要使用NA来替代这些值,我们可以使用replace方法生成新的Series(除非你传入了inplace = True):
In [64]: data.replace(-999,np.nan)
Out[64]:
0 1.0
1 NaN
2 2.0
3 NaN
4 -1000.0
5 3.0
dtype: float64
如果你想要一次替代多个值,你可以传入一个列表和替代值:
In [66]: data.replace([-999,-1000],np.nan)
Out[66]:
0 1.0
1 NaN
2 2.0
3 NaN
4 NaN
5 3.0
dtype: float64
要将不同的值替换为不同的值,可以传入替代值的列表:
In [67]: data.replace([-999,-1000],[np.nan,0])
Out[67]:
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
参数也可以通过字典传递:
In [68]: data.replace({-999:np.nan,-1000:0})
Out[68]:
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
7.2.4 重命名轴索引
和Series中的值一样,可以通过函数或某种形式的映射对轴标签进行类似的转换,生成新的且带有不同标签的对象。你也可以在不生成新的数据结构的情况下修改轴。下面的简单的示例:
In [69]: data = pd.DataFrame(np.arange(12).reshape((3,4)),index=['Ohio','Colorrando','New York'],columns=['one','two','three','fou
...: r'])
与Series类似,轴索引也有一个map方法:
In [72]: transform = lambda x: x[:4].upper()
In [73]: data.index.map(transform)
Out[73]: Index(['OHIO', 'COLO', 'NEW '], dtype='object')
你可以赋值给index,修改DataFrame:
In [72]: transform = lambda x: x[:4].upper() # 将小写字母转为大写
In [73]: data.index.map(transform)
Out[73]: Index(['OHIO', 'COLO', 'NEW '], dtype='object')
In [74]: data.index = data.index.map(transform)
In [75]: data
Out[75]:
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
如果你想要创建数据集转换后的版本,并且不修改原有的数据集,一个有用的方法是rename:
In [76]: data.rename(index=str.title,columns=str.upper)
Out[76]:
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colo 4 5 6 7
New 8 9 10 11
值得注意的是,rename可以结合字典型对象使用,为轴标签的子集提供新的值:
In [77]: data.rename(index={'OHIO':'INDIANA'},columns={'three':'peekaboo'})
Out[77]:
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
rename可以让你从手动赋值DataFrame并为其分配索引和列属性的烦琐工作中解放出来。如果你想要修改原有的数据集,传入inplace=True:
In [78]: data.rename(index={'OHIO':'INDIANA'},inplace=True)
In [79]: data
Out[79]:
one two three four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
7.2.5 离散化和分箱
连续值经常需要离散化,或者分离成“箱子”进行分析。假设你有某项研究中一组人群的数据,你想将他们进行分组,放入离散的年龄框中:
In [80]: ages = [20,22,25,27,21,23,37,31,61,45,41,32]
让我们将这些年来氛围18~25、26~35、36~60以及61及以上等若干组。为了实现这个,你可以使用pandas中的cut:
In [83]: ages = [20,22,25,27,21,23,37,31,61,45,41,32]
In [84]: bins = [18,25,35,60,100]
In [85]: cats = pd.cut(ages,bins)
In [86]: cats
Out[86]:
[(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计算出的箱。你可以将它当做一个表示箱名的字符串数组;它在内部包含一个categories(类别)数组,它指定了不同的类别名称以及codes属性中的ages(年龄)数据标签:
In [87]: cats.codes
Out[87]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
In [88]: cats.categories
Out[88]:
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]],
closed='right',
dtype='interval[int64]')
In [90]: pd.value_counts(cats)
Out[90]:
(18, 25] 5
(35, 60] 3
(25, 35] 3
(60, 100] 1
dtype: int64
请注意,pd.value_counts(cat)是对pandas.cut的结果中的箱数量的计数。
这里稍微说明一下,cats = pd.cut(ages,bins)产生的代码,是把每个值进行了判断,是否符合18-25或其他区间的标准。如果符合,则展示这个区间。
cats.codes是把上面的区间用位置的方式表示了一下,18-25位置是0,所以符合这个区间的,就显示0。
pd.value_counts(cats)就很好理解了,统计每个区间到底有多少人。
与区间的数学符号一致,小括号表示边是开放的,中括号表示它是封闭的(包括边)。你可以通过传递right=Flase来改变哪一边是封闭的:
In [92]: pd.cut(ages,[18,26,36,61,100],right=False)
Out[92]:
[[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 [96]: group_name=['Youth','YoungAdult','MiddleAged','Senior']
In [97]: pd.cut(ages,bins,labels=group_name)
Out[97]:
['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', 'MiddleAged', 'MiddleAged', 'YoungAdult']
Length: 12
Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior']
用‘Youth’代替了[18,26)这个区间,后面以此类推。
如果你传给cut整数个的箱来代替显式的箱边,pandas将根据数据中的最小值和最大值计算出登场的箱。请考虑一些均匀分布的数据被切成四份的情况:
In [98]: data = np.random.rand(20)
In [99]: pd.cut(data,4,precision=2)
Out[99]:
[(0.24, 0.47], (0.47, 0.7], (0.24, 0.47], (0.24, 0.47], (0.24, 0.47], ..., (0.7, 0.92], (0.7, 0.92], (0.7, 0.92], (0.7, 0.92], (0.013, 0.24]]
Length: 20
Categories (4, interval[float64]): [(0.013, 0.24] < (0.24, 0.47] < (0.47, 0.7] < (0.7, 0.92]]
precision=2的选项将十进制精度限制在两位。
qcut是一个与分箱密切相关的函数,它基于样本分位数进行分箱。取决于数据的分部,使用cut通常不会使每个箱具有相同数据量的数据点。由于qcut使用样本的分位数,你可以通过qcut获得等长的箱:
In [100]: data = np.random.randn(1000) # 创建1000个符合正态分布的数字
In [101]: cats = pd.qcut(data,4) #分成四份
In [102]: cats
Out[102]:
[(0.612, 3.813], (0.612, 3.813], (-0.00382, 0.612], (-0.687, -0.00382], (-0.687, -0.00382], ..., (0.612, 3.813], (0.612, 3.813], (-3.812, -0.687], (-0.00382, 0.612], (-0.687, -0.00382]]
Length: 1000
Categories (4, interval[float64]): [(-3.812, -0.687] < (-0.687, -0.00382] < (-0.00382, 0.612] <
(0.612, 3.813]]
In [103]: pd.value_counts(cats)
Out[103]:
(0.612, 3.813] 250
(-0.00382, 0.612] 250
(-0.687, -0.00382] 250
(-3.812, -0.687] 250
dtype: int64
与cut类似,你可以传入自定义的分位数(0和1之间的数据,包括边):
In [105]: pd.qcut(data,[0,0.1,0.5,0.9,1.])
Out[105]:
[(-0.00382, 1.251], (-0.00382, 1.251], (-0.00382, 1.251], (-1.237, -0.00382], (-1.237, -0.00382], ..., (-0.00382, 1.251], (-0.00382, 1.251], (-1.237, -0.00382], (-0.00382, 1.251], (-1.237, -0.00382]]
Length: 1000
Categories (4, interval[float64]): [(-3.812, -1.237] < (-1.237, -0.00382] < (-0.00382, 1.251] <
(1.251, 3.813]]
后续章节中,在讨论聚合和分组操作时,我们将会继续讨论cut和qcut,因为这些离散化函数对于分位数和分组分析特别有用。
7.2.6 检测和过滤异常值
过滤和转换异常值在很大程度上是应用数组操作的事情。考虑一个具有正态分布数据的DataFrame:
In [106]: data = pd.DataFrame(np.random.randn(1000,4))
In [107]: data.describe()
Out[107]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.018082 0.021869 0.090763 0.025524
std 1.002398 0.999319 1.009378 1.005402
min -3.222422 -3.918949 -2.756499 -3.425593
25% -0.650670 -0.656746 -0.588106 -0.606468
50% 0.000861 0.017076 0.130587 0.006299
75% 0.674098 0.690391 0.769313 0.735913
max 2.881582 2.967609 3.284050 3.422062
假设你想要找出一列中绝对值大于三的值:
In [108]: col = data[2]
In [109]: col[np.abs(col) > 3]
Out[109]:
464 3.120978
663 3.284050
Name: 2, dtype: float64
要选出所有值大于3或小于-3的行,你可以对布尔值DataFrame使用any方法:
In [110]: data[(np.abs(data)>3).any(1)]
Out[110]:
0 1 2 3
65 0.725354 -0.229617 1.439439 -3.037837
134 0.132391 -1.720085 -1.005669 3.281496
238 -1.333250 0.497088 0.621423 -3.425593
254 -0.196575 -0.170967 -0.969176 3.259875
316 -1.018200 2.374311 0.367179 -3.024408
416 0.428719 -3.918949 -0.307882 0.367570
449 -3.222422 -1.151136 0.072224 1.287579
464 0.470770 -0.487070 3.120978 1.347797
539 -3.093332 0.979261 -0.016025 0.494266
663 -0.438668 1.410007 3.284050 -1.194417
667 0.876969 -0.055498 -1.432704 3.005850
754 0.191503 -1.069135 1.291623 -3.034431
858 -2.178947 0.246048 1.875275 3.422062
927 -3.108568 -0.208819 -1.006349 -1.378480
值可以根据这些标准来设置,下面代码限制了-3到3之间的数值:
In [111]: data[np.abs(data)>3] = np.sign(data)*3
In [112]: data.describe()
Out[112]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.017658 0.022788 0.090358 0.025077
std 1.001099 0.996109 1.008161 1.000694
min -3.000000 -3.000000 -2.756499 -3.000000
25% -0.650670 -0.656746 -0.588106 -0.606468
50% 0.000861 0.017076 0.130587 0.006299
75% 0.674098 0.690391 0.769313 0.735913
max 2.881582 2.967609 3.000000 3.000000
语句np.sign(data)根据数值中的值的正负分别生成1和-1的数值:
In [113]: np.sign(data).head()
Out[113]:
0 1 2 3
0 -1.0 -1.0 -1.0 -1.0
1 1.0 -1.0 -1.0 -1.0
2 1.0 -1.0 -1.0 1.0
3 1.0 1.0 1.0 1.0
4 -1.0 -1.0 1.0 -1.0
7.2.7 置换和随机抽样
使用numpy.random.permutation对DataFrame中的Series或行进行置换(随机重排序)是非常方便的。在调用permutation时根据你想要的轴长度可以产生一个表示新顺序的整数数组:
In [114]: df = pd.DataFrame(np.arange(5*4).reshape((5,4)))
In [115]: sampler = np.random.permutation(5)
In [116]: sampler
Out[116]: array([1, 0, 4, 2, 3])
整数数组可以用在基于iloc的索引或等价的take函数中:
In [117]: df
Out[117]:
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 [118]: df.take(sampler)
Out[118]:
0 1 2 3
1 4 5 6 7
0 0 1 2 3
4 16 17 18 19
2 8 9 10 11
3 12 13 14 15
这里是指将默认的(0、1、2、3、4)排序规则更改为(1、0、4、2、3)
要选出一个不含有替代值的随机子集,你可以使用Series和DataFrame的sample方法:
In [119]: df.sample(n=3) # 在数据集中提取指定行数,这里为提取三行数据
Out[119]:
0 1 2 3
4 16 17 18 19
2 8 9 10 11
0 0 1 2 3
要生成一个带有替代值的样本(允许有重复选择),将replace=True传入sample方法:
In [120]: choices = pd.Series([5,7,-1,6,4])
In [121]: draws = choices.sample(n=10,replace=True)
In [122]: draws
Out[122]:
2 -1
3 6
4 4
1 7
2 -1
2 -1
2 -1
4 4
3 6
1 7
dtype: int64
7.2.8 计算指标/虚拟变量
将分类变量转换为“虚拟”或“指标”矩阵是另一种用于统计建模或机器学习的转换操作。如果DataFrame中的一列有K个不同的值,则可以衍生一个K列的值为1和0的矩阵或DataFrame。pandas有一个get_dummies函数用于实现该功能,尽管你自行实现也不难。让我们回顾一下之前的一个示例DataFrame:
In [123]: df = pd.DataFrame({'key':['b','b','a','c','a','b'],'data1':range(6)})
In [124]: pd.get_dummies(df['key'])
Out[124]:
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方法中有一个前缀参数用于实现该功能:
In [125]: dummies = pd.get_dummies(df['key'],prefix='key') # prefix就是前缀的意思
In [126]: df_with_dummy = df[['data1']].join(dummies)
In [127]: df_with_dummy
Out[127]:
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
如果DataFrame中的一行属于多个类别,则情况略微复杂,让我们看看MovieLens的IM数据集,在第14章中有更为详细的介绍:
In [130]: mnames = ['movie_id','title','genres']
In [131]: movies = pd.read_table('datasets/movielens/movies.dat',sep='::',header=None,names=mnames)
/opt/anaconda3/lib/python3.8/site-packages/pandas/io/parsers.py:765: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support regex separators (separators > 1 char and different from '\s+' are interpreted as regex); you can avoid this warning by specifying engine='python'.
return read_csv(**locals())
In [133]: movies[:10]
Out[133]:
movie_id title genres
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy
5 6 Heat (1995) Action|Crime|Thriller
6 7 Sabrina (1995) Comedy|Romance
7 8 Tom and Huck (1995) Adventure|Children's
8 9 Sudden Death (1995) Action
9 10 GoldenEye (1995) Action|Adventure|Thriller
为每个电影流派添加指标变量需要进行一些数据处理。首先,我们从数据集中提取出所有不同的流派的列表:
In [136]: all_genres= []
In [137]: for x in movies.genres:
...: all_genres.extend(x.split('|'))
...:
In [138]: genres = pd.unique(all_genres) # 去重
然后我们得到:
In [139]: genres
Out[139]:
array(['Animation', "Children's", 'Comedy', 'Adventure', 'Fantasy',
'Romance', 'Drama', 'Action', 'Crime', 'Thriller', 'Horror',
'Sci-Fi', 'Documentary', 'War', 'Musical', 'Mystery', 'Film-Noir',
'Western'], dtype=object)
使用全0的DataFrame是构建指标DataFrame的一种方式:
In [142]: zero_matrix = np.zeros((len(movies),len(genres)))
In [143]: dummies=pd.DataFrame(zero_matrix,columns=genres)
之后,使用.loc根据这些指标来设置值:
In [145]: for i ,gen in enumerate(movies.genres):
...: indices = dummies.columns.get_indexer(gen.split('|'))
...: dummies.iloc[i,indices] = 1
只有,和前面一样,你可以将结果与movies进行联合:
In [146]: movies_windic = movies.join(dummies.add_prefix("Genre_"))
In [147]: movies_windic.iloc[0]
Out[147]:
movie_id 1
title Toy Story (1995)
genres Animation|Children's|Comedy
Genre_Animation 1
Genre_Children's 1
Genre_Comedy 1
Genre_Adventure 0
Genre_Fantasy 0
Genre_Romance 0
Genre_Drama 0
Genre_Action 0
Genre_Crime 0
Genre_Thriller 0
Genre_Horror 0
Genre_Sci-Fi 0
Genre_Documentary 0
Genre_War 0
Genre_Musical 0
Genre_Mystery 0
Genre_Film-Noir 0
Genre_Western 0
Name: 0, dtype: object
对于更大的数据,上面这种使用多成员构建指标变量并不是特别快速。更好的方法是写一个直接将数据写为Numpy数组的底层函数,然后将结果封装进DataFrame。
将get_dummies与cut等离散化函数结合使用是统计应用上一个有用方法:
In [148]: np.random.seed(12345)
In [149]: values = np.random.rand(10)
In [150]: values
Out[150]:
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
In [151]: bins = [0,0.2,0.4,0.6,0.8,1]
In [152]: pd.get_dummies(pd.cut(values,bins))
Out[152]:
(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
我们使用numpy.random.seed来设置随机种子以确保示例的确定性。我们将在后面的内容中再次讨论pandas.get_dummies。
7.3 字符串操作
7.3.1 字符串对象方法
在很多字符串处理和脚本应用中,内建的字符串方法是够用的。例如,一个逗号分隔的字符串可以使用split方法拆分成多块:
In [153]: val = "a,b, guido"
In [154]: val.split(',')
Out[154]: ['a', 'b', ' guido']
split常和strip一起使用,用于清除空格(包括换行):
In [155]: pieces = [x.strip() for x in val.split(',')]
In [156]: pieces
Out[156]: ['a', 'b', 'guido']
这些子字符串可以使用加法与两个冒号分隔符连接在一起:
In [159]: first + '::' + second +'::'+third
Out[159]: 'a::b::guido'
但这并不是一个实用的通用方法。在字符串'::'的join方法中传入一个列表或元组是一种更快且更加pythonic的方法:
In [160]: '::'.join(pieces)
Out[160]: 'a::b::guido'
其他方法涉及定位子字符串。使用Python的in关键字是检测子字符串的最佳方法,尽管index和find也能实现同样的功能:
In [161]: 'guido' in val
Out[161]: True
In [162]: val.index(',')
Out[162]: 1
In [163]: val.find(':')
Out[163]: -1
请注意find和index 的区别在于index在字符串没有找到时会抛出一个异常(而find是返回-1):
In [164]: val.index(':')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-164-2c016e7367ac> in <module>
----> 1 val.index(':')
ValueError: substring not found
相关地,count返回的是某个特定的子字符串在字符串出现的次数:
In [165]: val.count(',')
Out[165]: 2
replace 将用一种模式替代另一种模式。它通常也用于传入空字符串来删除某个模式。
In [166]: val.replace(',','::') # ','占位符,在什么位置上插入::
Out[166]: 'a::b:: guido'
In [167]: val.replace(',','')
Out[167]: 'ab guido'
7.3.2 正则表达式
正则表达式提供了一种在文本中灵活查找或匹配(通常更复杂的)字符串模式的方法。单个表达式通常被称为regex,是根据正则表达式语言形成的字符串。Python内建的re模块是用于将正则表达式应用到字符串上的库。
re模块主要有三个主题:模块匹配、替代、拆分。当然,这三部分主题是相关联的。一个正则表达式描述了在文本中需要定位的一种模式,可以用于多种目标。让我们看一个简单的示例:假设我们想将含有多种空白字符(制表符、空格、换行符)的字符串拆分开。描述一个或多个空白字符的正则表达式是\s+:
In [168]: import re
In [169]: text = "foo var\t baz \tqux"
In [170]: re.split('\s+',text)
Out[170]: ['foo', 'var', 'baz', 'qux']
当你调用re.split('\s+',text),正则表达式首先会被编译,然后正则表达式的split方法在传入文本上被调用。你可以使用re.compile自行编译,形成一个可复用的正则表达式对象:
In [171]: regex = re.compile('\s+')
In [172]: regex.split(text)
Out[172]: ['foo', 'var', 'baz', 'qux']
如果你想获得的是一个所有匹配正则表达式的模式的列表,你可以使用findall方法:
In [173]: regex.findall(text) # 就是把符合你正则表达式要求的内容摘出来了
Out[173]: [' ', '\t ', ' \t']
为了在正则表达式中避免转义符\的影响,可以使用原生字符串语法,比如r'c:\x'或用等价的'C:\\x'
如果你需要将相同的表达式应用到多个字符串上,推荐使用re.compile创建一个正则表达式对象,这样做有利于节约CPU周期:
match和search与findall相关性很大。findall返回的是字符串中所有的匹配项,而search返回的仅仅是第一个匹配项。match更为严格,它只在字符串的起始位置进行匹配。作为一个不重要的示例,我们考虑下一段文本以及一个可以识别大部分电子邮件地址的正则表达式:
In [174]: text = """Dave dave@google.com
...: Steve steve@gmail.com
...: Rob rob@gmail.com
...: Ryan ryan@yahoo.com
...: """
In [175]: pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
# re.IGNORECASE 使正则表达式不区分大小写
In [177]: regex = re.compile(pattern,flags=re.IGNORECASE)
在文本上使用findall会生成一个电子邮件地址的列表:
In [178]: regex.findall(text)
Out[178]: ['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']
search返回的是文本中第一个匹配到的电子邮件地址。对于前面提到的正则表达式,匹配对象只能告诉我们模式在字符串中起始和结束的位置:
In [179]: m = regex.search(text)
In [180]: m
Out[180]: <re.Match object; span=(5, 20), match='dave@google.com'>
In [181]: text[m.start():m.end()]
Out[181]: 'dave@google.com'
regex.match只在模式出现于字符串起始位置时进行匹配,如果没有匹配到,返回None:
In [182]: print(regex.match(text))
None
相关的,sub会返回一个新的字符串,原字符串中的模式会被一个新的字符串替代:
In [183]: print(regex.sub('demaxiya',text))
Dave demaxiya
Steve demaxiya
Rob demaxiya
Ryan demaxiya
假设你想查找电子邮件地址,并将每个地址分成三个部分:用户名、域名和域名后缀。要实现这一点,可以用括号将模式包起来:
In [184]: pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
In [186]: regex = re.compile(pattern,flags=re.IGNORECASE)
由这个修改后的正则表达式产生的匹配对象的groups方法,返回的是模式组件的元组:
In [187]: m = regex.match('wesm@bright.net')
In [188]: m.groups()
Out[188]: ('wesm', 'bright', 'net')
当模式可以拆分时,findall返回的是包含元组的列表:
In [189]: regex.findall(text)
Out[189]:
[('dave', 'google', 'com'),
('steve', 'gmail', 'com'),
('rob', 'gmail', 'com'),
('ryan', 'yahoo', 'com')]
sub也可以使用特殊符号,如\1和\2,访问每个匹配对象中的分组。符号\1代表的是第一个匹配分组,\2代表的是第二个匹配分组,以此类推:
In [191]: print(regex.sub(r'Username:\1,Domain:\2,Suffix:\3',text))
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
7.3.3 pandas中的向量化字符串函数
清理杂乱的数据集用于分析通常需要大量的字符串处理和正则表。包含字符串的列有时会含有缺失数据,使事情变得复杂:
In [193]: data = {'Dave':'dave@google.com','Steve':'steve@gamil.com','Rob':'rob@gmail.com','wes':np.nan}
In [194]: data = pd.Series(data)
In [195]: data
Out[195]:
Dave dave@google.com
Steve steve@gamil.com
Rob rob@gmail.com
wes NaN
dtype: object
In [196]: data.isnull()
Out[196]:
Dave False
Steve False
Rob False
wes True
dtype: bool
你可以使用data.map将字符串和有效的正则表达式方法(以lambda或其他函数的方式传递)应用到每个值上,但是NA(null)值上回失败。为了解决这个问题,Series有面向数组的方法用于跳过NA值的字符串操作。这些方法通过Series的str属性进行调用,例如,我们可以通过str.contains来检查每个电子邮件地址是否含有‘gmail’:
In [203]: data = {'Dave':'dave@gmail.com','Steve':'steve@gamil.com','Rob':'rob@gmail.com','wes':np.nan}
In [204]: data = pd.Series(data)
In [205]: data.str.contains('gmail')
Out[205]:
Dave True
Steve False
Rob True
wes NaN
dtype: object
正则表达式也可以结合任意的re模块选项使用,例如IGNORECASE:
In [206]: pattern
Out[206]: '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
In [207]: data.str.findall(pattern,flags=re.IGNORECASE)
Out[207]:
Dave [(dave, gmail, com)]
Steve [(steve, gamil, com)]
Rob [(rob, gmail, com)]
wes NaN
dtype: object
有多种方法可以进行向量化的元素检索。可以使用str.get或在str属性内部检索:
In [208]: matches = data.str.match(pattern,flags=re.IGNORECASE)
In [209]: matches
Out[209]:
Dave True
Steve True
Rob True
wes NaN
dtype: object
要访问嵌入式列表中的元素,我们可以将索引传递给这些函数中的任意一个:
matches.str.get(1)
matches.str.get(0)
这里跟着书里写会报错:AttributeError:Can only use .str accessor with string values!
我面向百度编程了一下,发现解决方案是:
In [216]: x = data.str.findall(pattern,flags=re.IGNORECASE)
In [217]: x.str.get(1)
Out[217]:
Dave NaN
Steve NaN
Rob NaN
wes NaN
dtype: float64
In [218]: x.str.get(0)
Out[218]:
Dave (dave, gmail, com)
Steve (steve, gamil, com)
Rob (rob, gmail, com)
wes NaN
dtype: object
至于为什么这样可以,就不知道了。知其然,不知其所以然。
好在致敬大神的视频讲了这个部分,原文上的matches输出的布尔值,布尔值不是字符串,所以无法使用str的方式取值。
如果直接使用data的原数据取值,那也是可以的。
In [220]: data.str.get(1)
Out[220]:
Dave a
Steve t
Rob o
wes NaN
dtype: object
In [221]: data.str.get(0)
Out[221]:
Dave d
Steve s
Rob r
wes NaN
dtype: object
原文上这部分应该是有错吧。
你可以使用字符串切片的类似语法进行向量化切片:
In [219]: data.str[:5]
Out[219]:
Dave dave@
Steve steve
Rob rob@g
wes NaN
dtype: object
下图为部分向量化字符串方法列表:
本章结束。
同时,B站up主致敬大神也就录到这章结束了,虽然号称能填坑,但都坑一年多了,我估计是没希望了。
一下子感觉就像之前是两个人爬山,遇到困难了,有人能搀一下我。现在变成一个人了,面对沟沟坎坎只能自己靠百度和谷歌跨过。孤独感和恐慌感大幅度up,难受。
胭惜雨
2021年03月31日