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

                           Col1  Col2
Date          Range  Label
'2018-08-01'  1      A     900   815
                     B     850   820
                     C     800   820
                     D     950   840
              2      A     900   820
                     B     750   850
                     C     850   820
                     D     850   800

И я хотел бы выбрать все значения, пока Col1 не станет меньше, чем Col2. Как только у меня есть экземпляр, для которого Col1

                           Col1  Col2
Date          Range  Label
'2018-08-01'  1      A     900   815
                     B     850   820
              2      A     900   820

Я перепробовал несколько вариантов, но пока не нашел хорошего решения. Я могу легко сохранить все данные, для которых Col1> Col2 с:

df_new=df[df['Col1']>df['Col2']]

Но это не то, что мне нужно. Я также думал о циклическом прохождении индекса уровня 1 и обрезке фрейма данных с помощью pd.IndexSlice:

idx = pd.IndexSlice
idx_lev1=df.index.get_level_values(1).unique()

for j in (idx_lev1):
    df_lev1=df.loc[idx[:,j,:],:]
    idxs=df_lev1.index.get_level_values(2)[np.where(df_lev1['Col1']<df_lev1['Col2'])[0][0]-1]
    df_sliced= df_lev1.loc[idx[:,:,:idxs],:]

А затем объединить различные кадры данных впоследствии. Тем не менее, это неэффективно (мой фрейм данных содержит более 3 миллионов записей, поэтому я тоже должен учесть это), и у меня проблема в том, что индекс диапазона повторяется для разных дат, поэтому мне, вероятно, придется вложить 2 цикла или что-то похожее.

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

Если вы хотите сгенерировать приведенный выше кадр для тестирования, вы можете использовать:

from io import StringIO
s="""                         
Date  Range  Label  Col1  Col2
'2018-08-01'  1  A  900   815
'2018-08-01'  1  B  850   820
'2018-08-01'  1  C  800   820
'2018-08-01'  1  D  950   840
'2018-08-01'  2  A  900   820
'2018-08-01'  2  B  750   850
'2018-08-01'  2  C  850   820
'2018-08-01'  2  D  850   800
"""
df2 = pd.read_csv(StringIO(s),
             sep='\s+',
             index_col=['Date','Range','Label'])

< Сильный > Update :

Я пытался реализовать как решение из Adam.Er8, так и из Alexandre B. они отлично работают с тестовым фреймом данных, который я создал для SO, но не с реальными данными.
Проблема в том, что могут быть случаи, для которых значения Col1 всегда больше, чем Col2, и в этом случае я просто хотел бы сохранить все данные. Ни одно из предложенных решений не может реально решить эту проблему.

Для более реалистичного теста вы можете использовать этот пример:

s="""                         
Date  Range  Label  Col1  Col2
'2018-08-01'  1  1  900   815
'2018-08-01'  1  2  950   820
'2018-08-01'  1  3  900   820
'2018-08-01'  1  4  950   840
'2018-08-01'  2  1  900   820
'2018-08-01'  2  2  750   850
'2018-08-01'  2  3  850   820
'2018-08-01'  2  4  850   800
'2018-08-02'  1  1  900   815
'2018-08-02'  1  2  850   820
'2018-08-02'  1  3  800   820
'2018-08-02'  1  4  950   840
'2018-08-02'  2  1  900   820
'2018-08-02'  2  2  750   850
'2018-08-02'  2  3  850   820
'2018-08-02'  2  4  850   800
"""

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

5
baccandr 4 Июл 2019 в 15:14

4 ответа

Лучший ответ

Я пытался использовать {{ X0}} для нумерации каждой строки, затем найдите первую строку, которая имеет правильное условие, и используйте ее для фильтрации только тех строк, число которых меньше этого.

Попробуй это:

from collections import defaultdict

import pandas as pd
from io import StringIO

s="""
Date  Range  Label  Col1  Col2
'2018-08-01'  1  1  900   815
'2018-08-01'  1  2  950   820
'2018-08-01'  1  3  900   820
'2018-08-01'  1  4  950   840
'2018-08-01'  2  1  900   820
'2018-08-01'  2  2  750   850
'2018-08-01'  2  3  850   820
'2018-08-01'  2  4  850   800
'2018-08-02'  1  1  900   815
'2018-08-02'  1  2  850   820
'2018-08-02'  1  3  800   820
'2018-08-02'  1  4  950   840
'2018-08-02'  2  1  900   820
'2018-08-02'  2  2  750   850
'2018-08-02'  2  3  850   820
'2018-08-02'  2  4  850   800
"""
df = pd.read_csv(StringIO(s),
                 sep='\s+',
                 index_col=['Date', 'Range', 'Label'])

groupby_date_range = df.groupby(["Date", "Range"])
df["cumcount"] = groupby_date_range.cumcount()

first_col1_lt_col2 = defaultdict(lambda: len(df), df[df['Col1'] < df['Col2']].groupby(["Date", "Range"])["cumcount"].min().to_dict())

result = df[df.apply(lambda row: row["cumcount"] < first_col1_lt_col2[row.name[:2]], axis=1)].drop(columns="cumcount")
print(result)

Выход:

                          Col1  Col2
Date         Range Label            
'2018-08-01' 1     1       900   815
                   2       950   820
                   3       900   820
                   4       950   840
             2     1       900   820
'2018-08-02' 1     1       900   815
                   2       850   820
             2     1       900   820
3
Adam.Er8 5 Июл 2019 в 14:43

Вы можете сделать это следующим образом:

# create a dataframe with a similar structure as yours
data={
'Date': ['2019-04-08', '2019-06-27', '2019-04-05', '2019-05-01', '2019-04-09', '2019-06-19', '2019-04-25', '2019-05-18', '2019-06-10', '2019-05-19', '2019-07-01', '2019-04-07', '2019-03-31', '2019-04-01', '2019-06-09', '2019-04-17', '2019-04-27', '2019-05-27', '2019-06-29', '2019-04-24'],
'Key1': ['B', 'B', 'C', 'A', 'C', 'B', 'A', 'C', 'A', 'C', 'A', 'A', 'C', 'A', 'A', 'B', 'B', 'B', 'A', 'A'],
'Col1': [670, 860, 658, 685, 628, 826, 871, 510, 707, 775, 707, 576, 800, 556, 833, 551, 591, 492, 647, 414],
'Col2': [442, 451, 383, 201, 424, 342, 315, 548, 321, 279, 379, 246, 269, 461, 461, 371, 342, 327, 226, 467],
}

df= pd.DataFrame(data)
df.sort_values(['Date', 'Key1'], ascending=True, inplace=True)
df.set_index(['Date', 'Key1'], inplace=True)

# here the real work starts
# temporarily create a dataframe with the comparison
# which has a simple numeric index to be used later
# to slice the original dataframe
df2= (df['Col1']<df['Col2']).reset_index()

# we only want to see the rows from the first row
# to the last row before a row in which Col1<Col2
all_unwanted= (df2.loc[df2[0] == True, [0]])
if len(all_unwanted) > 0:
    # good there was such a row, so we can use it's index
    # to slice our dataframe
    show_up_to= all_unwanted.idxmin()[0]
else:
    # no, there was no such row, so just display everything
    show_up_to= len(df)
# use the row number to slice our dataframe
df.iloc[0:show_up_to]

Выход:

                 Col1  Col2
Date       Key1            
2019-03-31 C      800   269
2019-04-01 A      556   461
2019-04-05 C      658   383
2019-04-07 A      576   246
2019-04-08 B      670   442
2019-04-09 C      628   424
2019-04-17 B      551   371
--------------------------- <-- cutting off the following lines:
2019-04-24 A      414   467
2019-04-25 A      871   315
2019-04-27 B      591   342
2019-05-01 A      685   201
2019-05-18 C      510   548
2019-05-19 C      775   279
2019-05-27 B      492   327
2019-06-09 A      833   461
2019-06-10 A      707   321
2019-06-19 B      826   342
2019-06-27 B      860   451
2019-06-29 A      647   226
2019-07-01 A      707   379
0
jottbe 4 Июл 2019 в 13:23

Сначала мы создаем столбец "helper" для подсчета каждой группы. Затем мы фильтруем все строки в нашем groupby, где Col1 < Col2, и получаем cumcount выше этого:

df2['cumcount'] = df2.groupby(level=1).cumcount()

dfs = []

for idx, d in df2.groupby(level=1):
    n = d.loc[(d['Col1'] < d['Col2']), 'cumcount'].min()-1
    dfs.append(d.loc[d['cumcount'].le(n)])

df_final = pd.concat(dfs).drop('cumcount', axis=1)

< Сильный > Выход


                          Col1  Col2
Date         Range Label            
'2018-08-01' 1     A       900   815
                   B       850   820
             2     A       900   820
0
Erfan 4 Июл 2019 в 12:58

Другой способ - использовать np.where и выбрать первый индекс.

as_index=False в группе позволяет вам игнорировать столбец индекса в groupby. Ознакомьтесь с обсуждением.

Код:

df2 = df2.reset_index() \
         .groupby(by=["Range", "Date"], as_index=False) \
         .apply(lambda x: x.head(np.where(x.Col1 < x.Col2)[0][0])) \
         .set_index(["Date", "Range", "Label"])

print(df2)
#                           Col1  Col2
# Date         Range Label
# '2018-08-01' 1     A       900   815
#                    B       850   820
#              2     A       900   820
1
Alexandre B. 4 Июл 2019 в 13:01