资讯详情

资讯详情

股票数据线性拟合与matplotlib可视化实战

1. 项目整体思路与选型1.1 这个项目到底在做什么股票数据的线性拟合听起来是个统计学作业但真正动手做过的朋友都知道它同时卡了数据处理、数学建模、图表可视化三座山。标题里那句“对现有的股票数据进行线性拟合并用matplotlib绘图”本质上是想干这么一件事手里已经攒了一份行情历史数据希望用一条直线把价格的整体运动方向描述出来再把这条线的效果直接画在价格走势图上让人一眼就能看出“这段时间价格重心是往上走还是往下走”。为什么要用一条直线去拟合股票价格因为线性模型是所有趋势分析里最朴素、最可解释的基准。斜率的正负代表大方向斜率的绝对值代表日均变化的快慢截距则告诉我们拟合线在时间起点的参考位置。对于很多只需要做初步观察的场景——比如看看某只股票近一年是涨是跌、跌势稳不稳、波段大概什么幅度——线性拟合给出的两个数字已经足够用。它不像ARIMA、LSTM那样需要调一堆超参数也不存在过拟合的风险属于那种“五秒钟能解释清楚结果”的工具。这类项目更大的价值在于可视化。同样的斜率是一回事带拟合线的走势图是另一回事。做分析的人和看结果的人对数据的理解速度完全不一样。所以这个项目真正考验的不是“会不会调用np.polyfit”而是能不能把一张图做得干净、准确、有信息量。matplotlib在这里承担了最终交付的重任标题点名它不无道理。1.2 工具箱里为什么是这几位做这个项目之前我是认真对比过方案组合的。可选路径大致有这么几条环节备选方案我最终的选择理由数据读取手动读取CSV、openpyxlpandas时间索引、缺失值和切片处理都是现成API拟合计算手写最小二乘、sklearn、scipynumpy.polyfit一行拿到系数支持多项式和画图无缝衔接显著性校验Excel、手动公式scipy.stats.linregress顺带拿到R值和P值帮助判断拟合到底靠谱不靠谱绘图seaborn、plotlymatplotlib灵活度最高、自定义能力最强且能直接接收numpy结果选pandas不是因为它花哨而是股票数据天然长成DataFrame的样子日期当索引开高低收成交量当列。对这种结构做筛选、排序、重采样、缺失值处理pandas都有现成接口比纯Python手工遍历要省太多体力。numpy负责计算polyfit底层走的是最小二乘法对一维价格序列这种规模的数据来说速度和精度都绰绰有余。matplotlib在这个项目里属于不可替代的角色。它内置了日期刻度定位器和格式化器处理时间轴不用自己写字符串转换逻辑。而且numpy算出来的系数直接扔给matplotlib画线中间零转换成本。这种平滑衔接是很多教程没讲透、但我实测下来最省心的点。1.3 适合谁来参考这个项目的受众其实很广。刚入门Python数据分析的同学可以通过它把pandas、numpy、matplotlib三件套串起来练一遍做量化的初级工程师可以用它快速验证某只票或某个指数在某一区间内的趋势特征哪怕是只对股票分析感兴趣、没系统学过编程的人照着文章里的代码一行行跑也能在半小时内把自己电脑里的行情数据变成一张趋势图。唯一要提前说明的是线性拟合更偏描述性分析它回答的是“过去发生了什么”而不是“未来一定怎么样”。拿它做复盘、做筛选、做数据快照都很好用但如果想直接拿斜率去预测下个月的股价我建议扩展成更完整的时间序列模型而不是停留在这一条直线上。这个态度会在后面高频出现先立在这里。2. 数据获取与预处理2.1 数据从哪里来长什么样题目说的是“现有的股票数据”这句话在实际操作里通常对应三种出处。第一种是本地文件最常见的是CSV或Excel例如从行情软件打断点导出的日K线第二种是通过免费行情接口直接拉取在代码里输入股票代码和起止日期就能批量下载第三种是本地数据库每天把行情自动写入再按需读取。不管来源是哪一种我建议进入拟合前先做一次“数据体检”。以CSV文件为例典型的日线数据长下面这样date,open,high,low,close,volume 2024-01-02,10.35,10.68,10.12,10.47,1534200 2024-01-03,10.50,10.92,10.38,10.85,1721900 2024-01-04,10.78,11.02,10.55,10.79,1483600这套字段结构是最标准的OHLCV格式其中close收盘价是我们做拟合最常用的列。读进pandas之后我习惯先做三件事打印字段类型、查看形状和索引范围、确认日期列被解析成了datetime类型而不是字符串。代码是这样import pandas as pd df pd.read_csv(stock_daily.csv, parse_dates[date]) df df.set_index(date).sort_index() print(df.shape) print(df.index.min(), -, df.index.max()) print(df.dtypes)仅仅多了parse_dates这一个参数后面就能避免一大类时间轴错乱的问题。然后set_index把date列提升为索引sort_index保证顺序。如果有人觉得“排序是多此一举”那大概率还没遇到过pandas处理乱序索引时的诡异行为——滚动计算错位、绘图线段乱跳都是从这里开始的。先花十秒排序后面省半小时。2.2 缺失值、异常值、复权口径都不能跳过股票数据从来不是一尘不染的。停牌会导致交易日直接缺行少量数据源还会出现时间不连续、成交量为0、价格异常跳空之类的情况。处理这些数据大原则是“先删后补、删补结合”但不能一删了之。我的常规清洗流程分四步。第一步去掉全空行也就是所有价格字段都是NaN的记录第二步针对单列缺失如果只是收盘价缺一两行用上一交易日的收盘价填充ffill如果缺口太长就直接删除这段因为强行补出来的中间价格对拟合没有任何意义第三步检查价格是否出现负数或0这类明显不属于正常行情的记录要先标记出来结合是否除权除息来判断第四步去掉重复索引比如同一天出现两行数据时保留最后一行的数据。还有一处特别容易被忽略就是复权口径。普通未复权数据在分红送股后会出现价格跳空前一天收盘价10块除权日直接变成8块从图上看起来像暴跌一样。线性拟合如果碰上这种跳空斜率会被严重拉偏。所以拿到数据的第一件事不是算斜率而是确认数据是不是复权价、是前复权还是后复权。做长期趋势分析推荐用后复权因为它的历史价格连续性最好做近期价格参考前复权更直观。这个选择务必在代码备注里写清楚否则三个月后回看代码自己都分不清当时算的是什么口径的价格了。3. 线性拟合原理与实现3.1 最小二乘法的人话版解释很多代码教程上来就调np.polyfit但对拟合背后的原理讲得太浅导致后面想调整数据范围、想换成多项式时无从下手。这里用人话补一堂微型数学课。最小二乘法的目标狠简单找到这样一条直线ykxb让所有真实样本点到这条直线的纵向距离平方和最小。写成公式是minimize Σ(yᵢ - kxᵢ - b)²。为什么用平方而不是绝对值因为平方运算会放大离群样本的影响力让拟合结果更关注那些偏离主趋势的点同时平方函数处处可导能用求导的方法直接解出最优k和b。放到股票场景里x是时间序号通常是0、1、2这样递增的整数y是收盘价。拟合出来的斜率k含义就是“每个交易日价格平均变化多少元”。这个单位虽然看着粗糙但在日常复盘里非常直观。比如某股票在120个交易日里拟合出k0.04就代表这段时间平均每个交易日涨4分钱如果k-0.03就代表每天平均跌3分钱。配合当前股价大概能估算出一个月后的价格重心位置。有一点要泼冷水线性拟合假设趋势是均匀线性的。但股票价格在长周期里经常是先涨后跌再横盘这时一条直线只能反映“平均或“重心”,无法捕捉结构变化。所以我在拿到斜率之后会习惯性看一下R²它决定了这条线的可信度到底有多高。3.2 用numpy三行代码算出趋势线实现部分直接用numpy。先给DataFrame添加一列序号再调用polyfit系数就出来了import numpy as np df[seq] np.arange(len(df)) k, b np.polyfit(df[seq], df[close], 1) print(f斜率 k {k:.4f}) print(f截距 b {b:.4f})np.polyfit第三个参数传1表示用一元一次多项式拟合。返回值是按降幂排列的系数所以k是最高次项系数b是常数项。如果某天想升级成二次曲线只要把参数改成2返回值就变成[a, b, c]画出来的就是抛物线趋势原理一模一样。不过我建议输出不止这两个数。单纯“算出来了”和“结果有意义”是两码事。用scipy.stats.linregress再补几个统计量结果会专业得多from scipy import stats res stats.linregress(df[seq], df[close]) print(slope , res.slope) print(intercept , res.intercept) print(R , res.rvalue) print(R-squared , res.rvalue ** 2) print(p-value , res.pvalue)R值皮尔森相关系数衡量的是价格和时间序号之间的线性相关强度。R接近1或-1说明走势非常贴近一条直线R接近0说明价格基本是围着水平线乱蹦拟合出的斜率虽然算得出但参考价值很低。p-value则用来判断斜率是否统计显著通常小于0.05才认为这个线性关系不是随机折腾出来的。3.3 什么时候该放弃线性拟合这一步容易被忽略但我认为必须做。把R²列出来之后根据自己的场景判断这条线到底能不能用。我给自己定过几条简单标准R² 0.8价格走势非常笔直线性拟合信息量很高0.3 R² 0.8线性趋势存在但波动不小适合做方向参考R² 0.3价格基本是横盘震荡或大幅波动线性拟合的价值很低不如改用区间分析或分段拟合。判断完了再看一眼残差。把真实收盘价减去拟合值如果残差呈现明显的“先正后负”或“先负后正”的形态说明数据中存在明显的趋势拐点此时一条直线并不够用改成两段式的分段拟合会合适得多。这个细节后面还会专门提到。4. matplotlib可视化细节4.1 先把收盘价折线画出来拟合参数到手后重头戏才刚开始。matplotlib绘图如果只是“把点连成线”交作业没问题但要达到“一张图看懂趋势”的效果还需要在视觉细节上下点功夫。最基础的收盘价折线图我推荐用这种写法import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(12, 6)) ax.plot(df.index, df[close], lw1.2, color#2b6fa5, labelClose Price) ax.set_title(Stock Price and Linear Trend) ax.set_ylabel(Close Price (CNY)) ax.legend(locbest)这里面有几个细节值得展开。第一颜色选深蓝而不是matplotlib默认的亮蓝亮蓝色在深色背景下刺眼在白底下又显得廉价深蓝的观感更接近金融终端第二线宽设成1.2而不是默认的1.5或11.5以上在密集K线上会显得臃肿1又太细看不清第三figsize直接开到12x6既方便看清细节又适合直接放进PPT或报告中。4.2 把拟合直线叠加到同一张图叠加拟合直线的核心思路是让原始折线和拟合直线共享同一个x轴也就是同一个时间基准。这里推荐一个稳妥的端点式画法x_start 0 x_end len(df) - 1 y_start k * x_start b y_end k * x_end b ax.plot([df.index[x_start], df.index[x_end]], [y_start, y_end], --, color#d9534f, lw2, labelLinear Fit)为什么用端点的预测值而不是把整条拟合值y_pred当成一个序列传进去因为后者要求y_pred的长度必须和df.index完全对齐一旦后面做了切片、去重、重采样稍不留神就会报错或者画出错位的线段。端点方式只依赖两个计算点画出来的线段必然落在正确的时间范围内省心。还有一个小技巧如果想把拟合公式直接标到图上去可以在图例里带上斜率ax.plot([...], [...], --, color#d9534f, lw2, labelfLinear Fit (k{k:.3f}))这样一来看图的人不需要回头查代码就能知道斜率数值信息的可读性提升很直接。4.3 让整张图有“可交付感”画到这一步图能用了。但离“可以放进汇报材料”还差几个细节网格、坐标轴样式和导出设置。网格能极大降低读图成本。但我只加横向网格不加竖向网格因为纵向网格容易和日期刻度互相干扰。写法是ax.grid(axisy, alpha0.35, linestyle--)然后是坐标轴刻度特别是日期。matplotlib有专门的日期格式化工具。如果想让刻度更稀疏可以使用MonthLocator和DateFormatterimport matplotlib.dates as mdates ax.xaxis.set_major_locator(mdates.MonthLocator(interval3)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) plt.setp(ax.get_xticklabels(), rotation30)每三个月一个主刻度再旋转30度防重叠这样的时间轴既美观又容易读。最后保存图片时dpi设成200用bbox_inchestight裁掉多余白边fig.tight_layout() fig.savefig(stock_linear_fit.png, dpi200, bbox_inchestight)有些朋友喜欢直接plt.show()然后手动截图风味也行就是分辨率往往不够。既然做都做了保存一张高清PNG后续不论是自己复盘还是发给别人看都从容得多。5. 常见问题与排查技巧实录5.1 中文标签全变成小方块这是matplotlib用户遇到频率最高的一个问题没有之一。明明代码跑得好好的保存出来的图片上中文全变成了一个个方框原因就一句matplotlib的默认字体里没有中文字符。解决思路有两条。第一条在绘图脚本最前面设置全局字体plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, Noto Sans CJK SC] plt.rcParams[axes.unicode_minus] False其中第一个配置指定了中文字体的候选列表第二个配置必须带上否则坐标轴上的负号会显示成奇怪的方块。第二条思路干脆全部用英文标签完全绕过字体依赖。我的习惯是本地调试时开中文方便自己看输出到报告或公共场合时切英文避免别人环境没有对应字体导致渲染变形。5.2 日期轴变成1970或者挤成一坨时间轴错乱是时序绘图里的第二个高频坑。症状一般有两种一是X轴刻度出现一堆1970-01-01二是所有日期像被压实了一样叠成一条黑带。第一种情况基本可以断定是索引不是datetime类型。读取数据时没加parse_dates或者某个数据源返回的是字符串日期matplotlib就会按类别处理然后内部转换时对时间戳处理出错。解决办法是在读取时就确保索引是datetime64df.index pd.to_datetime(df.index)第二种黑带情况多半是数据量太大而图幅太小。日线数据几百条还好如果直接拿几千条分钟线画图默认刻度全挤在一起视觉上就是一根黑条。处理办法是前面提过的MonthLocator把刻度频率拉大到合适间隔。还有一个我踩过的坑绘图过程中如果把索引强制转成int64时间戳喂给其他坐标轴时间样式就会错乱。比如df.index.astype(int64)这种写法除非你明确知道自己在干什么否则别用。matplotlib本身能处理datetime索引就不要画蛇添足。5.3 拟合线画出来后和直觉完全不符有时候斜率算出来是正的但图上看起来价格明明在下跌拟合线却横在中间。这种感觉很诡异但原因其实不复杂线性拟合对整段数据“一视同仁”如果样本区间既包含大涨又包含大跌拟合出来的直线往往接近水平代表的是整体均值状态而不是最近一段趋势。解决方法是分段拟合。比如把数据按年份切块或者按明显的波段切块分别计算各段斜率再用不同颜色的拟合线画在同一张图上。这样既能看出各阶段的方向又能保留整段走势的连续性。分段切割点时可以参考价格的高点和低点也可以直接用滚动斜率的变化来辅助判断。这类问题提醒我一件事不要拿到全量数据就盲目拟合先画出原始折线图看一眼再决定用整段拟合还是分段拟合。肉眼判断虽然看起来不“量化”但在建模之前做一次形态观察能少走很多弯路。5.4 换了数据源之后结果对不上这个问题最隐蔽。同一只股票、同一段时间在A数据源拉出来拟合的斜率和在B数据源算出来的对不上很多人第一反应是代码出了问题。其实大概率是数据口径不一致。常见的原因前文提到过——复权方式不同还有可能是停牌处理方式不同有的源把停牌日保留为空值有的直接跳过又或者是价格单位不同有的返回元有的返回分。排查这类问题可以直接对比两个数据源在相同交易日的前收盘值和后收盘值是否一致。一旦确认是口径问题只需要在拉数据时统一设置复权方式并明确写入代码注释结论就能对齐。6. 从这套流程延伸出去的想法写到这里线性拟合加matplotlib绘图的完整路径已经走完了。但我还想说几句关于“扩展”的个人体会。第一线性拟合最大的价值不是预测而是快速感知一个数据集的重心方向。如果把数据按照不同的时间窗口反复计算斜率就能生成一条“斜率变化曲线”从而观察到趋势本身是加速还是减速。这个思路我在做项目复盘时经常用到比单纯看一根拟合线更有信息量。第二matplotlib的交互化改造并不难。最近我试着把这张静态图升级成带滑块的版本拖动时间窗口范围绘图区立刻重新做拟合并更新直线。整个过程只需要在回调函数里重新跑一遍polyfit和plot成本很低但演示效果和在本地分析时的体验比静态图好出一个量级。第三如果想进一步观察中长线趋势可以试试配合pandas的resample把日线重采样成周线或月线再做同样的拟合。重采样之后噪声大幅减少拟合直线的形态往往清晰很多。日线拟合适合看短期斜率周线和月线拟合适合看长期方向两者配合起来观察维度会比单张图丰富得多。踩过几次坑之后我的实际感受是做这种小型数据分析项目最重要的不是堆技术而是保持对数据的敏感——读数据前先确认口径拟合前先看形态画图前先想清楚图表要给谁看。工具只是把思路落地的载体顺着这个节奏任何一张股票走势图都能被整理成信息量充足、经得起推敲的趋势分析图。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →