本文介绍使用Python进行数据清洗的11个案例,强调数据品质对决策的重要性。
原文标题:独家 | 为数据分析而清洗数据——Python的21个案例和代码(下)
原文作者:数据派THU
冷月清谈:
怜星夜思:
2、在数据清洗过程中遇到最大挑战的是什么?
3、你们如何看待学习Python在数据清洗中的应用?
原文内容

作者:Kamna Sinha翻译:陈超校对:赵茹萱本文约6100字,建议阅读15分钟
本文介绍了为数据分析而准备的数据清洗的另外11个Python案例及代码。
数据清洗是识别和纠正错误以及数据集不一致性的过程,以便于数据可以进行分析。在此过程中,数据专家可以更清楚地了解他们的业务中正在发生的事情,提供任何用户都可以利用的可靠分析,并帮助他们的组织运转更高效。
数据清洗的特征
不同数据特征和属性都用于衡量数据集的清洁度和整体质量,包括以下方面:
-
准确性
-
完整性
-
一致性
-
整体性
-
适时性
-
统一性
-
有效性
在上文当中,我们介绍了10使用Python进行清理的示例,在文本中,我们将继续介绍另外11个示例。
例11
用于更复杂示例的正则表达式:
例如,我们如何清理看起来像这样的电话号码列?其中电话号码可以包含一系列符号、加号、破折号、括号,也许更多。这就是正则表达式的用武之地。正则表达式使我们能够搜索文本数据中的任何模式,就像仅搜索数字这样。它们就像浏览器中的控制+查找,但是更加灵活和稳定。
如何仅从电话号码列中提取数字:
# Replace letters with nothing
phones['Phone number'] = phones['Phone number'].str.replace(r'\D+', '')
phones.head()
1. 高级数据问题
现在我们继续研究更高级的数据问题以及如何解决它们:
a. 统一性
我们将看到单位统一性。例如,我们可以有华氏度和摄氏度两种值的温度数据,千克和石的重量数据,多种格式的日期,等等。验证单位的统一性对于进行准确的分析是必要的。
例12
我们将在这里看一个新的例子:temperature.csv,它显示纽约市每天的温度读数:
temperatures = pd.read_csv('temperature.csv')
temperatures.head()

我们可以看到,除非发生重大气候事件,否则这里的值很可能是华氏度,而不是摄氏度。让我们从视觉上确认这些值的存在。
我们可以通过绘制数据的散点图来实现。我们可以使用matplotlib来做到这一点。Pyplot,它被导入为plt。我们使用plt dot scatter函数,它接收在x轴、y轴上绘制的内容以及要使用的数据源。我们使用下面的辅助函数设置标题、轴标签,使用plt dot show显示绘图:
# Import matplotlib
import matplotlib.pyplot as plt
# Create scatter plot
plt.scatter(x = 'Date', y = 'Temperature', data = temperatures)
# Create title, xlabel and ylabel
plt.title('Temperature in Celsius March 2019 - NYC')
plt.xlabel('Dates')
plt.ylabel('Temperature in Celsius')
# Show plot
plt.show()
处理温度数据:
步骤如下:
1. 为了转换温度数据,我们使用loc方法隔离温度列中温度高于40的所有行。
2.我们选择40摄氏度是因为这是纽约市的最高摄氏温度。【领域知识】
3.然后我们使用web中的公式将这些值转换为摄氏度,并将它们重新分配为各自的华氏温度值。
4.通过确保temperature的最大值小于40,我们可以使用assert语句来确保转换是正确的。
temp_fah = temperatures.loc[temperatures['Temperature'] > 40, 'Temperature']
temp_cels = (temp_fah - 32) * (5/9)
temperatures.loc[temperatures['Temperature'] > 40, 'Temperature'] = temp_cels
# Assert conversion is correct
assert temperatures['Temperature'].max() < 40
例13
另一个有趣的例子处理日期,这是一个非常常见的数据不一致的问题,需要处理:
这是一个名为“生日”的数据框,其中包含各种个人的出生日期。它是从各种来源收集并合并成一个。
我们将在这里使用datetime()函数:
datetime接受不同的格式,帮助您根据需要格式化日期。pandas to datetime函数自动接受大多数日期格式,但是当某些格式无法识别时可能会引发错误。
通过将日期列转换为datetime,我们可以很容易地处理这些日期不一致。我们可以在pandas中使用to_datetime函数来实现这一点。然而,这是不够的,而且很可能会返回一个错误,因为我们有多种格式的日期,特别是奇怪的day/day/格式,它会触发一个月的错误。相反,我们将parameter_datetime_format参数设置为True,并将errors设置为coercion。这将推断格式,并返回无法识别和转换的日期的缺失值,而不是值错误。
birthdays['Birthday'] =
pd.to_datetime(birthdays['Birthday'],infer_datetime_format=True,errors = 'coerce')
birthdays.head()
b. 交叉字段验证
交叉字段验证是使用数据集中的多个字段来检查数据的完整性。
例14
为此,我们以航班统计为例,其中包括经济舱、商务舱和头等舱的总乘客人数以及每个航班的总乘客人数。
当从不同的数据源收集数据时,可能会出现问题,合并来自不同数据源的数据时的一个常见挑战是数据完整性,或者更广泛地说,确保我们的数据是正确的。
在手头的示例中,我们可以通过将经济舱、商务舱和头等舱的值相加并确保它们等于飞机上的总乘客数来进行交叉字段验证。
sum_classes = flights[['economy_class', 'business_class', 'first_class']].sum(axis = 1)
passenger_equ = sum_classes == flights['total_passengers']
# Find and filter out rows with inconsistent passenger totals
inconsistent_pass = flights[~passenger_equ]
consistent_pass = flights[passenger_equ]
例15
下面是包含一组用户的用户id、生日和年龄值的另一个示例。
例如,我们可以通过减去今天的日期和每个生日之间的年数来确保年龄和生日列是正确的。
import pandas as pd
import datetime as dt
# Convert to datetime and get today's date
users['Birthday'] = pd.to_datetime(users['Birthday'])
today = dt.date.today()
# For each row in the Birthday column, calculate year difference
age_manual = today.year - users['Birthday'].dt.year
# Find instances where ages match
age_equ = age_manual == users['Age']
# Find and filter out rows with inconsistent age
inconsistent_age = users[~age_equ]
consistent_age = users[age_equ]
c. 完整性
缺失数据是最常见和最重要的数据清理问题之一。从本质上讲,缺少数据是指在观察中没有为变量存储数据值。缺失数据通常表示为NA或NaN,但可以采用任意值,如0或点。
这主要是由于技术或人为错误造成的。
例16
对于这个用例,我们查看一个空气质量数据集。它包含了不同日期的温度和二氧化碳测量值。

isna方法
我们可以通过使用dot is na方法找到缺失值的行,该方法对缺失值返回True,对所有行和列的完整值返回False。
# Return missing values
airquality.isna()
我们还可以将isna方法与sum方法链接起来,该方法将返回数据框架中每列缺失值的细分。
# Get summary of missingness
airquality.isna().sum()
我们注意到CO2列是唯一缺少值的列。
利用可视化发现缺失数据的本质:
让我们通过首先想象我们缺失的价值观来找出原因并进一步挖掘这种缺失的本质。
Missingno:用于可视化和理解丢失数据的有用包
import missingno as msno
import matplotlib.pyplot as plt
# Visualize missingness
msno.matrix(airquality)
plt.show()
这个矩阵本质上显示了缺失值在一列中的分布情况。我们看到缺失的二氧化碳值随机地分散在整个列中,但事实真的是这样吗?让我们深入挖掘一下。
步骤1
我们首先在一个数据框中分离出缺少CO2值的空气质量行,并在另一个数据框中分离出完整的CO2值。
# Isolate missing and complete values aside
missing = airquality[airquality['CO2'].isna()]
complete = airquality[~airquality['CO2'].isna()]
# Describe complete DataFramee
complete.describe()
# Describe missing DataFramee
missing.describe()



观测:
我们可以看到所有的CO2缺失值在低温时出现,平均气温为零下39度,最低和最高气温分别为零下49度和零下30度。让我们用丢失的包来确认这一点。
步骤2
我们首先根据温度列对数据框进行排序。然后将排序后的数据框从msno输入到矩阵函数。剩下这个矩阵。
sorted_airquality = airquality.sort_values(by = 'Temperature')
msno.matrix(sorted_airquality)
plt.show()
注意到所有缺失的值都在顶部吗?这是因为默认情况下,值是从最小到最大排序的。这基本上证实了在非常低的温度下,二氧化碳的测量值是丢失的。一定是传感器故障!
失踪类型:
在上面的例子中,我们看到了一种类型的缺失。这被称为随机数据缺失:当缺失数据与其他观测值之间存在关系时。
另外两种类型是:
完全缺失随机数据:由于随机性导致数据完全缺失,且缺失数据与剩余值之间没有关系时,这种数据输入错误。
而且,缺失不是随机的:缺失的数据和未观察到的值之间存在系统的关系。
例17
非随机缺失的例子:
例如,当外面非常热的时候,温度计可能会停止工作,所以我们在高温的日子里没有温度测量。然而,我们无法仅从数据中得知这一点,因为我们实际上无法看到缺失的温度是什么。
处理丢失的数据-简单的方法:
1. 删除缺失值
2. 用统计方法(平均值,中位数,众数…)
3. 使用算法方法进行推算
4. 用机器学习模型进行灌输
第一种方法:
我们可以通过使用dot dropna方法和子集参数来删除缺失的值,子集参数让我们选择要删除的列的缺失值。
例18
airquality_dropped = airquality.dropna(subset = ['CO2'])
airquality_dropped.head()

第二种方法:
例19
我们也可以将CO2的缺失值替换为CO2的平均值,使用fillna方法,在本例中为1.73。Fillna接受一个字典,其中列作为键,输入值作为值。如果我们对数据集有足够的领域知识,我们甚至可以将自定义值输入到与缺失数据相关的填充中。
co2_mean = airquality['CO2'].mean()
airquality_imputed = airquality.fillna({'CO2': co2_mean})
airquality_imputed.head()
接下来的两种方法是先进的方法,超出了本文的范围。
2. 记录链接
字符串相似度和最小编辑距离:
在继续进行记录链接之前,我们需要了解两个重要的概念:字符串相似性和最小编辑距离。
字符串相似度是确定两个字符串有多接近的过程,它使用最小编辑距离的系统方法来完成。
例20
例如,让我们来看看下面的两个词:intention和execution。
它们之间的最小编辑距离是指从单词INTENTION到EXECUTION所需的尽可能少的步骤,可用的操作包括:插入新字符、删除它们、替换它们和调换连续字符。
我们如何从INTENTION到EXECUTION?
步骤1:从INTENTION中删除I。
步骤2:在E和N之间添加C。
步骤3:将第一个N替换成E
步骤4:T替换成X
步骤5:N 替换U
这导致最小编辑距离为5。
编辑距离越低,两个词越接近。
a.字符串比较
有各种各样的算法和相应的包可以用来测量最小距离,对于我们的例子,我们将选择Levenshtein距离,因为它是使用fuzz包进行字符串匹配的最一般形式。
例21
# Lets us compare between two strings
from thefuzz import fuzz
# Compare reeding vs reading
fuzz.WRatio('Reeding', 'Reading')
对于任何使用thefuzz的比较函数,输出是0到100之间的分数,0表示完全不相似,100表示完全匹配。
例22
比较数组:
我们还可以使用fuzzy wuzzy库中的process模块的extract函数比较字符串和字符串数组。Extract接受一个字符串、一个字符串数组以及要返回的从高到低排列的可能匹配数。它返回一个包含3个元素的元组列表,第一个是要返回的匹配字符串,第二个是它的相似性评分,第三个是它在数组中的索引。
前面我们看到,将数据折叠成类别是处理分类和文本数据的一个重要方面,我们还看到了如何手动替换DataFrame列中的类别。但是,如果我们有太多不一致的类别,人工替换根本是不可行的呢?我们可以很容易地用字符串相似度做到这一点!
亚州的受访者的答案的调查,询问他们搬家的可能性有多大,从0到5分。现在让我们假设这个调查的“state”字段是自由文本,并且包含数百个错字。手动重新映射它们将花费大量时间。我们将用字符串匹配折叠不正确的类别。
为此,我们首先创建一个类别数据框,其中包含正确的类别,以便我们可以将其与“state”列的值进行比较:
categories
print(survey['state'].unique())
# For each correct category
for state in categories['state']:
# Find potential matches in states with typoes
matches = process.extract(state, survey['state'], limit = survey.shape[0])
# For each potential match match
for potential_match in matches:
# If high similarity score
if potential_match[1] >= 80:
# Replace typo with correct category
survey.loc[survey['state'] == potential_match[0], 'state'] = state
代码说明:
我们首先创建一个for循环,遍历类别DataFrame中的每个正确类型的状态。对于每个状态,我们在调查DataFrame的状态列中找到它的匹配项,通过将extract的limit参数设置为调查DataFrame的长度,返回所有可能的匹配项。然后我们遍历每个可能的匹配项,用if语句隔离相似性得分高于或等于80的匹配项。然后,对于每个返回的字符串,我们使用loc方法将其替换为正确的状态。
例23
记录链接尝试连接具有类似模糊重复值的数据源,因此我们最终使用字符串相似性获得没有重复值的最终数据框。当来自多个数据源的数据以略微不同的数据收集方法组合在一起时,也可能出现这种情况。
例如,这里有两个包含NBA比赛及其时间表的数据框。它们都是从不同的网站上抓取而来,我们希望将它们合并在一起,并拥有一个包含所有独特游戏的数据框。
由于有比赛同时发生,数据框之间没有共同的唯一标识符,事件的命名也不同,因此常规的连接或合并将不起作用。
这就是记录链接的用武之地。
记录链接是将来自不同来源的关于同一实体的数据链接起来的行为。
涉及的步骤有:
1. 清理两个或多个dataframe,
2. 生成成对的可能匹配的记录,
3. 根据字符串相似度和其他相似度度量对这些对进行评分,并且
4. 链接它们。
b. 生成配对
例24
这是最后一个也是最长的例子!
这里我们有两个数据框, census_A和census_B,包含各州个人的数据。我们希望合并它们,同时使用记录链接避免重复,因为它们是手动收集的,容易出现错别字,因此它们之间没有一致的id。
步骤1 生成配对
理想的做法是在数据框架之间生成所有可能的对。但如果数据框很大,它将创建数百万甚至数十亿对,这是不可扩展的。
为此,我们使用block——基于匹配列(在本例中是状态列)创建配对,减少可能的配对数量。
# Import recordlinkage import recordlinkage
Create indexing object
indexer = recordlinkage.Index()
Generate pairs blocked on state
indexer.block(‘state’)
pairs = indexer.index(census_A, census_B)
代码解释:
1.使用recordlinkage dot Index函数,创建一个索引对象。这实际上是一个可以用来从数据框生成对的对象。
2. 为了在状态上生成阻塞对,我们使用block方法,输入状态列作为输入。
3. 索引器对象初始化后,我们使用dot index方法生成对,该方法接受两个数据帧。
4. 生成的对象是一个pandas多索引对象,其中包含来自两个数据框的行索引对——一个包含可能的索引对的数组,它使数据框的子集更容易。
结果:
print(pairs)
步骤2 查找潜在匹配
# Generate the pairs pairs = indexer.index(census_A, census_B) # Create a Compare object compare_cl = recordlinkage.Compare()
Find exact matches for pairs of date_of_birth and state
compare_cl.exact(‘date_of_birth’, ‘date_of_birth’, label=‘date_of_birth’)
compare_cl.exact(‘state’, ‘state’, label=‘state’)Find similar matches for pairs of surname and address_1 using string similarity
compare_cl.string(‘surname’, ‘surname’, threshold=0.85, label=‘surname’)
compare_cl.string(‘address_1’, ‘address_1’, threshold=0.85, label=‘address_1’)Find matches
potential_matches = compare_cl.compute(pairs, census_A, census_B)
print(potential_matches)
代码说明:
既然我们已经生成了配对,现在是时候寻找潜在的匹配了。
1. 我们首先使用recordlinkage点compare函数创建一个比较对象。这类似于我们在生成对时创建的索引对象,但它负责为对分配不同的比较过程。
2. 假设有一些列我们想要它们之间完全匹配。要做到这一点,我们使用精确的方法。它接受每个DataFrame的列名,在本例中是date_of_birth和state,以及一个label参数,该参数允许我们在结果DataFrame中设置列名。
3. 现在,为了计算具有模糊值的列的行对之间的字符串相似性,我们使用点字符串方法,它也接受有问题的列名,阈值参数中的相似性截止点,它接受一个0到1之间的值,在这里我们设定为0.85。
4. 最后,为了计算匹配,我们使用compute函数,它接受可能的配对和正在讨论的两个数据框。
请注意,在将数据框作为参数插入、生成对、在列之间进行比较和计算比较时,需要始终使用相同的数据框顺序。
结果:
输出是一个多索引数据框,其中第一个索引是来自第一个数据框或census_A的行索引,第二个索引是census_B中所有行索引的列表。列是要比较的列,匹配的值为1,不匹配的值为0。
步骤3 找到我们想要的配对
筛选行值之和高于某个阈值的行。在这种情况下大于等于2。
potential_matches[potential_matches.sum(axis = 1) => 2]
c.链接数据框
步骤4 链接数据框
到目前为止,我们已经在它们之间生成了对,比较了它们的四个列,两个用于精确匹配,两个用于字符串相似度(阈值为0.85),并找到了潜在的匹配。
仔细观察上面结果的潜在匹配:它是一个多索引数据框,其中我们有两个索引列,记录id 1和记录id 2。第一个索引列,存储来自census a的索引。第二个索引列,存储来自census_B的所有可能的索引,用于census_A的每一行索引。
潜在匹配的列是我们选择链接两个数据框的列,其中匹配的值为1,否则为0。
链接数据框的步骤:
1. 把可能匹配的配对和我们非常确定的配对分开。我们在上一课中看到了如何做到这一点,通过对行和大于一定列数的行进行子化,在这个例子中是3。
2. 输出是人口普查A和人口普查B之间的行索引,它们很可能是重复的。
3. 提取其中一个索引列,对其关联的数据框进行子设置以筛选重复项。
matches = potential_matches[potential_matches.sum(axis =1) >=3]
print(matches)
4. 在这里,我们选择第二个索引列,它表示census_B的行索引。我们想提取这些索引,并在它们上面建立一个子集census_B,以便在将它们附加在一起之前,用census_A删除重复的索引。
matches.index
# Get indices from census_B only
duplicate_rows = matches.index.get_level_values(1)
print(census_B_index)
5. 我们可以使用index属性访问数据框的索引。由于这是一个多索引数据框,因此它返回一个多索引对象,其中分别包含来自census_A和census_B的行索引对。我们希望提取所有census_B索引,因此我们将其与get_level_values方法链接起来,该方法包含我们想要提取其值的列索引。我们既可以输入索引列的名称,也可以输入它的顺序,在本例中是1。
6. 链接数据框架:为了在census_B中找到重复的数据,我们简单地对census_B的所有索引进行子集,并使用通过记录链接找到的索引。可以选择进一步检查它们与census_A中的重复项的相似性,但是如果对分析有信心,可以通过重复完全相同的代码来继续查找非重复项,只需要在子集的开头添加一个波浪线。现在已经有了非重复项,所需要的就是使用census_A的数据框append方法进行一个简单的追加,然后就拥有了链接数据!
7.
# Finding duplicates in census_B
census_B_duplicates = census_B[census_B.index.isin(duplicate_rows)]
# Finding new rows in census_B
census_B_new = census_B[~census_B.index.isin(duplicate_rows)]
# Link the DataFrames!
full_census = census_A.append(census_B_new)
至此,我们结束对数据清理的讨论。
请记住,我们已经简要地触及了所有涉及的主要主题,并且能够使用上述任何一种技术解决各种问题都需要对该技术进行更深入的研究和理解,我有意将其排除在本文的范围之外。
如果你喜欢这个内容,请鼓掌/评论:)
译者简介
翻译组招募信息
工作内容:需要一颗细致的心,将选取好的外文文章翻译成流畅的中文。如果你是数据科学/统计学/计算机类的留学生,或在海外从事相关工作,或对自己外语水平有信心的朋友欢迎加入翻译小组。
你能得到:定期的翻译培训提高志愿者的翻译水平,提高对于数据科学前沿的认知,海外的朋友可以和国内技术应用发展保持联系,THU数据派产学研的背景为志愿者带来好的发展机遇。
其他福利:来自于名企的数据科学工作者,北大清华以及海外等名校学生他们都将成为你在翻译小组的伙伴。
点击文末“阅读原文”加入数据派团队~
转载须知
如需转载,请在开篇显著位置注明作者和出处(转自:数据派ID:DatapiTHU),并在文章结尾放置数据派醒目二维码。有原创标识文章,请发送【文章名称-待授权公众号名称及ID】至联系邮箱,申请白名单授权并按要求编辑。
发布后请将链接反馈至联系邮箱(见下方)。未经许可的转载以及改编者,我们将依法追究其法律责任。