# (c) 2026 William J. Dwyer. Free and open-source software under the MIT License (see LICENSE).
# If you use or build upon this software, please retain this notice and cite the accompanying
# methodology paper together with the archived software (Zenodo DOI: 10.5281/zenodo.20709963).
"""
CV-Ratio Screen — interactive companion app
================================================================
Detecting non-normal (log-normal) distributions from summary statistics,
and recovering parametric inference.  Companion software for:

  Dwyer WJ. "The Coefficient of Variation as a Cutoff for Detecting
  Non-Normal Distributions from Summary Statistics."

Run with:
    pip install streamlit numpy scipy pandas matplotlib
    streamlit run cvscreen_app.py

Three tabs:
  1. Reproduce the findings  — interactive Monte-Carlo simulator.
  2. Analyze your data        — upload/enter two groups; get a recommended test.
  3. Summary-statistic screen — enter published mean & SD; flag likely log-normality.
================================================================
"""
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt
import streamlit as st

# Core statistics live in cvscreen_core.py (no GUI deps; importable & testable).
from cvscreen_core import (lognormal_params, draw_lognormal, permutation_t_p,
                           cv_ratio_screen, analyze_two_groups)

st.set_page_config(page_title="CV-Ratio Screen", layout="wide")

# =====================================================================
#  HEADER
# =====================================================================
st.title("CV-Ratio Screen")
st.caption("Detecting non-normal (log-normal) distributions from summary statistics, and recovering parametric inference.")

tab_sim, tab_data, tab_summary = st.tabs(
    ["① Reproduce the findings", "② Analyze your data", "③ Summary-statistic screen"]
)

# =====================================================================
#  TAB 1 — SIMULATION
# =====================================================================
with tab_sim:
    st.subheader("Monte-Carlo simulator")
    finding = st.selectbox(
        "Choose a finding to reproduce",
        ["Detectability: normality-test power vs sample size",
         "Inference cost: t-test power (raw vs log) vs Mann-Whitney",
         "The cutoff: CV where the log transform starts to matter"],
    )
    c1, c2, c3 = st.columns(3)
    reps = c1.slider("Monte-Carlo replicates", 500, 8000, 2000, step=500)
    seed = c2.number_input("Random seed", value=2026, step=1)
    st.divider()
    rng = np.random.default_rng(int(seed))
    ns = [10, 15, 20, 30, 50, 100, 200]

    if finding.startswith("Detectability"):
        cv = st.slider("Coefficient of variation (CV = SD/mean)", 0.2, 3.0, 1.0, step=0.1)
        if st.button("Run simulation", key="run1"):
            power = {t: [] for t in ["Shapiro-Wilk", "Anderson-Darling", "D'Agostino-Pearson", "Jarque-Bera"]}
            prog = st.progress(0.0)
            for i, n in enumerate(ns):
                X = draw_lognormal(1.0, cv, n, reps, rng)
                power["Shapiro-Wilk"].append(np.mean([stats.shapiro(r).pvalue < .05 for r in X]))
                power["Anderson-Darling"].append(np.mean([stats.anderson(r, "norm").statistic >= stats.anderson(r, "norm").critical_values[2] for r in X]))
                power["D'Agostino-Pearson"].append(np.mean(stats.normaltest(X, axis=1).pvalue < .05))
                S = stats.skew(X, axis=1); K = stats.kurtosis(X, axis=1)
                jb = X.shape[1] / 6.0 * (S ** 2 + (K ** 2) / 4.0)
                power["Jarque-Bera"].append(np.mean(stats.chi2.sf(jb, 2) < .05))
                prog.progress((i + 1) / len(ns))
            fig, ax = plt.subplots(figsize=(7, 4))
            for t, y in power.items():
                ax.plot(ns, y, "-o", ms=4, label=t)
            ax.axhline(0.8, ls="--", c="grey"); ax.set_xscale("log"); ax.set_xticks(ns); ax.set_xticklabels(ns)
            ax.set_xlabel("sample size per group, n"); ax.set_ylabel("power to reject normality")
            ax.set_title(f"Detecting log-normal departures (CV = {cv})"); ax.legend(fontsize=8); ax.grid(alpha=.3)
            st.pyplot(fig)
            st.info("All tests are underpowered at small n — the small-sample blind spot is shared, not a Shapiro-Wilk artifact.")

    elif finding.startswith("Inference cost"):
        cv = st.slider("Coefficient of variation", 0.3, 3.0, 1.5, step=0.1)
        ratio = st.slider("True median ratio (effect size)", 1.0, 3.0, 1.5, step=0.1)
        if st.button("Run simulation", key="run2"):
            shift = np.log(ratio)
            raw, log, mw = [], [], []
            prog = st.progress(0.0)
            for i, n in enumerate(ns):
                g1 = draw_lognormal(1.0, cv, n, reps, rng)
                g2 = draw_lognormal(1.0, cv, n, reps, rng, logshift=shift)
                raw.append(np.mean(stats.ttest_ind(g1, g2, axis=1, equal_var=False).pvalue < .05))
                log.append(np.mean(stats.ttest_ind(np.log(g1), np.log(g2), axis=1, equal_var=False).pvalue < .05))
                mw.append(np.mean([stats.mannwhitneyu(a, b, alternative="two-sided").pvalue < .05 for a, b in zip(g1, g2)]))
                prog.progress((i + 1) / len(ns))
            fig, ax = plt.subplots(figsize=(7, 4))
            ax.plot(ns, log, "-o", label="log-scale t-test", color="#1f4e79")
            ax.plot(ns, mw, "-s", label="Mann-Whitney", color="#dd8452")
            ax.plot(ns, raw, "-^", label="raw-scale t-test", color="#c44e52")
            ax.axhline(0.8, ls="--", c="grey"); ax.set_xscale("log"); ax.set_xticks(ns); ax.set_xticklabels(ns)
            ax.set_xlabel("sample size per group, n"); ax.set_ylabel("power"); ax.legend(); ax.grid(alpha=.3)
            ax.set_title(f"CV = {cv}, median ratio = {ratio}")
            st.pyplot(fig)
            st.info("The log-scale t-test recovers the power the raw t-test forfeits and exceeds the non-parametric test.")

    else:  # cutoff
        n = st.slider("Sample size per group", 10, 100, 30, step=5)
        if st.button("Run simulation", key="run3"):
            cvg = [0.2, 0.3, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0]
            sw_flag, log_gain = [], []
            prog = st.progress(0.0)
            for i, c in enumerate(cvg):
                go = draw_lognormal(1.0, c, n, reps, rng)
                gd = draw_lognormal(1.5, c, n, reps, rng)  # real 50% mean increase
                f = np.mean([(stats.shapiro(a).pvalue < .05) | (stats.shapiro(b).pvalue < .05) for a, b in zip(go, gd)])
                pr = np.mean(stats.ttest_ind(go, gd, axis=1, equal_var=False).pvalue < .05)
                pl = np.mean(stats.ttest_ind(np.log(go), np.log(gd), axis=1, equal_var=False).pvalue < .05)
                sw_flag.append(f); log_gain.append(pl - pr); prog.progress((i + 1) / len(cvg))
            fig, (a1, a2) = plt.subplots(1, 2, figsize=(11, 4))
            for ax in (a1, a2):
                ax.axvspan(0.5, 1.0, color="gold", alpha=.15); ax.set_xlabel("CV (each group)"); ax.grid(alpha=.3)
            a1.plot(cvg, sw_flag, "-o", color="#1f4e79"); a1.axhline(.8, ls="--", c="grey"); a1.set_ylabel("P(Shapiro-Wilk flags)"); a1.set_title("Detectability")
            a2.plot(cvg, log_gain, "-o", color="#c44e52"); a2.axhline(.05, ls="--", c="grey"); a2.set_ylabel("power gained by log transform"); a2.set_title("Cost of staying raw")
            fig.suptitle(f"The cutoff region CV ≈ 0.5–1.0 (n = {n})")
            st.pyplot(fig)
            st.info("Below CV≈0.3 data are effectively normal; by CV≈1.0 the raw t-test is clearly deficient.")

# =====================================================================
#  TAB 2 — ANALYZE YOUR DATA
# =====================================================================
with tab_data:
    st.subheader("Analyze two groups")
    st.write("Provide your two groups, then the app applies the CV-ratio screen, recommends a scale, and runs the competing tests.")
    mode = st.radio("Input method", ["Paste values", "Upload CSV (value,group)"], horizontal=True)
    x = y = None
    if mode == "Paste values":
        c1, c2 = st.columns(2)
        t1 = c1.text_area("Group 1 (control) — comma/space/newline separated", "0.4 0.5 0.6 0.3 1.2 0.5 0.7 0.4 0.6 0.5")
        t2 = c2.text_area("Group 2 (disease)", "1.1 2.3 0.9 4.5 1.4 1.0 6.2 1.3 2.0 1.5")
        def parse(t):
            return np.array([float(v) for v in t.replace(",", " ").split()]) if t.strip() else np.array([])
        x, y = parse(t1), parse(t2)
    else:
        up = st.file_uploader("CSV with a numeric 'value' column and a 'group' column (two levels)", type=["csv"])
        if up is not None:
            df = pd.read_csv(up)
            st.dataframe(df.head())
            vcol = st.selectbox("Value column", df.columns)
            gcol = st.selectbox("Group column", [c for c in df.columns if c != vcol])
            levels = list(pd.unique(df[gcol]))[:2]
            x = df[df[gcol] == levels[0]][vcol].to_numpy(float)
            y = df[df[gcol] == levels[1]][vcol].to_numpy(float)

    nperm = st.slider("Permutation replicates", 500, 10000, 3000, step=500)
    if x is not None and y is not None and len(x) >= 3 and len(y) >= 3 and st.button("Analyze", key="analyze"):
        R = analyze_two_groups(x, y, n_perm=nperm, seed=1)
        c1, c2 = st.columns(2)
        with c1:
            st.markdown("**Summary**")
            st.write(pd.DataFrame({
                "Group 1": [str(R["n1"]), f'{R["mean1"]:.3g}', f'{R["sd1"]:.3g}', f'{R["cv1"]:.3g}'],
                "Group 2": [str(R["n2"]), f'{R["mean2"]:.3g}', f'{R["sd2"]:.3g}', f'{R["cv2"]:.3g}'],
            }, index=["n", "mean", "SD", "CV"]))
            st.metric("CV ratio (eq 17)", f'{R["screen_cv_ratio"]:.2f}')
            st.write(f'**Screen verdict:** {R["screen_verdict"]}')
        with c2:
            st.markdown("**Test results (two-sided p-values)**")
            rows = {"raw-scale t (Welch)": R.get("p_raw_t"), "log-scale t (Welch)": R.get("p_log_t"),
                    "permutation t (log)": R.get("p_perm_log"), "Mann-Whitney U": R.get("p_mw")}
            st.write(pd.DataFrame({"p-value": {k: (f"{v:.4f}" if v is not None else "n/a") for k, v in rows.items()}}))
            sw = {"raw G1": R.get("sw_p_raw_1"), "raw G2": R.get("sw_p_raw_2"),
                  "log G1": R.get("sw_p_log_1"), "log G2": R.get("sw_p_log_2")}
            st.caption("Shapiro-Wilk p: " + ", ".join(f"{k}={v:.3f}" for k, v in sw.items() if v is not None and not np.isnan(v)))
        st.success("Recommended analysis: " + R["recommended"])
        # plots
        if R["positive"]:
            fig, axes = plt.subplots(1, 2, figsize=(11, 3.5))
            axes[0].hist(x, alpha=.6, label="G1"); axes[0].hist(y, alpha=.6, label="G2")
            axes[0].set_title("Raw scale"); axes[0].legend()
            axes[1].hist(np.log(x), alpha=.6, label="G1"); axes[1].hist(np.log(y), alpha=.6, label="G2")
            axes[1].set_title("Log scale"); axes[1].legend()
            st.pyplot(fig)
    elif x is not None and (len(x) < 3 or len(y) < 3):
        st.warning("Need at least 3 observations per group.")

# =====================================================================
#  TAB 3 — SUMMARY-STATISTIC SCREEN
# =====================================================================
with tab_summary:
    st.subheader("Screen published summary statistics")
    st.write("Enter the reported mean and SD for each group (e.g., from a published table). No raw data needed.")
    c1, c2 = st.columns(2)
    mu1 = c1.number_input("Group 1 mean", value=0.5, format="%.4f")
    sd1 = c1.number_input("Group 1 SD", value=0.5, format="%.4f")
    mu2 = c2.number_input("Group 2 mean", value=1.5, format="%.4f")
    sd2 = c2.number_input("Group 2 SD", value=3.4, format="%.4f")
    if mu1 > 0 and mu2 > 0:
        scr = cv_ratio_screen(mu1, sd1, mu2, sd2)
        me1, se1 = lognormal_params(mu1, sd1 / mu1)
        me2, se2 = lognormal_params(mu2, sd2 / mu2)
        a, b, c = st.columns(3)
        a.metric("CV — Group 1", f"{scr['cv1']:.2f}")
        b.metric("CV — Group 2", f"{scr['cv2']:.2f}")
        c.metric("CV ratio", f"{scr['cv_ratio']:.2f}")
        st.write(f"**Implied log-domain parameters** — G1: μₑ={me1:.3f}, σₑ={se1:.3f};  G2: μₑ={me2:.3f}, σₑ={se2:.3f}")
        (st.error if scr["suspect"] else st.success)("Screen verdict: " + scr["verdict"])
        st.caption("Rule of thumb: suspect log-normality when either CV approaches/exceeds 1, "
                   "or when the higher-variance group's CV exceeds the other's by more than ~1.5× while also having the larger mean.")
    else:
        st.warning("Means must be positive for the coefficient of variation to be meaningful.")

st.divider()
st.caption("Companion software for Dwyer WJ, \"The Coefficient of Variation as a Cutoff for Detecting "
           "Non-Normal Distributions from Summary Statistics.\"  Methods: CV-ratio screen (eq 17), "
           "log-scale and permutation t-tests, Mann-Whitney U.")
