Skip to content

Models and the tree IR

Every supported model is parsed into one intermediate representation (EnsembleIR): trees of (feature, threshold, op, missing_left) nodes plus a raw-space intercept and an output link. Backends only ever see the IR — no backend touches a native model object.

Supported inputs

Library Native objects Dump input Notes
XGBoost Booster, sklearn wrappers save_model("*.json") path or dict binary:logistic, reg:squarederror; LT convention; enable_categorical splits parsed as set-membership nodes
LightGBM Booster, sklearn wrappers dump_model() dict or its JSON binary, regression; LE convention; categorical_feature splits parsed (NaN routes right at them, LightGBM's own rule); zero_as_missing raises
CatBoost classifier/regressor save_model(format="json") oblivious trees expanded to binary trees; native categorical one-hot and single-feature-statistic splits parsed (categories= required; feature combinations raise ParserError)
scikit-learn RF, GB, HistGB — see raw-score semantics below; HistGB categorical_features bitsets parsed through the estimator's own encoder

Native categorical splits become set-membership IR nodes — the semantics, cost model, and search treatment are in Categorical features. Unsupported constructs (multiclass, dart/gblinear) raise UnsupportedModelError — parsers never degrade silently. Every parser is gated by a conformance suite that compares IR evaluation against native predictions on ≥10k probes including NaN patterns and threshold-adjacent points.

Raw-score semantics per family

  • XGBoost / LightGBM / CatBoost / GradientBoosting / HistGradientBoosting: raw score = margin (log-odds for binary classifiers, SIGMOID link).
  • RandomForestClassifier: the raw score is the averaged class-1 probability with an IDENTITY link. Use Target.raw(range=(0.0, 0.3)) — a Target.probability on a forest raises, because there is no sigmoid to invert.

Float32 pitfalls handled for you

GBDT libraries store thresholds and inputs as float32 and their JSON dumps use shortest-round-trip decimals; treecf casts thresholds back through float32 where the library compares in float32, and handles LightGBM's zeroing of values with magnitude below 1e-35. Without this, counterfactual values equal to a threshold would route differently in the deployed model.

The libraries also cast their inputs to float32 before comparing (sklearn, XGBoost, CatBoost; LightGBM compares in float64), so a float64 value within half a float32 ulp of a split routes one way natively and the other way under a plain float64 comparison. Each of those parsers therefore stores not the library's threshold but the float64 boundary of that cast — the largest value the native model still routes left — and float64 inputs then route exactly as the deployed model routes them, without any pre-rounding on your side.