Skip to main content
v2026.11,610 entries · CC-BY 4.0

Running a t-Test in R: t.test() Syntax and Output

Base R’s t.test() function: formula vs. vector calling syntax, why var.equal defaults to Welch’s t-test instead of the pooled version, and extracting p-values, confidence intervals, and group means from the returned object for a report.

Ask about Running a t-Test in R: t.test() Syntax and Output

Answers are drawn from this guide and the rest of the CASRAI corpus, with a link to every source.

Answers are AI-generated from CASRAI’s own published pages and can be wrong, so check the linked sources before relying on one; your question is logged without personal data — never sold, never used to train a third-party model — to show us what CASRAI is missing, so please do not type personal or confidential details. How we use this

Written and maintained by CASRAI Editorial Board

Last updated

R’s base t.test() function runs all three t-test designs — one-sample, two-sample independent, and paired — through one function, accepts data either as a formula (outcome ~ group) or as separate vectors, and defaults to Welch’s t-test rather than the equal-variances (pooled) version most other stats packages start from. This page is the R-specific procedure: both calling conventions, what the var.equal argument actually changes, and how to pull the numbers back out of the returned object for a write-up instead of re-typing them by hand.

For the statistical logic behind the test itself — why the three designs exist, the assumptions, and when a t-test is the wrong tool — see CASRAI’s guide to the t-test first and come back here to run it in R specifically. Running the same test in another package? See Stata or SPSS.

Formula interface vs. vector interface

t.test() is a generic function with two methods you’ll actually use:

  • Formula interfacet.test(outcome ~ group, data = df). outcome is a numeric column, group is a factor (or a column R can coerce to one) with exactly two levels. This is the natural call when your data is already in one long-format data frame — one row per observation, one column marking which group it belongs to.
  • Vector (default) interfacet.test(x, y), where x and y are two separate numeric vectors. This is the natural call for a one-sample test (just x) or when your two groups already live in two separate columns/objects rather than one long column plus a grouping factor.

The formula method only accepts a two-level grouping factor — a third level, or a numeric/character column with more than two distinct values, fails with grouping factor must have exactly 2 levels rather than silently comparing the first two.

One-sample t-test

t.test(x, mu = 0)

mu is the fixed value you’re testing the sample mean against (it defaults to 0, so set it explicitly for anything else). Example — testing whether a set of survey satisfaction scores differs from a scale midpoint of 3:

t.test(scores, mu = 3)

Independent two-sample t-test

Formula interface, with data in one long-format data frame:

t.test(score ~ group, data = df)

Vector interface, with the two groups already in separate objects:

t.test(treatment, control)

Both return the same test — t.test() doesn’t care which interface produced the two samples it’s comparing, only what they contain.

Paired t-test

Set paired = TRUE and pass the two measurement vectors in matching order (row 1 of before pairs with row 1 of after, and so on):

t.test(before, after, paired = TRUE)

Passing unpaired data with paired = TRUE — vectors of different length, or the wrong row order — either errors immediately (unequal lengths) or silently pairs the wrong observations (matching lengths, wrong order), so confirm the row correspondence before running it, not after reading the output.

The var.equal argument: why R defaults to Welch’s test

t.test()‘s var.equal argument defaults to FALSE. That default matters more than it looks: with var.equal = FALSE, R runs Welch’s t-test — it does not assume the two groups have equal population variances, and it adjusts the degrees of freedom downward (via the Welch–Satterthwaite equation) to compensate, which is why a Welch result’s degrees of freedom is usually a non-integer like 27.19 rather than a round number. Set var.equal = TRUE to run the classic Student’s pooled-variance t-test instead — the version that assumes both groups share one population variance and uses the simpler n1 + n2 - 2 degrees of freedom.

This is a real, load-bearing difference from the other packages this same guide series covers: Stata’s ttest command pools by default and needs the explicit unequal option to switch to Welch’s version, and SPSS runs Levene’s Test for Equality of Variances alongside the t-test and expects you to pick the correct output row based on that result, rather than picking the test up front. R instead defaults to the option that doesn’t require the equal-variances assumption at all — Welch’s test is valid whether or not the variances are actually equal, which is also why current statistical guidance increasingly treats Welch’s as the safer default rather than a special case (see CASRAI’s z-test vs. t-test comparison for related test-selection reasoning). If you have a specific reason to assume equal variances — a study design that guarantees it, or a requirement to match a pooled-variance result reported elsewhere — set var.equal = TRUE explicitly rather than relying on the default doing something else.

Reading the object t.test() returns

t.test() doesn’t just print output — it returns a list (class "htest") you can assign and query directly, which is what makes it usable inside a script or an R Markdown/Quarto report instead of copy-typed from the console:

result <- t.test(score ~ group, data = df)
result$statistic   # the t value
result$parameter   # degrees of freedom
result$p.value     # the p-value
result$estimate    # group mean(s)
result$conf.int    # confidence interval for the difference (or mean)
result$method      # which test actually ran, e.g. "Welch Two Sample t-test"

result$method is worth checking explicitly the first time you run a new comparison — it states in plain text whether R ran the Welch or the Student version, which is a fast way to confirm var.equal did what you expected without re-deriving it from the degrees of freedom.

Extracting results for a report

For inline reporting, pull the specific values out of the list rather than re-typing console output by hand — this is both less error-prone and reproducible if the underlying data changes:

sprintf("t(%.2f) = %.2f, p = %.3f",
        result$parameter, result$statistic, result$p.value)

The broom package’s tidy() function does the same job for a whole table of results at once, which is useful when a script runs the same test across several outcome variables in a loop: broom::tidy(result) returns a one-row tibble with estimate (the mean difference, or group means as estimate1/estimate2 for the two-sample case), statistic, p.value, parameter (df), and conf.low/conf.high as plain columns — row-bindable across multiple tests instead of extracting each field by hand every time. See CASRAI’s guide to reporting confidence intervals and what a p-value means for how to phrase the surrounding sentence once you have the numbers, and Cohen’s d for the effect-size figure a t-test result alone doesn’t give you — t.test() reports significance, not effect size, and a complete report needs both.

Common errors

  • data are essentially constant — one of the groups (or the single sample, for a one-sample test) has zero variance, so the standard error is zero and the t-statistic is undefined. Check for a data-entry problem (a column that didn’t actually vary, or got recoded to one repeated value) before assuming the test is broken.
  • grouping factor must have exactly 2 levels — the formula interface’s grouping column has one level (nothing to compare) or three-plus (not what a t-test does — that’s ANOVA’s job). Subset to the two groups you actually want, or run droplevels() if an unused factor level survived a filter.
  • Not enough observationst.test() needs at least 2 observations per group to compute a variance at all; fewer produces a not enough 'x' observations error rather than a result.

Frequently asked questions

Does t.test() require normally distributed data?

The t-test’s validity rests on the sampling distribution of the mean being approximately normal, not the raw data itself — by the Central Limit Theorem, that holds increasingly well as sample size grows, even when the underlying data is skewed. For small samples with visibly non-normal data, check a QQ plot (qqnorm()/qqline()) or consider wilcox.test(), R’s nonparametric alternative, instead.

How do I run a one-tailed t-test in R?

Set the alternative argument to "less" or "greater" instead of the default "two.sided" — e.g. t.test(x, y, alternative = "greater") tests whether x‘s mean exceeds y‘s, one direction only.

What’s the difference between t.test() in R and Excel’s T.TEST function?

Excel’s T.TEST returns only a p-value as a single number, with the test type and tails selected via numeric argument codes; R’s t.test() returns the full test object — statistic, degrees of freedom, confidence interval, and group estimates together — so nothing extra needs to be computed separately once you’ve run it.

Do I need a package to run a t-test in R?

No — t.test() ships in the stats package, which loads automatically with every R session. No library() call or CRAN install is needed for anything covered on this page; packages like broom are only needed for the optional tidy-output step above.

See also CASRAI’s guide to logistic regression in R for a second common inferential procedure using the same extract-and-report pattern, and the research tools hub for CASRAI’s full coverage of statistical software.

Follow CASRAI

Research-administration guidance, standards updates and independent tool reviews.

Referenced across the research world

University of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logoUniversity of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logo
  • University of Cambridge logo
  • Columbia University logo
  • Crossref logo
  • University of Edinburgh logo
  • Harvard University logo
  • University of Oxford logo
  • Princeton University logo
  • Stanford School of Medicine logo
  • University College London logo
  • ORCID logo

View CASRAI adoption →

Regulatory Radar

Stop finding out after the fact

$29/month, cancel anytime. Daily digest updates from our analysis, a dashboard holding the same items, and a cited assistant for everything they raise.

  • Federal Register, Federal Register+, Grants.gov, Regulations.gov, NSF News, UKRI, plus CASRAI’s own published content.
  • 44,322 indexed passages, and every answer cites the ones it drew on.