<jupyter_start><jupyter_text>Прогноз среднгей стоимости клика в рекламной сети Яндекс.Директ Автор: <NAME><jupyter_code>%matplotlib inline


from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
# отключим предупреждения Anaconda
import warnings
warnings.simplefilter('ignore')

# будем отображать графики прямо в jupyter'e
%pylab inline


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import mean_squared_error

from sklearn.preprocessing import StandardScaler, LabelEncoder, OneHotEncoder, LabelBinarizer
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LinearRegression
from sklearn.svm import LinearSVR


from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.svm import LinearSVR
from sklearn.learning_curve import learning_curve
from scipy import stats
from scipy.sparse import csr_matrix, coo_matrix, hstack

from sklearn.feature_extraction.text import TfidfVectorizer

df=pd.read_csv('ya_click.csv')
df.head(5)<jupyter_output><empty_output><jupyter_text>Часть 1. Описание набора данных и признаков Задача взята с реального проекта. Заказчику требуется по показателям рекламы в Яндекс.Директа предсказывать среднюю цену клика. Данные брались из личного кабинета заказчика, была сделана выгрузка за определенный период.Ценность задачи для заказчика в том, что он хочет с помощью прогноза оптимальнее управлять ставками в объявлениях.Настроить этот процесс усправления ставками автоматически. Не смотря на то, что на рынке есть уже готовые решения для этого, заказчик хочет сделать свое инливидуальное решение.**Дата** - дата рекламной кампании содержит число, месяц, год**Кампания** - название рекламной кампании**№ Кампании** - номер рекламной кампании в Яндекс.Директ**№ Объявления** - номер объявления в рекламной системе Яндекс.Директ**Тип объявления** - тип объявления в рекламной системе Яндекс.Директ(бывает текстово-графическим, те содержит текст и картинку, бывает просто графическим, те содержит только одну картинку) **Условие показа** - условия показа рекламного объявления Яндекс.Директ (это условие может создаться самим владельцем сайта, например: брошенные корзины - это условие берется из Яндекс.Метрики. Либо содержит ключевое слово, по которому показываются объявление, например: "подарки на 8 марта"**Тип условия показа** - показывает был ли параметр "условие показа" выше придуман владельцем сайта, те взять из Яндекс.Метрики или объявление показывалось по ключевому слову (типы: "условие подбора аудитории" - придумано владельцем сайта, "фраза" - показ по ключевому слову) .***Тип площадки*** - на какой площадке ( рекламной сети Яндекс или на поиске ) было показано объявление.***Внешние сети*** - на каких площадках(сайтах) было показано объявление. В нашем датасете это Яндекс.Сети и Яндекс.Поиск, но в расширенном могут быть и другие сайты.***Регион таргетинга*** - в каком регионе/городе показывалось объявление.  ***Позиция***  - где размещалось объвления, бывает VIP размещение, называемое "спецразмещением" и "прочее", те объявление показывается на плохо видимых местах.***Тип устройства*** - на каком типе устройства показывалось объявление (десктоп, планшет, мобаил)***Показы*** - количество показов объявления***Клики*** - количество кликов по объявлению***Расход (руб.)*** - расходны затраченные на объявление***Ср. цена клика (руб.)*** - средняя цена клика по объявлению (пронозируемая пременная)<jupyter_code>df.columns<jupyter_output><empty_output><jupyter_text>Часть 2. Первичный анализ признаков<jupyter_code>#Посмотрим размер датасета и пропущенные значения.
print("Размер датасета:", df.shape)

#Всего у нас в датасете 443.009 строк
#Посмотрим есть ли пропуски целевой переменной:
df['Ср. цена клика (руб.)'].value_counts()<jupyter_output><empty_output><jupyter_text>Благодаря отчеты выше поняли, что из 443009 строк, данные осутсвуют в 407672, те в таком количестве строк просто не присутствует наша целевая переменная. Таким образом для анализа у нас остается  всего 443009 - 407672 = 35337 строк.PS Поговорив со специалистами из рекламного отдела, стало понятно почему в стольких позициях целевая переменная отсуствует. Специалисты сказали, что рекламные объявления создаются в автоматическом режиме и многие из них просто не показываются, тк таких запросов пользователи не делают.Например узконаправленный запрос типа "купить по низкой цене Boshe-XC-003-87 в Марьино на улице Северянин" скорее всего никогда не покажется. Поэтому в таком большом количестве объявлений показов нет.<jupyter_code>#По причине выше было принято создать новый DataFrame убрал строки, где целевая переменная не присутсвует
df = df[df['Ср. цена клика (руб.)']!='-'].copy()
df.info()<jupyter_output><empty_output><jupyter_text>Можно заметить, что в основном у нас категориальные признаки.Но здравый разум подсказывает,что  "Расход (руб.)" и "Ср. цена клика (руб.)" - числовые пременные.Для простоты дальнейшего анализа преобразуем их.<jupyter_code># Преобразуем "Расходы" и "Ср. цену клика" к вещественным значениям
df['Ср. цена клика']=df['Ср. цена клика (руб.)'].apply(lambda x: float(x.replace(',','.')))
df['Расход']=df['Расход (руб.)'].apply(lambda x: float(x.replace(',','.')))

#Для красоты уберем из названия колонок "руб"
df = df.drop(['Ср. цена клика (руб.)', 'Расход (руб.)'], axis=1) 
df.info()
#Посмотрим на распределение целевого признака
sns.set(font_scale=1.3)
plt.figure(figsize=(8,5))
df['Ср. цена клика'].hist(bins=300);
# Посмотрим, что говорят статистические тесты.
print(stats.normaltest(df['Ср. цена клика']))
print('skew=',stats.skew(df['Ср. цена клика']))
print(stats.skewtest(df['Ср. цена клика']))<jupyter_output><empty_output><jupyter_text>***Вывод***: Благодаря статистическим тестам, ну это можно увидеть на графике, наши данные не распределены по нормальному распределению.  Также мы видим скошенность распределения. Пропуски в данных мы изучили выше. При этом хочется заметить, что кроме пропусков в целвой переменной, пропусков в других данных не наблюдалось.Теперь исследуем выбросы<jupyter_code>sns.violinplot(x='Ср. цена клика', scale='count',data=df);
sns.boxplot( x='Ср. цена клика', data=df, orient="h");<jupyter_output><empty_output><jupyter_text>**Вывод:** Видим, что в данных есть выборсы.Единственно, посоветовавшись со специалистами было принято не убирать эти выбросы, тк группа выбросов, которая скучковалась около 120 являлась важным направлением для кампании. Те это были товары из одной категории.  Часть 3. Первичный визуальный анализ признаков<jupyter_code>#Построим матрицу корреляции
plt.figure(figsize=(8,5))
corr_matrix=df.corr()
sns.heatmap(corr_matrix,annot=True,fmt = ".2f",cbar = True,cmap='PuOr',annot_kws={"size":10});<jupyter_output><empty_output><jupyter_text>Матрица корреляции для непреобразованных признаков не очень информативна. Тк в нашем датасете в основном категориальные признаки и логичнее было строить эту матрицу корреляции не при первичном анализе данных, а уже после преобразования признаков.Видим, что средняя цена наиболее коррелирует с расходами.При этом с другими признаками корреляция очень мала. Единственно, что можно заметить, что у нас хорошо коррелируют признаки "Клики" и "Расходы", но это и логично.<jupyter_code># Посмотрим на попарное распределение признаков
plt.figure(figsize=(12,8))
sns_plot = sns.pairplot(df);<jupyter_output><empty_output><jupyter_text>Вприцнипе подтвердились все выводы, которые сделаны выше.  Часть 4. Закономерности, "инсайты", особенности данных Частично выводы были описаны выше, резюмируем здесь:1) В силу того, что объявления создаются в автоматическом режиме, в бОльшинстве своем перебором различных параметров например перебираются различные комбинации параметров марки холодильника, станций метро где их можно купить и тд... Возникают слишком узкие запросы для объявления, например "купить по низкой цене Boshe-XC-003-87 в Марьино на улице Северянин". А такие узкие запросы практически не набираются пользователями. Следовательно у нас много строчек в датасете, где не только нет цлевой переменной, но и показов / кликов объявлений.2) У нас видная небольша кучка данных около "средней цены" 120р - это оказалась определенная группа товаров у заказчика, причем важная группа товаров. Поэтому было приянто решение ее не очищать3) Провели статистические тесты и поняли, что наша целевая переменная распределена не по нормальному закону и имееет скошенность.4) Поняли, что матрица коррелиции, если не преобразовывать категориальные ризнаки,а запустить только на первычных признаках не несет никакой серьезной инормативности.5) Также было замечено, что наш датасет основывается только на месяце марте. Если более внимательно изучить датасет, то можно заметить, что в основном он состоит из рекламных объявлений про подарки на 8марта. Изначально, кстати с заказчиком не оговаривалась такая особенность датасета, но это нам на руку в дальнейшем мы сможем выделить интересные признаки из этого.6) Также удалим "Внешние сети" тк там одно значение для всех переменных.  Часть 5. Выбор метрики Так как мы решаем задачу регрессии, то можно использовать метрику MSE. Впринципе, можно было бы использовать и R2 метрику (коэффициент детерминации), но, вооружившись baseline в виде начального приближения средним, будем использовать MSE. Также самим заказчиком было предложено использовать метрику MSE, поэтому было принято оставить ее.  Часть 6. Выбор модели Для данной задачи случайный лес, не очень хорошо подойдет, так как в задаче приличное количество признаков категориального типа. Также мы будем использовать sparse формат, а в этом случаи хорошо заходят линейные модели. Может пригодиться и простая линейная регрессия.Впрочем, ничто не мешает опробовать и другие регрессоры, что мы и сделаем далее.Испробуем несколько моделей, после выберем наилучшую.<jupyter_code>y = df['Ср. цена клика']
X = df.drop(['Ср. цена клика'], axis=1) 
#Разбиение выборки на train и test, первый 'baseline'
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30,shuffle=True,random_state=42)
#Среднее значение оценки на трейне
y_mean=y_train.mean()
y_mean_for_test=[float(y_mean) for x in range(len(y_test))]
y_mean_for_train=[float(y_mean) for x in range(len(y_train))]
#Результаты приближения средним на трейне
print(mean_squared_error(y_train,y_mean_for_train))
#Результаты приближения средним на тесте.
#Получили baseline
baseline_mean=mean_squared_error(y_test,y_mean_for_test)
print(mean_squared_error(y_test,y_mean_for_test))<jupyter_output><empty_output><jupyter_text>Мы создали для себя baseline, который будем стараться побить дальше.Вaseline - приближение среднем всех значений.  Часть 7. Предобработка данных Так как в нашей задаче много категориальных признаков закодируем их OneHotEncoder.Также есть признаки Расход, который является комбинацией целевого признака и признака "клики" - уберем его.Также принято удалить признаки "Показы" и "Клики". Так как мы хотим предсказывать цену клика еще до показа объявлений, тк в нашем случаи на реальных данных этой колонки просто не будет.PS Из самого df не будем удалять эти колонки, тк далее мы будем делать sparse матрицу, просто в нее не включим колонки выше, которые решили удалить.<jupyter_code>#Создадим новый признак CTR.
df['CTR'] = df['Клики']/df['Показы']
def OneHotEncoder_Me(df, colums):
    
    df_new = df.copy()
    
    le = LabelEncoder()
    df_new[colums] = le.fit_transform(df_new[colums])
        
    enc = OneHotEncoder(sparse=True)
    colums_enc = enc.fit_transform(df_new[colums].reshape(-1, 1))
    
    return colums_enc
type_adevrtising = OneHotEncoder_Me(df,'Тип объявления')
type_uslovie_pokaza = OneHotEncoder_Me(df,'Условие показа')
type_type_uslovie_pokaza = OneHotEncoder_Me(df,'Тип условия показа')
type_ploshadki = OneHotEncoder_Me(df,'Тип площадки')
type_region_targeting = OneHotEncoder_Me(df,'Регион таргетинга')
type_position = OneHotEncoder_Me(df,'Позиция')
type_device = OneHotEncoder_Me(df,'Тип устройства')



X = csr_matrix(hstack([type_adevrtising,
                       type_uslovie_pokaza, 
                       type_type_uslovie_pokaza,
                       type_ploshadki,
                       type_region_targeting,
                       type_position,
                       type_device
                    ]))

y = df['Ср. цена клика'].values
#Разобьем нашу выборку на Train и Test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30,shuffle=True,random_state=42)<jupyter_output><empty_output><jupyter_text>Часть 8. Кросс-валидация и настройка гиперпараметров модели<jupyter_code>#Приступим к выбору модели
#Список регрессоров
regressors = [LinearRegression(),
               RandomForestRegressor(random_state=42), 
               LinearSVR(random_state=42)]
regressor_name = ['LinearRegression',
                    'RandomForestRegressor', 
                    'LinearSVR']
#Параметры к регрессорам
scores = []
fits = []
linear_params = {'normalize': (True, False)}
forest_params = {'n_estimators': [100], 
                 'max_depth': list(range(3, 12, 4)), 
                 #'min_samples_leaf': list(range(10, 31, 10))
                }

svm_params = {'loss' : ('epsilon_insensitive', 'squared_epsilon_insensitive'), 'C': (0.1,0.25,0.5, 1, 2)}
params = [linear_params, forest_params, svm_params]
#Перебираем параметры регрессоров в поисках лучшего (на 5 фолдах)
np.random.seed(42)
for i, each_regressor in enumerate(regressors):
    reg = each_regressor
    reg_params = params[i]
    grid = GridSearchCV(reg, reg_params, 
                        cv=5,
                        scoring='neg_mean_squared_error',
                        n_jobs=-1)
    grid.fit(X_train, y_train)
    fits.append(grid.best_params_)
    reg_best_score = grid.best_score_
    scores.append(reg_best_score)
    print(regressor_name[i], -reg_best_score, "\n", grid.best_params_, "\n")<jupyter_output><empty_output><jupyter_text>**Вывод:** Из моделей лучше всего сработал SVM.  Лучшие его параметры можно посмотреть выше.  Часть 9. Создание новых признаков и описание этого процесса  Создадим новые признаки: 1) поработаем с датой (выделим, день, месяц и выходные)2) создадим TF-IDF на признаке "условие показа" объявления**Инсайт:** Также воспользуемся инсайтом, который заметили при первичном анализе данных. Напомню, что в нашем датасете все данные взяты только за март, соотвественно хорошим признаком будет выделить около празничные дни 8марта (те 7,8 марта)<jupyter_code>df['DateTime'] = df['Дата'].apply(pd.to_datetime)
df['weekend'] =  df['DateTime'].apply(lambda x: 0 if x.weekday() < 5 else 1 )
df['marсh_7_8'] = df['DateTime'].apply(lambda x: 1 if x.day in[7,8] else 0 )
tfd=TfidfVectorizer(ngram_range=(1,2))
uslovie_pokaza_tfd=tfd.fit_transform(df['Условие показа'])
weekend = df['weekend'].reshape(-1,1)
marxh_7_8 = df['marсh_7_8'].reshape(-1,1)
X = csr_matrix(hstack([type_adevrtising,
                       type_uslovie_pokaza, 
                       type_type_uslovie_pokaza,
                       type_ploshadki,
                       type_region_targeting,
                       type_position,
                       type_device,
                       uslovie_pokaza_tfd,
                       weekend,
                       marxh_7_8
                    ]))

y = df['Ср. цена клика'].values
# Снова разобьем на Train и Test , но уже с новыми призаками
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30,shuffle=True,random_state=42)
#Сделаем опять перебор по всем моедлям с теми же параметрами, но новыми признаками

np.random.seed(42)
for i, each_regressor in enumerate(regressors):
    reg = each_regressor
    reg_params = params[i]
    grid = GridSearchCV(reg, reg_params, 
                        cv=5,
                        scoring='neg_mean_squared_error',
                        n_jobs=-1)
    grid.fit(X_train, y_train)
    fits.append(grid.best_params_)
    reg_best_score = grid.best_score_
    scores.append(reg_best_score)
    print(regressor_name[i], -reg_best_score, "\n", grid.best_params_, "\n")<jupyter_output><empty_output><jupyter_text>**Вывод:** Видим, что наши признаки улучшили метрику. Незначительно, но улучишили нашу метрику, причем на всех моделях.Особенно улучшился RandomForestПредыдущие значения:LinearRegression 31.7588321129922  {'normalize': False} RandomForestRegressor 42.5449357017418  {'max_depth': 11, 'n_estimators': 100} LinearSVR 30.983540220587678  {'C': 0.5, 'loss': 'squared_epsilon_insensitive'}   Часть 10. Построение кривых валидации и обучения (4 балла)<jupyter_code>#Лучше всего показал себя SVM 

svm = LinearSVR(C=0.5, loss='squared_epsilon_insensitive')

def plot_with_std(x, data, **kwargs):
        mu, std = data.mean(1), data.std(1)
        lines = plt.plot(x, mu, '-', **kwargs)
        plt.fill_between(x, mu - std, mu + std, edgecolor='none',
                         facecolor=lines[0].get_color(), alpha=0.2)
        
def plot_learning_curve(reg, X, y, scoring, cv=5):
 
    train_sizes = np.linspace(0.05, 1, 20)
    n_train, val_train, val_test = learning_curve(reg,
                                                  X, y, train_sizes, cv=cv,
                                                  scoring=scoring)
    plot_with_std(n_train, val_train, label='training scores', c='green')
    plot_with_std(n_train, val_test, label='validation scores', c='red')
    plt.xlabel('Training Set Size'); plt.ylabel(scoring)
    plt.legend()
#Это график MSE со знаком минус, поэтому функции возрастают.
plot_learning_curve(svm,
                    X_train, y_train, scoring='neg_mean_squared_error', cv=5)<jupyter_output><empty_output><jupyter_text>Часть 11. Прогноз для тестовой или отложенной выборке<jupyter_code># Обучим нащшу модель
svm.fit(X_train, y_train)
predict = svm.predict(X_test)
mean_squared_error(y_test, predict)
print(mean_squared_error(y_test, predict),'vs baseline:',baseline_mean)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08705765077255358, 'fraction_numerical': 0.013235864207786455, 'mean_word_length': 4.7454661151765825, 'pattern_counts': {'":': 1, '<': 59, '<?xml version=': 0, '>': 58, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '321799', 'license': 'Unlicense', 'n_tokens_mistral': 7170, 'n_tokens_neox': 8024, 'n_words': 1728, 'path': 'jupyter_russian/projects_individual/project_yandex_click_edvard2901.ipynb', 'repo_name': 'salman394/AI-ml--course', 'size': '18067'}