Pandas很出众的一个特性就是高性能的,内存内的联合和合并操作。如果你曾经使用过数据库,你将会对这种类型的数据交互很熟悉。主要接口是pd.merge函数,将会看到几个实际工作的例子:
为了方便,我们由重新定义前面章节的display()函数开始:
import pandas as pd
import numpy as np
class 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 = args
def _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在Series和DataFrame的d.merge()和join()方法中实现一些这样的操作基石。如你将会看到,它们让你高效的连接不同数据源的数据。
联合的种类
pd.merge()函数实现了联合类别:一对一,多对一,多对多。所有三种类型的联合都可以通过统一调用pd.merge()接口来访问;联合类型的执行取决于输入数据。这里将展示3种合并的简单例子,随后讨论相信的选项。
一对一联合
也许最简单的合并表达类型是一对一的联合,它在许多方面都有点像按列的级联。作为一个具体例子,考虑如下两个包含几名公司职员信息的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。注意,不一定要维持每个列中条目的顺序:在本例中,df1和df2中“employee”列的次序是不同的,pd.merge()函数正确的处理了这种情况。另外要注意的是,合并操作通常会丢弃索引,除了特殊指定按索引合并的情况(参见关键字lefg_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', 'coding', 'linux',
'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 coding
3 Engineering linux
4 HR spreadsheets
5 HR organization
pd.merge(df1, df5)
employee group skills
0 Bob Accounting math
1 Bob Accounting spreadsheets
2 Jake Engineering coding
3 Jake Engineering linux
4 Lisa Engineering coding
5 Lisa Engineering linux
6 Sue HR spreadsheets
7 Sue HR organization
这三种联合同其他的Pandas工具一起使用来实现许多功能。但实际上,数据集很少像我们这里使用的那样干净。在下面的部分中,我们将考虑pd.merge()提供的一些选项,使您能够调整联合操作的工作方式。
合并键的说明
我们已经看到了pd.merge()的默认行为:它在两个输入中查找一个或多个匹配的列名称,并且把找到的列作为键。但是,通常情况下列名称不会匹配的那么好,pd.merge()提供了多种选择来处理这个问题。
关键字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')
f1a
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
Lisa Engineering 2004
Bob Accounting 2008
Jake Engineering 2012
Sue HR 2014
为了便利,DataFrame实现了join()方法,它用来执行默认基于索引的合并:
display('df1a', 'df2a', 'df1a.join(df2a)')
df1a
group
employee
Bob Accounting
Jake Engineering
Lisa Engineering
Sue HR
df2a
hire_date
employee
Lisa 2004
Bob 2008
Jake 2012
Sue 2014
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文档"Merge, Join, and Concatenate" section
指定用于联合的集合算法
所有前面的例子中,我们并没有提到执行联合操作时需要的一个重要考虑因素:用在联合操作上集合算法的类型。当某个值在一个键值列中存在,而另一键值列中不存在时,问题就出现了。考虑下面的例子:
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)连接。我们可以使用how关键字来显示的指定合并方式,how的默认值就是inner
pd.merge(df6, df7, how='inner')
name food drink
0 Mary bread wine
关键字how其他可选值有:'outer','left','right'。outer方式得到的是输入列的合集联合,并且会把缺失的数据设置为NaN:
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 Peter fish NaN
1 Paul beans NaN
2 Mary bread wine
3 Joseph NaN beer
'left'和'right'得到的结果分别是基于左面输入和右面输入的联合:例如:
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关键字来定制自己的后缀:
display('df8', 'df9', 'pd.merge(df8, df9, on="name", suffixes=["_L", "_R"])')
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", suffixes=["_L", "_R"])
name rank_L rank_R
0 Bob 1 3
1 Jake 2 1
2 Lisa 3 4
3 Sue 4 2
后缀操作在任何可能的连接模式下都工作,在多个重叠列的情况下也可以。
对应这些模式的更多信息,参见Aggregation and Grouping,在那里我们有更深入的研究相关算法。也可以参见Pandas "Merge, Join and Concatenate" documentation来获取相关主题的讨论
例子:美国州数据
合并和联合操作使用最多是在合并不同数据源的数据时。我们考虑一个关于美国州及其人口数据的例子。数据在http://github.com/jakevdp/data-USstates/:
# Following are shell commands to download the data 以下时用来下载数据的脚本命令
# !curl -O https://raw.githubusercontent.com/jakevdp/data-USstates/master/state-population.csv
# !curl -O https://raw.githubusercontent.com/jakevdp/data-USstates/master/state-areas.csv
# !curl -O https://raw.githubusercontent.com/jakevdp/data-USstates/master/state-abbrevs.csv
让我们使用Pandas的read_csv()函数来看一下这3个数据集:
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', 1) # drop duplicate info
merged.head()
state/region ages year population state
0 AL under18 2012 1117489.0 Alabama
1 AL total 2012 4817528.0 Alabama
2 AL under18 2010 1130966.0 Alabama
3 AL total 2010 4785570.0 Alabama
4 AL under18 2011 1125763.0 Alabama
让我们通过检查空行,来确认是否有不匹配的地方:
merged.isnull().any()
state/region False
ages False
year False
population True
state True
dtype: bool
一些人口信息是空的;让我们找出是哪些!
merged[merged['population'].isnull()].head()
state/region ages year population state
2448 PR under18 1990 NaN NaN
2449 PR total 1990 NaN NaN
2450 PR total 1991 NaN NaN
2451 PR under18 1991 NaN NaN
2452 PR total 1993 NaN NaN
看起来空的人口值时来自于2000之前的Puerto Rico;这好像是因为它在原始数据中就是不可以的。
更重要的是,我们还发现一些州的条目也是空的,这意味着在abbrevs键中并没有对应的值。让我们找到那个区域缺失这些匹配:
merged.loc[merged['state'].isnull(), 'state/region'].unique()
array(['PR', 'USA'], dtype=object)
我们可以迅速的定位问题:人口数据中包含条目Puerto Rico (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 AL under18 2012 1117489.0 Alabama 52423.0
1 AL total 2012 4817528.0 Alabama 52423.0
2 AL under18 2010 1130966.0 Alabama 52423.0
3 AL total 2010 4785570.0 Alabama 52423.0
4 AL under18 2011 1125763.0 Alabama 52423.0
让我们再次检查空值来确定是否有不匹配的地方:
final.isnull().any()
state/region False
ages False
year False
population True
state False
area (sq. mi) True
dtype: bool
在area列有空值,我们看一眼哪个地区被忽略了:
final['state'][final['area (sq. mi)'].isnull()].unique()
array(['United States'], dtype=object)
我们看到areasDataFrame并不包含美国作为整体的面积数据。我们可以给它添加合适的值(比如,使用使用所有州面积和),但是在本例中,我们只是把这些空值去掉,因为整个美国的人口密度同当前的问题并不相关:
final.dropna(inplace=True)
final.head()
state/region ages year population state area (sq. mi)
0 AL under18 2012 1117489.0 Alabama 52423.0
1 AL total 2012 4817528.0 Alabama 52423.0
2 AL under18 2010 1130966.0 Alabama 52423.0
3 AL total 2010 4785570.0 Alabama 52423.0
4 AL under18 2011 1125763.0 Alabama 52423.0
现在我们已经有了所有需要的数据。为了回答感兴趣的问题,让我们首先来选择对应于2000年和总人口信息这部分数据。我们会使用query()函数来快速实现(这个函数需要安装numexprbao;参见High-Performance Pandas: eval()
and query()
)
data2010 = final.query("year == 2010 & ages == 'total'")
data2010.head()
state/region ages year population state area (sq. mi)
3 AL total 2010 4785570.0 Alabama 52423.0
91 AK total 2010 713868.0 Alaska 656425.0
101 AZ total 2010 6408790.0 Arizona 114006.0
189 AR total 2010 2922280.0 Arkansas 53182.0
197 CA total 2010 37333601.0 California 163707.0
现在我们还计算人口密度并且按次序显示。我们开始对数据按state进行重新索引,然后计算结果:
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
结果是美国州加上Washington,DC 和 Puerto Rico的2010按人口密度(每平方公里的居民数目)的排名。我们可以看到在数据集中最密的地区是Washington, DC;所有州中,最密的是新泽西。
我们也可以这儿列表的后面:
density.tail()
state
South Dakota 10.583512
North Dakota 9.537565
Montana 6.736171
Wyoming 5.768079
Alaska 1.087509
dtype: float64
我们看到人口最稀薄的州是阿拉斯加,每平方英里才一个多居民。
在使用实际数据源来回答问题时,这类混杂数据的合并时很平常的工作。我希望这个例子可以带给你一些想法,关于如果使用这些我们学习过的工具来从数据里面获取洞察力。