<jupyter_start><jupyter_text>Семинар № 5. Подготовка данных или Feature [Preprocessing](https://hackernoon.com/what-steps-should-one-take-while-doing-data-preprocessing-502c993e1caa) Здесь мы рассмотрим несколько важных моментов связанных с подговкой данных к обучению.<jupyter_code>%matplotlib inline
import numpy as np
import seaborn as sns
from sklearn import metrics
import matplotlib.pyplot as plt<jupyter_output><empty_output><jupyter_text>Feature Engineering (Создание новых признаков) Под этим словосочетанием подразумевают создание и подготовку признаков объектов, чтобы они лучше подходили под алгоритмы и давали наилучший результат. Рассмотрим пример концентрических окружностей.<jupyter_code>from random import gauss

num_samples = 1000
theta = np.linspace(0, 2*np.pi, num_samples)

r1 = 1
r2 = 2

rng = np.random.RandomState(1)

circle = np.hstack([np.cos(theta).reshape((-1, 1)) + (rng.randn(num_samples)[:,np.newaxis] / 8), 
                    np.sin(theta).reshape((-1, 1)) + (rng.randn(num_samples)[:,np.newaxis] / 8)])
lil = r1 * circle
big = r2 * circle
X = np.vstack([lil, big])
y = np.hstack([np.zeros(num_samples), np.ones(num_samples)])

# plots
plt.figure(figsize=(7,6))
plt.scatter(lil[:,0],lil[:,1])
plt.scatter(big[:,0],big[:,1])
plt.grid()<jupyter_output><empty_output><jupyter_text>Давайте попробуем классифицировать их деревом<jupyter_code>from sklearn.tree import DecisionTreeClassifier as DTC
clf = DTC()
clf.fit(X, y)
clf.tree_.max_depth<jupyter_output><empty_output><jupyter_text>Глубина дерева - 9, несмотря на такую простую с первого взгляда классификацию. Можно ли что-то с этим сделать?А давайте добавим радиус в наши данные.<jupyter_code>r = X[:,0]**2 + X[:,1]**2
X_new = np.hstack([X, r.reshape([-1,1])])
clf2 = DTC()
clf2.fit(X_new, y)
clf2.tree_.max_depth
cross_val_score(clf, X, y, cv=5, scoring=metrics.scorer.accuracy_scorer)
cross_val_score(clf, X_new, y, cv=5, scoring=metrics.scorer.accuracy_scorer)<jupyter_output><empty_output><jupyter_text>Добавив логичный признак, который мы предположили, мы сумели классифицировать данные гораздо более простой моделью, да еще и с намного лучшим качеством. Этот процесс и называется `Feature Engineering`.  Линейная Регрессия и Polynomial Features Собственно, с помощью создания фич мы можем заставить работать линейную регрессию. Вернемся к примеру с прошлого семинара.<jupyter_code>def plot_reg(X, y, clf_dtc, X_test, dim=0):
    clf_dtc.fit(X, y)
    Y_test = clf_dtc.predict(X_test)
    plt.figure(figsize=(8, 6))
    plt.scatter(X[:,dim], y, cmap='bwr', s=50, alpha=1)
    plt.plot(X_test[:,dim], Y_test, color='r', alpha=1)
    plt.grid()
def f(x):
    return np.sqrt(x) + np.sin(x)

vf = np.vectorize(f)
rng = np.random.RandomState(1)

X_reg = np.arange(0, 10, 0.2)[:, np.newaxis]
y_reg = vf(X_reg) + (rng.rand(50)[:,np.newaxis] / 2)#добавляем шумы
plt.figure(figsize=(6, 5))
plt.scatter(X_reg, y_reg, cmap='bwr', s=50, alpha=1)
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
from sklearn.linear_model import LinearRegression as LR
reg_lr = LR()
X_test = np.arange(0, 10, 0.5)[:,np.newaxis]
plot_reg(X_reg, y_reg, reg_lr, X_test)<jupyter_output><empty_output><jupyter_text>Теперь добавим функции второй, третей и четвертых степеней.<jupyter_code>X_reg_new = np.hstack([X_reg, X_reg**2, X_reg**3, X_reg**4])
X_test_new = np.hstack([X_test, X_test**2, X_test**3, X_test**4])
from sklearn.linear_model import LinearRegression as LR
reg_lr = LR()
plot_reg(X_reg_new, y_reg, reg_lr, X_test_new)<jupyter_output><empty_output><jupyter_text>В изначальной линейной регрессии мы находимся в пространстве линейных алгоритмов:$$a(x) = a_0x_0 + a_1x_1 + \ldots a_nx_n$$    Но теперь место фич у нас заняли полиномиальные функции и мы получили степенную функцию:$$a(x) = a_0 + a_1x^1 + \ldots a_nx^n$$ Чтобы не делать это вручную, есть механизм `PolynomialFeatures`<jupyter_code>from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=8)
X_reg2 = poly.fit_transform(X_reg)
X_test2 = poly.fit_transform(X_test)
from sklearn.linear_model import LinearRegression as LR
reg_lr = LR()
plot_reg(X_reg2, y_reg, reg_lr, X_test2, dim=1)<jupyter_output><empty_output><jupyter_text>С другой стороны мы можем догадаться, что тут не степенные функции и использовать те функции, которые нужны.<jupyter_code>X_reg3 = np.hstack([X_reg, X_reg**0.5, np.sin(X_reg)])
X_test3 = np.hstack([X_test, X_test**0.5, np.sin(X_test)])
from sklearn.linear_model import LinearRegression as LR
reg_lr = LR()
plot_reg(X_reg3, y_reg, reg_lr, X_test3, dim=0)<jupyter_output><empty_output><jupyter_text>Масштабирование (Scaling) Рассмотрим такую ситуацию у вас есть данные о дороге с 2мя признаками: `длинна участка дороги` и `толщина слоя асфальта`. Обе величины даются в одной метрике - метры. И нам нужно бинарно классифицировать качество дороги: `хорошая`,`плохая`.<jupyter_code>X = np.array([[10000, 7],[11000, 6],[9000, 5],[5000, 2],[6000, 1],[13000, 2],[14000, 1]])
y = np.array([1,1,1,0,0,0,0]) # 1 - хорошие, 0 - плохие
plt.scatter(X[:,0], X[:,1], c=y, cmap='bwr', s=50, alpha=1)
plt.xlabel('длина дороги')
plt.ylabel('толщина дороги')
plt.grid()<jupyter_output><empty_output><jupyter_text>Из логики нам будет очевидно, что первый признак практически бесполезен, а второй играет ключевую роль. Но, если запустить KNN или любой другой `метрический` алгоритм, мы получим обратную ситуацию, признак который в абсолюте больше - **важнее**, чем признак, который очень мало отличается.<jupyter_code>model = KNN(3)
model.fit(X,y)
X_bad = [[10000, 1]] 
y_pred = model.predict(X_bad)
X_new = np.vstack([X, X_bad])
y_new = np.hstack([y, y_pred])

plt.scatter(X_new[:,0], X_new[:,1], c=y_new, cmap='bwr', s=50, alpha=1)
plt.xlabel('длина дороги')
plt.ylabel('толщина дороги')
plt.grid()<jupyter_output><empty_output><jupyter_text>Почему новая точка классифицировалась неправильно? Потому что, с точки зрения эвклидовой метрики, до `хороших` ей действительно `ближе`.$$\rho((10000, 7), (10000, 1)) = \sqrt{(10000 - 10000)^2 + (7-1)^2} = \sqrt{6^2} << \rho((6000, 1), (10000, 1)) = \sqrt{4000^2 + (1-1)^2}$$ Чтобы справиться с этой проблемой важно `масштабировать` численные признаки, чтобы они были корректно сравнимы.Есть 2 подхода к масштабированию:* Нормализация (MinMax Scaling) $$x^* = \frac{x-x_{min}}{x_{max} - x_{min}}$$* Стандартизация (Standart Scaling) $$x^* = \frac{x-\mu}{\sigma}$$, где $\mu$ - среднее, а $\sigma$ - стандартное отклонение<jupyter_code>from sklearn.preprocessing import StandardScaler, MinMaxScaler
stan = StandardScaler().fit(X)
norm = MinMaxScaler().fit(X)
X_st = stan.transform(X)
X_norm = norm.transform(X)<jupyter_output><empty_output><jupyter_text>Метод `fit` подбирает параметры масштабирования, а метод `transform` непосредственно преобразует значения.<jupyter_code>plt.scatter(X_st[:,0], X_st[:,1], c=y, cmap='bwr', s=50, alpha=1)
plt.title('Standart')
plt.xlabel('длина дороги')
plt.ylabel('толщина дороги')
plt.grid()<jupyter_output><empty_output><jupyter_text>И вот теперь мы можем верно классифицировать данные, не забыв трансформировать точку, которую предсказываем<jupyter_code>model = KNN(3)
model.fit(X_st,y)
X_bad = stan.transform([[10000, 1]]) 
y_pred = model.predict(X_bad)

X_new = np.vstack([X_st, X_bad])
y_new = np.hstack([y, y_pred])
plt.scatter(X_new[:,0], X_new[:,1], c=y_new, cmap='bwr', s=50, alpha=1)
plt.title('Standart')
plt.xlabel('длина дороги')
plt.ylabel('толщина дороги')
plt.grid()<jupyter_output><empty_output><jupyter_text>Интересный факт, что любые древесные алгоритмы: DecisionTree, RandomForest - не требуют масштабирования. Потому что они работают на пороговых правилах и все признаки у них остаются независимыми. Резюме: Хороший паттерн делать масштабирование - `всегда`. Потому что для метрических алгоритмов оно необходимо, а для деревьев оно ничего не портит.   Категориальные признаки Окей, с числовыми признаками разобрались, но ведь доставать необходимую информацию нужно еще и из категориальных признаков. Как с ними можно работать?  Label Encoding Рассмотрим выборку UCI bank, в которой большая часть признаков – категориальные.<jupyter_code>import pandas as pd
df = pd.read_csv('bank_train.csv')
labels = pd.read_csv('bank_train_target.csv', header=None)

df.head()<jupyter_output><empty_output><jupyter_text>Чтобы найти решение, давайте рассмотрим признак education:<jupyter_code>df['job'].value_counts().plot.barh();<jupyter_output><empty_output><jupyter_text>Естественным решением такой проблемы было бы однозначное отображение каждого значения в уникальное число. К примеру, мы могли бы преобразовать `university.degree` в 0, а `basic.9y` в 1. Эту простую операцию приходится делать часто, поэтому в модуле `preprocessing` библиотеки `sklearn` именно для этой задачи реализован класс `LabelEncoder`:<jupyter_code>from sklearn.preprocessing import LabelEncoder, OneHotEncoder
label_encoder = LabelEncoder()<jupyter_output><empty_output><jupyter_text>Метод `fit` этого класса находит все уникальные значения и строит таблицу для соответствия каждой категории некоторому числу, а метод `transform` непосредственно преобразует значения в числа.<jupyter_code>mapped_education = pd.Series(label_encoder.fit_transform(df['education']))
mapped_education.value_counts().plot.barh()
print(dict(enumerate(label_encoder.classes_)))<jupyter_output>{0: 'basic.4y', 1: 'basic.6y', 2: 'basic.9y', 3: 'high.school', 4: 'illiterate', 5: 'professional.course', 6: 'university.degree', 7: 'unknown'}<jupyter_text>Что произойдет, если у нас появятся данные с другими категориями?<jupyter_code>try:
    label_encoder.transform(df['education'].replace('high.school', 'high_school'))
except Exception as e:
    print('Error:', e)<jupyter_output>Error: y contains previously unseen labels: 'high_school'<jupyter_text>Таким образом, при использовании этого подхода мы всегда должны быть уверены, что признак не может принимать неизвестных ранее значений. К этой проблеме мы вернемся чуть позже, а сейчас заменим весь столбец education на преобразованный:<jupyter_code>df['education'] = mapped_education
df.head()<jupyter_output><empty_output><jupyter_text>Продолжим преобразование для всех столбцов, имеющих тип `object` – именно этот тип задается в pandas для таких данных.<jupyter_code>categorical_columns = df.columns[df.dtypes == 'object'].union(['education'])
for column in categorical_columns:
    df[column] = label_encoder.fit_transform(df[column])
df.head()<jupyter_output><empty_output><jupyter_text>Основная проблема такого представления заключается в том, что числовой код создал евклидово представление для данных.К примеру, нами неявным образом была введена алгебра над значениями работы - мы можем вычесть работу клиента 1 из работы клиента 2:<jupyter_code>df.loc[1].job - df.loc[2].job<jupyter_output><empty_output><jupyter_text>Конечно же, эта операция не имеет никакого смысла. Но именно на этом основаны метрики близости объектов, что делает бессмысленным применение метода ближайшего соседа на данных в таком виде. Аналогичным образом, никакого смысла не будет иметь применение линейных моделей.Пример внизу: у класса 1 `precision` = 0, `recall`=0, `f1-score`=0 -никакая классификация не осуществялется.<jupyter_code>import warnings
warnings.filterwarnings('ignore')
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report

def logistic_regression_accuracy_on(dataframe, labels):
    features = dataframe.as_matrix()
    train_features, test_features, train_labels, test_labels = \
        train_test_split(features, labels)

    logit = LogisticRegression()
    logit.fit(train_features, train_labels)
    return classification_report(test_labels, logit.predict(test_features))

print(logistic_regression_accuracy_on(df[categorical_columns], labels))<jupyter_output>precision    recall  f1-score   support

           0       0.89      1.00      0.94      6126
           1       0.00      0.00      0.00       773

    accuracy                           0.89      6899
   macro avg       0.44      0.50      0.47      6899
weighted avg       0.79      0.89      0.84      6899<jupyter_text>Однако для древесных моделей `LabelEncoding` вполне сгодится. Для того, чтобы мы смогли применять линейные модели на таких данных нам необходим другой метод, который называется One-Hot Encoding One-Hot EncodingПредположим, что некоторый признак может принимать 10 разных значений. В этом случае one hot encoding подразумевает создание 10 признаков, все из которых равны нулю *за исключением одного*. На позицию, соответствующую численному значению признака мы помещаем 1:<jupyter_code>one_hot_example = pd.DataFrame([{i: 0 for i in range(10)}])
one_hot_example.loc[0, 6] = 1
one_hot_example<jupyter_output><empty_output><jupyter_text>По умолчанию `OneHotEncoder` преобразует данные в разреженную матрицу, чтобы не расходовать память на хранение многочисленных нулей. Однако в этом примере размер данных не является для нас проблемой, поэтому мы будем использовать "плотное" представление.<jupyter_code>onehot_encoder = OneHotEncoder(sparse=False)

encoded_categorical_columns = pd.DataFrame(onehot_encoder.fit_transform(df[categorical_columns]))
encoded_categorical_columns.head()<jupyter_output><empty_output><jupyter_text>Мы получили 53 столбца - именно столько различных уникальных значений могут принимать категориальные столбцы исходной выборки. Преобразованные с помощью One-Hot Encoding данные начинают обретать смысл для линейной модели:<jupyter_code>print(logistic_regression_accuracy_on(encoded_categorical_columns, labels))<jupyter_output>precision    recall  f1-score   support

           0       0.91      0.99      0.95      6139
           1       0.66      0.18      0.28       760

    accuracy                           0.90      6899
   macro avg       0.78      0.58      0.61      6899
weighted avg       0.88      0.90      0.87      6899<jupyter_text>Проблема возникает когда, количество различных значений у категориального признака сильно возрастает. В этом случае `OneHotEncoding` станет очень затратным по памяти (даже если будем использовать разряженные таблицы). Но в целом лучше всегда использовать `OneHot`.  Хэширование признаков (Hashing trick)С течением времени категориальные признаки могут принимать новых значений. Это затрудняет использование уже обученных моделей на новых данных. Кроме того, `LabelEncoder` подразумевает предварительный анализ всей выборки и хранение построенных отображений в памяти, что затрудняет работу в режиме больших данных.Для решения этих проблем существует более простой подход к векторизации категориальных признаков, основанный на хэшировании, известный как hashing trick. Хэш-функции могут помочь нам в задаче поиска уникальных кодов для различных значений признака, к примеру:<jupyter_code>for s in ('university.degree', 'high.school', 'illiterate'):
    print(s, '->', hash(s))<jupyter_output>university.degree -> -68740083847602807
high.school -> 4321647289974080590
illiterate -> -3642350026650764040<jupyter_text>Отрицательные и настолько большие по модулю значения нам не подойдут. Ограничим область значений хэш-функции:<jupyter_code>hash_space = 25
for s in ('university.degree', 'high.school', 'illiterate'):
    print(s, '->', hash(s) % hash_space)<jupyter_output>university.degree -> 18
high.school -> 15
illiterate -> 10<jupyter_text>Представим, что у нас в выборке есть холостой студент, которому позвонили в понедельник, тогда его вектор признаков будет сформирован аналогично One-Hot Encoding, но в едином пространстве фиксированного размера для всех признаков:<jupyter_code>hashing_example = pd.DataFrame([{i: 0.0 for i in range(hash_space)}])
for s in ('job=student', 'marital=single', 'day_of_week=mon'):
    print(s, '->', hash(s) % hash_space)
    hashing_example.loc[0, hash(s) % hash_space] = 1
hashing_example<jupyter_output>job=student -> 16
marital=single -> 1
day_of_week=mon -> 0<jupyter_text>Стоит обратить внимание, что в этом примере хэшировались не только значения признаков, а пары **название признака + значение признака**. Это необходимо, чтобы разделить одинаковые значения разных признаков между собой, к примеру:<jupyter_code>assert hash('no') == hash('no')
assert hash('housing=no') != hash('loan=no')<jupyter_output><empty_output><jupyter_text>Может ли произойти коллизия хэш-функции, то есть совпадение кодов для двух разных значений? Нетрудно доказать, что при достаточном размере пространства хэширования это происходит редко, но даже в тех случаях, когда это происходит, это не будет приводить к существенному ухудшению качества классификации или регрессии.Возможно, вы спросите: "а что за хрень вообще происходит?", и покажется, что при хэшировании признаков страдает здравый смысл. Возможно, но эта эвристика – по сути, единственный подход к тому, чтобы работать с категориальными признаками, у которых много уникальных значений. Более того, эта техника себя хорошо зарекомендовала по результатами на практике. Подробней про хэширование признаков (learning to hash) можно почитать в [этом](https://arxiv.org/abs/1509.05472) обзоре, а также в [материалах](https://github.com/esokolov/ml-course-hse/blob/master/2016-fall/lecture-notes/lecture06-linclass.pdf) Евгения Соколова.  Blending И на финал мы познакомимся с внутренним устройством классификаторов и попытаемся сделать свой смешанный классификатор.Часто на практике оказывается возможным увеличить качество предсказания путем смешивания разных моделей. Это происходит за счет того, что разные модели могут хранить в себе разные скрытые зависимости от данных, которые могут вместе дать синергию.Посмотрим на наш небольшой класс классификатора.<jupyter_code>from sklearn.base import BaseEstimator, ClassifierMixin
class BlendClassifier(BaseEstimator, ClassifierMixin):# предок класса всех классификаторов
    def __init__(self, clf1, clf2, alpha=0.5):
        self.clf1 = clf1 
        self.clf2 = clf2
        self.alpha = alpha #параметр смешивания

    def fit(self, X, y): #обучаем классификатор
        self.X_ = X
        self.y_ = y 
        self.clf1.fit(X, y)
        self.clf2.fit(X, y)
        return self

    def predict(self, X): #возвращаем значения
        predict1 = self.clf1.predict(X)
        predict2 = self.clf2.predict(X)
        
        return np.round(predict1 * self.alpha + predict2 * (1 - self.alpha))

    def predict_proba(self, X): #возвращаем вероятности
        predict1 = self.clf1.predict_proba(X)
        predict2 = self.clf2.predict_proba(X)
        
        return predict1 * self.alpha + predict2 * (1 - self.alpha)
from sklearn.linear_model import SGDClassifier as SGD
from sklearn.ensemble import RandomForestClassifier as RF
clf = BlendClassifier(
        clf1 = SGD(loss = 'log'), 
        clf2 = RF(n_estimators = 50)
    )
clf.fit(iris.data, iris.target)<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup