3.10 Bonus: Tables & Plots of Results

The examples use the GSS marijuana legalization data from 3.5, with three models that add IVs step by step:

use "https://tdmize.github.io/data/data/cda_gss22", clear
(GSS 1972-2022 CDA - Categorical Data Analysis | 2024-12-03)
drop if missing(grass, age, conserv, faminc, college, woman, race4)
(37,924 observations deleted)
recode grass 2 = 0
(0 changes made to grass)
lab def glab 0"not be legal" 1 "should be legal"
lab val grass glab
* Create year since 1975 variable
gen year1975 = year - 1975
lab var year1975 "years since 1975"
// stepwise binary logit model, storing each time //
qui logit grass c.age c.year1975##c.year1975, or
est store mjmod1
* mecompare without a varlist estimates AMEs for all IVs.
** Add store() option to save MEs to make a table or plot
qui mecompare, amount(sd) store(mes1) model(mjmod1)
qui logit grass c.age c.year1975##c.year1975 i.conserv, or
est store mjmod2
qui mecompare, amount(sd) store(mes2) model(mjmod2)
qui logit grass c.age c.year1975##c.year1975 i.conserv ///
    c.faminc i.degree i.woman i.race4, or
est store mjmod3
qui mecompare, amount(sd) store(mes3) model(mjmod3)
library(haven)           # import Stata data and labels
library(dplyr)           # data manipulation
library(ggplot2)         # graphs
library(modelsummary)    # tables and plots of results
library(marginaleffects) # predictions and marginal effects
library(cleanplots)      # graph scheme
cleanplots_defaults()

# Center changes in continuous IVs on each observation's value
options(marginaleffects_contrast_direction = "center")

gss <- read_dta("https://tdmize.github.io/data/data/cda_gss22.dta")

# Drop any missing data on model vars
gss <- gss[complete.cases(gss[c("grass", "age", "conserv", "faminc", "college",
  "woman", "race4")]), ]

# Create year since 1975 variable; treat binary/nominal variables as factors
gss <- gss |>
  mutate(year1975 = as.numeric(year) - 1975,
    conserv = as_factor(conserv), degree = as_factor(degree),
    woman = as_factor(woman), race4 = as_factor(race4)) |>
  droplevels()

# stepwise binary logit model, storing each time
mjmod1 <- glm(grass ~ age + poly(year1975, 2, raw = TRUE),
  data = gss, family = binomial(link = "logit"))
mjmod2 <- glm(grass ~ age + poly(year1975, 2, raw = TRUE) + conserv,
  data = gss, family = binomial(link = "logit"))
mjmod3 <- glm(grass ~ age + poly(year1975, 2, raw = TRUE) + conserv + faminc + degree +
  woman + race4, data = gss, family = binomial(link = "logit"))

Tables of odds ratios

  • Tables of odds ratios are traditional and should generally be included somewhere
    • I usually include these in the Online Appendix
    • With esttab, use option eform (exponentiate) to get odds ratios
* Use esttab to make single table of ORs
esttab mjmod1 mjmod2 mjmod3, ///
    eform nobase b(3) se(3) nogaps label varwidth(35)
--------------------------------------------------------------------------------
> ---
                                             (1)             (2)             (3)
>    
                                    should mar~l    should mar~l    should mar~l
>    
--------------------------------------------------------------------------------
> ---
should marijuana be made legal                                                  
>    
age of respondent                          0.976***        0.978***        0.976
> ***
                                         (0.001)         (0.001)         (0.001)
>    
years since 1975                           0.956***        0.958***        0.957
> ***
                                         (0.003)         (0.003)         (0.003)
>    
years since 1975 # years since 1975        1.002***        1.002***        1.002
> ***
                                         (0.000)         (0.000)         (0.000)
>    
Conservative                                               0.467***        0.433
> ***
                                                         (0.013)         (0.012)
>    
Family Income in 1000s                                                     0.999
>    
                                                                         (0.000)
>    
high school                                                                1.274
> ***
                                                                         (0.049)
>    
associate/junior college                                                   1.435
> ***
                                                                         (0.085)
>    
bachelor's                                                                 1.583
> ***
                                                                         (0.075)
>    
graduate                                                                   1.782
> ***
                                                                         (0.102)
>    
Women                                                                      0.669
> ***
                                                                         (0.017)
>    
Black                                                                      0.856
> ***
                                                                         (0.033)
>    
Other                                                                      0.510
> ***
                                                                         (0.036)
>    
Hispanic                                                                   0.438
> ***
                                                                         (0.025)
>    
--------------------------------------------------------------------------------
> ---
Observations                               34466           34466           34466
>    
--------------------------------------------------------------------------------
> ---
Exponentiated coefficients; Standard errors in parentheses
* p<0.05, ** p<0.01, *** p<0.001
modelsummary(list("Model 1" = mjmod1, "Model 2" = mjmod2, "Model 3" = mjmod3),
  exponentiate = TRUE, fmt = 3,
  stars = c("*" = .05, "**" = .01, "***" = .001),
  coef_rename = TRUE, gof_map = "nobs")
Model 1 Model 2 Model 3
* p < 0.05, ** p < 0.01, *** p < 0.001
(Intercept) 1.014 1.166** 1.288***
(0.047) (0.055) (0.077)
age of respondent 0.976*** 0.978*** 0.976***
(0.001) (0.001) (0.001)
year1975 [1st degree] 0.956*** 0.958*** 0.957***
(0.003) (0.003) (0.003)
year1975 [2nd degree] 1.002*** 1.002*** 1.002***
(0.000) (0.000) (0.000)
conserv [Conservative] 0.467*** 0.433***
(0.013) (0.012)
Family Income in 1000s 0.999
(0.000)
degree [high school] 1.274***
(0.049)
degree [associate/junior college] 1.435***
(0.085)
degree [bachelor's] 1.583***
(0.075)
degree [graduate] 1.782***
(0.102)
woman [Women] 0.669***
(0.017)
race4 [Black] 0.856***
(0.033)
race4 [Other] 0.510***
(0.036)
race4 [Hispanic] 0.438***
(0.025)
Num.Obs. 34466 34466 34466
  • Default is to show table within Stata/R, can export to Word (.rtf or .docx), Excel, LaTeX, etc.
* Use .rtf format to export to Word
esttab mjmod1 mjmod2 mjmod3 using or_table.rtf, replace ///
    eform nobase b(3) se(3) nogaps label varwidth(35)
modelsummary(list("(1)" = mjmod1, "(2)" = mjmod2, "(3)" = mjmod3),
  exponentiate = TRUE, fmt = 3,
  stars = c("*" = .05, "**" = .01, "***" = .001),
  coef_rename = TRUE, gof_map = "nobs",
  output = "mjmods_or.docx")

Tables of marginal effects

  • In Stata, use mecompare – see “plotting and tabulating results”
  • mecompare without a variable list will calculate AMEs for all IVs in the model
    • I always add amount(sd) so all continuous IVs are a +SD effect
    • Option store(sname) will save the MEs; sname is your name for what they are called
    • Also pass the stored model(mname)
      • Stored MEs are named sname_mname
    • Use esttab to make the table
  • In R, use avg_comparisons and list all IVs in a variables = list() option
    • Save each call as an object
    • Use modelsummary to make the table
      • Note my shape option which is needed here
* Use esttab to make a single table of MEs.
** mecompare saves them as the store() name _ model() name
esttab mes1_mjmod1 mes2_mjmod2 mes3_mjmod3, ///
    b(3) se(3) nogaps label varwidth(32)
--------------------------------------------------------------------------------
                                          (1)             (2)             (3)   
                                 should mar~l    should mar~l    should mar~l   
--------------------------------------------------------------------------------
age + SD (centered)                    -0.080***       -0.073***       -0.076***
                                      (0.002)         (0.002)         (0.002)   
year1975 + SD (centered)                0.155***        0.154***        0.157***
                                      (0.002)         (0.002)         (0.002)   
Conservative - Not Conservative                        -0.143***       -0.153***
                                                      (0.005)         (0.005)   
faminc + SD (centered)                                                 -0.003   
                                                                      (0.003)   
high school - less than high sch                                        0.044***
                                                                      (0.007)   
associate/junio - less than high                                        0.067***
                                                                      (0.011)   
bachelor's - less than high scho                                        0.086***
                                                                      (0.009)   
graduate - less than high school                                        0.109***
                                                                      (0.011)   
Women - Men                                                            -0.075***
                                                                      (0.005)   
Black - White                                                          -0.029***
                                                                      (0.007)   
Other - White                                                          -0.117***
                                                                      (0.011)   
Hispanic - White                                                       -0.140***
                                                                      (0.008)   
--------------------------------------------------------------------------------
Observations                            34466           34466           34466   
--------------------------------------------------------------------------------
Standard errors in parentheses
* p<0.05, ** p<0.01, *** p<0.001
sd_age <- round(sd(gss$age), 2)
sd_year <- round(sd(gss$year1975), 2)
sd_inc <- round(sd(gss$faminc), 2)

mes1 <- avg_comparisons(mjmod1, variables = list(age = sd_age, year1975 = sd_year))
mes2 <- avg_comparisons(mjmod2,
  variables = list(age = sd_age, year1975 = sd_year, conserv = "reference"))
mes3 <- avg_comparisons(mjmod3,
  variables = list(age = sd_age, year1975 = sd_year, conserv = "reference",
    faminc = sd_inc, degree = "reference", woman = "reference", race4 = "reference"))

modelsummary(list("Model 1" = mes1, "Model 2" = mes2, "Model 3" = mes3),
  shape = term : contrast ~ model, fmt = 3,
  stars = c("*" = .05, "**" = .01, "***" = .001),
  gof_map = "nobs")
Model 1 Model 2 Model 3
* p < 0.05, ** p < 0.01, *** p < 0.001
age +17.19 -0.080*** -0.073*** -0.076***
(0.002) (0.002) (0.002)
year1975 +14.61 0.155*** 0.154*** 0.157***
(0.002) (0.002) (0.002)
conserv Conservative - Not Conservative -0.143*** -0.153***
(0.005) (0.005)
degree associate/junior college - less than high school 0.067***
(0.011)
degree bachelor's - less than high school 0.086***
(0.009)
degree graduate - less than high school 0.109***
(0.011)
degree high school - less than high school 0.044***
(0.007)
faminc +30.29 -0.003
(0.003)
race4 Black - White -0.029***
(0.007)
race4 Hispanic - White -0.140***
(0.008)
race4 Other - White -0.117***
(0.011)
woman Women - Men -0.075***
(0.005)
Num.Obs. 34466 34466 34466

esttab and modelsummary have tons of customization options to produce publication-ready tables

Plots of marginal effects

  • You could plot the odds ratios, but I don’t find it very informative and it can be problematic
    • Effect size is hard to understand
      • ORs rescale across models, so are not comparable / cannot be used to compare effect sizes across models
    • All continuous IVs are +1 effects, which are not comparable across IVs, and usually don’t reflect much of that variable’s effect
  • Marginal effects do not have these limitations, can be compared directly across models, and can be put in a meaningful unit of change (my default is to plot the +SD MEs for continuous IVs)
    • 2sd or trimrange is also reasonable
  • Always include a reference line at 0 which indicates no effect
* Use coefplot. Add a xline(0) to show no effect
coefplot mes1_mjmod1 mes2_mjmod2 mes3_mjmod3, xline(0) ///
    legend(order(2 "Model 1 " 4 "Model 2" 6 "Model 3")) ///
    xtitle("ME on Pr(support legal)") graphregion(margin(l=10)) ///
    title("[3.10.a] Average marginal effects from binary logit: Pr(supports ma
rijuana legalization)", span)

Average marginal effects with confidence intervals from the three models: the effects of age and year change little across models, and conservatives, women, and Black, Other, and Hispanic adults are less supportive while more educated adults are more supportive.

modelplot(list("Model 1" = mes1, "Model 2" = mes2, "Model 3" = mes3),
  shape = term : contrast ~ model) +
  geom_vline(xintercept = 0) +
  scale_y_discrete(limits = rev) +
  labs(title = "[3.10.a] Average marginal effects from binary logit: Pr(supports marijuana legalization)",
    x = "ME on Pr(support legal)", color = NULL) +
  theme(plot.title.position = "plot")

Average marginal effects with confidence intervals from the three models: the effects of age and year change little across models, and conservatives, women, and Black, Other, and Hispanic adults are less supportive while more educated adults are more supportive.

  • In Stata, can customize coefplot, e.g. with headings for nominal IVs
* can customize coefplot, e.g. with headings for nominal IVs
coefplot mes1_mjmod1 mes2_mjmod2 mes3_mjmod3, ///
    headings("high school - *" = "{bf:Education}" ///
             "Black - *" = "{bf:Race-Ethnicity}") ///
    xline(0) legend(order(2 "Model 1 " 4 "Model 2" 6 "Model 3")) ///
    xtitle("ME on Pr(support legal)") graphregion(margin(l=10)) ///
    title("[3.10.b] Average marginal effects from binary logit: Pr(supports ma
rijuana legalization)", span)

The same average marginal effects plot with headings that group the education and race-ethnicity contrasts.

Back to top