Мои DataFrames:
import pandas as pd
df_1 = pd.DataFrame(
{
'a': [10, 12, 14, 20, 25, 30, 42, 50, 80]
}
)
df_2 = pd.DataFrame(
{
'start': [9, 19],
'end': [26, 50],
'label': ['a', 'b']
}
)
Ожидаемый результат: добавление столбца label к df_1:
a label
10 a
12 a
14 a
20 a
25 a
20 b
25 b
30 b
42 b
50 b
df_2 определяет диапазоны меток. Например, первая строка df_2 начала диапазона — 9, а конец — 22. Теперь я хочу разрезать df_1 на основе начала и конца и присвоить эту метку срезу. Обратите внимание, что start является исключающим, а end включает. И диапазоны моих ярлыков перекрываются.
Это мои попытки. Первый работает, но я не уверен, что он лучший.
# attempt_1
dfc = pd.DataFrame([])
for idx, row in df_2.iterrows():
start = row['start']
end = row['end']
label = row['label']
df_slice = df_1.loc[df_1.a.between(start, end, inclusive='right')]
df_slice['label'] = label
dfc = pd.concat([df_slice, dfc], ignore_index=True)
## attempt 2
idx = pd.IntervalIndex.from_arrays(df_2['start'], df_2['end'], closed='both')
label = df_2.iloc[idx.get_indexer(df_1.a), 'label']
df_1['label'] = label.to_numpy()






быстрый вариант — Conditional_join из pyjanitor:
# pip install pyjanitor
import pandas as pd
import janitor
(df_1
.conditional_join(
df_2,
('a','start','>='),
('a','end','<='),
df_columns = 'a',
right_columns='label')
)
a label
0 10 a
1 12 a
2 14 a
3 20 a
4 20 b
5 25 a
6 25 b
7 30 b
8 42 b
9 50 b
Не могли бы вы объяснить, что вы подразумеваете под словом «быстро» в этом контексте?
Обычно перед фильтрацией соединения диапазонов обрабатываются как декартовы соединения. Это может быть дорого, поскольку размер данных увеличивается. Conditional_join использует двоичный поиск, отсюда и слово быстрое (относительно декартова соединения).
Вы можете использовать понимание списка с помощью range и explode, чтобы создать фрейм данных для присоединения к df_1 для каждого элемента в начальном и конечном диапазонах df_2.
df_2_join = df_2.assign(
a=[range(s, e + 1) for s, e in zip(df_2["start"], df_2["end"])]
).explode("a")
df_1.merge(df_2_join).sort_values('start')
Выход:
a start end label
0 10 9 26 a
1 12 9 26 a
2 14 9 26 a
3 20 9 26 a
5 25 9 26 a
4 20 19 50 b
6 25 19 50 b
7 30 19 50 b
8 42 19 50 b
9 50 19 50 b
Это форма декартова соединения, которая подходит для небольших размеров, но становится дороже по мере увеличения размера данных или увеличения диапазонов.
Я бы попытался объединить сгенерированные помеченные диапазоны, используя pandas.concat следующим образом:
template = df_1.set_index('a')
ranges = df_2.values
output = pd.concat(
template.loc[start:end].assign(label=label)
for start, end, label in ranges
).reset_index()
Это близко к вашему решению с двумя основными отличиями:
df_1['a'] в качестве индекса, что подразумевается по его природе.import pandas as pd
import numpy as np
df_1 = pd.DataFrame(
{
'a': [10, 12, 14, 20, 25, 30, 42, 50, 80]
}
)
df_2 = pd.DataFrame(
{
'start': [9, 19],
'end': [26, 50],
'label': ['a', 'b']
}
)
# Convert columns to numpy arrays
a_values = df_1['a'].to_numpy()
start_values = df_2['start'].to_numpy()
end_values = df_2['end'].to_numpy()
labels = df_2['label'].to_numpy()
# Create a boolean matrix where each element (i, j) indicates whether
# a_values[i] falls within the j-th range
m1 = (a_values[:, None] > start_values)
m2 = (a_values[:, None] <= end_values)
mask = (m1 & m2)
# Find the indices where mask is True
rows, cols = np.where(mask)
# Create the new DataFrame with all valid (value, label) pairs
expanded_df = pd.DataFrame({
'a': a_values[rows],
'label': labels[cols]
})
print(expanded_df)
'''
a label
0 10 a
1 12 a
2 14 a
3 20 a
4 20 b
5 25 a
6 25 b
7 30 b
8 42 b
9 50 b
'''
Ваш подход — декартово/перекрестное соединение, которое подходит для небольших размеров, но дорого по мере роста размера данных. Использование iterrows тоже не улучшает ситуацию. Лучшим и более быстрым подходом является использование двоичного поиска. Ваши начальный и конечный столбцы монотонно отсортированы - двоичный поиск идеально подходит для этого и будет хорош как с точки зрения памяти, так и с точки зрения производительности.