2025, Sep 29 17:32

cross‑validation में Brier Skill Score का NaN क्यों आता है और इसे predict_proba से कैसे ठीक करें

असंतुलित डेटा पर cross‑validation में Brier Skill Score (BSS) के NaN का कारण जानें और scikit‑learn में response_method='predict_proba' से इसे स्थिर व सही ढंग से मापें.

मॉडलों को स्कोर करते समय NaN मिलना वाकई раздражает, खासकर जब आप असंतुलित डेटासेट पर संभाव्य (probabilistic) क्लासिफायरों की तुलना कर रहे हों। यहाँ क्रॉस‑वैलिडेशन में ब्रायर स्किल स्कोर (BSS) के साथ ऐसा क्यों होता है और बिना मॉडलिंग सेटअप बदले इसे कैसे ठीक किया जा सकता है—इसका छोटा, понятный разбор है।

प्रसंग: असंतुलित फ्रॉड डेटासेट पर BSS का मूल्यांकन

टार्गेट काफी असंतुलित है: लगभग 2133 पंक्तियों में Counter({0: 2067, 1: 66}). 10‑fold क्रॉस‑वैलिडेशन में हर फोल्ड में सिर्फ़ 6–7 पॉज़िटिव आते हैं, इसलिए अस्थिरता का संदेह होना स्वाभाविक है। फिर भी यहाँ NaN स्कोर का कारण कुछ और निकला: स्कोरर की कॉन्फ़िगरेशन।

पुनरुत्पादन: NaN BSS कैसे दिखाई देता है

ब्रायर स्किल स्कोर आपके मॉडल के ब्रायर स्कोर की तुलना उस बेसलाइन से करता है जो हमेशा पॉज़िटिव रेट की भविष्यवाणी करता है। शून्य से बड़ा मान बेसलाइन से सुधार दर्शाता है; नकारात्मक मान बदतर‑से‑बेसलाइन प्रदर्शन को दिखाता है।

import pandas as pd
import numpy as np
from numpy import mean, std
from collections import Counter
from sklearn.preprocessing import LabelEncoder, StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from imblearn.pipeline import Pipeline as ImbPipeline
from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
from sklearn.metrics import brier_score_loss, make_scorer
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
# --------------------------------------------------
# 1) ब्रायर स्किल स्कोर
# --------------------------------------------------
def skill_brier(y_obs, y_pred_prob):
    pos_share = np.count_nonzero(y_obs) / len(y_obs)
    ref_pred = [pos_share for _ in range(len(y_obs))]
    ref_bs = brier_score_loss(y_obs, ref_pred)
    mdl_bs = brier_score_loss(y_obs, y_pred_prob)
    if ref_bs == 0:
        return 0.0
    return 1.0 - (mdl_bs / ref_bs)
# --------------------------------------------------
# 2) क्रॉस‑वैलिडेटेड मूल्यांकन (समस्याग्रस्त सेटअप)
# --------------------------------------------------
def run_eval(X, y, estimator, folds=10, reps=3):
    kfold = RepeatedStratifiedKFold(n_splits=folds, n_repeats=reps, random_state=42)
    # यहीं स्कोर में NaN आ सकता है
    scorer = make_scorer(skill_brier, needs_proba=True)
    out = cross_val_score(estimator, X, y, scoring=scorer, cv=kfold, n_jobs=-1)
    print("Mean BSS: %.3f (%.3f)" % (mean(out), std(out)))
    return out
# --------------------------------------------------
# 3) प्रीप्रोसेसिंग + मॉडल पाइपलाइन
# --------------------------------------------------
def make_flow(X, base_estimator=None):
    num_feats = X.select_dtypes(include=["int64", "float64"]).columns
    cat_feats = X.select_dtypes(include=["object", "category"]).columns
    num_block = Pipeline(steps=[
        ("num_impute", SimpleImputer(strategy="mean")),
        ("num_scale", StandardScaler())
    ])
    cat_block = Pipeline(steps=[
        ("cat_impute", SimpleImputer(strategy="most_frequent")),
        ("cat_ohe", OneHotEncoder(handle_unknown="ignore"))
    ])
    features = ColumnTransformer(
        transformers=[
            ("num_blk", num_block, num_feats),
            ("cat_blk", cat_block, cat_feats)
        ]
    )
    final_est = base_estimator if base_estimator is not None else RandomForestClassifier(random_state=42)
    pipe = ImbPipeline(steps=[
        ("prep", features),
        ("clf", final_est)
    ])
    return pipe
# --------------------------------------------------
# 4) उदाहरण उपयोग
# --------------------------------------------------
# df = pd.read_csv("credit_card.csv")
# X = df.drop("Fraud_Flag", axis=1)
# y = LabelEncoder().fit_transform(df["Fraud_Flag"])
print(X.shape, y.shape, Counter(y))
base = DummyClassifier(strategy="prior")
base_pipe = make_flow(X, base)
print("\nBaseline (DummyClassifier):")
run_eval(X, y, base_pipe)
lr_pipe = make_flow(X, LogisticRegression(max_iter=1000))
print("\nLogistic Regression:")
run_eval(X, y, lr_pipe)
rf_pipe = make_flow(X, RandomForestClassifier(random_state=42))
print("\nRandom Forest:")
run_eval(X, y, rf_pipe)
gb_pipe = make_flow(X, GradientBoostingClassifier(random_state=42))
print("\nGradient Boosting:")
run_eval(X, y, gb_pipe)

असल में गड़बड़ी कहाँ है

NaN मान स्कोरर की परिभाषा से आते हैं। हाल के scikit‑learn संस्करणों में needs_proba=True के साथ make_scorer का उपयोग अप्रचलित (deprecated) है और अस्थिर व्यवहार करा सकता है। इस सेटअप में मेट्रिक को भविष्यवाणी की संभावनाएँ चाहिए, इसलिए स्कोरिंग फ़ंक्शन को predict_proba को स्पष्ट रूप से कॉल करना चाहिए।

समाधान: response_method के जरिए predict_proba को अनिवार्य करें

स्कोरर बनाते समय response method को सीधे निर्दिष्ट करें। इससे अस्थिरता दूर होती है और मान्य BSS मान मिलते हैं।

def run_eval_fixed(X, y, estimator, folds=10, reps=3):
    kfold = RepeatedStratifiedKFold(n_splits=folds, n_repeats=reps, random_state=42)
    # संभाव्य मेट्रिक्स के लिए predict_proba को स्पष्ट रूप से अनुरोध करें
    scorer = make_scorer(skill_brier, response_method="predict_proba")
    out = cross_val_score(estimator, X, y, scoring=scorer, cv=kfold, n_jobs=-1)
    print("Mean BSS: %.3f (%.3f)" % (mean(out), std(out)))
    return out
# Example usage (same pipelines as above)
print("\nBaseline (DummyClassifier) with fixed scorer:")
run_eval_fixed(X, y, base_pipe)
print("\nLogistic Regression with fixed scorer:")
run_eval_fixed(X, y, lr_pipe)
print("\nRandom Forest with fixed scorer:")
run_eval_fixed(X, y, rf_pipe)
print("\nGradient Boosting with fixed scorer:")
run_eval_fixed(X, y, gb_pipe)

यह क्यों महत्वपूर्ण है

जब आप ब्रायर स्किल स्कोर से संभाव्य मॉडल का मूल्यांकन करते हैं, तो स्कोरर को क्लास की संभावनाएँ चाहिए—क्लास लेबल या decision scores नहीं। अप्रचलित फ्लैग्स पर निर्भर रहना इस अपेक्षा को चुपचाप तोड़ सकता है और NaN पैदा कर सकता है, जिससे वास्तविक प्रदर्शन छिप जाता है। response_method="predict_proba" को स्पष्ट रूप से सेट करने से अस्पष्टता दूर होती है और स्कोरिंग का मार्ग निर्धारक (deterministic) बनता है।

इस तरह के अत्यधिक असंतुलित डेटासेट—यहाँ 2133 में से 66 पॉज़िटिव—में 10‑fold CV पर हर फोल्ड में लगभग 6–7 पॉज़िटिव ही आते हैं। यह संदर्भ स्कोर को शोर‑भरा महसूस करा सकता है। लेकिन इस मामले में NaN की समस्या स्कोरर कॉन्फ़िगरेशन ठीक करते ही समाप्त हो गई।

मुख्य बातें

अगर कोई संभाव्य मेट्रिक क्रॉस‑वैलिडेशन के दौरान NaN लौटाए, तो देखें कि स्कोरर आपकी मेट्रिक को भविष्यवाणियाँ कैसे भेज रहा है। ब्रायर स्किल स्कोर के लिए make_scorer में response_method="predict_proba" सेट करें ताकि यह संभावनाओं पर ही काम करे। असंतुलित टार्गेट के साथ, फोल्ड की संरचना पर ध्यान रखें और ऊपर दिखाए अनुसार stratified क्रॉस‑वैलिडेशन का उपयोग करें।

यह लेख StackOverflow पर प्रश्न Br0k3nS0u1 द्वारा और उनके ही उत्तर पर आधारित है।