Tutorial: Counterfactual explanations for scorecard with continuous target

This tutorial shows how to generate counterfactual explanations on scorecard models with continuous target. The dataset for this tutorial is https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_california_housing.html.

[1]:
import pandas as pd

from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import HuberRegressor

from optbinning import BinningProcess
from optbinning import Scorecard
from optbinning.scorecard import Counterfactual

Load the dataset

[2]:
data = fetch_california_housing()

target = "target"
variable_names = data.feature_names
X = pd.DataFrame(data.data, columns=variable_names)
y = data.target

Scorecard model

First, we develop a scorecard following the steps presented in previous tutorials.

[3]:
binning_process = BinningProcess(variable_names)

estimator = HuberRegressor(max_iter=200)

scorecard = Scorecard(binning_process=binning_process,
                      estimator=estimator, scaling_method="min_max",
                      scaling_method_params={"min": 0, "max": 100},
                      reverse_scorecard=True,verbose=True)

scorecard.fit(X, y)
2026-09-12 18:17:35,275 | INFO : Scorecard building process started.
2026-09-12 18:17:35,277 | INFO : Options: check parameters.
2026-09-12 18:17:35,508 | INFO : Dataset: continuous target.
2026-09-12 18:17:35,511 | INFO : Binning process started.
2026-09-12 18:17:38,523 | INFO : Binning process terminated. Time: 3.0109s
2026-09-12 18:17:38,525 | INFO : Fitting estimator.
2026-09-12 18:17:39,060 | INFO : Fitting terminated. Time 0.5344s
2026-09-12 18:17:39,064 | INFO : Scorecard table building started.
2026-09-12 18:17:39,173 | INFO : Scorecard table terminated. Time: 0.1097s
2026-09-12 18:17:39,176 | INFO : Scorecard building process terminated. Time: 3.8983s
[3]:
Scorecard(binning_process=BinningProcess(variable_names=['MedInc', 'HouseAge',
                                                         'AveRooms',
                                                         'AveBedrms',
                                                         'Population',
                                                         'AveOccup', 'Latitude',
                                                         'Longitude']),
          estimator=HuberRegressor(max_iter=200), reverse_scorecard=True,
          scaling_method='min_max',
          scaling_method_params={'max': 100, 'min': 0}, verbose=True)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Generating counterfactual explanations

As an input data point or query, we select the first sample. Note that a query must be either a dictionary of a pandas DataFrame.

[4]:
query = X.iloc[0, :].to_frame().T
[5]:
query
[5]:
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude
0 8.3252 41.0 6.984127 1.02381 322.0 2.555556 37.88 -122.23
[6]:
scorecard.predict(query)
[6]:
array([4.29854245])

The predicted outcome (house value) for this query (house) is 4.3. We want to generate counterfactual explanations to find out how to increase the house value to at least 4.5.

[7]:
cf = Counterfactual(scorecard=scorecard, verbose=True)
[8]:
cf.fit(X)
2026-09-12 18:17:39,422 | INFO : Counterfactual fit started.
2026-09-12 18:17:39,424 | INFO : Options: check parameters.
2026-09-12 18:17:39,425 | INFO : Compute optimization problem data.
2026-09-12 18:17:39,477 | INFO : Counterfactual fit terminated. Time: 0.0546s
[8]:
Counterfactual(scorecard=Scorecard(binning_process=BinningProcess(variable_names=['MedInc',
                                                                                  'HouseAge',
                                                                                  'AveRooms',
                                                                                  'AveBedrms',
                                                                                  'Population',
                                                                                  'AveOccup',
                                                                                  'Latitude',
                                                                                  'Longitude']),
                                   estimator=HuberRegressor(max_iter=200),
                                   reverse_scorecard=True,
                                   scaling_method='min_max',
                                   scaling_method_params={'max': 100, 'min': 0},
                                   verbose=True),
               verbose=True)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
[9]:
cf.generate(query=query, y=4.5, outcome_type="continuous", n_cf=1,
            max_changes=3, hard_constraints=["min_outcome"])
2026-09-12 18:17:39,560 | INFO : Counterfactual generation started.
2026-09-12 18:17:39,565 | INFO : Options: check parameters.
2026-09-12 18:17:39,573 | INFO : Options: check objectives and constraints.
2026-09-12 18:17:39,575 | INFO : Optimizer started.
2026-09-12 18:17:39,576 | INFO : Optimizer: build model...
2026-09-12 18:17:39,597 | INFO : Optimizer: solve...
2026-09-12 18:17:40,067 | INFO : Optimizer terminated. Time: 0.4912s
2026-09-12 18:17:40,074 | INFO : Post-processing started.
2026-09-12 18:17:40,105 | INFO : Post-processing terminated. Time: 0.0284s
2026-09-12 18:17:40,119 | INFO : Counterfactual generation terminated. Status: OPTIMAL. Time: 0.5583s
[9]:
Counterfactual(scorecard=Scorecard(binning_process=BinningProcess(variable_names=['MedInc',
                                                                                  'HouseAge',
                                                                                  'AveRooms',
                                                                                  'AveBedrms',
                                                                                  'Population',
                                                                                  'AveOccup',
                                                                                  'Latitude',
                                                                                  'Longitude']),
                                   estimator=HuberRegressor(max_iter=200),
                                   reverse_scorecard=True,
                                   scaling_method='min_max',
                                   scaling_method_params={'max': 100, 'min': 0},
                                   verbose=True),
               verbose=True)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
[10]:
cf.information()
optbinning (Version 1.0.0)
Copyright (c) 2019-2026 Guillermo Navas-Palencia, Apache License 2.0

  Status  : OPTIMAL

  Solver statistics
    Type                                 mip
    Number of variables                   42
    Number of constraints                120
    Objective value                   7.7965
    Best objective bound              7.7965

  Objectives
    proximity                         0.9862
    closeness                         6.8103

  Timing
    Total time                          0.57 sec
    Fit                                 0.05 sec   (  9.51%)
    Solver                              0.49 sec   ( 85.54%)
    Post-processing                     0.03 sec   (  5.79%)

The generate counterfactual suggest increasing the block population, reduce the average house occupancy and change the house block longitude. None of them seems doable.

[11]:
cf.display(show_only_changes=True, show_outcome=True)
[11]:
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude outcome
0 - - - [1.05, 1.07) [986.50, 1426.50) - [34.10, 34.18) - 4.522533

Now, let’s generate several counterfactuals aiming to limit the house value to 4.0.

[12]:
cf.generate(query=query, y=4.0, outcome_type="continuous", n_cf=3,
            max_changes=3,
            hard_constraints=["diversity_features", "max_outcome"],
            time_limit=30
           ).display(show_only_changes=True, show_outcome=True)
2026-09-12 18:17:40,551 | INFO : Counterfactual generation started.
2026-09-12 18:17:40,555 | INFO : Options: check parameters.
2026-09-12 18:17:40,598 | INFO : Options: check objectives and constraints.
2026-09-12 18:17:40,602 | INFO : Optimizer started.
2026-09-12 18:17:40,613 | INFO : Optimizer: build model...
2026-09-12 18:17:40,819 | INFO : Optimizer: solve...
2026-09-12 18:18:10,663 | INFO : Optimizer terminated. Time: 30.0500s
2026-09-12 18:18:10,668 | INFO : Post-processing started.
2026-09-12 18:18:10,693 | INFO : Post-processing terminated. Time: 0.0229s
2026-09-12 18:18:10,696 | INFO : Counterfactual generation terminated. Status: FEASIBLE. Time: 30.1446s
[12]:
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude outcome
0 - - - - [986.50, 1426.50) [3.11, 3.24) - [-118.91, inf) 3.948499
0 [5.79, 6.82) - - - [986.50, 1426.50) [2.90, 3.01) - - 3.233206
0 [5.79, 6.82) - - [1.07, 1.10) [986.50, 1426.50) - - - 3.350462

And the same generation enforcing diversity on feature values.

[13]:
cf.generate(query=query, y=3.0, outcome_type="continuous", n_cf=3,
            max_changes=3,
            hard_constraints=["diversity_features", "diversity_values", "max_outcome"],
            time_limit=30
           ).display(show_only_changes=True, show_outcome=True)
2026-09-12 18:18:10,757 | INFO : Counterfactual generation started.
2026-09-12 18:18:10,761 | INFO : Options: check parameters.
2026-09-12 18:18:10,770 | INFO : Options: check objectives and constraints.
2026-09-12 18:18:10,775 | INFO : Optimizer started.
2026-09-12 18:18:10,777 | INFO : Optimizer: build model...
2026-09-12 18:18:10,883 | INFO : Optimizer: solve...
2026-09-12 18:18:40,906 | INFO : Optimizer terminated. Time: 30.1295s
2026-09-12 18:18:40,909 | INFO : Post-processing started.
2026-09-12 18:18:40,931 | INFO : Post-processing terminated. Time: 0.0196s
2026-09-12 18:18:40,933 | INFO : Counterfactual generation terminated. Status: FEASIBLE. Time: 30.1765s
[13]:
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude outcome
0 [5.04, 5.79) - - - [1426.50, 1911.50) - - [-118.91, inf) 2.744065
0 [4.53, 5.04) - - [1.07, 1.10) [1911.50, 2720.50) - - - 2.620019
0 [5.79, 6.82) - - - [986.50, 1426.50) [3.52, 3.82) - - 2.918294