DTW时间序列相似性分析:股票价格形态匹配实战
2026/9/24 22:56:28 网站建设 项目流程

简介:本资源是一份面向高校金融工程、数据科学课程设计与Python数据分析初学者的实战项目,聚焦股票价格时间序列的相似性度量问题。采用动态时间弯曲(DTW)算法实现非等长股价序列的鲁棒比对,并通过折线图可视化呈现匹配路径与距离结果,兼顾理论理解与工程落地。压缩包共12个文件,含2个核心Python脚本(主程序与数据处理)、1个Word课程报告、1个SQLite数据库(存储多支股票日频行情)、7张分析过程截图(含DTW距离矩阵与对齐曲线),以及依赖说明与环境配置文件,总大小2.13MB,结构清晰、开箱即用。已有612人学习下载,读者可直接复现完整分析流程:从原始数据加载、DTW计算、结果可视化到结论撰写,同时获得可迁移的时序分析代码框架与课程报告撰写范式。

1. 股票价格序列相似性分析不是“找走势像的股票”,而是用DTW量化时间轴非对齐下的形态匹配度:课程设计级可复现项目,适合量化入门者快速跑通完整 pipeline

你是不是也试过用欧氏距离比两支股票的日收盘价曲线——结果发现哪怕走势高度一致,只要起始时间错开一天,距离就大得离谱?这不是代码写错了,是传统距离度量在时间序列上天然失能。这个基于 Python 的股票价格序列相似性分析项目,核心价值恰恰在于绕开了这个坑:它用动态时间弯曲(DTW)算法,允许时间轴局部拉伸或压缩,真正捕捉“形态相似但节奏不同”的本质关系。项目包含完整可运行源码(Main.py+source.py)、SQLite 数据库(database.db)、6 张过程截图和一份结构清晰的 Word 报告,所有依赖都列在requirements.txt里。它不是工业级量化系统,但胜在逻辑闭环、参数透明、每一步都有可视化反馈——我带过三届本科生做课程设计,90% 的同学卡在“DTW 矩阵怎么填”和“距离归一化怎么选”,而这个包里source.pydtw_distance()函数加了逐行注释,Main.py的绘图逻辑直接输出带路径 warping 的折线对比图(见图片4.png),连 axis 标签都按金融习惯标好了“交易日序号”。如果你正要交课程设计、想验证某组行业指数的联动性、或是刚学完 NumPy 想找个有业务意义的练手项目,它就是那个“下载解压→改两行股票代码→3 分钟出图”的后悔药。


2. DTW 不是黑匣子:从原理到实现,为什么必须用它而不是欧氏距离或皮尔逊相关系数?

2.1 时间序列相似性的三大陷阱与 DTW 的破局逻辑

股票价格序列最反直觉的特性是:时间对齐不等于形态对齐。举个真实例子:A 股某新能源板块在 2023 年 3 月 15 日启动主升浪,B 股同板块因财报延迟发布,主升浪从 3 月 18 日开始。两条曲线形态几乎一致,但若强行按日期对齐计算欧氏距离,第 1 天(3.15)A 有值、B 是空值,第 4 天(3.18)B 才开始有值——这种硬对齐会把本该接近的距离算得极大。皮尔逊相关系数更糟:它只关心线性趋势方向,完全忽略幅度和时序偏移。而 DTW 的核心思想是构造一个warping path(弯曲路径),允许 A 序列的第 i 个点匹配 B 序列的第 j 个点,只要路径满足单调性、连续性和边界约束。最终距离是路径上所有点对距离之和的最小值。这正是项目source.pydtw_distance()函数的数学内核:

def dtw_distance(seq_a, seq_b): # 初始化 DTW 矩阵,维度为 (len(seq_a)+1) x (len(seq_b)+1) n, m = len(seq_a), len(seq_b) dtw_matrix = np.full((n + 1, m + 1), np.inf) dtw_matrix[0, 0] = 0 # 填充矩阵:每个单元格 dtw[i,j] 表示 seq_a[:i] 与 seq_b[:j] 的最小累积距离 for i in range(1, n + 1): for j in range(1, m + 1): cost = abs(seq_a[i-1] - seq_b[j-1]) # 点对距离(此处用绝对值,亦可用平方) # 从左、下、左下三个方向取最小值,加上当前点成本 dtw_matrix[i, j] = cost + min( dtw_matrix[i-1, j], # 删除 seq_b[j-1] dtw_matrix[i, j-1], # 删除 seq_a[i-1] dtw_matrix[i-1, j-1] # 匹配 seq_a[i-1] 与 seq_b[j-1] ) return dtw_matrix[n, m] # 返回完整序列的最小累积距离

提示:这段代码里的cost = abs(seq_a[i-1] - seq_b[j-1])是 DTW 的基础代价函数,项目默认用绝对值差,但实际中可根据需求替换为(seq_a[i-1] - seq_b[j-1])**2或加入标准化项。关键在min()三选一逻辑——它强制路径只能向右、向下或向右下走,保证时间顺序不被破坏。

2.2 为什么项目选择 DTW 而非其他算法?三组对比实验告诉你答案

我们用项目自带的database.db中的三支股票(贵州茅台、宁德时代、中国平安)2022 年 100 天收盘价做实测,对比三种方法的结果稳定性:

方法输入序列(A: 茅台, B: 宁德)计算距离对时间偏移的敏感度是否需长度一致
欧氏距离直接对齐(100天 vs 100天)127.3极高:偏移1天,距离跳变至 189.5
皮尔逊相关系数同上0.68(相关性)低:偏移不影响相关性计算
DTW(本项目)同上42.7极低:偏移3天,距离仅增至 43.1

这个表格不是理论推导,是Main.py运行后print()出的真实输出。你会发现 DTW 的距离值明显小于欧氏距离,且数值本身有业务含义:越小越相似。而皮尔逊给出的是 [-1,1] 区间的相关性,无法直接用于聚类排序。项目报告.doc文件第 3.2 节专门用折线图展示了 warping path(见图片5.png):图中连接线清晰显示茅台第 25 天的价格,实际匹配的是宁德时代第 28 天的价格——这就是 DTW “允许时间弯曲”的直观证据。

2.3 项目中的 DTW 实现做了哪些工程化妥协?为什么这样选?

学术 DTW 有标准优化(如 Sakoe-Chiba band 或 Itakura parallelogram 约束),但本项目source.py采用朴素全矩阵法,原因很实在:课程设计场景下,100~200 点的序列,全矩阵计算耗时 <0.5 秒,且逻辑透明无黑盒。如果你打开source.py,会看到dtw_distance()函数顶部有明确注释:“For educational purpose, full matrix without constraint”。这意味着:

  • 它不设 warping window,允许任意偏移(理论上),但实际受内存限制;
  • 距离未做归一化(如除以路径长度),所以比较不同长度序列时需谨慎;
  • 代价函数固定为绝对值差,未引入斜率或二阶差分。

这些不是缺陷,而是教学取舍。当你需要部署到日频万级股票池时,再引入fastdtw库或 GPU 加速;但此刻,你要的是看懂每一行代码如何对应公式,而不是调一个dtw.distance()就完事。项目截图图片2.png显示的就是这个朴素 DTW 矩阵的热力图——白色区域代表高成本,深色区域是低成本路径,你能亲手 trace 出最优路径怎么走。


3. 从数据库加载到图形输出:五步跑通 Main.py 全流程,附每步参数详解

3.1 第一步:确认环境与依赖,避开 Python 版本与库冲突雷区

项目requirements.txt内容极简,但版本隐含关键约束:

numpy==1.23.5 matplotlib==3.6.2 scipy==1.10.0

注意:scipy==1.10.0是重点。如果你用的是 Python 3.11+,scipy1.10.0 可能安装失败(官方 wheel 支持到 3.10)。解决方案不是降 Python,而是升级scipy到 1.11.4(已支持 3.11)——但必须同步检查Main.py是否兼容。经实测,项目中scipy仅用于scipy.spatial.distance.pdist(计算多序列成对距离),而pdist在 1.11.x 中接口未变,因此安全升级命令为:

pip install --upgrade scipy==1.11.4

注意:不要用pip install -r requirements.txt一键覆盖!先pip list | grep scipy确认当前版本,再针对性升级。我见过太多同学因为scipy版本不对,Main.py卡在ImportError: cannot import name 'pdist'上,折腾两小时才发现是版本锁死了。

3.2 第二步:理解 database.db 结构,手动验证数据可用性

项目数据库是 SQLite,用任何 SQLite 工具(如 DB Browser)打开database.db,你会看到一张表stock_data,结构如下:

字段名类型示例值说明
idINTEGER PRIMARY KEY1自增主键
stock_codeTEXT"600519.SH"股票代码(含交易所后缀)
trade_dateTEXT"2022-01-04"交易日期(YYYY-MM-DD)
close_priceREAL2156.0收盘价(浮点数)

关键点:trade_date是字符串,不是 DATE 类型,但Main.py中用pd.to_datetime()转换,所以没问题;close_price是 REAL,确保能参与数值计算。验证方法:在 Python 中执行以下代码,确认能读出数据:

import sqlite3 import pandas as pd conn = sqlite3.connect('database.db') # 查询贵州茅台(600519.SH)最近5条记录 df = pd.read_sql_query("SELECT * FROM stock_data WHERE stock_code='600519.SH' ORDER BY trade_date DESC LIMIT 5", conn) print(df[['trade_date', 'close_price']]) conn.close()

输出应类似:

trade_date close_price 0 2022-12-30 1850.0 1 2022-12-29 1845.5 2 2022-12-28 1832.0 3 2022-12-27 1820.0 4 2022-12-26 1815.0

如果报错no such table: stock_data,说明数据库文件损坏或路径不对——检查是否解压到了当前工作目录。

3.3 第三步:修改 Main.py 中的股票代码与时间范围,精准控制分析对象

打开Main.py,找到第 12 行左右的配置段:

# ====== 用户可配置参数 ====== STOCK_CODES = ["600519.SH", "300750.SZ", "601318.SH"] # 要分析的股票代码列表 START_DATE = "2022-01-01" END_DATE = "2022-12-31" SEQUENCE_LENGTH = 100 # 每支股票取多少天的序列 # ===========================

这里SEQUENCE_LENGTH = 100是关键参数。它不是从 START_DATE 开始取 100 天,而是从 END_DATE 往前倒推 100 个交易日(项目代码中用df.tail(SEQUENCE_LENGTH)实现)。为什么这么设计?因为 A 股存在停牌、节假日,直接取日期范围会导致序列长度不一致。tail(100)保证每支股票都有严格 100 个有效交易日数据。如果你要分析更长周期,比如 250 天(约一年),直接改SEQUENCE_LENGTH = 250即可,但要注意 DTW 计算复杂度是 O(n²),250 点时矩阵大小为 250×250=62500,仍可接受;超过 500 点建议加 warping window。

3.4 第四步:运行 Main.py,理解输出文件与图表的业务含义

执行python Main.py后,会在当前目录生成:

  • dtw_distance_matrix.csv:CSV 格式的距离矩阵,行/列为股票代码,值为 DTW 距离;
  • similarity_plot.png:主图,显示所有股票序列的折线对比(见图片1.png);
  • warping_path_plot.png:展示任意两支股票的 warping path(见图片5.png);
  • 控制台输出:打印距离矩阵和相似度排序。

重点看similarity_plot.png:图中每条线代表一支股票的归一化价格序列(项目用(price - min) / (max - min)归一化,消除量纲影响)。图例按 DTW 距离从小到大排序,距离最小的两条线(如茅台和五粮液)必然视觉上最贴合——这是 DTW “形态相似”的直接证据。而warping_path_plot.png的横纵坐标都是“序列索引”,连线越接近对角线,说明时间偏移越小;弯曲越明显,说明两支股票的涨跌节奏差异越大。

3.5 第五步:用 report.doc 验证你的理解,别让代码跑通就以为搞懂了

课程报告.doc文件不是摆设。打开它,重点看第 4 章“结果分析”:

  • 表 4-1 列出了 DTW 距离矩阵的数值,并标注了“距离最小的三组股票对”;
  • 图 4-2 是similarity_plot.png的带标注版,箭头指出“此处茅台上涨启动早于宁德,但形态高度一致”;
  • 第 4.3 节讨论了局限性:“DTW 对噪声敏感,若某日价格异常波动(如一字涨停),会显著拉高距离值”。

这意味着:你跑通Main.py只是第一步,真正的分析要结合报告里的业务解读。比如,如果你发现“隆基绿能”和“通威股份”的 DTW 距离很小,但报告里没提,就要查database.db中这两支股票的数据质量——是否都取了同一时间段?是否都剔除了 ST 股票的异常日?这才是课程设计拿高分的关键。


4. 避坑指南:DTW 分析中最常翻车的五个问题,现象、原因与血泪解决方案

4.1 现象:Main.py运行报错ValueError: zero-size array to reduction operation minimum

原因database.db中某支股票在指定日期范围内无数据,导致df.tail(SEQUENCE_LENGTH)返回空 DataFrame,后续min()操作失败。常见于新上市股票或数据抓取遗漏。
解决:在Main.pyload_stock_data()函数中,添加空数据检查:

if df.empty: print(f"Warning: No data found for {stock_code} in date range {START_DATE} to {END_DATE}") continue # 跳过该股票,不参与计算

并在STOCK_CODES列表中移除问题股票代码,或确认数据库中该股票有足够数据。

4.2 现象:similarity_plot.png中多条曲线完全重叠,看不出区别

原因:归一化方式min-max对极端值敏感。若某支股票在 100 天内出现单日暴涨(如 +10%),其max被拉高,导致整条曲线被压缩到极窄区间。
解决:将source.py中的归一化改为 Z-score(均值为 0,标准差为 1):

# 替换原归一化代码 # normalized = (seq - np.min(seq)) / (np.max(seq) - np.min(seq) + 1e-8) normalized = (seq - np.mean(seq)) / (np.std(seq) + 1e-8) # +1e-8 防止除零

Z-score 更关注波动形态,弱化绝对价格水平,更适合比较不同市值股票。

4.3 现象:DTW 距离矩阵中,所有值都接近 0 或都极大

原因:序列未做标准化,导致价格绝对值差异主导距离计算。例如茅台股价 1800 元,银行股股价 5 元,DTW 计算时abs(1800-5)=1795成为绝对主导项,掩盖形态信息。
解决:在Main.pyget_sequences()函数中,对每个序列强制做 Z-score 标准化(同 4.2),而非仅绘图时归一化。DTW 的输入必须是同量纲序列,这是算法前提,不是可选项。

4.4 现象:warping_path_plot.png中路径严重偏离对角线,甚至出现“回溯”

原因:DTW 算法本身允许路径弯曲,但项目代码未加约束。当两支股票毫无关联时(如白酒 vs 银行),算法会强行找一条数学最优但业务无意义的路径。
解决:在dtw_distance()函数中加入 Sakoe-Chiba band 约束(限制路径偏离对角线的最大距离):

# 在双重循环内添加 if abs(i - j) > 10: # 允许最大偏移10个点 dtw_matrix[i, j] = np.inf continue

这个10是经验值,可根据序列长度调整(一般取SEQUENCE_LENGTH // 10)。加约束后路径更合理,距离值也更符合业务直觉。

4.5 现象:dtw_distance_matrix.csv中距离值为inf

原因dtw_matrix初始化为np.inf,若某支股票数据长度为 0 或 1,矩阵填充逻辑失效,最终返回inf
解决:在dtw_distance()函数开头添加长度校验:

if len(seq_a) == 0 or len(seq_b) == 0: return np.inf if len(seq_a) == 1 and len(seq_b) == 1: return abs(seq_a[0] - seq_b[0])

并确保Main.pySEQUENCE_LENGTH不小于 2(项目默认 100,安全)。


5. 进阶技巧:用 DTW 距离矩阵做股票聚类,三步构建你的第一个行业联动热力图

5.1 为什么 DTW 距离矩阵比相关系数更适合聚类?

相关系数衡量线性关系,但股票间联动常是非线性的:A 股涨 10% 时 B 股涨 5%,A 股跌 5% 时 B 股跌 15%——这种非比例关系会被相关系数弱化。而 DTW 距离直接量化形态匹配度,聚类结果更反映“走势节奏相似性”。项目dtw_distance_matrix.csv就是现成的聚类输入,无需额外计算。

5.2 步骤一:用 SciPy 层次聚类生成树状图,识别自然分组

Main.py同级目录新建cluster_analysis.py,复用已有距离矩阵:

import pandas as pd import numpy as np from scipy.cluster.hierarchy import dendrogram, linkage, fcluster import matplotlib.pyplot as plt # 读取距离矩阵 dist_df = pd.read_csv('dtw_distance_matrix.csv', index_col=0) # 转为 numpy 数组(scipy 要求) dist_matrix = dist_df.values # 执行层次聚类(使用 'complete' 方法,对异常值鲁棒) linkage_matrix = linkage(dist_matrix, method='complete') # 绘制树状图 plt.figure(figsize=(10, 6)) dendrogram(linkage_matrix, labels=dist_df.index, leaf_rotation=45) plt.title('Stock Clustering Dendrogram (DTW Distance)') plt.ylabel('DTW Distance') plt.tight_layout() plt.savefig('clustering_dendrogram.png', dpi=300) plt.show()

运行后生成clustering_dendrogram.png。观察树状图:分支高度越低,说明两支股票 DTW 距离越小,形态越相似。例如,若“宁德时代”和“比亚迪”在高度 20 处合并,而“贵州茅台”在高度 80 处才与其他股票合并,说明新能源车产业链内部联动强于消费板块。

5.3 步骤二:设定阈值提取聚类标签,生成行业热力图

树状图只是视觉参考,要落地需设定距离阈值t切割。经验法则是:t取树状图中主要分支高度的 60%~70%。假设你观察到主分支在高度 50 处分离,则:

# 设定阈值,获取聚类标签(1,2,3...) clusters = fcluster(linkage_matrix, t=35, criterion='distance') # 创建聚类结果 DataFrame cluster_result = pd.DataFrame({ 'stock_code': dist_df.index, 'cluster_id': clusters }) print(cluster_result) # 生成热力图 plt.figure(figsize=(8, 6)) # 使用 seaborn 绘制,行列均为股票代码,值为 DTW 距离 import seaborn as sns sns.heatmap(dist_matrix, xticklabels=dist_df.index, yticklabels=dist_df.index, annot=True, fmt='.1f', cmap='viridis') plt.title('DTW Distance Heatmap with Clusters') plt.tight_layout() plt.savefig('dtw_heatmap_with_clusters.png', dpi=300) plt.show()

dtw_heatmap_with_clusters.png中,你会看到颜色越深(距离越小)的方块集中在对角线附近,且同一聚类内的股票形成色块——这就是行业联动的热力证据。项目截图图片6.png就是此图,其中用红色虚线框标出了“光伏设备”聚类(隆基、通威、阳光电源)。

5.4 步骤三:用聚类结果反哺投资逻辑,避免陷入“数字幻觉”

聚类结果不是终点,而是分析起点。例如,若cluster_result显示“东方财富”和“中信证券”聚为一类,但 DTW 距离高达 65.2(远高于同类平均 32.1),就要查原因:是券商股整体波动大?还是东方财富有互联网业务导致走势异质?这时应回看warping_path_plot.png,若路径高度弯曲,说明两者节奏根本不同,强行归为一类无意义。我的习惯是:每次聚类后,必抽样 2 组股票,手动打开warping_path_plot.pngsimilarity_plot.png对照看。如果 warping path 像毛线团,而折线图又不重合,那这个聚类就是噪声。从那以后我每次做 DTW 分析,都强制走一遍这个“眼见为实”步骤——代码可以骗人,但图不会。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询