В этой статье я рассмотрю набор данных Airbnb Istanbul. Первый набор данных — это набор данных отзывов. Столбцы: listing_id (для свойства), id (идентификатор комментария), date (дата комментария), Reviewer_id, Reviewer_name, комментарии. Второй — набор данных списков. В этом наборе данных намного больше столбцов, однако я буду использовать только id и review_scores_rating (средний рейтинг этого свойства). Оба набора данных можно найти ниже.

http://data.insideairbnb.com/turkey/marmara/istanbul/2021-02-26/data/reviews.csv.gz

http://data.insideairbnb.com/turkey/marmara/istanbul/2021-02-26/data/listings.csv.gz

Анализируя эти два набора данных, я попытаюсь ответить на три вопроса.

1. Можно ли предсказать мнение клиентов по комментариям?

Да, их можно предсказать. Для этой цели я использовал Vader Sentiment Analyzer. Во-первых, необходимо импортировать библиотеку nltk. Затем необходимо загрузить «vader_lexicon» и оттуда импортировать SentimentInsensityAnalyzer.

import nltk
nltk.download('vader_lexicon')
from nltk.sentiment.vader import SentimentIntensityAnalyzer

После чтения набора данных отзывов как df_com комментарии должны быть предварительно обработаны для задачи НЛП.

def preprocess(text):
# transform all to the lowercase
    text=text.lower()
    # remove hyperlinks if any
    text = re.sub(r'https?:\/\/.*[\r\n]*', '', text)
    text = re.sub(r'http?:\/\/.*[\r\n]*', '', text)
    #Replace &amp, &lt, &gt with &,<,> respectively
    text=text.replace(r'&amp;?',r'and')
    text=text.replace(r'&lt;',r'<')
    text=text.replace(r'&gt;',r'>') 
    #remove non ascii chars
    text=text.encode("ascii",errors="ignore").decode()
    #remove some puncts
    text=re.sub(r'[:"#$%&\*+,-/:;<=>@\\^_`!?.{|}~]+','',text)
    text=re.sub(r"'","",text)
    text=re.sub(r"\(","",text)
    text=re.sub(r"\)","",text)
    
    text=" ".join(text.split())
    return text
df_com['comments'] =df_com['comments'].apply(preprocess)

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

wordcloud_text = " ".join(desc for desc in df_com.comments)
print ("There are {} words in the combination of all descriptions.".format(len(wordcloud_text)))

В комбинации всех комментариев 47204197 слов.

stopwords = set(STOPWORDS)
wordcloud = WordCloud(stopwords=stopwords, background_color="white").generate(wordcloud_text)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

Как видно, слова «пребывание», «Стамбул», «квартира», «место» являются общеупотребительными и используются очень часто. Я предпочитаю удалять их в первую очередь, потому что они не являются отличительными для мнений. Итак, давайте обновим стоп-слова этими словами, чтобы потом их исключить, и снова посмотрим на облако слов.

stopwords.update(['stay','istanbul','apartment','place'])
wordcloud = WordCloud(stopwords=stopwords, background_color="white").generate(wordcloud_text)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

Вроде нормально. Теперь у нас всего 32 150 465 слов вместо 47 204 197. Кроме того, кажется, что нет ничего общего с манипулятивными общеупотребительными словами — то есть словами, которые очень часто используются и не имеют ничего общего с мнением. Итак, давайте удалим стоп-слова из текста.

df_com['comments'] = df_com['comments'].apply(lambda x: ' '.join([item for item in x.split() if item not in stopwords]))

Теперь мы готовы начать анализ мнений. Давайте определим анализатор и создадим столбец «оценки», применив анализ мнений к комментариям.

sid = SentimentIntensityAnalyzer()
df_com['scores'] = df_com['comments'].apply(lambda review: sid.polarity_scores(review))

Теперь у нас есть такая колонка:

{«отрицательный»: 0,026, «ней»: 0,671, «положительный»: 0,303, «составной»: 0,9913}

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

df_com['compound']  = df_com['scores'].apply(lambda score_dict: score_dict['compound'])

Давайте посмотрим на составной столбец.

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

2. Что можно сказать о распределении оценок?

Мы классифицировали все комментарии. Теперь давайте посмотрим рейтинги. Во-первых, давайте возьмем среднее значение всех составных оценок мнений и сгруппируем по listing_id (идентификатору свойства). Затем создайте фрейм данных.

df_comments_avg = df_com.groupby(['listing_id'])['compound'].mean()
df_comments_avg = pd.DataFrame(df_comments_avg)

Теперь из второго набора данных, как было сказано ранее, нам нужны только «id» и «review_scores_rating». Набор данных листинга был прочитан как «df_list».

df_ovr_ratings = df_list[['id','review_scores_rating']]

Теперь давайте посмотрим на нулевые значения.

print('There is total of',len(df_ovr_ratings),'rows with', (df_ovr_ratings['id'].nunique()/len(df_ovr_ratings))*100,'% of unique id numbers and', 100*df_ovr_ratings['review_scores_rating'].isna().sum()/len(df_ovr_ratings),'% of null values')

Всего имеется 23650 строк со 100,0 % уникальных идентификаторов и 54,00845665961945 % нулевых значений. Конечно, нулевые рейтинги могут быть интерполированы другими столбцами из набора данных списков с помощью другого алгоритма машинного обучения. Однако это не входит в предмет данной статьи. Следовательно, давайте отбросим нулевые значения для простоты.

df_ovr_ratings.dropna(inplace=True)
print('There is total of',len(df_ovr_ratings),'rows with', (df_ovr_ratings['id'].nunique()/len(df_ovr_ratings))*100,'% of unique id numbers')

Всего имеется 10877 строк со 100,0 % уникальных идентификаторов. Убрано более половины строк. Конечно, это нежелательно в первую очередь, однако, как было сказано ранее, давайте оставим это так для простоты. Теперь посмотрим на распределение оценок.

plt.hist(df_ovr_ratings['review_scores_rating'])
plt.title('Histogram of overall ratings')
plt.xlabel('Ratings')
plt.show()

Как видно, подавляющее большинство оценок выше 90%. Распределение смещено вправо и не имеет ничего общего с нормальным распределением. На самом деле, кажется, существует четкая положительная корреляция между оценками и мнениями. Давайте немного углубимся в отношения.

3. Какая связь между оценками и мнениями?

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

df_comments_avg = df_com.groupby(['listing_id'])['compound'].mean()
df_comments_avg = pd.DataFrame(df_comments_avg)

Теперь соберите их вместе. У нас будут идентификаторы, составные оценки мнений и общие рейтинги в одном и том же наборе данных. Итак, давайте сделаем внутреннее соединение.

merged_df = df_ovr_ratings.join(df_comments_avg,how='inner',on='id')
merged_df.head()

Давайте посмотрим на взаимосвязь между рейтингами и оценками мнений.

plt.scatter(merged_df['review_scores_rating'],merged_df['compound'])
plt.xlabel('Rating')
plt.ylabel('Opinion Score')
plt.show()

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