Русский
Русский
English
Статистика
Реклама

Регрессия

Python, корреляция и регрессия часть 1

18.05.2021 14:13:42 | Автор: admin

Чем больше я узнаю людей, тем больше мне нравится моя собака.

Марк Твен

В предыдущих сериях постов из ремикса книги Генри Гарнера Clojure для исследования данных (Clojure for Data Science) на языке Python мы рассмотрели методы описания выборок с точки зрения сводных статистик и методов статистического вывода из них параметров популяции. Такой анализ сообщает нам нечто о популяции в целом и о выборке в частности, но он не позволяет нам делать очень точные утверждения об их отдельных элементах. Это связано с тем, что в результате сведения данных всего к двум статистикам - среднему значению и стандартному отклонению - теряется огромный объем информации.

Нам часто требуется пойти дальше и установить связь между двумя или несколькими переменными либо предсказать одну переменную при наличии другой. И это подводит нас к теме данной серии из 5 постов - исследованию корреляции и регрессии. Корреляция имеет дело с силой и направленностью связи между двумя или более переменными. Регрессия определяет природу этой связи и позволяет делать предсказания на ее основе.

В этой серии постов будет рассмотрена линейная регрессия. При наличии выборки данных наша модель усвоит линейное уравнение, позволяющее ей делать предсказания о новых, не встречавшихся ранее данных. Для этого мы снова обратимся к библиотеке pandas и изучим связь между ростом и весом спортсменов-олимпийцев. Мы введем понятие матриц и покажем способы управления ими с использованием библиотеки pandas.

О данных

В этой серии постов используются данные, любезно предоставленные компанией Guardian News and Media Ltd., о спортсменах, принимавших участие в Олимпийских Играх 2012 г. в Лондоне. Эти данные изначально были взяты из блога газеты Гардиан.

Обследование данных

Когда вы сталкиваетесь с новым набором данных, первая задача состоит в том, чтобы его обследовать с целью понять, что именно он содержит.

Файл all-london-2012-athletes.tsv достаточно небольшой. Мы можем обследовать данные при помощи pandas, как мы делали в первой серии постов Python, исследование данных и выборы, воспользовавшись функцией read_csv:

def load_data(): return pd.read_csv('data/ch03/all-london-2012-athletes-ru.tsv', '\t') def ex_3_1(): '''Загрузка данных об участниках  олимпийских игр в Лондоне 2012 г.''' return load_data()

Если выполнить этот пример в консоли интерпретатора Python либо в блокноте Jupyter, то вы должны увидеть следующий ниже результат:

Столбцы данных (нам повезло, что они ясно озаглавлены) содержат следующую информацию:

  • ФИО атлета

  • страна, за которую он выступает

  • возраст, лет

  • рост, см.

  • вес, кг.

  • пол "М" или "Ж"

  • дата рождения в виде строки

  • место рождения в виде строки (со страной)

  • число выигранных золотых медалей

  • число выигранных серебряных медалей

  • число выигранных бронзовых медалей

  • всего выигранных золотых, серебряных и бронзовых медалей

  • вид спорта, в котором он соревновался

  • состязание в виде списка, разделенного запятыми

Даже с учетом того, что данные четко озаглавлены, очевидно присутствие пустых мест в столбцах с ростом, весом и местом рождения. При наличии таких данных следует проявлять осторожность, чтобы они не сбили с толку.

Визуализация данных

В первую очередь мы рассмотрим разброс роста спортсменов на Олимпийских играх 2012 г. в Лондоне. Изобразим эти значения роста в виде гистограммы, чтобы увидеть характер распределения данных, не забыв сначала отфильтровать пропущенные значения:

def ex_3_2(): '''Визуализация разброса значений  роста спортсменов на гистограмме''' df = load_data() df['Рост, см'].hist(bins=20) plt.xlabel('Рост, см.') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

Как мы и ожидали, данные приближенно нормально распределены. Средний рост спортсменов составляет примерно 177 см. Теперь посмотрим на распределение веса олимпийских спортсменов:

def ex_3_3(): '''Визуализация разброса значений веса спортсменов''' df = load_data() df['Вес'].hist(bins=20) plt.xlabel('Вес') plt.ylabel('Частота') plt.show()

Приведенный выше пример сгенерирует следующую ниже гистограмму:

Данные показывают четко выраженную асимметрию. Хвост с правой стороны намного длиннее, чем с левой, и поэтому мы говорим, что асимметрия - положительная. Мы можем оценить асимметрию данных количественно при помощи функции библиотеки pandas skew:

def ex_3_4(): '''Вычисление асимметрии веса спортсменов''' df = load_data() swimmers = df[ df['Вид спорта'] == 'Swimming'] return swimmers['Вес'].skew()
0.23441459903001483

К счастью, эта асимметрия может быть эффективным образом смягчена путем взятия логарифма веса при помощи функции библиотеки numpy np.log:

def ex_3_5(): '''Визуализация разброса значений веса спортсменов на полулогарифмической гистограмме с целью удаления  асимметрии''' df = load_data() df['Вес'].apply(np.log).hist(bins=20) plt.xlabel('Логарифмический вес') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

Теперь данные намного ближе к нормальному распределению. Из этого следует, что вес распределяется согласно логнормальному распределению.

Логнормальное распределение

Логнормальное распределение это распределение набора значений, чей логарифм нормально распределен. Основание логарифма может быть любым положительным числом за исключением единицы. Как и нормальное распределение, логнормальное распределение играет важную роль для описания многих естественных явлений.

Логарифм показывает степень, в которую должно быть возведено фиксированное число (основание) для получения данного числа. Изобразив логарифмы на графике в виде гистограммы, мы показали, что эти степени приближенно нормально распределены. Логарифмы обычно берутся по основанию 10 или основанию e, трансцендентному числу, приближенно равному 2.718. В функции библиотеки numpy np.log и ее инверсии np.exp используется основание e. Выражение loge также называется натуральным логарифмом, или ln, из-за свойств, делающих его особенно удобным в исчислении.

Логнормальное распределение обычно имеет место в процессах роста, где темп роста не зависит от размера. Этот феномен известен как закон Джибрэта, который был cформулирован в 1931 г. Робертом Джибрэтом, заметившим, что он применим к росту фирм. Поскольку темп роста пропорционален размеру, более крупные фирмы демонстрируют тенденцию расти быстрее, чем фирмы меньшего размера.

Нормальное распределение случается в ситуациях, где много мелких колебаний, или вариаций, носит суммирующий эффект, тогда как логнормальное распределение происходит там, где много мелких вариаций имеет мультипликативный эффект.

С тех пор выяснилось, что закон Джибрэта применим к большому числу ситуаций, включая размеры городов и, согласно обширному математическому ресурсу Wolfram MathWorld, к количеству слов в предложениях шотландского писателя Джорджа Бернарда Шоу.

В остальной части этой серии постов мы будем использовать натуральный логарифм веса спортсменов, чтобы наши данные были приближенно нормально распределены. Мы выберем популяцию спортсменов примерно с одинаковыми типами телосложения, к примеру, олимпийских пловцов.

Визуализация корреляции

Один из самых быстрых и самых простых способов определить наличие корреляции между двумя переменными состоит в том, чтобы рассмотреть их на графике рассеяния. Мы отфильтруем данные, выбрав только пловцов, и затем построим график роста относительно веса спортсменов:

def swimmer_data(): '''Загрузка данных роста и веса только олимпийских пловцов''' df = load_data() return df[df['Вид спорта'] == 'Swimming'].dropna()def ex_3_6(): '''Визуализация корреляции между ростом и весом''' df = swimmer_data() xs = df['Рост, см'] ys = df['Вес'].apply( np.log ) pd.DataFrame(np.array([xs,ys]).T).plot.scatter(0, 1, s=12, grid=True) plt.xlabel('Рост, см.') plt.ylabel('Логарифмический вес') plt.show()

Этот пример сгенерирует следующий ниже график:

Результат ясно показывает, что между этими двумя переменными имеется связь. График имеет характерно смещенную эллиптическую форму двух коррелируемых, нормально распределенных переменных с центром вокруг среднего значения. Следующая ниже диаграмма сравнивает график рассеяния с распределениями вероятностей роста и логарифма веса:

Точки, близко расположенные к хвосту одного распределения, также демонстрируют тенденцию близко располагаться к тому же хвосту другого распределения, и наоборот. Таким образом, между двумя распределениями существует связь, которую в ближайших нескольких разделах мы покажем, как определять количественно. Впрочем, если мы внимательно посмотрим на предыдущий график рассеяния, то увидим, что из-за округления измерений точки уложены в столбцы и строки (в см. и кг. соответственно для роста и веса). Там, где это происходит, иногда желательно внести в данные искажения, которые также называются сдвигом или джиттером с тем, чтобы яснее показать силу связи. Без генерирования джиттера (в виде случайных отклонений) может оказаться, что, то, что по внешнему виду составляет одну точку, фактически представляет много точек, которые обозначены одинаковой парой значений. Внесение нескольких случайных помех делает эту ситуацию вряд ли возможной.

Генерирование джиттера

Поскольку каждое значение округлено до ближайшего сантиметра или килограмма, то значение, записанное как 180 см, на самом деле может быть каким угодно между 179.5 и 180.5 см, тогда как значение 80 кг на самом деле может быть каким угодно между 79.5 и 80.5 кг. Для создания случайных искажений, мы можем добавить случайные помехи в каждую точку данных роста в диапазоне между -0.5 и 0.5 и в том же самом диапазоне проделать с точками данных веса (разумеется, это нужно cделать до того, как мы возьмем логарифм значений веса):

def jitter(limit): '''Генератор джиттера (произвольного сдвига точек данных)''' return lambda x: random.uniform(-limit, limit) + xdef ex_3_7(): '''Визуализация корреляции между ростом и весом с джиттером''' df = swimmer_data() xs = df['Рост, см'].apply(jitter(0.5)) ys = df['Вес'].apply(jitter(0.5)).apply(np.log) pd.DataFrame(np.array([xs,ys]).T).plot.scatter(0, 1, s=12, grid=True) plt.xlabel('Рост, см.') plt.ylabel('Логарифмический вес') plt.show()

График с джиттером выглядит следующим образом:

Как и в случае с внесением прозрачности в график рассеяния в первой серии постов об описательной статистике, генерирование джиттера это механизм, который обеспечивает исключение несущественных факторов, таких как объем данных или артефакты округления, которые могут заслонить от нас возможность увидеть закономерности в данных.

Ковариация

Одним из способов количественного определения силы связи между двумя переменными является их ковариация. Она измеряет тенденцию двух переменных изменяться вместе.

Если у нас имеется два ряда чисел, X и Y, то их отклонения от среднего значения составляют:

dx_i= x_i-x dy_i=y_i-y

Здесь xi это значение X с индексом i, yi значение Y с индексом i, x среднее значение X, и y среднее значение Y. Если X и Y проявляют тенденцию изменяться вместе, то их отклонения от среднего будет иметь одинаковый знак: отрицательный, если они меньше среднего, положительный, если они больше среднего. Если мы их перемножим, то произведение будет положительным, когда у них одинаковый знак, и отрицательным, когда у них разные знаки. Сложение произведений дает меру тенденции этих двух переменных отклоняться от среднего значения в одинаковом направлении для каждой заданной выборки.

Ковариация определяется как среднее этих произведений:

На чистом Python ковариация вычисляется следующим образом:

def covariance(xs, ys): '''Вычисление ковариации (несмещенная, т.е. n-1)''' dx = xs - xs.mean()  dy = ys - ys.mean() return (dx * dy).sum() / (dx.count() - 1)

В качестве альтернативы, мы можем воспользоваться функцией pandas cov:

df['Рост, см'].cov(df['Вес'])
1.3559273321696459

Ковариация роста и логарифма веса для наших олимпийских пловцов равна 1.356, однако это число сложно интерпретировать. Единицы измерения здесь представлены произведением единиц на входе.

По этой причине о ковариации редко сообщают как об отдельной сводной статистике. Сделать число более понятным можно, разделив отклонения на произведение стандартных отклонений. Это позволяет трансформировать единицы измерения в стандартные оценки и ограничить выход числом в диапазоне между -1 и +1. Этот результат называется корреляцией Пирсона.

Стандартная оценка, англ. standard score, также z-оценка это относительное число стандартных отклонений, на которые значение переменной отстоит от среднего значения. Положительная оценка показывает, что переменная находится выше среднего, отрицательная ниже среднего. Это безразмерная величина, получаемая при вычитании популяционного среднего из индивидуальных значений и деления разности на популяционное стандартное отклонение.

Корреляция Пирсона

Корреляция Пирсона часто обозначается переменной rи вычисляется следующим образом, где отклонения от среднего dxiи dyiвычисляются как и прежде:

Поскольку для переменных X и Y стандартные отклонения являются константными, уравнение может быть упрощено до следующего, где xи y это стандартные отклонения соответственно X и Y:

В таком виде формула иногда упоминается как коэффициент корреляции смешанных моментов Пирсона или попросту коэффициент корреляции и, как правило, обозначается буквой r.

Ранее мы уже написали функции для вычисления стандартного отклонения. В сочетании с нашей функцией с вычислением ковариации получится следующая реализация корреляции Пирсона:

def variance(xs): '''Вычисление корреляции, несмещенная дисперсия при n <= 30''' x_hat = xs.mean() n = xs.count() n = n - 1 if n in range( 1, 30 ) else n  return sum((xs - x_hat) ** 2) / ndef standard_deviation(xs): '''Вычисление стандартного отклонения''' return np.sqrt(variance(xs))def correlation(xs, ys):  '''Вычисление корреляции''' return covariance(xs, ys) / (standard_deviation(xs) *  standard_deviation(ys))

В качестве альтернативы мы можем воспользоваться функцией pandas corr:

df['Рост, см'].corr(df['Вес'])

Поскольку стандартные оценки безразмерны, то и коэффициент корреляции rтоже безразмерен. Если rравен -1.0 либо 1.0, то переменные идеально антикоррелируют либо идеально коррелируют.

Правда, если r = 0, то с необходимостью вовсе не следует, что переменные не коррелируют. Корреляция Пирсона измеряет лишь линейные связи. Как продемонстрировано на следующих графиках, между переменными может существовать еще некая нелинейная связь, которую rне объясняет:

Отметим, что корреляция центрального примера не определена, потому что стандартное отклонение y = 0. Поскольку наше уравнение для rсодержало бы деление ковариации на 0, то результат получается бессмысленным. В этом случае между переменными не может быть никакой корреляции; yвсегда будет иметь среднее значение. Простое обследование стандартных отклонений это подтвердит.

Мы можем вычислить коэффициент корреляции для данных роста и логарифма веса наших пловцов следующим образом:

def ex_3_8(): '''Вычисление корреляции средствами pandas на примере данных роста и веса''' df = swimmer_data() return df['Рост, см'].corr( df['Вес'].apply(np.log))
0.86748249283924894

В результате получим ответ 0.867, который количественно выражает сильную, положительную корреляцию, уже наблюдавшуюся нами на точечном графике.

Выборочный rи популяционный

Аналогично среднему значению и стандартному отклонению, коэффициент корреляции является сводной статистикой. Он описывает выборку; в данном случае, выборку спаренных значений: роста и веса. Коэффициент корреляции известной выборки обозначается буквой r, тогда как коэффициент корреляции неизвестной популяции обозначается греческой буквой (рхо).

Как мы убедились в предыдущей серии постов о тестировании гипотез, мы не должны исходить из того, что результаты, полученные в ходе измерения нашей выборки, применимы к популяции в целом. К примеру, наша популяция может состоять из всех пловцов всех недавних Олимпийских игр. И будет совершенно недопустимо обобщать, например, на другие олимпийские виды спорта, такие как тяжелая атлетика или фитнес-плавание.

Даже в допустимой популяции такой как пловцы, выступавшие на недавних Олимпийских играх, наша выборка коэффициента корреляции является всего лишь одной из многих потенциально возможных. То, насколько мы можем доверять нашему r, как оценке параметра , зависит от двух факторов:

  • Размера выборки

  • Величины r

Безусловно, чем больше выборка, тем больше мы ей доверяем в том, что она представляет всю совокупность в целом. Возможно, не совсем интуитивно очевидно, но величина тоже оказывает влияние на степень нашей уверенности в том, что выборка представляет параметр . Это вызвано тем, что большие коэффициенты вряд ли возникли случайным образом или вследствие случайной ошибки при отборе.

Проверка статистических гипотез

В предыдущей серии постов мы познакомились с проверкой статистических гипотез, как средством количественной оценки вероятности, что конкретная гипотеза (как, например, что две выборки взяты из одной и той же популяции) истинная. Чтобы количественно оценить вероятность, что корреляция существует в более широкой популяции, мы воспользуемся той же самой процедурой.

В первую очередь, мы должны сформулировать две гипотезы, нулевую гипотезу и альтернативную:

H_0=0H_1\ne 0

H0 - это гипотеза, что корреляция в популяции нулевая. Другими словами, наше консервативное представление состоит в том, что измеренная корреляция целиком вызвана случайной ошибкой при отборе.

H1 - это альтернативная возможность, что корреляция в популяции не нулевая. Отметим, что мы не определяем направление корреляции, а только что она существует. Это означает, что мы выполняем двустороннюю проверку.

Стандартная ошибка коэффициента корреляции rпо выборке задается следующей формулой:

Эта формула точна, только когда rнаходится близко к нулю (напомним, что величина влияет на нашу уверенность), но к счастью, это именно то, что мы допускаем согласно нашей нулевой гипотезы.

Мы можем снова воспользоваться t-распределением и вычислить t-статистику:

В приведенной формуле df это степень свободы наших данных. Для проверки корреляции степень свободы равна n - 2, где n это размер выборки. Подставив это значение в формулу, получим:

В итоге получим t-значение 102.21. В целях его преобразования в p-значение мы должны обратиться к t-распределению. Библиотека scipy предоставляет интегральную функцию распределения (ИФР) для t-распределения в виде функции stats.t.cdf, и комплементарной ей (1-cdf) функции выживания stats.t.sf. Значение функции выживания соответствует p-значению для односторонней проверки. Мы умножаем его на 2, потому что выполняем двустороннюю проверку:

def t_statistic(xs, ys): '''Вычисление t-статистики''' r = xs.corr(ys) # как вариант, correlation(xs, ys) df = xs.count() - 2 return r * np.sqrt(df / 1 - r ** 2)def ex_3_9(): '''Выполнение двухстороннего t-теста''' df = swimmer_data() xs = df['Рост, см'] ys = df['Вес'].apply(np.log) t_value = t_statistic(xs, ys) df = xs.count() - 2  p = 2 * stats.t.sf(t_value, df) # функция выживания  return {'t-значение':t_value, 'p-значение':p}
{'p-значение': 1.8980236317815443e-106, 't-значение': 25.384018200627057}

P-значение настолько мало, что в сущности равно 0, означая, что шанс, что нулевая гипотеза является истинной, фактически не существует. Мы вынуждены принять альтернативную гипотезу о существовании корреляции.

Интервалы уверенности

Установив, что в более широкой популяции, безусловно, существует корреляция, мы, возможно, захотим количественно выразить диапазон значений, внутри которого, как мы ожидаем, будет лежать параметр , вычислив для этого интервал уверенности. Как и в случае со средним значением в предыдущей серии постов, интервал уверенности для rвыражает вероятность (выраженную в %), что параметр популяции находится между двумя конкретными значениями.

Однако при попытке вычислить стандартную ошибку коэффициента корреляции возникает сложность, которой не было в случае со средним значением. Поскольку абсолютное значение коэффициента корреляции rне может превышать 1, распределение возможных выборок коэффициентов корреляции rсмещается по мере приближения rк пределу своего диапазона.

Приведенный выше график показывает отрицательно скошенное распределение r-выборок для параметра , равного 0.6.

К счастью, трансформация под названием z-преобразование Фишера стабилизирует дисперсию r по своему диапазону. Она аналогична тому, как наши данные о весе спортсменов стали нормально распределенными, когда мы взяли их логарифм.

Уравнение для z-преобразования следующее:

Стандартная ошибка z равна:

Таким образом, процедура вычисления интервалов уверенности состоит в преобразовании rв z с использованием z-преобразования, вычислении интервала уверенности в терминах стандартной ошибки SEzи затем преобразовании интервала уверенности в r.

В целях вычисления интервала уверенности в терминах SEz, мы можем взять число стандартных отклонений от среднего, которое дает нам требуемый уровень доверия. Обычно используют число 1.96, так как оно является числом стандартных отклонений от среднего, которое содержит 95% площади под кривой. Другими словами, 1.96 стандартных ошибок от среднего значения выборочного rсодержит истинную популяционную корреляцию с 95%-ой определенностью.

Мы можем убедиться в этом, воспользовавшись функцией scipy stats.norm.ppf. Она вернет стандартную оценку, связанную с заданной интегральной вероятностью в условиях односторонней проверки.

Однако, как показано на приведенном выше графике, мы хотели бы вычесть ту же самую величину, т.е. 2.5%, из каждого хвоста с тем, чтобы 95%-й интервал уверенности был центрирован на нуле. Для этого при выполнении двусторонней проверки нужно просто уменьшить разность наполовину и вычесть результат из 100%. Так что, требуемый уровень доверия в 95% означает, что мы обращаемся к критическому значению 97.5%:

def critical_value(confidence, ntails): # ДИ и число хвостов '''Расчет критического значения путем вычисления квантиля и получения  для него нормального значения''' lookup = 1 - ((1 - confidence) / ntails)  return stats.norm.ppf(lookup, 0, 1) # mu=0, sigma=1critical_value(0.95, 2)
1.959963984540054

Поэтому наш 95%-й интервал уверенности в z-пространстве для задается следующей формулой:

Подставив в нашу формулу zrи SEz, получим:

Для r=0.867и n=859она даст нижнюю и верхнюю границу соответственно 1.137 и 1.722. В целях их преобразования из z-оценок в r-значения, мы используем следующее обратное уравнение z-преобразования:

Преобразования и интервал уверенности можно вычислить при помощи следующего исходного кода:

def z_to_r(z): '''Преобразование z-оценки обратно в r-значение''' return (np.exp(z*2) - 1) / (np.exp(z*2) + 1)def r_confidence_interval(crit, xs, ys):  '''Расчет интервала уверенности для критического значения и данных''' r = xs.corr(ys) n = xs.count() zr = 0.5 * np.log((1 + r) / (1 - r))  sez = 1 / np.sqrt(n - 3) return (z_to_r(zr - (crit * sez))), (z_to_r(zr + (crit * sez)))def ex_3_10(): '''Расчет интервала уверенности на примере данных роста и веса''' df = swimmer_data() X = df['Рост, см'] y = df['Вес'].apply(np.log) interval = r_confidence_interval(1.96, X, y)  print('Интервал уверенности (95%):', interval)
Интервал уверенности (95%): (0.8499088588880347, 0.8831284878884087)

В результате получаем 95%-й интервал уверенности для , расположенный между 0.850 и 0.883. Мы можем быть абсолютно уверены в том, что в более широкой популяции олимпийских пловцов существует сильная положительная корреляция между ростом и весом.

В следующем посте, посте 2, будет рассмотрена сама тема серии постов - регрессия и приемы оценивания ее качества.

Подробнее..

Из песочницы Moneyball и Формула-1 модель прогнозирования результатов квалификаций

07.07.2020 18:11:11 | Автор: admin


Сразу скажу: я не IT-специалист, а энтузиаст в сфере статистики. Помимо этого, я на протяжении многих лет участвовал в различных конкурсах прогнозов по Формуле-1. Отсюда вытекают и задачи, стоявшие перед моей моделью: выдавать прогнозы, которые были бы не хуже тех, которые создаются на глаз. А в идеале модель, конечно, должна обыгрывать человеческих оппонентов.

Эта модель посвящена прогнозированию исключительно результатов квалификаций, поскольку квалификации более предсказуемы, чем гонки, и их проще моделировать. Однако, конечно, в будущем я планирую создать модель, позволяющую с достаточно хорошей точностью предсказывать и результаты гонок.

Для создания модели я свел в одну таблицу все результаты практик и квалификаций за сезоны 2018 и 2019. 2018-й год служил в качестве обучающей выборки, а 2019-й в качестве тестовой. По этим данным мы построили линейную регрессию. Если максимально просто объяснять регрессию, то наши данные это совокупность точек на координатной плоскости. Мы провели прямую, которая меньше всего отклоняется от совокупности этих точек. И функция, графиком которой является эта прямая это и есть наша линейная регрессия.

От известной из школьной программы формулы $inline$y = kx + b $inline$ нашу функцию отличает только то, что переменных у нас две. Первая переменная (X1) это отставание в третьей практике, а вторая переменная (X2) среднее отставание по предыдущим квалификациям. Эти переменные не равнозначны, и одна из наших целей определить вес каждой переменной в диапазоне от 0 до 1. Чем дальше переменная от нуля, тем большее значение она имеет при объяснении зависимой переменной. В нашем случае в качестве зависимой переменной выступает время на круге, выраженное в отставании от лидера (или точнее, от некоего идеального круга, поскольку у всех пилотов эта величина была положительной).

Поклонники книги Moneyball (в фильме этот момент не объясняется) могут вспомнить, что там с помощью линейной регрессии определили, что процент занятия базы, aka OBP (on-base percentage), более тесно связан с заработанными ранами, чем другие статистические показатели. Мы преследуем примерно такую же цель: понять, какие именно факторы наиболее тесно связаны с результатами квалификаций. Один из больших плюсов регрессии в том, что она не требует продвинутого знания математики: мы просто задаем данные, а потом Excel или другой табличный редактор выдает нам готовые коэффициенты.

По сути, с помощью линейной регрессии мы хотим узнать две вещи. Во-первых, насколько выбранные нами независимые переменные объясняют изменение функции. И во-вторых, насколько велика значимость каждой из этих независимых переменных. Иначе говоря, что лучше объясняет результаты квалификации: результаты заездов на предыдущих трассах или итоги тренировок на этой же трассе.

Тут надо отметить важный момент. Конечный результат складывался из двух независимых параметров, каждый из которых вытекал из двух независимых регрессий. Первый параметр сила команды на этом этапе, точнее, отставание лучшего пилота команды от лидера. Второй параметр распределение сил внутри команды.

Что это значит на примере? Допустим, мы берем Гран-при Венгрии сезона-2019. Модель показывает, что отставание Феррари от лидера составит 0,218 секунды. Но это отставание первого пилота, а кто им будет Феттель или Леклер и какой разрыв между ними будет, определяется другим параметром. В этом примере модель показала, что впереди будет Феттель, а Леклер проиграет ему 0,096 секунды.



К чему такие сложности? Не проще ли рассматривать каждого пилота по отдельности вместо этой разбивки на отставание команды и отставание первого пилота от второго внутри команды? Возможно, это так, но мои личные наблюдения показывают, что смотреть на результаты команды гораздо надежнее, чем на результаты каждого пилота. Один пилот может допустить ошибку, или вылететь с трассы, или у него будут технические проблемы все это будет вносить хаос в работу модели, если только не отслеживать вручную каждую форс-мажорную ситуацию, что требует слишком много времени. Влияние форс-мажоров на результаты команды гораздо меньше.

Но вернемся к моменту, где мы хотели оценить, насколько хорошо выбранные нами независимые переменные объясняют изменения функции. Это можно сделать с помощью коэффициента детерминации. Он продемонстрирует, в какой степени результаты квалификации объясняются результатами практик и предыдущих квалификаций.

Поскольку мы строили две регрессии, то и коэффициента детерминации у нас тоже два. Первая регрессия отвечает за уровень команды на этапе, вторая за противостояние между пилотами одной команды. В первом случае коэффициент детерминации равен 0,82, то есть 82% результатов квалификаций объясняются выбранными нами факторами, а еще 18% какими-то другими факторами, которые мы не учли. Это достаточно неплохой результат. Во втором случае коэффициент детерминации составил 0,13.

Эти показатели, по сути, означают, что модель достаточно хорошо предсказывает уровень команды, но испытывает проблемы с определением разрыва между партнерами по команде. Однако для итоговой цели нам не нужно знать разрыв, нам достаточно знать, кто из двух пилотов будет выше, и с этим модель в основном справляется. В 62% случаев модель ставила выше того пилота, который действительно был выше по итогам квалификации.

При этом при оценке силы команды результаты последней тренировки были в полтора раза важнее, чем результаты предыдущих квалификаций, а вот во внутрикомандных дуэлях все было наоборот. Тенденция проявилась как на данных 2018, так и 2019 года.

Итоговая формула выглядит так:

Первый пилот:

$$display$$Y1 = (0,618 * X1 + 0,445 * X2)$$display$$


Второй пилот:

$$display$$Y2 = Y1 + (0,313 * X1 + 0,511 * X2)$$display$$



Напоминаю, что X1 это отставание в третьей практике, а X2 среднее отставание по предыдущим квалификациям.

Что означают эти цифры. Они означают, что уровень команды в квалификации на 60% определяется результатами третьей практики и на 40% результатами квалификаций на предыдущих этапах. Соответственно, результаты третьей практики в полтора раза более значимый фактор, чем результаты предыдущих квалификаций.

Поклонники Формулы-1 наверняка знают ответ на этот вопрос, но для остальных следует прокомментировать, почему я брал результаты именно третьей практики. В Формуле-1 проводится три практики. Однако именно в последней из них команды традиционно тренируют квалификацию. Однако же в тех случаях, когда третья практика срывается из-за дождя или других форс-мажоров, я брал результаты второй практики. Насколько я помню, в 2019 году был только один такой случай на Гран-при Японии, когда из-за тайфуна этап прошел в укороченном формате.

Также кто-то наверняка заметил, что модель использует среднее отставание в предыдущих квалификациях. Но как быть на первом этапе сезона? Я использовал отставания с предыдущего года, но не оставлял их как есть, а вручную их корректировал, основываясь на здравом смысле. Например, в 2019 году Феррари в среднем была быстрее, чем Ред Булл на 0,3 секунды. Однако похоже, что у итальянской команды не будет такого преимущества в этом году, а может, они и вовсе будут позади. Поэтому для первого этапа сезона 2020, Гран-при Австрии, я вручную приблизил Ред Булл к Феррари.

Таким образом я получал отставание каждого пилота, ранжировал пилотов по отставанию и получил итоговый прогноз на квалификацию. При этом важно понимать, что первый и второй пилот это чистые условности. Возвращаясь к примеру с Феттелем и Леклером, на Гран-при Венгрии модель посчитала первым пилотом Себастьяна, но на многих других этапах она отдавала предпочтение Леклеру.

Результаты


Как я говорил, задачей было создать такую модель, которая позволит прогнозировать не хуже людей. За основу я брал свои прогнозы и прогнозы своих партнеров по команде, которые создавались на глаз, но при внимательном изучении результатов практик и совместном обсуждении.

Система оценки была следующая. Учитывалась только первая десятка пилотов. За точное попадание прогноз получал 9 баллов, за промах в 1 позицию 6 баллов, за промах в 2 позиции 4 балла, за промах в 3 позиции 2 балла и за промах в 4 позиции 1 балл. То есть если в прогнозе пилот стоит на 3-м месте, а в результате он взял поул-позишн, то прогноз получал 4 балла.

При такой системе максимальное количество баллов за 21 Гран-при 1890.
Человеческие участники набрали 1056, 1048 и 1034 балла соответственно.
Модель набрала 1031 балл, хотя при легкой манипуляции с коэффициентами я также получал 1045 и 1053 балла.



Лично я доволен итогами, поскольку это мой первый опыт в построении регрессий, и он привел к достаточно приемлемым результатам. Безусловно, хотелось бы их улучшить, поскольку я уверен, что с помощью построения моделей, даже таких простых, как эта, можно добиваться лучшего результата, чем просто оценивая данные на глаз. В рамках этой модели можно было бы, например, учесть тот фактор, что некоторые команды слабы в практиках, но выстреливают в квалификациях. Например, есть наблюдение, что Мерседес часто не был лучшей командой в ходе тренировок, но гораздо лучше выступал в квалификациях. Однако эти человеческие наблюдения не нашли отражения в модели. Поэтому в сезоне 2020, который начнется в июле (если не произойдет ничего непредвиденного), я хочу проверить эту модель в соревновании против живых прогнозистов, а также найти, как ее можно сделать лучше.

Помимо этого, я надеюсь вызвать отклик в комьюнити любителей Формулы-1 и верю, что с помощью обмена идеями мы сможем лучше понимать, из чего складываются результаты квалификаций и гонок, а это в конечном счете цель любого человека, который делает прогнозы.
Подробнее..

Из песочницы Разработка модели в PySpark ML на датасете с разными типами данных для ржавых чайников

06.10.2020 16:11:14 | Автор: admin
А ты уже умеешь работать с несколькими типами данных в PySpark ML? Нет? Тогда тебе срочно нужно к нам.

image

Всем привет! Хочу раскрыть подробно одну интересную, но, к несчастью, не встречающуюся тему в документации Spark: как обучать модель в PySpark ML на датасете с разными типами данных (строковыми и числовыми)? Желание написать данную статью было вызвано необходимостью в течение нескольких дней просматривать Интернет в поисках необходимой статьи с кодом, ведь в официальном туториале от Spark приведён пример работы не то что с признаками одного типа данных, а вообще с одним признаком, а информация, как работать с несколькими колонками тем более разных типов данных, там отсутствует. Однако, подробно изучив возможности PySpark для работы с данными, у меня получилось написать рабочий код и понять как всё происходит, чем хочу поделиться и с вами. Так что полный вперёд, друзья!

Первоначально давайте импортируем все необходимые библиотеки для работы, а потом подробно разберём код, чтобы любой уважающий себя ржавый чайник, как, впрочем, и я недавно, всё понял:

#импортируем необходимые библиотекиfrom pyspark.context import SparkContextfrom pyspark.sql.session import SparkSessionfrom pyspark.ml import Pipelinefrom pyspark.ml.feature import HashingTF, Tokenizerfrom pyspark.sql.functions import UserDefinedFunctionfrom pyspark.sql.types import *from pyspark.ml import Pipelinefrom pyspark.ml.feature import StringIndexer, VectorIndexerfrom pyspark.ml.evaluation import MulticlassClassificationEvaluatorimport pyspark.sql.functions as sffrom pyspark.ml.feature import OneHotEncoder, StringIndexer, VectorAssemblerfrom pyspark.ml import Pipelinefrom pyspark.ml.regression import GBTRegressor#other types of regression models#можно использовать и другие виды регрессии#from pyspark.ml.regression import LinearRegression#from pyspark.ml.regression import RandomForestRegressor#from pyspark.ml.regression import GeneralizedLinearRegression#from pyspark.ml.regression import DecisionTreeRegressorfrom pyspark.ml.feature import VectorIndexerfrom pyspark.ml.evaluation import RegressionEvaluator

Теперь создадим (локальный) спарковский контекст и спарковскую сессию и проверим, всё ли работает, выведя полученное на экран. Создание спарковской сессии является отправной точкой в работе с датасетами в Spark:

#создаём спарк сессеюsc = SparkContext('local')spark = SparkSession(sc)spark



Инструмент для работы с данными есть, теперь загрузим их. В статье используется датасет, который был взят с сайта соревнований по машинному обучению Kaggle:
https://www.kaggle.com/unitednations/international-greenhouse-gas-emissions
который после скачивания хранится в path_csv в формате .csv и имеет следующие опции:

  • header: если в нашем файле первая строка является заголовком, то ставим true
  • delimiter: ставим знак, разделяющий данные одной строки по признакам, зачастую это "," или ";"
  • inferSchema: если true, то PySpark автоматически определит тип каждой колонки, иначе вам придётся прописывать его самостоятельно

#загружаем данные формата .csv из path_csvpath_csv = 'greenhouse_gas_inventory_data_data.csv'data = spark.read.format("csv")\        .option("header", "true")\        .option("delimiter", ",")\        .option("inferSchema", "true")\        .load(path_csv)

Чтобы лучше понимать, с какими данными мы имеем дело, посмотрим на несколько их строк:

#посмотрим на часть данныхdata.show()


Также посмотрим сколько у нас всего строк в датасете:
#количество строк данныхdata.select('year').count()



И, наконец, выведем типы наших данных, которые, как мы помним, мы попросили PySpark определить автоматически с помощью option(inferSchema, true):

#посмотрим на типы всех наших колонокdata.printSchema()



Теперь переходим к нашему основному блюду работе с несколькими признаками разных типов данных. Spark может обучить модель на преобразованных данных, где предсказываемая колонка является вектором и колонки с признаками тоже вектор, что усложняет задачу Но мы не сдаёмся, и чтобы обучить модель в PySpark мы будем использовать Pipeline, в который мы передадим некий план действий (переменная stages):

  1. шаг label_stringIdx: мы преобразовываем колонку датасета value, которую мы хотим предсказывать, в спарковскую строку-вектор и переназываем на label с параметром handleInvalid = 'keep', означающий, что наша предсказываемая колонка поддерживает null
  2. шаг stringIndexer: преобразовываем строковые колонки в спарковские категориальные строки
  3. шаг encoder: преобразовываем категориальные колонки в бинарные (числовые) вектора благодаря строковому преобразователю
  4. шаг assembler: чтобы обучить модель в Spark, мы должны колонки с признаками преобразовать в один вектор, что можно достичь с помощью VectorAssembler(), который берёт на вход название численных (для этого мы и преобразовали строки в числа в предыдущем шаге) колонок (assemblerInputs) и преобразовываем все колонки в один вектор с именем features
  5. шаг gbt: в качестве модели регрессии из PySpark ML выбран GBTRegressor, потому что бустинг наше всё

#value - это зависимая и предсказываемая переменная - меткаstages = []label_stringIdx = StringIndexer(inputCol = 'value', outputCol = 'label', handleInvalid = 'keep')stages += [label_stringIdx]#depend on categorical columns: country and types of emission#зависит от категориаьных колонок: страны и категории загрязненияcategoricalColumns = ['country_or_area', 'category']for categoricalCol in categoricalColumns:    #преобразование категориальных колонок в бинарные вектора благодаря строковому преобразователю    stringIndexer = StringIndexer(inputCol = categoricalCol,                                  outputCol = categoricalCol + 'Index',                                  handleInvalid = 'keep')    encoder = OneHotEncoder(inputCol=stringIndexer.getOutputCol(),                            outputCol=categoricalCol + "classVec")    stages += [stringIndexer, encoder]#зависит от численной колонки: годаnumericCols = ['year']assemblerInputs = [c + "classVec" for c in categoricalColumns] + numericCols#преобразование нескольких колонок в вектор-колонку - признакиassembler = VectorAssembler(inputCols=assemblerInputs, outputCol="features")stages += [assembler]

Разделим наш датасет на тренировочную и тестовую выборку в любимом соотношении соотношении 70% к 30% соответственно и начнём тренировать модель с помощью градиентного регрессионого дерева бустинга (GBTRegressor), который должен предсказывать вектор label по признакам, ранее объединённым в один вектор features с ограничением по итерируемости maxIter=10:

#делим данные на обучающую и тестовую выборки (30% тестовая)(trainingData, testData) = data.randomSplit([0.7, 0.3])#тренируем модель (градиентного регрессионого дерева бустинга)gbt = GBTRegressor(labelCol="label", featuresCol="features", maxIter=10)stages += [gbt]# задаем план stages для обучения модели pipeline = Pipeline(stages=stages)

А теперь нам осталось только отправить компьютеру план действий и тренировочный датасет:

# тренируем модельmodel = pipeline.fit(trainingData)# делаем предсказания на тестовой выборкеpredictions = model.transform(testData)

Сохраним нашу модель, чтобы мы всегда могли вернуться к её использованию без повторного обучения:

#сохраняем модельpipeline.write().overwrite().save('model/gbtregr_model')

И если вы решили вновь начать использовать обученную модель для предсказаний, то просто напишите:

#загружаем модель для работы после обученияload_model = pipeline.read().load('model/gbtregr_model')


Итак, мы посмотрели, как в инструменте для работы с большими данными на языке Python, PySpark, реализуется работа с несколькими признаковыми колонками разных типов данных.

Теперь пора применить это в ваших моделях
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru