Reputation: 181
I've got a list of numbers, and I also compute all possible combinations of two of this list. What I want to do is check whether the sum of the first combination is less than 5, and if so, choose a random number out of the 2 from the combination, remove that number from the initial list and re-calculate the combinations, etc.
import itertools
import random
numbers = [1,2,3,4,5,6,7,8,9,10]
all_combos = list(itertools.combinations(numbers, 2))
for combo in all_combos:
if combo[0] + combo[1] < 5:
choice = random.choice([combo[0],combo[1]])
numbers.remove(choice)
The error I get
ValueError: list.remove(x): x not in list
Initially I thought it was the inplace changing of the list in combination with the for loop that caused this, so I also tried implementing a while loop, but it always ends up being an infinite loop since I can't seem to find the correct condition for it.
Upvotes: 1
Views: 153
Reputation: 114230
Right now, you are removing a number from numbers
but not updating all_combos
. That means the number will almost certainly show up in all_combos
again, but be missing from numbers
. You do not need to regenerate all possible combinations every time you remove a number, but you do need to filter all_combos
and adjust your position accordingly. A simple method for doing so is to maintain a list of booleans that you update every time you discard a number:
from itertools import combinations
from random import choice
numbers = list(range(1, 11))
all_combos = list(combinations(numbers, 2))
valid = [True] * len(all_combos)
for i, combo in enumerate(all_combos):
if not valid[i]: continue
if sum(combo) < 5:
c = random.choice(combo)
valid[i + 1:] = (valid[j] and c not in all_combos[j] for j in range(i + 1, len(all_combos)))
This algorithm is much cheaper than regenerating the entire list of combinations at every step since it just skips invalidated combos. You could further reduce the complexity from O(n2)
(because you revisit the entire tail of all_combos
at each step) by using a set
to keep track of discarded elements and deciding which ones to skip on the fly. This will be O(n2)
because set lookup is ostensibly O(1)
:
from itertools import combinations
from random import choice
numbers = list(range(1, 11))
all_combos = list(combinations(numbers, 2))
dropped = set()
for i, combo in enumerate(all_combos):
if any(x in dropped for x in dropped): continue
if sum(combo) < 5:
dropped.add(random.choice(combo))
Upvotes: 0
Reputation: 77337
As mentioned, you may choose the same number more than once, causing the error. You could put the numbers you want to remove in a set, which removes duplicates, and do the remove later.
import itertools
import random
numbers = [1,2,3,4,5,6,7,8,9,10]
all_combos = list(itertools.combinations(numbers, 2))
remove_this = {}
for combo in all_combos:
if combo[0] + combo[1] < 5:
choice = random.choice([combo[0],combo[1]])
remove_this.add(choice)
for choice in remove_this:
numbers.remove(choice)
That can be reduced to
remove_this = {random.choice(combo)
for combo in itertools.combinations(numbers, 2)
if sum(combo) < 5}
for choice in remove_this:
numbers.remove(choice)
print(numbers)
Upvotes: 1
Reputation: 92440
If you would like to actually recalculate the combinations as you say in the question, you need to put that itertools.combinations
call somewhere in the loop and regenerate it after mutating numbers
.
One way would be to use a while loop and only break once you've made it through the combinations without finding more pairs that meet your criteria:
import itertools
import random
numbers = [1,2,3,4,5,6,7,8,9,10]
while True:
all_combos = itertools.combinations(numbers, 2)
try:
pair = next(p for p in all_combos if sum(p) < 5)
except StopIteration:
break
choice = random.choice(pair)
numbers.remove(choice)
This will make a small difference in the statistics of the choices because removing 2
in the first iteration removes all possibility of removing 3
in the pair (2, 3)
later where simply removing it from numbers
and continuing leaves that choice in the combinations. This means your odds of having 3
left in your number list are higher than the other way of doing this.
Upvotes: 2
Reputation: 103744
Since it is random, you may end up trying to remove a number already removed.
Just wrap in a try / except
:
try:
numbers.remove(choice)
except ValueError:
pass
Or, ask permission:
if choice in numbers:
numbers.remove(choice)
Upvotes: 1