CognitiveScience commited on
Commit
086fb86
·
verified ·
1 Parent(s): bf4f8d3

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +726 -0
app.py ADDED
@@ -0,0 +1,726 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
4
+
5
+ from pathlib import Path
6
+ import io
7
+ import tempfile
8
+ import traceback
9
+ import zipfile
10
+ import xml.etree.ElementTree as ET
11
+
12
+ import gradio as gr
13
+ import matplotlib.pyplot as plt
14
+ import numpy as np
15
+ import pandas as pd
16
+ import pingouin as pg
17
+ import seaborn as sns
18
+ from bs4 import BeautifulSoup
19
+ from PIL import Image
20
+
21
+ BASE = Path(__file__).parent
22
+
23
+ DATASETS = {
24
+ "General teaching data": ("general_data.csv", "Descriptives, normality, correlations, t-tests, ANOVA, ANCOVA, and regression."),
25
+ "Repeated-measures data": ("repeated_measures_data.csv", "Repeated-measures ANOVA and Friedman: score, time, participant."),
26
+ "Reliability data": ("reliability_data.csv", "Cronbach alpha: select item1 through item8."),
27
+ "Categorical data": ("categorical_data.csv", "Chi-square: treatment × outcome, adherence × outcome, or smoker × outcome."),
28
+ "Nonparametric data": ("nonparametric_data.csv", "Mann–Whitney, Kruskal–Wallis, and Wilcoxon."),
29
+ "Penguins (Pingouin)": (None, "A familiar dataset for descriptives, correlations, ANOVA, and regression."),
30
+ }
31
+
32
+ ANALYSES = [
33
+ "1. Descriptive statistics",
34
+ "2. Normality and Q-Q plot",
35
+ "3. Correlation",
36
+ "4. Partial correlation",
37
+ "5. T-test",
38
+ "6. One-way ANOVA",
39
+ "7. Repeated-measures ANOVA",
40
+ "8. ANCOVA",
41
+ "9. Nonparametric test",
42
+ "10. Linear regression",
43
+ "11. Cronbach alpha",
44
+ "12. Chi-square test",
45
+ "13. Exam generator",
46
+ "14. Statsmodels regression models",
47
+ ]
48
+
49
+ EDITOR_CSS = """
50
+ #analysis-code .cm-editor {
51
+ background: #292d30;
52
+ color: #dddddd;
53
+ font-family: "JetBrains Mono", "Cascadia Code", "DejaVu Sans Mono", monospace;
54
+ font-size: 14px;
55
+ line-height: 1.65;
56
+ }
57
+ #analysis-code .cm-gutters {
58
+ background: #292d30;
59
+ color: #a0a0a0;
60
+ border-right: 1px solid #454a4e;
61
+ }
62
+ #analysis-code .cm-activeLine,
63
+ #analysis-code .cm-activeLineGutter {
64
+ background: #343a3f;
65
+ }
66
+ #analysis-code .cm-cursor {
67
+ border-left-color: #fda331;
68
+ }
69
+ #analysis-code .cm-selectionBackground,
70
+ #analysis-code .cm-content ::selection {
71
+ background: #46596b !important;
72
+ }
73
+ """
74
+
75
+ DARK_MODE_HEAD = """<script>
76
+ (() => {
77
+ const url = new URL(window.location.href);
78
+ if (!url.searchParams.has("__theme")) {
79
+ url.searchParams.set("__theme", "dark");
80
+ window.history.replaceState(null, "", url);
81
+ }
82
+ })();
83
+ </script>"""
84
+
85
+
86
+ def load_data(dataset_name, uploaded):
87
+ if uploaded:
88
+ path = Path(uploaded)
89
+ suffix = path.suffix.lower()
90
+ if suffix == ".csv":
91
+ df = pd.read_csv(path)
92
+ elif suffix in {".xlsx", ".xls"}:
93
+ df = pd.read_excel(path)
94
+ elif suffix == ".sav":
95
+ df = pd.read_spss(path)
96
+ else:
97
+ raise ValueError("Upload a CSV, Excel, or SPSS .sav file.")
98
+ note = f"Uploaded file: {path.name}"
99
+ else:
100
+ filename, note = DATASETS[dataset_name]
101
+ df = pg.read_dataset("penguins") if filename is None else pd.read_csv(BASE / filename)
102
+
103
+ df.columns = [str(c).strip() for c in df.columns]
104
+ return df, note
105
+
106
+
107
+ def defaults_for(dataset_name, all_cols, num_cols):
108
+ def choose(name, pool, fallback=0):
109
+ if name in pool:
110
+ return name
111
+ return pool[min(fallback, len(pool) - 1)] if pool else None
112
+
113
+ x = choose("score", num_cols)
114
+ y = choose("age", num_cols, 1)
115
+ group = choose("group", all_cols)
116
+ subject = choose("participant", all_cols)
117
+ within = choose("time", all_cols)
118
+ dv = choose("score", num_cols)
119
+ covars = [c for c in ["age", "stress"] if c in num_cols]
120
+ items = [c for c in num_cols if c.lower().startswith("item")]
121
+
122
+ if dataset_name == "General teaching data":
123
+ x = choose("pre_score", num_cols)
124
+ y = choose("post_score", num_cols, 1)
125
+ dv = choose("outcome", num_cols)
126
+ elif dataset_name == "Reliability data":
127
+ x = choose("item1", num_cols)
128
+ y = choose("item2", num_cols, 1)
129
+ elif dataset_name == "Categorical data":
130
+ group = choose("treatment", all_cols)
131
+ within = choose("outcome", all_cols, 1)
132
+ elif dataset_name == "Nonparametric data":
133
+ x = choose("before", num_cols)
134
+ y = choose("after", num_cols, 1)
135
+ dv = choose("skewed_score", num_cols)
136
+
137
+ return x, y, group, subject, within, dv, covars, items
138
+
139
+
140
+ def refresh(dataset_name, uploaded):
141
+ try:
142
+ df, note = load_data(dataset_name, uploaded)
143
+ all_cols = list(df.columns)
144
+ num_cols = list(df.select_dtypes(include=np.number).columns)
145
+ x, y, group, subject, within, dv, covars, items = defaults_for(dataset_name, all_cols, num_cols)
146
+ return (
147
+ note,
148
+ df,
149
+ gr.update(choices=num_cols, value=x),
150
+ gr.update(choices=num_cols, value=y),
151
+ gr.update(choices=all_cols, value=group),
152
+ gr.update(choices=all_cols, value=subject),
153
+ gr.update(choices=all_cols, value=within),
154
+ gr.update(choices=num_cols, value=dv),
155
+ gr.update(choices=num_cols, value=[]),
156
+ gr.update(choices=num_cols, value=covars),
157
+ gr.update(choices=num_cols, value=items),
158
+ gr.update(choices=all_cols, value=dv),
159
+ gr.update(choices=all_cols, value=[]),
160
+ gr.update(
161
+ choices=[str(value) for value in df[dv].dropna().unique()],
162
+ value=[str(value) for value in df[dv].dropna().unique()],
163
+ ),
164
+ gr.update(choices=all_cols, value=[column for column in all_cols if column != dv][:3]),
165
+ )
166
+ except Exception:
167
+ blank = gr.update(choices=[], value=None)
168
+ 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=[])
169
+
170
+
171
+ def refresh_outcome_levels(dataset_name, uploaded, outcome):
172
+ try:
173
+ df, _ = load_data(dataset_name, uploaded)
174
+ levels = [str(value) for value in df[outcome].dropna().unique()] if outcome in df else []
175
+ return gr.update(choices=levels, value=levels)
176
+ except Exception:
177
+ return gr.update(choices=[], value=[])
178
+
179
+
180
+ def update_analysis_controls(analysis, t_design, nonparametric_test, model_family):
181
+ visible = {
182
+ name: False
183
+ for name in (
184
+ "x", "y", "dv", "group", "within", "subject", "selected_vars",
185
+ "covars", "items", "categorical_predictors", "method", "t_design",
186
+ "test_value", "anova_type", "posthoc_type", "nonparametric_test",
187
+ "model_family", "glm_family", "model_outcome", "outcome_order",
188
+ "model_predictors",
189
+ )
190
+ }
191
+
192
+ def show(*names):
193
+ for name in names:
194
+ visible[name] = True
195
+
196
+ analysis = analysis or ""
197
+ if analysis.startswith("1."):
198
+ show("selected_vars")
199
+ elif analysis.startswith("2."):
200
+ show("x")
201
+ elif analysis.startswith("3."):
202
+ show("x", "y", "method")
203
+ elif analysis.startswith("4."):
204
+ show("x", "y", "covars")
205
+ elif analysis.startswith("5."):
206
+ show("t_design")
207
+ if t_design == "One sample":
208
+ show("x", "test_value")
209
+ elif t_design == "Paired":
210
+ show("x", "y")
211
+ else:
212
+ show("dv", "group")
213
+ elif analysis.startswith("6."):
214
+ show("dv", "group", "anova_type", "posthoc_type")
215
+ elif analysis.startswith("7."):
216
+ show("dv", "within", "subject")
217
+ elif analysis.startswith("8."):
218
+ show("dv", "group", "covars")
219
+ elif analysis.startswith("9."):
220
+ show("nonparametric_test")
221
+ if nonparametric_test == "Mann-Whitney":
222
+ show("dv", "group")
223
+ elif nonparametric_test == "Wilcoxon":
224
+ show("x", "y")
225
+ elif nonparametric_test == "Kruskal-Wallis":
226
+ show("dv", "group")
227
+ elif nonparametric_test == "Friedman":
228
+ show("dv", "within", "subject")
229
+ elif analysis.startswith("10."):
230
+ show("dv", "selected_vars")
231
+ elif analysis.startswith("11."):
232
+ show("items")
233
+ elif analysis.startswith("12."):
234
+ show("group", "within")
235
+ elif analysis.startswith("14."):
236
+ show("model_family", "model_outcome", "model_predictors", "categorical_predictors")
237
+ if model_family == "GLM":
238
+ show("glm_family")
239
+ if model_family == "Ordinal logit":
240
+ show("outcome_order")
241
+
242
+ return [gr.update(visible=visible[name]) for name in visible]
243
+
244
+
245
+ def build_code(analysis, x, y, group, subject, within, dv, selected_vars, covars, items,
246
+ method, t_design, test_value, anova_type, posthoc_type, nonparametric_test,
247
+ model_family, glm_family, model_outcome, outcome_order, categorical_predictors,
248
+ model_predictors):
249
+ selected_vars = selected_vars or []
250
+ covars = covars or []
251
+ items = items or []
252
+
253
+ if analysis.startswith("1."):
254
+ vars_ = selected_vars or [c for c in [x, y] if c]
255
+ return "\n".join([
256
+ f"x = df[{vars_!r}]",
257
+ "result = pd.DataFrame({",
258
+ ' "N": x.count(),',
259
+ ' "Missing": x.isna().sum(),',
260
+ ' "Mean": x.mean(),',
261
+ ' "SD": x.std(),',
262
+ ' "Median": x.median(),',
263
+ ' "Minimum": x.min(),',
264
+ ' "Maximum": x.max(),',
265
+ ' "Skewness": x.skew(),',
266
+ ' "Kurtosis": x.kurt()',
267
+ "})",
268
+ ])
269
+
270
+ if analysis.startswith("2."):
271
+ return "\n".join([
272
+ f"result = pg.normality(df[{x!r}].dropna())",
273
+ f"sns.histplot(df[{x!r}].dropna(), kde=True)",
274
+ f'plt.title("Distribution of {x}")',
275
+ "plt.show()",
276
+ f"pg.qqplot(df[{x!r}].dropna())",
277
+ f'plt.title("Q-Q plot of {x}")',
278
+ "plt.show()",
279
+ ])
280
+
281
+ if analysis.startswith("3."):
282
+ return "\n".join([
283
+ f"result = pg.corr(x=df[{x!r}], y=df[{y!r}], method={method!r})",
284
+ f"sns.regplot(data=df, x={x!r}, y={y!r})",
285
+ "plt.show()",
286
+ ])
287
+
288
+ if analysis.startswith("4."):
289
+ return f"result = pg.partial_corr(data=df, x={x!r}, y={y!r}, covar={covars!r})"
290
+
291
+ if analysis.startswith("5."):
292
+ if t_design == "One sample":
293
+ return f"result = pg.ttest(df[{x!r}].dropna(), {float(test_value)})"
294
+ if t_design == "Paired":
295
+ return f"d = df[[{x!r}, {y!r}]].dropna()\nresult = pg.ttest(d[{x!r}], d[{y!r}], paired=True)"
296
+ return "\n".join([
297
+ f"levels = list(df[{group!r}].dropna().unique())",
298
+ 'if len(levels) != 2: raise ValueError("Independent t-test requires exactly two groups.")',
299
+ f"a = df.loc[df[{group!r}] == levels[0], {dv!r}].dropna()",
300
+ f"b = df.loc[df[{group!r}] == levels[1], {dv!r}].dropna()",
301
+ "result = pg.ttest(a, b)",
302
+ ])
303
+
304
+ if analysis.startswith("6."):
305
+ code = (f"result = pg.anova(data=df, dv={dv!r}, between={group!r}, detailed=True)"
306
+ if anova_type == "Classical"
307
+ else f"result = pg.welch_anova(data=df, dv={dv!r}, between={group!r})")
308
+ if posthoc_type == "Tukey":
309
+ code += f"\nposthoc = pg.pairwise_tukey(data=df, dv={dv!r}, between={group!r})"
310
+ elif posthoc_type == "Games-Howell":
311
+ code += f"\nposthoc = pg.pairwise_gameshowell(data=df, dv={dv!r}, between={group!r})"
312
+ return code
313
+
314
+ if analysis.startswith("7."):
315
+ return "\n".join([
316
+ f"d = df[[{subject!r}, {within!r}, {dv!r}]].dropna().copy()",
317
+ f"counts = d.groupby({subject!r})[{within!r}].nunique()",
318
+ "valid = counts[counts >= 2].index",
319
+ f"d = d[d[{subject!r}].isin(valid)]",
320
+ f'if d.empty or d[{within!r}].nunique() < 2: raise ValueError("No valid repeated observations.")',
321
+ f"result = pg.rm_anova(data=d, dv={dv!r}, within={within!r}, subject={subject!r}, detailed=True)",
322
+ f"posthoc = pg.pairwise_tests(data=d, dv={dv!r}, within={within!r}, subject={subject!r}, padjust='holm')",
323
+ ])
324
+
325
+ if analysis.startswith("8."):
326
+ return f"result = pg.ancova(data=df, dv={dv!r}, between={group!r}, covar={covars!r})"
327
+
328
+ if analysis.startswith("9."):
329
+ if nonparametric_test == "Mann-Whitney":
330
+ return "\n".join([
331
+ f"levels = list(df[{group!r}].dropna().unique())",
332
+ 'if len(levels) < 2: raise ValueError("At least two groups are required.")',
333
+ f"a = df.loc[df[{group!r}] == levels[0], {dv!r}].dropna()",
334
+ f"b = df.loc[df[{group!r}] == levels[1], {dv!r}].dropna()",
335
+ "result = pg.mwu(a, b)",
336
+ ])
337
+ if nonparametric_test == "Wilcoxon":
338
+ return f"d = df[[{x!r}, {y!r}]].dropna()\nresult = pg.wilcoxon(d[{x!r}], d[{y!r}])"
339
+ if nonparametric_test == "Kruskal-Wallis":
340
+ return f"result = pg.kruskal(data=df, dv={dv!r}, between={group!r})"
341
+ 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})"
342
+
343
+ if analysis.startswith("10."):
344
+ predictors = selected_vars or [x]
345
+ return f"result = pg.linear_regression(X=df[{predictors!r}], y=df[{dv!r}], remove_na=True)"
346
+
347
+ if analysis.startswith("11."):
348
+ return "\n".join([
349
+ f"alpha, ci = pg.cronbach_alpha(data=df[{items!r}])",
350
+ 'result = pd.DataFrame({"Cronbach alpha": [alpha], "CI lower": [ci[0]], "CI upper": [ci[1]]})',
351
+ ])
352
+
353
+ if analysis.startswith("14."):
354
+ outcome = model_outcome or dv
355
+ predictors = model_predictors or [c for c in [x, y] if c and c != outcome]
356
+ categorical_predictors = categorical_predictors or []
357
+ terms = [
358
+ f"C(Q({column!r}))" if column in categorical_predictors else f"Q({column!r})"
359
+ for column in predictors
360
+ ]
361
+ rhs = " + ".join(terms) or "1"
362
+ selected = [outcome, *predictors]
363
+ if model_family == "OLS (ANOVA / ANCOVA)":
364
+ return f'''import statsmodels.formula.api as smf
365
+ from statsmodels.stats.anova import anova_lm
366
+
367
+ d = df[{selected!r}].dropna().copy()
368
+ model = smf.ols(formula={f"Q({outcome!r}) ~ {rhs}"!r}, data=d).fit()
369
+ result = model.summary2().tables[1]
370
+ overall = pd.DataFrame({{"F": [model.fvalue], "df_model": [model.df_model], "p-value": [model.f_pvalue]}})
371
+ anova = anova_lm(model, typ=2)'''
372
+ if model_family in {"Logit (binary)", "Probit (binary)"}:
373
+ fit = "logit" if model_family.startswith("Logit") else "probit"
374
+ return f'''import statsmodels.formula.api as smf
375
+ from scipy.stats import chi2
376
+
377
+ d = df[{selected!r}].dropna().copy()
378
+ d["__outcome"] = pd.Categorical(d[{outcome!r}]).codes
379
+ if d["__outcome"].nunique() != 2:
380
+ raise ValueError("The selected outcome must have exactly two levels.")
381
+ model = smf.{fit}(formula={f"__outcome ~ {rhs}"!r}, data=d).fit(disp=False)
382
+ result = pd.DataFrame({{"coef": model.params, "SE": model.bse, "z": model.tvalues, "p-value": model.pvalues}})
383
+ overall = pd.DataFrame({{"LR chi2": [model.llr], "df": [model.df_model], "p-value": [model.llr_pvalue]}})'''
384
+ if model_family == "Ordinal logit":
385
+ levels = outcome_order or []
386
+ return f'''import numpy as np
387
+ import patsy
388
+ from scipy.stats import chi2
389
+ from statsmodels.miscmodels.ordinal_model import OrderedModel
390
+
391
+ d = df[{selected!r}].dropna().copy()
392
+ d["__outcome"] = pd.Categorical(d[{outcome!r}].astype(str), categories={levels!r}, ordered=True).codes
393
+ if (d["__outcome"] < 0).any() or d["__outcome"].nunique() < 2:
394
+ raise ValueError("Select all outcome levels in their intended order.")
395
+ X = patsy.dmatrix({rhs!r}, d, return_type="dataframe").drop(columns="Intercept", errors="ignore")
396
+ if X.empty:
397
+ raise ValueError("Select at least one predictor for ordinal logit.")
398
+ model = OrderedModel(d["__outcome"], X, distr="logit").fit(method="bfgs", disp=False)
399
+ 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]}})
400
+ null_model = OrderedModel(d["__outcome"], np.empty((len(d), 0)), distr="logit").fit(method="bfgs", disp=False)
401
+ lr_chi2 = 2 * (model.llf - null_model.llf)
402
+ overall = pd.DataFrame({{"LR chi2": [lr_chi2], "df": [X.shape[1]], "p-value": [chi2.sf(lr_chi2, X.shape[1])]}})'''
403
+ family = {
404
+ "Poisson": "sm.families.Poisson()",
405
+ "Negative binomial": "sm.families.NegativeBinomial()",
406
+ "Gaussian": "sm.families.Gaussian()",
407
+ "Binomial": "sm.families.Binomial()",
408
+ "Gamma": "sm.families.Gamma()",
409
+ }.get(glm_family or "Gaussian", "sm.families.Gaussian()")
410
+ response = f'd["__outcome"] = pd.Categorical(d[{outcome!r}]).codes\n' if glm_family == "Binomial" else ""
411
+ glm_outcome = "__outcome" if glm_family == "Binomial" else f"Q({outcome!r})"
412
+ return f'''import statsmodels.api as sm
413
+ import statsmodels.formula.api as smf
414
+ from scipy.stats import chi2
415
+
416
+ d = df[{selected!r}].dropna().copy()
417
+ {response}family = {family}
418
+ model = smf.glm(formula={f"{glm_outcome} ~ {rhs}"!r}, data=d, family=family).fit()
419
+ result = pd.DataFrame({{"coef": model.params, "SE": model.bse, "z": model.tvalues, "p-value": model.pvalues}})
420
+ null_model = smf.glm(formula={f"{glm_outcome} ~ 1"!r}, data=d, family=family).fit()
421
+ lr_chi2 = max(0, 2 * (model.llf - null_model.llf))
422
+ lr_df = model.df_model - null_model.df_model
423
+ overall = pd.DataFrame({{"LR chi2": [lr_chi2], "df": [lr_df], "p-value": [chi2.sf(lr_chi2, lr_df)]}})'''
424
+
425
+ if analysis.startswith("13."):
426
+ return "# Generate exam versions in the Exam generator tab."
427
+ return f"expected, observed, result = pg.chi2_independence(data=df, x={group!r}, y={within!r})"
428
+
429
+
430
+ def _as_frame(value):
431
+ if isinstance(value, pd.Series):
432
+ frame = value.to_frame(name=value.name or "Value")
433
+ if isinstance(value.index, pd.MultiIndex):
434
+ return frame.reset_index()
435
+ return frame.rename_axis(value.index.name or "Statistic").reset_index()
436
+ if isinstance(value, pd.DataFrame):
437
+ if (
438
+ isinstance(value.index, pd.RangeIndex)
439
+ and value.index.name is None
440
+ and value.index.equals(pd.RangeIndex(len(value)))
441
+ ):
442
+ return value
443
+ if isinstance(value.index, pd.MultiIndex):
444
+ return value.reset_index()
445
+ index_name = value.index.name or (
446
+ "Statistic" if all(isinstance(label, str) for label in value.index) else "Index"
447
+ )
448
+ return value.rename_axis(index_name).reset_index()
449
+ if pd.api.types.is_scalar(value):
450
+ return pd.DataFrame({"value": [value]})
451
+ try:
452
+ return pd.DataFrame(value)
453
+ except (TypeError, ValueError):
454
+ return pd.DataFrame({"value": [repr(value)]})
455
+
456
+
457
+ def run_code(dataset_name, uploaded, edited_data, code):
458
+ try:
459
+ df, _ = load_data(dataset_name, uploaded)
460
+ if isinstance(edited_data, pd.DataFrame) and not edited_data.empty:
461
+ df = edited_data.copy()
462
+ env = {"df": df.copy(), "pd": pd, "np": np, "pg": pg, "sns": sns, "plt": plt}
463
+ plt.close("all")
464
+ exec(code, env)
465
+ outputs = []
466
+ named_outputs = [(name, env[name]) for name in ["result", "posthoc", "observed", "expected", "overall", "anova"] if name in env]
467
+ for name in ["result", "posthoc", "observed", "expected", "overall", "anova"]:
468
+ outputs.append(_as_frame(env[name]) if name in env else pd.DataFrame())
469
+ figures = []
470
+ plot_pngs = []
471
+ for number in plt.get_fignums():
472
+ image_buffer = io.BytesIO()
473
+ plt.figure(number).savefig(image_buffer, format="png", bbox_inches="tight")
474
+ png_data = image_buffer.getvalue()
475
+ plot_pngs.append(png_data)
476
+ figures.append(Image.open(io.BytesIO(png_data)).convert("RGB"))
477
+ with tempfile.NamedTemporaryFile(prefix="statistics-results-", suffix=".zip", delete=False) as archive_file:
478
+ archive_path = archive_file.name
479
+ with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive:
480
+ archive.writestr("data.csv", df.to_csv(index=False))
481
+ archive.writestr("analysis.py", code or "")
482
+ for name, value in named_outputs:
483
+ archive.writestr(f"{name}.csv", _as_frame(value).to_csv(index=False))
484
+ for index, png_data in enumerate(plot_pngs, start=1):
485
+ archive.writestr(f"plot_{index}.png", png_data)
486
+ return (*outputs, figures, "Analysis completed.", archive_path)
487
+ except Exception:
488
+ return *(pd.DataFrame() for _ in range(6)), [], traceback.format_exc(), None
489
+
490
+
491
+ def load_python_file(uploaded_code):
492
+ if not uploaded_code:
493
+ return gr.update()
494
+ try:
495
+ return Path(uploaded_code).read_text(encoding="utf-8-sig")
496
+ except UnicodeDecodeError:
497
+ raise gr.Error("The selected Python file must use UTF-8 encoding.")
498
+
499
+
500
+ def save_python_file(code):
501
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", prefix="analysis-", encoding="utf-8", delete=False) as output:
502
+ output.write(code or "")
503
+ return output.name
504
+
505
+
506
+ def save_dataframe_csv(data):
507
+ if not isinstance(data, pd.DataFrame):
508
+ data = pd.DataFrame(data)
509
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", prefix="data-", encoding="utf-8", delete=False) as output:
510
+ data.to_csv(output, index=False)
511
+ return output.name
512
+
513
+
514
+ def generate_exam_package(uploaded_qti, num_tests, compact):
515
+ if not uploaded_qti:
516
+ raise gr.Error("Upload a QTI XML file first.")
517
+ try:
518
+ root = ET.parse(uploaded_qti).getroot()
519
+ questions = []
520
+ for item in root.iter():
521
+ if not item.tag.endswith("item"):
522
+ continue
523
+ prompt = item.find(".//{*}presentation//{*}mattext")
524
+ question_text = BeautifulSoup(prompt.text or "", "html.parser").get_text(" ", strip=True) if prompt is not None else ""
525
+ options = []
526
+ id_to_text = {}
527
+ for label in item.findall(".//{*}response_label"):
528
+ option = label.find(".//{*}mattext")
529
+ text = BeautifulSoup(option.text or "", "html.parser").get_text(" ", strip=True) if option is not None else ""
530
+ options.append(text)
531
+ id_to_text[label.attrib.get("ident", "")] = text
532
+ correct = ""
533
+ for condition in item.findall(".//{*}respcondition"):
534
+ score = condition.find(".//{*}setvar")
535
+ answer = condition.find(".//{*}varequal")
536
+ if score is not None and float((score.text or "0").strip()) == 100 and answer is not None:
537
+ correct = id_to_text.get((answer.text or "").strip(), "")
538
+ break
539
+ if question_text:
540
+ questions.append({"question": question_text, "options": options, "correct": correct})
541
+ if not questions:
542
+ raise ValueError("No questions were found in the QTI file.")
543
+ with tempfile.NamedTemporaryFile(suffix=".zip", prefix="exam-versions-", delete=False) as output:
544
+ archive_path = output.name
545
+ with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive:
546
+ for test_num in range(1, int(num_tests) + 1):
547
+ rng = np.random.default_rng(test_num)
548
+ shuffled = questions.copy()
549
+ rng.shuffle(shuffled)
550
+ test_lines, answer_key = [], []
551
+ for question_num, question in enumerate(shuffled, start=1):
552
+ options = question["options"].copy()
553
+ rng.shuffle(options)
554
+ option_lines = [f"{chr(65 + index)}) {option}" for index, option in enumerate(options)]
555
+ correct_letter = next((chr(65 + index) for index, option in enumerate(options) if option == question["correct"]), "")
556
+ if compact:
557
+ test_lines.append(f"{question_num}. {question['question']} " + " ".join(option_lines))
558
+ else:
559
+ test_lines.append(f"{question_num}. {question['question']}\n" + "\n".join(option_lines))
560
+ answer_key.append({"Question": question_num, "Answer": correct_letter, "Correct_Text": question["correct"]})
561
+ archive.writestr(f"test{test_num}.txt", "\n\n".join(test_lines))
562
+ archive.writestr(f"key{test_num}.csv", pd.DataFrame(answer_key).to_csv(index=False))
563
+ return archive_path, f"Generated {int(num_tests)} versions from {len(questions)} questions."
564
+ except Exception as error:
565
+ raise gr.Error(f"Could not generate exams: {error}")
566
+
567
+
568
+ with gr.Blocks(title="Student Statistics Lab — Gradio") as demo:
569
+ gr.Markdown("# Student Statistics Lab\nStatistical analyses, editable Python, and teaching-data tools.")
570
+
571
+ with gr.Row():
572
+ with gr.Column(scale=1):
573
+ dataset = gr.Dropdown(list(DATASETS), value="General teaching data", label="Built-in teaching dataset")
574
+ upload = gr.File(label="Or upload CSV, Excel, or SAV", file_types=[".csv", ".xlsx", ".xls", ".sav"], type="filepath")
575
+ dataset_note = gr.Markdown()
576
+ analysis = gr.Dropdown(ANALYSES, value=ANALYSES[0], label="Analysis")
577
+ with gr.Column(scale=2):
578
+ preview = gr.Dataframe(label="Editable data preview", interactive=True, max_height=420)
579
+ download_dataframe = gr.DownloadButton("Download dataframe (.csv)")
580
+ download_dataframe.click(save_dataframe_csv, [preview], [download_dataframe])
581
+
582
+ with gr.Accordion("Variables and options", open=True):
583
+ with gr.Row():
584
+ x = gr.Dropdown(label="Variable X / Variable 1")
585
+ y = gr.Dropdown(label="Variable Y / Variable 2")
586
+ dv = gr.Dropdown(label="Dependent/outcome variable")
587
+ with gr.Row():
588
+ group = gr.Dropdown(label="Grouping/factor/row variable")
589
+ within = gr.Dropdown(label="Within factor/column variable")
590
+ subject = gr.Dropdown(label="Subject ID")
591
+ with gr.Row():
592
+ selected_vars = gr.Dropdown(multiselect=True, label="Variables / predictors")
593
+ covars = gr.Dropdown(multiselect=True, label="Covariates")
594
+ items = gr.Dropdown(multiselect=True, label="Scale items")
595
+ categorical_predictors = gr.Dropdown(multiselect=True, label="Categorical predictors")
596
+ with gr.Row():
597
+ method = gr.Dropdown(["pearson", "spearman", "kendall", "bicor"], value="pearson", label="Correlation method")
598
+ t_design = gr.Dropdown(["One sample", "Paired", "Independent"], value="Independent", label="T-test design")
599
+ test_value = gr.Number(value=0, label="One-sample test value")
600
+ with gr.Row():
601
+ anova_type = gr.Dropdown(["Classical", "Welch"], value="Classical", label="ANOVA type")
602
+ posthoc_type = gr.Dropdown(["None", "Tukey", "Games-Howell"], value="None", label="Post hoc")
603
+ nonparametric_test = gr.Dropdown(["Mann-Whitney", "Wilcoxon", "Kruskal-Wallis", "Friedman"], value="Mann-Whitney", label="Nonparametric test")
604
+ with gr.Row():
605
+ model_family = gr.Dropdown(
606
+ ["OLS (ANOVA / ANCOVA)", "Logit (binary)", "Probit (binary)", "Ordinal logit", "GLM"],
607
+ value="OLS (ANOVA / ANCOVA)", label="Statsmodels family",
608
+ )
609
+ glm_family = gr.Dropdown(
610
+ ["Poisson", "Negative binomial", "Gaussian", "Binomial", "Gamma"],
611
+ value="Gaussian", label="GLM distribution",
612
+ )
613
+ model_outcome = gr.Dropdown(label="Statsmodels outcome")
614
+ outcome_order = gr.Dropdown(multiselect=True, label="Ordinal outcome order")
615
+ model_predictors = gr.Dropdown(multiselect=True, label="Statsmodels predictors")
616
+
617
+ generate = gr.Button("Generate code")
618
+ code = gr.Code(
619
+ label="Editable Python code",
620
+ language="python",
621
+ interactive=True,
622
+ lines=22,
623
+ max_lines=36,
624
+ wrap_lines=True,
625
+ autocomplete=True,
626
+ buttons=["copy"],
627
+ elem_id="analysis-code",
628
+ )
629
+ with gr.Row():
630
+ code_upload = gr.File(label="Import Python code", file_types=[".py"], type="filepath")
631
+ load_code = gr.Button("Load into editor")
632
+ download_code = gr.DownloadButton("Download .py")
633
+ load_code.click(load_python_file, [code_upload], [code])
634
+ download_code.click(save_python_file, [code], [download_code])
635
+ run = gr.Button("Run analysis", variant="primary")
636
+ status = gr.Textbox(label="Status / error", lines=8)
637
+
638
+ with gr.Tabs():
639
+ with gr.Tab("Result"):
640
+ result = gr.Dataframe(interactive=False)
641
+ with gr.Tab("Post hoc"):
642
+ posthoc = gr.Dataframe(interactive=False)
643
+ with gr.Tab("Observed"):
644
+ observed = gr.Dataframe(interactive=False)
645
+ with gr.Tab("Expected"):
646
+ expected = gr.Dataframe(interactive=False)
647
+ with gr.Tab("Overall model"):
648
+ overall = gr.Dataframe(interactive=False)
649
+ with gr.Tab("ANOVA table"):
650
+ anova = gr.Dataframe(interactive=False)
651
+ with gr.Tab("Plots"):
652
+ plots = gr.Gallery(label="Plots", columns=2, object_fit="contain")
653
+ with gr.Tab("Exam generator"):
654
+ qti_upload = gr.File(label="Upload QTI XML", file_types=[".xml"], type="filepath")
655
+ exam_count = gr.Number(label="Number of test versions", value=4, minimum=1, maximum=100, step=1)
656
+ compact_exam = gr.Checkbox(label="Compact one-line format", value=True)
657
+ generate_exams = gr.Button("Generate exam package", variant="primary")
658
+ exam_status = gr.Textbox(label="Exam generation status")
659
+ exam_download = gr.DownloadButton("Download ZIP package")
660
+ generate_exams.click(
661
+ generate_exam_package,
662
+ [qti_upload, exam_count, compact_exam],
663
+ [exam_download, exam_status],
664
+ )
665
+ with gr.Tab("Downloads"):
666
+ results_download = gr.DownloadButton("Download data, code, results, and plots (.zip)")
667
+
668
+ analysis_controls = {
669
+ "x": x,
670
+ "y": y,
671
+ "dv": dv,
672
+ "group": group,
673
+ "within": within,
674
+ "subject": subject,
675
+ "selected_vars": selected_vars,
676
+ "covars": covars,
677
+ "items": items,
678
+ "categorical_predictors": categorical_predictors,
679
+ "method": method,
680
+ "t_design": t_design,
681
+ "test_value": test_value,
682
+ "anova_type": anova_type,
683
+ "posthoc_type": posthoc_type,
684
+ "nonparametric_test": nonparametric_test,
685
+ "model_family": model_family,
686
+ "glm_family": glm_family,
687
+ "model_outcome": model_outcome,
688
+ "outcome_order": outcome_order,
689
+ "model_predictors": model_predictors,
690
+ }
691
+ visibility_inputs = [analysis, t_design, nonparametric_test, model_family]
692
+ visibility_outputs = list(analysis_controls.values())
693
+ demo.load(update_analysis_controls, visibility_inputs, visibility_outputs)
694
+ analysis.change(update_analysis_controls, visibility_inputs, visibility_outputs)
695
+ t_design.change(update_analysis_controls, visibility_inputs, visibility_outputs)
696
+ nonparametric_test.change(update_analysis_controls, visibility_inputs, visibility_outputs)
697
+ model_family.change(update_analysis_controls, visibility_inputs, visibility_outputs)
698
+
699
+ refresh_inputs = [dataset, upload]
700
+ refresh_outputs = [dataset_note, preview, x, y, group, subject, within, dv,
701
+ selected_vars, covars, items, model_outcome, categorical_predictors,
702
+ outcome_order, model_predictors]
703
+ demo.load(refresh, refresh_inputs, refresh_outputs)
704
+ dataset.change(refresh, refresh_inputs, refresh_outputs)
705
+ upload.change(refresh, refresh_inputs, refresh_outputs)
706
+ model_outcome.change(refresh_outcome_levels, [dataset, upload, model_outcome], [outcome_order])
707
+
708
+ code_inputs = [analysis, x, y, group, subject, within, dv, selected_vars, covars, items,
709
+ method, t_design, test_value, anova_type, posthoc_type, nonparametric_test,
710
+ model_family, glm_family, model_outcome, outcome_order, categorical_predictors,
711
+ model_predictors]
712
+ generate.click(build_code, code_inputs, code)
713
+ run.click(
714
+ run_code,
715
+ [dataset, upload, preview, code],
716
+ [result, posthoc, observed, expected, overall, anova, plots, status, results_download],
717
+ )
718
+
719
+ if __name__ == "__main__":
720
+ demo.launch(
721
+ server_name="0.0.0.0",
722
+ server_port=7860,
723
+ ssr_mode=False,
724
+ css=EDITOR_CSS,
725
+ head=DARK_MODE_HEAD,
726
+ )