duckdb_stats: 64 个宏补齐 DuckDB 统计推断层
仓库地址:https://github.com/alitrack/duckdb_stats
DuckDB 的聚合和分布函数已经很全,均值、方差、分位数、t 检验这些开箱即用。但真到写分析报告的时候,缺口马上露出来:置信区间怎么算?效应量呢?事后检验呢?功效和样本量呢?这些"推断统计"层的东西,DuckDB 里一个都没有。
我给 DuckDB 补了这一层,仓库本周已开源。一个社区扩展加一个 SQL 文件,64 个宏,零编译。
● ● ●
先看能干什么
装好之后(两条命令,后面讲),这些查询直接跑:
-- 均值的置信区间:返回 lower, upper, level
SELECT ci_mean(array_agg(x ORDER BY id)) FROM t;
-- 单因素方差分析
SELECT * FROM oneway_anova(
(SELECT array_agg(x ORDER BY id) FROM t),
(SELECT array_agg(grp ORDER BY id) FROM t));
-- 返回 F, p_value, df_between, df_within, eta_squared
-- 效应量:Cohen's d(两独立样本)
SELECT cohens_d_2samp(array_agg(a ORDER BY id),
array_agg(b ORDER BY id)) FROM t;
-- Fisher 精确检验,双侧 p 值
SELECT fisher_exact_p(20, 10, 5, 15);
-- 两样本 t 检验的功效
SELECT power_2samp(50, 0.5);
-- Tukey 事后检验
SELECT * FROM tukey_hsd(
(SELECT array_agg(x ORDER BY id) FROM t),
(SELECT array_agg(grp ORDER BY id) FROM t));
全部 64 个宏的签名和返回字段,见仓库的 docs/FUNCTION_REFERENCE.md。
● ● ●
stats_fill.sql 一个文件,10 个分区:
这些正是 R 和 SciPy 里最常被用到、而 DuckDB 完全空白的那一层。
● ● ●
为什么对标 statcpp
动手之前需要一个完整的参照系。statcpp 是 GitHub 上一个 C++ 统计库,386 个函数、30 个功能模块,从描述统计到生存分析都有。我把它当作清单,逐模块核对 DuckDB 三层(内核 + stats_duck 扩展 + duckdb-ml 扩展)能覆盖多少。
结论:386 个函数,每个都有落点。
逐模块矩阵在 docs/COVERAGE.md(30 行,每行都在真实 DuckDB 1.5.x 上实测过)。这就是仓库起名 duckdb_stats 的意思:不是又一个统计函数集,是把 DuckDB 的统计能力补到"能对上 R"的位置。
● ● ●
怎么对拍验证的
64 个宏不是"写完能跑"就完事。tests/ 里有一套对拍:gen_refs.py 用固定随机种子生成参考值,verify_fill.py 加载全部 64 个宏,对 scipy 逐项断言。
本次发布前在 WSL 里完整重跑了一遍:
宏创建:64/64 ok,0 失败 断言核对:58 项 vs scipy 1.18,TOTAL FAILS: 0
抽样看几个对拍结果(SQL 宏 vs scipy):
全表数字,前 8 位有效数字逐位一致。复跑命令在仓库 README 的 Verification 一节,任何机器五分钟可以复现。
● ● ●
安装:两条命令
INSTALL stats_duck FROM community;
LOAD stats_duck;
-- 再执行仓库里的 stats_fill.sql(64 条 CREATE OR REPLACE MACRO)
Python 用户更省事,一行引导:
from duckdb_stats_setup import stats_connect
con = stats_connect() # 自动 INSTALL、LOAD、挂载全部宏
DuckDB 1.4 及以上,跨平台,零编译。要在同一个连接里做机器学习,stats_connect(ml_extension=...) 可以再挂一层 duckdb-ml(34 个算法的 Rust 扩展,kmeans、Cox、ARIMA、XGBoost 都在里面)。
● ● ●
几个踩出来的坑
写 DuckDB 宏和写普通 SQL 不一样,有几条是用失败换来的:
- 01
DuckDB 列表下标从 1 开始,不是 0。遍历要写 unnest(range(1, len(l)+1))。 - 02
多个 array_agg 是彼此独立的聚合,行序不对齐。分组类宏必须共享同一个 ORDER BY 键,比如 array_agg(x ORDER BY id) 和 array_agg(grp ORDER BY id)。 - 03
表宏的入参必须是标量子查询,不能裸聚合,也不能写双层 FROM。 - 04
stats_duck 的 pf/pt/pchisq 是下尾 CDF,要上尾 p 值得写 1 - pf(...)。
这几条 README 里都有,初次写 DuckDB 宏的人可以直接抄作业。
● ● ●
诚实框
Tukey 和 Dunnett 的 p 值是正态近似(DuckDB 没有 studentized range 分布的 CDF),小样本下偏"激进",标记为 APPROX。scheffe_p 是精确的。 30 个模块里 2 个明确不做:数值工具(内部 helper)、高级缺失数据(多重插补超出 SQL 扩展的合理边界)。做不了的事写清楚,比硬凑一个近似好。 对标是"覆盖"不是"等价":386 个函数每个都有落点,但个别落点是近似解或可选层。
● ● ●
为什么不写 Rust 扩展
我评估过三条路。Lua 路线(duckdb-luajit)实测过:有状态聚合做不了,标量函数比内置慢约 7 倍,放弃。Rust 包装 statcpp 只在需要单一闭源依赖或 386 函数完全等价时才值得。现在这条路:一个社区扩展加一个 SQL 文件,零编译、跨平台、可读可改,宏本身就是文档。
统计需求超出这层的时候(Mahalanobis 距离、logrank、偏自相关),再上 Rust 也不迟——补的顺序和理由都写在 COVERAGE.md 里。
● ● ●
参考来源
https://github.com/alitrack/duckdb_stats
https://github.com/mitsuruk/statcpp
https://github.com/kometa-ai/kolistat
https://duckdb.org/community_extensions/list_of_extensions