Module 7 Lab Companion

BAN 6003: Data Management and Analytics Integration

Aggregation, Reshaping, and Light Validation

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 (Part 9: Final Practice: Carrier-Month Dashboard Table; Part 10: Offline Assignment: Route Performance Summary) is not answered for you. Questions about grades, deadlines or extensions go to your instructor.

How to use this companion

Open the Module 7 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 7 Learning Goals

In this lab you return to the nycflights13 flights and answer manager questions with two kinds of moves. Aggregation summarizes many rows into fewer rows, for example one row per carrier. Reshaping rearranges the same values between a wide layout and a long layout, without changing them. By the end of the lab you should be able to:

The main idea: aggregation and reshaping can change what one row means. Say the new meaning out loud, then check it.

You will use only four main tools: groupby() with agg(), and melt() with pivot(). Combined, they can do a lot.

↑ Back to Lab Contents

Before You Start: Repository and Setup

Weeks 7 and 8 use a new repository. Both the Module 7 and the Module 8 notebooks are in it.

  1. Open the template repository tianhaiz/ban6003-week-07-08-aggregation-integration-template on GitHub (the link is in the Canvas lab guide).
  2. Select Use this template, then Create a new repository. Use a clear name, for example ban6003-week-07-08- plus your name.
  3. Keep it public if you can. If you make it private, invite the instructor account zzz1990771 as a collaborator, or the instructor cannot grade it.
  4. Open your new repository in GitHub Codespaces. A new Codespace usually needs 4–5 minutes the first time; reopening it later is much faster.
  5. Open notebooks/module7_aggregation_reshaping_guided_lab.ipynb and run the cells from the top. If you restart the kernel, run from the top again.

Optional local path: clone your repository, open the folder in Visual Studio Code and trust the folder when VS Code asks. Choose the base conda environment as the kernel and install requirements.txt once.

Load the packages

import nycflights13
import pandas as pd
from pathlib import Path

↑ Back to Lab Contents

Part 1: What Does One Row Mean Now?

Before you transform anything, be crystal clear about the current unit of analysis: what one row represents. Every later step changes it, so this is your starting point.

flights = nycflights13.flights.copy()
flights.head()
flights.shape
flights.columns
flights[["year", "month", "day", "carrier", "flight", "origin", "dest", "dep_delay", "arr_delay", "distance", "air_time"]].head()
The columns this lab uses, first five flights
monthdaycarrierflightorigindestdep_delayarr_delaydistanceair_time
11UA1545EWRIAH2.011.01400227.0
11UA1714LGAIAH4.020.01416227.0
11AA1141JFKMIA2.033.01089160.0
11B6725JFKBQN-1.0-18.01576183.0
11DL461LGAATL-6.0-25.0762116.0

↑ Back to Lab Contents

Part 2: Aggregation with groupby() and agg()

Business question: which airline carrier had the highest average departure delay?

Before you write code, answer two questions. What should one row of the answer be? One carrier, not one flight. Which column do you summarize, and how? dep_delay, with the mean.

The pattern is data.groupby("grouping_column").agg({"value_column": "summary_function"}). The column in groupby() sets the new row level. The dictionary in agg() says which column to summarize (the key) and how (the value). Common summary functions are "mean", "median", "count", "max", "min" and "std".

2.1 Average departure delay by carrier

carrier_delay = flights.groupby("carrier").agg({
    "dep_delay": "mean"
})

carrier_delay.head()

2.2 Turn the result back into a regular table

carrier_delay = flights.groupby("carrier").agg({
    "dep_delay": "mean"
}).reset_index()

carrier_delay.head()

2.3 Sort to find the highest average delay

carrier_delay.sort_values("dep_delay", ascending=False).head()

↑ Back to Lab Contents

What Changed in the Row?

The original table had one row per flight. The summary has one row per carrier. So dep_delay no longer means “the delay of one flight”. It now means “the average departure delay of that carrier”. The column kept its name, but its meaning changed. That is why you should always write down what one row means after a summary.

↑ Back to Lab Contents

Your Turn 1: Lowest Average Arrival Delay

Try first

Copy the pattern from Part 2 and change only what the question changes. Decide: what is one row, which column do you summarize, and which sort order puts the answer first?

  1. Which carrier had the lowest average arrival delay? Use groupby(), agg(), reset_index() and sort_values().
  2. Write one sentence in the notebook explaining what one row means in your result.
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.

carrier_arr_delay = flights.groupby("carrier").agg({
    "arr_delay": "mean"
}).reset_index()

carrier_arr_delay.sort_values("arr_delay").head()

What it prints:

   carrier  arr_delay
2       AS  -9.930889
8       HA  -6.915205
1       AA   0.364291
4       DL   1.644341
13      VX   1.764464

Use a new name such as carrier_arr_delay, so you do not overwrite carrier_delay. For the lowest value, keep the default ascending order.

↑ Back to Lab Contents

Part 3: Multiple Aggregations and Named Aggregation

Business question: for each carrier, what are the average departure delay, the maximum departure delay and the number of flights?

An average alone can hide risk. A carrier can keep a good average and still have a few extreme delays, and those few delays hurt customers a lot. A very small carrier can also get an extreme average from only a few flights. So you summarize several things at once.

3.1 Average, maximum and count in one summary

carrier_summary = flights.groupby("carrier").agg(
    avg_dep_delay=("dep_delay", "mean"),
    max_dep_delay=("dep_delay", "max"),
    flight_count=("flight", "count")
).reset_index()

carrier_summary.head()

3.2 Sort by average departure delay

carrier_summary.sort_values("avg_dep_delay", ascending=False).head(10)

↑ Back to Lab Contents

Your Turn 2: A Monthly Delay Summary

Try first

The unit of analysis changes again. There is no carrier now: one row is one month. Change the grouping column and give the result a new name.

  1. Create one row per month with the average arrival delay, the median arrival delay and the number of flights.
  2. Sort by average arrival delay from highest to lowest.
  3. Write one sentence explaining what one row means in your monthly summary.
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.

month_summary = flights.groupby("month").agg(
    avg_arr_delay=("arr_delay", "mean"),
    median_arr_delay=("arr_delay", "median"),
    flight_count=("flight", "count")
).reset_index()

month_summary.sort_values("avg_arr_delay", ascending=False)

What it prints:

    month  avg_arr_delay  median_arr_delay  flight_count
6       7      16.711307              -2.0         29425
5       6      16.481330              -2.0         28243
11     12      14.870355               2.0         28135
3       4      11.176063              -2.0         28330
...

The output shows all 12 months; only the first four are shown here.

↑ Back to Lab Contents

Part 4: Aggregating at Two Levels: Carrier and Month

Business question: do some carriers have worse delays in certain months? Now one row should be one carrier in one month. To group by two columns, put both names in a list: groupby(["carrier", "month"]).

4.1 Group by carrier and month

carrier_month_delay = flights.groupby(["carrier", "month"]).agg(
    avg_arr_delay=("arr_delay", "mean"),
    flight_count=("flight", "count")
).reset_index()

carrier_month_delay.head()

4.2 Sort by average arrival delay

carrier_month_delay.sort_values("avg_arr_delay", ascending=False).head(10)

Light validation after aggregation

After a groupby(), run three quick checks. Each one catches a different mistake.

Check 1: the number of rows

carrier_month_delay.shape

Check 2: unique keys

carrier_month_delay[["carrier", "month"]].duplicated().sum()

Check 3: missing values

carrier_month_delay.isna().sum()

↑ Back to Lab Contents

Your Turn 3: A Route-Level Summary

Try first

One row should be one route: an origin and a dest together. Group by both, and give the result a new name such as route_summary.

  1. Create one row per origin–destination route with the number of flights, the average arrival delay and the average distance.
  2. Sort to find the busiest routes.
  3. Run a light validation check: does any origin–destination pair appear more than once?
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_summary = flights.groupby(["origin", "dest"]).agg(
    flight_count=("flight", "count"),
    avg_arr_delay=("arr_delay", "mean"),
    avg_distance=("distance", "mean")
).reset_index()

route_summary.sort_values("flight_count", ascending=False).head()

What it prints:

    origin dest  flight_count  avg_arr_delay  avg_distance
117    JFK  LAX         11262      -0.480599        2475.0
156    LGA  ATL         10263      11.322478         762.0
204    LGA  ORD          8857       1.833431         733.0
...
Show sample solution for the Your Turn 3 validation check

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_summary[["origin", "dest"]].duplicated().sum()

What it prints:

np.int64(0)

↑ Back to Lab Contents

Part 5: Wide and Long Data

Aggregation summarizes. Reshaping keeps the same values and changes only how they are arranged.

To go long to wide you pivot. To go wide back to long you “melt” the pivot table, with melt().

↑ Back to Lab Contents

Part 6: Wide to Long with melt()

Business question: a manager sends a small report with one column per month. How can you reshape it so each row is one carrier-month?

The next three cells only build that small wide “manager report”, so you have something to melt. Run them; you do not need to study the code yet.

monthly_carrier = flights.groupby(["carrier", "month"]).agg(
    avg_arr_delay=("arr_delay", "mean")
).reset_index()

monthly_carrier.head()
small_summary = monthly_carrier.query("carrier in ['AA', 'DL', 'UA'] and month in [1, 2, 3]")
small_summary

6.1 The manager's wide report

wide_delay = small_summary.pivot(
    index="carrier",
    columns="month",
    values="avg_arr_delay"
).reset_index()

wide_delay

6.2 Melt the wide report

long_delay = wide_delay.melt(
    id_vars="carrier",
    var_name="month",
    value_name="avg_arr_delay"
)

long_delay
The three arguments of melt()
ArgumentWhat it doesHere
id_varsColumns that stay as they are (identifiers)carrier
var_nameName of the new column that holds the old column headersmonth
value_nameName of the new column that holds the old cell valuesavg_arr_delay

↑ Back to Lab Contents

Your Turn 4: Practice melt()

Try first

Write the arguments yourself instead of copying them; that is the point of this exercise.

  1. Use melt() on wide_delay to create long_delay_practice, with carrier as the ID variable, delay_month as the variable name and average_delay as the value name.
  2. Light validation: how many rows should the long table have, based on the number of carriers and month columns?
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.

long_delay_practice = wide_delay.melt(
    id_vars="carrier",
    var_name="delay_month",
    value_name="average_delay"
)

long_delay_practice

What it prints:

  carrier delay_month  average_delay
0      AA           1       0.982379
1      DL           1      -4.404651
2      UA           1       3.175599
...
Show sample solution for the Your Turn 4 validation check

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.

expected_rows = wide_delay["carrier"].nunique() * (wide_delay.shape[1] - 1)
print("expected:", expected_rows, "| actual:", long_delay_practice.shape[0])

What it prints:

expected: 9 | actual: 9

wide_delay.shape[1] - 1 counts the month columns: all columns except carrier.

↑ Back to Lab Contents

Part 7: Long to Wide with pivot() and pivot_table()

Business question: a manager wants a compact comparison with one row per carrier and one column per month. You move from long back to wide.

7.1 pivot() turns the long table back into the report

wide_again = long_delay.pivot(
    index="carrier",
    columns="month",
    values="avg_arr_delay"
).reset_index()

wide_again
The three arguments of pivot()
ArgumentWhat it doesHere
indexWhat defines each row (like id_vars in melt)carrier
columnsWhich column's values become the new column headersmonth
valuesWhich column fills the cellsavg_arr_delay

7.2 pivot_table() gives the same table here

wide_with_pivot_table = small_summary.pivot_table(
    index="carrier",
    columns="month",
    values="avg_arr_delay",
    aggfunc="mean"
).reset_index()

wide_with_pivot_table

↑ Back to Lab Contents

Your Turn 5: A Wide Carrier-Month Table

Try first

Start from carrier_month_delay (Part 4). Decide what goes in index, columns and values.

  1. Use pivot_table() to create a wide table with one row per carrier, one column per month, and the average arrival delay as the values.
  2. Light validation: how many rows does your wide table have, and does that match the number of unique carriers?
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.

carrier_month_wide = carrier_month_delay.pivot_table(
    index="carrier",
    columns="month",
    values="avg_arr_delay",
    aggfunc="mean"
).reset_index()

carrier_month_wide

The output has 16 rows and 13 columns: carrier plus months 1 to 12.

Show sample solution for the Your Turn 5 validation check

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.

print("rows:", carrier_month_wide.shape[0])
print("unique carriers:", carrier_month_delay["carrier"].nunique())

What it prints:

rows: 16
unique carriers: 16

↑ Back to Lab Contents

Part 8: Light Validation Checklist

After any aggregation or reshape, ask five questions:

  1. What does one row mean now?
  2. Did the number of rows change in a way that makes sense?
  3. Are the intended keys unique?
  4. Did missing values appear or disappear?
  5. Do the column names still describe the values?

The notebook runs three of these checks on long_delay:

Repeated carrier-month pairs

long_delay[["carrier", "month"]].duplicated().sum()

Missing values

long_delay.isna().sum()

Number of unique carrier-month pairs

long_delay[["carrier", "month"]].drop_duplicates().shape[0]

↑ Back to Lab Contents

Part 9: Final Practice: Carrier-Month Dashboard Table

Complete independently

This practice is graded and intentionally not solved in the companion. Use the exact instructions in the notebook (Steps 1–4).

  1. Create carrier_month_summary with one row per carrier-month and four named columns: avg_departure_delay, avg_arrival_delay, flight_count and avg_distance.
  2. Create carrier_month_summary_sorted with the highest average arrival delay first.
  3. Check for duplicate carrier-month keys. The expected result is 0.
  4. Check missing values in carrier_month_summary.

What to check: the table has the same carrier-months as carrier_month_delay in Part 4, so the row count should match. The first rows of the sorted table rest on very few flights; mention that in your reflection. Replace None in Step 1, or the export cell will not save a file.

output_dir = Path("../data/processed")
output_dir.mkdir(parents=True, exist_ok=True)

if carrier_month_summary is None:
    print("Complete Final Practice Step 1 before exporting.")
else:
    module7_output_path = output_dir / "carrier_month_summary_module7.csv"
    carrier_month_summary.to_csv(module7_output_path, index=False)
    print(f"Saved {len(carrier_month_summary)} rows to: {module7_output_path}")

Final reflection

Write 3–5 sentences in the notebook: what one row means in carrier_month_summary; which transformation changed the row meaning the most; what validation evidence supports the table's structure; and how the table could support an operations decision.

↑ Back to Lab Contents

Part 10: Offline Assignment: Route Performance Summary

Complete independently

This assignment is intentionally not solved in the companion. Use the exact instructions in the notebook.

  1. Create route_performance with one row per origin–dest pair.
  2. Use named aggregation for the flight count, the average arrival delay and the average distance.
  3. Keep routes with at least 500 flights and sort from the highest to the lowest average arrival delay.
  4. Verify that route keys are unique and inspect missing values.
  5. Save data/processed/route_performance_module7.csv.

Before coding: say the workflow to yourself as group → summarize → filter → sort → check → save. Your Turn 3 already did the first two steps. Count the routes before and after the 500-flight filter: that difference is what the filter removed, and your interpretation should mention it.

Route summary interpretation

Write 4–5 sentences: what one row represents, how the 500-flight filter changes the scope, what validation evidence supports the table, and one decision the summary can support. Do not claim that the table explains the cause of delays.

↑ Back to Lab Contents

Common Problems

If a cell fails, read the last line of the error message first. Then find it below.

Still stuck? Post the error message and the cell number in the course Q&A discussion. Do not post your full answers.

↑ Back to Lab Contents

Save and Submit

  1. Complete every Your Turn, the final practice, the offline assignment and both reflections.
  2. Restart the kernel and run all cells from top to bottom. Confirm there are no errors.
  3. Confirm that both Module 7 files exist in data/processed: carrier_month_summary_module7.csv and route_performance_module7.csv.
  4. Save the notebook, then commit and push: git add ., git commit -m "Complete Module 7 aggregation and reshaping lab", git push. In Codespaces you can also use the Source Control panel.
  5. Submit your repository link in Canvas. The Module 8 notebook is in the same repository.

↑ Back to Lab Contents

Lab Wrap-Up

You used four tools as one connected pattern:

Choose the shape that serves your purpose. A compact table for a report is usually wide; data for a chart is usually long. Your project report should include at least a few charts, so you will want long tables for those. Module 8 builds on this by joining tables with merge(), where the same checks on row counts and keys become even more important.