0

Using randint() how do I give lower values a higher weight (higher chance to be picked)?

I have the following code:

def grab_int():
    current = randint(0,10) 
    # I want the weight of the numbers to go down the higher they get (so I will get more 0s than 1s, more 1s than 2, etc)

OBS!

I would like to do this in a more elegant fashion than found in some other answers, (such as: Python Weighted Random). Is there a way to do this by perhaps importing some kind of weight module?

Specification: I would like to have a randint() where the chance of it returning 0 is 30% and then it linearly deteriorates down to 10 being a 1% chance.

quamrana
  • 37,849
  • 12
  • 53
  • 71
Divide05
  • 27
  • 5
  • Does this answer your question? [Generating random natural numbers with higher probability for lower numbers?](https://stackoverflow.com/questions/66941506/generating-random-natural-numbers-with-higher-probability-for-lower-numbers) – Peter O. Apr 23 '21 at 07:53
  • Does this answer your question? [Python Weighted Random](https://stackoverflow.com/questions/14992521/python-weighted-random) – quamrana Apr 23 '21 at 07:54
  • Maybe [`random.choices`](https://docs.python.org/3/library/random.html#random.choices) is the way to go. – Matthias Apr 23 '21 at 07:54
  • @PeterO. Not in a way that I can wrap my head around, no. – Divide05 Apr 23 '21 at 07:55
  • @quamrana I looked at that before posting, but it seems very botched, so I was wondering if there was a more elegant way of doing it? – Divide05 Apr 23 '21 at 07:56
  • @Matthias I was thinking of using that, but I can't seems to wrap my head around using it in a more elegant fashion than the seemingly botched solution in this thread: https://stackoverflow.com/questions/14992521/python-weighted-random – Divide05 Apr 23 '21 at 07:58
  • You should edit your question to explain how the existing questions don't help you. Isn't the solution of generating the minimum of multiple `randint` numbers in the same range "elegant" enough? – Peter O. Apr 23 '21 at 07:58
  • @PeterO. Not really, no. I would like to do it using some form of `module` or ideally a `weight` function. It seems to be a lot harder to customize weights using that method. (at least if I'm not miss understanding you). – Divide05 Apr 23 '21 at 08:05
  • A weight _function_, not just an array of weights? – Peter O. Apr 23 '21 at 08:11
  • @PeterO. Is there no way to say something along the lines of: ``` randint(0,10)[weight: 30 - 10] ``` So there is a 30% chance for it to be 0, and a 10% chance for 10. Then it calculates the avrages to spread them out equally over the rest of the numbers (1,2,3,4,5,6,7,8,9)? – Divide05 Apr 23 '21 at 08:24
  • @PeterO. I edited the question a bit to further explain exactly what I wish to accomplish. – Divide05 Apr 23 '21 at 08:33
  • In your previous comment's example, though, the weights would be (30, 6.66, 6.66, ..., 6.66, 10), which sum to 100 percent, rather than what you intend, which is (30, 28, 26, ... 12, 10), which don't sum to 100 percent. – Peter O. Apr 23 '21 at 08:36
  • @PeterO. Even though my math was off, you clearly understand what I want to accomplish. Is there a way to do that? – Divide05 Apr 23 '21 at 08:40
  • I am working on an answer. – Peter O. Apr 23 '21 at 08:41

2 Answers2

1

The following method satisfies your requirements. It uses the rejection sampling approach: Generate an integer uniformly at random, and accept it with probability proportional to its weight. If the number isn't accepted, we reject it and try again (see also this answer of mine).

import random

def weighted_random(mn, mx, mnweight, mxweight):
 while True:
    # Get the highest weight.
    highestweight=max(mnweight,mxweight)
    # Generate a uniform random integer in the interval [mn, mx].
    r=random.randint(mn,mx)
    # Calculate the weight for this integer.  This ensures the min's
    # weight is mnweight and the max's weight is mxweight
    weight=mnweight+(mxweight-mnweight)*((i-mn)/(mx-mn))
    # Generate a random value between 0 and the highest weight
    v=random.random()*highestweight
    # Is it less than this weight?
    if v<weight:
       # Yes, so return it
       return r
    # No, so try again

(Admittedly due to floating-point division as well as random.random(), which outputs floating-point numbers, the implementation is not exactly "elegant", but the example below is, once we've written it. The implementation could be improved by the use of Fractions in the fractions module.)

This method can also be implemented using the existing random.choices method in Python as follows. First we calculate the required weights for random.choices, then we pass those weights. However, this approach is not exactly efficient if the range between the minimum and maximum is very high.

import random

# Calculate weights for `random.choices`
def make_weights(mn, mx, mnweight, mxweight):
 r=(mx-mn)
 return [mnweight+(mxweight-mnweight)*(i/r) for i in range(mn, mx+1)]

def weighted_random(mn, mx, mnweight, mxweight):
 weights=make_weights(mn, mx, mnweight, mxweight)
 return random.choices(range(mn, mx+1), weights=weights)[0]

With the NumPy library this can even be implemented as follows:

import random
import numpy

def weighted_random(mn, mx, mnweight, mxweight):
 return random.choices(range(mn, mx+1), \
      weights=numpy.linspace(mnweight,mxweight,(mx-mn)+1))[0]

An example of the weighted_random function follows:

# Generate 100 random integers in the interval [0, 10],
# where 0 is assigned the weight 30 and 10 is assigned the
# weight 10 and numbers in between are assigned
# decreasing weights.
print([weighted_random(0,10,30,10) for i in range(100)])
Peter O.
  • 32,158
  • 14
  • 82
  • 96
-1

The easiest way I found was to just manually assign weights:

def grab_int():
    global percent
    global percentLeft
    global upby

    # I want the weight of the numbers to go down the higher they get (so I will get more 0s than 1s, more 1s than 2, etc)
    current = randint(0,100)
    if current < 30:
        upby = randint(1,2)
        #0
    elif current < 40:
        upby = 1
        #1
    elif current < 45:
        upby = 2
        #2
    elif current < 50:
        upby = 3
        #3
        upby = 4
        #4
    elif current < 60:
        upby = 5
        #5
    elif current < 65:
        upby = 6
        #6
    elif current < 70:
        upby = 7
        #7
    elif current < 75:
        upby = 8
        #8
    elif current < 90:
        upby = 9
        #9
    elif current < 95:
        upby = 10
        #10
    else: # I'm dumb so I accidentally only added up to 95%, This just gives 0 a 5% higher chance without having to rewrite all the other values
        upby = 0
        #0
m4n0
  • 29,823
  • 27
  • 76
  • 89
Divide05
  • 27
  • 5
  • Can you explain why you decided to use this solution rather than mine? Is it because you care more about "giving lower values a higher weight" than about the exact distribution of weights? – Peter O. Oct 04 '21 at 19:53
  • @PeterO. Because I am a year 1 programming student and can not for the life of me wrap my head around the code example you provided. I am very greatful that you put your time into this, however I am not capable of understanding your code. So it's on me brother! :) – Divide05 Oct 15 '21 at 08:20