Алгоритм поиска в двумерном массиве

Рейтинг: 0Ответов: 1Опубликовано: 04.03.2025

Возникла необходимость написать некий анализатор, который рассчитывает выживаемость пациентов. Упрощенно его алгоритм работы выглядит так: введите сюда описание изображения

Некоторое рассчитанное врачом значение VAL [1] сравнивается со значениями в двумерном массиве [2]. Сравнение должно происходить только с данными из первого столбца.

Значение VAL может либо точно совпасть с одним из значений в массиве, не совпасть ни с одним из значений в массиве, но быть очень близко к одному из них; выйти за диапазон значений в массиве.

Если значение VAL точно совпало с одним из значений в массиве, алгоритм должен вывести врачу следующее значение в строке [3] (например, как на картинке, VAL = 11 и алгоритм выведет в print следующее значение в строке - 280).

На данный момент я написал упрощенную модель [ 2 ] этапа. Код ниже:

import numpy as np

a = np.array([
    [1, 244], [2, 211], [3, 466], [4, 698], [5, 899], [6, 109], [7, 129], [8, 140], 
[9, 168], [10, 188], [11, 280], [12, 282], [13, 245], [14, 256], [15, 258], 
[16, 305], [17, 352], [18, 345], [19, 365], [20, 348], [21, 440],[22, 424], 
[23, 444], [24, 446], [25, 477], [26, 479], [27, 571], [28, 573], [29, 557], 
[30, 577]
    ], int)

val = 11

if val in a:
    print('СОВПАТЕНИЕ ЕСТЬ!')
    idx = np.where(a == val)
    row = idx[0][0]
    print(f'Связанное значение: {a[row][1]}')
else:
    print('СОВПАДЕНИЙ НЕТ!')
    nearest_val = a.flat[np.abs(a - val).argmin()]
    print(f'Ближайшее значение: {nearest_val}')
    idx = np.where(a == nearest_val)
    row = idx[0][0]
    print(f'Связанное значение: {a[row][1]}')

Модель работает с двумя сценариями: когда значение VAL точно совпадает с одним из значений в массиве и когда точного значения нет, но, есть близкое к нему. Третий пока не писал. Используемый в модели массив небольшой всего 30 значений, тот, который будет использоваться в полноценном калькуляторе содержит более 5000 значений на один столбец и, как мне кажется, в такой ситуации логично воспользоваться инструментарием Numpy.

Я хотел бы услышать мнение более опытных в этих делах коллег:

  1. Насколько вменяемо реализована его логика? Есть ли более грамотный вариант с точки зрения опытного программиста?

  2. Алгоритм ищет сразу по всем значениям в массиве, а как можно ограничить поиск только одним столбцом?

  3. В модели массив небольшой, но, тот, что будет использоваться в полноценной версии калькулятора содержит более 5000 значений на столбец. Как лучше организовать такой массив? Сделать его внешним файлом? Или перенести все значения в сам код?

  4. Как можно ли как-то вытаскивать значения индексов переменных находящихся в массиве и использовать их как обычные int-значения? В модели я это сделал при помощи:

idx = np.where(a == val)
row = idx[0][0]

Это для точного совпадения. Но, у меня есть большие сомнения в правильности такого подхода.

ДОПОЛНЕНИЕ: В процессе изучения замечаний strawdog получилось собрать такой вариант алгоритма:

import numpy as np

a = np.array([
    [1, 244], [2, 211], [3, 466], [4, 698], [5, 899], [6, 109], [7, 129], [8, 140], 
[9, 168], [10, 188], [11, 280], [12, 282], [13, 245], [14, 256], [15, 258], 
[16, 305], [17, 352], [18, 345], [19, 365], [20, 348], [21, 440],[22, 424], 
[23, 444], [24, 446], [25, 477], [26, 479], [27, 571], [28, 573], [29, 557], 
[30, 577]
    ], int)

val = 2

row = a[np.abs(a[:,0] - val).argmin()]

if val in a[:,0]:
    print(f'СОВПАТЕНИЕ ЕСТЬ!\n'
          f'Связанное значение: {a[np.where(a == row)][1]}')
else:
    print(f'СОВПАДЕНИЙ НЕТ!\n'
          f'Ближайшее значение: {a[np.where(a == row)][0]}\n'
          f'Связанное значение: {a[np.where(a == row)][1]}')

В свою очередь он очень похож на вариант предложенный Serge3leo. Поэтому на нём пока и остановлюсь, в дальнейшем, дополню ответ добавлением сценариев обработки варианта вылета значений VAL за диапазон в массиве. Большое спасибо strawdog и Serge3leo за помощь в решении задачи!

Ответы

▲ 1Принят

Сам код, стоит немного подсократить:

import numpy as np

a = np.array([
        [1, 244], [2, 211], [3, 466], [4, 698], [5, 899], [6, 109], [7, 129], [8, 140], 
        [9, 168], [10, 188], [11, 280], [12, 282], [13, 245], [14, 256], [15, 258], 
        [16, 305], [17, 352], [18, 345], [19, 365], [20, 348], [21, 440],[22, 424], 
        [23, 444], [24, 446], [25, 477], [26, 479], [27, 571], [28, 573], [29, 557], 
        [30, 577]
        ], int,
        order='F')  # Т.к. массовые операции происходят по первому индексу, 
                    # лучше его упорядочить так. Как вариант, транспонировать массив.

val = 11.1

# В диапазоне нормализованых или целых чисел 0 == abs(a-b) <=> a == b
nearest_idx = np.argmin(np.abs(a[:, 0] - val))  
if val == a[nearest_idx, 0]: 
    print('СОВПАТЕНИЕ ЕСТЬ!')
else:
    # assert(abs(val) < np.max(np.abs(a[:, 0]))/np.finfo(val).eps)
    # assert(abs(val) < np.max(np.abs(a[:, 0])) * 10**15)
    # TODO: При произвольном val, необходимо сравнивать с min/max.
    print('СОВПАДЕНИЙ НЕТ!')
    print(f'Ближайшее значение: {a[nearest_idx, 0]}')
print(f'Связанное значение: {a[nearest_idx, 1]}')

Сделать его внешним файлом? Или перенести все значения в сам код?

Это зависит от порядка его изменения. К примеру, если это калибровки, то в код точно не стоит, точно лучше в файл, а если мировые константы, то можно и в код.

При прочих равных, в файл предпочтительнее, но это зависит от предполагаемого процесса тиражирования программы/модуля.

P.S.

Дополнения к вопросу о "файл/код". Строго говоря, "код" тоже разбивается два варианта, "код" может быть в своём модуле, и "код" может быть в отдельном модуле и подключаться import. ИМХО, два основных аспекта этого вопроса: "как распространять?" и "как массив изменяется?" (всегда же появляются исправления ошибок и улучшения).

"Как распространять?":

  • Если приложение состоит из одного файла "myapp.py", то задание массива прямо в нём не накладывает никаких ограничение на распространение приложения. Можно сделать как угодно просто, вплоть до простого копирования файла;
  • Размещение массива в отдельном модуле, предполагает, что приложение будет состоять из нескольких файлов. При использовании pip/Hatch/Setuptools/PyInstaller/... это не составляет неудобств, но без них это уже не так удобно;
  • Размещение массива в отдельном файле данных предоставляет большую гибкость, но требует определенных раздумий и решений.

"Как массив изменяется?":

  • Если массив является неотъемлемой частью алгоритма приложения (к примеру, таблица атомных весов, квантили χ², какие-нибудь методики из ГОСТ или ISO), т.е. изменение алгоритма приводит к изменению массива и наоборот, то варианты: "код" в своём или отдельном модуле, выглядят предпочтительнее;
  • Если массив это поправки конкретного прибора или, скажем, нормативы для групп пациентов, т.е. возможны (вероятны) изменения массива без изменения алгоритма, то вариант в отдельном файле данных выглядит более предпочтительным (как бы, зачем редактировать и плодить версии файла если сам алгоритм остаётся неизменным?).

Могут быть промежуточные варианты.

P.P.S.

содержит более 5000 значений на столбец

Не то чтобы это было большое значение, которое приведёт к критическому энергопотреблению или замедлению, но заметим, что:

  • Выражение val in a[:,0] приведёт к более, чем 2500 сравнениям, в среднем;
  • А выражение np.where(a == row) к 2500..5000 (в зависимости от реализации).