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.
Files changed (79) hide show
  1. {vertiq-0.2.0/vertiq.egg-info → vertiq-0.2.2}/PKG-INFO +20 -44
  2. {vertiq-0.2.0 → vertiq-0.2.2}/README.md +19 -43
  3. {vertiq-0.2.0 → vertiq-0.2.2}/pyproject.toml +1 -1
  4. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_mapping.py +17 -0
  5. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_patterns.py +8 -0
  6. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_report.py +112 -2
  7. vertiq-0.2.2/tests/test_segments.py +58 -0
  8. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/__init__.py +1 -1
  9. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/affinity.py +8 -5
  10. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/autotune.py +78 -37
  11. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/harness.py +61 -2
  12. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/learn.py +20 -6
  13. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/mapping.py +23 -3
  14. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/menu.py +47 -2
  15. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/narrate.py +15 -0
  16. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/patterns.py +94 -29
  17. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/report.py +122 -6
  18. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/tables.py +65 -12
  19. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/tasks.py +90 -17
  20. {vertiq-0.2.0 → vertiq-0.2.2/vertiq.egg-info}/PKG-INFO +20 -44
  21. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/SOURCES.txt +1 -0
  22. {vertiq-0.2.0 → vertiq-0.2.2}/LICENSE +0 -0
  23. {vertiq-0.2.0 → vertiq-0.2.2}/setup.cfg +0 -0
  24. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_autotune.py +0 -0
  25. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_boost.py +0 -0
  26. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_boundary.py +0 -0
  27. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_category.py +0 -0
  28. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_context.py +0 -0
  29. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_core.py +0 -0
  30. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_design.py +0 -0
  31. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_diagnose.py +0 -0
  32. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_drift.py +0 -0
  33. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_guard.py +0 -0
  34. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_guides.py +0 -0
  35. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_mapping_messy.py +0 -0
  36. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_menu.py +0 -0
  37. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_narrate.py +0 -0
  38. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_plan.py +0 -0
  39. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_public_benchmark.py +0 -0
  40. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_sequences_profiles.py +0 -0
  41. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_statistics.py +0 -0
  42. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_summary.py +0 -0
  43. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_survival.py +0 -0
  44. {vertiq-0.2.0 → vertiq-0.2.2}/tests/test_tasks.py +0 -0
  45. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/actions.py +0 -0
  46. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/boost.py +0 -0
  47. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/category.py +0 -0
  48. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/causal.py +0 -0
  49. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/checkout.py +0 -0
  50. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/churn.py +0 -0
  51. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/context.py +0 -0
  52. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/contract.py +0 -0
  53. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/dataset.py +0 -0
  54. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/design.py +0 -0
  55. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/diagnose.py +0 -0
  56. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/drift.py +0 -0
  57. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/experiments.py +0 -0
  58. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/features.py +0 -0
  59. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/guard.py +0 -0
  60. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/insights.py +0 -0
  61. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/inventory.py +0 -0
  62. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/offers.py +0 -0
  63. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/plan.py +0 -0
  64. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/plugins.py +0 -0
  65. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/profiles.py +0 -0
  66. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/research.py +0 -0
  67. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/rules.py +0 -0
  68. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/sequences.py +0 -0
  69. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/sequential.py +0 -0
  70. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/summary.py +0 -0
  71. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/survival.py +0 -0
  72. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/trends.py +0 -0
  73. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/uplift.py +0 -0
  74. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/value.py +0 -0
  75. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq/verticals.py +0 -0
  76. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/dependency_links.txt +0 -0
  77. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/entry_points.txt +0 -0
  78. {vertiq-0.2.0 → vertiq-0.2.2}/vertiq.egg-info/requires.txt +0 -0
  79. {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.0
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 196 seconds on a laptop using 4 cores.
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
- 196 seconds on a laptop using 4 cores, arm64, Python 3.10.10).
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 196 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
+ 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
- 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
373
- also holds back some real improvements until more data arrives: with 240 assets and a real signal, 7 of 15 candidates were
374
- promoted, against 13 of 15 with the check off. `confidence=None` turns the check off; the candidate then only has to not be worse.
375
- - Plain-language descriptions (`narrate`) are written from templates. A local model can rewrite them, and a rewrite is kept only
376
- if every number in it appears in the template and it is no longer. On 17 descriptions, Qwen 3.5 4B given the full result
377
- attached real numbers to the wrong quantity in 4; given only the template, its accepted rewrites were close paraphrases, and
378
- Qwen 3.5 9B returned the template unchanged. Fluent rewrites would need fine-tuning on reviewed examples.
379
- - After validation, Vertiq LM maps 54% of held-out tables exactly (42% from domains it was
380
- not trained on) and loads 100%
381
- without errors. Proposed mappings should be reviewed before production use.
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
- KKBOX (churn), and Hillstrom and Criteo (randomised uplift); protocols in [proposal 0001](proposals/0001-statistical-validation.md), part C.
409
- - Distribution on PyPI.
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 196 seconds on a laptop using 4 cores.
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
- 196 seconds on a laptop using 4 cores, arm64, Python 3.10.10).
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 196 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
+ 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
- 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
343
- also holds back some real improvements until more data arrives: with 240 assets and a real signal, 7 of 15 candidates were
344
- promoted, against 13 of 15 with the check off. `confidence=None` turns the check off; the candidate then only has to not be worse.
345
- - Plain-language descriptions (`narrate`) are written from templates. A local model can rewrite them, and a rewrite is kept only
346
- if every number in it appears in the template and it is no longer. On 17 descriptions, Qwen 3.5 4B given the full result
347
- attached real numbers to the wrong quantity in 4; given only the template, its accepted rewrites were close paraphrases, and
348
- Qwen 3.5 9B returned the template unchanged. Fluent rewrites would need fine-tuning on reviewed examples.
349
- - After validation, Vertiq LM maps 54% of held-out tables exactly (42% from domains it was
350
- not trained on) and loads 100%
351
- without errors. Proposed mappings should be reviewed before production use.
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
- KKBOX (churn), and Hillstrom and Criteo (randomised uplift); protocols in [proposal 0001](proposals/0001-statistical-validation.md), part C.
379
- - Distribution on PyPI.
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.0"
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 Channel is Online" in text
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 Segment is Gold average" in text or "where Segment is Basic average" in text
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"])
@@ -20,4 +20,4 @@ it likes.
20
20
  experiments comparing message variants against each other and a control group
21
21
  verticals per-vertical settings (windows, nouns, currency, tactics)
22
22
  """
23
- __version__ = "0.2.0"
23
+ __version__ = "0.2.2"
@@ -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 count, so a single
11
- big order never makes a pattern. Plain data in: [(customer id, bill id, when, item name)].
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["lift"] * min(c["bills_together"], 50), c["items"]))
81
+ out.sort(key=lambda c: (-c["bills_together"] * math.log2(c["lift"]), c["items"]))
79
82
  return out[:10]
80
83
 
81
84