- Confidence interval and sample size
- ANOVA and Kruskal-Wallis on three diets
- Clinical trial 2x2 table
- Bayesian A/B test with Beta posteriors
- Before / after treatment: paired t-test or Wilcoxon
- z-test with known sigma, power and sample size
- Summary of a 5-point Likert survey item
- Which mean? Arithmetic, geometric and harmonic in practice
- The Student t density from the Gamma and Beta functions
- Mincer wage equation from a survey table in SQL
Confidence interval and sample size
A pilot study measures the drop of systolic pressure on 10 patients. The script gives the 95 % confidence interval of the mean and the number of patients needed for a margin of 1.5 mmHg.
#1. Pilot study: drop of systolic pressure in mmHg after 8 weeks, 10 patients; -> "[d]" "[8, 12, 5, 15, 10, 7, 13, 9, 11, 6]"; json load "rx_st_ci" (stat conf_interval_mean [d] 0.95); #2. Sample size for a margin of 1.5 mmHg at 95 percent: n = z s / E squared; -> "[z]" (stat dist_inv "normal" "[0, 1]" 0.975); -> "[n]" (math ceil (math calc (concat "([z]*" (stat stddev [d]) "/1.5)^2"))); concat "mean = " (json select "rx_st_ci" /mean) " mmHg, 95% CI [" (math round (json select "rx_st_ci" /lower) 2) ", " (math round (json select "rx_st_ci" /upper) 2) "], margin " (math round (json select "rx_st_ci" /margin) 2) " -> " [n] " patients needed for a margin of 1.5 mmHg"
mean = 9.6 mmHg, 95% CI [7.31, 11.89], margin 2.29 -> 18 patients needed for a margin of 1.5 mmHg
- Use the Student quantile with
stat dist_inv"student" and iterate on n - Add the power of a test: n = ((z_alpha + z_beta) s / delta)^2
- Check the hypothesis of the paired difference with
stat ttest_1
ANOVA and Kruskal-Wallis on three diets
Weight loss is measured on three diets of six subjects. The script runs a one-way ANOVA with its effect size and the non-parametric Kruskal-Wallis test as a check.
#1. Weight loss in kg with three diets; -> "[g]" "[[3.1, 4.2, 2.8, 3.9, 3.5, 4.0], [5.2, 4.8, 6.1, 5.5, 4.9, 5.8], [3.8, 4.5, 4.1, 3.6, 4.9, 4.4]]"; #2. Parametric one-way ANOVA; json load "rx_st_a" (stat anova [g]); #3. Non-parametric check: Kruskal-Wallis on the ranks; json load "rx_st_k" (stat kruskal [g]); concat "ANOVA F(" (json select "rx_st_a" /df_between) ", " (json select "rx_st_a" /df_within) ") = " (math round (json select "rx_st_a" /f) 2) ", p = " (math round_sig (json select "rx_st_a" /p_value) 3) ", eta2 = " (math round (json select "rx_st_a" /eta2) 3) " ; Kruskal-Wallis H = " (math round (json select "rx_st_k" /h) 2) ", p = " (math round_sig (json select "rx_st_k" /p_value) 3)
ANOVA F(2, 15) = 18.92, p = 0.0000791, eta2 = 0.716 ; Kruskal-Wallis H = 12.07, p = 0.00240
- Check the residuals with
stat shapiro - Compare the pairs with
stat ttest_2and a Bonferroni correction - Use
stat mann_whitneyfor two groups only
Clinical trial 2x2 table
A trial counts the events in a treatment group and a placebo group of 100 patients each. The script runs the chi-square and Fisher exact tests and computes the relative risk, the absolute risk reduction and the number needed to treat.
#1. Trial: events / no event, treatment 12 / 88, placebo 27 / 73; -> "[t]" "[[12, 88], [27, 73]]"; #2. Chi-square test of independence and Fisher exact test; json load "rx_st_c" (stat chi2_independence [t]); json load "rx_st_f" (stat fisher_exact [t]); #3. Relative risk, absolute risk reduction and number needed to treat; -> "[rr]" (math calc "(12/100)/(27/100)"); -> "[arr]" (math calc "27/100-12/100"); concat "chi2 = " (math round (json select "rx_st_c" /chi2) 2) ", p = " (math round_sig (json select "rx_st_c" /p_value) 3) " ; Fisher p = " (math round_sig (json select "rx_st_f" /p_value) 3) ", OR = " (math round (json select "rx_st_f" /odds_ratio) 3) " ; RR = " (math round [rr] 3) ", ARR = " (math round (math calc "100*[arr]") 1) " %, NNT = " (math ceil (math calc "1/[arr]"))
chi2 = 7.17, p = 0.00743 ; Fisher p = 0.0118, OR = 0.369 ; RR = 0.444, ARR = 15 %, NNT = 7
- Get the confidence interval of the log odds ratio with
stat dist_inv - Test the goodness of fit of a distribution with
stat chi2_test - Use
stat wilcoxonfor paired continuous outcomes
Bayesian A/B test with Beta posteriors
Conversions 120/1000 against 145/1000 with uniform priors give Beta posteriors. The script computes the probability that B is better than A by a double integral of the joint posterior (normalised with lgamma) and checks it in one dimension with the incomplete beta function.
#1. A/B test: A 120 conversions out of 1000, B 145 out of 1000, uniform priors: Beta posteriors; -> "[a1]" 121; -> "[b1]" 881; -> "[a2]" 146; -> "[b2]" 856; #2. Log normalising constants with lgamma: ln B of a,b = lgamma a + lgamma b - lgamma a+b; -> "[lB1]" (math calc "lgamma([a1])+lgamma([b1])-lgamma([a1]+[b1])"); -> "[lB2]" (math calc "lgamma([a2])+lgamma([b2])-lgamma([a2]+[b2])"); #3. P of pB > pA: double integral of the joint posterior over y > x; -> "[P]" (num integrate2 (concat "exp(120*ln(x)+880*ln(1-x)-(" [lB1] ")+145*ln(y)+855*ln(1-y)-(" [lB2] "))") "x" 0.05 0.25 "y" "x" 0.3); #4. Check in one dimension with the incomplete beta function; -> "[P1]" (num integrate (concat "exp(120*ln(x)+880*ln(1-x)-(" [lB1] "))*(1-beta_inc(146, 856, x))") "x" 0.05 0.25); concat "P(B better than A) = " (math round [P] 4) " (1D check " (math round [P1] 4) "), posterior means " (math round (math calc "[a1]/([a1]+[b1])") 4) " and " (math round (math calc "[a2]/([a2]+[b2])") 4)
P(B better than A) = 0.9502 (1D check 0.9502), posterior means 0.1208 and 0.1457
- Compare with the frequentist test
stat chi2_independenceon the same 2x2 table. - The expected loss of choosing B is another integral with
num integrate2.
Before / after treatment: paired t-test or Wilcoxon
The systolic pressure of 12 patients is measured before and after 8 weeks of treatment. Test the normality of the differences (Shapiro-Wilk), run the paired t-test and the Wilcoxon signed-rank test, and give the effect size Cohen dz. Here the differences are not normal (p < 0.05), so the Wilcoxon test is the one to report.
#1. Systolic blood pressure in mmHg of 12 patients before and after 8 weeks of treatment; -> "[before]" "[152, 148, 160, 145, 158, 151, 163, 149, 155, 147, 159, 150]"; -> "[after]" "[141, 145, 148, 140, 150, 147, 151, 146, 144, 145, 149, 148]"; #2. Differences and their normality, which the paired t-test assumes; -> "[d]" (vector sub [before] [after]); json load "rx_sw" (stat shapiro [d]); #3. Paired t-test and its non-parametric counterpart; json load "rx_tp" (stat ttest_paired [before] [after]); json load "rx_wx" (stat wilcoxon [before] [after]); #4. Effect size: Cohen dz = mean of d / sd of d; -> "[dz]" (/ (stat mean [d]) (stat stddev [d])); concat "mean drop " (math round (stat mean [d]) 2) " mmHg | Shapiro W = " (math round (json select "rx_sw" "/w") 3) ", p = " (math round (json select "rx_sw" "/p_value") 3) " | paired t = " (math round (json select "rx_tp" "/t") 3) ", df = " (json select "rx_tp" "/df") ", p = " (math round_sig (json select "rx_tp" "/p_value") 3) " | Wilcoxon W = " (json select "rx_wx" "/w") ", p = " (math round_sig (json select "rx_wx" "/p_value") 3) " (" (json select "rx_wx" "/method") ") | Cohen dz = " (math round [dz] 2)
mean drop 6.92 mmHg | Shapiro W = 0.848, p = 0.035 | paired t = 5.812, df = 11, p = 0.000117 | Wilcoxon W = 0, p = 0.000488 (permutation) | Cohen dz = 1.68
- Give a 95 pct confidence interval of the mean drop with
stat conf_interval_meanon d. - Compute the sample size for a power of 80 pct with
stat dist_inv.
z-test with known sigma, power and sample size
A standardised reading test has a known sigma of 15 and a national mean of 100. A school tested 12 pupils. The statistician runs the z-test, computes the power to detect a 5-point shift with this n, and the n needed for 80 % power, all from the error function.
#1. Scores of the class, H0: mu = 100, sigma = 15; -> "[x]" "[112, 98, 105, 121, 93, 108, 117, 101, 99, 110, 125, 104]"; json load "rx_p4g_z" (stat ztest [x] 100 15); -> "[t]" (concat "mean = " (math round (stat mean [x]) 2) ", z = " (math round (json select "rx_p4g_z" /z) 3) ", p = " (math round (json select "rx_p4g_z" /p_value) 4)); json unload "rx_p4g_z"; #2. Critical values: Phi^-1 of p = sqrt2 . erf_inv of 2p - 1; -> "[za]" (* (math sqrt 2) (math erf_inv 0.95)); -> "[zb]" (* (math sqrt 2) (math erf_inv 0.6)); #3. Power for delta = 5 with n = 12, Phi of x = 0.5 . erfc of -x / sqrt2; -> "[k]" (- (/ (* 5 (math sqrt 12)) 15) [za]); -> "[pw]" (* 0.5 (math erfc (/ (- 0 [k]) (math sqrt 2)))); #4. n for 80 % power; -> "[n]" (math ceil (math power (/ (* (+ [za] [zb]) 15) 5) 2)); concat [t] [_n_] "z 0.975 = " (math round [za] 4) ", z 0.80 = " (math round [zb] 4) [_n_] "power for +5 points with n = 12: " (math round (* 100 [pw]) 1) " %" [_n_] "n for 80 % power: " [n]
mean = 107.75, z = 1.79, p = 0.0735 z 0.975 = 1.96, z 0.80 = 0.8416 power for +5 points with n = 12: 21 % n for 80 % power: 71
- Compare with
stat ttest_1when sigma is estimated from the sample. - Plot the power curve against n with
plot chart.
Summary of a 5-point Likert survey item
An item of a satisfaction survey is ordinal (1 = strongly disagree to 5 = strongly agree). The statistician reports what fits ordinal data: the frequency table, the mode, the median, the top-2-box and the bottom-2-box, with the mean only for information.
#1. Answers of 24 respondents; -> "[a]" "[4, 5, 3, 4, 4, 2, 5, 4, 3, 1, 4, 5, 4, 3, 2, 4, 5, 4, 3, 4, 5, 2, 4, 3]"; json load "rx_p4g_lk" (stat frequency [a]); #2. Frequency per answer; -> "[tab]" ""; for (-> "[i]" 0) (< [i] (json count "rx_p4g_lk" /)) (++ "[i]") { -> "[tab]" (concat [tab] (json select "rx_p4g_lk" (concat "/[" [i] "]/value")) ": " (math round (* 100 (json select "rx_p4g_lk" (concat "/[" [i] "]/frequency"))) 1) " % "); }; #3. Bottom-2 box = cumulative frequency of answer 2, top-2 = 1 - cumulative of 3; -> "[b2]" (json select "rx_p4g_lk" "/[1]/cumulative_frequency"); -> "[t2]" (- 1 (json select "rx_p4g_lk" "/[2]/cumulative_frequency")); json unload "rx_p4g_lk"; concat [tab] [_n_] "mode = " (stat mode [a]) ", median = " (stat median [a]) ", mean = " (math round (math avg [a]) 2) [_n_] "top-2-box = " (math round (* 100 [t2]) 1) " %, bottom-2-box = " (math round (* 100 [b2]) 1) " %, net score = " (math round (* 100 (- [t2] [b2])) 1)
1: 4.2 % 2: 12.5 % 3: 20.8 % 4: 41.7 % 5: 20.8 % mode = [4], median = 4, mean = 3.63 top-2-box = 62.5 %, bottom-2-box = 16.7 %, net score = 45.8
- Compare two groups of respondents with
stat mann_whitney. - Test the distribution against last year with
stat chi2_test.
Which mean? Arithmetic, geometric and harmonic in practice
Three classic cases where the arithmetic mean misleads: the average yearly growth of an investment (geometric mean), the P/E ratio of an equal-amount portfolio (harmonic mean), and the population versus sample variance of a full census (divide by n).
#1. Yearly growth factors: +25 %, -20 %, +15 %, +5 %; -> "[g]" "[1.25, 0.80, 1.15, 1.05]"; -> "[ga]" (math avg [g]); -> "[gg]" (stat geo_mean [g]); #2. Same amount invested in 4 shares with these P/E ratios; -> "[pe]" "[12, 18, 30, 45]"; #3. Census of the 6 branches of a company: staff per branch; -> "[s]" "[14, 22, 9, 31, 18, 26]"; concat "growth: arithmetic " (math round (* 100 (- [ga] 1)) 2) " %/yr is wrong, geometric " (math round (* 100 (- [gg] 1)) 2) " %/yr, check final value " (math round (math power [gg] 4) 4) " = " (math round (* 1.25 (* 0.8 (* 1.15 1.05))) 4) [_n_] "portfolio P/E: harmonic " (math round (stat harm_mean [pe]) 2) " (arithmetic would say " (math avg [pe]) ")" [_n_] "census variance /n = " (math round (stat pvariance [s]) 3) ", sample variance /(n-1) = " (math round (stat variance [s]) 3)
growth: arithmetic 6.25 %/yr is wrong, geometric 4.83 %/yr, check final value 1.2075 = 1.2075 portfolio P/E: harmonic 20.57 (arithmetic would say 26.25) census variance /n = 53.667, sample variance /(n-1) = 64.4
- Show the inequality harmonic <= geometric <= arithmetic on any positive series.
- Use
stat weighted_meanwhen the amounts invested differ.
The Student t density from the Gamma and Beta functions
To teach where the t density comes from, the script evaluates f(t) = Gamma((v+1)/2) / (sqrt(v pi) Gamma(v/2)) (1 + t^2/v)^(-(v+1)/2) with the Gamma function, then with the Beta form, then with lngamma for v = 200 (where Gamma overflows), and compares with the built-in law.
#1. v = 5 degrees of freedom, t = 1.5; -> "[v]" 5; -> "[t]" 1.5; -> "[k]" (math power (+ 1 (/ (* [t] [t]) [v])) (- 0 (/ (+ [v] 1) 2))); -> "[f1]" (* (/ (math gamma (/ (+ [v] 1) 2)) (* (math sqrt (* [v] (math pi))) (math gamma (/ [v] 2)))) [k]); #2. Same with the Beta function; -> "[f2]" (/ [k] (* (math sqrt [v]) (math beta 0.5 (/ [v] 2)))); #3. v = 200: Gamma of 100.5 is huge, use lngamma; -> "[c200]" (math exp (- (math lngamma 100.5) (math lngamma 100))); -> "[f200]" (* (/ [c200] (math sqrt (* 200 (math pi)))) (math power (+ 1 (/ 2.25 200)) -100.5)); concat "v = 5: Gamma form " (math round [f1] 8) ", Beta form " (math round [f2] 8) ", stat dist_pdf " (math round (stat dist_pdf "student" "[5]" 1.5) 8) [_n_] "v = 200: lngamma form " (math round [f200] 6) ", stat dist_pdf " (math round (stat dist_pdf "student" "[200]" 1.5) 6) ", normal " (math round (stat dist_pdf "normal" "[0, 1]" 1.5) 6)
v = 5: Gamma form 0.12451734, Beta form 0.12451734, stat dist_pdf 0.12451734 v = 200: lngamma form 0.129445, stat dist_pdf 0.129445, normal 0.129518
- Integrate the density with
num integrateto get the p-value. - Do the same for the chi2 density with
math gamma.
Mincer wage equation from a survey table in SQL
Survey records sit in a database table (embedded H2 here). The statistician loads log hourly wage against years of education and experience straight from a SELECT into a multiple regression, and reports the coefficients, their standard errors and the adjusted R2.
#1. Survey database, embedded H2 file under tmp; cm set "tuto_p4g_svy" {execute "db.h2.embedded.config.get" "[database]" "./tmp/tuto_p4g_svy/survey" "[user]" "tuto" "[password]" "tuto_pwd" ;}; sql connect "tuto_p4g_s2" {cm get "tuto_p4g_svy";}; sql dml "tuto_p4g_s2" "create table resp (educ double, exper double, lnwage double)"; sql dml "tuto_p4g_s2" "insert into resp values (10, 5, 2.45), (12, 3, 2.52), (12, 10, 2.78), (14, 2, 2.70), (16, 4, 2.98), (16, 12, 3.21), (11, 20, 2.80), (18, 6, 3.25), (13, 8, 2.80), (15, 15, 3.12), (12, 25, 2.95), (17, 9, 3.20)"; sql disconnect "tuto_p4g_s2"; #2. Multiple regression straight from the query, unused X fields empty; pa rm load "rx_p4g_wage" "tuto_p4g_svy" "educ" "exper" "" "" "" "lnwage" "select educ, exper, lnwage from resp"; json load "rx_p4g_b" (pa rm estimate_regression_parameters "rx_p4g_wage"); json load "rx_p4g_se" (pa rm estimate_regression_parameters_standard_errors "rx_p4g_wage"); #3. Return to one year of schooling = exp of b1 - 1; -> "[b1]" (json select "rx_p4g_b" "/[1]"); -> "[r]" (concat "ln w = " (math round (json select "rx_p4g_b" "/[0]") 3) " + " (math round [b1] 4) " educ + " (math round (json select "rx_p4g_b" "/[2]") 4) " exper" [_n_] "SE: " (math round (json select "rx_p4g_se" "/[0]") 3) ", " (math round (json select "rx_p4g_se" "/[1]") 4) ", " (math round (json select "rx_p4g_se" "/[2]") 4) ", t educ = " (math round (/ [b1] (json select "rx_p4g_se" "/[1]")) 2) [_n_] "adjusted R2 = " (math round (pa rm calculate_adjusted_r_squared "rx_p4g_wage") 4) ", return to schooling = " (math round (* 100 (- (math exp [b1]) 1)) 2) " % per year"); #4. Close and remove the temporary database; json unload "rx_p4g_b"; json unload "rx_p4g_se"; pa rm close "rx_p4g_wage"; cm remove "tuto_p4g_svy"; file delete "tmp/tuto_p4g_svy"; [r]
ln w = 1.296 + 0.1018 educ + 0.0195 exper SE: 0.087, 0.0057, 0.0021, t educ = 17.93 adjusted R2 = 0.9695, return to schooling = 10.71 % per year
- Add experience squared as a third field to get the concave profile.
- Check the residuals with
pa rm estimate_residualsandstat shapiro.
