Skip to content

Modeling_Tool.Model

Model training layer: logistic regression (LRMaster), gradient boosting with LightGBM, XGBoost, and CatBoost (GradientBoostingModel), and backward variable elimination. The subpackage loads on first access of one of its names, and the boosting libraries load when you build a model of that type.

Logistic Regression: LRM_Tool

LRM_Tool

FeatureSelectionAnalyzer

Feature selection analyzer using statistical tests.

Analyzes feature relevance using chi-squared tests, correlation analysis, and variance inflation factor (VIF) for multicollinearity detection.

Parameters:

Name Type Description Default
significance_level float

Significance level for statistical tests

0.05

Attributes:

Name Type Description
significance_level float

Significance level; chi2_selection marks a feature as selected when its p-value is below it.

selected_features_ list of str or None

Features selected by the last chi2_selection call, in the order of its result table (None until that method runs).

chi2_results_ DataFrame or None

Result table of the last chi2_selection call (None until that method runs).

Examples:

>>> analyzer = FeatureSelectionAnalyzer(significance_level=0.05)
>>> results = analyzer.chi2_selection(train_df, feature_cols, 'target')
>>> vif_df = analyzer.compute_vif(train_df[feature_cols])
Source code in Modeling_Tool/Model/LRM_Tool.py
class FeatureSelectionAnalyzer:
    """
    Feature selection analyzer using statistical tests.

    Analyzes feature relevance using chi-squared tests, correlation analysis,
    and variance inflation factor (VIF) for multicollinearity detection.

    Parameters
    ----------
    significance_level : float, default 0.05
        Significance level for statistical tests

    Attributes
    ----------
    significance_level : float
        Significance level; ``chi2_selection`` marks a feature as selected when its p-value is below it.
    selected_features_ : list of str or None
        Features selected by the last ``chi2_selection`` call, in the order of its result table (``None`` until
        that method runs).
    chi2_results_ : pandas.DataFrame or None
        Result table of the last ``chi2_selection`` call (``None`` until that method runs).

    Examples
    --------
    >>> analyzer = FeatureSelectionAnalyzer(significance_level=0.05)
    >>> results = analyzer.chi2_selection(train_df, feature_cols, 'target')
    >>> vif_df = analyzer.compute_vif(train_df[feature_cols])
    """

    def __init__(self, significance_level=0.05):
        """
        Initialize FeatureSelectionAnalyzer.

        Parameters
        ----------
        significance_level : float, default 0.05
            Significance level threshold for feature selection
        """
        self.significance_level = significance_level
        self.selected_features_ = None
        self.chi2_results_ = None

    def chi2_selection(self, data, feature_cols, target_col, nan_handling="fillna_median", nan_warn_threshold=0.05):
        """
        Select features using chi-squared test.

        Parameters
        ----------
        data : pd.DataFrame
            Input data
        feature_cols : list of str
            Feature column names to evaluate
        target_col : str
            Target variable column name
        nan_handling : str, default "fillna_median"
            How missing values in the feature columns are handled before the test. ``"fillna_0"``, ``"fillna_mean"`` and
            ``"fillna_median"`` fill them with 0, the column mean and the column median (0 for a column that is entirely
            missing); ``"drop_rows"`` drops every row with a missing feature value (the target is taken from the
            remaining rows); ``"raise"`` raises ``ValueError`` if any feature column has a missing value. Any other
            value raises ``ValueError``.
        nan_warn_threshold : float, default 0.05
            A ``RuntimeWarning`` is issued for each feature column whose share of NaN/Inf values exceeds this fraction
            (measured before ``nan_handling`` is applied). The warning does not change how the values are handled.

        Returns
        -------
        pd.DataFrame
            Results with columns ['feature', 'chi2', 'p_value', 'selected'], sorted by ``chi2`` in descending order.
            ``selected`` is ``p_value < significance_level``.

        Notes
        -----
        The features are scaled to [0, 1] with ``MinMaxScaler`` before the test, because the chi-squared statistic needs
        non-negative values. The result table is also stored in ``chi2_results_`` and the selected feature names in
        ``selected_features_``.
        """
        from sklearn.feature_selection import chi2
        from sklearn.preprocessing import MinMaxScaler

        x = _prepare_nan_handled_frame(
            data[feature_cols],
            nan_handling=nan_handling,
            warn_threshold=nan_warn_threshold,
            context="FeatureSelectionAnalyzer.chi2_selection",
        )
        y = data.loc[x.index, target_col]

        scaler = MinMaxScaler()
        x_scaled = scaler.fit_transform(x)

        chi2_vals, p_vals = chi2(x_scaled, y)

        results = pd.DataFrame({
            'feature': feature_cols,
            'chi2': chi2_vals,
            'p_value': p_vals,
            'selected': p_vals < self.significance_level
        }).sort_values('chi2', ascending=False).reset_index(drop=True)

        self.chi2_results_ = results
        self.selected_features_ = results.loc[results['selected'], 'feature'].tolist()
        return results

    def compute_vif(
        self,
        data,
        nan_handling="fillna_median",
        nan_warn_threshold=0.05,
        sample_weight=None,
    ):
        """
        Compute Variance Inflation Factor (VIF) for multicollinearity detection.

        Parameters
        ----------
        data : pd.DataFrame
            Feature matrix (should not include target variable)
        nan_handling : str, default "fillna_median"
            How missing values are handled before the VIF is computed. Same options as in ``chi2_selection``:
            ``"fillna_0"``, ``"fillna_mean"``, ``"fillna_median"``, ``"drop_rows"`` (rows with a missing value are
            dropped, together with their ``sample_weight`` entries) or ``"raise"`` (``ValueError`` if any column has a
            missing value). Any other value raises ``ValueError``.
        nan_warn_threshold : float, default 0.05
            A ``RuntimeWarning`` is issued for each column whose share of NaN/Inf values exceeds this fraction
            (measured before ``nan_handling`` is applied). The warning does not change how the values are handled.
        sample_weight : array-like, optional
            Per-row frequency/sample weights, one per row of ``data`` (finite and non-negative with a positive sum,
            otherwise ``ValueError``). Constant weights deliberately
            use the legacy OLS implementation for strict parity. Non-constant
            weights use WLS auxiliary regressions with the same no-intercept
            design as variance_inflation_factor.

        Returns
        -------
        pd.DataFrame
            DataFrame with columns ['feature', 'VIF'] sorted by VIF descending. Perfectly collinear features get a
            very large or infinite VIF.

        Raises
        ------
        ImportError
            If ``statsmodels`` (an optional extra) is not installed.
        """
        try:
            from statsmodels.stats.outliers_influence import variance_inflation_factor
        except ImportError as exc:
            raise ImportError(
                "compute_vif requires statsmodels (optional extra). Install it "
                "with: pip install \"SuperModelingFactory[stats]\""
            ) from exc

        weight = (
            resolve_sample_weight(sample_weight=sample_weight, expected_len=len(data))
            if sample_weight is not None
            else None
        )
        work = _prepare_nan_handled_frame(
            data,
            nan_handling=nan_handling,
            warn_threshold=nan_warn_threshold,
            context="FeatureSelectionAnalyzer.compute_vif",
        )
        x = work.values
        if weight is not None and nan_handling == "drop_rows":
            weight = weight[~data.isna().any(axis=1).to_numpy()]
            weight = resolve_sample_weight(
                sample_weight=weight, expected_len=len(work)
            )

        # Preserve the exact legacy call path for no weight and every
        # constant-weight vector.
        # A perfectly collinear feature has R² = 1 and VIF = inf; that is the
        # expected result, not a floating-point problem worth a warning.
        with np.errstate(divide="ignore"):
            if weight is None or bool(np.all(weight == weight[0])):
                vif_values = [variance_inflation_factor(x, i) for i in range(x.shape[1])]
            else:
                from statsmodels.regression.linear_model import WLS

                vif_values = []
                for i in range(x.shape[1]):
                    others = np.arange(x.shape[1]) != i
                    r_squared = WLS(x[:, i], x[:, others], weights=weight).fit().rsquared
                    vif_values.append(1.0 / (1.0 - r_squared))
        vif_data = pd.DataFrame({
            'feature': work.columns,
            'VIF': vif_values,
        }).sort_values('VIF', ascending=False).reset_index(drop=True)

        return vif_data

    def correlation_filter(self, data, threshold=0.8):
        """
        Remove highly correlated features.

        The absolute pairwise Pearson correlations of the columns are compared with ``threshold``: when two columns
        correlate above it, the one that comes later in ``data`` is dropped. ``data`` itself is not modified; the
        names of the kept columns are returned.

        Parameters
        ----------
        data : pd.DataFrame
            Feature matrix
        threshold : float, default 0.8
            Correlation threshold above which features are removed (an absolute correlation strictly greater than
            the threshold)

        Returns
        -------
        list of str
            List of features to keep (low correlation subset), in the column order of ``data``
        """
        corr_matrix = data.corr().abs()
        upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
        to_drop = [col for col in upper.columns if any(upper[col] > threshold)]
        return [col for col in data.columns if col not in to_drop]

chi2_selection

chi2_selection(data, feature_cols, target_col, nan_handling='fillna_median', nan_warn_threshold=0.05)

Select features using chi-squared test.

Parameters:

Name Type Description Default
data DataFrame

Input data

required
feature_cols list of str

Feature column names to evaluate

required
target_col str

Target variable column name

required
nan_handling str

How missing values in the feature columns are handled before the test. "fillna_0", "fillna_mean" and "fillna_median" fill them with 0, the column mean and the column median (0 for a column that is entirely missing); "drop_rows" drops every row with a missing feature value (the target is taken from the remaining rows); "raise" raises ValueError if any feature column has a missing value. Any other value raises ValueError.

"fillna_median"
nan_warn_threshold float

A RuntimeWarning is issued for each feature column whose share of NaN/Inf values exceeds this fraction (measured before nan_handling is applied). The warning does not change how the values are handled.

0.05

Returns:

Type Description
DataFrame

Results with columns ['feature', 'chi2', 'p_value', 'selected'], sorted by chi2 in descending order. selected is p_value < significance_level.

Notes

The features are scaled to [0, 1] with MinMaxScaler before the test, because the chi-squared statistic needs non-negative values. The result table is also stored in chi2_results_ and the selected feature names in selected_features_.

Source code in Modeling_Tool/Model/LRM_Tool.py
def chi2_selection(self, data, feature_cols, target_col, nan_handling="fillna_median", nan_warn_threshold=0.05):
    """
    Select features using chi-squared test.

    Parameters
    ----------
    data : pd.DataFrame
        Input data
    feature_cols : list of str
        Feature column names to evaluate
    target_col : str
        Target variable column name
    nan_handling : str, default "fillna_median"
        How missing values in the feature columns are handled before the test. ``"fillna_0"``, ``"fillna_mean"`` and
        ``"fillna_median"`` fill them with 0, the column mean and the column median (0 for a column that is entirely
        missing); ``"drop_rows"`` drops every row with a missing feature value (the target is taken from the
        remaining rows); ``"raise"`` raises ``ValueError`` if any feature column has a missing value. Any other
        value raises ``ValueError``.
    nan_warn_threshold : float, default 0.05
        A ``RuntimeWarning`` is issued for each feature column whose share of NaN/Inf values exceeds this fraction
        (measured before ``nan_handling`` is applied). The warning does not change how the values are handled.

    Returns
    -------
    pd.DataFrame
        Results with columns ['feature', 'chi2', 'p_value', 'selected'], sorted by ``chi2`` in descending order.
        ``selected`` is ``p_value < significance_level``.

    Notes
    -----
    The features are scaled to [0, 1] with ``MinMaxScaler`` before the test, because the chi-squared statistic needs
    non-negative values. The result table is also stored in ``chi2_results_`` and the selected feature names in
    ``selected_features_``.
    """
    from sklearn.feature_selection import chi2
    from sklearn.preprocessing import MinMaxScaler

    x = _prepare_nan_handled_frame(
        data[feature_cols],
        nan_handling=nan_handling,
        warn_threshold=nan_warn_threshold,
        context="FeatureSelectionAnalyzer.chi2_selection",
    )
    y = data.loc[x.index, target_col]

    scaler = MinMaxScaler()
    x_scaled = scaler.fit_transform(x)

    chi2_vals, p_vals = chi2(x_scaled, y)

    results = pd.DataFrame({
        'feature': feature_cols,
        'chi2': chi2_vals,
        'p_value': p_vals,
        'selected': p_vals < self.significance_level
    }).sort_values('chi2', ascending=False).reset_index(drop=True)

    self.chi2_results_ = results
    self.selected_features_ = results.loc[results['selected'], 'feature'].tolist()
    return results

compute_vif

compute_vif(data, nan_handling='fillna_median', nan_warn_threshold=0.05, sample_weight=None)

Compute Variance Inflation Factor (VIF) for multicollinearity detection.

Parameters:

Name Type Description Default
data DataFrame

Feature matrix (should not include target variable)

required
nan_handling str

How missing values are handled before the VIF is computed. Same options as in chi2_selection: "fillna_0", "fillna_mean", "fillna_median", "drop_rows" (rows with a missing value are dropped, together with their sample_weight entries) or "raise" (ValueError if any column has a missing value). Any other value raises ValueError.

"fillna_median"
nan_warn_threshold float

A RuntimeWarning is issued for each column whose share of NaN/Inf values exceeds this fraction (measured before nan_handling is applied). The warning does not change how the values are handled.

0.05
sample_weight array - like

Per-row frequency/sample weights, one per row of data (finite and non-negative with a positive sum, otherwise ValueError). Constant weights deliberately use the legacy OLS implementation for strict parity. Non-constant weights use WLS auxiliary regressions with the same no-intercept design as variance_inflation_factor.

None

Returns:

Type Description
DataFrame

DataFrame with columns ['feature', 'VIF'] sorted by VIF descending. Perfectly collinear features get a very large or infinite VIF.

Raises:

Type Description
ImportError

If statsmodels (an optional extra) is not installed.

Source code in Modeling_Tool/Model/LRM_Tool.py
def compute_vif(
    self,
    data,
    nan_handling="fillna_median",
    nan_warn_threshold=0.05,
    sample_weight=None,
):
    """
    Compute Variance Inflation Factor (VIF) for multicollinearity detection.

    Parameters
    ----------
    data : pd.DataFrame
        Feature matrix (should not include target variable)
    nan_handling : str, default "fillna_median"
        How missing values are handled before the VIF is computed. Same options as in ``chi2_selection``:
        ``"fillna_0"``, ``"fillna_mean"``, ``"fillna_median"``, ``"drop_rows"`` (rows with a missing value are
        dropped, together with their ``sample_weight`` entries) or ``"raise"`` (``ValueError`` if any column has a
        missing value). Any other value raises ``ValueError``.
    nan_warn_threshold : float, default 0.05
        A ``RuntimeWarning`` is issued for each column whose share of NaN/Inf values exceeds this fraction
        (measured before ``nan_handling`` is applied). The warning does not change how the values are handled.
    sample_weight : array-like, optional
        Per-row frequency/sample weights, one per row of ``data`` (finite and non-negative with a positive sum,
        otherwise ``ValueError``). Constant weights deliberately
        use the legacy OLS implementation for strict parity. Non-constant
        weights use WLS auxiliary regressions with the same no-intercept
        design as variance_inflation_factor.

    Returns
    -------
    pd.DataFrame
        DataFrame with columns ['feature', 'VIF'] sorted by VIF descending. Perfectly collinear features get a
        very large or infinite VIF.

    Raises
    ------
    ImportError
        If ``statsmodels`` (an optional extra) is not installed.
    """
    try:
        from statsmodels.stats.outliers_influence import variance_inflation_factor
    except ImportError as exc:
        raise ImportError(
            "compute_vif requires statsmodels (optional extra). Install it "
            "with: pip install \"SuperModelingFactory[stats]\""
        ) from exc

    weight = (
        resolve_sample_weight(sample_weight=sample_weight, expected_len=len(data))
        if sample_weight is not None
        else None
    )
    work = _prepare_nan_handled_frame(
        data,
        nan_handling=nan_handling,
        warn_threshold=nan_warn_threshold,
        context="FeatureSelectionAnalyzer.compute_vif",
    )
    x = work.values
    if weight is not None and nan_handling == "drop_rows":
        weight = weight[~data.isna().any(axis=1).to_numpy()]
        weight = resolve_sample_weight(
            sample_weight=weight, expected_len=len(work)
        )

    # Preserve the exact legacy call path for no weight and every
    # constant-weight vector.
    # A perfectly collinear feature has R² = 1 and VIF = inf; that is the
    # expected result, not a floating-point problem worth a warning.
    with np.errstate(divide="ignore"):
        if weight is None or bool(np.all(weight == weight[0])):
            vif_values = [variance_inflation_factor(x, i) for i in range(x.shape[1])]
        else:
            from statsmodels.regression.linear_model import WLS

            vif_values = []
            for i in range(x.shape[1]):
                others = np.arange(x.shape[1]) != i
                r_squared = WLS(x[:, i], x[:, others], weights=weight).fit().rsquared
                vif_values.append(1.0 / (1.0 - r_squared))
    vif_data = pd.DataFrame({
        'feature': work.columns,
        'VIF': vif_values,
    }).sort_values('VIF', ascending=False).reset_index(drop=True)

    return vif_data

correlation_filter

correlation_filter(data, threshold=0.8)

Remove highly correlated features.

The absolute pairwise Pearson correlations of the columns are compared with threshold: when two columns correlate above it, the one that comes later in data is dropped. data itself is not modified; the names of the kept columns are returned.

Parameters:

Name Type Description Default
data DataFrame

Feature matrix

required
threshold float

Correlation threshold above which features are removed (an absolute correlation strictly greater than the threshold)

0.8

Returns:

Type Description
list of str

List of features to keep (low correlation subset), in the column order of data

Source code in Modeling_Tool/Model/LRM_Tool.py
def correlation_filter(self, data, threshold=0.8):
    """
    Remove highly correlated features.

    The absolute pairwise Pearson correlations of the columns are compared with ``threshold``: when two columns
    correlate above it, the one that comes later in ``data`` is dropped. ``data`` itself is not modified; the
    names of the kept columns are returned.

    Parameters
    ----------
    data : pd.DataFrame
        Feature matrix
    threshold : float, default 0.8
        Correlation threshold above which features are removed (an absolute correlation strictly greater than
        the threshold)

    Returns
    -------
    list of str
        List of features to keep (low correlation subset), in the column order of ``data``
    """
    corr_matrix = data.corr().abs()
    upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
    to_drop = [col for col in upper.columns if any(upper[col] > threshold)]
    return [col for col in data.columns if col not in to_drop]

LRMaster

Logistic Regression Master Class.

A unified wrapper for logistic regression modeling that encapsulates:

  • Model training and prediction
  • Variable importance analysis
  • Statistical summary generation
  • Stepwise variable selection
  • Holdout-based hyperparameter grid search
  • AIC/BIC calculation
  • Optional feature standardization (off by default)

Parameters:

Name Type Description Default
params dict

Parameters for sklearn LogisticRegression, e.g., {'C': 1.0, 'solver': 'lbfgs'}

None
model sklearn-like LogisticRegression object

Existing fitted LR model object. If provided, LRMaster will wrap this model directly.

None
varlist list

Feature names used by the existing model. Required when model does not have feature_names_in_.

None
tgt_name str

Target variable name. Useful when wrapping an existing fitted model and later calling summary/evaluation methods.

None
standardize bool

If True, fit a scaler on the training features during fit / stepwise_selection and apply it consistently in every prediction / evaluation entry point. Default False keeps the original behavior (no standardization) for full backward compatibility.

False
scaler sklearn-like transformer

Custom scaler prototype to use when standardize=True (e.g. MinMaxScaler()). The prototype is cloned before fitting, so the passed instance is never mutated. Defaults to StandardScaler when not provided.

None

Attributes:

Name Type Description
params dict

Model parameters (a sanitized copy of the params argument; grid_search_params merges the best combination into it)

model LogisticRegression

Trained model (None until fit() is called)

calibrated_model CalibratedClassifierCV or None

Calibrated model stored by calibrate_model() (None until it runs; fit() does not reset it)

varlist list

List of feature names

tgt_name str

Target variable name

standardize bool

Whether feature standardization is enabled

standardizer sklearn-like scaler or None

Fitted scaler (None until fit() runs with standardize=True)

best_params_ dict or None

Best hyperparameters from grid_search_params (None until it runs)

search_results_ DataFrame or None

Full grid_search_params results table (None until it runs)

Examples:

>>> lr = LRMaster(params={'C': 1.0, 'solver': 'lbfgs'})
>>> lr.fit(train_df, ['age', 'income'], 'target')
>>> predictions = lr.predict(test_df)
>>> importance = lr.get_variable_importance()
>>> # With standardization (defaults to StandardScaler)
>>> lr = LRMaster(params={'C': 1.0}, standardize=True)
>>> lr.fit(train_df, ['age', 'income'], 'target')
>>> proba = lr.predict_proba(test_df)  # test_df is scaled with the fitted scaler
Source code in Modeling_Tool/Model/LRM_Tool.py
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
class LRMaster:
    """
    Logistic Regression Master Class.

    A unified wrapper for logistic regression modeling that encapsulates:

    - Model training and prediction
    - Variable importance analysis
    - Statistical summary generation
    - Stepwise variable selection
    - Holdout-based hyperparameter grid search
    - AIC/BIC calculation
    - Optional feature standardization (off by default)

    Parameters
    ----------
    params : dict, optional
        Parameters for sklearn LogisticRegression, e.g., {'C': 1.0, 'solver': 'lbfgs'}
    model : sklearn-like LogisticRegression object, optional
        Existing fitted LR model object. If provided, LRMaster will wrap this model directly.
    varlist : list, optional
        Feature names used by the existing model. Required when model does not have
        `feature_names_in_`.
    tgt_name : str, optional
        Target variable name. Useful when wrapping an existing fitted model and later
        calling summary/evaluation methods.
    standardize : bool, default False
        If True, fit a scaler on the training features during `fit` /
        `stepwise_selection` and apply it consistently in every prediction /
        evaluation entry point. Default False keeps the original behavior
        (no standardization) for full backward compatibility.
    scaler : sklearn-like transformer, optional
        Custom scaler prototype to use when `standardize=True` (e.g.
        `MinMaxScaler()`). The prototype is cloned before fitting, so the
        passed instance is never mutated. Defaults to `StandardScaler` when
        not provided.

    Attributes
    ----------
    params : dict
        Model parameters (a sanitized copy of the ``params`` argument; ``grid_search_params`` merges the best
        combination into it)
    model : sklearn.linear_model.LogisticRegression
        Trained model (None until fit() is called)
    calibrated_model : sklearn.calibration.CalibratedClassifierCV or None
        Calibrated model stored by calibrate_model() (None until it runs; fit() does not reset it)
    varlist : list
        List of feature names
    tgt_name : str
        Target variable name
    standardize : bool
        Whether feature standardization is enabled
    standardizer : sklearn-like scaler or None
        Fitted scaler (None until fit() runs with standardize=True)
    best_params_ : dict or None
        Best hyperparameters from grid_search_params (None until it runs)
    search_results_ : pandas.DataFrame or None
        Full grid_search_params results table (None until it runs)

    Examples
    --------
    >>> lr = LRMaster(params={'C': 1.0, 'solver': 'lbfgs'})
    >>> lr.fit(train_df, ['age', 'income'], 'target')
    >>> predictions = lr.predict(test_df)
    >>> importance = lr.get_variable_importance()

    >>> # With standardization (defaults to StandardScaler)
    >>> lr = LRMaster(params={'C': 1.0}, standardize=True)
    >>> lr.fit(train_df, ['age', 'income'], 'target')
    >>> proba = lr.predict_proba(test_df)  # test_df is scaled with the fitted scaler
    """

    def __init__(self, params=None, model=None, varlist=None, tgt_name=None,
                 standardize=False, scaler=None):
        """
        Initialize LRMaster instance.

        Parameters
        ----------
        params : dict, optional
            LogisticRegression parameters
        model : sklearn-like LogisticRegression object, optional
            Existing fitted LR model object. If provided, LRMaster will wrap this model directly.
        varlist : list, optional
            Feature names used by the existing model. Required when model does not have
            `feature_names_in_`.
        tgt_name : str, optional
            Target variable name. Useful when wrapping an existing fitted model and later
            calling summary/evaluation methods.
        standardize : bool, default False
            If True, fit a scaler on the training features during `fit` /
            `stepwise_selection` and apply it consistently in every prediction /
            evaluation entry point. Default False keeps the original behavior
            (no standardization) for full backward compatibility.
        scaler : sklearn-like transformer, optional
            Custom scaler prototype to use when `standardize=True` (e.g.
            `MinMaxScaler()`). The prototype is cloned before fitting, so the
            passed instance is never mutated. Defaults to `StandardScaler` when
            not provided.
        """
        self.params = _sanitize_lr_params(params)
        self.model = model
        self.calibrated_model = None
        self.varlist = varlist
        self.tgt_name = tgt_name
        self._data = None
        self.standardize = standardize
        # Unfitted prototype used to derive the fitted scaler during fit().
        self._scaler_proto = scaler
        # Fitted scaler; stays None until fit()/stepwise runs with standardize=True.
        self.standardizer = None
        # Populated by grid_search_params(): best param dict + full results table.
        self.best_params_ = None
        self.search_results_ = None

    def _make_scaler(self):
        """
        Return a fresh, unfitted scaler instance for standardization.

        Uses the user-provided `scaler` prototype when given (cloned so the
        original stays unfitted); otherwise defaults to `StandardScaler`.
        """
        from sklearn.preprocessing import StandardScaler
        if self._scaler_proto is not None:
            from sklearn.base import clone as _sk_clone
            return _sk_clone(self._scaler_proto)
        return StandardScaler()

    def _fit_standardizer(self, x):
        """
        Fit a scaler on `x` and store it as `self.standardizer`.

        Returns the standardized `x` (a DataFrame when `x` is one). When
        `self.standardize` is False this is a no-op that clears any scaler and
        returns `x` unchanged.
        """
        if not self.standardize:
            self.standardizer = None
            return x
        scaler = self._make_scaler()
        scaler.fit(x)
        self.standardizer = scaler
        return self._apply_standardizer(x)

    def _apply_standardizer(self, x):
        """
        Apply the fitted standardizer to `x`, preserving DataFrame layout.

        No-op when no fitted standardizer is present (e.g. `standardize=False`
        or when wrapping an externally fitted model), so existing behavior is
        unchanged unless standardization was explicitly enabled and fitted.
        """
        if self.standardizer is None:
            return x
        values = self.standardizer.transform(x)
        if hasattr(x, 'columns'):
            return pd.DataFrame(values, columns=x.columns, index=x.index)
        return values

    def set_data(self, data):
        """
        Store reference data for later use (e.g., calibration).

        Parameters
        ----------
        data : pd.DataFrame
            Training data to store

        Returns
        -------
        self
        """
        self._data = data
        return self

    def __getstate__(self):
        """
        Exclude the reference training frame from pickling.

        ``self._data`` holds the full training set (row-level customer records)
        only as a convenience fallback for
        ``get_statsmodel_summary``/``get_aic``/``get_bic``. Serializing it bloats
        saved models (tens to hundreds of MB) and leaks raw data alongside the
        model artifact. Prediction never depends on it, so it is dropped on
        pickle; pass ``data=`` explicitly to the diagnostic methods on a loaded
        model.
        """
        state = self.__dict__.copy()
        state["_data"] = None
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self.__dict__.setdefault("_data", None)

    def _require_data(self, data):
        """
        Resolve evaluation data, falling back to the stored training frame.

        Raises a clear error when neither an explicit ``data`` argument nor a
        stored ``self._data`` is available -- e.g. on a model loaded from disk,
        where ``_data`` is intentionally dropped during pickling.
        """
        if data is None:
            data = self._data
        if data is None:
            raise ValueError(
                "No data available: this model was loaded without its training "
                "frame (_data is dropped on save). Pass data=... to "
                "get_statsmodel_summary/get_aic/get_bic."
            )
        return data

    def fit(self, data, varlist, tgt_name, val_data=None, val_varlist=None, val_tgt_name=None, weight_col=None):
        """
        Train the logistic regression model.

        When `standardize=True`, a scaler is fitted on the training features and
        stored as `self.standardizer`; the model is then trained on the scaled
        features. The same scaler is reused at prediction / evaluation time.

        Parameters
        ----------
        data : pd.DataFrame
            Training dataset containing features and target
        varlist : list of str
            Feature column names to use for training
        tgt_name : str
            Target variable column name
        val_data : pd.DataFrame, optional
            Validation dataset (currently used for reference; not used in fitting)
        val_varlist : list of str, optional
            Validation feature column names
        val_tgt_name : str, optional
            Validation target variable column name
        weight_col : str, optional
            Column in ``data`` with per-sample training weights (non-negative).
            Mutually exclusive with passing ``sample_weight`` to lower-level helpers.
            With ``None`` the training is unweighted.

        Returns
        -------
        self
            The fitted ``LRMaster``.

        Notes
        -----
        Besides fitting ``model``, this call sets ``varlist`` and ``tgt_name`` and keeps a reference to ``data``, which
        ``get_statsmodel_summary``, ``get_aic``, ``get_bic`` and ``calibrate_model`` use when they are called without
        data (the reference is not pickled). ``calibrated_model`` is not reset: call ``calibrate_model`` again after
        refitting.
        """
        self.varlist = varlist
        self.tgt_name = tgt_name
        self._data = data

        train_x = data[varlist]
        if self.standardize:
            train_x = self._fit_standardizer(train_x)
        else:
            self.standardizer = None

        val_x = val_data[val_varlist] if val_data is not None and val_varlist is not None else None
        val_y = val_data[val_tgt_name] if val_data is not None and val_tgt_name is not None else None
        if val_x is not None:
            val_x = self._apply_standardizer(val_x)

        sample_weight = resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))
        self.model = lr_model(train_x, data[tgt_name], val_x, val_y, self.params, sample_weight=sample_weight)
        return self

    def calibrate_model(self, model=None, train_df=None, method='sigmoid', cv=5, weight_col=None, sample_weight=None):
        """Model calibration with optional sample weights.

        Wrap the model in a scikit-learn ``CalibratedClassifierCV`` and fit it; the result is stored as
        ``self.calibrated_model`` and is used by ``predict`` and ``predict_proba`` when they are called with
        ``calibrated_model=True``.

        Parameters
        ----------
        model : sklearn-like LogisticRegression object, optional
            Model to calibrate. Defaults to ``self.model``.
        train_df : pandas.DataFrame, optional
            Calibration data, with the model's feature columns and the target column ``self.tgt_name``. Defaults to
            the frame stored by ``fit``, ``stepwise_selection`` or ``set_data``. Calibrating on the
            training frame is rarely what you want: pass a separate holdout frame.
        method : str, default 'sigmoid'
            Calibration method, ``'sigmoid'`` (Platt scaling) or ``'isotonic'``.
        cv : int or str, default 5
            Cross-validation strategy of ``CalibratedClassifierCV``. An integer clones the model and refits it on the
            folds of ``train_df``; ``'prefit'`` keeps the fitted model as it is and only fits the calibrator on
            ``train_df``.
        weight_col : str, optional
            Name of a column of ``train_df`` with non-negative per-row sample weights used when fitting. Mutually
            exclusive with ``sample_weight``.
        sample_weight : array-like, optional
            Per-row sample weights aligned with ``train_df``, used like ``weight_col``. Mutually exclusive with
            ``weight_col``.

        Returns
        -------
        self
            The ``LRMaster``, whose ``calibrated_model`` is now set (an earlier calibrated model is replaced).

        Raises
        ------
        ValueError
            If the feature list cannot be inferred (the model has no ``feature_names_in_`` and ``varlist`` is not set),
            if ``cv='prefit'`` is used with a model that is not fitted, or if the sample weights are invalid or both
            ``weight_col`` and ``sample_weight`` are given.
        KeyError
            If ``weight_col`` is not a column of ``train_df``.

        Notes
        -----
        The calibration features are scaled with the fitted standardizer when ``standardize=True``, so the calibrated
        model works in the same feature space as ``model``.
        """
        from sklearn.calibration import CalibratedClassifierCV
        from sklearn.base import clone

        if train_df is None:
            train_df = self._data

        if model is None:
            model = self.model

        if hasattr(model, "feature_names_in_"):
            varlist = model.feature_names_in_.tolist()
        elif self.varlist is not None:
            varlist = self.varlist
        else:
            raise ValueError(
                "Cannot infer feature list from model. Please provide `varlist` when initializing LRMaster."
            )

        if hasattr(model, "get_params") and model.get_params().get("multi_class") == "deprecated":
            if cv == "prefit":
                model.set_params(multi_class="auto")
            else:
                model = clone(model)
                model.set_params(multi_class="auto")

        if cv == "prefit" and not hasattr(model, "classes_"):
            raise ValueError(
                "cv='prefit' requires a fitted model with `classes_`. "
                "Please pass a fitted LR model object or use cv=5 to refit during calibration."
            )

        # Standardize calibration features with the fitted scaler so the
        # calibrated model operates in the same feature space as self.model.
        cal_x = self._apply_standardizer(train_df[varlist])

        # sklearn 1.6 deprecated cv="prefit" in favour of wrapping the fitted
        # estimator in FrozenEstimator, and 1.8 removed "prefit" entirely. Use
        # FrozenEstimator when available, falling back to cv="prefit" on <1.6.
        estimator = model
        calib_kwargs = {"method": method}
        if cv == "prefit":
            try:
                from sklearn.frozen import FrozenEstimator
                estimator = FrozenEstimator(model)
            except ImportError:
                calib_kwargs["cv"] = "prefit"
        else:
            calib_kwargs["cv"] = cv

        # sklearn 1.2+ renamed base_estimator -> estimator; support both
        try:
            calibrated_model = CalibratedClassifierCV(estimator=estimator, **calib_kwargs)
        except TypeError:
            calibrated_model = CalibratedClassifierCV(base_estimator=estimator, **calib_kwargs)
        fit_weight = resolve_sample_weight(
            data=train_df,
            weight_col=weight_col,
            sample_weight=sample_weight,
            expected_len=len(train_df),
        )
        calibrated_model.fit(cal_x, train_df[self.tgt_name], sample_weight=fit_weight)

        self.calibrated_model = calibrated_model

        return self

    def eval_calibrated_outcome(self, evalset, plot=False, weight_col=None, sample_weight=None):
        """Evaluate calibrated vs raw probabilities on a holdout set.

        Compute the Brier score of the raw and of the calibrated positive-class probabilities of ``evalset`` and,
        optionally, plot the reliability curves of both. ``calibrate_model`` must have been called first.

        Parameters
        ----------
        evalset : pandas.DataFrame
            Holdout data with the feature columns of ``self.varlist`` and the target column ``self.tgt_name``.
        plot : bool, default False
            If True, show a matplotlib reliability plot (10 bins) with the raw curve, the calibrated curve and the
            diagonal of perfect calibration.
        weight_col : str, optional
            Name of a column of ``evalset`` with non-negative per-row weights, used for the Brier scores and for the
            calibration curves. Mutually exclusive with ``sample_weight``.
        sample_weight : array-like, optional
            Per-row weights aligned with ``evalset``, used like ``weight_col``. Mutually exclusive with ``weight_col``.

        Returns
        -------
        None
            Nothing is returned. The two Brier scores are written with ``logging`` at INFO level (logger
            ``Modeling_Tool.Model.LRM_Tool``), so they appear only when INFO logging is enabled; they are not printed.

        Notes
        -----
        The calibration curves are weighted only when the installed scikit-learn accepts ``sample_weight`` in
        ``calibration_curve``; otherwise the curves are computed unweighted (the Brier scores stay weighted).
        """
        from sklearn.calibration import calibration_curve
        from sklearn.metrics import brier_score_loss

        y_val = evalset[self.tgt_name]
        eval_weight = resolve_sample_weight(
            data=evalset,
            weight_col=weight_col,
            sample_weight=sample_weight,
            expected_len=len(evalset),
        )

        # Raw probabilities
        prob_raw = self.predict_proba(evalset)[:, 1]
        # Calibrated probabilities (Platt scaling)
        prob_cal = self.predict_proba(evalset, calibrated_model=True)[:, 1]

        # 1. Brier score (lower is better)
        logger.info(
            "Raw Brier: %.6f",
            brier_score_loss(y_val, prob_raw, sample_weight=eval_weight),
        )
        logger.info(
            "Cal Brier: %.6f",
            brier_score_loss(y_val, prob_cal, sample_weight=eval_weight),
        )

        # 2. Reliability curve
        curve_kwargs = {} if eval_weight is None else {"sample_weight": eval_weight}
        try:
            fraction_of_positives_raw, mean_predicted_value_raw = calibration_curve(
                y_val, prob_raw, n_bins=10, **curve_kwargs
            )
            fraction_of_positives_cal, mean_predicted_value_cal = calibration_curve(
                y_val, prob_cal, n_bins=10, **curve_kwargs
            )
        except TypeError:
            fraction_of_positives_raw, mean_predicted_value_raw = calibration_curve(
                y_val, prob_raw, n_bins=10
            )
            fraction_of_positives_cal, mean_predicted_value_cal = calibration_curve(
                y_val, prob_cal, n_bins=10
            )

        if plot:
            import matplotlib.pyplot as plt
            plt.plot(mean_predicted_value_raw, fraction_of_positives_raw, 's-', label='Raw')
            plt.plot(mean_predicted_value_cal, fraction_of_positives_cal, 'o-', label='Platt')
            plt.plot([0,1], [0,1], 'k--', label='Perfect')
            plt.xlabel('Mean Predicted Probability')
            plt.ylabel('Fraction of Positives')
            plt.legend()
            plt.show()

    def predict(self, data, varlist=None, calibrated_model = False):
        """
        Predict using the trained model.

        When standardization is enabled, the input features are scaled with the
        scaler fitted during `fit` before being passed to the model.

        Parameters
        ----------
        data : pandas.DataFrame
            Input data for prediction
        varlist : list, optional
            Feature names (uses training features if None)
        calibrated_model : bool, default False
            If True, predict with the calibrated model stored by ``calibrate_model`` instead of the raw model.
            ``calibrate_model`` must have been called first: otherwise ``calibrated_model`` is ``None`` and an
            ``AttributeError`` is raised.

        Returns
        -------
        numpy.ndarray
            Predicted class labels (hard labels such as 0/1, not probabilities; use ``predict_proba`` for
            probabilities)
        """
        if varlist is None:
            varlist = self.varlist

        x = self._apply_standardizer(data[varlist])

        if calibrated_model:
            _patch_calibrated_model(self.calibrated_model)
            return self.calibrated_model.predict(x)

        return self.model.predict(x)

    def predict_proba(self, data, varlist=None, calibrated_model = False):
        """
        Predict class probabilities.

        When standardization is enabled, the input features are scaled with the
        scaler fitted during `fit` before being passed to the model.

        Parameters
        ----------
        data : pandas.DataFrame
            Input data for prediction
        varlist : list, optional
            Feature names (uses training features if None)
        calibrated_model : bool, default False
            If True, return the probabilities of the calibrated model stored by ``calibrate_model`` instead of
            those of the raw model. ``calibrate_model`` must have been called first: otherwise
            ``calibrated_model`` is ``None`` and an ``AttributeError`` is raised.

        Returns
        -------
        numpy.ndarray
            Array of shape (n_samples, 2) with class probabilities (the columns follow the model's ``classes_``, so
            column 1 is the probability of class 1 for a 0/1 target)
        """
        if varlist is None:
            varlist = self.varlist

        x = self._apply_standardizer(data[varlist])

        if calibrated_model:
            _patch_calibrated_model(self.calibrated_model)
            return self.calibrated_model.predict_proba(x)

        return self.model.predict_proba(x)

    def get_variable_importance(self):
        """
        Get variable importance (coefficients) from the model.

        Returns
        -------
        pandas.DataFrame
            DataFrame with columns ['varlist', 'coef', 'importance'] sorted by
            importance in descending order

        Notes
        -----
        When standardization is enabled the coefficients are expressed in the
        standardized feature space (i.e. they are directly comparable in
        magnitude across features).
        """
        return lr_varimp(self.model)

    def get_statsmodel_summary(self, data=None, varlist=None, tgt_name=None):
        """
        Generate a statsmodels-style summary for the trained LR model.

        Parameters
        ----------
        data : pd.DataFrame, optional
            Data for computing the summary (uses stored training data if None)
        varlist : list of str, optional
            Feature names (uses stored varlist if None)
        tgt_name : str, optional
            Target variable name (uses stored tgt_name if None)

        Returns
        -------
        pandas.DataFrame
            Summary table with coefficients, standard errors, z-scores and p-values (columns ``coef``, ``std_err``,
            ``z``, ``p_value``, ``ci_lower``, ``ci_upper``), indexed by ``'Intercept'`` followed by the feature names;
            see ``get_lr_statsmodel_summary``.

        Raises
        ------
        ValueError
            If ``data`` is ``None`` and no training frame is stored (for example on a model loaded from disk, because
            the stored frame is not pickled).

        Notes
        -----
        When standardization is enabled the summary is computed on the
        standardized feature space, consistent with how the model was trained.
        """
        data = self._require_data(data)
        if varlist is None:
            varlist = self.varlist
        if tgt_name is None:
            tgt_name = self.tgt_name

        return get_lr_statsmodel_summary(
            self.model,
            self._apply_standardizer(data[varlist]),
            data[tgt_name],
            feature_names=varlist
        )

    def get_aic(self, data=None, varlist=None, tgt_name=None, weight_col=None):
        """
        Compute AIC for the trained model.

        Parameters
        ----------
        data : pd.DataFrame, optional
            Data to evaluate the model on (uses the stored training data if None).
        varlist : list of str, optional
            Feature names (uses the stored varlist if None).
        tgt_name : str, optional
            Target variable name (uses the stored tgt_name if None).
        weight_col : str, optional
            Name of a column of ``data`` with non-negative per-row weights; the weighted log-likelihood is used. With
            ``None`` the AIC is unweighted.

        Returns
        -------
        float
            AIC value (lower is better).

        Raises
        ------
        ValueError
            If ``data`` is ``None`` and no training frame is stored (for example on a model loaded from disk, because
            the stored frame is not pickled), or if the weights are invalid.
        KeyError
            If ``weight_col`` is not a column of ``data``.
        """
        data = self._require_data(data)
        if varlist is None:
            varlist = self.varlist
        if tgt_name is None:
            tgt_name = self.tgt_name
        return compute_aic(
            self.model,
            self._apply_standardizer(data[varlist]),
            data[tgt_name],
            sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data)),
        )

    def get_bic(self, data=None, varlist=None, tgt_name=None, weight_col=None):
        """
        Compute BIC for the trained model.

        Parameters
        ----------
        data : pd.DataFrame, optional
            Data to evaluate the model on (uses the stored training data if None).
        varlist : list of str, optional
            Feature names (uses the stored varlist if None).
        tgt_name : str, optional
            Target variable name (uses the stored tgt_name if None).
        weight_col : str, optional
            Name of a column of ``data`` with non-negative per-row weights; the weighted log-likelihood is used and the
            sample size in the penalty is the sum of the weights. With ``None`` the BIC is unweighted.

        Returns
        -------
        float
            BIC value (lower is better).

        Raises
        ------
        ValueError
            If ``data`` is ``None`` and no training frame is stored (for example on a model loaded from disk, because
            the stored frame is not pickled), or if the weights are invalid.
        KeyError
            If ``weight_col`` is not a column of ``data``.
        """
        data = self._require_data(data)
        if varlist is None:
            varlist = self.varlist
        if tgt_name is None:
            tgt_name = self.tgt_name
        return compute_bic(
            self.model,
            self._apply_standardizer(data[varlist]),
            data[tgt_name],
            sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data)),
        )

    def stepwise_selection(
        self,
        data,
        varlist,
        tgt_name,
        criterion='aic',
        direction='both',
        max_iter=100,
        verbose=True,
        weight_col=None,
    ):
        """
        Perform stepwise variable selection.

        Iteratively adds or removes features based on AIC/BIC improvement.

        When `standardize=True`, all interim fits and the final model are
        trained on standardized features, and the fitted scaler for the selected
        columns is stored on the instance for later prediction.

        Parameters
        ----------
        data : pd.DataFrame
            Training data
        varlist : list of str
            Initial feature list (the candidate variables). With ``direction='forward'`` the search starts from an
            empty model and adds variables from this list; otherwise it starts from all of them.
        tgt_name : str
            Target variable name
        criterion : str, default 'aic'
            Selection criterion, 'aic' or 'bic'. Any value other than 'aic' selects by BIC.
        direction : str, default 'both'
            Direction of stepwise selection: 'forward', 'backward', or 'both'. 'both' starts from all variables like
            'backward' but can add a removed variable back. Any other value performs no step and keeps all of
            ``varlist``.
        max_iter : int, default 100
            Maximum number of iterations. Each iteration makes at most one forward step and one backward step, and the
            search stops earlier when no step lowers the criterion.
        verbose : bool, default True
            Whether to log progress (one INFO message per step and a final summary, through the module logger);
            nothing is printed.
        weight_col : str, optional
            Name of a column of ``data`` with non-negative per-row weights, used to fit every candidate model and for
            the weighted AIC/BIC. With ``None`` the selection is unweighted.

        Returns
        -------
        list of str
            Selected feature list

        Notes
        -----
        The instance is updated: ``varlist``, ``tgt_name`` and the stored training frame are replaced, the standardizer
        is refitted on the selected columns when ``standardize=True``, and ``model`` is retrained on the selected
        features. A candidate whose fit or scoring raises an exception is skipped silently.
        """
        if criterion == 'aic':
            score_fn = lambda model, x, y: compute_aic(
                model, x, y, sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))
            )
        else:
            score_fn = lambda model, x, y: compute_bic(
                model, x, y, sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))
            )
        sample_weight = resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))

        # When standardizing, operate on a once-standardized feature frame.
        # Column-wise scalers (StandardScaler / MinMaxScaler) make slicing a
        # subset of columns equivalent to standardizing that subset.
        if self.standardize:
            interim_scaler = self._make_scaler()
            interim_scaler.fit(data[varlist])
            work = pd.DataFrame(
                interim_scaler.transform(data[varlist]),
                columns=list(varlist), index=data.index,
            )
        else:
            work = data

        current_vars = list(varlist) if direction != 'forward' else []
        remaining_vars = list(varlist) if direction == 'forward' else []

        best_model = lr_model(
            work[current_vars] if current_vars else pd.DataFrame(index=data.index),
            data[tgt_name], None, None, self.params, sample_weight=sample_weight
        ) if current_vars else None

        best_score = score_fn(best_model, work[current_vars], data[tgt_name]) if best_model else float('inf')

        for iteration in range(max_iter):
            improved = False

            # Forward step
            if direction in ('forward', 'both') and remaining_vars:
                scores = {}
                for var in remaining_vars:
                    trial_vars = current_vars + [var]
                    try:
                        model = lr_model(work[trial_vars], data[tgt_name], None, None, self.params, sample_weight=sample_weight)
                        scores[var] = score_fn(model, work[trial_vars], data[tgt_name])
                    except Exception:
                        continue
                if scores:
                    best_var = min(scores, key=scores.get)
                    if scores[best_var] < best_score:
                        current_vars.append(best_var)
                        remaining_vars.remove(best_var)
                        best_score = scores[best_var]
                        improved = True
                        if verbose:
                            logger.info(f"[Step {iteration+1}] ADD '{best_var}', {criterion.upper()}={best_score:.4f}")

            # Backward step
            if direction in ('backward', 'both') and len(current_vars) > 1:
                scores = {}
                for var in current_vars:
                    trial_vars = [v for v in current_vars if v != var]
                    try:
                        model = lr_model(work[trial_vars], data[tgt_name], None, None, self.params, sample_weight=sample_weight)
                        scores[var] = score_fn(model, work[trial_vars], data[tgt_name])
                    except Exception:
                        continue
                if scores:
                    worst_var = min(scores, key=scores.get)
                    if scores[worst_var] < best_score:
                        current_vars.remove(worst_var)
                        if direction == 'both':
                            remaining_vars.append(worst_var)
                        best_score = scores[worst_var]
                        improved = True
                        if verbose:
                            logger.info(f"[Step {iteration+1}] REMOVE '{worst_var}', {criterion.upper()}={best_score:.4f}")

            if not improved:
                break

        if verbose:
            logger.info(f"Stepwise selection complete. Selected {len(current_vars)} features.")

        self.varlist = current_vars
        self.tgt_name = tgt_name
        self._data = data

        if self.standardize:
            self.standardizer = self._make_scaler()
            self.standardizer.fit(data[current_vars])
            final_x = self._apply_standardizer(data[current_vars])
        else:
            self.standardizer = None
            final_x = data[current_vars]

        self.model = lr_model(final_x, data[tgt_name], None, None, self.params, sample_weight=sample_weight)
        return current_vars

    def grid_search_params(self, data, varlist, tgt_name, eval_sets, param_grid,
                           objective='oot_gap_penalized', primary_set=None,
                           gap_ref_sets=None, metric='auc', refit=True, verbose=True,
                           weight_col=None, eval_weight_col=None):
        """
        Grid-search LogisticRegression hyperparameters over a holdout-based objective.

        For every combination in ``param_grid`` (Cartesian product), a candidate model is
        trained on ``data`` and scored by AUC on each dataset in ``eval_sets``. The best
        combination is chosen by ``objective`` (default rewards a high primary-set AUC while
        penalizing the train/holdout AUC gap, i.e. overfitting). This is a **holdout** search
        (not k-fold CV), intended for the typical INS/OOS/OOT credit-scoring setup.

        When ``standardize=True`` on this instance, every candidate inherits the same
        standardization config (each candidate fits its own scaler on ``data``), so the
        search runs in the same feature space the final (optionally refit) model uses.

        Parameters
        ----------
        data : pandas.DataFrame
            Training dataset (e.g. the in-sample set used for fitting).
        varlist : list
            Feature column names.
        tgt_name : str
            Target column name.
        eval_sets : dict of {str: pandas.DataFrame}
            Ordered mapping of datasets to score by AUC, e.g.
            ``{'ins': ins_df, 'oos': oos_df, 'oot': oot_df}``.
        param_grid : dict of {str: iterable}
            Hyperparameter search space, e.g. ``{'C': np.logspace(-3, 2, 31)}``.
            Multiple keys are combined as a Cartesian product.
        objective : str or callable, default 'oot_gap_penalized'
            How to score each candidate from its per-set AUCs:

            - ``'oot_gap_penalized'`` : ``AUC[primary] - |mean(AUC[gap_refs]) - AUC[primary]|``
              (maximize the primary set while penalizing the overfitting gap).
            - ``'max_primary'`` : ``AUC[primary]``.
            - callable : ``f(auc_dict) -> float`` where ``auc_dict`` maps set name to AUC.
        primary_set : str, optional
            Key of ``eval_sets`` whose AUC to maximize. Defaults to the last key.
        gap_ref_sets : list of str, optional
            Set names whose mean AUC forms the gap reference. Defaults to all sets except
            ``primary_set``. Only used by ``'oot_gap_penalized'``.
        metric : str, default 'auc'
            Evaluation metric. Currently only ``'auc'`` is supported.
        refit : bool, default True
            If True, refit ``self`` on ``data`` with the best parameters after searching.
        verbose : bool, default True
            Print progress / best result.
        weight_col : str, optional
            Name of a column of ``data`` with non-negative per-row sample weights, used to train every candidate model
            (and the final refit). With ``None`` the training is unweighted.
        eval_weight_col : str, optional
            Name of a column that every dataset in ``eval_sets`` must contain, holding non-negative per-row weights for
            the AUC computed on that set. With ``None`` the AUCs are unweighted. The training weights are not applied to
            the evaluation sets, and these weights are not applied to training.

        Returns
        -------
        pandas.DataFrame
            Search results sorted by ``score`` descending, with columns: the param name(s)
            + ``AUC_<name>`` per eval set + ``gap`` (gap objective only: the mean AUC of ``gap_ref_sets`` minus the
            primary AUC) + ``score``.

        Raises
        ------
        ValueError
            If ``metric`` is not ``'auc'``, ``eval_sets`` is empty, or ``primary_set`` is not a key of ``eval_sets``.
        KeyError
            If ``data`` or an evaluation dataset lacks a column of ``varlist`` or ``tgt_name``, or if ``weight_col`` /
            ``eval_weight_col`` names a column that is missing.

        Side Effects
        ------------
        Sets ``self.best_params_`` (dict) and ``self.search_results_`` (the returned table),
        and merges the best combo into ``self.params``; if ``refit=True``, also retrains
        ``self.model`` on ``data``.

        Examples
        --------
        >>> tuner = LRMaster(params={'C': 1.0, 'solver': 'lbfgs'})
        >>> res = tuner.grid_search_params(
        ...     data=ins_fit, varlist=woe_cols, tgt_name='bad_flag',
        ...     eval_sets={'ins': ins_woe, 'oos': oos_woe, 'oot': oot_woe},
        ...     param_grid={'C': np.logspace(-3, 2, 31)},
        ...     primary_set='oot', gap_ref_sets=['ins', 'oos'], refit=False,
        ... )
        >>> best_C = tuner.best_params_['C']
        """
        import itertools
        from sklearn.metrics import roc_auc_score

        if metric != 'auc':
            raise ValueError("Only metric='auc' is currently supported.")
        if not eval_sets:
            raise ValueError("eval_sets must be a non-empty {name: DataFrame} mapping.")

        set_names = list(eval_sets.keys())
        if primary_set is None:
            primary_set = set_names[-1]
        if primary_set not in eval_sets:
            raise ValueError("primary_set '{0}' not in eval_sets {1}".format(primary_set, set_names))
        if gap_ref_sets is None:
            gap_ref_sets = [n for n in set_names if n != primary_set]

        # Validate columns up-front for a clear error instead of a deep KeyError.
        missing = [c for c in (list(varlist) + [tgt_name]) if c not in data.columns]
        if missing:
            raise KeyError("training data missing columns: {0}".format(missing))
        for _name, _df in eval_sets.items():
            _miss = [c for c in (list(varlist) + [tgt_name]) if c not in _df.columns]
            if _miss:
                raise KeyError("eval set '{0}' missing columns: {1}".format(_name, _miss))

        param_names = list(param_grid.keys())
        combos = list(itertools.product(*[list(param_grid[k]) for k in param_names]))
        use_gap = (not callable(objective)) and objective == 'oot_gap_penalized' and len(gap_ref_sets) > 0

        if verbose:
            print("grid_search_params: {0} combinations (params={1}), training set {2:,} rows, eval={3}".format(
                len(combos), param_names, len(data), set_names))

        def _score(auc_dict):
            if callable(objective):
                return objective(auc_dict)
            if objective == 'max_primary':
                return auc_dict[primary_set]
            if objective == 'oot_gap_penalized':
                primary = auc_dict[primary_set]
                if gap_ref_sets:
                    ref = float(np.mean([auc_dict[n] for n in gap_ref_sets]))
                    return primary - abs(ref - primary)
                return primary
            raise ValueError("Unknown objective: {0}".format(objective))

        rows = []
        for combo in combos:
            combo_dict = dict(zip(param_names, combo))
            # Candidates inherit this instance's standardization config so the
            # search happens in the same feature space the final model uses.
            cand = LRMaster(
                params={**self.params, **combo_dict},
                standardize=self.standardize,
                scaler=self._scaler_proto,
            )
            cand.fit(data, varlist, tgt_name, weight_col=weight_col)

            auc_dict = {}
            for name, df_eval in eval_sets.items():
                proba = cand.predict_proba(df_eval, varlist)[:, 1]
                eval_sw = resolve_sample_weight(data=df_eval, weight_col=eval_weight_col, expected_len=len(df_eval))
                auc_dict[name] = roc_auc_score(df_eval[tgt_name], proba, sample_weight=eval_sw)

            row = dict(combo_dict)
            for name in set_names:
                row['AUC_{0}'.format(name)] = round(auc_dict[name], 5)
            if use_gap:
                ref = float(np.mean([auc_dict[n] for n in gap_ref_sets]))
                row['gap'] = round(ref - auc_dict[primary_set], 5)
            row['score'] = round(_score(auc_dict), 5)
            rows.append(row)

        search_df = pd.DataFrame(rows).sort_values('score', ascending=False).reset_index(drop=True)

        # Best params from the (unrounded) top row; cast numpy scalars to native
        # Python types for a clean repr / JSON-serializable params.
        best_row = search_df.iloc[0]

        def _native(v):
            return v.item() if hasattr(v, 'item') else v

        # Read each parameter from its own column: ``best_row`` is one row Series of a frame with float AUC columns, so
        # an integer parameter (``max_iter=100``) came out as ``100.0`` and the final fit raised InvalidParameterError.
        self.best_params_ = {k: _native(search_df[k].iloc[0]) for k in param_names}
        self.search_results_ = search_df
        self.params = {**self.params, **self.best_params_}

        # Round float param columns for display only (does not affect best_params_).
        for k in param_names:
            if pd.api.types.is_float_dtype(search_df[k]):
                search_df[k] = search_df[k].round(5)

        if verbose:
            print("★ best: {0} | score={1:.5f} | AUC_{2}={3:.5f}".format(
                self.best_params_, best_row['score'], primary_set,
                best_row['AUC_{0}'.format(primary_set)]))

        if refit:
            self.fit(data, varlist, tgt_name, weight_col=weight_col)

        return search_df

    def clone(self):
        """
        Create a copy of this LRMaster with the same parameters.

        The standardization configuration (`standardize` flag and scaler
        prototype) is carried over, but no fitted model or fitted scaler is
        copied.

        Returns
        -------
        LRMaster
            New instance with same params/standardization config but no fitted model
        """
        return LRMaster(
            params=dict(self.params),
            standardize=self.standardize,
            scaler=self._scaler_proto,
        )

set_data

set_data(data)

Store reference data for later use (e.g., calibration).

Parameters:

Name Type Description Default
data DataFrame

Training data to store

required

Returns:

Type Description
self
Source code in Modeling_Tool/Model/LRM_Tool.py
def set_data(self, data):
    """
    Store reference data for later use (e.g., calibration).

    Parameters
    ----------
    data : pd.DataFrame
        Training data to store

    Returns
    -------
    self
    """
    self._data = data
    return self

fit

fit(data, varlist, tgt_name, val_data=None, val_varlist=None, val_tgt_name=None, weight_col=None)

Train the logistic regression model.

When standardize=True, a scaler is fitted on the training features and stored as self.standardizer; the model is then trained on the scaled features. The same scaler is reused at prediction / evaluation time.

Parameters:

Name Type Description Default
data DataFrame

Training dataset containing features and target

required
varlist list of str

Feature column names to use for training

required
tgt_name str

Target variable column name

required
val_data DataFrame

Validation dataset (currently used for reference; not used in fitting)

None
val_varlist list of str

Validation feature column names

None
val_tgt_name str

Validation target variable column name

None
weight_col str

Column in data with per-sample training weights (non-negative). Mutually exclusive with passing sample_weight to lower-level helpers. With None the training is unweighted.

None

Returns:

Type Description
self

The fitted LRMaster.

Notes

Besides fitting model, this call sets varlist and tgt_name and keeps a reference to data, which get_statsmodel_summary, get_aic, get_bic and calibrate_model use when they are called without data (the reference is not pickled). calibrated_model is not reset: call calibrate_model again after refitting.

Source code in Modeling_Tool/Model/LRM_Tool.py
def fit(self, data, varlist, tgt_name, val_data=None, val_varlist=None, val_tgt_name=None, weight_col=None):
    """
    Train the logistic regression model.

    When `standardize=True`, a scaler is fitted on the training features and
    stored as `self.standardizer`; the model is then trained on the scaled
    features. The same scaler is reused at prediction / evaluation time.

    Parameters
    ----------
    data : pd.DataFrame
        Training dataset containing features and target
    varlist : list of str
        Feature column names to use for training
    tgt_name : str
        Target variable column name
    val_data : pd.DataFrame, optional
        Validation dataset (currently used for reference; not used in fitting)
    val_varlist : list of str, optional
        Validation feature column names
    val_tgt_name : str, optional
        Validation target variable column name
    weight_col : str, optional
        Column in ``data`` with per-sample training weights (non-negative).
        Mutually exclusive with passing ``sample_weight`` to lower-level helpers.
        With ``None`` the training is unweighted.

    Returns
    -------
    self
        The fitted ``LRMaster``.

    Notes
    -----
    Besides fitting ``model``, this call sets ``varlist`` and ``tgt_name`` and keeps a reference to ``data``, which
    ``get_statsmodel_summary``, ``get_aic``, ``get_bic`` and ``calibrate_model`` use when they are called without
    data (the reference is not pickled). ``calibrated_model`` is not reset: call ``calibrate_model`` again after
    refitting.
    """
    self.varlist = varlist
    self.tgt_name = tgt_name
    self._data = data

    train_x = data[varlist]
    if self.standardize:
        train_x = self._fit_standardizer(train_x)
    else:
        self.standardizer = None

    val_x = val_data[val_varlist] if val_data is not None and val_varlist is not None else None
    val_y = val_data[val_tgt_name] if val_data is not None and val_tgt_name is not None else None
    if val_x is not None:
        val_x = self._apply_standardizer(val_x)

    sample_weight = resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))
    self.model = lr_model(train_x, data[tgt_name], val_x, val_y, self.params, sample_weight=sample_weight)
    return self

calibrate_model

calibrate_model(model=None, train_df=None, method='sigmoid', cv=5, weight_col=None, sample_weight=None)

Model calibration with optional sample weights.

Wrap the model in a scikit-learn CalibratedClassifierCV and fit it; the result is stored as self.calibrated_model and is used by predict and predict_proba when they are called with calibrated_model=True.

Parameters:

Name Type Description Default
model sklearn-like LogisticRegression object

Model to calibrate. Defaults to self.model.

None
train_df DataFrame

Calibration data, with the model's feature columns and the target column self.tgt_name. Defaults to the frame stored by fit, stepwise_selection or set_data. Calibrating on the training frame is rarely what you want: pass a separate holdout frame.

None
method str

Calibration method, 'sigmoid' (Platt scaling) or 'isotonic'.

'sigmoid'
cv int or str

Cross-validation strategy of CalibratedClassifierCV. An integer clones the model and refits it on the folds of train_df; 'prefit' keeps the fitted model as it is and only fits the calibrator on train_df.

5
weight_col str

Name of a column of train_df with non-negative per-row sample weights used when fitting. Mutually exclusive with sample_weight.

None
sample_weight array - like

Per-row sample weights aligned with train_df, used like weight_col. Mutually exclusive with weight_col.

None

Returns:

Type Description
self

The LRMaster, whose calibrated_model is now set (an earlier calibrated model is replaced).

Raises:

Type Description
ValueError

If the feature list cannot be inferred (the model has no feature_names_in_ and varlist is not set), if cv='prefit' is used with a model that is not fitted, or if the sample weights are invalid or both weight_col and sample_weight are given.

KeyError

If weight_col is not a column of train_df.

Notes

The calibration features are scaled with the fitted standardizer when standardize=True, so the calibrated model works in the same feature space as model.

Source code in Modeling_Tool/Model/LRM_Tool.py
def calibrate_model(self, model=None, train_df=None, method='sigmoid', cv=5, weight_col=None, sample_weight=None):
    """Model calibration with optional sample weights.

    Wrap the model in a scikit-learn ``CalibratedClassifierCV`` and fit it; the result is stored as
    ``self.calibrated_model`` and is used by ``predict`` and ``predict_proba`` when they are called with
    ``calibrated_model=True``.

    Parameters
    ----------
    model : sklearn-like LogisticRegression object, optional
        Model to calibrate. Defaults to ``self.model``.
    train_df : pandas.DataFrame, optional
        Calibration data, with the model's feature columns and the target column ``self.tgt_name``. Defaults to
        the frame stored by ``fit``, ``stepwise_selection`` or ``set_data``. Calibrating on the
        training frame is rarely what you want: pass a separate holdout frame.
    method : str, default 'sigmoid'
        Calibration method, ``'sigmoid'`` (Platt scaling) or ``'isotonic'``.
    cv : int or str, default 5
        Cross-validation strategy of ``CalibratedClassifierCV``. An integer clones the model and refits it on the
        folds of ``train_df``; ``'prefit'`` keeps the fitted model as it is and only fits the calibrator on
        ``train_df``.
    weight_col : str, optional
        Name of a column of ``train_df`` with non-negative per-row sample weights used when fitting. Mutually
        exclusive with ``sample_weight``.
    sample_weight : array-like, optional
        Per-row sample weights aligned with ``train_df``, used like ``weight_col``. Mutually exclusive with
        ``weight_col``.

    Returns
    -------
    self
        The ``LRMaster``, whose ``calibrated_model`` is now set (an earlier calibrated model is replaced).

    Raises
    ------
    ValueError
        If the feature list cannot be inferred (the model has no ``feature_names_in_`` and ``varlist`` is not set),
        if ``cv='prefit'`` is used with a model that is not fitted, or if the sample weights are invalid or both
        ``weight_col`` and ``sample_weight`` are given.
    KeyError
        If ``weight_col`` is not a column of ``train_df``.

    Notes
    -----
    The calibration features are scaled with the fitted standardizer when ``standardize=True``, so the calibrated
    model works in the same feature space as ``model``.
    """
    from sklearn.calibration import CalibratedClassifierCV
    from sklearn.base import clone

    if train_df is None:
        train_df = self._data

    if model is None:
        model = self.model

    if hasattr(model, "feature_names_in_"):
        varlist = model.feature_names_in_.tolist()
    elif self.varlist is not None:
        varlist = self.varlist
    else:
        raise ValueError(
            "Cannot infer feature list from model. Please provide `varlist` when initializing LRMaster."
        )

    if hasattr(model, "get_params") and model.get_params().get("multi_class") == "deprecated":
        if cv == "prefit":
            model.set_params(multi_class="auto")
        else:
            model = clone(model)
            model.set_params(multi_class="auto")

    if cv == "prefit" and not hasattr(model, "classes_"):
        raise ValueError(
            "cv='prefit' requires a fitted model with `classes_`. "
            "Please pass a fitted LR model object or use cv=5 to refit during calibration."
        )

    # Standardize calibration features with the fitted scaler so the
    # calibrated model operates in the same feature space as self.model.
    cal_x = self._apply_standardizer(train_df[varlist])

    # sklearn 1.6 deprecated cv="prefit" in favour of wrapping the fitted
    # estimator in FrozenEstimator, and 1.8 removed "prefit" entirely. Use
    # FrozenEstimator when available, falling back to cv="prefit" on <1.6.
    estimator = model
    calib_kwargs = {"method": method}
    if cv == "prefit":
        try:
            from sklearn.frozen import FrozenEstimator
            estimator = FrozenEstimator(model)
        except ImportError:
            calib_kwargs["cv"] = "prefit"
    else:
        calib_kwargs["cv"] = cv

    # sklearn 1.2+ renamed base_estimator -> estimator; support both
    try:
        calibrated_model = CalibratedClassifierCV(estimator=estimator, **calib_kwargs)
    except TypeError:
        calibrated_model = CalibratedClassifierCV(base_estimator=estimator, **calib_kwargs)
    fit_weight = resolve_sample_weight(
        data=train_df,
        weight_col=weight_col,
        sample_weight=sample_weight,
        expected_len=len(train_df),
    )
    calibrated_model.fit(cal_x, train_df[self.tgt_name], sample_weight=fit_weight)

    self.calibrated_model = calibrated_model

    return self

eval_calibrated_outcome

eval_calibrated_outcome(evalset, plot=False, weight_col=None, sample_weight=None)

Evaluate calibrated vs raw probabilities on a holdout set.

Compute the Brier score of the raw and of the calibrated positive-class probabilities of evalset and, optionally, plot the reliability curves of both. calibrate_model must have been called first.

Parameters:

Name Type Description Default
evalset DataFrame

Holdout data with the feature columns of self.varlist and the target column self.tgt_name.

required
plot bool

If True, show a matplotlib reliability plot (10 bins) with the raw curve, the calibrated curve and the diagonal of perfect calibration.

False
weight_col str

Name of a column of evalset with non-negative per-row weights, used for the Brier scores and for the calibration curves. Mutually exclusive with sample_weight.

None
sample_weight array - like

Per-row weights aligned with evalset, used like weight_col. Mutually exclusive with weight_col.

None

Returns:

Type Description
None

Nothing is returned. The two Brier scores are written with logging at INFO level (logger Modeling_Tool.Model.LRM_Tool), so they appear only when INFO logging is enabled; they are not printed.

Notes

The calibration curves are weighted only when the installed scikit-learn accepts sample_weight in calibration_curve; otherwise the curves are computed unweighted (the Brier scores stay weighted).

Source code in Modeling_Tool/Model/LRM_Tool.py
def eval_calibrated_outcome(self, evalset, plot=False, weight_col=None, sample_weight=None):
    """Evaluate calibrated vs raw probabilities on a holdout set.

    Compute the Brier score of the raw and of the calibrated positive-class probabilities of ``evalset`` and,
    optionally, plot the reliability curves of both. ``calibrate_model`` must have been called first.

    Parameters
    ----------
    evalset : pandas.DataFrame
        Holdout data with the feature columns of ``self.varlist`` and the target column ``self.tgt_name``.
    plot : bool, default False
        If True, show a matplotlib reliability plot (10 bins) with the raw curve, the calibrated curve and the
        diagonal of perfect calibration.
    weight_col : str, optional
        Name of a column of ``evalset`` with non-negative per-row weights, used for the Brier scores and for the
        calibration curves. Mutually exclusive with ``sample_weight``.
    sample_weight : array-like, optional
        Per-row weights aligned with ``evalset``, used like ``weight_col``. Mutually exclusive with ``weight_col``.

    Returns
    -------
    None
        Nothing is returned. The two Brier scores are written with ``logging`` at INFO level (logger
        ``Modeling_Tool.Model.LRM_Tool``), so they appear only when INFO logging is enabled; they are not printed.

    Notes
    -----
    The calibration curves are weighted only when the installed scikit-learn accepts ``sample_weight`` in
    ``calibration_curve``; otherwise the curves are computed unweighted (the Brier scores stay weighted).
    """
    from sklearn.calibration import calibration_curve
    from sklearn.metrics import brier_score_loss

    y_val = evalset[self.tgt_name]
    eval_weight = resolve_sample_weight(
        data=evalset,
        weight_col=weight_col,
        sample_weight=sample_weight,
        expected_len=len(evalset),
    )

    # Raw probabilities
    prob_raw = self.predict_proba(evalset)[:, 1]
    # Calibrated probabilities (Platt scaling)
    prob_cal = self.predict_proba(evalset, calibrated_model=True)[:, 1]

    # 1. Brier score (lower is better)
    logger.info(
        "Raw Brier: %.6f",
        brier_score_loss(y_val, prob_raw, sample_weight=eval_weight),
    )
    logger.info(
        "Cal Brier: %.6f",
        brier_score_loss(y_val, prob_cal, sample_weight=eval_weight),
    )

    # 2. Reliability curve
    curve_kwargs = {} if eval_weight is None else {"sample_weight": eval_weight}
    try:
        fraction_of_positives_raw, mean_predicted_value_raw = calibration_curve(
            y_val, prob_raw, n_bins=10, **curve_kwargs
        )
        fraction_of_positives_cal, mean_predicted_value_cal = calibration_curve(
            y_val, prob_cal, n_bins=10, **curve_kwargs
        )
    except TypeError:
        fraction_of_positives_raw, mean_predicted_value_raw = calibration_curve(
            y_val, prob_raw, n_bins=10
        )
        fraction_of_positives_cal, mean_predicted_value_cal = calibration_curve(
            y_val, prob_cal, n_bins=10
        )

    if plot:
        import matplotlib.pyplot as plt
        plt.plot(mean_predicted_value_raw, fraction_of_positives_raw, 's-', label='Raw')
        plt.plot(mean_predicted_value_cal, fraction_of_positives_cal, 'o-', label='Platt')
        plt.plot([0,1], [0,1], 'k--', label='Perfect')
        plt.xlabel('Mean Predicted Probability')
        plt.ylabel('Fraction of Positives')
        plt.legend()
        plt.show()

predict

predict(data, varlist=None, calibrated_model=False)

Predict using the trained model.

When standardization is enabled, the input features are scaled with the scaler fitted during fit before being passed to the model.

Parameters:

Name Type Description Default
data DataFrame

Input data for prediction

required
varlist list

Feature names (uses training features if None)

None
calibrated_model bool

If True, predict with the calibrated model stored by calibrate_model instead of the raw model. calibrate_model must have been called first: otherwise calibrated_model is None and an AttributeError is raised.

False

Returns:

Type Description
ndarray

Predicted class labels (hard labels such as 0/1, not probabilities; use predict_proba for probabilities)

Source code in Modeling_Tool/Model/LRM_Tool.py
def predict(self, data, varlist=None, calibrated_model = False):
    """
    Predict using the trained model.

    When standardization is enabled, the input features are scaled with the
    scaler fitted during `fit` before being passed to the model.

    Parameters
    ----------
    data : pandas.DataFrame
        Input data for prediction
    varlist : list, optional
        Feature names (uses training features if None)
    calibrated_model : bool, default False
        If True, predict with the calibrated model stored by ``calibrate_model`` instead of the raw model.
        ``calibrate_model`` must have been called first: otherwise ``calibrated_model`` is ``None`` and an
        ``AttributeError`` is raised.

    Returns
    -------
    numpy.ndarray
        Predicted class labels (hard labels such as 0/1, not probabilities; use ``predict_proba`` for
        probabilities)
    """
    if varlist is None:
        varlist = self.varlist

    x = self._apply_standardizer(data[varlist])

    if calibrated_model:
        _patch_calibrated_model(self.calibrated_model)
        return self.calibrated_model.predict(x)

    return self.model.predict(x)

predict_proba

predict_proba(data, varlist=None, calibrated_model=False)

Predict class probabilities.

When standardization is enabled, the input features are scaled with the scaler fitted during fit before being passed to the model.

Parameters:

Name Type Description Default
data DataFrame

Input data for prediction

required
varlist list

Feature names (uses training features if None)

None
calibrated_model bool

If True, return the probabilities of the calibrated model stored by calibrate_model instead of those of the raw model. calibrate_model must have been called first: otherwise calibrated_model is None and an AttributeError is raised.

False

Returns:

Type Description
ndarray

Array of shape (n_samples, 2) with class probabilities (the columns follow the model's classes_, so column 1 is the probability of class 1 for a 0/1 target)

Source code in Modeling_Tool/Model/LRM_Tool.py
def predict_proba(self, data, varlist=None, calibrated_model = False):
    """
    Predict class probabilities.

    When standardization is enabled, the input features are scaled with the
    scaler fitted during `fit` before being passed to the model.

    Parameters
    ----------
    data : pandas.DataFrame
        Input data for prediction
    varlist : list, optional
        Feature names (uses training features if None)
    calibrated_model : bool, default False
        If True, return the probabilities of the calibrated model stored by ``calibrate_model`` instead of
        those of the raw model. ``calibrate_model`` must have been called first: otherwise
        ``calibrated_model`` is ``None`` and an ``AttributeError`` is raised.

    Returns
    -------
    numpy.ndarray
        Array of shape (n_samples, 2) with class probabilities (the columns follow the model's ``classes_``, so
        column 1 is the probability of class 1 for a 0/1 target)
    """
    if varlist is None:
        varlist = self.varlist

    x = self._apply_standardizer(data[varlist])

    if calibrated_model:
        _patch_calibrated_model(self.calibrated_model)
        return self.calibrated_model.predict_proba(x)

    return self.model.predict_proba(x)

get_variable_importance

get_variable_importance()

Get variable importance (coefficients) from the model.

Returns:

Type Description
DataFrame

DataFrame with columns ['varlist', 'coef', 'importance'] sorted by importance in descending order

Notes

When standardization is enabled the coefficients are expressed in the standardized feature space (i.e. they are directly comparable in magnitude across features).

Source code in Modeling_Tool/Model/LRM_Tool.py
def get_variable_importance(self):
    """
    Get variable importance (coefficients) from the model.

    Returns
    -------
    pandas.DataFrame
        DataFrame with columns ['varlist', 'coef', 'importance'] sorted by
        importance in descending order

    Notes
    -----
    When standardization is enabled the coefficients are expressed in the
    standardized feature space (i.e. they are directly comparable in
    magnitude across features).
    """
    return lr_varimp(self.model)

get_statsmodel_summary

get_statsmodel_summary(data=None, varlist=None, tgt_name=None)

Generate a statsmodels-style summary for the trained LR model.

Parameters:

Name Type Description Default
data DataFrame

Data for computing the summary (uses stored training data if None)

None
varlist list of str

Feature names (uses stored varlist if None)

None
tgt_name str

Target variable name (uses stored tgt_name if None)

None

Returns:

Type Description
DataFrame

Summary table with coefficients, standard errors, z-scores and p-values (columns coef, std_err, z, p_value, ci_lower, ci_upper), indexed by 'Intercept' followed by the feature names; see get_lr_statsmodel_summary.

Raises:

Type Description
ValueError

If data is None and no training frame is stored (for example on a model loaded from disk, because the stored frame is not pickled).

Notes

When standardization is enabled the summary is computed on the standardized feature space, consistent with how the model was trained.

Source code in Modeling_Tool/Model/LRM_Tool.py
def get_statsmodel_summary(self, data=None, varlist=None, tgt_name=None):
    """
    Generate a statsmodels-style summary for the trained LR model.

    Parameters
    ----------
    data : pd.DataFrame, optional
        Data for computing the summary (uses stored training data if None)
    varlist : list of str, optional
        Feature names (uses stored varlist if None)
    tgt_name : str, optional
        Target variable name (uses stored tgt_name if None)

    Returns
    -------
    pandas.DataFrame
        Summary table with coefficients, standard errors, z-scores and p-values (columns ``coef``, ``std_err``,
        ``z``, ``p_value``, ``ci_lower``, ``ci_upper``), indexed by ``'Intercept'`` followed by the feature names;
        see ``get_lr_statsmodel_summary``.

    Raises
    ------
    ValueError
        If ``data`` is ``None`` and no training frame is stored (for example on a model loaded from disk, because
        the stored frame is not pickled).

    Notes
    -----
    When standardization is enabled the summary is computed on the
    standardized feature space, consistent with how the model was trained.
    """
    data = self._require_data(data)
    if varlist is None:
        varlist = self.varlist
    if tgt_name is None:
        tgt_name = self.tgt_name

    return get_lr_statsmodel_summary(
        self.model,
        self._apply_standardizer(data[varlist]),
        data[tgt_name],
        feature_names=varlist
    )

get_aic

get_aic(data=None, varlist=None, tgt_name=None, weight_col=None)

Compute AIC for the trained model.

Parameters:

Name Type Description Default
data DataFrame

Data to evaluate the model on (uses the stored training data if None).

None
varlist list of str

Feature names (uses the stored varlist if None).

None
tgt_name str

Target variable name (uses the stored tgt_name if None).

None
weight_col str

Name of a column of data with non-negative per-row weights; the weighted log-likelihood is used. With None the AIC is unweighted.

None

Returns:

Type Description
float

AIC value (lower is better).

Raises:

Type Description
ValueError

If data is None and no training frame is stored (for example on a model loaded from disk, because the stored frame is not pickled), or if the weights are invalid.

KeyError

If weight_col is not a column of data.

Source code in Modeling_Tool/Model/LRM_Tool.py
def get_aic(self, data=None, varlist=None, tgt_name=None, weight_col=None):
    """
    Compute AIC for the trained model.

    Parameters
    ----------
    data : pd.DataFrame, optional
        Data to evaluate the model on (uses the stored training data if None).
    varlist : list of str, optional
        Feature names (uses the stored varlist if None).
    tgt_name : str, optional
        Target variable name (uses the stored tgt_name if None).
    weight_col : str, optional
        Name of a column of ``data`` with non-negative per-row weights; the weighted log-likelihood is used. With
        ``None`` the AIC is unweighted.

    Returns
    -------
    float
        AIC value (lower is better).

    Raises
    ------
    ValueError
        If ``data`` is ``None`` and no training frame is stored (for example on a model loaded from disk, because
        the stored frame is not pickled), or if the weights are invalid.
    KeyError
        If ``weight_col`` is not a column of ``data``.
    """
    data = self._require_data(data)
    if varlist is None:
        varlist = self.varlist
    if tgt_name is None:
        tgt_name = self.tgt_name
    return compute_aic(
        self.model,
        self._apply_standardizer(data[varlist]),
        data[tgt_name],
        sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data)),
    )

get_bic

get_bic(data=None, varlist=None, tgt_name=None, weight_col=None)

Compute BIC for the trained model.

Parameters:

Name Type Description Default
data DataFrame

Data to evaluate the model on (uses the stored training data if None).

None
varlist list of str

Feature names (uses the stored varlist if None).

None
tgt_name str

Target variable name (uses the stored tgt_name if None).

None
weight_col str

Name of a column of data with non-negative per-row weights; the weighted log-likelihood is used and the sample size in the penalty is the sum of the weights. With None the BIC is unweighted.

None

Returns:

Type Description
float

BIC value (lower is better).

Raises:

Type Description
ValueError

If data is None and no training frame is stored (for example on a model loaded from disk, because the stored frame is not pickled), or if the weights are invalid.

KeyError

If weight_col is not a column of data.

Source code in Modeling_Tool/Model/LRM_Tool.py
def get_bic(self, data=None, varlist=None, tgt_name=None, weight_col=None):
    """
    Compute BIC for the trained model.

    Parameters
    ----------
    data : pd.DataFrame, optional
        Data to evaluate the model on (uses the stored training data if None).
    varlist : list of str, optional
        Feature names (uses the stored varlist if None).
    tgt_name : str, optional
        Target variable name (uses the stored tgt_name if None).
    weight_col : str, optional
        Name of a column of ``data`` with non-negative per-row weights; the weighted log-likelihood is used and the
        sample size in the penalty is the sum of the weights. With ``None`` the BIC is unweighted.

    Returns
    -------
    float
        BIC value (lower is better).

    Raises
    ------
    ValueError
        If ``data`` is ``None`` and no training frame is stored (for example on a model loaded from disk, because
        the stored frame is not pickled), or if the weights are invalid.
    KeyError
        If ``weight_col`` is not a column of ``data``.
    """
    data = self._require_data(data)
    if varlist is None:
        varlist = self.varlist
    if tgt_name is None:
        tgt_name = self.tgt_name
    return compute_bic(
        self.model,
        self._apply_standardizer(data[varlist]),
        data[tgt_name],
        sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data)),
    )

stepwise_selection

stepwise_selection(data, varlist, tgt_name, criterion='aic', direction='both', max_iter=100, verbose=True, weight_col=None)

Perform stepwise variable selection.

Iteratively adds or removes features based on AIC/BIC improvement.

When standardize=True, all interim fits and the final model are trained on standardized features, and the fitted scaler for the selected columns is stored on the instance for later prediction.

Parameters:

Name Type Description Default
data DataFrame

Training data

required
varlist list of str

Initial feature list (the candidate variables). With direction='forward' the search starts from an empty model and adds variables from this list; otherwise it starts from all of them.

required
tgt_name str

Target variable name

required
criterion str

Selection criterion, 'aic' or 'bic'. Any value other than 'aic' selects by BIC.

'aic'
direction str

Direction of stepwise selection: 'forward', 'backward', or 'both'. 'both' starts from all variables like 'backward' but can add a removed variable back. Any other value performs no step and keeps all of varlist.

'both'
max_iter int

Maximum number of iterations. Each iteration makes at most one forward step and one backward step, and the search stops earlier when no step lowers the criterion.

100
verbose bool

Whether to log progress (one INFO message per step and a final summary, through the module logger); nothing is printed.

True
weight_col str

Name of a column of data with non-negative per-row weights, used to fit every candidate model and for the weighted AIC/BIC. With None the selection is unweighted.

None

Returns:

Type Description
list of str

Selected feature list

Notes

The instance is updated: varlist, tgt_name and the stored training frame are replaced, the standardizer is refitted on the selected columns when standardize=True, and model is retrained on the selected features. A candidate whose fit or scoring raises an exception is skipped silently.

Source code in Modeling_Tool/Model/LRM_Tool.py
def stepwise_selection(
    self,
    data,
    varlist,
    tgt_name,
    criterion='aic',
    direction='both',
    max_iter=100,
    verbose=True,
    weight_col=None,
):
    """
    Perform stepwise variable selection.

    Iteratively adds or removes features based on AIC/BIC improvement.

    When `standardize=True`, all interim fits and the final model are
    trained on standardized features, and the fitted scaler for the selected
    columns is stored on the instance for later prediction.

    Parameters
    ----------
    data : pd.DataFrame
        Training data
    varlist : list of str
        Initial feature list (the candidate variables). With ``direction='forward'`` the search starts from an
        empty model and adds variables from this list; otherwise it starts from all of them.
    tgt_name : str
        Target variable name
    criterion : str, default 'aic'
        Selection criterion, 'aic' or 'bic'. Any value other than 'aic' selects by BIC.
    direction : str, default 'both'
        Direction of stepwise selection: 'forward', 'backward', or 'both'. 'both' starts from all variables like
        'backward' but can add a removed variable back. Any other value performs no step and keeps all of
        ``varlist``.
    max_iter : int, default 100
        Maximum number of iterations. Each iteration makes at most one forward step and one backward step, and the
        search stops earlier when no step lowers the criterion.
    verbose : bool, default True
        Whether to log progress (one INFO message per step and a final summary, through the module logger);
        nothing is printed.
    weight_col : str, optional
        Name of a column of ``data`` with non-negative per-row weights, used to fit every candidate model and for
        the weighted AIC/BIC. With ``None`` the selection is unweighted.

    Returns
    -------
    list of str
        Selected feature list

    Notes
    -----
    The instance is updated: ``varlist``, ``tgt_name`` and the stored training frame are replaced, the standardizer
    is refitted on the selected columns when ``standardize=True``, and ``model`` is retrained on the selected
    features. A candidate whose fit or scoring raises an exception is skipped silently.
    """
    if criterion == 'aic':
        score_fn = lambda model, x, y: compute_aic(
            model, x, y, sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))
        )
    else:
        score_fn = lambda model, x, y: compute_bic(
            model, x, y, sample_weight=resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))
        )
    sample_weight = resolve_sample_weight(data=data, weight_col=weight_col, expected_len=len(data))

    # When standardizing, operate on a once-standardized feature frame.
    # Column-wise scalers (StandardScaler / MinMaxScaler) make slicing a
    # subset of columns equivalent to standardizing that subset.
    if self.standardize:
        interim_scaler = self._make_scaler()
        interim_scaler.fit(data[varlist])
        work = pd.DataFrame(
            interim_scaler.transform(data[varlist]),
            columns=list(varlist), index=data.index,
        )
    else:
        work = data

    current_vars = list(varlist) if direction != 'forward' else []
    remaining_vars = list(varlist) if direction == 'forward' else []

    best_model = lr_model(
        work[current_vars] if current_vars else pd.DataFrame(index=data.index),
        data[tgt_name], None, None, self.params, sample_weight=sample_weight
    ) if current_vars else None

    best_score = score_fn(best_model, work[current_vars], data[tgt_name]) if best_model else float('inf')

    for iteration in range(max_iter):
        improved = False

        # Forward step
        if direction in ('forward', 'both') and remaining_vars:
            scores = {}
            for var in remaining_vars:
                trial_vars = current_vars + [var]
                try:
                    model = lr_model(work[trial_vars], data[tgt_name], None, None, self.params, sample_weight=sample_weight)
                    scores[var] = score_fn(model, work[trial_vars], data[tgt_name])
                except Exception:
                    continue
            if scores:
                best_var = min(scores, key=scores.get)
                if scores[best_var] < best_score:
                    current_vars.append(best_var)
                    remaining_vars.remove(best_var)
                    best_score = scores[best_var]
                    improved = True
                    if verbose:
                        logger.info(f"[Step {iteration+1}] ADD '{best_var}', {criterion.upper()}={best_score:.4f}")

        # Backward step
        if direction in ('backward', 'both') and len(current_vars) > 1:
            scores = {}
            for var in current_vars:
                trial_vars = [v for v in current_vars if v != var]
                try:
                    model = lr_model(work[trial_vars], data[tgt_name], None, None, self.params, sample_weight=sample_weight)
                    scores[var] = score_fn(model, work[trial_vars], data[tgt_name])
                except Exception:
                    continue
            if scores:
                worst_var = min(scores, key=scores.get)
                if scores[worst_var] < best_score:
                    current_vars.remove(worst_var)
                    if direction == 'both':
                        remaining_vars.append(worst_var)
                    best_score = scores[worst_var]
                    improved = True
                    if verbose:
                        logger.info(f"[Step {iteration+1}] REMOVE '{worst_var}', {criterion.upper()}={best_score:.4f}")

        if not improved:
            break

    if verbose:
        logger.info(f"Stepwise selection complete. Selected {len(current_vars)} features.")

    self.varlist = current_vars
    self.tgt_name = tgt_name
    self._data = data

    if self.standardize:
        self.standardizer = self._make_scaler()
        self.standardizer.fit(data[current_vars])
        final_x = self._apply_standardizer(data[current_vars])
    else:
        self.standardizer = None
        final_x = data[current_vars]

    self.model = lr_model(final_x, data[tgt_name], None, None, self.params, sample_weight=sample_weight)
    return current_vars

grid_search_params

grid_search_params(data, varlist, tgt_name, eval_sets, param_grid, objective='oot_gap_penalized', primary_set=None, gap_ref_sets=None, metric='auc', refit=True, verbose=True, weight_col=None, eval_weight_col=None)

Grid-search LogisticRegression hyperparameters over a holdout-based objective.

For every combination in param_grid (Cartesian product), a candidate model is trained on data and scored by AUC on each dataset in eval_sets. The best combination is chosen by objective (default rewards a high primary-set AUC while penalizing the train/holdout AUC gap, i.e. overfitting). This is a holdout search (not k-fold CV), intended for the typical INS/OOS/OOT credit-scoring setup.

When standardize=True on this instance, every candidate inherits the same standardization config (each candidate fits its own scaler on data), so the search runs in the same feature space the final (optionally refit) model uses.

Parameters:

Name Type Description Default
data DataFrame

Training dataset (e.g. the in-sample set used for fitting).

required
varlist list

Feature column names.

required
tgt_name str

Target column name.

required
eval_sets dict of {str: pandas.DataFrame}

Ordered mapping of datasets to score by AUC, e.g. {'ins': ins_df, 'oos': oos_df, 'oot': oot_df}.

required
param_grid dict of {str: iterable}

Hyperparameter search space, e.g. {'C': np.logspace(-3, 2, 31)}. Multiple keys are combined as a Cartesian product.

required
objective str or callable

How to score each candidate from its per-set AUCs:

  • 'oot_gap_penalized' : AUC[primary] - |mean(AUC[gap_refs]) - AUC[primary]| (maximize the primary set while penalizing the overfitting gap).
  • 'max_primary' : AUC[primary].
  • callable : f(auc_dict) -> float where auc_dict maps set name to AUC.
'oot_gap_penalized'
primary_set str

Key of eval_sets whose AUC to maximize. Defaults to the last key.

None
gap_ref_sets list of str

Set names whose mean AUC forms the gap reference. Defaults to all sets except primary_set. Only used by 'oot_gap_penalized'.

None
metric str

Evaluation metric. Currently only 'auc' is supported.

'auc'
refit bool

If True, refit self on data with the best parameters after searching.

True
verbose bool

Print progress / best result.

True
weight_col str

Name of a column of data with non-negative per-row sample weights, used to train every candidate model (and the final refit). With None the training is unweighted.

None
eval_weight_col str

Name of a column that every dataset in eval_sets must contain, holding non-negative per-row weights for the AUC computed on that set. With None the AUCs are unweighted. The training weights are not applied to the evaluation sets, and these weights are not applied to training.

None

Returns:

Type Description
DataFrame

Search results sorted by score descending, with columns: the param name(s) + AUC_<name> per eval set + gap (gap objective only: the mean AUC of gap_ref_sets minus the primary AUC) + score.

Raises:

Type Description
ValueError

If metric is not 'auc', eval_sets is empty, or primary_set is not a key of eval_sets.

KeyError

If data or an evaluation dataset lacks a column of varlist or tgt_name, or if weight_col / eval_weight_col names a column that is missing.

Side Effects

Sets self.best_params_ (dict) and self.search_results_ (the returned table), and merges the best combo into self.params; if refit=True, also retrains self.model on data.

Examples:

>>> tuner = LRMaster(params={'C': 1.0, 'solver': 'lbfgs'})
>>> res = tuner.grid_search_params(
...     data=ins_fit, varlist=woe_cols, tgt_name='bad_flag',
...     eval_sets={'ins': ins_woe, 'oos': oos_woe, 'oot': oot_woe},
...     param_grid={'C': np.logspace(-3, 2, 31)},
...     primary_set='oot', gap_ref_sets=['ins', 'oos'], refit=False,
... )
>>> best_C = tuner.best_params_['C']
Source code in Modeling_Tool/Model/LRM_Tool.py
def grid_search_params(self, data, varlist, tgt_name, eval_sets, param_grid,
                       objective='oot_gap_penalized', primary_set=None,
                       gap_ref_sets=None, metric='auc', refit=True, verbose=True,
                       weight_col=None, eval_weight_col=None):
    """
    Grid-search LogisticRegression hyperparameters over a holdout-based objective.

    For every combination in ``param_grid`` (Cartesian product), a candidate model is
    trained on ``data`` and scored by AUC on each dataset in ``eval_sets``. The best
    combination is chosen by ``objective`` (default rewards a high primary-set AUC while
    penalizing the train/holdout AUC gap, i.e. overfitting). This is a **holdout** search
    (not k-fold CV), intended for the typical INS/OOS/OOT credit-scoring setup.

    When ``standardize=True`` on this instance, every candidate inherits the same
    standardization config (each candidate fits its own scaler on ``data``), so the
    search runs in the same feature space the final (optionally refit) model uses.

    Parameters
    ----------
    data : pandas.DataFrame
        Training dataset (e.g. the in-sample set used for fitting).
    varlist : list
        Feature column names.
    tgt_name : str
        Target column name.
    eval_sets : dict of {str: pandas.DataFrame}
        Ordered mapping of datasets to score by AUC, e.g.
        ``{'ins': ins_df, 'oos': oos_df, 'oot': oot_df}``.
    param_grid : dict of {str: iterable}
        Hyperparameter search space, e.g. ``{'C': np.logspace(-3, 2, 31)}``.
        Multiple keys are combined as a Cartesian product.
    objective : str or callable, default 'oot_gap_penalized'
        How to score each candidate from its per-set AUCs:

        - ``'oot_gap_penalized'`` : ``AUC[primary] - |mean(AUC[gap_refs]) - AUC[primary]|``
          (maximize the primary set while penalizing the overfitting gap).
        - ``'max_primary'`` : ``AUC[primary]``.
        - callable : ``f(auc_dict) -> float`` where ``auc_dict`` maps set name to AUC.
    primary_set : str, optional
        Key of ``eval_sets`` whose AUC to maximize. Defaults to the last key.
    gap_ref_sets : list of str, optional
        Set names whose mean AUC forms the gap reference. Defaults to all sets except
        ``primary_set``. Only used by ``'oot_gap_penalized'``.
    metric : str, default 'auc'
        Evaluation metric. Currently only ``'auc'`` is supported.
    refit : bool, default True
        If True, refit ``self`` on ``data`` with the best parameters after searching.
    verbose : bool, default True
        Print progress / best result.
    weight_col : str, optional
        Name of a column of ``data`` with non-negative per-row sample weights, used to train every candidate model
        (and the final refit). With ``None`` the training is unweighted.
    eval_weight_col : str, optional
        Name of a column that every dataset in ``eval_sets`` must contain, holding non-negative per-row weights for
        the AUC computed on that set. With ``None`` the AUCs are unweighted. The training weights are not applied to
        the evaluation sets, and these weights are not applied to training.

    Returns
    -------
    pandas.DataFrame
        Search results sorted by ``score`` descending, with columns: the param name(s)
        + ``AUC_<name>`` per eval set + ``gap`` (gap objective only: the mean AUC of ``gap_ref_sets`` minus the
        primary AUC) + ``score``.

    Raises
    ------
    ValueError
        If ``metric`` is not ``'auc'``, ``eval_sets`` is empty, or ``primary_set`` is not a key of ``eval_sets``.
    KeyError
        If ``data`` or an evaluation dataset lacks a column of ``varlist`` or ``tgt_name``, or if ``weight_col`` /
        ``eval_weight_col`` names a column that is missing.

    Side Effects
    ------------
    Sets ``self.best_params_`` (dict) and ``self.search_results_`` (the returned table),
    and merges the best combo into ``self.params``; if ``refit=True``, also retrains
    ``self.model`` on ``data``.

    Examples
    --------
    >>> tuner = LRMaster(params={'C': 1.0, 'solver': 'lbfgs'})
    >>> res = tuner.grid_search_params(
    ...     data=ins_fit, varlist=woe_cols, tgt_name='bad_flag',
    ...     eval_sets={'ins': ins_woe, 'oos': oos_woe, 'oot': oot_woe},
    ...     param_grid={'C': np.logspace(-3, 2, 31)},
    ...     primary_set='oot', gap_ref_sets=['ins', 'oos'], refit=False,
    ... )
    >>> best_C = tuner.best_params_['C']
    """
    import itertools
    from sklearn.metrics import roc_auc_score

    if metric != 'auc':
        raise ValueError("Only metric='auc' is currently supported.")
    if not eval_sets:
        raise ValueError("eval_sets must be a non-empty {name: DataFrame} mapping.")

    set_names = list(eval_sets.keys())
    if primary_set is None:
        primary_set = set_names[-1]
    if primary_set not in eval_sets:
        raise ValueError("primary_set '{0}' not in eval_sets {1}".format(primary_set, set_names))
    if gap_ref_sets is None:
        gap_ref_sets = [n for n in set_names if n != primary_set]

    # Validate columns up-front for a clear error instead of a deep KeyError.
    missing = [c for c in (list(varlist) + [tgt_name]) if c not in data.columns]
    if missing:
        raise KeyError("training data missing columns: {0}".format(missing))
    for _name, _df in eval_sets.items():
        _miss = [c for c in (list(varlist) + [tgt_name]) if c not in _df.columns]
        if _miss:
            raise KeyError("eval set '{0}' missing columns: {1}".format(_name, _miss))

    param_names = list(param_grid.keys())
    combos = list(itertools.product(*[list(param_grid[k]) for k in param_names]))
    use_gap = (not callable(objective)) and objective == 'oot_gap_penalized' and len(gap_ref_sets) > 0

    if verbose:
        print("grid_search_params: {0} combinations (params={1}), training set {2:,} rows, eval={3}".format(
            len(combos), param_names, len(data), set_names))

    def _score(auc_dict):
        if callable(objective):
            return objective(auc_dict)
        if objective == 'max_primary':
            return auc_dict[primary_set]
        if objective == 'oot_gap_penalized':
            primary = auc_dict[primary_set]
            if gap_ref_sets:
                ref = float(np.mean([auc_dict[n] for n in gap_ref_sets]))
                return primary - abs(ref - primary)
            return primary
        raise ValueError("Unknown objective: {0}".format(objective))

    rows = []
    for combo in combos:
        combo_dict = dict(zip(param_names, combo))
        # Candidates inherit this instance's standardization config so the
        # search happens in the same feature space the final model uses.
        cand = LRMaster(
            params={**self.params, **combo_dict},
            standardize=self.standardize,
            scaler=self._scaler_proto,
        )
        cand.fit(data, varlist, tgt_name, weight_col=weight_col)

        auc_dict = {}
        for name, df_eval in eval_sets.items():
            proba = cand.predict_proba(df_eval, varlist)[:, 1]
            eval_sw = resolve_sample_weight(data=df_eval, weight_col=eval_weight_col, expected_len=len(df_eval))
            auc_dict[name] = roc_auc_score(df_eval[tgt_name], proba, sample_weight=eval_sw)

        row = dict(combo_dict)
        for name in set_names:
            row['AUC_{0}'.format(name)] = round(auc_dict[name], 5)
        if use_gap:
            ref = float(np.mean([auc_dict[n] for n in gap_ref_sets]))
            row['gap'] = round(ref - auc_dict[primary_set], 5)
        row['score'] = round(_score(auc_dict), 5)
        rows.append(row)

    search_df = pd.DataFrame(rows).sort_values('score', ascending=False).reset_index(drop=True)

    # Best params from the (unrounded) top row; cast numpy scalars to native
    # Python types for a clean repr / JSON-serializable params.
    best_row = search_df.iloc[0]

    def _native(v):
        return v.item() if hasattr(v, 'item') else v

    # Read each parameter from its own column: ``best_row`` is one row Series of a frame with float AUC columns, so
    # an integer parameter (``max_iter=100``) came out as ``100.0`` and the final fit raised InvalidParameterError.
    self.best_params_ = {k: _native(search_df[k].iloc[0]) for k in param_names}
    self.search_results_ = search_df
    self.params = {**self.params, **self.best_params_}

    # Round float param columns for display only (does not affect best_params_).
    for k in param_names:
        if pd.api.types.is_float_dtype(search_df[k]):
            search_df[k] = search_df[k].round(5)

    if verbose:
        print("★ best: {0} | score={1:.5f} | AUC_{2}={3:.5f}".format(
            self.best_params_, best_row['score'], primary_set,
            best_row['AUC_{0}'.format(primary_set)]))

    if refit:
        self.fit(data, varlist, tgt_name, weight_col=weight_col)

    return search_df

clone

clone()

Create a copy of this LRMaster with the same parameters.

The standardization configuration (standardize flag and scaler prototype) is carried over, but no fitted model or fitted scaler is copied.

Returns:

Type Description
LRMaster

New instance with same params/standardization config but no fitted model

Source code in Modeling_Tool/Model/LRM_Tool.py
def clone(self):
    """
    Create a copy of this LRMaster with the same parameters.

    The standardization configuration (`standardize` flag and scaler
    prototype) is carried over, but no fitted model or fitted scaler is
    copied.

    Returns
    -------
    LRMaster
        New instance with same params/standardization config but no fitted model
    """
    return LRMaster(
        params=dict(self.params),
        standardize=self.standardize,
        scaler=self._scaler_proto,
    )

lr_model

lr_model(mdlx, mdly, valx, valy, params_dict, sample_weight=None)

Train a Logistic Regression model.

Parameters:

Name Type Description Default
mdlx DataFrame or ndarray

Training feature matrix

required
mdly Series or ndarray

Training target variable

required
valx DataFrame or ndarray

Validation feature matrix (used for reference only: it is accepted for interface symmetry and never read, so None is fine)

required
valy Series or ndarray

Validation target variable (used for reference only: it is accepted for interface symmetry and never read, so None is fine)

required
params_dict dict

Dictionary of parameters for LogisticRegression. None is treated as an empty dict (library defaults) and a multi_class='deprecated' entry is replaced by 'auto'.

required
sample_weight array - like

Per-sample weights passed to LogisticRegression.fit.

None

Returns:

Type Description
LogisticRegression

Trained logistic regression model

Source code in Modeling_Tool/Model/LRM_Tool.py
def lr_model(mdlx, mdly, valx, valy, params_dict, sample_weight=None):
    """
    Train a Logistic Regression model.

    Parameters
    ----------
    mdlx : pandas.DataFrame or numpy.ndarray
        Training feature matrix
    mdly : pandas.Series or numpy.ndarray
        Training target variable
    valx : pandas.DataFrame or numpy.ndarray
        Validation feature matrix (used for reference only: it is accepted for interface symmetry and never read, so
        ``None`` is fine)
    valy : pandas.Series or numpy.ndarray
        Validation target variable (used for reference only: it is accepted for interface symmetry and never read, so
        ``None`` is fine)
    params_dict : dict
        Dictionary of parameters for LogisticRegression. ``None`` is treated as an empty dict (library defaults) and a
        ``multi_class='deprecated'`` entry is replaced by ``'auto'``.
    sample_weight : array-like, optional
        Per-sample weights passed to ``LogisticRegression.fit``.

    Returns
    -------
    sklearn.linear_model.LogisticRegression
        Trained logistic regression model
    """
    params_dict = _sanitize_lr_params(params_dict)
    model = LogisticRegression(**params_dict)
    model.fit(mdlx, mdly, sample_weight=sample_weight)
    return model

lr_varimp

lr_varimp(model)

Get variable importance from a Logistic Regression model.

Computes the absolute value of the model coefficients as a measure of variable importance.

Parameters:

Name Type Description Default
model LogisticRegression

Trained logistic regression model

required

Returns:

Type Description
DataFrame

DataFrame with columns ['varlist', 'coef', 'importance'] sorted by importance in descending order. The names in varlist come from model.feature_names_in_ when it exists, otherwise they are x0, x1, ...

Source code in Modeling_Tool/Model/LRM_Tool.py
def lr_varimp(model):
    """
    Get variable importance from a Logistic Regression model.

    Computes the absolute value of the model coefficients as a measure of
    variable importance.

    Parameters
    ----------
    model : sklearn.linear_model.LogisticRegression
        Trained logistic regression model

    Returns
    -------
    pandas.DataFrame
        DataFrame with columns ['varlist', 'coef', 'importance'] sorted by
        importance in descending order. The names in ``varlist`` come from
        ``model.feature_names_in_`` when it exists, otherwise they are ``x0``, ``x1``, ...
    """
    if hasattr(model, 'feature_names_in_'):
        varnames = model.feature_names_in_.tolist()
    else:
        varnames = [f'x{i}' for i in range(len(model.coef_[0]))]

    varimp_df = pd.DataFrame({
        'varlist': varnames,
        'coef': model.coef_[0],
        'importance': np.abs(model.coef_[0])
    })
    return varimp_df.sort_values('importance', ascending=False).reset_index(drop=True)

fast_lr_pvalues

fast_lr_pvalues(model, x, feature_names)

Coefficient p-values for a fitted sklearn LogisticRegression via the observed Fisher information — same formula as get_lr_statsmodel_summary but vectorized (no n x n diagonal weight matrix), so it stays O(n*k) at pipeline sample sizes. Returns a Series indexed by ['Intercept', *feature_names].

Parameters:

Name Type Description Default
model LogisticRegression

Fitted logistic regression model.

required
x DataFrame or ndarray

Feature matrix whose columns follow the order of the model coefficients.

required
feature_names list of str

Names of the columns of x; they label the p-values after 'Intercept'.

required

Returns:

Type Description
Series

Two-sided p-values of the intercept and of the coefficients, indexed by ['Intercept', *feature_names].

Source code in Modeling_Tool/Model/LRM_Tool.py
def fast_lr_pvalues(model, x, feature_names):
    """Coefficient p-values for a fitted sklearn LogisticRegression via the
    observed Fisher information — same formula as get_lr_statsmodel_summary
    but vectorized (no n x n diagonal weight matrix), so it stays ``O(n*k)``
    at pipeline sample sizes. Returns a Series indexed by
    ``['Intercept', *feature_names]``.

    Parameters
    ----------
    model : sklearn.linear_model.LogisticRegression
        Fitted logistic regression model.
    x : pandas.DataFrame or numpy.ndarray
        Feature matrix whose columns follow the order of the model coefficients.
    feature_names : list of str
        Names of the columns of ``x``; they label the p-values after ``'Intercept'``.

    Returns
    -------
    pandas.Series
        Two-sided p-values of the intercept and of the coefficients, indexed by ``['Intercept', *feature_names]``.
    """
    from scipy import stats

    x_arr = x.values if hasattr(x, 'values') else np.array(x)
    prob = _predict_positive_proba(model, x_arr)
    w = prob * (1 - prob)
    X_design = np.hstack([np.ones((x_arr.shape[0], 1)), x_arr])
    fisher = X_design.T @ (X_design * w[:, None])
    try:
        cov_matrix = np.linalg.inv(fisher)
    except np.linalg.LinAlgError:
        cov_matrix = np.linalg.pinv(fisher)
    all_coefs = np.concatenate([[model.intercept_[0]], model.coef_[0]])
    std_errs = np.sqrt(np.diag(cov_matrix))
    z_scores = all_coefs / std_errs
    p_values = 2 * (1 - stats.norm.cdf(np.abs(z_scores)))
    return pd.Series(p_values, index=['Intercept'] + list(feature_names))

get_lr_statsmodel_summary

get_lr_statsmodel_summary(model, x, y, feature_names=None)

Generate a statsmodels-style summary for a sklearn LogisticRegression model.

Computes standard errors, z-scores, p-values, and confidence intervals for the logistic regression coefficients using the observed Fisher information matrix.

Parameters:

Name Type Description Default
model LogisticRegression

Trained logistic regression model

required
x DataFrame or ndarray

Feature matrix used for training

required
y Series or ndarray

Target variable used for training (not used in the computation: the Fisher information depends only on the predicted probabilities for x)

required
feature_names list of str

Feature names (inferred from x if not provided: the columns of x, else model.feature_names_in_, else x0, x1, ...)

None

Returns:

Type Description
DataFrame

Summary table with columns: ['coef', 'std_err', 'z', 'p_value', 'ci_lower', 'ci_upper'], indexed by 'Intercept' followed by the feature names. ci_lower and ci_upper are the 95% Wald bounds coef -/+ 1.96 * std_err.

Notes

The weight matrix is a dense n x n array (n is the number of rows), so memory use grows with the square of the sample size. fast_lr_pvalues returns the same p-values without that matrix.

Source code in Modeling_Tool/Model/LRM_Tool.py
def get_lr_statsmodel_summary(model, x, y, feature_names=None):
    """
    Generate a statsmodels-style summary for a sklearn LogisticRegression model.

    Computes standard errors, z-scores, p-values, and confidence intervals
    for the logistic regression coefficients using the observed Fisher information
    matrix.

    Parameters
    ----------
    model : sklearn.linear_model.LogisticRegression
        Trained logistic regression model
    x : pandas.DataFrame or numpy.ndarray
        Feature matrix used for training
    y : pandas.Series or numpy.ndarray
        Target variable used for training (not used in the computation: the Fisher information depends only on the
        predicted probabilities for ``x``)
    feature_names : list of str, optional
        Feature names (inferred from x if not provided: the columns of ``x``, else ``model.feature_names_in_``, else
        ``x0``, ``x1``, ...)

    Returns
    -------
    pandas.DataFrame
        Summary table with columns: ['coef', 'std_err', 'z', 'p_value',
        'ci_lower', 'ci_upper'], indexed by ``'Intercept'`` followed by the feature names. ``ci_lower`` and
        ``ci_upper`` are the 95% Wald bounds ``coef -/+ 1.96 * std_err``.

    Notes
    -----
    The weight matrix is a dense ``n x n`` array (``n`` is the number of rows), so memory use grows with the square of
    the sample size. ``fast_lr_pvalues`` returns the same p-values without that matrix.
    """
    from scipy import stats

    if feature_names is None:
        if hasattr(x, 'columns'):
            feature_names = x.columns.tolist()
        elif hasattr(model, 'feature_names_in_'):
            feature_names = model.feature_names_in_.tolist()
        else:
            feature_names = [f'x{i}' for i in range(x.shape[1])]

    x_arr = x.values if hasattr(x, 'values') else np.array(x)
    y_arr = y.values if hasattr(y, 'values') else np.array(y)

    prob = _predict_positive_proba(model, x_arr)
    w = prob * (1 - prob)
    W = np.diag(w)
    X_design = np.hstack([np.ones((x_arr.shape[0], 1)), x_arr])

    try:
        cov_matrix = np.linalg.inv(X_design.T @ W @ X_design)
    except np.linalg.LinAlgError:
        cov_matrix = np.linalg.pinv(X_design.T @ W @ X_design)

    intercept = model.intercept_[0]
    coefs = model.coef_[0]
    all_coefs = np.concatenate([[intercept], coefs])

    std_errs = np.sqrt(np.diag(cov_matrix))
    z_scores = all_coefs / std_errs
    p_values = 2 * (1 - stats.norm.cdf(np.abs(z_scores)))
    ci_lower = all_coefs - 1.96 * std_errs
    ci_upper = all_coefs + 1.96 * std_errs

    all_names = ['Intercept'] + feature_names

    summary_df = pd.DataFrame({
        'coef': all_coefs,
        'std_err': std_errs,
        'z': z_scores,
        'p_value': p_values,
        'ci_lower': ci_lower,
        'ci_upper': ci_upper
    }, index=all_names)

    return summary_df

compute_aic

compute_aic(model, x, y, sample_weight=None)

Compute AIC (Akaike Information Criterion) for a logistic regression model.

Parameters:

Name Type Description Default
model LogisticRegression

Fitted logistic regression model

required
x DataFrame or ndarray

Feature matrix

required
y Series or ndarray

Target variable

required
sample_weight array - like

Per-sample weights for log-likelihood / information criteria.

None

Returns:

Type Description
float

AIC value (lower is better): 2 * k - 2 * log_likelihood, where k is the number of coefficients plus one for the intercept and the log-likelihood is weighted when sample_weight is given.

Source code in Modeling_Tool/Model/LRM_Tool.py
def compute_aic(model, x, y, sample_weight=None):
    """
    Compute AIC (Akaike Information Criterion) for a logistic regression model.

    Parameters
    ----------
    model : sklearn.linear_model.LogisticRegression
        Fitted logistic regression model
    x : pandas.DataFrame or numpy.ndarray
        Feature matrix
    y : pandas.Series or numpy.ndarray
        Target variable
    sample_weight : array-like, optional
        Per-sample weights for log-likelihood / information criteria.

    Returns
    -------
    float
        AIC value (lower is better): ``2 * k - 2 * log_likelihood``, where ``k`` is the number of coefficients plus one
        for the intercept and the log-likelihood is weighted when ``sample_weight`` is given.
    """
    log_likelihood = _compute_log_likelihood(model, x, y, sample_weight=sample_weight)
    k = model.coef_.shape[1] + 1  # number of params including intercept
    aic = 2 * k - 2 * log_likelihood
    return aic

compute_bic

compute_bic(model, x, y, sample_weight=None)

Compute BIC (Bayesian Information Criterion) for a logistic regression model.

Parameters:

Name Type Description Default
model LogisticRegression

Fitted logistic regression model

required
x DataFrame or ndarray

Feature matrix

required
y Series or ndarray

Target variable

required
sample_weight array - like

Per-sample weights for log-likelihood / information criteria.

None

Returns:

Type Description
float

BIC value (lower is better): k * log(n) - 2 * log_likelihood, where k is the number of coefficients plus one for the intercept. n is the number of rows, or the sum of sample_weight when weights are given.

Source code in Modeling_Tool/Model/LRM_Tool.py
def compute_bic(model, x, y, sample_weight=None):
    """
    Compute BIC (Bayesian Information Criterion) for a logistic regression model.

    Parameters
    ----------
    model : sklearn.linear_model.LogisticRegression
        Fitted logistic regression model
    x : pandas.DataFrame or numpy.ndarray
        Feature matrix
    y : pandas.Series or numpy.ndarray
        Target variable
    sample_weight : array-like, optional
        Per-sample weights for log-likelihood / information criteria.

    Returns
    -------
    float
        BIC value (lower is better): ``k * log(n) - 2 * log_likelihood``, where ``k`` is the number of coefficients plus
        one for the intercept. ``n`` is the number of rows, or the sum of ``sample_weight`` when weights are given.
    """
    x_arr = x.values if hasattr(x, 'values') else np.array(x)
    weight = None if sample_weight is None else np.asarray(sample_weight, dtype=float)
    log_likelihood = _compute_log_likelihood(model, x, y, sample_weight=weight)
    k = model.coef_.shape[1] + 1
    n = float(np.sum(weight)) if weight is not None else x_arr.shape[0]
    bic = k * np.log(n) - 2 * log_likelihood
    return bic

Gradient Boosting Models: GBM_Tool

GBM_Tool

Gradient Boosting Model Training Toolkit

This module provides quick training and evaluation utilities for LightGBM, XGBoost, and CatBoost models, including model training and feature-importance extraction.

Functions:

Name Description
set_num_leaves

Compute the number of leaves from the maximum depth, to avoid overfitting.

lgb_model

Quickly train a LightGBM model.

lgb_varimp

Get LightGBM feature importance.

lgbm_quick_train

Quickly train a LightGBM model (DataFrame interface).

xgb_model

Train an XGBoost model.

xgb_varimp

Get XGBoost feature importance.

xgbm_quick_train

Quickly train an XGBoost model (DataFrame interface).

catboost_model

Train a CatBoost model.

catboost_varimp

Get CatBoost feature importance.

catboost_quick_train

Quickly train a CatBoost model (DataFrame interface).

Classes:

Name Description
LightGBMModel

LightGBM model wrapper class providing a unified training and evaluation interface.

XGBoostModel

XGBoost model wrapper class providing a unified training and evaluation interface.

CatBoostModel

CatBoost model wrapper class providing a unified training and evaluation interface.

GradientBoostingModel

Unified wrapper class that supports switching between LightGBM, XGBoost, and CatBoost.

Examples:

Functional interface

>>> model = lgb_model(x_train, y_train, x_val, y_val, params)
>>> varimp = lgb_varimp(model)

Class-based interface

>>> lgb_model = LightGBMModel(params)
>>> lgb_model.fit(x_train, y_train, x_val, y_val)
>>> varimp = lgb_model.get_feature_importance()

Unified interface

>>> model = GradientBoostingModel('lgb', params)
>>> model.fit(x_train, y_train, x_val, y_val)

LightGBMModel

LightGBM model wrapper class.

Provide a unified interface for training, predicting, saving, and loading LightGBM models. Support model calibration, feature-importance retrieval, and more.

Parameters:

Name Type Description Default
params dict

LightGBM model parameter dictionary.

required
model LGBMClassifier

Preloaded model instance.

None

Attributes:

Name Type Description
params dict

The parameter dictionary given to the constructor, used by fit.

model LGBMClassifier or None

The underlying estimator: the preloaded model, the classifier trained by fit, the object read by load or, after calibrate, the CalibratedClassifierCV that wraps it. None until one of these happens.

feature_names_ list of str or None

Column names of the training features, recorded by fit when x is a DataFrame; None before that.

Examples:

>>> lgb_clf = LightGBMModel(params)
>>> lgb_clf.fit(x_train, y_train, x_val, y_val)
>>> preds = lgb_clf.predict(x_test)
Source code in Modeling_Tool/Model/GBM_Tool.py
class LightGBMModel:
    """
    LightGBM model wrapper class.

    Provide a unified interface for training, predicting, saving, and loading LightGBM models.
    Support model calibration, feature-importance retrieval, and more.

    Parameters
    ----------
    params : dict
        LightGBM model parameter dictionary.
    model : lgb.LGBMClassifier, optional
        Preloaded model instance.

    Attributes
    ----------
    params : dict
        The parameter dictionary given to the constructor, used by ``fit``.
    model : lgb.LGBMClassifier or None
        The underlying estimator: the preloaded ``model``, the classifier trained by ``fit``, the object read by
        ``load`` or, after ``calibrate``, the ``CalibratedClassifierCV`` that wraps it. ``None`` until one of
        these happens.
    feature_names_ : list of str or None
        Column names of the training features, recorded by ``fit`` when ``x`` is a DataFrame; ``None`` before that.

    Examples
    --------
    >>> lgb_clf = LightGBMModel(params)
    >>> lgb_clf.fit(x_train, y_train, x_val, y_val)
    >>> preds = lgb_clf.predict(x_test)
    """

    def __init__(self, params, model=None):
        """
        Initialize the LightGBM model wrapper class.

        Parameters
        ----------
        params : dict
            LightGBM model parameter dictionary.
        model : lgb.LGBMClassifier, optional
            Preloaded model instance.
        """
        lgb = _get_lgb()
        self.params = params
        self.model = model
        self.feature_names_ = None

    def fit(self, x, y, valx, valy, wgt=None, init_score=None, sample_weight=None, eval_sample_weight=None, eval_init_score=None):
        """Train the LightGBM model.

        Train the model on the training and validation sets, with early stopping support.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Training-set features.
        y : array-like
            Training-set labels.
        valx : array-like or pd.DataFrame
            Validation-set features.
        valy : array-like
            Validation-set labels.
        wgt : array-like, optional
            Sample weights of the training set.
        init_score : array-like, optional
            Initial scores (log-odds offset) of the training set. Unless ``eval_init_score`` is given, the validation set
            gets no offset.
        sample_weight : array-like, optional
            Alias of ``wgt`` for the training-set sample weights. It is used only when ``wgt`` is ``None``; ``wgt`` wins
            when both are given.
        eval_sample_weight : array-like, optional
            Sample weights of the validation set. They weight the validation metric that drives early stopping; with
            ``None`` the validation set is unweighted.
        eval_init_score : array-like, optional
            Initial scores (log-odds offset) of the validation set, so that early stopping measures the combined model.

        Returns
        -------
        self
            The fitted wrapper.

        Notes
        -----
        ``params`` must contain ``early_stopping_rounds`` (see ``lgb_model``). Training replaces ``model``; when
        ``x`` has a ``columns`` attribute (a DataFrame) it also sets ``feature_names_``, otherwise ``feature_names_`` is
        left as it was.
        """
        if wgt is None:
            wgt = sample_weight
        self.model = lgb_model(
            x=x, y=y, valx=valx, valy=valy,
            params_dict=self.params, wgt=wgt, init_score=init_score,
            eval_sample_weight=eval_sample_weight, eval_init_score=eval_init_score,
        )
        if hasattr(x, 'columns'):
            self.feature_names_ = list(x.columns)
        return self

    def predict(self, x):
        """Predict the positive-class probability of each sample.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Features to predict on.

        Returns
        -------
        np.ndarray
            Predicted probabilities.
        """
        return self.model.predict_proba(x)[:, 1]

    def get_feature_importance(self, importance_type='gain'):
        """Get the feature importance.

        Parameters
        ----------
        importance_type : str, default 'gain'
            Ignored. It is accepted for API symmetry only: the importance is always the total gain computed by
            ``lgb_varimp``.

        Returns
        -------
        pd.DataFrame
            DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order.
        """
        return lgb_varimp(self.model)

    def save(self, path):
        """Save the model.

        Parameters
        ----------
        path : str
            Path to save the model to. Only the underlying estimator ``model`` is written, with ``joblib.dump``;
            ``params`` and ``feature_names_`` are not stored.
        """
        save_model(self.model, path)

    def load(self, path):
        """Load the model.

        Parameters
        ----------
        path : str
            Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
            from a trusted source.

        Returns
        -------
        self
            The wrapper, with ``model`` replaced by the loaded object. ``params`` and ``feature_names_`` are unchanged.
        """
        self.model = load_model(path)
        return self

    def calibrate(self, x, y, method='sigmoid', cv='prefit'):
        """Calibrate the model probabilities.

        Parameters
        ----------
        x : array-like
            Calibration features.
        y : array-like
            Calibration labels.
        method : str, default 'sigmoid'
            Calibration method, 'sigmoid' or 'isotonic'.
        cv : str or int, default 'prefit'
            Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
            as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
            refitted on each fold without a validation set, which fails while ``early_stopping_rounds`` is set.

        Returns
        -------
        self
            The wrapper, whose ``model`` is now the fitted ``CalibratedClassifierCV``.

        Notes
        -----
        After calibration ``model`` is the ``CalibratedClassifierCV`` and not the LightGBM classifier: ``predict``
        returns calibrated probabilities, but ``get_feature_importance`` no longer works.
        """
        self.model = _calibrated_classifier(self.model, method, cv)
        self.model.fit(x, y)
        return self

    def calibration_curve(self, x, y, n_bins=10):
        """Get the calibration curve data.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.
        n_bins : int, default 10
            Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than ``n_bins``.

        Returns
        -------
        tuple
            (fraction_of_positives, mean_predicted_value)
        """
        y_prob = self.predict(x)
        return calibration_curve(y, y_prob, n_bins=n_bins)

    def brier_score(self, x, y):
        """Compute the Brier score.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            Brier score.
        """
        y_prob = self.predict(x)
        return brier_score_loss(y, y_prob)

    def roc_auc(self, x, y):
        """Compute the ROC AUC.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            ROC AUC score.
        """
        y_prob = self.predict(x)
        return roc_auc_score(y, y_prob)

fit

fit(x, y, valx, valy, wgt=None, init_score=None, sample_weight=None, eval_sample_weight=None, eval_init_score=None)

Train the LightGBM model.

Train the model on the training and validation sets, with early stopping support.

Parameters:

Name Type Description Default
x array - like or DataFrame

Training-set features.

required
y array - like

Training-set labels.

required
valx array - like or DataFrame

Validation-set features.

required
valy array - like

Validation-set labels.

required
wgt array - like

Sample weights of the training set.

None
init_score array - like

Initial scores (log-odds offset) of the training set. Unless eval_init_score is given, the validation set gets no offset.

None
sample_weight array - like

Alias of wgt for the training-set sample weights. It is used only when wgt is None; wgt wins when both are given.

None
eval_sample_weight array - like

Sample weights of the validation set. They weight the validation metric that drives early stopping; with None the validation set is unweighted.

None
eval_init_score array - like

Initial scores (log-odds offset) of the validation set, so that early stopping measures the combined model.

None

Returns:

Type Description
self

The fitted wrapper.

Notes

params must contain early_stopping_rounds (see lgb_model). Training replaces model; when x has a columns attribute (a DataFrame) it also sets feature_names_, otherwise feature_names_ is left as it was.

Source code in Modeling_Tool/Model/GBM_Tool.py
def fit(self, x, y, valx, valy, wgt=None, init_score=None, sample_weight=None, eval_sample_weight=None, eval_init_score=None):
    """Train the LightGBM model.

    Train the model on the training and validation sets, with early stopping support.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Training-set features.
    y : array-like
        Training-set labels.
    valx : array-like or pd.DataFrame
        Validation-set features.
    valy : array-like
        Validation-set labels.
    wgt : array-like, optional
        Sample weights of the training set.
    init_score : array-like, optional
        Initial scores (log-odds offset) of the training set. Unless ``eval_init_score`` is given, the validation set
        gets no offset.
    sample_weight : array-like, optional
        Alias of ``wgt`` for the training-set sample weights. It is used only when ``wgt`` is ``None``; ``wgt`` wins
        when both are given.
    eval_sample_weight : array-like, optional
        Sample weights of the validation set. They weight the validation metric that drives early stopping; with
        ``None`` the validation set is unweighted.
    eval_init_score : array-like, optional
        Initial scores (log-odds offset) of the validation set, so that early stopping measures the combined model.

    Returns
    -------
    self
        The fitted wrapper.

    Notes
    -----
    ``params`` must contain ``early_stopping_rounds`` (see ``lgb_model``). Training replaces ``model``; when
    ``x`` has a ``columns`` attribute (a DataFrame) it also sets ``feature_names_``, otherwise ``feature_names_`` is
    left as it was.
    """
    if wgt is None:
        wgt = sample_weight
    self.model = lgb_model(
        x=x, y=y, valx=valx, valy=valy,
        params_dict=self.params, wgt=wgt, init_score=init_score,
        eval_sample_weight=eval_sample_weight, eval_init_score=eval_init_score,
    )
    if hasattr(x, 'columns'):
        self.feature_names_ = list(x.columns)
    return self

predict

predict(x)

Predict the positive-class probability of each sample.

Parameters:

Name Type Description Default
x array - like or DataFrame

Features to predict on.

required

Returns:

Type Description
ndarray

Predicted probabilities.

Source code in Modeling_Tool/Model/GBM_Tool.py
def predict(self, x):
    """Predict the positive-class probability of each sample.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Features to predict on.

    Returns
    -------
    np.ndarray
        Predicted probabilities.
    """
    return self.model.predict_proba(x)[:, 1]

get_feature_importance

get_feature_importance(importance_type='gain')

Get the feature importance.

Parameters:

Name Type Description Default
importance_type str

Ignored. It is accepted for API symmetry only: the importance is always the total gain computed by lgb_varimp.

'gain'

Returns:

Type Description
DataFrame

DataFrame with feature and importance columns, sorted by importance in descending order.

Source code in Modeling_Tool/Model/GBM_Tool.py
def get_feature_importance(self, importance_type='gain'):
    """Get the feature importance.

    Parameters
    ----------
    importance_type : str, default 'gain'
        Ignored. It is accepted for API symmetry only: the importance is always the total gain computed by
        ``lgb_varimp``.

    Returns
    -------
    pd.DataFrame
        DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order.
    """
    return lgb_varimp(self.model)

save

save(path)

Save the model.

Parameters:

Name Type Description Default
path str

Path to save the model to. Only the underlying estimator model is written, with joblib.dump; params and feature_names_ are not stored.

required
Source code in Modeling_Tool/Model/GBM_Tool.py
def save(self, path):
    """Save the model.

    Parameters
    ----------
    path : str
        Path to save the model to. Only the underlying estimator ``model`` is written, with ``joblib.dump``;
        ``params`` and ``feature_names_`` are not stored.
    """
    save_model(self.model, path)

load

load(path)

Load the model.

Parameters:

Name Type Description Default
path str

Path to the model file, as written by save. The file is read with joblib.load: only load files from a trusted source.

required

Returns:

Type Description
self

The wrapper, with model replaced by the loaded object. params and feature_names_ are unchanged.

Source code in Modeling_Tool/Model/GBM_Tool.py
def load(self, path):
    """Load the model.

    Parameters
    ----------
    path : str
        Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
        from a trusted source.

    Returns
    -------
    self
        The wrapper, with ``model`` replaced by the loaded object. ``params`` and ``feature_names_`` are unchanged.
    """
    self.model = load_model(path)
    return self

calibrate

calibrate(x, y, method='sigmoid', cv='prefit')

Calibrate the model probabilities.

Parameters:

Name Type Description Default
x array - like

Calibration features.

required
y array - like

Calibration labels.

required
method str

Calibration method, 'sigmoid' or 'isotonic'.

'sigmoid'
cv str or int

Cross-validation strategy passed to CalibratedClassifierCV. With 'prefit' the fitted model is kept as it is and only the calibrator is fitted on (x, y). With an integer the estimator is cloned and refitted on each fold without a validation set, which fails while early_stopping_rounds is set.

'prefit'

Returns:

Type Description
self

The wrapper, whose model is now the fitted CalibratedClassifierCV.

Notes

After calibration model is the CalibratedClassifierCV and not the LightGBM classifier: predict returns calibrated probabilities, but get_feature_importance no longer works.

Source code in Modeling_Tool/Model/GBM_Tool.py
def calibrate(self, x, y, method='sigmoid', cv='prefit'):
    """Calibrate the model probabilities.

    Parameters
    ----------
    x : array-like
        Calibration features.
    y : array-like
        Calibration labels.
    method : str, default 'sigmoid'
        Calibration method, 'sigmoid' or 'isotonic'.
    cv : str or int, default 'prefit'
        Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
        as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
        refitted on each fold without a validation set, which fails while ``early_stopping_rounds`` is set.

    Returns
    -------
    self
        The wrapper, whose ``model`` is now the fitted ``CalibratedClassifierCV``.

    Notes
    -----
    After calibration ``model`` is the ``CalibratedClassifierCV`` and not the LightGBM classifier: ``predict``
    returns calibrated probabilities, but ``get_feature_importance`` no longer works.
    """
    self.model = _calibrated_classifier(self.model, method, cv)
    self.model.fit(x, y)
    return self

calibration_curve

calibration_curve(x, y, n_bins=10)

Get the calibration curve data.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required
n_bins int

Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than n_bins.

10

Returns:

Type Description
tuple

(fraction_of_positives, mean_predicted_value)

Source code in Modeling_Tool/Model/GBM_Tool.py
def calibration_curve(self, x, y, n_bins=10):
    """Get the calibration curve data.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.
    n_bins : int, default 10
        Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than ``n_bins``.

    Returns
    -------
    tuple
        (fraction_of_positives, mean_predicted_value)
    """
    y_prob = self.predict(x)
    return calibration_curve(y, y_prob, n_bins=n_bins)

brier_score

brier_score(x, y)

Compute the Brier score.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

Brier score.

Source code in Modeling_Tool/Model/GBM_Tool.py
def brier_score(self, x, y):
    """Compute the Brier score.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        Brier score.
    """
    y_prob = self.predict(x)
    return brier_score_loss(y, y_prob)

roc_auc

roc_auc(x, y)

Compute the ROC AUC.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

ROC AUC score.

Source code in Modeling_Tool/Model/GBM_Tool.py
def roc_auc(self, x, y):
    """Compute the ROC AUC.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        ROC AUC score.
    """
    y_prob = self.predict(x)
    return roc_auc_score(y, y_prob)

XGBoostModel

XGBoost model wrapper class.

Provide a unified interface for training, predicting, saving, and loading XGBoost models. Support model calibration, feature-importance retrieval, and more.

Parameters:

Name Type Description Default
params dict

XGBoost model parameter dictionary.

required
model XGBClassifier

Preloaded model instance.

None

Attributes:

Name Type Description
params dict

The parameter dictionary given to the constructor, used by fit.

model XGBClassifier or None

The underlying estimator: the preloaded model, the classifier trained by fit, the object read by load or, after calibrate, the CalibratedClassifierCV that wraps it. None until one of these happens.

feature_names_ list of str or None

Column names of the training features, recorded by fit when x is a DataFrame; None before that.

Examples:

>>> xgb_clf = XGBoostModel(params)
>>> xgb_clf.fit(x_train, y_train, x_val, y_val)
>>> preds = xgb_clf.predict(x_test)
Source code in Modeling_Tool/Model/GBM_Tool.py
class XGBoostModel:
    """
    XGBoost model wrapper class.

    Provide a unified interface for training, predicting, saving, and loading XGBoost models.
    Support model calibration, feature-importance retrieval, and more.

    Parameters
    ----------
    params : dict
        XGBoost model parameter dictionary.
    model : xgb.XGBClassifier, optional
        Preloaded model instance.

    Attributes
    ----------
    params : dict
        The parameter dictionary given to the constructor, used by ``fit``.
    model : xgb.XGBClassifier or None
        The underlying estimator: the preloaded ``model``, the classifier trained by ``fit``, the object read by
        ``load`` or, after ``calibrate``, the ``CalibratedClassifierCV`` that wraps it. ``None`` until one of
        these happens.
    feature_names_ : list of str or None
        Column names of the training features, recorded by ``fit`` when ``x`` is a DataFrame; ``None`` before that.

    Examples
    --------
    >>> xgb_clf = XGBoostModel(params)
    >>> xgb_clf.fit(x_train, y_train, x_val, y_val)
    >>> preds = xgb_clf.predict(x_test)
    """

    def __init__(self, params, model=None):
        """
        Initialize the XGBoost model wrapper class.

        Parameters
        ----------
        params : dict
            XGBoost model parameter dictionary.
        model : xgb.XGBClassifier, optional
            Preloaded model instance.
        """
        xgb = _get_xgb()
        self.params = params
        self.model = model
        self.feature_names_ = None

    def fit(self, x, y, valx, valy, sample_weight=None, sample_weight_eval_set=None, base_margin=None, eval_base_margin=None):
        """Train the XGBoost model.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Training-set features.
        y : array-like
            Training-set labels.
        valx : array-like or pd.DataFrame
            Validation-set features.
        valy : array-like
            Validation-set labels.
        sample_weight : array-like, optional
            Sample weights of the training set.
        sample_weight_eval_set : list, optional
            List of sample weights for the validation set. It holds one array-like, because the single evaluation set
            is ``(valx, valy)``.
        base_margin : array-like, optional
            Base margin (init_score / log-odds offset) of the training set, used for incremental training (warm start).
            Unless ``eval_base_margin`` is given, no margin is passed for the validation set.
        eval_base_margin : array-like, optional
            Base margin of the validation set, so that early stopping measures the combined model.

        Returns
        -------
        self
            The fitted wrapper.

        Notes
        -----
        The ``eval_metric`` key of ``params`` is dropped (see ``xgb_model``), and early stopping is active only when
        ``params`` contains ``early_stopping_rounds``. Training replaces ``model``; when ``x`` has a ``columns``
        attribute (a DataFrame) it also sets ``feature_names_``, otherwise ``feature_names_`` is left as it was.
        """
        self.model = xgb_model(
            x=x, y=y, valx=valx, valy=valy,
            params_dict=self.params,
            sample_weight=sample_weight,
            sample_weight_eval_set=sample_weight_eval_set,
            base_margin=base_margin,
            eval_base_margin=eval_base_margin,
        )
        if hasattr(x, 'columns'):
            self.feature_names_ = list(x.columns)
        return self

    def predict(self, x):
        """Predict the positive-class probability of each sample.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Features to predict on.

        Returns
        -------
        np.ndarray
            Predicted probabilities.
        """
        return self.model.predict_proba(x)[:, 1]

    def get_feature_importance(self, importance_type='gain'):
        """Get the feature importance.

        Parameters
        ----------
        importance_type : str, default 'gain'
            Ignored. It is accepted for API symmetry only: the importance is always the number of splits that use each
            feature, as computed by ``xgb_varimp``.

        Returns
        -------
        pd.DataFrame
            DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order. Features
            that no tree uses are absent.
        """
        return xgb_varimp(self.model)

    def save(self, path):
        """Save the model.

        Parameters
        ----------
        path : str
            Path to save the model to. Only the underlying estimator ``model`` is written, with ``joblib.dump``;
            ``params`` and ``feature_names_`` are not stored.
        """
        save_model(self.model, path)

    def load(self, path):
        """Load the model.

        Parameters
        ----------
        path : str
            Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
            from a trusted source.

        Returns
        -------
        self
            The wrapper, with ``model`` replaced by the loaded object. ``params`` and ``feature_names_`` are unchanged.
        """
        self.model = load_model(path)
        return self

    def calibrate(self, x, y, method='sigmoid', cv='prefit'):
        """Calibrate the model probabilities.

        Parameters
        ----------
        x : array-like
            Calibration features.
        y : array-like
            Calibration labels.
        method : str, default 'sigmoid'
            Calibration method, 'sigmoid' or 'isotonic'.
        cv : str or int, default 'prefit'
            Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
            as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
            refitted on each fold without a validation set, which fails while ``early_stopping_rounds`` is set.

        Returns
        -------
        self
            The wrapper, whose ``model`` is now the fitted ``CalibratedClassifierCV``.

        Notes
        -----
        After calibration ``model`` is the ``CalibratedClassifierCV`` and not the XGBoost classifier: ``predict``
        returns calibrated probabilities, but ``get_feature_importance`` no longer works.
        """
        self.model = _calibrated_classifier(self.model, method, cv)
        self.model.fit(x, y)
        return self

    def calibration_curve(self, x, y, n_bins=10):
        """Get the calibration curve data.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.
        n_bins : int, default 10
            Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than ``n_bins``.

        Returns
        -------
        tuple
            (fraction_of_positives, mean_predicted_value)
        """
        y_prob = self.predict(x)
        return calibration_curve(y, y_prob, n_bins=n_bins)

    def brier_score(self, x, y):
        """Compute the Brier score.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            Brier score.
        """
        y_prob = self.predict(x)
        return brier_score_loss(y, y_prob)

    def roc_auc(self, x, y):
        """Compute the ROC AUC.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            ROC AUC score.
        """
        y_prob = self.predict(x)
        return roc_auc_score(y, y_prob)

fit

fit(x, y, valx, valy, sample_weight=None, sample_weight_eval_set=None, base_margin=None, eval_base_margin=None)

Train the XGBoost model.

Parameters:

Name Type Description Default
x array - like or DataFrame

Training-set features.

required
y array - like

Training-set labels.

required
valx array - like or DataFrame

Validation-set features.

required
valy array - like

Validation-set labels.

required
sample_weight array - like

Sample weights of the training set.

None
sample_weight_eval_set list

List of sample weights for the validation set. It holds one array-like, because the single evaluation set is (valx, valy).

None
base_margin array - like

Base margin (init_score / log-odds offset) of the training set, used for incremental training (warm start). Unless eval_base_margin is given, no margin is passed for the validation set.

None
eval_base_margin array - like

Base margin of the validation set, so that early stopping measures the combined model.

None

Returns:

Type Description
self

The fitted wrapper.

Notes

The eval_metric key of params is dropped (see xgb_model), and early stopping is active only when params contains early_stopping_rounds. Training replaces model; when x has a columns attribute (a DataFrame) it also sets feature_names_, otherwise feature_names_ is left as it was.

Source code in Modeling_Tool/Model/GBM_Tool.py
def fit(self, x, y, valx, valy, sample_weight=None, sample_weight_eval_set=None, base_margin=None, eval_base_margin=None):
    """Train the XGBoost model.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Training-set features.
    y : array-like
        Training-set labels.
    valx : array-like or pd.DataFrame
        Validation-set features.
    valy : array-like
        Validation-set labels.
    sample_weight : array-like, optional
        Sample weights of the training set.
    sample_weight_eval_set : list, optional
        List of sample weights for the validation set. It holds one array-like, because the single evaluation set
        is ``(valx, valy)``.
    base_margin : array-like, optional
        Base margin (init_score / log-odds offset) of the training set, used for incremental training (warm start).
        Unless ``eval_base_margin`` is given, no margin is passed for the validation set.
    eval_base_margin : array-like, optional
        Base margin of the validation set, so that early stopping measures the combined model.

    Returns
    -------
    self
        The fitted wrapper.

    Notes
    -----
    The ``eval_metric`` key of ``params`` is dropped (see ``xgb_model``), and early stopping is active only when
    ``params`` contains ``early_stopping_rounds``. Training replaces ``model``; when ``x`` has a ``columns``
    attribute (a DataFrame) it also sets ``feature_names_``, otherwise ``feature_names_`` is left as it was.
    """
    self.model = xgb_model(
        x=x, y=y, valx=valx, valy=valy,
        params_dict=self.params,
        sample_weight=sample_weight,
        sample_weight_eval_set=sample_weight_eval_set,
        base_margin=base_margin,
        eval_base_margin=eval_base_margin,
    )
    if hasattr(x, 'columns'):
        self.feature_names_ = list(x.columns)
    return self

predict

predict(x)

Predict the positive-class probability of each sample.

Parameters:

Name Type Description Default
x array - like or DataFrame

Features to predict on.

required

Returns:

Type Description
ndarray

Predicted probabilities.

Source code in Modeling_Tool/Model/GBM_Tool.py
def predict(self, x):
    """Predict the positive-class probability of each sample.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Features to predict on.

    Returns
    -------
    np.ndarray
        Predicted probabilities.
    """
    return self.model.predict_proba(x)[:, 1]

get_feature_importance

get_feature_importance(importance_type='gain')

Get the feature importance.

Parameters:

Name Type Description Default
importance_type str

Ignored. It is accepted for API symmetry only: the importance is always the number of splits that use each feature, as computed by xgb_varimp.

'gain'

Returns:

Type Description
DataFrame

DataFrame with feature and importance columns, sorted by importance in descending order. Features that no tree uses are absent.

Source code in Modeling_Tool/Model/GBM_Tool.py
def get_feature_importance(self, importance_type='gain'):
    """Get the feature importance.

    Parameters
    ----------
    importance_type : str, default 'gain'
        Ignored. It is accepted for API symmetry only: the importance is always the number of splits that use each
        feature, as computed by ``xgb_varimp``.

    Returns
    -------
    pd.DataFrame
        DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order. Features
        that no tree uses are absent.
    """
    return xgb_varimp(self.model)

save

save(path)

Save the model.

Parameters:

Name Type Description Default
path str

Path to save the model to. Only the underlying estimator model is written, with joblib.dump; params and feature_names_ are not stored.

required
Source code in Modeling_Tool/Model/GBM_Tool.py
def save(self, path):
    """Save the model.

    Parameters
    ----------
    path : str
        Path to save the model to. Only the underlying estimator ``model`` is written, with ``joblib.dump``;
        ``params`` and ``feature_names_`` are not stored.
    """
    save_model(self.model, path)

load

load(path)

Load the model.

Parameters:

Name Type Description Default
path str

Path to the model file, as written by save. The file is read with joblib.load: only load files from a trusted source.

required

Returns:

Type Description
self

The wrapper, with model replaced by the loaded object. params and feature_names_ are unchanged.

Source code in Modeling_Tool/Model/GBM_Tool.py
def load(self, path):
    """Load the model.

    Parameters
    ----------
    path : str
        Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
        from a trusted source.

    Returns
    -------
    self
        The wrapper, with ``model`` replaced by the loaded object. ``params`` and ``feature_names_`` are unchanged.
    """
    self.model = load_model(path)
    return self

calibrate

calibrate(x, y, method='sigmoid', cv='prefit')

Calibrate the model probabilities.

Parameters:

Name Type Description Default
x array - like

Calibration features.

required
y array - like

Calibration labels.

required
method str

Calibration method, 'sigmoid' or 'isotonic'.

'sigmoid'
cv str or int

Cross-validation strategy passed to CalibratedClassifierCV. With 'prefit' the fitted model is kept as it is and only the calibrator is fitted on (x, y). With an integer the estimator is cloned and refitted on each fold without a validation set, which fails while early_stopping_rounds is set.

'prefit'

Returns:

Type Description
self

The wrapper, whose model is now the fitted CalibratedClassifierCV.

Notes

After calibration model is the CalibratedClassifierCV and not the XGBoost classifier: predict returns calibrated probabilities, but get_feature_importance no longer works.

Source code in Modeling_Tool/Model/GBM_Tool.py
def calibrate(self, x, y, method='sigmoid', cv='prefit'):
    """Calibrate the model probabilities.

    Parameters
    ----------
    x : array-like
        Calibration features.
    y : array-like
        Calibration labels.
    method : str, default 'sigmoid'
        Calibration method, 'sigmoid' or 'isotonic'.
    cv : str or int, default 'prefit'
        Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
        as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
        refitted on each fold without a validation set, which fails while ``early_stopping_rounds`` is set.

    Returns
    -------
    self
        The wrapper, whose ``model`` is now the fitted ``CalibratedClassifierCV``.

    Notes
    -----
    After calibration ``model`` is the ``CalibratedClassifierCV`` and not the XGBoost classifier: ``predict``
    returns calibrated probabilities, but ``get_feature_importance`` no longer works.
    """
    self.model = _calibrated_classifier(self.model, method, cv)
    self.model.fit(x, y)
    return self

calibration_curve

calibration_curve(x, y, n_bins=10)

Get the calibration curve data.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required
n_bins int

Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than n_bins.

10

Returns:

Type Description
tuple

(fraction_of_positives, mean_predicted_value)

Source code in Modeling_Tool/Model/GBM_Tool.py
def calibration_curve(self, x, y, n_bins=10):
    """Get the calibration curve data.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.
    n_bins : int, default 10
        Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than ``n_bins``.

    Returns
    -------
    tuple
        (fraction_of_positives, mean_predicted_value)
    """
    y_prob = self.predict(x)
    return calibration_curve(y, y_prob, n_bins=n_bins)

brier_score

brier_score(x, y)

Compute the Brier score.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

Brier score.

Source code in Modeling_Tool/Model/GBM_Tool.py
def brier_score(self, x, y):
    """Compute the Brier score.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        Brier score.
    """
    y_prob = self.predict(x)
    return brier_score_loss(y, y_prob)

roc_auc

roc_auc(x, y)

Compute the ROC AUC.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

ROC AUC score.

Source code in Modeling_Tool/Model/GBM_Tool.py
def roc_auc(self, x, y):
    """Compute the ROC AUC.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        ROC AUC score.
    """
    y_prob = self.predict(x)
    return roc_auc_score(y, y_prob)

CatBoostModel

CatBoost model wrapper class.

Provide a unified interface for training, predicting, saving, and loading CatBoost models. Support model calibration, feature-importance retrieval, and more.

Parameters:

Name Type Description Default
params dict

CatBoost model parameter dictionary.

required
model CatBoostClassifier

Preloaded model instance.

None

Attributes:

Name Type Description
params dict

The parameter dictionary given to the constructor, used by fit.

model CatBoostClassifier or None

The underlying estimator: the preloaded model, the classifier trained by fit, the object read by load or, after calibrate, the CalibratedClassifierCV that wraps it. None until one of these happens.

feature_names_ list of str or None

Column names of the training features, recorded by fit when x is a DataFrame; None before that.

Examples:

>>> cat_clf = CatBoostModel(params)
>>> cat_clf.fit(x_train, y_train, x_val, y_val)
>>> preds = cat_clf.predict(x_test)
Source code in Modeling_Tool/Model/GBM_Tool.py
class CatBoostModel:
    """
    CatBoost model wrapper class.

    Provide a unified interface for training, predicting, saving, and loading CatBoost models.
    Support model calibration, feature-importance retrieval, and more.

    Parameters
    ----------
    params : dict
        CatBoost model parameter dictionary.
    model : CatBoostClassifier, optional
        Preloaded model instance.

    Attributes
    ----------
    params : dict
        The parameter dictionary given to the constructor, used by ``fit``.
    model : CatBoostClassifier or None
        The underlying estimator: the preloaded ``model``, the classifier trained by ``fit``, the object read by
        ``load`` or, after ``calibrate``, the ``CalibratedClassifierCV`` that wraps it. ``None`` until one of
        these happens.
    feature_names_ : list of str or None
        Column names of the training features, recorded by ``fit`` when ``x`` is a DataFrame; ``None`` before that.

    Examples
    --------
    >>> cat_clf = CatBoostModel(params)
    >>> cat_clf.fit(x_train, y_train, x_val, y_val)
    >>> preds = cat_clf.predict(x_test)
    """

    def __init__(self, params, model=None):
        """
        Initialize the CatBoost model wrapper class.

        Parameters
        ----------
        params : dict
            CatBoost model parameter dictionary.
        model : CatBoostClassifier, optional
            Preloaded model instance.
        """
        _get_catboost()
        self.params = params
        self.model = model
        self.feature_names_ = None

    def fit(self, x, y, valx, valy, sample_weight=None):
        """Train the CatBoost model.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Training-set features.
        y : array-like
            Training-set labels.
        valx : array-like or pd.DataFrame
            Validation-set features.
        valy : array-like
            Validation-set labels.
        sample_weight : array-like, optional
            Sample weights of the training set. The validation set is never weighted.

        Returns
        -------
        self
            The fitted wrapper.

        Notes
        -----
        ``params`` is interpreted as in ``catboost_model`` (``early_stopping_rounds``, ``eval_metric`` and
        ``cat_features`` are read from it). Training replaces ``model``; when ``x`` has a ``columns`` attribute (a
        DataFrame) it also sets ``feature_names_``, otherwise ``feature_names_`` is left as it was.
        """
        self.model = catboost_model(
            x=x, y=y, valx=valx, valy=valy,
            params_dict=self.params,
            sample_weight=sample_weight,
        )
        if hasattr(x, 'columns'):
            self.feature_names_ = list(x.columns)
        return self

    def predict(self, x):
        """Predict the positive-class probability of each sample.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Features to predict on.

        Returns
        -------
        np.ndarray
            Predicted probabilities.
        """
        return self.model.predict_proba(x)[:, 1]

    def get_feature_importance(self, importance_type='gain'):
        """Get the feature importance.

        Parameters
        ----------
        importance_type : str, default 'gain'
            Ignored. It is accepted for API symmetry only: the importance is always CatBoost's default
            ``PredictionValuesChange``, as computed by ``catboost_varimp``.

        Returns
        -------
        pd.DataFrame
            DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order.
        """
        return catboost_varimp(self.model)

    def save(self, path):
        """Save the model.

        Parameters
        ----------
        path : str
            Path to save the model to. Only the underlying estimator ``model`` is written, with ``joblib.dump``;
            ``params`` and ``feature_names_`` are not stored.
        """
        save_model(self.model, path)

    def load(self, path):
        """Load the model.

        Parameters
        ----------
        path : str
            Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
            from a trusted source.

        Returns
        -------
        self
            The wrapper, with ``model`` replaced by the loaded object. ``params`` and ``feature_names_`` are unchanged.
        """
        self.model = load_model(path)
        return self

    def calibrate(self, x, y, method='sigmoid', cv='prefit'):
        """Calibrate the model probabilities.

        Parameters
        ----------
        x : array-like
            Calibration features.
        y : array-like
            Calibration labels.
        method : str, default 'sigmoid'
            Calibration method, 'sigmoid' or 'isotonic'.
        cv : str or int, default 'prefit'
            Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
            as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
            refitted on each fold.

        Returns
        -------
        self
            The wrapper, whose ``model`` is now the fitted ``CalibratedClassifierCV``.

        Notes
        -----
        After calibration ``model`` is the ``CalibratedClassifierCV`` and not the CatBoost classifier: ``predict``
        returns calibrated probabilities, but ``get_feature_importance`` no longer works.
        """
        self.model = _calibrated_classifier(self.model, method, cv)
        self.model.fit(x, y)
        return self

    def calibration_curve(self, x, y, n_bins=10):
        """Get the calibration curve data.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.
        n_bins : int, default 10
            Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than ``n_bins``.

        Returns
        -------
        tuple
            (fraction_of_positives, mean_predicted_value)
        """
        y_prob = self.predict(x)
        return calibration_curve(y, y_prob, n_bins=n_bins)

    def brier_score(self, x, y):
        """Compute the Brier score.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            Brier score.
        """
        y_prob = self.predict(x)
        return brier_score_loss(y, y_prob)

    def roc_auc(self, x, y):
        """Compute the ROC AUC.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            ROC AUC score.
        """
        y_prob = self.predict(x)
        return roc_auc_score(y, y_prob)

fit

fit(x, y, valx, valy, sample_weight=None)

Train the CatBoost model.

Parameters:

Name Type Description Default
x array - like or DataFrame

Training-set features.

required
y array - like

Training-set labels.

required
valx array - like or DataFrame

Validation-set features.

required
valy array - like

Validation-set labels.

required
sample_weight array - like

Sample weights of the training set. The validation set is never weighted.

None

Returns:

Type Description
self

The fitted wrapper.

Notes

params is interpreted as in catboost_model (early_stopping_rounds, eval_metric and cat_features are read from it). Training replaces model; when x has a columns attribute (a DataFrame) it also sets feature_names_, otherwise feature_names_ is left as it was.

Source code in Modeling_Tool/Model/GBM_Tool.py
def fit(self, x, y, valx, valy, sample_weight=None):
    """Train the CatBoost model.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Training-set features.
    y : array-like
        Training-set labels.
    valx : array-like or pd.DataFrame
        Validation-set features.
    valy : array-like
        Validation-set labels.
    sample_weight : array-like, optional
        Sample weights of the training set. The validation set is never weighted.

    Returns
    -------
    self
        The fitted wrapper.

    Notes
    -----
    ``params`` is interpreted as in ``catboost_model`` (``early_stopping_rounds``, ``eval_metric`` and
    ``cat_features`` are read from it). Training replaces ``model``; when ``x`` has a ``columns`` attribute (a
    DataFrame) it also sets ``feature_names_``, otherwise ``feature_names_`` is left as it was.
    """
    self.model = catboost_model(
        x=x, y=y, valx=valx, valy=valy,
        params_dict=self.params,
        sample_weight=sample_weight,
    )
    if hasattr(x, 'columns'):
        self.feature_names_ = list(x.columns)
    return self

predict

predict(x)

Predict the positive-class probability of each sample.

Parameters:

Name Type Description Default
x array - like or DataFrame

Features to predict on.

required

Returns:

Type Description
ndarray

Predicted probabilities.

Source code in Modeling_Tool/Model/GBM_Tool.py
def predict(self, x):
    """Predict the positive-class probability of each sample.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Features to predict on.

    Returns
    -------
    np.ndarray
        Predicted probabilities.
    """
    return self.model.predict_proba(x)[:, 1]

get_feature_importance

get_feature_importance(importance_type='gain')

Get the feature importance.

Parameters:

Name Type Description Default
importance_type str

Ignored. It is accepted for API symmetry only: the importance is always CatBoost's default PredictionValuesChange, as computed by catboost_varimp.

'gain'

Returns:

Type Description
DataFrame

DataFrame with feature and importance columns, sorted by importance in descending order.

Source code in Modeling_Tool/Model/GBM_Tool.py
def get_feature_importance(self, importance_type='gain'):
    """Get the feature importance.

    Parameters
    ----------
    importance_type : str, default 'gain'
        Ignored. It is accepted for API symmetry only: the importance is always CatBoost's default
        ``PredictionValuesChange``, as computed by ``catboost_varimp``.

    Returns
    -------
    pd.DataFrame
        DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order.
    """
    return catboost_varimp(self.model)

save

save(path)

Save the model.

Parameters:

Name Type Description Default
path str

Path to save the model to. Only the underlying estimator model is written, with joblib.dump; params and feature_names_ are not stored.

required
Source code in Modeling_Tool/Model/GBM_Tool.py
def save(self, path):
    """Save the model.

    Parameters
    ----------
    path : str
        Path to save the model to. Only the underlying estimator ``model`` is written, with ``joblib.dump``;
        ``params`` and ``feature_names_`` are not stored.
    """
    save_model(self.model, path)

load

load(path)

Load the model.

Parameters:

Name Type Description Default
path str

Path to the model file, as written by save. The file is read with joblib.load: only load files from a trusted source.

required

Returns:

Type Description
self

The wrapper, with model replaced by the loaded object. params and feature_names_ are unchanged.

Source code in Modeling_Tool/Model/GBM_Tool.py
def load(self, path):
    """Load the model.

    Parameters
    ----------
    path : str
        Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
        from a trusted source.

    Returns
    -------
    self
        The wrapper, with ``model`` replaced by the loaded object. ``params`` and ``feature_names_`` are unchanged.
    """
    self.model = load_model(path)
    return self

calibrate

calibrate(x, y, method='sigmoid', cv='prefit')

Calibrate the model probabilities.

Parameters:

Name Type Description Default
x array - like

Calibration features.

required
y array - like

Calibration labels.

required
method str

Calibration method, 'sigmoid' or 'isotonic'.

'sigmoid'
cv str or int

Cross-validation strategy passed to CalibratedClassifierCV. With 'prefit' the fitted model is kept as it is and only the calibrator is fitted on (x, y). With an integer the estimator is cloned and refitted on each fold.

'prefit'

Returns:

Type Description
self

The wrapper, whose model is now the fitted CalibratedClassifierCV.

Notes

After calibration model is the CalibratedClassifierCV and not the CatBoost classifier: predict returns calibrated probabilities, but get_feature_importance no longer works.

Source code in Modeling_Tool/Model/GBM_Tool.py
def calibrate(self, x, y, method='sigmoid', cv='prefit'):
    """Calibrate the model probabilities.

    Parameters
    ----------
    x : array-like
        Calibration features.
    y : array-like
        Calibration labels.
    method : str, default 'sigmoid'
        Calibration method, 'sigmoid' or 'isotonic'.
    cv : str or int, default 'prefit'
        Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
        as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
        refitted on each fold.

    Returns
    -------
    self
        The wrapper, whose ``model`` is now the fitted ``CalibratedClassifierCV``.

    Notes
    -----
    After calibration ``model`` is the ``CalibratedClassifierCV`` and not the CatBoost classifier: ``predict``
    returns calibrated probabilities, but ``get_feature_importance`` no longer works.
    """
    self.model = _calibrated_classifier(self.model, method, cv)
    self.model.fit(x, y)
    return self

calibration_curve

calibration_curve(x, y, n_bins=10)

Get the calibration curve data.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required
n_bins int

Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than n_bins.

10

Returns:

Type Description
tuple

(fraction_of_positives, mean_predicted_value)

Source code in Modeling_Tool/Model/GBM_Tool.py
def calibration_curve(self, x, y, n_bins=10):
    """Get the calibration curve data.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.
    n_bins : int, default 10
        Number of bins. Bins without samples are dropped, so the returned arrays can be shorter than ``n_bins``.

    Returns
    -------
    tuple
        (fraction_of_positives, mean_predicted_value)
    """
    y_prob = self.predict(x)
    return calibration_curve(y, y_prob, n_bins=n_bins)

brier_score

brier_score(x, y)

Compute the Brier score.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

Brier score.

Source code in Modeling_Tool/Model/GBM_Tool.py
def brier_score(self, x, y):
    """Compute the Brier score.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        Brier score.
    """
    y_prob = self.predict(x)
    return brier_score_loss(y, y_prob)

roc_auc

roc_auc(x, y)

Compute the ROC AUC.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

ROC AUC score.

Source code in Modeling_Tool/Model/GBM_Tool.py
def roc_auc(self, x, y):
    """Compute the ROC AUC.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        ROC AUC score.
    """
    y_prob = self.predict(x)
    return roc_auc_score(y, y_prob)

GradientBoostingModel

Unified gradient boosting model wrapper class.

Provide one interface for the three frameworks LightGBM, XGBoost, and CatBoost. The framework is selected with the model_type parameter; the rest of the interface stays the same.

Parameters:

Name Type Description Default
model_type str

Model type: 'lgb', 'xgb', or 'cat' ('catboost' is an alias).

required
params dict

Model parameter dictionary.

required

Attributes:

Name Type Description
model_type str

The framework, 'lgb', 'xgb', or 'cat' (the alias 'catboost' is stored as 'cat').

params dict

The parameter dictionary given to the constructor.

Notes

Attributes that the wrapper does not define are delegated to the fitted underlying estimator, so get_params(), predict_proba(x) or feature_names_in_ work on a fitted instance as on the original LightGBM / XGBoost / CatBoost estimator. save and load handle only that estimator, not the wrapper.

Examples:

>>> model = GradientBoostingModel('lgb', params)
>>> model.fit(x_train, y_train, x_val, y_val)
>>> preds = model.predict(x_test)

Incremental learning (warm start)

>>> base_margin = init_model.get_base_margin(x_train)
>>> new_model = GradientBoostingModel('xgb', params)
>>> new_model.fit(x_train, y_train, x_val, y_val, init_score=base_margin)
>>> proba = new_model.predict_with_base_margin(
...     x_score, init_model.get_base_margin(x_score))

Adapt an already fitted bare estimator (e.g. an XGBClassifier that was pickled directly)

>>> init_model = GradientBoostingModel.from_fitted(load_model(path))
>>> init_model.get_base_margin(x_train)
Source code in Modeling_Tool/Model/GBM_Tool.py
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
class GradientBoostingModel:
    """
    Unified gradient boosting model wrapper class.

    Provide one interface for the three frameworks LightGBM, XGBoost, and CatBoost.
    The framework is selected with the ``model_type`` parameter; the rest of the interface stays the same.

    Parameters
    ----------
    model_type : str
        Model type: 'lgb', 'xgb', or 'cat' ('catboost' is an alias).
    params : dict
        Model parameter dictionary.

    Attributes
    ----------
    model_type : str
        The framework, 'lgb', 'xgb', or 'cat' (the alias 'catboost' is stored as 'cat').
    params : dict
        The parameter dictionary given to the constructor.

    Notes
    -----
    Attributes that the wrapper does not define are delegated to the fitted underlying estimator, so ``get_params()``,
    ``predict_proba(x)`` or ``feature_names_in_`` work on a fitted instance as on the original LightGBM / XGBoost /
    CatBoost estimator. ``save`` and ``load`` handle only that estimator, not the wrapper.

    Examples
    --------
    >>> model = GradientBoostingModel('lgb', params)
    >>> model.fit(x_train, y_train, x_val, y_val)
    >>> preds = model.predict(x_test)

    # Incremental learning (warm start)
    >>> base_margin = init_model.get_base_margin(x_train)
    >>> new_model = GradientBoostingModel('xgb', params)
    >>> new_model.fit(x_train, y_train, x_val, y_val, init_score=base_margin)
    >>> proba = new_model.predict_with_base_margin(
    ...     x_score, init_model.get_base_margin(x_score))

    # Adapt an already fitted bare estimator (e.g. an XGBClassifier that was pickled directly)
    >>> init_model = GradientBoostingModel.from_fitted(load_model(path))
    >>> init_model.get_base_margin(x_train)
    """

    def __init__(self, model_type, params):
        """
        Initialize the unified model wrapper class.

        Parameters
        ----------
        model_type : str
            Model type: 'lgb', 'xgb', or 'cat' ('catboost' is an alias).
        params : dict
            Model parameter dictionary.

        Raises
        ------
        ValueError
            If ``model_type`` is not a supported type.
        """
        if model_type == 'catboost':
            model_type = 'cat'
        if model_type not in ('lgb', 'xgb', 'cat'):
            raise ValueError(
                f"model_type must be 'lgb', 'xgb', or 'cat', got: {model_type!r}"
            )
        self.model_type = model_type
        self.params = params
        if model_type == 'lgb':
            self._model = LightGBMModel(params)
        elif model_type == 'xgb':
            self._model = XGBoostModel(params)
        else:
            self._model = CatBoostModel(params)

    @staticmethod
    def _detect_model_type(estimator):
        """Infer 'lgb', 'xgb', or 'cat' from the class name / module of a fitted estimator.

        Parameters
        ----------
        estimator : object
            A fitted XGBClassifier / LGBMClassifier / CatBoostClassifier, etc.

        Returns
        -------
        str
            'lgb', 'xgb', or 'cat'.

        Raises
        ------
        ValueError
            If the type cannot be inferred from the estimator.
        """
        cls = type(estimator)
        tag = f"{cls.__module__}.{cls.__name__}".lower()
        if 'catboost' in tag:
            return 'cat'
        if 'xgboost' in tag or 'xgb' in tag:
            return 'xgb'
        if 'lightgbm' in tag or 'lgb' in tag:
            return 'lgb'
        raise ValueError(
            f"cannot infer model_type from {cls.__name__!r}; "
            "pass model_type='lgb', 'xgb', or 'cat' explicitly"
        )

    @classmethod
    def from_fitted(cls, model, model_type=None, params=None):
        """Build a ``GradientBoostingModel`` from an already fitted estimator (or wrapper).

        Adapt models that were saved historically as bare scikit-learn estimators
        (``XGBClassifier`` / ``LGBMClassifier`` / ``CatBoostClassifier``) so that they can use the
        unified interface, such as :meth:`get_base_margin` / :meth:`predict_with_base_margin`,
        without retraining.

        Parameters
        ----------
        model : object
            A fitted ``XGBClassifier`` / ``LGBMClassifier`` / ``CatBoostClassifier``, or a
            ``LightGBMModel`` / ``XGBoostModel`` / ``CatBoostModel`` / ``GradientBoostingModel`` wrapper.
        model_type : str or None, default None
            One of 'lgb', 'xgb' or 'cat' ('catboost' is accepted as an alias of 'cat'). If omitted, inferred
            automatically from the estimator type. Ignored when ``model`` is already a ``GradientBoostingModel``.
        params : dict, optional
            Parameter dictionary. If omitted, it is read from the estimator's ``get_params()`` where possible (an empty
            dict when the estimator has no ``get_params``). Ignored when ``model`` is already a
            ``GradientBoostingModel``.

        Returns
        -------
        GradientBoostingModel
            A wrapper around the estimator. When ``model`` already is a ``GradientBoostingModel`` it is returned
            unchanged.

        Raises
        ------
        ValueError
            If an unfitted or empty model is passed (a wrapper whose ``model`` attribute is ``None``), if
            ``model_type`` is omitted and cannot be inferred from the estimator, or if a loaded
            ``GradientBoostingModel`` lost its inner model.

        Notes
        -----
        When the feature names can be read from the estimator, they are stored as ``feature_names_`` of the inner
        wrapper, and an estimator that lacks ``feature_names_in_`` gets that attribute set when it allows it (a side
        effect on the estimator that was passed in).
        """
        if isinstance(model, cls):
            try:
                object.__getattribute__(model, '_model')
                return model
            except AttributeError:
                raise ValueError(
                    "Loaded GradientBoostingModel is missing _model (Cython pickle bug "
                    "in a previous package version). Please retrain and re-save the model."
                )
        # Unwrap LightGBMModel / XGBoostModel / CatBoostModel (they keep the bare estimator in .model)
        estimator = getattr(model, 'model', model)
        if estimator is None:
            raise ValueError("from_fitted received an unfitted / empty model")
        mt = model_type or cls._detect_model_type(estimator)
        if mt == 'catboost':
            mt = 'cat'
        if params is None:
            params = estimator.get_params() if hasattr(estimator, 'get_params') else {}
        obj = cls(mt, params)
        obj._model.model = estimator
        feat = _extract_estimator_feature_names(estimator)
        if feat is not None:
            obj._model.feature_names_ = feat
            _ensure_feature_names_in(estimator, feat)
        return obj

    def __getattr__(self, name):
        """Delegate unknown attributes to the underlying fitted estimator.

        Make the wrapper instance a superset of the original LGBM/XGB/CatBoost estimator
        (``get_params`` / ``feature_names_in_`` / ``predict_proba``, etc. are passed straight
        through). Called only when the regular attribute lookup fails; for dunder names and a
        missing ``_model`` (e.g. in the middle of deserialization) it raises AttributeError
        safely, so that pickle / copy are not disturbed.
        """
        if name.startswith('__') and name.endswith('__'):
            raise AttributeError(name)
        if name == '_model':
            raise AttributeError(name)
        try:
            model = object.__getattribute__(self, '_model')
        except AttributeError:
            raise AttributeError(name)
        inner = getattr(model, 'model', None)
        if inner is not None and hasattr(inner, name):
            return getattr(inner, name)
        if name == 'feature_names_in_':
            feat = getattr(model, 'feature_names_', None)
            if feat is None and inner is not None:
                feat = _extract_estimator_feature_names(inner)
            if feat is not None:
                return np.asarray(feat, dtype=object)
        raise AttributeError(name)

    def __getstate__(self):
        return {
            'model_type': self.model_type,
            'params': self.params,
            '_model_params': self._model.params,
            '_model_model': self._model.model,
            '_model_feature_names_': getattr(self._model, 'feature_names_', None),
        }

    def __setstate__(self, state):
        self.model_type = state['model_type']
        self.params = state['params']
        mt = self.model_type
        mp = state.get('_model_params', self.params)
        if mt == 'lgb':
            self._model = LightGBMModel(mp)
        elif mt == 'xgb':
            self._model = XGBoostModel(mp)
        else:
            self._model = CatBoostModel(mp)
        self._model.model = state.get('_model_model')
        self._model.feature_names_ = state.get('_model_feature_names_')

    @staticmethod
    def _sigmoid(z):
        """Numerically stable sigmoid (log-odds to probability)."""
        z = np.clip(np.asarray(z, dtype=float), -709, 709)
        return 1.0 / (1.0 + np.exp(-z))

    def fit(self, x, y, valx, valy, init_score=None, sample_weight=None, eval_sample_weight=None, sample_weight_eval_set=None,
            eval_init_score=None, **kwargs):
        """Train the model (supports incremental learning with a warm start).

        When ``init_score`` is passed, it is used as a log-odds offset and training continues on
        the new data: LightGBM uses ``init_score`` and XGBoost uses ``base_margin`` (the two are
        semantically equivalent; this method exposes both uniformly as ``init_score``).
        CatBoost does not support ``init_score``.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Training-set features.
        y : array-like
            Training-set labels.
        valx : array-like or pd.DataFrame
            Validation-set features.
        valy : array-like
            Validation-set labels.
        init_score : array-like, optional
            Initial log-odds offset (the starting point of incremental learning). Usually produced
            by the base model's :meth:`get_base_margin`. With ``None`` this is ordinary training
            from scratch.
        sample_weight : array-like, optional
            Sample weights of the training set, used by all three frameworks.
        eval_sample_weight : array-like, optional
            Sample weights of the validation set, which weight the validation metric that drives early
            stopping. Used by 'lgb'; for 'xgb' it is passed as ``sample_weight_eval_set=[eval_sample_weight]``
            when ``sample_weight_eval_set`` is ``None``; silently ignored by 'cat'.
        sample_weight_eval_set : list, optional
            XGBoost only: list of sample weights for the validation set (one array-like, because there is one
            validation set). It takes precedence over ``eval_sample_weight``. Silently ignored by 'lgb' and 'cat'.
        eval_init_score : array-like, optional
            Log-odds offset of the validation set (LightGBM ``eval_init_score``, XGBoost ``base_margin_eval_set``). With
            it, early stopping measures the combined model (offset plus trees), which is what ``init_score`` makes the
            training fit. ``None`` (default) keeps the legacy behavior in which the validation set gets no offset.
            CatBoost raises ``NotImplementedError``.
        **kwargs
            Remaining keyword arguments are passed through to the ``fit`` of the underlying wrapper
            (``LightGBMModel`` / ``XGBoostModel`` / ``CatBoostModel``). In practice only ``wgt`` (for 'lgb', an
            alias of ``sample_weight`` that takes precedence over it) is accepted; any other name raises
            ``TypeError``, and 'xgb' and 'cat' accept no extra keyword at all.

        Returns
        -------
        self
            The fitted wrapper.

        Raises
        ------
        NotImplementedError
            CatBoost does not support incremental learning with ``init_score``.

        Notes
        -----
        By default, as in the existing production workflow, the offset is applied to the training set only.
        The validation set receives no offset, so the early-stopping eval metric is evaluated
        in the space "without the offset". Pass ``eval_init_score`` (the offset of the validation rows) to
        evaluate the combined model instead.
        """
        if self.model_type == 'cat':
            if init_score is not None or eval_init_score is not None:
                raise NotImplementedError(
                    "CatBoost does not support init_score in GradientBoostingModel.fit"
                )
            self._model.fit(x, y, valx, valy, sample_weight=sample_weight, **kwargs)
        elif self.model_type == 'lgb':
            self._model.fit(
                x, y, valx, valy,
                init_score=init_score,
                sample_weight=sample_weight,
                eval_sample_weight=eval_sample_weight,
                eval_init_score=eval_init_score,
                **kwargs,
            )
        else:
            if sample_weight_eval_set is None and eval_sample_weight is not None:
                sample_weight_eval_set = [eval_sample_weight]
            self._model.fit(
                x, y, valx, valy,
                base_margin=init_score,
                eval_base_margin=eval_init_score,
                sample_weight=sample_weight,
                sample_weight_eval_set=sample_weight_eval_set,
                **kwargs,
            )
        return self

    def get_base_margin(self, x):
        """Return this model's raw log-odds for ``x`` (base margin / init score).

        Obtain the "raw score before the sigmoid" uniformly across the three frameworks:

        - XGBoost: ``predict(x, output_margin=True)``
        - LightGBM: ``predict(x, raw_score=True)``
        - CatBoost: ``predict(x, prediction_type='RawFormulaVal')``

        The result can be used as the ``init_score`` of the next incremental model's :meth:`fit`,
        or fed to :meth:`predict_with_base_margin` for fused prediction.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Features to compute the margin for.

        Returns
        -------
        np.ndarray
            One-dimensional log-odds array of shape ``(n_samples,)``.

        Raises
        ------
        RuntimeError
            If the model has not been trained yet (before ``fit``).
        """
        est = self._model.model
        if est is None:
            raise RuntimeError("model is not fitted yet; call fit() first")
        if self.model_type == 'lgb':
            margin = est.predict(x, raw_score=True)
        elif self.model_type == 'cat':
            margin = est.predict(x, prediction_type='RawFormulaVal')
        else:
            margin = est.predict(x, output_margin=True)
        return np.asarray(margin).ravel()

    def predict_with_base_margin(self, x, base_margin, return_prob=True):
        """Fused prediction: ``sigmoid(base_margin + this model's raw score)``.

        Add the log-odds of a base model (``base_margin``, usually from
        ``init_model.get_base_margin(x)``) to this (incremental) model's own raw score in
        log-odds space, then apply the sigmoid. This manual fusion is the only approach that
        behaves identically for lgb and xgb, because LightGBM does not support injecting an
        init_score at prediction time.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Features to predict on.
        base_margin : array-like
            Log-odds offset of the base model; its length must match the number of samples in ``x``.
        return_prob : bool, default True
            If ``True``, return probabilities (after the sigmoid); if ``False``, return the fused
            raw log-odds.

        Returns
        -------
        np.ndarray
            One-dimensional array; with ``return_prob=True`` the values lie in ``[0, 1]``.
        """
        combined = np.asarray(base_margin).ravel() + self.get_base_margin(x)
        return self._sigmoid(combined) if return_prob else combined

    def predict(self, x):
        """Predict the positive-class probability of each sample.

        Parameters
        ----------
        x : array-like or pd.DataFrame
            Features to predict on.

        Returns
        -------
        np.ndarray
            Predicted probabilities of the positive class.
        """
        return self._model.predict(x)

    def get_feature_importance(self, importance_type='gain'):
        """Get the feature importance.

        Parameters
        ----------
        importance_type : str, default 'gain'
            Ignored. It is accepted for API symmetry only: each framework returns one fixed kind of importance, the
            total gain for 'lgb', the number of splits that use the feature for 'xgb' and CatBoost's default
            ``PredictionValuesChange`` for 'cat'.

        Returns
        -------
        pd.DataFrame
            DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order. For 'xgb'
            features that no tree uses are absent.
        """
        return self._model.get_feature_importance(importance_type=importance_type)

    def save(self, path):
        """Save the model.

        Parameters
        ----------
        path : str
            Path to save the model to. Only the fitted underlying estimator is written, with ``joblib.dump``; the
            wrapper itself, ``model_type`` and ``params`` are not stored.
        """
        self._model.save(path)

    def load(self, path):
        """Load the model.

        Parameters
        ----------
        path : str
            Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
            from a trusted source.

        Returns
        -------
        self
            The wrapper, with its underlying estimator replaced by the loaded object. ``model_type`` and ``params`` are
            not changed, so load a file saved from a model of the same framework.
        """
        self._model.load(path)
        return self

    def calibrate(self, x, y, method='sigmoid', cv='prefit'):
        """Calibrate the model probabilities.

        Parameters
        ----------
        x : array-like
            Calibration features.
        y : array-like
            Calibration labels.
        method : str, default 'sigmoid'
            Calibration method, 'sigmoid' or 'isotonic'.
        cv : str or int, default 'prefit'
            Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
            as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
            refitted on each fold without a validation set, which fails for 'lgb' and 'xgb' while
            ``early_stopping_rounds`` is set.

        Returns
        -------
        self
            The wrapper, whose underlying estimator is now the fitted ``CalibratedClassifierCV``.

        Notes
        -----
        After calibration the underlying estimator is the ``CalibratedClassifierCV`` and not the boosting classifier:
        ``predict`` returns calibrated probabilities, but ``get_feature_importance``, ``get_base_margin``
        and ``predict_with_base_margin`` no longer work.
        """
        self._model.calibrate(x, y, method=method, cv=cv)
        return self

    def brier_score(self, x, y):
        """Compute the Brier score.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            Brier score of the predicted positive-class probabilities.
        """
        return self._model.brier_score(x, y)

    def roc_auc(self, x, y):
        """Compute the ROC AUC.

        Parameters
        ----------
        x : array-like
            Features.
        y : array-like
            Labels.

        Returns
        -------
        float
            ROC AUC score of the predicted positive-class probabilities.
        """
        return self._model.roc_auc(x, y)

from_fitted classmethod

from_fitted(model, model_type=None, params=None)

Build a GradientBoostingModel from an already fitted estimator (or wrapper).

Adapt models that were saved historically as bare scikit-learn estimators (XGBClassifier / LGBMClassifier / CatBoostClassifier) so that they can use the unified interface, such as :meth:get_base_margin / :meth:predict_with_base_margin, without retraining.

Parameters:

Name Type Description Default
model object

A fitted XGBClassifier / LGBMClassifier / CatBoostClassifier, or a LightGBMModel / XGBoostModel / CatBoostModel / GradientBoostingModel wrapper.

required
model_type str or None

One of 'lgb', 'xgb' or 'cat' ('catboost' is accepted as an alias of 'cat'). If omitted, inferred automatically from the estimator type. Ignored when model is already a GradientBoostingModel.

None
params dict

Parameter dictionary. If omitted, it is read from the estimator's get_params() where possible (an empty dict when the estimator has no get_params). Ignored when model is already a GradientBoostingModel.

None

Returns:

Type Description
GradientBoostingModel

A wrapper around the estimator. When model already is a GradientBoostingModel it is returned unchanged.

Raises:

Type Description
ValueError

If an unfitted or empty model is passed (a wrapper whose model attribute is None), if model_type is omitted and cannot be inferred from the estimator, or if a loaded GradientBoostingModel lost its inner model.

Notes

When the feature names can be read from the estimator, they are stored as feature_names_ of the inner wrapper, and an estimator that lacks feature_names_in_ gets that attribute set when it allows it (a side effect on the estimator that was passed in).

Source code in Modeling_Tool/Model/GBM_Tool.py
@classmethod
def from_fitted(cls, model, model_type=None, params=None):
    """Build a ``GradientBoostingModel`` from an already fitted estimator (or wrapper).

    Adapt models that were saved historically as bare scikit-learn estimators
    (``XGBClassifier`` / ``LGBMClassifier`` / ``CatBoostClassifier``) so that they can use the
    unified interface, such as :meth:`get_base_margin` / :meth:`predict_with_base_margin`,
    without retraining.

    Parameters
    ----------
    model : object
        A fitted ``XGBClassifier`` / ``LGBMClassifier`` / ``CatBoostClassifier``, or a
        ``LightGBMModel`` / ``XGBoostModel`` / ``CatBoostModel`` / ``GradientBoostingModel`` wrapper.
    model_type : str or None, default None
        One of 'lgb', 'xgb' or 'cat' ('catboost' is accepted as an alias of 'cat'). If omitted, inferred
        automatically from the estimator type. Ignored when ``model`` is already a ``GradientBoostingModel``.
    params : dict, optional
        Parameter dictionary. If omitted, it is read from the estimator's ``get_params()`` where possible (an empty
        dict when the estimator has no ``get_params``). Ignored when ``model`` is already a
        ``GradientBoostingModel``.

    Returns
    -------
    GradientBoostingModel
        A wrapper around the estimator. When ``model`` already is a ``GradientBoostingModel`` it is returned
        unchanged.

    Raises
    ------
    ValueError
        If an unfitted or empty model is passed (a wrapper whose ``model`` attribute is ``None``), if
        ``model_type`` is omitted and cannot be inferred from the estimator, or if a loaded
        ``GradientBoostingModel`` lost its inner model.

    Notes
    -----
    When the feature names can be read from the estimator, they are stored as ``feature_names_`` of the inner
    wrapper, and an estimator that lacks ``feature_names_in_`` gets that attribute set when it allows it (a side
    effect on the estimator that was passed in).
    """
    if isinstance(model, cls):
        try:
            object.__getattribute__(model, '_model')
            return model
        except AttributeError:
            raise ValueError(
                "Loaded GradientBoostingModel is missing _model (Cython pickle bug "
                "in a previous package version). Please retrain and re-save the model."
            )
    # Unwrap LightGBMModel / XGBoostModel / CatBoostModel (they keep the bare estimator in .model)
    estimator = getattr(model, 'model', model)
    if estimator is None:
        raise ValueError("from_fitted received an unfitted / empty model")
    mt = model_type or cls._detect_model_type(estimator)
    if mt == 'catboost':
        mt = 'cat'
    if params is None:
        params = estimator.get_params() if hasattr(estimator, 'get_params') else {}
    obj = cls(mt, params)
    obj._model.model = estimator
    feat = _extract_estimator_feature_names(estimator)
    if feat is not None:
        obj._model.feature_names_ = feat
        _ensure_feature_names_in(estimator, feat)
    return obj

fit

fit(x, y, valx, valy, init_score=None, sample_weight=None, eval_sample_weight=None, sample_weight_eval_set=None, eval_init_score=None, **kwargs)

Train the model (supports incremental learning with a warm start).

When init_score is passed, it is used as a log-odds offset and training continues on the new data: LightGBM uses init_score and XGBoost uses base_margin (the two are semantically equivalent; this method exposes both uniformly as init_score). CatBoost does not support init_score.

Parameters:

Name Type Description Default
x array - like or DataFrame

Training-set features.

required
y array - like

Training-set labels.

required
valx array - like or DataFrame

Validation-set features.

required
valy array - like

Validation-set labels.

required
init_score array - like

Initial log-odds offset (the starting point of incremental learning). Usually produced by the base model's :meth:get_base_margin. With None this is ordinary training from scratch.

None
sample_weight array - like

Sample weights of the training set, used by all three frameworks.

None
eval_sample_weight array - like

Sample weights of the validation set, which weight the validation metric that drives early stopping. Used by 'lgb'; for 'xgb' it is passed as sample_weight_eval_set=[eval_sample_weight] when sample_weight_eval_set is None; silently ignored by 'cat'.

None
sample_weight_eval_set list

XGBoost only: list of sample weights for the validation set (one array-like, because there is one validation set). It takes precedence over eval_sample_weight. Silently ignored by 'lgb' and 'cat'.

None
eval_init_score array - like

Log-odds offset of the validation set (LightGBM eval_init_score, XGBoost base_margin_eval_set). With it, early stopping measures the combined model (offset plus trees), which is what init_score makes the training fit. None (default) keeps the legacy behavior in which the validation set gets no offset. CatBoost raises NotImplementedError.

None
**kwargs

Remaining keyword arguments are passed through to the fit of the underlying wrapper (LightGBMModel / XGBoostModel / CatBoostModel). In practice only wgt (for 'lgb', an alias of sample_weight that takes precedence over it) is accepted; any other name raises TypeError, and 'xgb' and 'cat' accept no extra keyword at all.

{}

Returns:

Type Description
self

The fitted wrapper.

Raises:

Type Description
NotImplementedError

CatBoost does not support incremental learning with init_score.

Notes

By default, as in the existing production workflow, the offset is applied to the training set only. The validation set receives no offset, so the early-stopping eval metric is evaluated in the space "without the offset". Pass eval_init_score (the offset of the validation rows) to evaluate the combined model instead.

Source code in Modeling_Tool/Model/GBM_Tool.py
def fit(self, x, y, valx, valy, init_score=None, sample_weight=None, eval_sample_weight=None, sample_weight_eval_set=None,
        eval_init_score=None, **kwargs):
    """Train the model (supports incremental learning with a warm start).

    When ``init_score`` is passed, it is used as a log-odds offset and training continues on
    the new data: LightGBM uses ``init_score`` and XGBoost uses ``base_margin`` (the two are
    semantically equivalent; this method exposes both uniformly as ``init_score``).
    CatBoost does not support ``init_score``.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Training-set features.
    y : array-like
        Training-set labels.
    valx : array-like or pd.DataFrame
        Validation-set features.
    valy : array-like
        Validation-set labels.
    init_score : array-like, optional
        Initial log-odds offset (the starting point of incremental learning). Usually produced
        by the base model's :meth:`get_base_margin`. With ``None`` this is ordinary training
        from scratch.
    sample_weight : array-like, optional
        Sample weights of the training set, used by all three frameworks.
    eval_sample_weight : array-like, optional
        Sample weights of the validation set, which weight the validation metric that drives early
        stopping. Used by 'lgb'; for 'xgb' it is passed as ``sample_weight_eval_set=[eval_sample_weight]``
        when ``sample_weight_eval_set`` is ``None``; silently ignored by 'cat'.
    sample_weight_eval_set : list, optional
        XGBoost only: list of sample weights for the validation set (one array-like, because there is one
        validation set). It takes precedence over ``eval_sample_weight``. Silently ignored by 'lgb' and 'cat'.
    eval_init_score : array-like, optional
        Log-odds offset of the validation set (LightGBM ``eval_init_score``, XGBoost ``base_margin_eval_set``). With
        it, early stopping measures the combined model (offset plus trees), which is what ``init_score`` makes the
        training fit. ``None`` (default) keeps the legacy behavior in which the validation set gets no offset.
        CatBoost raises ``NotImplementedError``.
    **kwargs
        Remaining keyword arguments are passed through to the ``fit`` of the underlying wrapper
        (``LightGBMModel`` / ``XGBoostModel`` / ``CatBoostModel``). In practice only ``wgt`` (for 'lgb', an
        alias of ``sample_weight`` that takes precedence over it) is accepted; any other name raises
        ``TypeError``, and 'xgb' and 'cat' accept no extra keyword at all.

    Returns
    -------
    self
        The fitted wrapper.

    Raises
    ------
    NotImplementedError
        CatBoost does not support incremental learning with ``init_score``.

    Notes
    -----
    By default, as in the existing production workflow, the offset is applied to the training set only.
    The validation set receives no offset, so the early-stopping eval metric is evaluated
    in the space "without the offset". Pass ``eval_init_score`` (the offset of the validation rows) to
    evaluate the combined model instead.
    """
    if self.model_type == 'cat':
        if init_score is not None or eval_init_score is not None:
            raise NotImplementedError(
                "CatBoost does not support init_score in GradientBoostingModel.fit"
            )
        self._model.fit(x, y, valx, valy, sample_weight=sample_weight, **kwargs)
    elif self.model_type == 'lgb':
        self._model.fit(
            x, y, valx, valy,
            init_score=init_score,
            sample_weight=sample_weight,
            eval_sample_weight=eval_sample_weight,
            eval_init_score=eval_init_score,
            **kwargs,
        )
    else:
        if sample_weight_eval_set is None and eval_sample_weight is not None:
            sample_weight_eval_set = [eval_sample_weight]
        self._model.fit(
            x, y, valx, valy,
            base_margin=init_score,
            eval_base_margin=eval_init_score,
            sample_weight=sample_weight,
            sample_weight_eval_set=sample_weight_eval_set,
            **kwargs,
        )
    return self

get_base_margin

get_base_margin(x)

Return this model's raw log-odds for x (base margin / init score).

Obtain the "raw score before the sigmoid" uniformly across the three frameworks:

  • XGBoost: predict(x, output_margin=True)
  • LightGBM: predict(x, raw_score=True)
  • CatBoost: predict(x, prediction_type='RawFormulaVal')

The result can be used as the init_score of the next incremental model's :meth:fit, or fed to :meth:predict_with_base_margin for fused prediction.

Parameters:

Name Type Description Default
x array - like or DataFrame

Features to compute the margin for.

required

Returns:

Type Description
ndarray

One-dimensional log-odds array of shape (n_samples,).

Raises:

Type Description
RuntimeError

If the model has not been trained yet (before fit).

Source code in Modeling_Tool/Model/GBM_Tool.py
def get_base_margin(self, x):
    """Return this model's raw log-odds for ``x`` (base margin / init score).

    Obtain the "raw score before the sigmoid" uniformly across the three frameworks:

    - XGBoost: ``predict(x, output_margin=True)``
    - LightGBM: ``predict(x, raw_score=True)``
    - CatBoost: ``predict(x, prediction_type='RawFormulaVal')``

    The result can be used as the ``init_score`` of the next incremental model's :meth:`fit`,
    or fed to :meth:`predict_with_base_margin` for fused prediction.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Features to compute the margin for.

    Returns
    -------
    np.ndarray
        One-dimensional log-odds array of shape ``(n_samples,)``.

    Raises
    ------
    RuntimeError
        If the model has not been trained yet (before ``fit``).
    """
    est = self._model.model
    if est is None:
        raise RuntimeError("model is not fitted yet; call fit() first")
    if self.model_type == 'lgb':
        margin = est.predict(x, raw_score=True)
    elif self.model_type == 'cat':
        margin = est.predict(x, prediction_type='RawFormulaVal')
    else:
        margin = est.predict(x, output_margin=True)
    return np.asarray(margin).ravel()

predict_with_base_margin

predict_with_base_margin(x, base_margin, return_prob=True)

Fused prediction: sigmoid(base_margin + this model's raw score).

Add the log-odds of a base model (base_margin, usually from init_model.get_base_margin(x)) to this (incremental) model's own raw score in log-odds space, then apply the sigmoid. This manual fusion is the only approach that behaves identically for lgb and xgb, because LightGBM does not support injecting an init_score at prediction time.

Parameters:

Name Type Description Default
x array - like or DataFrame

Features to predict on.

required
base_margin array - like

Log-odds offset of the base model; its length must match the number of samples in x.

required
return_prob bool

If True, return probabilities (after the sigmoid); if False, return the fused raw log-odds.

True

Returns:

Type Description
ndarray

One-dimensional array; with return_prob=True the values lie in [0, 1].

Source code in Modeling_Tool/Model/GBM_Tool.py
def predict_with_base_margin(self, x, base_margin, return_prob=True):
    """Fused prediction: ``sigmoid(base_margin + this model's raw score)``.

    Add the log-odds of a base model (``base_margin``, usually from
    ``init_model.get_base_margin(x)``) to this (incremental) model's own raw score in
    log-odds space, then apply the sigmoid. This manual fusion is the only approach that
    behaves identically for lgb and xgb, because LightGBM does not support injecting an
    init_score at prediction time.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Features to predict on.
    base_margin : array-like
        Log-odds offset of the base model; its length must match the number of samples in ``x``.
    return_prob : bool, default True
        If ``True``, return probabilities (after the sigmoid); if ``False``, return the fused
        raw log-odds.

    Returns
    -------
    np.ndarray
        One-dimensional array; with ``return_prob=True`` the values lie in ``[0, 1]``.
    """
    combined = np.asarray(base_margin).ravel() + self.get_base_margin(x)
    return self._sigmoid(combined) if return_prob else combined

predict

predict(x)

Predict the positive-class probability of each sample.

Parameters:

Name Type Description Default
x array - like or DataFrame

Features to predict on.

required

Returns:

Type Description
ndarray

Predicted probabilities of the positive class.

Source code in Modeling_Tool/Model/GBM_Tool.py
def predict(self, x):
    """Predict the positive-class probability of each sample.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Features to predict on.

    Returns
    -------
    np.ndarray
        Predicted probabilities of the positive class.
    """
    return self._model.predict(x)

get_feature_importance

get_feature_importance(importance_type='gain')

Get the feature importance.

Parameters:

Name Type Description Default
importance_type str

Ignored. It is accepted for API symmetry only: each framework returns one fixed kind of importance, the total gain for 'lgb', the number of splits that use the feature for 'xgb' and CatBoost's default PredictionValuesChange for 'cat'.

'gain'

Returns:

Type Description
DataFrame

DataFrame with feature and importance columns, sorted by importance in descending order. For 'xgb' features that no tree uses are absent.

Source code in Modeling_Tool/Model/GBM_Tool.py
def get_feature_importance(self, importance_type='gain'):
    """Get the feature importance.

    Parameters
    ----------
    importance_type : str, default 'gain'
        Ignored. It is accepted for API symmetry only: each framework returns one fixed kind of importance, the
        total gain for 'lgb', the number of splits that use the feature for 'xgb' and CatBoost's default
        ``PredictionValuesChange`` for 'cat'.

    Returns
    -------
    pd.DataFrame
        DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order. For 'xgb'
        features that no tree uses are absent.
    """
    return self._model.get_feature_importance(importance_type=importance_type)

save

save(path)

Save the model.

Parameters:

Name Type Description Default
path str

Path to save the model to. Only the fitted underlying estimator is written, with joblib.dump; the wrapper itself, model_type and params are not stored.

required
Source code in Modeling_Tool/Model/GBM_Tool.py
def save(self, path):
    """Save the model.

    Parameters
    ----------
    path : str
        Path to save the model to. Only the fitted underlying estimator is written, with ``joblib.dump``; the
        wrapper itself, ``model_type`` and ``params`` are not stored.
    """
    self._model.save(path)

load

load(path)

Load the model.

Parameters:

Name Type Description Default
path str

Path to the model file, as written by save. The file is read with joblib.load: only load files from a trusted source.

required

Returns:

Type Description
self

The wrapper, with its underlying estimator replaced by the loaded object. model_type and params are not changed, so load a file saved from a model of the same framework.

Source code in Modeling_Tool/Model/GBM_Tool.py
def load(self, path):
    """Load the model.

    Parameters
    ----------
    path : str
        Path to the model file, as written by ``save``. The file is read with ``joblib.load``: only load files
        from a trusted source.

    Returns
    -------
    self
        The wrapper, with its underlying estimator replaced by the loaded object. ``model_type`` and ``params`` are
        not changed, so load a file saved from a model of the same framework.
    """
    self._model.load(path)
    return self

calibrate

calibrate(x, y, method='sigmoid', cv='prefit')

Calibrate the model probabilities.

Parameters:

Name Type Description Default
x array - like

Calibration features.

required
y array - like

Calibration labels.

required
method str

Calibration method, 'sigmoid' or 'isotonic'.

'sigmoid'
cv str or int

Cross-validation strategy passed to CalibratedClassifierCV. With 'prefit' the fitted model is kept as it is and only the calibrator is fitted on (x, y). With an integer the estimator is cloned and refitted on each fold without a validation set, which fails for 'lgb' and 'xgb' while early_stopping_rounds is set.

'prefit'

Returns:

Type Description
self

The wrapper, whose underlying estimator is now the fitted CalibratedClassifierCV.

Notes

After calibration the underlying estimator is the CalibratedClassifierCV and not the boosting classifier: predict returns calibrated probabilities, but get_feature_importance, get_base_margin and predict_with_base_margin no longer work.

Source code in Modeling_Tool/Model/GBM_Tool.py
def calibrate(self, x, y, method='sigmoid', cv='prefit'):
    """Calibrate the model probabilities.

    Parameters
    ----------
    x : array-like
        Calibration features.
    y : array-like
        Calibration labels.
    method : str, default 'sigmoid'
        Calibration method, 'sigmoid' or 'isotonic'.
    cv : str or int, default 'prefit'
        Cross-validation strategy passed to ``CalibratedClassifierCV``. With ``'prefit'`` the fitted model is kept
        as it is and only the calibrator is fitted on ``(x, y)``. With an integer the estimator is cloned and
        refitted on each fold without a validation set, which fails for 'lgb' and 'xgb' while
        ``early_stopping_rounds`` is set.

    Returns
    -------
    self
        The wrapper, whose underlying estimator is now the fitted ``CalibratedClassifierCV``.

    Notes
    -----
    After calibration the underlying estimator is the ``CalibratedClassifierCV`` and not the boosting classifier:
    ``predict`` returns calibrated probabilities, but ``get_feature_importance``, ``get_base_margin``
    and ``predict_with_base_margin`` no longer work.
    """
    self._model.calibrate(x, y, method=method, cv=cv)
    return self

brier_score

brier_score(x, y)

Compute the Brier score.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

Brier score of the predicted positive-class probabilities.

Source code in Modeling_Tool/Model/GBM_Tool.py
def brier_score(self, x, y):
    """Compute the Brier score.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        Brier score of the predicted positive-class probabilities.
    """
    return self._model.brier_score(x, y)

roc_auc

roc_auc(x, y)

Compute the ROC AUC.

Parameters:

Name Type Description Default
x array - like

Features.

required
y array - like

Labels.

required

Returns:

Type Description
float

ROC AUC score of the predicted positive-class probabilities.

Source code in Modeling_Tool/Model/GBM_Tool.py
def roc_auc(self, x, y):
    """Compute the ROC AUC.

    Parameters
    ----------
    x : array-like
        Features.
    y : array-like
        Labels.

    Returns
    -------
    float
        ROC AUC score of the predicted positive-class probabilities.
    """
    return self._model.roc_auc(x, y)

set_num_leaves

set_num_leaves(max_depth=5, wgt=1)

Set the number of leaves from the maximum depth, to avoid overfitting.

Compute a suitable number of leaves from the given maximum depth and weight coefficient. Formula: 2^max_depth - 2^max_depth * wgt

Parameters:

Name Type Description Default
max_depth int

Maximum depth of the tree.

5
wgt float

Weight coefficient, in the range [0, 1]. It is the fraction of the 2 ** max_depth leaves of a full tree that is removed: wgt=0 keeps all of them and the default wgt=1 gives 0 leaves. Values outside [0, 1] are not rejected.

1

Returns:

Type Description
int

Suggested number of leaves (the formula result truncated to an integer).

Examples:

>>> set_num_leaves(max_depth=5, wgt=0.5)
16
>>> set_num_leaves(max_depth=5)  # default wgt=1: 2**5 - 2**5 * 1
0
Source code in Modeling_Tool/Model/GBM_Tool.py
def set_num_leaves(max_depth=5, wgt=1):
    """Set the number of leaves from the maximum depth, to avoid overfitting.

    Compute a suitable number of leaves from the given maximum depth and weight coefficient.
    Formula: 2^max_depth - 2^max_depth * wgt

    Parameters
    ----------
    max_depth : int, default 5
        Maximum depth of the tree.
    wgt : float, default 1
        Weight coefficient, in the range [0, 1]. It is the fraction of the ``2 ** max_depth`` leaves of a full tree
        that is removed: ``wgt=0`` keeps all of them and the default ``wgt=1`` gives 0 leaves. Values outside [0, 1]
        are not rejected.

    Returns
    -------
    int
        Suggested number of leaves (the formula result truncated to an integer).

    Examples
    --------
    >>> set_num_leaves(max_depth=5, wgt=0.5)
    16
    >>> set_num_leaves(max_depth=5)  # default wgt=1: 2**5 - 2**5 * 1
    0
    """
    return int(2 ** max_depth - 2 ** max_depth * wgt)

lgb_model

lgb_model(x, y, valx, valy, params_dict, wgt=None, init_score=None, eval_sample_weight=None, eval_init_score=None)

Quickly train a LightGBM model.

Train a LightGBM model on the training and validation sets, with early stopping support.

Parameters:

Name Type Description Default
x array - like or DataFrame

Training-set features.

required
y array - like

Training-set labels.

required
valx array - like or DataFrame

Validation-set features.

required
valy array - like

Validation-set labels.

required
params_dict dict

LightGBM parameter dictionary, passed to LGBMClassifier except for the eval_metric key. It must contain early_stopping_rounds (a missing key raises KeyError). The evaluation metric given to fit, which early stopping monitors, is params_dict['eval_metric'] if present, else params_dict['metric'], else 'auc'.

required
wgt array - like

Sample weights of the training set.

None
init_score array - like

Initial scores (log-odds offset) of the training set. Unless eval_init_score is given, the validation set gets no offset.

None
eval_sample_weight array - like

Sample weights of the validation set. They weight the validation metric that drives early stopping; with None the validation set is unweighted.

None
eval_init_score array - like

Initial scores (log-odds offset) of the validation set. With it, early stopping measures the combined model (offset plus trees) instead of the trees alone. None keeps the legacy behavior.

None

Returns:

Type Description
LGBMClassifier

The trained LightGBM model.

Examples:

>>> params = {
...     'n_estimators': 100,
...     'max_depth': 5,
...     'learning_rate': 0.1,
...     'early_stopping_rounds': 20,
...     'eval_metric': 'auc'
... }
>>> model = lgb_model(x_train, y_train, x_val, y_val, params)
Source code in Modeling_Tool/Model/GBM_Tool.py
def lgb_model(x, y, valx, valy, params_dict, wgt=None, init_score=None, eval_sample_weight=None, eval_init_score=None):
    """Quickly train a LightGBM model.

    Train a LightGBM model on the training and validation sets, with early stopping support.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Training-set features.
    y : array-like
        Training-set labels.
    valx : array-like or pd.DataFrame
        Validation-set features.
    valy : array-like
        Validation-set labels.
    params_dict : dict
        LightGBM parameter dictionary, passed to ``LGBMClassifier`` except for the ``eval_metric`` key. It must contain
        ``early_stopping_rounds`` (a missing key raises ``KeyError``). The evaluation metric given to ``fit``, which
        early stopping monitors, is ``params_dict['eval_metric']`` if present, else ``params_dict['metric']``, else
        ``'auc'``.
    wgt : array-like, optional
        Sample weights of the training set.
    init_score : array-like, optional
        Initial scores (log-odds offset) of the training set. Unless ``eval_init_score`` is given, the validation set
        gets no offset.
    eval_sample_weight : array-like, optional
        Sample weights of the validation set. They weight the validation metric that drives early stopping; with
        ``None`` the validation set is unweighted.
    eval_init_score : array-like, optional
        Initial scores (log-odds offset) of the validation set. With it, early stopping measures the combined model
        (offset plus trees) instead of the trees alone. ``None`` keeps the legacy behavior.

    Returns
    -------
    lgb.LGBMClassifier
        The trained LightGBM model.

    Examples
    --------
    >>> params = {
    ...     'n_estimators': 100,
    ...     'max_depth': 5,
    ...     'learning_rate': 0.1,
    ...     'early_stopping_rounds': 20,
    ...     'eval_metric': 'auc'
    ... }
    >>> model = lgb_model(x_train, y_train, x_val, y_val, params)
    """
    lgb = _get_lgb()

    lgb_params = {k: v for k, v in params_dict.items() if k != 'eval_metric'}
    model = lgb.LGBMClassifier(**lgb_params)
    # NOTE: `verbose=False` was removed from .fit() in lightgbm>=4. Log control
    # is now done via callbacks only (log_evaluation).
    model.fit(
        x, y,
        eval_set=[(valx, valy)],
        eval_metric=params_dict['eval_metric'] if 'eval_metric' in params_dict else params_dict.get('metric', 'auc'),
        callbacks=[
            lgb.early_stopping(stopping_rounds=params_dict['early_stopping_rounds'], verbose=False),
            lgb.log_evaluation(period=0),
        ],
        sample_weight=wgt,
        eval_sample_weight=[eval_sample_weight] if eval_sample_weight is not None else None,
        init_score=init_score,
        eval_init_score=[eval_init_score] if eval_init_score is not None else None,
    )
    return model

lgb_varimp

lgb_varimp(model)

Get the feature importance of a LightGBM model.

Return a DataFrame sorted by feature importance.

Parameters:

Name Type Description Default
model LGBMClassifier

The trained LightGBM model.

required

Returns:

Type Description
DataFrame

DataFrame with feature and importance columns, sorted by importance in descending order. The importance is the total gain of the splits that use the feature.

Examples:

>>> varimp = lgb_varimp(model)
>>> varimp.head(10)
Source code in Modeling_Tool/Model/GBM_Tool.py
def lgb_varimp(model):
    """Get the feature importance of a LightGBM model.

    Return a DataFrame sorted by feature importance.

    Parameters
    ----------
    model : lgb.LGBMClassifier
        The trained LightGBM model.

    Returns
    -------
    pd.DataFrame
        DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order. The importance
        is the total gain of the splits that use the feature.

    Examples
    --------
    >>> varimp = lgb_varimp(model)
    >>> varimp.head(10)
    """
    feature_names = model.booster_.feature_name()
    importance = model.booster_.feature_importance(importance_type='gain')
    varimp_df = pd.DataFrame({'feature': feature_names, 'importance': importance})
    varimp_df = varimp_df.sort_values('importance', ascending=False).reset_index(drop=True)
    return varimp_df

lgbm_quick_train

lgbm_quick_train(train_data, validation_data, x, y, params, wgt_col=None, val_wgt_col=None, cat_x_train=None)

Quickly train a LightGBM model (DataFrame interface).

Accept DataFrames for the training and validation sets and extract the features and labels automatically.

Parameters:

Name Type Description Default
train_data DataFrame

Training dataset.

required
validation_data DataFrame

Validation dataset.

required
x list of str

List of feature column names.

required
y str

Name of the target column.

required
params dict

LightGBM parameter dictionary, passed to lgb_model; it must contain early_stopping_rounds.

required
wgt_col str

Name of the sample-weight column of train_data. With None the training set is unweighted.

None
val_wgt_col str

Name of the sample-weight column of validation_data. The weights apply to the validation metric that drives early stopping; with None the validation set is unweighted.

None
cat_x_train list of str

List of categorical feature column names. Accepted for backward compatibility but currently ignored: the value is never used, so no categorical features are declared.

None

Returns:

Type Description
LGBMClassifier

The trained LightGBM model.

Examples:

>>> model = lgbm_quick_train(
...     train_data=train_df,
...     validation_data=val_df,
...     x=['feat1', 'feat2'],
...     y='target',
...     params=params_dict
... )
Source code in Modeling_Tool/Model/GBM_Tool.py
def lgbm_quick_train(train_data, validation_data, x, y, params, wgt_col = None, val_wgt_col = None, cat_x_train=None):
    """Quickly train a LightGBM model (DataFrame interface).

    Accept DataFrames for the training and validation sets and extract the features and labels automatically.

    Parameters
    ----------
    train_data : pd.DataFrame
        Training dataset.
    validation_data : pd.DataFrame
        Validation dataset.
    x : list of str
        List of feature column names.
    y : str
        Name of the target column.
    params : dict
        LightGBM parameter dictionary, passed to ``lgb_model``; it must contain ``early_stopping_rounds``.
    wgt_col : str, optional
        Name of the sample-weight column of ``train_data``. With ``None`` the training set is unweighted.
    val_wgt_col : str, optional
        Name of the sample-weight column of ``validation_data``. The weights apply to the validation metric that drives
        early stopping; with ``None`` the validation set is unweighted.
    cat_x_train : list of str, optional
        List of categorical feature column names. Accepted for backward compatibility but currently ignored: the
        value is never used, so no categorical features are declared.

    Returns
    -------
    lgb.LGBMClassifier
        The trained LightGBM model.

    Examples
    --------
    >>> model = lgbm_quick_train(
    ...     train_data=train_df,
    ...     validation_data=val_df,
    ...     x=['feat1', 'feat2'],
    ...     y='target',
    ...     params=params_dict
    ... )
    """
    lgb = _get_lgb()

    wgt = train_data[wgt_col] if wgt_col is not None else None
    eval_wgt = validation_data[val_wgt_col] if val_wgt_col is not None else None
    model = lgb_model(
        x=train_data[x],
        y=train_data[y],
        valx=validation_data[x],
        valy=validation_data[y],
        params_dict=params,
        wgt=wgt,
        eval_sample_weight=eval_wgt
    )
    return model

xgb_model

xgb_model(x, y, valx, valy, params_dict, sample_weight=None, sample_weight_eval_set=None, base_margin=None, eval_base_margin=None)

Train an XGBoost model.

Train an XGBoost model on the training and validation sets, with early stopping support.

Parameters:

Name Type Description Default
x array - like or DataFrame

Training-set features.

required
y array - like

Training-set labels.

required
valx array - like or DataFrame

Validation-set features.

required
valy array - like

Validation-set labels.

required
params_dict dict

XGBoost parameter dictionary, passed to XGBClassifier except for the eval_metric key, which is dropped. Early stopping is active only when early_stopping_rounds is in this dictionary.

required
sample_weight array - like

Sample weights for the training set.

None
sample_weight_eval_set list

List of sample weights for the validation set. It holds one array-like, because the single evaluation set is (valx, valy).

None
base_margin array - like

Base margin (initial prediction offset) of the training set. Unless eval_base_margin is given, no margin is passed for the validation set.

None
eval_base_margin array - like

Base margin of the validation set, so that early stopping measures the combined model (offset plus trees). None keeps the legacy behavior.

None

Returns:

Type Description
XGBClassifier

The trained XGBoost model.

Notes

The eval_metric key is removed from params_dict before the classifier is built, so a metric given there has no effect: early stopping uses XGBoost's own default metric for the objective (logloss for the default binary objective). The model is fitted with verbose=False.

Examples:

>>> params = {
...     'n_estimators': 100,
...     'max_depth': 5,
...     'learning_rate': 0.1,
...     'early_stopping_rounds': 20,
...     'eval_metric': 'auc'  # dropped: has no effect on XGBoost
... }
>>> model = xgb_model(x_train, y_train, x_val, y_val, params)
Source code in Modeling_Tool/Model/GBM_Tool.py
def xgb_model(x, y, valx, valy, params_dict, sample_weight=None, sample_weight_eval_set=None, base_margin=None,
              eval_base_margin=None):
    """Train an XGBoost model.

    Train an XGBoost model on the training and validation sets, with early stopping support.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Training-set features.
    y : array-like
        Training-set labels.
    valx : array-like or pd.DataFrame
        Validation-set features.
    valy : array-like
        Validation-set labels.
    params_dict : dict
        XGBoost parameter dictionary, passed to ``XGBClassifier`` except for the ``eval_metric`` key, which is dropped.
        Early stopping is active only when ``early_stopping_rounds`` is in this dictionary.
    sample_weight : array-like, optional
        Sample weights for the training set.
    sample_weight_eval_set : list, optional
        List of sample weights for the validation set. It holds one array-like, because the single evaluation set is
        ``(valx, valy)``.
    base_margin : array-like, optional
        Base margin (initial prediction offset) of the training set. Unless ``eval_base_margin`` is given, no margin is
        passed for the validation set.
    eval_base_margin : array-like, optional
        Base margin of the validation set, so that early stopping measures the combined model (offset plus trees).
        ``None`` keeps the legacy behavior.

    Returns
    -------
    xgb.XGBClassifier
        The trained XGBoost model.

    Notes
    -----
    The ``eval_metric`` key is removed from ``params_dict`` before the classifier is built, so a metric given there has
    no effect: early stopping uses XGBoost's own default metric for the objective (``logloss`` for the default binary
    objective). The model is fitted with ``verbose=False``.

    Examples
    --------
    >>> params = {
    ...     'n_estimators': 100,
    ...     'max_depth': 5,
    ...     'learning_rate': 0.1,
    ...     'early_stopping_rounds': 20,
    ...     'eval_metric': 'auc'  # dropped: has no effect on XGBoost
    ... }
    >>> model = xgb_model(x_train, y_train, x_val, y_val, params)
    """
    xgb = _get_xgb()

    xgb_params = {k: v for k, v in params_dict.items() if k not in ('eval_metric',)}
    model = xgb.XGBClassifier(**xgb_params)
    model.fit(
        x, y,
        eval_set=[(valx, valy)],
        verbose=False,
        sample_weight=sample_weight,
        sample_weight_eval_set=sample_weight_eval_set,
        base_margin=base_margin,
        base_margin_eval_set=[eval_base_margin] if eval_base_margin is not None else None,
    )
    return model

xgb_varimp

xgb_varimp(model)

Get the feature importance of an XGBoost model.

Return a DataFrame sorted by feature importance.

Parameters:

Name Type Description Default
model XGBClassifier

The trained XGBoost model.

required

Returns:

Type Description
DataFrame

DataFrame with feature and importance columns, sorted by importance in descending order. The importance is the number of splits that use the feature (get_fscore), so features that no tree uses are absent.

Examples:

>>> varimp = xgb_varimp(model)
>>> varimp.head(10)
Source code in Modeling_Tool/Model/GBM_Tool.py
def xgb_varimp(model):
    """Get the feature importance of an XGBoost model.

    Return a DataFrame sorted by feature importance.

    Parameters
    ----------
    model : xgb.XGBClassifier
        The trained XGBoost model.

    Returns
    -------
    pd.DataFrame
        DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order. The importance
        is the number of splits that use the feature (``get_fscore``), so features that no tree uses are absent.

    Examples
    --------
    >>> varimp = xgb_varimp(model)
    >>> varimp.head(10)
    """
    importance = model.get_booster().get_fscore()
    varimp_df = pd.DataFrame(
        list(importance.items()),
        columns=['feature', 'importance']
    ).sort_values('importance', ascending=False).reset_index(drop=True)
    return varimp_df

xgbm_quick_train

xgbm_quick_train(train_data, validation_data, x, y, wgt_col=None, params=None, sample_weight_eval_set=None, val_wgt_col=None)

Quickly train an XGBoost model (DataFrame interface).

Accept DataFrames for the training and validation sets and extract the features and labels automatically.

Parameters:

Name Type Description Default
train_data DataFrame

Training dataset.

required
validation_data DataFrame

Validation dataset.

required
x list of str

List of feature column names.

required
y str

Name of the target column.

required
wgt_col str

Name of the sample-weight column of train_data. With None the training set is unweighted.

None
params dict

XGBoost parameter dictionary, passed to xgb_model. Despite the default None it must be supplied: None raises AttributeError.

None
sample_weight_eval_set list

List of sample weights for the validation set. When given, it takes precedence over val_wgt_col.

None
val_wgt_col str

Name of the sample-weight column of validation_data. Used only when sample_weight_eval_set is None; the weights are then passed as [validation_data[val_wgt_col]].

None

Returns:

Type Description
XGBClassifier

The trained XGBoost model.

Examples:

>>> model = xgbm_quick_train(
...     train_data=train_df,
...     validation_data=val_df,
...     x=['feat1', 'feat2'],
...     y='target',
...     wgt_col='weight',
...     params=params_dict
... )
Source code in Modeling_Tool/Model/GBM_Tool.py
def xgbm_quick_train(train_data, validation_data, x, y, wgt_col=None, params=None,
                     sample_weight_eval_set=None, val_wgt_col=None):
    """Quickly train an XGBoost model (DataFrame interface).

    Accept DataFrames for the training and validation sets and extract the features and labels automatically.

    Parameters
    ----------
    train_data : pd.DataFrame
        Training dataset.
    validation_data : pd.DataFrame
        Validation dataset.
    x : list of str
        List of feature column names.
    y : str
        Name of the target column.
    wgt_col : str, optional
        Name of the sample-weight column of ``train_data``. With ``None`` the training set is unweighted.
    params : dict, optional
        XGBoost parameter dictionary, passed to ``xgb_model``. Despite the default ``None`` it must be supplied:
        ``None`` raises ``AttributeError``.
    sample_weight_eval_set : list, optional
        List of sample weights for the validation set. When given, it takes precedence over ``val_wgt_col``.
    val_wgt_col : str, optional
        Name of the sample-weight column of ``validation_data``. Used only when ``sample_weight_eval_set`` is ``None``;
        the weights are then passed as ``[validation_data[val_wgt_col]]``.

    Returns
    -------
    xgb.XGBClassifier
        The trained XGBoost model.

    Examples
    --------
    >>> model = xgbm_quick_train(
    ...     train_data=train_df,
    ...     validation_data=val_df,
    ...     x=['feat1', 'feat2'],
    ...     y='target',
    ...     wgt_col='weight',
    ...     params=params_dict
    ... )
    """
    xgb = _get_xgb()

    wgt = train_data[wgt_col] if wgt_col is not None else None
    if sample_weight_eval_set is None and val_wgt_col is not None:
        sample_weight_eval_set = [validation_data[val_wgt_col]]
    model = xgb_model(
        x=train_data[x],
        y=train_data[y],
        valx=validation_data[x],
        valy=validation_data[y],
        params_dict=params,
        sample_weight=wgt,
        sample_weight_eval_set=sample_weight_eval_set
    )
    return model

catboost_model

catboost_model(x, y, valx, valy, params_dict, sample_weight=None)

Train a CatBoost model.

Train a CatBoost model on the training and validation sets, with early stopping support.

Parameters:

Name Type Description Default
x array - like or DataFrame

Training-set features.

required
y array - like

Training-set labels.

required
valx array - like or DataFrame

Validation-set features.

required
valy array - like

Validation-set labels.

required
params_dict dict

CatBoost parameter dictionary (the aliases n_estimators / max_depth / random_state are supported). The keys early_stopping_rounds and cat_features are passed to fit (early stopping is active only when early_stopping_rounds is present). The evaluation metric is eval_metric, else metric, else 'AUC'.

required
sample_weight array - like

Sample weights for the training set. The validation set is never weighted.

None

Returns:

Type Description
CatBoostClassifier

The trained CatBoost model.

Notes

params_dict itself is not modified. When absent, loss_function='Logloss' and verbose=False are added to the CatBoost parameters, and so is allow_writing_files=False unless train_dir is given, so no catboost_info folder is written by default.

Examples:

>>> params = {
...     'n_estimators': 100,
...     'max_depth': 5,
...     'learning_rate': 0.1,
...     'early_stopping_rounds': 20,
...     'eval_metric': 'AUC'
... }
>>> model = catboost_model(x_train, y_train, x_val, y_val, params)
Source code in Modeling_Tool/Model/GBM_Tool.py
def catboost_model(x, y, valx, valy, params_dict, sample_weight=None):
    """Train a CatBoost model.

    Train a CatBoost model on the training and validation sets, with early stopping support.

    Parameters
    ----------
    x : array-like or pd.DataFrame
        Training-set features.
    y : array-like
        Training-set labels.
    valx : array-like or pd.DataFrame
        Validation-set features.
    valy : array-like
        Validation-set labels.
    params_dict : dict
        CatBoost parameter dictionary (the aliases n_estimators / max_depth / random_state are supported). The keys
        ``early_stopping_rounds`` and ``cat_features`` are passed to ``fit`` (early stopping is active only when
        ``early_stopping_rounds`` is present). The evaluation metric is ``eval_metric``, else ``metric``, else
        ``'AUC'``.
    sample_weight : array-like, optional
        Sample weights for the training set. The validation set is never weighted.

    Returns
    -------
    CatBoostClassifier
        The trained CatBoost model.

    Notes
    -----
    ``params_dict`` itself is not modified. When absent, ``loss_function='Logloss'`` and ``verbose=False`` are added to
    the CatBoost parameters, and so is ``allow_writing_files=False`` unless ``train_dir`` is given, so no
    ``catboost_info`` folder is written by default.

    Examples
    --------
    >>> params = {
    ...     'n_estimators': 100,
    ...     'max_depth': 5,
    ...     'learning_rate': 0.1,
    ...     'early_stopping_rounds': 20,
    ...     'eval_metric': 'AUC'
    ... }
    >>> model = catboost_model(x_train, y_train, x_val, y_val, params)
    """
    CatBoostClassifier = _get_catboost()
    cb_params, early_stopping_rounds, eval_metric, cat_features = (
        _normalize_catboost_params(params_dict)
    )
    # eval_metric is folded into cb_params by _normalize_catboost_params because
    # CatBoost only accepts it as a constructor argument, not in fit().
    model = CatBoostClassifier(**cb_params)
    fit_kwargs = {
        'eval_set': (valx, valy),
        'verbose': cb_params.get('verbose', False),
    }
    if early_stopping_rounds is not None:
        fit_kwargs['early_stopping_rounds'] = early_stopping_rounds
    if cat_features is not None:
        fit_kwargs['cat_features'] = cat_features
    if sample_weight is not None:
        fit_kwargs['sample_weight'] = sample_weight
    model.fit(x, y, **fit_kwargs)
    return model

catboost_varimp

catboost_varimp(model)

Get the feature importance of a CatBoost model.

Return a DataFrame sorted by feature importance.

Parameters:

Name Type Description Default
model CatBoostClassifier

The trained CatBoost model.

required

Returns:

Type Description
DataFrame

DataFrame with feature and importance columns, sorted by importance in descending order. The importance is CatBoost's default PredictionValuesChange. When the feature names cannot be read from the model, the column indices are used as names.

Examples:

>>> varimp = catboost_varimp(model)
>>> varimp.head(10)
Source code in Modeling_Tool/Model/GBM_Tool.py
def catboost_varimp(model):
    """Get the feature importance of a CatBoost model.

    Return a DataFrame sorted by feature importance.

    Parameters
    ----------
    model : CatBoostClassifier
        The trained CatBoost model.

    Returns
    -------
    pd.DataFrame
        DataFrame with ``feature`` and ``importance`` columns, sorted by importance in descending order. The importance
        is CatBoost's default ``PredictionValuesChange``. When the feature names cannot be read from the model, the
        column indices are used as names.

    Examples
    --------
    >>> varimp = catboost_varimp(model)
    >>> varimp.head(10)
    """
    feature_names = _extract_estimator_feature_names(model)
    if feature_names is None:
        feature_names = list(range(model.feature_count_))
    importance = model.get_feature_importance()
    varimp_df = pd.DataFrame({'feature': feature_names, 'importance': importance})
    varimp_df = varimp_df.sort_values('importance', ascending=False).reset_index(drop=True)
    return varimp_df

catboost_quick_train

catboost_quick_train(train_data, validation_data, x, y, params, wgt_col=None, val_wgt_col=None, cat_features=None)

Quickly train a CatBoost model (DataFrame interface).

Accept DataFrames for the training and validation sets and extract the features and labels automatically.

Parameters:

Name Type Description Default
train_data DataFrame

Training dataset.

required
validation_data DataFrame

Validation dataset.

required
x list of str

List of feature column names.

required
y str

Name of the target column.

required
params dict

CatBoost parameter dictionary, passed to catboost_model (the dictionary itself is not modified).

required
wgt_col str

Name of the sample-weight column of train_data. With None the training set is unweighted.

None
val_wgt_col str

Name of the validation-set sample-weight column. Accepted for symmetry with lgbm_quick_train and xgbm_quick_train but ignored: the validation set is never weighted for CatBoost.

None
cat_features list

List of categorical feature column names or indices. It is stored as cat_features in the CatBoost parameters (replacing a cat_features key already present in params) and passed to fit.

None

Returns:

Type Description
CatBoostClassifier

The trained CatBoost model.

Examples:

>>> model = catboost_quick_train(
...     train_data=train_df,
...     validation_data=val_df,
...     x=['feat1', 'feat2'],
...     y='target',
...     params=params_dict
... )
Source code in Modeling_Tool/Model/GBM_Tool.py
def catboost_quick_train(train_data, validation_data, x, y, params, wgt_col=None,
                         val_wgt_col=None, cat_features=None):
    """Quickly train a CatBoost model (DataFrame interface).

    Accept DataFrames for the training and validation sets and extract the features and labels automatically.

    Parameters
    ----------
    train_data : pd.DataFrame
        Training dataset.
    validation_data : pd.DataFrame
        Validation dataset.
    x : list of str
        List of feature column names.
    y : str
        Name of the target column.
    params : dict
        CatBoost parameter dictionary, passed to ``catboost_model`` (the dictionary itself is not modified).
    wgt_col : str, optional
        Name of the sample-weight column of ``train_data``. With ``None`` the training set is unweighted.
    val_wgt_col : str, optional
        Name of the validation-set sample-weight column. Accepted for symmetry with ``lgbm_quick_train`` and
        ``xgbm_quick_train`` but ignored: the validation set is never weighted for CatBoost.
    cat_features : list, optional
        List of categorical feature column names or indices. It is stored as ``cat_features`` in the CatBoost parameters
        (replacing a ``cat_features`` key already present in ``params``) and passed to ``fit``.

    Returns
    -------
    CatBoostClassifier
        The trained CatBoost model.

    Examples
    --------
    >>> model = catboost_quick_train(
    ...     train_data=train_df,
    ...     validation_data=val_df,
    ...     x=['feat1', 'feat2'],
    ...     y='target',
    ...     params=params_dict
    ... )
    """
    _get_catboost()

    params_dict = dict(params)
    if cat_features is not None:
        params_dict['cat_features'] = cat_features
    wgt = train_data[wgt_col] if wgt_col is not None else None
    model = catboost_model(
        x=train_data[x],
        y=train_data[y],
        valx=validation_data[x],
        valy=validation_data[y],
        params_dict=params_dict,
        sample_weight=wgt,
    )
    return model

GBM_Search_Tool attaches param_search to GradientBoostingModel when Modeling_Tool.Model is imported, so the generated reference above does not list it. It runs a grid or Optuna search over holdout datasets, stores the winning parameters in best_params_ and the full table in search_results_, and refits the model when refit=True. The signature, without self:

param_search(data, varlist, tgt_name, eval_sets, search_space, engine='grid', objective='oot_gap_penalized',
             primary_set=None, gap_ref_sets=None, metric='auc', validation_set=None, n_trials=50, refit=True,
             verbose=True, fit_kwargs=None, random_state=None, weight_col=None, eval_weight_col=None)

See GBM Hyperparameter Search for the arguments and examples. engine='optuna' needs the optuna extra.

Backward Variable Elimination: Backward_Tool

Backward_Tool

Backward Variable Elimination Toolkit (Unified Version)

This module provides backward variable elimination based on LightGBM and XGBoost. Variables are screened with a cumulative feature-importance threshold, and post-training performance analysis is supported.

Functions:

Name Description
backward_lgbm

Run backward variable elimination with a LightGBM model

backward_xgbm

Run backward variable elimination with an XGBoost model

Classes:

Name Description
BackwardVariableEliminator

Backward variable eliminator supporting LightGBM and XGBoost

BackwardEliminationAnalyzer

Analyzer for backward elimination results

BackwardVariableEliminator

Backward variable eliminator for LightGBM and XGBoost.

Wrap the LightGBM/XGBoost backward variable elimination workflow, with support for multiple elimination rounds and result summaries.

Parameters:

Name Type Description Default
train_data DataFrame

Training dataset.

required
varlist list of str

Initial list of feature variables.

required
dep str

Name of the target column.

required
model_type str

Model type, either "lgbm" (LightGBM) or "xgbm" (XGBoost), compared case-insensitively. Any other value is not rejected: it silently runs XGBoost.

"lgbm"
validation_data DataFrame

Validation dataset.

None
test_data_dict dict

Dictionary of test datasets.

None
weight_col str

Name of the sample-weight column, used for training and for the weighted performance summaries (see backward_lgbm).

None
validation_weight_col str

Name of the sample-weight column of validation_data; weight_col is used when it is None.

None
wgt_col str

Alias of weight_col, used only when weight_col is None.

None

Attributes:

Name Type Description
train_data DataFrame

The training dataset.

varlist list of str

The initial list of feature variables.

dep str

Name of the target column.

model_type str

The model type, converted to lower case.

validation_data DataFrame or None

The validation dataset.

test_data_dict dict

The test datasets (an empty dict when none were given).

weight_col str or None

The sample-weight column (weight_col, or wgt_col when weight_col is None).

validation_weight_col str or None

The sample-weight column of the validation dataset.

Examples:

>>> eliminator = BackwardVariableEliminator(
...     train_data=train_df,
...     varlist=feature_cols,
...     dep='target',
...     model_type='lgbm',
...     validation_data=val_df
... )
>>> results = eliminator.run(n_rounds=5)
Source code in Modeling_Tool/Model/Backward_Tool.py
class BackwardVariableEliminator:
    """
    Backward variable eliminator for LightGBM and XGBoost.

    Wrap the LightGBM/XGBoost backward variable elimination workflow,
    with support for multiple elimination rounds and result summaries.

    Parameters
    ----------
    train_data : pd.DataFrame
        Training dataset.
    varlist : list of str
        Initial list of feature variables.
    dep : str
        Name of the target column.
    model_type : str, default "lgbm"
        Model type, either "lgbm" (LightGBM) or "xgbm" (XGBoost), compared case-insensitively. Any other value is not
        rejected: it silently runs XGBoost.
    validation_data : pd.DataFrame, optional
        Validation dataset.
    test_data_dict : dict, optional
        Dictionary of test datasets.
    weight_col : str, optional
        Name of the sample-weight column, used for training and for the weighted performance summaries (see
        ``backward_lgbm``).
    validation_weight_col : str, optional
        Name of the sample-weight column of ``validation_data``; ``weight_col`` is used when it is None.
    wgt_col : str, optional
        Alias of ``weight_col``, used only when ``weight_col`` is None.

    Attributes
    ----------
    train_data : pd.DataFrame
        The training dataset.
    varlist : list of str
        The initial list of feature variables.
    dep : str
        Name of the target column.
    model_type : str
        The model type, converted to lower case.
    validation_data : pd.DataFrame or None
        The validation dataset.
    test_data_dict : dict
        The test datasets (an empty dict when none were given).
    weight_col : str or None
        The sample-weight column (``weight_col``, or ``wgt_col`` when ``weight_col`` is None).
    validation_weight_col : str or None
        The sample-weight column of the validation dataset.

    Examples
    --------
    >>> eliminator = BackwardVariableEliminator(
    ...     train_data=train_df,
    ...     varlist=feature_cols,
    ...     dep='target',
    ...     model_type='lgbm',
    ...     validation_data=val_df
    ... )
    >>> results = eliminator.run(n_rounds=5)
    """

    def __init__(
        self,
        train_data: pd.DataFrame,
        varlist: List[str],
        dep: str,
        model_type: str = "lgbm",
        validation_data: Optional[pd.DataFrame] = None,
        test_data_dict: Optional[Dict[str, pd.DataFrame]] = None,
        weight_col: Optional[str] = None,
        validation_weight_col: Optional[str] = None,
        wgt_col: Optional[str] = None,
    ):
        self.train_data = train_data
        self.varlist = varlist
        self.dep = dep
        self.model_type = model_type.lower()
        self.validation_data = validation_data
        self.test_data_dict = test_data_dict or {}
        self.weight_col = weight_col or wgt_col
        self.validation_weight_col = validation_weight_col
        self._results = []

    def run(
        self,
        n_rounds: int = 5,
        varreduct_params: Optional[Dict] = None,
        stopping_metric: str = "auc",
        seed: int = 42,
        num_boost_round: int = 200,
        early_stopping_rounds: int = 20,
        importance_type: str = "gain",
        cum_importance_threshold: float = 0.99,
        min_vars: int = 10,
        ret_perf: bool = True,
        nbins: int = 10,
        **kwargs,
    ) -> List[Dict]:
        """
        Run multiple rounds of backward variable elimination.

        Every round trains a model on the variables that survived the previous round (the first round uses ``varlist``) with
        ``backward_lgbm`` or ``backward_xgbm`` and keeps the selected variables. The run stops after ``n_rounds`` rounds, or
        earlier once at most ``min_vars`` variables are left. Calling ``run`` again discards the previous results.

        Parameters
        ----------
        n_rounds : int, default 5
            Number of elimination rounds.
        varreduct_params : dict, optional
            Model hyperparameters. Every round works on a deep copy, so the caller's dict is not modified.
        stopping_metric : str, default "auc"
            Early-stopping metric.
        seed : int, default 42
            Random seed.
        num_boost_round : int, default 200
            Maximum number of boosting rounds.
        early_stopping_rounds : int, default 20
            Number of early-stopping rounds.
        importance_type : str, default "gain"
            Feature importance type.
        cum_importance_threshold : float, default 0.99
            Cumulative importance threshold.
        min_vars : int, default 10
            Minimum number of variables to keep.
        ret_perf : bool, default True
            Whether to return performance metrics. When False the ``"perf"`` entry of every round is an empty dict.
        nbins : int, default 10
            Number of bins.
        **kwargs
            Further keyword arguments forwarded to ``backward_lgbm`` / ``backward_xgbm`` (for example ``precision``,
            ``min_bin_prop``, ``include_missing``, ``equal_freq`` or, for XGBoost only, ``monotone_constraints``).
            ``weight_col`` and ``validation_weight_col`` are set by the constructor: passing them here raises ``TypeError``.

        Returns
        -------
        list of dict
            Results of each elimination round, also kept by the eliminator. Every dict has the keys ``"round"`` (1-based round
            number), ``"n_vars_in"``, ``"n_vars_out"``, ``"selected_vars"`` (list of the variables kept), ``"model"`` (the
            trained booster) and ``"perf"`` (dict of performance summaries by dataset name).
        """
        current_vars = self.varlist.copy()
        self._results = []

        backward_fn = backward_lgbm if self.model_type == "lgbm" else backward_xgbm

        for round_idx in range(1, n_rounds + 1):
            logging.info(f"[BackwardVariableEliminator] Round {round_idx}/{n_rounds}, vars={len(current_vars)}")

            result = backward_fn(
                train_data=self.train_data,
                varlist=current_vars,
                dep=self.dep,
                varreduct_params=copy.deepcopy(varreduct_params),
                stopping_metric=stopping_metric,
                seed=seed,
                num_boost_round=num_boost_round,
                early_stopping_rounds=early_stopping_rounds,
                importance_type=importance_type,
                cum_importance_threshold=cum_importance_threshold,
                min_vars=min_vars,
                validation_data=self.validation_data,
                test_data_dict=self.test_data_dict,
                ret_perf=ret_perf,
                nbins=nbins,
                weight_col=self.weight_col,
                validation_weight_col=self.validation_weight_col,
                **kwargs,
            )

            if ret_perf:
                selected_vars, model, perf_dict = result
            else:
                selected_vars, model = result
                perf_dict = {}

            round_result = {
                "round": round_idx,
                "n_vars_in": len(current_vars),
                "n_vars_out": len(selected_vars),
                "selected_vars": selected_vars,
                "model": model,
                "perf": perf_dict,
            }
            self._results.append(round_result)
            current_vars = selected_vars

            if len(current_vars) <= min_vars:
                logging.info(f"[BackwardVariableEliminator] Reached min_vars={min_vars}, stopping early.")
                break

        return self._results

    def get_final_vars(self) -> List[str]:
        """
        Return the final list of variables after elimination.

        Returns
        -------
        list of str
            The variables selected by the last round, or ``varlist`` itself when ``run`` has not produced results yet.
        """
        if not self._results:
            return self.varlist
        return self._results[-1]["selected_vars"]

    def get_summary(self) -> pd.DataFrame:
        """
        Return the summary table of each elimination round.

        Returns
        -------
        pd.DataFrame
            One row per round with the columns ``round``, ``n_vars_in``, ``n_vars_out`` and ``vars_removed``. An empty
            DataFrame (without columns) when ``run`` has not produced results yet.
        """
        rows = []
        for r in self._results:
            rows.append({
                "round": r["round"],
                "n_vars_in": r["n_vars_in"],
                "n_vars_out": r["n_vars_out"],
                "vars_removed": r["n_vars_in"] - r["n_vars_out"],
            })
        return pd.DataFrame(rows)

run

run(n_rounds: int = 5, varreduct_params: Optional[Dict] = None, stopping_metric: str = 'auc', seed: int = 42, num_boost_round: int = 200, early_stopping_rounds: int = 20, importance_type: str = 'gain', cum_importance_threshold: float = 0.99, min_vars: int = 10, ret_perf: bool = True, nbins: int = 10, **kwargs) -> List[Dict]

Run multiple rounds of backward variable elimination.

Every round trains a model on the variables that survived the previous round (the first round uses varlist) with backward_lgbm or backward_xgbm and keeps the selected variables. The run stops after n_rounds rounds, or earlier once at most min_vars variables are left. Calling run again discards the previous results.

Parameters:

Name Type Description Default
n_rounds int

Number of elimination rounds.

5
varreduct_params dict

Model hyperparameters. Every round works on a deep copy, so the caller's dict is not modified.

None
stopping_metric str

Early-stopping metric.

"auc"
seed int

Random seed.

42
num_boost_round int

Maximum number of boosting rounds.

200
early_stopping_rounds int

Number of early-stopping rounds.

20
importance_type str

Feature importance type.

"gain"
cum_importance_threshold float

Cumulative importance threshold.

0.99
min_vars int

Minimum number of variables to keep.

10
ret_perf bool

Whether to return performance metrics. When False the "perf" entry of every round is an empty dict.

True
nbins int

Number of bins.

10
**kwargs

Further keyword arguments forwarded to backward_lgbm / backward_xgbm (for example precision, min_bin_prop, include_missing, equal_freq or, for XGBoost only, monotone_constraints). weight_col and validation_weight_col are set by the constructor: passing them here raises TypeError.

{}

Returns:

Type Description
list of dict

Results of each elimination round, also kept by the eliminator. Every dict has the keys "round" (1-based round number), "n_vars_in", "n_vars_out", "selected_vars" (list of the variables kept), "model" (the trained booster) and "perf" (dict of performance summaries by dataset name).

Source code in Modeling_Tool/Model/Backward_Tool.py
def run(
    self,
    n_rounds: int = 5,
    varreduct_params: Optional[Dict] = None,
    stopping_metric: str = "auc",
    seed: int = 42,
    num_boost_round: int = 200,
    early_stopping_rounds: int = 20,
    importance_type: str = "gain",
    cum_importance_threshold: float = 0.99,
    min_vars: int = 10,
    ret_perf: bool = True,
    nbins: int = 10,
    **kwargs,
) -> List[Dict]:
    """
    Run multiple rounds of backward variable elimination.

    Every round trains a model on the variables that survived the previous round (the first round uses ``varlist``) with
    ``backward_lgbm`` or ``backward_xgbm`` and keeps the selected variables. The run stops after ``n_rounds`` rounds, or
    earlier once at most ``min_vars`` variables are left. Calling ``run`` again discards the previous results.

    Parameters
    ----------
    n_rounds : int, default 5
        Number of elimination rounds.
    varreduct_params : dict, optional
        Model hyperparameters. Every round works on a deep copy, so the caller's dict is not modified.
    stopping_metric : str, default "auc"
        Early-stopping metric.
    seed : int, default 42
        Random seed.
    num_boost_round : int, default 200
        Maximum number of boosting rounds.
    early_stopping_rounds : int, default 20
        Number of early-stopping rounds.
    importance_type : str, default "gain"
        Feature importance type.
    cum_importance_threshold : float, default 0.99
        Cumulative importance threshold.
    min_vars : int, default 10
        Minimum number of variables to keep.
    ret_perf : bool, default True
        Whether to return performance metrics. When False the ``"perf"`` entry of every round is an empty dict.
    nbins : int, default 10
        Number of bins.
    **kwargs
        Further keyword arguments forwarded to ``backward_lgbm`` / ``backward_xgbm`` (for example ``precision``,
        ``min_bin_prop``, ``include_missing``, ``equal_freq`` or, for XGBoost only, ``monotone_constraints``).
        ``weight_col`` and ``validation_weight_col`` are set by the constructor: passing them here raises ``TypeError``.

    Returns
    -------
    list of dict
        Results of each elimination round, also kept by the eliminator. Every dict has the keys ``"round"`` (1-based round
        number), ``"n_vars_in"``, ``"n_vars_out"``, ``"selected_vars"`` (list of the variables kept), ``"model"`` (the
        trained booster) and ``"perf"`` (dict of performance summaries by dataset name).
    """
    current_vars = self.varlist.copy()
    self._results = []

    backward_fn = backward_lgbm if self.model_type == "lgbm" else backward_xgbm

    for round_idx in range(1, n_rounds + 1):
        logging.info(f"[BackwardVariableEliminator] Round {round_idx}/{n_rounds}, vars={len(current_vars)}")

        result = backward_fn(
            train_data=self.train_data,
            varlist=current_vars,
            dep=self.dep,
            varreduct_params=copy.deepcopy(varreduct_params),
            stopping_metric=stopping_metric,
            seed=seed,
            num_boost_round=num_boost_round,
            early_stopping_rounds=early_stopping_rounds,
            importance_type=importance_type,
            cum_importance_threshold=cum_importance_threshold,
            min_vars=min_vars,
            validation_data=self.validation_data,
            test_data_dict=self.test_data_dict,
            ret_perf=ret_perf,
            nbins=nbins,
            weight_col=self.weight_col,
            validation_weight_col=self.validation_weight_col,
            **kwargs,
        )

        if ret_perf:
            selected_vars, model, perf_dict = result
        else:
            selected_vars, model = result
            perf_dict = {}

        round_result = {
            "round": round_idx,
            "n_vars_in": len(current_vars),
            "n_vars_out": len(selected_vars),
            "selected_vars": selected_vars,
            "model": model,
            "perf": perf_dict,
        }
        self._results.append(round_result)
        current_vars = selected_vars

        if len(current_vars) <= min_vars:
            logging.info(f"[BackwardVariableEliminator] Reached min_vars={min_vars}, stopping early.")
            break

    return self._results

get_final_vars

get_final_vars() -> List[str]

Return the final list of variables after elimination.

Returns:

Type Description
list of str

The variables selected by the last round, or varlist itself when run has not produced results yet.

Source code in Modeling_Tool/Model/Backward_Tool.py
def get_final_vars(self) -> List[str]:
    """
    Return the final list of variables after elimination.

    Returns
    -------
    list of str
        The variables selected by the last round, or ``varlist`` itself when ``run`` has not produced results yet.
    """
    if not self._results:
        return self.varlist
    return self._results[-1]["selected_vars"]

get_summary

get_summary() -> DataFrame

Return the summary table of each elimination round.

Returns:

Type Description
DataFrame

One row per round with the columns round, n_vars_in, n_vars_out and vars_removed. An empty DataFrame (without columns) when run has not produced results yet.

Source code in Modeling_Tool/Model/Backward_Tool.py
def get_summary(self) -> pd.DataFrame:
    """
    Return the summary table of each elimination round.

    Returns
    -------
    pd.DataFrame
        One row per round with the columns ``round``, ``n_vars_in``, ``n_vars_out`` and ``vars_removed``. An empty
        DataFrame (without columns) when ``run`` has not produced results yet.
    """
    rows = []
    for r in self._results:
        rows.append({
            "round": r["round"],
            "n_vars_in": r["n_vars_in"],
            "n_vars_out": r["n_vars_out"],
            "vars_removed": r["n_vars_in"] - r["n_vars_out"],
        })
    return pd.DataFrame(rows)

BackwardEliminationAnalyzer

Analyzer for backward elimination results.

Analyze and visualize the results of a BackwardVariableEliminator run.

Parameters:

Name Type Description Default
results list of dict

Return value of BackwardVariableEliminator.run().

required

Attributes:

Name Type Description
results list of dict

The results given to the constructor.

Examples:

>>> analyzer = BackwardEliminationAnalyzer(results)
>>> analyzer.plot_var_reduction()
>>> final_vars = analyzer.get_stable_vars(top_n=20)
Source code in Modeling_Tool/Model/Backward_Tool.py
class BackwardEliminationAnalyzer:
    """
    Analyzer for backward elimination results.

    Analyze and visualize the results of a ``BackwardVariableEliminator`` run.

    Parameters
    ----------
    results : list of dict
        Return value of ``BackwardVariableEliminator.run()``.

    Attributes
    ----------
    results : list of dict
        The results given to the constructor.

    Examples
    --------
    >>> analyzer = BackwardEliminationAnalyzer(results)
    >>> analyzer.plot_var_reduction()
    >>> final_vars = analyzer.get_stable_vars(top_n=20)
    """

    def __init__(self, results: List[Dict]):
        self.results = results

    def get_stable_vars(self, top_n: Optional[int] = None) -> List[str]:
        """
        Return the stable variables that are kept in every round.

        Parameters
        ----------
        top_n : int, optional
            Number of stable variables to return (the first N in alphabetical order); None returns all of them.

        Returns
        -------
        list of str
            The variables present in the ``"selected_vars"`` of every round, sorted alphabetically (an empty list when there
            are no results).
        """
        if not self.results:
            return []

        stable = set(self.results[0]["selected_vars"])
        for r in self.results[1:]:
            stable &= set(r["selected_vars"])

        stable_list = sorted(stable)
        if top_n is not None:
            stable_list = stable_list[:top_n]
        return stable_list

    def plot_var_reduction(
        self,
        figsize: Tuple[int, int] = (8, 4),
        save_path: Optional[str] = None,
    ) -> None:
        """
        Plot a line chart of the number of variables against the elimination round.

        Parameters
        ----------
        figsize : tuple, default (8, 4)
            Figure size.
        save_path : str, optional
            File path for saving the figure (written with ``dpi=150`` and the figure is then closed); if None, the figure is
            displayed directly with ``plt.show()``.

        Returns
        -------
        None
        """
        rounds = [r["round"] for r in self.results]
        n_vars = [r["n_vars_out"] for r in self.results]

        fig, ax = plt.subplots(figsize=figsize)
        ax.plot(rounds, n_vars, marker="o", linewidth=2, color="#4C72B0")
        ax.set_xlabel("Elimination Round")
        ax.set_ylabel("Number of Variables Kept")
        ax.set_title("Backward Variable Elimination: Change in Variable Count")
        ax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True))
        plt.tight_layout()

        if save_path:
            fig.savefig(save_path, dpi=150, bbox_inches="tight")
            plt.close(fig)
        else:
            plt.show()

    def get_perf_trend(self, dataset: str = "mdl", metric: str = "IV") -> pd.DataFrame:
        """
        Return the trend of a performance metric over the rounds for a given dataset.

        Parameters
        ----------
        dataset : str, default "mdl"
            Dataset name, e.g. "mdl", "hd", "oot": "mdl" is the training data, "hd" the validation data and any other name is
            a key of ``test_data_dict``.
        metric : str, default "IV"
            Name of the performance metric column of the performance summary (for example "IV", "AUC" or "KS").

        Returns
        -------
        pd.DataFrame
            One row per round that has a performance summary for the dataset, with the columns ``round`` and the metric (the
            first row of the summary is used; the value is ``None`` when the summary has no such column). Rounds without a
            summary for the dataset, for example after a run with ``ret_perf=False``, are skipped.
        """
        rows = []
        for r in self.results:
            perf = r.get("perf", {})
            if dataset in perf and perf[dataset] is not None:
                summary = perf[dataset]
                val = None
                if isinstance(summary, dict):
                    val = summary.get(metric, None)
                elif isinstance(summary, pd.DataFrame) and metric in summary.columns and len(summary):
                    # BackwardVariableEliminator stores each split's get_perf_summary() frame
                    val = summary[metric].iloc[0]
                rows.append({"round": r["round"], metric: val})
        return pd.DataFrame(rows)

get_stable_vars

get_stable_vars(top_n: Optional[int] = None) -> List[str]

Return the stable variables that are kept in every round.

Parameters:

Name Type Description Default
top_n int

Number of stable variables to return (the first N in alphabetical order); None returns all of them.

None

Returns:

Type Description
list of str

The variables present in the "selected_vars" of every round, sorted alphabetically (an empty list when there are no results).

Source code in Modeling_Tool/Model/Backward_Tool.py
def get_stable_vars(self, top_n: Optional[int] = None) -> List[str]:
    """
    Return the stable variables that are kept in every round.

    Parameters
    ----------
    top_n : int, optional
        Number of stable variables to return (the first N in alphabetical order); None returns all of them.

    Returns
    -------
    list of str
        The variables present in the ``"selected_vars"`` of every round, sorted alphabetically (an empty list when there
        are no results).
    """
    if not self.results:
        return []

    stable = set(self.results[0]["selected_vars"])
    for r in self.results[1:]:
        stable &= set(r["selected_vars"])

    stable_list = sorted(stable)
    if top_n is not None:
        stable_list = stable_list[:top_n]
    return stable_list

plot_var_reduction

plot_var_reduction(figsize: Tuple[int, int] = (8, 4), save_path: Optional[str] = None) -> None

Plot a line chart of the number of variables against the elimination round.

Parameters:

Name Type Description Default
figsize tuple

Figure size.

(8, 4)
save_path str

File path for saving the figure (written with dpi=150 and the figure is then closed); if None, the figure is displayed directly with plt.show().

None

Returns:

Type Description
None
Source code in Modeling_Tool/Model/Backward_Tool.py
def plot_var_reduction(
    self,
    figsize: Tuple[int, int] = (8, 4),
    save_path: Optional[str] = None,
) -> None:
    """
    Plot a line chart of the number of variables against the elimination round.

    Parameters
    ----------
    figsize : tuple, default (8, 4)
        Figure size.
    save_path : str, optional
        File path for saving the figure (written with ``dpi=150`` and the figure is then closed); if None, the figure is
        displayed directly with ``plt.show()``.

    Returns
    -------
    None
    """
    rounds = [r["round"] for r in self.results]
    n_vars = [r["n_vars_out"] for r in self.results]

    fig, ax = plt.subplots(figsize=figsize)
    ax.plot(rounds, n_vars, marker="o", linewidth=2, color="#4C72B0")
    ax.set_xlabel("Elimination Round")
    ax.set_ylabel("Number of Variables Kept")
    ax.set_title("Backward Variable Elimination: Change in Variable Count")
    ax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True))
    plt.tight_layout()

    if save_path:
        fig.savefig(save_path, dpi=150, bbox_inches="tight")
        plt.close(fig)
    else:
        plt.show()

get_perf_trend

get_perf_trend(dataset: str = 'mdl', metric: str = 'IV') -> DataFrame

Return the trend of a performance metric over the rounds for a given dataset.

Parameters:

Name Type Description Default
dataset str

Dataset name, e.g. "mdl", "hd", "oot": "mdl" is the training data, "hd" the validation data and any other name is a key of test_data_dict.

"mdl"
metric str

Name of the performance metric column of the performance summary (for example "IV", "AUC" or "KS").

"IV"

Returns:

Type Description
DataFrame

One row per round that has a performance summary for the dataset, with the columns round and the metric (the first row of the summary is used; the value is None when the summary has no such column). Rounds without a summary for the dataset, for example after a run with ret_perf=False, are skipped.

Source code in Modeling_Tool/Model/Backward_Tool.py
def get_perf_trend(self, dataset: str = "mdl", metric: str = "IV") -> pd.DataFrame:
    """
    Return the trend of a performance metric over the rounds for a given dataset.

    Parameters
    ----------
    dataset : str, default "mdl"
        Dataset name, e.g. "mdl", "hd", "oot": "mdl" is the training data, "hd" the validation data and any other name is
        a key of ``test_data_dict``.
    metric : str, default "IV"
        Name of the performance metric column of the performance summary (for example "IV", "AUC" or "KS").

    Returns
    -------
    pd.DataFrame
        One row per round that has a performance summary for the dataset, with the columns ``round`` and the metric (the
        first row of the summary is used; the value is ``None`` when the summary has no such column). Rounds without a
        summary for the dataset, for example after a run with ``ret_perf=False``, are skipped.
    """
    rows = []
    for r in self.results:
        perf = r.get("perf", {})
        if dataset in perf and perf[dataset] is not None:
            summary = perf[dataset]
            val = None
            if isinstance(summary, dict):
                val = summary.get(metric, None)
            elif isinstance(summary, pd.DataFrame) and metric in summary.columns and len(summary):
                # BackwardVariableEliminator stores each split's get_perf_summary() frame
                val = summary[metric].iloc[0]
            rows.append({"round": r["round"], metric: val})
    return pd.DataFrame(rows)

backward_lgbm

backward_lgbm(train_data: DataFrame, varlist: List[str], dep: str, varreduct_params: Optional[Dict] = None, stopping_metric: str = 'auc', seed: int = 42, num_boost_round: int = 200, early_stopping_rounds: int = 20, importance_type: str = 'gain', cum_importance_threshold: float = 0.99, min_vars: int = 10, validation_data: Optional[DataFrame] = None, test_data_dict: Optional[Dict[str, DataFrame]] = None, ret_perf: bool = True, nbins: int = 10, precision: int = 5, min_bin_prop: float = 0.05, include_missing: bool = True, equal_freq: bool = True, ascending: bool = True, fillna: Optional[float] = None, spec_values: Optional[List] = None, weight_col: Optional[str] = None, validation_weight_col: Optional[str] = None, wgt_col: Optional[str] = None) -> Tuple

Run backward variable elimination with a LightGBM model.

Train a LightGBM model and screen the variables by a cumulative feature-importance threshold (backward variable elimination).

Parameters:

Name Type Description Default
train_data DataFrame

Training dataset. Must contain the dep column and all feature columns in varlist.

required
varlist list of str

List of feature variables used for modeling.

required
dep str

Name of the target column (binary 0/1 variable).

required
varreduct_params dict

LightGBM hyperparameter dictionary. Required parameters that are not specified use preset values (metric from stopping_metric, seed from seed, objective='binary', boosting_type='gbdt' and num_threads=8). The dictionary is completed in place, so the caller's dict is modified.

None
stopping_metric str

Evaluation metric for early stopping, e.g. "auc" or "binary_logloss". It is only used when varreduct_params does not already contain metric.

"auc"
seed int

Random seed for reproducibility. It is only used when varreduct_params does not already contain seed.

42
num_boost_round int

Maximum number of boosting rounds.

200
early_stopping_rounds int

Number of early-stopping rounds; training stops when the validation metric has not improved for this many consecutive rounds. The metric is monitored on validation_data, or on train_data when no validation data is given.

20
importance_type str

Feature importance type, "gain" or "split".

"gain"
cum_importance_threshold float

Cumulative feature-importance threshold. The features are ranked by importance and kept while their cumulative share of the total importance is at most this value (the feature that would push the share above it is not kept).

0.99
min_vars int

Minimum number of variables to keep. When fewer variables pass the threshold, the min_vars most important features are returned instead.

10
validation_data DataFrame

Validation dataset, used for early stopping. It is also scored in the performance summary under the key "hd".

None
test_data_dict dict

Dictionary of test datasets, in the form {name: DataFrame}. They are only scored for the performance summary, under their names. The training data uses the key "mdl" and the validation data "hd", so a test dataset with one of these names replaces that entry.

None
ret_perf bool

Whether to return model performance metrics.

True
nbins int

Number of bins in the Gains table (passed to get_perf_summary as pct_bins; performance summary only).

10
precision int

Numeric precision (performance summary only).

5
min_bin_prop float

Minimum bin proportion (performance summary only).

0.05
include_missing bool

Whether to include a missing-value bin (performance summary only).

True
equal_freq bool

Whether to use equal-frequency binning (performance summary only).

True
ascending bool

Not used: accepted for API compatibility and ignored.

True
fillna float

Not used: accepted for API compatibility and ignored.

None
spec_values list

Not used: accepted for API compatibility and ignored.

None
weight_col str

Name of the sample-weight column. It is used as the training weight (KeyError if train_data lacks it), as the validation weight unless validation_weight_col is given, and for the weighted performance summary of every dataset that contains the column (datasets without it are evaluated unweighted).

None
validation_weight_col str

Name of the sample-weight column of validation_data; weight_col is used when it is None. It is also the weight column of the validation performance summary when validation_data contains it.

None
wgt_col str

Alias of weight_col, used only when weight_col is None.

None

Returns:

Type Description
tuple

(selected_vars, model, perf_dict), or (selected_vars, model) when ret_perf is False. selected_vars is the list of selected variable names (most important first), model the trained lightgbm.Booster and perf_dict a dict {dataset name: performance summary DataFrame} (see get_perf_summary).

Raises:

Type Description
TypeError

If the input data is not a pandas.DataFrame.

ImportError

If lightgbm is not installed.

Examples:

>>> selected_vars, model, perf = backward_lgbm(
...     train_data=train_df,
...     varlist=feature_cols,
...     dep='target',
...     validation_data=val_df
... )
Source code in Modeling_Tool/Model/Backward_Tool.py
def backward_lgbm(
    train_data: pd.DataFrame,
    varlist: List[str],
    dep: str,
    varreduct_params: Optional[Dict] = None,
    stopping_metric: str = "auc",
    seed: int = 42,
    num_boost_round: int = 200,
    early_stopping_rounds: int = 20,
    importance_type: str = "gain",
    cum_importance_threshold: float = 0.99,
    min_vars: int = 10,
    validation_data: Optional[pd.DataFrame] = None,
    test_data_dict: Optional[Dict[str, pd.DataFrame]] = None,
    ret_perf: bool = True,
    nbins: int = 10,
    precision: int = 5,
    min_bin_prop: float = 0.05,
    include_missing: bool = True,
    equal_freq: bool = True,
    ascending: bool = True,
    fillna: Optional[float] = None,
    spec_values: Optional[List] = None,
    weight_col: Optional[str] = None,
    validation_weight_col: Optional[str] = None,
    wgt_col: Optional[str] = None,
) -> Tuple:
    """
    Run backward variable elimination with a LightGBM model.

    Train a LightGBM model and screen the variables by a cumulative
    feature-importance threshold (backward variable elimination).

    Parameters
    ----------
    train_data : pd.DataFrame
        Training dataset. Must contain the ``dep`` column and all feature columns in ``varlist``.
    varlist : list of str
        List of feature variables used for modeling.
    dep : str
        Name of the target column (binary 0/1 variable).
    varreduct_params : dict, optional
        LightGBM hyperparameter dictionary. Required parameters that are not specified use preset values (``metric`` from
        ``stopping_metric``, ``seed`` from ``seed``, ``objective='binary'``, ``boosting_type='gbdt'`` and
        ``num_threads=8``). The dictionary is completed in place, so the caller's dict is modified.
    stopping_metric : str, default "auc"
        Evaluation metric for early stopping, e.g. "auc" or "binary_logloss". It is only used when ``varreduct_params``
        does not already contain ``metric``.
    seed : int, default 42
        Random seed for reproducibility. It is only used when ``varreduct_params`` does not already contain ``seed``.
    num_boost_round : int, default 200
        Maximum number of boosting rounds.
    early_stopping_rounds : int, default 20
        Number of early-stopping rounds; training stops when the validation metric has not improved for this many consecutive rounds.
        The metric is monitored on ``validation_data``, or on ``train_data`` when no validation data is given.
    importance_type : str, default "gain"
        Feature importance type, "gain" or "split".
    cum_importance_threshold : float, default 0.99
        Cumulative feature-importance threshold. The features are ranked by importance and kept while their cumulative
        share of the total importance is at most this value (the feature that would push the share above it is not kept).
    min_vars : int, default 10
        Minimum number of variables to keep. When fewer variables pass the threshold, the ``min_vars`` most important
        features are returned instead.
    validation_data : pd.DataFrame, optional
        Validation dataset, used for early stopping. It is also scored in the performance summary under the key ``"hd"``.
    test_data_dict : dict, optional
        Dictionary of test datasets, in the form ``{name: DataFrame}``. They are only scored for the performance summary,
        under their names. The training data uses the key ``"mdl"`` and the validation data ``"hd"``, so a test dataset with
        one of these names replaces that entry.
    ret_perf : bool, default True
        Whether to return model performance metrics.
    nbins : int, default 10
        Number of bins in the Gains table (passed to ``get_perf_summary`` as ``pct_bins``; performance summary only).
    precision : int, default 5
        Numeric precision (performance summary only).
    min_bin_prop : float, default 0.05
        Minimum bin proportion (performance summary only).
    include_missing : bool, default True
        Whether to include a missing-value bin (performance summary only).
    equal_freq : bool, default True
        Whether to use equal-frequency binning (performance summary only).
    ascending : bool, default True
        Not used: accepted for API compatibility and ignored.
    fillna : float, optional
        Not used: accepted for API compatibility and ignored.
    spec_values : list, optional
        Not used: accepted for API compatibility and ignored.
    weight_col : str, optional
        Name of the sample-weight column. It is used as the training weight (``KeyError`` if ``train_data`` lacks it), as
        the validation weight unless ``validation_weight_col`` is given, and for the weighted performance summary of every
        dataset that contains the column (datasets without it are evaluated unweighted).
    validation_weight_col : str, optional
        Name of the sample-weight column of ``validation_data``; ``weight_col`` is used when it is None. It is also the
        weight column of the validation performance summary when ``validation_data`` contains it.
    wgt_col : str, optional
        Alias of ``weight_col``, used only when ``weight_col`` is None.

    Returns
    -------
    tuple
        ``(selected_vars, model, perf_dict)``, or ``(selected_vars, model)`` when ``ret_perf`` is False. ``selected_vars`` is
        the list of selected variable names (most important first), ``model`` the trained ``lightgbm.Booster`` and
        ``perf_dict`` a dict ``{dataset name: performance summary DataFrame}`` (see ``get_perf_summary``).

    Raises
    ------
    TypeError
        If the input data is not a ``pandas.DataFrame``.
    ImportError
        If lightgbm is not installed.

    Examples
    --------
    >>> selected_vars, model, perf = backward_lgbm(
    ...     train_data=train_df,
    ...     varlist=feature_cols,
    ...     dep='target',
    ...     validation_data=val_df
    ... )
    """
    try:
        import lightgbm as lgb
    except ImportError:
        raise ImportError("Please install lightgbm: pip install lightgbm")

    if varreduct_params is None:
        varreduct_params = {}

    if test_data_dict is None:
        test_data_dict = {}

    if spec_values is None:
        spec_values = []

    datain_all = OrderedDict()
    datain_all["mdl"] = train_data
    if validation_data is not None:
        datain_all["hd"] = validation_data
    datain_all.update(test_data_dict)

    # Check data format consistency
    try:
        for k, v in datain_all.items():
            assert isinstance(v, pd.DataFrame)
    except AssertionError:
        logging.warning("Please provide data in pandas.DataFrame format")
        raise TypeError("Please provide data in pandas.DataFrame format")

    # Preset parameters (to keep the model reproducible)
    hyperparams_preset = {
        "metric": stopping_metric,
        "seed": seed,
        "objective": "binary",
        "boosting_type": "gbdt",
        "num_threads": 8
    }

    # Fill in any missing required parameters
    lacked_params = [k for k in list(hyperparams_preset.keys()) if k not in list(varreduct_params.keys())]
    for param in lacked_params:
        varreduct_params[param] = hyperparams_preset[param]

    weight_col = weight_col or wgt_col

    # Build the LightGBM datasets
    train_weight = resolve_sample_weight(data=train_data, weight_col=weight_col, expected_len=len(train_data))
    lgb_train = lgb.Dataset(train_data[varlist], label=train_data[dep], weight=train_weight)

    if validation_data is not None:
        valid_weight = resolve_sample_weight(
            data=validation_data,
            weight_col=validation_weight_col or weight_col,
            expected_len=len(validation_data),
        )
        lgb_valid = lgb.Dataset(validation_data[varlist], label=validation_data[dep], weight=valid_weight)
        valid_sets = [lgb_valid]
        valid_names = ["hd"]
    else:
        valid_sets = [lgb_train]
        valid_names = ["mdl"]

    callbacks = [
        lgb.early_stopping(stopping_rounds=early_stopping_rounds, verbose=False),
        lgb.log_evaluation(period=-1)
    ]

    # Train the model
    model = lgb.train(
        params=varreduct_params,
        train_set=lgb_train,
        num_boost_round=num_boost_round,
        valid_sets=valid_sets,
        valid_names=valid_names,
        callbacks=callbacks
    )

    # Get the feature importance and screen the variables
    importance_df = pd.DataFrame({
        "feature": model.feature_name(),
        "importance": model.feature_importance(importance_type=importance_type)
    }).sort_values("importance", ascending=False).reset_index(drop=True)

    importance_df["cum_importance"] = importance_df["importance"].cumsum() / importance_df["importance"].sum()

    # Select the variables within the cumulative-importance threshold
    selected_idx = importance_df[importance_df["cum_importance"] <= cum_importance_threshold].index.tolist()

    # Make sure at least min_vars variables are kept
    if len(selected_idx) < min_vars:
        selected_idx = list(range(min(min_vars, len(importance_df))))

    selected_vars = importance_df.loc[selected_idx, "feature"].tolist()

    if not ret_perf:
        return selected_vars, model

    # Compute performance
    score_col = "_lgbm_score"
    perf_dict = {}

    for name, df in datain_all.items():
        df_score = df.copy()
        df_score[score_col] = model.predict(df_score[varlist])
        perf_dict[name] = _backward_perf_summary(
            df_score,
            split_name=name,
            dep=dep,
            score_col=score_col,
            weight_col=weight_col,
            validation_weight_col=validation_weight_col,
            nbins=nbins,
            precision=precision,
            min_bin_prop=min_bin_prop,
            include_missing=include_missing,
            equal_freq=equal_freq,
        )

    return selected_vars, model, perf_dict

backward_xgbm

backward_xgbm(train_data: DataFrame, varlist: List[str], dep: str, varreduct_params: Optional[Dict] = None, stopping_metric: str = 'auc', seed: int = 42, num_boost_round: int = 200, early_stopping_rounds: int = 20, importance_type: str = 'gain', cum_importance_threshold: float = 0.99, min_vars: int = 10, validation_data: Optional[DataFrame] = None, test_data_dict: Optional[Dict[str, DataFrame]] = None, ret_perf: bool = True, nbins: int = 10, precision: int = 5, min_bin_prop: float = 0.05, include_missing: bool = True, equal_freq: bool = True, ascending: bool = True, fillna: Optional[float] = None, spec_values: Optional[List] = None, monotone_constraints: Optional[Dict[str, int]] = None, weight_col: Optional[str] = None, validation_weight_col: Optional[str] = None, wgt_col: Optional[str] = None) -> Tuple

Run backward variable elimination with an XGBoost model.

Train an XGBoost model and screen the variables by a cumulative feature-importance threshold (backward variable elimination).

Parameters:

Name Type Description Default
train_data DataFrame

Training dataset. Must contain the dep column and all feature columns in varlist.

required
varlist list of str

List of feature variables used for modeling.

required
dep str

Name of the target column (binary 0/1 variable).

required
varreduct_params dict

XGBoost hyperparameter dictionary. Required parameters that are not specified use preset values (eval_metric from stopping_metric, tree_method='exact', booster='gbtree', seed from seed and monotone_constraints built from monotone_constraints). The dictionary is completed in place, so the caller's dict is modified. No objective is preset, so XGBoost's default reg:squarederror applies and the scores are not probabilities unless the dictionary sets one, such as {'objective': 'binary:logistic'}.

None
stopping_metric str

Evaluation metric for early stopping. It is only used when varreduct_params does not already contain eval_metric.

"auc"
seed int

Random seed. It is only used when varreduct_params does not already contain seed.

42
num_boost_round int

Maximum number of boosting rounds.

200
early_stopping_rounds int

Number of early-stopping rounds. The metric is monitored on validation_data, or on train_data when no validation data is given.

20
importance_type str

Feature importance type passed to Booster.get_score: "weight", "gain", "cover", "total_gain" or "total_cover".

"gain"
cum_importance_threshold float

Cumulative feature-importance threshold. The features are ranked by importance and kept while their cumulative share of the total importance is at most this value (the feature that would push the share above it is not kept). Features that XGBoost never used get an importance of 0.

0.99
min_vars int

Minimum number of variables to keep. When fewer variables pass the threshold, the min_vars most important features are returned instead.

10
validation_data DataFrame

Validation dataset, used for early stopping. It is also scored in the performance summary under the key "hd".

None
test_data_dict dict

Dictionary of test datasets, in the form {name: DataFrame}. They are only scored for the performance summary, under their names. The training data uses the key "mdl" and the validation data "hd", so a test dataset with one of these names replaces that entry.

None
ret_perf bool

Whether to return performance metrics.

True
nbins int

Number of bins in the Gains table (passed to get_perf_summary as pct_bins; performance summary only).

10
precision int

Numeric precision (performance summary only).

5
min_bin_prop float

Minimum bin proportion (performance summary only).

0.05
include_missing bool

Whether to include a missing-value bin (performance summary only).

True
equal_freq bool

Whether to use equal-frequency binning (performance summary only).

True
ascending bool

Not used: accepted for API compatibility and ignored.

True
fillna float

Not used: accepted for API compatibility and ignored.

None
spec_values list

Not used: accepted for API compatibility and ignored.

None
monotone_constraints dict

Dictionary of monotone constraints, in the form {feature_name: 1 or -1}. Features that are not listed get 0 (no constraint). It is only used when varreduct_params does not already contain monotone_constraints.

None
weight_col str

Name of the sample-weight column. It is used as the training weight (KeyError if train_data lacks it), as the validation weight unless validation_weight_col is given, and for the weighted performance summary of every dataset that contains the column (datasets without it are evaluated unweighted).

None
validation_weight_col str

Name of the sample-weight column of validation_data; weight_col is used when it is None. It is also the weight column of the validation performance summary when validation_data contains it.

None
wgt_col str

Alias of weight_col, used only when weight_col is None.

None

Returns:

Type Description
tuple

(selected_vars, model, perf_dict), or (selected_vars, model) when ret_perf is False. selected_vars is the list of selected variable names (most important first), model the trained xgboost.Booster and perf_dict a dict {dataset name: performance summary DataFrame} (see get_perf_summary).

Raises:

Type Description
TypeError

If the input data is not a pandas.DataFrame.

ImportError

If xgboost is not installed.

Examples:

>>> selected_vars, model, perf = backward_xgbm(
...     train_data=train_df,
...     varlist=feature_cols,
...     dep='target',
...     validation_data=val_df
... )
Source code in Modeling_Tool/Model/Backward_Tool.py
def backward_xgbm(
    train_data: pd.DataFrame,
    varlist: List[str],
    dep: str,
    varreduct_params: Optional[Dict] = None,
    stopping_metric: str = "auc",
    seed: int = 42,
    num_boost_round: int = 200,
    early_stopping_rounds: int = 20,
    importance_type: str = "gain",
    cum_importance_threshold: float = 0.99,
    min_vars: int = 10,
    validation_data: Optional[pd.DataFrame] = None,
    test_data_dict: Optional[Dict[str, pd.DataFrame]] = None,
    ret_perf: bool = True,
    nbins: int = 10,
    precision: int = 5,
    min_bin_prop: float = 0.05,
    include_missing: bool = True,
    equal_freq: bool = True,
    ascending: bool = True,
    fillna: Optional[float] = None,
    spec_values: Optional[List] = None,
    monotone_constraints: Optional[Dict[str, int]] = None,
    weight_col: Optional[str] = None,
    validation_weight_col: Optional[str] = None,
    wgt_col: Optional[str] = None,
) -> Tuple:
    """
    Run backward variable elimination with an XGBoost model.

    Train an XGBoost model and screen the variables by a cumulative
    feature-importance threshold (backward variable elimination).

    Parameters
    ----------
    train_data : pd.DataFrame
        Training dataset. Must contain the ``dep`` column and all feature columns in ``varlist``.
    varlist : list of str
        List of feature variables used for modeling.
    dep : str
        Name of the target column (binary 0/1 variable).
    varreduct_params : dict, optional
        XGBoost hyperparameter dictionary. Required parameters that are not specified use preset values (``eval_metric``
        from ``stopping_metric``, ``tree_method='exact'``, ``booster='gbtree'``, ``seed`` from ``seed`` and
        ``monotone_constraints`` built from ``monotone_constraints``). The dictionary is completed in place, so the caller's
        dict is modified. No ``objective`` is preset, so XGBoost's default ``reg:squarederror`` applies and the scores are
        not probabilities unless the dictionary sets one, such as ``{'objective': 'binary:logistic'}``.
    stopping_metric : str, default "auc"
        Evaluation metric for early stopping. It is only used when ``varreduct_params`` does not already contain
        ``eval_metric``.
    seed : int, default 42
        Random seed. It is only used when ``varreduct_params`` does not already contain ``seed``.
    num_boost_round : int, default 200
        Maximum number of boosting rounds.
    early_stopping_rounds : int, default 20
        Number of early-stopping rounds. The metric is monitored on ``validation_data``, or on ``train_data`` when no
        validation data is given.
    importance_type : str, default "gain"
        Feature importance type passed to ``Booster.get_score``: "weight", "gain", "cover", "total_gain" or "total_cover".
    cum_importance_threshold : float, default 0.99
        Cumulative feature-importance threshold. The features are ranked by importance and kept while their cumulative
        share of the total importance is at most this value (the feature that would push the share above it is not kept).
        Features that XGBoost never used get an importance of 0.
    min_vars : int, default 10
        Minimum number of variables to keep. When fewer variables pass the threshold, the ``min_vars`` most important
        features are returned instead.
    validation_data : pd.DataFrame, optional
        Validation dataset, used for early stopping. It is also scored in the performance summary under the key ``"hd"``.
    test_data_dict : dict, optional
        Dictionary of test datasets, in the form ``{name: DataFrame}``. They are only scored for the performance summary,
        under their names. The training data uses the key ``"mdl"`` and the validation data ``"hd"``, so a test dataset with
        one of these names replaces that entry.
    ret_perf : bool, default True
        Whether to return performance metrics.
    nbins : int, default 10
        Number of bins in the Gains table (passed to ``get_perf_summary`` as ``pct_bins``; performance summary only).
    precision : int, default 5
        Numeric precision (performance summary only).
    min_bin_prop : float, default 0.05
        Minimum bin proportion (performance summary only).
    include_missing : bool, default True
        Whether to include a missing-value bin (performance summary only).
    equal_freq : bool, default True
        Whether to use equal-frequency binning (performance summary only).
    ascending : bool, default True
        Not used: accepted for API compatibility and ignored.
    fillna : float, optional
        Not used: accepted for API compatibility and ignored.
    spec_values : list, optional
        Not used: accepted for API compatibility and ignored.
    monotone_constraints : dict, optional
        Dictionary of monotone constraints, in the form ``{feature_name: 1 or -1}``. Features that are not listed get 0 (no
        constraint). It is only used when ``varreduct_params`` does not already contain ``monotone_constraints``.
    weight_col : str, optional
        Name of the sample-weight column. It is used as the training weight (``KeyError`` if ``train_data`` lacks it), as
        the validation weight unless ``validation_weight_col`` is given, and for the weighted performance summary of every
        dataset that contains the column (datasets without it are evaluated unweighted).
    validation_weight_col : str, optional
        Name of the sample-weight column of ``validation_data``; ``weight_col`` is used when it is None. It is also the
        weight column of the validation performance summary when ``validation_data`` contains it.
    wgt_col : str, optional
        Alias of ``weight_col``, used only when ``weight_col`` is None.

    Returns
    -------
    tuple
        ``(selected_vars, model, perf_dict)``, or ``(selected_vars, model)`` when ``ret_perf`` is False. ``selected_vars`` is
        the list of selected variable names (most important first), ``model`` the trained ``xgboost.Booster`` and
        ``perf_dict`` a dict ``{dataset name: performance summary DataFrame}`` (see ``get_perf_summary``).

    Raises
    ------
    TypeError
        If the input data is not a ``pandas.DataFrame``.
    ImportError
        If xgboost is not installed.

    Examples
    --------
    >>> selected_vars, model, perf = backward_xgbm(
    ...     train_data=train_df,
    ...     varlist=feature_cols,
    ...     dep='target',
    ...     validation_data=val_df
    ... )
    """
    try:
        import xgboost as xgb
    except ImportError:
        raise ImportError("Please install xgboost: pip install xgboost")

    if varreduct_params is None:
        varreduct_params = {}

    if test_data_dict is None:
        test_data_dict = {}

    if spec_values is None:
        spec_values = []

    if monotone_constraints is None:
        monotone_constraints = {}

    # Build the monotone constraint mapping for all variables
    mc_dict = {var: monotone_constraints.get(var, 0) for var in varlist}

    datain_all = OrderedDict()
    datain_all["mdl"] = train_data
    if validation_data is not None:
        datain_all["hd"] = validation_data
    datain_all.update(test_data_dict)

    # Check data format consistency
    try:
        for k, v in datain_all.items():
            assert isinstance(v, pd.DataFrame)
    except AssertionError:
        logging.warning("Please provide data in pandas.DataFrame format")
        raise TypeError("Please provide data in pandas.DataFrame format")

    # Preset parameters (to keep the model reproducible)
    hyperparams_preset = {
        'eval_metric': stopping_metric,
        'tree_method': 'exact',
        'booster': 'gbtree',
        'seed': seed,
        'monotone_constraints': mc_dict
    }

    # Fill in any missing required parameters
    lacked_params = [k for k in list(hyperparams_preset.keys()) if k not in list(varreduct_params.keys())]
    for param in lacked_params:
        varreduct_params[param] = hyperparams_preset[param]

    weight_col = weight_col or wgt_col

    # Build the XGBoost datasets
    train_weight = resolve_sample_weight(data=train_data, weight_col=weight_col, expected_len=len(train_data))
    xgb_train = xgb.DMatrix(train_data[varlist], label=train_data[dep], weight=train_weight)

    evals = [(xgb_train, "mdl")]
    if validation_data is not None:
        valid_weight = resolve_sample_weight(
            data=validation_data,
            weight_col=validation_weight_col or weight_col,
            expected_len=len(validation_data),
        )
        xgb_valid = xgb.DMatrix(validation_data[varlist], label=validation_data[dep], weight=valid_weight)
        evals.append((xgb_valid, "hd"))

    # Train the model
    evals_result = {}
    model = xgb.train(
        params=varreduct_params,
        dtrain=xgb_train,
        num_boost_round=num_boost_round,
        evals=evals,
        early_stopping_rounds=early_stopping_rounds,
        evals_result=evals_result,
        verbose_eval=False
    )

    # Get the feature importance and screen the variables
    importance_raw = model.get_score(importance_type=importance_type)
    importance_df = pd.DataFrame(
        list(importance_raw.items()), columns=["feature", "importance"]
    ).sort_values("importance", ascending=False).reset_index(drop=True)

    # Add features with zero importance
    missing_feats = [f for f in varlist if f not in importance_df["feature"].values]
    if missing_feats:
        zero_df = pd.DataFrame({"feature": missing_feats, "importance": 0.0})
        importance_df = pd.concat([importance_df, zero_df], ignore_index=True)

    importance_df["cum_importance"] = importance_df["importance"].cumsum() / (importance_df["importance"].sum() or 1)

    selected_idx = importance_df[importance_df["cum_importance"] <= cum_importance_threshold].index.tolist()

    if len(selected_idx) < min_vars:
        selected_idx = list(range(min(min_vars, len(importance_df))))

    selected_vars = importance_df.loc[selected_idx, "feature"].tolist()

    if not ret_perf:
        return selected_vars, model

    # Compute performance
    score_col = "_xgbm_score"
    perf_dict = {}

    for name, df in datain_all.items():
        df_score = df.copy()
        xgb_dmat = xgb.DMatrix(df_score[varlist])
        df_score[score_col] = model.predict(xgb_dmat)
        perf_dict[name] = _backward_perf_summary(
            df_score,
            split_name=name,
            dep=dep,
            score_col=score_col,
            weight_col=weight_col,
            validation_weight_col=validation_weight_col,
            nbins=nbins,
            precision=precision,
            min_bin_prop=min_bin_prop,
            include_missing=include_missing,
            equal_freq=equal_freq,
        )

    return selected_vars, model, perf_dict