Каков наиболее эффективный способ проверить, существует ли значение в диапазоне начальных и конечных позиций в Python?

Допустим, у меня есть текстовый файл с начальной и конечной позициями, например:

Start End
  1     5
 11    14
 15    19
 23    30

Я хочу проверить, существует ли заданный набор значений между этими позициями и включая их, например. 4,14,20 вернет ИСТИНА, ИСТИНА, ЛОЖЬ.

Что было бы наиболее эффективным способом сделать это?

Идея 1) Я мог бы сгенерировать каждое возможное число в список и проверить, находится ли значение в списке - псевдокод будет выглядеть так:

list = []
values = [4,14,20]
for line in file:
    for position in range(int(line.split()[0]),int(line.split()[1])+1):
        list.append(position) #Populate list with every viable position

for value in values:
    if value in list:
        print("TRUE")
    else:
        print("FALSE")

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

list = []
for line in file:
    list.append(line) #Save only start and end into list

for value in values:
    for start_end in list:
        for position in range(int(start_end.split()[0]),int(start_end.split()[1])+1):
            if value == position:
                print("TRUE")

Если мой файл очень большой, я подозреваю, что идея 1 займет много памяти, но, с другой стороны, будет ли идея 2 выполняться намного дольше, поскольку она должна выполнять гораздо больше итераций?

Или есть какой-то совершенно другой способ, который лучше? Большое спасибо!


person reaker    schedule 09.11.2019    source источник
comment
не превращайте range в список, просто используйте объект range. Вот для чего они нужны.   -  person juanpa.arrivillaga    schedule 09.11.2019
comment
Вы уверены, что приведенный вами пример верен? не должно ли это быть Истина, Ложь, Ложь? Проверка должна производиться построчно или это какой-то другой тест?   -  person Pynchia    schedule 09.11.2019


Ответы (7)


Проверка того, что значение находится в наборе диапазонов

Нет необходимости генерировать и перебирать списки. Я подозреваю, что было бы более эффективно просто использовать операторы сравнения.

Однако, если вы хотите подтвердить, какой подход является наиболее эффективным, используйте инструменты профилирования.

Вот пример профилирования нескольких подходов к выяснению того, находится ли значение в пределах набора диапазонов, чтобы проверить, какой из подходов наиболее эффективен.

import cProfile

# Input data.
ranges = [[1, 5], [11, 14], [15, 19], [23, 30]]
values = [4, 14, 40]

# An implementation using a comparison operator (i.e. "<=").
def is_in_range_comparison_operator(v):
    for r in ranges:
        if r[0] <= v <= r[1]:
            return True
    return False

# An implementation using a range object.
def is_in_range_range_object(v):
    for r in ranges:
        if v in range(r[0], r[1]+1):
            return True
    return False

# An implementation using precomputed range objects.
range_objects = [range(r[0], r[1]+1) for r in ranges]
def is_in_range_precomputed_range_objects(v):
    for r in range_objects:
        if v in r:
            return True
    return False

# A list of the implementations, to make looping through them easier.
implementations = [
        is_in_range_comparison_operator,
        is_in_range_range_object,
        is_in_range_precomputed_range_objects,
        ]

# A function to execute an implementation and print output.
def is_in_range(is_in_range_func):
    print("Using {}:".format(is_in_range_func.func_name))
    for v in values:
        if is_in_range_func(v):
            print ("True")
        else:
            print ("False")
    print

# Run each implementation, printing out the results.
for f in implementations:
    is_in_range(f)

# A function for executing a implementation repeatedly, for profiling purposes.
def test_is_in_range(is_in_range_func, num_iterations):
    for _ in range(num_iterations):
        for v in values:
            if is_in_range_func(v):
               pass

# Profile each implementation by running it num_iterations times.
num_iterations = 100000
for f in implementations:
    command = "test_is_in_range({}, {})".format(
            f.func_name, num_iterations)
    print("Profiling the following command: {}".format(command))
    cProfile.run(command)

А вот результат выполнения скрипта:

$ python in_range.py
Using is_in_range_comparison_operator:
True
True
False

Using is_in_range_range_object:
True
True
False

Using is_in_range_precomputed_range_objects:
True
True
False

Profiling the following command: test_is_in_range(is_in_range_comparison_operator, 100000)
         300004 function calls in 0.388 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.388    0.388 <string>:1(<module>)
        1    0.172    0.172    0.388    0.388 in_range.py:51(test_is_in_range)
   300000    0.212    0.000    0.212    0.000 in_range.py:8(is_in_range_comparison_operator)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.004    0.004    0.004    0.004 {range}


Profiling the following command: test_is_in_range(is_in_range_range_object, 100000)
         1000004 function calls in 1.209 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    1.209    1.209 <string>:1(<module>)
   300000    0.639    0.000    1.033    0.000 in_range.py:15(is_in_range_range_object)
        1    0.174    0.174    1.209    1.209 in_range.py:51(test_is_in_range)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
   700001    0.396    0.000    0.396    0.000 {range}


Profiling the following command: test_is_in_range(is_in_range_precomputed_range_objects, 100000)
         300004 function calls in 0.391 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.391    0.391 <string>:1(<module>)
   300000    0.220    0.000    0.220    0.000 in_range.py:23(is_in_range_precomputed_range_objects)
        1    0.171    0.171    0.391    0.391 in_range.py:51(test_is_in_range)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.001    0.001    0.001    0.001 {range}

Некоторые выводы по результатам профилирования:

  • Использование операторов сравнения более эффективно, чем многократное создание экземпляров объектов диапазона.
  • Использование предварительно вычисленных объектов диапазона равнозначно использованию операторов сравнения.

Однако, если у вас есть огромное количество диапазонов для обработки, вероятно, будет более эффективно отказаться от использования объектов диапазона. Что приводит к...


Эффективная обработка произвольно большого набора диапазонов

Если ваш входной набор диапазонов достаточно велик, и вы беспокоитесь об исчерпании памяти, вот подход, который обрабатывает каждый диапазон по одному, проверяя все значения на соответствие этому диапазону.

Заметки:

  • Подход заключается в обработке каждого диапазона по одному, сравнивая все значения с ним. Это позволяет обрабатывать произвольное количество диапазонов, не загружая их все заранее.
  • Это не учитывает произвольное количество значений для поиска.
  • Есть некоторые оптимизации, основанные как на входных диапазонах, так и на сортируемых значениях. Если нельзя полагаться на отсортированный ввод, оптимизация может быть соответствующим образом изменена или удалена.
ranges = [[4, 5], [11, 14], [15, 19], [23, 30]]
values = [4, 14, 20]

# Create a dictionary to keep track whether or not each value falls within
# a range, with a default value of False.
is_value_in_range = {}

def init_results():
    global is_value_in_range
    is_value_in_range = {v: False for v in values}

def print_values_and_results():
    for v in values:
        print("{}: {}".format(v, is_value_in_range[v]))

def check_for_values_in_range(r, values_index):
    for i, v in enumerate(values[values_index:]):

        # If the value is greater than the upper end of the range, move on
        # to the next range.
        if v > r[1]:
            return (values_index + i, True)

        # If the value is less than the lower end and we're on the last
        # value, stop altogether.
        if v < r[0] and values_index == len(values) - 1:
            return (0, False)

        if r[0] <= v <= r[1]:
            is_value_in_range[v] = True

    return (values_index, True)

def check_for_values_in_ranges(verbose=False):
    init_results()
    if verbose:
        print("Initial results:")
        print_values_and_results()
        print
    i = 0
    for r in ranges:
        i, continue_searching = check_for_values_in_range(r, i)
        if verbose:
            print("After checking range: {}".format(r))
            print_values_and_results()
            print
        if not continue_searching:
            break

    print("Final results:")
    print_values_and_results()
    print

print("*** Check ranges for values (non-verbose) ***")
check_for_values_in_ranges()

print("*** Check ranges for values (verbose) ***")
check_for_values_in_ranges(True)

Вывод скрипта:

$ python large_input.py
*** Check ranges for values (non-verbose) ***
Final results:
4: True
14: True
20: False

*** Check ranges for values (verbose) ***
Initial results:
4: False
14: False
20: False

After checking range: [4, 5]
4: True
14: False
20: False

After checking range: [11, 14]
4: True
14: True
20: False

After checking range: [15, 19]
4: True
14: True
20: False

After checking range: [23, 30]
4: True
14: True
20: False

Final results:
4: True
14: True
20: False
person chuckx    schedule 09.11.2019
comment
Профилирование — хорошая идея. Но вы идете по вертикали, сверяя каждое значение с каждым диапазоном (т.е. каждая строка в файле загружается в память, но файл может быть большим). 2. Я думаю оператор не зависит от итерации по диапазонам. Вынесите итерацию по диапазонам из него в основной цикл и позвольте оператору выполнять свою работу по одному диапазону за раз - person Pynchia; 09.11.2019
comment
Если основной задачей вопроса является механика обработки большого файла, ее также можно изучить. Однако в обоих подходах, представленных в вопросе, в качестве первого шага принимается полное содержимое файла. В центре моего ответа была оптимизация логики проверки диапазона. - person chuckx; 09.11.2019
comment
ОП явно запрашивает наиболее эффективный способ решения проблемы. Проблема указывает диапазон значений в файле и влечет за собой большие файлы. Я думаю, что ваш подход к решению проблемы хороший и профессиональный. Просто идите по диапазону, а не по значению, поскольку ввод-вывод перевешивает любой выигрыш в обработке, а большие наборы данных влияют на проблему (часто они и являются проблемой). - person Pynchia; 09.11.2019
comment
Примечание: переход по строке (т. е. по диапазону) позволяет вам вычислить range() только один раз и использовать его для всех значений. Это ускоряет работу - person Pynchia; 09.11.2019
comment
Неплохо подмечено. Теперь мне нужно решить, на каком аспекте сосредоточиться... пересмотре уже обсуждавшегося подхода или изучении подходов к обработке большого входного набора диапазонов. - person chuckx; 09.11.2019
comment
@Pynchia Спасибо за толчок, исследовать это проблемное пространство было весело. - person chuckx; 09.11.2019

Я не знаю, может я что-то неправильно понял, но почему бы не попробовать что-то подобное? Это проще и работает.

limits = {"a":[1,5],"b":[11,14],"c":[15,19],"d":[23,30]}
values = [3,14,20]
for var in limits:
    for a in values:
        if a in range(limits[var][0],limits[var][1]+1):
            print ("TRUE")
        else:
            print ("FALSE")

выход:

ИСТИНА, ЛОЖЬ, ЛОЖЬ, / ЛОЖЬ, ИСТИНА, ЛОЖЬ, / ЛОЖЬ, ЛОЖЬ, ЛОЖЬ / ЛОЖЬ, ЛОЖЬ, ЛОЖЬ

person Lin1_k    schedule 09.11.2019
comment
не могли бы вы использовать входные значения OP и проверить правильность вывода? Спасибо - person Pynchia; 09.11.2019
comment
Кроме того, пожалуйста, сделайте так, чтобы код работал с входным файлом, как это описано - person Pynchia; 09.11.2019

Можно читать из большого файла построчно после создания ссылки на него с помощью open. Для краткого примера:

values = [4, 14, 20]
with open("some_file.txt", "r") as f:
    for test_val in values:
        min_val, max_val = f.readline().split()
        print(test_val >= int(min_val) and test_val <= int(max_val))

Это распечатает:

True
True
False

Open создает итератор, который хранит последнюю доступную часть файла, на который ссылается. В этом примере readline использует этот итератор как своего рода «закладку данных». При первом вызове readline тянет с начала файла до тех пор, пока не будет найден разрыв строки, а затем устанавливает закладку после этого разрыва строки. Каждый последующий вызов readline после первого начинается с того места, где закладка была помещена в последний раз, до следующего найденного разрыва строки. Как только найден следующий найденный разрыв строки, закладка перемещается туда.

person nBurn    schedule 09.11.2019
comment
Вам не нужен собор, чтобы молиться. open уже возвращает итератор, не нужно снова уступать, просто чтобы извлечь пределы диапазона. Опрятнее да, но тут проблема в наиболее эффективном способе выполнения задачи. Это влечет за собой чтение из файла и проверку того, попадают ли значения в каждый диапазон. Пожалуйста, добавьте вторую половину - person Pynchia; 09.11.2019
comment
Ах, хороший момент. Это то, что я получаю, когда думаю об обратном, начиная с оператора yield вместо того, чтобы начинать с open. Я изменю свой ответ. - person nBurn; 09.11.2019

Мое решение простое и лаконичное, но оно позволяет обрабатывать большие файлы и не требует индексации/перечисления:

vals = (4,14,20)
with open('rangefile.txt') as f:
    next(f)  # skip header
    for line in f:
        start, end = map(int, line.split())
        matches = [start <= v <= end for v in vals]
        print(matches)

который с входным файлом

Start End
  1     5
 11    14
 15    19
 23    30

он производит

[True, False, False]
[False, True, False]
[False, False, False]
[False, False, False]
person Pynchia    schedule 09.11.2019

Я думаю, вам также не нужен диапазон

Вам нужно просто проверить < и >:

listRange=[[ 1     ,5],[ 11 ,   14],[ 15  ,  19],[ 23  ,  30]]
value =[4,14,20]
for i , t in enumerate(value,start=0): 
  if t>=listRange[i][0] and t<=listRange[i][1]:
    print ('TRUE')
  else:
    print ('FALSE')
person Divyesh patel    schedule 09.11.2019
comment
1. Индексация абсолютно не нужна. 2. Пожалуйста, заставьте код работать с входным файлом, как это описано - person Pynchia; 09.11.2019
comment
@Pynchia Индексирование абсолютно не нужно о, я скучаю по этому - person Divyesh patel; 09.11.2019
comment
И ваш код предполагает, что все строки находятся в памяти, что неверно по ряду причин. ОП говорит, что файл может быть очень большим, поэтому ваш подход неверен. - person Pynchia; 09.11.2019
comment
@Pynchia пожалуйста, заставьте код работать с входным файлом, как это описано Как я и думал, здесь мы можем предоставить краткую идею для решения проблемы, а не полное решение - person Divyesh patel; 09.11.2019
comment
Это не все решение. Речь о том, что у файлов есть свои плюсы и минусы. Особенно крупные - person Pynchia; 09.11.2019

Оптимизация вашей «идеи 2»:

  • анализировать ввод только один раз; под синтаксическим анализом я имею в виду разделение строк и преобразование строк в целые числа
  • использовать простое и быстрое сравнение start <= value <= end, не требуется range объектов
  • закорачивать оценку, т.е. останавливаться после того, как будет найден первый совпадающий интервал
  • а также: избегайте list в качестве имени переменной

Результат часть1:

selist = []
with open(filename) as file:
    for line in file:
        n1, n2 = line.split()
        selist.append((int(n1), int(n2))) #Save only start and end into list

и часть2:

for value in values:
    result = any(start <= value <= end for start, end in selist)
    print(result)   # True or False

Дальнейшая оптимизация возможна, если диапазоны не пересекаются и их много. Затем вы можете отсортировать их и использовать деление пополам.

person VPfB    schedule 09.11.2019
comment
Не стесняйтесь отмечать, если вы видите что-то неправильное, бесполезное или вводящее в заблуждение в моем ответе, но, пожалуйста, оставьте комментарий. - person VPfB; 10.11.2019

Пример использования DataFrame:

import pandas as pd
df = pd.DataFrame({'Start': [1, 11, 15, 23], 'End': [5, 14, 19, 30]})
your_list = [4, 14, 20]

for x in your_list:
    print([item[0] <= x <= item[1] for item in df.itertuples(index=False)])
> [True, False, False, False]
> [False, True, False, False]
> [False, False, False, False]
person igorkf    schedule 09.11.2019
comment
зачем вообще привлекать сюда панд, если вы не собираетесь этим пользоваться? - person juanpa.arrivillaga; 09.11.2019
comment
Почему я получил -1? - person igorkf; 09.11.2019