Почему в DataFrame намного эффективнее, чем на базовом массиве numpy?

Я заметил, что использование len в DataFrame намного быстрее, чем использование len в базовом массиве numpy. Я не понимаю, почему. Доступ к одной и той же информации через shape тоже не поможет. Это более актуально, поскольку я пытаюсь получить число столбцов и количество строк. Я всегда обсуждал, какой метод использовать.

Я собрал следующий эксперимент, и очень ясно, что я буду использовать len в dataframe. Но может кто-нибудь объяснить, почему?

from timeit import timeit
import pandas as pd
import numpy as np

ns = np.power(10, np.arange(6))
results = pd.DataFrame(
    columns=ns,
    index=pd.MultiIndex.from_product(
        [['len', 'len(values)', 'shape'],
         ns]))
dfs = {(n, m): pd.DataFrame(np.zeros((n, m))) for n in ns for m in ns}

for n, m in dfs.keys():
    df = dfs[(n, m)]
    results.loc[('len', n), m] = timeit('len(df)', 'from __main__ import df', number=10000)
    results.loc[('len(values)', n), m] = timeit('len(df.values)', 'from __main__ import df', number=10000)
    results.loc[('shape', n), m] = timeit('df.values.shape', 'from __main__ import df', number=10000)


fig, axes = plt.subplots(2, 3, figsize=(9, 6), sharex=True, sharey=True)
for i, (m, col) in enumerate(results.iteritems()):
    r, c = i // 3, i % 3
    col.unstack(0).plot.bar(ax=axes[r, c], title=m)

введите описание изображения здесь

Ответ 1

От взгляда на различные методы основная причина заключается в том, что построение массива numpy df.values занимает львиную долю времени.


len(df) и df.shape

Эти два являются быстрыми, потому что они по существу

len(df.index._data)

и

(len(df.index._data), len(df.columns._data))

где _data - numpy.ndarray. Таким образом, использование df.shape должно быть в два раза быстрее, чем len(df), поскольку оно находит длину как df.index, так и df.columns (оба типа pd.Index)


len(df.values) и df.values.shape

Скажем, вы уже извлекли vals = df.values. Тогда

In [1]: df = pd.DataFrame(np.random.rand(1000, 10), columns=range(10))

In [2]: vals = df.values

In [3]: %timeit len(vals)
10000000 loops, best of 3: 35.4 ns per loop

In [4]: %timeit vals.shape
10000000 loops, best of 3: 51.7 ns per loop

По сравнению с:

In [5]: %timeit len(df.values)
100000 loops, best of 3: 3.55 µs per loop

Таким образом, узкое место не len, а построение df.values. Если вы исследуете pandas.DataFrame.values(), вы найдете (примерно эквивалентные) методы:

def values(self):
    return self.as_matrix()

def as_matrix(self, columns=None):
    self._consolidate_inplace()
    if self._AXIS_REVERSED:
        return self._data.as_matrix(columns).T

    if len(self._data.blocks) == 0:
        return np.empty(self._data.shape, dtype=float)

    if columns is not None:
        mgr = self._data.reindex_axis(columns, axis=0)
    else:
        mgr = self._data

    if self._data._is_single_block or not self._data.is_mixed_type:
        return mgr.blocks[0].get_values()
    else:
        dtype = _interleaved_dtype(self.blocks)
        result = np.empty(self.shape, dtype=dtype)
        if result.shape[0] == 0:
            return result

        itemmask = np.zeros(self.shape[0])
        for blk in self.blocks:
            rl = blk.mgr_locs
            result[rl.indexer] = blk.get_values(dtype)
            itemmask[rl.indexer] = 1

        # vvv here is your final array assuming you actually have data
        return result 

def _consolidate_inplace(self):
    def f():
        if self._data.is_consolidated():
            return self._data

        bm = self._data.__class__(self._data.blocks, self._data.axes)
        bm._is_consolidated = False
        bm._consolidate_inplace()
        return bm
    self._protect_consolidate(f)

def _protect_consolidate(self, f):
    blocks_before = len(self._data.blocks)
    result = f()
    if len(self._data.blocks) != blocks_before:
        if i is not None:
            self._item_cache.pop(i, None)
        else:
            self._item_cache.clear()
    return result

Обратите внимание, что df._data является pandas.core.internals.BlockManager, а не a numpy.ndarray.

Ответ 2

Если вы посмотрите __len__ для pd.DataFrame, они фактически просто вызовут len(df.index): https://github.com/pandas-dev/pandas/blob/master/pandas/core/frame.py#L770

Для a RangeIndex это очень быстрая операция, поскольку это просто вычитание и деление значений, хранящихся в объекте индекса:

return max(0, -(-(self._stop - self._start) // self._step))

https://github.com/pandas-dev/pandas/blob/master/pandas/indexes/range.py#L458

Я подозреваю, что если вы протестировали с помощью не RangeIndex, разница во времени была бы гораздо более схожей. Я, вероятно, попробую изменить то, что вы видите, если это произойдет.

EDIT: после быстрой проверки разница во времени по-прежнему сохраняется даже со стандартным Index, поэтому там еще должна быть какая-то другая оптимизация.