基于 NLTK 的酒店评论情感分析实战:从数据过滤、标签清洗到 VADER 情感打分(ML-For-Beginners 6-NLP 第 5 课)
基于 NLTK 的酒店评论情感分析实战从数据过滤、标签清洗到 VADER 情感打分ML-For-Beginners 6-NLP 第 5 课【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners导读本文是 ML-For-Beginners 课程 6-NLP 第 5 课Hotel-Reviews-2的完整技术指南讲解如何对欧洲 515K 条酒店评论数据集执行过滤列 → 清洗 Tags → 停用词优化 → VADER 情感打分的完整 NLP 流水线。读完本文你将掌握用 pandas 对大规模文本数据做列级清洗与自洽校验、把文本形式的标签列表转化为可统计的 0/1 特征列以及用 NLTK VADER 为 50 余万条正负面评论文本生成复合情感分值并落地为可直接用于下游推荐/聚类任务的 CSV 数据集。本课紧接上一课 6-NLP/4-Hotel-Reviews-1/README.md该课完成了对原始数据集的探索性分析本课则把探索结论落地为可执行的过滤与情感分析代码。数据本身约 230MB解压后下载后放在 6-NLP/data/README.md 所指示的6-NLP/data目录下即可按本文步骤复现。一、课程定位一份无法完全信任的数据集在开始前先明确目标场景本课假设你在为一个酒店推荐机器人准备数据需要基于游客评论的情感和标签来帮助挑选最佳酒店。原始数据集来自 Booking.com 公开爬取作者 Jiashen LiuCC0 公共领域许可规模为515,000 行、1493 家酒店、6 座欧洲城市其 16 个列可以分为四组酒店列Hotel_Name、Hotel_Address、lat、lng酒店元评论列Average_Score、Total_Number_of_Reviews、Additional_Number_of_Scoring评论列Reviewer_Score、Negative_Review、Positive_Review、Review_Total_Negative_Word_Counts、Review_Total_Positive_Word_Counts、Review_Date、days_since_review、Tags评论者列Total_Number_of_Reviews_Reviewer_Has_Given、Reviewer_Nationality。然而正如本课指出的这份数据有些列塞满了无用信息有些列看起来不正确即使正确也不清楚它们是如何计算出来的无法用你自己的计算独立验证。因此整条流水线的第一要务就是删掉无法验证的列用自己的计算重建可信的统计列。本课对应的完整代码位于 6-NLP/5-Hotel-Reviews-2/notebook.ipynb 与 solution 目录下的三个官方 notebook 中。二、过滤操作一初始列处理——去掉坐标、标准化酒店地址2.1 删除经纬度列lat/lng虽然理论上可以用于在地图上按正负情感着色酒店但本任务中并不需要地理可视化直接删除# dropping columns we will not use: df.drop([lat, lng], axis 1, inplaceTrue)2.2 把完整地址压缩为城市 国家Hotel_Address是完整街道地址对排序和按国家聚合没有帮助。本数据集只覆盖 6 座城市因此可以写一个基于关键字匹配的映射函数把地址规约为统一格式Ámsterdam阿姆斯特丹Países Bajos荷兰Barcelona巴塞罗那España西班牙Londres伦敦Reino Unido英国Milán米兰Italia意大利París巴黎Francia法国Viena维也纳Austria奥地利def replace_address(row): if Netherlands in row[Hotel_Address]: return Amsterdam, Netherlands elif Barcelona in row[Hotel_Address]: return Barcelona, Spain elif United Kingdom in row[Hotel_Address]: return London, United Kingdom elif Milan in row[Hotel_Address]: return Milan, Italy elif France in row[Hotel_Address]: return Paris, France elif Vienna in row[Hotel_Address]: return Vienna, Austria # Replace all the addresses with a shortened, more useful form df[Hotel_Address] df.apply(replace_address, axis 1) # The sum of the value_counts() should add up to the total number of reviews print(df[Hotel_Address].value_counts())实现细节见 solution/1-notebook.ipynb 第 2 个 cell官方 notebook 中的replace_address还带有一个else: return row.Hotel_Address分支兜底保留未匹配到的地址避免引入None。value_counts()的总和应等于评论总数这一行注释即是对清洗结果的校验。地址规约后就可以用一行代码查询国家/城市粒度的酒店数量display(df.groupby(Hotel_Address).agg({Hotel_Name: nunique}))Hotel_AddressHotel_NameAmsterdam, Netherlands105Barcelona, Spain211London, United Kingdom400Milan, Italy162Paris, France458Vienna, Austria158可以看到 6 个城市合计 1494 家酒店458 400 211 162 158 105与上一课提到的 1493 家酒店量级一致。三、过滤操作二酒店元评论列——用自算值替换可疑字段原始数据中的三个元评论列都有问题Additional_Number_of_Scoring表示给了评分但没写正/负面文字的额外评分数量本任务用不到Total_Number_of_Reviews不写代码无法确认它是否指数据集内的评论数Average_Score按数据创建者说明是基于最近一年最新评论计算的酒店平均分这种算法无法独立复现验证。处理策略是删掉Additional_Number_of_Scoring然后用groupby基于数据集内的真实行数重建另外两列# Drop Additional_Number_of_Scoring df.drop([Additional_Number_of_Scoring], axis 1, inplaceTrue) # Replace Total_Number_of_Reviews and Average_Score with our own calculated values df.Total_Number_of_Reviews df.groupby(Hotel_Name).transform(count) df.Average_Score round(df.groupby(Hotel_Name).Reviewer_Score.transform(mean), 1)关键点groupby(Hotel_Name).transform(count)为每一行填上该酒店在数据集内实际出现的评论行数与上一课发现的数据集内评论数 ≠ 网站显示总数的差异问题形成对照transform(mean)用每一行自己的Reviewer_Score重新计算酒店平均分并round(..., 1)保留 1 位小数与原始Reviewer_Score的取值范围最低 2.5、最高 10最多 1 位小数保持一致。这样得到的两列完全可复现、可验证。四、过滤操作三/四评论列与评论者列对评论列的处理遵循只保留对推荐任务有用的信息原则删除Review_Total_Negative_Word_Counts、Review_Total_Positive_Word_Counts、Review_Date、days_since_review这些只是字数统计与新鲜度信息且情感分析阶段会重新从文本计算语义无需保留保留Reviewer_Score、Negative_Review、Positive_Review原样它们是情感分析的核心输入Tags暂时保留——下一节要对它做进一步过滤之后才会删除。对评论者列删除Total_Number_of_Reviews_Reviewer_Has_Given。理由在上一课有详细说明评论者没有唯一 ID无法把写过 N 条评论的人与具体评论关联起来这个字段对推荐模型帮助有限保留Reviewer_Nationality国籍可作为特征但需警惕把国籍决定评分的刻板印象硬编码进模型。五、Tags 列把文本形式的列表变成可用特征5.1 Tags 为什么难用Tags列是一个以文本形式存储的列表形如[ Business trip , Solo traveler , Single Room , Stayed 5 nights , Submitted from a mobile device ]它有两大问题顺序与子项数量不固定不同酒店给评论者的可选项不同有的行是 5 个子项、有的是 3 个、有的是 6 个规模太大515,000 行、1427 家酒店人工逐条识别不现实而对 6,762,646 个词直接跑多词短语频次分布算法又会异常耗时。这正是 NLP 的用武之地扫描文本、统计最常见短语。而探索性分析进一步告诉你可以先大幅缩小处理范围不必对所有短语做统计。5.2 决定哪些标签值得保留回到任务目标用情感和标签帮用户挑酒店对标签做如下业务判断标签类别是否保留理由出行类型如商务/休闲✅ 保留与推荐强相关客群类型如情侣/独自旅行/家庭✅ 保留与推荐强相关房型/套房/公寓❌ 删除所有酒店房型大同小异提交设备如移动设备❌ 删除与酒店体验无关入住晚数❌ 删除理论上长住更喜欢但牵强且大概率无关结论只保留 2 类标签其余从统计范围内剔除。5.3 第一步清洗括号与引号统计前先要把标签变成干净格式——去掉方括号和引号。pandas 的str访问器可以最快完成# Remove opening and closing brackets df.Tags df.Tags.str.strip([]) # remove all quotes too df.Tags df.Tags.str.replace( , , ,, regex False)此时每个标签变成类似Business trip, Solo traveler, Single Room, Stayed 5 nights, Submitted from a mobile device5.4 第二步利用顺序不一致做频率统计接下来遇到的难题是不同行子项数量不同3/5/6 个且顺序各异直接对整列做短语计数会漏标或错标。本课给出的巧妙解法是把顺序不一致变成统计工具——既然每个标签是多词短语且以逗号分隔就按其在行内的位置拆成 6 个临时列再melt合并成一列最后对合并列跑value_counts()# Now split the strings into a list tag_list_df df.Tags.str.split(,, expand True) # Remove leading and trailing spaces df[Tag_1] tag_list_df[0].str.strip() df[Tag_2] tag_list_df[1].str.strip() df[Tag_3] tag_list_df[2].str.strip() df[Tag_4] tag_list_df[3].str.strip() df[Tag_5] tag_list_df[4].str.strip() df[Tag_6] tag_list_df[5].str.strip() # Merge the 6 columns into one with melt df_tags df.melt(value_vars[Tag_1, Tag_2, Tag_3, Tag_4, Tag_5, Tag_6])以上两段拆分/合并代码出自 solution/2-notebook.ipynb其中第 9 个 cell 用df_tags.shape打印出未过滤标签矩阵为(2514684, 2)随后用value_counts()得到 2428 个唯一标签。该 notebook 还示范了如何用str.contains(Standard|room|Stayed|device|Beds|Suite|Studio|King|Superior|Double, caseFalse)一次性排除房型/晚数/设备标签并用query(count 1000)收拢高频标签。打印前 22 个高频标签部分TagCountLeisure trip417778Submitted from a mobile device307640Couple252294Stayed 1 night193645Stayed 2 nights133937Solo traveler108545Stayed 3 nights95821Business trip82939Group65392Family with young children61015Stayed 4 nights47817Double Room35207Standard Double Room32248Superior Double Room31393Family with older children26349Deluxe Double Room24823Double or Twin Room22393Stayed 5 nights20845Standard Double or Twin Room17483Classic Double Room16989Superior Double or Twin Room135702 rooms123935.5 第三步剔除入住晚数与房型标签注意剔除不等于从数据集中删除行只是把这类值从统计/保留范围里拿掉。入住晚数类标签部分Length of stayCountStayed 1 night193645Stayed 2 nights133937Stayed 3 nights95821Stayed 4 nights47817Stayed 5 nights20845Stayed 6 nights9776Stayed 7 nights7399Stayed 8 nights2502Stayed 9 nights1293......房型类标签部分Type of roomCountDouble Room35207Standard Double Room32248Superior Double Room31393Deluxe Double Room24823Double or Twin Room22393Standard Double or Twin Room17483Classic Double Room16989Superior Double or Twin Room13570剔除后最终保留下来的有用标签只剩 8 个TagCountLeisure trip417778Couple252294Solo traveler108545Business trip82939Group (combined with Travellers with friends)67535Family with young children61015Family with older children26349With a pet1405其中Travellers with friends1610 次见 2-notebook 输出与Group语义近似官方代码将二者合并计数51593 1610 ≈ 53198与表格中 67535 的数量级差异来自不同清洗阶段的计数口径均来自官方 notebook 输出。5.6 第四步为每个有用标签生成 0/1 特征列最后为上述 8 个标签各建一列某行评论的Tags命中该标签则填 1否则填 0。最终得到的聚合计数有多少评论者选择这家酒店是为了商务 vs 休闲、或者带了宠物正是推荐系统可用的特征# Process the Tags into new columns # The file Hotel_Reviews_Tags.py, identifies the most important tags # Leisure trip, Couple, Solo traveler, Business trip, Group combined with Travelers with friends, # Family with young children, Family with older children, With a pet df[Leisure_trip] df.Tags.apply(lambda tag: 1 if Leisure trip in tag else 0) df[Couple] df.Tags.apply(lambda tag: 1 if Couple in tag else 0) df[Solo_traveler] df.Tags.apply(lambda tag: 1 if Solo traveler in tag else 0) df[Business_trip] df.Tags.apply(lambda tag: 1 if Business trip in tag else 0) df[Group] df.Tags.apply(lambda tag: 1 if Group in tag or Travelers with friends in tag else 0) df[Family_with_young_children] df.Tags.apply(lambda tag: 1 if Family with young children in tag else 0) df[Family_with_older_children] df.Tags.apply(lambda tag: 1 if Family with older children in tag else 0) df[With_a_pet] df.Tags.apply(lambda tag: 1 if With a pet in tag else 0)5.7 保存过滤结果把剩余不需要的列一并删除并保存为Hotel_Reviews_Filtered.csv官方过滤 notebook 实测整个过滤阶段耗时约 23.74 秒df.drop([Review_Total_Negative_Word_Counts, Review_Total_Positive_Word_Counts, days_since_review, Total_Number_of_Reviews_Reviewer_Has_Given], axis 1, inplaceTrue) # Saving new data file with calculated columns print(Saving results to Hotel_Reviews_Filtered.csv) df.to_csv(r../data/Hotel_Reviews_Filtered.csv, index False)至此数据集完成瘦身坐标、原始统计列、晚数/房型/设备标签全部剔除取而代之的是可验证的Total_Number_of_Reviews、Average_Score与 8 个标签 0/1 列。六、情感分析阶段加载过滤数据与停用词优化6.1 加载过滤后的数据而不是原始数据情感分析阶段必须加载上一阶段保存的Hotel_Reviews_Filtered.csv而不是原始Hotel_Reviews.csvimport time import pandas as pd import nltk as nltk from nltk.corpus import stopwords from nltk.sentiment.vader import SentimentIntensityAnalyzer nltk.download(vader_lexicon) # Load the filtered hotel reviews from CSV df pd.read_csv(../../data/Hotel_Reviews_Filtered.csv)nltk.download(vader_lexicon)会下载 VADER 情感词典官方 3-notebook 的 cell 9 输出显示首次运行时下载到本机nltk_data目录并返回True。6.2 为什么先去掉停用词直接对正负评论两列跑情感分析非常慢官方在配置较高的测试笔记本上实测耗时1214 分钟视情感分析库而定。而停用词不改变句子情感的常见英文词去掉后分析会更快且不会损失准确率——它们不影响情感只拖慢速度。效果示例最长的负面评论原有395 词去停用词后只剩195 词代价示例对 515,000 行、2 列评论去停用词在测试设备上只用了3.3 秒官方 3-notebook 实测约 5.77 秒视 CPU/RAM/SSD 而异。6.3 用 set 缓存加速停用词去除把停用词列表转成set哈希查找 O(1)再逐词过滤是官方代码采用的高效写法并引用了 Ryan Han 在 Kaggle 上的性能对比方案from nltk.corpus import stopwords # Load the hotel reviews from CSV df pd.read_csv(../../data/Hotel_Reviews_Filtered.csv) # Remove stop words - can be slow for a lot of text! start time.time() cache set(stopwords.words(english)) def remove_stopwords(review): text .join([word for word in review.split() if word not in cache]) return text # Remove the stop words from both columns df.Negative_Review df.Negative_Review.apply(remove_stopwords) df.Positive_Review df.Positive_Review.apply(remove_stopwords)七、VADER 情感打分复合分值与边界情况7.1 认识 VADER本课选用 NLTK 自带的VADERValence Aware Dictionary and sEntiment Reasoner一个基于规则的情感分析模型专为社交媒体文本设计。NLTK 还提供其他分析器可以替换后对比准确率。VADER 出处Hutto, C.J. Gilbert, E.E. (2014). VADER: A Parsimonious Rule-based Model for Sentiment Analysis of Social Media Text. Eighth International Conference on Weblogs and Social Media (ICWSM-14). Ann Arbor, MI, June 2014.7.2 处理无文本边界情况Negative_Review/Positive_Review两列中评论者没写字时值为No Negative/No Positive。因此打分函数要先处理这三种输入之一from nltk.sentiment.vader import SentimentIntensityAnalyzer # Create the vader sentiment analyser (there are others in NLTK you can try too) vader_sentiment SentimentIntensityAnalyzer() # There are 3 possibilities of input for a review: # It could be No Negative, in which case, return 0 # It could be No Positive, in which case, return 0 # It could be a review, in which case calculate the sentiment def calc_sentiment(review): if review No Negative or review No Positive: return 0 return vader_sentiment.polarity_scores(review)[compound]polarity_scores(review)[compound]是 VADER 输出的复合分范围在-1极度负面到 1极度正面之间。7.3 应用到全量数据并核对结果对两列分别apply生成Negative_Sentiment与Positive_Sentiment# Add a negative sentiment and positive sentiment column print(Calculating sentiment columns for both positive and negative reviews) start time.time() df[Negative_Sentiment] df.Negative_Review.apply(calc_sentiment) df[Positive_Sentiment] df.Positive_Review.apply(calc_sentiment) end time.time() print(Calculating sentiment took str(round(end - start, 2)) seconds)文档实测约120 秒机器不同会浮动官方 3-notebook cell 16 输出为 201.07 秒。验证方法把情感分与同一行的Reviewer_Score对比。若一条负面评论的情感分为 1极度正面而评论者给了最低分说明要么评论文本与评分不匹配要么分析器判断失误。预期会有看似错误的结果例如讽刺性评论Of course I LOVED sleeping in a room with no heating我当然超爱睡在没有暖气的房间VADER 会判为正面而人眼能识别这是讽刺——这是规则型情感模型的已知局限。按情感分排序打印快速肉眼核查df df.sort_values(by[Negative_Sentiment], ascendingTrue) print(df[[Negative_Review, Negative_Sentiment]]) df df.sort_values(by[Positive_Sentiment], ascendingTrue) print(df[[Positive_Review, Positive_Sentiment]])官方 3-notebookcell 17输出显示Negative_Sentiment最低可达 -0.9920、最高 0.9948Positive_Sentiment最低 -0.9820、最高 0.9987共 515738 行 × 2 列——与 515,000 行总量吻合且可以看到去停用词后的文本片段如 So bad experience memories...。八、整理列顺序并保存最终数据集保存前把新列重排到顺手的位置纯美观便于人工浏览数据# Reorder the columns (This is cosmetic, but to make it easier to explore the data later) df df.reindex([Hotel_Name, Hotel_Address, Total_Number_of_Reviews, Average_Score, Reviewer_Score, Negative_Sentiment, Positive_Sentiment, Reviewer_Nationality, Leisure_trip, Couple, Solo_traveler, Business_trip, Group, Family_with_young_children, Family_with_older_children, With_a_pet, Negative_Review, Positive_Review], axis1) print(Saving results to Hotel_Reviews_NLP.csv) df.to_csv(r../data/Hotel_Reviews_NLP.csv, index False)最终列结构清晰体现了元数据在前、情感分居中、标签特征与原文在后的设计酒店名/地址 → 评论数与均分 → 评论者分数 → 正负情感分 → 国籍 → 8 个标签 0/1 列 → 正负评论文本。九、整条流水线回顾与验证本课的完整处理链也即官方文档给出的四步回顾上一课用 6-NLP/4-Hotel-Reviews-1/solution/notebook.ipynb 探索原始Hotel_Reviews.csv用 6-NLP/5-Hotel-Reviews-2/solution/1-notebook.ipynb过滤 notebook把 Hotel_Reviews.csv 处理为Hotel_Reviews_Filtered.csv约 24 秒用 6-NLP/5-Hotel-Reviews-2/solution/3-notebook.ipynb情感分析 notebook需先执行过滤 notebook把 Filtered 处理为Hotel_Reviews_NLP.csv去停用词约 5.8 秒 情感打分约 201 秒在随后的挑战中直接使用 Hotel_Reviews_NLP.csv。如果你只想快速复现标签筛选这一步可单独参考 6-NLP/5-Hotel-Reviews-2/solution/2-notebook.ipynb它演示了从拆列、melt、value_counts 到str.contains排除与query(count 1000)收拢高频标签的完整中间过程输出The shape of the tags with no filtering: (2514684, 2)。十、课后挑战用聚类探索情感模式得到带Negative_Sentiment/Positive_Sentiment的最终数据集后本课挑战要求尝试用课程中学过的策略例如聚类找出与情感相关的模式比如哪些酒店/城市/客群组合的情感分分布有显著差异。完整的作业说明与评分标准见 6-NLP/5-Hotel-Reviews-2/assignment.md作业要求换一个不同数据集新建 notebook 并记录数据处理与情感打分的完整思路优秀标准为notebook 完整、单元格注释充分、清楚解释情感如何被赋予。关键要点小结先验证再使用对无法独立复现的统计列Average_Score、Total_Number_of_Reviews用groupby transform基于真实数据重建是保证数据可信度的核心手段。把结构问题转化为 NLP 问题Tags列顺序/数量不一致时先stripreplace清洗格式再按位置拆列 meltvalue_counts即可在数秒内得到 2428 个唯一标签的全量频次。业务判断先行标签的取舍出行类型、客群保留房型、设备、晚数剔除来自推荐场景的业务推理而不是数据本身的统计显著性。大规模文本分析的性价比优化set(stopwords.words(english))缓存 逐词过滤用 36 秒换掉 1214 分钟的情感分析等待去掉停用词不影响 VADER 的情感判断准确率。情感分需要人类核查VADER 的compound分-11要与Reviewer_Score交叉验证讽刺等修辞是规则型模型的已知盲区。【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考