Skip to content

Native missing-value (NaN) support for kernel uplift/causal trees (prereq for #955) - #972

Merged
jeongyoonlee merged 1 commit into
masterfrom
feature/955-native-missing-values
Jul 25, 2026
Merged

Native missing-value (NaN) support for kernel uplift/causal trees (prereq for #955)#972
jeongyoonlee merged 1 commit into
masterfrom
feature/955-native-missing-values

Conversation

@jeongyoonlee

Copy link
Copy Markdown
Collaborator

What

Prerequisite for the #955 switchover (tree-unification epic #945). Adds native missing-value (NaN) support to the kernel-backed uplift and causal trees/forests, mirroring scikit-learn's tree implementation closely. Today these estimators reject NaN in X; the switched-over public UpliftTreeClassifier / UpliftRandomForestClassifier need to accept it, so this lands the criterion-level work first, isolated from the compat/switchover mechanics.

How (follows scikit-learn's design)

The shared CausalRegressionCriterion (inherited by every uplift and causal criterion) now implements sklearn's per-feature missing-value machinery:

  • NodeSplitState gains a per-group missing NodeState. init_missing(n_missing) accumulates the missing tail (sample_indices[end - n_missing:end], the sklearn contract) into both the flat sum_missing (used by StandardMSE) and the per-group state — still respecting the group-encoding NaN mask (isnan(y[i, k]) = "sample not in group k").
  • reset / reverse_reset fold the missing block into the child chosen by missing_go_to_left; update scans only the non-missing range (end - n_missing); StandardMSE.children_impurity adds the missing squared-sum when routed left. All are no-ops when n_missing == 0, so clean-data behaviour is unchanged.
  • The flat sum_missing allocation (init_sum_missing) is inherited from RegressionCriterion unchanged.

The Python tree layer follows sklearn 1.7:

  • _support_missing_values + an allow_nan tag via __sklearn_tags__ (dense best splitter only — random/sparse don't carry the machinery). The vendored base's _get_tags()["allow_nan"] is dead under sklearn 1.7; the causal/uplift layers now use __sklearn_tags__().input_tags.allow_nan.
  • fit validates with ensure_all_finite=False and threads _compute_missing_values_in_feature_mask(X) into builder.build(...).
  • _prepare_data and both forests allow NaN through validation. inf is still rejected. The forest allow_nan tag is delegated automatically by sklearn's BaseForest.__sklearn_tags__.

Scope

  • Enabled for: _KernelUpliftTreeClassifier, _KernelUpliftRandomForestClassifier, CausalTreeRegressor, CausalRandomForestRegressor (the criterion is shared, so causal benefits for free — as discussed for the epic).
  • Not covered (consistent with sklearn): the random splitter and sparse input; and a feature whose non-missing values are all constant is still treated as constant (its missing-only split is not separately proposed).

Tests

New missing-value tests for both the uplift and causal trees/forests:

  • acceptance (fit + predict with NaN, finite/valid outputs), the allow_nan tag + _support_missing_values (dense True / sparse False),
  • all-NaN column and all-NaN prediction row,
  • learned NaN routing — effect concentrated on the missing rows is recovered (NaN rows → high-effect leaf, observed rows → ~0),
  • inf still rejected, forest fit/predict with NaN.

Full tests/test_uplift_trees_kernel.py + tests/test_causal_trees.py pass (154); black clean.

Part of #945. Prerequisite for #955.

@jeongyoonlee
jeongyoonlee marked this pull request as ready for review July 25, 2026 21:12
…945)

Prerequisite for the #955 switchover: mirror scikit-learn's tree missing-value
design so the kernel uplift and causal trees/forests accept NaNs in X natively,
rather than rejecting them.

The shared CausalRegressionCriterion (inherited by every uplift and causal
criterion) now implements scikit-learn's per-feature missing-value machinery:

* NodeSplitState gains a per-group `missing` NodeState; init_missing accumulates
  the tail (sample_indices[end - n_missing:end]) into both the flat sum_missing
  (used by StandardMSE) and the per-group state, respecting the group-encoding
  NaN mask.
* reset/reverse_reset fold the missing block into the child chosen by
  missing_go_to_left; update scans only the non-missing range; StandardMSE's
  children_impurity adds the missing squared-sum when routed left. These are
  no-ops when n_missing == 0, so clean-data behavior is unchanged.

The Python tree layer follows sklearn 1.7: `_support_missing_values` and an
`allow_nan` tag (dense best splitter only), fit validates with
ensure_all_finite=False and threads `_compute_missing_values_in_feature_mask`
into the builder. `_prepare_data` and the forests allow NaN through validation;
non-NaN non-finite values (inf) are still rejected.

Adds missing-value tests (acceptance, tag/support, all-NaN column/row, learned
NaN routing, inf-rejection, forest) for both the kernel uplift and causal
trees/forests. Full uplift-kernel and causal-tree suites pass (154).
@jeongyoonlee
jeongyoonlee force-pushed the feature/955-native-missing-values branch from f88624b to 3310316 Compare July 25, 2026 21:24
@jeongyoonlee jeongyoonlee added the enhancement New feature or request label Jul 25, 2026
@jeongyoonlee
jeongyoonlee merged commit 9fdb6d4 into master Jul 25, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant