Module 11 Lab Companion

BAN 6003: Data Management and Analytics Integration

Final Data Checks, Data Dictionary, and ABT Documentation

Experimental pilot. Use the Ask about this lab panel (on the right, or the Ask a question button on small screens) to ask about the section you are reading, or open Common questions for saved answers. Answers explain the companion in plain language and may add general background. Each answer is labelled: instructor-approved answers were approved by your instructor; AI-generated answers are new and not yet reviewed. The notebook is still the authoritative source. Graded work (Section 9: Offline Assignment: Handoff Package; Section 11: Final Reflection) is not answered for you. Questions about grades, deadlines or extensions go to your instructor.

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.

Adjust text-to-speech speed from one half to two times normal speed.

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 contentCode, official prompts, required tasks and the lab sequence come from the notebook. Each code block shows its notebook cell number.
Companion-added instructor noteThese clearly labeled notes are explanations, cautions or study advice added by this companion, many of them from the in-person lab session. They are not extra required notebook steps unless the note says so.

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

↑ Back to Lab Contents

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?

  1. Run final sanity checks on a prepared ABT: row meaning and duplicate keys, missing values, impossible or suspicious values, and target variable issues.
  2. Create a data dictionary: one dictionary row for each ABT column.
  3. Write a transformation and integration summary, with findings and limitations.
  4. Gather your checks into a validation checklist and prepare a handoff package.
  5. Apply the same process to your semester project ABT.

↑ Back to Lab Contents

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()

↑ Back to Lab Contents

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()

↑ Back to Lab Contents

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()

↑ Back to Lab Contents

Your Turn 1: A Different Grain

Try first

A teammate grouped the flights by origin, dest and carrier instead.

  1. Build route_carrier: group flights by 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"].
  2. In 2 sentences: what does one row represent, which columns are its key, and why does the duplicate check on origin and dest no 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.

↑ Back to Lab Contents

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?

↑ Back to Lab Contents

Your Turn 2: One Column with Missing Values

Try first

Choose one column with missing values other than the target avg_arr_delay.

  1. Show the rows where your column is missing, with origin, dest, flight_count and your column.
  2. 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:

  1. Expected: yes, once you see which airports they are.
  2. Not from the raw flights: every one of these routes has real flights (from 189 to 4,752).
  3. It came from the integration step: BQN, PSE and SJU (Puerto Rico) and STT (U.S. Virgin Islands) are not in the airports table, so the left merge found no match.
  4. Keep the column; the routes are real, and the names could be filled from another airport source later.
  5. 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.

↑ Back to Lab Contents

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)

↑ Back to Lab Contents

Your Turn 3: A Negative Maximum Delay

Try first

describe() shows a minimum max_arr_delay of -22 minutes.

  1. Show the routes where max_arr_delay is below 0, with their flight_count.
  2. 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.

↑ Back to Lab Contents

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.

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

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)

↑ Back to Lab Contents

Your Turn 4: What the Target Covers

Try first

Use the coverage table and the route in its first row.

  1. In 2 sentences: how many flights does flight_count include for this route, and how many does avg_arr_delay average?
  2. 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.

↑ Back to Lab Contents

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

↑ Back to Lab Contents

Your Turn 5: Document One ABT Column

Try first

Complete the dictionary row for avg_dep_delay, using the same pattern as the examples.

  1. Fill the four values in the order of doc_fields.
  2. 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.

↑ Back to Lab Contents

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.

↑ Back to Lab Contents

Your Turn 6: Findings and Limitations

Try first

The example says which checks were performed, but not what they found.

  1. 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 airports table.
  • 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.

↑ Back to Lab Contents

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:

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)

↑ Back to Lab Contents

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.

  1. Step 1: complete the dictionary rows for the five ABT columns that are still blank.
  2. Step 2: complete the validation checklist: for each new row, compute result with code (reuse Sections 2–6), then write the status and a note.
  3. Step 3: run the export cell to save the ABT, the data dictionary and the validation checklist.
  4. 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.

↑ Back to Lab Contents

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.

↑ Back to Lab Contents

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:

  1. What does one row mean, and which columns are the key?
  2. What is your likely target, and is it known for every row?
  3. 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.

↑ Back to Lab Contents

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

↑ Back to Lab Contents

Save and Submit

  1. Complete Your Turn 1–6, the offline assignment (dictionary, validation checklist, handoff decision) and the final reflection.
  2. Restart the kernel and run all cells from top to bottom; confirm there are no errors.
  3. Confirm the three files exist in outputs: route_abt_module11.csv, route_abt_data_dictionary_module11.csv and route_abt_validation_checklist_module11.csv.
  4. 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).

↑ Back to Lab Contents

Lab Wrap-Up

Before an ABT is used for modeling, check and document it:

Next, Module 12 starts modeling with linear regression, and the time-of-prediction question from Section 5 comes back as a central idea.