ML-For-Beginners NLP 实战:用 pandas 探索 51.5 万条欧洲酒店评价数据(Hotel Reviews 课程)
ML-For-Beginners NLP 实战用 pandas 探索 51.5 万条欧洲酒店评价数据Hotel Reviews 课程【免费下载链接】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 课程仓库 NLP 章节的《酒店评价情感分析数据处理》一课带你完成一次真实场景的探索性数据分析EDA从加载 515K 条 Booking.com 酒店评价 CSV 开始逐列理解 17 个字段的业务含义与数据陷阱非标准Tags、口径不明的Average_Score并掌握用 pandas 计算列频率、按酒店分组求均分、以及 Lambda 与向量化sum两种行级统计的取舍。读完并跟练后你将能独立判断哪些列可信、哪些列应弃用为后续的情感分析与推荐模型准备干净的数据。课程目标与准备这一节承接 NLP 前面的基础课用真实数据集做探索性数据分析。一旦搞清楚各列的可用性你将学会三件事如何删除不必要的列如何基于现有列计算新数据如何保存处理后的数据集供最终挑战final challenge使用数据说明课程使用的是欧洲 515K 条酒店评价数据集Kaggle 上的 515K Hotel Reviews Data in Europe作者 Jiashen LiuCC0 公共领域许可从 Booking.com 公开页面爬取。数据集覆盖 6 座城市中的 1493 家酒店解压后约 230 MB。环境准备清单能运行 Python 3 的.ipynbnotebookpandas库NLTK库需本地安装后续情感分析课使用数据集本身下载后放到 NLP 课程的/data根目录。仓库中的 6-NLP/data/README.md 即提示Download the hotel review data to this folder即把Hotel_Reviews.csv放入 6-NLP/data/ 目录代码中使用的相对路径../../data/Hotel_Reviews.csv正是从 6-NLP/4-Hotel-Reviews-1/ 回退两级指向该目录。假设的业务场景你要基于情感分析和住客评分构建一个酒店推荐机器人。借助 Python、该数据集和 NLTK 的情感分析你可以回答评价中最高频的词和短语是什么官方Tags如带小孩的家庭是否与评分相关例如某酒店差评是否更多来自Family with young children而非Solo traveller若如此可能说明该酒店更适合独行旅客NLTK 算出的情感分是否与评价者的数字评分一致配套练习文件为 notebook.ipynb仓库同时提供了 solution/notebook.ipynb 参考答案 notebook可对照运行验证。数据集列结构17 列的分组解读用 VS Code 或 Excel 打开 CSV 后列头共 17 个Hotel_Address, Additional_Number_of_Scoring, Review_Date, Average_Score, Hotel_Name, Reviewer_Nationality, Negative_Review, Review_Total_Negative_Word_Counts, Total_Number_of_Reviews, Positive_Review, Review_Total_Positive_Word_Counts, Total_Number_of_Reviews_Reviewer_Has_Given, Reviewer_Score, Tags, days_since_review, lat, lng按业务含义可分成四组酒店列Hotel_Name、Hotel_Address、lat纬度、lng经度用lat/lng可以画地图展示酒店位置甚至按正/负面评价着色Hotel_Address对模型帮助不大实践中通常替换为国家字段以便排序检索酒店元评价列Average_Score据数据集作者是基于过去一年最新评论计算的酒店平均分——这是一种很特别的算法但既然这是爬取到的原始值暂且按字面接受✅ 思考题基于其他列你能想出另一种计算平均分的方式吗答案就是用Reviewer_Score自行分组求均值见下文Total_Number_of_Reviews该酒店获得的评论总数——在不写代码的情况下无法确认它是否指本数据集内的评论Additional_Number_of_Scoring表示打了分但没有写正/负面评论的评分数量评价列Reviewer_Score最多一位小数的数字取值范围 2.5 到 10注意下限是 2.5 而不是 0且没有解释为什么Negative_Review若评价者没写内容字段值为No Negative注意评价者也可能在负面栏写好评如这酒店没什么不好Review_Total_Negative_Word_Counts负面词数量越多往往评分越低未经情感验证的弱信号Positive_Review没写内容时为No Positive同理正面栏里也可能出现差评Review_Total_Positive_Word_Counts正面词数量越多评分往往越高Review_Date与days_since_review可施加新鲜度/陈旧度度量——酒店可能换了管理层、翻新、加了泳池旧评价未必准确Tags评价者自选的短描述客人类型、房型、入住晚数、评价提交设备。使用这些标签是有坑的见下节评价者列Total_Number_of_Reviews_Reviewer_Has_Given可能是推荐模型的一个因子例如高产评价者是否更偏负面但单条评价中的评价者没有唯一 ID无法关联其全部评论虽有 30 位评价者写了 100 条以上评论却难以在推荐模型中利用Reviewer_Nationality警惕把某些国籍的人更爱给差评/好评这种基于国家甚至种族的刻板印象写进模型——每位评价者都是基于个人体验过往住宿、旅途距离、个人性情等多重因素写评论的把分数归因于国籍很难站住脚一个典型样本平均分总评论数评价者评分负面评价正面评价标签7.819452.5这现在根本不是酒店是工地。从清晨起我就被建筑噪音折磨……115 词的长差评没有。糟糕的地方。别来。商务出行, 情侣, 标准双床房, 入住两晚这条样本暴露了文本数据的本质酒店整体平均分 7.8、评论 1945 条但这位评价者给了 2.5 分并写了一篇 115 词的差评正面栏只写了 7 个词的警告Nothing / Terrible place / Stay away。如果只数词数而不理解词义与情感就会严重误判评价者的真实意图。另外 2.5 分也令人困惑——入住体验那么差为何还打几分因为该数据集的最低分就是 2.5最高 10。Tags 列的麻烦没有标准化初看用Tags给数据分类很合理但它并不统一某家酒店可能给Single room / Twin room / Double room下一家却是Deluxe Single Room / Classic Queen Room / Executive King Room。它们可能是一回事但变体太多只能二选一试图把所有词归一到单一标准——非常难因为很多情况下映射路径不清晰Classic single room能映射到Single room但Superior Queen Room with Courtyard Garden or City View就很难走 NLP 路线统计Solo、Business Traveller、Family with young children等特定词组在每家酒店的出现频率把它作为推荐模型的输入Tags 通常但不总是是一个包含 56 个逗号分隔值的字段分别对应行程类型、客人类型、房型、入住晚数、提交评价的设备。由于有些评价者会留空某个字段值的顺序并不固定。以客人类型为例Tags列中该字段有 1025 种唯一取值其中只有一部分指代客人类型另一些是房型等。只筛选提到 family 的值结果里混入大量Family room房型把with也纳入条件、只统计Family with开头结果就好多了——515,000 条结果中有超过 80,000 条包含 Family with young children 或 Family with older children。结论Tags 列并非完全无用但要让它有用需要额外清洗工作。Average_Score 的口径疑云数据集存在几处文档作者也没完全解释清楚的怪异/矛盾点建模时必须心里有数数据集中与平均分、评论数相关的列有Hotel_Name、Additional_Number_of_Scoring、Average_Score、Total_Number_of_Reviews、Reviewer_Score评论数最多的酒店是Britannia International Hotel Canary Wharf数据集内 4789 条评论但它的Total_Number_of_Reviews是 9086。也许有大量只打分不写评的记录于是加上Additional_Number_of_Scoring 26824789 2682 7471仍差 1615 到 9086若猜Average_Score是数据集内评论的均值Kaggle 描述却是按过去一年最新评论计算。同样以该酒店为例给出的平均分是 7.1而用数据集中的Reviewer_Score算出来是 6.8——接近但不相等只能猜测是那些只打分不写评的分数把均值拉高到 7.1。无法验证也就难以信任基于我们手中没有的数据的Average_Score、Additional_Number_of_Scoring、Total_Number_of_Reviews更复杂的评论数第二多的酒店计算均分 8.12 与Average_Score8.1 几乎吻合——这是巧合还是第一家酒店是异常值基于可能只是个别异常、大多数值能对上的假设正确做法就是写代码验证——这正是下面练习的核心。⚠️ 一个提醒使用这份数据时你会写代码从文本中提取含义而无需亲自逐条阅读——这正是 NLP 的精髓由程序解释语义/情感不依赖人工。但注意部分负面评价可能包含冒犯性内容公开网站爬取的通病。建议让代码去度量情感而不是自己逐条读并受其影响。练习一加载数据# Load the hotel reviews from CSV import pandas as pd import time # importing time so the start and end time can be used to calculate file loading time print(Loading data file now, this could take a while depending on file size) start time.time() # df is DataFrame - make sure you downloaded the file to the data folder df pd.read_csv(../../data/Hotel_Reviews.csv) end time.time() print(Loading took str(round(end - start, 2)) seconds)该 CSV 超过 50 万行但只有 17 列。这份数据恰好是干净的——没有会干扰只预期英文字符的算法的其他语言字符可以直接处理。✅ 拓展思考如果拿到的是含非英语字符的文本你会先做哪些预处理先别急着对Negative_Review/Positive_Review开 NLP——要先用下面的代码验证数据集给定的统计值是否与你用 pandas 算出的值一致。练习二DataFrame 九问含完整参考答案把以下问题当编程题处理先自己作答再对照。原则同许多编程任务有几条路可走时选最简单、日后回看最容易理解的那条。任务清单打印 DataFrame 的 shape行、列数计算评价者国籍的频率Reviewer_Nationality有多少个不同取值最常见的国籍是什么打印国家与评论数接下来 10 个高频国籍及频率对上述 Top 10 国籍各自被评最多的酒店是哪家数据集中每家酒店各有多少条评论新增一列Calc_Average_Score用数据集中各评价者Reviewer_Score按酒店分组求均值保留一位小数是否存在Average_Score与Calc_Average_Score四舍五入到一位小数后相等的酒店提示写一个接收行 Series 的函数比较两值不等时打印消息再用.apply()逐行执行统计Negative_Review为 No Negative 的行数统计Positive_Review为 No Positive 的行数同时满足 7 和 8 的行数参考答案与结果解读1. 形状print(The shape of the data (rows, cols) is str(df.shape)) # The shape of the data (rows, cols) is (515738, 17)2. 国籍频率# value_counts() creates a Series object that has index and values in this case, # the country and the frequency they occur in reviewer nationality nationality_freq df[Reviewer_Nationality].value_counts() print(There are str(nationality_freq.size) different nationalities) print(nationality_freq)结果227 个不同国籍英国以 245,246 条居首随后是美国35,437、澳大利亚21,686、爱尔兰14,827、阿联酋10,235长尾一直到只出现 1 次的科摩罗、帕劳等。print(The highest frequency reviewer nationality is str(nationality_freq.index[0]).strip() with str(nationality_freq[0]) reviews.) # Notice there is a leading space on the values, strip() removes that for printing print(The next 10 highest frequency reviewer nationalities are:) print(nationality_freq[1:11].to_string())注意输出值前面带一个空格strip()是为此准备的。Top 10 之后依次是United States of America 35437、Australia 21686、Ireland 14827、United Arab Emirates 10235、Saudi Arabia 8951、Netherlands 8772、Switzerland 8678、Germany 7941、Canada 7894、France 7296。3. Top 10 国籍各自最常评的酒店# Normally with pandas you will avoid an explicit loop, but wanted to show # creating a new dataframe using criteria # (dont do this with large amounts of data because it could be very slow) for nat in nationality_freq[:10].index: # First, extract all the rows that match the criteria into a new dataframe nat_df df[df[Reviewer_Nationality] nat] # Now get the hotel freq freq nat_df[Hotel_Name].value_counts() print(The most reviewed hotel for str(nat).strip() was str(freq.index[0]) with str(freq[0]) reviews.)输出摘录英国 → Britannia International Hotel Canary Wharf3833 条美国 → Hotel Esther a423 条澳大利亚 → Park Plaza Westminster Bridge London167 条加拿大 → St James Court A Taj Hotel London61 条等。注意这里用显式循环只是为了演示按条件构造新 DataFrame的写法原作者特别提示大数据量下这种过滤循环会很慢平时 pandas 应尽量避免。4. 每酒店评论数# First create a new dataframe based on the old one, removing the unneeded columns hotel_freq_df df.drop([Hotel_Address, Additional_Number_of_Scoring, Review_Date, Average_Score, Reviewer_Nationality, Negative_Review, Review_Total_Negative_Word_Counts, Positive_Review, Review_Total_Positive_Word_Counts, Total_Number_of_Reviews_Reviewer_Has_Given, Reviewer_Score, Tags, days_since_review, lat, lng], axis 1) # Group the rows by Hotel_Name, count them and put the result in a new column Total_Reviews_Found hotel_freq_df[Total_Reviews_Found] hotel_freq_df.groupby(Hotel_Name).transform(count) # Get rid of all the duplicated rows hotel_freq_df hotel_freq_df.drop_duplicates(subset [Hotel_Name]) display(hotel_freq_df)Hotel_NameTotal_Number_of_ReviewsTotal_Reviews_FoundBritannia International Hotel Canary Wharf90864789Park Plaza Westminster Bridge London121584169Copthorne Tara Hotel London Kensington71053578………Mercure Paris Porte d Orleans11010Hotel Wagner13510Hotel Gallitzinberg1738可以看到数据集内实际数出来的条数与Total_Number_of_Reviews对不上不确定该列是酒店总评论数但没全爬下来还是别的口径。正因为这种模糊后续模型不使用Total_Number_of_Reviews。这里用到的关键 API 是groupby(Hotel_Name).transform(count)——分组计数后把结果广播回每一行再用drop_duplicates收敛到每酒店一行。5. 自算平均分Calc_Average_Score# define a function that takes a row and performs some calculation with it def get_difference_review_avg(row): return row[Average_Score] - row[Calc_Average_Score] # mean is mathematical word for average df[Calc_Average_Score] round(df.groupby(Hotel_Name).Reviewer_Score.transform(mean), 1) # Add a new column with the difference between the two average scores df[Average_Score_Difference] df.apply(get_difference_review_avg, axis 1) # Create a df without all the duplicates of Hotel_Name (so only 1 row per hotel) review_scores_df df.drop_duplicates(subset [Hotel_Name]) # Sort the dataframe to find the lowest and highest average score difference review_scores_df review_scores_df.sort_values(by[Average_Score_Difference]) display(review_scores_df[[Average_Score_Difference, Average_Score, Calc_Average_Score, Hotel_Name]])偏差最大的酒店摘录Average_Score_DifferenceAverage_ScoreCalc_Average_ScoreHotel_Name-0.87.78.5Best Western Hotel Astoria-0.78.89.5Hotel Stendhal Place Vend me Paris MGallery-0.77.58.2Mercure Paris Porte d Orleans-0.77.98.6Renaissance Paris Vendome Hotel-0.57.07.5Hotel Royal Elys es…………0.77.56.8Mercure Paris Op ra Faubourg Montmartre0.87.16.3Holiday Inn Paris Montparnasse Pasteur0.96.85.9Villa Eugenie0.98.67.7MARQUIS Faubourg St Honor Relais Ch teaux1.37.25.9Kube Hotel Ice Bar结论只有 1 家酒店的偏差超过 1 分因此可以接受自算均分替代Average_Score。这也回答了之前的思考题——既然无法解释为何部分值吻合、部分不一致最稳妥就是用自己手里的Reviewer_Score重新算。参考实现见 solution/notebook.ipynb 中对应的get_difference_review_avg与transform(mean)单元格。6–9. 统计 No Negative / No Positive# with lambdas: start time.time() no_negative_reviews df.apply(lambda x: True if x[Negative_Review] No Negative else False , axis1) print(Number of No Negative reviews: str(len(no_negative_reviews[no_negative_reviews True].index))) no_positive_reviews df.apply(lambda x: True if x[Positive_Review] No Positive else False , axis1) print(Number of No Positive reviews: str(len(no_positive_reviews[no_positive_reviews True].index))) both_no_reviews df.apply(lambda x: True if x[Negative_Review] No Negative and x[Positive_Review] No Positive else False , axis1) print(Number of both No Negative and No Positive reviews: str(len(both_no_reviews[both_no_reviews True].index))) end time.time() print(Lambdas took str(round(end - start, 2)) seconds) # Number of No Negative reviews: 127890 # Number of No Positive reviews: 35946 # Number of both No Negative and No Positive reviews: 127 # Lambdas took 9.64 seconds更高效的写法向量化 sum 对比 Lambda同一统计不用 Lambda改用布尔序列加sum# without lambdas (using a mixture of notations to show you can use both) start time.time() no_negative_reviews sum(df.Negative_Review No Negative) print(Number of No Negative reviews: str(no_negative_reviews)) no_positive_reviews sum(df[Positive_Review] No Positive) print(Number of No Positive reviews: str(no_positive_reviews)) both_no_reviews sum((df.Negative_Review No Negative) (df.Positive_Review No Positive)) print(Number of both No Negative and No Positive reviews: str(both_no_reviews)) end time.time() print(Sum took str(round(end - start, 2)) seconds) # Number of No Negative reviews: 127890 # Number of No Positive reviews: 35946 # Number of both No Negative and No Positive reviews: 127 # Sum took 0.19 seconds两种写法结果完全一致127890 / 35946 / 127但向量化sum用了0.19 秒而df.apply(lambda ...)用了9.64 秒——在 51 万行规模上相差约 50 倍。这正是课程反复强调的原则DataFrame 的向量化 API 优先apply逐行 Lambda 仅作兜底。另一个值得记住的发现有127 行同时是 No Negative 和 No Positive占 515738 行的 0.02%——这些评价者只打了数字分、拒绝写任何文字评论。虽然比例小到不太可能让模型偏向某侧但评论数据集里竟有零文本的行这一点本身就说明动手前探索数据才能发现这些意料之外的形态。挑战与延伸本课的核心启示在文本数据上做任何运算之前先彻底理解数据的形态与毛病foibles。课程挑战建议你去翻几个文本密集型数据集找出可能给模型引入偏见或扭曲情感的字段。下一步课程的下一节 Hotel-Reviews-2将在本 EDA 基础上过滤数据、加入情感分析配套延伸练习是深入阅读 NLTK 官方书籍并完成其习题见 assignment.md。适用前提与限制本文所有代码路径../../data/Hotel_Reviews.csv基于仓库目录结构要求先把约 230 MB 的Hotel_Reviews.csv下载至 6-NLP/data/数据集本身托管在 Kaggle 且不在仓库内且Reviewer_Score最低 2.5、Tags非标准化等特性均源于该特定数据集换数据集时需重新验证这些结论。【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考