Constraints¶
Constraints are declared once, validated against the model's feature space, and compiled by a single visitor into every backend — the abstract constraint form and the genetic engines' vectorized check/repair pair share one source of truth.
from treecf import Freeze, Monotone, Range, Linear, Implies, Equals, OneHot, constraint
Freeze("age_of_bureau_file") # immutable
Monotone("age", "increase") # directional
Range("utilization", 0.0, 1.5) # hard bounds
constraint("max_dpd_30d <= max_dpd_12m") # string sugar -> Linear
constraint("2*a - b <= c + 5") # any linear expression
Implies(Equals("has_mortgage", 0), Equals("mortgage_balance", 0))
OneHot(("channel_web", "channel_app", "channel_branch"))
Only linear expressions are expressible as strings by design; richer constraints are objects. Parse errors carry a caret marking the offending token.
When the factual violates a constraint¶
If the factual itself violates a declared constraint (say debt <= income when
the row has more debt than income), the search still runs, but every returned
plan must land in the feasible region — so the plan will contain changes made
solely to satisfy the constraint, indistinguishable from score-driven changes.
explain emits a TreecfWarning naming each violated constraint when this
happens; explain_batch emits one aggregate warning with per-constraint row
counts instead of one warning per row.
How constraints steer the search¶
Single-feature linear constraints (constraint("income >= 100")) are lowered
into per-feature bounds at compile time, so candidates are clipped into the
feasible interval directly. Multi-feature linears are enforced by a cyclic
projection repair: each violated constraint projects the candidate onto its
boundary (three rounds, respecting frozen and pinned features), which handles
feasible regions far from the factual. Repair is best-effort — with mutually
conflicting constraints no repair exists, the feasibility check remains the
arbiter, and the result is reported as Infeasible rather than silently bent.
Mining candidates from data¶
suggest_constraints scans a background sample for invariants — pairwise
orders, equalities, binary implications, one-hot groups, missingness links,
integer-valuedness — and returns them for human review; nothing is ever
auto-applied:
import treecf
result = treecf.suggest_constraints(X_train, feature_names=names)
for s in result[:20]:
print(s.as_code()) # constraint("n_active_loans <= n_loans_total") # support=1.0000, n=48211
accepted = [s.constraint for s in result if s.kind == "order"]
Hierarchical time-window features (dpd_7d <= dpd_30d <= dpd_90d) are reduced
to a minimal generating set (transitive reduction), and suggestions are ranked
by support and shared name tokens — never filtered.
Near-invariants (support ≥ 99.9% but below 1.0) are returned separately as
DataQualityFinding records with the violating rows: a handful of violations
of an otherwise universal rule usually signals an ETL defect, not a domain
exception.