Medical statistics / Choosing a test

How to Choose Statistical Tests for Medical Papers: t-test, ANOVA, Chi-squared, Nonparametric Tests, Correlation and Regression

This page gives a decision table for choosing a test by data type, number of groups, pairing and distribution. It explains how to use normality and equal-variance checks, multiple comparison adjustment and reporting rules. Every class of test was run in R and Python on R built-in datasets, and the page lists where default settings in R, Python and SPSS give different results.

Short answer

Choose a statistical method by checking four things: whether the outcome is continuous, ordinal or categorical; how many groups are compared; whether the data are independent, paired or repeated measures; and the distribution and sample size of continuous variables. For two independent groups of continuous data, use Welch's t-test by default, and the Mann-Whitney U test for small, clearly skewed samples. For paired data, use the paired t-test or the Wilcoxon signed-rank test. For three or more groups, use one-way ANOVA (Welch's ANOVA when variances differ) or the Kruskal-Wallis test, followed by Tukey, Games-Howell or Dunn tests for pairwise comparisons. For several time points on the same subjects, use repeated-measures ANOVA or a mixed model. Use the chi-squared test for categorical data, Fisher's exact test when expected counts are below 5, and McNemar's test for paired binary data. Use Pearson or Spearman correlation for the association of two continuous variables, and linear, logistic or Cox regression for multivariable analysis depending on the outcome. Report effect sizes, 95% confidence intervals and exact P values.

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.

QuestionOutcomeDesignFirst choiceAlternatives and when to use themEffect size to report
Compare two meansContinuousTwo independent groupsWelch's t-testWhen 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 UMean difference with 95% CI, Hedges' g
Compare two distributionsSkewed continuous or ordinalTwo independent groupsMann-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 meansHodges-Lehmann shift with 95% CI, rank-biserial correlation
Before-after or matched comparisonContinuousPaired (same or matched subjects)Paired t-testWilcoxon signed-rank test when differences are clearly skewedMean difference with 95% CI, dz
Compare three or more meansContinuousSeveral independent groupsOne-way ANOVA + Tukey testWelch's ANOVA + Games-Howell when variances differ; Dunnett when comparing only with a controlη² or ω², pairwise differences with 95% CI
Compare three or more distributionsSkewed continuous or ordinalSeveral independent groupsKruskal-Wallis test + Dunn test (Holm or Bonferroni adjustment)Ordinal logistic regression for ordered categoriesε² or η²H
Compare several time pointsContinuousRepeated measures on the same subjectsRepeated-measures ANOVA, reporting Mauchly's test and the Greenhouse-Geisser correctionLinear mixed model with missing time points or uneven intervals; Friedman test for skewed dataGeneralized η², means with 95% CI at each time point
Effects of two factorsContinuousFactorial designTwo-way ANOVA with interaction (type III sums of squares)State the sum-of-squares type for unbalanced designs; see the software sectionPartial η², simple effects
Compare proportions between groupsBinary or nominalIndependentPearson chi-squared testFisher'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 5Risk difference, RR or OR with 95% CI
Compare paired binary outcomesBinarySame subjects, two methods or before-afterMcNemar's testExact binomial test when the number of discordant pairs b+c is smallDifference in discordant proportions or paired OR
Association of two continuous variablesContinuousTwo variables on the same subjectsPearson correlation (linear, no marked outliers)Spearman correlation for skewed, ordinal or outlier-prone data; repeated measurements per subject cannot be pooledr with 95% CI
Agreement of two measurement methodsContinuousSame subjects, two methodsBland-Altman limits of agreementIntraclass correlation coefficient (ICC); do not judge agreement with a correlation coefficientMean difference and 95% limits of agreement
Multivariable adjustmentContinuousAnyLinear regressionLog-transform or use a generalized linear model for right-skewed outcomesCoefficients with 95% CI, R²
Multivariable adjustmentBinaryAnyLogistic regressionWhen the outcome is common (above about 10%), the OR overstates the RR; use log-binomial or modified Poisson regression to report RROR with 95% CI
Survival timeTime + event indicator (censored)AnyKaplan-Meier curves + log-rank test; Cox regression for multivariable analysisPiecewise or time-dependent covariates when proportional hazards failHR with 95% CI, median survival
Based on the SAMPL guidelines, the Bland and Altman BMJ Statistics Notes and Delacre et al. (2017) on Welch's t-test; see References.

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 practiceProblemRecommended practice
Run Shapiro-Wilk on every variable and switch to a nonparametric test when P<0.05Large samples give P<0.05 for trivial departures; small samples miss severe departuresInspect 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 tThe two-stage choice itself changes the type I error rate; Levene's test has low power in small samplesUse 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 testRochon et al. (2012) found this two-stage procedure formally incorrectDecide 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.

Comparisonn, mean (SD) per groupStudent's tWelch's tMann-Whitney UR 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.000165t=−4.075, df=39.3, P=0.000217W=127.5, P=0.000121Yes (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.034t=−3.092, df=30.0, P=0.0043W=57.5, P=0.026Yes
Shapiro-Wilk P=8.3×10⁻⁶ for May and P=0.090 for August. With equal sample sizes, Student's and Welch's t statistics are identical and only the degrees of freedom differ; with unequal sample sizes, the P values differ about eightfold.

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 trt1ctrl vs trt2trt1 vs trt2
Pairwise Welch t-tests, unadjusted (incorrect)P=0.250P=0.048P=0.009
Pairwise Welch t-tests + Holm adjustmentP=0.250P=0.096P=0.028
Tukey HSD (first choice with equal variances)P=0.391P=0.198P=0.012
Games-Howell (unequal variances)P=0.475P=0.113P=0.024
Tested on this page. One-way ANOVA F(2, 27)=4.85, P=0.016, η²=0.26, ω²=0.20; Welch's ANOVA F(2, 17.1)=5.18, P=0.017. R (TukeyHSD, rstatix::games_howell_test) and Python (pingouin, statsmodels pairwise_tukeyhsd) agree.
  • 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.

Tested on this page: one set of 10 P values under three adjustments (R p.adjust and statsmodels multipletests agree)text
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
SituationMethodNotes
All pairwise comparisons after ANOVATukey (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 controlDunnettNumber of comparisons is the number of groups minus 1
A few pre-planned comparisons, or a mix of different testsHolmControls 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 regionsBenjamini-Hochberg (FDR)Controls the proportion of false positives among positive results; confirm findings in independent data
Confirmatory study with a single primary outcomeNo adjustmentPre-specify it in the protocol or statistical analysis plan
Exploratory analyses, secondary outcomesMay be left unadjusted but labeled exploratoryRecommended 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 testOR 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 incorrectP=0.070R 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 typeExample wording (data tested on this page)
Two-group meansOzone 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 comparisonDrug 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 groupsDry 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).
ProportionsA 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.
SurvivalAfter 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.

Tested on this page: mtcars unbalanced two-factor design (mpg ~ cylinders × transmission), P value for the transmission main effecttext
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
MethodRPythonSPSS menuDefault differences
Welch t / Student tt.test(y ~ g); var.equal = TRUE for Studentscipy.stats.ttest_ind(a, b, equal_var=False); pingouin.ttestAnalyze > Compare Means > Independent-Samples T TestR defaults to Welch; SciPy defaults to Student; pingouin's correction='auto' uses Welch only when sample sizes differ; SPSS prints both rows
Mann-Whitney Uwilcox.test(y ~ g)scipy.stats.mannwhitneyu; pingouin.mwuAnalyze > Nonparametric Tests > Independent SamplesWith ties, R and SciPy both use the normal approximation with continuity correction; same P on this page (0.000121)
Paired t / Wilcoxon signed-rankt.test(x, y, paired = TRUE); wilcox.test(x, y, paired = TRUE)scipy.stats.ttest_rel; scipy.stats.wilcoxonAnalyze > Compare Means > Paired-Samples T Test; Nonparametric Tests > Related SamplesWith 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 ANOVAaov; oneway.testpingouin.anova; pingouin.welch_anovaAnalyze > Compare Means > One-Way ANOVA (tick Welch under Options)Same results
Post hoc testsTukeyHSD; rstatix::games_howell_test; rstatix::dunn_testpingouin.pairwise_tukey; pairwise_gameshowell; statsmodels pairwise_tukeyhsdOne-Way ANOVA Post Hoc: Tukey, Dunnett, Games-HowellSame results
Multi-factor ANOVAcar::Anova(lm(...), type = 3) with contr.sumstatsmodels anova_lm(typ=3) with C(x, Sum); pingouin.anova defaults to type IIAnalyze > 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 ANOVAafex::aov_ezpingouin.rm_anova(correction=True)Analyze > General Linear Model > Repeated MeasuresSame results; all report Mauchly's test and the GG correction
Chi-squared / Fisherchisq.test; fisher.testscipy.stats.chi2_contingency; fisher_exactAnalyze > Descriptive Statistics > Crosstabs > Statistics: Chi-squareR and SciPy apply Yates by default for 2×2 tables; Fisher OR definitions differ (conditional MLE vs sample OR)
McNemarmcnemar.teststatsmodels mcnemarCrosstabs > Statistics: McNemarR defaults to the continuity-corrected chi-squared, statsmodels to the exact binomial test
Correlationcor.test(method = 'pearson' / 'spearman')scipy.stats.pearsonr; pingouin.corrAnalyze > Correlate > BivariateSame results; pingouin reports the 95% CI directly
Linear / logistic regressionlm; glm(family = binomial)statsmodels ols; logitAnalyze > Regression > Linear; Binary LogisticR's confint(glm) gives profile-likelihood intervals; statsmodels and SPSS give Wald intervals
Cox regressionsurvival::coxphlifelines.CoxPHFitter; statsmodels PHRegAnalyze > Survival > Cox RegressionR and lifelines default to Efron ties, statsmodels to Breslow
Multiple comparison adjustmentp.adjust(p, 'holm' / 'BH')statsmodels multipletests(method='holm' / 'fdr_bh')Bonferroni built into some proceduresSame results
Principal component analysisprcomp(x, scale. = TRUE)sklearn PCA + StandardScalerAnalyze > 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: all testsr
# 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)
R: export data for the Python sider
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: the corresponding testspython
# 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.

R: PCA, loadings and parallel analysisr
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))
Python: PCA on the same datapython
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))
  1. 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.

  2. 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%.

  3. 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.

  4. 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.

  5. 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

FAQ

SPSS prints two rows for the independent-samples t-test. Which one should I use?

Use the second row, "Equal variances not assumed", which is Welch's t-test. Switching rows based on Levene's test affects the type I error rate, and Welch's t loses little power when variances are equal. If you do choose by Levene's test, note that IBM's own tutorial uses 0.10, not 0.05, as the cutoff.

How large must the sample be to skip normality testing and use a t-test?

There is no single cutoff. The review by Lumley et al. found that about 100 per group is usually enough for common data, and no more than 500 even for extremely skewed medical cost data. With fewer than 30 per group and clear skew (for example absolute skewness above 2), use the Mann-Whitney U test or a t-test on log-transformed data.

ANOVA gives P<0.05 overall, but no pair differs in post hoc tests. How do I report this?

Report it as it is. The overall F test and the post hoc tests answer different questions; the F test can be driven by a combined contrast of several groups. Report the overall result and each pairwise difference with its 95% CI, and do not switch to unadjusted pairwise t-tests.

Why do R and Python give different P values?

Usually because of default settings: SciPy's ttest_ind defaults to Student's t while R defaults to Welch; for the Wilcoxon signed-rank test with zero differences, SciPy defaults to the exact method and R to the normal approximation; R's McNemar test applies a continuity correction while statsmodels defaults to the exact test; and R's aov uses type I sums of squares for multi-factor ANOVA. After aligning these settings, every test on this page gave the same results in both.

What is the difference between principal component analysis and factor analysis?

PCA decomposes the total variance into uncorrelated components, for dimension reduction and composite indices. Factor analysis assumes observed variables are produced by a few latent factors plus unique error, aims to explain correlations among variables and usually involves rotation. SPSS puts both in the same Factor menu; choosing principal components extraction without rotation gives PCA loadings.

How many predictors can a logistic regression include?

A common rule is at least 10 outcome events per predictor (EPV≥10), counting the less frequent outcome category. For example, with 60 events, include about 6 predictors at most. For prediction models, determine the sample size with the method of Riley et al.

Hand your statistical analysis to Scientify

The science agent reads your data in an isolated cloud computer, chooses tests by study design, computes them in both R and Python and cross-checks the results, and produces SAMPL-compliant results paragraphs, tables and figures while keeping every script and log. New users get USD 5 of free credit.