Gini Impurity vs Entropy Comparison — Pygal

A theoretical comparison plot showing Gini impurity and entropy (information gain) as splitting criteria for decision trees across the probability range [0, 1]. Both curves are displayed on the same axes to illustrate their similar behavior and slight differences. This educational visualization helps understand the mathematical foundation of tree-based algorithms and why both criteria lead to similar tree structures in practice.

Gini Impurity vs Entropy Comparison rendered with Pygal

Python source (Pygal)

""" anyplot.ai
line-impurity-comparison: Gini Impurity vs Entropy Comparison
Library: pygal 3.1.0 | Python 3.13.13
Quality: 87/100 | Updated: 2026-05-29
"""

import os
import re
import sys


# Prevent importing local pygal.py file
sys.path = [p for p in sys.path if not p.endswith("/implementations/python")]

import cairosvg
import numpy as np
import pygal
from pygal.style import Style


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"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Data — 200 points for smooth curves across full [0, 1] probability range
p = np.linspace(0, 1, 200)
gini = 2 * p * (1 - p)

with np.errstate(divide="ignore", invalid="ignore"):
    entropy = -p * np.log2(p) - (1 - p) * np.log2(1 - p)
entropy = np.nan_to_num(entropy, nan=0.0)

_font = "Helvetica, Arial, sans-serif"

# Imprint palette — first series is brand green, second is lavender
custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=(
        "#009E73",  # 1: Gini — Imprint green (first categorical series)
        "#C475FD",  # 2: Entropy — Imprint lavender
        INK_MUTED,  # 3: vertical guide line — muted neutral
        "#009E73",  # 4: Gini peak dot — matches Gini
        "#C475FD",  # 5: Entropy peak dot — matches Entropy
    ),
    opacity="1",
    opacity_hover="1",
    stroke_opacity="1",
    stroke_opacity_hover="1",
    stroke_width=2.5,
    guide_stroke_color=INK_MUTED,
    guide_stroke_dasharray="3, 5",
    major_guide_stroke_color=INK_SOFT,
    major_guide_stroke_dasharray="0",
    title_font_size=66,
    label_font_size=56,
    major_label_font_size=44,
    legend_font_size=44,
    value_font_size=36,
    value_label_font_size=48,
    tooltip_font_size=32,
    font_family=_font,
    label_font_family=_font,
    major_label_font_family=_font,
    legend_font_family=_font,
    title_font_family=_font,
    value_font_family=_font,
    value_label_font_family=_font,
)

chart = pygal.XY(
    width=3200,
    height=1800,
    title="line-impurity-comparison · python · pygal · anyplot.ai",
    x_title="Probability p",
    y_title="Impurity measure",
    style=custom_style,
    show_dots=False,
    fill=False,
    show_y_guides=True,
    show_x_guides=False,
    legend_at_bottom=True,
    legend_at_bottom_columns=2,
    legend_box_size=24,
    truncate_legend=-1,
    xrange=(0, 1),
    range=(0, 1.05),
    y_labels=[0, 0.2, 0.4, 0.6, 0.8, 1.0],
    show_y_labels=True,
    x_label_rotation=0,
    margin=80,
    margin_bottom=120,
    margin_left=160,
    margin_right=80,
    margin_top=80,
    interpolate="cubic",
    print_values=False,
    print_zeroes=False,
    print_labels=True,
    x_value_formatter=lambda x: f"{x:.1f}",
    js=[],
)

# Convert to pygal XY point lists
gini_points = list(zip(p.tolist(), gini.tolist(), strict=True))
entropy_points = list(zip(p.tolist(), entropy.tolist(), strict=True))

# Series 1: Gini impurity — Imprint green, first categorical series
chart.add("Gini: 2p(1−p)", gini_points, stroke_style={"width": 5})

# Series 2: Shannon entropy (normalized to [0,1]) — Imprint lavender
chart.add("Entropy: −p log₂p − (1−p) log₂(1−p)", entropy_points, stroke_style={"width": 5})

# Series 3: Vertical guide at p=0.5 (both maxima occur here)
chart.add(None, [(0.5, 0.0), (0.5, 1.05)], stroke_style={"width": 1.5, "dasharray": "8, 6"})

# Series 4: Gini peak annotation dot at (0.5, 0.5)
chart.add(
    None, [{"value": (0.5, 0.5), "label": "Gini peak = 0.50"}], stroke_style={"width": 0}, show_dots=True, dots_size=10
)

# Series 5: Entropy peak annotation dot at (0.5, 1.0)
chart.add(
    None,
    [{"value": (0.5, 1.0), "label": "Entropy peak = 1.00"}],
    stroke_style={"width": 0},
    show_dots=True,
    dots_size=10,
)


def _render_png(chart, filepath):
    """Render chart to PNG, softening the default hard rectangular axis border."""
    svg_bytes = chart.render()
    svg_str = svg_bytes.decode("utf-8")
    # Extract chart id to scope the injected rule precisely
    m = re.search(r'id="(chart-[^"]+)"', svg_str)
    chart_id = f"#{m.group(1)} " if m else ""
    # Soften the default axis border lines (left/bottom frame) to INK_MUTED at 45% opacity.
    # .axis .line targets the axis spine paths; more-specific .axis .major.line continues
    # to use the theme INK color for the major grid anchors, both at reduced opacity.
    border_css = (
        f"\n      {chart_id}.axis .line {{"
        f" stroke: {INK_MUTED} !important;"
        f" stroke-opacity: 0.45 !important; }}"
        f"\n      {chart_id}.axis .major.line {{"
        f" stroke: {INK_MUTED} !important;"
        f" stroke-opacity: 0.45 !important; }}"
        f"\n    "
    )
    svg_str = svg_str.replace("</style>", border_css + "</style>", 1)
    cairosvg.svg2png(bytestring=svg_str.encode("utf-8"), write_to=filepath, output_width=3200, output_height=1800)


_render_png(chart, f"plot-{THEME}.png")
chart.render_to_file(f"plot-{THEME}.svg")
with open(f"plot-{THEME}.html", "wb") as f:
    f.write(chart.render())

Part of Gini Impurity vs Entropy Comparison on anyplot.ai.

Other implementations