2.6. 多表操作#
之前的操作主要在单个 DataFrame ,实际上,我们经常需要对多个 DataFrame 联合起来进行分析。pandas 提供了多 DataFrame 之间的合并和连接的操作,分别是 merge() 和 concat() 函数。比如,我们可以将两个 DataFrame 合并成一个,且保留所有的列。
merge#
pandas 的 merge() 操作可以合并两个 DataFrame(或者称为表) ,类似于 SQL 中的 JOIN 操作。 我们可以想象成:一个大表被拆分成两个小表,两个小表都包含一些同样的数据。现在我们需要把两个小表合并,生成一个大表,大表包含了两个小表的字段。
图 2.8 对两个 DataFrame 进行 merge 操作#
import pandas as pd
import numpy as np
# customer_prod_df
d1 = {'customer_id':pd.Series([1,2,3,4,5,6]),
'product':pd.Series(['Oven','Oven','Oven','Television','Television','Television'])}
customer_prod_df = pd.DataFrame(d1)
print("customer_prod_df:")
customer_prod_df
customer_prod_df:
| customer_id | product | |
|---|---|---|
| 0 | 1 | Oven |
| 1 | 2 | Oven |
| 2 | 3 | Oven |
| 3 | 4 | Television |
| 4 | 5 | Television |
| 5 | 6 | Television |
# customer_state_df
d2 = {'customer_id':pd.Series([2,4,6,7,8]),
'state':pd.Series(['California','California','Texas','New York','Indiana'])}
customer_state_df = pd.DataFrame(d2)
print("customer_state_df:")
customer_state_df
customer_state_df:
| customer_id | state | |
|---|---|---|
| 0 | 2 | California |
| 1 | 4 | California |
| 2 | 6 | Texas |
| 3 | 7 | New York |
| 4 | 8 | Indiana |
我们先用下面的代码得到合并的结果,所使用的各类参数后面详细探讨。
inner_join_df= pd.merge(customer_prod_df, customer_state_df, on='customer_id', how='inner')
inner_join_df
| customer_id | product | state | |
|---|---|---|---|
| 0 | 2 | Oven | California |
| 1 | 4 | Television | California |
| 2 | 6 | Television | Texas |
合并两个表时,通常需要使用键(key)来判断哪些数据与哪些数据合并。比如本例中,两个表都包含 customer_id 这个字段,customer_id 可以被用来连接这两个 DataFrame。相同 customer_id 的行被合并在一起。
我们回到 merge 函数的参数。完整的参数形式为:pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False,suffixes=('_x','_y'))。虽然参数很多,其实大部分时间我们主要关心的是 left、right、how 和 on。
left 与 right 是两个 DataFrame,分别为左侧表和右侧表。
on 表示两个表依据哪个字段(或哪些字段)进行合并。这个字段或这些字段被称为键(key)。两个表的 key 字段应该是表示同一个事物。现在我们要根据 key,把两个 DataFrame 合并成一个更大的表。这个例子中,以 customer_id 为字段进行合并,主要对左右两个表中相同 customer_id 进行操作。
how 表示合并的方式。在合并过程中,左侧表和右侧表的 key 不一定都包含同样的值。比如,如果左侧表包含某个 customer_id 但右侧表不包含这个 customer_id,那应该遵循什么样的逻辑去合并。how='inner' 是其中一种方式,叫做内连接:只有在左侧 DataFrame 和右侧 DataFrame 中 ** 都 ** 存在的行才会包含在结果中。
how 除了 inner,还有其他的选项:outer、left 和 right。
:width:
800px
:label:merge-how
连接方式#
外连接
外连接把左右两侧所有的数据都合并到一起,如果某个表有缺失值,无法左右对齐,则填充 NaN。
outer_join_df = pd.merge(customer_prod_df, customer_state_df, on='customer_id', how='outer')
outer_join_df
| customer_id | product | state | |
|---|---|---|---|
| 0 | 1 | Oven | NaN |
| 1 | 2 | Oven | California |
| 2 | 3 | Oven | NaN |
| 3 | 4 | Television | California |
| 4 | 5 | Television | NaN |
| 5 | 6 | Television | Texas |
| 6 | 7 | NaN | New York |
| 7 | 8 | NaN | Indiana |
左连接
左侧表的数据都被保留,如果右侧表有缺失,填充 NaN。
left_join_df= pd.merge(customer_prod_df, customer_state_df, on='customer_id', how='left')
left_join_df
| customer_id | product | state | |
|---|---|---|---|
| 0 | 1 | Oven | NaN |
| 1 | 2 | Oven | California |
| 2 | 3 | Oven | NaN |
| 3 | 4 | Television | California |
| 4 | 5 | Television | NaN |
| 5 | 6 | Television | Texas |
右连接
右侧表的数据都被保留,如果左侧表有缺失,填充 NaN。
right_join_df= pd.merge(customer_prod_df, customer_state_df, on='customer_id', how='right')
right_join_df
| customer_id | product | state | |
|---|---|---|---|
| 0 | 2 | Oven | California |
| 1 | 4 | Television | California |
| 2 | 6 | Television | Texas |
| 3 | 7 | NaN | New York |
| 4 | 8 | NaN | Indiana |
其他合并方式#
还有一些其他合并,不是根据 key 去在左右表中查找。比如,根据 DataFrame 最左侧的 index 列。DataFrame 默认有 index 列,根据该列合并两个表,由于此时不是根据 customer_id 来合并,合并之后保留了两个 customer_id,为了区别左右两表的 customer_id,这里加了后缀 _x 和 _y。
index_df = pd.merge(customer_prod_df, customer_state_df, right_index=True, left_index=True)
index_df
| customer_id_x | product | customer_id_y | state | |
|---|---|---|---|---|
| 0 | 1 | Oven | 2 | California |
| 1 | 2 | Oven | 4 | California |
| 2 | 3 | Oven | 6 | Texas |
| 3 | 4 | Television | 7 | New York |
| 4 | 5 | Television | 8 | Indiana |
concat#
与 merge() 对表结构进行横向的操作不同,concat() 函数默认是进行纵向的合并,也就是将多行进行 堆叠式 的合并,如 图 2.9 所示。
图 2.9 对两个 DataFrame 进行 concat 操作#
比如下面的例子,把所有的行和所有的列合并在一起,缺失的填充 NaN。
row_concat_df = pd.concat([customer_prod_df, customer_state_df])
row_concat_df
| customer_id | product | state | |
|---|---|---|---|
| 0 | 1 | Oven | NaN |
| 1 | 2 | Oven | NaN |
| 2 | 3 | Oven | NaN |
| 3 | 4 | Television | NaN |
| 4 | 5 | Television | NaN |
| 5 | 6 | Television | NaN |
| 0 | 2 | NaN | California |
| 1 | 4 | NaN | California |
| 2 | 6 | NaN | Texas |
| 3 | 7 | NaN | New York |
| 4 | 8 | NaN | Indiana |
下面的例子,设定了 join="inner",两个表中都包含 customer_id 字段进行了纵向合并。需要注意的是,concat 是进行纵向的堆叠,而没有去除重复的 customer_id。
concat_inner_df = pd.concat([customer_prod_df, customer_state_df], join = 'inner')
concat_inner_df
| customer_id | |
|---|---|
| 0 | 1 |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
| 4 | 5 |
| 5 | 6 |
| 0 | 2 |
| 1 | 4 |
| 2 | 6 |
| 3 | 7 |
| 4 | 8 |
案例:学生成绩#
我们使用一个学生成绩的案例来演示如何对两个 DataFrame 进行 merge。
Show code cell content
import urllib.request
import os
import pandas as pd
download_url_prefix = "https://py-ds.godaai.org/datasets/student-score"
folder_path = os.path.join(os.getcwd(), "../data/student-score")
score_download_url = f"{download_url_prefix}/score.csv"
student_download_url = f"{download_url_prefix}/student.csv"
score_file_name = score_download_url.split("/")[-1]
student_file_name = student_download_url.split("/")[-1]
score_path = os.path.join(folder_path, score_file_name)
student_path = os.path.join(folder_path, student_file_name)
if not os.path.exists(folder_path):
os.makedirs(folder_path)
if not os.path.exists(score_path):
urllib.request.urlretrieve(score_download_url, score_path)
if not os.path.exists(student_path):
urllib.request.urlretrieve(student_download_url, student_path)
print("student DF:")
student_df = pd.read_csv(student_path, encoding = "UTF-8")
print(f"shape of this DF: {student_df.shape}")
print("the first 2 rows of this DF:")
student_df.head(2)
student DF:
shape of this DF: (44, 7)
the first 2 rows of this DF:
| sno | sname | sex | dept | major | claasno | home_address | |
|---|---|---|---|---|---|---|---|
| 0 | 2019101101 | 张元 | 男 | 信息学院 | 计算机 | 1 | 北京市 |
| 1 | 2019101102 | 李玲 | 女 | 信息学院 | 计算机 | 1 | 天津市 |
print("score DF:")
score_df = pd.read_csv(score_path, encoding = "UTF-8")
print(f"Shape of this DF: {score_df.shape}")
print("the first 2 rows of this DF:")
score_df.head(2)
score DF:
Shape of this DF: (43, 4)
the first 2 rows of this DF:
| sno | cno | cname | score | |
|---|---|---|---|---|
| 0 | 2019101103 | E202 | 宏观经济 | 78 |
| 1 | 2019102105 | E202 | 宏观经济 | 97 |
可以看到两个表都包含了 sno,也就是学号,可以使用学号作为 key 进行合并。
merged_df = pd.merge(left = student_df, right = score_df, how = 'left', on = 'sno')
print(f"shape of this DF: {merged_df.shape}")
print("the first 2 rows:")
merged_df.head(2)
shape of this DF: (44, 10)
the first 2 rows:
| sno | sname | sex | dept | major | claasno | home_address | cno | cname | score | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2019101101 | 张元 | 男 | 信息学院 | 计算机 | 1 | 北京市 | CS101 | 程序设计 | 89.0 |
| 1 | 2019101102 | 李玲 | 女 | 信息学院 | 计算机 | 1 | 天津市 | CS203 | 数据结构与算法 | 80.0 |
关于合并后的新表,列数一共10列。student_df 一共7列, score_df 一共4列,即:7 + 4 - 1,因为两个表都包含同样的一列 sno,所以需要减一。
我们使用的是 how='left' 的方式进行的合并。行数一共44,这与 student_df 的列数一致。我们打印完整合并后的新表,可以发现,在student_df中包含但 score_df 中不包含的成绩相关的信息显示 NaN。
接下来我们就可以使用各类其他分析方式,对这个新的大表进行数据分析,这里不再赘述。