How to check that all list elements have a minimum difference of x

Question:

I’m currently trying to iterate through a small list of integers and update any values which fail to meet a condition of absolute difference. The aim is to use this over multiple small lists as part of a much larger for loop.

I have the following list:

y_list = [16, 29, 10]

This list needs to satisfy two conditions:

  1. No two numbers can be identical; and
  2. Each number should have a difference of at least 10

If either of these conditions are not satisfied then the number should be adjusted to have a difference of at least 10. For example:

y_list[0] is compared with y_list[1]: It meets both conditions and moves on.

y_list[0] is compared with y_list[2]: It fails to meet condition 2 and adds 10 minus the existing difference.

y_list[1] is compared with y_list[0]: This now fails to meet both conditions. But rather than adjusting y_list[0] it increases y_list[1] by 10 minus the difference.

So far I’ve written the following code, which doesn’t account for the final element of the example above. The print statements aren’t necessary, but I’ve just been using them to help me ensure that the different parts of the loop are being triggered or not:

for i in range(len(y_list)):
    print(f'Iteration {i}')
    print(f'Old y_list: {y_list}')
    for idx, value in enumerate(y_list):
        difference = abs(value - y_list[i])
        if value != y_list[i]:
            print(f'Comparing {y_list[idx]} with {y_list[i]}')
            print(f'Difference of {difference}')
            if difference < 10:
                print(f'Updating {y_list[idx]}n')
                y_list[idx] += 10 - difference
        else:
            continue
        print()
    print(f'New list{y_list}n')

This gives me an updated list, but obviously it only iterates over the entire list for three rounds.

Output:

Iteration 0
Old y_list: [16, 29, 10]
Comparing 29 with 16
Difference of 13

Comparing 10 with 16
Difference of 6
Updating 10


New list[16, 29, 14]

Iteration 1
Old y_list: [16, 29, 14]
Comparing 16 with 29
Difference of 13

Comparing 14 with 29
Difference of 15

New list[16, 29, 14]

Iteration 2
Old y_list: [16, 29, 14]
Comparing 16 with 14
Difference of 2
Updating 16


Comparing 29 with 14
Difference of 15

New list[24, 29, 14]

I’ve attempted to use a while True loop before the second for loop to continue the iteration haven’t been successful.

I’ve seen examples of meeting conditions with the all() function and itertools.takewhile() but haven’t been able get either to function with the while loop.

Any assistance very gratefully received!

EDIT

So just to come back on this and thanks for all your assistance. I found the @treuss solution to be the closest to what I was after, using pairwise for checking.

Where I differed in my solution was on the adjust function, opting to use permutations rather than pairwise. The reason for this was that I wanted to ensure that all possible combinations were assessed against each other, so that combinations could be tested after an update had been made, to ensure that it still met the necessary conditions. When using the pairwise function I experienced some overlaps that didn’t work in my final application. I also added a space parameter rather than baking in the value, just to allow for some adjustment if necessary.

This has now allowed me to loop through the production of 52 plotly charts, with trace annotations appropriately spaced. It sets y values from the final y value of each trace and adjusts the final position of the label as necessary. The final asignment statement applies it to my annotations list of dictionaries which is read by plotly.

This is the final function that I used:

def adjust(l, space):
    for (idx1,num1), (idx2,num2) in itertools.permutations(enumerate(l), 2):
        difference = abs(l[idx1] - l[idx2])
        if difference < space:
            largest = max((idx1,num1), (idx2,num2), key=lambda x:x[1])
            largest_index = largest[0]
            l[largest_index] = l[largest_index] + (space - difference)
            annotations[largest_index]['y'] = l[largest_index]
Asked By: Alex H

||

Answers:

Create a function that evaluates whether your list satisfies the condition and then use a while loop that runs until that function returns True. The trick then is to have more code-compact ways to check for uniqueness and do all comparisons. For uniqueness, we can just compare the set vs. the list and if the lengths match, all elements are unique. For comparisons, we use itertools.combinations to generate all index pairs. If we run into any failure condition, we immediately return False. If it survives the gauntlet of checks, we finally return True.

from itertools import combinations

y_list = [16, 29, 10]

def satisfies_conditions(ys):
    unique = len(set(ys)) == len(ys)
    
    if not unique:
        return False
    
    for i, j in combinations(range(len(ys)), 2):
        diff = abs(ys[i] - ys[j])
        if diff < 10:
            return False
        
    return True

while not satisfies_conditions(y_list):
    for i in range(len(y_list)):
        print(f'Iteration {i}')
        print(f'Old y_list: {y_list}')
        for idx, value in enumerate(y_list):
            difference = abs(value - y_list[i])
            if value != y_list[i]:
                print(f'Comparing {y_list[idx]} with {y_list[i]}')
                print(f'Difference of {difference}')
                if difference < 10:
                    print(f'Updating {y_list[idx]}n')
                    y_list[idx] += 10 - difference
            else:
                continue
            print()
        print(f'New list{y_list}n')
Answered By: Michael Cao

The requirements for the solution are not all clear, so I am making some additional assumptions:

  1. The lowest number in the list shall not be adjusted
  2. No number shall be adjusted downwards
  3. For equal numbers, it does not matter which number is adjusted (or in which order the numbers are adjusted if multiple adjustments are required)
  4. With the exception of 3, the order of numbers shall be retained
  5. Taking into account all the above, adjustments should be minimal. Note that possibly a lower adjustment could be achieved with ignoring rules 1 and 2, but that will make the alogrithm a lot more comples.

Given the above, the best way is to work on sorted data, i.e. first compare the lowest to the second-lowest number, then the second-lowest to the third-lowest, etc. pairwise from itertools (available from Python 3.10 on) is great for doing such iterations, with older versions of python you can use zip to create something similar. Using pairwise a check would boil down to a simple:

def check(l):
    return all(x2-x1 >= 10 for x1,x2 in itertools.pairwise(sorted(l)))

For older python versions, use zip on the sorted items:

def check2(l):
    sl = sorted(l)
    return all(x2-x1 >= 10 for x1,x2 in zip(sl, sl[1:]))

For adjusting the list, you can also use pairwise, but as you are changing the list inflight, you need to use the indexes to do any changes, enumerate gives you those:

def adjust(l):
    for (idx1,num1), (idx2,num2) in itertools.pairwise(sorted(enumerate(l), key=lambda tup: tup[1])):
        if l[idx2] < l[idx1]+10: # Note: Don't be tempted to use num1 or num2 here
            l[idx2] = l[idx1]+10
Answered By: treuss

The accumulate function from itertools can make this quite easy as it will progress through the list working the the last and current values:

from itertools import accumulate

def adjust(L,diff=10):
    return [*accumulate(L,lambda a,b:max(b,a+diff))]

print(adjust([16, 29, 10])) # [16, 29, 39]

print(adjust([16, 29, 10, 35, 60, 67])) # [16, 29, 39, 49, 60, 70]

If you’re not allowed to use libraries, you can obtain a similar result with the extend method:

def adjust(L,diff=10):
    result = L[:1]
    result.extend(max(v,result[-1]+diff) for v in L[1:])
    return result

If you need to change the values "in-place" in the same list instance, enumerate() can help with traversal and indexing:

for i,v in enumerate(y_list[1:]):
    y_list[i+1] = max(v,y_list[i]+10)

Note that I didn’t bother with condition #1 since it will never happen when condition #2 is met. Also, this approach assumes that you’re not trying to minimize the number of changes

[EDIT] Here’s an example of a process that anchors on the smallest elements working forward and backward in a way that will keep that element the smallest (i.e. minimally adjusting others around it):

y_list = [16, 29, 16, 10, 35, 60, 67]

anchor = y_list.index(min(y_list)) # anchor on smallest
for i,v in enumerate(y_list[anchor+1:],anchor):                     # forward
    y_list[i+1] = y_list[i]+10 if abs(v-y_list[i])<10 else v
for i,v in enumerate(reversed(y_list[:anchor]),len(y_list)-anchor): # backward
    y_list[-i-1] = y_list[-i]+10 if abs(v-y_list[-i])<10 else v
    
print(y_list)
# [16, 30, 20, 10, 35, 60, 70]
Answered By: Alain T.

Another take:

You can sort the input array (remembering the original indices), make sure the values have the difference at least 10 and put the values back in original order:

y_list = [16, 29, 10]

x = sorted((v, i) for i, v in enumerate(y_list))

out = [x[0]]
for v, i in x[1:]:
    how_much = v - out[-1][0]
    if how_much < 10:
        v += 10 - how_much
    out.append((v, i))

out = [v for v, _ in sorted(out, key=lambda v: v[1])]
print(out)

Prints:

[20, 30, 10]
Answered By: Andrej Kesely

If you don’t care about preserving order, then I would do this:

y_list = [16, 29, 10, 50]
y_list.sort()
for i in range(1, len(y_list)):
    y_list[i] += max(0, 10 - (y_list[i] - y_list[i-1]))
print(y_list)

giving you:

[10, 20, 30, 50]

If you do care about sorting then the answer by @andrej-kesely looks like a winner to me.

Answered By: JonSG
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.