Sample ratio mismatch check: was traffic split as configured? Returns the probability that random assignment alone would drift from the configured split at least as much as we observed. A tiny value (< 0.001 by convention) means the split is broken and the results can't be trusted.
(
observed: Sequence[int],
expected_shares: Sequence[float],
)
| 85 | |
| 86 | |
| 87 | def srm_p_value( |
| 88 | observed: Sequence[int], |
| 89 | expected_shares: Sequence[float], |
| 90 | ) -> float | None: |
| 91 | """Sample ratio mismatch check: was traffic split as configured? |
| 92 | |
| 93 | Returns the probability that random assignment alone would drift from the |
| 94 | configured split at least as much as we observed. A tiny value (< 0.001 by |
| 95 | convention) means the split is broken and the results can't be trusted. |
| 96 | ``None`` when the question is meaningless (no traffic, one variant). |
| 97 | """ |
| 98 | total = sum(observed) |
| 99 | if len(observed) < 2 or total == 0 or any(s <= 0 for s in expected_shares): |
| 100 | return None |
| 101 | |
| 102 | # Chi-squared statistic: total squared gap between observed and expected |
| 103 | # counts, scaled by what's expected. Bigger gap == bigger number. |
| 104 | statistic = sum( |
| 105 | (count - total * share) ** 2 / (total * share) |
| 106 | for count, share in zip(observed, expected_shares, strict=True) |
| 107 | ) |
| 108 | return _chi_squared_survival(statistic, degrees_of_freedom=len(observed) - 1) |
| 109 | |
| 110 | |
| 111 | def _chi_squared_survival(statistic: float, degrees_of_freedom: int) -> float: |
searching dependent graphs…