Повод

Пару месяцев назад в рамках сотрудничества Amazon и Formula 1 исследователи «с использованием мощностей облачных технологий» выкатили сравнение скорости пилотов всех времен и народов (ссылка). Естественно, материал был рекламно-хайповый, и цели своей он достиг. Во всем формульном мире еще пару дней только и было разговоров в духе «а почему в рейтинге нет пилота N?» и «да как M может быть быстрее K, если K сделал его в сезоне L». Мне стало интересно более-менее повторить это исследование и, по возможности, обойтись без «всей мощи облачных технологий».

оригинальный рейтинг

Задача

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

Как любят говорить в F1, пилот – это лишь 10% успеха в гонке, остальные 90% - машина и работа команды. Поэтому с хоть какой-то степенью достоверности можно сравнивать только напарников по команде между собой, т.к. это, в теории, нивелирует фактор машины. Здесь тоже всегда есть масса домыслов и слухов, что при необходимости команда может топить одного своего пилота, либо просто не иметь возможности привозить последние обновления сразу на обе машины и отдавать лучшее своему быстрейшему гонщику. Но, в рамках нашей задачи, будем считать болиды сокомандников одинаковыми.

Идея

В таком случае каждая квалификация для нас превращается в набор парных сравнений скорости пилотов в рамках каждой команды. К счастью, мир формульных гонок довольно мал, и «большие» пилоты успевают поездить сразу с несколькими напарниками в рамках разных команд. Таким образом, мы сможем получить сравнение 2 пилотов, никогда не выступавших в рамках одной команды, «через третьего» - пилота, с которым оба в разное время оказывались в одном коллективе. Тут так же есть очень серьезные допущения, которые портят малину. Это и допущение, что уровень пилота остается стабильным несколько лет подряд, и допущение, что машина всегда идеально подходит пилоту.

Еще один неприятный момент. Сравнить так уж прям всех пилотов за всю историю, к сожалению, не удастся. Даже используя все «попарные» мостики, не удастся добраться до всех пилотов, выступавших в Ф1. Возможны как отдельные пары пилотов, всю карьеру выступавшие только в одной команде друг с другом, так и более сложные закрытые кластера пилотов, не пилотировавшие ни с кем из общего кластера одновременно в одной команде.

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

Т.о. скорость прохождения круга в квалификации V = V0*kp*kc*Tr, где kc – такой же коэффициент скорости болида конкретной команды в конкретной гонке, а Tr – некая константа, характерная для трассы в конкретных условиях.

После серии преобразований можно вывести, что в каждой квалификации ln(ki)-ln(kj)=ln(tj)-ln(ti) для болидов гонщиков одной команды.

Получаем систему линейных уравнений. Систему довольно просто представить как матрицу m*(n+1), где n – число пилотов, m – суммарное число гонщиков в квалификациях. В каждой строке есть коэффициенты 1 и -1 в столбцах, соответствующих пилотам, и логарифм обратного отношения их времен в столбце свободных коэффициентов.

В отличие от специалистов AWS, мы посчитаем коэффициент динамически, считая, что он остается стабильным только в течение одного-двух лет. Для того, чтобы прокинуть мостик между соседними годами, искусственно внесем в таблицу результаты «из прошлого» - поменяем лишь год для пилота на предыдущий, остальные столбцы оставив неизменными. Т.о. мы получим «замыленные» на перспективу 2 лет данные для каждого пилота и поставим его прошлого к ему теперешнему в напарники с 0 разницей в скорости.

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

 Здесь мы будем фиксировать пилота и трассу и смотреть, насколько на одной машине он прошел ее быстрее, чем на другой в перспективе 2 лет. У этого подхода есть 3 больших минуса. 1 – машина может быть быстрой, но не подходить пилоту. Мы же увидим, что она «хуже». Второй – трассы постоянно ротируются. Добавляются новые, уходят либо перестраиваются старые. Нам же для этого подхода нужен некоторый набор стабильных трасс. И третий – дождь. Если в один год он был, а в другой нет, то мы можем списать это на скорость машины.

Решение

Данные квалификаций спарсим из Википедии, откуда еще. Формат квалификации менялся много раз, в разные периоды она состояла из 1, 2 и как теперь 3 фаз. Хоть это и не до конца верно, будем считать, что итоговое время пилота было показано в последней доступной фазе квалификации.

import sys
import re
import urllib.request


def get_wikipedia_page(title, lang='en'):
    url = 'https://'+lang+'.wikipedia.org/wiki/'+(title.replace(' ', '_'))
    fp = urllib.request.urlopen(url)
    mybytes = fp.read()
    
    mystr = mybytes.decode("utf8")
    fp.close()
    return mystr

title = 'List of Formula One Grands Prix'

try:
    print('process: '+title)
    th = get_wikipedia_page(title)
    r1 = re.findall(r'href="/wiki/[\d][\d][\d][\d]_[\w]*_Grand_Prix"',th)
    list_of_GP = list(set(r1))
except:
    print("Unexpected error:", sys.exc_info()[0])

titles = list(map(lambda x: x[12: -1].replace('_', ' '), list_of_GP))


for title in titles:
    try:
        print('process: '+title)
        th = get_wikipedia_page(title)
        with open('texts/'+title+'.txt', 'w', encoding='utf8') as the_file:
            the_file.write(th)
            the_file.close()
    except:
        print("Unexpected error:", sys.exc_info()[0])

Из полученных html файлов регулярками выпарсим квалификационные таблицы. Также слегка стандартизируем текстовую часть данных. Приведем названия треков к одному виду (иногда встречаются несколько написаний одного и того же трека), сохраним его длину (это поможет различить разные версии одного и того же трека), поудаляем цифры и прочий «мусор» из имен пилотов. Сохраним итоговые данные в удобную csv.

Считаем полученный DataFrame. Создадим несколько столбцов, которые сильно нам помогут в дальнейшем:

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

Имя пилота + год – будем считать, что в течение 1 года пилот сохраняет один и тот же уровень формы, в течение 2 лет изменяет его незначительно

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

qual_df = pd.read_csv('qual_results.csv')

qual_df['Track_pl_len'] = qual_df['Track'] + '_' +qual_df['Track_len'].apply(str)
qual_df['Car'] = qual_df['Constructor'] + '_' +qual_df['Year'].apply(str)
qual_df['Driver_pl_year'] = qual_df['Driver']+'_'+qual_df['Year'].apply(str)

qual_df_2 = qual_df.copy()
qual_df_2['Driver_pl_year'] = qual_df_2['Driver']+'_'+((qual_df_2['Year'].apply(int)-1)).apply(str)
double_df = pd.concat([qual_df, qual_df_2])
del qual_df2

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

А дальше произведем небольшую манипуляцию с данными. Произведем 2 раза для разных рейтингов, но подробно разберу для рейтинга пилотов.

  • Посчитаем логарифм из времени на круге

  • Разность логарифмов времен на квалификационном круге двух сокомандников равна обратной разности логарифмов коэффициентов их скорости, т.к. все остальные переменные считаем одинаковыми и зафиксированными

  • Соответственно, просто мерджим 2 одинаковых датафрейма между собой по машине и гонке – и получаем большую простыню нужных нам попарных сравнений

  • Фильтруем полученный датасет, чтобы каждый «мостик» из сравнения двух пилотов был достаточно «надежен» - я выставил лимит в 6 и более совместных квалификаций и разницу в скорости не более 2%. Также оставляем только пилотов, отметившихся в «главной» линии пилотов Ф1 – в той, что были основные чемпионы. Некоторое количество пилотов просто не пересекались с ними ни через кого в одной команде, поэтому узнать их скорость этим методом невозможно

  • Выворачиваем 2 столбца с именами пилотов One Hot Encoding так, чтобы пилот x оказался с коэффициентом = 1 в столбце со своей фамилией, пилот y с -1

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

  • Вытаскиваем посчитанные коэффициенты линейной регрессии. По нашей задумке они будут равны ln(k) - логарафму от коэффициента скорости пилота.

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

Результаты

Прежде чем подводить итоги по тому методу, по которому считал я, хочу все же показать результаты «карьерного» измерения, т.е. то, что презентовали F1 и Amazon.

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

Последний столбец пересчитал исходя из того, чтобы точкой отсчета был Сенна

Это 18 первых пилотов, если составлять рейтинг без учета динамики по ходу карьеры. Опять же, параметров довольно много + ошибки парсинга + оговорки, какое время брать в многоуровневых квалификациях. Серым я затемнил пилотов, которых нет в официальной лучшей 10. Если их не учитывать, то рейтинги очень похожи. Все те же люди на похожих расстояниях. Ниже Ферстаппен, выше Феттель.

Самое интересное здесь – как в топе появился перспективный дебютант Рассел. Появился за счет 2019 года, в котором разнес вернувшегося после кучи пропущенных лет и тяжелейшей травмы некогда быстрого поляка Роберта Кубицу. Роберт весь год жаловался на машину, воевал с командой и на следующий год ушел из Ф1.

Сравнивать с ним Рассела – некорректно. Столь длинное отсутствие не могло не сказаться на форме Кубицы. В сравнении же мы считаем, что это одинаково быстрый пилот. Видимо, так же посчитали аналитики Ф1 и подчистили рейтинг. Как из него исчезли сверстабильные Боттас и Риккьярдо для меня загадка. Много лет в Ф1, точно пересекались с Хэмильтоном и Ферстаппеным и смотрелись/смотрятся на их фоне отлично.

А вот что получается, если построить рейтинг год к году:

N

Driver

Total min

1

Ayrton Senna

-     0,435

2

Michael Schumacher

-     0,408

3

Alain Prost

-     0,289

4

Damon Hill

-     0,037

5

Lewis Hamilton

-     0,037

6

Charles Leclerc

       0,016

7

Rubens Barrichello

       0,024

8

Fernando Alonso

       0,067

9

Nico Rosberg

       0,081

10

Nigel Mansell

       0,102

11

Carlos Pace

       0,117

12

Mika Häkkinen

       0,145

13

Max Verstappen

       0,147

14

Valtteri Bottas

       0,153

15

Elio de Angelis

       0,164

16

Daniel Ricciardo

       0,165

17

Jarno Trulli

       0,172

18

Giancarlo Fisichella

       0,184

 И расширить его в перспективе времени (для всех пилотов здесь первый отмеченный год предваряет дебют или возвращение после перерыва, т.е. фактически в Ф1 пилота еще не было):

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

И такой же рейтинг для машин:

Мерседес 2020 года – быстрейшая машина не только чемпионата, но и за всю историю (формально – с 1979 года, дальше проследить связи между конкретными машинами тяжело ввиду изменений в календаре).

На графике бросается в глаза, как Renault сбросило секунду за счет нового двигателя не только на заводских машинах, но и на клиентах. Невероятно быстрый Racing Point, сбросивший почти 2 сек за межсезонье, Alpha Tauri 2020, неприлично похожая на Red Bull 2019 и провал двигателя Ferrari 2020, из-за которого команда откатилась на уровень 2018 года вместе со своими клиентами.

Интересно, что по этой графике быстрейшей машиной 2019 года была не Mercedes, которая взяла 10 поулов, а Red Bull и только более быстрый Хэмилтон позволял команде завоевывать поулы и титул. Леклер же, который взял поулов больше, чем Хэмилтон и Боттас каждый, творил просто чудеса, не вписывающиеся в модель.

Здесь во всей красе проявляется несовершенство самого метода попарных сравнений. Если у пилота есть длительные периоды стабильных выступлений с одним и тем же сокомандником, построить по нему статистику крайне тяжело. Сравнения получаются через много-много лет и пилотов, а каждое лишнее звено добавляет неточности. То же самое касается рейтинга машин. Чудо, что он вообще строится и выглядит адекватно.

Поскольку абсурдность и низкое качество полученной модели уже понятны, приложу ее предсказания на предстоящий этап в Имоле (результаты Уильямсов предсказать не получится, т.к. в команде 2 молодых пилота, не пересекавшихся с остальными пилотами в Ф1):

Driver

Car

 Time_predicted_sec

Lewis Hamilton

Mercedes

                        77,711

Valtteri Bottas

Mercedes

                        77,850

Max Verstappen

Red Bull Racing-Honda

                        78,252

Lando Norris

McLaren-Renault

                        78,324

Sergio Pérez

Racing Point-BWT Mercedes

                        78,345

Lance Stroll

Racing Point-BWT Mercedes

                        78,439

Daniel Ricciardo

Renault

                        78,451

Carlos Sainz Jr.

McLaren-Renault

                        78,549

Esteban Ocon

Renault

                        78,665

Alexander Albon

Red Bull Racing-Honda

                        78,878

Pierre Gasly

AlphaTauri-Honda

                        78,985

Daniil Kvyat

AlphaTauri-Honda

                        79,108

Charles Leclerc

Ferrari

                        79,116

Sebastian Vettel

Ferrari

                        79,531

Romain Grosjean

Haas-Ferrari

                        79,656

Kevin Magnussen

Haas-Ferrari

                        79,738

Kimi Räikkönen

Alfa Romeo Racing-Ferrari

                        80,399

Antonio Giovinazzi

Alfa Romeo Racing-Ferrari

                        80,658

Выводы

Формула 1 и Amazon добились своей цели и привлекли внимание к себе. Так же они подкинули интересную задачку: как разделить влияние пилота и машины в таком своеобразном виде спорта.

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

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

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

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

Ссылка на Github