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

У меня есть два кадра данных Pandas. Назовем их df1 и df2. Вот как выглядит df1 с фиктивными данными:

year,weight
2023,0.392
2022,0.382
2021,0.858
2020,0.135

И это df2

start_year,end_year
2022,2023
2021,2023
2020,2023
2020,2020
2021,2022
2022,2022

Ключевым фактом является то, что они не имеют одинаковой формы. Я хочу создать новый столбец в df2, который представляет собой среднее значение весов в df1, имеющих year между start_year и end_year. Так, например, для первой строки это даст мне (0,382 + 0,392)/2 = 0,387. И для всей таблицы:

start_year,end_year,weight
2022,2023,0.387
2021,2023,0.544
2020,2023,0.44175
2020,2020,0.135
2021,2022,0.625
2022,2022,0.382

Вот упрощенная версия моей попытки решения, позволяющей сопоставить две мои фиктивные таблицы и фиктивные данные:

df2["weight"] = df1[df1["year"].between(df2["start_year"], df2["end_year"], inclusive = "both")["weight"].mean()

Это приводит к следующей ошибке.

ValueError: Can only compare identically-labeled Series objects

Я предполагаю, что это потому, что Pandas предполагает, что я пытаюсь сравнить их по строкам (т. е. я пытаюсь проверить, находится ли строка 1 из df1 между начальным и конечным годами строки 1 из df2, строки 2 со строкой 2 и и т. д.), а не делать своего рода декартово произведение.

Я понимаю, что это можно сделать довольно просто с помощью цикла for, но это превратит задачу, занимающую доли секунды, в задачу, которая, вероятно, займет несколько секунд (поскольку df1 имеет около 110 строк, а df2 может иметь до Десятки тысяч строк в самых экстремальных случаях). Я хочу, чтобы это была векторизованная операция.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
89
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Пытаться:

tmp = df1.set_index("year").sort_index()

df2["weight"] = df2.apply(
    lambda x: tmp.loc[x["start_year"] : x["end_year"], "weight"].mean(), axis=1
)
print(df2)

Распечатки:

   start_year  end_year   weight
0        2022      2023  0.38700
1        2021      2023  0.54400
2        2020      2023  0.44175
3        2020      2020  0.13500
4        2021      2022  0.62000
5        2022      2022  0.38200

Спасибо! Это работает очень хорошо. Это намного быстрее, чем решения, которые мне удалось создать с момента публикации этого поста. Чтобы уточнить (прежде чем я приму ваш ответ), означает ли это ваше решение, что вы не думаете, что это возможно сделать векторизованным способом? Или каким-то более быстрым способом, чем pd.DataFrame.apply?

jso_ 01.09.2024 13:26

Вы можете использовать методы pd.merge и groupby.

Сначала разверните df2, чтобы включить все годы между start_year и end_year.

df2_expanded = df2.apply(lambda x: pd.Series(range(x['start_year'], x['end_year'] + 1)), axis=1).stack().reset_index(level=1, drop=True).to_frame('year')
df2_expanded = df2[['start_year', 'end_year']].join(df2_expanded)

Слияние с df1 по году

merged = pd.merge(df2_expanded, df1, on='year', how='left')

Сгруппируйте по исходным значениям start_year и end_year и рассчитайте средний вес.

result = merged.groupby(['start_year', 'end_year'])['weight'].mean().reset_index()

Затем объедините результат обратно в df2.

df2['weight'] = pd.merge(df2, result, on=['start_year', 'end_year'], how='left')['weight']
print(df2)

Это соединение диапазона — соединение неравенства, при котором один столбец из левого фрейма данных находится между столбцами из правого фрейма данных. Один из способов решить эту проблему — использовать Conditional_join от pyjanitor — я сотрудничаю с библиотекой. Под капотом он использует двоичный поиск для повышения производительности, особенно если условия подходящие. Бинарный поиск срабатывает, если оба столбца справа монотонно увеличиваются — таким образом он может пропускать несовпадающие фрагменты без прохождения линейного поиска:

# pip install pyjanitor
import pandas as pd
import janitor 

(df2
.conditional_join(
    df1, 
    ('start_year', 'year', '<='), 
    ('end_year', 'year', '>='),
    how='inner')
.groupby(['start_year', 'end_year'],sort=False)
.weight
.mean()
)

start_year  end_year
2022        2023        0.38700
2021        2023        0.54400
2020        2023        0.44175
            2020        0.13500
2021        2022        0.62000
2022        2022        0.38200
Name: weight, dtype: float64

Вы можете вернуться к исходному фрейму данных, если хотите сохранить все строки. Также обратите внимание, что df1 находится справа — таким образом мы получаем монотонное увеличение обоих правых столбцов и, следовательно, используем преимущества двоичного поиска. На данный момент это расточительно, поскольку мы материализуем фрейм данных перед группировкой. В будущем я надеюсь, что смогу выполнять агрегацию без необходимости материализации всего кадра данных (это должно обеспечить лучшую производительность), однако, скорее всего, это будет реализовано в numba.

Попробуйте и поделитесь своими результатами, а также производительностью.

Ответ принят как подходящий

Векторизованный подход:

# get full range of years in df1
year_range = np.array(range(df1["year"].min(), df1["year"].max() + 1))
# create boolean matrix to check years in year_range that fall between start_year and end_year in df2
m = (df2["start_year"].values <= year_range[:, None]) & (
    year_range[:, None] <= df2["end_year"].values
)
# align weights from df1 with year_range
w = df1.set_index("year").reindex(year_range)["weight"].values[:, None]
# get weighted average for each row
df2["weight"] = (w * m).sum(axis=0) / m.sum(axis=0)
   start_year  end_year   weight
0        2022      2023  0.38700
1        2021      2023  0.54400
2        2020      2023  0.44175
3        2020      2020  0.13500
4        2021      2022  0.62000
5        2022      2022  0.38200

Я думаю, что это эффективный ответ.

Panda Kim 01.09.2024 16:32

булева матрица — это декартово соединение, верно?

sammywemmy 01.09.2024 16:45

@sammywemmy Может быть, в каком-то смысле, поскольку он сочетается каждый год в year_range с каждым диапазоном в df2.

e-motta 01.09.2024 20:25

Другие вопросы по теме