Spaces:
Sleeping
Sleeping
| # 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() | |