web-dev-qa-db-fra.com

Echantillonnage d'un sous-ensemble aléatoire d'un tableau

Quelle est la manière propre de prélever un échantillon aléatoire, sans le remplacer par un tableau en javascript? Alors supposons qu'il y ait un tableau

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

et je veux échantillonner au hasard 5 valeurs uniques; c’est-à-dire générer un sous-ensemble aléatoire de longueur 5. Pour générer un échantillon aléatoire, on peut faire quelque chose comme:

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

Mais si cela est fait plusieurs fois, il y a un risque de saisir la même entrée plusieurs fois.

18
Jeroen

Je suggère de mélanger une copie du tableau à l'aide de Fisher-Yates shuffle et de prendre une tranche:

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);

Notez que ce n'est pas la méthode la plus efficace pour obtenir un petit sous-ensemble aléatoire d'un grand tableau, car elle mélange inutilement tout le tableau. Pour de meilleures performances, vous pouvez effectuer un mélange partiel:

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);
}
38
Tim Down

Un peu tard pour la fête mais cela pourrait être résolu avec la nouvelle méthode de soulignement sample (soulignement 1.5.2 - sept 2013):

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

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

Ou ... si vous utilisez underscore.js ...

_und = require('underscore');

...

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

Assez simple.

6
ntalbs

Vous pouvez supprimer les éléments d'une copie du tableau lorsque vous les sélectionnez. Les performances ne sont probablement pas idéales, mais elles pourraient convenir à vos besoins:

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

Si vous utilisez lodash, l'API a changé en 4.x:

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

https://lodash.com/docs#sampleSize

2
chovy

Bien que je soutienne fortement l’utilisation du shuffle de Fisher-Yates, comme suggéré par Tim Down , voici une méthode très courte pour obtenir un sous-ensemble aléatoire comme demandé, mathématiquement correcte, y compris l’ensemble vide et l’ensemble donné lui-même.

Remarque solution dépend de lodash / trait de soulignement :

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

À mon avis, je ne pense pas qu'il soit nécessaire de mélanger tout le paquet. Vous devez juste vous assurer que votre échantillon est aléatoire et non votre deck. Ce que vous pouvez faire, c'est sélectionner le montant size à partir de l'avant, puis échanger chaque élément de la matrice d'échantillonnage avec une autre position. Donc, si vous autorisez le remplacement, vous êtes de plus en plus mélangé.

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);
}

Cet algorithme ne comporte que 2*size étapes, si vous incluez la méthode slice, pour sélectionner l'échantillon aléatoire.


Plus aléatoire

Pour rendre l'échantillon plus aléatoire, nous pouvons sélectionner au hasard le point de départ de l'échantillon. Mais obtenir un échantillon coûte un peu plus cher.

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;
}

Ce qui rend cela plus aléatoire est le fait que lorsque vous mélangez toujours les éléments du recto vous avez tendance à ne pas les avoir très souvent dans l'échantillon si le tableau d'échantillonnage est grand et si l'échantillon est petit. Ce ne serait pas un problème si le tableau n'était pas censé être toujours le même. Donc, ce que fait cette méthode est de changer cette position où la région mélangée commence.


Pas de remplacement

Pour ne pas avoir à copier le tableau d'échantillonnage et ne pas vous préoccuper du remplacement, vous pouvez procéder comme suit, mais cela vous donne 3*size vs 2*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;
}

Pas de remplacement et plus aléatoire

Pour appliquer l'algorithme qui donnait un peu plus d'échantillons aléatoires à la fonction sans remplacement:

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;
}

Plus rapide...

Comme tous ces articles, cela utilise le shuffle Fisher-Yates. Mais, j'ai enlevé la tête de la copie du tableau.

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 = [];
2
tkellehe

Vous pouvez obtenir un échantillon de 5 éléments de cette manière:

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]);

Vous pouvez le définir comme une fonction à utiliser dans votre code:

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]); }

Ou ajoutez-le à l'objet Array lui-même:

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]); };

si vous le souhaitez, vous pouvez séparer le code pour avoir 2 fonctionnalités (Shuffle et Sample):

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); };
1
Luis Marin

Voici une autre implémentation basée sur Fisher-Yater Shuffle. Mais celui-ci est optimisé pour le cas où la taille de l'échantillon est nettement inférieure à la longueur du tableau. Cette implémentation n'analyse pas l'intégralité du tableau, ni n'alloue de tableaux de la taille du tableau d'origine. Il utilise des tableaux fragmentés pour réduire l’allocation de mémoire.

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;
}
1
Jesús López

Peut-être me manque quelque chose, mais il semble qu'il existe une solution qui ne nécessite pas la complexité ou les frais généraux potentiels d'un remaniement:

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;
}
0
AnyWhichWay