Visualizing the Effects of Proportional-Odds Logistic Regression

Proportional-odds logistic regression is often used to model an ordered categorical response. By "ordered", we mean categories that have a natural ordering, such as "Disagree", "Neutral", "Agree", or "Everyday", "Some days", "Rarely", "Never". For a primer on proportional-odds logistic regression, see our post, Fitting and Interpreting a Proportional Odds Model. In this post we demonstrate how to visualize a proportional-odds model in R.

To begin, we load the effects package. The effects package provides functions for visualizing regression models. This post is essentially a tutorial for using the effects package with proportional-odds models. We also load the car, MASS and splines packages for particular functions, which we'll explain as we encounter them. If you don't have the effects or car packages, uncomment the lines below and run them in R. MASS and splines are recommended packages that come with R.


# install.packages("effects")
# install.packages("car")

library(effects)
library(car)
library(MASS)
library(splines) 

To demonstrate how to visualize a proportional-odds model we'll use data from the World Values Surveys (1995-1997) for Australia, Norway, Sweden, and the United States. This dataset, WVS, comes with the effects package. Once we load the effects package, the data is ready to access.


head(WVS)


      poverty religion degree country age gender
1  Too Little      yes     no     USA  44   male
2 About Right      yes     no     USA  40 female
3  Too Little      yes     no     USA  36 female
4    Too Much      yes    yes     USA  25 female
5  Too Little      yes    yes     USA  39   male
6 About Right      yes     no     USA  80 female

The response variable of interest is poverty, which is a 3-level ordered categorical variable. It contains the answer to the question "Do you think that what the government is doing for people in poverty in this country is about the right amount, too much, or too little?" The answers are an ordered categorical variable with levels "Too Little", "About Right", and "Too Much". The other variables will serve as our predictors. These include country, gender, religion (belong to a religion?), education (hold a university degree?), and age in years. The data contains 5381 records.

Before we get started we need to note this example comes from an article on the effects package in the Journal of Statistical Software by John Fox and Jangman Hong, the authors of the effects package. You should definitely take the time to read through that article and cite it if you plan to use the effects package for your own research. What we seek to do in this blog post is elaborate on the example and provide some additional details.

Before we can visualize a proportional odds model we need to fit it. For this we use the polr() function from the MASS package. The first model we fit models poverty as a function of country interacted with gender, religion, degree and age. The interaction allows the effects of the predictors to vary with each country.


wvs.1 <- polr(poverty ~ country*(gender + religion + degree + age), data = WVS)
summary(wvs.1)


Call:
polr(formula = poverty ~ country * (gender + religion + degree + 
    age), data = WVS)

Coefficients:
                               Value Std. Error  t value
countryNorway              0.5308176   0.286989  1.84961
countrySweden              0.5446552   0.546029  0.99748
countryUSA                -0.0347317   0.248059 -0.14001
gendermale                 0.0696120   0.090212  0.77165
religionyes                0.0094685   0.112476  0.08418
degreeyes                 -0.1242920   0.167603 -0.74158
age                        0.0155849   0.002597  6.00185
countryNorway:gendermale   0.1873611   0.144503  1.29659
countrySweden:gendermale   0.0563508   0.154414  0.36493
countryUSA:gendermale      0.2119735   0.139513  1.51938
countryNorway:religionyes -0.2186724   0.216256 -1.01118
countrySweden:religionyes -0.8789724   0.513263 -1.71252
countryUSA:religionyes     0.6002277   0.174433  3.44101
countryNorway:degreeyes    0.0558595   0.208202  0.26829
countrySweden:degreeyes    0.6281743   0.214295  2.93136
countryUSA:degreeyes       0.3030866   0.206394  1.46848
countryNorway:age         -0.0157142   0.004367 -3.59846
countrySweden:age         -0.0092122   0.004657 -1.97826
countryUSA:age             0.0005419   0.003975  0.13635

Intercepts:
                       Value   Std. Error t value
Too Little|About Right  0.7161  0.1535     4.6644
About Right|Too Much    2.5355  0.1578    16.0666

Residual Deviance: 10347.07 
AIC: 10389.07 

The summary output is imposing. In addition to 19 coefficients we have 2 intercepts. Larger coefficients with large t-values are indicative of important predictors, but with so many interactions it's hard to to see what's happening or what the model "says". To evaluate whether the interactions are significant, we use the Anova() function from the car package. By default the Anova() function returns Type II tests, which tests each term after all others, save interactions. (The base R anova() function performs Type I tests which tests each term sequentially.)


Anova(wvs.1)


Analysis of Deviance Table (Type II tests)

Response: poverty
                 LR Chisq Df Pr(>Chisq)    
country           250.881  3  < 2.2e-16 ***
gender             10.749  1  0.0010435 ** 
religion            4.132  1  0.0420698 *  
degree              4.284  1  0.0384725 *  
age                49.950  1  1.577e-12 ***
country:gender      3.049  3  0.3841657    
country:religion   21.143  3  9.833e-05 ***
country:degree     12.861  3  0.0049476 ** 
country:age        17.529  3  0.0005501 ***

The Anova result shows all interactions except country:gender are significant. But what do the interactions mean? How do, say, country and age interact?

This is where the effects package enters. The effects package allows us to easily create effect displays. What are effect displays? The documentation for the effects package explains it this way:

"To create an effect display, predictors in a term are allowed to range over their combinations of values, while other predictors in the model are held to typical values."

In other words, we take our model and use it to calculate predicted values for various combinations of certain "focal" predictors while holding other predictors at fixed values. Then we plot our predicted values versus the "focal" predictors to see how the response changes. Let's demonstrate.

The two primary functions are Effect() and plot(). Effect() generates the predictions and plot() creates the display. Let's say we're interested in the age and country interaction. We want to visualize how age affects views on poverty for each country. Since our model includes an interaction, which was significant, we expect to see different trajectories for each country.

The following code generates the predicted values. The first argument, focal.predictors, is where we list the predictors we're interested in. Notice it requires a vector, which is why we use the c() function. The second argument is the fitted model.


Effect(focal.predictors = c("age","country"), wvs.1)


age*country effect (probability) for Too Little
    country
age  Australia    Norway    Sweden       USA
  20 0.5958889 0.5632140 0.6496015 0.4330782
  30 0.5578683 0.5635318 0.6349615 0.3939898
  40 0.5191570 0.5638496 0.6200674 0.3562127
  50 0.4802144 0.5641674 0.6049438 0.3201442
  60 0.4415107 0.5644851 0.5896166 0.2861049
  70 0.4035049 0.5648027 0.5741134 0.2543308
  80 0.3666230 0.5651203 0.5584631 0.2249734
  90 0.3312402 0.5654379 0.5426958 0.1981045

age*country effect (probability) for About Right
    country
age  Australia    Norway    Sweden       USA
  20 0.3050532 0.3250955 0.2699789 0.3918455
  30 0.3282699 0.3249058 0.2797785 0.4064109
  40 0.3502854 0.3247160 0.2895692 0.4171736
  50 0.3704966 0.3245261 0.2993161 0.4237414
  60 0.3883075 0.3243362 0.3089823 0.4258700
  70 0.4031610 0.3241462 0.3185295 0.4234793
  80 0.4145714 0.3239561 0.3279182 0.4166592
  90 0.4221528 0.3237659 0.3371079 0.4056632

age*country effect (probability) for Too Much
    country
age   Australia    Norway     Sweden       USA
  20 0.09905788 0.1116905 0.08041952 0.1750762
  30 0.11386182 0.1115624 0.08526005 0.1995993
  40 0.13055755 0.1114343 0.09036331 0.2266137
  50 0.14928897 0.1113065 0.09574006 0.2561143
  60 0.17018180 0.1111787 0.10140107 0.2880252
  70 0.19333407 0.1110511 0.10735706 0.3221899
  80 0.21880555 0.1109236 0.11361867 0.3583674
  90 0.24660694 0.1107962 0.12019630 0.3962323

Notice the output lists three sections of probabilities corresponding to each level of the response. The first section lists predicted probabilities for answering "Too Little" for each country for ages ranging from 20 to 90 in increments of 10. The predicted probability a 20-year-old from the USA answers "Too Little" is about 0.43. The second section is for "About right". The predicted probability a 20-year-old from the USA answers "About Right" is about 0.39. Finally, the third section is for "Too Much". The predicted probability a 20-year-old from the USA answers "Too Much" is about 0.18.

This information is much easier to digest as an effect display. Simply insert the original Effect() function call into the plot() function to create the effect display.


plot(Effect(focal.predictors = c("age","country"), wvs.1), rug = FALSE)

Effect plot of the age and country interaction in model wvs.1 with non-focal predictors set to their means.

Now we see how the model "works". For example, in the upper right plot, we see that in the USA, the probability of answering "Too Much" increases rather dramatically with age, while the probabilities for answering the same in Norway and Sweden stay low and constant. Likewise we see that the probability of USA respondents answering "Too Little" decreases with age while the probabilities for Norway and Sweden stay rather high and constant. The effect display shows us where the interactions are happening and to what degree. (Note: setting rug = FALSE turns off the rug plot. When set to TRUE, the default, the marginal distribution of the predictor is displayed on the x axis. In this case we don't find it very helpful since we have so much data.)

Recall that we need to use all predictors to generate these plots. Country and age are the focal predictors, so they are varied. This means gender, religion and degree are held fixed. We can find out the values they're fixed at by saving the result of the Effect() function and viewing the model matrix.


e.out <- Effect(focal.predictors = c("age","country"), wvs.1)
e.out$model.matrix[1,c("gendermale","religionyes","degreeyes")]


 gendermale religionyes   degreeyes 
  0.4935886   0.8539305   0.2124140 

This says gender was set to 0.4935886, religion to 0.8539305, and degree to 0.2124140. In the original data these are indicator variables that take values of 0 or 1 corresponding to No and Yes. Does it make sense to plug in decimals instead of 0s or 1s? It does if you think of modeling a population that is about 49% men, 85% religious, and 21% with a college degree. In fact this describes our sample. By default, the effects package will take the mean of numeric variables that are held fixed. We can verify that's what it's doing:


mean(WVS$gender=="male")


[1] 0.4935886


mean(WVS$religion=="yes")


[1] 0.8539305


mean(WVS$degree=="yes")


[1] 0.212414

If we want to change these values, we can use the given.values argument. It needs to be a named vector that uses the terms as listed in the output summary. For example, to create an effect plot for religious men without a college degree:


e.out <- Effect(focal.predictors = c("country","age"), mod = wvs.1, 
                given.values = c(gendermale = 1, religionyes = 1, degreeyes = 0))
plot(e.out, rug = FALSE)

Effect plot of the age and country interaction in model wvs.1 with non-focal predictors set to religious men without a college degree.

We see that the overall "story" of the display does not change; the trajectories remain the same in each plot. But the overall probabilities have slightly changed due to the new levels of the non-focal predictors.

We can also change the range and number of focal predictors using the xlevels argument. For example, we can set age to range from 20 to 80 in steps of 10. Notice it needs to be a named list.


e.out <- Effect(focal.predictors = c("country","age"), mod = wvs.1,
                 xlevels = list(age = seq(20,80,10)))
plot(e.out, rug = FALSE)

Effect plot of the age and country interaction in model wvs.1 with age ranging from 20 to 80 in steps of 10.

We can investigate other interactions using the same syntax. Below is the effect display for the religion and country interaction. In this case, age is no longer a focal predictor and is held fixed at its mean (45.04).


plot(Effect(focal.predictors = c("religion","country"), wvs.1), rug = FALSE)

Effect plot of the religion and country interaction in model wvs.1 with non-focal predictors set to their means.

We notice, for example, that religious people in the USA have a higher probability of answering "Too Much" compared to their counterparts in the other countries surveyed. We also notice there is much more uncertainty about estimates in Sweden for people who answered "No" to the religion question. This is due to the small numbers of respondents in those categories, as we can see with the xtabs() function.


xtabs(~ religion + country + poverty, data = WVS, country == "Sweden", 
        drop.unused.levels = TRUE)


, , poverty = Too Little

        country
religion Sweden
     no       7
     yes    597

, , poverty = About Right

        country
religion Sweden
     no       5
     yes    369

, , poverty = Too Much

        country
religion Sweden
     no       3
     yes     22

Following the example in Fox's article, let's fit another model that relaxes the linearity assumption for age. We can do this by generating what's called a basis matrix for natural cubic splines. Instead of fitting a regular polynomial such as age + age^2, we fit piecewise cubic polynomials over the range of age separated by a certain number of intervals, or knots. The ns() function in the splines package makes this easy to do. Below we use it in the model formula and specify 4 knots. Harrell (2001) suggests 3-5 knots is usually a good choice (p. 23), so 4 seems wise in this case.


wvs.2 <- polr(poverty ~ country*(gender + religion + degree + ns(age, 4)),data = WVS)
summary(wvs.2)
Anova(wvs.2)

Due to space considerations we don't print the output of the summary() and Anova() functions. The model summary shows information for 31 coefficients and is very difficult to interpret. The Anova result is similar in substance to the first model, showing all interactions except country:gender significant. It's worth noting the AIC of the second model is somewhat lower than the first (10373.85 vs 10389.07), suggesting a slightly better fit.

Let's look at the country and age interaction while allowing age to range from 20 - 80:


plot(Effect(focal.predictors = c("country","age"), mod = wvs.2, 
            xlevels = list(age = 20:80)), rug = FALSE)

Effect plot of the age and country interaction in model wvs.2 with age ranging from 20 to 80 and non-focal predictors set to their means.

We see that using a natural spline allows a nonlinear effect of age. For example we see the probability of answering "Too Little" in the USA decreases sharply from 20 to 30, increases from about age 30 to 45, and then decreases and levels out through age 80.

The effects package also allows us to create "stacked" effect displays for proportional-odds models. We do this by setting style="stacked" in the plot() function.


plot(Effect(focal.predictors = c("country","age"), mod = wvs.2, 
            xlevels = list(age = 20:80)), 
     rug = FALSE,
     style="stacked")

Stacked effect plot of the age and country interaction in model wvs.2  with age ranging from 20 to 80 and non-focal predictors set to their means.

This plot is useful for allowing us to compare probabilities across the response categories. For example, in Norway and Sweden, people are most likely to answer "Too Little" regardless of age. The blue shaded regions dominate their graphs.

We can also create a "latent" version of the effect display. In this plot, the y axis is on the logit scale, which we interpret to be a latent, or hidden, scale from which the ordered categories are derived. We create it by setting latent = TRUE in the Effect() function.


plot(Effect(focal.predictors = c("country","age"), mod = wvs.2, 
            xlevels = list(age = 20:80),
            latent = TRUE),
     rug = FALSE,
     ylim = c(0,3.5))

Latent version of effect plot for the age and country interaction in model wvs.2 with age ranging from 20 to 80 and non-focal predictors set to their means.

This plot is useful when we're more interested in classification than probability. The horizontal lines in the plots correspond to the intercepts in the summary output. We can think of these lines as threshholds that define where we crossover from one category to the next on the latent scale. The TL-AR line indicates the boundary between the "Too Little" and "About Right" categories. The AR-TM line indicates the boundary between the "About Right" and "Too Much" categories. Like the "stacked" effect display, we see that someone from Norway and Sweden would be expected to answer "Too Little" regardless of age, though the confidence ribbon indicates this expectation is far from certain, especially for older and younger respondents. On the other hand, most USA respondents are expected to answer "About Right".

Let's see how the "latent" plot changes when we set the non-focal predictors to college-educated, non-religious female.


plot(Effect(focal.predictors = c("country","age"), mod = wvs.2, 
            xlevels = list(age = 20:80),
            given.values = c(gendermale = 0, religionyes = 0, degreeyes = 1),
            latent = TRUE),
     rug = FALSE,
     ylim = c(0,3.5))

Latent version of effect plot for the age and country interaction in model wvs.2 with age ranging from 20 to 80 and non-focal predictors set to college-educated, non-religious female.

Notice now that predicted classification for Sweden is "About Right" over the age range but with increased uncertainty. We also see increased chances of answering "Too Little" for certain age ranges in the USA.

We can add gender as a focal predictor to compare plots for males versus females:


plot(Effect(focal.predictors = c("country","age","gender"), mod = wvs.2, 
            xlevels = list(age = 20:80),
            latent = TRUE),
     rug = FALSE,
     ylim = c(0,3.5))

Latent version of effect plot for the country, age, and gender interaction in model wvs.2 with age ranging from 20 to 80 and non-focal predictors set to their means.

Since we didn't fit a 3-way interaction between country, gender and age, the trajectories do not change between genders. They simply shift horizontally between the two levels of gender.


References

  • Fox J, Hong J (2009). Effect displays in R for multinomial and proportional-odds logit models: Extensions to the effects package. Journal of Statistical Software 32:1, 1–24, http://www.jstatsoft.org/v32/i01/.
  • Fox J, Weisberg S (2019). An R Companion to Applied Regression, Third edition. Sage, Thousand Oaks CA. .
  • Harrell F (2001). Regression Modeling Strategies. Springer.
  • R Core Team (2017). R: A language and environment for statistical computing. R Foundation for Statistical Computing, Vienna, Austria. URL https://www.R-project.org/.
  • Venables W, Ripley B (2002). Modern Applied Statistics with S. Fourth Edition. Springer, New York. ISBN 0-387-95457-0

Clay Ford
Statistical Research Consultant
University of Virginia Library
May 10, 2017
Updated May 24, 2023


For questions or clarifications regarding this article, contact statlab@virginia.edu.

View the entire collection of UVA Library StatLab articles, or learn how to cite.