eaglelandsonce's picture
Update app.py
17016d9 verified
Raw
History Blame Contribute Delete
6.49 kB
# normal_explorer_app.py
# Run locally:
# pip install -r requirements.txt
# python normal_explorer_app.py
#
# This launches a Gradio UI where you can change μ, σ, sample size, etc.,
# and see the curve update along with computed descriptive statistics.
import numpy as np
import gradio as gr
import matplotlib.pyplot as plt
from math import sqrt, pi
def normal_pdf(x, mu, sigma):
return (1.0 / (sigma * sqrt(2.0 * pi))) * np.exp(-0.5 * ((x - mu) / sigma) ** 2)
def theoretical_stats(mu, sigma):
# For a perfect Normal(μ, σ^2)
variance = sigma**2
median = mu
mode = mu
# IQR for a normal: ≈ 1.349 * σ
iqr = 1.3489795 * sigma
return {
"mean": mu,
"median": median,
"mode": mode,
"variance": variance,
"std_dev": sigma,
"IQR": iqr,
"range": float("inf"), # theoretical
"skewness": 0.0,
"kurtosis": 3.0, # Fisher definition
"excess_kurtosis": 0.0
}
def sample_stats(sample):
n = len(sample)
if n < 2:
# Degenerate case handling
s_mean = float(sample[0]) if n == 1 else float("nan")
return {
"mean": s_mean,
"median": s_mean,
"mode": s_mean,
"variance": 0.0,
"std_dev": 0.0,
"IQR": 0.0,
"range": 0.0,
"skewness": 0.0,
"kurtosis": 3.0,
"excess_kurtosis": 0.0
}
s = np.asarray(sample, dtype=float)
s_mean = float(np.mean(s))
s_median = float(np.median(s))
# Estimate mode via histogram bin center
counts, bin_edges = np.histogram(s, bins=min(50, max(5, int(np.sqrt(n)))))
max_bin_idx = int(np.argmax(counts))
mode_est = float((bin_edges[max_bin_idx] + bin_edges[max_bin_idx + 1]) / 2.0)
# Sample variance with ddof=1
s_var = float(np.var(s, ddof=1))
s_std = float(np.sqrt(s_var))
q1 = float(np.percentile(s, 25))
q3 = float(np.percentile(s, 75))
iqr = q3 - q1
s_range = float(np.max(s) - np.min(s))
# Skewness and kurtosis
m2 = np.mean((s - s_mean)**2)
m3 = np.mean((s - s_mean)**3)
m4 = np.mean((s - s_mean)**4)
if m2 <= 0:
skew = 0.0
kurt = 3.0
else:
skew = m3 / (m2 ** 1.5)
kurt = m4 / (m2 ** 2)
ex_kurt = kurt - 3.0
return {
"mean": s_mean,
"median": s_median,
"mode": mode_est,
"variance": s_var,
"std_dev": s_std,
"IQR": float(iqr),
"range": s_range,
"skewness": float(skew),
"kurtosis": float(kurt),
"excess_kurtosis": float(ex_kurt)
}
def format_stats_block(title, d):
# Handle range separately (∞ if inf)
range_str = "∞" if d["range"] == float("inf") else f"{d['range']:.6g}"
lines = [
f"**{title}**",
f"- Mean: {d['mean']:.6g}",
f"- Median: {d['median']:.6g}",
f"- Mode: {d['mode']:.6g}",
f"- Variance: {d['variance']:.6g}",
f"- Std Dev: {d['std_dev']:.6g}",
f"- IQR: {d['IQR']:.6g}",
f"- Range: {range_str}",
f"- Skewness: {d['skewness']:.6g}",
f"- Kurtosis: {d['kurtosis']:.6g}",
f"- Excess Kurtosis: {d['excess_kurtosis']:.6g}",
]
return "\n".join(lines)
def render(mu, sigma, n, seed, x_min, x_max, bins, show_hist, overlay_empirical_pdf):
sigma = max(1e-6, sigma)
# X range
if x_min >= x_max:
x_min = mu - 4 * sigma
x_max = mu + 4 * sigma
x = np.linspace(x_min, x_max, 600)
y = normal_pdf(x, mu, sigma)
# Sample
rng = np.random.default_rng(int(seed))
sample = rng.normal(loc=mu, scale=sigma, size=int(n))
# Stats
theo = theoretical_stats(mu, sigma)
samp = sample_stats(sample)
# Plot
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=120)
ax.plot(x, y, label="Theoretical PDF")
if show_hist:
ax.hist(sample, bins=int(bins), density=True, alpha=0.5, label="Sample histogram")
if overlay_empirical_pdf:
bw = 1.06 * samp["std_dev"] * (len(sample) ** (-1/5)) if samp["std_dev"] > 0 else sigma / sqrt(n)
bw = max(bw, 1e-6)
diffs = (x.reshape(-1, 1) - sample.reshape(1, -1)) / bw
kernel_vals = np.exp(-0.5 * diffs**2) / (sqrt(2 * pi) * bw)
kde = np.mean(kernel_vals, axis=1)
ax.plot(x, kde, linestyle="--", label="Empirical density (KDE-like)")
ax.set_title("Normal Distribution Explorer")
ax.set_xlabel("x")
ax.set_ylabel("density")
ax.legend(loc="best")
ax.grid(True, linestyle="--")
# Stats text
left = format_stats_block("Theoretical (Normal)", theo)
right = format_stats_block("Sample (from sliders)", samp)
stats_md = left + "\n\n" + right
return fig, stats_md
with gr.Blocks(title="Normal Distribution Explorer") as demo:
gr.Markdown("# Normal Distribution Explorer")
gr.Markdown(
"Adjust **mean (μ)**, **standard deviation (σ)**, **sample size (n)**, and the plotting window. "
"See the theoretical PDF curve update live, optionally overlay a **sample histogram** and an "
"empirical density, and compare **theoretical** vs **sample** descriptive statistics."
)
with gr.Row():
with gr.Column(scale=1):
mu = gr.Slider(-10.0, 10.0, value=0.0, step=0.1, label="Mean (μ)")
sigma = gr.Slider(0.1, 10.0, value=1.0, step=0.1, label="Std Dev (σ)")
n = gr.Slider(10, 200000, value=1000, step=10, label="Sample size (n)")
seed = gr.Slider(0, 99999, value=42, step=1, label="Random seed")
with gr.Accordion("Plot window & layers", open=False):
x_min = gr.Number(value=-5.0, label="x min")
x_max = gr.Number(value=5.0, label="x max")
bins = gr.Slider(5, 200, value=40, step=1, label="Histogram bins")
show_hist = gr.Checkbox(value=True, label="Show sample histogram")
overlay_empirical_pdf = gr.Checkbox(value=False, label="Overlay empirical density (KDE-like)")
with gr.Column(scale=2):
plot = gr.Plot(label="Curve / Histogram")
stats = gr.Markdown(label="Descriptive Statistics")
inputs = [mu, sigma, n, seed, x_min, x_max, bins, show_hist, overlay_empirical_pdf]
demo.load(render, inputs=inputs, outputs=[plot, stats])
for w in inputs:
w.change(render, inputs=inputs, outputs=[plot, stats])
if __name__ == "__main__":
demo.launch()