Press "Enter" to skip to content

在 Twitter 帖子中查找时间模式:使用 Python 进行探索性数据分析(第二部分)

使用Python和Pandas进行用户行为分析

用户时间轴示例,作者提供的图片

在本文的第一部分中,我分析了约70,000条Twitter帖子的时间戳,并得出了一些有趣的结果;例如,可以检测到机器人或用户从克隆帐户发布消息。但我无法获得准确的消息时间;至少对于免费账户来说,Twitter API响应没有时区,所有消息都具有UTC时间。现在有数百万人使用社交网络,分析用户行为不仅有趣,而且对于社会学或心理学研究也可能很重要。例如,了解人们是否在晚上、晚上或白天发布更多的消息可能会很有趣,但如果没有正确的时间,就无法知道。最后,我能够找到一个解决方法,即使使用免费API的限制也能很好地工作。

本文将展示从收集数据到使用Python和Pandas进行分析的完整工作流程。

方法

我们的数据处理流程将包括以下几个步骤:

  • 使用Tweepy库收集数据。
  • 加载数据并获取基本见解。
  • 数据转换。我们将按用户分组数据并找到特定的有用于分析的指标。
  • 分析结果。

让我们开始吧。

1. 收集数据

如上一部分所述,我们无法获得Twitter消息的正确时区;Twitter API返回的所有消息都具有UTC时间。作为解决方法,我决定测试三种方法:

  • 我尝试使用“*”掩码获取所有消息并分析每条消息的“位置”字段。并不是每个用户在Twitter上都指定了位置,但是有相当多的用户指定了。这个想法很好,但实际上并没有起作用。Twitter是一个大型社交网络,它生成了大量的数据,即使一周收集所有推文也是不现实的。每秒数千条消息的数量不仅对于普通PC的处理来说太大,而且也超出了免费Twitter开发者账户的限制。
  • 我可以使用城市名称作为请求;例如,我可以搜索所有带有“#Berlin”标签的推文。然后很容易过滤掉具有“德国”位置的用户,对于德国,我们知道时区。这个想法很好用,但问题是结果可能存在偏差。例如,带有“#Berlin”标签的消息可能是由对政治感兴趣的人或体育迷发布的。但总体而言,这个方法很有趣;通过不同的搜索查询,可能可以达到不同类型的受众。
  • 最后,我找到了一种适合我的解决方案。我决定通过指定“*”掩码和语言代码来获取所有 特定语言的消息。这显然不适用于英语,但世界上有许多地理范围足够小的国家可以很容易地确定他们的公民的时区。我选择了荷兰语,因为世界上讲荷兰语的人数并不是很多;这种语言主要用于荷兰和比利时,这两个国家有相同的时区。一些人可能住在国外,苏里南和库拉索也有母语为荷兰语的人,但这些人数不是很多。

收集数据本身很简单。代码已经在第一部分中使用过;我只指定了“*”作为查询掩码和“nl”作为语言代码。免费的Twitter API在获取历史数据时有7天的限制。但实际上,它证明了分页的限制约为100,000条消息。这够多吗?实际上,不是。大多数人可能从未意识到社交媒体上有多少消息。全球只有约2500万荷兰语使用者。而这些人在Twitter上发布的消息数量仅在3小时内就达到了10万条!实际上,我需要每2小时运行一次代码才能获取所有推文。

每隔两个小时收集数据并不是问题;可以在云上轻松完成,但作为免费解决方案,我只拿了我的树莓派:

树莓派4,图片来源https://en.wikipedia.org/wiki/Raspberry_Pi

树莓派是一台小型信用卡大小的Linux电脑,配备1-8 GB的RAM和1-2 GHz的CPU。这些规格绝对足够我们的任务,而且树莓派没有散热器,不产生噪音,只有2-5 W的功耗,非常适合运行一两周的代码。

我稍微修改了Python脚本,以便每隔2小时进行一次请求,并为每个CSV文件的名称添加了时间戳。在进行SSH登录到树莓派之后,我可以使用Linux的“nohup”命令在后台运行此脚本:

nohup python3 twit_grabs.py >/dev/null 2>&1 &

默认情况下,“nohup”将控制台输出保存到“nohup.out”文件中。这个文件可能很大,所以我使用转发到“/dev/null”来防止这种情况。也可以使用类似Cron的其他解决方案,但这个简单的命令已足以完成此任务。

该过程在后台运行,因此我们在屏幕上看不到任何东西,但我们可以使用“tail”命令实时观看日志(这里“20230601220000”是当前文件的名称):

tail -f -n 50 tweets_20230601220000.csv

在控制台中获取推文的样子如下:

收集Twitter消息,图片作者

必要时,我们可以使用“scp”命令从树莓派复制新日志:

scp pi@raspberrypi:/home/pi/Documents/Twitter/tweets_20230601220000.csv .

这里,“/home/pi/Documents/…”是树莓派上的远程路径,“.”是桌面PC上的当前文件夹,CSV文件应该复制到该文件夹中。

在我的情况下,我让树莓派运行了大约10天,这足以收集一些数据。但一般来说,时间越长,结果越好。在为本文的前一部分准备数据时,我看到了足够的用户,他们每周只发布一次Twitter帖子;显然,需要更长的时间间隔才能看到这些用户的行为模式。

2. 加载数据

Python脚本每隔2小时获取新的Twitter消息,并生成大量的CSV文件作为输出。我们可以在Pandas中加载所有文件,并将它们合并成一个数据集:

df_tweets = []files = glob.glob("data/*.csv")for file_name in files:    df_tweets.append(pd.read_csv(file_name, sep=';',                                  usecols=['id', 'created_at', 'user_name', 'user_location', 'full_text'],                                  parse_dates=["created_at"],                                  lineterminator='\n', quoting=csv.QUOTE_NONE))df = pd.concat(df_tweets).drop_duplicates('id').sort_values(by=['id'], ascending=True)

代码很简单。我将每个文件加载到数据帧中,然后使用pd.concat组合所有数据帧。时间间隔会重叠;为避免重复记录,我使用drop_duplicates方法。

让我们看看我们拥有哪些数据:

display(df)

结果如下:

包含所有消息的数据帧,图片作者

文本和消息ID实际上并不重要;对于分析,我们只需要“created_at”字段。为了更容易进行进一步的处理,让我们将日期、时间和当天的小时作为单独的列提取出来。我们还可以为所有记录添加时区偏移量:

tz_offset_hours = 2def update_timezone(t_utc: np.datetime64):    """ 添加时区到UTC时间 """    return (t_utc + np.timedelta64(tz_offset_hours, 'h')).tz_convert(None)def get_time(dt: datetime.datetime):    """ 从datetime获取HHMM格式的时间 """    return dt.time().replace(        second=0,         microsecond=0)        def get_date(dt: datetime.datetime):    """ 从datetime获取日期 """    return dt.date()def get_datetime_hhmm(dt: datetime.datetime):    """ 获取日期和时间的HHMM格式 """    return dt.to_pydatetime().replace(second=0, microsecond=0)def get_hour(dt: datetime.datetime):    """ 从datetime获取小时 """    return dt.hourdf["time_local"] = df['created_at'].map(update_timezone)df["datetime_hhmm"] = df['time_local'].map(get_datetime_hhmm)df["date"] = df['time_local'].map(get_date)df["time"] = df['time_local'].map(get_time)df["hour"] = df['time_local'].map(get_hour)# 可选,我们可以只选择几天df = df[(df['date'] >= datetime.date(2023, 5, 30)) & (df['date'] <= datetime.date(2023, 5, 31))].sort_values(by=['id'], ascending=True)# 显示display(df)

结果如下:

添加列的数据框,作者:图片

数据加载已完成。让我们看看数据长什么样。

3. 总体洞见

本文旨在分析“时间”领域的模式。作为热身,让我们看一下单个时间轴上的所有消息。为了绘制文章中的所有图形,我将使用Bokeh库:

from bokeh.io import show, output_notebookfrom bokeh.plotting import figurefrom bokeh.models import ColumnDataSourcefrom bokeh.models import SingleIntervalTicker, LinearAxisfrom bokeh.transform import factor_cmap, factor_mark, linear_cmapfrom bokeh.palettes import *output_notebook()def draw_summary_timeline(df_in: pd.DataFrame):    """ 将所有消息按时间分组并绘制时间轴 """    print("所有消息:", df_in.shape[0])    users_total = df_in['user_name'].unique().shape[0]    print("所有用户:", users_total)    days_total = df_in['date'].unique().shape[0]    print("总天数:", days_total)    print()    gr_messages = df_in.groupby(['datetime_hhmm'], as_index=False).size() # .sort_values(by=['size'], ascending=False)    gr_messages["msg_per_sec"] = gr_messages['size'].div(60)    datetime_hhmm = gr_messages['datetime_hhmm']    amount = gr_messages['msg_per_sec']        palette = RdYlBu11    p = figure(x_axis_type='datetime', width=2200, height=500,                title="每秒钟的消息数")    p.vbar(x=datetime_hhmm, top=amount, width=datetime.timedelta(seconds=50), line_color=palette[0])    p.xaxis[0].ticker.desired_num_ticks = 30    p.xgrid.grid_line_color = None    show(p)     draw_summary_timeline(df_)

在此方法中,我将所有消息按日期和时间分组。我之前创建的时间戳具有“HH:MM”格式。每分钟的消息数不是方便的度量标准,因此我将所有值除以60以获取每秒的消息数。

结果如下:

所有Twitter消息,作者:图片

代码在树莓派上运行了约10天。结果,收集了1,515,139个唯一用户发布的6,487,433条Twitter消息。但是在图片中,我们可以看到一些问题。有些时间间隔是缺失的;可能这段时间没有互联网连接。另一天部分缺失,我不知道是什么原因;可能免费的Twitter帐户比所有其他请求的优先级都低。无论如何,我们不能抱怨免费的API,而我的目标是至少收集一周的数据,我有足够的信息来做到这一点。我可以在最后删除损坏的时间间隔:

df = df[(df['date'] >= datetime.date(2023, 5, 30)) & \        (df['date'] <= datetime.date(2023, 6, 5))]

顺便说一句,时间轴上的另一个点引起了我的注意;高峰出现在6月4日,每秒发布的消息数量翻了一番。我对此很好奇。我们可以轻松过滤数据框:

df_short = df[(df['datetime_hhmm'] >= datetime.datetime(2023, 6, 4, 23, 35, 0)) & \              (df['datetime_hhmm'] <= datetime.datetime(2023, 6, 4, 23, 55, 0))]with pd.option_context('display.max_colwidth', 80):    display(df_short[["created_at", "full_text"]])

结果如下:

高峰期间发布的推文,由作者提供的图像

结果显示,高峰期持续了约一小时;也许可以更长,但是已经很晚了;根据时间轴,宣布消息的时间是23:35。

但是让我们回到Pandas。为了进行进一步的时间分析,让我们创建两个辅助方法来绘制所有按白天时间分组的消息

from bokeh.io import showfrom bokeh.plotting import figure, output_filefrom bokeh.models import ColumnDataSourcefrom bokeh.transform import linear_cmapfrom bokeh.palettes import *def draw_dataframe(p: figure, df_in: pd.DataFrame, color: str, legend_label: str):    """ 在00..24时间轴上绘制所有消息 """    messages_per_day = df_in.groupby(['time'], as_index=False).size()        days_total = df["date"].unique().shape[0]    msg_time = messages_per_day['time']    # 数据按每分钟汇总,除以60得到秒数    msg_count = messages_per_day['size']/(days_total*60)      source = ColumnDataSource(data=dict(xs=msg_time, ys=msg_count))        p.vbar(x='xs', top='ys', width=datetime.timedelta(seconds=50),            color=color, legend_label=legend_label, source=source)                def draw_timeline(df_filtered: pd.DataFrame, df_full: pd.DataFrame):    """ 将时间轴绘制成条形图 """    p = figure(width=1600, height=400, title="每秒消息数", x_axis_type="datetime", x_axis_label='时间')        palette = RdYlBu11    draw_dataframe(p, df_full, color=palette[0], legend_label="所有值")    if df_filtered is not None:        draw_dataframe(p, df_filtered, color=palette[1], legend_label="筛选值")            p.xgrid.grid_line_color = None    p.x_range.start = 0    p.x_range.end = datetime.time(23, 59, 59)    p.xaxis.ticker.desired_num_ticks = 24    p.toolbar_location = None    show(p)

这将允许我们在单个24小时时间轴上查看所有消息:

draw_timeline(df_filtered=None, df_full=df)

可选的“df_filtered”参数将在稍后使用。结果如下所示:

Messages per day, Image by author

我们可以清楚地看到白天/黑夜的差异,所以我对荷兰语中的大多数消息来自同一个时区的假设是正确的。

我们还可以绘制单个用户的时间轴。我在上一部分中已经使用了这种方法。为了方便那些将本文用作教程的读者,我也会把代码放在这里:

def draw_user_timeline(df_in: pd.DataFrame, user_name: str):    """ 绘制特定用户的累积消息时间 """    df_u = df_in[df_in["user_name"] == user_name]    # 按天时间分组消息    messages_per_day = df_u.groupby(['time'], as_index=False).size()    msg_time = messages_per_day['time']    msg_count = messages_per_day['size']          # 绘图    p = figure(x_axis_type='datetime', width=1600, height=150,                title=f"累积推文时间线:{name}({sum(msg_count)}条消息)")    p.vbar(x=msg_time, top=msg_count, width=datetime.timedelta(seconds=30), line_color='black')    p.xaxis[0].ticker.desired_num_ticks = 30    p.xgrid.grid_line_color = None    p.toolbar_location = None    p.x_range.start = datetime.time(0,0,0)    p.x_range.end = datetime.time(23,59,0)    p.y_range.start = 0    p.y_range.end = 1    p.yaxis.major_tick_line_color = None    p.yaxis.minor_tick_line_color = None    p.yaxis.major_label_text_color = None    show(p)draw_user_timeline(df, user_name="Ell_____")

结果如下所示:

Messages timeline for a single user, Image by author

4. 数据转换

在上一步中,我们得到了一个由所有用户发布的消息组成的“原始”数据帧。我们将找到每日模式,因此作为输入数据,让我们获取按小时分组并计算每个用户的消息数:

gr_messages_per_user = df.groupby(['user_name', 'hour'], as_index=True).size()display(gr_messages_per_user)

结果如下所示:

在 Twitter 帖子中查找时间模式:使用 Python 进行探索性数据分析(第二部分) 数据科学 第10张

作为提醒,我使用了7天的数据。在这个例子中,我们可以看到在这个区间内,该用户在上午7点发布了4条消息,在上午8点发布了1条消息,在下午5点发布了3条消息,等等。

对于分析,我决定使用三个指标:

  • 用户在一天中“繁忙小时”的总数(在上一个例子中,该数字为5)。
  • 每个用户的消息总数(在上一个例子中,该数字为20)。
  • 由24个数字组成的数组,表示按小时分组的消息数量。作为重要步骤,我还将归一化数组总和为100%。

输出将是按用户名称分组的新数据框。此方法执行所有计算:

def get_user_hours_dataframe(df_in: pd.DataFrame):       """ 获取新用户数据框 """        busy_hours = []    messages = []    hour_vectors = []    vectors_per_hour = [[] for _ in range(24)]    gr_messages_per_user = df_in.groupby(['user_name', 'hour'], as_index=True).size()    users = gr_messages_per_user.index.get_level_values('user_name').unique().values    for ind, user in enumerate(users):        if ind % 50000 == 0:            print(f"正在处理 {ind}/{users.shape[0]}")        hours_all = [0]*24        for hr, value in gr_messages_per_user[user].items():            hours_all[hr] = value                    busy_hours.append(get_busy_hours(hours_all))        messages.append(sum(hours_all))        hour_vectors.append(np.array(hours_all))        hours_normalized = get_hours_normalized(hours_all)        for hr in range(24):            vectors_per_hour[hr].append(hours_normalized[hr])          print("正在生成数据框...")    cdf = pd.DataFrame({        "user_name": users,        "messages": messages,        "hours": hour_vectors,        "busy_hours": busy_hours    })    # 将小时列添加到数据框中    for hr in range(24):        cdf[str(hr)] = vectors_per_hour[hr]    return cdf.sort_values(by=['messages'], ascending=False) def get_hours_normalized(hours_all: List) -> np.array:    """将列表中的所有值归一化为总和100%"""    a = np.array(hours_all)    return (100*a/linalg.norm(a, ord=1)).astype(int)df_users = get_user_hours_dataframe(df)with pd.option_context('display.max_colwidth', None):    display(df_users)

结果如下:

数据指标,按用户分组,作者提供的图片

现在我们有了一个包含所有指标的数据帧,我们准备开始玩弄这些数据。

5. 分析

在最后一步中,我们将所有 Twitter 消息转换为按用户分组的数据。这个数据帧实际上更有用。作为热身,让我们从简单的事情开始。让我们获得每个用户的消息数量数据帧已经排序好了,我们可以轻松地看到发布最多消息的“前五名”用户:

display(df_users[:5])
按用户分组的指标数据帧,作者提供的图片

我们还可以查找百分位数

> print(df_users["messages"].quantile([0.05, 0.1, 0.5, 0.9, 0.95]))0.05     1.00.10     1.00.50     1.00.90     4.00.95    10.0

结果很有趣。这些数据是在7天内收集的。数据帧中有1,198,067个唯一的用户在此期间至少发布了一条消息。90th百分位数只有4,这意味着在这一周中,90%的用户只发布了4条消息。与发布5000多条推文的顶级用户相比,差异很大!但是,正如在第一部分中所讨论的那样,一些“顶级用户”可能是机器人。好吧,我们可以通过使用每小时的消息数量轻松验证这一点。我已经有了按小时分组并标准化为100%的消息数量。让我们找出连续发布消息而没有任何延迟的用户。为此,我们只需要过滤那些每小时发布其消息的4%的用户:

    df_users_filtered = df_users.copy()    for p in range(24):        df_users_filtered = df_users_filtered[(df_users_filtered[str(p)] >= 2) & \                                              (df_users_filtered[str(p)] <= 5)]            display(df_users_filtered)        for user_name in df_users_filtered["user_name"].values:        draw_user_timeline(df, user_name)

数字可能不是完全准确的4%,因此我使用了2..5%作为过滤范围。结果发现有28个“用户”每小时发布相同数量的消息:

“用户”,以相等的间隔发布消息,作者提供的图片

在前面的部分中,我已经使用聚类算法检测到了一些机器人。在这里,我们可以看到,即使使用更简单的方法,我们也可以获得类似的结果。

让我们进入更有趣的部分,按他们的活动时间对用户进行分组。由于每小时的消息总数已标准化为100%,因此可以进行相当复杂的请求。例如,让我们添加“早上”、“白天”、“晚上”和“夜间”等新列:

df_users["night"] = df_users["23"] + df_users["0"] + df_users["1"] + df_users["2"] + df_users["3"] + df_users["4"] + df_users["5"] + df_users["6"]df_users["morning"] = df_users["7"] + df_users["8"] + df_users["9"] + df_users["10"]df_users["day"] = df_users["11"] + df_users["12"] + df_users["13"] + df_users["14"] + df_users["15"] + df_users["16"] + df_users["17"] + df_users["18"]df_users["evening"] = df_users["19"] + df_users["20"] + df_users["21"] + df_users["22"]

为了分析,我将仅使用发布了10条以上信息的用户:

df_users_ = df_users[(df_users['messages'] > 10)]df_ = df[df["user_name"].isin(df_users_["user_name"])]

当然,10不是一个统计上显著的数字。这只是一个概念证明,对于真正的研究,建议在较长的时间间隔内收集数据。

无论如何,结果很有趣。例如,我们可以使用一行代码找到那些大部分信息在早上发布的用户。我们还可以获取所有这些信息并将它们绘制在时间轴上:

df_users_filtered = df_users_[df_users_['morning'] >= 50]print(f"Users: {100*df_users_filtered.shape[0]/df_users_.shape[0]}%")df_filtered = df_[df_["user_name"].isin(df_users_filtered["user_name"])]draw_timeline(df_filtered, df_)

结果如下所示:

Users posting tweets in the morning, Image by author

有趣的是,这个数字只有约3%。作为比较,46%的活跃用户在白天发送超过50%的消息:

Users posting tweets during the day, Image by author

我们可以提出其他请求;例如,让我们找到80%的消息在晚上发布的用户:

df_users_filtered = df_users_[df_users_['evening'] >= 80]

结果如下所示:

Users posting tweets in the evening, Image by author

我们还可以显示一些用户的时间轴以验证结果:

for user_name in df_users_filtered[:5]["user_name"].values:    draw_user_timeline(df_, user_name)

输出如下所示:

Timeline of selected users, Image by author

结果可能很有趣;例如,“rhod***”用户在19:00之后几乎每次发布信息都是在同一时间。

我必须再次重申,这些结果并非最终结果。我只分析了发布了一周内10条或更多推文的活跃用户。但是,大量用户发布了较少的信息,为了收集更多关于他们的见解,应该在几周甚至几个月内收集数据。

结论

在本文中,我们能够获取特定语言(在我们的例子中为荷兰语)发布的所有Twitter信息。这种语言主要用于荷兰和比利时,这两个国家彼此相邻。这使我们可以知道用户的时区,但可惜的是,我无法从Twitter API中获取到此信息,至少使用免费帐户无法。通过对消息时间戳进行分析,我们可以获得很多有趣的信息,例如,可以找出更多用户在早上、工作时间或晚上活跃。找出用户行为中的时间模式可以对心理学、文化人类学甚至医学有用。数百万人使用社交网络,了解它如何影响我们的生活、工作节奏或睡眠是很有趣的。正如本文所示,使用简单的请求可以进行这种行为分析,它们实际上并不比学校的数学难。

看到社交网络可以存储多少数据也很有趣。我想大多数人从未考虑过有多少信息被发布。即使对于一个相对较小的荷兰语社区(全球约有2500万母语使用者),也可能会产生每秒10条以上的推文。在本文中,分析了来自1515139个用户的6487433条Twitter信息,而这些仅仅是在10天内发布的信息!对于像德国这样的大国,获取所有信息可能会超出免费Twitter开发帐户的限制。在这种情况下,可以通过用户位置过滤来结合不同的请求查询。

无论如何,社交网络是关于我们的信息的有趣来源,我祝愿读者在他们自己的实验中好运。对于那些有兴趣的人,也欢迎阅读有关使用K-Means算法对Twitter用户进行聚类的第一部分。另外,还解释了对Twitter帖子的NLP分析作为另一种方法。

如果您喜欢这个故事,请随时订阅 小猪AI,您将收到我的新文章发布的通知,以及来自其他作者的成千上万个故事的完全访问权限。

谢谢阅读。

Leave a Reply

Your email address will not be published. Required fields are marked *