Skip to content

Release v0.1.7#2

Merged
xRiskLab merged 3 commits intomainfrom
release/0.1.7a0
Apr 11, 2026
Merged

Release v0.1.7#2
xRiskLab merged 3 commits intomainfrom
release/0.1.7a0

Conversation

@xRiskLab
Copy link
Copy Markdown
Owner

@xRiskLab xRiskLab commented Apr 11, 2026

Summary

  • finetune() method: WOE recalibration without redevelopment - update one feature's likelihood ratios while keeping bin edges, other features, and the global prior frozen
  • Asymptotic SE of Somers' D: Uncertainty quantification via Goktas & Oznur (2011) contingency-table ASE, validated against VUROCS R package
  • somersd_se() in fastwoe.metrics: Works for binary, ordinal, and continuous targets; new somersd_se, somersd_ci_lower, somersd_ci_upper fields in feature stats
  • Deprecation of fastwoe.fast_somersd: Import from fastwoe.metrics instead

Test plan

  • 132 tests passing (116 test_fastwoe + 16 test_metrics)
  • mypy clean
  • Somers' D ASE validated against VUROCS R package (exact numerical match)
  • Finetune: 13 test cases covering categorical, numerical, error handling, prior update, missing bins, transform-after-finetune, method chaining

Summary by Sourcery

Release version 0.1.7 with WOE finetuning, Somers' D standard error support, enhanced CAP plotting, instance-level Gini contributions, and expanded Python/CI compatibility.

New Features:

  • Add FastWoe.finetune() to recalibrate WOE values on new data while preserving existing bin structures and optional prior updates.
  • Expose asymptotic standard error computation for Somers' D via somersd_se(), integrating Somers' D SE and confidence intervals into feature statistics.
  • Introduce gini_contributions() for instance-level decomposition of the Gini coefficient and update init exports accordingly.
  • Enhance CAP/power curve plotting with configurable zoom into the top fraction of the population via the top_p parameter and styling tweaks.
  • Provide new example notebooks demonstrating finetuning, CAP curves with weighted Gini, and Gini contributions usage.

Enhancements:

  • Refactor binning logic into a reusable _apply_binning_to_column helper used by both transform() and finetune().
  • Improve Somers' D internals with clearer typing and contingency-table based concordant/discordant matrix helpers.
  • Update plot_performance aesthetics (figure size, colors, labels, and ideal/random curve layering) for clearer CAP visualisation.
  • Deprecate fastwoe.fast_somersd in favour of fastwoe.metrics, emitting a deprecation warning while preserving backward compatibility.

Build:

  • Relax supported Python upper bound to allow >=3.9 and add ruff and mypy configuration tweaks for examples and core modules.
  • Add a test-all Makefile target to run the test suite across all supported Python versions.

CI:

  • Extend CI and compatibility workflows to test against Python 3.13 and 3.14 with appropriate scikit-learn versions.

Documentation:

  • Document the Somers' D asymptotic standard error derivation and usage in a dedicated somersd_ase.md reference.
  • Update README links for monotonic constraints and add notebooks showcasing new finetuning, CAP, and Gini contributions functionality.
  • Update CHANGELOG for version 0.1.7 describing new finetuning, Somers' D SE, and testing additions.

Tests:

  • Add comprehensive tests for Somers' D standard error including validation against VUROCS and feature-level SE/CI consistency checks.
  • Add a TestFinetune suite covering categorical and numerical features, prior updates, error handling, missing bins, transform-after-finetune, and method chaining behavior.
  • Extend metrics tests for Gini contributions to verify sum-to-gini property, agreement with Somers' D, edge cases, and known-value scenarios.

Chores:

  • Bump library version metadata from 0.1.6 to 0.1.7 and adjust logging message formatting for Somers' D-related tests.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 11, 2026

Reviewer's Guide

Implements WOE finetuning without rebucketing, adds asymptotic standard errors for Somers’ D/Gini with per-feature CI outputs, introduces instance-level Gini contribution decomposition and CAP-curve enhancements, updates docs/examples and test/CI tooling, and formally deprecates the old fast_somersd import path in favor of fastwoe.metrics.

Sequence diagram for FastWoe.finetune WOE recalibration

sequenceDiagram
    actor DataScientist
    participant FastWoe as FastWoe_instance
    participant Metrics as MetricsModule

    DataScientist->>FastWoe: finetune(X_new, y_new, update_prior)
    FastWoe->>FastWoe: check is_fitted_
    FastWoe->>FastWoe: check is_multiclass_target
    FastWoe->>FastWoe: X_df = _ensure_dataframe(X_new, use_fitted_names=True)
    FastWoe->>FastWoe: y_ser = _ensure_series(y_new)
    FastWoe->>FastWoe: validate_lengths_and_binary(y_ser)
    FastWoe->>FastWoe: detect_unknown_cols(X_df, mappings_)
    FastWoe-->>DataScientist: warnings for unknown_cols
    alt update_prior_true
        FastWoe->>FastWoe: y_prior_ = mean(y_ser)
        FastWoe->>FastWoe: odds_prior_ = y_prior_ / (1 - y_prior_)
        FastWoe-->>DataScientist: warning about_stale_features
    end
    FastWoe->>FastWoe: odds_prior = y_prior_ / (1 - y_prior_)
    loop for each col in cols_to_update
        FastWoe->>FastWoe: _recalibrate_feature(col, X_df, y_ser, odds_prior)
        activate FastWoe
        FastWoe->>FastWoe: binned_col = _apply_binning_to_column(X_df, col)
        FastWoe->>FastWoe: old_mapping = mappings_[col]
        FastWoe->>FastWoe: new_stats = groupby_bin(binned_col, y_ser)
        FastWoe->>FastWoe: detect_missing_bins_and_extra_bins
        FastWoe-->>DataScientist: warnings for_missing_or_extra_bins
        FastWoe->>FastWoe: build new_mapping_df with updated counts and woe
        FastWoe->>FastWoe: mappings_[col] = new_mapping_df
        FastWoe->>FastWoe: enc = TargetEncoder(...)
        FastWoe->>FastWoe: enc.fit(binned_col, y_ser)
        FastWoe->>FastWoe: encoders_[col] = enc
        FastWoe->>FastWoe: stats = _calculate_feature_stats(col, binned_col, y_ser, new_mapping_df)
        FastWoe->>FastWoe: feature_stats_[col] = stats
        deactivate FastWoe
    end
    FastWoe-->>DataScientist: return self_for_chaining
Loading

Class diagram for FastWoe finetuning and related metrics

classDiagram
    class FastWoe {
        +bool is_fitted_
        +bool is_multiclass_target
        +dict mappings_
        +dict binners_
        +dict binning_info_
        +dict encoders_
        +dict feature_stats_
        +float y_prior_
        +float odds_prior_
        +dict encoder_kwargs
        +int random_state
        +list cat_features_
        +fit(X, y, cat_features)
        +transform(X)
        +fit_transform(X, y)
        +finetune(X_new, y_new, update_prior) FastWoe
        +get_mapping(feature, class_label) pd.DataFrame
        +get_feature_stats() pd.DataFrame
        -_calculate_gini(y_true, y_pred) float
        -_calculate_somersd_se(y_true, y_pred) float
        -_calculate_woe_se(good_count, bad_count) float
        -_calculate_woe_ci(woe, se) tuple
        -_calculate_iv_standard_error(mapping_df, total_good, total_bad) float
        -_calculate_iv_confidence_interval(iv_value, iv_se) tuple
        -_calculate_feature_stats(col, X, y, mapping_df) dict
        -_ensure_dataframe(X, use_fitted_names) pd.DataFrame
        -_ensure_series(y) pd.Series
        -_apply_binning_to_column(X, col) pd.Series
        -_recalibrate_feature(col, X_new, y_new, odds_prior) None
        -_bin_with_tree(col, X, y) dict
    }

    class MetricsModule {
        <<utility>>
        +somersd_yx(y, x) SomersDResult
        +somersd_xy(y, x) SomersDResult
        +somersd_pairwise(y_true, y_pred) SomersDResult
        +somersd_clustered_matrix(df, score_col, ...) pd.DataFrame
        +somersd_se(y_true, y_pred) float
        +gini_contributions(scores, labels) tuple
        -_somers_yx_core(y, x) tuple
        -_somers_xy_core(y, x) tuple
        -_concordant_discordant_matrices(CT) tuple
    }

    class PlotsModule {
        <<utility>>
        +plot_performance(y_true, y_pred, weights, ax, figsize, dpi, show_plot, labels, colors, top_p) tuple
    }

    FastWoe ..> MetricsModule : uses_somersd_se
    FastWoe ..> MetricsModule : uses_somersd_yx
    FastWoe ..> PlotsModule : user_level_integration
Loading

File-Level Changes

Change Details Files
Switch Somers’ D utilities to fastwoe.metrics and surface Somers’ D SE and CIs in feature statistics.
  • Import somersd_yx and new somersd_se from fastwoe.metrics instead of fastwoe.fast_somersd.
  • Add _calculate_somersd_se helper and compute Somers’ D SE and 95% CI per feature in _calculate_feature_stats, storing somersd_se, somersd_ci_lower, somersd_ci_upper fields.
  • Extend tests to validate Somers’ D SE properties and numerical match against the VUROCS R reference implementation.
fastwoe/fastwoe.py
fastwoe/metrics.py
tests/test_fastwoe.py
tests/test_metrics.py
docs/somersd_ase.md
Refactor numeric bin application and add WOE finetuning API that recalibrates bin-level likelihood ratios using new data while keeping bin structure fixed.
  • Extract the numeric/categorical bin-application logic from transform into a reusable _apply_binning_to_column that supports kbins, tree, and faiss_kmeans methods with consistent labeling and missing handling.
  • Update transform to call _apply_binning_to_column for each binned column, reducing duplication and centralizing bin application behavior.
  • Introduce finetune(X_new, y_new, update_prior=False) on FastWoe to recompute per-bin WOE and uncertainties for existing features only, with validation, unknown-column skipping, optional prior update, and detailed UserWarning paths.
  • Implement recalibrate_feature to handle per-feature binning of new data, aggregation of new bin stats, WOE/SE/CI recomputation (with fallbacks for missing/new bins), TargetEncoder refit, and feature_stats refresh.
  • Add a comprehensive TestFinetune suite covering categorical and numeric finetuning, length/type validation, multiclass rejection, unknown/missing bins, prior update behavior, post-finetune transform semantics, and method chaining.
fastwoe/fastwoe.py
tests/test_fastwoe.py
examples/notebooks/fastwoe_finetuning.ipynb
Add Gini contribution decomposition and Somers’ D ASE implementation in metrics, with extensive tests and documentation.
  • Implement gini_contributions(scores, labels) to compute per-observation Gini/Somers’ D contributions in O(n log n) using sorted positives/negatives and searchsorted, normalised so contributions.sum() equals the global Gini.
  • Implement contingency-table helpers _concordant_discordant_matrices and somersd_se(y_true, y_pred) that compute Goktas & Oznur style ASE via per-cell concordant/discordant counts and delta method, handling degenerate inputs gracefully.
  • Add TestGiniContributions test class covering sum-to-Gini equality, consistency with somersd_yx, known-value checks, perfect/worst separation edge cases, all-same-label behavior, and shape invariants.
  • Document the Somers’ D ASE derivation and implementation details in docs/somersd_ase.md and expose gini_contributions at the package root.
  • Tighten typing in low-level Somers’ D routines by annotating temporary arrays (y_rank, bit) as np.ndarray for mypy friendliness.
fastwoe/metrics.py
tests/test_metrics.py
docs/somersd_ase.md
fastwoe/__init__.py
examples/notebooks/fastwoe_gini_contributions.ipynb
Enhance CAP/performance plotting API and add notebooks demonstrating new metrics and plots.
  • Extend plot_performance with top_p parameter to zoom the x-axis to the top p fraction of the population, adjust labels to emphasize recall vs population fraction, and improve default figsize/dpi and color ordering.
  • Refine CAP rendering order to draw the random line first, model curves next, and ideal model line on top, and change legend text to report Gini instead of AR for each model.
  • Add notebooks for CAP curves and weighted Gini, Gini contribution diagnostics, and fastwoe finetuning workflows, replacing the previous CAP example notebook.
  • Improve error-handling for externally provided Axes by validating that ax.get_figure() returns a real Figure, not a SubFigure.
  • Update README monotonic constraints example path to the new examples/scripts layout.
fastwoe/plots.py
examples/notebooks/fastwoe_cap_curve.ipynb
examples/notebooks/fastwoe_gini_contributions.ipynb
examples/notebooks/fastwoe_finetuning.ipynb
README.md
Deprecate fastwoe.fast_somersd in favor of fastwoe.metrics and update packaging, tooling, and CI to support newer Python versions.
  • Add a module-level DeprecationWarning in fastwoe.fast_somersd directing users to import from fastwoe.metrics, while re-exporting the existing API for backward compatibility.
  • Bump package version to 0.1.7 and relax requires-python upper bound, adding classifiers for Python 3.13 and 3.14.
  • Extend CI and compatibility workflows to run against Python 3.13 and 3.14 and update Makefile with a test-all target that runs the test suite across supported Python versions via uv.
  • Refine ruff configuration to ignore docstring/name rules under examples/**/*, and tweak mypy config to disable warn_return_any for modules that return DataFrames from pandas operations.
  • Update the changelog with a detailed 0.1.7 entry summarizing finetune, Somers’ D ASE, internal refactors, tests, and new notebooks.
fastwoe/fast_somersd.py
fastwoe/__init__.py
pyproject.toml
Makefile
.github/workflows/ci.yml
.github/workflows/compatibility.yml
.ruff.toml
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues, and left some high level feedback:

  • In plot_performance, consider validating top_p (e.g., 0 < top_p <= 1) and raising a clear error for invalid values so users don’t get confusing axis behavior when passing out-of-range inputs.
  • The new _apply_binning_to_column duplicates bin-label construction logic that also appears in fit/transform; factoring this into a shared helper (e.g., format_bin_labels(bin_edges, method)) would reduce duplication and make it easier to keep label semantics consistent across code paths (including finetune).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `plot_performance`, consider validating `top_p` (e.g., 0 < top_p <= 1) and raising a clear error for invalid values so users don’t get confusing axis behavior when passing out-of-range inputs.
- The new `_apply_binning_to_column` duplicates bin-label construction logic that also appears in `fit`/`transform`; factoring this into a shared helper (e.g., `format_bin_labels(bin_edges, method)`) would reduce duplication and make it easier to keep label semantics consistent across code paths (including `finetune`).

## Individual Comments

### Comment 1
<location path="fastwoe/metrics.py" line_range="485-494" />
<code_context>
+def somersd_se(
</code_context>
<issue_to_address>
**issue (performance):** somersd_se builds a full contingency table and runs O(a*b) loops, which can explode for near-continuous inputs.

This builds a contingency table of shape `(len(unique(y_true)), len(unique(y_pred)))` and iterates over all cells, so for high-cardinality or continuous-like inputs it can approach O(n^2) time and memory. Since the docstring claims support for continuous targets, callers may hit this path with large arrays. Please either (a) document that this is intended for low-cardinality/binned inputs, and/or (b) guard against large `len(y_uniq) * len(x_uniq)` (e.g., return `np.nan` or similar), or (c) add an alternative path for continuous inputs that avoids quadratic behavior.
</issue_to_address>

### Comment 2
<location path="fastwoe/metrics.py" line_range="378-387" />
<code_context>
+def gini_contributions(
</code_context>
<issue_to_address>
**issue (bug_risk):** gini_contributions accepts arbitrary integer labels but assumes binary 0/1 semantics.

`labels` are cast to `int32` but never validated to be exactly {0,1}. If callers pass values like {−1,1}, booleans, or multi-class labels, the positive/negative logic and pair counts become incorrect while still returning a result. To prevent silent misuse, add an explicit check (e.g., via `np.unique(labels)` and comparing to `[0, 1]`) and raise a `ValueError` or at least emit a warning when the assumption is violated.
</issue_to_address>

### Comment 3
<location path="fastwoe/plots.py" line_range="44-47" />
<code_context>
     show_plot: bool = True,
     labels: Optional[list[str]] = None,
     colors: Optional[list[str]] = None,
+    top_p: Optional[float] = None,
 ) -> tuple:
     """
</code_context>
<issue_to_address>
**issue (bug_risk):** top_p is not validated and can lead to confusing axes or errors for out-of-range values.

The docstring says `top_p` is in (0, 1], but the code doesn’t enforce this. For `top_p <= 0` or `top_p > 1`, the x-axis limits/ticks become misleading and break the CAP interpretation. Please validate `0 < top_p <= 1` and raise `ValueError` otherwise, and consider clamping `xlim` to [0, 1] instead of starting slightly negative to avoid showing a negative population fraction.
</issue_to_address>

### Comment 4
<location path="fastwoe/fastwoe.py" line_range="1154-1163" />
<code_context>
+        total_new = new_stats["count"].sum() if len(new_stats) > 0 else len(y_new)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Mixed use of old and new sample sizes makes `count_pct` inconsistent across bins after finetune.

For bins present in `new_stats` you recompute `count`/`count_pct` using `total_new`, but for bins missing in the new data you keep the original `count`/`count_pct`. After finetune this breaks consistency: `count` no longer sums to a coherent total, and `count_pct` no longer sums to 100% and mixes baselines. If `count_pct` is meant to reflect the finetune sample, it should be recomputed against `total_new` for all bins (while preserving original WOE/CI where needed), or you should expose separate columns like `orig_count_pct` and `finetune_count_pct`.

Suggested implementation:

```python
        rows = []
        # Use a single, consistent total for all bins when recomputing count/count_pct
        total_new = int(new_stats["count"].sum()) if len(new_stats) > 0 else int(len(y_new))
        for cat in old_mapping.index:
            if cat in new_stats.index:
                # Start from new stats but ensure count_pct is recomputed against total_new
                row = new_stats.loc[cat].copy()
                count = int(row["count"])
                row["count_pct"] = count / total_new if total_new > 0 else 0.0

```

To fully enforce consistency:
1. In the `else` branch for `if cat in new_stats.index:` (the branch that handles bins missing in `new_stats` but present in `old_mapping`), ensure you also recompute `count_pct` using `total_new`:
   - Use the preserved `count` for that bin (likely coming from `old_mapping`) and set `row["count_pct"] = count / total_new if total_new > 0 else 0.0` before appending it to `rows`.
2. If `count` for missing bins is currently taken directly from `old_mapping` without converting to `int`, align it with the above pattern (`count = int(...)`) so all `count` values across bins sum coherently to the same universe (the finetune sample or the chosen effective total).
3. If the public API or downstream consumers rely on the original baseline percentages, consider adding separate columns such as `orig_count_pct` (from the original mapping) and `finetune_count_pct` (the recomputed one above) and populating both during mapping construction.
</issue_to_address>

### Comment 5
<location path="fastwoe/fast_somersd.py" line_range="13-17" />
<code_context>
+
 from .metrics import SomersDResult, somersd_pairwise, somersd_xy, somersd_yx

+warnings.warn(
+    "fastwoe.fast_somersd is deprecated. Import from fastwoe.metrics instead.",
+    DeprecationWarning,
+    stacklevel=2,
+)
+
</code_context>
<issue_to_address>
**suggestion:** Deprecation warning stacklevel is likely too low to point at the user’s import site.

Since this warning is raised at module import time, `stacklevel=2` will usually point at the top of `fast_somersd.py` rather than the caller’s import line. Consider increasing `stacklevel` (e.g., to 3 or 4) so it highlights the user’s `import fastwoe.fast_somersd` call instead.

```suggestion
warnings.warn(
    "fastwoe.fast_somersd is deprecated. Import from fastwoe.metrics instead.",
    DeprecationWarning,
    stacklevel=4,
)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +485 to +494
def somersd_se(
y_true: np.ndarray,
y_pred: np.ndarray,
) -> float:
"""Asymptotic SE of Somers' D(X|Y) per Goktas & Oznur (2011).

Computes the asymptotic standard error from the contingency table of
(y_true, y_pred) using per-cell concordant/discordant counts and the
delta method for the ratio statistic.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (performance): somersd_se builds a full contingency table and runs O(a*b) loops, which can explode for near-continuous inputs.

This builds a contingency table of shape (len(unique(y_true)), len(unique(y_pred))) and iterates over all cells, so for high-cardinality or continuous-like inputs it can approach O(n^2) time and memory. Since the docstring claims support for continuous targets, callers may hit this path with large arrays. Please either (a) document that this is intended for low-cardinality/binned inputs, and/or (b) guard against large len(y_uniq) * len(x_uniq) (e.g., return np.nan or similar), or (c) add an alternative path for continuous inputs that avoids quadratic behavior.

Comment on lines +378 to +387
def gini_contributions(
scores: np.ndarray,
labels: np.ndarray,
) -> tuple[np.ndarray, float]:
"""Calculate each observation's contribution to the Gini coefficient.

Assigns a signed contribution to every observation based on how well it is
ranked relative to observations of the opposite class (Somers' D definition,
so ties receive zero credit).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): gini_contributions accepts arbitrary integer labels but assumes binary 0/1 semantics.

labels are cast to int32 but never validated to be exactly {0,1}. If callers pass values like {−1,1}, booleans, or multi-class labels, the positive/negative logic and pair counts become incorrect while still returning a result. To prevent silent misuse, add an explicit check (e.g., via np.unique(labels) and comparing to [0, 1]) and raise a ValueError or at least emit a warning when the assumption is violated.

Comment on lines +44 to 47
top_p: Optional[float] = None,
) -> tuple:
"""
Plot model performance curve (CAP for binary, Power curve for continuous).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): top_p is not validated and can lead to confusing axes or errors for out-of-range values.

The docstring says top_p is in (0, 1], but the code doesn’t enforce this. For top_p <= 0 or top_p > 1, the x-axis limits/ticks become misleading and break the CAP interpretation. Please validate 0 < top_p <= 1 and raise ValueError otherwise, and consider clamping xlim to [0, 1] instead of starting slightly negative to avoid showing a negative population fraction.

Comment on lines +1154 to +1163
total_new = new_stats["count"].sum() if len(new_stats) > 0 else len(y_new)
for cat in old_mapping.index:
if cat in new_stats.index:
row = new_stats.loc[cat]
count = int(row["count"])
bad_count = int(row["bad_count"])
good_count = int(row["good_count"])
event_rate = np.clip(float(row["event_rate"]), 1e-15, 1 - 1e-15)
odds_cat = event_rate / (1 - event_rate)
woe = float(np.log(odds_cat / odds_prior))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Mixed use of old and new sample sizes makes count_pct inconsistent across bins after finetune.

For bins present in new_stats you recompute count/count_pct using total_new, but for bins missing in the new data you keep the original count/count_pct. After finetune this breaks consistency: count no longer sums to a coherent total, and count_pct no longer sums to 100% and mixes baselines. If count_pct is meant to reflect the finetune sample, it should be recomputed against total_new for all bins (while preserving original WOE/CI where needed), or you should expose separate columns like orig_count_pct and finetune_count_pct.

Suggested implementation:

        rows = []
        # Use a single, consistent total for all bins when recomputing count/count_pct
        total_new = int(new_stats["count"].sum()) if len(new_stats) > 0 else int(len(y_new))
        for cat in old_mapping.index:
            if cat in new_stats.index:
                # Start from new stats but ensure count_pct is recomputed against total_new
                row = new_stats.loc[cat].copy()
                count = int(row["count"])
                row["count_pct"] = count / total_new if total_new > 0 else 0.0

To fully enforce consistency:

  1. In the else branch for if cat in new_stats.index: (the branch that handles bins missing in new_stats but present in old_mapping), ensure you also recompute count_pct using total_new:
    • Use the preserved count for that bin (likely coming from old_mapping) and set row["count_pct"] = count / total_new if total_new > 0 else 0.0 before appending it to rows.
  2. If count for missing bins is currently taken directly from old_mapping without converting to int, align it with the above pattern (count = int(...)) so all count values across bins sum coherently to the same universe (the finetune sample or the chosen effective total).
  3. If the public API or downstream consumers rely on the original baseline percentages, consider adding separate columns such as orig_count_pct (from the original mapping) and finetune_count_pct (the recomputed one above) and populating both during mapping construction.

Comment on lines +13 to +17
warnings.warn(
"fastwoe.fast_somersd is deprecated. Import from fastwoe.metrics instead.",
DeprecationWarning,
stacklevel=2,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Deprecation warning stacklevel is likely too low to point at the user’s import site.

Since this warning is raised at module import time, stacklevel=2 will usually point at the top of fast_somersd.py rather than the caller’s import line. Consider increasing stacklevel (e.g., to 3 or 4) so it highlights the user’s import fastwoe.fast_somersd call instead.

Suggested change
warnings.warn(
"fastwoe.fast_somersd is deprecated. Import from fastwoe.metrics instead.",
DeprecationWarning,
stacklevel=2,
)
warnings.warn(
"fastwoe.fast_somersd is deprecated. Import from fastwoe.metrics instead.",
DeprecationWarning,
stacklevel=4,
)

@xRiskLab xRiskLab merged commit 0a8f5bb into main Apr 11, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant