Yelp - это локальный поисковый сервис для местных предприятий. Люди делятся своими отзывами о своем опыте работы в этом бизнесе, который является очень важным источником информации. Отзывы клиентов могут помочь определить сильные и слабые стороны и расставить приоритеты для дальнейшего развития бизнеса. Меня интересуют отзывы посетителей ресторанов рядом со мной, г. Чикаго, штат Иллинойс.

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

Анализируя все эти обзоры, мы можем собрать приличный объем количественных и качественных данных, проанализировать их и определить области, требующие улучшения. К счастью, python предоставляет библиотеки, которые легко справляются с этими задачами. Для парсинга веб-страниц я решил использовать библиотеку запросов, которая выполняет свою работу и очень проста в использовании. У меня нет предыдущего опыта работы с веб-парсингом, и я хочу создать свой собственный набор данных для выполнения анализа настроений.

Мы можем легко изучить структуру веб-сайта, проверив его в веб-браузере. Изучив структуру веб-сайта Yelp, я составил список возможных переменных данных для сбора:

  1. Имя рецензента
  2. Рассмотрение
  3. Дата
  4. Звездный рейтинг
  5. Название ресторана

Модуль запросов позволяет легко загружать файлы из Интернета. Вы можете установить модуль запросов, используя:

>>> pip install requests

Сначала мы заходим на сайт yelp и ищем рестораны рядом со мной, местоположение Чикаго, Иллинойс.

Затем мы импортируем все необходимые библиотеки и создадим DataFrame pandas.

import pandas as pd 
import time as t
from lxml import html 
import requests
reviews_df=pd.DataFrame()

Скачивание html-страницы с помощью requests.get ()

>>>import requests

>>> searchlink= 'https://www.yelp.com/search?find_desc=Restaurants&find_loc=Chicago,+IL'

user_agent = ‘ Enter you user agent here ’
headers = {‘User-Agent’: user_agent}

Вы можете получить свой пользовательский агент здесь.

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

page = requests.get(searchlink, headers = headers)
parser = html.fromstring(page.content)

Request.get загрузит html-страницу. Теперь нам нужно найти ссылки на несколько ресторанов на странице.

businesslink=parser.xpath('//a[@class="biz-name js-analytics-click"]')
links = [l.get('href') for l in businesslink]

Эти ссылки не полные, поэтому нам придется добавить к ним доменное имя.

u=[]
for link in links:
        
        u.append('https://www.yelp.com'+ str(link))

Теперь у нас есть все названия ресторанов с первой страницы; на каждой странице 30 результатов поиска. Теперь давайте перебираем каждую страницу и получаем отзывы.

for item in u:
    page = requests.get(item, headers = headers)
    parser = html.fromstring(page.content)

Обзоры содержатся в блоке с именем класса «обзор обзора - с боковой панелью». Давайте возьмем все эти div.

xpath_reviews = ‘//div[@class=”review review — with-sidebar”]’
reviews = parser.xpath(xpath_reviews)

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

for review in reviews:
            
       temp = review.xpath('.//div[contains(@class, "i-stars i-                                                stars--regular")]')
      
       rating = [td.get('title') for td in temp]
       xpath_author  = './/a[@id="dropdown_user-name"]//text()'
       xpath_body    = './/p[@lang="en"]//text()'
    
       author  = review.xpath(xpath_author)
    
       date    = review.xpath('.//span[@class="rating-qualifier"]//text()')
    
       body    = review.xpath(xpath_body)
        
       heading= parser.xpath('//h1[contains(@class,"biz-page-title embossed-text-white")]')
       bzheading = [td.text for td in heading]

Мы создадим словарь для всех этих элементов, а затем добавим этот словарь во фрейм данных pandas.

review_dict = {‘restaurant’ : bzheading,
                ‘rating’: rating, 
                ‘author’: author, 
                ‘date’: date,
                ‘Review’: body,
              }
 reviews_df = reviews_df.append(review_dict, ignore_index=True)

Теперь у нас есть все отзывы с одной страницы. Вы можете перемещаться по страницам, находя максимальное количество страниц. Номер последней страницы содержится в теге ‹a› с именем класса «доступный номер pagination-links_anchor».

page_nums = '//a[@class="available-number pagination-links_anchor"]'
pg = parser.xpath(page_nums)
max_pg=len(pg)+1

Не забудьте добавить время сна внутри каждого цикла for, чтобы сделать скрипт медленным и соблюсти политику очистки yelp.com. Отправка слишком большого количества запросов может привести к блокировке вашего IP-адреса.

import time as t
t.sleep(10)   

С помощью приведенного выше сценария я обработал в общей сложности 23 869 отзывов для 450 ресторанов и 20–60 отзывов на ресторан.

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

Сначала импортируйте необходимые библиотеки.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

Я сохранил данные в файле с именем all.csv

data=pd.read_csv(‘all.csv’)

Теперь давайте посмотрим на начало и конец фрейма данных.

У нас есть 23 869 записей с 5 столбцами. Как мы видим, данные нуждаются в форматировании. Все ненужные символы, теги и пробелы следует удалить. Также есть несколько значений Null / NaN.

Удалите все значения Null / NaN из фрейма данных.

data.dropna()

Теперь, используя нарезку строк, мы удалим ненужные символы и пробелы.

data['Review']=data.Review.str[2:-2]
data['author']=data.author.str[2:-2]
data['date']=data.date.str[12:-8]
data['rating']=data.rating.str[2:-2]
data['restaurant']=data.restaurant.str[16:-12]
data['rating']=data.rating.str[:1]

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

У нас есть максимальное количество обзоров с рейтингом 5 звезд, всего 11 859 записей, а самое низкое - 979 записей для рейтинга 1 звезда. Но есть и записи с неизвестным рейтингом «т». Эти записи следует отбросить.

data.drop(data[data.rating=='t'].index , inplace =True)

Чтобы лучше понимать данные, мы можем создать новую функцию call review_length. В этом столбце будет храниться количество символов в каждом обзоре, и мы удалим все пробелы в обзоре.

data['review_length'] = data['Review'].apply(lambda x: len(x) - x.count(' '))

Теперь давайте построим графики и разберемся с данными.

hist = sns.FacetGrid(data=data, col='rating')
hist.map(plt.hist, 'review_length', bins=50)

Мы видим, что количество отзывов с 4 и 5 звездами больше. Распределение длин обзоров очень похоже для всех рейтингов.

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

sns.boxplot(x='rating', y='review_length', data=data)

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

Анализ настроений

Чтобы определить, является ли отзыв положительным или отрицательным, мы сосредоточимся только на рейтингах 1 и 5 звезд. Давайте создадим новый фрейм данных для хранения оценок 1 и 5 звезд.

df = data[(data['rating'] == 1) | (data['rating'] == 5)]
df.shape
Output:(12838, 6)

Из 23 869 записей у нас теперь 12838 записей с рейтингом 1 и 5 звезд.

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

Похоже, здесь много знаков препинания и неизвестных кодов, например «\ xa0». «\ Xao» на самом деле является неразрывным пробелом в Latin1 (ISO 8859–1), также chr (160). Вы должны заменить его пробелом. Теперь давайте создадим функцию для удаления всех знаков препинания, стоп-слов и затем выполним лемматизацию текста.

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

Лемматизация. Лемматизация - это процесс группирования изменяемых форм слов, чтобы их можно было анализировать как единый термин, определяемый как лемма. Лемматизация всегда возвращает словарную форму слова. Например, слова: type, typed и typing будут считаться одним словом «тип». Эта функция будет очень полезна для нашего анализа.

import string     # Imports the library
import nltk        # Imports the natural language toolkit
nltk.download('stopwords')   # Download the stopwords dataset
nltk.download('wordnet')
wn=nltk.WordNetLemmatizer()
from nltk.corpus import stopwords
stopwords.words('english')[0:10]
Output: ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're"]

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

def text_process_lemmatize(revw):
    """
    Takes in a string of text, then performs the following:
    1. Remove all punctuation
    2. Remove all stopwords
    3. create a list of the cleaned text
    4. Return Lemmatize version of the list
    """
    
    # Replace the xa0 with a space
    revw=revw.replace('xa0',' ')
    
    # Check characters to see if they are in punctuation
    nopunc = [char for char in revw if char not in string.punctuation]
    # Join the characters again to form the string.
    nopunc = ''.join(nopunc)
    
    # Now just remove any stopwords
    token_text= [word for word in nopunc.split() if word.lower() not in stopwords.words('english')]
    
    # perform lemmatization of the above list
    cleantext= ' '.join(wn.lemmatize(word) for word in token_text)
        
    return cleantext

Теперь давайте превратим наш столбец обзора в только что созданную функцию.

df['LemmText']=df['Review'].apply(text_process_lemmatize)

Векторизация

Нам нужно преобразовать список лемм в df [‘LemmText’] в векторы, чтобы алгоритм машинного обучения и python могли его использовать и понимать. Этот процесс известен как векторизация. Этот процесс создаст матрицу с каждым обзором в виде строки и каждой уникальной леммой в виде столбца и будет содержать количество вхождений каждой леммы. Мы будем использовать Count Vectorizer и процесс N-грамм из библиотеки scikit-learn. Здесь мы остановимся только на униграммах.

from sklearn.feature_extraction.text import CountVectorizer
ngram_vect = CountVectorizer(ngram_range=(1,1))
X_counts = ngram_vect.fit_transform(df['LemmText'])

Тренировка тестового сплита

Теперь давайте создадим наборы данных для обучения и тестирования, используя train_test_split из scikit-learn.

X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3)
#0.3 mean the training set will be 70% and test set will be 30%

Мы будем использовать полиномиальный наивный байесовский метод для предсказания настроения. Оценки в 1 звезду представляют собой отрицательные отзывы, а оценки в 5 звезд представляют собой положительные отзывы. Давайте создадим модель MultinomialNB, которая будет соответствовать X_train и y_train.

from sklearn.naive_bayes import MultinomialNB
nb = MultinomialNB()
nb.fit(X_train,y_train)

Теперь давайте сделаем прогноз на тестовом наборе X_test.

NBpredictions = nb.predict(X_test)

Оценка

Теперь давайте сопоставим прогноз нашей модели с их фактическими звездными рейтингами из y_test.

from sklearn.metrics import confusion_matrix,classification_report
print(confusion_matrix(y_test,NBpredictions))
print('\n')
print(classification_report(y_test,NBpredictions))

Модель имеет точность 97%, что очень хорошо. Эта модель может предсказать, понравился или не понравился ресторан клиенту, по опубликованному им обзору.