web-dev-qa-db-ja.com

配列からランダムなサブセットをサンプリングする

JavaScriptの配列からの置き換えなしでランダムなサンプルを取得するクリーンな方法は何ですか?だから配列があるとします

x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

5つの一意の値をランダムにサンプリングしたい。つまり、長さ5のランダムなサブセットを生成します。1つのランダムなサンプルを生成するには、次のようにします。

x[Math.floor(Math.random()*x.length)];

ただし、これが複数回行われると、同じエントリを複数回取得するリスクがあります。

22
Jeroen

Fisher-Yates shuffle を使用して配列のコピーをシャッフルし、スライスを取ることをお勧めします。

function getRandomSubarray(arr, size) {
    var shuffled = arr.slice(0), i = arr.length, temp, index;
    while (i--) {
        index = Math.floor((i + 1) * Math.random());
        temp = shuffled[index];
        shuffled[index] = shuffled[i];
        shuffled[i] = temp;
    }
    return shuffled.slice(0, size);
}

var x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
var fiveRandomMembers = getRandomSubarray(x, 5);

これは、配列全体を不必要にシャッフルするため、大きな配列の小さなランダムなサブセットを取得するための最も効率的な方法ではないことに注意してください。パフォーマンスを向上させるために、代わりに部分シャッフルを行うことができます。

function getRandomSubarray(arr, size) {
    var shuffled = arr.slice(0), i = arr.length, min = i - size, temp, index;
    while (i-- > min) {
        index = Math.floor((i + 1) * Math.random());
        temp = shuffled[index];
        shuffled[index] = shuffled[i];
        shuffled[i] = temp;
    }
    return shuffled.slice(min);
}
45
Tim Down

パーティーには少し遅れますが、これはアンダースコアの新しい sample メソッド(アンダースコア1.5.2-2013年9月)で解決できます。

var x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];

var randomFiveNumbers = _.sample(x, 5);
14
alengel

または... underscore.jsを使用する場合...

_und = require('underscore');

...

function sample(a, n) {
    return _und.take(_und.shuffle(a), n);
}

十分に単純です。

6
ntalbs

私の意見では、デッキ全体をシャッフルする必要はないと思います。サンプルがデッキではなくランダムであることを確認する必要があります。あなたができることは、前部からsize量を選択し、サンプリング配列のそれぞれを別の位置と交換することです。したがって、交換を許可すると、どんどんシャッフルされます。

function getRandom(length) { return Math.floor(Math.random()*(length)); }

function getRandomSample(array, size) {
    var length = array.length;

    for(var i = size; i--;) {
        var index = getRandom(length);
        var temp = array[index];
        array[index] = array[i];
        array[i] = temp;
    }

    return array.slice(0, size);
}

このアルゴリズムは2*sizeステップ、sliceメソッドを含める場合は、ランダムサンプルを選択します。


よりランダム

サンプルをよりランダムにするために、サンプルの開始点をランダムに選択できます。しかし、サンプルを入手するのは少し高価です。

function getRandomSample(array, size) {
    var length = array.length, start = getRandom(length);

    for(var i = size; i--;) {
        var index = (start + i)%length, rindex = getRandom(length);
        var temp = array[rindex];
        array[rindex] = array[index];
        array[index] = temp;
    }
    var end = start + size, sample = array.slice(start, end);
    if(end > length)
        sample = sample.concat(array.slice(0, end - length));
    return sample;
}

これをよりランダムにするのは、サンプリングアレイが大きくサンプルが小さい場合、常にフロントアイテムをシャッフルするだけではサンプルで頻繁に取得されない傾向があるという事実です。配列が常に同じであると想定されていなければ、これは問題にはなりません。したがって、このメソッドが行うことは、シャッフルされた領域が始まるこの位置を変更することです。


交換なし

サンプリング配列をコピーする必要がなく、置き換えを心配する必要がない場合は、以下を実行できますが、3*size2*size

function getRandomSample(array, size) {
    var length = array.length, swaps = [], i = size, temp;

    while(i--) {
        var rindex = getRandom(length);
        temp = array[rindex];
        array[rindex] = array[i];
        array[i] = temp;
        swaps.Push({ from: i, to: rindex });
    }

    var sample = array.slice(0, size);

    // Put everything back.
    i = size;
    while(i--) {
         var pop = swaps.pop();
         temp = array[pop.from];
         array[pop.from] = array[pop.to];
         array[pop.to] = temp;
    }

    return sample;
}

代替品なし、よりランダム

ランダムサンプルをもう少し多く与えるアルゴリズムを非置換関数に適用するには:

function getRandomSample(array, size) {
    var length = array.length, start = getRandom(length),
        swaps = [], i = size, temp;

    while(i--) {
        var index = (start + i)%length, rindex = getRandom(length);
        temp = array[rindex];
        array[rindex] = array[index];
        array[index] = temp;
        swaps.Push({ from: index, to: rindex });
    }

    var end = start + size, sample = array.slice(start, end);
    if(end > length)
        sample = sample.concat(array.slice(0, end - length));

    // Put everything back.
    i = size;
    while(i--) {
         var pop = swaps.pop();
         temp = array[pop.from];
         array[pop.from] = array[pop.to];
         array[pop.to] = temp;
    }

    return sample;
}

もっと早く...

これらすべての投稿と同様に、これはフィッシャーイェーツシャッフルを使用します。しかし、私はアレイをコピーするオーバーヘッドを取り除きました。

function getRandomSample(array, size) {
    var r, i = array.length, end = i - size, temp, swaps = getRandomSample.swaps;

    while (i-- > end) {
        r = getRandom(i + 1);
        temp = array[r];
        array[r] = array[i];
        array[i] = temp;
        swaps.Push(i);
        swaps.Push(r);
    }

    var sample = array.slice(end);

    while(size--) {
        i = swaps.pop();
        r = swaps.pop();
        temp = array[i];
        array[i] = array[r];
        array[r] = temp;
    }

    return sample;
}
getRandomSample.swaps = [];
3
tkellehe

要素を選択すると、配列のコピーから要素を削除できます。パフォーマンスはおそらく理想的ではありませんが、必要なものには問題ないかもしれません。

function getRandom(arr, size) {
  var copy = arr.slice(0), Rand = [];
  for (var i = 0; i < size && i < copy.length; i++) {
    var index = Math.floor(Math.random() * copy.length);
    Rand.Push(copy.splice(index, 1)[0]);
  }
  return Rand;
}
2
mamapitufo

Lodashを使用している場合、APIは4.xで変更されました。

const oneItem = _.sample(arr);
const nItems = _.sampleSize(arr, n);

https://lodash.com/docs#sampleSize

2
chovy

Tim Downの推奨 として、Fisher-Yates Shuffleの使用を強くサポートしていますが、ここでは、空のセットと指定されたセット自体を含め、要求どおりに数学的に正しいランダムサブセットを達成するための非常に短い方法を示します。

注ソリューションは lodash / nderscore に依存します:

const _ = require('loadsh')

function subset(arr) {
    return _.sample(arr, _.random(arr.length));
}
2
Selfish

この方法で5つの要素のサンプルを取得できます。

var sample = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
.map(a => [a,Math.random()])
.sort((a,b) => {return a[1] < b[1] ? -1 : 1;})
.slice(0,5)
.map(a => a[0]);

コードで使用する関数として定義できます。

var randomSample = function(arr,num){ return arr.map(a => [a,Math.random()]).sort((a,b) => {return a[1] < b[1] ? -1 : 1;}).slice(0,num).map(a => a[0]); }

または、それをArrayオブジェクト自体に追加します。

    Array.prototype.sample = function(num){ return this.map(a => [a,Math.random()]).sort((a,b) => {return a[1] < b[1] ? -1 : 1;}).slice(0,num).map(a => a[0]); };

必要に応じて、2つの機能(シャッフルとサンプル)を持つようにコードを分離できます。

    Array.prototype.shuffle = function(){ return this.map(a => [a,Math.random()]).sort((a,b) => {return a[1] < b[1] ? -1 : 1;}).map(a => a[0]); };
    Array.prototype.sample = function(num){ return this.shuffle().slice(0,num); };
2
Luis Marin

ここに、Fisher-Yater Shuffleに基づく別の実装があります。しかし、これはサンプルサイズが配列の長さよりも大幅に小さい場合に最適化されています。この実装では、配列全体をスキャンしたり、元の配列と同じ大きさの配列を割り当てたりすることはありません。スパース配列を使用してメモリ割り当てを減らします。

function getRandomSample(array, count) {
    var indices = [];
    var result = new Array(count);
    for (let i = 0; i < count; i++ ) {
        let j = Math.floor(Math.random() * (array.length - i) + i);
        result[i] = array[indices[j] === undefined ? j : indices[j]];
        indices[j] = indices[i] === undefined ? i : indices[i];
    }
    return result;
}
2
Jesús López

おそらく何かが足りないかもしれませんが、シャッフルの複雑さや潜在的なオーバーヘッドを必要としないソリューションがあるようです:

function sample(array,size) {
  const results = [],
    sampled = {};
  while(results.length<size && results.length<array.length) {
    const index = Math.trunc(Math.random() * array.length);
    if(!sampled[index]) {
      results.Push(array[index]);
      sampled[index] = true;
    }
  }
  return results;
}
1
AnyWhichWay