Обработка данных NBA за 30 лет с помощью MongoDB Aggregation

в 13:30, , рубрики: big data, mongodb, Администрирование баз данных, анализ данных, Блог компании ITinvest, Майкл Льюс, НБА, статистика

Прим. перев.: Американский писатель Майкл Льюис известен не только своими историями о трейдерах с Уолл Стрит, но и (в первую очередь) книгой Moneyball, по которой впоследствии был снят одноименный фильм («Человек, который изменил все»). Главный ее герой – Билли Бин, генеральный менеджер бейсбольной команды «Oakland Athleticks», создает конкурентоспособную команду исключительно на основе анализа статистических показателей игроков.

Памятуя об этом, мы решили опубликовать один любопытный материал о том, к каким интересным и нетривиальным выводам можно прийти, анализируя публично доступную статистику игр NBA за последние 30 лет с помощью фреймворка MongoDB Aggregation. Несмотря на то, что в данном примере автор анализирует показатели команд в целом, а не статистику по отдельным игрокам (она также находится в открытом доступе), он приходит к весьма занимательным выводам – руководствуясь его выкладками вполне реально провести самостоятельный анализ, подобно тому, как в свое время поступили герои Moneyball.

image

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

Будучи с детства баскетбольным фанатом, я всегда мечтал научиться проводить комплексную аналитику статистики NBA. Когда настало время хакатона MongoDB Driver Days, и ведущий Ruby-инженер Гэри Мураками предложил составить интересный массив данных, мы сели и с полудня до вечера писали и запускали скрепер [англ. scraper, программа для извлечения данных с веб-страниц] для сайта basketball-reference.com. Полученный массив данных состоял из итогового счета и статистики игроков для каждого матча регулярного чемпионата NBA начиная с сезона 1985-1986 годов.

В документации к фреймворкам агрегирования [данных] часто используются массивы данных почтовых индексов, демонстрирующие способы использования подобного фреймворка. Однако обработка данных о населении США не сильно поражает мое воображение, и есть определенные форматы использования фреймворка агрегирования, которые не получится проиллюстрировать на примере массива почтовых индексов. Надеюсь, этот массив данных позволить вам по-новому взглянуть на фреймворк агрегирования, а вам понравится разбираться в статистике NBA. Вы можете скачать массив данных здесь и вставить его в свой экземпляр MongoDB с помощью команды mongorestore.

Исследуем данные

Для начала давайте рассмотрим структуру данных. Начиная с сезона 1985-86, в регулярном чемпионате NBA было сыграно 31 686 игр. Каждый отдельный документ представляет собой сведения об одной игре. Ниже представлены метаданные матча открытия сезона 1985-86 между командами «Washington Bullets» и «Atlanta Hawks», как показано в RoboMongo, универсальном графическом интерфейсе MongoDB:

image

В состав документа входят вложенный подраздел с подробной статистикой матча, поле даты и информация об игравших командах. Можно увидеть, что «Bullets» одержали победу на выезде со счетом 100-91. Данные статистики матча (box score) похожим образом разбиты по командам в массиве, начиная с победившей. Обратите внимание, что флажок «won» является элементом верхнеуровневого объекта box score наряду с элементами «team» и «players».

image

Далее статистика игры разбивается на статистику команды и статистику игроков. Статистика команды на рисунке выше отображает общие данные по команде «Atlanta Hawks», согласно которым они реализовали 41 из 92 бросков с игры и забросили всего лишь 9 из 18 мячей с линии штрафной. В массиве данных игроков представлена та же информация, но распределенная по отдельным игрокам. К примеру, ниже вы увидите, что звезда «Hawks» Доминик Уилкинс набрал 32 очка при 15 результативных бросках с игры из 29 и записал на свой счет 3 перехвата.

image

Проведение агрегирования

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

Прежде чем начать серьезные вычисления, предлагаю запустить простую проверку работоспособности системы и вычислить 5 команд, одержавших наибольшее количество побед в сезоне 1999-2000 годов. Эти команды можно определить, следуя цепочке операций из 6 этапов:

  1. Используем оператор $match, чтобы работать дальше только с играми, проходившими в период между 1 августа 1999 года и 1 августа 2000 года – две даты, отстоящие достаточно далеко от любой из игр NBA и надежно ограничивающие этот сезон.
  2. Используем оператор $unwind, чтобы сгенерировать по одному документу для каждой команды в матче.
  3. Снова применяем оператор $match, чтобы отсеять только победившие команды.
  4. Используем оператор $group, чтобы посчитать, сколько раз данная команда появляется в качестве результата на шаге 3.
  5. Используем оператор $sort для сортировки количества побед по убыванию.
  6. Применяем оператор $limit, чтобы отобрать 5 команд с наибольшим числом побед.

Итоговая шелл-команда представлена ниже. Эта команда на моем ноутбуке выполняется в режиме реального времени даже при отсутствии в базе индексов, так как в выборке всего 31 686 документов.

db.games.aggregate([
  {
    $match : {
      date : {
        $gt : ISODate("1999-08-01T00:00:00Z"),
        $lt : ISODate("2000-08-01T00:00:00Z")
      }
    }
  },
  {
    $unwind : '$teams'
  },
  {
    $match : {
      'teams.won' : 1
    }
  },
  {
    $group : {
      _id : '$teams.name',
      wins : { $sum : 1 }
    }
  },
  {
    $sort : { wins : -1 }
  },
  {
    $limit : 5
  }
]);

Этот простой пример можно обобщить для того, чтобы ответить на вопрос, какая из команд одержала больше всего побед в промежутке между сезонами 2000-2001 и 2009-2010 годов, заменяя на шаге использования функции $match время проведения игр на период между 1 августа 2000 года и 1 августа 2010 года. Выясняется, что «San Antonio Spurs» одержали в этот период 579 побед, ненамного обойдя «Dallas Mavericks» с 568 победами.

db.games.aggregate([
  {
    $match : {
      date : {
        $gt : ISODate("2000-08-01T00:00:00Z"),
        $lt : ISODate("2010-08-01T00:00:00Z")
      }
    }
  },
  {
    $unwind : '$teams'
  },
  {
    $match : {
      'teams.won' : 1
    }
  },
  {
    $group : {
      _id : '$teams.name',
      wins : { $sum : 1 }
    }
  },
  {
    $sort : { wins : -1 }
  },
  {
    $limit : 5
  }
]);

Определение связи статистики с числом побед

Теперь сделаем кое-что немного более интересное, используя пару операторов агрегирования, которые нечасто встретишь при анализе массивов данных почтовых индексов: оператор $gte и оператор $cond на этапе $project. Используем эти операторы, для того чтобы вычислить, как часто побеждает команда, которая сделала больше подборов в защите, чем их противники, на всем массиве данных.

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

  1. Используем оператор $unwind для получения документа с подробной статистикой по каждой команде в данной игре.
  2. Используем операторы $project и $cond, чтобы преобразовать каждый документ, так что общее число подборов команды в защите будет выражено отрицательным значением, если она проиграла: информация о результатах игры определяется по флажку «won».
  3. Используем операторы $group и $sum, чтобы подсчитать общее количество подборов в каждой игре. Так как в результате предыдущего этапа общее число подборов проигравшей команды стало отрицательным, то теперь в каждом документе есть разность между количеством подборов в защите победившей и проигравшей команд.
  4. Используем операторы $project и $gte для создания документа, в котором будет содержаться флажок winningTeamHigher, принимающий значение «true», если победившая команда сделала больше подборов в защите, чем проигравшая.
  5. Используем операторы $group и $sum, для того, чтобы вычислить, в скольких играх флажок winningTeamHigher принимал значение «true».

db.games.aggregate([
  {
    $unwind : '$box'
  },
  {
    $project : {
      _id : '$_id',
      stat : {
        $cond : [
          { $gt : ['$box.won', 0] },
          '$box.team.drb',
          { $multiply : ['$box.team.drb', -1] }
        ]
      }
    }
  },
  {
    $group : {
      _id : '$_id',
      stat : { $sum : '$stat' }
    }
  },
  {
    $project : {
      _id : '$_id',
      winningTeamHigher : { $gte : ['$stat', 0] }
    }
  },
  {
    $group : {
      _id : '$winningTeamHigher',
      count : { $sum : 1 }
    }
  }
]);

Результат получился довольно любопытным: команда, записавшая на свой счет больше подборов в защите, побеждала в 75% случаев. Для сравнения, команда, на счету которой больше бросков с игры, чем у другой команды, выигрывает лишь в 78,8% случаев! Попробуйте переписать алгоритм агрегирования для других показателей, таких как броски с игры, трехочковые, количество потерь и др. Вы получите ряд довольно интересных результатов. Количество подборов в нападении, как оказывается, не стоит использовать для предсказания результата игры, так как команда, забравшая больше подборов в нападении, победила всего в 51% случаев. Оказывается, что по количеству трехочковых бросков предсказать победителя можно гораздо точнее: команда с большим числом трехочковых попаданий победила в 64% случаев.

Сопоставляем подборы в защите и общее число подборов с процентом побед

Давайте проанализируем данные, для которых будет интересно нарисовать график. Будем вычислять процент побед команды как функцию от количества реализованных подборов в защите. Такую агрегацию провести довольно легко: все, что нужно сделать – это выполнить оператор $unwind для статистики матча и использовать $group, чтобы посчитать среднее число флажков «won» по всем значениям общего количества подборов в защите.

db.games.aggregate([
  {
    $unwind : '$box'
  },
  {
    $group : {
      _id : '$box.team.drb',
      winPercentage : { $avg : '$box.won' }
    }
  },
  {
    $sort : { _id : 1 }
  }
]);

Если визуализировать результаты агрегирования, то можно получить неплохой график, четко показывающий довольно высокую корреляцию между числом подборов в защите и процентом побед. Любопытный факт: командой, забравшей наименьшее количество подборов в случае победы, были «Toronto Raptors» в сезоне 1995-96 годов, обыгравшие «Milwaukee Bucks» со счетом 93-87 26 декабря 1995 года, несмотря на всего лишь 14 подборов в защите, сделанных в этой игре.

image

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

db.games.aggregate([
  {
    $unwind : '$box'
  },
  {
    $group : {
      _id : '$box.team.trb',
      winPercentage : { $avg : '$box.won' }
    }
  },
  {
    $sort : { _id : 1 }
  }
]);

И он действительно меняется! Где-то после результата в 53 общих подбора положительная корреляция между общим числом подборов и процентом побед полностью исчезает! Корреляция здесь явно не такая высокая, как в случае с подборами в защите. Кстати сказать, «Cleveland Cavaliers» взяли верх над «New York Knicks» со счетом 101-97 11 апреля 1996 года, несмотря на то, что в общем они забрали всего 21 подбор. С другой стороны, «San Antonio Spurs» уступили «Houston Rockets» со счетом 112-110 4 января 1992 года при том, что их общее число подборов составило 75.

Заключение

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

Автор: itinvest

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js