DATA 607 Week 5A – Airline Delays Approach

Author

Patricio Romero

Published

October 1, 2026

Introduction

Airline performance can be evaluated by comparing the number and percentage of flights that arrive on time or experience delays. However, comparisons based only on flight counts may be misleading because airlines do not operate the same number of flights in every city.

The objective of this assignment is to organize, tidy, and analyze flight-arrival data for Alaska Airlines and AM West across five destinations. The final analysis will compare the overall delay percentages of the two airlines and their delay percentages within each destination.

PostgreSQL was used to store the original observations in their supplied wide format. R was used to retrieve the data, populate the intentionally missing airline names, transform the data from wide to long format, calculate percentages, and present the findings.

Data Source

The original dataset was provided by the instructor as an image as part of the course assignment Tidying and Transforming Data. Because the source was not provided in a machine-readable format, I manually recreated the table as airline_delays.csv, preserving the original structure, flight counts, and empty airline cells.

The recreated CSV file was then imported into PostgreSQL for storage and retrieved into R for reproducible data preparation and analysis.

The airlines are:

  • Alaska Airlines
  • AM West

The destinations are:

  • Los Angeles
  • Phoenix
  • San Diego
  • San Francisco
  • Seattle

The recreated source dataset contains four rows representing on-time and delayed flights for the two airlines. As in the supplied source table, the airline name is intentionally blank in two rows.

Data Dictionary

Variable Description
airline Name of the airline
status Flight-arrival category: on time or delayed
los_angeles Number of flights for Los Angeles
phoenix Number of flights for Phoenix
san_diego Number of flights for San Diego
san_francisco Number of flights for San Francisco
seattle Number of flights for Seattle

Database Design

The recreated source data were saved as airline_delays.csv. I used pgAdmin 4 to create the PostgreSQL database data607_week5a and the table public.airline_delays_raw.

The PostgreSQL table preserves the original wide-format structure of the source data. A generated identifier named record_id was added to preserve the original row order. The remaining columns contain the airline name, flight status, and flight counts for each of the five destinations.

The two empty airline cells from the original source were preserved as missing values when the CSV file was imported into PostgreSQL. These values were intentionally not populated in the database because the assignment requires reproducible code to populate the missing data during the R data-preparation process.

PostgreSQL Table Creation

The database and table were created in PostgreSQL using pgAdmin 4. The SQL definition used to create the table is included below to document the database structure used for this assignment.

CREATE TABLE public.airline_delays_raw (
    record_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    airline VARCHAR(50),
    status VARCHAR(20) NOT NULL,
    los_angeles INTEGER NOT NULL,
    phoenix INTEGER NOT NULL,
    san_diego INTEGER NOT NULL,
    san_francisco INTEGER NOT NULL,
    seattle INTEGER NOT NULL
);

The airline column permits missing values so that the empty cells in the original source can be preserved. The destination columns store the original flight counts without modification.

Planned Approach

The analysis will follow these steps:

  1. Retrieve the original wide-format records from PostgreSQL.
  2. Confirm that the table contains four records and two intentionally missing airline values.
  3. Populate the missing airline names using the preceding valid airline name.
  4. Transform the five destination columns from wide format into long format.
  5. Create one row for each airline, flight status, and destination combination.
  6. Validate the transformed data, expected airlines, destinations, statuses, and flight counts.
  7. Calculate the number of on-time flights, delayed flights, and total flights for each airline.
  8. Calculate the overall delay percentage for each airline.
  9. Calculate delay percentages for each airline within each of the five destinations.
  10. Present the comparisons in tables and charts.
  11. Describe the difference between the overall and city-level comparisons.
  12. Explain how different flight distributions across cities produce the discrepancy.
  13. Publish the reproducible report on RPubs and place the project files in a public GitHub repository.

The delay percentage will be calculated as:

\[\text{Delay Percentage} = \frac{\text{Delayed Flights}} {\text{On-Time Flights} + \text{Delayed Flights}} \times 100\]

Data Retrieval from PostgreSQL

The required packages are loaded before connecting to PostgreSQL.

library(DBI)
library(RPostgres)
library(dplyr)
library(tidyr)
library(ggplot2)
library(knitr)

The database password is retrieved from the local .Renviron file and is not stored in this document or published on GitHub.

con <- dbConnect(
  RPostgres::Postgres(),
  host = "127.0.0.1",
  port = 55433,
  dbname = "data607_week5a",
  user = "postgres",
  password = Sys.getenv("DATA607_DB_PASSWORD")
)

The original wide-format records are retrieved directly from PostgreSQL. The missing airline values remain unchanged during this retrieval step.

airline_delays_raw <- dbGetQuery(
  con,
  "
  SELECT
    record_id,
    airline,
    status,
    los_angeles,
    phoenix,
    san_diego,
    san_francisco,
    seattle
  FROM public.airline_delays_raw
  ORDER BY record_id
  "
)

knitr::kable(
  airline_delays_raw,
  col.names = c(
    "Record ID",
    "Airline",
    "Status",
    "Los Angeles",
    "Phoenix",
    "San Diego",
    "San Francisco",
    "Seattle"
  ),
  align = c("r", "l", "l", "r", "r", "r", "r", "r"),
  caption = "Original wide-format airline-delay data"
)
Original wide-format airline-delay data
Record ID Airline Status Los Angeles Phoenix San Diego San Francisco Seattle
1 ALASKA on time 497 221 212 503 1841
2 NA delayed 62 12 20 102 305
3 AM WEST on time 694 4840 383 320 201
4 NA delayed 117 415 65 129 61

Initial Database Validation

Before beginning the transformation and analysis in R, the PostgreSQL table was inspected. The initial validation confirmed:

  • 4 total records
  • 2 populated airline cells
  • 2 intentionally missing airline cells
  • Complete numeric counts for all five destinations

No missing airline values were filled and no delay percentages were calculated during this initial database-validation stage.

validation_summary <- data.frame(
  validation_check = c(
    "Total records",
    "Populated airline cells",
    "Missing airline cells"
  ),
  result = c(
    nrow(airline_delays_raw),
    sum(!is.na(airline_delays_raw$airline)),
    sum(is.na(airline_delays_raw$airline))
  )
)

knitr::kable(
  validation_summary,
  col.names = c("Validation Check", "Result"),
  align = c("l", "c"),
  caption = "Validation of the Original PostgreSQL Records"
)
Validation of the Original PostgreSQL Records
Validation Check Result
Total records 4
Populated airline cells 2
Missing airline cells 2

Populating Missing Airline Names

The original source data contain two intentionally missing airline names. Each missing value belongs to the same airline identified in the preceding row.

To populate these missing values reproducibly, the airline names are filled downward using the preceding valid airline name. The original flight counts and flight-status values are not modified.

airline_delays_filled <- airline_delays_raw %>%
  tidyr::fill(airline, .direction = "down")

knitr::kable(
  airline_delays_filled,
  col.names = c(
    "Record ID",
    "Airline",
    "Status",
    "Los Angeles",
    "Phoenix",
    "San Diego",
    "San Francisco",
    "Seattle"
  ),
  align = c("r", "l", "l", "r", "r", "r", "r", "r"),
  caption = "Wide-format data after populating missing airline names"
)
Wide-format data after populating missing airline names
Record ID Airline Status Los Angeles Phoenix San Diego San Francisco Seattle
1 ALASKA on time 497 221 212 503 1841
2 ALASKA delayed 62 12 20 102 305
3 AM WEST on time 694 4840 383 320 201
4 AM WEST delayed 117 415 65 129 61

Transforming the Data from Wide to Long Format

The original dataset stores the five destinations in separate columns. To create a tidy structure for analysis, these destination columns are transformed from wide format into long format.

The transformation creates two new variables: destination, which identifies the city, and flight_count, which contains the corresponding number of flights. The airline and flight-status variables are preserved.

airline_delays_long <- airline_delays_filled %>%
  pivot_longer(
    cols = c(
      los_angeles,
      phoenix,
      san_diego,
      san_francisco,
      seattle
    ),
    names_to = "destination",
    values_to = "flight_count"
  ) %>%
  mutate(
    destination = recode(
      destination,
      los_angeles = "Los Angeles",
      phoenix = "Phoenix",
      san_diego = "San Diego",
      san_francisco = "San Francisco",
      seattle = "Seattle"
    )
  )

knitr::kable(
  airline_delays_long,
  col.names = c(
    "Record ID",
    "Airline",
    "Status",
    "Destination",
    "Flight Count"
  ),
  align = c("r", "l", "l", "l", "r"),
  caption = "Airline-delay data transformed from wide to long format"
)
Airline-delay data transformed from wide to long format
Record ID Airline Status Destination Flight Count
1 ALASKA on time Los Angeles 497
1 ALASKA on time Phoenix 221
1 ALASKA on time San Diego 212
1 ALASKA on time San Francisco 503
1 ALASKA on time Seattle 1841
2 ALASKA delayed Los Angeles 62
2 ALASKA delayed Phoenix 12
2 ALASKA delayed San Diego 20
2 ALASKA delayed San Francisco 102
2 ALASKA delayed Seattle 305
3 AM WEST on time Los Angeles 694
3 AM WEST on time Phoenix 4840
3 AM WEST on time San Diego 383
3 AM WEST on time San Francisco 320
3 AM WEST on time Seattle 201
4 AM WEST delayed Los Angeles 117
4 AM WEST delayed Phoenix 415
4 AM WEST delayed San Diego 65
4 AM WEST delayed San Francisco 129
4 AM WEST delayed Seattle 61

Validation of the Long-Format Data

After the wide-to-long transformation, each row represents one airline, one flight status, and one destination combination.

Because the dataset contains two airlines, two flight statuses, and five destinations, the transformed dataset is expected to contain 20 rows. The transformed data are validated below to confirm the expected structure and to ensure that no flight counts are missing.

long_format_validation <- data.frame(
  validation_check = c(
    "Total long-format rows",
    "Number of airlines",
    "Number of flight statuses",
    "Number of destinations",
    "Missing flight counts"
  ),
  expected = c(20, 2, 2, 5, 0),
  actual = c(
    nrow(airline_delays_long),
    n_distinct(airline_delays_long$airline),
    n_distinct(airline_delays_long$status),
    n_distinct(airline_delays_long$destination),
    sum(is.na(airline_delays_long$flight_count))
  )
)

long_format_validation <- long_format_validation %>%
  mutate(
    result = ifelse(expected == actual, "PASS", "CHECK")
  )

knitr::kable(
  long_format_validation,
  col.names = c(
    "Validation Check",
    "Expected",
    "Actual",
    "Result"
  ),
  align = c("l", "c", "c", "c"),
  caption = "Validation of the Long-Format Airline Data"
)
Validation of the Long-Format Airline Data
Validation Check Expected Actual Result
Total long-format rows 20 20 PASS
Number of airlines 2 2 PASS
Number of flight statuses 2 2 PASS
Number of destinations 5 5 PASS
Missing flight counts 0 0 PASS

Count Analysis by Airline

Before calculating percentages, the number of on-time flights, delayed flights, and total flights are summarized for each airline.

The total number of flights is calculated by adding all on-time and delayed flights across the five destinations. This count analysis provides the basis for calculating and comparing the overall delay percentages of the two airlines.

airline_count_summary <- airline_delays_long %>%
  group_by(airline) %>%
  summarise(
    on_time_flights = sum(
      flight_count[tolower(status) == "on time"]
    ),
    delayed_flights = sum(
      flight_count[tolower(status) == "delayed"]
    ),
    total_flights = sum(flight_count),
    .groups = "drop"
  )

knitr::kable(
  airline_count_summary,
  col.names = c(
    "Airline",
    "On-Time Flights",
    "Delayed Flights",
    "Total Flights"
  ),
  align = c("l", "r", "r", "r"),
  caption = "Flight Count Analysis by Airline"
)
Flight Count Analysis by Airline
Airline On-Time Flights Delayed Flights Total Flights
ALASKA 3274 501 3775
AM WEST 6438 787 7225

Overall Delay Percentage by Airline

Flight counts alone are not sufficient for comparing the two airlines because they operated different numbers of flights. Therefore, the overall delay percentage is calculated for each airline.

The delay percentage is calculated by dividing the number of delayed flights by the total number of flights and multiplying by 100.

overall_delay_summary <- airline_count_summary %>%
  mutate(
    delay_percentage = (delayed_flights / total_flights) * 100
  )

knitr::kable(
  overall_delay_summary,
  digits = 2,
  col.names = c(
    "Airline",
    "On-Time Flights",
    "Delayed Flights",
    "Total Flights",
    "Delay Percentage (%)"
  ),
  align = c("l", "r", "r", "r", "r"),
  caption = "Overall Delay Percentage by Airline"
)
Overall Delay Percentage by Airline
Airline On-Time Flights Delayed Flights Total Flights Delay Percentage (%)
ALASKA 3274 501 3775 13.27
AM WEST 6438 787 7225 10.89

Overall Comparison

Alaska had 501 delayed flights out of 3,775 total flights, resulting in an overall delay percentage of approximately 13.27%. AM West had 787 delayed flights out of 7,225 total flights, resulting in an overall delay percentage of approximately 10.89%.

Although AM West had more delayed flights in absolute numbers, it also operated substantially more flights. When the airlines are compared using percentages rather than counts, AM West had the lower overall delay rate. Therefore, AM West had better overall performance based on the delay percentage.

Delay Percentage by Destination

The overall comparison does not show how the airlines performed within individual destinations. To examine this difference, the flight counts are grouped by airline and destination.

For each airline and destination, the total number of flights, delayed flights, and delay percentage are calculated. This allows the two airlines to be compared separately across Los Angeles, Phoenix, San Diego, San Francisco, and Seattle.

city_delay_summary <- airline_delays_long %>%
  group_by(airline, destination) %>%
  summarise(
    total_flights = sum(flight_count),
    delayed_flights = sum(
      flight_count[tolower(status) == "delayed"]
    ),
    .groups = "drop"
  ) %>%
  mutate(
    delay_percentage = (delayed_flights / total_flights) * 100
  ) %>%
  arrange(destination, airline)

knitr::kable(
  city_delay_summary,
  digits = 2,
  col.names = c(
    "Airline",
    "Destination",
    "Total Flights",
    "Delayed Flights",
    "Delay Percentage (%)"
  ),
  align = c("l", "l", "r", "r", "r"),
  caption = "Delay Percentages by Airline and Destination"
)
Delay Percentages by Airline and Destination
Airline Destination Total Flights Delayed Flights Delay Percentage (%)
ALASKA Los Angeles 559 62 11.09
AM WEST Los Angeles 811 117 14.43
ALASKA Phoenix 233 12 5.15
AM WEST Phoenix 5255 415 7.90
ALASKA San Diego 232 20 8.62
AM WEST San Diego 448 65 14.51
ALASKA San Francisco 605 102 16.86
AM WEST San Francisco 449 129 28.73
ALASKA Seattle 2146 305 14.21
AM WEST Seattle 262 61 23.28

Visual Comparison of Airline Delay Percentages

The percentage comparisons are also presented graphically to make the differences between the two airlines easier to identify.

The first chart compares the overall delay percentage of Alaska and AM West. The second chart compares their delay percentages within each of the five destinations.

ggplot(
  overall_delay_summary,
  aes(
    x = airline,
    y = delay_percentage,
    fill = airline
  )
) +
  geom_col(width = 0.6) +
  geom_text(
    aes(label = paste0(round(delay_percentage, 2), "%")),
    vjust = -0.4
  ) +
  labs(
    title = "Overall Delay Percentage by Airline",
    x = "Airline",
    y = "Delay Percentage (%)"
  ) +
  theme_minimal() +
  theme(
    legend.position = "none"
  )

Delay Percentage by Destination

The following chart compares the delay percentages of Alaska and AM West within each of the five destinations.

ggplot(
  city_delay_summary,
  aes(
    x = destination,
    y = delay_percentage,
    fill = airline
  )
) +
  geom_col(
    position = "dodge",
    width = 0.7
  ) +
  geom_text(
    aes(
      label = paste0(round(delay_percentage, 2), "%")
    ),
    position = position_dodge(width = 0.7),
    vjust = -0.3,
    size = 3
  ) +
  labs(
    title = "Delay Percentage by Airline and Destination",
    x = "Destination",
    y = "Delay Percentage (%)",
    fill = "Airline"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(
      angle = 45,
      hjust = 1
    )
  )

Overall vs. City-Level Comparison

The overall results show that Alaska has a higher delay percentage than AM West. However, the overall percentages do not necessarily represent the pattern observed within each individual destination.

To compare the airlines city by city, the following table identifies the airline with the lower delay percentage within each destination.

city_comparison <- city_delay_summary %>%
  group_by(destination) %>%
  arrange(delay_percentage, .by_group = TRUE) %>%
  mutate(
    city_rank = row_number()
  ) %>%
  ungroup() %>%
  filter(city_rank == 1) %>%
  select(
    destination,
    airline,
    delay_percentage
  )

knitr::kable(
  city_comparison,
  digits = 2,
  col.names = c(
    "Destination",
    "Airline with Lower Delay Percentage",
    "Delay Percentage (%)"
  ),
  align = c("l", "l", "r"),
  caption = "Airline with the Lower Delay Percentage in Each Destination"
)
Airline with the Lower Delay Percentage in Each Destination
Destination Airline with Lower Delay Percentage Delay Percentage (%)
Los Angeles ALASKA 11.09
Phoenix ALASKA 5.15
San Diego ALASKA 8.62
San Francisco ALASKA 16.86
Seattle ALASKA 14.21

City-Level Comparison

The city-level comparison shows a different pattern from the overall comparison. Overall, AM West has the lower delay percentage, at approximately 10.89%, compared with 13.27% for Alaska.

However, when the airlines are compared separately within each destination, Alaska has the lower delay percentage in all five cities. Alaska’s delay percentages are approximately 11.09% in Los Angeles, 5.15% in Phoenix, 8.62% in San Diego, 16.86% in San Francisco, and 14.21% in Seattle.

Therefore, the overall comparison suggests that AM West performs better, while the city-by-city comparison shows that Alaska performs better within every destination. This demonstrates that the overall percentages alone can give a different impression of airline performance.

Explaining the Overall and City-Level Discrepancy

The difference between the overall and city-level results can occur because the two airlines do not operate the same proportion of their flights in each destination.

To examine this effect, the following analysis calculates the number and percentage of each airline’s total flights that are associated with each destination.

flight_distribution <- city_delay_summary %>%
  group_by(airline) %>%
  mutate(
    airline_total_flights = sum(total_flights),
    flight_share_percentage =
      (total_flights / airline_total_flights) * 100
  ) %>%
  ungroup() %>%
  select(
    airline,
    destination,
    total_flights,
    flight_share_percentage,
    delay_percentage
  ) %>%
  arrange(destination, airline)

knitr::kable(
  flight_distribution,
  digits = 2,
  col.names = c(
    "Airline",
    "Destination",
    "Total Flights",
    "Share of Airline Flights (%)",
    "Delay Percentage (%)"
  ),
  align = c("l", "l", "r", "r", "r"),
  caption = "Distribution of Flights and Delay Percentages by Destination"
)
Distribution of Flights and Delay Percentages by Destination
Airline Destination Total Flights Share of Airline Flights (%) Delay Percentage (%)
ALASKA Los Angeles 559 14.81 11.09
AM WEST Los Angeles 811 11.22 14.43
ALASKA Phoenix 233 6.17 5.15
AM WEST Phoenix 5255 72.73 7.90
ALASKA San Diego 232 6.15 8.62
AM WEST San Diego 448 6.20 14.51
ALASKA San Francisco 605 16.03 16.86
AM WEST San Francisco 449 6.21 28.73
ALASKA Seattle 2146 56.85 14.21
AM WEST Seattle 262 3.63 23.28

Flight Distribution Comparison

The following table compares how the flights of each airline are distributed across the five destinations. This information helps explain why the aggregated overall percentages can differ from the city-level comparisons.

flight_distribution_comparison <- flight_distribution %>%
  select(
    airline,
    destination,
    flight_share_percentage
  ) %>%
  pivot_wider(
    names_from = airline,
    values_from = flight_share_percentage
  )

knitr::kable(
  flight_distribution_comparison,
  digits = 2,
  align = c("l", "r", "r"),
  caption = "Percentage Distribution of Each Airline's Flights by Destination"
)
Percentage Distribution of Each Airline’s Flights by Destination
destination ALASKA AM WEST
Los Angeles 14.81 11.22
Phoenix 6.17 72.73
San Diego 6.15 6.20
San Francisco 16.03 6.21
Seattle 56.85 3.63

Explanation of the Discrepancy

The overall and city-level comparisons produce different conclusions because the airlines do not have the same distribution of flights across the five destinations.

The overall delay percentage is a weighted result. Destinations with more flights have a greater effect on an airline’s overall percentage than destinations with fewer flights. Therefore, the overall percentage is not simply the average of the five city-level delay percentages.

In this dataset, Alaska has a lower delay percentage than AM West within each of the five destinations. However, the airlines operate different numbers and proportions of flights across those destinations. These different weights change the aggregated results, causing AM West to have the lower overall delay percentage even though Alaska has the lower delay percentage in every individual city.

This reversal between the aggregated comparison and the comparisons within individual groups is an example of Simpson’s paradox. It demonstrates why both overall percentages and group-level percentages should be examined before drawing conclusions from aggregated data.

Conclusion

This analysis transformed the original airline-delay data from a wide, partially incomplete format into a complete and tidy long-format dataset.

The original missing airline names were populated reproducibly in R, and the five destination columns were transformed into a single destination variable. Count analysis and percentage calculations were then used to compare Alaska and AM West.

Overall, Alaska had 501 delayed flights out of 3,775 total flights, for a delay percentage of approximately 13.27%. AM West had 787 delayed flights out of 7,225 total flights, for a delay percentage of approximately 10.89%.

Although AM West had the lower overall delay percentage, Alaska had the lower delay percentage within each of the five destinations. The difference is explained by the unequal distribution of flights across destinations, which gives different weights to the city-level results when the overall percentages are calculated.

The analysis demonstrates the importance of examining both aggregated and group-level percentages when comparing performance.

Database Connection Cleanup

The PostgreSQL connection is closed after the required records have been retrieved and validated.

dbDisconnect(con)

AI Use

ChatGPT was used to help interpret the assignment requirements, organize the planned approach, improve the English writing, design the PostgreSQL table, and provide coding guidance. I recreated and imported the airline data, ran and reviewed the code, validated the database records, and will independently review and confirm the conclusions of the final analysis.