Skip to content

estimatr

2.0.1Package record

Fast Estimators for Design-Based Inference

18packages dependsuggested by 21
190.7Kdownloads / year
98.3%test coverage
13/13flavors OK

Overview

About

Fast procedures for a small set of commonly-used, design-appropriate estimators with robust standard errors and confidence intervals. Includes estimators for linear regression, instrumental variables regression, difference-in-means, Horvitz-Thompson estimation, and regression improving precision of experimental estimates by interacting treatment with centered pre-treatment covariates introduced by Lin (2013) doi:10.1214/12-AOAS583. Fixed effects are absorbed by alternating projections rather than by dummy expansion, and Horvitz-Thompson variance is computed from the randomization declaration.

Changes in 2.0.1

A rank-deficient fit is reported once

The fit messages that a column was dropped, naming it; tidy() and summary() then messaged again with the count, so a rank-deficient fit inside a tidy() pipeline announced itself twice for one event. stats::lm() says it once, in the printed summary. The tidy() and summary() message is gone; the named message at the fit and the count in the printed summary stay.

The message also says what the dropped column was collinear with, which the name alone does not. In four-person cells of Y ~ Z + X_woman + X_income a covariate was sometimes constant, aliased with the intercept and leaving the treatment coefficient meaning what it did, and sometimes equal to the treatment indicator, which makes the two inseparable; both printed the same sentence. The spanning set is a regression of the dropped column on the kept ones, reported as constant, identical to a named column, or a linear combination of named columns, for up to three dropped columns.

One rule for a full-leverage observation under HC2 and HC3

Before this release, an observation at or near leverage 1 was handled by three sets that had each been right when written and had come to disagree. The variance discarded an observation at 1 - h <= 0; the count that decided whether the fit warned, and the rule that returned NA for a coefficient such observations alone identify, both used sandwich's tolerant h > 1 - sqrt(eps). An observation at h = 1 - 5e-9 was therefore counted and made its coefficient NA, under a message saying it had been dropped, while its term stayed in the meat inflated by about 2e8. The discard now uses the same tolerance as the count and the NA rule, so the three are one set and the message is true of it.

The warning also fired on every absorbed fixed-effects fit with a singleton group, which is the most common fixed-effects structure there is, and told the user to change estimator on a fit that was correct as it stood: a singleton's demeaned row is zero, so it carries no share of any reported coefficient's variance and every standard error is the one the design without it gives, to machine precision. The fit now warns if and only if some standard error is NA because the discarded observations alone identified its coefficient. Where none is, the count is reported as a message, as fixest does, and it is stored on the fit as n_leverage_near_one either way. A second warning behind the first, raised for a NaN standard error with no observation near leverage 1, diagnosed it as a leverage problem; every NaN source in the variance code other than a negative variance diagonal is now trapped, and that case already warns with the right reason, so the second warning is gone.

A standard error of numerically zero is reported as NA

A variance computed as 6e-17 is rounding error, and reported as a number it carries a t statistic of 1e16 and a p-value of 0. Three shapes reach it and the entry above catches none of them, because none of them needs an observation at leverage 1. An outcome the regressors reproduce exactly, with residual degrees of freedom left over, returns that number for every coefficient; a constant outcome returns exactly 0. A factor level confined to one arm of a treatment returns it for that arm's contrast alone, with three stars on an estimate of 2e-17, while the rest of the fit is ordinary: on a covariate balance test over 88 studies this printed a significant imbalance on levels holding one or two observations. A cluster-robust fit reaches the same number by a third route, where the within-cluster sums of scores cancel although no residual is near zero: a paired conjoint whose outcome sums to 1 within respondent returns it for every respondent-level covariate, with residuals running to plus and minus 0.5.

A coefficient's standard error is now NA when its variance is below .Machine$double.eps times the classical variance it would carry if the regressors explained nothing, tss/df times XtX_inv(j,j). The ratio is dimensionless, so it is comparable across columns of unlike scale, and it is about 1 - R^2 for a classical fit, so a fit has to reproduce its outcome to the last bit to be caught: a design with an R-squared of 1 - 1e-9 is untouched. The rule applies under every se_type. Where the outcome is reproduced exactly, a classical fit returns the same 6e-17, so the rule is not about robust variances; where the fit is clustered, the residuals can be ordinary and it is their within-cluster sums that vanish, which se_type = "HC2" on the same data does not see. The fit messages once, saying whether the outcome is reproduced exactly, and otherwise naming the coefficients and which of the two mechanisms holds.

lm() reports these designs silently, as 1.0.6 did.

The F statistic is NA where the variance of the coefficients it tests cannot be inverted

The model F is a quadratic form in the inverse of the tested coefficients' variance block, guarded until now by chol() raising an error. A block at a reciprocal condition number of 1e-32 does not raise one: the factorization succeeds and the inverse is assembled out of cancellation. On the balance test above, glance() returned F = 389 on p = 1e-146 for a contrast with no variance at all, and F = 1275 for another. The statistic is now NA when rcond() of that block is below the double precision floor, and when any coefficient under test has the NA standard error of the entry above. The classical branch reaches the same place through an R-squared of exactly 1 and is covered by the same rule.

Show all changes in 2.0.1Hide the rest of the changes in 2.0.1

Reporting NA for a coefficient's own standard error while returning a number for the joint test over the same coefficients was the inconsistency this removes.

A factor clusters no longer loses the Horvitz-Thompson standard error

horvitz_thompson() with a clustered or blocked-and-clustered declaration returned NA for the standard error, on every draw, whenever the declaration's clusters was a factor. The estimate was right; only the variance was lost. The cluster-level aggregation is tapply(), which groups by a factor's levels rather than by the values present, so every cluster absent from an arm summed to NA. Under two-arm randomization every control cluster is absent from the treated arm, so the failure needed nothing unusual: an ordinary factor cluster column was enough.

It was also reported in the wrong direction. The NA reached the guard that reports an unestimable variance bound, whose message tells the user their declaration is the problem and suggests replacing it with one carrying the design's block and cluster structure, which is what they had already supplied.

1.0.6 answers these designs and never had this bug, so it is a 2.0.0 regression. The fix restores 1.0.6's answer to fifteen digits, and a factor, character, and integer coding of the same clusters now give the same standard error.

lm_lin() says what a dropped treatment interaction costs

A rank-deficient fit already names the columns it returned as NA. In lm_lin() that is not enough, because the treatment coefficient is the effect at the covariate means only while every covariate is interacted with the treatment, and the name of a dropped column does not say that the number the reader takes for the effect has changed meaning.

The two ways a Lin design goes rank deficient are not the same event. A duplicated or constant covariate takes its own main effect down with its interaction, and the fit that remains is lm_lin() on the covariates that are left. A covariate that is constant within one treatment arm keeps its main effect and loses only its interaction: centering happens before interacting, so x_c - Z * x_c equals -mean(x) * (1 - Z), and the intercept, the treatment indicator, the centered covariate, and its interaction are an exact four-column dependency. Which of the four is dropped is then an answer rather than a naming question. On the subgroup cells of one published replication the fitted values and r.squared agree to twelve printed digits under either drop while the treatment coefficient moves from -1.56 to -2.00, so nothing in the fit's own goodness of fit reveals it. The effect at the covariate means is not identified in that cell under any version of this package or any other.

The message now adds a sentence in the second case only, naming the treatment coefficient the drop affects and saying that it is no longer the effect at the covariate means. It stays a message rather than a warning for the reasons in the entry below: stats::lm() signals nothing here, and a warning raised inside nested grouped dplyr verbs, which is how a subgroup analysis reaches this code, crashes dplyr 1.2.1.

A dropped treatment indicator is different again, and is now a warning. The other two cases leave a treatment coefficient that means something narrower than the caller asked for; this one leaves no treatment coefficient at all, printed as an NA under a name that is still there. A treatment indicator can be collinear with the rest of a Lin design only where its arm is empty or where the covariates reproduce it exactly, so there is no reading of such a fit that recovers an effect. Reaching it takes a constant treatment column under the order-preserving drop rule, since the only column ahead of the treatment is the intercept; 2.0's norm-ranked pivot reached it on 6 of 30 rank-deficient fits of one published replication, which is the pairing that makes the warning worth having.

Which collinear column is dropped now follows stats::lm()

A rank-deficient design has no unique fit, so an estimator must choose which columns to drop. stats::lm() makes that choice with LINPACK's dqrdc2, which is order preserving: it walks the columns left to right and compares each column's residual norm, after the columns already kept are projected out, against that column's own original norm, moving a failing column to the end. The later column of a collinear set is therefore the one dropped, and the ordering the modeller wrote is respected. 2.0 detected rank with Eigen's ColPivHouseholderQR, which pivots by largest remaining norm and reads nothing but the numbers, so it could drop a column written first and keep one written last. 2.0 had reproduced dqrdc2's threshold criterion without its ordering.

The consequence is that 2.0 silently changed which coefficient comes back NA on every rank-deficient fit, relative to both 1.0.6 and lm(). On a published replication whose subgroup models fit ten covariates in cells of 29 to 370 observations, 30 of 56 fits are rank deficient; lm() drops the treatment column in 0 of those 30, and 2.0 drops it in 6. Over 300 random rank-deficient designs, 2.0's drop set matched lm()'s on 57 of 300, and 2.0.1's matches on 300 of 300 with the surviving coefficients equal to lm()'s.

lm_solver() and the HC2, HC3, and CR2 meat share the rule, so the variance is read off the same columns the coefficients were fitted at.

None of this makes an unidentified coefficient identified. Where a regressor lies in the span of the others no unique coefficient exists, the reported number still moves when the columns are reordered, and lm() reports such numbers with no indication that they are arbitrary. What changes is that the choice is now predictable and follows the order the model was written in.

try_cholesky = TRUE could return a full set of coefficients at a rank-deficient design

try_cholesky = TRUE asks for the faster arithmetic, and it is meant to select the arithmetic rather than the answer. That path tests rank by reading the diagonal of the Cholesky factor of the normalised Gram matrix, where each entry stands for one column's residual after the earlier columns are projected out, and it falls back to the QR when an entry is small. A Gram matrix carries about half the digits of the residual its factor describes, so an exactly dependent column, whose residual share is 1e-16, comes back with a diagonal entry of order 1e-8 to 1e-6 depending on how the cancellation falls, and the test was against dqrdc2's own 1e-7. Whether the fallback fired was therefore decided by rounding. On y ~ a1 + a2 + a3 + b1 + b2 + b3 at 200 observations, with a2 equal to a1 plus 5% noise, a3 = a1 - a2, and b1, b2, and b3 built the same way, the smallest entry lands at 2.4e-7, no fallback fired, and the fit returned seven finite coefficients at a design of rank five, where lm() and the QR path both return two NA.

The threshold is now the Cholesky's own resolution rather than dqrdc2's, so a design whose factor cannot separate the columns goes to the QR, which makes the rank decision at 1e-7 in full precision. No design above the threshold changes, since the fallback selects the path and not the answer. The near-dependencies that reach this code elsewhere are built as a + eps * noise, which does not cancel and so lands below 1e-7, which is why the gap survived a test that walks a design from full rank to singular in twelve steps and compares both paths against lm() at each one.

HC2 and HC3 return NA for a coefficient that a full-leverage observation alone identifies

An observation the fit reproduces exactly has leverage 1 and residual 0, so HC2's contribution for it is a 0/0. 2.0 resolved that by setting the denominator to 0 wherever 1 - h <= 0, dropping those observations from the variance. Dropping them is free for every coefficient the observation carries no information about, and by Frisch-Waugh-Lovell a singleton dummy's neighbours are exactly that: they keep the standard error of the design with the singleton's row and column removed. It is not free for the coefficient those observations alone identify. On a 50-row design with a one-member factor level, 2.0 returns 0.2303 for the singleton's coefficient against a classical 0.8501, which is 0.271 of it, and that number is assembled entirely from rows that say nothing about the coefficient it describes. Its standard error is now NA, and the other four on that design are unchanged to machine precision.

Which coefficients those are is computed rather than assumed. Observation i contributes a_ij^2 * sigma_i^2 to Var(beta_j), where a_i = (X'X)^-1 x_i, so discarding it is right exactly when a_ij is 0 and wrong otherwise. The criterion coincides with the partial leverage of Kranz (2024), h_ki = xtilde_ki^2 / sum_j xtilde_kj^2 with xtilde_k the residual from regressing x_k on the other regressors, which is positive on the same set of coefficients. The set depends on the parametrisation, as it should, since it is a claim about coefficients as written: under treatment coding one singleton level makes one coefficient non-estimable, and under sum coding the same data makes five, the intercept among them.

The result is narrower than sandwich::vcovHC(), not wider. A single NaN in the meat propagates through bread %*% meat %*% bread, so sandwich returns NaN for every coefficient on the design above, where estimatr returns four finite standard errors and one NA. Only HC2 and HC3 are affected. HC0, HC1, stata, CR0 and CR2 never form 1 - h, return a finite number here, and are left alone: what they report at full leverage is a property of those estimators rather than a computational artefact.

A dropped collinear regressor is a message, not a warning

2.0 added a warning naming the coefficients a rank-deficient fit returns as NA (#411), because a user comparing lm() and lm_robust() output had no way to see that a term had been dropped. stats::lm() signals nothing at all in that situation, at the fit, at summary(), at predict(), or through broom::tidy(): it prints Coefficients: (1 not defined because of singularities) in the summary and raises no condition. The notice is now a message, which keeps the information and matches lm() on the one axis a caller can intercept.

A caller who wrapped a rank-deficient fit in suppressWarnings() to silence the 2.0 notice will now see it printed; suppressMessages() silences it instead.

The warning also reached code no one was looking at. A warning raised inside a grouped dplyr verb that is itself nested inside another groupe

The NEWS section is longer than shown here.

From NEWS.md in the release

Install

Health

CRAN checks
13OK
Slowest check: 6.9 min · r-release-macos-x86_64
Status changes
  • OK13 OK2026-08-05
  • NOTE12 OK, 1 NOTE2026-08-01
  • OK13 OK2026-07-05
  • WARNING12 OK, 1 WARNING2026-07-03
  • OK12 OK2026-04-25
Show 4 earlier changesHide 4 earlier changes
  • NOTE12 OK, 2 NOTE2026-04-22
  • ERROR11 OK, 2 NOTE, 1 ERROR2026-04-18
  • NOTE12 OK, 2 NOTE2026-03-12
  • ERROR11 OK, 2 NOTE, 1 ERROR2026-03-10
Advisories

none known

Documentation

In release 2.0.1
READMEYes · 1,037 wordsVignettesYes · 3 · evaluation depends on the environmentNEWSYes · 67% structured · mentions 2.0.1Citation fileNoWebsiteYes
Help pages
14 (1 internal)includes a package overview page
Examples that run
100%12 run in a plain check
Exported functions with examples
90%
Documented parameters
89%
Return-value docs
80%
Help pages citing references
14%

Downloads

190.7K
CRAN downloads in the past year
Rank #944 · ~522/day · ~15.9K/mo
Daily download trend is not available in this view yet.
17.7K30 days
44K90 days
190.7K1 year
Compare downloads with other packages →
Also on7K r2u2 autocran6.7K conda_forge94 c2d4u

Repository

135Stars
22Forks
45Open issues
3Open PRs
17Releases
1,455Commits
17Commit identities
1,455 commits · Last activity 2026-10-10 · 0% stars, 30d
Responsiveness
How these are sampled
PRs merged (all time)
90%
Median days to close, 50 most recently updated closed issues
610
Median days to close, 50 most recently updated closed PRs
6

Stars over time

2025-05-01 · 1332026-07-07 · 135
PracticesBeta

9 development-tooling and community-health practices detected across 4 families in the upstream repository

Checks run against github.com/declaredesign/estimatr on 2026-10-25.

Continuous integration (2)
GitHub ActionsR CMD check workflow

The R CMD check workflow runs on Linux, macOS and Windows, and on R-devel

Docs source (3)
README.Rmdpkgdown

GitHub Pages site last deployed 2018-05-25

Reproducibility and dev environment (1)
data-raw/
CRAN release process (3)
cran-comments.mdrevdep/CRAN-SUBMISSION
How this is detected·Detection ruleset v4 (2026-10-20)
AI coding toolsBeta

This repository shows signs of AI coding tools.

  • Claude Codefirst seen 2026-05-10
    Named in .Rbuildignore: .claude
    Named in .gitignore: .claude
    Pull request it wrote: opened by a project member
    Commits: at least 175 crediting it in the message
Models named in its commits
  • Claude Opus 596 commits · Jul to Sep 2026
  • Claude Sonnet 4.615 commits · May 2026
  • Claude Fable 5.19 commits · Sep 2026

Some of these counts are a minimum, because the search stopped before the oldest commits.

Counts are commits, not lines of code. Commits by a tool's accounts count once across all of its accounts. Commits crediting it are the largest count among its credit lines, a minimum.How this is detected·Detection ruleset v4 (updated 2026-09-27)

Dependencies

Declared dependencies
22 external dependencies (excludes base and recommended)

Installing estimatr adds 14 packages (9 with compiled code, 7.6 MB of source tarballs). Heaviest: Rcpp 1.8 MB, RcppEigen 1.7 MB, vctrs 1.0 MB.

Depends (1)
R >= 3.6.0
Imports (7)
LinkingTo (2)
Enhances (0)
none

People & History

People (9)
Author, Maintainer since 2.0.0 (2026)
Contributor
Contributor · added in 0.20.0
Contributor · added in 0.16
Contributor · added in 1.0.6

Previously maintained by Graeme Blair (0.2.0 to 1.0.6).

Listed in earlier versions (1)
no longer listed · 0.2.0 to 0.4.0
Releases (24)

Pick two releases to compare their code metrics.

  • 2.0.1Latest
    record
    2026-10-10 · current release · diff ↗ ·
    8 hours in CRAN's queue
    Listed in CRAN's incoming queue from 2026-10-09 19:57 to 2026-10-10 03:36 UTC, in 3 readings.
  • 2.0.0
    record
    2026-09-16 · diff ↗ ·
    removed 3 exports
    declaration_to_condition_pr_mat, gen_pr_matrix_cluster, permutations_to_condition_pr_mat
    added 1 export
    augment
  • R
    R 4.6.0 released · 2026-04-24
  • R
    R 4.5.0 released · 2025-04-11
  • 1.0.6
    record
    2025-02-28 · diff ↗
  • R
    R 4.4.0 released · 2024-04-24
  • 1.0.4
    record
    2024-04-01 · diff ↗
  • 1.0.2
    record
    2024-01-17 · diff ↗
  • R
    R 4.3.0 released · 2023-04-21
  • 1.0.0
    record
    2022-07-04 · diff ↗
  • R
    R 4.2.0 released · 2022-04-22
  • 0.30.6
    record
    2022-01-31 · diff ↗
  • 0.30.4
    record
    2021-11-07 · diff ↗
  • R
    R 4.1.0 released · 2021-05-18
  • 0.30.2
    record
    2021-01-17 · diff ↗
  • 0.30.0
    record
    2021-01-07 · diff ↗
Show 18 earlier eventsHide 18 earlier events
  • 0.28.0
    record
    2020-11-19 · diff ↗
  • 0.26.0
    record
    2020-09-07 · diff ↗
  • 0.24.0
    record
    2020-09-02 · diff ↗
  • R
    R 4.0.0 released · 2020-04-24
  • 0.22.0
    record
    2020-03-19 · diff ↗
  • 0.20.0
    record
    2019-09-09 · diff ↗
  • 0.18.0
    record
    2019-05-26 · diff ↗
  • R
    R 3.6.0 released · 2019-04-26
  • 0.16
    record
    2019-03-02 · diff ↗ ·
    added 2 exports
    glance, lh_robust
  • 0.14
    record
    2018-11-06 · diff ↗ ·
    removed 4 exports
    tidy.difference_in_means, tidy.horvitz_thompson, tidy.iv_robust, tidy.lm_robust
  • 0.12
    record
    2018-09-16 · diff ↗
  • 0.10.0
    record
    2018-07-12 · diff ↗
  • 0.8.0
    record
    2018-06-02 · diff ↗ ·
    added 4 exports
    commarobust, extract.iv_robust, extract.lm_robust, starprep
  • R
    R 3.5.0 released · 2018-04-23
  • 0.6.0
    record
    2018-03-28 · diff ↗ ·
    removed 1 export
    na.omit_detailed.data.frame
    added 2 exports
    iv_robust, tidy.iv_robust
  • 0.4.0
    record
    2018-02-15 · diff ↗
  • 0.2.0
    record
    2018-01-29
  • R
    R 3.4.0 released · 2017-04-21
Across CRAN, the median accepted release of the past year spent 22 hours in the queue, 90% under 11 days.How queue time is measured

Package metadata

First published
2018-01-29
Total releases
24 / 8 yrs
License
MIT + file LICENSE OSI
Minimum R
≥ 3.6.0
Download size
483 KB
Appears in task views

Cite

Cite this package

Run in R for the authors' preferred citation:

citation("estimatr")
Coppock, A., Blair, G., Cooper, J., Fultz, N., Humphreys, M., Lenth, R., Medina, L., Offer-Westort, M., & Sonnet, L. (2026). estimatr: Fast Estimators for Design-Based Inference (Version 2.0.1) [Computer software]. https://doi.org/10.32614/CRAN.package.estimatr

This package has no citation file, so citation() returns this entry built from DESCRIPTION.

Cite the R Observatory

For a number measured here: a download total, a coverage figure, an archival date.

APA

Balamuta, J. J. (2026). R Observatory: Metrics for estimatr version 2.0.1 [Data set]. HJJB, LLC. Data release v2026-10-26. https://doi.org/10.5281/zenodo.21843040

From data release v2026-10-26, which the citation names so these numbers can be found later. More on citing and the projects behind them.

Report a problem with this page →

Privacy choices

These apply to this browser and are stored on this device only. Nothing about your choice is sent to us.

Read the privacy policy