本博客为《利用Python进行数据分析》的读书笔记,请勿转载用于其他商业用途。
文章目录
在进行数据分析和建模的过程中,大量的时间花在数据准备上:加载、清理、转换和重新排列。这样的工作占了分析师80%以上的时间。有时数据存储在文件或数据库中的方式对于特定的任务来说格式并不正确。许多研究人员选择使用通用编程语言(如Python、Perl、R或Java)或Unix文本处理工具(如sed或awk)进行从一种形式到另一种形式的特殊数据处理。幸运的是,pandas以及内置的Python语言功能为你提供了一个高级、灵活和快速的工具,使你能够将数据处理为正确的形式。
1. 处理缺失值
缺失数据会在很多数据分析应用中出现。pandas的目标之一就是尽可能无痛地处理缺失值。例如,pandas对象的所有描述性统计信息默认情况下是排除缺失值的。
pandas对象中表现缺失值的方式并不完美,但是它对大部分用户来说是有用的。对于数值型数据,pandas使用浮点值NaN(Not a Number)来表示缺失值。我们称NaN为容易检测到的标识值:
string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
print(string_data)
#
0 aardvark
1 artichoke
2 NaN
3 avocado
dtype: object
print(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
print(string_data.isnull())
#
0 True
1 False
2 True
3 False
dtype: bool
pandas项目持续改善处理缺失值的内部细节,但是用户API函数,比如pandas.isnull
,抽象掉了很多令人厌烦的细节。
表:NA处理方法
函数名 | 描述 |
---|---|
dropna | 根据每个标签的值是否是缺失数据来筛选轴标签,并根据允许丢失的数据量来确定阈值 |
fillna | 用某些值填充缺失的数据或使用插值方法(如‘ffill’或‘bfill’) |
isnull | 返回表明哪些值是缺失值的布尔值 |
notnull | isnull的反函数 |
1.1 过滤缺失值
有多种过滤缺失值的方法。虽然可以使用pandas.innull
和布尔值索引手动地过滤缺失值,但dropna
在过滤缺失值时是非常有用的。在Series上使用dropna
,它会返回Series中所有的非空数据及其索引值:
from numpy import nan as NA
data = pd.Series([1, NA, 3.5, NA, 7])
print(data.dropna())
#
0 1.0
2 3.5
4 7.0
dtype: float64
上面的例子与下面的代码是等价的:
print(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()
print(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
print(cleaned)
#
0 1 2
0 1.0 6.5 3.0
传入how='all'
时,将删除所有值均为NA的行:
print(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
print(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
print(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 -1.113580 NaN NaN
1 0.847513 NaN NaN
2 -0.437619 NaN 0.342308
3 1.261819 NaN 1.503347
4 1.155044 -2.045711 0.941944
5 1.543513 0.507086 -1.568366
6 0.313304 -0.434508 -1.550364
df.dropna()
#
0 1 2
4 1.155044 -2.045711 0.941944
5 1.543513 0.507086 -1.568366
6 0.313304 -0.434508 -1.550364
df.dropna(thresh=2)
#
0 1 2
2 -0.437619 NaN 0.342308
3 1.261819 NaN 1.503347
4 1.155044 -2.045711 0.941944
5 1.543513 0.507086 -1.568366
6 0.313304 -0.434508 -1.550364
1.2 补全缺失值
你有时可能需要以多种方式补全“漏洞”,而不是过滤缺失值(也可能丢弃其他数据)。大多数情况下,主要使用fillna
方法来补全缺失值。调用fillna
时,可以使用一个常数来替代缺失值:
df.fillna(0)
#
0 1 2
0 -1.113580 0.000000 0.000000
1 0.847513 0.000000 0.000000
2 -0.437619 0.000000 0.342308
3 1.261819 0.000000 1.503347
4 1.155044 -2.045711 0.941944
5 1.543513 0.507086 -1.568366
6 0.313304 -0.434508 -1.550364
在调用fillna
时使用字典,你可以为不同列设定不同的填充值。我们将第1列的NA修改为0.5,第2列的修改为0:
df.fillna({1: 0.5, 2: 0})
#
0 1 2
0 -1.113580 0.500000 0.000000
1 0.847513 0.500000 0.000000
2 -0.437619 0.500000 0.342308
3 1.261819 0.500000 1.503347
4 1.155044 -2.045711 0.941944
5 1.543513 0.507086 -1.568366
6 0.313304 -0.434508 -1.550364
fillna
返回的是一个新的对象,但你也可以修改已经存在的对象:
_ = df.fillna(0, inplace=True)
df
#
0 1 2
0 -1.113580 0.000000 0.000000
1 0.847513 0.000000 0.000000
2 -0.437619 0.000000 0.342308
3 1.261819 0.000000 1.503347
4 1.155044 -2.045711 0.941944
5 1.543513 0.507086 -1.568366
6 0.313304 -0.434508 -1.550364
用于重建索引的相同的插值方法也可以用于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.308387 1.710538 0.404252
1 1.509045 -1.402541 -0.603614
2 -0.148256 NaN 3.057599
3 -1.055766 NaN 1.814597
4 0.583656 NaN NaN
5 -0.686148 NaN NaN
df.fillna(method='ffill')
#
0 1 2
0 -0.308387 1.710538 0.404252
1 1.509045 -1.402541 -0.603614
2 -0.148256 -1.402541 3.057599
3 -1.055766 -1.402541 1.814597
4 0.583656 -1.402541 1.814597
5 -0.686148 -1.402541 1.814597
df.fillna(method='ffill', limit=2)
#
0 1 2
0 -0.308387 1.710538 0.404252
1 1.509045 -1.402541 -0.603614
2 -0.148256 -1.402541 3.057599
3 -1.055766 -1.402541 1.814597
4 0.583656 NaN 1.814597
5 -0.686148 NaN 1.814597
使用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参数函数
参数 | 描述 |
---|---|
value | 标量值或字典型对象用于填充缺失值 |
method | 插值方法,如果没有其他参数,默认是’ffill’ |
axis | 需要填充的轴,默认axis=0 |
inplace | 修改被调用的对象,而不是生成一个备份 |
limit | 用于前向或后向填充时最大的填充范围 |
2. 数据转换
2.1 删除重复值
由于各种原因,DataFrame中会出现重复行。例:
data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
'k2':[1, 1, 2, 3, 3, 4, 4]})
print(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反映的是每一行是否存在重复(与之前出现过的行相同)情况:
print(data.duplicated())
#
0 False
1 False
2 False
3 False
4 False
5 False
6 True
dtype: bool
drop_duplicates
返回的是DataFrame,内容是duplicated
返回数组中为False的部分:
print(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)
print(data.drop_duplicates(['k1']))
#
k1 k2 v1
0 one 1 0
1 two 1 1
这个例子我看了会儿才明白,实际上k1列都是‘one’和‘two’,基于‘k1’列去重就是只保留了最上面的‘one’和‘two’,下面的全被去重去掉了。因此结果只有2个。
duplicated
和drop_duplicated
默认都是保留第一个观测到的值。传入参数keep='last'
将会返回最后一个:
print(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
此处第5行和第6行重复,第5行被删除,而第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]})
print(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': 'pork',
'pulled pork': 'pork',
'pastrami': 'cow',
'corned beef': 'cow',
'honey ham': 'pork',
'nova lox': 'salmon'
}
Series的map
方法接收一个函数或一个包含映射关系的字典型对象,但是这里我们有一个小的问题在于一些肉类大写了,而另一部分肉类没有。因此我们需要使用Series的str.lower
方法将每个值都转换为小写:
lowercased = data['food'].str.lower()
print(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)
print(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
我们也传入一个能够完成所有工作的函数:
print(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
使用map是一种可以便捷执行按元素转换及其他清洗相关操作的方法。
2.3 替代值
使用fillna
填充缺失值是通用值替换的特殊案例。前面你已经看到,map
可以用来修改一个对象中的子集的值,但是replace
提供了更为简单灵活的实现。让我们考虑下面的Series:
data = pd.Series([1., -999., 2., -999., -1000., 3.])
print(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
):
print(data.replace(-999, np.nan))
#
0 1.0
1 NaN
2 2.0
3 NaN
4 -1000.0
5 3.0
dtype: float64
如果你想一次替代多个值,可以传入一个列表和替代值:
print(data.replace([-999, -1000], np.nan))
#
0 1.0
1 NaN
2 2.0
3 NaN
4 NaN
5 3.0
dtype: float64
要想将不同的值替换为不同的值,可以传入替代值的列表:
print(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
参数也可以通过字典传递:
print(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()
print(data.index.map(transform))
#
Index(['OHIO', 'COLO', 'NEW '], dtype='object')
你可以赋值给index,修改DataFrame:
data.index = data.index.map(transform)
print(data)
#
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11
如果你想要创建数据集转换后的版本,并且不修改原有的数据集,一个有用的方法是rename
:
print(data.rename(index=str.title, columns=str.upper))
#
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colo 4 5 6 7
New 8 9 10 11
注释:将行,名称改为首字母大写,列名称改为全部大写。
值得注意的是,rename
可以结合字典型对象使用,为轴标签的子集提供新的值:
print(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)
print(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)
print(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(年龄)数据标签:
print(cats.codes)
#
[0 0 0 1 0 0 2 1 3 2 2 1]
print(cats.categories)
#
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]],
closed='right',
dtype='interval[int64]')
print(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
来改变哪一边是封闭的:
print(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']
print(pd.cut(ages, bins, labels=group_names))
#
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult]
Length: 12
Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
如果你传给cut
整数个的箱来代替显式的箱边,pandas将根据数据中的最小值和最大值计算出等长的箱。请考虑一些均匀分布的数据被切成4份的情况:
data = np.random.rand(20)
pd.cut(data, 4, precision=2)
#
[(0.34, 0.55], (0.34, 0.55], (0.75, 0.96], (0.75, 0.96], (0.75, 0.96], ..., (0.13, 0.34], (0.55, 0.75], (0.75, 0.96], (0.75, 0.96], (0.55, 0.75]]
Length: 20
Categories (4, interval[float64]): [(0.13, 0.34] < (0.34, 0.55] < (0.55, 0.75] < (0.75, 0.96]]
precision=2
的选项将十进制精度限制在两位。
qcut
是一个与分箱密切相关的函数,它基于样本分位数进行分箱。取决于数据的分布,使用cut
通常不会使每个箱具有相同数据量的数据点。由于qcut
使用样本的分位数,你可以通过qcut
获得等长的箱:
data = np.random.randn(1000)
cats = pd.qcut(data, 4)
print(cats)
#
[(0.666, 3.696], (0.00928, 0.666], (0.00928, 0.666], (-0.631, 0.00928], (-2.8169999999999997, -0.631], ..., (0.666, 3.696], (0.666, 3.696], (-2.8169999999999997, -0.631], (-2.8169999999999997, -0.631], (-0.631, 0.00928]]
Length: 1000
Categories (4, interval[float64]): [(-2.8169999999999997, -0.631] < (-0.631, 0.00928] < (0.00928, 0.666] < (0.666, 3.696]]
print(pd.value_counts(cats))
#
pd.value_counts(cats)
(0.666, 3.696] 250
(0.00928, 0.666] 250
(-0.631, 0.00928] 250
(-2.8169999999999997, -0.631] 250
dtype: int64
可以看到,1000个随机数的划分是按照每个区域250个来划分的。
与cut
类似,你可以传入自定义的分位数(0和1之间的数据,包括边):
print(pd.cut(data, [0, 0.1, 0.5, 0.9, 1]))
#
[(0.6, 2.0], (0.1, 0.6], (0.1, 0.6], (-0.6, 0.1], (-2.0, -0.6], ..., (0.6, 2.0], (0.6, 2.0], (-2.0, -0.6], NaN, (-0.6, 0.1]]
Length: 1000
Categories (5, interval[float64]): [(-2.0, -0.6] < (-0.6, 0.1] < (0.1, 0.6] < (0.6, 2.0] < (2.0, 3.0]]
2.6 检测和过滤异常值
过滤或转换异常值很大程度上是应用数组操作的事情。考虑一个具有正态分布数据的DataFrame:
data = pd.DataFrame(np.random.randn(1000, 4))
print(data.describe())
#
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean 0.013423 -0.032813 -0.003508 0.028668
std 0.983263 0.987664 1.007118 0.987091
min -3.651250 -3.172490 -3.168098 -3.037493
25% -0.649790 -0.680888 -0.670289 -0.650754
50% -0.029006 -0.058494 0.010125 0.057053
75% 0.623833 0.607538 0.671462 0.681250
max 3.073316 3.274550 3.680209 3.330262
如果你要找出一列中绝对值大于3的值:
col = data[2]
print(col[np.abs(col) > 3])
#
447 3.180714
503 -3.168098
537 -3.098360
688 3.680209
Name: 2, dtype: float64
要选出所有值大于3或小于-3的行,你可以对布尔值DataFrame使用any
方法:
print(data[(np.abs(data) > 3).any(1)])
#
0 1 2 3
109 1.045472 3.223203 -1.080799 1.471738
246 -3.651250 -0.407008 0.503636 -0.895636
255 3.073316 0.093006 -0.312063 -0.070332
364 -1.279454 -3.172490 -0.665220 0.485306
417 0.756304 -1.016015 0.496410 -3.037493
447 -0.098264 -0.913000 3.180714 -0.181019
503 0.891415 0.534048 -3.168098 1.321264
537 -0.254152 -1.079117 -3.098360 -0.146553
626 0.549641 -0.736698 0.813719 3.330262
636 -0.901701 0.387548 0.020514 3.230154
665 -0.514525 3.274550 -2.048670 0.504948
688 2.002831 1.909448 3.680209 0.060585
值可以根据这些标准来设置,下面代码限制了-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.014001 -0.033138 -0.004102 0.028145
std 0.980821 0.985534 1.003483 0.985202
min -3.000000 -3.000000 -3.000000 -3.000000
25% -0.649790 -0.680888 -0.670289 -0.650754
50% -0.029006 -0.058494 0.010125 0.057053
75% 0.623833 0.607538 0.671462 0.681250
max 3.000000 3.000000 3.000000 3.000000
语句np.sign(data)
根据数据中的值的正负分别生成1和-1的数值:
print(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)
print(sampler)
#
array([0, 2, 1, 3, 4])
整数数组可以用在基于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
0 0 1 2 3
2 8 9 10 11
1 4 5 6 7
3 12 13 14 15
4 16 17 18 19
要选出一个不含有替代值的随机子集,你可以使用Series和DataFrame的sample
方法:
df.sample(n=3)
#
0 1 2 3
0 0 1 2 3
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
#
1 7
4 4
3 6
3 6
2 -1
1 7
0 5
4 4
1 7
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
看着可能不是特别直观,我们再改一下:
df = pd.DataFrame({'key': ['e', 'b', 'a', 'c', 'd', 'f'],
'data1': range(6)})
pd.get_dummies(df['key'])
#
a b c d e f
0 0 0 0 0 1 0
1 0 1 0 0 0 0
2 1 0 0 0 0 0
3 0 0 1 0 0 0
4 0 0 0 1 0 0
5 0 0 0 0 0 1
在某些情况下,你可能想在指标DataFrame的列上加入前缀,然后与其他数据合并。在get_dummies
方法中有一个前缀参数用于实现该功能:
dummies = pd.get_dummies(df['key'], prefix='key')
df_with_dummies = df[['data1']].join(dummies)
print(df_with_dummies)
#
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
将get_dummies
与cut
等离散化函数结合使用时统计应用的一个有用方法:
np.random.seed(12345)
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)))
#
[0.92961609 0.31637555 0.18391881 0.20456028 0.56772503 0.5955447
0.96451452 0.6531771 0.74890664 0.65356987]
(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
3. 字符串操作
3.1 字符串对象办法
在很多字符串处理和脚本应用中,内建的字符串方法是足够的。例如,一个逗号分隔的字符串可以使用split
方法拆分成多块:
val = 'a,b, guido'
print(val.split(','))
#
['a', 'b', ' guido']
split
常和strip
一起使用,用于清除空格(包括换行):
pieces = [x.strip() for x in val.split()]
print(pieces)
#
['a', 'b', 'guido']
这些字符串可以使用加法与两个冒号分隔符连接在一起:
first, second, third = pieces
print(first + '::' + second + '::' + third)
#
a::b::guido
但这并不是一个使用的通用方法。在字符串’::'的join
方法中传入一个列表或元组是一种更快且更加Pythonic的方法:
print("::".join(pieces))
#
a::b::guido
其他方法涉及定位子字符串。使用Python的in
关键字是检测字符串的最佳方法,尽管index
和find
也能实现同样的功能:
print('guido' in val)
print(val.index(','))
print(val.find(':'))
#
True
1
-1
请注意find
和index
的区别在于index
在字符串没有找到时会抛出一个异常,而find
是返回-1:
print(val.index(':'))
相应的,count
返回的是某个特定的子字符串在字符串中出现的次数:
print(val.count(','))
#
2
replace
将用一种模式替代另一种模式。它通常也用于传入空字符串来删除某个模式。
print(val.replace(',', '::'))
#
print(val.replace(',', '::'))
print(val.replace(',', ''))
#
ab guido
表:Python内建字符串方法
方法 | 描述 |
---|---|
count | 返回子字符串在字符串中的非重叠出现次数 |
endwith | 如果字符串以后缀结尾则返回True |
startwith | 如果字符串以前缀开始则返回True |
join | 使用字符串作为间隔符,用于粘合其他字符串的序列 |
index | 如果在字符串中找到,则返回子字符串中的第一个字符的位置;如果找不到则引发ValueError |
find | 返回字符串中第一个出现子字符串的第一个字符的位置;类似index ,但如果没有找到则返回-1 |
replace | 使用一个字符串替代另一个字符串 |
strip, rstrip, lstrip | 修剪空白,包括换行符;相当于对每个元素进行x.strip()(以及rstrip,lstrip) |
split | 使用分隔符将字符串拆分为子字符串的列表 |
lower | 将大写字母转换为小写字母 |
upper | 将小写字母转换为大写字母 |
casefold | 将字符转换为小写,并将在任何特定于区域的变量字符组合转换为常见的可比较形式 |
ljust,rjust | 左对齐或右对齐,用空格(或其他一些字符)填充字符串的相反侧以返回具有最小宽度的字符串 |
3.2 正则表达式
正则表达式提供了一种在文本中灵活查找或匹配(通常更为复杂的)字符串模式的方法。单个表达式通常被称为regex
,是根据正则表达式语言形成的字符串。Python内建的re
模块是用于将正则表达式应用到字符串上的库。
re
模块主要有三个主题:模式匹配、替代、拆分。一个正则表达式描述了在文本中需要定位的一种模式,可以用于多种目标。
假设我们想将含有多种空白字符(制表符、空格、换行符)的字符串拆分开。描述一个或多个空白字符串的正则表达式是:\s+:
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 [email protected]
Steve [email protected]
Rob [email protected]
Ryan [email protected]
"""
pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
regex = re.compile(pattern, flags=re.IGNORECASE)
regex.findall(text)
在文本上使用findall
会生成一个电子邮件地址的列表:
['[email protected]', '[email protected]', '[email protected]', '[email protected]']
search
返回的是文本中第一个匹配到的电子邮件地址。对于前面提到的正则表达式,匹配对象只能告诉我们模式在字符串中起始和结束的位置:
m = regex.search(text)
m
#
<re.Match object; span=(5, 20), match='[email protected]'>
text[m.start():m.end()]
#
'[email protected]'
regex.match
只在模式出现于字符串起始位置时进行匹配,如果没有匹配到,返回None:
print(regex.match(text))
#
None
相关地,sub
会返回一个新的字符串,原字符串中的模式会被一个新的字符串替代:
print(regex.sub('REDACTED', text))
#
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED
3.3 pandas中的向量化字符串函数
清理杂乱的数据集用于分析通常需要大量的字符串处理和正则化。包含字符串的列有时会含有缺失数据,使事情变得复杂:
data = {'Dave': '[email protected]',
'Steve': '[email protected]',
'Rob': '[email protected]',
'Ryam': '[email protected]',
'Wes': np.nan}
data = pd.Series(data)
data
#
Dave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Ryam ryan@yahoo.com
Wes NaN
dtype: object
data.isnull()
#
Dave False
Steve False
Rob False
Ryam False
Wes True
dtype: bool
你可以使用data.map
将字符串和有效的正则表达式方法(以lambda
或其他函数的方式传递)应用到每个值上,但是在NA(null)值上会失败。为了解决这个问题,Series有面向数组的方法用于跳过NA值的字符串操作。这些方法通过Series的str
属性进行调用,例如,我们可以通过str.contains
来检查每个电子邮件是否含有’gmail’:
data.str.contains('gmail')
#
Dave False
Steve True
Rob True
Ryam False
Wes NaN
dtype: object
正则表达式也可以结合任意的re
模块选项使用,例如IGNORECASE
:
pattern
#
'[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}'
data.str.findall(pattern, flags=re.IGNORECASE)
#
Dave [dave@google.com]
Steve [steve@gmail.com]
Rob [rob@gmail.com]
Ryam [ryan@yahoo.com]
Wes NaN
dtype: object
有多种方法可以进行向量化的元素检索。可以使用str.get
或在str
属性内部索引:
matches = data.str.match(pattern, flags=re.IGNORECASE)
matches
#
Dave True
Steve True
Rob True
Ryam True
Wes NaN
dtype: object
要访问嵌入式列表中的元素,我们可以将索引传递给这些函数中的任意一个:
matches.str.get(1)
#
Dave NaN
Steve NaN
Rob NaN
Ryam NaN
Wes NaN
dtype: float64
matches.str[0]
#
Dave NaN
Steve NaN
Rob NaN
Ryam NaN
Wes NaN
dtype: float64
你可以使用字符串切片的类似语法进行向量化切片:
data.str[:5]
#
Dave dave@
Steve steve
Rob rob@g
Ryam ryan@
Wes NaN
dtype: object
表:部分向量化字符串方法列表
方法 | 描述 |
---|---|
cat | 根据可选的分隔符按元素黏合字符串 |
contains | 返回是否含有某个模式/正则表达式的布尔值数组 |
count | 模式出现次数的计数 |
extract | 使用正则表达式从字符串Series中分组抽取一个或多个字符串;返回的结果是每个分组形成一列的DataFrame |
endswith | 等价于对每个元素使用x.endwith (模式) |
startwith | 等价于对每个元素使用x.stratwith (模式) |
findall | 找出字符串中所有的模式/正则表达式匹配项,以列表返回 |
get | 对每个元素进行索引(获得第i个元素) |
isalnum | 等价于内建的str.alnum |
isalpha | 等价于内建的str.isalpha |
isdecimal | 等价于内建的str.isdecimal |
islower | 等价于内建的str.islower |
isdigit | 等价于内建的str.isdigit |
isnumeric | 等价于内建的str.isnumeric |
isupper | 等价于内建的str.isupper |
join | 根据传递的分隔符,将Series中的字符串联合 |
len | 计算每个字符串的长度 |
lower, upper | 转换大小写;等价于对每个元素进行x.lower() 或x.upper() |
match | 使用re.match 将正则表达式应用到每个元素上,将匹配分组以列表形式返回 |
pad | 将空白加到字符串的左边、右边或两边 |
center | 等价于pad=(side='both') |
repeat | 重复值(例如s.str.repeat(3) 等价于对每个字符串进行x*3 |
replace | 以其他字符串替代模式/正则表达式 匹配项 |
slice | 对Series中的字符串进行切片 |
split | 以分隔符或正则表达式对字符串进行拆分 |
strip | 对字符串两侧的空白进行消除,包括换行符 |
rstrip | 消除字符串右边的空白 |
lstrip | 消除字符串左边的空白 |
4. 小结
高效的数据准备工作能使我们可以将更多的时间用于分析而不是准备数据,从而显著提升生产效率。我们已经探索了多个工具,但覆盖范围仍不够全面。今后,我们将探索pandas的数据连接和分组功能。