Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

4 / logreg

.py
Скачиваний:
9
Добавлен:
19.01.2023
Размер:
16.13 Кб
Скачать
import numpy as np
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.feature_selection import RFE
from sklearn.metrics import classification_report
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve

import matplotlib.pyplot as plt
import seaborn as sns

# Функция вывода отношения между количеством записей,
# относящихся к классу 1 и к классу 0 по заданному признаку (col_name)
def get_values_rel_by_class(dataset, col_name, class_col_name):
    print('%s 1/0 class relation:' % col_name)
    res1 = dataset.loc[dataset[class_col_name] == 1, [col_name]]
    res1 = res1.groupby([col_name]).size()
    res0 = dataset.loc[dataset[class_col_name] == 0, [col_name]]
    res0 = res0.groupby([col_name]).size()
    for r0, r1 in zip(res0, res1):
        print(r1/r0)

# Функция вывода графика кросстабуляции
def plot_crosstab(data, col_name_x, col_name_y):
    crosstb = pd.crosstab(data[col_name_x], data[col_name_y])
    crosstb.plot(kind='bar')
    plt.title(f'{col_name_x}_{col_name_y}_crosstab')
    plt.xlabel(col_name_x)
    plt.ylabel(col_name_y)
    #plt.savefig('%s_saved' % title)
    plt.show()
    return crosstb

# Функция вывод ROC-кривой
def plot_ROC(logreg, X_test, y_test):
    logit_roc_auc = roc_auc_score(y_test, logreg.predict(X_test))
    fpr, tpr, thresholds = roc_curve(y_test, logreg.predict_proba(X_test)[:, 1])
    plt.figure()
    plt.plot(fpr, tpr, label='Logistic Regression (area = %0.2f)' % logit_roc_auc)
    plt.plot([0, 1], [0, 1], 'r--')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.0, 1.05])
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver operating characteristic')
    plt.legend(loc="lower right")
    plt.savefig('Log_ROC')
    plt.show()


if __name__ == '__main__':
    dataset = pd.read_csv("export.csv", sep=';', header='infer', names=None,
                          encoding="utf-8")  # возвращает Pandas DataFrame
    pd.set_option("display.max_rows", None, "display.max_columns", None)  # Настройки вывода таблицы
    print(dataset.shape)  # Выводим размерность данных
    print(dataset.head(5)) # Выводим первые 5 строк DataFrame (проверяем, что файл загрузился верно)

    # Переименовываем анализируемую колонку (для простоты использования в коде)
    dataset = dataset.rename(columns={'Кол-во покупок нового товара': 'y'})
    # Приводим значения в анализируемой колонке к бинарному виду:
    # 1 - есть покупки, а значит и отклик, 0 - нет покупок.
    dataset['y'] = np.where(dataset['y'] >= 1, 1, dataset['y'])
    #print(dataset.head(5))

    # Считаем общее количество записей в каждом классе
    # ВАЖНО: Характер распределения записей по классам необходимо учитывать в дальнейшем
    # при формировании обучающего набора данных (записи в этом наборе должны распределять примерно также)
    #print(dataset['y'].value_counts())

    sns.countplot(x='y', data=dataset, palette='hls')
    plt.title("Общее количество записей в каждом классе")
    plt.savefig('count_all_plot')  # инструкция для сохранения графика в файл (если необходимо)
    plt.show()

    """ Этап анализа и подготовки данных для классификации """

    # очевидно, такие признаки как "Клиент. код" и "Дата актуальности" не представляют никакой полезности при данной классификации
    # удаляем их из анализируемого набора данных
    dataset = dataset.drop(['Клиент.Код'], axis=1)
    dataset = dataset.drop(['Дата актуальности'], axis=1)

    # Для оценки значимости признаков, которые имеют числовые значения
    # можно вывести среднее значение числовых признаков по заданным классам, это делается следующим образом:
    # print(dataset.groupby('y')[<наименование_признака>].mean())

    """ Строим графики кросстабуляции для анализа значимости того или иного признака для конечной классификации,
    # а также для определения необходимо ли сгруппировать значения какого-либо признака по категориям (классам), чтобы
    # обобщить редкие категории и снизить вероятность выбросов или экстремальных значений """

    # Выводим график по признаку 'Округ':
    #plot_crosstab(dataset, 'Округ', 'y')
    # Судя по графикам, распределение по классам в каждом округе практически одинаковое, меняется только общее количество
    # Посчитаем точное значение, вычислив отношение между количеством записей в классе 1 и 0 по каждому округу
    get_values_rel_by_class(dataset, 'Округ', 'y')
    # По полученным значениям видно, что изначальное предположение, что данный признак практически не влияет
    # на распределение по классам верно, следовательно данный признак также можно удалить из набора данных
    dataset = dataset.drop(['Округ'], axis=1)

    # Анализируем признак 'Пол' и выводим график кросстабуляции
    #plot_crosstab(dataset, 'Пол', 'y')
    w = dataset.loc[dataset['Пол'] == 'жен', ['Пол']].size
    m = dataset.loc[dataset['Пол'] == 'муж', ['Пол']].size
    nan = dataset.loc[dataset['Пол'].isna(), ['Пол']].size
    print("Распределение категории 'Пол':", m, w, nan)
    # Как видно по значениям в признаке 'Пол' есть большое количество пропущенных (NaN) значений, удалим эти записи
    dataset.drop(dataset.loc[dataset['Пол'].isna(), ['Возрастная группа']].index, inplace=True)
    # Приводим колонку 'Пол' к бинарному виду: 1 - 'муж', 0 - 'жен':
    dataset['Пол'] = np.where(dataset['Пол'] == 'муж', 1, dataset['Пол'])
    dataset['Пол'] = np.where(dataset['Пол'] == 'жен', 0, dataset['Пол'])

    """ ВАЖНО: построение графиков по признаку, содержащему большое количество разных категорий может занять длительное время,
    т.о. для анализа подобных признаков (например, различные суммы в чеках и счетах) лучше группировать значения и
    сначала выводить количество записей по группам
    """

    # Анализируем колонку 'Счет'
    print("Количество категорий в колонке 'Счет': ", pd.unique(dataset['Счет']).size)
    # Судя по выводу, колонка имеет строковое значние и имеет очень много категорий, т.о.
    # Преобразуем строковые значения во float и заполняем непреобразуемые любым числом (т.к. их немного)
    dataset['Счет'] = pd.to_numeric(dataset['Счет'].str.replace(',','.'), errors='coerce').fillna(50, downcast='infer')
    print("Средний счет по классам: ", dataset.groupby('y')['Счет'].mean())
    # Группируем суммы счетов по категориям:
    # до 1000 р.
    dataset['Счет'] = np.where(dataset['Счет'] <= 1000.0, 1000, dataset['Счет'])
    # 1000-5000 р.
    dataset['Счет'] = np.where((dataset['Счет'] > 1000.0) & (dataset['Счет'] <= 5000.0), 5000, dataset['Счет'])
    # 5000-10000 р.
    dataset['Счет'] = np.where((dataset['Счет'] > 5000.0) & (dataset['Счет'] <= 10000.0), 10000, dataset['Счет'])
    # 10000-25000 р.
    dataset['Счет'] = np.where((dataset['Счет'] > 10000.0) & (dataset['Счет'] <= 25000.0), 25000, dataset['Счет'])
    # 25000-50000 р.
    dataset['Счет'] = np.where((dataset['Счет'] > 25000.0) & (dataset['Счет'] <= 50000.0), 50000, dataset['Счет'])
    # 50000-100000 р.
    dataset['Счет'] = np.where((dataset['Счет'] > 50000.0) & (dataset['Счет'] <= 100000.0), 100000, dataset['Счет'])
    # от 100000 р. и выше
    dataset['Счет'] = np.where(dataset['Счет'] > 50000.0, 200000, dataset['Счет'])
    # Выводим график кросстабуляции по новым сгруппированным значениям
    dataset = dataset.rename(columns={'Счет': 'Счет до (р.)'})

    # Анализируем колонку 'Длительность регистрации (мес.)'
    #plot_crosstab(dataset, 'Длительность регистрации (мес.)', 'y')
    # По графику кросстабуляции видно, что в данной категории в небольшом присутствуют строковые (ошибочные) значения ('жен')
    # Преобразуем их в числовой тип, при этом заменив ошибочные значения (NaN) эквивалентным несущественным значением (например, 100 мес.)
    dataset['Длительность регистрации (мес.)'] = pd.to_numeric(dataset['Длительность регистрации (мес.)'], errors='coerce').fillna(100.0, downcast='infer')

    # Выводим график кросстабуляции по колонке 'Сегмент достатка'
    # Это значимый признак для классификации, но т.к. небольшое количество трок содержат ошибочные значения ('жен'),
    # удаляем эти строки, чтобы повысить точность классификации
    dataset.drop(dataset.loc[dataset['Сегмент достатка'] == 'жен', ['Сегмент достатка']].index, inplace=True)
    #plot_crosstab(dataset, 'Сегмент достатка', 'y')
    # Производим аналогичные действия с колонкой 'Возрастная группа'
    dataset.drop(dataset.loc[dataset['Возрастная группа'] == 'жен', ['Возрастная группа']].index, inplace=True)
    #plot_crosstab(dataset, 'Возрастная группа', 'y')

    # Примером хорошего признака является колонка 'Статус',
    #plot_crosstab(dataset, 'Статус', 'y')
    # это также видно по распределению по классам в каждой категории
    get_values_rel_by_class(dataset, 'Статус', 'y')

    """ ВАЖНО: для корректной работы алгорима необходимо нормализовать категориальные признаки,
    такие как в колонке 'Статус' ("Золотой", "Платиновый" и т.п.) с помощью one-hot (бинарного) кодирования
    """
    # Выбираем признаки для one-hot кодирования
    one_hot_cols = ['Возрастная группа', 'Статус', 'Сегмент достатка']
    # Используем встроенный в Pandas метод one-hot кодирования get_dummies
    for col_name in one_hot_cols:
        one_hot = pd.get_dummies(dataset[col_name])
        dataset = dataset.drop(col_name, axis=1)
        dataset = dataset.join(one_hot)

    print("Финальный вид набора данных после предобработки:\n", dataset.head(5))

    # Выделяем колонку, которая содержит определения классов в numpy array
    # (это выходной набор данных для обучения модели и тестирования)
    out_data = dataset['y']
    # Входной набор данных
    in_data = dataset.drop(['y'], axis=1)

    # Разделяем из общего набора данных тренировочную (70%) и тестовую выборку (30%),
    # параметр stratify=out_data позволяет делать выборку с сохранением пропорции распределения классов
    in_data_train, in_data_test, out_data_train, out_data_test = train_test_split(in_data, out_data, test_size=0.3, stratify=out_data)

    """ Для дополнительной оценки используемых для классификации признаков 
    можно также применить встроенный в пакет sklearn рекурсивный алгоритм оценки (RFE)
    """
    # Список признаков (названия колонок)
    in_data_features = in_data.columns.values.tolist()
    # Создаем модель классификатора (Логистическая регрессия)
    reg = 1.0 # коэффициент регуляризации (чем меньше значение, тем сильнее регуляризация, что влияет на итоговую точность модели)
    logreg = LogisticRegression(C=reg)

    # Алгоритм оценки RFE
    rfe = RFE(logreg, len(in_data_features))
    rfe = rfe.fit(in_data_train, out_data_train.ravel())
    # Вывод результатов оценки признаков (True/False для каждого)
    print(list(zip(in_data_features, rfe.support_)))

    # Обучаем наш классификатор
    logreg.fit(in_data_train, out_data_train)
    # Вывод оценки точности классификатора для тестового набора после обучения
    score = logreg.score(in_data_test, out_data_test)
    print("Точность классификатора: ", score)
    # Прогнозирование на обученной модели
    y_predicted= logreg.predict(in_data_test)
    # Вывод итогового отчета по результатам прогнозирования
    print(classification_report(out_data_test, y_predicted))
    # Вывод ROC-кривой
    plot_ROC(logreg, in_data_test, out_data_test)
Соседние файлы в папке 4