本人以简书作者 SeanCheney 系列专题文章并结合原书为学习资源,记录个人笔记,仅作为知识记录及后期复习所用,原作者地址查看 简书 SeanCheney,如有错误,还望批评指教。——ZJ
原作者:SeanCheney | 《利用 Python 进行数据分析·第2版》第7章 数据清洗和准备 | 來源:简书
Github:wesm | Github:中文 BrambleXu|
简书:利用 Python 进行数据分析·第2版
环境: Python 3.6
在许多数据分析工作中,缺失数据是经常发生的。 pandas 的目标之一就是尽量轻松地处理缺失数据。例如, pandas 对象的所有描述性统计默认都不包括缺失数据。
缺失数据在 pandas 中呈现的方式有些不完美,但对于大多数用户可以保证功能正常。对于数值数据, pandas 使用浮点值 NaN(Not a Number)表示缺失数据。我们称其为哨兵值,可以方便的检测出来:
In [1]: import numpy as np
In [2]: import pandas as pd
In [3]: string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
In [4]: string_data
Out[4]:
0 aardvark
1 artichoke
2 NaN
3 avocado
dtype: object
In [5]: string_data.isnull()
Out[5]:
0 False
1 False
2 True
3 False
dtype: bool
Python 内置的 None 值在对象数组中也可以作为 NA:
In [6]: type(string_data)
Out[6]: pandas.core.series.Series
In [7]: string_data[0] = None
In [8]: string_data.isnull()
Out[8]:
0 True
1 False
2 True
3 False
dtype: bool
pandas 项目中还在不断优化内部细节以更好处理缺失数据,像用户API功能,例如 pandas .isnull,去除了许多恼人的细节。表7-1列出了一些关于缺失数据处理的函数。
pandas.isnull
或布尔索引的手工方法,但 dropna 可能会更实用一些。In [9]: from numpy import nan as NA
In [10]: data = pd.Series([1, NA, 3.5, NA, 7])
In [11]: data.dropna()
Out[11]:
0 1.0
2 3.5
4 7.0
dtype: float64
In [12]: data
Out[12]:
0 1.0
1 NaN
2 3.5
3 NaN
4 7.0
dtype: float64
这等价于:
In [14]: data[data.notnull()]
Out[14]:
0 1.0
2 3.5
4 7.0
dtype: float64
In [15]: data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA],
...: [NA, NA, NA], [NA, 6.5, 3.]], index=['one', 't
...: wo', 'three', 'four'], columns=['first', 'second', 'third'])
...:
...:
In [16]: data
Out[16]:
first second third
one 1.0 6.5 3.0
two 1.0 NaN NaN
three NaN NaN NaN
four NaN 6.5 3.0
In [17]: cleaned = data.dropna()
In [18]: cleaned
Out[18]:
first second third
one 1.0 6.5 3.0
传入how='all'
将只丢弃全为 NA 的那些行
In [19]: data.dropna(how='all')
Out[19]:
first second third
one 1.0 6.5 3.0
two 1.0 NaN NaN
four NaN 6.5 3.0
用这种方式丢弃列(全是 NA 的列),只需传入axis=1
即可:
In [20]: data['forth'] = NA
In [21]: data
Out[21]:
first second third forth
one 1.0 6.5 3.0 NaN
two 1.0 NaN NaN NaN
three NaN NaN NaN NaN
four NaN 6.5 3.0 NaN
In [22]: data.dropna(axis=1, how='all')
Out[22]:
first second third
one 1.0 6.5 3.0
two 1.0 NaN NaN
three NaN NaN NaN
four NaN 6.5 3.0
另一个滤除 DataFrame 行的问题涉及时间序列数据。假设你只想留下一部分观测数据,可以用 thresh 参数实现此目的:
In [23]: df = pd.DataFrame(np.random.randn(7,3))
In [24]: df.iloc[:4, 1] = NA
In [25]: df
Out[25]:
0 1 2
0 -0.052880 NaN 0.192669
1 0.440543 NaN -0.058121
2 0.297282 NaN -0.808425
3 -0.429874 NaN -0.965913
4 0.132290 0.251065 0.853049
5 1.190240 -1.118041 -0.075022
6 0.530970 0.033641 -0.473945
In [26]: df.iloc[:2, 2] = NA
In [27]: df
Out[27]:
0 1 2
0 -0.052880 NaN NaN
1 0.440543 NaN NaN
2 0.297282 NaN -0.808425
3 -0.429874 NaN -0.965913
4 0.132290 0.251065 0.853049
5 1.190240 -1.118041 -0.075022
6 0.530970 0.033641 -0.473945
In [28]: df.dropna()
Out[28]:
0 1 2
4 0.13229 0.251065 0.853049
5 1.19024 -1.118041 -0.075022
6 0.53097 0.033641 -0.473945
In [29]: df.dropna(thresh=2) # 索引 0 1 行 含有 NA 的去掉了
Out[29]:
0 1 2
2 0.297282 NaN -0.808425
3 -0.429874 NaN -0.965913
4 0.132290 0.251065 0.853049
5 1.190240 -1.118041 -0.075022
6 0.530970 0.033641 -0.473945
In [30]: df.fillna(0)
Out[30]:
0 1 2
0 -0.052880 0.000000 0.000000
1 0.440543 0.000000 0.000000
2 0.297282 0.000000 -0.808425
3 -0.429874 0.000000 -0.965913
4 0.132290 0.251065 0.853049
5 1.190240 -1.118041 -0.075022
6 0.530970 0.033641 -0.473945
若是通过一个字典调用 fillna ,就可以实现对不同的列填充不同的值:
In [31]: df.fillna({1: 0.5, 2: 0})
Out[31]:
0 1 2
0 -0.052880 0.500000 0.000000
1 0.440543 0.500000 0.000000
2 0.297282 0.500000 -0.808425
3 -0.429874 0.500000 -0.965913
4 0.132290 0.251065 0.853049
5 1.190240 -1.118041 -0.075022
6 0.530970 0.033641 -0.473945
fillna 默认会返回新对象,但也可以对现有对象进行就地修改:
In [32]: _ = df.fillna(0, inplace= True)
In [33]: df
Out[33]:
0 1 2
0 -0.052880 0.000000 0.000000
1 0.440543 0.000000 0.000000
2 0.297282 0.000000 -0.808425
3 -0.429874 0.000000 -0.965913
4 0.132290 0.251065 0.853049
5 1.190240 -1.118041 -0.075022
6 0.530970 0.033641 -0.473945
对 reindexing 有效的那些插值方法也可用于 fillna :
In [34]: df = pd.DataFrame(np.random.randn(6,3))
In [35]: df.iloc[2:, 1] = NA
In [36]: df
Out[36]:
0 1 2
0 -0.775110 0.000504 1.445061
1 -0.469458 0.727227 -0.166666
2 0.019312 NaN -0.915137
3 -1.477259 NaN 0.423064
4 1.620944 NaN 1.165360
5 0.388970 NaN 0.230785
In [37]: df.iloc[4:, 2] = NA
In [38]: df
Out[38]:
0 1 2
0 -0.775110 0.000504 1.445061
1 -0.469458 0.727227 -0.166666
2 0.019312 NaN -0.915137
3 -1.477259 NaN 0.423064
4 1.620944 NaN NaN
5 0.388970 NaN NaN
In [39]: df.fillna(method='ffill')
Out[39]:
0 1 2
0 -0.775110 0.000504 1.445061
1 -0.469458 0.727227 -0.166666
2 0.019312 0.727227 -0.915137
3 -1.477259 0.727227 0.423064
4 1.620944 0.727227 0.423064
5 0.388970 0.727227 0.423064
In [40]: df.fillna(method='ffill', limit=2)
Out[40]:
0 1 2
0 -0.775110 0.000504 1.445061
1 -0.469458 0.727227 -0.166666
2 0.019312 0.727227 -0.915137
3 -1.477259 0.727227 0.423064
4 1.620944 NaN 0.423064
5 0.388970 NaN 0.423064
只要有些创新,你就可以利用 fillna 实现许多别的功能。比如说,你可以传入 Series 的平均值或中位数:
In [41]: data = pd.Series([1., NA, 3.5, NA, 7])
In [42]: data.fillna(data.mean())
Out[42]:
0 1.000000
1 3.833333
2 3.500000
3 3.833333
4 7.000000
dtype: float64
表7-2列出了 fillna 的参考。
本章到目前为止介绍的都是数据的重排。另一类重要操作则是过滤、清理以及其他的转换工作。
DataFrame 中出现重复行有多种原因。下面就是一个例子:
In [59]: data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
...: ....: 'k2': [1, 1, 2, 3, 3, 4, 4]})
...:
In [60]: data
Out[60]:
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 ,表示各行是否是重复行(前面出现过的行):
In [62]: data.duplicated()
Out[62]:
0 False
1 False
2 False
3 False
4 False
5 False
6 True
dtype: bool
还有一个与此相关的drop_duplicates
方法,它会返回一个 DataFrame ,重复的数组会标为 False(哪里标 False 了 ):
In [63]: data.drop_duplicates()
Out[63]:
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
In [64]: type(data.drop_duplicates())
Out[64]: pandas.core.frame.DataFrame
# 5 two 4
# 6 two 4
# 原来这两行 是相同的,所以重复 去掉最后一个
这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断。假设我们还有一列值,且只希望根据 k1 列过滤重复项:
In [65]: data['v1'] = range(7)
In [66]: data
Out[66]:
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
In [67]: data.drop_duplicates(['k1'])
Out[67]:
k1 k2 v1
0 one 1 0
1 two 1 1
# 只判断 k1 中重复的,不判断 k2, v2
duplicated
和 drop_duplicates
默认保留的是第一个出现的值组合。传入keep='last'
则保留最后一个:
In [68]: data.drop_duplicates(['k1', 'k2'], keep='last')
Out[68]:
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 two 4 5
# 6 two 4 6
# 这两行 因为只判断 'k1', 'k2' , keep='last' 保留最后一个,所以保留 索引 6 去掉索引 5
对于许多数据集,你可能希望根据数组、 Series 或 DataFrame 列中的值来实现转换工作。我们来看看下面这组有关肉类的数据:
In [70]: data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon',
...: ....: 'Pastrami', 'corned beef', 'Baco
...: n',
...: ....: 'pastrami', 'honey ham', 'nova l
...: ox'],
...: ....: 'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]}
...: )
...:
In [71]: data
Out[71]:
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 [72]: meat_to_animal = {
...: 'bacon': 'pig',
...: 'pulled pork': 'pig',
...: 'pastrami': 'cow',
...: 'corned beef': 'cow',
...: 'honey ham': 'pig',
...: 'nova lox': 'salmon'
...: }
str.lower
方法,将各个值转换为小写:In [73]: lowercased = data['food'].str.lower()
In [74]: lowercased
Out[74]:
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
# data 中添加 animal 一列,这列中的数据,是 lowercased (food 列 转化为小写后的)作为 key # 从 meat_to_animal 这个字典中取出对应的 value
In [75]: data['animal'] = lowercased.map(meat_to_animal)
In [76]: data
Out[76]:
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 [77]: data['food'].map(lambda x: meat_to_animal[x.lower()])
Out[77]:
0 pig
1 pig
2 pig
3 cow
4 cow
5 pig
6 cow
7 pig
8 salmon
Name: food, dtype: object
使用 map 是一种实现元素级转换以及其他数据清理工作的便捷方式。
In [78]: data = pd.Series([1., -999., 2., -999., -1000., 3.])
In [79]: data
Out[79]:
0 1.0
1 -999.0
2 2.0
3 -999.0
4 -1000.0
5 3.0
dtype: float64
-999 这个值可能是一个表示缺失数据的标记值。要将其替换为 pandas 能够理解的 NA 值,我们可以利用 replace 来产生一个新的 Series (除非传入inplace=True
):
In [84]: data.replace(-999.0, np.nan)
Out[84]:
0 1.0
1 NaN
2 2.0
3 NaN
4 -1000.0
5 3.0
dtype: float64
如果你希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值::
In [85]: data.replace([-999, -1000], NA)
Out[85]:
0 1.0
1 NaN
2 2.0
3 NaN
4 NaN
5 3.0
dtype: float64
要让每个值有不同的替换值,可以传递一个替换列表:
In [86]: data.replace([-999, -1000], [np.nan, 0])
Out[86]:
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
传入的参数也可以是字典:
In [87]: data.replace({-999: np.nan, -1000: 0})
Out[87]:
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
data.replace
方法与 data.str.replace
不同,后者做的是字符串的元素级替换。我们会在后面学习 Series 的字符串方法。跟 Series 中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新的不同标签的对象。轴还可以被就地修改,而无需新建一个数据结构。接下来看看下面这个简单的例子:
In [88]: data = pd.DataFrame(np.arange(12).reshape((3,4)),
index=['Ohio', 'Colorado', 'New York'],
...: columns=['one', 'two', 'three', 'four'])
...:
In [89]: data
Out[89]:
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
跟 Series 一样,轴索引也有一个 map 方法:
In [90]: transform = lambda x: x[:].upper()
In [91]: data.index.map(transform)
Out[91]: Index(['OHIO', 'COLORADO', 'NEW YORK'], dtype='object')
你可以将其赋值给 index,这样就可以对 DataFrame 进行就地修改:
In [92]: data.index
Out[92]: Index(['Ohio', 'Colorado', 'New York'], dtype='object')
In [93]: data.index = data.index.map(transform)
In [94]: data
Out[94]:
one two three four
OHIO 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是 rename:
In [95]: data.rename(index=str.title, columns=str.upper)
Out[95]:
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
特别说明一下,rename 可以结合字典型对象实现对部分轴标签的更新:
In [96]: data.rename(index={'OHIO': 'INDIANA'},
...: ....: columns={'three': 'peekaboo'})
...:
Out[96]:
one two peekaboo four
INDIANA 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
rename 可以实现复制 DataFrame 并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入inplace=True
即可:
In [102]: data.rename(index={'OHIO': 'INDIANA'}, inplace=True)
In [103]: data
Out[103]:
one two three four
INDIANA 0 1 2 3
COLORADO 4 5 6 7
NEW YORK 8 9 10 11
为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:
In [98]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,你需要使用 pandas 的cut函数:
In [99]: bins = [18, 25, 35, 60, 100]
In [100]: cats = pd.cut(ages, bins)
In [101]: cats
Out[101]:
[(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 [104]: cats.codes
Out[104]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
In [105]: cats.categories
Out[105]:
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
closed='right',
dtype='interval[int64]')
In [106]: pd.value_counts(cats)
Out[106]:
(18, 25] 5
(35, 60] 3
(25, 35] 3
(60, 100] 1
dtype: int64
pd.value_counts(cats)
是 pandas.cut
结果的面元计数。
跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False
进行修改:
In [107]: pd.cut(ages, [18, 26, 36, 61, 100], right=False)
Out[107]:
[[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 [108]: group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
In [109]: pd.cut(ages, bins, labels=group_names)
Out[109]:
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, Mid
dleAged, YoungAdult]
Length: 12
Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。下面这个例子中,我们将一些均匀分布的数据分成四组:
In [110]: data = np.random.rand(20)
In [111]: pd.cut(data, 4, precision=2)
Out[111]:
[(0.73, 0.96], (0.028, 0.26], (0.26, 0.49], (0.49, 0.73], (0.73, 0.96], ..., (0.2
6, 0.49], (0.26, 0.49], (0.26, 0.49], (0.49, 0.73], (0.73, 0.96]]
Length: 20
Categories (4, interval[float64]): [(0.028, 0.26] < (0.26, 0.49] < (0.49, 0.73] <
(0.73, 0.96]]
选项precision=2
,限定小数只有两位。
In [112]: data = np.random.randn(1000)
In [113]: cats = pd.qcut(data, 4)
In [114]: cats
Out[114]:
[(0.625, 2.842], (0.625, 2.842], (-3.054, -0.78], (0.625, 2.842], (-3.054, -0.78]
, ..., (-3.054, -0.78], (-3.054, -0.78], (-3.054, -0.78], (0.625, 2.842], (-3.054
, -0.78]]
Length: 1000
Categories (4, interval[float64]): [(-3.054, -0.78] < (-0.78, -0.0355] < (-0.0355
, 0.625] <
(0.625, 2.842]]
In [115]: pd.value_counts(cats)
Out[115]:
(0.625, 2.842] 250
(-0.0355, 0.625] 250
(-0.78, -0.0355] 250
(-3.054, -0.78] 250
dtype: int64
与 cut 类似,你也可以传递自定义的分位数(0到1之间的数值,包含端点):
In [117]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
Out[117]:
[(-0.0355, 1.241], (1.241, 2.842], (-1.359, -0.0355], (1.241, 2.842], (-3.054, -1
.359], ..., (-3.054, -1.359], (-3.054, -1.359], (-1.359, -0.0355], (-0.0355, 1.24
1], (-1.359, -0.0355]]
Length: 1000
Categories (4, interval[float64]): [(-3.054, -1.359] < (-1.359, -0.0355] < (-0.03
55, 1.241] <
(1.241, 2.842]
过滤或变换异常值(outlier)在很大程度上就是运用数组运算。来看一个含有正态分布数据的 DataFrame:
In [118]: data = pd.DataFrame(np.random.randn(1000, 4))
In [119]: data.describe()
Out[119]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.024284 0.000331 -0.033790 -0.005900
std 1.002445 1.030087 1.010808 1.027110
min -3.253857 -3.951871 -2.916422 -3.060088
25% -0.705493 -0.681622 -0.707278 -0.682978
50% -0.006853 0.011623 -0.016127 0.009688
75% 0.643390 0.688746 0.671249 0.653106
max 3.096767 3.574762 3.291177 3.513211
假设你想要找出某列中绝对值大小超过 3 的值:
In [121]: col = data[2]
In [122]: col[np.abs(col) > 3]
Out[122]:
583 3.291177
Name: 2, dtype: float64
要选出全部含有“超过 3 或 -3的值”的行,你可以在布尔型 DataFrame 中使用any方法:
In [123]: data[(np.abs(data) > 3).any(1)]
Out[123]:
0 1 2 3
583 -0.328060 0.512835 3.291177 0.649597
653 -0.388316 3.574762 -0.379249 -0.429550
706 -1.049828 0.397178 -0.011549 -3.060088
727 -3.082909 -0.310682 0.375530 0.242903
802 -3.253857 0.940367 0.040317 2.556581
856 0.779090 -3.239075 0.596105 -0.218275
901 -0.116137 -0.810662 0.290593 3.513211
903 0.125236 -3.951871 -0.210753 -1.271565
911 3.096767 1.309234 2.114700 -1.847553
根据这些条件,就可以对值进行设置。下面的代码可以将值限制在区间 -3 到 3 以内:
In [127]: data[(np.abs(data) > 3)] = np.sign(data) * 3
In [128]: data.describe()
Out[128]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.024044 0.000947 -0.034082 -0.006353
std 1.001111 1.024294 1.009890 1.025300
min -3.000000 -3.000000 -2.916422 -3.000000
25% -0.705493 -0.681622 -0.707278 -0.682978
50% -0.006853 0.011623 -0.016127 0.009688
75% 0.643390 0.688746 0.671249 0.653106
max 3.000000 3.000000 3.000000 3.000000
根据数据的值是正还是负,np.sign(data)
可以生成 1 和 -1:
In [129]: np.sign(data).head()
Out[129]:
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
利用numpy.random.permutation
函数可以轻松实现对 Series 或 DataFrame 的列的排列工作(permuting,随机重排序)。通过需要排列的轴的长度调用permutation
,可产生一个表示新顺序的整数数组:
In [130]: df = pd.DataFrame(np.arange(5*4).reshape((5, 4)))
In [131]: df
Out[131]:
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 [132]: sampler = np.random.permutation(5)
In [133]: sampler
Out[133]: array([2, 0, 4, 1, 3])
In [134]: sampler1 = np.random.permutation(5)
In [135]: sampler1
Out[135]: array([0, 1, 3, 2, 4])
然后就可以在基于iloc
的索引操作或take
函数中使用该数组了:
In [136]: df
Out[136]:
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 [138]: df.take(sampler)
Out[138]:
0 1 2 3
2 8 9 10 11
0 0 1 2 3
4 16 17 18 19
1 4 5 6 7
3 12 13 14 15
In [139]: df.take(sampler1)
Out[139]:
0 1 2 3
0 0 1 2 3
1 4 5 6 7
3 12 13 14 15
2 8 9 10 11
如果不想用替换的方式选取随机子集,可以在 Series 和 DataFrame 上使用 sample 方法:
In [140]: df.sample(n=3)
Out[140]:
0 1 2 3
1 4 5 6 7
0 0 1 2 3
2 8 9 10 11
要通过替换的方式产生样本(允许重复选择),可以传递 replace =True
到sample:
In [141]: choices = pd.Series([5, 7, -1, 6, 4])
In [142]: draws = choices.sample(n=10, replace=True)
In [143]: draws
Out[143]:
2 -1
1 7
0 5
0 5
4 4
3 6
0 5
2 -1
0 5
4 4
dtype: int64
另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。
如果 DataFrame 的某一列中含有 k 个不同的值,则可以派生出一个 k 列矩阵或 DataFrame (其值全为 1 和 0)。 pandas 有一个 get_dummies
函数可以实现该功能(其实自己动手做一个也不难)。使用之前的一个 DataFrame 例子:
In [144]: df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
...: .....: 'data1': range(6)})
...:
In [145]: pd.get_dummies(df['key'])
Out[145]:
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
In [146]: df
Out[146]:
data1 key
0 0 b
1 1 b
2 2 a
3 3 c
4 4 a
5 5 b
有时候,你可能想给指标 DataFrame 的列加上一个前缀,以便能够跟其他数据进行合并。 get_dummies
的prefix
参数可以实现该功能:
In [147]: dummies = pd.get_dummies(df['key'], prefix='key')
In [148]: df_with_dummy = df[['data1']].join(dummies)
In [149]: df_with_dummy
Out[149]:
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
In [151]: df
Out[151]:
data1 key
0 0 b
1 1 b
2 2 a
3 3 c
4 4 a
5 5 b
In [152]: df_with_dummy1 = df[['key']].join(dummies)
In [153]: df_with_dummy1
Out[153]:
key key_a key_b key_c
0 b 0 1 0
1 b 0 1 0
2 a 1 0 0
3 c 0 0 1
4 a 1 0 0
5 b 0 1 0
In [154]: df_with_dummy2 = df[['data1']].join(df[['key']].join(dummies))
In [155]: df_with_dummy2
Out[155]:
data1 key key_a key_b key_c
0 0 b 0 1 0
1 1 b 0 1 0
2 2 a 1 0 0
3 3 c 0 0 1
4 4 a 1 0 0
5 5 b 0 1 0
如果 DataFrame 中的某行同属于多个分类,则事情就会有点复杂。看一下 MovieLens 1M 数据集,14章会更深入地研究它:
In [156]: mnames = ['movie_id', 'title', 'genres']
...:
In [158]: movies[:10]
Out[158]:
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
要为每个 genre 添加指标变量就需要做一些数据规整操作。首先,我们从数据集中抽取出不同的genre 值:
In [159]: all_genres = []
In [160]: for x in movies.genres:
...: all_genres.extend(x.split('|')) # x.split('|') 对 Action|Adventure|Thriller 切割分割
...:
In [161]: genres = pd.unique(all_genres)
In [162]: genres
Out[162]:
array(['Animation', "Children's", 'Comedy', 'Adventure', 'Fantasy',
'Romance', 'Drama', 'Action', 'Crime', 'Thriller', 'Horror',
'Sci-Fi', 'Documentary', 'War', 'Musical', 'Mystery', 'Film-Noir',
'Western'], dtype=object)
构建指标 DataFrame 的方法之一是从一个全零 DataFrame 开始:
In [164]: zero_matrix = np.zeros((len(movies), len(genres)))
In [165]: dummies = pd.DataFrame(zero_matrix, columns=genres)
In [166]: zero_matrix
Out[166]:
array([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
In [167]: dummies
Out[167]:
Animation Children's Comedy Adventure Fantasy Romance Drama \
0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 0.0 0.0 0.0 0.0 0.0 0.0 0.0
5 0.0 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ...
3873 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3874 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3875 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3876 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3877 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3878 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3879 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3880 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3881 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3882 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Action Crime Thriller Horror Sci-Fi Documentary War Musical \
0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
5 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ...
3874 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3875 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3876 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3877 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3878 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3879 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3880 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3881 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3882 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Mystery Film-Noir Western
0 0.0 0.0 0.0
1 0.0 0.0 0.0
2 0.0 0.0 0.0
3 0.0 0.0 0.0
4 0.0 0.0 0.0
5 0.0 0.0 0.0
6 0.0 0.0 0.0
7 0.0 0.0 0.0
8 0.0 0.0 0.0
9 0.0 0.0 0.0
... ... ... ...
3873 0.0 0.0 0.0
3874 0.0 0.0 0.0
3875 0.0 0.0 0.0
3876 0.0 0.0 0.0
3877 0.0 0.0 0.0
3878 0.0 0.0 0.0
3879 0.0 0.0 0.0
3880 0.0 0.0 0.0
3881 0.0 0.0 0.0
3882 0.0 0.0 0.0
[3883 rows x 18 columns]
现在,迭代每一部电影,并将 dummies 各行的条目设为1。要这么做,我们使用dummies.columns
来计算每个类型的列索引:
In [168]: gen = movies.genres[0]
In [169]: gen.split('|')
Out[169]: ['Animation', "Children's", 'Comedy']
In [170]: dummies.columns.get_indexer(gen.split('|'))
Out[170]: array([0, 1, 2], dtype=int64)
然后,根据索引,使用.iloc
设定值:
In [171]: for i, gen in enumerate(movies.genres):
...: indices = dummies.columns.get_indexer(gen.split('|'))
...: dummies.iloc[i, indices] = 1
然后,和以前一样,再将其与movies
合并起来:
In [173]: movies_windic = movies.join(dummies.add_prefix('Genre_'))
In [174]: movies_windic.iloc[0]
Out[174]:
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 [179]: np.random.seed(12345)
In [180]: values = np.random.rand(10)
In [181]: values
Out[181]:
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
In [182]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
In [183]: pd.get_dummies(pd.cut(values, bins))
Out[183]:
(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
。
对于许多字符串处理和脚本应用,内置的字符串方法已经能够满足要求了。例如,以逗号分隔的字符串可以用split
拆分成数段:
In [184]: val = 'a,b, guido'
In [185]: val.split(',')
Out[185]: ['a', 'b', ' guido']
split
常常与 strip
一起使用,以去除空白符(包括换行符):
n [187]: pieces = [x.strip() for x in val.split(',')]
In [188]: pieces
Out[188]: ['a', 'b', 'guido']
利用加法,可以将这些子字符串以双冒号分隔符的形式连接起来:
In [189]: first, second, third = pieces
In [190]: first + '::' + second + '::' + third
Out[190]: 'a::b::guido'
但这种方式并不是很实用。一种更快更符合 Python 风格的方式是,向字符串”::”的join方法传入一个列表或元组:
In [191]: '::'.join(pieces)
Out[191]: 'a::b::guido'
其它方法关注的是子串定位。检测子串的最佳方式是利用 Python 的 in 关键字,还可以使用index
和find
:
In [192]: 'guido' in val
Out[192]: True
In [193]: val.index(',')
Out[193]: 1
In [194]: val.find(':')
Out[194]: -1
注意 find 和 index 的区别:如果找不到字符串,index 将会引发一个异常(而不是返回-1):
In [195]: val.index(':')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
input -195-2c016e7367ac> in <module>()
----> 1 val.index(':')
ValueError: substring not found
与此相关, count 可以返回指定子串的出现次数:
In [196]: val.count(',')
Out[196]: 2
replace 用于将指定模式替换为另一个模式。通过传入空字符串,它也常常用于删除模式:
In [197]: val.replace(',', '::')
Out[197]: 'a::b:: guido'
In [198]: val.replace(',','')
Out[198]: 'ab guido'
表7-3 列出了 Python 内置的字符串方法。
这些运算大部分都能使用正则表达式实现(马上就会看到)。
casefold 将字符转换为小写,并将任何特定区域的变量字符组合转换成一个通用的可比较形式。
正则表达式提供了一种灵活的在文本中搜索或匹配(通常比前者复杂)字符串模式的方式。正则表达式,常称作 regex ,是根据正则表达式语言编写的字符串。 Python 内置的 re 模块负责对字符串应用正则表达式。我将通过一些例子说明其使用方法。
笔记:正则表达式的编写技巧可以自成一章,超出了本书的范围。从网上和其它书可以找到许多非常不错的教程和参考资料。
re 模块的函数可以分为三个大类:模式匹配、替换以及拆分。当然,它们之间是相辅相成的。一个 regex 描述了需要在文本中定位的一个模式,它可以用于许多目的。
\s+
:In [1]: import re
In [2]: text = "foo bar\t baz \t qux"
In [3]: re.split('\s+', text)
Out[3]: ['foo', 'bar', 'baz', 'qux']
调用 re.split('\s+',text)
时,正则表达式会先被编译,然后再在text
上调用其split
方法。你可以用re.compile
自己编译 regex 以得到一个可重用的 regex 对象:
In [4]: regex = re.compile('\s+')
In [5]: regex.split(text)
Out[5]: ['foo', 'bar', 'baz', 'qux']
如果只希望得到匹配 regex 的所有模式,则可以使用findall
方法:
In [6]: regex.findall(text)
Out[6]: [' ', '\t ', ' \t']
笔记:如果想避免正则表达式中不需要的转义(\),则可以使用原始字符串字面量如r’C:\x’(也可以编写其等价式’C:\x’)。
如果打算对许多字符串应用同一条正则表达式,强烈建议通过 re.compile
创建 regex
对象。这样将可以节省大量的 CPU 时间。
match 和 search 跟 findall 功能类似。
来看一个小例子,假设我们有一段文本以及一条能够识别大部分电子邮件地址的正则表达式:
In [7]: text = """Dave [email protected]
...: Steve [email protected]
...: Rob [email protected]
...: Ryan [email protected]
...: """
In [8]: pattern = r'[A-Z0-9.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
In [9]: # re.IGNORECASE makes the regex case-insensitive
In [10]: regex = re.compile(pattern, flags=re.IGNORECASE)
对 text 使用 findall 将得到一组电子邮件地址:
In [11]: regex.findall(text)
Out[11]: ['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']
search 返回的是文本中第一个电子邮件地址(以特殊的匹配项对象形式返回)。对于上面那个 regex ,匹配项对象只能告诉我们模式在原字符串中的起始和结束位置:
In [12]: m = regex.search(text)
In [13]: m
Out[13]: <_sre.SRE_Match object; span=(5, 20), match='[email protected]'>
In [14]: text[m.start():m.end()]
Out[14]: '[email protected]'
regex .match
则将返回 None ,因为它只匹配出现在字符串开头的模式:
In [15]: print(regex.match(text))
None
相关的,sub 方法可以将匹配到的模式替换为指定字符串,并返回所得到的新字符串:
In [16]: print(regex.sub('REDACTED', text))
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED
假设你不仅想要找出电子邮件地址,还想将各个地址分成3个部分:用户名、域名以及域后缀。要实现此功能,只需将待分段的模式的各部分用圆括号包起来即可:
In [23]: pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
In [24]: regex = re.compile(pattern, flags=re.IGNORECASE)
由这种修改过的正则表达式所产生的匹配项对象,可以通过其 groups 方法返回一个由模式各段组成的元组:
In [25]: m = regex.match('[email protected]')
In [26]: m.groups()
Out[26]: ('wesm', 'bright', 'net')
对于带有分组功能的模式,findall 会返回一个元组列表:
In [6]:
In [27]: regex.findall(text)
Out[27]:
[('dave', 'google', 'com'),
('steve', 'gmail', 'com'),
('rob', 'gmail', 'com'),
('ryan', 'yahoo', 'com')]
sub还能通过诸如\1、\2之类的特殊符号访问各匹配项中的分组。符号\1对应第一个匹配的组,\2对应第二个匹配的组,以此类推:
In [28]: 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
Python 中还有许多的正则表达式,但大部分都超出了本书的范围。表7-4是一个简要概括。
清理待分析的散乱数据时,常常需要做一些字符串规整化工作。更为复杂的情况是,含有字符串的列有时还含有缺失数据:
In [33]: import numpy as np
In [34]: import pandas as pd
In [35]: data = {'Dave': '[email protected]', 'Steve': '[email protected]',
...: .....: 'Rob': '[email protected]', 'Wes': np.nan}
...:
In [36]:
In [36]: data = pd.Series(data)
In [37]: data
Out[37]:
Dave [email protected]
Rob [email protected]
Steve [email protected]
Wes NaN
dtype: object
In [38]: data.isnull()
Out[38]:
Dave False
Rob False
Steve False
Wes True
dtype: bool
通过data.map,所有字符串和正则表达式方法都能被应用于(传入lambda表达式或其他函数)各个值,但是如果存在 NA (null)就会报错。为了解决这个问题, Series 有一些能够跳过 NA 值的面向数组方法,进行字符串操作。通过 Series 的 str 属性即可访问这些方法。例如,我们可以通过 str .contains检查各个电子邮件地址是否含有”gmail”:
In [40]: data.str.contains('gmail')
Out[40]:
Dave False
Rob True
Steve True
Wes NaN
dtype: object
也可以使用正则表达式,还可以加上任意 re 选项(如 IGNORECASE):
In [41]: pattern
Out[41]: '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
In [42]: data.str.findall(pattern, flags=re.IGNORECASE)
Out[42]:
Dave [(dave, google, com)]
Rob [(rob, gmail, com)]
Steve [(steve, gmail, com)]
Wes NaN
dtype: object
有两个办法可以实现矢量化的元素获取操作:要么使用 str .get,要么在 str 属性上使用索引:
In [43]: matches = data.str.match(pattern, flags=re.IGNORECASE)
In [44]: matches
Out[44]:
Dave True
Rob True
Steve True
Wes NaN
dtype: object
要访问嵌入列表中的元素,我们可以传递索引到这两个函数中:
In [45]: matches.str.get(1)
Out[45]:
Dave NaN
Rob NaN
Steve NaN
Wes NaN
dtype: float64
In [46]: matches.str[0]
Out[46]:
Dave NaN
Rob NaN
Steve NaN
Wes NaN
dtype: float64
你可以利用这种方法对字符串进行截取:
In [47]: data.str[:5]
Out[47]:
Dave dave@
Rob rob@g
Steve steve
Wes NaN
dtype: object
表7-5介绍了更多的 pandas 字符串方法。
高效的数据准备可以让你将更多的时间用于数据分析,花较少的时间用于准备工作,这样就可以极大地提高生产力。我们在本章中学习了许多工具,但覆盖并不全面。下一章,我们会学习 pandas 的聚合与分组。