在本节中,您将使用前面课程中的技术对大型数据集进行一些探索性数据分析。一旦您对各个列的用处有了很好的了解,您将了解到:
到目前为止,您已经了解了文本数据与数字类型数据的不同之处。如果它是由人类书写或说出的文本,则可以对其进行分析以找到模式和频率、情感和意义。本课将带您进入一个具有真正挑战的真实数据集:欧洲的 515K 酒店评论数据,包括CC0:公共领域许可。它是从 Booking.com 的公共资源中抓取的。数据集的创建者是刘家申。
你会需要:
/data
将其下载到与这些 NLP 课程相关的根文件夹中。此挑战假设您正在使用情绪分析和客人评论分数构建酒店推荐机器人。您将使用的数据集包括 6 个城市的 1493 家不同酒店的评论。
使用 Python、酒店评论数据集和 NLTK 的情绪分析,您可以发现:
数据集
让我们探索一下您已下载并保存在本地的数据集。在 VS Code 甚至 Excel 等编辑器中打开文件。
数据集中的标头如下:
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、标签、days_since_review、lat、lng
在这里,它们以一种更容易检查的方式分组:
酒店专栏
Hotel_Name
, Hotel_Address
, lat
(纬度), lng
(经度)
酒店元评论专栏
Average_Score
✅根据这个数据中的其他列,你能想出另一种计算平均分的方法吗?
Total_Number_of_Reviews
Additional_Number_of_Scoring
评论栏目
Reviewer_Score
Negative_Review
Review_Total_Negative_Word_Counts
Positive_Review
Review_Total_Positive_Word_Counts
Review_Date
和days_since_review
Tags
审阅者栏
Total_Number_of_Reviews_Reviewer_Has_Given
Reviewer_Nationality
例子
平均分 | 评论总数 | 审稿人分数 | 负面 评论 |
正面评价 | 标签 |
---|---|---|---|---|---|
7.8 | 1945 | 2.5 | 目前这不是一家酒店,而是一个建筑工地,我从一大早开始就被吓坏了,一整天的建筑噪音让我在长途旅行后休息并在房间里工作,人们整天都在工作,即在相邻的房间里拿着手提钻我要求一个换了房间,但没有安静的房间 更糟糕的是,我被多收了我晚上退房,因为我不得不离开很早的航班并收到了适当的账单 一天后,酒店在未经我同意的情况下再次收取超过预订价格的费用这是一个可怕的地方不要在这里预订来惩罚自己 | 没有什么可怕的地方远离 | 出差情侣标准双人房住了2晚 |
如您所见,这位客人在这家酒店住得并不愉快。这家酒店的平均得分为 7.8 和 1945 条评论,但这位评论家给了它 2.5 分,并写了 115 个字来说明他们的住宿有多消极。如果他们在 Positive_Review 列中什么也没写,你可能会猜测没有任何正面的东西,但可惜他们写了 7 个警告字。如果我们只计算单词而不是单词的含义或情绪,我们可能会对审稿人的意图产生偏见。奇怪的是,他们的 2.5 分令人困惑,因为如果那次酒店住宿如此糟糕,为什么还要给它任何分数呢?仔细研究数据集,您会发现可能的最低分数是 2.5,而不是 0。可能的最高分数是 10。
标签
如上所述,乍一看,用于Tags
对数据进行分类的想法是有道理的。不幸的是,这些标签不是标准化的,这意味着在给定的酒店中,选项可能是Single room、Twin room和Double room,但在下一家酒店,它们是Deluxe Single Room、Classic Queen Room和Executive King Room。这些可能是相同的东西,但有很多变化,选择变成:
尝试将所有条款更改为单一标准,这非常困难,因为不清楚每种情况下的转换路径是什么(例如,经典单人房映射到单人房,但高级大床房有庭院花园或城市景观很多更难映射)
我们可以采用 NLP 方法并测量某些术语(例如Solo、Business Traveler或Family with young kids)应用于每家酒店时的频率,并将其纳入推荐中
标签通常(但不总是)是一个包含 5 到 6 个逗号分隔值列表的字段,这些值与提交的旅行类型、客人类型、房间类型、晚数和设备评论类型对齐。但是,由于一些审阅者不会填写每个字段(他们可能会留一个空白),因此这些值的顺序并不总是相同。
以 Group 类型为例。列中的该字段有 1025 个独特的可能性Tags
,不幸的是,其中只有一些是指一个组(一些是房间的类型等)。如果您只过滤提到家庭的那些,结果会包含许多家庭房间类型的结果。如果将术语与 一起包括在内,即用值计算家庭,结果会更好,515,000 个结果中有超过 80,000 个包含短语“有小孩的家庭”或“有大孩子的家庭”。
这意味着标签列对我们来说并非完全无用,但需要一些工作才能使其有用。
酒店平均分
数据集有一些我无法弄清楚的奇怪或差异,但在此处进行了说明,以便您在构建模型时了解它们。如果你弄明白了,请在讨论区告诉我们!
该数据集具有与平均分数和评论数量相关的以下列:
该数据集中评论最多的单一酒店是不列颠尼亚国际酒店金丝雀码头,共有 515,000 条评论,其中有 4789 条评论。但是如果我们看一下Total_Number_of_Reviews
这家酒店的值,它是 9086。您可能会猜测没有评论的分数要多得多,所以也许我们应该在Additional_Number_of_Scoring
列中添加值。该值为 2682,将其添加到 4789 得到 7,471,但仍比Total_Number_of_Reviews
.
如果你拿这些Average_Score
列,你可能会猜测它是数据集中评论的平均值,但 Kaggle 的描述是“酒店的平均分数,根据去年的最新评论计算得出”。这似乎没什么用,但我们可以根据数据集中的评论分数来计算我们自己的平均值。以同一家酒店为例,酒店平均得分为 7.1,但计算出的得分(数据集中的平均评论者得分)为 6.8。这很接近,但不是相同的值,我们只能猜测Additional_Number_of_Scoring
评论中给出的分数将平均值提高到 7.1。不幸的是,由于无法测试或证明该断言,因此难以使用或信任Average_Score
,Additional_Number_of_Scoring
并且Total_Number_of_Reviews
当它们基于或参考我们没有的数据时。
更复杂的是,评论数量第二高的酒店的计算平均得分为 8.12,数据集Average_Score
为 8.1。这个正确的分数是巧合还是第一家酒店的差异?
关于这些酒店可能是异常值的可能性,并且可能大多数值都符合(但有些不是出于某种原因),我们接下来将编写一个简短的程序来探索数据集中的值并确定正确的用法(或不使用)的值。
注意事项
使用此数据集时,您将编写从文本计算某些内容的代码,而无需自己阅读或分析文本。这就是 NLP 的精髓,无需人工就能解释意义或情感。但是,您可能会阅读一些负面评论。我劝你不要这样做,因为你不必这样做。其中一些是愚蠢的或无关紧要的负面酒店评论,例如“天气不好”,这是酒店或任何人无法控制的。但有些评论也有阴暗面。有时负面评论是种族主义、性别歧视或年龄歧视。这是不幸的,但在从公共网站上抓取的数据集中是可以预料的。一些评论者留下的评论会让您觉得反感、不舒服或令人不安。最好让代码衡量情绪,而不是自己阅读它们并感到沮丧。也就是说,写这样的东西是少数,但它们都是一样的。
直观地检查数据就足够了,现在您将编写一些代码并获得一些答案!本节使用 pandas 库。您的首要任务是确保您可以加载和读取 CSV 数据。pandas 库有一个快速的 CSV 加载器,并且结果被放置在一个数据框中,就像之前的课程一样。我们正在加载的 CSV 有超过 50 万行,但只有 17 列。Pandas 为您提供了许多与数据框交互的强大方法,包括对每一行执行操作的能力。
从本课开始,将有代码片段和一些代码解释,以及一些关于结果含义的讨论。将包含的notebook.ipynb用于您的代码。
让我们从加载您正在使用的数据文件开始:
# 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")
现在数据已经加载完毕,我们可以对其进行一些操作。将此代码保留在程序的顶部以供下一部分使用。
在这种情况下,数据已经是干净的,这意味着它已经准备好使用,并且没有其他语言中的字符,这些字符可能会使只需要英文字符的算法出错。
✅在应用 NLP 技术之前,您可能必须处理需要一些初始处理才能格式化的数据,但这次不是。如果必须,您将如何处理非英文字符?
花点时间确保加载数据后,您可以使用代码对其进行探索。想要专注于Negative_Review
和Positive_Review
列是很容易的。它们充满了自然文本,供您的 NLP 算法处理。可是等等!在你进入 NLP 和情绪之前,你应该按照下面的代码来确定数据集中给出的值是否与你用 pandas 计算的值相匹配。
本课的第一个任务是通过编写一些检查数据帧的代码(不更改它)来检查以下断言是否正确。
像许多编程任务一样,有几种方法可以完成此任务,但好的建议是以最简单、最简单的方式完成,尤其是当您将来回到这段代码时更容易理解时。使用数据帧,有一个全面的 API,通常可以有效地做你想做的事。
将以下问题视为编码任务,并尝试在不查看解决方案的情况下回答它们。
Reviewer_Nationality
它们是什么?Average_Score
数据集中的每家酒店都有一列,但您还可以计算平均分数(获取数据集中每家酒店的所有评论者分数的平均值)。Calc_Average_Score
使用包含计算的平均值的列标题向您的数据框添加一个新列。Average_Score
和Calc_Average_Score
?
.apply()
方法使用该函数处理每一行。Negative_Review
值为“No Negative”Positive_Review
值为“No Positive”Positive_Review
“No Positive”的列值和 Negative_Review
“No Negative”的值打印出刚刚加载的数据框的形状(形状是行数和列数)
print("The shape of the data (rows, cols) is " + str(df.shape))
> The shape of the data (rows, cols) is (515738, 17)
计算审稿人国籍的频率计数:
Reviewer_Nationality
它们是什么?# 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
下一个最常见的 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
# What is the top 10 most common nationalities and their frequencies?
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
在评论最多的 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.
数据集中每家酒店(酒店的频率计数)有多少条评论?
# 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_Number_of_Reviews | Total_Reviews_Found |
---|---|---|
金丝雀码头大不列颠国际酒店 | 9086 | 4789 |
公园广场威斯敏斯特桥伦敦 | 12158 | 4169 |
伦敦肯辛顿国敦塔拉酒店 | 7105 | 3578 |
... | ... | ... |
巴黎奥尔良门美居酒店 | 110 | 10 |
瓦格纳酒店 | 135 | 10 |
加利津伯格酒店 | 173 | 8 |
您可能会注意到数据集中的计数结果与 中的值不匹配Total_Number_of_Reviews
。目前尚不清楚数据集中的这个值是否代表酒店的评论总数,但并非所有评论都被抓取,或者其他一些计算。Total_Number_of_Reviews
由于这种不明确性,未在模型中使用。
虽然Average_Score
数据集中的每家酒店都有一列,但您还可以计算平均分数(获取数据集中每家酒店的所有评论者分数的平均值)。Calc_Average_Score
使用包含计算的平均值的列标题向您的数据框添加一个新列。打印出Hotel_Name
、Average_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"]])
您可能还想知道该Average_Score
值以及为什么它有时与计算的平均分数不同。由于我们不知道为什么某些值匹配,但其他值存在差异,因此在这种情况下使用我们必须自己计算平均值的评论分数是最安全的。也就是说,差异通常很小,以下是与数据集平均值和计算平均值偏差最大的酒店:
Average_Score_Difference | 平均分 | Calc_Average_Score | 酒店名称 |
---|---|---|---|
-0.8 | 7.7 | 8.5 | 阿斯托利亚贝斯特韦斯特酒店 |
-0.7 | 8.8 | 9.5 | 斯汤达广场 Vend me 巴黎美憬阁酒店 |
-0.7 | 7.5 | 8.2 | 巴黎奥尔良门美居酒店 |
-0.7 | 7.9 | 8.6 | 巴黎旺多姆万丽酒店 |
-0.5 | 7.0 | 7.5 | 皇家伊利斯酒店是 |
... | ... | ... | ... |
0.7 | 7.5 | 6.8 | Mercure Paris Op ra Faubourg Montmartre |
0.8 | 7.1 | 6.3 | 巴黎蒙帕纳斯巴斯德假日酒店 |
0.9 | 6.8 | 5.9 | 尤金妮别墅 |
0.9 | 8.6 | 7.7 | MARQUIS Faubourg St Honor Relais Châteaux |
1.3 | 7.2 | 5.9 | 库贝酒店冰吧 |
只有 1 家酒店的得分差异大于 1,这意味着我们可能可以忽略差异并使用计算出的平均得分。
计算并打印出有多少行的列Negative_Review
值为“No Negative”
计算并打印出有多少行的列Positive_Review
值为“No Positive”
计算并打印出有多少行具有Positive_Review
“No Positive”的列值和 Negative_Review
“No Negative”的值
# 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
另一种计算没有 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
您可能已经注意到,有 127 行的列Negative_Review
和Positive_Review
分别具有“No Negative”和“No Positive”值。这意味着评论者给了酒店一个数字分数,但拒绝写正面或负面评论。幸运的是,这是少量的行(515738 中的 127 行,或 0.02%),因此它可能不会使我们的模型或结果偏向任何特定方向,但您可能没想到评论数据集包含没有评论,因此值得探索数据以发现这样的行。
现在您已经探索了数据集,在下一课中,您将过滤数据并添加一些情绪分析。