Summarizing Your Data

Summaries by Category


Learning Objectives

  • You can find the unique values in a column of a list of rows using an ordinary loop.
  • You can compute a per-category summary by writing one outer loop over the unique categories and one inner loop over the data.
  • You can return several pieces of information per category — count, total, average — from a single inner loop.

The previous chapter answered “how much, on the subset I care about?” The natural next question is “how much, for each category?” — how many minutes on math, on physics, on history, on every subject in the log.

In this chapter, you learn to write summaries by category with the existing looping and list tools. You get to write code that works, but is not as clean as it could be. In the next part, we’ll learn about dictionaries and see how to do this more elegantly.

Summary Information Per Subject

Given a list of sessions, you want one row of summary information per subject:

History: 1 session, 30 minutes, average 30.0 minutes
Math: 3 sessions, 130 minutes, average 43.3 minutes
Physics: 1 session, 90 minutes, average 90.0 minutes

The challenge is that the number of subjects is not known in advance. You cannot just rely on creating total_math, total_physics, total_history as separate variables as the data could have other subjects too. Each subject needs its own counter and accumulator, but you cannot create new variables on the fly inside a loop.

Two Passes: Find Unique, Then Summarise Each

The way to create summaries by category with loops and lists is to do it in two passes: first find unique values, then summarise each.

The first pass walks through the sessions once and collects the unique subjects — the list of every subject that appears, with no repeats:

subjects_seen = []
for subject, minutes, day in sessions:
if subject not in subjects_seen:
subjects_seen.append(subject)

The second pass uses the filtered-total pattern from the previous chapter, once per subject:

for subject in subjects_seen:
total = 0
for row_subject, minutes, day in sessions:
if row_subject == subject:
total += minutes
print(f"{subject.title()}: {total} minutes")

The outer loop walks through every unique subject. For each one, the inner loop walks through the entire sessions list and totals the minutes that belong to that subject.

Two small things to point out. The inner-loop variable is named row_subject, not subject — because subject is already the name from the outer loop, and reusing it would shadow the outer one. And .title() formats the lowercase canonical name for display.

Two loops, two names

0 / 5 points

In a grouped-total function, the outer loop chooses one category and the inner loop scans all rows. Why is it helpful to use different variable names in the two loops?

Wrapping the pattern in a function:

def totals_by_subject(sessions):
"""Return a list of [subject, total_minutes] rows.
Returns [] for empty input.
"""
subjects_seen = []
for subject, minutes, day in sessions:
if subject not in subjects_seen:
subjects_seen.append(subject)
result = []
for subject in subjects_seen:
total = 0
for row_subject, minutes, day in sessions:
if row_subject == subject:
total += minutes
result.append([subject, total])
return result

The function returns a list of [subject, total] rows. The summary-contract table says collections of summaries return [] on empty input, and this function honours that without any special-case code — the first loop produces an empty subjects_seen, the second loop does not run, and the function returns [].

A small test using the fixture from the overview:

sessions = [
["math", 45, "2026-05-10"],
["physics", 90, "2026-05-10"],
["history", 30, "2026-05-11"],
["math", 60, "2026-05-11"],
["math", 25, "2026-05-12"],
]
assert ["history", 30] in totals_by_subject(sessions)
assert ["math", 130] in totals_by_subject(sessions)
assert ["physics", 90] in totals_by_subject(sessions)
assert totals_by_subject([]) == []
print("Tests passed.")

Route totals with lists only

0 / 5 points

Arrange the lines into a function that returns route totals as rows like ["A", 42]. Use only lists and loops.

One Inner Loop, Several Summaries

The previous section computed one number per subject — the total. But the inner loop walks every matching session anyway, and once you are walking them, computing several summaries at once costs almost nothing extra.

A richer function returns count, total, and average per subject:

def summaries_by_subject(sessions):
"""Return [subject, count, total_minutes, average_minutes] rows.
Returns [] for empty input.
"""
subjects_seen = []
for subject, minutes, day in sessions:
if subject not in subjects_seen:
subjects_seen.append(subject)
result = []
for subject in subjects_seen:
count = 0
total = 0
for row_subject, minutes, day in sessions:
if row_subject == subject:
count += 1
total += minutes
average = total / count
result.append([subject, count, total, average])
return result

The inner loop now updates both count and total in the same if — the same pattern as average_minutes_for_subject from the previous chapter. The average is computed after the inner loop completes.

Notice that this function does not check if count == 0 before dividing. A subject only appears in subjects_seen because there was at least one session for it, so the count is guaranteed to be at least 1. The empty case for the whole function is still [] (no subjects at all means an empty result); within a single subject’s calculation, count cannot be zero.

A test:

assert ["history", 1, 30, 30.0] in summaries_by_subject(sessions)
assert ["math", 3, 130, 43.333333333333336] in summaries_by_subject(sessions)
assert ["physics", 1, 90, 90.0] in summaries_by_subject(sessions)
assert summaries_by_subject([]) == []
print("Tests passed.")

The float 43.333333333333336 is what 130 / 3 actually produces. When displayed in the dashboard, this will be formatted to one decimal place (43.3); the function itself returns the full value, leaving formatting to whoever prints it.

Library section summaries

0 / 15 points

Write a function named library_section_summaries(loans).

Each row in loans has this shape:

[section, days, title]

The function must return one summary row per section:

[section, count, total, average]

Requirements:

  • use the provided unique_sorted_sections(loans) helper to get the section names,
  • use only lists and loops,
  • do not use dictionaries,
  • sort the result alphabetically by section,
  • return [] for an empty input list,
  • count all loans in each section,
  • total the loan days in each section,
  • compute the average loan days in each section,
  • use None as the average if a section has count 0,
  • do not print anything,
  • and do not use input(...).

Computational Look at the Two-Pass Pattern

Take a moment to look at summaries_by_subject and notice what it actually does, computationally.

For a list of N sessions covering K distinct subjects, the outer loop runs K times. For each of those K iterations, the inner loop walks through all N sessions. The function does roughly K x N work.

For the study log, this does not matter. You will have a few hundred sessions, maybe a dozen subjects, and the program will run in well under a millisecond. The pattern is not slow in any sense that affects you.

Despite the small data size, it is worth understanding the pattern. The code is doing more work than it needs to, because it does not have a way to say “a mapping from each subject to its running summary.” It has to find the unique subjects first, and then for each one, walk through the data again to compute the summary.

Data structures and algorithm courses look in more detail at the computational cost of different patterns. The two-pass pattern is a common one, and it is worth seeing how it works in practice before learning other patterns.

Variants to Practise

Once the shape is comfortable, several other useful summaries fall out of small edits to it. Each is the same outer-loop-plus-inner-loop pattern, with different lines inside.

Count of sessions per subject changes the inner-loop work to count += 1. Longest session per subject tracks the maximum within the matching rows. Earliest date per subject tracks the minimum date. The shape is the same every time; the only thing that varies is what you track and what you return.

As an example, if you take totals_by_subject, you can turn it into longest_session_by_subject that returns [subject, longest_minutes] with relative ease. The empty case is still []. The key change is that the inner loop would track a running maximum instead of a running sum — you can start with longest = None and then change it when iterating over the values if it is None or the value that is being iterated over is larger; the function longest_session_by_subject would be almost identical to totals_by_subject, with two or three lines changed.

You can try the same idea in a different category/value domain below.

Highest value by category

0 / 15 points

Write a function named highest_value_by_category(rows).

Each row in rows has this shape:

[category, value, label]

The function must return one row for each category:

[category, highest_value]

Requirements:

  • use only lists and loops,
  • do not use dictionaries,
  • keep the categories in the order where they first appear in the input,
  • return [] when the input list is empty,
  • keep the largest integer value seen for each category,
  • do not print anything,
  • and do not use input(...).

Example:

rows = [
["north", 12, "morning"],
["south", 8, "morning"],
["north", 15, "evening"],
]

highest_value_by_category(rows) should return:

[["north", 15], ["south", 8]]

Check Your Understanding

  1. Why does summaries_by_subject need to find the unique subjects first, in a separate pass?
  2. What would go wrong if the inner-loop variable were named subject instead of row_subject, as follows?
for subject in subjects_seen:
count = 0
total = 0
for subject, minutes, day in sessions:
if subject == subject:
count += 1
total += minutes
average = total / count
result.append([subject, count, total, average])
  1. For 500 sessions across 10 subjects, roughly how many “look at a session” operations does summaries_by_subject do?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0