Amazon рассматривает анализ настроений с использованием НЛП и создание механизма классификации настроений с использованием наивного Байеса

Понимание бизнеса

С растущей тенденцией к оцифровке и преобладанием мобильных телефонов и доступа в Интернет все больше потребителей присутствуют в Интернете, и их мнение имеет большое значение для любой компании, основанной на продуктах, особенно для предприятий B2C. Отрасли пытаются точно настроить свои стратегии в соответствии с потребностями потребителей, поскольку потребители оставляют некоторые намеки на свой выбор во время своего присутствия в Интернете.

Всякий раз, когда мы работаем над разработкой рыночных стратегий или над разработкой продукта, нам нужно обратить внимание на несколько стандартных вещей, которые можно разработать, проанализировав набор данных, с которым вы будете работать.

  • Анализируя настроение отзывов, вы можете найти характеристики телефонов, которые вызвали положительные/отрицательные чувства. Это поможет компаниям включить или улучшить эти конкретные функции при разработке нового продукта. Если данные относятся к брендам конкурентов, компания выиграет, не повторив той же ошибки при разработке продукта, что и их конкурент.
  • Компании могут эффективно разрабатывать свои рекламные кампании, выделяя функции, о которых больше всего говорят потребители.
  • Сравнение цен конкурентов и их доли рынка поможет компаниям определить цену своей продукции.
  • Можно предположить, что если количество отзывов о конкретном бренде велико, то и количество людей, покупающих телефоны этого бренда, также велико. Это поможет компаниям оценить долю рынка своих конкурентов.
  • Прежде чем купить какой-либо продукт, мы все смотрим на аналогичные товары разных брендов. Эти данные помогут компаниям узнать своих основных конкурентов на рынке.

Постановка задачи

Предположим, ваш клиент — производитель мобильных устройств из США, который вышел на рынок три года назад. Поскольку они являются новичком в этом секторе, они хотят понять своих конкурентов и предпочтения своих пользователей, чтобы соответствующим образом разрабатывать свои стратегии. Они хотят настроить маркетинговые стратегии, чтобы повысить ценность своего бренда, предоставить клиентам функции, которые приносят наибольшую пользу, и сократить разрыв между спросом и предложением. Их цель состоит в том, чтобы увеличить долю рынка, а также стоимость бренда.

Предположим, что к вам как к поставщику услуг по анализу данных обратился производитель мобильных телефонов. Они хотят, чтобы вы предоставили им важную информацию об индустрии мобильных телефонов, чтобы помочь им достичь своей цели. Их цель — оптимально разработать новый продукт и создать маркетинговые стратегии.

Бизнес-цель

  • Часть 1. Получение информации о бизнесе, полезной для разработки продукта и маркетинга.
  • Часть 2: Создание механизма классификации настроений.

Пройденные шаги

Шаг 1: Предварительная обработка данных

Шаг 2: ЭДА

Шаг 3: Текстовая аналитика

Шаг 4: Создание механизма классификации настроений.

Словарь данных

Вы можете найти файлы данных, нажав здесь.

  1. Данные телефона в виде файла .csv:содержит информацию о действиях потребителей.
  • общая: общая оценка конкретного продукта, данная пользователем.
  • Подтверждено: является ли пользователь проверенным рецензентом или нет.
  • reviewerID: идентификатор рецензента, например. A2SUAM1J3GNN3B
  • asin: идентификатор продукта, например. 0000013714. ASIN означает стандартный идентификационный номер Amazon. Это 10-значный буквенно-цифровой уникальный идентификатор, который присваивается Amazon.com и его партнерами. Он в основном используется для идентификации продукта в их каталоге продуктов.
  • стиль: информация о физических характеристиках, таких как цвет.
  • reviewerName: имя рецензента.
  • reviewText:отзыв, предоставленный рецензентом.
  • резюме:резюме обзора
  • unixReviewTime: время проверки (время Unix).
  • голосование: голосование за или против отзыва. Отзыв с большим количеством голосов может иметь более высокое значение.
  • изображение: изображение типа .png или .jpg и т. п., предоставленное рецензентом.
  • review_sentiment: настроенное заранее настроение отзыва, которое может быть как положительным, так и отрицательным. Метки не будут доступны для данных в реальном времени, и для такой классификации необходимо построить модель.

2. Метаданные телефона в виде заархивированного файла .json.Эти данные содержат информацию о продукте и не зависят от действий потребителя/рецензента и включают описание, цену, рейтинг продаж, информацию о бренде и ссылки для совместных покупок. , и т. д.

  • Категория.Список категорий, к которым относится продукт, например ["Сотовые телефоны и аксессуары", "Сотовые телефоны"].
  • tech1: технические характеристики и технология продукта, например Phablet (гибрид телефона и планшета).
  • tech2: содержит данные, аналогичные данным tech1.
  • описание: краткое описание продукта.
  • fit: столбец, в котором нет значений для набора данных мобильного телефона.
  • title: название продукта.
  • also_buy: идентификаторы продуктов, которые обычно покупаются вместе с конкретным продуктом.
  • изображение: изображение продукта, сделанное поставщиком.
  • brand: торговая марка продукта.
  • функция. Особенности продукта, включая такие характеристики, как 4G/3G, язык, поддерживаемые приложения и т. д.
  • рейтинг:рейтинг продукта, присвоенный Amazon на основе различных параметров в разных сегментах. Это метрика, которая объясняет взаимосвязь между продуктами в категории на основе их эффективности продаж. Рейтинг продаж обновляется ежечасно, он может варьироваться от 1 до более 1 миллиона и зависит от сезонности, а его алгоритм остается нераскрытым.
  • also_view: товары, которые также просматриваются при поиске этого товара.
  • детали:содержит детали с точки зрения доставки, такие как размер продукта, вес доставки и т. д.
  • main_cat: основная категория, к которой относится продукт, например, «Вся электроника».
  • similar_item:сопутствующие товары.
  • price: цена товара.
  • asin:идентификатор продукта, например, 0000031852

3. pos_words.txt: это текстовый файл, содержащий набор положительных слов. Этот файл может помочь вам извлечь некоторые функции из текста обзора. Есть несколько источников, доступных в Интернете, которые дают вам корпус положительных и отрицательных слов. Вы также можете настроить его на основе вашего приложения.

4. neg_words.txt:Подобно корпусу положительных слов, этот текстовый файл содержит список слов, которые могут быть частью отрицательных отзывов пользователей.

5. Stop_words_long.txt: обычно мы можем удалить стоп-слова с помощью библиотеки NLTK. Одним из недостатков этого является то, что слова отрицания, такие как ни, не и никогда, не считаются стоп-словами. Удаление этих слов, возможно, не повлияет на классификацию спама/любимой почты, но определенно повлияет на классификацию тональности. Поэтому они предоставили нам индивидуальный список стоп-слов, которые вы можете использовать для анализа.

Чтение и понимание данных

# Reading the json version of Meta Data (it shows how to read json file in colab notebooks)
df_meta

# Reading the Cell Phones and Accessories data 
df_celldata

# Reading the Phone data final (it's the consolidated & structured version of df_meta & df_celldata. Hence we will not use this data further)
df_phonedata

Статистика:

  1. df_phonedata кажется самым сложным и кульминацией как df_meta, так и df_celldata. Но df_celldata, кажется, имеет более крупные записи.
  2. Сотовые данные и данные телефона, по-видимому, имеют схожие атрибуты, но различаются по форме и содержанию. Следовательно, необходимо дополнительно анализировать и понимать соответствующие столбцы из обоих.
  3. Это также может включать объединение двух из них с уникальными столбцами после анализа. Давайте посмотрим.

Давайте начнем с объединения df_meta и df_celldata, поскольку они несут много информации для нашего анализа.

# Merging df_meta and df_celldata using the unique asin column values

merge_df = df_meta.merge(df_celldata, on = "asin")

merge_df.shape
(1129035, 29)

merge_df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1129035 entries, 0 to 1129034
Data columns (total 29 columns):
 #   Column            Non-Null Count    Dtype  
---  ------            --------------    -----  
 0   category          1129035 non-null  object 
 1   tech1             1129035 non-null  object 
 2   description       1129035 non-null  object 
 3   fit               1129035 non-null  object 
 4   title             1129035 non-null  object 
 5   also_buy          1129035 non-null  object 
 6   image_x           1129035 non-null  object 
 7   tech2             1129035 non-null  object 
 8   brand             1129035 non-null  object 
 9   feature           1129035 non-null  object 
 10  rank              1129035 non-null  object 
 11  also_view         1129035 non-null  object 
 12  details           1129035 non-null  object 
 13  main_cat          1129035 non-null  object 
 14  similar_item      1129035 non-null  object 
 15  date              1129035 non-null  object 
 16  price             1129035 non-null  object 
 17  asin              1129035 non-null  object 
 18  overall           1129035 non-null  float64
 19  verified          1129035 non-null  bool   
 20  reviewerID        1129035 non-null  object 
 21  style             605459 non-null   object 
 22  reviewerName      1128879 non-null  object 
 23  reviewText        1128267 non-null  object 
 24  summary           1128510 non-null  object 
 25  unixReviewTime    1129035 non-null  int64  
 26  vote              92176 non-null    object 
 27  image_y           27106 non-null    object 
 28  review_sentiment  1129035 non-null  object 
dtypes: bool(1), float64(1), int64(1), object(26)
memory usage: 250.9+ MB

merge_df.describe()
       overall      unixReviewTime
count 1.129035e+06  1.129035e+06
mean  4.221165e+00  1.440369e+09
std   1.232076e+00  4.579979e+07
min   1.000000e+00  1.035331e+09
25%   4.000000e+00  1.416355e+09
50%   5.000000e+00  1.444349e+09
75%   5.000000e+00  1.470442e+09
max   5.000000e+00  1.538438e+09

Статистика:

  1. Получив сводную картину, мы видим, что данные содержат много шума, который необходимо предварительно обработать.
  2. Как только данные очищены и готовы, мы можем начать наш исследовательский анализ данных и обработку текста.

Шаг 1: Краткий обзор предварительной обработки данных

Мы начинаем с определения нежелательных функций и тех, которые имеют большие пропущенные значения. Как только они будут идентифицированы, мы собираемся отбросить их, так как они не будут иметь большого значения в нашем дальнейшем анализе.

# Dropping some unwanted and noisy columns
merge_df.drop(['category', 'description', 'image_x', 'rank', 'details', 'similar_item', 'date', 'image_y'],  axis = 1, inplace = True)

# Checking missing values columns
import missingno as msno
msno.bar(merge_df)

# Checking missing values percentages

# Checking exact Null Values
def null_values(merge_df):
    return round((merge_df.isnull().sum()/len(merge_df)*100).sort_values(ascending = False),2)

null_values(merge_df)
vote                91.84
style               46.37
reviewText           0.07
summary              0.05
reviewerName         0.01
tech1                0.00
overall              0.00
unixReviewTime       0.00
reviewerID           0.00
verified             0.00
asin                 0.00
fit                  0.00
price                0.00
main_cat             0.00
also_view            0.00
feature              0.00
brand                0.00
tech2                0.00
also_buy             0.00
title                0.00
review_sentiment     0.00
dtype: float64

# Dropping vote and style as it has more than 45% missing values
merge_df.drop(['vote','style'], axis = 1, inplace = True)

Остальные столбцы можно разобрать для дальнейшего понимания и соответствующих действий. Некоторые из них могут быть вменены с помощью агрегированных значений или с использованием 0, а некоторые могут быть преобразованы. Самое главное, столбцы с очень меньшим количеством отсутствующих значений можно оставить как есть.

# Imputing mode values in the reviewerName column. 
merge_df['reviewerName'].fillna(merge_df['reviewerName'].mode()[0], axis = 0, inplace = True)

# Rechcking the remaing missing values
reviewText          0.07
summary             0.05
tech1               0.00
asin                0.00
unixReviewTime      0.00
reviewerName        0.00
reviewerID          0.00
verified            0.00
overall             0.00
price               0.00
fit                 0.00
main_cat            0.00
also_view           0.00
feature             0.00
brand               0.00
tech2               0.00
also_buy            0.00
title               0.00
review_sentiment    0.00
dtype: float64

# Dissecting Summary Column: 
merge_df.summary.unique()

# Dissecting reviewText Column: 
merge_df.reviewText.unique()

# Insights:
 # summary and reviewText seems to be in a string format with a small percentage of missing values, hence we will leave it as it is.

# Converting unix review time to date-time format & then dropping it

#Transforming unixReview time to date time format
from datetime import datetime, timedelta
merge_df['Date&Time'] = merge_df['unixReviewTime'].apply(lambda d: (datetime.fromtimestamp(d) - timedelta(hours=2)).strftime('%Y-%m-%d'))

# Dropping unixReviewTime 
merge_df.drop('unixReviewTime', axis = 1, inplace = True)

# Dissecting tech columns
merge_df["tech2"].unique()
# or 
merge_df.loc[:50,"tech2"]

# Dropping tech1 and tech2 as it has more than 45% missing values
merge_df.drop(['tech1','tech2'], axis = 1, inplace = True)

# Dissecting fit column
merge_df["fit"].unique()
# or
merge_df.loc[:10,"fit"]

# Dropping fit as it has more than 45% missing values
merge_df.drop('fit', axis = 1, inplace = True)

Insights:
Nothing could be understood by exploring tech columns hence dropping them both

Кроме того, мы подбираем набор данных для анализа нашей конкретной цели, которая называется Сотовые телефоны и аксессуары. Затем следуют еще несколько шагов предварительной обработки данных, необходимых после подмножества, которые можно найти в подробностях по ссылке блокнота jupyter.

Понимание данных (числовой и категориальный анализ)

# Check the summary for the numeric columns 

merge_df.describe()

         price        ratings       year        month
count 1.045517e+06 1.045517e+06 1.045517e+06 1.045517e+06
mean  1.465343e+01 4.218868e+00 2.015127e+03 6.453106e+00
std   3.204665e+01 1.233017e+00 1.487766e+00 3.512060e+00
min   1.000000e-02 1.000000e+00 2.002000e+03 1.000000e+00
25%   8.990000e+00 4.000000e+00 2.014000e+03 3.000000e+00
50%   9.990000e+00 5.000000e+00 2.015000e+03 7.000000e+00
75%   9.990000e+00 5.000000e+00 2.016000e+03 1.000000e+01
max   9.999900e+02 5.000000e+00 2.018000e+03 1.200000e+01

Инсайты

  • Поскольку мы заменили почти 45% нулевых значений цены на 9,99, это стало нашей средней ценой для продуктов, перечисленных в разделе «Сотовые телефоны и аксессуары». Кроме того, существует огромный разброс данных, поскольку значение стандартного отклонения больше среднего.
  • Большинство оценок положительные, т.е. 4 и 5.
  • Наибольшее количество отзывов получено в 2015 и 2016 годах.
# Checking the ratio of Actual Positive and Actual Neative labels under review_sentiment in the dataframe.

merge_df.review_sentiment.value_counts(normalize = True)*100

POSITIVE    78.406855
NEGATIVE    21.593145
Name: review_sentiment, dtype: float64

Шаг 2: ЭДА

Идея для выполнения EDA:

  • Использование TextBlob для вычисления полярности тональности, которая находится в диапазоне [-1,1], где 1 означает позитивную тональность, а -1 — негативную тональность.
  • Создание новой функции для продолжительности обзора, чтобы просмотреть их распространение.
  • Создание новой функции подсчета слов в обзоре для просмотра их распределения
# Creating a function to get the polarity of the sentiment in the range of -1 and 1
from textblob import TextBlob

def GetPolarity(text):
  try:
    return TextBlob(text).sentiment.polarity
  except: 
    return None
  
#Create a new columns ‘Polarity’ 
merge_df['polarity'] = merge_df['reviewText'].apply(GetPolarity)

# Creating a function to get the TB Scores. 

def GetTBScore(score):
  if score < 0:
    return 'Negative'
  elif score == 0:
    return 'Neutral'
  else:
    return 'Positive'

#Create a new columns 'TB_analysis'
merge_df['TB_score'] = merge_df['polarity'].apply(GetTBScore)
# Creating review length and review word count column. 

merge_df['review_len'] = merge_df['reviewText'].astype(str).apply(len)
merge_df['word_count'] = merge_df['reviewText'].apply(lambda x: len(str(x).split()))
merge_df.head()
# Let's understand how the sentiment polarity score works, for that we randomly select 5 reviews with the highest sentiment polarity score (1):
print('5 random reviews with the highest positive sentiment polarity: \n')
cl = merge_df.loc[merge_df.polarity == 1, ['reviewText']].sample(5).values
for c in cl:
    print(c[0])

5 random reviews with the highest positive sentiment polarity: 
Excellent, durable
These work well, and they're the perfect length!
Works perfect
Awesome!
Perfect for your LG g vista

# Let's understand how the sentiment polarity score works, for that we randomly select 5 reviews with the most neutral sentiment polarity score (zero):
print('5 random reviews with the most neutral sentiment(zero) polarity: \n')
cl = merge_df.loc[merge_df.polarity == 0, ['reviewText']].sample(5).values
for c in cl:
    print(c[0])

5 random reviews with the most neutral sentiment(zero) polarity: 
i like it
didnt work as promised
Never had a problem with these screen protectors.  Works as described
work, but don't show correct conpacity
Saves your phone

# Then randomly select 5 reviews with the most negative sentiment polarity score (zero):
print('5 reviews with the most negative polarity: \n')
cl = merge_df.loc[merge_df.polarity == -1.0, ['reviewText']].sample(5).values
for c in cl:
    print(c[0])

5 reviews with the most negative polarity: 
Bulky but protects like it should. Clip is crap!
Terrible reproduction Doesn't hold the phone
Horrible. Just horrible
Horrible
Son Loves it!! Still using, no issues!! Has been dropped REPEATEDLY.......PROTECTS LIKE CRAZY!!!

Статистика:

Вы можете точно увидеть, что предложенные случайные обзоры соответствуют нашим полярностям, определенным с помощью TextBlob.

Одномерный анализ

# importing necessary libraries 
import seaborn as sns
import matplotlib
import matplotlib.pyplot as plt
matplotlib.rcParams['figure.figsize'] = (10.0, 6.0)
import plotly.graph_objs as go
#import plotly.plotly as py
import plotly.figure_factory as ff
import plotly.express as px
from plotly.offline import iplot
import cufflinks as cf
cf.go_offline()
cf.set_config_file(world_readable=True, theme='pearl', offline=False)

%matplotlib inline
# Review Sentiment Polarity distribution 

sns.set_style("dark")
plt.style.use("ggplot")
plt.figure(figsize=[12,6])
sns.distplot(merge_df['polarity'], rug = True, color = 'royalblue').set_title('Sentiment Polarity Distribution')
plt.show()

Инсайты

Подавляющее большинство оценок полярности настроений больше нуля, что означает, что большинство из них являются положительными отзывами.

# Review Text Length distribution 

sns.set_style("dark")
plt.style.use("ggplot")
plt.figure(figsize=[12,6])
sns.distplot(merge_df['review_len'], rug = True, color = 'royalblue').set_title('Review Text Length Distribution')
plt.show()

Инсайты

Большинство обзоров имеют длину текста от 500 до 1000.

# Review Word Count Distribution 

sns.set_style("dark")
plt.style.use("ggplot")
plt.figure(figsize=[12,6])
sns.distplot(merge_df['word_count'], rug = True, color = 'royalblue').set_title('Review Word Count Distribution')
plt.show()

Инсайты

Каждый обзор содержит примерно от 50 до 200 слов.

# Review Price Distribution 

sns.set_style("dark")
plt.style.use("ggplot")
plt.figure(figsize=[12,6])
sns.distplot(merge_df['price'], rug = True, color = 'royalblue').set_title('Review Price Distribution')
plt.show()

Инсайты

Большинство перечисленных продуктов, т.е. сотовые телефоны и аксессуары, стоят от 10 до 150 долларов.

# Review Rating Count Distribution 

sns.set_theme(style="darkgrid")
plt.figure(figsize=[12,6])
sns.countplot(merge_df['ratings'], palette = 'inferno')
plt.title('Rating Count Distribution', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

Инсайты

  • Большинство продуктов получили положительные оценки, то есть 4 или 5.
# Review TB_Score count percentage wise

plt.figure(figsize=[12,6])
(merge_df.TB_score.value_counts(normalize = True)*100)[:10].plot.bar()
plt.title('TB_Score Count Distribution', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

Инсайты

  • Как видно, большинство отзывов относятся к категории положительных.
# Review verified reviews percentage wise

plt.figure(figsize=[12,6])
(merge_df.verified.value_counts(normalize = True)*100)[:10].plot.bar()
plt.title('Verified Reviews Count Distribution', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

Инсайты

Из всех полученных отзывов более 80% подтверждены на сайте.

# Distribution of brands listed on Amazon having more than 5000 reviews

plt.figure(figsize=[18,8])
brand_counts = merge_df.groupby('brand').count()['reviewerID'].sort_values(ascending=False)
brand_counts[brand_counts > 5000].plot.bar()
plt.title('Brands listed on Amazon having more than 5000 reviews', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

Инсайты

Samsung, Spigen, OtterBox, Generic, Anker и Motorola являются брендами-предшественниками на веб-сайте.

# Distribution of reviewers on Amazon having more than 500 reviews 

plt.figure(figsize=[18,8])
reviewer_count = merge_df.groupby('reviewerName').count()['reviewerID'].sort_values(ascending=False)
reviewer_count[reviewer_count > 500].plot.bar()
plt.title('Reviewers on Amazon having more than 500 reviews', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

Инсайты

Помимо первых двух, Джон, Крис и Майк являются рецензентами с наибольшим количеством отзывов на веб-сайте.

stop_words = [line.rstrip('\n') for line in open('/content/drive/My Drive/sentiment_analysis/stop_words_long.txt')]
# Top 20 unigrams distribution "before" removing stop words

from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import CountVectorizer

pd.set_option('max_colwidth', 100)

def get_top_n_words(corpus, n=None):
  vec = CountVectorizer().fit(corpus)
  bag_of_words = vec.transform(corpus)
  sum_words = bag_of_words.sum(axis=0) 
  words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
  words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
  return words_freq[:n]
common_words = get_top_n_words(merge_df['reviewText'].astype('U').values, 20)
for word, freq in common_words:
  print(word, freq)
df1 = pd.DataFrame(common_words, columns = ['reviewText' , 'count'])
plt.figure(figsize=[18,8])
df1.groupby('reviewText').sum()['count'].sort_values(ascending=False).plot.bar()
plt.title('Top 20 unigram words in review "before" removing stop words', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

the 2837750
it 1694863
and 1474004
to 1287563
is 918685
this 835847
my 742303
for 710246
phone 670016
of 668660
case 609319
on 529213
that 528426
in 520079
with 492427
but 430203
you 426257
not 411769
have 377421
was 325305

from wordcloud import WordCloud

text = " ".join(review for review in df1.reviewText)


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text)
plt.figure(figsize=[15,10])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

# Top 20 unigrams distribution "after" removing stop words

def get_top_n_words(corpus, n=None):
    vec = CountVectorizer(stop_words = 'english').fit(corpus)
    bag_of_words = vec.transform(corpus)
    sum_words = bag_of_words.sum(axis=0) 
    words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
    words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
    return words_freq[:n]
common_words = get_top_n_words(merge_df['reviewText'].apply(lambda x: np.str_(x)), 20)
for word, freq in common_words:
    print(word, freq)
df1 = pd.DataFrame(common_words, columns = ['reviewText' , 'count'])
plt.figure(figsize=[18,8])
df1.groupby('reviewText').sum()['count'].sort_values(ascending=False).plot.bar()
plt.title('Top 20 unigram words in review "after" removing stop words', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

phone 670016
case 609319
great 304849
screen 250315
good 239673
like 227181
just 182254
product 179609
use 165352
love 135928
works 130003
nice 124450
really 121383
iphone 120527
protector 116417
battery 112485
charge 106192
does 105446
fit 105378
time 105058

from wordcloud import WordCloud

text = " ".join(review for review in df1.reviewText)


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text)
plt.figure(figsize=[15,10])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

Инсайты

Игнорирование стоп-слов с помощью функции Countvector помогает нам определить отдельные слова для нашего анализа.

# Top 20 bigrams distribution "before" removing stop words

def get_top_n_words(corpus, n=None):
    vec = CountVectorizer(ngram_range=(2, 2)).fit(corpus)
    bag_of_words = vec.transform(corpus)
    sum_words = bag_of_words.sum(axis=0) 
    words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
    words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
    return words_freq[:n]
common_words = get_top_n_words(merge_df['reviewText'].apply(lambda x: np.str_(x)), 20)
for word, freq in common_words:
    print(word, freq)
df1 = pd.DataFrame(common_words, columns = ['reviewText' , 'count'])
plt.figure(figsize=[18,8])
df1.groupby('reviewText').sum()['count'].sort_values(ascending=False).plot.bar()
plt.title('Top 20 bigram words in review "before" removing stop words', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

the phone 225431
of the 209247
on the 164581
the case 156248
my phone 150240
it is 148639
this case 137482
in the 124266
for the 121788
this is 116948
and the 109934
and it 108450
with the 104141
for my 95523
to the 94894
if you 94280
the screen 87779
screen protector 84057
it was 75164
easy to 70398

from wordcloud import WordCloud

text = " ".join(review for review in df1.reviewText)


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text)
plt.figure(figsize=[15,10])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

# Top 20 bigrams distribution "after" removing stop words

def get_top_n_words(corpus, n=None):
    vec = CountVectorizer(ngram_range=(2, 2), stop_words='english').fit(corpus)
    bag_of_words = vec.transform(corpus)
    sum_words = bag_of_words.sum(axis=0) 
    words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
    words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
    return words_freq[:n]
common_words = get_top_n_words(merge_df['reviewText'].apply(lambda x: np.str_(x)), 20)
for word, freq in common_words:
    print(word, freq)
df1 = pd.DataFrame(common_words, columns = ['reviewText' , 'count'])
plt.figure(figsize=[18,8])
df1.groupby('reviewText').sum()['count'].sort_values(ascending=False).plot.bar()
plt.title('Top 20 bigram words in review "after" removing stop words', fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

screen protector 84730
phone case 36728
works great 35272
great product 25349
great case 21982
screen protectors 21856
love case 17672
samsung galaxy 17427
highly recommend 16376
case fits 15143
case phone 15011
good quality 14771
glass screen 14695
dropped phone 14656
cell phone 14483
really like 14383
protect phone 14270
battery life 14153
tempered glass 13569
easy install 13261

from wordcloud import WordCloud

text = " ".join(review for review in df1.reviewText)


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text)
plt.figure(figsize=[15,10])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

Инсайты

Игнорирование стоп-слов с помощью функции Countvector помогает нам идентифицировать двойные слова для нашего анализа.

Би/многомерный анализ

# Polarity Distribution Month Wise

plt.figure(figsize=[14,8])
sns.boxplot(data = merge_df, orient='v', x = 'month', y='polarity')
plt.title("polarity distribution - Month Wise", fontdict={'fontsize' : 20, 'fontweight' : 5, 'color': 'Green'})
plt.show()

# Polarity Distribution Year Wise

plt.figure(figsize=[14,8])
sns.boxplot(data= merge_df, orient='v', x = 'year', y='polarity')
plt.title("polarity distribution - Year Wise", fontdict={'fontsize' : 20, 'fontweight' : 5, 'color': 'Green'})
plt.show()

# Relationship between Review Length and Word count in a review

plt.figure(figsize=[12,8])
sns.scatterplot(data=merge_df, x="review_len", y="word_count", hue="TB_score", style="TB_score")
plt.show()

# Relationship between Review Length and Polarity in a review

plt.figure(figsize=[12,8])
sns.scatterplot(data=merge_df, x="review_len", y="polarity", hue="TB_score", style="TB_score")
plt.show()

# Relationship between Word Count and Polarity in a review

plt.figure(figsize=[12,8])
sns.scatterplot(data=merge_df, x="word_count", y="polarity", hue="TB_score", style="TB_score")
plt.show()

# Relationship between TB Score and Polarity in a review

plt.figure(figsize=[14,8])
sns.barplot(x=merge_df['TB_score'] , y=merge_df['polarity'], ci=None)
plt.title("TB Score vs Polarity Score", fontdict={'fontsize': 20, 'fontweight': 5, 'color': 'Green'})
plt.show()

Инсайты

С помощью двумерного/многомерного анализа получено не так много информации. Да, мы постепенно соглашаемся с тем, что категории мобильных телефонов оказали огромное влияние, так как было получено больше положительных отзывов. Кроме того, мы поняли, что количество слов в обзоре и его длина коррелируют.

Шаг 3: Текстовая аналитика

Текстовая аналитика в основном выполняется с использованием концепции модели Bag of Words. Модель BOG состоит из двух этапов предварительной обработки, т. е. токенизации и удаления стоп-слов, таких как «is», «an», «the» и т. д. Эти стоп-слова удаляются из текста по двум причинам:

  • В большинстве приложений они не предоставляют никакой полезной информации.
  • Они используют много памяти из-за такой высокой частоты, в которой они присутствуют.

После токенизации документов и удаления стоп-слов следующим шагом будет приведение слов к их базовой форме. Это приводит к избавлению от избыточной информации за счет использования только уникальных токенов. Существует два метода приведения слов к их базовой форме — выделение корней и лемматизация. Давайте рассмотрим каждый из них:

  1. Stemming: это метод, основанный на правилах, который просто отсекает суффикс слова, чтобы получить его корневую форму, которая называется «основой». Например, если вы используете стеммер для определения корней слов строки — «Водитель мчится на машине своего босса», слова «водитель» и «гонка» будут преобразованы в корневую форму путем простого
    отсечения суффиксы «er» и «ing». Таким образом, «водитель» будет преобразован в «драйв», а «гонки» будет преобразован в «гонки». Теперь вы можете подумать, что корневые формы (или основы) не похожи на корневые слова — «драйв» и «гонка». Вам не нужно об этом беспокоиться, потому что стеммер преобразует все варианты «драйв» и «гонки» только в эти корневые формы. Таким образом, он преобразует «драйв», «вождение» и т. д. в «драйв», а «гонка», «гонщик» и т. д. в «гонщик».
  2. Лемматизация: это более сложная техника, более интеллектуальная в том смысле, что она не просто отсекает суффикс слова. Вместо этого он берет входное слово и ищет его базовое слово, рекурсивно перебирая все варианты словарных слов. Базовое слово в этом случае называется «лемма». Такие слова, как «ноги», «погнали», «встали», «купил» и т. д. нельзя привести к их правильной базовой форме с помощью стеммера. Но лемматизатор может привести их к правильной базовой форме. Самый популярный лемматизатор — лемматизатор WordNet, созданный группой профессоров Принстонского университета.

Стеммер работает намного быстрее, но дает неточные результаты во многих случаях, когда вариант слова не имеет формы «корневое слово + суффикс». С другой стороны, лемматизатор дает точные результаты, но работает намного
медленнее, чем стеммер. Кроме того, вам необходимо передать тег POS слова вместе со словом, которое нужно лемматизировать.

Модель мешка слов в сообщениях с основой дает нам приведенное ниже облако слов.

# Word Cloud Using Porter Stemming Bags of Word Model 

from wordcloud import WordCloud

text1 = " ".join(review for review in vectorizer.get_feature_names())


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text1)
plt.figure(figsize = [15,12])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

Облако Word с использованием модели Lemmatizing Wordnet Bag of Word

# Word Cloud Using Lemmatizing Wordnet Bags of Word Model 

from wordcloud import WordCloud

text2 = " ".join(review for review in vectorizer.get_feature_names())


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text2)
plt.figure(figsize = [15,12])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

Статистика:

  • Лемматизация, кажется, гораздо глубже работает со словами.

Облако слов с использованием пакетов Tf-IDF модели Word

# Word Cloud Using Tf-IDF Bags of Word Model 

from wordcloud import WordCloud

text3 = " ".join(review for review in vectorizer.get_feature_names())


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text3)
plt.figure(figsize = [15,12])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

Статистика:

Наконец, мы видим, что метод TF-IDF производит гораздо более чистые и менее шумные слова, поэтому мы будем строить нашу модель на этом методе.

Примечание. Чтобы увидеть, как я подробно использовал методы анализа текста с кодом, вы можете посетить мой блог по этой ссылке.

Шаг 4. Создание механизма классификации настроений с использованием наивного Байеса.

При построении движка мы собираемся передать в субаренду данные, которые включают только те переменные, на основе которых будет построена и оценена Модель. Для этого мы выбираем фирменное наименование, имя рецензента и текст обзора, по которым будут делаться прогнозы, а также настроение отзыва, чтобы оценить, как работают наши прогнозы. Естественно, чтобы построить модель, мы начинаем с разделения наших данных на обучение и тестирование. Здесь мы собираемся применить метод построения модели «Мультиномиальный наивный байесовский классификатор». Ниже приведен код и вывод.

from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics import accuracy_score, confusion_matrix

mnb = Pipeline([('vect', CountVectorizer(stop_words = stop_words)),
               ('tfidf', TfidfTransformer()),
               ('clf', MultinomialNB()),
              ])
mnb.fit(X_train['reviewText'].apply(lambda x: np.str_(x)), y_train)

#%%time
from sklearn.metrics import classification_report
y_pred = mnb.predict(X_test['reviewText'].apply(lambda x: np.str_(x)))

print('accuracy %s' % accuracy_score(y_pred, y_test))
#print(classification_report(y_test, y_pred, target_names = df_text['review_sentiment'].unique()))

accuracy 0.8105504119162394

Статистика:

Точность = правильно спрогнозированные ярлыки / общее количество ярлыков

  • «Положительные» отзывы на самом деле идентифицируются как положительные
  • «Отрицательные» отзывы на самом деле идентифицируются как отрицательные

Так как мы достигли точности 81%, но теперь возникает вопрос — достаточно ли точности, чтобы оценить качество модели? И ответ - категорическое НЕТ!

Давайте посмотрим на матрицу путаницы, которую мы получили для нашей окончательной модели.

mnb

Pipeline(steps=[('vect',
                 CountVectorizer(stop_words=['a', 'about', 'above', 'across',
                                             'after', 'again', 'all', 'almost',
                                             'alone', 'along', 'already',
                                             'also', 'although', 'always',
                                             'among', 'an', 'and', 'another',
                                             'any', 'anybody', 'anyone',
                                             'anything', 'anywhere', 'are',
                                             'area', 'areas', 'around', 'as',
                                             'ask', 'asked', ...])),
                ('tfidf', TfidfTransformer()), ('clf', MultinomialNB())])

Статистика:

  • Использование стоп-слов в CountVectorizer помогло нам повысить точность модели.
# predicting probabilities of test data
proba = mnb.predict_proba(X_test['reviewText'].apply(lambda x: np.str_(x)))
proba

array([[0.05972756, 0.94027244],
       [0.09468221, 0.90531779],
       [0.05026127, 0.94973873],
       ...,
       [0.0150833 , 0.9849167 ],
       [0.0150833 , 0.9849167 ],
       [0.01459351, 0.98540649]])

# confusion matrix
from sklearn import metrics
metrics.confusion_matrix(y_test, y_pred)

array([[  8698,  58736],
       [   686, 245536]])

import seaborn as sns
import matplotlib
import matplotlib.pyplot as plt


cm = confusion_matrix(y_test,y_pred)
cm_df = pd.DataFrame(cm,
                     index = ['Negative','Positive'], 
                     columns = ['Negative','Positive'])
#Plotting the confusion matrix
plt.figure(figsize=(5,4))
sns.heatmap(cm_df, annot=True,fmt='g')
plt.title('Confusion Matrix')
plt.ylabel('Actal Values')
plt.xlabel('Predicted Values')
plt.show()

Статистика:

  • Фактические метки располагаются вдоль столбца, а прогнозируемые метки — вдоль строк.
  • 686 отзывов на самом деле являются «положительными», но прогнозируются моделью как «отрицательные», тогда как 8698 отзывов правильно прогнозируются как «отрицательные». С другой стороны, 686 и 245536 в целом являются положительными, но модель упустила небольшую часть, что вполне выполнимо.
  • Теперь модель предсказывает 58736 положительных отзывов, тогда как это отрицательные отзывы, что может ввести в заблуждение при идентификации тех брендов, у которых есть положительные отзывы. Это немного рискованно.

Это подводит нас к двум наиболее часто используемым метрикам для оценки модели классификации:

  • Чувствительность: (из всех положительных результатов, сколько вы действительно обнаружили)
  • Специфика: (из всех негативов, сколько вы на самом деле обнаружили)
  • Фактическое/прогнозируемое отсутствие оттока Отток
  • Не сбрасывать Истинные негативы Ложноположительные результаты
  • Отток ложноотрицательных истинных срабатываний
sensitivity = TP / float(FN + TP)
print("sensitivity",sensitivity)

sensitivity 0.9972138964024336

Статистика:

У нас хорошая чувствительность. Когда чувствительность теста высока, он с меньшей вероятностью даст ложноотрицательный результат. В тесте с высокой чувствительностью положительный результат является положительным. Это можно проверить, поскольку мы получаем 686 как ложноотрицательные результаты.

specificity = TN / float(TN + FP)
print("specificity",specificity)

specificity 0.12898537829581516

Статистика:

У нас очень низкая специфичность. Тест с низкой специфичностью можно рассматривать как слишком стремящийся найти положительный результат, даже если его нет, и может давать большое количество ложноположительных результатов. Это также можно проверить, так как у нас большее количество FP, то есть 58736.

from sklearn import metrics
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score

print("PRECISION SCORE :", precision_score(y_test, y_pred, pos_label='POSITIVE'))
print("RECALL SCORE :", recall_score(y_test, y_pred, pos_label='POSITIVE'))
print("F1 SCORE :", metrics.f1_score(y_test, y_pred, pos_label='POSITIVE'))

PRECISION SCORE : 0.806962191723195
RECALL SCORE : 0.9972138964024336
F1 SCORE : 0.8920569524826792
# mapping labels to 0 and 1 in y_pred
y_test = y_test.map({'NEGATIVE':0, 'POSITIVE':1})

# creating an ROC curve
from sklearn.metrics import confusion_matrix as sk_confusion_matrix
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt

false_positive_rate, true_positive_rate, thresholds = roc_curve(y_test, proba[:,1])
roc_auc = auc(false_positive_rate, true_positive_rate)

# area under the curve
print(roc_auc)

0.9300492881807698

Построение кривой ROC

Кривая ROC демонстрирует несколько вещей:

  • Он показывает компромисс между чувствительностью и специфичностью (любое повышение чувствительности будет сопровождаться снижением специфичности).
  • Чем ближе кривая следует к левой границе, а затем к верхней границе пространства ROC, тем точнее тест.
  • Чем ближе кривая подходит к 45-градусной диагонали пространства ROC, тем менее точен тест.
# plotting the ROC curve
%matplotlib inline  
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.title('ROC')
plt.plot(false_positive_rate, true_positive_rate)

Статистика:

Площадь под кривой равна 0,93, что достаточно хорошо.

Примечание. Хорошей моделью является та, в которой TPR высокий (ближе к 100 %), а FPR низкий (ближе к 0 %). Следовательно, необходимо сбалансировать эти два элемента.

TPR и FPR — это не что иное, как чувствительность и (1 — специфичность), поэтому их также можно рассматривать как компромисс между чувствительностью и специфичностью.

Отображение варианта использования в бизнесе

# Creating a new column from Predictions from model
X_test['sentiment_predicted'] = y_pred

# Assume you have launched a product in market and you need to see what is the sentiment of that particular product. 
temp = X_test.loc[X_test['brand']=="Motorola"].reset_index(drop=True)

# Visualizing the sentiments for the product
temp['sentiment_predicted'].hist()

negative_df = temp.loc[temp['sentiment_predicted']=='NEGATIVE'].reset_index(drop=True)
negative_df.head()

from wordcloud import WordCloud

text = " ".join(str(review) for review in negative_df.reviewText)


wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white", stopwords=stop_words).generate(text)
plt.figure(figsize=[15,10])
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show();

# Also, let's assume you want to see what is the ratio of reviews a particular reviewer has posted on Amazon
temp1 = X_test.loc[X_test['reviewerName']=="Daniel"].reset_index(drop=True)

# Visualizing the sentiments for a particular Reviewer 
temp1['sentiment_predicted'].hist()

Заключение:

  • Мы достигли оценки точности 81%, используя технику построения модели мультиномиального наивного байесовского классификатора.
  • Площадь под кривой ROC составляет 93%.

Исходный код можно найти на GitHub. Вы можете найти файлы данных, нажав здесь.

Я с нетерпением жду любых отзывов или вопросов.