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.

NoteHint

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.

TipSolution
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.

NoteHint

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.

TipSolution
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.

NoteHint

Use sns.boxplot. Label the axes with meaningful English names including measurement units (e.g. “Grade Point Average (0 to 4)”).

TipSolution
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.

NoteHint

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.

TipSolution
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.

NoteHint

Bar charts represent values through bar length from the baseline. Therefore, the lower y-limit must start at 0.

TipSolution
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?

NoteHint

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.

TipSolution
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).

NoteHint

The theoretical wellbeing instrument bounds are 0 and 100.

TipSolution
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.

NoteHint

Set col="dataset" in sns.lmplot() to create a small-multiple column for each of the four datasets (I, II, III, IV).

TipSolution
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.

NoteHint

Set x_jitter=0.2 to spread discrete integer points horizontally without altering their actual vertical wellbeing values.

TipSolution
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

NoteDownload the Chapter 4 project

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