Airline Delay Intelligence Dashboard

Wrangle real U.S. flight data and build an interactive Tableau dashboard.

Introduction

30 Second Summary

Every traveler knows the frustration of a delayed flight. Behind the scenes, airlines and government agencies track millions of data points about exactly why delays happen, who causes them, and when they peak.

In this project, you will build an end-to-end analytics pipeline that takes real U.S. government flight performance data, cleans and transforms it in Python, and visualizes it as an interactive Tableau dashboard published to the web.

What You'll Build

An interactive airline operations intelligence dashboard analyzing real U.S. domestic flight data from the Bureau of Transportation Statistics that answers the question: "Which airlines and airports have the worst delays, and why?"

By the end of this project, you'll have:

  • A Python data pipeline that cleans raw BTS flight data and derives operational KPIs like on-time percentage and average delay minutes.
  • A multi-view Tableau dashboard with carrier rankings, delay cause breakdowns, and time-of-day heatmaps.
  • A published portfolio piece on Tableau Public with the "Hire Me" button enabled on your profile.
  • Secret Mission: Add a seasonal trend analysis showing how each carrier's on-time performance changes month over month.

Are there any prerequisites?

This project assumes you have Python and SQL experience. You also need Cursor installed, Tableau Desktop Public Edition installed, and a Tableau Public account created.

Set Up Your Project and Download Flight Data

Every airline operations dashboard starts with real data. The U.S. Bureau of Transportation Statistics publishes monthly performance records for every domestic flight operated by major carriers. This is the same data that news outlets cite when reporting on the worst airlines for delays.

In this step, you will set up a Python project, download one month of real flight data from the BTS TranStats database, and confirm it loads correctly. This gives you the raw material for the cleaning and visualization work ahead.

In this step, get ready to:
  • Create a project directory with a Python virtual environment and install pandas.
  • Download one month of real U.S. flight performance data from BTS TranStats.
  • Verify the data loads and contains the expected columns.
Create your project directory and install pandas

You need a dedicated folder for your pipeline script, raw data, and exported outputs. A virtual environment keeps your Python packages isolated from other projects on your machine.

  • Open Cursor's integrated terminal by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir flight-delay-dashboard
cd flight-delay-dashboard
  • Create the data subdirectory for your raw CSV files by running:
mkdir -p data/raw
  • Confirm the folders exist by running:
ls data

You should see raw listed in the output.

  • Create a Python virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

  • Install pandas by running:
pip install pandas

What is pandas?

pandas is the standard Python library for tabular data manipulation. It lets you load CSVs, filter rows, compute aggregates, and export results. Think of it as a programmable spreadsheet.

You should see output ending with Successfully installed followed by package names including pandas.

Seeing a permission error or command not found?

  • Make sure you see (venv) in your prompt. If not, re-run the activation command above.
  • On macOS, if python3 is not found, confirm Python 3.11+ is installed by running python3 --version.
  • On Windows, try python instead of python3 if you get an error.

Why isn't my virtual environment activating or pip installing correctly?

Download flight data from BTS TranStats

The Bureau of Transportation Statistics publishes monthly performance data for every domestic flight in the U.S. You will download one month of this data using their web form.

  • Open the BTS TranStats download page in your browser:

Reporting Carrier On-Time Performance download page

  • In the Filter Geography dropdown, leave it set to All.
  • In the Filter Year dropdown, select 2024.
  • In the Filter Period dropdown, select June.
  • Scroll down to the field checkboxes. Click the Select All checkbox to include every available field in your download.
  • Click the Download button at the top or bottom of the page.

What gets downloaded?

BTS delivers the data as a zip file. Inside, you will find a CSV file named T_ONTIME_REPORTING.csv. This single file contains one row per flight for every domestic flight operated in June 2024.

  • Locate the downloaded zip file (usually in your Downloads folder).
  • Extract (unzip) the file.
  • Move the extracted T_ONTIME_REPORTING.csv file into your project's data/raw/ folder. The full path should be flight-delay-dashboard/data/raw/T_ONTIME_REPORTING.csv.

Move the file from terminal

If you prefer the terminal, you can move the file with this command (adjust the source path if your Downloads folder is named differently):

mv ~/Downloads/T_ONTIME_REPORTING.csv data/raw/

Don't see T_ONTIME_REPORTING.csv in the zip?

  • Make sure you selected Select All for the field checkboxes before clicking Download. The download button will not work if no fields are selected.
  • Confirm you chose 2024 for Year and June for Period. If the filters are wrong, the downloaded file may be empty or missing.
  • The file may be named slightly differently depending on your browser's unzip behavior. Look for a CSV file starting with T_ONTIME.

Help me find or extract my BTS TranStats download.

Verify the data loads correctly

Before building the full pipeline, confirm that pandas can read the file and that the key columns you need for analysis are present.

  • Make sure your terminal is still in the flight-delay-dashboard directory with the virtual environment active.
  • Run this verification command:
python3 -c "
import pandas as pd
df = pd.read_csv('data/raw/T_ONTIME_REPORTING.csv', nrows=5, low_memory=False)
print(f'Loaded successfully: {df.shape[1]} columns')
print()
expected = ['FlightDate', 'Reporting_Airline', 'Origin', 'Dest', 'DepDelay', 'ArrDelay', 'Cancelled', 'CarrierDelay', 'WeatherDelay', 'NASDelay', 'SecurityDelay', 'LateAircraftDelay', 'Distance']
for col in expected:
    status = 'FOUND' if col in df.columns else 'MISSING'
    print(f'  {status}: {col}')
"

What does this command do?

  • It loads only the first 5 rows of the CSV to keep things fast.
  • It prints the total column count (you should see 109 or close to it, since BTS provides 109 fields).
  • It checks for the 13 key columns needed for delay analysis and reports whether each one is found.

You should see all 13 columns listed as FOUND. This confirms your data is ready for the cleaning step.

Seeing FileNotFoundError?

  • Double-check that T_ONTIME_REPORTING.csv is inside data/raw/. Run ls data/raw/ to confirm the file is there.
  • Make sure your terminal's working directory is flight-delay-dashboard. Run pwd (macOS) or cd (Windows) to check.
  • If the file has a different name, rename it to T_ONTIME_REPORTING.csv so it matches what the pipeline script expects.

Help me debug why pandas cannot find or load my CSV file.

Your raw data is loaded and verified. Next up, you will clean this data and derive the operational KPIs that power your dashboard visualizations.

Clean and Explore the Data

You have raw flight data sitting in your project folder. Before you can build any visualizations, you need to understand what this data looks like and clean it up for analysis.

Real government datasets are messy. The BTS on-time data has null values scattered throughout its delay columns, cancelled flights mixed in with completed ones, and date fields stored as plain strings. In this step, you will load the raw CSV, filter out unusable records, and produce a clean dataset ready for KPI derivation.

In this step, get ready to:
  • Load the raw BTS flight data into a pandas DataFrame and inspect its structure.
  • Clean the dataset by removing cancelled and diverted flights, then fix null values in delay columns.
  • Print a summary report confirming on-time rates and average delays.
Load the raw data into pandas

The BTS CSV has over 100 fields and hundreds of thousands of rows. You need to load it into a pandas DataFrame so you can inspect, filter, and transform it programmatically.

  • In Cursor, create a new file called pipeline.py in the root of your project folder (the same level as your data/ folder).
  • Add the following code to set up the imports, configure file paths, and load the CSV:
import pandas as pd
from pathlib import Path

# --- Configuration ---
RAW_DATA_PATH = Path("data/raw")
OUTPUT_PATH = Path("data/output")
OUTPUT_PATH.mkdir(parents=True, exist_ok=True)

# --- Step 1: Load raw BTS data ---
print("Loading raw flight data...")
df = pd.read_csv(
    RAW_DATA_PATH / "T_ONTIME_REPORTING.csv",
    low_memory=False
)
print(f"Raw data shape: {df.shape}")

What does this code do?

The Path objects define where your raw data lives and where cleaned output will go. OUTPUT_PATH.mkdir(parents=True, exist_ok=True) creates the data/output/ folder if it does not already exist.

The low_memory=False parameter tells pandas to read the entire file before inferring column types. This prevents mixed-type warnings on large CSVs where different chunks might look like different data types.

  • Save pipeline.py.
  • Run the script in your terminal:
python pipeline.py
  • You should see output showing the raw data shape, something like Raw data shape: (rows, columns) where rows is in the hundreds of thousands.

Seeing a FileNotFoundError?

Make sure your CSV file is located at data/raw/T_ONTIME_REPORTING.csv relative to where pipeline.py lives. The file name is case-sensitive.

Also confirm you are running the script from your project root folder, not from inside the data/ folder.

help me debug a FileNotFoundError when loading my CSV with pandas

Clean and filter the dataset

The raw data contains cancelled and diverted flights that never completed their route. These records have no meaningful arrival delay data, so they would skew your analysis. You also need to convert the date column from a string to a proper datetime type.

  • Add the following code below the load section in pipeline.py:
# --- Step 2: Data Cleaning ---
print("\nCleaning data...")

# Convert FlightDate to datetime
df["FlightDate"] = pd.to_datetime(df["FlightDate"])

# Summary before filtering
total_flights = len(df)
cancelled_count = df["Cancelled"].sum()
diverted_count = df["Diverted"].sum()

print(f"Total flights: {total_flights:,}")
print(f"Cancelled: {cancelled_count:,} ({cancelled_count/total_flights*100:.1f}%)")
print(f"Diverted: {diverted_count:,} ({diverted_count/total_flights*100:.1f}%)")

# Filter to completed flights only (not cancelled, not diverted)
flights = df[(df["Cancelled"] == 0) & (df["Diverted"] == 0)].copy()
print(f"Completed flights: {len(flights):,}")

What does this code do?

First, pd.to_datetime() converts the FlightDate string column into a proper datetime type. This lets you do time-based grouping later.

The code prints the total flight count and cancellation/diversion rates BEFORE filtering. This gives you a baseline understanding of data quality.

The filter keeps only rows where Cancelled == 0 and Diverted == 0. The .copy() creates an independent DataFrame so edits to flights do not trigger pandas warnings about modifying a slice.

  • Save pipeline.py and run it again:
python pipeline.py
  • You should now see the cleaning output below the load message. The completed flights count is lower than total flights because cancelled and diverted records are removed.

Seeing a KeyError for Cancelled or Diverted?

Column names in the BTS CSV are case-sensitive. Make sure you typed Cancelled with a capital C and Diverted with a capital D, exactly as they appear in the dataset.

help me fix a KeyError when accessing columns in my pandas DataFrame

Compute on-time statistics and fill delay nulls

The BTS data has five delay cause columns that are null for flights that arrived on time. This is not missing data. It means no delay occurred. You need to fill these nulls with 0 so your aggregations produce correct averages later.

  • Add the following code at the bottom of pipeline.py:
# On-time stats
ontime_count = (flights["ArrDel15"] == 0).sum()
ontime_pct = ontime_count / len(flights) * 100
avg_delay = flights["ArrDelayMinutes"].mean()

print(f"On-time arrivals: {ontime_pct:.1f}%")
print(f"Average arrival delay: {avg_delay:.1f} minutes")

# Fill delay cause columns with 0 for on-time flights
delay_causes = ["CarrierDelay", "WeatherDelay", "NASDelay", "SecurityDelay", "LateAircraftDelay"]
for col in delay_causes:
    flights[col] = flights[col].fillna(0)

What does this code do?

The ArrDel15 column is a binary indicator where 1 means the flight arrived 15 or more minutes late. Counting rows where this equals 0 gives you the on-time count.

The fillna(0) loop replaces null values in the five delay cause columns with 0. These nulls represent flights that had no delay at all. Without filling them, any average or sum you compute later would incorrectly ignore on-time flights.

  • Save pipeline.py and run the script one more time:
python pipeline.py
  • You should see the full summary report now, including the on-time arrival percentage and average arrival delay in minutes. Your pipeline is working end to end.

On-time percentage looks wrong?

If you see 0% or 100%, double-check that the filter on the previous line uses ArrDel15 (with a capital A, D, and the number 15). This is the BTS standard field name for the 15-minute arrival delay indicator.

help me debug unexpected on-time percentage values in my flight data analysis

✔️ Awesome, I've got everything!

Great. Double check you have saved pipeline.py and that your terminal output shows all five print statements (raw shape, total flights, cancelled, diverted, completed flights, on-time percentage, and average delay).

ⓧ I'd like to double check the full code

Here is the complete pipeline.py as it should look at the end of this step:

import pandas as pd
from pathlib import Path

# --- Configuration ---
RAW_DATA_PATH = Path("data/raw")
OUTPUT_PATH = Path("data/output")
OUTPUT_PATH.mkdir(parents=True, exist_ok=True)

# --- Step 1: Load raw BTS data ---
print("Loading raw flight data...")
df = pd.read_csv(
    RAW_DATA_PATH / "T_ONTIME_REPORTING.csv",
    low_memory=False
)
print(f"Raw data shape: {df.shape}")

# --- Step 2: Data Cleaning ---
print("\nCleaning data...")

# Convert FlightDate to datetime
df["FlightDate"] = pd.to_datetime(df["FlightDate"])

# Summary before filtering
total_flights = len(df)
cancelled_count = df["Cancelled"].sum()
diverted_count = df["Diverted"].sum()

print(f"Total flights: {total_flights:,}")
print(f"Cancelled: {cancelled_count:,} ({cancelled_count/total_flights*100:.1f}%)")
print(f"Diverted: {diverted_count:,} ({diverted_count/total_flights*100:.1f}%)")

# Filter to completed flights only (not cancelled, not diverted)
flights = df[(df["Cancelled"] == 0) & (df["Diverted"] == 0)].copy()
print(f"Completed flights: {len(flights):,}")

# On-time stats
ontime_count = (flights["ArrDel15"] == 0).sum()
ontime_pct = ontime_count / len(flights) * 100
avg_delay = flights["ArrDelayMinutes"].mean()

print(f"On-time arrivals: {ontime_pct:.1f}%")
print(f"Average arrival delay: {avg_delay:.1f} minutes")

# Fill delay cause columns with 0 for on-time flights
delay_causes = ["CarrierDelay", "WeatherDelay", "NASDelay", "SecurityDelay", "LateAircraftDelay"]
for col in delay_causes:
    flights[col] = flights[col].fillna(0)

Your data is clean and the summary stats confirm the pipeline is working. Next, you will derive carrier-level KPIs and export everything as CSV files that Tableau can ingest.

Derive KPIs and Export to CSV

Your cleaned flight data is ready. Now you need to distill hundreds of thousands of individual flight records into meaningful metrics that Tableau can visualize.

In this step, you will build two summary tables (one ranking airlines, one revealing time-of-day patterns) and export everything as CSVs. These files become the data sources for your Tableau dashboard.

In this step, get ready to:
  • Build a carrier summary table with on-time percentage and delay cause breakdowns per airline.
  • Build a time-of-day summary table revealing when delays peak.
  • Export all tables as CSVs for Tableau ingestion.
Build the carrier summary table

The carrier summary answers the core question: which airlines perform best and what causes their delays? You will group flights by airline and compute KPIs that rank their operational performance.

  • In pipeline.py, find the line that says flights[col] = flights[col].fillna(0) (the last line in the cleaning loop).
  • Add the following code below it to build the carrier summary:
# --- Step 3: KPI Derivation ---
print("\nDeriving KPIs...")

# Carrier summary
carrier_summary = flights.groupby("Reporting_Airline").agg(
    total_flights=("Flights", "sum"),
    ontime_pct=("ArrDel15", lambda x: (x == 0).mean() * 100),
    avg_arr_delay=("ArrDelayMinutes", "mean"),
    avg_carrier_delay=("CarrierDelay", "mean"),
    avg_weather_delay=("WeatherDelay", "mean"),
    avg_nas_delay=("NASDelay", "mean"),
    avg_security_delay=("SecurityDelay", "mean"),
    avg_late_aircraft_delay=("LateAircraftDelay", "mean"),
).reset_index()

carrier_summary = carrier_summary.sort_values("ontime_pct", ascending=False)
print(f"Carrier summary: {len(carrier_summary)} airlines")
print(carrier_summary[["Reporting_Airline", "total_flights", "ontime_pct"]].to_string(index=False))

What does this code do?

The .groupby("Reporting_Airline").agg(...) groups every flight by its airline code, then computes multiple metrics at once. Each named aggregation (like ontime_pct) becomes a column in the output table.

  • The ontime_pct lambda counts flights where ArrDel15 == 0 (arrived within 15 minutes of schedule) and converts to a percentage.
  • Each avg_*_delay column shows how many minutes of delay, on average, are attributable to that specific cause.
  • Sorting by ontime_pct descending puts the best-performing airlines at the top.
  • Save pipeline.py.

Seeing a KeyError?

If you see a KeyError mentioning a column name, double-check that the column names in your .agg() call match the BTS dataset exactly. Column names are case-sensitive.

Help me debug my KeyError

Build the time-of-day summary table

Delays are not evenly distributed throughout the day. This table groups flights by their hourly departure block to reveal when congestion causes the most problems.

  • In pipeline.py, add the following code below the carrier summary print statement:
# Time-of-day summary
time_of_day = flights.groupby("DepTimeBlk").agg(
    total_flights=("Flights", "sum"),
    ontime_pct=("ArrDel15", lambda x: (x == 0).mean() * 100),
    avg_dep_delay=("DepDelayMinutes", "mean"),
).reset_index()

print(f"\nTime-of-day summary: {len(time_of_day)} time blocks")

What is DepTimeBlk?

The DepTimeBlk field is a BTS-defined hourly departure time block. It groups flights into intervals like "0600-0659" and "1700-1759". This makes it perfect for heatmap visualizations in Tableau.

  • Save pipeline.py.

Getting an empty DataFrame?

Make sure you are grouping on "DepTimeBlk" (capital D, capital T, capital B). The BTS column names use mixed case.

Help me debug my time-of-day groupby

Export all tables as CSVs for Tableau

Tableau Desktop Public Edition connects to CSV files directly. You will export three files: the carrier summary, the time-of-day summary, and the full cleaned flight-level data with only the columns Tableau needs.

  • Add the following export code at the bottom of pipeline.py:
# --- Step 4: Export for Tableau ---
print("\nExporting to CSV for Tableau...")

carrier_summary.to_csv(OUTPUT_PATH / "carrier_summary.csv", index=False)
time_of_day.to_csv(OUTPUT_PATH / "time_of_day.csv", index=False)

# Export cleaned flight-level data with selected columns for Tableau
tableau_columns = [
    "FlightDate", "Reporting_Airline", "Origin", "Dest",
    "DepTimeBlk", "DayOfWeek",
    "DepDelay", "DepDelayMinutes", "ArrDelay", "ArrDelayMinutes",
    "ArrDel15", "Distance",
    "CarrierDelay", "WeatherDelay", "NASDelay",
    "SecurityDelay", "LateAircraftDelay"
]
flights[tableau_columns].to_csv(OUTPUT_PATH / "flights_clean.csv", index=False)

print(f"Exported carrier_summary.csv ({len(carrier_summary)} rows)")
print(f"Exported time_of_day.csv ({len(time_of_day)} rows)")
print(f"Exported flights_clean.csv ({len(flights)} rows)")
print("\nDone! Open the CSV files in Tableau Desktop to build your dashboard.")

Why export a separate flights_clean.csv?

The summary tables power the bar charts and rankings. But the flight-level file lets Tableau compute its own aggregations on the fly. For example, the Time-of-Day Heatmap will cross-reference DepTimeBlk with DayOfWeek at the row level.

The index=False argument prevents pandas from writing a redundant row-number column that would clutter your Tableau field list.

  • Save pipeline.py.
  • Run the full script by executing this command in your terminal:
python pipeline.py
  • Confirm the output files exist by running:
ls data/output/
  • You should see carrier_summary.csv, time_of_day.csv, and flights_clean.csv listed in the output.
  • Your terminal should also show row counts for all three files and a final message reading Done! Open the CSV files in Tableau Desktop to build your dashboard..

Script fails partway through?

If the script errors during export, check that your data/output/ directory exists. The OUTPUT_PATH.mkdir(parents=True, exist_ok=True) line at the top of the script should handle this automatically.

If you see a permissions error, make sure you are running the script from your project directory (the folder containing pipeline.py).

Help me debug my export failure

✔️ Awesome, I've got everything!

Great. Double check you have saved pipeline.py and confirmed all three CSVs appear in data/output/.

ⓧ I'd like to double check the full code

Here is the complete pipeline.py as it should look after this step:

import pandas as pd
from pathlib import Path

# --- Configuration ---
RAW_DATA_PATH = Path("data/raw")
OUTPUT_PATH = Path("data/output")
OUTPUT_PATH.mkdir(parents=True, exist_ok=True)

# --- Step 1: Load raw BTS data ---
print("Loading raw flight data...")
df = pd.read_csv(
    RAW_DATA_PATH / "T_ONTIME_REPORTING.csv",
    low_memory=False
)
print(f"Raw data shape: {df.shape}")

# --- Step 2: Data Cleaning ---
print("\nCleaning data...")

# Convert FlightDate to datetime
df["FlightDate"] = pd.to_datetime(df["FlightDate"])

# Summary before filtering
total_flights = len(df)
cancelled_count = df["Cancelled"].sum()
diverted_count = df["Diverted"].sum()

print(f"Total flights: {total_flights:,}")
print(f"Cancelled: {cancelled_count:,} ({cancelled_count/total_flights*100:.1f}%)")
print(f"Diverted: {diverted_count:,} ({diverted_count/total_flights*100:.1f}%)")

# Filter to completed flights only (not cancelled, not diverted)
flights = df[(df["Cancelled"] == 0) & (df["Diverted"] == 0)].copy()
print(f"Completed flights: {len(flights):,}")

# On-time stats
ontime_count = (flights["ArrDel15"] == 0).sum()
ontime_pct = ontime_count / len(flights) * 100
avg_delay = flights["ArrDelayMinutes"].mean()

print(f"On-time arrivals: {ontime_pct:.1f}%")
print(f"Average arrival delay: {avg_delay:.1f} minutes")

# Fill delay cause columns with 0 for on-time flights
delay_causes = ["CarrierDelay", "WeatherDelay", "NASDelay", "SecurityDelay", "LateAircraftDelay"]
for col in delay_causes:
    flights[col] = flights[col].fillna(0)

# --- Step 3: KPI Derivation ---
print("\nDeriving KPIs...")

# Carrier summary
carrier_summary = flights.groupby("Reporting_Airline").agg(
    total_flights=("Flights", "sum"),
    ontime_pct=("ArrDel15", lambda x: (x == 0).mean() * 100),
    avg_arr_delay=("ArrDelayMinutes", "mean"),
    avg_carrier_delay=("CarrierDelay", "mean"),
    avg_weather_delay=("WeatherDelay", "mean"),
    avg_nas_delay=("NASDelay", "mean"),
    avg_security_delay=("SecurityDelay", "mean"),
    avg_late_aircraft_delay=("LateAircraftDelay", "mean"),
).reset_index()

carrier_summary = carrier_summary.sort_values("ontime_pct", ascending=False)
print(f"Carrier summary: {len(carrier_summary)} airlines")
print(carrier_summary[["Reporting_Airline", "total_flights", "ontime_pct"]].to_string(index=False))

# Time-of-day summary
time_of_day = flights.groupby("DepTimeBlk").agg(
    total_flights=("Flights", "sum"),
    ontime_pct=("ArrDel15", lambda x: (x == 0).mean() * 100),
    avg_dep_delay=("DepDelayMinutes", "mean"),
).reset_index()

print(f"\nTime-of-day summary: {len(time_of_day)} time blocks")

# --- Step 4: Export for Tableau ---
print("\nExporting to CSV for Tableau...")

carrier_summary.to_csv(OUTPUT_PATH / "carrier_summary.csv", index=False)
time_of_day.to_csv(OUTPUT_PATH / "time_of_day.csv", index=False)

# Export cleaned flight-level data with selected columns for Tableau
tableau_columns = [
    "FlightDate", "Reporting_Airline", "Origin", "Dest",
    "DepTimeBlk", "DayOfWeek",
    "DepDelay", "DepDelayMinutes", "ArrDelay", "ArrDelayMinutes",
    "ArrDel15", "Distance",
    "CarrierDelay", "WeatherDelay", "NASDelay",
    "SecurityDelay", "LateAircraftDelay"
]
flights[tableau_columns].to_csv(OUTPUT_PATH / "flights_clean.csv", index=False)

print(f"Exported carrier_summary.csv ({len(carrier_summary)} rows)")
print(f"Exported time_of_day.csv ({len(time_of_day)} rows)")
print(f"Exported flights_clean.csv ({len(flights)} rows)")
print("\nDone! Open the CSV files in Tableau Desktop to build your dashboard.")

Your data pipeline is complete. You have three analysis-ready CSV files sitting in data/output/ waiting to become visualizations. Next up, you will open Tableau Desktop and turn these numbers into an interactive dashboard.

Build Tableau Visualizations

Your Python pipeline produced three clean CSV files packed with operational KPIs. Numbers in a spreadsheet are useful, but they don't tell a story.

In this step, you'll turn those CSV files into three distinct Tableau visualizations. Each view answers a different question about airline performance: who's on time, why delays happen, and when they peak.

In this step, get ready to:
  • Connect your exported CSV files to Tableau Desktop Public Edition.
  • Build a Carrier Scorecard ranking airlines by on-time performance with color-coded tiers.
  • Create a Delay Cause Breakdown and a Time-of-Day Heatmap revealing delay patterns.
Connect your CSV data sources and create the Carrier Scorecard

The Carrier Scorecard is the headline view of your dashboard. It ranks every airline by on-time arrival percentage so a viewer can instantly see who performs best and worst.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Tableau Desktop Public Edition and press Enter to open it.
  • On the Start page, in the Connect pane on the left, click Text File.
  • Navigate to your project's data/output folder and select carrier_summary.csv.
  • Click Open.

Tableau opens the Data Source page showing a preview of your carrier data.

What is the Data Source page?

The Data Source page shows the first 1,000 rows of your connected file. You can inspect field names, data types, and rename columns here without altering the original CSV.

  • Click the sheet tab at the bottom (labelled Sheet 1) to open a blank worksheet.
  • Right-click the sheet tab and select Rename. Name it Carrier Scorecard.

Now build the horizontal bar chart.

  • In the Data pane on the left, drag Reporting Airline to the Rows shelf.
  • Drag Ontime Pct to the Columns shelf.
  • Click the sort descending icon in the toolbar (the bar chart icon with a downward arrow) to rank airlines from highest to lowest on-time percentage.

You should see a horizontal bar chart with airlines on the left axis and on-time percentage extending to the right.

Why horizontal bars?

Horizontal bars make it easy to read airline names without rotating your head. They also create a natural leaderboard layout where the best performer sits at the top.

Next, add performance tier coloring so viewers can instantly distinguish strong, moderate, and weak performers.

  • Click Analysis in the top menu bar.
  • Select Create Calculated Field.
  • Name the field Performance Tier.
  • Enter the following formula in the Calculation Editor:
IF [Ontime Pct] >= 80 THEN "Green"
ELSEIF [Ontime Pct] >= 70 THEN "Yellow"
ELSE "Red"
END
  • Click OK to save the calculated field.
  • From the Data pane, drag Performance Tier to Color on the Marks card.

Tableau assigns a categorical color to each tier value. Now assign the actual green, yellow, and red colors.

  • Click in the upper-right corner of the color legend that appeared on the right side of the view.
  • Select Edit Colors.
  • Click the Green item in the list. Select a green color from the palette on the right.
  • Click the Yellow item. Select a yellow/amber color.
  • Click the Red item. Select a red color.
  • Click OK to apply.

Your Carrier Scorecard now shows a ranked bar chart where green bars indicate airlines with 80%+ on-time arrivals, yellow bars show 70-80%, and red bars flag airlines below 70%.

Bars not showing colors?

Make sure you dragged Performance Tier to Color on the Marks card, not to the Rows or Columns shelf. The Marks card is the area between the Data pane and the view canvas.

If the calculated field shows an error, double-click it in the Data pane to re-open the Calculation Editor and check the formula matches exactly.

Help me fix my Carrier Scorecard color tiers in Tableau.

Create the Delay Cause Breakdown

The Carrier Scorecard shows who is late. The Delay Cause Breakdown reveals why. By splitting each airline's average delay minutes across four cause categories, this view exposes whether an airline's problems are internal (carrier-caused) or external (weather, air traffic control, or cascading late aircraft).

  • Click the new sheet tab (the + icon at the bottom of the workbook) to create a new worksheet.
  • Right-click the new tab and rename it Delay Cause Breakdown.
  • You need to connect the carrier_summary.csv data source for this sheet. Since you already connected it in the previous sheet, it is available in the Data pane on the left.

Build the stacked bar chart showing delay causes per carrier.

  • Drag Reporting Airline to the Rows shelf.
  • Drag Avg Carrier Delay to the Columns shelf.
  • Drag Avg Weather Delay to the Columns shelf, dropping it to the right of the existing pill.
  • Drag Avg Nas Delay to the Columns shelf, dropping it to the right.
  • Drag Avg Late Aircraft Delay to the Columns shelf, dropping it to the right.

At this point you have four separate bar charts side by side. To stack them into a single combined bar, you need to use Measure Values.

  • Select all four measure pills on the Columns shelf (click the first, then hold Shift and click the last).
  • Right-click one of the selected pills and choose Remove.
  • From the Data pane, drag Measure Values to the Columns shelf.
  • From the Data pane, drag Measure Names to Color on the Marks card.
  • In the Measure Values shelf (which appears at the bottom or side of the view), remove any measures you do not need. Keep only Avg Carrier Delay, Avg Weather Delay, Avg Nas Delay, and Avg Late Aircraft Delay. Right-click any unwanted pill and select Remove.

You should now see a stacked horizontal bar chart where each airline has one bar segmented by delay cause. Each color represents a different delay source.

What does this chart reveal?

Airlines with a large carrier delay segment have internal operational problems (crew scheduling, maintenance). Airlines with large NAS or late aircraft segments are more affected by system-wide air traffic congestion and cascading effects from earlier flights.

This distinction matters to hiring managers. It shows you can identify root causes, not just symptoms.

Seeing too many measures in the view?

The Measure Values shelf often auto-includes all numeric fields. Make sure you removed total_flights, ontime_pct, avg_arr_delay, and avg_security_delay from the Measure Values shelf. Right-click each unwanted pill and select Remove.

Help me set up my stacked bar chart in Tableau with only the four delay cause measures.

Create the Time-of-Day Heatmap

The third view answers a timing question: when do delays peak during the day? A heatmap with departure time blocks on one axis and day of week on the other uses color intensity to highlight the worst slots. This pattern is valuable for operations teams scheduling crews and allocating runway capacity.

  • Click the new sheet tab (+ icon) to create another worksheet.
  • Right-click the tab and rename it Time-of-Day Heatmap.
  • This view uses flight-level data. Click Data in the top menu bar, then New Data Source.
  • Click Text File, navigate to data/output, and select flights_clean.csv.
  • Click Open.
  • Click the sheet tab to return to your Time-of-Day Heatmap worksheet.

Build the heatmap by placing dimensions on each axis and encoding color with the delay measure.

  • Drag Dep Time Blk to the Columns shelf.
  • Drag Day Of Week to the Rows shelf.
  • Drag Dep Delay Minutes to Color on the Marks card. Tableau aggregates it as SUM by default.
  • Right-click the SUM(Dep Delay Minutes) pill on the Marks card and select Measure then Average to change the aggregation to AVG.
  • On the Marks card, click the dropdown that says Automatic and change the mark type to Square. This creates the heat map cell appearance.

You should see a grid of colored squares. Darker squares represent time blocks with higher average departure delays. Late afternoon and evening slots typically show the most intense color.

Why does delay increase later in the day?

Delays cascade throughout the day. A late morning departure pushes back the next flight using the same aircraft. By evening, these compounding delays stack up, making late-day slots consistently worse across all carriers.

  • Also connect the time_of_day.csv file now so it is available for the dashboard. Click Data in the top menu bar, then New Data Source.
  • Click Text File, navigate to data/output, select time_of_day.csv, and click Open.
  • Save your workbook locally by pressing Ctrl+S (Windows) or Cmd+S (macOS). Tableau will prompt you to save as a .twbx file.

Heatmap showing one giant colored block?

Make sure Dep Time Blk is on Columns and Day Of Week is on Rows. If both fields ended up on the same shelf, the view collapses into a single aggregated block.

Also confirm you changed the mark type to Square on the Marks card. The default Automatic type may render text or circles instead of a clean grid.

Help me fix my heatmap layout in Tableau Desktop.

All three visualizations are ready and your workbook is saved locally. Next up, you'll assemble these sheets into a polished dashboard and publish it to Tableau Public for the world to see.

Assemble the Dashboard and Publish

Your three individual visualizations are complete. Each tells a different part of the delay story. Now it's time to combine them into a single, interactive dashboard that tells the full picture at a glance.

A dashboard in Tableau is a canvas where you arrange multiple sheets together. Adding filters that connect views lets your audience explore the data on their own terms.

In this step, get ready to:
  • Assemble all three views into a cohesive dashboard layout with titles and data source attribution.
  • Publish the dashboard to Tableau Public for anyone to explore.
  • Enable the "Hire Me" button on your Tableau Public profile and copy the dashboard URL.
Create the dashboard layout

Tableau dashboards combine multiple sheets into a single view. You will arrange the Carrier Scorecard at the top with the Delay Cause Breakdown and Time-of-Day Heatmap side-by-side below it.

  • At the bottom of your workbook, click the New Dashboard icon (to the right of your existing sheet tabs).
  • In the Dashboard pane on the left, you will see your three sheets listed under Sheets.
  • Drag Carrier Scorecard from the Sheets list onto the empty dashboard canvas.
  • Drag Delay Cause Breakdown and drop it below the Carrier Scorecard.
  • Drag Time-of-Day Heatmap and drop it to the right of the Delay Cause Breakdown so the two views sit side-by-side in the lower half.

What does this layout achieve?

Placing the Carrier Scorecard across the full width at the top gives your audience an immediate performance ranking. The two detail views below answer "why" (delay causes) and "when" (time-of-day patterns) for any airline they spot in the scorecard.

Next, add a title and data source attribution so viewers know what they are looking at.

  • Double-click the default dashboard title at the top of the canvas.
  • Replace the placeholder text with US Airline Operations Intelligence - June 2024.
  • From the Objects section at lower left, drag a Text object onto the dashboard just below the title.
  • In the text editor that opens, type Source: U.S. DOT Bureau of Transportation Statistics and click OK.

Now add an interactive filter so viewers can explore delays at a specific origin airport.

  • Select the Carrier Scorecard view on the dashboard by clicking it.
  • Click the drop-down arrow in the upper-right corner of the selected view.
  • Select Filters, then select Origin.
  • A filter card for Origin appears on the dashboard. Click its drop-down arrow and select Apply to Worksheets, then All Using This Data Source.

Why apply to all worksheets?

This setting ensures that selecting an airport code in the Origin filter updates all three views simultaneously. Your audience can pick an airport like ATL or ORD and instantly see that airport's carrier rankings, delay causes, and time patterns together.

Your dashboard layout is complete. You should see the Carrier Scorecard at the top, the two detail views side-by-side below, a title, a subtitle, and an Origin filter card.

Views overlapping or squished?

If your views overlap, click on each sheet in the dashboard and use the resize handles to adjust proportions. The Carrier Scorecard should take roughly 40% of the height, with the two lower views splitting the remaining space equally.

If a view disappeared after dragging, press Cmd+Z (macOS) or Ctrl+Z (Windows) to undo and try again.

help me fix my Tableau dashboard layout

Publish to Tableau Public

Publishing makes your dashboard accessible to anyone on the internet. Tableau Public hosts it for free indefinitely.

  • In the top menu bar, click Server.
  • Select Tableau Public.
  • Click Save to Tableau Public.
  • Sign in with your Tableau Public account credentials.
  • In the workbook name field, type US Airline Operations Intelligence - June 2024.
  • Click Save.

What happens when you publish?

Tableau creates an extract of your data and uploads both the workbook and data to Tableau Public's servers. After publishing, your browser opens automatically to show the live dashboard on your Tableau Public profile.

All workbooks published to Tableau Public are publicly visible. Do not include any sensitive or private data.

  • Verify that the dashboard loads correctly in your browser. Click the Origin filter to confirm it updates all three views.

Publish option greyed out?

Make sure you are using Tableau Desktop Public Edition (not the Free Edition, which cannot publish). The Save to Tableau Public option also requires at least one field in a viz.

If you see a sign-in error, check that your Tableau Public account email and password are correct by signing in at public.tableau.com in your browser first.

help me troubleshoot publishing to Tableau Public

Enable the "Hire Me" button and copy your dashboard URL

The "Hire Me" button on your Tableau Public profile signals to hiring managers that you are available for data visualization and analytics work. Enabling it turns your dashboard into an active portfolio piece.

  • On your Tableau Public profile page (which opened after publishing), click your profile icon in the top right.
  • Select My Profile.
  • Click Edit Profile.
  • Check the Available for Hire box.
  • Confirm your preferred contact email address.
  • Click Save.

What does the Hire Me button do?

When enabled, a "Hire Me" button appears on your public profile. Hiring managers can click it and submit a short contact form. You will receive their inquiry via email at the address you specified.

Finally, grab the shareable URL for your resume and LinkedIn profile.

  • Navigate back to your published dashboard on your Tableau Public profile.
  • Click Share at the bottom of the viz.
  • Copy the link URL. Save this somewhere safe for your resume and LinkedIn.

Add it to LinkedIn

On your LinkedIn profile, add the dashboard URL under the Featured section or in the Projects section. Hiring managers often check these before reaching out.

Your airline operations intelligence dashboard is now live. Anyone with the link can explore carrier rankings, delay causes, and time-of-day patterns across U.S. domestic flights. In the Secret Mission, you will extend this analysis with seasonal trend data.

Secret mission

Seasonal Trend Analysis

Your current dashboard shows a single month of data. Airlines perform differently across seasons. Summer thunderstorms, winter snowstorms, and holiday travel surges all create distinct delay patterns. Can you extend your pipeline to capture seasonal variation and reveal which carriers handle busy periods best?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses Tableau Public for free hosting. There are no ongoing costs or paid services to worry about.

Decide whether to keep your resources as they are, or delete them entirely.

Resources you used:

  • Tableau Public dashboard (free, hosted indefinitely by Tableau).
  • Local project files (pipeline.py, pipeline_seasonal.py, data/ folder with raw CSVs and output CSVs).
  • Python virtual environment in your project folder.

Keep everything running

No action needed. Choose this if you want to keep your dashboard live and your local files available for future reference.

  • Your Tableau Public dashboard stays published permanently at no cost. Anyone with the link can view and interact with it.
  • Your local Python scripts and CSV files remain on your machine for future extension.
  • Your Python virtual environment stays on disk. It uses some storage but has no ongoing cost.

Pause - I'll come back to this later

There are no running services to pause in this project. Everything is either a static hosted dashboard or local files on your machine.

  • Your Tableau Public dashboard stays live at no cost. No action needed.
  • If you want to free up disk space, you can delete the raw CSV files in data/raw/. Each file is several hundred MB. You can re-download them from BTS TranStats later.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Delete the Tableau Public dashboard:
  • Go to your Tableau Public profile page.
  • Hover over the viz and select the More Actions icon.
  • Select Delete This Viz.
  • Delete your local project files:
  • On macOS/Linux, open Terminal and run:
cd ~/Desktop
rm -rf airline-dashboard
  • On Windows, open PowerShell and run:
cd ~\Desktop
Remove-Item -Recurse -Force airline-dashboard

This removes all Python scripts, the virtual environment, and both raw and output CSV files in one step.

Nice Work!

Nice Work!

You just built a complete airline operations intelligence dashboard from scratch. Real government data, a Python pipeline, and a published interactive visualization that anyone can explore.

Here's what you learned:

  • Downloaded and explored real U.S. government flight operations data from BTS TranStats, understanding delay cause fields and on-time performance indicators.
  • Built a Python data pipeline that cleans raw CSV data, filters cancelled and diverted flights, and derives operational KPIs like on-time percentage and average delay by carrier.
  • Created a multi-view Tableau dashboard with carrier rankings, delay cause breakdowns, and time-of-day heatmaps, then published it to Tableau Public with a shareable portfolio link.
  • Secret Mission: Added seasonal trend analysis showing month-over-month carrier performance shifts across multiple months of data.

Ready to quiz yourself?