Python for Data Analysis 7

Python for Data Analysis

数据清洗和准备

在数据分析和建模的过程中,相当多的时间要用在数据准备上:加载、清理、转换以及重塑。这些工作会占到分析师时间的80%或更多。有时,存储在文件和数
据库中的数据的格式不适合某个特定的任务。许多研究者都选择使用通用编程语言(如Python、Perl、R或Java)或UNIX文本处理工具(如sed或awk)对数据
格式进行专门处理。幸运的是,pandas和内置的Python标准库提供了一组高级的、灵活的、快速的工具,可以让你轻松地将数据规变为想要的格式。

如果你发现了一种本书或pandas库中没有的数据操作方式,请尽管在邮件列表或GitHub网站上提出。实际上,pandas的许多设计和实现都是由真实应用的需求
所驱动的。

在本章中,我会讨论处理缺失数据、重复数据、字符串操作和其它分析数据转换的工具。下一章,我会关注于用多种方法合并、重塑数据集。

7.1 处理缺失数据

在许多数据分析工作中,缺失数据是经常发生的。pandas的目标之一就是尽量轻松地处理缺失数据。例如,pandas对象的所有描述性统计默认都不包括缺失数据。

缺失数据在pandas中呈现的方式有些不完美,但对于大多数用户可以保证功能正常。对于数值数据,pandas使用浮点值NaN(Not a Number)表示缺失数据。
我们称其为哨兵值,可以方便的检测出来:

import pandas as pd
import numpy as np
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
string_data.notnull()
0    False
1     True
2    False
3     True
dtype: bool
string_data.dropna()
1    artichoke
3      avocado
dtype: object
滤除缺失数据

过滤掉缺失数据的办法有很多种。你可以通过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
012
01.06.53.0
11.0NaNNaN
2NaNNaNNaN
3NaN6.53.0
cleaned
012
01.06.53.0
# 传入 how = all 只会丢弃全为Na的行

data.dropna(how = 'all')
012
01.06.53.0
11.0NaNNaN
3NaN6.53.0
# 用这种方式丢弃列只需传入 axis = 1即可

data[4] = NA
data
0124
01.06.53.0NaN
11.0NaNNaNNaN
2NaNNaNNaNNaN
3NaN6.53.0NaN
data.dropna(how = 'all', axis = 1)
012
01.06.53.0
11.0NaNNaN
2NaNNaNNaN
3NaN6.53.0
# 另一个滤除DataFrame行的问题涉及时间序列数据。假设你只想留下一部分观测数据,可以用thresh参数实现此目的:

df = pd.DataFrame(np.random.randn(7, 3))
df.iloc[:4, 1] = NA
df.iloc[:2, 2] = NA
df
012
0-0.346179NaNNaN
1-1.104578NaNNaN
21.239385NaN0.267791
3-0.668442NaN-0.534018
4-2.2217050.6629560.364180
5-0.4443802.801167-0.349031
6-1.111454-1.105126-1.414799
df.dropna()
012
4-2.2217050.6629560.364180
5-0.4443802.801167-0.349031
6-1.111454-1.105126-1.414799
df.dropna(thresh = 2)              # 至少有两个不是NA
012
21.239385NaN0.267791
3-0.668442NaN-0.534018
4-2.2217050.6629560.364180
5-0.4443802.801167-0.349031
6-1.111454-1.105126-1.414799
df.dropna(thresh = 3)             # 至少有3个不是NA
012
4-2.2217050.6629560.364180
5-0.4443802.801167-0.349031
6-1.111454-1.105126-1.414799
填充缺失数据

你可能不想滤除缺失数据(有可能会丢弃跟它有关的其他数据),而是希望通过其他方式填补那些“空洞”。对于大多数情况而言,fillna方法是最主要的函数。
通过一个常数调用fillna就会将缺失值替换为那个常数值:

df.fillna(0)
012
0-0.3461790.0000000.000000
1-1.1045780.0000000.000000
21.2393850.0000000.267791
3-0.6684420.000000-0.534018
4-2.2217050.6629560.364180
5-0.4443802.801167-0.349031
6-1.111454-1.105126-1.414799
# 通过字典实现不同列填充不同的值

df.fillna({1:0.5, 2:0})
012
0-0.3461790.5000000.000000
1-1.1045780.5000000.000000
21.2393850.5000000.267791
3-0.6684420.500000-0.534018
4-2.2217050.6629560.364180
5-0.4443802.801167-0.349031
6-1.111454-1.105126-1.414799
# fillna默认返回新对象,也可以对现有对象进行就地修改

_ = df.fillna(0, inplace = True)
df
012
0-0.3461790.0000000.000000
1-1.1045780.0000000.000000
21.2393850.0000000.267791
3-0.6684420.000000-0.534018
4-2.2217050.6629560.364180
5-0.4443802.801167-0.349031
6-1.111454-1.105126-1.414799
# 对reindex有效的那些插值方法也可以用于fillna

df = pd.DataFrame(np.random.randn(6, 3))
df.iloc[2:, 1] = NA
df.iloc[4:, 2] = NA
df
012
0-0.2932670.0381330.784212
11.219223-1.928950-0.499293
2-0.593383NaN-0.605512
3-0.684834NaN-1.687495
41.076735NaNNaN
50.090188NaNNaN
df.fillna(method = 'ffill')
012
0-0.2932670.0381330.784212
11.219223-1.928950-0.499293
2-0.593383-1.928950-0.605512
3-0.684834-1.928950-1.687495
41.076735-1.928950-1.687495
50.090188-1.928950-1.687495
df.fillna(method = 'ffill', limit = 2)               #每一列最多limit 个数可以填充
012
0-0.2932670.0381330.784212
11.219223-1.928950-0.499293
2-0.593383-1.928950-0.605512
3-0.684834-1.928950-1.687495
41.076735NaN-1.687495
50.090188NaN-1.687495
df.fillna(method = 'ffill', limit = 3)
012
0-0.2932670.0381330.784212
11.219223-1.928950-0.499293
2-0.593383-1.928950-0.605512
3-0.684834-1.928950-1.687495
41.076735-1.928950-1.687495
50.090188NaN-1.687495
df.fillna(method = 'ffill', limit = 1)
012
0-0.2932670.0381330.784212
11.219223-1.928950-0.499293
2-0.593383-1.928950-0.605512
3-0.684834NaN-1.687495
41.076735NaN-1.687495
50.090188NaNNaN

df.fillna?

Signature: df.fillna(value=None, method=None, axis=None, inplace=False, limit=None, downcast=None, **kwargs)
Docstring:
Fill NA/NaN values using the specified method

Parameters

value : scalar, dict, Series, or DataFrame
Value to use to fill holes (e.g. 0), alternately a
dict/Series/DataFrame of values specifying which value to use for
each index (for a Series) or column (for a DataFrame). (values not
in the dict/Series/DataFrame will not be filled). This value cannot
be a list.
method : {‘backfill’, ‘bfill’, ‘pad’, ‘ffill’, None}, default None
Method to use for filling holes in reindexed Series
pad / ffill: propagate last valid observation forward to next valid 将最后一次有效观察向前传播到下一个有效
backfill / bfill: use NEXT valid observation to fill gap 使用下一个有效的观察来填补空白
axis : {0 or ‘index’, 1 or ‘columns’}
inplace : boolean, default False
If True, fill in place. Note: this will modify any
other views on this object, (e.g. a no-copy slice for a column in a
DataFrame).
limit : int, default None
If method is specified, this is the maximum number of consecutive
NaN values to forward/backward fill. In other words, if there is
a gap with more than this number of consecutive NaNs, it will only
be partially filled. If method is not specified, this is the
maximum number of entries along the entire axis where NaNs will be
filled. Must be greater than 0 if not None.
downcast : dict, default is None
a dict of item->dtype of what to downcast if possible,
or the string ‘infer’ which will try to downcast to an appropriate
equal type (e.g. float64 to int64 if possible)

7.2数据转换

移除重复数据
# DataFrame中出现重复行有多种原因。下面就是一个例子:

data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],'k2': [1, 1, 2, 3, 3, 4, 4]})
data
k1k2
0one1
1two1
2one2
3two3
4one3
5two4
6two4
# 查看是否是重复行

data.duplicated()
0    False
1    False
2    False
3    False
4    False
5    False
6     True
dtype: bool
# 还有一个与此相关的drop_duplicates方法,它会返回一个DataFrame,重复的数组会标为False:

data.drop_duplicates()
k1k2
0one1
1two1
2one2
3two3
4one3
5two4
# 这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断。假设我们还有一列值,且只希望根据k1列过滤重复项:

data['v1'] = range(7)
data.drop_duplicates(['k1'])
k1k2v1
0one10
1two11
# duplicated and drop_duplicates 默认保留第一个出现的值组合,传入 keep = 'last' , 则保留最后一个

data.drop_duplicates(['k1', 'k2'], keep = 'last')
k1k2v1
0one10
1two11
2one22
3two33
4one34
6two46
利用函数或映射进行数据转换
#b对于许多数据集,你可能希望根据数组、Series或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
foodounces
0bacon4.0
1pulled pork3.0
2bacon12.0
3Pastrami6.0
4corned beef7.5
5Bacon8.0
6pastrami3.0
7honey ham5.0
8nova lox6.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
foodouncesanimal
0bacon4.0pig
1pulled pork3.0pig
2bacon12.0pig
3Pastrami6.0cow
4corned beef7.5cow
5Bacon8.0pig
6pastrami3.0cow
7honey ham5.0pig
8nova lox6.0salmon
# 也可以传入一个能够完成这些人物的函数

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
替换值

利用fillna方法填充缺失数据可以看做值替换的一种特殊情况。前面已经看到,map可用于修改对象的数据子集,而replace则提供了一种实现该功能的更简单、更灵活的方式。我们来看看下面这个Series:

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
data.replace(-999., NA)
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
重命名轴索引

跟Series中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新的不同标签的对象。轴还可以被就地修改,而无需新建一个数据结构。
接下来看看下面这个简单的例子:

data = pd.DataFrame(np.arange(12).reshape((3, 4)),index=['Ohio', 'Colorado', 'New York'],columns=['one', 'two', 'three', 'four'])
data
onetwothreefour
Ohio0123
Colorado4567
New York891011
# 与Series 一样。轴索引也有一个map方法

transform = lambda x: x[:4].upper()
data.index.map(transform)
Index(['OHIO', 'COLO', 'NEW '], dtype='object')
data.index = data.index.map(transform)
data
onetwothreefour
OHIO0123
COLO4567
NEW891011
# 如果想要创建数据集的转换版(而不是修改原始数据, 比较实用的方法是rename

data.rename(index = str.title, columns = str.upper)
ONETWOTHREEFOUR
Ohio0123
Colo4567
New891011
# 特别说明一下, rename可以结合字典对象实现对部分轴标签的更新

data.rename(index = {'OHIO':'INDIANA'}, columns = {'three':'peekaboo'})
onetwopeekaboofour
INDIANA0123
COLO4567
NEW891011
#rename可以实现复制DataFrame并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入inplace=True即可:

data.rename(index = {'OHIO':'INDIANA'}, inplace = True)
data
onetwothreefour
INDIANA0123
COLO4567
NEW891011
#### 离散化和面元划分

为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
#接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,你需要使用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划分的面元。你可以将其看做一组表示面元名称的字符串。
#它的底层含有一个表示不同分类名称的类型数组,以及一个codes属性中的年龄数据的标签:

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)是pandas.cut结果的面元计数。
#跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。

pd.value_counts(cats)
(18, 25]     5
(35, 60]     3
(25, 35]     3
(60, 100]    1
dtype: int64
# 哪边是闭端可以通过right=False进行修改

cats1 = pd.cut(ages,bins,right = False)
pd.value_counts(cats1)
[25, 35)     4
[18, 25)     4
[35, 60)     3
[60, 100)    1
dtype: int64
# 可以通过传递一个列表或数组到labels , 设置自己的面元名称

group_name = ['Youth', 'YoungAdult', 'middleAged', 'Senior']
cats2 = pd.cut(ages, bins, labels = group_name)
pd.value_counts(cats2)
Youth         5
middleAged    3
YoungAdult    3
Senior        1
dtype: int64
#如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。
#下面这个例子中,我们将一些均匀分布的数据分成四组:

data = np.random.rand(20)
pd.cut(data, 4, precision=2)
[(0.48, 0.72], (0.25, 0.48], (0.023, 0.25], (0.72, 0.95], (0.72, 0.95], ..., (0.023, 0.25], (0.48, 0.72], (0.023, 0.25], (0.48, 0.72], (0.023, 0.25]]
Length: 20
Categories (4, interval[float64]): [(0.023, 0.25] < (0.25, 0.48] < (0.48, 0.72] < (0.72, 0.95]]
#选项precision=2,限定小数只有两位。

#qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分。根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点。
#而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元:

data = np.random.randn(1000)
cats3 = pd.qcut(data, 4)
cats3
[(-0.0449, 0.642], (-3.288, -0.699], (-0.699, -0.0449], (0.642, 4.066], (-0.699, -0.0449], ..., (0.642, 4.066], (-0.0449, 0.642], (0.642, 4.066], (0.642, 4.066], (0.642, 4.066]]
Length: 1000
Categories (4, interval[float64]): [(-3.288, -0.699] < (-0.699, -0.0449] < (-0.0449, 0.642] < (0.642, 4.066]]
pd.value_counts(cats3)
(0.642, 4.066]       250
(-0.0449, 0.642]     250
(-0.699, -0.0449]    250
(-3.288, -0.699]     250
dtype: int64
# 与cut类似。 也可以传递自定义的分位数
data = np.random.rand(1000)
cats4 = pd.qcut(data, [0, 0.1, 0.5, 0.9,1])

pd.value_counts(cats4)
(0.497, 0.899]         400
(0.0933, 0.497]        400
(0.899, 1.0]           100
(-0.000456, 0.0933]    100
dtype: int64
检测和过滤异常值

过滤或变换异常值(outlier)在很大程度上就是运用数组运算。来看一个含有正态分布数据的DataFrame:

data = pd.DataFrame(np.random.randn(1000,4))
data.describe()
0123
count1000.0000001000.0000001000.0000001000.000000
mean0.0255680.0050770.013687-0.018901
std0.9714721.0028940.9997600.988159
min-3.680465-3.521226-3.023962-2.680190
25%-0.630894-0.650209-0.628694-0.655666
50%0.032636-0.0105180.004974-0.027237
75%0.6830230.6761760.7204780.609793
max2.5985243.1420243.8818573.318166
# 查找某列绝对值大小超过3 的值

col = data[2]
col[np.abs(col)>3]
219    3.174416
653   -3.023962
768    3.881857
Name: 2, dtype: float64
# 选中绝对值大于3 的行

data[(np.abs(data) > 3).any(1)]
0123
112-0.004411-0.651096-0.7372303.318166
1240.6974703.1420240.9688510.829005
1800.150495-3.0591500.4959180.685093
219-0.031274-0.0242623.1744161.564903
615-0.059439-0.3193100.3126833.288938
6531.0151170.996914-3.0239621.096058
768-0.0212630.3157273.881857-0.832166
801-0.835077-3.5212260.3393331.658599
905-3.680465-1.1457940.807243-0.184747
929-0.3072623.0660030.8479770.586551
9560.950834-3.066277-0.098671-0.223054
# 根据这些条件就可以对值进行设置, 下面的代码可以将值限制在区间-3 与 3 之间

data[np.abs(data) > 3] = np.sign(data)*3
data.describe()
0123
count1000.0000001000.0000001000.0000001000.000000
mean0.0262480.0055150.012655-0.019508
std0.9691091.0001770.9961170.986207
min-3.000000-3.000000-3.000000-2.680190
25%-0.630894-0.650209-0.628694-0.655666
50%0.032636-0.0105180.004974-0.027237
75%0.6830230.6761760.7204780.609793
max2.5985243.0000003.0000003.000000
# 根据数据值得正负, np.sign() 可以生产 -1 和 1

np.sign(data).head()
0123
0-1.01.01.0-1.0
11.0-1.01.0-1.0
21.0-1.0-1.0-1.0
3-1.01.01.0-1.0
4-1.01.0-1.01.0
排列和随机采样

利用numpy.random.permutation函数可以轻松实现对Series或DataFrame的列的排列工作(permuting,随机重排序)。通过需要排列的轴的长度调用
permutation,可产生一个表示新顺序的整数数组:

df = pd.DataFrame(np.arange(20).reshape((5,4)))
sampler = np.random.permutation(5)
sampler
array([0, 2, 3, 1, 4])
df
0123
00123
14567
2891011
312131415
416171819
df.take(sampler)
0123
00123
2891011
312131415
14567
416171819
# 如果不想用替换的方式随机选取子集, 可以在Series 和 DataFrame上使用sample方法

df.sample(n = 3)
0123
00123
312131415
416171819
# 要通过替换的方式产生样本(允许重复选择), 可以传递replace = True到sample

choices = pd.Series([5,7,-1,6,4])
draws = choices.sample(n = 10, replace = True)
draws
1    7
1    7
4    4
3    6
3    6
3    6
1    7
0    5
0    5
4    4
dtype: int64
计算指标/哑变量

另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。

如果DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)。pandas有一个get_dummies函数可以实现该功能
(其实自己动手做一个也不难)。使用之前的一个DataFrame例子:

df = pd.DataFrame({'key' : ['b','b','a','c','a','b'], 'data1' : range(6)})
df
data1key
00b
11b
22a
33c
44a
55b
pd.get_dummies(df['key'])
abc
0010
1010
2100
3001
4100
5010
# 有时候,你可能想给指标DataFrame的列加上一个前缀,以便能够跟其他数据进行合并。get_dummies的prefix参数可以实现该功能:

dummies = pd.get_dummies(df['key'], prefix = 'key')
dummies
key_akey_bkey_c
0010
1010
2100
3001
4100
5010
df_with_dummy = df[['data1','key']].join(dummies)
df_with_dummy
data1keykey_akey_bkey_c
00b010
11b010
22a100
33c001
44a100
55b010

如果DataFrame中的某行同属于多个分类,则事情就会有点复杂。看一下MovieLens 1M数据集,14章会更深入地研究它:

In [114]: mnames = [‘movie_id’, ‘title’, ‘genres’]

In [115]: movies = pd.read_table(‘datasets/movielens/movies.dat’, sep=’::’,
…: header=None, names=mnames)

In [116]: movies[:10]
Out[116]:
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 [117]: all_genres = []

In [118]: for x in movies.genres:
…: all_genres.extend(x.split(’|’))

In [119]: genres = pd.unique(all_genres)
现在有:

In [120]: genres
Out[120]:
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 [121]: zero_matrix = np.zeros((len(movies), len(genres)))

In [122]: dummies = pd.DataFrame(zero_matrix, columns=genres)
现在,迭代每一部电影,并将dummies各行的条目设为1。要这么做,我们使用dummies.columns来计算每个类型的列索引:

In [123]: gen = movies.genres[0]

In [124]: gen.split(’|’)
Out[124]: [‘Animation’, “Children’s”, ‘Comedy’]

In [125]: dummies.columns.get_indexer(gen.split(’|’))
Out[125]: array([0, 1, 2])
然后,根据索引,使用.iloc设定值:

In [126]: for i, gen in enumerate(movies.genres):
…: indices = dummies.columns.get_indexer(gen.split(’|’))
…: dummies.iloc[i, indices] = 1
…:
然后,和以前一样,再将其与movies合并起来:

In [127]: movies_windic = movies.join(dummies.add_prefix(‘Genre_’))

In [128]: movies_windic.iloc[0]
Out[128]:
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

一个对统计应用有用的秘诀是:结合get_dummies和诸如cut之类的离散化函数:

In [129]: np.random.seed(12345)

In [130]: values = np.random.rand(10)

In [131]: values
Out[131]:
array([ 0.9296, 0.3164, 0.1839, 0.2046, 0.5677, 0.5955, 0.9645,
0.6532, 0.7489, 0.6536])

In [132]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]

In [133]: pd.get_dummies(pd.cut(values, bins))
Out[133]:
(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

7.3 字符串操作

Python能够成为流行的数据处理语言,部分原因是其简单易用的字符串和文本处理功能。大部分文本运算都直接做成了字符串对象的内置方法。
对于更为复杂的模式匹配和文本操作,则可能需要用到正则表达式。pandas对此进行了加强,它使你能够对整组数据应用字符串表达式和正则表达式,而且能处理烦人的缺失数据。

字符串对象方法

对于许多字符串处理和脚本应用,内置的字符串方法已经能够满足要求了。例如,以逗号分隔的字符串可以用split拆分成数段:

val = 'a,b,guido'
val.split(',')

['a', 'b', 'guido']
# strip 去除空白字符

pieces = [x.strip() for x in val.split(',')]
pieces
['a', 'b', 'guido']
# 利用+将这些字符串一双冒号分隔符的形式分开

first, second, third = pieces
first + '::' + second + '::' + third
'a::b::guido'
#但这种方式并不是很实用。一种更快更符合Python风格的方式是,向字符串"::"的join方法传入一个列表或元组:

'::'.join(pieces)
'a::b::guido'
#其它方法关注的是子串定位。检测子串的最佳方式是利用Python的in关键字,还可以使用index和find

'a' in val
True
val.index(',')
1
val.index(':')
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-115-2c016e7367ac> in <module>()
----> 1 val.index(':')


ValueError: substring not found
val.find(':')             # 找不到返回-1      ----------------  与 index 的区别
-1
val.find(',')  
1
# 统计出现的次数

val.count(',')
2
# replace 用于将指定模式替换为另一个模式

val.replace(',', ':')
'a:b:guido'
val.replace(',', '')
'abguido'
val.upper()
'A,B,GUIDO'
正则表达式

正则表达式提供了一种灵活的在文本中搜索或匹配(通常比前者复杂)字符串模式的方式。正则表达式,常称作regex,是根据正则表达式语言编写的字符串。
Python内置的re模块负责对字符串应用正则表达式。我将通过一些例子说明其使用方法。

笔记:正则表达式的编写技巧可以自成一章,超出了本书的范围。从网上和其它书可以找到许多非常不错的教程和参考资料。
re模块的函数可以分为三个大类:模式匹配、替换以及拆分。当然,它们之间是相辅相成的。一个regex描述了需要在文本中定位的一个模式,它可以用于许多
目的。我们先来看一个简单的例子:假设我想要拆分一个字符串,分隔符为数量不定的一组空白符(制表符、空格、换行符等)。描述一个或多个空白符的regex 是\s+:

import re
text = 'foo    bar\t baz     \tqux'
re.split('\s+', text)
['foo', 'bar', 'baz', 'qux']
#调用re.split('\s+',text)时,正则表达式会先被编译,然后再在text上调用其split方法。你可以用re.compile自己编译regex以得到一个可重用的
#regex对象:

import re
text = 'foo    bar\t baz     \tqux'
regex = re.compile('\s+')
regex.split(text)
['foo', 'bar', 'baz', 'qux']
# 如果只希望得到匹配regex的所有模式,则可以使用findall方法:

regex.findall(text)
['    ', '\t ', '     \t']
pandas的矢量化字符串函数

清理待分析的散乱数据时,常常需要做一些字符串规整化工作。更为复杂的情况是,含有字符串的列有时还含有缺失数据:

import pandas as pd
import numpy as np
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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值