首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Rust 量化统计实战系列 第 3 篇:描述性统计 · Polars 一行搞定均值、方差、偏度与分组对比

Rust 量化统计实战系列 第 3 篇:描述性统计 · Polars 一行搞定均值、方差、偏度与分组对比

作者头像
不吃草的牛德
发布2026-04-23 13:08:40
发布2026-04-23 13:08:40
540
举报
文章被收录于专栏:RustRust

你有 5000 只股票、10 年的日收益率数据,老板要你 5 分钟内给出:全市场平均收益、各行业差异、大市值 vs 小市值谁更稳。

你还在逐个计算、手动拼接?

一个 group_by + agg,全市场分组对比秒出结果。

这就是 Polars 描述性统计的威力——不是它算得快(当然也快),而是它让复杂分析变成一行表达式。


为什么描述性统计是量化第一课?

拿到数据,你第一个要回答的问题不是"能赚多少钱",而是数据长什么样

三个核心问题:

  1. 1. 中心在哪? 均值告诉你"平均"收益,但平均可能被极端值拉偏
  2. 2. 散得多开? 标准差量化波动,波动就是风险
  3. 3. 尾巴有多肥? 偏度和峰度暴露极端事件的概率

这三个问题搞不清,后面所有建模都是空中楼阁。

指标速查表

指标

金融含义

关键判断

均值

平均收益

正值≠赚钱(看分布)

标准差

波动率/风险

越大越不可预测

偏度

不对称性

负偏=暴跌概率更大

峰度

极端值概率

>3 = 肥尾(黑天鹅温床)

Sharpe

风险调整收益

>1 不错,>2 优秀

偏度是量化最容易忽略的指标。 正态分布假设偏度=0,但 A 股日收益率偏度常年为负——暴跌比暴涨更常见。忽略这一点,你的 VaR 就是错的。


Polars 一行流:全市场描述统计

先看最基础的——对一列收益率做完整描述统计:

代码语言:javascript
复制
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)
}

输出示例(全市场日收益率):

代码语言:javascript
复制
┌──────────┬──────────┬──────────┬────────────┬────────────┐
│ 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。 肥尾确认。这不是异常值——这是金融市场的常态。


分组对比:行业 × 市值的交叉分析

全市场统计是地基,分组对比才是楼房。

按行业分组

代码语言:javascript
复制
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)
}

输出:

代码语言:javascript
复制
┌──────────────┬──────────────┬────────────┬──────────┬──────────┬───────┐
│ 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  │
└──────────────┴──────────────┴────────────┴──────────┴──────────┴───────┘

科技行业收益最高,但波动也大;公用事业最稳,但收益最低。这就是风险-收益权衡的最直观体现。

按市值分组

代码语言:javascript
复制
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)
}

输出:

代码语言:javascript
复制
┌──────────────────┬──────────────┬────────────┬──────────────┐
│ 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 来源:

代码语言:javascript
复制
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 比率:三个计算陷阱

Sharpe 比率是量化最常用的指标,也是最容易算错的指标。

陷阱 1:无风险利率

代码语言:javascript
复制
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 偏高或偏低。

陷阱 2:年化因子

不同频率的收益率,年化方式不同:

频率

年化收益

年化波动

日频

(1+r)^252 - 1

σ × √252

周频

(1+r)^52 - 1

σ × √52

月频

(1+r)^12 - 1

σ × √12

波动率用 √N 年化,收益用复利年化——不是同一个公式

陷阱 3:负 Sharpe 的解读

Sharpe = (Rp - Rf) / σp

当 Rp < Rf 时,Sharpe 为负。此时 Sharpe 越小(越负)不代表风险越大——因为分母是波动率,波动率越小,负 Sharpe 的绝对值反而越大。

负 Sharpe 只能说明一件事:跑不赢无风险利率。 比较负 Sharpe 时,应直接比较超额收益,而非 Sharpe 值本身。


完整实战:从 Parquet 到分组统计报告

把前面的模块串起来,形成完整的数据流:

代码语言:javascript
复制
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")
}

运行结果:

代码语言:javascript
复制
数据量: 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.02149


核心要点回顾

  1. 1. describe() 是你的起手式——任何数据先看一眼全貌
  2. 2. group_by + agg 是分组统计的核心模式——行业、市值、时间维度随意组合
  3. 3. 偏度和峰度不是装饰——它们决定你的模型假设是否站得住脚
  4. 4. Sharpe 计算三个坑:利率匹配、年化方式、负值解读

描述性统计是量化的第一块基石。没有它,所有模型都是盲人摸象。

下一站:概率分布——用 Rust 验证你的收益到底是不是正态分布。


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust火箭工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么描述性统计是量化第一课?
    • 指标速查表
  • Polars 一行流:全市场描述统计
  • 分组对比:行业 × 市值的交叉分析
    • 按行业分组
    • 按市值分组
    • 行业 × 市值交叉分析
  • Sharpe 比率:三个计算陷阱
    • 陷阱 1:无风险利率
    • 陷阱 2:年化因子
    • 陷阱 3:负 Sharpe 的解读
  • 完整实战:从 Parquet 到分组统计报告
  • 核心要点回顾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档