Skip to content

Beginner: data foundations · Python for data · topic 4 of 62 · 30 min

Counting and grouping: collections and statistics

Explanation

Two questions come up in almost every analysis: "how many of each?" and "what is the total per group?". collections.Counter answers the first, and defaultdict makes grouping easy for the second. The statistics module gives mean, median and mode in one line.

Why counting and grouping matter

Most business questions are counts or totals by group. How many orders came from each city? What is the total sale per item? Which complaint type is most common? In SQL this is GROUP BY, in pandas it is groupby, and in Excel it is a pivot table. Learning it in plain Python first shows you exactly what those tools do.

Counter: how many of each

Counter, from the collections module, takes a list and counts how many times each value appears. It behaves like a dict from value to count. Asking for a value that never appeared gives 0, not a KeyError.

python
from collections import Counter

cities = ["Pune", "Delhi", "Pune", "Mumbai", "Pune", "Delhi"]
c = Counter(cities)
print(c)                  # Counter({'Pune': 3, 'Delhi': 2, 'Mumbai': 1})
print(c["Pune"], c["Goa"])  # 3 0
print(c.most_common(2))   # [('Pune', 3), ('Delhi', 2)]
most_common(k) gives the k most frequent values with their counts, biggest first.

most_common breaks ties by the order in which values were first seen. If you need a fixed rule, such as alphabetical order for ties, sort yourself: sorted(c.items(), key=lambda kv: (-kv[1], kv[0])). The minus sign on the count sorts the biggest count first.

Grouping with defaultdict

Grouping means putting rows into buckets by a key, such as city. With a normal dict you must check whether the bucket exists before adding to it. defaultdict(list) creates an empty list automatically the first time you use a new key, so the loop stays short.

python
from collections import defaultdict

orders = [("Pune", 250), ("Delhi", 900), ("Pune", 120), ("Chennai", 640), ("Delhi", 300)]
by_city = defaultdict(list)
for city, amount in orders:
    by_city[city].append(amount)
print(dict(by_city))   # {'Pune': [250, 120], 'Delhi': [900, 300], 'Chennai': [640]}
Each city key gets a list of its amounts. dict() is only used to print it plainly.

Group, then aggregate

To aggregate means to squash a group of values into one number: a sum, a count, an average, a maximum. Group first, then aggregate each bucket. For a running total you can also skip the lists and use defaultdict(int), which starts every new key at 0.

python
totals = {city: sum(amounts) for city, amounts in by_city.items()}
print(totals)   # {'Pune': 370, 'Delhi': 1200, 'Chennai': 640}

totals2 = defaultdict(int)
for city, amount in orders:
    totals2[city] += amount
print(dict(totals2) == totals)   # True
Both give the same totals. Keep lists when you need several aggregates (sum, count, average) from the same group.
Think of it like this: Grouping is like sorting the post at a post office into pigeonholes by PIN code. Aggregating is counting how many letters are in each pigeonhole.

A preview of the statistics module

Python's statistics module gives common summaries in one call. The mean is the ordinary average, the median is the middle value after sorting, and the mode is the most common value. Module 2 explains when each one is the right choice and teaches you to compute them by hand.

python
import statistics

marks = [72, 85, 85, 90, 64]
print(statistics.mean(marks))     # 79.2
print(statistics.median(marks))   # 85
print(statistics.mode(marks))     # 85
Sorted, the marks are 64, 72, 85, 85, 90, so the middle one is 85, and 85 also appears most often.

The same idea in pandas and SQL

python
import pandas as pd

df = pd.DataFrame({"city": ["Pune", "Delhi", "Pune", "Chennai", "Delhi"],
                   "amount": [250, 900, 120, 640, 300]})
print(df["city"].value_counts())            # Pune 2, Delhi 2, Chennai 1
print(df.groupby("city")["amount"].sum())   # Chennai 640, Delhi 1200, Pune 370
Shown only. value_counts is pandas' Counter; groupby(...).sum() is group-then-aggregate. In SQL: SELECT city, SUM(amount) FROM orders GROUP BY city;

Common mistakes

  • KeyError when adding to a normal dict: totals[city] += amount fails for a new city. Fix: use defaultdict(int), or totals[city] = totals.get(city, 0) + amount.
  • Counting text that is not clean: "Pune", "pune" and "Pune " become three different keys. Fix: clean first, for example city.strip().title().
  • Trusting tie order from most_common when the order matters. Fix: sort with an explicit key such as (-count, name).
  • Calling statistics.mean on an empty list, which raises StatisticsError. Fix: check that the list has values first.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Counting and grouping: collections and statistics · 1/7

Count and group

In this lesson you will answer the two most common data questions: how many of each, and what is the total per group.

Prefer a video by a teacher? Find free videos on “python collections Counter defaultdict group by tutorial” on YouTube ↗

In real life: Complaint types at a bank call centre

A bank's customer care team logs each complaint with a type, such as "UPI failed", "card blocked" or "KYC update". Every week an analyst counts the types, finds the top three, and totals the refund amounts per branch. The sample below is made up, but the method is exactly what such a weekly report does.

python
from collections import Counter, defaultdict

complaints = ["UPI failed", "card blocked", "UPI failed", "KYC update", "UPI failed", "card blocked"]
print(Counter(complaints).most_common(2))

refunds = [("Andheri", 500), ("Thane", 1200), ("Andheri", 300)]
per_branch = defaultdict(int)
for branch, amount in refunds:
    per_branch[branch] += amount
print(dict(per_branch))
Output: [('UPI failed', 3), ('card blocked', 2)], then {'Andheri': 800, 'Thane': 1200}.

Practice (0/3 done)

Task 1 · Code · Python

Write count_values(items) that returns a plain dict mapping each value to how many times it appears. Example: count_values(["a", "b", "a"]) returns {"a": 2, "b": 1}.

Task 2 · Code · Python

Write total_per_group(rows, group_key, value_key) that returns a dict mapping each group to the sum of value_key in that group. Example: rows of {"city": ..., "amount": ...} grouped by "city" summing "amount".

Task 3 · Code · Python

Write top_k(items, k) that returns the k most common values as a list of (value, count) tuples, biggest count first. When counts tie, put the values in alphabetical order. If there are fewer than k distinct values, return them all.

Quick quiz

Question 1

1.c = Counter(["a", "b", "a"]). What is c["z"]?

Question 2

2.Why use defaultdict(list) for grouping?

Question 3

3.What does "group, then aggregate" mean?

Question 4

4.Counting cities gives {"Pune": 3, "pune": 1, "Pune ": 2}. What is the best fix?

Question 5

5.marks = [40, 50, 90]. What is statistics.median(marks)?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

c = Counter(["a", "b", "a"]). What is c["z"]?

Answer: 0

A Counter returns 0 for values it has never seen.

Why use defaultdict(list) for grouping?

Answer: It creates an empty list automatically for a new key

Without it, you must check whether a key exists before appending.

What does "group, then aggregate" mean?

Answer: Put rows into buckets by a key, then turn each bucket into one number such as a sum

It is the idea behind SQL GROUP BY, pandas groupby and Excel pivot tables.

Counting cities gives {"Pune": 3, "pune": 1, "Pune ": 2}. What is the best fix?

Answer: Clean the text first, for example city.strip().title()

The three keys are the same city written differently. Cleaning merges them into Pune: 6.

marks = [40, 50, 90]. What is statistics.median(marks)?

Answer: 50

Sorted, the middle value of 40, 50, 90 is 50. The mean would be 60.

With d = defaultdict(int), what happens on d["x"] += 1 when "x" is not a key yet?Related

Answer: d["x"] becomes 1

defaultdict(int) first creates d["x"] = 0, then adds 1.

From collections and itertools

What does SELECT city, SUM(amount) FROM orders GROUP BY city return?Related

Answer: One row per city with its total

GROUP BY city makes one group per city, and SUM runs on each group.

From Counting and totals: aggregate functions

students has 8 rows in 5 different cities. How many rows does SELECT city, COUNT(*) FROM students GROUP BY city return?Related

Answer: 5

GROUP BY gives one row per group, and there are 5 cities.

From GROUP BY

In right-skewed data such as house prices, how do the mean and median usually compare?Related

Answer: The mean is bigger

The long tail of very expensive houses pulls the mean above the median.

From Types of data, and the centre: mean, median, mode

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Python for data” before its exam: Writing data code that others can trust.
  2. Module 1 exam: Python for data · 14 questions · 20 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Statistics made simple. 62 topics left in the course before the final exam.

← Previous: Reading CSV and JSON with the standard library