Как я могу отфильтровать строки, в которых разница меньше порогового значения?

#python #pandas #dataframe #function #filter

Вопрос:

Проблема и контекст:

Я работаю с электронным оборудованием, которое записывает события, измеренные с помощью геркона. Он записывает время, в течение которого геркон открывается (записывается как 0) и закрывается (записывается как 1). Это оборудование иногда регистрирует ложные измерения в масштабах времени, которые, как мы знаем, нереалистичны. «Состояние по умолчанию» устройства обычно закрыто.

То, что я пытаюсь сделать:

Отфильтруйте изменения в состоянии, когда разница во времени составляет менее 60 секунд. Но…часто бывает множество ложных открытий и закрытий, следующих друг за другом. Кроме того, в результирующем кадре данных должны быть события, упорядоченные как 1,0,1,0,1 и т.д. — Устройство может только открываться, затем закрываться, затем открываться и т.д.

Что я пробовал:

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

Пример исходного кадра данных

 import numpy as np
import pandas as pd

df = pd.DataFrame({'date' : ['2019-05-10 12:20:31', '2019-05-10 12:25:43', '2019-05-10 12:25:57', '2019-05-10 12:28:00', '2019-05-10 12:30:01', '2019-05-10 12:35:00', '2019-05-10 12:35:30', '2019-05-10 12:35:45', '2019-05-10 12:37:00', '2019-05-10 12:40:00', '2019-05-10 12:45:00'], 'event' : [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]}, index=range(11))

df['date'] = pd.to_datetime(df['date'], errors='raise', dayfirst=True)
df['event'] = pd.to_numeric(df['event'])
 
 df
Out[21]: 
                  date  event
0  2019-05-10 12:20:31      1
1  2019-05-10 12:25:43      0
2  2019-05-10 12:25:57      1
3  2019-05-10 12:28:00      0
4  2019-05-10 12:30:01      1
5  2019-05-10 12:35:00      0
6  2019-05-10 12:35:30      1
7  2019-05-10 12:35:45      0
8  2019-05-10 12:37:00      1
9  2019-05-10 12:40:00      0
10 2019-05-10 12:45:00      1
 

Созданные функции

 
def filter_event_filter_dups(input_data, input_variable_name):
    """
    This is an 'inside' function for the main filter_event function and iteratively removes duplicates

    """
    df_tmp = input_data.copy()
    df_tmp = df_tmp.dropna()
    df_tmp = df_tmp.reset_index()
    df_tmp_final = pd.DataFrame(columns=['date', input_variable_name], index=range(len(df_tmp)))
    for index, row in df_tmp.iterrows():
        if index == 0:
            print('')
            df_tmp_final['date'].loc[index] = df_tmp['date'].loc[index]
            df_tmp_final[input_variable_name].loc[index] = df_tmp[input_variable_name].loc[index]
        else:
            if index   1 == len(df_tmp):
                print('---------------------------------------------------------------')
                print('Index = ', index)
                print('End!')
            else:
                print('---------------------------------------------------------------')
                print('Index = ', index)
                
                if df_tmp[input_variable_name].loc[index] == df_tmp[input_variable_name].loc[index-1]:
                    # keep only first
                    # delete second
                    # df_tmp = df_tmp.drop([df_tmp.loc[index 1]])
                    df_tmp_final['date'].loc[index-1] = df_tmp['date'].loc[index-1]
                    df_tmp_final[input_variable_name].loc[index-1] = df_tmp[input_variable_name].loc[index-1]
                elif df_tmp[input_variable_name].loc[index] != df_tmp[input_variable_name].loc[index-1]:
                    df_tmp_final['date'].loc[index] = df_tmp['date'].loc[index]
                    df_tmp_final[input_variable_name].loc[index] = df_tmp[input_variable_name].loc[index]
    return(df_tmp_final)


def filter_event(input_data, input_variable_name, input_delta_time_threshold_seconds):
    """
    This function is designed to filer out events which are very close together.
    """
    df_tmp = input_data.reset_index()
    df_tmp[input_variable_name] = pd.to_numeric(df_tmp[input_variable_name])
    del df_tmp['index']
    # output_df = pd.DataFrame()
    output_df = pd.DataFrame(columns=['date', input_variable_name], index=range(len(df_tmp)))
    for index, row in df_tmp.iterrows():
        if index 1 == len(df_tmp):
            print('---------------------------------------------------------------')
            print('Index = ', index)
            print('End!')
        else:
            # if df_tmp[input_variable_name].loc[index] == 0:
            print('---------------------------------------------------------------')
            print('Index = ', index)
            if ((df_tmp['date'].iloc[index 1] - df_tmp['date'].iloc[index]).total_seconds())<input_delta_time_threshold_seconds:
                # If less than threshold
                print('This is LESS than threshold - Threshold = ', input_delta_time_threshold_seconds, 'seconds')
                print('Time difference to next 0 = ', df_tmp['date'].loc[index 1]-df_tmp['date'].loc[index])
                print('Ignore')
            if ((df_tmp['date'].iloc[index 1] - df_tmp['date'].iloc[index]).total_seconds())>input_delta_time_threshold_seconds:
                # If more than threshold
                print('This is MORE than threshold - Threshold = ', input_delta_time_threshold_seconds, 'seconds') 
                print('Time difference to next 0 = ', df_tmp['date'].loc[index 1]-df_tmp['date'].loc[index])
                output_df['date'].loc[index] = df_tmp['date'].loc[index]
                output_df[input_variable_name].loc[index] = df_tmp[input_variable_name].loc[index]
                # output_df['date'].loc[index 1] = df_tmp['date'].loc[index 1]
                # output_df[input_variable_name].loc[index 1] = df_tmp[input_variable_name].loc[index 1]
    output_df = output_df.dropna()
    output_df = output_df.reset_index()
    
    output_df[input_variable_name] = pd.to_numeric(output_df[input_variable_name])
    #
    #
    #
    output_df_final = filter_event_filter_dups(input_data = output_df, input_variable_name = input_variable_name)
    
    # for i in range(2):
    #     output_df_final = filter_event_filter_dups(input_data = output_df_final, input_variable_name = input_variable_name)
    
    output_df_final = output_df_final.dropna()
    output_df_final[input_variable_name] = pd.to_numeric(output_df_final[input_variable_name])
    # output_df_final['date'] = pd.to_datetime(output_df_final, errors='raise', dayfirst=True)
    output_df_final['date'] = output_df_final['date'].apply(pd.to_datetime)


    return(output_df_final)
    

test = filter_event(input_data = df
                    , input_variable_name = 'event'
                    , input_delta_time_threshold_seconds = 60
                    )

 

Как должен выглядеть окончательный DF

 final_df
Out[20]: 
                 date  event
0 2019-05-10 12:20:31      1
1 2019-05-10 12:28:00      0
2 2019-05-10 12:30:01      1
3 2019-05-10 12:40:00      0
4 2019-05-10 12:45:00      1

 

Ответ №1:

Вы могли бы использовать маску:

 mask = df['date'].diff().lt('60s')
df[~(mask|mask.shift(-1))]
 

и если вы хотите обеспечить обмен событиями:

 mask = df['date'].diff().lt('60s')
df[~(mask|mask.shift(-1))][lambda d: d['event'].ne(d['event'].shift())]
 

выход:

                   date  event
0  2019-05-10 12:20:31      1
3  2019-05-10 12:28:00      0
4  2019-05-10 12:30:01      1
9  2019-05-10 12:40:00      0
10 2019-05-10 12:45:00      1
 

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

1. Не-меньше-чем, а не больше-равно, чтобы избежать наполнения, это приятный штрих 🙂

2. спасибо @Cimbali , также я понимаю, что забыл условие обмена :p

Ответ №2:

С .diff() помощью мы можем получить разницу во временных метках и удалить их с интервалом менее 60 секунд:

 >>> delay = df['date'].diff().dt.total_seconds()
>>> df_min60s = df[delay.fillna(np.inf).ge(60) amp; delay.shift(-1).fillna(np.inf).ge(60)]
>>> df_min60s
                  date  event
0  2019-05-10 12:20:31      1
3  2019-05-10 12:28:00      0
4  2019-05-10 12:30:01      1
8  2019-05-10 12:37:00      1
9  2019-05-10 12:40:00      0
10 2019-05-10 12:45:00      1
 

Тогда мы сможем удалить повторяющиеся события:

 >>> df_final = df_min60s[df_min60s['event'].ne(df_min60s['event'].shift())]
>>> df_final
                  date  event
0  2019-05-10 12:20:31      1
3  2019-05-10 12:28:00      0
4  2019-05-10 12:30:01      1
9  2019-05-10 12:40:00      0
10 2019-05-10 12:45:00      1
 

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

1. Причина, по которой 0 сохраняется в 12:28 вместо 12:25, заключается в том, что открытие (0) в 12:25:43 продолжается закрытием (1) в 12:25:57, что составляет менее 60 секунд. Следовательно, эти два события являются ложными измерениями. Следовательно, мы сохраняем 12:28, так как это следующее реальное событие открытия. Я надеюсь, что в этом есть смысл.

2. Ах, я вижу @CairanVanRooyen, когда вы удаляете пары событий, а не отдельные события. Я привыкну.

3. Действительно, позиция «по умолчанию» закрыта. Если мы обнаружим, что устройство открывается (0), а затем закрывается (1) менее чем через 60 секунд, то измерения открытия и закрытия являются ложными.

4. ОК @CairanVanRooyen, отмечено. Может ли устройство повторно открыться менее чем через 60 секунд после последнего закрытия? (Значит, разрыв между одной действительной парой открытия-закрытия и следующей будет меньше 60?)

5. Устройство может снова открыться менее чем через 60 секунд после закрытия, и мы тоже хотим это игнорировать. Поэтому иногда устройство будет измерять множество событий открытия, закрытия, открытия, закрытия в течение нескольких секунд друг от друга, и мне нужен код, чтобы игнорировать все события, когда они находятся на расстоянии менее 60 секунд друг от друга.