赞
踩
在很多数据分析任务中,经常会遇到处理时间相关的数据。比如电商网站经常需要根据下单记录来分析不同时间段的商品偏好,以此来决定网站不同时间段的促销信息;又或者是通过对过去十年的金融市场的数据进行分析,来预测某个细分版本的未来走势。在这些任务中,时间信息的处理都是重中之重。
时间数据的处理不同于对常见的数字、字符串等数据的处理方式,时间数据处理起来往往会比较复杂。
比如数据表中有一个表示时间的字符串:"2018/02/01",我们希望提取其年、月、日,就需要去解析,分割该字符串。而往往我们会遇到各种不同格式的表示,比如"01/02/2018",或者 "2018-2-1", 等等。如果要完全实现针对不同格式的兼容,往往需要书写大量琐碎的代码。而这还只是最简单的提取年月日。其他比如时间的加减,都不是简单就能够完成的。
pandas 作为数据分析最强大的工具集,自然也提供了一套非常强大的处理时间数据的工具,本讲我们就来具体介绍。
pandas 提供了丰富的处理时间的工具和类,其中最常用的有以下几种。
Timestamp:代表某一个时间点。比如用户某个购物订单下单的时间,或者某次网页点击的时间。
DatetimeIndex:代表一个时间点的序列,换句话说就是多个 Timestamp 构成的列表。DatetimeIndex 可以作为 Series 和 DataFrame 的索引。
Timedelta:单个时长。比如 2 个小时,4 分钟等都算时长,时长具有不同的单位,常见的单位有天、时、分、秒等等。本质上,时长代表两点时间点(Timestamp)的距离。
TimedeltaIndex:多个时长数据的序列。类似 DatetimeIndex 和 Timestamp 的关系。TimedeltaIndex 就是多个 Timedelat 组成的列表,也可以作为 Series 和 DataFrame 的索引。
DataOffset:时间在日历维度的偏移。比如 2018 年 2 月 1 日早上 6 点,在日历上偏移一点就是 2018 年 1 月 31 日早上 6 点。DataOffset 提供了各种方便的偏移方式,比如按照工作日偏移。星期五早上 10 点,偏移一个工作日,可以自动返回下周一早上 10 点。
在使用 pandas 做时间处理的时候,最常见的场景就是:
将来自数据源的时间描述(比如字符串或者整型)等表示,转化为 Timestamp类型;
使用 Timestamp 类型来访问时间的各种属性,比如年月日、星期几等;
使用 Timestamp 配合 Timedelta 来做时间相关的计算和加减等,如果是在日历维度的计算,则配合 DataOffset 一起使用;
如果需要从时间的维度来筛选 DataFrame 里的记录,则需要先将时间列设置为 DatetimeIndex, 然后按照普通索引的用法通过时间来筛选。
接下来,我来逐一介绍下这 4 种场景的实现方式。
首先我们创建 chapter15 的文件夹,用 VS code 打开,并新建 chapter15.ipynb,保存到该文件夹中。
时间数据的解析本质就是将各种不同类型的时间表示都统一转换为 pandas 的 Timestamp 类型。因为只有转换为 Timestamp 之后才能进行后续的操作。
pandas 提供了 to_datetime 方法,来将各种不同类型的时间数据转换为 Timestamp 类型。
字符串是最常见的数据源中存储时间的方式,to_datetime 函数近乎支持所有主流的时间字符串标记法,比如:
import pandas as pd
# 常见的日期+时间的表示方法
pd_time = pd.to_datetime("2018-08-29 17:17:22")
print(type(pd_time),pd_time)
# 时间简写,并用12小时制的表示方法
pd_time1 = pd.to_datetime("2018-08-29 5:17pm")
print(type(pd_time1), pd_time1)
# / 表示法
pd_time2 = pd.to_datetime("08/29/2018")
print(type(pd_time2), pd_time2)
# 结合英文月份的表示方法
pd_time3 = pd.to_datetime("Aug 29, 2018")
print(type(pd_time3), pd_time3)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
执行之后,输出:
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2018-08-29
2018-08-29 17:17:00
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2018-08-29 00:00:00
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2018-08-29 00:00:00
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2018-08-29 00:00:00
- 1
- 2
- 3
- 4
- 5
从上面输出的结果可以看到,to_datetime 函数返回的是 Timestamp 类型。并且该函数默认就支持从常见的用字符串表示的时间格式中解析出 Timestamp 结构。
如果我们想解析的时间字符串不是常见的类型呢?比如中文环境中,类似“2018 年 8 月 29 日”这样的表示方法还是会经常遇到的。答案是可以的。
to_datetime 支持我们自定义时间格式字符串来进行解析。在时间格式字符串中,%Y 表示年份,%m 代表月,%d 代表日。
比如要解析刚才的中文时间,对应的格式字符串就是: "%Y年%月%日"。代码如下:
# 使用自定义格式字符串解析任意时间字符串
pd_time4 = pd.to_datetime("2018年8月29日", format="%Y年%m月%d日")
print(type(pd_time4), pd_time4)
- 1
- 2
- 3
执行之后,输出如下。因为我们没有指定时分秒,所以这个部分默认为 0 。
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2018-08-29 00:00:00
- 1
在很多数据系统中,时间也经常以时间戳的形式存在。时间戳一般指的是 1970 年 1 月 1 日到某个时间点的秒数。比如一个特定的时间点:北京时间的 2021-05-09 21:06:44, 对应的时间戳就是:1620565604,代表从 1970 年 1 月 1 日零时零分零秒到 2021 年 5 月 29 日下午 9 点 6 分 44 秒一共有 1620565604 秒。
to_datetime 同样支持直接将时间戳转换为 Timestamp 类型,用法如下:
time_value = 1620565604
# 将数字时间戳转换为 Timestamp 类型,并指定单位为秒
pd_time5 = pd.to_datetime(time_value, unit="s")
print(type(pd_time5), pd_time5)
- 1
- 2
- 3
- 4
输出:
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2021-05-09 13:06:44
- 1
Timestamp 对象已经正确构建,但是为什么是 13 点 06 分,而不是刚才的 21 点 06 分? 原因是通过 to_datetime 默认是格林威治时间,也就是零时区,落后北京时间 8 小时。如果算上 8 小时的偏移,13+8 就正好是 21 点 06 分了。如果我们希望在构造 Timestamp 对象时就指定时区,可以调用 tz_localize 指定。
# 转换时间戳并指定时区
pd_time6 = pd.to_datetime(time_value, unit="s").tz_localize("Asia/Shanghai")
print(type(pd_time6), pd_time6)
- 1
- 2
- 3
输出:
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2021-05-09 13:06:44+08:00
- 1
可以看到,这次输出的内容多了一个 +08:00 代表已经带上了时区。
除了上述两种方式外,我们可以直接构建 Timestamp 对象。比如通过指定年月日,或者直接获取程序运行的时间。主要包括以下用法:
# 通过单独指定年月日等信息来创建 Timestamp 对象
pd_time7 = pd.Timestamp(year=2018, month=8, day=29, hour= 21)
print(type(pd_time7), pd_time7)
# 获取当前的时间
pd_time8 = pd.Timestamp("now")
print(type(pd_time8), pd_time8)
- 1
- 2
- 3
- 4
- 5
- 6
输出:
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2018-08-29 21:00:00
<class 'pandas._libs.tslibs.timestamps.Timestamp'> 2021-05-09 21:54:38.064474
- 1
- 2
当我们获取到 Timestamp 对象之后,就可以通过 Timestamp 对象提供的方法来轻松获取各种时间的属性了。常见的属性获取方法如下所示:
print("当前时间对象:", pd_time8)
print("星期几,星期一为0:", pd_time8.dayofweek)
print("星期几,字符串表示:", pd_time8.day_name())
print("一年中的第几天:", pd_time8.dayofyear)
print("这个月的有几天:",pd_time8.daysinmonth)
print("今年是否是闰年", pd_time8.is_leap_year)
print("当前日期是否是本月最后一天", pd_time8.is_month_end)
print("当前日期是否是本月第一天", pd_time8.is_month_start)
print("当前日期是否是本季度最后一天", pd_time8.is_quarter_end)
print("当前日期是否是本季第一天", pd_time8.is_quarter_start)
print("当前日期是否是本年度最后一天", pd_time8.is_year_end)
print("当前日期是否是本年度第一天", pd_time8.is_year_start)
print("当前第几季度:", pd_time8.quarter)
print("当前的时区:", pd_time8.tz)
print("本年第几周:", pd_time8.week)
print("年:", pd_time8.year)
print("月:", pd_time8.month)
print("日:",pd_time8.day)
print("小时:", pd_time8.hour)
print("分钟:", pd_time8.minute)
print("秒:", pd_time8.second)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
输出:
当前时间对象: 2021-05-09 21:54:38.064474
星期几,星期一为0: 6
星期几,字符串表示: Sunday
一年中的第几天: 129
这个月的有几天: 31
今年是否是闰年 False
当前日期是否是本月最后一天 False
当前日期是否是本月第一天 False
当前日期是否是本季度最后一天 False
当前日期是否是本季第一天 False
当前日期是否是本年度最后一天 False
当前日期是否是本年度第一天 False
当前第几季度: 2
当前的时区: None
本年第几周: 18
年: 2021
月: 5
日: 9
小时: 21
分钟: 54
秒: 38
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
使用方法比较直观,这里就不展开解释。
pandas 中,时间数据的计算值的是时间数据的加减,比如在一个时间点上增加几小时、几分钟、或者几天,几个月来得到加了之后的时间。因为时间并不像数字运算一样简单,而是有很多潜在的规则在里面,比如一分钟 60 秒,一小时 60 分钟,一天 24 小时,一个月可能有 28 天,也可能有 30、31 天,等等,如果我们手写计算逻辑将会非常复杂。
pandas 提供了一套强大的时间计算机制来让我们不用关系背后的规则就能完成时间的计算。pandas 的时间计算是通过 Timestamp 对象和 Timedelta 对象混合运算来实现。Timedelta 可以理解成一个时间段,或者说,时间长度。最常见的运算有以下两种类型:
两个 Timestamp 对象相减,可以得到一个 Timedetla 对象;
一个 Timestamp 对象 加上或者减去一个 Timedelta 对象,可以获得一个新的 Timestamp 对象。
所以要实现时间的运算,我们首先要创建 Timedelta 对象。
Timedelta 对象和 Timestamp 对象类似,也支持多种形式的创建。
(1) 从字符串来创建
Timedelta 对象支持解析多种描述时长的格式。我们通过代码来展示:
delta1 = pd.Timedelta('0.5 days')
print("半天:", delta1)
delta2 = pd.Timedelta("2 days 3 hour 20 minutes")
print("2天零3小时20分钟", delta2)
delta3 = pd.Timedelta("1 days 20:36:00")
print("1天零8小时36分钟:", delta3)
- 1
- 2
- 3
- 4
- 5
- 6
执行之后输出:
半天: 0 days 12:00:00
2天零3小时20分钟 2 days 03:20:00
1天零8小时36分钟: 1 days 20:36:00
- 1
- 2
- 3
(2)从单元时间创建
除了通过一定格式的字符串来创建 Timedelta 对象之外,我们还可以通过设置函数的参数来创建 Timedelta 对象,比如这样表示:
delta4 = pd.Timedelta(days = 1.5)
print("1天半:", delta4)
delta5 = pd.Timedelta(days = 10, hours= 9)
print("十天零九小时:", delta5)
- 1
- 2
- 3
- 4
输出:
1天半: 1 days 12:00:00
十天零九小时: 10 days 09:00:00
- 1
- 2
(3)从时间缩写创建
还有一种简洁的形式来创建 Timedelta,就是通过数字+缩写的形式。缩写主要有以下几种:
W:代表周、星期
D:代表天
H:代表小时
M:代表分钟
S:代表秒
具体使用方法如下:
delta6 = pd.Timedelta("2W3D")
print("两周零三天:", delta6)
delta7 = pd.Timedelta("6H30M12S")
print("6小时30分钟12秒:", delta7)
- 1
- 2
- 3
- 4
输出
两周零三天: 17 days 00:00:00
6小时30分钟12秒: 0 days 06:30:12
- 1
- 2
在学会如何创建 Timedelta 对象之后,要做时间的计算就非常简单了。我们直接上代码:
# 获得当前的时间
current_time = pd.Timestamp("now")
print("当前时间:", current_time)
# 获得当前时间减去两周的时间
two_week_ago = current_time - pd.Timedelta("2W")
print("两周前:", two_week_ago)
# 获得当前时间30天零7小时之后的时间
future_time = current_time + pd.Timedelta("30D7H")
print("30天零7小时之后的时间:",future_time)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
执行之后,输出:
当前时间: 2021-05-09 23:46:09.346063
两周前: 2021-04-25 23:46:09.346063
30天零7小时之后的时间: 2021-06-09 06:46:09.346063
- 1
- 2
- 3
除了计算 Timedelta 和 Timestamp 外,两个 Timestamp 也能相减,得出一个时长(也就是 Timedelta)。
# 创建去年国庆节上午八点的时间
national_day = pd.to_datetime("2020-10-01 08:00:00")
# 计算当前时间和国庆时间的 Timedelta
delta8 = current_time - national_day
print("距离去年国庆已经过了:", delta8)
- 1
- 2
- 3
- 4
- 5
执行之后,输出:
距离去年国庆已经过了: 220 days 15:46:09.346063
- 1
除了两个时间点的各种操作之外, pandas 还支持将时间数据作为索引,这样就能够支持各种时间维度的选择。为什么这个特性非常重要,我们以一个例子来说明。
首先从课程的 Github 仓库的 chapter15 目录中,下载 order_record.csv 文件,并将其保存在你 chapter15 的工作目录中。
我们首先先将数据集加载出来,看看里面有什么:
# 加载 order_record.csv 文件
df_log = pd.read_csv("order_record.csv")
# 查看 DataFrame
df_log
- 1
- 2
- 3
- 4
输出:
这是一个电商网站用户购买的记录数据,一共有一千条内容。从最后一列时间列来看,时间跨度在 2018 年 6 月到 11 月都有。
如果我们希望能够方便地进行时间维度的分析,比如查看 9 月 1 日到 9 月 15 日的记录,或者 8 月到 9 月的记录。那可以考虑将 time 一列转化为 DatetimeIndex。这样我们就能够直接对时间进行索引。
将字符串的时间一列转化为 DatetimeIndex, 一般分为两步:第一步首先将时间一列转化为 Timestamp 对象。
# 将 time 列转化为 Timestamp对象
df_log["time"] = pd.to_datetime(df_log["time"])
# 查看 time 列
df_log["time"]
- 1
- 2
- 3
- 4
执行之后输出:
0 2018-08-29 17:17:22.300959410
1 2018-08-29 20:59:58.841378430
2 2018-08-01 19:20:06.479644547
3 2018-08-01 17:25:58.912202131
4 2018-06-02 11:00:51.123221777
...
995 2018-11-08 11:10:13.586269568
996 2018-11-08 19:10:55.214335543
997 2018-11-08 16:54:37.687285776
998 2018-11-08 17:46:17.253211617
999 2018-06-26 20:38:07.950590072
Name: time, Length: 1000, dtype: datetime64[ns]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
可以看到,目前 time 列的数据类型已经转换为 Timestamp。
第二步就是将新的 time 这一列设置成索引。
# 设置 time 一列为 df_log 的索引
df_log.set_index("time", inplace=True)
# 查看最新的 DataFrame
df_log
- 1
- 2
- 3
- 4
执行之后,输出:
可以看到,现在时间列已经替代了之前默认的数字序号,成为 DataFrame 新的行索引。
现在我们可以查看一下 DataFrame 的索引类型。
df_log.index
- 1
输出:
DatetimeIndex(['2018-08-29 17:17:22.300959410',
'2018-08-29 20:59:58.841378430',
'2018-08-01 19:20:06.479644547',
'2018-08-01 17:25:58.912202131',
...
'2018-11-08 16:54:37.687285776',
'2018-11-08 17:46:17.253211617',
'2018-06-26 20:38:07.950590072'],
dtype='datetime64[ns]', name='time', length=1000, freq=None)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
可以看到,目前 df_log 表的索引就是我们开头介绍的 DatetimeIndex 类型。
在设置完 DatetimeIndex 之后,我们在之前提到的根据时间维度筛选就小菜一碟了。我们直接可以使用之前学习的 loc 索引器, 然后在行索引部分以字符串的形式写时间范围(开始时间和结束时间之间以冒号链接),具体用法见代码:
(1)选择从 9 月 1 日到 9 月 15 日的数据
df_log.loc["2018-09-01" : "2018-09-15",:]
- 1
输出(只截取了部分):
(2)选择从 8月到9月的数据
df_log.loc["2018-08" : "2018-09", :]
- 1
输出:
(3)选择从 8 月 1 日到 9 月 2 日下午两点之前的数据
df_log.loc["2018-08-01" : "2018-09-02 14:00:00", :]
- 1
输出:
可以看到,当我们把 Timestamp 作为索引时,就可以非常简单地实现各种不同时间范围的筛选,并且时间范围的写法也非常自由。
关于时间常用的处理技术至此就学习完毕了,我们来复习一下今天学习的内容。
pandas 的时间处理体系主要包含这几个类。
Timestamp 代表时间点。
DatetimeIndex 代表多个 Timestamp构成的索引列表。
Timedelta 代表时间长度,用于做时间的计算
TimedeltaIndex 用于将 Timedelta做索引,但不常用。
通过 to_datetime 函数,可以将各类时间字符串、时间戳等表示形式转换为Timestamp 对象。同时也可以自定义时间格式字符串,用%Y、%m、%d 等格式字符来自定义解析。
Timestamp 对象提供了丰富的访问时间各种维度信息的能力,比如当前时间是星期几、在一年中是第几天,等等,具体见上面的示例代码。
在某个时间点上加减时间,需要用 Timedelta 对象来描述时间的长度。同样,Timedelta 对象也能从各种不同的数据生成,比如字符串、单位时间等。Timedelta 同时也可以表示两个 Timestamp 相减后的差。
当我们希望从时间维度去筛选数据表中的数据的时候,可以将时间相关的列转换成 DatetimeIndex, 这样可以在行索引中直接写时间范围来筛选数据,非常方便。
学完了本讲,我们 pandas 相关的学习已经进入了尾声,是不是已经迫不及待想要用 pandas 做一个略微复杂的练习了呢? 下一讲我们将会融合最近几讲学习的内容,完成一个较为完整的数据分析。
思考题
思考一下,Timedelta 为什么不能按月创建?
答案:
Timedelta 代表一个绝对的时间长度,而一个月的天数是不固定的。
在互联网行业中,要说哪一个领域是数据分析最发光发热的地方,那就是电子商务领域。绝大多数电商网站,都强依赖于数据分析帮助其挖掘新的用户与订单增长机会。
典型的应用场景如通过数据分析发现不同类型用户对于商品、店铺的偏好,从而进行针对性的推荐,又或者通过数据分析发现用户未来潜在的购买概率,以决定是否要给用户发放红包或者优惠券来引导用户下单。
无论是哪个方向,数据分析都能显著提升业务的效果,是电商业务不可或缺的一环。
今天,我就带你利用本章节学到的 pandas 知识,来对一个真实的电商数据集进行分析。一方面,我们可以系统性地复习 pandas 知识,同时你也可以亲自感受下电商数据分析实战项目的分析流程。
阿普尔星球最大的电商网站阿普闪购计划策划一场推广活动,通过发短信的形式,向潜在的用户发送广告和优惠信息,吸引他们来阿普闪购注册并购物。
由于预算以及短信服务商的限制,没有办法对大范围的用户投放,这就需要缩小人群的范围,找出最有可能产生转化的人群。此外,人们的下单行为往往也和时间呈现一定的相关性,什么时候推送营销短信也很重要。
现阶段你作为阿普闪购的数据分析师,这个任务自然就落到了你的肩上,你的目标是:
通过数据分析,找到最有可能转化的人群特征(年龄、性别、地域)等;
通过数据分析,决定出最有利于转化的营销短信投放时间。
通过和多个部门的沟通,最终你从阿普闪购的数据部门申请到了如下数据权限。
用户行为表:最近 6 个月的用户行为记录。
VIP 会员表:用户 VIP 会员开通情况。
用户信息表:用户的相关信息。
你可以从这个链接,下载本次案例所用到的数据集。
解压之后可以看到三个文件:
user_behavior_time_resampled.csv
代表用户行为表;
vip_users.csv
代表 VIP 会员表;
user_info.csv
代表用户信息表。
csv 文件是 Excel 兼容的,你可以通过 Excel 打开,看到每个表的信息如图所示。
每一个表的第一行是表头,剩下的就是具体数据。每个表头的含义“字如其名”,很好理解。
首先是用户行为表, user_behavior_time_resampled.csv
。
然后是 VIP 会员表,vip_user.csv
最后,再看用户信息表, user_info.csv
看完了上述数据集的描述,细心的你恐怕已经发现用户行为( user_log )表中存在的小问题:有两个时间相关的字段,一个叫 time_stamp,一个叫 timestamp。 这两个字段的关系是什么?哪个能代表正确的时间?目前没有更多的资料。我们将在后续从数据本身尝试对这两个字段进行分析与解读。
接下来我们就通过 pandas 加载这些数据,来进行初步分析。
首先在你的工作目录中,新建一个 chapter17 目录,用来保存本讲实战的相关内容。之后在该目录中新建目录:data_set,用于保存下载到的数据集。然后,我们把上面下载的三个 csv 文件拷贝到 chapter17/dataset 目录。
接下来,我们开始对这些数据文件进行分析。
打开 VS Code,按【CTRL+P】 调出命令面板,输入 >Jupyter: ,在列表中选择 【Create New Blank Jupyter Notebook】来新建一个 Notebook,之后按【CTRL + S】保存为 chapter17.ipynb。
结合我们这一章的内容,对于数据集的加载和分析,最核心就是要使用 pandas 工具包,所以首先新建一个 Cell,导入必要的工具:pandas。
import pandas as pd
- 1
输入之后,运行 Cell(Shift + Enter)。运行之后,pd 这个变量就代表了 pandas 。这样我们后续就可以使用 pd
调用 pandas 的函数了。
现在我们需要加载 csv 文件。之前我们的课程有介绍过,加载 csv 文件可以使用 pandas 的 read_csv 函数,该方法可以接收一个文件路径,返回 pandas 的 DataFrame。
在这里,我们用三个 DataFrame 去加载这三个 csv 文件。新建 Cell,输入如下代码,并执行。
df_user_log = pd.read_csv("EComm/user_behavior_time_resampled.csv")
df_vip_user = pd.read_csv("EComm/vip_user.csv")
df_user_info = pd.read_csv("EComm/user_info.csv")
- 1
- 2
- 3
这几行代码的执行时间一般取决于 csv 文件本身的大小,由于用户行为表的数据量较多,所以会执行一段时间。当 Cell 的序号部分由 [*] 变为 [4] 的时候,说明 Cell 已经执行完毕。
执行中:
执行完毕:
接下来我们需要看一看加载之后的 DataFrame 的大概情况。前面的内容里介绍过,查看 DataFrame 的方法非常简单,只需要把 DataFrame 的变量放在 Cell 的最后一行并运行 Cell, Notebook 会自动打印出该 DataFrame 的概述。
现在,我们来分别查看一下这三个 DataFrame。新建 Cell,输入以下代码并执行。
df_user_log
- 1
Notebook 会有如下输出:
可以看到,df_user_log 一共包含 10,985,066 条记录。
同样的方法分别查看 df_vip_user 与 df_user_info, 代码与结果如下图所示。
df_vip_user
- 1
输出结果:
df_user_info
- 1
输出结果:
从上面 3 个 notebook 输出的表格摘要,我们对于三个表的数据量级有了一个大概的认识。
首先是量级上,user_log 表的记录是最多的,有 1098 万行,user_info 表则有 26 万行,vip_user 表则有 42 万行。
其次,三个表都包含 user_id 这一列,代表后续我们可以基于 user_id 来做一些跨表联动分析。
还记得之前讲到,数据集中往往有缺失的数据,在 Notebook 中打印出来的时候一般显示 NaN。上面 vip_user 表的概述也出现了 NaN,代表存在缺失的字段。所以在后面的步骤里我们还会针对每个表都进行缺失字段的分析与处理。
前面的内容,我们提到目前 user_log 表中存在两个时间戳字段,比较迷惑。这两个字段究竟代表什么呢?
我们还是再看一次 user_log 表的概要,如下图所示。
可以看出,两个时间戳字段 time_stamp 和 timestamp 的值是不一样的。 time_stamp 看起来是个整数,值比较小,而 timestamp 看起来是个浮点数,值比较大,普遍在五位数,其他暂时看不出什么有用的信息。
这个时候,对于不明确含义的字段,通常的做法是先看一下它的边界(即最大值和最小值)。
新建 Cell,输入以下代码并执行。下面的代码分别通过 pandas 的 max 函数和 min 函数,获取了 time_stamp 与 timestamp 的最大值与最小值,并将通过在 print 函数中打印出来,并且因为打印的时候要和提示字符串进行拼接,所以 max 和 min 函数返回的结果都使用 str 函数进行了类型转换,将整数转换为了字符串。
time_stamp_max = str(df_user_log['time_stamp'].max())
time_stamp_min = str(df_user_log['time_stamp'].min())
print("time_stamp max: " + time_stamp_max, "time_stamp min: " + time_stamp_min)
timestamp_max = str(df_user_log['timestamp'].max())
timestamp_min = str(df_user_log['timestamp'].min())
print("timestamp max: " + timestamp_max, "timestamp min: " + timestamp_min)
- 1
- 2
- 3
- 4
- 5
- 6
输出以下结果:
time_stamp max: 1112, time_stamp min: 511
timestamp max: 86399.99327792758, timestamp min: 0.10787397733480476
- 1
- 2
可以看到,time_stamp 的最大值为 1112,最小值为 511,而 timestamp 的最大值为 86399.99 最小值为 0.1。
从数据集的描述中,用户行为表是用户 6 个月的行为,那 time_stamp 最大 1112,最小 511 看起来就特别像日期。代表最小日期是 5 月 11 日,最大日期是 11 月 12 日。
那既然 time_stamp 是日期,那 timestamp 会不会是具体的时间呢?timestamp 的最大值为 86399 ,而一天最大的秒数为 24*3600 = 86400。两个数字非常接近,那基本可以认定 timestamp 代表的是一天中的第几秒发生了这个行为。
破解了两个时间字段的问题,为了避免后面有歧义,我们将 time_stamp 列重命名为 date。
新建 Cell,输入以下代码并执行。
df_user_log.rename(columns={'time_stamp':'date'}, inplace = True)
df_user_log
- 1
- 2
重命名 DataFrame 的某一列的名字,可以使用 rename 函数,第一个参数代表要重命名的列的映射关系,这里表示把 time_stamp 重命名为 date,第二个参数 inplace = True 代表在当前 DataFrame 中生效。
输出结果为:
可以看到,原来的 time_stamp 列已经被改为 date。
在现实世界中,除了字段的含义可能有不对之外,数据集本身也会有缺失的情况,比如某些条记录缺少字段之类的。所以在正式分析之前,我们都需要对这些缺失值进行处理。
在前面的章节中,我们已经学过使用 isna 函数来获得缺失值,pandas 同时还提供了另一个类似的函数 isnull,这次我们来使用这个函数完成任务。接下来我们就使用这个工具来分析我们的三个表。
首先从 user_log 表开始,新建 Cell, 输入以下代码并执行。
df_user_log.isnull().sum()
- 1
结果输出如下所示。
user_id 0
item_id 0
cat_id 0
seller_id 0
brand_id 18132
date 0
action_type 0
timestamp 0
dtype: int64
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
从上述结果中可以看出,user_log 表中大概有 1.8 w 条数据缺少品牌 id 的字段,缺失率为0.16%(1.8w/1098w),一般这个数据量级不会影响到数据分布的分析,暂时不处理。
接下来用同样的方法分析 user_info 表。
df_user_info.isnull().sum()
- 1
结果输出如下所示。
user_id 0
age_range 2217
gender 6436
dtype: int64
- 1
- 2
- 3
- 4
从上述结果可以看出, user_info 表中有 2217 条记录缺失了年龄字段,有 6436 条记录中缺失了性别字段。
在后续的分析中,我们会尝试分析目标用户群性别和年龄的分布,所以这里需要对这些缺失的值进行处理。由于缺失的数据量较少,所以我们选择直接删掉这些有缺失的记录。
在章节的前面部分,我们讲过缺失值的常见处理方式,对于删除缺失值,可以使用 dropna 函数,所以这里我们可以轻松完成这个任务。如下所示。
df_user_info = df_user_info.dropna()
df_user_info
- 1
- 2
结果输出为:
可以看到 user_info 的表记录数变为了 417708(之前是 424170,减少了 6462 条记录)。现在,我们通过上述方法排除了字段值为 NULL 的记录。
但你还记得我们对 user_info 表中年龄字段的描述吗?age_range 为 0.0 或者 gender 为 0 同样代表数据缺失。我们来看一下这部分的数据有多少。
print(df_user_info.loc[df_user_info["age_range"] == 0.0, ["user_id"]].shape)
print(df_user_info.loc[df_user_info["gender"] == 0.0, ["user_id"]].shape)
- 1
- 2
输出:
(90638, 1)
(285634, 1)
- 1
- 2
第一行代表年龄为空的记录数,第二行代表性别为空的记录数。整体的比例并不低,所以这里我们选择暂时保留这些数据,在后续分析环节再进行过滤。(因为当某条记录性别为空年龄不为空时,对于分析年龄分布仍然有价值,反之亦然)。
接下来,我们同样使用 isnull 函数来分析 vip_user 表。
df_vip_user.isnull().sum()
- 1
输出:
user_id 0
merchant_id 0
label 0
dtype: int64
- 1
- 2
- 3
- 4
从输出结果来看, vip_user 这个表没有数据缺失,不用清理。
接下来,就进入了我们本次分析的核心环节,数据分布分析。
我们希望从数据分布中分析出最有可能转化的用户的特征。直白地说,我们希望分析出目前阿普闪购的用户中,什么年龄段的用户最多,什么性别的用户最多。进一步,希望分析出下单的用户里,年龄和性别的分布,这样才能知道给哪个年龄段和哪个性别的用户发营销短信是最有用的。
这里我们会用到一个非常强大的 pandas 函数:value_counts,value_counts 一般是针对 Series 对象,用于统计 Series 中有多少个不同的值,以及每个值出现的次数。比如:
ser1 = pd.Series([1,1,3,3,3,3,4,5])
ser1.value_counts()
- 1
- 2
输出为:
3 4
1 2
5 1
4 1
dtype: int64
- 1
- 2
- 3
- 4
- 5
左边是值,右边是出现的次数,并且按照出现的次数从高到低排序。
通过 value_counts 函数可以非常便捷地实现某个字段的分布分析。
第一步,我们先分析用户表中用户的年龄段分布。如上文所说,数据分布可以直接使用 DataFrame 的 value_counts 函数。
df_user_info.age_range.value_counts()
- 1
输出
3.0 110952
0.0 90638
4.0 79649
2.0 52420
5.0 40601
6.0 35257
7.0 6924
8.0 1243
1.0 24
Name: age_range, dtype: int64
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
除开未知的数据不看(假设未知的部分的分布符合整体的分布),可以发现,除去取值为 0 (代表未知)之外,取值为 3.0 和 4.0 的占到绝大多数,回忆上面数据集的定义,取值为 3 代表 25~30 岁,取值为 4 代表 30~34 岁。所以年龄在可以得到, 25~34 岁之间的用户占绝大多数。(age_range 取值 3.0、4.0)。
然后,我们可以通过代码计算出 25~34 岁用户的比例。首先通过 loc 函数筛选出所有不等于 0 的记录,然后计算年龄段等于 3.0 与年龄段等于 4.0 的总数除以所有非 0 记录的总数,计算的代码如下:
user_ages = df_user_info.loc[df_user_info["age_range"] != 0, "age_range"]
user_ages.loc[(user_ages == 3) | (user_ages == 4) ].shape[0] / user_ages.shape[0]
- 1
- 2
输出
0.5827529275078729
- 1
可以看到,25~34 岁用户占到了 58% 的比例。
接下来,我们使用 value_counts 来计算用户信息中性别的分布信息。
df_user_info.gender.value_counts()
- 1
输出
0.0 285634
1.0 121655
2.0 10419
Name: gender, dtype: int64
- 1
- 2
- 3
- 4
结合之前的数据集定义,0 代表女性,1 代表男性,2 代表未知。可以看到,阿普闪购的核心用户群是女性,是男性数量的 2.35 倍。
从用户群体的分析上,我们大概已经勾勒出我们的目标用户画像,25~34 岁之间的女性群体。但目前分析的只是注册用户的信息,会不会存在男性虽然注册用户少但是购买力却更强呢?为了验证这个假象,我们就需要结合订单数据来分析。
分析完了独立的用户信息表后,我们希望分析不同用户群的下单行为特征。但是下单量在 user_log 表中,而用户信息在 user_info 表中,这样的话就不能简单地用 value_counts 来统计了,通过观察两个表的表结构不难发现,它们有共同的字段:user_id。
像这样的场景,我们可以通过 user_id 这样的共同字段来把两个表合并在一起。
df_user_log = df_user_log.join(df_user_info.set_index('user_id'), on = 'user_id')
df_user_log
- 1
- 2
上述代码将 user_info 表合并到 user_log 表,通过 user_id 的字段进行关联,之后第二行我们查看最新的 user_log 表,显示如下。
可以看到,user_info 表的 age_range、gender 字段已经被合并到了 user_log 表中。接下来,我们就可以分析下单用户的性别与年龄特征了。
首先我们需要过滤出下单用户,在第 13 课时介绍过,我们可以使用 loc 函数来过滤出符合某个条件的记录。要过滤下单用户,集合数据集的定义,就是 action_type = order。 我们搭配 loc 函数与 value_counts 函数,即可实现针对下单用户的年龄段分析。
df_user_log.loc[df_user_log["action_type"] == "order", ["age_range"]].age_range.value_counts()
- 1
输出:
3.0 172525
4.0 153795
0.0 114908
5.0 79298
6.0 61534
2.0 59072
7.0 10785
8.0 1924
1.0 21
Name: age_range, dtype: int64
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
虽然 user_log 表中有很多行为,但这里我们核心关注的还是最终的转化:下单,所以我们筛选了行为为 “order”的记录,查看其年龄段分布。可以看到,下单的年龄段分布和用户信息的分布基本一致,25-34 岁的人占到 59.9%。
接下来,我们进行下单用户的性别分析。
df_user_log.loc[df_user_log["action_type"] == "order", ["gender"]].gender.value_counts()
- 1
输出
0.0 467381
1.0 161999
2.0 24482
Name: gender, dtype: int64
- 1
- 2
- 3
- 4
可以看到,女性用户不仅注册用户远超过男性,购买力同样惊人,是男性下单量的 2.9 倍。基本可以确定,我们发送营销信息要聚焦的用户群应该是 25~34 岁的女性。
用户群确定了之后,下一步要确定发送的时间。接下来,我们需要分析用户下单的时间特征。
虽然我们可以直接对 date 字段进行 value_count,但我们更希望是找到一个适合投放的日期范围,而不是具体某一天。所以这里我们使用分组的 value_count, 下面的代码我们将所有日期分成六组,统计每组的订单量的分布。
对 value_counts 的结果进行分组,我们之前介绍过,可以通过给 value_counts 函数增加 bins 参数,bins 的值代表要分成几组,之后的数据分布就会按组输出。在这里,因为数据集是半年的,所以我们分六组(看每个月的分布)。
df_user_log.loc[df_user_log["action_type"] == "order", ["date"]].date.value_counts(bins = 6)
- 1
输出
(1011.0, 1111.0] 333721
(811.0, 911.0] 70699
(911.0, 1011.0] 69427
(510.399, 611.0] 68776
(611.0, 711.0] 62901
(711.0, 811.0] 54053
Name: date, dtype: int64
- 1
- 2
- 3
- 4
- 5
- 6
- 7
可以看到,用户在 10月中下旬一直到 11 月上旬这个时间段,下单量较为集中。分析完了日期分布后,接下来我们分析一下一天中的时间段的分布。
timestamp 字段存储了每条记录下单的时间,从当天零点开始累积的秒数。并不是很直观,我们更希望可以基于小时级的数据去分析。所以我们考虑基于 timestamp 这一列,新创建一列时间,来表示小时。
根据高级索引一讲我们学习的内容,新建列只需要直接给对应的列名赋值即可,值需要是一个合法的 Series。在这里,我们就是以 timestamp 列为基础,将其值除 3600, 然后用这个值创建一个新的列:time_hours_view.
df_user_log.loc["time_hours_view"] = df_user_log["timestamp"]/3600
df_user_log
- 1
- 2
输出:
从图中可以看到,我们的 time_hours_view 已经被添加到了表格的最后一列。
接下来的事情就比较简单了,我们直接用 value_count 来统计新增的 time_hours_view 字段,就可以实现对一天中的小时级分布进行分布统计。我们以两个小时为尺度,来查看分布,所以分为 12 组。
df_user_log.loc[df_user_log["action_type"] == "order", ["time_hours_view"]].time_hours_view.value_counts(bins = 12)
- 1
输出
(20.0, 22.0] 94209
(22.0, 24.0] 91529
(18.0, 20.0] 91330
(16.0, 18.0] 85681
(14.0, 16.0] 75372
(12.0, 14.0] 63580
(10.0, 12.0] 50909
(8.0, 10.0] 38938
(6.0, 8.0] 27962
(4.0, 6.0] 19428
(2.0, 4.0] 12639
(-0.025, 2.0] 8000
Name: time_hours_view, dtype: int64
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
从上述结果可以看到,晚上 8 点到 10 点之间是下单最为密集的,订单量为 94209 单。
至此,我们本次短信营销的目标人群和时间就已经基本分析完毕了。我们应该针对 25~34 岁的女性,在 10 月中下旬到 11 月中旬的晚上 8 点到 10 点进行短信的批量发送,这样应该可以收获最好的转化效率。
让我们一起来回顾一下这次任务的方法步骤:
认真阅读数据源描述;
结合数据源描述和观察数据集的字段,以及抽样查看一些记录,查看是否有错误的地方;
筛选数据表中的缺失值以及结合缺失值的量级决定保留或者抛弃;
针对原始表进行数据分布分析(user_info 表);
多个表联合查询数据分布(user_info 联合 user_log 表);
对于连续值, 通过 bins 参数实现分段统计。
再回顾一下,我们这个案例中用到的关键技术:
解决 timestamp 问题时,我们通过 max 函数和 min 函数,分析了部分列的边界;
清洗数据时,我们使用 isnull 函数来帮助我们发现缺失值,使用 dropna 函数来删除包含缺失值的记录;
在分布分析时,我们使用 value_counts 来计算某个字段的数据分布,并且针对部分数据范围很大的字段,使用 bins 参数来对分布结果进行分组;
当我们需要将具备共同字段的多张表拼接到一起时,使用 join 函数,并且在参数中通过 set_index 指定共同字段;
在部分字段不方便直接分析时,比如一天中的秒数,我们可以通过 df.apply 来将其转化为一个易于分析的新字段,比如我们将其转换为小时级的数值;
在需要数据筛选时,使用 loc 函数配合条件,可以筛选出 DataFrame 中符合条件的一个子集,比如 user_log 表中,我们仅关注下单的记录,所以就可以通过 loc 函数配合 action_type=="order" 筛选出相应的记录。
这次,我用了最基础的基于数据分布的分析法来进行分析,其实还有其他的办法,你知道哪些更好的方法与思路?可以写在留言区与大家分享。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。