[Stata] Coefficient Plots: coefplot

A regression table with three models and a dozen covariates is hard to read, and a coefficient plot often shows the same results more directly: one point and confidence interval per coefficient, with a reference line at zero, or at one for odds ratios. Ben Jann’s coefplot draws these plots from stored estimates and handles the things that usually take the most work by hand: exponentiating coefficients, choosing and relabeling terms, combining several models in one plot, and splitting models into panels.

The examples plot survey-weighted logistic regressions of three access outcomes on limited English proficiency, nativity, insurance, and income.

Installation

Stata
ssc install coefplot

The current version is 1.8.8 (August 2025), and it runs on Stata 11 or newer.

Data

The examples use the 2024 California Health Interview Survey (CHIS) adult public use file, which has 24,810 adult respondents. The CHIS public use files are free and available through a brief application on the UCLA Center for Health Policy Research site. Every post in this series builds its variables with the same short do-file, 00_chis_prep.do:

Stata
use "chis_adult_2024.dta", clear

* outcomes
gen k6      = distress if distress >= 0          // K6 score, past month (0-24)
gen nousual = usual == 2                         // no usual source of care
gen delay   = ah22 == 1                          // delayed or did not get medical care, past 12 months
gen erv     = er == 1                            // any ER visit, past 12 months
gen uninsured = ins == 2
gen needhelp  = af81 == 1 if af81 > 0            // needed help for emotional/mental or substance problem
gen fairpoor  = ab1 >= 4                         // fair or poor self-rated health
gen spd       = dstrs12 == 1                     // likely serious psychological distress, past year

* groups
gen lep     = spk_eng == 3                       // speaks English not well or not at all
gen foreign = ah33new == 2
gen female  = srsex == 2

* covariates
gen age = srage_p1
recode ahedc_p1 (1 2 = 1 "Less than high school") (3 = 2 "High school") ///
    (4 5 6 = 3 "Some college") (7 8 9 = 4 "Bachelor's or more"), gen(educ)
gen pov = povll
label define povllf 4 "300%+ FPL", modify
label values pov povllf
recode ombsrr_p1 (1 = 1 "Latino") (2 = 2 "White") (3 = 3 "Black") (5 = 4 "Asian") ///
    (4 6 = 5 "Other or multiracial"), gen(race)

* CHIS design: final weight plus 80 jackknife replicate weights
svyset [pw=rakedw0], jkrweight(rakedw1-rakedw80, multiplier(1)) vce(jackknife) mse

Limited English proficiency (lep) means the respondent reported speaking English not well or not at all; 1,520 respondents (6.1 percent) meet that definition. Commands that accept the svy prefix use the jackknife replicate weights; commands that do not accept it use the final weight rakedw0 as a probability weight, which gives the right point estimates and robust standard errors that ignore the replicate design.

Models

Each model is a survey logit with the same covariates, saved with estimates store so coefplot can call it by name.

Stata
foreach y in nousual delay erv {
    svy: logit `y' lep foreign age female i.educ i.pov uninsured
    estimates store m_`y'
}

One model

The simplest plot takes one stored model. eform reports odds ratios instead of log odds, drop(_cons) removes the constant, and xline(1) draws the null reference line. Odds ratios are multiplicative, so a log scale (xscale(log)) gives an odds ratio of 0.5 and an odds ratio of 2 the same distance from 1.

Stata
coefplot m_nousual, eform drop(_cons) xline(1) ///
    xscale(log range(.25 8)) xlabel(.25 .5 1 2 4 8) ///
    coeflabels(4.pov = "300%+ FPL") ///
    xtitle("Odds ratio (95% CI), log scale") title("No usual source of care") scale(1.45)
Coefficient plot of odds ratios for no usual source of care: uninsured about 4.4, limited English proficiency about 1.4, higher income categories below 1
Odds ratios from the survey logit for no usual source of care.

coefplot uses variable and value labels for the axis, which is why most terms need no relabeling; coeflabels() overrides any label. scale() enlarges all text and markers together, which keeps the labels readable when the image is shrunk on a web page or a slide.

The plot shows the pattern in the model: being uninsured goes with about four times the odds of having no usual source of care (odds ratio 4.4), limited English proficiency with 1.4 times the odds, and each higher income category with lower odds than the lowest.

Several models in one plot

To compare outcomes, list each model in its own parentheses with a legend label. Giving each model its own marker (msymbol()) and interval line pattern (ciopts(lpattern())) keeps the series distinguishable in grayscale and for readers who do not see the colors. keep() restricts the plot to the terms that matter for the comparison, and *.pov matches every income category.

Stata
coefplot (m_nousual, label(No usual source) msymbol(O) ciopts(lpattern(solid))) ///
         (m_delay, label(Delayed care) msymbol(S) ciopts(lpattern(dash))) ///
         (m_erv, label(ER visit) msymbol(T) ciopts(lpattern(shortdash))), ///
    eform keep(lep foreign uninsured *.pov) xline(1) ///
    xscale(log range(.25 8)) xlabel(.25 .5 1 2 4 8) ///
    xtitle("Odds ratio (95% CI), log scale") ///
    coeflabels(lep = "Limited English" foreign = "Born outside US" uninsured = "Uninsured" ///
        2.pov = "Income 100-199% FPL" 3.pov = "Income 200-299% FPL" 4.pov = "Income 300%+ FPL") ///
    legend(position(6) rows(1)) scale(1.45)
Coefficient plot with three colored series for no usual source of care, delayed care, and ER visit, across limited English, born outside US, three income categories, and uninsured
Odds ratios for three outcomes from separate survey logits.

Limited English proficiency is associated with higher odds of having no usual source of care, but its intervals for delayed care and emergency room visits include 1. Being born outside the US goes with lower odds of delayed care (odds ratio 0.65), and being uninsured goes with higher odds of no usual source of care but lower odds of an emergency room visit.

Panels

With many terms, overlapping series become crowded. Separating models with || instead of listing them puts each in its own panel, with the same axis.

Stata
coefplot m_nousual || m_delay || m_erv, eform keep(lep foreign uninsured) xline(1) ///
    xscale(log range(.4 6)) xlabel(.5 1 2 4) ///
    bylabels("No usual source" "Delayed care" "ER visit") byopts(rows(1)) ///
    coeflabels(lep = "Limited English" foreign = "Born outside US" uninsured = "Uninsured") ///
    xtitle("Odds ratio (95% CI), log scale") scale(1.45)
Three panels, one per outcome, each showing odds ratios for limited English, born outside US, and uninsured
The same models as separate panels.

Other options worth knowing

levels(99 95) draws two confidence intervals at once, ciopts(recast(rcap)) changes the interval to capped spikes, order() changes the order of terms, headings() and groups() add labeled blocks of terms, and vertical turns the plot on its side. coefplot also plots results that are not stored estimates; the dominance analysis post in this series uses coefplot matrix() to draw a bar chart from a matrix.

Reference

Jann, B. (2014). Plotting regression coefficients and other estimates. The Stata Journal, 14(4), 708–737. https://doi.org/10.1177/1536867X1401400402

  • September 9, 2026