跳转至

时间序列距离与 TigerBeetle:从相似度度量到金融账本

这一章前半回答「两条时间序列有多像」,后半回答「钱应该怎么存」。 前半是数据挖掘:六种距离度量,快的 O(n) 怕时间错位,能错位的贵几千倍; 三种摘要方法在精度和速度之间找平衡。后半是工程流程:把这些算法做成一个 可发布的 Go 包(workspace → 文档 → tag → go get),再用它找出 8 条真实序列 里的「最近邻居」。最后转头 TigerBeetle——一个把复式记账焊进内核的数据库。 读完你会拿到「度量怎么选、包怎么发、专用数据库长什么样」的三件套。

1. 这一章在原书里的位置

原书第 12 章,第三部分的中段。第 10、11 章造语言(结构),本章和下一章做 数据分析(计算)。章首把两件事缝在一起:金融账本本质上也是时间序列—— 一条不可变的、只追加的事件流;TigerBeetle 甚至能查「任意过去时刻的余额」, 把时间维度变成了头等公民1。

2. 顶层全景:六种度量 × 两种代价

  锁定步长(lock-step,O(n))        弹性(elastic,≥O(n×m))
  ├─ Euclidean: 平方差开方          ├─ DTW:时间轴可伸缩对齐(可不同长)
  ├─ Manhattan: 绝对差求和           ├─ LCSS:epsilon 内的最长公共子序列
  └─ Chebyshev: 最大单点差           └─ MPdist:子序列最近邻距离的第 5 百分位

  贵的补救:摘要(降长度)
  PAA(分段平均,最准) / Stepping(每 k 取一,最快会丢尖峰) / 随机采样(最糙)

图说:选择树只有两层——「时间对得上吗?」决定用哪一类; 「数据太长吗?」决定要不要先摘要。benchmark 把这张表的代价列成了数字(§6)。

3. 六种度量的脾气

锁定步长三兄弟:快,但怕时间错位

Euclidean:把每条序列当成高维空间里的一个点,距离 = 对应点差值的 平方和再开方。快、好懂,但只接受等长序列(书上的取舍:不等长时用较短者的长度截齐); 对噪声、时间平移、量纲差异都敏感2。

Chebyshev:只看对应点差值里的最大绝对值。尖峰敏感派—— 「任一点上的最大偏差」比累积偏差更要紧时用它3。

Manhattan:绝对差直接求和。不平方便,离群点不像 Euclidean 那样被平方放大, 「城市街区」直觉4。同样要求等长。

弹性三杰:容忍错位,代价是几千倍

LCSS:换一个视角——不比「差多少」,比「有多长的子序列能对上」: 两点差值在 epsilon 阈值内就算匹配。对噪声、空洞、时间错位鲁棒; 相似度 = 匹配点占比,距离 = 1 − 相似度(用之前记住这个方向, 「距离越小越像」)5。

DTW: Gold standard——允许「时间轴弯一弯」:第 i 点对上的未必是第 i 点, 算法找累计代价最小的对齐路径。同一句话说两遍、一慢一快,Euclidean 抓瞎 (峰值对不齐),DTW 把快的拉长相配。原生支持不同长度。两个代价: 填满 n×m 的代价矩阵(O(n×m),万点序列 = 一亿次运算); 不约束时会病态扭曲(一个点映射到几百个点)——业界的约束是 Sakoe-Chiba 带状窗或 Itakura 平行四边形,限制路径离对角线多远6。

MPdist:最年轻也最贵。思想一句话:真实数据很脏,两段序列整体不像, 不代表里面没有共同的形状。做法:把两条序列各切成重叠小窗,互相找 每个子序列的最近邻(AB-join 与 BA-join),合并距离列表, 取第 5 百分位——把最差的 95% 匹配直接扔掉, 只问「最好的那部分共享子结构有多像」。对尖峰、数据缺失、 头部尾部的垃圾数据近乎免疫;代价是每个子序列一次最近邻,O(n²) 起步7。

4. 主线例子:两个十元序列,六种度量各说一遍

书上的 app/main.go 用两条小序列把六种度量各跑一遍——这组数字值得整段记住:

  A = [1,2,3,4,5,6,7,8,9,10]
  B = [1,2,3,5,5,6,7,8,9,12]     ← 第 4、5、10 个位置不一样

  Euclidean: 2.2361     = √((0+0+0)² + (-1)² + 0² + … + 2²) = √(1+4)……² 总和平方式开方
  Manhattan: 3.0000     = 0+0+0+1+0+0+0+0+0+2,绝对差直接加
  Chebyshev: 2.0000     = max(|4-5|, |5-5|, |10-12|) = max(1, 0, 2) = 2
  DTW(w=2):  3.0000     ← 带窗约束的代价矩阵累计
  LCSS(d=2,ε=1): 0.1000 ← 1 - 匹配占比
  MPdist(w=3): 0.0000   ← 两序列共享的子序列结构几乎一致

图说:六个数字六种视角——Chebyshev 盯住最差一点(2),Manhattan 累计一切(3), MPdist 说「骨子里是一样的」(0)。选度量 = 选你想让哪种差异说话8。

5. 摘要:先变短,再比对

三种降长手法的取舍(书上明说)9:

方法 做什么 取舍
PAA 均分成段、每段取平均 保趋势最准;均值要算,稍慢
Stepping 每 k 个点留一个 最快、值原样不动;尖峰落在步距之间就没了(混叠)
随机采样 随机取 k 个、保持原顺序 最快;只保分布不保形状,大 dataset 的糙近似

「随机但保序」是个容易被忽略的细节:索引乱取、值按时间排回去, 形状才不塌成噪声9。

6. 把它做成一个真包:workspace → 文档 → 发布

这一步把第 6 号拆解的「workspace 概念」走成完整动线10:

  1. 本地联调:go mod init github.com/mactsouk/tslib(库)+ go mod init app(使用者),go work init ./tslib ./app 缝合—— import 写的是将来发布的路径,解析却走本地,GitHub 上那行代码还不存在也没关系;
  2. 文档即注释:函数上方的 // 注释直接进生态——IDE 悬停、go doc tslib.Stepping 命令行、godoc -http=:6060 本地文档站,三层同一来源;
  3. 发布:git init → add → commit → remote add → push --force → git tag v0.1.0 → push tag;消费方 go get github.com/mactsouk/tslib@v0.1.0 拿到带版本的模块。go.sum 的哈希校验从第 9 号拆解的依赖方视角在这里闭环;
  4. benchmark 定版(distances_test.go,M4 Pro)11:
  Euclidean   235.6 ns/op     ┐
  Manhattan   236.4 ns/op     ├ 线性度量:不到四分之一微秒
  Chebyshev   874.6 ns/op     ┘ (max 的内循环比求和慢约 4 倍)
  DTW         762,658 ns/op   ┐ 弹性度量:0.77–1.57 毫秒
  LCSS        867,052 ns/op   ┤ = 比线性慢 3000–6000 倍
  MPdist    1,573,517 ns/op   ┘ (14 核并行都救不了 O(n²) 的体量)

7. 最近邻实战:八条序列的「谁像谁」

findNN.go 吃 8 个 .gz 序列文件,每个都对其他 7 个算六种距离找最小。 输出藏着三层信息12:

  1. 互配对模式:ts1↔ts2、ts3↔ts4、ts5↔ts6 互为最近邻—— 数据集里显然有三对「同源」序列,六种度量全部认可;
  2. 度量的分歧:ts7 上 Euclidean/Manhattan/DTW 都说 ts6 最近, MPdist 独说 ts8——整体走势 ts7 像 ts6,但局部形状(motif)更像 ts8; 只有子序列级度量看得见这层关系;
  3. 配置陷阱:ts5–ts8 的 LCSS 全部报 1.00(零相似)且「最近邻」是 ts1—— 这是回退默认值的假象:这几条数据量级在百万,epsilon=1.0 的匹配阈值 严到没有任何点对能对上。度量没有万能的默认参数,量纲决定一切。

8. TigerBeetle:为「钱」而生的数据库

为什么不用 PostgreSQL 记帐?章首的论证:账户、转账、复式记账在通用库里 不存在——得自己拿表和事务模拟,行锁手动管、余额查询写得像个学术研究, 并发稍大就「慢查询或者悄悄坏掉的账本」13。TigerBeetle 的思路是把 金融原语下沉进数据库内核:

  • 复式记账内建:每笔 transfer 有 DebitAccountID 和 CreditAccountID, 引擎保证借等于贷、钱不生不灭——通用 SQL 库把这一切留给应用层;
  • 128 位 ID:ID、金额全是 Uint128(全球唯一 + 大数);
  • Ledger 作为命名空间:账户只能与同 Ledger 的账户互转 (USD 与 EUR 隔离);
  • 批次 + 两层错误:一次发一批;整体网络错误 vs 批内逐条错误分开返回 (AccountExists 被视为「幂等成功」,初始化脚本可以安全重跑);
  • transfer ID 幂等:重试不会重复扣款;
  • 工程激进:shared-nothing、io_uring 绕过 OS、 确定性模拟测试(fuzz 掉「模拟数年网络故障」)—— 「正确性不是测出来的,是设计出来的」的活标本14。

docker-compose 的巧思(init 容器 + service_completed_successfully): init 容器负责 format 数据文件(带 seccomp:unconfined、IPC_LOCK 这些 io_uring 需要的高权限),主容器依赖它成功退出后才启动——没有这个顺序, 主服务第一次启动必崩(数据文件还不存在)15。Go 侧三小步:connect (集群 ID 必须和 format 时的 --cluster 一致)→ createAccounts → CreateTransfers(id=100,账户 1→2,金额 100)→ 打印 Transfer of 100 units successful.16。

9. 作者的判断与证据

判断 性质 依据
度量选择 = 「时间对得上吗 × 数据多长」 作者的决策框架 六度量分两类 + 摘要层279
DTW 是形状比对的金标准但有病态风险 领域共识 Sakoe-Chiba/Itakura 约束段6
PAA/Stepping/采样各有明确代价 作者对比 三方法取舍表9
benchmark:弹性比线性慢 3000–6000× 实测 六个 ns/op 数字11
金融系统该用专用账本库 作者立场(TigerBeetle 官方叙事) 通用库的三宗罪13;这是产品选型主张,需按场景验证
TigerBeetle 的确定性模拟是可靠性工程样板 作者推崇(事实陈述) 章内介绍14

10. 边界与局限

  • DTW/LCSS/MPdist 的约束参数(窗宽、epsilon、百分位)全是魔法数—— nearest neighbor 一节的 LCSS 翻车就是教训,书里给了「试试归一化」一个方向, 没有展开(无量纲化方法本身不在本章)。
  • MPdist 没有给近似算法:O(n²) 的最近邻全配对在百万点序列上不可行, 实际工程的 MASS/STUMPY 类 FFT 加速一句未提。
  • 包发布流程里 --force push 一句带过风险(会覆盖远端历史), 对新手是个该被拦一下的操作。
  • TigerBeetle 只演示了写:查询余额、过滤查询、多 transfer 原子批都没展开; 「能查任意历史时刻余额」这个卖点在代码里没有兑现。
  • 时间序列数据本身的读取(.gz 解析)没有展示——findNN 的输入怎么变成 []float64 这一环在转码文本里缺席。

11. 可带走的

  1. 度量两问:时间对得上吗(锁定 vs 弹性)?数据多长(要不要摘要)?
  2. Euclidean 快而脆;Chebyshev 盯尖峰;Manhattan 抗离群; LCSS 容忍噪声(距离 = 1 − 匹配率);DTW 弯时间轴;MPdist 只看最好的 5%。
  3. 代价差是数量级的:线性纳秒级,DTW/LCSS 毫秒级,MPdist 再翻倍—— 大数据先摘要(PAA 最稳、Stepping 最快会混叠、采样最糙)。
  4. 包工程动线:go.work 本地假扮已发布路径 → 注释即文档(go doc/godoc 三层)→ tag 发版 → go get 验证。
  5. 最近邻输出要会读:互配对 = 同源;度量分歧 = 不同视角各有真相; 全员 1.00 的回退值 = 参数错了,不是数据无关。
  6. TigerBeetle:复式记账在内核、借必贷、Ledger 隔离、ID 幂等、 批次两层错误;init 容器 + depends_on condition 是带状态服务的 docker-compose 标准姿势。

12. 原文地图

主题 原书章 原文位置
金融账本 = 时间序列 Using TigerBeetle(章首论证) text/118-ch12.txt:18(搜「financial systems」) · text/123-fm-using-tigerbeetle-for-financial-accounting.txt:16(搜「immutable events」)
Euclidean/Chebyshev/Manhattan Time series distances text/120-fm-time-series-distances.txt:12(搜「Euclidean distance」) · :30(搜「Chebyshev」) · :47(搜「Manhattan」)
LCSS 与「1 减相似度」 Time series distances text/120-fm-time-series-distances.txt:71(搜「within a given threshold epsilon」) · :77(搜「1 minus」)
DTW 与约束窗 Time series distances text/120-fm-time-series-distances.txt:86(搜「non-linear alignment」) · :106(搜「O(n×m)」) · :110(搜「Sakoe-Chiba」)
MPdist 第 5 百分位 Time series distances text/120-fm-time-series-distances.txt:115(搜「MPdist」) · :133(搜「5th percentile」)
复杂度权衡与摘要三法 Time series distances text/120-fm-time-series-distances.txt:145(搜「linear O(n)」) · :166(搜「PAA」) · :177(搜「Stepping」)
workspace 与包结构 Creating a Go package text/121-fm-creating-a-go-package.txt:27(搜「Workspaces」) · :57(搜「目录结构」)
godoc 三层 Creating a Go package text/121-fm-creating-a-go-package.txt:203(搜「go doc tslib.Stepping」)
发布与 tag Creating a Go package text/121-fm-creating-a-go-package.txt:240(搜「git init」) · :276(搜「git tag v0.1.0」)
六度量 benchmark Creating a Go package text/121-fm-creating-a-go-package.txt:315(搜「BenchmarkEuclidean」) · :329(搜「3,000 to 6,000 times slower」)
两序列六度量输出 Creating a Go package text/121-fm-creating-a-go-package.txt:129(搜「2.2361」)
最近邻三层发现 Finding the nearest neighbor text/122-fm-finding-the-nearest-neighbor-of-a-time-series.txt:16(搜「ts2.gz (4.07)」) · text/123-fm-using-tigerbeetle-for-financial-accounting.txt:6(搜「identifying ts8.gz」) · :9(搜「1.00 represents zero similarity」)
docker-compose init 容器 Using TigerBeetle text/123-fm-using-tigerbeetle-for-financial-accounting.txt:53(搜「tigerbeetle-init」) · :96(搜「service_completed_successfully」)
Go 连接与写入 Using TigerBeetle text/123-fm-using-tigerbeetle-for-financial-accounting.txt:154(搜「NewClient」) · :205(搜「CreateAccounts」) · :266(搜「CreateTransfers」) · :308(搜「Transfer of 100 units」)


  1. 出处:「Time Series Distances」(text/118-ch12.txt:18,搜「financial systems」)与「Using TigerBeetle for financial accounting」章首(text/123-fm-using-tigerbeetle-for-financial-accounting.txt:16,搜「immutable events」;:21,搜「any past timestamp」)。 ↩

  2. 出处:「About the Euclidean distance」(text/120-fm-time-series-distances.txt:12,搜「Euclidean distance」;:24,搜「same length」;:26,搜「length of the smaller vector」)。 ↩↩

  3. 出处:「About the Chebyshev distance」(text/120-fm-time-series-distances.txt:31,搜「maximum metric」;:36,搜「maximum absolute difference」)。 ↩

  4. 出处:「About the Manhattan distance」(text/120-fm-time-series-distances.txt:48,搜「city-block」;:53,搜「robust to outliers」)。 ↩

  5. 出处:「About LCSS」(text/120-fm-time-series-distances.txt:71,搜「within a given threshold epsilon」;:76,搜「ratio of matched points」;:77,搜「1 minus」)。 ↩

  6. 出处:「About DTW」(text/120-fm-time-series-distances.txt:86,搜「non-linear alignment」;:88,搜「same word spoken at different speeds」;:105,搜「O(n×m)」;:110,搜「Sakoe-Chiba band」)。 ↩↩

  7. 出处:「About MPdist」(text/120-fm-time-series-distances.txt:116,搜「Matrix Profile」;:128,搜「AB-join」;:133,搜「5th percentile」;:141,搜「O(n2)」)。 ↩↩

  8. 出处:「Creating a Go package」(text/121-fm-creating-a-go-package.txt:93,搜「func main」;:124,搜「Euclidean: 2.2361」;:129,搜「2.2361」起六行输出)。 ↩

  9. 出处:「Creating a summary of a time series」(text/120-fm-time-series-distances.txt:167,搜「PAA」;:173,搜「chronological order」;:177,搜「Stepping」)与取舍总结(text/121-fm-creating-a-go-package.txt:4,搜「highest fidelity」)。 ↩↩↩↩

  10. 出处:「Working with workspaces」(text/121-fm-creating-a-go-package.txt:27,搜「Workspaces」;:52,搜「go work init」;:57,搜「目录结构」;:86,搜「Because of go.work」)。 ↩

  11. 出处:「Benchmarking the distances」(text/121-fm-creating-a-go-package.txt:315,搜「BenchmarkEuclidean」;:320,搜「1573517」;:329,搜「3,000 to 6,000 times slower」)。 ↩↩

  12. 出处:「Finding the nearest neighbor of a time series」(text/122-fm-finding-the-nearest-neighbor-of-a-time-series.txt:16,搜「ts2.gz (4.07)」;:39,搜「mutual pairings」)与发现总结(text/123-fm-using-tigerbeetle-for-financial-accounting.txt:6,搜「identifying ts8.gz」;:9,搜「1.00 represents zero similarity」;:13,搜「normalization」)。 ↩

  13. 出处:「Time Series Distances」章首(text/118-ch12.txt:21,搜「must build from scratch」;:22,搜「row locking」;:24,搜「subtle concurrency bugs」)。 ↩↩

  14. 出处:「Using TigerBeetle for financial accounting」(text/123-fm-using-tigerbeetle-for-financial-accounting.txt:35,搜「double-entry accounting」;:38,搜「debits match credits」;:43,搜「io_uring」;:46,搜「deterministic simulation」)。 ↩↩

  15. 出处:「Using TigerBeetle for financial accounting」(text/123-fm-using-tigerbeetle-for-financial-accounting.txt:53,搜「tigerbeetle-init」;:63,搜「format」;:82,搜「seccomp:unconfined」;:96,搜「service_completed_successfully」)。 ↩

  16. 出处:「Connecting to TigerBeetle」与「Writing data to TigerBeetle」(text/123-fm-using-tigerbeetle-for-financial-accounting.txt:154,搜「NewClient」;:171,搜「--cluster=0」;:205,搜「CreateAccounts」;:232,搜「AccountExists」;:266,搜「CreateTransfers」;:292,搜「refuse to execute it a second time」;:308,搜「Transfer of 100 units」)。 ↩