6.3. Предварительная обработка данных

Пакет sklearn.preprocessing предоставляет несколько общих служебных функций и классов преобразователей для преобразования необработанных векторов признаков в представление, более подходящее для последующих моделей.

В целом, многие алгоритмы обучения, такие как линейные модели, выигрывают от стандартизации набора данных (см. Importance of Feature Scaling). Если в наборе присутствуют выбросы, то более подходящими могут быть робастные масштабирующие или другие трансформаторы. Поведение различных алгоритмов маштабирования, трансформации и нормализации на наборе данных, содержащем выбросы, показано в Compare the effect of different scalers on data with outliers.

6.3.1. Стандартизация, или удаление среднего и масштабирование дисперсии

Стандартизация наборов данных является общим требованием для многих моделей машинного обучения, реализованных в scikit-learn; они могут вести себя плохо, если отдельные признаки не будут более или менее похожи на стандартные нормально распределенные данные: гауссовы с нулевым средним и единичной дисперсией.

На практике мы часто игнорируем форму распределения и просто преобразуем данные, чтобы центрировать их, удаляя среднее значение каждого признака, а затем масштабируем его, разделив непостоянные признаки на их стандартное отклонение.

Например, многие элементы, используемые в объективной функции алгоритма обучения (такие как RBF-ядро метода опорных векторов (SVM) или регуляризаторы l1 и l2 линейных моделей), могут предполагать, что все признаки сосредоточены вокруг нуля или имеют дисперсию одного и того же порядка. Если у признака дисперсия на порядки больше, чем у других, он может доминировать в объективной функции и сделать так, что модель не сможет правильно обучаться на других признаках, как ожидалось.

Модуль preprocessing предоставляет служебный класс StandardScaler, который представляет собой быстрый и простой способ выполнить следующую операцию с набором данных, подобным массиву:

>>> from sklearn import preprocessing
>>> import numpy as np
>>> X_train = np.array([[ 1., -1.,  2.],
...                     [ 2.,  0.,  0.],
...                     [ 0.,  1., -1.]])
>>> scaler = preprocessing.StandardScaler().fit(X_train)
>>> scaler
StandardScaler()

>>> scaler.mean_
array([1. ..., 0. ..., 0.33...])

>>> scaler.scale_
array([0.81..., 0.81..., 1.24...])

>>> X_scaled = scaler.transform(X_train)
>>> X_scaled
array([[ 0.  ..., -1.22...,  1.33...],
       [ 1.22...,  0.  ..., -0.26...],
       [-1.22...,  1.22..., -1.06...]])

Масштабированные данные имеют нулевое среднее значение и единичную дисперсию:

>>> X_scaled.mean(axis=0)
array([0., 0., 0.])

>>> X_scaled.std(axis=0)
array([1., 1., 1.])

Этот класс реализует API Transformer для вычисления среднего и стандартного отклонения на обучающем наборе, чтобы иметь возможность позже повторно применить то же преобразование к тестовому набору. Следовательно, этот класс подходит для использования на ранних этапах Pipeline:

>>> from sklearn.datasets import make_classification
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.pipeline import make_pipeline
>>> from sklearn.preprocessing import StandardScaler

>>> X, y = make_classification(random_state=42)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
>>> pipe = make_pipeline(StandardScaler(), LogisticRegression())
>>> pipe.fit(X_train, y_train)  # apply scaling on training data
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('logisticregression', LogisticRegression())])

>>> pipe.score(X_test, y_test)  # apply scaling on testing data, without leaking training data.
0.96

Можно отключить центрирование или масштабирование, передав with_mean=False или with_std=False в конструктор StandardScaler.

6.3.1.1. Масштабирование признаков до диапазона

Альтернативная стандартизация заключается в масштабировании характеристик таким образом, чтобы они находились между заданным минимальным и максимальным значением, часто между нулем и единицей, или таким образом, чтобы максимальное абсолютное значение каждой характеристики масштабировалось до размера единицы. Этого можно добиться с помощью MinMaxScaler или MaxAbsScaler соответственно.

Мотивацией к использованию этого масштабирования является устойчивость к очень малым стандартным отклонениям признаков и сохранение нулевых записей в разреженных данных.

Вот пример масштабирования матрицы данных до диапазона [0, 1]:

>>> X_train = np.array([[ 1., -1.,  2.],
...                     [ 2.,  0.,  0.],
...                     [ 0.,  1., -1.]])
...
>>> min_max_scaler = preprocessing.MinMaxScaler()
>>> X_train_minmax = min_max_scaler.fit_transform(X_train)
>>> X_train_minmax
array([[0.5       , 0.        , 1.        ],
       [1.        , 0.5       , 0.33333333],
       [0.        , 1.        , 0.        ]])

Тот же экземпляр преобразователя затем может быть применен к некоторым новым тестовым данным, неизвестным во время вызова обучения: будут применены те же операции масштабирования и сдвига, чтобы соответствовать преобразованию, выполняемому над данными для обучения:

>>> X_test = np.array([[-3., -1.,  4.]])
>>> X_test_minmax = min_max_scaler.transform(X_test)
>>> X_test_minmax
array([[-1.5       ,  0.        ,  1.66666667]])

Можно проанализировать атрибуты маштабирования, чтобы узнать точную природу преобразования, полученного на обучающих данных:

>>> min_max_scaler.scale_
array([0.5       , 0.5       , 0.33...])

>>> min_max_scaler.min_
array([0.        , 0.5       , 0.33...])

Если MinMaxScaler явно задан feature_range=(min, max), полная формула будет выглядеть так:

X_std = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

X_scaled = X_std * (max - min) + min

MaxAbsScaler работает очень похожим образом, но масштабируется таким образом, чтобы обучающие данные находились в диапазоне [-1, 1] путем деления на наибольшее максимальное значение в каждом признаке. Он предназначен для данных, которые уже сосредоточены на нуле или разреженных данных.

Вот как можно использовать данные из предыдущего примера с этим маштабированием:

>>> X_train = np.array([[ 1., -1.,  2.],
...                     [ 2.,  0.,  0.],
...                     [ 0.,  1., -1.]])
...
>>> max_abs_scaler = preprocessing.MaxAbsScaler()
>>> X_train_maxabs = max_abs_scaler.fit_transform(X_train)
>>> X_train_maxabs
array([[ 0.5, -1. ,  1. ],
       [ 1. ,  0. ,  0. ],
       [ 0. ,  1. , -0.5]])
>>> X_test = np.array([[ -3., -1.,  4.]])
>>> X_test_maxabs = max_abs_scaler.transform(X_test)
>>> X_test_maxabs
array([[-1.5, -1. ,  2. ]])
>>> max_abs_scaler.scale_
array([2.,  1.,  2.])

6.3.1.2. Масштабирование разреженных данных

Центрирование разреженных данных разрушило бы структуру разреженности данных, и поэтому это редко бывает разумным решением. Однако может иметь смысл масштабировать разреженные входные данные, особенно если объекты имеют разные масштабы.

MaxAbsScaler был специально разработан для масштабирования разреженных данных, и это рекомендуемый способ сделать это. Однако StandardScaler может принимать матрицы scipy.sparse в качестве входных данных, если конструктору явно передано with_mean=False. В противном случае будет выдано сообщение ValueError, поскольку простое центрирование приведет к нарушению разреженности и часто приведет к сбою выполнения из-за непреднамеренного выделения чрезмерного объема памяти. RobustScaler не может быть адаптирован для разреженных входных данных, но вы можете использовать метод transform для разреженных входных данных.

Обратите внимание, что средства масштабирования принимают формат как сжатых разреженных строк, так и сжатых разреженных столбцов (см. scipy.sparse.csr_matrix и scipy.sparse.csc_matrix). Любые другие разреженные входные данные будут преобразованы в представление сжатых разреженных строк. Чтобы избежать ненужных копий памяти, рекомендуется выбирать представление CSR или CSC в восходящем направлении.

Наконец, если ожидается, что центрированные данные будут достаточно маленькими, другим вариантом является явное преобразование входных данных в массив с использованием метода разреженных матриц toarray.

6.3.1.3. Масштабирование данных с выбросами

Если ваши данные содержат много выбросов, масштабирование с использованием среднего значения и дисперсии данных, скорее всего, не будет работать очень хорошо. В этих случаях вы можете вместо этого использовать RobustScaler в качестве замены. Он использует более надежные оценки для центра и диапазона ваших данных.

6.3.1.4. Центрирование матриц ядра

Если у вас есть матрица ядра \(K\), которая вычисляет скалярное произведение в пространстве признаков (возможно, неявно), определенном функцией \(\phi(\cdot)\), KernelCenterer может преобразовать матрицу ядра так, чтобы она содержала внутренние продукты в пространстве признаков, определенном \(\phi\), с последующим удалением среднего значения в этом пространстве. Другими словами, KernelCenterer вычисляет центрированную матрицу Грама, связанную с положительным полуопределенным ядром \(K\).

Математическая формулировка

Теперь, когда у нас есть интуиция, мы можем взглянуть на математическую формулировку. Пусть \(K\) будет матрицей ядра формы (n_samples, n_samples), вычисленной из \(X\), матрицы данных формы (n_samples, n_features), на этапе обучения. \(K\) определяется как

\[K(X, X) = \phi(X) . \phi(X)^{T}\]

\(\phi(X)\) - это отображение функции \(X\) в гильбертово пространство. Центрированное ядро \(\tilde{K}\) определяется как:

\[\tilde{K}(X, X) = \tilde{\phi}(X) . \tilde{\phi}(X)^{T}\]

где \(\tilde{\phi}(X)\) получается в результате центрирования \(\phi(X)\) в гильбертовом пространстве.

Таким образом, можно вычислить \(\tilde{K}\), отобразив \(X\) с помощью функции \(\phi(\cdot)\) и центрируя данные в этом новом пространстве. Однако ядра часто используются, поскольку они позволяют выполнять некоторые алгебраические вычисления, позволяющие избежать явного вычисления этого отображения с помощью \(\phi(\cdot)\). Действительно, можно неявно центрировать, как показано в Приложении B в [Scholkopf1998]:

\[\tilde{K} = K - 1_{\text{n}_{samples}} K - K 1_{\text{n}_{samples}} + 1_{\text{n}_{samples}} K 1_{\text{n}_{samples}}\]

\(1_{\text{n}_{samples}}\) представляет собой матрицу (n_samples, n_samples), где все записи равны \(\frac{1}{\text{n}_{samples}}\). На этапе преобразования (transform) ядро становится \(K_{test}(X, Y)\), определяемым как:

\[K_{test}(X, Y) = \phi(Y) . \phi(X)^{T}\]

\(Y\) - это тестовый набор данных формы (n_samples_test, n_features) и, следовательно, \(K_{test}\) имеет форму (n_samples_test, n_samples). В этом случае центрирование \(K_{test}\) выполняется следующим образом:

\[\tilde{K}_{test}(X, Y) = K_{test} - 1'_{\text{n}_{samples}} K - K_{test} 1_{\text{n}_{samples}} + 1'_{\text{n}_{samples}} K 1_{\text{n}_{samples}}\]

\(1'_{\text{n}_{samples}}\) - это матрица формы (n_samples_test, n_samples), где все записи равны \(\frac{1}{\text{n}_{samples}}\).

6.3.2. Нелинейное преобразование

Доступны два типа преобразований: квантильные преобразования и степенные преобразования. Как квантильное, так и степенное преобразование основано на монотонных преобразованиях признаков и, таким образом, сохраняет ранг значений по каждому признаку.

Квантильные преобразования помещают все признаки в одно и то же желаемое распределение на основе формулы \(G^{-1}(F(X))\) где \(F\) - кумулятивная функция распределения признака и \(G^{-1}\) функция квантиля желаемого выходного распределения \(G\). В этой формуле используются два следующих факта:

    1. если \(X\) является случайной величиной с непрерывной кумулятивной функцией распределения \(F\), то \(F(X)\) равномерно распределено на \([0,1]\);

    1. если \(U\) - случайная величина с равномерным распределением на \([0,1]\), то \(G^{-1}(U)\) имеет распределение \(G\). Выполняя ранговое преобразование, квантильное преобразование сглаживает необычные распределения и меньше подвержено влиянию выбросов, чем методы масштабирования. Однако это искажает корреляции и расстояния внутри и между объектами.

Степенные преобразования - это семейство параметрических преобразований, целью которых является сопоставление данных из любого распределения с распределением, максимально близким к распределению Гаусса.

6.3.2.1. Сопоставление с равномерным распределением

QuantileTransformer обеспечивает непараметрическое преобразование для сопоставления данных с равномерным распределением со значениями от 0 до 1:

>>> from sklearn.datasets import load_iris
>>> from sklearn.model_selection import train_test_split
>>> X, y = load_iris(return_X_y=True)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
>>> quantile_transformer = preprocessing.QuantileTransformer(random_state=0)
>>> X_train_trans = quantile_transformer.fit_transform(X_train)
>>> X_test_trans = quantile_transformer.transform(X_test)
>>> np.percentile(X_train[:, 0], [0, 25, 50, 75, 100]) 
array([ 4.3,  5.1,  5.8,  6.5,  7.9])

Этот признак соответствует длине чашелистика в см (из набора дата сета Iris plants dataset). После применения квантильного преобразования эти ориентиры близко приближаются к ранее определенным процентилям:

>>> np.percentile(X_train_trans[:, 0], [0, 25, 50, 75, 100])
... 
array([ 0.00... ,  0.24...,  0.49...,  0.73...,  0.99... ])

Это можно подтвердить на независимом наборе тестов с аналогичными замечаниями:

>>> np.percentile(X_test[:, 0], [0, 25, 50, 75, 100])
... 
array([ 4.4  ,  5.125,  5.75 ,  6.175,  7.3  ])
>>> np.percentile(X_test_trans[:, 0], [0, 25, 50, 75, 100])
... 
array([ 0.01...,  0.25...,  0.46...,  0.60... ,  0.94...])

6.3.2.2. Сопоставление с распределением Гаусса

Во многих сценариях моделирования желательна нормальность объектов в наборе данных. Степенные преобразования - это семейство параметрических монотонных преобразований, цель которых - сопоставить данные из любого распределения с распределением, максимально близким к гауссовскому, чтобы стабилизировать дисперсию и минимизировать асимметрию.

Класс PowerTransformer в настоящее время предоставляет два таких степенных преобразования: преобразование Йео-Джонсона (Yeo-Johnson) и преобразование Бокса-Кокса (Box-Cox).

Преобразование Йео-Джонсона определяется следующим образом:

\[\begin{split}x_i^{(\lambda)} = \begin{cases} [(x_i + 1)^\lambda - 1] / \lambda & \text{if } \lambda \neq 0, x_i \geq 0, \\[8pt] \ln{(x_i + 1)} & \text{if } \lambda = 0, x_i \geq 0 \\[8pt] -[(-x_i + 1)^{2 - \lambda} - 1] / (2 - \lambda) & \text{if } \lambda \neq 2, x_i < 0, \\[8pt] - \ln (- x_i + 1) & \text{if } \lambda = 2, x_i < 0 \end{cases}\end{split}\]

в то время как преобразование Бокса-Кокса определяется следующим образом:

\[\begin{split}x_i^{(\lambda)} = \begin{cases} \dfrac{x_i^\lambda - 1}{\lambda} & \text{if } \lambda \neq 0, \\[8pt] \ln{(x_i)} & \text{if } \lambda = 0, \end{cases}\end{split}\]

Бокс-Кокс может применяться только к строго положительным данным. В обоих методах преобразование параметризуется \(\lambda\), которое определяется посредством оценки максимального правдоподобия. Вот пример использования Box-Cox для сопоставления выборок, взятых из логарифмически нормального распределения, в нормальное распределение:

>>> pt = preprocessing.PowerTransformer(method='box-cox', standardize=False)
>>> X_lognormal = np.random.RandomState(616).lognormal(size=(3, 3))
>>> X_lognormal
array([[1.28..., 1.18..., 0.84...],
       [0.94..., 1.60..., 0.38...],
       [1.35..., 0.21..., 1.09...]])
>>> pt.fit_transform(X_lognormal)
array([[ 0.49...,  0.17..., -0.15...],
       [-0.05...,  0.58..., -0.57...],
       [ 0.69..., -0.84...,  0.10...]])

Хотя в приведенном выше примере для параметра standardize установлено значение False, PowerTransformer по умолчанию будет применять нормализацию с нулевым средним и единичной дисперсией к преобразованному выводу.

Ниже приведены примеры Бокса-Кокса и Йео-Джонсона, примененные к различным распределениям вероятностей. Обратите внимание, что при применении к определенным распределениям степенные преобразования дают результаты, очень похожие на гауссовы, но с другими они неэффективны. Это подчеркивает важность визуализации данных до и после преобразования.

../_images/sphx_glr_plot_map_data_to_normal_001.png

Также можно сопоставить данные с нормальным распределением, используя QuantileTransformer, установив output_distribution='normal'. Используя предыдущий пример с набором данных Ирис:

>>> quantile_transformer = preprocessing.QuantileTransformer(
...     output_distribution='normal', random_state=0)
>>> X_trans = quantile_transformer.fit_transform(X)
>>> quantile_transformer.quantiles_
array([[4.3, 2. , 1. , 0.1],
       [4.4, 2.2, 1.1, 0.1],
       [4.4, 2.2, 1.2, 0.1],
       ...,
       [7.7, 4.1, 6.7, 2.5],
       [7.7, 4.2, 6.7, 2.5],
       [7.9, 4.4, 6.9, 2.5]])

Таким образом, медиана входных данных становится средним значением выходных данных с центром в 0. Обычный выходной сигнал обрезается так, что минимум и максимум входных данных — соответствуют квантилям 1e-7 и 1 - 1e-7 соответственно — - не становитесь бесконечными при трансформации.

6.3.3. Нормализация

Нормализация – это процесс масштабирования отдельных образцов до единичной нормы. Этот процесс может быть полезен, если вы планируете использовать квадратичную форму, такую как скалярное произведение или любое другое ядро, для количественной оценки сходства любой пары образцов.

Это предположение лежит в основе Векторной пространственной модели, часто используемой в контексте классификации текста и кластеризации.

Функция normalize обеспечивает быстрый и простой способ выполнить эту операцию с одним массивом данных, используя либо нормы l1, l2 или max.

>>> from sklearn import preprocessing
>>> import numpy as np
>>> X = np.array([[ 1., -1.,  2.],
...      [ 2.,  0.,  0.],
...      [ 0.,  1., -1.]])
>>> X_normalized = preprocessing.normalize(X, norm='l2')

>>> X_normalized
array([[ 0.40..., -0.40...,  0.81...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 0.  ...,  0.70..., -0.70...]])

6.3.3.1. Описание расчета

Если используется l2-норма, то по каждой строке происходит рассчет:

\[l2_i = \sqrt{x_1^2+x_2^2+...+x_n^2} = \sqrt{\sum_{k=1}^{n} x_k^2}\]

где l2_i l2-норма для каждой строки.

После этого каждое значение строки делится на l2_i:

\[\frac{x_1}{l2_i},\frac{x_2}{l2_i},...,\frac{x_n}{l2_i}\]

Продолжая пример выше:

>>> L2 = ((X**2).sum(axis=1) ** 0.5).reshape(-1,1)
>>> L2
array([[2.44...],
       [2.     ],
       [1.41...]])
>>> X / L2
array([[ 0.40..., -0.40...,  0.81...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 0.  ...,  0.70..., -0.70...]])

Если используется l1-норма, то по каждой строке происходит рассчет:

\[l1_i = |x_1|+|x_2|+...+|x_n| = \sqrt{\sum_{k=1}^{n} |x_k|}\]

где l1_i l1-норма для каждой строки.

После этого каждое значение строки делится на l1_i:

\[\frac{x_1}{l1_i},\frac{x_2}{l1_i},...,\frac{x_n}{l1_i}\]

Продолжая пример выше:

>>> L1 = np.abs(X).sum(axis=1).reshape(-1,1)
>>> L1
array([[4.],
       [2.],
       [2.]])
>>> X / L1
array([[ 0.25, -0.25,  0.5 ],
       [ 1.  ,  0.  ,  0.  ],
       [ 0.  ,  0.5 , -0.5 ]])

Если используется max, то из каждой строки выбирается максимальное значение:

\[max_i = max(|x_1|,|x_2|,...,|x_n|)\]

где max_i максимальное значение для каждой строки.

После этого каждое значение строки делится на max_i:

\[\frac{x_1}{max_i},\frac{x_2}{max_i},...,\frac{x_n}{max_i}\]

Продолжая пример выше:

>>> max_val = X.max(axis=1).reshape(-1,1)
>>> max_val
array([[2.],
       [2.],
       [1.]])
>>> X / max_val
array([[ 0.5, -0.5,  1. ],
       [ 1. ,  0. ,  0. ],
       [ 0. ,  1. , -1. ]])

Модуль preprocessing дополнительно предоставляет служебный класс Normalizer, который реализует ту же операцию с использованием Transformer API (хотя метод fit в этом случае бесполезен: класс не имеет состояния, поскольку эта операция обрабатывает образцы независимо).

Следовательно, этот класс подходит для использования на ранних этапах Pipeline:

>>> normalizer = preprocessing.Normalizer().fit(X)  # fit does nothing
>>> normalizer
Normalizer()

Экземпляр нормализатора затем можно использовать для выборочных векторов как любой преобразователь:

>>> normalizer.transform(X)
array([[ 0.40..., -0.40...,  0.81...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 0.  ...,  0.70..., -0.70...]])

>>> normalizer.transform([[-1.,  1., 0.]])
array([[-0.70...,  0.70...,  0.  ...]])

Примечание. Нормализация L2 также известна как предварительная обработка пространственных знаков.

6.3.4. Кодирование категориальных признаков

Часто признаки задаются не как непрерывные значения, а как категориальные. Например, человек может иметь особенности ["male", "female"], ["from Europe", "from US", "from Asia"], ["uses Firefox", "uses Chrome", "uses Safari", "uses Internet Explorer"]. Такие признаки могут быть эффективно закодированы как целые числа, например ["male", "from US", "uses Internet Explorer"] могут быть выражены как [0, 1, 3] тогда ["female", "from Asia", "uses Chrome"] было бы [1, 2, 1].

Чтобы преобразовать категориальные признаки в такие целочисленные коды, мы можем использовать OrdinalEncoder. Эта модель преобразует каждый категориальный признак в один новый целочисленный признак (от 0 до n_categories - 1):

>>> enc = preprocessing.OrdinalEncoder()
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OrdinalEncoder()
>>> enc.transform([['female', 'from US', 'uses Safari']])
array([[0., 1., 1.]])

Такое целочисленное представление, однако, не может использоваться напрямую со всеми моделями scikit-learn, поскольку они ожидают непрерывного ввода и будут интерпретировать категории как упорядоченные, что часто нежелательно (т.е. набор браузеров был упорядочен произвольно).

По умолчанию OrdinalEncoder не обрабатывает пропущенные значения, указанные в np.nan.

>>> enc = preprocessing.OrdinalEncoder()
>>> X = [['male'], ['female'], [np.nan], ['female']]
>>> enc.fit_transform(X)
array([[ 1.],
       [ 0.],
       [nan],
       [ 0.]])

OrdinalEncoder предоставляет параметр encoded_missing_value для кодирования пропущенных значений без необходимости создания конвейера и использования SimpleImputer.

>>> enc = preprocessing.OrdinalEncoder(encoded_missing_value=-1)
>>> X = [['male'], ['female'], [np.nan], ['female']]
>>> enc.fit_transform(X)
array([[ 1.],
       [ 0.],
       [-1.],
       [ 0.]])

Вышеуказанная обработка эквивалентна следующему конвейеру:

>>> from sklearn.pipeline import Pipeline
>>> from sklearn.impute import SimpleImputer
>>> enc = Pipeline(steps=[
...     ("encoder", preprocessing.OrdinalEncoder()),
...     ("imputer", SimpleImputer(strategy="constant", fill_value=-1)),
... ])
>>> enc.fit_transform(X)
array([[ 1.],
       [ 0.],
       [-1.],
       [ 0.]])

Другая возможность преобразовать категориальные признаки в признаки, которые можно использовать с моделями scikit-learn, - это использовать кодирование “один из K”, также известное как “one-hot” или фиктивное кодирование. Этот тип кодирования можно получить с помощью OneHotEncoder, который преобразует каждый категориальный признак с возможными значениями n_categories в двоичные функции n_categories, причем один из них равен 1, а все остальные - 0.

Продолжая приведенный выше пример:

>>> enc = preprocessing.OneHotEncoder()
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OneHotEncoder()
>>> enc.transform([['female', 'from US', 'uses Safari'],
...                ['male', 'from Europe', 'uses Safari']]).toarray()
array([[1., 0., 0., 1., 0., 1.],
       [0., 1., 1., 0., 0., 1.]])

По умолчанию значения, которые может принимать каждый объект, автоматически выводятся из набора данных и могут быть найдены в атрибуте categories_:

>>> enc.categories_
[array(['female', 'male'], dtype=object), array(['from Europe', 'from US'], dtype=object), array(['uses Firefox', 'uses Safari'], dtype=object)]

Это можно указать явно, используя параметр categories. В нашем наборе данных есть два пола, четыре возможных континента и четыре веб-браузера:

>>> genders = ['female', 'male']
>>> locations = ['from Africa', 'from Asia', 'from Europe', 'from US']
>>> browsers = ['uses Chrome', 'uses Firefox', 'uses IE', 'uses Safari']
>>> enc = preprocessing.OneHotEncoder(categories=[genders, locations, browsers])
>>> # Note that for there are missing categorical values for the 2nd and 3rd
>>> # feature
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OneHotEncoder(categories=[['female', 'male'],
                          ['from Africa', 'from Asia', 'from Europe',
                           'from US'],
                          ['uses Chrome', 'uses Firefox', 'uses IE',
                           'uses Safari']])
>>> enc.transform([['female', 'from Asia', 'uses Chrome']]).toarray()
array([[1., 0., 0., 1., 0., 0., 1., 0., 0., 0.]])

Если существует вероятность того, что в обучающих данных могут отсутствовать категориальные характеристики, зачастую лучше указать handle_unknown='infrequent_if_exist' вместо установки categories вручную, как указано выше. Если указан handle_unknown='infrequent_if_exist' и во время преобразования встречаются неизвестные категории, ошибка не возникнет, но результирующие столбцы с one-hot кодированием для этим признаком будут содержать все нули или рассматриваться как нечастая категория, если она включена. (handle_unknown='infrequent_if_exist' поддерживается только для однократного кодирования):

>>> enc = preprocessing.OneHotEncoder(handle_unknown='infrequent_if_exist')
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OneHotEncoder(handle_unknown='infrequent_if_exist')
>>> enc.transform([['female', 'from Asia', 'uses Chrome']]).toarray()
array([[1., 0., 0., 0., 0., 0.]])

Также возможно закодировать каждый столбец в столбцы n_categories - 1 вместо столбцов n_categories, используя параметр drop. Этот параметр позволяет пользователю указать категорию для каждого удаляемого объекта. Это полезно, чтобы избежать коллинеарности входной матрицы в некоторых классификаторах. Такая функциональность полезна, например, при использовании нерегуляризованной регрессии (LinearRegression), поскольку коллинеарность приведет к тому, что ковариационная матрица станет необратимой:

>>> X = [['male', 'from US', 'uses Safari'],
...      ['female', 'from Europe', 'uses Firefox']]
>>> drop_enc = preprocessing.OneHotEncoder(drop='first').fit(X)
>>> drop_enc.categories_
[array(['female', 'male'], dtype=object), array(['from Europe', 'from US'], dtype=object),
 array(['uses Firefox', 'uses Safari'], dtype=object)]
>>> drop_enc.transform(X).toarray()
array([[1., 1., 1.],
       [0., 0., 0.]])

Возможно, вам захочется удалить один из двух столбцов только для объектов с двумя категориями. В этом случае вы можете установить параметр drop='if_binary'.

>>> X = [['male', 'US', 'Safari'],
...      ['female', 'Europe', 'Firefox'],
...      ['female', 'Asia', 'Chrome']]
>>> drop_enc = preprocessing.OneHotEncoder(drop='if_binary').fit(X)
>>> drop_enc.categories_
[array(['female', 'male'], dtype=object), array(['Asia', 'Europe', 'US'], dtype=object),
 array(['Chrome', 'Firefox', 'Safari'], dtype=object)]
>>> drop_enc.transform(X).toarray()
array([[1., 0., 0., 1., 0., 0., 1.],
       [0., 0., 1., 0., 0., 1., 0.],
       [0., 1., 0., 0., 1., 0., 0.]])

В преобразованном X первый столбец представляет собой кодировку признака с категориями “мужской”/”женский”, а остальные 6 столбцов представляют собой кодировку двух признаков с соответственно тремя категориями каждый.

Если handle_unknown='ignore' и drop не равны None, неизвестные категории будут закодированы как все нули:

>>> drop_enc = preprocessing.OneHotEncoder(drop='first',
...                                        handle_unknown='ignore').fit(X)
>>> X_test = [['unknown', 'America', 'IE']]
>>> drop_enc.transform(X_test).toarray()
array([[0., 0., 0., 0., 0.]])

Все категории в X_test неизвестны во время преобразования и будут сопоставлены всем нулям. Это означает, что неизвестные категории будут иметь то же сопоставление, что и удаленная категория. OneHotEncoder.inverse_transform сопоставит все нули с удаленной категорией, если категория удалена, и None, если категория не удалена:

>>> drop_enc = preprocessing.OneHotEncoder(drop='if_binary', sparse_output=False,
...                                        handle_unknown='ignore').fit(X)
>>> X_test = [['unknown', 'America', 'IE']]
>>> X_trans = drop_enc.transform(X_test)
>>> X_trans
array([[0., 0., 0., 0., 0., 0., 0.]])
>>> drop_enc.inverse_transform(X_trans)
array([['female', None, None]], dtype=object)

OneHotEncoder поддерживает категориальные функции с отсутствующими значениями, рассматривая отсутствующие значения как дополнительную категорию:

>>> X = [['male', 'Safari'],
...      ['female', None],
...      [np.nan, 'Firefox']]
>>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
>>> enc.categories_
[array(['female', 'male', nan], dtype=object),
 array(['Firefox', 'Safari', None], dtype=object)]
>>> enc.transform(X).toarray()
array([[0., 1., 0., 0., 1., 0.],
       [1., 0., 0., 0., 0., 1.],
       [0., 0., 1., 1., 0., 0.]])

Если объект содержит как np.nan, так и None, они будут считаться отдельными категориями:

>>> X = [['Safari'], [None], [np.nan], ['Firefox']]
>>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
>>> enc.categories_
[array(['Firefox', 'Safari', None, nan], dtype=object)]
>>> enc.transform(X).toarray()
array([[0., 1., 0., 0.],
       [0., 0., 1., 0.],
       [0., 0., 0., 1.],
       [1., 0., 0., 0.]])

См. Загрузка признаков из словаря для категориальных функций, которые представлены как dict, а не как скаляры.

6.3.4.1. Редкие категории (Infrequent categories)

OneHotEncoder и OrdinalEncoder поддерживают агрегирование редких категорий в единый вывод для каждого признака. Параметрами, позволяющими собирать редкие категории, являются min_frequency и max_categories.

  1. min_frequency - это либо целое число, большее или равное 1, либо число с плавающей точкой в интервале (0.0, 1.0). Если min_frequency - целое число, категории с мощностью меньше min_frequency будут считаться редкими. Если min_frequency является числом с плавающей точкой, категории с мощностью меньше этой доли от общего количества выборок будут считаться редкими. Значение по умолчанию - 1, что означает, что каждая категория кодируется отдельно.

  2. max_categories имеет значение None или любое целое число больше 1. Этот параметр устанавливает верхний предел количества выходных объектов для каждого входного объекта. max_categories включает признак, который объединяет редкие категории.

В следующем примере с OrdinalEncoder категории 'dog' и 'snake' считаются редко встречающимися:

>>> X = np.array([['dog'] * 5 + ['cat'] * 20 + ['rabbit'] * 10 +
...               ['snake'] * 3], dtype=object).T
>>> enc = preprocessing.OrdinalEncoder(min_frequency=6).fit(X)
>>> enc.infrequent_categories_
[array(['dog', 'snake'], dtype=object)]
>>> enc.transform(np.array([['dog'], ['cat'], ['rabbit'], ['snake']]))
array([[2.],
       [0.],
       [1.],
       [2.]])

Праметр max_categories класса OrdinalEncoder не учитывает отсутствующие или неизвестные категории. Установка unknown_value или encoded_missing_value в целое число увеличит количество уникальных целочисленных кодов на один каждый. Это может привести к целочисленным кодам до max_categories + 2. В следующем примере “a” и “d” считаются нечастыми и сгруппированы в одну категорию, “b” и “c” - это отдельные категории, неизвестные значения кодируются как 3, а отсутствующие значения кодируются как 4.

>>> X_train = np.array(
...     [["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 3 + [np.nan]],
...     dtype=object).T
>>> enc = preprocessing.OrdinalEncoder(
...     handle_unknown="use_encoded_value", unknown_value=3,
...     max_categories=3, encoded_missing_value=4)
>>> _ = enc.fit(X_train)
>>> X_test = np.array([["a"], ["b"], ["c"], ["d"], ["e"], [np.nan]], dtype=object)
>>> enc.transform(X_test)
array([[2.],
       [0.],
       [1.],
       [2.],
       [3.],
       [4.]])

Сходство, OneHotEncoder можно настроить для группировки редких категорий:

>>> enc = preprocessing.OneHotEncoder(min_frequency=6, sparse_output=False).fit(X)
>>> enc.infrequent_categories_
[array(['dog', 'snake'], dtype=object)]
>>> enc.transform(np.array([['dog'], ['cat'], ['rabbit'], ['snake']]))
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

При установке для handle_unknown значения 'infrequent_if_exist' неизвестные категории будут считаться редкими:

>>> enc = preprocessing.OneHotEncoder(
...    handle_unknown='infrequent_if_exist', sparse_output=False, min_frequency=6)
>>> enc = enc.fit(X)
>>> enc.transform(np.array([['dragon']]))
array([[0., 0., 1.]])

Метод OneHotEncoder.get_feature_names_out использует ‘infrequent’ в качестве редкого имени признака:

>>> enc.get_feature_names_out()
array(['x0_cat', 'x0_rabbit', 'x0_infrequent_sklearn'], dtype=object)

Когда для параметра handle_unknown установлено значение 'infrequent_if_exist' и при преобразовании встречается неизвестная категория:

  1. Если поддержка редких категорий не была настроена или во время обучения не было редкой категории, в результирующих столбцах с one-hot кодированием для этой функции будут все нули. При обратном преобразовании неизвестная категория будет обозначена как None.

  2. Если во время обучения присутствует редкая категория, неизвестная категория будет считаться редкой. В обратном преобразовании ‘infrequent_sklearn’ будет использоваться для представления редко встречающейся категории.

Редкие категории также можно настроить с помощью max_categories. В следующем примере мы устанавливаем max_categories=2, чтобы ограничить количество объектов в выходных данных. Это приведет к тому, что все категории, кроме категории 'cat', будут считаться редкими, что приведет к появлению двух признаков: один для категории 'cat' и один для редких категорий, которыми являются все остальные:

>>> enc = preprocessing.OneHotEncoder(max_categories=2, sparse_output=False)
>>> enc = enc.fit(X)
>>> enc.transform([['dog'], ['cat'], ['rabbit'], ['snake']])
array([[0., 1.],
       [1., 0.],
       [0., 1.],
       [0., 1.]])

Если оба значения max_categories и min_frequency не являются значениями по умолчанию, то категории сначала выбираются на основе min_frequency, а категории max_categories сохраняются. В следующем примере min_frequency=4 считает, что только snake встречается редко, но max_categories=3 заставляет dog также быть редким:

>>> enc = preprocessing.OneHotEncoder(min_frequency=4, max_categories=3, sparse_output=False)
>>> enc = enc.fit(X)
>>> enc.transform([['dog'], ['cat'], ['rabbit'], ['snake']])
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

Если на границе max_categories есть редкие категории с одинаковой мощностью, то первые max_categories берутся на основе порядка лексикона. В следующем примере “b”, “c” и “d” имеют одинаковую мощность, а при max_categories=2 “b” и “c” встречаются редко, поскольку имеют более высокий порядок словаря.

>>> X = np.asarray([["a"] * 20 + ["b"] * 10 + ["c"] * 10 + ["d"] * 10], dtype=object).T
>>> enc = preprocessing.OneHotEncoder(max_categories=3).fit(X)
>>> enc.infrequent_categories_
[array(['b', 'c'], dtype=object)]

6.3.4.2. Целевое кодирование (Target Encoder)

TargetEncoder использует целевое среднее значение, обусловленное категориальным признаком, для кодирования неупорядоченных категорий, то есть номинальных категорий [PAR] [MIC]. Эта схема кодирования полезна для категориальных признаков с высокой мощностью, где one-hot кодирование приведет к увеличению пространства признаков, что сделает его обработку более дорогой для последующей модели. Классическим примером категорий с высокой мощностью являются местоположение, например почтовый индекс или регион. Для цели двоичной классификации целевая кодировка определяется следующим образом:

\[S_i = \lambda_i\frac{n_{iY}}{n_i} + (1 - \lambda_i)\frac{n_Y}{n}\]

где \(S_i\) - кодировка категории \(i\), \(n_{iY}\) - количество наблюдений с \(Y=1\) и категорией \(i\), \(n_i\) - количество наблюдений с категорией \(i\), \(n_Y\) - количество наблюдений с \(Y=1\), \(n\) - количество наблюдений, а \(\lambda_i\) - коэффициент сжатия для категории \(i\). Коэффициент усадки определяется следующим образом:

\[\lambda_i = \frac{n_i}{m + n_i}\]

где \(m\) - коэффициент сглаживания, который контролируется параметром smooth в TargetEncoder. Большие коэффициенты сглаживания придадут больший вес глобальному среднему значению. Когда smooth="auto", коэффициент сглаживания вычисляется как эмпирическая оценка Байеса: \(m=\sigma_i^2/\tau^2\), где \(\sigma_i^2\) - дисперсия из y с категорией \(i\) и \(\tau^2\) - это глобальная дисперсия y.

Для целей многоклассовой классификации формулировка аналогична бинарной классификации:

\[S_{ij} = \lambda_i\frac{n_{iY_j}}{n_i} + (1 - \lambda_i)\frac{n_{Y_j}}{n}\]

где \(S_{ij}\) - кодировка категории \(i\) и класса \(j\), \(n_{iY_j}\) - количество наблюдений с :math: Y=j и категория \(i\), \(n_i\) - это количество наблюдений с категорией \(i\), \(n_{Y_j}\) - это количество наблюдений с: math:Y=j, \(n\) - количество наблюдений, а \(\lambda_i\) - коэффициент сжатия для категории \(i\).

Для непрерывных целей формулировка аналогична бинарной классификации:

\[S_i = \lambda_i\frac{\sum_{k\in L_i}Y_k}{n_i} + (1 - \lambda_i)\frac{\sum_{k=1}^{n}Y_k}{n}\]

где \(L_i\) - это набор наблюдений с категорией \(i\), а \(n_i\) - это количество наблюдений с категорией \(i\).

Метод fit_transform внутренне использует схему cross fitting для предотвращения утечки целевой информации в представление времени обучения, особенно для неинформативных категориальных переменных с высокой мощностью, и помогает предотвратить нисходящая модель от переобучения ложных корреляций. Обратите внимание, что в результате fit(X, y).transform(X) не равно fit_transform(X, y). В fit_transform обучающие данные разбиваются на k фолдов (определяемых параметром cv), и каждый фолд кодируется с использованием кодировок, полученных с использованием других фолдов k-1. На следующей диаграмме показана схема cross fitting в fit_transform со значением по умолчанию cv=5:

../_images/target_encoder_cross_validation.svg

Метод fit_transform также делает кодировку “полных данных”, используя весь обучающий набор. Он никогда не используется в fit_transform, но сохраняется в атрибуте encodings_ для использования при вызове transform. Обратите внимание, что кодировки, полученные для каждой фолда во время схемы cross fitting, не сохраняются в атрибуте.

Метод fit не использует какие-либо схемы cross fitting и изучает одну кодировку во всем обучающем наборе, которая используется для кодирования категорий в transform. Эта кодировка аналогична кодировке “полных данных”, получаемые в fit_transform.

Примечание

TargetEncoder рассматривает пропущенные значения, такие как np.nan или None, как другую категорию и кодирует их, как любую другую категорию. Категории, которые не отображаются во время “обучения”, кодируются целевым средним значением, т.е. target_mean_.

6.3.5. Дискретизация

Дискретизация (также известная как квантование или объединение) обеспечивает способ разделения непрерывного признака на дискретные значения. Определенные наборы данных с непрерывными признаками могут выиграть от дискретизации, поскольку дискретизация может преобразовать набор данных с непрерывными атрибутами в набор данных только с номинальными атрибутами.

Дискретные признаки с one-hot кодированием могут сделать модель более выразительной, сохраняя при этом интерпретируемость. Например, предварительная обработка с помощью дискретизатора может привести к нелинейности линейных моделей. Более продвинутые возможности, см. в разделе Генерация полиномиальных признаков ниже.

6.3.5.1. Дискретизация K-бинов

KBinsDiscretizer дискретизирует объекты в k-бинов:

>>> X = np.array([[ -3., 5., 15 ],
...               [  0., 6., 14 ],
...               [  6., 3., 11 ]])
>>> est = preprocessing.KBinsDiscretizer(n_bins=[3, 2, 2], encode='ordinal').fit(X)

По умолчанию вывод закодирован в разреженную матрицу (см. Кодирование категориальных признаков), и это можно настроить с помощью параметра encode. Для каждого объекта края интервала вычисляются во время обучения (fit) и вместе с количеством интервалов определяются бины. Следовательно, для текущего примера эти интервалы определяются как:

  • признак 1: \({[-\infty, -1), [-1, 2), [2, \infty)}\)

  • признак 2: \({[-\infty, 5), [5, \infty)}\)

  • признак 3: \({[-\infty, 14), [14, \infty)}\)

На основе этих бинов, X преобразуется следующим образом:

>>> est.transform(X)                      
array([[ 0., 1., 1.],
       [ 1., 1., 1.],
       [ 2., 0., 0.]])

Результирующий набор данных содержит порядковые атрибуты, которые в дальнейшем можно использовать в Pipeline.

Дискретизация аналогична построению гистограмм для непрерывных данных. Однако гистограммы фокусируются на подсчете объектов, которые попадают в определенные бины, тогда как дискретизация фокусируется на присвоении значений признаков этим бинам.

Класс KBinsDiscretizer реализует различные стратегии биннинга, которые можно выбрать с помощью параметра strategy. Стратегия ‘uniform’ использует интервалы постоянной ширины. Стратегия ‘quantile’ использует значения квантилей, чтобы иметь одинаково заполненные интервалы в каждом объекте. Стратегия ‘kmeans’ определяет интервалы на основе процедуры кластеризации k-средних, выполняемой для каждого объекта независимо.

Имейте в виду, что можно указать пользовательские интервалы, передав вызываемый объект, определяющий стратегию дискретизации, в FunctionTransformer. Например, мы можем использовать функцию Pandas pandas.cut:

>>> import pandas as pd
>>> import numpy as np
>>> from sklearn import preprocessing
>>>
>>> bins = [0, 1, 13, 20, 60, np.inf]
>>> labels = ['infant', 'kid', 'teen', 'adult', 'senior citizen']
>>> transformer = preprocessing.FunctionTransformer(
...     pd.cut, kw_args={'bins': bins, 'labels': labels, 'retbins': False}
... )
>>> X = np.array([0.2, 2, 15, 25, 97])
>>> transformer.fit_transform(X)
['infant', 'kid', 'teen', 'adult', 'senior citizen']
Categories (5, object): ['infant' < 'kid' < 'teen' < 'adult' < 'senior citizen']

6.3.5.2. Бинаризация признаков

Бинаризация объектов - это процесс порогового определения числовых признаков для получения логических значений. Это может быть полезно для последующих вероятностных оценок, которые исходят из предположения, что входные данные распределены в соответствии с многомерным распределением Бернулли. Например, это относится к BernoulliRBM.

В сообществе по обработке текста также распространено использование двоичных значений признаков (вероятно, для упрощения вероятностных рассуждений), даже если нормализованные подсчеты (так называемые частоты терминов - term frequencies) или признаков, оцениваемые TF-IDF, на практике часто работают немного лучше.

Что касается Normalizer, то служебный класс Binarizer предназначен для использования на ранних стадиях Pipeline. Метод fit ничего не делает, поскольку каждый образец обрабатывается независимо от других:

>>> X = [[ 1., -1.,  2.],
...      [ 2.,  0.,  0.],
...      [ 0.,  1., -1.]]
>>> binarizer = preprocessing.Binarizer().fit(X)  # fit does nothing
>>> binarizer
Binarizer()
>>> binarizer.transform(X)
array([[1., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.]])

Возможна настройка порога бинаризатора:

>>> binarizer = preprocessing.Binarizer(threshold=1.1)
>>> binarizer.transform(X)
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 0., 0.]])

Что касается класса Normalizer, модуль предварительной обработки предоставляет сопутствующую функцию binarize, которая будет использоваться, когда API преобразователя не требуется.

Обратите внимание, что Binarizer аналогичен KBinsDiscretizer, когда k = 2, и когда край интервала находится на значении threshold.

6.3.6. Восстановление пропущенных значений

Инструменты для восстановления пропущенных значений обсуждаются на странице Восстановление пропущенных значений.

6.3.7. Генерация полиномиальных признаков

Часто полезно усложнить модель, принимая во внимание нелинейные особенности входных данных. Мы показываем две возможности, обе из которых основаны на полиномах: первый использует чистые полиномы, второй использует сплайны, то есть кусочные полиномы.

6.3.7.1. Полиномиальные признаки

Простой и распространенный метод - это полиномиальные признаки, которые могут получать члены высшего порядка и взаимодействия функций. Это реализовано в PolynomialFeatures:

>>> import numpy as np
>>> from sklearn.preprocessing import PolynomialFeatures
>>> X = np.arange(6).reshape(3, 2)
>>> X
array([[0, 1],
       [2, 3],
       [4, 5]])
>>> poly = PolynomialFeatures(2)
>>> poly.fit_transform(X)
array([[ 1.,  0.,  1.,  0.,  0.,  1.],
       [ 1.,  2.,  3.,  4.,  6.,  9.],
       [ 1.,  4.,  5., 16., 20., 25.]])

Характеристики X были преобразованы из \((X_1, X_2)\) в \((1, X_1, X_2, X_1^2, X_1X_2, X_2^2)\).

В некоторых случаях требуются только условия взаимодействия между признаками, и это можно получить с помощью настройки interaction_only=True:

>>> X = np.arange(9).reshape(3, 3)
>>> X
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> poly = PolynomialFeatures(degree=3, interaction_only=True)
>>> poly.fit_transform(X)
array([[  1.,   0.,   1.,   2.,   0.,   0.,   2.,   0.],
       [  1.,   3.,   4.,   5.,  12.,  15.,  20.,  60.],
       [  1.,   6.,   7.,   8.,  42.,  48.,  56., 336.]])

Признаки X были преобразованы из \((X_1, X_2, X_3)\) в \((1, X_1, X_2, X_3, X_1X_2, X_1X_3, X_2X_3, X_1X_2X_3)\).

Обратите внимание, что полиномиальные функции неявно используются в ядерный методах (например, SVC, KernelPCA) при использовании полинома Функциональные ядра.

См. Polynomial and Spline interpolation для Ридж регрессии с использованием созданных полиномиальных признаков.

6.3.7.2. Сплайновый трансформатор (Spline transformer)

Другой способ добавить нелинейные члены вместо чистых полиномов объектов - создать базисные признаки сплайна для каждого объекта с помощью SplineTransformer. Сплайны представляют собой кусочные полиномы, параметризованные степенью полинома и положениями узлов. SplineTransformer реализует основу B-сплайна, см. ссылки ниже.

Примечание

SplineTransformer обрабатывает каждый признак отдельно, т.е. он не дает вам условий взаимодействия.

Некоторые преимущества сплайнов перед полиномами:

  • B-сплайны очень гибкие и надежные, если вы сохраняете фиксированную низкую степень, обычно 3, и экономно регулируете количество узлов. Полиномам потребуется более высокая степень, что приводит к следующему пункту.

  • B-сплайны не имеют колебательного поведения на границах, как полиномы (чем выше степень, тем хуже). Это известно как “феномен Рунге <https://ru.wikipedia.org/wiki/%D0%A4%D0%B5%D0%BD%D0%BE%D0%BC%D0%B5%D0%BD_%D0%A0%D1%83%D0%BD%D0%B3%D0%B5>”.

  • B-сплайны предоставляют хорошие возможности для экстраполяции за пределы границ, т.е. за пределы диапазона подобранных значений. Взгляните на опцию extrapolation.

  • B-сплайны создают матрицу признаков с полосатой структурой. Для одного объекта каждая строка содержит только ненулевые элементы “степень + 1”, которые встречаются последовательно и даже являются положительными. В результате получается матрица с хорошими числовыми свойствами, например низкое число обусловленности, что резко контрастирует с матрицей полиномов, которая называется матрица Вандермонда. Низкое число обусловленности важно для стабильных алгоритмов линейных моделей.

Следующий фрагмент кода показывает сплайны в действии:

>>> import numpy as np
>>> from sklearn.preprocessing import SplineTransformer
>>> X = np.arange(5).reshape(5, 1)
>>> X
array([[0],
       [1],
       [2],
       [3],
       [4]])
>>> spline = SplineTransformer(degree=2, n_knots=3)
>>> spline.fit_transform(X)
array([[0.5  , 0.5  , 0.   , 0.   ],
       [0.125, 0.75 , 0.125, 0.   ],
       [0.   , 0.5  , 0.5  , 0.   ],
       [0.   , 0.125, 0.75 , 0.125],
       [0.   , 0.   , 0.5  , 0.5  ]])

Поскольку X отсортирован, можно легко увидеть выходные данные полосовой матрицы. Только три средние диагонали отличны от нуля для степени=2 (degree=2). Чем выше степень, тем больше перекрытие сплайнов.

Интересно, что SplineTransformer для степени=0 аналогичен KBinsDiscretizer с encode='onehot-dense' и n_bins = n_knots - 1, если knots = strategy.

6.3.8. Кастомные Трансформаторы

Часто вам потребуется преобразовать существующую функцию Python в преобразователь, чтобы облегчить очистку или обработку данных. Вы можете реализовать преобразователь из произвольной функции с помощью FunctionTransformer. Например, чтобы создать преобразователь, который применяет логорифмическое преобразование в конвейере, выполните:

>>> import numpy as np
>>> from sklearn.preprocessing import FunctionTransformer
>>> transformer = FunctionTransformer(np.log1p, validate=True)
>>> X = np.array([[0, 1], [2, 3]])
>>> # Поскольку во время метода fit FunctionTransformer не работает, мы можем напрямую вызвать преобразование.
>>> transformer.transform(X)
array([[0.        , 0.69314718],
       [1.09861229, 1.38629436]])

Вы можете гарантировать, что func и inverse_func являются обратными друг другу, установив check_inverse=True и вызвав fit перед transform. Обратите внимание, что выдается предупреждение, которое можно превратить в ошибку с помощью filterwarnings:

>>> import warnings
>>> warnings.filterwarnings("error", message=".*check_inverse*.",
...                         category=UserWarning, append=False)

Полный пример кода, демонстрирующий использование FunctionTransformer для извлечения признаков из текстовых данных, см. в Column Transformer with Heterogeneous Data Sources и Time-related feature engineering.