1. 项目整体思路与选型
1.1 这个项目到底在做什么
股票数据的线性拟合,听起来是个统计学作业,但真正动手做过的朋友都知道,它同时卡了数据处理、数学建模、图表可视化三座山。标题里那句“对现有的股票数据进行线性拟合并用matplotlib绘图”,本质上是想干这么一件事:手里已经攒了一份行情历史数据,希望用一条直线把价格的整体运动方向描述出来,再把这条线的效果直接画在价格走势图上,让人一眼就能看出“这段时间价格重心是往上走还是往下走”。
为什么要用一条直线去拟合股票价格?因为线性模型是所有趋势分析里最朴素、最可解释的基准。斜率的正负代表大方向,斜率的绝对值代表日均变化的快慢,截距则告诉我们拟合线在时间起点的参考位置。对于很多只需要做初步观察的场景——比如看看某只股票近一年是涨是跌、跌势稳不稳、波段大概什么幅度——线性拟合给出的两个数字已经足够用。它不像ARIMA、LSTM那样需要调一堆超参数,也不存在过拟合的风险,属于那种“五秒钟能解释清楚结果”的工具。
这类项目更大的价值在于可视化。同样的斜率是一回事,带拟合线的走势图是另一回事。做分析的人和看结果的人,对数据的理解速度完全不一样。所以这个项目真正考验的不是“会不会调用np.polyfit”,而是能不能把一张图做得干净、准确、有信息量。matplotlib在这里承担了最终交付的重任,标题点名它,不无道理。
1.2 工具箱里为什么是这几位
做这个项目之前,我是认真对比过方案组合的。可选路径大致有这么几条:
| 环节 | 备选方案 | 我最终的选择 | 理由 |
|---|---|---|---|
| 数据读取 | 手动读取CSV、openpyxl | pandas | 时间索引、缺失值和切片处理都是现成API |
| 拟合计算 | 手写最小二乘、sklearn、scipy | numpy.polyfit | 一行拿到系数,支持多项式,和画图无缝衔接 |
| 显著性校验 | Excel、手动公式 | scipy.stats.linregress | 顺带拿到R值和P值,帮助判断拟合到底靠谱不靠谱 |
| 绘图 | seaborn、plotly | matplotlib | 灵活度最高、自定义能力最强,且能直接接收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,但对拟合背后的原理讲得太浅,导致后面想调整数据范围、想换成多项式时无从下手。这里用人话补一堂微型数学课。
最小二乘法的目标狠简单:找到这样一条直线y=kx+b,让所有真实样本点到这条直线的纵向距离平方和最小。写成公式是minimize Σ(yᵢ - kxᵢ - b)²。为什么用平方而不是绝对值?因为平方运算会放大离群样本的影响力,让拟合结果更关注那些偏离主趋势的点;同时平方函数处处可导,能用求导的方法直接解出最优k和b。
放到股票场景里,x是时间序号,通常是0、1、2这样递增的整数;y是收盘价。拟合出来的斜率k,含义就是“每个交易日价格平均变化多少元”。这个单位虽然看着粗糙,但在日常复盘里非常直观。比如某股票在120个交易日里拟合出k=0.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'], lw=1.2, color='#2b6fa5', label='Close Price') ax.set_title('Stock Price and Linear Trend') ax.set_ylabel('Close Price (CNY)') ax.legend(loc='best')这里面有几个细节值得展开。第一,颜色选深蓝而不是matplotlib默认的亮蓝,亮蓝色在深色背景下刺眼,在白底下又显得廉价,深蓝的观感更接近金融终端;第二,线宽设成1.2而不是默认的1.5或1,1.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', lw=2, label='Linear Fit')为什么用端点的预测值而不是把整条拟合值y_pred当成一个序列传进去?因为后者要求y_pred的长度必须和df.index完全对齐,一旦后面做了切片、去重、重采样,稍不留神就会报错或者画出错位的线段。端点方式只依赖两个计算点,画出来的线段必然落在正确的时间范围内,省心。
还有一个小技巧,如果想把拟合公式直接标到图上去,可以在图例里带上斜率:
ax.plot([...], [...], '--', color='#d9534f', lw=2, label=f'Linear Fit (k={k:.3f})')这样一来,看图的人不需要回头查代码就能知道斜率数值,信息的可读性提升很直接。
4.3 让整张图有“可交付感”
画到这一步,图能用了。但离“可以放进汇报材料”还差几个细节:网格、坐标轴样式和导出设置。
网格能极大降低读图成本。但我只加横向网格,不加竖向网格,因为纵向网格容易和日期刻度互相干扰。写法是:
ax.grid(axis='y', alpha=0.35, linestyle='--')然后是坐标轴刻度,特别是日期。matplotlib有专门的日期格式化工具。如果想让刻度更稀疏,可以使用MonthLocator和DateFormatter:
import matplotlib.dates as mdates ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3)) ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) plt.setp(ax.get_xticklabels(), rotation=30)每三个月一个主刻度,再旋转30度防重叠,这样的时间轴既美观又容易读。最后保存图片时,dpi设成200,用bbox_inches='tight'裁掉多余白边:
fig.tight_layout() fig.savefig('stock_linear_fit.png', dpi=200, bbox_inches='tight')有些朋友喜欢直接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就会按类别处理,然后内部转换时对时间戳处理出错。解决办法是在读取时就确保索引是datetime64:
df.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,把日线重采样成周线或月线,再做同样的拟合。重采样之后噪声大幅减少,拟合直线的形态往往清晰很多。日线拟合适合看短期斜率,周线和月线拟合适合看长期方向,两者配合起来,观察维度会比单张图丰富得多。
踩过几次坑之后,我的实际感受是:做这种小型数据分析项目,最重要的不是堆技术,而是保持对数据的敏感——读数据前先确认口径,拟合前先看形态,画图前先想清楚图表要给谁看。工具只是把思路落地的载体,顺着这个节奏,任何一张股票走势图都能被整理成信息量充足、经得起推敲的趋势分析图。