I find a lot of the suggestions here for determining nonlinearity in logistic regression, while good, are more indirect approximations of the issue rather than visualizing the problem like you would in OLS. One thing you can do is create a component plus residual (CR) plot by plotting the predictor against the component and the residuals, or:
$$
\hat{\beta_i}X_i ~\text{versus } ~X
$$
With some programming, one can achieve this by fitting the model, plotting the predictor against the components + residuals, and layer on a LOESS line to see if the line is linear or nonlinear. Below I have a custom-made function in R which achieves this that I wrote some time ago (forgive me for the long code below, I am copy-pasting it here since I am in a rush):
#### Logistic GLM(M) Linearity Check
log_linear_check <- function(model,
data,
predictor,
random = F){ # defaults to GLM
Require tidyverse
require(tidyverse)
Toggle correct residuals
type.res <- ifelse(random == F, # if GLM
"deviance", # deviance residuals
"pearson") # if GLMM, Pearson used
Get probabilities
probabilities <- predict(model, type = "response")
Get logits
logits <- log(probabilities/(1-probabilities))
Calculate residuals
residuals <- residuals(model, type = type.res)
Create a dataframe
df <- data.frame(predictor = data[[predictor]],
residuals = residuals,
logits = logits)
Create component + residual plot
ggplot(df, aes(x = predictor,
y = residuals + logits)) + # component plus residual
geom_jitter(
width=.02,
height=.02,
alpha=.4,
size=1,
color = "gray"
)+
geom_smooth(
method = "loess",
color = "darkgreen",
formula = y ~ x
) +
theme_classic() +
ylab("Partial Residual") +
xlab(predictor)+
ggtitle("Logistic Regression Linearity Check")+
geom_vline(aes(xintercept = min(predictor)),
linetype = "dashed")+
geom_vline(aes(xintercept = max(predictor)),
linetype = "dashed")+
scale_x_continuous(n.breaks = 10)+
scale_y_continuous(n.breaks = 10)
}
To check if it works, I will fit a standard logistic regression to the wesdr data in the gamair package in R, which is normally used to showcase logistic GAMs, thus making it a nice candidate for the function. Here I fit the model with a typical GLM using the glm function:
#### Get Data and Inspect ####
library(gamair)
data("wesdr")
head(wesdr)
Fit Model
fit.linear <- glm(
ret ~ dur,
data = wesdr,
family = binomial
)
Check Linearity
log_linear_check(
fit.linear,
wesdr,
"dur",
random =F
)
You can see the CR plot shows substantial nonlinearity:

One can see when fitting a GAM instead that the default plots show very similar lines:
#### Load Libraries ####
library(mgcv)
library(gratia)
Fit GAM
fit.gam <- gam(
ret ~ s(dur),
data = wesdr,
family = binomial,
method = "REML"
)
Plot
draw(fit.gam)
