Decision table
Determine the outcome type and the study design first, then look at the distribution. "First choice" is the approach most medical journal reviewers accept; "alternatives" apply when conditions are not met or as sensitivity analyses.
Decide between independent and paired by how the data were generated. The left and right side of the same animal, a patient before and after treatment, and cases and controls matched 1:1 on age and sex are all paired data. Several lesions, teeth or follow-up visits from the same patient are not independent observations either; the unit of analysis must match the unit of randomization or sampling.
When comparing two groups on an ordinal outcome (for example cured, markedly improved, improved, no effect), use the Mann-Whitney U test. A chi-squared test discards the ordering.
| Question | Outcome | Design | First choice | Alternatives and when to use them | Effect size to report |
|---|---|---|---|---|---|
| Compare two means | Continuous | Two independent groups | Welch's t-test | When sample sizes and variances are similar, Student's and Welch's t give almost the same result; for clearly skewed data with fewer than about 30 per group, use Mann-Whitney U | Mean difference with 95% CI, Hedges' g |
| Compare two distributions | Skewed continuous or ordinal | Two independent groups | Mann-Whitney U test (Wilcoxon rank-sum test) | Right-skewed concentrations or costs can be log-transformed and analyzed with Welch's t; interpret as a ratio of geometric means | Hodges-Lehmann shift with 95% CI, rank-biserial correlation |
| Before-after or matched comparison | Continuous | Paired (same or matched subjects) | Paired t-test | Wilcoxon signed-rank test when differences are clearly skewed | Mean difference with 95% CI, dz |
| Compare three or more means | Continuous | Several independent groups | One-way ANOVA + Tukey test | Welch's ANOVA + Games-Howell when variances differ; Dunnett when comparing only with a control | η² or ω², pairwise differences with 95% CI |
| Compare three or more distributions | Skewed continuous or ordinal | Several independent groups | Kruskal-Wallis test + Dunn test (Holm or Bonferroni adjustment) | Ordinal logistic regression for ordered categories | ε² or η²H |
| Compare several time points | Continuous | Repeated measures on the same subjects | Repeated-measures ANOVA, reporting Mauchly's test and the Greenhouse-Geisser correction | Linear mixed model with missing time points or uneven intervals; Friedman test for skewed data | Generalized η², means with 95% CI at each time point |
| Effects of two factors | Continuous | Factorial design | Two-way ANOVA with interaction (type III sums of squares) | State the sum-of-squares type for unbalanced designs; see the software section | Partial η², simple effects |
| Compare proportions between groups | Binary or nominal | Independent | Pearson chi-squared test | Fisher's exact test for 2×2 tables with expected counts below 5 or total n below 40; Fisher-Freeman-Halton exact test when more than 1/5 of cells in an R×C table have expected counts below 5 | Risk difference, RR or OR with 95% CI |
| Compare paired binary outcomes | Binary | Same subjects, two methods or before-after | McNemar's test | Exact binomial test when the number of discordant pairs b+c is small | Difference in discordant proportions or paired OR |
| Association of two continuous variables | Continuous | Two variables on the same subjects | Pearson correlation (linear, no marked outliers) | Spearman correlation for skewed, ordinal or outlier-prone data; repeated measurements per subject cannot be pooled | r with 95% CI |
| Agreement of two measurement methods | Continuous | Same subjects, two methods | Bland-Altman limits of agreement | Intraclass correlation coefficient (ICC); do not judge agreement with a correlation coefficient | Mean difference and 95% limits of agreement |
| Multivariable adjustment | Continuous | Any | Linear regression | Log-transform or use a generalized linear model for right-skewed outcomes | Coefficients with 95% CI, R² |
| Multivariable adjustment | Binary | Any | Logistic regression | When the outcome is common (above about 10%), the OR overstates the RR; use log-binomial or modified Poisson regression to report RR | OR with 95% CI |
| Survival time | Time + event indicator (censored) | Any | Kaplan-Meier curves + log-rank test; Cox regression for multivariable analysis | Piecewise or time-dependent covariates when proportional hazards fail | HR with 95% CI, median survival |
Assumptions
t-tests and ANOVA require approximate normality within each group (or of regression residuals), not of all data pooled. In large samples they are quite robust to non-normality; the main risk is unequal variances combined with unequal sample sizes.
Sensitivity of normality tests to sample size (tested on this page)
Sampling from a t distribution with 10 degrees of freedom (only mildly heavy-tailed), set.seed(2026): Shapiro-Wilk P=0.60 at n=30, P=0.42 at n=100, P=0.0019 at n=1000 and P=8.6×10⁻¹² at n=5000. Over 200 repetitions, 12% of samples were judged non-normal at n=30 and 100% at n=5000. The distribution is the same; only the sample size changes the conclusion.
Thresholds for skewness and kurtosis
Kim (2013, Restor Dent Endod): for n<50, an absolute z value of skewness or kurtosis above 1.96 indicates non-normality; for 50≤n<300 the threshold is 3.29; for n≥300, ignore z values and look at the histogram and whether absolute skewness exceeds 2 or absolute kurtosis exceeds 7. Shapiro-Wilk and K-S tests suit samples with n<300.
How large is large enough to ignore normality
Lumley et al. (2002, Annu Rev Public Health) reviewed simulation studies and concluded that the sample size needed for t-tests and linear regression is often under 100, and under 500 even for extremely skewed medical cost data. This assumes the variances are not very different, or that Welch's t-test and robust standard errors are used.
Different versions of Levene's test
R's car::leveneTest and SciPy's stats.levene center on the median by default (the Brown-Forsythe test); the SPSS independent-samples t-test output reports the classic mean-centered Levene test. On airquality ozone data for May vs August: median version F=9.50, P=0.0033; mean version F=10.09, P=0.0026. Add center = mean in R to match SPSS.
| Common practice | Problem | Recommended practice |
|---|---|---|
| Run Shapiro-Wilk on every variable and switch to a nonparametric test when P<0.05 | Large samples give P<0.05 for trivial departures; small samples miss severe departures | Inspect Q-Q plots and histograms with skewness and kurtosis; in large samples use the t-test or ANOVA directly |
| Run Levene's test; use Student's t if P>0.05, otherwise Welch's t | The two-stage choice itself changes the type I error rate; Levene's test has low power in small samples | Use Welch's t directly. It loses little power when variances are equal and keeps the type I error rate closer to nominal when they differ |
| Use a normality test on the current data to choose between the t-test and a rank test | Rochon et al. (2012) found this two-stage procedure formally incorrect | Decide in advance from earlier studies, pilot data or the nature of the variable; when unsure, use the nonparametric test unconditionally |
| Report a Mann-Whitney U result as "the medians differ" | When the groups differ in shape or spread, a significant test does not mean the medians differ (Hart 2001) | Report medians and interquartile ranges and describe the difference with the Hodges-Lehmann shift |
Tested: two groups
Tested on this page: R 4.4.3 (car 3.1.5, effectsize 1.0.3, rstatix 1.1.0, afex 1.5.1, survival 3.8.12) and Python 3.12 (SciPy 1.18.1, statsmodels 0.15.0, pingouin 0.7.0, lifelines 0.30.3) on macOS arm64. The data are R built-in datasets exported to CSV so both languages read identical data. The data are ozone concentrations (ppb) from airquality, right-skewed and with clearly different variances between months.
In the June vs August row, the larger group also has the larger variance. Student's t pools the variances toward the larger group, overestimates the standard error and gives a larger P value (0.034 vs 0.0043). If instead the smaller group has the larger variance, Student's t gives too small a P value and more false positives. Welch's t needs no prior judgment in either case.
Effect sizes and confidence intervals: May vs August mean difference −36.3 ppb (Welch 95% CI −54.4 to −18.3), Hedges' g=−1.11 (95% CI −1.69 to −0.53); June vs August mean difference −30.5 ppb (95% CI −50.7 to −10.4), Hedges' g with unpooled SD −0.96 (95% CI −1.61 to −0.30).
Paired data analyzed as independent: the sleep dataset records extra sleep for 10 subjects under each of two drugs. The paired t-test gives t=4.06, df=9, P=0.0028, mean difference 1.58 hours (95% CI 0.70 to 2.46). An independent-samples t-test gives t=1.86, df=18, P=0.079. Between-subject variation is not removed, and a clear difference goes undetected.
| Comparison | n, mean (SD) per group | Student's t | Welch's t | Mann-Whitney U | R and Python agree? |
|---|---|---|---|---|---|
| May vs August (equal n) | 26, 23.6 (22.2); 26, 60.0 (39.7) | t=−4.075, df=50, P=0.000165 | t=−4.075, df=39.3, P=0.000217 | W=127.5, P=0.000121 | Yes (SciPy and pingouin match to 6 decimal places) |
| June vs August (unequal n) | 9, 29.4 (18.2); 26, 60.0 (39.7) | t=−2.211, df=33, P=0.034 | t=−3.092, df=30.0, P=0.0043 | W=57.5, P=0.026 | Yes |
Several groups and repeated measures
The overall test answers whether all groups are equal; post hoc tests answer which pairs differ. Report both.
The ctrl vs trt2 comparison shows the problem with pairwise t-tests: unadjusted P=0.048 would be written as "statistically significant" at 0.05, while the Tukey-adjusted P=0.198. Three groups need 3 comparisons; five groups need 10, and the inflation grows.
Kruskal-Wallis (airquality ozone, May to September, n=116): H=29.27, df=4, P=6.9×10⁻⁶, η²H=0.23. Post hoc Dunn tests with Holm adjustment show differences for May vs July, May vs August, July vs September and August vs September. Do not follow Kruskal-Wallis with unadjusted pairwise Mann-Whitney tests.
Repeated measures (Indometh, indomethacin plasma concentrations in 6 subjects at 0.5, 1, 2, 4 and 8 hours): Mauchly's W=0.00063, P=0.0059, so sphericity fails; the uncorrected F(4, 20)=106.6; Greenhouse-Geisser ε=0.378, corrected degrees of freedom 1.51 and 7.55, P=4.7×10⁻⁶. R's afex::aov_ez and pingouin.rm_anova agree. Friedman χ²=24.0, P=8.0×10⁻⁵. State which correction was used and the ε value in the paper.
| Method (PlantGrowth, 3 groups of 10 plants) | ctrl vs trt1 | ctrl vs trt2 | trt1 vs trt2 |
|---|---|---|---|
| Pairwise Welch t-tests, unadjusted (incorrect) | P=0.250 | P=0.048 | P=0.009 |
| Pairwise Welch t-tests + Holm adjustment | P=0.250 | P=0.096 | P=0.028 |
| Tukey HSD (first choice with equal variances) | P=0.391 | P=0.198 | P=0.012 |
| Games-Howell (unequal variances) | P=0.475 | P=0.113 | P=0.024 |
- The Tukey test works with unequal group sizes (Tukey-Kramer method); R's TukeyHSD and SPSS's Tukey handle this automatically.
- When only comparing each treatment with one control, use Dunnett's test: fewer comparisons and more power than Tukey.
- The LSD method controls the familywise error rate only with exactly 3 groups and a significant overall F test; do not use it with more groups.
- The SNK method does not control the familywise error rate when only some group means are equal; reviewers often ask for Tukey or Holm instead.
- With dropouts, repeated-measures ANOVA deletes the whole subject; a linear mixed model uses incomplete observations.
Multiple comparisons
With 10 independent tests whose null hypotheses are all true, the probability of at least one P<0.05 is 1−0.95¹⁰=40%. The adjustment method depends on the structure of the comparisons and the nature of the study.
Six of these raw P values are below 0.05; only the first remains after Bonferroni or Holm adjustment, and the first three after BH adjustment. In the paper, state the adjustment method and the family of tests adjusted (for example, "Holm adjustment for the 6 between-group comparisons of 3 secondary outcomes"), and report adjusted P values or the adjusted significance level.
raw P Bonferroni Holm BH(FDR)
0.001 0.010 0.010 0.0100
0.008 0.080 0.072 0.0400
0.012 0.120 0.096 0.0400
0.021 0.210 0.147 0.0525
0.035 0.350 0.210 0.0700
0.048 0.480 0.240 0.0800
0.090 0.900 0.360 0.1286
0.200 1.000 0.600 0.2500
0.410 1.000 0.820 0.4556
0.740 1.000 0.820 0.7400| Situation | Method | Notes |
|---|---|---|
| All pairwise comparisons after ANOVA | Tukey (Games-Howell with unequal variances) | Uses the error estimate from all groups and has more power than adjusting each pair separately |
| Each group compared only with one control | Dunnett | Number of comparisons is the number of groups minus 1 |
| A few pre-planned comparisons, or a mix of different tests | Holm | Controls the familywise error rate, is never less powerful than Bonferroni and needs no extra assumptions, so it can replace Bonferroni |
| Exploratory screening of dozens of outcomes, genes or imaging regions | Benjamini-Hochberg (FDR) | Controls the proportion of false positives among positive results; confirm findings in independent data |
| Confirmatory study with a single primary outcome | No adjustment | Pre-specify it in the protocol or statistical analysis plan |
| Exploratory analyses, secondary outcomes | May be left unadjusted but labeled exploratory | Recommended by Bender and Lange (2001); the NEJM 2019 statistical guidelines require secondary outcomes without a pre-specified adjustment method to be reported as estimates with 95% CIs only |
Categorical data
For 2×2 tables, check the total sample size and the smallest expected count before choosing a test. R's chisq.test and SciPy's chi2_contingency both apply the Yates continuity correction to 2×2 tables by default; SPSS prints both the uncorrected and corrected rows.
The rule in Chinese medical statistics textbooks for 2×2 tables: with n≥40 and all expected counts T≥5, use Pearson's chi-squared; with n≥40 and some 1≤T<5, use the continuity-corrected chi-squared; with n<40 or any T<1, use Fisher's exact test. R×C tables need no cell with T<1 and no more than 1/5 of cells with 1≤T<5.
Campbell (2007, Stat Med) compared methods by simulation and found the Yates correction too conservative. He recommended the N−1 chi-squared test (Pearson χ² multiplied by (N−1)/N) when all expected counts are at least 1, and the Fisher test otherwise. For Chinese journals, follow the textbook rule; for international journals, Fisher's exact test or the uncorrected chi-squared can be reported directly with the reason stated.
When reporting an OR from the Fisher test, note that R gives the conditional maximum likelihood estimate and SciPy gives the sample OR ad/bc; with small samples they differ noticeably (8.15 vs 9.33). State how the OR and its 95% CI were computed, or estimate it with logistic regression throughout.
McNemar's test (the 1600-person repeated poll from the R help page, discordant pairs b=150, c=86): continuity-corrected χ²=16.82, P=4.1×10⁻⁵; uncorrected χ²=17.36, P=3.1×10⁻⁵; exact binomial test on discordant pairs P=3.7×10⁻⁵. R's mcnemar.test applies the correction by default, while statsmodels' mcnemar defaults to exact=True, so the default results differ.
| Data (tested on this page) | Pearson χ² (uncorrected) | Yates-corrected χ² | Fisher's exact test | OR estimate |
|---|---|---|---|---|
| infert: history of spontaneous abortion × case/control (n=248, smallest expected count 35.8) | χ²=27.18, P=1.8×10⁻⁷ | χ²=25.79, P=3.8×10⁻⁷ | P=3.5×10⁻⁷ | R fisher.test conditional MLE OR=4.24 (95% CI 2.35 to 7.80); scipy fisher_exact sample OR=4.27 |
| Constructed small table [[7,3],[2,8]] (n=20, expected counts 4.5 and 5.5) | — | χ²=3.23, P=0.072; R warns Chi-squared approximation may be incorrect | P=0.070 | R conditional OR=8.15; scipy sample OR=9.33 |
Correlation and regression
A correlation coefficient describes the strength of a linear or monotonic association; a regression coefficient describes an effect size adjusted for other variables. Neither alone supports a causal conclusion.
The number of predictors in logistic and Cox regression is limited by the number of events. Peduzzi et al. (1996) proposed at least 10 events per variable (EPV≥10) from simulation; later work treats this only as a rough lower bound, and sample sizes for prediction models should be calculated with the method of Riley et al. Putting 10 predictors into a model with only 30 events gives severely biased coefficients.
Pearson and Spearman (tested on this page)
airquality ozone vs temperature (n=116): Pearson r=0.698 (95% CI 0.591 to 0.781), Spearman ρ=0.774. Ozone is right-skewed, and the gap between the two indicates a monotonic but non-linear relationship. R's cor.test warns Cannot compute exact p-value with ties for Spearman with ties; add exact = FALSE to use the approximation.
Correlation is not agreement
When two measurement methods are compared, their readings can be highly correlated yet systematically different. Bland and Altman (1986, Lancet) showed that judging agreement with correlation is misleading; compute the mean of the differences and the 95% limits of agreement (mean ±1.96 SD).
Repeated measurements per subject cannot be pooled
Pooling repeated measurements from the same subjects inflates the sample size, and the correlation may only reflect differences between subjects. Bland and Altman (BMJ 1995) gave methods for within-subject and between-subject correlation.
Linear regression (tested on this page)
log(ozone) ~ temperature + wind: temperature coefficient 0.0574 (95% CI 0.0446 to 0.0702), so each 1°F rise increases ozone by about exp(0.0574)−1=5.9%; wind coefficient −0.0525 (95% CI −0.0865 to −0.0186); R²=0.582. R's lm and statsmodels' ols agree. Coefficients on a log-transformed outcome are read as percentage changes.
Logistic regression (tested on this page)
infert data: each additional spontaneous abortion gives OR=3.37 (Wald 95% CI 2.22 to 5.12), P=1.2×10⁻⁸. R's confint(glm) returns profile-likelihood intervals by default (2.24 to 5.19); statsmodels and SPSS give Wald intervals, matching R's confint.default. The two are close in large samples and differ in small samples or with large ORs.
Cox regression (tested on this page)
survival::lung (n=228, 165 events): female vs male HR=0.599 (95% CI 0.431 to 0.831), P=0.0022; age HR=1.017 (95% CI 0.999 to 1.036), P=0.065. The cox.zph global test P=0.25, so proportional hazards is not rejected. R's coxph and lifelines use the Efron method for tied events by default and agree; statsmodels' PHReg defaults to Breslow, giving a sex coefficient of −0.51256 (R: −0.51322).
Common errors
Each of these can be checked before submission; each item gives the consequence and the fix.
Treating repeated or paired data as independent
The standard error is wrong. On the sleep data: paired t-test P=0.0028, independent-samples t-test P=0.079. Ordinary ANOVA on several time points from the same patients, or treating several lesions from one patient as separate samples, are the same error. Fix: paired t-test, repeated-measures ANOVA or mixed model, with the unit of analysis matching the sampling unit.
Pairwise t-tests among several groups
False positives increase. On PlantGrowth: ctrl vs trt2 pairwise t-test P=0.048, Tukey-adjusted P=0.198. Fix: run ANOVA or Kruskal-Wallis first, then Tukey, Games-Howell, Dunnett or Dunn tests.
Reading correlation as causation or as agreement
Correlations in observational data may come from confounding. Write "associated with", not "causes" or "affects"; adjust for confounders with multivariable regression and list the adjusted variables. Assess measurement agreement with the Bland-Altman method.
Many tests on a small sample
Comparing 20 outcomes with 10 subjects per group yields about 1 P<0.05 on average even if the drug has no effect. Fix: specify the primary outcome in the protocol; report the rest as exploratory or adjust for multiple comparisons.
Reporting P values without effect sizes and confidence intervals
A P value does not show the size of a difference. SAMPL requires effect estimates (mean difference, risk difference, OR, HR, etc.) with 95% CIs for primary outcomes. P=0.04 with a mean difference of 0.5 mmHg and with 15 mmHg mean very different things clinically.
Writing P≥0.05 as "no difference between groups"
Absence of statistical significance is not evidence of no difference (Altman and Bland 1995, BMJ). With small samples the confidence interval often includes both clinically important differences and 0. Fix: write "the difference was not statistically significant" and report the confidence interval; showing equivalence needs an equivalence or non-inferiority design.
Using SE to describe variability
The SE is an inferential statistic, roughly the half-width of a 68% confidence interval. SAMPL requires the SD for approximately normal data, written as "mean (SD)" rather than "mean ± SD"; skewed data are summarized with medians and interquartile ranges, giving both bounds.
Reporting
Compiled from the SAMPL guidelines (Lang and Altman). Where the target journal has its own statistical requirements, follow the journal.
Choosing an effect size: use Hedges' g for two-group means (less biased than Cohen's d in small samples); for paired designs, dz (mean difference divided by the SD of differences) is common. Software defines "paired Cohen's d" differently: on the sleep data dz=1.28, while pingouin's ttest(paired=True) reports cohen_d=0.83 (using the average SD of the two measurements as the denominator). State the formula in the paper. Cohen's 0.2, 0.5 and 0.8 benchmarks come from the behavioral sciences; in medical research, interpret differences in original units against the minimal clinically important difference.
| Result type | Example wording (data tested on this page) |
|---|---|
| Two-group means | Ozone concentrations in June and August were 29.4 (18.2) and 60.0 (39.7) ppb; mean difference −30.5 ppb (95% CI −50.7 to −10.4), Welch t=−3.09, df=30.0, P=0.004, Hedges' g=−0.96 (95% CI −1.61 to −0.30). |
| Paired comparison | Drug 2 increased sleep by 1.58 hours more than drug 1 (95% CI 0.70 to 2.46), paired t=4.06, df=9, P=0.003. |
| Several groups | Dry weight differed among the 3 groups, F(2, 27)=4.85, P=0.016, ω²=0.20; Tukey tests showed trt2 was 0.87 g higher than trt1 (95% CI 0.17 to 1.56, adjusted P=0.012). |
| Proportions | A history of spontaneous abortion was more common among cases than controls, OR=4.24 (95% CI 2.35 to 7.80), Fisher's exact test P<0.001. |
| Survival | After adjustment for age, women had a lower risk of death than men, HR=0.60 (95% CI 0.43 to 0.83), P=0.002. |
- Report P values as equalities: P=0.03, P=0.22, not P<0.05 or "NS". The smallest P value that needs reporting is P<0.001, except in genetic association studies.
- For primary outcomes, report effect estimates with 95% CIs, such as mean differences, risk differences, ORs and HRs.
- State which method was used for each analysis instead of listing all method names at the end of the statistics paragraph.
- State whether tests were one- or two-sided (justify one-sided tests), the alpha level, and whether and how multiple comparisons were adjusted.
- State how assumptions were checked: skewed data were analyzed with nonparametric methods, paired data with paired methods, and linear regression checked for linearity and residuals.
- Give numerators and denominators for percentages and the sample size for each analysis.
- Name the statistical software and version, for example R 4.4.3 (car 3.1) or SPSS 27.0.
- Label post hoc and unplanned subgroup analyses as exploratory.
Software map
The three packages use different defaults for the same method, so the same data can give different P values. R and Python results in the table were run on this page; SPSS menus were checked against IBM documentation and not run locally.
R aov, cylinders entered first (type I) P = 0.056
R aov, transmission entered first (type I) P = 4.9e-07
car::Anova type = 2 / pingouin P = 0.056
car::Anova type = 3 (contr.sum) P = 0.083
statsmodels anova_lm(typ=3) P = 0.083 # matches SPSS default type III| Method | R | Python | SPSS menu | Default differences |
|---|---|---|---|---|
| Welch t / Student t | t.test(y ~ g); var.equal = TRUE for Student | scipy.stats.ttest_ind(a, b, equal_var=False); pingouin.ttest | Analyze > Compare Means > Independent-Samples T Test | R defaults to Welch; SciPy defaults to Student; pingouin's correction='auto' uses Welch only when sample sizes differ; SPSS prints both rows |
| Mann-Whitney U | wilcox.test(y ~ g) | scipy.stats.mannwhitneyu; pingouin.mwu | Analyze > Nonparametric Tests > Independent Samples | With ties, R and SciPy both use the normal approximation with continuity correction; same P on this page (0.000121) |
| Paired t / Wilcoxon signed-rank | t.test(x, y, paired = TRUE); wilcox.test(x, y, paired = TRUE) | scipy.stats.ttest_rel; scipy.stats.wilcoxon | Analyze > Compare Means > Paired-Samples T Test; Nonparametric Tests > Related Samples | With zero differences and ties, R uses the normal approximation with correction (P=0.0091) while SciPy 1.18 gives an exact P by default (0.0039); method='approx', correction=True matches R |
| One-way ANOVA / Welch's ANOVA | aov; oneway.test | pingouin.anova; pingouin.welch_anova | Analyze > Compare Means > One-Way ANOVA (tick Welch under Options) | Same results |
| Post hoc tests | TukeyHSD; rstatix::games_howell_test; rstatix::dunn_test | pingouin.pairwise_tukey; pairwise_gameshowell; statsmodels pairwise_tukeyhsd | One-Way ANOVA Post Hoc: Tukey, Dunnett, Games-Howell | Same results |
| Multi-factor ANOVA | car::Anova(lm(...), type = 3) with contr.sum | statsmodels anova_lm(typ=3) with C(x, Sum); pingouin.anova defaults to type II | Analyze > General Linear Model > Univariate (type III by default) | R's aov and statsmodels default to type I (sequential) sums of squares; in unbalanced designs the result depends on variable order, see the test below |
| Repeated-measures ANOVA | afex::aov_ez | pingouin.rm_anova(correction=True) | Analyze > General Linear Model > Repeated Measures | Same results; all report Mauchly's test and the GG correction |
| Chi-squared / Fisher | chisq.test; fisher.test | scipy.stats.chi2_contingency; fisher_exact | Analyze > Descriptive Statistics > Crosstabs > Statistics: Chi-square | R and SciPy apply Yates by default for 2×2 tables; Fisher OR definitions differ (conditional MLE vs sample OR) |
| McNemar | mcnemar.test | statsmodels mcnemar | Crosstabs > Statistics: McNemar | R defaults to the continuity-corrected chi-squared, statsmodels to the exact binomial test |
| Correlation | cor.test(method = 'pearson' / 'spearman') | scipy.stats.pearsonr; pingouin.corr | Analyze > Correlate > Bivariate | Same results; pingouin reports the 95% CI directly |
| Linear / logistic regression | lm; glm(family = binomial) | statsmodels ols; logit | Analyze > Regression > Linear; Binary Logistic | R's confint(glm) gives profile-likelihood intervals; statsmodels and SPSS give Wald intervals |
| Cox regression | survival::coxph | lifelines.CoxPHFitter; statsmodels PHReg | Analyze > Survival > Cox Regression | R and lifelines default to Efron ties, statsmodels to Breslow |
| Multiple comparison adjustment | p.adjust(p, 'holm' / 'BH') | statsmodels multipletests(method='holm' / 'fdr_bh') | Bonferroni built into some procedures | Same results |
| Principal component analysis | prcomp(x, scale. = TRUE) | sklearn PCA + StandardScaler | Analyze > Dimension Reduction > Factor (extraction: principal components) | R's rotation and sklearn's components_ are eigenvectors; the SPSS component matrix contains loadings; sklearn's explained_variance_ is n/(n−1) times the correlation-matrix eigenvalues |
Runnable code
The code below ran in this page's test environment. The R code uses built-in datasets directly; the Python code reads CSV files exported from R so both sides use identical data.
If rstatix and effectsize are both loaded in R, both provide a cohens_d function; the one loaded later masks the other, and incompatible arguments raise unused argument (pooled_sd = FALSE). Writing effectsize::cohens_d avoids this.
pingouin is slow to import the first time (about 80 seconds on the test machine while it builds its cache) and normal afterwards.
# R 4.4:install.packages(c("car", "effectsize", "rstatix", "afex", "survival"))
library(car); library(effectsize); library(rstatix); library(afex); library(survival)
# Two independent groups: airquality ozone, June (n=9) vs August (n=26)
aq <- subset(airquality, Month %in% c(6, 8) & !is.na(Ozone)); aq$Month <- factor(aq$Month)
t.test(Ozone ~ Month, data = aq) # R defaults to Welch
t.test(Ozone ~ Month, data = aq, var.equal = TRUE) # Student t, the first row in SPSS
wilcox.test(Ozone ~ Month, data = aq, conf.int = TRUE)
effectsize::hedges_g(Ozone ~ Month, data = aq, pooled_sd = FALSE)
# Paired: sleep data, same subjects under two drugs
w <- reshape(sleep, idvar = "ID", timevar = "group", direction = "wide")
t.test(w$extra.2, w$extra.1, paired = TRUE)
wilcox.test(w$extra.2, w$extra.1, paired = TRUE, exact = FALSE)
# Several groups: one-way ANOVA, Welch ANOVA, post hoc tests, Kruskal-Wallis + Dunn
fit <- aov(weight ~ group, data = PlantGrowth); summary(fit)
oneway.test(weight ~ group, data = PlantGrowth) # Welch ANOVA
TukeyHSD(fit)
games_howell_test(PlantGrowth, weight ~ group) # post hoc test for unequal variances
effectsize::omega_squared(fit)
aq_all <- subset(airquality, !is.na(Ozone)); aq_all$Month <- factor(aq_all$Month)
kruskal.test(Ozone ~ Month, data = aq_all)
dunn_test(aq_all, Ozone ~ Month, p.adjust.method = "holm")
# Repeated measures: Indometh, 6 subjects at 5 time points
ind <- subset(as.data.frame(Indometh), time %in% c(0.5, 1, 2, 4, 8))
ind$Subject <- factor(as.character(ind$Subject)); ind$time <- factor(ind$time)
rm <- aov_ez(id = "Subject", dv = "conc", data = ind, within = "time")
summary(rm) # Mauchly test and GG/HF corrections
friedman.test(conc ~ time | Subject, data = ind)
# Categorical: chi-squared, Fisher, McNemar
tab <- table(infert$spontaneous > 0, infert$case)
chisq.test(tab)$expected # check expected counts first
chisq.test(tab, correct = FALSE); fisher.test(tab)
mcnemar.test(matrix(c(794, 86, 150, 570), 2))
# Correlation and regression
cor.test(aq_all$Ozone, aq_all$Temp) # Pearson, with 95% CI
cor.test(aq_all$Ozone, aq_all$Temp, method = "spearman", exact = FALSE)
m <- lm(log(Ozone) ~ Temp + Wind, data = aq_all); summary(m); confint(m)
g <- glm(case ~ spontaneous + induced + age, family = binomial, data = infert)
exp(cbind(OR = coef(g), confint.default(g)))
cx <- coxph(Surv(time, status) ~ age + sex, data = lung); summary(cx); cox.zph(cx)
# Multiple comparison adjustment
p <- c(0.001, 0.008, 0.012, 0.021, 0.035, 0.048, 0.09, 0.2, 0.41, 0.74)
round(cbind(p, bonf = p.adjust(p, "bonferroni"), holm = p.adjust(p, "holm"), BH = p.adjust(p, "BH")), 4)library(survival)
write.csv(airquality, "airquality.csv", row.names = FALSE); write.csv(sleep, "sleep.csv", row.names = FALSE)
write.csv(PlantGrowth, "PlantGrowth.csv", row.names = FALSE); write.csv(as.data.frame(Indometh), "Indometh.csv", row.names = FALSE)
write.csv(infert, "infert.csv", row.names = FALSE); write.csv(lung, "lung.csv", row.names = FALSE)# Python 3.12:pip install scipy statsmodels pingouin pandas lifelines
# Export the data from R first: write.csv(airquality, "airquality.csv", row.names = FALSE), etc.
import numpy as np, pandas as pd, pingouin as pg
from scipy import stats
import statsmodels.formula.api as smf
from statsmodels.stats.multitest import multipletests
from statsmodels.stats.contingency_tables import mcnemar
aq = pd.read_csv("airquality.csv").dropna(subset=["Ozone"])
x6, x8 = aq.Ozone[aq.Month == 6], aq.Ozone[aq.Month == 8]
print(stats.ttest_ind(x6, x8, equal_var=False)) # SciPy defaults to equal_var=True; set False explicitly for Welch
print(pg.ttest(x6, x8, correction=True)) # pingouin default 'auto' uses Welch only when sample sizes differ
print(stats.mannwhitneyu(x6, x8))
sleep = pd.read_csv("sleep.csv").pivot(index="ID", columns="group", values="extra")
print(stats.ttest_rel(sleep[2], sleep[1]))
print(stats.wilcoxon(sleep[2], sleep[1], method="approx", correction=True)) # matches the R default
pg_df = pd.read_csv("PlantGrowth.csv")
print(pg.anova(pg_df, dv="weight", between="group", detailed=True))
print(pg.welch_anova(pg_df, dv="weight", between="group"))
print(pg.pairwise_tukey(pg_df, dv="weight", between="group"))
print(pg.pairwise_gameshowell(pg_df, dv="weight", between="group"))
print(pg.kruskal(aq, dv="Ozone", between="Month"))
ind = pd.read_csv("Indometh.csv"); ind = ind[ind.time.isin([0.5, 1, 2, 4, 8])]
print(pg.rm_anova(ind, dv="conc", within="time", subject="Subject", correction=True))
print(pg.friedman(ind, dv="conc", within="time", subject="Subject"))
inf = pd.read_csv("infert.csv")
tab = pd.crosstab(inf.spontaneous > 0, inf.case).to_numpy()
chi2, p, dof, expected = stats.chi2_contingency(tab, correction=False); print(chi2, p, expected.round(1))
print(stats.fisher_exact(tab))
print(mcnemar(np.array([[794, 150], [86, 570]]), exact=False, correction=True))
print(pg.corr(aq.Ozone, aq.Temp), pg.corr(aq.Ozone, aq.Temp, method="spearman"))
print(smf.ols("np.log(Ozone) ~ Temp + Wind", data=aq).fit().summary())
logit = smf.logit("case ~ spontaneous + induced + age", data=inf).fit(disp=0)
print(np.exp(pd.concat([logit.params, logit.conf_int()], axis=1)))
from lifelines import CoxPHFitter
lung = pd.read_csv("lung.csv")[["time", "status", "age", "sex"]].dropna(); lung["status"] -= 1
print(CoxPHFitter().fit(lung, "time", "status").summary[["exp(coef)", "exp(coef) lower 95%", "exp(coef) upper 95%", "p"]])
p = [0.001, 0.008, 0.012, 0.021, 0.035, 0.048, 0.09, 0.2, 0.41, 0.74]
for m in ["bonferroni", "holm", "fdr_bh"]: print(m, multipletests(p, method=m)[1].round(4))Principal component analysis
Principal component analysis (PCA) combines several correlated continuous variables into a few uncorrelated composite variables, for dimension reduction, composite indices or handling collinearity before regression. It describes the variance structure of the variables and tests no hypothesis.
R's prcomp returns eigenvectors in rotation, while the SPSS "component matrix" from the Factor menu with principal components extraction contains loadings. To get the coefficients of the component equations from the SPSS component matrix, divide each column by the square root of its eigenvalue. sklearn's explained_variance_ uses n−1 as the divisor while StandardScaler standardizes with n, so here sklearn's first eigenvalue is 2.622 versus the correlation-matrix eigenvalue 2.613; the proportions of variance explained are unaffected.
library(survival)
labs <- c("bili", "chol", "albumin", "copper", "alk.phos", "ast", "trig", "platelet", "protime")
pb <- na.omit(pbc[, labs]) # 276 complete cases
pca <- prcomp(pb, scale. = TRUE) # standardize: the variables have different units
summary(pca) # proportion and cumulative proportion of variance
eig <- pca$sdev^2 # eigenvalues
loadings <- sweep(pca$rotation, 2, pca$sdev, "*") # loadings = eigenvectors x sqrt(eigenvalues), i.e. the SPSS component matrix
round(loadings[, 1:2], 3)
# Parallel analysis: keep components whose eigenvalue exceeds the 95th percentile of random data
set.seed(2026)
sim <- replicate(500, eigen(cor(matrix(rnorm(nrow(pb) * ncol(pb)), nrow(pb))))$values)
rbind(observed = round(eig, 3), random95 = round(apply(sim, 1, quantile, 0.95), 3))import numpy as np, pandas as pd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
pb = pd.read_csv("pbc_labs.csv") # exported in R with write.csv(na.omit(pbc[, labs]), ...)
pca = PCA().fit(StandardScaler().fit_transform(pb))
print(pca.explained_variance_ratio_.round(4)) # same as Proportion of Variance in R summary(pca)
eig = np.linalg.eigvalsh(np.corrcoef(pb.T.to_numpy()))[::-1] # eigenvalues of the correlation matrix, same as R and SPSS
loadings = pca.components_.T * np.sqrt(eig) # signs may be flipped relative to R; interpret relative directions
print(pd.DataFrame(loadings[:, :2], index=pb.columns, columns=["PC1", "PC2"]).round(3))- 01
Check that PCA applies
Variables are continuous and moderately correlated (most |r|>0.3 in the correlation matrix). A common rule of thumb is a sample size of at least 5 to 10 times the number of variables. The KMO measure and Bartlett's test of sphericity come from factor analysis; they can be consulted for PCA but are not required.
- 02
Standardize first
Variables with different units must be standardized (scale. = TRUE in R, i.e. a correlation-matrix PCA). On the PBC data (9 laboratory variables from survival::pbc, 276 complete cases): without standardization, the first component explains 98.3% of the variance, almost all from alkaline phosphatase, which has the largest values and a loading of −1.000; after standardization the first component explains 29.0%.
- 03
Decide how many components to keep
The standardized eigenvalues are 2.613, 1.483, 0.991, 0.876 and so on. The Kaiser rule (eigenvalue >1) keeps 2; parallel analysis (comparison with the 95th percentiles of eigenvalues from random data of the same size, 1.362, 1.249, 1.165) also keeps 2. The third eigenvalue, 0.991, is close to 1; the Kaiser rule is unstable in such borderline cases and parallel analysis is more reliable. A cumulative 85% of variance is a common criterion in Chinese tutorials; here the first 2 components explain only 45.5% cumulatively, so these variables cannot be summarized by a few components.
- 04
Read the loadings
A loading is the correlation between a variable and a component, equal to the eigenvector multiplied by the square root of its eigenvalue. Here bilirubin (0.836), copper (0.661), AST (0.618), triglycerides (0.561) and cholesterol (0.527) load strongly on the first component and albumin at −0.476, which can be read as severity of liver damage; the second component is mainly platelets (0.756), prothrombin time (−0.533) and cholesterol (0.513). Component signs are arbitrary: the first component has opposite signs in R and Python, so interpret the relative directions of variables.
- 05
Report
Report the standardization, the number of components kept and why, the eigenvalue and variance explained for each component, and the loading matrix (usually listing variables with |loading|≥0.4). When component scores enter a regression, state how the scores were computed.
Chinese practice
The items below come from the "Xiaobai Learns Statistics" column by Feng Guoshuang, study notes on a Chinese medical statistics textbook and SPSS PCA tutorials on CSDN, checked against this page's tests and the English literature.
"Standard drug + new drug" vs "standard drug" does not prove the new drug works
Feng Guoshuang describes a manuscript he reviewed: 24 mice in three groups (control, standard drug A, A + new drug B), with the A+B vs A difference used to claim B works. A and B may interact, so the difference cannot be attributed to B alone. The fix is to add a B-only group, forming a 2×2 factorial design analyzed with two-way ANOVA to test both main effects and the interaction.
Do not analyze postoperative time points as a randomized block design
His second example: visual fields of 46 LASIK patients measured before surgery and 1 day, 1 month, 3 months and 6 months after, analyzed as a randomized block ANOVA. Time points cannot be randomized, and adjacent time points of the same patient are usually more strongly correlated, which violates the randomized block assumptions. Analyze them as repeated measures, reporting the sphericity test and correction, or use a mixed model.
Chinese textbook rules for 2×2 and paired chi-squared tests
From textbook study notes: for 2×2 tables, Pearson's chi-squared when n≥40 and all T≥5, continuity-corrected chi-squared when n≥40 and some 1≤T<5, Fisher's exact test when n<40 or any T<1; for paired 2×2 tables, the corrected McNemar formula when b+c<40. Reviewers at Chinese journals mostly follow these rules. R's chisq.test applies the correction regardless, so set correct = FALSE and apply the rule yourself when reporting by the textbook.
Dividing the SPSS component matrix by the square root of the eigenvalue gives the eigenvectors
SPSS PCA tutorials on CSDN (for example zyq357's "SPSS steps for principal component analysis") commonly run PCA through Dimension Reduction > Factor, then divide each column of the component matrix by the square root of its eigenvalue to get the coefficients of the component equations. This page verified the relationship in R: rotation multiplied by sdev equals the loading matrix.
Two incorrect claims about post hoc tests in Chinese tutorials
Some Chinese tutorials state that "Tukey's method only applies when group sizes are equal" and that "the SNK method controls type I error". The first is wrong: the Tukey-Kramer method handles unequal sizes, and R and SPSS apply it automatically. The second holds only when all group means are equal; when only some are equal, SNK does not control the familywise error rate.
Hand off to an agent
Data cleaning, test selection, double computation in R and Python and writing results in SAMPL format can be handed to the science agent; judgment and interpretation remain the researcher's job.
Example instruction: "Here is my clinical data, data.xlsx. The primary outcome is HbA1c at 6 months, comparing three treatment groups, with repeated measurements at baseline and 3 months. Choose tests by data type, compute them in both R and Python and cross-check, and write the results paragraph and tables following the SAMPL guidelines."
The agent works in an isolated cloud computer: it reads the data and checks missing values, outliers and variable types; draws histograms and Q-Q plots by group and computes skewness and kurtosis; chooses the overall and post hoc tests from this page's decision table; computes them in R and Python and compares P values, effect sizes and confidence intervals item by item, tracing any difference to a default setting; and produces result tables, figures and a methods paragraph. The workspace keeps the scripts, software versions, logs and intermediate results, and the agent reviews its results adversarially. The task keeps running after you shut down your computer.
You still need to check: whether the unit of analysis matches the sampling unit; whether the primary and secondary outcomes match the protocol; the family of tests used for multiple comparison adjustment; and the clinical interpretation of the results.
References
- Lang T, Altman DG. SAMPL Guidelines (EQUATOR Network) — P value format, effect sizes with 95% CIs, mean (SD) format, SE not used for variability
- Delacre M, Lakens D, Leys C. Why psychologists should by default use Welch's t-test instead of Student's t-test. Int Rev Soc Psychol 2017 — Basis for Welch's t as the default; problems with choosing after a variance test
- Rochon J, Gondan M, Kieser M. To test or not to test: preliminary assessment of normality. BMC Med Res Methodol 2012 — The two-stage procedure of testing normality before choosing a t-test or rank test
- Lumley T, et al. The importance of the normality assumption in large public health data sets. Annu Rev Public Health 2002 — Robustness of t-tests and linear regression in large samples; "large enough" is often under 100
- Kim HY. Assessing normal distribution (2) using skewness and kurtosis. Restor Dent Endod 2013 — Skewness and kurtosis thresholds by sample size
- Hart A. Mann-Whitney test is not just a test of medians. BMJ 2001 — Interpreting Mann-Whitney results
- Bland JM, Altman DG. Multiple significance tests: the Bonferroni method. BMJ 1995 — False positives from multiple tests and the Bonferroni correction
- Bender R, Lange S. Adjusting for multiple testing—when and how? J Clin Epidemiol 2001 — When to adjust in confirmatory and exploratory studies
- Summary of the NEJM 2019 statistical reporting guidelines (Harrington et al.) — Secondary outcomes without pre-specified adjustment reported as estimates with 95% CIs
- Campbell I. Chi-squared and Fisher–Irwin tests of two-by-two tables with small sample recommendations. Stat Med 2007 — Choosing between the N−1 chi-squared and Fisher tests
- Bland JM, Altman DG. Statistical methods for assessing agreement between two methods of clinical measurement. Lancet 1986 — Correlation cannot assess agreement
- Bland JM, Altman DG. Statistics Notes index (University of York) — Correlation with repeated observations, Absence of evidence is not evidence of absence, and others
- Peduzzi P, et al. A simulation study of the number of events per variable in logistic regression analysis. J Clin Epidemiol 1996 — Source of EPV≥10
- What's new in IBM SPSS Statistics 27 — Effect sizes with confidence intervals for t-tests and one-way ANOVA from SPSS 27
- IBM SPSS tutorial: Independent-Samples T Test table — Meaning of the two rows in the SPSS independent-samples t-test
- SciPy documentation: scipy.stats.ttest_ind — equal_var defaults to True
- pingouin documentation: pingouin.ttest — Behavior of correction='auto'
- Practitioner post: Xiaobai Learns Statistics, "Common misuses of ANOVA in papers" (repost by Yikahui, CSDN, Chinese) — Review cases of a missing factorial group and repeated measures analyzed as randomized blocks
- Practitioner post: study notes on Practical Medical Statistics and SAS, comparing categorical data (CSDN, Chinese) — Chinese textbook rules for 2×2, paired and R×C chi-squared tests
- Practitioner post: SPSS steps for principal component analysis (CSDN, Chinese) — Dividing the SPSS component matrix by the square root of the eigenvalue
- Practitioner post: meaning and choice of pairwise multiple comparison methods in ANOVA (CSDN, Chinese) — Incorrect claims about Tukey and SNK in Chinese tutorials, corrected on this page