机器学习(ML)|带有酒店评论的情绪分析 - 处理数据

在本节中,您将使用前面课程中的技术对大型数据集进行一些探索性数据分析。一旦您对各个列的用处有了很好的了解,您将了解到:

  • 如何删除不必要的列
  • 如何根据现有列计算一些新数据
  • 如何保存生成的数据集以用于最终挑战
课前测验 介绍
到目前为止,您已经了解了文本数据与数字类型数据的不同之处。如果它是由人类书写或??说出的文本,则可以对其进行分析以找到模式和频率、情感和意义。本课将带您进入一个具有真正挑战的真实数据集:欧洲的 515K 酒店评论数据,包括CC0:公共领域许可。它是从 Booking.com 的公共资源中抓取的。数据集的创建者是刘家申。
准备
你会需要:
  • 使用 Python 3 运行 .ipynb 笔记本的能力
  • 熊猫
  • NLTK,您应该在本地安装
  • 该数据集可在欧洲的 Kaggle 515K 酒店评论数据中找到。解压后大约有 230 MB。/data将其下载到与这些 NLP 课程相关的根文件夹中。
探索性数据分析 此挑战假设您正在使用情绪分析和客人评论分数构建酒店推荐机器人。您将使用的数据集包括 6 个城市的 1493 家不同酒店的评论。
使用 Python、酒店评论数据集和 NLTK 的情绪分析,您可以发现:
  • 评论中最常用的单词和短语是什么?
  • 描述酒店的官方标签是否与评论分数相关(例如,对于有小孩的家庭对特定酒店的负面评论是否 比单人旅行者更负面,也许表明它对单人旅行者更好?)
  • 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(经度)
    • 使用latlng,您可以使用 Python 绘制显示酒店位置的地图(可能对负面和正面评论进行颜色编码)
    • Hotel_Address 显然对我们没有用处,我们可能会将其替换为国家/地区以便于排序和搜索
酒店元评论专栏
  • Average_Score
    • 根据数据集创建者的说法,这一列是酒店的平均得分,根据去年的最新评论计算得出。这似乎是一种不寻常的计算分数的方法,但它是抓取的数据,所以我们现在可以把它当作面值。
    ?根据这个数据中的其他列,你能想出另一种计算平均分的方法吗?
  • Total_Number_of_Reviews
    • 这家酒店收到的评论总数 - 不清楚(不写一些代码)这是否指的是数据集中的评论。
  • Additional_Number_of_Scoring
    • 这意味着给出了评论分数,但评论者没有写正面或负面评论
评论栏目
  • Reviewer_Score
    • 这是一个数值,在最小值和最大值 2.5 和 10 之间最多有 1 个小数位
    • 没有解释为什么 2.5 是可能的最低分数
  • Negative_Review
    • 如果审稿人什么都没写,这个字段将有“ No Negative ”
    • 请注意,评论者可能会在负面评论栏中写正面评论(例如“这家酒店没有什么不好的”)
  • Review_Total_Negative_Word_Counts
    • 较高的负字数表示较低的分数(不检查多愁善感)
  • Positive_Review
    • 如果审稿人什么都没写,这个字段将有“ No Positive ”
    • 请注意,评论者可能会在正面评论栏中写负面评论(例如“这家酒店没有任何好处”)
  • Review_Total_Positive_Word_Counts
    • 较高的正字数表示较高的分数(不检查多愁善感)
  • Review_Datedays_since_review
    • 可能会对评论应用新鲜度或陈旧度衡量标准(旧评论可能不如新评论准确,因为酒店管理发生了变化,或者已经完成装修,或者添加了游泳池等)
  • Tags
    • 这些是评论者可以选择的简短描述符来描述他们的客人类型(例如单人或家庭)、他们拥有的房间类型、入住时间以及如何提交评论。
    • 不幸的是,使用这些标签是有问题的,请查看下面讨论它们的有用性的部分
审阅者栏
  • Total_Number_of_Reviews_Reviewer_Has_Given
    • 这可能是推荐模型中的一个因素,例如,如果您可以确定拥有数百条评论的多产评论者更有可能是负面的而不是正面的。但是,任何特定评论的评论者都没有唯一的代码标识,因此无法链接到一组评论。有 30 位评论者有 100 条或更多评论,但很难看出这对推荐模型有何帮助。
  • 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 roomTwin roomDouble room,但在下一家酒店,它们是Deluxe Single RoomClassic Queen RoomExecutive King Room。这些可能是相同的东西,但有很多变化,选择变成:
  1. 尝试将所有条款更改为单一标准,这非常困难,因为不清楚每种情况下的转换路径是什么(例如,经典单人房映射到单人房,但高级大床房有庭院花园或城市景观很多更难映射)
  2. 我们可以采用 NLP 方法并测量某些术语(例如SoloBusiness TravelerFamily with young kids)应用于每家酒店时的频率,并将其纳入推荐中
标签通常(但不总是)是一个包含 5 到 6 个逗号分隔值列表的字段,这些值与提交的旅行类型、客人类型、房间类型晚数设备评论类型对齐。但是,由于一些审阅者不会填写每个字段(他们可能会留一个空白),因此这些值的顺序并不总是相同。
以 Group 类型为例。列中的该字段有 1025 个独特的可能性Tags,不幸的是,其中只有一些是指一个组(一些是房间的类型等)。如果您只过滤提到家庭的那些,结果会包含许多家庭房间类型的结果。如果将术语与 一起包括在内,即用值计算家庭,结果会更好,515,000 个结果中有超过 80,000 个包含短语“有小孩的家庭”或“有大孩子的家庭”。
这意味着标签列对我们来说并非完全无用,但需要一些工作才能使其有用。
酒店平均分
数据集有一些我无法弄清楚的奇怪或差异,但在此处进行了说明,以便您在构建模型时了解它们。如果你弄明白了,请在讨论区告诉我们!
该数据集具有与平均分数和评论数量相关的以下列:
  1. 酒店名称
  2. Additional_Number_of_Scoring
  3. 平均分
  4. Total_Number_of_Reviews
  5. Reviewer_Score
该数据集中评论最多的单一酒店是不列颠尼亚国际酒店金丝雀码头,共有 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_ScoreAdditional_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_ReviewPositive_Review列是很容易的。它们充满了自然文本,供您的 NLP 算法处理。可是等等!在你进入 NLP 和情绪之前,你应该按照下面的代码来确定数据集中给出的值是否与你用 pandas 计算的值相匹配。
数据框操作 本课的第一个任务是通过编写一些检查数据帧的代码(不更改它)来检查以下断言是否正确。
像许多编程任务一样,有几种方法可以完成此任务,但好的建议是以最简单、最简单的方式完成,尤其是当您将来回到这段代码时更容易理解时。使用数据帧,有一个全面的 API,通常可以有效地做你想做的事。
将以下问题视为编码任务,并尝试在不查看解决方案的情况下回答它们。
  1. 打印出刚刚加载的数据框的形状(形状是行数和列数)
  2. 计算审稿人国籍的频率计数:
    1. 该列有多少不同的值,Reviewer_Nationality它们是什么?
    2. 数据集中最常见的审稿人国籍是什么(印刷国家和审稿数量)?
    3. 下一个最常见的 10 个国籍是什么,以及他们的频率计数?
  3. 在评论最多的 10 个国家/地区中,哪家酒店的评论最多?
  4. 数据集中每家酒店(酒店的频率计数)有多少条评论?
  5. 虽然Average_Score数据集中的每家酒店都有一列,但您还可以计算平均分数(获取数据集中每家酒店的所有评论者分数的平均值)。Calc_Average_Score使用包含计算的平均值的列标题向您的数据框添加一个新列。
  6. 是否有任何酒店具有相同的(四舍五入到小数点后一位)Average_ScoreCalc_Average_Score
    1. 尝试编写一个 Python 函数,将 Series(行)作为参数并比较值,当值不相等时打印出一条消息。然后使用该.apply()方法使用该函数处理每一行。
  7. 计算并打印出有多少行的列Negative_Review值为“No Negative”
  8. 计算并打印出有多少行的列Positive_Review值为“No Positive”
  9. 计算并打印出有多少行具有Positive_Review“No Positive”的列值和 Negative_Review“No Negative”的值
代码答案
  1. 打印出刚刚加载的数据框的形状(形状是行数和列数)
    print("The shape of the data (rows, cols) is " + str(df.shape)) > The shape of the data (rows, cols) is (515738, 17)


  2. 计算审稿人国籍的频率计数:
    1. 该列有多少不同的值,Reviewer_Nationality它们是什么?
    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 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 Kingdom245246 United States of America35437 Australia21686 Ireland14827 United Arab Emirates10235 ... Comoros1 Palau1 Northern Mariana Islands1 Cape Verde1 Guinea1 Name: Reviewer_Nationality, Length: 227, dtype: int64

    1. 下一个最常见的 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 America35437 Australia21686 Ireland14827 United Arab Emirates10235 Saudi Arabia8951 Netherlands8772 Switzerland8678 Germany7941 Canada7894 France7296

  3. 在评论最多的 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.

  4. 数据集中每家酒店(酒店的频率计数)有多少条评论?
    # 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由于这种不明确性,未在模型中使用。
  5. 虽然Average_Score数据集中的每家酒店都有一列,但您还可以计算平均分数(获取数据集中每家酒店的所有评论者分数的平均值)。Calc_Average_Score使用包含计算的平均值的列标题向您的数据框添加一个新列。打印出Hotel_NameAverage_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 Chateaux
    1.3 7.2 5.9 库贝酒店冰吧
    只有 1 家酒店的得分差异大于 1,这意味着我们可能可以忽略差异并使用计算出的平均得分。
  6. 计算并打印出有多少行的列Negative_Review值为“No Negative”
  7. 计算并打印出有多少行的列Positive_Review值为“No Positive”
  8. 计算并打印出有多少行具有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_ReviewPositive_Review分别具有“No Negative”和“No Positive”值。这意味着评论者给了酒店一个数字分数,但拒绝写正面或负面评论。幸运的是,这是少量的行(515738 中的 127 行,或 0.02%),因此它可能不会使我们的模型或结果偏向任何特定方向,但您可能没想到评论数据集包含没有评论,因此值得探索数据以发现这样的行。
【机器学习(ML)|带有酒店评论的情绪分析 - 处理数据】现在您已经探索了数据集,在下一课中,您将过滤数据并添加一些情绪分析。

    推荐阅读