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 は 1 つの標本の記述統計を保持します。

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 になり、ソートにおけるその位置は未規定になります。コスト: 2 回のソートに 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")
}

平均は 1 つの遅い値に引っ張られて 22 になりますが、中央値、四分位数、MAD はそうなりません。以下の比較が中央値に基づいて構築されているのはそのためです。

対応のある比較

Decision

Decision は対応のある比較の判定です。

pub enum Decision {
  Faster
  Slower
  Equivalent
  Invalid
  Unknown
}
コンストラクタ意味
Faster中央値の相対差が −t-t 以下
Slower中央値の相対差が tt 以上
Equivalent中央値の相対差が −t-t と tt の間に厳密に収まる
Invalid2 つの配列の長さが異なる
Unknownペアが 1 つもない

ここで 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]

第 1 引数がベースライン、第 2 引数が候補です。結果は min⁡(∣b∣,∣c∣)\min(\lvert b\rvert, \lvert c\rvert) 個の要素を持ちます。長い方の配列の余った値は無視されるため、長さの不一致をエラーとしたい場合は自分で長さを確認してください。

compare_paired

compare_paired は、揃えられた 2 つの計時値の配列を実用的な閾値を用いて比較します。

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、区間のラベル。この関数はサイズ nn の再標本を復元抽出で BB 個引き、それぞれの中央値を取り、それらの中央値のタイプ 7 分位点を α/2\alpha/2 と 1−α/21-\alpha/2 で返します。ここで α=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]

最初の 6 つの引数は compare_paired と同じで、最後の 3 つはシード、再標本化の回数、パーセント単位の信頼水準です。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")
}