Почему pandas прокат использует одиночный размер ndarray

Я был мотивирован использовать функцию pandas rolling для выполнения скользящей многофакторной регрессии (этот вопрос НЕ о скользящей многофакторной регрессии). Я ожидал, что после df.rolling(2) я смогу использовать apply и возьму полученный pd.DataFrame извлечение ndarray с помощью .values и выполнить требуемое умножение матрицы. Это не получилось.

Вот что я нашел:

import pandas as pd
import numpy as np

np.random.seed([3,1415])
df = pd.DataFrame(np.random.rand(5, 2).round(2), columns=['A', 'B'])
X = np.random.rand(2, 1).round(2)

Как выглядят объекты:

print "\ndf = \n", df
print "\nX = \n", X
print "\ndf.shape =", df.shape, ", X.shape =", X.shape

df = 
      A     B
0  0.44  0.41
1  0.46  0.47
2  0.46  0.02
3  0.85  0.82
4  0.78  0.76

X = 
[[ 0.93]
 [ 0.83]]

df.shape = (5, 2) , X.shape = (2L, 1L)

Матричное умножение ведет себя нормально:

df.values.dot(X)

array([[ 0.7495],
       [ 0.8179],
       [ 0.4444],
       [ 1.4711],
       [ 1.3562]])

Использование применить для выполнения строки по строке. Точечный продукт ведет себя как ожидалось:

df.apply(lambda x: x.values.dot(X)[0], axis=1)

0    0.7495
1    0.8179
2    0.4444
3    1.4711
4    1.3562
dtype: float64

Groupby → Apply ведет себя так, как я ожидал:

df.groupby(level=0).apply(lambda x: x.values.dot(X)[0, 0])

0    0.7495
1    0.8179
2    0.4444
3    1.4711
4    1.3562
dtype: float64

Но когда я запускаю:

df.rolling(1).apply(lambda x: x.values.dot(X))

Я получаю:

AttributeError: объект 'numpy.ndarray' не имеет атрибутов 'values'

Итак, pandas использует прямую ndarray в своей реализации rolling. Я могу справиться с этим. Вместо .values, чтобы получить ndarray, попробуйте:

df.rolling(1).apply(lambda x: x.dot(X))

фигуры (1,) и (2,1) не выровнены: 1 (dim 0)!= 2 (dim 0)

Подождите! Что?!

Итак, я создал пользовательскую функцию, чтобы посмотреть, что делает прокатка.

def print_type_sum(x):
    print type(x), x.shape
    return x.sum()

Затем выполнил:

print df.rolling(1).apply(print_type_sum)

<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
<type 'numpy.ndarray'> (1L,)
      A     B
0  0.44  0.41
1  0.46  0.47
2  0.46  0.02
3  0.85  0.82
4  0.78  0.76

В результате получается pd.DataFrame то же самое, что хорошо. Но он напечатал 10 одномерных объектов ndarray. Что насчет rolling(2)

print df.rolling(2).apply(print_type_sum)

<type 'numpy.ndarray'> (2L,)
<type 'numpy.ndarray'> (2L,)
<type 'numpy.ndarray'> (2L,)
<type 'numpy.ndarray'> (2L,)
<type 'numpy.ndarray'> (2L,)
<type 'numpy.ndarray'> (2L,)
<type 'numpy.ndarray'> (2L,)
<type 'numpy.ndarray'> (2L,)
      A     B
0   NaN   NaN
1  0.90  0.88
2  0.92  0.49
3  1.31  0.84
4  1.63  1.58

То же самое, ожидайте вывод, но он напечатал 8 ndarray объектов. rolling производит одиночный размер ndarray длины window для каждого столбца в отличие от ожидаемого, который был ndarray формы (window, len(df.columns)).

Вопрос: почему?

Теперь у меня нет возможности легко запускать многокомпонентную регрессию.

Ответ 1

Используя strides views concept on dataframe, здесь используется векторный подход -

get_sliding_window(df, 2).dot(X) # window size = 2

Тест времени выполнения -

In [101]: df = pd.DataFrame(np.random.rand(5, 2).round(2), columns=['A', 'B'])

In [102]: X = np.array([2, 3])

In [103]: rolled_df = roll(df, 2)

In [104]: %timeit rolled_df.apply(lambda df: pd.Series(df.values.dot(X)))
100 loops, best of 3: 5.51 ms per loop

In [105]: %timeit get_sliding_window(df, 2).dot(X)
10000 loops, best of 3: 43.7 µs per loop

Подтвердить результаты -

In [106]: rolled_df.apply(lambda df: pd.Series(df.values.dot(X)))
Out[106]: 
      0     1
1  2.70  4.09
2  4.09  2.52
3  2.52  1.78
4  1.78  3.50

In [107]: get_sliding_window(df, 2).dot(X)
Out[107]: 
array([[ 2.7 ,  4.09],
       [ 4.09,  2.52],
       [ 2.52,  1.78],
       [ 1.78,  3.5 ]])

Огромное улучшение там, которое я надеюсь, останется заметным на больших массивах!

Ответ 2

Я хотел поделиться тем, что я сделал, чтобы обойти эту проблему.

Учитывая pd.DataFrame и окно, я создаю сложный ndarray с помощью np.dstack ( см. ответ). Затем я конвертирую его в pd.Panel и используя pd.Panel.to_frame преобразует его в pd.DataFrame. На данный момент у меня есть pd.DataFrame, который имеет дополнительный уровень по своему индексу относительно исходного pd.DataFrame, а новый уровень содержит информацию о каждом прокатанном периоде. Например, если в окне календаря 3, новый уровень индекса будет содержать [0, 1, 2]. Элемент за каждый период. Теперь я могу groupby level=0 и вернуть объект groupby. Теперь это дает мне объект, который я могу более интуитивно манипулировать.

Функция рулона

import pandas as pd
import numpy as np

def roll(df, w):
    roll_array = np.dstack([df.values[i:i+w, :] for i in range(len(df.index) - w + 1)]).T
    panel = pd.Panel(roll_array, 
                     items=df.index[w-1:],
                     major_axis=df.columns,
                     minor_axis=pd.Index(range(w), name='roll'))
    return panel.to_frame().unstack().T.groupby(level=0)

Демонстрация

np.random.seed([3,1415])
df = pd.DataFrame(np.random.rand(5, 2).round(2), columns=['A', 'B'])

print df

      A     B
0  0.44  0.41
1  0.46  0.47
2  0.46  0.02
3  0.85  0.82
4  0.78  0.76

Пусть sum

rolled_df = roll(df, 2)

print rolled_df.sum()

major     A     B
1      0.90  0.88
2      0.92  0.49
3      1.31  0.84
4      1.63  1.58

Чтобы заглянуть под капот, мы можем увидеть структуру:

print rolled_df.apply(lambda x: x)

major      A     B
  roll            
1 0     0.44  0.41
  1     0.46  0.47
2 0     0.46  0.47
  1     0.46  0.02
3 0     0.46  0.02
  1     0.85  0.82
4 0     0.85  0.82
  1     0.78  0.76

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

X = np.array([2, 3])

print rolled_df.apply(lambda df: pd.Series(df.values.dot(X))) 

      0     1
1  2.11  2.33
2  2.33  0.98
3  0.98  4.16
4  4.16  3.84

Ответ 3

Сделал следующие изменения для вышеупомянутого ответа, так как мне нужно было вернуть все окно качения, как это сделано в pd.DataFrame.rolling()

def roll(df, w):
    roll_array = np.dstack([df.values[i:i+w, :] for i in range(len(df.index) - w + 1)]).T
    roll_array_full_window = np.vstack((np.empty((w-1 ,len(df.columns), w)), roll_array))
    panel = pd.Panel(roll_array_full_window, 
                 items=df.index,
                 major_axis=df.columns,
                 minor_axis=pd.Index(range(w), name='roll'))
    return panel.to_frame().unstack().T.groupby(level=0)

Ответ 4

Так как pandas v0.23 теперь можно передать Series вместо ndarray в Rolling.apply(). Просто установите raw=False.

raw: bool, default None

False: передает каждую строку или столбец в виде серии в функцию.

True или None: переданная функция вместо этого будет получать объекты ndarray. Если вы просто применяете функцию уменьшения NumPy, это приведет к гораздо лучшей производительности. Необработанный параметр является обязательным и покажет FutureWarning, если не будет передан. В будущем raw по умолчанию будет False.

Новое в версии 0.23.0.

Как указано; если вам нужно только одно измерение, передача его в необработанном виде, очевидно, более эффективна. Это, вероятно, ответ на ваш вопрос; Rolling.apply() изначально был создан для прохождения ndarray только потому, что это наиболее эффективно.