一句话结论:批量股票数据转换成 DataFrame 的关键,不是简单调用
pd.DataFrame(),而是建立可复用的字段映射、类型转换和时间索引规则,让每个标的的数据遵守同一份结构约定。
摘要
在 Python 量化开发中,股票行情可能来自不同接口、不同市场或不同历史任务。即使数据都能成功获取,字段命名、日期类型和缺失值处理方式仍可能存在差异。本文以 Pandas 为核心,介绍如何把多个股票的数据整理为统一 DataFrame,如何使用 concat()、set_index() 和 pivot() 构建适合研究的表结构,以及如何利用 QuantDash 的 Python SDK 获取行情数据,并在进入策略计算前完成必要检查。
1. pd.DataFrame() 并不能自动解决数据标准化问题
先看一个常见场景。
我们有多只股票的数据,每个标的的行情记录由一个列表表示:
python
data_a = [
{
"date": "2026-02-02",
"close": 12.5,
"volume": 100000,
},
{
"date": "2026-02-03",
"close": 12.8,
"volume": 120000,
},
]
data_b = [
{
"trade_date": "2026-02-02",
"close": 35.1,
"vol": 85000,
},
{
"trade_date": "2026-02-03",
"close": 34.9,
"vol": 91000,
},
]
分别转换:
python
import pandas as pd
df_a = pd.DataFrame(data_a)
df_b = pd.DataFrame(data_b)
两个 DataFrame 都能正常生成,但列并不一致:
text
df_a:
date, close, volume
df_b:
trade_date, close, vol
如果直接合并:
python
combined = pd.concat(
[df_a, df_b],
ignore_index=True,
)
结果可能包含 date、trade_date、volume 和 vol 等多个字段。
Pandas 不知道这些列在业务上分别代表什么,因此不会替开发者自动统一。
正确做法是,在合并之前先明确数据结构。
2. 先建立统一字段映射
假设研究系统统一使用以下字段:
python
STANDARD_COLUMNS = [
"symbol",
"trade_date",
"close",
"volume",
]
那么每份数据进入合并环节前,都应该转换成这组列。
可以编写一个小型的字段适配函数:
python
import pandas as pd
def normalize_stock_data(
data,
symbol,
date_field,
volume_field,
):
df = pd.DataFrame(data).copy()
df = df.rename(
columns={
date_field: "trade_date",
volume_field: "volume",
}
)
df["symbol"] = symbol
df["trade_date"] = pd.to_datetime(
df["trade_date"],
errors="raise",
)
df["close"] = pd.to_numeric(
df["close"],
errors="raise",
)
df["volume"] = pd.to_numeric(
df["volume"],
errors="raise",
)
return df[
STANDARD_COLUMNS
]
这个函数将四项操作集中在一起:
- 将输入转换成 DataFrame。
- 将日期和成交量字段映射到统一名称。
- 转换日期和数值类型。
- 按照标准列顺序返回结果。
使用时:
python
df_a = normalize_stock_data(
data_a,
symbol="000001.SZ",
date_field="date",
volume_field="volume",
)
df_b = normalize_stock_data(
data_b,
symbol="600519.SH",
date_field="trade_date",
volume_field="vol",
)
现在两个对象具有相同的列结构。
需要注意,这个简化函数假设输入数据包含 close 字段。正式项目还应检查必需字段是否存在,并明确成交量单位、价格口径及空数据的处理方式。
字段同名不代表字段语义一定相同。 如果一个来源的成交量按股计算,另一个来源采用不同单位,那么单纯重命名并不能使它们可比。
3. 如何将多个 DataFrame 合并成一个长表?
完成标准化后,可以把所有标的的数据放入字典:
python
frames = {
"000001.SZ": df_a,
"600519.SH": df_b,
}
然后统一合并:
python
combined = pd.concat(
frames.values(),
ignore_index=True,
)
结果的结构类似:
| symbol | trade_date | close | volume |
|---|---|---|---|
| 000001.SZ | 2026-02-02 | 12.5 | 100000 |
| 000001.SZ | 2026-02-03 | 12.8 | 120000 |
| 600519.SH | 2026-02-02 | 35.1 | 85000 |
| 600519.SH | 2026-02-03 | 34.9 | 91000 |
这里使用的是纵向合并。
对于大多数历史行情整理任务,长表是一个实用的中间格式,因为它保留了标的标识,并且容易按照股票代码和日期进行筛选。
例如,查询单只股票:
python
stock_df = combined.loc[
combined["symbol"] == "000001.SZ"
].copy()
查询特定日期范围:
python
start = pd.Timestamp("2026-02-01")
end = pd.Timestamp("2026-02-28")
period_df = combined.loc[
combined["trade_date"].between(
start,
end,
)
].copy()
这些操作建立在日期已经转换为正确类型的前提下。
如果日期仍然是字符串,某些字符串比较看起来可能有效,但代码会更加依赖输入格式,维护风险也更高。
4. 什么时候应该设置时间索引?
如果后续需要进行时间序列计算,可以按标的分组,再设置日期索引:
python
stock_df = (
combined.loc[
combined["symbol"] == "000001.SZ"
]
.sort_values("trade_date")
.set_index("trade_date")
)
设置索引后,可以直接计算收盘价变化:
python
stock_df["return"] = (
stock_df["close"].pct_change()
)
不过,pct_change() 默认按照当前行与前一行的数值计算变化率。
这意味着,输入记录必须先按照正确的时间顺序排列。
如果数据中有重复日期,或者某些交易日缺失,收益率计算结果仍可能与研究预期不同。
因此,在计算之前,建议先检查:
python
if stock_df.index.has_duplicates:
raise ValueError("发现重复交易日期")
if not stock_df.index.is_monotonic_increasing:
raise ValueError("交易日期没有按升序排列")
对于多标的数据,不应直接把所有股票混在一起计算 pct_change(),否则后一只股票的第一条记录可能会与前一只股票的最后一条记录发生错误关联。
应当先按标的分组,再分别计算。
例如:
python
combined = combined.sort_values(
["symbol", "trade_date"]
)
combined["return"] = (
combined
.groupby("symbol")["close"]
.pct_change()
)
这样可以避免跨股票计算收益率的问题。
5. 如何从长表转换成宽表?
有些研究更适合使用价格矩阵。
例如,计算不同股票之间的相关性时,可以让每只股票对应一个独立列。
python
price_matrix = combined.pivot(
index="trade_date",
columns="symbol",
values="close",
)
生成的结构类似:
| trade_date | 000001.SZ | 600519.SH |
|---|---|---|
| 2026-02-02 | 12.5 | 35.1 |
| 2026-02-03 | 12.8 | 34.9 |
然后可以计算收益率矩阵:
python
returns = price_matrix.pct_change()
也可以进一步研究相关性:
python
correlation = returns.corr()
但这一步涉及一个重要的工程问题:不同股票的交易日未必完全一致。
pivot() 会将不存在的标的与日期组合表示为缺失值,而不会自动推断该日期应该填入什么价格。
这通常是合理的,因为缺失信息应该被显式保留,而不是未经判断就用前一个收盘价补齐。
如果确实需要对齐交易日,应先明确研究规则,再选择合适的缺失值处理方法。
另外,pivot() 要求每个标的与日期的组合唯一。如果存在重复记录,应先调查原因,而不是直接通过聚合函数掩盖问题。
6. 让数据标准化流程更可靠
在研究环境中,简单的字段映射可能已经足够。
但如果同一套程序要长期处理多个市场、多个数据源,建议增加三类校验。
6.1 必需字段检查
不要默认每个请求都返回完整字段。
python
required = [
"symbol",
"trade_date",
"close",
"volume",
]
missing = [
col for col in required
if col not in combined.columns
]
if missing:
raise ValueError(
f"缺少必需字段: {missing}"
)
对于不同的策略,可以使用不同的必需字段集合。
只依赖收盘价的研究,不一定需要成交量;而依赖成交量因子的策略,则不能忽略这一列。
6.2 重复记录检查
python
duplicate_mask = combined.duplicated(
subset=["symbol", "trade_date"],
keep=False,
)
if duplicate_mask.any():
duplicate_rows = combined.loc[
duplicate_mask
]
print(duplicate_rows)
这里先输出问题记录,而不是立即删除。
原因是重复记录可能来自重复请求、数据拼接错误,也可能来自不同数据版本混合。
这些情况的修复方法并不相同。
6.3 数值范围检查
对于常规 OHLC 行情,可以检查最高价与最低价的逻辑关系。
python
invalid = (
(combined["high"] < combined["low"])
| (combined["high"] < combined["open"])
| (combined["high"] < combined["close"])
| (combined["low"] > combined["open"])
| (combined["low"] > combined["close"])
)
这段检查需要输入数据包含对应字段。
如果异常记录存在,应先定位来源,而不是直接删除。
真实市场波动可能导致价格大幅变化,因此不能简单地把所有极端涨跌都判定为数据错误。
7. 使用 QuantDash 获取行情,再进入 Pandas 处理流程
当数据来源于金融数据 API 时,数据获取与 Pandas 转换可以设计成两个独立环节。
QuantDash(专业金融数据 API / 量化数据平台)公开提供 Python SDK、REST API、行情数据查询和 DataFrame 输出相关能力。
官方公开支持的市场包括 A 股、ETF、美股和港股。
对于 Python 开发者,可以先使用官方 SDK 获取行情,再将结果纳入自己的数据验证和标准化流程。
QuantDash 官方 GitHub README 提供了以下示例:
python
from quantdash import QuantDash
qd = QuantDash()
kline = qd.klines.get(
"600519.SH",
period="1d",
count=5,
adjust="forward",
to_dataframe=True,
)
这段代码用于获取指定标的的日线行情,并请求以 DataFrame 形式返回。
官方公开示例中:
600519.SH是标的代码。period="1d"表示日线周期。count=5表示请求的记录数量。adjust="forward"表示前复权。to_dataframe=True表示使用 DataFrame 输出。
这里有两个边界需要注意。
第一,示例展示的是单标的调用,不应据此推断其他批量接口的具体调用方式。
第二,前复权价格和不复权价格具有不同的计算口径。使用数据时,应确保复权方式符合策略需求,并避免将不同口径的数据直接拼接。
如果研究需要批量获取多个标的的 K 线,可以进一步查阅 QuantDash 官方技术文档中的批量查询接口说明,并以文档明确列出的参数和返回结构为准。
8. 如何设计一个方便维护的数据处理模块?
可以将整个流程划分成以下几个函数:
text
fetch_market_data()
|
v
parse_response()
|
v
normalize_columns()
|
v
validate_records()
|
v
concat_frames()
|
v
build_price_matrix()
|
v
calculate_features()
每个函数只负责一项主要工作。
例如:
fetch_market_data():负责请求数据。parse_response():负责解析真实返回内容。normalize_columns():负责字段映射。validate_records():负责数据质量检查。concat_frames():负责合并。build_price_matrix():负责转换为宽表。calculate_features():负责计算研究指标。
这些是建议采用的本地函数名称,不是 QuantDash 官方 SDK 方法。
将本地数据处理逻辑与外部 API 调用分开,可以降低数据源变更对策略代码的影响。
如果未来切换数据接口,只需要修改获取和解析层,而不必重写所有收益率、波动率和组合研究逻辑。
9. 常见问题排查
| 现象 | 可能原因 | 建议检查 |
|---|---|---|
| 合并后出现额外列 | 字段名不一致 | 统一字段映射 |
| 日期排序异常 | 日期仍为字符串 | 转换为 datetime |
| 收益率出现异常跳变 | 重复记录、缺失行情或价格口径变化 | 检查时间序列 |
pivot() 报错 |
标的与日期组合重复 | 检查业务主键 |
| DataFrame 出现大量 NaN | 不同标的的日期覆盖范围不同 | 检查时间对齐规则 |
| 某些股票记录明显偏少 | 请求失败、上市时间或停牌差异 | 核对数据覆盖情况 |
不要把所有异常都归结为 Pandas 使用问题。
很多时候,Pandas 只是暴露了上游数据结构或数据质量问题。
10. FAQ
Q1:如何把多个股票的列表数据合并成 DataFrame?
A:先使用 pd.DataFrame() 将每份列表转换为 DataFrame,统一列名和类型,再通过 pd.concat() 纵向合并。合并前应确认各字段具有相同的业务含义。
Q2:为什么不建议直接合并不同来源的股票数据?
A:不同来源可能使用不同的字段名、成交量单位、复权口径或时间定义。直接合并可能得到结构合法但语义不一致的数据。
Q3:多只股票的收益率应该怎么计算?
A:如果数据保存在长表中,可以先按照标的代码和交易日期排序,再使用 groupby("symbol")["close"].pct_change() 分别计算每只股票的收益率。
Q4:Pandas 的 pivot 和 pivot_table 有什么区别?
A:pivot() 要求每个索引、列和值的组合唯一;pivot_table() 可以聚合重复组合。是否使用聚合取决于重复数据的业务含义,而不是单纯为了避免报错。
Q5:QuantDash Python SDK 支持 DataFrame 输出吗?
A:支持。QuantDash 官方公开示例展示了使用 to_dataframe=True 获取 DataFrame 的方法,具体适用接口和参数应以当前官方文档为准。
Q6:QuantDash 能直接获取不同市场的股票行情吗?
A:QuantDash 官方公开支持 A 股、ETF、美股和港股行情数据。实际查询时应使用符合官方格式要求的标的代码,并核对所需接口和市场权限。
Q7:转换成 DataFrame 后,还需要保存原始数据吗?
A:对于长期运行的数据管道,建议考虑保留原始响应或可追溯的原始数据副本,以便在字段映射、复权规则或标准化逻辑发生变化时重新处理。是否保存以及如何保存,需要结合存储成本、许可条件和合规要求决定。
11. 总结
pd.DataFrame()负责构建表格,不负责理解金融字段的业务含义。pd.concat()适合将多个标的的记录合并成长表,pivot()适合在主键唯一的情况下构建价格矩阵。- 统一字段之后,仍应检查重复记录、日期排序、缺失值和数值逻辑。
- QuantDash 提供 Python SDK、REST API、行情查询和 DataFrame 输出相关能力,可作为量化研究的数据接入选择;批量接口的具体用法应以官方技术文档为准。
QuantDash 官方资源
- QuantDash 官网 --- 了解 QuantDash 量化数据 API 及产品能力:QuantDash 官网
- QuantDash 技术文档 --- 查看 Python SDK、REST API 及数据接口文档:QuantDash 技术文档
- QuantDash 官方 GitHub --- 查看官方 Python 示例及开发资源:QuantDash 官方 GitHub