solvi 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {solvi-0.2.2 → solvi-0.3.0}/PKG-INFO +9 -3
- {solvi-0.2.2 → solvi-0.3.0}/README.md +8 -2
- {solvi-0.2.2 → solvi-0.3.0}/pyproject.toml +1 -1
- {solvi-0.2.2 → solvi-0.3.0}/pyproject.toml.orig +1 -1
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/core.py +40 -2
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/system.py +124 -6
- {solvi-0.2.2 → solvi-0.3.0}/LICENSE +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/__init__.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/extract_long.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/extract_model.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/extract_multi.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/fast.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/heads.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/py.typed +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/rules.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/runtime.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/show.py +0 -0
- {solvi-0.2.2 → solvi-0.3.0}/src/solvi/strategist.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solvi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Decision systems from a catalog of Python functions and checks: a strategist plans the flow, every answer comes with a quote or formula and a verifiable trace
|
|
5
5
|
Keywords: decision systems,rules,document extraction,ModernBERT,explainability,verification,audit trail
|
|
6
6
|
Author: mxkuzn
|
|
@@ -54,8 +54,13 @@ script that computes everything, and 152 ms when an expired policy settles the c
|
|
|
54
54
|
|
|
55
55
|
- [solvi playground](https://huggingface.co/spaces/solvi-ai/playground): write a decision task in Python and run it, watch the
|
|
56
56
|
strategist's plan, tamper with a trace and see the replay catch it, learn rules from examples.
|
|
57
|
-
- [solvi
|
|
58
|
-
|
|
57
|
+
- [solvi documents](https://huggingface.co/spaces/solvi-ai/documents): cited, typed answers from contracts, invoices, receipts,
|
|
58
|
+
leases and more — the ModernBERT extractor (ONNX) and the decisions both run in your browser; add a field by describing it.
|
|
59
|
+
- [solvi arcade](https://huggingface.co/spaces/solvi-ai/arcade): game agents that explain every move — tic-tac-toe, maze,
|
|
60
|
+
minesweeper, 20 questions, Mafia detective, a bot arena, and "hack the trace".
|
|
61
|
+
- [solvi realms](https://huggingface.co/spaces/solvi-ai/realms): an endless strategy game whose factions are solvi systems —
|
|
62
|
+
tested for 100 000 turns: flat decision time (~0.3–0.6 ms), bounded memory and state, every sampled trace replay OK.
|
|
63
|
+
- All run **entirely in your browser** (Pyodide): no server, no GPU, nothing you type leaves the page.
|
|
59
64
|
- Models: [solvi-ai/extract-base](https://huggingface.co/solvi-ai/extract-base) (fields by description) and
|
|
60
65
|
[solvi-ai/extract-receipts](https://huggingface.co/solvi-ai/extract-receipts).
|
|
61
66
|
|
|
@@ -236,6 +241,7 @@ receipt with the one-pass extractor on an A100).
|
|
|
236
241
|
| [examples/06_learned_rules.py](examples/06_learned_rules.py) | `learn_rule`: route parcels to delivery zones from free-form addresses with a readable if-then list learned from labels |
|
|
237
242
|
| [examples/09_strategy_at_scale.py](examples/09_strategy_at_scale.py) | Insurance claim desk: the strategist generates a different plan per question set, hard checks first with early exit, slow services in parallel; timed |
|
|
238
243
|
| [examples/10_learn_in_milliseconds.py](examples/10_learn_in_milliseconds.py) | `fit_fast`: a new question learned in milliseconds, then corrected one example at a time (each correction ~0.2 ms, nothing retrained) |
|
|
244
|
+
| [examples/11_answer_types_and_constraints.py](examples/11_answer_types_and_constraints.py) | Multi-label and ordinal answers tied by constraints between answers; contradictions in learned answers are repaired by joint decoding |
|
|
239
245
|
| [examples/07_receipts_model.py](examples/07_receipts_model.py) | Expense check on a scanned receipt: a receipts-tuned extractor cites each field, rules and a hard check decide (needs `solvi[model]`) |
|
|
240
246
|
| [examples/08_contracts_by_description.py](examples/08_contracts_by_description.py) | Contract review with fields defined only in words: the general extractor reads the whole contract, cites clauses or says "absent" (needs `solvi[model]`) |
|
|
241
247
|
|
|
@@ -27,8 +27,13 @@ script that computes everything, and 152 ms when an expired policy settles the c
|
|
|
27
27
|
|
|
28
28
|
- [solvi playground](https://huggingface.co/spaces/solvi-ai/playground): write a decision task in Python and run it, watch the
|
|
29
29
|
strategist's plan, tamper with a trace and see the replay catch it, learn rules from examples.
|
|
30
|
-
- [solvi
|
|
31
|
-
|
|
30
|
+
- [solvi documents](https://huggingface.co/spaces/solvi-ai/documents): cited, typed answers from contracts, invoices, receipts,
|
|
31
|
+
leases and more — the ModernBERT extractor (ONNX) and the decisions both run in your browser; add a field by describing it.
|
|
32
|
+
- [solvi arcade](https://huggingface.co/spaces/solvi-ai/arcade): game agents that explain every move — tic-tac-toe, maze,
|
|
33
|
+
minesweeper, 20 questions, Mafia detective, a bot arena, and "hack the trace".
|
|
34
|
+
- [solvi realms](https://huggingface.co/spaces/solvi-ai/realms): an endless strategy game whose factions are solvi systems —
|
|
35
|
+
tested for 100 000 turns: flat decision time (~0.3–0.6 ms), bounded memory and state, every sampled trace replay OK.
|
|
36
|
+
- All run **entirely in your browser** (Pyodide): no server, no GPU, nothing you type leaves the page.
|
|
32
37
|
- Models: [solvi-ai/extract-base](https://huggingface.co/solvi-ai/extract-base) (fields by description) and
|
|
33
38
|
[solvi-ai/extract-receipts](https://huggingface.co/solvi-ai/extract-receipts).
|
|
34
39
|
|
|
@@ -209,6 +214,7 @@ receipt with the one-pass extractor on an A100).
|
|
|
209
214
|
| [examples/06_learned_rules.py](examples/06_learned_rules.py) | `learn_rule`: route parcels to delivery zones from free-form addresses with a readable if-then list learned from labels |
|
|
210
215
|
| [examples/09_strategy_at_scale.py](examples/09_strategy_at_scale.py) | Insurance claim desk: the strategist generates a different plan per question set, hard checks first with early exit, slow services in parallel; timed |
|
|
211
216
|
| [examples/10_learn_in_milliseconds.py](examples/10_learn_in_milliseconds.py) | `fit_fast`: a new question learned in milliseconds, then corrected one example at a time (each correction ~0.2 ms, nothing retrained) |
|
|
217
|
+
| [examples/11_answer_types_and_constraints.py](examples/11_answer_types_and_constraints.py) | Multi-label and ordinal answers tied by constraints between answers; contradictions in learned answers are repaired by joint decoding |
|
|
212
218
|
| [examples/07_receipts_model.py](examples/07_receipts_model.py) | Expense check on a scanned receipt: a receipts-tuned extractor cites each field, rules and a hard check decide (needs `solvi[model]`) |
|
|
213
219
|
| [examples/08_contracts_by_description.py](examples/08_contracts_by_description.py) | Contract review with fields defined only in words: the general extractor reads the whole contract, cites clauses or says "absent" (needs `solvi[model]`) |
|
|
214
220
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "solvi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Decision systems from a catalog of Python functions and checks: a strategist plans the flow, every answer comes with a quote or formula and a verifiable trace"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "solvi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Decision systems from a catalog of Python functions and checks: a strategist plans the flow, every answer comes with a quote or formula and a verifiable trace"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -21,16 +21,33 @@ class Quote:
|
|
|
21
21
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class AnswerType:
|
|
24
|
-
kind: str # yes_no | choice
|
|
24
|
+
kind: str # yes_no | choice | ordinal | multi
|
|
25
25
|
options: list
|
|
26
|
+
descriptions: dict = field(default_factory=dict) # option -> what it means (for people and for zero-shot scoring)
|
|
26
27
|
|
|
27
28
|
def normalize(self, v):
|
|
29
|
+
if self.kind == "multi":
|
|
30
|
+
vals = [v] if isinstance(v, str) else list(v or [])
|
|
31
|
+
bad = [x for x in vals if x not in self.options]
|
|
32
|
+
if bad:
|
|
33
|
+
raise ValueError(f"answer {bad!r} not among {self.options}")
|
|
34
|
+
return tuple(o for o in self.options if o in set(vals)) # canonical: in option order, no duplicates
|
|
28
35
|
if self.kind == "yes_no" and isinstance(v, bool):
|
|
29
36
|
return "yes" if v else "no"
|
|
30
37
|
if v not in self.options:
|
|
31
38
|
raise ValueError(f"answer {v!r} is not one of {self.options}")
|
|
32
39
|
return v
|
|
33
40
|
|
|
41
|
+
def rank(self, v):
|
|
42
|
+
"""Position of an ordinal (or choice) answer: 0 for the first option."""
|
|
43
|
+
return self.options.index(v)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _opts(options):
|
|
47
|
+
if isinstance(options, dict):
|
|
48
|
+
return list(options), dict(options)
|
|
49
|
+
return list(options), {}
|
|
50
|
+
|
|
34
51
|
|
|
35
52
|
class Answer:
|
|
36
53
|
@staticmethod
|
|
@@ -39,7 +56,19 @@ class Answer:
|
|
|
39
56
|
|
|
40
57
|
@staticmethod
|
|
41
58
|
def choice(options) -> AnswerType:
|
|
42
|
-
|
|
59
|
+
"""One of the options. `options` is a list, or a dict {option: description}."""
|
|
60
|
+
return AnswerType("choice", *_opts(options))
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def ordinal(levels) -> AnswerType:
|
|
64
|
+
"""One of ordered levels, lowest first (e.g. ["low", "medium", "high"]). A learned head answers with the median of its
|
|
65
|
+
distribution instead of the most likely level, so it never jumps over the middle."""
|
|
66
|
+
return AnswerType("ordinal", *_opts(levels))
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def multi(options) -> AnswerType:
|
|
70
|
+
"""Any subset of the options (possibly empty), returned as a tuple in option order. A rule may return a list or set."""
|
|
71
|
+
return AnswerType("multi", *_opts(options))
|
|
43
72
|
|
|
44
73
|
|
|
45
74
|
@dataclass
|
|
@@ -69,6 +98,7 @@ class Catalog:
|
|
|
69
98
|
def __init__(self):
|
|
70
99
|
self.parts: dict[str, Part] = {}
|
|
71
100
|
self.rules: dict[str, Part] = {}
|
|
101
|
+
self.constraints: dict[str, Part] = {} # rules between answers of different questions
|
|
72
102
|
|
|
73
103
|
def _add(self, kind, f, **kw):
|
|
74
104
|
sig = inspect.signature(f)
|
|
@@ -76,6 +106,8 @@ class Catalog:
|
|
|
76
106
|
if kind == "rule":
|
|
77
107
|
p.name = "answer:" + p.question
|
|
78
108
|
self.rules[p.question] = p
|
|
109
|
+
elif kind == "constraint":
|
|
110
|
+
self.constraints[p.name] = p
|
|
79
111
|
else:
|
|
80
112
|
if p.name in self.parts:
|
|
81
113
|
raise ValueError(f"part {p.name} is already in the catalog")
|
|
@@ -96,5 +128,11 @@ class Catalog:
|
|
|
96
128
|
def rule(self, question):
|
|
97
129
|
return lambda f: self._add("rule", f, question=question)
|
|
98
130
|
|
|
131
|
+
def constraint(self, f):
|
|
132
|
+
"""A rule between answers: argument names are question names, it returns True when the answers fit together
|
|
133
|
+
(e.g. `def unsafe_if_harm(verdict, harm): return harm == "none" or verdict == "unsafe"`). When learned answers break it,
|
|
134
|
+
solvi picks the most probable combination that satisfies every constraint; answers from rules and hard checks stay."""
|
|
135
|
+
return self._add("constraint", f)
|
|
136
|
+
|
|
99
137
|
def producer(self, fact):
|
|
100
138
|
return self.parts.get(fact)
|
|
@@ -18,6 +18,8 @@ class Response:
|
|
|
18
18
|
trace: object
|
|
19
19
|
values: dict
|
|
20
20
|
ms: float
|
|
21
|
+
feasible: bool = True # do the answers satisfy every applicable constraint?
|
|
22
|
+
violations: list = None # names of the constraints still broken (fixed answers that conflict)
|
|
21
23
|
|
|
22
24
|
def __getitem__(self, q):
|
|
23
25
|
return self.results[q]
|
|
@@ -61,7 +63,8 @@ class System:
|
|
|
61
63
|
if q.name in self.calib and r.status == "ok":
|
|
62
64
|
r.confidence = _platt(r.confidence, *self.calib[q.name])
|
|
63
65
|
results[q.name] = r
|
|
64
|
-
|
|
66
|
+
feasible, violations = self._joint(results)
|
|
67
|
+
resp = Response(results, flow, trace, vals, now_ms() - t0, feasible, violations)
|
|
65
68
|
if self.journal:
|
|
66
69
|
self._log(init_state, resp)
|
|
67
70
|
return resp
|
|
@@ -109,14 +112,86 @@ class System:
|
|
|
109
112
|
if lost: # a head never guesses from facts that could not be computed
|
|
110
113
|
return Result(None, 0.0, "head features not computed: " + ", ".join(lost), "abstain")
|
|
111
114
|
p = head.predict(vals)
|
|
112
|
-
|
|
115
|
+
if q.answer.kind == "multi": # p: option -> probability it applies
|
|
116
|
+
a = tuple(o for o in q.answer.options if p[o] >= 0.5)
|
|
117
|
+
base = min(max(v, 1 - v) for v in p.values())
|
|
118
|
+
elif q.answer.kind == "ordinal": # median of the distribution over ordered levels
|
|
119
|
+
acc = 0.0
|
|
120
|
+
for a in q.answer.options:
|
|
121
|
+
acc += p[a]
|
|
122
|
+
if acc >= 0.5:
|
|
123
|
+
break
|
|
124
|
+
base = p[a]
|
|
125
|
+
else:
|
|
126
|
+
a = max(p, key=p.get)
|
|
127
|
+
base = p[a]
|
|
113
128
|
contrib = head.contributions(vals)
|
|
114
129
|
why = ", ".join(f"{f} = {vals.get(f)!r} ({c:+.2f})" for f, c in sorted(contrib.items(), key=lambda t: -abs(t[1]))[:4])
|
|
115
130
|
if soft_failed:
|
|
116
131
|
why += "; failed checks: " + ", ".join(soft_failed)
|
|
117
|
-
conf =
|
|
132
|
+
conf = base * path_confidence(self.catalog, trace, head.features)
|
|
118
133
|
return Result(a, conf, why, probs=p)
|
|
119
134
|
|
|
135
|
+
# --- constraints between answers: joint decoding
|
|
136
|
+
def _joint(self, results, max_combos=50_000):
|
|
137
|
+
import itertools
|
|
138
|
+
import math
|
|
139
|
+
cons = [c for c in self.catalog.constraints.values() if all(q in results for q in c.inputs)]
|
|
140
|
+
if not cons:
|
|
141
|
+
return True, []
|
|
142
|
+
|
|
143
|
+
def ok(assign, c):
|
|
144
|
+
if any(assign.get(q) is None for q in c.inputs):
|
|
145
|
+
return True # an abstained answer: nothing to check
|
|
146
|
+
try:
|
|
147
|
+
return bool(c.func(**{q: assign[q] for q in c.inputs}))
|
|
148
|
+
except Exception: # noqa: BLE001
|
|
149
|
+
return False
|
|
150
|
+
current = {q: r.answer for q, r in results.items()}
|
|
151
|
+
if all(ok(current, c) for c in cons):
|
|
152
|
+
return True, []
|
|
153
|
+
# candidates: learned answers may change (their distribution), rule / forced / abstained answers are fixed
|
|
154
|
+
qs = sorted({q for c in cons for q in c.inputs})
|
|
155
|
+
cands = {}
|
|
156
|
+
for q in qs:
|
|
157
|
+
r, at = results[q], self.questions[q].answer
|
|
158
|
+
if r.status == "ok" and r.probs:
|
|
159
|
+
if at.kind == "multi":
|
|
160
|
+
opts = []
|
|
161
|
+
for bits in itertools.product([0, 1], repeat=len(at.options)):
|
|
162
|
+
combo = tuple(o for o, b in zip(at.options, bits) if b)
|
|
163
|
+
lp = sum(math.log(max(1e-9, r.probs[o] if b else 1 - r.probs[o])) for o, b in zip(at.options, bits))
|
|
164
|
+
opts.append((combo, lp))
|
|
165
|
+
else:
|
|
166
|
+
opts = [(o, math.log(max(1e-9, pr))) for o, pr in r.probs.items()]
|
|
167
|
+
cands[q] = sorted(opts, key=lambda t: -t[1])
|
|
168
|
+
else:
|
|
169
|
+
cands[q] = [(r.answer, 0.0)]
|
|
170
|
+
k = max(len(v) for v in cands.values())
|
|
171
|
+
while k > 1 and math.prod(min(len(v), k) for v in cands.values()) > max_combos:
|
|
172
|
+
k -= 1
|
|
173
|
+
best = None
|
|
174
|
+
for combo in itertools.product(*[cands[q][:k] for q in qs]):
|
|
175
|
+
assign = dict(current)
|
|
176
|
+
assign.update({q: a for q, (a, _) in zip(qs, combo)})
|
|
177
|
+
if all(ok(assign, c) for c in cons):
|
|
178
|
+
score = sum(lp for _, lp in combo)
|
|
179
|
+
if best is None or score > best[0]:
|
|
180
|
+
best = (score, assign)
|
|
181
|
+
if best is None:
|
|
182
|
+
return False, [c.name for c in cons if not ok(current, c)]
|
|
183
|
+
for q in qs:
|
|
184
|
+
r = results[q]
|
|
185
|
+
if best[1][q] != r.answer:
|
|
186
|
+
was = r.answer
|
|
187
|
+
r.answer = best[1][q]
|
|
188
|
+
if r.probs:
|
|
189
|
+
r.confidence = (math.exp(dict(cands[q])[r.answer]) if self.questions[q].answer.kind == "multi"
|
|
190
|
+
else r.probs.get(r.answer, r.confidence))
|
|
191
|
+
broken = [c.name for c in cons if q in c.inputs and not ok({**current}, c)]
|
|
192
|
+
r.why += f"; changed from {was!r} to satisfy {', '.join(broken)}"
|
|
193
|
+
return True, []
|
|
194
|
+
|
|
120
195
|
# --- task-specific training
|
|
121
196
|
def facts_for(self, init_state):
|
|
122
197
|
"""All computable facts (for head training): a "compute everything" flow without rules."""
|
|
@@ -135,7 +210,11 @@ class System:
|
|
|
135
210
|
ans = [q.answer.normalize(a) for _, a in examples]
|
|
136
211
|
keys = set(examples[0][0].keys())
|
|
137
212
|
cands = sorted(f for f in computable(self.catalog, keys) - keys)
|
|
138
|
-
|
|
213
|
+
if q.answer.kind == "multi":
|
|
214
|
+
self.heads[question] = MultiHead(q.answer.options, lambda: Head(["yes", "no"]),
|
|
215
|
+
lambda h, ys: h.fit(rows, ys, cands)).fit(ans)
|
|
216
|
+
else:
|
|
217
|
+
self.heads[question] = Head(q.answer.options).fit(rows, ans, cands)
|
|
139
218
|
return self.heads[question]
|
|
140
219
|
|
|
141
220
|
def fit_fast(self, question, examples, features=None, lam=None):
|
|
@@ -150,7 +229,12 @@ class System:
|
|
|
150
229
|
if features is None:
|
|
151
230
|
keys = set(examples[0][0].keys())
|
|
152
231
|
features = sorted(f for f in computable(self.catalog, keys) - keys)
|
|
153
|
-
|
|
232
|
+
ans = [q.answer.normalize(a) for _, a in examples]
|
|
233
|
+
if q.answer.kind == "multi":
|
|
234
|
+
head = MultiHead(q.answer.options, lambda: FastHead(["yes", "no"], lam=lam),
|
|
235
|
+
lambda h, ys: h.fit(rows, ys, list(features))).fit(ans)
|
|
236
|
+
else:
|
|
237
|
+
head = FastHead(q.answer.options, lam=lam).fit(rows, ans, list(features))
|
|
154
238
|
head.fit_ms = (time.perf_counter() - t0) * 1000
|
|
155
239
|
self.heads[question] = head
|
|
156
240
|
return head
|
|
@@ -201,7 +285,7 @@ class System:
|
|
|
201
285
|
from .fast import FastHead
|
|
202
286
|
ms = None
|
|
203
287
|
head = self.heads.get(question)
|
|
204
|
-
if isinstance(head, FastHead):
|
|
288
|
+
if isinstance(head, (FastHead, MultiHead)) and getattr(head, "online", True):
|
|
205
289
|
ms = head.update(self.facts_for(init_state), self.questions[question].answer.normalize(correct))
|
|
206
290
|
if self.journal:
|
|
207
291
|
with open(self.journal, "a") as fh:
|
|
@@ -216,6 +300,40 @@ class System:
|
|
|
216
300
|
"records": [[r.step, r.name, r.hash] for r in resp.trace.records]}, ensure_ascii=False) + "\n")
|
|
217
301
|
|
|
218
302
|
|
|
303
|
+
class MultiHead:
|
|
304
|
+
"""A multi-label answer as one yes/no head per option (each learned with fit or fit_fast)."""
|
|
305
|
+
|
|
306
|
+
def __init__(self, options, make, train):
|
|
307
|
+
self.options, self.make, self.train = list(options), make, train
|
|
308
|
+
self.heads = {}
|
|
309
|
+
|
|
310
|
+
def fit(self, answers):
|
|
311
|
+
for o in self.options:
|
|
312
|
+
self.heads[o] = self.train(self.make(), ["yes" if o in a else "no" for a in answers])
|
|
313
|
+
h = next(iter(self.heads.values()))
|
|
314
|
+
self.features = sorted({f for hh in self.heads.values() for f in hh.features})
|
|
315
|
+
self.online = hasattr(h, "update")
|
|
316
|
+
self.loo_acc = getattr(h, "loo_acc", None)
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def cv_acc(self):
|
|
321
|
+
return self.loo_acc
|
|
322
|
+
|
|
323
|
+
def predict(self, row):
|
|
324
|
+
return {o: h.predict(row)["yes"] for o, h in self.heads.items()}
|
|
325
|
+
|
|
326
|
+
def contributions(self, row):
|
|
327
|
+
out = {}
|
|
328
|
+
for h in self.heads.values():
|
|
329
|
+
for f, c in h.contributions(row).items():
|
|
330
|
+
out[f] = out.get(f, 0.0) + c
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
def update(self, row, answer):
|
|
334
|
+
return sum(h.update(row, "yes" if o in answer else "no") for o, h in self.heads.items())
|
|
335
|
+
|
|
336
|
+
|
|
219
337
|
def governs(part, question):
|
|
220
338
|
"""Does a failed hard check decide this question? Yes if the question is in its `then`, names it as a checkpoint, or the
|
|
221
339
|
check has no `then` at all."""
|
|
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
|