Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# skew_normal_explorer_app.py
|
2 |
+
# Run locally:
|
3 |
+
# pip install -r requirements.txt
|
4 |
+
# python skew_normal_explorer_app.py
|
5 |
+
|
6 |
+
import numpy as np
|
7 |
+
import gradio as gr
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
from math import sqrt, pi
|
10 |
+
from scipy.stats import skewnorm
|
11 |
+
|
12 |
+
def theoretical_stats_skewnorm(alpha, mu, sigma):
|
13 |
+
"""
|
14 |
+
Compute theoretical moments for the Skew-Normal(alpha, loc=mu, scale=sigma).
|
15 |
+
Returns mean, median, mode (numeric), variance, std dev, IQR, skewness,
|
16 |
+
kurtosis (Pearson) & excess kurtosis.
|
17 |
+
"""
|
18 |
+
m, v, s, exk = skewnorm.stats(alpha, loc=mu, scale=sigma, moments="mvsk")
|
19 |
+
m = float(m); v = float(v); s = float(s); exk = float(exk)
|
20 |
+
std = float(np.sqrt(v))
|
21 |
+
|
22 |
+
# Quartiles
|
23 |
+
q1 = float(skewnorm.ppf(0.25, alpha, loc=mu, scale=sigma))
|
24 |
+
q3 = float(skewnorm.ppf(0.75, alpha, loc=mu, scale=sigma))
|
25 |
+
iqr = q3 - q1
|
26 |
+
|
27 |
+
# Median
|
28 |
+
median = float(skewnorm.ppf(0.5, alpha, loc=mu, scale=sigma))
|
29 |
+
|
30 |
+
# Mode via grid search
|
31 |
+
xs = np.linspace(m - 6*std, m + 6*std, 4001)
|
32 |
+
pdf_vals = skewnorm.pdf(xs, alpha, loc=mu, scale=sigma)
|
33 |
+
mode = float(xs[int(np.argmax(pdf_vals))])
|
34 |
+
|
35 |
+
return {
|
36 |
+
"mean": m,
|
37 |
+
"median": median,
|
38 |
+
"mode": mode,
|
39 |
+
"variance": v,
|
40 |
+
"std_dev": std,
|
41 |
+
"IQR": iqr,
|
42 |
+
"range": float("inf"),
|
43 |
+
"skewness": s,
|
44 |
+
"kurtosis": exk + 3.0,
|
45 |
+
"excess_kurtosis": exk
|
46 |
+
}
|
47 |
+
|
48 |
+
def sample_stats(sample):
|
49 |
+
n = len(sample)
|
50 |
+
if n < 2:
|
51 |
+
s_mean = float(sample[0]) if n == 1 else float("nan")
|
52 |
+
return {
|
53 |
+
"mean": s_mean, "median": s_mean, "mode": s_mean,
|
54 |
+
"variance": 0.0, "std_dev": 0.0, "IQR": 0.0, "range": 0.0,
|
55 |
+
"skewness": 0.0, "kurtosis": 3.0, "excess_kurtosis": 0.0
|
56 |
+
}
|
57 |
+
|
58 |
+
s = np.asarray(sample, dtype=float)
|
59 |
+
s_mean = float(np.mean(s))
|
60 |
+
s_median = float(np.median(s))
|
61 |
+
|
62 |
+
# Mode estimate via histogram
|
63 |
+
counts, bin_edges = np.histogram(s, bins=min(50, max(5, int(np.sqrt(n)))))
|
64 |
+
max_bin_idx = int(np.argmax(counts))
|
65 |
+
mode_est = float((bin_edges[max_bin_idx] + bin_edges[max_bin_idx+1]) / 2.0)
|
66 |
+
|
67 |
+
s_var = float(np.var(s, ddof=1))
|
68 |
+
s_std = float(np.sqrt(s_var))
|
69 |
+
|
70 |
+
q1, q3 = np.percentile(s, [25, 75])
|
71 |
+
iqr = q3 - q1
|
72 |
+
s_range = float(np.max(s) - np.min(s))
|
73 |
+
|
74 |
+
m2 = np.mean((s - s_mean)**2)
|
75 |
+
m3 = np.mean((s - s_mean)**3)
|
76 |
+
m4 = np.mean((s - s_mean)**4)
|
77 |
+
if m2 <= 0:
|
78 |
+
skew, kurt = 0.0, 3.0
|
79 |
+
else:
|
80 |
+
skew = m3 / (m2 ** 1.5)
|
81 |
+
kurt = m4 / (m2 ** 2)
|
82 |
+
ex_kurt = kurt - 3.0
|
83 |
+
|
84 |
+
return {
|
85 |
+
"mean": s_mean,
|
86 |
+
"median": s_median,
|
87 |
+
"mode": mode_est,
|
88 |
+
"variance": s_var,
|
89 |
+
"std_dev": s_std,
|
90 |
+
"IQR": iqr,
|
91 |
+
"range": s_range,
|
92 |
+
"skewness": skew,
|
93 |
+
"kurtosis": kurt,
|
94 |
+
"excess_kurtosis": ex_kurt
|
95 |
+
}
|
96 |
+
|
97 |
+
def format_stats_block(title, d):
|
98 |
+
range_str = "∞" if d["range"] == float("inf") else f"{d['range']:.6g}"
|
99 |
+
lines = [
|
100 |
+
f"**{title}**",
|
101 |
+
f"- Mean: {d['mean']:.6g}",
|
102 |
+
f"- Median: {d['median']:.6g}",
|
103 |
+
f"- Mode: {d['mode']:.6g}",
|
104 |
+
f"- Variance: {d['variance']:.6g}",
|
105 |
+
f"- Std Dev: {d['std_dev']:.6g}",
|
106 |
+
f"- IQR: {d['IQR']:.6g}",
|
107 |
+
f"- Range: {range_str}",
|
108 |
+
f"- Skewness: {d['skewness']:.6g}",
|
109 |
+
f"- Kurtosis: {d['kurtosis']:.6g}",
|
110 |
+
f"- Excess Kurtosis: {d['excess_kurtosis']:.6g}",
|
111 |
+
]
|
112 |
+
return "\n".join(lines)
|
113 |
+
|
114 |
+
def render(alpha, mu, sigma, n, seed, x_min, x_max, bins, show_hist, overlay_empirical_pdf):
|
115 |
+
sigma = max(1e-6, sigma)
|
116 |
+
|
117 |
+
if x_min >= x_max:
|
118 |
+
theo_tmp = theoretical_stats_skewnorm(alpha, mu, sigma)
|
119 |
+
m, std = theo_tmp["mean"], max(1e-9, theo_tmp["std_dev"])
|
120 |
+
x_min, x_max = m - 4*std, m + 4*std
|
121 |
+
|
122 |
+
x = np.linspace(x_min, x_max, 800)
|
123 |
+
y = skewnorm.pdf(x, alpha, loc=mu, scale=sigma)
|
124 |
+
|
125 |
+
rng = np.random.default_rng(int(seed))
|
126 |
+
sample = skewnorm.rvs(alpha, loc=mu, scale=sigma, size=int(n), random_state=rng)
|
127 |
+
|
128 |
+
theo = theoretical_stats_skewnorm(alpha, mu, sigma)
|
129 |
+
samp = sample_stats(sample)
|
130 |
+
|
131 |
+
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=120)
|
132 |
+
ax.plot(x, y, label="Theoretical PDF (Skew-Normal)")
|
133 |
+
if show_hist:
|
134 |
+
ax.hist(sample, bins=int(bins), density=True, alpha=0.5, label="Sample histogram")
|
135 |
+
|
136 |
+
if overlay_empirical_pdf:
|
137 |
+
bw = 1.06 * max(1e-8, samp["std_dev"]) * (len(sample) ** (-1/5))
|
138 |
+
bw = max(bw, 1e-6)
|
139 |
+
diffs = (x.reshape(-1, 1) - sample.reshape(1, -1)) / bw
|
140 |
+
kernel_vals = np.exp(-0.5 * diffs**2) / (sqrt(2*pi) * bw)
|
141 |
+
kde = np.mean(kernel_vals, axis=1)
|
142 |
+
ax.plot(x, kde, linestyle="--", label="Empirical density (KDE-like)")
|
143 |
+
|
144 |
+
ax.set_title("Skew-Normal & Normal Explorer (α=0 gives Normal)")
|
145 |
+
ax.set_xlabel("x")
|
146 |
+
ax.set_ylabel("density")
|
147 |
+
ax.legend(loc="best")
|
148 |
+
ax.grid(True, linestyle="--")
|
149 |
+
|
150 |
+
left = format_stats_block("Theoretical (Skew-Normal)", theo)
|
151 |
+
right = format_stats_block("Sample (from sliders)", samp)
|
152 |
+
stats_md = left + "\n\n" + right
|
153 |
+
|
154 |
+
return fig, stats_md
|
155 |
+
|
156 |
+
with gr.Blocks(title="Skew-Normal & Normal Explorer") as demo:
|
157 |
+
gr.Markdown("# Skew-Normal & Normal Explorer")
|
158 |
+
gr.Markdown(
|
159 |
+
"Slide **α (skewness)** to skew left/right. **α=0 → Normal(μ, σ²)**. "
|
160 |
+
"Adjust μ, σ, n, and window. Compare theoretical vs sample stats."
|
161 |
+
)
|
162 |
+
|
163 |
+
with gr.Row():
|
164 |
+
with gr.Column(scale=1):
|
165 |
+
alpha = gr.Slider(-15.0, 15.0, value=0.0, step=0.1, label="Skewness (α)")
|
166 |
+
mu = gr.Slider(-10.0, 10.0, value=0.0, step=0.1, label="Location (μ)")
|
167 |
+
sigma = gr.Slider(0.1, 10.0, value=1.0, step=0.1, label="Scale (σ)")
|
168 |
+
n = gr.Slider(10, 200000, value=2000, step=10, label="Sample size (n)")
|
169 |
+
seed = gr.Slider(0, 99999, value=42, step=1, label="Random seed")
|
170 |
+
|
171 |
+
with gr.Accordion("Plot window & layers", open=False):
|
172 |
+
x_min = gr.Number(value=-5.0, label="x min")
|
173 |
+
x_max = gr.Number(value=5.0, label="x max")
|
174 |
+
bins = gr.Slider(5, 200, value=40, step=1, label="Histogram bins")
|
175 |
+
show_hist = gr.Checkbox(value=True, label="Show sample histogram")
|
176 |
+
overlay_empirical_pdf = gr.Checkbox(value=False, label="Overlay empirical density (KDE-like)")
|
177 |
+
|
178 |
+
with gr.Column(scale=2):
|
179 |
+
plot = gr.Plot(label="Curve / Histogram")
|
180 |
+
stats = gr.Markdown(label="Descriptive Statistics")
|
181 |
+
|
182 |
+
inputs = [alpha, mu, sigma, n, seed, x_min, x_max, bins, show_hist, overlay_empirical_pdf]
|
183 |
+
demo.load(render, inputs=inputs, outputs=[plot, stats])
|
184 |
+
for w in inputs:
|
185 |
+
w.change(render, inputs=inputs, outputs=[plot, stats])
|
186 |
+
|
187 |
+
if __name__ == "__main__":
|
188 |
+
demo.launch()
|
189 |
+
|