Алла Тамбовцева, НИУ ВШЭ
numpy
и характеристики дискретных случайных величин¶Сегодня мы познакомимся с библиотекой numpy
(сокращение от numeric Python), которая часто используется в задачах, связанных с машинным обучением и построением статистических моделей.
Массивы numpy очень похожи на списки (даже больше на вложенные списки), только они имеют одну особенность: элементы массива должны быть одного типа. Либо все элементы целые числа, либо числа с плавающей точкой, либо строки. Для обычных списков это условие не является обязательным:
# все работает
D = [[1, 3, 6], ['a', 'b', 'c']]
print(D)
[[1, 3, 6], ['a', 'b', 'c']]
Чем хороши массивы numpy? Почему обычных списков недостаточно? Во-первых, обработка массивов занимает меньше времени (а их хранение меньше памяти), что очень актуально в случае работы с большими объемами данных. Во-вторых, функции numpy являются векторизованными ‒ их можно применять сразу ко всему массиву, то есть поэлементно. В этом смысле работа с массивами напоминает работу с векторами в R. Если в R у нас есть вектор c(1, 2, 5)
, то, прогнав строчку кода c(1, 2, 5)**2
, мы получим вектор, состоящий из квадратов значений: c(1, 4, 25)
. Со списками в Python такое проделать не получится: понадобятся циклы или списковые включения. Зато с массивами numpy ‒ легко, и без всяких циклов! И в этом мы сегодня убедимся.
Для начала импортируем библиотеку (и сократим название до np
для удобства):
import numpy as np
Получить массив numpy можно из обычного списка, просто используя функцию array()
. Главное, правильно указать список внутри array()
– не забыть квадратные скобки:
X = np.array([-5, 0, 1, 2]) # создаем массив X
X
array([-5, 0, 1, 2])
Массивы numpy удобно использовать для операций со случайными величинами: например, можно сохранить значения дискретной случайной величины в один массив, соответствующие им вероятности – в другой, а далее перемножать эти массивы поэлементно, получая необходимые значения для расчета математического ожидания и дисперсии. Значения X
мы уже сохранили выше, а теперь создадим массив p
с вероятностями случайной величины X
:
p = np.array([0.2, 0.4, 0.1, 0.3])
p
array([0.2, 0.4, 0.1, 0.3])
Теперь мы можем посчитать математическое ожидание случайной величины X
. Для этого необходимо попарно перемножить элементы массивов X
и p
:
X * p # четыре произведения
array([-1. , 0. , 0.1, 0.6])
А затем сложить результаты:
Ex = sum(X * p) # sum - функция для суммирования
Ex
-0.30000000000000004
Получили математическое ожидание равное $-0.3$, то есть $E(X)=-0.3$.
Дополнение. Для тех, кто помнит про скалярное произведение (хотя бы способ его расчета, без геометрической интерпретации): можно заметить, что если мы будем считать список значений $X$ и список значений $p$ векторами, то математическое ожидание в таком контексте – ни что иное как скалярное произведение этих векторов. И в numpy
есть специальная функция для поиска скалярного произведения:
np.dot(X, p) # dot - от английского термина dot product (скалярное произведение)
-0.30000000000000004
Как можно заметить, результаты совпадают. Другой вопрос: почему ответ такой странный? Если бы мы выполняли аналогичные действия вручную, у нас получился бы красивый ответ $-0.3$. Проблема в том, что числа с плавающей точкой (float, дробные числа) в Python хранятся в несколько ином виде по сравнению с тем, что мы видим на экране. Импортируем функцию Decimal()
из библиотеки decimal
, чтобы посмотреть на представление чисел с плавающей точкой внутри Python:
from decimal import Decimal
Decimal(-0.3)
Decimal('-0.299999999999999988897769753748434595763683319091796875')
Еще лучше: теперь число $-0.3$ не похоже ни на $-0.3$, ни на результат выше! К сожалению, так будет всегда. Коварство чисел с плавающей точкой заключается еще в том, что они приводят к получению неожиданного результата при округлении. Округлим число $2.525$ до второго знака после запятой:
round(2.525, 2) # округление до 2 знака после запятой
2.52
По правилам арифметики это число должно было округлиться до $2.53$, но этого не произошло. И дело вовсе не в том, что функция round()
округляет в меньшую сторону. Убедимся в этом:
round(2.66, 1)
2.7
Дело опять в том, что число $2.525$ в Python хранится несколько иначе:
Decimal(2.525)
Decimal('2.524999999999999911182158029987476766109466552734375')
Если мы посмотрим на это число, то увидим, что на третьем месте после запятой стоит $4$, а не $5$, что и приводит к округлению до $2.52$ по правилам арифметики.