Module: LLMExperiment::Stats
- Defined in:
- lib/llm_experiment/stats.rb
Overview
Exact, not approximate. With five tasks per cell the normal approximation to the Mann-Whitney U is not trustworthy, and the whole permutation set is small enough to enumerate: C(10,5) is 252. So the p-values here are counted, not estimated.
Class Method Summary collapse
-
.exact_mann_whitney(a, b) ⇒ Object
Exact two-sided p: enumerate every way to split the pooled values into two samples of the observed sizes and count the splits at least as extreme.
- .median(values) ⇒ Object
-
.ranks(values) ⇒ Object
Mid-ranks, so ties do not silently inflate the statistic.
-
.u_statistic(a, b) ⇒ Object
U for sample a against sample b.
Class Method Details
.exact_mann_whitney(a, b) ⇒ Object
Exact two-sided p: enumerate every way to split the pooled values into two samples of the observed sizes and count the splits at least as extreme.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/llm_experiment/stats.rb', line 44 def exact_mann_whitney(a, b) n1 = a.size n2 = b.size return { u: nil, p: nil, note: "empty sample" } if n1.zero? || n2.zero? total = n1 + n2 combos = (1..total).reduce(1, :*) / ((1..n1).reduce(1, :*) * (1..n2).reduce(1, :*)) return { u: u_statistic(a, b), p: nil, note: "#{combos} permutations; too many to enumerate" } if combos > 200_000 pooled = a + b observed = u_statistic(a, b) mean_u = n1 * n2 / 2.0 observed_deviation = (observed - mean_u).abs at_least_as_extreme = 0 (0...total).to_a.combination(n1).each do |idx| set = idx.to_h { |i| [i, true] } left = idx.map { |i| pooled[i] } right = (0...total).reject { |i| set[i] }.map { |i| pooled[i] } at_least_as_extreme += 1 if (u_statistic(left, right) - mean_u).abs >= observed_deviation - 1e-9 end { u: observed, p: at_least_as_extreme.to_f / combos, permutations: combos } end |
.median(values) ⇒ Object
11 12 13 14 15 16 17 |
# File 'lib/llm_experiment/stats.rb', line 11 def median(values) return nil if values.empty? sorted = values.sort mid = sorted.size / 2 sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0 end |
.ranks(values) ⇒ Object
Mid-ranks, so ties do not silently inflate the statistic.
20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/llm_experiment/stats.rb', line 20 def ranks(values) indexed = values.each_with_index.sort_by { |v, _| v } result = Array.new(values.size) i = 0 while i < indexed.size j = i j += 1 while j + 1 < indexed.size && indexed[j + 1][0] == indexed[i][0] mid = ((i + 1) + (j + 1)) / 2.0 (i..j).each { |k| result[indexed[k][1]] = mid } i = j + 1 end result end |
.u_statistic(a, b) ⇒ Object
U for sample a against sample b.
35 36 37 38 39 40 |
# File 'lib/llm_experiment/stats.rb', line 35 def u_statistic(a, b) all = a + b r = ranks(all) rank_sum_a = r.first(a.size).sum rank_sum_a - (a.size * (a.size + 1) / 2.0) end |