What quasi-complete separation looks like
Quasi-complete separation is a data configuration problem, not a coding error. It occurs when one predictor, or a combination of predictors, almost perfectly predicts the outcome. In ordinal regression, it typically manifests when all subjects with a certain treatment or characteristic fall into the extreme categories of the scale, leaving no overlap with the other group in the middle categories.
Example: in a small cosmetic study with 20 panelists, suppose all panelists in the active treatment group achieve a score of 4 (maximum improvement) at day 56, while all placebo panelists score 0-2. The model tries to estimate an odds ratio for "treatment at day 56 vs baseline" and finds the data perfectly consistent with an infinite OR. The maximum likelihood estimate does not exist in the usual sense.
Why it happens in small cosmetic/clinical studies
Small sample sizes with many time points are the primary risk factor. With 20-30 subjects and 4 time points, some time-treatment combinations will have very few observations. Late time points in successful trials are especially prone: if the treatment works well, most treated subjects will be in the top category by week 8, and the lower categories will be empty for that group.
Ordinal scales with few categories are also at risk: a 4-point scale with 30 subjects means each cell in your treatment × time × outcome contingency table has very few observations.
Detection: warning signs in R output
The ordinal package will often converge but give you inflated estimates:
library(ordinal)
model <- clmm(
score_ord ~ treatment * visit + (1 | subject_id),
data = study_data
)
summary(model)
# Warning signs:
# 1. Coefficient estimates > 10 in absolute value for a treatment term
# 2. Standard errors > 5 for those same terms
# 3. Very wide confidence intervals: exp(coef - 1.96*SE) to exp(coef + 1.96*SE)
# 4. Warning message: "Model may not have converged"
# Check for separation directly
table(study_data$treatment, study_data$visit, study_data$score_ord)
# Look for cells with all zeros - this is separation
The three-way table is the definitive diagnostic. If you see entire rows or columns of zeros, you have separation in that subset.
Penalized likelihood: the Firth approach
The standard solution for logistic regression is the Firth penalized likelihood. For ordinal models, it is less directly available, but can be approximated. The key idea: add a penalty to the likelihood function that shrinks extreme estimates toward zero.
# For binary outcomes (if you collapse ordinal to binary for sensitivity):
library(logistf)
model_firth <- logistf(
response_binary ~ treatment + visit,
data = study_data
)
# For ordinal: Bayesian regularization is more practical (see next section)
Bayesian regularization as an alternative
The most practical solution for quasi-complete separation in ordinal models is Bayesian estimation with weakly informative priors. The prior acts as a regularizer that prevents estimates from diverging to infinity.
library(brms)
model_bayes <- brm(
score_ord ~ treatment * visit + (1 | subject_id),
data = study_data,
family = cumulative("logit"),
prior = c(
prior(normal(0, 2.5), class = "b"), # shrink coefficients toward 0
prior(normal(0, 5), class = "Intercept")
),
chains = 4, iter = 2000, seed = 42
)
summary(model_bayes)
# Posterior median and credible intervals for treatment effects
With priors centered at 0 with moderate variance, the model will not estimate infinite ORs. The posterior will be dominated by the prior in the separated cells and by the data in the non-separated cells.
Collapsing categories: when and how
A simpler solution when separation occurs in extreme categories is to collapse adjacent categories. If all treated subjects score 3 or 4, and you are trying to distinguish 3 from 4, consider whether that distinction is clinically or commercially meaningful.
# Collapse 4-point scale to 3-point: merge categories 0 and 1
study_data$score_collapsed <- dplyr::case_when(
study_data$score_ord %in% c("0", "1") ~ "0-1",
study_data$score_ord == "2" ~ "2",
study_data$score_ord %in% c("3", "4") ~ "3-4"
)
# Refit model on collapsed scale
model_collapsed <- clmm(
factor(score_collapsed) ~ treatment * visit + (1 | subject_id),
data = study_data
)
Category collapsing should be pre-specified in the analysis plan if you anticipate it. Post-hoc collapsing is a sensitivity analysis, not the primary analysis.
Reporting when separation occurs
Transparency is essential. In the results section, note that quasi-complete separation was observed, describe where it occurred (which time-treatment combinations), and report both the primary analysis result (with the caveat about instability) and the sensitivity analysis using penalized or Bayesian estimation.
Do not simply report the inflated OR with a note that results should be interpreted with caution and leave it at that. A specific sensitivity analysis with a concrete alternative estimate gives the reader something to act on.
Key takeaway
Quasi-complete separation is common in small ordinal studies and is not a coding error. Diagnose it with contingency tables, handle it with Bayesian priors or category collapsing, and report it transparently with a sensitivity analysis. An inflated OR with a huge SE is not a result: it is a diagnostic.