7.合并数据集:合并与联合

Pandas很出众的一个特性就是高性能的,内存内的联合和合并操作。如果你曾经使用过数据库,你将会对这种类型的数据交互很熟悉。主要接口是pd.merge函数,将会看到几个实际工作的例子:
为了方便,我们由重新定义前面章节的display()函数开始:

import pandas as pd
import numpy as np

class display(object):
    """Display HTML representation of multiple objects"""
    template = """

{0}

{1}
""" 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

我们看到人口最稀薄的州是阿拉斯加,每平方英里才一个多居民。
在使用实际数据源来回答问题时,这类混杂数据的合并时很平常的工作。我希望这个例子可以带给你一些想法,关于如果使用这些我们学习过的工具来从数据里面获取洞察力。

你可能感兴趣的:(7.合并数据集:合并与联合)