Эффективно фильтровать строки из подмножеств фрейма данных Pandas

#pandas #dataframe #filtering #data-manipulation

Вопрос:

У меня есть фрейм данных, состоящий из медицинских данных, где столбцы [«Patient_ID», «Код», «Данные»], где «Код» просто представляет некоторое медицинское взаимодействие, которое пациент «Patient_ID» имел на «Дату». У любого пациента, как правило, будет более одного ряда, так как у них более одного взаимодействия. Я хочу применить к этим данным два типа фильтрации.

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

Поэтому мне нужно изменить подмножества общего фрейма данных, но модификация включает в себя изменение размера подмножества. У меня оба они реализованы как часть более крупного конвейера, однако они являются существенным узким местом с точки зрения времени. Мне интересно, есть ли более эффективный способ добиться того же самого, так как я действительно просто собрал то, что сработало, и я не слишком хорошо знаком с эффективностью операций pandas. Вот как они у меня есть в настоящее время:

 def Filter_by_length(df, min_len = 1):
    print("Filtering short sequences...")
    df = df.sort_values(axis = 0, by = ['ID', 'DATE']).copy(deep = True)
    new_df = []
    for sub_df in tqdm((df[df.ID == sub] for sub in df.ID.unique()), total = len(df.ID.unique()), miniters = 1):
        if len(sub_df) >= min_len:
            new_df.append(sub_df.copy(deep = True))
    if len(new_df) != 0:
        df =  pd.concat(new_df, sort = False)
    else:
        df = pd.DataFrame({})
    print("Done")
    return df
 
 def shuffle_col(df, col):
    df[col] = np.random.permutation(df[col])
    return df

def Filter_by_redundancy(df, T, min_len = 1):
    print("Filtering redundant concepts and short sequences...")
    df = df.sort_values(axis = 0, by = ['ID', 'DATE']).copy(deep = True)
    new_df = []
    for sub_df in tqdm((df[df.ID == sub] for sub in df.ID.unique()), total = len(df.ID.unique()), miniters = 1):
        start_date = sub_df.DATE.min()
        end_date = sub_df.DATE.max()
        next_date = start_date   dt.timedelta(days = T)
        while start_date <= end_date:
            sub_df = pd.concat([sub_df[sub_df.DATE < start_date],
                                shuffle_col(sub_df[(sub_df.DATE <= next_date) amp; (sub_df.DATE >= start_date)]
                                            .drop_duplicates(subset = ['CODE']), "CODE"),
                                sub_df[sub_df.DATE > next_date]], sort = False )
            start_date  = dt.timedelta(days = int(T/2))
            next_date  = dt.timedelta(days = int(T/2))
        if len(sub_df) >= min_len:
            new_df.append(sub_df.copy(deep = True))
    if len(new_df) != 0:
        df =  pd.concat(new_df, sort = False)
    else:
        df = pd.DataFrame({})
    print("Done")
    return df
 

Как вы можете видеть, во втором случае я фактически применяю оба фильтра, потому что важно иметь возможность применять оба вместе или один отдельно, но я заинтересован в любом повышении производительности, которое может быть сделано для одного или обоих.

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

1. Получите длину группы df.groupby('id').size() , а затем просто удалите любой идентификатор с недостаточной длиной. Я не уверен, что делает ваш другой раздел в целом, но вы также можете заменить любое время, когда скажете for sub_df in [df[df['id'] == i] for i in df['id'].unique()] , на for id, sub_df in df.groupby('id')

2. Этот подход к длине намного быстрее, спасибо.

Ответ №1:

Для первой части, вместо того, чтобы считать в вашей группе таким образом, я бы использовал этот подход:

 >>> d = pd.DataFrame({'id': [1, 2, 3, 4, 5], 'q': [np.random.randint(1, 15, size=np.random.randint(1, 5)) for _ in range(5)]}).explode('q')
   id   q
0   1   1
0   1   9
1   2   9
1   2  10
1   2   4
2   3   3
2   3   6
2   3   2
2   3  10
3   4  11
3   4   5
4   5   5
4   5   6
4   5   3
4   5   2

>>> sizes = d.groupby('id').size()
>>> d[d['id'].isin(sizes[sizes >= 3].index)]  # index is list of IDs meeting criteria
   id   q
1   2   9
1   2  10
1   2   4
2   3   3
2   3   6
2   3   2
2   3  10
4   5   5
4   5   6
4   5   3
4   5   2
 

Я не совсем понимаю, почему вы хотите перетасовать свои коды в каком-то окне. Чтобы избежать проблемы X-Y, что вы на самом деле пытаетесь там сделать?

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

1. Я попробую первый подход, спасибо. Идея перетасовки заключается в том, что в целом важен последовательный характер данных, но в течение некоторого временного интервала порядок является произвольным. В качестве простейшего примера, если пациенту назначают 3 препарата в один и тот же день, то заказ, если он не важен для этих 3 препаратов. Это тоже реализация работы из документа, а не то, что я решил сделать сам. Конечно, возможно, что перетасовка окажется бессмысленной.