<jupyter_start><jupyter_text>Рекомендация тарифов Оператор мобильной связи «Мегалайн» выяснил: многие клиенты пользуются архивными тарифами. Они хотят построить систему, способную проанализировать поведение клиентов и предложить пользователям новый тариф: «Смарт» или «Ультра».---В нашем распоряжении данные о поведении клиентов, которые уже перешли на эти тарифы (из проекта курса «Статистический анализ данных»).🎯 _Нужно построить модель для задачи классификации, которая выберет подходящий тариф._Предобработка данных не понадобится — мы её уже сделали.Построим модель с максимально большим значением *accuracy*. Чтобы сдать проект успешно, нужно довести долю правильных ответов по крайней мере до 0.75. Проверим *accuracy* на тестовой выборке самостоятельно.  1. Откроем и изучим файл датасета<jupyter_code>import pandas as pd

from sklearn.model_selection import train_test_split

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression

from sklearn.metrics import accuracy_score
users_behavior = pd.read_csv('users_behavior.csv')
users_behavior.shape
users_behavior.head()<jupyter_output><empty_output><jupyter_text>📝 Датасет состоит из _3 214 объектов_ и _5 признаков_. **Целевой признак** для нашей задачи – `is_ultra`, т.е. модель, которую мы попытаемся построить, будет предсказывать значение `1` если клиенту нужно предложить тариф "Ультра" или `0` если тариф "Смарт". В этом и заключается задача классификации, т.к. наш целевой признак является категориальным.  2. Разобьем данные на выборки Спрятанной тестовой выборки в задании нет, поэтому может разбить исходный набор данных на три части: обучающую, валидационную и тестовую. Размеры тестового и валидационного наборов сделаем равными.<jupyter_code># Зафиксируем псевдослучайность для всех используемых в проекте алгоритмов
rnd_state = 201911
# 60% отводим под обучающую выборку
df_train, df_valid = train_test_split(users_behavior, test_size=0.4, random_state=rnd_state)
# половину из оставшихся 40% отдаем на валидационную, другую половину – на тестовую
df_valid, df_test = train_test_split(df_valid, test_size=0.5, random_state=rnd_state)<jupyter_output><empty_output><jupyter_text>В итоге получили следующее соотношение: 60% | 20% | 20% Чуть позднее, после проверки модели на валидационной выборке выполним оценку ещё и на тестовом наборе. Это позволит правильно оценить готовую модель.  3. Исследуем возможные модели Для решения задачи классификации рассмотрим следующие изученные модели:* дерево решений / decision tree* случайный лес  / random forest* логистическую регрессию / logistic regression<jupyter_code># Подготовим фичи и целевые признаки обучающей и валидационных выборок
features_train = df_train.drop(['is_ultra'], axis=1)
target_train = df_train['is_ultra']

features_valid = df_valid.drop(['is_ultra'], axis=1)
target_valid = df_valid['is_ultra']
# Хэлпер, рассчитывающий оценку правильности на валидационной выборке для обученной модели
def accuracy_score_valid(model):
    predictions_valid = model.predict(features_valid)
    return accuracy_score(target_valid, predictions_valid)<jupyter_output><empty_output><jupyter_text>Дерево решений / Decision Tree<jupyter_code># Посмотрим как глубина решающего дерева влияет на оценку правильности
for max_depth in range(1, 21, 2):
    model = DecisionTreeClassifier(max_depth=max_depth, random_state=rnd_state)
    model.fit(features_train, target_train)
    print(f"max_depth = {max_depth}:\t{accuracy_score_valid(model)}")<jupyter_output>max_depth = 1:	0.7418351477449455
max_depth = 3:	0.776049766718507
max_depth = 5:	0.7791601866251944
max_depth = 7:	0.7869362363919129
max_depth = 9:	0.7853810264385692
max_depth = 11:	0.7636080870917574
max_depth = 13:	0.7713841368584758
max_depth = 15:	0.7573872472783826
max_depth = 17:	0.7511664074650077
max_depth = 19:	0.7573872472783826<jupyter_text>📝 Судя по валидационной выборке, дерево решений имеет самую высокую оценку правильности `0.7869362363919129`, когда задан гиперпараметр глубины равный `7`.  Случайный лес / Random Forest<jupyter_code># В качестве гиперпараметра глубины дерева возьмем значение, найденное для предыдущей модели.
# А количество деревьев для нашего случайного леса будет искать в диапазоне от 10 до 100 с шагом 10.
for estim in range(10, 101, 10):
    model = RandomForestClassifier(n_estimators=estim, max_depth=7, random_state=rnd_state)
    model.fit(features_train, target_train)
    print(f"n_estimators = {estim}:\t{accuracy_score_valid(model)}")<jupyter_output>n_estimators = 10:	0.7838258164852255
n_estimators = 20:	0.8040435458786936
n_estimators = 30:	0.8040435458786936
n_estimators = 40:	0.8087091757387247
n_estimators = 50:	0.80248833592535
n_estimators = 60:	0.807153965785381
n_estimators = 70:	0.8102643856920684
n_estimators = 80:	0.8087091757387247
n_estimators = 90:	0.8087091757387247
n_estimators = 100:	0.8087091757387247<jupyter_text>📝 Модель случайного леса предсказывает тариф точнее, но, как мы видим, не на много – `0.8102643856920684`, даже при количестве деревьев леса равным `70`.  Логистическая регрессия / Logistic Regression<jupyter_code>def logistic_regression(solver='liblinear'):
    model = LogisticRegression(solver=solver, random_state=rnd_state)
    model.fit(features_train, target_train)
    return accuracy_score_valid(model)
logistic_regression()<jupyter_output><empty_output><jupyter_text>Логистическая регрессия на валидационной выборке дала самые худшие результаты. Попробуем их улучшить изменив гиперпараметры, например, `solver` – алгоритм задачи оптимизации.<jupyter_code>solvers = ['newton-cg', 'lbfgs']
for solver in solvers:
    print(f'{solver}: {logistic_regression(solver=solver)}')<jupyter_output>newton-cg: 0.7309486780715396
lbfgs: 0.6811819595645412<jupyter_text>📝 Даже используя другой алгоритм для решения задачи оптимизации модель логистической регрессии менее точна, чем дерево решений и случайный лес.  4. Проверим модель на тестовой выборкеВ результате наших экспериментов на валидационной выборке мы выяснили, что самую высокую оценку правильности дает лес решений с глубиной равной `7` и количеством деревьев равным `70`.Проверим как поведет себя модель на тестовой выборке.<jupyter_code>features_test = df_test.drop(['is_ultra'], axis=1)
target_test = df_test['is_ultra']

model = RandomForestClassifier(n_estimators=70, max_depth=7, random_state=rnd_state)
model.fit(features_train, target_train)

predictions_test = model.predict(features_test)
accuracy_score(target_test, predictions_test)<jupyter_output><empty_output><jupyter_text>💭 На тестовой выборке наша модель случайного леса показывает приближенное к тренировочной выборке значение _accuracy_. Таким образом, мы не видим здесь явной проблемы с переобучением, и можем считать, что _нам удалось построить модель для задачи **бинарной** классификации, которая выберет подходящий тариф_.  5. (бонус) Проверка модели на адекватность В машинном обучении быть скептиком насчет хороших результатов полученной модели – полезное качество. Мы хотим, чтобы выпущенная в продакшен модель не была подвержена переобучению, т.е. работала хорошо на данных, которые она не видела в процессе обучения.Проверка модели на адекватность _sanity check_ – нетривиальная задача. Для разных моделей машинного обучения существуют разные подходы. Выбор же ошибочной метрики качества может ввести в заблуждение. Например, мы не можем полагаться только на _accuracy_ для задач классификации, особенно в ситуации с дисбалансом классов: когда одна метка встречается чаще чем другая; или же когда в обучающей и валидационных выборках оказываются дубликаты.<jupyter_code>users_behavior['is_ultra'].value_counts()<jupyter_output><empty_output><jupyter_text>Для задач бинарной классификации могут быть использованы следующие методы:    * Матрица ошибок / confusion matrix* Тесты бинарной классификации / binary classification tests* Коэффициент конверсии / conversion rates* ROC-кривая / ROC curve* Совокупный доход / cumulative gain* Lift-кривая / lift chart---Работает и сравнение модели со случайной.<jupyter_code>df_test['is_ultra'].value_counts()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.06807626497188951, 'fraction_numerical': 0.06184307015399658, 'mean_word_length': 6.96011673151751, 'pattern_counts': {'":': 0, '<': 38, '<?xml version=': 0, '>': 38, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '306299', 'license': 'MIT', 'n_tokens_mistral': 3525, 'n_tokens_neox': 3695, 'n_words': 800, 'path': 'sprint6/notebook.ipynb', 'repo_name': 'akrisanov/yandex-praktikum-data-science', 'size': '8182'}