Pandas. Удаление записей о повторных ошибках из dataframe

Рейтинг: 0Ответов: 4Опубликовано: 03.08.2023

Привет всем!

Имеется dataframe типа:
SrvName Started Status Size
server1 2023-06-01 14:00:00 sucsess 10000
server2 2023-06-01 14:05:00 sucsess 12000
server3 2023-06-01 14:25:00 sucsess 20000
server1 2023-06-02 14:00:00 failed 0
server2 2023-06-01 14:05:00 sucsess 1000
server1 2023-06-02 14:10:00 failed 0
server1 2023-06-02 14:20:00 failed 0
server3 2023-06-02 14:25:00 sucsess 20000
server1 2023-06-02 14:25:00 failed 0
server1 2023-06-02 14:30:00 failed 0

итд.

Нужно дропнуть записи со значением failed в столбце Status которые возникли в течении 30 минут, после первого failed в разрезе SrvName (выделил болдом). В итоге должна остаться одна запись failed на сервер в диапазоне 30 минут от возникновения первого failed.

Update: События Started не обязательно могут быть кратным 5 минутам. Решил вот так:

import pandas as pd
df = pd.DataFrame([
['server1','2023-06-01 14:00:00','sucsess',10000],
['server2','2023-06-01 14:05:00','sucsess',12000],
['server3','2023-06-01 14:25:00','sucsess',20000],
['server1','2023-06-02 14:00:00','failed',0],
['server2','2023-06-01 14:05:00','sucsess',1000],
['server1','2023-06-02 14:10:00','failed',0],
['server1','2023-06-02 14:20:00','failed',0],
['server3','2023-06-02 14:25:00','sucsess',20000],
['server1','2023-06-02 14:25:00','failed',0],
['server1','2023-06-02 14:30:00','failed',0],
['server2','2023-07-02 14:30:00','failed',0]],
columns=['SrvName','Started','Status','Size'])
df['Started']=pd.to_datetime(df.Started)

df_temp=df.loc[df['Status']=='failed'].groupby(['SrvName'])['Started'].diff(periods=1).fillna(pd.Timedelta('01:30:00'))
df_temp2=df_temp[df_temp>pd.Timedelta('00:30:00')]
df_filtered = df.loc[(df.Status == 'sucsess') | df.index.isin(df_temp2.index)]

ИМХО несколько корявенько выглядит решение. Хотя бы тем, что я присваиваю первым событиям со статусом failed timedelta равное полутора часам, ну что бы априори было больше 30 минут. Кроме того, в этом решении фильтруются события со статусом failed произошедшие в течении 30 минут от предыдущего события, а не от того, что было первым. Вот это пока не смог победить.


Спасибо камрадам, что натолкнули на правильные мысли!

Ответы

▲ 1

Я взял побольше датафрейм, чтобы было на чем тестить.

import pandas as pd

columns = ['SrvNam',    'Started',  'Status',   'Size']
data = [['server1', '2023-06-01 14:00:00',  'sucsess',  '10000'],
['server2', '2023-06-01 14:05:00',  'sucsess',  12000],
['server3', '2023-06-01 14:25:00',  'sucsess',  20000],
['server1', '2023-06-02 14:00:00',  'failed',   0],
['server2', '2023-06-01 14:05:00',  'sucsess',  1000],
['server1', '2023-06-02 14:10:00',  'failed',   0],
['server1', '2023-06-02 14:20:00',  'failed',   0],
['server3', '2023-06-02 14:25:00',  'sucsess',  20000],
['server3', '2023-06-02 14:26:00',  'failed',   0],
['server2', '2023-06-02 14:30:00',  'failed',   0],
['server2', '2023-06-02 14:35:00',  'failed',   0],
['server3', '2023-06-02 14:28:00',  'failed',   0],
['server1', '2023-06-02 14:30:00',  'failed',   0],
['server2', '2023-06-02 15:01:00',  'failed',   0],
['server2', '2023-06-02 15:01:00',  'failed',   0],
['server3', '2023-06-02 15:30:00',  'failed',   0],
['server3', '2023-06-02 15:40:00',  'failed',   0]
]


df = pd.DataFrame(data, columns=columns)

Дальше, если у Вас колонка с датой/временем в формате datetime - работаем дальше, если в формате str переводим в datetime.

df.Started = pd.to_datetime(df.Started)

Далее так:

  1. Фильтруем столбец 'Status' по значению 'failed'
  2. Добавляем колонку с индексами внутрь датафрейма
  3. Группируем по именам серверов
  4. Внутри групп делаем ресемплирование по колонке даты/времени на блоки по 30 мин.
  5. Берем индекс минимального элемента в каждой группе
df_g = df[df.Status == 'failed'].reset_index().groupby('SrvNam').resample('30T', on='Started', origin='start').min()['index']

В результате мы получаем серию индексов первых элементов в каждой группе серверов, разбитых на отрезки по 30 мин.

Последним шагом применяем фильтрацию к исходному датафрейму с двумя условиями:

  1. Или статус == 'sucsess'
  2. Или берем индексы из полученного списка
df_filtered = df.loc[(df.Status == 'sucsess') | df.index.isin(df_g)]
▲ 1

Решение с помощью машины состояний. Создается функция - генератор, в которую поступают строки фрейма по очереди. Для отслеживания времени сбоев в разрезе серверов создан словарь lastf, в который пишется ключ "имя сервера" и значение - последний актуальный сбой. Если поступает новое время сбоя по тому же серверу с разницей более 180 секунд, информация обновляется на новое время. Далее реализована логика, которая на выходе возвращает резюме для каждой строки на удаление - истину или ложь.

import pandas as pd

columns = ['SrvNam', 'Started', 'Status', 'Size']
data = [['server1', '2023-06-01 14:00:00', 'sucsess', '10000'],
        ['server2', '2023-06-01 14:05:00', 'sucsess', 12000],
        ['server3', '2023-06-01 14:25:00', 'sucsess', 20000],
        ['server1', '2023-06-02 14:00:00', 'failed', 0],
        ['server2', '2023-06-01 14:05:00', 'sucsess', 1000],
        ['server1', '2023-06-02 14:10:00', 'failed', 0],
        ['server1', '2023-06-02 14:20:00', 'failed', 0],
        ['server3', '2023-06-02 14:25:00', 'sucsess', 20000],
        ['server3', '2023-06-02 14:26:00', 'failed', 0],
        ['server2', '2023-06-02 14:30:00', 'failed', 0],
        ['server2', '2023-06-02 14:35:00', 'failed', 0],
        ['server3', '2023-06-02 14:28:00', 'failed', 0],
        ['server1', '2023-06-02 14:30:00', 'failed', 0],
        ['server1', '2023-06-02 14:30:01', 'failed', 0],
        ['server2', '2023-06-02 15:01:00', 'failed', 0],
        ['server2', '2023-06-02 15:01:00', 'failed', 0],
        ['server1', '2023-06-02 14:45:01', 'failed', 0],
        ['server3', '2023-06-02 15:30:00', 'failed', 0],
        ['server1', '2023-06-02 15:40:00', 'failed', 0]
        ]


def state_machine():
    srv, started, status = yield
    lastf = {}
    tdelta = pd.Timedelta(30, 'm')
    while True:
        delete = False
        if status == 'failed' and lastf.get(srv):
            if started - lastf[srv] > tdelta:
                lastf[srv] = started
            else:
                delete = True
        elif status == 'failed' and lastf.get(srv) is None:
            lastf[srv] = started

        srv, started, status = yield delete


machine = state_machine()
next(machine)

df = pd.DataFrame(data, columns=columns)
df.Started = pd.to_datetime(df.Started, format='%Y-%m-%d %H:%M:%S')

df['К удалению'] = df.apply(lambda x: machine.send((x['SrvNam'], x['Started'], x['Status'])), axis=1)
print(df[df.SrvNam.eq('server1')])

df = df[df['К удалению'].eq(False)].drop(columns=['К удалению'])  # далее можно отфильтровать ненужные строки и удалив вспомогательный столбец
print(df)

Промежуточный отладочный вывод по серверу 1

     SrvNam             Started   Status   Size  К удалению
0   server1 2023-06-01 14:00:00  sucsess  10000       False
3   server1 2023-06-02 14:00:00   failed      0       False
5   server1 2023-06-02 14:10:00   failed      0        True
6   server1 2023-06-02 14:20:00   failed      0        True
12  server1 2023-06-02 14:30:00   failed      0        True
13  server1 2023-06-02 14:30:01   failed      0       False
16  server1 2023-06-02 14:45:01   failed      0        True
18  server1 2023-06-02 15:40:00   failed      0       False

Итоговый вывод

     SrvNam             Started   Status   Size
0   server1 2023-06-01 14:00:00  sucsess  10000
1   server2 2023-06-01 14:05:00  sucsess  12000
2   server3 2023-06-01 14:25:00  sucsess  20000
3   server1 2023-06-02 14:00:00   failed      0
4   server2 2023-06-01 14:05:00  sucsess   1000
7   server3 2023-06-02 14:25:00  sucsess  20000
8   server3 2023-06-02 14:26:00   failed      0
9   server2 2023-06-02 14:30:00   failed      0
13  server1 2023-06-02 14:30:01   failed      0
14  server2 2023-06-02 15:01:00   failed      0
17  server3 2023-06-02 15:30:00   failed      0
18  server1 2023-06-02 15:40:00   failed      0
▲ 0

Сделал вот так:

df_temp=df.loc[df['Status']=='failed'].groupby(['SrvNam'])['Started'].diff(periods=1).fillna(pd.Timedelta('01:30:00'))
df_temp2=df_temp[df_temp>pd.Timedelta('00:30:00')]
df_filtered = df.loc[(df.Status == 'sucsess') | df.index.isin(df_temp2.index)]

ИМХО несколько корявенько выглядит решение. Хотя бы тем, что я присваиваю первым событиям со статусом failed timedelta равное полутора часам, ну что бы априори было больше 30 минут. Кроме того, в этом решении фильтруются события со статусом failed произошедшие в течении 30 минут от предыдущего события, а не от того, что было первым. Вот это пока не смог победить.

▲ 0

я бы сделал проще:

Исходный df:

    SrvName             Started   Status   Size
0   server1 2023-06-01 14:11:11  sucsess  10000
1   server2 2023-06-01 14:05:00  sucsess  12000
2   server3 2023-06-01 14:25:00  sucsess  20000
3   server1 2023-06-02 14:11:00   failed      0
4   server2 2023-06-01 14:15:00  sucsess   1000
5   server1 2023-06-02 14:22:00   failed      0
6   server1 2023-06-02 14:27:00   failed      0
7   server3 2023-06-02 14:25:00  sucsess  20000
8   server1 2023-06-02 14:31:00   failed      0
9   server1 2023-06-02 14:33:00   failed      0
10  server1 2023-06-02 14:35:00   failed      0
11  server1 2023-06-02 14:45:00   failed      0
12  server1 2023-06-02 15:47:00   failed      0
13  server1 2023-06-02 15:52:00   failed      0
14  server1 2023-06-02 16:02:00   failed      0

решение:

# группируем по серверам и статусу:
for _, g in df[df["Status"]=="failed"].groupby("SrvName"):
# получаем диффы во времени, кратные 30 минутам от первого времени
    g["diff"] = (g["Started"].diff().cumsum()//pd.Timedelta(30, "min")).fillna(method="bfill")
# группируем по этим диффам, получаем индексы всех рядов, кроме первого
    for _, t in g.groupby("diff"):
        idx = t.iloc[1:].index
# удаляем эти ряды из исходного датафрейма
        df = df.drop(index=idx)

теперь df:

    SrvName             Started   Status   Size
0   server1 2023-06-01 14:11:11  sucsess  10000
1   server2 2023-06-01 14:05:00  sucsess  12000
2   server3 2023-06-01 14:25:00  sucsess  20000
3   server1 2023-06-02 14:11:00   failed      0
4   server2 2023-06-01 14:15:00  sucsess   1000
7   server3 2023-06-02 14:25:00  sucsess  20000
11  server1 2023-06-02 14:45:00   failed      0
12  server1 2023-06-02 15:47:00   failed      0