资讯动态

ML-For-Beginners 实战:用 NLTK/VADER 对酒店评论做数据清洗、标签挖掘与情感分析

发布时间:2026/9/11 22:41:15 来源:尧图企业网站定制
ML-For-Beginners 实战用 NLTK/VADER 对酒店评论做数据清洗、标签挖掘与情感分析【免费下载链接】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」承接上一课对酒店评论数据集Hotel_Reviews.csv51.5 万行、1427 家酒店的探索性分析。你将掌握一条完整的文本分析流水线对原始列做清洗与重算、把脏乱的 Tags 列转换为可用的标签特征、移除停用词后使用 NLTK 自带的 VADER 情感分析器为每一条正/负面评论打分最终产出可直接用于酒店推荐或聚类分析的Hotel_Reviews_NLP.csv。学完本课你既能复现完整的可运行代码也能理解何时该清洗、哪些特征该保留的取舍思路。课程背景与整体数据流水线上一课6-NLP/4-Hotel-Reviews-1已经对原始数据集做了初步探索但数据仍存在几个问题部分列充满无用信息、部分数值看似错误、即便数值正确其计算方式也不透明无法用你自己的计算独立验证。因此本课的目标非常明确——在不可信的数据之上重建一套可验证、可用的特征集。本课整体分为两条主线对应两个练习过滤Filtering清洗列、重算统计量、把 Tags 列加工成 8 个有业务意义的标签特征产出Hotel_Reviews_Filtered.csv情感分析Sentiment Analysis移除停用词后用 VADER 对正负评论文本打分产出Hotel_Reviews_NLP.csv。四个步骤的数据流可以概括为上一课用探索 notebook 查看原始Hotel_Reviews.csv本课的过滤 notebook6-NLP/5-Hotel-Reviews-2/solution/1-notebook.ipynb把 Hotel_Reviews.csv 过滤为Hotel_Reviews_Filtered.csv情感分析 notebook6-NLP/5-Hotel-Reviews-2/solution/3-notebook.ipynb把过滤后的文件加工为Hotel_Reviews_NLP.csv使用 Hotel_Reviews_NLP.csv 完成课后挑战与作业。注意原始数据文件并不在本仓库中需要按 6-NLP/data/README.md 的说明下载到6-NLP/data/目录本课代码中的相对路径均假设数据位于6-NLP/data/下。练习一进一步的数据清洗Filtering1. 初始列处理删除经纬度、归一化酒店地址第一步是删除对后续分析毫无用处的经纬度列lat和lng然后处理地址列。原始Hotel_Address包含完整街道地址而本数据集实际只涉及 6 座城市因此可以用一个映射函数把完整地址压缩为城市, 国家格式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())在解决方案 notebook6-NLP/5-Hotel-Reviews-2/solution/1-notebook.ipynb中replace_address还带有else: return row.Hotel_Address兜底分支确保无法匹配的地址不会被置空。归一化之后地址就变成了可查询的国家级维度display(df.groupby(Hotel_Address).agg({Hotel_Name: nunique}))Hotel_AddressHotel_Name酒店数Amsterdam, Netherlands105Barcelona, Spain211London, United Kingdom400Milan, Italy162Paris, France458Vienna, Austria158注意value_counts()的计数之和应与评论总数一致可用此方法做一次快速校验。2. 处理酒店元评论列重算评论数与平均分Additional_Number_of_Scoring来自其他平台的额外评分数量无法验证直接删除Total_Number_of_Reviews和Average_Score这两列既然来源不明就用自己的数据重新计算# 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)做分组变换Total_Number_of_Reviews变成每家酒店在本数据集中实际出现的评论条数Average_Score变成该酒店所有Reviewer_Score的均值并保留 1 位小数。由此得到的统计量可以随时用原始列复算验证彻底解决了数值不可信的问题。3. 处理评论文本列与评论者列评论文本列中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评论者历史评论数删除Reviewer_Nationality评论者国籍保留它之后可用来分析不同国家用户的评价差异。第二步Tags 列的清洗与标签挖掘Tags 列的结构问题Tags列非常棘手它其实是以文本形式存储的列表例如[ Business trip , Solo traveler , Single Room , Stayed 5 nights , Submitted from a mobile device ]。麻烦之处在于每个评论的 Tags 数量不一致有的 3 个、有的 5 个、有的 6 个标签出现的顺序也不固定数据规模大51.5 万行、1427 家酒店、约 676 万词人类根本无法逐一阅读筛选。这正是 NLP 的用武之地扫描文本、统计最常见的短语及其频次。但要注意我们需要的是多词短语如Business trip而并非单个词如果对全部 676 万词直接跑多词短语频次算法耗时将非常可观。好在我们已经通过探索性分析看到了 Tags 的样例可以大幅缩小处理范围。清洗格式去掉方括号与引号先要把 Tags 从文本形式的列表变成干净的逗号分隔字符串。pandas 的字符串方法提供了最快捷的做法# Remove opening and closing brackets df.Tags df.Tags.str.strip([]) # remove all quotes too df.Tags df.Tags.str.replace( , , ,, regex False)处理后每个 Tag 变成类似Business trip, Solo traveler, Single Room, Stayed 5 nights, Submitted from a mobile device的格式。这里用regexFalse做字面替换而非正则替换就是为了在大数据量下保持速度。利用逗号与顺序差异melt value_counts 统计短语下一步的难点是每个标签是多词短语且在各评论中顺序不一如果逐行做精确匹配计数会出错酒店可能拿不到应得的标签。解决方案 notebook6-NLP/5-Hotel-Reviews-2/solution/2-notebook.ipynb给出了巧妙的解法既然每个标签都用逗号分隔就先把每行按逗号拆分成 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]) # Get the value counts tag_vc df_tags.value.value_counts() print(The shape of the tags with no filtering:, str(df_tags.shape))对结果打印value_counts()可以发现共有2428 个唯一标签。下表是其中一小部分注意计数来自文档示例实际以 notebook 运行输出为准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 rooms12393像Submitted from a mobile device这类高频标签对我们的目标没有用处不过统计它几乎不花时间可以留着忽略。在 2-notebook 中还演示了用str.contains一次性剔除无关短语的过滤写法Standard|room|Stayed|device|Beds|Suite|Studio|King|Superior|Double并把计数低于 1000 的罕见标签过滤掉。决定保留哪些标签保留两类、剔除其余本数据集的目标是加入情感与能帮你选酒店的特征无论为自己选酒店还是为客户开发酒店推荐机器人。因此要追问哪些 Tags 对最终数据集有用本课给出的取舍是旅行类型商务/休闲——相关保留客群类型伴侣、独自、家庭、团体等——重要保留房型、套房、单间类型——不相关所有酒店房间大同小异剔除提交评论的设备——不相关剔除入住晚数——也许能用于推断住得久更喜欢但很牵强大概率无关剔除。简而言之只保留 2 类标签其余全部排除。第一步剔除入住晚数标签注意不是从数据集中删除这些行只是不再把它们作为要统计/保留的值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 friends与Group含义几乎相同合并为一类67535 65392 2143 附近具体以运行结果为准是合理的。识别这些标签的完整代码见 6-NLP/5-Hotel-Reviews-2/solution/2-notebook.ipynb。为每个标签创建 one-hot 特征列最后一步是为这 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)保存过滤后的文件把剩余不需要的列一并删除然后以新文件名保存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)解决方案 notebook 的运行输出显示整个过滤流程约耗时 23.74 秒具体取决于机器配置生成的文件即Hotel_Reviews_Filtered.csv。练习二VADER 情感分析本节对过滤后的评论列做情感分析并把结果保存到新数据集。注意这里加载的是上一节保存的过滤文件而不是原始数据集import 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 所需的词典数据。移除停用词用最小代价换取大幅提速如果不做任何处理就对正负评论列直接跑情感分析会很慢——文档记录在配置较好的测试笔记本上需要1214 分钟取决于所用的情感库。因此值得先做优化移除停用词不影响句子情感的常见英文单词。移除停用词不会降低准确率停用词不携带情感只是拖慢分析却能让分析显著提速。例如数据集中最长的负面评论有 395 个词移除停用词后只剩 195 个词。移除停用词本身也很快在 515000 行数据上对 2 个评论列做处理文档记录的测试耗时约 3.3 秒solution 3-notebook 的运行输出为 5.77 秒视机器 CPU 速度、内存、是否 SSD 而异。既然代价如此之低只要对情感分析有帮助就值得做。实现上采用先把停用词放入 set 缓存、再做集合成员判断的写法避免逐词反复在词表中线性查找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! # 这里采用 Kaggle 用户 Ryan Han 实测过的高效停用词移除方案 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 计算情感分数接下来对正负评论列分别计算情感并把结果存入 2 个新列。验证情感是否准确的方法是对照同一条评论的Reviewer_Score例如情感分析认为某条负面评论的情感分是 1极度正面、正面评论情感分也是 1而评论者却给了酒店最低分那么要么评论文本与分数不匹配要么分析器没能正确识别情感。应当预期一部分情感分完全错误且往往可以解释——例如极端讽刺的评论当然我超喜欢睡在没有暖气的房间分析器会把它判为正面而人类读者一看便知是反讽。NLTK 提供了多种可学习的情感分析器可以替换着比较哪种更准。本课使用VADERValence Aware Dictionary and sEntiment ReasonerHutto Gilbert, 2014 提出的基于规则的社交文本情感分析模型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]calc_sentiment处理了三种输入评论为占位文本No Negative或No Positive表示该评论者没有写负面/正面文字时直接返回 0否则调用polarity_scores(review)[compound]取复合情感分数范围约 -1 到 1负值为负面、正值为正面。真正计算时把函数 apply 到两个评论列即可solution 3-notebook 的实际输出约为 201.07 秒文档记录的参考值约 120 秒均取决于机器# 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)如果想打印结果、直观检查情感是否与评论相符可以按情感分排序后输出solution 3-notebook 中约 515738 行评论的排序输出显示最负面的负面评论分数可低至约 -0.99最正面的接近 0.99正面评论列中同样可能出现极负面的分数——这正是文本与评分不一致、需要人工复核的案例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]])列重排与最终保存进入课后挑战之前最后一步是保存文件同时把新增列重排到便于阅读的顺序纯属美观调整便于后续人工探索数据# 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)建议按顺序完整运行 6-NLP/5-Hotel-Reviews-2/solution/3-notebook.ipynb先运行过滤 notebook 生成Hotel_Reviews_Filtered.csv再运行过滤 notebook 之前也可先运行 2-notebook 观察 Tags 的频次分布理解 8 个标签的来历。数据流水线回顾步骤输入处理方式输出1Hotel_Reviews.csv上一课探索 notebook 探索分析6-NLP/4-Hotel-Reviews-1/solution/notebook.ipynb理解数据、确定处理方向2Hotel_Reviews.csv本课过滤 notebook1-notebook.ipynb删列、重算统计量、Tags 挖掘Hotel_Reviews_Filtered.csv3Hotel_Reviews_Filtered.csv情感分析 notebook3-notebook.ipynb去停用词 VADER 打分Hotel_Reviews_NLP.csv4Hotel_Reviews_NLP.csv用于课后挑战与作业发现情感相关模式课后挑战与作业挑战既然数据已经完成了情感分析试着用本课程学过的策略比如聚类在情感分数周围发现模式——例如不同酒店类型、不同客群的情感分布差异。作业见 6-NLP/5-Hotel-Reviews-2/assignment.md——换一个数据集用 NLTK 为文本赋予情感分数。由于新数据集通常也需要数据清洗作业要求新建一个 notebook 并把思考过程记录成文评价标准包括 notebook 是否完整、单元格是否解释清楚情感如何赋值优秀 / 合格 / 待改进三档。结语从原始数据到可用特征本课完成了四件事探索并识别数据中的问题过滤掉无用信息把结构混乱的 Tags 列转换成 8 个可聚合、可解释的标签特征重算平均值并新增情感列。经此流程Hotel_Reviews_NLP.csv中的每一列都有了可验证的来源——地址被归一化、统计量可由原始列复算、情感分可直接对照评分检验。这条清洗 → 特征工程 → 文本分析的流水线正是把原始文本数据变成可决策信息的标准范式。【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价