Почему метод .fit() преобразует входные данные X в np.array?

Вопрос или проблема

Почему метод (пользовательский) .fit() преобразует pd.DataFrame X в numpy.ndarray?

import pandas as pd
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.compose import TransformedTargetRegressor, make_column_transformer
from xgboost import XGBRegressor

class XGBRPipeline(XGBRegressor):
    def __init__(self, preprocessing, cardinality=10, **xgboost_params):
        super().__init__(**xgboost_params)
        self.preprocessing = preprocessing
        self.cardinality = cardinality
        self.pipeline = None

    def _get_num_cols(self, df):
        return [f for f in df.columns if pd.api.types.is_numeric_dtype(df[f])]

    def _get_cat_cols(self, df):
        return [f for f in df.columns if df[f].dtype.name == 'category']   

    def _get_ord_cols(self, df):
        cat_cols = self._get_cat_cols(df)
        return [f for f in cat_cols if df[f].cat.ordered]

    def _get_non_ord_cols(self, df):
        cat_cols = self._get_cat_cols(df)
        return [f for f in cat_cols if not df[f].cat.ordered]

    def _get_llc_cols(self, df, cardinality):
        non_ord_cols = self._get_non_ord_cols(df)
        return [f for f in non_ord_cols if len(df[f].cat.categories) <= cardinality]

    def _get_hcc_cols(self, df, cardinality):
        non_ordinal_cols = self._get_non_ord_cols(df)
        return [f for f in non_ord_cols if len(df[f].cat.categories) > cardinality]

    def fit(self, X, y=None, **fit_params):

        preprocessing = make_column_transformer(
            (self.preprocessing.get('numeric'), self._get_num_cols(X)),
            (self.preprocessing.get('ordinal'), self._get_ord_cols(X)),
            (self.preprocessing.get('cardinal'), self._get_llc_cols(X, self.cardinality))
        )

        ttr = TransformedTargetRegressor(
            regressor=self,
            func=np.log1p, 
            inverse_func=np.expm1,
            check_inverse=False
        )

        self.pipeline = make_pipeline(preprocessing, ttr)    
        self.pipeline.fit(X, y, **fit_params)
        return self  

    def transform(self, X, **transform_params): 
        return self.pipeline.transform(X, **transform_params)

xgbr = XGBRPipeline(
    preprocessing=preprocessing_xgbr, 
    cardinality=200, 
    n_estimators=100, 
    learning_rate=0.1
)
print(type(train))
xgbr.fit(X=train.drop(columns=[target]), y=train[target]))



---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [67], in ()
     53 xgbr = XGBRPipeline(preprocessing=preprocessing_xgbr, cardinality=200, n_estimators=100, learning_rate=0.1)
     54 print(type(train))
---> 55 xgbr.fit(X=train.drop(columns=[target]), y=train[target])

Input In [67], in XGBRPipeline.fit(self, X, y, **fit_params)
     39 ttr = TransformedTargetRegressor(
     40     regressor=self,
     41     func=np.log1p, 
     42     inverse_func=np.expm1,
     43     check_inverse=False
     44 )
     46 self.pipeline = make_pipeline(preprocessing, ttr)    
---> 47 self.pipeline.fit(X, y, **fit_params)
     48 return self

File ~\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\pipeline.py:405, in Pipeline.fit(self, X, y, **fit_params)
    403     if self._final_estimator != "passthrough":
    404         fit_params_last_step = fit_params_steps[self.steps[-1][0]]
--> 405         self._final_estimator.fit(Xt, y, **fit_params_last_step)
    407 return self

File ~\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\compose\_target.py:262, in TransformedTargetRegressor.fit(self, X, y, **fit_params)
    259 else:
    260     self.regressor_ = clone(self.regressor)
--> 262 self.regressor_.fit(X, y_trans, **fit_params)
    264 if hasattr(self.regressor_, "feature_names_in_"):
    265     self.feature_names_in_ = self.regressor_.feature_names_in_

Input In [67], in XGBRPipeline.fit(self, X, y, **fit_params)
     32 def fit(self, X, y=None, **fit_params):
     33     preprocessing = make_column_transformer(
---> 34         (self.preprocessing.get('numeric'), self._get_num_cols(X)),
     35         (self.preprocessing.get('ordinal'), self._get_ord_cols(X)),
     36         (self.preprocessing.get('cardinal'), self._get_llc_cols(X, self.cardinality))
     37     )
     39     ttr = TransformedTargetRegressor(
     40         regressor=self,
     41         func=np.log1p, 
     42         inverse_func=np.expm1,
     43         check_inverse=False
     44     )
     46     self.pipeline = make_pipeline(preprocessing, ttr)    

Input In [67], in XGBRPipeline._get_num_cols(self, df)
     10 def _get_num_cols(self, df):
---> 11     return [f for f in df.columns if pd.api.types.is_numeric_dtype(df[f])]

AttributeError: 'numpy.ndarray' object has no attribute 'columns'

Это принцип проектирования sklearn… Я думаю, он начался с numpy -> только numpy, и когда появился df, они работали над тем, чтобы сначала принять формат, сохранив выходные данные numpy для согласованности. В настоящее время ведется работа над тем, чтобы sklearn мог выводить датафреймы, когда предоставляются датафреймы. Но библиотека большая, и это требует много утомительной работы.

Ответ или решение

Метод .fit() в вашем кастомном классе XGBRPipeline трансформирует входной параметр X из типа pd.DataFrame в np.ndarray по нескольким причинам, которые связаны с проектированием библиотеки Scikit-learn и ее интеграцией с библиотекой NumPy. Далее, я подробно разложу на составляющие этот процесс.

F (Факт)

Метод .fit() в данном классе используется для обучения модели на основе предоставленных входных данных. В процессе работы с X необходимо выполнять определенные преобразования, включая:

  1. Упрощение обработки данных: NumPy обеспечивает легкость и производительность работы с многомерными массивами, что делает его предпочтительным форматом для математических и статистических операций, необходимых в машинном обучении.

  2. Совместимость с библиотеками: Существует широкая практика в экосистеме Python, где многие библиотеки (например, Scikit-learn) ожидают, что входные данные будут представлены в виде массивов NumPy. Это решение принято для обеспечения однородности и производительности.

O (Объяснение)

Когда метод .fit() вызывается, данный ваш класс начинает обрабатывать датафрейм pd.DataFrame, переходя к этапе подготовки данных, в котором он использует make_column_transformer. На этом шаге, в зависимости от специфики обработки и используемых трансформеров, библиотека может использовать NumPy для выполнения различных математических расчетов.

  1. Конвертация данных: Scikit-learn самостоятельно гарантирует, что входные данные преобразуются в формат, оптимальный для работы, в данном случае — это чаще всего np.ndarray. При этом, если входные данные представлены в виде DataFrame, метод автоматически конвертирует их в массив NumPy.

  2. Производительность: NumPy был разработан для эффективного выполнения операций над массивами. При использовании массивов NumPy вместо DataFrame значительно увеличивается производительность при обработке массивных данных.

R (Результат)

В вашем коде, когда происходит вызов self.pipeline.fit(X, y, **fit_params), интерфейс ожидает, что X будет массивом NumPy. Однако, если X остается DataFrame, попытка обращения к X.columns (как в методе _get_num_cols) приводит к AttributeError, так как NumPy-массивы не имеют атрибута columns. Это подчеркивает важность факта, что Scikit-learn и, в частности, ваш класс, рассчитаны на использование массивов NumPy для выполнения расчетов.

S (Ситуация)

Таким образом, применение метода .fit() в контексте вашего кастомного класса является достаточно стандартной практикой в Python для обработки данных в машинном обучении. Разработчики библиотек стремятся к упрощению интерфейсов и реализации алгоритмов, что и приводит к необходимости преобразования DataFrame в массивы NumPy. Это соответствует философии Scikit-learn — быть максимально совместимыми с NumPy, обеспечивая высокую производительность.

T (Точка зрения)

Считаю, что поддержка формата NumPy как основного способа для работы с данными в методах Scikit-learn является оправданной и целесообразной. Несмотря на то что в сообществе машинного обучения наблюдается растущий интерес к DataFrame как формату для хранения данных, конвертация в массивы NumPy гарантирует более быструю обработку и совместимость с большинством существующих библиотек.

Заключение

Таким образом, метод .fit() изменяет входные данные X из типа pd.DataFrame на np.ndarray, что обуславливается целым рядом факторов, включая оптимизацию производительности, совместимость с библиотеками и стандартные практики, которые формируют современные подходы в разработке алгоритмов машинного обучения. Это решение является не капризом разработчиков, а необходимостью, вытекающей из структуры и философии самой библиотеки Scikit-learn.

Оцените материал
Добавить комментарий

Капча загружается...