
本教程旨在解决如何高效、准确地统计Pandas DataFrame中多列日期数据在特定时间范围内的行数,这在分析多阶段流程数据时尤为常见。文章将深入探讨常见的错误做法及其原因,并提供一个基于Pandas向量化操作的优化解决方案,以实现精准的日期范围筛选和计数,最终构建出适用于漏斗分析等场景的汇总表格。
在业务流程分析中,我们经常会遇到跟踪一个记录(例如客户、订单)在不同阶段进入时间的需求。数据通常以DataFrame的形式存储,其中每一列代表一个流程阶段,每个单元格记录了该记录进入该阶段的日期。我们的目标是统计在给定日期范围内,每个阶段有多少条记录的日期落入此范围,以便进行漏斗分析,比较不同阶段的转化或流失情况。
例如,给定以下DataFrame,它展示了三条记录在三个阶段的进入日期:
| stage 1 | stage 2 | stage 3 | |
|---|---|---|---|
| row 1 | 1/3/2023 | 4/3/2023 | 5/7/2023 |
| row 2 | 2/5/2023 | 2/6/2023 | 3/4/2023 |
| row 3 | 1/15/2023 | 6/3/2023 | 7/8/2023 |
我们希望得到一个汇总表,显示不同日期范围内每个阶段的计数,例如:
| stage 1 | stage 2 | stage 3 | |
|---|---|---|---|
| 1/1/2023 - 4/1/2023 | 2 | 1 | 1 |
| 4/1/2023 - 7/1/2023 | 0 | 2 | 1 |
| 7/1/2023 - 10/1/2023 | 0 | 1 | 1 |
初学者在处理这类问题时,常会尝试使用any(axis=1)结合日期范围筛选。例如:
import pandas as pd
# 示例数据
data = {
'stage 1': ['1/3/2023', '2/5/2023', '1/15/2023'],
'stage 2': ['4/3/2023', '2/6/2023', '6/3/2023'],
'stage 3': ['5/7/2023', '3/4/2023', '7/8/2023']
}
df = pd.DataFrame(data, index=[f'row {i+1}' for i in range(3)])
df = df.apply(pd.to_datetime) # 确保日期格式正确
start = pd.to_datetime('2023-1-1')
end = pd.to_datetime('2023-3-30')
stage_cols = ['stage 1', 'stage 2', 'stage 3']
# 错误示例
# (df[stage_cols] >= start).any(axis=1) 会检查任何列是否大于等于start
# (df[stage_cols] <= end).any(axis=1) 会检查任何列是否小于等于end
# 这种方法会错误地将满足任一条件的行全部纳入计数
filtered_df_incorrect = df[(df[stage_cols] >= start).any(axis=1) & (df[stage_cols] <= end).any(axis=1)]
print("错误筛选结果的行数:\n", filtered_df_incorrect.shape[0])
# 这种筛选方式可能导致行3被计数,即使其stage 2和stage 3的日期超出了范围,
# 因为stage 1的日期满足条件,且stage 2的日期可能满足另一个条件。
# 并且,最终我们想要的是每个阶段的独立计数,而不是基于行的整体筛选。这种方法的问题在于,它首先在行级别进行聚合(any(axis=1)),然后才进行逻辑与操作。这意味着只要一行中的任何一个阶段日期满足起始条件,且任何一个阶段日期满足结束条件,该行就会被选中。这无法实现对每个阶段日期进行独立的范围检查和计数。例如,对于row 3,stage 1的日期在范围内,但stage 2和stage 3的日期不在范围内。如果使用上述逻辑,row 3仍可能被错误地包含在计数中。
此外,用户可能会编写一个循环函数来逐个日期范围、逐列地进行计数,如问题中提到的funnel_by_time函数。虽然这种方法能够得到正确结果,但由于使用了Python循环,在大规模数据集上性能会比较低下,不符合Pandas的向量化操作最佳实践。
Pandas提供了强大的向量化操作能力,可以高效地处理这类问题。核心思想是:先对DataFrame中的每个日期进行独立的范围检查,生成布尔矩阵,然后对该布尔矩阵按列求和。
步骤1:确保日期列为datetime类型
这是进行日期比较的前提。
import pandas as pd
data = {
'stage 1': ['1/3/2023', '2/5/2023', '1/15/2023'],
'stage 2': ['4/3/2023', '2/6/2023', '6/3/2023'],
'stage 3': ['5/7/2023', '3/4/2023', '7/8/2023']
}
df = pd.DataFrame(data, index=[f'row {i+1}' for i in range(3)])
# 将所有日期列转换为datetime对象
tmp_df = df.apply(pd.to_datetime)
print("转换后的DataFrame (tmp_df):\n", tmp_df)输出:
转换后的DataFrame (tmp_df):
stage 1 stage 2 stage 3
row 1 2023-01-03 00:00:00 2023-04-03 00:00:00 2023-05-07 00:00:00
row 2 2023-02-05 00:00:00 2023-02-06 00:00:00 2023-03-04 00:00:00
row 3 2023-01-15 00:00:00 2023-06-03 00:00:00 2023-07-08 00:00:00步骤2:定义日期范围并进行元素级比较
定义起始日期和结束日期,然后使用Pandas的ge()(大于等于)和le()(小于等于)方法进行元素级的布尔比较。
start_date = pd.to_datetime('2023-1-1')
end_date = pd.to_datetime('2023-3-30')
# 检查每个日期是否大于等于起始日期
ge_mask = tmp_df.ge(start_date)
print("\n大于等于起始日期的布尔矩阵 (ge_mask):\n", ge_mask)
# 检查每个日期是否小于等于结束日期
le_mask = tmp_df.le(end_date)
print("\n小于等于结束日期的布尔矩阵 (le_mask):\n", le_mask)输出:
大于等于起始日期的布尔矩阵 (ge_mask):
stage 1 stage 2 stage 3
row 1 True True True
row 2 True True True
row 3 True True True
小于等于结束日期的布尔矩阵 (le_mask):
stage 1 stage 2 stage 3
row 1 True False False
row 2 True True True
row 3 True False False步骤3:结合布尔矩阵并按列求和
将两个布尔矩阵通过逻辑与操作符&结合,生成最终的布尔矩阵,其中True表示该日期在该范围内。然后,对这个最终的布尔矩阵按列求和,即可得到每个阶段在指定日期范围内的计数。
# 结合两个布尔矩阵
final_mask = ge_mask & le_mask
print("\n最终布尔矩阵 (final_mask):\n", final_mask)
# 按列求和得到每个阶段的计数
counts = final_mask.sum()
print("\n单个日期范围内的计数结果:\n", counts)输出:
最终布尔矩阵 (final_mask):
stage 1 stage 2 stage 3
row 1 True False False
row 2 True True True
row 3 True False False
单个日期范围内的计数结果:
stage 1 3
stage 2 1
stage 3 1
dtype: int64通过这种方法,我们可以看到stage 1有3个日期在范围内,stage 2有1个,stage 3有1个。这与预期结果一致。
为了生成漏斗分析所需的汇总表,我们需要对多个日期范围重复上述过程,并将结果收集到一个新的DataFrame中。
import pandas as pd
# 原始数据
data = {
'stage 1': ['1/3/2023', '2/5/2023', '1/15/2023'],
'stage 2': ['4/3/2023', '2/6/2023', '6/3/2023'],
'stage 3': ['5/7/2023', '3/4/2023', '7/8/2023']
}
df = pd.DataFrame(data, index=[f'row {i+1}' for i in range(3)])
tmp_df = df.apply(pd.to_datetime) # 确保日期格式正确
# 定义日期范围列表
date_ranges = [
(pd.to_datetime('2023-1-1'), pd.to_datetime('2023-4-1')),
(pd.to_datetime('2023-4-1'), pd.to_datetime('2023-7-1')),
(pd.to_datetime('2023-7-1'), pd.to_datetime('2023-10-1'))
]
results = {}
for start_date, end_date in date_ranges:
# 执行向量化筛选和计数
ge_mask = tmp_df.ge(start_date)
le_mask = tmp_df.lt(end_date) # 注意:这里使用lt (<) 而不是 le (<=),以确保区间不重叠且符合常规统计习惯
# 如果包含结束日期,则使用le
final_mask = ge_mask & le_mask
counts = final_mask.sum()
range_label = f"{start_date.strftime('%m/%d/%Y')} - {end_date.strftime('%m/%d/%Y')}"
results[range_label] = counts.tolist() # 将Series转换为列表
# 构建最终的汇总DataFrame
output_df = pd.DataFrame.from_dict(results, orient='index', columns=tmp_df.columns)
print("\n最终漏斗分析汇总表:\n", output_df)输出:
最终漏斗分析汇总表:
stage 1 stage 2 stage 3
01/01/2023 - 04/01/2023 2 1 1
04/01/2023 - 07/01/2023 0 2 1
07/01/2023 - 10/01/2023 0 1 1请注意,在定义日期范围时,通常会使用半开区间[start, end),即包含起始日期但不包含结束日期,以避免区间重叠导致重复计数。因此,在上述代码中,我们将le_mask = tmp_df.le(end_date)改为了le_mask = tmp_df.lt(end_date)。如果业务需求是包含结束日期,则应使用le()。
通过本教程,我们学习了如何利用Pandas的向量化能力,高效且准确地统计DataFrame中多列日期数据在指定时间范围内的行数。关键在于理解并正确应用元素级的布尔比较和列求和操作,避免了常见的筛选误区和低效的循环方法。掌握这种技术,能够有效地支持多阶段流程的性能分析和可视化需求。
以上就是使用Pandas高效统计多列日期数据在指定范围内的行数的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号