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.
Parameters
| binning_process | BinningProces... 'Longitude']) | |
| estimator | HuberRegressor(max_iter=200) | |
| scaling_method | 'min_max' | |
| scaling_method_params | {'max': 100, 'min': 0} | |
| reverse_scorecard | True | |
| verbose | True | |
| intercept_based | False | |
| rounding | False |
Fitted attributes
| Name | Type | Value |
|---|---|---|
| binning_process_ | BinningProcess | BinningProces... 'Longitude']) |
| estimator_ | HuberRegressor | HuberRegressor(max_iter=200) |
| intercept_ | int | 0 |
BinningProcess(variable_names=['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms',
'Population', 'AveOccup', 'Latitude',
'Longitude'])Parameters
| variable_names | ['MedInc', 'HouseAge', ...] | |
| max_n_prebins | 20 | |
| min_prebin_size | 0.05 | |
| min_n_bins | None | |
| max_n_bins | None | |
| min_bin_size | None | |
| max_bin_size | None | |
| max_pvalue | None | |
| max_pvalue_policy | 'consecutive' | |
| selection_criteria | None | |
| fixed_variables | None | |
| categorical_variables | None | |
| special_codes | None | |
| split_digits | None | |
| binning_fit_params | None | |
| binning_transform_params | None | |
| n_jobs | None | |
| verbose | False |
HuberRegressor(max_iter=200)
Parameters
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.
Parameters
| scorecard | Scorecard(bin... verbose=True) | |
| verbose | True | |
| special_missing | False | |
| n_jobs | 1 |
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)BinningProcess(variable_names=['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms',
'Population', 'AveOccup', 'Latitude',
'Longitude'])Parameters
| variable_names | ['MedInc', 'HouseAge', ...] | |
| max_n_prebins | 20 | |
| min_prebin_size | 0.05 | |
| min_n_bins | None | |
| max_n_bins | None | |
| min_bin_size | None | |
| max_bin_size | None | |
| max_pvalue | None | |
| max_pvalue_policy | 'consecutive' | |
| selection_criteria | None | |
| fixed_variables | None | |
| categorical_variables | None | |
| special_codes | None | |
| split_digits | None | |
| binning_fit_params | None | |
| binning_transform_params | None | |
| n_jobs | None | |
| verbose | False |
HuberRegressor(max_iter=200)
Parameters
[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.
Parameters
| scorecard | Scorecard(bin... verbose=True) | |
| verbose | True | |
| special_missing | False | |
| n_jobs | 1 |
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)BinningProcess(variable_names=['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms',
'Population', 'AveOccup', 'Latitude',
'Longitude'])Parameters
| variable_names | ['MedInc', 'HouseAge', ...] | |
| max_n_prebins | 20 | |
| min_prebin_size | 0.05 | |
| min_n_bins | None | |
| max_n_bins | None | |
| min_bin_size | None | |
| max_bin_size | None | |
| max_pvalue | None | |
| max_pvalue_policy | 'consecutive' | |
| selection_criteria | None | |
| fixed_variables | None | |
| categorical_variables | None | |
| special_codes | None | |
| split_digits | None | |
| binning_fit_params | None | |
| binning_transform_params | None | |
| n_jobs | None | |
| verbose | False |
HuberRegressor(max_iter=200)
Parameters
[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 |