我有一个日期范围的DataFrame(实际的DataFrame附加了更多数据,但具有相同的start
和end
列)。最终需要在周日至周六进行每周一次的数据分析。因此,我想遍历DataFrame,并拆分从星期六到星期日的任何日期范围(start
至finish
)。例如,给定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)
任何关于如何有效地在熊猫中做到这一点的想法将不胜感激。目前,我正在做坏事情,并且遍历行,并且当数据集变大时,它非常慢。
答案 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
您会注意到,最后一行中的值已被复制到该间隔的两行中。