Celeus

Logistic regression assumptions, real and invented

Which assumptions logistic regression really makes, which ones it never made, and how to check each one in R, worked through on the birthwt data.

Updated . Written by Matt Gencer.

On this page

Logistic regression makes fewer assumptions than most people check, and different ones. It does not care whether your predictors are normally distributed, it has no equal-variance assumption, and it never claimed a straight line between a predictor and the probability of the outcome. What it does need is independent observations, a log-odds that is linear in each continuous predictor, data that do not separate perfectly, a model that contains what matters, and enough events to estimate what you asked for.

This post sorts the real assumptions from the invented ones, says how to check each real one in R, and works through all of them on one model: low birth weight on maternal smoking, age, weight and race in the birthwt data from the MASS package, run through a deterministic engine. If you want to know how to read odds ratios, or what to do once a fit has separated, the logistic regression guide covers that; this page is about which checks belong in your methods section and which do not.

It sits under our overview of parametric test assumptions, which covers the normality and equal-variance checks that t-tests and ANOVA really do need.

The short answer

Claimed assumption Real? What to do
Binary outcome, event level known Yes Check which level the software models
Independent observations Yes Decided by the design, not by a test
Linear in the logit, for each continuous predictor Yes Spline refit and likelihood-ratio test
No complete or quasi-complete separation Yes Watch for huge standard errors; use a penalised fit
Model contains the predictors and interactions that matter Yes Subject knowledge first, fit checks second
Enough events per estimated parameter Yes, as a guide Count the smaller class and the parameters
Normally distributed predictors or residuals No Nothing
Equal variances (homoscedasticity) No Nothing
Straight line between predictor and probability No Check the logit instead
Balanced outcome classes No Count events instead
Uncorrelated predictors No, a precision issue Read variance inflation factors and intervals
No outliers No, an influence question Look at influential rows, do not delete extremes

The assumptions logistic regression actually makes

A binary outcome, coded the way you think

The model describes the probability of one of two outcomes, and two things go wrong in practice. An outcome with three or more categories squeezed into two throws information away, and ordered or unordered categories have their own models, compared in ordinal vs multinomial logistic regression. The ordinal model adds an assumption of its own, proportional odds: each predictor shifts the odds of being above a cut-point by the same factor at every cut-point between categories. And the software has to know which level is the event: if it models the odds of the other level, every odds ratio comes out inverted. Check which level is the reference before you read a single coefficient.

Independent observations

Repeated measurements on the same person, patients within the same clinic or pupils within the same class are correlated, and an ordinary logistic fit treats them as if they were not, which makes standard errors too small and p-values too optimistic. No test on the fitted model reliably detects this; answer it from the design. If the data are clustered, you need a mixed model or cluster-robust errors; the same reasoning for continuous outcomes is in repeated measures ANOVA vs mixed models.

Linearity in the logit

The model is linear on the log-odds scale: each unit of a continuous predictor adds the same amount to log(p / (1 - p)), wherever you are on that predictor’s range. It is not a straight line between the predictor and the probability, which follows an S-shaped curve by construction.

The assumption applies to continuous predictors and to anything you entered as a number. That includes category codes. In birthwt, mother’s race is stored as the integers 1, 2 and 3. Enter it as it stands and the model fits one slope across those codes, which assumes that the second category sits exactly halfway between the first and the third on the log-odds scale. Enter it with factor(race) instead, which the MASS documentation’s own example does and the worked example below does too.

The practical check is to let the model bend and see whether it wants to. Refit with a natural spline for each continuous predictor and compare the two fits with a likelihood-ratio test, then plot the spline terms. A Box-Tidwell test is the older alternative and works only for positive predictors.

No complete or quasi-complete separation

If a predictor, or a combination of predictors, perfectly divides the events from the non-events (complete separation), or does so except for ties on the dividing boundary (quasi-complete separation), at least one coefficient has no finite maximum-likelihood estimate: the fit drives it towards infinity. Albert and Anderson (1984) sorted every possible data pattern into three mutually exclusive groups, complete separation, quasi-complete separation and overlap, and proved existence theorems for them; finite estimates exist when the data overlap. In practice the warning sign is a coefficient with an absurd standard error and fitted probabilities pinned at 0 or 1. The fix is a penalised fit rather than dropping the predictor; the logistic regression guide walks through a separated example and the Firth penalised logistic regression reference describes the remedy.

The right predictors and interactions

For an adjusted association, a well-specified model means the confounders you know about are in the model, and that an effect which genuinely differs between groups is not forced to be the same in each. No goodness-of-fit test can tell you which variables you forgot. Subject knowledge does that work, before the data are fitted.

Enough events for the parameters you estimate

A logistic model learns mostly from the rarer outcome class. The usual rule of thumb is at least 10 events per estimated parameter, from a simulation study in which Peduzzi and colleagues (1996) saw no major problems at that level and biased coefficients and poorly covered intervals below it. Count the smaller of the two classes, whichever it is, and count parameters rather than variable names: a four-level factor costs three. Treat the rule as a floor, not a guarantee. Van Smeden and colleagues (2016) concluded that the evidence behind the 10-events rule is weak and that problems at low values also depend on total sample size, and that a few separated data sets can dominate such simulations.

The assumptions people invent

Normally distributed predictors

The model makes no assumption about their distribution, so a skewed predictor is not a violation. A strongly skewed continuous predictor can still make linearity in the logit less plausible, which is a reason to check that assumption more carefully, not to run a normality test.

Normal residuals and equal variances

These belong to linear regression. A binary outcome has a variance fixed by its probability, p(1 - p), so there is no separate variance to be equal, and residuals from a logistic model are not expected to look normal at all. A Shapiro-Wilk test on logistic residuals, or a Levene test across groups, checks something the model never assumed.

A straight line to the probability

Plotting the outcome against a predictor and looking for a straight line tests the wrong scale. Probabilities flatten towards 0 and 1 by design. The straight line, if there is one, is on the log-odds scale, which is why the spline check above works there.

Balanced outcome classes

There is no requirement that events and non-events be equally common. What it limits is the information available, which is why the events-per-parameter count uses the smaller class. Rebalancing the data by discarding rows of the common class throws away information to solve a problem the model does not have.

Uncorrelated predictors

Predictors are allowed to be correlated. Strong correlation between predictors makes each one’s coefficient less precise, so the intervals widen and the coefficients become hard to tell apart. That is a precision problem, not bias. Only exact collinearity, where one predictor is a perfect combination of others, stops the model from estimating a coefficient at all. Variance inflation factors show how much each coefficient’s variance is inflated.

No outliers

An unusual predictor value is not a violation of anything. What matters is influence: whether a few rows move the estimates. Look at those rows for data-entry errors, refit without them to see how much the answer moves, and report both if it does. Deleting genuine extremes to tidy a model is the mistake.

Worked example: checking the assumptions on birthwt

The birthwt data hold 189 births recorded at one US hospital in 1986, one row per birth, with a binary indicator of birth weight under 2.5 kg. The example asks whether smoking during pregnancy is associated with the odds of low birth weight, adjusting for the mother’s age, her weight at her last menstrual period and her race. Before the run, race was recoded from the codes 1, 2 and 3 to the MASS documentation’s labels (white, black, other), so the engine treated it as a category.

The engine fitted glm(low ~ smoke + age + lwt + race, family = binomial()) on all n = 189 rows, none dropped for missing values. It modelled the odds of low equal to 1, a low birth weight. Race entered as two indicator terms, with black as the reference level (the first label alphabetically).

What the engine checked

Independence. Not testable from the output. Each row is one birth and the data carry no identifier that links rows, so the analysis treats them as independent. That is a design assumption, not something the model checked.

Events per variable. Low birth weight is the smaller class, 59 of the 189 births, and the model estimates five coefficients besides the intercept (smoking, age, weight and two race terms), which gives 11.8 events per parameter. That clears the floor of 10, which is a minimum, not an assessment that the model is valid.

Separation. The engine raised no separation flag, and no coefficient shows the runaway standard error a separated fit produces: the one on smoking, for example, is 0.380.

Calibration. The Hosmer-Lemeshow test (Hosmer and Lemeshow 1980), computed over 10 risk groups, gave a statistic of 10.84 and p = 0.211. The engine marks the check as passed; read that as miscalibration not detected, not as evidence that the model is calibrated or correctly specified. The engine added a caution of its own: at least one risk group expects fewer than 5 events or fewer than 5 non-events, which is where the chi-squared approximation behind that p-value becomes unreliable. A calibration plot of observed against predicted risk by decile shows where any mismatch sits, which a single p-value cannot. Hosmer and colleagues (1997) also point out that different groupings can disagree about the same model, and in their simulations the grouped test had no power against a missing interaction between a binary and a continuous predictor, and poor power against a wrong link function at a sample size of 100.

Influence and extreme values. The engine flagged 6 of the 189 observations with a Cook’s distance above the 4/n screening threshold, the largest 0.040, and 13 maternal weights beyond Tukey’s 1.5 times IQR fence. Nothing was removed; the flags say which rows to look at, not that anything is wrong with them.

What the engine did not check, and how to do it

The engine does not test linearity in the logit or report variance inflation factors. Run them yourself:

library(splines)
data(birthwt, package = "MASS")
birthwt$race_code <- birthwt$race
# Labels from the MASS help page; "black" is the reference level, as in the engine run
birthwt$race <- factor(birthwt$race, levels = 1:3, labels = c("white", "black", "other"))
birthwt$race <- relevel(birthwt$race, ref = "black")
fit <- glm(low ~ smoke + age + lwt + race, family = binomial, data = birthwt)

# Linearity in the logit: let age and weight bend, then compare the fits
fit_spline <- glm(low ~ smoke + ns(age, df = 3) + ns(lwt, df = 3) + race,
                  family = binomial, data = birthwt)
anova(fit, fit_spline, test = "Chisq")
termplot(fit_spline, terms = 2:3, se = TRUE)

# Correlated predictors: generalized variance inflation factors (race has two terms)
car::vif(fit)

# The trap: race entered as the codes 1, 2, 3, one slope across categories
fit_codes <- glm(low ~ smoke + age + lwt + race_code, family = binomial, data = birthwt)
summary(fit_codes)$coefficients

# The rows behind the influence flag
head(sort(cooks.distance(fit), decreasing = TRUE))

Read the spline comparison as you would any test of an assumption: a non-significant result says curvature was not detected with this sample, so look at the plotted terms as well. With a factor in the model, car::vif() reports a generalized VIF per predictor rather than one per coefficient. The last model shows the trap from earlier: it fits one slope across the race codes, which forces the black category, coded 2, to sit exactly halfway between white and other on the log-odds scale whatever the data say.

The result, read after the checks

Smoking during pregnancy had an odds ratio of 2.87 for low birth weight, with a 95% profile-likelihood interval of 1.382 to 6.186, z = 2.775 and p = 0.00552. On the log-odds scale the coefficient is 1.054 with an interval of 0.324 to 1.822. The data are compatible with anything from about 1.4 times to about 6 times the odds, so the estimate is imprecise even though the interval excludes 1.

Age had a coefficient of -0.0225 per year, interval -0.0909 to 0.0436, p = 0.511, so the data are compatible with odds that fall, stay the same or rise slightly with age. Maternal weight had a coefficient of -0.0125 per pound, interval -0.0259 to -0.00064, p = 0.0498: the interval only just excludes zero, so this is weak evidence. For race, relative to the black reference level, the white coefficient was -1.232 (interval -2.265 to -0.221, p = 0.0172) and the other coefficient -0.288 (interval -1.327 to 0.754, p = 0.584).

This is an observational association adjusted for age, weight and race. It is not an estimate of what smoking causes. The data also record previous premature labours, hypertension, uterine irritability and early physician visits, none of which is in this model, and confounders that were never recorded cannot be adjusted for at all, so read the smoking estimate as an illustration of the checks rather than a definitive adjusted effect. It does not support any claim about individual pregnancies.

What to report

Name the outcome and event level, the numbers of events and non-events, each predictor with its unit or reference level, and the odds ratios with their intervals and the interval method. Then report the checks that apply: how independence follows from the design, how you checked linearity in the logit and what you did with a predictor that bent, whether separation occurred, the events per parameter, and any calibration or influence check with its limits stated. Leave out normality and equal-variance tests. If you tested many predictors or fitted many candidate models, read the multiple comparisons guide before interpreting the p-values, and say on which scale your effect is reported; odds ratio vs relative risk covers why an odds ratio is not a risk ratio for a common outcome.

Rerunning the example

The R script beside the example reproduces the model fit from the bundled copy of the data, in which race is already recoded to its labels: the coefficients, their standard errors, the z statistics and the Wald p-values. The profile-likelihood intervals, the events-per-variable count, the Hosmer-Lemeshow result and the influence flags are engine output that the script does not recompute; the code above reproduces the influence ranking. The logistic regression method reference lists what the engine reports for this model.

Worked example

Data: Hosmer DW, Lemeshow S (1989). Applied Logistic Regression. New York: Wiley. Data collected at Baystate Medical Center, Springfield, Massachusetts, 1986; distributed as birthwt in the MASS R package (Venables and Ripley 2002). https://cran.r-project.org/package=MASS. Seed: 20261022.

Run on Celeus engine 0.30.0, seed 20261022, public data.

Output

# Results

- Test: Logistic regression
- n: 189
- statistic: 2.775
- p-value: 0.00552
- effect size: odds ratio = 2.87
- 95% CI: [0.3238, 1.822]
- seed: 20261022

## Assumption checks
- sample size: passed=TRUE (adequate n)
- events per variable: passed=TRUE (at or above the rule-of-thumb floor (a convention, not a guarantee of a stable model))
- calibration (Hosmer-Lemeshow, 10 groups): passed=TRUE (no evidence of poor calibration (observed event rates track the predicted risks) - caution: at least one risk group expects fewer than 5 events or fewer than 5 non-events, where the chi-squared approximation behind this p-value is unreliable)

## Power
- Power is not calculated for this test.
R script
# Celeus - reproduce.R  (run from this package folder; bundled data in data/)
# Test: Logistic regression  |  engine 0.30.0  |  catalog c39-d3cd
# Research context: not provided for this run.
# Reproduces the recorded result deterministically.

set.seed(20261022)
.data_path <- "data/logistic-regression-assumptions.csv"
if (!file.exists(.data_path)) stop("Run reproduce.R from the package folder (the bundled data is in data/).")
data <- readr::read_csv(.data_path, show_col_types = FALSE)  # needs the readr package
clean_cells <- function (df) 
{
    ctrl_collapse <- "[\r\n\t\v\f]+"
    zero_width_chars <- intToUtf8(c(8203L, 65279L), multiple = TRUE)
    uni_space_chars <- intToUtf8(c(160L, 8239L, 8199L), multiple = TRUE)
    clean_text <- function(x) {
        out <- x
        for (ch in zero_width_chars) out <- gsub(ch, "", out, 
            fixed = TRUE)
        out <- gsub(ctrl_collapse, " ", out)
        for (ch in uni_space_chars) out <- gsub(ch, " ", out, 
            fixed = TRUE)
        trimws(gsub(" +", " ", out))
    }
    changed_cols <- character(0)
    cells_cleaned <- 0L
    retyped <- character(0)
    for (j in seq_along(df)) {
        col <- df[[j]]
        if (!is.character(col)) 
            next
        cleaned <- clean_text(col)
        n_changed <- sum(!is.na(col) & !is.na(cleaned) & col != 
            cleaned, na.rm = TRUE)
        if (n_changed > 0L) {
            changed_cols <- c(changed_cols, names(df)[j])
            cells_cleaned <- cells_cleaned + n_changed
        }
        df[[j]] <- cleaned
        non_blank <- cleaned[!is.na(cleaned) & nzchar(cleaned)]
        if (length(non_blank) && all(grepl("^[+-]?(0|[1-9][0-9]*)([.][0-9]+)?$", 
            non_blank))) {
            parsed <- suppressWarnings(as.numeric(cleaned))
            was_blank <- is.na(cleaned) | !nzchar(cleaned)
            lost <- (is.na(parsed) | is.infinite(parsed)) & !was_blank
            if (!any(lost)) {
                finite <- parsed[!is.na(parsed)]
                all_whole <- all(finite == floor(finite))
                in_range <- all(abs(finite) <= .Machine$integer.max)
                df[[j]] <- if (all_whole && in_range) 
                  as.integer(parsed)
                else parsed
                retyped <- c(retyped, names(df)[j])
            }
        }
    }
    names(df) <- clean_text(names(df))
    if (length(changed_cols)) {
        attr(df, "ingest_clean_columns") <- changed_cols
        attr(df, "ingest_clean_cells_cleaned") <- cells_cleaned
    }
    if (length(retyped)) 
        attr(df, "ingest_clean_retyped") <- retyped
    df
}
data <- clean_cells(data)
# Complete-case analysis: drop rows missing any analysis value.
.acols <- c("low", "smoke", "age", "lwt", "race")
data <- data[stats::complete.cases(data[, .acols, drop = FALSE]), , drop = FALSE]
result <- summary(glm(low ~ smoke + age + lwt + race, data = data, family = binomial()))
print(result)

Common questions

What are the assumptions of logistic regression?

A binary outcome coded the way you think it is, independent observations, a log-odds that is linear in each continuous predictor (or modelled with splines when it is not), no complete or quasi-complete separation, a model that includes the predictors and interactions that matter, and enough events in the smaller outcome class for the number of parameters you estimate.

Does logistic regression assume normality?

No. Neither the predictors nor the outcome nor the residuals are assumed to be normal, and there is no equal-variance assumption: the variance of a binary outcome is fixed by its probability. A Shapiro-Wilk test on a predictor or on logistic residuals answers a question the model never asked.

How do I check linearity in the logit in R?

Refit the model with a natural spline for each continuous predictor, for example splines::ns(age, df = 3), and compare the two fits with a likelihood-ratio test using anova(..., test = "Chisq"). Plot the spline terms as well, because a non-significant test means curvature was not detected, not that the log-odds is straight.

Is multicollinearity an assumption of logistic regression?

Not in the sense that it biases the estimates. Correlated predictors make the individual coefficients less precise, so their intervals widen and they become hard to separate from each other. Only exact collinearity breaks the fit, because the coefficients are then not identifiable. Check variance inflation factors and read the intervals.

Is a non-significant Hosmer-Lemeshow test proof that the model fits?

No. It means the test did not detect miscalibration across its risk groups, which is not evidence that the model fits. The result can change with the grouping, its power against a wrong link function is poor in samples of around 100, and in simulations it could not detect some specification errors at all, such as a missing interaction between a binary and a continuous predictor.

What are the assumptions of ordinal logistic regression?

The same ones as the binary model for independence, linearity of continuous predictors on the log-odds scale and enough data, plus proportional odds: each predictor is assumed to shift the odds of being above a cut-point by the same factor at every cut-point between the ordered categories. If that fails, a less constrained model is needed, such as a partial proportional odds model or a multinomial model that ignores the ordering.

How many events do I need for logistic regression?

A common floor is 10 events in the smaller outcome class per estimated parameter, where a factor costs one parameter per level beyond its reference. Treat it as a minimum rather than a target, because the evidence behind it is contested and problems at low values also depend on total sample size.

Sources

  1. Albert A, Anderson JA (1984). On the existence of maximum likelihood estimates in logistic regression models. Biometrika 71(1):1-10. doi:10.1093/biomet/71.1.1
  2. Peduzzi P, Concato J, Kemper E, Holford TR, Feinstein AR (1996). A simulation study of the number of events per variable in logistic regression analysis. Journal of Clinical Epidemiology 49(12):1373-1379. doi:10.1016/S0895-4356(96)00236-3
  3. van Smeden M, de Groot JAH, Moons KGM, Collins GS, Altman DG, Eijkemans MJC, Reitsma JB (2016). No rationale for 1 variable per 10 events criterion for binary logistic regression analysis. BMC Medical Research Methodology 16:163. doi:10.1186/s12874-016-0267-3
  4. Hosmer DW, Lemeshow S (1980). Goodness of fit tests for the multiple logistic regression model. Communications in Statistics - Theory and Methods 9(10):1043-1069. doi:10.1080/03610928008827941
  5. Hosmer DW, Hosmer T, le Cessie S, Lemeshow S (1997). A comparison of goodness-of-fit tests for the logistic regression model. Statistics in Medicine 16(9):965-980. doi:10.1002/(SICI)1097-0258(19970515)16:9<965::AID-SIM509>3.0.CO;2-O

Methods discussed

Try it on your own data

Upload a dataset and Celeus suggests a method, checks its assumptions, runs it in R, and gives you the script to rerun it.

Try it in Celeus