Python股价数据分析实战:从数据清洗到可视化完整指南

📅 发布时间:2026/10/13 16:20:08
Python股价数据分析实战:从数据清洗到可视化完整指南
1. 项目缘起与整体设计思路1.1 为什么选股价数据作为数据分析练手项目做数据分析练手选数据集这件事本身就挺讲究的。我见过太多人一上来就拿经典的鸢尾花数据集或者泰坦尼克号幸存者数据跑完一遍分类模型就觉得自己会数据分析。说实话这类数据集太干净了干净到跟真实业务场景脱节。你拿它练手练的是API调用不是分析思维。股价数据就不一样。它有几个特别适合练手的特性第一数据获取门槛低很多公开渠道都能拿到历史行情数据不需要申请什么特殊权限第二数据本身有明确的业务含义开盘价、收盘价、最高价、最低价、成交量每一个字段都能对应到真实的交易行为第三数据质量足够“脏”有停牌导致的缺失、有除权除息造成的价格跳变、有极端行情下的异常值这些在真实工作中都会遇到。我这次拿一家知名科技公司后文统一用“A公司”代称的股价数据来做完整分析时间跨度覆盖了多个完整财年。选它不是因为有什么内幕消息纯粹是因为它的数据记录完整、交易活跃、市场关注度高作为教学案例非常合适。1.2 分析框架的搭建逻辑一个完整的股价数据分析项目我把它拆成四个层次描述性分析先搞清楚数据长什么样价格在什么区间波动成交量是什么量级趋势分析识别价格的长期走向和中期波动用移动平均线来平滑噪声关联性分析看价格和成交量之间有没有联动关系不同时间窗口的收益率有没有自相关性可视化呈现把上面三步的结论用图表讲清楚让别人一眼就能看懂这个框架不是拍脑袋定的。描述性分析解决“是什么”的问题趋势分析解决“往哪走”的问题关联性分析解决“为什么”的问题可视化解决“怎么讲”的问题。四步走下来一个分析项目的逻辑链条就完整了。注意很多人做数据分析喜欢直接跳到建模预测跳过描述性分析这一步。我的经验是这一步绝对不能省。你对数据的分布、量级、缺失情况没有直观感受后面建出来的模型大概率是空中楼阁。1.3 工具选型与版本说明整个项目用Python生态里最成熟的几个库来完成工具用途选它的理由pandas数据清洗与处理时间序列处理能力最强resample和rolling方法开箱即用numpy数值计算底层数组运算效率高收益率计算离不开它matplotlib基础绘图灵活度最高适合定制化图表seaborn统计可视化默认样式好看画分布图和热力图省事版本方面我用的pandas是2.x版本numpy是1.26以上。这里提醒一句pandas 2.x在数据类型和性能上有不少改进特别是对Arrow后端的支持处理大文件时速度提升明显。如果你还在用1.x版本建议升级但要注意append方法已经被移除了得换成concat。2. 数据获取与清洗的核心细节2.1 数据字段解读与获取途径一份标准的日线行情数据通常包含以下字段Date交易日期注意是交易日不是自然日Open当日开盘价High当日最高价Low当日最低价Close当日收盘价Adj Close复权收盘价这个字段最容易被忽略但最重要Volume当日成交量这里重点说Adj Close。股票在分红、拆股之后价格会出现跳变。比如某天收盘100块第二天拆股变成50块如果你直接用Close字段算收益率会得到一个-50%的荒谬结果。Adj Close就是把这些公司行为的影响剔除了让价格序列在时间上可比。做收益率计算和趋势分析一定要用Adj Close。获取途径上公开渠道能拿到历史数据的方式不少但各有各的限制。有的接口有频率限制有的只提供最近几年的数据有的需要注册账号。我建议在练手阶段先把数据下载成CSV文件存在本地这样每次跑代码不用重新请求调试效率高很多。2.2 数据加载与初步体检拿到CSV文件之后第一步不是急着画图而是做数据体检。我通常按这个顺序来import pandas as pd import numpy as np # 加载数据把日期列直接解析成datetime类型 df pd.read_csv(stock_data.csv, parse_dates[Date], index_colDate) # 看前五行确认字段和格式 print(df.head()) # 看数据类型和缺失情况 print(df.info()) # 看描述性统计快速了解数值分布 print(df.describe())parse_dates这个参数很关键。如果不加Date列就是字符串类型后面做时间序列重采样的时候会报错。index_colDate把日期设为索引这样就能直接用df[2023]这种方式切片非常方便。df.info()会告诉你每个字段有多少非空值。如果发现某个字段的计数明显少于总行数说明有缺失。df.describe()给出均值、标准差、四分位数这些统计量能帮你快速判断有没有异常值。比如成交量如果最大值和75分位数差了好几个数量级那大概率有极端交易日。2.3 缺失值与异常值处理股价数据的缺失通常有两种情况一种是停牌导致的整行缺失另一种是某个字段单独缺失。处理方式不一样。停牌导致的缺失我一般用前向填充forward fill因为停牌期间价格没有变化用停牌前最后一个交易日的价格填充是合理的df.fillna(methodffill, inplaceTrue)但要注意如果数据开头就有缺失前向填充填不了这时候得用后向填充兜底。另外pandas 2.x里fillna(methodffill)的写法已经过时了推荐用df.ffill()。异常值方面股价数据里最常见的是涨跌停和乌龙指。涨跌停是真实交易行为不能删乌龙指是错误数据得处理。判断方法是用收益率的标准差超过5倍标准差的点标记出来人工核查。实操心得我一般会保留一个原始数据的副本所有清洗操作在副本上进行。这样万一清洗逻辑有问题还能回溯到原始状态重新来。这个习惯帮我省过好几次事。3. 核心分析环节的完整实现3.1 描述性统计与价格分布分析数据清洗完之后先做描述性分析。这一步的目标是回答几个问题价格中枢在哪波动范围多大分布是对称的还是偏斜的。# 计算关键统计量 stats df[Adj Close].describe() print(stats) # 计算偏度和峰度 skewness df[Adj Close].skew() kurtosis df[Adj Close].kurtosis() print(f偏度: {skewness:.3f}, 峰度: {kurtosis:.3f})偏度衡量分布的对称性。如果偏度为正说明分布右偏也就是有少数极高的价格拉高了均值。峰度衡量分布的尖峭程度正态分布的峰度是0pandas默认减去3如果峰度为正说明极端值比正态分布更多。股价的Adj Close通常是右偏的因为价格下跌有下限最多跌到0上涨没有上限。这个特性对后续建模有影响比如用线性回归的时候可能需要对价格取对数。除了整体统计我还习惯按年份分组看统计量yearly_stats df[Adj Close].resample(Y).agg([mean, std, min, max]) print(yearly_stats)resample(Y)按年重采样agg传入多个聚合函数。这样一眼就能看出哪些年份波动大哪些年份相对平稳。3.2 移动平均线与趋势识别移动平均线是趋势分析里最基础也最实用的工具。它的逻辑很简单把最近N天的价格取平均消掉短期波动留下长期趋势。# 计算不同窗口的移动平均线 df[MA20] df[Adj Close].rolling(window20).mean() df[MA60] df[Adj Close].rolling(window60).mean() df[MA120] df[Adj Close].rolling(window120).mean()窗口选20、60、120是有讲究的。20个交易日大约是一个月60个交易日是一个季度120个交易日是半年。这三个窗口分别对应短期、中期、长期趋势。移动平均线有个特性叫滞后性。因为它是用历史数据算的所以趋势反转的时候均线会晚于价格变化。窗口越大滞后越明显。这不是缺陷是特性。做趋势跟踪的时候滞后反而能过滤掉假信号。判断趋势方向我常用两个规则均线排列短期均线在长期均线之上且都在向上走说明上升趋势明确价格与均线的关系价格持续在均线之上运行说明买盘强劲# 判断金叉和死叉 df[Signal] 0 df.loc[df[MA20] df[MA60], Signal] 1 df.loc[df[MA20] df[MA60], Signal] -1 # 找出信号变化的点 df[Signal_Change] df[Signal].diff() cross_points df[df[Signal_Change] ! 0] print(cross_points[[Adj Close, MA20, MA60, Signal]])金叉短期均线上穿长期均线通常被视为买入信号死叉反之。但我要泼一盆冷水单纯靠均线交叉做交易决策胜率并不高。均线交叉在震荡市里会频繁发出假信号来回打脸。它更适合作为趋势确认的辅助工具而不是唯一的决策依据。3.3 日收益率计算与波动率分析收益率是股价分析里最核心的衍生指标。有两种算法简单收益率(P_t - P_{t-1}) / P_{t-1}对数收益率ln(P_t / P_{t-1})我一般用对数收益率因为它有两个好处第一多期收益率可以直接相加第二对数收益率更接近正态分布方便做统计推断。# 计算对数收益率 df[Log_Return] np.log(df[Adj Close] / df[Adj Close].shift(1)) # 年化波动率假设一年252个交易日 annual_volatility df[Log_Return].std() * np.sqrt(252) print(f年化波动率: {annual_volatility:.2%})年化波动率的计算逻辑是日波动率乘以根号252。为什么是根号252而不是252因为波动率是标准差标准差不可加但方差可加。日方差乘以252得到年方差再开根号就是年标准差。这个指标的含义是如果收益率服从正态分布那么大约68%的情况下一年的收益率会落在正负一个年化波动率的范围内。比如年化波动率是30%那一年赚30%或亏30%都是正常波动。我还习惯算滚动波动率看波动率随时间的变化# 20日滚动波动率 df[Rolling_Vol] df[Log_Return].rolling(window20).std() * np.sqrt(252)滚动波动率能帮你识别市场情绪的变化。波动率突然放大往往意味着有重大事件发生或者市场进入恐慌阶段。3.4 成交量与价格的关联分析成交量是价格的“燃料”。价格上涨如果伴随成交量放大说明上涨有资金支撑持续性更强如果价格上涨但成交量萎缩说明追高意愿不足可能是强弩之末。# 计算价量相关系数 correlation df[Adj Close].corr(df[Volume]) print(f价格与成交量的相关系数: {correlation:.3f}) # 按价格涨跌分组看成交量 df[Price_Change] df[Adj Close].pct_change() df[Direction] np.where(df[Price_Change] 0, Up, Down) volume_by_direction df.groupby(Direction)[Volume].mean() print(volume_by_direction)相关系数只能看线性关系而且股价和成交量的关系往往是非线性的。所以我还会画散点图用seaborn的regplot加上拟合线直观感受一下。另外我会构造一个量价背离的指标价格创20日新高但成交量没有创20日新高这种情况标记出来重点关注。df[Price_High_20] df[Adj Close].rolling(20).max() df[Volume_High_20] df[Volume].rolling(20).max() df[Divergence] (df[Adj Close] df[Price_High_20]) (df[Volume] df[Volume_High_20]) print(f量价背离天数: {df[Divergence].sum()})3.5 可视化图表的定制与优化图表是分析结论的载体。一张好的图表不用看文字说明就能传递核心信息。我通常会把下面几张图作为标配第一张价格走势与均线叠加图import matplotlib.pyplot as plt import matplotlib.dates as mdates fig, ax plt.subplots(figsize(14, 7)) ax.plot(df.index, df[Adj Close], label收盘价, linewidth1, alpha0.7) ax.plot(df.index, df[MA20], labelMA20, linewidth1.5) ax.plot(df.index, df[MA60], labelMA60, linewidth1.5) ax.plot(df.index, df[MA120], labelMA120, linewidth1.5) ax.set_title(A公司股价走势与移动平均线, fontsize14) ax.set_xlabel(日期) ax.set_ylabel(价格) ax.legend() ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) plt.tight_layout() plt.show()第二张收益率分布直方图fig, ax plt.subplots(figsize(10, 6)) ax.hist(df[Log_Return].dropna(), bins100, edgecolorblack, alpha0.7) ax.axvline(df[Log_Return].mean(), colorred, linestyle--, label均值) ax.set_title(日对数收益率分布, fontsize14) ax.set_xlabel(对数收益率) ax.set_ylabel(频次) ax.legend() plt.tight_layout() plt.show()第三张成交量柱状图fig, ax plt.subplots(figsize(14, 5)) ax.bar(df.index, df[Volume], width1, alpha0.6) ax.set_title(日成交量, fontsize14) ax.set_xlabel(日期) ax.set_ylabel(成交量) plt.tight_layout() plt.show()画图有几个细节要注意figsize控制图片大小14x7适合宽屏展示alpha控制透明度叠加多条线的时候很有用tight_layout自动调整子图间距避免标签被截断。避坑提醒matplotlib默认不支持中文显示会显示成方框。需要在代码开头加两行配置plt.rcParams[font.sans-serif] [SimHei]和plt.rcParams[axes.unicode_minus] False。前者设置中文字体后者解决负号显示问题。4. 常见问题排查与实操避坑指南4.1 数据层面的典型问题问题一日期解析失败报错信息通常是ValueError: time data xxx does not match format。原因一般是日期格式不统一比如有的行是2023-01-01有的是01/01/2023。解决办法是用pd.to_datetime的format参数指定格式或者用errorscoerce把解析失败的变成NaT再处理。问题二复权价格与原始价格混用这个坑很隐蔽。如果你用Close算收益率用Adj Close画价格图两边的数值对不上分析结论就会自相矛盾。我的做法是统一用Adj Close除非有特殊需求要看真实交易价格。问题三时区问题如果数据来源涉及不同时区日期索引可能会错位。建议统一转换成UTC或者交易所所在时区再做后续分析。4.2 计算层面的典型问题问题四滚动窗口计算出现NaNrolling(window20).mean()的前19个值是NaN这是正常的因为数据不够。但如果NaN出现在中间说明原始数据有缺失。处理方式是先填充缺失值再做滚动计算。问题五收益率计算的首日NaNpct_change()和shift(1)都会让第一个值变成NaN因为第一天没有前一天的数据。这个NaN不用管后续分析自动忽略即可。问题六年化系数用错日收益率年化用252周收益率年化用52月收益率年化用12。用错系数会导致波动率高估或低估。252是交易日数量不是自然日数量这个要记清楚。4.3 可视化层面的典型问题问题七中文显示为方框前面提过了加字体配置就行。如果SimHei不可用可以试试Microsoft YaHei或者PingFang SC。问题八日期标签重叠数据点多的时候x轴的日期标签会挤在一起。解决办法是用mdates.AutoDateLocator()自动调整标签密度或者用plt.xticks(rotation45)旋转标签。问题九图表保存后分辨率不够plt.savefig的时候加上dpi300参数输出高清图。如果要在网页上展示bbox_inchestight能去掉多余的白边。4.4 分析层面的常见误区误区一把相关性当因果性价格和成交量有相关性但不能说成交量放大导致了价格上涨。可能是某个消息同时影响了两者。分析的时候要谨慎表述。误区二过度拟合历史数据用复杂模型去拟合历史价格回测收益很漂亮实盘一塌糊涂。股价数据信噪比很低简单模型往往比复杂模型更稳健。误区三忽略交易成本回测的时候如果不考虑手续费和滑点高频策略的收益会被严重高估。做任何策略评估都要把交易成本算进去。误区四用未来数据做决策这个错误在写代码的时候很容易犯。比如用当天的最高价决定是否买入但实际交易中你不可能提前知道最高价。所有信号必须基于当前时刻之前的数据。下面这张表是我整理的问题速查表遇到问题可以快速定位问题现象可能原因排查方法解决方案日期解析报错格式不统一打印前几行看格式指定format或coerce收益率异常大用了未复权价格检查除权日附近数据改用Adj Close滚动计算中间有NaN原始数据缺失df.isnull().sum()先填充再滚动中文显示方框字体未配置看报错信息设置rcParams图表标签重叠数据点太密观察x轴旋转标签或自动定位相关系数很低关系是非线性的画散点图用斯皮尔曼相关系数5. 分析结论的呈现与后续扩展方向5.1 如何把分析结果讲成一个完整的故事数据分析的最终产出不是代码是结论。代码只是手段结论才是目的。我见过不少人跑了一堆图但问他“所以呢”答不上来。这就是缺少结论提炼的环节。我的做法是每张图下面写三句话第一句说图里看到了什么第二句说这意味着什么第三句说所以应该关注什么。比如价格走势图第一句“过去三年价格整体上行但中间有两次超过20%的回撤”第二句“说明长期趋势向好但短期波动剧烈”第三句“需要关注回撤期间的成交量变化判断是恐慌抛售还是正常调整”。这样一套下来分析报告就有了逻辑主线读者跟着你的思路走不会迷失在图表里。5.2 这个分析框架还能怎么扩展这套框架不只适用于单只股票。把数据源换成指数就是大盘分析换成多只股票就是板块轮动分析换成加密货币就是数字资产分析。核心方法完全一样。再往深了做可以加入这些维度技术指标扩展MACD、RSI、布林带这些指标的计算逻辑都不复杂pandas几行代码就能实现多资产对比把几只同行业公司的股价放在一起看相对强弱事件驱动分析标记财报发布日、产品发布会等事件看事件前后的价格和成交量变化简单预测模型用ARIMA或者LSTM做价格预测但要注意预测精度通常很低别抱太大期望5.3 我个人的几条实操建议第一数据质量永远比模型复杂度重要。清洗干净的数据加上简单模型效果远好于脏数据加上复杂模型。第二可视化不是装饰是分析工具。画图的过程中你往往能发现统计量看不出来的模式。第三保留分析过程的中间结果。每一步的DataFrame都存一份后面想换个角度分析的时候不用从头跑。第四代码要模块化。把数据加载、清洗、计算、绘图拆成独立函数改一个环节不影响其他环节。第五结论要留有余地。股价受太多因素影响数据分析能告诉你“发生了什么”但很难告诉你“为什么发生”和“接下来会怎样”。保持谦逊别过度解读。最后分享一个我踩过的坑有一次我算年化波动率忘了用根号252直接乘以252结果算出来年化波动率超过500%自己看了半天觉得不对劲。后来检查代码才发现是年化系数用错了。这个错误很隐蔽因为代码不报错结果看起来也像个数字但量级完全不对。所以每次算完指标都要用常识判断一下这个数值合理吗股价年化波动率通常在20%到60%之间超过100%就要警惕了。