stats API

Luna-Flow/mare_mark/stats 把计时数组转化为描述性摘要、成对比较、带种子的自助法区间以及过滤了离群值的视图。每个函数都是纯函数:它读取参数、分配结果,从不接触事件流。每个估计量背后的数学推导见 stats 设计。

源码:src/stats/stats.mbt。

import {
  "Luna-Flow/mare_mark/model",
  "Luna-Flow/mare_mark/stats",
}

描述性摘要

SummaryStats

SummaryStats 保存一个样本的描述性统计量。

pub struct SummaryStats {
  count : Int
  min : Double
  max : Double
  mean : Double
  median : Double
  mad : Double
  q1 : Double
  q3 : Double
  iqr : Double
  std_dev : Double
}

对于顺序统计量为 x(1)≤⋯≤x(n)x_{(1)} \le \dots \le x_{(n)} 的样本 x1,…,xnx_1, \dots, x_n,各字段为:

字段定义
countnn
min, maxx(1)x_{(1)}, x(n)x_{(n)}
meanxˉ=1n∑ixi\bar x = \frac{1}{n}\sum_i x_i
medianQ(0.5)Q(0.5)
q1, q3Q(0.25)Q(0.25), Q(0.75)Q(0.75)
iqrQ(0.75)−Q(0.25)Q(0.75) - Q(0.25)
mad∣xi−Q(0.5)∣\lvert x_i - Q(0.5)\rvert 的中位数,不带正态一致性因子 1.48261.4826
std_dev总体标准差 1n∑i(xi−xˉ)2\sqrt{\frac{1}{n}\sum_i (x_i - \bar x)^2}

Q(p)Q(p) 是线性插值的样本分位数(Hyndman–Fan 第 7 型):令 h=(n−1)ph = (n-1)p,

Q(p)=x(⌊h⌋+1)+(h−⌊h⌋) (x(⌊h⌋+2)−x(⌊h⌋+1)).Q(p) = x_{(\lfloor h \rfloor + 1)} + (h - \lfloor h \rfloor)\,\bigl(x_{(\lfloor h \rfloor + 2)} - x_{(\lfloor h \rfloor + 1)}\bigr).

这些字段在包外是只读的;其值由 summarize 产生。

summarize

summarize 为一个值数组计算 SummaryStats。

pub fn summarize(Array[Double]) -> SummaryStats

输入在排序前会被复制,因此调用者的数组保持原有顺序。空数组得到 count == 0,其余字段均为 0.0;读取其他字段前请先检查 count。非有限值不会被拒绝:一个 NaN 会使均值和标准差变为 NaN,且它在排序中的位置是未指定的。代价:两次排序需要 O(nlog⁡n)O(n \log n) 时间,额外空间为 O(n)O(n)。

test "summarize a sample" {
  let summary = @stats.summarize([4.0, 1.0, 3.0, 2.0, 100.0])
  inspect(summary.count, content="5")
  inspect(summary.median, content="3")
  inspect(summary.q1, content="2")
  inspect(summary.q3, content="4")
  inspect(summary.mad, content="1")
  inspect(summary.mean, content="22")
}

一个慢值就把均值拉到了 22;中位数、四分位数和 MAD 则不受影响。这就是下面的比较建立在中位数之上的原因。

成对比较

Decision

Decision 是成对比较的判定结果。

pub enum Decision {
  Faster
  Slower
  Equivalent
  Invalid
  Unknown
}
构造器含义
Faster相对中位数差值小于或等于 −t-t
Slower相对中位数差值大于或等于 tt
Equivalent相对中位数差值严格位于 −t-t 与 tt 之间
Invalid两个数组长度不同
Unknown没有任何配对

这里 tt 是以 practical_delta_pct 传入的实际阈值。Decision 在包外是只读的:你可以对它进行匹配,但不能构建它。

Interval

Interval 是一对边界,并标注了它所属的阶段。

pub struct Interval {
  low : Double
  high : Double
  mode : @model.IntervalMode
}

compare_paired 用成对差值的四分位距填充它;bootstrap_interval 和 compare_paired_with_bootstrap 用百分位自助法区间填充它。mode 是从调用者处复制来的标签;它不会改变计算。

Comparison

Comparison 是把候选与基线进行比较的结果。

pub struct Comparison {
  baseline_id : String
  candidate_id : String
  relative_delta_pct : Double
  speedup : Double
  interval : Interval
  decision : Decision
  valid_samples : Int
}
字段含义
relative_delta_pctr=100⋅med⁡(d)/med⁡(b)r = 100 \cdot \operatorname{med}(d) / \operatorname{med}(b),当 med⁡(b)=0\operatorname{med}(b) = 0 时为 0.0
speedupmed⁡(b)/(med⁡(b)+med⁡(d))\operatorname{med}(b) / (\operatorname{med}(b) + \operatorname{med}(d)),当 med⁡(d)=0\operatorname{med}(d) = 0 时为 1.0
interval成对差值的边界,单位与输入相同(例如 µs)
decision上文描述的 Decision
valid_samples配对数,min⁡(∣b∣,∣c∣)\min(\lvert b\rvert, \lvert c\rvert)

bb 是基线数组,cc 是候选数组,di=ci−bid_i = c_i - b_i 是成对差值。speedup 大于 1.0 表示候选更快。

paired_deltas

paired_deltas 用相同索引处的候选值减去基线值。

pub fn paired_deltas(Array[Double], Array[Double]) -> Array[Double]

第一个参数是基线,第二个是候选。结果有 min⁡(∣b∣,∣c∣)\min(\lvert b\rvert, \lvert c\rvert) 个元素:较长数组多出的值会被忽略,因此当长度不一致应视为错误时,请自行检查长度。

compare_paired

compare_paired 用实际阈值比较两个对齐的计时数组。

pub fn compare_paired(String, String, Array[Double], Array[Double], Double, @model.IntervalMode) -> Comparison

参数:基线 id、候选 id、基线值、候选值、以百分比表示的实际阈值 tt,以及区间标签。两个数组的索引 ii 必须描述同一个数据集、重复和区组。决策只使用点估计 rr;区间是差值的四分位距 [Qd(0.25),Qd(0.75)][Q_d(0.25), Q_d(0.75)],是描述性的,而不是置信区间。

test "paired comparison" {
  let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
  let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
  let comparison = @stats.compare_paired(
    "baseline",
    "candidate",
    baseline,
    candidate,
    2.0,
    @model.confirmatory_interval(),
  )
  inspect(comparison.relative_delta_pct, content="-9.5")
  inspect(comparison.speedup, content="1.1049723756906078")
  inspect(comparison.interval.low, content="-10")
  inspect(comparison.interval.high, content="-9")
  inspect(@stats.is_faster(comparison), content="true")
}

is_faster

is_faster 报告比较的决策是否为 Faster。

pub fn is_faster(Comparison) -> Bool

它为仪表盘和门禁提供便利。请在它旁边记录完整的 Comparison:false 同样涵盖 Slower、Equivalent、Invalid 和 Unknown。

自助法区间

BootstrapError

BootstrapError 解释为何无法计算自助法区间。

pub(all) enum BootstrapError {
  EmptySamples
  MismatchedPairs(Int, Int)
  InvalidResamples(Int)
  InvalidConfidence(Double)
  NonFiniteSample(Int)
}
构造器触发条件
EmptySamples没有可重抽样的内容
MismatchedPairs(baseline, candidate)成对数组长度不同
InvalidResamples(count)重抽样次数不是正数
InvalidConfidence(pct)置信度不是严格介于 0 和 100 之间的有限数
NonFiniteSample(index)index 处的值为 NaN 或无穷大

bootstrap_interval

bootstrap_interval 为样本中位数计算带种子的百分位自助法区间。

pub fn bootstrap_interval(Array[Double], UInt64, Int, Double, @model.IntervalMode) -> Result[Interval, BootstrapError]

参数:值、种子、重抽样次数 BB、以百分比表示的置信水平 γ\gamma,以及区间标签。该函数有放回地抽取 BB 个大小为 nn 的重抽样样本,取每个样本的中位数,并返回这些中位数在 α/2\alpha/2 和 1−α/21-\alpha/2 处的第 7 型分位数,其中 α=1−γ/100\alpha = 1 - \gamma/100。检查按空输入、重抽样次数、置信度、有限性的顺序进行,返回第一个失败。

结果只是其参数的函数:相同的值、种子、BB 和 γ\gamma 在每个目标上都给出相同的边界。边界总是满足 min⁡ixi≤\min_i x_i \le low ≤\le high ≤max⁡ixi\le \max_i x_i。种子 0 会被替换为一个固定的非零常数。代价:O(B nlog⁡n)O(B\, n \log n) 时间和 O(n+B)O(n + B) 空间。

test "bootstrap interval of a median" {
  let deltas = [-10.0, -9.0, -10.0, -9.0, -8.0, -9.0, -10.0, -10.0]
  let interval = @stats.bootstrap_interval(
    deltas,
    42UL,
    1000,
    95.0,
    @model.confirmatory_interval(),
  ).unwrap()
  inspect(interval.low, content="-10")
  inspect(interval.high, content="-9")
  let error = @stats.bootstrap_interval(
    deltas,
    42UL,
    1000,
    100.0,
    @model.confirmatory_interval(),
  )
  inspect(error is Err(@stats.BootstrapError::InvalidConfidence(_)), content="true")
}

compare_paired_with_bootstrap

compare_paired_with_bootstrap 即把四分位区间替换为成对差值中位数的自助法区间的 compare_paired。

pub fn compare_paired_with_bootstrap(String, String, Array[Double], Array[Double], Double, @model.IntervalMode, UInt64, Int, Double) -> Result[Comparison, BootstrapError]

前六个参数与 compare_paired 相同;后三个是种子、重抽样次数和以百分比表示的置信度。与 compare_paired 不同,长度不等是错误(MismatchedPairs),空数组也是错误(EmptySamples)。relative_delta_pct、speedup、decision 和 valid_samples 与 compare_paired 的结果相同;只有 interval 不同。区间的单位与输入相同,而决策以百分比表示。

test "paired comparison with a bootstrap interval" {
  let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
  let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
  let comparison = @stats.compare_paired_with_bootstrap(
    "baseline",
    "candidate",
    baseline,
    candidate,
    2.0,
    @model.confirmatory_interval(),
    7UL,
    2000,
    95.0,
  ).unwrap()
  inspect(comparison.decision is Faster, content="true")
  inspect(comparison.interval.low, content="-10")
  inspect(comparison.interval.high, content="-9")
}

离群值视图

filter_outliers

filter_outliers 按原始顺序返回离群值策略保留下来的值。

pub fn filter_outliers(Array[Double], @model.OutlierPolicy) -> Array[Double]
策略保留的值
ReportOnly所有值(一个副本)
TukeyFenceQ(0.25)−1.5 IQR≤x≤Q(0.75)+1.5 IQRQ(0.25) - 1.5\,\mathrm{IQR} \le x \le Q(0.75) + 1.5\,\mathrm{IQR}
MADTrim∣x−Q(0.5)∣≤3 MAD\lvert x - Q(0.5)\rvert \le 3\,\mathrm{MAD}

输入数组永远不会被修改。当超过一半的值相等时,MAD 为 0,MADTrim 只保留等于中位数的值。请把结果用作派生视图;保留原始观测。

test "outlier views" {
  let values = [10.0, 11.0, 10.0, 12.0, 11.0, 48.0]
  debug_inspect(
    @stats.filter_outliers(values, @model.OutlierPolicy::TukeyFence),
    content="[10, 11, 10, 12, 11]",
  )
  inspect(values.length(), content="6")
}