Residual Plot in plotnine (Python)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl; JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts. Compare all 15 side by side: Residual Plot in Python, R, Julia and JavaScript.

A residual plot displays the difference between observed and predicted values (residuals) against fitted values in regression analysis. This diagnostic visualization helps identify violations of regression assumptions including non-linearity, heteroscedasticity (non-constant variance), and outliers. A well-fitting model shows residuals randomly scattered around zero with no discernible pattern.

Residual Plot rendered with plotnine

Renders

Python source (plotnine)

""" anyplot.ai
residual-plot: Residual Plot
Library: plotnine 0.15.4 | Python 3.13.13
Quality: 86/100 | Updated: 2026-05-10
"""

import os

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    element_line,
    element_rect,
    element_text,
    geom_hline,
    geom_point,
    geom_smooth,
    ggplot,
    labs,
    scale_color_manual,
    theme,
    theme_minimal,
)


# Theme tokens (see prompts/default-style-guide.md)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
RULE = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"

# Okabe-Ito palette
IMPRINT = [
    "#009E73",  # Brand green (normal points)
    "#C475FD",  # Vermillion (outliers)
    "#4467A3",  # Blue (LOWESS line)
]

# Data - Generate realistic regression data with better labels
np.random.seed(42)
n_points = 150

# House prices regression example
house_size = np.linspace(1000, 5000, n_points)  # Square feet
price_true = 150 * house_size + 50000 + np.random.normal(0, 30000, n_points)

# Add a few outliers (priced unusually high or low for their size)
outlier_indices = [20, 75, 130]
price_true[outlier_indices] += np.array([150000, -120000, 100000])

# Fit linear regression using OLS
size_mean = np.mean(house_size)
price_mean = np.mean(price_true)
slope = np.sum((house_size - size_mean) * (price_true - price_mean)) / np.sum((house_size - size_mean) ** 2)
intercept = price_mean - slope * size_mean
price_pred = slope * house_size + intercept

# Calculate residuals
residuals = price_true - price_pred

# Identify outliers (beyond 2 standard deviations)
std_resid = np.std(residuals)
is_outlier = np.abs(residuals) > 2 * std_resid
point_type = np.where(is_outlier, "Outlier", "Normal")

# Create DataFrame
df = pd.DataFrame({"fitted": price_pred, "residuals": residuals, "point_type": point_type})

# Create theme customization
anyplot_theme = theme(
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG),
    panel_grid_major=element_line(color=INK, size=0.3, alpha=0.10),
    panel_grid_minor=element_line(color=INK, size=0.2, alpha=0.05),
    panel_border=element_rect(color=INK_SOFT, fill=None),
    axis_title=element_text(color=INK, size=20),
    axis_text=element_text(color=INK_SOFT, size=16),
    axis_line=element_line(color=INK_SOFT),
    plot_title=element_text(color=INK, size=24),
    legend_background=element_rect(fill=PAGE_BG, color=INK_SOFT),
    legend_text=element_text(color=INK_SOFT, size=16),
    legend_title=element_text(color=INK, size=18),
    figure_size=(16, 9),
)

# Create residual plot
plot = (
    ggplot(df, aes(x="fitted", y="residuals", color="point_type"))
    + geom_hline(yintercept=0, color=INK_SOFT, size=1.2, linetype="solid", alpha=0.8)
    + geom_hline(yintercept=2 * std_resid, color=INK_SOFT, size=0.8, linetype="dashed", alpha=0.5)
    + geom_hline(yintercept=-2 * std_resid, color=INK_SOFT, size=0.8, linetype="dashed", alpha=0.5)
    + geom_point(size=4, alpha=0.7)
    + geom_smooth(aes(group=1), method="lowess", color=IMPRINT[2], size=1.5, se=False, span=0.5)
    + scale_color_manual(values={"Normal": IMPRINT[0], "Outlier": IMPRINT[1]}, name="Point Type")
    + labs(x="Fitted Values ($)", y="Residuals ($)", title="residual-plot · plotnine · anyplot.ai")
    + theme_minimal()
    + anyplot_theme
)

# Save
output_path = os.path.join(os.path.dirname(__file__), f"plot-{THEME}.png")
plot.save(output_path, dpi=300)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/residual-plot/plotnine/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "residual-plot",
  "language": "python",
  "library": "plotnine",
  "page": "https://anyplot.ai/residual-plot/python/plotnine",
  "hub": "https://anyplot.ai/residual-plot",
  "code_json": "https://api.anyplot.ai/specs/residual-plot/plotnine/code",
  "spec_json": "https://api.anyplot.ai/specs/residual-plot",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/residual-plot/python/plotnine/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/residual-plot/python/plotnine/plot-dark.png",
  "quality_score": 86.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Residual Plot on anyplot.ai.

Other implementations