PYTHON / CAPSTONE PROJECTS
Project: from raw CSV to a finished chart
Turn a messy CSV into a saved PNG chart: coerce types at the read boundary, aggregate into x-axis keys, then draw.
What you will learn
- Coerce strings to dates and numbers once, at the CSV boundary, and count skipped rows
- Aggregate into a dict keyed by the exact labels the x-axis will show
- Save charts with the Agg backend and fig.savefig instead of plt.show
- Sort and gap-fill the axis yourself; matplotlib draws whatever order you hand it
Understanding Project: from raw CSV to a finished chart
csv.DictReader gives you strings and nothing else. "3" is a string, "" is a string, and "not a date" is a string that looks close enough to fool you at a glance. So the first stage of the pipeline has exactly one job: convert each field to the type it should be, and decide what to do with the rows that refuse to convert. Wrapping the conversions in one try/except and counting failures means a bad row never silently becomes a zero-height bar.
The middle stage collapses many rows into few points. A chart with twelve bars needs a mapping from twelve labels to twelve numbers, so build that mapping directly: key it by the string you want printed under the bar, accumulate into it, then produce two parallel lists with sorted() and a comprehension. Once you have labels and values as lists of the same length, the plotting stage becomes three lines and there is nothing left to debug about the data.
matplotlib is deliberately literal. If you pass it strings for x, it treats them as ordered categories in the order given, so "2024-10" landing between "2024-1" and "2024-2" is your sort's fault, not the library's. Likewise a month with no rows simply does not exist unless you insert it with a zero, which makes a gap read as continuity. In a script, choose the Agg backend and call fig.savefig: plt.show needs a display and will do nothing useful on a server or in a cron job.
import csv, io
from collections import defaultdict
from datetime import date
RAW = """order_id,order_date,region,units,unit_price
1001,2024-01-05,north,3,19.99
1002,2024-01-17,south,,19.99
1003,2024-02-02,north,10,4.50
1004,2024-02-14,south,2,4.50
1005,not a date,north,1,19.99
1006,2024-03-08,north,5,4.50
"""
monthly = defaultdict(float)
skipped = 0
for row in csv.DictReader(io.StringIO(RAW)):
try:
d = date.fromisoformat(row["order_date"])
units = int(row["units"])
price = float(row["unit_price"])
except ValueError:
skipped += 1
continue
monthly[f"{d.year}-{d.month:02d}"] += units * price
labels = sorted(monthly)
values = [round(monthly[k], 2) for k in labels]
print("skipped rows:", skipped)
for label, value in zip(labels, values):
print(label, value)
A CSV-to-chart project is three stages with clean handoffs: strings become typed values, typed values become label/value pairs, and label/value pairs become pixels.
Worked examples
Drawing and saving the chart
Takes the labels and values from the aggregation stage and writes a labelled bar chart to a PNG file.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
labels = ["2024-01", "2024-02", "2024-03"]
values = [59.97, 54.0, 22.5]
fig, ax = plt.subplots(figsize=(6, 3))
ax.bar(labels, values, color="#3b6ea5")
ax.set_ylabel("revenue (USD)")
ax.set_title("Revenue by month")
for label, value in zip(labels, values):
ax.annotate(f"{value:.0f}", (label, value), ha="center", va="bottom")
fig.tight_layout()
fig.savefig("revenue.png", dpi=120)
plt.close(fig)
print("wrote revenue.png")
Example explained
Line 1matplotlib.use("Agg") must run before pyplot is imported; it picks a file-only renderer that needs no display.
Line 2ax.bar with string labels creates one category per label, evenly spaced, in the order the list gives.
Line 3ax.annotate positions text at the top of each bar by passing the same category name as the x coordinate.
Line 4fig.tight_layout() recomputes margins so the y-label and title are inside the saved image rather than clipped.
Why zero-padding the month matters
Shows how string keys sort lexically, which silently scrambles the x-axis when months are not padded.
padded = ["2024-02", "2024-10", "2024-01"]
unpadded = ["2024-2", "2024-10", "2024-1"]
print(sorted(padded))
print(sorted(unpadded))
Example explained
Line 1sorted() on strings compares character by character, so "1" < "2" decides the order before the rest is read.
Line 2f"{d.month:02d}" pads single-digit months to two characters, which makes lexical order match calendar order.
Line 3The unpadded list is not an error anywhere, so the chart renders happily with October in the middle.
Filling months that have no rows
Builds the axis from the intended date range rather than from the data, so empty months show as zero instead of vanishing.
monthly = {"2024-01": 59.97, "2024-03": 22.5}
def month_keys(start, end):
year, month = start
keys = []
while (year, month) <= end:
keys.append(f"{year}-{month:02d}")
month += 1
if month == 13:
year, month = year + 1, 1
return keys
axis = month_keys((2024, 1), (2024, 3))
values = [monthly.get(key, 0.0) for key in axis]
print(axis)
print(values)
Example explained
Line 1month_keys generates the labels from the reporting period, so the axis does not depend on which months happen to have sales.
Line 2monthly.get(key, 0.0) supplies the missing February instead of raising KeyError.
Line 3Without this step February disappears and March sits next to January, making a 78% drop look like a mild dip.
Important notes
matplotlib is not in the standard library; install it with pip before running the plotting step, and keep the aggregation code importable without it so you can test the numbers separately.
Round only when producing labels. Rounding each row before summing shifts the total, and float addition already leaves values like 54.000000000000004 that need a format spec, not a repaired dataset.
Common mistakes
Passing the DictReader values straight into ax.bar: heights are strings, so matplotlib treats every distinct price as a separate category and the bars all come out the same height.
Calling plt.show() in a script run on a server: nothing appears, no file is written, and the process may block until it is killed.
Formatting month keys as f"{d.year}-{d.month}": sorted() then places 2024-10 between 2024-1 and 2024-2 and the chart tells the wrong story with no error anywhere.
Try it yourself
Change, predict, then run
Take the RAW string from the main example and aggregate revenue by region instead of by month, printing each region with its total. Then add a line that prints which region contributed the largest share, as a whole-number percentage.
Open the Python workspaceCheck your understanding
Your monthly bar chart runs January through December but December's bar appears second from the left. The totals themselves are all correct. What is the most likely cause?
- The month labels are strings and were sorted lexically, so "2024-12" ranks just after "2024-1"
- matplotlib sorts bars by height when x values are categorical
- The values list is a float list, so the axis was drawn in reverse
- fig.tight_layout() reorders the categories when labels would otherwise overlap
Show answer
sorted() on unpadded strings compares characters, so "2024-12" sorts before "2024-2" and the bar lands early; zero-padding the month fixes it. Sorting by height is tempting because the offending bar often is tall, but matplotlib never reorders categorical x values, it draws them in the order you supply.