Программирование и научные вычисления на языке Python/§17

Случайные числа находят множество применений в науке и программировании. Цель этого урока в том, чтобы рассмотреть некоторые практические задачи случайных чисел и научиться писать программы, с ними работающие. Для генерации случайных чисел в Python имеется специальный модуль `random`. Функция `random.random()` генерирует возвращает случайные числа в интервале [0; 1). Попробуем:



```
>>> import random
>>> random.random()
0.33643236965993584
>>> random.random()
0.53110615544542961
>>> random.random()
0.80435595999923271
```


Получение случайных чисел основано на специальных алгоритмах, то есть они не являются действительно случайными, однако, таковыми в силу их близких свойств, мы можем считать.



## Seed - задает случайность

Каждый раз, когда мы используем функцию `random.random()`, мы на выходе получаем различные числа. Это ясно и естественно, но может быть не очень удобным, когда мы ищем ошибку и тогда удобнее получать один и тот же расклад для каждого запуска программы. Для этого имеется функция `random.seed()`, вызываемая сразу же перед началом генерации чисел:



```
>>> random.seed(2)
>>> ['%.2f' % random.random() for i in range(7)]
['0.96', '0.95', '0.06', '0.08', '0.84', '0.74', '0.67']
>>> ['%.2f' % random.random() for i in range(7)]
['0.31', '0.61', '0.61', '0.58', '0.16', '0.43', '0.39']
```


А теперь, если `random.seed()` снова передать то же число:



```
>>> random.seed(2)
>>> ['%.2f' % random.random() for i in range(7)]
['0.96', '0.95', '0.06', '0.08', '0.84', '0.74', '0.67']
```


Если мы не устанавливаем `seed` сами, то модуль устанавливает его по значению текущего времени. Поэтому каждый раз `seed` будет иметь другое значение, следовательно и наборы случайных чисел будут различны.



## Равномерно распределенные случайные числа

Числа, генерируемые функцией `random.random()` равномерно распределяются между 0 и 1, то есть вероятность обнаружить случайное число в любой точке этого интервала одинакова. Такое распределение называется *равномерным* (uniform). Функция `random.uniform(a, b)` более общая — она генерирует равномерно распределенные случайные числа для любого интервала [a, b). Посмотрим, правда ли они распределены равномерно:



```
import random
import matplotlib.pyplot as plt
N = 500
x = range(N)
y = [random.uniform(-1,1) for i in x]
plt.plot(x, y, 'o')
plt.show()
```


Итак, судя по картинке, это действительно равномерное распределение. Но естественно, что имеется и некоторое отклонение, поскольку мы ожидаем настоящей случайности во всем, а значит в разные участки выбранного интервала может попасть разное число точек. Давайте посмотрим, так ли это. Разделим наш интервал на несколько промежутков (пусть это будет 20) и посмотрим гистограммы распределения для разного числа точек:



```
import random
import matplotlib.pyplot as plt
y = [random.random() for i in xrange(10**3)]
plt.hist(y, 20)
plt.show()
```


Как видно из иллюстраций справа, при стремлении числа точек к бесконечности, равномерность распределения только улучшается.

## Random из NumPy

В уже известном нам пакете Numerical Python также есть эффективный модуль построения массивов случайных чисел:



```
from numpy import random
r = random.random() # одно число между 0 и 1
r = random.random(size=10000) # массив с 10000 чисел
r = random.uniform(-1, 10) # одно число между -1 и 10
r = random.uniform(-1, 10, size=10000) # массив
```


Видно, что синтаксис этого модуля от стандартного отличается только наличием параметра `size`. Но построение массива для оптимизированного `numpy`-модуля произойдет быстрее.



## Среднее значение и среднеквадратическое отклонение

Что такое среднее, вам, конечно известно. Может быть, вы также знакомы из теории вероятностей или статистики с такими понятиями как дисперсия (variance), среднеквадратическое отклонение (standard deviation). Все эти понятия очень важны как параметры, характеризующее распределение случайных величин и потому их вычисление уже встроено в Numerical Python:



```
N = 10**6
from numpy import random, mean, var, std
x = random.uniform(-1, 1, size=N)
xm = mean(x) # среднее значение
xv = var(x) # дисперсия
xs = std(x) # СКО
print '''Number: %d
mean: %.2e
var: %.2e
stdev: %.2e''' % (N, xm, xv, xs)
```



## Нормальное распределение

В физических приложениях и гораздо чаще встречается не равномерное, а нормальное (гауссово) распределение вокруг главного значения, вероятность обнаружить которое максимальна, а вероятность обнаружения случайных чисел вокруг него одинаково уменьшается в обе стороны. Например, такое распределение имеет рост человека, ошибки при стрельбе, неточность измерений. Распределение задается этим средним значением (m), называемым математическим ожиданием, и среднеквадратическим отклонением (s).

Отдельные случайные числа мы можем получить либо с помощью стандартного модуля:



```
import random as random_number
r = random_number.normalvariate(m, s)
```


либо с помощью numpy, где мы можем получить и массив чисел:



```
from numpy import random
r = random.normal(m, s, size=N)
```


Чтобы представить вид гауссова распределения, напишем немного кода; в качестве параметров возьмем стандартное нормальное распределение — с математическим ожиданием в нуле и СКО, равным 1:



```
from numpy import random
import matplotlib.pyplot as plt
N = 1E6
m = 0
s = 1
y = random.normal(m, s, size=N)
plt.hist(y, 100)
plt.show()
```



## Целые числа

Довольно часто и не требуются распределения, а нужно выбрать из какого-то конкретного набора целых чисел [a, b], например, при броске кубика может выпасть лишь одно из шести чисел [1, 6], при этом с равной вероятностью. Бросить кубик может родной модуль:



```
import random as random_number
r = random_number.randint(a, b)
```

А может и numpy. Но в отличие от встроенного, интервал здесь полуоткрытый - [a, b) и можно задавать длину возвращаемого массива с помощью N:



```
from numpy import random
r = random.randint(a, b+1, N)
```


Но, на самом деле, есть функция, позволяющая включать и b, то есть задавать интервал как в первом случае - [a, b]:



```
from numpy import random
r = random.random_integers(a, b, N)
```



### Пример: бросание кубика

Напишем программу, которая кидает кубик N раз и подсчитывает сколько раз выпала шестерка:



```
import random as random_number
N = int(raw_input('How many experiments: '))
M = 0
for i in xrange(N):
outcome = random_number.randint(1, 6)
if outcome == 6:
M += 1
print 'Got six %d times out of %d' % (M, N)
```


Код очень понятный, разумный, в духе старого доброго программирования. Функция `xrange`, как мы помним, более быстрый аналог `range`. Но мы можем написать и элегантную «векторизованную» версию:



```
from numpy import random, sum
N = int(raw_input('How many experiments: '))
eyes = random.random_integers(1, 6, N)
success = eyes == 6 # True/False array
M = sum(success) # True как 1, False как 0
print 'Got six %d times out of %d' % (M, N)
```


Конструкция `eyes == 6` векторно сравнивает все элементы массива с 6 и возвращает True в случае совпадения. Далее True это то же, что единица, False то же, что 0 и происходит суммирование. При этом важным моментом является использование именно `numpy.sum`, а не стандартной функции Python, это ускоряет решение для массива до 50 раз.



## Случайный элемент списка

Взять случайный элемент списка `a` очень просто:



```
number = random_number.choice(a)
```


Вызов `random_number.choice(a)` задает случайное число, `number` — переменная, его запоминающая. Кроме того можно перемешать элементы списка с помощью вызова функции `shuffle`:



```
random_number.shuffle(a)
```


Заметьте, что она ничего не возвращает, а преобразует сам список. Вот несколько примеров:



```
>>> awards = ['car', 'computer', 'ball', 'pen']
>>> import random as random_number
>>> random_number.choice(awards)
'car'
>>> awards[random_number.randint(0, len(awards)-1)] # заметьте, это ведь то же самое
'pen'
>>> random_number.shuffle(awards)
>>> awards[0]
'computer'
```



## Пример: Колода карт

Следующая функция создает и тасует колоду карт, где каждая карта представлена строкой, а колода представляет список таких строк:



```
def make_deck():
ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
suits = ['C', 'D', 'H', 'S']
deck = 
for s in suits:
for r in ranks:
deck.append(s + r)
random_number.shuffle(deck)
return deck
```


Здесь ’A’ означает туз (ace), ’J’ соответствует валету (jack), ’Q’ — королеве (queen), ’K’ — королю (king); ’C’ это трефы (clubs), ’D’ — бубны (diamonds), ’H’ — черви (hearts) и ’S’ — пики (spades). Достать колоду из карты можно так:



```
deck = make_deck()
card = deck.pop(0) # возвратить и удалить элемент с индексом 0 (="вынуть")
```


Раздать несколько карт можно так:



```
def deal_hand(n, deck):
hand = [deck[i] for i in range(n)]
del deck[:n]
return hand, deck
```


Заметьте, что мы должны передать функции изменяемый список `deck`. Следующая функция раздает карты нескольким игрокам:



```
def deal(cards_per_hand, no_of_players):
deck = make_deck()
hands = 
for i in range(no_of_players):
hand, deck = deal_hand(cards_per_hand, deck)
hands.append(hand)
return hands
```



```
players = deal(5, 4)
import pprint; pprint.pprint(players)
```


```
[['DQ', 'H6', 'H4', 'CJ', 'DA'],
```


['C2', 'SJ', 'HA', 'C9', 'H3'],

['S6', 'D3', 'CA', 'H7', 'S7'],

['C7', 'S10', 'S2', 'H9', 'C3']]


Теперь смотрим что у нас на руках. Интересным для нас будет наличие пары или тройки карт с одинаковым значением и сколько имеется карт одинаковой масти. Для этого две функции:



```
def same_rank(hand, n_of_a_kind):
ranks = [card[1:] for card in hand]
counter = 0
already_counted = 
for rank in ranks:
if rank not in already_counted and ranks.count(rank) == n_of_a_kind:
counter += 1
already_counted.append(rank)
return counter
def same_suit(hand):
suits = [card[0] for card in hand]
counter = {}
for suit in suits:
count = suits.count(suit)
if count > 1:
counter[suit] = count
return counter
```


Теперь посмотрим что игроков на руках после первой раздачи:



```
for hand in players:
print '''The hand %s
has %d pairs, %s 3-of-a-kind and
%s cards of the same suit.''' % \
(', '.join(hand), same_rank(hand, 2), same_rank(hand, 3),\
'+'.join([str(s) for s in same_suit(hand).values()]))
```


`The hand DQ, H6, H4, CJ, DA`


has 0 pairs, 0 3-of-a-kind and

2+2 cards of the same suit.

The hand C2, SJ, HA, C9, H3

has 0 pairs, 0 3-of-a-kind and

2+2 cards of the same suit.

The hand S6, D3, CA, H7, S7

has 1 pairs, 0 3-of-a-kind and

2 cards of the same suit.

The hand C7, S10, S2, H9, C3

has 0 pairs, 0 3-of-a-kind and

2+2 cards of the same suit.



## Продолжение примера: Класс

На двух предыдущих уроках мы познакомились с классами и их специальными методами. Этот пример отлично годится для того, чтобы применить к нему наши знания. Можно заметить, что `deck` мы перемещаем из одной функции в другую и эти функции представляются отличными кандидатами на методы класса:



```
class Deck:
def __init__(self):
ranks = ['A', '2', '3', '4', '5', '6', '7',
'8', '9', '10', 'J', 'Q', 'K']
suits = ['C', 'D', 'H', 'S']
self.deck = [s+r for s in suits for r in ranks]
random_number.shuffle(self.deck)
def hand(self, n=1):
"""Deal n cards. Return hand as list."""
hand = [self.deck[i] for i in range(n)] # pick cards
del self.deck[:n] # remove cards
return hand
def deal(self, cards_per_hand, no_of_players):
"""Deal no_of_players hands. Return list of lists."""
return [self.hand(cards_per_hand) \
for i in range(no_of_players)]
def putback(self, card):
"""Put back a card under the rest."""
self.deck.append(card)
def __str__(self):
return str(self.deck)
```


Далее мы можем этим пользоваться этим классом, например из файла, куда мы его поместили Deck.py:



```
from Deck import Deck
deck = Deck()
print deck
players = deck.deal(5, 4)
```