Skip to main content

How to Plot Predicted vs Actual Values with Matplotlib

· 7 min read
Jagdish Kumawat
Founder @ Dewiride

A single error score tells you how much a regression model misses on average. A predicted vs actual chart shows where it misses: which values it gets right, which it underestimates, and where the data itself is odd. This guide draws the chart with Matplotlib, saves it as a PNG, and reads a real one from a scikit-learn model.

Syntax​

Syntax
from matplotlib.figure import Figure

figure = Figure(figsize=(<width>, <height>))
axes = figure.subplots()
axes.scatter(<actual values>, <predicted values>)
axes.plot([<low>, <high>], [<low>, <high>], linestyle="--")
figure.savefig("<file name>.png")
PartWhat it means
Figure(figsize=(width, height))A new, empty chart, width by height inches
figure.subplots()Adds one set of axes to draw on
axes.scatter(actual, predicted)One dot per row: the real value across, the prediction up
axes.plot([low, high], [low, high])The diagonal where the prediction equals the real value. A dot on it is a perfect prediction
figure.savefig(path)Saves the chart to a file. The extension picks the format

Prerequisites​

Step 1: Write the Plot Function​

The predicted_vs_actual function in a macOS-style code window: it creates a 6 by 6 Figure, draws a scatter of actual against predicted values, adds a dashed gray perfect-prediction line, labels both axes in units of $100,000, sets the title and adds a legend.

plot_predicted_vs_actual.py (the function)
import numpy as np
from matplotlib.figure import Figure


def predicted_vs_actual(actual: np.ndarray, predicted: np.ndarray) -> Figure:
"""Plot each prediction against the true value. Perfect predictions sit on the dashed line."""
figure = Figure(figsize=(6, 6), layout="constrained")
axes = figure.subplots()
axes.scatter(actual, predicted, s=4, alpha=0.3)
low = min(np.min(actual), np.min(predicted))
high = max(np.max(actual), np.max(predicted))
axes.plot([low, high], [low, high], linestyle="--", color="gray", label="Perfect prediction")
axes.set_xlabel("Actual median house value ($100,000s)")
axes.set_ylabel("Predicted median house value ($100,000s)")
axes.set_title("Predicted vs actual house values")
axes.legend(loc="upper left")
return figure
LineWhat it does
Figure(figsize=(6, 6), layout="constrained")A square figure, 6 by 6 inches. A square makes the diagonal a true 45° line. constrained keeps the labels from being cut off
axes.scatter(..., s=4, alpha=0.3)One small, semi-transparent dot per prediction, so dense areas look darker
axes.plot([low, high], [low, high], ...)The dashed diagonal where predicted equals actual
set_xlabel, set_ylabel, set_titleLabels with units, so the chart makes sense on its own

Why Figure and not pyplot? matplotlib.pyplot keeps every figure it creates in memory until you close it, and needs a display backend. Matplotlib's own documentation says: "When using Matplotlib in a web server it is strongly recommended to not use pyplot (pyplot maintains references to the opened figures to make show work, but this will cause memory leaks unless the figures are properly closed)." A Figure built directly is an ordinary object that Python frees when you're done, so it's safe in scripts, servers and training jobs.

Step 2: Train a Model and Save the Chart​

Add below the function:

plot_predicted_vs_actual.py (add at the end)
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

frame = fetch_california_housing(as_frame=True).frame
X, y = frame.drop(columns="MedHouseVal"), frame["MedHouseVal"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1).fit(X_train, y_train)

figure = predicted_vs_actual(y_test.to_numpy(), model.predict(X_test))
figure.savefig("predicted_vs_actual.png", dpi=150)
print("Saved predicted_vs_actual.png")

In the complete file, keep all imports together at the top. Run it:

Terminal
uv run python plot_predicted_vs_actual.py
Output
Saved predicted_vs_actual.png

savefig writes the image; dpi=150 makes it 900 by 900 pixels (6 inches × 150 dots per inch).

Step 3: Read the Chart​

A predicted vs actual scatter plot of 4,128 California districts: dots cluster tightly around the dashed diagonal at low values, spread wider above about $300,000, and form a vertical column at an actual value of 5.0 where predictions range from about 2 to 5.

Each dot is one district from the test set: its real value across, the model's guess up.

  • Dots on the dashed line are perfect predictions. Most cheap and mid-priced districts sit close to it: the model is good there.
  • The cloud widens as values grow. Above about 3 ($300,000) the dots spread further from the line, so errors get bigger for expensive districts.
  • Dots below the line are underestimates. At the top right, most dots are under the line: the model tends to guess too low for expensive areas.
  • The vertical column at 5.0 is the data, not the model. The dataset caps every district at 5.0 ($500,000), so districts worth far more all share the same "actual" value, while the model's guesses for them range from about 2 to 5.

That last point is something no single score would ever show you.

Common Mistakes​

MistakeWhat happensDo this instead
Plotting predictions for the training rowsThe dots hug the line, because the model has seen those rows: this forest scores R² 0.974 on its training rows against 0.804 on the test rowsPlot the test set
A rectangular figureThe diagonal isn't at 45°, so distances to it look skewedUse a square figsize
Creating many figures with pyplot in a loopMemory grows until you call plt.close()Build a Figure directly, as above
No axis labels or unitsNobody can read the chart laterLabel both axes with units

Troubleshooting​

SymptomLikely causeFix
A solid blob with no detailToo many large, opaque dotsLower s and alpha, as in s=4, alpha=0.3
Labels cut off at the edgesThe default layout doesn't make roomUse layout="constrained"

FAQ​

Can I log this chart to MLflow instead of saving a file?​

Yes. Pass the figure to mlflow.log_figure(): How to Log a Matplotlib Figure to MLflow.

Does this work for classification models?​

No, a classifier predicts classes, not numbers. Classifiers use other charts, such as a confusion matrix.

What about the error numbers?​

They complement the chart: How to Calculate MAE, RMSE and R² in scikit-learn.

Conclusion​

predicted_vs_actual() built a square Figure with one dot per test district and a dashed perfect-prediction line, and savefig wrote it as a PNG. The chart showed what the scores alone couldn't: good predictions for most districts, bigger misses and underestimates for expensive ones, and a column of capped values at $500,000.

Additional Resources​