получить неперекрывающийся период из двух кадров данных с диапазонами дат - PullRequest
0 голосов
/ 15 октября 2019

Я работаю над биллинговой системой.

С одной стороны, у меня есть контракты с начальной и конечной датой, которые мне нужно ежемесячно выставлять. Один контракт может иметь несколько дат начала / окончания, но они не могут перекрываться для одного и того же контракта.

С другой стороны, у меня есть df со счетом-фактурой, выставляемым по контракту, с их датой начала и окончания. Даты начала / окончания счетов для конкретного договора также не могут перекрываться. Хотя может быть разрыв между датой окончания счета и началом другого счета.

Моя цель - просмотреть даты начала / окончания контракта и удалить весь период, выставленный для одного контракта, так что язнаю, что осталось для выставления счета.

Вот мои данные для контракта:

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')}})

contractdf

Вот мои данные счета (нет счета для 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')}})

invoicedf

Мой ожидаемый результат:

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')}})

tobilldf

Поэтому мне нужно просмотреть каждую строку contract_df, идентифицировать счета-фактуры, соответствующие соответствующему периоду, и удалить периоды, которые уже были рассчитаны, из contract_df, в итоге разделив строку contract_df на 2 строки. если есть пробел.

Проблема в том, что так будет казаться очень тяжелым, учитывая, что у меня будут миллионы счетов и контрактов, я чувствую, что с пандами есть легкий путь, но я не уверенкак я мог это сделать

ThaНКС

1 Ответ

1 голос
/ 15 октября 2019

Я решал подобную проблему на днях. Это не простое решение, но оно должно быть общим для определения любых непересекающихся интервалов.

Идея состоит в том, чтобы преобразовать ваши даты в непрерывные целые числа, а затем мы можем удалить перекрытие с помощью оператора set ИЛИ. Приведенная ниже функция преобразует ваш DataFrame в словарь, который содержит список непересекающихся целочисленных дат для каждого идентификатора.

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

Здесь мы хотим сделать вычитание набора, чтобы найти даты и идентификаторы дляЕсть контракты, но нет счетов-фактур:

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, который дает нам каждую дату, для которой нет счетов-фактур. Чтобы преобразовать его в выходной формат, нам нужно отделить каждую непрерывную группу для каждого идентификатора. Используя этот ответ , мы приходим к следующему:

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))

Возможно, не то простое решение, которое вы искали, но оно должно быть достаточно эффективным.

...