Module: Perfgate::Statistics::MannWhitneyU
- Defined in:
- lib/perfgate/statistics/mann_whitney_u.rb
Overview
One-sided Mann-Whitney U test: estimates how likely it is that the candidate distribution is drawn from a "worse" (larger-valued) population than the baseline distribution, without assuming normality (spec section 16.3). Uses a normal approximation with a tie correction, adapted from the Milestone 0 spike (spikes/regression_injection.rb) that validated this approach against a seeded +20% duration regression.
Baseline never surfaces this p-value directly to users (spec 16.3: "do not expose p-values alone as user-facing proof") -- it is combined with a practical-significance threshold by Comparison::MetricDecision.
Class Method Summary collapse
-
.assign_tie_group(sorted, ranked, start_index) ⇒ Object
Finds the run of tied values starting at start_index, assigns them all the same averaged rank, and returns the index just past the run.
- .candidate_rank_sum(baseline, candidate) ⇒ Object
-
.one_sided_p(baseline, candidate) ⇒ Object
Returns p, the probability of observing rank sums this extreme (or more) under the null hypothesis of no difference, tested against the one-sided alternative that
candidatetends to be larger thanbaseline. -
.rank(pairs) ⇒ Object
Assigns tied (averaged) ranks to a list of [value, label] pairs, returning [value, label, rank] triples sorted by value.
- .standard_deviation_u(baseline_size, candidate_size) ⇒ Object
- .tie_group_end(sorted, start_index) ⇒ Object
-
.z_score(baseline, candidate) ⇒ Object
Standardized U statistic for the candidate sample, or nil when the null distribution has zero variance (e.g. every sample tied).
Class Method Details
.assign_tie_group(sorted, ranked, start_index) ⇒ Object
Finds the run of tied values starting at start_index, assigns them all the same averaged rank, and returns the index just past the run.
70 71 72 73 74 75 |
# File 'lib/perfgate/statistics/mann_whitney_u.rb', line 70 def assign_tie_group(sorted, ranked, start_index) end_index = tie_group_end(sorted, start_index) average_rank = ((start_index + 1) + (end_index + 1)) / 2.0 (start_index..end_index).each { |k| ranked[k] = sorted[k] + [average_rank] } end_index + 1 end |
.candidate_rank_sum(baseline, candidate) ⇒ Object
50 51 52 53 |
# File 'lib/perfgate/statistics/mann_whitney_u.rb', line 50 def candidate_rank_sum(baseline, candidate) ranks = rank(baseline.map { |v| [v, :baseline] } + candidate.map { |v| [v, :candidate] }) ranks.each_index.sum { |i| ranks[i][1] == :candidate ? ranks[i][2] : 0 } end |
.one_sided_p(baseline, candidate) ⇒ Object
Returns p, the probability of observing rank sums this extreme
(or more) under the null hypothesis of no difference, tested
against the one-sided alternative that candidate tends to be
larger than baseline. Smaller p is stronger evidence the
candidate is worse.
25 26 27 28 29 30 31 32 |
# File 'lib/perfgate/statistics/mann_whitney_u.rb', line 25 def one_sided_p(baseline, candidate) return 1.0 if baseline.empty? || candidate.empty? z = z_score(baseline, candidate) return 0.5 if z.nil? 0.5 * Math.erfc(z / Math.sqrt(2)) end |
.rank(pairs) ⇒ Object
Assigns tied (averaged) ranks to a list of [value, label] pairs, returning [value, label, rank] triples sorted by value.
57 58 59 60 61 62 63 64 65 |
# File 'lib/perfgate/statistics/mann_whitney_u.rb', line 57 def rank(pairs) sorted = pairs.sort_by { |value, _label| value } ranked = Array.new(sorted.size) index = 0 index = assign_tie_group(sorted, ranked, index) while index < sorted.size ranked end |
.standard_deviation_u(baseline_size, candidate_size) ⇒ Object
46 47 48 |
# File 'lib/perfgate/statistics/mann_whitney_u.rb', line 46 def standard_deviation_u(baseline_size, candidate_size) Math.sqrt(baseline_size * candidate_size * (baseline_size + candidate_size + 1) / 12.0) end |
.tie_group_end(sorted, start_index) ⇒ Object
77 78 79 80 81 |
# File 'lib/perfgate/statistics/mann_whitney_u.rb', line 77 def tie_group_end(sorted, start_index) end_index = start_index end_index += 1 while end_index + 1 < sorted.size && sorted[end_index + 1][0] == sorted[start_index][0] end_index end |
.z_score(baseline, candidate) ⇒ Object
Standardized U statistic for the candidate sample, or nil when the null distribution has zero variance (e.g. every sample tied).
36 37 38 39 40 41 42 43 44 |
# File 'lib/perfgate/statistics/mann_whitney_u.rb', line 36 def z_score(baseline, candidate) n1 = baseline.size n2 = candidate.size u_candidate = candidate_rank_sum(baseline, candidate) - (n2 * (n2 + 1) / 2.0) std_u = standard_deviation_u(n1, n2) return nil if std_u.zero? (u_candidate - (n1 * n2 / 2.0)) / std_u end |