Data Visualization with Matplotlib
fig, ax = plt.subplots() pattern unlocks Seaborn, Plotly, and every other library built on top.After this lesson, you will be able to:
- Create line plots, scatter plots, bar charts, and histograms with Matplotlib
- Customize plots with titles, labels, colors, legends, and annotations
- Build multi-panel figures using subplots for side-by-side comparisons
- Know when to use each chart type for different kinds of data
- Understand how Seaborn extends Matplotlib for statistical visualizations
Before You Start
#Your First Plot
pyplot module provides a MATLAB-like interface:import matplotlib.pyplot as plt
import numpy as np
# Generate data
x = np.linspace(0, 10, 100) # 100 points from 0 to 10
y = np.sin(x)
# Create a line plot
plt.figure(figsize=(8, 4)) # width x height in inches
plt.plot(x, y)
plt.title("A Simple Sine Wave")
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.grid(True)
plt.show()#Adding Style and Detail
x = np.linspace(0, 2 * np.pi, 100)
plt.figure(figsize=(10, 5))
plt.plot(x, np.sin(x), label="sin(x)", color="blue", linewidth=2)
plt.plot(x, np.cos(x), label="cos(x)", color="red", linewidth=2, linestyle="--")
plt.plot(x, np.sin(x) + np.cos(x), label="sin(x) + cos(x)", color="purple", alpha=0.7)
plt.title("Trigonometric Functions", fontsize=16, fontweight="bold")
plt.xlabel("x (radians)", fontsize=12)
plt.ylabel("y", fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color="black", linewidth=0.5) # horizontal line at y=0
plt.tight_layout()
plt.show()Key parameters:
label— text for the legendcolor— line color (names, hex codes, or RGB tuples)linewidth— line thicknesslinestyle—"-"solid,"--"dashed,":"dotted,"-."dash-dotalpha— transparency (0.0 = invisible, 1.0 = opaque)
#Scatter Plots
Scatter plots show the relationship between two numerical variables:
import numpy as np
import matplotlib.pyplot as plt
# Generate correlated data
np.random.seed(42)
hours_studied = np.random.uniform(1, 10, 50)
scores = 40 + 5 * hours_studied + np.random.normal(0, 5, 50)
scores = np.clip(scores, 0, 100)
plt.figure(figsize=(8, 6))
plt.scatter(hours_studied, scores, c="steelblue", alpha=0.7, edgecolors="white", s=80)
# Add a trend line
z = np.polyfit(hours_studied, scores, 1)
p = np.poly1d(z)
x_line = np.linspace(1, 10, 100)
plt.plot(x_line, p(x_line), "r--", linewidth=2, label=f"Trend: y = {z[0]:.1f}x + {z[1]:.1f}")
plt.title("Study Hours vs Test Scores", fontsize=14)
plt.xlabel("Hours Studied", fontsize=12)
plt.ylabel("Test Score", fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()#Color-Coded Scatter Plots
# Color by a third variable
np.random.seed(42)
n = 100
math_scores = np.random.normal(75, 12, n)
science_scores = np.random.normal(78, 10, n)
attendance = np.random.normal(85, 10, n).clip(50, 100)
plt.figure(figsize=(8, 6))
scatter = plt.scatter(
math_scores, science_scores,
c=attendance, # color by attendance
cmap="RdYlGn", # red-yellow-green colormap
s=60, alpha=0.8, edgecolors="white"
)
plt.colorbar(scatter, label="Attendance %")
plt.title("Math vs Science Scores (colored by Attendance)")
plt.xlabel("Math Score")
plt.ylabel("Science Score")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()#Bar Charts and Histograms
#Bar Charts — Comparing Categories
subjects = ["Math", "Science", "English", "History", "Art"]
averages = [78.5, 82.3, 80.1, 75.8, 88.2]
colors = ["#3b82f6", "#22c55e", "#f59e0b", "#ef4444", "#a855f7"]
plt.figure(figsize=(8, 5))
bars = plt.bar(subjects, averages, color=colors, edgecolor="white", width=0.6)
# Add value labels on top of each bar
for bar, val in zip(bars, averages):
plt.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
f"{val}", ha="center", fontsize=11, fontweight="bold")
plt.title("Average Scores by Subject", fontsize=14)
plt.ylabel("Average Score", fontsize=12)
plt.ylim(0, 100)
plt.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()#Grouped Bar Charts
classes = ["9A", "9B", "9C"]
math_avgs = [82, 76, 79]
sci_avgs = [85, 80, 83]
eng_avgs = [78, 81, 77]
x = np.arange(len(classes))
width = 0.25
plt.figure(figsize=(8, 5))
plt.bar(x - width, math_avgs, width, label="Math", color="#3b82f6")
plt.bar(x, sci_avgs, width, label="Science", color="#22c55e")
plt.bar(x + width, eng_avgs, width, label="English", color="#f59e0b")
plt.title("Subject Averages by Class", fontsize=14)
plt.xlabel("Class")
plt.ylabel("Average Score")
plt.xticks(x, classes)
plt.legend()
plt.ylim(0, 100)
plt.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()#Histograms — Seeing Distributions
np.random.seed(42)
scores = np.random.normal(75, 12, 500) # 500 students
plt.figure(figsize=(8, 5))
plt.hist(scores, bins=25, color="steelblue", edgecolor="white", alpha=0.8)
# Add mean and median lines
mean_score = np.mean(scores)
median_score = np.median(scores)
plt.axvline(mean_score, color="red", linestyle="--", linewidth=2, label=f"Mean: {mean_score:.1f}")
plt.axvline(median_score, color="orange", linestyle="-.", linewidth=2, label=f"Median: {median_score:.1f}")
plt.title("Distribution of Student Scores (n=500)", fontsize=14)
plt.xlabel("Score", fontsize=12)
plt.ylabel("Frequency", fontsize=12)
plt.legend(fontsize=11)
plt.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()When should you use a histogram instead of a bar chart?
#Subplots — Multi-Panel Figures
Real analysis often requires multiple plots side by side:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
n = 200
math = np.random.normal(75, 12, n).clip(0, 100)
science = np.random.normal(78, 10, n).clip(0, 100)
english = np.random.normal(80, 11, n).clip(0, 100)
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
fig.suptitle("Student Performance Dashboard", fontsize=16, fontweight="bold")
# Plot 1: Score distributions (overlapping histograms)
ax1 = axes[0, 0]
ax1.hist(math, bins=20, alpha=0.6, label="Math", color="#3b82f6")
ax1.hist(science, bins=20, alpha=0.6, label="Science", color="#22c55e")
ax1.hist(english, bins=20, alpha=0.6, label="English", color="#f59e0b")
ax1.set_title("Score Distributions")
ax1.set_xlabel("Score")
ax1.set_ylabel("Frequency")
ax1.legend()
ax1.grid(axis="y", alpha=0.3)
# Plot 2: Math vs Science scatter
ax2 = axes[0, 1]
ax2.scatter(math, science, alpha=0.5, c="steelblue", edgecolors="white", s=40)
ax2.set_title("Math vs Science Scores")
ax2.set_xlabel("Math Score")
ax2.set_ylabel("Science Score")
ax2.grid(True, alpha=0.3)
# Plot 3: Average by subject (bar chart)
ax3 = axes[1, 0]
subjects = ["Math", "Science", "English"]
means = [np.mean(math), np.mean(science), np.mean(english)]
stds = [np.std(math), np.std(science), np.std(english)]
bars = ax3.bar(subjects, means, color=["#3b82f6", "#22c55e", "#f59e0b"],
edgecolor="white", yerr=stds, capsize=5)
ax3.set_title("Average Scores (with Std Dev)")
ax3.set_ylabel("Score")
ax3.set_ylim(0, 100)
ax3.grid(axis="y", alpha=0.3)
# Plot 4: Box plots for comparison
ax4 = axes[1, 1]
bp = ax4.boxplot([math, science, english], tick_labels=subjects,
patch_artist=True, medianprops={"color": "black", "linewidth": 2})
# (Matplotlib 3.9+: use `tick_labels=`. The older `labels=` is deprecated.)
colors = ["#3b82f6", "#22c55e", "#f59e0b"]
for patch, color in zip(bp["boxes"], colors):
patch.set_facecolor(color)
patch.set_alpha(0.7)
ax4.set_title("Score Distribution (Box Plots)")
ax4.set_ylabel("Score")
ax4.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()#Subplot Syntax
# Method 1: fig, axes = plt.subplots(rows, cols)
fig, axes = plt.subplots(2, 3, figsize=(15, 8)) # 2 rows, 3 columns
axes[0, 0].plot(x, y) # access by row, col index
axes[1, 2].bar(...) # bottom-right plot
# Method 2: Single row or column
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 4))
ax1.plot(x, y)
ax2.scatter(x, y)
ax3.hist(y)
# Always call tight_layout() to prevent overlapping
plt.tight_layout()#Customization and Best Practices
#Choosing the Right Chart
| Chart Type | Best For | Example |
|---|---|---|
| Line plot | Trends over time or continuous sequences | Training loss over epochs |
| Scatter plot | Relationship between two numerical variables | Height vs weight |
| Bar chart | Comparing categories | Average score per subject |
| Histogram | Distribution of a single numerical variable | Score distribution |
| Box plot | Comparing distributions across groups | Scores by class section |
| Pie chart | Part-to-whole relationships (use sparingly) | Market share |
| Heatmap | Correlation matrices or 2D data | Feature correlations |
#Saving Plots
# Save to file (always save BEFORE plt.show())
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot([1, 2, 3], [4, 5, 6])
ax.set_title("My Plot")
fig.savefig("my_plot.png", dpi=150, bbox_inches="tight") # PNG
fig.savefig("my_plot.pdf", bbox_inches="tight") # PDF (vector)
fig.savefig("my_plot.svg", bbox_inches="tight") # SVG (vector)
plt.show()#Matplotlib Style Sheets
# Use a built-in style
plt.style.use("seaborn-v0_8-whitegrid") # clean, modern look
# Other popular styles:
# plt.style.use("ggplot") # R's ggplot2 look
# plt.style.use("dark_background") # dark theme
# plt.style.use("fivethirtyeight") # FiveThirtyEight blog style#A Taste of Seaborn
import seaborn as sns
import pandas as pd
# Seaborn works beautifully with Pandas DataFrames
df = pd.DataFrame({
"student": [f"S{i}" for i in range(100)],
"hours_studied": np.random.uniform(1, 10, 100),
"score": np.random.normal(75, 12, 100).clip(0, 100).round(),
"class": np.random.choice(["9A", "9B", "9C"], 100),
})
# Scatter with regression line -- one line of code!
sns.lmplot(data=df, x="hours_studied", y="score", hue="class",
height=5, aspect=1.3)
plt.title("Study Hours vs Score by Class")
plt.show()
# Distribution plot
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
sns.histplot(data=df, x="score", hue="class", kde=True, ax=axes[0])
axes[0].set_title("Score Distribution by Class")
sns.boxplot(data=df, x="class", y="score", hue="class", palette="Set2", legend=False, ax=axes[1])
axes[1].set_title("Score Box Plots by Class")
plt.tight_layout()
plt.show()
# Heatmap -- great for correlation matrices
numeric_cols = df[["hours_studied", "score"]].copy()
numeric_cols["attendance"] = np.random.normal(85, 10, 100)
corr = numeric_cols.corr()
plt.figure(figsize=(6, 4))
sns.heatmap(corr, annot=True, cmap="RdBu_r", center=0, fmt=".2f",
square=True, linewidths=1)
plt.title("Feature Correlation Matrix")
plt.tight_layout()
plt.show()Seaborn is not a replacement for Matplotlib — it is built on top of it. You often use both together: Seaborn for high-level statistical plots, Matplotlib for customization.
#Code Playground
Tests · Prepare data for visualization, calculate statistics, and explore trends!
#ML-Specific Plots You'll Use Every Day
import matplotlib.pyplot as plt
import numpy as np
# 1. Training loss curve — the most important ML plot
epochs = range(1, 21)
train_loss = [1.0 * (0.85 ** e) + np.random.normal(0, 0.02) for e in epochs]
val_loss = [1.0 * (0.87 ** e) + np.random.normal(0, 0.03) for e in epochs]
plt.figure(figsize=(8, 4))
plt.plot(epochs, train_loss, 'b-o', label='Training Loss', markersize=4)
plt.plot(epochs, val_loss, 'r-o', label='Validation Loss', markersize=4)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Progress — watch for validation loss rising (overfitting!)')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
# 2. Feature importance bar chart
features = ['Credit Score', 'Income', 'Loan Amount', 'Employment Yrs']
importance = [0.38, 0.27, 0.22, 0.13]
colors = ['#818cf8', '#22d3ee', '#34d399', '#fbbf24']
plt.barh(features, importance, color=colors)
plt.xlabel('Importance Score')
plt.title('Which features does the model rely on?')
plt.tight_layout()
plt.show()#Key Takeaways
plt.plot()for lines,plt.scatter()for relationships,plt.bar()for categories,plt.hist()for distributions — choosing the right chart type is the most important visualization decision- Always label your plots — every chart needs a title, axis labels, and a legend (if multiple series). Unlabeled plots are useless to everyone, including your future self
- Subplots let you build dashboards —
fig, axes = plt.subplots(rows, cols)creates a grid of plots. Use this to show multiple perspectives of the same data side by side - Seaborn makes statistical plots effortless — for scatter with regression (
lmplot), distributions (histplot), correlations (heatmap), and box plots (boxplot), Seaborn produces beautiful results in one line - Save before you show —
fig.savefig("plot.png", dpi=150)must come beforeplt.show(). Aftershow(), the figure object is cleared and saving produces a blank image
Which chart type is best for showing the distribution of a single numerical variable?