Глава 5. Обработка категориальных данных
5.0 Введение
Часто полезно измерять объекты не с точки зрения их количества, а с точки зрения некоторого качества. Мы часто представляем качественную информацию в виде категорий, таких как пол, цвета или марка автомобиля. Однако не все категориальные данные одинаковы. Наборы категорий без внутренней упорядоченности называются номинальными. Примеры номинальных категорий включают:
Синий, Красный, Зеленый
Мужчина, Женщина
Банан, Клубника, Яблоко
Напротив, когда набор категорий имеет некоторую естественную упорядоченность, мы называем его ординальным. Например:
Низкий, Средний, Высокий
Молодой, Старый
Согласен, Нейтрален, Не согласен
Кроме того, категориальная информация часто представлена в данных в виде вектора или столбца строк (например, "Maine", "Texas", "Delaware"). Проблема в том, что большинство алгоритмов машинного обучения требуют числовых значений в качестве входных данных.
Алгоритм k-ближайших соседей является примером алгоритма, который требует числовых данных. Один из шагов в алгоритме — вычисление расстояний между наблюдениями — часто с использованием Евклидова расстояния:
Рисунок: Формула Евклидова расстояния
где x и y — два наблюдения, а индекс i обозначает значение для i-го признака наблюдений. Однако вычисление расстояния, очевидно, невозможно, если значение xi является строкой (например, "Texas"). Вместо этого нам нужно преобразовать строку в некоторый числовой формат, чтобы ее можно было ввести в уравнение Евклидова расстояния. Наша цель — преобразовать данные таким образом, чтобы правильно отразить информацию в категориях (ординальность, относительные интервалы между категориями и т. д.). В этой главе мы рассмотрим методы выполнения этого преобразования, а также преодоления других проблем, часто возникающих при обработке категориальных данных.
5.1 Кодирование номинальных категориальных признаков
Проблема
У вас есть признак с номинальными классами, который не имеет внутренней упорядоченности (например, яблоко, груша, банан), и вы хотите закодировать этот признак в числовые значения.
Решение
Однократное кодирование признака с использованием LabelBinarizer scikit-learn:
# Import librariesimport numpy as npfrom sklearn.preprocessing import LabelBinarizer, MultiLabelBinarizerCreate featurefeature = np.array([["Texas"],["California"],["Texas"],["Delaware"],["Texas"]])Create one-hot encoderone_hot = LabelBinarizer()One-hot encode featureone_hot.fit_transform(feature)
array([[0, 0, 1],[1, 0, 0],[0, 0, 1],[0, 1, 0],[0, 0, 1]])
Мы можем использовать атрибут classes_ для вывода классов:
# View feature classesone_hot.classes_
array(['California', 'Delaware', 'Texas'],dtype='
Если мы хотим отменить однократное кодирование, мы можем использовать inverse_transform:
# Reverse one-hot encodingone_hot.inverse_transform(one_hot.transform(feature))
array(['Texas', 'California', 'Texas', 'Delaware', 'Texas'],dtype='
Мы даже можем использовать pandas для однократного кодирования признака:
# Import libraryimport pandas as pdCreate dummy variables from featurepd.get_dummies(feature[:,0])
California | Delaware | Texas | |
---|---|---|---|
0 | 0 | 0 | 1 |
1 | 1 | 0 | 0 |
2 | 0 | 0 | 1 |
3 | 0 | 1 | 0 |
4 | 0 | 0 | 1 |
Одной из полезных возможностей scikit-learn является обработка ситуации, когда каждое наблюдение перечисляет несколько классов:
# Create multiclass featuremulticlass_feature = [("Texas", "Florida"),("California", "Alabama"),("Texas", "Florida"),("Delware", "Florida"),("Texas", "Alabama")]Create multiclass one-hot encoderone_hot_multiclass = MultiLabelBinarizer()One-hot encode multiclass featureone_hot_multiclass.fit_transform(multiclass_feature)
array([[0, 0, 0, 1, 1],[1, 1, 0, 0, 0],[0, 0, 0, 1, 1],[0, 0, 1, 1, 0],[1, 0, 0, 0, 1]])
Еще раз, мы можем увидеть классы с помощью метода classes_:
# View classesone_hot_multiclass.classes_
array(['Alabama', 'California', 'Delware', 'Florida', 'Texas'], dtype=object)
Обсуждение
Обсуждение
Мы можем подумать, что правильная стратегия — присвоить каждому классу числовое значение (например, Texas = 1, California = 2). Однако, когда наши классы не имеют внутренней упорядоченности (например, Texas не «меньше», чем California), наши числовые значения ошибочно создают упорядоченность, которой нет.
Правильная стратегия — создать бинарный признак для каждого класса в исходном признаке. Это часто называется однократным кодированием (one-hot encoding) в литературе по машинному обучению или даммированием (dummying) в статистической и исследовательской литературе. Признак в нашем решении был вектором, содержащим три класса (т.е. Texas, California и Delaware). При однократном кодировании каждый класс становится своим собственным признаком с 1, когда класс появляется, и 0 в противном случае. Поскольку наш признак имел три класса, однократное кодирование вернуло три бинарных признака (по одному для каждого класса). Используя однократное кодирование, мы можем зафиксировать принадлежность наблюдения к классу, сохраняя при этом представление о том, что класс не имеет какой-либо иерархии.
Наконец, часто рекомендуется после однократного кодирования признака удалить один из признаков, полученных при однократном кодировании, в результирующей матрице, чтобы избежать линейной зависимости.
См. также
См. также
Dummy Variable Trap, Algosome
Dropping one of the columns when using one-hot encoding, CrossValidated
5.2 Кодирование ординальных категориальных признаков
5.2 Кодирование ординальных категориальных признаков
Проблема
Проблема
У вас есть ординальный категориальный признак (например, высокий, средний, низкий), и вы хотите преобразовать его в числовые значения.
Решение
Решение
Используйте метод replace pandas DataFrame для преобразования строковых меток в числовые эквиваленты:
# Load libraryimport pandas as pdCreate featuresdataframe = pd.DataFrame({"Score": ["Low", "Low", "Medium", "Medium", "High"]})Create mapperscale_mapper = {"Low":1,"Medium":2,"High":3}Replace feature values with scaledataframe["Score"].replace(scale_mapper)
0 11 12 23 24 3Name: Score, dtype: int64
Обсуждение
Обсуждение
Часто у нас есть признак с классами, которые имеют некоторую естественную упорядоченность. Известный пример — шкала Лайкерта:
Полностью согласен
Согласен
Нейтрален
Не согласен
Полностью не согласен
При кодировании признака для использования в машинном обучении нам необходимо преобразовать ординальные классы в числовые значения, сохраняющие представление об упорядоченности. Самый распространенный подход — создать словарь, который сопоставляет строковую метку класса с числом, а затем применить эту карту к признаку.
Важно, чтобы наш выбор числовых значений основывался на нашей предварительной информации об ординальных классах. В нашем решении «высокий» буквально в три раза больше, чем «низкий». Это хорошо во многих случаях, но может сломаться, если предполагаемые интервалы между классами не равны:
dataframe = pd.DataFrame({"Score": ["Low","Low","Medium","Medium","High","Barely More Than Medium"]})scale_mapper = {"Low":1,"Medium":2,"Barely More Than Medium": 3,"High":4}dataframe["Score"].replace(scale_mapper)
0 11 12 23 24 45 3Name: Score, dtype: int64
В этом примере расстояние между «Низким» и «Средним» такое же, как и расстояние между «Средним» и «Едва больше, чем средний», что почти наверняка не точно. Лучший подход — осознанно подходить к числовым значениям, сопоставленным с классами:
scale_mapper = {"Low":1,"Medium":2,"Barely More Than Medium": 2.1,"High":3}dataframe["Score"].replace(scale_mapper)
0 1.01 1.02 2.03 2.04 3.05 2.1Name: Score, dtype: float64
5.3 Кодирование словарей признаков
5.3 Кодирование словарей признаков
Проблема
Проблема
У вас есть словарь, и вы хотите преобразовать его в матрицу признаков.
Решение
Решение
Используйте DictVectorizer:
# Import libraryfrom sklearn.feature_extraction import DictVectorizerCreate dictionarydata_dict = [{"Red": 2, "Blue": 4},{"Red": 4, "Blue": 3},{"Red": 1, "Yellow": 2},{"Red": 2, "Yellow": 2}]Create dictionary vectorizerdictvectorizer = DictVectorizer(sparse=False)Convert dictionary to feature matrixfeatures = dictvectorizer.fit_transform(data_dict)View feature matrixfeatures
array([[ 4., 2., 0.],[ 3., 4., 0.],[ 0., 1., 2.],[ 0., 2., 2.]])
По умолчанию DictVectorizer выводит разреженную матрицу, которая хранит только элементы со значением, отличным от 0. Это может быть очень полезно, когда у нас есть массивные матрицы (часто встречающиеся в обработке естественного языка), и мы хотим минимизировать требования к памяти. Мы можем заставить DictVectorizer выводить плотную матрицу, используя sparse=False.
Мы можем получить имена каждого сгенерированного признака с помощью метода get_feature_names:
# Get feature namesfeature_names = dictvectorizer.get_feature_names()View feature namesfeature_names
['Blue', 'Red', 'Yellow']
Хотя это не обязательно, для иллюстрации мы можем создать pandas DataFrame для лучшего просмотра вывода:
# Import libraryimport pandas as pdCreate dataframe from featurespd.DataFrame(features, columns=feature_names)
Blue | Red | Yellow | |
---|---|---|---|
0 | 4.0 | 2.0 | 0.0 |
1 | 3.0 | 4.0 | 0.0 |
2 | 0.0 | 1.0 | 2.0 |
3 | 0.0 | 2.0 | 2.0 |
Обсуждение
Обсуждение
Словарь является популярной структурой данных, используемой многими языками программирования; однако алгоритмы машинного обучения ожидают данные в виде матрицы. Мы можем добиться этого с помощью DictVectorizer scikit-learn.
Это распространенная ситуация при работе с обработкой естественного языка. Например, у нас может быть коллекция документов, и для каждого документа у нас есть словарь, содержащий количество раз, когда каждое слово появляется в документе. Используя DictVectorizer, мы можем легко создать матрицу признаков, где каждый признак — это количество раз, когда слово появляется в каждом документе:
# Create word counts dictionaries for four documentsdoc_1_word_count = {"Red": 2, "Blue": 4}doc_2_word_count = {"Red": 4, "Blue": 3}doc_3_word_count = {"Red": 1, "Yellow": 2}doc_4_word_count = {"Red": 2, "Yellow": 2}Create listdoc_word_counts = [doc_1_word_count,doc_2_word_count,doc_3_word_count,doc_4_word_count]Convert list of word count dictionaries into feature matrixdictvectorizer.fit_transform(doc_word_counts)
array([[ 4., 2., 0.],[ 3., 4., 0.],[ 0., 1., 2.],[ 0., 2., 2.]])
В нашем игрушечном примере есть только три уникальных слова (Red, Yellow, Blue), поэтому в нашей матрице только три признака; однако вы можете представить, что если каждый документ на самом деле был бы книгой в университетской библиотеке, наша матрица признаков была бы очень большой (и тогда мы захотели бы установить sparse в True).
См. также
См. также
How to use dictionaries in Python
SciPy Sparse Matrices
5.4 Импутация пропущенных значений класса
5.4 Импутация пропущенных значений класса
Проблема
Проблема
У вас есть категориальный признак, содержащий пропущенные значения, которые вы хотите заменить предсказанными значениями.
Решение
Решение
Идеальное решение — обучить алгоритм классификатора машинного обучения для предсказания пропущенных значений, обычно классификатор k-ближайших соседей (KNN):
# Load librariesimport numpy as npfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.impute import SimpleImputerfrom sklearn.preprocessing import StandardScalerfrom sklearn.datasets import make_blobsCreate feature matrix with categorical featureX = np.array([[0, 2.10, 1.45],[1, 1.18, 1.33],[0, 1.22, 1.27],[1, -0.21, -1.19]])Create feature matrix with missing values in the categorical featureX_with_nan = np.array([[np.nan, 0.87, 1.31],[np.nan, -0.67, -0.22]])Train KNN learnerclf = KNeighborsClassifier(3, weights='distance')trained_model = clf.fit(X[:,1:], X[:,0])Predict missing values' classimputed_values = trained_model.predict(X_with_nan[:,1:])Join column of predicted class with their other featuresX_with_imputed = np.hstack((imputed_values.reshape(-1,1), X_with_nan[:,1:]))Join two feature matricesnp.vstack((X_with_imputed, X))
array([[ 0. , 0.87, 1.31],[ 1. , -0.67, -0.22],[ 0. , 2.1 , 1.45],[ 1. , 1.18, 1.33],[ 0. , 1.22, 1.27],[ 1. , -0.21, -1.19]])
Альтернативное решение — заполнить пропущенные значения наиболее частым значением признака:
from sklearn.impute import SimpleImputerJoin the two feature matricesX_complete = np.vstack((X_with_nan, X))imputer = SimpleImputer(strategy='most_frequent')imputer.fit_transform(X_complete)
array([[ 0. , 0.87, 1.31],[ 0. , -0.67, -0.22],[ 0. , 2.1 , 1.45],[ 1. , 1.18, 1.33],[ 0. , 1.22, 1.27],[ 1. , -0.21, -1.19]])
Обсуждение
Обсуждение
Когда у нас есть пропущенные значения в категориальном признаке, наше лучшее решение — открыть наш набор инструментов алгоритмов машинного обучения для предсказания значений пропущенных наблюдений. Мы можем добиться этого, рассматривая признак с пропущенными значениями как целевой вектор, а остальные признаки — как матрицу признаков. Часто используемый алгоритм — KNN (подробно рассмотренный далее в этой книге), который присваивает пропущенному значению наиболее частый класс из k ближайших наблюдений.
Альтернативно, мы можем заполнить пропущенные значения наиболее частым классом признака. Хотя это менее изощренный метод, чем KNN, он гораздо более масштабируем для больших данных. В любом случае, рекомендуется включить бинарный признак, указывающий, содержат ли наблюдения импутированные значения.
См. также
См. также
Imputation of missing values with scikit-learn
Overcoming Missing Values in a Random Forest Classifier
A Study of K-Nearest Neighbour as an Imputation Method
5.5 Обработка несбалансированных классов
5.5 Обработка несбалансированных классов
Проблема
Проблема
У вас есть целевой вектор с сильно несбалансированными классами, и вы хотите внести коррективы, чтобы справиться с несбалансированностью классов.
Решение
Решение
Соберите больше данных. Если это невозможно, измените метрики, используемые для оценки вашей модели. Если это не работает, рассмотрите возможность использования встроенных параметров веса класса модели (если доступны), даунсэмплинга или апсэмплинга. Мы рассмотрим метрики оценки в более поздней главе, поэтому пока сосредоточимся на параметрах веса класса, даунсэмплинге и апсэмплинге.
Чтобы продемонстрировать наши решения, нам нужно создать некоторые данные с несбалансированными классами. Набор данных Ириса Фишера содержит три сбалансированных класса по 50 наблюдений каждый, указывающих вид цветка (Iris setosa, Iris virginica и Iris versicolor). Чтобы сделать набор данных несбалансированным, мы удаляем 40 из 50 наблюдений Iris setosa, а затем объединяем классы Iris virginica и Iris versicolor. Конечным результатом является бинарный целевой вектор, указывающий, является ли наблюдение цветком Iris setosa или нет. Результат — 10 наблюдений Iris setosa (класс 0) и 100 наблюдений не Iris setosa (класс 1):
# Load librariesimport numpy as npfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.datasets import load_irisLoad iris datairis = load_iris()Create feature matrixfeatures = iris.dataCreate target vectortarget = iris.targetRemove first 40 observationsfeatures = features[40:,:]target = target[40:]Create binary target vector indicating if class 0target = np.where((target == 0), 0, 1)Look at the imbalanced target vectortarget
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1])
Многие алгоритмы в scikit-learn предлагают параметр для взвешивания классов во время обучения, чтобы противодействовать эффекту их несбалансированности. Хотя мы еще не рассмотрели его, RandomForestClassifier — популярный алгоритм классификации, который включает параметр class_weight. Вы можете передать аргумент, явно указывающий желаемые веса классов:
# Create weightsweights = {0: .9, 1: 0.1}Create random forest classifier with weightsRandomForestClassifier(class_weight=weights)
RandomForestClassifier(class_weight={0: 0.9, 1: 0.1})
Или вы можете передать balanced, который автоматически создает веса, обратно пропорциональные частотам классов:
# Train a random forest with balanced class weightsRandomForestClassifier(class_weight="balanced")
RandomForestClassifier(class_weight='balanced')
Альтернативно, мы можем снизить выборку (downsample) мажоритарного класса или увеличить выборку (upsample) миноритарного класса. При снижении выборки мы случайно выбираем без замены из мажоритарного класса (т.е. класса с большим количеством наблюдений), чтобы создать новое подмножество наблюдений, равное по размеру миноритарному классу. Например, если миноритарный класс имеет 10 наблюдений, мы случайно выберем 10 наблюдений из мажоритарного класса и используем эти 20 наблюдений в качестве наших данных. Здесь мы делаем именно это, используя наши несбалансированные данные Ириса:
# Indicies of each class' observationsi_class0 = np.where(target == 0)[0]i_class1 = np.where(target == 1)[0]Number of observations in each classn_class0 = len(i_class0)n_class1 = len(i_class1)For every observation of class 0, randomly samplefrom class 1 without replacementi_class1_downsampled = np.random.choice(i_class1, size=n_class0, replace=False)Join together class 0's target vector with thedownsampled class 1's target vectornp.hstack((target[i_class0], target[i_class1_downsampled]))
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
# Join together class 0's upsampled feature matrix with class 1's feature matrixnp.vstack((features[i_class0_upsampled,:], features[i_class1,:]))[0:5]
array([[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.3, 1.4, 0.2],[ 4.5, 2.3, 1.3, 0.3],[ 4.8, 3. , 1.4, 0.3]])
Другой вариант — увеличить выборку миноритарного класса. При увеличении выборки для каждого наблюдения в мажоритарном классе мы случайно выбираем наблюдение из миноритарного класса с заменой. Конечным результатом является одинаковое количество наблюдений из миноритарного и мажоритарного классов. Увеличение выборки реализуется очень похоже на снижение выборки, только наоборот:
# For every observation in class 1, randomly sample from class 0 with replacementi_class0_upsampled = np.random.choice(i_class0, size=n_class1, replace=True)Join together class 0's upsampled target vector with class 1's target vectornp.concatenate((target[i_class0_upsampled], target[i_class1]))
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
# Join together class 0's upsampled feature matrix with class 1's feature matrixnp.vstack((features[i_class0_upsampled,:], features[i_class1,:]))[0:5]
array([[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.3, 1.4, 0.2],[ 4.5, 2.3, 1.3, 0.3],[ 4.8, 3. , 1.4, 0.3]])
Обсуждение
Обсуждение
В реальном мире несбалансированные классы встречаются повсеместно — большинство посетителей не нажимают кнопку покупки, и многие типы рака, к счастью, редки. По этой причине обработка несбалансированных классов является распространенной задачей в машинном обучении.
Наша лучшая стратегия — просто собрать больше наблюдений — особенно наблюдений миноритарного класса. Однако это часто невозможно, поэтому нам приходится прибегать к другим вариантам.
Вторая стратегия — использовать метрику оценки модели, лучше подходящую для несбалансированных классов. Точность часто используется в качестве метрики для оценки производительности модели, но при наличии несбалансированных классов точность может быть плохо подходящей. Например, если только у 0.5% наблюдений есть редкий рак, то даже наивная модель, которая предсказывает, что ни у кого нет рака, будет иметь точность 99.5%. Ясно, что это не идеальный вариант. Некоторые лучшие метрики, которые мы обсудим в более поздних главах, — это матрицы ошибок, точность (precision), полнота (recall), F1-метрика и ROC-кривые.
Третья стратегия — использовать параметры взвешивания классов, включенные в реализации некоторых моделей. Это позволяет алгоритму корректировать несбалансированные классы. К счастью, многие классификаторы scikit-learn имеют параметр class_weight, что делает его хорошим вариантом.
Четвертая и пятая стратегии связаны: снижение выборки (downsampling) и увеличение выборки (upsampling). При снижении выборки мы создаем случайное подмножество мажоритарного класса, равное по размеру миноритарному классу. При увеличении выборки мы многократно выбираем с заменой из миноритарного класса, чтобы сделать его равным по размеру мажоритарному классу. Решение между использованием снижения выборки и увеличения выборки зависит от контекста, и в целом мы должны попробовать оба варианта, чтобы увидеть, какой дает лучшие результаты.