Structuring Your Data

Dictionaries for Counting and Grouping


Learning Objectives

  • You can count occurrences of each distinct value using a dictionary.
  • You can accumulate a running total per category in a single pass.
  • You can recognise the “build a dictionary as you go” pattern and apply it to grouping problems.
  • You can compare using dictionaries to the two-pass list approach, and explain why this is shorter and faster than the two-pass list approach.

In the Summaries by Category chapter of the last part, you wrote a totals_by_subject function that first collected the distinct subjects in a list, and then looped over that list to total the minutes for each subject.

Here is the shape of the function:

def totals_by_subject(sessions):
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 does two passes. The first walks every session to collect the distinct subjects. The second walks every session again, once per subject, to total the minutes. For a hundred sessions across ten subjects, the inner loop runs a thousand times.

The reason it had to be done this way is that a list has no way to say “the running total for math” as there is no slot named math.

A dictionary has exactly the feature this problem needs, because it can say “the running total for math is totals["math"]”. The dictionary can hold the running total for each subject under a key of that subject’s name. When you see a session, you look up its subject in the dictionary and add to that slot.

Counting With a Dictionary

Start with the simplest version of the problem: counting. How many sessions are there for each subject?

The pattern is to build a dictionary as you walk the data once. The keys are subjects; the values are running counts.

def count_by_subject(sessions):
counts = {}
for subject, minutes, day in sessions:
counts[subject] = counts.get(subject, 0) + 1
return counts

Let’s walk through it. counts starts empty. For each session, you look up its subject in counts to get the current count. If the subject is not a key — it has not been seen before — .get() returns 0, which is the right starting point for counting. You add one to that count and store it back under the same key.

Starting a running count

0 / 5 points

In counts[kind] = counts.get(kind, 0) + 1, what is the role of 0?

The dictionary holds the running count for each subject as you go. When you are done, it has the final count for each subject.

For the usual fixture:

sessions = [
["math", 45, "2024-03-10"],
["physics", 90, "2024-03-10"],
["history", 30, "2024-03-11"],
["math", 60, "2024-03-11"],
["math", 25, "2024-03-12"],
]
print(count_by_subject(sessions))
# {'math': 3, 'physics': 1, 'history': 1}

One pass. No subjects_seen list, no inner loop, no re-scanning. The dictionary is the mapping from subject to count, built up as you go.

The Check-Then-Set Pattern

The above code uses the “check-then-set” pattern: check if the key is there, and if it is, add to it; otherwise start it. This is a common pattern when building a dictionary as you go. The .get() method is a convenient way to shorten this pattern, as you will see in the next section.

You could also write the getting and setting with two lines:

count = counts.get(subject, 0)
counts[subject] = count + 1

The one-liner is more common, but the two-liner can be clearer when the contribution is more complex than + 1.


Count borrowed genres

0 / 5 points

Arrange the lines into a function that counts how many borrowed books there are for each genre.

Each book is a dictionary with a "genre" key. The function must return a dictionary such as {"history": 2, "fiction": 1}.


If/Else Instead of Check-Then-Set

An alternative to the check-then-set pattern is to use an if/else:

def count_by_subject(sessions):
counts = {}
for subject, minutes, day in sessions:
if subject in counts:
counts[subject] += 1
else:
counts[subject] = 1
return counts

Or, just an if that sets the count to 0 if the key is missing, and then adds one:

def count_by_subject(sessions):
counts = {}
for subject, minutes, day in sessions:
if subject not in counts:
counts[subject] = 0
counts[subject] += 1
return counts

Totalling With a Dictionary

Counting adds 1 each time. Totalling adds the minutes each time. That is the only change:

def totals_by_subject(sessions):
totals = {}
for subject, minutes, day in sessions:
totals[subject] = totals.get(subject, 0) + minutes
return totals

Compare this to the two-pass version at the top of the chapter. Same result, but the function went from twelve lines with a nested loop to four lines with a single pass:

print(totals_by_subject(sessions))
# {'math': 130, 'physics': 90, 'history': 30}

Math’s total is 45 + 60 + 25 = 130, accumulated across the three times "math" came up as the loop walked the sessions once. The dictionary held the running total under the key "math" the whole way through.

Building Several Summaries at Once

In Summaries by Category, the function summaries_by_subject returned count, total, and average together. The function required first collecting the subjects, then looping once per subject to compute the count, the total, and the average.

The dictionary version can do all of that in one pass, because it can hold multiple running values for each subject at the same time. The count and total are accumulated together in the first loop, and the average is computed at the end once the counts and totals are final:

def summaries_by_subject(sessions):
counts = {}
totals = {}
for subject, minutes, day in sessions:
counts[subject] = counts.get(subject, 0) + 1
totals[subject] = totals.get(subject, 0) + minutes
result = {}
for subject in counts:
result[subject] = {
"count": counts[subject],
"total": totals[subject],
"average": totals[subject] / counts[subject],
}
return result

The first loop accumulates counts and totals in one pass. The second loop — which runs once per subject, not once per session — assembles the final summary. Notice that the inner value is itself a dictionary: result["math"] is {"count": 3, "total": 130, "average": 43.33...}. A dictionary whose values are themselves dictionaries is a normal and useful shape, and the next chapter looks at it more closely.

summaries = summaries_by_subject(sessions)
print(summaries["math"])
# {'count': 3, 'total': 130, 'average': 43.333333333333336}

The average is computed only at the end, once each subject’s count and total are final — never inside the accumulation loop, where the count is still partial.

The Pattern in This Chapter

Every example in this chapter is the same shape:

result = {}
for item in data:
key = something_about(item)
result[key] = result.get(key, starting_value) + contribution(item)

Start with an empty dictionary. Walk the data once. For each item, work out which key it belongs to, and fold its contribution into that key’s running value. For counting, the contribution is 1 and the starting value is 0. For totalling, the contribution is the minutes.

This is one of the most common patterns in all of programming — grouping a pile of records by some key and summarising each group. You will reach for it constantly. It is worth recognising the moment a problem says “per subject”, “per day”, “per user”, “for each category”: that is a dictionary accumulation, one pass, keyed by the category.

Stats from file

0 / 20 points

Write a program that reads product ratings from data.csv and prints the average rating for each product.

The file has this header:

category,product,rating

Required helpers:

HelperContract
load_ratings(path)read the CSV file at path and return rows shaped as [category, product, rating], where rating is a float
average_ratings_by_product(rows)return a dictionary where each key is a product and each value is that product’s average rating

Requirements:

  • use the csv module and csv.reader,
  • open files with encoding="utf-8",
  • skip the header row,
  • convert ratings to floats,
  • print one line per product,
  • print products in alphabetical order,
  • format each average with one decimal place (f"{average:.1f}"),
  • do not use input(...),
  • and at the bottom of the program, read data.csv and print the product averages.

For this data.csv:

category,product,rating
school,Notebook,5
home,Desk Lamp,4
school,Notebook,4
home,Desk Lamp,5

the program should print:

Desk Lamp: 4.5
Notebook: 4.5

Dictionaries Versus Two-Pass for Grouping

The two-pass list version that we used in the previous part was not wrong. It produced the right answer. It, however, had two costs that the dictionary version sheds.

  • It was slower in a way that grows: re-scanning all the sessions once per subject means the work multiplies as both the data and the number of subjects grow. The dictionary version touches each session once, full stop.

  • It was harder to read. The two-pass version made you hold “we collect the subjects first, then loop again for each one” in your head. The dictionary version reads straight down: for each session, add its minutes to its subject’s total.

The next chapter looks at the richer shapes you can build once values can themselves be lists or dictionaries.

Check Your Understanding

  1. In the line counts[subject] = counts.get(subject, 0) + 1, what is the role of the 0?
  2. The two-pass list version of totals_by_subject was slower “in a way that grows”. Grows with what?
  3. How would you change the totalling loop so that it counts sessions per subject instead of summing minutes?
  4. Why is the average in summaries_by_subject computed in the second loop, rather than inside the first accumulation loop?
  5. A problem asks you to report the number of orders per customer. What data shape and what pattern does that call for?

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