Case Study 1: Priya Automates the Weekly Regional Sales Summary
Chapter 5 — Loops and Iteration: Automating Repetitive Tasks Python for Business for Beginners
Background
Every Monday morning at 8:45 a.m., Priya Okonkwo sits down at her desk and starts the same ritual: open four spreadsheet tabs — one per Acme Corp region — copy this week's figures into a master summary, calculate the totals, format the table, and email it to Sandra Chen before the 9:30 all-hands.
The task takes forty minutes. It requires almost no judgment. And Priya has to do it fifty-two times a year.
She has started making errors — not often, but enough to notice. Last month she pasted a column of figures one row off and the West Coast numbers were inflated by $14,000 before Sandra caught it. The conversation was polite, but the implication was clear: this workflow is not scaling.
Marcus Webb has suggested automating it three times. Each time, Priya has nodded and changed the subject. She knows how to use Excel. She does not know how to code.
Until now.
The Data
Priya is working with five weeks of sales figures across four regions. In the real version she pulls these from a spreadsheet; here they live directly in Python as a 2D list. (Chapter 9 covers pulling them from a file, and Chapter 16 covers reading them directly from Excel.)
regions = ["Northeast", "Southeast", "Midwest", "West Coast"]
# Five weeks of sales data. Each inner list is one week across all four regions.
# Order within each week matches the `regions` list above.
weekly_sales = [
[145_200, 98_400, 112_600, 187_300], # Week 1
[152_800, 103_700, 108_900, 201_400], # Week 2
[138_500, 91_200, 119_300, 195_600], # Week 3
[161_300, 107_800, 124_500, 215_700], # Week 4
[158_700, 112_300, 131_200, 208_900], # Week 5
]
weekly_targets = [150_000, 100_000, 115_000, 200_000]
Step 1 — Total Sales Per Region
Priya's first task is to get each region's five-week cumulative total. She learned about zip() in Chapter 5: it lets her combine two parallel sequences. Here she uses zip(*weekly_sales) — the asterisk "unpacks" the outer list so zip can group by column rather than by row.
regional_totals = {}
for region, weekly_figures in zip(regions, zip(*weekly_sales)):
regional_totals[region] = sum(weekly_figures)
print(f" {region}: ${regional_totals[region]:,.0f}")
Output:
Northeast: $756,500
Southeast: $513,400
Midwest: $596,500
West Coast: $1,008,900
The zip(*weekly_sales) idiom is worth pausing on. weekly_sales is a list of lists — five rows, each with four figures. Using * unpacks it into five separate arguments to zip. The result groups all Week-1-through-5 figures for the Northeast together, all figures for the Southeast together, and so on. It is a transpose operation without importing anything.
Step 2 — Running Totals Week by Week
The cumulative picture is useful, but Sandra actually wants to see momentum: is revenue growing, holding steady, or declining week over week? Priya uses enumerate() to track the week number alongside the figures.
print("\nCumulative Revenue by Week")
print("-" * 50)
running_total = 0
for week_num, figures in enumerate(weekly_sales, start=1):
week_total = sum(figures)
running_total += week_total
print(f" Week {week_num}: ${week_total:>10,.0f} | Running total: ${running_total:>12,.0f}")
Output:
Cumulative Revenue by Week
--------------------------------------------------
Week 1: $ 543,500 | Running total: $ 543,500
Week 2: $ 566,800 | Running total: $ 1,110,300
Week 3: $ 544,600 | Running total: $ 1,654,900
Week 4: $ 609,300 | Running total: $ 2,264,200
Week 5: $ 611,100 | Running total: $ 2,875,300
The pattern here — running_total += week_total — is called an accumulator. You initialize a variable to zero before the loop, then add to it with each iteration. At the end of the loop, it holds the grand total.
Step 3 — Best and Worst Performers
Sandra always asks the same question in the Monday meeting: "Who's leading and who needs support?" Priya wants to identify the top- and bottom-performing regions automatically rather than scanning numbers by eye.
best_region = ""
best_total = 0
worst_region = ""
worst_total = float("inf") # Start with positive infinity so any real value is smaller
for region, total in regional_totals.items():
if total > best_total:
best_total = total
best_region = region
if total < worst_total:
worst_total = total
worst_region = region
print(f"\nTop performer: {best_region} (${best_total:,.0f})")
print(f"Needs attention: {worst_region} (${worst_total:,.0f})")
Output:
Top performer: West Coast ($1,008,900)
Needs attention: Southeast ($513,400)
The float("inf") initialization is a reliable trick. When you want to find a minimum value by looping, starting with positive infinity guarantees that the very first real value will be smaller and replace it. Similarly, starting best_total at zero guarantees the first positive sales figure wins.
Step 4 — Target vs. Actual
Every region has a monthly target, and Priya wants to show Sandra not just the raw numbers but whether each region hit its goal. She uses zip() to pair regions, totals, and targets simultaneously, and a list comprehension to calculate the variance figures.
# Calculate the per-period target (5 weeks, so multiply weekly target by 5)
period_targets = [target * 5 for target in weekly_targets]
variance_data = [
(region, regional_totals[region], target,
regional_totals[region] - target,
((regional_totals[region] - target) / target) * 100)
for region, target in zip(regions, period_targets)
]
print("\nTarget vs. Actual Performance")
print("-" * 60)
print(f" {'Region':<14} {'Actual':>10} {'Target':>10} {'Variance':>12} {'% vs Target':>12}")
print("-" * 60)
for region, actual, target, variance, variance_pct in variance_data:
sign = "+" if variance >= 0 else ""
print(
f" {region:<14} "
f"${actual:>9,.0f} "
f"${target:>9,.0f} "
f"${variance:>+10,.0f} "
f" {sign}{variance_pct:.1f}%"
)
Output:
Target vs. Actual Performance
------------------------------------------------------------
Region Actual Target Variance % vs Target
------------------------------------------------------------
Northeast $756,500 $750,000 +$6,500 +0.9%
Southeast $513,400 $500,000 +$13,400 +2.7%
Midwest $596,500 $575,000 +$21,500 +3.7%
West Coast $1,008,900 $1,000,000 +$8,900 +0.9%
Step 5 — The Formatted Report
Now Priya combines everything into a single function that generates the full report. This is what she will eventually schedule to run automatically every Monday at 8:30 a.m.
def generate_weekly_summary(regions, weekly_sales, weekly_targets, week_labels=None):
"""
Generate a formatted weekly regional sales summary.
Parameters
----------
regions : list of region name strings
weekly_sales : list of lists; each inner list is one week of regional figures
weekly_targets: list of weekly target amounts, aligned to regions
week_labels : optional list of label strings for each week
"""
if week_labels is None:
week_labels = [f"Week {i}" for i in range(1, len(weekly_sales) + 1)]
num_weeks = len(weekly_sales)
period_targets = [t * num_weeks for t in weekly_targets]
# Compute regional totals
regional_totals = {
region: sum(figures)
for region, figures in zip(regions, zip(*weekly_sales))
}
grand_total = sum(regional_totals.values())
# Header
print("=" * 65)
print(" ACME CORP — WEEKLY REGIONAL SALES SUMMARY")
print(f" Period: {week_labels[0]} through {week_labels[-1]}")
print("=" * 65)
# Week-by-week breakdown
print("\nWEEK BY WEEK (ALL REGIONS COMBINED)")
print("-" * 65)
running_total = 0
prev_week_total = None
for week_label, figures in zip(week_labels, weekly_sales):
week_total = sum(figures)
running_total += week_total
if prev_week_total is not None:
change = week_total - prev_week_total
change_str = f" ({'+' if change >= 0 else ''}{change:,.0f})"
else:
change_str = " (baseline)"
print(f" {week_label:<10} ${week_total:>10,.0f}{change_str}")
prev_week_total = week_total
print("-" * 65)
print(f" {'TOTAL':<10} ${grand_total:>10,.0f}")
# Regional breakdown
print("\nREGIONAL PERFORMANCE vs. TARGET")
print("-" * 65)
print(f" {'Region':<14} {'Actual':>10} {'Target':>10} {'vs. Target':>12}")
print("-" * 65)
best_region = max(regional_totals, key=regional_totals.get)
worst_region = min(regional_totals, key=regional_totals.get)
for region, period_target in zip(regions, period_targets):
actual = regional_totals[region]
variance = actual - period_target
flag = " <-- TOP" if region == best_region else (" <-- WATCH" if region == worst_region else "")
print(
f" {region:<14} ${actual:>9,.0f} ${period_target:>9,.0f} "
f"${variance:>+10,.0f}{flag}"
)
print("=" * 65)
print(f" Grand Total: ${grand_total:>10,.0f} | {num_weeks}-week period")
print("=" * 65)
# Run it
week_labels = ["Oct W1", "Oct W2", "Oct W3", "Oct W4", "Oct W5"]
generate_weekly_summary(regions, weekly_sales, weekly_targets, week_labels)
Output:
=================================================================
ACME CORP — WEEKLY REGIONAL SALES SUMMARY
Period: Oct W1 through Oct W5
=================================================================
WEEK BY WEEK (ALL REGIONS COMBINED)
-----------------------------------------------------------------
Oct W1 $ 543,500 (baseline)
Oct W2 $ 566,800 (+23,300)
Oct W3 $ 544,600 (-22,200)
Oct W4 $ 609,300 (+64,700)
Oct W5 $ 611,100 (+1,800)
-----------------------------------------------------------------
TOTAL $2,875,300
REGIONAL PERFORMANCE vs. TARGET
-----------------------------------------------------------------
Region Actual Target vs. Target
-----------------------------------------------------------------
Northeast $756,500 $750,000 +$6,500
Southeast $513,400 $500,000 +$13,400
Midwest $596,500 $575,000 +$21,500
West Coast $1,008,900 $1,000,000 +$8,900 <-- TOP
=================================================================
Grand Total: $2,875,300 | 5-week period
=================================================================
What Priya Learned
This project took Priya about three hours to write and test — a little more than twice the time the manual process takes each week. But every Monday after this one, the process takes ten seconds: run the script.
More importantly, she learned something that's harder to articulate than a specific function: she learned to see data as a sequence of operations rather than a collection of cells. The zip(*weekly_sales) line would have been opaque to her two chapters ago. Now it reads like a sentence.
When she showed Marcus the script, he laughed. "That's cleaner than the Excel macro," he said. "Want me to set up a cron job so it runs automatically?"
Priya had no idea what a cron job was. She said yes.
Key Concepts Demonstrated
| Concept | Where Used |
|---|---|
for loop over a list of lists |
Weekly totals calculation |
enumerate() |
Week numbering in the running-total loop |
zip() |
Pairing regions with figures |
zip(*iterable) |
Transposing rows to columns |
| Accumulator pattern | running_total variable |
float("inf") initialization |
Finding the minimum value safely |
| List comprehension | Variance data table |
| f-string formatting | All output lines |
| Named function with parameters | generate_weekly_summary() |
Challenge Extension
Once you have finished Chapter 5, extend this script:
- Add a week-over-week change column to the regional table (not just the grand total).
- Use a nested loop to print every individual region-week cell in a grid layout.
- Use
continueto skip any week where a region's figures areNone(representing a data gap). - Calculate and display the standard deviation of weekly totals using only loops and arithmetic (no external libraries).