Python之数据清洗与准备

本博客为《利用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个。

duplicateddrop_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_dummiescut等离散化函数结合使用时统计应用的一个有用方法:

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关键字是检测字符串的最佳方法,尽管indexfind也能实现同样的功能:

print('guido' in val)
print(val.index(','))
print(val.find(':'))
#
True
1
-1

请注意findindex的区别在于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周期。
matchsearchfindall相关性很大。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的数据连接和分组功能。

发布了27 篇原创文章 · 获赞 9 · 访问量 981

猜你喜欢

转载自blog.csdn.net/sinat_42574069/article/details/104211223