Visualizing models 101, using R. So you’ve got yourself a model, now... | by Peter Nistrup | Towards Data Science

This is (almost) a direct sequel to my previous article on “Model selection 101, using R”: (I should probably have made this a series huh?)
medium.com
So please check that out if you haven’t already as most examples will be using the same data and model from that analysis.
That being said, this is a sequel as well as a 101 in and of itself, meaning— like in the last article — that this will be mostly introductory. Go ahead to the end of this article if you want to download the data for yourself and follow along!
Make sure to follow my profile if you enjoy this article and want to see more!
In this article we’ll be taking a look at visualizing some of the findings you’ve made with your model, like the marginal effects of a given explanatory-variable on our response-variable.
We’ll be making pretty plots like this:
We have to do a little bit of house-keeping to prepare our data and models for proper visualization:
dat$gov.support <- exp(dat$gov.support)dat$employed <- factor(dat$employed, levels(dat$employed)[c(2,1)])
The first command applies the exp() transformation on our ‘gov.support’ values, this is because the values are log-transformed by default in the raw data. The reason we’re doing this is so that when we fit the model with ‘log(gov.support)’ we get the same results as in the previous article but we get the added benefit of ‘back’-transforming our data which I will show later!
The second command simply swaps the order of our categorical response-variable ‘employed’ so that our model-fits will default to fitting for ‘employed == “yes”’ even when not explicitly defining it. This is important because now we can fit our model without using a “logical” response-variable which checks whether or not ‘employed == “yes”’ or not.
This means that while the base-model from the last article was created like this:
fit.1 <- glm(employed == "yes" ~ ., data = dat, family = binomial)
Now we define it like this:
fit.1 <- glm(employed ~ . - gov.support + log(gov.support), data = dat, family = binomial)summary(fit.1)
Notice we don’t define a logical condition for our response-variable and we replace the new exponentially transformed ‘gov.support’ with a log transformation of the variable to revert back to our initial model:
As evident above we get the exact same summary as we did in the previous article before doing our “cleaning”.
The last thing I want to do is redefine our factor variables to be more clear:
dat$zero.young.children <- factor(dat$young.children == 0)dat$zero.school.children <- factor(dat$school.children == 0)dat$zero.children <- factor(dat$young.children + dat$school.children == 0)
This means our final fit looks like this:
glm(employed ~ foreigner                  + log(gov.support)                  + age                  + school.children                  + zero.young.children                  + zero.school.children                 + zero.children                 + I(age^2)                  + foreigner:age                  + foreigner:zero.children                 + age:school.children                  + log(gov.support):zero.young.children, family = binomial, data = dat)
Lets start by plotting what I find the most interesting, the relationship between “employed” and “gov.support”.
I’m using an absolutely brilliant package called “sjPlot” for this:
library("sjPlot")plot_model(full.fit, type = “pred”, terms = c(“gov.support”))
Giving us this plot:
Completely useless! What’s going on here? Well for our convenience ‘plot_model’ spits our the following error:
This is exactly why we needed to transform the data! Let’s try again:
plot_model(full.fit, type = “pred”, terms = c(“gov.support [exp]”))
This is pretty damn close to being useless as well, what’s going on now?
Lets try taking a look at our model and how it tries to capture the relationships in the data. There’s a few different ways to do this but lets just begin by looking at the coefficients of our full model:
What’s the first thing you notice when looking at this? For me it’s the factor-variable ‘zero.young.children’ having a coefficient of almost 13! This isn’t necessarily a problem though but lets take a look at the confidence-intervals of the coefficients:
confint(full.fit)
Right, this looks like trouble! Take a look at the span on ‘zero.young.children’ this means that we’ll have huge confidence-intervals in our marginal-effect plots. This is most likely why our plot has such a huge confidence-interval.
sjPlot can plot these as well:
plot_model(full.fit, transform = NULL, show.values = TRUE, axis.labels = “”, value.offset = .4)
This makes it real easy to see that the factor variable ‘zero.young.children’ is quite problematic with regard to making any plots with any real confidence.
Lets take a little detour and create a more simple model without any variable-interactions, turns out this is what we get:
simple.fit <- glm(employed ~ foreigner  + age  + zero.young.children  + zero.children  + log(gov.support)  + I(age^2), family = binomial, data = dat)summary(simple.fit)
A small increase in AIC which isn’t a good thing but it’s not too much and we gain a lot of simplicity by using this model instead! (If we measure performance with BIC instead we actually find that this model is better due to BIC penalizing harder on complex models! 1047 vs 1068)
Lets look at the new coefficients:
plot_model(simple.fit, transform = NULL, show.values = TRUE, axis.labels = “”, value.offset = .4)
This look a lot better! Let’s try making our plot once more using this model instead!
plot_model(simple.fit, type = “pred”, terms = c(“gov.support [exp]”))
Nice, something useful! So obviously the probability of being employed is lower the more government support you’re entitled to. This makes quite a bit of intuitive sense! How about the difference between foreigners and non-foreigners? Well plot_model makes this real easy to do as well, just add it as a term!:
plot_model(simple.fit, type = “pred”, terms = c(“gov.support [exp]”, “foreigner”))
This is a breeze! So this model suggests that foreigners are more likely to be employed than non-foreigners given that the other variables are identical.
How about age? This required a slightly different approach because we don’t really assume a strictly positive or negative relationship with age which is why the power-transformation I(age2) makes sense. That is, we don’t expect a 10 year old to be employed, neither a 70 year old, but we do expect a 30 year old to be employed.
Sadly sjPlot doesn’t take too kindly to these kinds of power-transformations (Or I’m just an imbecile who can’t work it out), so I’m using the package ‘jtools’ instead:
effect_plot(simple.fit, pred = age, data = dat, interval = TRUE)
So it seems employment “tops” around the mid to late 30s and then tampers off, this also makes a lot of intuitive sense!
Now what about having children?:
plot_model(simple.fit, type = “pred”, terms = c(“gov.support [exp]”, “zero.children”))
Surprise surprise! Not having any children makes you more likely to be employed and on the market!
Now this probably seems a bit backwards but lets try to compare our plots to the coefficients of our model and try to make sense of it! (I’m a visual learner so this makes sense to me damn it!):
The categorical variables foreigner, zero.children and zero.young.children are easily interpreted, being a non-foreigner (foreignerno) lowers your Log-Odds of being employed by 1.12, not having any young kids improve your Log-Odds by 1.31 and not having any kids at all improves it by 1.06, you might wonder “Why doesn’t zero.children improve the odds more than zero.young.children” well, you have to consider than whenever zero.children is TRUE so is zero.young.children, basically giving an “accumulated improvement” to the Log-Odds.
Higher age improves your Log-Odds of being employed by 0.37, BUT the odds also declines by age2 * -0.0055, what does this mean? Well this is simply a concave 2. degree polynomial, somewhat similar to our plot of age, that is, we see an initial rise, followed by some maxima and then a decline.
That’s it! We’ve successfully replicated the values of our coefficient in a visual and intuitive manner, job well done.
If you want to follow along get the data here: https://github.com/pela15ae/statmod/blob/master/employment_data.txt and since it’s Danish you can run the code below to translate all variable-names and categorical values:
names(dat) <- c(“employed”, “foreigner”, “gov.support”, “age”, “education”, “young.children”, “school.children”)levels(dat$employed)[1] <- “yes”levels(dat$employed)[2] <- “no”levels(dat$foreigner)[1] <- “yes”levels(dat$foreigner)[2] <- “no”
Also take a look at data visualization on high-dimensional data:
medium.com
If you want to see and learn more, be sure to follow me on Medium🔍 and Twitter 🐦