Cheat sheet
Every test in the course on one printable page: its formula, when to use it, and the Python (scipy.stats and statsmodels) and Excel that run it on the course's own data. The core formulas are the tutorial's Appendix A. Every value is the lab's engine's, and the tests run every line against it, with each library's own conventions.
The Python lines assume these imports:
import numpy as np
from scipy import stats
from scipy.stats import norm, t, chi2, f
from statsmodels.stats.proportion import proportions_ztest, proportion_effectsize, confint_proportions_2indep
from statsmodels.stats.rates import test_poisson_2indep
from statsmodels.stats.contingency_tables import mcnemar
from statsmodels.stats.weightstats import ttost_ind
from statsmodels.stats.multitest import multipletests
from statsmodels.stats.power import TTestPower, TTestIndPower, NormalIndPowerThe logic of a test
The one-sample z test (σ known)
Taught in chapter 3: The p-valueWhen to use it: When σ comes from a long record of the process, not from this sample. Reject H₀ when p ≤ α, or when z lies beyond the critical value.
Chapter 12's line A, 40 bottles (ml), in A2:A41.
line_a = np.array([506.9, 504.9, 492.6, 486.9, 516, 514.9, 500.2, 478.7, 520, 507.5, 503.4, 499.1, 496.2, 508, 487.7, 503.9, 498.8, 500.3, 498.5, 495.6, 509.9, 498.6, 491.4, 502.6, 505.9, 480.2, 500.6, 485.5, 510.3, 507.6, 497.7, 488.8, 494.5, 510.6, 504.1, 500, 504.5, 504.6, 509, 491.3])| What it computes | Python | Excel | Value |
|---|---|---|---|
| z for the bottles: x̄ = 504, μ₀ = 500, σ = 10, n = 25 | (504 - 500) / (10 / np.sqrt(25)) |
=(504-500)/(10/SQRT(25)) |
2.00 |
| Its two-tailed p-value | 2 * norm.sf(abs((504 - 500) / (10 / np.sqrt(25)))) |
=2*(1-NORM.S.DIST(ABS((504-500)/(10/SQRT(25))), TRUE)) |
0.0455 |
| The two-tailed critical value at α = 0.05 | norm.ppf(0.975) |
=NORM.S.INV(0.975) |
1.9600 |
| Line A against 500 ml with σ = 9 known: Excel's Z.TEST is the upper one-tailed p | norm.sf((line_a.mean() - 500) / (9 / np.sqrt(40))) |
=Z.TEST(A2:A41, 500, 9) |
0.3772 |
The confidence interval for a mean
Taught in chapter 5: Tests and confidence intervalsWhen to use it: To report the range of values for μ the data support. A two-sided test at α rejects exactly the values outside the 1 − α interval (for the z and t tests of a mean).
| What it computes | Python | Excel | Value |
|---|---|---|---|
| The bottles' 95 % t interval, σ estimated by s = 10 | t.interval(0.95, 24, loc=504, scale=10 / np.sqrt(25)) |
=504-CONFIDENCE.T(0.05, 10, 25)=504+CONFIDENCE.T(0.05, 10, 25) |
(499.87, 508.13) |
| The same with σ = 10 known (z) | norm.interval(0.95, loc=504, scale=10 / np.sqrt(25)) |
=504-CONFIDENCE.NORM(0.05, 10, 25)=504+CONFIDENCE.NORM(0.05, 10, 25) |
(500.08, 507.92) |
Means
The one-sample t test
Taught in chapter 6: One mean: z and t, one complete testWhen to use it: One mean against a claimed value when σ is estimated from the sample, which is almost always. Check the shape for small samples; the Wilcoxon signed-rank test is the fallback.
Chapter 12's line A, 40 bottles (ml), in A2:A41.
line_a = np.array([506.9, 504.9, 492.6, 486.9, 516, 514.9, 500.2, 478.7, 520, 507.5, 503.4, 499.1, 496.2, 508, 487.7, 503.9, 498.8, 500.3, 498.5, 495.6, 509.9, 498.6, 491.4, 502.6, 505.9, 480.2, 500.6, 485.5, 510.3, 507.6, 497.7, 488.8, 494.5, 510.6, 504.1, 500, 504.5, 504.6, 509, 491.3])| What it computes | Python | Excel | Value |
|---|---|---|---|
| The bottles' two-tailed p-value, t = 2.00 on 24 degrees of freedom | 2 * t.sf(2.0, 24) |
=T.DIST.2T(2, 24) |
0.0569 |
| The two-tailed critical value at α = 0.05 | t.ppf(0.975, 24) |
=T.INV.2T(0.05, 24) |
2.0639 |
| Line A's mean against 500 ml, from the data | stats.ttest_1samp(line_a, 500).pvalue |
=T.DIST.2T(ABS((AVERAGE(A2:A41)-500)/(STDEV.S(A2:A41)/SQRT(40))), 39) |
0.7656 |
| Cohen's d for the bottles | (504 - 500) / 10 |
=(504-500)/10 |
0.40 |
Two independent means: Welch's t test
Taught in chapter 7: Two independent meansWhen to use it: Two separate groups of units. Use Welch's test by default: it does not assume equal variances and costs almost nothing when they are equal. scipy pools the variances unless you pass equal_var=False.
Chapter 14's settlement times in days: office A in A2:A16, office B in B2:B16.
a = np.array([6.6, 2.1, 26.4, 5.3, 3.7, 3.4, 11.8, 5, 20.5, 9.7, 3.2, 8.2, 7, 3.6, 3.4])
b = np.array([18.8, 9.4, 6.4, 78.2, 7.5, 6.8, 12, 6.9, 15.8, 18.8, 7.5, 29.8, 15.4, 8.6, 28.8])| What it computes | Python | Excel | Value |
|---|---|---|---|
| Welch's t for the two offices' settlement times | stats.ttest_ind(a, b, equal_var=False).statistic |
=(AVERAGE(A2:A16)-AVERAGE(B2:B16))/SQRT(VAR.S(A2:A16)/15+VAR.S(B2:B16)/15) |
-1.9891 |
| Welch's two-tailed p-value | stats.ttest_ind(a, b, equal_var=False).pvalue |
=T.TEST(A2:A16, B2:B16, 2, 3) |
0.0622 |
| The pooled (Student) test: what scipy runs when equal_var is left out | stats.ttest_ind(a, b).pvalue |
=T.TEST(A2:A16, B2:B16, 2, 2) |
0.0565 |
Paired samples: the paired t test
Taught in chapter 8: Paired samplesWhen to use it: The same units measured twice, or matched pairs. It is a one-sample t test on the differences; the design, not the p-value, decides that the data are paired.
Helion's first 12 agents, average daily calls before (A2:A13) and after (B2:B13).
pre = np.array([50.1, 40.6, 62.7, 69.6, 63.9, 68.6, 60.7, 54.2, 59.2, 53.3, 85.2, 64.5])
post = np.array([36.5, 38.2, 59.4, 63.3, 58, 63.7, 64.6, 41.8, 45, 50, 76.5, 67.6])| What it computes | Python | Excel | Value |
|---|---|---|---|
| The first 12 agents, after against before | stats.ttest_rel(post, pre).pvalue |
=T.TEST(B2:B13, A2:A13, 2, 1) |
0.0067 |
| The same data wrongly treated as two independent groups | stats.ttest_ind(post, pre, equal_var=False).pvalue |
=T.TEST(B2:B13, A2:A13, 2, 3) |
0.2618 |
| Cohen's d_z, the mean difference in standard deviations of the differences | (post - pre).mean() / (post - pre).std(ddof=1) |
=AVERAGE(B2:B13-A2:A13)/STDEV.S(B2:B13-A2:A13) |
-0.9621 |
Proportions and counts
One proportion: the z test and the exact binomial test
Taught in chapter 9: One proportionWhen to use it: A yes/no outcome against a target rate. The z test needs n p₀ and n(1 − p₀) both at least 10; otherwise, or when in doubt, use the exact binomial test.
| What it computes | Python | Excel | Value |
|---|---|---|---|
| 470 of 500 on time against 95 %, z with the null standard error | proportions_ztest(470, 500, value=0.95, prop_var=0.95)[1] |
=2*NORM.S.DIST(-ABS((470/500-0.95)/SQRT(0.95*0.05/500)), TRUE) |
0.3049 |
| statsmodels' default uses the sample proportion instead | proportions_ztest(470, 500, value=0.95)[1] |
=2*NORM.S.DIST(-ABS((470/500-0.95)/SQRT(0.94*0.06/500)), TRUE) |
0.3464 |
| The exact binomial test, two-sided | stats.binomtest(470, 500, 0.95).pvalue |
Excel has no two-sided exact test: BINOM.DIST gives each tail, and the two-sided p adds every outcome no more likely than the one observed. | 0.3037 |
| The exact test, one-sided: fewer on time than promised | stats.binomtest(470, 500, 0.95, alternative='less').pvalue |
=BINOM.DIST(470, 500, 0.95, TRUE) |
0.1765 |
| Meridian's large losses, 171 of 4,663: the exact (Clopper–Pearson) 95 % interval | stats.binomtest(171, 4663).proportion_ci(method='exact') |
=BETA.INV(0.025, 171, 4663-171+1)=BETA.INV(0.975, 171+1, 4663-171) |
(0.0315, 0.0425) |
Two proportions: the z test
Taught in chapter 10: Two proportionsWhen to use it: Two groups' rates, the workhorse of A/B testing. The test uses the pooled rate p̂, because H₀ says the rates are equal; the interval for the gap uses each group's own rate.
| What it computes | Python | Excel | Value |
|---|---|---|---|
| Helion's H1: 2,290 of 6,031 against 2,052 of 5,969 | proportions_ztest([2290, 2052], [6031, 5969])[0] |
=(2290/6031-2052/5969)/SQRT(4342/12000*(1-4342/12000)*(1/6031+1/5969)) |
4.0952 |
| The small pilot: 138 against 110 of 1,000 | proportions_ztest([138, 110], [1000, 1000])[1] |
=2*NORM.S.DIST(-ABS((0.138-0.11)/SQRT(0.124*0.876*2/1000)), TRUE) |
0.0575 |
| The 95 % Wald interval for H1's gap | confint_proportions_2indep(2290, 6031, 2052, 5969, method='wald') |
=2290/6031-2052/5969-NORM.S.INV(0.975)*SQRT(2290/6031*(1-2290/6031)/6031+2052/5969*(1-2052/5969)/5969)=2290/6031-2052/5969+NORM.S.INV(0.975)*SQRT(2290/6031*(1-2290/6031)/6031+2052/5969*(1-2052/5969)/5969) |
(0.0187, 0.0531) |
| Cohen's h for H1 | proportion_effectsize(2290 / 6031, 2052 / 5969) |
=2*ASIN(SQRT(2290/6031))-2*ASIN(SQRT(2052/5969)) |
0.0748 |
Small counts: Fisher's exact test
Taught in chapter 10: Two proportionsWhen to use it: A 2 × 2 table with fewer than 10 successes or failures in a group. Given the table's totals, it adds the exact probabilities of every table at least as extreme as the observed one.
| What it computes | Python | Excel | Value |
|---|---|---|---|
| The tiny trial: 6 sales of 15 calls against 1 of 15, two-sided | stats.fisher_exact([[6, 9], [1, 14]]).pvalue |
Excel has no two-sided Fisher test: HYPGEOM.DIST gives one table at a time (the one-sided line below adds them). | 0.0801 |
| One-sided: the new script sells more | stats.fisher_exact([[6, 9], [1, 14]], alternative='greater').pvalue |
=1-HYPGEOM.DIST(5, 15, 7, 30, TRUE) |
0.0400 |
Chi-square goodness of fit
Taught in chapter 11: Chi-square: goodness of fit and independenceWhen to use it: Counts in categories against a mix fixed in advance: a forecast, a 50/50 split (the sample ratio mismatch check). Counts, never percentages; every expected count at least 5.
Helion's contacts by channel in A2:A5, and the forecast's expected counts (the shares times 1,600) in B2:B5.
observed = np.array([742, 388, 271, 199])
expected = np.array([736, 384, 288, 192])| What it computes | Python | Excel | Value |
|---|---|---|---|
| Helion's channel mix: χ² | stats.chisquare(observed, expected).statistic |
=SUM((A2:A5-B2:B5)^2/B2:B5) |
1.3493 |
| Its p-value | stats.chisquare(observed, expected).pvalue |
=CHISQ.TEST(A2:A5, B2:B5) |
0.7175 |
| The sample ratio mismatch check: 5,969 against 6,031 for 50/50 | stats.chisquare([5969, 6031]).pvalue |
=CHISQ.DIST.RT((5969-6000)^2/6000+(6031-6000)^2/6000, 1) |
0.5714 |
Chi-square test of independence
Taught in chapter 11: Chi-square: goodness of fit and independenceWhen to use it: Two categorical variables on the same units: is one associated with the other? scipy applies Yates' correction to a 2 × 2 table by default; Excel's CHISQ.TEST never does.
H1's counts in A2:B3 (Portal v2 first; paperless, then not), and the expected counts built from the margins in D2:E3.
table = np.array([[2290, 3741], [2052, 3917]])
expected = np.array([[2182.2168333333334, 3848.7831666666666], [2159.7831666666666, 3809.2168333333334]])| What it computes | Python | Excel | Value |
|---|---|---|---|
| H1 as a 2 × 2 table, with scipy's default Yates correction | stats.chi2_contingency(table)[1] |
Excel's CHISQ.TEST has no Yates correction: the next line is what it computes. | 4.6e-05 |
| Without the correction, as Excel computes it | stats.chi2_contingency(table, correction=False)[1] |
=CHISQ.TEST(A2:B3, D2:E3) |
4.2e-05 |
| Uncorrected χ², which is z² of the two-proportion test | stats.chi2_contingency(table, correction=False)[0] |
=((2290/6031-2052/5969)/SQRT(4342/12000*(1-4342/12000)*(1/6031+1/5969)))^2 |
16.7706 |
| Cramér's V | stats.contingency.association(table, method='cramer') |
Excel has no function for Cramér's V: use the formula above. | 0.0374 |
Spread, several groups, and correlation
One variance: the χ² test
Taught in chapter 12: Variances: χ², F and LeveneWhen to use it: One group's spread against a specification. Valid only for normal data, and very sensitive to anything else.
Chapter 12's line B, 40 bottles (ml), in B2:B41.
line_b = np.array([514.1, 478.4, 496.2, 503.6, 483.4, 507.7, 482.1, 513.5, 502, 496.4, 516.5, 518.6, 490.6, 496.9, 501.4, 498.5, 497.4, 535.8, 470.9, 519.9, 514.9, 498.5, 483.9, 516.6, 510.2, 497.2, 510.3, 502, 493.3, 521.2, 506.1, 502.4, 512.1, 497.5, 488, 499.3, 489.2, 499.9, 499.6, 500.1])| What it computes | Python | Excel | Value |
|---|---|---|---|
| Is line B more erratic than σ = 10 ml allows? Upper tail | chi2.sf(39 * np.var(line_b, ddof=1) / 10**2, 39) |
=CHISQ.DIST.RT(39*VAR.S(B2:B41)/10^2, 39) |
0.0042 |
Two variances: the F test, Levene and Fligner–Killeen
Taught in chapter 12: Variances: χ², F and LeveneWhen to use it: Are two groups equally consistent? The F test is exact only for normal data; Levene's test (centred on the median) is the practical tool, and Fligner–Killeen a rank-based second opinion.
Chapter 12's two filling lines, 40 bottles each (ml): line A in A2:A41, line B in B2:B41.
line_a = np.array([506.9, 504.9, 492.6, 486.9, 516, 514.9, 500.2, 478.7, 520, 507.5, 503.4, 499.1, 496.2, 508, 487.7, 503.9, 498.8, 500.3, 498.5, 495.6, 509.9, 498.6, 491.4, 502.6, 505.9, 480.2, 500.6, 485.5, 510.3, 507.6, 497.7, 488.8, 494.5, 510.6, 504.1, 500, 504.5, 504.6, 509, 491.3])
line_b = np.array([514.1, 478.4, 496.2, 503.6, 483.4, 507.7, 482.1, 513.5, 502, 496.4, 516.5, 518.6, 490.6, 496.9, 501.4, 498.5, 497.4, 535.8, 470.9, 519.9, 514.9, 498.5, 483.9, 516.6, 510.2, 497.2, 510.3, 502, 493.3, 521.2, 506.1, 502.4, 512.1, 497.5, 488, 499.3, 489.2, 499.9, 499.6, 500.1])| What it computes | Python | Excel | Value |
|---|---|---|---|
| F for line B against line A, two-sided | 2 * min(f.sf(np.var(line_b, ddof=1) / np.var(line_a, ddof=1), 39, 39), f.cdf(np.var(line_b, ddof=1) / np.var(line_a, ddof=1), 39, 39)) |
=F.TEST(B2:B41, A2:A41) |
0.0430 |
| The upper critical value at α = 0.05, two-sided | f.ppf(0.975, 39, 39) |
=F.INV.RT(0.025, 39, 39) |
1.8907 |
| Levene's test, median-centred (scipy's default) | stats.levene(line_a, line_b).pvalue |
Excel has no function for this test. | 0.1339 |
| Fligner–Killeen | stats.fligner(line_a, line_b).pvalue |
Excel has no function for this test. | 0.1856 |
One-way ANOVA and Tukey's HSD
Taught in chapter 13: One-way ANOVA and afterWhen to use it: Three or more groups' means at once, instead of many t tests. A significant F says some means differ; Tukey's HSD (the Tukey–Kramer form) says which.
A made-up example: delivery days at three depots, five parcels each, in A2:A6, B2:B6 and C2:C6.
d1 = np.array([2.9, 3.4, 3.1, 3.8, 3.3])
d2 = np.array([3.9, 4.4, 3.6, 4.1, 4.5])
d3 = np.array([3.2, 3, 3.7, 3.5, 2.8])| What it computes | Python | Excel | Value |
|---|---|---|---|
| F for the three depots | stats.f_oneway(d1, d2, d3).statistic |
=(5*((AVERAGE(A2:A6)-AVERAGE(A2:A6, B2:B6, C2:C6))^2+(AVERAGE(B2:B6)-AVERAGE(A2:A6, B2:B6, C2:C6))^2+(AVERAGE(C2:C6)-AVERAGE(A2:A6, B2:B6, C2:C6))^2)/2)/((DEVSQ(A2:A6)+DEVSQ(B2:B6)+DEVSQ(C2:C6))/12) |
9.0287 |
| Its p-value | stats.f_oneway(d1, d2, d3).pvalue |
=F.DIST.RT((5*((AVERAGE(A2:A6)-AVERAGE(A2:A6, B2:B6, C2:C6))^2+(AVERAGE(B2:B6)-AVERAGE(A2:A6, B2:B6, C2:C6))^2+(AVERAGE(C2:C6)-AVERAGE(A2:A6, B2:B6, C2:C6))^2)/2)/((DEVSQ(A2:A6)+DEVSQ(B2:B6)+DEVSQ(C2:C6))/12), 2, 12) |
0.0040 |
| Tukey's HSD: depot 1 against depot 2 | stats.tukey_hsd(d1, d2, d3).pvalue[0, 1] |
Excel has no Tukey test and no studentized range distribution. | 0.0105 |
| η², the share of the variation between depots | stats.f_oneway(d1, d2, d3).statistic * 2 / (stats.f_oneway(d1, d2, d3).statistic * 2 + 12) |
In Excel, divide the between sum of squares by the total: DEVSQ of all the values is the total. | 0.6008 |
Rank tests: Mann–Whitney, Wilcoxon and Kruskal–Wallis
Taught in chapter 14: When the assumptions fail: ranks and permutationsWhen to use it: When the data are skewed or ordinal and n is small. Each asks whether one group tends to give larger values, not whether the means differ.
The offices' settlement times (a, b), the 12 agents' calls (pre, post) and the three depots (d1, d2, d3), as in the entries above.
a = np.array([6.6, 2.1, 26.4, 5.3, 3.7, 3.4, 11.8, 5, 20.5, 9.7, 3.2, 8.2, 7, 3.6, 3.4])
b = np.array([18.8, 9.4, 6.4, 78.2, 7.5, 6.8, 12, 6.9, 15.8, 18.8, 7.5, 29.8, 15.4, 8.6, 28.8])
pre = np.array([50.1, 40.6, 62.7, 69.6, 63.9, 68.6, 60.7, 54.2, 59.2, 53.3, 85.2, 64.5])
post = np.array([36.5, 38.2, 59.4, 63.3, 58, 63.7, 64.6, 41.8, 45, 50, 76.5, 67.6])
d1 = np.array([2.9, 3.4, 3.1, 3.8, 3.3])
d2 = np.array([3.9, 4.4, 3.6, 4.1, 4.5])
d3 = np.array([3.2, 3, 3.7, 3.5, 2.8])| What it computes | Python | Excel | Value |
|---|---|---|---|
| Mann–Whitney for the two offices | stats.mannwhitneyu(a, b).pvalue |
Excel has no function for this test. | 0.0070 |
| Wilcoxon signed-rank for the 12 agents | stats.wilcoxon(post, pre).pvalue |
Excel has no function for this test. | 0.0093 |
| Kruskal–Wallis for the three depots | stats.kruskal(d1, d2, d3).pvalue |
Excel has no function for this test. | 0.0185 |
Correlation: Pearson and Spearman
Taught in chapter 15: Which test? The map, the assumptions, and correlationWhen to use it: Two numbers measured on the same units: do they move together? Pearson for a straight line; Spearman (Pearson on ranks) when the relationship is curved but one-directional, or there are outliers.
Helion's first 12 agents, average daily calls before (A2:A13) and after (B2:B13).
pre = np.array([50.1, 40.6, 62.7, 69.6, 63.9, 68.6, 60.7, 54.2, 59.2, 53.3, 85.2, 64.5])
post = np.array([36.5, 38.2, 59.4, 63.3, 58, 63.7, 64.6, 41.8, 45, 50, 76.5, 67.6])| What it computes | Python | Excel | Value |
|---|---|---|---|
| Pearson's r for the 12 agents, before and after | stats.pearsonr(pre, post).statistic |
=CORREL(A2:A13, B2:B13) |
0.8883 |
| Its two-tailed p-value | stats.pearsonr(pre, post).pvalue |
=T.DIST.2T(ABS(CORREL(A2:A13, B2:B13))*SQRT(10)/SQRT(1-CORREL(A2:A13, B2:B13)^2), 10) |
0.00011 |
| Its 95 % interval (Fisher's z) | stats.pearsonr(pre, post).confidence_interval(0.95) |
Excel has no function for it: FISHER, FISHERINV and NORM.S.INV build it. | (0.6414, 0.9685) |
| Spearman's ρ: Pearson on the ranks | stats.spearmanr(pre, post).statistic |
=CORREL(RANK.AVG(A2:A13, A2:A13, 1), RANK.AVG(B2:B13, B2:B13, 1)) |
0.8531 |
Using tests well
Power and sample size
Taught in chapter 17: Power and sample sizeWhen to use it: Before the test, for the smallest effect worth acting on; always round the sample size up. Never after it at the observed effect: that is a restatement of the p-value.
| What it computes | Python | Excel | Value |
|---|---|---|---|
| Bottles for 80 % power against d = 0.4 (round up: 52) | TTestPower().solve_power(effect_size=0.4, alpha=0.05, power=0.8) |
Excel has no non-central t: the normal approximation below is close, and slightly low. | 51.0094 |
| The normal approximation | ((norm.ppf(0.975) + norm.ppf(0.8)) / 0.4) ** 2 |
=((NORM.S.INV(0.975)+NORM.S.INV(0.8))/0.4)^2 |
49.0555 |
| The power of 25 bottles against d = 0.4 | TTestPower().power(effect_size=0.4, nobs=25, alpha=0.05) |
Excel has no non-central t. | 0.4840 |
| Customers per arm for a 3-point lift from 34 % | NormalIndPower().solve_power(effect_size=proportion_effectsize(0.37, 0.34), alpha=0.05, power=0.8) |
In Excel: ((NORM.S.INV(0.975)+NORM.S.INV(0.8))/h)^2 × 2, which leaves out the far tail and is a hair lower. | 3,992.1 |
| The smallest d that 5,969 per arm detect 80 % of the time | TTestIndPower().solve_power(nobs1=5969, alpha=0.05, power=0.8) |
Excel has no non-central t. | 0.0513 |
Multiple testing: Bonferroni, Holm and Benjamini–Hochberg
Taught in chapter 18: Multiple testing, pre-registration and p-hackingWhen to use it: Several tests on one experiment. Bonferroni and Holm control the chance of any false win (the family-wise error rate); Benjamini–Hochberg controls the false discovery rate. Pre-register which.
Four p-values from one experiment, sorted, in B2:B5.
p = np.array([0.001, 0.012, 0.03, 0.2])| What it computes | Python | Excel | Value |
|---|---|---|---|
| The chance of at least one false win in 20 tests | 1 - (1 - 0.05) ** 20 |
=1-(1-0.05)^20 |
0.6415 |
| Bonferroni: the second smallest p-value, adjusted | multipletests(p, method='bonferroni')[1][1] |
=MIN(1, 4*B3) |
0.048 |
| Holm: the same p-value, adjusted | multipletests(p, method='holm')[1][1] |
=MAX(MIN(1, 4*B2), MIN(1, 3*B3)) |
0.036 |
| Benjamini–Hochberg: the third, adjusted | multipletests(p, method='fdr_bh')[1][2] |
=MIN(B4*4/3, B5*4/4) |
0.040 |
The risk review's tests
Rates over exposure: the Poisson rate ratio
Taught in chapter 20: A risk review end to end: the Meridian finaleWhen to use it: Counts of events over unequal time in service (claims per vehicle-year, incidents per machine-hour): compare the rates, not the share of units that had an event.
| What it computes | Python | Excel | Value |
|---|---|---|---|
| Meridian's claim frequency, telematics against standard: the rate ratio | (2062 / 23671.791) / (2601 / 23560.917) |
=(2062/23671.791)/(2601/23560.917) |
0.7891 |
| The score test's z | test_poisson_2indep(2062, 23671.791, 2601, 23560.917, method='score', compare='ratio').statistic |
=(2062-2601*(23671.791/23560.917))/SQRT(4663*23671.791/23560.917) |
-8.0536 |
| Its two-sided p-value | test_poisson_2indep(2062, 23671.791, 2601, 23560.917, method='score', compare='ratio').pvalue |
=2*NORM.S.DIST(-ABS((2062-2601*(23671.791/23560.917))/SQRT(4663*23671.791/23560.917)), TRUE) |
8.0e-16 |
Paired yes/no: McNemar's test
Taught in chapter 20: A risk review end to end: the Meridian finaleWhen to use it: Two models or raters scored on the same cases. Only the disagreements count: under H₀ each goes either way with probability ½. statsmodels' mcnemar is exact unless you pass exact=False.
| What it computes | Python | Excel | Value |
|---|---|---|---|
| Meridian's fraud models: 5 against 54 disagreements, continuity-corrected χ² | mcnemar([[87, 54], [5, 954]], exact=False).statistic |
=(ABS(54-5)-1)^2/(54+5) |
39.0508 |
| Its p-value | mcnemar([[87, 54], [5, 954]], exact=False).pvalue |
=CHISQ.DIST.RT((ABS(54-5)-1)^2/(54+5), 1) |
4.1e-10 |
| The exact form, statsmodels' default | mcnemar([[87, 54], [5, 954]]).pvalue |
=MIN(1, 2*BINOM.DIST(5, 59, 0.5, TRUE)) |
1.9e-11 |
Equivalence: two one-sided tests (TOST)
Taught in chapter 20: A risk review end to end: the Meridian finaleWhen to use it: To show that a difference is small enough to ignore, within a margin fixed before the data and on the scale the decision uses. Not significant is not the same as equivalent.
Chapter 12's two filling lines, 40 bottles each (ml): line A in A2:A41, line B in B2:B41.
line_a = np.array([506.9, 504.9, 492.6, 486.9, 516, 514.9, 500.2, 478.7, 520, 507.5, 503.4, 499.1, 496.2, 508, 487.7, 503.9, 498.8, 500.3, 498.5, 495.6, 509.9, 498.6, 491.4, 502.6, 505.9, 480.2, 500.6, 485.5, 510.3, 507.6, 497.7, 488.8, 494.5, 510.6, 504.1, 500, 504.5, 504.6, 509, 491.3])
line_b = np.array([514.1, 478.4, 496.2, 503.6, 483.4, 507.7, 482.1, 513.5, 502, 496.4, 516.5, 518.6, 490.6, 496.9, 501.4, 498.5, 497.4, 535.8, 470.9, 519.9, 514.9, 498.5, 483.9, 516.6, 510.2, 497.2, 510.3, 502, 493.3, 521.2, 506.1, 502.4, 512.1, 497.5, 488, 499.3, 489.2, 499.9, 499.6, 500.1])| What it computes | Python | Excel | Value |
|---|---|---|---|
| Do lines A and B fill the same within ±3 ml? The TOST p-value (pooled) | ttost_ind(line_a, line_b, -3, 3)[0] |
=MAX(T.DIST.RT((AVERAGE(A2:A41)-AVERAGE(B2:B41)+3)/SQRT((VAR.S(A2:A41)+VAR.S(B2:B41))/40), 78), T.DIST((AVERAGE(A2:A41)-AVERAGE(B2:B41)-3)/SQRT((VAR.S(A2:A41)+VAR.S(B2:B41))/40), 78, TRUE)) |
0.2414 |