У меня есть два фрейма данных под названием df и ranges:
data = {
'group': ['A', 'B', 'A', 'C', 'B'],
'start': [10, 20, 15, 30, 25],
'end': [50, 40, 60, 70, 45],
'val1': [5, 10, 11, 12, 6],
'val2': [5, 2, 1, 1, 0],
}
df = pd.DataFrame(data)
data = {
'group': ['A', 'B', 'C'],
'start': [0, 5, 25],
'end': [50, 7, 35],
}
ranges = pd.DataFrame(data)
Моя цель — объединить строки в df вместе на основе того, попадают ли они в один и тот же диапазон, определенный в ranges. Я хотел бы объединить их вместе так, чтобы для каждого столбца val1, val2 я получал min, max, mean, sum этого столбца в контексте группы агрегации.
Загвоздка в том, что мне нужно сделать это примерно для 5000 диапазонов в ranges и 500 000 строк в df. Поэтому мне нужно быстрое, но эффективное (относительно) решение с точки зрения памяти. Я открыт для решений с использованием подобных фреймворков, таких как vaex.
Ожидаемый результат, где range_id — это просто способ идентификации групп, предполагая, что они не уникальны:
range_id val1 val2
min max mean sum min max mean sum
0 0 5 5 5.0 5 5 5 5.0 5
Да, в моем примере этого не было.
Под «да» вы имеете в виду: «да, одна и та же группа может иметь несколько диапазонов»? Например, входные данные для ranges также могут быть data = {'group': ['A', 'A'],'start': [0, 5],'end': [50, 7]}? Если да, то вы приняли ответ, который (по крайней мере, в его нынешней форме) не соответствует этому подходу (@mozway, в остальном отличный map подход)... И в этом случае, пожалуйста, обновите свой вопрос, указав пример ranges, который включает эту возможность, а также желаемый результат, основанный на этих скорректированных входных данных.
Извините, мне следовало выразиться яснее, да, одна и та же группа может иметь несколько диапазонов. Я скорректировал пример, чтобы описать это.
@donkey Понятно, однако твой пример не совсем ясен. Не существует A, который соответствовал бы второй группе A из ranges. Поскольку на этот вопрос уже получено много хороших ответов на исходное описание проблемы, я бы рекомендовал открыть новый вопрос с хорошо продуманным примером.
@mozway: Я поддерживаю это предложение. @donkey: пожалуйста, верните вопрос к предыдущей версии без дубликатов в ranges. Затем создайте новую публикацию для варианта с дубликатами, сделав ссылку на эту публикацию для справки. В новом сообщении четко покажите, как наличие дубликатов меняет желаемый результат, и включите этот результат в сообщение. Недавнее обновление здесь делает недействительными все существующие ответы и сводит на нет значительные усилия нескольких участников, а это не то, как SO должен работать.






Вот неэффективное решение с использованием памяти:
ranges['range_id'] = ranges.index
df_merged = pd.merge(df, ranges, on='group')
df_filtered = df_merged[(df_merged['start_x'] >= df_merged['start_y'])
& (df_merged['end_x'] <= df_merged['end_y'])]
aggregation_dict = {"val1": ['min', 'max', 'mean', 'sum'],
"val2": ['min', 'max', 'mean', 'sum']}
result = df_filtered.groupby('range_id').agg(aggregation_dict).reset_index()
Для эффективной работы вам необходимо использовать мощные возможности Pandas groupby и aggregation:
import pandas as pd
import numpy as np
data_df = {
'group': ['A', 'B', 'A', 'C', 'B'],
'start': [10, 20, 15, 30, 25],
'end': [50, 40, 60, 70, 45],
'val1': ['5', '10', '11', '12', '6'],
'val2': ['5', '2', '1', '1', '0'],
}
data_ranges = {
'group': ['A', 'B', 'C'],
'start': [0, 5, 25],
'end': [50, 7, 35],
}
df = pd.DataFrame(data_df)
ranges = pd.DataFrame(data_ranges)
df['val1'] = pd.to_numeric(df['val1'])
df['val2'] = pd.to_numeric(df['val2'])
def aggregate_range(df, ranges):
results = []
for idx, row in ranges.iterrows():
mask = (
(df['group'] == row['group']) &
(df['start'] >= row['start']) &
(df['end'] <= row['end'])
)
filtered_df = df[mask]
if not filtered_df.empty:
agg_dict = {
'val1_min': filtered_df['val1'].min(),
'val1_max': filtered_df['val1'].max(),
'val1_mean': filtered_df['val1'].mean(),
'val1_sum': filtered_df['val1'].sum(),
'val2_min': filtered_df['val2'].min(),
'val2_max': filtered_df['val2'].max(),
'val2_mean': filtered_df['val2'].mean(),
'val2_sum': filtered_df['val2'].sum(),
}
agg_dict['range_id'] = idx
results.append(agg_dict)
return pd.DataFrame(results)
aggregated_df = aggregate_range(df, ranges)
aggregated_df = aggregated_df.set_index('range_id')
aggregated_df.columns = pd.MultiIndex.from_tuples(
[('val1', 'min'), ('val1', 'max'), ('val1', 'mean'), ('val1', 'sum'),
('val2', 'min'), ('val2', 'max'), ('val2', 'mean'), ('val2', 'sum')]
)
print(aggregated_df)
Iterrows конечно не быстрый
@sammywemmy на самом деле это не так уж и драматично для больших DataFrames, и это эффективно использует память. Я добавил к своему ответу некоторые профили таймингов/памяти.
хороший! молодец @mozway
если подумать об этом сейчас, то iterrows будет эффективно использовать память, потому что он выполняет одну строку за раз, и это имеет смысл. производительность, однако, я ожидаю, что она будет плохой (я думаю, все относительно), поскольку размер данных (диапазонов растет), чем больше ranges становится, тем медленнее обычно он становится
Вероятно, это правда, я использовал до 20 групп в своих тестах, для большего количества групп ситуация может стать хуже (хотя groupby тоже повлияет)
IIUC, я бы предварительно отфильтровал фрейм данных с помощью карты и логического индексирования , а затем выполнил бы классический groupby.agg. При этом маски и промежуточный (отфильтрованный) DataFrame должны быть минимальными для повышения эффективности использования памяти, а также минимизировать размер входных данных для groupby.
# columns to aggregate
cols = ['val1', 'val2']
# ensure data is numeric
df[cols] = df[cols].astype(int)
# optional, just to avoid having to `set_index` twice
tmp = ranges.set_index('group')
# pre-filter the rows for memory efficiency
# then perform a groupby.agg
out = (df[( df['start'].ge(df['group'].map(tmp['start']))
&df['end'].le(df['group'].map(tmp['end'])))]
.groupby('group', as_index=False)[cols].agg(['min', 'max', 'mean', 'sum'])
)
Выход:
group val1 val2
min max mean sum min max mean sum
0 A 5 5 5.0 5 5 5 5.0 5
Промежуточный перед groupby:
group start end val1 val2
0 A 10 50 5 5
@sammywemmy предложил вариант моего решения. Вместо того, чтобы вычислять все агрегаты одновременно в groupby.agg, вы можете вычислить их по отдельности и объединить их с помощью concat. Это быстрее и потенциально немного более эффективно с точки зрения памяти.
from itertools import product
cols = ['val1', 'val2']
tmp = ranges.set_index('group')
grouped = (df[( df['start'].ge(df['group'].map(tmp['start']))
&df['end'].le(df['group'].map(tmp['end'])))]
).groupby('group')
aggs = ['min','mean','max','sum']
bunch = product(cols, aggs)
contents = []
for col, _agg in bunch:
outcome = grouped[col].agg(_agg)
outcome.name = (col,_agg)
contents.append(outcome)
out = pd.concat(contents,axis=1)
пример производящей функции
def init(N):
df = pd.DataFrame({'group': np.random.randint(0, 20, N),
'start': np.random.randint(0, 100, N),
'end': np.random.randint(0, 100, N),
'val1': np.random.randint(0, 100, N),
'val2': np.random.randint(0, 100, N),
})
# ensure start <= end
df[['start', 'end']] = np.sort(df[['start', 'end']], axis=1)
group = df['group'].unique()
ranges = pd.DataFrame({'group': group,
'start': np.random.randint(0, 110, len(group)),
'end': np.random.randint(0, 110, len(group)),
})
ranges[['start', 'end']] = np.sort(ranges[['start', 'end']], axis=1)
return df, ranges
по 10 млн строк
# mozway_pre_filter
Line # Mem usage Increment Occurrences Line Contents
=============================================================
49 711.2 MiB 711.2 MiB 1 def mozway_pre_filter(df, ranges):
50 # columns to aggregate
51 711.2 MiB 0.0 MiB 1 cols = ['val1', 'val2']
52
53 # ensure data is numeric
54 863.9 MiB 152.6 MiB 1 df[cols] = df[cols].astype(int)
55
56 # optional, just to avoid having to `set_index` twice
57 863.9 MiB 0.0 MiB 1 tmp = ranges.set_index('group')
58
59 # pre-filter the rows for memory efficiency
60 # then perform a groupby.agg
61 950.9 MiB 11.2 MiB 4 return (df[( df['start'].ge(df['group'].map(tmp['start']))
62 881.6 MiB 9.5 MiB 1 &df['end'].le(df['group'].map(tmp['end'])))]
63 950.7 MiB -66.6 MiB 2 .groupby('group', as_index=False)[cols].agg(['min', 'max', 'mean', 'sum'])
# donkey_merge
Line # Mem usage Increment Occurrences Line Contents
=============================================================
66 884.4 MiB 884.4 MiB 1 def donkey_merge(df, ranges):
67 884.4 MiB 0.0 MiB 1 ranges = ranges.assign(range_id=ranges.index)
68 1484.4 MiB 600.1 MiB 1 df_merged = pd.merge(df, ranges, on='group')
69 1602.8 MiB 109.0 MiB 2 df_filtered = df_merged[(df_merged['start_x'] >= df_merged['start_y'])
70 1494.0 MiB 9.4 MiB 1 & (df_merged['end_x'] <= df_merged['end_y'])]
71 1602.8 MiB 0.0 MiB 2 aggregation_dict = {"val1": ['min', 'max', 'mean', 'sum'],
72 1602.8 MiB 0.0 MiB 1 "val2": ['min', 'max', 'mean', 'sum']}
73 1585.3 MiB -17.6 MiB 1 return df_filtered.groupby('range_id').agg(aggregation_dict).reset_index()
# Nayem_aggregate_range
Line # Mem usage Increment Occurrences Line Contents
=============================================================
19 905.9 MiB 905.9 MiB 1 def Nayem_aggregate_range(df, ranges):
20 905.9 MiB 0.0 MiB 1 results = []
21 961.5 MiB 0.0 MiB 21 for idx, row in ranges.iterrows():
22 961.5 MiB 0.0 MiB 20 mask = (
23 961.5 MiB 55.6 MiB 60 (df['group'] == row['group']) &
24 961.5 MiB 0.0 MiB 20 (df['start'] >= row['start']) &
25 961.5 MiB 0.0 MiB 20 (df['end'] <= row['end'])
26 )
27 961.5 MiB 0.0 MiB 20 filtered_df = df[mask]
28 961.5 MiB 0.0 MiB 20 if not filtered_df.empty:
29 961.5 MiB 0.0 MiB 20 agg_dict = {
30 961.5 MiB 0.0 MiB 20 'val1_min': filtered_df['val1'].min(),
31 961.5 MiB 0.0 MiB 20 'val1_max': filtered_df['val1'].max(),
32 961.5 MiB 0.0 MiB 20 'val1_mean': filtered_df['val1'].mean(),
33 961.5 MiB 0.0 MiB 20 'val1_sum': filtered_df['val1'].sum(),
34 961.5 MiB 0.0 MiB 20 'val2_min': filtered_df['val2'].min(),
35 961.5 MiB 0.0 MiB 20 'val2_max': filtered_df['val2'].max(),
36 961.5 MiB 0.0 MiB 20 'val2_mean': filtered_df['val2'].mean(),
37 961.5 MiB 0.0 MiB 20 'val2_sum': filtered_df['val2'].sum(),
38 }
39 961.5 MiB 0.0 MiB 20 agg_dict['range_id'] = idx
40 961.5 MiB 0.0 MiB 20 results.append(agg_dict)
41 961.5 MiB 0.0 MiB 1 aggregated_df = pd.DataFrame(results)
42 961.5 MiB 0.0 MiB 1 aggregated_df = aggregated_df.set_index('range_id')
43 961.5 MiB 0.0 MiB 2 aggregated_df.columns = pd.MultiIndex.from_tuples(
44 961.5 MiB 0.0 MiB 1 [('val1', 'min'), ('val1', 'max'), ('val1', 'mean'), ('val1', 'sum'),
45 ('val2', 'min'), ('val2', 'max'), ('val2', 'mean'), ('val2', 'sum')]
46 )
47 961.5 MiB 0.0 MiB 1 return aggregated_df
# user24714692_merge_agg_
Line # Mem usage Increment Occurrences Line Contents
=============================================================
3 879.9 MiB 879.9 MiB 1 def user24714692_merge_agg_(df, ranges):
4 1429.1 MiB 549.2 MiB 1 mdf = pd.merge(df, ranges, on='group', suffixes=('', '_range'))
5
6 1527.7 MiB 70.3 MiB 2 fdf = mdf[
7 1457.4 MiB 19.1 MiB 2 (mdf['start'] >= mdf['start_range']) &
8 1438.3 MiB 9.2 MiB 1 (mdf['end'] <= mdf['end_range'])
9 ]
10
11 1527.9 MiB 0.3 MiB 3 res = fdf.groupby(['group', 'start_range', 'end_range']).agg({
12 1527.7 MiB 0.0 MiB 1 'val1': ['min', 'max', 'mean', 'sum'],
13 1527.7 MiB 0.0 MiB 1 'val2': ['min', 'max', 'mean', 'sum']
14 1527.9 MiB 0.0 MiB 1 }).reset_index()
15
16 1527.9 MiB 0.0 MiB 14 res.columns = ['_'.join(col).strip() if col[1] else col[0] for col in res.columns.values]
17 1527.9 MiB 0.0 MiB 1 return res
Максимальное использование памяти
вы могли бы получить больше производительности, вычислив агрегацию индивидуально перед объединением - но я думаю, OP справится с этим
Не уверен, что подписан на тебя @sammywemmy, ты имеешь в виду pd.concat в цикле в группах?
поэтому создайте сгруппированный объект, создайте каждую агрегацию, затем объедините список агрегированных значений через pd.concat. странный маршрут, но мог бы быть быстрее (исходя из личного опыта... YMMV)
@sammywemmy IIUC, тогда вам понадобится шаг unstack, чтобы иметь тот же формат. Кажется, это менее эффективно
Добавил фрагмент кода того, что я имел в виду - в этом случае разницы в производительности нет.
@sammywemmy Понятно (ты забыл grouped = out.groupby('group')), это действительно быстрее
удален. надеюсь, это имело смысл
@sammywemmy Вы можете оставить это (хорошо отформатировать), и я обновлю тайминги. Или добавьте свой ответ.
я оставлю это тебе. Я не думаю, что нужен другой ответ, поскольку ваш очень хорошо справляется со своей задачей. просто делюсь знаниями, а также учусь
@sammywemmy обновлено, спасибо за ваш отзыв :)
@mozway отличный ответ, спасибо, я должен был четко указать, что в некоторых случаях одна и та же группа может иметь несколько диапазонов, как указал Уроборос. Я отредактировал свой вопрос, чтобы отразить это. С другой стороны, мне также было интересно, могут ли поляры лучше подойти для чего-то подобного...
import numpy as np
import pandas as pd
df = pd.DataFrame({
'group': ['A', 'A', 'A', 'B', 'B', 'B'],
'start': [10, 20, 30, 15, 25, 35],
'end': [50, 60, 70, 55, 65, 75],
'val1': [5, 10, 15, 20, 25, 30],
'val2': [5, 6, 7, 8, 9, 10]
})
print(df)
'''
group start end val1 val2
0 A 10 50 5 5
1 A 20 60 10 6
2 A 30 70 15 7
3 B 15 55 20 8
4 B 25 65 25 9
5 B 35 75 30 10
'''
ranges = pd.DataFrame({
'group': ['A', 'B'],
'start': [5, 10],
'end': [40, 60]
})
# Convert ranges to a dictionary for quick lookup
range_dict = {group: (start, end) for group, start, end in ranges.values}
print(range_dict)#{'A': (5, 40), 'B': (10, 60)}
m1 = df['start'] >= df['group'].map(range_dict).str[0].values
m2 = df['end'] <= df['group'].map(range_dict).str[1].values
filtered_indices = np.where(m1 & m2)[0]
print(filtered_indices)#[3]
filtered_df = df.iloc[filtered_indices]
'''
group start end val1 val2
3 B 15 55 20 8
'''
# Aggregate using groupby and strings for functions
cols= ['val1', 'val2']
agg_functions = ['min','max','mean','sum']
res = filtered_df.groupby('group')[cols].agg({'val1':agg_functions,'val2':agg_functions}).reset_index()
print(res)
'''
group val1 val2
min max mean sum min max mean sum
0 B 20 20 20.0 20 8 8 8.0 8
'''
В чем преимущество
range_idпо сравнению с использованием столбцаgroupдля идентификации групп? Может ли одна и та же группа иметь несколько диапазонов?