利用Python进行数据分析2-数据清洗和准备
目录
在本文中,我会讨论处理缺失数据、重复数据、字符串操作和其它分析数据转换的⼯具。
处理缺失数据
在许多数据分析⼯作中,缺失数据是经常发⽣的。pandas的⽬标之⼀就是尽量轻松地处理缺失数据。例如,pandas对象的所有描述性统计默认都不包括缺失数据。
缺失数据在pandas中呈现的⽅式有些不完美,但对于⼤多数⽤户可以保证功能正常。对于数值数,pandas使⽤浮点值NaN(Not a Number)表示缺失数据。我们称其为哨兵值,可以⽅便的检测出来:
在pandas中,我们采⽤了R语⾔中的惯⽤法,即将缺失值表示为NA,它表示不可⽤not available。在统计应⽤中,NA数据可能是不存在的数据或者虽然存在,但是没有观察到(例如,数据采集中发⽣了问题)。当进⾏数据清洗以进⾏分析时,最好直接对缺失数据进⾏分析,以判断数据采集的问题或缺失数据可能导致的偏差。
Python内置的None值在对象数组中也可以作为NA:
pandas项⽬中还在不断优化内部细节以更好处理缺失数据,像⽤户API功能,例如pandas.isnull,去除了许多恼⼈的细节。下表列出了⼀些关于缺失数据处理的函数:
方法 | 说明 |
---|---|
dropna | 根据各标签的值中是否存在缺失数据对轴标签进行过滤,可通过阈值调节对缺失值的容忍度 |
fillna | 用指定值或插值方法(如ffill或bfill)填充缺失数据 |
isnull | 返回一个含有布尔值的对象,这些布尔值表示哪些值是缺失值/NA,该对象的类型与源类型一样 |
notnull | isnull的否定式 |
滤除缺失数据
过滤掉缺失数据的办法有很多种。你可以通过pandas.isnull或布尔索引的⼿⼯⽅法,但dropna可能会更实⽤⼀些。对于⼀个Series,dropna返回⼀个仅含⾮空数据和索引值的Series:
这等价于:
⽽对于DataFrame对象,事情就有点复杂了。你可能希望丢弃全NA或含有NA的⾏或列。dropna默认丢弃任何含有缺失值的⾏:
传⼊how='all’将只丢弃全为NA的那些⾏:
⽤这种⽅式丢弃列,只需传⼊axis=1即可:
另⼀个滤除DataFrame⾏的问题涉及时间序列数据。假设你只想留下⼀部分观测数据,可以⽤thresh参数实现此⽬的:
填充缺失数据
你可能不想滤除缺失数据(有可能会丢弃跟它有关的其他数据),⽽是希望通过其他⽅式填补那些“空洞”。对于⼤多数情况⽽⾔,fillna⽅法是最主要的函数。通过⼀个常数调⽤fillna就会将缺失值替换为那个常数值:
若是通过⼀个字典调⽤fillna,就可以实现对不同的列填充不同的值:
fillna默认会返回新对象,但也可以对现有对象进⾏就地修改:
对reindexing有效的那些插值⽅法也可⽤于fillna:
只要有些创新,你就可以利⽤fillna实现许多别的功能。⽐如说,你可以传⼊Series的平均值或中位数:
下表列出了fillna的参考:
参数 | 说明 |
---|---|
value | 用于填充缺失值的标量值成字典对象 |
method | 插值方式。如果舀数调用时未指定其他参数的话,默认为“ffill" |
axis | 待填充的轴,默认axis=0 |
inplace | 修改调用者对象而不产生副本 |
limit | (对于前向和后向填充)可以连续填究的最大数量 |
数据转换
下面介绍则是过滤、清理以及其他的转换⼯作。
移除重复数据
DataFrame中出现重复⾏有多种原因。下⾯就是⼀个例⼦:
DataFrame的duplicated⽅法返回⼀个布尔型Series,表示各⾏是否是重复⾏(前⾯出现过的⾏):
还有⼀个与此相关的drop_duplicates⽅法,它会返回⼀个DataFrame,重复的数组会标为False:
这两个⽅法默认会判断全部列,你也可以指定部分列进⾏重复项判断。假设我们还有⼀列值,且只希望根据k1列过滤重复项:
duplicated和drop_duplicates默认保留的是第⼀个出现的值组合。传⼊keep='last’则保留最后⼀个:
利⽤函数或映射进⾏数据转换
对于许多数据集,你可能希望根据数组、Series或DataFrame列中的值来实现转换⼯作。我们来看看下⾯这组有关⾁类的数据:
假设你想要添加⼀列表示该⾁类⻝物来源的动物类型。我们先编写⼀个不同⾁类到动物的映射:
Series的map⽅法可以接受⼀个函数或含有映射关系的字典型对象,但是这⾥有⼀个⼩问题,即有些⾁类的⾸字⺟⼤写了,⽽另⼀些则没有。因此,我们还需要使⽤Series的str.lower⽅法,将各个值转换为⼩写:
我们也可以传⼊⼀个能够完成全部这些⼯作的函数:
使⽤map是⼀种实现元素级转换以及其他数据清理⼯作的便捷⽅式。
替换值
利⽤fillna⽅法填充缺失数据可以看做值替换的⼀种特殊情况。前⾯已经看到,map可⽤于修改对象的数据⼦集,⽽replace则提供了⼀种实现该功能的更简单、更灵活的⽅式。我们来看看下⾯这Series:
-999这个值可能是⼀个表示缺失数据的标记值。要将其替换为pandas能够理解的NA值,我们可以利⽤replace来产⽣⼀个新的Series(除⾮传⼊inplace=True):
如果你希望⼀次性替换多个值,可以传⼊⼀个由待替换值组成的列表以及⼀个替换值:
要让每个值有不同的替换值,可以传递⼀个替换列表:
传⼊的参数也可以是字典:
PS:data.replace⽅法与data.str.replace不同,后者做的是字符串的元素级替换。
重命名轴索引
跟Series中的值⼀样,轴标签也可以通过函数或映射进⾏转换,从⽽得到⼀个新的不同标签的对象。轴还可以被就地修改,⽽⽆需新建⼀个数据结构。接下来看看下⾯这个简单的例⼦:
跟Series⼀样,轴索引也有⼀个map⽅法:
你可以将其赋值给index,这样就可以对DataFrame进⾏就地修改:
如果想要创建数据集的转换版(⽽不是修改原始数据),⽐较实⽤的⽅法是rename:
特别说明⼀下,rename可以结合字典型对象实现对部分轴标签的更新:
rename可以实现复制DataFrame并对其索引和列标签进⾏赋值。如果希望就地修改某个数据集,传⼊inplace=True即可:
离散化和⾯元划分
为了便于分析,连续数据常常被离散化或拆分为“⾯元”(bin)。假设有⼀组⼈员数据,⽽你希望将它们划分为不同的年龄组:
接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”⼏个⾯元。要实现该功能,你需要使⽤pandas的cut函数:
pandas返回的是⼀个特殊的Categorical对象。结果展示了pandas.cut划分的⾯元。你可以将其看做⼀组表示⾯元名称的字符串。它的底层含有⼀个表示不同分类名称的类型数组,以及⼀个codes属性中的年龄数据的标签:
pd.value_counts(cats)是pandas.cut结果的⾯元计数。
跟“区间”的数学符号⼀样,圆括号表示开端,⽽⽅括号则表示闭端(包括)。哪边是闭端可以通过right=False进⾏修改:
你可 以通过传递⼀个列表或数组到labels,设置⾃⼰的⾯元名称:
如果向cut传⼊的是⾯元的数量⽽不是确切的⾯元边界,则它会根据数据的最⼩值和最⼤值计算等⻓⾯元。下⾯这个例⼦中,我们将⼀些均匀分布的数据分成四组:
选项precision=2,限定⼩数只有两位。
qcut是⼀个⾮常类似于cut的函数,它可以根据样本分位数对数据进⾏⾯元划分。根据数据的分布情况,cut可能⽆法使各个⾯元中含有相同数量的数据点。⽽qcut由于使⽤的是样本分位数,因此可以得到⼤⼩基本相等的⾯元:
与cut类似,你也可以传递⾃定义的分位数(0到1之间的数值,包含端点):
稍后在讲解聚合和分组运算时会再次⽤到cut和qcut,因为这两个离散化函数对分位和分组分析⾮常重要。
检测和过滤异常值
过滤或变换异常值(outlier)在很⼤程度上就是运⽤数组运算。来看⼀个含有正态分布数据的DataFrame:
假设你想要找出某列中绝对值⼤⼩超过3的值:
要选出全部含有“超过3或-3的值”的⾏,你可以在布尔型DataFrame中使⽤any⽅法:
根据这些条件,就可以对值进⾏设置。下⾯的代码可以将值限制在区间-3到3以内:
根据数据的值是正还是负,np.sign(data)可以⽣成1和-1:
排列和随机采样
利⽤numpy.random.permutation函数可以轻松实现对Series或DataFrame的列的排列⼯作(permuting,随机重排序)。通过需要排列的轴的⻓度调⽤permutation,可产⽣⼀个表示新顺序的整数数组:
然后就可以在基于iloc的索引操作或take函数中使⽤该数组了:
如果不想⽤替换的⽅式选取随机⼦集,可以在Series和DataFrame上使⽤sample⽅法:
要通过替换的⽅式产⽣样本(允许重复选择),可以传递replace=True到sample:
计算指标/哑变量
另⼀种常⽤于统计建模或机器学习的转换⽅式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。如果DataFrame的某⼀列中含有k个不同的值,则可以派⽣出⼀个k列矩阵或DataFrame(其值全为1和0)。pandas有⼀个get_dummies函数可以实现该功能(其实⾃⼰动⼿做⼀个也不难)。使⽤之前的⼀个DataFrame:
有时候,你可能想给指标DataFrame的列加上⼀个前缀,以便能够跟其他数据进⾏合并。get_dummies的prefix参数可以实现该功能:
如果DataFrame中的某⾏同属于多个分类,则事情就会有点复杂。看⼀下MovieLens 1M数据集,后续会更深⼊地研究它:
要为每个genre添加指标变量就需要做⼀些数据规整操作。⾸先,我们从数据集中抽取出不同的genre值:
现在有:
构建指标DataFrame的⽅法之⼀是从⼀个全零DataFrame开始:
现在,迭代每⼀部电影,并将dummies各⾏的条⽬设为1。要这么做,我们使⽤dummies.columns来计算每个类型的列索引:
然后,根据索引,使⽤.iloc设定值:
然后,和以前⼀样,再将其与movies合并起来:
笔记:对于很⼤的数据,⽤这种⽅式构建多成员指标变量就会变得⾮常慢。最好使⽤更低级的函数,将其写⼊NumPy数组,然后结果包装在DataFrame中。
⼀个对统计应⽤有⽤的秘诀是:结合get_dummies和诸如cut之类的离散化函数:
我们⽤numpy.random.seed,使这个例⼦具有确定性。
字符串操作
Python能够成为流⾏的数据处理语⾔,部分原因是其简单易⽤的字符串和⽂本处理功能。⼤部分⽂本运算都直接做成了字符串对象的内置⽅法。对于更为复杂的模式匹配和⽂本操作,则可能需要⽤到正则表达式。pandas对此进⾏了加强,它使你能够对整组数据应⽤字符串表达式和正则表达式,⽽且能处理烦⼈的缺失数据。
字符串对象⽅法
对于许多字符串处理和脚本应⽤,内置的字符串⽅法已经能够满⾜要求了。例如,以逗号分隔的字符串可以⽤split拆分成数段:
split常常与strip⼀起使⽤,以去除空⽩符(包括换⾏符):
利⽤加法,可以将这些⼦字符串以双冒号分隔符的形式连接起来:
但这种⽅式并不是很实⽤。⼀种更快更符合Python⻛格的⽅式是向字符串"::"的join⽅法传⼊⼀个列表或元组:
其它⽅法关注的是⼦串定位。检测⼦串的最佳⽅式是利⽤Python的in关键字,还可以使⽤index和find:
注意find和index的区别:如果找不到字符串,index将会引发⼀个异常(⽽不是返回-1):
与此相关,count可以返回指定⼦串的出现次数:
replace⽤于将指定模式替换为另⼀个模式。通过传⼊空字符串,它也常常⽤于删除模式:
下表列出了Python内置的字符串⽅法。这些运算⼤部分都能使⽤正则表达式实现(⻢上就会看到)。
方法 | 说明 |
---|---|
count | 返回子串在字符串中的出现次数(非重叠) |
endswith, startswith | 如果字符串以某个后缀结尾(以某个前缀开头),则返回True |
join | 将字符串用作连接其他字符串序列的分隔符 |
index | 如果在字符串中找到子串,则返回子串第一个字符所在的位置。如果没有找到,则引发ValueError |
find | 如果在字符串中找到子串,则返回第一个发现的子串的第一个字符所在的位置。如果没有找到,则返回-1 |
rfind | 如果在字符串中找到子串,则返回最后一个发现的子串的第一个字符所在的位置。如果没有找到,则返回- 1 |
replace | 用另一个字符串替换指定子串 |
strip,rstrip, lstrip | 去除空白符(包括换行符)。相当于对各个元素执行x.strip((以及rstrip. Istrip) 。 |
split | 通过指定的分隔符将字符串拆分为一组子串 |
lower,upper | 分别将字母字符转换为小写或大写 |
ljust,rjust | 用空格(或其他字符)填充字符串的空白侧以返回符合最低宽度的字符串 |
casefold 将字符转换为⼩写,并将任何特定区域的变量字符组合转换成⼀个通⽤的可⽐较形式。
正则表达式
正则表达式提供了⼀种灵活的在⽂本中搜索或匹配(通常⽐前者复杂)字符串模式的⽅式。正则表达式,常称作regex,是根据正则表达式语⾔编写的字符串。Python内置的re模块负责对字符串应⽤则表达式。我将通过⼀些例⼦说明其使⽤⽅法。
笔记:正则表达式的编写技巧可以⾃成⼀章。从⽹上和其它书可以找到许多⾮常不错的教程和参考资料。
re模块的函数可以分为三个⼤类:模式匹配、替换以及拆分。当然,它们之间是相辅相成的。⼀个regex描述了需要在⽂本中定位的⼀个模式,它可以⽤于许多⽬的。我们先来看⼀个简单的例⼦:假设我想要拆分⼀个字符串,分隔符为数量不定的⼀组空⽩符(制表符、空格、换⾏符等)。描述⼀个或多个空⽩符的regex是\s+:
调⽤re.split(’\s+’,text)时,正则表达式会先被编译,然后再在text上调⽤其split⽅法。你可以⽤re.compile⾃⼰编译regex以得到⼀个可重⽤的regex对象:
如果只希望得到匹配regex的所有模式,则可以使⽤findall⽅法:
笔记:如果想避免正则表达式中不需要的转义(\),则可以使⽤原始字符串字⾯量如r’C:\x’(也可以编写其等价式’C:\x’)。
若打算对许多字符串应⽤同⼀条正则表达式,强烈建议通过re.compile创建regex对象。这样将可以节省⼤量的CPU时间.match和search跟findall功能类似。findall返回的是字符串中所有的匹项,⽽search则只返回第⼀个匹配项。match更加严格,它只匹配字符串的⾸部。来看⼀个⼩例⼦,假设我们有⼀段⽂本以及⼀条能够识别⼤部分电⼦邮件地址的正则表达式:
对text使⽤findall将得到⼀组电⼦邮件地址:
search返回的是⽂本中第⼀个电⼦邮件地址(以特殊的匹配项对象形式返回)。对于上⾯那个regex,匹配项对象只能告诉我们模式在原字符串中的起始和结束位置:
regex.match则将返回None,因为它只匹配出现在字符串开头的模式:
相关的,sub⽅法可以将匹配到的模式替换为指定字符串,并返回所得到的新字符串:
假设你不仅想要找出电⼦邮件地址,还想将各个地址分成3个部分:⽤户名、域名以及域后缀。要实现此功能,只需将待分段的模式的各部分⽤圆括号包起来即可:
由这种修改过的正则表达式所产⽣的匹配项对象,可以通过其groups⽅法返回⼀个由模式各段组成的元组:
对于带有分组功能的模式,findall会返回⼀个元组列表:
sub还能通过诸如\1、\2之类的特殊符号访问各匹配项中的分组。符号\1对应第⼀个匹配的组,\2对应第⼆个匹配的组,以此类推:
Python中还有许多的正则表达式,但⼤部分都超出了《利用Python进行数据分析》的范围。下表是⼀个简要概括。
方法 | 说明 |
---|---|
findall,finditer | 返 回字符串中所有的非重叠匹配模式。findall返回的是由所有模式组成的列表,而finditer则通过 一个迭代器逐个返回 |
match | 从字符串起始位置匹配模式,还可以对模式各部分进行分组。如果匹配到模式,则返回一个匹配项对象,否则返回None |
search | 扫描整个字符串以匹配模式。如果找到则返回一个匹配项对象。跟match不同,其匹配项可以位于字符串的任意位置,而不仅仅是起始处 |
split | 根据找到的模式将字符串拆分为数段 |
sub,subn | 将字符串中所有的(sub) 或前n个(subn)模式替换为指定表达式。在替换字符串中可以通过\1. \2等符号表示各分组项 |
pandas的⽮量化字符串函数
清理待分析的散乱数据时,常常需要做⼀些字符串规整化⼯作。更为复杂的情况是,含有字符串的列有时还含有缺失数据:
通过data.map,所有字符串和正则表达式⽅法都能被应⽤于(传⼊lambda表达式或其他函数)各个值,但是如果存在NA(null)就会报错。为了解决这个问题,Series有⼀些能够跳过NA值的⾯向数组⽅法,进⾏字符串操作。通过Series的str属性即可访问这些⽅法。例如,我们可以通过str.contains检查各个电⼦邮件地址是否含有 “gmail”。
也可以使⽤正则表达式,还可以加上任意re选项(如IGNORECASE):
有两个办法可以实现⽮量化的元素获取操作:要么使⽤str.get,要么在str属性上使⽤索引:
要访问嵌⼊列表中的元素,我们可以传递索引到这两个函数中:《利用Python进行数据分析》原书中的代码运行错误:
原因是macthes不是字符串。
于是我自己改了一下:
你可以利⽤这种⽅法对字符串进⾏截取:
下表介绍了更多的pandas字符串⽅法。
方法 | 说明 |
---|---|
cat | 实现元素级的字符串连接操作,可指定分隔符 |
count | 返回表示个字符串是否含有指定模式的布尔型数组 |
extract | 使用带分组的正则表达式从字符串Series提取一个或多个字符 串,结果是一个DataFrame,每组有一列 |
endswith | 相当于对每个元素执行x.endswith(patterm) |
startswith | 相当于对每个元素执行x.startswithlpattern) |
findall | 计算各字符串的模式列表 |
get | 获取各元素的第i个字符 |
isalnum | 相当于内置的st.alnum |
isalpha | 相当于内置的strisalpha |
isdecimal | 相当于内置的strisdecimal |
isdigit | 相当于内置的strisdigit |
islower | 相当于内置的strislower |
isnumeric | 相当于内置的strinumeric |
isupper | 相当于内置的strisupper |
join | 根据指定的分隔符将Series中各元素的字符串连接起来 |
len | 计算各字符串的长度 |
lowe,upper | 转换大小写。相当于对各个元素执行x.lower()或x.upper() |
match | 根据指定的正则表达式对各个元素执行re.match,返回匹配的组为列表 |
pad | 在字符串的左边、右边或两边添加空白符 |
center | 相当于pad(side=‘bot’) |
repeat | 重复值。例如,s.st.repeat(3)相当于对各个字符串 执行x*3 |
replace | 用指定字符串替换找到的模式 |
slice | 对Series中的各个字符串进行子串截取 |
split | 根据分隔符或正则表达式对字符事进行拆分 |
strip | 去除两边的空白符,包括新行 |
rstrip | 去除右边的空白符 |
lstrip | 去除左边的空白符 |
PS:有代码需要的可以在评论区就要或者私信我