vertiq 0.2.0__tar.gz → 0.2.2__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.
- {vertiq-0.2.0/vertiq.egg-info → vertiq-0.2.2}/PKG-INFO +20 -44
- {vertiq-0.2.0 → vertiq-0.2.2}/README.md +19 -43
- {vertiq-0.2.0 → vertiq-0.2.2}/pyproject.toml +1 -1
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_mapping.py +17 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_patterns.py +8 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_report.py +112 -2
- vertiq-0.2.2/tests/test_segments.py +58 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/__init__.py +1 -1
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/affinity.py +8 -5
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/autotune.py +78 -37
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/harness.py +61 -2
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/learn.py +20 -6
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/mapping.py +23 -3
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/menu.py +47 -2
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/narrate.py +15 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/patterns.py +94 -29
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/report.py +122 -6
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/tables.py +65 -12
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/tasks.py +90 -17
- {vertiq-0.2.0 → vertiq-0.2.2/vertiq.egg-info}/PKG-INFO +20 -44
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/SOURCES.txt +1 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/LICENSE +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/setup.cfg +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_autotune.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_boost.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_boundary.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_category.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_context.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_core.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_design.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_diagnose.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_drift.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_guard.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_guides.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_mapping_messy.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_menu.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_narrate.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_plan.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_public_benchmark.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_sequences_profiles.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_statistics.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_summary.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_survival.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_tasks.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/actions.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/boost.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/category.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/causal.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/checkout.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/churn.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/context.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/contract.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/dataset.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/design.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/diagnose.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/drift.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/experiments.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/features.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/guard.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/insights.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/inventory.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/offers.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/plan.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/plugins.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/profiles.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/research.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/rules.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/sequences.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/sequential.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/summary.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/survival.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/trends.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/uplift.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/value.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/verticals.py +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/dependency_links.txt +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/entry_points.txt +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/requires.txt +0 -0
- {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vertiq
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Self-improving decision intelligence: a local language model that maps any operational dataset and decision models trained on real outcomes, improved through recursive self-improvement under an evaluation gate.
|
|
5
5
|
Author: Biswajit Tripathy
|
|
6
6
|
License: Apache-2.0
|
|
@@ -135,7 +135,7 @@ Intervention effects are estimated against control groups rather than inferred f
|
|
|
135
135
|
|
|
136
136
|
- **Control-group uplift.** vertiq.uplift compares treated entities with a randomly held-out group over the same window and ranks entities by estimated incremental effect. In the benchmark it estimated a planted −20% offer effect at -20.2%.
|
|
137
137
|
- **Configurable horizons.** window_days sets the prediction horizon, cutoffs the number of historical training points and history the event types used as inputs, so each task matches the operating cycle of the industry.
|
|
138
|
-
- **Small footprint.** The decision engine has no third-party dependencies and no network layer. It runs wherever Python 3.10 runs; the full benchmark suite completes in
|
|
138
|
+
- **Small footprint.** The decision engine has no third-party dependencies and no network layer. It runs wherever Python 3.10 runs; the full benchmark suite completes in 70 seconds on a laptop using 4 cores.
|
|
139
139
|
|
|
140
140
|
### Defensive deployment
|
|
141
141
|
|
|
@@ -143,6 +143,7 @@ A candidate model is treated as unproven until it has been evaluated.
|
|
|
143
143
|
|
|
144
144
|
- **Held-out evaluation.** Candidates are scored on entities excluded from training, using features built only from events recorded before each cutoff.
|
|
145
145
|
- **Champion–challenger gate.** A candidate must outperform the rule-based baseline and the production model, and its gain over the baseline must be clear at 90% confidence by default. min_gain sets a required margin: an AUC margin for event tasks and a relative error reduction for value tasks.
|
|
146
|
+
- **Segment check.** A candidate that wins on average but is clearly worse than the baseline for a segment of the held-out entities is not promoted. Segments come from the data (entity groups, entity-table labels, length of history), each with at least 30 held-out entities, and the confidence level is corrected for the number checked.
|
|
146
147
|
- **Explicit rejection.** A rejected candidate returns status "kept" with the reason, such as the AUC it reached against the required threshold. The production model remains in service.
|
|
147
148
|
|
|
148
149
|
| Principle | Mechanism | Configuration | Operational value |
|
|
@@ -254,7 +255,7 @@ Plugins add features; the evaluation gate determines whether they improve perfor
|
|
|
254
255
|
## Benchmarks
|
|
255
256
|
|
|
256
257
|
Synthetic scenarios with known ground truth, 5 runs each, mean ± standard deviation (`python benchmarks/run.py`,
|
|
257
|
-
|
|
258
|
+
70 seconds on a laptop using 4 cores, arm64, Python 3.10.10).
|
|
258
259
|
|
|
259
260
|
| Scenario | Vertiq | Reference |
|
|
260
261
|
|---|---|---|
|
|
@@ -343,7 +344,7 @@ Production-like test set (audit columns and locale-specific number formats):
|
|
|
343
344
|
|
|
344
345
|
## Verify the results
|
|
345
346
|
|
|
346
|
-
Every decision-engine figure can be reproduced from a clean checkout with Python and pytest; the benchmark suite runs in about
|
|
347
|
+
Every decision-engine figure can be reproduced from a clean checkout with Python and pytest; the benchmark suite runs in about 70 seconds on a laptop using 4 cores. Vertiq LM figures require Apple Silicon and the model weights; see [lm/README.md](lm/README.md).
|
|
347
348
|
|
|
348
349
|
```bash
|
|
349
350
|
git clone https://github.com/BiswajitBiswa/vertiq && cd vertiq
|
|
@@ -367,50 +368,25 @@ The methods are established and deliberately transparent: regularised logistic a
|
|
|
367
368
|
|
|
368
369
|
## Limitations
|
|
369
370
|
|
|
370
|
-
- Most benchmarks use synthetic data. They verify that the methods detect planted signal, reject noise and retain the baseline
|
|
371
|
-
|
|
372
|
-
- The
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
- Uplift estimation requires a randomised control group. Without one, `causal.matched_att` gives an estimate whose validity
|
|
383
|
-
depends on every confounder being observed; `design.next_step` reports the experiment size needed to settle the question.
|
|
384
|
-
- On the synthetic benchmarks, boosted trees improve on linear models where effects are non-linear and do not where effects are
|
|
385
|
-
additive. Across 100 to 6,400 entities the default tree settings were within 0.01 AUC of the best of five alternatives.
|
|
386
|
-
`learner="auto"` chooses by 3-fold cross-validation on the training entities; with a few hundred entities or fewer its
|
|
387
|
-
choice is close to random when the two learners are within a point or two of each other.
|
|
388
|
-
- Doubly robust estimates are consistent if either the propensity or the outcome model is right, but they vary more than the
|
|
389
|
-
outcome model alone (standard deviation 0.17 across benchmark runs); large weights from extreme
|
|
390
|
-
propensities are clipped and counted in the result.
|
|
391
|
-
- The rule-based mapping proposer gets 57% of roles right on the held-out real exports and Vertiq LM 83%; both are checked by
|
|
392
|
-
validation, which flags what it cannot settle (for example two complete amount columns, or day/month order), but meaning
|
|
393
|
-
(which amount is the one that matters) still needs a person or the model.
|
|
394
|
-
- Tables with one row per entity (customer lists, asset registers) often hold totals and dates computed when the file was
|
|
395
|
-
exported; Vertiq uses them to describe, never as prediction features.
|
|
396
|
-
- `plan` and `guard` rely on uplift tasks, so they are only as good as the recorded actions and control groups; an action
|
|
397
|
-
that was never measured is excluded from plans and held by the check. `guard` does not decide on its own: it allows only
|
|
398
|
-
measured, clearly helpful actions.
|
|
399
|
-
- `menu` proposes features from a fixed set of operations; anything outside it (external data, formulas specific to a
|
|
400
|
-
sector) is added as a plugin by a person and judged by the same gate.
|
|
401
|
-
- `autotune` scores every trial on the same training entities, so many trials can still favour a lucky change; it keeps a change
|
|
402
|
-
only when most validation folds agree, and the held-out gate makes the final decision.
|
|
371
|
+
- Most benchmarks use synthetic data. They verify that the methods detect planted signal, reject noise and retain the baseline when appropriate; one public dataset (Online Retail II) has been run. None measures accuracy on a specific organisation's data.
|
|
372
|
+
- The gate requires the gain over the baseline to be clear at 90% confidence (`confidence=0.9`, the default). On small datasets this also holds back some real improvements until more data arrives: with 240 assets and a real signal, 7 of 15 candidates were promoted, against 13 of 15 with the check off. `confidence=None` turns the check off; the candidate then only has to not be worse.
|
|
373
|
+
- Plain-language descriptions (`narrate`) are written from templates by default. The trained rewrite model gives real rewrites for 27 of 50 held-out descriptions, and every rewrite is checked: each number must be the template's and stay next to what it described. The checks catch invented and moved numbers, not every misleading sentence.
|
|
374
|
+
- After validation, Vertiq LM maps 54% of held-out tables exactly (42% from domains it was not trained on) and loads 100% without errors. Proposed mappings should be reviewed before production use.
|
|
375
|
+
- Uplift estimation requires a randomised control group. Without one, `causal.matched_att` gives an estimate whose validity depends on every confounder being observed; `design.next_step` reports the experiment size needed to settle the question.
|
|
376
|
+
- On the synthetic benchmarks, boosted trees improve on linear models where effects are non-linear and do not where effects are additive. Across 100 to 6,400 entities the default tree settings were within 0.01 AUC of the best of five alternatives. `learner="auto"` chooses by 3-fold cross-validation on the training entities; with a few hundred entities or fewer its choice is close to random when the two learners are within a point or two of each other.
|
|
377
|
+
- Doubly robust estimates are consistent if either the propensity or the outcome model is right, but they vary more than the outcome model alone (standard deviation 0.17 across benchmark runs); large weights from extreme propensities are clipped and counted in the result.
|
|
378
|
+
- The rule-based mapping proposer gets 57% of roles right on the held-out real exports and Vertiq LM 83%; both are checked by validation, which flags what it cannot settle (for example two complete amount columns, or day/month order), but meaning (which amount is the one that matters) still needs a person or the model.
|
|
379
|
+
- Tables with one row per entity (customer lists, asset registers) often hold totals and dates computed when the file was exported; Vertiq uses them to describe, never as prediction features.
|
|
380
|
+
- `plan` and `guard` rely on uplift tasks, so they are only as good as the recorded actions and control groups; an action that was never measured is excluded from plans and held by the check. `guard` does not decide on its own: it allows only measured, clearly helpful actions.
|
|
381
|
+
- `menu` proposes features from a fixed set of operations; anything outside it (external data, formulas specific to a sector) is added as a plugin by a person and judged by the same gate.
|
|
382
|
+
- `autotune` scores every trial on the same training entities, so many trials can still favour a lucky change; it keeps a change only when most validation folds agree, and the held-out gate makes the final decision.
|
|
403
383
|
- Version 0.2. The API may change before 1.0; changes are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
404
384
|
|
|
405
385
|
## Roadmap
|
|
406
386
|
|
|
407
|
-
- Further public benchmarks: M5 (value prediction), NASA C-MAPSS (equipment failure),
|
|
408
|
-
|
|
409
|
-
-
|
|
410
|
-
- Vertiq LM: a training round on tables with real-export clutter (`lm/make_data.py --messy`), scored on the synthetic and the real
|
|
411
|
-
held-out sets; evaluate a 9B student and publish the adapter with a `vertiq map` command.
|
|
412
|
-
- `autotune` for value tasks and uplift; menu operations for group-level signals and external series (weather, prices) joined by
|
|
413
|
-
date and location; menu items suggested by a language model for a sector, checked by `menu.parse`.
|
|
387
|
+
- Further public benchmarks: M5 (value prediction), NASA C-MAPSS (equipment failure), KKBOX (churn), and Hillstrom and Criteo (randomised uplift); protocols in [proposal 0001](proposals/0001-statistical-validation.md), part C.
|
|
388
|
+
- Vertiq LM: a training round on tables with real-export clutter (`lm/make_data.py --messy`), scored on the synthetic and the real held-out sets; evaluate a 9B student and publish the adapter with a `vertiq map` command.
|
|
389
|
+
- `autotune` for value tasks and uplift; menu operations for group-level signals and external series (weather, prices) joined by date and location; menu items suggested by a language model for a sector, checked by `menu.parse`.
|
|
414
390
|
- Turning free-text fields (support tickets, maintenance notes) into validated events.
|
|
415
391
|
|
|
416
392
|
Further reading on recursive self-improvement is kept in [proposals/reading-list.md](proposals/reading-list.md), and an external assessment of the competitive landscape in [proposals/landscape-review.md](proposals/landscape-review.md).
|
|
@@ -105,7 +105,7 @@ Intervention effects are estimated against control groups rather than inferred f
|
|
|
105
105
|
|
|
106
106
|
- **Control-group uplift.** vertiq.uplift compares treated entities with a randomly held-out group over the same window and ranks entities by estimated incremental effect. In the benchmark it estimated a planted −20% offer effect at -20.2%.
|
|
107
107
|
- **Configurable horizons.** window_days sets the prediction horizon, cutoffs the number of historical training points and history the event types used as inputs, so each task matches the operating cycle of the industry.
|
|
108
|
-
- **Small footprint.** The decision engine has no third-party dependencies and no network layer. It runs wherever Python 3.10 runs; the full benchmark suite completes in
|
|
108
|
+
- **Small footprint.** The decision engine has no third-party dependencies and no network layer. It runs wherever Python 3.10 runs; the full benchmark suite completes in 70 seconds on a laptop using 4 cores.
|
|
109
109
|
|
|
110
110
|
### Defensive deployment
|
|
111
111
|
|
|
@@ -113,6 +113,7 @@ A candidate model is treated as unproven until it has been evaluated.
|
|
|
113
113
|
|
|
114
114
|
- **Held-out evaluation.** Candidates are scored on entities excluded from training, using features built only from events recorded before each cutoff.
|
|
115
115
|
- **Champion–challenger gate.** A candidate must outperform the rule-based baseline and the production model, and its gain over the baseline must be clear at 90% confidence by default. min_gain sets a required margin: an AUC margin for event tasks and a relative error reduction for value tasks.
|
|
116
|
+
- **Segment check.** A candidate that wins on average but is clearly worse than the baseline for a segment of the held-out entities is not promoted. Segments come from the data (entity groups, entity-table labels, length of history), each with at least 30 held-out entities, and the confidence level is corrected for the number checked.
|
|
116
117
|
- **Explicit rejection.** A rejected candidate returns status "kept" with the reason, such as the AUC it reached against the required threshold. The production model remains in service.
|
|
117
118
|
|
|
118
119
|
| Principle | Mechanism | Configuration | Operational value |
|
|
@@ -224,7 +225,7 @@ Plugins add features; the evaluation gate determines whether they improve perfor
|
|
|
224
225
|
## Benchmarks
|
|
225
226
|
|
|
226
227
|
Synthetic scenarios with known ground truth, 5 runs each, mean ± standard deviation (`python benchmarks/run.py`,
|
|
227
|
-
|
|
228
|
+
70 seconds on a laptop using 4 cores, arm64, Python 3.10.10).
|
|
228
229
|
|
|
229
230
|
| Scenario | Vertiq | Reference |
|
|
230
231
|
|---|---|---|
|
|
@@ -313,7 +314,7 @@ Production-like test set (audit columns and locale-specific number formats):
|
|
|
313
314
|
|
|
314
315
|
## Verify the results
|
|
315
316
|
|
|
316
|
-
Every decision-engine figure can be reproduced from a clean checkout with Python and pytest; the benchmark suite runs in about
|
|
317
|
+
Every decision-engine figure can be reproduced from a clean checkout with Python and pytest; the benchmark suite runs in about 70 seconds on a laptop using 4 cores. Vertiq LM figures require Apple Silicon and the model weights; see [lm/README.md](lm/README.md).
|
|
317
318
|
|
|
318
319
|
```bash
|
|
319
320
|
git clone https://github.com/BiswajitBiswa/vertiq && cd vertiq
|
|
@@ -337,50 +338,25 @@ The methods are established and deliberately transparent: regularised logistic a
|
|
|
337
338
|
|
|
338
339
|
## Limitations
|
|
339
340
|
|
|
340
|
-
- Most benchmarks use synthetic data. They verify that the methods detect planted signal, reject noise and retain the baseline
|
|
341
|
-
|
|
342
|
-
- The
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
- Uplift estimation requires a randomised control group. Without one, `causal.matched_att` gives an estimate whose validity
|
|
353
|
-
depends on every confounder being observed; `design.next_step` reports the experiment size needed to settle the question.
|
|
354
|
-
- On the synthetic benchmarks, boosted trees improve on linear models where effects are non-linear and do not where effects are
|
|
355
|
-
additive. Across 100 to 6,400 entities the default tree settings were within 0.01 AUC of the best of five alternatives.
|
|
356
|
-
`learner="auto"` chooses by 3-fold cross-validation on the training entities; with a few hundred entities or fewer its
|
|
357
|
-
choice is close to random when the two learners are within a point or two of each other.
|
|
358
|
-
- Doubly robust estimates are consistent if either the propensity or the outcome model is right, but they vary more than the
|
|
359
|
-
outcome model alone (standard deviation 0.17 across benchmark runs); large weights from extreme
|
|
360
|
-
propensities are clipped and counted in the result.
|
|
361
|
-
- The rule-based mapping proposer gets 57% of roles right on the held-out real exports and Vertiq LM 83%; both are checked by
|
|
362
|
-
validation, which flags what it cannot settle (for example two complete amount columns, or day/month order), but meaning
|
|
363
|
-
(which amount is the one that matters) still needs a person or the model.
|
|
364
|
-
- Tables with one row per entity (customer lists, asset registers) often hold totals and dates computed when the file was
|
|
365
|
-
exported; Vertiq uses them to describe, never as prediction features.
|
|
366
|
-
- `plan` and `guard` rely on uplift tasks, so they are only as good as the recorded actions and control groups; an action
|
|
367
|
-
that was never measured is excluded from plans and held by the check. `guard` does not decide on its own: it allows only
|
|
368
|
-
measured, clearly helpful actions.
|
|
369
|
-
- `menu` proposes features from a fixed set of operations; anything outside it (external data, formulas specific to a
|
|
370
|
-
sector) is added as a plugin by a person and judged by the same gate.
|
|
371
|
-
- `autotune` scores every trial on the same training entities, so many trials can still favour a lucky change; it keeps a change
|
|
372
|
-
only when most validation folds agree, and the held-out gate makes the final decision.
|
|
341
|
+
- Most benchmarks use synthetic data. They verify that the methods detect planted signal, reject noise and retain the baseline when appropriate; one public dataset (Online Retail II) has been run. None measures accuracy on a specific organisation's data.
|
|
342
|
+
- The gate requires the gain over the baseline to be clear at 90% confidence (`confidence=0.9`, the default). On small datasets this also holds back some real improvements until more data arrives: with 240 assets and a real signal, 7 of 15 candidates were promoted, against 13 of 15 with the check off. `confidence=None` turns the check off; the candidate then only has to not be worse.
|
|
343
|
+
- Plain-language descriptions (`narrate`) are written from templates by default. The trained rewrite model gives real rewrites for 27 of 50 held-out descriptions, and every rewrite is checked: each number must be the template's and stay next to what it described. The checks catch invented and moved numbers, not every misleading sentence.
|
|
344
|
+
- After validation, Vertiq LM maps 54% of held-out tables exactly (42% from domains it was not trained on) and loads 100% without errors. Proposed mappings should be reviewed before production use.
|
|
345
|
+
- Uplift estimation requires a randomised control group. Without one, `causal.matched_att` gives an estimate whose validity depends on every confounder being observed; `design.next_step` reports the experiment size needed to settle the question.
|
|
346
|
+
- On the synthetic benchmarks, boosted trees improve on linear models where effects are non-linear and do not where effects are additive. Across 100 to 6,400 entities the default tree settings were within 0.01 AUC of the best of five alternatives. `learner="auto"` chooses by 3-fold cross-validation on the training entities; with a few hundred entities or fewer its choice is close to random when the two learners are within a point or two of each other.
|
|
347
|
+
- Doubly robust estimates are consistent if either the propensity or the outcome model is right, but they vary more than the outcome model alone (standard deviation 0.17 across benchmark runs); large weights from extreme propensities are clipped and counted in the result.
|
|
348
|
+
- The rule-based mapping proposer gets 57% of roles right on the held-out real exports and Vertiq LM 83%; both are checked by validation, which flags what it cannot settle (for example two complete amount columns, or day/month order), but meaning (which amount is the one that matters) still needs a person or the model.
|
|
349
|
+
- Tables with one row per entity (customer lists, asset registers) often hold totals and dates computed when the file was exported; Vertiq uses them to describe, never as prediction features.
|
|
350
|
+
- `plan` and `guard` rely on uplift tasks, so they are only as good as the recorded actions and control groups; an action that was never measured is excluded from plans and held by the check. `guard` does not decide on its own: it allows only measured, clearly helpful actions.
|
|
351
|
+
- `menu` proposes features from a fixed set of operations; anything outside it (external data, formulas specific to a sector) is added as a plugin by a person and judged by the same gate.
|
|
352
|
+
- `autotune` scores every trial on the same training entities, so many trials can still favour a lucky change; it keeps a change only when most validation folds agree, and the held-out gate makes the final decision.
|
|
373
353
|
- Version 0.2. The API may change before 1.0; changes are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
374
354
|
|
|
375
355
|
## Roadmap
|
|
376
356
|
|
|
377
|
-
- Further public benchmarks: M5 (value prediction), NASA C-MAPSS (equipment failure),
|
|
378
|
-
|
|
379
|
-
-
|
|
380
|
-
- Vertiq LM: a training round on tables with real-export clutter (`lm/make_data.py --messy`), scored on the synthetic and the real
|
|
381
|
-
held-out sets; evaluate a 9B student and publish the adapter with a `vertiq map` command.
|
|
382
|
-
- `autotune` for value tasks and uplift; menu operations for group-level signals and external series (weather, prices) joined by
|
|
383
|
-
date and location; menu items suggested by a language model for a sector, checked by `menu.parse`.
|
|
357
|
+
- Further public benchmarks: M5 (value prediction), NASA C-MAPSS (equipment failure), KKBOX (churn), and Hillstrom and Criteo (randomised uplift); protocols in [proposal 0001](proposals/0001-statistical-validation.md), part C.
|
|
358
|
+
- Vertiq LM: a training round on tables with real-export clutter (`lm/make_data.py --messy`), scored on the synthetic and the real held-out sets; evaluate a 9B student and publish the adapter with a `vertiq map` command.
|
|
359
|
+
- `autotune` for value tasks and uplift; menu operations for group-level signals and external series (weather, prices) joined by date and location; menu items suggested by a language model for a sector, checked by `menu.parse`.
|
|
384
360
|
- Turning free-text fields (support tickets, maintenance notes) into validated events.
|
|
385
361
|
|
|
386
362
|
Further reading on recursive self-improvement is kept in [proposals/reading-list.md](proposals/reading-list.md), and an external assessment of the competitive landscape in [proposals/landscape-review.md](proposals/landscape-review.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vertiq"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Self-improving decision intelligence: a local language model that maps any operational dataset and decision models trained on real outcomes, improved through recursive self-improvement under an evaluation gate."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -99,3 +99,20 @@ def test_a_column_that_mixes_decimal_styles_reads_each_value_on_its_own():
|
|
|
99
99
|
def test_dates_with_times_are_known_formats():
|
|
100
100
|
from vertiq.mapping import date_formats
|
|
101
101
|
assert date_formats(["15/01/2023 09:45", "03/02/2023 18:10"]) == ["%d/%m/%Y %H:%M"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_short_customer_numbers_stay_the_entity_and_a_country_never_replaces_them():
|
|
105
|
+
"""Online-store exports: 5-digit customer numbers (an id, not a price) beside a country column with a few
|
|
106
|
+
dozen values. Repair keeps the customer; a column with few values is a label, not whom records are about."""
|
|
107
|
+
from vertiq.mapping import fitness, repair
|
|
108
|
+
import random
|
|
109
|
+
rnd = random.Random(3)
|
|
110
|
+
countries = [f"Country {i}" for i in range(24)]
|
|
111
|
+
rows = [{"Invoice": str(489434 + i), "Date": f"2010-0{1 + i % 9}-1{i % 10} 10:{i % 60:02d}:00",
|
|
112
|
+
"Customer ID": str(rnd.randint(12000, 18999)), "Country": rnd.choice(countries), "Total": f"{rnd.uniform(5, 900):.2f}"}
|
|
113
|
+
for i in range(600)]
|
|
114
|
+
assert fitness(rows, "Customer ID", "entity") > 0.9
|
|
115
|
+
m, notes = repair(Mapping(entity="Customer ID", time="Date", value="Total", attrs=["Invoice"]), rows, exclude={"Invoice"})
|
|
116
|
+
assert m.entity == "Customer ID", notes
|
|
117
|
+
m, notes = repair(Mapping(entity="Date", time="Date", value="Total"), rows, exclude={"Invoice"})
|
|
118
|
+
assert m.entity != "Country"
|
|
@@ -49,3 +49,11 @@ def test_nothing_is_found_in_random_data():
|
|
|
49
49
|
|
|
50
50
|
def test_small_data_waits():
|
|
51
51
|
assert patterns.find(_orders(n=12))["status"] == "learning"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_column_names_and_yes_no_flags_read_as_words():
|
|
55
|
+
assert patterns._where("payment_mode = Cash") == "where payment mode is Cash"
|
|
56
|
+
assert patterns._where("is_return = True") == "marked return"
|
|
57
|
+
assert patterns._where("no_guest = False") == "not marked no guest"
|
|
58
|
+
assert patterns._does("no_guest = True") == "are marked no guest"
|
|
59
|
+
assert patterns._does("PaymentMode = Card") == "have payment mode Card"
|
|
@@ -3,9 +3,17 @@ import csv
|
|
|
3
3
|
import json
|
|
4
4
|
import random
|
|
5
5
|
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
6
8
|
from vertiq import report
|
|
7
9
|
from vertiq.tables import find_join, propose
|
|
8
10
|
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def _no_search(monkeypatch):
|
|
14
|
+
"""The input search is slow and is tested on its own below; these tests check what the report says."""
|
|
15
|
+
monkeypatch.setattr(report, "TUNE", False)
|
|
16
|
+
|
|
9
17
|
MENU = {"Margherita": "Pizza", "Farmhouse": "Pizza", "Cola": "Drinks", "Lemonade": "Drinks", "Fries": "Sides", "Salad": "Sides"}
|
|
10
18
|
|
|
11
19
|
|
|
@@ -56,7 +64,7 @@ def test_report_runs_from_file_names_only(tmp_path):
|
|
|
56
64
|
cfg = {"records": {"file": "orders.csv"}, "items": {"file": "lines.csv"}, "noun": "orders", "currency": "₹", "event": "order"}
|
|
57
65
|
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
|
58
66
|
text = report.run(cfg, tmp_path)
|
|
59
|
-
assert "## Patterns" in text and "include Drinks" in text and "where
|
|
67
|
+
assert "## Patterns" in text and "include Drinks" in text and "where channel is Online" in text
|
|
60
68
|
assert "a summary row" in text and "placeholder entity" in text
|
|
61
69
|
assert "## Predictions" in text
|
|
62
70
|
|
|
@@ -132,7 +140,7 @@ def test_entity_table_labels_join_events_and_show_in_patterns(tmp_path):
|
|
|
132
140
|
cfg = {"records": {"file": "orders.csv"}, "entity_table": {"file": "customers.csv"}, "noun": "orders", "entities": "customers"}
|
|
133
141
|
text = report.run(cfg, tmp_path)
|
|
134
142
|
assert "entity table joined on 'Customer Id'" in text
|
|
135
|
-
assert "where
|
|
143
|
+
assert "where segment is Gold average" in text or "where segment is Basic average" in text
|
|
136
144
|
|
|
137
145
|
|
|
138
146
|
def test_a_customer_list_alone_is_read_as_one_row_per_entity(tmp_path):
|
|
@@ -140,3 +148,105 @@ def test_a_customer_list_alone_is_read_as_one_row_per_entity(tmp_path):
|
|
|
140
148
|
ds, _g, log = report.build({"records": {"file": "customers.csv"}}, tmp_path)
|
|
141
149
|
assert any("one row per entity" in x for x in log)
|
|
142
150
|
assert '"time": "Last Order"' in next(x for x in log if x.startswith("Mapping used"))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_a_product_code_beside_the_product_name_is_not_an_item_group():
|
|
154
|
+
"""Online-store lines carry a stock code for each product name. The code is the item again, not a group,
|
|
155
|
+
so patterns and combos speak in product names."""
|
|
156
|
+
from vertiq.tables import find_lines
|
|
157
|
+
rnd = random.Random(4)
|
|
158
|
+
products = [(f"{20000 + i}", f"PRODUCT NUMBER {i}") for i in range(60)]
|
|
159
|
+
rows = []
|
|
160
|
+
for inv in range(300):
|
|
161
|
+
at = f"2010-03-{1 + inv % 28:02d} 1{inv % 10}:00:00"
|
|
162
|
+
for code, name in rnd.sample(products, 3):
|
|
163
|
+
rows.append({"Invoice": str(489000 + inv), "StockCode": code, "Description": name,
|
|
164
|
+
"Quantity": str(rnd.randint(1, 12)), "InvoiceDate": at, "Price": f"{rnd.uniform(1, 9):.2f}",
|
|
165
|
+
"Customer ID": str(rnd.randint(12000, 12400))})
|
|
166
|
+
spec, _ = find_lines(rows)
|
|
167
|
+
assert spec["key"] == "Invoice" and spec["item"] in ("Description", "StockCode")
|
|
168
|
+
assert spec["item_group"] is None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _with_consent(tmp_path, allow):
|
|
172
|
+
"""The customers table with a trainable column: True for customers `allow` picks."""
|
|
173
|
+
_customers(tmp_path)
|
|
174
|
+
rows = list(csv.DictReader(open(tmp_path / "customers.csv")))
|
|
175
|
+
for r in rows:
|
|
176
|
+
r["trainable"] = "True" if allow(r["Customer Id"]) else "False"
|
|
177
|
+
with open(tmp_path / "customers.csv", "w", newline="") as f:
|
|
178
|
+
w = csv.DictWriter(f, fieldnames=list(rows[0]))
|
|
179
|
+
w.writeheader()
|
|
180
|
+
w.writerows(rows)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_no_model_trains_on_entities_that_may_not_be_used(tmp_path):
|
|
184
|
+
_with_consent(tmp_path, lambda cid: False)
|
|
185
|
+
cfg = {"records": {"file": "orders.csv"}, "entity_table": {"file": "customers.csv", "trainable": "trainable"}, "noun": "orders"}
|
|
186
|
+
text = report.run(cfg, tmp_path)
|
|
187
|
+
assert "No model was trained: none of the" in text and "column 'trainable'" in text
|
|
188
|
+
assert "was promoted" not in text and "## Patterns" in text # description still runs
|
|
189
|
+
assert "No model was trained" in report.run({"records": {"file": "orders.csv"}, "train": False}, tmp_path)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_models_use_only_trainable_entities(tmp_path, monkeypatch):
|
|
193
|
+
_with_consent(tmp_path, lambda cid: int(cid[-1]) % 2 == 0 if cid[-1].isdigit() else False)
|
|
194
|
+
seen = []
|
|
195
|
+
real = report.predict_event
|
|
196
|
+
|
|
197
|
+
def spy(ds, **kw):
|
|
198
|
+
seen.append({e.entity for e in ds.events})
|
|
199
|
+
return real(ds, **kw)
|
|
200
|
+
monkeypatch.setattr(report, "predict_event", spy)
|
|
201
|
+
cfg = {"records": {"file": "orders.csv"}, "entity_table": {"file": "customers.csv", "trainable": "trainable"}, "noun": "orders"}
|
|
202
|
+
text = report.run(cfg, tmp_path)
|
|
203
|
+
assert "Models use only the" in text
|
|
204
|
+
assert seen and all(int(x[-1]) % 2 == 0 for x in seen[0])
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_promoted_models_score_every_entity(tmp_path, monkeypatch):
|
|
208
|
+
"""Scores come only from promoted models; a kept model gives none."""
|
|
209
|
+
_customers(tmp_path)
|
|
210
|
+
|
|
211
|
+
class FakeTask:
|
|
212
|
+
def __init__(self, kind, status):
|
|
213
|
+
self.kind, self.status, self.window = kind, status, 30
|
|
214
|
+
self.b = type("B", (), {"target": "order", "by": {"C1": [], "C2": [], "anonymous:7": []}})()
|
|
215
|
+
|
|
216
|
+
def report(self):
|
|
217
|
+
return {"status": "learning", "rows": 0, "need_rows": 200, "task": self.kind, "target": "order", "window_days": 30}
|
|
218
|
+
|
|
219
|
+
def score(self, eid):
|
|
220
|
+
return {"entity": eid, "value": 100.0 if eid == "C1" else 50.0, "raises": ["recent orders"], "lowers": []}
|
|
221
|
+
|
|
222
|
+
monkeypatch.setattr(report, "predict_event", lambda ds, **kw: FakeTask("event", "kept"))
|
|
223
|
+
monkeypatch.setattr(report, "predict_value", lambda ds, **kw: FakeTask("value", "trained"))
|
|
224
|
+
text = report.run({"records": {"file": "orders.csv"}, "noun": "orders", "currency": "₹", "scores": "scores.csv"}, tmp_path)
|
|
225
|
+
rows = list(csv.DictReader(open(tmp_path / "scores.csv")))
|
|
226
|
+
assert [r["entity"] for r in rows] == ["C1", "C2"] and all(r["model"] == "value of the next order" for r in rows)
|
|
227
|
+
assert "- C1: ₹100 (raised by recent orders)" in text
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_items_written_in_one_cell_become_line_items(tmp_path):
|
|
231
|
+
"""A POS "products" column ("Shirtx999,Tshirtx699") is read as line items when there is no items table."""
|
|
232
|
+
from vertiq.tables import find_item_text, item_tokens
|
|
233
|
+
assert item_tokens("Shirtx999,Tshirtx199ND; XYXX-TRUNK-L") == [("Shirt", 999.0), ("Tshirt", 199.0), ("XYXX-TRUNK-L", None)]
|
|
234
|
+
rnd = random.Random(8)
|
|
235
|
+
goods = ["Shirtx999", "Jeansx1499", "Tshirtx399", "Trouserx799", "Lowerx599"]
|
|
236
|
+
rows = [{"Bill": f"B{i:05d}", "Guest": f"G{rnd.randint(1, 120):04d}", "Date": f"2026-0{1 + i % 9}-{1 + i % 28:02d}",
|
|
237
|
+
"Amount": str(rnd.randint(300, 3000)), "Products": ",".join(rnd.sample(goods, rnd.randint(1, 3))),
|
|
238
|
+
"Remarks": f"note {i}"} for i in range(400)]
|
|
239
|
+
col, why = find_item_text(rows)
|
|
240
|
+
assert col == "Products" and "lists" in why[0]
|
|
241
|
+
with open(tmp_path / "bills.csv", "w", newline="") as f:
|
|
242
|
+
w = csv.DictWriter(f, fieldnames=list(rows[0]))
|
|
243
|
+
w.writeheader()
|
|
244
|
+
w.writerows(rows)
|
|
245
|
+
text = report.run({"records": {"file": "bills.csv"}, "noun": "bills"}, tmp_path)
|
|
246
|
+
assert "items written in one cell: column 'Products'" in text and "## Items bought together" in text
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_search_inputs_read_as_words():
|
|
250
|
+
from vertiq.menu import words
|
|
251
|
+
assert words("share:bill[@item=Jeans]@180d") == "share of bills including Jeans over 180 days"
|
|
252
|
+
assert words("since:bill[payment_mode=Cash]") == "time since the last bill with payment mode Cash"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The gate's segment check: a model that wins on average but is clearly worse for one segment is not promoted,
|
|
2
|
+
and checking many segments does not block a model that is better everywhere."""
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
from vertiq import harness, narrate, plugins
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _data(harm: bool, seed: int = 3, n_entities: int = 1500, extra_dims: int = 0):
|
|
9
|
+
"""Entities in segment A (85%) or B. The outcome follows x in A; in B it follows x too, unless `harm`, when it
|
|
10
|
+
follows -x. The baseline knows nothing in A and, under `harm`, is right in B. A model trained mostly on A
|
|
11
|
+
learns x, so under `harm` it wins on average and loses in B."""
|
|
12
|
+
rnd = random.Random(seed)
|
|
13
|
+
rows, y, keys, base, segs = [], [], [], [], []
|
|
14
|
+
for e in range(n_entities):
|
|
15
|
+
seg = "B" if rnd.random() < 0.15 else "A"
|
|
16
|
+
extra = {f"dim{d}": rnd.choice(["p", "q", "r"]) for d in range(extra_dims)}
|
|
17
|
+
for _cut in range(2):
|
|
18
|
+
x = rnd.gauss(0, 1)
|
|
19
|
+
sign = -1 if (harm and seg == "B") else 1
|
|
20
|
+
outcome = 1 if sign * x + rnd.gauss(0, 0.6) > 0 else 0
|
|
21
|
+
rows.append({"x": x})
|
|
22
|
+
y.append(outcome)
|
|
23
|
+
keys.append(f"E{e}")
|
|
24
|
+
base.append(-x if (harm and seg == "B") else rnd.random())
|
|
25
|
+
segs.append({"segment": seg, **extra})
|
|
26
|
+
return rows, y, keys, base, segs
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _gate(harm: bool, **kw):
|
|
30
|
+
rows, y, keys, base, segs = _data(harm, **kw)
|
|
31
|
+
return harness.evaluate(rows, y, keys, [""] * len(y), base, plugins.learner("linear", "classify"), kind="classify",
|
|
32
|
+
segments=segs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_a_model_that_harms_one_segment_is_not_promoted():
|
|
36
|
+
res = _gate(harm=True)
|
|
37
|
+
b = next(s for s in res["metrics"]["segments"] if s["label"] == "B")
|
|
38
|
+
assert res["metrics"]["auc"] > res["metrics"]["baseline_auc"] # it wins on average ...
|
|
39
|
+
assert b["worse"] and res["status"] == "kept" # ... and loses in B, so it is kept
|
|
40
|
+
assert "segment B" in res["reason"]
|
|
41
|
+
text = narrate.gate({"status": res["status"], "metrics": res["metrics"], "reason": res["reason"],
|
|
42
|
+
"task": "event", "target": "purchase", "window_days": 30})
|
|
43
|
+
assert "clearly worse than the baseline rule for segment B" in text
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_checking_many_segments_does_not_block_a_model_better_everywhere():
|
|
47
|
+
res = _gate(harm=False, extra_dims=6)
|
|
48
|
+
assert res["status"] == "trained"
|
|
49
|
+
assert len(res["metrics"]["segments"]) >= 10 and not any(s["worse"] for s in res["metrics"]["segments"])
|
|
50
|
+
text = narrate.gate({"status": "trained", "metrics": res["metrics"], "task": "event", "target": "purchase", "window_days": 30})
|
|
51
|
+
assert "not clearly worse than the baseline rule in any of them" in text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_small_segments_are_not_judged():
|
|
55
|
+
rows, y, keys, base, segs = _data(harm=False, n_entities=1500)
|
|
56
|
+
segs = [dict(s, tiny="rare" if k in ("E1", "E2", "E3") else "") for s, k in zip(segs, keys)]
|
|
57
|
+
res = harness.evaluate(rows, y, keys, [""] * len(y), base, plugins.learner("linear", "classify"), kind="classify", segments=segs)
|
|
58
|
+
assert not any(s["dimension"] == "tiny" for s in res["metrics"]["segments"])
|
|
@@ -7,11 +7,14 @@ try next (cross-sell), and items people buy again on a cycle (refills).
|
|
|
7
7
|
refills items bought again on a steady cycle, and who is due now
|
|
8
8
|
|
|
9
9
|
Lift = how many times more often two things go together than if choices were independent:
|
|
10
|
-
1.0 is chance, 2.0 is twice as often. Only pairs seen at least MIN_PAIR times
|
|
11
|
-
|
|
10
|
+
1.0 is chance, 2.0 is twice as often. Only pairs seen at least MIN_PAIR times, and on at least 1 in 1,000
|
|
11
|
+
bills (or customers) in large data, count, so a single big order or two rare items never make a pattern.
|
|
12
|
+
Combos rank by how often the pair occurs times log2 of its lift: frequent strong pairs first, not the
|
|
13
|
+
rarest. Plain data in: [(customer id, bill id, when, item name)].
|
|
12
14
|
"""
|
|
13
15
|
from __future__ import annotations
|
|
14
16
|
|
|
17
|
+
import math
|
|
15
18
|
from collections import Counter, defaultdict
|
|
16
19
|
from datetime import datetime, timedelta
|
|
17
20
|
from itertools import combinations
|
|
@@ -56,8 +59,8 @@ def analyse(lines, min_pair: int = MIN_PAIR) -> dict:
|
|
|
56
59
|
pair_cust.update(combinations(sorted(items)[:MAX_ITEMS_PER_BILL * 3], 2))
|
|
57
60
|
result["top_items"] = [{"item": i, "customers": c, "share": round(100 * c / n_cust, 1)}
|
|
58
61
|
for i, c in item_cust.most_common(8)]
|
|
59
|
-
result["combos"] = _combos(pair_bills, item_bills, n_bills, min_pair)
|
|
60
|
-
lifts, cross = _cross_sell(pair_cust, item_cust, n_cust, bought, min_pair)
|
|
62
|
+
result["combos"] = _combos(pair_bills, item_bills, n_bills, max(min_pair, math.ceil(n_bills / 1000)))
|
|
63
|
+
lifts, cross = _cross_sell(pair_cust, item_cust, n_cust, bought, max(min_pair, math.ceil(n_cust / 1000)))
|
|
61
64
|
result["cross_sell"], result["_lifts"] = cross, lifts
|
|
62
65
|
result["_bought"] = {cid: {i: max(ds) for i, ds in items.items()} for cid, items in bought.items()}
|
|
63
66
|
result["refills"], result["_cycles"] = _refills(bought)
|
|
@@ -75,7 +78,7 @@ def _combos(pair_bills: Counter, item_bills: Counter, n: int, min_pair: int = 5)
|
|
|
75
78
|
out.append({"items": [a, b], "bills_together": together, "lift": round(lift, 2),
|
|
76
79
|
"a_then_b": round(100 * together / item_bills[a], 1),
|
|
77
80
|
"b_then_a": round(100 * together / item_bills[b], 1)})
|
|
78
|
-
out.sort(key=lambda c: (-c["
|
|
81
|
+
out.sort(key=lambda c: (-c["bills_together"] * math.log2(c["lift"]), c["items"]))
|
|
79
82
|
return out[:10]
|
|
80
83
|
|
|
81
84
|
|