0Pricing
Pandas & NumPy Academy · Урок

Расширяющиеся окна

Вычисляйте накопительные статистики, учитывающие все строки от начала до каждой текущей точки, с помощью expanding().sum().

«Расширяющиеся окна» — бесплатный урок Pandas & NumPy Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Pandas & NumPy Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Pandas & NumPy Academy содержит 4 уроков всего.

Что такое расширяющееся окно

Расширяющееся окно включает все строки от самого начала Series до текущей строки — окно увеличивается с каждой новой строкой, а не сохраняет фиксированный размер. В строке 0 оно содержит только одно значение; в строке 5 — шесть значений; в строке 100 — 101 значение. Расширяющиеся окна используются для вычисления накопительных статистик, отражающих текущую сумму, среднее или максимум от начала набора данных до текущего момента.

import pandas as pd
import numpy as np

sales = pd.Series(
    [100, 150, 120, 200, 180, 160, 220, 190],
    index=pd.date_range('2024-01-01', periods=8)
)

# Expanding mean — grows with each row
df = pd.DataFrame({'sales': sales})
df['cumulative_mean'] = df['sales'].expanding().mean()
print(df)

Метод expanding()

Series.expanding(min_periods=1) возвращает объект Expanding. Параметр min_periods (по умолчанию 1) задаёт минимальное число наблюдений, необходимое для получения первого результата, отличного от NaN. В отличие от rolling(n), окно в expanding() не имеет фиксированного размера и всегда начинается с начала. Можно последовательно вызвать любую агрегатную функцию: .sum(), .mean(), .std(), .min(), .max().

import pandas as pd
import numpy as np

np.random.seed(42)
monthly_revenue = pd.Series(
    np.random.randint(500, 1500, 12),
    index=pd.date_range('2024-01', periods=12, freq='ME'),
    name='revenue'
)

df = pd.DataFrame({'revenue': monthly_revenue})
df['cumsum'] = df['revenue'].expanding().sum()
df['cummean'] = df['revenue'].expanding().mean()
df['cummax'] = df['revenue'].expanding().max()
df['cummin'] = df['revenue'].expanding().min()
print(df)

Pandas cumsum, cumprod, cummax, cummin

Для наиболее распространённых накопительных статистик Pandas предоставляет прямые методы для Series и DataFrame, поэтому использовать expanding() не требуется: cumsum(), cumprod(), cummax() и cummin(). Это оптимизированные реализации, немного более быстрые, чем соответствующие варианты с expanding(). Используйте эти сокращённые варианты для типичных бизнес-показателей, таких как выручка с начала года (cumsum) или максимальная цена за всё время (cummax).

import pandas as pd
import numpy as np

prices = pd.Series(
    [100, 95, 110, 105, 120, 115, 130, 125],
    index=pd.date_range('2024-01-01', periods=8)
)

df = pd.DataFrame({'price': prices})
df['cummax'] = df['price'].cummax()    # all-time high
df['cummin'] = df['price'].cummin()    # all-time low
df['cumsum'] = df['price'].cumsum()    # cumulative total
df['cumprod'] = (1 + df['price'].pct_change()).cumprod()  # growth index
print(df.round(3))

Расчёт показателя с начала года с помощью expanding

Классический пример использования расширяющихся окон — выручка с начала года (YTD). Для каждого дня нужно вычислить сумму всей выручки с 1 января текущего года до этого дня. Это всего лишь применение cumsum() внутри каждой годовой группы. Реализуйте это с помощью groupby(year).cumsum() — cumsum сбрасывается в начале каждого календарного года и накапливает значения в течение года.

import pandas as pd
import numpy as np

np.random.seed(0)
dates = pd.date_range('2023-01-01', '2024-06-30', freq='ME')
daily = pd.DataFrame({
    'date': dates,
    'revenue': np.random.randint(100, 500, len(dates))
})

# Year-to-date cumulative revenue
daily['year'] = daily['date'].dt.year
daily['ytd_revenue'] = daily.groupby('year')['revenue'].cumsum()
daily = daily.drop(columns='year')
print(daily.tail(10).to_string(index=False))

Стандартное отклонение в расширяющемся окне

expanding().std() вычисляет текущее стандартное отклонение, используя все данные от начала до текущей строки. Это полезно для контроля того, увеличивается или уменьшается со временем изменчивость показателя — например, чтобы отслеживать, становятся ли ежедневные продажи продукта более или менее предсказуемыми по мере развития бизнеса. В отличие от rolling std, которое отражает только недавнюю изменчивость, expanding std учитывает весь исторический разброс.

import pandas as pd
import numpy as np

np.random.seed(42)
sales = pd.Series(
    np.concatenate([
        np.random.normal(100, 5, 30),   # stable period
        np.random.normal(100, 25, 30)   # volatile period
    ]),
    index=pd.date_range('2024-01-01', periods=60)
)

df = pd.DataFrame({'sales': sales})
df['expanding_std'] = df['sales'].expanding().std()
df['rolling_30d_std'] = df['sales'].rolling(30).std()
print(df.tail(10).round(2))

Сравнение скользящих и расширяющихся окон

Ключевое различие заключается в следующем: скользящие окна отражают недавние результаты (последние n дней) и не зависят от данных давнего прошлого, тогда как расширяющиеся окна учитывают всю историю и поэтому медленно приближаются к стабильным значениям. Используйте rolling, когда Вас интересуют недавние тренды (7-дневное скользящее среднее быстрее реагирует на недавние изменения, чем 90-дневное). Используйте expanding, когда нужны статистики за всё время или показатели YTD, которые не должны забывать более ранние данные.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(10)
data = pd.Series(
    np.random.randn(60).cumsum() + 50,
    index=pd.date_range('2024-01-01', periods=60)
)

df = pd.DataFrame({'value': data})
df['rolling_10'] = data.rolling(10).mean()
df['expanding'] = data.expanding().mean()

df.plot(figsize=(12, 5), title='Rolling 10-day Mean vs. Expanding Mean')
plt.ylabel('Value')
plt.show()

Expanding apply для пользовательских накопительных статистик

expanding().apply(func) применяет пользовательскую функцию ко всем данным от начала до текущей строки, как и rolling().apply(). Это позволяет вычислять накопительные статистики, недоступные среди встроенных функций, — например, накопительный коэффициент вариации (std/mean), текущее медианное значение или накопительный коэффициент Шарпа. Функция получает массив NumPy со всеми значениями, встреченными на данный момент, и должна возвращать скаляр.

import pandas as pd
import numpy as np

np.random.seed(0)
returns = pd.Series(
    np.random.normal(0.001, 0.02, 60),
    index=pd.date_range('2024-01-01', periods=60)
)

# Cumulative Sharpe ratio (mean/std of returns)
def sharpe(arr):
    if len(arr) < 2:
        return float('nan')
    return arr.mean() / arr.std() * (252 ** 0.5)  # annualised

df = pd.DataFrame({'returns': returns})
df['cum_sharpe'] = df['returns'].expanding().apply(sharpe, raw=True)
print(df.tail(8).round(4))

Расширяющиеся окна с обработкой NaN

Расширяющиеся окна по умолчанию пропускают значения NaN — значение NaN в середине Series игнорируется при вычислении накопительной суммы или среднего. Это можно проверить с помощью skipna=True (значение по умолчанию для большинства агрегирующих функций Pandas). Если во временном ряду есть настоящие пропуски (например, из-за отсутствия торгов по выходным), расширяющиеся статистики вычисляются только по непропущенным значениям, встреченным на данный момент, что обычно и требуется.

import pandas as pd
import numpy as np

data = pd.Series([10, np.nan, 20, 30, np.nan, 40, 50])

# cumsum skips NaN by default
df = pd.DataFrame({'value': data})
df['cumsum'] = df['value'].cumsum()      # NaN propagates in cumsum!
df['expanding_sum'] = df['value'].expanding().sum()  # NaN skipped

print(df)
print('\nNote: cumsum propagates NaN; expanding().sum() skips it.')

Расширяющиеся окна для текущих ориентиров

Расширяющиеся окна идеально подходят для вычисления текущих ориентиров: максимального значения за всё время, лучшего квартала за всю историю или самого низкого уровня ошибок с момента запуска. Для этих KPI необходимо помнить все исторические значения — скользящее окно забыло бы старые записи. Использование cummax() или cummin() возвращает лучший (или худший) текущий результат в каждой точке, что упрощает отслеживание момента установления новых рекордов.

import pandas as pd
import numpy as np

np.random.seed(5)
sales = pd.Series(
    np.random.randint(100, 300, 20),
    index=pd.date_range('2024-01-01', periods=20),
    name='daily_sales'
)

df = pd.DataFrame({'sales': sales})
df['all_time_max'] = df['sales'].cummax()
df['new_record'] = df['sales'] == df['all_time_max']

print('Days where a new sales record was set:')
print(df[df['new_record']].drop(columns='new_record'))

Практический пример: накопительная доходность

В финансах накопительная доходность показывает, насколько выросла инвестиция от начальной даты до каждой последующей даты. Если начать с дневной процентной доходности, то произведение значений (1 + daily_return) даёт общий коэффициент роста. Здесь накопительное произведение естественнее скользящего: нужно отслеживать общий рост с момента начала инвестирования, а не только за последние n дней.

import pandas as pd
import numpy as np

np.random.seed(7)
start_price = 100
daily_returns = pd.Series(
    np.random.normal(0.0005, 0.015, 252),
    index=pd.date_range('2024-01-01', periods=252)
)

# Cumulative return = product of (1 + r_i)
cum_returns = (1 + daily_returns).cumprod()
portfolio_value = start_price * cum_returns

print(f'Start value: ${start_price:.2f}')
print(f'End value:   ${portfolio_value.iloc[-1]:.2f}')
print(f'Total return: {(portfolio_value.iloc[-1]/start_price - 1)*100:.1f}%')

Когда использовать expanding, rolling и cumsum

Рекомендации по выбору подхода:

  • Используйте cumsum() / cummax() / cummin() для простых накопительных вычислений — это самый быстрый вариант.
  • Используйте expanding().mean() / std(), когда нужны текущие накопительные средние или дисперсия.
  • Используйте expanding().apply(custom_func) для сложных статистик за всё время, недоступных среди встроенных функций.
  • Используйте rolling(n) вместо expanding, когда на текущее значение должна влиять только недавняя история.

Быстрая проверка

Проверьте, насколько хорошо Вы поняли тему расширяющихся окон из этого урока.

Итоги урока

В этом уроке Вы узнали, что expanding() вычисляет накопительные статистики по увеличивающемуся окну от начала Series, а cumsum/cummax/cummin — это оптимизированные сокращённые варианты для распространённых накопительных операций. Расширяющиеся окна идеально подходят для рекордов за всё время, показателей YTD и накопительной доходности инвестиций. Далее мы рассмотрим экспоненциально взвешенные скользящие средние (EWMA), в которых недавним наблюдениям придаётся больший вес.

Часто задаваемые вопросы

Урок «Расширяющиеся окна» бесплатный?

Да — полный текст урока «Расширяющиеся окна» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Pandas & NumPy Academy, подпишись на CoddyKit PRO. Курс Pandas & NumPy Academy содержит 4 уроков всего.

Чему я научусь в уроке «Расширяющиеся окна»?

Вычисляйте накопительные статистики, учитывающие все строки от начала до каждой текущей точки, с помощью expanding().sum(). Ты практикуешь Pandas & NumPy Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Pandas & NumPy Academy?

Предыдущий опыт не требуется. Pandas & NumPy Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Расширяющиеся окна»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Pandas & NumPy Academy?

Да. Каждый урок Pandas & NumPy Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Скользящие окна
  2. Расширяющиеся окна
  3. Экспоненциально взвешенное скользящее среднее
  4. Ранги и процентили внутри групп
← Назад к Pandas & NumPy Academy