Pandas 合并数据集:merge 和 join
文章目录
- Pandas 合并数据集:merge 和 join
- 关系代数
- 连接的类别
- 一对一连接
- 多对一连接
- 多对多连接
- 指定合并键
- on 关键字
- left_on 和 right_on 关键字
- left_index 和 right_index 关键字
- 为连接指定集合运算方式
- 列名重叠:suffixes 关键字
- 示例:美国各州数据
Pandas 合并数据集:merge 和 join
Pandas 提供了一个重要的功能,即高性能的内存中连接(join)和合并(merge)操作,如果你曾经使用过数据库,可能已经很熟悉这些操作。
主要的接口是 pd.merge
函数,接下来我们将通过一些示例来了解它的实际用法。
为了方便起见,我们会在常规导入之后再次定义上一章中的 display
函数:
import pandas as pd
import numpy as npclass display(object):"""Display HTML representation of multiple objects"""template = """<div style="float: left; padding: 10px;"><p style='font-family:"Courier New", Courier, monospace'>{0}</p>{1}</div>"""def __init__(self, *args):self.args = argsdef _repr_html_(self):return '\n'.join(self.template.format(a, eval(a)._repr_html_())for a in self.args)def __repr__(self):return '\n\n'.join(a + '\n' + repr(eval(a))for a in self.args)
关系代数
pd.merge
实现的行为是关系代数的一部分。关系代数是一套用于操作关系型数据的正式规则,是大多数数据库操作的概念基础。
关系代数方法的优势在于它提出了若干基本操作,这些操作成为对任何数据集进行更复杂操作的构建模块。
只要在数据库或其他程序中高效实现了这些基本操作,就可以执行各种相当复杂的复合操作。
Pandas 在 pd.merge
函数以及 Series
和 DataFrame
对象的相关 join
方法中实现了这些基本构建模块中的几个。
正如你将看到的,这些方法可以高效地将来自不同来源的数据关联起来。
连接的类别
pd.merge
函数实现了多种类型的连接:一对一、多对一 和 多对多。
这三种连接类型都可以通过相同的 pd.merge
接口调用实现;实际执行哪种连接,取决于输入数据的形式。
我们将从这三种合并类型的简单示例开始,稍后再讨论更详细的选项。
一对一连接
也许最简单的合并类型就是一对一连接,这在很多方面类似于你在合并数据集:concat 和 append中看到的按列拼接。
举个具体的例子,考虑下面两个 DataFrame
对象,它们包含了公司中几位员工的信息:
df1 = pd.DataFrame({'employee': ['Bob', 'Jake', 'Lisa', 'Sue'],'group': ['Accounting', 'Engineering','Engineering', 'HR']})
df2 = pd.DataFrame({'employee': ['Lisa', 'Bob', 'Jake', 'Sue'],'hire_date': [2004, 2008, 2012, 2014]})
display('df1', 'df2')
df1
employee | group | |
---|---|---|
0 | Bob | Accounting |
1 | Jake | Engineering |
2 | Lisa | Engineering |
3 | Sue | HR |
df2
employee | hire_date | |
---|---|---|
0 | Lisa | 2004 |
1 | Bob | 2008 |
2 | Jake | 2012 |
3 | Sue | 2014 |
要将这些信息合并到一个 DataFrame
中,我们可以使用 pd.merge
函数:
df3 = pd.merge(df1, df2)
df3
employee | group | hire_date | |
---|---|---|---|
0 | Bob | Accounting | 2008 |
1 | Jake | Engineering | 2012 |
2 | Lisa | Engineering | 2004 |
3 | Sue | HR | 2014 |
pd.merge
函数会自动识别每个 DataFrame
中的 employee
列,并以此作为键进行连接。
合并的结果是一个新的 DataFrame
,它将两个输入的数据整合在一起。
请注意,每一列中的条目顺序不一定会被保留:在本例中,employee
列在 df1
和 df2
中的顺序不同,而 pd.merge
函数能够正确处理这一点。
另外需要注意的是,合并操作通常会丢弃索引,除非使用按索引合并(稍后会讨论 left_index
和 right_index
关键字)。
多对一连接
多对一连接是指两个键列中的一个包含重复项的连接。
对于多对一连接,结果 DataFrame
会适当地保留这些重复项。
请看下面关于多对一连接的示例:
df4 = pd.DataFrame({'group': ['Accounting', 'Engineering', 'HR'],'supervisor': ['Carly', 'Guido', 'Steve']})
display('df3', 'df4', 'pd.merge(df3, df4)')
df3
employee | group | hire_date | |
---|---|---|---|
0 | Bob | Accounting | 2008 |
1 | Jake | Engineering | 2012 |
2 | Lisa | Engineering | 2004 |
3 | Sue | HR | 2014 |
df4
group | supervisor | |
---|---|---|
0 | Accounting | Carly |
1 | Engineering | Guido |
2 | HR | Steve |
pd.merge(df3, df4)
employee | group | hire_date | supervisor | |
---|---|---|---|---|
0 | Bob | Accounting | 2008 | Carly |
1 | Jake | Engineering | 2012 | Guido |
2 | Lisa | Engineering | 2004 | Guido |
3 | Sue | HR | 2014 | Steve |
结果的 DataFrame
会多出一列 “supervisor”(主管)信息,其中的信息会根据输入数据的需要在一个或多个位置重复出现。
多对多连接
多对多连接在概念上可能有些令人困惑,但它们依然有明确的定义。
如果左右数组中的键列都包含重复项,那么结果就是多对多合并。
通过具体的例子可以更清楚地理解这一点。
请看下面的例子,我们有一个 DataFrame
,显示了每个小组关联的一项或多项技能。
通过执行多对多连接,我们可以获得与每个个人相关的技能信息:
df5 = pd.DataFrame({'group': ['Accounting', 'Accounting','Engineering', 'Engineering', 'HR', 'HR'],'skills': ['math', 'spreadsheets', 'software', 'math','spreadsheets', 'organization']})
display('df1', 'df5', "pd.merge(df1, df5)")
df1
employee | group | |
---|---|---|
0 | Bob | Accounting |
1 | Jake | Engineering |
2 | Lisa | Engineering |
3 | Sue | HR |
df5
group | skills | |
---|---|---|
0 | Accounting | math |
1 | Accounting | spreadsheets |
2 | Engineering | software |
3 | Engineering | math |
4 | HR | spreadsheets |
5 | HR | organization |
pd.merge(df1, df5)
employee | group | skills | |
---|---|---|---|
0 | Bob | Accounting | math |
1 | Bob | Accounting | spreadsheets |
2 | Jake | Engineering | software |
3 | Jake | Engineering | math |
4 | Lisa | Engineering | software |
5 | Lisa | Engineering | math |
6 | Sue | HR | spreadsheets |
7 | Sue | HR | organization |
这三种连接类型可以与其他 Pandas 工具结合使用,实现各种功能。
但在实际应用中,数据集很少像我们这里演示的那样干净整齐。
在接下来的部分,我们将介绍 pd.merge
提供的一些选项,这些选项可以帮助你调整连接操作的具体方式。
指定合并键
我们已经看到了 pd.merge
的默认行为:它会在两个输入数据中查找一个或多个匹配的列名,并将其作为连接键。
然而,很多时候列名并不会如此完美地匹配,pd.merge
提供了多种选项来处理这种情况。
on 关键字
最简单的方式是使用 on
关键字显式指定连接键的列名。on
可以接受一个列名或列名列表:
display('df1', 'df2', "pd.merge(df1, df2, on='employee')")
df1
employee | group | |
---|---|---|
0 | Bob | Accounting |
1 | Jake | Engineering |
2 | Lisa | Engineering |
3 | Sue | HR |
df2
employee | hire_date | |
---|---|---|
0 | Lisa | 2004 |
1 | Bob | 2008 |
2 | Jake | 2012 |
3 | Sue | 2014 |
pd.merge(df1, df2, on='employee')
employee | group | hire_date | |
---|---|---|---|
0 | Bob | Accounting | 2008 |
1 | Jake | Engineering | 2012 |
2 | Lisa | Engineering | 2004 |
3 | Sue | HR | 2014 |
此选项仅在左右两个 DataFrame
都包含指定的列名时有效。
left_on 和 right_on 关键字
有时你可能希望合并两个具有不同列名的数据集;例如,我们可能有一个数据集,其中员工姓名的列名是 “name” 而不是 “employee”。
在这种情况下,我们可以使用 left_on
和 right_on
关键字来分别指定两个列名:
df3 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'],'salary': [70000, 80000, 120000, 90000]})
display('df1', 'df3', 'pd.merge(df1, df3, left_on="employee", right_on="name")')
df1
employee | group | |
---|---|---|
0 | Bob | Accounting |
1 | Jake | Engineering |
2 | Lisa | Engineering |
3 | Sue | HR |
df3
name | salary | |
---|---|---|
0 | Bob | 70000 |
1 | Jake | 80000 |
2 | Lisa | 120000 |
3 | Sue | 90000 |
pd.merge(df1, df3, left_on="employee", right_on="name")
employee | group | name | salary | |
---|---|---|---|---|
0 | Bob | Accounting | Bob | 70000 |
1 | Jake | Engineering | Jake | 80000 |
2 | Lisa | Engineering | Lisa | 120000 |
3 | Sue | HR | Sue | 90000 |
结果中有一个多余的列,如果需要,可以通过 DataFrame.drop()
方法将其删除:
pd.merge(df1, df3, left_on="employee", right_on="name").drop('name', axis=1)
employee | group | salary | |
---|---|---|---|
0 | Bob | Accounting | 70000 |
1 | Jake | Engineering | 80000 |
2 | Lisa | Engineering | 120000 |
3 | Sue | HR | 90000 |
left_index 和 right_index 关键字
有时候,你可能希望根据索引而不是某一列来进行合并。
例如,你的数据可能如下所示:
df1a = df1.set_index('employee')
df2a = df2.set_index('employee')
display('df1a', 'df2a')
df1a
group | |
---|---|
employee | |
Bob | Accounting |
Jake | Engineering |
Lisa | Engineering |
Sue | HR |
df2a
hire_date | |
---|---|
employee | |
Lisa | 2004 |
Bob | 2008 |
Jake | 2012 |
Sue | 2014 |
你可以通过在 pd.merge()
中指定 left_index
和/或 right_index
参数,将索引作为合并的键来使用:
display('df1a', 'df2a',"pd.merge(df1a, df2a, left_index=True, right_index=True)")
df1a
group | |
---|---|
employee | |
Bob | Accounting |
Jake | Engineering |
Lisa | Engineering |
Sue | HR |
df2a
hire_date | |
---|---|
employee | |
Lisa | 2004 |
Bob | 2008 |
Jake | 2012 |
Sue | 2014 |
pd.merge(df1a, df2a, left_index=True, right_index=True)
group | hire_date | |
---|---|---|
employee | ||
Bob | Accounting | 2008 |
Jake | Engineering | 2012 |
Lisa | Engineering | 2004 |
Sue | HR | 2014 |
为方便起见,Pandas 提供了 DataFrame.join()
方法,它可以在不需要额外关键字的情况下基于索引进行合并:
df1a.join(df2a)
group | hire_date | |
---|---|---|
employee | ||
Bob | Accounting | 2008 |
Jake | Engineering | 2012 |
Lisa | Engineering | 2004 |
Sue | HR | 2014 |
如果你希望混合使用索引和列进行合并,可以结合使用 left_index
与 right_on
,或 left_on
与 right_index
,以实现所需的行为:
display('df1a', 'df3', "pd.merge(df1a, df3, left_index=True, right_on='name')")
df1a
group | |
---|---|
employee | |
Bob | Accounting |
Jake | Engineering |
Lisa | Engineering |
Sue | HR |
df3
name | salary | |
---|---|---|
0 | Bob | 70000 |
1 | Jake | 80000 |
2 | Lisa | 120000 |
3 | Sue | 90000 |
pd.merge(df1a, df3, left_index=True, right_on='name')
group | name | salary | |
---|---|---|---|
0 | Accounting | Bob | 70000 |
1 | Engineering | Jake | 80000 |
2 | Engineering | Lisa | 120000 |
3 | HR | Sue | 90000 |
所有这些选项同样适用于多个索引和/或多列;其接口设计非常直观易用。
更多相关内容,请参阅 Pandas 文档中的“合并、连接与拼接”部分。
为连接指定集合运算方式
在之前的所有示例中,我们都忽略了执行连接时一个重要的考虑因素:连接中所使用的集合运算类型。
当某个值出现在一个键列中但未出现在另一个键列时,这个问题就会出现。请看下面的例子:
df6 = pd.DataFrame({'name': ['Peter', 'Paul', 'Mary'],'food': ['fish', 'beans', 'bread']},columns=['name', 'food'])
df7 = pd.DataFrame({'name': ['Mary', 'Joseph'],'drink': ['wine', 'beer']},columns=['name', 'drink'])
display('df6', 'df7', 'pd.merge(df6, df7)')
df6
name | food | |
---|---|---|
0 | Peter | fish |
1 | Paul | beans |
2 | Mary | bread |
df7
name | drink | |
---|---|---|
0 | Mary | wine |
1 | Joseph | beer |
pd.merge(df6, df7)
name | food | drink | |
---|---|---|---|
0 | Mary | bread | wine |
这里我们合并了两个数据集,它们只有一个共同的 “name” 条目:Mary。
默认情况下,结果只包含两个输入集合的交集;这就是所谓的内连接(inner join)。
我们可以通过 how
关键字显式指定这一点,how
的默认值为 "inner"
:
pd.merge(df6, df7, how='inner')
name | food | drink | |
---|---|---|---|
0 | Mary | bread | wine |
how
关键字的其他选项包括 'outer'
、'left'
和 'right'
。
外连接(outer join)会返回输入列的并集,并用 NA 填充所有缺失值:
display('df6', 'df7', "pd.merge(df6, df7, how='outer')")
df6
name | food | |
---|---|---|
0 | Peter | fish |
1 | Paul | beans |
2 | Mary | bread |
df7
name | drink | |
---|---|---|
0 | Mary | wine |
1 | Joseph | beer |
pd.merge(df6, df7, how='outer')
name | food | drink | |
---|---|---|---|
0 | Joseph | NaN | beer |
1 | Mary | bread | wine |
2 | Paul | beans | NaN |
3 | Peter | fish | NaN |
左连接(left join)和右连接(right join)分别返回以左侧条目或右侧条目为基础的连接结果。
例如:
display('df6', 'df7', "pd.merge(df6, df7, how='left')")
df6
name | food | |
---|---|---|
0 | Peter | fish |
1 | Paul | beans |
2 | Mary | bread |
df7
name | drink | |
---|---|---|
0 | Mary | wine |
1 | Joseph | beer |
pd.merge(df6, df7, how='left')
name | food | drink | |
---|---|---|---|
0 | Peter | fish | NaN |
1 | Paul | beans | NaN |
2 | Mary | bread | wine |
输出的行现在对应于左侧输入中的条目。使用 how='right'
的方式也类似。
所有这些选项都可以直接应用于前面介绍的各种连接类型。
列名重叠:suffixes 关键字
最后,你可能会遇到两个输入的 DataFrame
存在列名冲突的情况。
请看下面的例子:
df8 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'],'rank': [1, 2, 3, 4]})
df9 = pd.DataFrame({'name': ['Bob', 'Jake', 'Lisa', 'Sue'],'rank': [3, 1, 4, 2]})
display('df8', 'df9', 'pd.merge(df8, df9, on="name")')
df8
name | rank | |
---|---|---|
0 | Bob | 1 |
1 | Jake | 2 |
2 | Lisa | 3 |
3 | Sue | 4 |
df9
name | rank | |
---|---|---|
0 | Bob | 3 |
1 | Jake | 1 |
2 | Lisa | 4 |
3 | Sue | 2 |
pd.merge(df8, df9, on="name")
name | rank_x | rank_y | |
---|---|---|---|
0 | Bob | 1 | 3 |
1 | Jake | 2 | 1 |
2 | Lisa | 3 | 4 |
3 | Sue | 4 | 2 |
由于输出结果中会有两个冲突的列名,merge
函数会自动在输出列名后添加 _x
和 _y
后缀,以确保列名唯一。
如果默认的后缀不合适,可以通过 suffixes
关键字自定义后缀:
pd.merge(df8, df9, on="name", suffixes=["_L", "_R"])
name | rank_L | rank_R | |
---|---|---|---|
0 | Bob | 1 | 3 |
1 | Jake | 2 | 1 |
2 | Lisa | 3 | 4 |
3 | Sue | 4 | 2 |
这些后缀适用于所有可能的连接方式,并且在存在多个重叠列时同样有效。
我们将在其中更深入地探讨关系代数。
另请参阅 Pandas 文档中的“合并、连接、拼接与比较”部分,以获取这些主题的进一步讨论。
示例:美国各州数据
合并和连接操作最常见于将来自不同来源的数据组合在一起。
这里我们将以美国各州及其人口数据为例进行说明。
相关数据文件可在 http://github.com/jakevdp/data-USstates 找到:
# Following are commands to download the data
# repo = "https://raw.githubusercontent.com/jakevdp/data-USstates/master"
# !cd data && curl -O {repo}/state-population.csv
# !cd data && curl -O {repo}/state-areas.csv
# !cd data && curl -O {repo}/state-abbrevs.csv
让我们使用 Pandas 的 read_csv
函数来查看这三个数据集:
pop = pd.read_csv('data/state-population.csv')
areas = pd.read_csv('data/state-areas.csv')
abbrevs = pd.read_csv('data/state-abbrevs.csv')display('pop.head()', 'areas.head()', 'abbrevs.head()')
pop.head()
state/region | ages | year | population | |
---|---|---|---|---|
0 | AL | under18 | 2012 | 1117489.0 |
1 | AL | total | 2012 | 4817528.0 |
2 | AL | under18 | 2010 | 1130966.0 |
3 | AL | total | 2010 | 4785570.0 |
4 | AL | under18 | 2011 | 1125763.0 |
areas.head()
state | area (sq. mi) | |
---|---|---|
0 | Alabama | 52423 |
1 | Alaska | 656425 |
2 | Arizona | 114006 |
3 | Arkansas | 53182 |
4 | California | 163707 |
abbrevs.head()
state | abbreviation | |
---|---|---|
0 | Alabama | AL |
1 | Alaska | AK |
2 | Arizona | AZ |
3 | Arkansas | AR |
4 | California | CA |
根据上述信息,假设我们想要计算一个相对简单的结果:按 2010 年人口密度对美国各州和领地进行排名。
我们已经拥有了实现这一目标所需的数据,但需要将这些数据集进行合并。
我们首先进行一次多对一合并,以便在人口 DataFrame
中获得完整的州名。
我们希望基于 pop
的 state/region
列和 abbrevs
的 abbreviation
列进行合并。
我们将使用 how='outer'
,以确保不会因为标签不匹配而丢弃任何数据:
merged = pd.merge(pop, abbrevs, how='outer',left_on='state/region', right_on='abbreviation')
merged = merged.drop('abbreviation', axis=1) # drop duplicate info
merged.head()
state/region | ages | year | population | state | |
---|---|---|---|---|---|
0 | AK | total | 1990 | 553290.0 | Alaska |
1 | AK | under18 | 1990 | 177502.0 | Alaska |
2 | AK | total | 1992 | 588736.0 | Alaska |
3 | AK | under18 | 1991 | 182180.0 | Alaska |
4 | AK | under18 | 1992 | 184878.0 | Alaska |
让我们仔细检查一下是否存在不匹配的情况,可以通过查找包含空值的行来实现:
merged.isnull().any()
state/region False
ages False
year False
population True
state True
dtype: bool
有些 population
(人口)值是空的;让我们找出这些值对应的是哪些数据!
merged[merged['population'].isnull()].head()
state/region | ages | year | population | state | |
---|---|---|---|---|---|
1872 | PR | under18 | 1990 | NaN | NaN |
1873 | PR | total | 1990 | NaN | NaN |
1874 | PR | total | 1991 | NaN | NaN |
1875 | PR | under18 | 1991 | NaN | NaN |
1876 | PR | total | 1993 | NaN | NaN |
所有人口为 null 的值似乎都来自 2000 年之前的波多黎各;这很可能是因为原始数据源中没有这些数据。
更重要的是,我们发现有些新的 state
条目也是 null,这意味着在 abbrevs
键中没有对应的条目!
让我们找出哪些地区缺少这种匹配:
merged.loc[merged['state'].isnull(), 'state/region'].unique()
array(['PR', 'USA'], dtype=object)
我们可以很快推断出问题所在:我们的人口数据中包含了波多黎各(PR)和美国整体(USA)的条目,而这些条目在州缩写键中并未出现。
我们可以通过补充合适的条目来快速修复这个问题:
merged.loc[merged['state/region'] == 'PR', 'state'] = 'Puerto Rico'
merged.loc[merged['state/region'] == 'USA', 'state'] = 'United States'
merged.isnull().any()
state/region False
ages False
year False
population True
state False
dtype: bool
state
列中已无空值:一切准备就绪!
现在我们可以用类似的方法将结果与面积数据进行合并。
检查我们的结果后,我们会希望在两个数据集中都以 state
列作为连接键:
final = pd.merge(merged, areas, on='state', how='left')
final.head()
state/region | ages | year | population | state | area (sq. mi) | |
---|---|---|---|---|---|---|
0 | AK | total | 1990 | 553290.0 | Alaska | 656425.0 |
1 | AK | under18 | 1990 | 177502.0 | Alaska | 656425.0 |
2 | AK | total | 1992 | 588736.0 | Alaska | 656425.0 |
3 | AK | under18 | 1991 | 182180.0 | Alaska | 656425.0 |
4 | AK | under18 | 1992 | 184878.0 | Alaska | 656425.0 |
再次检查是否存在空值,以确定是否有不匹配的情况:
在 area
列中存在空值;我们可以查看一下哪些地区在这里被忽略了:
final['state'][final['area (sq. mi)'].isnull()].unique()
array(['United States'], dtype=object)
我们发现 areas
这个 DataFrame
并没有包含美国整体的面积数据。
我们可以插入一个合适的值(例如所有州面积的总和),但在这里我们只需删除这些空值,因为整个美国的人口密度并不是我们当前讨论的重点:
final.dropna(inplace=True)
final.head()
state/region | ages | year | population | state | area (sq. mi) | |
---|---|---|---|---|---|---|
0 | AK | total | 1990 | 553290.0 | Alaska | 656425.0 |
1 | AK | under18 | 1990 | 177502.0 | Alaska | 656425.0 |
2 | AK | total | 1992 | 588736.0 | Alaska | 656425.0 |
3 | AK | under18 | 1991 | 182180.0 | Alaska | 656425.0 |
4 | AK | under18 | 1992 | 184878.0 | Alaska | 656425.0 |
现在我们已经拥有了所需的全部数据。为了回答我们关心的问题,首先需要选取年份为 2010 且人口类型为总人口的数据部分。
我们将使用 query
函数来快速完成这一操作(这需要安装 NumExpr 包:
data2010 = final.query("year == 2010 & ages == 'total'")
data2010.head()
state/region | ages | year | population | state | area (sq. mi) | |
---|---|---|---|---|---|---|
43 | AK | total | 2010 | 713868.0 | Alaska | 656425.0 |
51 | AL | total | 2010 | 4785570.0 | Alabama | 52423.0 |
141 | AR | total | 2010 | 2922280.0 | Arkansas | 53182.0 |
149 | AZ | total | 2010 | 6408790.0 | Arizona | 114006.0 |
197 | CA | total | 2010 | 37333601.0 | California | 163707.0 |
现在让我们计算人口密度并按顺序显示结果。
我们将首先以州为索引重新排列数据,然后计算结果:
data2010.set_index('state', inplace=True)
density = data2010['population'] / data2010['area (sq. mi)']
density.sort_values(ascending=False, inplace=True)
density.head()
state
District of Columbia 8898.897059
Puerto Rico 1058.665149
New Jersey 1009.253268
Rhode Island 681.339159
Connecticut 645.600649
dtype: float64
结果是美国各州(包括华盛顿特区和波多黎各)按 2010 年人口密度(每平方英里居民数)排序的排名。
可以看到,在这个数据集中,人口密度最高的地区是华盛顿特区(即哥伦比亚特区);在各州中,人口密度最高的是新泽西州。
我们还可以查看排名末尾的地区:
density.tail()
state
South Dakota 10.583512
North Dakota 9.537565
Montana 6.736171
Wyoming 5.768079
Alaska 1.087509
dtype: float64
我们可以看到,人口密度最低的州远远是阿拉斯加,平均每平方英里仅有一名居民。
这种数据合并操作在使用真实世界数据源回答问题时非常常见。
希望这个例子能让你了解如何结合我们介绍过的工具,从数据中获得洞见!