Автор материала: Минасян Гульнара (Slack: @Gulnara)
В исследовании используется датасет, содержащий данные о пользователях интернет-магазина одежды, их отзывы на купленные товары, а также бинарный признак, указывающий. рекомендует клиент данный товар к покупке или нет.
Цель исследования: определить, будет ли человек рекомендовать покупку другим на основе оставленного отзыва и других признаков. Будем решать задачу бинарной классификации
Датасет включает 23486 строки и 10 столбцов переменных. Каждая строка соответствует отзыву покупателя и включает следующие переменные:
Clothing ID: Целочисленная категориальная переменная, обозначающая конкретный товар, на который дается отзыв.
Age: Положительная целочисленная переменная, обозначающая возраст покупателя, оставившего отзыв.
Title: Строковая переменная, содержащая заголовок отзыва.
Review Text: Строковая переменная, содержащая тело отзыва.
Rating: Положительная целочисленная категориальная переменная отражает оценку товара покупателем, от 1 до 5, где 1 - худшая оценка, а 5 - наилучшая.
Recommended IND: Бинарная переменная, указывающая, рекомендует клиент продукт или нет, где рекомендует - 1, 0 - не рекомендует.
Positive Feedback Count: Положительная целочисленная переменная, отражающая количество других клиентов, считающих отзыв положительным.
Division Name: Наименование категории товара первого порядка.
Department Name: Наименование категории товара второго порядка.
Class Name: Наименование категории товара третьего порядка.
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.metrics import roc_auc_score, f1_score, recall_score, precision_score
%matplotlib inline
from matplotlib import pyplot as plt
plt.style.use(['seaborn-darkgrid'])
plt.rcParams['figure.figsize'] = (12, 9)
plt.rcParams['font.family'] = 'DejaVu Sans'
df = pd.read_csv('Womens Clothing E-Commerce Reviews.csv')
df = df.drop('Unnamed: 0', axis=1)
df.info()
df.head()
Посмотрим какие категории включают в себя Division Name, Department Name, Class Name и как они распределены
#Division Name
plt.figure(figsize=(10,4))
ax = sns.countplot(df['Division Name'])
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right")
plt.tight_layout()
plt.show()
#Department Name
plt.figure(figsize=(10,4))
ax = sns.countplot(df['Department Name'])
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right")
plt.tight_layout()
plt.show()
#Department Name
plt.figure(figsize=(10,4))
ax = sns.countplot(df['Class Name'])
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right")
plt.tight_layout()
plt.show()
Оценим распределение клиентов интернет-магазина по возрасту
plt.figure(figsize=(10,4))
ax = sns.countplot(df['Age'])
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right")
plt.tight_layout()
plt.show()
Разобьем клиентов на возрастные группы для удобства дальнейшего анализа:
18 - 30 - 1 группа
30 - 40 - 1 группа
40 - 50 - 3 группа
50 - 60 - 4 группа
60 - 70 - 5 группа
свыше 70 - 6 группа
df['Age_str'] = df['Age']
df.loc[df['Age_str'] <= 30, 'Age_str'] = 1
df.loc[(df['Age_str'] >= 31) & (df['Age_str'] <= 40), 'Age_str'] = 2
df.loc[(df['Age_str'] >= 41) & (df['Age_str'] <= 50), 'Age_str'] = 3
df.loc[(df['Age_str'] >= 51) & (df['Age_str'] <= 60), 'Age_str'] = 4
df.loc[(df['Age_str'] >= 61) & (df['Age_str'] <= 70), 'Age_str'] = 5
df.loc[(df['Age_str'] > 70), 'Age_str'] = 6
df.head()
Заменим пропуски в данных. Поскольку, как мы увидели в информации о данных пропуски наблюдаются в категориальных данных, заменим их на категорию nan
df['Title'] = df['Title'].fillna('nan')
df['Review Text'] = df['Review Text'].fillna('nan')
df['Division Name'] = df['Division Name'].fillna('nan')
df['Department Name'] = df['Department Name'].fillna('nan')
df['Class Name'] = df['Class Name'].fillna('nan')
df.head()
plt.figure(figsize=(10,4))
ax = sns.countplot(df['Age_str'])
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right")
plt.tight_layout()
plt.show()
Оценим сколько клиентов в выборке рекомендуют и нерекомендуют товар после покупки
df_recommended = df[df['Recommended IND'] == 1]
df_not_recommended = df[df['Recommended IND'] == 0]
df_recommended.shape
df_not_recommended.shape
Посмотрим распределение в данных выборках по возрастам
plt.figure(figsize=(10,4))
ax = sns.countplot(df_recommended['Age_str'])
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right")
plt.tight_layout()
plt.show()
ax1 = sns.countplot(df_not_recommended['Age_str'])
ax1.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right")
plt.tight_layout()
plt.show()
Проведем анализ влияния признаков на целевую переменную:
def plot_distribution(df, var, target, yl=4, **kwargs):
row = kwargs.get('row', None)
col = kwargs.get('col', None)
facet = sns.FacetGrid(df, hue=target, aspect=4, row=row, col=col)
facet.map(sns.kdeplot, var, shade=True)
facet.set(xlim=(0, df[var].max()), ylim=(0, yl))
facet.add_legend()
plt.show()
plot_distribution(df, 'Age_str', 'Recommended IND', 1.2)
Исходя из этого графика, мы видим, что люди возрастом от 30 до 50 лет чаще нерекомендуют товар (чаще недовольны товаром)
plot_distribution(df, 'Rating', 'Recommended IND', 3)
С рейтингом связь очевидна, чем выше бал рейтинга, тем больше человек рекомнедуют товар, однако вызывает интерес рейтинг 3, когда оценка нейтральна, однако количество нерекомендаций выше и 4, когда рейтинг хороший, однако есть некоторое количество людей, не рекомендующих товар.
Они выбранные модели сравнивались на основе трёх метрик: точность, полнота и F-мера. Доля правильных решений классификатора (accuracy) не использовалась для сопоставления моделей, так как при обучении на несбалансированных данных она не даёт никакой важной информации.
Для обучения модели были выбраны два наиболее часто используемых алгоритма машинного обучения для работы с несбалансированными данными: логистическая регрессия и метод опорных векторов с линейным ядром (Linear SVM).
Разделим выборку на обучающую и тестовую в соотношении 70 к 30
from sklearn.model_selection import train_test_split
train, test = train_test_split(df, test_size=0.3)
Зададим вектор признаков и целевую переменную
X_train, y_train = train.drop('Recommended IND', axis = 1), train['Recommended IND']
X_test, y_test = test.drop('Recommended IND', axis = 1), test['Recommended IND']
Проведем подготовку и токенизацию отзывов и их тем при помощи "мешка слов"
X_text_train = X_train['Review Text']
X_text_test = X_test['Review Text']
cv = CountVectorizer()
cv.fit(X_text_train)
len(cv.vocabulary_)
Закодируем предложения из текстов обучающей выборки индексами входящих слов. Используем разреженный формат.
X_text_train_cv = cv.transform(X_text_train)
Преобразуем так же тестовую выборку.
X_text_test_cv = cv.transform(X_text_test)
Обучим логистическую регрессию на данных отзывов
%%time
logit = LogisticRegression(n_jobs=-1, random_state=7)
logit.fit(X_text_train_cv, y_train)
Обучим LinearSVC
%%time
svm = LinearSVC(random_state=7)
svm.fit(X_text_train_cv, y_train)
%%time
from sklearn.pipeline import make_pipeline
text_pipe_logit = make_pipeline(CountVectorizer(),
LogisticRegression(n_jobs=-1, random_state=7))
text_pipe_logit.fit(X_text_train, y_train)
%%time
from sklearn.model_selection import GridSearchCV
param_grid_logit = {'logisticregression__C':
np.logspace(-5, 0, 6)}
grid_logit = GridSearchCV(text_pipe_logit, param_grid_logit, cv=3, n_jobs=-1)
grid_logit.fit(X_text_train, y_train)
Лучшее значение C и соответствующее качество на кросс-валидации.
grid_logit.best_params_, grid_logit.best_score_
То же самое для LinearSVC.
%%time
text_pipe_svm = make_pipeline(CountVectorizer(), LinearSVC(random_state=7))
text_pipe_svm.fit(X_text_train, y_train)
print(text_pipe_svm.score(X_text_test, y_test))
%%time
param_grid_svm = {'linearsvc__C': np.logspace(-5, 0, 6)}
grid_svm = GridSearchCV(text_pipe_svm, param_grid_svm, cv=3, n_jobs=-1)
grid_svm.fit(X_text_train, y_train);
grid_svm.best_params_, grid_svm.best_score_
def plot_grid_scores(grid, param_name):
plt.plot(grid.param_grid[param_name], grid.cv_results_['mean_train_score'],
color='green', label='train')
plt.plot(grid.param_grid[param_name], grid.cv_results_['mean_test_score'],
color='red', label='test')
plt.legend();
plot_grid_scores(grid_logit, 'logisticregression__C')
grid_svm.best_params_, grid_svm.best_score_
plot_grid_scores(grid_svm, 'linearsvc__C')
%%time
logit = LogisticRegression(C = 0.1, n_jobs=-1, random_state=7)
logit.fit(X_text_train_cv, y_train)
logit_pred = logit.predict(X_text_test_cv)
a = round(recall_score(y_test, logit_pred), 5)
b = round(precision_score(y_test, logit_pred), 5)
c = round(f1_score(y_test, logit_pred), 5)
print('Recall score :', a, 'Precision score:', b, 'F1 score:', c)
%%time
svm = LinearSVC(C = 0.01, random_state=7)
svm.fit(X_text_train_cv, y_train)
svm_pred = svm.predict(X_text_test_cv)
a = round(recall_score(y_test, svm_pred), 5)
b = round(precision_score(y_test, svm_pred), 5)
c = round(f1_score(y_test, svm_pred), 5)
print('Recall score :', a, 'Precision score:', b, 'F1 score:', c)
По итогу были построены модели логистической регрессии и опорных векторов, предсказывающие по тексту отзыва, рекомендует ли клиент этот товар. Было выявлено, что модель опорных векторов справляется с этой задачей лучше по параметрам точность, F-мера. По параметру полнота получился примерно одинаковый результат.
Полученное качество модели логистической регрессии, оцененное с помощью метрики F-меры составляет 0.94, что говорит о высокой предсказательной способности модели.
Использовать данную модель можно при анализе отзывов на товары в социальных сетях, либо интернет магазинах.
Возможные улучшения:
добавить в модель дополнительные признаки,
провести более детмальную токенизацию с удалением стоп слов,
проанализровать наиболее часто встречаемые слова в отзывах, за которыми следует рекомендация или нет.