第 10 章:衡量政治人物的 Twitter 活动

在第 9 章中,我们对大型数据集进行了第一次数据分析,并了解了如何基于简单的分类来回答研究问题。尽管它产生了良好的结果,但这种分析是有限的:它只查看了一个时间点的数据。另一方面,跨时间分析数据使我们能够寻找趋势并更好地了解我们遇到的异常情况。通过探索数据的变化并隔离特定事件,我们可以在它们之间建立有意义的联系。

在本章中,我们将查看随时间变化的数据。具体来说,我们将检查 Twitter 于 2018 年发布的数据集,其中包含位于伊朗的政治演员在 2016 年美国总统大选之前、期间和之后发布的推文,以影响美国和其他地方的公众舆论。数据转储是该平台持续努力的一部分,旨在让研究人员能够分析由虚假和雇佣 Twitter 帐户运行的媒体操纵活动。我们想查看使用与唐纳德特朗普和/或希拉里克林顿相关的主题标签的推文,并确定他们随着时间的推移表现如何。他们在选举前增加了吗?它们是在 2016 年大选后立即下降还是继续增长?

在此过程中,您将学习如何使用一种称为 lambda 的函数来过滤数据。您将看到如何格式化原始数据并将其转换为时间序列或重新采样。最后但并非最不重要的一点是,您将了解一个与 Pandas 一起使用的新库,称为 matplotlib (https://matplotlib.org/)。 matplotlib 库将通过在 Jupyter Notebook 环境中可视化数据,使用简单的图形来说明数据波动来帮助我们理解数据。到本项目结束时,您应该对 Pandas 以及您可以用它做的事情有深刻的了解。

入门

2017 年和 2018 年,Twitter、Facebook 和谷歌因允许国际代理人传播旨在影响美国和国外舆论的虚假或误导性内容而受到严厉批评。这种公开审查最终导致了两个主要数据包的发布:一条俄罗斯推文——根据 Twitter、国会和各种媒体报道——被用来操纵美国媒体格局,另一条伊朗推文也在做同样的事情。

俄罗斯数据集要大得多,可能会减慢我们的进度,因为加载和处理需要很长时间。因此,如前所述,我们将关注另一个数据集:伊朗数据。特别是,我们将查看名为“iranian_tweets_csv_hashed.csv”的电子表格,您可以直接从 https://archive.org/details/iranian_tweets_csv_hashed 下载/.

我们的研究问题很简单:随着时间的推移,伊朗演员发布了多少与特朗普和克林顿有关的推文?我们将与特朗普和克林顿相关的推文定义为使用包含字符串特朗普或克林顿(忽略大小写)的主题标签的推文。正如第 9 章所讨论的,这种分类可能会漏掉一些与两位总统候选人相关的推文,但我们只是出于教学目的对这个程序做了一个简单的版本。在真实的数据分析中,我们可能会撒下更大的网。

现在我们有了一个研究问题,让我们开始这个项目吧!

设置您的环境

正如我们在上一章所做的那样,首先我们需要为我们的项目设置一个新文件夹。然后,在该文件夹中,我们需要创建三个子文件夹:data、notebooks 和 output。完成后,将下载的 Twitter 数据放在数据文件夹中。

接下来,导航到 CLI 中的项目目录并输入“python3 -m venv myvenv”。这会在您的项目中创建一个虚拟环境,您可以使用命令 source myvenv/bin/activate 激活它。如果你的 CLI 以 (env) 开头,你的虚拟环境就会被激活(如果你需要复习,请重温第 8 章)。

激活虚拟环境后,现在我们需要安装我们将使用的所有库。我们需要 jupyter、pandas 和 matplotlib,我们可以使用 pip 安装所有这些,如下所示:

pip install jupyter
pip install pandas
pip install matplotlib

安装所有三个库后,在控制台中输入 jupyter notebook,一旦 Jupyter 启动,导航到 notebooks 文件夹并通过选择 New>Python 3 创建一个新 notebook。最后,让我们重命名新 notebook:单击标题,这通常是默认的 为 Untitled,并将其重命名为 twitter_analysis。

完成所有这些后,您就可以开始了!

将数据加载到您的笔记本中

首先,让我们确保我们可以使用刚刚安装的所有库。 要加载 pandas 和 matplotlib,我们将使用 import 语句。 在笔记本的第一个单元格中,键入以下内容:

import pandas as pd
import matplotlib.pyplot as plt

在第一行中,我们导入了 pandas 并使用 pd 作为简写,因此我们可以简单地引用 pd 来访问该库的所有功能。 我们将在 matplotlib 的第二行中做同样的事情,除了这里我们只需要一个名为 pyplot 的函数子集,我们将调用 plt 作为快捷方式。 这意味着,不必写出 matplotlib.pyplot,我们可以简单地使用 plt,这应该有助于避免混乱的代码。

注意 使用 pd 作为速记的约定出现在 pandas 库的文档中。 同样,在 matplotlib 文档中使用了 plt 快捷方式。

单击运行(或使用 shift-enter)运行此单元格,您应该能够访问后面单元格中的两个库。

接下来,我们需要加载数据。 让我们创建一个名为 tweets 的变量来保存我们要检查的数据。 在下一个单元格中输入以下内容并运行它:

tweets = pd.read_csv("../data/iranian_tweets_csv_hashed.csv")

该行使用 pandas 函数 read_csv() 提取我们数据文件夹中的 Twitter 数据,该函数获取 .csv 文件的文件路径并返回我们可以使用的数据框。 有关数据框的复习,请查看第 8 章。

现在我们的数据已被摄取,让我们考虑下一步需要采取的措施。 该数据集包括涵盖广泛主题的推文,但我们只对有关唐纳德特朗普和希拉里克林顿的推文感兴趣。 这意味着我们需要将我们的数据过滤到那些推文,就像我们在第 9 章中过滤我们的 r/askscience 数据到与疫苗接种相关的帖子时所做的那样。

再一次,在我们缩小范围之前,我们需要更好地了解我们的数据。 由于我们已经加载了它,我们可以使用 head() 函数开始探索它。 在单元格中输入以下内容并运行它:

tweets.head()

您应该会看到前五行数据,如图 10-1 所示。

图 10-1:加载的数据框

如您所见,此转储包含大量与推文相关的元数据。 每一行代表一条推文,包括有关推文本身以及发布推文的用户的信息。 您可能还记得第 8 章中的内容,要将所有列名称视为列表,您可以使用以下代码行:

tweets.columns

运行该单元格后,您应该会看到以下列列表:

Index(['tweetid', 'userid', 'user_display_name', 'user_screen_name',
       'user_reported_location', 'user_profile_description',
       'user_profile_url', 'follower_count', 'following_count',
       'account_creation_date', 'account_language', 'tweet_language',
       'tweet_text', 'tweet_time', 'tweet_client_name', 'in_reply_to_tweetid',
       'in_reply_to_userid', 'quoted_tweet_tweetid', 'is_retweet',
       'retweet_userid', 'retweet_tweetid', 'latitude', 'longitude',
       'quote_count', 'reply_count', 'like_count', 'retweet_count', 'hashtags',
       'urls', 'user_mentions', 'poll_choices'],
      dtype='object')

对于我们的目的,最重要的列是主题标签和 tweet_time。 主题标签列将每条推文中使用的所有主题标签显示为一个单词列表,以逗号分隔,位于左方括号和右方括号之间。 尽管它们遵循列表的模式,但 Python 将它们解释为一个长字符串。 图 10-2 显示,例如,在第 359 行中,使用的主题标签是“Impeachment”和“MuellerMonday”,并存储为一个长字符串“[Impeachment, MuellerMonday]”。 请注意,并非每条推文都使用主题标签,我们的分析将仅考虑使用主题标签的那些。

图 10-2:使用 .iloc[] 方法显示的推文 DataFrame 第 359 行中存储的值

然后,主题标签列将允许我们使用包含字符串 trump 或 clinton 的主题标签来识别推文。 tweet_time 列包含发送推文的时间戳。在过滤数据后,我们将使用 tweet_time 列来计算与特朗普和克林顿相关的推文的月度统计数据。

为了过滤我们的数据,我们将重复我们之前对疫苗接种相关数据的分析中的一些相同步骤。在那里,我们创建了一个新列,并通过选择另一列并使用 contains() 函数查看它是否包含字符串 vaccin 来用 True 或 False 填充它。对于这个项目,我们还将创建一个 True 或 False 列,但我们将使用更强大的新 Pandas 功能而不是使用 contains() 函数:lambda 函数。

拉姆达

lambda 函数是一个小的、无名的函数,我们可以应用于列中的每个值。我们可以使用自定义 lambdas 来修改我们想要的数据,而不是局限于 Pandas 开发人员已经编写的函数,比如 contains()。

让我们看一下 lambda 函数的结构。假设我们要给一个数字加 1 并返回新数字。用于此的常规 Python 函数可能如下所示:

def add_one(x):
    return x + 1

在这里,我们使用 def 关键字创建了一个函数并将其命名为 add_one(),将 x 定义为唯一的参数,并将我们的主要代码行写在冒号后缩进的新行上。 现在让我们看一下相同函数的 lambda 等价物:

lambda x: x + 1

与缩进 Python 函数不同,Lambda 主要编写为紧凑的单行代码,并在 apply() 函数内部使用。 我们不使用 def 来编写和命名新函数,而是使用单词 lambda 后跟参数 x(请注意,我们不需要使用括号)。 然后我们指定我们想要对 x 做什么——在这种情况下,加 1。注意这个函数没有名字,这就是为什么有人称 lambdas 匿名函数。

要将这个 lambda 函数应用于列,我们将函数本身作为参数传递,如清单 10-1 所示。

dataframe["column_name"].apply(lambda x: x + 1)

清单 10-1:将 lambda 函数传递给 apply()

在此示例中,我们从名为 dataframe 的假设数据框中选择名为 column_name 的列,并对其应用 lambda x + 1。 (请注意,要使其正常工作,column_name 中的值需要是数字,而不是字符串。)这行代码将返回一系列结果,我们将这些结果添加到 column_name 列中每一行的值上 . 换句话说,它将显示 column_name 列中的每个值,并为其添加 1。

如果您需要对列应用更复杂的操作,您还可以将传统的 Python 函数传递给 apply()。 例如,如果我们想使用我们之前定义的 add_one() 函数重写示例 10-1,我们只需将名称传递给 apply() 函数,如下所示:

dataframe["column_name"].apply(add_one)

Lambdas 是我们修改整列的一种非常有用和有效的方式。 它们是一次性任务的理想选择,因为它们未命名(匿名)并且编写起来简单快捷。

好了,关于 lambdas 就够了! 让我们回到我们的分析。

过滤数据集

我们想要一个仅包含与 2016 年总统候选人相关的推文的数据框。 如前所述,我们将为此使用一个简单的启发式方法:仅包含主题标签包含字符串 trump、clinton 或两者的推文。 虽然这可能无法捕捉到关于唐纳德特朗普或希拉里克林顿的每一条推文,但它是一种清晰且易于理解的方式来看待这些错误信息代理人的活动。

首先,我们需要创建一个包含布尔值 True 或 False 的列,指示给定的推文是否包含字符串 trump 或 clinton。 我们可以使用示例 10-2 中的代码来做到这一点。

tweets["includes_trump_or_clinton"] = tweets["hashtags"].apply(lambda x: "clinton" in str(x).lower() or "trump" in str(x).lower())

清单 10-2:从我们的推文数据集中创建一个新的 True 或 False 列

这段代码非常密集,所以让我们把它分解成几部分。 在等号的左侧,我们创建了一个新列 includes_trump_or_clinton,它将存储我们的 lambda 函数的结果。 然后,在右侧,我们选择 hashtags 列并应用以下 lambda 函数:

lambda x: "trump" in str(x).lower() or "clinton" in str(x).lower()

我们在 lambda 函数中做的第一件事是使用 str(x).lower() 中的“trump”行检查字符串“trump”是否在更长的标签字符串中。这一行获取在 hashtags 列中传递的值 x,使用 str() 函数将 x 转换为字符串,使用 lower() 函数将字符串中的每个字母变为小写,以及最后检查字符串 trump 是否在那个小写字符串中。如果是,则该函数将返回 True;如果不是,它将返回 False。使用 str() 函数是一种很好的方式,可以将我们必须处理的任何值,甚至是空列表和 NaN 值,转换为我们可以查询的字符串;如果没有 str(),空值可能会导致我们的代码出错。它还让我们跳过过滤空值的步骤,就像我们在第 9 章的分析中所做的那样。

在 或者 在我们的 lambda 函数的另一侧,我们使用相同的代码,但用于 clinton。因此,如果主题标签包含字符串 trumpclinton(忽略大小写),则该行将填充为 True;否则,它将填充 False。

一旦我们有了 True 或 False 列,我们需要将我们的推文过滤到只有在 includes_trump_or_clinton 中具有 True 值的推文。我们可以使用示例 10-3 中的代码来做到这一点。

tweets_subset = tweets[tweets["includes_trump_or_clinton"] == True]

清单 10-3:将推文过滤为仅包含特朗普或克林顿的推文

这将创建一个名为 tweets_subset 的新变量,该变量将存储仅包含使用与特朗普或克林顿相关的主题标签的推文的简化数据框。 然后我们使用括号根据条件选择推文的子集——在这种情况下,值 tweets["includes_trump_or_clinton"] 是否为 True

通过这些代码行,我们现在已经将我们的数据集缩小到我们有兴趣观察的推文正文。 我们可以在单独的单元格中使用代码 len(tweets_subset) 来找出 tweets_subset 中的行数,应该是 15,264。 现在是时候看看包含特朗普或克林顿相关主题标签的推文数量是如何随时间变化的。

将数据格式化为日期时间

过滤数据集后,我们希望计算特定时间段内包含与特朗普或克林顿相关的主题标签的推文数量——这个计数通常被称为时间序列。为此,我们将数据列格式化为时间戳,并使用 pandas 函数根据这些时间戳创建计数。

正如我们在第 6 章中使用 Google Sheets 的冒险中看到的那样,在我们的代码中指定我们正在处理的数据类型是很重要的。尽管 Sheets 和 Pandas 可以自动检测整数、浮点数和字符串等数据类型,但它们可能会出错,因此最好是具体的而不是任其偶然。一种方法是选择包含每条推文的时间戳的列,并告诉 Python 将它们解释为日期时间数据类型。

不过,首先,我们需要了解 pandas 当前如何解释我们的数据列。为此,我们将使用 dtypes 属性,正如您在第 8 章中看到的,它允许我们查看数据的某些特征(在这种情况下,每列包含的数据类型):

tweets_subset.dtypes

如果我们在一个单元格中运行这一行,我们的 Jupyter notebook 应该在我们的数据框中显示列的列表以及它们的数据类型:

tweetid                       int64
userid                       object
user_display_name            object
user_screen_name             object
user_reported_location       object
user_profile_description     object
user_profile_url             object
follower_count                int64
following_count               int64
account_creation_date        object
account_language             object
tweet_language               object
tweet_text                   object
tweet_time                   object
tweet_client_name            object
in_reply_to_tweetid         float64
in_reply_to_userid           object
quoted_tweet_tweetid        float64
is_retweet                     bool
retweet_userid               object
retweet_tweetid             float64
latitude                    float6
longitude                   float64
quote_count                 float64
reply_count                 float64
like_count                  float64
retweet_count               float64
hashtags                     object
urls                         object
user_mentions                object
poll_choices                 object
dtype: object

正如您在此处看到的,有两列包含与时间相关的数据:account_creation_date 和 tweet_time。 我们有兴趣了解推文,而不是帐户,因此我们将重点关注 tweet_time。 目前,pandas 将 tweet_time 列解释为对象数据类型,在pandas 中通常表示字符串。 但是 Pandas 有一种专门为时间戳创建的数据类型:datetime64[ns]。

要将这些数据格式化为 datetime64[ns],我们可以使用 pandas 函数 astype(),它将用包含相同数据但被解释为日期时间对象的数据列替换 tweet_time 列。 清单 10-4 向我们展示了方法。

tweets_subset["tweet_time"] = tweets_subset["tweet_time"].astype("datetime64[ns]")

清单 10-4:使用 asytype() 函数格式化 tweet_time 列的数据类型

和以前一样,我们通过将“tweet_time”列放在方括号中来选择它,然后通过对其应用.astype() 函数来替换它。 换句话说,这会将所选列 (tweet_time) 转换为我们在括号内指定的数据类型 ("datetime64[ns]")。

要检查我们的转换是否有效,我们可以简单地在单独的单元格中再次对 tweets_subset 变量运行 dtypes:

tweets_subset.dtypes

在包含此代码的单元格下方,我们现在应该看到 tweet_time 列包含 datetime64[ns] 值。

--snip--
account_language                    object
tweet_language                      object
tweet_text                          object
tweet_time                  datetime64[ns]
tweet_client_name                   object
in_reply_to_tweetid                float64
in_reply_to_userid                  object
--snip--

现在 tweet_time 数据是正确的数据类型,我们可以使用它来开始计算 includes_trump_or_clinton 中的值。

重新采样数据

请记住,我们希望找到每个时间段包含与特朗普或克林顿相关的主题标签的推文数量。为此,我们将使用称为重采样的过程,在该过程中我们会在特定时间间隔内聚合数据;在这里,这可能意味着计算每天、每周或每月的推文数量。在我们的分析中,我们将使用每月的统计数据,但如果我们想要更精细的分析,我们可能会按周甚至按天重新采样。

重采样数据的第一步是将 tweet_time 设置为索引(请记住,索引的作用类似于行标签)。这将允许我们根据 tweet_time 值选择和定位条目,然后对其应用不同类型的数学运算。

要将 tweet_time 列设置为索引,我们可以使用 set_index() 函数。我们通过将 tweet_time 作为参数传递给 tweet_time 来应用 set_index()。 set_index() 函数将返回一个新索引的数据帧,我们将其存储在一个名为 tweets_over_time 的变量中(示例 10-5)。

tweets_over_time = tweets_subset.set_index(“tweet_time”)

示例 10-5:设置新索引并存储结果数据框

要查看新索引的数据帧是什么样子,请运行 tweets_over_time.head() 函数,该函数应返回类似于图 10-3 的内容。

图 10-3:将 tweet_time 列作为索引的数据框

这里有一个微妙但重要的视觉变化:tweet_time 列中的值替换了数据左侧的基于整数的索引号,现在以粗体显示。 这意味着我们已经用 tweet_time 列中的时间戳替换了基于数字的索引。

使用我们的新索引,我们可以使用 resample() 函数随着时间的推移对数据进行分组和聚合,如示例 10-6 所示。

tweet_tally = tweets_over_time.resample("M").count()

清单 10-6:使用 resample() 对数据框中的数据进行分组和聚合

我们可以在 resample() 函数的括号内指定我们希望在一段时间内聚合数据的频率——每天、每周或每月。 在这种情况下,因为我们对这些推文可能如何被用来影响 2016 年美国总统大选感兴趣,所以我们想要一个月度统计数据,所以我们传入字符串“M”(月份的缩写)。 最后但并非最不重要的是,我们需要指定我们希望如何随着时间的推移聚合我们的数据。 由于我们想要每月的推文计数,我们将使用 count() 函数。

一旦我们运行了这个单元格,我们就可以在一个新的单元格中运行 tweet_tally.head() 来查看我们的数据。 我们应该会看到一个数据框,其中包含每月每列中所有值的计数,如图 10-4 所示。

图 10-4:重新采样的数据框,包含每个列值的每月计数。请注意索引为 2015-01-31 的行中数据的计数差异。

如您所见,pandas 计算了每个月每列中包含的值的数量,并将该结果存储为新的列值。现在每一行代表一个月,从 tweet_time 中的日期开始。

这样的结果并不理想。我们现在有很多值散布在大约 30 列中,除此之外,给定月份每列中的计数也可能有所不同。以图 10-4 中标有“2015-01-31”的行为例。对于那个月,我们在 user_profile_url 列中计数为零值,即使 tweet_id 列中的计数为 3。这意味着伊朗特工在该月发布了三条带有包含字符串 trumpclinton 的主题标签的推文,但它们都没有包含用户个人资料 URL。

基于这一观察,我们应该小心我们依靠什么来最好地确定每月的推文总数——我们应该小心我们计算哪些值。如果我们计算 user_profile_url 列中的值的数量,我们只会捕获包含用户个人资料 URL 的推文;我们不会捕获那些没有这些 URL 的推文,我们会低估数据框中的整体推文数量。

因此,在我们重新采样我们的数据集之前,我们应该查看包含每一行的值的数据列并计算这些值。这是非常重要的一步:当我们使用 head()tail() 函数渲染数据时,有些值似乎出现在每一行中,但我们不能简单地通过查看海量数据集中相对较小的一部分。它有助于思考哪一列最可靠地代表数据集合中不能省略的独特实体(例如,推文可能并不总是包含标签,但它必须具有唯一标识符或 ID)。在示例 10-5 中存储在 tweets_over_time 变量中的数据集中,“tweetid”列的每一行都有一个值。

因为我们只需要每月的推文总数,所以我们可以只使用 tweetid 列,我们将其存储在变量 Monthly_tweet_count 中,如下所示:

monthly_tweet_count = tweet_tally["tweetid"]

现在,如果我们使用 head() 函数检查我们的monthly_tweet_count,我们会得到一个更清晰的数据框每月统计:

tweet_time
2013-08-31    1
2013-09-30    0
2013-10-31    0
2013-11-30    2
2013-12-31    0
Freq: M, Name: tweetid, dtype: int64

我们的代码现在已经创建了一个数据框,可以让我们随着时间的推移更好地理解我们的数据,但这种逐行预览仍然有限。 我们希望看到整个数据集的主要趋势。

绘制数据

为了更全面地了解我们的数据,我们将使用 matplotlib,这是我们在本章前面安装和导入的库。 matplotlib 库允许我们绘制和可视化 Pandas 数据帧——非常适合这个项目,因为时间序列在可视化时通常更清晰。

在这个项目开始时,我们将 matplotlib 库的 pyplot 函数导入为 plt。 要访问这些函数,我们键入 plt 后跟我们要使用的函数,如下所示:

plt.plot(monthly_tweet_count)

在这种情况下,我们使用 plot() 函数,将我们的数据框monthly_tweet_count 作为其参数,在 x 轴上绘制数据框的日期,在 y 轴上绘制每月的推文数量,如图所示 图 10-5。

注意 matplotlib 中有很多自定义绘图的方法; 要了解有关它们的更多信息,请访问 https://matplotlib.org/

图 10-5:在 Jupyter Notebook 中由 matplotlib 创建的图表

达达!现在我们有一个图表,显示了使用与特朗普或克林顿相关的主题标签的推文数量。正如我们所预料的那样,它们在 2016 年底急剧增加,向我们表明这些虚假特工在 2016 年美国总统大选之前和之后变得非常活跃——尽管似乎有很多选举发生几个月后,他们的活动发生了变化。虽然我们无法进行所有必要的研究来解释这一活动,但数字取证研究实验室的研究人员已在 https://web.archive.org/web/20190504181644/ 上发表了对伊朗账户的更深入分析。

概括

该项目展示了在 Python 中进行数据分析的强大功能。只需几行代码,我们就能够打开海量数据集,根据其内容对其进行过滤,将其汇总为每月计数,并将其可视化以便更好地理解。在此过程中,您了解了 lambda 函数和基于日期时间对象的数据重采样。

本章总结了本书的实践练习。在下一章也是最后一章中,我们将讨论如何进一步学习这些介绍性课程,以成为一个积极且自给自足的 Python 学习者。