web-dev-qa-db-ja.com

交換せずに段階的にサンプリングする方法は?

Pythonには、_[0, 100)_からの置換なしにランダムにサンプリングするmy_sample = random.sample(range(100), 10)があります。

nのような数値をサンプリングし、置換せずに(以前にサンプリングしたnを含めずに)もう1つサンプリングしたいとします。これを超効率的に行うにはどうすればよいですか。

pdate:「合理的に効率的」から「超効率的」に変更されました(ただし、一定の要素は無視されます)

21
necromancer

OPからの読者への注意:最初に受け入れられた回答 を見てロジックを理解し、この回答を理解してください。

Aaaaaandと完全性のために:これは ネクロマンサーの答え の概念ですが、禁止された数値のリストを入力として受け取るように調整されています。これは 私の以前の回答 と同じコードですが、数値を生成する前に、forbidから状態を構築します。

  • これは時間O(f+k)とメモリO(f+k)です。明らかに、これはforbid(sorted/set)のフォーマットを必要とせずに可能な限り最速のものです。これにより、何らかの形でこれが勝者になると思います^^。
  • forbidがセットの場合、繰り返し推定方法はO(k⋅n/(n-(f+k)))の方が速く、f+kO(k)に非常に近い非常にnに近い。
  • forbidがソートされている場合、 私のばかげたアルゴリズム は次のように高速です:
    O(k⋅(log(f+k)+log²(n/(n-(f+k))))
import random
def sample_gen(n, forbid):
    state = dict()
    track = dict()
    for (i, o) in enumerate(forbid):
        x = track.get(o, o)
        t = state.get(n-i-1, n-i-1)
        state[x] = t
        track[t] = x
        state.pop(n-i-1, None)
        track.pop(o, None)
    del track
    for remaining in xrange(n-len(forbid), 0, -1):
        i = random.randrange(remaining)
        yield state.get(i, i)
        state[i] = state.get(remaining - 1, remaining - 1)
        state.pop(remaining - 1, None)

使用法:

gen = sample_gen(10, [1, 2, 4, 8])
print gen.next()
print gen.next()
print gen.next()
print gen.next()
9
Chronial

オーバーラップせずに複数のサンプルが必要になることが事前にわかっている場合、random.shuffle()list(range(100))を実行するのが最も簡単です(Python 3-= list()をスキップできます= Python 2)、必要に応じてスライスをはがします。

s = list(range(100))
random.shuffle(s)
first_sample = s[-10:]
del s[-10:]
second_sample = s[-10:]
del s[-10:]
# etc

そうでない場合、@ Chronialの回答はかなり効率的です。

21
Tim Peters

短い道

サンプリングされた数が母集団よりもはるかに少ない場合は、サンプリングして、それが選択されているかどうかを確認し、選択されている間繰り返します。これはばかげているように聞こえるかもしれませんが、同じ数を選択する可能性が指数関数的に減衰する可能性があるため、選択されていない割合が少しでもある場合はO(n)よりもはるかに高速です。


長い道のり

Pythonは、PRNGとしてMersenne Twisterを使用しています。 良い十分 。完全に別のものを使用して、予測可能な方法で重複しない数値を生成できます。

ここに秘密があります:

  • 二次残差x² mod pは、2x < pおよびpが素数の場合に一意です。

  • 今回もその[p = 3 mod 4]を指定すると、残りのp - (x² % p)を「反転」すると、結果は残りのスペースになります。

  • これは非常に説得力のある数値の広がりではないので、パワーを増やし、ファッジ定数を追加すれば、分布はかなり良くなります。


最初に素数を生成する必要があります:

from itertools import count
from math import ceil
from random import randrange

def modprime_at_least(number):
    if number <= 2:
        return 2

    number = (number // 4 * 4) + 3
    for number in count(number, 4):
        if all(number % factor for factor in range(3, ceil(number ** 0.5)+1, 2)):
            return number

素数を生成するコストについて心配するかもしれません。 10⁶要素​​の場合、これには10分の1ミリ秒かかります。 [None] * 10**6の実行にはそれよりも時間がかかります。計算は1回だけなので、これは実際の問題ではありません。

さらに、アルゴリズムは素数の正確な値を必要としません。必要なのは、入力された数値よりも大きい定数定数のみです。これは、値のリストを保存して検索することで可能になります。線形スキャンを行う場合はO(log number)であり、バイナリ検索を行う場合はO(log number of cached primes)です。実際、 ギャロッピング を使用すると、これをO(log log number)に下げることができます。これは基本的に定数(log log googol = 2)です。

次に、ジェネレータを実装します

def sample_generator(up_to):
    prime = modprime_at_least(up_to+1)

    # Fudge to make it less predictable
    fudge_power = 2**randrange(7, 11)
    fudge_constant = randrange(prime//2, prime)
    fudge_factor = randrange(prime//2, prime)

    def permute(x):
        permuted = pow(x, fudge_power, prime) 
        return permuted if 2*x <= prime else prime - permuted

    for x in range(prime):
        res = (permute(x) + fudge_constant) % prime
        res = permute((res * fudge_factor) % prime)

        if res < up_to:
            yield res

そして、それが機能することを確認してください:

set(sample_generator(10000)) ^ set(range(10000))
#>>> set()

さて、これの素晴らしい点は、プライマシーテスト(およそO(√n)であるnは要素の数)を無視すると、このアルゴリズムは時間の複雑さO(k)、ここで、kはサンプルのサイズとO(1)のメモリ使用量です。技術的にはO(√n + k)ですが、実際にはO(k)です。


要件:

  1. 実績のあるPRNGは必要ありません。これはPRNGは線形合同ジェネレーターよりもはるかに優れています(これは一般的です Javaが使用しています ))が、メルセンヌツイスターほど証明されていません。

  2. 最初に、別の機能を持つアイテムを生成しません。これにより、チェックではなく数学を通じて重複を回避します。次のセクションでは、この制限を削除する方法を示します。

  3. Shortメソッドは不十分である必要があります(knに近づく必要があります)。 kが半分だけnの場合は、元の提案をそのまま使用してください。

利点:

  1. 極端なメモリ節約。これは一定のメモリを必要とします... O(k)ですらありません!

  2. 次のアイテムを生成するための一定の時間。これも実際には定数でかなり高速です。組み込みのメルセンヌツイスターほど高速ではありませんがasですが、2倍以内です。

  3. 涼しさ。


この要件を削除するには:

最初に、別の機能を持つアイテムを生成しません。これにより、チェックではなく数学を通じて重複を回避します。

私は可能な限り最良のアルゴリズムを作成しましたおよびスペースの複雑さ、これは以前のジェネレーターの単純な拡張です。

以下がその概要です(nは数字のプールの長さ、kは「外部」キーの数):

初期化時間O(√n); O(log log n)すべての妥当な入力

これは、O(√n)のコストが原因で、アルゴリズムの複雑さに関して技術的に完全ではない、私のアルゴリズムの唯一の要因です。実際には、事前計算によってO(log log n)まで低下するため、これは問題になりません。これは、一定の時間に計り知れないほど近い値です。

イテラブルを一定の割合で使い切った場合、コストは無料で償却されます。

これは実際的な問題ではありません。

償却後のO(1)鍵生成時間

明らかにこれを改善することはできません。

最悪の場合のO(k)鍵生成時間

外部から生成されたキーがあり、このジェネレータがすでに生成したキーであってはならないという要件のみがある場合、これらは「外部キー」と呼ばれます。外部キーは完全にランダムであると想定されます。そのため、プールからアイテムを選択できるすべての関数で選択できます。

任意の数の外部キーがあり、完全にランダムになる可能性があるため、完全なアルゴリズムの最悪のケースはO(k)です。

最悪の場合のスペースの複雑さO(k)

外部キーが完全に独立していると想定される場合、それぞれは別個の情報項目を表します。したがって、すべてのキーを保存する必要があります。アルゴリズムは、キーが見つかるたびにキーを破棄するため、ジェネレーターの存続期間を通じてメモリコストはクリアされます。

アルゴリズム

まあ、それは私のアルゴリズムの両方です。実際には非常に簡単です。

def sample_generator(up_to, previously_chosen=set(), *, Prune=True):
    prime = modprime_at_least(up_to+1)

    # Fudge to make it less predictable
    fudge_power = 2**randrange(7, 11)
    fudge_constant = randrange(prime//2, prime)
    fudge_factor = randrange(prime//2, prime)

    def permute(x):
        permuted = pow(x, fudge_power, prime) 
        return permuted if 2*x <= prime else prime - permuted

    for x in range(prime):
        res = (permute(x) + fudge_constant) % prime
        res = permute((res * fudge_factor) % prime)

        if res in previously_chosen:
            if Prune:
                previously_chosen.remove(res)

        Elif res < up_to:
            yield res

変更は追加するのと同じくらい簡単です:

if res in previously_chosen:
    previously_chosen.remove(res)

渡したsetに追加することで、いつでもpreviously_chosenに追加できます。実際には、潜在的なプールに戻すためにセットから削除することもできますが、これによりsample_generatorがまだ生成していないか、Prune=Falseでスキップした場合にのみ機能します。

あります。すべての要件を満たしていることは簡単で、要件が絶対的なものであることは簡単です。セットがない場合でも、オーバーヘッドは増加しますが、入力をセットに変換することで最悪のケースに対応できます。


RNGの品質のテスト

統計的に言えば、このPRNGが実際にどれほど優れているか、気になった。

いくつかのクイック検索により、これらの3つのテストを作成することができました。これらはすべて良い結果を示しているようです!

まず、いくつかの乱数:

N = 1000000

my_gen = list(sample_generator(N))

target = list(range(N))
random.shuffle(target)

control = list(range(N))
random.shuffle(control)

これらは0から10⁶-1までの10⁶の数字の「シャッフル」リストです。1つはファッジされた楽しいPRNGを使用し、もう1つはベースラインとしてMersenne Twisterを使用しています。 3番目はコントロールです。


これは、線に沿った2つの乱数間の平均距離を調べるテストです。違いはコントロールと比較されます:

from collections import Counter

def birthdat_calc(randoms):
    return Counter(abs(r1-r2)//10000 for r1, r2 in Zip(randoms, randoms[1:]))

def birthday_compare(randoms_1, randoms_2):
    birthday_1 = sorted(birthdat_calc(randoms_1).items())
    birthday_2 = sorted(birthdat_calc(randoms_2).items())

    return sum(abs(n1 - n2) for (i1, n1), (i2, n2) in Zip(birthday_1, birthday_2))

print(birthday_compare(my_gen, target), birthday_compare(control, target))
#>>> 9514 10136

これは、それぞれの分散よりも小さいです。


5つの数値を順番に取り、要素の順序を確認するテストを次に示します。120の可能なすべての順序に均等に配分する必要があります。

def permutations_calc(randoms):
    permutations = Counter()        

    for items in Zip(*[iter(randoms)]*5):
        sorteditems = sorted(items)
        permutations[Tuple(sorteditems.index(item) for item in items)] += 1

    return permutations

def permutations_compare(randoms_1, randoms_2):
    permutations_1 = permutations_calc(randoms_1)
    permutations_2 = permutations_calc(randoms_2)

    keys = sorted(permutations_1.keys() | permutations_2.keys())

    return sum(abs(permutations_1[key] - permutations_2[key]) for key in keys)

print(permutations_compare(my_gen, target), permutations_compare(control, target))
#>>> 5324 5368

これも、それぞれの分散よりも小さくなります。


これは、「実行」の長さを確認するテストです。連続的な増加または減少のセクション。

def runs_calc(randoms):
    runs = Counter()

    run = 0
    for item in randoms:
        if run == 0:
            run = 1

        Elif run == 1:
            run = 2
            increasing = item > last

        else:
            if (item > last) == increasing:
                run += 1

            else:
                runs[run] += 1
                run = 0

        last = item

    return runs

def runs_compare(randoms_1, randoms_2):
    runs_1 = runs_calc(randoms_1)
    runs_2 = runs_calc(randoms_2)

    keys = sorted(runs_1.keys() | runs_2.keys())

    return sum(abs(runs_1[key] - runs_2[key]) for key in keys)

print(runs_compare(my_gen, target), runs_compare(control, target))
#>>> 1270 975

ここでの分散は非常に大きく、いくつかの実行で、私は両方の均等な広がりを見せています。そのため、このテストは合格です。


線形合同ジェネレーターは、おそらく「より実り多い」と私に言われました。これが正確な記述かどうかを確認するために、自分でLCGを不適切に実装しました。

LCG(AFAICT)は、通常のジェネレーターのようなものであり、循環式にはなりません。したがって、私が調べたほとんどのリファレンスは、別名。ウィキペディアは、特定の期間の強力なLCGを作成する方法ではなく、期間を定義するもののみをカバーしました。これは結果に影響を与えた可能性があります。

ここに行く:

from operator import mul
from functools import reduce

# Credit http://stackoverflow.com/a/16996439/1763356
# Meta: Also Tobias Kienzler seems to have credit for my
#       edit to the post, what's up with that?
def factors(n):
    d = 2
    while d**2 <= n:
        while not n % d:
            yield d
            n //= d
        d += 1
    if n > 1:
       yield n

def sample_generator3(up_to):
    for modulier in count(up_to):
        modulier_factors = set(factors(modulier))
        multiplier = reduce(mul, modulier_factors)
        if not modulier % 4:
            multiplier *= 2

        if multiplier < modulier - 1:
            multiplier += 1
            break

    x = randrange(0, up_to)

    fudge_constant = random.randrange(0, modulier)
    for modfact in modulier_factors:
        while not fudge_constant % modfact:
            fudge_constant //= modfact

    for _ in range(modulier):
        if x < up_to:
            yield x

        x = (x * multiplier + fudge_constant) % modulier

素数をチェックすることはもうありませんが、因子を使って奇妙なことをする必要があります。

  • modulier ≥ up_to > multiplier, fudge_constant > 0
  • a - 1modulier...のすべての要素で割り切れる必要があります.
  • ...fudge_constantcoprimeで、modulierでなければなりません。

これらはLCGのルールではなく、全期間のLCGであり、明らかにmodulierに等しいことに注意してください。

私はそのようにしました:

  • すべてのmodulierを少なくともup_to試し、条件が満たされたときに停止します
    • その要因のセットを作成します_????
    • multiplier????の製品とし、重複を削除します
    • multipliermodulier以上の場合は、次のmodulierに進みます
    • fudge_constantmodulierよりも小さい数にして、ランダムに選択します
    • fudge_constantにある????から因子を削除します

これはそれを生成する良い方法ではありませんが、低いfudge_constantsとmultiplierがより一般的であるという事実を除けば、なぜそれが数値の品質に影響を与えるのかはわかりませんこれらのための完璧なジェネレーターは作るかもしれません。

とにかく、結果はappallingです。

print(birthday_compare(lcg, target), birthday_compare(control, target))
#>>> 22532 10650

print(permutations_compare(lcg, target), permutations_compare(control, target))
#>>> 17968 5820

print(runs_compare(lcg, target), runs_compare(control, target))
#>>> 8320 662

要約すると、私のRNGは優れており、線形合同ジェネレーターはそうではありません。 Javaは線形合同法ジェネレーターを使用しますが、下位ビットのみを使用します)ことを考えると、私のバージョンは十分以上であると期待します。

9
Veedrac

はい、ここに行きます。これは、可能な限り最速の非確率的アルゴリズムでなければなりません。ランタイムはO(k⋅log²(s) + f⋅log(f)) ⊂ O(k⋅log²(f+k) + f⋅log(f)))およびスペースO(k+f)です。 fは禁止番号の量、sは禁止番号の最長ストリークの長さです。そのための予想はより複雑ですが、明らかにfに拘束されます。 s^log₂(s)fよりも大きいと想定する場合、またはsが再び確率的であるという事実に不満がある場合は、ログ部分を2分割検索に変更できます。 _forbidden[pos:]_を取得するにはO(k⋅log(f+k) + f⋅log(f))を使用します。

ここでの実際の実装はO(k⋅(k+f)+f⋅log(f))です。リストへの挿入forbidO(n)です。これは、リストを blist sortedlist に置き換えることで簡単に修正できます。

また、このアルゴリズムは途方もなく複雑であるため、コメントも追加しました。 lin部分はlog部分と同じですが、log²(s)時間ではなくsが必要です。

_import bisect
import random

def sample(k, end, forbid):
    forbidden = sorted(forbid)
    out = []
    # remove the last block from forbidden if it touches end
    for end in reversed(xrange(end+1)):
        if len(forbidden) > 0 and forbidden[-1] == end:
            del forbidden[-1]
        else:
            break

    for i in xrange(k):
        v = random.randrange(end - len(forbidden) + 1)
        # increase v by the number of values < v
        pos = bisect.bisect(forbidden, v)
        v += pos
        # this number might also be already taken, find the
        # first free spot
        ##### linear
        #while pos < len(forbidden) and forbidden[pos] <=v:
        #    pos += 1
        #    v += 1
        ##### log
        while pos < len(forbidden) and forbidden[pos] <= v:
            step = 2
            # when this is finished, we know that:
            # • forbidden[pos + step/2] <= v + step/2
            # • forbidden[pos + step]   >  v + step
            # so repeat until (checked by outer loop):
            #   forbidden[pos + step/2] == v + step/2
            while (pos + step <= len(forbidden)) and \
                  (forbidden[pos + step - 1] <= v + step - 1):
                step = step << 1
            pos += step >> 1
            v += step >> 1

        if v == end:
            end -= 1
        else:
            bisect.insort(forbidden, v)
        out.append(v)
    return out
_

これを、Veedracが提案した「ハック」(およびPythonのデフォルト実装)と比較します。これには、スペースO(f+k)があり、(n/(n-(f+k))「推測」 )時間:

O(f+k*(n/(n <a href=f+k)))">

私はただ---(これをプロットしました for _k=10_およびかなり大きな_n=10000_です(これは、より大きなnに対してのみ極端になります)。そして、私は言わなければなりません:これは楽しい挑戦のように思えたので、これを実装しただけですが、これがどれほど極端であるかに驚いています:

enter image description here

ズームインして何が起こっているのか見てみましょう:

enter image description here

はい–生成した9998番目の数値の推測はさらに高速です。最初のプロットでわかるように、私の1行でも_f/n_が大きいほど高速であることに注意してください(ただし、大きなnにはかなり恐ろしいスペース要件があります)。

要点を理解するには:ここで時間を費やしているのは、セットを生成することだけです。これは、Veedracのメソッドのf係数だからです。

enter image description here

ですから、ここでの時間を無駄にせずに、Veedracの方法が単に進むべき道であると納得させることができたと思います。私はその確率的な部分があなたを困らせる理由をある程度理解することができますが、ハッシュマップ(= python dicts)や他の多くのアルゴリズムが同様の方法で機能し、それらがうまくいっているようです。

あなたは繰り返しの数の違いを恐れるかもしれません。上記のように、これは 幾何分布 と_p=n-f/n_に従います。したがって、標準偏差(=結果が期待される平均から逸脱することを「期待するはずの量」)は

enter image description here

これは基本的には平均と同じです(_√f⋅n < √n² = n_)。

****編集**:
私はsが実際にはn/(n-(f+k))でもあることに気づきました。したがって、私のアルゴリズムのより正確なランタイムはO(k⋅log²(n/(n-(f+k))) + f⋅log(f))です。上記のグラフが与えられているのでそれはいいです、それはそれがO(k⋅log(f+k) + f⋅log(f))よりかなり速いという私の直感を証明します。ただし、f⋅log(f)はランタイムで最も重要な部分であるため、上記の結果についても何も変更されません。

9
Chronial

OK、最後にもう一度試してください;-)基本シーケンスを変更することを犠牲にして、これは追加のスペースを必要とせず、sample(n)呼び出しごとにnに比例した時間を必要とします。

_class Sampler(object):
    def __init__(self, base):
        self.base = base
        self.navail = len(base)
    def sample(self, n):
        from random import randrange
        if n < 0:
            raise ValueError("n must be >= 0")
        if n > self.navail:
            raise ValueError("fewer than %s unused remain" % n)
        base = self.base
        for _ in range(n):
            i = randrange(self.navail)
            self.navail -= 1
            base[i], base[self.navail] = base[self.navail], base[i]
        return base[self.navail : self.navail + n]
_

小さなドライバー:

_s = Sampler(list(range(100)))
for i in range(9):
    print s.sample(10)
    print s.sample(1)
print s.sample(1)
_

実際、これは再開可能なrandom.shuffle()を実装し、n要素が選択された後に一時停止します。 baseは破棄されませんが、置換されます。

8
Tim Peters

以下は、明示的に差分セットを構築しない方法です。ただし、@ Veedracの「受け入れ/拒否」ロジックの形式を使用します。進むにつれて基本シーケンスを変更するつもりがない場合は、それは避けられないと思います。

def sample(n, base, forbidden):
    # base is iterable, forbidden is a set.
    # Every element of forbidden must be in base.
    # forbidden is updated.
    from random import random
    nusable = len(base) - len(forbidden)
    assert nusable >= n
    result = []
    if n == 0:
        return result
    for elt in base:
        if elt in forbidden:
            continue
        if nusable * random() < n:
            result.append(elt)
            forbidden.add(elt)
            n -= 1
            if n == 0:
                return result
        nusable -= 1
    assert False, "oops!"

ここに小さなドライバーがあります:

base = list(range(100))
forbidden = set()
for i in range(10):
    print sample(10, base, forbidden)
7
Tim Peters

これは、最初に Tim Peters によって投稿され、 Eric によって整えられ、次に necromancer によって適切にスペース最適化された、Knuthシャッフルの私のバージョンです。

私は確かに彼のコードがとてもきれいだと思ったので、これはエリックのバージョンに基づいています:)。

import random
def shuffle_gen(n):
    # this is used like a range(n) list, but we don’t store
    # those entries where state[i] = i.
    state = dict()
    for remaining in xrange(n, 0, -1):
        i = random.randrange(remaining)
        yield state.get(i,i)
        state[i] = state.get(remaining - 1,remaining - 1)
        # Cleanup – we don’t need this information anymore
        state.pop(remaining - 1, None)

使用法:

out = []
gen = shuffle_gen(100)
for n in range(100):
    out.append(gen.next())
print out, len(set(out))
6
Chronial

Wikipediaの "Fisher--Yates shuffle#Modern method" に基づいて、シャッフルジェネレーターを実装できます。

def shuffle_gen(src):
    """ yields random items from base without repetition. Clobbers `src`. """
    for remaining in xrange(len(src), 0, -1):
        i = random.randrange(remaining)
        yield src[i]
        src[i] = src[remaining - 1]

次に、itertools.isliceを使用してスライスできます。

>>> import itertools
>>> sampler = shuffle_gen(range(100))
>>> sample1 = list(itertools.islice(sampler, 10))
>>> sample1
[37, 1, 51, 82, 83, 12, 31, 56, 15, 92]
>>> sample2 = list(itertools.islice(sampler, 80))
>>> sample2
[79, 66, 65, 23, 63, 14, 30, 38, 41, 3, 47, 42, 22, 11, 91, 16, 58, 20, 96, 32, 76, 55, 59, 53, 94, 88, 21, 9, 90, 75, 74, 29, 48, 28, 0, 89, 46, 70, 60, 73, 71, 72, 93, 24, 34, 26, 99, 97, 39, 17, 86, 52, 44, 40, 49, 77, 8, 61, 18, 87, 13, 78, 62, 25, 36, 7, 84, 2, 6, 81, 10, 80, 45, 57, 5, 64, 33, 95, 43, 68]
>>> sample3 = list(itertools.islice(sampler, 20))
>>> sample3
[85, 19, 54, 27, 35, 4, 98, 50, 67, 69]
6
Eric

編集: @TimPetersおよび@Chronialによる以下のよりクリーンなバージョンを参照してください。マイナーな編集が​​これを上に押し上げました。

これが、インクリメンタルサンプリングの最も効率的なソリューションであると私が信じているものです。以前にサンプリングされた数値のリストの代わりに、呼び出し元によって維持される状態は、インクリメンタルサンプラーで使用できるディクショナリと、範囲内に残っている数値のカウントで構成されます。

以下は、実証的な実装です。他のソリューションと比較して:

  • ループなし(標準のPython/Veedracハックなし。Python implとVeedracの間でクレジットを共有)
  • 時間の複雑さはO(log(number_previously_sampled))です
  • スペースの複雑さはO(number_previously_sampled)です

コード:

import random

def remove (i, n, state):
  if i == n - 1:
    if i in state:
      t = state[i]
      del state[i]
      return t
    else:
      return i
  else:
    if i in state:
      t = state[i]
      if n - 1 in state:
        state[i] = state[n - 1]
        del state[n - 1]
      else:
        state[i] = n - 1
      return t
    else:
      if n - 1 in state:
        state[i] = state[n - 1]
        del state[n - 1]
      else:
        state[i] = n - 1
      return i

s = dict()
for n in range(100, 0, -1):
  print remove(random.randrange(n), n, s)
5
necromancer

これは@necromancerのクールなソリューションを書き直したものです。正しく使用するためにクラスでラップし、より多くのdictメソッドを使用してコード行をカットします。

_from random import randrange

class Sampler:
    def __init__(self, n):
        self.n = n # number remaining from original range(n)
        # i is a key iff i < n and i already returned;
        # in that case, state[i] is a value to return
        # instead of i.
        self.state = dict()

    def get(self):
        n = self.n
        if n <= 0:
            raise ValueError("range exhausted")
        result = i = randrange(n)
        state = self.state
        # Most of the fiddling here is just to get
        # rid of state[n-1] (if it exists).  It's a
        # space optimization.
        if i == n - 1:
            if i in state:
                result = state.pop(i)
        Elif i in state:
            result = state[i]
            if n - 1 in state:
                state[i] = state.pop(n - 1)
            else:
                state[i] = n - 1
        Elif n - 1 in state:
            state[i] = state.pop(n - 1)
        else:
            state[i] = n - 1
        self.n = n-1
        return result
_

基本的なドライバーは次のとおりです。

_s = Sampler(100)
allx = [s.get() for _ in range(100)]
assert sorted(allx) == list(range(100))

from collections import Counter
c = Counter()
for i in range(6000):
    s = Sampler(3)
    one = Tuple(s.get() for _ in range(3))
    c[one] += 1
for k, v in sorted(c.items()):
    print(k, v)
_

およびサンプル出力:

_(0, 1, 2) 1001
(0, 2, 1) 991
(1, 0, 2) 995
(1, 2, 0) 1044
(2, 0, 1) 950
(2, 1, 0) 1019
_

眼球では、その分布は良好です(懐疑的な場合はカイ2乗検定を実行します)。ここでのいくつかの解決策は、各確率を等しい確率で与えない(たとえそれらが等しい確率でnの各kサブセットを返すとしても)ので、その点でrandom.sample()とは異なります。

5
Tim Peters

合理的に高速なワンライナー(O(n + m)、n = range、m = old samplesize):

next_sample = random.sample(set(range(100)).difference(my_sample), 10)
5
Chronial

これがコア関数の1つにまだ実装されていないのは驚くべきことですが、ここに、置換されずにサンプル値とリストを返すクリーンバージョンがあります。

def sample_n_points_without_replacement(n, set_of_points):
    sampled_point_indices = random.sample(range(len(set_of_points)), n)
    sampled_point_indices.sort(reverse=True)
    sampled_points = [set_of_points[sampled_point_index] for sampled_point_index in sampled_point_indices]
    for sampled_point_index in sampled_point_indices:
        del(set_of_points[sampled_point_index])
    return sampled_points, set_of_points
0
legel

これはサイドノートです。リストを置き換えずにサンプリングするまったく同じ問題(sample_spaceと呼びます)を解決したいとしますが、まだサンプリングしていない要素のセットを均一にサンプリングするのではなく、 、初期確率分布pが与えられます。これは、空間全体にわたってサンプリングした場合の、分布のi^th要素をサンプリングする確率を示します。

次に、numpyを使用した次の実装は数値的に安定しています。

import numpy as np

def iterative_sampler(sample_space, p=None):
    """
        Samples elements from a sample space (a list) 
        with a given probability distribution p (numPy array) 
        without replacement. If called until StopIteration is raised,
        effectively produces a permutation of the sample space.
    """
    if p is None:
        p = np.array([1/len(sample_space) for _ in sample_space])

    try:
        assert isinstance(sample_space, list)
        assert isinstance(p, np.ndarray)
    except AssertionError:
        raise TypeError("Required types: \nsample_space: list \np type: np.ndarray")

    # Main loop
    n = len(sample_space)   
    idxs_left = list(range(n))
    for i in range(n):
        idx = np.random.choice(
            range(n-i), 
            p= p[idxs_left] / p[idxs_left].sum()
        )
        yield sample_space[idxs_left[idx]]
        del idxs_left[idx]

短くて簡潔です。皆さんの考えを教えてください!

0