У меня есть DataFrame (DF) с депозитами и снятием средств, агрегированными по дням, и я хочу знать, как самый быстрый способ рассчитать баланс на каждый день. Потому что он должен иметь возможность масштабироваться. Ответы как в Pandas, так и в Spark приветствуются! Вот пример того, как выглядит входной DF:
Вход
Эти депозиты и снятие средств осуществляются с инвестиционного счета, который приносит 10% в день. Если баланс не отрицательный. В этом случае ежедневная доходность должна быть равна нулю. Вычисления псевдокода для получения столбцов daily_return
и balance
:
Движения = Баланс предыдущего дня + Депозит - Вывод
Процент = 0,1, если Движение > 0, иначе 0
Ежедневная доходность = Движения * Интерес
Баланс = Движения + Ежедневная доходность
Ниже приведен пример желаемого выходного DF:
Желаемый результат
Что у меня есть
У меня есть решение в Pandas, которое достигает желаемого результата, однако оно перебирает каждую строку DF, т. е. оно медленное. Есть ли способ векторизовать этот расчет, чтобы ускорить его? Или, может быть, другой подход? Вот моя реализация:
import pandas as pd
df = pd.DataFrame({
"date": pd.date_range(start = "2024-01-01", end = "2024-01-08"),
"deposit": [100.0, 0.0, 50.0, 0.0, 0.0, 20.0, 20.0, 0.0],
"withdrawal": [0.0, 0.0, 30.0, 0.0, 200.0, 0.0, 0.0, 0.0]
})
daily_returns = []
balances = []
prev_balance = 0
for _, row in df.iterrows():
movements = prev_balance + row["deposit"] - row["withdrawal"]
interest = 0.1 if movements > 0 else 0
daily_return = movements * interest
balance = movements + daily_return
daily_returns.append(daily_return)
balances.append(balance)
prev_balance = balance
df["daily_return"] = daily_returns
df["balance"] = balances
Для этого типа вычислений я бы использовал цифру, например:
from numba import njit
@njit
def calculate(deposits, withdrawals, out_daily_return, out_balance):
prev_balance = 0
for i, (deposit, withdrawal) in enumerate(zip(deposits, withdrawals)):
movements = prev_balance + deposit - withdrawal
interest = 0.1 if movements > 0 else 0
daily_return = movements * interest
balance = movements + daily_return
out_daily_return[i] = daily_return
out_balance[i] = balance
prev_balance = balance
df["daily_return"] = 0.0
df["balance"] = 0.0
calculate(
df["deposit"].values,
df["withdrawal"].values,
df["daily_return"].values,
df["balance"].values,
)
print(df)
Распечатки:
date deposit withdrawal daily_return balance
0 2024-01-01 100.0 0.0 10.0000 110.0000
1 2024-01-02 0.0 0.0 11.0000 121.0000
2 2024-01-03 50.0 30.0 14.1000 155.1000
3 2024-01-04 0.0 0.0 15.5100 170.6100
4 2024-01-05 0.0 200.0 -0.0000 -29.3900
5 2024-01-06 20.0 0.0 -0.0000 -9.3900
6 2024-01-07 20.0 0.0 1.0610 11.6710
7 2024-01-08 0.0 0.0 1.1671 12.8381
Быстрый тест:
from time import monotonic
df = pd.DataFrame(
{
"date": pd.date_range(start = "2024-01-01", end = "2024-01-08"),
"deposit": [100.0, 0.0, 50.0, 0.0, 0.0, 20.0, 20.0, 0.0],
"withdrawal": [0.0, 0.0, 30.0, 0.0, 200.0, 0.0, 0.0, 0.0],
}
)
df = pd.concat([df] * 1_000_000)
print(f"{len(df)=}")
start_time = monotonic()
df["daily_return"] = 0.0
df["balance"] = 0.0
calculate(
df["deposit"].values,
df["withdrawal"].values,
df["daily_return"].values,
df["balance"].values,
)
print("Time = ", monotonic() - start_time)
Печатает на моем AMD 5700x:
len(df)=8000000
Time = 0.11215395800536498
Я не знал Нумбы, но думаю, что это полностью решит мою проблему. Кроме того, после некоторых исследований я нашел эту статью Франсуа Пакуля, в которой сравнивается производительность различных функций строк в Pandas. Это может помочь другим людям. Спасибо за помощь, Андрей Кесели!