vertiq 0.2.2__tar.gz → 0.2.3__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.2/vertiq.egg-info → vertiq-0.2.3}/PKG-INFO +15 -1
- {vertiq-0.2.2 → vertiq-0.2.3}/README.md +14 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/pyproject.toml +1 -1
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_report.py +59 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/__init__.py +1 -1
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/patterns.py +10 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/report.py +68 -4
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/tables.py +56 -6
- {vertiq-0.2.2 → vertiq-0.2.3/vertiq.egg-info}/PKG-INFO +15 -1
- {vertiq-0.2.2 → vertiq-0.2.3}/LICENSE +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/setup.cfg +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_autotune.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_boost.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_boundary.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_category.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_context.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_core.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_design.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_diagnose.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_drift.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_guard.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_guides.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_mapping.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_mapping_messy.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_menu.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_narrate.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_patterns.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_plan.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_public_benchmark.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_segments.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_sequences_profiles.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_statistics.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_summary.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_survival.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/tests/test_tasks.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/actions.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/affinity.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/autotune.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/boost.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/category.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/causal.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/checkout.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/churn.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/context.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/contract.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/dataset.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/design.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/diagnose.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/drift.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/experiments.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/features.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/guard.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/harness.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/insights.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/inventory.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/learn.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/mapping.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/menu.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/narrate.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/offers.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/plan.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/plugins.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/profiles.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/research.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/rules.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/sequences.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/sequential.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/summary.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/survival.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/tasks.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/trends.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/uplift.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/value.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq/verticals.py +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq.egg-info/SOURCES.txt +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq.egg-info/dependency_links.txt +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq.egg-info/entry_points.txt +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/vertiq.egg-info/requires.txt +0 -0
- {vertiq-0.2.2 → vertiq-0.2.3}/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.3
|
|
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
|
|
@@ -298,6 +298,20 @@ Recency baseline AUC 0.761.
|
|
|
298
298
|
| Auto (chose boosted) | 0.812 | 0.795–0.827 | +0.043 to +0.059 |
|
|
299
299
|
| scikit-learn logistic regression (reference) | 0.807 | | |
|
|
300
300
|
|
|
301
|
+
### Real data across sectors
|
|
302
|
+
|
|
303
|
+
`vertiq-report` on public data from 5 sectors, nothing tuned per sector; windows of 30, 60 and 90 days, each through the
|
|
304
|
+
gate with its confidence corrected for the three, and its segment check (`benchmarks/sectors/fetch.sh`, then
|
|
305
|
+
`python benchmarks/sectors/run.py`; full reports in `benchmarks/sectors/reports/`).
|
|
306
|
+
|
|
307
|
+
| Sector | Data | Size | Question | Result | Patterns kept |
|
|
308
|
+
|---|---|---|---|---|---|
|
|
309
|
+
| Retail | UCI Online Retail II: a UK online shop's invoice lines, 2009-2010 | 28,816 records, 9,612 entities | Will a customer buy again, and how much? | Promoted: 30 days, AUC 0.881 vs 0.776; 60 days, AUC 0.888 vs 0.750; 90 days, AUC 0.890 vs 0.708 | 20 of 471 |
|
|
310
|
+
| Food safety | City of Chicago food inspections since 2019 (up to 150,000) | 137,034 records, 27,264 entities | Which establishments will fail an inspection? | Promoted: 30 days, AUC 0.813 vs 0.698; 60 days, AUC 0.822 vs 0.703; 90 days, AUC 0.809 vs 0.692 | 8 of 94 |
|
|
311
|
+
| Animal services | Austin Animal Center intakes (up to 200,000) | 173,812 records, 156,287 entities | Which animals will come back to the shelter? | Not promoted: best AUC 0.870 vs 0.865; the current rule stays | 20 of 266 |
|
|
312
|
+
| Manufacturing | Microsoft Azure predictive maintenance sample: 100 machines, 2015 (errors, failures, machine models) | 4,680 records, 100 entities | Which machines will fail? | Promoted: 60 days, AUC 0.720 vs 0.545. Kept the rule: 30 days, 90 days | 7 of 221 |
|
|
313
|
+
| Transport | NYC motor vehicle collisions since January 2025 (up to 200,000) | 121,970 records, 43,061 entities | Where will collisions happen again? | Promoted: 30 days, AUC 0.971 vs 0.894; 60 days, AUC 0.958 vs 0.859; 90 days, AUC 0.950 vs 0.840. Next value promoted: average error 0.44 vs 0.48 | 5 of 86 |
|
|
314
|
+
|
|
301
315
|
### Real exports (held out)
|
|
302
316
|
|
|
303
317
|
Six public exports from different sectors, never used for training; a role is correct when it is one of the accepted answers
|
|
@@ -268,6 +268,20 @@ Recency baseline AUC 0.761.
|
|
|
268
268
|
| Auto (chose boosted) | 0.812 | 0.795–0.827 | +0.043 to +0.059 |
|
|
269
269
|
| scikit-learn logistic regression (reference) | 0.807 | | |
|
|
270
270
|
|
|
271
|
+
### Real data across sectors
|
|
272
|
+
|
|
273
|
+
`vertiq-report` on public data from 5 sectors, nothing tuned per sector; windows of 30, 60 and 90 days, each through the
|
|
274
|
+
gate with its confidence corrected for the three, and its segment check (`benchmarks/sectors/fetch.sh`, then
|
|
275
|
+
`python benchmarks/sectors/run.py`; full reports in `benchmarks/sectors/reports/`).
|
|
276
|
+
|
|
277
|
+
| Sector | Data | Size | Question | Result | Patterns kept |
|
|
278
|
+
|---|---|---|---|---|---|
|
|
279
|
+
| Retail | UCI Online Retail II: a UK online shop's invoice lines, 2009-2010 | 28,816 records, 9,612 entities | Will a customer buy again, and how much? | Promoted: 30 days, AUC 0.881 vs 0.776; 60 days, AUC 0.888 vs 0.750; 90 days, AUC 0.890 vs 0.708 | 20 of 471 |
|
|
280
|
+
| Food safety | City of Chicago food inspections since 2019 (up to 150,000) | 137,034 records, 27,264 entities | Which establishments will fail an inspection? | Promoted: 30 days, AUC 0.813 vs 0.698; 60 days, AUC 0.822 vs 0.703; 90 days, AUC 0.809 vs 0.692 | 8 of 94 |
|
|
281
|
+
| Animal services | Austin Animal Center intakes (up to 200,000) | 173,812 records, 156,287 entities | Which animals will come back to the shelter? | Not promoted: best AUC 0.870 vs 0.865; the current rule stays | 20 of 266 |
|
|
282
|
+
| Manufacturing | Microsoft Azure predictive maintenance sample: 100 machines, 2015 (errors, failures, machine models) | 4,680 records, 100 entities | Which machines will fail? | Promoted: 60 days, AUC 0.720 vs 0.545. Kept the rule: 30 days, 90 days | 7 of 221 |
|
|
283
|
+
| Transport | NYC motor vehicle collisions since January 2025 (up to 200,000) | 121,970 records, 43,061 entities | Where will collisions happen again? | Promoted: 30 days, AUC 0.971 vs 0.894; 60 days, AUC 0.958 vs 0.859; 90 days, AUC 0.950 vs 0.840. Next value promoted: average error 0.44 vs 0.48 | 5 of 86 |
|
|
284
|
+
|
|
271
285
|
### Real exports (held out)
|
|
272
286
|
|
|
273
287
|
Six public exports from different sectors, never used for training; a role is correct when it is one of the accepted answers
|
|
@@ -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.3"
|
|
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"
|
|
@@ -250,3 +250,62 @@ def test_search_inputs_read_as_words():
|
|
|
250
250
|
from vertiq.menu import words
|
|
251
251
|
assert words("share:bill[@item=Jeans]@180d") == "share of bills including Jeans over 180 days"
|
|
252
252
|
assert words("since:bill[payment_mode=Cash]") == "time since the last bill with payment mode Cash"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _write_csv(path, rows):
|
|
256
|
+
with open(path, "w", newline="") as f:
|
|
257
|
+
w = csv.DictWriter(f, fieldnames=list(rows[0]))
|
|
258
|
+
w.writeheader()
|
|
259
|
+
w.writerows(rows)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _machines(tmp_path):
|
|
263
|
+
"""100 machines numbered 1 to 100 (small whole numbers), an error log and a failure log, and a machine list."""
|
|
264
|
+
rnd = random.Random(9)
|
|
265
|
+
errors, failures = [], []
|
|
266
|
+
for m in range(1, 101):
|
|
267
|
+
for _ in range(rnd.randint(10, 40)):
|
|
268
|
+
errors.append({"datetime": f"2015-{rnd.randint(1, 12):02d}-{rnd.randint(1, 28):02d} 0{rnd.randint(0, 9)}:00:00",
|
|
269
|
+
"machineID": str(m), "errorID": rnd.choice(["error1", "error2", "error3"])})
|
|
270
|
+
for _ in range(rnd.randint(2, 9)):
|
|
271
|
+
failures.append({"datetime": f"2015-{rnd.randint(1, 12):02d}-{rnd.randint(1, 28):02d} 06:00:00",
|
|
272
|
+
"machineID": str(m), "failure": rnd.choice(["comp1", "comp2", "comp3"])})
|
|
273
|
+
_write_csv(tmp_path / "errors.csv", errors)
|
|
274
|
+
_write_csv(tmp_path / "failures.csv", failures)
|
|
275
|
+
_write_csv(tmp_path / "machines.csv", [{"machineID": str(m), "model": f"model{m % 4 + 1}", "age": str(m % 20)} for m in range(1, 101)])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_several_event_files_merge_on_a_small_number_id(tmp_path):
|
|
279
|
+
_machines(tmp_path)
|
|
280
|
+
cfg = {"records": [{"file": "errors.csv", "event": "error"}, {"file": "failures.csv", "event": "failure"}],
|
|
281
|
+
"entity_table": {"file": "machines.csv"}, "target": "failure", "noun": "events", "entities": "machines"}
|
|
282
|
+
collect = {}
|
|
283
|
+
text = report.run(cfg, tmp_path, collect)
|
|
284
|
+
assert "entity 'machineID'" in text and "Merged:" in text and collect["entities"] == 100
|
|
285
|
+
assert any(p["target"] == "failure" for p in collect["predictions"])
|
|
286
|
+
# a failure code exists only on failures: no "failure is comp1 ... have type error" patterns
|
|
287
|
+
assert not any("have type" in f and "failure is" in f for f in collect["patterns"]["found"])
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_identifiers_are_found_by_name_and_never_taken_as_amounts():
|
|
291
|
+
from vertiq.tables import id_named, id_worded, propose
|
|
292
|
+
assert id_named("machineID") and id_named("customer_id") and id_named("Customer ID") and not id_named("paid")
|
|
293
|
+
assert id_worded("license_") and id_worded("Invoice No") and not id_worded("number_of_persons_injured")
|
|
294
|
+
rnd = random.Random(2)
|
|
295
|
+
rows = [{"date": f"2024-{rnd.randint(1, 12):02d}-{rnd.randint(1, 28):02d}", "store_id": str(rnd.randint(1, 40)),
|
|
296
|
+
"license_": str(rnd.randint(1, 2_500_000)), "amount": f"{rnd.uniform(5, 500):.2f}"} for _ in range(800)]
|
|
297
|
+
m, _ = propose(rows)
|
|
298
|
+
assert m.entity == "store_id" and m.value == "amount"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_an_outcome_named_by_value_sets_the_event_type(tmp_path):
|
|
302
|
+
rnd = random.Random(4)
|
|
303
|
+
rows = [{"inspection_id": str(100000 + i), "establishment": f"Shop {rnd.randint(1, 150)}",
|
|
304
|
+
"inspection_date": f"202{rnd.randint(1, 4)}-{rnd.randint(1, 12):02d}-{rnd.randint(1, 28):02d}",
|
|
305
|
+
"results": rnd.choice(["Pass", "Pass", "Fail", "Pass w/ Conditions"])} for i in range(1500)]
|
|
306
|
+
_write_csv(tmp_path / "inspections.csv", rows)
|
|
307
|
+
collect = {}
|
|
308
|
+
text = report.run({"records": {"file": "inspections.csv"}, "target": "Fail", "noun": "inspections"}, tmp_path, collect)
|
|
309
|
+
assert "'Fail' is a value of 'results'" in text
|
|
310
|
+
assert all(p["target"] == "Fail" for p in collect["predictions"])
|
|
311
|
+
assert not any("results is" in f for f in collect["patterns"]["found"]) # the type column is not also an attribute
|
|
@@ -157,10 +157,20 @@ def find(ds: Dataset, *, item_groups: dict[str, str] | None = None, noun: str =
|
|
|
157
157
|
tests.append({"kind": "value", "feature": k[1], "n_with": a[0], "n_without": b[0],
|
|
158
158
|
"with": ma, "without": mb, "p": _mean_p_sums(a, b),
|
|
159
159
|
"big": abs(ma - mb) >= min_value_gap * abs(overall)})
|
|
160
|
+
# an attribute recorded on one event type only (a failure code exists only on failures) says nothing new
|
|
161
|
+
# about the type: comparing the two would report how the data was exported, not a pattern
|
|
162
|
+
types_of: dict[str, set] = defaultdict(set)
|
|
163
|
+
for e in events:
|
|
164
|
+
for k, v in (e.attrs or {}).items():
|
|
165
|
+
if v not in (None, "", "-"):
|
|
166
|
+
types_of[f"attr:{k}"].add(e.type)
|
|
167
|
+
one_type = {src for src, ts in types_of.items() if len(ts) == 1 and len({e.type for e in events}) > 1}
|
|
160
168
|
for i, ka in enumerate(keys):
|
|
161
169
|
for kb in keys[i + 1:]:
|
|
162
170
|
if ka[0] == kb[0]:
|
|
163
171
|
continue # levels of one attribute exclude each other by construction
|
|
172
|
+
if {ka[0], kb[0]} & one_type and "type" in (ka[0], kb[0]):
|
|
173
|
+
continue
|
|
164
174
|
if ka[0].startswith("item:") and kb[0].startswith("item:"):
|
|
165
175
|
continue # items bought together: vertiq.affinity, with its own thresholds
|
|
166
176
|
# context (weekday, part of day, group) is the condition; otherwise the first feature is
|
|
@@ -12,6 +12,13 @@ config.json names the tables and how they map to the contract; nothing else is c
|
|
|
12
12
|
"entity_table": {"file": "customers.csv", "trainable": "trainable"},
|
|
13
13
|
"noun": "orders", "currency": "₹", "entities": "customers"}
|
|
14
14
|
|
|
15
|
+
"records" can also list several event files about the same entities, each named by its "event" (default: the
|
|
16
|
+
file name), with "target" naming the event to predict. "target" may also be an outcome as written in a column ("Fail" in a
|
|
17
|
+
results column); that column then gives each record's type:
|
|
18
|
+
|
|
19
|
+
{"records": [{"file": "errors.csv", "event": "error"}, {"file": "failures.csv", "event": "failure"}],
|
|
20
|
+
"entity_table": {"file": "machines.csv"}, "target": "failure", "noun": "events", "entities": "machines"}
|
|
21
|
+
|
|
15
22
|
Predictions try return windows of 30, 60 and 90 days ("windows": [...] or "window_days": N to change), with the
|
|
16
23
|
gate's confidence corrected for the number tried, and search the inputs vertiq.menu proposes from the data
|
|
17
24
|
("tune": false to skip). A column listing each record's items in one cell is read as line items when no items
|
|
@@ -107,10 +114,44 @@ def read_table(path: str | Path) -> list[dict]:
|
|
|
107
114
|
return [dict(zip(head, r + [""] * (len(head) - len(r)))) for r in rows[at + 1:]]
|
|
108
115
|
|
|
109
116
|
|
|
117
|
+
def _build_many(config: dict, base: Path) -> tuple[object, dict, list[str]]:
|
|
118
|
+
"""Several event files about the same entities (a machine's error log and its failure records, a customer's
|
|
119
|
+
orders and returns): each file is read and mapped on its own, its events named after the file (or its
|
|
120
|
+
"event"), and the files merged on the entity; an entity table joins once, at the end."""
|
|
121
|
+
from .contract import Dataset, Entity
|
|
122
|
+
ents: dict[str, Entity] = {}
|
|
123
|
+
events, groups, log = [], {}, []
|
|
124
|
+
for r in config["records"]:
|
|
125
|
+
name = r.get("event") or Path(r["file"]).stem
|
|
126
|
+
sub = {**config, "records": r, "entity_table": None, "_entity_table": config.get("entity_table"), "event": name}
|
|
127
|
+
ds, g, lg = build(sub, base)
|
|
128
|
+
log += [f"{name}: {x}" for x in lg]
|
|
129
|
+
if ds is None:
|
|
130
|
+
continue
|
|
131
|
+
for e in ds.entities:
|
|
132
|
+
old = ents.get(e.id)
|
|
133
|
+
ents[e.id] = e if old is None else Entity(e.id, old.group or e.group, {**(e.attrs or {}), **(old.attrs or {})})
|
|
134
|
+
events += ds.events
|
|
135
|
+
groups.update(g)
|
|
136
|
+
if not events:
|
|
137
|
+
return None, {}, log
|
|
138
|
+
ds = Dataset([ents[k] for k in sorted(ents)], sorted(events, key=lambda e: e.at))
|
|
139
|
+
log.append(f"Merged: {len(events):,} events of {len({e.type for e in events})} types from {len(config['records'])} files")
|
|
140
|
+
if config.get("entity_table"):
|
|
141
|
+
from .tables import attach_entities
|
|
142
|
+
et = config["entity_table"]
|
|
143
|
+
ds, more, _labels = attach_entities(ds, read_table(base / et["file"]), et.get("key"))
|
|
144
|
+
log += [f"Entities: {n}" for n in more]
|
|
145
|
+
return ds, groups, log
|
|
146
|
+
|
|
147
|
+
|
|
110
148
|
def build(config: dict, base: Path = Path(".")) -> tuple[object, dict, list[str]]:
|
|
111
149
|
"""The dataset, item groups and loading notes from a config. Without a mapping, Vertiq proposes one from
|
|
112
|
-
the values (vertiq.tables.propose); without join columns, it finds them (vertiq.tables.find_join).
|
|
150
|
+
the values (vertiq.tables.propose); without join columns, it finds them (vertiq.tables.find_join).
|
|
151
|
+
"records" may also be a list of event files about the same entities (see _build_many)."""
|
|
113
152
|
from dataclasses import replace
|
|
153
|
+
if isinstance(config["records"], list):
|
|
154
|
+
return _build_many(config, base)
|
|
114
155
|
from .tables import find_join, find_lines, propose, records_from_lines
|
|
115
156
|
rec = config["records"]
|
|
116
157
|
rows = read_table(base / rec["file"])
|
|
@@ -138,12 +179,27 @@ def build(config: dict, base: Path = Path(".")) -> tuple[object, dict, list[str]
|
|
|
138
179
|
if rec.get("mapping"):
|
|
139
180
|
proposal = Mapping.parse(rec["mapping"])
|
|
140
181
|
else:
|
|
141
|
-
|
|
182
|
+
hint = None
|
|
183
|
+
et = config.get("entity_table") or config.get("_entity_table")
|
|
184
|
+
if et: # the entity table names the entity: its key's column
|
|
185
|
+
from .tables import entity_key_hint
|
|
186
|
+
hint = entity_key_hint(rows, read_table(base / et["file"]))
|
|
187
|
+
proposal, why = propose(rows, record_key=it.get("event_key"), latest=bool(shape and shape["counts"]),
|
|
188
|
+
entity_hint=hint)
|
|
142
189
|
log += [f"Mapping proposal: {w}" for w in why]
|
|
143
190
|
if proposal is None:
|
|
144
191
|
return None, {}, log
|
|
145
192
|
m, notes = repair(proposal, rows, exclude={it["event_key"]} if it.get("event_key") else ())
|
|
146
193
|
ds, problems = apply(m, rows)
|
|
194
|
+
target = config.get("target")
|
|
195
|
+
if target and ds.events and target not in {e.type for e in ds.events}:
|
|
196
|
+
# the outcome is named as written in the data ("Fail" in a results column): that column is the event type
|
|
197
|
+
holder = next((a for a in m.attrs if any(str((e.attrs or {}).get(a, "")).strip() == target for e in ds.events)), None)
|
|
198
|
+
if holder:
|
|
199
|
+
ds = type(ds)(ds.entities, [replace(e, type=str((e.attrs or {}).get(holder) or "").strip() or "other",
|
|
200
|
+
attrs={k: v for k, v in (e.attrs or {}).items() if k != holder})
|
|
201
|
+
for e in ds.events], ds.actions)
|
|
202
|
+
log.append(f"Target: {target!r} is a value of {holder!r}, so {holder!r} gives each record's type")
|
|
147
203
|
if ds.events and not any(e.type for e in ds.events):
|
|
148
204
|
noun = config.get("noun", "")
|
|
149
205
|
name = config.get("event") or (noun[:-1] if noun.endswith("s") else noun) or "record"
|
|
@@ -173,11 +229,16 @@ def build(config: dict, base: Path = Path(".")) -> tuple[object, dict, list[str]
|
|
|
173
229
|
return ds, groups, log
|
|
174
230
|
|
|
175
231
|
|
|
176
|
-
def run(config: dict, base: Path = Path(".")) -> str:
|
|
232
|
+
def run(config: dict, base: Path = Path("."), collect: dict | None = None) -> str:
|
|
233
|
+
"""The report as Markdown. `collect`, when given, is filled with the same results as data (loading notes,
|
|
234
|
+
counts, patterns, combinations, and each prediction task's gate report) for benchmarks to record."""
|
|
235
|
+
collect = collect if collect is not None else {}
|
|
177
236
|
ds, groups, log = build(config, base)
|
|
237
|
+
collect["log"] = log
|
|
178
238
|
noun, cur = config.get("noun", "records"), config.get("currency", "")
|
|
179
239
|
if ds is None:
|
|
180
240
|
return "\n".join(["# Vertiq report", "", "## Loading", ""] + [f"- {x}" for x in log] + ["", "No mapping could be proposed."]) + "\n"
|
|
241
|
+
collect.update(records=len(ds.events), entities=len(ds.entities))
|
|
181
242
|
out = ["# Vertiq report", "", f"{len(ds.events)} {noun} from {len(ds.entities)} entities.", "", "## Loading", ""]
|
|
182
243
|
out += [f"- {x}" for x in log]
|
|
183
244
|
if not ds.events:
|
|
@@ -186,6 +247,7 @@ def run(config: dict, base: Path = Path(".")) -> str:
|
|
|
186
247
|
days = config.get("summary_days") or max(1, span // 2)
|
|
187
248
|
out += ["", "## What happened", "", narrate.summary(summary.summarise(ds, days=days), entities=config.get("entities", "entities"))]
|
|
188
249
|
pat = patterns.find(ds, item_groups=groups or None, noun=noun, currency=cur)
|
|
250
|
+
collect["patterns"] = {k: pat.get(k) for k in ("status", "features", "tested")} | {"found": [f["text"] for f in pat.get("found", [])]}
|
|
189
251
|
out += ["", "## Patterns", ""]
|
|
190
252
|
if pat["status"] == "learning":
|
|
191
253
|
out.append(f"Not enough {noun} to test patterns: {pat['records']}, {pat['need']} needed.")
|
|
@@ -197,6 +259,7 @@ def run(config: dict, base: Path = Path(".")) -> str:
|
|
|
197
259
|
if lines:
|
|
198
260
|
a = affinity.analyse(lines)
|
|
199
261
|
combos = a.get("combos", [])
|
|
262
|
+
collect["combos"] = [{"items": c["items"], "bills_together": c["bills_together"], "lift": c["lift"]} for c in combos[:10]]
|
|
200
263
|
out += ["", "## Items bought together", ""]
|
|
201
264
|
out += ([f"- {' + '.join(c['items'])}: on {c['bills_together']} {noun} together, {c['lift']} times as often as chance" for c in combos[:10]]
|
|
202
265
|
or [f"No pair of items appears together on at least {affinity.MIN_PAIR} {noun}, the minimum for a combination."])
|
|
@@ -218,7 +281,7 @@ def run(config: dict, base: Path = Path(".")) -> str:
|
|
|
218
281
|
# several windows are several chances to pass the gate: correct the confidence for how many are tried
|
|
219
282
|
conf = 1 - (1 - harness.CONFIDENCE) / len(windows)
|
|
220
283
|
if len(windows) > 1:
|
|
221
|
-
out.append(f"-
|
|
284
|
+
out.append(f"- Windows tried: {', '.join(f'{w} days' for w in windows)}; each must pass the gate at "
|
|
222
285
|
f"{conf:.1%} confidence, corrected for trying {len(windows)}.")
|
|
223
286
|
tune = config.get("tune", TUNE)
|
|
224
287
|
for w in windows:
|
|
@@ -227,6 +290,7 @@ def run(config: dict, base: Path = Path(".")) -> str:
|
|
|
227
290
|
tasks.append(_task(ds, "value", target, max(windows), harness.CONFIDENCE, tune, out))
|
|
228
291
|
else:
|
|
229
292
|
out.append("- No single event type to predict; set \"target\" in the config.")
|
|
293
|
+
collect["predictions"] = [t.report() for t in tasks]
|
|
230
294
|
if config.get("scores"):
|
|
231
295
|
out += scores(tasks, base / config["scores"], cur, config.get("entities", "entities"))
|
|
232
296
|
return "\n".join(out) + "\n"
|
|
@@ -24,13 +24,57 @@ def _vals(rows, c):
|
|
|
24
24
|
return [str(r.get(c) if r.get(c) is not None else "").strip() for r in rows]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
_ID_NAME = (re.compile(r"(?i)(?:^|[\s_.\-])id$"), re.compile(r"[a-z]I[dD]$")) # customer_id, Customer ID, machineID
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_ID_WORDS = re.compile(r"(?i)(?:^|[^a-z])(?:id|ids|no|nbr|num|number|code|key|ref|reference|licen[cs]e|account|acct|"
|
|
31
|
+
r"zip|postcode|pin|phone|mobile)(?:[^a-z]|$)|[a-z](?:Id|ID|No|Number|Code|Key)$")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def id_worded(column: str) -> bool:
|
|
35
|
+
"""Whether a column's name says it holds codes rather than amounts: "license_", "Invoice No", "zip_code",
|
|
36
|
+
"accountNumber". Such a column is never proposed as the value."""
|
|
37
|
+
if re.search(r"(?i)(?:number|num|no)[\s_.]*of(?:[^a-z]|$)", column):
|
|
38
|
+
return False # "number of persons injured" is a count, not a code
|
|
39
|
+
return bool(_ID_WORDS.search(column.strip())) or id_named(column)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def id_named(column: str) -> bool:
|
|
43
|
+
"""Whether a column's name says it holds identifiers: "id", "customer_id", "Customer ID", "machineID"."""
|
|
44
|
+
return any(p.search(column.strip()) for p in _ID_NAME)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def entity_key_hint(rows: list[dict], entity_rows: list[dict]) -> str | None:
|
|
48
|
+
"""The column of `rows` whose values are the key of an entity table (a customer list, an asset register): the
|
|
49
|
+
table's first column with a distinct value on every row, matched by at least 90% of the column's values."""
|
|
50
|
+
ents = _records(entity_rows)
|
|
51
|
+
if not ents:
|
|
52
|
+
return None
|
|
53
|
+
key = next((c for c in ents[0] if len({str(r.get(c) or "").strip() for r in ents}) == len(ents)), None)
|
|
54
|
+
if key is None:
|
|
55
|
+
return None
|
|
56
|
+
keys = {str(r.get(key) or "").strip() for r in ents}
|
|
57
|
+
data = _records(rows)
|
|
58
|
+
best = None
|
|
59
|
+
for c in (data[0] if data else {}):
|
|
60
|
+
vals = {v for v in _vals(data, c) if v}
|
|
61
|
+
share = len(vals & keys) / max(1, len(vals))
|
|
62
|
+
if vals and share >= 0.9 and (best is None or share > best[0]):
|
|
63
|
+
best = (share, c)
|
|
64
|
+
return best[1] if best else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def propose(rows: list[dict], record_key: str | None = None, latest: bool = False,
|
|
68
|
+
entity_hint: str | None = None) -> tuple[Mapping | None, list[str]]:
|
|
28
69
|
"""A mapping chosen from the values: the time is the date column that is filled best, with a separate
|
|
29
70
|
time-of-day column if there is one; the value is the most complete amount column; the entity is the best
|
|
30
71
|
identifier that repeats (a person or asset), else the best identifier; attributes are the remaining
|
|
31
72
|
columns with 2 to 12 repeated labels, and columns that identify each record (kept for joins).
|
|
32
73
|
`record_key`: a column known to identify the record itself (the key line items join on); it is kept as
|
|
33
|
-
an attribute and never proposed as the entity, since it names the order, not whom it is about.
|
|
74
|
+
an attribute and never proposed as the entity, since it names the order, not whom it is about.
|
|
75
|
+
`entity_hint`: a column known to hold the entity (its values are an entity table's key); it is the entity.
|
|
76
|
+
A column named as an identifier (id_named) that holds whole numbers counts as one with 20 or more values,
|
|
77
|
+
so machine 7 or store 12 are entities, not counts."""
|
|
34
78
|
skip = structural_rows(rows)
|
|
35
79
|
data = [r for i, r in enumerate(rows, start=1) if i not in skip]
|
|
36
80
|
if not data:
|
|
@@ -58,7 +102,9 @@ def propose(rows: list[dict], record_key: str | None = None, latest: bool = Fals
|
|
|
58
102
|
fmts = date_formats(_vals(data, time))
|
|
59
103
|
clock = next((c for c in kind if c != time and sum(1 for v in _vals(data, c) if _TIME_OF_DAY.match(v)) >= 0.8 * len(data)), None)
|
|
60
104
|
qcols = _quantities(data, kind)
|
|
61
|
-
|
|
105
|
+
# an identifier (named or joined as one) is never the amount
|
|
106
|
+
amounts = [c for c in kind if kind[c] == "number" and c != entity_hint and not id_worded(c)
|
|
107
|
+
and fitness(data, c, "value", kind) >= 0.5 and c not in qcols
|
|
62
108
|
and not _id_numbers(_vals(data, c))]
|
|
63
109
|
value = None
|
|
64
110
|
if amounts:
|
|
@@ -75,12 +121,16 @@ def propose(rows: list[dict], record_key: str | None = None, latest: bool = Fals
|
|
|
75
121
|
for c in kind:
|
|
76
122
|
if c in (time, clock, value, record_key, item_col):
|
|
77
123
|
continue
|
|
78
|
-
f = fitness(data, c, "entity", kind)
|
|
79
|
-
if f < 0.2:
|
|
80
|
-
continue # records without an id become one-off entities (reported)
|
|
81
124
|
ph = placeholder_ids(data, c) # once per column, not once per value
|
|
82
125
|
real = [v for v in _vals(data, c) if v not in ph]
|
|
83
126
|
distinct = len(set(real)) / max(1, len(real))
|
|
127
|
+
if c == entity_hint or (id_named(c) and real and all(v.isdigit() for v in real) and len(set(real)) >= 20):
|
|
128
|
+
# named or joined as an identifier: small whole numbers here are ids, not counts
|
|
129
|
+
ids.append((True if c == entity_hint else distinct < 0.98, 2.0 if c == entity_hint else len(real) / n, c))
|
|
130
|
+
continue
|
|
131
|
+
f = fitness(data, c, "entity", kind)
|
|
132
|
+
if f < 0.2:
|
|
133
|
+
continue # records without an id become one-off entities (reported)
|
|
84
134
|
# an identifier is a code with letters and digits, a fixed-length whole number (an account, customer or phone
|
|
85
135
|
# number), or a long number; short numbers and numbers with decimals are amounts or counts
|
|
86
136
|
coded = _id_numbers(real) or sum(1 for v in real if any(ch.isdigit() for ch in v)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vertiq
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -298,6 +298,20 @@ Recency baseline AUC 0.761.
|
|
|
298
298
|
| Auto (chose boosted) | 0.812 | 0.795–0.827 | +0.043 to +0.059 |
|
|
299
299
|
| scikit-learn logistic regression (reference) | 0.807 | | |
|
|
300
300
|
|
|
301
|
+
### Real data across sectors
|
|
302
|
+
|
|
303
|
+
`vertiq-report` on public data from 5 sectors, nothing tuned per sector; windows of 30, 60 and 90 days, each through the
|
|
304
|
+
gate with its confidence corrected for the three, and its segment check (`benchmarks/sectors/fetch.sh`, then
|
|
305
|
+
`python benchmarks/sectors/run.py`; full reports in `benchmarks/sectors/reports/`).
|
|
306
|
+
|
|
307
|
+
| Sector | Data | Size | Question | Result | Patterns kept |
|
|
308
|
+
|---|---|---|---|---|---|
|
|
309
|
+
| Retail | UCI Online Retail II: a UK online shop's invoice lines, 2009-2010 | 28,816 records, 9,612 entities | Will a customer buy again, and how much? | Promoted: 30 days, AUC 0.881 vs 0.776; 60 days, AUC 0.888 vs 0.750; 90 days, AUC 0.890 vs 0.708 | 20 of 471 |
|
|
310
|
+
| Food safety | City of Chicago food inspections since 2019 (up to 150,000) | 137,034 records, 27,264 entities | Which establishments will fail an inspection? | Promoted: 30 days, AUC 0.813 vs 0.698; 60 days, AUC 0.822 vs 0.703; 90 days, AUC 0.809 vs 0.692 | 8 of 94 |
|
|
311
|
+
| Animal services | Austin Animal Center intakes (up to 200,000) | 173,812 records, 156,287 entities | Which animals will come back to the shelter? | Not promoted: best AUC 0.870 vs 0.865; the current rule stays | 20 of 266 |
|
|
312
|
+
| Manufacturing | Microsoft Azure predictive maintenance sample: 100 machines, 2015 (errors, failures, machine models) | 4,680 records, 100 entities | Which machines will fail? | Promoted: 60 days, AUC 0.720 vs 0.545. Kept the rule: 30 days, 90 days | 7 of 221 |
|
|
313
|
+
| Transport | NYC motor vehicle collisions since January 2025 (up to 200,000) | 121,970 records, 43,061 entities | Where will collisions happen again? | Promoted: 30 days, AUC 0.971 vs 0.894; 60 days, AUC 0.958 vs 0.859; 90 days, AUC 0.950 vs 0.840. Next value promoted: average error 0.44 vs 0.48 | 5 of 86 |
|
|
314
|
+
|
|
301
315
|
### Real exports (held out)
|
|
302
316
|
|
|
303
317
|
Six public exports from different sectors, never used for training; a role is correct when it is one of the accepted answers
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|