Session 7 — Creating Visualizations
Introduction
transform.py is complete. The pipeline now produces three summary DataFrames — top schools, by ZIP, and by school size — plus the full merged DataFrame. This session builds report.py v1: two functions that turn those summaries into saved chart files. By the end of the session, two .png files are written to the reports directory and the chart-generation layer of the pipeline is done.
Setting Up
Open VS Code and activate your conda environment in the terminal.
In the Explorer pane, right-click the student_report/ folder and choose New File. Name it report.py.
In the terminal:
conda activate student-reportConfirm (student-report) appears in your terminal prompt before continuing.
No VPN or prior sessions required. The
__main__block imports fromtransform.pyand loads the pre-committed CSV files fromstudent_report/data/.
Building report.py v1
Starting the file
Create student_report/report.py and add the imports:
import matplotlib.pyplot as plt
import pandas as pd
from pathlib import Pathmatplotlib is already installed in the conda environment. pd and Path are included because the __main__ block uses them; neither is used by the two chart functions directly.
save_top_schools_chart()
Add save_top_schools_chart() below the imports:
def save_top_schools_chart(top_schools_df, output_dir):
path = Path(output_dir) / "top_middle_schools.png"
1 fig, ax = plt.subplots(figsize=(10, 6))
2 ax.barh(
top_schools_df['middle_school_name'],
top_schools_df['student_count'],
color='steelblue'
)
3 ax.set_xlabel("Number of Students")
4 ax.set_title("Top 10 Middle Schools by Student Count")
5 ax.invert_yaxis()
6 plt.tight_layout()
7 plt.savefig(path)
8 plt.close()
9 return path- 1
-
Creates a figure and a single set of axes.
figis the overall container;axis the drawing area where the chart is placed.figsize=(10, 6)sets the width and height in inches — wide enough to show school names without overlap. - 2
-
Draws a horizontal bar chart. The first argument is the y-axis labels (
middle_school_name), the second is the bar lengths (student_count). Horizontal bars are the right choice here because school names are long strings that would overlap on a vertical chart’s x-axis. - 3
- Labels the x-axis. On a horizontal bar chart the x-axis carries the numeric values, so this is the axis that needs a unit label.
- 4
- Adds a title above the chart.
- 5
-
By default
barhplaces the first row at the bottom of the chart. After sorting descending insummarize_top_schools(), the school with the highest count is in the first row — which would end up at the bottom without this call.invert_yaxis()flips the order so the largest bar appears at the top, which is the natural reading direction for a ranked list. - 6
- Adjusts spacing so axis labels and the title are not clipped at the figure edges. It should be called after all labels are set and before saving.
- 7
-
Writes the figure to a file. The format is inferred from the file extension (
.png). The file is written tooutput_dirregardless of the current working directory. - 8
-
Releases the figure from memory. Without this call, every chart created in a session accumulates in memory, and matplotlib will print a warning after 20 open figures. Always call
plt.close()immediately after saving. - 9
-
Returns the saved file path so the caller (later,
main.py) can log or display it.
save_size_chart()
Add save_size_chart() below save_top_schools_chart(). The structure mirrors save_top_schools_chart() with a few differences:
def save_size_chart(size_df, output_dir):
path = Path(output_dir) / "school_size_distribution.png"
fig, ax = plt.subplots(figsize=(7, 5))
1 ax.bar(
2 size_df['school_size'].astype(str),
size_df['student_count'],
color='teal'
)
ax.set_xlabel("School Size")
3 ax.set_ylabel("Number of Students")
ax.set_title("Students by Middle School Size")
plt.tight_layout()
plt.savefig(path)
plt.close()
return path- 1
-
ax.bar()instead ofax.barh()— vertical bars. The three size category labels are short (Small,Medium,Large) and fit comfortably on a horizontal x-axis, so there is no reason to rotate the chart. - 2
-
size_df['school_size'].astype(str)—school_sizeis a pandasCategoricalcolumn (created bypd.cut()in Session 6). matplotlib does not know how to place categorical tick marks automatically, so converting to plain strings produces clean, evenly spaced labels on the x-axis. - 3
-
ax.set_ylabel("Number of Students")— a vertical chart carries the numeric values on the y-axis, which now needs the unit label. The x-axis label (ax.set_xlabel) describes the categories.
No invert_yaxis() call — the three size bins are not a ranked list, so the default bottom-to-top direction is fine.
Testing with a main block
Add a if __name__ == '__main__': block that imports from transform.py and produces both charts:
if __name__ == '__main__':
from transform import get_students, merge_data, summarize_top_schools, summarize_by_size
enrollment_df = pd.read_csv('student_report/data/enrollment.csv')
survey_df = pd.read_csv('student_report/data/survey_middle_schools.csv')
school_df = pd.read_csv('student_report/data/schools.csv')
students = get_students(enrollment_df)
merged = merge_data(students, survey_df, school_df)
top_schools = summarize_top_schools(merged)
size_summary = summarize_by_size(merged)
output_dir = 'student_report/reports'
chart1 = save_top_schools_chart(top_schools, output_dir)
chart2 = save_size_chart(size_summary, output_dir)
print(f"Saved: {chart1}")
print(f"Saved: {chart2}")The from transform import ... line works because running python student_report/report.py adds the student_report/ directory to Python’s module search path automatically, making transform.py importable by name.
Run from the repo root:
python student_report/report.pyYou should see two file paths printed. Open student_report/reports/ and confirm that top_middle_schools.png and school_size_distribution.png were created or updated. Open each file to verify the charts look right — the horizontal bar chart should show schools ranked largest at the top; the vertical bar chart should show three labeled columns.
report.py v1 — Complete File
Remove the if __name__ == '__main__': block. The final report.py v1 defines three imports and two functions:
import matplotlib.pyplot as plt
import pandas as pd
from pathlib import Path
def save_top_schools_chart(top_schools_df, output_dir):
path = Path(output_dir) / "top_middle_schools.png"
fig, ax = plt.subplots(figsize=(10, 6))
ax.barh(top_schools_df['middle_school_name'], top_schools_df['student_count'], color='steelblue')
ax.set_xlabel("Number of Students")
ax.set_title("Top 10 Middle Schools by Student Count")
ax.invert_yaxis()
plt.tight_layout()
plt.savefig(path)
plt.close()
return path
def save_size_chart(size_df, output_dir):
path = Path(output_dir) / "school_size_distribution.png"
fig, ax = plt.subplots(figsize=(7, 5))
ax.bar(size_df['school_size'].astype(str), size_df['student_count'], color='teal')
ax.set_xlabel("School Size")
ax.set_ylabel("Number of Students")
ax.set_title("Students by Middle School Size")
plt.tight_layout()
plt.savefig(path)
plt.close()
return pathmain.py (Session 13) will call both functions by importing report.py. There is no top-level code after the imports, so the import is safe — no file I/O or computation happens until the functions are explicitly called.
In Session 8, we add save_excel_report() to report.py: a function that writes the full five-sheet Excel workbook and embeds the saved chart images.
Practice Exercise
Optional enrichment — complete during the session if time allows, or finish independently on your fork.
The starter script is at exercises/session_07_exercise.py. It contains instructions and fill-in-the-blank placeholders. If you get stuck, the completed version is at exercises/session_07_answer.py.
Run from the repo root:
python exercises/session_07_exercise.py
Additional Resources
- matplotlib — Horizontal bar chart (barh)
- matplotlib — Bar chart (bar)
- matplotlib — savefig
- matplotlib — Pyplot tutorial
- Automate the Boring Stuff with Python, 3rd Ed. — Chapter 17 (working with files and directories)