mekiki 0.2.0__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.
- mekiki-0.2.0/.gitignore +19 -0
- mekiki-0.2.0/LICENSE +21 -0
- mekiki-0.2.0/PKG-INFO +543 -0
- mekiki-0.2.0/README.md +508 -0
- mekiki-0.2.0/mekiki/__init__.py +207 -0
- mekiki-0.2.0/mekiki/adaptive.py +199 -0
- mekiki-0.2.0/mekiki/diagnose.py +1247 -0
- mekiki-0.2.0/mekiki/errors.py +35 -0
- mekiki-0.2.0/mekiki/fallback.py +258 -0
- mekiki-0.2.0/mekiki/feature.py +469 -0
- mekiki-0.2.0/mekiki/jev.py +644 -0
- mekiki-0.2.0/mekiki/knowledge.py +1226 -0
- mekiki-0.2.0/mekiki/leakage.py +186 -0
- mekiki-0.2.0/mekiki/llm.py +657 -0
- mekiki-0.2.0/mekiki/paths.py +116 -0
- mekiki-0.2.0/mekiki/predictor.py +2052 -0
- mekiki-0.2.0/mekiki/preprocess.py +63 -0
- mekiki-0.2.0/mekiki/py.typed +0 -0
- mekiki-0.2.0/mekiki/screening.py +300 -0
- mekiki-0.2.0/mekiki/vectorizers.py +228 -0
- mekiki-0.2.0/pyproject.toml +63 -0
- mekiki-0.2.0/sampledata/sample/bank_sample500.csv +501 -0
- mekiki-0.2.0/sampledata/sample/news_sample500.csv +501 -0
- mekiki-0.2.0/sampledata/sample/vehicles_sample500.csv +501 -0
- mekiki-0.2.0/tests/test_adaptive.py +404 -0
- mekiki-0.2.0/tests/test_diagnose.py +454 -0
- mekiki-0.2.0/tests/test_examples.py +61 -0
- mekiki-0.2.0/tests/test_feature.py +242 -0
- mekiki-0.2.0/tests/test_jev.py +740 -0
- mekiki-0.2.0/tests/test_knowledge.py +470 -0
- mekiki-0.2.0/tests/test_leakage.py +184 -0
- mekiki-0.2.0/tests/test_llm.py +251 -0
- mekiki-0.2.0/tests/test_paths.py +81 -0
- mekiki-0.2.0/tests/test_predictor.py +543 -0
- mekiki-0.2.0/tests/test_predictor_generic.py +427 -0
- mekiki-0.2.0/tests/test_samples.py +272 -0
- mekiki-0.2.0/tests/test_screening.py +129 -0
mekiki-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# APIキー。絶対にコミットしないこと(.env.example が雛形)
|
|
2
|
+
.env
|
|
3
|
+
|
|
4
|
+
# LLM 応答・埋め込みのキャッシュ。コードで再生成できる
|
|
5
|
+
sampledata/processed/*
|
|
6
|
+
!sampledata/processed/.gitkeep
|
|
7
|
+
|
|
8
|
+
# Python
|
|
9
|
+
.venv/
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.pyc
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
|
|
14
|
+
# パッケージのビルド成果物
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
*.egg-info/
|
|
18
|
+
|
|
19
|
+
.DS_Store
|
mekiki-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atsushi Matsumoto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mekiki-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mekiki
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A scikit-learn-compatible library that turns unstructured columns into typed features and puts an LLM on top of statistical models
|
|
5
|
+
Project-URL: Repository, https://github.com/attuan/mekiki
|
|
6
|
+
Author: Atsushi Matsumoto
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: automl,feature-engineering,llm,scikit-learn,tabular
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: pandas
|
|
16
|
+
Requires-Dist: scikit-learn
|
|
17
|
+
Provides-Extra: all
|
|
18
|
+
Requires-Dist: anthropic; extra == 'all'
|
|
19
|
+
Requires-Dist: lightgbm; extra == 'all'
|
|
20
|
+
Requires-Dist: litellm>=1.101.0; extra == 'all'
|
|
21
|
+
Requires-Dist: sentence-transformers; extra == 'all'
|
|
22
|
+
Requires-Dist: xgboost; extra == 'all'
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
25
|
+
Provides-Extra: embed
|
|
26
|
+
Requires-Dist: sentence-transformers; extra == 'embed'
|
|
27
|
+
Provides-Extra: litellm
|
|
28
|
+
Requires-Dist: litellm>=1.101.0; extra == 'litellm'
|
|
29
|
+
Provides-Extra: llm
|
|
30
|
+
Requires-Dist: anthropic; extra == 'llm'
|
|
31
|
+
Provides-Extra: models
|
|
32
|
+
Requires-Dist: lightgbm; extra == 'models'
|
|
33
|
+
Requires-Dist: xgboost; extra == 'models'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<picture>
|
|
38
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/attuan/mekiki/main/assets/logo_dark.svg">
|
|
39
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/logo_light.svg" width="520" alt="mekiki — a trained eye for the columns your model cannot read">
|
|
40
|
+
</picture>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-2a78d6">
|
|
45
|
+
<img alt="scikit-learn compatible" src="https://img.shields.io/badge/scikit--learn-compatible-2a78d6">
|
|
46
|
+
<img alt="LLM: Claude by default, others through LiteLLM" src="https://img.shields.io/badge/LLM-Claude%20%C2%B7%20LiteLLM-e8692f">
|
|
47
|
+
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-59636e">
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
`mekiki` (目利き, "a trained eye") is a scikit-learn-compatible Python library that turns
|
|
51
|
+
unstructured columns (free text today; images are in the design) into typed features and puts an LLM
|
|
52
|
+
**on top of** statistical models rather than in place of them.
|
|
53
|
+
|
|
54
|
+
Structured columns rarely determine a target on their own. In used-car pricing, the listing
|
|
55
|
+
title and the seller's description carry what the mileage and model year do not. Classical
|
|
56
|
+
regression could only absorb that as dummy variables, which is where accuracy plateaued.
|
|
57
|
+
`mekiki` closes that gap with an LLM, but keeps the LLM where it is cheap, auditable and
|
|
58
|
+
easy to switch off:
|
|
59
|
+
|
|
60
|
+
<table>
|
|
61
|
+
<tr>
|
|
62
|
+
<td align="center" valign="top" width="33%">
|
|
63
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/icon_diagnose.svg" width="40" height="40" alt=""><br>
|
|
64
|
+
<b><code>diagnose()</code></b><br>
|
|
65
|
+
<sub>Hand it a DataFrame and a target; get back a recommended configuration as objects you can pass straight in.</sub>
|
|
66
|
+
</td>
|
|
67
|
+
<td align="center" valign="top" width="33%">
|
|
68
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/icon_typed_column.svg" width="40" height="40" alt=""><br>
|
|
69
|
+
<b><code>SemanticEncoder</code></b><br>
|
|
70
|
+
<sub>Declare an unstructured column as a typed one. Embed → neighbour vote → confidence; the LLM is only a fallback for unsure rows.</sub>
|
|
71
|
+
</td>
|
|
72
|
+
<td align="center" valign="top" width="33%">
|
|
73
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/icon_knowledge_column.svg" width="40" height="40" alt=""><br>
|
|
74
|
+
<b><code>KnowledgeEncoder</code></b><br>
|
|
75
|
+
<sub>Columns the table lacks, from the LLM's world knowledge. Asked once per key value, kept only if they help.</sub>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<td align="center" valign="top" width="33%">
|
|
80
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/icon_evidence_predictor.svg" width="40" height="40" alt=""><br>
|
|
81
|
+
<b><code>EvidenceRegressor</code> / <code>EvidenceClassifier</code></b><br>
|
|
82
|
+
<sub>LightGBM, XGBoost and a semantic k-NN solve it first; the LLM makes the final call from their evidence. Regression and classification.</sub>
|
|
83
|
+
</td>
|
|
84
|
+
<td align="center" valign="top" width="33%">
|
|
85
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/icon_routing.svg" width="40" height="40" alt=""><br>
|
|
86
|
+
<b>Confidence routing</b><br>
|
|
87
|
+
<sub>One ratio, <code>escalate_rate</code>, decides which rows reach the LLM at all, from signals that exist before any call.</sub>
|
|
88
|
+
</td>
|
|
89
|
+
<td align="center" valign="top" width="33%">
|
|
90
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/icon_provenance.svg" width="40" height="40" alt=""><br>
|
|
91
|
+
<b>Provenance</b><br>
|
|
92
|
+
<sub>Every cell knows its source (human / model / llm), confidence, evidence and cost. <code>explain()</code> traces it back.</sub>
|
|
93
|
+
</td>
|
|
94
|
+
</tr>
|
|
95
|
+
</table>
|
|
96
|
+
|
|
97
|
+
<p align="center">
|
|
98
|
+
<picture>
|
|
99
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/attuan/mekiki/main/assets/concept_dark.svg">
|
|
100
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/concept_light.svg" width="100%" alt="How mekiki works: free text becomes typed columns, statistical models solve the task first, and only the rows they are unsure about reach the LLM, which makes the final call from their predictions and similar records">
|
|
101
|
+
</picture>
|
|
102
|
+
</p>
|
|
103
|
+
|
|
104
|
+
| part | what it gives you | calls the LLM | LLM cost grows with |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| `diagnose()` | a recommended configuration, as objects | once per table (optional) | — |
|
|
107
|
+
| `screen()` | whether the text carries signal at all | never | — |
|
|
108
|
+
| `SemanticEncoder` | a typed column from an unstructured one | only for low-confidence rows | rows escalated |
|
|
109
|
+
| `KnowledgeEncoder` | columns the table does not have | once per distinct key value | distinct keys, not rows |
|
|
110
|
+
| `EvidenceRegressor` / `EvidenceClassifier` | predictions with evidence and a reason | once per routed row | rows × `escalate_rate` |
|
|
111
|
+
| `check_duplicates` / `check_overlap` | duplicate-record leakage warnings | never | — |
|
|
112
|
+
|
|
113
|
+
Every cell carries provenance (human / model / llm, confidence, evidence, cost). LLM responses
|
|
114
|
+
are cached on disk so no row is paid for twice. Duplicate-record leakage is checked
|
|
115
|
+
automatically on fit / predict. And `screen()` tells you, for free, whether text carries any
|
|
116
|
+
signal on your data before you spend anything.
|
|
117
|
+
|
|
118
|
+
The project started as an internship project on used-car price prediction; used cars are the
|
|
119
|
+
flagship use case, not the scope.
|
|
120
|
+
|
|
121
|
+
**Documentation site: the same tutorials plus an API reference, built from the `docs/` folder of this repository with Mintlify.**
|
|
122
|
+
|
|
123
|
+
**New here? Open [`examples/quickstart.ipynb`](https://github.com/attuan/mekiki/blob/main/examples/quickstart.ipynb) first**: used-car prices end
|
|
124
|
+
to end on the bundled data, committed with the outputs of a real run so it can be read right here on
|
|
125
|
+
GitHub (every notebook has an *Open in Colab* badge). Running it yourself takes an API key and about
|
|
126
|
+
$1 of LLM calls. Then there is one notebook per part, each ending with how to plug in your own pieces:
|
|
127
|
+
[`diagnose`](https://github.com/attuan/mekiki/blob/main/examples/diagnose.ipynb) (free except one cell),
|
|
128
|
+
[`semantic_encoder`](https://github.com/attuan/mekiki/blob/main/examples/semantic_encoder.ipynb),
|
|
129
|
+
[`knowledge_encoder`](https://github.com/attuan/mekiki/blob/main/examples/knowledge_encoder.ipynb) and
|
|
130
|
+
[`evidence_predictor`](https://github.com/attuan/mekiki/blob/main/examples/evidence_predictor.ipynb).
|
|
131
|
+
|
|
132
|
+
## Installation
|
|
133
|
+
|
|
134
|
+
Python 3.12 or later. The only required dependencies are pandas, numpy and scikit-learn;
|
|
135
|
+
everything else is an extra.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pip install "mekiki[all]"
|
|
139
|
+
uv add "mekiki[all]" # with uv
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| extra | installs | needed for |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `models` | lightgbm, xgboost | `EvidencePredictor`, confidence routing, `screen()`, `diagnose()` scoring |
|
|
145
|
+
| `llm` | anthropic | actually calling the LLM (import works without it) |
|
|
146
|
+
| `litellm` | litellm | calling a provider other than Claude (`openai/...`, `gemini/...`, ...) |
|
|
147
|
+
| `embed` | sentence-transformers (pulls in torch) | stronger embeddings than the default character TF-IDF |
|
|
148
|
+
| `all` | all four above | |
|
|
149
|
+
| `dev` | pytest | development |
|
|
150
|
+
|
|
151
|
+
For development, clone and install in editable mode. With
|
|
152
|
+
[uv](https://docs.astral.sh/uv/) — which fetches Python 3.12 itself, so the system Python
|
|
153
|
+
is never involved:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
git clone git@github.com:attuan/mekiki.git && cd mekiki
|
|
157
|
+
uv venv --python 3.12
|
|
158
|
+
uv pip install -e ".[all,dev]"
|
|
159
|
+
.venv/bin/python -m pytest tests -q # the LLM is stubbed; everything passes without an API key
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Or with pip:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
python3.12 -m venv .venv
|
|
166
|
+
.venv/bin/pip install -e ".[all,dev]"
|
|
167
|
+
.venv/bin/python -m pytest tests -q
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
On macOS, xgboost / lightgbm need `brew install libomp`.
|
|
171
|
+
|
|
172
|
+
`pyproject.toml` also carries a `research` dependency group and `[tool.ruff]` settings
|
|
173
|
+
that belong to the development repository. Dependency groups are never part of a wheel or
|
|
174
|
+
an sdist, so they change nothing about what `pip install mekiki` gives you — but the
|
|
175
|
+
project-level commands (`uv sync`, and `uv run`, which syncs first) would install that
|
|
176
|
+
group here. That is why the steps above use `uv pip install` and call the interpreter in
|
|
177
|
+
`.venv` directly.
|
|
178
|
+
|
|
179
|
+
## API key
|
|
180
|
+
|
|
181
|
+
Only needed when the LLM is actually called. Keys are read from the environment, or from
|
|
182
|
+
a `.env` file found by walking up from the current directory (template: `.env.example`).
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
ANTHROPIC_API_KEY=sk-ant-api03-...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Keys are issued at https://console.anthropic.com/settings/keys.
|
|
189
|
+
|
|
190
|
+
Claude is the default. To use another provider, install the `litellm` extra
|
|
191
|
+
(`pip install "mekiki[litellm]"`), put that provider's key in the same `.env` under the
|
|
192
|
+
name LiteLLM expects (`OPENAI_API_KEY`, `GEMINI_API_KEY`, ... — see
|
|
193
|
+
https://docs.litellm.ai/docs/providers), and pass the model with its provider prefix:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from mekiki import LLMClient, EvidenceRegressor
|
|
197
|
+
|
|
198
|
+
client = LLMClient(model="openai/gpt-5") # or "gemini/gemini-2.5-pro", "ollama/..."
|
|
199
|
+
print(client.why_unavailable()) # None when the key is in place
|
|
200
|
+
model = EvidenceRegressor(client=client, ...)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Claude models (`claude-...`) always go through the official Anthropic SDK, everything
|
|
204
|
+
else through LiteLLM. `ClaudeClient` is the former name of `LLMClient` and still works.
|
|
205
|
+
|
|
206
|
+
`TYPESAFE_API_KEY` (optional) turns on the Jev middle tier described below; leave it out and
|
|
207
|
+
the tier is skipped.
|
|
208
|
+
|
|
209
|
+
### One key for everything: the Vercel AI Gateway
|
|
210
|
+
|
|
211
|
+
Instead of one key per provider, a single [Vercel AI Gateway](https://vercel.com/ai-gateway)
|
|
212
|
+
key reaches Claude, the other providers and Jev:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
AI_GATEWAY_API_KEY=vck_...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Every model whose own key is missing then goes through the gateway (`client.via_gateway`
|
|
219
|
+
tells which way a client goes). Claude keeps the official SDK there, with structured output
|
|
220
|
+
and prompt caching unchanged, and its cache key does not change, so answers cached from
|
|
221
|
+
Anthropic directly keep hitting. To send Claude through the gateway even with an Anthropic
|
|
222
|
+
key present, set `ANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh` or pass
|
|
223
|
+
`LLMClient(base_url="https://ai-gateway.vercel.sh")`. The gateway charges each provider's
|
|
224
|
+
list price, and it needs a credit card on file before it serves any request.
|
|
225
|
+
|
|
226
|
+
## Quick start — no API key, no data download
|
|
227
|
+
|
|
228
|
+
The bundled 500-row excerpts are enough. `SemanticEncoder` in seven lines:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
import pandas as pd
|
|
232
|
+
from mekiki import SemanticEncoder
|
|
233
|
+
from mekiki.paths import sample_data
|
|
234
|
+
|
|
235
|
+
df = pd.read_csv(sample_data("news_sample500.csv"))
|
|
236
|
+
topic = SemanticEncoder(source="Headline", type="category",
|
|
237
|
+
values=["economy", "microsoft", "obama", "palestine"])
|
|
238
|
+
df["topic_typed"] = topic.fit_transform(df) # no labels, no API key, about two seconds
|
|
239
|
+
print((df["topic_typed"] == df["Topic"]).mean()) # 0.92 against the true topic
|
|
240
|
+
print(topic.explain(1)) # why row 1 got its value
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
That is a typed column from four words of supervision, with a confidence and a provenance
|
|
244
|
+
record per cell. The notebooks in [`examples/`](https://github.com/attuan/mekiki/tree/main/examples/) take it from there: which rows the
|
|
245
|
+
library is unsure about, whether a text column is worth an LLM at all (`screen()`), and then
|
|
246
|
+
`EvidencePredictor` on used-car prices with the bill shown before anything is spent.
|
|
247
|
+
|
|
248
|
+
## Start here — `diagnose()`
|
|
249
|
+
|
|
250
|
+
Give it the data and the target column and it returns a recommended setup.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from mekiki import diagnose, EvidenceRegressor
|
|
254
|
+
|
|
255
|
+
rec = diagnose(df, target="price")
|
|
256
|
+
print(rec) # findings and the recommended configuration
|
|
257
|
+
rec.spec # ColumnSpec: which columns are numeric / categorical / text / long text
|
|
258
|
+
rec.domain # Domain: role, what a record is, what the target is called
|
|
259
|
+
print(rec.to_code()) # a Python snippet that reproduces the recommendation
|
|
260
|
+
|
|
261
|
+
model = EvidenceRegressor(target="price", spec=rec.spec, domain=rec.domain)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The first stage is rule-based and free (task type, target skew and outliers, per-column
|
|
265
|
+
profile, duplicates, text screening, estimated cost of using the LLM on every row). The
|
|
266
|
+
second stage hands that diagnosis table, with column names and a few sample values but
|
|
267
|
+
not the data itself, to the LLM once, so it can decide what rules cannot: which columns are
|
|
268
|
+
identifiers, which vary between listings of the same item, what the target should be called.
|
|
269
|
+
It also lists candidate columns you could build next — `SemanticEncoder` candidates extracted
|
|
270
|
+
from the free text and `KnowledgeEncoder` candidates keyed by structured columns — as
|
|
271
|
+
commented-out lines in `to_code()`, since building them costs LLM calls.
|
|
272
|
+
Every recommendation carries `reasons` pointing back at the numbers it came from.
|
|
273
|
+
Without an API key (or with `llm=False`) the rule-based recommendation is returned as is.
|
|
274
|
+
|
|
275
|
+
## `SemanticEncoder` — feature generation
|
|
276
|
+
|
|
277
|
+
Turn an unstructured column into a typed one by declaration alone. Internally it is
|
|
278
|
+
"embed -> nearest-neighbour vote"; the LLM only appears as a fallback for low-confidence rows.
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from mekiki import SemanticEncoder
|
|
282
|
+
|
|
283
|
+
df["body_type"] = SemanticEncoder(
|
|
284
|
+
source="model", # free-form string column
|
|
285
|
+
type="category",
|
|
286
|
+
values=["pickup truck", "sedan", "suv", "van", "coupe"],
|
|
287
|
+
escalate_rate=0.15, # send the 15% least confident rows to the LLM
|
|
288
|
+
).fit_transform(df)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
If a labelled column already exists, pass it as `labels="..."` and it becomes the reference
|
|
292
|
+
set. Rows the model cannot decide are queued in `review_queue()`; approving them turns them
|
|
293
|
+
into labels for next time, so the fast path grows.
|
|
294
|
+
|
|
295
|
+
## `EvidenceRegressor` / `EvidenceClassifier` — LLM predict
|
|
296
|
+
|
|
297
|
+
The LLM is not handed a raw record and asked for the answer. **LightGBM, XGBoost and a
|
|
298
|
+
nearest-neighbour index solve it first; their predictions, plus similar records whose actual
|
|
299
|
+
target is known, are passed to the LLM as evidence, and the LLM only makes the final call.**
|
|
300
|
+
Similar records are retrieved per row (few-shot, not pasted once).
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from mekiki import EvidenceRegressor, USED_CAR
|
|
304
|
+
|
|
305
|
+
model = EvidenceRegressor(
|
|
306
|
+
target="price", unit="USD", domain=USED_CAR,
|
|
307
|
+
numeric=["age", "odometer"], categorical=["manufacturer", "state"],
|
|
308
|
+
text="model", long_text="description",
|
|
309
|
+
)
|
|
310
|
+
pred = model.fit(train_df).predict(test_df)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
No separate labels are needed: the training targets are the few-shot examples.
|
|
314
|
+
|
|
315
|
+
**The column arguments can be left out.** With none of `numeric` / `boolean` / `categorical` /
|
|
316
|
+
`text` / `long_text` given, `fit` assigns every column except the target by the same rules
|
|
317
|
+
`diagnose` starts from (dtype, number of distinct values, mean length; no LLM call), leaving
|
|
318
|
+
out identifier, date and constant columns. The assignment it used is in `model.spec_`. Give
|
|
319
|
+
any column and only the columns given are used.
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
model = EvidenceRegressor(target="price").fit(train_df)
|
|
323
|
+
model.spec_ # the ColumnSpec fit chose
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**What is being predicted is described in words through a `Domain`.** The role ("a news
|
|
327
|
+
editor"), what one record is ("news article"), what the target is called ("the topic") and any
|
|
328
|
+
domain-specific hints are assembled into the prompt. Leave it out and the wording commits
|
|
329
|
+
to no domain. The used-car wording is kept as the `USED_CAR` preset.
|
|
330
|
+
|
|
331
|
+
Classification is `EvidenceClassifier`, with the same arguments minus `unit` (both classes share
|
|
332
|
+
the base `EvidencePredictor`, which takes `task=` if you need to switch at runtime): the tree models contribute
|
|
333
|
+
per-class probabilities, the neighbours contribute label frequencies, and the LLM returns a
|
|
334
|
+
label plus per-class probabilities.
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from mekiki import EvidenceClassifier, Domain
|
|
338
|
+
|
|
339
|
+
model = EvidenceClassifier(
|
|
340
|
+
target="Churn",
|
|
341
|
+
domain=Domain(role="a churn analyst", subject="customer", target_name="churn",
|
|
342
|
+
class_names={"Yes": "churned", "No": "stayed"}),
|
|
343
|
+
numeric=["tenure", "MonthlyCharges"], categorical=["Contract"],
|
|
344
|
+
)
|
|
345
|
+
model.fit(train_df)
|
|
346
|
+
label = model.predict(test_df) # labels
|
|
347
|
+
proba = model.predict_proba(test_df) # probabilities in the order of model.classes_
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Confidence routing — which rows reach the LLM
|
|
351
|
+
|
|
352
|
+
`EvidencePredictor` calls the LLM **once per row**, so the row count is the cost and the
|
|
353
|
+
time (60,000 rows is roughly $516 and 17 hours). Confidence routing chooses the rows to
|
|
354
|
+
send using only signals available before any LLM call and leaves the rest to the
|
|
355
|
+
statistical models. One ratio moves continuously between "call every row" and "never call".
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
model = EvidenceRegressor(target="price", unit="USD",
|
|
359
|
+
numeric=["age", "odometer"],
|
|
360
|
+
categorical=["manufacturer", "state"], text="model",
|
|
361
|
+
escalate_rate=0.3) # send only the 30% strongest-signal rows
|
|
362
|
+
model.fit(train)
|
|
363
|
+
model.plan(test) # before calling: how many rows, how much, how long (free)
|
|
364
|
+
pred = model.predict(test)
|
|
365
|
+
model.route() # per row: fast path or llm, and the signal value
|
|
366
|
+
model.curve(test) # accuracy / cost / latency as the ratio is swept
|
|
367
|
+
model.approve() # approve the LLM's answers -> those rows are served without a call next time
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The default signal is the **disagreement between LightGBM and XGBoost** (rows where the
|
|
371
|
+
statistical models themselves are unsure), which measured best. `signal="unseen"` prefers
|
|
372
|
+
rows that contain values absent from the training data (an unknown `model`, for example).
|
|
373
|
+
Leaving `escalate_rate` out sends every row and warns with the estimated cost;
|
|
374
|
+
`escalate_rate=1.0` says so explicitly and silences the warning.
|
|
375
|
+
|
|
376
|
+
<picture>
|
|
377
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/attuan/mekiki/main/assets/routing_curve_dark.png">
|
|
378
|
+
<img src="https://raw.githubusercontent.com/attuan/mekiki/main/assets/routing_curve_light.png" width="720"
|
|
379
|
+
alt="MAE against the share of rows sent to the LLM, 600 used-car rows: 3,043 at 0% ($0.00), 2,674 at 30% ($1.57), 2,581 at 50% ($2.62), 2,265 at 100% ($5.23)">
|
|
380
|
+
</picture>
|
|
381
|
+
|
|
382
|
+
Measured on 600 held-out Craigslist rows: the error falls steadily as more rows are sent, so
|
|
383
|
+
routing is not a way to gain accuracy but a way to **choose how much accuracy to buy**. Sending
|
|
384
|
+
half the rows costs half as much and keeps 59% of the improvement.
|
|
385
|
+
|
|
386
|
+
| rows sent to the LLM | 0% | 10% | 20% | 30% | 50% | 75% | 100% |
|
|
387
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
388
|
+
| MAE (USD) | 3,043 | 2,907 | 2,795 | 2,674 | 2,581 | 2,450 | 2,265 |
|
|
389
|
+
| LLM cost (USD) | 0.00 | 0.52 | 1.05 | 1.57 | 2.62 | 3.92 | 5.23 |
|
|
390
|
+
|
|
391
|
+
## A cheaper middle tier — Jev
|
|
392
|
+
|
|
393
|
+
Every part that calls an LLM can put a second, much cheaper model between the statistical
|
|
394
|
+
answer and the frontier LLM: **Jev**, TypeSafe AI's "System One" model. It answers typed
|
|
395
|
+
questions (pick one option, rate on ordered levels, yes/no) with calibrated probabilities in a
|
|
396
|
+
few hundred milliseconds, for about $0.00004 per row (input tokens only). It cannot write free
|
|
397
|
+
text or numbers, which is exactly why it fits: most of what mekiki asks a model is "which of
|
|
398
|
+
these", and even regression becomes "which statistical model's prediction to trust".
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
from mekiki import SemanticEncoder, EvidenceRegressor, KnowledgeEncoder, JevFallback, LLMFallback
|
|
402
|
+
|
|
403
|
+
col = SemanticEncoder(source="description", values=["dealer", "private"],
|
|
404
|
+
fallback=[JevFallback(), LLMFallback()]) # neighbours -> Jev -> LLM
|
|
405
|
+
model = EvidenceRegressor(target="price", numeric=["age", "odometer"],
|
|
406
|
+
jev=True, escalate_rate=0.2, signal="jev") # models -> Jev weights -> LLM
|
|
407
|
+
cols = KnowledgeEncoder(keys=["manufacturer", "model"], attribute="body style",
|
|
408
|
+
values=["sedan", "SUV", "pickup"], jev=True) # Jev per key -> LLM for the rest
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The key is `TYPESAFE_API_KEY` in the settings file, or `AI_GATEWAY_API_KEY` to reach Jev
|
|
412
|
+
through the Vercel AI Gateway (model `typesafe-ai/jev`). Without either the Jev tier is skipped and
|
|
413
|
+
everything runs as the two-tier version. Provenance records `jev` as a source, and `cost()` /
|
|
414
|
+
`plan()` report the Jev bill separately. The client (`JevClient`) shares the design of
|
|
415
|
+
`LLMClient`: disk cache, cost accounting, parallel batches, and no extra dependency.
|
|
416
|
+
|
|
417
|
+
## `KnowledgeEncoder` — columns the table does not have
|
|
418
|
+
|
|
419
|
+
Ask the LLM's world knowledge for an attribute of each key value, once per unique key, and
|
|
420
|
+
join the result to the table. With `target=` each proposed column is scored (a tree model
|
|
421
|
+
with and without it, on the same folds, no LLM call) and **only columns that helped are
|
|
422
|
+
returned**.
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
from mekiki import KnowledgeEncoder, USED_CAR
|
|
426
|
+
|
|
427
|
+
# fully automatic: the LLM proposes what to look up, scoring keeps what helps
|
|
428
|
+
new_cols = KnowledgeEncoder(target="price").fit_transform(df)
|
|
429
|
+
df = df.join(new_cols)
|
|
430
|
+
|
|
431
|
+
# or say exactly what you want
|
|
432
|
+
col = KnowledgeEncoder(keys=["manufacturer", "model"], attribute="typical new price in USD",
|
|
433
|
+
type="numeric", domain=USED_CAR)
|
|
434
|
+
df = df.join(col.fit_transform(df))
|
|
435
|
+
col.cost() # callable after fit, before any lookup: how many keys, how much
|
|
436
|
+
col.status() # accepted / unknown / rejected per column, agreement with an existing column
|
|
437
|
+
col.review_queue() # key values a human should look at, most frequent first
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Cost scales with the number of distinct key values, not rows. On 60,000 Craigslist rows a
|
|
441
|
+
"typical new price" column cost about $4 and cut LightGBM's MAE by 13.7% without using the
|
|
442
|
+
`model` string at all.
|
|
443
|
+
|
|
444
|
+
## Before you start — does text help on this data?
|
|
445
|
+
|
|
446
|
+
`EvidencePredictor` only pays off when the text moves the target. `screen()` measures that
|
|
447
|
+
**without calling the LLM**, by comparing tree models with and without the text column.
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
from mekiki import screen
|
|
451
|
+
|
|
452
|
+
print(screen(df, target="price", text="model", unit="USD",
|
|
453
|
+
numeric=["age", "odometer"], categorical=["manufacturer", "state"]))
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The report gives the text contribution as a percentage against a threshold and a verdict
|
|
457
|
+
(worth trying or not).
|
|
458
|
+
|
|
459
|
+
## A caution about free text
|
|
460
|
+
|
|
461
|
+
Listing text often **contains the asking price verbatim** (43.7% of Craigslist
|
|
462
|
+
`description` rows). Passed as is, the task becomes reading the answer rather than
|
|
463
|
+
predicting it, so `mekiki` **masks amounts by default** in `long_text` for regression.
|
|
464
|
+
|
|
465
|
+
```python
|
|
466
|
+
model = EvidenceRegressor(..., long_text="description") # amounts become <AMOUNT>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Leakage check — duplicate records
|
|
470
|
+
|
|
471
|
+
**When the same record is in both train and test, prediction becomes lookup.** The
|
|
472
|
+
Craigslist data lists the same car in several regions; evaluating with duplicates left in
|
|
473
|
+
inflated R² from 0.880 to 0.914, and **the more text is used as a feature, the larger the
|
|
474
|
+
inflation.** `mekiki` therefore checks on fit / predict and warns.
|
|
475
|
+
|
|
476
|
+
```python
|
|
477
|
+
from mekiki import check_duplicates, check_overlap
|
|
478
|
+
|
|
479
|
+
print(check_duplicates(df, keys=["VIN"])) # duplicates within one table
|
|
480
|
+
print(check_overlap(train, test)) # duplicates across train and test
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
It warns rather than raises (duplicates can be intentional). Switch it off with
|
|
484
|
+
`EvidenceRegressor(..., check_leakage=False)`.
|
|
485
|
+
|
|
486
|
+
## Provenance — inspecting why
|
|
487
|
+
|
|
488
|
+
Every feature answers "why this value".
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
model.explain(0) # one row: evidence, retrieved examples, the LLM's reason
|
|
492
|
+
model.confidence() # per-row confidence
|
|
493
|
+
model.examples() # the similar records used
|
|
494
|
+
model.cost() # what was spent and the cost per row
|
|
495
|
+
model.provenance() # all rows in one table
|
|
496
|
+
model.report() # a short text summary of a predict() run
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
LLM responses are cached on disk (inside a clone: `sampledata/processed/llm_cache/`; when
|
|
500
|
+
installed: the OS cache directory; override with `MEKIKI_CACHE_DIR`). **The same row is
|
|
501
|
+
never paid for twice**, so re-running after a code change is free.
|
|
502
|
+
|
|
503
|
+
## Bundled data
|
|
504
|
+
|
|
505
|
+
`sampledata/sample/` holds 500-row excerpts so that the examples and the tests run with no
|
|
506
|
+
download. Each is a random sample (seed 42) of a public dataset; column names and values are
|
|
507
|
+
left exactly as the source has them. **Only sources that permit redistribution are bundled**,
|
|
508
|
+
and each excerpt keeps the license of its source — MIT covers the code in `mekiki/`, not the
|
|
509
|
+
data.
|
|
510
|
+
|
|
511
|
+
- `vehicles_sample500.csv` — Craigslist used-car listings; free-form seller descriptions and
|
|
512
|
+
a price. Used by most of the notebooks in `examples/`.
|
|
513
|
+
*Used Cars Dataset*, Austin Reese, Kaggle. **CC0 1.0** (public domain dedication).
|
|
514
|
+
- `news_sample500.csv` — online news headlines, their topic, and how often each was shared.
|
|
515
|
+
Both answers `screen()` can give live here: the share count barely follows from the
|
|
516
|
+
headline, the topic does.
|
|
517
|
+
Nuno Moniz and Luís Torgo, *Multi-Source Social Feedback of Online News Feeds* (2018),
|
|
518
|
+
UCI Machine Learning Repository, [10.24432/C5H029](https://doi.org/10.24432/C5H029).
|
|
519
|
+
**CC BY 4.0**.
|
|
520
|
+
- `bank_sample500.csv` — a Portuguese bank's term-deposit campaign. No text column at all,
|
|
521
|
+
which is the baseline for what `EvidencePredictor` does without one.
|
|
522
|
+
S. Moro, P. Rita and P. Cortez, *Bank Marketing* (2014), UCI Machine Learning Repository,
|
|
523
|
+
[10.24432/C5K306](https://doi.org/10.24432/C5K306). **CC BY 4.0**.
|
|
524
|
+
|
|
525
|
+
## Layout
|
|
526
|
+
|
|
527
|
+
| path | contents |
|
|
528
|
+
|---|---|
|
|
529
|
+
| `mekiki/feature.py` | `SemanticEncoder`. embed -> nearest-neighbour classification -> LLM fallback |
|
|
530
|
+
| `mekiki/predictor.py` | `EvidencePredictor`. Statistical models and similar cases as evidence, LLM makes the final call; confidence routing lives here too |
|
|
531
|
+
| `mekiki/jev.py` | `JevClient`: the cheap middle tier (TypeSafe AI's Jev). Same cache / cost / parallel design as `LLMClient`; the other module that emits HTTP |
|
|
532
|
+
| `mekiki/adaptive.py` | Routing building blocks: signals, row selection, cost and time estimates |
|
|
533
|
+
| `mekiki/knowledge.py` | `KnowledgeEncoder`. Columns from world knowledge, asked once per key value |
|
|
534
|
+
| `mekiki/diagnose.py` | `diagnose()`. Data + target -> recommended configuration |
|
|
535
|
+
| `mekiki/screening.py` | `screen()`. Measures the text contribution without calling the LLM |
|
|
536
|
+
| `mekiki/leakage.py` | Duplicate-record detection, run automatically on fit / predict |
|
|
537
|
+
| `mekiki/fallback.py` | What happens to low-confidence rows: queue for review, or ask the LLM |
|
|
538
|
+
| `mekiki/vectorizers.py` | Embeddings: character TF-IDF (default), sentence-transformers, precomputed |
|
|
539
|
+
| `mekiki/llm.py` | **The frontier-LLM client** (`LLMClient`): Claude through the official SDK, other providers through LiteLLM. Disk cache, cost accounting, parallel requests |
|
|
540
|
+
| `mekiki/paths.py` | Where the cache, `.env` and bundled data live; differs inside a clone and after install |
|
|
541
|
+
| `examples/` | Tutorial notebooks, committed with their outputs |
|
|
542
|
+
| `assets/` | Figures used by this README |
|
|
543
|
+
| `tests/` | `pytest tests -q` |
|