Summarizing Your Data

Summaries by Day


Learning Objectives

  • You can compute per-day totals using the same two-pass pattern as per-subject totals, with the date as the category.
  • You can return the daily totals in chronological order using ISO date strings.
  • You can select the heaviest day with a “best so far” loop, honouring an explicit tie policy.
  • You can compose date-window selection with daily totals to produce “the last seven days.”

The per-subject pattern from the Summaries by Category chapter was: find the distinct subjects, then loop again to summarise each one. The same shape works when the category is the date instead of the subject. How many minutes did I study each day? is the same question as how many minutes per subject?, just with a different field.

This chapter applies the pattern, picks out the heaviest day with the “best so far” loop from the previous chapter, and composes the result with date windows.

Per-Day Totals

The function follows the Summaries by Category shape exactly, with day_string where subject used to be:

def totals_by_day(sessions):
"""Return [date_string, total_minutes] rows, sorted by date ascending.
Returns [] for empty input.
"""
days_seen = []
for subject, minutes, day in sessions:
if day not in days_seen:
days_seen.append(day)
days_seen.sort()
result = []
for day in days_seen:
total = 0
for subject, minutes, row_day in sessions:
if row_day == day:
total += minutes
result.append([day, total])
return result

The same two-pass approach, the same outer-loop-over-categories, the same inner-loop-over-sessions.

The extra bit is the days_seen.sort() step. Now, the function returns its rows in date order, which is what every caller wants. The sort is a small extra cost on top of the two-pass pattern, but it gives you the right order for free — you do not have to write a more complicated loop to insert rows in the right place.

A test using the same fixture as before:

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 totals_by_day(sessions) == [
["2026-05-10", 135],
["2026-05-11", 90],
["2026-05-12", 25],
]
assert totals_by_day([]) == []
print("totals_by_day tests passed.")

The arithmetic: May 10 has 45 + 90 = 135 minutes. May 11 has 30 + 60 = 90. May 12 has just 25. Output is in date order because of the sort.

The Heaviest Day

Once totals_by_day exists, “which day had the most minutes?” is the “best so far” loop from the previous chapter, applied to the daily totals:

def heaviest_day(sessions):
"""Return the [date_string, total_minutes] row for the heaviest day.
Ties are broken by the earliest tied day.
Returns None for empty input.
"""
totals = totals_by_day(sessions)
if len(totals) == 0:
return None
best = totals[0]
for row in totals[1:]:
if row[1] > best[1]:
best = row
return best

The function calls totals_by_day (which already returns rows sorted by date). If the result is empty, the function returns None. Otherwise the “best so far” loop walks the rows and keeps the largest one.

A test:

assert heaviest_day([]) is None
assert heaviest_day(sessions) == ["2026-05-10", 135]

And a test for the tie behaviour:

tied = [
["math", 60, "2026-05-10"],
["math", 60, "2026-05-11"],
]
assert heaviest_day(tied) == ["2026-05-10", 60]

Recent Daily Totals: Composing Windows and Days

To find recent daily totals, you can compose the sessions_between function from Working With Dates and Date Windows with totals_by_day:

from datetime import date, timedelta
def recent_daily_totals(sessions, today=None):
"""Return totals_by_day for the last seven days ending at today (inclusive).
Returns [] for empty input or no sessions in the window.
"""
if today is None:
today = date.today()
start = today - timedelta(days=6)
recent = sessions_between(sessions, start, today)
return totals_by_day(recent)

The function is a few lines of work, all of which are already-tested pieces. sessions_between filters the sessions to the date window. totals_by_day summarises them.

A test:

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"],
["math", 20, "2026-02-15"], # well outside the window
]
today = date.fromisoformat("2026-05-12")
assert recent_daily_totals(sessions, today) == [
["2026-05-10", 135],
["2026-05-11", 90],
["2026-05-12", 25],
]

The row from the past is correctly excluded — it falls outside the seven-day window. The remaining rows are grouped by day and totalled.

The next exercise transfers recent-window grouping to route logs, where the category is not the date but the filter still depends on dates.

Weekly minutes by route

0 / 15 points

Write a function named weekly_minutes_by_route(logs, today=None).

Each row in logs has this shape:

[route, minutes, day]

The function must return route totals for today and the previous six days:

[route, total_minutes]

Requirements:

  • use the provided logs_between(logs, start, end) helper to filter by date window,
  • use date.today() when today is None,
  • include both today and the date six days before today,
  • skip rows outside the seven-day window,
  • group totals by route using lists and loops only,
  • do not use dictionaries,
  • sort result rows alphabetically by route,
  • return [] when no rows are inside the window,
  • do not print anything,
  • and do not use input(...).

A Note on the Repeated Pattern

This is the third time you have written code with this shape: the per-subject totals, the per-subject summaries, and now the per-day totals. Each one finds the unique values of a category, then walks the data once per category to summarise.

Take a moment and look at what those three functions have in common. They all need a place to map each category to a running summary. Lists do not give them one, so the code creates it — with the unique-values list as the keys and the inner loop as the lookup. Every variant of this pattern pays the same cost: an inner loop that almost always runs and almost always does the same thing.

Putting It Together

The functions in this chapter feed directly into the project’s dashboard:

totals_by_day(sessions) # for any per-day analysis
heaviest_day(sessions) # for "Heaviest day:" line
recent_daily_totals(sessions, today) # for "Recent days:" block

Each one is small. Each one is built from the date and sorting tools you already have, applied with patterns you have seen before. None of them needs anything beyond lists, ordinary loops, and the standard built-ins.

Check Your Understanding

  1. Why does totals_by_day sort days_seen before the second pass?
  2. The tie policy for heaviest_day says “earliest tied day wins.” Where in the code is this policy actually enforced? Two things work together — what are they?
  3. What does recent_daily_totals do when no sessions fall inside the seven-day window? How does this follow from the functions it composes?
  4. Why do recent_daily_totals and total_in_last_seven_days both take today=None? What does this give you that hard-coding date.today() does not?

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