Playground: Chapter 4
Data Visualization with Seaborn and Matplotlib
This page practises the ideas of Chapter 4 in four parts. Practise the chapter works through the book’s exercises, with starter code, hints, and solutions. Go further goes beyond the book, with new questions and datasets. Check your understanding asks short conceptual questions whose answers open with a click. Do it yourself gives open tasks with an empty code space and no answers: you decide which visualization best answers the research question, as you will in your own thesis.
In the exercises, replace each ______ with your own code and press Run Code.
Practise the chapter
These are the exercises at the end of Chapter 4, with the same numbers.
Exercise 1: Choosing a binwidth
Draw a histogram of study_hours in the first semester. Try binwidths of 1, 5, and 10, and explain which shows the distribution shape best.
The binwidth parameter in sns.histplot() determines the width of each bar in hours. Run the code three times with 1, 5, and 10 and compare the resulting visualizations.
fig, ax = plt.subplots(figsize=(7, 4))
sns.histplot(
data=first_sem,
x="study_hours",
binwidth=5,
color="steelblue",
edgecolor="white",
ax=ax
)
ax.set_title("Distribution of Study Hours")
ax.set_xlabel("Weekly Study Hours")
ax.set_ylabel("Student Count")
plt.show()A binwidth of 5 hours shows the shape best: most students study between about 15 and 40 hours a week, the peak is in the mid-twenties, and a long tail stretches to the right up to 67 hours. With a binwidth of 1, the bars are overly jagged and the underlying distribution drowns in sampling noise; with 10, only a few thick bars remain, hiding the right-tail skew.
Exercise 2: Employment in order, in one colour
Draw a count bar chart of how many students have each level of employment. Make the bars a single colour of your choice, and order the levels from no job to a full-time job.
Pass emp_order to categories inside pd.Categorical. To apply a uniform color without generating an unneeded legend, set color="steelblue" (or any single hex/named color) instead of hue.
emp_order = ["None", "Part-time job", "Full-time job"]
students["employment"] = pd.Categorical(students["employment"], categories=emp_order, ordered=True)
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(
data=students,
x="employment",
color="steelblue",
ax=ax
)
ax.set_xlabel("Paid Employment Status")
ax.set_ylabel("Number of Students")
plt.show()307 students have no job, 196 a part-time job, and 97 a full-time job. Employment is an ordinal variable, so ordering the categories ensures the bars follow logical progression rather than alphabetical order. Using color="steelblue" paints all bars consistently without generating an extraneous legend.
Exercise 3: GPA by faculty
Draw box plots of first-semester GPA for each faculty, with clear axis labels.
Use sns.boxplot. Label the axes with meaningful English names including measurement units (e.g. “Grade Point Average (0 to 4)”).
fig, ax = plt.subplots(figsize=(8, 4))
sns.boxplot(
data=first_sem,
x="faculty",
y="gpa",
color="skyblue",
ax=ax
)
ax.set_xlabel("Faculty")
ax.set_ylabel("Grade Point Average (GPA, 0 to 4)")
plt.show()The five boxes are almost identical across faculties, with medians hovering between 3.06 and 3.14. The interquartile ranges overlap heavily, demonstrating that baseline academic performance does not vary systematically by academic division.
Exercise 4: Caffeine and sleep
Draw a scatter plot of caffeine against sleep in the first semester, with a linear regression trend line, and describe what it suggests.
alpha ranges from 0 (transparent) to 1 (solid). For 600 points, setting alpha=0.35 or 0.4 effectively reveals dense clusters of points without blacking out the region.
fig, ax = plt.subplots(figsize=(7, 4))
sns.regplot(
data=first_sem,
x="caffeine_mg",
y="sleep_hours",
scatter_kws={"alpha": 0.35, "color": "darkblue"},
line_kws={"color": "darkred"},
ax=ax
)
ax.set_xlabel("Daily Caffeine Consumption (mg)")
ax.set_ylabel("Hours of Sleep per Night")
plt.show()The regression line slopes downward: students consuming higher amounts of caffeine report fewer hours of sleep (roughly half an hour less per extra 100 mg of caffeine). While observational data cannot establish causal direction, the visual association is pronounced.
Exercise 5: Saving a publication-ready graph
Take any graph from this chapter and save it as a high-resolution PNG file (300 DPI) and as a vector PDF.
Saving files to disk requires local file access, so this exercise is in the download project.
fig, ax = plt.subplots(figsize=(6.3, 3.9)) # ~16 cm x 10 cm
sns.histplot(data=first_sem, x="sleep_hours", binwidth=0.5, color="steelblue", edgecolor="white", ax=ax)
ax.set_xlabel("Hours of Sleep per Night")
ax.set_ylabel("Count")
# Save high-res raster and scalable vector
fig.savefig("sleep.png", dpi=300, bbox_inches="tight")
fig.savefig("sleep.pdf", bbox_inches="tight")
plt.close(fig)Explicitly passing bbox_inches="tight" prevents axis labels from being clipped outside figure bounds, and specifying dpi=300 ensures sharp print reproduction.
Exercise 6: The misleading graph, redrawn
Redraw the misleading bar chart of wellbeing by faculty with an honest zero baseline (0 to 100), with faculties ordered by average wellbeing.
Bar charts represent values through bar length from the baseline. Therefore, the lower y-limit must start at 0.
faculty_means = (
first_sem.groupby("faculty", as_index=False)["wellbeing"]
.mean()
.sort_values("wellbeing")
)
fig, ax = plt.subplots(figsize=(7, 4))
sns.barplot(
data=faculty_means,
x="faculty",
y="wellbeing",
color="grey",
ax=ax
)
ax.set_ylim(0, 100)
ax.set_xlabel("Faculty")
ax.set_ylabel("Average Wellbeing in Semester 1 (0 to 100)")
plt.show()On an honest 0-to-100 scale, the five faculties differ by only 3 points (58.9 to 61.9). Truncating the axis at 55 had exaggerated this minor variation into an apparent five-fold chasm. An even better thesis alternative is a jittered strip plot with overlaid means, revealing that within-faculty variation dwarfs between-faculty differences.
Go further
These exercises go beyond the book.
Exercise 7: The same numbers three ways (Dot plot vs Bar plot)
Show faculty average wellbeing as a horizontal dot plot. Why does a dot plot allow zooming where a bar chart does not?
Try setting ax.set_xlim(55, 65). Because the points encode value by position rather than bar length, zooming the axis does not mislead the viewer about relative proportions.
fig, ax = plt.subplots(figsize=(7, 3.5))
ax.scatter(faculty_means["wellbeing"], faculty_means["faculty"], color="navy", s=60)
ax.set_xlim(55, 65)
ax.set_xlabel("Average Wellbeing (0 to 100)")
ax.set_ylabel("Faculty")
ax.set_title("Dot Plot: Zoomed scale is honest because points denote position, not length")
plt.show()Cleveland & McGill showed that position along a common scale is decoded with highest accuracy. Unlike bars (which encode quantity via physical length from zero), dots encode quantity purely by coordinate position. Thus, a zoomed axis in a dot plot is honest as long as the axis bounds and units are clearly stated.
Exercise 8: A line chart, zoomed and not
Draw average wellbeing across semesters for both workshop conditions twice: once zoomed in, and once on the full theoretical scale (0 to 100).
The theoretical wellbeing instrument bounds are 0 and 100.
axes[1].set_ylim(0, 100)Zoomed in, the axis spans roughly 58 to 66, making the workshop group’s post-intervention gain appear dramatic. On the full 0–100 scale, the 5-point rise appears modest and realistic. Both views are legitimate in research if the figure caption informs the reader of the axis bounds.
Exercise 9: Anscombe’s quartet yourself
Calculate summary statistics for Anscombe’s four datasets, then plot them in separate facet panels using Seaborn.
Set col="dataset" in sns.lmplot() to create a small-multiple column for each of the four datasets (I, II, III, IV).
g = sns.lmplot(
data=anscombe,
x="x",
y="y",
col="dataset",
ci=None,
scatter_kws={"s": 50, "alpha": 0.8},
line_kws={"color": "crimson"}
)
plt.show()All four datasets share identical numerical summaries (\(\bar{x}=9.0\), \(\bar{y}=7.5\), \(r=0.82\), linear regression line \(y = 3.0 + 0.5x\)), yet visually reveal completely different phenomena: a linear noisy trend (I), a nonlinear curve (II), a line with an extreme outlier (III), and an influential leverage point (IV). Always inspect visual distributions before accepting summary metrics!
Exercise 10: Exercise days and wellbeing
Graph the relationship between weekly exercise days (0 to 7) and first-semester wellbeing. Add horizontal jitter to separate overlapping discrete points, set transparency, and add a linear trend line.
Set x_jitter=0.2 to spread discrete integer points horizontally without altering their actual vertical wellbeing values.
fig, ax = plt.subplots(figsize=(7, 4))
sns.regplot(
data=first_sem,
x="exercise_days",
y="wellbeing",
x_jitter=0.2,
scatter_kws={"alpha": 0.3, "color": "teal"},
line_kws={"color": "darkred"},
ax=ax
)
ax.set_xlabel("Days of Exercise per Week")
ax.set_ylabel("Wellbeing Score (0 to 100)")
plt.show()Horizontal jittering resolves overplotting along discrete whole-number days of exercise while preserving exact vertical measurement values. The trend line reveals a positive relationship between exercise frequency and student psychological wellbeing.
Check your understanding
Answer each question in your own words first, then click to see a model answer.
1. Why do researchers prefer a bar chart or dot plot to a pie chart?
Human perception judges positions and lengths along a common aligned scale far more accurately than angles, arc lengths, or 2D slice areas (Cleveland & McGill, 1984). In a bar chart or dot plot, differences are immediately discernible; in a pie chart, slices of similar proportions are nearly indistinguishable without numeric text callouts.
2. When is a y axis that does not start at zero acceptable?
When values are represented by coordinate position rather than ink length (e.g., in line charts, scatter plots, or dot plots), provided the reader is informed in the figure caption. It is never acceptable in bar charts, where visual ink length directly represents physical magnitude from baseline zero.
3. What is the difference between mapping a color to data and setting a fixed color?
Mapping (e.g., hue="faculty") links color to a dataset variable, assigning distinct palette swatches and generating an automated legend. Setting a fixed color (e.g., color="steelblue") paints all elements uniformly without creating a redundant legend.
4. Which kind of palette suits categories, and which suits quantities?
Categories require qualitative/discrete palettes (e.g. Set2, tab10) with balanced perceived luminance. Continuous quantities require sequential palettes (light-to-dark gradient, e.g. viridis, Blues) or diverging palettes centered on a meaningful midpoint (e.g. vlag, coolwarm).
5. What is an exploratory graph for, and how does it differ from an explanatory graph?
Exploratory graphs are private, rapid, and numerous—used by the researcher to inspect distributions, identify coding anomalies, and uncover relationships. Explanatory graphs are carefully designed public artifacts prepared for publication, featuring clear titles, labelled axes with measurement units, honest scales, and an informative caption.
Do it yourself
These tasks have no starter code and no answers. Each code space below is ready to run: write your own Python code, as you would for a thesis. The datasets (students, semesters, and first_sem) and libraries (pandas, numpy, matplotlib.pyplot, seaborn) are preloaded.
1. Choose a question about two variables of the study that the chapter did not graph. Select the appropriate plot type, build an explanatory thesis-ready figure, and write its caption.
2. The code below draws a deliberately poor graph. Improve it, and note each change you made and why:
3. Load seaborn’s built-in mpg dataset (sns.load_dataset("mpg")). Create three quick exploratory plots examining engine displacement (displacement) against fuel efficiency (mpg), then polish the clearest version into an explanatory thesis graphic.
4. Create a small-multiples grid showing the distribution of wellbeing across faculties using sns.displot(..., col="faculty", kde=True) or sns.FacetGrid.
5. In your local environment, save your polished graphic as both a vector PDF and a 300 DPI PNG, and inspect the clarity of text and vector lines when magnified.
Work on your own computer
The project contains the data and all four parts of this page as a standalone Python project: exercises with blanks (exercises.py), questions with model solutions (solutions.py), and open tasks.
Download chapter04.zip, unzip it, and open the folder in VS Code or your preferred Python IDE.
Make sure you have pandas, matplotlib, and seaborn installed:
pip install pandas matplotlib seaborn