Введение в машинное обучение.
Вводятся базовые элементы питона (Python версии 3.xx) на базе ключевых библиотек относящихся к анализу данных: построение графиков/гистограмм и численой статистике. Последнее, в частности, используется для ввода ключевых понятий из анализа данных: выборка, плотность распределения и подгонка модели под данные.
Это предварительная версия! Любые замечания приветсвуются.
Списки хороши, но над ними как мы помним нельзя выполнять нужные нами арфиметических операций. В частности, списки нельзя складывать. Точнее эта операция будет иметь иное толкование:
import numpy as np
[1.1, 2.2, -4.4] + [2.5, 1.9, 5.1] # Они совместяться в один список (контакенация)
Для содания списоков чисел, которые можно складывать поэлементно, нужны именно вектора/массивы.
# Они из списка создаются так:
np.array( [1.1, 2.2, -4.4])
# Такие объекты, массивы, можно складывть по элементно.
np.array( [1.1, 2.2, -4.4]) + np.array( [2.5, -1.9, 5.1])
# Размер массивов должен совпадать. Иначе система выдаст соответствующую ошибку.
np.array( [1.1, 2.2, -4.4]) + np.array( [2.5, -1.9, 5.1, 7.1])
При выводе ошибки система указала, что один массив имеет размер 3, а другой -- 4. Даже была указана их одномерность.
Упр. По массиву значений построй массив отношения текущего элемента к последующему (результирующий массив будет на один элемент меньше).
Размер массива всегда можно запросить у самого объекта.
l = np.array( [1.1, 2.2, -4.4]) # Сохраняем массив в переменную l.
l.shape # Запрашиваем у переменной её размер. Он хранится в переменной shape.
bb = [55, 22, 33] # Создаем список.
bb.shape # Размер имеют объекты array. Другие не обязаны давать значение на это поле.
Замечу, что тип размер/размерность не указывает. Последнее связано с тем, что все элементы массива должны иметь одтн и тот же тип.
type( l ) # Запросим и тип.
a0 = np.array([-1, 2])
a0
a1 = np.array([3, 5])
a1
a0 * a1 # Поэлементное умножение массивов, а не скалярное.
# Ну или в явном виде, т.е. без создания дополнительных промежуточных переменных.
np.array([-2, 7]) * np.array([ -2 , 5])
# Расширение данной идеи на матрицы.
a0 = np.array([[-1], [2]])
a0 # Отмечу, если раньше a0 был массивом, то теперь стал матрицей, т.е. поменялась размерность.
# Поэлементное умножение матриц (элемент массива является одноэлементным массивом).
a0 * np.array([ [3] , [5]]) # Не путать с матричным умножением!
# Размеры у матрицы a0 были:
a0.shape # т.е. две строки, в каждой из которых по одному элементу.
a0 = np.array([[-1, 2]]) # Можно сделать матрицу наоборот (транспонирование).
a0
a0.shape # Одна строка, содержащая два элемента.
# Такие матрицы тоже можно умножать по-элементно.
np.array([[-1, 2]]) * np.array([ [3 , 5]])
# Матрицы можно формировать из других матриц.
a0 = np.array( [ 3, -6 ] )
a1 = np.array( [ -1, 2 ] )
a = np.array( [ a0, a1] )
a
# Размеры должны конечно должны соответствовать.
a0 = np.array( [ 3, -6, 5 ] )
a1 = np.array( [ -1, 2 ] )
a = np.array( [ a0, a1] )
a # Иначе это бует массив объектов. Аккуратно сравни и увидь разницу!
l[4] # Как и со списками нельзя обарщатся к эелементу, которого нет в массиве.
Как было показано выше, массивы можно создавать по спискам, т.е. сначала делается список, а потом массив по нему.
l = [i for i in range(3, 10)] # Создаем список с 3 до 10 (не включительно) с шагом 1.
np.array( l ) # Создаем по списку массив.
Их также можно генерировать готовой функцией.
np.arange(2, 20, 3) # В данному слачае, это последовательность
# с первого числа (2) по последнее (18), но не включая его, с шагом (3).
Существуют и другие вспомогательные функции для формирования массивов. Например функция равномерного разбиения отрезка.
np.linspace(4, 25, 5) # А эта функция создает числа с первого (4) по последнее (включительно) (25) в количестве (5).
# В отличии от списка, все элементы массива должны иметь один и тот же тип. Если объявить так:
q = np.array( [1.0, "aa"] )
q
type(q[0]), type(q[1]) # то число тоже станет строчкой.
np.array( [1.0, "aa"] ) + np.array( [2.0, "bb"] ) # Сложить их как раньше не получится.
В библиотеке есть набор функций для обработки массивов. Так, помимо поэлеметных функий наподобии np.sin
np.sin( l )
имеются и редуцирующие к числу. Например,
np.sum( l ) # Сумма элементов
np.mean( l ), np.std( l ) # Срежнее значение и среднеквадратичное откланение.
np.median( l ), np.max( l ), np.min( l ) # Медиана, максимальное и мимимальное значение.
Упр. По массиву посчитай скользящее среднее, т.е. среднее окна из например 10 элементов. Окно скользит по массиву.
Будем строить функцию по-точечно, т.е. построем значение в каждой из выбранных точек и соеденим соседнии прямой. Для начала сгенерируем точки в которых будет вычислена функция.
x = np.linspace(-5, 10, 20) # Сгенерируем именно равномерный на набор точек.
y = x*x - 2*x +3 # Пишем выражение, которое ествественно будет вычислено в каждой точке, т.е. по-элементно.
Точки и значения вы сформировали. Теперь будем строить график. Для этого нужна ещё одна вспомогательная библиотека.
Библиотека matplotlib применяется для вывода графиков. Точнее её подмодуль (подбиблиотека) pyplot.
import matplotlib.pyplot as plt # Обозначи plt длинное название к отмеченной подбиблиотеке.
# Следуюшща строка позволяет выводить данные прям на данной странице в блоках. Иначе будет "всплывать" отдельное окно.
%matplotlib inline
Отмечу, что последняя строчка относится к система jupyter, а не самому языку python.
Отрисуем нужную функцию. Используем для этого 100 точек. После загрузки библитеки в систему, график по сформированным ранее данным стротися так:
plt.plot( y ) # Значений достаточно.
Но можно конечно добавить отсчеты по оси x и разукрасить график.
plt.plot( x, y, 'g.-') # Строит график по-точечно. g -- green (зеленая), . и - задают стиль точки и прямой.
plt.plot( x, y, 'b*--') # b - синий, * -- звездочка в каждой точке. -- соединение пунктиром.
# Цвета есть такие:
# 'b' -- синий | 'c' -- бирюзовый | 'k' -- черный
# 'g' -- зеленый | 'm' -- пурпурный | 'w' -- белый
# 'r' -- красный | 'y' -- желтый |
# стили прямых такие:
# '-' -- сплошная | '-.' -- точка-дефис
# '--' -- пунктирная | ':' -- по-точечно
# А стили точек:
# Различные размеры:
# '.', ',' и 'o' -- точек
# 'v', '^', '<' и '>' -- треугольников
# '1', '2', '3' и '4' -- уголка.
# Специфика:
# 's' -- квадрат | 'h' -- гексагон-1 | 'x' -- знак x | '|' -- вертикальные
# 'p' -- пентагон | 'H' -- гексагон-2 | 'D' -- ромб | '_' -- горизонтальные
# '*' -- звезда | '+' -- знак плюс | 'd' -- ромб утонченный |
# Можно рисовать несколько графиков на одном чертеже:
y2 = 10*np.sin( x ) + 25
plt.plot( x, y, 'g.-', x, y2, 'b*--')
# Ествественно, что значения можно считать "налету":
plt.plot( x, np.sin(x), 'g.-', x, x*x/100, 'b*--')
plt.legend( ['sin', 'x^2'] ) # Добавим и легенду.
Ввиду того, что количество точек по которым строилась кривая было небольшим, синусойда получилась угловатой.
plt.plot( x, y, 'g.-')
plt.xlabel( "Ось абсцисс" ) # Задаем название для оси абсцисс.
plt.ylabel( "Ось ординат") # Задаем название для оси ординат.
plt.plot( x, y, 'g.-')
plt.xticks( [-2, 0.5, 2, 5] ) # Задаем отсчеты вдоль оси абсцисс.
plt.xlim(-4, 8) # Зданаем границы дипазона для оси абсцисс.
plt.plot( x, np.exp(x), 'g.-')
plt.plot( x, np.exp(x), 'g.-')
plt.yscale( 'log' ) # Меняем шкалу на логарифмическую.
Упр. Нарисуй параболу (зеленым цветом) и отметь корни соответствуюшего уравнения точками (красного цвета).
Берем откуда-то простой csv (comma seperated values) файл. Отмечу, что расширение у некго не обязано быть csv, оно может быть и txt.
Например с https://www.finam.ru/profile/moex-akcii/mechel/export/ Параметры можно менять: сменить эмитента (например, выбрать Газпром, Сбербанк и тому подобное), период (например, выбрать день). Промежуток времени пока лучше выбрать поменьше. Внимание, пока скачиваем файл без заголовка (убрать соответствующий флажок)!
Для считывания данных файлов потребуется библиотека csv.
import csv # Для считывания csv файлов.
data = []
with open('MTLR_190101_190110.txt') as f:
data_rows = csv.reader( f )
for row in data_rows:
data.append( row )
data
Видно, что строчки из файла считались целиком, как текстовые строчки (обрати внимание на одинарные кавычки). Хотелось бы чтобы каждый столбец отделился от других столбцов. Для этого можно указать дополнительный параметр функции обработки csv файлов. Параметр delimiter, он указывает какой разделитель используется для отделения стобцов.
data = []
with open('MTLR_190101_190110.txt') as f:
data_rows = csv.reader( f, delimiter=';' )
for row in data_rows:
data.append( row )
data
Теперь видно, что все стобцы считались по отедльности. Но каждый элемент по прежнему является стройчкой. Нужно для тех стобцов про кторые мы знаем наверняка выполнить явное пробразование из текста в число.
Ранее нам уже встречалось явное преобразование, когда мы преобразовывали истинность в число.
int( 5 < 7 )
По аналогии с этим:
int('34') # Преобразуем текст в число.
Более дотошно:
a = '55'
b = int( a )
type( a ), type( b )
Нам должна быть известна структура входного файла. Точнее должно быть известно назначение столбцов. Для ранее скаченного файла она такая: TICKER, PER, DATE, TIME, OPEN, HIGH, LOW, CLOSE, VOL. Предпоследний столбец соответствует цене закрытия.
close = [ float(row[-2]) for row in data ]
close
Теперь все сработало и был считан столбец значений.
Теперь возьмем файл побольше и построим график.
data = []
with open('MTLR_180101_190110.txt') as f:
data_rows = csv.reader( f, delimiter=';' )
for row in data_rows:
data.append( row )
close = [ float(row[-2]) for row in data ]
plt.plot( close )
plt.ylabel( 'Цена рубли')
plt.xlabel( 'День начиная с 2018 г')
Рассмотрим какой-либо элемент (строчку) из файла.
data[3]
Тогда можно при считывании файла преобразовать все стобцы кроме первых двух в число
data = []
with open('MTLR_180101_190110.txt') as f:
data_rows = csv.reader( f, delimiter=';' )
for row in data_rows:
for i in range(2, len(row)):
row[i] = float( row[i] )
data.append( row )
data[3] # Теперь все числовые даные являются числом, а не текстом.
close = [ day[-2] for day in data ] # -2 это цена закрытия.
plt.plot( close )
plt.ylabel( 'Цена рубли')
plt.xlabel( 'День начиная с 2018 г')
Упр. Нарисуй график цены и его какое-то скользящее среднее.
Упр. Нарисуй графики процентного изменения цен двух эмитентов с начала периуда. С легендой.
Упр. Нарисуй графики процентного поэлементого изменения цен двух эмитентов. С легендой.
Генерирование случайных чисел.
Существуют специальные (библиотечные) функции для создания случайных чисел. Библиотека Numpy уже загружена и переименована как np. Поэтому возможен код:
# В данном случае формируем числа согласно нормальному распределению.
np.random.randn( 4 ) # В скобках указывается размер массива.
a = np.random.randn( 4, 3) # Можно размеры и по другим осям. В данном случае, будет матрица 4x3.
a
a.shape
np.random является модулем, т.е. он содержит набор функций со схожим смыслом. Равномерное распределение задается просто функцией rand из модуля np.random
np.random.rand(4) # Равномерное распределение
# Раз уж if был упомянут...
If конструкция
Обеспечение ветвления является тоже крайне важным элементом в программировании. В питое это достигается конструкцией
if условие:
elif условие:
else:
data = np.random.randint(-10, 10, 10) # Равномерное целочисленное распределение между -10 и 10 невключительно.
data
cnt_n = 0 # Количество отрицательных
cnt_p = 0 # Количество положительных
cnt_e = 0 # Количество равных нулю
data = np.random.randint(-10, 10, 100)
for d in data:
if d > 0:
cnt_p += 1 # cnt_p = cnt_p + 1
elif d < 0:
cnt_n += 1 # cnt_n = cnt_n + 1
else:
cnt_e += 1 # cnt_e = cnt_e + 1
print( ">0 : ", cnt_p, " <0 : ", cnt_n, " ==0 : ", cnt_e)
100/20 # Приблизительно равно количеству равных 0.
Упр. Реши квадратное уравнение. Если корней нет, то напиши об этом.
Формирование случайной величины
gen_data = np.random.randn( 10000 ) # Вывода нет потому что мы присвоили результат переменой.
gen_data.shape
# Строим гистограмму по заданному массиву чисел.
hist = plt.hist( gen_data, 50); # В переменную hist сохраняются сами значения гистограммы.
# Изучим переменую hist, которая соответствует гистограмме.
type( hist )
# Она является парой (tuple)...
type( hist[0] ), type( hist[1] )
# состоящей из двух массивов (numpy.ndarray).
hist # Смотрим содержимое переменной hist.
h = np.histogram( gen_data, 50 ) # Вызовем функцию, которая вычисляет только гистограммы, она её не рисует.
h
Видим, что результат совпадает. Скоре всего код функции plt.hist вызывает функцию np.histogram.
Выражение из случайных величин
# Изучим Хи-квадрат численно.
gen_data = np.random.randn( 1000, 5) # 1000 раз по 5 нормальных распределений.
# Убедимся в правильности размера массива (матрицы).
gen_data.shape # В скобках перечислены размер массива вдоль каждого из измерений.
gen_data[0].shape, gen_data[999].shape
a = np.array( [[2, 3], [-3, 4]])
a * a # Исходя из материала выше таким способом можно возвести в квадрат, по-элементно.
# В Numpy есть ряд функций, которые выполняют операцию reduce.
np.sum( a ) # Например, вычисляет сумму массива a: 3 + -6 + -1 + 2.
Такой вариант вызова выполняет операцию для всех элементов массива как для единого массива, т.е. без учета размерности. Но можно и указать вдоль какой именно размерности делать вычисления.
a = np.array( [[[2, 3], [-3, 4]], [[-5, 7], [-9, 11]] ])
a.shape
a[:, 1, 0]
np.sum( a, axis = 0) # Вдоль нулевой делаем и сохраняем результат в другие размерности ( матрица 2 на 2).
# Например, -3 + -9 равно 12 сохранено в элемент матрицы 1, 0.
a[0, :, 1]
np.sum( a, axis = 1) # Суммируем вдоль первой и сохранаяем результат в оставшуюся размерность.
# Например, 3 + 4 равно 7 сохранено в элемент матрицы 0, 1.
# Теперь продолжим наши изыскания...
sq_data = gen_data * gen_data # Массив состоящий их квадратов нормального распределения.
sq_data.shape
gen_data2 = np.sum( sq_data, axis = 1 ) # сумируем вдоль оси с индекосм 1 (нумирация начинается с 0).
gen_data2.shape # получаем массив состоящий из суммы 5 квадратов номального распределения
# т.е. выборка из Хи-квадрат с параметром 5.
Теперь забывае как мы его создали. Считаем, что данные откуда-то взялись.
hist = plt.hist( gen_data2, 50); # Сторим гистограмму. Она должна напоминать гистограмму Хи-квадрат.
11 < 5 # Помимо арифметических операций существуют и логические. В частности, операции сравнения.
type( 11 < 5 )
# Лож (False) с точки зрения чисел равна 0,
# а истина (True) 1.
int( 12 < 15 ) # Явное преобразование типа.
# Тогда можно строить такие гибридные выражения, где суммируются логические выражения.
( 13 < 12 ) + ( 13 < 14 ) + ( 13 < 16 ) # В данном случае мы посчитали скольких чисел строго больше 13.
cnt = sum( x < 7 for x in gen_data2 ) # Данная строчка посчитает сколько числе в выборке меньше 7.
cnt
cnt / gen_data2.shape[0] # Поделив на общее количество чисел, получаем значение распределения,
# т.е. вероятность того, что значение Хи-квадрат меньше 7.
import scipy.stats as models
#?models.chi2
# Можно воспользоватся явной функцией (chi2), которая использует формулу.
models.chi2.cdf( 7, 5 ) # Первое число указывает точку (7), воторое значение параметра (5) распределения.
# Замечаем, что значение близко к рассчетному.
models.chi2.cdf( 7, 5, 0, 1 ) # Более полный набор параметров включает смещение (0) и масштаб (1).
# models.chi2.cdf.__doc__ # Так можно вызвать документацию по немонятной функции.
params = models.chi2.fit( gen_data2 ) # Можем найти параметры, которые наилучшим образом соответсвуют выборочным данным.
params
models.chi2.cdf( 7, params[0], params[1] ) # Вычисляет вероятность по оценинным параметрам рапределения.
it = models.chi2.interval( 0.95, 5 ) # Строим доверительный интервал.
it
models.chi2.cdf( it[0], 5 ), 1.0 - models.chi2.cdf( it[1], 5 ) # Проверяем доверительный интервал.
models.chi2.cdf( it[1], 5 ) - models.chi2.cdf( it[0], 5 ) # Проверяем его иначе.
sum( it[0] <= x and x <= it[1] for x in gen_data2 ) / gen_data2.shape[0] # Проверяем численно.
models.chi2.mean( 5 ) # Вычисляем матожидание распределения по формуле.
np.mean( gen_data2 ) # Вычисляем выборочное среднее, т.е. по данным.
УПР 0) Выбрать ожну из статистик и выполнить сравнение численного анализа с известными формулами из учебника
#data_fb.shape
#per = data_fb[1:]/data_fb[:-1] -1 # Процентное изменение из о дня в день.
#per
#per.shape
#plt.hist( per, 10); # Строим распределение процентного изменения изо дня в день.
ДЗ1) Выбрать эмитент, некое условие и построить распределение некого другого условия.
#models.chi2.fit( per )
#models.norm.fit( per )
Pearson
models.pearsonr([1, 2, 3, 4, 5], [4, 7, 10, 13, 16])
plt.plot( [1, 2, 3, 4, 5] )
plt.plot( [4, 7, 10, 13, 16] )
models.pearsonr([1, 2, 3, 4, 5], [-12, -14, -16, -18, -20])
models.pearsonr([1, 2, 1, 3, 1], [10, 12, 10, 14, 10]) # Необязательно монотонный данные.
plt.plot( [1, 2, 1, 3, 1] )
plt.plot( [10, 12, 10, 14, 10] )
Spearman
models.spearmanr([1,2,3,4,5], [50,51,60,100,200])
plt.plot( [1,2,3,4,5] )
plt.plot( [50,51,60,100,200] )
models.spearmanr([1,2,6,4,5], [50,51,300,100,200])
plt.plot( [1,2,6,4,5] )
plt.plot( [50,51,300,100,200] )
models.spearmanr([1,5,4,3,2], [5,6,7,8,7])
Статистика Шапиро-Уилка
np.random.seed( 2018 ) # Инициализация генератора случайных чисел.
dat = models.norm.rvs( 2, 3, size=10 ) # 2 и 3 это параметры нормального распределения.
plt.hist( dat );
t = models.shapiro( dat ) # Шапиро-Уилка тест для нормальности. Предполагается, что данных не очень много ( < 5000).
t
# t[0] -- Это значение самой статистики.
# t[1] -- Это значение p-value
print( "Статистика -- ", t[0], ", p-значение ", t[1])
Смотрим на p-значение. Если оно больше, например, 5%, то говорим, что с такой значимостью не можем отвергнуть гипотезу о нормальности даных.
plt.hist( dat * dat ); # Строим гистограмму для квадратов нормального распределения. Для данного распределения
models.shapiro( dat * dat ) # p-значение очень маленькое. Поэтому гипотезна о нормальности отвергнута.
plt.hist( 1/dat ); # Строим гистограмму для обратной величины от нормального распределения.
models.shapiro( 1/dat ) # Она тем более не являются нормальными. p-значение ну очень маленькое.
dat = models.norm.rvs( 2, 3, 10 ) # Возьмем новый набор данных.
plt.hist( dat * dat );
models.shapiro( dat * dat ) # p-значение чуть повыше, но все-равно меньше традиционной значимости 0.05.
plt.hist( 1/dat ); # При молой выборке конечно возможны ложные результаты
models.shapiro( 1/dat ) # Действительно смахивает на нормальное распределение. p-значение значительно выше 5%.
dat = models.norm.rvs( 2, 3, 100 ) # Если взять больше данных, то вероятность такого мала.
plt.hist( dat ); # С увеличением данных, статестический тест может давать странные результаты.
models.shapiro( dat ) # В данном случае хоть тест и пройден, все-таки 8% не сильно далеко от 5%.
plt.hist( dat * dat ); # Для произведения нормальных распределений.
models.shapiro( dat * dat ) # p-значение совсем маленькое. Поэтому, тест не пройден.
plt.hist( 1/dat ); # При большой выборке вероятность ложного результата для сильных отклонений от нормального мала.
models.shapiro( 1/dat ) # p-значение совсем маленькое. Поэтому, тест не пройден.
dat = models.uniform.rvs( size = 10 ) # Возьмем проcто-напросто другой тип раcпределения, а именно равномерное.
plt.hist( dat );
models.shapiro( dat ) # Тем не менее тест ломается на равномерном распределении, так как p-значение пройдено.
dat = models.uniform.rvs( size = 100 ) # Теперь увеличим объем выборки для равномерного распределения.
plt.hist( dat );
models.shapiro( dat ) # p-значение не пройдено, поэтому гипотезу отвергаем.
dat = models.f.rvs( 5, 7, size = 100 ) # f распределение с параметрами 5 и 7.
plt.hist( dat );
models.shapiro( dat ) # f распределении отвергаем, так как p-значение существенно меньше.
dat = models.f.rvs( 20, 35, size = 100 ) # f распределение с параметрами 20 и 35.
plt.hist( dat );
models.shapiro( dat ) # Для данных параметров оно тоже отвергнуто.
dat = models.norm.rvs( 2, 3, 1000 )
plt.hist( dat );
models.shapiro( dat ) # Все хорошо с p-значением.