A histogram showing the distribution of financial returns (daily, weekly, or monthly) with a normal distribution overlay for comparison. This visualization is essential for risk analysis, allowing analysts to assess whether returns follow a normal distribution, identify fat tails indicating higher-than-expected extreme events, and measure asymmetry through skewness. Key statistics are displayed directly on the plot for quick interpretation.

""" anyplot.ai
histogram-returns-distribution: Returns Distribution Histogram
Library: plotnine 0.15.4 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-20
"""
import os
import numpy as np
import pandas as pd
from plotnine import (
aes,
annotate,
element_blank,
element_line,
element_rect,
element_text,
geom_histogram,
geom_line,
geom_vline,
ggplot,
labs,
scale_fill_manual,
theme,
theme_minimal,
)
from scipy import stats
# Theme tokens
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"
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477"]
# Data - 252 trading days with slight fat tails (mixture of normals)
np.random.seed(42)
n_days = 252
normal_returns = np.random.normal(0.0005, 0.015, int(n_days * 0.9))
fat_tail_returns = np.random.normal(0, 0.04, int(n_days * 0.1))
returns = np.concatenate([normal_returns, fat_tail_returns])
np.random.shuffle(returns)
returns = returns[:n_days]
returns_pct = returns * 100
mean_ret = np.mean(returns_pct)
std_ret = np.std(returns_pct)
skewness = stats.skew(returns_pct)
kurtosis = stats.kurtosis(returns_pct)
lower_tail = mean_ret - 2 * std_ret
upper_tail = mean_ret + 2 * std_ret
df = pd.DataFrame({"returns": returns_pct})
df["region"] = pd.cut(
df["returns"], bins=[-np.inf, lower_tail, upper_tail, np.inf], labels=["Left Tail", "Center", "Right Tail"]
)
# Normal distribution overlay (count scale to match histogram)
x_range = np.linspace(returns_pct.min() - 1, returns_pct.max() + 1, 300)
normal_pdf = stats.norm.pdf(x_range, mean_ret, std_ret)
bin_width = (returns_pct.max() - returns_pct.min()) / 30
normal_scaled = normal_pdf * len(returns_pct) * bin_width
df_normal = pd.DataFrame({"x": x_range, "y": normal_scaled})
stats_label = f"Mean: {mean_ret:.2f}%\nStd Dev: {std_ret:.2f}%\nSkewness: {skewness:.2f}\nExc. Kurtosis: {kurtosis:.2f}"
# Okabe-Ito fill: center green (pos 1), both tails vermillion (pos 2)
fill_colors = {"Left Tail": IMPRINT[1], "Center": IMPRINT[0], "Right Tail": IMPRINT[1]}
# Plot
plot = (
ggplot(df, aes(x="returns", fill="region"))
+ geom_histogram(bins=30, color=PAGE_BG, alpha=0.85, size=0.3)
+ geom_line(data=df_normal, mapping=aes(x="x", y="y"), color=IMPRINT[2], size=1.2, inherit_aes=False)
+ geom_vline(xintercept=mean_ret, linetype="dashed", color=INK, size=0.8)
+ geom_vline(xintercept=lower_tail, linetype="dotted", color=IMPRINT[1], size=0.8)
+ geom_vline(xintercept=upper_tail, linetype="dotted", color=IMPRINT[1], size=0.8)
+ scale_fill_manual(values=fill_colors, name="Region")
+ annotate(
"label",
x=returns_pct.max() - 0.5,
y=max(normal_scaled) * 0.92,
label=stats_label,
ha="right",
va="top",
size=9,
fill=ELEVATED_BG,
color=INK,
label_size=0.3,
label_padding=0.4,
)
+ labs(
x="Daily Returns (%)", y="Frequency", title="histogram-returns-distribution · python · plotnine · anyplot.ai"
)
+ theme_minimal()
+ theme(
figure_size=(8, 4.5),
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
panel_background=element_rect(fill=PAGE_BG),
panel_border=element_blank(),
axis_line=element_line(color=INK_SOFT),
panel_grid_major=element_line(color=INK_SOFT, size=0.3, alpha=0.10),
panel_grid_minor=element_blank(),
plot_title=element_text(color=INK, size=12),
axis_title=element_text(color=INK, size=10),
axis_text=element_text(color=INK_SOFT, size=8),
legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
legend_text=element_text(color=INK_SOFT, size=8),
legend_title=element_text(color=INK, size=8),
legend_position="right",
)
)
# Save
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)
Part of Returns Distribution Histogram on anyplot.ai.