У меня есть Dataframe pandas, который выглядит примерно так:
Для каждого идентификатора в моем df я хочу взять второе последнее время_1 (если оно существует). Затем я хочу сравнить это время с временными метками в time_2 и удалить все строки из моего df, где time_2 раньше, чем это время. Мой ожидаемый результат будет:
Эта проблема выше моего уровня панд. Я спросил ChatGPT и получил следующее решение, которое в принципе делает то, что я хочу:
import pandas as pd
ids = [101, 101, 101, 102, 102, 103, 103, 103]
time_1 = ['2024-06-20 14:32:22', '2024-06-20 15:21:31', '2024-06-20 15:21:31', '2024-06-20 16:26:51', '2024-06-20 16:26:51', '2024-06-20 20:05:44', '2024-06-20 22:41:22', '2024-06-20 23:11:56']
time_2 = ['2024-06-20 14:10:31', '2024-06-20 14:32:22', '2024-06-20 15:21:31', '2024-06-20 15:21:31', '2024-06-20 16:56:24', '2024-06-20 21:17:35', '2024-06-20 22:21:31', '2024-06-20 23:01:31']
df = pd.DataFrame({
'id': ids,
'time_1': pd.to_datetime(time_1),
'time_2': pd.to_datetime(time_2)
})
grouped = df.groupby('id')['time_1']
mask = pd.Series(False, index=df.index)
for id_value, group in df.groupby('id'):
# Remove duplicates and sort timestamps
unique_sorted_times = group['time_1'].drop_duplicates().sort_values()
# Check if there's more than one unique time
if len(unique_sorted_times) > 1:
# Select the second last time
second_last_time = unique_sorted_times.iloc[-2]
# Update the mask for rows with time_2 greater than or equal to the second last time_1
mask |= (df['id'] == id_value) & (df['time_2'] >= second_last_time)
else:
# If there's only one unique time, keep the row(s)
mask |= (df['id'] == id_value)
filtered_data = df[mask]
Моя проблема с этим решением - цикл for. Это кажется довольно неэффективным, и мои реальные данные довольно велики. А еще мне любопытно, есть ли лучшее и более эффективное решение для этой проблемы.
🤔 А знаете ли вы, что...
С Python можно создавать ботов для социальных сетей и мессенджеров.
Вот возможное решение, используя groupby
Я добавил пример с одним элементом в группе.
import pandas as pd
ids = [101, 101, 101, 102, 102, 103, 103, 103, 104]
time_1 = [
'2024-06-20 14:32:22', '2024-06-20 15:21:31', '2024-06-20 15:21:31',
'2024-06-20 16:26:51', '2024-06-20 16:26:51', '2024-06-20 20:05:44',
'2024-06-20 22:41:22', '2024-06-20 23:11:56', '2024-06-20 23:11:56']
time_2 = [
'2024-06-20 14:10:31', '2024-06-20 14:32:22', '2024-06-20 15:21:31',
'2024-06-20 15:21:31', '2024-06-20 16:56:24', '2024-06-20 21:17:35',
'2024-06-20 22:21:31', '2024-06-20 23:01:31', '2024-06-20 23:01:31']
df = pd.DataFrame({
'id': ids,
'time_1': pd.to_datetime(time_1),
'time_2': pd.to_datetime(time_2)
})
Мы определяем функцию, которая учитывает логику внутри группы
def fun(x):
if len(x) > 1:
unique_times = x['time_1'].unique()
if len(unique_times) >= 2:
second_last_time = unique_times[-2]
else:
second_last_time = unique_times[0]
x = x[x['time_2'].ge(second_last_time)]
return x
df.groupby('id').apply(lambda x: fun(x)).reset_index(drop=True)
id time_1 time_2
0 101 2024-06-20 15:21:31 2024-06-20 14:32:22
1 101 2024-06-20 15:21:31 2024-06-20 15:21:31
2 102 2024-06-20 16:26:51 2024-06-20 16:56:24
3 103 2024-06-20 23:11:56 2024-06-20 23:01:31
4 104 2024-06-20 23:11:56 2024-06-20 23:01:31
При таком подходе вы увидите выгоду, если ваш df станет больше. С фреймом данных из 90 000 строк я увидел улучшение на 25%.
IIUC, вы можете использовать groupby.transform с drop_duulates , чтобы удалить повторяющиеся значения, и .iloc[-2:-1].squeeze()
, чтобы получить предпоследние значения, если таковые имеются, иначе NaT. Затем выполните логическое индексирование:
out = df.loc[df.sort_values(by='time_1') # optional, if not already sorted
.groupby('id')['time_1']
.transform(lambda x: x.drop_duplicates().iloc[-2:-1].squeeze())
.fillna(pd.Timestamp(0))
.le(df['time_2'])
]
Выход:
id time_1 time_2
1 101 2024-06-20 15:21:31 2024-06-20 14:32:22
2 101 2024-06-20 15:21:31 2024-06-20 15:21:31
3 102 2024-06-20 16:26:51 2024-06-20 15:21:31
4 102 2024-06-20 16:26:51 2024-06-20 16:56:24
7 103 2024-06-20 23:11:56 2024-06-20 23:01:31
Промежуточные продукты:
id time_1 time_2 transform fillna
0 101 2024-06-20 14:32:22 2024-06-20 14:10:31 2024-06-20 14:32:22 2024-06-20 14:32:22
1 101 2024-06-20 15:21:31 2024-06-20 14:32:22 2024-06-20 14:32:22 2024-06-20 14:32:22
2 101 2024-06-20 15:21:31 2024-06-20 15:21:31 2024-06-20 14:32:22 2024-06-20 14:32:22
3 102 2024-06-20 16:26:51 2024-06-20 15:21:31 NaT 1970-01-01 00:00:00
4 102 2024-06-20 16:26:51 2024-06-20 16:56:24 NaT 1970-01-01 00:00:00
5 103 2024-06-20 20:05:44 2024-06-20 21:17:35 2024-06-20 22:41:22 2024-06-20 22:41:22
6 103 2024-06-20 22:41:22 2024-06-20 22:21:31 2024-06-20 22:41:22 2024-06-20 22:41:22
7 103 2024-06-20 23:11:56 2024-06-20 23:01:31 2024-06-20 22:41:22 2024-06-20 22:41:22
Вы можете использовать .transform() для создания маски.
Сортировка не обязательна, если вы можете просто использовать .nlargest() и выбрать второй, если он существует. Или, если time_1
уже отсортировано, вы можете вообще пропустить .nlargest()
(или сортировку).
Тогда вам просто нужно заменить NaT
на наименьшее возможное значение Timestamp, чтобы time_2
не могло быть раньше этого значения при сравнении.
second_last_times = df.groupby('id')['time_1'].transform(
lambda s: s.drop_duplicates().nlargest(2).iloc[1:].squeeze())
mask = second_last_times.fillna(pd.Timestamp.min).le(df['time_2'])
df[mask]
Результат:
id time_1 time_2
1 101 2024-06-20 15:21:31 2024-06-20 14:32:22
2 101 2024-06-20 15:21:31 2024-06-20 15:21:31
3 102 2024-06-20 16:26:51 2024-06-20 15:21:31
4 102 2024-06-20 16:26:51 2024-06-20 16:56:24
7 103 2024-06-20 23:11:56 2024-06-20 23:01:31
Для справки second_last_times
:
0 2024-06-20 14:32:22
1 2024-06-20 14:32:22
2 2024-06-20 14:32:22
3 NaT
4 NaT
5 2024-06-20 22:41:22
6 2024-06-20 22:41:22
7 2024-06-20 22:41:22
Name: time_1, dtype: datetime64[ns]
Если вы хотите обобщить это, замените .nlargest(2).iloc[1:]
на .nlargest(n).iloc[n-1:]
.
P.S. Это похоже на решение Mozway, но на самом деле я написал код до того, как они его опубликовали, за исключением техники squeeze
— спасибо за это.