Pyhonic use of if statements for inequalities applied to chunked list

Viewed 145

Given the following lists in Python:

l1 = [0,1000,5000,10000,20000,30000,40000,50000] #8 values, 7 intervals
v1 = [1,2,3,4,5,6,7] #7 values
v2 = [a,b,c,d,e,f,g] #7 letters

I want to check whether a number is contained in any interval, and return another value based on which interval this is.

Example:

1. My test value is 1111
2. It belongs to the second interval: 1000 < 1111 < 5000
3. Hence I need to return b

I would address the problem by:

  1. Creating chunks of l1
  2. Iterating over each chunk
  3. Writing one if statement for each chunk
  4. Return the letter corresponding to the correct chunk

I can create chunks of it by looking at every consecutive pair of numbers:

def chunker(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq)))

for group in chunker(l1, 2):
   print(group)

This returns:

[0, 1000]
[1000, 5000]
[5000, 10000]
[10000, 20000]
[20000, 30000]
[30000, 40000]
[40000, 50000]
[50000]

My questions:

  1. Is there a Pythonic way of writing those if statements instead of having one for each chunk? What if I have 1,000 chunks?
  2. If there is, how to deal with the last chunk without creating a specific case for it?
6 Answers

You can zip list l1 and l1[1:] which will make the intervals in a fashion that you need. And for some extra pointers you can incorporate binary search into this since the intervals are sorted, that I'll leave for you to optimise. The current runtime of this algorithm is O(n) you can reduce the runtime to O(log(n)).

from __future__ import print_function
l1 = [0,1000,5000,10000,20000,30000,40000,50000]
v2 = ['a','b','c','d','e','f','g']
element = 1111
interval = []
letter = None
for index,(left,right) in enumerate(zip(l1,l1[1:])):
    if left <= element <= right:
        interval = [left,right]
        letter = index
        break
if letter:
    print("answer is ",v2[letter])
    print("interval is" ,interval)
else:
    print("No interval found")

EDIT

I have added a algorithm using the bisect module in python it produces the same output but uses binary search hence it should be faster and runs in O(log(n))

from __future__ import print_function
import bisect
li = [0,1000,5000,10000,20000,30000,40000,50000]
v2 = ['a','b','c','d','e','f','g']
element = 1111
index = bisect.bisect(li, element)
if index == 0 or index == len(li):
    print("No interval found")
else:
    print("answer is",v2[index - 1])
    print("interval is",li[index - 1], li[index])

OUTPUT

answer is  b
interval is [1000, 5000]

You can just use a generator to get the result:

l1 = [0,1000,5000,10000,20000,30000,40000,50000]
v2 = ['a','b','c','d','e','f','g']

value = 1111

result = next((v for v, i, j in zip(v2, l1[:-1], l1[1:]) if value in range(i, j)), None)

Output:

>>> result
'b'

By adding a default of None as second argument of next(), you can handle cases when not found as well:

value = -50

result = next((v for v, i, j in zip(v2, l1[:-1], l1[1:]) if value in range(i, j)), None)

>>> result
# None

If you just wanted the index, you can use enumerate instead:

result = next((v for v, (i, j) in enumerate(zip(l1[:-1], l1[1:])) if value in range(i, j)), None)

>>> result
# 1

Explanation:

The result line consists of a few parts:

next(iterator [, default])

This is a function to retrieve the next item in the iterator passed in the argument. The default serves to return a default value if a StopIteration is encountered. The iterator in question is the generator here (broken down for clarity):

(
  v                                  # point 4
    for v, i, j                      # point 2
      in zip(v2, l1[:-1], l1[1:])    # point 1
    if value in range(i, j)          # point 3
)
  1. The zip function collates the lists that were passed, so that v2[0], l1[:-1][0] and l1[1:][0] forms into a tuple ('a', 0, 1000), and so on for each index.

  2. The for v, i, j serves to extract the elements within the tuple.

  3. The if value in range(i, j) serves to check that 1111 is within the range between range(0, 1000).

  4. If it's a match, v is returned. If not, continue to the next iteration.

Straight from the itertools recipe book:

def get_thing(value):

    def pairwise(iterable):
        from itertools import tee
        a, b = tee(iterable)
        next(b, None)
        return zip(a, b)

    interval_ranges = [
        0,
        100,
        500,
        1000
    ]

    # There are four interval ranges, so three intervals.
    things = [
        "A", # 0-100
        "B", # 100-500
        "C" # 500-1000
    ]

    for (begin, end), thing in zip(pairwise(interval_ranges), things):
        if begin <= value < end: # modify this to suit your needs. Is the range inclusive/exclusive?
            return thing
    return None


def main():

    thing = get_thing(400)
    print(thing)

    return 0


if __name__ == "__main__":
    import sys
    sys.exit(main())

Output:

B

I believe you need to get the corresponding interval index and use that to query v2. This should do it:

l1 = [0,1000,5000,10000,20000,30000,40000,50000] #8 values, 7 intervals
v2 = ['a','b','c','d','e','f','g'] #7 letters


def intervals(l):
    for i in range(len(l)-1):
        yield i, l[i:i+2]


def interval_value(val, interval_list, value_list):
    for i, interval in intervals(interval_list):
        if interval[0] <= val <= interval[1]:
            return value_list[i]


print(interval_value(1111, l1, v2))
print(interval_value(0, l1, v2))
print(interval_value(51000, l1, v2))
print(interval_value(40000, l1, v2))

Output:

b
a
None
f

You don't need the v1 values - you can work with the index on v2 directly

Though other answer is useful but almost all of them is finding interval not desire output from v2

l1 = [0,1000,5000,10000,20000,30000,40000,50000]
v2 = ['a','b','c','d','e','f','g']
element = 1111
def get_interval(l1):
    for index, left, right in zip(range(len(l1)), l1, l1[1:]):
        if left <= element <= right:
            return v2[index]

>>> print("answer is:" ,get_interval(element))
>>> answer is: b

This works only with chunks of len 2.

l1 = [0,1000,5000,10000,20000,30000,40000,50000] #8 values, 7 intervals
# v1 = [1,2,3,4,5,6,7] #7 values
v2 = ['a', 'b', 'c', 'd', 'e', 'f', 'g']  # 7 letters

def chunker(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq)))

def chunk_with_value(list_of_chunks, value):
    """returns the chunk if the value is inside the range"""
    for chunk in list_of_chunks:
        if chunk[0] < value < chunk[1]:
            return chunk

def chunk_to_letter(value_list, letter_list, chunk):
    """returns the letter based on the index of the first chunk element"""
    for i, value in enumerate(l1):
        if value == chunk[0]:
            return v2[i]


chunks = chunker(l1, 2)
chunk = chunk_with_value(chunks, 1111)
print(chunk)  # [1000, 5000]
print(chunk_to_letter(l1, v2, chunk))  # b
Related