learnstat 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {learnstat-0.2.0 → learnstat-0.2.1}/PKG-INFO +29 -1
- {learnstat-0.2.0 → learnstat-0.2.1}/README.md +28 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/__init__.py +1 -1
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/cli.py +7 -14
- learnstat-0.2.1/learnstat/guide.py +266 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/PKG-INFO +29 -1
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/SOURCES.txt +1 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/pyproject.toml +1 -1
- {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_ai_config.py +63 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/LICENSE +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/advisor.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/commands.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/config.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/ipython_ext.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/knowledge.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/llm.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/memory.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/profile.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/providers.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/recovery.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/safety.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/session.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/setup.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat/suggestions.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/dependency_links.txt +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/entry_points.txt +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/requires.txt +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/learnstat.egg-info/top_level.txt +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/setup.cfg +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_advisor.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_commands.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_ipython.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_profile_memory.py +0 -0
- {learnstat-0.2.0 → learnstat-0.2.1}/tests/test_recovery.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learnstat
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An AI-assisted learning companion for statistical analysis with pandas, NumPy, SciPy and Matplotlib.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/umorfaruksupto/leanstat
|
|
@@ -54,6 +54,33 @@ pip install -e ".[all]" # pandas, numpy, matplotlib, scipy, ipython
|
|
|
54
54
|
pip install -e . # core only: profiling, rules, recovery
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### Quick start (no commands to learn)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install learnstat
|
|
61
|
+
learnstat
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That is all. learnstat walks you through everything in plain English:
|
|
65
|
+
|
|
66
|
+
1. **Pick your AI** (optional): choose the company, paste your key, choose a model. learnstat tests it and says OK or what is wrong.
|
|
67
|
+
2. **Choose your data**: press Enter and a normal file window opens (CSV or Excel), or drag the file into the terminal.
|
|
68
|
+
3. **Pick what to do from a menu** by typing a number:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
1) See a summary of my data
|
|
72
|
+
2) Ask the AI a question about my data
|
|
73
|
+
3) Get suggestions for a Python script
|
|
74
|
+
4) Run a Python script with automatic help
|
|
75
|
+
5) Write Python code here (my data is ready as "df")
|
|
76
|
+
6) Learn a statistics topic
|
|
77
|
+
7) Load a different data file
|
|
78
|
+
8) Change the AI (company, key, model)
|
|
79
|
+
9) Help
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Nothing else needs to be memorised: every option asks for what it needs, and option 9 explains the menu. Skip the AI and everything still works with built-in rules.
|
|
83
|
+
|
|
57
84
|
### Add an AI (optional)
|
|
58
85
|
|
|
59
86
|
Just run `learnstat`. If no AI is set up yet it offers a guided setup, and you can repeat it any time with `learnstat setup` (or `/ai` inside a session):
|
|
@@ -177,6 +204,7 @@ learnstat/
|
|
|
177
204
|
config.py saved AI accounts: provider, api_key, base_url, model
|
|
178
205
|
providers.py presets for the AI companies
|
|
179
206
|
setup.py guided terminal setup (company -> key -> model -> check)
|
|
207
|
+
guide.py the numbered home menu students use
|
|
180
208
|
session.py ties it together; read_csv/track/check/run
|
|
181
209
|
commands.py slash commands
|
|
182
210
|
ipython_ext.py %load_ext learnstat
|
|
@@ -24,6 +24,33 @@ pip install -e ".[all]" # pandas, numpy, matplotlib, scipy, ipython
|
|
|
24
24
|
pip install -e . # core only: profiling, rules, recovery
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
### Quick start (no commands to learn)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install learnstat
|
|
31
|
+
learnstat
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That is all. learnstat walks you through everything in plain English:
|
|
35
|
+
|
|
36
|
+
1. **Pick your AI** (optional): choose the company, paste your key, choose a model. learnstat tests it and says OK or what is wrong.
|
|
37
|
+
2. **Choose your data**: press Enter and a normal file window opens (CSV or Excel), or drag the file into the terminal.
|
|
38
|
+
3. **Pick what to do from a menu** by typing a number:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
1) See a summary of my data
|
|
42
|
+
2) Ask the AI a question about my data
|
|
43
|
+
3) Get suggestions for a Python script
|
|
44
|
+
4) Run a Python script with automatic help
|
|
45
|
+
5) Write Python code here (my data is ready as "df")
|
|
46
|
+
6) Learn a statistics topic
|
|
47
|
+
7) Load a different data file
|
|
48
|
+
8) Change the AI (company, key, model)
|
|
49
|
+
9) Help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Nothing else needs to be memorised: every option asks for what it needs, and option 9 explains the menu. Skip the AI and everything still works with built-in rules.
|
|
53
|
+
|
|
27
54
|
### Add an AI (optional)
|
|
28
55
|
|
|
29
56
|
Just run `learnstat`. If no AI is set up yet it offers a guided setup, and you can repeat it any time with `learnstat setup` (or `/ai` inside a session):
|
|
@@ -147,6 +174,7 @@ learnstat/
|
|
|
147
174
|
config.py saved AI accounts: provider, api_key, base_url, model
|
|
148
175
|
providers.py presets for the AI companies
|
|
149
176
|
setup.py guided terminal setup (company -> key -> model -> check)
|
|
177
|
+
guide.py the numbered home menu students use
|
|
150
178
|
session.py ties it together; read_csv/track/check/run
|
|
151
179
|
commands.py slash commands
|
|
152
180
|
ipython_ext.py %load_ext learnstat
|
|
@@ -118,8 +118,9 @@ def _repl(s: Session, data: str | None) -> int:
|
|
|
118
118
|
ready = ["pd", "np"]
|
|
119
119
|
if data:
|
|
120
120
|
s.ns["df"] = s.read_csv(data)
|
|
121
|
+
if "df" in s.ns:
|
|
121
122
|
ready.insert(0, "df")
|
|
122
|
-
print(f"learnstat REPL - AI: {s.ai_status}. Ready: {', '.join(ready)}. Type /help for commands, /ai to change the AI, Ctrl+Z/Ctrl+D to exit.")
|
|
123
|
+
print(f"learnstat REPL - AI: {s.ai_status}. Ready: {', '.join(ready)}. Type /help for commands, /ai to change the AI, 'menu' to go back, Ctrl+Z/Ctrl+D to exit.")
|
|
123
124
|
buffer: list[str] = []
|
|
124
125
|
while True:
|
|
125
126
|
try:
|
|
@@ -127,6 +128,8 @@ def _repl(s: Session, data: str | None) -> int:
|
|
|
127
128
|
except (EOFError, KeyboardInterrupt):
|
|
128
129
|
print()
|
|
129
130
|
return 0
|
|
131
|
+
if not buffer and line.strip().lower() in ("menu", "back", "exit", "quit", "exit()", "quit()"):
|
|
132
|
+
return 0
|
|
130
133
|
if not buffer and is_command(line):
|
|
131
134
|
print(s.command(line))
|
|
132
135
|
continue
|
|
@@ -205,18 +208,6 @@ def cmd_config(args) -> int:
|
|
|
205
208
|
return 0
|
|
206
209
|
|
|
207
210
|
|
|
208
|
-
def _ask_data() -> str | None:
|
|
209
|
-
try:
|
|
210
|
-
print()
|
|
211
|
-
path = input("Dataset to load (CSV/Excel path, Enter to skip): ").strip().strip('"')
|
|
212
|
-
except (EOFError, KeyboardInterrupt):
|
|
213
|
-
return None
|
|
214
|
-
if path and not Path(path).exists():
|
|
215
|
-
print(f" '{path}' was not found - starting without a dataset.")
|
|
216
|
-
return None
|
|
217
|
-
return path or None
|
|
218
|
-
|
|
219
|
-
|
|
220
211
|
def _start(args, run_setup: bool) -> int:
|
|
221
212
|
"""The guided start: (optional) AI setup, a quick AI check, then the interactive session."""
|
|
222
213
|
from . import config
|
|
@@ -251,8 +242,10 @@ def _start(args, run_setup: bool) -> int:
|
|
|
251
242
|
print()
|
|
252
243
|
print(f"X {a.last_error}")
|
|
253
244
|
print(" Continuing with offline rules. Type /ai in the session to fix it.")
|
|
245
|
+
from .guide import run_guide
|
|
246
|
+
|
|
254
247
|
s = _session(args)
|
|
255
|
-
return _repl(s, args.data
|
|
248
|
+
return run_guide(s, repl=lambda: _repl(s, None), data=args.data)
|
|
256
249
|
|
|
257
250
|
|
|
258
251
|
def cmd_setup(args) -> int:
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""The guided home screen: students pick numbers from a menu instead of learning commands.
|
|
2
|
+
|
|
3
|
+
After the AI is set up, ``learnstat`` shows this menu. Every action asks for what it
|
|
4
|
+
needs in plain English (pick a file in a window, type a question) and prints the result.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
from .session import Session
|
|
14
|
+
from .setup import Cancelled, IO, run_wizard
|
|
15
|
+
|
|
16
|
+
DATA_TYPES = [("Data files", "*.csv *.tsv *.txt *.xlsx *.xls *.json *.parquet"), ("All files", "*.*")]
|
|
17
|
+
SCRIPT_TYPES = [("Python files", "*.py"), ("All files", "*.*")]
|
|
18
|
+
|
|
19
|
+
MENU = """
|
|
20
|
+
What would you like to do?
|
|
21
|
+
|
|
22
|
+
1) See a summary of my data
|
|
23
|
+
2) Ask the AI a question about my data
|
|
24
|
+
3) Get suggestions for a Python script
|
|
25
|
+
4) Run a Python script with automatic help
|
|
26
|
+
5) Write Python code here (my data is ready as "df")
|
|
27
|
+
6) Learn a statistics topic
|
|
28
|
+
7) Load a different data file
|
|
29
|
+
8) Change the AI (company, key, model)
|
|
30
|
+
9) Help
|
|
31
|
+
q) Quit"""
|
|
32
|
+
|
|
33
|
+
HELP_TEXT = """
|
|
34
|
+
How learnstat works
|
|
35
|
+
-------------------
|
|
36
|
+
1 Shows what is in your data: columns, types, missing values and possible problems.
|
|
37
|
+
2 Type any question in your own words, for example "which column has the most missing values?".
|
|
38
|
+
3 Pick a Python file. learnstat reads it (without running it) and suggests better statistics
|
|
39
|
+
or charts for your data. Enter a suggestion's number to get it explained.
|
|
40
|
+
4 Runs your Python file. If it crashes on a simple mistake (like a misspelled column) learnstat
|
|
41
|
+
explains it and can fix it for you.
|
|
42
|
+
5 A Python prompt with your data already loaded as df. Type "menu" to come back here.
|
|
43
|
+
Inside it, lines starting with / are shortcuts, and /help lists them.
|
|
44
|
+
6 Short lessons, for example "t-test", "correlation" or "missing values".
|
|
45
|
+
7 Load another file. 8 Switch to another AI company or key.
|
|
46
|
+
|
|
47
|
+
The AI is optional: without it learnstat still gives rule-based suggestions and explanations."""
|
|
48
|
+
|
|
49
|
+
PYTHON_TIPS = (
|
|
50
|
+
'Python prompt - your data is "df". Try: df.head() or df.describe()\n'
|
|
51
|
+
' Type /help for shortcuts, "menu" to go back to the menu.'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pick_file(title: str, types: list[tuple[str, str]]) -> str | None:
|
|
56
|
+
"""Open the system file window. Returns None when cancelled or when no window is available."""
|
|
57
|
+
try:
|
|
58
|
+
import tkinter
|
|
59
|
+
from tkinter import filedialog
|
|
60
|
+
|
|
61
|
+
root = tkinter.Tk()
|
|
62
|
+
root.withdraw()
|
|
63
|
+
root.attributes("-topmost", True)
|
|
64
|
+
try:
|
|
65
|
+
path = filedialog.askopenfilename(title=title, filetypes=types)
|
|
66
|
+
finally:
|
|
67
|
+
root.destroy()
|
|
68
|
+
return path or None
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _clean(path: str) -> str:
|
|
74
|
+
return path.strip().strip("\"'")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def ask_file(io: IO, pick: Callable, what: str, types: list[tuple[str, str]]) -> Path | None:
|
|
78
|
+
answer = io.line(f"\nChoose your {what}. Press Enter to browse, or paste / drag the file here (b = back): ")
|
|
79
|
+
if answer.lower() in ("b", "back"):
|
|
80
|
+
return None
|
|
81
|
+
if not answer:
|
|
82
|
+
chosen = pick(f"Choose your {what}", types)
|
|
83
|
+
if not chosen:
|
|
84
|
+
io.say("No file chosen. You can also paste the full path to the file.")
|
|
85
|
+
return None
|
|
86
|
+
answer = chosen
|
|
87
|
+
path = Path(_clean(answer)).expanduser()
|
|
88
|
+
if not path.is_file():
|
|
89
|
+
io.say(f"I can't find '{path}'. Check the name, or press Enter next time to browse for it.")
|
|
90
|
+
return None
|
|
91
|
+
return path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_dataset(s: Session, io: IO, path: Path):
|
|
95
|
+
suffix = path.suffix.lower()
|
|
96
|
+
try:
|
|
97
|
+
if suffix in (".xlsx", ".xls"):
|
|
98
|
+
df = s.read_excel(path)
|
|
99
|
+
elif suffix == ".json":
|
|
100
|
+
df = s.read_json(path)
|
|
101
|
+
elif suffix == ".parquet":
|
|
102
|
+
df = s.read_parquet(path)
|
|
103
|
+
elif suffix == ".tsv":
|
|
104
|
+
df = s.read_csv(path, sep="\t")
|
|
105
|
+
else:
|
|
106
|
+
df = s.read_csv(path)
|
|
107
|
+
except ImportError as e:
|
|
108
|
+
io.say(f"Reading {suffix} files needs an extra package: {e}")
|
|
109
|
+
return None
|
|
110
|
+
except Exception as e:
|
|
111
|
+
io.say(f"I couldn't read that file as data ({type(e).__name__}: {e}).")
|
|
112
|
+
return None
|
|
113
|
+
s.ns["df"] = df
|
|
114
|
+
io.say('\nYour data is ready, and is available as "df" when you write Python code.')
|
|
115
|
+
return df
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# menu actions
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _summary(s: Session, io: IO) -> None:
|
|
123
|
+
if not s.datasets:
|
|
124
|
+
io.say("No data is loaded yet - choose 7 to load a file.")
|
|
125
|
+
return
|
|
126
|
+
io.say("\n" + s.profile_markdown())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _ask_ai(s: Session, io: IO) -> None:
|
|
130
|
+
if not s.assistant:
|
|
131
|
+
io.say(f"The AI is {s.ai_status}. Choose 8 to set one up - or use 6 for built-in lessons.")
|
|
132
|
+
return
|
|
133
|
+
io.say('\nAsk anything about your data. Example: "which columns have missing values?"')
|
|
134
|
+
while True:
|
|
135
|
+
question = io.line("\nYour question (Enter to go back): ")
|
|
136
|
+
if not question:
|
|
137
|
+
return
|
|
138
|
+
io.say("\nThinking ...")
|
|
139
|
+
io.say(s.command(f"/ask {question}"))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _read_script(io: IO, pick: Callable) -> tuple[Path, str] | None:
|
|
143
|
+
path = ask_file(io, pick, "Python file", SCRIPT_TYPES)
|
|
144
|
+
if path is None:
|
|
145
|
+
return None
|
|
146
|
+
code = path.read_text(encoding="utf-8")
|
|
147
|
+
try:
|
|
148
|
+
compile(code, str(path), "exec")
|
|
149
|
+
except SyntaxError as e:
|
|
150
|
+
io.say(f"There is a syntax error on line {e.lineno}: {e.msg}. Fix it and try again.")
|
|
151
|
+
return None
|
|
152
|
+
return path, code
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _check_script(s: Session, io: IO, pick: Callable) -> None:
|
|
156
|
+
got = _read_script(io, pick)
|
|
157
|
+
if got is None:
|
|
158
|
+
return
|
|
159
|
+
path, code = got
|
|
160
|
+
found = s.check(code, show=False, extra_frames=s.profiles_for_code(code, base_dir=path.parent))
|
|
161
|
+
if not found:
|
|
162
|
+
io.say("\nNo suggestions - the rules found nothing to improve for this data.")
|
|
163
|
+
return
|
|
164
|
+
io.say(f"\n{len(found)} suggestion(s):\n")
|
|
165
|
+
for sug in found:
|
|
166
|
+
io.say(sug.render())
|
|
167
|
+
io.say("")
|
|
168
|
+
while True:
|
|
169
|
+
pick_id = io.line("Enter a suggestion number to have it explained (Enter to go back): ")
|
|
170
|
+
if not pick_id:
|
|
171
|
+
return
|
|
172
|
+
io.say("\n" + s.command(f"/describe {pick_id.lstrip('#')}") + "\n")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _run_script(s: Session, io: IO, pick: Callable) -> None:
|
|
176
|
+
got = _read_script(io, pick)
|
|
177
|
+
if got is None:
|
|
178
|
+
return
|
|
179
|
+
path, code = got
|
|
180
|
+
s.ns.update({"__name__": "__main__", "__file__": str(path.resolve())})
|
|
181
|
+
sys.path.insert(0, str(path.parent.resolve()))
|
|
182
|
+
s.check(code, extra_frames=s.profiles_for_code(code, base_dir=path.parent))
|
|
183
|
+
result = s.run(code, check=False, filename=str(path))
|
|
184
|
+
if result.ok:
|
|
185
|
+
io.say("\nDone - the script finished.")
|
|
186
|
+
if result.changed and io.yes("learnstat corrected something. Save a corrected copy next to your file?"):
|
|
187
|
+
out = path.with_suffix(".fixed.py")
|
|
188
|
+
out.write_text(result.final_code, encoding="utf-8")
|
|
189
|
+
io.say(f"Saved {out} (your original is unchanged).")
|
|
190
|
+
else:
|
|
191
|
+
io.say("\nThe script stopped with an error.")
|
|
192
|
+
if io.yes("Explain what went wrong?"):
|
|
193
|
+
io.say("\n" + s.command("/describe"))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _learn(s: Session, io: IO) -> None:
|
|
197
|
+
io.say('\nExamples: t-test, correlation, missing values, skewed data, pie charts, histograms')
|
|
198
|
+
topic = io.line("Topic (Enter for a lesson about your latest suggestion): ")
|
|
199
|
+
io.say("\n" + s.command(f"/learn {topic}".strip()))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _load(s: Session, io: IO, pick: Callable) -> None:
|
|
203
|
+
path = ask_file(io, pick, "data file (CSV or Excel)", DATA_TYPES)
|
|
204
|
+
if path is not None:
|
|
205
|
+
load_dataset(s, io, path)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _change_ai(s: Session, io: IO) -> None:
|
|
209
|
+
run_wizard(io)
|
|
210
|
+
s.connect_ai()
|
|
211
|
+
io.say(f"\nAI: {s.ai_status}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def run_guide(s: Session, io: IO | None = None, pick: Callable = pick_file,
|
|
218
|
+
repl: Callable[[], object] | None = None, data: str | None = None) -> int:
|
|
219
|
+
"""Show the home menu until the user quits."""
|
|
220
|
+
io = io or IO()
|
|
221
|
+
io.say("\n" + "=" * 60 + "\n learnstat - learn statistics with Python\n" + "=" * 60)
|
|
222
|
+
io.say(f"AI: {s.ai_status}")
|
|
223
|
+
try:
|
|
224
|
+
if data:
|
|
225
|
+
path = Path(_clean(data)).expanduser()
|
|
226
|
+
if path.is_file():
|
|
227
|
+
load_dataset(s, io, path)
|
|
228
|
+
else:
|
|
229
|
+
io.say(f"I can't find '{path}'.")
|
|
230
|
+
elif io.yes("\nLoad your data file now?"):
|
|
231
|
+
try:
|
|
232
|
+
_load(s, io, pick)
|
|
233
|
+
except Cancelled:
|
|
234
|
+
pass
|
|
235
|
+
actions: dict[str, Callable[[], None]] = {
|
|
236
|
+
"1": lambda: _summary(s, io),
|
|
237
|
+
"2": lambda: _ask_ai(s, io),
|
|
238
|
+
"3": lambda: _check_script(s, io, pick),
|
|
239
|
+
"4": lambda: _run_script(s, io, pick),
|
|
240
|
+
"6": lambda: _learn(s, io),
|
|
241
|
+
"7": lambda: _load(s, io, pick),
|
|
242
|
+
"8": lambda: _change_ai(s, io),
|
|
243
|
+
"9": lambda: io.say(HELP_TEXT),
|
|
244
|
+
}
|
|
245
|
+
if repl is not None:
|
|
246
|
+
def python_mode() -> None:
|
|
247
|
+
io.say("\n" + PYTHON_TIPS)
|
|
248
|
+
repl()
|
|
249
|
+
actions["5"] = python_mode
|
|
250
|
+
while True:
|
|
251
|
+
io.say(MENU)
|
|
252
|
+
choice = io.line("\nType a number: ").lower()
|
|
253
|
+
if choice in ("q", "quit", "exit"):
|
|
254
|
+
break
|
|
255
|
+
action = actions.get(choice)
|
|
256
|
+
if action is None:
|
|
257
|
+
io.say("Please type one of the numbers in the menu (or q to quit).")
|
|
258
|
+
continue
|
|
259
|
+
try:
|
|
260
|
+
action()
|
|
261
|
+
except Cancelled:
|
|
262
|
+
io.say("\n(back to the menu)")
|
|
263
|
+
except Cancelled:
|
|
264
|
+
pass
|
|
265
|
+
io.say("\nGoodbye - your dataset profiles are saved in .learnstat/ so next time is faster.")
|
|
266
|
+
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learnstat
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An AI-assisted learning companion for statistical analysis with pandas, NumPy, SciPy and Matplotlib.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/umorfaruksupto/leanstat
|
|
@@ -54,6 +54,33 @@ pip install -e ".[all]" # pandas, numpy, matplotlib, scipy, ipython
|
|
|
54
54
|
pip install -e . # core only: profiling, rules, recovery
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### Quick start (no commands to learn)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install learnstat
|
|
61
|
+
learnstat
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That is all. learnstat walks you through everything in plain English:
|
|
65
|
+
|
|
66
|
+
1. **Pick your AI** (optional): choose the company, paste your key, choose a model. learnstat tests it and says OK or what is wrong.
|
|
67
|
+
2. **Choose your data**: press Enter and a normal file window opens (CSV or Excel), or drag the file into the terminal.
|
|
68
|
+
3. **Pick what to do from a menu** by typing a number:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
1) See a summary of my data
|
|
72
|
+
2) Ask the AI a question about my data
|
|
73
|
+
3) Get suggestions for a Python script
|
|
74
|
+
4) Run a Python script with automatic help
|
|
75
|
+
5) Write Python code here (my data is ready as "df")
|
|
76
|
+
6) Learn a statistics topic
|
|
77
|
+
7) Load a different data file
|
|
78
|
+
8) Change the AI (company, key, model)
|
|
79
|
+
9) Help
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Nothing else needs to be memorised: every option asks for what it needs, and option 9 explains the menu. Skip the AI and everything still works with built-in rules.
|
|
83
|
+
|
|
57
84
|
### Add an AI (optional)
|
|
58
85
|
|
|
59
86
|
Just run `learnstat`. If no AI is set up yet it offers a guided setup, and you can repeat it any time with `learnstat setup` (or `/ai` inside a session):
|
|
@@ -177,6 +204,7 @@ learnstat/
|
|
|
177
204
|
config.py saved AI accounts: provider, api_key, base_url, model
|
|
178
205
|
providers.py presets for the AI companies
|
|
179
206
|
setup.py guided terminal setup (company -> key -> model -> check)
|
|
207
|
+
guide.py the numbered home menu students use
|
|
180
208
|
session.py ties it together; read_csv/track/check/run
|
|
181
209
|
commands.py slash commands
|
|
182
210
|
ipython_ext.py %load_ext learnstat
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "learnstat"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "An AI-assisted learning companion for statistical analysis with pandas, NumPy, SciPy and Matplotlib."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -256,3 +256,66 @@ def test_ai_slash_list(server):
|
|
|
256
256
|
s = Session(ai=False, out=lambda *_: None)
|
|
257
257
|
out = dispatch(s, "/ai list")
|
|
258
258
|
assert "x:" in out and "gpt-a" in out and "k1234567890" not in out
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# guided menu
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _guide_run(tmp_path, answers, files=None, **kw):
|
|
266
|
+
from learnstat.guide import run_guide
|
|
267
|
+
from learnstat.session import Session
|
|
268
|
+
|
|
269
|
+
s = Session(memory_dir=tmp_path / "mem", ai=False, out=lambda *_: None, confirm=lambda q: True)
|
|
270
|
+
script = Script(answers)
|
|
271
|
+
queue = list(files or [])
|
|
272
|
+
pick = lambda title, types: queue.pop(0) if queue else None
|
|
273
|
+
rc = run_guide(s, script.io(), pick=pick, **kw)
|
|
274
|
+
return s, script, rc
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_guide_load_data_with_file_window_and_summary(tmp_path):
|
|
278
|
+
csv = tmp_path / "survey.csv"
|
|
279
|
+
csv.write_text("age,income\n20,100\n30,200\n40,\n", encoding="utf-8")
|
|
280
|
+
# load now? y -> Enter opens the file window -> menu: 1 (summary) -> q
|
|
281
|
+
s, script, rc = _guide_run(tmp_path, ["y", "", "1", "q"], files=[str(csv)])
|
|
282
|
+
assert rc == 0 and "df" in s.ns and list(s.datasets) == ["survey"]
|
|
283
|
+
assert 'available as "df"' in script.text and "survey" in script.text
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_guide_missing_file_and_bad_menu_choice(tmp_path):
|
|
287
|
+
s, script, _ = _guide_run(tmp_path, ["y", str(tmp_path / "nope.csv"), "zzz", "q"])
|
|
288
|
+
assert "I can't find" in script.text and "Please type one of the numbers" in script.text
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_guide_check_script_and_explain(tmp_path):
|
|
292
|
+
csv = tmp_path / "survey.csv"
|
|
293
|
+
csv.write_text("country,income\n" + "\n".join(f"c{i % 12},{i * 10}" for i in range(60)) + "\n", encoding="utf-8")
|
|
294
|
+
py = tmp_path / "analysis.py"
|
|
295
|
+
py.write_text(f"import pandas as pd\ndf = pd.read_csv(r'{csv}')\ndf['country'].value_counts().plot.pie()\n", encoding="utf-8")
|
|
296
|
+
s, script, _ = _guide_run(tmp_path, ["n", "3", str(py), "1", "", "q"])
|
|
297
|
+
assert "suggestion(s)" in script.text and "Pie chart" in script.text
|
|
298
|
+
assert "In your code:" in script.text # the /describe explanation
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_guide_run_script_with_automatic_help(tmp_path):
|
|
302
|
+
py = tmp_path / "ok.py"
|
|
303
|
+
py.write_text("total = 1 + 1\n", encoding="utf-8")
|
|
304
|
+
s, script, _ = _guide_run(tmp_path, ["n", "4", str(py), "q"])
|
|
305
|
+
assert "the script finished" in script.text and s.ns["total"] == 2
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_guide_without_ai_points_to_setup(tmp_path):
|
|
309
|
+
s, script, _ = _guide_run(tmp_path, ["n", "2", "q"])
|
|
310
|
+
assert "Choose 8" in script.text
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_guide_python_mode_returns_to_menu(tmp_path):
|
|
314
|
+
called = []
|
|
315
|
+
s, script, _ = _guide_run(tmp_path, ["n", "5", "q"], repl=lambda: called.append(1))
|
|
316
|
+
assert called == [1] and "Python prompt" in script.text
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_guide_ctrl_c_leaves_cleanly(tmp_path):
|
|
320
|
+
s, script, rc = _guide_run(tmp_path, []) # input ends immediately (EOF)
|
|
321
|
+
assert rc == 0 and "Goodbye" in script.text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|