Как сохранить мажорный порядок при копировании или группировании панд DataFrame?

Как я могу использовать или манипулировать панками (monkey-patch) для того, чтобы всегда сохранять один и тот же мажорный порядок в результирующем объекте для скоплений и групповых агрегаций?

я использую pandas.DataFrame как структура данных в бизнес-приложении (модель риска) и требует быстрой агрегации многомерных данных. Агрегация с пандами в решающей степени зависит от схемы упорядочения мажоров, используемой в базовом массиве numpy.

К сожалению, pandas (версия 0.23.4) меняет мажорный порядок нижележащего массива numpy, когда я создаю копию или когда я выполняю агрегирование с помощью groupby и sum.

Влияние это:

случай 1: 17,2 секунды

случай 2: 5 минут 46 секунд

на DataFrame и его копии с 45023 строками и 100000 столбцами. Агрегация была выполнена по индексу. Индекс является pd.MultiIndex с 15 уровнями. Агрегация держит три уровня и приводит к 239 группам.

Я обычно работаю на DataFrames с 45000 строк и 100000 столбцов. На ряду у меня есть pandas.MultiIndex около 15 уровней. Для вычисления статистики по различным узлам иерархии мне нужно агрегировать (суммировать) по измерению индекса.

Агрегация выполняется быстро, если основной массив NumPy c_contiguous, следовательно, проводится в мажорном столбце (порядок C). Это очень медленно, если это f_contiguous, следовательно, в мажорном порядке строк (порядок F).

К сожалению, панды изменяют мажорный порядок с C на F, когда

  • создание копии DataFrame и даже когда,

  • выполняя агрегацию с помощью grouby и получая сумму на grouper. Следовательно, результирующий DataFrame имеет другой мажорный порядок (!)

Конечно, я мог бы придерживаться другой "модели данных", просто сохраняя MultiIndex на столбцах. Тогда текущая версия для панд всегда будет работать в мою пользу. Но это не пойдет. Я думаю, что можно ожидать, что для двух рассматриваемых операций (групповая сумма и копия) мажорный порядок не должен изменяться.

import numpy as np
import pandas as pd

print("pandas version: ", pd.__version__)

array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
array.flags
print("Numpy array is C-contiguous: ", data.flags.c_contiguous)

dataframe = pd.DataFrame(array, index = pd.MultiIndex.from_tuples([('A', 'U'), ('A', 'V'), ('B', 'W')], names=['dim_one', 'dim_two']))
print("DataFrame is C-contiguous: ", dataframe.values.flags.c_contiguous)

dataframe_copy = dataframe.copy()
print("Copy of DataFrame is C-contiguous: ", dataframe_copy.values.flags.c_contiguous)

aggregated_dataframe = dataframe.groupby('dim_one').sum()
print("Aggregated DataFrame is C-contiguous: ", aggregated_dataframe.values.flags.c_contiguous)


## Output in Jupyter Notebook
# pandas version:  0.23.4
# Numpy array is C-contiguous:  True
# DataFrame is C-contiguous:  True
# Copy of DataFrame is C-contiguous:  False
# Aggregated DataFrame is C-contiguous:  False

Основной порядок данных должен быть сохранен. Если pandas любит переключаться на неявные предпочтения, то это должно позволить перезаписать это. Numpy позволяет вводить заказ при создании копии.

Исправленная версия панд должна привести к

## Output in Jupyter Notebook
# pandas version:  0.23.4
# Numpy array is C-contiguous:  True
# DataFrame is C-contiguous:  True
# Copy of DataFrame is C-contiguous:  True
# Aggregated DataFrame is C-contiguous:  True

для примера кода, приведенного выше.

0 ответов

Патч обезьяны для панд (0.23.4 и, возможно, другие версии тоже)

Я создал патч, которым хочу с вами поделиться. Это приводит к увеличению производительности, упомянутому в вопросе выше.

Работает для панд версии 0.23.4. Для других версий нужно попробовать, все ли работает.

Следующие два модуля необходимы, вы можете адаптировать импорт в зависимости от того, где вы их разместили.

memory_layout.py   
memory.py

Чтобы исправить свой код, вам просто нужно импортировать следующее в самом начале вашей программы или записной книжки и установить параметр разметки памяти. Он будет патчить pandas и следить за тем, чтобы копии DataFrames работали с запрошенным макетом.

from memory_layout import memory_layout
# memory_layout.order = 'F'  # assert F-order on copy
# memory_layout.order = 'K'  # Keep given layout on copy 
memory_layout.order = 'C'  # assert C-order on copy

memory_layout.py

Создайте файл memory_layout.py со следующим содержимым.

import numpy as np
from pandas.core.internals import Block
from memory import memory_layout

# memory_layout.order = 'F'  # set memory layout order to 'F' for np.ndarrays in DataFrame copies (fortran/row order)
# memory_layout.order = 'K'  # keep memory layout order for np.ndarrays in DataFrame copies (order out is order in)
memory_layout.order = 'C'  # set memory layout order to 'C' for np.ndarrays in DataFrame copies (C/column order)


def copy(self, deep=True, mgr=None):
    """
    Copy patch on Blocks to set or keep the memory layout
    on copies.

    :param self: `pandas.core.internals.Block`
    :param deep: `bool`
    :param mgr: `BlockManager`
    :return: copy of `pandas.core.internals.Block`
    """
    values = self.values
    if deep:
        if isinstance(values, np.ndarray):
memory_layout))
            values = memory_layout.copy_transposed(values)
memory_layout))
        else:
            values = values.copy()
    return self.make_block_same_class(values)


Block.copy = copy  # Block for pandas 0.23.4: in pandas.core.internals.Block

memory.py

Создайте файл memory.py со следующим содержимым.

"""
Implements MemoryLayout copy factory to change memory layout
of `numpy.ndarrays`.
Depending on the use case, operations on DataFrames can be much
faster if the appropriate memory layout is set and preserved.

The implementation allows for changing the desired layout. Changes apply when
copies or new objects are created, as for example, when slicing or aggregating
via groupby ...

This implementation tries to solve the issue raised on GitHub
https://github.com/pandas-dev/pandas/issues/26502

"""
import numpy as np

_DEFAULT_MEMORY_LAYOUT = 'K'


class MemoryLayout(object):
    """
    Memory layout management for numpy.ndarrays.

    Singleton implementation.

    Example:
    >>> from memory import memory_layout
    >>> memory_layout.order = 'K'  #
    >>> # K ... keep array layout from input
    >>> # C ... set to c-contiguous / column order
    >>> # F ... set to f-contiguous / row order
    >>> array = memory_layout.apply(array)
    >>> array = memory_layout.apply(array, 'C')
    >>> array = memory_layout.copy(array)
    >>> array = memory_layout.apply_on_transpose(array)

    """

    _order = _DEFAULT_MEMORY_LAYOUT
    _instance = None

    @property
    def order(self):
        """
        Return memory layout ordering.

        :return: `str`
        """
        if self.__class__._order is None:
            raise AssertionError("Array layout order not set.")
        return self.__class__._order

    @order.setter
    def order(self, order):
        """
        Set memory layout order.
        Allowed values are 'C', 'F', and 'K'. Raises AssertionError
        when trying to set other values.

        :param order: `str`
        :return: `None`
        """
        assert order in ['C', 'F', 'K'], "Only 'C', 'F' and 'K' supported."
        self.__class__._order = order

    def __new__(cls):
        """
        Create only one instance throughout the lifetime of this process.

        :return: `MemoryLayout` instance as singleton
        """
        if cls._instance is None:
            cls._instance = super(MemoryLayout, cls).__new__(MemoryLayout)
        return cls._instance

    @staticmethod
    def get_from(array):
        """
        Get memory layout from array

        Possible values:
           'C' ... only C-contiguous or column order
           'F' ... only F-contiguous or row order
           'O' ... other: both, C- and F-contiguous or both
           not C- or F-contiguous (as on empty arrays).

        :param array: `numpy.ndarray`
        :return: `str`
        """
        if array.flags.c_contiguous == array.flags.f_contiguous:
            return 'O'
        return {True: 'C', False: 'F'}[array.flags.c_contiguous]

    def apply(self, array, order=None):
        """
        Apply the order set or the order given as input on the array
        given as input.

        Possible values:
           'C' ... apply C-contiguous layout or column order
           'F' ... apply F-contiguous layout or row order
           'K' ... keep the given layout

        :param array: `numpy.ndarray`
        :param order: `str`
        :return: `np.ndarray`
        """
        order = self.__class__._order if order is None else order

        if order == 'K':
            return array

        array_order = MemoryLayout.get_from(array)
        if array_order == order:
            return array

        return np.reshape(np.ravel(array), array.shape, order=order)

    def copy(self, array, order=None):
        """
        Return a copy of the input array with the memory layout set.
        Layout set:
           'C' ... return C-contiguous copy
           'F' ... return F-contiguous copy
           'K' ... return copy with same layout as
           given by the input array.

        :param array: `np.ndarray`
        :return: `np.ndarray`
        """
        order = order if order is not None else self.__class__._order
        return array.copy(order=self.get_from(array)) if order == 'K' \
            else array.copy(order=order)

    def copy_transposed(self, array):
        """
        Return a copy of the input array in order that its transpose
        has the memory layout set.

        Note: numpy simply changes the memory layout from row to column
        order instead of reshuffling the data in memory.

        Layout set:
           'C' ... return F-contiguous copy
           'F' ... return C-contiguous copy
           'K' ... return copy with oposite (C versus F) layout as
           given by the input array.

        :param array: `np.ndarray`
        :return: `np.ndarray`

        :param array:
        :return:
        """
        if self.__class__._order == 'K':
            return array.copy(
                order={'C': 'C', 'F': 'F', 'O': None}[self.get_from(array)])
        else:
            return array.copy(
                order={'C': 'F', 'F': 'C'}[self.__class__._order])

    def __str__(self):
        return str(self.__class__._order)


memory_layout = MemoryLayout()  # Singleton
Другие вопросы по тегам