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

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

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

Если я что-то пропустил, укажите это в комментариях, и я обновлю пост с упоминанием.

В этом посте я использую следующие наборы данных:

Я загрузил все в репозиторий GitHub: https://github.com/michaelabehsera/feature-engineering-cookbook

Начиная

%matplotlib inline
import pandas as pd
import numpy as np
import missingno as msno
from numpy import random
import matplotlib.pyplot as plt

1. Замена значений NaN (или NULL)

Давайте посмотрим, сколько значений NULL содержится в каждом столбце. Кажется, что "Age" и "Cabin" чаще всего не имеют значения. Мы сосредоточимся на замене нулевых значений для Age.

df.isnull().sum()
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64

Среднее / среднее значение

df['Age'].fillna((df['Age'].mean()), inplace=True)
df['Age'].fillna((df['Age'].median()), inplace=True)
print('Now Age has {} null values.'.format(df.isnull().sum()['Age']))

Замена на 0 или -1

df['Age'].fillna(value=0, inplace=True)

Замена случайным числом. Вменение случайной выборки

Мы можем заменить возраст случайными числами от 1 до 100 следующим образом.

null_rows = df['Age'].isnull()
num_null_rows = sum(null_rows)
rand = random.randint(1, 101, size=num_null_rows)
df.loc[null_rows, 'Age'] = rand
df['Age'].plot.hist(title='Distribution of Age - replace null with random value')
plt.show()

Более разумным подходом было бы заменить Age случайными выборками из ненулевого распределения Age (как и при предыдущем подходе, мы бы сгенерировали столько же 99-летних, сколько 25-летних).

rand = np.random.choice(df.loc[~null_rows, 'Age'], replace=True, size=num_null_rows)
df.loc[null_rows, 'Age'] = rand
df['Age'].plot.hist(title='Distribution of Age - replace null with samples from distribution of Age')
plt.show()
print('Note the lack of 100 year olds in the above plot compared to the previous plot.')

Указание на отсутствие

Мы также могли бы использовать дополнительную переменную 0/1, чтобы указать нашей модели, когда возраст отсутствует.

df['Age_Missing'] = np.where(df['Age'].isnull(), 1, 0)

Вменение NA по ценностям в конце распределения

df['Age'].fillna(df.Age.mean() + df.Age.std() * 3, inplace=True)

Замена ценностями вашего выбора на основе предположения.

df['Age'].fillna(value='My Unique Value', inplace=True)

Использование регрессии для вменения недостающих значений атрибутов

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

from sklearn.linear_model import LinearRegression
numeric_vars = ['Pclass', 'SibSp', 'Parch', 'Fare']
null_rows = df[numeric_vars + ['Age']].isnull().any(1)  # rows where Age or any feature var. is null

Подобрать регрессионную модель к ненулевым строкам данных, спрогнозировать нулевые строки.

lr = LinearRegression()
lr.fit(df.loc[~null_rows, numeric_vars], df.loc[~null_rows, 'Age'])
df.loc[null_rows, 'Age'] = lr.predict(df.loc[null_rows, numeric_vars])

2. Масштабирование функций

Стандартный скалер

from sklearn.preprocessing import StandardScaler
x = StandardScaler().fit_transform(x)

Скалер MinMax

from sklearn.preprocessing import MinMaxScaler
x = MinMaxScaler().fit_transform(x)

Надежный скалер

from sklearn.preprocessing import RobustScaler
x = RobustScaler().fit_transform(x)

3. Технические выбросы в числовых переменных

Среднее / медианное вменение или случайная выборка

Если у нас есть основания полагать, что выбросы вызваны механической ошибкой или проблемами во время измерения. Это означает, что если выбросы по своей природе похожи на отсутствующие данные, то для замены выбросов можно применить любой из методов, обсуждаемых для отсутствующих данных. Поскольку количество выбросов по своей природе невелико (в противном случае они не были бы выбросами), разумно использовать условное исчисление среднего / медианного значения для их замены.

Выявление выбросов с помощью квантилей

q25 = df['Age'].quantile(0.25)
q75 = df['Age'].quantile(0.75)
IQR = q75 - q25
    
# Any value higher than ulimit or below llimit is an outlier
ulimit = q75 + 1.5*IQR
llimit = q25 - 1.5*IQR
print(ulimit, llimit, 'are the ulimit and llimit')
print('Imply Age outliers:')
df['Age'][np.bitwise_or(df['Age'] > ulimit, df['Age'] < llimit)]

Определите выбросы со средним значением

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

ulimit = np.mean(df['Age']) + 3 * np.std(df['Age'])
llimit = np.mean(df['Age']) - 3 * np.std(df['Age'])
ulimit, llimit
#out: (73.248081099510756, -13.849845805393123)

Дискретность

# Re-read data and fill nulls with mean (could use other null-filling method)
df = pd.read_csv('train.csv')
not_null = ~df['Age'].isnull()
# Get the bin edges using np.histogram
num_bins = 10
_, bin_edges = np.histogram(df['Age'][not_null], bins=num_bins)
# Optionally create labels
# labels = ['Bin_{}'.format(i) for i in range(1, len(intervals))]
labels = [i for i in range(num_bins)]
# Create new feature with pd.cut
df['discrete_Age'] = pd.cut(df['Age'], bins=bin_edges, labels=labels, include_lowest=True)

Обрезка

# Let's first remove any missing values
df['Age'].fillna((df['Age'].mean()), inplace=True)
# Get the outlier values
index_of_high_age = df[df.Age > 70].index
# Drop them
df = df.drop(index_of_high_age, axis=0)

Винсоризация (верхнее кодирование нижнее кодирование)

# Get the value of the 99th percentile
ulimit = np.percentile(df.Age.values, 99)
# Get the value of the 1st percentile (bottom 1%)
llimit = np.percentile(df.Age.values, 1)
# Create a copy of the age variable
df['Age_truncated'] = df.Age.copy()
# Replace all values above ulimit with value of ulimit
df.loc[df.Age > ulimit, 'Age_truncated'] = ulimit
# Replace all values below llimit with value of llimit
df.loc[df.Age < llimit, 'Age_truncated'] = llimit

Преобразование ранга (когда расстояние не так важно)

from scipy.stats import rankdata
# This is like sorting the variable and then assigning an index starting from 1 to each value
rankdata(df['Age'], method='dense')

4. Инженерные ярлыки, категориальные переменные

One-Hot-Encoding и Pandas получают манекены

Быстрое кодирование

from sklearn.preprocessing import OneHotEncoder

one = OneHotEncoder(sparse=False).fit_transform(df[['Parch']])
#Convert transformed column from numpy to a DataFrame and merge new column with older one.
onecol = pd.DataFrame(one)
results = pd.merge(onecol, df, left_index=True, right_index=True)

Получите манекены

# Generally, pd.get_dummies is an easier approach to one-hot encoding
pd.get_dummies(df['Sex']).head()

Выпадение первым

В моделях, которые используют все функции одновременно (большинство моделей, кроме моделей ансамбля деревьев и т. они мужские, поэтому мы можем отбросить женский).

df['Male'] = pd.get_dummies(df['Sex'], drop_first=True)

Среднее кодирование

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

# Calculate the mean encoding
means_pclass = df[['Survived']].groupby(df['Pclass']).apply(np.mean)
means_pclass.columns = ['Mean Encoding']
means_pclass
# Merge the encoding into our dataframe (by matching Pclass to the index of our prob. ratio dataframe)
df = pd.merge(df, means_pclass, left_on=df.Pclass, right_index=True)

Кодирование отношения вероятности

def probability_ratio(x):
    probability_eq_1 = np.mean(x)
    probability_eq_0 = 1.0 - probability_eq_1
    return probability_eq_1 / probability_eq_0
# Calculate the probability ratio encoding
prob_ratios_pclass = df[['Survived']].groupby(df['Pclass']).apply(lambda x: probability_ratio(x))
prob_ratios_pclass.columns = ['Prob Ratio Encoding']
prob_ratios_pclass
# Merge the encoding into our dataframe (by matching Pclass to the index of our prob. ratio dataframe)
df = pd.merge(df, prob_ratios_pclass, left_on=df.Pclass, right_index=True)
df.head()

Кодировка веса доказательств

def weight_of_evidence(x):
    probability_eq_1 = np.mean(x)
    probability_eq_0 = 1.0 - probability_eq_1
    return np.log(probability_eq_1 / probability_eq_0)
# Calculate the probability ratio encoding
woe_pclass = df[['Survived']].groupby(df['Pclass']).apply(lambda x: weight_of_evidence(x))
woe_pclass.columns = ['WOE Encoding']
woe_pclass
df = pd.merge(df, woe_pclass, left_on=df.Pclass, right_index=True)

Кодировка метки

Коды каталогов

# You can use cat.codes to convert the variable into a binary one
df['Sex'] = df['Sex'].astype('category')
# Cat.codes only works if the dtype is 'category'
df['Sex'].cat.codes.head()

Факторизация

Также достигается тот же конец, что и `cat.codes`, но дает нам ярлыки для каждой категории.

label, val = pd.factorize(df['Sex'])
df['IsFemale'] = label

Двоичное кодирование

# Можно также использовать np.where, чтобы указать, какие значения должны быть 1 или 0, как показано ниже

df['Sex_Binary'] = np.where(df['Sex'].isin(['Male','Female']), 1, 0)

5. Технические даты

Создание столбцов на основе часов / минут…

df = pd.read_csv('news_sample.csv')
time = pd.to_datetime(df['time'])

Панды поставляются с упакованными свойствами DateTime, которые вы можете проверить здесь: https://pandas.pydata.org/pandas-docs/stable/api.html#datetimelike-properties. Вы даже можете получить столбец с микросекундами. Вот некоторые из них, которые я использую чаще всего.

  • Месяц
  • Мин.
  • Секунды
  • Четверть
  • Семестр
  • День (число)
  • День недели
  • Hr
df[‘Month’] = time.dt.month
df[‘Day’] = time.dt.day
df[‘Hour’] = time.dt.hour
df[‘Minute’] = time.dt.minute
df[‘Seconds’] = time.dt.second

Создание столбца isweekend

df['is_weekend'] = np.where(df['Day'].isin([5,6]), 1, 0)

6. Инженерные смешанные переменные

Мы видели, что смешанные переменные - это переменные, значения которых содержат как числа, так и метки.

Как мы можем спроектировать переменные этого типа для использования в машинном обучении?

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

Здесь особо не о чем рассказывать, кроме одного фальшивого примера.

data = ['Apple', 'Banana', '2', '6']
lst_strings = []
lst_int = []
for i in data:
    if i == 'Apple':
        lst_strings.append(i)
    elif i == 'Banana':
        lst_strings.append(i)
    if i == '2':
        lst_int.append(int(i))
    elif i == '6':
        lst_int.append(int(i))

7. Разработка редких меток в категориальных переменных

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

Эти наблюдения можно разделить на следующие категории:

  • Замена редкой метки самой частой меткой
  • Группировка наблюдений, показывающих редкие ярлыки, в уникальную категорию (с новым ярлыком, например "Редкий" или "Другой").

Замена редкой метки самой частой меткой

val_counts = df['Age'].value_counts()
uncommon_ages = val_counts[val_counts<3].index.values
most_freq_age = val_counts.index[0]
df.loc[df['Age'].isin(uncommon_ages), 'Age'] = most_freq_age

Группирование наблюдений, показывающих редкие метки, в уникальную категорию (с новой меткой, например «Редкие» или «Другое»)

val_counts = df['Age'].value_counts()
uncommon_ages = val_counts[val_counts<3].index.values
df.loc[:, 'Age_is_Rare'] = 0
df.loc[df['Age'].isin(uncommon_ages), 'Age_is_Rare'] = 1
df.loc[:, 'Age_is_Rare'].head()

8. Преобразование переменных

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

Преобразование Гаусса

df['Age'].plot.hist()
df['Age Log'] = df['Age'].apply(np.log)
df['Age Log'].plot.hist()

Взаимное преобразование

df['Age Reciprocal'] = 1.0 / df['Age']
df['Age Reciprocal'].plot.hist()

Преобразование квадратного корня

df['Age Sqrt'] = np.sqrt(df['Age'])
df['Age Sqrt'].plot.hist()

Экспоненциальное преобразование

df['Age Exp'] = np.exp(df['Age'])
df['Age Exp'].plot.hist()

Трансформация Boxcox

from scipy.stats import boxcox
df[‘Age BoxCox’] = boxcox(df[‘Age’])[0]
df[‘Age BoxCox’].plot.hist()

9. Особенности взаимодействия

Возможно, например, пассажиры старшего возраста, которые также заплатили более высокую плату за проезд, имели * особенно * высокие шансы не выжить на Титанике. В таком случае мы бы назвали это эффектом * взаимодействия * между Age и Fare. Чтобы помочь нашей модели учесть этот эффект взаимодействия, мы можем добавить новую переменную Age * Fare.

df = pd.read_csv('train.csv')
df['Age_x_Fare'] = df['Age'] * df['Fare']

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