有效日期范围一键编码分组

时间:2019-05-30 20:16:10

标签: python pandas pandas-groupby dask

从此示例数据开始...

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分组,然后进行汇总以匹配目标输出。

向任何能提出适当利用Dask方法的人提供特殊奖励。由于可伸缩性,这就是我们在流水线的其他部分使用的东西。这可能是个白日梦,但我想我会把它寄出去,看看会回来什么。

4 个答案:

答案 0 :(得分:2)

这个问题很难,我只能想到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

答案 1 :(得分:1)

类似于@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

答案 2 :(得分:0)

这是一项根据beg_colend_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()来应用该功能,如果需要在分布式环境中运行该功能,则可以使用Dask中的.map_partitions()函数。只需首先将索引设置为您计划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

答案 3 :(得分: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年开始-我想将2019年的月份排除在2019年之外。此功能可以实现这一目标。
  • 应根据使用情况分析和更改参数“ DAILY”
  • 对于以NaT作为结束日期的观察,我认为每个月都为TRUE。我不是100%清楚OP希望如何处理它。如果用户希望以不同的方式处理此问题,我将所有空白值设置为一个明确的日期,以避免任何意外的结果