Обробка даних NBA за 30 років з допомогою MongoDB Aggregation

Прим. перекл.: Американський письменник Майкл Льюїс відомий не тільки своїми історіями про трейдерах з Уолл-Стріт, але й (у першу чергу) книгою 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 м'ячів з лінії штрафної. В масиві даних гравців представлена та ж інформація, але розподілена по окремим гравцям. Наприклад, нижче ви побачите, що зірка «Яструбів» Домінік Уілкінс набрав 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.

Висновок

Я сподіваюся, цей пост вас зацікавив темою фреймворків агрегування так само, як і мене. Ще раз нагадаю, що ви можете завантажити масив даних тут — я настійно рекомендую вам попрацювати з цими даними самостійно.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.