Pandas: Скользящее окно с размером шага

Созданный на 9 февр. 2017  ·  38Комментарии  ·  Источник: pandas-dev/pandas

Просто предложение - расширить rolling для поддержки скользящего окна с размером шага, например rollapply(by=X) в R.

Образец кода

Панды - неэффективное решение (применить функцию к каждому окну, затем срезать, чтобы получать каждый второй результат)

import pandas
ts = pandas.Series(range(0, 40, 2))
ts.rolling(5).apply(max).dropna()[::2]

Предположение:

ts = pandas.Series(range(0, 40, 2))
ts.rolling(window=5, step=2).apply(max).dropna()

На основе R (см. Документацию по rollapply ):

require(zoo)
TS <- zoo(seq(0, 40, 2))
rollapply(TS, 5, FUN=max, by=2)

8 12 16 20 24 28 32 36 40

Enhancement Needs Discussion Numeric Window

Самый полезный комментарий

«это можно сделать, но я хотел бы увидеть вариант использования, в котором это имеет значение».

Независимо от того, над каким проектом я работал с использованием pandas, я почти всегда упускал эту функцию, она полезна каждый раз, когда вам нужно вычислить приложение только время от времени, но все же необходимо хорошее разрешение внутри каждого окна.

Все 38 Комментарий

Если вы используете «стандартные» функции, они векторизованы, и поэтому v быстро ( ts.rolling(5).max().dropna()[::2] ).

IIUC, здесь экономия может быть достигнута только за счет применения функции лишь часть времени (например, каждое n-е значение). Но есть ли случай, когда это имеет практическое значение?

это можно сделать, но я хотел бы увидеть вариант использования, в котором это имеет значение. Это также нарушит API «возвращать тот же размер, что и входной». Хотя я не думаю, что это на самом деле сложно реализовать (хотя потребует ряда изменений в реализации). Мы используем маргинальные окна (IOW, вычисляем окно и по мере продвижения отбрасываем оставшиеся точки и добавляем очки, которые вы набираете). Так что все равно придется все вычислять, но вы просто не станете выводить это.

Спасибо за ответы!

IIUC, здесь экономия может быть достигнута только за счет применения функции лишь часть времени (например, каждое n-е значение). Но есть ли случай, когда это имеет практическое значение?

Мой вариант использования - это выполнение функций агрегирования (а не только max) по некоторым большим фреймам данных таймсерий - 400 столбцов, часы данных с частотой 5-25 Гц. Я также делал аналогичные вещи (разработка функций для данных датчиков) в прошлом с данными до 20 кГц. Запуск 30-секундных окон с 5-секундным шагом экономит большую часть обработки - например, при 25 Гц с шагом 5 секунд это 1/125 часть работы, что составляет разницу между ее запуском за 1 минуту или 2 часа.

Я, очевидно, могу вернуться к numpy, но было бы неплохо, если бы для этого был API более высокого уровня. Я просто подумал, что это предложение того стоило на тот случай, если другие тоже сочтут его полезным - я не ожидаю, что вы создадите функцию только для меня!

вы можете сначала попробовать resamplimg на более высокий частотный интервал, а затем прокатить

что-то вроде

df = df.resample ('30-е')
df.rolling (..). max () (или любая другая функция)

Привет, @jreback , спасибо за предложение.

Это сработало бы, если бы я просто запускал max для своих данных (для повторной выборки требуется функция сокращения, иначе по умолчанию используется mean , верно?):

df.resample('1s').max().rolling(30).max()

Однако я хотел бы запустить свою функцию сокращения для 30 секунд данных, затем переместиться на 1 секунду вперед и запустить ее на следующих 30 секундах данных и т. Д. Вышеупомянутый метод применяет функцию к 1 секунде данных, а затем к другой функция по 30 результатам первой функции.

Вот краткий пример - вычисление от пика до пика не работает дважды (очевидно):

# 10 minutes of data at 5Hz
n = 5 * 60 * 10
rng = pandas.date_range('1/1/2017', periods=n, freq='200ms')
np.random.seed(0)
d = np.cumsum(np.random.randn(n), axis=0)
s = pandas.Series(d, index=rng)

# Peak to peak
def p2p(d):
    return d.max() - d.min()

def p2p_arr(d):
    return d.max(axis=1) - d.min(axis=1)

def rolling_with_step(s, window, step, func):
    # See https://ga7g08.github.io/2015/01/30/Applying-python-functions-in-moving-windows/
    vert_idx_list = np.arange(0, s.size - window, step)
    hori_idx_list = np.arange(window)
    A, B = np.meshgrid(hori_idx_list, vert_idx_list)
    idx_array = A + B
    x_array = s.values[idx_array]
    idx = s.index[vert_idx_list + int(window/2.)]
    d = func(x_array)
    return pandas.Series(d, index=idx)

# Plot data
ax = s.plot(figsize=(12, 8), legend=True, label='Data')

# Plot resample then rolling (obviously does not work)
s.resample('1s').apply(p2p).rolling(window=30, center=True).apply(p2p).plot(ax=ax, label='1s p2p, roll 30 p2p', legend=True)

# Plot rolling window with step
rolling_with_step(s, window=30 * 5, step=5, func=p2p_arr).plot(ax=ax, label='Roll 30, step 1s', legend=True)

rolling window

@alexlouden из вашего исходного описания я думаю что-то вроде

df.resample('5s').max().rolling('30s').mean() (или любые другие сокращения) больше соответствует тому, что вы хотите

IOW, возьмите все, что находится в 5-секундном бункере, затем уменьшите его до одной точки, затем пролистайте эти бункеры. Общая идея состоит в том, что у вас есть много данных, которые можно суммировать за короткий промежуток времени, но на самом деле вы хотите, чтобы они были развернуты на более высоком уровне.

Привет @jreback , я действительно хочу запускать функцию для 30 секунд данных каждые 5 секунд. См. Функцию Rolling_with_step в моем предыдущем примере. Дополнительный шаг max / mean не работает для моего варианта использования.

@jreback , существует реальная потребность в пошаговой функции, которая еще не упоминалась в этом обсуждении. Я поддерживаю все, что описал

Предположим, что мы проводим анализ временных рядов с выборкой входных данных приблизительно от 3 до 10 миллисекунд. Нас интересуют особенности частотной области. Первым шагом в их построении будет определение частоты Найквиста. Предположим, мы знаем из предметной области, что это 10 Гц (каждые 100 мс). Это означает, что нам нужно, чтобы данные имели частоту не менее 20 Гц (каждые 50 мс), если функции должны хорошо улавливать входной сигнал. Мы не можем выполнить повторную выборку на более низкую частоту, чем эта. В конечном итоге вот вычисления, которые мы делаем:

df.resample('50ms').mean().rolling(window=32).aggregate(power_spectrum_coeff)

Здесь мы выбрали размер окна, кратный 8, а выбор 32 делает размер окна равным 1,6 секунды. Агрегатная функция возвращает односторонние коэффициенты частотной области без первого компонента среднего (функция fft является симметричной и со средним значением в 0-м элементе). Ниже приводится пример агрегатной функции:

def power_spectrum_coeff():
    def power_spectrum_coeff_(x):
        return np.fft.fft(x)[1 : int(len(x) / 2 + 1)]

    power_spectrum_coeff_.__name__ = 'power_spectrum_coeff'
    return power_spectrum_coeff_

Теперь мы хотели бы повторять это в скользящем окне, например, каждые 0,4 секунды или каждые 0,8 секунды. Нет смысла тратить зря вычисления и вместо этого вычислять БПФ каждые 50 мс, а затем разрезать их позже. Кроме того, передискретизация до 400 мс не является вариантом, потому что 400 мс - это всего лишь 2,5 Гц, что намного ниже частоты Найквиста, и это приведет к потере всей информации из функций.

Это были особенности частотной области, которые находят применение во многих научных экспериментах, связанных с временными рядами. Однако даже более простые агрегатные функции во временной области, такие как стандартное отклонение, не могут эффективно поддерживаться повторной выборкой.

Хотя я не думаю, что это на самом деле сложно реализовать (хотя потребует ряда изменений в реализации). Мы используем маргинальные окна (IOW, вычисляем окно и по мере продвижения отбрасываем оставшиеся точки и добавляем очки, которые вы набираете). Так что все равно придется все вычислять, но вы просто не станете выводить это.

Наличие параметра «шаг» и возможность сократить фактические вычисления за счет его использования должно стать будущей целью Pandas. Если параметр step возвращает только меньшее количество очков, этого не стоит делать, потому что мы все равно можем разрезать вывод. Возможно, учитывая объем работы, связанный с этим, мы могли бы просто порекомендовать всем проектам с такими потребностями использовать Numpy.

@Murmuria, вы можете отправить запрос на

Пока я второй запрос на параметр step в rolling() , я хотел бы отметить, что можно получить желаемый результат с помощью параметра base в resample() , если размер шага является целой частью размера окна . Используя пример @alexlouden :

pandas.concat([
    s.resample('30s', label='left', loffset=pandas.Timedelta(15, unit='s'), base=i).agg(p2p) 
    for i in range(30)
]).sort_index().plot(ax=ax, label='Solution with resample()', legend=True, style='k:')

Получаем тот же результат (обратите внимание, что линия удлиняется на 30 сек в обе стороны):
rolling_with_step_using_resample

Это все еще несколько расточительно, в зависимости от типа агрегирования. В частном случае вычисления размаха, как в примере @alexlouden , p2p_arr() почти в 200 раз быстрее, потому что он преобразует ряд в max() и min() .

Параметр step при прокрутке также позволит использовать эту функцию без индекса datetime. Кто-нибудь уже над этим работает?

@alexlouden выше сказал следующее:

Я, очевидно, могу вернуться к numpy, но было бы неплохо, если бы для этого был API более высокого уровня.

Может ли @alexlouden или кто-нибудь еще, кто знает, поделиться некоторыми сведениями о том, как это сделать с помощью numpy? Из моих исследований до сих пор кажется, что сделать это в numpy нетривиально. Фактически, здесь есть открытая проблема https://github.com/numpy/numpy/issues/7753

Спасибо

Привет, @tsando - функция rolling_with_step я использовал выше, не сработала для вас?

@alexlouden, спасибо, только что проверил эту функцию, и, похоже, она все еще зависит от панд (принимает серию в качестве входных данных, а также использует индекс серии). Мне было интересно, есть ли в этом чисто непонятный подход. В потоке, который я упомянул https://github.com/numpy/numpy/issues/7753, они предлагают функцию, которая использует несколько шагов, но их трудно понять и преобразовать в оконные и пошаговые входы.

@tsando Вот PDF-файл сообщения в блоге, на которое я ссылался выше - похоже, что автор изменил свое имя пользователя Github и больше не запускал свой сайт . (Я просто запустил его локально, чтобы преобразовать в PDF).

Моя функция выше заключалась в том, что я просто преобразовал его последний пример для работы с Pandas - если вы хотите использовать numpy напрямую, вы можете сделать что-то вроде этого: https://gist.github.com/alexlouden/e42f1d96982f7f005e62ebb737dcd987

Надеюсь это поможет!

@alexlouden спасибо! Я просто попробовал его на массиве формы (13, 1313) но он дал мне эту ошибку:

image

«это можно сделать, но я хотел бы увидеть вариант использования, в котором это имеет значение».

Независимо от того, над каким проектом я работал с использованием pandas, я почти всегда упускал эту функцию, она полезна каждый раз, когда вам нужно вычислить приложение только время от времени, но все же необходимо хорошее разрешение внутри каждого окна.

Я согласен и поддерживаю эту функцию

Это необходимо почти каждый раз при работе с временными рядами, эта функция может дать гораздо лучший контроль над созданием функций временных рядов как для визуализации, так и для анализа. Решительно поддерживаю эту идею!

согласен и поддерживаю эту функцию тоже

Это было бы очень полезно для сокращения времени вычислений при сохранении хорошего разрешения окна.

Я предоставляю коды решений, которые можно дополнительно адаптировать к вашей конкретной цели.

def average_smoothing(signal, kernel_size, stride):
    sample = []
    start = 0
    end = kernel_size
    while end <= len(signal):
        start = start + stride
        end = end + stride
        sample.append(np.mean(signal[start:end]))
    return np.array(sample)

Я согласен и поддерживаю эту функцию. Я вижу, что сейчас в режиме покадровой съёмки.

Вычисление с последующей понижающей дискретизацией невозможно, если у вас есть ТБ данных.

Это было бы очень полезно в том, чем я занимаюсь. У меня есть ТБ данных, где мне нужна различная статистика неперекрывающихся окон, чтобы понять местные условия. Мое текущее «исправление» - просто создать генератор, который нарезает кадры данных и выводит статистику. Было бы очень полезно иметь эту функцию.

Эта функция действительно необходима, когда речь идет о временных рядах!

Согласитесь, определенно нужна эта функция. Пытаюсь выполнить корреляцию окна бега между ценами на акции, и мне нужно создать для нее свою собственную функцию.

Не могу поверить, что такой базовой функции еще нет!
Когда будет решена эта проблема?
Спасибо

Чтобы внести свой вклад в «дальнейшее обсуждение»:
Мой вариант использования - вычислить одно минимальное / максимальное / среднее значение в час для данных за месяц с разрешением в 1 секунду. Это данные об использовании энергии, и есть пики в течение 1-2 секунд, которые я потеряю при передискретизации. Помимо этого, повторная выборка, например, до 5 секунд / 1 минута не изменит того факта, что мне все еще нужно вычислять 4k / 1k окон в день, которые нужно выбросить, вместо того, чтобы просто вычислять необходимые 24 окна в день. .

Можно было бы обойти это, используя groupby aso, но это не кажется ни интуитивно понятным, ни таким быстрым, как скользящая реализация (2 секунды для 2,5-миллионных часовых окон с сортировкой). Он впечатляюще быстр и полезен, но нам действительно нужен аргумент, чтобы полностью использовать его возможности.

Я взглянул на проблему. Это относительно тривиально, однако способ реализации кода, беглый взгляд, я думаю, потребует, чтобы кто-то вручную отредактировал все повторяющиеся процедуры. Ни один из них не соблюдает границы окна, заданные классами индексатора. Если бы они это сделали, то и этот запрос, и # 11704 были бы очень легко разрешимы. В любом случае, я думаю, это выполнимо для любого, кто хочет потратить некоторое время на то, чтобы привести вещи в порядок. Я инициировал наполовину готовый PR (ожидалось, что меня отвергнут, только для MVP), чтобы продемонстрировать, как я буду решать эту проблему.

Бег:

import numpy as np
import pandas as pd

data = pd.Series(
    np.arange(100),
    index=pd.date_range('2020/05/12 12:00:00', '2020/05/12 12:00:10', periods=100))

print('1s rolling window every 2s')
print(data.rolling('1s', step='2s').apply(np.mean))

data.sort_index(ascending=False, inplace=True)

print('1s rolling window every 500ms (and reversed)')
print(data.rolling('1s', step='500ms').apply(np.mean))

дает

1s rolling window every 2s
2020-05-12 12:00:00.000000000     4.5
2020-05-12 12:00:02.020202020    24.5
2020-05-12 12:00:04.040404040    44.5
2020-05-12 12:00:06.060606060    64.5
2020-05-12 12:00:08.080808080    84.5
dtype: float64
1s rolling window every 500ms (and reversed)
2020-05-12 12:00:10.000000000    94.5
2020-05-12 12:00:09.494949494    89.5
2020-05-12 12:00:08.989898989    84.5
2020-05-12 12:00:08.484848484    79.5
2020-05-12 12:00:07.979797979    74.5
2020-05-12 12:00:07.474747474    69.5
2020-05-12 12:00:06.969696969    64.5
2020-05-12 12:00:06.464646464    59.5
2020-05-12 12:00:05.959595959    54.5
2020-05-12 12:00:05.454545454    49.5
2020-05-12 12:00:04.949494949    44.5
2020-05-12 12:00:04.444444444    39.5
2020-05-12 12:00:03.939393939    34.5
2020-05-12 12:00:03.434343434    29.5
2020-05-12 12:00:02.929292929    24.5
2020-05-12 12:00:02.424242424    19.5
2020-05-12 12:00:01.919191919    14.5
2020-05-12 12:00:01.414141414     9.5
2020-05-12 12:00:00.909090909     4.5
dtype: float64

Подробности реализации смотрите в PR (или здесь: https://github.com/anthonytw/pandas/tree/rolling-window-step)

Хотя мне бы хотелось потратить больше времени на его завершение, у меня, к сожалению, не осталось ничего, чтобы взяться за тяжелую работу по переработке всех функций прокрутки. Моя рекомендация для всех, кто хочет решить эту проблему, - обеспечить соблюдение границ окна, сгенерированных классами индексатора, и унифицировать скользящие _ * _ фиксированные / переменные функции. С начальной и конечной границами я не вижу причин, по которым они должны отличаться, если только у вас нет функции, которая делает что-то особенное с неравномерно выбранными данными (в этом случае эта конкретная функция могла бы лучше справиться с нюансом, поэтому, возможно, установить флаг или что-то в этом роде).

Будет ли это работать и для настраиваемого окна с использованием подхода get_window_bounds() ?

Привет, я тоже второе предложение, пожалуйста. Это была бы действительно полезная функция.

Если вы используете «стандартные» функции, они векторизованы, и поэтому v быстро ( ts.rolling(5).max().dropna()[::2] ).

IIUC, здесь экономия может быть достигнута только за счет применения функции лишь часть времени (например, каждое n-е значение). Но есть ли случай, когда это имеет практическое значение?

У меня вот такой пример: https://stackoverflow.com/questions/63729190/pandas-resample-daily-data-to-annual-data-with-overlap-and-offset

Каждый N-й будет каждые 365-й. Размер окна меняется в течение всего времени существования программы, и не гарантируется, что шаг будет целой долей от размера окна.

Мне в основном нужен установленный размер окна, который изменяется на «количество дней в году, на которое он смотрит», что невозможно с каждым решением, которое я нашел для этой проблемы до сих пор.

У меня также есть аналогичная потребность в следующем контексте (адаптированном из реальной и профессиональной потребности):

  • У меня есть хронологический фрейм данных со столбцом временной метки и столбцом значений, которые представляют нерегулярные события. Например, отметка времени, когда собака прошла под моим окном, и сколько секунд ей потребовалось, чтобы пройти. У меня может быть 6 мероприятий в один день, а в следующие 2 дня не будет событий вообще
  • Я хотел бы вычислить метрику (скажем, среднее время, проведенное собаками перед моим окном) с скользящим окном в 365 дней, которое будет катиться каждые 30 дней.

Насколько я понимаю, API dataframe.rolling () позволяет мне указать продолжительность 365 дней, но не необходимость пропускать 30 дней значений (что является непостоянным количеством строк) для вычисления следующего среднего по сравнению с другим выбор 365 дней значений.

Очевидно, что ожидаемый фрейм данных будет иметь (намного) меньшее количество строк, чем исходный фрейм данных «собачьих событий».

Просто чтобы прояснить этот запрос на простом примере.

Если у нас есть эта серия:

In [1]: s = pd.Series(range(5))

In [2]: s
Out[2]:
0    0
1    1
2    2
3    3
4    4
dtype: int64

и у нас есть размер окна 2 и размер шага 1 . Это первое окно с индексом 0 будет оцениваться, переходить через окно с индексом 1 , оценивать окно по индексу 2 и т. Д.?

In [3]: s.rolling(2, step=1, min_periods=0).max()

Out[3]:
0    0.0
1    NaN # step over this observation
2    2.0
3    NaN # step over this observation
4    4.0
dtype: float64

Аналогично, если у нас есть эта временная серия

In [1]: s = pd.Series(range(5), index=pd.DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-06', '2020-01-09']))

In [2]: s
Out[2]:
2020-01-01    0
2020-01-02    1
2020-01-03    2
2020-01-06    3
2020-01-09    4
dtype: int64

и у нас есть размер окна '3D' и размер шага '3D' . Будет ли это правильный результат?

In [3]: s.rolling('3D', step='3D', min_periods=0).max()

Out[3]:
2020-01-01    0.0       # evaluate this window
2020-01-02    NaN    # step over this observation (2020-01-01 + 3 days > 2020-01-02)
2020-01-03    NaN    # step over this observation (2020-01-01 + 3 days > 2020-01-03)
2020-01-06    3.0      # evaluate this window ("snap back" to this observation)
2020-01-09    4.0      # evaluate this window (2020-01-06 + 3 days = 2020-01-09)
dtype: float64

@mroeschke относительно первого примера ([3]), результаты не такие, как я ожидал. Я предполагаю, что это конечное окно (например, при index = 0 это будет максимум элементов с -1 и 0, поэтому просто max ([0]), тогда он должен перейти на индекс «1», чтобы index = 0 + step = 1, и следующим вычислением будет max ([0,1]), затем max ([1,2]) и т. д. Похоже, вы хотели иметь размер шага два, поэтому вы должны перейти от index = 0 к index = 0 + 2 = 2 (пропуская индекс 1) и продолжить в том же духе. В этом случае это почти правильно, но не должно быть NaN. Хотя это может быть "только" в два раза больше размера в этом В других случаях это существенно. Например, у меня есть данные ЭКГ пациента с частотой 500 Гц за час, это 1,8 миллиона выборок. Если бы я хотел получать 5-минутное скользящее среднее каждые две минуты, это был бы массив 1,8 миллиона элементов с 30 действительными вычислениями и чуть меньше 1,8 миллиона NaN .:-)

Для индексации размер шага = 1 является текущим поведением, т. Е. Вычислить интересующий объект, используя данные в окне, сдвинуть окно на единицу, затем повторить. В этом примере я хочу вычислить интересующий объект, используя данные в окне, затем сдвинуть его на 60 000 индексов, а затем повторить.

Подобные замечания для того времени. В этом случае могут быть некоторые разногласия относительно правильного способа реализации этого типа окна, но, на мой взгляд, «лучший» (TM) способ - начать с момента t0, найти все элементы в диапазоне (t0-window , t0], вычислите объект, затем переместитесь на размер шага. Выбросьте все окна, в которых количество элементов меньше минимального (можно настроить, по умолчанию - 1). Этот пример относится к конечному окну, но вы можете изменить подходит для любой конфигурации окна. Недостаток этого заключается в том, что тратится время на большие промежутки, но промежутки можно обрабатывать разумно, и даже если вы вычисляете наивный способ (потому что вы ленивы, как я), я еще не видел этого на практике , поскольку пропуски обычно недостаточно велики, чтобы иметь значение в реальных данных.

Может что понятнее? Взгляните на мой пример + код выше, это может лучше объяснить.

Спасибо за разъяснение @anthonytw. Действительно, похоже, мне нужно было интерпретировать step как «шаг до точки».

Что касается NaN, я понимаю настроения автоматически отбрасывать NaN в выходном результате, но, как упоминалось в https://github.com/pandas-dev/pandas/issues/15354#issuecomment -278676420 от @jreback , есть соображение согласованности API, чтобы выходные данные имели ту же длину, что и входные. Может быть пользователь, который хотел бы также сохранить NaN (возможно?), И dropna все еще будет доступен после операции rolling(..., step=...).func() .

@mroeschke Я считаю, что следует делать исключения. Если в документации указано явное примечание и поведение не задано по умолчанию, никто не пострадает, если не вернет вектор, полный мусора. Сохранение NaN сводит на нет половину цели. Одна из целей - ограничить количество раз, когда мы выполняем дорогостоящие вычисления. Другая цель - свести к минимуму набор функций до чего-то управляемого. Тот пример, который я вам привел, реален, и он не так много данных, как на самом деле нужно обработать в приложении для наблюдения за пациентами. Действительно ли необходимо выделить 60000x необходимого пространства, а затем выполнить поиск по массиву, чтобы удалить NaN? Для каждой функции, которую мы хотим вычислить?

Обратите внимание, что одно вычисление может дать массив значений. Что мне делать с кривой ЭКГ? Ну, конечно, вычислим спектр мощности! Поэтому мне нужно выделить достаточно места для 1 полного PSD-вектора (150 000 элементов) 1,8 миллиона раз (2 ТБ данных), а затем выполнить фильтрацию, чтобы получить части, которые мне интересны (34 МБ). Для всех серий. Для всех пациентов. Думаю, мне нужно купить больше оперативной памяти!

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

Хотя я понимаю желание поддерживать API, это не та функция, которая нарушит какой-либо существующий код (потому что это новая функция, которой раньше не было), и, учитывая эту функциональность, нет причин, по которым кто-то мог бы ожидать, что она даст выход такого же размера. И даже если бы они это сделали, отметки в документации о размере шага было бы достаточно. Недостатки намного перевешивают любые преимущества наличия «последовательного» API (заметьте, для функции, которой раньше не было). Если не поступить таким образом, эта функция будет нарушена, в этом случае ее почти не стоит реализовывать (по моему опыту, стоимость места почти всегда является большим фактором).

Была ли эта страница полезной?
0 / 5 - 0 рейтинги