Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/user_guide/selection/DropHighPSIFeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ to the PSI value. The most commonly used thresholds are:
- Below 10%, the variable has not experienced a significant shift.
- Above 25%, the variable has experienced a major shift.
- Between those two values, the shift is intermediate.
- 'auto': the threshold will be calculated based on the size of the base and target datasets and the number of bins.
- 'auto': the threshold is calculated from the sizes of the basis and test datasets and
the number of bins fitted for each feature. Repeated quantiles can lead to fewer bins
than requested when using the equal-frequency strategy.

When 'auto', the threshold is calculated using the chi2 approximation, proposed by B. Yurdakul:

Expand Down
20 changes: 10 additions & 10 deletions feature_engine/selection/drop_psi_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,16 +460,6 @@ def fit(self, X: pd.DataFrame, y: pd.Series = None):
else:
bucketer = EqualFrequencyDiscretiser(q=self.bins)

# Set up the threshold for numerical features
if self.threshold == "auto":
threshold_num = self._calculate_auto_threshold(
basis_df.shape[0],
test_df.shape[0],
self.bins,
)
else:
threshold_num = self.threshold

# Set up the generic threshold for categorical features if used
if len(cat_variables_) > 0:
if self.threshold != "auto":
Expand All @@ -495,6 +485,16 @@ def fit(self, X: pd.DataFrame, y: pd.Series = None):
(test_distrib - basis_distrib) * np.log(test_distrib / basis_distrib)
)

if self.threshold == "auto":
# Equal-frequency discretization can drop duplicate boundaries.
threshold_num = self._calculate_auto_threshold(
basis_df.shape[0],
test_df.shape[0],
len(bucketer.binner_dict_[feature]) - 1,
)
else:
threshold_num = self.threshold

# Assess if feature should be dropped
if self.psi_values_[feature] > threshold_num:
self.features_to_drop_.append(feature)
Expand Down
18 changes: 18 additions & 0 deletions tests/test_selection/test_drop_high_psi_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,24 @@ def test_fit_attributes_with_autothreshold(variables, expected_psi, df):
]


@pytest.mark.parametrize("bins", [2, 10])
def test_auto_threshold_uses_realized_quantile_bins(bins):
X = pd.DataFrame(
{
"score": [0] * 50 + [1] * 50 + [0] * 27 + [1] * 73,
"stable": list(range(100)) * 2,
}
)
transformer = DropHighPSIFeatures(bins=bins, threshold="auto")
result = transformer.fit_transform(X)

# Repeated quantiles leave two bins. The PSI for score is above the
# one-degree-of-freedom threshold, but below the nine-degree threshold.
assert transformer.psi_values_["score"] == pytest.approx(0.23 * np.log(0.73 / 0.27))
assert transformer.features_to_drop_ == ["score"]
assert list(result.columns) == ["stable"]


def test_calculation_when_strategy_equal_width(df):
transformer = DropHighPSIFeatures(strategy="equal_width")
transformer.fit(df)
Expand Down