Chapter 9 — Data Visualization with Matplotlib, Seaborn, and Plotly
Visualization reveals patterns numbers hide. This chapter tours the three main Python plotting libraries: Matplotlib for fine control, Seaborn for statistical plots, and Plotly for interactive graphics.
Learning Objectives
- Create line, bar, scatter, histogram, and box plots with Matplotlib.
- Use Seaborn for statistical, good-looking defaults.
- Build interactive plots with Plotly.
- Choose the right chart for the data and question.
- Label axes, add titles, and save figures.
Prerequisites / Imports
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
sns.set_theme(style='whitegrid')
1 The Grammar of Matplotlib
A Figure holds one or more Axes. Plot, then label, then show.
x = np.linspace(0, 10, 100)
fig, ax = plt.subplots(figsize=(7,4))
ax.plot(x, np.sin(x), label='sin')
ax.plot(x, np.cos(x), label='cos')
ax.set_title('Sine and Cosine')
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.legend()
plt.show()
2 Bar Charts
Compare categorical quantities.
revenue = pd.DataFrame({'product':['A','B','C','D'], 'sales':[250, 180, 320, 140]})
fig, ax = plt.subplots(figsize=(7,4))
ax.bar(revenue['product'], revenue['sales'], color='steelblue')
ax.set_title('Sales by Product')
ax.set_ylabel('Sales ($)')
plt.show()
3 Histograms
Show the distribution of a numeric variable.
tips = sns.load_dataset('tips')
plt.figure(figsize=(7,4))
plt.hist(tips['total_bill'], bins=20, color='coral', edgecolor='black')
plt.title('Distribution of Total Bill')
plt.xlabel('Total bill ($)'); plt.ylabel('Frequency')
plt.show()
4 Scatter Plots
Reveal relationships between two numeric variables.
plt.figure(figsize=(7,4))
plt.scatter(tips['total_bill'], tips['tip'], alpha=0.6)
plt.title('Tip vs Total Bill')
plt.xlabel('Total bill ($)'); plt.ylabel('Tip ($)')
plt.show()
5 Seaborn Statistical Plots
Seaborn makes attractive statistical graphics with little code.
plt.figure(figsize=(7,4))
sns.boxplot(data=tips, x='day', y='total_bill')
plt.title('Total bill by day')
plt.show()
penguins = sns.load_dataset('penguins').dropna()
plt.figure(figsize=(7,5))
sns.scatterplot(data=penguins, x='flipper_length_mm', y='body_mass_g', hue='species')
plt.title('Penguin flipper length vs body mass')
plt.show()
6 Correlation Heatmap
A heatmap of the correlation matrix spotlights linear relationships.
num = tips.select_dtypes('number').corr()
plt.figure(figsize=(6,5))
sns.heatmap(num, annot=True, cmap='coolwarm', vmin=-1, vmax=1)
plt.title('Correlation matrix (tips)')
plt.show()
7 Interactive Plots with Plotly
Plotly figures are interactive (zoom, hover). We render a static PNG here; in a live notebook the figure is fully interactive.
import plotly.express as px
tips = sns.load_dataset('tips')
fig = px.scatter(tips, x='total_bill', y='tip', color='time',
size='size', hover_data=['day'],
title='Tip vs Total Bill (interactive)')
fig
Figure({
'data': [{'customdata': array([['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sun'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Sat'],
['Thur']], dtype=object),
'hovertemplate': ('time=Dinner<br>total_bill=%{x}' ... '{customdata[0]}<extra></extra>'),
'legendgroup': 'Dinner',
'marker': {'color': '#636efa',
'size': array([2, 3, 3, 2, 4, 4, 2, 4, 2, 2, 2, 4, 2, 4, 2, 2, 3, 3, 3, 3, 2, 2, 2, 4,
2, 4, 2, 2, 2, 2, 2, 4, 2, 4, 2, 3, 3, 3, 3, 3, 3, 2, 2, 2, 4, 2, 2, 4,
3, 2, 2, 2, 4, 2, 4, 2, 4, 2, 2, 4, 2, 2, 2, 4, 3, 3, 2, 1, 2, 2, 2, 3,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2,
2, 2, 1, 3, 2, 3, 2, 4, 2, 2, 3, 4, 4, 5, 6, 4, 2, 4, 4, 2, 3, 2, 2, 3,
2, 4, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 2, 3, 4, 2, 5, 3, 5, 3, 3,
2, 3, 4, 2, 2, 3, 4, 4, 2, 3, 2, 5, 2, 2, 4, 4, 2, 2, 4, 3, 2, 2, 2, 2,
2, 2, 3, 3, 2, 2, 2, 2], dtype=int64),
'sizemode': 'area',
'sizeref': 0.015,
'symbol': 'circle'},
'mode': 'markers',
'name': 'Dinner',
'orientation': 'v',
'showlegend': True,
'type': 'scatter',
'x': array([16.99, 10.34, 21.01, 23.68, 24.59, 25.29, 8.77, 26.88, 15.04, 14.78,
10.27, 35.26, 15.42, 18.43, 14.83, 21.58, 10.33, 16.29, 16.97, 20.65,
17.92, 20.29, 15.77, 39.42, 19.82, 17.81, 13.37, 12.69, 21.7 , 19.65,
9.55, 18.35, 15.06, 20.69, 17.78, 24.06, 16.31, 16.93, 18.69, 31.27,
16.04, 17.46, 13.94, 9.68, 30.4 , 18.29, 22.23, 32.4 , 28.55, 18.04,
12.54, 10.29, 34.81, 9.94, 25.56, 19.49, 38.01, 26.41, 11.24, 48.27,
20.29, 13.81, 11.02, 18.29, 17.59, 20.08, 16.45, 3.07, 20.23, 15.01,
12.02, 17.07, 26.86, 25.28, 14.73, 10.51, 17.92, 28.97, 22.49, 5.75,
16.32, 22.75, 40.17, 27.28, 12.03, 21.01, 12.46, 11.35, 15.38, 44.3 ,
22.42, 20.92, 15.36, 20.49, 25.21, 18.24, 14.31, 14. , 7.25, 38.07,
23.95, 25.71, 17.31, 29.93, 14.07, 13.13, 17.26, 24.55, 19.77, 29.85,
48.17, 25. , 13.39, 16.49, 21.5 , 12.66, 16.21, 13.81, 17.51, 24.52,
20.76, 31.71, 10.59, 10.63, 50.81, 15.81, 7.25, 31.85, 16.82, 32.9 ,
17.89, 14.48, 9.6 , 34.63, 34.65, 23.33, 45.35, 23.17, 40.55, 20.69,
20.9 , 30.46, 18.15, 23.1 , 15.69, 26.59, 38.73, 24.27, 12.76, 30.06,
25.89, 48.33, 13.27, 28.17, 12.9 , 28.15, 11.59, 7.74, 30.14, 20.45,
13.28, 22.12, 24.01, 15.69, 11.61, 10.77, 15.53, 10.07, 12.6 , 32.83,
35.83, 29.03, 27.18, 22.67, 17.82, 18.78]),
'xaxis': 'x',
'y': array([ 1.01, 1.66, 3.5 , 3.31, 3.61, 4.71, 2. , 3.12, 1.96, 3.23,
1.71, 5. , 1.57, 3. , 3.02, 3.92, 1.67, 3.71, 3.5 , 3.35,
4.08, 2.75, 2.23, 7.58, 3.18, 2.34, 2. , 2. , 4.3 , 3. ,
1.45, 2.5 , 3. , 2.45, 3.27, 3.6 , 2. , 3.07, 2.31, 5. ,
2.24, 2.54, 3.06, 1.32, 5.6 , 3. , 5. , 6. , 2.05, 3. ,
2.5 , 2.6 , 5.2 , 1.56, 4.34, 3.51, 3. , 1.5 , 1.76, 6.73,
3.21, 2. , 1.98, 3.76, 2.64, 3.15, 2.47, 1. , 2.01, 2.09,
1.97, 3. , 3.14, 5. , 2.2 , 1.25, 3.08, 3. , 3.5 , 1. ,
4.3 , 3.25, 4.73, 4. , 1.5 , 3. , 1.5 , 2.5 , 3. , 2.5 ,
3.48, 4.08, 1.64, 4.06, 4.29, 3.76, 4. , 3. , 1. , 4. ,
2.55, 4. , 3.5 , 5.07, 2.5 , 2. , 2.74, 2. , 2. , 5.14,
5. , 3.75, 2.61, 2. , 3.5 , 2.5 , 2. , 2. , 3. , 3.48,
2.24, 4.5 , 1.61, 2. , 10. , 3.16, 5.15, 3.18, 4. , 3.11,
2. , 2. , 4. , 3.55, 3.68, 5.65, 3.5 , 6.5 , 3. , 5. ,
3.5 , 2. , 3.5 , 4. , 1.5 , 3.41, 3. , 2.03, 2.23, 2. ,
5.16, 9. , 2.5 , 6.5 , 1.1 , 3. , 1.5 , 1.44, 3.09, 3. ,
2.72, 2.88, 2. , 3. , 3.39, 1.47, 3. , 1.25, 1. , 1.17,
4.67, 5.92, 2. , 2. , 1.75, 3. ]),
'yaxis': 'y'},
{'customdata': array([['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Thur'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri'],
['Fri']], dtype=object),
'hovertemplate': ('time=Lunch<br>total_bill=%{x}<' ... '{customdata[0]}<extra></extra>'),
'legendgroup': 'Lunch',
'marker': {'color': '#EF553B',
'size': array([4, 2, 2, 2, 2, 1, 2, 2, 4, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 6, 2, 2,
2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 6, 5, 6, 2, 2, 3, 2, 2, 2, 2, 2,
2, 2, 2, 2, 4, 2, 2, 3, 2, 2, 2, 4, 3, 2, 2, 1, 3, 2, 2, 2], dtype=int64),
'sizemode': 'area',
'sizeref': 0.015,
'symbol': 'circle'},
'mode': 'markers',
'name': 'Lunch',
'orientation': 'v',
'showlegend': True,
'type': 'scatter',
'x': array([27.2 , 22.76, 17.29, 19.44, 16.66, 10.07, 32.68, 15.98, 34.83, 13.03,
18.28, 24.71, 21.16, 10.65, 12.43, 24.08, 11.69, 13.42, 14.26, 15.95,
12.48, 29.8 , 8.52, 14.52, 11.38, 22.82, 19.08, 20.27, 11.17, 12.26,
18.26, 8.51, 10.33, 14.15, 16. , 13.16, 17.47, 34.3 , 41.19, 27.05,
16.43, 8.35, 18.64, 11.87, 9.78, 7.51, 19.81, 28.44, 15.48, 16.58,
7.56, 10.34, 43.11, 13. , 13.51, 18.71, 12.74, 13. , 16.4 , 20.53,
16.47, 12.16, 13.42, 8.58, 15.98, 13.42, 16.27, 10.09]),
'xaxis': 'x',
'y': array([4. , 3. , 2.71, 3. , 3.4 , 1.83, 5. , 2.03, 5.17, 2. , 4. , 5.85,
3. , 1.5 , 1.8 , 2.92, 2.31, 1.68, 2.5 , 2. , 2.52, 4.2 , 1.48, 2. ,
2. , 2.18, 1.5 , 2.83, 1.5 , 2. , 3.25, 1.25, 2. , 2. , 2. , 2.75,
3.5 , 6.7 , 5. , 5. , 2.3 , 1.5 , 1.36, 1.63, 1.73, 2. , 4.19, 2.56,
2.02, 4. , 1.44, 2. , 5. , 2. , 2. , 4. , 2.01, 2. , 2.5 , 4. ,
3.23, 2.2 , 3.48, 1.92, 3. , 1.58, 2.5 , 2. ]),
'yaxis': 'y'}],
'layout': {'legend': {'itemsizing': 'constant', 'title': {'text': 'time'}, 'tracegroupgap': 0},
'template': '...',
'title': {'text': 'Tip vs Total Bill (interactive)'},
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'title': {'text': 'total_bill'}},
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0], 'title': {'text': 'tip'}}}
})fig2 = px.bar(tips.groupby('day', as_index=False)['total_bill'].mean(),
x='day', y='total_bill', title='Average total bill by day')
fig2
<cell-prefix>:1: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
Figure({
'data': [{'alignmentgroup': 'True',
'hovertemplate': 'day=%{x}<br>total_bill=%{y}<extra></extra>',
'legendgroup': '',
'marker': {'color': '#636efa', 'pattern': {'shape': ''}},
'name': '',
'offsetgroup': '',
'orientation': 'v',
'showlegend': False,
'textposition': 'auto',
'type': 'bar',
'x': array(['Thur', 'Fri', 'Sat', 'Sun'], dtype=object),
'xaxis': 'x',
'y': array([17.68274194, 17.15157895, 20.44137931, 21.41 ]),
'yaxis': 'y'}],
'layout': {'barmode': 'relative',
'legend': {'tracegroupgap': 0},
'template': '...',
'title': {'text': 'Average total bill by day'},
'xaxis': {'anchor': 'y', 'domain': [0.0, 1.0], 'title': {'text': 'day'}},
'yaxis': {'anchor': 'x', 'domain': [0.0, 1.0], 'title': {'text': 'total_bill'}}}
})8 Choosing the Right Chart
| Question | Chart |
|---|---|
| Distribution of one variable | histogram, box plot |
| Relationship of two variables | scatter, line |
| Comparison across categories | bar chart |
| Part of a whole | stacked bar, pie (sparingly) |
| Correlation among many variables | heatmap |
Case Study: Sales Dashboard Elements
Combine a bar chart of sales by category and a line chart of monthly sales — the building blocks of a business dashboard.
rng = np.random.default_rng(1)
cats = ['Electronics','Clothing','Books','Home']
sales_by_cat = pd.DataFrame({'category':cats, 'sales':rng.integers(1000,5000,size=4)})
months = pd.date_range('2026-01-01', periods=12, freq='MS')
monthly = pd.DataFrame({'month':months, 'sales':np.cumsum(rng.normal(200, 40, size=12)).round(0)})
fig, axes = plt.subplots(1, 2, figsize=(12,4))
axes[0].bar(sales_by_cat['category'], sales_by_cat['sales'], color='teal')
axes[0].set_title('Sales by category'); axes[0].set_ylabel('Sales ($)')
axes[1].plot(monthly['month'], monthly['sales'], marker='o', color='darkorange')
axes[1].set_title('Cumulative monthly sales'); axes[1].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()
<cell-expr>:1: UserWarning:
Exercises
- Plot the functions $y=x^2$ and $y=x^3$ on the same axes for $x\in[-2,2]$.
- Make a bar chart of five cities and their populations.
- Draw a histogram with 30 bins of a normal random sample of 1000 points.
- Create a scatter plot of two numeric columns and color by a category.
- Use Seaborn to make a box plot of total bill by smoker status.
- Produce a correlation heatmap of the penguins numeric columns.
- Build an interactive Plotly scatter of penguin flipper vs body mass.
- Save a Matplotlib figure to a PNG file with
plt.savefig. - Create a 2x1 subplot with a line chart and a bar chart.
- Explain when you would choose a heatmap over a scatter plot.
Python Data Science: From Foundations to Applications — Chapter 9