Доступ к строкам и столбцам в одном и том же фрейме данных dtype

#python #pandas #dataframe

#python #pandas #фрейм данных

Вопрос:

Я немного удивлен, что для уникального фрейма данных dtype (nxn DataFrame) доступ к строке происходит медленнее, чем к столбцу. Из того, что я собрал, фрейм данных с идентичным dtype должен храниться в виде непрерывного блока в памяти, поэтому доступ к строкам или столбцам должен быть одинаково быстрым (просто вопрос обновления правильного шага).

Пример кода:

 df = pd.DataFrame(np.random.randn(100, 100))

%timeit df[0]
%timeit df.loc[0]
  

Самый медленный запуск занял в 12,86 раза больше времени, чем самый быстрый. Это может означать, что промежуточный результат кэшируется.

 100000 loops, best of 3: 2.72 µs per loop
10000 loops, best of 3: 116 µs per loop    
  

Я определенно чего-то не понимаю в том, как хранится фрейм данных, спасибо за вашу помощь!

Комментарии:

1. Я думаю, что более справедливое сравнение может быть df.loc[:, 0] против df.loc[0, :] , хотя первое все еще быстрее.

2. Не могли бы вы, пожалуйста, более четко описать свой вопрос?

3. Конечно: почему доступ к столбцу быстрее, чем к строке в фрейме данных того же типа (со строками и столбцами одинаковой длины)?

Ответ №1:

Я не эксперт в деталях реализации Pandas, но я использовал его достаточно, чтобы сделать обоснованное предположение.

Насколько я понимаю, структура данных Pandas наиболее непосредственно сопоставима со словарем словарей, где первым индексом являются столбцы. Таким образом, DF:

    a b
 c 1 2
 d 3 4
  

по сути {'a': {'c': 1, 'd': 3}, 'b': {'c': 2, 'd': 4}} . Я предполагаю, что с этого момента я прав в этом утверждении, и хотел бы, чтобы меня исправили, если кто-то знает больше о pandas.

Таким образом, индексирование столбца — это простой поиск по хэшу, тогда как индексирование строки требует перебора всех столбцов и выполнения хэш-поиска для каждого из них.

Я думаю, причина в том, что это делает действительно эффективным доступ к определенному атрибуту всех строк и добавление новых столбцов, что обычно и является способом взаимодействия с фреймом данных. Для таких табличных вариантов использования это намного быстрее, чем простая компоновка матрицы, поскольку вам не нужно перемещаться по памяти (целый столбец хранится более или менее локально), но, конечно, это компромисс, который делает взаимодействие со строками менее эффективным (отсюда, почему это не так просто синтаксически сделать; вы заметите, что большинство операций Pandas по умолчанию взаимодействуют со столбцами, а взаимодействие со строками является более или менее второстепенной задачей в модуле).

Комментарии:

1. Спасибо. Я согласен с вами, если мы можем предположить вложенную структуру dict. Но я подумал, что, поскольку он хранится блоками dtype, можно было пропустить эту структуру, когда все dtypes одинаковы.

Ответ №2:

Если вы посмотрите на базовый numpy массив, вы увидите, что скорость доступа к строкам / столбцам одинакова, по крайней мере, в моем тесте:

 %timeit df.values[0]
# 10.2 µs ± 596 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit df.values[:, 0]
# 10.2 µs ± 730 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  

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

Вот еще одно сравнение времени:

 %timeit df.iloc[0, :]
# 141 µs ± 7 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit df.iloc[:, 0]
# 61.9 µs ± 1.76 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
  

Доступ к столбцам также выполняется быстрее, хотя и намного медленнее. Я не уверен, чем бы это объяснить. Я предполагаю, что замедление по сравнению с прямым доступом к строке / столбцу связано с необходимостью возврата pd.Series . При доступе к строке может pd.Series потребоваться создать новую. Но я не знаю, почему iloc для столбцов тоже медленнее — возможно, он также создает новую серию каждый раз, поскольку iloc может использоваться довольно гибко и может не возвращать существующую серию (или может возвращать фрейм данных). Но если оба раза создается новая серия, то я снова не понимаю, почему одна операция превосходит другую.

И для большей полноты

 %timeit df.loc[0, :]
# 155 µs ± 6.48 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit df.loc[:, 0]
# 35.6 µs ± 1.28 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
  

Комментарии:

1. Спасибо за это, я думал (все еще думаю?), что это было бы возможно, поскольку все данные являются непрерывными для доступа к данным точно так же, как в массиве numpy, следовательно, с той же скоростью.

2. @MdM Напомним, однако, что вы не получаете numpy.ndarray обратно, вы получаете pandas.Series Так что для создания нового объекта серии потребуется по крайней мере немного дополнительного времени, включая определение позиции индекса, чтобы вы могли назначить его в качестве имени серии. Также имеет смысл, что метод, который написан для обработки очень общего индексирования ( .loc or .iloc ), работает медленнее, чем df[col_name] который специализируется только на одной простой операции