How to use this companion
Open the Module 11 notebook and this companion at the same time. Work through the companion from top to bottom and run the matching notebook cells as you go. The notebook is the authoritative source for the lab sequence, code and required work. This companion explains what each step means, what to notice in the output and why the step matters.
Read aloud: controls and directions
Before you start: choose a reading mode, speed, and voice below. Select Read Aloud from Beginning to hear the full companion, or use Read Aloud from This Section beside any major section to continue from that point. While audio is playing, a floating control bar remains visible at the bottom of the page. Use Pause to pause temporarily, Resume to continue, and Stop to end the current reading.
Study mode reads explanations, results, tables, and notes while skipping exact Python code. Full mode also reads Python code and verbalizes common programming symbols.
Ready to read.
The read-aloud feature uses your browser's speech engine. A screen reader can access the page independently of these controls.
What is in the notebook, and what this companion adds
- Notebook: the exact Python code, the Your Turn tasks, the offline assignment and the written answers.
- Companion: plain-language explanations, the results you should expect (taken from a real run of the notebook), sample solutions for the Your Turn tasks (folded, so you try first), and notes from the lab session.
- If anything ever seems inconsistent: follow the current notebook.
This companion is designed for keyboard and screen-reader access, but it is not a separate version for students who need accessibility features. It is the same learning guide for everyone. You can read it visually, use a screen reader, or use the built-in read-aloud controls while you work in the notebook.
Use either course environment
- GitHub Codespaces (main path): open your own repository in Codespaces, open the Module 11 notebook and use the default Python kernel.
- Local VS Code (optional, for experienced students): clone your repository, use the
baseconda environment, installrequirements.txt, open the repository folder in Visual Studio Code and open the notebook. - In either environment: the notebook and this companion are the same. Keep the repository structure unchanged so the notebook paths and output folders work.
Module 11 Learning Goals
Module 11 is the last preparation step before modeling. There are few new functions this week; you reuse what you
already know (groupby, merge, isna, describe, .loc) to
answer one question: is this ABT ready for modeling, handoff and business interpretation?
- Run final sanity checks on a prepared ABT: row meaning and duplicate keys, missing values, impossible or suspicious values, and target variable issues.
- Create a data dictionary: one dictionary row for each ABT column.
- Write a transformation and integration summary, with findings and limitations.
- Gather your checks into a validation checklist and prepare a handoff package.
- Apply the same process to your semester project ABT.
Before You Start: Repository and Setup
Start from the Week 11 template repository, create your own repository with Use this template > Create a
new repository, and open notebooks/module11_abt_documentation_guided_lab.ipynb in Codespaces or
local VS Code. The notebook builds its own demo ABT from nycflights13, so no data files or database
connection are needed.
# Codespaces: nycflights13 is installed from requirements.txt.
# Local only: if the import below fails, uncomment and run this line:
# %pip install nycflights13 "setuptools<81"
# (setuptools<81 is needed because nycflights13 uses pkg_resources, which newer setuptools removed.)
import nycflights13
import pandas as pd
from pathlib import Path
repo_root = Path.cwd()
if not (repo_root / "README.md").exists() and (repo_root.parent / "README.md").exists():
repo_root = repo_root.parent
output_dir = repo_root / "outputs"
output_dir.mkdir(parents=True, exist_ok=True)
flights = nycflights13.flights.copy()
airports = nycflights13.airports.copy()
airlines = nycflights13.airlines.copy()
Section 1: Build a Demo ABT
The demo ABT has one row per origin-destination route. It is built in two steps you already know: aggregate the flight-level table to the route level, then add destination details with a left merge.
1.1 Aggregate flights to one row per route
route_abt = (
flights
.groupby(["origin", "dest"])
.agg(
flight_count=("flight", "count"),
avg_dep_delay=("dep_delay", "mean"),
avg_arr_delay=("arr_delay", "mean"),
max_arr_delay=("arr_delay", "max"),
avg_distance=("distance", "mean"),
avg_air_time=("air_time", "mean")
)
.reset_index()
)
route_abt.head()
1.2 Add destination airport details with a left merge
dest_airports = airports[["faa", "name", "lat", "lon"]].rename(
columns={
"faa": "dest",
"name": "dest_airport_name",
"lat": "dest_lat",
"lon": "dest_lon"
}
)
route_abt = route_abt.merge(dest_airports, how="left", on="dest")
route_abt.head()
Section 2: Row Meaning and Shape
Before checking any values, confirm what one row means. For this ABT, one row should be one origin-destination
route, so the key columns are origin and dest.
2.1 Shape and key columns
route_abt.shape
route_abt[["origin", "dest"]].head()
2.2 Check duplicate entity rows
route_abt[["origin", "dest"]].duplicated().sum()
Your Turn 1: A Different Grain
Try first
A teammate grouped the flights by origin, dest and carrier instead.
- Build
route_carrier: groupflightsby the three columns, count with.size()and use.reset_index(name="flight_count"). Print its number of rows and the duplicate check on["origin", "dest"]. - In 2 sentences: what does one row represent, which columns are its key, and why does the duplicate check on
originanddestno longer return 0?
Show sample solution for Your Turn 1
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
route_carrier = (
flights
.groupby(["origin", "dest", "carrier"])
.size()
.reset_index(name="flight_count")
)
print("rows:", len(route_carrier))
print("duplicate origin-dest keys:", route_carrier[["origin", "dest"]].duplicated().sum())Sample answer: one row of route_carrier is one route flown by one carrier, so its key is origin + dest + carrier. Many routes are flown by several carriers, so the same origin-dest pair appears once per carrier and the two-column check finds 215 repeats; checked on all three key columns, the count is 0.
Section 3: Missing Values
Missing values are not always wrong, but they must be understood: they affect modeling, interpretation, business recommendations and whether a column should be used at all.
missing_summary = route_abt.isna().sum().reset_index()
missing_summary.columns = ["column_name", "missing_count"]
missing_summary["missing_percent"] = (missing_summary["missing_count"] / len(route_abt) * 100).round(1)
missing_summary.sort_values("missing_count", ascending=False)
Questions to ask
For any column with missing values: Is this missingness expected? Did it come from the raw data? Did it come from an integration step? Should the column be kept, dropped or imputed later? Should the limitation be documented?
Your Turn 2: One Column with Missing Values
Try first
Choose one column with missing values other than the target avg_arr_delay.
- Show the rows where your column is missing, with
origin,dest,flight_countand your column. - Answer the five Questions to ask for that column, one short sentence each.
Show sample solution for Your Turn 2
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
route_abt[route_abt["dest_airport_name"].isna()][
["origin", "dest", "flight_count", "dest_airport_name"]
]Sample answer for dest_airport_name:
- Expected: yes, once you see which airports they are.
- Not from the raw flights: every one of these routes has real flights (from 189 to 4,752).
- It came from the integration step: BQN, PSE and SJU (Puerto Rico) and STT (U.S. Virgin Islands) are not
in the
airportstable, so the left merge found no match. - Keep the column; the routes are real, and the names could be filled from another airport source later.
- Yes: document it in the dictionary notes.
If you chose avg_dep_delay, max_arr_delay or avg_air_time:
the one missing row is EWR to LGA, whose only flight was cancelled, so there was no departure, arrival or air time to
average. That missingness comes from the raw data, not from an integration step.
Section 4: Impossible or Suspicious Values
Some values are not missing but still impossible or suspicious. Not every extreme value is wrong, but every one should be noticed and explained.
4.1 Summary statistics
route_abt.describe()
4.2 Basic impossible-value checks
# Basic impossible-value checks
checks = {
"negative_flight_count": (route_abt["flight_count"] < 0).sum(),
"zero_or_negative_distance": (route_abt["avg_distance"] <= 0).sum(),
"zero_or_negative_air_time": (route_abt["avg_air_time"] <= 0).sum(),
}
checks
4.3 Inspect the routes with the highest average arrival delay
route_abt.sort_values("avg_arr_delay", ascending=False).head(10)
Your Turn 3: A Negative Maximum Delay
Try first
describe() shows a minimum max_arr_delay of -22 minutes.
- Show the routes where
max_arr_delayis below 0, with theirflight_count. - In 1–2 sentences: is this an error, and what does it tell you about averages and maximums on routes with very few flights?
Show sample solution for Your Turn 3
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
route_abt[route_abt["max_arr_delay"] < 0][["origin", "dest", "flight_count", "max_arr_delay"]]Sample answer: not an error. Each of the four routes had a single flight that arrived early, so its maximum (and its average) is that one early arrival. Averages and maximums based on one or a few flights are unstable and should not be compared with routes that have thousands of flights; this is a limitation to document.
Section 5: Target Variable Issues
If the ABT will be used for modeling, the target must be clearly defined. The notebook sets up a business question: an airline planning team wants to estimate, before a new schedule season starts, how late flights on each route will arrive on average, so it can add time buffers to the schedule.
- Target:
avg_arr_delay, in minutes. This is the value a model would predict. - Predictors (features): route information known when the schedule is planned, such as
avg_distance,flight_countand the destination location.
5.1 Missing values and distribution of the target
route_abt["avg_arr_delay"].isna().sum()
route_abt["avg_arr_delay"].describe()
5.2 Questions to ask about the target
- What exactly does the target measure, and in which unit?
- Is it missing for some rows?
- When is it known? Only after the flights have arrived. The target is never known at the time of prediction; that is why we predict it.
- Is every predictor known at the time of prediction? A predictor that is not yet known then is data leakage.
- Are there extreme values to review?
- Does the target support the business question?
5.3 Which flights does the target cover?
arr_delay_counts = (
flights.groupby(["origin", "dest"])["arr_delay"].count()
.rename("flights_with_arr_delay").reset_index()
)
target_coverage = route_abt[["origin", "dest", "flight_count", "avg_arr_delay"]].merge(
arr_delay_counts, how="left", on=["origin", "dest"]
)
target_coverage["flights_without_arr_delay"] = (
target_coverage["flight_count"] - target_coverage["flights_with_arr_delay"]
)
print("Flights without an arrival delay, all routes:", target_coverage["flights_without_arr_delay"].sum())
target_coverage.sort_values("flights_without_arr_delay", ascending=False).head()
5.4 Columns that are not known at the time of prediction
The planning team estimates before the season starts. max_arr_delay and avg_dep_delay are
measured on the same flights as the target, so they exist only after those flights have flown.
route_abt[["avg_arr_delay", "avg_dep_delay", "max_arr_delay"]].corr().round(2)
Your Turn 4: What the Target Covers
Try first
Use the coverage table and the route in its first row.
- In 2 sentences: how many flights does
flight_countinclude for this route, and how many doesavg_arr_delayaverage? - Why are the other flights left out of the target? Name the pandas behavior that leaves them out.
Show sample solution for Your Turn 4
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
Sample answer: for LGA to ORD, flight_count includes 8,857 flight records, but avg_arr_delay averages only the 8,507 flights that have a recorded arrival delay. The other 350 flights have a missing arr_delay, mostly cancelled flights (309 of them never departed) plus a few that departed but have no recorded arrival; pandas mean() skips missing values by default (skipna=True), so they are silently left out of the target.
Section 6: Create a Data Dictionary
A data dictionary explains what each ABT column means. Each row of the data dictionary describes one ABT column, so the 11-column route ABT needs 11 dictionary rows. Each row has the column name, data type, description, source, transformation or derivation, and notes or concerns.
6.1 Create the dictionary template
data_dictionary = pd.DataFrame({
"column_name": route_abt.columns,
"data_type": [str(route_abt[col].dtype) for col in route_abt.columns],
"description": "",
"source": "",
"transformation_or_derivation": "",
"notes_or_concerns": ""
})
data_dictionary
6.2 Fill in five dictionary rows as examples
doc_fields = ["description", "source", "transformation_or_derivation", "notes_or_concerns"]
# One entry per ABT column (= one dictionary row). The four values follow the order of doc_fields.
example_entries = {
"origin": [
"Origin airport code for the route", "flights table",
"Used as part of the route-level grouping key",
"Forms the route key together with dest; only the three New York City airports (EWR, JFK, LGA)"
],
"dest": [
"Destination airport code for the route", "flights table",
"Used as part of the route-level grouping key",
"Includes one unusual route, EWR to LGA, with a single cancelled flight"
],
"flight_count": [
"Number of flights observed for the route", "flights table",
"Count of flight records grouped by origin and destination",
"Includes cancelled flights; ranges from 1 to 11,262; 10 routes have fewer than 10 flights, so their "
"averages are unstable"
],
"avg_arr_delay": [
"Average arrival delay for flights on the route, in minutes (target)", "flights table",
"Mean of arr_delay grouped by origin and destination",
"Averages only flights with a recorded arrival delay; missing for EWR to LGA (its only flight was "
"cancelled); negative means early on average"
],
"dest_airport_name": [
"Full name of the destination airport", "airports table",
"Joined from airports using dest = faa",
"Missing for 7 routes whose destination (BQN, PSE, SJU, STT) is not in the airports table"
],
}
for abt_column, values in example_entries.items():
data_dictionary.loc[data_dictionary["column_name"] == abt_column, doc_fields] = values
data_dictionary
Your Turn 5: Document One ABT Column
Try first
Complete the dictionary row for avg_dep_delay, using the same pattern as the examples.
- Fill the four values in the order of
doc_fields. - In
notes_or_concerns, record the route where it is missing and the timing concern from Section 5.
Show sample solution for Your Turn 5
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
your_turn_entries = {
"avg_dep_delay": [
"Average departure delay for flights on the route, in minutes",
"flights table",
"Mean of dep_delay grouped by origin and destination",
"Missing for EWR to LGA (its only flight was cancelled); measured on the same flights as the target, "
"so not known before the season and must not be used as a predictor (correlation 0.83 with avg_arr_delay)"
],
}
for abt_column, values in your_turn_entries.items():
data_dictionary.loc[data_dictionary["column_name"] == abt_column, doc_fields] = values
data_dictionary[data_dictionary["column_name"] == "avg_dep_delay"]Your wording can differ. What matters is that a reader who never saw this notebook learns where the column comes from, how it was computed, and the two concerns.
Section 7: Transformation and Integration Summary
The data dictionary documents columns; the transformation summary documents the process: the raw data, cleaning and filtering, transformations, aggregations that changed the unit of analysis, joins, checks, and remaining assumptions or limitations.
The notebook's example lists eight steps for the demo ABT, from starting with flights to naming
avg_arr_delay as the possible target.
Your Turn 6: Findings and Limitations
Try first
The example says which checks were performed, but not what they found.
- Add a Findings and limitations list of 3–4 bullets to the demo summary, using numbers from Sections 2–5 and Your Turns 2–4.
Show sample solution for Your Turn 6
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
Sample bullets:
- No duplicate route keys: all 224 rows are distinct origin-dest routes.
- Seven routes to BQN, PSE, SJU and STT have no airport name or
coordinates, because those airports are missing from the
airportstable. - Ten routes have fewer than 10 flights; the four one-flight routes even show negative maximum delays, so their averages are unstable.
- The target averages only flights with a recorded arrival delay: 9,430 flight records, mostly cancellations, are excluded, and EWR to LGA has no target value.
Section 8: Prepare a Handoff Package
A complete handoff package usually has five parts: the final ABT file, a complete data dictionary, a transformation and integration summary, a validation checklist, and a README or short handoff note.
8.1 What a validation checklist is
A validation checklist gathers the validation code you already ran into one table: one row per check, with the
number the check found (result), a status and a short note. Each check from
Sections 2–6 becomes one row, for example the Section 2 duplicate check becomes "Duplicate route keys". The three
statuses:
Pass: the check found no problem (the result is 0).Documented: the check found something real, and the data dictionary notes explain it.Needs attention: a problem that must be fixed or explained before handoff.
8.2 The first two checklist rows
checklist_rows = [
{
"check": "Duplicate route keys (origin + dest)",
"result": route_abt[["origin", "dest"]].duplicated().sum(),
"status": "Pass",
"notes": "Each route appears once",
},
{
"check": "Missing key values (origin, dest)",
"result": route_abt[["origin", "dest"]].isna().sum().sum(),
"status": "Pass",
"notes": "Every row has both key values",
},
]
pd.DataFrame(checklist_rows)
Section 9: Offline Assignment: Handoff Package
Complete independently
This assignment is intentionally not solved in the companion. An analyst on another team will use the route ABT next week and cannot ask you about undocumented details. Finish the package and decide whether it is ready.
- Step 1: complete the dictionary rows for the five ABT columns that are still blank.
- Step 2: complete the validation checklist: for
each new row, compute
resultwith code (reuse Sections 2–6), then write thestatusand anote. - Step 3: run the export cell to save the ABT, the data dictionary and the validation checklist.
- Step 4: write the handoff decision.
What to
check: use the status definitions in Section 8. A real finding that the dictionary explains is
Documented, not a failure. A real extreme delay can remain if it is documented; do not silently remove
it.
# Offline Assignment, step 3 - export the three handoff files. Run this after the checklist cell.
unfinished = validation_checklist["result"].isna() | validation_checklist[["status", "notes"]].eq("TODO").any(axis=1)
if unfinished.any():
print("Complete these checklist rows before exporting:", list(validation_checklist.loc[unfinished, "check"]))
else:
handoff_files = {
"route_abt_module11.csv": route_abt,
"route_abt_data_dictionary_module11.csv": data_dictionary,
"route_abt_validation_checklist_module11.csv": validation_checklist,
}
for file_name, table in handoff_files.items():
table.to_csv(output_dir / file_name, index=False)
print("Saved:", output_dir / file_name)
Handoff decision
Write 5–6 sentences that approve the handoff, approve it with conditions, or hold it for revision. Cite at least four results from your checklist, name one remaining limitation or target concern, and state what the receiving analyst should know before modeling.
Section 10: Apply This to Your Project (After the Lab)
Not part of the lab grade. When you prepare your project ABT, use the same process: a clear statement of the unit of analysis, a summary of your validation checks (a checklist works well), a data dictionary, a transformation and integration summary, and a clean handoff folder in GitHub.
Section 11: Final Reflection
This is a graded written answer about your own project ABT, so the companion will not write it for you. In 3–4 sentences, answer from what you know now; you do not need to build anything:
- What does one row mean, and which columns are the key?
- What is your likely target, and is it known for every row?
- Which check from this lab will you run first on your project ABT, and which column will need the clearest dictionary entry?
Short, specific answers are enough. Leaving the reflection empty loses points.
Optional Practice: Diagnose a Mini Handoff Package (Not Graded)
A teammate sends a four-row route ABT and its dictionary. Before repairing anything, build a validation checklist
named practice_checklist with the columns check and result: duplicate keys,
missing key values, missing target values, ABT columns without a dictionary row, and blank dictionary cells.
Show sample solution for the optional practice
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
text_fields = ["description", "source", "transformation_or_derivation", "notes_or_concerns"]
practice_checklist = pd.DataFrame({
"check": [
"Duplicate route keys",
"Missing key values",
"Missing target values",
"ABT columns without a dictionary row",
"Blank dictionary cells",
],
"result": [
practice_abt.duplicated(["origin", "dest"]).sum(),
practice_abt[["origin", "dest"]].isna().sum().sum(),
practice_abt["avg_arr_delay"].isna().sum(),
len(set(practice_abt.columns) - set(practice_dictionary["column_name"])),
practice_dictionary[text_fields].fillna("").astype(str)
.apply(lambda col: col.str.strip()).eq("").sum().sum(),
],
})
practice_checklist
Save and Submit
- Complete Your Turn 1–6, the offline assignment (dictionary, validation checklist, handoff decision) and the final reflection.
- Restart the kernel and run all cells from top to bottom; confirm there are no errors.
- Confirm the three files exist in
outputs:route_abt_module11.csv,route_abt_data_dictionary_module11.csvandroute_abt_validation_checklist_module11.csv. - Save, then commit and push:
git add .,git commit -m "Complete Module 11 ABT documentation lab",git push. Submit the repository link in Canvas (the new repository, if you recreated it).
Lab Wrap-Up
Before an ABT is used for modeling, check and document it:
- Grain: what one row means, its key columns, and a duplicate check on the key.
- Missing and suspicious values: where they come from, investigated and documented rather than silently removed.
- Target: what it measures, which records it covers, and which columns would not be known at the time of prediction.
- Documentation: a dictionary row for every column, a summary of the process with findings, and a checklist that gathers the checks.
Next, Module 12 starts modeling with linear regression, and the time-of-prediction question from Section 5 comes back as a central idea.