Сопоставление длины субиндексов в фрейме данных pandas

#python #pandas

#python #pandas

Вопрос:

У меня есть многоиндексный фрейм данных, который выглядит следующим образом:

                    target_q_0  target_q_1  target_q_2  target_q_3  target_q_4
sample_nr  event                                                             
1          0         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           1         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           2         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           3         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           4         0.086743   -1.085944    1.304110   -0.174707   -0.037001
2          0         0.092704   -1.123734    1.368322   -0.206030   -0.006364
           1         0.092376   -1.121655    1.364788   -0.204306   -0.008050
           2         0.092057   -1.119634    1.361355   -0.202632   -0.009688
           3         0.091748   -1.117672    1.358021   -0.201005   -0.011279
3          0         0.092704   -1.123734    1.368322   -0.206030   -0.006364
          
         
 

Каждый образец может иметь разное количество событий.

Я хочу найти самую длинную выборку, то есть выборку с наибольшим количеством событий, и обнулить все остальные выборки, чтобы они соответствовали ей по длине.

Желаемый результат был бы таким:

                    target_q_0  target_q_1  target_q_2  target_q_3  target_q_4
sample_nr  event                                                             
1          0         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           1         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           2         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           3         0.086743   -1.085944    1.304110   -0.174707   -0.037001
           4         0.086743   -1.085944    1.304110   -0.174707   -0.037001
2          0         0.092704   -1.123734    1.368322   -0.206030   -0.006364
           1         0.092376   -1.121655    1.364788   -0.204306   -0.008050
           2         0.092057   -1.119634    1.361355   -0.202632   -0.009688
           3         0.091748   -1.117672    1.358021   -0.201005   -0.011279
           4         0          0            0          0                0
3          0         0.092704   -1.123734    1.368322   -0.206030   -0.006364
           1         0          0            0          0                0
           2         0          0            0          0                0
           3         0          0            0          0                0
           4         0          0            0          0                0
 

У меня есть рабочий способ сделать это, но он очень медленный.

 def pad_df(df):
    max_rows = df.index.get_level_values(1).max()
    for sample, new_df in df.groupby(level=0):
        new_df = (new_df.unstack(level=0).reindex(list(range(max_rows)),
                                                  fill_value=0))

        new_df = new_df.stack('sample_nr').swaplevel(0, 1).sort_index()
        df.loc[experiment_data.index.get_level_values(0) == sample] = new_df
 

Эта функция вызывается с моим полным фреймом данных experiment_data в качестве входных данных:

 experiment_data = load_some_stuff()
pad_df(experiment_data)
 

Ответ №1:

Если не удалось найти волшебный метод, справедливой стратегией может быть предварительное выделение требуемого массива и заполнение его с помощью цикла for . Обычно это намного быстрее, чем прямые операции с фреймом данных.

Требуемый MultiIndex для ответа массив может быть сгенерирован с использованием pd.MultiIndex.from_product() в вашем случае, поскольку длина каждого уровня фиксирована.

Данные

 import pandas as pd
from pandas import DataFrame
import io
import numpy as np

df = pd.read_csv(io.StringIO("""
sample_nr  event   target_q_0  target_q_1  target_q_2  target_q_3  target_q_4
1          0         0.086743   -1.085944    1.304110   -0.174707   -0.037001
1          1         0.086743   -1.085944    1.304110   -0.174707   -0.037001
1          2         0.086743   -1.085944    1.304110   -0.174707   -0.037001
1          3         0.086743   -1.085944    1.304110   -0.174707   -0.037001
1          4         0.086743   -1.085944    1.304110   -0.174707   -0.037001
2          0         0.092704   -1.123734    1.368322   -0.206030   -0.006364
2          1         0.092376   -1.121655    1.364788   -0.204306   -0.008050
2          2         0.092057   -1.119634    1.361355   -0.202632   -0.009688
2          3         0.091748   -1.117672    1.358021   -0.201005   -0.011279
3          0         0.092704   -1.123734    1.368322   -0.206030   -0.006364
"""), sep=r"s{2,}", engine="python", index_col=["sample_nr", "event"])
 

Код

 # 1. compute the sizes of each sample_nr
sr_sizes = df.groupby(df.index.get_level_values(0)).size()
# compute max size and #sample_nr
max_size = sr_sizes.max()
n_sample_nrs = len(sr_sizes)

# 2. preallocate the output array and fill
arr = np.zeros((max_size * n_sample_nrs, 5))
idx_lv0 = df.index.get_level_values(0)  # get sample_nr
for i in range(n_sample_nrs):
    row = i*max_size
    arr[row:row   sr_sizes.iloc[i], :] =
        df[idx_lv0 == sr_sizes.index[i]].values

# 3. convert to dataframe
df_ans = pd.DataFrame(
    data=arr,
    index=pd.MultiIndex.from_product([sr_sizes.index, range(max_size)]),
    columns=df.columns
).rename_axis(df.index.names, axis=0)
 

Результат

 print(df_ans)
                 target_q_0  target_q_1  target_q_2  target_q_3  target_q_4
sample_nr event                                                            
1         0        0.086743   -1.085944    1.304110   -0.174707   -0.037001
          1        0.086743   -1.085944    1.304110   -0.174707   -0.037001
          2        0.086743   -1.085944    1.304110   -0.174707   -0.037001
          3        0.086743   -1.085944    1.304110   -0.174707   -0.037001
          4        0.086743   -1.085944    1.304110   -0.174707   -0.037001
2         0        0.092704   -1.123734    1.368322   -0.206030   -0.006364
          1        0.092376   -1.121655    1.364788   -0.204306   -0.008050
          2        0.092057   -1.119634    1.361355   -0.202632   -0.009688
          3        0.091748   -1.117672    1.358021   -0.201005   -0.011279
          4        0.000000    0.000000    0.000000    0.000000    0.000000
3         0        0.092704   -1.123734    1.368322   -0.206030   -0.006364
          1        0.000000    0.000000    0.000000    0.000000    0.000000
          2        0.000000    0.000000    0.000000    0.000000    0.000000
          3        0.000000    0.000000    0.000000    0.000000    0.000000
          4        0.000000    0.000000    0.000000    0.000000    0.000000
 

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

1. Это действительно намного быстрее! Примерно в 30 раз быстрее, если быть точным, что является значительным улучшением. Единственная проблема, которую я обнаружил, — это скачкообразное использование памяти. Поскольку исходный фрейм данных имеет ~ 6 ГБ и потенциально может увеличиваться, предварительное выделение другого массива сопоставимого размера может быть проблемой. Я не уверен, как Python работает с выделением памяти в этом случае.

2. Вы можете использовать h5py для хранения массива на диске вместо ОЗУ. Или вы можете переделать свою работу в некоторую логику пакетной обработки. Это сильно зависит от вашего реального варианта использования. Возможно, это хорошо описано в другом вопросе.