ML-For-Beginners 实战教程:用 pandas 完成 51.5 万条欧洲酒店评论的探索性数据分析(NLP 情感分析的数据准备阶段)
导读
本文是微软开源机器学习入门课程 ML-For-Beginners 之 NLP(自然语言处理)章节中的第 4 课解析,围绕一个真实的业务挑战展开:构建酒店推荐机器人,用评论情感分析与住客评分给出推荐。课程给出的实战任务不是直接训练模型,而是先用 pandas 对一个包含 51.5 万条欧洲酒店评论、覆盖 6 座城市 1493 家酒店的巨型 CSV 做探索性数据分析(EDA),摸清 17 列字段的真实含义与数据陷阱,并学会“删除无用列、基于现有列派生新列、为下一课保留清洗结果”这一完整数据准备链路。学完本文,你将掌握在大体量文本数据上核对列取值、识别评分口径矛盾、用 groupby/apply/value_counts 高效取证的方法,为后续真正的 NLTK 情感分析打好地基。
本课对应仓库文档为 6-NLP/4-Hotel-Reviews-1/README.md(本节原文),本文章基于该文档展开并补充源码佐证。课程完整上下文见 6-NLP/README.md 与全局课程主页 README.md。
一、课程定位与业务场景
在 6-NLP/README.md 列出的五节课序列中,本课处于“翻译与情感分析基础(第 3 课)”与“用 NLTK 做情感分析(第 5 课)”之间,角色是承上启下的数据准备课:
| 课程 | 主题 | 与本文关系 |
|---|---|---|
| 1-Introduction-to-NLP | NLP 入门 | 前置知识 |
| 2-Tasks | 常见 NLP 任务与技术 | 前置知识 |
| 3-Translation-Sentiment | 翻译与情感分析 | 前置知识 |
| 4-Hotel-Reviews-1(本课) | 评论数据 EDA 与预处理 | 本文主题 |
| 5-Hotel-Reviews-2 | 用 NLTK 做酒店评论情感分析 | 下一课,消费本课产出 |
本课的核心挑战是:用 Python + 评论数据 + NLTK 情感分析回答三个业务问题——
- 评论中最常出现的词与短语是什么?
- 官方
Tags(描述住客类型/房型/入住时长的标签)是否与评分相关?例如某家酒店来自“带幼儿的家庭(Family with young children)”的差评是否显著多于“独自出行者(Solo traveller)”,从而暗示该酒店更适合后者? - NLTK 计算出的情感分数是否与住客给出的数值评分“互相印证”?
而这一切的前提,是先把这个大而杂的 CSV 彻底读懂、验算与收拾干净。
二、实验环境与数据准备
文档明确列出的软硬件准备清单如下:
- 能运行
.ipynb记事本的 Python 3 环境; - pandas(本课所有 dataframe 操作的基础库);
- NLTK(需本地安装,供后续课程做情感分析);
- 数据集 515K Hotel Reviews Data in Europe(Kaggle 平台公开数据集,采用 CC0 公共领域许可,数据由创建者 Jiashen Liu 从 Booking.com 公开来源抓取,本仓库不内嵌该数据文件)。解压后体积约 230 MB。
在仓库中,数据应放置于 NLP 课程数据目录 6-NLP/data,该目录下 README.md 明确写着 “Download the hotel review data to this folder.”,即把下载的 CSV 放入该文件夹。工作记事本为 notebook.ipynb(供读者边学边写代码),课程同时提供了一份带完整实现与输出的参考答案 solution/notebook.ipynb,另有 R、Julia 两种语言的对照解法,分别位于 solution/R/README.md 与 solution/Julia/README.md。
路径提示:文档内代码写作
pd.read_csv('../../data/Hotel_Reviews.csv'),该相对路径随运行环境而定,请按你把 CSV 实际存放的位置(建议6-NLP/data/下)调整,确保文件能被读入。
三、读懂 17 列:把评论数据拆成四个逻辑组
在写任何代码之前,先用编辑器(VS Code 甚至 Excel)打开 CSV 观察列名。文档给出一行完整的表头:
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
为便于分析,文档把这 17 列分成四组,其语义与“坑点”如下。
1. 酒店维度列(Hotel columns)
Hotel_Name、Hotel_Address、lat(纬度)、lng(经度):利用经纬度可用 Python 把 1493 家酒店画在地图上,甚至按差评/好评着色,得到直观的空间分布视图。Hotel_Address:对推荐模型没有直接用处,文档建议后续用更便于排序检索的“国家/城市”字段替换它(这一替换动作正是下一课 5-Hotel-Reviews-2 的实操内容)。
2. 酒店聚合评分列(Hotel Meta-review columns)
Average_Score:按数据创建者说明,代表“酒店基于最近一年内最新评论计算的平均分”——这个口径很不寻常,文档提醒读者“先按表面值接受”。随堂提问:能否利用数据中其他列自行推导出另一种平均分算法?(这正是第五节练习 5 的由来。)Total_Number_of_Reviews:酒店收到的评论总数;不写代码无法确认它是否等于数据集中该酒店的评论条数。Additional_Number_of_Scoring:仅打分、但未写正面或负面文字评论的“纯评分”条数。
3. 单条评论维度列(Review columns)
Reviewer_Score:数值评分,最多保留 1 位小数,取值范围 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:可据此构建“新鲜度/陈旧度”度量——酒店管理层更替、翻修、新建泳池等都会让老评论逐渐失真,是后续做时间衰减加权时的潜在特征。
4. 评论者维度列(Reviewer columns)
Total_Number_of_Reviews_Reviewer_Has_Given:该评论者累计贡献的评论数。理论上高产评论者(写了上百条)可能更倾向给差评,可作为推荐模型因子;但数据中没有唯一评论者 ID,无法把同一个人多条评论关联起来。文档提到数据集中有 30 位评论数达到 100+ 的评论者,但很难看出这能如何支撑推荐模型。Reviewer_Nationality:评论者国籍。文档给出重要的伦理提醒:不要臆测“某国人不爱给好评”之类的刻板印象并将其写进模型——每位评论者都是基于自身体验(过往入住经历、旅途远近、个人性情等多重滤镜)写作的个体,把国籍当作评分原因难以自圆其说。
5. 一个具象样本:看“糟糕入住”如何被记录
文档给出了一行真实样例数据,便于理解四组列如何共同描述一次体验:
| Average_Score | Total_Number_of_Reviews | Reviewer_Score | Negative_Review(节选) | Positive_Review | Tags |
|---|---|---|---|---|---|
| 7.8 | 1945 | 2.5 | “这根本不是酒店而是工地……全天被施工噪音折磨,要求换房无果还被多收费……太可怕了,别订这里惩罚自己” | 无(只写了警告性短句“可怕的地方,请远离”) | 商务出行、情侣、标准双人房、入住 2 晚 |
这行数据至少揭示了三个要点:其一,酒店均分 7.8、评论数 1945,看似体面,但这位住客只给了 2.5 分并写下 115 个词吐槽;其二,Positive_Review 虽写了 7 个词,却是彻头彻尾的“警告”,说明若只数词数而不看词义/情感,会严重误判评论者意图——这正是文档反复强调“语义 > 计数”的原因;其三,住客打 2.5 分这件事本身很蹊跷:既然体验如此之差,为何还给分?仔细排查数据后会发现最低分不是 0,而是 2.5,最高分是 10。
四、数据里藏着的三个“坑”
文档花了大量篇幅告诫读者:这份数据并非开箱即信。以下是三个必须在建模前想清楚的陷阱。
坑 1:Tags 列非标准化,难以直接分类
表面上看,Tags 很适合做酒店类型标签化,但文档指出它们不标准化:A 酒店的选项可能是 “Single room / Twin room / Double room”,B 酒店却变成 “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 kids等关键词在每个酒店的出现频次,再融入推荐逻辑——这是文档认可的更可行路径。
更糟的是格式本身不稳定:Tags 通常是一条由 5~6 个逗号分隔值组成的字段,对应着 出行类型、住客类型、房型、入住夜数、提交评论的设备类型,但因为有人留空,值的顺序并不固定。以“住客群体类型”为例,Tags 列存在 1025 种唯一取值且只有部分是群体描述;若仅按含 "family" 过滤会混入大量 “Family room” 房型噪声,改为统计含 "Family with" 的短语后结果才可用——51.5 万条里约 8 万余条命中 “Family with young children” 或 “Family with older children”。结论是:Tags 并非全无用,但需要额外加工才能变废为宝。
坑 2:平均分与评论数存在无法解释的口径出入
围绕评分与评论数,数据集暴露出一组无法自洽的数字(以样本内评论最多的 Britannia International Hotel Canary Wharf 为例):
- 该酒店在数据集中出现 4789 条评论(数据集共 51.5 万条),但其
Total_Number_of_Reviews却写着 9086; - 试着把
Additional_Number_of_Scoring(2682)加进去,得到 7471,仍比 9086 少 1615; - 数据集给该酒店的
Average_Score为 7.1,而用数据集内评论者分数自算的均值是 6.8,二者不一致,只能猜测是那些不在数据集中的纯评分(Additional_Number_of_Scoring对应记录)把均值抬到了 7.1; - 令人困惑的是:评论量第二的酒店,其自算均值 8.12 与数据集的 8.1 几乎一致——到底哪个酒店才是异常值?
文档给出的结论务实而关键:由于无法验证这些列引用的外部数据,建模时不要信任 Average_Score、Additional_Number_of_Scoring、Total_Number_of_Reviews 三列,应基于手头数据自行计算均值。这也直接引出了第五节练习中的“自算平均分”。从源码侧印证,参考答案 solution/notebook.ipynb 也正是围绕这些列做校验与自算。
坑 3(重要提醒):负面评论里存在令人不适的内容
文档在此处专门给出 🚨 提示:本课所有代码的目标是让程序从文本中提取含义与情感,而不需要你逐条去读文本——这正是 NLP 的精髓。但若你好奇去翻差评原文,会看到诸如“天气不太好”(酒店无法控制的事),也会看到少数种族歧视、性别歧视、年龄歧视等阴暗内容。既然这些内容源自公开网站抓取,它们客观存在,因此文档强烈建议:把测情感的工作交给代码,而不是自己逐条阅读以免被冒犯。这一提醒对任何真实世界 NLP 项目都有普适价值。
五、动手:加载 51.5 万行数据并计时
视觉检查到此为止,接下来正式写代码。首先引入 pandas 与 time(后者用于统计文件加载耗时):
# 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 列,pandas 的快速 CSV 加载器会把结果放进 DataFrame。加载后先别急着冲向 Negative_Review / Positive_Review 这两列自然语言文本——文档特意提醒:在下结论之前,先用代码核对数据集自带的取值是否与你用 pandas 算出来的一致。这正是一个数据科学家应有的职业习惯。
值得一提的是,本例数据已经相当“干净”:不含会干扰纯英文 NLP 算法的其他语言字符。文档借机抛出思考题:如果换一批需要清洗的语料,你会怎样处理非英文字符(编码归一、去重、繁简转换、emoji 过滤等)?——在真实项目中这些往往都是绕不开的前置步骤。
六、九道 EDA 编程练习题与逐题解答
文档把下列问题当作“不允许偷看答案”的编程作业。所有练习都遵循一个总原则:能不改动 dataframe 就不改动,尽量用最直白、未来回读最容易理解的写法。以下是问题与参考答案的完整梳理。
练习 1:打印 dataframe 的 shape
shape 即(行数, 列数)。
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() 会返回一个 Series——索引用的是国家名、值是该国籍出现的频次。
# 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 first and last rows of the Series. Change to nationality_freq.to_string() to print all of the data
print(nationality_freq)
# 输出(节选)
There are 227 different nationalities
United Kingdom 245246
United States of America 35437
Australia 21686
Ireland 14827
United Arab Emirates 10235
...
Comoros 1
Palau 1
Northern Mariana Islands 1
Cape Verde 1
Guinea 1
Name: Reviewer_Nationality, Length: 227, dtype: int64
随后找出最高频国籍与 Top 10 榜单:
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())
# 输出
The highest frequency reviewer nationality is United Kingdom with 245246 reviews.
The next 10 highest frequency reviewer nationalities are:
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
实现细节提示:原始取值前带前导空格,打印前用 .strip() 去掉可读性更好。
练习 3:Top 10 国籍各自最常评论的酒店
# What was the most frequently reviewed hotel for the top 10 nationalities
# Normally with pandas you will avoid an explicit loop, but wanted to show
# creating a new dataframe using criteria (don't 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.")
# 输出(节选)
The most reviewed hotel for United Kingdom was Britannia International Hotel Canary Wharf with 3833 reviews.
The most reviewed hotel for United States of America was Hotel Esther a with 423 reviews.
The most reviewed hotel for Australia was Park Plaza Westminster Bridge London with 167 reviews.
The most reviewed hotel for Ireland was Copthorne Tara Hotel London Kensington with 239 reviews.
The most reviewed hotel for United Arab Emirates was Millennium Hotel London Knightsbridge with 129 reviews.
The most reviewed hotel for Saudi Arabia was The Cumberland A Guoman Hotel with 142 reviews.
The most reviewed hotel for Netherlands was Jaz Amsterdam with 97 reviews.
The most reviewed hotel for Switzerland was Hotel Da Vinci with 97 reviews.
The most reviewed hotel for Germany was Hotel Da Vinci with 86 reviews.
The most reviewed hotel for Canada was St James Court A Taj Hotel London with 61 reviews.
文档特别注明:pandas 惯例是尽量避免显式 for 循环,但这里刻意展示“用筛选条件不断构造新 dataframe”的过程;在数据量很大时应慎用此写法以免过慢。
练习 4:每个酒店在数据集中的评论条数(频次计数)
思路:先基于原表 drop 掉与本问无关的列,再按 Hotel_Name 分组计数,写入新列 Total_Reviews_Found,最后按酒店去重。
# First create a new dataframe based on the old one, removing the uneeded 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)
输出节选如下,右侧 Total_Reviews_Found(数据集内实测)与 Total_Number_of_Reviews(表内声明)存在明显出入:
| Hotel_Name | Total_Number_of_Reviews | Total_Reviews_Found |
|---|---|---|
| Britannia International Hotel Canary Wharf | 9086 | 4789 |
| Park Plaza Westminster Bridge London | 12158 | 4169 |
| Copthorne Tara Hotel London Kensington | 7105 | 3578 |
| … | … | … |
| Mercure Paris Porte d Orleans | 110 | 10 |
| Hotel Wagner | 135 | 10 |
| Hotel Gallitzinberg | 173 | 8 |
重要建模决策:正是因为无法判断 Total_Number_of_Reviews 到底代表“该酒店全部评论但未被抓全”还是别的口径,后续建模时弃用该列。
练习 5:自算每家酒店的平均分并对比
尽管表里有 Average_Score,我们仍可以用 groupby(...).transform('mean') 基于数据集内全部 Reviewer_Score 自算均值,新增列 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"]])
这里同时示范了两种 pandas 高阶手法:transform('mean') 把分组均值广播回每一行,供新列使用;.apply(函数, axis=1) 让函数逐行处理 Series。偏差最大的酒店如下:
| Average_Score_Difference | Average_Score | Calc_Average_Score | Hotel_Name |
|---|---|---|---|
| -0.8 | 7.7 | 8.5 | Best Western Hotel Astoria |
| -0.7 | 8.8 | 9.5 | Hotel Stendhal Place Vend me Paris MGallery |
| -0.7 | 7.5 | 8.2 | Mercure Paris Porte d Orleans |
| -0.7 | 7.9 | 8.6 | Renaissance Paris Vendome Hotel |
| -0.5 | 7.0 | 7.5 | Hotel Royal Elys es |
| … | … | … | … |
| 0.7 | 7.5 | 6.8 | Mercure Paris Op ra Faubourg Montmartre |
| 0.8 | 7.1 | 6.3 | Holiday Inn Paris Montparnasse Pasteur |
| 0.9 | 6.8 | 5.9 | Villa Eugenie |
| 0.9 | 8.6 | 7.7 | MARQUIS Faubourg St Honor Relais Ch teaux |
| 1.3 | 7.2 | 5.9 | Kube Hotel Ice Bar |
结论:全部酒店中仅有 1 家偏差超过 1 分,绝大多数差异极小——因此完全可以忽略原表 Average_Score,直接采用自算的 Calc_Average_Score,口径可控、可复现。
练习 6~8:统计 “No Negative” / “No Positive” / 双空的记录数
问题 6、7、8 分别统计:负面栏为空串 "No Negative" 的行数、正面栏为空串 "No Positive" 的行数,以及两栏同时为空的行数。文档给出两种实现。
方式 A:lambda + .apply()
# with lambdas:
start = time.time()
no_negative_reviews = df.apply(lambda x: True if x['Negative_Review'] == "No Negative" else False , axis=1)
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 , axis=1)
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 , axis=1)
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
方式 B:sum + 布尔 Series(不使用 lambda)
# 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
结果的业务解读:两栏同时为空的记录有 127 条——住客给了数值分却拒绝写任何文字评论。好在它仅占 51.5 万条的 0.02%,基本不会把模型带偏。但这件事本身就值得记录:一个“评论数据集”里竟然存在“有分无评”的行,正说明动手探索远比想当然可靠。
值得一提的性能细节:两种写法结果完全一致,而耗时差异巨大——apply + lambda 约 9.64 秒,sum 布尔计数约 0.19 秒(约快 50 倍)。原因在于 lambda 需要逐 Python 行回调、产生海量中间布尔 Series;而 sum(布尔Series) 走的是向量化比较与 C 层求和。对 51.5 万行规模的数据,这个性能差距非常直观,也是文档安排“Another way”小节的用意。
七、本课沉淀的建模决策清单
综合全部探索,可以对 17 列做出如下取舍决策(也是下一课数据清洗的输入):
- 直接可用:
Reviewer_Score、Negative_Review、Positive_Review、Review_Total_Negative_Word_Counts、Review_Total_Positive_Word_Counts、Review_Date/days_since_review、Hotel_Name、Reviewer_Nationality(注意种族/国籍偏见伦理问题)、lat/lng。 - 需要加工后用:
Tags(非标准化、顺序不固定,走关键词频次路线);Hotel_Address(应替换为国家/城市);Average_Score(与自算均值基本一致,可换用自算口径)。 - 不建议直接信任:
Total_Number_of_Reviews、Additional_Number_of_Scoring(口径指向数据集外数据,无法验证)。 - 警惕占位符与语义反转:处理
Negative_Review/Positive_Review时须先处理 "No Negative"/"No Positive" 占位,并意识到评论者可能“正话反写/反话正写”——纯词数统计会失真,这正是下一课引入 NLTK 情感分析的根本动机。
下一课 5-Hotel-Reviews-2 将正式执行:丢弃 lat/lng、把 Hotel_Address 映射为 城市, 国家(Amsterdam/Barcelona/London/Milan/Paris/Vienna 六城),再叠加情感分析运算,从而把“数据已经探明”转化为“模型可以消费的特征”。
八、课后自测与延伸学习
文档在结尾安排了三个学习动作:
- 🚀 挑战:本课最重要的方法论是——操作数据之前先理解数据及其怪癖,基于文本的数据尤其要仔细审视。建议主动寻找更多文本密集型数据集,训练自己发现可能给模型引入偏见或扭曲情感的字段。
- 课后巩固:本课配套的作业 assignment.md 要求系统研读 NLTK 官方书籍并动手做练习,以此深入掌握这个计算语言学与 NLP 的经典工具库,为下一课直接用 NLTK 做情感分析做好铺垫。
- 对照实现:除 Python 外,仓库还为每个练习提供了多语言实现参考,可从 solution/notebook.ipynb(Python 完整可运行输出)与 solution/R/README.md、solution/Julia/README.md 入手对比不同技术栈下同一套 EDA 逻辑的写法。
小结
围绕“酒店推荐机器人”这一实战目标,本课证明了一条普适的数据科学工作流:先用手与眼理解数据,再用 pandas 一行行地证伪直觉,最后才放心地把数据交给模型。515K 欧洲酒店评论教会我们的不仅是 value_counts()、groupby().transform()、.apply() 这些 API,更是对 Tags 非标准化、Average_Score 口径存疑、“No Positive/No Negative” 占位符等真实脏数据的高度警惕。带着这份“已校验、可自算、口径自洽”的清洗结果进入下一课,NLTK 情感分析才能真正建立在坚实的数据地基之上。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00