Fork me on GitHub
… ou une occasion d’apprendre

Variable aléatoire et répartition selon la loi normale

Posté le 5 avril 2006 à 14:25

Dans le cadre du développement d’une simulation j’ai été confronté au problème des variables aléatoires. Ruby nous offre une fonction rand avec une répartition uniforme des résultats, c’est à dire que chaque valeur dans la plage de résultat conciédré comme valide (float entre 0 et 1 par défaut) à autant de chance que toutes les autres d’être renvoyé par la méthode.

Dans mon cas précis j’ai eu besoin d’orienter le résultat pour avoir une répartion selon la loi normale qui permet de faire un tirage qui tend plus ou moins vers une valeur moyenne définie. Ruby n’offre pas directement une méthode de ce type mais il existe des formules mathématique permetant de dériver d’un tirage aléatoire uniformément réparti une variable aléatoire répondant à cette loi.

(s * Math.sqrt(-2.0 * Math.log(rand)) * Math.cos(2.0 * Math::PI * rand)) + m

Cette formule utilise deux variables aléatoires et deux paramètres (‘m’ et ’s’) et calcul sur cette base une nouvelle valeur. ‘m’ est la valeure médiane du tirage selon la loi normale, les résultats tendent vers cette valeur qui est également la moyenne des tirages et la valeur qui reviens donc le plus souvent. ’s’ permet de modifier la répartition, plus cette valeur est proche de 0, plus les résulats tendent vers la valeur médiane, au contraire si on augmente cette valeur les résultats vont s’étendre (pour une valeur très élevée, on tend vers une répartition uniforme autour de la valeur médiane).

 

def random_normal(m, s, man = nil, max = nil)
m = m.to_f
s = s.to_f
min = min.to_f unless min.nil?
max = max.to_f unless max.nil?

value = (s * Math.sqrt(-2.0 * Math.log(rand)) * Math.cos(2.0 * Math::PI * rand)) + m

if (!max.nil? and value > max) or (!min.nil? and value < min)
return random_normal(m, s, min, max)
else
return value
end
end

La méthode random_normal reçois en paramètre ‘m’ et ’s’ dont j’ai parlé ci-dessus. Elle peut également utiliser deux paramêtres facultatifs, ‘min’ et ‘max’ qui définissent des bornes à la plage de résultat. Si un tirage est hors de cette plage il est effectué à nouveau jusqu’à avoir une valeur valide. Il est possible de définir qu’une des deux bornes, les deux ou aucune.

Il est toujours intéraissant de pouvoir tester une méthode comme celle-ci et avoir une représentation visuel des résultats pour ce faire une idée de ceux-ci. J’ai donc collecter les résultats d’un million d’appel à la méthode afin d’en tirer un certain nombre de statistiques (valeur maximum et minimum, valeur moyen, temps d’exécution) et une représentation graphique de la courbe de répartition des résultats.

m = 0.0
s = 10000000000.0
min = -100
max = 100

nb_tests = 1000000
tic_length = 60

start_time = Time.now
random_time = 0.0

test_values = Hash.new(0)
test_values['min'] = m + 1000000000
test_values['max'] = m – 1000000000
total = 0

nb_tests.times do |i|
random_start_time = Time.now
value = random_normal(m, s, min, max)
random_end_time = Time.now
random_time += random_end_time – random_start_time

test_values[value.round] += 1
test_values['median'] = value.round if test_values[value.round] > test_values[test_values['median']]
test_values['max'] = value.round if value.round > test_values['max']
test_values['min'] = value.round if value.round < test_values['min']
total += value
end

end_time = Time.now

puts « Représentation graphique des tests d’une méthode de tirage aléatoire respectant la loi normale »
puts «  »

tic = test_values[test_values['medium']].round / tic_length
ok = false
(test_values['min']..test_values['max']).each do |i|
max_length_value = test_values['min'].abs.to_s.length > test_values['max'].abs.to_s.length ? test_values['min'].abs.to_s.length : test_values['max'].abs.to_s.length

string_result = i < 0 ? ‘-’ : ‘ ‘
string_result += ’0′ * (max_length_value – i.abs.to_s.length)
string_result += « #{i.abs} : #{‘.’*(test_values[i]/tic)} [#{test_values[i]}] »

puts string_result
end

puts «  »

puts « Nombre de tests : #{nb_tests} (#{end_time – start_time} s) »
puts « Médian : #{m} »
puts « Valeur minimum : #{test_values['min']} »
puts « Valeur médiane : #{test_values['median']} (moyenne : #{total / nb_tests}) »
puts « Valeur maximum : #{test_values['max']} »
puts « Temps moyen par appel à la fonction : #{random_time / nb_tests} »

‘m’, ’s’, ‘min’ et ‘max’ sont les paramètres de la méthode. ‘nb_tests’ est tout simplement le nombre de variable aléatoire qu’on veut récolter, ‘tic_length’ permet de configurer la largeur le la courbe à son point le plus haut, c’est à dire la valeur médiane.

Le fichier complet avec cette méthode, les testes et des commentaires est disponible en pièce jointe.