Скользящее количество окон за интервал дат в пандах

У меня есть история проектов и связанные с ними запланированные времена начала и окончания:

id   planned_start planned_end
1    2017-09-12    2017-09-13
2    2017-09-12    2017-09-14
3    2017-09-12    2017-09-13
4    2017-09-13    2017-09-13
5    2017-09-12    2017-09-12
6    2017-09-12    2017-09-20
7    2017-09-14    2017-09-15
8    2017-09-14    2017-09-20

Я хочу посчитать количество одновременных проектов для каждой из дат начала вышеуказанных проектов. Вот моя логика для этого:

for project_id in df['id']:
    start_date = df[df['id'] == project_id]['planned_start'].values[0]
    concurrent_projects = df[(df['planned_start'] <= start_date) & (df['planned_end'] >= start_date)]
    df.ix[df['id'] == project_id, 'concurrent_projects'] = concurrent_projects.shape[0]

Который производит это:

   id planned_start planned_end  concurrent_projects
0   1    2017-09-12  2017-09-13                  5.0
1   2    2017-09-12  2017-09-14                  5.0
2   3    2017-09-12  2017-09-13                  5.0
3   4    2017-09-13  2017-09-13                  5.0
4   5    2017-09-12  2017-09-12                  5.0
5   6    2017-09-12  2017-09-20                  5.0
6   7    2017-09-14  2017-09-15                  4.0
7   8    2017-09-14  2017-09-20                  4.0

Тем не менее, я знаю, насколько неоптимальным, с точки зрения времени, выше for петля есть. На самом деле у меня есть более 500000 проектов, для которых мне нужно сделать эту математику. Может кто-нибудь дать совет, как это ускорить? Я знаю, что должно быть решение для чистых панд или даже тупица, которое убило бы то, что я получил выше.

3 ответа

Решение

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

s = df.planned_start.values
e = df.planned_end.values

s_ = s >= s[:, None]
e_ = s <= e[:, None]

df.assign(concurrent_projects=(e_ & s_).sum(0))

   id planned_start planned_end  concurrent_projects
0   1    2017-09-12  2017-09-13                    5
1   2    2017-09-12  2017-09-14                    5
2   3    2017-09-12  2017-09-13                    5
3   4    2017-09-13  2017-09-13                    5
4   5    2017-09-12  2017-09-12                    5
5   6    2017-09-12  2017-09-20                    5
6   7    2017-09-14  2017-09-15                    4
7   8    2017-09-14  2017-09-20                    4

Мои извинения, у меня нет времени объяснять. Но я не хотел оставлять тебя в покое

k = len(df)

d = np.column_stack([df.planned_start.values, df.planned_end.values + 1]).ravel()

i = np.tile([1, -1], k)
a = d.argsort()

f = np.arange(k).repeat(2)
r = np.zeros(k, int)
z = np.zeros(k, int)
m = np.zeros(k, int)

cumsum = 0

for j in range(f.size):
    x = f[a[j]]
    y = i[a[j]]
    r[x] = cumsum
    z[x] = (y + 1) // 2
    r += y * z
    m = np.column_stack([m, r]).max(1)
    cumsum += y

m

array([5, 5, 5, 5, 5, 5, 4, 4])

Это мое решение, используя crosstabв основном использует martix из четных, чтобы сделать расчет (вход Dataframedf2):

df=pd.crosstab(df2.planned_end,df2.planned_start,margins=True)
df=pd.concat([df,pd.DataFrame(columns=list(set(df.index)- set(df.columns)))]).fillna(0)
df2['concurrent_projects']=df2.planned_start.map(df.loc['All',:].cumsum()-df.All.cumsum().shift().fillna(0))



df2
Out[112]: 
   id planned_start planned_end  concurrent_projects
0   1    2017-09-12  2017-09-13                  5.0
1   2    2017-09-12  2017-09-14                  5.0
2   3    2017-09-12  2017-09-13                  5.0
3   4    2017-09-13  2017-09-13                  5.0
4   5    2017-09-12  2017-09-12                  5.0
5   6    2017-09-12  2017-09-20                  5.0
6   7    2017-09-14  2017-09-15                  4.0
7   8    2017-09-14  2017-09-20                  4.0

С помощью apply дает примерно 3-кратное ускорение.

Текущий подход:

%%timeit
def concurrent_count_using_loop():
    for project_id in df['id']:
        start_date = df[df['id'] == project_id]['planned_start'].values[0]
        concurrent_projects = df[(df['planned_start'] <= start_date) & (df['planned_end'] >= start_date)]
        df.ix[df['id'] == project_id, 'concurrent_projects'] = concurrent_projects.shape[0]

concurrent_count_using_loop()

# 10 loops, best of 3: 21.4 ms per loop

С apply():

%%timeit
def concurrent_count(project):
    valid_start = df.planned_start <= project["planned_start"]
    valid_end = df.planned_end >= project["planned_start"]
    return (valid_start & valid_end).sum()

df["concurrent_projects"] = df.apply(concurrent_count, axis=1)

# 100 loops, best of 3: 6.94 ms per loop
Другие вопросы по тегам