Эффективный диапазон дат One-Hot-Encode groupby

Начиная с этого примера данных...

import pandas as pd

start_data = {"person_id": [1, 1, 1, 1, 2], "nid": [1, 2, 3, 4, 1],
              "beg": ["Jan 1 2018", "Jan 5 2018", "Jan 10 2018", "Feb 5 2018", "Jan 25 2018"],
              "end": ["Feb 1 2018", "Mar 4 2018", "", "Oct 18 2018", "Nov 10 2018"]}
df = pd.DataFrame(start_data)
df["beg"] = pd.to_datetime(df["beg"])
df["end"] = pd.to_datetime(df["end"])

Отправная точка:

   person_id  nid        beg        end
0          1    1 2018-01-01 2018-02-01
1          1    2 2018-01-05 2018-03-04
2          1    3 2018-01-10        NaT
3          1    4 2018-02-05 2018-10-18
4          2    1 2018-01-25 2018-11-10

Выход цели:

person_id date       1 2 3 4
        1 2018-01-01 1 0 0 0
        1 2018-01-05 1 1 0 0
        1 2018-01-10 1 1 1 0
        1 2018-02-01 0 1 1 0
        1 2018-02-05 0 1 1 1
        1 2018-03-04 0 0 1 1
        1 2018-10-18 0 0 1 0 
        2 2018-01-25 1 0 0 0
        2 2018-11-10 0 0 0 0

Я пытаюсь связать все активные nid с соответствующим person_id Затем он будет присоединен к другому фрейму данных на основе последнего date меньше, чем датированный столбец активности. И, наконец, это будет частью входных данных для прогностической модели.

Делая что-то вроде pd.get_dummies(df["nid"]), получаем этот вывод:

   1  2  3  4
0  1  0  0  0
1  0  1  0  0
2  0  0  1  0
3  0  0  0  1
4  1  0  0  0

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

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

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
6
0
487
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Вопрос сложный, я могу думать только о numpy трансляции, чтобы ускорить цикл for

s=df.set_index('person_id')[['beg','end']].stack()
l=[]
for x , y in df.groupby('person_id'):
    y=y.fillna({'end':y.end.max()})
    s1=y.beg.values
    s2=y.end.values
    t=s.loc[x].values
    l.append(pd.DataFrame(((s1-t[:,None]).astype(float)<=0)&((s2-t[:,None]).astype(float)>0),columns=y.nid,index=s.loc[[x]].index))
s=pd.concat([s,pd.concat(l).fillna(0).astype(int)],1).reset_index(level=0).sort_values(['person_id',0])
s
Out[401]: 
     person_id          0  1  2  3  4
beg          1 2018-01-01  1  0  0  0
beg          1 2018-01-05  1  1  0  0
beg          1 2018-01-10  1  1  1  0
end          1 2018-02-01  0  1  1  0
beg          1 2018-02-05  0  1  1  1
end          1 2018-03-04  0  0  1  1
end          1 2018-10-18  0  0  0  0
beg          2 2018-01-25  1  0  0  0
end          2 2018-11-10  0  0  0  0

Подобно подходу @WenYoBen, немного отличающемуся в трансляции и возврате:

def onehot(group):
    pid, g = group

    ends = g.end.fillna(g.end.max())
    begs = g.beg

    days = pd.concat((ends,begs)).sort_values().unique()

    ret = pd.DataFrame((days[:,None] < ends.values) & (days[:,None]>= begs.values),
                    columns= g.nid)
    ret['persion_id'] = pid
    return ret


new_df = pd.concat([onehot(group) for group in df.groupby('person_id')], sort=False)
new_df.fillna(0).astype(int)

Выход:

    1   2   3   4   persion_id
0   1   0   0   0   1
1   1   1   0   0   1
2   1   1   1   0   1
3   0   1   1   0   1
4   0   1   1   1   1
5   0   0   1   1   1
6   0   0   0   0   1
0   1   0   0   0   2
1   0   0   0   0   2
Ответ принят как подходящий

Вот функция, которая выполняет горячее кодирование данных на основе диапазона дат beg_col и end_col. Крайним случаем, на который следует обратить внимание, является несколько дат начала действия для одного и того же столбца target. Вы можете добавить к функции несколько умных фильтров, чтобы справиться с этим, но я просто оставлю здесь простую версию.

def effective_date_range_one_hot_encode(x, beg_col = "beg", end_col = "end", target = "nid"):
    pos_change = x.loc[:, [beg_col, target]]
    pos_change = pos_change.set_index(beg_col)
    pos_change = pd.get_dummies(pos_change[target])

    neg_change = x.loc[:, [end_col, target]]
    neg_change = neg_change.set_index(end_col)
    neg_change = pd.get_dummies(neg_change[target]) * -1

    changes = pd.concat([pos_change, neg_change])

    changes = changes.sort_index()
    changes = changes.cumsum()

    return changes


new_df = df.groupby("person_id").apply(effective_date_range_one_hot_encode).fillna(0).astype(int)
new_df.index = new_df.index.set_names(["person_id", "date"])
new_df = new_df.reset_index()
new_df = new_df.dropna(subset=["date"], how = "any")

Функцию можно применить с помощью .groupby(), и если вам нужно запустить ее в распределенной среде, вы можете использовать функцию .map_partitions() в Dask. Просто сначала установите индекс для столбца, который вы планируете groupby, а затем создайте вспомогательную функцию для сброса индекса.

Выход

   person_id effective_date  1  2  3  4
0          1     2018-01-01  1  0  0  0
1          1     2018-01-05  1  1  0  0
2          1     2018-01-10  1  1  1  0
3          1     2018-02-01  0  1  1  0
4          1     2018-02-05  0  1  1  1
5          1     2018-03-04  0  0  1  1
6          1     2018-10-18  0  0  1  0
8          2     2018-01-25  1  0  0  0
9          2     2018-11-10  0  0  0  0

Немного поздно для OP, но это должно помочь другим, у которых есть эта проблема. Я столкнулся с очень похожей проблемой и решил ее следующим образом.

Исходные данные из OP:

start_data = {"person_id": [1, 1, 1, 1, 2], "nid": [1, 2, 3, 4, 1],
              "beg": ["Jan 1 2018", "Jan 5 2018", "Jan 10 2018", "Feb 5 2018", "Jan 25 2018"],
              "end": ["Feb 1 2018", "Mar 4 2018", "", "Oct 18 2018", "Nov 10 2018"]}
df = pd.DataFrame(start_data)
df["beg"] = pd.to_datetime(df["beg"])
df["end"] = pd.to_datetime(df["end"])

Предложенное решение:

from dateutil.rrule import rrule, DAILY

# Create an empty df which we'll append the results to 
months_df = pd.DataFrame( columns= ['jan', 'feb', 'mar', 'apr', 'may', 'jun',
        'july', 'aug', 'sep', 'oct', 'nov', 'dec'])

# Create function to loop through a list and remove any dates that occured before a certain date 
def remove_dates(date_range, date_range2):    
    for i in range(0,len(date_range)):
        if date_range[i] > datetime.datetime(2017,12,31):
            date_range2.append(date_range[i])
    return date_range2

months = [1,2,3,4,5,6,7,8,9,10,11,12] # this is used in the list comprehension 

for i in range(0, len(df)):
    # Return list of weeks that are in each date range (i.e. weeks between "Day of Start Date" and "Day of End Date")
    date_range = [dt for dt in rrule(DAILY, dtstart=df.loc[:,'beg'][i],\
                                     until=df.loc[:,'end'][i])]
    
    # Remove any dates that occurred before some arbitrary cutoff
    date_range2 = []
    date_range = remove_dates(date_range, date_range2)
    
    months_list = set([date.month for date in date_range]) # Return unique months
    months_list = [elem in months_list for elem in months] # Check which months of the year are present in the date range
    # Append results to months_df
    months_df = months_df.append(pd.DataFrame(months_list,\
                             index=['jan', 'feb', 'mar', 'apr', 'may', 'jun',
        'july', 'aug', 'sep', 'oct', 'nov', 'dec']).T, ignore_index=False)


df = df.join(months_df.reset_index(drop=True)) # Merge the two dfs

Выход

   person_id  nid        beg        end    jan   feb    mar    apr    may  \
0          1    1 2018-01-01 2018-02-01   True  True  False  False  False   
1          1    2 2018-01-05 2018-03-04   True  True   True  False  False   
2          1    3 2018-01-10        NaT   True  True   True   True   True   
3          1    4 2018-02-05 2018-10-18  False  True   True   True   True   
4          2    1 2018-01-25 2018-11-10   True  True   True   True   True   

     jun   july    aug    sep    oct    nov    dec  
0  False  False  False  False  False  False  False  
1  False  False  False  False  False  False  False  
2   True   True   True   True   True   True   True  
3   True   True   True   True   True  False  False  
4   True   True   True   True   True   True  False  

Комментарии:

  • Я включил функцию remove_dates. Это было потому, что я хотел исключить даты, которые произошли до какого-то произвольного отсечки. Например, я просматривал данные за 2019 год, но некоторые контракты могли начаться в 2018 году — я хотел исключить эти месяцы 2018 года из подсчета в 2019 году. Эта функция выполняет это.
  • Параметр «ЕЖЕДНЕВНО» следует анализировать и изменять в зависимости от варианта использования.
  • Для наблюдения с NaT в качестве конечной даты я считал это ИСТИННЫМ для каждого месяца. Мне не было на 100% ясно, как ОП хотел, чтобы с этим справились. В случае, если пользователь хочет обработать это по-другому, я бы установил все пустые значения как явную дату, чтобы избежать любых неожиданных результатов.

Другие вопросы по теме