"""Distribution-level statistics for performance reports."""
from __future__ import annotations
import itertools
import math
import random
import statistics as stats
from dataclasses import dataclass
@dataclass(frozen=True)
class DistributionStats:
hl_overhead_percent: float
ci_low_percent: float
ci_high_percent: float
ks_p_value: float
mw_p_value: float
decision: str
def distribution_stats(
baseline: list[float],
observed: list[float],
overhead_threshold_percent: float,
alpha: float,
bootstrap_resamples: int,
permutation_resamples: int,
max_exact_permutations: int,
rng: random.Random,
) -> DistributionStats:
threshold = overhead_threshold_percent / 100.0
ratio = hodges_lehmann_ratio(baseline, observed)
ci_low, ci_high = bootstrap_hl_ratio_ci(
baseline,
observed,
alpha,
bootstrap_resamples,
rng,
)
ks_p_value = ks_same_distribution_p_value(
baseline,
observed,
permutation_resamples,
max_exact_permutations,
rng,
)
mw_p_value = mann_whitney_greater_p_value(
[value * (1.0 + threshold) for value in baseline],
observed,
permutation_resamples,
max_exact_permutations,
rng,
)
decision = distribution_decision(ci_low, ci_high, threshold, alpha, mw_p_value)
return DistributionStats(
hl_overhead_percent=(ratio - 1.0) * 100.0,
ci_low_percent=(ci_low - 1.0) * 100.0,
ci_high_percent=(ci_high - 1.0) * 100.0,
ks_p_value=ks_p_value,
mw_p_value=mw_p_value,
decision=decision,
)
def distribution_decision(
ci_low: float,
ci_high: float,
threshold: float,
alpha: float,
mw_p_value: float,
) -> str:
threshold_ratio = 1.0 + threshold
if ci_high <= threshold_ratio:
return "CI supports <= threshold"
if ci_low > threshold_ratio and mw_p_value <= alpha:
return "CI and rank test support > threshold"
if ci_low > threshold_ratio:
return "CI supports > threshold; rank test inconclusive"
if mw_p_value <= alpha:
return "rank test supports > threshold; CI overlaps"
return "inconclusive"
def hodges_lehmann_ratio(baseline: list[float], observed: list[float]) -> float:
ratios = [observed_value / baseline_value for observed_value in observed for baseline_value in baseline]
return stats.median(ratios)
def bootstrap_hl_ratio_ci(
baseline: list[float],
observed: list[float],
alpha: float,
resamples: int,
rng: random.Random,
) -> tuple[float, float]:
values = []
for _ in range(resamples):
baseline_sample = [rng.choice(baseline) for _ in baseline]
observed_sample = [rng.choice(observed) for _ in observed]
values.append(hodges_lehmann_ratio(baseline_sample, observed_sample))
values.sort()
return (
quantile_sorted(values, alpha / 2.0),
quantile_sorted(values, 1.0 - alpha / 2.0),
)
def ks_same_distribution_p_value(
baseline: list[float],
observed: list[float],
resamples: int,
max_exact_permutations: int,
rng: random.Random,
) -> float:
observed_stat = ks_statistic(baseline, observed)
pooled = baseline + observed
baseline_size = len(baseline)
total = math.comb(len(pooled), baseline_size)
if total <= max_exact_permutations:
at_least = 0
for indexes in itertools.combinations(range(len(pooled)), baseline_size):
baseline_indexes = set(indexes)
permuted_baseline = [pooled[index] for index in baseline_indexes]
permuted_observed = [
value for index, value in enumerate(pooled) if index not in baseline_indexes
]
if ks_statistic(permuted_baseline, permuted_observed) >= observed_stat:
at_least += 1
return at_least / total
at_least = 0
for _ in range(resamples):
sample = pooled[:]
rng.shuffle(sample)
permuted_baseline = sample[:baseline_size]
permuted_observed = sample[baseline_size:]
if ks_statistic(permuted_baseline, permuted_observed) >= observed_stat:
at_least += 1
return at_least / resamples
def ks_statistic(first: list[float], second: list[float]) -> float:
first_sorted = sorted(first)
second_sorted = sorted(second)
thresholds = sorted(set(first_sorted + second_sorted))
result = 0.0
first_index = 0
second_index = 0
for threshold in thresholds:
while first_index < len(first_sorted) and first_sorted[first_index] <= threshold:
first_index += 1
while second_index < len(second_sorted) and second_sorted[second_index] <= threshold:
second_index += 1
first_cdf = first_index / len(first_sorted)
second_cdf = second_index / len(second_sorted)
result = max(result, abs(first_cdf - second_cdf))
return result
def mann_whitney_greater_p_value(
baseline_shifted: list[float],
observed: list[float],
resamples: int,
max_exact_permutations: int,
rng: random.Random,
) -> float:
observed_stat = mann_whitney_u(observed, baseline_shifted)
pooled = observed + baseline_shifted
observed_size = len(observed)
ranks = midranks(pooled)
total = math.comb(len(pooled), observed_size)
if total <= max_exact_permutations:
at_least = 0
for indexes in itertools.combinations(range(len(pooled)), observed_size):
if u_from_rank_indexes(ranks, indexes, observed_size) >= observed_stat:
at_least += 1
return at_least / total
at_least = 0
for _ in range(resamples):
indexes = rng.sample(range(len(pooled)), observed_size)
if u_from_rank_indexes(ranks, indexes, observed_size) >= observed_stat:
at_least += 1
return at_least / resamples
def mann_whitney_u(first: list[float], second: list[float]) -> float:
ranks = midranks(first + second)
return u_from_rank_indexes(ranks, range(len(first)), len(first))
def midranks(values: list[float]) -> list[float]:
ordered = sorted(enumerate(values), key=lambda item: item[1])
ranks = [0.0] * len(values)
position = 0
while position < len(ordered):
end = position + 1
while end < len(ordered) and ordered[end][1] == ordered[position][1]:
end += 1
rank = (position + 1 + end) / 2.0
for index in range(position, end):
ranks[ordered[index][0]] = rank
position = end
return ranks
def u_from_rank_indexes(ranks: list[float], indexes, sample_size: int) -> float:
rank_sum = sum(ranks[index] for index in indexes)
return rank_sum - sample_size * (sample_size + 1) / 2.0
def quantile_sorted(values: list[float], probability: float) -> float:
if len(values) == 1:
return values[0]
rank = (len(values) - 1) * probability
lower = int(rank)
upper = min(lower + 1, len(values) - 1)
fraction = rank - lower
return values[lower] + (values[upper] - values[lower]) * fraction