在 DuckDB 中执行假设检验
你有没有想过,用 SQL 就能做假设检验?
今天我们就来聊聊,如何用 DuckDB 快速完成数据分析中的两个经典任务:
- 对分类变量做卡方检验
- 对连续变量做相关性分析
整个流程非常丝滑:
- 用 DuckDB 直接读取 CSV 或 Parquet 文件
- 用熟悉的 SQL 做聚合计算
- 把精简后的结果交给 Python 计算 p 值
- 连续变量甚至可以直接用 DuckDB 内置的
corr()函数搞定
看完你会发现自己又掌握了一个“快、准、狠”的数据分析利器!
下面用共享单车骑行记录来演示。
数据文件叫 bike_rides.csv,字段大概这些:
ride_iduser_type:月卡 / 单次is_weekend:是 / 否distance_km:骑行距离duration_min:骑行时长start_hour:开始小时bike_type:普通车 / 电助力车
1. 环境准备
# pip install duckdb pandas scipy
import duckdb
from scipy.stats import chi2_contingency
con = duckdb.connect()
DuckDB 的好处是,不用先建表,也不用把 CSV 导进数据库。直接查文件就行。
con.execute("""
SELECT *
FROM read_csv_auto('bike_rides.csv')
LIMIT 5
""").df()
| ride_id | user_type | is_weekend | distance_km | duration_min | start_hour | bike_type |
|---|---|---|---|---|---|---|
| 1 | 月卡 | 是 | 2.5 | 13 | 9 | 普通车 |
| 2 | 月卡 | 是 | 3.2 | 15 | 10 | 电助力车 |
| 3 | 月卡 | 是 | 1.8 | 10 | 14 | 普通车 |
| 4 | 月卡 | 是 | 4.5 | 19 | 16 | 电助力车 |
| 5 | 月卡 | 是 | 5.1 | 21 | 8 | 普通车 |
如果是 Parquet,就换成:
read_parquet('bike_rides.parquet')
这一步只是确认数据能读进来。真正做检验时,我们不会把全量数据拉到 Python,而是先用 DuckDB 做聚合。
2. 假设检验一:月卡用户和单次用户,周末骑行比例一样吗?
在我们探索数据的过程中,一个很自然的问题是:不同类型的用户在周末骑行的偏好上是否存在差异?
为了回答这个问题,我们可以借助分类变量的常用分析方法——卡方检验。
首先,我们设定一个简单的假设:
- H₀(原假设):用户类型与周末是否骑行之间没有关联。
- H₁(备择假设):用户类型与周末是否骑行之间存在某种关联。
这样的假设设定让我们可以更系统地探索数据背后的故事,而不是仅仅依赖直观感受。
接下来,我们就可以通过卡方检验来验证这些假设,看看数据是否支持我们拒绝原假设。
用 DuckDB 先聚合出列联表:
df = con.execute("""
SELECT
user_type,
SUM(CASE WHEN is_weekend = '是' THEN 1 ELSE 0 END) AS weekend,
SUM(CASE WHEN is_weekend = '否' THEN 1 ELSE 0 END) AS weekday
FROM read_csv_auto('bike_rides.csv')
GROUP BY user_type
ORDER BY user_type
""").df()
print(df)
| user_type | weekend | weekday |
|---|---|---|
| 单次 | 4.0 | 16.0 |
| 月卡 | 14.0 | 6.0 |
拿到列联表后,交给 scipy 做卡方检验:
table = df[['weekend', 'weekday']].values
chi2, p, dof, expected = chi2_contingency(table)
print(f"卡方统计量: {chi2:.4f}")
print(f"p 值: {p:.4f}")
卡方统计量: 8.1818
p 值: 0.0042
结果分析:
- p = 0.0042 < 0.05:拒绝 H₀,说明用户类型和周末骑行比例有关系。
- 卡方统计量为 8.1818,在显著性水平 α = 0.05 下达到显著,有足够证据表明不同用户类型(单次 vs 月卡)在周末/工作日骑行比例上存在显著差异。
DuckDB 没有内置卡方检验,所以这里的分工是:DuckDB 负责把大表聚合成小列联表,scipy 负责算检验。
这样不会把全量骑行记录拉到 Python,内存压力小很多。
3. 假设检验二:骑行距离和骑行时长相关吗?
接下来,我们看看骑行的距离和骑行的时长之间到底有没有什么关系?
其实吧,这个问题听起来挺直觉的——骑得久,自然骑得远嘛。
但作为一个喜欢数据的人,咱不能光靠感觉说话,我们得拿出点真凭实据来。
所以我们可以提出两个假设:
- 原假设(H₀):这两者之间啥关系都没有,也就是说,骑行距离和骑行时长完全是“各自为政”,互不干扰。
- 备择假设(H₁):它们之间其实是有关系的,而且这个关系还挺明显的,不是巧合。
接下来,我们就可以用一些统计方法来检验一下,看看到底哪个假设更靠谱。
比如说,我们可以做一下相关性检验,算算它们之间的相关系数,看看是不是显著不为零。
这个可以直接用 DuckDB 的 corr():
corr_df = con.execute("""
SELECT
corr(distance_km, duration_min) AS corr
FROM read_csv_auto('bike_rides.csv')
WHERE distance_km IS NOT NULL
AND duration_min IS NOT NULL
""").df()
print(corr_df)
计算出来的 corr=0.996897。
结果怎么读:
corr接近 1:强正相关,距离越远,时长越长。corr接近 -1:强负相关。corr接近 0:基本没有线性相关。
DuckDB 直接算相关系数,不用把两列数据全部拉到 Python。数据量大时,这一点很省事。
如果你还想要 p 值,可以再用 scipy.stats.pearsonr,但通常先看 corr() 就能判断方向和强度。
4. 为什么这套流程适合用 DuckDB
- 直接读
CSV / Parquet,不用先导入数据库。 SQL聚合快,适合先做分组、计数、求和。- 卡方检验只需要列联表,
DuckDB聚合后数据很小,再给scipy很合适。 - 相关性检验可以直接用
corr(),不用把全量数据搬到Python。 - 和
Python衔接顺,con.execute(...).df()直接拿DataFrame。
5. 小结
这篇文章用共享单车场景演示了两个常见假设检验:
- 分类变量比较:用户类型 vs 是否周末骑行,用卡方检验。
- 连续变量关系:骑行距离 vs 骑行时长,用 Pearson 相关系数。
核心套路是:
- 能用
SQL聚合的,先让DuckDB做。 - 需要统计检验的,再把小结果交给
Python。 - 能直接用
DuckDB内置函数的,比如corr(),就别把数据拉出来。
换成其他字段,比如“不同品牌单车是否影响骑行时长”“早晚高峰与骑行距离”,流程基本一样。

浙公网安备 33010602011771号