编辑推荐: |
本文来自于微信公众号:Python数据科学,文章主要讲解了怎么删除DataFrame的列,改变DataFrame的索引,数据清洗得从简单得字段到清洗整个数据集等等。 |
|
数据科学家花了大量的时间清洗数据集,并将这些数据转换为他们可以处理的格式。事实上,很多数据科学家声称开始获取和清洗数据的工作量要占整个工作的80%。
因此,如果你正巧也在这个领域中,或者计划进入这个领域,那么处理这些杂乱不规则数据是非常重要的,这些杂乱数据包括一些缺失值,不连续格式,错误记录,或者是没有意义的异常值。
在这个教程中,我们将利用Python的Pandas和Numpy包来进行数据清洗。
主要内容如下:
删除 DataFrame 中的不必要 columns
改变 DataFrame 的 index
使用 .str() 方法来清洗 columns
使用 DataFrame.applymap() 函数按元素的清洗整个数据集
重命名 columns 为一组更易识别的标签
滤除 CSV文件中不必要的 rows
下面是要用到的数据集:
BL-Flickr-Images-Book.csv - 一份来自英国图书馆包含关于书籍信息的CSV文档
university_towns.txt - 一份包含美国各大洲大学城名称的text文档
olympics.csv - 一份总结了各国家参加夏季与冬季奥林匹克运动会情况的CSV文档
你可以从Real Python 的 GitHub repository 下载数据集来进行下面的例子。
注意:建议使用Jupter Notebooks来学习下面的知识。
学习之前假设你已经有了对Pandas和Numpy库的基本认识,包括Pandas的工作基础Series和DataFrame对象,应用到这些对象上的常用方法,以及熟悉了NumPy的NaN值。
让我们导入这些模块开始我们的学习。
>>> import
pandas as pd
>>> import numpy as np |
删除DataFrame的列
经常的,你会发现数据集中不是所有的字段类型都是有用的。例如,你可能有一个关于学生信息的数据集,包含姓名,分数,标准,父母姓名,住址等具体信息,但是你只想分析学生的分数。
这个情况下,住址或者父母姓名信息对你来说就不是很重要。这些没有用的信息会占用不必要的空间,并会使运行时间减慢。
Pandas提供了一个非常便捷的方法drop()函数来移除一个DataFrame中不想要的行或列。让我们看一个简单的例子如何从DataFrame中移除列。
首先,我们引入BL-Flickr-Images-Book.csv文件,并创建一个此文件的DataFrame。在下面这个例子中,我们设置了一个pd.read_csv的相对路径,意味着所有的数据集都在Datasets文件夹下的当前工作目录中:
我们使用了head()方法得到了前五个行信息,这些列提供了对图书馆有帮助的辅助信息,但是并不能很好的描述这些书籍:Edition
Statement, Corporate Author, Corporate Contributors,
Former owner, Engraver, Issuance type and Shelfmarks。
因此,我们可以用下面的方法移除这些列:
>>> to_drop
= ['Edition Statement',
... 'Corporate Author',
... 'Corporate Contributors',
... 'Former owner',
... 'Engraver',
... 'Contributors',
... 'Issuance type',
... 'Shelfmarks']
>>> df.drop(to_drop, inplace=True,
axis=1) |
在上面,我们定义了一个包含我们不要的列的名称列表。接着,我们在对象上调用drop()函数,其中inplace参数是True,axis参数是1。这告诉了Pandas我们想要直接在我们的对象上发生改变,并且它应该可以寻找对象中被移除列的信息。
我们再次看一下DataFrame,我们会看到不要想的信息已经被移除了。
同样的,我们也可以通过给columns参数赋值直接移除列,而就不用分别定义to_drop列表和axis了。
>>> df.drop(columns=to_drop,
inplace=True) |
这种语法更直观更可读。我们这里将要做什么就很明显了。
改变DataFrame的索引
Pandas索引index扩展了Numpy数组的功能,以允许更多多样化的切分和标记。在很多情况下,使用唯一的值作为索引值识别数据字段是非常有帮助的。
例如,仍然使用上一节的数据集,可以想象当一个图书管理员寻找一个记录,他们也许会输入一个唯一标识来定位一本书。
>>>
df['Identifier'].is_unique
True |
让我们用set_index把已经存在的索引改为这个列。
技术细节:不像在SQL中的主键一样,pandas的索引不保证唯一性,尽管许多索引和合并操作将会使运行时间变长如果是这样。
我们可以用一个直接的方法loc[]来获取每一条记录。尽管loc[]这个词可能看上去没有那么直观,但它允许我们使用基于标签的索引,这个索引是行的标签或者不考虑位置的记录。
换句话说,206是索引的第一个标签。如果想通过位置获取它,我们可以使用df.iloc[0],是一个基于位置的索引。
之前,我们的索引是一个范围索引:从0开始的整数,类似Python的内建range。通过给set_index一个列名,我们就把索引变成了Identifier中的值。
你也许注意到了我们通过df = df.set_index(...)的返回变量重新给对象赋了值。这是因为,默认的情况下,这个方法返回一个被改变对象的拷贝,并且它不会直接对原对象做任何改变。我们可以通过设置参数inplace来避免这个问题。
df.set_index('Identifier',
inplace=True) |
清洗数据字段
到现在为止,我们移除了不必要的列并改变了我们的索引变得更有意义。这个部分,我们将清洗特殊的列,并使它们变成统一的格式,这样可以更好的理解数据集和加强连续性。特别的,我们将清洗Date
of Publication和Place of Publication。
根据上面观察,所有的数据类型都是现在的objectdtype类型,差不多类似于Python中的str。
它包含了一些不能被适用于数值或是分类的数据。这也正常,因为我们正在处理这些初始值就是杂乱无章字符串的数据。
>>>
df.get_dtype_counts()
object 6 |
一个需要被改变为数值的的字段是the date of publication所以我们做如下操作:
>>>
df.loc[1905:, 'Date of Publication'].head(10)
Identifier
1905 1888
1929 1839, 38-54
2836 [1897?]
2854 1865
2956 1860-63
2957 1873
3017 1866
3131 1899
4598 1814
4884 1820
Name: Date of Publication, dtype: object |
一本书只能有一个出版日期data of publication。因此,我们需要做以下的一些事情:
移除在方括号内的额外日期,任何存在的:1879[1878]。
将日期范围转化为它们的起始日期,任何存在的:1860-63;1839,38-54。
完全移除我们不关心的日期,并用Numpy的NaN替换:[1879?]。
将字符串nan转化为Numpy的NaN值。
考虑这些模式,我们可以用一个简单的正则表达式来提取出版日期:
上面正则表达式的意思在字符串开头寻找任何四位数字,符合我们的情况。
\d代表任何数字,{4}重复这个规则四次。^符号匹配一个字符串最开始的部分,圆括号表示一个分组,提示pandas我们想要提取正则表达式的部分。
让我们看看运行这个正则在数据集上之后会发生什么。
>>>
extr = df['Date of Publication'].str.extract(r'^(\d{4})',
expand=False)
>>> extr.head()
Identifier
206 1879
216 1868
218 1869
472 1851
480 1857
Name: Date of Publication, dtype: object |
其实这个列仍然是一个object类型,但是我们可以使用pd.to_numeric轻松的得到数字的版本:
>>>
df['Date of Publication'] = pd.to_numeric(extr)
>>> df['Date of Publication'].dtype
dtype('float64') |
这个结果中,10个值里大约有1个值缺失,这让我们付出了很小的代价来对剩余有效的值做计算。
>>>
df['Date of Publication'].isnull().sum() / len(df)
0.11717147339205986 |
结合str方法与Numpy清洗列
上面,你可以观察到df['Date of Publication'].str. 的使用。这个属性是pandas里的一种提升字符串操作速度的方法,并有大量的Python字符串或编译的正则表达式上的小操作,例如.split(),.replace(),和.capitalize()。
为了清洗Place of Publication字段,我们可以结合pandas的str方法和numpy的np.where函数配合完成。
它的语法如下:
>>> np.where(condition,
then, else) |
这里,condition可以使一个类数组的对象,也可以是一个布尔表达。如果condition值为真,那么then将被使用,否则使用else。
它也可以组网使用,允许我们基于多个条件进行计算。
>>>
np.where(condition1, x1,
np.where(condition2, x2,
np.where(condition3, x3, ...))) |
我们将使用这两个方程来清洗Place of Publication由于这列有字符串对象。以下是这个列的内容:
我们看到,对于一些行,place of publication还被一些其它没有用的信息围绕着。如果我们看更多的值,我们发现这种情况中有些行
让我们看看两个特殊的:
这两本书在同一个地方出版,但是一个有连字符,另一个没有。
为了一次性清洗这个列,我们使用str.contains()来获取一个布尔值。
我们清洗的列如下:
>>>
pub = df['Place of Publication']
>>> london = pub.str.contains('London')
>>> london[:5]
Identifier
206 True
216 True
218 True
472 True
480 True
Name: Place of Publication, dtype: bool
>>> oxford = pub.str.contains('Oxford')
|
我们将它与np.where结合。
df['Place of
Publication'] = np.where(london, 'London',
np.where(oxford, 'Oxford',
pub.str.replace('-', ' ')))
>>> df['Place of Publication'].head()
Identifier
206 London
216 London
218 London
472 London
480 London
Name: Place of Publication, dtype: object |
这里,np.where方程在一个嵌套的结构中被调用,condition是一个通过st.contains()得到的布尔的Series。contains()方法与Python内建的in关键字一样,用于发现一个个体是否发生在一个迭代器中。
使用的替代物是一个代表我们期望的出版社地址字符串。我们也使用str.replace()将连字符替换为空格,然后给DataFrame中的列重新赋值。
尽管数据集中还有更多的不干净数据,但是我们现在仅讨论这两列。
让我们看看前五行,现在看起来比我们刚开始的时候好点了。
在这一点上,Place of Publication就是一个很好的需要被转换成分类数据的类型,因为我们可以用整数将这相当小的唯一城市集编码。(分类数据的使用内存与分类的数量以及数据的长度成正比)
使用applymap方法清洗整个数据集
在一定的情况下,你将看到并不是仅仅有一条列不干净,而是更多的。
在一些实例中,使用一个定制的函数到DataFrame的每一个元素将会是很有帮助的。pandas的applyma()方法与内建的map()函数相似,并且简单的应用到一个DataFrame中的所有元素上。
让我们看一个例子。我们将基于"university_towns.txt"文件创建一个DataFrame。
$ head Datasets/univerisity_towns.txt
Alabama[edit]
Auburn (Auburn University)[1]
Florence (University of North Alabama)
Jacksonville (Jacksonville State University)[2]
Livingston (University of West Alabama)[2]
Montevallo (University of Montevallo)[2]
Troy (Troy University)[2]
Tuscaloosa (University of Alabama, Stillman College,
Shelton State)[3][4]
Tuskegee (Tuskegee University)[5]
Alaska[edit] |
我们可以看到每个state后边都有一些在那个state的大学城:StateA TownA1 TownA2
StateB TownB1 TownB2...。如果我们仔细观察state名字的写法,我们会发现它们都有"[edit]"的自字符串。
我们可以利用这个特征创建一个含有(state,city)元组的列表,并将这个列表嵌入到DdataFrame中,
>>>
university_towns = []
>>> with open('Datasets/university_towns.txt')
as file:
... for line in file:
... if '[edit]' in line:
... # Remember this `state` until the next is
found
... state = line
... else:
... # Otherwise, we have a city; keep `state`
as last-seen
... university_towns.append((state, line))
>>> university_towns[:5]
[('Alabama[edit]\n', 'Auburn (Auburn University)[1]\n'),
('Alabama[edit]\n', 'Florence (University of North
Alabama)\n'),
('Alabama[edit]\n', 'Jacksonville (Jacksonville
State University)[2]\n'),
('Alabama[edit]\n', 'Livingston (University of
West Alabama)[2]\n'),
('Alabama[edit]\n', 'Montevallo (University of
Montevallo)[2]\n')] |
我们可以在DataFrame中包装这个列表,并设列名为"State"和"RegionName"。pandas将会使用列表中的每个元素,然后设置State到左边的列,RegionName到右边的列。
最终的DataFrame是这样的:
我们可以像上面使用for loop来进行清洗,但是pandas提供了更简单的办法。我们只需要state
name和town name,然后就可以移除所以其他的了。这里我们可以再次使用pandas的.str()方法,同时我们也可以使用applymap()将一个python
callable映射到DataFrame中的每个元素上。
我们一直在使用"元素"这个摄于,但是我们到底是什么意思呢?看看下面这个"toy"的DataFrame:
在这个例子中,每个单元 (‘Mock’, ‘Dataset’, ‘Python’,
‘Pandas’, etc.) 都是一个元素。因此,applymap()将分别应用一个函数到这些元素上。让我们定义这个函数。
pandas的applymap()只用一个参数,就是要应用到每个元素上的函数(callable)。
>>>
towns_df = towns_df.applymap(get_citystate) |
首先,我们定义一个函数,它将从DataFrame中获取每一个元素作为自己的参数。在这个函数中,检验元素中是否有一个(或者[。
基于上面的检查,函数返回相应的值。最后,applymap()函数被用在我们的对象上。现在DataFrame就看起来更干静了。
applymap()方法从DataFrame中提取每个元素,传递到函数中,然后覆盖原来的值。就是这么简单!
技术细节:虽然.applymap是一个方便和灵活的方法,但是对于大的数据集它将会花费很长时间运行,因为它需要将python
callable应用到每个元素上。一些情况中,使用Cython或者NumPY的向量化的操作会更高效。
重命名列和移除行
经常的,你处理的数据集会有让你不太容易理解的列名,或者在头几行或最后几行有一些不重要的信息,例如术语定义,或是附注。
这种情况下,我们想重新命名列和移除一定的行以让我们只留下正确和有意义的信息。
为了证明我们如何处理它,我们先看一下"olympics.csv"数据集的头5行:
$ head -n 5
Datasets/olympics.csv
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
,? Summer,01 !,02 !,03 !,Total,? Winter,01 !,02
!,03 !,Total,? Games,01 !,02 !,03 !,Combined total
Afghanistan (AFG),13,0,0,2,2,0,0,0,0,0,13,0,0,2,2
Algeria (ALG),12,5,2,8,15,3,0,0,0,0,15,5,2,8,15
Argentina (ARG),23,18,24,28,70,18,0,0,0,0,41,18,24,28,70
|
现在我们将它读入pandas的DataFrame。
这的确有点乱!列名是以整数的字符串形式索引的,以0开始。本应该是列名的行却处在olympics_df.iloc[0]。发生这个是因为CSV文件以0,
1, 2, …, 15起始的。
同样,如果我们去数据集的源文件观察,上面的NaN真的应该是像"Country"这样的,?
Summer应该代表"Summer Games", 而01 !应该是"Gold"之类的。
因此,我们需要做两件事:
移除第一行并设置header为第一行
重新命名列
当我们读CSV文件的时候,可以通过传递一些参数到read_csv函数来移除行和设置列名称。
这个函数有很多可选桉树,但是这里我们只需要header
来移除第0行:
我们现在有了设置为header的正确行,并且所有没用的行都被移除了。记录一下pandas是如何将包含国家的列名NaN改变为Unnamed:0的。
为了重命名列,我们将使用DataFrame的rename()方法,允许你以一个映射(这里是一个字典)重新标记一个轴。
让我们开始定义一个字典来将现在的列名称(键)映射到更多的可用列名称(字典的值)。
我们在对象上调用rename()函数:
>>>
olympics_df.rename (columns=new_names, inplace=True)
|
设置inplace为True可以让我们的改变直接反映在对象上。让我们看看是否正确:
Python数据清洗:回顾
这个教程中,你学会了从数据集中如何使用drop()函数去除不必要的信息,也学会了如何为数据集设置索引,以让items可以被容易的找到。
更多的,你学会了如何使用.str()清洗对象字段,以及如何使用applymap对整个数据集清洗。最后,我们探索了如何移除CSV文件的行,并且使用rename()方法重命名列。
掌握数据清洗非常重要,因为它是数据科学的一个大的部分。你现在应该有了一个如何使用pandas和numpy进行数据清洗的基本理解了。
|