Medical Appointment Waiting Times

Author

Your Group Names

Introduction

For this project, we analyze a cross-sectional dataset containing information about medical appointments.

The goal of our analysis is to investigate factors that are associated with how long patients wait for their medical appointments.

Our main research question is:

What factors are associated with the number of days a patient waits between scheduling and their medical appointment?

Our dependent variable, or Y variable, is WaitingDays.

We create this variable by calculating the number of days between the date an appointment was scheduled and the date the appointment occurred.

We then use multiple linear regression to examine whether characteristics such as age, gender, health conditions, SMS reminders, and appointment attendance are associated with waiting time.

Setup

# Load packages used in the project
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(knitr)

Import the Data

# Import the original dataset
appointments <- read.csv("KaggleV2-May-2016.csv")

# Display the first six rows
head(appointments)
     PatientId AppointmentID Gender         ScheduledDay       AppointmentDay
1 2.987250e+13       5642903      F 2016-04-29T18:38:08Z 2016-04-29T00:00:00Z
2 5.589978e+14       5642503      M 2016-04-29T16:08:27Z 2016-04-29T00:00:00Z
3 4.262962e+12       5642549      F 2016-04-29T16:19:04Z 2016-04-29T00:00:00Z
4 8.679512e+11       5642828      F 2016-04-29T17:29:31Z 2016-04-29T00:00:00Z
5 8.841186e+12       5642494      F 2016-04-29T16:07:23Z 2016-04-29T00:00:00Z
6 9.598513e+13       5626772      F 2016-04-27T08:36:51Z 2016-04-29T00:00:00Z
  Age     Neighbourhood Scholarship Hipertension Diabetes Alcoholism Handcap
1  62   JARDIM DA PENHA           0            1        0          0       0
2  56   JARDIM DA PENHA           0            0        0          0       0
3  62     MATA DA PRAIA           0            0        0          0       0
4   8 PONTAL DE CAMBURI           0            0        0          0       0
5  56   JARDIM DA PENHA           0            1        1          0       0
6  76         REPÚBLICA           0            1        0          0       0
  SMS_received No.show
1            0      No
2            0      No
3            0      No
4            0      No
5            0      No
6            0      No

Checking the Dataset

First, we check the size of the dataset.

# Number of rows and columns
dim(appointments)
[1] 110527     14

Next, we look at the names of all variables.

# Display column names
names(appointments)
 [1] "PatientId"      "AppointmentID"  "Gender"         "ScheduledDay"  
 [5] "AppointmentDay" "Age"            "Neighbourhood"  "Scholarship"   
 [9] "Hipertension"   "Diabetes"       "Alcoholism"     "Handcap"       
[13] "SMS_received"   "No.show"       

We can also examine the structure of the dataset.

# Examine variable types
str(appointments)
'data.frame':   110527 obs. of  14 variables:
 $ PatientId     : num  2.99e+13 5.59e+14 4.26e+12 8.68e+11 8.84e+12 ...
 $ AppointmentID : int  5642903 5642503 5642549 5642828 5642494 5626772 5630279 5630575 5638447 5629123 ...
 $ Gender        : chr  "F" "M" "F" "F" ...
 $ ScheduledDay  : chr  "2016-04-29T18:38:08Z" "2016-04-29T16:08:27Z" "2016-04-29T16:19:04Z" "2016-04-29T17:29:31Z" ...
 $ AppointmentDay: chr  "2016-04-29T00:00:00Z" "2016-04-29T00:00:00Z" "2016-04-29T00:00:00Z" "2016-04-29T00:00:00Z" ...
 $ Age           : int  62 56 62 8 56 76 23 39 21 19 ...
 $ Neighbourhood : chr  "JARDIM DA PENHA" "JARDIM DA PENHA" "MATA DA PRAIA" "PONTAL DE CAMBURI" ...
 $ Scholarship   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ Hipertension  : int  1 0 0 0 1 1 0 0 0 0 ...
 $ Diabetes      : int  0 0 0 0 1 0 0 0 0 0 ...
 $ Alcoholism    : int  0 0 0 0 0 0 0 0 0 0 ...
 $ Handcap       : int  0 0 0 0 0 0 0 0 0 0 ...
 $ SMS_received  : int  0 0 0 0 0 0 0 0 0 0 ...
 $ No.show       : chr  "No" "No" "No" "No" ...

Each row represents a medical appointment.

Some of the important original variables include:

  • Gender: gender of the patient
  • ScheduledDay: date the appointment was scheduled
  • AppointmentDay: date of the appointment
  • Age: age of the patient
  • Scholarship: scholarship program status
  • Hipertension: hypertension status
  • Diabetes: diabetes status
  • Alcoholism: alcoholism status
  • Handcap: handicap category
  • SMS_received: whether an SMS reminder was received
  • No.show: whether the patient missed the appointment

Data Cleaning

Missing Values

First, we check the dataset for missing values.

# Count missing values in each column
colSums(is.na(appointments))
     PatientId  AppointmentID         Gender   ScheduledDay AppointmentDay 
             0              0              0              0              0 
           Age  Neighbourhood    Scholarship   Hipertension       Diabetes 
             0              0              0              0              0 
    Alcoholism        Handcap   SMS_received        No.show 
             0              0              0              0 

This allows us to determine whether any observations are missing information.

Duplicate Rows

We also check for exact duplicate rows.

# Count exact duplicate rows
sum(duplicated(appointments))
[1] 0

We remove any exact duplicate rows.

# Remove exact duplicates
appointments <- appointments |>
  distinct()

Creating Waiting Days

The original dataset contains both the scheduling date and the appointment date.

We first convert these variables into dates.

# Convert the original date variables into dates
appointments <- appointments |>
  mutate(
    ScheduledDate = as.Date(ScheduledDay),
    AppointmentDate = as.Date(AppointmentDay)
  )

Now we calculate the number of days between the two dates.

# Calculate appointment waiting time
appointments <- appointments |>
  mutate(
    WaitingDays = as.numeric(
      AppointmentDate - ScheduledDate
    )
  )

We examine the new variable.

# Summary of waiting days
summary(appointments$WaitingDays)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  -6.00    0.00    4.00   10.18   15.00  179.00 

Negative waiting times would mean that an appointment occurred before it was scheduled.

These observations do not make sense for our analysis, so we remove them.

# Remove observations with impossible waiting times
appointments <- appointments |>
  filter(WaitingDays >= 0)

Cleaning Age

We examine the age variable.

# Summary of patient age
summary(appointments$Age)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  -1.00   18.00   37.00   37.09   55.00  115.00 

A negative age is impossible, so we remove any observations with an age below zero.

# Remove impossible ages
appointments <- appointments |>
  filter(Age >= 0)

Creating Scheduled Hour

We can create another quantitative variable showing the hour of the day when an appointment was scheduled.

First, we convert ScheduledDay into a date-time variable.

# Convert ScheduledDay into date-time format
appointments$ScheduledDay <- as.POSIXct(
  appointments$ScheduledDay,
  format = "%Y-%m-%dT%H:%M:%SZ",
  tz = "UTC"
)

Now we extract the hour.

# Create scheduled hour variable
appointments <- appointments |>
  mutate(
    ScheduledHour = as.numeric(
      format(ScheduledDay, "%H")
    )
  )

Creating Appointments Per Patient

Some patients appear multiple times in the dataset.

We calculate how many appointments are associated with each patient.

# Count the number of appointments for each patient
appointments <- appointments |>
  group_by(PatientId) |>
  mutate(
    PatientAppointments = n()
  ) |>
  ungroup()

Converting Qualitative Variables

Several variables are stored as numbers even though they represent categories.

We convert these variables into factors.

# Convert qualitative variables into factors
appointments <- appointments |>
  mutate(
    Gender = factor(Gender),
    Scholarship = factor(Scholarship),
    Hipertension = factor(Hipertension),
    Diabetes = factor(Diabetes),
    Alcoholism = factor(Alcoholism),
    Handcap = factor(Handcap),
    SMS_received = factor(SMS_received),
    No_show = factor(No.show)
  )

We created No_show from the original No.show variable to make the name easier to understand and use.

Checking the Cleaned Dataset

Now we examine the cleaned dataset.

# Examine structure after cleaning
str(appointments)
tibble [110,521 × 20] (S3: tbl_df/tbl/data.frame)
 $ PatientId          : num [1:110521] 2.99e+13 5.59e+14 4.26e+12 8.68e+11 8.84e+12 ...
 $ AppointmentID      : int [1:110521] 5642903 5642503 5642549 5642828 5642494 5626772 5630279 5630575 5638447 5629123 ...
 $ Gender             : Factor w/ 2 levels "F","M": 1 2 1 1 1 1 1 1 1 1 ...
 $ ScheduledDay       : POSIXct[1:110521], format: "2016-04-29 18:38:08" "2016-04-29 16:08:27" ...
 $ AppointmentDay     : chr [1:110521] "2016-04-29T00:00:00Z" "2016-04-29T00:00:00Z" "2016-04-29T00:00:00Z" "2016-04-29T00:00:00Z" ...
 $ Age                : int [1:110521] 62 56 62 8 56 76 23 39 21 19 ...
 $ Neighbourhood      : chr [1:110521] "JARDIM DA PENHA" "JARDIM DA PENHA" "MATA DA PRAIA" "PONTAL DE CAMBURI" ...
 $ Scholarship        : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
 $ Hipertension       : Factor w/ 2 levels "0","1": 2 1 1 1 2 2 1 1 1 1 ...
 $ Diabetes           : Factor w/ 2 levels "0","1": 1 1 1 1 2 1 1 1 1 1 ...
 $ Alcoholism         : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
 $ Handcap            : Factor w/ 5 levels "0","1","2","3",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ SMS_received       : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
 $ No.show            : chr [1:110521] "No" "No" "No" "No" ...
 $ ScheduledDate      : Date[1:110521], format: "2016-04-29" "2016-04-29" ...
 $ AppointmentDate    : Date[1:110521], format: "2016-04-29" "2016-04-29" ...
 $ WaitingDays        : num [1:110521] 0 0 0 0 0 2 2 2 0 2 ...
 $ ScheduledHour      : num [1:110521] 18 16 16 17 16 8 15 15 8 12 ...
 $ PatientAppointments: int [1:110521] 2 2 2 2 1 2 1 2 1 1 ...
 $ No_show            : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 2 2 1 1 ...

We also check the final number of observations and variables.

# Final dataset dimensions
dim(appointments)
[1] 110521     20

Variable Types

Our project requires at least four quantitative and four qualitative variables.

Quantitative Variables

We use the following quantitative variables:

  1. Age — age of the patient
  2. WaitingDays — number of days between scheduling and appointment
  3. ScheduledHour — hour when the appointment was scheduled
  4. PatientAppointments — number of appointments associated with the patient

Qualitative Variables

We use the following qualitative variables:

  1. Gender
  2. Scholarship
  3. Hipertension
  4. Diabetes
  5. Alcoholism
  6. Handcap
  7. SMS_received
  8. No_show

Therefore, our dataset meets the requirement of at least four quantitative and four qualitative variables.

Summary Statistics

We create a summary statistics table for the quantitative variables.

# Create summary statistics table
summary_stats <- data.frame(
  
  Variable = c(
    "Age",
    "Waiting Days",
    "Scheduled Hour",
    "Appointments Per Patient"
  ),
  
  Mean = c(
    mean(appointments$Age),
    mean(appointments$WaitingDays),
    mean(appointments$ScheduledHour),
    mean(appointments$PatientAppointments)
  ),
  
  Median = c(
    median(appointments$Age),
    median(appointments$WaitingDays),
    median(appointments$ScheduledHour),
    median(appointments$PatientAppointments)
  ),
  
  SD = c(
    sd(appointments$Age),
    sd(appointments$WaitingDays),
    sd(appointments$ScheduledHour),
    sd(appointments$PatientAppointments)
  ),
  
  Minimum = c(
    min(appointments$Age),
    min(appointments$WaitingDays),
    min(appointments$ScheduledHour),
    min(appointments$PatientAppointments)
  ),
  
  Maximum = c(
    max(appointments$Age),
    max(appointments$WaitingDays),
    max(appointments$ScheduledHour),
    max(appointments$PatientAppointments)
  )
)

Now we display the table.

# Display summary statistics table
kable(
  summary_stats,
  digits = 2,
  caption = "Summary Statistics for Quantitative Variables"
)
Summary Statistics for Quantitative Variables
Variable Mean Median SD Minimum Maximum
Age 37.09 37 23.11 0 115
Waiting Days 10.18 4 15.26 0 179
Scheduled Hour 10.77 10 3.22 6 21
Appointments Per Patient 3.54 2 6.56 1 88

Summary of Qualitative Variables

We can summarize our qualitative variables using summary().

# Summary of qualitative variables
summary(
  appointments[
    c(
      "Gender",
      "Scholarship",
      "Hipertension",
      "Diabetes",
      "Alcoholism",
      "Handcap",
      "SMS_received",
      "No_show"
    )
  ]
)
 Gender    Scholarship Hipertension Diabetes   Alcoholism Handcap   
 F:71836   0:99660     0:88720      0:102578   0:107161   0:108282  
 M:38685   1:10861     1:21801      1:  7943   1:  3360   1:  2040  
                                                          2:   183  
                                                          3:    13  
                                                          4:     3  
 SMS_received No_show    
 0:75039      No :88207  
 1:35482      Yes:22314  
                         
                         
                         

Visualizations

We now visualize our quantitative and qualitative variables.

Histograms are used for quantitative variables and bar charts are used for qualitative variables.

Age

ggplot(
  appointments,
  aes(x = Age)
) +
  geom_histogram(
    binwidth = 5,
    color = "black"
  ) +
  labs(
    title = "Distribution of Patient Age",
    x = "Age",
    y = "Number of Appointments"
  ) +
  theme_minimal()

The dataset contains patients from a wide range of ages.

Waiting Days

ggplot(
  appointments,
  aes(x = WaitingDays)
) +
  geom_histogram(
    binwidth = 5,
    color = "black"
  ) +
  labs(
    title = "Distribution of Appointment Waiting Times",
    x = "Waiting Time (Days)",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Waiting time is particularly important because it is our dependent variable.

Scheduled Hour

ggplot(
  appointments,
  aes(x = ScheduledHour)
) +
  geom_histogram(
    binwidth = 1,
    color = "black"
  ) +
  labs(
    title = "Time of Day Appointments Were Scheduled",
    x = "Hour of Day",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Appointments Per Patient

ggplot(
  appointments,
  aes(x = PatientAppointments)
) +
  geom_histogram(
    binwidth = 1,
    color = "black"
  ) +
  labs(
    title = "Appointments Per Patient",
    x = "Number of Appointments",
    y = "Number of Observations"
  ) +
  theme_minimal()

Gender

ggplot(
  appointments,
  aes(x = Gender)
) +
  geom_bar() +
  labs(
    title = "Appointments by Gender",
    x = "Gender",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Scholarship Status

ggplot(
  appointments,
  aes(x = Scholarship)
) +
  geom_bar() +
  labs(
    title = "Scholarship Status",
    x = "Scholarship",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Hypertension

ggplot(
  appointments,
  aes(x = Hipertension)
) +
  geom_bar() +
  labs(
    title = "Hypertension Status",
    x = "Hypertension",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Diabetes

ggplot(
  appointments,
  aes(x = Diabetes)
) +
  geom_bar() +
  labs(
    title = "Diabetes Status",
    x = "Diabetes",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Alcoholism

ggplot(
  appointments,
  aes(x = Alcoholism)
) +
  geom_bar() +
  labs(
    title = "Alcoholism Status",
    x = "Alcoholism",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Handicap Category

ggplot(
  appointments,
  aes(x = Handcap)
) +
  geom_bar() +
  labs(
    title = "Handicap Categories",
    x = "Handicap Category",
    y = "Number of Appointments"
  ) +
  theme_minimal()

SMS Reminder

ggplot(
  appointments,
  aes(x = SMS_received)
) +
  geom_bar() +
  labs(
    title = "SMS Reminder Status",
    x = "SMS Received",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Appointment Attendance

ggplot(
  appointments,
  aes(x = No_show)
) +
  geom_bar() +
  labs(
    title = "Appointment No-Show Status",
    x = "No-Show",
    y = "Number of Appointments"
  ) +
  theme_minimal()

Exploring Waiting Time

Now that we have visualized our individual variables, we examine relationships between our Y variable and several explanatory variables.

Age vs. Waiting Time

ggplot(
  appointments,
  aes(
    x = Age,
    y = WaitingDays
  )
) +
  geom_point(alpha = 0.1) +
  geom_smooth(
    method = "lm",
    se = FALSE
  ) +
  labs(
    title = "Age vs. Appointment Waiting Time",
    x = "Age",
    y = "Waiting Time (Days)"
  ) +
  theme_minimal()
`geom_smooth()` using formula = 'y ~ x'

The fitted line shows the overall relationship between age and waiting time.

No-Show Status vs. Waiting Time

ggplot(
  appointments,
  aes(
    x = No_show,
    y = WaitingDays
  )
) +
  geom_boxplot() +
  labs(
    title = "Waiting Time by No-Show Status",
    x = "No-Show Status",
    y = "Waiting Time (Days)"
  ) +
  theme_minimal()

This graph allows us to compare waiting times between appointments where the patient attended and appointments where the patient did not attend.

SMS Reminder vs. Waiting Time

ggplot(
  appointments,
  aes(
    x = SMS_received,
    y = WaitingDays
  )
) +
  geom_boxplot() +
  labs(
    title = "Waiting Time by SMS Reminder",
    x = "SMS Received",
    y = "Waiting Time (Days)"
  ) +
  theme_minimal()

Gender vs. Waiting Time

ggplot(
  appointments,
  aes(
    x = Gender,
    y = WaitingDays
  )
) +
  geom_boxplot() +
  labs(
    title = "Waiting Time by Gender",
    x = "Gender",
    y = "Waiting Time (Days)"
  ) +
  theme_minimal()

Multiple Linear Regression

We now run a multiple linear regression.

Our dependent variable is:

Y = WaitingDays

Our explanatory variables are:

  • Age
  • Gender
  • Scholarship status
  • Hypertension status
  • Diabetes status
  • SMS reminder status
  • No-show status
  • Number of appointments per patient

The purpose of the regression is to determine which of these characteristics are associated with differences in appointment waiting time.

# Run multiple linear regression
waiting_model <- lm(
  WaitingDays ~
    Age +
    Gender +
    Scholarship +
    Hipertension +
    Diabetes +
    SMS_received +
    No_show +
    PatientAppointments,
  data = appointments
)

We display the regression results using summary().

# Display regression results
summary(waiting_model)

Call:
lm(formula = WaitingDays ~ Age + Gender + Scholarship + Hipertension + 
    Diabetes + SMS_received + No_show + PatientAppointments, 
    data = appointments)

Residuals:
    Min      1Q  Median      3Q     Max 
-22.364  -6.353  -4.401   2.538 174.479 

Coefficients:
                     Estimate Std. Error t value Pr(>|t|)    
(Intercept)          4.775219   0.099806  47.845   <2e-16 ***
Age                  0.042051   0.002115  19.880   <2e-16 ***
GenderM             -0.101564   0.088476  -1.148    0.251    
Scholarship1        -1.549274   0.140901 -10.995   <2e-16 ***
Hipertension1       -1.219515   0.128543  -9.487   <2e-16 ***
Diabetes1           -1.580893   0.178678  -8.848   <2e-16 ***
SMS_received1       12.202729   0.089709 136.026   <2e-16 ***
No_showYes           5.319342   0.104188  51.055   <2e-16 ***
PatientAppointments -0.169503   0.006378 -26.576   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 13.76 on 110512 degrees of freedom
Multiple R-squared:  0.1866,    Adjusted R-squared:  0.1866 
F-statistic:  3170 on 8 and 110512 DF,  p-value: < 2.2e-16

Understanding the Regression Results

There are several important parts of the regression output.

Coefficients

A regression coefficient estimates the relationship between an explanatory variable and waiting time while holding the other variables constant.

For example, the coefficient for Age tells us the predicted change in waiting days associated with a one-year increase in age, holding the other variables constant.

For qualitative variables, coefficients represent differences compared with a reference category.

P-Values

We use a significance level of:

0.05

A p-value below 0.05 provides evidence of a statistically significant relationship between that explanatory variable and waiting time.

Because this dataset is very large, we should also pay attention to the size of the coefficients. A very small relationship could be statistically significant simply because we have many observations.

R-Squared

R-squared measures the proportion of variation in waiting time explained by the explanatory variables in our regression model.

A larger R-squared means that our model explains more of the differences in appointment waiting times.

Regression Diagnostics

Linear regression makes several assumptions.

We examine diagnostic plots to determine whether our model appears reasonable.

Residuals vs. Fitted Values

# Residuals versus fitted values
plot(
  waiting_model,
  which = 1
)

Ideally, the residuals should be scattered around zero without a strong pattern.

Normal Q-Q Plot

# Normal Q-Q plot
plot(
  waiting_model,
  which = 2
)

The Q-Q plot helps us examine whether the regression residuals are approximately normally distributed.

Predicted Waiting Time

We can use the regression model to generate a predicted waiting time for each appointment.

# Add model predictions to the dataset
appointments <- appointments |>
  mutate(
    PredictedWaitingDays = predict(waiting_model)
  )

Actual vs. Predicted Waiting Time

ggplot(
  appointments,
  aes(
    x = PredictedWaitingDays,
    y = WaitingDays
  )
) +
  geom_point(alpha = 0.1) +
  geom_smooth(
    method = "lm",
    se = FALSE
  ) +
  labs(
    title = "Actual vs. Predicted Waiting Time",
    x = "Predicted Waiting Days",
    y = "Actual Waiting Days"
  ) +
  theme_minimal()
`geom_smooth()` using formula = 'y ~ x'

This graph compares the waiting times predicted by our model with the actual waiting times observed in the dataset.

Key Findings

Our analysis investigates the factors associated with the amount of time between scheduling and a medical appointment.

When interpreting our regression results, we focus on:

  1. Which explanatory variables have p-values below 0.05.
  2. Whether each relationship is positive or negative.
  3. The size of the regression coefficients.
  4. The R-squared value of the model.
  5. Whether the regression diagnostic plots suggest that the linear regression model is reasonable.

These results allow us to identify which patient and appointment characteristics have the strongest associations with waiting time.

Limitations

There are several limitations to this analysis.

First, this is observational data. Therefore, our regression results identify associations but cannot prove that one variable causes another.

Second, there may be important factors affecting appointment waiting time that are not included in the dataset.

Third, some patients appear in the dataset multiple times, meaning that the observations may not be completely independent.

Finally, the dataset is very large. This means that even small relationships may have very small p-values. Therefore, both statistical significance and the size of the estimated relationships should be considered.

Conclusion

The purpose of this project was to investigate factors associated with medical appointment waiting times.

We created our dependent variable, WaitingDays, by calculating the number of days between the date an appointment was scheduled and the date the appointment occurred.

We then used patient age, gender, scholarship status, health conditions, SMS reminder status, appointment attendance, and appointment frequency as explanatory variables.

Through data cleaning, summary statistics, visualization, and multiple linear regression, we can determine which characteristics have the strongest relationships with appointment waiting time.

Overall, this project demonstrates how cross-sectional data and multiple linear regression can be used to analyze real-world healthcare scheduling patterns.