概要
- 准备工作
- 检查数据
- 处理缺失数据
- 添加默认值
- 删除不完整的行
- 删除不完整的列
- 规范化数据类型
- 必要的转换
- 重命名列名
- 保存结果
- 更多资源
Pandas 是 Python 中很流行的类库,使用它可以进行数据科学计算和数据分。他可以联合其他数据科学计算工具一块儿使用,比如,SciPy,NumPy 和
Matplotlib,建模工程师可以通过创建端到端的分析工作流来解决业务问题。
虽然我们可以 Python 和数据分析做很多强大的事情,但是我们的分析结果的好坏依赖于数据的好坏。很多数据集存在数据缺失,或数据格式不统一(畸形数据),或错误数据的情况。不管是不完善的报表,还是技术处理数据的失当都会不可避免的引起“脏”数据。
庆幸的是,Pandas 提供功能强大的类库,不管数据处于什么状态,他可以帮助我们通过清洗数据,排序数据,最后得到清晰明了的数据。对于案例的数据,准备使用
movie_metadata.csv(链接: https://pan.baidu.com/s/1i673EsPLY2iSQWXwKO_E_w 密码: dvqqhttps://pan.baidu.com/s/1i5zUvOD)。这个数据集包含了很多信息,演员、导演、预算、总输入,以及 IMDB 评分和上映时间。实际上,可以使用上百万或者更大的数据库,但是,案例数据集对于开始入门还是很好的。
不幸的是,有一些列的值是缺失的,有些列的默认值是0,有的是 NaN(Nota Number)。
下面我们通过使用 Pandas 提供的功能来清洗“脏”数据
准备工作
首先,第一次使用 Pandas 之前,我们需要安装 Pandas。安装命令如下:
pip install pandas
接下来,导入 Pandas 到我们的代码中,代码如下:
#可以使用其他的别名, 但是,pd 是官方推荐的别名,也是大家习惯的别名
import pandas as pd
最后,加载数据集,代码如下:
data = pd.read_csv('../data/tmdb_5000_credits.csv')
注意,确保已经下载数据集,如果你的代码和数据集的存放结构与我的一样,直接运行就可以
否则,要根据实际的情况,修改 read_csv() 的文件路径
检查数据
检查一下我们刚刚读入数据的基本结构,Pandas 提供了 head() 方法打印输出前五行数据。目的是让我们对读入的数据有一个大致的了解。
data.head()
我们可以通过上面介绍的 Pandas 的方法查看数据,也可以通过传统的 Excel 程序查看数据,这个时候,我们可以开始记录数据上的问题,然后,我们再想办法解决问题。
Pandas 提供了一些选择的方法,这些选择的方法可以把数据切片,也可以把数据切块。下面我们简单介绍一下:
- 查看一列的一些基本统计信息:data.columnname.describe()
- 选择一列:data['columnname']
- 选择一列的前几行数据:data['columnsname'][:n]
- 选择多列:data[['column1','column2']]
- Where 条件过滤:data[data['columnname'] >
condition]
处理缺失数据
缺失数据是最常见的问题之一。产生这个问题可能的原因
- 从来没有填正确过
- 数据不可用
- 计算错误
无论什么原因,只要有空白值得存在,就会引起后续的数据分析的错误。下面介绍几个处理缺失数据的方法:
- 为缺失数据赋值默认值
- 去掉/删除缺失数据行
- 去掉/删除缺失率高的列
添加默认值
我们应该去掉那些不友好的 NaN 值。但是,我们应该用什么值替换呢?在这里,我们就应该稍微掌握一下数据。对于我们的例子,我们检查一下“country”列。这一列非常简单,然而有一些电影没有提供地区,所以有些数据的值是
NaN。在我们的案例中,我们推断地区并不是很重要,所以,我们可是使用“”空字符串或其他默认值。
data.country= data.country.fillna('')
上面,我们就将“country”整个列使用“”空字符串替换了,或者,我们也可以轻易地使用“None Given”这样的默认值进行替换。如果想了解更多 fillna() 的详细信息参考
pandas.DataFrame.fillna。
使用数字类型的数据,比如,电影的时长,计算像电影平均时长可以帮我们甚至是数据集。这并不是最优解,但这个持续时间是根据其他数据估算出来的。这样的方式下,就不会因为像 0 或者 NaN这样的值在我们分析的时候而抛错。
data.duration = data.duration.fillna(data.duration.mean())
删除不完整的行
假设我们想删除任何有缺失值得行。这种操作太据侵略性,但是我们可以根据我们的需要进行扩展。
删除任何包含 NA 值的行是很容的:
data.dropna()
当然,我们也可以删除一整行的值都为 NA:
data.dropna(how='all')
我们也可以增加一些限制,在一行中有多少非空值的数据是可以保留下来的(在下面的例子中,行数据中至少要有 5 个非空值)
data.drop(thresh=5)
比如说,我们不想要不知道电影上映时间的数据:
data.dropna(subset=['title_year'])
上面的 subset 参数允许我们选择想要检查的列。如果是多个列,可以使用列名的 list 作为参数。
删除不完整的列
我们可以上面的操作应用到列上。我们仅仅需要在代码上使用 axis=1 参数。这个意思就是操作列而不是行。(我们已经在行的例子中使用了 axis=0,因为如果我们不传参数 axis,默认是axis=0。)
删除一正列为 NA 的列:
data.drop(axis=1, how='all')
删除任何包含空值的列:
data.drop(axis=1. how='any')
这里也可以使用像上面一样的 threshold 和 subset,更多的详情和案例,请参考pandas.DataFrame.dropna。
规范化数据类型
有的时候,尤其当我们读取 csv 中一串数字的时候,有的时候数值类型的数字被读成字符串的数字,或将字符串的数字读成数据值类型的数字。Pandas 还是提供了规范化我们数据类型的方式:
data = pd.read_csv('../data/moive_metadata.csv', dtype={'duration': int})
这就是告诉 Pandas ‘duration’列的类型是数值类型。同样的,如果想把上映年读成字符串而不是数值类型,我们使用和上面类似的方法:
data = pd.read_csv('./data/moive_metadata.csv', dtype={'title_year':str})
注意,需要记住的是,再次从磁盘上读取 csv ,确保规范化了我们的数据类型,或者在读取之前已经保存了中间结果。
必要的变换
人工录入的数据可能都需要进行一些必要的变换。
- 错别字
- 英文单词时大小写的不统一
- 输入了额外的空格
将我们数据中所有的 movie_title 改成大写:
data['movie_title'].str.upper()
同样的,干掉末尾空格:
data['movie_title'].str.strip()
这里并没有介绍关于英文的拼写错误的问题,可以参考模糊匹配。
重命名列名
最终的数据可能是有计算机生成的,那么,列名有可能也是计算机按照一定计算规律生成的。这些列名对计算机没有什么,但是对于人来说可能就不够友好,这时候,我们就需要重命名成对人友好的列名,代码如下:
data,rename(columns = {‘title_year’:’release_date’, ‘movie_facebook_likes’:’facebook_likes’})
像上面这样,我们就完成了两个列的重命名。需要注意的是,这个方法并没有提供
inpalce 参数,我们需要将结果赋值给自己才可以:
data = data.rename(columns = {‘title_year’:’release_date’, ‘movie_facebook_likes’:’facebook_likes’})
保存结果
我们完成数据清洗之后,一般会把结果再以 csv 的格式保存下来,以便后续其他程序的处理。同样,Pandas 提供了非常易用的方法:
data.to_csv(‘cleanfile.csv’ encoding=’utf-8’)
更多资源
这次介绍仅仅是冰山一角。有很多方式可能造成数据集变“脏”或被破坏:
- 用户环境的不同、
- 所使用语言的差异
- 用户输入的差别
在这里,我介绍了 Python 用 Pandas 清洗数据最一般的方式。
概要
- 了解数据
- 分析数据问题
- 清洗数据
- 整合代码
了解数据
在处理任何数据之前,我们的第一任务是理解数据以及数据是干什么用的。我们尝试去理解数据的列/行、记录、数据格式、语义错误、缺失的条目以及错误的格式,这样我们就可以大概了解数据分析之前要做哪些“清理”工作。
本次我们需要一个 patient_heart_rate.csv (链接:https://pan.baidu.com/s/1geX8oYf 密码:odj0)的数据文件,这个数据很小,可以让我们一目了然。这个数据是 csv 格式。数据是描述不同个体在不同时间的心跳情况。数据的列信息包括人的年龄、体重、性别和不同时间的心率。
import pandas as pd df = pd.read_csv('../data/patient_heart_rate.csv') df.head()
分析数据问题
- 没有列头
- 一个列有多个参数
- 列数据的单位不统一
- 缺失值
- 空行
- 重复数据
- 非 ASCII 字符
- 有些列头应该是数据,而不应该是列名参数
清洗数据
下面我们就针对上面的问题一一击破。
1. 没有列头
如果我们拿到的数据像上面的数据一样没有列头,Pandas 在读取 csv 提供了自定义列头的参数。下面我们就通过手动设置列头参数来读取 csv,代码如下:
import pandas as pd # 增加列头 column_names= ['id', 'name', 'age', 'weight','m0006','m0612','m1218','f0006','f0612','f1218'] df = pd.read_csv('../data/patient_heart_rate.csv', names = column_names) df.head()
上面的结果展示了我们自定义的列头。我们只是在这次读取 csv 的时候,多了传了一个参数 names = column_names,这个就是告诉 Pandas 使用我们提供的列头。
2. 一个列有多个参数
在数据中不难发现,Name 列包含了两个参数 Firtname 和 Lastname。为了达到数据整洁目的,我们决定将 name 列拆分成 Firstname 和 Lastname
从技术角度,我们可以使用 split 方法,完成拆分工作。
我们使用 str.split(expand=True),将列表拆成新的列,再将原来的 Name 列删除
# 切分名字,删除源数据列 df[['first_name','last_name']] = df['name'].str.split(expand=True) df.drop('name', axis=1, inplace=True)
上面就是执行执行代码之后的结果。
3. 列数据的单位不统一
如果仔细观察数据集可以发现 Weight 列的单位不统一。有的单位是 kgs,有的单位是 lbs
# 获取 weight 数据列中单位为 lbs 的数据 rows_with_lbs = df['weight'].str.contains('lbs').fillna(False) df[rows_with_lbs]
为了解决这个问题,将单位统一,我们将单位是 lbs 的数据转换成 kgs。
# 将 lbs 的数据转换为 kgs 数据 for i,lbs_row in df[rows_with_lbs].iterrows(): weight = int(float(lbs_row['weight'][:-3])/2.2) df.at[i,'weight'] = '{}kgs'.format(weight)
4. 缺失值
在数据集中有些年龄、体重、心率是缺失的。我们又遇到了数据清洗最常见的问题——数据缺失。一般是因为没有收集到这些信息。我们可以咨询行业专家的意见。典型的处理缺失数据的方法:
- 删:删除数据缺失的记录(数据清洗- Pandas 清洗“脏”数据(一)/[数据清洗]-Pandas 清洗“脏”数据(一))
- 赝品:使用合法的初始值替换,数值类型可以使用 0,字符串可以使用空字符串“”
- 均值:使用当前列的均值
- 高频:使用当前列出现频率最高的数据
- 源头优化:如果能够和数据收集团队进行沟通,就共同排查问题,寻找解决方案。
5. 空行
仔细对比会发现我们的数据中一行空行,除了 index 之外,全部的值都是 NaN。
Pandas 的 read_csv() 并没有可选参数来忽略空行,这样,我们就需要在数据被读入之后再使用 dropna() 进行处理,删除空行.
# 删除全空的行 df.dropna(how='all',inplace=True)
6. 重复数据
有的时候数据集中会有一些重复的数据。在我们的数据集中也添加了重复的数据。
首先我们校验一下是否存在重复记录。如果存在重复记录,就使用 Pandas 提供的 drop_duplicates() 来删除重复数据。
# 删除重复数据行 df.drop_duplicates(['first_name','last_name'],inplace=True)
7. 非 ASCII 字符
在数据集中 Fristname 和 Lastname 有一些非 ASCII 的字符。
处理非 ASCII 数据方式有多种
- 删除
- 替换
- 仅仅提示一下
我们使用删除的方式:
# 删除非 ASCII 字符 df['first_name'].replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True) df['last_name'].replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True)
8. 有些列头应该是数据,而不应该是列名参数
有一些列头是有性别和时间范围组成的,这些数据有可能是在处理收集的过程中进行了行列转换,或者收集器的固定命名规则。这些值应该被分解为性别(m,f),小时单位的时间范围(00-06,06-12,12-18)
# 切分 sex_hour 列为 sex 列和 hour 列 sorted_columns = ['id','age','weight','first_name','last_name'] df = pd.melt(df, id_vars=sorted_columns,var_name='sex_hour',value_name='puls_rate').sort_values(sorted_columns) df[['sex','hour']] = df['sex_hour'].apply(lambda x:pd.Series(([x[:1],'{}-{}'.format(x[1:3],x[3:])])))[[0,1]] df.drop('sex_hour', axis=1, inplace=True) # 删除没有心率的数据 row_with_dashes = df['puls_rate'].str.contains('-').fillna(False) df.drop(df[row_with_dashes].index, inplace=True)
整合代码
import pandas as pd # 增加列头 column_names= ['id', 'name', 'age', 'weight','m0006','m0612','m1218','f0006','f0612','f1218'] df = pd.read_csv('../data/patient_heart_rate.csv', names = column_names) # 切分名字,删除源数据列 df[['first_name','last_name']] = df['name'].str.split(expand=True) df.drop('name', axis=1, inplace=True) # 获取 weight 数据列中单位为 lbs 的数据 rows_with_lbs = df['weight'].str.contains('lbs').fillna(False) df[rows_with_lbs] # 将 lbs 的数据转换为 kgs 数据 for i,lbs_row in df[rows_with_lbs].iterrows(): weight = int(float(lbs_row['weight'][:-3])/2.2) df.at[i,'weight'] = '{}kgs'.format(weight) # 删除全空的行 df.dropna(how='all',inplace=True) # 删除重复数据行 df.drop_duplicates(['first_name','last_name'],inplace=True) # 删除非 ASCII 字符 df['first_name'].replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True) df['last_name'].replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True) # 切分 sex_hour 列为 sex 列和 hour 列 sorted_columns = ['id','age','weight','first_name','last_name'] df = pd.melt(df, id_vars=sorted_columns,var_name='sex_hour',value_name='puls_rate').sort_values(sorted_columns) df[['sex','hour']] = df['sex_hour'].apply(lambda x:pd.Series(([x[:1],'{}-{}'.format(x[1:3],x[3:])])))[[0,1]] df.drop('sex_hour', axis=1, inplace=True) # 删除没有心率的数据 row_with_dashes = df['puls_rate'].str.contains('-').fillna(False) df.drop(df[row_with_dashes].index, inplace=True) # 重置索引,不做也没关系,主要是为了看着美观一点 df = df.reset_index(drop=True) print(df)
还有一些问题在本例中没有提及内容,下面有两个比较重要,也比较通用的问题:
- 日期的处理
- 字符编码的问题
预览数据
这次我们使用 Artworks.csv ,我们选取 100 行数据来完成本次内容。具体步骤:
- 导入 Pandas
- 读取 csv 数据到 DataFrame(要确保数据已经下载到指定路径)
DataFrame 是 Pandas 内置的数据展示的结构,展示速度很快,通过 DataFrame 我们就可以快速的预览和分析数据。代码如下:
import pandas as pd df = pd.read_csv('../data/Artworks.csv').head(100) df.head(10)
统计日期数据
我们仔细观察一下 Date 列的数据,有一些数据是年的范围(1976-1977),而不是单独的一个年份。在我们使用年份数据画图时,就不能像单独的年份那样轻易的画出来。我们现在就使用 Pandas 的 value_counts() 来统计一下每种数据的数量。
首先,选择要统计的列,并调用 value_counts():
df['Date'].value_counts()
日期数据问题
Date 列数据,除了年份是范围外,还有三种非正常格式。下面我们将这几种列出来:
- 问题一,时间范围(1976-77)
- 问题二,估计(c. 1917,1917 年前后)
- 问题三,缺失数据(Unknown)
- 问题四,无意义数据(n.d.)
接下来我们会处理上面的每一个问题,使用 Pandas 将这些不规则的数据转换为统一格式的数据。
问题一和二是有数据的只是格式上欠妥当,问题三和四实际上不是有效数据。针对前两个问题,我们可以通过代码将据格式化来达到清洗的目的,然而,后两个问题,代码上只能将其作为缺失值来处理。简单起见,我们将问题三和四的数据处理为0。
处理问题一
问题一的数据都是两个年时间范围,我们选择其中的一个年份作为清洗之后的数据。为了简单起见,我们就使用开始的时间来替换这样问题的数据,因为这个时间是一个四位数的数字,如果要使用结束的年份,我们还要补齐前两位的数字。
首先,我们需要找到问题一的数据,这样我们才能将其更新。要保证其他的数据不被更新,因为其他的数据有可能是已经格式化好的,也有可能是我们下面要处理的。
我们要处理的时间范围的数据,其中包含有“-”,这样我们就可以通过这个特殊的字符串来过滤我们要处理的数据,然后,通过 split() 利用“-”将数据分割,将结果的第一部分作为处理的最终结果。
代码如下
row_with_dashes = df['Date'].str.contains('-').fillna(False) for i, dash in df[row_with_dashes].iterrows(): df.at[i,'Date'] = dash['Date'][0:4] df['Date'].value_counts()
处理问题二
问题二的数据体现了数据本身的不准确性,是一个估计的年份时间,我们将其转换为年份,那么,就只要保留最后四位数字即可,该数据的特点就是数据包含“c”,这样我们就可以通过这一特征将需要转换的数据过滤出来。
row_with_cs = df['Date'].str.contains('c').fillna(False) for i,row in df[row_with_cs].iterrows(): df.at[i,'Date'] = row['Date'][-4:] df[row_with_cs]
处理问题三四
将这问题三四的数据赋值成初始值 0。
df['Date'] = df['Date'].replace('Unknown','0',regex=True) df['Date'] = df['Date'].replace('n.d.','0',regex=True) df['Date']
代码整合
mport pandas as pd df = pd.read_csv('../data/Artworks.csv').head(100) df.head(10) df['Date'].value_counts() row_with_dashes = df['Date'].str.contains('-').fillna(False) for i, dash in df[row_with_dashes].iterrows(): df.at[i,'Date'] = dash['Date'][0:4] df['Date'].value_counts() row_with_cs = df['Date'].str.contains('c').fillna(False) for i,row in df[row_with_cs].iterrows(): df.at[i,'Date'] = row['Date'][-4:] df['Date'].value_counts() df['Date'] = df['Date'].replace('Unknown','0',regex=True) df['Date'] = df['Date'].replace('n.d.','0',regex=True) df['Date'].value_counts()