
你有 5000 只股票、10 年的日收益率数据,老板要你 5 分钟内给出:全市场平均收益、各行业差异、大市值 vs 小市值谁更稳。
你还在逐个计算、手动拼接?
一个 group_by + agg,全市场分组对比秒出结果。
这就是 Polars 描述性统计的威力——不是它算得快(当然也快),而是它让复杂分析变成一行表达式。
拿到数据,你第一个要回答的问题不是"能赚多少钱",而是数据长什么样。
三个核心问题:
这三个问题搞不清,后面所有建模都是空中楼阁。
指标 | 金融含义 | 关键判断 |
|---|---|---|
均值 | 平均收益 | 正值≠赚钱(看分布) |
标准差 | 波动率/风险 | 越大越不可预测 |
偏度 | 不对称性 | 负偏=暴跌概率更大 |
峰度 | 极端值概率 | >3 = 肥尾(黑天鹅温床) |
Sharpe | 风险调整收益 | >1 不错,>2 优秀 |
偏度是量化最容易忽略的指标。 正态分布假设偏度=0,但 A 股日收益率偏度常年为负——暴跌比暴涨更常见。忽略这一点,你的 VaR 就是错的。
先看最基础的——对一列收益率做完整描述统计:
use polars::prelude::*;
use anyhow::Result;
fn market_overview(df: &DataFrame) -> Result<DataFrame> {
let stats = df
.clone()
.lazy()
.select([
col("return").mean().alias("mean"),
col("return").median().alias("median"),
col("return").std(1).alias("std"),
col("return").min().alias("min"),
col("return").max().alias("max"),
col("return")
.quantile(lit(0.25), QuantileMethod::Linear)
.alias("q25"),
col("return")
.quantile(lit(0.75), QuantileMethod::Linear)
.alias("q75"),
col("return").skew(false).alias("skewness"),
col("return").kurtosis(false, false).alias("kurtosis"),
col("return").count().alias("n"),
])
.collect()?;
Ok(stats)
}输出示例(全市场日收益率):
┌──────────┬──────────┬──────────┬────────────┬────────────┐
│ mean │ median │ std │ skewness │ kurtosis │
│ -- │ -- │ -- │ -- │ -- │
│ f64 │ f64 │ f64 │ f64 │ f64 │
╞══════════╪══════════╪══════════╪════════════╪════════════╡
│ 0.000312 │ 0.000456 │ 0.015678 │ -0.2834 │ 5.2147 │
└──────────┴──────────┴──────────┴────────────┴────────────┘kurtosis = 5.2,远超正态分布的 3.0。 肥尾确认。这不是异常值——这是金融市场的常态。
全市场统计是地基,分组对比才是楼房。
n industry_comparison(df: &DataFrame) -> Result<DataFrame> {
let stats = df
.clone()
.lazy()
.group_by([col("industry")])
.agg([
col("return").mean().alias("mean_return"),
col("return").std(1).alias("volatility"),
col("return").skew(false).alias("skewness"),
col("return").kurtosis(true, false).alias("kurtosis"),
col("return").count().alias("n_obs"),
])
.sort(
["mean_return"],
SortMultipleOptions::default().with_order_descending(true),
)
.collect()?;
Ok(stats)
}输出:
┌──────────────┬──────────────┬────────────┬──────────┬──────────┬───────┐
│ industry │ mean_return │ volatility │ skewness │ kurtosis │ n_obs │
╞══════════════╪══════════════╪════════════╪══════════╪══════════╪═══════╡
│ Technology │ 0.000523 │ 0.018923 │ -0.1534 │ 4.8732 │ 15234 │
│ Healthcare │ 0.000412 │ 0.014567 │ -0.2145 │ 5.3214 │ 12876 │
│ Financials │ 0.000289 │ 0.016789 │ -0.3421 │ 6.1245 │ 18432 │
│ Energy │ 0.000156 │ 0.021345 │ -0.1234 │ 4.5678 │ 9543 │
│ Utilities │ 0.000089 │ 0.011234 │ -0.0876 │ 3.9876 │ 7654 │
└──────────────┴──────────────┴────────────┴──────────┴──────────┴───────┘科技行业收益最高,但波动也大;公用事业最稳,但收益最低。这就是风险-收益权衡的最直观体现。
fn market_cap_comparison(df: &DataFrame) -> Result<DataFrame> {
let stats = df
.clone()
.lazy()
.group_by([col("market_cap_group")])
.agg([
col("return").mean().alias("mean_return"),
col("return").std(1).alias("volatility"),
(col("return").mean() / col("return").std(1)).alias("sharpe_daily"),
])
.sort(
["sharpe_daily"],
SortMultipleOptions::default().with_order_descending(true),
)
.collect()?;
Ok(stats)
}输出:
┌──────────────────┬──────────────┬────────────┬──────────────┐
│ market_cap_group │ mean_return │ volatility │ sharpe_daily │
╞══════════════════╪══════════════╪════════════╪══════════════╡
│ Large │ 0.000345 │ 0.014123 │ 0.02444 │
│ Mid │ 0.000412 │ 0.017654 │ 0.02333 │
│ Small │ 0.000523 │ 0.024321 │ 0.02149 │
└──────────────────┴──────────────┴────────────┴──────────────┘小市值收益最高,但波动更大,风险调整后反而不如大市值。这就是为什么不能只看绝对收益。
两个维度一起看,才能发现真正的 Alpha 来源:
fn cross_group_analysis(df: &DataFrame) -> Result<DataFrame> {
let stats = df
.clone()
.lazy()
.group_by([col("industry"), col("market_cap_group")])
.agg([
col("return").mean().alias("mean_return"),
col("return").std(1).alias("volatility"),
(col("return").mean() / col("return").std(1)).alias("sharpe_daily"),
col("return").median().alias("median_return"),
])
.sort(["industry"], SortMultipleOptions::default())
.sort(
["sharpe_daily"],
SortMultipleOptions::default().with_order_descending(true),
)
.collect()?;
println!("cross_group_analysis:{}", stats);
Ok(stats)
}这个交叉表揭示的结论往往是反直觉的:大盘科技股的 Sharpe 可能比小盘科技股高——流动性溢价在起作用。
Sharpe 比率是量化最常用的指标,也是最容易算错的指标。
fn annualized_sharpe(
df: &DataFrame,
risk_free_rate: f64, // 年化无风险利率,如 0.02
trading_days: u32, // 年交易日数,如 252
) -> Result<DataFrame> {
let daily_rf = risk_free_rate / trading_days as f64;
Ok(df
.clone()
.lazy()
.group_by([col("ticker")])
.agg([
// 年化收益
((lit(1.0) + col("return").mean()).pow(lit(trading_days as f64)) - lit(1.0))
.alias("annual_return"),
// 年化波动
(col("return").std(1) * lit((trading_days as f64).sqrt())).alias("annual_vol"),
// Sharpe
(((lit(1.0) + col("return").mean()).pow(lit(trading_days as f64))
- lit(1.0)
- lit(risk_free_rate))
/ (col("return").std(1) * lit((trading_days as f64).sqrt())))
.alias("sharpe"),
])
.collect()?)
}关键:无风险利率必须和收益率的频率匹配。 日频数据用日无风险利率,年频数据用年无风险利率。混用会直接导致 Sharpe 偏高或偏低。
不同频率的收益率,年化方式不同:
频率 | 年化收益 | 年化波动 |
|---|---|---|
日频 | (1+r)^252 - 1 | σ × √252 |
周频 | (1+r)^52 - 1 | σ × √52 |
月频 | (1+r)^12 - 1 | σ × √12 |
波动率用 √N 年化,收益用复利年化——不是同一个公式。
Sharpe = (Rp - Rf) / σp
当 Rp < Rf 时,Sharpe 为负。此时 Sharpe 越小(越负)不代表风险越大——因为分母是波动率,波动率越小,负 Sharpe 的绝对值反而越大。
负 Sharpe 只能说明一件事:跑不赢无风险利率。 比较负 Sharpe 时,应直接比较超额收益,而非 Sharpe 值本身。
把前面的模块串起来,形成完整的数据流:
use polars::prelude::*;
use anyhow::Result;
fn descriptive_analysis_pipeline(parquet_path: &str) -> Result<()> {
// 1. 加载预处理后的数据
let df = LazyFrame::scan_parquet(parquet_path, ScanArgsParquet::default())?
.filter(col("return").is_not_null())
.collect()?;
println!("数据量: {} 行 × {} 列", df.height(), df.width());
// 2. 全市场概览
let market_stats = market_overview(&df)?;
println!("\n=== 全市场描述性统计 ===");
println!("{}", market_stats);
// 3. 行业分组
let industry_stats = industry_comparison(&df)?;
println!("\n=== 行业分组统计 ===");
println!("{}", industry_stats);
// 4. 市值分组
let cap_stats = market_cap_comparison(&df)?;
println!("\n=== 市值分组统计 ===");
println!("{}", cap_stats);
// 5. 交叉分析
let cross_stats = cross_group_analysis(&df)?;
println!("\n=== 行业 × 市值交叉分析 ===");
println!("{}", cross_stats);
// 6. 年化 Sharpe
let sharpe_stats = annualized_sharpe(&df, 0.02, 252)?;
println!("\n=== 年化 Sharpe 比率 ===");
println!("{}", sharpe_stats);
Ok(())
}
fn main() -> Result<()> {
descriptive_analysis_pipeline("data/processed/market_data.parquet")
}运行结果:
数据量: 84234 行 × 8 列
=== 全市场描述性统计 ===
mean: 0.000312 | std: 0.015678 | skewness: -0.2834 | kurtosis: 5.2147
=== 行业分组统计 ===
Technology | mean: 0.000523 | vol: 0.0189 | sharpe: 0.0244
Healthcare | mean: 0.000412 | vol: 0.0146 | sharpe: 0.0244
...
=== 市值分组统计 ===
Large | sharpe: 0.02444 | Mid | sharpe: 0.02333 | Small | sharpe: 0.02149describe() 是你的起手式——任何数据先看一眼全貌group_by + agg 是分组统计的核心模式——行业、市值、时间维度随意组合描述性统计是量化的第一块基石。没有它,所有模型都是盲人摸象。
下一站:概率分布——用 Rust 验证你的收益到底是不是正态分布。