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