Программирование и научные вычисления на языке 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 будет иметь другое значение, следовательно и наборы случайных чисел будут различны.


Равномерно распределенные случайные числа[править]

500 точек по random.uniform(-1, 1)
Равномерно распределенные 1000 случайных чисел
То же, но для миллиона случайных чисел

Числа, генерируемые функцией 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).

Нормальное распределение миллиона точек с помощью random.normal()

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


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)

Ссылки[править]