@1007477689
2020-04-20T18:36:57.000000Z
字数 9936
阅读 831
Python
Pandas
Pandas 是 Python 中很流行的类库,使用它可以进行数据科学计算和数据分析。他可以联合其他数据科学计算工具一块儿使用,比如,SciPy,NumPy 和Matplotlib,建模工程师可以通过创建端到端的分析工作流来解决业务问题。
虽然我们可以使用 Python 和数据分析做很多强大的事情,但是我们的分析结果的好坏依赖于数据的好坏。很多数据集存在数据缺失,或数据格式不统一(畸形数据),或错误数据的情况。不管是不完善的报表,还是技术处理数据的失当都会不可避免的引起“脏”数据。
庆幸的是,Pandas 提供功能强大的类库,不管数据处于什么状态,他可以帮助我们通过清洗数据,排序数据,最后得到清晰明了的数据。对于案例的数据,准备使用 movie_metadata.csv
(链接:https://pan.baidu.com/s/1UfWB-vsBObiog3ZCiNJHmg 提取码:kkhc)。这个数据集包含了很多信息,演员、导演、预算、总输入,以及 IMDB 评分和上映时间。实际上,可以使用上百万或者更大的数据库,但是,案例数据集对于开始入门还是很好的。
不幸的是,有一些列的值是缺失的,有些列的默认值是“0”,有的是“NaN”(Not a Number)。
下面我们通过使用 Pandas 提供的功能来清洗“脏”数据。
首先,第一次使用 Pandas 之前,我们需要安装 Pandas。安装命令如下:
pip install pandas
接下来,导入 Pandas
到我们的代码中,代码如下:
#可以使用其他的别名,但是,pd是官方推荐的别名,也是大家习惯的别名
import pandas as pd
最后,加载数据集,代码如下:
data = pd.read_csv('../data/tmdb_5000_credits.csv')
注意,确保已经下载数据集,如果你的代码和数据集的存放结构与我的一样,直接运行就可以
否则,要根据实际的情况,修改 pd.read_csv()
的文件路径
检查一下我们刚刚读入数据的基本结构,Pandas 提供了 .head()
方法打印输出前五行数据。目的是让我们对读入的数据有一个大致的了解。
data.head()
我们可以通过上面介绍的 Pandas 的方法查看数据,也可以通过传统的 Excel 程序查看数据,这个时候,我们可以开始记录数据上的问题,然后,我们再想办法解决问题。
Pandas 提供了一些选择的方法,这些选择的方法可以把数据切片,也可以把数据切块。下面我们简单介绍一下:
data.columnname.describe()
data['columnname']
data['columnsname'][:n]
data[['column1','column2']]
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())
假设:我们想删除任何有缺失值的行。这种操作太据侵略性,但是我们可以根据我们的需要进行扩展。
data.dropna()
data.dropna(how = 'all')
我们也可以增加一些限制:在一行中有多少非空值的数据是可以保留下来的
data.drop(thresh = 5)
比如说,我们不想要不知道电影上映时间的数据:
data.dropna(subset = ['title_year'])
上面的 subset
参数允许我们选择想要检查的列。如果是多个列,可以使用列名的 list
作为参数。
我们可以上面的操作应用到列上。我们仅仅需要在代码上使用 axis = 1
参数。这个意思就是操作列而不是行。(我们已经在行的例子中使用了 axis = 0
,因为如果我们不传参数 axis
,默认是 axis = 0
。)
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()
这里并没有介绍关于英文的拼写错误的问题,可以参考“模糊匹配”(https://github.com/seatgeek/fuzzywuzzy)。
最终的数据可能是有计算机生成的,那么,列名有可能也是计算机按照一定计算规律生成的。这些列名对计算机没有什么,但是对于人来说可能就不够友好,这时候,我们就需要重命名成对人友好的列名,代码如下:
columns_reset = {‘title_year’: ’release_date’,
‘movie_facebook_likes’: ’facebook_likes’}
data.rename(columns = columns_reset)
像上面这样,我们就完成了两个列的重命名。需要注意的是,这个方法并没有提供 inpalce
参数,我们需要将结果赋值给自己才可以:
columns_reset = {‘title_year’: ’release_date’,
‘movie_facebook_likes’: ’facebook_likes’}
data = data.rename(columns = columns_reset)
我们完成数据清洗之后,一般会把结果再以 .csv
的格式保存下来,以便后续其他程序的处理。同样,Pandas 提供了非常易用的方法:
data.to_csv("cleanfile.csv", encoding = ’utf-8’)
这次介绍仅仅是冰山一角。有很多方式可能造成数据集变“脏”或被破坏:
在这里,我们介绍了 Python 用 Pandas 清洗数据最一般的方式。
在处理任何数据之前,我们的第一任务是理解数据以及数据是干什么用的。我们尝试去理解数据的列/行、记录、数据格式、语义错误、缺失的条目以及错误的格式,这样我们就可以大概了解数据分析之前要做哪些“清理”工作。
本次我们需要一个 patient_heart_rate.csv
(链接:https://pan.baidu.com/s/1l-nk1VYF0u9cyponldD40A 密码:7r53)的数据文件,这个数据很小,可以让我们一目了然。这个数据是 csv
格式。数据是描述不同个体在不同时间的心跳情况。数据的列信息包括人的年龄、体重、性别和不同时间的心率。
import pandas as pd
df = pd.read_csv('../data/patient_heart_rate.csv')
df.head()
下面我们就针对上面的问题一一击破。
如果我们拿到的数据像上面的数据一样没有列头,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 使用我们提供的列头。
在数据中不难发现,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)
上面就是执行执行代码之后的结果。
如果仔细观察数据集可以发现 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'] = f'{weight}kgs'
在数据集中有些年龄、体重、心率是缺失的。我们又遇到了数据清洗最常见的问题——数据缺失。一般是因为没有收集到这些信息。我们可以咨询行业专家的意见。典型的处理缺失数据的方法:
仔细对比我们会发现数据中一行空行,除了 index
之外,全部的值都是 NaN。
Pandas 的 read_csv()
并没有可选参数来忽略空行,这样,我们就需要在数据被读入之后再使用 dropna()
进行处理,删除空行.
# 删除全空的行
df.dropna(how = 'all', inplace = True)
有的时候数据集中会有一些重复的数据。在我们的数据集中也添加了重复的数据。
首先,我们校验一下是否存在重复记录。如果存在重复记录,就使用 Pandas 提供的 drop_duplicates()
来删除重复数据。
# 删除重复数据行
df.drop_duplicates(subset = ['first_name', 'last_name'], inplace = True)
在数据集中 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)
有一些列头是有性别和时间范围组成的,这些数据有可能是在处理收集的过程中进行了行列转换,或者收集器的固定命名规则。这些值应该被分解为性别(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)
f = lambda x: pd.Series(([x[:1], f'{x[1:3]}-{x[3:]}']))
df[['sex','hour']] = df['sex_hour'].apply(f)[[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)
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)
还有一些问题在本例中没有提及内容,下面有两个比较重要,也比较通用的问题:
日期的处理
字符编码的问题
本次又介绍了一些关于 Pandas 清洗数据的技能。至少用这几次介绍的处理方法,应该可以对数据做很多清洗工作。
这次我们使用 Artworks.csv
,我们选取 100 行数据来完成本次内容。具体步骤:
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 列数据,除了年份是范围外,还有三种非正常格式。下面我们将这几种列出来:
接下来我们会处理上面的每一个问题,使用 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']
import 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()