大熊猫-在特定日期范围内划分日期范围

时间:2020-01-17 17:02:44

标签: python pandas dataframe

我有一个日期范围的DataFrame(实际的DataFrame附加了更多数据,但具有相同的startend列)。最终需要在周日至周六进行每周一次的数据分析。因此,我想遍历DataFrame,并拆分从星期六到星期日的任何日期范围(startfinish)。例如,给定DataFrame:

import pandas as pd

date_ranges = [
    {'start': '2020-01-16 22:30:00', 'end': '2020-01-17 01:00:00'}, # spans thurs-fri, ok as is
    {'start': '2020-01-17 04:30:00', 'end': '2020-01-17 12:30:00'}, # no span, ok as is
    {'start': '2020-01-18 10:15:00', 'end': '2020-01-18 14:00:00'}, # no span, ok as is
    {'start': '2020-01-18 22:30:00', 'end': '2020-01-19 02:00:00'}  # spans sat-sun, must split
]
data_df = pd.DataFrame(date_ranges)

我希望我的结果看起来像这样:

result_ranges = [
    {'start': '2020-01-16 22:30:00', 'end': '2020-01-17 01:00:00'}, # spans thurs-fri, ok as is
    {'start': '2020-01-17 04:30:00', 'end': '2020-01-17 12:30:00'}, # no span, ok as is
    {'start': '2020-01-18 10:15:00', 'end': '2020-01-18 14:00:00'}, # no span, ok as is
    {'start': '2020-01-18 22:30:00', 'end': '2020-01-19 00:00:00'}, # split out saturday portion
    {'start': '2020-01-19 00:00:00', 'end': '2020-01-19 02:00:00'}  # and the sunday portion
]

result_df = pd.DataFrame(result_ranges)

任何关于如何有效地在熊猫中做到这一点的想法将不胜感激。目前,我正在做坏事情,并且遍历行,并且当数据集变大时,它非常慢。

3 个答案:

答案 0 :(得分:6)

这样的操作总是很困难,在某种程度上,我认为循环是必要的。在这种情况下,我们可以遍历边缘,而不是遍历行。当您的数据跨度的周数远小于您拥有的行数时,这将导致性能的大幅提高。

我们定义边缘并在必要时修改DataFrame端点。最后,所需的DataFrame是我们修改后的DataFrame剩下的,再加上我们存储在l中的所有单独的时间跨度。原始索引被保留,因此您可以确切地看到被拆分的行。如果单个时间跨度跨N个边,它将分成N+1个单独的行。

设置

import pandas as pd

df[['start', 'end']]= df[['start', 'end']].apply(pd.to_datetime)

edges = pd.date_range(df.start.min().normalize() - pd.Timedelta(days=7),
                      df.end.max().normalize() + pd.Timedelta(days=7), freq='W-Sun')

代码

l = []
for edge in edges:
    m = df.start.lt(edge) & df.end.gt(edge)  # Rows to modify
    l.append(df.loc[m].assign(end=edge))     # Clip end of modified rows
    df.loc[m, 'start'] = edge                # Fix start for next edge

result = pd.concat(l+[df]).sort_values('start')

输出

                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 00:00:00
3 2020-01-19 00:00:00 2020-01-19 02:00:00

答案 1 :(得分:2)

我定义的解决方案更加通用,即创建 来自每个源行的“周行”序列,即使两个日期 在它们之间包含两个星期六/星期日休息。

为了检查它是否有效,我在您的DataFrame中添加了这样的一行,以便 它包含:

                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 02:00:00
4 2020-01-25 20:30:00 2020-02-02 03:00:00

请注意,最后一行包括从 25.01 26.01 2 周六/周日休息 并从 1.02 2.02

从两列到 datetime 的转换开始:

data_df.start = pd.to_datetime(data_df.start)
data_df.end = pd.to_datetime(data_df.end)

要处理数据,请定义以下函数,将其应用于每一行:

def weekRows(row):
    row.index = pd.DatetimeIndex(row)
    gr = row.resample('W-SUN', closed='left')
    ngr = gr.ngroups  # Number of groups
    i = 1
    data = []
    for key, grp in gr:
        dt1 = key - pd.Timedelta('7D')
        dt2 = key
        if i == 1:
            dt1 = row.iloc[0]
        if i == ngr:
            dt2 = row.iloc[1]
        data.append([dt1, dt2])
        i += 1
    return pd.DataFrame(data, columns=['start', 'end'])

让我们“单独”介绍它在最后两行中的操作方式:

运行时:

row = data_df.loc[3]
weekRows(row)

(对于最后一行,但最后一行),您将获得:

                start                 end
0 2020-01-18 22:30:00 2020-01-19 00:00:00
1 2020-01-19 00:00:00 2020-01-19 02:00:00

运行时:

row = data_df.loc[4]
weekRows(row)

(对于最后一个),您将获得:

                start                 end
0 2020-01-25 20:30:00 2020-01-26 00:00:00
1 2020-01-26 00:00:00 2020-02-02 00:00:00
2 2020-02-02 00:00:00 2020-02-02 03:00:00

要获得所需的结果,请运行:

result = pd.concat(data_df.apply(weekRows, axis=1).values, ignore_index=True)

结果是:

                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 00:00:00
4 2020-01-19 00:00:00 2020-01-19 02:00:00
5 2020-01-25 20:30:00 2020-01-26 00:00:00
6 2020-01-26 00:00:00 2020-02-02 00:00:00
7 2020-02-02 00:00:00 2020-02-02 03:00:00

前3行来自您的前3个源行。 接下来的两行(索引 3 4 )来自具有索引 3 的源行。 最后3行(索引 5 7 )来自最后一个源行。

答案 2 :(得分:0)

类似于@Valdi_Bo's answer,我研究了将(start, end)的单个时间间隔分解为一系列时间间隔,包括介于其间的星期日的所有午夜。

这是通过以下功能完成的:

def break_weekly(start, end):
    edges = list(pd.date_range(start, end, freq='W', normalize=True, closed='right'))
    if edges and edges[-1] == end:
        edges.pop()
    return pd.Series(list(zip([start] + edges, edges + [end])))

此代码将创建一个从“开始”到“结束”的每周日期范围,归一化为午夜时间(即周日午夜),并使间隔保持在左侧打开状态(因此它从开始后的周日开始)。 / p>

有一个极端的情况,当“结束”恰好是星期日的午夜时,由于间隔的一侧需要关闭,因此我们将其保持在右侧,因此我们正在检查这两个间隔是否匹配并下降如果它们相同的话。

然后,我们使用zip()创建具有每个日期对的元组,包括在左边开头的“开始”和在右边结尾的“结束”时间戳。

我们最终返回了其中的pd.Series个元组,因为这使apply()达到了我们的预期。

用法示例:

>>> break_weekly(pd.Timestamp('2020-01-18 22:30:00'), pd.Timestamp('2020-01-19 02:00:00'))
0    (2020-01-18 22:30:00, 2020-01-19 00:00:00)
1    (2020-01-19 00:00:00, 2020-01-19 02:00:00)
dtype: object

这时,您可以将其应用于原始数据框以查找间隔的完整列表。

首先,将列的类型转换为pd.Timestamp(示例中的列中包含字符串):

data_df = data_df.apply(pd.to_datetime)

然后您可以使用以下信息找到整个时间间隔列表:

intervals = (data_df
    .apply(lambda r: break_weekly(r.start, r.end), axis=1)
    .unstack().dropna().reset_index(level=0, drop=True)
    .apply(lambda r: pd.Series(r, index=['start', 'end'])))

第一步是将break_weekly()逐行应用于“开始”和“结束”列。由于break_weekly()返回了pd.Series,因此最终将生成一个新的DataFrame,该DataFrame的每个日期间隔为一列(间隔​​中的星期数为该数)。

然后unstack()将这些列合并回去,并且dropna()将丢弃由于每一行具有不同列数(每行的间隔数不同)而生成的NaN。

这时我们有了一个多索引,因此reset_index(level=0, drop=True)会删除我们不在乎的索引级别,而只保留与原始DataFrame匹配的索引级别。

最后,最后一个apply()将Python元组中的条目转换回pd.Series,并将列分别命名为“开始”和“结束”。

查看到目前为止的结果:

>>> intervals
                start                 end
0 2020-01-16 22:30:00 2020-01-17 01:00:00
1 2020-01-17 04:30:00 2020-01-17 12:30:00
2 2020-01-18 10:15:00 2020-01-18 14:00:00
3 2020-01-18 22:30:00 2020-01-19 00:00:00
3 2020-01-19 00:00:00 2020-01-19 02:00:00

由于索引与原始DataFrame中的索引匹配,因此您现在可以使用此DataFrame将其连接回原始数据库,如果您在那里有更多带有值的列,并且想要在此处重复这些,则只需将他们聚在一起。

例如:

>>> data_df['value'] = ['abc', 'def', 'ghi', 'jkl']
>>> intervals.join(df.drop(['start', 'end'], axis=1))
                start                 end value
0 2020-01-16 22:30:00 2020-01-17 01:00:00   abc
1 2020-01-17 04:30:00 2020-01-17 12:30:00   def
2 2020-01-18 10:15:00 2020-01-18 14:00:00   ghi
3 2020-01-18 22:30:00 2020-01-19 00:00:00   jkl
3 2020-01-19 00:00:00 2020-01-19 02:00:00   jkl

您会注意到,最后一行中的值已被复制到该间隔的两行中。