用最小二乘法拟合股票K线趋势:Python数据分析与可视化
1. 为什么给K线做一条直线拟合趋势可视化的起点1.1 从折线图到拟合线先说说我遇到的实际问题很多新手拿到股票数据之后第一件事就是画收盘价折线图。看着起伏的价格曲线心里想着“这票最近到底是在涨还是在跌”。可说实话单看折线图趋势这个事非常容易误判——尤其是当波动比较大的时候眼睛很容易被最近几根大阳线或者大阴线带跑反而看不清整段走势的整体方向。我之前在整理某只样本标的的日线数据时就遇到这种事。当时股价在一个区间里来回震荡折线图看起来像一团乱麻。我借着绘图工具放大缩小时隐约觉得它整体重心有点往上抬但说不准。后来用最小二乘法对这组股票数据做了一次线性拟合画出一条趋势直线整个判断立刻清楚了很多斜率是正的重心确实在缓慢上移。趋势线一画出来图表信息量完全不同了。这篇文章就是把这套完整流程拆开来讲从数据准备、清洗口径到用np.polyfit做线性拟合再到用matplotlib绘图呈现出带拟合线的趋势图以及拟合结果怎么解读、有哪些容易翻车的地方。适合正在学数据分析、想把量化分析基础操作落地的同学也适合想给自己的看盘图表加一条“参考趋势线”的投资者。1.2 线性拟合在股票分析里到底扮演什么角色先明确一件事对股票数据做线性拟合不是为了预测明天涨跌而是为了回答一个更基础的问题——过去这一段区间价格的“平均走向”到底是什么。这里的核心思想是最小二乘法。它的目标很朴素找一条直线使得所有样本点到这条直线的竖直距离残差的平方和最小。为什么用平方而不是直接用距离因为距离有正有负直接相加会互相抵消平方之后不仅统一了方向还放大了偏离较大的点的影响让拟合结果更倾向于照顾大多数点。这就是为什么拟合出来的直线并不一定穿过任何一个真实收盘价但它在统计意义上代表了这组数据的主体趋势。在股票分析场景里这条直线可以回答几个实际问题这段区间整体是上升还是下跌趋势的斜率大致是多少价格的波动中心在哪里这些信息在裸看K线图时很难量化但一条拟合直线可以直观地给出来。不过也要说清楚线性拟合是对历史数据的描述性工具不是预言工具。它假设价格围绕一条直线波动但股票市场明显不是这么简单的过程。后面我会单独讲它的局限这里先不神化它。2. 数据准备先让数据“能算”再“能画”2.1 本地CSV读取与字段检查不管数据从哪里来到最后一定得整理成结构化表格。我习惯先用pandas读进来再检查字段因为后续做拟合和绘图的代码都建立在干净的DataFrame上。假设你手头有一份日线数据包含日期、开盘价、最高价、最低价、收盘价、成交量这几个常见字段。CSV文件长这样字段名类型说明dateobject/str交易日期需要解析成datetimeopenfloat开盘价highfloat最高价lowfloat最低价closefloat收盘价volumeint/float成交量读取代码很常规import pandas as pd df pd.read_csv(stock_daily.csv) df[date] pd.to_datetime(df[date]) # 把日期字符串转成datetime类型 df df.sort_values(date).reset_index(dropTrue) # 按时间升序排列并重置索引 print(df.head())这里有一个很小的细节但却很容易踩坑pd.to_datetime解析日期是后续所有日期相关绘图工作的前提。如果你跳过这一步matplotlib会把日期当成一串文本处理画出来的横轴坐标完全是乱的。我早些年第一次画时间序列图时就是没做转换结果横轴间距完全不对还以为是图表库的问题。另外检查一下有没有重复日期。同一个交易日出现两行数据多半是数据源合并时出了问题。重复日期会直接污染拟合结果因为它等于给某些点加了额外权重。处理方式很简单df df.drop_duplicates(subsetdate, keeplast).reset_index(dropTrue)2.2 复权价格与缺失值新手最常栽的两个坑接下来是很多人忽略的环节复权。股票在分红、送股、配股时会发生除权除息价格会直接跳空。比如某只股票前一天收盘20元第二天因为10派5开盘价直接变成18元左右。这种“断崖”不是市场行为而是会计口径造成的价格修正。如果在未复权的数据上直接做线性拟合拟合直线会被这种人为跳空严重带偏图形上看着像崩盘实际上什么都没发生。行情软件里通常有“前复权”和“后复权”两种选项。前复权是以当前价格为基准把历史价格向下调整后复权是以历史价格为基准把后续价格向上调整。我的经验是做历史趋势分析时优先用前复权数据因为它的最新价格和现实行情一致画图出来最贴近大家日常看到的走势。如果你拿到的数据接口没有复权选项至少要在拟合前检查一下close序列有没有因为除权而出现莫名其妙的巨大跳变有的话就得换复权口径。缺失值也比较常见。停牌日、节假日、数据断更都会留下NaN。拟合前必须处理否则np.polyfit会直接报错或者算出NaN结果。最简单的办法是df df.dropna(subset[close])如果缺失的是中间某几天而你希望保持时间的连续性也可以用前向填充补上但用在拟合上最好还是先删掉因为填充值本身并没有交易含义会扭曲趋势。2.3 没有真实数据时怎么先验证流程我不太建议刚开始跑流程就直接上真实行情——数据量一大问题往往出在数据质量上而不是算法本身很容易打击信心。更稳的做法是先造一段模拟数据把整体流程跑通确认代码没问题再替换成真实数据。模拟数据可以很简单import numpy as np import pandas as pd np.random.seed(42) n 120 dates pd.date_range(2024-01-01, periodsn, freqB) # B代表工作日跳过周末 trend np.linspace(10, 13, n) # 整体向上走的基准趋势 noise np.random.normal(0, 0.08, n) # 随机噪声模拟价格波动 close trend noise df pd.DataFrame({date: dates, close: close})这段代码生成了一段120个交易日的模拟行情里面有明确的上行趋势又叠加了随机噪声非常适合用来验证后续的拟合和绘图逻辑。等全部流程都跑通了再把df替换成从CSV读进来的真实数据就行。3. 拟合核心np.polyfit背后的算法与参数选择3.1 最小二乘法的直觉与代码实现线性拟合本质上是在解一个最优化问题。我们想找到斜率和截距使得所有点的残差平方和最小残差平方和 Σ(实际收盘价 - 拟合直线值)²这个公式看着抽象但想一下物理中的质心概念就懂了拟合直线是数据点在整体上的“重心走向”。它不追求完全穿过每个点而是追求对所有点的综合误差最小。numpy里最直接的接口就是np.polyfitx np.arange(len(df)) # 用0,1,2,...作为自变量 y df[close].values # 收盘价作为因变量 coef np.polyfit(x, y, deg1) # deg1表示拟合一次多项式即直线 slope, intercept coef print(f斜率: {slope:.4f}, 截距: {intercept:.4f})这里需要解释一个关键设计为什么自变量用0,1,2,...而不是用日期因为np.polyfit处理的是数值回归日期对象不能直接参与运算。把日期映射成等间隔的整数序列数学意义是“每个交易日间隔相等”这也符合我们对日线数据的常规假设。得到系数后用np.polyval生成拟合直线上的点方便后续绘图fit_line np.polyval(coef, x)看代码就知道整个过程短得让人有点意外。但真正值钱的不是这三行调用而是你知不知道polyfit背后在算什么、算出来的系数代表什么含义、什么时候这个结果会失真。这些恰恰是文档里不写的东西。3.2 为什么阶数默认是1而不是更高np.polyfit之所以把deg参数留给你是因为你可以选择任意多项式阶数。deg2是抛物线deg3是三次曲线以此类推。那为什么股票趋势拟合中几乎都用deg1关键在于“过拟合”。阶数越高拟合曲线越能逼近每一个历史点但它拟合的不再是趋势而是噪声。想象你用手掌压平一张被揉皱的纸一次多项式像是用一个平整的木板去压压出来的是一个整体平面高次多项式则像是用无数根手指精确地按压每个褶皱表面看起来贴合度极高但换个角度看那些褶皱根本不是你需要保留的信息。股票价格噪声非常大日线级别的随机波动往往远超真实趋势信号。如果用高次多项式拟合曲线会疯狂摆动去追逐每一根K线的波动画出来虽然“完美穿越”所有价格点但完全丧失了趋势分析的价值。线性拟合的意义恰恰在于它“钝感”它只提取整体走向忽略短期噪音。3.3 用R²判断拟合效果这个值怎么看光有斜率和截距还不够你得知道这条拟合线到底解释了多少价格变动。这就引入了R²拟合优度residuals y - fit_line ss_res np.sum(residuals ** 2) ss_tot np.sum((y - np.mean(y)) ** 2) r_squared 1 - ss_res / ss_tot print(fR²: {r_squared:.4f})R²的取值范围在0到1之间。越接近1表示价格波动中能被这条直线解释的比例越高接近0则表示价格基本是随机游走直线拟合没有太大意义。这里要给新手打个预防针股票日线数据的R²往往很低0.2到0.5都算正常。别看到R²低就觉得代码写错了这是股票市场本身的特性——短周期内影响价格的因素太多线性趋势只是众多分量中的一小部分。如果你改成周线或者月线数据聚合之后噪声被平均掉了R²通常会明显上升。这也是为什么做长周期趋势判断时我会先对数据做降采样再拟合。4. 绘图实战matplotlib产出干净的趋势图4.1 最小可用版本收盘价折线 拟合直线接下来进入重头戏用matplotlib绘图。先把最小可用的版本写出来后面再逐步加细节。import matplotlib.pyplot as plt import matplotlib.dates as mdates fig, ax plt.subplots(figsize(12, 6)) ax.plot(df[date], df[close], lw1.5, label收盘价) ax.plot(df[date], fit_line, --, colorred, lw2, label线性拟合趋势) ax.set_xlabel(日期) ax.set_ylabel(价格) ax.set_title(股票收盘价与线性拟合趋势线) ax.legend() ax.grid(alpha0.3) plt.tight_layout() plt.show()这段代码画出来的图已经具备基本的信息表达真实价格曲线作为背景拟合直线作为趋势参考。注意fit_line在代码里已经通过np.polyval计算成了和df[close]长度相同的数组所以可以直接用df[date]作为横轴来绘制。红虚线代表过去这段区间的“平均走向”比折线图本身更清晰地传递趋势信息。这就是整套任务的核心产出。4.2 日期坐标轴的坑为什么刻度总是挤成一团用matplotlib画时间序列十个人里有八个会遇到刻度标签重叠的问题。这是因为它默认自动选择刻度位置日期一多标签就会互相覆盖最终画出来的图没法看。解决办法是手动控制日期格式和刻度间隔ax.xaxis.set_major_locator(mdates.MonthLocator(interval2)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m-%d)) fig.autofmt_xdate() # 自动旋转日期标签避免重叠 plt.xticks(rotation45)这里有两个关键点。第一ax.plot(df[date], ...)传入的必须是datetime类型matplotlib内部会把它转换成数值坐标这样才能用mdates的定位器和格式化器来控制刻度。如果你传入的是字符串列表后面的MonthLocator根本不起作用。这也是我在数据准备阶段强调日期解析的原因。第二fig.autofmt_xdate()会自动调整横轴标签的旋转角度。我个人的习惯是搭配plt.xticks(rotation45)用两个叠加之后10个以上的日期刻度也能排得整整齐齐。4.3 进阶信息增强置信带、斜率标注、图表导出一个真正有价值的趋势图不应该只画一条线和一条趋势线。我会额外加三样东西一是误差带用来展示价格围绕拟合线的波动范围。用fill_between画出来能直观看出波动有多大pred_std np.std(y - fit_line) ax.fill_between( df[date], fit_line - 1.96 * pred_std, fit_line 1.96 * pred_std, alpha0.15, colorred, label约95%波动范围 )这里的1.96 * pred_std是从正态分布假设推导的近似95%的波动区间。严格来说样本量不大时应该用t分布的值或残差经验分位数但画图展示用这个近似完全够用。它给人的直觉是价格虽然波动但整体围绕红虚线在一定带宽内运动。二是关键数值标注。直接在图上写出拟合斜率、R²两个指标看图时不用再去代码里翻计算结果ax.text( 0.02, 0.95, f斜率: {slope:.4f} R²: {r_squared:.4f}, transformax.transAxes, fontsize12, verticalalignmenttop, bboxdict(boxstyleround, facecolorwhite, alpha0.7) )transformax.transAxes表示坐标位置以整个坐标轴为参照0到1之间是相对位置。这样不管数据范围怎么变标注永远在图的左上角。三是导出图片。日常分析我会输出300dpi的PNG保证放大后依然清晰fig.savefig(stock_trend.png, dpi300) plt.close(fig)plt.close(fig)这一步很多新手会忽略。在交互式环境里不关图会越积越多严重时内存被拖垮。养成画图后关闭的肌肉记忆能省掉很多莫名其妙的卡顿问题。5. 拟合结果解读与常见翻车点5.1 斜率不是一切先归一化才能横向比较很多人画完趋势图看到斜率是正的就觉得可以买看到斜率是负的就觉得要跑。这个直觉没毛病但要注意一个致命的细节斜率的绝对值没有任何跨股票比较的意义。原因很简单斜率的数值大小受价格量纲影响。一只股价200元的股票就算涨幅很温和拟合斜率可能也是0.5另一只股价5元的股票就算涨得很猛拟合斜率可能也只有0.03。你拿这两个斜率直接对比完全没有意义。正确的做法是把斜率归一化。最常用的方式是用斜率除以区间内的平均价格得到“单位价格的单位时间变化率”normalized_slope slope / np.mean(df[close])这个值相当于把不同价位的股票拉平到同一个起跑线上比较。比如归一化斜率为0.001意思是每个交易日平均上涨0.1%。如果你想知道年化水平再乘以每年的交易日数量通常按250天算就能粗略估算。5.2 样本区间的选择会偷偷改变结论这是隐藏最深的坑。同一只股票同一套拟合代码只要你把样本区间换一换结果可能完全翻转。比如某只标的前面涨了半年后面跌了三个月如果取最近三个月拟合斜率是负的取最近一年拟合斜率可能又是正的。两个结论都对因为描述的是不同时间尺度下的“平均状态”。没有哪个区间是“唯一正确”的关键是你得清楚自己分析的是哪一段趋势以及为什么选这一段。我踩过的例子是有次分析标的的时候我用60个交易日拟合斜率明显向下但后来把窗口拉长到120天发现其实只是最近一个月回调整体还是上涨中的回调。趋势判断完全取决于你选择了哪个“时间滤波器”。因此在做结论前最好多跑几个窗口长度对比一下20日拟合反映极短期走向噪声大波动敏感60日拟合约一个季度适合捕捉中期波段120日拟合约半年适合看中长期趋势没有一个窗口是永远最优的但多窗口对比能让你看清当前走势处在哪个层级避免被单一窗口欺骗。5.3 别把趋势线当成“未来的路”最后一个必须泼冷水的点线性拟合出来的趋势线本质上是历史数据的均值回归线它不构成任何买卖依据更不能被当成预测工具。股票价格的形成过程包含大量不可预测因素。拟合直线只代表“如果过去的平均状态延续下去价格大概会沿着这条线走”但市场的运行显然不是一个稳定线性过程。突发事件、资金流向、行业周期任何一个因素都可能让价格瞬间偏离拟合线很大距离。更实际的做法是把线性拟合当成一个“快速体检工具”用来初筛趋势状态发现问题后用其他分析方法交叉验证而不是单独拿一条直线的斜率做决策。这套流程的价值在于可视化、直观化、历史统计量化它降低了趋势判断的模糊性但绝不是在给你一台印钞机。6. 从拟合到实用还能往哪个方向扩展6.1 滚动窗口动态观察斜率变化静态拟合只能看到“截至今天”的趋势但市场是动态变化的。一个自然的扩展是引入滚动窗口计算随时间推移的滚动斜率window 60 df[rolling_slope] ( df[close] .rolling(window) .apply(lambda y: np.polyfit(range(window), y, 1)[0], rawTrue) )这段代码做了这么一件事每往前滑动一个交易日就用最近60天的数据重新拟合一条直线只取斜率存下来。这样你就得到了一个“斜率历史序列”——斜率在0轴上方持续走高说明趋势在加速上行斜率跌破0轴说明价格从上涨转入下跌斜率在0轴附近反复穿越说明处于震荡期。画图时把滚动斜率和主图做成一个双轴图效果非常好。滚动斜率做出来的曲线比传统技术指标早半拍因为它本质上是价格的导数数学上天然领先于价格本身。6.2 对数价格拟合长周期分析的更优选择前面我们一直对原始收盘价做线性拟合。但如果把时间拉长到几年这里有个数学上的隐患价格增长通常不是线性的而是近似复利式的指数增长。指数曲线在普通坐标里会越来越陡一条直线很难描述这种动态加速度。解决方案很简单对收盘价取对数再拟合直线。log_close np.log(df[close]) log_coef np.polyfit(x, log_close, 1) log_slope, log_intercept log_coef对数空间里的一次函数对应原空间里的指数趋势。此时拟合出来的斜率有一个很漂亮的含义它直接代表“日对数收益率”的近似水平。如果年化对数斜率为0.15相当于趋势年化收益率约16%。这对长周期趋势分析特别有用而且数值不再受价格绝对量纲影响天然具备横向可比性。之前我一直用原始价格拟合画图后来发现拉长到500天以上时趋势线经常偏高或偏低换成对数拟合之后明显合理很多。同样的道理也适用于计算长期均线——用对数通道做出来的均线系统在长周期上更贴近真实走势。6.3 与移动均线组合使用线性拟合趋势线和移动均线是一对很好的互补工具。均线反映的是“最近N日平均成本”拟合直线反映的是“最近N日统计趋势方向”。二者结合可以互相验证信号。比如我常用的做法是当拟合斜率由负转正同时收盘价上穿60日均线视为趋势弱转强的候选信号当拟合斜率由正转负同时收盘价跌破60日均线视为趋势转弱的提醒。注意我说的是“候选”和“提醒”不是买卖信号——技术分析最大的陷阱就是追求确定性而市场从来不给确定性。把这些工具组合起来画一张信息完整的趋势图比单看任何一条线都要可靠得多。你要的不只是一种分析手段而是一套能互相印证的观察体系。最后分享一个我自己很受用的检查习惯每次画完趋势图先别急着读结论把时间窗口来回调三次看看结论是否稳定。如果三个合理窗口得出的斜率符号一致我会认为趋势判断比较可靠如果符号不一致说明当前价格正处在一个多空分歧区域这时候线性拟合给出的信号本身也是紊乱的最好的选择是等待区间收敛。这套流程做完np.polyfit和matplotlib在你手里就不再是两条孤立的知识点而是一整套“数据到可视化再到结论”的完整链路。