Reproducing Your Study Log Summaries with pandas
Learning Objectives
- You can load the
StudyLogfrom Structuring Your Data and turn its sessions into a DataFrame. - You can reproduce overall and per-subject summaries with pandas.
- You can preserve the complete class-based dashboard while changing its calculation layer.
- You can keep JSON loading, object serialization, and pandas summary logic separate.
- You can test pandas helpers with small inline
Sessionobjects.
This chapter is the project checkpoint for this part. The goal is to reproduce the study log summaries with pandas.
This is not a new product feature in the way file persistence was. The study log already had summaries. The change is the implementation: instead of writing every summary with loops over Session objects, you will move the summary calculations into pandas helpers.
Starting Point
This checkpoint starts from the released study log in Refactoring the Study Log with Classes. The file on disk is still a JSON list of dictionaries shaped like this:
{"subject": "math", "minutes": 45, "date": "2026-05-10"}Inside the program, however, the class-based loader rebuilds those dictionaries as Session objects inside a StudyLog object:
log = load_study_log("sessions.json")sessions = log.sessionsAfter that, sessions is a list of Session objects. Each object has subject, minutes, and date attributes, and each object can be converted back to a plain dictionary with session.to_dict().
Required Helper: sessions_dataframe
Write a helper that turns the log’s Session objects into a DataFrame:
import pandas as pd
def sessions_dataframe(sessions): records = [] for session in sessions: records.append(session.to_dict())
df = pd.DataFrame(records, columns=["subject", "minutes", "date"]) df["date"] = pd.to_datetime(df["date"]) return dfThe loop converts each Session object to the same plain dictionary shape that is stored in JSON. The columns=[...] argument keeps the expected column order explicit, even when there are no sessions. The date conversion makes the date column ready for comparisons and date windows.
Test this helper with a tiny list of Session objects before connecting it to your real file:
sample = [ Session("math", 45, "2026-05-10"), Session("history", 30, "2026-05-11"),]
df = sessions_dataframe(sample)print(df)print(df.dtypes)Build the Summary Helpers in Layers
The checkpoint uses pandas_ at the start of calculation-helper names so they remain distinct from the existing StudyLog methods. Start with total minutes and the complete per-subject summary:
def pandas_total_minutes(df): return int(df["minutes"].sum())
def pandas_summaries_by_subject(df): if len(df) == 0: return {}
grouped = df.groupby("subject")["minutes"].agg( ["count", "sum", "mean"] ) summaries = {} for subject, row in grouped.iterrows(): summaries[subject] = { "count": int(row["count"]), "total": int(row["sum"]), "average": float(row["mean"]), } return summariesThe helpers use pandas internally but return plain Python values. That keeps the StudyLog methods, dashboard formatting, and JSON boundaries simple.
Required Tests
Before writing these helpers into the full study log, test them with inline data:
sample = [ Session("math", 45, "2026-05-08"), Session("history", 30, "2026-05-09"), Session("math", 60, "2026-05-15"),]
df = sessions_dataframe(sample)
assert pandas_total_minutes(df) == 135assert pandas_summaries_by_subject(df) == { "history": {"count": 1, "total": 30, "average": 30.0}, "math": {"count": 2, "total": 105, "average": 52.5},}Also test the empty case:
empty = sessions_dataframe([])assert pandas_total_minutes(empty) == 0assert pandas_summaries_by_subject(empty) == {}These tests are not busywork. Pandas code can produce results with the right-looking shape but the wrong meaning. Small fixed examples let you check the meaning directly.
Date-Window Helper
For recent sessions:
def recent_sessions(df, today_text): today = pd.Timestamp(today_text) start = today - pd.Timedelta(days=6) mask = (df["date"] >= start) & (df["date"] <= today) return df[mask]This returns a DataFrame because the result is still table-shaped: a filtered set of rows.
If you want to inspect it while testing, iterate over the rows:
for row in recent_sessions(df, "2026-05-15").itertuples(): print(row.subject, row.minutes, row.date.strftime("%Y-%m-%d"))The .itertuples() method is a pandas row-iteration helper. It gives you one row at a time, with fields you can access by name, such as row.subject and row.minutes.
The strftime("%Y-%m-%d") call converts a pandas timestamp back into a plain date string for display.
Test the boundaries of the window. If today_text is "2026-05-15", the included window starts on "2026-05-09". A session on May 9 should be included. A session on May 8 should not. A session on May 15 should be included. A session on May 16 should not.
These four rows catch most off-by-one errors:
sample = [ Session("old", 10, "2026-05-08"), Session("start", 20, "2026-05-09"), Session("today", 30, "2026-05-15"), Session("future", 40, "2026-05-16"),]The recent helper should return only "start" and "today".
Ranking Game Scores for Plain Output
Suppose a game stores one row per score with player, score, and played_on columns. You can combine stable row sorting, date conversion, and record conversion in one helper:
def top_scores(df, n=3): rows = df.sort_values( "score", ascending=False, kind="stable" ).head(n) dates = pd.to_datetime(rows["played_on"]) records = rows.to_dict("records") for record, value in zip(records, dates): record["played_on"] = value.strftime("%Y-%m-%d") return recordstop_scores returns a list of dictionaries because the result is a small set of rows that another part of a program can print or serialize as JSON. The stable sort keeps input order when two players have the same score. The loop formats each converted date as a plain string. The "records" argument means “make one dictionary per row.”
Top sessions with pandas
0 / 10 points
Define a function named top_sessions(df, n).
df is a pandas DataFrame with columns subject, minutes, and date.
The tests pass a DataFrame shaped like this:
df = pd.DataFrame([ {"subject": "math", "minutes": 45, "date": "2026-05-10"}, {"subject": "history", "minutes": 30, "date": "2026-05-11"}, {"subject": "programming", "minutes": 90, "date": "2026-05-12"}, {"subject": "math", "minutes": 60, "date": "2026-05-13"},])Convert the selected date values with pd.to_datetime, then return a regular Python list of dictionaries for the n longest sessions, sorted from longest to shortest. Format each returned date as an ISO YYYY-MM-DD string.
If two sessions have the same number of minutes, keep those tied rows in their original input order, including when a tie crosses the n-row cutoff. Use a stable pandas sort, such as sort_values(..., kind="stable"), to make that rule explicit.
Example return value for top_sessions(df, 2):
[ {"subject": "programming", "minutes": 90, "date": "2026-05-12"}, {"subject": "math", "minutes": 60, "date": "2026-05-13"},]Use pandas sort_values inside the function, and convert the sorted rows back to dictionaries with .to_dict("records"). Format converted dates by iterating over them and calling strftime("%Y-%m-%d"); this works for pandas timestamp values and ordinary Python date values. If n is larger than the number of rows, return all rows sorted from longest to shortest.
For local testing, you can copy the study_sessions.csv block from the pandas DataFrame chapter into a local file named study_sessions.csv, then use df = pd.read_csv("study_sessions.csv"). The grader still passes df directly to your function.
The returned value must contain ordinary Python values so it can be passed to json.dumps. Do not ask for user input and do not print inside the function.
Preserve the Complete Dashboard
Keep StudyLog.print_dashboard(today=None) as the public API. The refactor in Using Libraries changes how its facts are calculated, not which facts learners see. The dashboard must still include:
- the overall session count and total time,
- each subject’s session count, total, and average,
- recent daily totals for the inclusive seven-day window,
- the heaviest day, choosing the earliest date on a tie,
- the current streak,
- and the longest streak.
Move each calculation into a pandas helper, then keep presentation in the method. The method can have this shape:
def print_dashboard(self, today=None): if today is None: today = date.today()
if len(self.sessions) == 0: print("No sessions logged yet -- ready to start.") return
df = sessions_dataframe(self.sessions) total = pandas_total_minutes(df) hours, minutes = divmod(total, 60) print(f"So far: {len(df)} sessions, {hours} hours {minutes} minutes") print()
print("By subject:") summaries = pandas_summaries_by_subject(df) for subject in sorted(summaries): info = summaries[subject] count = info["count"] word = "session" if count == 1 else "sessions" print( f" {subject.title()}: {count} {word}, {info['total']} minutes, " f"average {info['average']:.1f} minutes" ) print()
recent = pandas_recent_daily_totals(df, today) if len(recent) > 0: print("Recent days:") for day, day_total in recent: print(f" {day}: {day_total} minutes") print()
heavy = pandas_heaviest_day(df) if heavy is not None: print(f"Heaviest day: {heavy[0]} ({heavy[1]} minutes)")
print(f"Current streak: {pandas_current_streak(df, today)} days") print(f"Longest streak: {pandas_longest_streak(df)} days")This is the same presentation contract as Structuring Your Data. Only the calculation layer behind it is new.
Before and After: One Dashboard Section
Here is a small loop-based dashboard section:
totals = {}for session in sessions: subject = session.subject minutes = session.minutes totals[subject] = totals.get(subject, 0) + minutes
print("By subject:")for subject, minutes in totals.items(): print(f" {subject}: {minutes} minutes")This is the class-based shape from Structuring Your Data: the loop walks Session objects and reads their attributes.
The pandas-backed version should move the complete calculation into the checkpoint helper:
summaries = pandas_summaries_by_subject(df)Then the printing code remains simple:
print("By subject:")for subject in sorted(summaries): info = summaries[subject] print( f" {subject.title()}: {info['count']} sessions, " f"{info['total']} minutes, average {info['average']:.1f} minutes" )The output can stay almost the same. What changed is where the summary calculation lives. This is the kind of refactor Using Libraries asks for: replace a manual summary calculation with a pandas-backed helper, while keeping the user-facing behavior understandable.
Do this section by section. Replace the overall total, then per-subject summaries, daily totals, heaviest day, and finally the streak calculations. If something breaks, the most recent replacement is the likely place to inspect.
Integration Steps
A safe order for the project update:
- Install and verify pandas in a tiny check file.
- Add
import pandas as pdto the study log file that will contain the summary helpers. - Add
sessions_dataframe(sessions)and test it with inlineSessionobjects. - Add one summary helper at a time.
- Replace one old
StudyLog.print_dashboardcalculation with the pandas-backed helper. - Compare the complete dashboard output with the fixture from Refactoring the Study Log with Classes.
- Run the program with an empty data file and with a small known data file.
Do not convert the entire project in one edit. The existing loading and saving boundary should remain responsible for file persistence and serialization: load_study_log(...) reads JSON into objects, and StudyLog.save(...) writes objects back as JSON. Pandas should take over the summary calculations, not the whole program.
The complete executable flow stays in main(). There is no separate today_text variable to invent: print_dashboard() uses date.today() by default, while tests can pass an explicit date.
def main(): print("=== Study Log ===") print()
log = load_study_log(SESSIONS_FILE) log.print_dashboard() print()
while True: subject, minutes = get_session() if subject is None: break
session = Session(subject, minutes, date.today().isoformat()) log.add(session) log.save(SESSIONS_FILE) print_response(subject, minutes) print()
print_final_summary(log)
if __name__ == "__main__": main()The project still has one source of truth: the StudyLog loaded from JSON. The DataFrame is a working view of log.sessions for summaries. Do not store the DataFrame in the JSON file, and do not replace Session with pandas rows throughout the program.
Required Dashboard Behavior
The final dashboard must preserve the layout established in Refactoring the Study Log with Classes and answer the same questions:
- How many sessions and how much total time are on record?
- What are the count, total, and average for each subject?
- What are the recent daily totals relative to the chosen
today? - Which day was heaviest?
- What are the current and longest streaks?
For an empty data file, print one honest message and stop the dashboard:
No sessions logged yet -- ready to start.For the canonical four-session sample from that checkpoint, the output is:
So far: 4 sessions, 3 hours 45 minutes
By subject: History: 1 session, 30 minutes, average 30.0 minutes Math: 2 sessions, 105 minutes, average 52.5 minutes Physics: 1 session, 90 minutes, average 90.0 minutes
Recent days: 2026-05-10: 75 minutes 2026-05-11: 60 minutes 2026-05-12: 90 minutes
Heaviest day: 2026-05-12 (90 minutes)Current streak: 3 daysLongest streak: 3 daysEach printed fact should come from a helper whose behavior you have tested with a small fixed DataFrame.
The exercise below checks the complete cumulative project. Start from your class-based solution from Refactoring the Study Log with Classes, add the pandas helper layer, and retain the loading, dashboard, input, saving, and final-summary flow.
Pandas study log dashboard
0 / 40 points
Update the complete class-based study log from Structuring Your Data so its dashboard calculations use pandas without losing any existing behavior.
Start from your completed StudyLog program from Refactoring the Study Log with Classes. Keep its public classes, methods, JSON format, input loop, response messages, saving behavior, and if __name__ == "__main__": main() guard. In particular, keep StudyLog.print_dashboard(today=None) as the public dashboard API. Do not replace it with a separate reduced dashboard.
Add pandas-backed helper functions with these contracts:
| Helper | Contract |
|---|---|
sessions_dataframe(sessions) | call to_dict() on each Session, build a DataFrame with subject, minutes, and date columns, and convert date with pd.to_datetime |
pandas_total_minutes(df) | return total minutes as an ordinary Python int, or 0 when empty |
pandas_summaries_by_subject(df) | use pandas groupby and return {subject: {"count": int, "total": int, "average": float}} |
pandas_totals_by_day(df) | return chronologically sorted [date_string, total_minutes] rows |
pandas_heaviest_day(df) | return the earliest row with the largest daily total, or None |
pandas_recent_daily_totals(df, today) | return daily totals from today - 6 days through today, including both endpoints |
pandas_current_streak(df, today) | yesterday keeps a streak alive; an empty/expired streak is 0; a future session raises ValueError |
pandas_longest_streak(df) | return the longest consecutive run of distinct study days, or 0 |
StudyLog.total_minutes, summaries_by_subject, heaviest_day, recent_daily_totals, current_streak, and longest_streak should delegate to these pandas helpers. Work from a DataFrame view of self.sessions; keep Session objects and the JSON list of dictionaries as the source of truth.
In pandas_recent_daily_totals, create the endpoints with pd.Timestamp and pd.Timedelta, then combine the inclusive date masks with &. These operations are part of the required pandas calculation, not merely one possible implementation detail.
The dashboard must retain every section from that class-based checkpoint. For this fixture:
StudyLog([ Session("math", 45, "2026-05-10"), Session("history", 30, "2026-05-10"), Session("math", 60, "2026-05-11"), Session("physics", 90, "2026-05-12"),])print_dashboard(today=date(2026, 5, 12)) must print exactly:
So far: 4 sessions, 3 hours 45 minutes
By subject: History: 1 session, 30 minutes, average 30.0 minutes Math: 2 sessions, 105 minutes, average 52.5 minutes Physics: 1 session, 90 minutes, average 90.0 minutes
Recent days: 2026-05-10: 75 minutes 2026-05-11: 60 minutes 2026-05-12: 90 minutes
Heaviest day: 2026-05-12 (90 minutes)Current streak: 3 daysLongest streak: 3 daysFor an empty log, print exactly No sessions logged yet -- ready to start. and return from the dashboard. The seven-day window is inclusive. If daily totals tie for heaviest, choose the earliest date.
Keep the complete executable flow: load sessions.json, print the dashboard, collect sessions until done, save immediately after each added session, and print the final summary. Do not ask for input when the module is imported.
Using AI for This Project
This is a direct place to apply the previous chapter’s library-checking habit. AI assistants can be useful here, but pandas makes compact wrong answers easy. A good prompt is specific about the input shape, the output shape, and the edge cases:
Write a function pandas_summaries_by_subject(df) fora pandas DataFrame with columns subject, minutes, anddate. It should group by subject and return a plainnested dictionary containing count, total, and averagefor each subject. An empty DataFrame should return {}.Then verify the result with your sample assertions. Do not ask for “a pandas dashboard” all at once. Ask for one helper, test it, then move to the next helper.
If the AI gives you a dense method chain, you may ask it to rewrite the code with named intermediate variables. That is not making the code less advanced. It is making the assumptions visible.
A Final Self-Check
Before considering the checkpoint done, check these points:
- The program still loads and saves JSON records as before.
load_study_log(...)still returns aStudyLog, andStudyLog.save(...)still writes JSON.sessions_dataframe([])returns an empty DataFrame with the expected columns.minutesis numeric before any summary helper runs.dateis converted withpd.to_datetimebefore date filtering.- Each helper has at least one small test or assertion.
- The inclusive recent window, earliest heaviest-day tie, empty streaks, and future dates are tested.
- The complete class-based dashboard output is preserved.
- The empty dashboard does not crash.
If all nine are true, the pandas refactor is doing its job. It has changed how summaries are calculated, but it has not made the rest of the study log harder to reason about.
Extra Things to Try Out
A few optional extensions:
- Return per-subject summaries sorted by total minutes.
- Add a helper that returns the single longest session as a dictionary.
- Add a helper that returns the number of distinct study days.
Each extension should have a tiny sample input and at least one test or assertion before it is wired into the full project.
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.