Skip to content

Where do outpatient no-shows come from?

Real public data. Analysis of 110,521 real outpatient appointments (Kaggle, CC BY-NC-SA 4.0). The interpretation and recommendations are my own.

Key findings

  • 20.2% of 110,521 booked appointments were no-shows. The rate climbs from 4.6% for same-day bookings to 33.0% for bookings made 31 or more days ahead.
  • Bookings made 8+ days ahead are 36% of volume but 57% of all no-shows, so a single reminder or reconfirmation step reaches most of the problem.
  • SMS reminders look harmful in the raw data (27.6% vs 16.7%) only because they were never sent for bookings made 0 to 2 days ahead. Within each lead-time band, recipients no-showed 2.8 to 7.3 points less (observational).
20.2%
of booked appointments end in a no-show
7x
no-show rate of 31+ day bookings versus same-day (33.0% vs 4.6%)
57%
of all no-shows come from the 36% of bookings made 8+ days ahead

Context

An empty appointment slot is lost capacity: the clinician is paid, the room is staffed, and another patient could have been seen. In this dataset about one booked appointment in five ended in a no-show. The operational question is which no-shows are predictable and which intervention is worth testing first.

I framed the work as a short operations brief for a multi-clinic network: where do no-shows concentrate, is the SMS reminder working, and what would we change and how would we know it worked?

Data and licence. Dataset: Medical Appointment No Shows, Joni Hoppen and Aquarela Analytics, licensed CC BY-NC-SA 4.0. 110,527 appointments at public health facilities in Brazil, 29 April to 8 June 2016. Real data; the analysis is mine. Patient-level data is not republished here.

Approach

I loaded the file into SQLite and answered each question with a query. Every figure below is produced by the SQL shown beside it; the queries ran on the real database and the results are rendered into this page.

  • No-show: the patient did not attend the booked appointment.
  • Lead time: calendar days between the day the appointment was booked and the appointment day. The source has no time of day for the appointment, so days are the finest grain.
  • Cleaning: 6 of 110,527 rows removed (1 negative age; 5 booked after the appointment date), leaving 110,521.

Descriptive analysis only: grouped rates and shares, then a stratified comparison to test whether the SMS pattern survives control for lead time. I did not fit a predictive model, and nothing here is a causal estimate.

Analysis

1. Lead time is the dominant pattern

The no-show rate rises with every step in lead time, from 4.6% for same-day bookings to 33.0% for bookings made more than a month ahead (Fig. 1). Same-day bookings are partly low by construction, because the patient is already seeking care that day; the gradient across the other bands is the actionable part.

Fig. 1No-show rate rises steadily with how far ahead the slot was bookedNo-show rate by booking lead time, % of appointments
Same day4.6%
1-2 days22.7%
3-7 days25.0%
8-14 days30.5%
15-30 days32.6%
31+ days33.0%
0%10%20%30%40%50%
All appointments 20.2%

Source: Kaggle, Medical Appointment No Shows (Joni Hoppen, Aquarela Analytics), CC BY-NC-SA 4.0; n=110,521. Analysis: Rajul Raman.

Appointments booked 8 or more days ahead are 36% of bookings but 57% of no-shows (Fig. 2).

Fig. 2Long-lead bookings are a third of volume but over half of no-showsShare of bookings and of no-shows, by whether the appointment was booked 8+ days ahead, %
Share of all bookings
64.0%36.0%
Booked 0 to 7 days ahead8+ days ahead
Share of all no-shows
42.9%57.1%
Booked 0 to 7 days ahead8+ days ahead

Source: as Fig. 1. 8+ days = the 8-14, 15-30 and 31+ day bands. Analysis: Rajul Raman.

SQL for Figs. 1 and 2 (run on the real database):

WITH base AS (
  SELECT no_show, sms_received, age,
         CAST(julianday(appointment_date)
            - julianday(substr(scheduled_at, 1, 10)) AS INT) AS lead_days,
         CAST(strftime('%w', appointment_date) AS INT) AS dow
  FROM   appointments
  WHERE  age >= 0                                      -- 1 impossible age removed
    AND  appointment_date >= substr(scheduled_at, 1, 10) -- 5 booked after the visit removed
)
SELECT CASE WHEN lead_days = 0  THEN 'Same day'
            WHEN lead_days <= 2 THEN '1-2 days'
            WHEN lead_days <= 7 THEN '3-7 days'
            WHEN lead_days <= 14 THEN '8-14 days'
            WHEN lead_days <= 30 THEN '15-30 days'
            ELSE '31+ days' END
         AS lead_band,
       COUNT(*) AS appointments,
       SUM(no_show) AS no_shows,
       ROUND(100.0 * AVG(no_show), 1) AS no_show_pct,
       ROUND(100.0 * COUNT(*) / (SELECT COUNT(*) FROM base), 1) AS pct_of_bookings,
       ROUND(100.0 * SUM(no_show) / (SELECT SUM(no_show) FROM base), 1) AS pct_of_no_shows
FROM   base
GROUP  BY 1
ORDER  BY MIN(lead_days);

Result:

lead_bandappointmentsno_showsno_show_pctpct_of_bookingspct_of_no_shows
Same day38,5621,7924.634.98.0
1-2 days11,9382,71522.710.812.2
3-7 days20,2475,05725.018.322.7
8-14 days12,0253,66430.510.916.4
15-30 days17,3715,66132.615.725.4
31+ days10,3783,42533.09.415.3

Explore the numbers

Switch the breakdown below: hover a bar for the exact rate and volume. Every value comes from the SQL above and below, run on the real database.

ExploreInteractive chartLead time, age and SMS breakdowns of the real data.

2. The SMS reminder: a confounder, then the real signal

Taken at face value the SMS looks counter-productive: patients who received one no-showed more (27.6% against 16.7%). The reason is how the reminder was deployed. It was only ever sent for bookings made three or more days ahead; none of the 50,500 bookings made 0 to 2 days ahead received one. Those are the safest bookings, and they all sit in the no-SMS group, which drags its average down.

WITH base AS (
  SELECT no_show, sms_received, age,
         CAST(julianday(appointment_date)
            - julianday(substr(scheduled_at, 1, 10)) AS INT) AS lead_days,
         CAST(strftime('%w', appointment_date) AS INT) AS dow
  FROM   appointments
  WHERE  age >= 0                                      -- 1 impossible age removed
    AND  appointment_date >= substr(scheduled_at, 1, 10) -- 5 booked after the visit removed
)
SELECT CASE WHEN lead_days = 0  THEN 'Same day'
            WHEN lead_days <= 2 THEN '1-2 days'
            WHEN lead_days <= 7 THEN '3-7 days'
            WHEN lead_days <= 14 THEN '8-14 days'
            WHEN lead_days <= 30 THEN '15-30 days'
            ELSE '31+ days' END
         AS lead_band,
       COUNT(*) AS appointments,
       SUM(sms_received) AS sms_sent
FROM   base
GROUP  BY 1
ORDER  BY MIN(lead_days);
lead_bandappointmentssms_sent
Same day38,5620
1-2 days11,9380
3-7 days20,24711,548
8-14 days12,0257,004
15-30 days17,37110,520
31+ days10,3786,410

Comparing like with like (same lead-time band) reverses the sign: in every band, recipients no-showed 2.8 to 7.3 percentage points less (Fig. 3). Because SMS was not randomly assigned this is an association, not proof the message works; it is a hypothesis to test properly (see Recommendations).

Fig. 3Like for like, patients who received an SMS missed fewer appointmentsNo-show rate with and without an SMS reminder, bookings made 3+ days ahead, %
No SMS sentSMS sent
All (crude)16.7% vs 27.6%
3-7 d26.6% vs 23.8%
8-14 d33.8% vs 28.1%
15-30 d36.9% vs 29.8%
31+ d37.5% vs 30.2%
0%10%20%30%40%50%

Source: as Fig. 1. SMS was never sent for bookings made 0 to 2 days ahead, so the crude comparison mixes low-risk no-SMS bookings into the control group. Analysis: Rajul Raman.

WITH base AS (
  SELECT no_show, sms_received, age,
         CAST(julianday(appointment_date)
            - julianday(substr(scheduled_at, 1, 10)) AS INT) AS lead_days,
         CAST(strftime('%w', appointment_date) AS INT) AS dow
  FROM   appointments
  WHERE  age >= 0                                      -- 1 impossible age removed
    AND  appointment_date >= substr(scheduled_at, 1, 10) -- 5 booked after the visit removed
)
SELECT CASE WHEN lead_days <= 7  THEN '3-7 days'
            WHEN lead_days <= 14 THEN '8-14 days'
            WHEN lead_days <= 30 THEN '15-30 days'
            ELSE '31+ days' END AS lead_band,
       sms_received,
       COUNT(*) AS appointments,
       ROUND(100.0 * AVG(no_show), 1) AS no_show_pct
FROM   base
WHERE  lead_days >= 3          -- the only bands where SMS was ever sent
GROUP  BY 1, 2
ORDER  BY MIN(lead_days), 2;
lead_bandsms_receivedappointmentsno_show_pct
3-7 days08,69926.6
3-7 days111,54823.8
8-14 days05,02133.8
8-14 days17,00428.1
15-30 days06,85136.9
15-30 days110,52029.8
31+ days03,96837.5
31+ days16,41030.2

3. Who, and when

No-show rates are highest for 13 to 24 year-olds (25.6%) and lowest for over-70s (15.6%) (Fig. 4). Monday to Friday sit between 19.3% and 21.2%: with this many appointments the differences are real but too small to justify weekday-specific rules.

Fig. 4Young adults miss most; over-70s leastNo-show rate by patient age band, %
0-1220.5%
13-2425.6%
25-3922.5%
40-5419.4%
55-6915.8%
70+15.6%
0%10%20%30%40%
All 20.2%

Source: as Fig. 1. Analysis: Rajul Raman.

WITH base AS (
  SELECT no_show, sms_received, age,
         CAST(julianday(appointment_date)
            - julianday(substr(scheduled_at, 1, 10)) AS INT) AS lead_days,
         CAST(strftime('%w', appointment_date) AS INT) AS dow
  FROM   appointments
  WHERE  age >= 0                                      -- 1 impossible age removed
    AND  appointment_date >= substr(scheduled_at, 1, 10) -- 5 booked after the visit removed
)
SELECT CASE WHEN age <= 12 THEN '0-12'
            WHEN age <= 24 THEN '13-24'
            WHEN age <= 39 THEN '25-39'
            WHEN age <= 54 THEN '40-54'
            WHEN age <= 69 THEN '55-69'
            ELSE '70+' END
         AS age_band,
       COUNT(*) AS appointments,
       ROUND(100.0 * AVG(no_show), 1) AS no_show_pct
FROM   base
GROUP  BY 1
ORDER  BY MIN(age);
age_bandappointmentsno_show_pct
0-1221,03520.5
13-2416,22925.6
25-3922,01322.5
40-5422,31619.4
55-6919,63215.8
70+9,29615.6
Fig. 5The day of the week barely mattersNo-show rate by appointment weekday, %
Mon20.6%
Tue20.1%
Wed19.7%
Thu19.3%
Fri21.2%
Sat (n=39)23.1%
0%10%20%30%40%
All 20.2%

Source: as Fig. 1. Saturday has only 39 appointments and should not be read. Analysis: Rajul Raman.

WITH base AS (
  SELECT no_show, sms_received, age,
         CAST(julianday(appointment_date)
            - julianday(substr(scheduled_at, 1, 10)) AS INT) AS lead_days,
         CAST(strftime('%w', appointment_date) AS INT) AS dow
  FROM   appointments
  WHERE  age >= 0                                      -- 1 impossible age removed
    AND  appointment_date >= substr(scheduled_at, 1, 10) -- 5 booked after the visit removed
)
SELECT CASE dow WHEN 1 THEN 'Mon' WHEN 2 THEN 'Tue' WHEN 3 THEN 'Wed'
                WHEN 4 THEN 'Thu' WHEN 5 THEN 'Fri' ELSE 'Sat' END AS weekday,
       COUNT(*) AS appointments,
       ROUND(100.0 * AVG(no_show), 1) AS no_show_pct
FROM   base
GROUP  BY dow
ORDER  BY dow;
weekdayappointmentsno_show_pct
Mon22,71320.6
Tue25,63820.1
Wed25,86619.7
Thu17,24619.3
Fri19,01921.2
Sat3923.1

Combining the two strongest signals isolates a high-risk segment: appointments booked 15 or more days ahead for patients aged 13 to 39 number 9,642 (8.7% of volume) and no-show at 38.8%, nearly double the average. This subgroup was found by exploring the data, so it needs validating on a later period before it is acted on.

WITH base AS (
  SELECT no_show, sms_received, age,
         CAST(julianday(appointment_date)
            - julianday(substr(scheduled_at, 1, 10)) AS INT) AS lead_days,
         CAST(strftime('%w', appointment_date) AS INT) AS dow
  FROM   appointments
  WHERE  age >= 0                                      -- 1 impossible age removed
    AND  appointment_date >= substr(scheduled_at, 1, 10) -- 5 booked after the visit removed
)
SELECT COUNT(*) AS appointments,
       SUM(no_show) AS no_shows,
       ROUND(100.0 * AVG(no_show), 1) AS no_show_pct,
       ROUND(100.0 * COUNT(*) / (SELECT COUNT(*) FROM base), 1) AS pct_of_bookings
FROM   base
WHERE  lead_days >= 15 AND age BETWEEN 13 AND 39;
appointmentsno_showsno_show_pctpct_of_bookings
9,6423,74038.88.7

Recommendations

I would sequence the work by size of prize and strength of evidence: start with the intervention aimed at the concentration finding, run the SMS question as a proper experiment, and hold the subgroup idea until it is validated.

RecommendationEvidence in the dataHow to test it
1. Reconfirm long-lead bookings. A confirmation message at 48 hours for anything booked 8+ days ahead; unconfirmed slots released to a waitlist at 24 hours.8+ day bookings: 39,774 appointments, 12,750 no-shows (57% of the total from 36% of bookings).Pilot in one clinic for four weeks against a matched clinic. Primary metric: no-show rate. Guardrails: waitlist fill rate, patient complaints.
2. Extend SMS to the 1 to 2 day band. The one band never messaged: 11,938 appointments at 22.7%.Where SMS was sent, recipients no-showed 2.8 to 7.3 points less within each lead-time band.Randomised 50/50 holdout, because the existing SMS effect is observational. Agree the minimum worthwhile effect against message cost first.
3. Offer a flexible slot to the highest-risk group. Booked 15+ days ahead and aged 13 to 39: a reschedule link and a nearer slot.9,642 appointments (8.7% of volume) at 38.8%, against 20.2% overall. Exploratory subgroup.Validate the segment on a later period first; then track reschedule take-up and the no-show rate of rescheduled slots.
4. Do not build weekday rules.Monday to Friday range is 19.3% to 21.2%: real but too small to act on.None needed; revisit only if volumes change.

Impact and limitations

What the data supports. The lead-time gradient, its concentration (Fig. 2) and the stratified SMS pattern (Fig. 3) are measured in 110,521 real appointments.

Scenario, not a measurement. If reconfirmation recovered 20% of the 12,750 no-shows among 8+ day bookings, about 2,550 slots would be saved per 110,521 bookings and the overall no-show rate would fall from 20.2% to 17.9%. The 20% recovery rate is my assumption, and it also assumes the waitlist fills the freed slots. Recommendation 1 is the experiment that would replace it with a measured number.

Limitations

  • The dataset has no cancellation field, so bookings cancelled early are absent and the lead-time gradient is partly a selection effect. Same-day bookings are low partly because the patient is already there.
  • Data are from 2016 and one health system; re-check every pattern on the clinic's own bookings before acting.
  • The SMS comparison is observational and stratified on lead time only; other confounders may remain. No confidence intervals were computed.
  • The high-risk segment was found by exploration, not pre-specified. I also looked at prior no-show history; in this six-week window it was not decision-grade, so I excluded it.