How to Perform Logistic Regression in R (Step-by-Step)


Logistic regression is a method we can use to fit a regression model when the response variable is binary.

Logistic regression uses a method known as maximum likelihood estimation to find an equation of the following form:

log[p(X) / (1-p(X))]  =  β0 + β1X1 + β2X2 + … + βpXp

where:

  • Xj: The jth predictor variable
  • βj: The coefficient estimate for the jth predictor variable

The formula on the right side of the equation predicts the log odds of the response variable taking on a value of 1.

Thus, when we fit a logistic regression model we can use the following equation to calculate the probability that a given observation takes on a value of 1:

p(X) = eβ0 + β1X1 + β2X2 + … + βpXp / (1 + eβ0 + β1X1 + β2X2 + … + βpXp)

We then use some probability threshold to classify the observation as either 1 or 0.

For example, we might say that observations with a probability greater than or equal to 0.5 will be classified as “1” and all other observations will be classified as “0.”

This tutorial provides a step-by-step example of how to perform logistic regression in R.

Step 1: Load the Data

For this example, we’ll use the Default dataset from the ISLR package. We can use the following code to load and view a summary of the dataset:

#load dataset
data <- ISLR::Default

#view summary of dataset
summary(data)

 default    student       balance           income     
 No :9667   No :7056   Min.   :   0.0   Min.   :  772  
 Yes: 333   Yes:2944   1st Qu.: 481.7   1st Qu.:21340  
                       Median : 823.6   Median :34553  
                       Mean   : 835.4   Mean   :33517  
                       3rd Qu.:1166.3   3rd Qu.:43808  
                       Max.   :2654.3   Max.   :73554  

#find total observations in dataset
nrow(data)

[1] 10000

This dataset contains the following information about 10,000 individuals:

  • default: Indicates whether or not an individual defaulted.
  • student: Indicates whether or not an individual is a student.
  • balance: Average balance carried by an individual.
  • income: Income of the individual.

We will use student status, bank balance, and income to build a logistic regression model that predicts the probability that a given individual defaults.

Step 2: Create Training and Test Samples

Next, we’ll split the dataset into a training set to train the model on and a testing set to test the model on.

#make this example reproducible
set.seed(1)

#Use 70% of dataset as training set and remaining 30% as testing set
sample <- sample(c(TRUE, FALSE), nrow(data), replace=TRUE, prob=c(0.7,0.3))
train <- data[sample, ]
test <- data[!sample, ]  

Step 3: Fit the Logistic Regression Model

Next, we’ll use the glm (general linear model) function and specify family=”binomial” so that R fits a logistic regression model to the dataset:

#fit logistic regression model
model <- glm(default~student+balance+income, family="binomial", data=train)

#disable scientific notation for model summary
options(scipen=999)

#view model summary
summary(model)

Call:
glm(formula = default ~ student + balance + income, family = "binomial", 
    data = train)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-2.5586  -0.1353  -0.0519  -0.0177   3.7973  

Coefficients:
                 Estimate    Std. Error z value            Pr(>|z|)    
(Intercept) -11.478101194   0.623409555 -18.412 <0.0000000000000002 ***
studentYes   -0.493292438   0.285735949  -1.726              0.0843 .  
balance       0.005988059   0.000293765  20.384 <0.0000000000000002 ***
income        0.000007857   0.000009965   0.788              0.4304    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 2021.1  on 6963  degrees of freedom
Residual deviance: 1065.4  on 6960  degrees of freedom
AIC: 1073.4

Number of Fisher Scoring iterations: 8

The coefficients in the output indicate the average change in log odds of defaulting. For example, a one unit increase in balance is associated with an average increase of 0.005988 in the log odds of defaulting.

The p-values in the output also give us an idea of how effective each predictor variable is at predicting the probability of default:

  • P-value of student status: 0.0843
  • P-value of balance: <0.0000
  • P-value of income: 0.4304

We can see that balance and student status seem to be important predictors since they have low p-values while income is not nearly as important.

Assessing Model Fit:

In typical linear regression, we use R2 as a way to assess how well a model fits the data. This number ranges from 0 to 1, with higher values indicating better model fit.

However, there is no such R2 value for logistic regression. Instead, we can compute a metric known as McFadden’s R2, which ranges from 0 to just under 1. Values close to 0 indicate that the model has no predictive power. In practice, values over 0.40 indicate that a model fits the data very well.

We can compute McFadden’s R2 for our model using the pR2 function from the pscl package:

pscl::pR2(model)["McFadden"]

 McFadden 
0.4728807 

A value of 0.4728807 is quite high for McFadden’s R2, which indicates that our model fits the data very well and has high predictive power.

Variable Importance:

We can also compute the importance of each predictor variable in the model by using the varImp function from the caret package:

caret::varImp(model)

             Overall
studentYes  1.726393
balance    20.383812
income      0.788449

Higher values indicate more importance. These results match up nicely with the p-values from the model. Balance is by far the most important predictor variable, followed by student status and then income.

VIF Values:

We can also calculate the VIF values of each variable in the model to see if multicollinearity is a problem:

#calculate VIF values for each predictor variable in our model
car::vif(model)

 student  balance   income 
2.754926 1.073785 2.694039

As a rule of thumb, VIF values above 5 indicate severe multicollinearity. Since none of the  predictor variables in our models have a VIF over 5, we can assume that multicollinearity is not an issue in our model.

Step 4: Use the Model to Make Predictions

Once we’ve fit the logistic regression model, we can then use it to make predictions about whether or not an individual will default based on their student status, balance, and income:

#define two individuals
new <- data.frame(balance = 1400, income = 2000, student = c("Yes", "No"))

#predict probability of defaulting
predict(model, new, type="response")

         1          2 
0.02732106 0.04397747

The probability of an individual with a balance of $1,400, an income of $2,000, and a student status of “Yes” has a probability of defaulting of .0273. Conversely, an individual with the same balance and income but with a student status of “No” has a probability of defaulting of 0.0439

We can use the following code to calculate the probability of default for every individual in our test dataset:

#calculate probability of default for each individual in test dataset
predicted <- predict(model, test, type="response")

Step 5: Model Diagnostics

Lastly, we can analyze how well our model performs on the test dataset.

By default, any individual in the test dataset with a probability of default greater than 0.5 will be predicted to default. However, we can find the optimal probability to use to maximize the accuracy of our model by using the optimalCutoff() function from the InformationValue package:

library(InformationValue)

#convert defaults from "Yes" and "No" to 1's and 0's
test$default <- ifelse(test$default=="Yes", 1, 0)

#find optimal cutoff probability to use to maximize accuracy
optimal <- optimalCutoff(test$default, predicted)[1]
optimal

[1] 0.5451712

This tells us that the optimal probability cutoff to use is 0.5451712. Thus, any individual with a probability of defaulting of 0.5451712 or higher will be predicted to default, while any individual with a probability less than this number will be predicted to not default.

Using this threshold, we can create a confusion matrix which shows our predictions compared to the actual defaults:

confusionMatrix(test$default, predicted)

     0  1
0 2912 64
1   21 39

We can also calculate the sensitivity (also known as the “true positive rate”) and specificity (also known as the “true negative rate”) along with the total misclassification error (which tells us the percentage of total incorrect classifications):

#calculate sensitivity
sensitivity(test$default, predicted)

[1] 0.3786408

#calculate specificity
specificity(test$default, predicted)

[1] 0.9928401

#calculate total misclassification error rate
misClassError(test$default, predicted, threshold=optimal)

[1] 0.027

The total misclassification error rate is 2.7% for this model. In general, the lower this rate the better the model is able to predict outcomes, so this particular model turns out to be very good at predicting whether an individual will default or not.

Lastly, we can plot the ROC (Receiver Operating Characteristic) Curve which displays the percentage of true positives predicted by the model as the prediction probability cutoff is lowered from 1 to 0. The higher the AUC (area under the curve), the more accurately our model is able to predict outcomes:

#plot the ROC curve
plotROC(test$default, predicted)

ROC curve in R

We can see that the AUC is 0.9131, which is quite high. This indicates that our model does a good job of predicting whether or not an individual will default.

The complete R code used in this tutorial can be found here.

15 Replies to “How to Perform Logistic Regression in R (Step-by-Step)”

  1. I learned more from this post than I did from all other explanations of logistic regression that I have looked at. Great job, Zach!

  2. You have found the optimal threshold to maximize accuracy but by looking at the code you haven’t used it to create the confusion matrix, have you?

  3. Such a detailed and easy-to-understand article. Thank you so much! I have learned from this blog more than I did in class!

  4. Issues:
    The pscl and InformationValue packages not available anymore. Are there alternative packages to accomplish the same tasks?

    1. Hi Andrew…Yes, the example of performing Logistic Regression in R can be updated to use a different package. Below, I’ll show how to perform logistic regression using the **`caret`** package, which is widely used for building and evaluating models in R.

      ### Step-by-Step Logistic Regression in R Using `caret`

      #### 1. Install and Load Required Libraries
      “`R
      # Install required packages
      install.packages(“caret”)
      install.packages(“e1071”) # Required for `caret` to run logistic regression

      # Load the libraries
      library(caret)
      “`

      #### 2. Load and Prepare the Dataset
      We’ll use the built-in `mtcars` dataset, assuming we want to predict whether a car has an automatic (`am = 0`) or manual (`am = 1`) transmission based on other features.

      “`R
      # Load the dataset
      data(mtcars)

      # Convert the dependent variable `am` to a factor (for classification)
      mtcars$am <- as.factor(mtcars$am) # Split the data into training and testing sets set.seed(123) # For reproducibility train_index <- createDataPartition(mtcars$am, p = 0.8, list = FALSE) train_data <- mtcars[train_index, ] test_data <- mtcars[-train_index, ] ``` --- #### 3. Train the Logistic Regression Model Use the `train` function from `caret` to train the model. ```R # Train a logistic regression model model <- train( am ~ ., # Formula for the model data = train_data, # Training dataset method = "glm", # Specify Generalized Linear Model family = "binomial", # Logistic regression trControl = trainControl(method = "cv", number = 10) # 10-fold cross-validation ) # View the model summary summary(model) ``` --- #### 4. Make Predictions Use the trained model to make predictions on the test data. ```R # Predict probabilities predicted_probs <- predict(model, test_data, type = "prob")[,2] # Predict class labels predicted_classes <- predict(model, test_data) ``` --- #### 5. Evaluate the Model Evaluate the performance using accuracy, confusion matrix, and AUC. ```R # Confusion matrix confusionMatrix(predicted_classes, test_data$am) # Calculate AUC library(pROC) roc_curve <- roc(test_data$am, predicted_probs) auc(roc_curve) ``` --- ### Key Differences with `caret`: - **Cross-validation**: `caret` supports automated cross-validation through the `trainControl` function. - **Metrics**: You can specify various metrics (e.g., accuracy, AUC) directly during training. - **Flexibility**: Supports numerous algorithms and hyperparameter tuning. This approach is more versatile and widely used for logistic regression and other modeling tasks in R.

    1. Applying **logistic regression** to **panel data** (also called longitudinal or repeated measures data) requires special attention because panel data contains multiple observations per subject (or unit) over time, leading to **correlated errors** that violate basic logistic regression assumptions.

      Here’s a step-by-step guide to applying logistic regression for panel data:

      ### ✅ **1. Understand the Structure of Your Data**
      Panel data has this general format:
      – **ID**: Identifier for each individual (e.g., person, firm, country).
      – **Time**: Time periods (e.g., years, months).
      – **Outcome**: A binary outcome (0 or 1).
      – **Covariates**: Time-varying or fixed characteristics.

      Example:

      | ID | Time | Outcome (Y) | X1 | X2 |
      |—-|——|————-|—-|—-|
      | 1 | 2001 | 1 | 5 | 7 |
      | 1 | 2002 | 0 | 4 | 8 |
      | 2 | 2001 | 0 | 6 | 6 |
      | 2 | 2002 | 1 | 5 | 7 |

      ### ✅ **2. Choose the Appropriate Model**
      Since ordinary logistic regression assumes independence between observations, for panel data, use models that **account for within-subject correlation**:

      #### ▶️ **Fixed Effects Logistic Regression**
      – Controls for unobserved individual-specific effects that don’t vary over time.
      – Example in R (using `clogit` from `survival` or `fixest::feglm`):

      “`r
      library(survival)
      clogit(Y ~ X1 + X2 + strata(ID), data = panel_data)
      “`

      **Pros**: Controls for all time-invariant confounders.
      **Cons**: Can’t estimate coefficients of time-invariant variables.

      #### ▶️ **Random Effects Logistic Regression**
      – Assumes individual-specific effects are random and uncorrelated with covariates.
      – Example in R (using `glmer` from `lme4`):

      “`r
      library(lme4)
      glmer(Y ~ X1 + X2 + (1 | ID), data = panel_data, family = binomial)
      “`

      **Pros**: Estimates both time-varying and time-invariant predictors.
      **Cons**: Risk of bias if random effects assumption is violated.

      #### ▶️ **Population-Averaged Model (GEE)**
      – Uses **Generalized Estimating Equations** to model average population effects.
      – Example in R (using `geepack`):

      “`r
      library(geepack)
      geeglm(Y ~ X1 + X2, data = panel_data, id = ID, family = binomial)
      “`

      **Pros**: Robust to misspecification of the correlation structure.
      **Cons**: Less efficient if correlation structure is mis-specified.

      ### ✅ **3. Model Selection and Assumptions**
      – **Hausman Test**: Can be used to decide between fixed vs random effects.
      – Consider using **clustered standard errors** if you’re using pooled logistic regression and suspect within-group correlation.

      ### ✅ **4. Software Support**
      – **R**: `lme4`, `geepack`, `fixest`, `plm`, `survival`
      – **Stata**:
      – Fixed effects: `xtlogit Y X1 X2, fe`
      – Random effects: `xtlogit Y X1 X2, re`
      – **Python** (more limited): Use `statsmodels` with GEE (`GEE` class).

      ### 🧠 Summary Table

      | Model Type | Accounts for ID Effects | Time-invariant Predictors | Example |
      |——————-|————————-|—————————-|———|
      | Fixed Effects | Yes (non-random) | ❌ Not estimated | `clogit` |
      | Random Effects | Yes (random) | ✅ Can estimate | `glmer` |
      | GEE (Population Avg) | Marginal effects | ✅ Can estimate | `geeglm` |

Leave a Reply

Your email address will not be published. Required fields are marked *