Python数据清洗80%的工作量,看这篇就够了

干净整洁的数据是后续进行研究和分析的基础。数据科学家们会花费大量的时间来清理数据集,毫不夸张地说,数据清洗会占据他们80%的工作时间,而真正用来分析数据的时间只占到20%左右。

所以,数据清洗到底是在清洗些什么?

通常来说,你所获取到的原始数据不能直接用来分析,因为它们会有各种各样的问题,如包含无效信息,列名不规范、格式不一致,存在重复值,缺失值,异常值等.....

本文会给大家介绍如何用Python中自带的PandasNumPy库进行数据清洗。在正式讲解之前,先简单介绍一下这两个非常好用的库。

Pandas的名称来自于Panel data和Python数据分析data analysis,是Python的一个数据分析包,最初由AQR Capital Management于2008年4月开发,被作为金融数据分析工具,为时间序列分析提供了很好的支持,并于2009年底开源出来。

NumPyNumeric Python的缩写,是Python的一种开源的数值计算扩展,可用来存储和处理大型矩阵matrix,比Python自身的嵌套列表结构要高效的多,提供了许多高级的数值编程工具,如:矩阵数据类型、矢量处理,以及精密的运算库,专为进行严格的数字处理而产生。

目录

一、了解数据

二、清洗数据

去除不需要的行、列

重新命名列

重新设置索引

用字符串操作规范列

用函数规范列

删除重复数据

填充缺失值


三、总结

【注】为了清晰直观地展示数据清洗操作,本文会用到几个不同的数据集,重点是方法的讲解。

【工具】Python 3

一、了解数据

拿到一个全新的数据集,应该从哪里入手?

没错,我们需要先了解数据,看看它长什么样子。这里用tushare.pro上面的日线行情数据进行展示,以浦发银行(600000.SH)为例。常用的方法和属性如下:

.head()

.tail()

.shape

.columns

.info()

.describe()

.value_counts()

首先,获取数据:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tushare as ts

pd.set_option('display.max_columns', 100)  # 设置显示数据的最大列数,防止出现省略号…,导致数据显示不全
pd.set_option('expand_frame_repr', False)  # 当列太多时不自动换行

pro = ts.pro_api()
df = pro.daily(ts_code='600000.SH', start_date='20190401', end_date='20190430')

.head() 查看前n行数据,默认值是5

df.head()Out[1]:      ts_code trade_date   open   high    low  close  pre_close  change  pct_chg         vol       amount0  600000.SH   20190430  11.70  12.09  11.70  11.97      11.48    0.49   4.2683  1234747.38  1466714.7101  600000.SH   20190429  11.35  11.54  11.34  11.48      11.32    0.16   1.4134   385869.38   442046.7272  600000.SH   20190426  11.43  11.56  11.28  11.32      11.54   -0.22  -1.9064   424695.81   485267.2613  600000.SH   20190425  11.56  11.69  11.48  11.54      11.62   -0.08  -0.6885   408761.29   473973.5274  600000.SH   20190424  11.76  11.77  11.51  11.62      11.70   -0.08  -0.6838   382011.08   444929.313

.tail() 查看后n行数据,默认值是5

df.tail()
Out[2]: 
      ts_code trade_date   open   high    low  close  pre_close  change  pct_chg        vol      amount
16  600000.SH   20190408  11.79  11.96  11.65  11.72      11.71    0.01   0.0854  778703.73  920513.531
17  600000.SH   20190404  11.55  11.71  11.54  11.71      11.50    0.21   1.8261  752325.27  876099.547
18  600000.SH   20190403  11.37  11.54  11.34  11.50      11.44    0.06   0.5245  502710.29  575799.446
19  600000.SH   20190402  11.50  11.52  11.41  11.44      11.44    0.00   0.0000  467147.10  534896.810
20  600000.SH   20190401  11.36  11.52  11.29  11.44      11.28    0.16   1.4184  706374.05  808657.530

.shape 查看数据维数

df.shapeOut[3]: (21, 11)

.columns 查看所有列名

df.columns
Out[4]: 
Index(['ts_code', 'trade_date', 'open', 'high', 'low', 'close', 'pre_close',
       'change', 'pct_chg', 'vol', 'amount'],
      dtype='object')

.info() 查看索引、数据类型和内存信息

df.info()<class 'pandas.core.frame.DataFrame'>RangeIndex: 21 entries, 0 to 20Data columns (total 11 columns):ts_code       21 non-null objecttrade_date    21 non-null objectopen          21 non-null float64high          21 non-null float64low           21 non-null float64close         21 non-null float64pre_close     21 non-null float64change        21 non-null float64pct_chg       21 non-null float64vol           21 non-null float64amount        21 non-null float64dtypes: float64(9), object(2)memory usage: 1.9+ KB

.describe() 查看每列数据的基本统计值,包括计数值、均值、标准差、最小最大值、1/4、1/2、3/4分位数。

df.describe()
Out[7]: 
            open       high        low      close  pre_close     change    pct_chg           vol        amount
count  21.000000  21.000000  21.000000  21.000000  21.000000  21.000000  21.000000  2.100000e+01  2.100000e+01
mean   11.630476  11.777619  11.524286  11.637143  11.604286   0.032857   0.296252  5.734931e+05  6.704836e+05
std     0.215348   0.228930   0.184840   0.207512   0.206799   0.193213   1.671099  2.333355e+05  2.792896e+05
min    11.350000  11.520000  11.280000  11.320000  11.280000  -0.300000  -2.497900  2.627369e+05  3.017520e+05
25%    11.470000  11.560000  11.410000  11.480000  11.470000  -0.060000  -0.519900  4.102754e+05  4.739735e+05
50%    11.560000  11.750000  11.480000  11.540000  11.540000   0.000000   0.000000  5.027103e+05  5.757994e+05
75%    11.760000  11.990000  11.650000  11.720000  11.710000   0.100000   0.839600  7.050917e+05  8.161270e+05
max    12.020000  12.200000  11.880000  12.010000  12.010000   0.490000   4.268300  1.234747e+06  1.466715e+06

.value_counts() 查看Series对象的唯一值和计数值

df['close'].value_counts(dropna=False)Out[8]: 11.48    211.47    211.71    211.54    211.91    211.44    211.72    111.95    111.70    111.32    111.49    112.01    111.62    111.50    111.97    1Name: close, dtype: int64

如果上面这些操作还不够直观的话,就作图看看,需要先导入Python可视化库matplotlib, 为了规范代码书写,统一写在了最前面。

直方图

df['close'].plot(kind='hist', rot=0)
plt.show()

箱型图



df.boxplot(column='close', by='ts_code', rot=0)plt.show()


散点图



df.plot(kind='scatter', x='close', y='pre_close', rot=0)
plt.show()

二、清洗数据

了解数据集之后,我们就可以开始对数据集进行清洗了,前面提到通常要处理的问题有包含无效信息,列名不规范、格式不一致,存在重复值,缺失值,异常值等,下面我们一个一个来看。

01


去除不需要的行、列

在分析一个数据集的时候,很多信息其实是用不到的,因此,需要去除不必要的行或列。这里以csv文件为例,在导入的时候就可以通过设置pd.read_csv()里面的参数来实现这个目的。

先来感受一下官方文档中给出的详细解释,里面的参数是相当的多,本文只介绍比较常用的几个,感兴趣的话,可以好好研究一下文档,这些参数还是非常好用的,能省去很多导入后整理的工作。

header】默认header=0,即将文件中的0行作为列名和数据的开头,但有时候0行的数据是无关的,我们想跳过0行,让1行作为数据的开头,可以通过将header设置为1来实现。

usecols】根据列的位置或名字,如[0,1,2]或[‘a’, ‘b’, ‘c’],选出特定的列。

nrows】要导入的数据行数,在数据量很大、但只想导入其中一部分时使用。

获取数据:

从NYC OpenData网站下载csv格式原始数据

数据样本如下:

导入数据,只选取前100行和特定几列。

subset_columns = ['Job #', 'Doc #', 'Borough', 'Initial Cost', 'Total Est. Fee']df = pd.read_csv('文件路径', nrows=100, usecols=subset_columns)df.head()Out[15]:        Job #  Doc #   Borough Initial Cost Total Est. Fee0  420291794      1    QUEENS     $2000.00        $100.001  420291801      1    QUEENS    $15000.00        $151.502  340644128      1  BROOKLYN    $44726.00        $234.003  421685439      1    QUEENS        $0.00        $243.004  421677974      2    QUEENS   $105000.00       $1275.60

再看一下将header设置为1的效果,但这里其实不需要这么做,因为0行数据是有用的。

df = pd.read_csv('文件路径', nrows=100, header=1)
df.head()
Out[15]: 
0  420291794      1    QUEENS     $2000.00        $100.00
1  420291801      1    QUEENS    $15000.00        $151.50
2  340644128      1  BROOKLYN    $44726.00        $234.00
3  421685439      1    QUEENS        $0.00        $243.00
4  421677974      2    QUEENS   $105000.00       $1275.60

如果在数据导入之后,还想删除某些行和列,可以用 .drop() 方法。

先创建一个列表list,把不需要的列名放进去,再调用.drop() 方法,参数axis1时代表列,为0时代表行,参数inplace=True表示不创建新的对象,直接对原始对象进行修改。这里我们删除前两列。

to_drop = ['Job #', 'Doc #']df.drop(to_drop, axis=1, inplace=True)df.head()Out[22]:     Borough Initial Cost Total Est. Fee0    QUEENS     $2000.00        $100.001    QUEENS    $15000.00        $151.502  BROOKLYN    $44726.00        $234.003    QUEENS        $0.00        $243.004    QUEENS   $105000.00       $1275.60

02


重新命名列

当原始数据的列名不好理解,或者不够简洁时,可以用.rename()方法进行修改。这里我们把英文的列名改成中文,先创建一个字典,把要修改的列名定义好,然后调用rename()方法。

new_names = {'Borough': '区', 'Initial Cost': '初始成本', 'Total Est. Fee': '总附加费用'}
df.rename(columns=new_names, inplace=True)
df.head()
Out[23]: 
          区        初始成本     总附加费用
0    QUEENS    $2000.00   $100.00
1    QUEENS   $15000.00   $151.50
2  BROOKLYN   $44726.00   $234.00
3    QUEENS       $0.00   $243.00
4    QUEENS  $105000.00  $1275.60

03


重新设置索引

数据默认的索引是从0开始的有序整数,但如果想把某一列设置为新的索引,可以用.set_index()方法实现,在示例中我们把'区'这列设置为新索引。

df.set_index('区', inplace=True)df.head()Out[24]:                 初始成本     总附加费用区                             QUEENS      $2000.00   $100.00QUEENS     $15000.00   $151.50BROOKLYN   $44726.00   $234.00QUEENS         $0.00   $243.00QUEENS    $105000.00  $1275.60

04


用字符串操作规范列

字符串str操作是非常实用的,因为列中总是会包含不必要的字符,常用的方法如下:

lower()

upper()

capitalize()

replace()

strip()

split()

get()

contains()

find()

str.lower() 是把大写转换成小写,同理,str.upper()是把小写转换成大写,将示例中用大写字母表示的索引转换成小写,效果如下:

df.index = df.index.str.lower()
df.head()
Out[25]: 
                初始成本     总附加费用
区                             
queens      $2000.00   $100.00
queens     $15000.00   $151.50
brooklyn   $44726.00   $234.00
queens         $0.00   $243.00
queens    $105000.00  $1275.60

str.capitalize() 设置首字母大写

df.index = df.index.str.capitalize()df.head()Out[26]:                 初始成本     总附加费用区                             Queens      $2000.00   $100.00Queens     $15000.00   $151.50Brooklyn   $44726.00   $234.00Queens         $0.00   $243.00Queens    $105000.00  $1275.60

str.replace('$', '') 替换特定字符。这里把列中的美元符号$去掉,替换成空字符。

df['初始成本'] = df['初始成本'].str.replace('$', '')
df['总附加费用'] = df['总附加费用'].str.replace('$', '')
df.head()
Out[27]: 
               初始成本    总附加费用
区                           
Queens      2000.00   100.00
Queens     15000.00   151.50
Brooklyn   44726.00   234.00
Queens         0.00   243.00
Queens    105000.00  1275.60

str.strip() 去除字符串中的头尾空格、以及\n \t

df['初始成本'] = '   ' + df['初始成本']df['初始成本'][0]Out[28]: '   2000.00'

df['初始成本'] = df['初始成本'].str.strip()df['初始成本'][0]Out[29]: '2000.00'

str.split('x') 使用字符串中的'x'字符作为分隔符,将字符串分隔成列表。这里将列中的值以'.'进行分割,效果如下:

df['总附加费用'] = df['总附加费用'].str.split('.')
df.head()
Out[30]: 
               初始成本       总附加费用
区                              
Queens      2000.00   [100, 00]
Queens     15000.00   [151, 50]
Brooklyn   44726.00   [234, 00]
Queens         0.00   [243, 00]
Queens    105000.00  [1275, 60]

str.get() 选取列表中某个位置的值。接着上面分割后的结果,我们用str.get(0)取出列表中前一个位置的数值,生成新的一列“总附加费用_整数”,即取出金额中的整数部分。

df['总附加费用_整数'] = df['总附加费用'].str.get(0)df.head()Out[31]:                初始成本       总附加费用 总附加费用_整数区                                       Queens      2000.00   [100, 00]      100Queens     15000.00   [151, 50]      151Brooklyn   44726.00   [234, 00]      234Queens         0.00   [243, 00]      243Queens    105000.00  [1275, 60]     1275

str.contains() 判断是否存在某个字符,返回的是布尔值。这里判断一下'总附加费用_整数'列中是否包含字符'0'。

df['总附加费用_整数'].str.contains('0')
Out[33]: 

Queens            True
Queens           False
Brooklyn         False
Queens           False
Queens           False

str.find()检测字符串中是否包含子字符串str,如果是,则返回该子字符串开始位置的索引值。示例中的'0'字符最开始出现的位置是1。

df['总附加费用_整数'][0]Out[13]: '100'df['总附加费用_整数'][0].find('0')Out[14]: 1

学完基本的字符串操作方法,我们来看一下如何结合NumPy来提高字符串操作的效率。

获取数据,这里我们用一个新的数据集,下载链接如下,里面包含两个csv文件和一个txt文件:

https://github.com/realpython/python-data-cleaning

① BL-Flickr-Images-Book.csv

② olympics.csv

③ university_towns.txt

导入csv文件①,先观察一下'Place of Publication'这一列。

df = pd.read_csv('文件路径')
df['Place of Publication'].head(10)
Out[38]: 
0                                 London
1               London; Virtue & Yorston
2                                 London
3                                 London
4                                 London
5                                 London
6                                 London
7    pp. 40. G. Bryan & Co: Oxford, 1898
8                                London]
9                                 London
Name: Place of Publication, dtype: object

我们发现,这一列中的格式并不统一,比如1行中的London; Virtue & Yorston,London后面的部分我们不需要,还有7行的pp. 40. G. Bryan & Co: Oxford, 1898,有效信息只是Oxford。

再用.tail(10)方法观察这一列的最后十行:

df['Place of Publication'].tail(10)Out[39]: 8277               New York8278                 London8279               New York8280                 London8281    Newcastle-upon-Tyne8282                 London8283                  Derby8284                 London8285    Newcastle upon Tyne8286                 LondonName: Place of Publication, dtype: object

我们发现,8281行的Newcastle-upon-Tyne中间有连字符,但8285行却没有,这些都是要解决的格式不规范的问题。

为了清洗这一列,我们可以将Pandas中的.str()方法与NumPy的np.where函数相结合,np.where函数是Excel的IF()宏的矢量化形式,它的语法如下:

>>> np.where(condition, then, else)

如果condition条件为真,则执行then,否则执行else。这里的condition条件可以是一个类数组的对象,也可以是一个布尔表达式,我们也可以利用np.where函数嵌套多个条件进行矢量化计算和判断。

>>> np.where(condition1, x1,         np.where(condition2, x2,             np.where(condition3, x3, ...)))

下面的这个实例,就是同时嵌套两个条件解决上面提到的那两个字符串问题。思路是,如果字符串里面包含'London',就用'London'代替,这样可以去除其他冗余信息,否则,如果字符串里面包含'Oxford',则用'Oxford'代替,同时如果字符串里面包含符号'-',则用空格代替。

pub = df['Place of Publication']
london = pub.str.contains('London')
oxford = pub.str.contains('Oxford')

df['Place of Publication'] = np.where(london, 'London',
                                      np.where(oxford, 'Oxford',
                                               pub.str.replace('-', ' ')))

打印出前十行和后十行,结果如下,可以和整理前的数据进行对比。

df['Place of Publication'].head(10)Out[42]: 0    London1    London2    London3    London4    London5    London6    London7    Oxford8    London9    LondonName: Place of Publication, dtype: object
df['Place of Publication'].tail(10)
Out[43]: 
8277               New York
8278                 London
8279               New York
8280                 London
8281    Newcastle upon Tyne
8282                 London
8283                  Derby
8284                 London
8285    Newcastle upon Tyne
8286                 London
Name: Place of Publication, dtype: object

05


用函数规范列

在某些情况下,数据不规范的情况并不局限于某一列,而是更广泛地分布在整个表格中。因此,自定义函数并应用于整个表格中的每个元素会更加高效。用applymap()方法可以实现这个功能,它类似于内置的map()函数,只不过它是将函数应用于整个表格中的所有元素。

我们打开文件txt文件③,先观察一下数据:

$ 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]

观察发现,数据格式有如下特点:

州A[edit]

城市A(大学)

城市B(大学)

州B[edit]

城市A(大学)

城市B(大学)

......

我们可以利用这一数据格式,创建一个(州、市)元组列表,并将该列表转化成一个DataFrame。先创建一个列表,列表中包含州和城市(大学)信息。

university_towns = []
with open('D:/code/tushare interpret and tech team/python-data-cleaning-master/Datasets/university_towns.txt') as file:
    for line in file:
        if '[edit]' in line:  # 该行有[edit]
            state = line      # 将改行信息赋值给“州”,记住这个“州”,直到找到下一个为止
        else:
            university_towns.append((state, line))  # 否则,改行为城市信息,并且它们都属于上面的“州”

university_towns[:5]
Out[44]: 
[('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')]

pd.DataFrame()方法将这个列表转换成一个DataFrame,并将列设置为'State'和'RegionName'。Pandas将接受列表中的每个元素,并将元组左边的值传入'State'列,右边的值传入'RegionName'列。

towns_df = pd.DataFrame(university_towns, columns=['State', 'RegionName'])towns_df.head()Out[45]:              State                                         RegionName0  Alabama[edit]\n                    Auburn (Auburn University)[1]\n1  Alabama[edit]\n           Florence (University of North Alabama)\n2  Alabama[edit]\n  Jacksonville (Jacksonville State University)[2]\n3  Alabama[edit]\n       Livingston (University of West Alabama)[2]\n4  Alabama[edit]\n         Montevallo (University of Montevallo)[2]\n

接下来就要对列中的字符串进行整理,'State'列中的有效信息是州名,'RegionName'列中的有效信息是城市名,其他的字符都可以删掉。当然,除了用之前提到的利用循环和.str()方法相结合的方式进行操作,我们还可以选择用applymap()方法,它会将传入的函数作用于整个DataFrame所有行列中的每个元素。

先定义函数get_citystate(item),功能是只提取元素中的有效信息。

def get_citystate(item):
    if ' (' in item:
        return item[:item.find(' (')]
    elif '[' in item:
        return item[:item.find('[')]
    else:
        return item

然后,我们将这个函数传入applymap(),并应用于towns_df,结果如下:

towns_df = towns_df.applymap(get_citystate)towns_df.head()Out[48]:      State    RegionName0  Alabama        Auburn1  Alabama      Florence2  Alabama  Jacksonville3  Alabama    Livingston4  Alabama    Montevallo

现在towns_df表格看起来是不是干净多了!

06


删除重复数据

重复数据会消耗不必要的内存,在处理数据时执行不必要的计算,还会使分析结果出现偏差。因此,我们有必要学习如何删除重复数据。

先看一个来自DataCamp的数据集,调用info()方法打印出每列数据的具体信息和内存信息,共有24092行数据,内存占用量是753.0+ KB。

tracks = billboard[['year', 'artist', 'track', 'time']]
print(tracks.info())

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 24092 entries, 0 to 24091
Data columns (total 4 columns):
year      24092 non-null int64
artist    24092 non-null object
track     24092 non-null object
time      24092 non-null object
dtypes: int64(1), object(3)
memory usage: 753.0+ KB
None

下面调用.drop_duplicates()函数删除重复数据。

In [11]: tracks_no_duplicates = tracks.drop_duplicates()... print(tracks_no_duplicates.info())... <class 'pandas.core.frame.DataFrame'>Int64Index: 317 entries, 0 to 316Data columns (total 4 columns):year      317 non-null int64artist    317 non-null objecttrack     317 non-null objecttime      317 non-null objectdtypes: int64(1), object(3)memory usage: 12.4+ KBNone

删完之后我们发现,数据量减少到了317个,内存占用缩减至12.4+ KB。

07

填充缺失值

数据集中经常会存在缺失值,学会正确处理它们很重要,因为在计算的时候,有些无法处理缺失值,有些则在默认情况下跳过缺失值。而且,了解缺失的数据,并思考用什么值来填充它们,对做出无偏的数据分析至关重要。

同样是来自DataCamp的一个存在缺失值的数据集:

In [3]: airquality.head(10)
Out[3]: 
   Ozone  Solar.R  Wind  Temp  Month  Day
0   41.0    190.0   7.4    67      5    1
1   36.0    118.0   8.0    72      5    2
2   12.0    149.0  12.6    74      5    3
3   18.0    313.0  11.5    62      5    4
4    NaN      NaN  14.3    56      5    5
5   28.0      NaN  14.9    66      5    6
6   23.0    299.0   8.6    65      5    7
7   19.0     99.0  13.8    59      5    8
8    8.0     19.0  20.1    61      5    9
9    NaN    194.0   8.6    69      5   10

以'Ozone'列为例,我们可以调用fillna()函数,用该列的均值.mean()填充NaN值。

oz_mean = airquality.Ozone.mean()airquality['Ozone'] = airquality['Ozone'].fillna(oz_mean)print(airquality.head(10))

       Ozone  Solar.R  Wind  Temp  Month  Day0  41.000000    190.0   7.4    67      5    11  36.000000    118.0   8.0    72      5    22  12.000000    149.0  12.6    74      5    33  18.000000    313.0  11.5    62      5    44  43.195402      NaN  14.3    56      5    55  28.000000      NaN  14.9    66      5    66  23.000000    299.0   8.6    65      5    77  19.000000     99.0  13.8    59      5    88   8.000000     19.0  20.1    61      5    99  43.195402    194.0   8.6    69      5   10

三、总结

了解如何进行数据清洗非常重要,因为它是数据科学的重要组成部分。好在Python提供了非常好用的PandasNumPy库来帮助我们清理数据集,本文介绍的方法都是在实际中经常会用到的,希望大家能牢记于心。

(0)

相关推荐