Cumulative Explained Variance for PCA Component Selection — lets-plot

A line plot showing the cumulative proportion of explained variance as a function of the number of Principal Component Analysis (PCA) components. This visualization helps determine the optimal number of components to retain by displaying the trade-off between dimensionality reduction and information preservation. The cumulative curve typically exhibits an elbow pattern where additional components yield diminishing returns, and horizontal threshold lines (e.g., 90%, 95%) guide component selection decisions.

Cumulative Explained Variance for PCA Component Selection rendered with lets-plot

Python source (lets-plot)

""" anyplot.ai
line-pca-variance-cumulative: Cumulative Explained Variance for PCA Component Selection
Library: letsplot 4.10.1 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-29
"""

import os

import numpy as np
import pandas as pd
from lets_plot import *
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler


LetsPlot.setup_html()

# Theme tokens — Imprint palette, theme-adaptive chrome
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
GRID = "#D8D7D0" if THEME == "light" else "#303030"

BRAND = "#009E73"  # Imprint position 1 — cumulative line (first series)
ANYPLOT_AMBER = "#DDCC77"  # semantic anchor — caution, 90% threshold
MATTE_RED = "#AE3030"  # Imprint position 5 — alert, 95% threshold

# Data — PCA on Wine dataset (13 chemical features)
wine = load_wine()
X = StandardScaler().fit_transform(wine.data)
pca = PCA()
pca.fit(X)

n_components = np.arange(1, len(pca.explained_variance_ratio_) + 1)
individual_var = pca.explained_variance_ratio_ * 100
cumulative_var = np.cumsum(individual_var)

df_cumulative = pd.DataFrame(
    {"Component": n_components, "Cumulative": np.round(cumulative_var, 2), "Individual": np.round(individual_var, 2)}
)
df_individual = pd.DataFrame({"Component": n_components, "Individual": np.round(individual_var, 2)})

# Thresholds and component crossings
threshold_90, threshold_95 = 90.0, 95.0
comp_90 = int(n_components[cumulative_var >= threshold_90][0])
comp_95 = int(n_components[cumulative_var >= threshold_95][0])

# Elbow: component with max perpendicular distance from start→end chord
c1, v1 = 1, cumulative_var[0]
cn, vn = int(n_components[-1]), cumulative_var[-1]
dx, dy = cn - c1, vn - v1
perp_dist = abs(dy * n_components - dx * cumulative_var + cn * v1 - vn * c1) / np.sqrt(dx**2 + dy**2)
elbow_comp = int(n_components[np.argmax(perp_dist)])
elbow_cum = float(cumulative_var[elbow_comp - 1])

df_elbow = pd.DataFrame({"Component": [elbow_comp], "Cumulative": [elbow_cum], "label": [f"PC {elbow_comp}"]})

title = "line-pca-variance-cumulative · python · letsplot · anyplot.ai"

anyplot_chrome = theme(
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG),
    panel_grid_major_x=element_blank(),
    panel_grid_minor=element_blank(),
    panel_grid_major_y=element_line(color=GRID, size=0.4),
    axis_title=element_text(color=INK, size=12),
    axis_text=element_text(color=INK_SOFT, size=10),
    axis_line=element_line(color=INK_SOFT),
    plot_title=element_text(color=INK, size=16),
    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
    legend_text=element_text(color=INK_SOFT, size=10),
    legend_title=element_text(color=INK),
)

plot = (
    ggplot()
    # Secondary: individual variance bars (muted, behind the cumulative line)
    + geom_bar(
        data=df_individual,
        mapping=aes(x="Component", y="Individual"),
        stat="identity",
        fill=INK_MUTED,
        alpha=0.28,
        width=0.65,
    )
    # Threshold reference lines
    + geom_hline(yintercept=threshold_90, linetype="dashed", color=ANYPLOT_AMBER, size=1.0)
    + geom_hline(yintercept=threshold_95, linetype="dashed", color=MATTE_RED, size=1.0)
    + geom_vline(xintercept=comp_90, linetype="dotted", color=ANYPLOT_AMBER, size=0.8, alpha=0.65)
    + geom_vline(xintercept=comp_95, linetype="dotted", color=MATTE_RED, size=0.8, alpha=0.65)
    # Threshold labels (right-aligned near edge)
    + geom_text(
        data=pd.DataFrame({"x": [12.8], "y": [threshold_90 + 2.0], "label": ["90%"]}),
        mapping=aes(x="x", y="y", label="label"),
        color=ANYPLOT_AMBER,
        size=4,
        hjust=1,
    )
    + geom_text(
        data=pd.DataFrame({"x": [12.8], "y": [threshold_95 + 2.0], "label": ["95%"]}),
        mapping=aes(x="x", y="y", label="label"),
        color=MATTE_RED,
        size=4,
        hjust=1,
    )
    # Primary: cumulative variance line (Imprint brand green, first series)
    + geom_line(data=df_cumulative, mapping=aes(x="Component", y="Cumulative"), size=2.0, color=BRAND)
    # Points with lets-plot native tooltips (hover shows both cumulative and individual)
    + geom_point(
        data=df_cumulative,
        mapping=aes(x="Component", y="Cumulative"),
        size=5,
        color=BRAND,
        alpha=0.9,
        tooltips=layer_tooltips()
        .line("PC @Component")
        .line("Cumulative: @Cumulative%")
        .line("Individual: @Individual%"),
    )
    # Elbow annotation: hollow ring marker + label (spec: annotate elbow if detectable)
    + geom_point(
        data=df_elbow, mapping=aes(x="Component", y="Cumulative"), size=9, shape=21, color=INK, fill=ELEVATED_BG
    )
    + geom_text(
        data=df_elbow, mapping=aes(x="Component", y="Cumulative", label="label"), color=INK_SOFT, size=4, hjust=-0.25
    )
    + scale_x_continuous(breaks=list(n_components))
    + scale_y_continuous(breaks=list(range(0, 101, 10)), limits=[0, 106])
    + labs(title=title, x="Number of Principal Components", y="Explained Variance (%)")
    + theme_minimal()
    + anyplot_chrome
    + ggsize(800, 450)
)

# Save
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Cumulative Explained Variance for PCA Component Selection on anyplot.ai.

Other implementations