Python 管道 returns 在交叉验证中使用时的 NaN 分数

Python Pipeline returns NaN Scores When Used in Cross Validation

我想用 sklearn 创建一个管道,包括一些预处理步骤和最后一个模型以适应数据。我使用此管道通过交叉验证获得分数。稍后我想使用GridSearchCV中的管道进行参数优化。

截至目前,预处理步骤包括:

问题是我得到的分数都是nan。它运行得非常快,似乎传递给模型的空数组:

from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.model_selection import cross_validate
import numpy as np

# Create random dataframe
num_data = np.random.random_sample((5,4))
cat_data = ['good','bad','fair','excellent','bad']
col_list_stack = ['SalePrice','Id','TotalBsmtSF','GrdLivArea']
data = pd.DataFrame(num_data, columns = col_list_stack)

data['Quality'] = cat_data

X_train = data.drop(labels = ['SalePrice'], axis = 1)
y_train = data['SalePrice']

#------------------------------------------------------------#
# create a custom transformer to remove columns
class ColumnsRemoval(BaseEstimator, TransformerMixin):
    def __init__(self, skip = False, remove_cols = ['Id','TotalBsmtSF']):
        self._remove_cols = remove_cols
        self._skip = skip
        
    def fit(self, X, y = None):
        return self
                
    def transform(self, X, y = None):
        if not self._skip:
            return X.drop(labels = self._remove_cols,axis = 1)
        else:
            return X

#------------------------------------------------------------#
# PIPELINE and cross-validation        
# Preprocessing steps common to numerical and categorical data
preprocessor_common = Pipeline(steps=[
    ('remove_features', ColumnsRemoval())])

# Separated preprocessing steps
numeric_transformer = Pipeline(steps=[
    ('scaler', StandardScaler())])

categorical_transformer = Pipeline(steps=[    
    ('onehot', OneHotEncoder(handle_unknown='ignore'))])

preprocessor_by_cat = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, ['GrdLivArea']),
        ('cat', categorical_transformer, ['Quality'])], remainder = 'passthrough')

# Full pipeline with model
pipe = Pipeline(steps = [('preprocessor_common', preprocessor_common),
                    ('preprocessor_by_cat', preprocessor_by_cat),
                  ('model', LinearRegression())])

# Use cross validation to obtain scores
scores = cross_validate(pipe, X_train, y_train,
            scoring = ["neg_mean_squared_error","r2"], cv = 4)

我试过以下方法:

pipe = Pipeline(steps = [('preprocessor_common', preprocessor_common),
                            ('preprocessor_by_cat', preprocessor_by_cat),
                      ])
X_processed = pipe.fit_transform(X_train)

# Use cross validation to obtain scores
scores = cross_validate(LinearRegression(), X_processed, y_train,
            scoring = ["neg_mean_squared_error","r2"], cv = 4)

据我了解,在管道中进行预处理或对管道进行预处理+模型是相同的,这就是为什么我认为获取 NaN 值是一个问题。

我希望问题很清楚,恭喜你做到了这里:)

TL;DR

您需要重新定义自定义 ColumnsRemoval__init()__ 函数,因为将 Python 列表作为默认值传递会导致错误。一种可能的解决方案:

class ColumnsRemoval(BaseEstimator, TransformerMixin):
    def __init__(self, skip=False, remove_cols=None):
        if remove_cols is None:
            remove_cols = ['Id', 'TotalBsmtSF']
        self._remove_cols = remove_cols
        self._skip = skip

    def fit(self, X, y=None):
        return self

    def transform(self, X, y=None):
        if not self._skip:
            return X.drop(labels=self._remove_cols, axis=1)
        else:
            return X

有了这个,您的管道应该按预期工作。


背景

我 运行 你的 MWE 出现了以下错误:

FitFailedWarning: Estimator fit failed. The score on this train-test partition for these parameters will be set to nan.

它与您自定义的以下行有关 ColumnsRemoval:

return X.drop(labels=self._remove_cols, axis=1)

抛出了错误:

ValueError: Need to specify at least one of 'labels', 'index' or 'columns'

将标准 Python 列表传递给 drop() 函数时,这似乎是一个已知问题,在此 中进行了讨论。解决方案是通过例如numpy 数组或 pandas 索引对象。我提出的另一个解决方案是不在函数定义中为 remove_cols 设置默认值,而是在函数体中分配它。这也有效。

看起来没有人真正知道为什么会这样。抱歉,我无法详细说明实际原因(如果有人可以补充,我会很高兴)。不过问题应该解决了。

我找到问题所在了。我一直在做一些进一步的测试,也使用 float 而不是列表作为默认值。

详述 here,在 Instantiation 部分下:

the object's attributes used in __init__() should have exactly the name of the argument in the constructor.

所以我所做的是使用与 __init__() 中传递的参数名称相同的对象属性名称,现在一切正常。例如:

class ColumnsRemoval(BaseEstimator, TransformerMixin):
    def __init__(self, threshold = 0.9)
        self.threshold = threshold

Using self._threshold(注意 threshold 之前的 _)有一个奇怪的行为,在某些情况下,对象与提供的值(或默认值)一起使用,但是在其他情况下 self._threshold 被设置为 None。这也允许使用 list 作为默认值来传递 __init__() (尽管应避免使用 list 作为默认值,详情请参阅 afsharov 的回答)