52 数据框拼接
52.1 引言多源数据整合的挑战
在真实的数据分析项目中,数据往往分散在多个来源中。对于金融分析师而言,将不同来源的数据整合在一起是日常工作的核心挑战。
52.1.1 金融数据整合的典型场景
多源数据整合的必要性:
- 行情数据:来自交易所的实时价格、成交量数据
- 财务数据:来自公司财报的资产负债表、利润表数据
- 宏观数据:来自统计部门的GDP、CPI、利率数据
- 情绪数据:来自新闻舆情、社交媒体的投资者情绪指标
52.1.2 数据整合的核心问题
为什么数据整合如此复杂?
- 粒度不匹配:日频行情数据 vs 季频财务数据
- 时间对齐:不同市场的交易日历不同(如A股 vs 美股)
- 键值识别:如何正确匹配同一公司的不同数据源?
- 重复数据:同一指标可能来自多个提供商,如何去重?
- 性能瓶颈:大规模数据集的合并操作可能极其耗时
本章学习目标: - 理解concat、merge、join的本质区别与应用场景 - 掌握不同类型连接(inner/left/right/outer)的数学含义 - 学习金融时间序列数据的对齐与聚合技术 - 了解大数据环境下的性能优化策略 - 掌握多级索引在复杂数据整合中的应用
52.2 数据拼接的数学基础
52.2.1 垂直拼接(行向堆叠)
定义:将多个数据集沿行方向(纵向)堆叠,增加观测数量。
设两个数据矩阵: \[ D_1 = \begin{bmatrix} x_{11} & x_{12} \\ x_{21} & x_{22} \\ \vdots & \vdots \\ x_{n1} & x_{n2} \end{bmatrix}, \quad D_2 = \begin{bmatrix} y_{11} & y_{12} \\ y_{21} & y_{22} \\ \vdots & \vdots \\ y_{m1} & y_{m2} \end{bmatrix} \]
垂直拼接结果: \[ D_{\text{concat}} = \begin{bmatrix} D_1 \\ D_2 \end{bmatrix} = \begin{bmatrix} x_{11} & x_{12} \\ \vdots & \vdots \\ x_{n1} & x_{n2} \\ y_{11} & y_{12} \\ \vdots & \vdots \\ y_{m1} & y_{m2} \end{bmatrix} \]
前提条件:两个数据集必须具有相同的列结构(相同列名和数据类型)。
52.2.2 水平拼接(列向合并)
定义:将多个数据集沿列方向(横向)合并,增加变量数量。
\[ D_{\text{merge}} = [D_1 \mid D_2] = \begin{bmatrix} x_{11} & x_{12} & y_{11} & y_{12} \\ \vdots & \vdots & \vdots & \vdots \\ x_{n1} & x_{n2} & y_{n1} & y_{n2} \end{bmatrix} \]
前提条件:两个数据集必须具有相同的行数或可通过键值对齐。
52.2.3 关系代数基础
Pandas的merge操作基于关系代数(Relational Algebra)中的连接(Join)运算:
\[ R \bowtie_{\theta} S = \{ (r, s) \in R \times S \mid \theta(r, s) \} \]
其中: - \(R, S\): 两个关系(数据表) - \(\bowtie\): 连接运算符 - \(\theta\): 连接条件(通常是键值相等)
52.3 concat函数垂直拼接的首选工具
52.3.1 基础语法与参数
平台任务1解答代码
以下代码与教学平台任务要求完全一致:
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)#从外部导入Sheet1的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_JantoMar.head()) #查看前五行数据
print(price_JantoMar.tail()) #查看后五行数据
price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)##从外部导入Sheet2的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_AprtoJui.head()) #查看前五行数据
print(price_AprtoJui.tail()) #查看后五行数据平台任务2解答代码
以下代码与教学平台任务要求完全一致:
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
# 从Excel文件读取数据存入price_JantoMar
price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)
# 从Excel文件读取数据存入price_AprtoJui
price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)
price_JantoJul = pd.concat([price_JantoMar,price_AprtoJui],axis=0) #使用concat函数按行拼接
print(price_JantoJul.head()) #前五行数据
print(price_JantoJul.tail()) #后五行数据平台任务3解答代码
以下代码与教学平台任务要求完全一致:
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_3stocks.head()) #查看前五行数据
print(price_3stocks.tail()) #查看后五行数据
price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx
print(price_2stocks.head()) #查看前五行数据
print(price_2stocks.tail()) #查看后五行数据 平台任务4解答代码
以下代码与教学平台任务要求完全一致:
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
# 从Excel文件读取数据存入price_3stocks
price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)
# 从Excel文件读取数据存入price_2stocks
price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)
price_5stocks_concat = pd.concat([price_3stocks,price_2stocks],axis=1) #使用concat函数按列拼接
print(price_5stocks_concat.head()) #查看前五行数据
price_5stocks_merge = pd.merge(left=price_3stocks,right=price_2stocks,left_index=True,right_index=True) #使用merge函数按列拼接
print(price_5stocks_merge.head()) #查看前五行数据
price_5stocks_join = price_3stocks.join(price_2stocks,on="日期") #用join函数按列拼接
print(price_5stocks_join.head()) #查看前五行数据# =============================================================================
# 题目:使用concat进行垂直拼接
# =============================================================================
# 本任务演示如何使用pd.concat()函数将多个数据框垂直拼接(沿行方向堆叠)
# 场景:将多只股票的收益率数据合并成一个长格式数据框
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库
# ==================== 创建股票A的收益率数据 ====================
# 场景:贵州茅台(600519.SH)连续3个交易日的收益率数据
# pd.date_range():生成日期范围,'2024-01-01'为起始日期,periods=3表示生成3个日期
stock_a_returns = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=3), # 生成3个连续日期
'股票代码': ['600519.SH'] * 3, # 股票代码重复3次(列表乘法)
'收益率': [0.02, -0.01, 0.03] # 3个交易日的收益率:2%, -1%, 3%
})
# ==================== 创建股票B的收益率数据 ====================
# 场景:五粮液(000858.SZ)接下来的3个交易日收益率数据
# 注意:这里的起始日期是'2024-01-04',正好接续股票A的最后日期
stock_b_returns = pd.DataFrame({
'日期': pd.date_range('2024-01-04', periods=3), # 从2024-01-04开始生成3个日期
'股票代码': ['000858.SZ'] * 3, # 五粮液的股票代码
'收益率': [0.01, 0.02, -0.02] # 3个交易日的收益率:1%, 2%, -2%
})
# ==================== 使用concat进行垂直拼接 ====================
# pd.concat():拼接函数,将多个数据框沿指定轴拼接
# 参数说明:
# [stock_a_returns, stock_b_returns]:要拼接的数据框列表
# ignore_index=True:忽略原始索引,重新生成从0开始的连续索引
# - False(默认):保留原始索引,可能出现重复索引
# - True:重置索引为0, 1, 2, ..., n-1
all_returns = pd.concat([stock_a_returns, stock_b_returns], ignore_index=True)
# ==================== 打印原始数据 ====================
print('股票A数据:')
print(stock_a_returns)
# 输出解读:贵州茅台3天的收益率数据,索引为0, 1, 2
print('\n股票B数据:')
print(stock_b_returns)
# 输出解读:五粮液3天的收益率数据,索引为0, 1, 2
# ==================== 打印拼接结果 ====================
print('\n拼接结果:')
print(all_returns)
# 输出解读:两个数据框垂直拼接,共6行数据
# ignore_index=True确保索引从0到5连续递增
# 如果ignore_index=False,索引会是0,1,2,0,1,2(重复)关键参数解析:
| 参数 | 作用 | 默认值 | 推荐用法 |
|---|---|---|---|
objs |
要拼接的对象列表 | 必需 | 使用列表 [df1, df2, ...] |
axis |
拼接方向(0=行,1=列) | 0 | axis=0垂直,axis=1水平 |
ignore_index |
忽略原索引,重新生成 | False | 拼接后通常设为True |
keys |
创建多级索引标识来源 | None | 需要追溯数据源时使用 |
join |
列对齐方式(‘inner’/‘outer’) | ‘outer’ | ’inner’只保留共有列 |
sort |
是否对列排序 | True | 大数据集设为False提升性能 |
52.3.2 多级索引的应用
场景:需要追踪每条数据的来源。
# =============================================================================
# 题目:使用concat创建多级索引
# =============================================================================
# 本任务演示如何使用keys参数在拼接时创建多级索引,以便追溯数据来源
# 场景:多源数据质量检查时,需要快速定位问题数据来自哪个数据源
# ==================== 使用keys参数创建多级索引 ====================
# keys参数:为每个输入的数据框分配一个键值,用于创建多级索引
# 参数说明:
# [stock_a_returns, stock_b_returns]:要拼接的数据框列表
# keys=['股票A', '股票B']:为两个数据框分别指定标识键
# names=['数据源', '行号']:给多级索引的每一级命名
multi_index_returns = pd.concat(
[stock_a_returns, stock_b_returns],
keys=['股票A', '股票B'], # 第一级索引:标识数据来源
names=['数据源', '行号'] # 给两级索引分别命名
)
# ==================== 打印多级索引结构 ====================
print('多级索引结构:')
print(multi_index_returns)
# 输出解读:
# - 索引变成了两级行:(股票A, 0), (股票A, 1), (股票A, 2), (股票B, 0), (股票B, 1), (股票B, 2)
# - 第一级'数据源'标识数据来自股票A还是股票B
# - 第二级'行号'保留原始数据框的行索引
# - 这种结构便于后续按数据源筛选或分析
print('\n索引信息:')
print(multi_index_returns.index)
# 输出解读:显示多级索引的完整结构,包括索引名称和级别
# ==================== 选择特定来源的数据 ====================
# multi_index_returns.loc['股票A']:使用第一级索引'数据源'进行筛选
# .loc[]:按标签索引,这里只指定第一级索引'股票A',会选出所有第二级索引的数据
print('\n仅选择股票A的数据:')
print(multi_index_returns.loc['股票A'])
# 输出解读:只显示来自股票A的3行数据
# 应用场景:发现某天数据异常时,可以快速定位是哪个数据源的问题金融应用:多源数据质量检查时,可通过多级索引快速定位问题数据的来源。
52.4 merge函数基于键值的水平合并
52.4.1 基础连接操作
# =============================================================================
# 题目:使用merge进行基础连接
# =============================================================================
# 本任务演示如何使用pd.merge()函数基于共同的键值(股票代码)进行水平合并
# 场景:将股票基本信息与财务数据合并,得到完整的分析数据集
# ==================== 创建股票基本信息数据 ====================
# 场景:3只A股的基本信息(股票代码、名称、行业)
stock_info = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH'], # 3只股票的代码
'股票名称': ['贵州茅台', '五粮液', '招商银行'], # 对应的股票名称
'行业': ['食品饮料', '食品饮料', '金融'] # 所属行业
})
# ==================== 创建股票财务数据 ====================
# 场景:3只股票的估值指标(注意:股票代码与基本信息不完全相同)
financial_data = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '601318.SH'], # 注意:第3只是中国平安,不是招商银行
'PE': [35.2, 25.8, 10.5], # 市盈率(Price-to-Earnings ratio)
'PB': [12.5, 8.3, 1.2] # 市净率(Price-to-Book ratio)
})
# ==================== 内连接(Inner Join)====================
# pd.merge():基于键值合并两个数据框
# 参数说明:
# stock_info, financial_data:要合并的两个数据框
# on='股票代码':指定合并的键值列(两边都有这一列)
# how='inner':内连接,只保留键值在两边都存在的行
# - inner(默认):交集,只保留两边都有的键
# - left:保留左表所有行
# - right:保留右表所有行
# - outer:并集,保留所有键
inner_result = pd.merge(
stock_info,
financial_data,
on='股票代码', # 基于股票代码列进行匹配
how='inner' # 内连接,只保留两边都有的股票
)
# ==================== 打印原始数据 ====================
print('股票基本信息:')
print(stock_info)
# 输出解读:包含3只股票(贵州茅台、五粮液、招商银行)
print('\n财务数据:')
print(financial_data)
# 输出解读:包含3只股票(贵州茅台、五粮液、中国平安)
# 注意:招商银行(600036.SH)在基本信息中,但不在财务数据中
# 中国平安(601318.SH)在财务数据中,但不在基本信息中
# ==================== 打印内连接结果 ====================
print('\n内连接结果(只保留两边都有的股票):')
print(inner_result)
# 输出解读:只有2只股票(贵州茅台、五粮液)被保留
# 原因:招商银行和中国平安的代码只在一边出现,被内连接过滤掉了
# 应用场景:确保分析的股票同时具备基本信息和财务数据,避免缺失值内连接的数学含义:
\[ R \bowtie S = \{ (r, s) \mid r[\text{key}] = s[\text{key}] \} \]
只有键值在两个数据集中都存在的行才会被保留。
52.4.2 连接类型的完整对比
# =============================================================================
# 题目:四种连接类型的对比
# =============================================================================
# 本任务演示merge函数的四种连接类型(inner/left/right/outer)的区别
# 场景:根据不同的业务需求,选择合适的连接方式整合数据
# ==================== 左连接(Left Join)====================
# how='left':保留左表(stock_info)的所有行
# 右表(financial_data)中匹配不到的行,其列填充为NaN(缺失值)
left_result = pd.merge(stock_info, financial_data, on='股票代码', how='left')
# 输出预期:
# - 贵州茅台:匹配成功,完整数据
# - 五粮液:匹配成功,完整数据
# - 招商银行:左表有但右表无,PE和PB列填充为NaN
# ==================== 右连接(Right Join)====================
# how='right':保留右表(financial_data)的所有行
# 左表(stock_info)中匹配不到的行,其列填充为NaN
right_result = pd.merge(stock_info, financial_data, on='股票代码', how='right')
# 输出预期:
# - 贵州茅台:匹配成功,完整数据
# - 五粮液:匹配成功,完整数据
# - 中国平安:右表有但左表无,股票名称和行业列填充为NaN
# ==================== 外连接(Outer Join)====================
# how='outer':保留所有行(左右表的并集)
# 匹配不到的列都填充为NaN
outer_result = pd.merge(stock_info, financial_data, on='股票代码', how='outer')
# 输出预期:
# - 贵州茅台:匹配成功,完整数据
# - 五粮液:匹配成功,完整数据
# - 招商银行:只在左表,右表的PE、PB列为NaN
# - 中国平安:只在右表,左表的股票名称、行业列为NaN
# ==================== 打印各种连接结果 ====================
print('左连接结果(保留左边所有股票):')
print(left_result)
# 输出解读:招商银行被保留,但其财务指标为NaN
# 应用场景:基本信息是主表,不能丢失任何股票,财务数据只是补充信息
print('\n右连接结果(保留右边所有股票):')
print(right_result)
# 输出解读:中国平安被保留,但其基本信息为NaN
# 应用场景:财务数据是主表,需要确保所有有财务数据的股票都被分析
print('\n外连接结果(保留所有股票,缺失值填充为NaN):')
print(outer_result)
# 输出解读:所有4只股票都被保留,缺失的相应位置填充为NaN
# 应用场景:最大化信息利用,后续可以分析哪些股票缺失哪些数据连接类型的决策树:
是否需要保留左边所有数据?
├─ 是 → 使用 left join
└─ 否 → 是否需要保留右边所有数据?
├─ 是 → 使用 right join
└─ 否 → 是否需要保留所有数据?
├─ 是 → 使用 outer join
└─ 否 → 使用 inner join (最严格)
金融应用指南:
| 场景 | 推荐连接类型 | 理由 |
|---|---|---|
| 主数据表匹配补充信息 | left |
保证主表数据不丢失 |
| 数据源可靠性相同 | inner |
只保留两边都有的高质量数据 |
| 整合多个不完整来源 | outer |
最大化信息利用,后续处理缺失值 |
52.5 join方法索引对齐的便捷工具
# =============================================================================
# 题目:使用join基于索引合并
# =============================================================================
# 本任务演示如何使用df.join()方法基于索引进行数据合并
# join是merge的特例,专门用于基于索引合并,代码更简洁
# 场景:两个数据框都已将股票代码设为索引,需要基于索引合并
# ==================== 创建以股票代码为索引的数据 ====================
# 场景:收益率数据,以股票代码为行索引
returns = pd.DataFrame({
'日收益率': [0.02, 0.01, -0.01] # 3只股票的日收益率
}, index=['600519.SH', '000858.SZ', '600036.SH']) # 将股票代码设为索引
# 场景:波动率数据,也以股票代码为行索引
# 注意:第3只股票是中国平安(601318.SH),与收益率数据不同
volatility = pd.DataFrame({
'年化波动率': [0.25, 0.30, 0.20] # 3只股票的年化波动率
}, index=['600519.SH', '000858.SZ', '601318.SH']) # 股票代码索引
# ==================== 基于索引进行左连接 ====================
# df.join():基于索引合并两个数据框
# 参数说明:
# volatility:要合并的右表
# how='left':左连接,保留左表(returns)的所有索引
# 右表中匹配不到的索引,其列填充为NaN
joined_data = returns.join(volatility, how='left')
# 等价于:pd.merge(returns, volatility, left_index=True, right_index=True, how='left')
# 但join的代码更简洁,专门针对基于索引的合并场景
# ==================== 打印原始数据 ====================
print('收益率数据(以股票代码为索引):')
print(returns)
# 输出解读:3只股票的收益率,索引是股票代码
print('\n波动率数据(以股票代码为索引):')
print(volatility)
# 输出解读:3只股票的波动率,索引也是股票代码
# 注意:招商银行(600036.SH)在收益率中,但不在波动率中
# 中国平安(601318.SH)在波动率中,但不在收益率中
# ==================== 打印基于索引的左连接结果 ====================
print('\n基于索引的左连接结果:')
print(joined_data)
# 输出解读:
# - 贵州茅台(600519.SH):两边都有,完整数据
# - 五粮液(000858.SZ):两边都有,完整数据
# - 招商银行(600036.SH):只在左表,年化波动率列为NaN
# - 中国平安(601318.SH):不在结果中,因为左连接不保留右表独有的索引join vs merge的选用原则:
- 使用join: 数据已经以键值为索引,代码更简洁
- 使用merge: 需要基于列进行连接,或需要更复杂的连接条件
52.6 金融应用多源数据整合案例
52.6.1 场景上市公司多维数据整合
任务:整合股票基本信息、行情数据、财务指标,构建完整的分析数据集。
# =============================================================================
# 题目:金融多源数据整合实战
# =============================================================================
# 本任务演示如何逐步整合多个数据源,构建完整的股票分析数据集
# 场景:整合股票基本信息、日行情数据、季度财务指标
# ==================== 导入必要的库 ====================
import pandas as pd
# ==================== 数据源1:股票基本信息 ====================
# 场景:4只A股的基本信息(代码、名称、上市日期、行业)
# 这是主表,后续合并时以这个表为基础(左连接)
stock_basic = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH'],
'股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安'],
'上市日期': ['2001-08-27', '1998-04-27', '2002-04-09', '2007-03-01'],
'行业': ['食品饮料', '食品饮料', '金融', '金融']
})
# ==================== 数据源2:日行情数据(某日)====================
# 场景:某日的收盘价和涨跌幅数据
# 注意:只有3只股票有行情数据,中国平安缺失
daily_quote = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
'收盘价': [1850.00, 158.50, 32.80], # 当日收盘价(元)
'涨跌幅': [1.5, -0.8, 0.5] # 当日涨跌幅(%)
})
# ==================== 数据源3:财务指标(季频)====================
# 场景:最新季度的财务指标(ROE、负债率)
# 注意:只有3只股票有财务数据,招商银行缺失
financial_metrics = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
'ROE': [25.8, 22.3, 15.6], # 净资产收益率(Return on Equity,%)
'负债率': [18.5, 30.2, 92.5] # 资产负债率(%)
})
# ==================== 步骤1:以基本信息为主表,左连接行情数据 ====================
# pd.merge():基于股票代码合并基本信息和行情数据
# 参数说明:
# stock_basic:左表(主表),包含所有股票的基本信息
# daily_quote:右表,包含当日的行情数据
# on='股票代码':基于股票代码列进行匹配
# how='left':左连接,保留左表(基本信息)的所有股票
# 右表中匹配不到的股票,其行情数据列填充为NaN
# indicator=True:添加一列'_merge',标识每行数据的来源
# - 'both':两边都有
# - 'left_only':只在左表
# - 'right_only':只在右表
step1 = pd.merge(
stock_basic,
daily_quote,
on='股票代码',
how='left', # 左连接,确保所有股票都被保留
indicator=True # 添加_merge列标识数据来源
)
print('步骤1:基本信息 + 行情数据(左连接)')
print(step1)
# 输出解读:中国平安的收盘价和涨跌幅为NaN(因为行情数据中没有这只股票)
# ==================== 步骤2:继续左连接财务指标 ====================
# pd.merge():将步骤1的结果与财务数据继续合并
# 参数说明:
# step1:左表,已经包含基本信息+行情数据
# financial_metrics:右表,包含财务指标
# on='股票代码':继续基于股票代码匹配
# how='left':左连接,保留左表的所有股票
# suffixes=('', '_财务'):处理列名冲突
# - 如果两个数据框有重名列,分别添加后缀区分
# - 这里只是示例,实际没有重名列
final_data = pd.merge(
step1,
financial_metrics,
on='股票代码',
how='left',
suffixes=('', '_财务') # 处理潜在的列名冲突
)
print('\n最终整合结果:')
print(final_data)
# 输出解读:
# - 贵州茅台、五粮液:完整数据(基本信息、行情、财务都有)
# - 招商银行:缺失财务指标(ROE和负债率为NaN)
# - 中国平安:缺失行情数据(收盘价和涨跌幅为NaN)
# ==================== 分析数据完整性 ====================
print('\n数据完整性分析:')
# 选择关键列并重命名,方便阅读
# []:选择列,.rename():重命名列
print(final_data[['股票代码', '股票名称', '_merge']].rename(columns={'_merge': '行情数据'}))
# 输出解读:_merge列显示哪些股票有行情数据('both'),哪些没有('left_only')
print('\n缺失值统计:')
# .isna().sum():统计每列的缺失值数量
print(final_data.isna().sum())
# 输出解读:收盘价、涨跌幅各有1个缺失(中国平安),ROE、负债率各有1个缺失(招商银行)数据整合的关键决策:
- 主表选择:以股票基本信息为主表,使用
left join确保每只股票都保留 - 数据来源追踪:使用
indicator=True标识每条数据是否成功匹配 - 列名冲突:使用
suffixes参数处理重名列 - 缺失值处理:财务指标缺失可能意味着该股票尚未发布财报
52.6.2 性能优化策略
大数据集合并的性能陷阱:
# =============================================================================
# 题目:大数据集合并的性能优化
# =============================================================================
# 本任务演示如何优化大规模数据集的合并性能
# 场景:500万行行情数据与5000行财务数据的合并
# ==================== 导入必要的库 ====================
import pandas as pd
import numpy as np
import time # 用于计时的库
# ==================== 创建大规模测试数据 ====================
n_stocks = 5000 # 股票数量
n_dates = 1000 # 交易日期数量
# 生成股票行情数据(500万行)
# 场景:5000只股票在1000个交易日的收盘价数据
quotes = pd.DataFrame({
# 股票代码列:每只股票的代码重复1000次(对应1000个交易日)
# np.repeat():重复数组,[f'{i:06d}.SH' for i in range(n_stocks)]生成股票代码列表
# 每个代码重复n_dates次
'股票代码': np.repeat([f'{i:06d}.SH' for i in range(n_stocks)], n_dates),
# 日期列:1000个日期重复5000次
# list(pd.date_range(...)) * n_stocks:将日期列表复制5000次
'日期': list(pd.date_range('2020-01-01', periods=n_dates)) * n_stocks,
# 收盘价列:生成500万个10到100之间的随机数
'收盘价': np.random.uniform(10, 100, n_stocks * n_dates)
})
# 生成财务数据(5000行)
# 场景:5000只股票的季度财务指标
financials = pd.DataFrame({
'股票代码': [f'{i:06d}.SH' for i in range(n_stocks)], # 5000只股票的代码
'ROE': np.random.uniform(5, 30, n_stocks), # ROE:5%到30%之间的随机数
'市值': np.random.uniform(50, 5000, n_stocks) # 市值:50亿到5000亿之间的随机数
})
# ==================== 方法1:未优化的合并 ====================
# 场景:直接合并,不进行任何优化处理
print('开始未优化的合并...')
start_time = time.time() # 记录开始时间
# pd.merge():基于股票代码合并500万行行情数据和5000行财务数据
# 默认情况下,Pandas会对合并键进行排序,这在数据量大时很耗时
result_slow = pd.merge(quotes, financials, on='股票代码')
slow_time = time.time() - start_time # 计算耗时
# ==================== 方法2:优化后的合并(设置数据类型)====================
# 优化策略1:将字符串类型的键值转换为category类型
# category类型使用整数编码,比较和匹配的速度更快
quotes_opt = quotes.copy()
# .astype('category'):将股票代码列转换为category类型
# - 对于重复值多的列(如500万行中只有5000个唯一值),效率提升显著
quotes_opt['股票代码'] = quotes_opt['股票代码'].astype('category')
financials_opt = financials.copy()
financials_opt['股票代码'] = financials_opt['股票代码'].astype('category')
print('开始优化后的合并...')
start_time = time.time()
# pd.merge():合并优化后的数据
result_fast = pd.merge(quotes_opt, financials_opt, on='股票代码')
fast_time = time.time() - start_time
# ==================== 性能对比 ====================
print(f'未优化合并时间: {slow_time:.2f}秒')
print(f'优化后合并时间: {fast_time:.2f}秒')
print(f'性能提升: {slow_time/fast_time:.1f}倍')
# 输出解读:优化后的合并通常能提升2-5倍的性能
# 提升幅度取决于数据规模和硬件配置性能优化清单:
✅ 键值类型优化:将字符串键值转换为category类型 ✅ 索引优化:对键值列建立索引(df.set_index()) ✅ 避免重复:合并前检查并删除重复数据 ✅ 分块处理:超大文件考虑分块读取和合并 ✅ 使用Dask:超出内存容量时使用并行计算框架
52.7 高级主题复杂连接条件
52.7.1 多键连接
# =============================================================================
# 题目:基于多个键值进行连接
# =============================================================================
# 本任务演示如何基于多个列(股票代码+日期)进行合并
# 场景:需要精确匹配股票和日期,确保同一股票在同一天的行情和财务数据合并
# ==================== 创建包含日期的行情数据 ====================
# 场景:两只股票在两个交易日的收盘价数据
quotes = pd.DataFrame({
'股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
'日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
'收盘价': [1850.0, 1870.0, 158.5] # 注意:五粮液只有1月1日的数据
})
# ==================== 创建包含日期的财务数据 ====================
# 场景:两只股票在两个交易日的估值指标
financals = pd.DataFrame({
'股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
'日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
'PE': [35.2, 35.8, 25.8] # 市盈率数据
})
# ==================== 基于股票代码和日期两个键进行合并 ====================
# pd.merge():多键连接
# 参数说明:
# on=['股票代码', '日期']:指定多个键值列
# - 只有当两个键都匹配时,行才会被连接
# - 相当于 SQL 的 ON a.股票代码=b.股票代码 AND a.日期=b.日期
# how='inner':内连接,只保留两边都有的行
merged = pd.merge(
quotes,
financals,
on=['股票代码', '日期'], # 多键连接:同时匹配股票代码和日期
how='inner'
)
print('多键连接结果:')
print(merged)
# 输出解读:3行数据都成功匹配
# - 贵州茅台1月1日:股票代码和日期都匹配
# - 贵州茅台1月2日:股票代码和日期都匹配
# - 五粮液1月1日:股票代码和日期都匹配
# 应用场景:确保分析的是同一股票在同一天的完整数据
# 避免错误地将不同日期的数据拼接在一起多键连接的数学含义:
\[ R \bowtie_{k_1, k_2} S = \{ (r, s) \mid r[k_1] = s[k_1] \land r[k_2] = s[k_2] \} \]
只有所有指定的键值都匹配时,两行数据才会被连接。