【问题标题】:Pandas calculate total hours from overlapping time rangesPandas 从重叠的时间范围计算总小时数
【发布时间】:2020-12-14 18:31:25
【问题描述】:

我有以下数据框

import pandas as pd
from datetime import datetime

df_dict = {
    'id':[1,1,1,1,2,2,2,2],
    'start_time':[
    datetime.strptime('Jun 1 2020  1:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  2:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  3:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  4:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  1:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  2:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  3:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  4:30PM', '%b %d %Y %I:%M%p'),
    ],
    'end_time':[
    datetime.strptime('Jun 1 2020  2:45PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  3:00PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  4:50PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  4:30PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  3:45PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  5:00PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  5:50PM', '%b %d %Y %I:%M%p'),
    datetime.strptime('Jun 1 2020  6:30PM', '%b %d %Y %I:%M%p'),
    ]
}
df = pd.DataFrame.from_dict(df_dict)

#    id          start_time            end_time
# 0   1 2020-06-01 13:30:00 2020-06-01 14:45:00
# 1   1 2020-06-01 14:30:00 2020-06-01 15:00:00
# 2   1 2020-06-01 15:30:00 2020-06-01 16:50:00
# 3   1 2020-06-01 16:30:00 2020-06-01 16:30:00
# 4   2 2020-06-01 13:30:00 2020-06-01 15:45:00
# 5   2 2020-06-01 14:30:00 2020-06-01 17:00:00
# 6   2 2020-06-01 15:30:00 2020-06-01 17:50:00
# 7   2 2020-06-01 16:30:00 2020-06-01 18:30:00

我想计算每个 id 的总小时数,而不重复计算重叠间隔。

我有下面的代码,它给出了正确的结果

import sqlite3

conn = sqlite3.connect(':memory:')
df.to_sql('df', conn, index=False)

query = '''
SELECT id, SUM(CAST((JulianDay(end_time)-JulianDay(start_time))*24 AS real)) AS total_hours
FROM (
    SELECT s1.id,
           s1.start_time,
           MIN(t1.end_time) AS end_time
    FROM df s1 
    INNER JOIN df t1 ON s1.start_time <= t1.end_time
      AND s1.id = t1.id
      AND NOT EXISTS(SELECT * FROM df t2 
                     WHERE t1.end_time >= t2.start_time AND t1.end_time < t2.end_time AND t2.id = t1.id) 
    WHERE NOT EXISTS(SELECT * FROM df s2 
                     WHERE s1.start_time > s2.start_time AND s1.start_time <= s2.end_time AND s2.id = t1.id)
    GROUP BY s1.start_time, s1.id
    ORDER BY s1.id, s1.start_time
    ) x
GROUP BY id
'''

df = pd.read_sql_query(query, conn)
print(df)

#    id  total_hours
# 0   1     2.833333
# 1   2     5.000000

但我想知道是否有更好/更优雅的方法来解决这个问题,而不使用 SQL。

【问题讨论】:

  • "计算每个 id 的总小时数" => df.groupby('id') 然后用 .apply() 或类似方法计算您的聚合。另一个提示是您的 SQL 包含 GROUP BY s1.start_time, s1.id

标签: python pandas


【解决方案1】:

基本上,您的 SQL 代码所做的是删除重叠间隔。你应该在这里做同样的事情。我的建议如下:

def remove_overlap_intervals(intervals):
    sorted = sorted(intervals, key=lambda tup: tup[0])
    merged = []

    for a in sorted:
        if not merged:
            merged.append(a)
        else:
            b = merged[-1]
            if a[0] <= b[1]:
                upper_bound = max(b[1], a[1])
                merged[-1] = (b[0], upper_bound) 
            else:
                merged.append(a)
    return merged

如果间隔不重叠,则完全按照您的操作:


df['time'] = df[['start_time', 'end_time']].apply(tuple, axis=1)
Grouped = df.groupby(['id'])['time'].apply(list)
Grouped_no_overlap = Grouped.apply(remove_overlap_intervals)

Grouped = Grouped_no_overlap.apply(lambda x: sum([(y[1]-y[0]).seconds for y in x]))/3600

给出:

id
1    2.833333
2    5.000000
Name: time, dtype: float64

【讨论】:

    【解决方案2】:

    您可以使用 pandas 的groupby 功能。以下代码将完成这项工作:

    import numpy as np
    df['start_time_tmp']=np.where((df['start_time'] <= df['end_time'].shift(1))&
                                  (df['end_time'] >= df['end_time'].shift(1)), df['end_time'].shift(1), df['start_time'])
    df['diff'] = df['end_time']-df['start_time_tmp']
    df.groupby(by='id')['diff'].sum().dt.total_seconds()/60/60
    

    输出是:

    id
    1    2.833333
    2    5.000000
    Name: diff, dtype: float64
    

    我希望,代码是不言自明的。如果您需要有关groupby 功能的帮助,可以查看docs

    【讨论】:

      【解决方案3】:

      据我所知,您的日期时间的时间分辨率仅限于 分钟

      所以一种可能的解决方案是:

      • 对于组中的每一行(按id),以分钟 的频率生成date_range 对象, 不包括相应范围的右边缘,
      • 连接这些范围,
      • 计算唯一值的总和(现在我们有分钟数),
      • 除以 60,得到小时数。

      代码如下:

      1. 定义getRng函数从当前生成date_range对象:

        def getRng(row):
            return pd.date_range(row.start_time, row.end_time, freq='min', closed='left')
        
      2. 定义getHrs函数来计算当前组的小时数:

        def getHrs(grp):
            return np.unique(np.hstack(grp.apply(getRng, axis=1))).size / 60
        

        我特意选择了 Numpy 函数,因为它们是众所周知的 比 Pandas 更快。​​

      3. 将上述函数应用于每个组(按id)并转换结果 到 DataFrame:

        result = df.groupby('id').apply(getHrs).rename('total_hours').reset_index()
        

      对于您的数据样本,结果是:

         id  total_hours
      0   1     2.833333
      1   2     5.000000
      

      ​ 我认为,这个解决方案更短(只有 5 行代码),更具可读性 比你的 SQL 和更多 pandasonic

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-01-19
        • 1970-01-01
        • 1970-01-01
        • 2019-06-29
        • 1970-01-01
        • 2018-12-28
        • 1970-01-01
        • 2013-06-12
        相关资源
        最近更新 更多