Генерация даты на основе более чем одного ограничения - PullRequest
1 голос
/ 03 ноября 2019

У меня есть фрейм данных df1, имеющий столбец для date_1 со значениями с 01/09/2019 по 30/09/2019. т.е. 30 значений и соответствующий счет.

DF1

    date_1    count
    01/09/2019  5
    02/09/2019  4
    03/09/2019  5
    04/09/2019  6
    05/09/2019  7
    06/09/2019  8
    07/09/2019  10
    08/09/2019  9
    09/09/2019  11
    10/09/2019  12
    11/09/2019  13
    12/09/2019  14
    13/09/2019  15
    14/09/2019  16

Я хочу создать кадр данных df2 с использованием df1 с некоторыми ограничениями, такими как:

  1. Тамэто новый столбец date_2.

  2. date_2 генерируется для функции счетчика, присутствующего в df1.

Например: новый фрейм данных df2 будет иметь 5 записей (так как count = 5) для 01/09/2019, а столбец date_2 может принимать значения от 30 дней до даты_1 до 30/08/ 2019 (текущая дата в формате date_1 - 1), т.е. на 01/09/2019, значения, которые может принимать date_2, варьируются от (01/09/2019 - 30 = 01/08/2019) до (01/09/2019 - 1= 30/08/2019).

дата_2 может быть случайным образом выбрана из диапазона (30 - date_1 до date_1 - 1), т.е. в нашем примере на 01/09/2019, с 08/08/2019 по 30/08/2019

Важно отметить, что дата_2 также должна иметь увеличивающееся число в df2.

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

    date_1    count   date_2
    01/09/2019  5     02/08/2019
    01/09/2019  5     10/08/2019
    01/09/2019  5     12/08/2019
    01/09/2019  5     25/08/2019
    01/09/2019  5     28/08/2019
    02/09/2019  4     03/08/2019
    02/09/2019  4     10/08/2019
    02/09/2019  4     20/08/2019
    02/09/2019  4     25/08/2019

РЕДАКТИРОВАТЬ

Мне удалось сгенерировать date_2 с помощью функции:

def pick_random_delta_in_range(min_days=1, max_days=30):
    if min_days is None and max_days is None:
        return datetime.timedelta(days=1, minutes=0, seconds=0)
    if min_days is None:
        return max_days
    if max_days is None:
        return min_days
    days_to_be_added = random.randint(min_days, max_days)
    return datetime.timedelta(days=days_to_be_added, minutes=0, seconds=0)

def gen_date_by_delta(src_dates, date_format, delta_min, delta_max):
    gen_dates = []
    for dt in src_dates:
        src_date = datetime.datetime.strptime(dt, date_format)

        if src_date is None:
            gen_dates.append("")
            continue

        chosen_delta = pick_random_delta_in_range(min_days=delta_min, max_days=delta_max)

        result_date = (src_date + chosen_delta).strftime(date_format)
        gen_dates.append(result_date)

    return gen_dates

date_2 = gen_date_by_delta(src_dates=df1["date_1"], date_format=date_format, delta_min=1, delta_max=30)

* В настоящее время дельта генерируется случайным образом и может генерировать ту же дельту для той же даты_1, что приводит к дублированию записей. Я не хочу создавать повторяющиеся записи. *

Я также не могу понять, как копировать поля в кадре данных в соответствии с количеством и генерировать даты соответственно.

Может кто-нибудь помочь / предложить способ создать то же самое.

Спасибо

Ответы [ 2 ]

2 голосов
/ 03 ноября 2019

Определите следующую функцию «репликации»:

def repl(row):
    d1 = row.date_1
    cnt = row['count']
    dates = [ d1 - pd.Timedelta(n, 'D') for n in
            np.sort(np.random.choice(30, cnt, False))[::-1] ]
    return pd.DataFrame({'date_1': d1, 'count': cnt, 'date_2': dates})

Затем примените ее, объедините результаты и сохраните как DF2 :

DF2 = pd.concat(df.apply(repl, axis=1).tolist(), ignore_index=True)

Обратите внимание, что вприведенный выше код row ['count'] можно не заменить на row.count , потому что есть метод Pandas этогоимя. На самом деле это просто пример того, как не назначать имена столбцов. Вы должны не использовать имена существующих методов.

Редактировать после комментария о "всех значениях"

Чтобы использовать все даты из диапазона, процедура более сложна ивключает создание выделенного класса для генерации дат из пула.

Алгоритм назначения даты выглядит следующим образом:

  • Пул дат создается в начале, начиная с мин. date - 30 дней до max date.
  • При каждом вызове:
    • Шаг 1: Получить первую возможную дату.
    • Шаг2: Получить дополнительные даты из пула.
    • На обоих этих этапах назначенные даты удаляются из пула.
    • Шаг 3: Если в пуле больше нет дат из допустимого диапазона, но нам нужно больше, генерировать даты из возможного диапазона, но без повторения дат, выбранных для этой строки.

Этот класс содержит еще один "трюк", чтобы компенсироватьза то, что применяемая функция вызывается дважды за первый ряд. Это часть оптимизации, которая содержится в Pandas , но в этом случае она имеет побочный эффект (потребление некоторых «начальных» дат, которые на самом деле не включены в результат), поэтому мне пришлось компенсироватьдля этого.

Выполните следующие действия:

Создайте пару Timedelta переменных, используемых в различных точках:

td1 = pd.Timedelta(1, 'D')
td30 = pd.Timedelta(30, 'D')

Затем определите генератор датыкласс:

class DateGen:
    ''' Dates generator
    d1, d2 - date range
    '''
    def __init__(self, d1, d2):
        rng = pd.date_range(d1, d2, freq='D')
        self.dates = pd.Series(rng, index=rng)
        self.firstCall = True

    def popDate(self, d1, d2):
        wrk = self.dates[self.dates.between(d1, d2)]
        siz = wrk.size
        if siz > 0:
            dat = wrk.sample().iloc[0] if siz > 1 else wrk.iloc[0]
            self.dates.pop(dat)
            return dat, True
        return None, False

    def popDates(self, d1, d2, n):
        ret = []
        if self.firstCall:
            self.firstCall = False
            return ret
        # Step 1: Get the first possible date
        dat, ok = self.popDate(d1, d1)
        if ok:
            ret.append(dat)
        # Step 2: Get further dates not consumed so far
        while len(ret) < n:
            dat, ok = self.popDate(d1, d2)
            if not ok:
                break
            ret.append(dat)
        # Step 3: Repeat dates already consumed
        while len(ret) < n:
            shft = np.random.randint(30)
            dat = d2 - pd.Timedelta(shft, 'D')
            if dat not in ret:   # Without repetitions
                ret.append(dat)
        return ret

Создание объекта этого класса с диапазоном дат в соответствии с датами в DF1 :

dg = DateGen(DF1.date_1.min() - td30, DF1.date_1.max())

Функция репликации на этот раз немного отличается:

def repl(row):
    d1 = row.date_1
    cnt = row['count']
    dates = np.sort(dg.popDates(d1 - td30, d1 - td1, cnt))
    return pd.DataFrame({'date_1': d1, 'count': cnt, 'date_2': dates})

Он извлекает даты из объекта генератора, затем сортирует их и возвращает в возвращенный DataFrame.

И последний шаг - применить его:

DF2 = pd.concat(DF1.sort_values('date_1').\
    apply(repl, axis=1).tolist(), ignore_index=True)

Поскольку порядок дат теперь важен, я начал с сортировки по date_1 .

0 голосов
/ 03 ноября 2019

Я бы использовал другой подход. Создайте функцию, которая принимает строку вашего фрейма данных в качестве аргумента и допустимые даты, из которых выбирается количество случайных дат, и позволяет ей возвращать фрейм данных с указанным количеством строк. Вы можете использовать apply, чтобы использовать эту функцию в каждой строке. Затем вы можете объединить все эти кадры данных.

Чтобы не генерировать дубликаты 'date_2' значений, используйте numpy random shuffle . Это случайным образом изменит порядок элементов в массиве. Затем вы можете просто выбрать первые n элементов.

Здесь я также использую pandas date_range , чтобы сгенерировать диапазон дат, из которых выбираются случайные даты. Это делается один раз (это более эффективно, поскольку они всегда одинаковы), а затем даты передаются в функцию makedate2 при вызове apply.

def makedate2(row, dates):
    cnt = row['count']
    np.random.shuffle(dates) #randomly change the order of dates
    return pd.DataFrame({'date_1':row['date_1'],
                         'count':cnt,
                         'date_2':dates[:cnt]}
                       )

alldates = pd.date_range(df['date_1'].min() - pd.Timedelta(30, unit='D'), df['date_1'].max() - pd.Timedelta(30, unit='D')).to_numpy()
res = df.apply(lambda x : makedate2(x, alldates), axis=1)
df2 = pd.concat(res.to_numpy()).reset_index(drop=True)

Возможный df2используя предоставленные вами образцы данных:

        date_1  count     date_2
0   2019-01-09      5 2019-09-26
1   2019-01-09      5 2019-09-11
2   2019-01-09      5 2019-05-18
3   2019-01-09      5 2019-10-15
4   2019-01-09      5 2019-06-06
..         ...    ...        ...
130 2019-09-14     16 2019-04-12
131 2019-09-14     16 2019-04-05
132 2019-09-14     16 2019-10-08
133 2019-09-14     16 2019-05-05
134 2019-09-14     16 2019-11-09
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...