openstat-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openstat/__init__.py +3 -0
- openstat/__main__.py +4 -0
- openstat/backends/__init__.py +16 -0
- openstat/backends/duckdb_backend.py +70 -0
- openstat/backends/polars_backend.py +52 -0
- openstat/cli.py +92 -0
- openstat/commands/__init__.py +82 -0
- openstat/commands/adv_stat_cmds.py +1255 -0
- openstat/commands/advanced_ml_cmds.py +576 -0
- openstat/commands/advreg_cmds.py +207 -0
- openstat/commands/alias_cmds.py +135 -0
- openstat/commands/arch_cmds.py +82 -0
- openstat/commands/arules_cmds.py +111 -0
- openstat/commands/automodel_cmds.py +212 -0
- openstat/commands/backend_cmds.py +82 -0
- openstat/commands/base.py +170 -0
- openstat/commands/bayes_cmds.py +71 -0
- openstat/commands/causal_cmds.py +269 -0
- openstat/commands/cluster_cmds.py +152 -0
- openstat/commands/data_cmds.py +996 -0
- openstat/commands/datamanip_cmds.py +672 -0
- openstat/commands/dataquality_cmds.py +174 -0
- openstat/commands/datetime_cmds.py +176 -0
- openstat/commands/dimreduce_cmds.py +184 -0
- openstat/commands/discrete_cmds.py +149 -0
- openstat/commands/dsl_cmds.py +143 -0
- openstat/commands/epi_cmds.py +93 -0
- openstat/commands/equiv_tobit_cmds.py +94 -0
- openstat/commands/esttab_cmds.py +196 -0
- openstat/commands/export_beamer_cmds.py +142 -0
- openstat/commands/export_cmds.py +201 -0
- openstat/commands/export_extra_cmds.py +240 -0
- openstat/commands/factor_cmds.py +180 -0
- openstat/commands/groupby_cmds.py +155 -0
- openstat/commands/help_cmds.py +237 -0
- openstat/commands/i18n_cmds.py +43 -0
- openstat/commands/import_extra_cmds.py +561 -0
- openstat/commands/influence_cmds.py +134 -0
- openstat/commands/iv_cmds.py +106 -0
- openstat/commands/manova_cmds.py +105 -0
- openstat/commands/mediate_cmds.py +233 -0
- openstat/commands/meta_cmds.py +284 -0
- openstat/commands/mi_cmds.py +228 -0
- openstat/commands/mixed_cmds.py +79 -0
- openstat/commands/mixture_changepoint_cmds.py +166 -0
- openstat/commands/ml_adv_cmds.py +147 -0
- openstat/commands/ml_cmds.py +178 -0
- openstat/commands/model_eval_cmds.py +142 -0
- openstat/commands/network_cmds.py +288 -0
- openstat/commands/nlquery_cmds.py +161 -0
- openstat/commands/nonparam_cmds.py +149 -0
- openstat/commands/outreg_cmds.py +247 -0
- openstat/commands/panel_cmds.py +141 -0
- openstat/commands/pdf_cmds.py +226 -0
- openstat/commands/pipeline_cmds.py +319 -0
- openstat/commands/plot_cmds.py +189 -0
- openstat/commands/plugin_cmds.py +79 -0
- openstat/commands/posthoc_cmds.py +153 -0
- openstat/commands/power_cmds.py +172 -0
- openstat/commands/profile_cmds.py +246 -0
- openstat/commands/rbridge_cmds.py +81 -0
- openstat/commands/regex_cmds.py +104 -0
- openstat/commands/report_cmds.py +48 -0
- openstat/commands/repro_cmds.py +129 -0
- openstat/commands/resampling_cmds.py +109 -0
- openstat/commands/reshape_cmds.py +223 -0
- openstat/commands/sem_cmds.py +177 -0
- openstat/commands/stat_cmds.py +1040 -0
- openstat/commands/stata_import_cmds.py +215 -0
- openstat/commands/string_cmds.py +124 -0
- openstat/commands/surv_cmds.py +145 -0
- openstat/commands/survey_cmds.py +153 -0
- openstat/commands/textanalysis_cmds.py +192 -0
- openstat/commands/ts_adv_cmds.py +136 -0
- openstat/commands/ts_cmds.py +195 -0
- openstat/commands/tui_cmds.py +111 -0
- openstat/commands/ux_cmds.py +191 -0
- openstat/commands/validate_cmds.py +270 -0
- openstat/commands/viz_adv_cmds.py +312 -0
- openstat/commands/viz_extra_cmds.py +251 -0
- openstat/commands/watch_cmds.py +69 -0
- openstat/config.py +106 -0
- openstat/dsl/__init__.py +0 -0
- openstat/dsl/parser.py +332 -0
- openstat/dsl/tokenizer.py +105 -0
- openstat/i18n.py +120 -0
- openstat/io/__init__.py +0 -0
- openstat/io/loader.py +187 -0
- openstat/jupyter/__init__.py +18 -0
- openstat/jupyter/display.py +18 -0
- openstat/jupyter/magic.py +60 -0
- openstat/logging_config.py +59 -0
- openstat/plots/__init__.py +0 -0
- openstat/plots/plotter.py +437 -0
- openstat/plots/surv_plots.py +32 -0
- openstat/plots/ts_plots.py +59 -0
- openstat/plugins/__init__.py +5 -0
- openstat/plugins/manager.py +69 -0
- openstat/repl.py +457 -0
- openstat/reporting/__init__.py +0 -0
- openstat/reporting/eda.py +208 -0
- openstat/reporting/report.py +67 -0
- openstat/script_runner.py +319 -0
- openstat/session.py +133 -0
- openstat/stats/__init__.py +0 -0
- openstat/stats/advanced_regression.py +269 -0
- openstat/stats/arch_garch.py +84 -0
- openstat/stats/bayesian.py +103 -0
- openstat/stats/causal.py +258 -0
- openstat/stats/clustering.py +206 -0
- openstat/stats/discrete.py +311 -0
- openstat/stats/epidemiology.py +119 -0
- openstat/stats/equiv_tobit.py +163 -0
- openstat/stats/factor.py +174 -0
- openstat/stats/imputation.py +282 -0
- openstat/stats/influence.py +78 -0
- openstat/stats/iv.py +131 -0
- openstat/stats/manova.py +124 -0
- openstat/stats/mixed.py +128 -0
- openstat/stats/ml.py +275 -0
- openstat/stats/ml_advanced.py +117 -0
- openstat/stats/model_eval.py +183 -0
- openstat/stats/models.py +1342 -0
- openstat/stats/nonparametric.py +130 -0
- openstat/stats/panel.py +179 -0
- openstat/stats/power.py +295 -0
- openstat/stats/resampling.py +203 -0
- openstat/stats/survey.py +213 -0
- openstat/stats/survival.py +196 -0
- openstat/stats/timeseries.py +142 -0
- openstat/stats/ts_advanced.py +114 -0
- openstat/types.py +11 -0
- openstat/web/__init__.py +1 -0
- openstat/web/app.py +117 -0
- openstat/web/session_manager.py +73 -0
- openstat/web/static/app.js +117 -0
- openstat/web/static/index.html +38 -0
- openstat/web/static/style.css +103 -0
- openstat_cli-1.0.0.dist-info/METADATA +748 -0
- openstat_cli-1.0.0.dist-info/RECORD +143 -0
- openstat_cli-1.0.0.dist-info/WHEEL +4 -0
- openstat_cli-1.0.0.dist-info/entry_points.txt +2 -0
- openstat_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Pipeline, batch processing, git integration, progress bars, config profiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from openstat.commands.base import command, CommandArgs, friendly_error
|
|
9
|
+
from openstat.session import Session
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ── Pipeline ─────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
@command("pipeline", usage="pipeline define|run|list|show|rm <name> [commands...]")
|
|
15
|
+
def cmd_pipeline(session: Session, args: str) -> str:
|
|
16
|
+
"""Define and run named command pipelines.
|
|
17
|
+
|
|
18
|
+
A pipeline is a named sequence of commands that can be run in one go.
|
|
19
|
+
Pipelines are stored in the session and can be saved via 'session save'.
|
|
20
|
+
|
|
21
|
+
Sub-commands:
|
|
22
|
+
pipeline define <name> <cmd1> | <cmd2> | ... — define a pipeline
|
|
23
|
+
pipeline run <name> — run a defined pipeline
|
|
24
|
+
pipeline list — list all pipelines
|
|
25
|
+
pipeline show <name> — show pipeline steps
|
|
26
|
+
pipeline rm <name> — remove a pipeline
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
pipeline define clean "drop id" | "rename old_name new_name" | "generate x2 = x*x"
|
|
30
|
+
pipeline run clean
|
|
31
|
+
pipeline define model_run "ols y x1 x2" | "plot coef" | "export pdf"
|
|
32
|
+
pipeline run model_run
|
|
33
|
+
"""
|
|
34
|
+
_PIPELINES: dict = getattr(session, "_pipelines", {})
|
|
35
|
+
if not hasattr(session, "_pipelines"):
|
|
36
|
+
session._pipelines = _PIPELINES # type: ignore[attr-defined]
|
|
37
|
+
|
|
38
|
+
tokens = args.strip().split(None, 2)
|
|
39
|
+
if not tokens:
|
|
40
|
+
return "Usage: pipeline define|run|list|show|rm <name> [steps]"
|
|
41
|
+
subcmd = tokens[0].lower()
|
|
42
|
+
|
|
43
|
+
if subcmd == "list":
|
|
44
|
+
if not _PIPELINES:
|
|
45
|
+
return "No pipelines defined. Use: pipeline define <name> <cmd> | <cmd>"
|
|
46
|
+
lines = ["Defined pipelines:"]
|
|
47
|
+
for name, steps in _PIPELINES.items():
|
|
48
|
+
lines.append(f" {name} ({len(steps)} steps)")
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
if subcmd == "rm":
|
|
52
|
+
name = tokens[1] if len(tokens) > 1 else ""
|
|
53
|
+
if not name:
|
|
54
|
+
return "Usage: pipeline rm <name>"
|
|
55
|
+
if name in _PIPELINES:
|
|
56
|
+
del _PIPELINES[name]
|
|
57
|
+
return f"Pipeline '{name}' removed."
|
|
58
|
+
return f"Pipeline '{name}' not found."
|
|
59
|
+
|
|
60
|
+
if subcmd == "show":
|
|
61
|
+
name = tokens[1] if len(tokens) > 1 else ""
|
|
62
|
+
if name not in _PIPELINES:
|
|
63
|
+
return f"Pipeline '{name}' not found."
|
|
64
|
+
steps = _PIPELINES[name]
|
|
65
|
+
lines = [f"Pipeline '{name}' ({len(steps)} steps):"]
|
|
66
|
+
for i, step in enumerate(steps, 1):
|
|
67
|
+
lines.append(f" {i}. {step}")
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
if subcmd == "define":
|
|
71
|
+
if len(tokens) < 3:
|
|
72
|
+
return "Usage: pipeline define <name> <cmd1> | <cmd2> | ..."
|
|
73
|
+
name = tokens[1]
|
|
74
|
+
rest = tokens[2]
|
|
75
|
+
steps = [s.strip().strip('"\'') for s in rest.split("|")]
|
|
76
|
+
steps = [s for s in steps if s]
|
|
77
|
+
_PIPELINES[name] = steps
|
|
78
|
+
return f"Pipeline '{name}' defined with {len(steps)} steps."
|
|
79
|
+
|
|
80
|
+
if subcmd == "run":
|
|
81
|
+
name = tokens[1] if len(tokens) > 1 else ""
|
|
82
|
+
if not name:
|
|
83
|
+
return "Usage: pipeline run <name>"
|
|
84
|
+
if name not in _PIPELINES:
|
|
85
|
+
return f"Pipeline '{name}' not found. Use 'pipeline list' to see available."
|
|
86
|
+
steps = _PIPELINES[name]
|
|
87
|
+
from openstat.commands.base import run_command
|
|
88
|
+
results = [f"Running pipeline '{name}' ({len(steps)} steps):", ""]
|
|
89
|
+
for i, step in enumerate(steps, 1):
|
|
90
|
+
results.append(f"[{i}/{len(steps)}] {step}")
|
|
91
|
+
try:
|
|
92
|
+
out = run_command(session, step)
|
|
93
|
+
if out:
|
|
94
|
+
results.append(f" → {out[:200]}")
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
results.append(f" → ERROR: {exc}")
|
|
97
|
+
results.append(f"Pipeline stopped at step {i}.")
|
|
98
|
+
break
|
|
99
|
+
results.append("")
|
|
100
|
+
results.append("Pipeline complete.")
|
|
101
|
+
return "\n".join(results)
|
|
102
|
+
|
|
103
|
+
return f"Unknown pipeline sub-command: {subcmd}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Batch ────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
@command("batch", usage="batch <script1.ost> [script2.ost ...] [--stop-on-error]")
|
|
109
|
+
def cmd_batch(session: Session, args: str) -> str:
|
|
110
|
+
"""Run multiple script files in sequence.
|
|
111
|
+
|
|
112
|
+
Options:
|
|
113
|
+
--stop-on-error halt if any script fails (default: continue)
|
|
114
|
+
--fresh start each script with a fresh session
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
batch clean.ost model.ost report.ost
|
|
118
|
+
batch *.ost --stop-on-error
|
|
119
|
+
"""
|
|
120
|
+
import glob as _glob
|
|
121
|
+
|
|
122
|
+
ca = CommandArgs(args)
|
|
123
|
+
if not ca.positional:
|
|
124
|
+
return "Usage: batch <script1.ost> [script2.ost ...] [--stop-on-error]"
|
|
125
|
+
|
|
126
|
+
stop_on_error = "--stop-on-error" in args
|
|
127
|
+
fresh = "--fresh" in args
|
|
128
|
+
|
|
129
|
+
# Expand glob patterns
|
|
130
|
+
files = []
|
|
131
|
+
for pat in ca.positional:
|
|
132
|
+
matched = sorted(_glob.glob(pat))
|
|
133
|
+
if matched:
|
|
134
|
+
files.extend(matched)
|
|
135
|
+
else:
|
|
136
|
+
files.append(pat)
|
|
137
|
+
|
|
138
|
+
if not files:
|
|
139
|
+
return "No script files found."
|
|
140
|
+
|
|
141
|
+
from openstat.script_runner import run_script_advanced
|
|
142
|
+
results = [f"Batch: {len(files)} scripts", ""]
|
|
143
|
+
errors = 0
|
|
144
|
+
|
|
145
|
+
for i, fpath in enumerate(files, 1):
|
|
146
|
+
if not Path(fpath).exists():
|
|
147
|
+
msg = f"[{i}/{len(files)}] SKIP {fpath} — file not found"
|
|
148
|
+
results.append(msg)
|
|
149
|
+
errors += 1
|
|
150
|
+
if stop_on_error:
|
|
151
|
+
break
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
results.append(f"[{i}/{len(files)}] Running: {fpath}")
|
|
155
|
+
try:
|
|
156
|
+
run_sess = Session() if fresh else session
|
|
157
|
+
run_script_advanced(fpath, run_sess)
|
|
158
|
+
results.append(f" → OK")
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
results.append(f" → ERROR: {exc}")
|
|
161
|
+
errors += 1
|
|
162
|
+
if stop_on_error:
|
|
163
|
+
results.append("Stopped due to error (--stop-on-error).")
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
results += ["", f"Batch complete. {len(files) - errors}/{len(files)} scripts succeeded."]
|
|
167
|
+
return "\n".join(results)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ── Git integration ──────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
@command("git", usage="git init|status|add|commit|log|diff [args]")
|
|
173
|
+
def cmd_git(session: Session, args: str) -> str:
|
|
174
|
+
"""Git version control for scripts and outputs (requires git).
|
|
175
|
+
|
|
176
|
+
Runs git commands in the current working directory.
|
|
177
|
+
Useful for versioning analysis scripts.
|
|
178
|
+
|
|
179
|
+
Sub-commands:
|
|
180
|
+
git init — initialize a git repository
|
|
181
|
+
git status — show working tree status
|
|
182
|
+
git add <file> [file2 ...] — stage files
|
|
183
|
+
git commit "<message>" — commit staged changes
|
|
184
|
+
git log [--n=10] — show recent commits
|
|
185
|
+
git diff [<file>] — show unstaged changes
|
|
186
|
+
git tag <name> — create a tag for this analysis version
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
git init
|
|
190
|
+
git add analysis.ost outputs/results.pdf
|
|
191
|
+
git commit "Add OLS regression results"
|
|
192
|
+
git log --n=5
|
|
193
|
+
"""
|
|
194
|
+
tokens = args.strip().split(None, 1)
|
|
195
|
+
if not tokens:
|
|
196
|
+
return "Usage: git init|status|add|commit|log|diff [args]"
|
|
197
|
+
|
|
198
|
+
subcmd = tokens[0].lower()
|
|
199
|
+
rest = tokens[1].strip() if len(tokens) > 1 else ""
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
import subprocess
|
|
203
|
+
|
|
204
|
+
def _run(cmd_parts):
|
|
205
|
+
result = subprocess.run(
|
|
206
|
+
cmd_parts, capture_output=True, text=True, cwd=os.getcwd()
|
|
207
|
+
)
|
|
208
|
+
out = (result.stdout + result.stderr).strip()
|
|
209
|
+
return out or "(no output)"
|
|
210
|
+
|
|
211
|
+
if subcmd == "init":
|
|
212
|
+
return _run(["git", "init"])
|
|
213
|
+
elif subcmd == "status":
|
|
214
|
+
return _run(["git", "status", "--short"])
|
|
215
|
+
elif subcmd == "add":
|
|
216
|
+
files = rest.split() if rest else ["."]
|
|
217
|
+
return _run(["git", "add"] + files)
|
|
218
|
+
elif subcmd == "commit":
|
|
219
|
+
msg = rest.strip().strip('"\'') or "OpenStat analysis update"
|
|
220
|
+
return _run(["git", "commit", "-m", msg])
|
|
221
|
+
elif subcmd == "log":
|
|
222
|
+
n = 10
|
|
223
|
+
if "--n=" in rest:
|
|
224
|
+
try:
|
|
225
|
+
n = int(rest.split("--n=")[1].split()[0])
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
return _run(["git", "log", f"--oneline", f"-{n}"])
|
|
229
|
+
elif subcmd == "diff":
|
|
230
|
+
file_arg = [rest] if rest else []
|
|
231
|
+
return _run(["git", "diff"] + file_arg)
|
|
232
|
+
elif subcmd == "tag":
|
|
233
|
+
tag = rest.strip().strip('"\'')
|
|
234
|
+
if not tag:
|
|
235
|
+
return "Usage: git tag <name>"
|
|
236
|
+
return _run(["git", "tag", tag])
|
|
237
|
+
else:
|
|
238
|
+
# Pass through to git directly
|
|
239
|
+
return _run(["git", subcmd] + (rest.split() if rest else []))
|
|
240
|
+
except FileNotFoundError:
|
|
241
|
+
return "git not found. Install git: https://git-scm.com/"
|
|
242
|
+
except Exception as e:
|
|
243
|
+
return friendly_error(e, "git")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ── Config profiles ───────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
@command("profile config", usage="profile config save|load|list|rm <name>")
|
|
249
|
+
def cmd_config_profile(session: Session, args: str) -> str:
|
|
250
|
+
"""Save and load configuration profiles.
|
|
251
|
+
|
|
252
|
+
A profile saves the current config (output dir, plot size, etc.)
|
|
253
|
+
with a name so you can switch between setups.
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
profile config save presentation
|
|
257
|
+
profile config save publication
|
|
258
|
+
profile config load presentation
|
|
259
|
+
profile config list
|
|
260
|
+
profile config rm presentation
|
|
261
|
+
"""
|
|
262
|
+
import json
|
|
263
|
+
from openstat.config import get_config
|
|
264
|
+
|
|
265
|
+
PROFILE_DIR = Path.home() / ".openstat" / "profiles"
|
|
266
|
+
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
tokens = args.strip().split()
|
|
269
|
+
if not tokens:
|
|
270
|
+
return "Usage: profile config save|load|list|rm <name>"
|
|
271
|
+
|
|
272
|
+
subcmd = tokens[0].lower()
|
|
273
|
+
name = tokens[1] if len(tokens) > 1 else ""
|
|
274
|
+
|
|
275
|
+
if subcmd == "list":
|
|
276
|
+
profiles = list(PROFILE_DIR.glob("*.json"))
|
|
277
|
+
if not profiles:
|
|
278
|
+
return "No saved profiles. Use: profile config save <name>"
|
|
279
|
+
lines = ["Saved config profiles:"]
|
|
280
|
+
for p in profiles:
|
|
281
|
+
lines.append(f" {p.stem}")
|
|
282
|
+
return "\n".join(lines)
|
|
283
|
+
|
|
284
|
+
if not name:
|
|
285
|
+
return f"Usage: profile config {subcmd} <name>"
|
|
286
|
+
|
|
287
|
+
profile_path = PROFILE_DIR / f"{name}.json"
|
|
288
|
+
|
|
289
|
+
if subcmd == "save":
|
|
290
|
+
cfg = get_config()
|
|
291
|
+
data = {
|
|
292
|
+
"output_dir": str(cfg.output_dir),
|
|
293
|
+
"plot_figsize_w": cfg.plot_figsize_w,
|
|
294
|
+
"plot_figsize_h": cfg.plot_figsize_h,
|
|
295
|
+
"plot_dpi": cfg.plot_dpi,
|
|
296
|
+
"csv_separator": cfg.csv_separator,
|
|
297
|
+
"max_undo_stack": cfg.max_undo_stack,
|
|
298
|
+
}
|
|
299
|
+
profile_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
300
|
+
return f"Profile '{name}' saved to {profile_path}"
|
|
301
|
+
|
|
302
|
+
elif subcmd == "load":
|
|
303
|
+
if not profile_path.exists():
|
|
304
|
+
return f"Profile '{name}' not found. Use 'profile config list' to see available."
|
|
305
|
+
data = json.loads(profile_path.read_text(encoding="utf-8"))
|
|
306
|
+
cfg = get_config()
|
|
307
|
+
for k, v in data.items():
|
|
308
|
+
if hasattr(cfg, k):
|
|
309
|
+
setattr(cfg, k, v)
|
|
310
|
+
return f"Profile '{name}' loaded: {data}"
|
|
311
|
+
|
|
312
|
+
elif subcmd == "rm":
|
|
313
|
+
if not profile_path.exists():
|
|
314
|
+
return f"Profile '{name}' not found."
|
|
315
|
+
profile_path.unlink()
|
|
316
|
+
return f"Profile '{name}' removed."
|
|
317
|
+
|
|
318
|
+
else:
|
|
319
|
+
return f"Unknown sub-command: {subcmd}. Use save, load, list, or rm."
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Plot commands: hist, scatter, line, box, bar, heatmap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openstat.session import Session
|
|
6
|
+
from openstat.plots.plotter import (
|
|
7
|
+
plot_histogram, plot_scatter, plot_line, plot_box, plot_bar, plot_heatmap,
|
|
8
|
+
plot_residuals_vs_fitted, plot_qq, plot_scale_location, plot_coef,
|
|
9
|
+
plot_interaction,
|
|
10
|
+
)
|
|
11
|
+
from openstat.commands.base import command, CommandArgs, friendly_error
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@command("plot", usage="plot hist|scatter|line|box|bar|heatmap|coef|interaction ...")
|
|
15
|
+
def cmd_plot(session: Session, args: str) -> str:
|
|
16
|
+
"""Create plots: hist, scatter, line, box, bar, heatmap, coef, interaction."""
|
|
17
|
+
df = session.require_data()
|
|
18
|
+
ca = CommandArgs(args)
|
|
19
|
+
if not ca.positional:
|
|
20
|
+
return (
|
|
21
|
+
"Usage:\n"
|
|
22
|
+
" plot hist <col>\n"
|
|
23
|
+
" plot scatter <y> <x>\n"
|
|
24
|
+
" plot line <y> <x>\n"
|
|
25
|
+
" plot box <col> [by <group_col>]\n"
|
|
26
|
+
" plot bar <col> [by <group_col>]\n"
|
|
27
|
+
" plot heatmap [col1 col2 ...]\n"
|
|
28
|
+
" plot coef (coefficient plot after model)\n"
|
|
29
|
+
" plot margins (marginal effects plot after margins)\n"
|
|
30
|
+
" plot interaction <y> <x> <mod> (interaction plot, ±1SD split)\n"
|
|
31
|
+
" plot diagnostics (diagnostic plots after model)"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
subcmd = ca.positional[0]
|
|
35
|
+
try:
|
|
36
|
+
if subcmd == "hist":
|
|
37
|
+
if len(ca.positional) < 2:
|
|
38
|
+
return "Usage: plot hist <col>"
|
|
39
|
+
col = ca.positional[1]
|
|
40
|
+
path = plot_histogram(df, col, session.output_dir)
|
|
41
|
+
session.plot_paths.append(str(path))
|
|
42
|
+
return f"Histogram saved: {path}"
|
|
43
|
+
|
|
44
|
+
elif subcmd == "scatter":
|
|
45
|
+
if len(ca.positional) < 3:
|
|
46
|
+
return "Usage: plot scatter <y_col> <x_col>"
|
|
47
|
+
y_col, x_col = ca.positional[1], ca.positional[2]
|
|
48
|
+
path = plot_scatter(df, y_col, x_col, session.output_dir)
|
|
49
|
+
session.plot_paths.append(str(path))
|
|
50
|
+
return f"Scatter plot saved: {path}"
|
|
51
|
+
|
|
52
|
+
elif subcmd == "line":
|
|
53
|
+
if len(ca.positional) < 3:
|
|
54
|
+
return "Usage: plot line <y_col> <x_col>"
|
|
55
|
+
y_col, x_col = ca.positional[1], ca.positional[2]
|
|
56
|
+
path = plot_line(df, y_col, x_col, session.output_dir)
|
|
57
|
+
session.plot_paths.append(str(path))
|
|
58
|
+
return f"Line plot saved: {path}"
|
|
59
|
+
|
|
60
|
+
elif subcmd == "box":
|
|
61
|
+
if len(ca.positional) < 2:
|
|
62
|
+
return "Usage: plot box <col> [by <group_col>]"
|
|
63
|
+
col = ca.positional[1]
|
|
64
|
+
by_rest = ca.rest_after("by")
|
|
65
|
+
group_col = by_rest.split()[0] if by_rest else None
|
|
66
|
+
path = plot_box(df, col, session.output_dir, group_col=group_col)
|
|
67
|
+
session.plot_paths.append(str(path))
|
|
68
|
+
return f"Box plot saved: {path}"
|
|
69
|
+
|
|
70
|
+
elif subcmd == "bar":
|
|
71
|
+
if len(ca.positional) < 2:
|
|
72
|
+
return "Usage: plot bar <col> [by <group_col>]"
|
|
73
|
+
col = ca.positional[1]
|
|
74
|
+
by_rest = ca.rest_after("by")
|
|
75
|
+
group_col = by_rest.split()[0] if by_rest else None
|
|
76
|
+
path = plot_bar(df, col, session.output_dir, group_col=group_col)
|
|
77
|
+
session.plot_paths.append(str(path))
|
|
78
|
+
return f"Bar chart saved: {path}"
|
|
79
|
+
|
|
80
|
+
elif subcmd == "heatmap":
|
|
81
|
+
cols = ca.positional[1:] if len(ca.positional) > 1 else None
|
|
82
|
+
path = plot_heatmap(df, cols, session.output_dir)
|
|
83
|
+
session.plot_paths.append(str(path))
|
|
84
|
+
return f"Heatmap saved: {path}"
|
|
85
|
+
|
|
86
|
+
elif subcmd == "acf":
|
|
87
|
+
if len(ca.positional) < 2:
|
|
88
|
+
return "Usage: plot acf <col>"
|
|
89
|
+
col = ca.positional[1]
|
|
90
|
+
from openstat.plots.ts_plots import plot_acf
|
|
91
|
+
series = df[col].drop_nulls().to_numpy()
|
|
92
|
+
path = plot_acf(series, col, session.output_dir)
|
|
93
|
+
session.plot_paths.append(str(path))
|
|
94
|
+
return f"ACF plot saved: {path}"
|
|
95
|
+
|
|
96
|
+
elif subcmd == "pacf":
|
|
97
|
+
if len(ca.positional) < 2:
|
|
98
|
+
return "Usage: plot pacf <col>"
|
|
99
|
+
col = ca.positional[1]
|
|
100
|
+
from openstat.plots.ts_plots import plot_pacf
|
|
101
|
+
series = df[col].drop_nulls().to_numpy()
|
|
102
|
+
path = plot_pacf(series, col, session.output_dir)
|
|
103
|
+
session.plot_paths.append(str(path))
|
|
104
|
+
return f"PACF plot saved: {path}"
|
|
105
|
+
|
|
106
|
+
elif subcmd == "coef":
|
|
107
|
+
# session._last_model is the raw statsmodels result object
|
|
108
|
+
raw = session._last_model
|
|
109
|
+
if raw is None:
|
|
110
|
+
return "No model fitted yet. Run ols/logit/etc. first, then plot coef."
|
|
111
|
+
if not hasattr(raw, "params"):
|
|
112
|
+
return "Current model does not support coefficient plots."
|
|
113
|
+
import numpy as np
|
|
114
|
+
# Get parameter names from model or FitResult
|
|
115
|
+
fit_result = session._last_fit_result
|
|
116
|
+
if fit_result is not None and hasattr(fit_result, "params") and isinstance(fit_result.params, dict):
|
|
117
|
+
names = list(fit_result.params.keys())
|
|
118
|
+
coef_vals = [fit_result.params[n] for n in names]
|
|
119
|
+
se_vals = [fit_result.std_errors.get(n, 0.0) for n in names]
|
|
120
|
+
params = dict(zip(names, coef_vals))
|
|
121
|
+
ci_lower = {n: v - 1.96 * se for n, v, se in zip(names, coef_vals, se_vals)}
|
|
122
|
+
ci_upper = {n: v + 1.96 * se for n, v, se in zip(names, coef_vals, se_vals)}
|
|
123
|
+
else:
|
|
124
|
+
# Fallback: use exog_names from the statsmodels model
|
|
125
|
+
names = list(getattr(getattr(raw, "model", None), "exog_names", None) or [])
|
|
126
|
+
coef_arr = np.atleast_1d(raw.params)
|
|
127
|
+
if not names:
|
|
128
|
+
names = [f"x{i}" for i in range(len(coef_arr))]
|
|
129
|
+
params = dict(zip(names, coef_arr.tolist()))
|
|
130
|
+
try:
|
|
131
|
+
ci_arr = np.atleast_2d(raw.conf_int())
|
|
132
|
+
ci_lower = dict(zip(names, ci_arr[:, 0].tolist()))
|
|
133
|
+
ci_upper = dict(zip(names, ci_arr[:, 1].tolist()))
|
|
134
|
+
except Exception:
|
|
135
|
+
se_arr = np.atleast_1d(raw.bse)
|
|
136
|
+
ci_lower = {n: v - 1.96 * se for n, v, se in zip(names, coef_arr, se_arr)}
|
|
137
|
+
ci_upper = {n: v + 1.96 * se for n, v, se in zip(names, coef_arr, se_arr)}
|
|
138
|
+
model_cls = type(getattr(raw, "model", raw)).__name__
|
|
139
|
+
title = f"Coefficient Plot ({model_cls})"
|
|
140
|
+
path = plot_coef(params, ci_lower, ci_upper, session.output_dir, title=title)
|
|
141
|
+
session.plot_paths.append(str(path))
|
|
142
|
+
return f"Coefficient plot saved: {path}"
|
|
143
|
+
|
|
144
|
+
elif subcmd == "margins":
|
|
145
|
+
mg = session._last_margins
|
|
146
|
+
if mg is None:
|
|
147
|
+
return "No margins result. Run 'margins' after logit/probit first."
|
|
148
|
+
if not hasattr(mg, "effects"):
|
|
149
|
+
return "Stored margins result has no 'effects' attribute."
|
|
150
|
+
params = dict(mg.effects)
|
|
151
|
+
ci_lower = dict(mg.conf_int_low)
|
|
152
|
+
ci_upper = dict(mg.conf_int_high)
|
|
153
|
+
path = plot_coef(
|
|
154
|
+
params, ci_lower, ci_upper, session.output_dir,
|
|
155
|
+
title=f"Marginal Effects ({mg.method})",
|
|
156
|
+
drop_const=False,
|
|
157
|
+
)
|
|
158
|
+
session.plot_paths.append(str(path))
|
|
159
|
+
return f"Marginal effects plot saved: {path}"
|
|
160
|
+
|
|
161
|
+
elif subcmd == "interaction":
|
|
162
|
+
# plot interaction <y> <x> <moderator> [--levels=N]
|
|
163
|
+
if len(ca.positional) < 4:
|
|
164
|
+
return "Usage: plot interaction <y> <x> <moderator> [--levels=3]"
|
|
165
|
+
y_col = ca.positional[1]
|
|
166
|
+
x_col = ca.positional[2]
|
|
167
|
+
mod_col = ca.positional[3]
|
|
168
|
+
n_levels = int(ca.options.get("levels", 3))
|
|
169
|
+
path = plot_interaction(df, y_col, x_col, mod_col, session.output_dir, n_levels=n_levels)
|
|
170
|
+
session.plot_paths.append(str(path))
|
|
171
|
+
return f"Interaction plot saved: {path}"
|
|
172
|
+
|
|
173
|
+
elif subcmd == "diagnostics":
|
|
174
|
+
if session._last_model is None or session._last_model_vars is None:
|
|
175
|
+
return "No model fitted yet. Run ols first, then plot diagnostics."
|
|
176
|
+
from openstat.stats.models import compute_residuals
|
|
177
|
+
dep, indeps = session._last_model_vars
|
|
178
|
+
diag = compute_residuals(session._last_model, df, dep, indeps)
|
|
179
|
+
paths = []
|
|
180
|
+
paths.append(plot_residuals_vs_fitted(diag["fitted"], diag["residuals"], session.output_dir))
|
|
181
|
+
paths.append(plot_qq(diag["std_residuals"], session.output_dir))
|
|
182
|
+
paths.append(plot_scale_location(diag["fitted"], diag["std_residuals"], session.output_dir))
|
|
183
|
+
session.plot_paths.extend(str(p) for p in paths)
|
|
184
|
+
return "Diagnostic plots saved:\n" + "\n".join(f" {p}" for p in paths)
|
|
185
|
+
|
|
186
|
+
else:
|
|
187
|
+
return f"Unknown plot type: {subcmd}. Available: hist, scatter, line, box, bar, heatmap, coef, margins, interaction, diagnostics"
|
|
188
|
+
except Exception as e:
|
|
189
|
+
return friendly_error(e, "Plot error")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Plugin management commands: plugin list, plugin info."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from openstat.session import Session
|
|
9
|
+
from openstat.commands.base import command, rich_to_str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Global plugin manager — initialized at REPL startup
|
|
13
|
+
_manager = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_plugin_manager():
|
|
17
|
+
global _manager
|
|
18
|
+
if _manager is None:
|
|
19
|
+
from openstat.plugins.manager import PluginManager
|
|
20
|
+
_manager = PluginManager()
|
|
21
|
+
return _manager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def init_plugins() -> list[str]:
|
|
25
|
+
"""Discover and load all plugins. Called at REPL startup."""
|
|
26
|
+
mgr = get_plugin_manager()
|
|
27
|
+
return mgr.discover()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@command("plugin", usage="plugin list | plugin info <name>")
|
|
31
|
+
def cmd_plugin(session: Session, args: str) -> str:
|
|
32
|
+
"""Manage plugins: list installed plugins or show plugin details."""
|
|
33
|
+
mgr = get_plugin_manager()
|
|
34
|
+
parts = args.strip().split(None, 1)
|
|
35
|
+
subcmd = parts[0].lower() if parts else ""
|
|
36
|
+
|
|
37
|
+
if subcmd == "list":
|
|
38
|
+
plugins = mgr.list_plugins()
|
|
39
|
+
if not plugins:
|
|
40
|
+
return "No plugins installed. Install plugins with: pip install <plugin-package>"
|
|
41
|
+
|
|
42
|
+
def render(console: Console) -> None:
|
|
43
|
+
table = Table(title="Installed Plugins")
|
|
44
|
+
table.add_column("Name", style="cyan")
|
|
45
|
+
table.add_column("Version", justify="right")
|
|
46
|
+
table.add_column("Description")
|
|
47
|
+
table.add_column("Commands", style="green")
|
|
48
|
+
for p in plugins:
|
|
49
|
+
table.add_row(
|
|
50
|
+
p.name, p.version, p.description,
|
|
51
|
+
", ".join(p.commands) if p.commands else "—",
|
|
52
|
+
)
|
|
53
|
+
console.print(table)
|
|
54
|
+
|
|
55
|
+
errors = mgr.errors
|
|
56
|
+
result = rich_to_str(render)
|
|
57
|
+
if errors:
|
|
58
|
+
result += "\n\nFailed to load:"
|
|
59
|
+
for name, err in errors.items():
|
|
60
|
+
result += f"\n {name}: {err}"
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
elif subcmd == "info":
|
|
64
|
+
name = parts[1].strip() if len(parts) > 1 else ""
|
|
65
|
+
if not name:
|
|
66
|
+
return "Usage: plugin info <name>"
|
|
67
|
+
info = mgr.get_info(name)
|
|
68
|
+
if info is None:
|
|
69
|
+
return f"Plugin '{name}' not found. Use 'plugin list' to see installed plugins."
|
|
70
|
+
lines = [
|
|
71
|
+
f"Plugin: {info.name}",
|
|
72
|
+
f"Version: {info.version}",
|
|
73
|
+
f"Description: {info.description or '(none)'}",
|
|
74
|
+
f"Commands: {', '.join(info.commands) if info.commands else '(none)'}",
|
|
75
|
+
]
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
return "Usage: plugin list | plugin info <name>"
|