资讯动态

Python股票大数据分析实战:从数据清洗到分层回测

发布时间:2026/10/15 19:33:49 来源:尧图企业网站定制
简介《基于股票大数据分析Python实战》是一份面向金融数据分析初学者与Python开发者的实战型学习资料目标是从零打通爬虫采集、数据清洗、统计建模到结果展示的完整数据链路。压缩包采用7z格式约4.47MB便于快速下载查阅。目前已有3645人学习。内容围绕真实项目展开一方面讲解借助BeautifulSoup、Scrapy编写爬虫抓取股票历史行情、公告资讯并处理常见反爬策略另一方面展示用Pandas清洗缺失值、异常值并统一格式结合NumPy/SciPy完成统计计算再利用Matplotlib、Seaborn绘制K线、成交量及波动箱型图。机器学习部分以Scikit-learn为主实践线性回归、决策树、随机森林等预测模型并延伸到Tushare/Wind实时数据接口对接以及Flask/Django部署交互分析页面。跟随这套资料能够沉淀一套可复用的股票数据分析思路和代码实现为个人搭建简易行情分析工具提供基础。1. 股票大数据分析为什么卡在Python而不是模型几千只股票、五六年的日线行情连同财务指标和资金流数据随便一凑就是几千万行。想做“基于股票大数据分析Python实战”的人第一反应往往是赶紧上模型、跑机器学习结果真正打开数据一看字段对不上、除权缺口到处都是、停牌的NaN成片出现、成交量单位还不统一——最耗时间的根本不是算法而是数据链路本身。这篇实战笔记就沿着这条路展开怎么用Python把行情数据拉下来、清洗成干净的时间序列、算出能入模的特征再用手写分层回测验证特征到底有没有用。适合已经会写Python、想认真做股票数据分析但还没完整跑通过数据管线的读者也适合刚学会pandas、想拿真实行情数据练手的新手。2. 把全市场行情拉进本地数据源选型与落盘2.1 为什么是Pythonpandas和生态决定了选型做股票数据分析经常会有人纠结要不要换Java、换C。语言本身在这种场景真不是瓶颈真正决定效率的是pandas。DataFrame做日期索引、分组聚合、滚动窗口几乎就是为行情数据设计的。比如算20日均线一条rolling就能完成换成Java光是维护一个按时间排序的价格数组结构就要写半天。Python生态里还有akshare、tushare这类数据接口把行情、财务、资金流、板块数据统一返回成DataFrame省掉了自己写爬虫解析的麻烦。数据接口的选型也很关键。我一般先用免费的开源接口把策略原型跑通因为原型阶段要频繁调整字段数据源便宜、易得比数据质量更重要。等到真要长期跑实盘程序再考虑接入付费数据或券商柜台数据那一步会涉及权限和服务器部署。免费接口的缺点是字段口径和稳定性要自己兜底这一点后面避坑章节会专门讲。整体上Python做股票数据分析是投入产出比最高的选择不需要一上来就在语言层面炫技。2.2 用akshare拉取日线行情的第一个脚本环境准备很简单。装akshare时会把pandas、numpy、requests一起带上来不需要手动单独装。我的建议是在干净环境里一次性装齐避免各自安装时版本冲突。命令如下pip install akshare pandas numpy这样装的好处是避免手动画个依赖图。akshare对pandas版本有要求如果先装了一个最新版pandas再装akshare经常会出现接口内部兼容问题。统一由pip根据依赖关系装踩的坑最少。接入数据接口第一个脚本建议只拉单只股票先把流程跑通再铺开import akshare as ak import pandas as pd # 拉取平安银行的日线行情先用单只股票验证整个链路 df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20200101, end_date20251231, adjustqfq ) # 看一眼字段结构和行数确认数据源正常 print(df.shape) print(df.dtypes) print(df.head()) # 统一成小写列名后面写代码省事 df.columns [c.lower() for c in df.columns] df.to_csv(000001_daily.csv, indexFalse)这段代码里最关键的是adjust参数。qfq是前复权hfq是后复权空字符串则是不复权。对绝大多数走势分析和因子计算我都建议先用前复权把历史价格对齐否则除权除息日会出现价格断层后面算收益率全是错的。period参数控制K线周期日线填daily想拉分钟级可以换成1、5、15等。接口默认返回的列包含日期、开盘、收盘、最高、最低、成交量、成交额、振幅、涨跌幅、涨跌额、换手率字段已经比较接近数据分析需要的口径。2.3 字段口径与落盘增量更新别全量重拉单只股票跑通之后马上要做两件事把全市场股票代码列表拿下来设计落盘和更新策略。代码列表可以用ak.stock_zh_a_spot_em()取全市场实时快照里面带着最新价和股票代码然后循环调用日线接口逐只落盘。这里有一个常见性能误区不要每次都全量拉五年数据。全量拉不仅慢还容易把免费接口的限频打满。我一般把历史数据一次性存成parquet或csv之后每日更新只拉最近5个交易日做增量拼接。下面是一个增量更新示例假设本地已经存过历史数据import akshare as ak import pandas as pd from pathlib import Path def update_daily(symbol: str, data_dir: Path, start: str 20200101) - None: path data_dir / f{symbol}.csv if path.exists(): old pd.read_csv(path, parse_dates[date]) # 从本地最后一条日期开始往后拉留一天重叠用于校验 last_date old[date].max().strftime(%Y%m%d) new ak.stock_zh_a_hist(symbolsymbol, perioddaily, start_datelast_date, end_date20251231, adjustqfq) new.columns [c.lower() for c in new.columns] new[date] pd.to_datetime(new[date]) merged pd.concat([old, new]).drop_duplicates(subset[date]).sort_values(date) merged.to_csv(path, indexFalse) else: # 首次拉取全量历史 df ak.stock_zh_a_hist(symbolsymbol, perioddaily, start_datestart, end_date20251231, adjustqfq) df.columns [c.lower() for c in df.columns] df.to_csv(path, indexFalse)增量更新时往前多留一天重叠是为了覆盖数据源可能修正过的前一日收盘价或成交量。如果只从最后一条日期的下一天开始拉这个修正就永远漏掉了。用drop_duplicates(subset[date])按日期去重即可。落盘格式用csv还是parquet取决于数据量。几千只股票五年日线csv大概几百MBparquet可以压到几十MB读取也更快。我一般存parquet但首版脚本先用csv调试时能直接用文本编辑器打开看。全市场循环拉取时要控制并发常见做法是每只股票之间sleep(0.1)避免触发限频后批量报错。字段口径是跨数据源对比时最容易翻车的点。有的接口返回中文列名有的返回英文列名volume单位可能是手也可能是股amount单位可能是元也可能是千元。我的习惯是进入分析流程前先做一层标准化列名映射统一成下划线英文名并把单位固定下来。字段口径参考下表字段说明常见坑date交易日期需要转成datetime否则排序按字符串排会错open/high/low/close价格注意复权口径不复权时除权日跳变volume成交量可能是手也可能是股要先确认数据源单位amount成交额量级大做因子前建议统一单位pct_chg涨跌幅有的源返回百分数有的返回小数统一口径3. 清洗和特征工程从行情表到因子矩阵3.1 复权处理前复权为什么是默认选择数据拉回来之后第一步不是算指标而是确认价格口径。股票发生分红送转时如果直接用不复权价格除权日当天会出现一根假阴线所有均线、突破信号、回测结果都会被污染。前复权让最新价格保持真实价格把历史价格按分红比例往下调整后复权则保持历史价格不变当前价格被放大。两者都不会改变收益率的相对关系但前复权更贴近实盘因为你委托时看到的股价就是真实价格。前复权计算可以自己完成前复权因子等于除权日前收盘价除以除权参考价把除权日之前的所有价格乘以这个因子并按时间顺序往前逐次累乘。网上有很多代码把因子迭代顺序写反导致早期价格变成负数或突变。我的习惯是任何数据源拿回来都抽一只分红次数多的股票对比几个数据源的前复权收盘价差几分钱可以接受差几个百分点就要查口径。这个动作叫口径校验花不了几分钟但能避免整个分析建立在错误价格上。停牌产生的缺失值也要谨慎处理。A股停牌期间没有行情记录DataFrame里会出现NaN或直接缺行。如果直接dropna会把停牌前后的连续交易日关系弄断导致收益率计算跨越停牌期。常见做法是保留日期索引成交量填0、价格用最近收盘价填充再单独加一列停牌标记import pandas as pd df pd.read_csv(000001_daily.csv, parse_dates[date]) df df.sort_values(date).set_index(date) # 以工作日为频率重建完整时间轴缺失行就是停牌日 full_idx pd.date_range(df.index.min(), df.index.max(), freqB) df df.reindex(full_idx).rename_axis(date) # 先填成交量再向前填充价格顺序不能反 df[is_suspended] df[close].isna().astype(int) df[volume] df[volume].fillna(0) df[close] df[close].ffill() df[open] df[open].ffill() df[high] df[high].ffill() df[low] df[low].ffill() # 停牌当日涨跌幅置0避免跨期收益虚假 df[pct_chg] df[pct_chg].fillna(0.0)这里用freqB生成工作日索引把周末排除掉。先fillna成交量再ffill价格顺序很重要如果先ffill价格再填成交量停牌日的成交量会被后面正常交易日的成交量错误覆盖。pct_chg填0是保守做法表示停牌期间没有交易、收益为0。如果想把停牌当作缺失信息让模型自己处理也可以保留NaN但必须保证训练和预测时用同一套填充逻辑否则线上线下不一致这是典型的变相未来函数。3.2 成交量异动与涨跌停特征真实场景里的数据陷阱特征工程里我最常用的是均线、动量、波动率、成交量异动、涨跌停标记这几类。均线和动量用rolling窗口直接算波动率用收益率滚动标准差成交量异动用当日成交量除以过去20日均量。涨跌停特征要特别注意A股主板涨停是10%创业板和科创板是20%ST是5%。如果直接拿10%去卡全市场创业板涨停全会被漏掉。构造涨跌停标记需要结合股票代码前缀判断板块# 假设df里有一列symbol形如“300750”“600000” def limit_pct(symbol: str) - float: if symbol.startswith((300, 301, 688)): return 0.20 if ST in symbol or symbol.startswith(ST): return 0.05 return 0.10 df[limit_pct] df[symbol].apply(limit_pct) df[is_limit_up] (df[pct_chg] df[limit_pct] - 0.001).astype(int) df[is_limit_down] (df[pct_chg] -df[limit_pct] 0.001).astype(int)阈值判断里减掉0.001是为了抵抗浮点误差。实际数据里涨停对应的涨跌幅经常是10.03%或9.97%硬等精确等于10%会漏掉大量涨停。更稳的做法是拿收盘价等于涨停价判断但多数行情数据没有涨停价字段退而求其次就是在阈值附近留出容差。还要注意股票代码前缀与板块规则随时间变化比如创业板注册制后涨跌幅限制一直是20%但早期是10%做历史回测时要不要按时间段切换规则取决于你回测区间有多长。成交量异动因子也是被单位坑的高发区。有些数据源成交量单位是手有些是股。我的经验是把所有成交量统一成股之后再算异动倍数。检查方法很简单找一只日均成交额上亿的股票看volume乘以前收盘价是否约等于成交额如果量级差100倍几乎可以肯定手和股换算出了问题。这个检查能挡掉大量隐蔽的数据错误。3.3 标签构造未来N日收益和未来函数做完特征下一步是构造标签。最常见的是未来N日收益即t1到tN的累计收益。pandas里用shift实现# 未来5日收益和未来20日收益 df[ret_5d] df[close].shift(-5) / df[close] - 1 df[ret_20d] df[close].shift(-20) / df[close] - 1关键点在于计算标签时t日的特征只能用t日及之前的信息标签必须用t1日之后的信息。很多初学者把当日涨跌幅当作标签又把当日涨跌幅做成特征模型训练时直接看到了答案回测漂亮实盘失效。避免未来函数的办法是严格区分信息边界特征列全部使用截至t日的收盘数据标签列使用t1日之后的未来数据。shift(-5)是把未来第5天的数据拉到今天如果符号写反就是把历史数据拉过来当未来问题非常隐蔽。另一个容易踩的是时间错位全市场数据直接shift没有按股票分组会导致跨股票边界的行被错误配对。正确做法是先按股票代码分组再shiftdf df.sort_values([symbol, date]) df[ret_5d] df.groupby(symbol)[close].shift(-5) / df.groupby(symbol)[close] - 1标签周期与调仓频率必须匹配。做日频策略一般用5天和10天收益做月度调仓用20天和60天。一个按20天调仓的策略如果拿5天收益当标签训练出来的模型和实际持仓周期不匹配效果不会好。另外标签里会有极端值通常是停复牌或涨跌停造成的跳变。我一般会对标签做截面上的去极值处理例如把前后0.5%分位以外的值拉回分位边界避免少数极端样本主导损失函数。4. 可视化验证把因子和价格放到同一张图上4.1 用matplotlib叠加K线、均线和成交量数据清洗和特征计算完成后不要急着建模。先用可视化把样本拉出来看几眼很多数据问题在图上一眼就能看出来。最基本的图是价格、均线、成交量三合一。matplotlib没有内置K线组件常见做法是用mplfinance或者直接用vlines绘制高低区间。我习惯用matplotlib的plot画收盘价和均线用bar画成交量简单可控不引入额外依赖import matplotlib.pyplot as plt import pandas as pd df pd.read_csv(000001_daily.csv, parse_dates[date]) df df.set_index(date).sort_index() df[ma20] df[close].rolling(20).mean() df[ma60] df[close].rolling(60).mean() fig, axes plt.subplots(2, 1, figsize(12, 8), sharexTrue, gridspec_kw{height_ratios: [3, 1]}) ax axes[0] ax.plot(df.index, df[close], labelclose, lw1) ax.plot(df.index, df[ma20], labelma20, lw1) ax.plot(df.index, df[ma60], labelma60, lw1) ax.legend(locbest) ax.set_title(price and ma) ax2 axes[1] ax2.bar(df.index, df[volume], width0.8, colorgray, alpha0.6) ax2.set_ylabel(volume) plt.tight_layout() plt.savefig(price_volume.png, dpi150)两个子图用sharexTrue缩放上方价格图时成交量能同步移动方便对照量价关系。子图高度比例用height_ratios[3, 1]控制价格图占3份成交量占1份。保存图片用savefig而不是show尤其在批量分析时show会把进程卡住每条数据都弹一个窗口。价格跨度大的股票可以考虑ax.set_yscale(log)否则早期低价段的波动在图上几乎是一条直线。初版图不需要美化目标是快速暴露数据问题。4.2 横坐标太密集的三种解法横坐标太密集是股票数据分析里最常见的问题之一。几千个交易日全部显示在x轴上日期标签叠成一片黑色。解决方案有三种按常用程度排序。第一种是设置定位器和格式器只显示少数几个日期标签import matplotlib.dates as mdates ax.xaxis.set_major_locator(mdates.MonthLocator(interval6)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) ax.tick_params(axisx, rotation30)MonthLocator(interval6)表示每6个月显示一个刻度。如果数据跨度5年大约显示10个标签干净很多。interval可以调成3、12取决于数据长度。第二种是直接用索引画图最后手工替换刻度标签适合需要精确控制显示哪些日期的场景x range(len(df)) plt.plot(x, df[close]) step 120 # 每120个交易日标一个刻度 ticks list(range(0, len(df), step)) labels [df.index[i].strftime(%Y-%m-%d) for i in ticks] plt.xticks(ticks, labels, rotation30)第三种是只截取最近一段。分析长周期数据时全量图看不清楚近期趋势截断视图反而更有用df_tail df.tail(250) # 只看最近约一年 df_tail[close].plot()我一般在初版分析里用第一种批量出图时用第二种需要重点观察近期形态时用第三种。注意rotation30几乎总是需要否则即使只有10个标签日期字符串也会互相压住。还有一种情况是x轴数据类型混了一部分是datetime一部分是字符串matplotlib会把它们当成不同类别画成离散点出现这种问题先检查df.index.dtype。4.3 相关性矩阵先删掉一批冗余因子特征越叠越多但很多因子之间高度相关。均线类因子本身就是价格的线性变换ma5、ma10、ma20之间相关性经常超过0.95全塞进模型只会增加过拟合风险。我习惯在做建模之前先跑一个相关性矩阵import seaborn as sns import matplotlib.pyplot as plt feat_cols [ma20, ma60, vol_ratio, ret_5d, volatility_20, is_limit_up] corr df[feat_cols].corr() plt.figure(figsize(8, 6)) sns.heatmap(corr, annotTrue, fmt.2f, cmapcoolwarm, center0, xticklabelsTrue, yticklabelsTrue) plt.tight_layout() plt.savefig(factor_corr.png, dpi150)corr()默认用皮尔逊相关系数适合线性关系。看矩阵时不要只盯对角线重点看两两之间大于0.8或小于-0.8的格子。两个因子相关性过高保留解释性更强的那一个。比如ma5和ma10我一般只留ma10因为ma5噪声更大。相关性矩阵还能暴露数据错误如果收益率和价格因子相关系数异常高说明可能把未来信息混进来了。单看分布看不出这种问题看图反而容易暴露。5. 股票数据实战避坑现象、原因、排查顺序5.1 除权缺口让突破信号全部失真现象用不复权数据画K线某天价格突然从20元跳到12元图上出现一个向下的巨大缺口所有均线跟着急转弯突破类信号被大量误触发。原因股票在那天除权除息每股派息或送股后价格按比例下调账户资产没变但股价数字变了。如果不复权这个缺口会被当成真实的下跌。解决统一使用前复权数据。但要注意前复权数据在每次除权后历史价格都会被重新计算所以增量更新时发现同样的日期价格变了这是正常现象不必惊讶。我一般把前复权历史数据每季度重拉一次全量既保证因子计算用的历史价格连续又不至于频繁打满免费接口。排查顺序是先看除权日附近是否有跳空再确认数据源的复权参数最后拿分红公告里的除权参考价手动验证一天。5.2 停牌和NaN被直接丢弃现象样本量从几万行掉到几千行回测结果异常偏正面策略在停牌多的股票上表现格外好。原因dropna把所有经历过停牌的日期都删掉了留在样本里的几乎全是连续活跃交易的日子天然避开了流动性风险高的时段。这相当于只做了“能交易”的样本忽略了真实持有期里的停牌风险。解决保留日期重建完整时间轴成交量填0价格向前填充单独记录停牌标记。如果模型不能处理停牌标记至少应当把停牌日的标签设置成中性值而不是直接删除。排查时把样本量和原始数据行数做个对比如果drop后行数少了20%以上就要警惕是否误删了大量合理缺失。5.3 未来函数让回测曲线虚高现象回测年化收益高得离谱最大回撤却极小策略在“第二天开盘买入”的假设下几乎不亏。原因特征和标签混用了同一时段的信息。常见的未来函数有三种用当日收盘后才知道的成交额去预测当日收盘价计算shift(-N)时没有按股票分组跨股票错位特征归一化时用了全样本的均值和方差把未来分布信息泄漏给了训练集。解决严格区分t日特征和t1日后的标签。归一化等预处理只能在训练集上fit再transform到测试集不能先在整个DataFrame上算好再切分。排查时抽一只股票打印最近10行demo df[df[symbol] 000001].sort_values(date) print(demo[[date, close, ret_5d]].tail(10))最后5行的ret_5d应该是NaN因为还没有未来数据。如果全字段都有值说明shift方向填错了。这类问题在代码审查里最难发现我的办法是写一个数据边界检查函数预测时只允许传入t日及之前的数据再拿t1日的真实结果验证。5.4 字段漂移接口字段名说变就变现象上周还能跑的脚本这周突然报KeyError或者同一只股票的成交量明显小了一个数量级。原因免费数据源的字段名和单位会随着版本升级变化有时中英文混用有时列顺序调整有时单位从“手”变成“股”。解决所有数据入口统一做一层字段映射代码里只认标准化后的列名。一旦报错第一时间打印df.columns和df.head()对比数据源文档不要直接改下游公式。字段标准化封装示例def normalize_columns(df: pd.DataFrame) - pd.DataFrame: mapping { 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount, } df df.rename(columnsmapping) # 假设源数据成交量单位是手统一转成股 if df[volume].max() 1e6: df[volume] df[volume] * 100 return df这个设计把数据源变化隔离在入口函数内上游源变了只改这一处分析逻辑不做任何改动。排查字段漂移的关键是要有“基准值”意识手动记录一两只股票最近一年的日均成交量脚本跑出来和基准值对不上第一时间怀疑单位或字段映射。5.5 内存翻车全市场数据别用object类型现象脚本跑着跑着内存占用升到十几个GB电脑直接卡死甚至被系统杀掉进程。原因pandas默认把字符串列存成object类型几千只股票几千万行数据object列的开销远高于数值列。股票代码这种低基数列用object存是巨大的浪费。解决读数据时直接指定dtype代码列存category数值列尽量用float32dtype_spec { symbol: category, open: float32, high: float32, low: float32, close: float32, volume: float32, } df pd.read_csv(all_daily.csv, dtypedtype_spec, parse_dates[date])这几行改动通常能把内存占用降到三分之一以下。还有一个实际经验做全市场截面分析时不要一次读进全部历史按月分批读取算完因子就释放内存中间结果用parquet缓存。判断内存瓶颈的方法很简单跑之前看任务管理器跑完再看峰值。如果峰值翻了几倍优先查有没有新增列的dtype变成了object。6. 进阶用手写分层回测验证特征是否真的有用6.1 分层回测的思路特征构造完之后有人直接上机器学习模型。我不建议一上来就这么做先用分层回测检验单个因子的有效性成本低、解释性强。思路很朴素每个调仓日按因子值把股票分成5组第1组是因子值最低的第5组是因子值最高的然后统计每组在未来N日的平均收益。如果第5组和第1组的收益差单调且稳定这个因子就有信息量如果分组收益完全随机说明这个因子基本是噪声。这正是“股票大数据分析”里最值得先做的验证步骤它能让后续建模少走很多弯路。6.2 简化实现与结果解读下面给一个最小可运行的实现。这里用日频数据每20个交易日调仓一次未来收益窗口也是20天。import pandas as pd def layered_backtest(df, factor_col, n_groups5, hold20): df df.sort_values([symbol, date]).copy() # 未来20日收益作为标签 df[future_ret] df.groupby(symbol)[close].transform( lambda s: s.shift(-hold) / s - 1 ) results [] # 每隔hold天取一个调仓日 for date, group in df.groupby(date): if len(group) n_groups * 10: continue group group.dropna(subset[factor_col, future_ret]) try: group[group] pd.qcut(group[factor_col], n_groups, labelsFalse, duplicatesdrop) except ValueError: continue stats group.groupby(group)[future_ret].mean() results.append(stats) res_df pd.DataFrame(results) return pd.DataFrame({ group_mean: res_df.mean(), group_std: res_df.std(), group_positive_ratio: (res_df 0).mean() })逐行说明df.groupby(date)得到每一天的全市场截面。len(group) n_groups * 10防止某天可交易股票太少导致分组不稳定。pd.qcut是等频分箱把因子值分成5组duplicatesdrop处理因子值大量重复的情况。最后统计每组未来收益的均值、标准差和胜率。这里没有做市值中性化也没有剔除ST和停牌对初版结论只能看个大概方向不能直接指导实盘。结果怎么看先看group_mean是否单调。如果第5组收益0.06、第1组收益-0.02多空差8个点信号方向明确。但如果最高收益在第3组、第5组反而回落说明因子存在非线性简单分组不一定能抓住。再看group_std标准差大说明因子表现在不同时期波动剧烈时序上不稳定。最后看group_positive_ratio接近0.5说明靠天吃饭0.6以上才有一定区分度。实操中这个分层回测会暴露很多问题。某个因子第5组收益高但group_std也高细看发现只有某一年贡献了全部收益这种因子过拟合风险很大。还有一种情况是因子在涨市里有效、跌市里失效按年份拆分看会更清楚。我的习惯是分层回测跑完顺手把每个调仓日的多空收益画成累计曲线如果曲线是逐年稳步向上的线性形状才值得进入下一步建模如果是最后一年突然拉出来的翘尾那基本是数据错误或者过拟合别急着用。这些年我看过太多“完美因子”最后大多死在复盘时的未来函数上。现在每写完一个因子我都会强迫自己先跑一次分层回测再决定要不要继续。数据驱动的活慢就是快希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑