[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
ssc install coefplotThe 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:
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) mseLimited 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.
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.
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)
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.
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)
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.
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)
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
