第7章 数据合并与重塑
7.1 数据合并:concat 横向与纵向拼接
在数据分析过程中,我们经常会遇到需要将多个数据集合并在一起的情况。pandas 提供了 concat 函数来实现数据的拼接操作,这是数据合并的基础技能。
concat 函数基础
concat 函数可以沿着指定轴(axis)将多个 pandas 对象连接在一起。最常用的是沿着行(axis=0,纵向拼接)或列(axis=1,横向拼接)进行合并。
下面的表格总结了 concat 的主要用法:
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 纵向拼接 | pd.concat([df1, df2], axis=0) | 沿行方向拼接,默认行为,要求列名一致或兼容 |
| 横向拼接 | pd.concat([df1, df2], axis=1) | 沿列方向拼接,要求索引一致或兼容 |
| 忽略原索引 | pd.concat([df1, df2], ignore_index=True) | 重新创建连续的整数索引,避免重复索引问题 |
| 内连接拼接 | pd.concat([df1, df2], join='inner') | 只保留所有数据集中都存在的列(横向)或索引(纵向) |
让我们通过具体的代码示例来理解这些概念:
# 导入必要的库
import pandas as pd
import numpy as np
# 创建第一个示例DataFrame
df1 = pd.DataFrame({
'A': [1, 2, 3],
'B': [4, 5, 6],
'C': [7, 8, 9]
}, index=['x', 'y', 'z'])
# 创建第二个示例DataFrame
df2 = pd.DataFrame({
'A': [10, 11],
'B': [12, 13],
'C': [14, 15]
}, index=['w', 'v'])
# 纵向拼接两个DataFrame(默认axis=0)
try:
result_vertical = pd.concat([df1, df2])
print("纵向拼接结果:")
print(result_vertical)
except Exception as e:
print(f"纵向拼接出错: {e}")
# 创建用于横向拼接的DataFrame(注意索引要匹配)
df3 = pd.DataFrame({
'D': [100, 200, 300]
}, index=['x', 'y', 'z']) # 索引与df1相同
# 横向拼接
try:
result_horizontal = pd.concat([df1, df3], axis=1)
print("\n横向拼接结果:")
print(result_horizontal)
except Exception as e:
print(f"横向拼接出错: {e}")
# 使用ignore_index参数重新索引
try:
result_reset_index = pd.concat([df1, df2], ignore_index=True)
print("\n忽略原索引的拼接结果:")
print(result_reset_index)
except Exception as e:
print(f"重置索引拼接出错: {e}")这个小节介绍了 concat 函数的基本用法,包括纵向和横向拼接,以及如何处理索引问题。掌握这些基础操作对于后续更复杂的数据合并任务至关重要。
7.2 主键连接:merge 的 inner、left、outer
当需要基于一个或多个共同列(主键)来合并数据时,merge 函数比 concat 更加灵活和强大。它类似于 SQL 中的 JOIN 操作,提供了多种连接方式。
merge 连接类型详解
pandas 的 merge 函数支持四种主要的连接类型:
- inner: 只保留两个数据集中都存在的键
- left: 保留左数据集的所有行,右数据集中不存在的键用 NaN 填充
- right: 保留右数据集的所有行,左数据集中不存在的键用 NaN 填充
- outer: 保留两个数据集中的所有键,不存在的用 NaN 填充
下面的表格总结了 merge 的主要参数和用法:
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 内连接 | pd.merge(df1, df2, on='key', how='inner') | 只返回两个DataFrame中都存在的键对应的行 |
| 左连接 | pd.merge(df1, df2, on='key', how='left') | 保留df1的所有行,df2中没有匹配的用NaN填充 |
| 外连接 | pd.merge(df1, df2, on='key', how='outer') | 返回所有键的并集,缺失值用NaN填充 |
| 指定连接列 | pd.merge(df1, df2, left_on='col1', right_on='col2') | 当左右DataFrame的连接列名不同时使用 |
让我们通过代码示例来演示这些连接类型:
# 导入必要的库
import pandas as pd
import numpy as np
# 创建员工基本信息DataFrame
employees = pd.DataFrame({
'employee_id': [1, 2, 3, 4, 5],
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'department': ['HR', 'IT', 'Finance', 'IT', 'Marketing']
})
# 创建员工薪资信息DataFrame
salaries = pd.DataFrame({
'employee_id': [1, 2, 4, 6], # 注意:员工5没有薪资记录,员工6不在员工表中
'salary': [50000, 60000, 55000, 70000],
'bonus': [5000, 6000, 5500, 7000]
})
print("员工基本信息:")
print(employees)
print("\n员工薪资信息:")
print(salaries)
# 内连接 - 只保留两个表中都存在的员工
try:
inner_join = pd.merge(employees, salaries, on='employee_id', how='inner')
print("\n内连接结果 (只显示都有记录的员工):")
print(inner_join)
except Exception as e:
print(f"内连接出错: {e}")
# 左连接 - 保留所有员工,薪资信息缺失的用NaN填充
try:
left_join = pd.merge(employees, salaries, on='employee_id', how='left')
print("\n左连接结果 (显示所有员工,薪资缺失的为NaN):")
print(left_join)
except Exception as e:
print(f"左连接出错: {e}")
# 外连接 - 显示所有员工和所有薪资记录
try:
outer_join = pd.merge(employees, salaries, on='employee_id', how='outer')
print("\n外连接结果 (显示所有记录):")
print(outer_join)
except Exception as e:
print(f"外连接出错: {e}")
# 演示不同列名的连接
departments = pd.DataFrame({
'dept_code': ['HR', 'IT', 'Finance', 'Marketing'],
'dept_budget': [100000, 200000, 150000, 120000]
})
# 当连接列名不同时,使用left_on和right_on
try:
dept_join = pd.merge(employees, departments,
left_on='department', right_on='dept_code',
how='left')
print("\n不同列名连接结果:")
print(dept_join)
except Exception as e:
print(f"不同列名连接出错: {e}")这个小节详细介绍了 merge 函数的各种连接方式,这对于处理来自不同数据源但有共同标识符的数据集非常有用。理解不同连接类型的特点可以帮助你根据分析需求选择合适的合并策略。
7.3 索引对齐连接与多列连接
在实际的数据分析工作中,我们经常需要处理更复杂的连接场景,比如基于索引进行连接,或者需要同时基于多个列进行连接。pandas 的 merge 函数同样支持这些高级功能。
索引对齐连接
有时候,DataFrame 的索引本身就包含了重要的连接信息。在这种情况下,我们可以直接基于索引来进行连接操作,而不需要指定具体的列名。
多列连接
当单一的连接键不足以唯一标识记录时,我们需要基于多个列的组合来进行连接。这在处理复合主键或需要更精确匹配的场景中非常常见。
下面的表格总结了这些高级连接功能:
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 索引连接 | pd.merge(df1, df2, left_index=True, right_index=True) | 基于两个DataFrame的索引进行连接 |
| 左索引右列连接 | pd.merge(df1, df2, left_index=True, right_on='col') | 左DataFrame用索引,右DataFrame用指定列进行连接 |
| 多列连接 | pd.merge(df1, df2, on=['col1', 'col2']) | 基于多个列的组合进行连接 |
| 多列不同名列连接 | pd.merge(df1, df2, left_on=['c1','c2'], right_on=['c3','c4']) | 左右DataFrame使用不同的多列组合进行连接 |
让我们通过代码示例来演示这些高级连接技术:
# 导入必要的库
import pandas as pd
import numpy as np
# 创建基于日期索引的销售数据
sales_data = pd.DataFrame({
'product': ['A', 'B', 'C', 'A', 'B'],
'quantity': [100, 150, 200, 120, 180]
}, index=pd.to_datetime(['2023-01-01', '2023-01-01', '2023-01-01',
'2023-01-02', '2023-01-02']))
sales_data.index.name = 'date'
# 创建基于日期索引的价格数据
price_data = pd.DataFrame({
'product': ['A', 'B', 'C'],
'price': [10.5, 15.0, 20.0]
}, index=pd.to_datetime(['2023-01-01', '2023-01-01', '2023-01-01']))
price_data.index.name = 'date'
print("销售数据 (索引为日期):")
print(sales_data)
print("\n价格数据 (索引为日期):")
print(price_data)
# 基于索引和产品列的多条件连接
try:
# 首先重置索引,将日期变为普通列
sales_reset = sales_data.reset_index()
price_reset = price_data.reset_index()
# 基于日期和产品进行多列连接
merged_multi = pd.merge(sales_reset, price_reset,
on=['date', 'product'], how='left')
print("\n基于日期和产品的多列连接结果:")
print(merged_multi)
except Exception as e:
print(f"多列连接出错: {e}")
# 创建用于索引连接的示例数据
stock_symbols = pd.DataFrame({
'company': ['Apple', 'Google', 'Microsoft', 'Amazon']
}, index=['AAPL', 'GOOGL', 'MSFT', 'AMZN'])
stock_prices = pd.DataFrame({
'current_price': [150.0, 2800.0, 300.0, 3200.0],
'market_cap': [2.5e12, 1.8e12, 2.2e12, 1.6e12]
}, index=['AAPL', 'GOOGL', 'MSFT', 'AMZN'])
print("\n股票代码数据:")
print(stock_symbols)
print("\n股票价格数据:")
print(stock_prices)
# 基于索引的连接
try:
index_merged = pd.merge(stock_symbols, stock_prices,
left_index=True, right_index=True)
print("\n基于索引连接的结果:")
print(index_merged)
except Exception as e:
print(f"索引连接出错: {e}")
# 创建复合主键示例
orders = pd.DataFrame({
'customer_id': [1, 1, 2, 2, 3],
'order_date': ['2023-01-01', '2023-01-15', '2023-01-02', '2023-01-20', '2023-01-05'],
'order_amount': [100, 150, 200, 250, 300]
})
customer_info = pd.DataFrame({
'customer_id': [1, 1, 2, 2, 3, 4], # 注意customer_id重复
'region': ['North', 'South', 'North', 'East', 'West', 'North'],
'discount_rate': [0.1, 0.05, 0.15, 0.1, 0.2, 0.05]
})
# 假设同一个customer_id在不同区域有不同的信息
print("\n订单数据:")
print(orders)
print("\n客户信息数据:")
print(customer_info)
# 基于customer_id和region的多列连接
# 首先给orders添加region列(简化示例)
orders['region'] = ['North', 'South', 'North', 'East', 'West']
try:
complex_merge = pd.merge(orders, customer_info,
on=['customer_id', 'region'], how='left')
print("\n基于客户ID和区域的多列连接结果:")
print(complex_merge)
except Exception as e:
print(f"复合主键连接出错: {e}")这个小节展示了如何处理更复杂的连接场景,包括基于索引的连接和多列连接。这些技术在实际项目中非常实用,特别是在处理时间序列数据或具有复合主键的数据库表时。
7.4 长宽表转换:melt 与 pivot
在数据分析中,数据的组织形式对分析的便利性有很大影响。长格式(Long format)和宽格式(Wide format)是两种常见的数据组织方式,它们各有优势。pandas 提供了 melt 和 pivot(以及 pivot_table)函数来在这两种格式之间进行转换。
长格式 vs 宽格式
- 宽格式:每个变量占一列,每个观测占一行。适合展示和某些类型的分析。
- 长格式:包含标识变量的列和值的列,通常有更多行但列更少。适合大多数统计分析和可视化。
melt 函数:宽转长
melt 函数将宽格式数据转换为长格式,它会将指定的列"融化"成变量-值对。
pivot 函数:长转宽
pivot 函数将长格式数据转换为宽格式,它会将一列的唯一值作为新列,另一列的值填充到对应位置。
下面的表格总结了这些转换函数的用法:
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 宽转长 | pd.melt(df, id_vars=['id_col'], value_vars=['var1','var2']) | 将指定的变量列转换为变量-值对,id_vars保持不变 |
| 长转宽 | df.pivot(index='id_col', columns='var_col', values='value_col') | 将var_col的唯一值作为新列,value_col的值作为数据 |
| 带聚合的长转宽 | df.pivot_table(index='id', columns='var', values='val', aggfunc='mean') | 当存在重复索引时,使用聚合函数处理 |
| 重置索引 | df.pivot(...).reset_index() | 将pivot后的层次化索引转换为普通列 |
让我们通过代码示例来演示这些转换操作:
# 导入必要的库
import pandas as pd
import numpy as np
# 创建宽格式的销售数据
wide_sales = pd.DataFrame({
'product': ['A', 'B', 'C'],
'Q1_2023': [100, 150, 200],
'Q2_2023': [120, 180, 220],
'Q3_2023': [110, 160, 210],
'Q4_2023': [130, 190, 230]
})
print("原始宽格式数据:")
print(wide_sales)
# 使用melt将宽格式转换为长格式
try:
long_sales = pd.melt(wide_sales,
id_vars=['product'], # 保持不变的列
value_vars=['Q1_2023', 'Q2_2023', 'Q3_2023', 'Q4_2023'], # 要融化的列
var_name='quarter', # 新的变量名列名
value_name='sales') # 新的值列名
print("\n转换后的长格式数据:")
print(long_sales)
except Exception as e:
print(f"宽转长出错: {e}")
# 创建长格式的学生成绩数据
long_grades = pd.DataFrame({
'student': ['Alice', 'Alice', 'Bob', 'Bob', 'Charlie', 'Charlie'],
'subject': ['Math', 'English', 'Math', 'English', 'Math', 'English'],
'grade': [85, 90, 78, 88, 92, 87]
})
print("\n原始长格式数据:")
print(long_grades)
# 使用pivot将长格式转换为宽格式
try:
wide_grades = long_grades.pivot(index='student',
columns='subject',
values='grade')
print("\n转换后的宽格式数据:")
print(wide_grades)
# 重置索引,使student变为普通列
wide_grades_reset = wide_grades.reset_index()
print("\n重置索引后的宽格式数据:")
print(wide_grades_reset)
except Exception as e:
print(f"长转宽出错: {e}")
# 演示pivot_table处理重复值的情况
# 创建包含重复记录的数据
duplicate_data = pd.DataFrame({
'date': ['2023-01-01', '2023-01-01', '2023-01-02', '2023-01-02'],
'product': ['A', 'A', 'B', 'B'],
'sales': [100, 120, 150, 160] # 同一天同一产品的多次销售记录
})
print("\n包含重复记录的数据:")
print(duplicate_data)
# 使用pivot_table进行聚合
try:
pivoted_with_agg = duplicate_data.pivot_table(
index='date',
columns='product',
values='sales',
aggfunc='sum' # 对重复值求和
)
print("\n使用pivot_table聚合后的结果:")
print(pivoted_with_agg)
except Exception as e:
print(f"pivot_table出错: {e}")
# 演示melt的更多选项
time_series_wide = pd.DataFrame({
'region': ['North', 'South', 'East', 'West'],
'Jan': [100, 120, 90, 110],
'Feb': [110, 130, 95, 115],
'Mar': [115, 135, 100, 120]
})
print("\n时间序列宽格式数据:")
print(time_series_wide)
# 自定义melt的列名
try:
time_series_long = pd.melt(time_series_wide,
id_vars=['region'],
var_name='month',
value_name='revenue')
print("\n转换后的时间序列长格式数据:")
print(time_series_long)
except Exception as e:
print(f"时间序列melt出错: {e}")