赞
踩
本文利用python语言,对餐馆的大众点评评论文本进行分析。首先,通过数据清洗、中文分词以及特征工程完成数据的预处理。对完成预处理的数据作了描述统计说明。
其次,构建了逻辑回归、支持向量机和朴素贝叶斯三个分类器模型,计算召回率、精确度、F1和准确率等指标来评价它们的实际性能。逻辑回归模型表现最好,准确率为85%,可以用于实现对餐馆评论文本的精确分类。
最后,基于处理好的数据利用关键字查询餐馆的名称,并通过可视化手段展示某个餐馆的评论、好评/差评比例、环境评分、口味评分、服务评分以及随时间变化的好评/差评线图等指标。
本文研究使用的餐馆大众点评评论文本包含3个数据集:restaurants.csv、ratings.csv、links.csv。数据集字段信息及部分示例数据如表1-3。
restaurant数据集包含 243247条餐馆数据
餐馆数目(有名称): 209132
餐馆数目(没有名称):34115
餐馆数目(总计): 243247
ratings数据集包含 4422473条评论数据、542706个用户
用户数目:542706
评分/评论数目(总计): 4422473
总体评分数目([1,5]): 3293878
环境评分数目([1,5]): 4076220
口味评分数目([1,5]): 4093819
服务评分数目([1,5]): 4076220
评论数目: 4107409
links数据集包含243247条数据
对缺失值的处理主要是删除数据中的空值。
print(restaurants.isnull().sum(axis=0))
restaurant数据集name列有34115条缺失值。因为后文有可视化分析,没有餐馆名称的数据直接删掉。
print(ratings.isnull().sum(axis=0))
ratings数据集总共4422473条数据
评分有1128580条缺失值
环境评分有346253条缺失值
口味评分有328654缺失值
服务评分有346253条缺失值
评论内容有315064条缺失值
首先删除restaurant数据集中餐馆名称为nan的restId,对应的ratings数据集数据。然后再删除评论内容中的缺失值。
# 删除餐馆名称为nan的restId,对应的ratings数据,要删除的id→id_with_missing_values
# 根据id删除行
data = ratings.set_index('restId').drop(id_with_missing_values).reset_index()
# 删除评论内容为nan的值
data.dropna(axis=0, subset='comment', inplace=True)
处理完之后再查看ratings数据集的缺失值。
处理之后发现rating_env、rating_flavor、rating_service均有55个缺失值,对缺失值进行具体探查。
# 查看rating_env列有缺失值的行
null_rating_env = data[data['rating_env'].isna()]
print(null_rating_env)
从图中可以看出rating_env、rating_flavor、rating_service是一起缺失的,所以直接删掉就好了。
然后rating列还存在1013107条缺失值,因为缺失值占据数据集的1/4,所以不能直接删除,初步的想法是对rating列进行填充。
因为数据量太大了,对于我个人的电脑来说要运行很久,所以直接删除好了。
同时呢我们观察到在ratings数据集中存在环境、口味、服务的评分与总评分不符合的情况,例如对环境、口味、服务评分只有1-2分,但是总评分却是4分及以上。可能是因为该餐馆在其他方面得到了更高的评价,例如价格、装修或者地理位置等。这些低分的评价可能是由于个别顾客对某些方面的不满意而导致的,不能完全反映该餐厅的整体评价,或者是某些用户的评分标准与其他用户存在很大差异,导致他们给的评分与整体评分相差较大。会影响对评论文本的情感分析,需要进行处理。
# 筛选rating_env、rating_flavor、rating_service评分为3分以下,rating评分为3分及以上的数据
data1 = known_rating[(known_rating['rating_env'] < 3) & (known_rating['rating_flavor'] < 3)
& (known_rating['rating_service'] < 3) & (known_rating['rating'] >= 4)]
# 筛选rating_env、rating_flavor、rating_service评分为3分以上,rating评分为3分以下的数据
data2 = known_rating[(known_rating['rating_env'] >= 3) & (known_rating['rating_flavor'] >= 3)
& (known_rating['rating_service'] >= 3) & (known_rating['rating'] < 4)]
所以对于rating列异常值和异常值本文采取的处理方法是对所有维度的评分进行加权,并将其作为综合评分使用。这样可以更准确地反映顾客的整体评价,避免过于依赖单个维度(例如总评分)而导致评估不准确或误导。
权值的设置是自定义的,该赋权过程可以用公式表示如下:
rating_2=0.4×rating+0.2×rating_env+0.2×rating_flavor+0.2×rating_service
其中rating_2表示计算出的综合评分,其为0-5的数值。
利用综合评分的分值可以划分评论数据为好评数据和差评数据,设置rating_2值为[0,3]的评论为差评,rating_2值为[4,5]的评论为好评,将结果保存到evaluation列,其中“1”代表好评,“0”代表差评。
# 计算总的得分rating_2
data['rating_2'] = data['rating_env']*0.2 + data['rating_flavor']*0.2 + data['rating_service']*0.2 + data['rating']*0.4
# 划分好评和差评
data['evaluation'] = data['rating_2'].apply(lambda x: 0 if x < 4 else 1)
评论字段中字符长度较小的数据,利用价值小。将评论文本中字符长度小于10的数据删除。
data = data[data['comment'].apply(len) >= 10]
还有一些特殊字符如英文、数字、表情符号、字符串等,对于特殊字符采用正则表达式re进行剔除,这个在去停用词部分处理。
中文文本与英文文本之间的差别很大,在中文表达中没有作为分隔符的空格出现,同时具体的汉字表达的意思也是极不明确的,单个词语更不能十分准确地表达意思。因此需要先对文本进行分词操作,然后对分词后的结果逐一进行表示。本文使用的是jieba分词技术来进行中文文本分词。
文本在经过分词之后存在很多像“的”、“了”、“吧”、“啊”、“这”等一些无实际意义的语气词、代词、介词等,还有标点符号、特殊符号等都称为停用词。这类词不仅会提升模型的复杂性还会影响分类效果,所以需要过滤这些无意义的词,从而减少文本的噪音,增大关键词密度,提高文本分类的准确率和主题分析的效率。
基于前人的研究成果,本文搜集了一些停用词典,主要包括哈工大停用词典、百度停用词典、四川大学机器智能实验室停用词库以及中文停用词表,将它们合并去重最后得到包含 1893个停用词的停用词典。
值得注意的是,由于本研究的数据量过大,在分词过程中将数据集划分为10个批次,对每个批次都进行了去停用词的处理,将结果依次保存为文件,最后将10个文件合并。
import jieba import re from sklearn.model_selection import KFold # 加载停用词表 stop_words = set() with open('D:/课程/数据挖掘技术与实践/作业/tc-corpus-answer/停用词表/stopwords.txt', encoding=encoding) as f: for line in f: stop_words.add(line.strip()) def preprocess_text(text): # 去除空白字符 text = re.sub(r'\s+', '', text) # 分词 seg_list = jieba.lcut(text) # 去除停用词和非中文字符 seg_list = [token for token in seg_list if token not in stop_words and re.match(r'^[\u4e00-\u9fa5]+$', token)] return ' '.join(seg_list) # 划分数据集为 10 个批次,并对每个批次中的 comment 列进行预处理,并单独保存到文件中 kfold = KFold(n_splits=10, shuffle=True, random_state=42) for i, (train_idx, test_idx) in enumerate(kfold.split(df)): df_batch = df.iloc[test_idx].copy() df_batch['comment'] = df_batch['comment'].apply(preprocess_text) df_batch.to_csv(f'batches_{i+1}.csv', encoding='utf-8', index=False) # 读取第 1 个数据集文件 batch_data = pd.read_csv('batches_1.csv') # 读取其他 9 个数据集文件,并与第 1 个数据集合并 for i in range(2, 11): batch_data_i = pd.read_csv(f'batches_{i}.csv') batch_data = pd.concat([batch_data, batch_data_i], ignore_index=True) # 将合并后的数据集保存为一个文件 batch_data.to_csv('without_data.csv', index=False) batch_data = pd.read_csv('without_data.csv')
在进行分词和去停用词操作后,评论文本中出现了缺失值和重复值,删除这些数据。
将restaurants数据集和ratings数据集根据restId合并数据集。
batch_data = pd.merge(batch_data, restaurants[['restId', 'name']], on='restId', how='left'
原始文本经过分词和去停用词操作后的结果如下表所示
原始文本 | 整体感觉还是不错的了,,比较安静惬意,,坐坐喝喝茶真的是挺舒服的,,,四周的环境绿化也挺不错 |
---|---|
分词、去停用词 | 整体 感觉 不错 安静 惬意 坐坐 喝 喝茶 真的 挺舒服 四周 环境 绿化 挺不错 感觉 |
可以看出处理效果是非常明显的,许多符号和无意义文本都被去除,为后文建立模型提供了优质的数据基础。
统计comment列的字段长度,绘制箱线图如下:
import pandas as pd
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 6))
ax.boxplot(data['comment_length'],vert=False, widths=0.4, patch_artist=True, boxprops=dict(facecolor='LightBlue'))
ax.set_title('Comment Length Boxplot', fontsize=14)
ax.set_xlabel('Comment Length', fontsize=12)
plt.show()
从上图我们可以看出,居然有评论内容的长度有2000字!that‘s amazing!
在数据预处理的过程中,试图筛选出网络水军发表的评论,并进行清除,但是在实际操作过程中没有确定的标准来进行辨别。
根据同一userId号在相近的时间内以相似的内容进行评论,被高度怀疑是网络水军的做法,数据预处理时仅根据这一实践经验进行了部分高度疑似水军数据的删除。
from difflib import SequenceMatcher import math data = batch_data.copy() # 将数据集分成20个批次 num_batches = 20 batch_size = math.ceil(len(data) / num_batches) for i in range(num_batches): start_idx = i * batch_size end_idx = min((i+1) * batch_size, len(data)) # 取出当前批次的数据 batch_data = data.iloc[start_idx:end_idx] # 删除高度疑似水军数据 idx_to_drop = [] for j in range(1, len(batch_data)): # 判断userId是否相同 if batch_data.iloc[j]['userId'] == batch_data.iloc[j-1]['userId']: # 判断时间间隔是否小于等于1分钟 if pd.Timestamp(batch_data.iloc[j]['timestamp']) - pd.Timestamp(batch_data.iloc[j-1]['timestamp']) <= pd.Timedelta(minutes=1): # 计算相似度 similarity = SequenceMatcher(None, batch_data.iloc[j]['comment'], batch_data.iloc[j-1]['comment']).ratio() if similarity > 0.8: idx_to_drop.append(j) # 删除高度疑似水军数据 batch_data = batch_data.drop(idx_to_drop) print(idx_to_drop) # 保存处理后的数据到文件 batch_data.to_csv(f'batch_data_processed_{i}.csv', index=False)
# 将数据进行汇总
# 读取第 1 个数据集文件
batch = pd.read_csv('batch_data_processed_0.csv')
# 读取其他 9 个数据集文件,并与第 1 个数据集合并
for i in range(1, 19):
batch_i = pd.read_csv(f'batch_data_processed_{i}.csv')
batch = pd.concat([batch, batch_i], ignore_index=True)
# 将合并后的数据集保存为一个文件
batch.to_csv('drop_water_data.csv', index=False)
经过上文对数据进行预处理后,本节分别统计好评和差评评论文本中各个词语出现的频次并绘制相应词云图,最后绘制所有评论的词云图。词云图中,词频越高,词的字号就越大,词频越低,词的字号就越小。
from wordcloud import WordCloud, STOPWORDS import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 def generate_wordcloud_batches(batch_data, num_batches, save_path): # 每批次处理数据的大小 batch_size = int(len(batch_data) / num_batches) batch_data['comment'] = batch_data['comment'].astype(str) # 定义生成词云图函数 def generate_wordcloud(text_list, output_file): text = " ".join(text_list) wc = WordCloud(font_path='simkai.ttf', background_color='white', width=800, height=600, max_words=100, collocations=False) wc.generate(text) wc.to_file(output_file) # 分批次处理数据并生成词云图 for i in range(num_batches): start = i * batch_size end = start + batch_size if i != (num_batches - 1) else len(batch_data) batch_text_list = batch_data.iloc[start:end, :]["comment"].tolist() output_file = f"{save_path}batch_wordcloud_f_{i}.png" generate_wordcloud(batch_text_list, output_file) # 合并所有词云图 all_text_list = batch_data["comment"].tolist() output_file = f"{save_path}all_f.png" generate_wordcloud(all_text_list, output_file)
将数据集划分为好评数据集和差评数据集,分别绘制好评、差评和总的评论词云图如下:
df = a_data.copy()
final_wordcloud = generate_wordcloud_batches(df, num_batches=10, save_path='a/')
df = c_data.copy()
final_wordcloud = generate_wordcloud_batches(df, num_batches=10, save_path='c/')
df = batch_data.copy()
final_wordcloud = generate_wordcloud_batches(df, num_batches=10, save_path='all_data/')
观察图可以看出,不错、味道、好吃、吃、喜欢、感觉、环境、价格、服务等词语较为醒目,可以知道在评论中大部分消费者经常使用这些词汇对餐馆做出评价,观察好评词云图高频词是一些正面评价的词汇,而在差评词云图中出现了一些负面高频词,如:贵、少、不好等词汇。其中在差评词云图中,关于服务的词汇出现了两个:服务员、服务,说明做出负面评价的消费者对餐馆的服务质量并不满意。建议餐馆管理者加强对服务员的培训、提高服务质量。
对预处理操作完成后的数据进行统计,筛选出餐馆的评论量大于50的数据,过滤掉评论量较少的餐馆,集中分析有更多评论量的餐馆,按照餐馆的好评/差评数据占比总数据的比例降序排列,查看好评评论和差评评论中top10餐馆。统计情况如下图。
import pandas as pd import matplotlib.pyplot as plt # 读取数据 data = batch_data.copy() # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 筛选出评论数量大于50的餐馆数据 filtered_data = data.groupby('restId').filter(lambda x: len(x) > 50) # 计算每个餐馆的总评数目、好评数目和差评数目 grouped = filtered_data.groupby(['restId', 'evaluation']).size().reset_index(name='count') good_counts = grouped[grouped['evaluation'] == 1] bad_counts = grouped[grouped['evaluation'] == 0] total_counts = grouped.groupby('restId')['count'].sum().reset_index(name='total_count') good_ratios = good_counts.merge(total_counts, on='restId') good_ratios['good_ratio'] = good_ratios['count'] / good_ratios['total_count'] bad_ratios = bad_counts.merge(total_counts, on='restId') bad_ratios['bad_ratio'] = bad_ratios['count'] / bad_ratios['total_count'] ratios = good_ratios.merge(bad_ratios, on='restId').merge(data[['name','restId']].drop_duplicates(), on='restId') # 按照好评/总评比例或者差评/总评比例大小排序,并取出前10名餐馆 top10_good = good_ratios.sort_values(by='good_ratio', ascending=False).head(10) top10_bad = bad_ratios.sort_values(by='bad_ratio', ascending=False).head(10) # 横向柱状图可视化好评/差评比例 plt.subplots(figsize=(10, 7)) plt.suptitle('Restaurants Rating Ratio - Top 10 Good Rating vs. Top 10 Bad Rating', fontsize=16, y=1) plt.subplot(1, 2, 1) plt.barh(y=ratios.nlargest(10,'good_ratio')['name'], width=ratios.nlargest(10,'good_ratio')['good_ratio'], color='green') plt.xticks(rotation=0) plt.xlabel('Good Rating Ratio') plt.ylabel('Restaurant Name') plt.title('Top 10 Restaurants by Good Rating Ratio') plt.subplot(1, 2, 2) plt.barh(y=ratios.nlargest(10,'bad_ratio')['name'], width=ratios.nlargest(10,'bad_ratio')['bad_ratio'], color='red') plt.xticks(rotation=0) plt.xlabel('Bad Rating Ratio') plt.ylabel('Restaurant Name') plt.title('Top 10 Restaurants by Bad Rating Ratio') plt.subplots_adjust(wspace=1) plt.show()
随着时间推移,餐馆的服务水平、菜品质量、环境卫生等方面可能发生变化,对应的好评/差评率也会有所反应。整个数据中时间跨度为2009-07-04 00:16:00到2012-02-08 14:04:00,总共有32个月,137个周,950个日,数据量比较大。绘制按照周的时间粒度的随时间变化的餐馆的好评/差评率线图如下。
def plot_evaluation_by_week(top10, filtered_data, ratios, is_good_evaluation=True): # 绘制每个餐馆的评分随时间变化的线图 for restId in top10['restId']: rest_data = filtered_data[filtered_data['restId'] == restId] rest_data['date'] = pd.to_datetime(rest_data['timestamp']) rest_data = rest_data.set_index('date').resample('W').mean() rest_name = ratios.loc[ratios['restId'] == restId, 'name'].iloc[0] fig, ax = plt.subplots() ax.plot(rest_data.index, rest_data['rating_2'], label=rest_name, color='blue') ax.set_xlabel('时间') ax.set_ylabel('评分') title = '好评排名top10的餐厅' if not is_good_evaluation else '差评排名top10的餐厅' plt.title(title) plt.xticks(rotation=45) ax.legend(loc='best') plt.show()
筛选出好评评论和差评评论中top10餐馆后,为更深入地了解好评评论量较高的餐馆和差评评论量较高的餐馆的综合表现,绘制每个餐馆的环境、口味、服务评分雷达图如下。
def draw_radar_chart(top_restids, data): """ 绘制top_restids中餐厅的雷达图 :param top_restids: 需要绘制雷达图的餐厅列表 :param data:所有餐厅的评分数据 """ # 选择对应的restid数据 top_restaurants_data = data[data['restId'].isin(top_restids)] # 计算rating_env,rating_flavor,rating_service的平均值 avg_ratings = top_restaurants_data.groupby('name').mean()[['rating_env', 'rating_flavor', 'rating_service']] # 绘制雷达图 attributes = ['rating_env', 'rating_flavor', 'rating_service'] for name, row in avg_ratings.iterrows(): values = row.values.flatten().tolist() values += values[:1] angles = [n / float(len(attributes)) * 2 * pi for n in range(len(attributes))] angles += angles[:1] ax = plt.subplot(111, polar=True) plt.xticks(angles[:-1], attributes,rotation=90,ha='left') ax.set_rlabel_position(0) plt.yticks([1.0, 2.0, 3.0, 4.0,5.0], ["1.0", "2.0", "3.0", '4.0','5.0'], color="grey",size=9, rotation=90, ha='center') plt.ylim(1, 5) ax.plot(angles, values, linewidth=1, linestyle='solid') ax.fill(angles, values, 'b', alpha=0.1) #添加数值标记(保留两位数) for i in range(len(attributes)): angle_rad = i / float(len(attributes)) * 2 * pi if values[i] >= 3: ha = 'left' else: ha = 'right' plt.text(angle_rad, values[i]+0.2, format(values[i], '.2f'), ha=ha, va='center') plt.title('Ratings for Restaurant {}'.format(name)) plt.show()
观察好评率top10餐馆可以发现,大部分好评餐馆在环境、服务、口味3各方面中环境和服务评分比口味高,由于好评率是所有维度的综合体现,环境和服务评分可能对总体的好评率产生更大的影响。
观察差评率top10餐馆可以发现大部分差评餐馆在环境、服务、口味3各方面中环境和服务评分比口味低,这与上文得出的结论相同,环境和服务评分可能对总体的好评率产生更大的影响。餐馆经营者需要注意维护餐厅的整体形象,提高服务质量、卫生和环境等方面的表现,并尽可能提高各方面的综合表现,以获得更高的好评率和更多的客户支持。
餐馆的评论文本数据经过预处理后,已经具备构建分类器的条件。将评论文本分成训练数据集和测试数据集,首先在训练数据集上建立预测模型,构建文本分类器,然后用训练出来的分类器模型在测试数据集上进行预测,根据预测情况对分类器性能进行评估。
把预处理后的数据的comment列作为特征值,evaluation列作为目标值。采用随机抽样的方式来抽取训练数据集和测试数据集,以8:2的比例划分训练集和测试集。
from sklearn. model_selection import train_test_split
X = batch_data['comment']
y = batch_data['evaluation']
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,random_state=22)
采用Count Vectorizer文本特征提取方法,将原始文本数据转化为数字特征向量。该方法将每个文档表示为一个向量,其中向量的每个元素表示该文档中相应单词(或特征)出现的次数。
设置Count Vectorizer方法中的两个参数max_df=0.9,min_df=3, 当某个单词在文档中出现的频率达到max_df这个阈值时,某个单词在所有文档中出现的频率小于min_df这个阈值时,就会被认为是停用词(即被过滤掉),可以帮助减小特征空间的维度并提高向量化后数据的质量和可解释性。
将评论文本数据转换为特征向量后,用TfidfTransformer文本特征转换方法,将CountVectorizer输出的词频矩阵转换为TF-IDF矩阵。TF-IDF矩阵可以将常见的词语的权重降低,同时却保留那些区分度较高的词语的权重,以优化文本特征表示的效果。
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
# 使用Count Vectorizer提取文本特征
count_vect = CountVectorizer(max_df = 0.9,min_df = 3)
X_train_counts = count_vect.fit_transform(X_train)
X_test_counts = count_vect.transform(X_test)
from sklearn.feature_extraction.text import TfidfVectorizer
# 使用TF-IDF Vectorizer调整特征权重
tfidf_transformer = TfidfTransformer()
X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
X_test_tfidf = tfidf_transformer.transform(X_test_counts)
接下来就可以使用 Python 语言进行模型构建,首先在训练数据集上构建文本分类模型,然后在测试数据集上进行预测,根据预测情况计算模型评价指标对分类模型性能进行评估。
本研究选取了朴素贝叶斯、逻辑回归、支持向量机共3个算法,建立中文文本二值分类模型。各模型主要参数如下:
(1)逻辑回归模型。首先考虑加入正则化项来防止模型出现过拟合现象,比较常见的有参数向量的范数
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。