Уникальный Numpy: количество значений, также отсутствующих в массиве?

#python #numpy

Вопрос:

У меня есть массив как таковой:

 myarray = [['a', 'b', 'c'],
           ['b', 'c', 'd'],
           ['c', 'd', 'e']]
 

И для этого np.unique(myarray, return_counts=True) работает потрясающе и дает мне желаемый результат. Однако затем я хотел бы применить его строка за строкой, и чтобы он мог сообщить мне, что в строке № 1 значения d и e равны 0.

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

 for i in range(mylen):
    unique, counts = np.unique(np.array([list(myarray[i]), 'a', 'b', 'c', 'd', 'e']), return_counts=True) # attempt 1
    unique, counts = np.unique(np.vstack((myarray[i], 'a', 'b', 'c', 'd', 'e')), return_counts=True) # attempt 2
 

Но ни то, ни другое не работает. У кого-нибудь есть элегантное решение? Это будет использоваться для тысяч, возможно, миллионов значений, поэтому время вычисления имеет некоторое отношение к обсуждению.

Ответ №1:

Вы можете использовать np.unique с return_inverse=True , чтобы получить то, что вы хотите:

 letters, inv = np.unique(myarray, return_inverse=True)
inv = inv.reshape(myarray.shape)
 

inv это сейчас

 array([[0, 1, 2],
       [1, 2, 3],
       [2, 3, 4]], dtype=int64)
 

Вы можете получить количество всех уникальных элементов в одной строке:

 >>> (inv == np.arange(len(letters)).reshape(-1, 1, 1)).sum(-1)
array([[1, 0, 0],
       [1, 1, 0],
       [1, 1, 1],
       [0, 1, 1],
       [0, 0, 1]])
 

Первое измерение соответствует букве в letters , второе-номеру строки, так sum(-1) как суммы по столбцам. Вы можете получить подсчеты для столбцов, используя sum(1) вместо этого. В вашем симметричном примере результат будет идентичным.

Никакого зацикливания, никакого np.apply_along_axis (что является прославленным циклом), все векторизовано. Вот краткий временной тест:

 np.random.seed(42)
myarray = np.random.choice(list(string.ascii_lowercase), size=(100, 100))

def Epsi95(arr):
    uniques = np.unique(arr)
    def fun(x):
        base_dict = dict(zip(uniques, [0]*uniques.shape[0]))
        base_dict.update(dict(zip(*np.unique(x, return_counts=True))))
        return [i[-1] for i in sorted(base_dict.items())]
    return np.apply_along_axis(fun, 1, arr)

def MadPhysicist(myarray):
    letters, inv = np.unique(myarray, return_inverse=True)
    inv = inv.reshape(myarray.shape)
    return (inv == np.arange(len(letters)).reshape(-1, 1, 1)).sum(-1)    

%timeit Epsi95(myarray)
6.37 ms ± 26.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit MadPhysicist(myarray)
1.28 ms ± 6.85 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
 

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

1. Да, это решение намного превосходит по эффективности.

2. Это выглядит фантастически, спасибо. Просто чтобы мне было ясно по нескольким пунктам: 1) массив, который вы показываете после (inv == np.arange(len(letters)).reshape(-1, 1, 1)).sum(-1) , предназначен для каждой возможности в моей вселенной, количество раз, когда он появляется в каждой строке, верно? Это [1,0,0] означает, что он появляется один раз в первой строке, а затем вообще не в строках 2 и 3. 2) Если бы я хотел применить это к столбцам вместо строк, как бы это работало? Было бы проще всего просто перенести myarray и применить ту же функцию?

3. Вы можете играть с размерами да. И да, к интерпретации данных.

Ответ №2:

 myarray = [['a', 'b', 'c'],
           ['b', 'c', 'd'],
           ['c', 'd', 'e']]

arr = np.array(myarray)

uniques = np.unique(arr)

def fun(x):
    base_dict = dict(zip(uniques, [0]*uniques.shape[0]))
    base_dict.update(dict(zip(*np.unique(x, return_counts=True))))
    return [i[-1] for i in sorted(base_dict.items())]

np.apply_along_axis(fun, 1, arr)

# array([[1, 1, 1, 0, 0], # a=1 b=1 c=1 d=0 e=0
#        [0, 1, 1, 1, 0],
#        [0, 0, 1, 1, 1]], dtype=int64)
 

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

1. apply_along_axis это просто прославленный for цикл, что бы вам ни говорили документы.

2. Вам никогда не нужно сортировать выходные данные unique

3. вы правы (оба случая), во втором случае на самом деле я занимался return list(base_dict.values()) , так как дикт 3.6 поддерживает порядок, но позже подумал обобщить и забыл удалить первый сорт.

Ответ №3:

Вы можете выполнить итерацию по строкам списка, а затем по уникальным значениям всего набора. Приведу пример ниже, и его можно использовать для вставки элементов в словарь или любую другую структуру по вашему выбору.

Пример:

 import numpy as np

myarray = [['a', 'b', 'c'],
           ['b', 'c', 'd'],
           ['c', 'd', 'e']]

uniq = np.unique(np.array(myarray))

for idx, row in enumerate(myarray):
    for x in uniq:
        print(f"Row {idx} Element ({x}) Count: {row.count(x)}")
 

Выход:

 Row 0 Element (a) Count: 1
Row 0 Element (b) Count: 1
Row 0 Element (c) Count: 1
Row 0 Element (d) Count: 0
Row 0 Element (e) Count: 0
Row 1 Element (a) Count: 0
Row 1 Element (b) Count: 1
Row 1 Element (c) Count: 1
Row 1 Element (d) Count: 1
Row 1 Element (e) Count: 0
Row 2 Element (a) Count: 0
Row 2 Element (b) Count: 0
Row 2 Element (c) Count: 1
Row 2 Element (d) Count: 1
Row 2 Element (e) Count: 1
 

Чтобы использовать список словарей для каждой строки:

 import numpy as np

myarray = [['a', 'b', 'c'],
           ['b', 'c', 'd'],
           ['c', 'd', 'e']]

uniq = np.unique(np.array(myarray))
row_vals = []

for idx, row in enumerate(myarray):
    dict = {}
    for x in uniq:
        dict[x] = row.count(x)
    row_vals.append(dict)

for r in row_vals:
    print(r)
 

Выход:

 {'a': 1, 'b': 1, 'c': 1, 'd': 0, 'e': 0}
{'a': 0, 'b': 1, 'c': 1, 'd': 1, 'e': 0}
{'a': 0, 'b': 0, 'c': 1, 'd': 1, 'e': 1}