Precipitation and Departure Delays at JFK in 2026

Author

Enda Hughes

Published

November 12, 2026

Introduction

Is the mean daily average departure delay at JFK greater on days with measurable precipitation than on dry days during January through August 2026? Weather is a useful factor to study because travelers experience delays across entire operating days. This study compares observed precipitation groups without assuming that weather alone causes any difference. The question refines the original proposal by making the day the unit of statistical analysis. Averaging within each day avoids counting hundreds of flights exposed to the same weather as independent weather observations.

The flight source is the U.S. Department of Transportation’s Bureau of Transportation Statistics (BTS) Reporting Carrier On-Time Performance dataset (Bureau of Transportation Statistics 2026). The embedded flight table contains the JFK-only rows and nine selected columns from the eight monthly official archives. Each row represents one reported domestic flight, including canceled records. NOAA’s Daily Summaries supply precipitation at JFK station USW00094789 (NOAA National Centers for Environmental Information 2026a). Each NOAA row describes one station-day, with many weather measurements and associated flags. The analysis uses scheduled flight date, departure delay in minutes, cancellation status, and daily precipitation in inches. Departure delay and precipitation amount are quantitative; precipitation group is categorical. Flight date is the temporal matching field. The tables below report exact dimensions and exclusions. The flight-level source exceeds 500 records, while the hypothesis test uses daily averages and therefore has fewer than 500 observations.

Data analysis

I import the embedded frozen extracts, check dimensions and duplicate keys, and keep valid JFK departures. I then match each flight date to one NOAA date after checking weather quality and trace flags (NOAA National Centers for Environmental Information 2026b). NOAA reports precipitation ending at 2400 for the retained records; I match its reported date to the scheduled local departure date. Local standard time versus daylight saving time can still shift the boundary by one hour, so this is a daily weather comparison rather than an exact measure of conditions at takeoff. I use select, mutate, filter, left_join, group_by, summarise, and arrange to prepare the data (Posit Software PBC 2026a). The EDA uses summary and table, followed by descriptive group statistics (R Core Team 2026b). A box plot with individual days shows group distributions, while a time plot shows unusual days and seasonal patterns (Posit Software PBC 2026b). The time plot uses cubic interpolation as a visual guide through each group’s observed points, not as a statistical model (R Core Team 2026c).

# One frozen row per reported flight; the provenance appendix links the eight archives.
flights_raw <- read_embedded_csv(flights_embedded)
weather_raw <- read_embedded_csv(weather_embedded)
dimensions <- data.frame(
  Dataset = c("BTS JFK flight extract", "NOAA station-day extract"),
  Rows = c(nrow(flights_raw), nrow(weather_raw)),
  Columns = c(ncol(flights_raw), ncol(weather_raw))
)
kable(dimensions, caption = "Dimensions of the embedded source tables")
Dimensions of the embedded source tables
Dataset Rows Columns
BTS JFK flight extract 67736 9
NOAA station-day extract 243 300
flight_key <- c("FlightDate", "Reporting_Airline",
                "Flight_Number_Reporting_Airline", "Origin", "Dest", "CRSDepTime")
stopifnot(sum(duplicated(flights_raw[flight_key])) == 0)
stopifnot(sum(duplicated(weather_raw[c("STATION", "DATE")])) == 0)
stopifnot(all(flights_raw$Origin == "JFK"), nrow(flights_raw) >= 500)
summary(flights_raw$DepDelay)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.     NAs 
  -23.0    -6.0    -2.0    17.4    13.0  2044.0    3045 
table(Cancelled = flights_raw$Cancelled, useNA = "ifany")
Cancelled
    0     1 
64446  3290 
# NOAA attributes are measurement flag, quality flag, source flag, observation time.
# Exclude traces rather than treating a trace as zero rainfall.
attribute_parts <- strsplit(weather_raw$PRCP_ATTRIBUTES, ",", fixed = TRUE)
get_attribute <- function(i) vapply(attribute_parts, function(x) {
  if (length(x) >= i) x[i] else ""
}, character(1))
weather <- weather_raw |>
  select(STATION, DATE, PRCP, PRCP_ATTRIBUTES) |>
  mutate(date = as.Date(DATE),
         measurement_flag = get_attribute(1),
         quality_flag = get_attribute(2),
         observation_time = get_attribute(4),
         group = case_when(
           is.na(PRCP) | PRCP < 0 | quality_flag != "" ~ NA_character_,
           measurement_flag == "T" ~ NA_character_,
           PRCP > 0 ~ "Precipitation",
           PRCP == 0 ~ "Dry",
           TRUE ~ NA_character_))
flights <- flights_raw |>
  mutate(date = as.Date(substr(FlightDate, 1, 10))) |>
  filter(Cancelled == 0, !is.na(DepDelay))
joined <- flights |>
  left_join(select(weather, date, PRCP, group), by = "date")
analysis_flights <- joined |> filter(!is.na(group))
daily <- analysis_flights |>
  group_by(date, group) |>
  summarise(flights = n(), daily_delay = mean(DepDelay),
            precipitation_inches = first(PRCP), .groups = "drop") |>
  arrange(date) |>
  mutate(group = factor(group, levels = c("Dry", "Precipitation")))
audit <- data.frame(
  Stage = c("All JFK source flights", "Canceled flights excluded",
            "Noncanceled flights missing departure delay", "Flights with delay recorded",
            "Flights without eligible precipitation", "Flights retained", "Days retained",
            "NOAA trace days excluded", "NOAA missing or quality flagged days"),
  Count = c(nrow(flights_raw), sum(flights_raw$Cancelled == 1),
            sum(flights_raw$Cancelled == 0 & is.na(flights_raw$DepDelay)),
            nrow(flights), sum(is.na(joined$group)), nrow(analysis_flights), nrow(daily),
            sum(weather$measurement_flag == "T"),
            sum(is.na(weather$PRCP) | weather$quality_flag != ""))
)
kable(audit, caption = "Sequential exclusions and final sample sizes")
Sequential exclusions and final sample sizes
Stage Count
All JFK source flights 67736
Canceled flights excluded 3290
Noncanceled flights missing departure delay 0
Flights with delay recorded 64446
Flights without eligible precipitation 10420
Flights retained 54026
Days retained 203
NOAA trace days excluded 39
NOAA missing or quality flagged days 0
table(NOAA_observation_time = weather$observation_time, useNA = "ifany")
NOAA_observation_time
2400 
 243 
stopifnot(nrow(joined) == nrow(flights), !anyDuplicated(daily$date))
group_summary <- daily |>
  group_by(group) |>
  summarise(Days = n(), Flights = sum(flights),
            Mean = mean(daily_delay), Median = median(daily_delay),
            SD = sd(daily_delay), Minimum = min(daily_delay),
            Maximum = max(daily_delay), .groups = "drop")
kable(group_summary, digits = 2,
      caption = "Daily average departure delays in minutes, equally weighted by day")
Daily average departure delays in minutes, equally weighted by day
group Days Flights Mean Median SD Minimum Maximum
Dry 120 32577 13.92 8.74 17.59 -1.45 153.56
Precipitation 83 21449 24.71 20.09 21.31 -2.05 114.20
summary(daily$daily_delay)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  -2.05    6.02   12.13   18.33   22.59  153.56 
table(Group = daily$group)
Group
          Dry Precipitation 
          120            83 
# A different descriptive estimand: the mean across individual flights.
flight_summary <- analysis_flights |>
  group_by(group) |>
  summarise(Flights = n(), Flight_weighted_mean = mean(DepDelay), .groups = "drop")
kable(flight_summary, digits = 2, caption = "Flight-weighted means for context only")
Flight-weighted means for context only
group Flights Flight_weighted_mean
Dry 32577 12.99
Precipitation 21449 23.20
set.seed(101)
distribution_plot <- ggplot(daily, aes(group, daily_delay, fill = group)) +
  geom_boxplot(width = 0.48, outlier.shape = NA, alpha = 0.3) +
  geom_jitter(aes(color = group), width = 0.13, height = 0,
              alpha = 0.65, size = 1.7) +
  scale_fill_manual(values = colors) + scale_color_manual(values = colors) +
  labs(x = NULL, y = "Daily average departure delay (minutes)",
       title = "JFK departure delay by daily precipitation group") +
  theme(legend.position = "none")
print(distribution_plot)
Figure 1: Each point represents one day. The box shows the median and interquartile range.
all_daily <- flights |>
  group_by(date) |>
  summarise(daily_delay = mean(DepDelay), .groups = "drop") |>
  arrange(date)
all_daily <- data.frame(date = seq(as.Date("2026-01-01"), as.Date("2026-08-31"), by = "day")) |>
  left_join(all_daily, by = "date")
# Smooth paths are visual guides through observed group points, not fitted models.
# Monotone cubic interpolation avoids adding extra peaks between adjacent points.
smooth_series <- function(g) {
  part <- daily |> filter(group == g) |> arrange(date)
  x <- as.numeric(part$date)
  grid <- seq(min(x), max(x), length.out = 1200)
  curve <- splinefun(x, part$daily_delay, method = "monoH.FC")
  data.frame(date = as.Date(grid, origin = "1970-01-01"),
             daily_delay = curve(grid), group = g)
}
curve_data <- rbind(smooth_series("Dry"), smooth_series("Precipitation"))
time_plot <- ggplot(curve_data, aes(date, daily_delay, color = group)) +
  geom_line(linewidth = 0.9) +
  geom_point(data = daily, size = 1.9) +
  scale_color_manual(values = colors, name = "Daily weather") +
  labs(x = "Scheduled departure date in 2026", y = "Daily average departure delay (minutes)",
       title = "Daily departure delays by weather group",
       caption = "Curves bridge dates for visual guidance. Only dots are observed daily values.") +
  theme(legend.position = "bottom")
print(time_plot)
Figure 2: Dry days and precipitation days have separate colored curves and points. Curves connect the observed days in each group, including across gaps in dates; they do not estimate delay on unobserved or other-weather days. Statistical calculations use only the observed values.

Statistical analysis

Let μP\mu_P be the population mean of daily average departure delays on measurable-precipitation days and μ0\mu_0 the corresponding mean on dry days for comparable JFK operations in the study period. Each day receives equal weight. The null hypothesis is that the means are equal, and the alternative hypothesis is that the precipitation-day mean is greater:

H0:μP−μ0=0HA:μP−μ0>0.H_0: \mu_P - \mu_0 = 0 \qquad H_A: \mu_P - \mu_0 > 0.

I use a one-sided independent-samples Welch t-test at α=0.05\alpha = 0.05. The response is quantitative and the explanatory variable has two groups. Welch’s method allows unequal variances (R Core Team 2026a). Days are not paired. The diagnostics below assess group size, skew and unusual days, and lag-one correlation in the complete chronological series (R Core Team 2026c). Aggregation addresses dependence among flights within each day, but storms can affect consecutive days and seasonality can make days nonexchangeable. The t-test is therefore an approximate comparison under independent-day assumptions, rather than a causal or fully adjusted time-series analysis. The sample covers the available dates, not a random sample; the observed mean difference itself describes this period, while the p-value relies on the stated model assumptions.

wet <- daily$daily_delay[daily$group == "Precipitation"]
dry <- daily$daily_delay[daily$group == "Dry"]
stopifnot(length(wet) > 1, length(dry) > 1)
lag_one <- cor(head(all_daily$daily_delay, -1), tail(all_daily$daily_delay, -1),
               use = "complete.obs")
par(mfrow = c(1, 2))
qqnorm(dry, main = "Dry days", ylab = "Daily mean delay (minutes)")
qqline(dry, col = "#246B8E")
qqnorm(wet, main = "Precipitation days", ylab = "Daily mean delay (minutes)")
qqline(wet, col = "#B54D30")

Normal Q-Q plots of daily mean delays by group. Departures at the upper end show unusually disrupted days.
par(mfrow = c(1, 1))
data.frame(Dry_days = length(dry), Precipitation_days = length(wet),
           Lag_one_daily_correlation = lag_one)
  Dry_days Precipitation_days Lag_one_daily_correlation
1      120                 83                   0.50007

The comparison contains 120 dry days and 83 precipitation days. The largest daily mean is 153.56 minutes, so unusually disrupted days can influence the mean. The lag-one correlation is 0.500. This diagnostic does not prove independence; it is reported alongside the limitations rather than used to guarantee validity.

# The prespecified directional test uses precipitation minus dry.
test <- t.test(wet, dry, alternative = "greater", var.equal = FALSE, conf.level = 0.95)
print(test)

    Welch Two Sample t-test

data:  wet and dry
t = 3.8, df = 154, p-value = 0.0001
alternative hypothesis: true difference in means is greater than 0
95 percent confidence interval:
 6.1006    Inf
sample estimates:
mean of x mean of y 
   24.712    13.916 
# A separate two-sided interval makes the effect size easier to describe.
interval_test <- t.test(wet, dry, alternative = "two.sided", var.equal = FALSE)
effect <- mean(wet) - mean(dry)
results <- data.frame(
  Difference_minutes = effect,
  t_statistic = unname(test$statistic), df = unname(test$parameter),
  One_sided_p_value = test$p.value,
  Two_sided_95_CI_lower = interval_test$conf.int[1],
  Two_sided_95_CI_upper = interval_test$conf.int[2],
  One_sided_95_lower_bound = test$conf.int[1]
)
kable(results, digits = 4, caption = "Welch test results for precipitation minus dry")
Welch test results for precipitation minus dry
Difference_minutes t_statistic df One_sided_p_value Two_sided_95_CI_lower Two_sided_95_CI_upper One_sided_95_lower_bound
10.796 3.8049 153.93 0.0001 5.1907 16.401 6.1006

The mean daily delay is 24.71 minutes on precipitation days and 13.92 minutes on dry days. The estimated difference is 10.80 minutes (precipitation minus dry), with a two-sided 95% confidence interval from 5.19 to 16.40 minutes. This interval describes uncertainty in the mean difference, not the range of individual flight delays. The directional Welch test gives t=3.805t = 3.805, df=153.93df = 153.93, and p=0.0001021p = 0.0001021. Because the p-value is below 0.05, I reject the null hypothesis. This is a nominal result under the independent-day model. Under that model, there is statistical evidence that daily average departure delays are greater on measurable-precipitation days. The directional test corresponds to a one-sided 95% interval with lower bound 6.10 minutes and no finite upper bound; the two-sided interval above serves as an additional effect-size summary.

Sensitivity to consecutive-day dependence

The daily lag-one correlation is large enough that the nominal t-test should not stand alone. As a documented sensitivity analysis, I use boot::tsboot to resample consecutive blocks of calendar days, carrying both weather classification and delay together (Canty and Ripley 2026). Trace days and the day without a departure-delay mean remain in the calendar series to preserve spacing, but do not contribute to either group mean. I set the random seed to 101, use 4,999 replicates, and compare blocks of 7 and 14 days without selecting the length by its p-value. The bootstrap statistic is the precipitation-day mean minus the dry-day mean. The two-sided intervals use bootstrap percentiles. The approximate one-sided centered-bootstrap p-value is the fraction of centered bootstrap effects at least as large as the observed effect, with a plus-one adjustment. This is an additional method, not a replacement label for the course t-test. Block resampling accommodates local dependence but assumes the observed sequence represents a roughly stable process; seasonal change and the finite eight-month window limit that assumption.

# Keep all calendar days, including excluded weather days, in their original order.
calendar <- all_daily |>
  left_join(select(weather, date, group), by = "date") |>
  mutate(wet_flag = case_when(group == "Precipitation" ~ 1,
                             group == "Dry" ~ 0, TRUE ~ NA_real_))
block_statistic <- function(z) {
  mean(z[z[, 2] == 1 & !is.na(z[, 2]), 1], na.rm = TRUE) -
    mean(z[z[, 2] == 0 & !is.na(z[, 2]), 1], na.rm = TRUE)
}
bootstrap_input <- as.matrix(select(calendar, daily_delay, wet_flag))
set.seed(101)
boot7 <- tsboot(bootstrap_input, statistic = block_statistic,
                R = 4999, l = 7, sim = "fixed", endcorr = TRUE)
set.seed(101)
boot14 <- tsboot(bootstrap_input, statistic = block_statistic,
                 R = 4999, l = 14, sim = "fixed", endcorr = TRUE)
summarize_boot <- function(b, days) {
  values <- as.numeric(b$t)
  stopifnot(all(is.finite(values)))
  bounds <- quantile(values, c(0.025, 0.975))
  data.frame(Block_days = days, Replicates = length(values),
             Difference_minutes = effect,
             Bootstrap_SE = sd(values),
             CI_lower = unname(bounds[1]), CI_upper = unname(bounds[2]),
             Centered_one_sided_p = (1 + sum(values - effect >= effect)) /
               (length(values) + 1))
}
block_results <- rbind(summarize_boot(boot7, 7), summarize_boot(boot14, 14))
kable(block_results, digits = 4, caption = "Block-bootstrap sensitivity analysis")
Block-bootstrap sensitivity analysis
Block_days Replicates Difference_minutes Bootstrap_SE CI_lower CI_upper Centered_one_sided_p
7 4999 10.796 2.6007 5.9094 15.986 0.0002
14 4999 10.796 2.8844 5.0542 16.328 0.0004

With 7-day blocks, the 95% percentile interval is 5.91 to 15.99 minutes and the approximate centered one-sided p-value is 0.0002. With 14-day blocks, the interval is 5.05 to 16.33 minutes and the approximate p-value is 0.0004. These checks retain positive intervals at both block lengths. They do not remove seasonal confounding or establish causation.

Conclusion and future directions

For 54,026 retained flights across 203 eligible days at JFK, precipitation days had a mean daily delay that was 10.80 minutes greater than dry days. The nominal hypothesis test supports a higher precipitation-day mean under its assumptions. The block-bootstrap sensitivity results above provide an additional check on consecutive-day dependence. These results describe an association at one airport during January through August 2026 and should be treated as exploratory because the period is observational and seasonally changing. A daily precipitation total does not reveal whether precipitation occurred while a particular flight departed, and wet days can also differ in season, traffic, wind, and airline mix. Excluding cancellations means the outcome describes flights with recorded departure times, not all passenger disruption. Trace days are outside the target comparison. All 283 reported JFK flights on February 23 were canceled, leaving no daily departure-delay mean for that day. This illustrates how cancellation exclusions can omit a particularly disrupted day rather than treating its delay as zero.

Future work could match hourly weather to scheduled departure times, include cancellation rates as a separate outcome, and compare full years or additional airports. A method that accounts for serial dependence and seasonal differences would strengthen inference, but would require additional methods beyond this introductory comparison. Longer blocks and seasonal adjustment would also be useful sensitivity analyses. The source flight file exceeds 500 observations; the 203 daily observations used for inference should not be confused with that much larger flight count.

References

The dataset references link directly to the official providers. Documentation citations cover the statistical test, diagnostics, data manipulation, plotting, base R helpers, table formatting, and Quarto rendering, including functions that may not have appeared in class (R Core Team 2026b, 2026c; Posit Software PBC 2026a, 2026b, 2026c; Xie 2026).

Bureau of Transportation Statistics. 2026. “Reporting Carrier On-Time Performance 1987 to Present.” https://transtats.bts.gov/ONTIME/.
Canty, Angelo, and Brian Ripley. 2026. “Boot Tsboot Time Series Bootstrapping.” https://stat.ethz.ch/R-manual/R-devel/library/boot/html/tsboot.html.
NOAA National Centers for Environmental Information. 2026a. “Daily Summaries for JFK International Airport NY US Station USW00094789.” https://www.ncdc.noaa.gov/cdo-web/datasets/GHCND/stations/GHCND:USW00094789/detail.
NOAA National Centers for Environmental Information. 2026b. “GHCN Daily README and Quality Flag Documentation.” https://www.ncei.noaa.gov/pub/data/ghcn/daily/readme.txt.
Posit Software PBC. 2026a. “Dplyr Function Reference.” https://dplyr.tidyverse.org/reference/index.html.
Posit Software PBC. 2026b. “Ggplot2 Function Reference.” https://ggplot2.tidyverse.org/reference/index.html.
Posit Software PBC. 2026c. “Quarto Using R.” https://quarto.org/docs/computations/r.html.
R Core Team. 2026a. “Student t Test R Stats Documentation.” https://stat.ethz.ch/R-manual/R-devel/library/stats/html/t.test.html.
R Core Team. 2026b. “The R Base Package Reference Manual.” https://stat.ethz.ch/R-manual/R-devel/library/base/html/00Index.html.
R Core Team. 2026c. “The R Stats Package Reference Manual.” https://stat.ethz.ch/R-manual/R-devel/library/stats/html/00Index.html.
Xie, Yihui. 2026. “Knitr Kable Documentation.” https://cran.r-project.org/web/packages/knitr/knitr.pdf.

Embedded provenance appendix

The source CSV extracts are embedded in this QMD. These links and SHA-256 hashes document the downloads used to prepare them.

  • BTS month 1 of 2026. Archive SHA-256: 9ff58f560519dbc8fbd1b8b9fd2a172ca46d0a9e80f1df4fd48891efca41e89c.
  • BTS month 2 of 2026. Archive SHA-256: a18b6de822a63bb881b55983ef039bf02a7cac046e9624e5c1b19b470a5e05cb.
  • BTS month 3 of 2026. Archive SHA-256: c0864d5dabd522517976e29bb81f0c094d114e3e0e40cd51bc5a238ae4292b9d.
  • BTS month 4 of 2026. Archive SHA-256: 70e685ffa6042e6c851c4f1e821d774dfe7318d3eb212ac5bfc6a53ea23f3cfb.
  • BTS month 5 of 2026. Archive SHA-256: 4e7b96999440afec8c92dd23bfbc68a5852e14d9a56c3d0d366f884542ea80b3.
  • BTS month 6 of 2026. Archive SHA-256: acd9d03e223e079e54b77065015f0ce9b6de92f1f46148048600cc9813a47a6e.
  • BTS month 7 of 2026. Archive SHA-256: 61d657eb17f5b6f800ed2f05262a2223c5edf66fe7d9cfb3598c6664a34ed0b6.
  • BTS month 8 of 2026. Archive SHA-256: 203509350f574f81728ca51175a17360d0413aa990512f8e0c8f1342f5baf23a.
  • NOAA exact daily-data query. Response SHA-256: 67112cf188e700fa50a2f646acb5a63fa4d486bc509dfe61bca8520ab226d9b6.