Каков самый быстрый способ расчета ежедневного баланса со сложными процентами в Pandas или Spark?

У меня есть DataFrame (DF) с депозитами и снятием средств, агрегированными по дням, и я хочу знать, как самый быстрый способ рассчитать баланс на каждый день. Потому что он должен иметь возможность масштабироваться. Ответы как в Pandas, так и в Spark приветствуются! Вот пример того, как выглядит входной DF:

Вход

дата депозит снятие 01.01.2024 100.00 0,00 2024-01-02 0,00 0,00 03.01.2024 50.00 30.00 2024-01-04 0,00 0,00 05.01.2024 0,00 200.00 06.01.2024 20.00 0,00 07.01.2024 20.00 0,00 2024-01-08 0,00 0,00

Эти депозиты и снятие средств осуществляются с инвестиционного счета, который приносит 10% в день. Если баланс не отрицательный. В этом случае ежедневная доходность должна быть равна нулю. Вычисления псевдокода для получения столбцов daily_return и balance:

Движения = Баланс предыдущего дня + Депозит - Вывод
Процент = 0,1, если Движение > 0, иначе 0
Ежедневная доходность = Движения * Интерес
Баланс = Движения + Ежедневная доходность

Ниже приведен пример желаемого выходного DF:

Желаемый результат

дата депозит снятие daily_return баланс 01.01.2024 100.00 0,00 10.00 110.00 2024-01-02 0,00 0,00 11.00 121.00 03.01.2024 50.00 30.00 14.10 155,10 2024-01-04 0,00 0,00 15.51 170,61 05.01.2024 0,00 200.00 0,00 -29,39 06.01.2024 20.00 0,00 0,00 -9,39 07.01.2024 20.00 0,00 1.06 11.67 2024-01-08 0,00 0,00 1.17 12.84

Что у меня есть

У меня есть решение в 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
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
0
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Для этого типа вычислений я бы использовал цифру, например:

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. Это может помочь другим людям. Спасибо за помощь, Андрей Кесели!

Gabriel Tem Pass 03.07.2024 14:35

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