8.3. Предварительная обработка данных
Пакет sklearn.preprocessing предоставляет несколько распространённых служебных функций и классов-преобразователей для преобразования исходных векторов признаков в представление, более подходящее для последующих оценивателей.
В целом многие алгоритмы обучения, например линейные модели, выигрывают от стандартизации набора данных (см. Важность масштабирования признаков). Если в наборе присутствуют выбросы, более подходящими могут оказаться устойчивые методы масштабирования или другие преобразователи. Поведение различных методов масштабирования, преобразователей и нормализаторов на наборе данных с умеренными выбросами показано в примере Сравнение влияния различных методов масштабирования на данные с выбросами.
8.3.1. Стандартизация, или удаление среднего и масштабирование дисперсии
Стандартизация наборов данных — это распространённое требование для многих оценивателей машинного обучения, реализованных в scikit-learn; они могут работать некорректно, если отдельные признаки не имеют распределения, в той или иной степени близкого к стандартному нормальному: гауссовскому с нулевым средним и единичной дисперсией.
На практике мы часто не учитываем форму распределения и просто преобразуем данные: центрируем их, вычитая среднее значение каждого признака, а затем масштабируем, деля неконстантные признаки на их стандартное отклонение.
Например, многие элементы целевой функции алгоритма обучения (такие как ядро RBF метода опорных векторов или регуляризаторы 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.267],
[-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.
8.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. ]])
Тот же экземпляр преобразователя можно применить к новым тестовым данным, которые не встречались при вызове fit: для согласованности с преобразованием обучающих данных будут применены те же операции масштабирования и сдвига:
>>> 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.])
8.3.1.2. Масштабирование разреженных данных
Центрирование разреженных данных разрушило бы их структуру разреженности, поэтому обычно это не имеет смысла. Однако масштабирование разреженных входных данных может быть полезным, особенно если признаки имеют разные масштабы.
MaxAbsScaler специально разработан для масштабирования разреженных данных и является рекомендуемым вариантом. Однако StandardScaler может принимать на вход матрицы scipy.sparse, если конструктору явно передано значение with_mean=False. В противном случае будет вызвано исключение ValueError: незаметное центрирование нарушило бы разреженность и часто приводило бы к сбою выполнения из-за непреднамеренного выделения чрезмерного объёма памяти. Для разреженных входных данных нельзя выполнить fit для RobustScaler, но можно использовать метод transform.
Обратите внимание, что средства масштабирования принимают данные в форматах Compressed Sparse Rows и Compressed Sparse Columns (см. scipy.sparse.csr_matrix и scipy.sparse.csc_matrix). Любые другие разреженные входные данные будут преобразованы в представление Compressed Sparse Rows. Чтобы избежать ненужного копирования в памяти, рекомендуется заранее выбрать представление CSR или CSC.
Наконец, если ожидается, что центрированные данные будут достаточно малы, можно явно преобразовать входные данные в массив с помощью метода toarray разреженных матриц.
8.3.1.3. Масштабирование данных с выбросами
Если данные содержат много выбросов, масштабирование с использованием среднего значения и дисперсии данных, вероятно, будет неэффективным. В таких случаях вместо него можно использовать RobustScaler. Он использует более устойчивые оценки центра и диапазона данных.
Ссылки
Дополнительное обсуждение важности центрирования и масштабирования данных приведено в разделе часто задаваемых вопросов: Следует ли нормализовать, стандартизировать или повторно масштабировать данные?
Масштабирование и отбеливание
Иногда недостаточно независимо центрировать и масштабировать признаки, поскольку последующая модель может дополнительно предполагать их линейную независимость.
Чтобы решить эту проблему, можно использовать PCA с параметром whiten=True для дополнительного устранения линейной корреляции между признаками.
8.3.1.4. Центрирование матриц ядер
Если у вас есть матрица ядра \(K\), вычисляющего скалярное произведение в пространстве признаков, которое (возможно, неявно) задано функцией \(\phi(\cdot)\), класс KernelCenterer может преобразовать матрицу ядра так, чтобы она содержала скалярные произведения в пространстве признаков, заданном \(\phi\), с последующим удалением среднего значения в этом пространстве. Иными словами, KernelCenterer вычисляет центрированную матрицу Грама, соответствующую положительно полуопределённому ядру \(K\).
Математическая формулировка
Теперь, когда у нас есть интуитивное понимание, рассмотрим математическую формулировку. Пусть \(K\) — матрица ядра формы (n_samples, n_samples), вычисленная по матрице данных \(X\) формы (n_samples, n_features) на этапе fit. Матрица \(K\) определяется следующим образом:
\(\phi(X)\) — функция, отображающая \(X\) в гильбертово пространство. Центрированное ядро \(\tilde{K}\) определяется следующим образом:
где \(\tilde{\phi}(X)\) получается центрированием \(\phi(X)\) в гильбертовом пространстве.
Таким образом, можно было бы вычислить \(\tilde{K}\), отобразив \(X\) с помощью функции \(\phi(\cdot)\) и центрировав данные в новом пространстве. Однако ядра часто используют потому, что они позволяют выполнять алгебраические вычисления, которые избавляют от необходимости явно вычислять это отображение с помощью \(\phi(\cdot)\). Действительно, центрирование можно выполнить неявно, как показано в приложении B в работе [Scholkopf1998]:
\(1_{\text{n}_{samples}}\) — матрица (n_samples, n_samples), все элементы которой равны \(\frac{1}{\text{n}_{samples}}\). На этапе transform ядро становится \(K_{test}(X, Y)\), определяемым следующим образом:
\(Y\) — тестовый набор данных формы (n_samples_test, n_features), поэтому \(K_{test}\) имеет форму (n_samples_test, n_samples). В этом случае центрирование \(K_{test}\) выполняется следующим образом:
\(1'_{\text{n}_{samples}}\) — матрица формы (n_samples_test, n_samples), все элементы которой равны \(\frac{1}{\text{n}_{samples}}\).
Ссылки
Б. Шёлькопф, А. Смола и К. Р. Мюллер, «Нелинейный компонентный анализ как задача на собственные значения ядра». Neural computation 10.5 (1998): 1299–1319.
8.3.2. Нелинейное преобразование
Доступны два типа преобразований: квантильные и степенные. И квантильные, и степенные преобразования основаны на монотонных преобразованиях признаков, поэтому сохраняют порядок значений для каждого признака.
Квантильные преобразования приводят все признаки к одному и тому же заданному распределению по формуле \(G^{-1}(F(X))\), где \(F\) — функция распределения признака, а \(G^{-1}\) — квантильная функция желаемого выходного распределения \(G\). Эта формула основана на следующих двух фактах: (i) если \(X\) — случайная величина с непрерывной функцией распределения \(F\), то \(F(X)\) равномерно распределена на \([0,1]\); (ii) если \(U\) — случайная величина с равномерным распределением на \([0,1]\), то \(G^{-1}(U)\) имеет распределение \(G\). Выполняя ранговое преобразование, квантильное преобразование сглаживает необычные распределения и меньше подвержено влиянию выбросов, чем методы масштабирования. Однако оно искажает корреляции и расстояния как внутри признаков, так и между ними.
Степенные преобразования — это семейство параметрических преобразований, предназначенных для преобразования данных из произвольного распределения в распределение, максимально близкое к гауссовскому.
8.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])
Этот признак соответствует длине чашелистика в сантиметрах. После применения квантильного преобразования эти контрольные точки близко соответствуют ранее заданным процентилям:
>>> 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])
8.3.2.2. Преобразование к гауссовскому распределению
Во многих сценариях моделирования желательно, чтобы признаки набора данных имели нормальное распределение. Степенные преобразования — это семейство параметрических монотонных преобразований, предназначенных для преобразования данных из произвольного распределения в максимально близкое к гауссовскому, чтобы стабилизировать дисперсию и минимизировать асимметрию.
PowerTransformer реализует два таких степенных преобразования: преобразование Йео — Джонсона и преобразование Бокса — Кокса.
Преобразование Йео — Джонсона
Преобразование Бокса — Кокса
Преобразование Бокса — Кокса можно применять только к строго положительным данным. В обоих методах преобразование задаётся параметром \(\lambda\), который определяется методом максимального правдоподобия. Ниже приведён пример использования преобразования Бокса — Кокса для преобразования выборок из логнормального распределения в нормальное:
>>> 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.388],
[1.35, 0.217, 1.09 ]])
>>> pt.fit_transform(X_lognormal)
array([[ 0.49 , 0.179, -0.156],
[-0.051, 0.589, -0.576],
[ 0.69 , -0.849, 0.101]])
В приведённом выше примере параметр standardize установлен в значение False, однако PowerTransformer по умолчанию нормализует преобразованные данные до нулевого среднего и единичной дисперсии.
Ниже приведены примеры применения преобразований Бокса — Кокса и Йео — Джонсона к различным распределениям вероятностей. Обратите внимание, что для некоторых распределений степенные преобразования дают результаты, очень близкие к гауссовским, но для других они неэффективны. Это подчёркивает важность визуализации данных до и после преобразования.
Данные также можно преобразовать к нормальному распределению с помощью QuantileTransformer, задав параметр output_distribution='normal'. Рассмотрим ранее приведённый пример с набором данных iris:
>>> 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 соответственно, не становились бесконечными после преобразования.
8.3.3. Нормализация
Нормализация — это процесс масштабирования отдельных выборок до единичной нормы. Этот процесс может быть полезен, если вы планируете использовать квадратичную форму, например скалярное произведение, или любое другое ядро для оценки сходства между парами выборок.
Это предположение лежит в основе векторной модели пространства, часто используемой в задачах классификации и кластеризации текстов.
Функция normalize позволяет быстро и просто выполнить эту операцию над отдельным массивоподобным набором данных, используя нормы l1, l2 или max:
>>> X = [[ 1., -1., 2.],
... [ 2., 0., 0.],
... [ 0., 1., -1.]]
>>> X_normalized = preprocessing.normalize(X, norm='l2')
>>> X_normalized
array([[ 0.408, -0.408, 0.812],
[ 1. , 0. , 0. ],
[ 0. , 0.707, -0.707]])
Модуль preprocessing также предоставляет служебный класс Normalizer, который реализует ту же операцию с использованием API Transformer (хотя в этом случае метод fit бесполезен: класс не хранит состояние, поскольку эта операция обрабатывает выборки независимо друг от друга).
Поэтому этот класс подходит для использования на ранних этапах Pipeline:
>>> normalizer = preprocessing.Normalizer().fit(X) # fit does nothing >>> normalizer Normalizer()
Затем экземпляр нормализатора можно использовать для векторов выборок, как любой преобразователь:
>>> normalizer.transform(X)
array([[ 0.408, -0.408, 0.812],
[ 1. , 0. , 0. ],
[ 0. , 0.707, -0.707]])
>>> normalizer.transform([[-1., 1., 0.]])
array([[-0.707, 0.707, 0.]])
Примечание. Нормализация L2 также известна как предварительная обработка пространственным знаком.
Разреженные входные данные
normalize и Normalizer принимают на вход как плотные массивоподобные данные, так и разреженные матрицы из scipy.sparse.
Разреженные входные данные преобразуются в представление Compressed Sparse Rows (см. scipy.sparse.csr_matrix), прежде чем передаваться эффективным процедурам Cython. Чтобы избежать ненужного копирования в памяти, рекомендуется заранее выбрать представление CSR.
8.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' поддерживается только для one-hot-кодирования):
>>> 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 столбцов — кодирование двух признаков, каждый из которых имеет по 3 категории.
Если 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.]])
См. раздел Загрузка признаков из словарей, посвящённый категориальным признакам, представленным в виде словаря, а не скалярных значений.
8.3.4.1. Редкие категории
OneHotEncoder и OrdinalEncoder поддерживают объединение редких категорий в одну выходную категорию для каждого признака. Параметры для включения объединения редких категорий: min_frequency и max_categories.
-
min_frequency— это либо целое число, большее или равное 1, либо число с плавающей точкой в интервале(0.0, 1.0). Еслиmin_frequency— целое число, категории с количеством значений меньшеmin_frequencyсчитаются редкими. Еслиmin_frequency— число с плавающей точкой, редкими считаются категории, количество значений которых меньше этой доли от общего числа образцов. Значение по умолчанию — 1, то есть каждая категория кодируется отдельно. -
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' и при преобразовании встречается неизвестная категория:
- Если поддержка редких категорий не настроена или при обучении не было редких категорий, соответствующие one-hot-столбцы этого признака будут состоять из нулей. При обратном преобразовании неизвестная категория будет обозначена как
None. - Если при обучении была редкая категория, неизвестная категория будет считаться редкой. При обратном преобразовании для обозначения редкой категории будет использоваться ‘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)]
8.3.4.2. Кодировщик целевой переменной
TargetEncoder использует среднее целевого значения, обусловленное категориальным признаком, для кодирования неупорядоченных категорий, то есть номинальных категорий [PAR] [MIC]. Эта схема кодирования полезна для категориальных признаков с высокой кардинальностью, где one-hot-кодирование значительно увеличило бы пространство признаков, усложнив обработку для последующей модели. Классический пример категорий с высокой кардинальностью — географические данные, такие как почтовый индекс или регион.
Целевые переменные для бинарной классификации
Для целевой переменной бинарной классификации кодирование целевой переменной задаётся формулой:
где \(S_i\) — кодирование категории \(i\), \(n_{iY}\) — число наблюдений с \(Y=1\) и категорией \(i\), \(n_i\) — число наблюдений с категорией \(i\), \(n_Y\) — число наблюдений с \(Y=1\), \(n\) — число наблюдений, а \(\lambda_i\) — коэффициент сжатия для категории \(i\). Коэффициент сжатия задаётся формулой:
где \(m\) — коэффициент сглаживания, управляемый параметром smooth в TargetEncoder. При больших значениях коэффициента сглаживания больший вес придаётся глобальному среднему. Если smooth="auto", коэффициент сглаживания вычисляется как эмпирическая оценка Байеса: \(m=\sigma_i^2/\tau^2\), где \(\sigma_i^2\) — дисперсия y для категории \(i\), а \(\tau^2\) — глобальная дисперсия y.
Целевые переменные для многоклассовой классификации
Для целевых переменных многоклассовой классификации формула аналогична формуле для бинарной классификации:
где \(S_{ij}\) — кодирование категории \(i\) и класса \(j\), \(n_{iY_j}\) — число наблюдений с \(Y=j\) и категорией \(i\), \(n_i\) — число наблюдений с категорией \(i\), \(n_{Y_j}\) — число наблюдений с \(Y=j\), \(n\) — число наблюдений, а \(\lambda_i\) — коэффициент сжатия для категории \(i\).
Непрерывные целевые переменные
Для непрерывных целевых переменных формула аналогична формуле для бинарной классификации:
где \(L_i\) — множество наблюдений с категорией \(i\), а \(n_i\) — число наблюдений с категорией \(i\).
Примечание
В TargetEncoder fit(X, y).transform(X) не равно fit_transform(X, y).
fit_transform использует схему кросс-фиттинга, чтобы предотвратить утечку информации о целевой переменной в представление, используемое при обучении. Это особенно важно для неинформативных категориальных переменных с высокой кардинальностью (признаков с большим количеством уникальных категорий, каждая из которых встречается лишь несколько раз), и помогает предотвратить переобучение последующей модели на ложных корреляциях. В методе fit_transform обучающие данные разделяются на k фолдов (число задаётся параметром cv), и каждый фолд кодируется с использованием кодировок, полученных на остальных k-1 фолдах. Поэтому обучающие данные всегда следует обучать и преобразовывать с помощью fit_transform(X_train, y_train).
На этой схеме показан процесс кросс-фиттинга в методе fit_transform со значением cv=5 по умолчанию:
Метод fit не использует схемы кросс-фиттинга и обучает одну кодировку на всём обучающем наборе. Использовать этот метод не рекомендуется, поскольку, как сказано выше, он может привести к утечке данных. Вместо него используйте fit_transform.
Во время вызова fit_transform кодировщик обучает кодировки категорий на всех обучающих данных и сохраняет их в атрибуте encodings_. Промежуточные кодировки, полученные для каждого фолда в процессе кросс-фиттинга, являются временными и не сохраняются. Затем сохранённые кодировки можно использовать для преобразования тестовых данных с помощью encoder.transform(X_test).
Примечание
TargetEncoder рассматривает пропущенные значения, такие как np.nan или None, как отдельную категорию и кодирует их так же, как любую другую категорию. Категории, которые не встречались во время fit, кодируются средним целевым значением, то есть target_mean_.
Примеры
- Сравнение кодировщика целевой переменной с другими кодировщиками
- Внутренний кросс-фиттинг кодировщика целевой переменной
Список литературы
8.3.5. Дискретизация
Дискретизация (также известная как квантование или разбиение на интервалы) позволяет разбивать непрерывные признаки на дискретные значения. Некоторые наборы данных с непрерывными признаками могут выиграть от дискретизации, поскольку она позволяет преобразовать набор данных с непрерывными атрибутами в набор, содержащий только номинальные атрибуты.
Дискретизированные признаки, закодированные методом one-hot, могут повысить выразительность модели, сохранив при этом интерпретируемость. Например, предварительная обработка с помощью дискретизатора может добавить нелинейность линейным моделям. Дополнительные возможности, в частности гладкие преобразования, см. ниже в разделе Генерация полиномиальных признаков.
8.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)
По умолчанию выходные данные кодируются методом one-hot в разреженную матрицу (см. раздел Кодирование категориальных признаков); этот параметр можно настроить с помощью параметра 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, str): ['infant' < 'kid' < 'teen' < 'adult' < 'senior citizen']
Примеры
8.3.5.2. Бинаризация признаков
Бинаризация признаков — это процесс порогового преобразования числовых признаков в логические значения. Это может быть полезно для последующих вероятностных оценивателей, предполагающих, что входные данные распределены согласно многомерному распределению Бернулли. Например, это относится к BernoulliRBM.
В сообществе, занимающемся обработкой текста, также распространено использование бинарных значений признаков (вероятно, для упрощения вероятностных рассуждений), хотя на практике нормализованные частоты (также известные как частоты терминов) или признаки со значениями 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.
8.3.6. Заполнение пропущенных значений
Инструменты для заполнения пропущенных значений рассматриваются в разделе Заполнение пропущенных значений.
8.3.7. Генерация полиномиальных признаков
Часто бывает полезно повысить сложность модели, добавив нелинейные признаки исходных данных. Мы рассмотрим два варианта, основанных на полиномах: первый использует обычные полиномы, а второй — сплайны, то есть кусочно-полиномиальные функции.
8.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) при использовании полиномиальных ядерных функций.
См. раздел Полиномиальная и сплайновая интерполяция, где показана регрессия Риджа с использованием созданных полиномиальных признаков.
8.3.7.2. Сплайновый преобразователь
Ещё один способ добавить нелинейные члены вместо обычных полиномов признаков — сгенерировать базисные функции сплайнов для каждого признака с помощью SplineTransformer. Сплайны — это кусочно-полиномиальные функции, задаваемые степенью полинома и положениями узлов. SplineTransformer реализует базис B-сплайнов; см. приведённые ниже источники.
Примечание
SplineTransformer обрабатывает каждый признак отдельно, то есть не создаёт взаимодействий между признаками.
Некоторые преимущества сплайнов перед полиномами:
- B-сплайны очень гибки и устойчивы, если сохранять небольшую фиксированную степень, обычно 3, и разумно подбирать число узлов. Для полиномов потребовалась бы более высокая степень, что приводит к следующему пункту.
- На границах B-сплайны не демонстрируют осцилляций, характерных для полиномов (чем выше степень, тем сильнее осцилляции). Это явление известно как феномен Рунге.
- B-сплайны обеспечивают хорошие возможности экстраполяции за пределы границ, то есть за пределы диапазона обучающих значений. См. параметр
extrapolation. - B-сплайны создают матрицу признаков с ленточной структурой. Для одного признака каждая строка содержит только
degree + 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 отсортирован, ленточную структуру выходной матрицы легко увидеть. Для degree=2 ненулевыми являются только три средние диагонали. Чем выше степень, тем больше перекрываются сплайны.
Интересно, что SplineTransformer с параметром degree=0 эквивалентен KBinsDiscretizer с параметрами encode='onehot-dense' и n_bins = n_knots - 1, если knots = strategy.
Примеры
Литература
- Eilers, P., & Marx, B. (1996). Гладкое сглаживание с помощью B-сплайнов и штрафных функций. Statist. Sci. 11 (1996), no. 2, 89–121.
- Perperoglou, A., Sauerbrei, W., Abrahamowicz, M. et al. Обзор процедур использования сплайновых функций в R. BMC Med Res Methodol 19, 46 (2019).
8.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]])
>>> # Since FunctionTransformer is no-op during fit, we can call transform directly
>>> 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 для извлечения признаков из текстовых данных, см. в разделах Преобразователь столбцов для разнородных источников данных и Создание признаков, связанных со временем.
© 2007–2026 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.9/modules/preprocessing.html