Миграция с spmatrix на sparray#

Этот документ содержит рекомендации по преобразованию кода из разреженного матрицы в разреженный массивы в scipy.sparse.

Переход от разреженных матриц к разреженным массивам отражает преобразование из np.matrix to np.ndarray. По сути, мы должны перейти от ориентированной на умножение матриц 2D-модели matrix объект в 1D или 2D "массивный" объект, который поддерживает оператор матричного умножения и поэлементные вычисления.

Обозначение: В этом руководстве мы обозначаем классы разреженных массивов в общем виде как sparray и классы разреженных матриц spmatrix. Плотные массивы numpy обозначаются np.ndarray и классы плотных матриц являются np.matrix. Поддерживаемые разреженные форматы обозначаются BSR, COO, CSC, CSR, DIA, DOK, LIL, и все форматы поддерживаются как sparray, так и spmatrix. Термин sparse относится к sparray или spmatrix, в то время как dense относится к np.ndarray или np.matrix.

Обзор и общая картина#

  • Имена конструкторов *_matrix, например, csr_matrix, изменены на *_array.

  • spmatrix M всегда 2D (строки x столбцы), даже если, например, M.min(axis=0). sparray A может быть 1D или 2D. Скаляры NumPy возвращаются для полных (0D) редукций, т.е. M.min().

  • Итерация по sparray даёт одномерные sparray. Итерация по spmatrix даёт двумерные строковые spmatrix.

  • Операторы, изменяющие поведение: *, @, *=, @=, **

    • Скалярное умножение, например. 5 * A, использует *, и 5 @ A не реализовано.

    • sparrays используют * для поэлементного умножения и @ для умножения матриц, в то время как разреженные матрицы используют либо оператор * или @ для умножения матриц. Можно использовать A.multiply(B) для поэлементного умножения.

    • Скалярные показатели степени, например. A**2, используйте поэлементное возведение в степень для sparray и матричное возведение в степень для spmatrix. Матричное возведение в степень для sparrays использует scipy.sparse.linalg.matrix_power(A, n).

  • Когда функции конструкторам предоставляются индексные массивы, spmatrix выбирает dtype на основе dtype и значений входящих массивов, в то время как sparray учитывает только dtype входящих массивов. Например, M=csr_matrix((data, indices, indptr)) приводит к int32 dtype для M.indices до тех пор, пока значения в indices и indptr малы, даже если dtype входящих массивов int64. В отличие от A=csr_array((data, indices, indptr)) приводит к int64 dtype для A.indices когда входные массивы int64. Это обеспечивает более предсказуемые, часто большие, типы данных индексов в sparrays и меньшее приведение типов для соответствия типам данных.

  • Проверка типа и формата разреженной матрицы:

    • issparse(A) возвращает True для любого разреженного массива/матрицы.

    • isspmatrix(M) возвращает True для любой разреженной матрицы.

    • isspmatrix_csr(M) проверяет разреженную матрицу с определенным форматом. Её следует заменить на версию, совместимую с массивами, такую как:

    • issparse(A) and A.format == 'csr' который проверяет разреженный массив/матрицу CSR.

  • Обработка API вашего программного пакета с разреженным вводом/выводом:

    • Входные данные довольно легко заставить работать как с spmatrix, так и с sparray. Пока вы используете A.multiply(B) для поэлементных и A @ B для умножения матриц, и вы используете sparse.linalg.matrix_power для возведения матрицы в степень, вы будете в порядке после завершения «первого прохода» шагов миграции, описанных в следующем разделе. Ваш код будет обрабатывать оба типа входных данных взаимозаменяемо.

    • Перенос разреженных выходных данных из ваших функций требует немного больше размышлений. Составьте список всех ваших публичных функций, которые возвращают объекты spmatrix. Проверьте, считаете ли вы допустимым возвращать вместо них sparray. Это зависит от вашей библиотеки и её пользователей. Если вы хотите разрешить этим функциям продолжать возвращать объекты spmatrix или sparray, вы часто можете сделать это, используя разреженный вход, который также служит сигналом для того, какой тип выходных данных должен быть возвращён. Спроектируйте вашу функцию так, чтобы она возвращала тип, который был на входе. Этот подход можно расширить на плотные входные данные. Если вход — это np.matrix или маскированный массив с np.matrix в качестве его ._baseclass атрибут, затем вернуть spmatrix. В противном случае вернуть sparray. Без этих входных данных два других подхода — создать ключевой аргумент для указания, что возвращать, или создать новую функцию (как мы сделали, например, eye_array), который имеет тот же базовый синтаксис, но возвращает sparray. Какой метод вы выберете, должно зависеть от вашей библиотеки, ваших пользователей и ваших предпочтений.

Подробности: функции построения#

Эти четыре функции новые и работают только с sparrays: block_array, diags_array, eye_array, и random_array. Их сигнатуры:

def block_array(blocks, format=None, dtype=None):
def diags_array(diagonals, /, *, offsets=0, shape=None, format=None, dtype=None):
def eye_array(m, n=None, *, k=0, dtype=float, format=None):
def random_array(shape, density=0.01, format='coo', dtype=None, rng=None, data_sampler=None):

The random_array функция имеет shape (2-кортеж) arg вместо двух целых чисел. И rng аргумент по умолчанию соответствует новому в NumPy default_rng(). Это отличается от spmatrix rand и random которые по умолчанию используют глобальный экземпляр RandomState. Если вам не важны эти детали, оставление значения по умолчанию должно работать нормально. Если вы заботитесь о посеве случайных чисел, вам, вероятно, следует добавить rng=... keyword argument к этому вызову при переключении функций. Вкратце, для перехода на random_array измените имя функции, замените аргумент shape на один аргумент-кортеж, оставьте любые другие параметры как прежде и подумайте, какой тип rng= аргумент должен использоваться, если есть.

The diags_array функция использует правила только для ключевых слов аргументов. Поэтому вы должны вводить offsets= перед аргументами смещений. Это кажется неудобным при переходе от использования diags, но это помогает избежать путаницы и облегчает чтение. Единый параметр формы заменяет два целых числа для этой миграции.

Существующие функции, требующие осторожной миграции#

Эти функции возвращают sparray или spmatrix, в зависимости от типов входных данных, которые они получают: kron, kronsum, hstack, vstack, block_diag, tril, и triu. Их сигнатуры:

def kron(A, B, format=None):
def kronsum(A, B, format=None):
def hstack(blocks, format=None, dtype=None):
def vstack(blocks, format=None, dtype=None):
def block_diag(mats, format=None, dtype=None):
def tril(A, k=0, format=None):
def triu(A, k=0, format=None):

Использование этих функций должно быть проверено, а входные данные скорректированы, чтобы гарантировать возврат sparrays. В свою очередь, выходные данные должны рассматриваться как sparrays. Чтобы возвращать sparrays, хотя бы один входной аргумент должен быть sparray. Если вы используете списки списков или массивы numpy в качестве входных данных, вам следует преобразовать один из них в разреженный массив, чтобы получить разреженные массивы на выходе.

Функции, изменившие названия при миграции#

Функция

Новая функция

Комментарии

eye

eye_array

идентичность

eye_array

diags

diags_array

ввод только по ключевым словам

spdiags

dia_array

форма как 2-кортеж

bmat

блок

rand

random_array

форма как 2-кортеж и default_rng

random

random_array

форма как 2-кортеж и default_rng

Подробности: изменения формы и редукции#

  • Построение с использованием одномерного списка значений:

    • csr_array([1, 2, 3]).shape == (3,) 1D вход создаёт 1D массив.

    • csr_matrix([1, 2, 3]).shape == (1, 3) 1D вход создает 2D матрицу.

  • Индексирование и итерация:

    • Индексирование sparray допускает 1D объекты, которые могут быть преобразованы в 2D с помощью np.newaxis или None. Например, A[3, None, :] даёт 2D строку. Индексация 2D sparray с неявным (не заданным) индексом столбца даёт 1D результат, например. A[3] (примечание: лучше не делать этого — записать это как A[3, :] вместо этого). Если вам нужен 2D результат, используйте np.newaxis, или None в вашем индексе, или оберните целочисленный индекс в список, для которого причудливая индексация дает 2D, например, A[[3], :].

    • Итерация по разреженному объекту: next(M) возвращает разреженную двумерную матрицу-строку, next(A) возвращает разреженный одномерный массив.

  • Операции редукции вдоль оси уменьшают размерность:

    • M.min(axis=1) возвращает двумерную строковую матрицу минимумов вдоль оси 1.

    • A.min(axis=1) возвращает одномерный coo_array минимума вдоль оси 1. Некоторые операции редукции возвращают плотные массивы/матрицы вместо разреженных:

      Метод

      Результат

      sum(axis)

      dense

      mean(axis)

      dense

      argmin(axis)

      dense

      argmax(axis)

      dense

      min(axis)

      разреженный

      max(axis)

      разреженный

      nanmin(axis)

      разреженный

      nanmax(axis)

      разреженный

    Обычно двумерные разреженные массивы (sparray) приводят к одномерным результатам. Двумерные разреженные матрицы (spmatrix) приводят к двумерным результатам.

  • Некоторые редукции возвращают скаляр. Они должны вести себя как раньше и не должны учитываться при миграции. Например, A.min()

Удалённые методы и атрибуты#

  • Методы get_shape, getrow, getcol, asfptype, getnnz, getH и атрибуты .A и .H присутствуют только на spmatrices, не на sparrays. Рекомендуется заменить их использование альтернативами перед началом перехода на sparray.

    Функция

    Альтернативный

    M.get_shape()

    A.shape

    M.getformat()

    A.format

    M.asfptype(…)

    A.astype(…)

    M.getmaxprint()

    A.maxprint

    M.getnnz()

    A.nnz

    M.getnnz(axis)

    A.count_nonzero(axis)

    M.getH()

    A.conj().T

    M.getrow(i)

    A[i, :]

    M.getcol(j)

    A[:, j]

    M.A

    A.toarray()

    M.H

    A.conj().T

  • Назначение формы (M.shape = (2, 6)) не разрешён для sparray. Вместо этого следует использовать A.reshape.

  • M.getnnz() возвращает количество хранимых значений – не количество ненулевых элементов. A.nnz делает то же самое. Чтобы получить количество ненулевых элементов, используйте A.count_nonzero(). Это не ново для миграции, но может сбивать с толку.

    Для перехода с axis параметр M.getnnz(axis=...), вы можете использовать A.count_nonzero(axis=...) но это не точная замена, потому что она подсчитывает ненулевые значения вместо сохраненных значений. Разница заключается в количестве явно сохраненных нулевых значений. Если вам действительно нужно количество сохраненных значений по оси, вам потребуется использовать некоторые инструменты numpy.

    Подход с использованием инструментов numpy работает для форматов COO, CSR, CSC, поэтому преобразуйте в один из них. Для CSR и CSC основная ось сжимается и np.diff(A.indptr) возвращает плотный 1D массив с количеством сохранённых значений для каждого основного значения оси (строка для CSR и столбец для CSC). Второстепенные оси можно вычислить с помощью np.bincount(A.indices, minlength=N) где N — это длина малой оси (например, A.shape[1] для CSR). bincount функция работает для любой оси формата COO с использованием A.coords[axis] вместо A.indices.

Используйте тесты для поиска * и ** мест#

  • Может быть сложно отличить скалярное умножение * из матричного умножения * при миграции вашего кода. Python решил эту проблему, в теории, введя оператор матричного умножения @. * используется для скалярного умножения, в то время как @ для матричного умножения. Но преобразование выражений, использующих * для обоих может быть сложным и вызывать напряжение глаз. К счастью, если ваш код имеет набор тестов, который покрывает выражения, которые вам нужно преобразовать, вы можете использовать его для поиска мест, где * используется для матричного умножения с разреженными матрицами. Измените их на @.

    Подход изменяет методы класса spmatrix на лету, чтобы вызывать исключение, когда * используется для матричного умножения (и не вызывает ошибку при скалярном умножении). Тестовый набор будет отмечать сбой в этих местах. И неудачный тест здесь является успехом, потому что он показывает, где вносить изменения. Измените проблемный * to @, посмотрите рядом на другие похожие изменения и снова запустите тесты. Аналогично, этот подход помогает найти, где ** используется для возведения матрицы в степень. SciPy вызывает исключение, когда @ используется для скалярного умножения, так что это позволит обнаружить места, где вы изменяете то, что не должны были. Таким образом, тестовый набор с этой обезьяньей заплаткой также проверяет исправления.

    Добавьте следующий код в ваш conftest.py файл. Затем запустите тесты локально. Если есть много матричных выражений, возможно, вы захотите протестировать одну часть вашей кодовой базы за раз. Быстрое чтение кода показывает, что он вызывает ValueError всякий раз, когда * используется между двумя матрично-подобными объектами (разреженными или плотными), и всякий раз, когда ** используется для возведения матрицы в степень. Также выдает предупреждение при использовании sum/mean/min/max/argmin/argmax с осью, так что вывод будет 2D для spmatrix и 1D для sparray. Это означает, что вы проверяете, что код обрабатывает вывод как 1D, так и 2D через flatten/ravel, np.atleast_2d или индексирование.

    #================== Added to check spmatrix usage ========================
    import scipy
    from warnings import warn
    
    def flag_this_call(*args, **kwds):
        raise ValueError("Old spmatrix function names for rand/spdiags called")
    
    scipy.sparse._construct.rand = flag_this_call
    scipy.sparse._construct.spdiags = flag_this_call
    
    class _strict_mul_mixin:
        def __mul__(self, other):
            if not scipy.sparse._sputils.isscalarlike(other):
                raise ValueError('Operator * used here! Change to @?')
            return super().__mul__(other)
    
        def __rmul__(self, other):
            if not scipy.sparse._sputils.isscalarlike(other):
                raise ValueError('Operator * used here! Change to @?')
            return super().__rmul__(other)
    
        def __imul__(self, other):
            if not scipy.sparse._sputils.isscalarlike(other):
                raise ValueError('Operator * used here! Change to @?')
            return super().__imul__(other)
    
        def __pow__(self, *args, **kwargs):
            raise ValueError('spmatrix ** found! Use linalg.matrix_power?')
    
        @property
        def A(self):
            raise TypeError('spmatrix A property found! Use .toarray()')
    
        @property
        def H(self):
            raise TypeError('spmatrix H property found! Use .conjugate().T')
    
        def asfptype(self):
            raise TypeError('spmatrix asfptype found! rewrite needed')
    
        def get_shape(self):
            raise TypeError('spmatrix get_shape found! Use .shape')
    
        def getformat(self):
            raise TypeError('spmatrix getformat found! Use .format')
    
        def getmaxprint(self):
            raise TypeError('spmatrix getmaxprint found! Use .shape')
    
        def getnnz(self):
            raise TypeError('spmatrix getnnz found! Use .nnz')
    
        def getH(self):
            raise TypeError('spmatrix getH found! Use .conjugate().T')
    
        def getrow(self):
            raise TypeError('spmatrix getrow found! Use .row')
    
        def getcol(self):
            raise TypeError('spmatrix getcol found! Use .col')
    
        def sum(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix sum found using axis={axis}. "
                     "\nsparray with a single axis will produce 1D output. "
                     "\nCheck nearby to ensure 1D output is handled OK in this spot.\n")
            print(f"{args=} {axis=} {kwds=}")
            return super().sum(*args, **kwds)
    
        def mean(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix mean found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output.\n"
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().mean(*args, **kwds)
    
        def min(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix min found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().min(*args, **kwds)
    
        def max(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix max found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().max(*args, **kwds)
    
        def argmin(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix argmin found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().argmin(*args, **kwds)
    
        def argmax(self, *args, **kwds):
            axis = args[0] if len(args)==1 else args if args else kwds.get("axis", None)
            if axis is not None:
                warn(f"\nMIGRATION WARNING: spmatrix argmax found using axis={axis}."
                     "\nsparray with a single axis will produce 1D output. "
                     "Check nearby to ensure 1D output is handled OK in this spot.\n")
            return super().argmax(*args, **kwds)
    
    
    class coo_matrix_strict(_strict_mul_mixin, scipy.sparse.coo_matrix):
        pass
    
    class bsr_matrix_strict(_strict_mul_mixin, scipy.sparse.bsr_matrix):
        pass
    
    class csr_matrix_strict(_strict_mul_mixin, scipy.sparse.csr_matrix):
        pass
    
    class csc_matrix_strict(_strict_mul_mixin, scipy.sparse.csc_matrix):
        pass
    
    class dok_matrix_strict(_strict_mul_mixin, scipy.sparse.dok_matrix):
        pass
    
    class lil_matrix_strict(_strict_mul_mixin, scipy.sparse.lil_matrix):
        pass
    
    class dia_matrix_strict(_strict_mul_mixin, scipy.sparse.dia_matrix):
        pass
    
    scipy.sparse.coo_matrix = scipy.sparse._coo.coo_matrix = coo_matrix_strict
    scipy.sparse.bsr_matrix = scipy.sparse._bsr.bsr_matrix = bsr_matrix_strict
    scipy.sparse.csr_matrix = scipy.sparse._csr.csr_matrix = csr_matrix_strict
    scipy.sparse.csc_matrix = scipy.sparse._csc.csc_matrix = csc_matrix_strict
    scipy.sparse.dok_matrix = scipy.sparse._dok.dok_matrix = dok_matrix_strict
    scipy.sparse.lil_matrix = scipy.sparse._lil.lil_matrix = lil_matrix_strict
    scipy.sparse.dia_matrix = scipy.sparse._dia.dia_matrix = dia_matrix_strict
    
    scipy.sparse._compressed.csr_matrix = csr_matrix_strict
    
    scipy.sparse._construct.bsr_matrix = bsr_matrix_strict
    scipy.sparse._construct.coo_matrix = coo_matrix_strict
    scipy.sparse._construct.csc_matrix = csc_matrix_strict
    scipy.sparse._construct.csr_matrix = csr_matrix_strict
    scipy.sparse._construct.dia_matrix = dia_matrix_strict
    
    scipy.sparse._extract.coo_matrix = coo_matrix_strict
    
    scipy.sparse._matrix.bsr_matrix = bsr_matrix_strict
    scipy.sparse._matrix.coo_matrix = coo_matrix_strict
    scipy.sparse._matrix.csc_matrix = csc_matrix_strict
    scipy.sparse._matrix.csr_matrix = csr_matrix_strict
    scipy.sparse._matrix.dia_matrix = dia_matrix_strict
    scipy.sparse._matrix.dok_matrix = dok_matrix_strict
    scipy.sparse._matrix.lil_matrix = lil_matrix_strict
    
    del coo_matrix_strict
    del bsr_matrix_strict
    del csr_matrix_strict
    del csc_matrix_strict
    del dok_matrix_strict
    del lil_matrix_strict
    del dia_matrix_strict
    #==========================================
    

Типы данных индексных массивов#

Если вы предоставляете сжатые индексы конструктору, например, csr_array((data, indices, indptr)) разреженные массивы устанавливают dtype индекса, проверяя только dtype массивов индексов, в то время как разреженные матрицы проверяют также значения индексов и могут понизить до int32 (см. gh-18509 для более подробной информации). Это означает, что вы можете получить индексацию int64 там, где раньше получали int32. Вы можете управлять этим, установив dtype перед созданием экземпляра, или путём переприведения после построения.

Две разреженные служебные функции могут помочь с обработкой типа индекса. Используйте get_index_dtype(arrays, maxval, check_contents) при создании индексов для поиска подходящего типа данных (int32 или int64) для использования в ваших сжатых индексах.

Используйте safely_cast_index_arrays(A, idx_dtype) для преобразования после создания, обеспечивая отсутствие переполнений при понижении типа. Эта функция не изменяет входной массив. Преобразованные массивы возвращаются. Копии создаются только при необходимости, поэтому можно проверить, было ли выполнено преобразование, используя if indices is not A.indices:

Сигнатуры функций:

def get_index_dtype(arrays=(), maxval=None, check_contents=False):
def safely_cast_index_arrays(A, idx_dtype=np.int32, msg=""):

Примеры идиом включают следующие для get_index_dtype:

.. code-block:: python

    # select index dtype before construction based on shape
    shape = (3, 3)
    idx_dtype = scipy.sparse.get_index_dtype(maxval=max(shape))
    indices = np.array([0, 1, 0], dtype=idx_dtype)
    indptr = np.arange(3, dtype=idx_dtype)
    A = csr_array((data, indices, indptr), shape=shape)

и для safely_cast_index_arrays:

.. code-block:: python

    # rescast after construction, raising exception if shape too big
    indices, indptr = scipy.sparse.safely_cast_index_arrays(B, np.int32)
    B.indices, B.indptr = indices, indptr

Другие#

  • Бинарные операторы +, -, *, /, @, !=, > работать с разреженными и/или плотными операндами:

    • Если все входные данные разрежены, выходные данные обычно также разрежены. Исключением является / которая возвращает плотную матрицу (делением на значение по умолчанию 0 является nan).

    • Если входные данные смешанные (разреженные и плотные), результат обычно плотный (т.е., np.ndarray). Исключениями являются * который является разреженным, и / который не реализован для dense / sparse, и возвращает разреженную для sparse / dense.

  • Бинарные операторы +, -, *, /, @, !=, > с операндами массива и/или матрицы:

    • Если все входные данные являются массивами, выходные данные также являются массивами, и то же самое верно для матриц.

    • При смешивании разреженных массивов с разреженными матрицами, ведущий операнд определяет тип вывода, например, sparray + spmatrix дает разреженный массив, в то время как обратный порядок дает разреженную матрицу.

    • При смешивании плотных матриц с разреженными массивами результаты обычно являются массивами, за исключением сравнений, например. > которые возвращают плотные матрицы.

    • При смешивании плотных массивов с разреженными матрицами, результаты обычно являются матрицами, за исключением array @ sparse matrix который возвращает плотный массив.