Download app1.py from osaaso/ABC: direct link, hf CLI and curl.
- Browser
- Download file 32.6 kB
-
https://huggingface.co/spaces/osaaso/ABC/resolve/main/app1.py
- Command line
-
hf download hf://spaces/osaaso/ABC/app1.py
-
curl -L -o app1.py https://huggingface.co/spaces/osaaso/ABC/resolve/main/app1.py
32.6 kB
| from pathlib import Path | |
| import io | |
| import tempfile | |
| import traceback | |
| import zipfile | |
| import xml.etree.ElementTree as ET | |
| import gradio as gr | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import pandas as pd | |
| import pingouin as pg | |
| import seaborn as sns | |
| from bs4 import BeautifulSoup | |
| from PIL import Image | |
| BASE = Path(__file__).parent | |
| DATASETS = { | |
| "General teaching data": ("general_data.csv", "Descriptives, normality, correlations, t-tests, ANOVA, ANCOVA, and regression."), | |
| "Repeated-measures data": ("repeated_measures_data.csv", "Repeated-measures ANOVA and Friedman: score, time, participant."), | |
| "Reliability data": ("reliability_data.csv", "Cronbach alpha: select item1 through item8."), | |
| "Categorical data": ("categorical_data.csv", "Chi-square: treatment × outcome, adherence × outcome, or smoker × outcome."), | |
| "Nonparametric data": ("nonparametric_data.csv", "Mann–Whitney, Kruskal–Wallis, and Wilcoxon."), | |
| "Penguins (Pingouin)": (None, "A familiar dataset for descriptives, correlations, ANOVA, and regression."), | |
| } | |
| ANALYSES = [ | |
| "1. Descriptive statistics", | |
| "2. Normality and Q-Q plot", | |
| "3. Correlation", | |
| "4. Partial correlation", | |
| "5. T-test", | |
| "6. One-way ANOVA", | |
| "7. Repeated-measures ANOVA", | |
| "8. ANCOVA", | |
| "9. Nonparametric test", | |
| "10. Linear regression", | |
| "11. Cronbach alpha", | |
| "12. Chi-square test", | |
| "13. Exam generator", | |
| "14. Statsmodels regression models", | |
| ] | |
| EDITOR_CSS = """ | |
| #analysis-code .cm-editor { | |
| background: #292d30; | |
| color: #dddddd; | |
| font-family: "JetBrains Mono", "Cascadia Code", "DejaVu Sans Mono", monospace; | |
| font-size: 14px; | |
| line-height: 1.65; | |
| } | |
| #analysis-code .cm-gutters { | |
| background: #292d30; | |
| color: #a0a0a0; | |
| border-right: 1px solid #454a4e; | |
| } | |
| #analysis-code .cm-activeLine, | |
| #analysis-code .cm-activeLineGutter { | |
| background: #343a3f; | |
| } | |
| #analysis-code .cm-cursor { | |
| border-left-color: #fda331; | |
| } | |
| #analysis-code .cm-selectionBackground, | |
| #analysis-code .cm-content ::selection { | |
| background: #46596b !important; | |
| } | |
| """ | |
| DARK_MODE_HEAD = """<script> | |
| (() => { | |
| const url = new URL(window.location.href); | |
| if (!url.searchParams.has("__theme")) { | |
| url.searchParams.set("__theme", "dark"); | |
| window.history.replaceState(null, "", url); | |
| } | |
| })(); | |
| </script>""" | |
| def load_data(dataset_name, uploaded): | |
| if uploaded: | |
| path = Path(uploaded) | |
| suffix = path.suffix.lower() | |
| if suffix == ".csv": | |
| df = pd.read_csv(path) | |
| elif suffix in {".xlsx", ".xls"}: | |
| df = pd.read_excel(path) | |
| elif suffix == ".sav": | |
| df = pd.read_spss(path) | |
| else: | |
| raise ValueError("Upload a CSV, Excel, or SPSS .sav file.") | |
| note = f"Uploaded file: {path.name}" | |
| else: | |
| filename, note = DATASETS[dataset_name] | |
| df = pg.read_dataset("penguins") if filename is None else pd.read_csv(BASE / filename) | |
| df.columns = [str(c).strip() for c in df.columns] | |
| return df, note | |
| def defaults_for(dataset_name, all_cols, num_cols): | |
| def choose(name, pool, fallback=0): | |
| if name in pool: | |
| return name | |
| return pool[min(fallback, len(pool) - 1)] if pool else None | |
| x = choose("score", num_cols) | |
| y = choose("age", num_cols, 1) | |
| group = choose("group", all_cols) | |
| subject = choose("participant", all_cols) | |
| within = choose("time", all_cols) | |
| dv = choose("score", num_cols) | |
| covars = [c for c in ["age", "stress"] if c in num_cols] | |
| items = [c for c in num_cols if c.lower().startswith("item")] | |
| if dataset_name == "General teaching data": | |
| x = choose("pre_score", num_cols) | |
| y = choose("post_score", num_cols, 1) | |
| dv = choose("outcome", num_cols) | |
| elif dataset_name == "Reliability data": | |
| x = choose("item1", num_cols) | |
| y = choose("item2", num_cols, 1) | |
| elif dataset_name == "Categorical data": | |
| group = choose("treatment", all_cols) | |
| within = choose("outcome", all_cols, 1) | |
| elif dataset_name == "Nonparametric data": | |
| x = choose("before", num_cols) | |
| y = choose("after", num_cols, 1) | |
| dv = choose("skewed_score", num_cols) | |
| return x, y, group, subject, within, dv, covars, items | |
| def refresh(dataset_name, uploaded): | |
| try: | |
| df, note = load_data(dataset_name, uploaded) | |
| all_cols = list(df.columns) | |
| num_cols = list(df.select_dtypes(include=np.number).columns) | |
| x, y, group, subject, within, dv, covars, items = defaults_for(dataset_name, all_cols, num_cols) | |
| return ( | |
| note, | |
| df, | |
| gr.update(choices=num_cols, value=x), | |
| gr.update(choices=num_cols, value=y), | |
| gr.update(choices=all_cols, value=group), | |
| gr.update(choices=all_cols, value=subject), | |
| gr.update(choices=all_cols, value=within), | |
| gr.update(choices=num_cols, value=dv), | |
| gr.update(choices=num_cols, value=[]), | |
| gr.update(choices=num_cols, value=covars), | |
| gr.update(choices=num_cols, value=items), | |
| gr.update(choices=all_cols, value=dv), | |
| gr.update(choices=all_cols, value=[]), | |
| gr.update( | |
| choices=[str(value) for value in df[dv].dropna().unique()], | |
| value=[str(value) for value in df[dv].dropna().unique()], | |
| ), | |
| gr.update(choices=all_cols, value=[column for column in all_cols if column != dv][:3]), | |
| ) | |
| except Exception: | |
| blank = gr.update(choices=[], value=None) | |
| return traceback.format_exc(), pd.DataFrame(), blank, blank, blank, blank, blank, blank, blank, blank, blank, blank, blank, gr.update(choices=[], value=[]), gr.update(choices=[], value=[]) | |
| def refresh_outcome_levels(dataset_name, uploaded, outcome): | |
| try: | |
| df, _ = load_data(dataset_name, uploaded) | |
| levels = [str(value) for value in df[outcome].dropna().unique()] if outcome in df else [] | |
| return gr.update(choices=levels, value=levels) | |
| except Exception: | |
| return gr.update(choices=[], value=[]) | |
| def update_analysis_controls(analysis, t_design, nonparametric_test, model_family): | |
| visible = { | |
| name: False | |
| for name in ( | |
| "x", "y", "dv", "group", "within", "subject", "selected_vars", | |
| "covars", "items", "categorical_predictors", "method", "t_design", | |
| "test_value", "anova_type", "posthoc_type", "nonparametric_test", | |
| "model_family", "glm_family", "model_outcome", "outcome_order", | |
| "model_predictors", | |
| ) | |
| } | |
| def show(*names): | |
| for name in names: | |
| visible[name] = True | |
| analysis = analysis or "" | |
| if analysis.startswith("1."): | |
| show("selected_vars") | |
| elif analysis.startswith("2."): | |
| show("x") | |
| elif analysis.startswith("3."): | |
| show("x", "y", "method") | |
| elif analysis.startswith("4."): | |
| show("x", "y", "covars") | |
| elif analysis.startswith("5."): | |
| show("t_design") | |
| if t_design == "One sample": | |
| show("x", "test_value") | |
| elif t_design == "Paired": | |
| show("x", "y") | |
| else: | |
| show("dv", "group") | |
| elif analysis.startswith("6."): | |
| show("dv", "group", "anova_type", "posthoc_type") | |
| elif analysis.startswith("7."): | |
| show("dv", "within", "subject") | |
| elif analysis.startswith("8."): | |
| show("dv", "group", "covars") | |
| elif analysis.startswith("9."): | |
| show("nonparametric_test") | |
| if nonparametric_test == "Mann-Whitney": | |
| show("dv", "group") | |
| elif nonparametric_test == "Wilcoxon": | |
| show("x", "y") | |
| elif nonparametric_test == "Kruskal-Wallis": | |
| show("dv", "group") | |
| elif nonparametric_test == "Friedman": | |
| show("dv", "within", "subject") | |
| elif analysis.startswith("10."): | |
| show("dv", "selected_vars") | |
| elif analysis.startswith("11."): | |
| show("items") | |
| elif analysis.startswith("12."): | |
| show("group", "within") | |
| elif analysis.startswith("14."): | |
| show("model_family", "model_outcome", "model_predictors", "categorical_predictors") | |
| if model_family == "GLM": | |
| show("glm_family") | |
| if model_family == "Ordinal logit": | |
| show("outcome_order") | |
| return [gr.update(visible=visible[name]) for name in visible] | |
| def build_code(analysis, x, y, group, subject, within, dv, selected_vars, covars, items, | |
| method, t_design, test_value, anova_type, posthoc_type, nonparametric_test, | |
| model_family, glm_family, model_outcome, outcome_order, categorical_predictors, | |
| model_predictors): | |
| selected_vars = selected_vars or [] | |
| covars = covars or [] | |
| items = items or [] | |
| if analysis.startswith("1."): | |
| vars_ = selected_vars or [c for c in [x, y] if c] | |
| return "\n".join([ | |
| f"x = df[{vars_!r}]", | |
| "result = pd.DataFrame({", | |
| ' "N": x.count(),', | |
| ' "Missing": x.isna().sum(),', | |
| ' "Mean": x.mean(),', | |
| ' "SD": x.std(),', | |
| ' "Median": x.median(),', | |
| ' "Minimum": x.min(),', | |
| ' "Maximum": x.max(),', | |
| ' "Skewness": x.skew(),', | |
| ' "Kurtosis": x.kurt()', | |
| "})", | |
| ]) | |
| if analysis.startswith("2."): | |
| return "\n".join([ | |
| f"result = pg.normality(df[{x!r}].dropna())", | |
| f"sns.histplot(df[{x!r}].dropna(), kde=True)", | |
| f'plt.title("Distribution of {x}")', | |
| "plt.show()", | |
| f"pg.qqplot(df[{x!r}].dropna())", | |
| f'plt.title("Q-Q plot of {x}")', | |
| "plt.show()", | |
| ]) | |
| if analysis.startswith("3."): | |
| return "\n".join([ | |
| f"result = pg.corr(x=df[{x!r}], y=df[{y!r}], method={method!r})", | |
| f"sns.regplot(data=df, x={x!r}, y={y!r})", | |
| "plt.show()", | |
| ]) | |
| if analysis.startswith("4."): | |
| return f"result = pg.partial_corr(data=df, x={x!r}, y={y!r}, covar={covars!r})" | |
| if analysis.startswith("5."): | |
| if t_design == "One sample": | |
| return f"result = pg.ttest(df[{x!r}].dropna(), {float(test_value)})" | |
| if t_design == "Paired": | |
| return f"d = df[[{x!r}, {y!r}]].dropna()\nresult = pg.ttest(d[{x!r}], d[{y!r}], paired=True)" | |
| return "\n".join([ | |
| f"levels = list(df[{group!r}].dropna().unique())", | |
| 'if len(levels) != 2: raise ValueError("Independent t-test requires exactly two groups.")', | |
| f"a = df.loc[df[{group!r}] == levels[0], {dv!r}].dropna()", | |
| f"b = df.loc[df[{group!r}] == levels[1], {dv!r}].dropna()", | |
| "result = pg.ttest(a, b)", | |
| ]) | |
| if analysis.startswith("6."): | |
| code = (f"result = pg.anova(data=df, dv={dv!r}, between={group!r}, detailed=True)" | |
| if anova_type == "Classical" | |
| else f"result = pg.welch_anova(data=df, dv={dv!r}, between={group!r})") | |
| if posthoc_type == "Tukey": | |
| code += f"\nposthoc = pg.pairwise_tukey(data=df, dv={dv!r}, between={group!r})" | |
| elif posthoc_type == "Games-Howell": | |
| code += f"\nposthoc = pg.pairwise_gameshowell(data=df, dv={dv!r}, between={group!r})" | |
| return code | |
| if analysis.startswith("7."): | |
| return "\n".join([ | |
| f"d = df[[{subject!r}, {within!r}, {dv!r}]].dropna().copy()", | |
| f"counts = d.groupby({subject!r})[{within!r}].nunique()", | |
| "valid = counts[counts >= 2].index", | |
| f"d = d[d[{subject!r}].isin(valid)]", | |
| f'if d.empty or d[{within!r}].nunique() < 2: raise ValueError("No valid repeated observations.")', | |
| f"result = pg.rm_anova(data=d, dv={dv!r}, within={within!r}, subject={subject!r}, detailed=True)", | |
| f"posthoc = pg.pairwise_tests(data=d, dv={dv!r}, within={within!r}, subject={subject!r}, padjust='holm')", | |
| ]) | |
| if analysis.startswith("8."): | |
| return f"result = pg.ancova(data=df, dv={dv!r}, between={group!r}, covar={covars!r})" | |
| if analysis.startswith("9."): | |
| if nonparametric_test == "Mann-Whitney": | |
| return "\n".join([ | |
| f"levels = list(df[{group!r}].dropna().unique())", | |
| 'if len(levels) < 2: raise ValueError("At least two groups are required.")', | |
| f"a = df.loc[df[{group!r}] == levels[0], {dv!r}].dropna()", | |
| f"b = df.loc[df[{group!r}] == levels[1], {dv!r}].dropna()", | |
| "result = pg.mwu(a, b)", | |
| ]) | |
| if nonparametric_test == "Wilcoxon": | |
| return f"d = df[[{x!r}, {y!r}]].dropna()\nresult = pg.wilcoxon(d[{x!r}], d[{y!r}])" | |
| if nonparametric_test == "Kruskal-Wallis": | |
| return f"result = pg.kruskal(data=df, dv={dv!r}, between={group!r})" | |
| return f"d = df[[{subject!r}, {within!r}, {dv!r}]].dropna()\nresult = pg.friedman(data=d, dv={dv!r}, within={within!r}, subject={subject!r})" | |
| if analysis.startswith("10."): | |
| predictors = selected_vars or [x] | |
| return f"result = pg.linear_regression(X=df[{predictors!r}], y=df[{dv!r}], remove_na=True)" | |
| if analysis.startswith("11."): | |
| return "\n".join([ | |
| f"alpha, ci = pg.cronbach_alpha(data=df[{items!r}])", | |
| 'result = pd.DataFrame({"Cronbach alpha": [alpha], "CI lower": [ci[0]], "CI upper": [ci[1]]})', | |
| ]) | |
| if analysis.startswith("14."): | |
| outcome = model_outcome or dv | |
| predictors = model_predictors or [c for c in [x, y] if c and c != outcome] | |
| categorical_predictors = categorical_predictors or [] | |
| terms = [ | |
| f"C(Q({column!r}))" if column in categorical_predictors else f"Q({column!r})" | |
| for column in predictors | |
| ] | |
| rhs = " + ".join(terms) or "1" | |
| selected = [outcome, *predictors] | |
| if model_family == "OLS (ANOVA / ANCOVA)": | |
| return f'''import statsmodels.formula.api as smf | |
| from statsmodels.stats.anova import anova_lm | |
| d = df[{selected!r}].dropna().copy() | |
| model = smf.ols(formula={f"Q({outcome!r}) ~ {rhs}"!r}, data=d).fit() | |
| result = model.summary2().tables[1] | |
| overall = pd.DataFrame({{"F": [model.fvalue], "df_model": [model.df_model], "p-value": [model.f_pvalue]}}) | |
| anova = anova_lm(model, typ=2)''' | |
| if model_family in {"Logit (binary)", "Probit (binary)"}: | |
| fit = "logit" if model_family.startswith("Logit") else "probit" | |
| return f'''import statsmodels.formula.api as smf | |
| from scipy.stats import chi2 | |
| d = df[{selected!r}].dropna().copy() | |
| d["__outcome"] = pd.Categorical(d[{outcome!r}]).codes | |
| if d["__outcome"].nunique() != 2: | |
| raise ValueError("The selected outcome must have exactly two levels.") | |
| model = smf.{fit}(formula={f"__outcome ~ {rhs}"!r}, data=d).fit(disp=False) | |
| result = pd.DataFrame({{"coef": model.params, "SE": model.bse, "z": model.tvalues, "p-value": model.pvalues}}) | |
| overall = pd.DataFrame({{"LR chi2": [model.llr], "df": [model.df_model], "p-value": [model.llr_pvalue]}})''' | |
| if model_family == "Ordinal logit": | |
| levels = outcome_order or [] | |
| return f'''import numpy as np | |
| import patsy | |
| from scipy.stats import chi2 | |
| from statsmodels.miscmodels.ordinal_model import OrderedModel | |
| d = df[{selected!r}].dropna().copy() | |
| d["__outcome"] = pd.Categorical(d[{outcome!r}].astype(str), categories={levels!r}, ordered=True).codes | |
| if (d["__outcome"] < 0).any() or d["__outcome"].nunique() < 2: | |
| raise ValueError("Select all outcome levels in their intended order.") | |
| X = patsy.dmatrix({rhs!r}, d, return_type="dataframe").drop(columns="Intercept", errors="ignore") | |
| if X.empty: | |
| raise ValueError("Select at least one predictor for ordinal logit.") | |
| model = OrderedModel(d["__outcome"], X, distr="logit").fit(method="bfgs", disp=False) | |
| result = pd.DataFrame({{"coef": model.params.loc[X.columns], "SE": model.bse.loc[X.columns], "z": model.tvalues.loc[X.columns], "p-value": model.pvalues.loc[X.columns]}}) | |
| null_model = OrderedModel(d["__outcome"], np.empty((len(d), 0)), distr="logit").fit(method="bfgs", disp=False) | |
| lr_chi2 = 2 * (model.llf - null_model.llf) | |
| overall = pd.DataFrame({{"LR chi2": [lr_chi2], "df": [X.shape[1]], "p-value": [chi2.sf(lr_chi2, X.shape[1])]}})''' | |
| family = { | |
| "Poisson": "sm.families.Poisson()", | |
| "Negative binomial": "sm.families.NegativeBinomial()", | |
| "Gaussian": "sm.families.Gaussian()", | |
| "Binomial": "sm.families.Binomial()", | |
| "Gamma": "sm.families.Gamma()", | |
| }.get(glm_family or "Gaussian", "sm.families.Gaussian()") | |
| response = f'd["__outcome"] = pd.Categorical(d[{outcome!r}]).codes\n' if glm_family == "Binomial" else "" | |
| glm_outcome = "__outcome" if glm_family == "Binomial" else f"Q({outcome!r})" | |
| return f'''import statsmodels.api as sm | |
| import statsmodels.formula.api as smf | |
| from scipy.stats import chi2 | |
| d = df[{selected!r}].dropna().copy() | |
| {response}family = {family} | |
| model = smf.glm(formula={f"{glm_outcome} ~ {rhs}"!r}, data=d, family=family).fit() | |
| result = pd.DataFrame({{"coef": model.params, "SE": model.bse, "z": model.tvalues, "p-value": model.pvalues}}) | |
| null_model = smf.glm(formula={f"{glm_outcome} ~ 1"!r}, data=d, family=family).fit() | |
| lr_chi2 = max(0, 2 * (model.llf - null_model.llf)) | |
| lr_df = model.df_model - null_model.df_model | |
| overall = pd.DataFrame({{"LR chi2": [lr_chi2], "df": [lr_df], "p-value": [chi2.sf(lr_chi2, lr_df)]}})''' | |
| if analysis.startswith("13."): | |
| return "# Generate exam versions in the Exam generator tab." | |
| return f"expected, observed, result = pg.chi2_independence(data=df, x={group!r}, y={within!r})" | |
| def _as_frame(value): | |
| if isinstance(value, pd.Series): | |
| frame = value.to_frame(name=value.name or "Value") | |
| if isinstance(value.index, pd.MultiIndex): | |
| return frame.reset_index() | |
| return frame.rename_axis(value.index.name or "Statistic").reset_index() | |
| if isinstance(value, pd.DataFrame): | |
| if ( | |
| isinstance(value.index, pd.RangeIndex) | |
| and value.index.name is None | |
| and value.index.equals(pd.RangeIndex(len(value))) | |
| ): | |
| return value | |
| if isinstance(value.index, pd.MultiIndex): | |
| return value.reset_index() | |
| index_name = value.index.name or ( | |
| "Statistic" if all(isinstance(label, str) for label in value.index) else "Index" | |
| ) | |
| return value.rename_axis(index_name).reset_index() | |
| if pd.api.types.is_scalar(value): | |
| return pd.DataFrame({"value": [value]}) | |
| try: | |
| return pd.DataFrame(value) | |
| except (TypeError, ValueError): | |
| return pd.DataFrame({"value": [repr(value)]}) | |
| def run_code(dataset_name, uploaded, edited_data, code): | |
| try: | |
| df, _ = load_data(dataset_name, uploaded) | |
| if isinstance(edited_data, pd.DataFrame) and not edited_data.empty: | |
| df = edited_data.copy() | |
| env = {"df": df.copy(), "pd": pd, "np": np, "pg": pg, "sns": sns, "plt": plt} | |
| plt.close("all") | |
| exec(code, env) | |
| outputs = [] | |
| named_outputs = [(name, env[name]) for name in ["result", "posthoc", "observed", "expected", "overall", "anova"] if name in env] | |
| for name in ["result", "posthoc", "observed", "expected", "overall", "anova"]: | |
| outputs.append(_as_frame(env[name]) if name in env else pd.DataFrame()) | |
| figures = [] | |
| plot_pngs = [] | |
| for number in plt.get_fignums(): | |
| image_buffer = io.BytesIO() | |
| plt.figure(number).savefig(image_buffer, format="png", bbox_inches="tight") | |
| png_data = image_buffer.getvalue() | |
| plot_pngs.append(png_data) | |
| figures.append(Image.open(io.BytesIO(png_data)).convert("RGB")) | |
| with tempfile.NamedTemporaryFile(prefix="statistics-results-", suffix=".zip", delete=False) as archive_file: | |
| archive_path = archive_file.name | |
| with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive: | |
| archive.writestr("data.csv", df.to_csv(index=False)) | |
| archive.writestr("analysis.py", code or "") | |
| for name, value in named_outputs: | |
| archive.writestr(f"{name}.csv", _as_frame(value).to_csv(index=False)) | |
| for index, png_data in enumerate(plot_pngs, start=1): | |
| archive.writestr(f"plot_{index}.png", png_data) | |
| return (*outputs, figures, "Analysis completed.", archive_path) | |
| except Exception: | |
| return *(pd.DataFrame() for _ in range(6)), [], traceback.format_exc(), None | |
| def load_python_file(uploaded_code): | |
| if not uploaded_code: | |
| return gr.update() | |
| try: | |
| return Path(uploaded_code).read_text(encoding="utf-8-sig") | |
| except UnicodeDecodeError: | |
| raise gr.Error("The selected Python file must use UTF-8 encoding.") | |
| def save_python_file(code): | |
| with tempfile.NamedTemporaryFile(mode="w", suffix=".py", prefix="analysis-", encoding="utf-8", delete=False) as output: | |
| output.write(code or "") | |
| return output.name | |
| def save_dataframe_csv(data): | |
| if not isinstance(data, pd.DataFrame): | |
| data = pd.DataFrame(data) | |
| with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", prefix="data-", encoding="utf-8", delete=False) as output: | |
| data.to_csv(output, index=False) | |
| return output.name | |
| def generate_exam_package(uploaded_qti, num_tests, compact): | |
| if not uploaded_qti: | |
| raise gr.Error("Upload a QTI XML file first.") | |
| try: | |
| root = ET.parse(uploaded_qti).getroot() | |
| questions = [] | |
| for item in root.iter(): | |
| if not item.tag.endswith("item"): | |
| continue | |
| prompt = item.find(".//{*}presentation//{*}mattext") | |
| question_text = BeautifulSoup(prompt.text or "", "html.parser").get_text(" ", strip=True) if prompt is not None else "" | |
| options = [] | |
| id_to_text = {} | |
| for label in item.findall(".//{*}response_label"): | |
| option = label.find(".//{*}mattext") | |
| text = BeautifulSoup(option.text or "", "html.parser").get_text(" ", strip=True) if option is not None else "" | |
| options.append(text) | |
| id_to_text[label.attrib.get("ident", "")] = text | |
| correct = "" | |
| for condition in item.findall(".//{*}respcondition"): | |
| score = condition.find(".//{*}setvar") | |
| answer = condition.find(".//{*}varequal") | |
| if score is not None and float((score.text or "0").strip()) == 100 and answer is not None: | |
| correct = id_to_text.get((answer.text or "").strip(), "") | |
| break | |
| if question_text: | |
| questions.append({"question": question_text, "options": options, "correct": correct}) | |
| if not questions: | |
| raise ValueError("No questions were found in the QTI file.") | |
| with tempfile.NamedTemporaryFile(suffix=".zip", prefix="exam-versions-", delete=False) as output: | |
| archive_path = output.name | |
| with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive: | |
| for test_num in range(1, int(num_tests) + 1): | |
| rng = np.random.default_rng(test_num) | |
| shuffled = questions.copy() | |
| rng.shuffle(shuffled) | |
| test_lines, answer_key = [], [] | |
| for question_num, question in enumerate(shuffled, start=1): | |
| options = question["options"].copy() | |
| rng.shuffle(options) | |
| option_lines = [f"{chr(65 + index)}) {option}" for index, option in enumerate(options)] | |
| correct_letter = next((chr(65 + index) for index, option in enumerate(options) if option == question["correct"]), "") | |
| if compact: | |
| test_lines.append(f"{question_num}. {question['question']} " + " ".join(option_lines)) | |
| else: | |
| test_lines.append(f"{question_num}. {question['question']}\n" + "\n".join(option_lines)) | |
| answer_key.append({"Question": question_num, "Answer": correct_letter, "Correct_Text": question["correct"]}) | |
| archive.writestr(f"test{test_num}.txt", "\n\n".join(test_lines)) | |
| archive.writestr(f"key{test_num}.csv", pd.DataFrame(answer_key).to_csv(index=False)) | |
| return archive_path, f"Generated {int(num_tests)} versions from {len(questions)} questions." | |
| except Exception as error: | |
| raise gr.Error(f"Could not generate exams: {error}") | |
| with gr.Blocks(title="Student Statistics Lab — Gradio") as demo: | |
| gr.Markdown("# Student Statistics Lab\nStatistical analyses, editable Python, and teaching-data tools.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| dataset = gr.Dropdown(list(DATASETS), value="General teaching data", label="Built-in teaching dataset") | |
| upload = gr.File(label="Or upload CSV, Excel, or SAV", file_types=[".csv", ".xlsx", ".xls", ".sav"], type="filepath") | |
| dataset_note = gr.Markdown() | |
| analysis = gr.Dropdown(ANALYSES, value=ANALYSES[0], label="Analysis") | |
| with gr.Column(scale=2): | |
| preview = gr.Dataframe(label="Editable data preview", interactive=True, max_height=420) | |
| download_dataframe = gr.DownloadButton("Download dataframe (.csv)") | |
| download_dataframe.click(save_dataframe_csv, [preview], [download_dataframe]) | |
| with gr.Accordion("Variables and options", open=True): | |
| with gr.Row(): | |
| x = gr.Dropdown(label="Variable X / Variable 1") | |
| y = gr.Dropdown(label="Variable Y / Variable 2") | |
| dv = gr.Dropdown(label="Dependent/outcome variable") | |
| with gr.Row(): | |
| group = gr.Dropdown(label="Grouping/factor/row variable") | |
| within = gr.Dropdown(label="Within factor/column variable") | |
| subject = gr.Dropdown(label="Subject ID") | |
| with gr.Row(): | |
| selected_vars = gr.Dropdown(multiselect=True, label="Variables / predictors") | |
| covars = gr.Dropdown(multiselect=True, label="Covariates") | |
| items = gr.Dropdown(multiselect=True, label="Scale items") | |
| categorical_predictors = gr.Dropdown(multiselect=True, label="Categorical predictors") | |
| with gr.Row(): | |
| method = gr.Dropdown(["pearson", "spearman", "kendall", "bicor"], value="pearson", label="Correlation method") | |
| t_design = gr.Dropdown(["One sample", "Paired", "Independent"], value="Independent", label="T-test design") | |
| test_value = gr.Number(value=0, label="One-sample test value") | |
| with gr.Row(): | |
| anova_type = gr.Dropdown(["Classical", "Welch"], value="Classical", label="ANOVA type") | |
| posthoc_type = gr.Dropdown(["None", "Tukey", "Games-Howell"], value="None", label="Post hoc") | |
| nonparametric_test = gr.Dropdown(["Mann-Whitney", "Wilcoxon", "Kruskal-Wallis", "Friedman"], value="Mann-Whitney", label="Nonparametric test") | |
| with gr.Row(): | |
| model_family = gr.Dropdown( | |
| ["OLS (ANOVA / ANCOVA)", "Logit (binary)", "Probit (binary)", "Ordinal logit", "GLM"], | |
| value="OLS (ANOVA / ANCOVA)", label="Statsmodels family", | |
| ) | |
| glm_family = gr.Dropdown( | |
| ["Poisson", "Negative binomial", "Gaussian", "Binomial", "Gamma"], | |
| value="Gaussian", label="GLM distribution", | |
| ) | |
| model_outcome = gr.Dropdown(label="Statsmodels outcome") | |
| outcome_order = gr.Dropdown(multiselect=True, label="Ordinal outcome order") | |
| model_predictors = gr.Dropdown(multiselect=True, label="Statsmodels predictors") | |
| generate = gr.Button("Generate code") | |
| code = gr.Code( | |
| label="Editable Python code", | |
| language="python", | |
| interactive=True, | |
| lines=22, | |
| max_lines=36, | |
| wrap_lines=True, | |
| autocomplete=True, | |
| buttons=["copy"], | |
| elem_id="analysis-code", | |
| ) | |
| with gr.Row(): | |
| code_upload = gr.File(label="Import Python code", file_types=[".py"], type="filepath") | |
| load_code = gr.Button("Load into editor") | |
| download_code = gr.DownloadButton("Download .py") | |
| load_code.click(load_python_file, [code_upload], [code]) | |
| download_code.click(save_python_file, [code], [download_code]) | |
| run = gr.Button("Run analysis", variant="primary") | |
| status = gr.Textbox(label="Status / error", lines=8) | |
| with gr.Tabs(): | |
| with gr.Tab("Result"): | |
| result = gr.Dataframe(interactive=False) | |
| with gr.Tab("Post hoc"): | |
| posthoc = gr.Dataframe(interactive=False) | |
| with gr.Tab("Observed"): | |
| observed = gr.Dataframe(interactive=False) | |
| with gr.Tab("Expected"): | |
| expected = gr.Dataframe(interactive=False) | |
| with gr.Tab("Overall model"): | |
| overall = gr.Dataframe(interactive=False) | |
| with gr.Tab("ANOVA table"): | |
| anova = gr.Dataframe(interactive=False) | |
| with gr.Tab("Plots"): | |
| plots = gr.Gallery(label="Plots", columns=2, object_fit="contain") | |
| with gr.Tab("Exam generator"): | |
| qti_upload = gr.File(label="Upload QTI XML", file_types=[".xml"], type="filepath") | |
| exam_count = gr.Number(label="Number of test versions", value=4, minimum=1, maximum=100, step=1) | |
| compact_exam = gr.Checkbox(label="Compact one-line format", value=True) | |
| generate_exams = gr.Button("Generate exam package", variant="primary") | |
| exam_status = gr.Textbox(label="Exam generation status") | |
| exam_download = gr.DownloadButton("Download ZIP package") | |
| generate_exams.click( | |
| generate_exam_package, | |
| [qti_upload, exam_count, compact_exam], | |
| [exam_download, exam_status], | |
| ) | |
| with gr.Tab("Downloads"): | |
| results_download = gr.DownloadButton("Download data, code, results, and plots (.zip)") | |
| analysis_controls = { | |
| "x": x, | |
| "y": y, | |
| "dv": dv, | |
| "group": group, | |
| "within": within, | |
| "subject": subject, | |
| "selected_vars": selected_vars, | |
| "covars": covars, | |
| "items": items, | |
| "categorical_predictors": categorical_predictors, | |
| "method": method, | |
| "t_design": t_design, | |
| "test_value": test_value, | |
| "anova_type": anova_type, | |
| "posthoc_type": posthoc_type, | |
| "nonparametric_test": nonparametric_test, | |
| "model_family": model_family, | |
| "glm_family": glm_family, | |
| "model_outcome": model_outcome, | |
| "outcome_order": outcome_order, | |
| "model_predictors": model_predictors, | |
| } | |
| visibility_inputs = [analysis, t_design, nonparametric_test, model_family] | |
| visibility_outputs = list(analysis_controls.values()) | |
| demo.load(update_analysis_controls, visibility_inputs, visibility_outputs) | |
| analysis.change(update_analysis_controls, visibility_inputs, visibility_outputs) | |
| t_design.change(update_analysis_controls, visibility_inputs, visibility_outputs) | |
| nonparametric_test.change(update_analysis_controls, visibility_inputs, visibility_outputs) | |
| model_family.change(update_analysis_controls, visibility_inputs, visibility_outputs) | |
| refresh_inputs = [dataset, upload] | |
| refresh_outputs = [dataset_note, preview, x, y, group, subject, within, dv, | |
| selected_vars, covars, items, model_outcome, categorical_predictors, | |
| outcome_order, model_predictors] | |
| demo.load(refresh, refresh_inputs, refresh_outputs) | |
| dataset.change(refresh, refresh_inputs, refresh_outputs) | |
| upload.change(refresh, refresh_inputs, refresh_outputs) | |
| model_outcome.change(refresh_outcome_levels, [dataset, upload, model_outcome], [outcome_order]) | |
| code_inputs = [analysis, x, y, group, subject, within, dv, selected_vars, covars, items, | |
| method, t_design, test_value, anova_type, posthoc_type, nonparametric_test, | |
| model_family, glm_family, model_outcome, outcome_order, categorical_predictors, | |
| model_predictors] | |
| generate.click(build_code, code_inputs, code) | |
| run.click( | |
| run_code, | |
| [dataset, upload, preview, code], | |
| [result, posthoc, observed, expected, overall, anova, plots, status, results_download], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(css=EDITOR_CSS, head=DARK_MODE_HEAD) |