目录
1. 处理缺失值
pandas对象的所有描述性统计信息默认情况下是排除缺失值的。
对于数值型数据,pandas使用浮点值NaN(Not a Number来表示缺失值)。称NaN为容易检测到的标识值:
>>> string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
>>> string_data
0 aardvark
1 artichoke
2 NaN
3 avocado
dtype: object
>>> string_data.isnull()
0 False
1 False
2 True
3 False
dtype: bool
在pandas中,我们采用了R语言中的编程惯例,将缺失值成为NA,意思是not available(不可用)。在统计学应用中,NA数据可以是不存在的数据或者是存在但不可观察的数据(例如在数据收集过程中出现了问题)。当清洗数据用于分析时,对缺失数据本身进行分析以确定数据收集问题或数据丢失导致的数据偏差通常很重要。
Python内建的None值在对象数组中也被当作NA处理:
>>> string_data[0] = None
>>> string_data.isnull()
0 True
1 False
2 True
3 False
dtype: bool
NA处理方法
1.1 过滤缺失值
有多种过滤缺失值的方法。虽然可以使用pandas.isnull和布尔值索引手动地过滤缺失值,但dropna在过滤缺失值时是非常有用的。在Series上使用dropna,它会返回Series中所有的非空数据及其索引值:
>>> from numpy import nan as NA
>>> data = pd.Series([1, NA, 3.5, NA, 7])
>>> data.dropna()
0 1.0
2 3.5
4 7.0
dtype: float64
# 另一种写法
>>> data[data.notnull()]
0 1.0
2 3.5
4 7.0
dtype: float64
当处理DataFrame对象时,事情会稍微更复杂一点。你可能想要删除全部为NA或包含有NA的行或列。dropna默认情况下会删除包含缺失值的行:
>>> data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA],
>>> [NA, NA, NA], [NA, 6.5, 3.]])
>>> cleaned = data.dropna()
>>> data
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
>>> cleaned
0 1 2
0 1.0 6.5 3.0
传入how='all'时,将删除所有值均为NA的行:
>>> data.dropna(how='all')
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0
如果要用同样的方式去删除列,传入参数axis=1:
>>> data[4] = NA
>>> data
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
>>> data.dropna(axis=1, how='all')
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参数来表示:
>>> df = pd.DataFrame(np.random.randn(7, 3))
>>> df.iloc[:4, 1] = NA
>>> df.iloc[:2, 2] = NA
>>> df
0 1 2
0 -0.204708 NaN NaN
1 -0.555730 NaN NaN
2 0.092908 NaN 0.769023
3 1.246435 NaN -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
>>> df.dropna()
0 1 2
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
>>> df.dropna(thresh=2)
0 1 2
2 0.092908 NaN 0.769023
3 1.246435 NaN -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
1.2 补全缺失值
有时可能需要以多种方式补全“漏洞”,而不是过滤缺失值(也可能丢弃其他数据)。大多数情况下,主要使用fillna方法来补全缺失值。调用fillna时,可以使用一个常数来替代缺失值:
>>> df.fillna(0)
0 1 2
0 -0.204708 0.000000 0.000000
1 -0.555730 0.000000 0.000000
2 0.092908 0.000000 0.769023
3 1.246435 0.000000 -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
在调用fillna时使用字典,可以为不同列设定不同的填充值:
>>> df.fillna({1: 0.5, 2: 0})
0 1 2
0 -0.204708 0.500000 0.000000
1 -0.555730 0.500000 0.000000
2 0.092908 0.500000 0.769023
3 1.246435 0.500000 -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
fillna返回的是一个新的对象,但也可以修改已经存在的对象:
>>> _ = df.fillna(0, inplace=True)
>>> df
0 1 2
0 -0.204708 0.000000 0.000000
1 -0.555730 0.000000 0.000000
2 0.092908 0.000000 0.769023
3 1.246435 0.000000 -1.296221
4 0.274992 0.228913 1.352917
5 0.886429 -2.001637 -0.371843
6 1.669025 -0.438570 -0.539741
用于重建索引的相同的插值方法也可以用于fillna:
>>> df = pd.DataFrame(np.random.randn(6, 3))
>>> df.iloc[2:, 1] = NA
>>> df.iloc[4:, 2] = NA
>>> df
0 1 2
0 0.476985 3.248944 -1.021228
1 -0.577087 0.124121 0.302614
2 0.523772 NaN 1.343810
3 -0.713544 NaN -2.370232
4 -1.860761 NaN NaN
5 -1.265934 NaN NaN
>>> df.fillna(method='ffill')
0 1 2
0 0.476985 3.248944 -1.021228
1 -0.577087 0.124121 0.302614
2 0.523772 0.124121 1.343810
3 -0.713544 0.124121 -2.370232
4 -1.860761 0.124121 -2.370232
5 -1.265934 0.124121 -2.370232
>>> df.fillna(method='ffill', limit=2)
0 1 2
0 0.476985 3.248944 -1.021228
1 -0.577087 0.124121 0.302614
2 0.523772 0.124121 1.343810
3 -0.713544 0.124121 -2.370232
4 -1.860761 NaN -2.370232
5 -1.265934 NaN -2.370232
使用fillna可以完成很多带有一点创造性的工作。例如,可以将Series的平均值或中位数用于填充缺失值:
>>> data = pd.Series([1., NA, 3.5, NA, 7])
>>> data.fillna(data.mean())
0 1.000000
1 3.833333
2 3.500000
3 3.833333
4 7.000000
dtype: float64
fillna函数参数
2. 数据转换
2.1 删除重复值
由于各种原因,DataFrame中会出现重复行。
>>> data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
>>> 'k2': [1, 1, 2, 3, 3, 4, 4]})
>>> data
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反映的是每一行是否存在重复(与之前出现过的行相同)情况:
>>> data.duplicated()
0 False
1 False
2 False
3 False
4 False
5 False
6 True
dtype: bool
drop_duplicates返回的是DataFrame,内容是duplicated返回数组中为False的部分:
>>> data.drop_duplicates()
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
这些方法默认都是对列进行操作。可以指定数据的任何子集来检测是否有重复。假设我们有一个额外的列,并想基于'k1'列去除重复值:
>>> data['v1'] = range(7)
>>> data.drop_duplicates(['k1'])
k1 k2 v1
0 one 1 0
1 two 1 1
duplicated和drop_duplicates默认都是保留第一个观测到的值。传入参数keep='last'将会返回最后一个:
>>> data.drop_duplicates(['k1', 'k2'], keep='last')
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
2.2 使用函数或映射进行数据转换
对于许多数据集,可能希望基于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]})
>>> data
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
假设想要添加一列用于表明每种食物的动物肉类型。先写下一个食物和肉类的映射:
meat_to_animal = {
'bacon': 'pig',
'pulled pork': 'pig',
'pastrami': 'cow',
'corned beef': 'cow',
'honey ham': 'pig',
'nova lox': 'salmon'
}
Series的map方法接收一个函数或一个包含映射关系的字典型对象,但是这里我们有一个小的问题在于一些肉类大写了,而另一部分肉类没有。因此,需要使用Series的str.lower方法将每个值都转换为小写:
>>> lowercased = data['food'].str.lower()
>>> lowercased
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.map(meat_to_animal)
>>> data
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
也可以传入一个能够完成所有工作的函数:
>>> data['food'].map(lambda x: meat_to_animal[x.lower()])
0 pig
1 pig
2 pig
3 cow
4 cow
5 pig
6 cow
7 pig
8 salmon
Name: food, dtype: object
2.3 替代值
使用fillna填充缺失值是通用值替换的特殊案例。map可以用来修改一个对象中的子集的值,但是replace提供了更为简单灵活的实现。
>>> data = pd.Series([1., -999., 2., -999., -1000., 3.])
>>> data
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):
>>> data.replace(-999, np.nan)
0 1.0
1 NaN
2 2.0
3 NaN
4 -1000.0
5 3.0
dtype: float64
一次替代多个值,可以传入一个列表和替代值
>>> data.replace([-999, -1000], np.nan)
0 1.0
1 NaN
2 2.0
3 NaN
4 NaN
5 3.0
dtype: float64
要将不同的值替换为不同的值,可以传入替代值的列表:
>>> data.replace([-999, -1000], [np.nan, 0])
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
参数也可以通过字典传递:
>>> data.replace({-999: np.nan, -1000: 0})
0 1.0
1 NaN
2 2.0
3 NaN
4 0.0
5 3.0
dtype: float64
data.replace方法与data.str.replace方法是不同的,data.str.replace是对字符串进行按元素替代的。
2.4 重命名轴索引
和Series中的值一样,可以通过函数或某种形式的映射对轴标签进行类似的转换,生成新的且带有不同标签的对象。也可以在不生成新的数据结构的情况下修改轴。
>>> data = pd.DataFrame(np.arange(12).reshape((3, 4)),
>>> index=['Ohio', 'Colorado', 'New York'],
>>> columns=['one', 'two', 'three', 'four'])
与Series类似,轴索引也有一个map方法:
>>> transform = lambda x: x[:4].upper()
>>> data.index.map(transform)
Index(['OHIO', 'COLO', 'NEW '], dtype='object')
赋值给index,修改DataFrame:
>>> data.index = data.index.map(transform)
>>> data
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
rename可以结合字典型对象使用,为轴标签的子集提供新的值:
>>> data.rename(index={'OHIO': 'INDIANA'},
>>> columns={'three': 'peekaboo'})
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
rename可以让你从手动复制DataFrame并为其分配索引和列属性的烦琐工作中解放出来。如果想要修改原有的数据集,传入inplace=True:
>>> data.rename(index={'OHIO': 'INDIANA'}, inplace=True)
>>> data
one two three four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
2.5 离散化和分箱
连续值经常需要离散化,或者分离成”箱子“进行分析。假设有某项研究中一组人群的数据,将他们进行分组,放入离散的年龄框中:
>>> ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
将这些年龄分为18~25、26~35、36~60以及61及以上等若干组。使用pandas中的cut:
>>> bins = [18, 25, 35, 60, 100]
>>> cats = pd.cut(ages, bins)
>>> cats
[(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(年龄)数据标签:
>>> cats.codes
array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
>>> cats.categories
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
closed='right',
dtype='interval[int64]')
>>> pd.value_counts(cats)
(18, 25] 5
(35, 60] 3
(25, 35] 3
(60, 100] 1
dtype: int64
pd.value_counts(cats)是对pandas.cut的结果中的箱数量的计数。
与区间的数学符号一致,小括号表示边是开放的,中括号表示它是封闭的(包括边)。可以通过传递right=False来改变哪一边是封闭的:
>>> pd.cut(ages, [18, 26, 36, 61, 100], right=False)
[[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选项传递一个列表或数组来传入自定义的箱名:
>>> group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
>>> pd.cut(ages, bins, labels=group_names)
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, Mid
dleAged, YoungAdult]
Length: 12
Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
如果你传给cut整数个的箱来代替显式的箱边,pandas将根据数据中的最小值和最大值计算出等长的箱。请考虑一些均匀分布的数据被切成四份的情况:
>>> data = np.random.rand(20)
>>> pd.cut(data, 4, precision=2)
[(0.34, 0.55], (0.34, 0.55], (0.76, 0.97], (0.76, 0.97], (0.34, 0.55], ..., (0.34
, 0.55], (0.34, 0.55], (0.55, 0.76], (0.34, 0.55], (0.12, 0.34]]
Length: 20
Categories (4, interval[float64]): [(0.12, 0.34] < (0.34, 0.55] < (0.55, 0.76] <
(0.76, 0.97]]
precision=2的选项将十进制精度限制在两位。
qcut是一个与分箱密切相关的函数,它基于样本分位数进行分箱。取决于数据的分布,使用cut通常不会使每个箱具有相同数据量的数据点。由于qcut使用样本的分位数,你可以通过qcut获得等长的箱:
>>> data = np.random.randn(1000) # 正态分布
>>> cats = pd.qcut(data, 4) # 切成四份
>>> cats
[(-0.0265, 0.62], (0.62, 3.928], (-0.68, -0.0265], (0.62, 3.928], (-0.0265, 0.62]
, ..., (-0.68, -0.0265], (-0.68, -0.0265], (-2.95, -0.68], (0.62, 3.928], (-0.68,
-0.0265]]
Length: 1000
Categories (4, interval[float64]): [(-2.95, -0.68] < (-0.68, -0.0265] < (-0.0265,
0.62] <
(0.62, 3.928]]
>>> pd.value_counts(cats)
(0.62, 3.928] 250
(-0.0265, 0.62] 250
(-0.68, -0.0265] 250
(-2.95, -0.68] 250
dtype: int64
与cut类似,你可以传入自定义的分位数(0和1之间的数据,包括边):
>>> pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
[(-0.0265, 1.286], (-0.0265, 1.286], (-1.187, -0.0265], (-0.0265, 1.286], (-0.026
5, 1.286], ..., (-1.187, -0.0265], (-1.187, -0.0265], (-2.95, -1.187], (-0.0265,
1.286], (-1.187, -0.0265]]
Length: 1000
Categories (4, interval[float64]): [(-2.95, -1.187] < (-1.187, -0.0265] < (-0.026
5, 1.286] <
(1.286, 3.928]]
2.6 检测和过滤异常值
过滤或转换异常值在很大程度上是应用数组操作的事情。考虑一个具有正态分布数据的DataFrame:
>>> data = pd.DataFrame(np.random.randn(1000, 4))
>>> data.describe()
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean 0.049091 0.026112 -0.002544 -0.051827
std 0.996947 1.007458 0.995232 0.998311
min -3.645860 -3.184377 -3.745356 -3.428254
25% -0.599807 -0.612162 -0.687373 -0.747478
50% 0.047101 -0.013609 -0.022158 -0.088274
75% 0.756646 0.695298 0.699046 0.623331
max 2.653656 3.525865 2.735527 3.366626
想要找出一列中绝对值大于三的值:
>>> col = data[2]
>>> col[np.abs(col) > 3]
41 -3.399312
136 -3.745356
Name: 2, dtype: float64
要选出所有值大于3或小于-3的行,你可以对布尔值DataFrame使用any方法:
>>> data[(np.abs(data) > 3).any(1)]
0 1 2 3
41 0.457246 -0.025907 -3.399312 -0.974657
60 1.951312 3.260383 0.963301 1.201206
136 0.508391 -0.196713 -3.745356 -1.520113
235 -0.242459 -3.056990 1.918403 -0.578828
258 0.682841 0.326045 0.425384 -3.428254
322 1.179227 -3.184377 1.369891 -1.074833
544 -3.548824 1.553205 -2.186301 1.277104
635 -0.578093 0.193299 1.397822 3.366626
782 -0.207434 3.525865 0.283070 0.544635
803 -3.645860 0.255475 -0.549574 -1.907459
值可以根据这些标准来设置,下面代码限制了-3到3之间的数值:
>>> data[np.abs(data) > 3] = np.sign(data) * 3
>>> data.describe()
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean 0.050286 0.025567 -0.001399 -0.051765
std 0.992920 1.004214 0.991414 0.995761
min -3.000000 -3.000000 -3.000000 -3.000000
25% -0.599807 -0.612162 -0.687373 -0.747478
50% 0.047101 -0.013609 -0.022158 -0.088274
75% 0.756646 0.695298 0.699046 0.623331
max 2.653656 3.000000 2.735527 3.000000
语句np.sign(data)根据数据中的值的正负分别生成1和-1的数值:
>>> np.sign(data).head()
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
2.7 置换和随机抽样
使用numpy.random.permutation对DataFrame中的Series或行进行置换(随机重排序)是非常方便的。在调用permutation时根据你想要的轴长度可以产生一个表示新顺序的整数数组:
>>> df = pd.DataFrame(np.arange(5 * 4).reshape((5, 4)))
>>> sampler = np.random.permutation(5)
>>> sampler
array([3, 1, 4, 2, 0])
整数数组可以用在基于iloc的索引或等价的take函数中:
>>> df
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
>>> df.take(sampler)
0 1 2 3
3 12 13 14 15
1 4 5 6 7
4 16 17 18 19
2 8 9 10 11
0 0 1 2 3
要选出一个不含有替代值的随机子集,你可以使用Series和DataFrame的sample方法:
>>> df.sample(n=3)
0 1 2 3
3 12 13 14 15
4 16 17 18 19
2 8 9 10 11
要生成一个带有替代值的样本(允许有重复选择),将replace=True传入sample方法:
>>> choices = pd.Series([5, 7, -1, 6, 4])
>>> draws = choices.sample(n=10, replace=True)
>>> draws
4 4
1 7
4 4
2 -1
0 5
3 6
1 7
4 4
0 5
4 4
dtype: int64
2.8 计算指标/虚拟变量
将分类变量转换为“虚拟”或“指标”矩阵是另一种用于统计建模或机器学习的转换操作。如果DataFrame中的一列有k个不同的值,则可以衍生一个k列的值为1和0的矩阵或DataFrame。pandas有一个get_dummies函数用于实现该功能。
>>> df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
>>> 'data1': range(6)})
>>> pd.get_dummies(df['key'])
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方法中有一个前缀参数用于实现该功能:
>>> dummies = pd.get_dummies(df['key'], prefix='key')
>>> df_with_dummy = df[['data1']].join(dummies)
>>> df_with_dummy
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中的一行属于多个类别,则情况略为复杂。
>>> mnames = ['movie_id', 'title', 'genres']
>>> movies = pd.read_table('datasets/movielens/movies.dat', sep='::',
>>> header=None, names=mnames)
>>> movies[:10]
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
为每个电影流派添加指标变量需要进行一些数据处理。首先,从数据集中提取出所有不同的流派的列表:
>>> all_genres = []
>>> for x in movies.genres:
>>> all_genres.extend(x.split('|'))
>>> genres = pd.unique(all_genres)
>>> genres
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的一种方式:
>>> zero_matrix = np.zeros((len(movies), len(genres)))
>>> dummies = pd.DataFrame(zero_matrix, columns=genres)
现在,遍历每一部电影,将dummies每一行的条目设置为1。为了实现该功能,使用dummies.columns来计算每一个流派的列指标:
>>> gen = movies.genres[0]
>>> gen.split('|')
['Animation', "Children's", 'Comedy']
>>> dummies.columns.get_indexer(gen.split('|'))
array([0, 1, 2])
之后,使用.loc根据这些指标来设置值:
>>> for i, gen in enumerate(movies.genres):
>>> indices = dummies.columns.get_indexer(gen.split('|'))
>>> dummies.iloc[i, indices] = 1
之后,和前面一样,可以将结果与movies进行联合:
>>> movies_windic = movies.join(dummies.add_prefix('Genre_'))
>>> movies_windic.iloc[0]
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_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, Length: 21, dtype: object
对于更大的数据,上面这种使用多成员构建指标变量并不是特别快速。更好的方法是写一个直接将数据写为NumPy数组的底层函数,然后将结果封装进DataFrame。
将get_dummies与cut等离散化函数结合使用是统计应用的一个有用方法:
>>> np.random.seed(12345)
>>> values = np.random.rand(10)
>>> values
array([ 0.9296, 0.3164, 0.1839, 0.2046, 0.5677, 0.5955, 0.9645,
0.6532, 0.7489, 0.6536])
>>> bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
>>> pd.get_dummies(pd.cut(values, bins))
(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来设置随机种子以确保示例的确定性。
3 字符串操作
3.1 字符串对象方法
在很多字符串处理和脚本应用中,内建的字符串方法是足够的。例如,一个逗号分隔的字符串可以使用split方法拆分成多块:
>>> val = 'a,b, guido'
>>> val.split(',')
['a', 'b', ' guido']
split常和strip一起使用,用于清除空格(包括换行):
>>> pieces = [x.strip() for x in val.split(',')]
>>> pieces
['a', 'b', 'guido']
这些子字符串可以使用加法与两个冒号分隔符连接在一起:
>>> first, second, third = pieces
>>> first + '::' + second + '::' + third
'a::b::guido'
但是这并不是一个实用的通用方法。在字符串'::'的join方法中传入一个列表或元组是一种更快且更加Pythonic(Python风格化)的方法:
>>> '::'.join(pieces)
'a::b::guido'
其他方法涉及定位子字符串。使用Python的in关键字是检测子字符串的最佳方法,尽管index和find也能实现同样的功能:
>>> 'guido' in val
True
>>> val.index(',')
1
>>> val.find(':')
-1
find和index的区别在于index在字符串没有找到时会抛出一个异常(而find是返回-1)。
相关地,count返回的是某个特定的子字符串在字符串中出现的次数:
>>> val.count(',')
2
replace将用一种模式替代另一种模式。它通常也用于传入空字符串来删除某个模式。
>>> val.replace(',', '::')
'a::b:: guido'
>>> val.replace(',', '')
'ab guido'
Python内建字符串方法
3.2 正则表达式
正则表达式提供了一种在文本中灵活查找或匹配(通常更为复杂的)字符串模式的方法。单个表达式通常被称为regex,是根据正则表达式语言形成的字符串。Python内建的re模块是用于将正则表达式应用到字符串上的库。
re模块主要有三个主题:模式匹配、替代、拆分。当然,这三部分主题是相关联的。一个正则表达式描述了在文本中需要定位的一种模式,可以用于多种目标。让我们来看一个简单的示例:假设我们想将含有多种空白字符(制表符、空格、换行符)的字符串拆分开。描述一个或多个空白字符的正则表达式是\s+:
>>> import re
>>> text = "foo bar\t baz \tqux"
>>> re.split('\s+', text)
['foo', 'bar', 'baz', 'qux']
当你调用re.split('\s+',text),正则表达式首先会被编译,然后正则表达式的split方法在传入文本上被调用。你可以使用re.compile自行编译,形成一个可复用的正则表达式对象:
>>> regex = re.compile('\s+')
>>> regex.split(text)
['foo', 'bar', 'baz', 'qux']
如果你想获得的是一个所有匹配正则表达式的模式的列表,你可以使用findall方法:
>>> regex.findall(text)
[' ', '\t ', ' \t']
为了在正则表达式中避免转义符\的影响,可以使用原生字符串语法,比如r'C:\x'或者用等价的'C:\\x'
将相同的表达式应用到多个字符串上,使用re.compile创建一个正则表达式对象,这样做有利于节约CPU周期。
match和search与findall相关性很大。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}'
# re.IGNORECASE使正则表达式不区分大小写
regex = re.compile(pattern, flags=re.IGNORECASE)
在文本上使用findall会生成一个电子邮件地址的列表:
>>> regex.findall(text)
['dave@google.com',
'steve@gmail.com',
'rob@gmail.com',
'ryan@yahoo.com']
search返回的是文本中第一个匹配到的电子邮件地址。对于前面提到的正则表达式,匹配对象只能告诉我们模式在字符串中起始和结束的位置:
>>> m = regex.search(text)
>>> m
<_sre.SRE_Match object; span=(5, 20), match='dave@google.com'>
>>> text[m.start():m.end()]
'dave@google.com'
regex.match只在模式出现于字符串起始位置时进行匹配,如果没有匹配到,返回None:
>>> print(regex.match(text))
None
sub会返回一个新的字符串,原字符串中的模式会被一个新的字符串替代:
>>> print(regex.sub('REDACTED', text))
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED
假设想查找电子邮件地址,并将每个地址分为三个部分:用户名,域名和域名后缀。要实现这一点,可以用括号将模式包起来:
>>> pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
>>> regex = re.compile(pattern, flags=re.IGNORECASE)
由这个修改后的正则表达式产生的匹配对象的groups方法,返回的是模式组件的元组:
>>> m = regex.match('wesm@bright.net')
>>> m.groups()
('wesm', 'bright', 'net')
当模式可以分组时,findall返回的是包含元组的列表:
>>> regex.findall(text)
[('dave', 'google', 'com'),
('steve', 'gmail', 'com'),
('rob', 'gmail', 'com'),
('ryan', 'yahoo', 'com')]
sub也可以使用特殊符号,如\1和\2,访问每个匹配对象中的分组。符号\1代表的是第一个匹配分组,\2代表的是第二个匹配分组,以此类推:
>>> 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
正则表达式方法
3.3 pandas中的向量化字符串函数
清理杂乱的数据集用于分析通常需要大量的字符串处理和正则化。包含字符串的列有时会含有缺失数据,使事情变得复杂:
>>> data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com',
>>> 'Rob': 'rob@gmail.com', 'Wes': np.nan}
>>> data = pd.Series(data)
>>> data
Dave dave@google.com
Rob rob@gmail.com
Steve steve@gmail.com
Wes NaN
dtype: object
>>> data.isnull()
Dave False
Rob False
Steve False
Wes True
dtype: bool
可以使用data.map将字符串和有效的正则表达式方法(以lambda或其他函数的方式传递)应用到每个值上,但是在NA(null)值上会失败。为了解决这个问题,Series有面向数组的方法用于跳过NA值的字符串操作。这些方法通过Series的str属性进行调用,例如,可以通过str.contains来检查每个电子邮件地址是否含有'gmail':
>>> data.str.contains('gmail')
Dave False
Rob True
Steve True
Wes NaN
dtype: object
正则表达式也可以结合任意的re模块选项使用,例如IGNORECASE:
>>> pattern
'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
>>> x = data.str.findall(pattern, flags=re.IGNORECASE)
>>> x
Dave [(dave, google, com)]
Rob [(rob, gmail, com)]
Steve [(steve, gmail, com)]
Wes NaN
dtype: object
有多种方法可以进行向量化的元素检索。可以使用str.get或在str属性内部索引:
>>> matches = data.str.match(pattern, flags=re.IGNORECASE)
>>> matches
Dave True
Rob True
Steve True
Wes NaN
dtype: object
要访问嵌入式列表中的元素,可以将索引传递给这些函数中的任意一个:
>>> x.str.get(1)
Dave NaN
Rob NaN
Steve NaN
Wes NaN
dtype: float64
>>> x.str[0]
Dave NaN
Rob NaN
Steve NaN
Wes NaN
dtype: float64
可以使用字符串切片的类似语法进行向量化切片:
>>> data.str[:5]
Dave dave@
Rob rob@g
Steve steve
Wes NaN
dtype: object
部分向量化字符串方法列表