pandas 用于sklearn Pipeline的自定义转换器,可同时更改X和y

bn31dyow  于 2023-01-07  发布在  其他
关注(0)|答案(8)|浏览(156)

我想创建自己的变压器,用于sklearn Pipeline
我正在创建一个类,它同时实现fit和transform方法。transformer的作用是从矩阵中删除NaN超过指定数量的行。
我面临的问题是如何更改传递给转换器的X和y矩阵?
我认为这必须在fit方法中完成,因为它可以访问X和y。由于python通过赋值传递参数,一旦我将X重新分配给一个行数更少的新矩阵,对原始X的引用就会丢失(当然对y也是如此)。有可能维护这个引用吗?
我使用pandas DataFrame来轻松地删除NaN过多的行,这可能不是我的用例的正确方法。当前代码如下所示:

class Dropna():

    # thresh is max number of NaNs allowed in a row
    def __init__(self, thresh=0):
        self.thresh = thresh

    def fit(self, X, y):
        total = X.shape[1]
        # +1 to account for 'y' being added to the dframe                                                                                                                            
        new_thresh = total + 1 - self.thresh
        df = pd.DataFrame(X)
        df['y'] = y
        df.dropna(thresh=new_thresh, inplace=True)
        X = df.drop('y', axis=1).values
        y = df['y'].values
        return self

    def transform(self, X):
        return X
ygya80vv

ygya80vv1#

修改样本轴,例如删除样本,不符合scikit-learn transformer API。所以如果你需要这样做,你应该在scikit learn的任何调用之外做,作为预处理。
现在,转换器API用于将给定样本的特性转换为新的特性,这可以隐式地包含来自其他样本的信息,但样本永远不会被删除。
另一种选择是尝试插补缺失值,但是同样,如果你需要删除样本,在使用scikit learn之前将其视为预处理。

8e2ybdfx

8e2ybdfx2#

构建在sklearn之上的包imblearn包含一个估计器FunctionSampler,它允许在流水线步骤中操作特征数组X和目标数组y
请注意,在管道步骤中使用Pipeline需要使用imblearn中的Pipeline类,该类继承自sklearn中的Pipeline类。此外,默认情况下,在Pipeline的上下文中,如果方法resample不是在fit之后立即调用(如fit_resample),则它不执行任何操作。因此,请提前阅读文档。

8fq7wneg

8fq7wneg3#

您必须修改sklearn Pipeline的内部代码。
我们定义了一个转换器,它在拟合(fit_transform)期间移除至少一个特征或目标值为NaN的样本。而在推理(transform)期间移除至少一个特征值为NaN的样本。需要注意的是,我们的转换器在fit_transform中返回X和y,因此我们需要在sklearn Pipeline中处理此行为。

class Dropna():

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

    def fit_transform(self, X, y):
        
        mask = (np.isnan(X).any(-1) | np.isnan(y))
        if hasattr(X, 'loc'):
            X = X.loc[~mask]
        else:
            X = X[~mask]
        if hasattr(y, 'loc'):
            y = y.loc[~mask]
        else:
            y = y[~mask]
        
        return X, y   ###### make fit_transform return X and y
    
    def transform(self, X):
        
        mask = np.isnan(X).any(-1)
        if hasattr(X, 'loc'):
            X = X.loc[~mask]
        else:
            X = X[~mask]
        
        return X

我们只需要在fit_fit方法中的两个特定点修改原始sklearn Pipeline,其余保持不变。

from sklearn import pipeline
from sklearn.base import clone
from sklearn.utils import _print_elapsed_time
from sklearn.utils.validation import check_memory

class Pipeline(pipeline.Pipeline):

    def _fit(self, X, y=None, **fit_params_steps):
        self.steps = list(self.steps)
        self._validate_steps()
        memory = check_memory(self.memory)

        fit_transform_one_cached = memory.cache(pipeline._fit_transform_one)

        for (step_idx, name, transformer) in self._iter(
            with_final=False, filter_passthrough=False
        ):
                        
            if transformer is None or transformer == "passthrough":
                with _print_elapsed_time("Pipeline", self._log_message(step_idx)):
                    continue

            try:
                # joblib >= 0.12
                mem = memory.location
            except AttributeError:
                mem = memory.cachedir
            finally:
                cloned_transformer = clone(transformer) if mem else transformer

            X, fitted_transformer = fit_transform_one_cached(
                cloned_transformer,
                X,
                y,
                None,
                message_clsname="Pipeline",
                message=self._log_message(step_idx),
                **fit_params_steps[name],
            )
            
            if isinstance(X, tuple):    ###### unpack X if is tuple X = (X,y)
                X, y = X
            
            self.steps[step_idx] = (name, fitted_transformer)
        
        return X, y
    
    def fit(self, X, y=None, **fit_params):
        fit_params_steps = self._check_fit_params(**fit_params)
        Xt = self._fit(X, y, **fit_params_steps)
        
        if isinstance(Xt, tuple):    ###### unpack X if is tuple X = (X,y)
            Xt, y = Xt 
        
        with _print_elapsed_time("Pipeline", self._log_message(len(self.steps) - 1)):
            if self._final_estimator != "passthrough":
                fit_params_last_step = fit_params_steps[self.steps[-1][0]]
                self._final_estimator.fit(Xt, y, **fit_params_last_step)

        return self

为了在新的Xy中解压缩Dropna().fit_transform(X, y)生成的值,需要执行此操作。
以下是整个渠道的运作情况:

from sklearn.linear_model import Ridge

X = np.random.uniform(0,1, (100,3))
y = np.random.uniform(0,1, (100,))
X[np.random.uniform(0,1, (100)) < 0.1] = np.nan
y[np.random.uniform(0,1, (100)) < 0.1] = np.nan

pipe = Pipeline([('dropna', Dropna()), ('model', Ridge())])
pipe.fit(X, y)

pipe.predict(X).shape

另一项试验,进一步进行中间预处理步骤:

from sklearn.preprocessing import StandardScaler

pipe = Pipeline([('dropna', Dropna()), ('scaler', StandardScaler()), ('model', Ridge())])
pipe.fit(X, y)

pipe.predict(X).shape

更复杂的行为可以根据需要通过其他简单的修改来实现。如果您对Pipeline().fit_transformPipeline().fit_predict也感兴趣,您需要进行相同的修改。

falq053o

falq053o4#

添加到@João Matias回复:
下面是一个使用imblearn定义管道步骤的示例,该步骤删除包含缺失值的行:

from imblearn import FunctionSampler
def drop_rows_with_any_nan(X, y):
    return X[~np.isnan(X).any(axis=1), :], y[~np.isnan(X).any(axis=1)]
drop_rows_with_any_nan_sampler = FunctionSampler(func=drop_rows_with_any_nan, validate=False)
model_clf2 = pipeline.Pipeline(
    [
        ('preprocess', column_transformer),
        ('drop_na', drop_rows_with_any_nan_sampler),
        ('smote', SMOTE()),
        ('xgb', xgboost.XGBClassifier()),
    ]
)

注意,您必须使用imblearn管道。

eqoofvh9

eqoofvh95#

您可以使用sklearn.preprocessing.FunctionTransformer方法(http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.FunctionTransformer.html)轻松地解决这个问题
你只需要把你对X的替换放到一个函数中

def drop_nans(X, y=None):
    total = X.shape[1]                                           
    new_thresh = total - thresh
    df = pd.DataFrame(X)
    df.dropna(thresh=new_thresh, inplace=True)
    return df.values

你就可以通过调用

transformer = FunctionTransformer(drop_nans, validate=False)

可以在管道中使用。阈值可以在drop_nans函数之外设置。

4ngedf3f

4ngedf3f6#

@eickenberg是正确和干净的答案。尽管如此,我喜欢把所有东西都放在一个管道中,所以如果你感兴趣,我创建了一个库(还没有部署在pypi上),允许对Y:
https://gitlab.com/thibaultB/transformers/
用法如下:

df = pd.DataFrame([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
df.columns = ["a", "b", "target"]

spliter = SplitXY("target") # Create a new step and give it name of column target

pipe = Pipeline([
        ("imputer", SklearnPandasWrapper(KNNImputer())),
        ("spliter", spliter), 
        ("scaler", StandardScaler()),
        ("rf",
            EstimatorWithoutYWrapper(RandomForestRegressor(random_state=45),
                                    spliter)) # EstimatorWithoutYWrapper overwrite RandomForestRegressor to get y from spliter just before calling fit or transform
    ])
pipe.fit(df)

res = pipe.predict(df)

使用这段代码,如果你把所有修改行数的转换器放在“SplitXY”转换器之前,你就可以改变行数。SplitXY转换器之前的转换器应该保留列名,这就是为什么我还添加了一个SklearnPandasWrapper,它 Package sklearn转换器(通常返回numpy数组)来保留列名。

cunj1qz1

cunj1qz17#

您可以使用function transformer

df=pd.DataFrame([[1,2,3],[4,5,6],[np.NaN,np.NaN,9],[7,np.NaN,9]])

from sklearn.pipeline import FunctionTransformer,make_pipeline

def remove_na(df_,thresh=2):
    return df.dropna(thresh=2)

pipe=make_pipeline(FunctionTransformer(func=remove_na,
                                       validate=False,kw_args={"thresh":2}))
pipe.fit_transform(df)

l7wslrjt

l7wslrjt8#

继续使用“deep-copies”,沿着管道向下,X、**y**仍然受到保护

**.fit()**可以在每次调用时首先将 deep-copy 赋值给新的类变量

self.X_without_NaNs = X.copy()
self.y_without_NaNs = y.copy()

然后减少/变换这些以使NaN-s不多于按self.treshold排序的NaN-s

相关问题