数据清理与准备
一、处理缺失值
1.1 基本方法
缺失数据在pandas中呈现的⽅式有些不完美,但对于⼤多数⽤户可以保证功能正常。对于数值数据,pandas使⽤浮点值 NaN(Not a Number)表示缺失数据。我们称NaN为容易检测到的标识值。
string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
string_data
string_data.isnull()
在pandas中,我们采⽤了R语⾔中的惯⽤法,即将缺失值表示为 NA,它表示不可⽤not available。当进⾏数据清洗以进⾏分析时,最好直接对 缺失数据进⾏分析,以判断数据采集的问题或缺失数据可能导致的偏差。
Python内置的None值在对象数组中也可以作为NA:
string_data[0] = None
string_data.isnull()
~NA处理方法
1.2 过滤缺失值
过滤掉缺失数据的办法有很多种。你可以通过pandas.isnull或布尔索引的⼿⼯⽅法,但dropna可能会更实⽤⼀些。
from numpy import nan as NA
# Series的dropna()
data = pd.Series([1, NA, 3.5, NA, 7])
data.dropna()
#等价于:
data[data.notnull()]
# DataFrame的dropna()
data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA],
[NA, NA, NA], [NA, 6.5, 3.]])
#丢弃任何含有缺失值的行:
data.dropna()
#丢弃全为NA的那些行:
data.dropna(how='all')
#丢弃列
data[4] = NA
data.dropna(axis=1, how='all')
# 留下一部分观测数据:thresh参数
#实际上就是保留非缺失值的个数大于给定参数的行,其余的行丢弃掉
df = pd.DataFrame(np.random.randn(7, 3))
df.iloc[:4, 1] = NA
df.iloc[:2, 2] = NA
df.dropna(thresh=2) #即保留非缺失值个数大于2的行
# 判断哪些行/列存在缺失值
#哪些行
data.isnull().any(axis=1)
#哪些列
data.isnull().T.any(axis=1)
1.3 补全缺失值
为了补全缺失值,fillna⽅法是最主要的函数。通过⼀个常数调⽤fillna就会将缺失值替换为那个常数值:
# 填充缺失值
df.fillna(0)
# 不同列填充不同值
df.fillna({1: 0.5, 2: 0})
# fillna默认会返回新对象,但也可以对现有对象进⾏就地修改
_ = df.fillna(0, inplace=True)
# 插值方法
#对reindexing有效的那些插值⽅法也可⽤于fillna
df = pd.DataFrame(np.random.randn(6, 3))
df.iloc[2:, 1] = NA
df.iloc[4:, 2] = NA
df
df.fillna(method='ffill')
df.fillna(method='ffill', limit=2)
# 其他填充
data = pd.Series([1., NA, 3.5, NA, 7])
data.fillna(data.mean())
fillna函数的参数
二、数据转换
2.1 删除重复值
data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
'k2': [1, 1, 2, 3, 3, 4, 4]})
data
[Out]:
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4
DataFrame的duplicated⽅法返回⼀个布尔型Series,表示各⾏是否是重复⾏(前⾯出现过的⾏):
data.duplicated()
# 还有⼀个与此相关的drop_duplicates⽅法,它会返回⼀个DataFrame,重复的数组会被丢弃
data.drop_duplicates()
这两个⽅法默认会判断全部列,你也可以指定部分列进⾏重复项判断。假设我们还有⼀列值,且只希望根据k1列过滤重复项:
data['v1'] = range(7)
data.drop_duplicates(['k1'])
duplicated和drop_duplicates默认保留的是第⼀个出现的值组合。传⼊keep='last’则保留最后⼀个:
data.drop_duplicates(['k1', 'k2'], keep='last')
2.2 使用函数或映射进行数据转换
对于许多数据集,你可能希望根据数组、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
[Out]:
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()
data['animal'] = lowercased.map(meat_to_animal)
data
# 我们也可以传⼊⼀个能够完成全部这些⼯作的函数:
data['food'].map(lambda x: meat_to_animal[x.lower()])
使⽤map是⼀种实现元素级转换以及其他数据清理⼯作的便捷⽅式。
2.3 替代值
利⽤fillna⽅法填充缺失数据可以看做值替换的⼀种特殊情况。前⾯已经看到,map可⽤于修改对象的数据⼦集,⽽replace则提供了⼀种实现该功能的更简单、更灵活的⽅式。
data = pd.Series([1., -999., 2., -999., -1000., 3.])
data
# replace函数
data.replace(-999, np.nan)
# 一次性替换多个值
data.replace([-999, -1000], np.nan)
# 让每个值有不同的替换值
data.replace([-999, -1000], [np.nan, 0])
data.replace({-999: np.nan, -1000: 0})
注意:data.replace⽅法与data.str.replace不同,后者做的是字符串的元素级替换。我们会在后⾯学习Series的字符串⽅法。
2.4 重命名轴索引
跟Series中的值⼀样,轴标签也可以通过函数或映射进⾏转换,从⽽得到⼀个新的不同标签的对象。轴还可以被就地修改,⽽⽆需新建⼀个数据结构。
data = pd.DataFrame(np.arange(12).reshape((3, 4)),
index=['Ohio', 'Colorado', 'New York'],
columns=['one', 'two', 'three', 'four'])
data
[Out]:
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11
# 轴索引的map方法
transform = lambda x: x[:4].upper()
data.index.map(transform)
#赋值给index,这样就可以对DataFrame进⾏就地修改
data.index = data.index.map(transform)
# 如果想要创建数据集的转换版(⽽不是修改原始数据),可以使用rename函数:
data.rename(index=str.title, columns=str.upper)
# rename可以结合字典型对象实现对部分轴标签的更新:
data.rename(index={'OHIO': 'INDIANA'},
columns={'three': 'peekaboo'})
# rename函数默认赋值数据集,如果希望就地修改,可以传入inplace=True
data.rename(index={'OHIO': 'INDIANA'}, inplace=True)
2.5 离散化和分箱
连续值经常需要离散化,或者分离成“箱子”进行分析。假设有一组人的年龄数据:
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
# cut函数进行分箱
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)
cats
pandas返回的是⼀个特殊的Categorical对象。结果展示了 pandas.cut划分的箱。你可以将其看做⼀组表示箱名称的字符串。它的内部含有⼀个categories(类别)数组,它制定了不同类别名称以及codes属性中的ages(年龄)数据标签:
cats.codes
cats.categories
pd.value_counts(cats)
跟“区间”的数学符号⼀样,圆括号表示开端,⽽⽅括号则表示闭端(包括)。哪边是闭端可以通过right=False进⾏修改:
pd.cut(ages, [18, 26, 36, 61, 100], right=False)
你可以通过传递⼀个列表或数组到labels,设置自定义的箱名:
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
pd.cut(ages, bins, labels=group_names)
如果你传给cut整数个的箱来代替显式的箱边,pandas将根据数据中的最小值和最大值计算出等长等箱。考虑一些均匀分布的数据被切成四份的情况:
data = np.random.rand(20)
pd.cut(data, 4, precision=2)
#precision=2表示保留十进制精度限制在两位
qcut是一个与分箱密切相关的函数,它基于样本分位数进行分箱。取决于数据的分布,使用cut通常不会使每个箱具有相同数据量。由于qcut使用样本的分位数,你可以通过qcut获得等长的箱:
data = np.random.randn(1000) # Normally distributed
cats = pd.qcut(data, 4) # Cut into quartiles
cats
pd.value_counts(cats)
# 传入自定义的分位数:
pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
2.6 检测和过滤异常值
过滤或变换异常值(outlier)在很⼤程度上就是运⽤数组运算。来看⼀个含有正态分布数据的DataFrame:
data = pd.DataFrame(np.random.randn(1000, 4))
data.describe()
[Out]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.056269 0.043063 0.021109 -0.007811
std 0.993739 0.995920 1.008986 0.995057
min -3.428254 -3.645860 -3.184377 -3.745356
25% -0.747963 -0.599807 -0.642943 -0.697084
50% -0.091364 0.047101 -0.016127 -0.031732
75% 0.620197 0.738466 0.695298 0.692355
max 3.366626 2.653656 3.525865 2.735527
# 找出某列中绝对值大小超过3的值
col = data[2]
col[np.abs(col) > 3]
# 要选出全部含有“超过3或-3的值”的⾏,你可以在布尔型DataFrame中使⽤any⽅法:
data[(np.abs(data) > 3).any(1)]
# 根据这些条件,就可以对值进⾏设置。下⾯的代码可以将值限制在区间-3到3以内:
data[np.abs(data) > 3] = np.sign(data) * 3
data.describe()
# 根据数据的值是正还是负,np.sign(data)可以⽣成1和-1:
np.sign(data).head()
2.7 置换和随机抽样
利⽤numpy.random.permutation函数可以轻松实现对Series或DataFrame的列的排列⼯作(permuting,随机重排序)。通过需要排列的轴的⻓度调⽤permutation,可产⽣⼀个表示新顺序的整数数组:
df = pd.DataFrame(np.arange(5 * 4).reshape((5, 4)))
sampler = np.random.permutation(5)
sampler
# 整数数组可以用在基于iloc的索引或等价的take函数中:
df
df.take(sampler)
# 要选出一个不含有替代值的随机子集,可以使用Series和DataFrame的sample方法:
df.sample(n=3)
# 要通过替换的⽅式产⽣样本(允许重复选择),可以传递replace=True到sample:
choices = pd.Series([5, 7, -1, 6, 4])
draws = choices.sample(n=10, replace=True)
draws
2.8 计算指标/虚拟变量
将分类变量转换为“虚拟”或“指标”是另一种用于统计建模或机器学习的转换操作。如果DataFrame的某⼀列中含有k个不同的值,则可以派⽣出⼀个k列矩阵或DataFrame(其值全为1和0)。pandas有⼀个get_dummies函数可以实现该功能(其实⾃⼰动⼿做⼀个也不难)。使⽤之前的⼀个DataFrame例⼦:
df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
'data1': range(6)})
pd.get_dummies(df['key'])
# 给DataFrame的列加前缀:prefix参数
dummies = pd.get_dummies(df['key'], prefix='key')
df_with_dummy = df[['data1']].join(dummies)
df_with_dummy
如果DataFrame中的一行属于多个类别,则情况略微复杂:
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('datasets/movielens/movies.dat', sep='::',
header=None, names=mnames)
movies[:10]
[Out]:
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 Comedy
Part II (1995)
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添加指标变量就需要做⼀些数据处理。
# 首先,从数据集中提取出所有不同的流派的列表:
all_genres = []
for x in movies.genres:
all_genres.extend(x.split('|'))
genres = pd.unique(all_genres)
genres
[Out]:
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.
# 为了实现该功能,我们使用dummie.columns来计算每一个流派的列指标:
gen = movies.genres[0]
gen.split('|')
dummies.columns.get_indexer(gen.split('|'))
# 之后,使用.loc根据这些指标来设置值
for i, gen in enumerate(movies.genres):
indices = dummies.columns.get_indexer(gen.split('|'))
dummies.iloc[i, indices] = 1
# 上述循环与以下循环是等价的:
for i, gen in enumerate(movies.genres):
gen_list = gen.split('|')
for j in gen_list:
dummies[j].iloc[i] = 1
# 然后,和以前⼀样,再将其与movies合并起来:
movies_windic = movies.join(dummies.add_prefix('Genre_'))
movies_windic.iloc[0]
将get_dummies与cut等离散化函数结合使用是统计应用的一个有用方法:
np.random.seed(12345)
values = np.random.rand(10)
values
bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
pd.get_dummies(pd.cut(values, bins))
三、字符串操作
3.1 字符串对象方法
对于许多字符串处理和脚本应⽤,内置的字符串⽅法已经能够满⾜要求了。例如,以逗号分隔的字符串可以⽤split拆分成数段:
val = 'a,b, guido'
val.split(',')
split常常与strip⼀起使⽤,以去除空⽩符(包括换⾏符):
pieces = [x.strip() for x in val.split(',')]
pieces
利⽤加法,可以将这些⼦字符串以双冒号分隔符的形式连接起来:
first, second, third = pieces
first + '::' + second + '::' + third
#但这种⽅式并不是很实⽤。更常用的方式是利用join方法传入一个列表或元组
'::'.join(pieces)
检测⼦串的最佳⽅式是利⽤Python的in关键字,还可以使⽤index和find:
'guido' in val
val.index(',')
val.find(':')
#注意find和index的区别:如果找不到字符串,index将会引发⼀个异常(⽽不是返回-1)
与此相关,count可以返回指定⼦串的出现次数:
val.count(',')
replace⽤于将指定模式替换为另⼀个模式。通过传⼊空字符串,它也常常⽤于删除模式:
val.replace(',', '::')
val.replace(',', '')