it-swarm.com.de

Wählen Sie k zufällige Elemente aus einer Liste aus, deren Elemente Gewichtungen haben

Die Auswahl ohne Gewichtungen (gleiche Wahrscheinlichkeiten) wird hier schön beschrieben.

Ich habe mich gefragt, ob es einen Weg gibt, diesen Ansatz in einen gewichteten umzuwandeln.

Ich interessiere mich auch für andere Ansätze.

Update: Sampling ohne Ersetzung

67
nimcap

Ich weiß, dass dies eine sehr alte Frage ist, aber ich denke, es gibt einen ordentlichen Trick, um dies in O(n) zu tun, wenn Sie ein wenig Mathe anwenden!

Die Exponentialverteilung hat zwei sehr nützliche Eigenschaften.

  1. Wenn n Proben von verschiedenen Exponentialverteilungen mit unterschiedlichen Ratenparametern gegeben sind, ist die Wahrscheinlichkeit, dass eine gegebene Probe das Minimum ist, ihrem Ratenparameter dividiert durch die Summe aller Ratenparameter.

  2. Es ist "erinnerungslos". Wenn Sie also bereits das Minimum kennen, ist die Wahrscheinlichkeit, dass eines der verbleibenden Elemente das 2nd-to-Min-Element ist, mit der Wahrscheinlichkeit identisch, dass das Element, wenn das wahre Minimum entfernt (und niemals generiert) wäre, das Neue gewesen wäre Mindest. Dies erscheint offensichtlich, aber ich denke, aufgrund einiger bedingter Wahrscheinlichkeitsprobleme trifft dies für andere Verteilungen möglicherweise nicht zu.

Bei Verwendung von Fakt 1 wissen wir, dass die Auswahl eines einzelnen Elements erfolgen kann, indem diese Exponentialverteilungsabtastwerte mit dem Ratenparameter gleich der Gewichtung generiert werden und dann derjenige mit dem kleinsten Wert ausgewählt wird.

Mit Tatsache 2 wissen wir, dass wir die Exponential-Samples nicht erneut generieren müssen. Generieren Sie stattdessen einfach für jedes Element einen und nehmen Sie die k-Elemente mit den niedrigsten Abtastwerten.

Das niedrigste k kann in O (n) gefunden werden. Verwenden Sie den Quickselect - Algorithmus, um das k-te Element zu finden. Führen Sie einfach einen weiteren Durchlauf durch alle Elemente aus und geben Sie alle Werte aus, die niedriger als das k-th sind.

Ein nützlicher Hinweis: Wenn Sie nicht sofort Zugriff auf eine Bibliothek haben, um exponentielle Verteilungsbeispiele zu erstellen, können Sie dies ganz einfach über Folgendes tun: -ln(Rand())/weight

23
Joe K

Wenn das Sampling mit Ersetzung erfolgt, können Sie diesen Algorithmus verwenden (hier in Python implementiert):

import random

items = [(10, "low"),
         (100, "mid"),
         (890, "large")]

def weighted_sample(items, n):
    total = float(sum(w for w, v in items))
    i = 0
    w, v = items[0]
    while n:
        x = total * (1 - random.random() ** (1.0 / n))
        total -= x
        while x > w:
            x -= w
            i += 1
            w, v = items[i]
        w -= x
        yield v
        n -= 1

Dies ist O (n + m), wobei m die Anzahl der Elemente ist.

Warum funktioniert das? Es basiert auf dem folgenden Algorithmus:

def n_random_numbers_decreasing(v, n):
    """Like reversed(sorted(v * random() for i in range(n))),
    but faster because we avoid sorting."""
    while n:
        v *= random.random() ** (1.0 / n)
        yield v
        n -= 1

Die Funktion weighted_sample ist nur dieser Algorithmus, der mit einem Durchlauf der Liste items verbunden ist, um die von diesen Zufallszahlen ausgewählten Elemente auszuwählen.

Dies funktioniert wiederum, weil die Wahrscheinlichkeit, dass n Zufallszahlen 0 ..v zufällig kleiner als z sind, P = (z/v) ist)n. Löse nach z und du erhältst z = vP1/n. Durch Ersetzen einer Zufallszahl für P wird die größte Zahl mit der richtigen Verteilung ausgewählt. und wir können den Vorgang einfach wiederholen, um alle anderen Zahlen auszuwählen.

Wenn die Stichprobe ersatzlos ist, können Sie alle Elemente in einen binären Heap speichern, wobei jeder Knoten die Summe der Gewichtungen aller Elemente in diesem Subheap speichert. Der Aufbau des Heap ist 0 (m). Das Auswählen eines zufälligen Elements aus dem Heap unter Berücksichtigung der Gewichtungen ist O (log m). Das Entfernen dieses Elements und das Aktualisieren der zwischengespeicherten Summen ist ebenfalls O (log m). Sie können also n Elemente in der Zeit O (m + n log m) auswählen.

(Hinweis: "Gewicht" bedeutet hier, dass bei jeder Auswahl eines Elements die verbleibenden Möglichkeiten mit einer Wahrscheinlichkeit proportional zu ihren Gewichtungen ausgewählt werden. Dies bedeutet nicht, dass Elemente in der Ausgabe mit einer Wahrscheinlichkeit erscheinen, die proportional zu ihren Gewichtungen ist.)

Hier ist eine Implementierung davon, reichlich kommentiert:

import random

class Node:
    # Each node in the heap has a weight, value, and total weight.
    # The total weight, self.tw, is self.w plus the weight of any children.
    __slots__ = ['w', 'v', 'tw']
    def __init__(self, w, v, tw):
        self.w, self.v, self.tw = w, v, tw

def rws_heap(items):
    # h is the heap. It's like a binary tree that lives in an array.
    # It has a Node for each pair in `items`. h[1] is the root. Each
    # other Node h[i] has a parent at h[i>>1]. Each node has up to 2
    # children, h[i<<1] and h[(i<<1)+1].  To get this Nice simple
    # arithmetic, we have to leave h[0] vacant.
    h = [None]                          # leave h[0] vacant
    for w, v in items:
        h.append(Node(w, v, w))
    for i in range(len(h) - 1, 1, -1):  # total up the tws
        h[i>>1].tw += h[i].tw           # add h[i]'s total to its parent
    return h

def rws_heap_pop(h):
    gas = h[1].tw * random.random()     # start with a random amount of gas

    i = 1                     # start driving at the root
    while gas >= h[i].w:      # while we have enough gas to get past node i:
        gas -= h[i].w         #   drive past node i
        i <<= 1               #   move to first child
        if gas >= h[i].tw:    #   if we have enough gas:
            gas -= h[i].tw    #     drive past first child and descendants
            i += 1            #     move to second child
    w = h[i].w                # out of gas! h[i] is the selected node.
    v = h[i].v

    h[i].w = 0                # make sure this node isn't chosen again
    while i:                  # fix up total weights
        h[i].tw -= w
        i >>= 1
    return v

def random_weighted_sample_no_replacement(items, n):
    heap = rws_heap(items)              # just make a heap...
    for i in range(n):
        yield rws_heap_pop(heap)        # and pop n items off it.
65
Jason Orendorff

Wenn die Probenahme mit Ersatz erfolgt, verwenden Sie die Roulette-Rad-Auswahl Technik (häufig in genetischen Algorithmen verwendet):

  1. die Gewichte sortieren
  2. berechnen Sie die kumulativen Gewichte
  3. wählen Sie eine Zufallszahl in [0,1]*totalWeight aus.
  4. finden Sie das Intervall, in das diese Nummer fällt
  5. wählen Sie die Elemente mit dem entsprechenden Intervall aus
  6. k mal wiederholen

alt text

Wenn die Stichprobe ohne Ersatz ist, können Sie die obige Technik anpassen, indem Sie das ausgewählte Element nach jeder Iteration aus der Liste entfernen und die Gewichtungen anschließend so normalisieren, dass ihre Summe 1 ist (gültige Wahrscheinlichkeitsverteilungsfunktion).

41
Amro

Ich habe das in Ruby gemacht

https://github.com/fl00r/pickup

require 'pickup'
pond = {
  "selmon"  => 1,
  "carp" => 4,
  "crucian"  => 3,
  "herring" => 6,
  "sturgeon" => 8,
  "gudgeon" => 10,
  "minnow" => 20
}
pickup = Pickup.new(pond, uniq: true)
pickup.pick(3)
#=> [ "gudgeon", "herring", "minnow" ]
pickup.pick
#=> "herring"
pickup.pick
#=> "gudgeon"
pickup.pick
#=> "sturgeon"
3
fl00r

Wenn Sie große Arrays von zufälligen Ganzzahlen mit Ersetzung erzeugen möchten, können Sie die stückweise lineare Interpolation verwenden. Verwenden Sie beispielsweise NumPy/SciPy:

import numpy
import scipy.interpolate

def weighted_randint(weights, size=None):
    """Given an n-element vector of weights, randomly sample
    integers up to n with probabilities proportional to weights"""
    n = weights.size
    # normalize so that the weights sum to unity
    weights = weights / numpy.linalg.norm(weights, 1)
    # cumulative sum of weights
    cumulative_weights = weights.cumsum()
    # piecewise-linear interpolating function whose domain is
    # the unit interval and whose range is the integers up to n
    f = scipy.interpolate.interp1d(
            numpy.hstack((0.0, weights)),
            numpy.arange(n + 1), kind='linear')
    return f(numpy.random.random(size=size)).astype(int)

Dies ist nicht wirksam, wenn Sie ohne Ersatz abtasten möchten.

1
chairmanK

Wenn Sie x-Elemente aus einem Gewichtungssatz ersetzten möchten, ohne dass diese ersetzt werden, werden die Elemente mit einer Wahrscheinlichkeit ausgewählt, die proportional zu ihren Gewichtungen ist:

import random

def weighted_choose_subset(weighted_set, count):
    """Return a random sample of count elements from a weighted set.

    weighted_set should be a sequence of tuples of the form 
    (item, weight), for example:  [('a', 1), ('b', 2), ('c', 3)]

    Each element from weighted_set shows up at most once in the
    result, and the relative likelihood of two particular elements
    showing up is equal to the ratio of their weights.

    This works as follows:

    1.) Line up the items along the number line from [0, the sum
    of all weights) such that each item occupies a segment of
    length equal to its weight.

    2.) Randomly pick a number "start" in the range [0, total
    weight / count).

    3.) Find all the points "start + n/count" (for all integers n
    such that the point is within our segments) and yield the set
    containing the items marked by those points.

    Note that this implementation may not return each possible
    subset.  For example, with the input ([('a': 1), ('b': 1),
    ('c': 1), ('d': 1)], 2), it may only produce the sets ['a',
    'c'] and ['b', 'd'], but it will do so such that the weights
    are respected.

    This implementation only works for nonnegative integral
    weights.  The highest weight in the input set must be less
    than the total weight divided by the count; otherwise it would
    be impossible to respect the weights while never returning
    that element more than once per invocation.
    """
    if count == 0:
        return []

    total_weight = 0
    max_weight = 0
    borders = []
    for item, weight in weighted_set:
        if weight < 0:
            raise RuntimeError("All weights must be positive integers")
        # Scale up weights so dividing total_weight / count doesn't truncate:
        weight *= count
        total_weight += weight
        borders.append(total_weight)
        max_weight = max(max_weight, weight)

    step = int(total_weight / count)

    if max_weight > step:
        raise RuntimeError(
            "Each weight must be less than total weight / count")

    next_stop = random.randint(0, step - 1)

    results = []
    current = 0
    for i in range(count):
        while borders[current] <= next_stop:
            current += 1
        results.append(weighted_set[current][0])
        next_stop += step

    return results
1
ech

Hier ist eine Go-Implementierung von Geodns :

package foo

import (
    "log"
    "math/Rand"
)

type server struct {
    Weight int
    data   interface{}
}

func foo(servers []server) {
    // servers list is already sorted by the Weight attribute

    // number of items to pick
    max := 4

    result := make([]server, max)

    sum := 0
    for _, r := range servers {
        sum += r.Weight
    }

    for si := 0; si < max; si++ {
        n := Rand.Intn(sum + 1)
        s := 0

        for i := range servers {
            s += int(servers[i].Weight)
            if s >= n {
                log.Println("Picked record", i, servers[i])
                sum -= servers[i].Weight
                result[si] = servers[i]

                // remove the server from the list
                servers = append(servers[:i], servers[i+1:]...)
                break
            }
        }
    }

    return result
}
1

In der Frage, mit der Sie verlinkt haben, würde die Lösung von Kyle mit einer trivialen Generalisierung funktionieren. Dann sollte die Wahrscheinlichkeit, ein Element zu wählen, sein:

1 - (1 - (# benötigt/(Gewicht übrig))))/(Gewicht bei n). Wenn Sie einen Knoten besucht haben, ziehen Sie dessen Gewicht von der Summe ab. Wenn Sie n benötigen und n noch übrig sind, müssen Sie explizit aufhören.

Sie können dies mit allem überprüfen, das Gewicht 1 hat. Dies vereinfacht die Lösung von Kyle.

Editiert: (musste umdenken, was doppelt so wahrscheinlich war)

0
Kyle Butt

Genau das macht man mit O(n) und ohne übermäßigen Speicherbedarf. Ich glaube, dies ist eine clevere und effiziente Lösung, die sich leicht in jede Sprache portieren lässt. Die ersten beiden Zeilen dienen lediglich zum Auffüllen von Beispieldaten in Drupal.

function getNrandomGuysWithWeight($numitems){
  $q = db_query('SELECT id, weight FROM theTableWithTheData');
  $q = $q->fetchAll();

  $accum = 0;
  foreach($q as $r){
    $accum += $r->weight;
    $r->weight = $accum;
  }

  $out = array();

  while(count($out) < $numitems && count($q)){
    $n = Rand(0,$accum);
    $lessaccum = NULL;
    $prevaccum = 0;
    $idxrm = 0;
    foreach($q as $i=>$r){
      if(($lessaccum == NULL) && ($n <= $r->weight)){
        $out[] = $r->id;
        $lessaccum = $r->weight- $prevaccum;
        $accum -= $lessaccum;
        $idxrm = $i;
      }else if($lessaccum){
        $r->weight -= $lessaccum;
      }
      $prevaccum = $r->weight;
    }
    unset($q[$idxrm]);
  }
  return $out;
}
0
jacmkno

Ich stelle hier eine einfache Lösung für die Auswahl eines Artikels ein. Sie können ihn leicht für k Artikel erweitern (Java-Stil):

double random = Math.random();
double sum = 0;
for (int i = 0; i < items.length; i++) {
    val = items[i];
    sum += val.getValue();
    if (sum > random) {
        selected = val;
        break;
    }
}
0
shem

Probenahme ohne Ersatz mit Rekursion - elegante und sehr kurze Lösung in c #

// Wie viele Möglichkeiten gibt es 4 von 60 Schülern auszuwählen, so dass wir jedes Mal unterschiedliche 4 wählen

class Program
{
    static void Main(string[] args)
    {
        int group = 60;
        int studentsToChoose = 4;

        Console.WriteLine(FindNumberOfStudents(studentsToChoose, group));
    }

    private static int FindNumberOfStudents(int studentsToChoose, int group)
    {
        if (studentsToChoose == group || studentsToChoose == 0)
            return 1;

        return FindNumberOfStudents(studentsToChoose, group - 1) + FindNumberOfStudents(studentsToChoose - 1, group - 1);

    }
}
0
Angel

Ich habe einen Algorithmus implementiert, der Jason Orendorffs Idee in Rust hier ähnelt. Meine Version unterstützt außerdem Massenoperationen: Einfügen und Entfernen (wenn Sie eine Reihe von Elementen, die durch ihre IDs angegeben wurden, nicht über den gewichteten Auswahlpfad) aus der Datenstruktur in O(m + log n) time, wobei m die Anzahl der zu entfernenden Elemente und n ist die Anzahl der Artikel in gespeichert.

0
kirillkh

Ich habe nur ein paar Stunden damit verbracht, hinter den Algorithmen hinter Sampling zu stehen, ohne ersatzweise da draußen, und dieses Thema ist komplexer als ich zunächst dachte. Das ist aufregend! Zum Wohle zukünftiger Leser (einen guten Tag!) Dokumentiere ich meine Erkenntnisse hier einschließlich einer gebrauchsfertigen Funktion, die die angegebenen Einschlusswahrscheinlichkeiten weiter unten berücksichtigt. Einen guten und schnellen mathematischen Überblick über die verschiedenen Methoden finden Sie hier: Tillé: Algorithmen der Abtastung mit gleichen oder ungleichen Wahrscheinlichkeiten . Zum Beispiel ist die Methode von Jason auf Seite 46 zu finden. Der Nachteil bei seiner Methode ist, dass die Gewichte nicht proportional zu den Einschlusswahrscheinlichkeiten sind, wie auch im Dokument angegeben. Tatsächlich können die i -ten Einschlusswahrscheinlichkeiten wie folgt rekursiv berechnet werden:

def inclusion_probability(i, weights, k):
    """
        Computes the inclusion probability of the i-th element
        in a randomly sampled k-Tuple using Jason's algorithm
        (see https://stackoverflow.com/a/2149533/7729124)
    """
    if k <= 0: return 0
    cum_p = 0
    for j, weight in enumerate(weights):
        # compute the probability of j being selected considering the weights
        p = weight / sum(weights)

        if i == j:
            # if this is the target element, we don't have to go deeper,
            # since we know that i is included
            cum_p += p
        else:
            # if this is not the target element, than we compute the conditional
            # inclusion probability of i under the constraint that j is included
            cond_i = i if i < j else i-1
            cond_weights = weights[:j] + weights[j+1:]
            cond_p = inclusion_probability(cond_i, cond_weights, k-1)
            cum_p += p * cond_p
    return cum_p

Und wir können die Gültigkeit der obigen Funktion durch Vergleich überprüfen

In : for i in range(3): print(i, inclusion_probability(i, [1,2,3], 2))
0 0.41666666666666663
1 0.7333333333333333
2 0.85

zu

In : import collections, itertools
In : sample_tester = lambda f: collections.Counter(itertools.chain(*(f() for _ in range(10000))))
In : sample_tester(lambda: random_weighted_sample_no_replacement([(1,'a'),(2,'b'),(3,'c')],2))
Out: Counter({'a': 4198, 'b': 7268, 'c': 8534})

Eine Möglichkeit, die Einschlusswahrscheinlichkeiten zu spezifizieren, besteht ebenfalls darin, die Gewichtungen daraus zu berechnen. Die ganze Komplexität der gestellten Frage beruht auf der Tatsache, dass man das nicht direkt tun kann, da man die Rekursionsformel grundsätzlich umkehren muss. Ich behaupte symbolisch, dass dies unmöglich ist. Numerisch kann dies unter Verwendung aller Arten von Verfahren erfolgen, z. Newtons Methode. Die Komplexität des Invertierens des Jacobianers mit normalem Python wird jedoch schnell unerträglich. In diesem Fall empfehle ich wirklich, in numpy.random.choice nachzuschauen.

Glücklicherweise gibt es eine Methode, die reines Python verwendet, die für Ihre Zwecke ausreichend oder nicht ausreichend ist. Sie funktioniert hervorragend, wenn es nicht so viele unterschiedliche Gewichte gibt. Sie finden den Algorithmus auf Seite 75 und 76. Es funktioniert, indem der Abtastprozess in Teile mit den gleichen Einschlusswahrscheinlichkeiten aufgeteilt wird, d. H. Wir können random.sample erneut verwenden! Ich werde das Prinzip hier nicht erklären, da die Grundlagen auf Seite 69 gut dargestellt sind. Hier ist der Code mit hoffentlich ausreichender Menge an Kommentaren:

def sample_no_replacement_exact(items, k, best_effort=False, random_=None, ε=1e-9):
    """
        Returns a random sample of k elements from items, where items is a list of
        tuples (weight, element). The inclusion probability of an element in the
        final sample is given by
           k * weight / sum(weights).

        Note that the function raises if a inclusion probability cannot be
        satisfied, e.g the following call is obviously illegal:
           sample_no_replacement_exact([(1,'a'),(2,'b')],2)
        Since selecting two elements means selecting both all the time,
        'b' cannot be selected twice as often as 'a'. In general it can be hard to
        spot if the weights are illegal and the function does *not* always raise
        an exception in that case. To remedy the situation you can pass
        best_effort=True which redistributes the inclusion probability mass
        if necessary. Note that the inclusion probabilities will change
        if deemed necessary.

        The algorithm is based on the splitting procedure on page 75/76 in:
        http://www.eustat.eus/productosServicios/52.1_Unequal_prob_sampling.pdf
        Additional information can be found here:
        https://stackoverflow.com/questions/2140787/

        :param items: list of tuples of type weight,element
        :param k: length of resulting sample
        :param best_effort: fix inclusion probabilities if necessary,
                            (optional, defaults to False)
        :param random_: random module to use (optional, defaults to the
                        standard random module)
        :param ε: fuzziness parameter when testing for zero in the context
                  of floating point arithmetic (optional, defaults to 1e-9)
        :return: random sample set of size k
        :exception: throws ValueError in case of bad parameters,
                    throws AssertionError in case of algorithmic impossibilities
    """
    # random_ defaults to the random submodule
    if not random_:
        random_ = random

    # special case empty return set
    if k <= 0:
        return set()

    if k > len(items):
        raise ValueError("resulting Tuple length exceeds number of elements (k > n)")

    # sort items by weight
    items = sorted(items, key=lambda item: item[0])

    # extract the weights and elements
    weights, elements = list(Zip(*items))

    # compute the inclusion probabilities (short: π) of the elements
    scaling_factor = k / sum(weights)
    π = [scaling_factor * weight for weight in weights]

    # in case of best_effort: if a inclusion probability exceeds 1,
    # try to rebalance the probabilities such that:
    # a) no probability exceeds 1,
    # b) the probabilities still sum to k, and
    # c) the probability masses flow from top to bottom:
    #    [0.2, 0.3, 1.5] -> [0.2, 0.8, 1]
    # (remember that π is sorted)
    if best_effort and π[-1] > 1 + ε:
        # probability mass we still we have to distribute
        debt = 0.
        for i in reversed(range(len(π))):
            if π[i] > 1.:
                # an 'offender', take away excess
                debt += π[i] - 1.
                π[i] = 1.
            else:
                # case π[i] < 1, i.e. 'save' element
                # maximum we can transfer from debt to π[i] and still not
                # exceed 1 is computed by the minimum of:
                # a) 1 - π[i], and
                # b) debt
                max_transfer = min(debt, 1. - π[i])
                debt -= max_transfer
                π[i] += max_transfer
        assert debt < ε, "best effort rebalancing failed (impossible)"

    # make sure we are talking about probabilities
    if any(not (0 - ε <= π_i <= 1 + ε) for π_i in π):
        raise ValueError("inclusion probabilities not satisfiable: {}" \
                         .format(list(Zip(π, elements))))

    # special case equal probabilities
    # (up to fuzziness parameter, remember that π is sorted)
    if π[-1] < π[0] + ε:
        return set(random_.sample(elements, k))

    # compute the two possible lambda values, see formula 7 on page 75
    # (remember that π is sorted)
    λ1 = π[0] * len(π) / k
    λ2 = (1 - π[-1]) * len(π) / (len(π) - k)
    λ = min(λ1, λ2)

    # there are two cases now, see also page 69
    # CASE 1
    # with probability λ we are in the equal probability case
    # where all elements have the same inclusion probability
    if random_.random() < λ:
        return set(random_.sample(elements, k))

    # CASE 2:
    # with probability 1-λ we are in the case of a new sample without
    # replacement problem which is strictly simpler,
    # it has the following new probabilities (see page 75, π^{(2)}):
    new_π = [
        (π_i - λ * k / len(π))
        /
        (1 - λ)
        for π_i in π
    ]
    new_items = list(Zip(new_π, elements))

    # the first few probabilities might be 0, remove them
    # NOTE: we make sure that floating point issues do not arise
    #       by using the fuzziness parameter
    while new_items and new_items[0][0] < ε:
        new_items = new_items[1:]

    # the last few probabilities might be 1, remove them and mark them as selected
    # NOTE: we make sure that floating point issues do not arise
    #       by using the fuzziness parameter
    selected_elements = set()
    while new_items and new_items[-1][0] > 1 - ε:
        selected_elements.add(new_items[-1][1])
        new_items = new_items[:-1]

    # the algorithm reduces the length of the sample problem,
    # it is guaranteed that:
    # if λ = λ1: the first item has probability 0
    # if λ = λ2: the last item has probability 1
    assert len(new_items) < len(items), "problem was not simplified (impossible)"

    # recursive call with the simpler sample problem
    # NOTE: we have to make sure that the selected elements are included
    return sample_no_replacement_exact(
        new_items,
        k - len(selected_elements),
        best_effort=best_effort,
        random_=random_,
        ε=ε
    ) | selected_elements,

Beispiel:

In : sample_no_replacement_exact([(1,'a'),(2,'b'),(3,'c')],2)
Out: {'b', 'c'}

In : import collections, itertools
In : sample_tester = lambda f: collections.Counter(itertools.chain(*(f() for _ in range(10000))))
In : sample_tester(lambda: sample_no_replacement_exact([(1,'a'),(2,'b'),(3,'c'),(4,'d')],2))
Out: Counter({'a': 2048, 'b': 4051, 'c': 5979, 'd': 7922})

Die Gewichte summieren sich auf 10, daher berechnen sich die Einschlusswahrscheinlichkeiten zu: a → 20%, b → 40%, c → 60%, d → 80%. (Summe: 200% = k.) Es funktioniert!

Nur ein Wort der Vorsicht für die produktive Nutzung dieser Funktion, es kann sehr schwierig sein, illegale Eingaben für die Gewichte zu erkennen. Ein offensichtliches illegales Beispiel ist

In: sample_no_replacement_exact([(1,'a'),(2,'b')],2)
ValueError: inclusion probabilities not satisfiable: [(0.6666666666666666, 'a'), (1.3333333333333333, 'b')]

b kann nicht doppelt so oft erscheinen wie a, da beide immer ausgewählt sein müssen. Es gibt subtilere Beispiele. Um eine Ausnahme in der Produktion zu vermeiden, verwenden Sie einfach best_effort = True, wodurch die Einschlusswahrscheinlichkeitsmasse neu ausbalanciert wird, sodass always eine gültige Verteilung ist. Dies könnte natürlich die Einschlusswahrscheinlichkeiten verändern.

0