Вставьте ноль строк и столбцов одновременно в определенных индексах, а не в конце - PullRequest
0 голосов
/ 20 декабря 2018

У меня есть двумерный массив (матрица путаницы), например (3,3).Число в массиве относится к индексу в набор меток.Я знаю, что этот массив должен быть (5,5) вместо (3,3) для 5 строк и столбцов.Я могу найти метки, которые были «хитами»:

import numpy as np

x = np.array([[3, 0, 3],
              [0, 2, 0],
              [2, 3, 3]])
labels = ["a", "b", "c", "d", "e"]
missing_idxs = np.setdiff1d(np.arange(len(labels)), x)  # array([1, 4]

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

y = np.array([[3, 0, 0, 3, 0],
              [0, 0, 0, 0, 0],  # <- Inserted row at index 1 all zeros
              [0, 0, 2, 0, 0],
              [2, 0, 3, 3, 0],
              [0, 0, 0, 0, 0]])  # <- Inserted row at index 4 all zeros
              #   ^        ^
              #   |        |
              # Inserted columns at index 1 and 4 all zeros

Я могу сделать это с помощью нескольких вызовов np.insert в цикле по всем отсутствующим индексам:

def insert_rows_columns_at_slow(arr, indices):
    result = arr.copy()
    for idx in indices:
        result = np.insert(result, idx, np.zeros(result.shape[1]), 0)
        result = np.insert(result, idx, np.zeros(result.shape[0]), 1)

Однако мой реальный массив намного больше, и может быть много других отсутствующих индексов.Поскольку np.insert перераспределяет каждый раз, это не очень эффективно.

Как я могу достичь того же результата, но более эффективным, векторизованным способом?Бонусные баллы, если он работает в более чем 2 измерениях.

Ответы [ 2 ]

0 голосов
/ 20 декабря 2018

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

Вместо использования отсутствующих индексов используйте не пропущенные индексы:

non_missing_idxs = np.union1d(np.arange(len(labels)), x)  # array([0, 2, 3])
y = np.zeros((5,5))
y[non_missing_idxs[:,None], non_missing_idxs] = x

вывод:

array([[3., 0., 0., 3., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 2., 0., 0.],
       [2., 0., 3., 3., 0.],
       [0., 0., 0., 0., 0.]])
0 голосов
/ 20 декабря 2018

Это можно сделать, предварительно выделив полный результирующий массив и заполнив строки и столбцы старым массивом, даже в нескольких измерениях, и размеры не должны совпадать с размером:

def insert_at(arr, output_size, indices):
    """
    Insert zeros at specific indices over whole dimensions, e.g. rows and/or columns and/or channels.
    You need to specify indices for each dimension, or leave a dimension untouched by specifying
    `...` for it. The following assertion should hold:

            `assert len(output_size) == len(indices) == len(arr.shape)`

    :param arr: The array to insert zeros into
    :param output_size: The size of the array after insertion is completed
    :param indices: The indices where zeros should be inserted, per dimension. For each dimension, you can 
                specify: - an int
                         - a tuple of ints
                         - a generator yielding ints (such as `range`)
                         - Ellipsis (=...)
    :return: An array of shape `output_size` with the content of arr and zeros inserted at the given indices.
"""
    # assert len(output_size) == len(indices) == len(arr.shape)
    result = np.zeros(output_size)
    existing_indices = [np.setdiff1d(np.arange(axis_size), axis_indices,assume_unique=True)
                        for axis_size, axis_indices in zip(output_size, indices)]
    result[np.ix_(*existing_indices)] = arr
    return result

Для вашего случая использования вы можете использовать его так:

def fill_by_label(arr, labels):
    # If this is your only use-case, you can make it more efficient
    # By not computing the missing indices first, just to compute
    # The existing indices again
    missing_idxs = np.setdiff1d(np.arange(len(labels)), x)
    return insert_at(arr, output_size=(len(labels), len(labels)),
                                       indices=(missing_idxs, missing_idxs))

x = np.array([[3, 0, 3],
              [0, 2, 0],
              [2, 3, 3]])
labels = ["a", "b", "c", "d", "e"]
missing_idxs = np.setdiff1d(np.arange(len(labels)), x)
print(fill_by_label(x, labels))
>> [[3. 0. 0. 3. 0.]
    [0. 0. 0. 0. 0.]
    [0. 0. 2. 0. 0.]
    [2. 0. 3. 3. 0.]
    [0. 0. 0. 0. 0.]]

Но это очень гибко.Вы можете использовать его для заполнения нулями:

def zero_pad(arr):
    out_size = np.array(arr.shape) + 2
    indices = (0, out_size[0] - 1), (0, out_size[1] - 1)
    return insert_at(arr, output_size=out_size,
                                       indices=indices)

print(zero_pad(x))
>> [[0. 0. 0. 0. 0.]
    [0. 3. 0. 3. 0.]
    [0. 0. 2. 0. 0.]
    [0. 2. 3. 3. 0.]
    [0. 0. 0. 0. 0.]]

Он также работает с неквадратичными входами и выходами:

x = np.ones((3, 4))
print(insert_at(x, (4, 5), (2, 3)))
>>[[1. 1. 1. 0. 1.]
   [1. 1. 1. 0. 1.]
   [0. 0. 0. 0. 0.]
   [1. 1. 1. 0. 1.]]

С различным количеством вставок на измерение:

x = np.ones((3, 4))
print(insert_at(x, (4, 6), (1, (2, 4))))
>> [[1. 1. 0. 1. 0. 1.]
    [0. 0. 0. 0. 0. 0.]
    [1. 1. 0. 1. 0. 1.]
    [1. 1. 0. 1. 0. 1.]]

Вы можете использовать range (или другие генераторы) вместо перечисления каждого индекса:

x = np.ones((3, 4))
print(insert_at(x, (4, 6), (1, range(2, 4))))
>>[[1. 1. 0. 0. 1. 1.]
   [0. 0. 0. 0. 0. 0.]
   [1. 1. 0. 0. 1. 1.]
   [1. 1. 0. 0. 1. 1.]]

Работает с произвольными измерениями (если вы указали индексы для каждого измерения) 1:

x = np.ones((2, 2, 2))
print(insert_at(x, (3, 3, 3), (0, 0, 0)))
>>>[[[0. 0. 0.]
     [0. 0. 0.]
     [0. 0. 0.]]

    [[0. 0. 0.]
     [0. 1. 1.]
     [0. 1. 1.]]

    [[0. 0. 0.]
     [0. 1. 1.]
     [0. 1. 1.]]]

Вы можете использовать Ellipsis (= ...), чтобы указать, что вы не хотите изменять размер 1,2 :

x = np.ones((2, 2))
print(insert_at(x, (2, 4), (..., (0, 1))))
>>[[0. 0. 1. 1.]
   [0. 0. 1. 1.]]

1 : Вы можете автоматически определить это на основе arr.shape и output_size и при необходимости заполнить его ..., но я оставлю это на ваше усмотрение, если вынужно это.Если бы вы захотели, вы, возможно, могли бы вместо этого избавиться от параметра output_size, но тогда он усложняется при передаче в генераторы.

2 : это несколько отличается от обычного numpy... семантика, так как вам нужно указать ... для каждого измерения, которое вы хотите сохранить, т. Е. Следующее NOT работает:

x = np.ones((2, 2, 2))
print(insert_at(x, (2, 2, 3), (..., 0)))

Для синхронизации,Я выполнил вставку 10 строк и столбцов в массив 90x90 100000 раз, это результат:

x = np.random.random(size=(90, 90))
indices = np.arange(10) * 10


def measure_time_fast():
    insert_at(x, (100, 100), (indices, indices))


def measure_time_slow():
    insert_rows_columns_at_slow(x, indices)


if __name__ == '__main__':
    import timeit
    for speed in ("fast", "slow"):
        times = timeit.repeat(f"measure_time_{speed}()", setup=f"from __main__ import measure_time_{speed}", repeat=10, number=10000)
        print(f"Min: {np.min(times) / 10000}, Max: {np.max(times) / 10000}, Mean: {np.mean(times) / 10000} seconds per call")

Для быстрой версии:

Мин: 7.336409069976071e-05, Макс .: 7.7440657400075e-05, Среднее: 7.520040466995852e-05 секунд на вызов

Это примерно 75 микросекунд.

Для вашей медленной версии:

Мин: 0,00028272533010022016, Макс: 0,0002923079213000165, Среднее: 0,00028581595062998535 секунд на вызов

Это около 300 микросекунд.Разница будет тем больше, чем больше будут массивы.Например, для вставки 100 строк и столбцов в массив 900x900 это результаты (выполняются только 1000 раз):

Быстрая версия:

Мин .: 0.00022916630539984907, Макс .: 0.0022916630539984908, Среднее: 0,0022916630539984908 секунд на звонок

Медленная версия:

Мин: 0,013766934227399906, Макс .: 0,13766934227399907, Среднее: 0,13766934227399907 секунд на звонок

...