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.
Files changed (37) hide show
  1. mekiki-0.2.0/.gitignore +19 -0
  2. mekiki-0.2.0/LICENSE +21 -0
  3. mekiki-0.2.0/PKG-INFO +543 -0
  4. mekiki-0.2.0/README.md +508 -0
  5. mekiki-0.2.0/mekiki/__init__.py +207 -0
  6. mekiki-0.2.0/mekiki/adaptive.py +199 -0
  7. mekiki-0.2.0/mekiki/diagnose.py +1247 -0
  8. mekiki-0.2.0/mekiki/errors.py +35 -0
  9. mekiki-0.2.0/mekiki/fallback.py +258 -0
  10. mekiki-0.2.0/mekiki/feature.py +469 -0
  11. mekiki-0.2.0/mekiki/jev.py +644 -0
  12. mekiki-0.2.0/mekiki/knowledge.py +1226 -0
  13. mekiki-0.2.0/mekiki/leakage.py +186 -0
  14. mekiki-0.2.0/mekiki/llm.py +657 -0
  15. mekiki-0.2.0/mekiki/paths.py +116 -0
  16. mekiki-0.2.0/mekiki/predictor.py +2052 -0
  17. mekiki-0.2.0/mekiki/preprocess.py +63 -0
  18. mekiki-0.2.0/mekiki/py.typed +0 -0
  19. mekiki-0.2.0/mekiki/screening.py +300 -0
  20. mekiki-0.2.0/mekiki/vectorizers.py +228 -0
  21. mekiki-0.2.0/pyproject.toml +63 -0
  22. mekiki-0.2.0/sampledata/sample/bank_sample500.csv +501 -0
  23. mekiki-0.2.0/sampledata/sample/news_sample500.csv +501 -0
  24. mekiki-0.2.0/sampledata/sample/vehicles_sample500.csv +501 -0
  25. mekiki-0.2.0/tests/test_adaptive.py +404 -0
  26. mekiki-0.2.0/tests/test_diagnose.py +454 -0
  27. mekiki-0.2.0/tests/test_examples.py +61 -0
  28. mekiki-0.2.0/tests/test_feature.py +242 -0
  29. mekiki-0.2.0/tests/test_jev.py +740 -0
  30. mekiki-0.2.0/tests/test_knowledge.py +470 -0
  31. mekiki-0.2.0/tests/test_leakage.py +184 -0
  32. mekiki-0.2.0/tests/test_llm.py +251 -0
  33. mekiki-0.2.0/tests/test_paths.py +81 -0
  34. mekiki-0.2.0/tests/test_predictor.py +543 -0
  35. mekiki-0.2.0/tests/test_predictor_generic.py +427 -0
  36. mekiki-0.2.0/tests/test_samples.py +272 -0
  37. mekiki-0.2.0/tests/test_screening.py +129 -0
@@ -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` |