原文:
annas-archive.org/md5/2774e54b23314a6bebe51d6caf9cd592
译者:飞龙
第十章:解决合并 DataFrame 时的数据问题
在大多数数据清洗项目的某个阶段,分析师必须将来自不同数据表的数据进行合并。这包括将具有相同结构的数据追加到现有的数据行中,或执行合并以从另一张数据表中提取列。前者有时称为垂直合并数据,或连接数据,而后者则称为水平合并数据,或合并数据。
合并可以根据合并依据列值的重复量进行分类。在一对一合并中,合并依据列的值在每张数据表中各出现一次。在一对多合并中,合并依据列的值在一方没有重复,而在另一方则存在重复。在多对多合并中,合并依据列的值在两方中都有重复。合并过程进一步复杂化,因为数据表中的合并依据值通常没有完全匹配的关系;每个数据表的合并依据列可能包含在另一张数据表中没有的值。
在合并数据时,可能会引入新的数据问题。当数据被追加时,即使列的名称和数据类型相同,也可能与原始数据具有不同的逻辑值。对于合并操作,每当合并依据的某一方缺少值时,该方的其他列也将缺少值。对于一对一或一对多的合并,合并依据值可能会出现意外的重复,导致其他列的值被不小心重复。
本章将介绍如何垂直和水平合并 DataFrame,并考虑如何处理合并时常见的数据问题。具体来说,本章的内容将涵盖以下主题:
-
垂直合并 DataFrame
-
一对一合并
-
通过多列进行一对一合并
-
一对多合并
-
多对多合并
-
开发合并例程
技术要求
为了完成本章中的所有内容,您需要使用 pandas、NumPy 和 Matplotlib。我使用的是 pandas 2.1.4,但代码也可以在 pandas 1.5.3 或更高版本上运行。
本章的代码可以从本书的 GitHub 仓库下载,github.com/PacktPublishing/Python-Data-Cleaning-Cookbook-Second-Edition
。
垂直合并 DataFrame
在某些情况下,我们需要将一张数据表中的行追加到另一张数据表中。这通常是将几乎相同列和数据类型的表格数据进行合并。例如,我们每个月可能会获得一份包含医院病人结果的新的 CSV 文件,并需要将其添加到现有的数据中。或者,我们可能会在某个学区的办公室工作,接收来自不同学校的数据。在这种情况下,我们可能会想要在进行分析之前先将这些数据合并。
即使跨月份和跨学校(在这些示例中)的数据结构在理论上是相同的,实际情况中可能并非如此。商业实践可能会从一个时期到另一个时期发生变化。这可能是有意为之,也可能是由于人员流动或其他外部因素而无意发生的。一个机构或部门的实践可能与另一个有所不同,并且某些数据值可能在某些机构中有所不同,甚至完全缺失。
当我们放松警惕时,通常会遇到看似相似的数据变化,通常是在我们开始假设新数据将与旧数据相似时。我每次合并数据时都会提醒自己这一点。在本章的其余部分,我将使用“纵向合并”或“附加”来指代将数据纵向合并。
在本食谱中,我们将使用 pandas 的concat
函数将一组 pandas DataFrame 的行附加到另一个 DataFrame 中。我们还将对concat
操作进行一些常见检查,以确认最终的 DataFrame 是否符合我们的预期。
准备工作
在本食谱中,我们将使用来自多个国家的陆地温度数据。这些数据包括 2023 年期间每个国家多个气象站的月平均温度、纬度、经度和海拔。每个国家的数据都保存在一个 CSV 文件中。
数据备注
陆地温度 DataFrame 包含 2023 年来自全球 12,000 多个气象站的平均温度(以°C 为单位),尽管大多数气象站位于美国。原始数据是从全球历史气候学网络集成数据库获取的。美国国家海洋和大气管理局在www.ncei.noaa.gov/products/land-based-station/global-historical-climatology-network-monthly
上提供公开使用。
如何做……
在本食谱中,我们将纵向合并结构相似的 DataFrame,检查合并后数据的值,并修复缺失值。让我们开始吧:
-
导入
pandas
和numpy
,以及os
模块:import pandas as pd import numpy as np import os
-
从喀麦隆和阿曼加载数据并检查行数和列数:
ltcameroon = pd.read_csv("data/ltcountry/ltcameroon.csv") ltoman = pd.read_csv("data/ltcountry/ltoman.csv") ltcameroon.shape
(48, 11)
ltoman.shape
(288, 10)
比较喀麦隆和阿曼 DataFrame 中的列。通过查看列,我们可以看到喀麦隆的 DataFrame 中有latabs
列,而阿曼的 DataFrame 中没有。我们可以使用symetric_difference
来确认这一点,并确保没有其他列只出现在一个 DataFrame 中而不在另一个中。它显示latabs
是唯一只在一个 DataFrame 中出现的列:
ltcameroon.columns
Index(['locationid', 'year', 'month', 'temperature',
'latitude', 'longitude', 'elevation', 'station',
'countryid', 'country', 'latabs'],
dtype='object')
ltoman.columns
Index(['locationid', 'year', 'month', 'temperature',
'latitude', 'longitude', 'elevation', 'station',
'countryid', 'country'],
dtype='object')
ltcameroon.columns.\
symmetric_difference(ltoman.columns)
Index(['latabs'], dtype='object')
-
我们仍然可以合并这两个 DataFrame。唯一的问题是,现在我们有一个名为
latabs
的列,该列在喀麦隆的所有行中都有非缺失值,而在阿曼的所有行中都是缺失值。我们将在本食谱的最后一步解决这个问题:ltall = pd.concat([ltcameroon, ltoman]) ltall.country.value_counts()
country Oman 288 Cameroon 48 Name: count, dtype: int64
ltall[['country','station','temperature', 'latitude','latabs']].\ sample(5, random_state=3)
country station temperature latitude latabs 276 Oman BAHLA 21.44 23.000 NaN 26 Oman DIBA 21.85 25.617 NaN 281 Oman RAS_AL_HADD 23.74 22.300 NaN 15 Cameroon GAROUA 33.91 9.336 9.336 220 Oman SOHAR_MAJIS 30.85 24.467 NaN
ltall.groupby(['country'])['latabs'].count()
country Cameroon 48 Oman 0 Name: latabs, dtype: int64
-
创建一个函数来进行合并,并结合我们已做的数据检查。该函数接受一个文件名列表,遍历列表,读取与每个文件名相关联的 CSV 文件到一个 DataFrame 中,然后合并该 DataFrame。我们可以得到预期的计数。我们没有检查列名,接下来我们将在下一步进行。
def concatfiles(filelist): directory = "data/ltcountry/" ltall = pd.DataFrame() for filename in filelist: ltnew = pd.read_csv(directory + filename + ".csv") print(filename + " has " + str(ltnew.shape[0]) + " rows.") ltall = pd.concat([ltall, ltnew]) return ltall ltall = concatfiles(['ltcameroon','ltoman'])
ltcameroon has 48 rows. ltoman has 288 rows.
ltall.country.value_counts()
country Oman 288 Cameroon 48 Name: count, dtype: int64
如果我们有很多文件需要合并,创建一个文件名列表可能会很麻烦。我们可以通过加载文件夹中所有具有 CSV 文件扩展名的文件,利用 Python 的os
模块来帮助我们。接下来我们来做这件事,同时还要加入一些代码来检查列。我们将基于前一步的代码进行构建。
- 合并文件夹中所有国家的数据文件。
遍历包含每个国家 CSV 文件的文件夹中的所有文件名。使用endswith
方法检查文件名是否具有 CSV 文件扩展名。使用read_csv
创建一个新的 DataFrame 并打印行数。使用concat
将新 DataFrame 的行附加到已附加的行上。最后,显示最近的 DataFrame 中缺失的列,或者是最近的 DataFrame 中有而之前的 DataFrame 中没有的列:
def concatallfiles():
directory = "data/ltcountry"
ltall = pd.DataFrame()
for filename in os.listdir(directory):
if filename.endswith(".csv"):
fileloc = os.path.join(directory, filename)
# open the next file
with open(fileloc):
ltnew = pd.read_csv(fileloc)
print(filename + " has " +
str(ltnew.shape[0]) + " rows.")
ltall = pd.concat([ltall, ltnew])
# check for differences in columns
columndiff = ltall.columns.\
symmetric_difference(ltnew.columns)
if (not columndiff.empty):
print("", "Different column names for:",
filename, columndiff, "", sep="\n")
return ltall
-
使用我们刚刚创建的函数来读取子文件夹中所有的国家 CSV 文件,显示行数并检查列名。我们再次看到
ltoman
DataFrame 缺少latabs
列:ltall = concatallfiles()
ltpoland.csv has 120 rows. ltcameroon.csv has 48 rows. ltmexico.csv has 852 rows. ltjapan.csv has 1800 rows. ltindia.csv has 1116 rows. ltoman.csv has 288 rows. Different column names for: ltoman.csv Index(['latabs'], dtype='object') ltbrazil.csv has 1008 rows.
-
显示一些合并后的数据:
ltall[['country','station','month', 'temperature','latitude']].\ sample(5, random_state=1)
country station month temperature latitude 583 Japan TOKUSHIMA 4 16 34 635 India NEW_DELHI_SAFDARJUN 7 31 29 627 Mexico COATZACOALCOSVER 9 30 18 28 Poland WLODAWA 3 5 52 775 Mexico ARRIAGACHIS 11 28 16
-
检查合并数据中的值。
注意到阿曼的latabs
值全部缺失。这是因为阿曼的 DataFrame 中缺少latabs
列(latabs
是每个站点纬度的绝对值):
ltall.country.value_counts().sort_index()
country
Brazil 1008
Cameroon 48
India 1116
Japan 1800
Mexico 852
Oman 288
Poland 120
Name: count, dtype: int64
ltall.groupby(['country']).\
agg({'temperature':['mean','max','count'],
'latabs':['mean','max','count']})
temperature latabs
mean max count mean max count
country
Brazil 25 34 900 14 34 1008
Cameroon 27 35 39 8 10 48
India 26 35 1096 21 34 1116
Japan 14 31 1345 36 45 1800
Mexico 23 36 685 22 32 852
Oman 28 38 264 NaN NaN 0
Poland 10 21 120 52 55 120
- 修复缺失的值。
将阿曼的latabs
值设置为latitude
值。(阿曼的所有latitude
值都位于赤道以北且为正数。在全球历史气候学网络集成数据库中,赤道以北的纬度值为正,而赤道以南的纬度值为负)。具体操作如下:
ltall['latabs'] = np.where(ltall.country=="Oman", ltall.latitude, ltall.latabs)
ltall.groupby(['country']).\
... agg({'temperature':['mean','max','count'],
... 'latabs':['mean','max','count']})
temperature latabs
mean max count mean max count
country
Brazil 25 34 900 14 34 1008
Cameroon 27 35 39 8 10 48
India 26 35 1096 21 34 1116
Japan 14 31 1345 36 45 1800
Mexico 23 36 685 22 32 852
Oman 28 38 264 22 26 288
Poland 10 21 120 52 55 120
到此为止,我们已经将选定文件夹中找到的七个 CSV 文件的数据合并了。我们还确认了已附加正确数量的行,找出了某些文件中缺失的列,并修复了缺失的值。
它是如何工作的…
我们在第 3 步中将一个 pandas DataFrame 的列表传递给了 pandas 的concat
函数。第二个 DataFrame 的行被附加到了第一个 DataFrame 的底部。如果我们列出了第三个 DataFrame,那么这些行就会附加到前两个 DataFrame 合并后的行上。在合并之前,我们在第 2 步使用了shape
属性来检查行数,并检查了列名。在第 3 步合并后,我们确认了结果 DataFrame 包含了每个国家的预期行数。
我们有时需要连接两个或三个以上的文件。第 4 步到第 6 步指导我们通过定义一个函数来重复代码,从而处理多个文件。在第 4 步中,我们将文件名列表传递给了这个函数。
在第 5 步和第 6 步中,我们查找了指定文件夹中的所有 CSV 文件,将找到的每个文件加载到内存中,然后将每个文件的行追加到一个 DataFrame 中。我们打印了每个加载的数据文件的行数,以便稍后将这些数字与拼接数据中的总数进行比较。我们还标识了任何与其他文件列不同的 DataFrame。在第 8 步中,我们使用 value_counts
确认了每个国家的行数是否正确。
pandas 的 groupby
方法可以用来检查每个原始 DataFrame 中的列值。我们按国家分组,因为它能标识每个原始 DataFrame 的行——每个 DataFrame 中的所有行对于国家都有相同的值。(即使该信息不用于后续分析,始终在拼接后的 DataFrame 中保留标识原始 DataFrame 的列也是很有帮助的。)在第 8 步中,这帮助我们注意到阿曼的 latabs
列没有值。在第 9 步中,我们替换了阿曼 latabs
列的缺失值。
还有更多内容…
根据你正在附加的 DataFrame 的大小和工作站的可用内存,合并 DataFrame 可能会消耗机器的资源,甚至在内存使用超过一定的资源量时导致代码提前终止。确保数据文件尽可能高效地存储数据始终是一个好主意。例如,将数字值进行降位处理,或者在适当时将字符数据转换为分类数据,都是良好的做法。
另请参见
我们在第九章《修复聚合时的脏数据》中详细讨论了强大的 pandas groupby
方法。
我们在第六章《使用 Series 操作清理和探索数据》中讨论了 NumPy 的 where
函数。
进行一对一合并
本章的其余部分将探讨横向合并数据;即,将一个数据表的列与另一个数据表的列合并。借用 SQL 开发中的术语,我们通常将这种操作称为连接操作:左连接、右连接、内连接和外连接。本节将研究一对一合并,其中合并依据的值在两个文件中都没有重复。后续的章节将展示一对多合并,其中合并依据的值在右数据表中有重复,以及多对多合并,其中合并依据的值在左和右数据表中都有重复。
我们常常说合并的左侧和右侧,在本章中我们将遵循这一约定。但这并没有实际的意义,除了为了更清晰的阐述。如果 A 是左侧数据表,B 是右侧数据表,我们可以通过合并完成完全相同的操作,反之亦然。
在本章中,我使用了“合并列”和“合并值”这些表达方式,而不是“关键列”或“索引列”。这样可以避免与 pandas 索引对齐产生可能的混淆。索引可以作为合并列使用,但也可以使用其他列。我还希望避免在讨论中依赖于关系数据库中的概念,如主键或外键。当我们从关系型系统中提取数据时,了解哪些数据列作为主键或外键是有帮助的,且我们在设置 pandas 索引时应考虑这一点。但是,对于大多数数据清洗项目中的合并,往往超出了这些键的范畴。
在简单的 1 对 1 合并情况下,左侧数据表中的每一行都会根据合并值与右侧数据表中的一行(且仅一行)匹配。当合并值出现在一张数据表中,但另一张数据表中没有时,合并结果的处理方式取决于指定的连接类型。下图展示了四种不同的连接类型:
图 10.1:展示四种不同类型连接的图示
当两个数据表通过内连接合并时,只有当合并值同时出现在左侧和右侧数据表中时,相关行才会保留。这是左侧和右侧数据表的交集,如前图中的B所示。外连接会返回所有行;也就是说,返回在两个数据表中都出现合并值的行、在左侧数据表中出现但在右侧数据表中未出现的行,以及在右侧数据表中出现但在左侧数据表中未出现的行——分别是B、A和C。这被称为并集。左连接返回合并值在左侧数据表中出现的行,无论它们是否出现在右侧数据表中。这是A和B。右连接返回合并值在右侧数据表中出现的行,无论它们是否出现在左侧数据表中。
缺失值可能由外连接、左连接或右连接产生。这是因为返回的合并数据表会在合并条件未找到的列中出现缺失值。例如,在执行左连接时,左侧数据集可能包含一些在右侧数据集中没有出现的合并条件值。在这种情况下,右侧数据集的所有列都会缺失。(这里我说可能是因为可以执行外连接、左连接或右连接,得到与内连接相同的结果,因为相同的合并条件值出现在两边。有时,我们做左连接是为了确保返回的所有行都来自左侧数据集,并且仅返回这些行。)
在本教程中,我们将查看四种连接类型。
准备工作
我们将处理来自全国纵向调查(NLS)的两个文件。这两个文件每个包含一行数据。一个包含就业、教育程度和收入数据,另一个文件包含受访者父母的收入和教育程度数据。
数据说明
全国纵向调查(NLS),由美国劳工统计局管理,是对 1997 年开始进行调查时在高中就读的个人进行的纵向调查。参与者每年都会接受调查,直到 2023 年。调查结果可以在nlsinfo.org上公开获取。
如何操作…
在本教程中,我们将对两个数据框执行左连接、右连接、内连接和外连接,每个合并条件值有一行数据。让我们开始吧:
-
导入
pandas
并加载两个 NLS 数据框:import pandas as pd nls97 = pd.read_csv("data/nls97f.csv", low_memory=False) nls97.set_index("personid", inplace=True) nls97add = pd.read_csv("data/nls97add.csv")
-
查看一些 NLS 数据:
nls97.head()
gender birthmonth birthyear ... \ personid ... 135335 Female 9 1981 ... 999406 Male 7 1982 ... 151672 Female 9 1983 ... 750699 Female 2 1981 ... 781297 Male 10 1982 ... colenrfeb22 colenroct22 originalid personid 135335 NaN NaN 1 999406 NaN NaN 2 151672 1\. Not enrolled NaN 3 750699 NaN NaN 4 781297 NaN NaN 5 [5 rows x 106 columns]
nls97.shape
(8984, 106)
nls97add.head()
originalid motherage parentincome \ 0 1 26 -3 1 2 19 -4 2 3 26 63000 3 4 33 11700 4 5 34 -3 fatherhighgrade motherhighgrade 0 16 8 1 17 15 2 -3 12 3 12 12 4 12 12
nls97add.shape
(8984, 5)
-
检查
originalid
的唯一值数量是否等于行数。
我们将稍后使用originalid
作为我们的合并列:
nls97.originalid.nunique()==nls97.shape[0]
True
nls97add.originalid.nunique()==nls97add.shape[0]
True
- 创建一些不匹配的 ID。
不幸的是,NLS 数据对于我们的目的来说有点过于干净。因此,我们将会故意修改一些originalid
的值:
nls97 = nls97.sort_values('originalid')
nls97add = nls97add.sort_values('originalid')
nls97.loc[[135335,999406], "originalid"] = \
nls97.originalid+10000
nls97.originalid.head(2)
personid
135335 10001
999406 10002
Name: originalid, dtype: int64
nls97add.loc[[0,1], "originalid"] = \
nls97add.originalid+20000
nls97add.originalid.head(2)
0 20001
1 20002
Name: originalid, dtype: int64
- 使用
join
执行左连接。
nls97
是左数据框,nls97add
是右数据框,当我们以这种方式使用join
时。显示不匹配 ID 的值。注意,在右数据框没有匹配 ID 时,来自右数据框的列值都会缺失(originalid
值 10001 和 10002 出现在左数据框中,但右数据框中没有这些 ID):
nls97.set_index("originalid", inplace=True)
nls97add.set_index("originalid", inplace=True)
nlsnew = nls97.join(nls97add)
nlsnew.loc[nlsnew.index>9999, ['originalid','gender','birthyear','motherage','parentincome']]
gender birthyear motherage parentincome
originalid
10001 Female 1981 NaN NaN
10002 Male 1982 NaN NaN
- 使用
merge
执行左连接。
第一个数据框是左数据框,第二个数据框是右数据框。使用on
参数指定合并的列。将how
参数的值设置为left
以执行左连接。我们得到的结果与使用join
时相同:
nlsnew = pd.merge(nls97, nls97add, on=['originalid'], how="left")
nlsnew.loc[nlsnew.index>9999, ['gender','birthyear','motherage','parentincome']]
gender birthyear motherage parentincome
originalid
10001 Female 1981 NaN NaN
10002 Male 1982 NaN NaN
- 执行右连接。
使用右连接时,当左数据框中没有匹配 ID 时,左数据框的值会缺失:
nlsnew = pd.merge(nls97, nls97add, on=['originalid'], how="right")
nlsnew.loc[nlsnew.index>9999, ['gender','birthyear','motherage','parentincome']]
gender birthyear motherage parentincome
originalid
20001 NaN NaN 26 -3
20002 NaN NaN 19 -4
- 执行内连接。
在内连接后,所有不匹配的 ID(值大于9999
)都不会出现。这是因为它们在两个数据框中都没有出现:
nlsnew = pd.merge(nls97, nls97add, on=['originalid'], how="inner")
nlsnew.loc[nlsnew.index>9999, ['gender','birthyear','motherage','parentincome']]
Empty DataFrame
Columns: [gender, birthyear, motherage, parentincome]
Index: []
- 执行外连接。
这会保留所有的行,因此左侧 DataFrame 中有合并列值但右侧没有的行将被保留(originalid
值为 10001
和 10002
),而右侧 DataFrame 中有合并列值但左侧没有的行也会被保留(originalid
值为 20001
和 20002
):
nlsnew = pd.merge(nls97, nls97add, on=['originalid'], how="outer")
nlsnew.loc[nlsnew.index>9999, ['gender','birthyear','motherage','parentincome']]
gender birthyear motherage parentincome
originalid
10001 Female 1,981 NaN NaN
10002 Male 1,982 NaN NaN
20001 NaN NaN 26 -3
20002 NaN NaN 19 -4
- 创建一个函数来检查 ID 不匹配。
该函数接受一个左侧和一个右侧的 DataFrame,以及一个合并列。它执行外连接,因为我们想查看在任一 DataFrame 中,或者两者中都存在的合并列值:
def checkmerge(dfleft, dfright, idvar):
... dfleft['inleft'] = "Y"
... dfright['inright'] = "Y"
... dfboth = pd.merge(dfleft[[idvar,'inleft']],\
... dfright[[idvar,'inright']], on=[idvar], how="outer")
... dfboth.fillna('N', inplace=True)
... print(pd.crosstab(dfboth.inleft, dfboth.inright))
...
checkmerge(nls97.reset_index(),nls97add.reset_index(), "originalid")
inright N Y
inleft
N 0 2
Y 2 8982
到此为止,我们展示了如何在一对一合并中执行四种类型的连接。
它是如何工作的…
一对一的合并相对简单。合并列值在左侧和右侧的 DataFrame 中仅出现一次。然而,某些合并列值可能只出现在其中一个 DataFrame 中。这使得连接的类型变得重要。如果所有的合并列值都出现在两个 DataFrame 中,那么左连接、右连接、内连接或外连接将返回相同的结果。我们在前几步中查看了这两个 DataFrame。
在第 3 步中,我们确认了按 originalid
列合并时,唯一值的数量等于两个 DataFrame 中的行数。这告诉我们将进行一对一的合并。
如果合并列是索引,则执行左连接的最简单方法是使用 join
DataFrame 方法。我们在第 5 步中做了这个操作。我们将右侧 DataFrame 传递给左侧 DataFrame 的 join
方法。当我们在第 6 步中使用 pandas 的 merge
函数执行左连接时,返回了相同的结果。我们使用 how
参数指定左连接,并通过 on
来指示合并列。
在第 7 步到第 9 步中,我们分别执行了右连接、内连接和外连接。这是通过 how
参数指定的,这是这些步骤中唯一不同的部分。
我们在第 10 步中创建的简单 checkmerge
函数统计了在一个 DataFrame 中存在但在另一个 DataFrame 中不存在的合并列值的行数,以及两个 DataFrame 中都存在的值的数量。将这两个 DataFrame 的副本传递给此函数告诉我们,左侧 DataFrame 中有两行不在右侧,右侧 DataFrame 中有两行不在左侧,且有 8,982 行在两者中都存在。
还有更多…
在进行任何非平凡的合并之前,你应该运行一个类似于我们在第 10 步中创建的 checkmerge
函数——在我看来,几乎所有的合并都算是非平凡合并。
merge
函数比我在本例中使用的示例更灵活。例如,在第 6 步中,我们不需要将左侧的 DataFrame 作为第一个参数。我本可以像这样显式地指定左侧和右侧的 DataFrame:
nlsnew = pd.merge(right=nls97add, left=nls97, on=['originalid'], how="left")
我们还可以通过使用 left_on
和 right_on
来指定左侧和右侧 DataFrame 的不同合并列,而不是使用 on
:
nlsnew = pd.merge(nls97, nls97add, left_on=['originalid'], right_on=['originalid'], how="left")
merge
函数的灵活性使它成为每当我们需要水平合并数据时的一个极好的工具。
使用多个列进行一对一合并
我们用来执行一对一合并的逻辑同样适用于多个合并列的合并。无论是两个还是更多的合并列,内连接、外连接、左连接和右连接的工作方式都是一样的。我们将在本食谱中演示这一点。
准备工作
我们将在本食谱中使用 NLS 数据,特别是 2017 年至 2021 年的工作周和大学注册数据。工作周和大学注册文件每个文件每年包含一行数据。
如何操作…
我们将使用多个合并列对两个数据框进行一对一合并。让我们开始:
-
导入
pandas
并加载 NLS 工作周和大学注册数据:import pandas as pd nls97weeksworked = pd.read_csv("data/nls97weeksworked.csv") nls97colenr = pd.read_csv("data/nls97colenr.csv")
-
看一下部分 NLS 工作周数据:
nls97weeksworked.loc[nls97weeksworked.\ originalid.isin([2,3])]
originalid year weeksworked 5 2 2017 52 6 2 2018 52 7 2 2019 52 8 2 2020 52 9 2 2021 46 10 3 2017 52 11 3 2018 52 12 3 2019 9 13 3 2020 0 14 3 2021 0
nls97weeksworked.shape
(44920, 3)
nls97weeksworked.originalid.nunique()
8984
-
看一下部分 NLS 大学注册数据:
nls97colenr.loc[nls97colenr.\ originalid.isin([2,3])]
originalid year colenr 1 2 2017 1\. Not enrolled 2 3 2017 1\. Not enrolled 8985 2 2018 1\. Not enrolled 8986 3 2018 1\. Not enrolled 17969 2 2019 1\. Not enrolled 17970 3 2019 1\. Not enrolled 26953 2 2020 1\. Not enrolled 26954 3 2020 1\. Not enrolled 35937 2 2021 1\. Not enrolled 35938 3 2021 1\. Not enrolled
nls97colenr.shape
(44920, 3)
nls97colenr.originalid.nunique()
8984
-
检查合并列中的唯一值。
我们获得的按列值组合的合并数量(44,920
)与两个数据框中的行数相同:
nls97weeksworked.groupby(['originalid','year'])\
... ['originalid'].count().shape
(44920,)
nls97colenr.groupby(['originalid','year'])\
... ['originalid'].count().shape
(44920,)
-
检查合并列中的不匹配情况。所有
originalid
和year
的组合在两个文件中都有出现:def checkmerge(dfleft, dfright, idvar): ... dfleft['inleft'] = "Y" ... dfright['inright'] = "Y" ... dfboth = pd.merge(dfleft[idvar + ['inleft']],\ ... dfright[idvar + ['inright']], on=idvar, how="outer") ... dfboth.fillna('N', inplace=True) ... print(pd.crosstab(dfboth.inleft, dfboth.inright)) ... checkmerge(nls97weeksworked.copy(),nls97colenr.copy(), ['originalid','year'])
inright Y inleft Y 44920
-
使用多个合并列执行合并:
nls97workschool = \ pd.merge(nls97weeksworked, nls97colenr, on=['originalid','year'], how="inner") nls97workschool.shape
(44920, 4)
nls97workschool.loc[nls97workschool.\ originalid.isin([2,3])]
originalid year weeksworked colenr 5 2 2017 52 1\. Not enrolled 6 2 2018 52 1\. Not enrolled 7 2 2019 52 1\. Not enrolled 8 2 2020 52 1\. Not enrolled 9 2 2021 46 1\. Not enrolled 10 3 2017 52 1\. Not enrolled 11 3 2018 52 1\. Not enrolled 12 3 2019 9 1\. Not enrolled 13 3 2020 0 1\. Not enrolled 14 3 2021 0 1\. Not enrolled
这些步骤表明,当有多个合并列时,运行合并的语法几乎没有变化。
工作原理…
NLS 数据中的每个人在工作周和大学注册数据框中都有五行数据,每年从 2017 年到 2021 年都有一行。两个文件都包含 44,920 行数据,涉及 8,984 个独特的个体(由originalid
表示)。这一切都是有道理的(8,984*5=44,920)。
第 4 步 确认了我们将用于合并的列组合即使在个人数据重复的情况下也不会重复。每个人每年只有一行数据。这意味着工作周和大学注册数据的合并将是一次一对一的合并。在第 5 步中,我们检查了是否有在一个数据框中存在但另一个数据框中不存在的个人和年份组合。没有发现。
最后,在第 6 步中,我们准备好了进行合并。我们将on
参数设置为一个包含两个列名的列表(['originalid','year']
),以告诉合并函数使用这两列进行合并。我们指定了内连接,尽管使用任何连接都会得到相同的结果。这是因为相同的合并列值在两个文件中都存在。
还有更多…
我们在前一个食谱中讨论的所有合并数据的逻辑和潜在问题都适用,无论我们是用一个合并列还是多个合并列。内连接、外连接、右连接和左连接的工作方式相同。我们仍然可以在执行合并之前计算出返回的行数。我们还应该检查合并列的唯一值数量以及两个数据框之间的匹配情况。
如果你在之前的章节中使用过涉及 NLS 工作周和大学入学数据的示例,你可能注意到这里的结构不同。在之前的示例中,每个人有一行数据,包含多个关于工作周数和大学入学的列,表示多年的工作周数和大学入学情况。例如,weeksworked21
表示 2021 年工作的周数。我们在本示例中使用的工作周数和大学入学数据框架结构比我们在早期示例中使用的 NLS 数据框架更为整洁。我们将在第十一章《整理和重塑数据》中学习如何整理数据。
执行一对多合并
在一对多合并中,左侧数据表的合并列(或列组合)具有不重复的值,而右侧数据表中的这些列则具有重复的值。对于这种合并,我们通常会使用内连接或左连接。当合并列的值在右侧数据表中缺失时,选择使用哪种连接方式是非常重要的。在进行左连接时,所有从内连接中返回的行都会返回,此外,对于左侧数据集中存在但右侧数据集缺失的每个合并列值,还会多返回一行。对于这些额外的行,右侧数据表中所有列的值将在结果合并数据中缺失。这个相对简单的事实实际上非常重要,在编写一对多合并代码之前,应该仔细考虑。
这是我开始感到紧张的地方,我认为在这里感到紧张是有道理的。在进行数据清洗工作坊时,我会在开始这个话题之前停顿一下,告诉大家,“在你能带上朋友之前,不要开始一对多合并。”
当然,我是在开玩笑……大部分时间是这样。我想表达的重点是,在进行非平凡的合并之前,应该有所停顿,而一对多的合并从来都不简单。我们的数据结构可能发生很多变化。
具体来说,在开始之前,我们需要了解将要合并的两个数据框架的一些信息。首先,我们应该知道每个数据框架上哪些列可以作为合并列。一对多合并通常用于从企业数据库系统中重新捕捉关系,并且需要与使用的主键和外键一致。(在关系型数据库中,左侧数据表上的主键通常与右侧数据表上的外键关联。)其次,我们应该知道将使用哪种连接方式以及原因。
第三,我们应该知道两个数据表各有多少行。第四,我们应该大致了解根据连接类型、每个数据集的行数以及对合并值的初步检查,将保留多少行。如果所有的合并值在两个数据集中都有,或者如果我们正在做内连接,那么行数将等于右侧数据集的行数,适用于一对多合并。但通常情况并不像这样简单。我们经常进行左连接的一对多合并。在左连接的情况下,保留的行数将等于右数据集中具有匹配合并值的行数,加上左数据集中没有匹配合并值的行数。
一旦我们通过本教程中的示例进行操作,这应该会更加清晰。
准备工作
本教程将使用来自全球历史气候网络(Global Historical Climatology Network)集成数据库的气象站数据。一个数据框包含每个国家的一行数据,另一个包含每个气象站的一行数据。每个国家通常有多个气象站。
如何实现…
在本教程中,我们将进行一对多的合并,将每个国家一行的数据与气象站数据合并,气象站数据包含每个国家的多个站点。让我们开始吧:
-
导入
pandas
并加载气象站和国家数据:import pandas as pd countries = pd.read_csv("data/ltcountries.csv") locations = pd.read_csv("data/ltlocations.csv")
-
为气象站(
locations
)和国家数据设置索引。
确认countries
数据框中的合并列值是唯一的:
countries.set_index(['countryid'], inplace=True)
locations.set_index(['countryid'], inplace=True)
countries.head()
country
countryid
AC Antigua and Barbuda
AE United Arab Emirates
AF Afghanistan
AG Algeria
AJ Azerbaijan
countries.index.nunique()==countries.shape[0]
True
locations[['locationid','latitude','stnelev']].head(10)
locationid latitude stnelev
countryid
AC ACW00011604 58 18
AE AE000041196 25 34
AE AEM00041184 26 31
AE AEM00041194 25 10
AE AEM00041216 24 3
AE AEM00041217 24 27
AE AEM00041218 24 265
AF AF000040930 35 3,366
AF AFM00040911 37 378
AF AFM00040938 34 977
-
使用
join
对国家和地点进行左连接:stations = countries.join(locations) stations[['locationid','latitude', ... 'stnelev','country']].head(10)
locationid latitude stnelev \ countryid AC ACW00011604 58 18 AE AE000041196 25 34 AE AEM00041184 26 31 AE AEM00041194 25 10 AE AEM00041216 24 3 AE AEM00041217 24 27 AE AEM00041218 24 265 AF AF000040930 35 3,366 AF AFM00040911 37 378 AF AFM00040938 34 977 country countryid AC Antigua and Barbuda AE United Arab Emirates AE United Arab Emirates AE United Arab Emirates AE United Arab Emirates AE United Arab Emirates AE United Arab Emirates AF Afghanistan AF Afghanistan AF Afghanistan
连接似乎正常工作。但我们试着改用 merge 方法。
- 在进行合并之前,检查合并列是否匹配。
首先,重新加载数据框,因为我们做了一些更改。checkmerge
函数显示,在两个数据框中有countryid
合并值的行数为27,472
,在countries
(左侧数据框)中存在但在locations
中不存在的行数为 2。这意味着内连接将返回27,472
行,左连接将返回27,474
行。该函数的最后语句标识出在一个数据框中有但在另一个数据框中没有的countryid
值:
countries = pd.read_csv("data/ltcountries.csv")
locations = pd.read_csv("data/ltlocations.csv")
def checkmerge(dfleft, dfright, idvar):
... dfleft['inleft'] = "Y"
... dfright['inright'] = "Y"
... dfboth = pd.merge(dfleft[[idvar,'inleft']],\
... dfright[[idvar,'inright']], on=[idvar], how="outer")
... dfboth.fillna('N', inplace=True)
... print(pd.crosstab(dfboth.inleft, dfboth.inright))
... print(dfboth.loc[(dfboth.inleft=='N') | (dfboth.inright=='N')])
...
checkmerge(countries.copy(), locations.copy(), "countryid")
inright N Y
inleft
N 0 1
Y 2 27472
countryid inleft inright
9715 LQ Y N
13103 ST Y N
27474 FO N Y
- 显示一个文件中有而另一个文件中没有的行。
上一步的最后语句显示了在countries
中有但在locations
中没有的两个countryid
值,以及在locations
中有但在countries
中没有的一个值:
countries.loc[countries.countryid.isin(["LQ","ST"])]
countryid country
124 LQ Palmyra Atoll [United States]
195 ST Saint Lucia
locations.loc[locations.countryid=="FO"]
locationid latitude longitude stnelev station countryid
7363 FOM00006009 61 -7 102 AKRABERG FO
- 合并
locations
和countries
数据框。
执行左连接。同时,统计在国家数据中存在但在气象站数据中缺失的每一列的缺失值数量:
stations = pd.merge(countries, locations, on=["countryid"], how="left")
stations[['locationid','latitude',
... 'stnelev','country']].head(10)
locationid latitude stnelev country
0 ACW00011604 58 18 Antigua and Barbuda
1 AE000041196 25 34 United Arab Emirates
2 AEM00041184 26 31 United Arab Emirates
3 AEM00041194 25 10 United Arab Emirates
4 AEM00041216 24 3 United Arab Emirates
5 AEM00041217 24 27 United Arab Emirates
6 AEM00041218 24 265 United Arab Emirates
7 AF000040930 35 3,366 Afghanistan
8 AFM00040911 37 378 Afghanistan
9 AFM00040938 34 977 Afghanistan
stations.shape
(27474, 7)
stations.loc[stations.countryid.isin(["LQ","ST"])].isnull().sum()
countryid 0
country 0
locationid 2
latitude 2
longitude 2
stnelev 2
station 2
dtype: int64
一对多合并返回预期的行数以及新的缺失值。
它是如何工作的…
在第 3 步中,我们使用了join
DataFrame 方法,执行了countries
和locations
DataFrame 的左连接。这是执行连接的最简单方法。由于join
方法使用 DataFrame 的索引进行连接,我们需要首先设置索引。然后,我们将右侧 DataFrame 传递给左侧 DataFrame 的join
方法。
尽管join
比这个例子中所示的稍微灵活(例如,你可以指定连接类型),但对于所有但最简单的连接,我更喜欢使用较为冗长的 pandas merge
函数。我可以确信,使用merge
函数时,所有我需要的选项都可以使用。在我们使用merge
函数之前,我们在第 4 步中做了一些检查。这告诉我们如果执行内连接或左连接,合并后的 DataFrame 预期行数分别为 27,472 或 27,474。
我们还展示了一个 DataFrame 中有 merge-by 值的行,而另一个没有。如果我们要做左连接,我们需要决定如何处理右侧 DataFrame 中缺失的值。在这个例子中,有两个 merge-by 值在右侧 DataFrame 中未找到,导致这些列出现了缺失值。
还有更多…
你可能已经注意到,在我们调用checkmerge
时,我们传递了countries
和locations
DataFrame 的副本:
checkmerge(countries.copy(), locations.copy(), "countryid")
我们在这里使用copy
,因为我们不希望checkmerge
函数对原始 DataFrame 做任何修改。
另见
我们在本章的进行一对一连接部分详细讨论了连接类型。
进行多对多连接
多对多连接会在左侧和右侧 DataFrame 中都产生重复的 merge-by 值。我们应该只在少数情况下才需要进行多对多的连接。即使数据以这种形式出现,通常也是因为我们缺少在多个一对多关系中的核心文件。例如,有捐赠者、捐赠者贡献和捐赠者联系信息的数据表,后两个文件每个捐赠者有多行。但是,在这个案例中,我们无法访问捐赠者文件,它与捐赠和联系信息文件有一对多关系。这种情况比你想象的更常见。人们有时会给我们提供数据,而不了解数据的底层结构。当我做多对多连接时,通常是因为我缺少一些关键信息,而不是因为数据库就是这么设计的。
多对多连接返回 merge-by 列值的笛卡尔积。因此,如果一个捐赠者 ID 在捐赠者联系信息文件中出现两次,在捐赠者贡献文件中出现五次,那么连接将返回 10 行数据。这在分析时通常会引发一些问题。在这个例子中,多对多连接将重复捐赠者的贡献,每个地址一次。
当面对潜在的多对多合并情况时,解决方案往往不是进行合并。相反,我们可以恢复隐含的单对多关系。以捐赠者为例,我们可以删除所有行,保留最新的联系信息,从而确保每个捐赠者只有一行数据。然后我们可以与捐赠者贡献文件进行单对多合并。但我们并不总能避免进行多对多合并。有时,我们必须生成一个分析文件或平面文件,保持所有数据,不考虑重复。这份指南展示了在需要时如何进行这些合并。
准备工作
我们将使用克利夫兰艺术博物馆(Cleveland Museum of Art)的数据。我们将使用两个 CSV 文件:一个包含集合中每个项目的媒体引用,另一个包含每个项目的创作者。
数据说明
克利夫兰艺术博物馆提供了一个公共 API,用于访问这些数据:openaccess-api.clevelandart.org/
。通过该 API 可以获取比引用和创作者数据更多的数据。本指南中的数据是 2024 年 4 月下载的。
如何做…
按照以下步骤完成这份指南:
-
加载
pandas
并加载克利夫兰艺术博物馆(CMA)的收藏数据:import pandas as pd cmacitations = pd.read_csv("data/cmacitations.csv") cmacreators = pd.read_csv("data/cmacreators.csv")
-
看看
citations
数据。itemid
是集合项的标识符。前 10 个引用都是针对集合项94979
的:cmacitations['citation'] = cmacitations.citation.str[0:15] cmacitations.head(10)
itemid citation 0 94979 Perkins, August 1 94979 Bayley, Frank W 2 94979 W. H. D. "The F 3 94979 <em>The America 4 94979 "Clevel'd Gets 5 94979 "The Inaugurati 6 94979 Bell, Hamilton. 7 94979 Cleveland Museu 8 94979 "Special Exhibi 9 94979 Dunlap, William
cmacitations.shape
(16053, 2)
cmacitations.itemid.nunique()
974
-
看看
creators
数据。creatorid
是创作者的标识符:cmacreators['creator'] = cmacreators.creator.str[0:15] cmacreators.loc[:,['itemid','creator','birth_year', 'creatorid']].head(10)
itemid creator birth_year creatorid 0 94979 John Singleton 1738 2409 1 102578 William Merritt 1849 3071 2 92937 George Bellows 1882 3005 3 151904 Thomas Eakins ( 1844 4037 4 141639 Frederic Edwin 1826 2697 5 110180 Albert Pinkham 1847 3267 6 149112 Charles Sheeler 1883 889 7 126769 Henri Rousseau 1844 1804 8 149410 Paul Gauguin (F 1848 1776 9 135299 Vincent van Gog 1853 1779
cmacreators.shape
(694, 8)
cmacreators.itemid.nunique()
618
cmacreators.creatorid.nunique()
486
-
显示
citations
数据中重复的合并值。
集合项 148758 有 182 条媒体引用:
cmacitations.itemid.value_counts().head(10)
itemid
148758 182
113164 127
122351 125
155783 119
151904 112
124245 108
92937 104
123168 98
94979 98
149112 97
Name: count, dtype: int64
-
显示
creators
数据中重复的合并值:cmacreators.itemid.value_counts().head(10)
itemid 149386 4 142753 3 112932 3 149042 3 114538 3 140001 3 146797 3 149041 3 140427 3 109147 2 Name: count, dtype: int64
-
检查合并结果。
使用我们在进行单对多合并中使用的checkmerge
函数:
def checkmerge(dfleft, dfright, idvar):
... dfleft['inleft'] = "Y"
... dfright['inright'] = "Y"
... dfboth = pd.merge(dfleft[[idvar,'inleft']],\
... dfright[[idvar,'inright']], on=[idvar], how="outer")
... dfboth.fillna('N', inplace=True)
... print(pd.crosstab(dfboth.inleft, dfboth.inright))
...
checkmerge(cmacitations.copy(), cmacreators.copy(), "itemid")
inright N Y
inleft
N 0 14
Y 4277 12710
-
显示在两个数据框中都重复的合并值:
cmacitations.loc[cmacitations.itemid==124733]
itemid citation 14533 124733 Weigel, J. A. G 14534 124733 Winkler, Friedr 14535 124733 Francis, Henry 14536 124733 Kurz, Otto. <em 14537 124733 Minneapolis Ins 14538 124733 Pilz, Kurt. "Ha 14539 124733 Koschatzky, Wal 14540 124733 Johnson, Mark M 14541 124733 Kaufmann, Thoma 14542 124733 Koreny, Fritz. 14543 124733 Achilles-Syndra 14544 124733 Schoch, Rainer, 14545 124733 DeGrazia, Diane 14546 124733 Dunbar, Burton
cmacreators.loc[cmacreators.itemid==124733, ... ['itemid','creator','birth_year','title']]
itemid creator birth_year title 591 124733 Hans Hoffmann ( 1545 Dead Blue Roller 592 124733 Albrecht Dürer 1471 Dead Blue Roller
-
进行多对多合并:
cma = pd.merge(cmacitations, cmacreators, on=['itemid'], how="outer") cma.set_index("itemid", inplace=True) cma.loc[124733, ['citation','creator','birth_year']]
citation creator birth_year itemid 124733 Weigel, J. A. G Hans Hoffmann ( 1545 124733 Weigel, J. A. G Albrecht Dürer 1471 124733 Winkler, Friedr Hans Hoffmann ( 1545 124733 Winkler, Friedr Albrecht Dürer 1471 124733 Francis, Henry Hans Hoffmann ( 1545 124733 Francis, Henry Albrecht Dürer 1471 124733 Kurz, Otto. <em Hans Hoffmann ( 1545 124733 Kurz, Otto. <em Albrecht Dürer 1471 124733 Minneapolis Ins Hans Hoffmann ( 1545 124733 Minneapolis Ins Albrecht Dürer 1471 124733 Pilz, Kurt. "Ha Hans Hoffmann ( 1545 124733 Pilz, Kurt. "Ha Albrecht Dürer 1471 124733 Koschatzky, Wal Hans Hoffmann ( 1545 124733 Koschatzky, Wal Albrecht Dürer 1471 ... last 14 rows removed to save space
现在,既然我已经带你了解了多对多合并的复杂性,我将再多说一点它是如何工作的。
它是如何工作的…
步骤 2告诉我们有 16,053 条引用,涉及 974 个独特项目。每个项目都有一个唯一的 ID,itemid
,表示博物馆中的每个项目。平均而言,每个项目有 16 条媒体引用(16,053/974)。步骤 3告诉我们,博物馆有 694 位创作者,涉及 618 个有创作者的项目,因此绝大多数作品只有一个创作者。但citations
和creators
数据框中都存在重复的itemid
(我们用于合并的值),这意味着我们的合并将是多对多合并。
步骤 4让我们了解了哪些itemid
在citations
DataFrame 中重复。博物馆中的一些项目有超过 100 个引用。值得仔细查看这些项目的引用,以判断它们是否合理。步骤 5显示,即使有多个创作者,通常也不会超过三个。在步骤 6中,我们看到大多数itemid
出现在citations
文件和creators
文件中,但也有相当一部分有citations
行但没有creators
行。如果我们进行内连接或右连接,我们将丢失那 4,277 行数据,但进行左连接或外连接则不会丢失。(假设citations
DataFrame 是左侧 DataFrame,creators
DataFrame 是右侧 DataFrame。)
我们查看了步骤 7中在两个 DataFrame 中重复的itemid
值。该集合项在citations
DataFrame 中有 14 行,在creators
DataFrame 中有 2 行。这将导致在合并的 DataFrame 中生成 28 行(2 * 14)。citations
数据将会为creators
中的每一行重复。
当我们查看在步骤 8中合并的结果时,这一点得到了确认。我们使用itemid
作为合并列进行了外连接。当我们展示合并文件中相同 ID 的行时,使用的是在步骤 7中使用的 ID,我们得到了预期的 28 行(为了节省空间,我删除了最后 14 行输出)。
还有更多…
理解何时进行多对多合并非常重要,因为有时这是无法避免的。但即便在这种情况下,我们可以知道,多对多关系其实只是两个一对多关系,只是数据文件在其中一侧缺失。很可能会有一个数据表,其中每一行代表一个集合项,并且与citations
数据和creators
数据都有一对多关系。当我们无法访问类似的文件时,最好尝试重新生成一个具有该结构的文件。利用这些数据,我们可以创建一个包含itemid
,也许还包括title
的文件,然后分别与citations
和creators
数据进行一对多合并。
然而,有时我们必须生成一个平面文件以供后续分析。当我们,或者从我们这里获取清洗数据的同事,使用无法很好处理关系数据的软件时,可能需要这么做。例如,其他部门的某个人可能会使用 Excel 做很多数据可视化工作。只要那个人知道哪些分析需要去除重复的行,那么像我们在步骤 8中生成的结构可能会很好用。
开发合并程序
我发现将数据合并看作数据清理过程中的停车场很有帮助。数据合并和停车似乎是例行公事,但它们是意外发生的高发地带。避免在停车场发生事故的一种方法是,每次进入某个特定停车场时,都使用类似的策略。也许你总是去一个相对交通较少的区域,并且大部分时间你都是通过相同的方式到达那里。
我认为类似的方法可以应用于在数据合并时相对不受损害地进出。如果我们选择一种对我们有效的通用方法,能在 80%到 90%的情况下起作用,我们可以专注于最重要的内容——数据,而不是操控数据的技术。
在这个方法中,我将展示对我有效的一般方法,但我使用的具体技术并不重要。我认为拥有一种你了解并且能够熟练使用的方法是很有帮助的。
准备工作
我们将回到本章的进行一对多合并方法中所关注的目标。我们想要对countries
数据和全球历史气候网络集成数据库中的locations
数据进行左连接。
如何实现……
在这个方法中,我们将在检查合并依据值的不匹配后,对countries
和locations
数据进行左连接。让我们开始吧:
-
导入
pandas
并加载气象站和国家数据:import pandas as pd countries = pd.read_csv("data/ltcountries.csv") locations = pd.read_csv("data/ltlocations.csv")
-
检查合并依据的列是否匹配:
def checkmerge(dfleft, dfright, mergebyleft, mergebyright): ... dfleft['inleft'] = "Y" ... dfright['inright'] = "Y" ... dfboth = \ ... pd.merge(dfleft[[mergebyleft,'inleft']],\ ... dfright[[mergebyright,'inright']],\ ... left_on=[mergebyleft],\ ... right_on=[mergebyright], how="outer") ... dfboth.fillna('N', inplace=True) ... print(pd.crosstab(dfboth.inleft, ... dfboth.inright)) ... print(dfboth.loc[(dfboth.inleft=='N') | \ ... (dfboth.inright=='N')].head(20)) checkmerge(countries.copy(), locations.copy(), "countryid", "countryid")
inright N Y inleft N 0 1 Y 2 27472 countryid inleft inright 7363 FO N Y 9716 LQ Y N 13104 ST Y N
-
合并国家和位置数据:
stations = pd.merge(countries, locations, left_on=["countryid"], right_on=["countryid"], how="left") stations[['locationid','latitude', ... 'stnelev','country']].head(10)
locationid latitude stnelev country 0 ACW00011604 57.7667 18.0 Antigua and Barbuda 1 AE000041196 25.3330 34.0 United Arab Emirates 2 AEM00041184 25.6170 31.0 United Arab Emirates 3 AEM00041194 25.2550 10.4 United Arab Emirates 4 AEM00041216 24.4300 3.0 United Arab Emirates 5 AEM00041217 24.4330 26.8 United Arab Emirates 6 AEM00041218 24.2620 264.9 United Arab Emirates 7 AF000040930 35.3170 3366.0 Afghanistan 8 AFM00040911 36.7000 378.0 Afghanistan 9 AFM00040938 34.2100 977.2 Afghanistan
stations.shape
(27474, 7)
在这里,我们从左连接中得到了预期的行数:27,472
行数据在两个 DataFrame 中都有合并依据值,另外有两行数据在左侧 DataFrame 中有合并依据值,但右侧没有。
它是如何工作的……
对于我进行的大多数合并,类似于步骤 2和步骤 3中使用的逻辑效果很好。我们在前一个方法中使用的checkmerge
函数中添加了一个第四个参数。这使我们能够为左右两个 DataFrame 指定不同的合并依据列。每次进行合并时我们不需要重新创建这个函数。我们只需要将它包含在一个模块中并导入即可。(在本书的最后一章,我们将讨论如何将辅助函数添加到模块中。)
在进行合并之前调用checkmerge
函数,能给我们足够的信息,让我们知道在使用不同的连接类型进行合并时会得到什么结果。我们将知道通过内连接、外连接、左连接或右连接返回多少行数据。我们还会知道在实际合并之前,哪些地方会生成新的缺失值。当然,这个操作相当耗费资源,因为每次我们都需要运行两次合并——一次诊断性的外连接,接着再进行我们选择的任何连接类型。但我认为这通常是值得的,至少可以帮助我们停下来思考我们在做什么。
最后,我们在步骤 3中进行了合并。这是我偏好的语法。我总是将左侧的 DataFrame 用作第一个参数,右侧的 DataFrame 用作第二个参数,尽管merge
允许我们以不同的方式指定左右 DataFrame。我还为left_on
和right_on
设置了值,即使合并列相同,我也可以使用on
(就像我们在前面的例子中所做的那样)。这样做是为了避免在合并列不同的情况下需要更改语法,我也喜欢它能让两个 DataFrame 的合并列变得显式。
一个稍微有争议的做法是,我默认使用左连接,将how
参数初始设置为左连接。我将其作为起始假设,然后问自己是否有理由使用其他连接类型。左侧 DataFrame 中的行通常代表我的分析单元(如学生、病人、客户等),而我正在从右侧 DataFrame 中添加补充数据(如 GPA、血压、邮政编码等)。如果合并列在右侧 DataFrame 中不存在(例如执行内连接时),删除分析单元中的行可能会导致问题。例如,在本章的一对一合并食谱中,如果删除主 NLS 数据中没有出现在我们为父母提供的补充数据中的行,可能就没有意义。
另请参见
我们将在第十二章,自动化数据清理与用户自定义函数、类和管道中创建包含有用数据清理功能的模块。
我们在本章的一对一合并食谱中讨论了连接的类型。
总结
我们在本章仔细研究了纵向合并数据(也称为拼接)和横向合并数据(也称为合并)。我们讨论了拼接数据时的关键数据问题,包括文件之间的不同列。我们还考虑了合并数据时的关键问题,如合并列的缺失值和数据的意外重复。我们还探讨了不同连接类型对这些问题的影响。在下一章中,我们将学习如何整理和重塑混乱的数据。
留下评论!
享受本书吗?通过在亚马逊上留下评论,帮助像您一样的读者。扫描下面的二维码获取您选择的免费电子书。
https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/py-dt-cln-cb-2e/img/Review_copy.png
第十一章:整理和重塑数据
引用列夫·托尔斯泰的智慧(“幸福的家庭都是相似的;每个不幸的家庭都有其不幸的方式。”),哈德利·威克姆告诉我们,所有整洁的数据本质上是相似的,但所有不整洁的数据都有其独特的混乱方式。我们多少次盯着某些数据行,心里想,“这…怎么回事…为什么他们这么做?” 这有点夸张。尽管数据结构不良的方式有很多,但在人类创造力方面是有限的。我们可以将数据集偏离标准化或整洁形式的最常见方式进行分类。
这是哈德利·威克姆在他关于整洁数据的开创性著作中的观察。我们可以依赖这项工作,以及我们自己在处理结构奇特的数据时的经验,为我们需要进行的重塑做好准备。不整洁的数据通常具有以下一种或多种特征:缺乏明确的按列合并关系;一对多关系中的一方数据冗余;多对多关系中的数据冗余;列名中存储值;将多个值存储在一个变量值中;数据未按分析单位进行结构化。(尽管最后一种情况不一定是数据不整洁的表现,但我们将在接下来的几个菜谱中回顾的某些技术也适用于常见的分析单位问题。)
在本章中,我们使用强大的工具来应对像前面那样的数据清理挑战。具体而言,我们将讨论以下内容:
-
移除重复行
-
修复多对多关系
-
使用
stack
和melt
将数据从宽格式重塑为长格式 -
多组列的合并
-
使用
unstack
和pivot
将数据从长格式重塑为宽格式
技术要求
完成本章的任务,您将需要 pandas、NumPy 和 Matplotlib。我使用的是 pandas 2.1.4,但该代码可以在 pandas 1.5.3 或更高版本上运行。
本章中的代码可以从本书的 GitHub 仓库下载,github.com/PacktPublishing/Python-Data-Cleaning-Cookbook-Second-Edition
。
移除重复行
数据在分析单位上的重复有几种原因:
-
当前的 DataFrame 可能是一次一对多合并的结果,其中一方是分析单位。
-
该 DataFrame 是反复测量或面板数据被压缩为平面文件,这只是第一种情况的特殊情况。
-
我们可能正在处理一个分析文件,其中多个一对多关系已被展平,形成多对多关系。
当单一侧面是分析单元时,多重侧面的数据可能需要以某种方式进行合并。例如,如果我们在分析一组大学生的结果,学生是分析单元;但我们也可能拥有每个学生的课程注册数据。为了准备数据进行分析,我们可能需要首先统计每个学生的课程数量、总学分或计算 GPA,最后得到每个学生的一行数据。通过这个例子,我们可以概括出,在去除重复数据之前,我们通常需要对多重侧面的信息进行聚合。
在这个示例中,我们查看了 pandas 处理去除重复行的技巧,并考虑了在这个过程中什么时候需要进行聚合,什么时候不需要。在下一示例中,我们将解决多对多关系中的重复问题。
准备工作
在本示例中,我们将处理 COVID-19 每日病例数据。该数据集为每个国家提供每一天的一行数据,每行记录当天的新病例和新死亡人数。每个国家还有人口统计数据和病例死亡的累计总数,因此每个国家的最后一行提供了总病例数和总死亡人数。
数据说明
Our World in Data 提供了 COVID-19 公共使用数据,网址是ourworldindata.org/covid-cases
。该数据集包括总病例和死亡人数、已进行的测试、医院床位以及诸如中位年龄、国内生产总值和糖尿病患病率等人口统计数据。本示例中使用的数据集是 2024 年 3 月 3 日下载的。
如何操作…
我们使用drop_duplicates
去除每个国家在 COVID-19 每日数据中的重复人口统计数据。当我们需要先进行一些聚合再去除重复数据时,我们也可以探索groupby
作为drop_duplicates
的替代方法:
-
导入
pandas
和 COVID-19 每日病例数据:import pandas as pd covidcases = pd.read_csv("data/covidcases.csv")
-
为每日病例和死亡列、病例总数列以及人口统计列创建列表(
total_cases
和total_deaths
列分别是该国家的病例和死亡的累计总数):dailyvars = ['casedate','new_cases','new_deaths'] totvars = ['location','total_cases','total_deaths'] demovars = ['population','population_density', ... 'median_age','gdp_per_capita', ... 'hospital_beds_per_thousand','region'] covidcases[dailyvars + totvars + demovars].head(2).T
0 1 casedate 2020-03-01 2020-03-15 new_cases 1 6 new_deaths 0 0 location Afghanistan Afghanistan total_cases 1 7 total_deaths 0 0 population 41128772 41128772 population_density 54 54 median_age 19 19 gdp_per_capita 1,804 1,804 hospital_beds_per_thousand 0 0 region South Asia South Asia
-
创建一个仅包含每日数据的 DataFrame:
coviddaily = covidcases[['location'] + dailyvars] coviddaily.shape
(36501, 4)
coviddaily.head()
location casedate new_cases new_deaths 0 Afghanistan 2020-03-01 1 0 1 Afghanistan 2020-03-15 6 0 2 Afghanistan 2020-03-22 17 0 3 Afghanistan 2020-03-29 67 2 4 Afghanistan 2020-04-05 183 3
-
为每个国家选择一行。
检查预计有多少个国家(位置),方法是获取唯一位置的数量。按位置和病例日期排序。然后使用drop_duplicates
选择每个位置的一个行,并使用 keep 参数指示我们希望为每个国家选择最后一行:
covidcases.location.nunique()
231
coviddemo = \
covidcases[['casedate'] + totvars + demovars].\
sort_values(['location','casedate']).\
drop_duplicates(['location'], keep='last').\
rename(columns={'casedate':'lastdate'})
coviddemo.shape
(231, 10)
coviddemo.head(2).T
204 379
lastdate 2024-02-04 2024-01-28
location Afghanistan Albania
total_cases 231,539 334,863
total_deaths 7,982 3,605
population 41128772 2842318
population_density 54 105
median_age 19 38
gdp_per_capita 1,804 11,803
hospital_beds_per_thousand 0 3
region South Asia Eastern Europe
- 对每个组的值进行求和。
使用 pandas 的 DataFrame groupby 方法来计算每个国家的病例和死亡总数。(我们在这里计算病例和死亡的总和,而不是使用 DataFrame 中已存在的病例和死亡的累计总数。)同时,获取一些在每个国家的所有行中都重复的列的最后一个值:median_age
、gdp_per_capita
、region
和casedate
。(我们只选择 DataFrame 中的少数几列。)请注意,数字与第 4 步中的一致:
covidtotals = covidcases.groupby(['location'],
... as_index=False).\
... agg({'new_cases':'sum','new_deaths':'sum',
... 'median_age':'last','gdp_per_capita':'last',
... 'region':'last','casedate':'last',
... 'population':'last'}).\
... rename(columns={'new_cases':'total_cases',
... 'new_deaths':'total_deaths',
... 'casedate':'lastdate'})
covidtotals.head(2).T
0 1
location Afghanistan Albania
total_cases 231,539 334,863
total_deaths 7,982 3,605
median_age 19 38
gdp_per_capita 1,804 11,803
region South Asia Eastern Europe
lastdate 2024-02-04 2024-01-28
population 41128772 2842318
选择使用drop_duplicates
还是groupby
来消除数据冗余,取决于我们是否需要在压缩多方之前进行任何聚合。
它是如何工作的……
COVID-19 数据每个国家每天有一行,但实际上很少有数据是每日数据。只有casedate
、new_cases
和new_deaths
可以视为每日数据。其他列则显示累计病例和死亡人数,或是人口统计数据。累计数据是冗余的,因为我们已有new_cases
和new_deaths
的实际值。人口统计数据在所有日期中对于每个国家来说值是相同的。
国家(及其相关人口统计数据)与每日数据之间有一个隐含的一对多关系,其中一方是国家,多方是每日数据。我们可以通过创建一个包含每日数据的 DataFrame 和另一个包含人口统计数据的 DataFrame 来恢复这种结构。我们在步骤 3和4中做到了这一点。当我们需要跨国家的总数时,我们可以自己生成,而不是存储冗余数据。
然而,运行总计变量并非完全没有用处。我们可以使用它们来检查我们关于病例总数和死亡总数的计算。步骤 5展示了如何在需要执行的不仅仅是去重时,使用groupby
来重构数据。在这种情况下,我们希望对每个国家的new_cases
和new_deaths
进行汇总。
还有更多……
我有时会忘记一个小细节。在改变数据结构时,某些列的含义可能会发生变化。在这个例子中,casedate
变成了每个国家最后一行的日期。我们将该列重命名为lastdate
。
另请参见
我们在第九章《聚合时修复混乱数据》中更详细地探讨了groupby
。
Hadley Wickham 的整洁数据论文可以在vita.had.co.nz/papers/tidy-data.pdf
找到。
修复多对多关系
有时我们必须处理从多对多合并创建的数据表。这是一种在左侧和右侧的合并列值都被重复的合并。正如我们在前一章中讨论的那样,数据文件中的多对多关系通常代表多个一对多关系,其中一方被移除。数据集 A 和数据集 B 之间有一对多关系,数据集 A 和数据集 C 之间也有一对多关系。我们有时面临的问题是,收到的数据文件将 B 和 C 合并在一起,而将 A 排除在外。
处理这种结构的数据的最佳方式是重新创建隐含的一对多关系,若可能的话。我们通过首先创建一个类似 A 的数据集来实现;也就是说,假设有一个多对多关系,我们可以推测 A 的结构是怎样的。能够做到这一点的关键是为数据两边的多对多关系识别出一个合适的合并列。这个列或这些列将在 B 和 C 数据集中重复,但在理论上的 A 数据集中不会重复。
本教程中使用的数据就是一个很好的例子。我们使用克利夫兰艺术博物馆的收藏数据。每个博物馆收藏品都有多行数据。这些数据包括收藏品的信息(如标题和创作日期);创作者的信息(如出生和死亡年份);以及该作品在媒体中的引文。当有多个创作者和多个引文时(这通常发生),行数会重复。更精确地说,每个收藏品的行数是引文和创作者数量的笛卡尔积。所以,如果有 5 条引文和 2 个创作者,我们将看到该项目有 10 行数据。
我们想要的是一个收藏品文件,每个收藏品一行(并且有一个唯一标识符),一个创作者文件,每个创作者对应一行,和一个引文文件,每个引文对应一行。在本教程中,我们将创建这些文件。
你们中的一些人可能已经注意到,这里还有更多的整理工作要做。我们最终需要一个单独的创作者文件,每个创作者一行,另一个文件只包含创作者 ID 和收藏品 ID。我们需要这种结构,因为一个创作者可能会为多个项目创作。我们在这个教程中忽略了这个复杂性。
我应该补充的是,这种情况并不是克利夫兰艺术博物馆的错,该博物馆慷慨地提供了一个 API,可以返回作为 JSON 文件的收藏数据。使用 API 的个人有责任创建最适合自己研究目的的数据文件。直接从更灵活的 JSON 文件结构中工作也是可能的,而且通常是一个不错的选择。我们将在 第十二章,使用用户定义的函数、类和管道自动清理数据 中演示如何操作。
准备工作
我们将使用克利夫兰艺术博物馆收藏的数据。CSV 文件包含有关创作者和引文的数据,这些数据通过 itemid
列合并,itemid
用来标识收藏品。每个项目可能有一行或多行关于引文和创作者的数据。
数据说明
克利夫兰艺术博物馆提供了一个公共访问数据的 API:openaccess-api.clevelandart.org/
。API 提供的数据远不止引文和创作者的数据。本文中的数据是 2024 年 4 月下载的。
如何实现…
我们通过恢复数据中隐含的多个一对多关系来处理 DataFrame 之间的多对多关系:
-
导入
pandas
和博物馆的collections
数据。为了更方便显示,我们还将限制collection
和title
列中值的长度:import pandas as pd cma = pd.read_csv("data/cmacollections.csv") cma['category'] = cma.category.str.strip().str[0:15] cma['title'] = cma.title.str.strip().str[0:30]
-
显示博物馆的一些
collections
数据。注意,几乎所有的数据值都是冗余的,除了citation
。
同时,显示唯一的 itemid
、citation
和 creator
值的数量。有 986 个独特的集合项,12,941 个引用,以及 1,062 对 item/creator 组合:
cma.shape
(17001, 9)
cma.head(4).T
0 1 \
itemid 75551 75551
citation Purrmann, Hans. <em>Henri Matis
creatorid 2,130 2,130
creator Henri Matisse ( Henri Matisse (
title Tulips Tulips
birth_year 1869 1869
death_year 1954 1954
category Mod Euro - Pain Mod Euro - Pain
creation_date 1914 1914
2 3
itemid 75551 75551
citation Flam, Jack D. < <em>Masters of
creatorid 2,130 2,130
creator Henri Matisse ( Henri Matisse (
title Tulips Tulips
birth_year 1869 1869
death_year 1954 1954
category Mod Euro - Pain Mod Euro - Pain
creation_date 1914 1914
cma.itemid.nunique()
986
cma.drop_duplicates(['itemid','citation']).\
itemid.count()
12941
cma.drop_duplicates(['itemid','creatorid']).\
itemid.count()
1062
- 显示一个包含重复引用和创作者的集合项。
只显示前 6 行(实际上共有 28 行)。注意,引用数据对于每个创作者都是重复的:
cma.set_index(['itemid'], inplace=True)
cma.loc[124733, ['title','citation',
'creation_date','creator','birth_year']].head(6)
title citation \
itemid
124733 Dead Blue Roller Weigel, J. A. G
124733 Dead Blue Roller Weigel, J. A. G
124733 Dead Blue Roller Winkler, Friedr
124733 Dead Blue Roller Winkler, Friedr
124733 Dead Blue Roller Francis, Henry
124733 Dead Blue Roller Francis, Henry
creation_date creator birth_year
itemid
124733 1583 Hans Hoffmann ( 1545
124733 1583 Albrecht Dürer 1471
124733 1583 Hans Hoffmann ( 1545
124733 1583 Albrecht Dürer 1471
124733 1583 Hans Hoffmann ( 1545
124733 1583 Albrecht Dürer 1471
-
创建一个集合 DataFrame。
title
、category
和creation_date
应该是每个集合项唯一的,因此我们创建一个仅包含这些列的 DataFrame,并带有itemid
索引。我们得到预期的行数986
:collectionsvars = \ ['title','category','creation_date'] cmacollections = cma[collectionsvars].\ reset_index().\ drop_duplicates(['itemid']).\ set_index(['itemid']) cmacollections.shape
(986, 3)
cmacollections.head()
title \ itemid 75551 Tulips 75763 Procession or Pardon at Perros 78982 The Resurrection of Christ 84662 The Orange Christ 86110 Sunset Glow over a Fishing Vil category creation_date itemid 75551 Mod Euro - Pain 1914 75763 Mod Euro - Pain 1891 78982 P - German befo 1622 84662 Mod Euro - Pain 1889 86110 ASIAN - Hanging 1460s–1550s
-
让我们看看新 DataFrame
cmacollections
中的同一项,该项在 步骤 3 中已经展示过:cmacollections.loc[124733]
title Dead Blue Roller category DR - German creation_date 1583 Name: 124733, dtype: object
-
创建一个引用(citations)DataFrame。
这将只包含 itemid
和 citation
:
cmacitations = cma[['citation']].\
reset_index().\
drop_duplicates(['itemid','citation']).\
set_index(['itemid'])
cmacitations.loc[124733]
citation
itemid
124733 Weigel, J. A. G
124733 Winkler, Friedr
124733 Francis, Henry
124733 Kurz, Otto. <em
124733 Minneapolis Ins
124733 Pilz, Kurt. "Ha
124733 Koschatzky, Wal
124733 Johnson, Mark M
124733 Kaufmann, Thoma
124733 Koreny, Fritz.
124733 Achilles-Syndra
124733 Schoch, Rainer,
124733 DeGrazia, Diane
124733 Dunbar, Burton
-
创建一个创作者 DataFrame:
creatorsvars = \ ['creator','birth_year','death_year'] cmacreators = cma[creatorsvars].\ reset_index().\ drop_duplicates(['itemid','creator']).\ set_index(['itemid']) cmacreators.loc[124733]
creator birth_year death_year itemid 124733 Hans Hoffmann ( 1545 1592 124733 Albrecht Dürer 1471 1528
-
统计出生在 1950 年后创作者的集合项数量。
首先,将 birth_year
值从字符串转换为数字。然后,创建一个只包含年轻艺术家的 DataFrame:
cmacreators['birth_year'] = \
cmacreators.birth_year.str.\
findall("\d+").str[0].astype(float)
youngartists = \
cmacreators.loc[cmacreators.birth_year>1950,
['creator']].assign(creatorbornafter1950='Y')
youngartists.shape[0]==youngartists.index.nunique()
True
youngartists
creator creatorbornafter1950
itemid
168529 Richard Barnes Y
369885 Simone Leigh (A Y
371392 Belkis Ayón (Cu Y
378931 Teresa Margolle Y
-
现在,我们可以将
youngartists
DataFrame 与集合 DataFrame 合并,创建一个标记,用于标识至少有一个创作者出生在 1950 年后 的集合项:cmacollections = \ pd.merge(cmacollections, youngartists, left_on=['itemid'], right_on=['itemid'], how='left') cmacollections.fillna({'creatorbornafter1950':'N'}, inplace=True) cmacollections.shape
(986, 9)
cmacollections.creatorbornafter1950.value_counts()
creatorbornafter1950 N 982 Y 4 Name: count, dtype: int64
现在我们有了三个 DataFrame——集合项(cmacollections
)、引用(cmacitations
)和创作者(cmacreators
)——而不是一个。cmacollections
与 cmacitations
和 cmacreators
都存在一对多关系。
它是如何工作的……
如果你主要直接处理企业数据,你可能很少会看到这种结构的文件,但许多人并没有这么幸运。如果我们从博物馆请求关于其收藏的媒体引用和创作者的数据,得到类似这样的数据文件并不完全令人惊讶,其中引用和创作者的数据是重复的。但看起来像是集合项唯一标识符的存在,让我们有希望恢复集合项与其引用之间、一对多的关系,以及集合项与创作者之间、一对多的关系。
步骤 2 显示有 986 个独特的 itemid
值。这表明在 17,001 行的 DataFrame 中,可能只包含 986 个集合项。共有 12,941 对独特的 itemid
和 citation
,即每个集合项平均有约 13 条引用。共有 1,062 对 itemid
和 creator
。
步骤 3 展示了集合项目值(如 title
)的重复情况。返回的行数等于左右合并条件的笛卡尔积。对于 Dead Blue Roller 项目,共有 28 行(我们在步骤 3中只展示了其中 6 行),因为它有 14 个引用和 2 个创作者。每个创作者的行会被重复 14 次;每个引用重复一次,针对每个创作者。每个引用会出现两次;一次针对每个创作者。对于非常少的用例,保留这种状态的数据是有意义的。
我们的“北极星”是 itemid
列,它帮助我们将数据转化为更好的结构。在步骤 4中,我们利用它来创建集合 DataFrame。我们仅保留每个 itemid
值的一行,并获取与集合项目相关的其他列,而非引用或创作者——title
、category
和 creation_date
(因为 itemid
是索引,我们需要先重置索引,然后再删除重复项)。
我们按照相同的程序,在步骤 6 和 步骤 7 中分别创建 citations
和 creators
DataFrame。我们使用 drop_duplicates
保留 itemid
和 citation
的唯一组合,和 itemid
和 creator
的唯一组合。这让我们得到了预期的行数:14 行 citations
数据和 2 行 creators
数据。
步骤 8 展示了我们如何使用这些 DataFrame 来构建新列并进行分析。我们想要计算至少有一个创作者出生在 1950 年之后的集合项目数量。分析的单位是集合项目,但我们需要从创作者 DataFrame 中获取信息来进行计算。由于 cmacollections
和 cmacreators
之间是多对一的关系,我们确保在创作者 DataFrame 中每个 itemid
只获取一行数据,即使某个项目有多个创作者出生在 1950 年之后:
youngartists.shape[0]==youngartists.index.nunique()
还有更多…
当我们处理定量数据时,多对多合并所产生的重复数据最为棘手。如果原始文件中包含了每个集合项目的评估价值,这些值将像 title
一样被重复。如果我们对评估价值生成描述性统计信息,结果会偏差很大。例如,如果 Dead Blue Roller 项目的评估价值为 1,000,000 美元,在汇总评估价值时,我们将得到 28,000,000 美元,因为有 28 个重复值。
这突显了标准化和整洁数据的重要性。如果有评估价值列,我们会将其包含在步骤 4中创建的 cmacollections
DataFrame 中。这个值将不会被重复,并且我们能够为集合生成汇总统计数据。
我发现始终回到分析单位是非常有帮助的,它与整洁数据的概念有重叠,但在某些方面有所不同。如果我们只关心 1950 年后出生的创作者的数量,而不是 1950 年后出生的创作者所对应的收藏项数量,第 8 步中的方法会完全不同。在这种情况下,分析单位将是创作者,我们只会使用创作者数据框。
另请参见
我们在第十章的处理合并数据框时的数据问题部分中讨论了多对多合并。
我们在第十二章的使用用户定义函数、类和管道自动化数据清理部分中的处理非表格数据结构的类示例中,展示了处理这种结构数据的完全不同方式。
使用 stack 和 melt 将数据从宽格式转换为长格式
Wickham 确定的一种不整洁数据类型是将变量值嵌入列名中。虽然在企业或关系型数据中这种情况很少发生,但在分析数据或调查数据中却相当常见。变量名可能会有后缀,指示时间段,如月份或年份。或者,调查中相似的变量可能有类似的名称,比如 familymember1age
和 familymember2age
,因为这样便于使用,并且与调查设计者对变量的理解一致。
调查数据中这种混乱相对频繁发生的一个原因是,一个调查工具上可能有多个分析单位。一个例子是美国的十年一次人口普查,它既询问家庭问题,也询问个人问题。调查数据有时还包括重复测量或面板数据,但通常每个受访者只有一行数据。在这种情况下,新测量值或新回答会存储在新列中,而不是新行中,列名将与早期时期的响应列名相似,唯一的区别是后缀的变化。
美国青年纵向调查(NLS)是一个很好的例子。它是面板数据,每个个体每年都进行调查。然而,分析文件中每个受访者只有一行数据。类似“在某一年工作了多少周”这样的问题的回答会放入新的列中。整理 NLS 数据意味着将如 weeksworked17
到 weeksworked21
(即 2017 年到 2021 年间的工作周数)等列,转换成仅有一列表示工作周数,另一列表示年份,且每个人有五行数据(每年一行),而不是一行数据。这有时被称为将数据从宽格式转换为长格式。
令人惊讶的是,pandas 有几个函数使得像这样的转换相对容易:stack
、melt
和 wide_to_long
。我们在这个示例中使用 stack
和 melt
,并在接下来的部分探讨 wide_to_long
。
准备工作
我们将处理 NLS 中每年工作周数和大学入学状态的数据。DataFrame 中每行对应一位调查参与者。
数据说明
国家纵向调查(NLS),由美国劳工统计局管理,是对 1997 年开始时在高中的个体进行的纵向调查。参与者每年接受一次调查,直到 2023 年。调查数据可供公众使用,网址为nlsinfo.org。
如何操作…
我们将使用stack
和melt
将 NLS 工作周数据从宽格式转换为长格式,同时提取列名中的年份值:
-
导入
pandas
和 NLS 数据:import pandas as pd nls97 = pd.read_csv("data/nls97g.csv", low_memory=False)
-
查看一些工作周数的值。
首先,设置索引:
nls97.set_index(['originalid'], inplace=True)
weeksworkedcols = ['weeksworked17','weeksworked18',
'weeksworked19','weeksworked20','weeksworked21']
nls97.loc[[2,3],weeksworkedcols].T
originalid 2 3
weeksworked17 52 52
weeksworked18 52 52
weeksworked19 52 9
weeksworked20 52 0
weeksworked21 46 0
nls97.shape
(8984, 110)
- 使用
stack
将数据从宽格式转换为长格式。
首先,仅选择weeksworked##
列。使用 stack 将原始 DataFrame 中的每个列名移入索引,并将weeksworked##
的值移入相应的行。重置index
,使得weeksworked##
列名成为level_1
列(我们将其重命名为year
)的值,weeksworked##
的值成为 0 列(我们将其重命名为weeksworked
)的值:
数据说明
对于未来升级到pandas 3.0
,我们需要在stack
函数中提到关键字参数(future_stack=True)
。
weeksworked = nls97[weeksworkedcols].\
stack().\
reset_index().\
rename(columns={'level_1':'year',0:'weeksworked'})
weeksworked.loc[weeksworked.originalid.isin([2,3])]
originalid year weeksworked
5 2 weeksworked17 52
6 2 weeksworked18 52
7 2 weeksworked19 52
8 2 weeksworked20 52
9 2 weeksworked21 46
10 3 weeksworked17 52
11 3 weeksworked18 52
12 3 weeksworked19 9
13 3 weeksworked20 0
14 3 weeksworked21 0
- 修正
year
值。
获取年份值的最后几位数字,将其转换为整数,并加上 2,000:
weeksworked['year'] = \
weeksworked.year.str[-2:].astype(int)+2000
weeksworked.loc[weeksworked.originalid.isin([2,3])]
originalid year weeksworked
5 2 2017 52
6 2 2018 52
7 2 2019 52
8 2 2020 52
9 2 2021 46
10 3 2017 52
11 3 2018 52
12 3 2019 9
13 3 2020 0
14 3 2021 0
weeksworked.shape
(44920, 3)
- 或者,使用
melt
将数据从宽格式转换为长格式。
首先,重置index
并选择originalid
和weeksworked##
列。使用melt
的id_vars
和value_vars
参数,指定originalid
作为ID
变量,并将weeksworked##
列作为要旋转或熔化的列。使用var_name
和value_name
参数将列名重命名为year
和weeksworked
。value_vars
中的列名成为新year
列的值(我们使用原始后缀将其转换为整数)。value_vars
列的值被分配到新weeksworked
列中的相应行:
weeksworked = nls97.reset_index().\
loc[:,['originalid'] + weeksworkedcols].\
melt(id_vars=['originalid'],
value_vars=weeksworkedcols,
var_name='year', value_name='weeksworked')
weeksworked['year'] = \
weeksworked.year.str[-2:].astype(int)+2000
weeksworked.set_index(['originalid'], inplace=True)
weeksworked.loc[[2,3]]
year weeksworked
originalid
2 2017 52
2 2018 52
2 2019 52
2 2020 52
2 2021 46
3 2017 52
3 2018 52
3 2019 9
3 2020 0
3 2021 0
- 使用
melt
重塑大学入学列。
这与melt
函数在处理工作周数列时的作用相同:
colenrcols = \
['colenroct17','colenroct18','colenroct19',
'colenroct20','colenroct21']
colenr = nls97.reset_index().\
loc[:,['originalid'] + colenrcols].\
melt(id_vars=['originalid'], value_vars=colenrcols,
var_name='year', value_name='colenr')
colenr['year'] = colenr.year.str[-2:].astype(int)+2000
colenr.set_index(['originalid'], inplace=True)
colenr.loc[[2,3]]
year colenr
originalid
2 2017 1\. Not enrolled
2 2018 1\. Not enrolled
2 2019 1\. Not enrolled
2 2020 1\. Not enrolled
2 2021 1\. Not enrolled
3 2017 1\. Not enrolled
3 2018 1\. Not enrolled
3 2019 1\. Not enrolled
3 2020 1\. Not enrolled
3 2021 1\. Not enrolled
-
合并工作周数和大学入学数据:
workschool = \ pd.merge(weeksworked, colenr, on=['originalid','year'], how="inner") workschool.shape
(44920, 3)
workschool.loc[[2,3]]
year weeksworked colenr originalid 2 2017 52 1\. Not enrolled 2 2018 52 1\. Not enrolled 2 2019 52 1\. Not enrolled 2 2020 52 1\. Not enrolled 2 2021 46 1\. Not enrolled 3 2017 52 1\. Not enrolled 3 2018 52 1\. Not enrolled 3 2019 9 1\. Not enrolled 3 2020 0 1\. Not enrolled 3 2021 0 1\. Not enrolled
这将通过熔化工作周数和大学入学列,生成一个 DataFrame。
它是如何工作的…
我们可以使用stack
或melt
将数据从宽格式重塑为长格式,但melt
提供了更多的灵活性。stack
会将所有列名移动到索引中。我们在第 4 步中看到,堆叠后得到了预期的行数44920
,这等于 5*8984,即初始数据中的行数。
使用melt
,我们可以根据不同于索引的ID
变量旋转列名和值。我们通过id_vars
参数来实现这一点。我们使用value_vars
参数指定要旋转的变量。
在 步骤 6 中,我们还重新塑造了大学入学的列。为了将重新塑造后的工作周和大学入学数据合并为一个 DataFrame,我们合并了 步骤 5 和 步骤 6 中创建的两个 DataFrame。我们将在下一个配方中看到如何一步完成 步骤 5 到 步骤 7 的工作。
融合多个列组
在前一个配方中,当我们需要融合多个列组时,我们使用了两次 melt
然后合并了结果的 DataFrame。那样也可以,但我们可以用 wide_to_long
函数在一步内完成相同的任务。wide_to_long
的功能比 melt
强大,但使用起来稍微复杂一些。
准备工作
我们将在本配方中使用 NLS 的工作周和大学入学数据。
如何操作…
我们将使用 wide_to_long
一次性转换多个列组:
-
导入
pandas
并加载 NLS 数据:import pandas as pd nls97 = pd.read_csv("data/nls97g.csv", low_memory=False) nls97.set_index('personid', inplace=True)
-
查看一些工作周和大学入学的数据:
weeksworkedcols = ['weeksworked17','weeksworked18', 'weeksworked19','weeksworked20','weeksworked21'] colenrcols = ['colenroct17','colenroct18', 'colenroct19','colenroct20','colenroct21'] nls97.loc[nls97.originalid.isin([2,3]), ['originalid'] + weeksworkedcols + colenrcols].T
personid 999406 151672 originalid 2 3 weeksworked17 52 52 weeksworked18 52 52 weeksworked19 52 9 weeksworked20 52 0 weeksworked21 46 0 colenroct17 1\. Not enrolled 1\. Not enrolled colenroct18 1\. Not enrolled 1\. Not enrolled colenroct19 1\. Not enrolled 1\. Not enrolled colenroct20 1\. Not enrolled 1\. Not enrolled colenroct21 1\. Not enrolled 1\. Not enrolled
-
运行
wide_to_long
函数。
将一个列表传递给 stubnames
以指示所需的列组。(所有列名以列表中每一项的相同字符开头的列都会被选中进行转换。)使用 i
参数指示 ID 变量(originalid
),并使用 j
参数指定基于列后缀(如 17
、18
等)命名的列(year
):
workschool = pd.wide_to_long(nls97[['originalid']
... + weeksworkedcols + colenrcols],
... stubnames=['weeksworked','colenroct'],
... i=['originalid'], j='year').reset_index()
workschool['year'] = workschool.year+2000
workschool = workschool.\
... sort_values(['originalid','year'])
workschool.set_index(['originalid'], inplace=True)
workschool.loc[[2,3]]
year weeksworked colenroct
originalid
2 2017 52 1\. Not enrolled
2 2018 52 1\. Not enrolled
2 2019 52 1\. Not enrolled
2 2020 52 1\. Not enrolled
2 2021 46 1\. Not enrolled
3 2017 52 1\. Not enrolled
3 2018 52 1\. Not enrolled
3 2019 9 1\. Not enrolled
3 2020 0 1\. Not enrolled
3 2021 0 1\. Not enrolled
wide_to_long
一步完成了我们在前一个配方中使用 melt
需要多个步骤才能完成的工作。
工作原理…
wide_to_long
函数几乎为我们完成了所有工作,尽管它的设置比 stack
或 melt
要复杂一些。我们需要向函数提供列组的字符(在这个例子中是 weeksworked
和 colenroct
)。由于我们的变量名称带有表示年份的后缀,wide_to_long
会将这些后缀转换为有意义的值,并将它们融合到用 j
参数命名的列中。这几乎就像魔法一样!
还有更多…
本配方中 stubnames
列的后缀是相同的:17 到 21。但这不一定是必然的。当某个列组有后缀,而另一个没有时,后者列组对应后缀的值将会缺失。通过排除 DataFrame 中的 weeksworked17
并添加 weeksworked16
,我们可以看到这一点:
weeksworkedcols = ['weeksworked16','weeksworked18',
'weeksworked19','weeksworked20','weeksworked21']
workschool = pd.wide_to_long(nls97[['originalid']
... + weeksworkedcols + colenrcols],
... stubnames=['weeksworked','colenroct'],
... i=['originalid'], j='year').reset_index()
workschool['year'] = workschool.year+2000
workschool = workschool.sort_values(['originalid','year'])
workschool.set_index(['originalid'], inplace=True)
workschool.loc[[2,3]]
year weeksworked colenroct
originalid
2 2016 53 NaN
2 2017 NaN 1\. Not enrolled
2 2018 52 1\. Not enrolled
2 2019 52 1\. Not enrolled
2 2020 52 1\. Not enrolled
2 2021 46 1\. Not enrolled
3 2016 53 NaN
3 2017 NaN 1\. Not enrolled
3 2018 52 1\. Not enrolled
3 2019 9 1\. Not enrolled
3 2020 0 1\. Not enrolled
3 2021 0 1\. Not enrolled
现在,2017 年的 weeksworked
值缺失了,2016 年的 colenroct
值也缺失了。
使用 unstack
和 pivot
将数据从长格式转换为宽格式
有时候,我们实际上需要将数据从整洁格式转换为杂乱格式。这通常是因为我们需要将数据准备为某些不擅长处理关系型数据的软件包分析,或者因为我们需要提交数据给某个外部机构,而对方要求以杂乱格式提供数据。unstack
和 pivot
在需要将数据从长格式转换为宽格式时非常有用。unstack
做的是与我们使用 stack
的操作相反的事,而 pivot
做的则是与 melt
相反的操作。
准备工作
我们在本食谱中继续处理关于工作周数和大学入学的 NLS 数据。
如何操作……
我们使用unstack
和pivot
将融化的 NLS 数据框恢复到其原始状态:
-
导入
pandas
并加载堆叠和融化后的 NLS 数据:import pandas as pd nls97 = pd.read_csv("data/nls97g.csv", low_memory=False) nls97.set_index(['originalid'], inplace=True)
-
再次堆叠数据。
这重复了本章早期食谱中的堆叠操作:
weeksworkedcols = ['weeksworked17','weeksworked18',
'weeksworked19','weeksworked20','weeksworked21']
weeksworkedstacked = nls97[weeksworkedcols].\
stack()
weeksworkedstacked.loc[[2,3]]
originalid
2 weeksworked17 52
weeksworked18 52
weeksworked19 52
weeksworked20 52
weeksworked21 46
3 weeksworked17 52
weeksworked18 52
weeksworked19 9
weeksworked20 0
weeksworked21 0
dtype: float64
- 再次融化数据。
这重复了本章早期食谱中的melt
操作:
weeksworkedmelted = nls97.reset_index().\
... loc[:,['originalid'] + weeksworkedcols].\
... melt(id_vars=['originalid'],
... value_vars=weeksworkedcols,
... var_name='year', value_name='weeksworked')
weeksworkedmelted.loc[weeksworkedmelted.\
originalid.isin([2,3])].\
sort_values(['originalid','year'])
originalid year weeksworked
1 2 weeksworked17 52
8985 2 weeksworked18 52
17969 2 weeksworked19 52
26953 2 weeksworked20 52
35937 2 weeksworked21 46
2 3 weeksworked17 52
8986 3 weeksworked18 52
17970 3 weeksworked19 9
26954 3 weeksworked20 0
35938 3 weeksworked21 0
-
使用
unstack
将堆叠的数据从长格式转换为宽格式:weeksworked = weeksworkedstacked.unstack() weeksworked.loc[[2,3]].T
originalid 2 3 weeksworked17 52 52 weeksworked18 52 52 weeksworked19 52 9 weeksworked20 52 0 weeksworked21 46 0
-
使用
pivot
将融化的数据从长格式转换为宽格式。
pivot
比unstack
稍微复杂一点。我们需要传递参数来执行 melt 的反向操作,告诉 pivot 使用哪个列作为列名后缀(year
),并从哪里获取要取消融化的值(在本例中来自weeksworked
列):
weeksworked = weeksworkedmelted.\
... pivot(index='originalid',
... columns='year', values=['weeksworked']).\
... reset_index()
weeksworked.columns = ['originalid'] + \
... [col[1] for col in weeksworked.columns[1:]]
weeksworked.loc[weeksworked.originalid.isin([2,3])].T
1 2
originalid 2 3
weeksworked17 52 52
weeksworked18 52 52
weeksworked19 52 9
weeksworked20 52 0
weeksworked21 46 0
这将 NLS 数据返回到其原始的无序形式。
它是如何工作的……
我们首先在步骤 2和步骤 3分别执行stack
和melt
。这将数据框从宽格式转换为长格式。然后我们使用unstack
(步骤 4)和pivot
(步骤 5)将数据框从长格式转换回宽格式。
unstack
使用由stack
创建的多重索引来确定如何旋转数据。
pivot
函数需要我们指定索引列(originalid
),将附加到列名中的列(year
),以及包含要取消融化值的列名称(weeksworked
)。pivot
将返回多级列名。我们通过从第二级提取[col[1] for col in weeksworked.columns[1:]]
来修复这个问题。
总结
本章中我们探讨了关键的 tidy 数据主题。这些主题包括处理重复数据,可以通过删除冗余数据的行或按组聚合来处理。我们还将以多对多格式存储的数据重构为 tidy 格式。最后,我们介绍了将数据从宽格式转换为长格式的几种方法,并在必要时将其转换回宽格式。接下来是本书的最后一章,我们将学习如何使用用户定义的函数、类和管道来自动化数据清理。
加入我们的社区,参与 Discord 讨论
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:
第十二章:使用用户定义的函数、类和管道来自动化数据清理
编写可重用的代码有很多很好的理由。当我们从当前的数据清理问题中退一步,考虑它与非常相似的问题的关系时,实际上能够帮助我们加深对关键问题的理解。当我们将目光投向长期解决方案而非短期解决方案时,也更有可能以系统化的方式处理任务。这还带来了一个额外的好处,即帮助我们将数据处理的实质问题与操作数据的机制分开。
在本章中,我们将创建多个模块来完成常规的数据清理任务。这些模块中的函数和类是可以跨 DataFrame 或针对一个 DataFrame 在较长时间内重用的代码示例。这些函数处理了我们在前十一章中讨论的许多任务,但方式是允许我们重用代码的。
本章中的教程具体包括以下内容:
-
获取数据初步信息的函数
-
显示汇总统计信息和频率的函数
-
识别异常值和意外值的函数
-
聚合或合并数据的函数
-
包含更新 Series 值逻辑的类
-
处理非表格数据结构的类
-
检查整体数据质量的函数
-
使用管道进行数据预处理:一个简单的例子
-
使用管道进行数据预处理:一个更复杂的例子
技术要求
完成本章中的教程,你将需要 pandas、NumPy 和 Matplotlib。我使用的是 pandas 2.1.4,但代码也能在 pandas 1.5.3 或更高版本上运行。
本章中的代码可以从本书的 GitHub 仓库下载,github.com/PacktPublishing/Python-Data-Cleaning-Cookbook-Second-Edition
。
获取数据初步信息的函数
我们在将数据导入 pandas DataFrame 后,所进行的最初几步操作基本上是相同的,不论数据的特点如何。我们几乎总是想知道列数和行数、列的数据类型,并查看前几行数据。我们也可能想查看索引,并检查是否存在唯一标识符来区分 DataFrame 的行。这些离散且容易重复的任务是很好的候选项,可以将其组织成一个模块中的函数。
在这个教程中,我们将创建一个包含函数的模块,这些函数可以让我们快速了解任何 pandas DataFrame 的基本情况。模块只是一个 Python 代码的集合,我们可以将其导入到其他 Python 程序中。模块便于重用,因为任何能够访问存储模块文件夹的程序都可以引用它。
准备工作
在这个示例中,我们创建两个文件:一个包含我们将用来查看数据的函数,另一个用来调用该函数。让我们将包含我们将使用的函数的文件命名为 basicdesciptives.py
,并将其放在名为 helperfunctions
的子文件夹中。
在这个示例中,我们使用国家纵向调查(NLS)数据。
数据注释
NLS 由美国劳工统计局管理,是一个关于在 1997 年高中毕业的个人的纵向调查集合。参与者每年接受调查直到 2023 年。这些调查可供公众在 nlsinfo.org 上使用。
如何做…
我们将创建一个函数来初步查看 DataFrame。
- 创建包含我们想要的函数的
basicdescriptives.py
文件。
getfirstlook
函数将返回一个包含 DataFrame 摘要信息的字典。将文件保存在 helperfunctions
子文件夹中,文件名为 basicdescriptives.py
。(你也可以直接从 GitHub 仓库下载代码。)另外,创建一个函数(displaydict
)来美化字典的显示:
import pandas as pd
def getfirstlook(df, nrows=5, uniqueids=None):
... out = {}
... out['head'] = df.head(nrows)
... out['dtypes'] = df.dtypes
... out['nrows'] = df.shape[0]
... out['ncols'] = df.shape[1]
... out['index'] = df.index
... if (uniqueids is not None):
... out['uniqueids'] = df[uniqueids].nunique()
... return out
def displaydict(dicttodisplay):
... print(*(': '.join(map(str, x)) \
... for x in dicttodisplay.items()), sep='\n\n')
- 创建一个单独的文件
firstlook.py
,用来调用getfirstlook
函数。
导入 pandas
、os
和 sys
库,并加载 NLS 数据:
import pandas as pd
import os
import sys
nls97 = pd.read_csv("data/nls97g.csv", low_memory=False)
nls97.set_index('personid', inplace=True)
- 导入
basicdescriptives
模块。
首先将 helperfunctions
子文件夹添加到 Python 路径中。然后我们可以导入 basicdescriptives
。我们使用与要导入的模块文件名相同的名称。我们创建一个别名 bd
,以便稍后更容易访问模块中的函数。(如果我们需要重新加载 basicdescriptives
,可以使用 importlib
,这里被注释掉,因为我们在该模块中进行了一些更改。)
sys.path.append(os.getcwd() + "/helperfunctions")
import basicdescriptives as bd
# import importlib
# importlib.reload(bd)
- 首先查看 NLS 数据。
我们可以将 DataFrame 直接传递给 basicdescriptives
模块中的 getfirstlook
函数,以快速摘要 NLS 数据。displaydict
函数为我们提供了字典的更漂亮打印:
dfinfo = bd.getfirstlook(nls97)
bd.displaydict(dfinfo)
head: gender birthmonth birthyear ... parentincome \
personid ...
135335 Female 9 1981 ... -3
999406 Male 7 1982 ... -4
151672 Female 9 1983 ... 63000
750699 Female 2 1981 ... 11700
781297 Male 10 1982 ... -3
fatherhighgrade motherhighgrade
personid
135335 16 8
999406 17 15
151672 -3 12
750699 12 12
781297 12 12
[5 rows x 110 columns]
dtypes: gender object
birthmonth int64
birthyear int64
sampletype object
ethnicity object
originalid int64
motherage int64
parentincome int64
fatherhighgrade int64
motherhighgrade int64
Length: 110, dtype: object
nrows: 8984
ncols: 110
index: Index([135335, 999406, 151672, 750699, 781297, 613800, 403743,
474817, 530234, 351406,
...
290800, 209909, 756325, 543646, 411195, 505861, 368078,
215605, 643085, 713757],
dtype='int64', name='personid', length=8984)
- 将值传递给
getfirstlook
的nrows
和uniqueids
参数。
这两个参数默认值分别为 5 和 None
,除非我们提供值:
dfinfo = bd.getfirstlook(nls97,2,'originalid')
bd.displaydict(dfinfo)
head: gender birthmonth birthyear ... parentincome \
personid ...
135335 Female 9 1981 ... -3
999406 Male 7 1982 ... -4
fatherhighgrade motherhighgrade
personid
135335 16 8
999406 17 15
[2 rows x 110 columns]
dtypes: gender object
birthmonth int64
birthyear int64
sampletype object
ethnicity object
originalid int64
motherage int64
parentincome int64
fatherhighgrade int64
motherhighgrade int64
Length: 110, dtype: object
nrows: 8984
ncols: 110
index: Index([135335, 999406, 151672, 750699, 781297, 613800, 403743,
474817, 530234, 351406,
...
290800, 209909, 756325, 543646, 411195, 505861, 368078,
215605, 643085, 713757],
dtype='int64', name='personid', length=8984)
uniqueids: 8984
- 使用一些返回的字典键和值。
我们还可以显示从 getfirstlook
返回的字典中选择的关键值。显示行数和数据类型,并检查每行是否具有 uniqueid
实例(dfinfo['nrows'] == dfinfo['uniqueids']
):
dfinfo['nrows']
8984
dfinfo['dtypes']
gender object
birthmonth int64
birthyear int64
sampletype object
ethnicity object
originalid int64
motherage int64
parentincome int64
fatherhighgrade int64
motherhighgrade int64
Length: 110, dtype: object
dfinfo['nrows'] == dfinfo['uniqueids']
True
让我们更仔细地看看函数的工作原理以及如何调用它。
工作原理…
在这个示例中,几乎所有的操作都在 getfirstlook
函数中,我们将在步骤 1中查看。我们将 getfirstlook
函数放在一个单独的文件中,命名为 basicdescriptives.py
,我们可以将其作为模块导入,名称为该名称(去掉扩展名)。
我们本可以将函数键入到我们正在使用的文件中,并从那里调用它。但是,将其放入一个模块中,我们可以从任何具有对模块保存的文件夹的访问权限的文件中调用它。当我们在步骤 3中导入 basicdescriptives
模块时,我们加载了 basicdescriptives
中的所有代码,从而可以调用该模块中的所有函数。
getfirstlook
函数返回一个关于传递给它的 DataFrame 的有用信息的字典。我们看到前五行、列数和行数、数据类型和索引。通过向 uniqueid
参数传递一个值,我们还可以得到该列的唯一值数。
通过添加具有默认值的关键字参数(nrows
和 uniqueid
),我们提高了 getfirstlook
的灵活性,而不会增加在不需要额外功能时调用函数所需的工作量。
在第一次调用中,在步骤 4中,我们没有为 nrows
或 uniqueid
传递值,保持默认值。在步骤 5中,我们指示只显示两行,并且要检查 originalid
的唯一值。
还有更多…
这个示例及其后续示例的重点不是提供可以下载并在自己的数据上运行的代码,尽管您当然可以这样做。我主要是想演示如何将您喜欢的数据清理方法收集到方便的模块中,以及如何通过这种方式实现轻松的代码重用。这里的具体代码只是一种供参考的建议。
每当我们使用位置参数和关键字参数的组合时,位置参数必须首先出现。
用于显示摘要统计和频率的函数
在与 DataFrame 工作的头几天,我们尝试对连续变量的分布和分类变量的计数有一个良好的了解。我们经常按选定的组进行计数。虽然 pandas 和 NumPy 有许多内置方法用于这些目的——describe
、mean
、valuecounts
、crosstab
等等——数据分析师通常对如何使用这些工具有自己的偏好。例如,如果分析师发现他们通常需要看到比describe
生成的更多的百分位数,他们可以使用自己的函数代替。我们将在这个示例中创建用于显示摘要统计和频率的用户定义函数。
准备工作
在这个示例中,我们将再次使用 basicdescriptives
模块。我们将定义的所有函数都保存在该模块中。我们将继续使用 NLS 数据。
如何做…
我们将使用我们创建的函数生成摘要统计和计数:
- 在
basicdescriptives
模块中创建gettots
函数。
该函数接受一个 pandas DataFrame,并创建一个包含选定摘要统计的字典。它返回一个 pandas DataFrame:
def gettots(df):
... out = {}
... out['min'] = df.min()
... out['per15'] = df.quantile(0.15)
... out['qr1'] = df.quantile(0.25)
... out['med'] = df.median()
... out['qr3'] = df.quantile(0.75)
... out['per85'] = df.quantile(0.85)
... out['max'] = df.max()
... out['count'] = df.count()
... out['mean'] = df.mean()
... out['iqr'] = out['qr3']-out['qr1']
... return pd.DataFrame(out)
- 导入
pandas
、os
和sys
库。
从一个不同的文件中执行此操作,您可以将其命名为 taking_measure.py
:
import pandas as pd
import os
import sys
nls97 = pd.read_csv("data/nls97g.csv", low_memory=False)
nls97.set_index('personid', inplace=True)
-
导入
basicdescriptives
模块:sys.path.append(os.getcwd() + "/helperfunctions") import basicdescriptives as bd
-
显示连续变量的汇总统计。
使用我们在步骤 1中创建的basicdescriptives
模块中的gettots
函数:
bd.gettots(nls97[['satverbal','satmath']]).T
satverbal satmath
min 14 7
per15 390 390
qr1 430 430
med 500 500
qr3 570 580
per85 620 621
max 800 800
count 1,406 1,407
mean 500 501
iqr 140 150
bd.gettots(nls97.filter(like="weeksworked"))
min per15 qr1 ... count mean iqr
weeksworked00 0 0 5 ... 8626 26 45
weeksworked01 0 0 10 ... 8591 30 41
weeksworked02 0 0 13 ... 8591 32 39
weeksworked03 0 0 14 ... 8535 34 38
weeksworked04 0 1 17 ... 8513 35 35
weeksworked05 0 5 22 ... 8468 37 31
weeksworked06 0 9 27 ... 8419 38 25
weeksworked07 0 10 30 ... 8360 39 22
weeksworked08 0 9 30 ... 8292 39 22
weeksworked09 0 0 22 ... 8267 38 30
weeksworked10 0 0 21 ... 8198 37 31
weeksworked11 0 0 22 ... 8123 38 31
weeksworked12 0 0 23 ... 7988 38 29
weeksworked13 0 0 28 ... 7942 39 24
weeksworked14 0 0 26 ... 7896 39 26
weeksworked15 0 0 33 ... 7767 40 19
weeksworked16 0 0 31 ... 7654 40 22
weeksworked17 0 0 38 ... 7496 40 14
weeksworked18 0 0 35 ... 7435 40 17
weeksworked19 0 4 42 ... 7237 41 10
weeksworked20 0 0 21 ... 6971 38 31
weeksworked21 0 0 35 ... 6627 36 15
weeksworked22 0 0 2 ... 2202 11 17
[23 rows x 10 columns]
- 创建一个函数,通过列和行来计算缺失值的数量。
getmissings
函数将接受一个 DataFrame 和一个显示百分比或计数的参数。它返回两个 Series,一个显示每列的缺失值,另一个显示每行的缺失值。将该函数保存到basicdescriptives
模块中:
def getmissings(df, byrowperc=False):
return df.isnull().sum(),\
df.isnull().sum(axis=1).\
value_counts(normalize=byrowperc).\
sort_index()
- 调用
getmissings
函数。
首先调用它,将byrowperc
(第二个参数)设置为True
。这样可以显示每行缺失值数量的百分比。例如,missingbyrows
值显示weeksworked20
和weeksworked21
的 73%的行没有缺失值。然后再次调用它,保持byrowperc
为默认值False
,以获取计数:
missingsbycols, missingsbyrows = \
bd.getmissings(nls97[['weeksworked20',
'weeksworked21']], True)
missingsbycols
weeksworked20 2013
weeksworked21 2357
dtype: int64
missingsbyrows
0 0.73
1 0.05
2 0.22
Name: proportion, dtype: float64
missingsbycols, missingsbyrows = \
bd.getmissings(nls97[['weeksworked20',
'weeksworked21']])
missingsbyrows
0 6594
1 410
2 1980
Name: count, dtype: int64
- 创建一个函数,计算所有类别变量的频率。
makefreqs
函数遍历传递的 DataFrame 中所有类别数据类型的列,并对每一列运行value_counts
。频率将保存到由outfile
指定的文件中:
def makefreqs(df, outfile):
... freqout = open(outfile, 'w')
... for col in df.\
... select_dtypes(include=["category"]):
... print(col, "----------------------",
... "frequencies",
... df[col].value_counts().sort_index(),
... "percentages",
... df[col].value_counts(normalize=True).\
... sort_index(),
... sep="\n\n", end="\n\n\n",
... file=freqout)
... freqout.close()
- 调用
makefreqs
函数。
首先,将每个对象列的数据类型更改为category
。此调用会对 NLS DataFrame 中的类别数据列运行value_counts
,并将频率保存到当前文件夹中的views
子文件夹下的nlsfreqs.txt
文件中。
nls97.loc[:, nls97.dtypes == 'object'] = \
... nls97.select_dtypes(['object']). \
... apply(lambda x: x.astype('category'))
bd.makefreqs(nls97, "views/nlsfreqs.txt")
- 创建一个函数,根据组来获取计数。
getcnts
函数计算cats
(一个列名列表)中每一组合列值的行数。它还计算排除cats
中最后一列后的每一组合列值的行数。这将提供最后一列所有值的总数。(接下来的步骤会展示它的效果。)
def getcnts(df, cats, rowsel=None):
... tots = cats[:-1]
... catcnt = df.groupby(cats, dropna=False).size().\
... reset_index(name='catcnt')
... totcnt = df.groupby(tots, dropna=False).size().\
... reset_index(name='totcnt')
... percs = pd.merge(catcnt, totcnt, left_on=tots,
... right_on=tots, how="left")
... percs['percent'] = percs.catcnt / percs.totcnt
... if (rowsel is not None):
... percs = percs.loc[eval("percs." + rowsel)]
... return percs
- 将
maritalstatus
和colenroct00
列传递给getcnts
函数。
这将返回一个 DataFrame,包含每个列值组合的计数,以及排除最后一列后的所有列值组合的计数。此结果用于计算组内的百分比。例如,有 669 名受访者已离婚,其中 560 人(即 84%)在 2000 年 10 月时没有入学:
bd.getcnts(nls97,
['maritalstatus','colenroct00'])
maritalstatus colenroct00 catcnt totcnt percent
0 Divorced 1\. Not enrolled 560 669 0.84
1 Divorced 2\. 2-year college 50 669 0.07
2 Divorced 3\. 4-year college 59 669 0.09
3 Married 1\. Not enrolled 2264 3068 0.74
4 Married 2\. 2-year college 236 3068 0.08
5 Married 3\. 4-year college 568 3068 0.19
6 Never-married 1\. Not enrolled 2363 2767 0.85
7 Never-married 2\. 2-year college 131 2767 0.05
8 Never-married 3\. 4-year college 273 2767 0.10
9 Separated 1\. Not enrolled 127 148 0.86
10 Separated 2\. 2-year college 13 148 0.09
11 Separated 3\. 4-year college 8 148 0.05
12 Widowed 1\. Not enrolled 19 23 0.83
13 Widowed 2\. 2-year college 1 23 0.04
14 Widowed 3\. 4-year college 3 23 0.13
15 NaN 1\. Not enrolled 1745 2309 0.76
16 NaN 2\. 2-year college 153 2309 0.07
17 NaN 3\. 4-year college 261 2309 0.11
18 NaN NaN 150 2309 0.06
-
使用
getcnts
的rowsel
参数限制输出为特定行。这将仅显示未入学的行:bd.getcnts(nls97, ['maritalstatus','colenroct20'], "colenroct20.str[0:1]=='1'")
maritalstatus colenroct00 catcnt totcnt percent 0 Divorced 1\. Not enrolled 560 669 0.84 3 Married 1\. Not enrolled 2264 3068 0.74 6 Never-married 1\. Not enrolled 2363 2767 0.85 9 Separated 1\. Not enrolled 127 148 0.86 12 Widowed 1\. Not enrolled 19 23 0.83 15 NaN 1\. Not enrolled 1745 2309 0.76
这些步骤展示了如何创建函数并使用它们生成汇总统计和频率。
它是如何工作的…
在步骤 1中,我们创建了一个名为gettots
的函数,用于计算 DataFrame 中所有列的描述性统计,并将这些结果返回为一个汇总的 DataFrame。大多数统计量可以通过describe
方法生成,但我们添加了一些额外的统计量——第 15 百分位数、第 85 百分位数和四分位距。我们在步骤 4中调用了这个函数两次,第一次是针对 SAT 语言和数学成绩,第二次是针对所有工作周数的列。
步骤 5 和 步骤 6 创建并调用一个函数,显示传递给 DataFrame 的每列的缺失值数量。该函数还计算每行的缺失值数量,并显示缺失值的频率。通过将True
传递给byrowperc
参数,还可以将每行的缺失值频率显示为所有行的百分比。
步骤 7 和 步骤 8 会生成一个文本文件,其中包含传递给 DataFrame 的所有分类变量的频率。我们只需循环遍历所有类别数据类型的列,并运行value_counts
。由于输出通常很长,我们将其保存到文件中。将频率保存在某处以供以后参考也是很有用的。
我们在 步骤 9 中创建并在 步骤 10 和 步骤 11 中调用的getcnts
函数有点特殊。pandas 有一个非常有用的crosstab
函数,我经常使用。但我经常需要一种简单的方法来查看组内子组的组计数和百分比。getcnts
函数可以做到这一点。
还有更多…
即使一个函数没有做很多事情,它也可能非常有帮助。getmissings
函数中的代码并不多,但我经常检查缺失值,所以小小的时间节省在累积起来时是显著的。它还提醒我按列和按行检查缺失值。
另请参阅
我们将在 第三章,测量数据中探索 pandas 用于生成摘要统计信息和频率的工具。
用于识别异常值和意外值的函数
如果我必须选择一个数据清理领域,我发现可重复使用的代码最有益处的领域,那就是识别异常值和意外值。这是因为我们的先前假设通常导致我们关注分布的中心趋势,而不是极端值。快速想象一只猫——除非你在想你生活中的某只特定猫,否则你脑海中可能会浮现一只体重在 8 到 10 磅之间的普通猫;而不是一只体重为 6 磅或 22 磅的猫。
我们经常需要更有意识地提升极端值到意识中。在这里,拥有一套标准的诊断函数来运行我们的数据非常有帮助。即使没有特别的触发条件,我们也可以运行这些函数。这个示例提供了我们可以定期使用的函数示例,用于识别异常值和意外值。
准备工作
在这个示例中,我们将创建两个文件,一个包含我们将用来检查异常值的函数,另一个包含我们将用来调用这些函数的代码。让我们把包含我们将使用的函数的文件命名为outliers.py
,并将其放在一个名为helperfunctions
的子文件夹中。
为了运行本示例中的代码,你需要matplotlib
和scipy
库,除了 pandas。你可以通过在终端客户端或 Windows PowerShell 中输入pip install matplotlib
和pip install scipy
来安装matplotlib
和scipy
。你还需要pprint
实用程序,你可以通过pip install pprint
来安装。
在本示例中,我们将处理 NLS 和 COVID-19 数据。COVID-19 数据中每一行代表一个国家,包含该国的累计病例和死亡数。
数据说明
Our World in Data 提供了 COVID-19 公共使用数据,网址:ourworldindata.org/covid-cases
。该数据集包括各国的累计病例和死亡数、已进行的测试、医院床位以及一些人口统计数据,如中位年龄、国内生产总值和糖尿病患病率。本示例使用的数据集是在 2024 年 3 月 3 日下载的。
如何操作…
我们创建并调用函数来检查变量的分布、列出极端值并可视化分布:
- 导入
pandas
、os
、sys
和pprint
库。
同时,加载 NLS 和 COVID-19 数据:
import pandas as pd
import os
import sys
import pprint
nls97 = pd.read_csv("data/nls97g.csv", low_memory=False)
nls97.set_index('personid', inplace=True)
covidtotals = pd.read_csv("data/covidtotals.csv")
- 创建一个函数来展示分布的一些重要属性。
getdistprops
函数接受一个 Series 并生成中心趋势、形状和分布的度量。该函数返回一个包含这些度量的字典。它还处理 Shapiro 正态性检验未返回值的情况。若发生这种情况,将不会为normstat
和normpvalue
添加键。将该函数保存在当前目录下helperfunctions
子文件夹中的一个名为outliers.py
的文件中。(同时加载我们在此模块中其他函数需要的pandas
、matplotlib
、scipy
和math
库。)
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as scistat
import math
def getdistprops(seriestotest):
... out = {}
... normstat, normpvalue = scistat.shapiro(seriestotest)
... if (not math.isnan(normstat)):
... out['normstat'] = normstat
... if (normpvalue>=0.05):
... out['normpvalue'] = str(round(normpvalue, 2)) + ": Accept Normal"
... elif (normpvalue<0.05):
... out['normpvalue'] = str(round(normpvalue, 2)) + ": Reject Normal"
... out['mean'] = seriestotest.mean()
... out['median'] = seriestotest.median()
... out['std'] = seriestotest.std()
... out['kurtosis'] = seriestotest.kurtosis()
... out['skew'] = seriestotest.skew()
... out['count'] = seriestotest.count()
... return out
- 将每百万总病例数的 Series 传递给
getdistprops
函数。
skew
和kurtosis
值表明total_cases_pm
的分布有正偏态且尾部比正态分布的变量更短。Shapiro 正态性检验(normpvalue
)证实了这一点。(使用pprint
改进getdistprops
返回的字典的显示效果。)
dist = ol.getdistprops(covidtotals.total_cases_pm)
pprint.pprint(dist)
{'count': 231,
'kurtosis': -0.4280595203351645,
'mean': 206177.79462337663,
'median': 133946.251,
'normpvalue': '0.0: Reject Normal',
'normstat': 0.8750641345977783,
'skew': 0.8349032460009967,
'std': 203858.09625231632}
- 创建一个函数来列出 DataFrame 中的异常值。
getoutliers
函数遍历sumvars
中的所有列。它为这些列确定异常值阈值,设置阈值为第一四分位数下方或第三四分位数上方 1.5 倍四分位间距的值。然后,它选择所有超出高阈值或低阈值的行。它还会添加表示检查的变量(varname
)的列以及阈值水平的列。它还会包括othervars
列表中的列,并将其返回为 DataFrame:
def getoutliers(dfin, sumvars, othervars):
... dfin = dfin[sumvars + othervars]
... dfout = pd.DataFrame(columns=dfin.columns, data=None)
... dfsums = dfin[sumvars]
... for col in dfsums.columns:
... thirdq, firstq = dfsums[col].quantile(0.75),\
... dfsums[col].quantile(0.25)
... interquartilerange = 1.5*(thirdq-firstq)
... outlierhigh, outlierlow = interquartilerange+thirdq,\
... firstq-interquartilerange
... df = dfin.loc[(dfin[col]>outlierhigh) | \
... (dfin[col]<outlierlow)]
... df = df.assign(varname = col, threshlow = outlierlow,\
... threshhigh = outlierhigh)
... dfout = pd.concat([dfout, df])
... return dfout
- 调用
getoutlier
函数。
将检查异常值的列的列表(sumvars
)和要包含在返回的 DataFrame 中的列的另一个列表(othervars
)传递给函数。展示每个变量的异常值计数,并查看 SAT 数学的异常值:
sumvars = ['satmath','wageincome20']
othervars = ['originalid','highestdegree','gender','maritalstatus']
outliers = ol.getoutliers(nls97, sumvars, othervars)
outliers.varname.value_counts(sort=False)
varname
satmath 10
wageincome20 234
Name: count, dtype: int64
outliers.loc[outliers.varname=='satmath', othervars + sumvars]
originalid highestdegree ... satmath wageincome20
337438 159 2\. High School ... 200.00 32,000.00
448463 326 4\. Bachelors ... 47.00 NaN
799095 535 5\. Masters ... 59.00 NaN
267254 1622 2\. High School ... 48.00 NaN
955430 2547 2\. High School ... 200.00 NaN
748274 3394 4\. Bachelors ... 42.00 NaN
399109 3883 2\. High School ... 36.00 37,000.00
223058 6696 0\. None ... 46.00 NaN
291029 7088 2\. High School ... 51.00 NaN
738290 7705 2\. High School ... 7.00 50,000.00
[10 rows x 6 columns]
outliers.to_excel("views/nlsoutliers.xlsx")
- 创建一个生成直方图和箱型图的函数。
makeplot
函数接受一个 Series、标题和x-轴的标签。默认绘图类型为直方图:
def makeplot(seriestoplot, title, xlabel, plottype="hist"):
... if (plottype=="hist"):
... plt.hist(seriestoplot)
... plt.axvline(seriestoplot.mean(), color='red',\
... linestyle='dashed', linewidth=1)
... plt.xlabel(xlabel)
... plt.ylabel("Frequency")
... elif (plottype=="box"):
... plt.boxplot(seriestoplot.dropna(), labels=[xlabel])
... plt.title(title)
... plt.show()
-
调用
makeplot
函数来创建直方图:ol.makeplot(nls97.satmath, "Histogram of SAT Math", "SAT Math")
这将生成以下直方图:
图 12.1:SAT 数学值的频率分布
-
使用
makeplot
函数创建一个箱线图:ol.makeplot(nls97.satmath, "Boxplot of SAT Math", "SAT Math", "box")
这将生成以下箱线图:
图 12.2:使用箱线图显示中位数、四分位距和异常值阈值
前面的步骤展示了我们如何开发可重复使用的代码来检查异常值和意外值。
工作原理…
我们首先通过将 Series 传递给步骤 3中的getdistprop
函数来获取分布的关键属性,包括均值、中位数、标准差、偏度和峰度。我们得到一个包含这些度量值的字典。
步骤 4中的函数选择了sumvars
中某一列具有异常值的行。它还包括了othervars
列的值和返回的 DataFrame 中的阈值金额。
我们在步骤 6中创建了一个函数,使得创建简单直方图或箱线图变得更加容易。matplotlib
的功能很强大,但是当我们只想创建一个简单的直方图或箱线图时,可能需要花一点时间来回想语法。我们可以通过定义一个带有几个常规参数的函数来避免这种情况:Series、标题和x-label。我们在步骤 7和8中调用该函数。
还有更多…
在对连续变量进行过多处理之前,我们需要先了解其数值分布情况;中心趋势和分布形状是什么?如果我们对关键连续变量运行类似本示例中的函数,那么我们就会有一个良好的起点。
Python 模块的相对轻松可移植性使得这变得相当容易。如果我们想使用本示例中使用的outliers
模块,只需将outliers.py
文件保存到我们的程序可以访问的文件夹中,将该文件夹添加到 Python 路径中,并导入它。
通常,当我们检查极端值时,我们希望更好地了解其他变量的背景,这些变量可能解释为什么该值是极端的。例如,178 厘米的身高对于成年男性来说不是异常值,但对于 9 岁的孩子来说绝对是异常值。步骤 4和5生成的 DataFrame 为我们提供了异常值以及可能相关的其他数据。将数据保存到 Excel 文件中使得以后检查异常行或与他人分享数据变得更加容易。
另请参阅
我们在第四章中详细讨论了如何检测异常值和意外值,在数据子集中识别异常值。我们在第五章中研究了直方图、箱线图和许多其他可视化方法,使用可视化方法识别意外值。
用于聚合或合并数据的函数
大多数数据分析项目都需要对数据进行某种形状的调整。我们可能需要按组聚合数据,或者纵向或横向合并数据。在准备数据以进行这些形状调整时,我们每次都会进行类似的任务。我们可以通过函数将其中一些任务标准化,从而提高代码的可靠性和完成工作的效率。有时我们需要在合并之前检查按值合并的列是否匹配,检查面板数据在一个周期到下一个周期之间的值是否发生了意外变化,或者一次性连接多个文件并验证数据是否已正确合并。
这些只是数据聚合和组合任务的一些示例,这些任务可能更适合使用更通用的编码解决方案。在这个示例中,我们定义了可以帮助完成这些任务的函数。
准备工作
在这个示例中,我们将使用 COVID-19 每日数据。该数据包括按天计算的每个国家的新病例和新死亡人数。我们还将使用 2023 年多个国家的土地温度数据。每个国家的数据在单独的文件中,并且每个月的每个气象站有一行数据。
数据说明
土地温度数据框包含来自全球超过 12,000 个站点的 2023 年平均温度数据(单位:°C),尽管大多数站点位于美国。原始数据来自全球历史气候学网络集成数据库。美国国家海洋和大气管理局在www.ncei.noaa.gov/products/land-based-station/global-historical-climatology-network-monthly
上为公众提供了这些数据。
如何实现…
我们将使用函数来聚合数据,纵向合并数据,并检查按值合并:
-
导入
pandas
、os
和sys
库:import pandas as pd import os import sys
-
创建一个函数(
adjmeans
)来按组汇总每个周期的值。
按组(byvar
)和然后按period
对传入的数据框的值进行排序。将数据框的值转换为 NumPy 数组。循环遍历这些值,按var
列做一个累加,并在遇到byvar
的新值时将累加值重置为 0。在进行聚合之前,检查每个周期之间值的极端变化。changeexclude
参数表示从一个周期到下一个周期应该视为极端变化的大小。excludetype
参数表示changeexclude
值是var
列均值的绝对值还是百分比。在helperfunctions
子文件夹中将该函数保存在名为combineagg.py
的文件中:
def adjmeans(df, byvar, var, period, changeexclude=None, excludetype=None):
... df = df.sort_values([byvar, period])
... df = df.dropna(subset=[var])
... # iterate using numpy arrays
... prevbyvar = 'ZZZ'
... prevvarvalue = 0
... rowlist = []
... varvalues = df[[byvar, var]].values
... # convert exclusion ratio to absolute number
... if (excludetype=="ratio" and changeexclude is not None):
... changeexclude = df[var].mean()*changeexclude
... # loop through variable values
... for j in range(len(varvalues)):
... byvar = varvalues[j][0]
... varvalue = varvalues[j][1]
... if (prevbyvar!=byvar):
... if (prevbyvar!='ZZZ'):
... rowlist.append({'byvar':prevbyvar, 'avgvar':varsum/byvarcnt,\
... 'sumvar':varsum, 'byvarcnt':byvarcnt})
... varsum = 0
... byvarcnt = 0
... prevbyvar = byvar
... # exclude extreme changes in variable value
... if ((changeexclude is None) or (0 <= abs(varvalue-prevvarvalue) \
... <= changeexclude) or (byvarcnt==0)):
... varsum += varvalue
... byvarcnt += 1
... prevvarvalue = varvalue
... rowlist.append({'byvar':prevbyvar, 'avgvar':varsum/byvarcnt, \
... 'sumvar':varsum, 'byvarcnt':byvarcnt})
... return pd.DataFrame(rowlist)
-
导入
combineagg
模块:sys.path.append(os.getcwd() + "/helperfunctions") import combineagg as ca
-
加载数据框(DataFrames):
coviddaily = pd.read_csv("data/coviddaily.csv") ltbrazil = pd.read_csv("data/ltbrazil.csv") countries = pd.read_csv("data/ltcountries.csv") locations = pd.read_csv("data/ltlocations.csv")
-
调用
adjmeans
函数按组和时间周期汇总面板数据。
指明我们希望按location
对new_cases
进行汇总:
ca.adjmeans(coviddaily, 'location','new_cases','casedate')
byvar avgvar sumvar byvarcnt
0 Afghanistan 1,129 231,539 205
1 Albania 1,914 334,863 175
2 Algeria 1,439 272,010 189
3 American Samoa 144 8,359 58
4 Andorra 304 48,015 158
.. ... ... ... ...
226 Vietnam 60,542 11,624,000 192
227 Wallis and Futuna 154 3,550 23
228 Yemen 98 11,945 122
229 Zambia 2,019 349,304 173
230 Zimbabwe 1,358 266,266 196
[231 rows x 4 columns]
-
再次调用
adjmeans
函数,这次排除new_cases
从一天到下一天的变化超过 5,000 的值。注意,一些国家的计数有所减少:ca.adjmeans(coviddaily, 'location','new_cases','casedate', 5000)
byvar avgvar sumvar byvarcnt 0 Afghanistan 1,129 231,539 205 1 Albania 1,855 322,772 174 2 Algeria 1,290 239,896 186 3 American Samoa 144 8,359 58 4 Andorra 304 48,015 158 .. ... ... ... ... 226 Vietnam 6,410 967,910 151 227 Wallis and Futuna 154 3,550 23 228 Yemen 98 11,945 122 229 Zambia 1,555 259,768 167 230 Zimbabwe 1,112 214,526 193 [231 rows x 4 columns]
-
创建一个函数来检查一个文件中合并列的值,但在另一个文件中没有这些值。
checkmerge
函数对传入的两个 DataFrame 进行外连接,使用第三个和第四个参数作为第一个和第二个 DataFrame 的合并列。然后,它生成一个交叉表,显示在两个 DataFrame 中都存在的合并列值的行数,以及只出现在一个 DataFrame 中而另一个 DataFrame 中不存在的行数。它还会显示最多 20 行只在一个文件中找到的合并列值的数据:
def checkmerge(dfleft, dfright, mergebyleft, mergebyright):
... dfleft['inleft'] = "Y"
... dfright['inright'] = "Y"
... dfboth = pd.merge(dfleft[[mergebyleft,'inleft']],\
... dfright[[mergebyright,'inright']], left_on=[mergebyleft],\
... right_on=[mergebyright], how="outer")
... dfboth.fillna('N', inplace=True)
... print(pd.crosstab(dfboth.inleft, dfboth.inright))
... print(dfboth.loc[(dfboth.inleft=='N') | (dfboth.inright=='N')].head(20))
- 调用
checkmerge
函数。
检查countries
土地温度 DataFrame(每个国家一行)和locations
DataFrame(每个国家每个气象站一行)之间的合并。交叉表显示,27,472 个合并列值同时存在于两个 DataFrame 中,两个值只在countries
文件中,而不在locations
文件中,一个值只在locations
文件中,而不在countries
文件中:
ca.checkmerge(countries.copy(), locations.copy(),\
... "countryid", "countryid")
inright N Y
inleft
N 0 1
Y 2 27472
countryid inleft inright
7363 FO N Y
9716 LQ Y N
13104 ST Y N
- 创建一个函数,用于连接文件夹中的所有 CSV 文件。
该函数遍历指定文件夹中的所有文件名。它使用endswith
方法检查文件名是否具有 CSV 文件扩展名。然后,它加载 DataFrame 并打印出行数。最后,它使用concat
将新 DataFrame 的行追加到已追加的行中。如果文件中的列名不同,它会打印出这些列名:
def addfiles(directory):
... dfout = pd.DataFrame()
... columnsmatched = True
... # loop through the files
... for filename in os.listdir(directory):
... if filename.endswith(".csv"):
... fileloc = os.path.join(directory, filename)
... # open the next file
... with open(fileloc) as f:
... dfnew = pd.read_csv(fileloc)
... print(filename + " has " + str(dfnew.shape[0]) + " rows.")
... dfout = pd.concat([dfout, dfnew])
... # check if current file has any different columns
... columndiff = dfout.columns.symmetric_difference(dfnew.columns)
... if (not columndiff.empty):
... print("", "Different column names for:", filename,\
... columndiff, "", sep="\n")
... columnsmatched = False
... print("Columns Matched:", columnsmatched)
... return dfout
- 使用
addfiles
函数连接所有countries
土地温度文件。
看起来阿曼(ltoman
)的文件稍有不同。它没有latabs
列。注意,合并后的 DataFrame 中每个国家的行数与每个国家文件中的行数相匹配:
landtemps = ca.addfiles("data/ltcountry")
ltpoland.csv has 120 rows.
ltcameroon.csv has 48 rows.
ltmexico.csv has 852 rows.
ltjapan.csv has 1800 rows.
ltindia.csv has 1116 rows.
ltoman.csv has 288 rows.
Different column names for:
ltoman.csv
Index(['latabs'], dtype='object')
ltbrazil.csv has 1008 rows.
Columns Matched: False
landtemps.country.value_counts()
country
Japan 1800
India 1116
Brazil 1008
Mexico 852
Oman 288
Poland 120
Cameroon 48
Name: count, dtype: int64
上述步骤演示了我们如何将一些杂乱的数据重塑工作系统化。我相信你可以想到许多其他可能有用的函数。
它是如何工作的……
你可能注意到,在步骤 2中定义的adjmeans
函数中,我们实际上并不会在到达下一个byvar
列值之前追加var
列值的汇总。这是因为,直到我们到达下一个byvar
列值时,无法知道我们是否已经到了某个byvar
值的最后一行。这不是问题,因为我们会在重置为0
之前将汇总追加到rowlist
中。这也意味着,我们需要做一些特别的处理来输出最后一个byvar
值的总计,因为没有下一个byvar
值。这是通过在循环结束后进行最终的追加操作来实现的。
在步骤 5中,我们调用了步骤 2中定义的adjmeans
函数。由于我们没有为changeexclude
参数设置值,该函数会将所有值包含在聚合中。这将给我们与使用groupby
和聚合函数相同的结果。然而,当我们传递一个参数给changeexclude
时,我们就能确定从聚合中排除哪些行。在步骤 6中,调用adjmeans
时的第五个参数表示我们应该排除新案例值与前一天的值相差超过 5000 个案例的行。
步骤 9中的函数在要合并的数据文件结构相同或几乎相同时效果很好。当列名不同时,我们会打印出警告,正如步骤 10所示。latabs
列在阿曼文件中不存在。这意味着在合并后的文件中,阿曼的所有行将缺少latabs
这一列。
还有更多…
adjmeans
函数在将每个新值加入总数之前会进行相对简单的检查。但我们也可以想象更加复杂的检查。我们甚至可以在adjmeans
函数中调用另一个函数,用来决定是否包含某行数据。
另见
我们将在第十章《合并数据框时处理数据问题》中讨论垂直和水平合并 DataFrame。
包含更新 Series 值逻辑的类
我们有时会长时间处理一个特定的数据集,甚至可能是几年。数据可能会定期更新,添加新的月份或年份,或者增加额外的个体,但数据结构可能相对稳定。如果该数据集还包含大量列,我们通过实现类,可能会提高代码的可靠性和可读性。
当我们创建类时,会定义对象的属性和方法。在我的数据清理工作中,我倾向于将类概念化为代表我的分析单元。所以,如果我的分析单元是学生,那么我会创建一个学生类。由该类创建的每个学生实例可能具有出生日期和性别属性,以及课程注册方法。我还可以为校友创建一个子类,继承学生类的方法和属性。
NLS DataFrame 的数据清理可以通过类来很好地实现。该数据集在过去 25 年里相对稳定,无论是变量还是每个变量的允许值。我们在本食谱中探索如何为 NLS 调查响应创建一个 respondent 类。
准备工作
为了运行本食谱中的代码,你需要在当前目录中创建一个名为helperfunctions
的子文件夹。我们将把新类的文件(respondent.py
)保存在这个子文件夹中。
如何操作…
我们将定义一个 respondent 类,根据 NLS 数据创建几个新的 Series:
- 导入
pandas
、os
、sys
和pprint
库。
我们将这段代码存储在一个不同于保存响应者类的文件中。我们将这个文件命名为class_cleaning.py
。我们将在这个文件中实例化响应者对象:
import pandas as pd
import os
import sys
import pprint
- 创建一个
Respondent
类,并将其保存到helperfunctions
子文件夹中的respondent.py
文件中。
当我们调用我们的类(实例化类对象)时,__init__
方法会自动运行。(init
前后都有双下划线。)__init__
方法的第一个参数是self
,正如任何实例方法一样。此类的__init__
方法还有一个respdict
参数,它期望一个来自 NLS 数据的字典值。在后续步骤中,我们将为 NLS DataFrame 中的每一行数据实例化一个响应者对象。
__init__
方法将传递的respdict
值赋给self.respdict
,以创建一个实例变量,我们可以在其他方法中引用它。最后,我们递增一个计数器respondentcnt
。稍后我们可以用它来确认我们创建的respondent
实例的数量。我们还导入了math
和datetime
模块,因为稍后会需要它们。(请注意,类名通常是大写的。)
import math
import datetime as dt
class Respondent:
... respondentcnt = 0
... def __init__(self, respdict):
... self.respdict = respdict
... Respondent.respondentcnt+=1
- 添加一个方法,用于计算孩子的数量。
这是一个非常简单的方法,它将与响应者同住的孩子数和不与响应者同住的孩子数相加,得到孩子的总数。它使用self.respdict
字典中的childathome
和childnotathome
键值:
def childnum(self):
... return self.respdict['childathome'] + self.respdict['childnotathome']
- 添加一个方法,用于计算调查中 25 年期间的平均工作周数。
使用字典推导式创建一个字典(workdict
),其中包含没有缺失值的工作周数键。将workdict
中的值相加并除以workdict
的长度:
def avgweeksworked(self):
... workdict = {k: v for k, v in self.respdict.items() \
... if k.startswith('weeksworked') and not math.isnan(v)}
... nweeks = len(workdict)
... if (nweeks>0):
... avgww = sum(workdict.values())/nweeks
... else:
... avgww = 0
... return avgww
- 添加一个方法,用于计算某个特定日期的年龄。
此方法采用一个日期字符串(bydatestring
)作为年龄计算的结束日期。我们使用datetime
模块将date
字符串转换为datetime
对象bydate
。我们从self.respdict
中减去出生年份值,并从该计算中减去 1,如果出生日期在该年还没有发生。(我们在 NLS 数据中只有出生月和出生年,所以我们选择 15 作为中间点。)
def ageby(self, bydatestring):
... bydate = dt.datetime.strptime(bydatestring, '%Y%m%d')
... birthyear = self.respdict['birthyear']
... birthmonth = self.respdict['birthmonth']
... age = bydate.year - birthyear
... if (bydate.month<birthmonth or (bydate.month==birthmonth \
... and bydate.day<15)):
... age = age -1
... return age
- 添加一个方法,如果响应者曾经在 4 年制大学注册过,则创建一个标志。
使用字典推导式检查是否有任何大学注册值为 4 年制大学:
def baenrollment(self):
... colenrdict = {k: v for k, v in self.respdict.items() \
... if k.startswith('colenr') and v=="3\. 4-year college"}
... if (len(colenrdict)>0):
... return "Y"
... else:
... return "N"
- 导入响应者类。
现在我们已经准备好实例化一些Respondent
对象了!我们从步骤 1开始的class_cleaning.py
文件中进行操作。首先,我们导入响应者类。(此步骤假设respondent.py
文件位于helperfunctions
子文件夹中。)
sys.path.append(os.getcwd() + "/helperfunctions")
import respondent as rp
- 加载 NLS 数据并创建字典列表。
使用to_dict
方法创建字典列表(nls97list
)。DataFrame 中的每一行将是一个字典,列名作为键。显示第一个字典的一部分(第一行):
nls97 = pd.read_csv("data/nls97g.csv", low_memory=False)
nls97list = nls97.to_dict('records')
nls97.shape
(8984, 111)
len(nls97list)
8984
pprint.pprint(nls97list[0:1])
[{'birthmonth': 9,
'birthyear': 1981,
'childathome': nan,
'childnotathome': nan,
'colenrfeb00': '3\. 4-year college',
'colenrfeb01': '3\. 4-year college',
...
'weeksworked21': nan,
'weeksworked22': nan}]
- 遍历列表,每次创建一个
respondent
实例。
我们将每个字典传递给rp.Respondent(respdict)
响应者类。一旦我们创建了一个响应者对象(resp
),我们就可以使用所有实例方法来获取我们需要的值。我们使用这些方法返回的值创建一个新的字典,然后将该字典追加到analysisdict
中:
analysislist = []
for respdict in nls97list:
... resp = rp.Respondent(respdict)
... newdict = dict(originalid=respdict['originalid'],
... childnum=resp.childnum(),
... avgweeksworked=resp.avgweeksworked(),
... age=resp.ageby('20201015'),
... baenrollment=resp.baenrollment())
... analysislist.append(newdict)
- 将字典传递给 pandas
DataFrame
方法。
首先,检查analysislist
中的项数和创建的实例数:
len(analysislist)
8984
resp.respondentcnt
8984
pprint.pprint(analysislist[0:2])
[{'age': 39,
'avgweeksworked': 48.4375,
'baenrollment': 'Y',
'childnum': nan,
'originalid': 1},
{'age': 38,
'avgweeksworked': 49.90909090909091,
'baenrollment': 'Y',
'childnum': nan,
'originalid': 2}]
analysis = pd.DataFrame(analysislist)
analysis.head(2)
originalid childnum avgweeksworked age baenrollment
0 1 NaN 48 39 Y
1 2 NaN 50 38 Y
这些步骤展示了如何在 Python 中创建一个类,如何向类传递数据,如何创建类的实例,以及如何调用类的方法来更新变量值。
它是如何工作的…
这个配方中的关键工作在第 2 步中完成。它创建了响应者类,并为剩余的步骤做好了准备。我们将每行的值传递给该类的__init__
方法。__init__
方法将该字典分配给一个实例变量,该变量对所有类的方法都可用(self.respdict = respdict
)。
第 3 步到第 6 步使用该字典来计算子女数量、每年平均工作周数、年龄和大学入学情况。第 4 步和第 6 步展示了字典推导式在我们需要对多个键测试相同值时的有用性。字典推导式选择相关的键(weeksworked##
、colenroct##
、colenrfeb##
),并允许我们检查这些键的值。当我们的数据以这种方式杂乱无章时,这非常有用,正如调查数据常常表现出来的那样。
在第 8 步中,我们使用to_dict
方法创建一个字典列表。它包含预期数量的列表项,8,984,与 DataFrame 中的行数相同。我们使用pprint
来显示字典在第一个列表项中的样子。该字典具有列名作为键,列值作为值。
我们在第 9 步中遍历列表,创建一个新的响应者对象并传递列表项。我们调用方法来获取我们想要的值,除了originalid
,它可以直接从字典中提取。我们用这些值创建一个字典(newdict
),并将其追加到列表(analysislist
)中。
在第 10 步中,我们从在第 9 步创建的列表(analysislist
)中创建一个 pandas DataFrame。我们通过将列表传递给 pandas DataFrame 方法来完成这一操作。
还有更多…
我们传递字典给类,而不是数据行,这也是一种选择。我们这么做是因为,相比于通过 itertuples
或 iterrows
遍历 DataFrame,遍历 NumPy 数组更高效。当我们使用字典而非 DataFrame 行时,我们并没有失去类所需的太多功能。我们依然可以使用诸如 sum
和 mean
等函数,并计算符合特定条件的值的数量。
在这种响应者类的概念化中,很难避免必须遍历数据。这个响应者类与我们对分析单元——调查响应者——的理解是一致的。这也正是数据呈现给我们的方式。但即使是更高效的 NumPy 数组,逐行遍历数据也是资源密集型的。
然而,我认为,在处理具有许多列且结构在时间上变化不大的数据时,通过构建像这样的类,你获得的好处超过了失去的。最重要的优势在于它符合我们对数据的直观理解,并将我们的工作集中在理解每个响应者的数据上。我还认为,当我们构建类时,通常会比否则情况减少很多遍历数据的次数。
另见
我们在 第九章,聚合时修复杂乱数据 中,探讨了如何遍历 DataFrame 行和 NumPy 数组。
这是一篇关于在 Python 中使用类的简要介绍。如果你想深入了解 Python 中的面向对象编程,我推荐 Dusty Phillips 编写的 Python 3 面向对象编程,第三版。
处理非表格数据结构的类
数据科学家越来越多地接收到非表格数据,通常是 JSON 或 XML 文件。JSON 和 XML 的灵活性使得组织能够在一个文件中捕捉数据项之间复杂的关系。在企业数据系统中存储在两个表中的一对多关系,可以通过 JSON 通过一个父节点来表示一方,多个子节点来表示多方数据,来很好地表示。
当我们接收 JSON 数据时,我们通常会尝试对其进行规范化。事实上,在本书的一些实例中,我们就是这么做的。我们尝试恢复由于 JSON 灵活性而混淆的数据中的一对一和一对多关系。但也有另一种处理这种数据的方式,它有许多优势。
我们可以创建一个类,在适当的分析单元上实例化对象,并使用类的方法来导航一对多关系的多个方面,而不是规范化数据。例如,如果我们获取一个包含学生节点的 JSON 文件,并且每个学生所修课程都有多个子节点,我们通常会通过创建一个学生文件和一个课程文件来规范化数据,学生 ID 作为两个文件的合并列。在本例中,我们将探讨另一种方法:保持数据原样,创建一个学生类,并创建方法对子节点进行计算,例如计算总学分。
让我们通过这个食谱来尝试,使用来自克利夫兰艺术博物馆的数据,其中包含了收藏项目、每个项目的一个或多个媒体引文节点,以及每个项目的一个或多个创作者节点。
准备工作
本示例假设你已经安装了requests
和pprint
库。如果没有安装,可以通过pip
进行安装。在终端或 PowerShell(Windows 系统中)输入pip install requests
和pip install pprint
。
这里展示了使用克利夫兰艺术博物馆(Cleveland Museum of Art)collections
API 时创建的 JSON 文件结构(我已将 JSON 文件缩短以节省空间)。
{
"id": 165157,
"title": "Fulton and Nostrand",
"creation_date": "1958",
"citations": [
{
"citation": "Annual Exhibition: Sculpture, Paintings, Watercolors, Drawings,
"page_number": "Unpaginated, [8],[12]",
"url": null
},
{
"citation": "\"Moscow to See Modern U.S. Art,\"<em> New York Times</em> (May 31, 1959).",
"page_number": "P. 60",
"url": null
}]
"creators": [
{
"description": "Jacob Lawrence (American, 1917-2000)",
"role": "artist",
"birth_year": "1917",
"death_year": "2000"
}
]
}
数据说明
克利夫兰艺术博物馆提供了一个 API,允许公众访问这些数据:openaccess-api.clevelandart.org/
。通过 API 可以访问的内容远远超过本例中使用的引文和创作者数据。
如何实现…
我们创建了一个收藏项目类,用于汇总创作者和媒体引文的数据:
- 导入
pandas
、json
、pprint
和requests
库。
首先,我们创建一个文件,用来实例化收藏项目对象,命名为class_cleaning_json.py
:
import pandas as pd
import json
import pprint
import requests
- 创建一个
Collectionitem
类。
我们将每个收藏项目的字典传递给类的__init__
方法,该方法在类的实例化时会自动运行。我们将收藏项目字典分配给实例变量。将该类保存为collectionitem.py
文件,并放置在helperfunctions
文件夹中:
class Collectionitem:
... collectionitemcnt = 0
... def __init__(self, colldict):
... self.colldict = colldict
... Collectionitem.collectionitemcnt+=1
- 创建一个方法来获取每个收藏项目的第一个创作者的出生年份。
请记住,收藏项目可能有多个创作者。这意味着creators
键的值可能是一个或多个列表项,这些项本身是字典。要获取第一个创作者的出生年份,我们需要['creators'][0]['birth_year']
。我们还需要考虑出生年份键可能缺失的情况,因此首先要进行检查:
def birthyearcreator1(self):
... if ("birth_year" in self.colldict['creators'][0]):
... byear = self.colldict['creators'][0]['birth_year']
... else:
... byear = "Unknown"
... return byear
- 创建一个方法来获取所有创作者的出生年份。
使用列表推导式循环遍历所有creators
项。这将返回一个包含出生年份的列表:
def birthyearsall(self):
... byearlist = [item.get('birth_year') for item in \
... self.colldict['creators']]
... return byearlist
-
创建一个方法来统计创作者的数量:
def ncreators(self): ... return len(self.colldict['creators'])
-
创建一个方法来统计媒体引文的数量:
def ncitations(self): ... return len(self.colldict['citations'])
-
导入
collectionitem
模块。
我们从步骤 1创建的class_cleaning_json.py
文件中执行此操作:
sys.path.append(os.getcwd() + "/helperfunctions")
import collectionitem as ci
- 加载艺术博物馆的收藏数据。
这返回的是一个字典列表。我们只提取了带有非裔美国艺术家数据的博物馆收藏子集:
response = requests.get("https://openaccess-api.clevelandart.org/api/artworks/?african_american_artists")
camcollections = json.loads(response.text)
camcollections = camcollections['data']
- 遍历
camcollections
列表。
为camcollections
中的每个项目创建一个集合项实例。将每个项目(即包含集合、创作者和引用键的字典)传递给类。调用我们刚刚创建的方法,并将它们返回的值分配给一个新的字典(newdict
)。将该字典附加到一个列表(analysislist
)中。(一些值可以直接从字典中提取,如title=colldict['title']
,因为我们不需要以任何方式更改其值。)
analysislist = []
for colldict in camcollections:
... coll = ci.Collectionitem(colldict)
... newdict = dict(id=colldict['id'],
... title=colldict['title'],
... type=colldict['type'],
... creationdate=colldict['creation_date'],
... ncreators=coll.ncreators(),
... ncitations=coll.ncitations(),
... birthyearsall=coll.birthyearsall(),
... birthyear=coll.birthyearcreator1())
... analysislist.append(newdict)
- 使用新的字典列表创建一个分析 DataFrame。
确认我们获得了正确的计数,并打印第一个项目的字典:
len(camcollections)
1000
len(analysislist)
1000
pprint.pprint(analysislist[0:1])
[{'birthyear': '1917',
'birthyearsall': ['1917'],
'creationdate': '1958',
'id': 165157,
'ncitations': 30,
'ncreators': 1,
'title': 'Fulton and Nostrand',
'type': 'Painting'}]
analysis = pd.DataFrame(analysislist)
analysis.birthyearsall.value_counts().head()
birthyearsall
[1951] 283
[1953] 119
[1961, None] 105
[1937] 55
[1922] 41
Name: count, dtype: int64
analysis.head(2).T
0 1
id 165157 163769
title Fulton and Nostrand Go Down Death
type Painting Painting
creationdate 1958 1934
ncreators 1 1
ncitations 30 18
birthyearsall [1917] [1899]
birthyear 1917 1899
这些步骤展示了我们如何使用类来处理非表格数据。
它是如何工作的……
本食谱展示了如何直接处理 JSON 文件,或任何包含隐式一对多或多对多关系的文件。我们在分析单元(本例中为一个集合项)中创建了一个类,然后创建了方法来汇总每个集合项的多个数据节点。
我们在步骤 3到步骤 6中创建的方法非常简洁。当我们第一次查看数据的结构时,它在本食谱的准备工作部分中展示,确实很难不觉得它会非常难以清理。看起来似乎什么都行。但事实证明,它有一个相对可靠的结构。我们可以依赖creators
和citations
中的一个或多个子节点。每个creators
和citations
节点也有子节点,这些子节点是键值对。这些键不总是存在,所以我们需要先检查它们是否存在,然后再尝试获取它们的值。这就是我们在步骤 3中所做的。
还有更多……
我在第二章《在处理 HTML、JSON 和 Spark 数据时预见数据清理问题》中详细讨论了直接处理 JSON 文件的优势。我认为博物馆的收藏数据是一个很好的例子,说明了为什么如果可能的话,我们可能更愿意坚持使用 JSON 格式。即使数据的形式非常不同,它的结构实际上是有意义的。当我们试图将其规范化时,始终存在一个风险,那就是我们可能会遗漏其结构的某些方面。
用于检查整体数据质量的函数
我们可以通过更明确地说明我们正在评估的内容来加强数据质量检查。我们在数据分析项目的初期,可能已经对变量值的分布、允许值的范围以及缺失值的数量有了一些预期。这些预期可能来自文档、我们对数据所代表的基础现实世界过程的理解,或者我们对统计学的理解。建立一个常规流程,用于明确这些初步假设、测试它们并在项目过程中修订假设是个不错的主意。本节将演示这个过程可能是什么样的。
我们为每个感兴趣的变量设定了数据质量目标。这包括类别变量的允许值和缺失值的阈值,也包括数值的取值范围、缺失值、偏度和峰度阈值,并检查异常值。我们将检查唯一标识符变量是否存在重复和缺失值。我们从这个 CSV 文件中的假设开始,关于 NLS 文件中的变量:
图 12.3:对选定 NLS 列的数据检查
图 12.3展示了我们的初步假设。例如,对于maritalstatus
,我们假设类别值为离婚|已婚|从未结婚|分居|寡妇,且不超过 20%的值将缺失。对于nightlyhrssleep
(一个数值变量),我们假设值将在 3 到 9 之间,不超过 30%的值将缺失,且其偏度和峰度接近正态分布。
我们还指明了我们想要检查异常值。最后一列是一个标志,如果我们只想对某些变量进行数据检查,可以使用它。在这里,我们指出要对maritalstatus
、originalid
、highestgradecompleted
、gpaenglish
和nightlyhrssleep
进行检查。
准备工作
我们将在本节中再次使用 NLS 数据。
如何操作…
我们使用预定义的数据检查目标来分析选定的 NLS 数据变量。
-
创建我们需要的函数进行数据检查,并将其保存在
helperfunctions
子文件夹中,命名为runchecks.py
。以下两个函数,checkcats
和checkoutliers
,将分别用于测试列表中的值和异常值。我们将在接下来的步骤中看到它是如何工作的:def checkcats(cat1,cat2): missingcats = \ set(cat1).symmetric_difference(set(cat2)) return missingcats def checkoutliers(values): thirdq, firstq = values.\ quantile(0.75),values.\ quantile(0.25) interquartilerange = 1.5*(thirdq-firstq) outlierhigh, outlierlow = \ interquartilerange+thirdq, \ firstq-interquartilerange return outlierhigh, outlierlow
-
然后我们定义一个函数来运行所有的检查,
runchecks
,它将接受一个 DataFrame(df
)、我们的数据目标(dc
)、一个数值列列表(numvars
)、一个类别列列表(catvars
)和一个标识符列列表(idvars
):def runchecks(df,dc,numvars,catvars,idvars):
-
在
runchecks
函数中,我们对数据检查中的分类变量列进行循环。我们通过dcvals = dc.loc[col]
获取该变量的所有目标值。我们从类别值中创建一个 NumPy 数组compcat
,然后将该数组与传入 DataFrame 中该列的所有值进行比较(df[col].dropna().str.strip().unique()
)。如果一个数组中有而另一个数组中没有某个类别(即valuediff
),我们会将其打印到控制台。我们还计算缺失值百分比。如果超出了我们指定的阈值,我们会打印一条消息:for col in df[catvars]: dcvals = dc.loc[col] print("\n\nChecks for categorical variable", col) compcat = list(dcvals.categories.split('|')) valuediff = checkcats(compcat,df[col].dropna().\ str.strip().unique()) if len(valuediff) > 0: print("at least one non-matching category:", valuediff) missingper = df[col].isnull().sum()/df.shape[0] if missingper > dcvals.missingthreshold: print("missing percent beyond threshold of", dcvals.missingthreshold, "is", missingper)
-
现在让我们看一下检查数值变量的循环。我们从数据检查目标中的范围值创建一个 NumPy 数组,
range = np.fromstring(dcvals.range, sep='|')
。range
的第一个元素是范围的下限,第二个元素是上限。然后,我们从 DataFrame 中获取变量的最小值和最大值,并将其与目标文件中指示的范围进行比较。
我们计算缺失值百分比,并在超过数据检查目标文件中设置的阈值时打印出来。
如果showoutliers
标志被设置为Y
,我们将显示离群值。我们使用之前设置的checkoutliers
函数,该函数通过简单的四分位距计算来确定离群值。最后,我们检查偏度和峰度,以便了解该变量与正态分布的差距:
for col in df[numvars]:
dcvals = dc.loc[col]
print("\n\nChecks for numeric variable", col)
range = np.fromstring(dcvals.range, sep='|')
min = df[col].min()
max = df[col].max()
if min < range[0]:
print("at least one record below range starting at ",
range[0], "min value is", min)
if max > range[1]:
print("at least one record above range ending at ",
range[1], "max value is", max)
missingper = df[col].isnull().sum()/df.shape[0]
if missingper > dcvals.missingthreshold:
print("missing percent beyond threshold of",
dcvals.missingthreshold, "is", missingper)
if dcvals.showoutliers == "Y":
outlierhigh, outlierlow = checkoutliers(df[col])
print("\nvalues less than", outlierlow, "\n",
df.loc[df[col]<outlierlow,col].\
agg(["min",'max','count']), end="\n")
print("\nvalues greater than", outlierhigh,
"\n", df.loc[df[col]>outlierhigh,col].\
agg(["min",'max','count']), end="\n")
skewcol = df[col].skew()
if abs(skewcol-dcvals.skewtarget)>1.2:
print("skew substantially different from target of",
dcvals.skewtarget, "is", skewcol)
kurtosiscol = df[col].kurtosis()
if abs(kurtosiscol-dcvals.kurtosistarget)>1.2:
print("kurtosis substantially different from target of",
dcvals.kurtosistarget, "is", kurtosiscol)
-
对于目标文件中标识为 id 变量的变量,我们进行一些简单的检查。我们检查该变量是否有重复值,并检查是否有缺失值:
for col in df[idvars]: print("\n\nChecks for id variable", col) uniquevals = df[col].nunique() nrows = df.shape[0] if uniquevals != nrows: print("not unique identifier", uniquevals, "unique values not equal to", nrows, "rows.") missingvals = df[col].isnull().sum() if missingvals > 0: print("unique value has", missingvals, "missing values")
-
现在我们准备开始运行数据检查。我们首先加载 NLS DataFrame 和数据检查目标。
import pandas as pd import numpy as np import os import sys nls97 = pd.read_csv("data/nls97g.csv", low_memory=False) dc = pd.read_csv("data/datacheckingtargets.csv") dc.set_index('varname', inplace=True)
-
我们导入刚刚创建的
runchecks
模块。sys.path.append(os.getcwd() + "/helperfunctions") import runchecks as rc
-
让我们故意破坏一些 id 变量值来测试代码。我们还修复了
highestgradecompleted
的逻辑缺失值,将其设置为实际缺失值。nls97.originalid.head(7)
0 1 1 2 2 3 3 4 4 5 5 6 6 7 Name: originalid, dtype: int64
nls97.loc[nls97.originalid==2,"originalid"] = 1 nls97.loc[nls97.originalid.between(3,7), "originalid"] = np.nan nls97.originalid.head(7)
0 1.0 1 1.0 2 NaN 3 NaN 4 NaN 5 NaN 6 NaN Name: originalid, dtype: float64
nls97["highestgradecompleted"] = nls97.highestgradecompleted.replace(95, np.nan)
-
我们只选择那些被标记为包含的目标。然后,我们根据数据检查目标文件创建分类变量、数值变量和 id 变量列表:
dc = dc.loc[dc.include=="Y"] numvars = dc.loc[dc.type=="numeric"].index.to_list() catvars = dc.loc[dc.type=="categorical"].index.to_list() idvars = dc.loc[dc.type=="unique"].index.to_list()
-
现在,我们准备开始运行检查。
rc.runchecks(nls97,dc,numvars,catvars,idvars)
这将产生以下输出:
图 12.4:运行检查
我们发现maritalstatus
的缺失值比我们设置的 20%的阈值(26%)多。highestgradecompleted
和gpaoverall
的值超出了预期范围。两个变量的峰度较低。nightlyhrssleep
有显著低于和高于四分位距的离群值。31 名受访者的nightlyhrssleep
为 2 小时或更少。27 名受访者的nightlyhrssleep
非常高,达到 12 小时或更多。
这些步骤展示了我们如何利用之前的领域知识和统计学理解,更好地针对数据质量问题进行调查。
它是如何工作的…
我们创建了一个包含数据检查目标的 CSV 文件,并在检查 NLS 数据时使用了该文件。我们通过将 NLS DataFrame 和数据检查目标一起传递给runchecks
来完成此操作。runchecks
中的代码遍历数据检查文件中的列名,并根据变量类型进行检查。
每个变量的目标由dcvals = dc.loc[col]
定义,它抓取目标文件中该行的所有目标值。然后我们可以引用dcvals.missingthreshold
来获取缺失值阈值,例如。接着,我们将缺失值的百分比(df[col].isnull().sum()/df.shape[0]
)与缺失阈值进行比较,如果缺失值百分比大于阈值,就打印一条消息。我们对值的范围、偏斜、异常值等进行相同类型的检查,具体取决于变量的类型。
我们可以在不更改runchecks
代码的情况下,向数据检查目标文件添加新变量。我们还可以更改目标值。
还有更多……
有时候,我们需要主动进行数据检查。展示一些样本统计数据和频率分布以大致了解数据,和动用我们领域知识和统计理解来仔细检查数据质量是不同的。一个更有目的的方法可能要求我们偶尔从 Python 开发环境中抽离片刻,反思我们对数据值及其分布的预期。设置初始的数据质量目标,并定期回顾这些目标,可以帮助我们做到这一点。
使用管道进行数据预处理:一个简单的示例
在进行预测分析时,我们通常需要将所有的预处理和特征工程折叠进管道中,包括缩放、编码以及处理异常值和缺失值。我们在第八章,编码、转换和缩放特征中讨论了为什么我们可能需要将所有数据准备工作纳入数据管道。那一章的主要观点是,当我们构建解释性模型并且需要避免数据泄漏时,管道至关重要。尤其是当我们使用k-折交叉验证进行模型验证时,这一问题更加复杂,因为在评估过程中,测试和训练的 DataFrame 会发生变化。交叉验证已成为构建预测模型的标准做法。
注意
k-折交叉验证通过对所有的k折(或部分)进行训练,留下一个折用于测试。这会重复k次,每次排除不同的折进行测试。性能度量是基于k折的平均得分。
管道的另一个好处是,它们有助于确保结果的可重复性,因为它们通常旨在将我们的分析从原始数据带到模型评估。
虽然这个配方演示了如何通过管道来进行模型评估,但我们不会在此详细讨论。关于模型评估和使用 scikit-learn 工具的管道,一个很好的资源是我写的书《数据清理与机器学习探索》。
我们从一个相对简单的例子开始,这里有一个包含两个数值特征和一个数值目标的模型。在下一个配方中,我们将处理一个更加复杂的例子。
准备工作
在这个配方中,我们将使用 scikit-learn 的管道工具,并结合其他一些模块来对数据进行编码、缩放,以及填补缺失值。我们将再次使用土地温度数据。
如何操作…
-
我们首先加载本配方中将要使用的
scikit-learn
模块,用于转换我们的数据。我们将使用StandardScaler
来标准化特征,使用SimpleImputer
填补缺失值,并使用make_pipeline
将所有预处理步骤组合在一起。我们还使用train_test_split
来创建训练和测试数据框。其他模块将在使用时进行讲解:import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression from sklearn.impute import SimpleImputer from sklearn.pipeline import make_pipeline from sklearn.model_selection import cross_validate from sklearn.model_selection import KFold
-
我们加载土地温度数据并创建训练和测试数据框。我们将尝试将温度建模为纬度和海拔的函数。考虑到
latabs
和elevation
变量的范围差异,数据缩放会变得非常重要:landtemps = pd.read_csv("data/landtemps2023avgs.csv") feature_cols = ['latabs','elevation'] X_train, X_test, y_train, y_test = \ train_test_split(landtemps[feature_cols],\ landtemps[['avgtemp']], test_size=0.1, random_state=0)
有关train_test_split
的介绍,请参见第八章,特征编码、转换和缩放。
-
我们设置了k-折交叉验证。我们指定希望有五个折叠,并且数据需要被打乱:
kf = KFold(n_splits=5, shuffle=True, random_state=0)
-
现在,我们准备好设置管道了。管道将进行标准缩放,当值缺失时填补均值,然后运行线性回归模型。两个特征将以相同的方式处理。
pipeline = \ make_pipeline(StandardScaler(), SimpleImputer(strategy="mean"),LinearRegression())
-
在构建好管道并实例化k-折交叉验证对象后,我们准备好执行预处理、估算模型并生成评估指标。我们将管道传递给
cross_validate
函数,并传入我们的训练数据。我们还传递了在步骤 3中创建的Kfold
对象。我们得到了一个相当不错的R平方值。scores = \ cross_validate(pipeline, X=X_train, y=y_train.values, cv=kf, scoring=['r2','neg_mean_absolute_error'], n_jobs=1) print("Mean Absolute Error: %.2f, R-squared: %.2f" % (scores['test_neg_mean_absolute_error'].mean(), scores['test_r2'].mean()))
Mean Absolute Error: -2.53, R-squared: 0.82
它是如何工作的…
我们使用了 scikit-learn 的make_pipeline
来创建一个仅包含三步的管道:应用标准缩放、基于该变量的均值填补缺失值,并拟合一个线性回归模型。管道的一个很有用的地方是它们会自动将一个步骤的转换结果传递给下一个步骤。一旦我们掌握了这项操作,尽管有k-折交叉验证的复杂性,这个过程仍然很简单。
我们可以想象一下,当训练和测试数据框不断变化时(比如k-折交叉验证中那样),如果我们自己编写代码来处理这些数据将会有多么复杂。即使是使用均值进行填补这样简单的操作也是棘手的。每次训练数据发生变化时,我们都需要重新计算训练数据的均值。我们的管道会自动处理这些问题。
那是一个相对简单的例子,只有几个我们可以用相同方式处理的特征。我们也没有检查离群值或对目标变量进行缩放。在下一个例子中,我们将使用管道处理一个更复杂的建模项目。
使用管道进行数据预处理:一个更复杂的例子
如果你曾经构建过数据管道,你就会知道,当你处理多个不同的数据类型时,事情可能会有些混乱。例如,我们可能需要对连续特征的缺失值进行中位数插补,对于类别特征则使用最频繁的值。我们可能还需要转换我们的目标变量。在这个例子中,我们将探讨如何对不同变量应用不同的预处理。
准备开始
在这个例子中,我们将使用相当多的 scikit-learn 模块。虽然一开始可能会有些混乱,但你会很快感激 scikit-learn 提供的几乎可以做任何你需要的工具。如果需要,scikit-learn 还允许我们将自定义的转换器添加到管道中。我将在这个例子中演示如何构建我们自己的转换器。
我们将使用来自 NLS 的工资和就业数据。
如何做…
-
我们首先加载在前一个例子中使用的库。然后我们添加
ColumnTransformer
和TransformedTargetRegressor
类。我们将使用这些类分别转换我们的特征和目标变量。import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression from sklearn.impute import SimpleImputer from sklearn.pipeline import make_pipeline from feature_engine.encoding import OneHotEncoder from sklearn.impute import KNNImputer from sklearn.model_selection import cross_validate, KFold from sklearn.compose import ColumnTransformer from sklearn.compose import TransformedTargetRegressor
-
列转换器非常灵活。我们甚至可以使用它与我们自己定义的预处理函数。下面的代码块从
helperfunctions
子文件夹中的preprocfunc
模块导入OutlierTrans
类。import os import sys sys.path.append(os.getcwd() + "/helperfunctions") from preprocfunc import OutlierTrans
-
OutlierTrans
类通过与四分位距的距离来识别缺失值。这是一种我们在第四章,“识别数据子集中的离群值”中演示过的技术,并且在本章中多次使用。
为了在 scikit-learn 管道中使用,我们的类必须具有 fit
和 transform
方法。我们还需要继承 BaseEstimator
和 TransformerMixin
类。
在这个类中,几乎所有的操作都发生在 transform
方法中。任何高于第三四分位数或低于第一四分位数超过 1.5 倍四分位距的值都会被标记为缺失,尽管这个阈值可以更改:
class OutlierTrans(BaseEstimator,TransformerMixin):
def __init__(self,threshold=1.5):
self.threshold = threshold
def fit(self,X,y=None):
return self
def transform(self,X,y=None):
Xnew = X.copy()
for col in Xnew.columns:
thirdq, firstq = Xnew[col].quantile(0.75),\
Xnew[col].quantile(0.25)
inlierrange = self.threshold*(thirdq-firstq)
outlierhigh, outlierlow = inlierrange+thirdq,\
firstq-inlierrange
Xnew.loc[(Xnew[col]>outlierhigh) | \
(Xnew[col]<outlierlow),col] = np.nan
return Xnew.values
我们的 OutlierTrans
类可以像使用 StandardScaler
和其他转换器一样,在我们的管道中使用。我们稍后会这样做。
- 现在我们准备加载需要处理的数据。我们将使用 NLS 工资数据。工资收入将作为我们的目标变量,而我们将使用高中 GPA、母亲和父亲的最高学历、父母收入、性别、工作周数以及个人是否完成本科学位作为特征。
我们在这里创建了处理不同方式的特征列表。稍后在我们指示管道对数值型、类别型和二进制特征进行不同操作时,这将非常有用。
nls97wages = pd.read_csv("data/nls97wages.csv" , low_memory=False)
nls97wages.set_index("personid", inplace=True)
num_cols = ['gpascience','gpaenglish','gpamath',
'gpaoverall','motherhighgrade','fatherhighgrade',
'parentincome','weeksworked20']
cat_cols = ['gender']
bin_cols = ['completedba']
target = nls97wages[['wageincome20']]
features = nls97wages[num_cols + cat_cols + bin_cols]
X_train, X_test, y_train, y_test = \
train_test_split(features,\
target, test_size=0.2, random_state=0)
- 现在我们可以设置列转换器。我们首先创建用于处理数值数据(
standtrans
)、分类数据和二元数据的管道。
对于数值数据(连续型数据),我们希望将异常值设为缺失值。我们将值 2 传递给OutlierTrans
的threshold
参数,表示我们希望将超出四分位距 2 倍范围以上或以下的值设置为缺失值。请记住,通常使用 1.5,所以我们这里比较保守。
我们对gender
列进行了独热编码,本质上创建了一个虚拟变量。我们丢弃了最后一个类别,以避免虚拟变量陷阱,正如第八章《编码、转换和缩放特征》中讨论的那样。
然后,我们创建一个ColumnTransformer
对象,将刚刚创建的三个管道传递给它,并指明每个管道应该应用于哪些特征。
对于数值变量,我们暂时不担心缺失值。我们稍后会处理它们:
standtrans = make_pipeline(OutlierTrans(2),
StandardScaler())
cattrans = \
make_pipeline(SimpleImputer(strategy=\
"most_frequent"),OneHotEncoder(drop_last=True))
bintrans = \
make_pipeline(SimpleImputer(strategy=\
"most_frequent"))
coltrans = ColumnTransformer(
transformers=[
("stand", standtrans, num_cols),
("cat", cattrans, ['gender']),
("bin", bintrans, ['completedba'])
]
)
- 现在,我们可以将列转换器添加到一个包含我们想要运行的线性模型的管道中。我们将 KNN 插补添加到管道中,以处理数值数据的缺失值。对于分类变量的缺失值,我们已经处理过了。
我们还需要对目标进行缩放,这不能在我们的管道中完成。我们使用 scikit-learn 的TransformedTargetRegressor
来完成这项工作。我们将刚才创建的管道传递给目标回归器的regressor
参数。
lr = LinearRegression()
pipe1 = make_pipeline(coltrans,
KNNImputer(n_neighbors=5), lr)
ttr=TransformedTargetRegressor(regressor=pipe1,
transformer=StandardScaler())
-
让我们使用这个管道进行k-折交叉验证。我们可以通过目标回归器
ttr
将我们的管道传递给cross_validate
函数。kf = KFold(n_splits=10, shuffle=True, random_state=0) scores = cross_validate(ttr, X=X_train, y=y_train, cv=kf, scoring=('r2', 'neg_mean_absolute_error'), n_jobs=1) print("Mean Absolute Error: %.2f, R-squared: %.2f" % (scores['test_neg_mean_absolute_error'].mean(), scores['test_r2'].mean()))
Mean Absolute Error: -32899.64, R-squared: 0.16
这些得分不是很好,尽管这并不是这次练习的重点。这里的关键是,我们通常希望将大部分预处理工作纳入管道中进行处理。这是避免数据泄露的最佳方法。列转换器是一个极其灵活的工具,允许我们对不同的特征应用不同的转换。
它是如何工作的…
我们创建了几个不同的管道来预处理数据,在拟合模型之前,一个用于数值数据,一个用于分类数据,一个用于二元数据。列转换器通过允许我们将不同的管道应用于不同的列来帮助我们。我们在步骤 5中设置了列转换器。
我们在步骤 6中创建了另一个管道。这个管道实际上从列转换器开始。然后,列转换器预处理后的数据集将传递给 KNN 插补器,以处理数值列中的缺失值,接着传递给线性回归模型。
值得注意的是,我们可以向 scikit-learn 管道中添加转换操作,即使是我们自己设计的转换操作,因为它们继承了BaseEstimator
和TransformerMixin
类,正如我们在步骤 3中看到的那样。
还有更多…
有一件关于管道的事非常酷而且实用,而在这个示例中并未展示。如果你曾经需要根据经过缩放或转化的变量生成预测,你可能会记得那是多么麻烦。好消息是,管道帮我们处理了这个问题,生成了适当单位的预测结果。
另见
这只是管道技术的一小部分内容。如果你想深入了解,请参阅我写的书《使用机器学习进行数据清理和探索》。
摘要
本章包含了不少内容,介绍了几种自动化数据清理的方式。我们创建了用于显示数据结构和生成描述性统计的函数。我们还创建了用于重构和聚合数据的函数。此外,我们还开发了用于处理大量变量的数据清理的 Python 类,这些变量需要非常不同的处理方式。我们还展示了如何利用 Python 类简化直接操作 JSON 文件的过程。我们还探讨了通过将数据与预定义目标进行对比,使数据清理更有针对性。最后,我们探讨了如何通过管道自动化数据清理。
留下评论!
喜欢这本书吗?帮助像你一样的读者,通过在亚马逊上留下评论。扫描下方二维码,获得你选择的免费电子书。
https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/py-dt-cln-cb-2e/img/Review_copy.png
订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助你规划个人发展并推动职业生涯发展。欲了解更多信息,请访问我们的网站。
为什么要订阅?
-
用来自 4,000 多位行业专家的实用电子书和视频,减少学习时间,增加编程时间
-
用专门为你打造的技能规划提升你的学习
-
每月获得一本免费电子书或视频
-
完全可搜索,便于获取关键信息
-
复制、粘贴、打印和书签内容
在www.packt.com,你还可以阅读一系列免费的技术文章,注册各种免费电子报,并获得 Packt 书籍和电子书的独家折扣和优惠。
你可能喜欢的其他书籍
如果你喜欢这本书,你可能会对 Packt 出版的以下其他书籍感兴趣:
https://www.packtpub.com/product/azure-data-factory-cookbook-second-edition/9781803246598
Azure 数据工厂实用指南(第二版)
Dmitry Foshin
Dimtry Anoshin
Tonya Chernyshova
Xenia Ireton
ISBN: 978-1-80324-659-8
-
在 ADF 中创建编排和转换作业
-
使用 Azure Synapse 开发、执行和监控数据流
-
使用 Databricks 和 Delta 表创建大数据管道
-
使用 Spark 池在 Azure Data Lake 中处理大数据
-
将本地 SSIS 作业迁移到 ADF
-
将 ADF 与常用的 Azure 服务(如 Azure ML、Azure Logic Apps 和 Azure Functions)集成
-
在 HDInsight 和 Azure Databricks 中运行大数据计算任务
-
使用 ADF 的内置连接器,将数据从 AWS S3 和 Google Cloud Storage 复制到 Azure Storage
https://www.packtpub.com/product/data-engineering-with-aws-second-edition/9781804614426
AWS 数据工程 - 第二版
Gareth Eagar
ISBN: 978-1-80461-442-6
-
无缝地摄取流数据,使用 Amazon Kinesis Data Firehose
-
使用 AWS Glue Studio 优化、反规范化并连接数据集
-
使用 Amazon S3 事件触发 Lambda 进程来转换文件
-
将数据加载到 Redshift 数据仓库中,并轻松运行查询
-
使用 Amazon QuickSight 可视化和探索数据
-
使用 Amazon Comprehend 从数据集中提取情感数据
-
使用 Apache Iceberg 与 Amazon Athena 构建事务性数据湖
-
了解如何在 AWS 上实现数据网格方法
Packt 正在寻找像你这样的作者
如果你有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天就申请。我们与成千上万的开发者和技术专业人士合作,帮助他们与全球技术社区分享他们的见解。你可以提交一般申请,申请我们正在招聘作者的特定热门话题,或提交你自己的想法。
分享你的想法
现在你已经完成了Python 数据清理食谱,第二版,我们很想听听你的想法!如果你是在亚马逊购买的这本书,请点击这里直接前往亚马逊书评页面,分享你的反馈或在购买网站上留下评论。
你的评论对我们和技术社区非常重要,将帮助我们确保提供卓越的内容质量。