从具有日期范围的 2 个数据框中获取非重叠周期
get non-overlapping period from 2 dataframe with date ranges
我正在开发计费系统。
一方面,我有开始和结束日期的合同,我需要按月开具账单。一份合同可以有多个 start/end 日期,但同一合同的日期不能重叠。
另一方面,我有一个 df,其中包含每份合同的发票,以及它们的开始和结束日期。特定合同的发票 start/end 日期也不能重叠。一张发票的结束日期和另一张发票的开始日期之间可能存在差距。
我的目标是查看合同 start/end 日期,并删除单个合同的所有计费期间,这样我就知道还剩下哪些需要计费。
这是我的合同数据:
contract_df = pd.DataFrame({'contract_id': {0: 'C00770052',
1: 'C00770052',
2: 'C00770052',
3: 'C00770052',
4: 'C00770053'},
'from': {0: pd.to_datetime('2018-07-01 00:00:00'),
1: pd.to_datetime('2019-01-01 00:00:00'),
2: pd.to_datetime('2019-07-01 00:00:00'),
3: pd.to_datetime('2019-09-01 00:00:00'),
4: pd.to_datetime('2019-10-01 00:00:00')},
'to': {0: pd.to_datetime('2019-01-01 00:00:00'),
1: pd.to_datetime('2019-07-01 00:00:00'),
2: pd.to_datetime('2019-09-01 00:00:00'),
3: pd.to_datetime('2021-01-01 00:00:00'),
4: pd.to_datetime('2024-01-01 00:00:00')}})
这是我的发票数据(C00770053 没有发票):
invoice_df = pd.DataFrame({'contract_id': {0: 'C00770052',
1: 'C00770052',
2: 'C00770052',
3: 'C00770052',
4: 'C00770052',
5: 'C00770052',
6: 'C00770052',
7: 'C00770052'},
'from': {0: pd.to_datetime('2018-07-01 00:00:00'),
1: pd.to_datetime('2018-08-01 00:00:00'),
2: pd.to_datetime('2018-09-01 00:00:00'),
3: pd.to_datetime('2018-10-01 00:00:00'),
4: pd.to_datetime('2018-11-01 00:00:00'),
5: pd.to_datetime('2019-05-01 00:00:00'),
6: pd.to_datetime('2019-06-01 00:00:00'),
7: pd.to_datetime('2019-07-01 00:00:00')},
'to': {0: pd.to_datetime('2018-08-01 00:00:00'),
1: pd.to_datetime('2018-09-01 00:00:00'),
2: pd.to_datetime('2018-10-01 00:00:00'),
3: pd.to_datetime('2018-11-01 00:00:00'),
4: pd.to_datetime('2019-04-01 00:00:00'),
5: pd.to_datetime('2019-06-01 00:00:00'),
6: pd.to_datetime('2019-07-01 00:00:00'),
7: pd.to_datetime('2019-09-01 00:00:00')}})
我的预期结果是:
to_bill_df = pd.DataFrame({'contract_id': {0: 'C00770052',
1: 'C00770052',
2: 'C00770053'},
'from': {0: pd.to_datetime('2019-04-01 00:00:00'),
1: pd.to_datetime('2019-09-01 00:00:00'),
2: pd.to_datetime('2019-10-01 00:00:00')},
'to': {0: pd.to_datetime('2019-05-01 00:00:00'),
1: pd.to_datetime('2021-01-01 00:00:00'),
2: pd.to_datetime('2024-01-01 00:00:00')}})
因此,我需要遍历 contract_df 的每一行,识别与相关期间匹配的发票,并从 contract_df 中删除已开具账单的期间,最终拆分 contract_df如果有空隙,则排成2行。
问题是,考虑到我将有数百万张发票和合同,这样做似乎很沉重,我觉得 pandas 有一个简单的方法,但我不确定我该怎么做去做
谢谢
前几天我正在解决类似的问题。这不是一个简单的解决方案,但在识别任何非重叠间隔时应该是通用的。
我们的想法是将您的日期转换为连续整数,然后我们可以使用集合或运算符删除重叠。下面的函数会将您的 DataFrame 转换为字典,其中包含每个 ID 的非重叠整数日期列表。
from functools import reduce
def non_overlapping_intervals(df, uid, date_from, date_to):
# Convert date to day integer
helper_from = date_from + '_helper'
helper_to = date_to + '_helper'
df[helper_from] = df[date_from].sub(pd.Timestamp('1900-01-01')).dt.days # set a reference date
df[helper_to] = df[date_to].sub(pd.Timestamp('1900-01-01')).dt.days
out = (
df[[uid, helper_from, helper_to]]
.dropna()
.groupby(uid)
[[helper_from, helper_to]]
.apply(
lambda x: reduce( # Apply for an arbitrary number of cases
lambda a, b: a | b, x.apply( # Eliminate the overlapping dates OR operation on set
lambda y: set(range(y[helper_from], y[helper_to])), # Create continuous integers for date ranges
axis=1
)
)
)
.to_dict()
)
return out
从这里开始,我们要进行集合减法以查找有合同但没有发票的日期和 ID:
from collections import defaultdict
invoice_dates = defaultdict(set, non_overlapping_intervals(invoice_df, 'contract_id', 'from', 'to'))
contract_dates = defaultdict(set, non_overlapping_intervals(contract_df, 'contract_id', 'from', 'to'))
missing_dates = {}
for k, v in contract_dates.items():
missing_dates[k] = list(v - invoice_dates.get(k, set()))
现在我们有一个名为 missing_dates
的字典,它为我们提供了每个没有发票的日期。要将其转换为您的输出格式,我们需要将每个 ID 的每个连续组分开。使用 this answer,我们得出以下结果:
from itertools import groupby
from operator import itemgetter
missing_invoices = []
for uid, dates in missing_dates.items():
for k, g in groupby(enumerate(sorted(dates)), lambda x: x[0] - x[1]):
group = list(map(int, map(itemgetter(1), g)))
missing_invoices.append([uid, group[0], group[-1]])
missing_invoices = pd.DataFrame(missing_invoices, columns=['contract_id', 'from', 'to'])
# Convert back to datetime
missing_invoices['from'] = missing_invoices['from'].apply(lambda x: pd.Timestamp('1900-01-01') + pd.DateOffset(days=x))
missing_invoices['to'] = missing_invoices['to'].apply(lambda x: pd.Timestamp('1900-01-01') + pd.DateOffset(days=x + 1))
可能不是您正在寻找的简单解决方案,但这应该相当有效。
我正在开发计费系统。
一方面,我有开始和结束日期的合同,我需要按月开具账单。一份合同可以有多个 start/end 日期,但同一合同的日期不能重叠。
另一方面,我有一个 df,其中包含每份合同的发票,以及它们的开始和结束日期。特定合同的发票 start/end 日期也不能重叠。一张发票的结束日期和另一张发票的开始日期之间可能存在差距。
我的目标是查看合同 start/end 日期,并删除单个合同的所有计费期间,这样我就知道还剩下哪些需要计费。
这是我的合同数据:
contract_df = pd.DataFrame({'contract_id': {0: 'C00770052',
1: 'C00770052',
2: 'C00770052',
3: 'C00770052',
4: 'C00770053'},
'from': {0: pd.to_datetime('2018-07-01 00:00:00'),
1: pd.to_datetime('2019-01-01 00:00:00'),
2: pd.to_datetime('2019-07-01 00:00:00'),
3: pd.to_datetime('2019-09-01 00:00:00'),
4: pd.to_datetime('2019-10-01 00:00:00')},
'to': {0: pd.to_datetime('2019-01-01 00:00:00'),
1: pd.to_datetime('2019-07-01 00:00:00'),
2: pd.to_datetime('2019-09-01 00:00:00'),
3: pd.to_datetime('2021-01-01 00:00:00'),
4: pd.to_datetime('2024-01-01 00:00:00')}})
这是我的发票数据(C00770053 没有发票):
invoice_df = pd.DataFrame({'contract_id': {0: 'C00770052',
1: 'C00770052',
2: 'C00770052',
3: 'C00770052',
4: 'C00770052',
5: 'C00770052',
6: 'C00770052',
7: 'C00770052'},
'from': {0: pd.to_datetime('2018-07-01 00:00:00'),
1: pd.to_datetime('2018-08-01 00:00:00'),
2: pd.to_datetime('2018-09-01 00:00:00'),
3: pd.to_datetime('2018-10-01 00:00:00'),
4: pd.to_datetime('2018-11-01 00:00:00'),
5: pd.to_datetime('2019-05-01 00:00:00'),
6: pd.to_datetime('2019-06-01 00:00:00'),
7: pd.to_datetime('2019-07-01 00:00:00')},
'to': {0: pd.to_datetime('2018-08-01 00:00:00'),
1: pd.to_datetime('2018-09-01 00:00:00'),
2: pd.to_datetime('2018-10-01 00:00:00'),
3: pd.to_datetime('2018-11-01 00:00:00'),
4: pd.to_datetime('2019-04-01 00:00:00'),
5: pd.to_datetime('2019-06-01 00:00:00'),
6: pd.to_datetime('2019-07-01 00:00:00'),
7: pd.to_datetime('2019-09-01 00:00:00')}})
我的预期结果是:
to_bill_df = pd.DataFrame({'contract_id': {0: 'C00770052',
1: 'C00770052',
2: 'C00770053'},
'from': {0: pd.to_datetime('2019-04-01 00:00:00'),
1: pd.to_datetime('2019-09-01 00:00:00'),
2: pd.to_datetime('2019-10-01 00:00:00')},
'to': {0: pd.to_datetime('2019-05-01 00:00:00'),
1: pd.to_datetime('2021-01-01 00:00:00'),
2: pd.to_datetime('2024-01-01 00:00:00')}})
因此,我需要遍历 contract_df 的每一行,识别与相关期间匹配的发票,并从 contract_df 中删除已开具账单的期间,最终拆分 contract_df如果有空隙,则排成2行。
问题是,考虑到我将有数百万张发票和合同,这样做似乎很沉重,我觉得 pandas 有一个简单的方法,但我不确定我该怎么做去做
谢谢
前几天我正在解决类似的问题。这不是一个简单的解决方案,但在识别任何非重叠间隔时应该是通用的。
我们的想法是将您的日期转换为连续整数,然后我们可以使用集合或运算符删除重叠。下面的函数会将您的 DataFrame 转换为字典,其中包含每个 ID 的非重叠整数日期列表。
from functools import reduce
def non_overlapping_intervals(df, uid, date_from, date_to):
# Convert date to day integer
helper_from = date_from + '_helper'
helper_to = date_to + '_helper'
df[helper_from] = df[date_from].sub(pd.Timestamp('1900-01-01')).dt.days # set a reference date
df[helper_to] = df[date_to].sub(pd.Timestamp('1900-01-01')).dt.days
out = (
df[[uid, helper_from, helper_to]]
.dropna()
.groupby(uid)
[[helper_from, helper_to]]
.apply(
lambda x: reduce( # Apply for an arbitrary number of cases
lambda a, b: a | b, x.apply( # Eliminate the overlapping dates OR operation on set
lambda y: set(range(y[helper_from], y[helper_to])), # Create continuous integers for date ranges
axis=1
)
)
)
.to_dict()
)
return out
从这里开始,我们要进行集合减法以查找有合同但没有发票的日期和 ID:
from collections import defaultdict
invoice_dates = defaultdict(set, non_overlapping_intervals(invoice_df, 'contract_id', 'from', 'to'))
contract_dates = defaultdict(set, non_overlapping_intervals(contract_df, 'contract_id', 'from', 'to'))
missing_dates = {}
for k, v in contract_dates.items():
missing_dates[k] = list(v - invoice_dates.get(k, set()))
现在我们有一个名为 missing_dates
的字典,它为我们提供了每个没有发票的日期。要将其转换为您的输出格式,我们需要将每个 ID 的每个连续组分开。使用 this answer,我们得出以下结果:
from itertools import groupby
from operator import itemgetter
missing_invoices = []
for uid, dates in missing_dates.items():
for k, g in groupby(enumerate(sorted(dates)), lambda x: x[0] - x[1]):
group = list(map(int, map(itemgetter(1), g)))
missing_invoices.append([uid, group[0], group[-1]])
missing_invoices = pd.DataFrame(missing_invoices, columns=['contract_id', 'from', 'to'])
# Convert back to datetime
missing_invoices['from'] = missing_invoices['from'].apply(lambda x: pd.Timestamp('1900-01-01') + pd.DateOffset(days=x))
missing_invoices['to'] = missing_invoices['to'].apply(lambda x: pd.Timestamp('1900-01-01') + pd.DateOffset(days=x + 1))
可能不是您正在寻找的简单解决方案,但这应该相当有效。