Title: Augment random.choices() with the alias method
Type: performance Stage:
Components: Library (Lib) Versions: Python 3.10
Status: open Resolution:
Dependencies: Superseder:
Assigned To: rhettinger Nosy List: mark.dickinson, rhettinger, tim.peters
Priority: normal Keywords:

Created on 2020-06-26 20:38 by rhettinger, last changed 2020-06-27 23:42 by rhettinger.

File name Uploaded Description Edit rhettinger, 2020-06-27 08:08 Binomial variant rhettinger, 2020-06-27 23:42 Alias variant
Messages (2)
msg372441 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2020-06-26 20:38
For n unequal weights and k selections, sample selection with the inverse-cdf method is O(k logâ‚‚ n).  Using the alias method, it improves to O(k).  The proportionally constants also favor the alias method so that if the set up times were the same, the alias method would always win (even when n=2).

However, the set up times are not the same.  For the inverse-cdf method, set up is O(1) if cum_weights are given; otherwise, it is O(n) with a fast loop.  The setup time for the alias method is also O(n) but is proportionally much slower.

So, there would need to be a method selection heuristic based on the best trade-off between setup time and sample selection time.

Both methods make k calls to random().


Notes on the attached draft implementation:

* Needs to add back the error checking code.

* Need a better method selection heuristic.

* The alias table K defaults to the original index
  so that there is always a valid selection even
  if there are small rounding errors.

* The condition for the aliasing loop is designed
  to have an early-out when the remaining blocks
  all have equal weights.  Also, the loop condition
  makes sure that the pops never fail even if there
  are small rounding errors when partitioning
  oversized bins or if the sum of weights isn't
  exactly 1.0.
msg372443 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2020-06-26 20:57
I also looked at another method using binomial variates but couldn't get it to run faster than the alias method:

    def choices(population, weights, *, k=1):
        r = 1.0
        n = k
        selections = []
        for elem, p in zip(population, weights):
            v = binomial_variate(n, p / r)
            selections += [elem] * v
            n -= v
            r -= p
        return selections

The shuffle step took as much time as the alias method.  Also, the binomial variate was hard to compute quickly and without overflow/underflow issues for large inputs.
Date User Action Args
2020-06-27 23:42:12rhettingersetfiles: -
2020-06-27 23:42:01rhettingersetfiles: +
2020-06-27 08:08:02rhettingersetfiles: +
2020-06-27 08:07:43rhettingersetfiles: -
2020-06-27 03:53:32rhettingersetfiles: +
2020-06-26 20:57:17rhettingersetmessages: + msg372443
2020-06-26 20:38:41rhettingercreate