第 9 章:在 Reddit 数据中寻找趋势

社交媒体数据的一部分以定量列举人类行为的方式构建,而其他部分本质上是非常定性的。例如,我们可以通过计算其赞成票来衡量 Reddit 帖子的受欢迎程度。这使我们可以进行简单的汇总,例如特定时间段内帖子收到的平均或中位数点赞数。然而,相同 Reddit 数据的其他部分可能更难以以定量方式进行总结。例如,评论包含的散文在内容和风格上可能大不相同。

总结人们在谈论什么,以及他们如何谈论它,比计算投票等参与度指标的平均值要困难得多,但对来自社交网络的数据进行有意义的分析需要我们浏览这两种信息。不过,学习如何做到这一点是非常有益的,因为它让我们可以探索真实人(以及偶尔的、也许越来越突出的机器人)的行为、想法和反应。

在本章中,您将学习如何浏览定性和定量信息。我们将通过分析第 8 章中的 r/askscience 数据并从我们的数据集中提出有针对性的问题,来探索人们如何参与社交媒体中的疫苗接种主题。首先,我们将通过在此 subreddit 中搜索包含词干疫苗(如疫苗接种和疫苗接种等词)的提交来尝试更好地了解如何处理基于文本的信息。然后,我们将比较疫苗帖子与非疫苗帖子的参与度指标——评论和投票的总和。

阐明我们的研究目标

在本章中,我们将使用来自 r/askscience subreddit 的在线对话,这是 Reddit 用户提出和回答与科学相关的问题的热门论坛,以衡量网络上对疫苗接种的讨论程度。

尽管 Reddit 用户并不代表整个美国人口,但我们可以通过查看该主题与平台上其他主题的相关性来尝试了解该主题在该特定论坛上的争议程度。与社交网络的任何其他检查一样,这里的关键是承认和理解我们检查的每个数据集的特殊性。

我们将首先问一个非常基本的问题:r/askscience Reddit 提交的包含“疫苗接种”、“疫苗”或“疫苗接种”等词的变体是否比 r/askscience 在 Reddit 上提交的内容更活跃?

概述一个方法

我们的分析包括以下步骤:

  • 过滤我们的数据并将其分组为两个数据框。第一个数据框将包含所有使用疫苗、疫苗接种或疫苗接种等词的提交。我们将与第一个数据框进行比较的第二个数据框将包含未提及这些词的提交。
  • 在每个数据框上运行简单的计算。通过查找平均或中值参与计数来总结我们的数据(在此分析中,参与计数由评论和赞成票的组合数量表示)可以帮助我们更好地理解收到的 r/askscience 数据的每个子集,并为我们的问题制定答案研究问题。

值得花一点时间来澄清这里使用的术语。平均值是从数据集中取出所有值,将它们相加,然后将它们的总和除以值总数的结果。中位数是出现在数据集中间的数字。要找到它,首先我们需要将数据集中的所有值从小到大排序。正好位于最小值和最大值之间的数字是中位数。如果有偶数个值,则取中间两个数字的平均值。均值和中值都是集中趋势的度量,这些度量允许我们通过查看某个中心点来评估数据集。当数据集包含很多值而没有很多异常值时,均值是衡量其集中趋势的好方法。另一方面,带有异常值的数据集可以通过中位数更好地衡量。在本章中,我们将着眼于分析的两种度量。

缩小数据的范围

Reddit 数据集可能相当大,即使您只查看一个 subreddit。虽然从尽可能全面的数据集开始很重要,但根据您的特定项目过滤数据可为您提供更好、更简洁的概览。过滤还减少了运行每个计算所需的时间。

本章还介绍了总体和样本数据的概念。人口数据描述了包含整个指定组的数据集。在这种情况下,此指定组包含 2014 年至 2017 年间在 r/askscience subreddit 中发布的任何提交。样本数据,顾名思义,是数据集的一个子集或样本。在本练习中,将有两个子集:一个由与疫苗接种相关的提交组成(我们将在接下来的页面中定义),另一个由所有其他提交组成。我们将对这两个子集进行分析以比较它们。

我们将使用第 8 章中设置的虚拟环境和 Jupyter Notebook 项目。以下练习旨在扩展该特定笔记本,您将使用我们在那里建立的相同变量名称。

从特定列中选择数据

为了过滤我们任务的数据,我们首先将其缩减为仅包含包含与我们的分析相关的信息的列。 然后我们将删除包含不相关样本的行,例如空值。

让我们从选择我们需要的列开始。 我们对两种不同的数据特别感兴趣——提交的标题,包括提交给 r/askscience 的文本,以及提交的反应。 如前一章所述,通过运行这行代码,我们可以将所有列名视为一个列表:

reddit_data.columns

这应该呈现一个字符串列表,每个字符串代表一个列名:

Index(['approved_at_utc', 'archived', 'author', 'author_cakeday',
       'author_flair_css_class', 'author_flair_text', 'banned_at_utc',
       'brand_safe', 'can_gild', 'can_mod_post', 'contest_mode', 'created',
       'created_utc', 'crosspost_parent', 'crosspost_parent_list',
       'distinguished', 'domain', 'edited', 'from', 'from_id', 'from_kind'
       'gilded', 'hidden', 'hide_score', 'id', 'is_crosspostable',
       'is_reddit_media_domain', 'is_self', 'is_video', 'link_flair_css_class',
       'link_flair_text', 'locked', 'media', 'media_embed', 'name',
       'num_comments', 'over_18', 'parent_whitelist_status', 'permalink',
       'pinned', 'post_hint', 'preview', 'quarantine', 'retrieved_on', 'saved',
       'score', 'secure_media', 'secure_media_embed', 'selftext', 'spoiler',
       'stickied', 'subreddit', 'subreddit_id', 'subreddit_type',
       'suggested_sort', 'thumbnail', 'thumbnail_height', 'thumbnail_width',
       'title', 'ups', 'url', 'whitelist_status'],
      dtype='object')

这里有 62 个列名。 为了进行这种分析,我们保留包含标题列中列出的标题、up 列中列出的赞成票以及 num_comments 列中列出的每个提交的评论数量的列是有意义的。

正如我们在前一章所做的那样,我们现在可以使用方括号选择数据集中的特定列。 确保你还在第 8 章的笔记本中,并且你已经运行了每个单元格。 (请记住:如果您已经运行它,单元格左边的方括号中应该有一个数字。)然后,使用加号 (+),在所有其他单元格下添加一个单元格并键入清单 9 中的行 -1 进去:

columns = ["title", "ups", "num_comments"]
ask_science_reduced = reddit_data[columns]

清单 9-1:从数据集中选择几列

我们首先创建一个名为 columns 的变量,我们为其分配一个字符串列表。我们列表中的字符串(“title”、“ups”、“num_comments”)代表我们想要包含在过滤数据集中的每一列的列标题。确保每个列名称的字符串与列标题的字符串完全匹配,包括标点符号、大小写和拼写——小错误可能会导致 Python 脚本出错。

在下一行中,我们创建变量 ask_science_reduced,它存储一个较小的数据框,仅包含列变量中列出的列。请注意,我们现在将变量列放在方括号内,而不是像之前那样在方括号内添加单个字符串。将整个列表而不是一个字符串放在括号内允许我们选择多列。

现在我们已经将数据缩减到特定列,让我们删除包含与我们的分析无关的值的数据行。

处理空值

在大型、不一致的数据集中,某些行或单元格可能不包含任何信息。这些“空”单元格可能根本不包含任何值或占位符,即设计数据结构的机构或人员使用的任意字符串。如果幸运的话,占位符将在数据字典中描述,数据字典是解释数据集的内容、结构和格式的文档。否则,我们必须通过研究自己解决,或者在最坏的情况下,通过基于列名的复杂猜测。 (幸运的是,在这种情况下,我们认识收集数据的人,并且能够向他提出问题。)

在数据解析中,我们将这些空单元格称为空值。在 Python 中,当我们尝试在交互式 shell 或其他 Python 接口中打印空值时,它们可能会返回为 None。在 Pandas 中,它们可能被称为 NaN 值(其中 NaN 代表“非数字”),并且数据框的行将显示 NaN 作为缺失值的占位符。

空值在收集社交媒体用户可选操作信息的列中尤为常见。例如,收集发布到 Facebook 的视频的链接的列仅包含用户实际发布视频的帖子的值。对于任何不包含视频的帖子,数据集将没有该单元格的值,并且可以使用占位符,例如字符串“no video”,或者将单元格留空,这意味着它将具有 None 值。

问题是占位符、无和 NaN 值会导致分析错误。当我们应用函数或计算时,我们的脚本将按照指示运行这些计算,直到它到达一个空单元格。我们将介绍两种处理空值的方法,每种方法对数据分析师的目的略有不同:一种是从分析中完全删除这些空值,而另一种则保留整个数据集并将空行计数为零。

删除空值

如果特定列的空值包含空值,我们可以选择排除整行数据。 pandas 库使用 dropna() 函数使我们可以轻松实现这一点。 清单 9-2 显示了根据列是否包含 NaN 值来删除整行数据的代码。

ask_science_dropped_rows = ask_science_reduced.dropna(
    subset=["ups", "num_comments"])

清单 9-2:删除特定列中具有 NaN 值的行

在没有规范的情况下,此函数会告诉 pandas 从数据框中删除整行。 但是 dropna() 函数也带有有用的参数,比如子集参数。 在这个例子中,我们使用子集告诉熊猫删除 ups 和 num_comments 列中包含 NaN 值的行。 (如果我们不向 dropna() 函数传递任何参数,pandas 默认会删除具有任何空值的行,并将在数据框的每一列中查找 NaN 值。)

填充空值

为了考虑 NaN 值但保留数据框的每一行,我们可以使用 fillna() 函数用字符串或数字填充每个空单元格,而不是将其删除。 清单 9-3 展示了如何使用 fillna() 函数将 num_comments 列中的空单元格填充为数字 0:

ask_science_data["num_comments"] = ask_science_data["num_comments"].fillna(value=0)

清单 9-3:用 0 填充空值

fillna() 函数的括号内,我们将 0 分配给 value 参数。此代码将 num_comments 列替换为自身的修改版本,现在包含零代替 NaN 值。

决定是删除空值还是填充它们取决于您的数据集以及您想如何回答研究问题。例如,如果我们想获得整个数据集评论的中位数,我们可能会问,假设缺失值仅意味着提交的评论没有评论是否安全。如果我们决定是,我们可以用 0 填充这些值并进行相应的计算。

根据包含缺失值或“空单元格”的行数,评论的中位数可能会发生显着变化。然而,因为这个数据集有时会将评论或赞数记录为零,有时记录为空值,我们不能自动假设这些列的包含空值的行应该被视为零(如果空值表示零,假设数据集不包含任何实际零可能是合理的)。相反,也许这是我们的档案管理员无法捕获的数据;也许这些帖子在他收集到这些信息之前就被删除了;或者也许这些指标是在其中一些年份引入的,但不是为其他年份引入的。因此,为了我们的练习,我们应该处理我们拥有的数据并删除不包含 ups 或 num_comments 列的值的数据行,就像我们在示例 9-2 中所做的那样。

对数据进行分类

下一步是根据我们关于疫苗接种的特定研究问题过滤我们的数据。我们需要对我们认为关于疫苗接种的 Reddit 提交的内容进行分类。

这种分类将是还原性的。这在处理大型数据集时是必要的,因为阅读每个帖子并单独解释每个帖子是非常耗费人力的。即使我们可以雇佣一大群人来阅读每篇文章并手工解释——这在一些散文驱动的项目中并不少见——也很难确保每个人都使用相同的量规进行解释,这可以难以以标准的、可识别的方式对数据进行分类。

在我们的示例中,我们将样本限制为包含某种形状或形式的疫苗接种或疫苗接种一词的提交。我们将专门寻找任何包含疫苗的标题。 (在语言学中,这通常被称为词干,是词的各种迭代和变形中最常见的部分。)这个子集可能不会涵盖所有关于疫苗接种的帖子,但它可以帮助我们定性地理解问题手。

我们将首先创建一个新列,该列根据其提交标题是否包含字符串“vaccin”对行进行分类。我们将用布尔值(即二进制值)、True 或 False 填充此列。清单 9-4 显示了创建此列所需的代码:

ask_science_dropped_rows["contains_vaccin"] = ask_science_dropped_rows["title"].str.contains("vaccin")

清单 9-4:根据列是否包含特定字符串过滤列

在等号的左侧,我们使用方括号创建了一个名为 contains_vaccin 的新列。 在右侧,我们使用了一个链式函数:首先我们使用括号表示法从我们的数据中选择标题列,然后我们对该列使用 str() 函数将值转换为字符串,以便我们可以使用 contains() 确定列值是否包含疫苗。

这个链的结果是一个布尔值:如果提交的标题包含疫苗,那么它将返回值 True; 否则它将返回值 False。 最后,我们应该有一个只有 True 或 False 值的新列 (contains_vaccin)。

现在我们有了这个额外的列,让我们过滤我们的数据! 在笔记本的一个新单元格中运行清单 9-5 中的代码:

ask_science_data_vaccinations = ask_science_dropped_rows[ask_science_dropped_rows["contains\
_vaccin"] == True]

清单 9-5:根据条件值过滤数据

这应该是熟悉的语法。 但请注意,在右侧的括号内,我们使用了条件 ask_science_data_dropped_rows["contains_vaccin"] == True 而不是列标题。 这告诉熊猫检查“contains_vaccin”列中的值是否等于“True”。 要将我们的数据过滤为仅包含不包含茎疫苗的行的子集,我们可以将条件设置为等于 False:

ask_science_data_no_vaccinations = ask_science_dropped_rows[ask_science_dropped_rows["contains\
_vaccin"] == False]

现在我们已经过滤了我们的数据,让我们查询一些有趣的见解。

总结数据

为了确定包含疫苗接种、疫苗或疫苗接种等词的变体的 r/askscience Reddit 提交是否比不包含这些词的 r/askscience 提交获得更大的反应,我们将查看收到最多综合反应的帖子,定义为点赞数和评论数的总和。

注意有多种方法可以回答这个问题,认识到这一事实很重要。然而,如前所述,在本书中,我们试图从对初学者最友好的立场来处理计算和分析,这意味着我们可以使用简单的方法进行数学计算。它们可能不是最优雅的,但它们介绍了一些最基本的 Pandas 方法,初学者可以在他们更多地了解库时建立这些方法。

对数据进行排序

首先,我们将创建一个列,将每行的赞数和评论数组合在一起。这是对 Pandas 的一个非常简单的操作,如清单 9-6 所示。

ask_science_data_vaccinations["combined_reactions"] = ask_science_data\
_vaccinations["ups"] + ask_science_data_vaccinations["num_comments"]
ask_science_data_no_vaccinations["combined_reactions"] = ask_science_data_no\
_vaccinations["ups"] + ask_science_data_no_vaccinations["num_comments"]

清单 9-6:将几列合并为一列

在这里,我们在每个数据框中创建一个名为 combine_reactions 的列,并为其分配一个等于 num_comments 和 ups 列之和的值。当你运行这段代码时,你可能会遇到一个 SettingWithCopyWarning,正如它的名字所暗示的那样——一个警告,而不是一个错误(虽然它看起来有点威胁,因为它显示在红色背景上)。错误和警告之间的区别在于错误会阻止您的代码运行,而警告只会促使您仔细检查您正在运行的代码是否正在执行您想要的操作。对于这本书,我们知道这里显示的代码完成了我们想要它做的事情:将点赞数添加到评论数中。如果您对编写此警告的开发人员希望您进一步调查的内容感到好奇,请参阅 http://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html。

注意 当我们读入包含数据的 .csv 文件时,我们没有指定哪一列包含哪种数据类型(pandas 提供的一个选项作为参数)。如果您不指定列的数据类型,pandas 会根据它找到的内容自动解释列中的类型(有时它不会统一为列分配类型!)。您可以在 https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html

既然我们在一列中有了组合反应的值,让我们使用 sort_values() 函数对值进行排序,如清单 9-7 所示。

ask_science_data_vaccinations.sort_values(by="combined_reactions", ascending=False)
ask_science_data_no_vaccinations.sort_values(by="combined_reactions", ascending=False)

清单 9-7:使用 sort_values() 对数据框进行排序

顾名思义, sort_values() 对您的数据框进行排序。 我们在这里给两个参数传递了参数:by,它告诉pandas按哪一列进行排序,accenting,它告诉pandas排序的顺序。在示例9-7中,我们将False传递给了升序,这意味着数据将是 按照从大到小的顺序。

图 9-1 显示了我们对 ask_science_data_vaccinations 列进行排序的数据框的一些结果。

图 9-1:按组合反应数排序的 ask_science_data_vaccinations 列部分数据框的 Jupyter Notebook 显示

图 9-2 显示了 ask_science_data_no_vaccinations 列的一些结果。

图 9-2:按组合反应数排序的 ask_science_data_no_vaccinations 列部分数据框的 Jupyter Notebook 显示

如您所见,非疫苗接种数据框中的最高提交比疫苗接种相关数据框中的最高提交获得了更多的反应。这对于两个数据集中前 10 名最大的提交也是如此。对于前 10 名提交的综合反应数量,不含茎疫苗的那些比那些含有疫苗的要高得多。因此,根据 r/askscience 数据的每个子集的前 10 项提交的参与总数来衡量,我们可能会得出结论,与疫苗接种相关的提交并没有像其他主题那样获得足够的关注。

但是这里有一个问题。我们只看了前 10 个帖子。过滤和排序我们的数据集可以让我们更接近更好地理解,但它只向我们展示了庞大数据集的极值。在下一节中,我们将介绍一些用于进一步分析数据的不同方法。

描述数据

汇总数据的一种常用方法是使用 mean() 函数,如清单 9-8 所示。

ask_science_data_vaccinations["combined_reactions"].mean()

清单 9-8:mean() 函数

我们在这里使用 mean() 函数来查找我们选择的列 (combined_reactions) 中所有值的平均值。 当您在单元格中运行此代码时,您应该得到以下数字:

13.723270440251572

现在为 ask_science_data_no_vaccinations 数据框运行相同的代码,换出数据框的名称,如下所示:

ask_science_data_no_vaccinations["combined_reactions"].mean()

这应该返回以下内容:

16.58500842788498

这个数字向我们表明,不包含疫苗的提交的平均参与度指标的平均数量高于那些包含疫苗的提交的平均参与度数。 换句话说,当我们查看整个数据集的参与度平均值时,我们之前的结论——与疫苗接种相关的提交请求的参与度较低,因此可能不会像没有的帖子那样吸引 Reddit 用户的关注——也得到了支持 ,而不仅仅是在观察前 10 个帖子时。

平均值只是汇总值的一种方式。 我们可以使用 describe() 函数在 pandas 中一次查看多个指标,如清单 9-9 所示。

ask_science_data_vaccinations["combined_reactions"].describe()

清单 9-9:describe() 函数

如果我们在一个单元格中运行示例 9-9 中的代码,它应该返回一个结果列表:

count    1272.000000
mean       13.723270
std       162.056708
min         0.000000
25%         1.000000
50%         1.000000
75%         2.000000
max      4283.000000
Name: combined_reactions, dtype: float64

此摘要包括计数或总行数; 平均值或平均值; std,标准差; min,该列中的最小数字; 第 25、50 和 75 个百分位数(第 50 个为中位数); 和最大值,该列中的最大值。

让我们在另一个单元格中使用以下代码为 ask_science_data_no_vaccinations 数据框运行相同的代码:

ask_science_data_no_vaccinations["combined_reactions"].describe()

如果我们在另一个单元格中运行此代码,我们应该得到如下内容:

count    476988.000000
mean         16.585008
std         197.908268
min           0.000000
25%           1.000000
50%           1.000000
75%           2.000000
max       19807.000000
Name: combined_reactions, dtype: float64

这向我们表明,两个数据框的中位数是相同的,这代表了另一种衡量帖子参与度的方法。然而,这里的平均值可能是我们比较提交的最佳方式,因为每个数据集的中位数 1 不允许我们明确区分一个数据集与另一个数据集的参与度。

最后但并非最不重要的一点是,当我们展示我们的发现时,为我们刚刚运行的各种分析提供背景是很重要的。虽然为我们的受众提供中位数和手段可能会有所帮助,但对我们对数据所做的一切保持透明也很重要。数据分析需要理解上下文:我们不仅应该提供有关数据范围的信息(我们在第 8 章中了解到),还要概述我们如何对数据进行分类(在这种情况下通过查找词干接种疫苗或接种疫苗)以及任何其他可以提供更多背景信息的有用观察结果。

这些观察之一可能是研究我们子集的分布。如前所述,均值和中位数都旨在帮助我们衡量数据集的集中趋势,但在这种情况下它们差异很大:中位数是我们数据的两个子集的 1 个赞成或评论,而与疫苗接种相关的帖子和所有其他帖子的平均值在 13 到 16 之间。通常,这种差异应该促使我们进一步检查数据集的分布(我们在第 7 章中简要介绍了数据集分布的概念),并包括一些在我们展示我们的发现时可能不寻常的特征。例如,看到我们数据的两个子集的中位数都是 1,我们可以安全地假设至少一半包含在任一子集中的帖子获得了 1 个或更少的赞或评论,这一事实可能值得注意。

无论我们最终在演示文稿、论文或文章中写什么,重要的是描述数据本身、使用的过程、发现的结果以及任何可能有助于我们的观众全面掌握的上下文我们的分析。

概括

在本章中,您学习了如何使用数据处理、过滤和分析等各个步骤来思考研究问题。我们完成了根据列中的潜在值对社交媒体数据进行分类和过滤所需的步骤。然后我们看到了如何对这些过滤后的数据集运行简单的数学计算。

重要的是要了解处理此类分析的方法不止一种。这可以从技术上体现出来:一些数据分析师可能会选择使用不同的函数来进行我们在本章中所做的过滤和聚合。在其他情况下,研究人员可能会尝试使用不同的方法论方法并考虑不同的方法来对他们的数据进行分类和总结。例如,不同的开发人员可能使用了另一种方式来对哪些内容构成有关疫苗接种的帖子进行分类,哪些内容不构成——他们可能不仅仅基于一个搜索词(在我们的例子中是疫苗)过滤了他们的数据。尽管一些固执己见的在线用户可能会这么想,但没有确定的方法可以用数据回答问题,尽管它确实有助于对我们拥有的数据进行更多实验并尝试不同的方法来回答同一研究问题,就像我们在本章。

虽然在本章中我们使用分类驱动的细分来总结我们的数据集,但在下一章中,我们将研究如何总结不同时间段的数据。