Программирование и научные вычисления на языке 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)