What is the funtional-style replacement of this loop? - python

I have the following loop:
import random
distribution = []
sum = 0
while True:
item = random.randint(1, 8)
if sum + item == 812:
distribution.append(item)
break
elif sum + item > 812:
distribution.append(812 - sum)
break
else:
distribution.append(item)
sum = sum + item
The idea here is to create a list with items where each item is a random number between 1 and 8 and the sum of items in the list has to be exactly 812. The challenge is, that the last item can randomly overshoot, so a simple stop condition won't do.
I'm racking my brain on how to express this functionally, but came up blank so far. Any ideas?

I am answering the title of this question:
What is the functional-style replacement of this loop?
def random_list(sum_rest):
item = random.randint(1, min(8, sum_rest))
return ([] if sum_rest - item == 0 else random_list(sum_rest - item)) + [item]
This solution is based on the improvement proposed by #DollarAkshay, once you get that, it is very straightforward: I am recursively calling random_list, but decreasing the sum_rest parameter by the item randomly chosen.
You get the list by calling
random_list(812)

A disciplined (and practical) approach is to generate sequences, stopping when the sum is at least 812, and then restarting if the sum is above 812. This is called "rejection sampling". This guarantees that you are picking sequences fairly, and not skewing the randomness.
You expect approximately 1 in 8 of the samples to be good, so you expect to generate 8 samples before you find a good one.
import random
def seqsum(n):
while True:
S = 0
s = []
while S < n:
s.append(random.randrange(8)+1)
S += s[-1]
if S == n:
return s
for _ in range(10):
print(seqsum(812))
Obviously this will be slower than skewing the last element or elements of the sequence to force the sum, but that's a trade-off for avoiding the skew.
Here's some code that prints out the distribution of the last element of the sequence, using this method, that of #hiro-protagonist and that of #DollarAkshay. It shows the frequencies of the numbers 1 to 8 in a sample of size 10000, printed as an array. You can see the heavy skew towards low numbers in those methods.
def seqhiro(n):
S = 0
s = []
while S < n:
s.append(min(812 - S, random.randint(1, 8)))
S += s[-1]
return s
def seqdollar(n):
S = 0
s = []
while S < n:
s.append(random.randint(1, min(8, 812 - S)))
S += s[-1]
return s
def sample(m):
last = [0] * 9
for _ in range(10000):
s = m(812)
last[s[-1]] += 1
return last[1:]
print('rejection', sample(seqsum))
print('hiro', sample(seqhiro))
print('dollar', sample(seqdollar))
Output:
rejection [1234, 1308, 1280, 1178, 1246, 1247, 1257, 1250]
hiro [2226, 1904, 1727, 1319, 1077, 895, 560, 292]
dollar [5220, 1803, 938, 595, 475, 393, 319, 257]
Dollar Akshay's method produces 1 as the final number approximately 20 times more than 8, and hiro's method is better, but still 1 appears approximately 7.5 times more often than 8 as the final entry.
The method of this answer (rejection sampling) gives an approximately equal distribution for all final digits in this sample of 10000 runs.
Obviously it's possible to hide the skew of the fast methods, for example by shuffling the array before returning it, but the statistically correct method in this answer has nothing to hide.

One solution is to generate random numbers such that they do not cross 812. For example when your sum is 806 you can only generate numbers between [1, 6].
So you should change your random number generator to be
item = random.randint(1, min(8, 812 - sum))
Also your entire program becomes much shorter. Note do not use the variable name sum as it is an inbuilt function
import random
distribution = []
list_sum = 0
while list_sum < 812:
item = random.randint(1, min(8, 812 - list_sum ))
distribution.append(item)
list_sum += item

this is exactly what your if statements do - just rewritten with min:
MAX = 812
distribution = []
s = 0
while s < MAX:
item = min(MAX - s, random.randint(1, 8))
distribution.append(item)
s += item
note that this will slightly skew your distribution. the last number will be consistently smaller than the others... one way to hide that a little would be to shuffle the list afterwards.
in order to have a really uniform distribution you'd have to start over in case you overshoot...

An answer that's similar to yours
import random
distribution = []
lsum = 0
while True:
item = random.randint(1, 8)
lsum = lsum + item
distribution.append(item)
if lsum > 812:
lsum = lsum - item
distribution.pop()
if lsum == 812:
break

Because you know the max value of items added, could you not just deterministically end with the last item as soon as you are within range of this max value at the end? In that case, you could do something like:
import random
def creat_random_list(total_sum, min_value=1, max_value=8):
distribution = []
current_sum = 0
done = False
while not done:
if total_sum - current_sum <= max_value:
new_item = total_sum - current_sum
done = True
else:
new_item = random.randint(min_value, max_value)
distribution.append(new_item)
current_sum += new_item
return distribution
random_numers = creat_random_list(total_sum=812)
print(f"sum {sum(random_numers)} of list {random_numers}")
Note that I have renamed your variable sum to current sum, because sum is an inbuilt function, so not a good variable name to pick
Result look like:
sum 812 of list [7, 5, 7, 4, 7, 7, 7, 1, 2, 6, 6, 4, 5, 3, 1, 4, 4, 2, 8, 7, 5, 5, 5, 5, 1, 1, 1, 4, 5, 5, 1, 1, 8, 8, 6, 5, 5, 5, 8, 2, 3, 7, 8, 6, 2, 6, 7, 4, 7, 7, 8, 7, 1, 4, 7, 2, 2, 6, 4, 4, 3, 4, 2, 6, 3, 3, 3, 4, 1, 3, 6, 6, 8, 5, 6, 3, 3, 7, 6, 8, 5, 3, 5, 4, 1, 7, 6, 5, 4, 1, 7, 1, 5, 1, 7, 3, 4, 2, 3, 3, 1, 4, 6, 5, 4, 1, 1, 1, 3, 7, 1, 3, 8, 8, 7, 4, 4, 8, 8, 8, 5, 6, 5, 6, 8, 5, 7, 2, 2, 8, 5, 1, 5, 4, 3, 2, 1, 1, 8, 8, 8, 8, 2, 1, 1, 4, 8, 4, 1, 2, 2, 2, 8, 5, 6, 5, 4, 1, 3, 4, 3, 3, 2, 2, 5, 7, 8, 1, 1, 8, 2, 7, 7, 2, 5, 1, 7, 7, 2, 3, 7, 7]

Related

Python - How to find the longest sequence in a integer list of neighbor numbers

How I find the longest sub-list in list of number pairs that has the difference of 1.
So "number neighbors" could be element 1 and 2 or element 7 and 6.
If the list is [7, 1, 2, 5, 7, 6, 5, 6, 3, 4, 2, 1, 0],
Then the desired output should be 4. The sub-list would be then [7, 6, 5, 6].
This is what I have for now. The for loop formula is really broken and I don't know how to solve this.
list = [7, 1, 2, 5, 7, 6, 5, 6, 3, 4, 2, 1, 0]
sublist = []
for i in list:
if list[i] - list[i+1] == 1 or list[i] - list[i+1] == -1:
sublist.append(i)
print(sublist)
print(len(sublist))
more-itertools makes it simple:
from more_itertools import split_when
lst = [7, 1, 2, 5, 7, 6, 5, 6, 3, 4, 2, 1, 0]
print(max(split_when(lst, lambda x, y: abs(x - y) != 1), key=len))
Its best to break these types of problems up into their parts
the main problem here is to get all sequential sequences
def get_sequences_iter(an_array):
# start a new sequence with first value (or empty)
sequence = an_array[:1]
# check each index with the value that comes before
for idx in range(1,len(an_array)):
if an_array[idx] - an_array[idx-1] in {1,-1}:
# this is part of a run append it
sequence.append(an_array[idx])
else:
# run is broken
yield sequence
# start a new run
sequence = [an_array[idx]]
#capture final sequence
yield sequence
once you have this you can get all the runs in a list in O(n) time
sequences = get_sequences_iter([7, 1, 2, 5, 7, 6, 5, 6, 3, 4, 2, 1, 0])
for sequence in sequences:
print(sequence)
hopefully with this information you can figure out how to solve your actual question
but just in case
print(max(sequences,key=len))

Fast python algorithm to find all possible partitions from a list of numbers that has subset sums equal to given ratios

Say I have a list of 20 random integers from 0 to 9. I want to divide the list into N subsets so that the ratio of subset sums equal to given values, and I want to find all possible partitions. I wrote the following code and got it work for the N = 2 case.
import random
import itertools
#lst = [random.randrange(10) for _ in range(20)]
lst = [2, 0, 1, 7, 2, 4, 9, 7, 6, 0, 5, 4, 7, 4, 5, 0, 4, 5, 2, 3]
def partition_sum_with_ratio(numbers, ratios):
target1 = round(int(sum(numbers) * ratios[0] / (ratios[0] + ratios[1])))
target2 = sum(numbers) - target1
p1 = [seq for i in range(len(numbers), 0, -1) for seq in
itertools.combinations(numbers, i) if sum(seq) == target1
and sum([s for s in numbers if s not in seq]) == target2]
p2 = [tuple(n for n in numbers if n not in seq) for seq in p1]
return list(zip(p1, p2))
partitions = partition_sum_with_ratios(lst, ratios=[4, 3])
print(partitions[0])
Output:
((2, 0, 1, 2, 4, 6, 0, 5, 4, 4, 5, 0, 4, 5, 2), (7, 9, 7, 7, 3))
If you calculate the sum of each subset, you will find the ratio is 44 : 33 = 4 : 3, which are exactly the input values. However, I want the function to work for any number of subsets. For example, I expect
partition_sum_with_ratio(lst, ratios=[4, 3, 3])
to return something like
((2, 0, 1, 2, 4, 6, 0, 5, 4, 4, 3), (5, 0, 4, 5, 2, 7), (9, 7, 7))
I have been thinking about this problem for a month and I found this to be extremely hard. My conclusion is that this problem can only be solved by a recursion. I would like to know if there are any relatively fast algorithm for this. Any suggestions?
Yes, recursion is called for. The basic logic is to do a bipartition into one part and the rest and then recursively split the rest in all possible ways. I've followed your lead in assuming that everything is distinguishable, which creates a lot of possibilities, possibly too many to enumerate. Nevertheless:
import itertools
def totals_from_ratios(sum_numbers, ratios):
sum_ratios = sum(ratios)
totals = [(sum_numbers * ratio) // sum_ratios for ratio in ratios]
residues = [(sum_numbers * ratio) % sum_ratios for ratio in ratios]
for i in sorted(
range(len(ratios)), key=lambda i: residues[i] * ratios[i], reverse=True
)[: sum_numbers - sum(totals)]:
totals[i] += 1
return totals
def bipartitions(numbers, total):
n = len(numbers)
for k in range(n + 1):
for combo in itertools.combinations(range(n), k):
if sum(numbers[i] for i in combo) == total:
set_combo = set(combo)
yield sorted(numbers[i] for i in combo), sorted(
numbers[i] for i in range(n) if i not in set_combo
)
def partitions_into_totals(numbers, totals):
assert totals
if len(totals) == 1:
yield [numbers]
else:
for first, remaining_numbers in bipartitions(numbers, totals[0]):
for rest in partitions_into_totals(remaining_numbers, totals[1:]):
yield [first] + rest
def partitions_into_ratios(numbers, ratios):
totals = totals_from_ratios(sum(numbers), ratios)
yield from partitions_into_totals(numbers, totals)
lst = [2, 0, 1, 7, 2, 4, 9, 7, 6, 0, 5, 4, 7, 4, 5, 0, 4, 5, 2, 3]
for part in partitions_into_ratios(lst, [4, 3, 3]):
print(part)

Replace values in a list if condition is true

how can I replace the numbers that are greater than 9 by their sum of digits?
right now the list multipliedlist =
[1, 4, 3, 8, 5, 12, 7, 16, 2]
I need to change it to (ex, num 12 and num 16 replaced to (3) and (7) )
[1, 4, 3, 8, 5, 3, 7, 7, 2]
I can use sum(map(int, str(number))) to add the digits but how can i change the values in the same list by their index?
def check_id_valid(id_number):
updatedid = map(int, str(id_number))
multipliedlist = [i * 1 if j % 2 == 0 else i * 2 for j, i in enumerate(updatedid)]
# for index, number in enumerate(multipliedlist):
# if multipliedlist[index] > 9:
# multipliedlist[index] = sum(map(int, str(number)))
# else:
# multipliedlist[index] == number #statement has no effect error.
print(check_id_valid(123456782))
New to python sorry if this is not explained as it's supposed to be
I appreciate any help,Thanks :)
Using a list comprehension
Ex:
data = [1, 4, 3, 8, 5, 12, 7, 16, 2]
print([sum(map(int, str(i))) if i > 9 else i for i in data])
Output:
[1, 4, 3, 8, 5, 3, 7, 7, 2]
Break your task into the constituent parts, namely
replacing a number with the sum of its digits
doing that for a list of numbers.
def sum_digits(number):
# Convert the number into a string (10 -> "10"),
# iterate over its characters to convert each of them
# back to an integer, then use the `sum()` builtin for
# summing.
return sum(int(digit_char) for digit_char in str(number))
def sum_all_digits(numbers):
return [sum_digits(number) for number in numbers]
print(sum_all_digits([1, 4, 3, 8, 5, 12, 7, 16, 2]))
outputs the expected
[1, 4, 3, 8, 5, 3, 7, 7, 2]
To change values by index you can use enumerate() function:
def sum_digits(n):
r = 0
while n:
r, n = r + n % 10, n // 10
return r
multipliedlist = [1, 4, 3, 8, 5, 12, 7, 16, 2]
for i, n in enumerate(multipliedlist):
multipliedlist[i] = sum_digits(multipliedlist[i])
print(multipliedlist)
[1, 4, 3, 8, 5, 3, 7, 7, 2]

python: how to strip lines efficiently from a matrix, which have elements appearing in other lines

list = [0, 1, 2, 3, 4, 1, 5, 0, 6, 5, 7, 8, 9, 10, 11, 12, 13, 2]
list is used "matrix like"
1. 0 1 2
2. 3 4 1
3. 5 0 6
... and so on. I would like to write all those lines into a new list/matrix, but without lines, that would repeat a number. However the order of a line has to be preserved.
So far I use this:
compa = [0,1,2,3,4,1,5,0,6,5,7,8,9,10,11,12,13,2] #the list to be used as base
temp = [0,1,2] #new list starts from the first element
temp2 = [12,13,2] #new list starts from the last element
Mischzahl = 3 #defines the number of elements in a line of the "matrix"
n = 0
while n < len(compa):
for m in range(0,len(temp)):
if temp[m] == compa[n]:
n = (int(n/Mischzahl) + 1) * Mischzahl - 1 #calculates the "foul" line and sets n to the next line
break
if (n + 1) % Mischzahl == 0 and m == len(temp) - 1 : #if the end of temp is reached, the current line is transferred to temp.
for p in range(Mischzahl):
temp.append(compa[Mischzahl*int(n/Mischzahl) + p])
n += 1
and the same backwards
n = len(compa) - 1
while n > 0: #same as above but starting from last element
for m in range(len(temp2)):
if temp2[m] == compa[n]:
n = (int(n/Mischzahl) - 1) * Mischzahl + Mischzahl
break
if (n) % Mischzahl == 0 and m == len(temp2) - 1:
for p in range(Mischzahl):
temp2.append(compa[Mischzahl*int(n/Mischzahl) + p])
n = n - 1
resulting output for temp and temp2:
[0, 1, 2, 3, 4, 1, 5, 0, 6, 5, 7, 8, 9, 10, 11, 12, 13, 2] #compa
[0, 1, 2, 5, 7, 8, 9, 10, 11] #temp
[12, 13, 2, 9, 10, 11, 5, 7, 8, 3, 4, 1] #temp2
Since this is the most time-consuming part of the script: Is there a more efficient way to do this? Any helpful advice or direction would be highly welcome.
You can define a function that iterates over the list in strides of a given length (in your case 3), checks if the elements of the stride are in a set of numbers, if not extend the out list and update the set.
from math import ceil
def unique_by_row(compa, stride_size=3, reverse=False):
strides = ceil(len(compa)/stride_size)
out = []
check = set()
it = range(strides)
if reverse:
it = reversed(it)
for i in it:
x = compa[stride_size*i:stride_size*(i+1)]
if not check.intersection(x):
out.extend(x)
check.update(x)
return out
Tests:
compa = [0, 1, 2, 3, 4, 1, 5, 0, 6, 5, 7, 8, 9, 10, 11, 12, 13, 2]
unique_by_row(compa)
# returns:
[0, 1, 2, 5, 7, 8, 9, 10, 11]
unique_by_row(compa, reverse=True)
# returns:
[12, 13, 2, 9, 10, 11, 5, 7, 8, 3, 4, 1]

Python swapping and slicing method

How do I find the last number of the list and then that number will take the top x number of card and place it at the bottom for example cards = [1, 2, 3, 4, 27, 28, 5, 6, 7, 2] and since 2 is last number it will get the first 2 card and place it at the bottom before it so it will turn to [3, 4, 27, 28, 5, 6, 7, 1, 2, 2]).
I also have the swap function but not sure how to use it since it takes a list and a index and swap the list by the index with the next number.
There is also a special case of when the [-1] value is 3 it will swap with the value of 2 but not changing the number
[1,2,3,4,5,6,1,2,3] -> [3,4,5,6,1,2,1,2,3]
def top_to_bottom(cards)
cards = cards.index(end)
def swap_me(cards, index)
if index == len(cards) - 1:
index2 = 0
else:
index2 = index + 1
cards[index], cards[index2] = cards[index2], cards[index]
If last index is valid, i.e. there are at least that much cards to move in the array then you can do this:
def move_cards(cards):
if cards[-1] == 3:
return cards[2:-1] + cards[-3:-1] + [3]
else:
return cards[cards[-1]:-1] + cards[0:cards[-1]] + [cards[-1]]
move_cards([1, 2, 3, 4, 27, 28, 5, 6, 7, 2])
[3, 4, 27, 28, 5, 6, 7, 1, 2, 2]
move_cards([1,2,3,4,5,6,1,2,3])
[3, 4, 5, 6, 1, 2, 1, 2, 3]

Categories