Візуалізація статистики ЄВРО-2016 за допомогою Python і Inkscape


Привіт, Хабр!

Минуло трохи більше тижня до закінчення Чемпіонату Європи 2016 у Франції. Цей чемпіонат запам'ятається нам невдалим виступом збірної Росії, виявленої волею збірної Ісландії, приголомшливою грою збірних Франції і Португалії. У цій статті ми попрацюємо з даними, побудуємо декілька графіків і відредагуємо їх у векторному редакторі Inkscape. Кому цікаво — прошу під кат.

Зміст
  1. Робота з даними API
  2. Візуалізація даних за допомогою Python
  3. " Редагування векторної графіки Inkscape


1. Робота з даними API
Щоб візуалізувати футбольні дані спочатку їх необхідно отримати. Для цих цілей ми будемо використовувати API football-data.org. Нам доступні наступні методи:
  • Список змагань — «api.football-data.org/v1/competitions/?season={year}»
  • Список команд (ідентифікатора змагання) — «api.football-data.org/v1/competitions/{competition_id}/teams»
  • Список матчів (ідентифікатора змагання) — «api.football-data.org/v1/competitions/{competition_id}/fixtures»
  • Інформація про команду — «api.football-data.org/v1/teams/{team_id}»
  • Список футболістів (ідентифікатора команди) — «api.football-data.org/v1/teams/{team_id}/players»
Докладна інформація про всіх методах знаходиться в документації API.
Давайте тепер спробуємо попрацювати з даними. Для початку спробуємо вивчити структуру повернутих даних для методу «competitions» (список змагань):
[{'_links': {'fixtures': {'href': 'http://api.football-data.org/v1/competitions/424/fixtures'},
'leagueTable': {'href': 'http://api.football-data.org/v1/competitions/424/leagueTable'},
'self': {'href': 'http://api.football-data.org/v1/competitions/424'},
'teams': {'href': 'http://api.football-data.org/v1/competitions/424/teams'}},
'caption': 'European Championships France 2016',
'currentMatchday': 7,
'id': 424,
'lastUpdated': '2016-07-10T21:32:20Z',
'league': 'EC',
'numberOfGames': 51,
'numberOfMatchdays': 7,
'numberOfTeams': 24,
'year': '2016'},
{'_links': {'fixtures': {'href': 'http://api.football-data.org/v1/competitions/426/fixtures'},
'leagueTable': {'href': 'http://api.football-data.org/v1/competitions/426/leagueTable'},
'self': {'href': 'http://api.football-data.org/v1/competitions/426'},
'teams': {'href': 'http://api.football-data.org/v1/competitions/426/teams'}},
'caption': 'Premiere League 2016/17',
'currentMatchday': 1,
'id': 426,
'lastUpdated': '2016-06-23T10:42:02Z',
'league': 'PL',
'numberOfGames': 380,
'numberOfMatchdays': 38,
'numberOfTeams': 20,
'year': '2016'},

Перш ніж почати працювати з Python, нам знадобиться зробити необхідні імпорти.
import datetime
import random
re import
import os
import requests
import json
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
pd.set_option('display.width', 1100)
plt.style.use('bmh')
DIR = os.path.dirname('__File__')
font = {'family': 'Verdana', 'weight': 'normal'}
rc('font', **font)

Структура даних зрозуміла, спробуємо завантажити дані про змагання за 2015-2016 рік в pandas.DataFrame. Паралельно будемо зберігати дані, з якими будемо працювати .csv файли, оскільки безкоштовно можна робити лише 50 запитів до API в день. Обмеження також накладаються і на період тимчасової даних — безкоштовно нам доступні лише 2015-2016 роки.
competitions_url = 'http://api.football-data.org/v1/competitions/?season={year}'
data = []
for y in [2015, 2016]:
response = requests.get(competitions_url.format(year=y))
competitions = json.loads(response.text)
competitions = [{"caption": c['caption'], 'id': c['id'], 'league': c['league'], 'year': c['year'],
'games_count': c['numberOfGames'], 'teams_count': c['numberOfTeams']} for c in competitions]
COMP = pd.DataFrame(competitions)
data.append(COMP)
COMP = pd.concat(data)
COMP.to_csv(os.path.join(DIR, 'input', 'competitions_2015_2016.csv'))
COMP.head(20)

Відмінно! Ми бачимо чемпіонат Європи 2016 у списку:






caption games_count id league teams_count year 12 League One 2015/16 552 425 EL1 24 2015 0 European Championships France 2016 51 424 EC 24 2016
Тепер ми можемо отримати список команд за ідентифікатором (id) цих змагань — використовуємо метод teams для отримання списку команд Євро-2016.
teams_url = 'http://api.football-data.org/v1/competitions/{competition_id}/teams'
response = requests.get(teams_url.format(competition_id=424))
teams = json.loads(response.text)['teams']
teams = [dict(code=team['code'], name=team['name'], flag_url=team['crestUrl'], players_url=team['_links']['players']['href'])
for team in teams]
TEAMS = pd.DataFrame(teams)
TEAMS.to_csv(os.path.join(DIR, 'input', 'teams_euro2016.cvs'))
TEAMS.head(24)









code flag_url name players_url 0 FRA upload.wikimedia.org/wikipedia/en/c/c3... France api.football-data.org/v1/teams/773/players 1 ROU upload.wikimedia.org/wikipedia/commons... Romania api.football-data.org/v1/teams/811/players 2 ALB upload.wikimedia.org/wikipedia/commons... Albania api.football-data.org/v1/teams/1065/pla... 3 SUI upload.wikimedia.org/wikipedia/commons... Switzerland api.football-data.org/v1/teams/788/players 4 WAL upload.wikimedia.org/wikipedia/commons... Wales api.football-data.org/v1/teams/833/players
Структура нашої таблиці таблиця (DataFrame) представлена з наступними стовпчиками:
  • code (тризначний код країни)
  • flag_url (посилання на .svg-файл прапора країни в нашому випадку, а в оригіналі crestUrl — .svg-файл символу команди)
  • name (назва команди)
  • players_url посилання на сторінку API з даними про гравців — чомусь даних про гравців збірних ні, можливо, це ще одне обмеження API)
В нашій подальшій роботі нам також знадобляться .svg файли прапорів команд — просто скачати їх в окрему директорію, я зробив це швидко і невимушено з допомогою розширення Chrono для Chrome.

Тепер спробуємо отримати дані про матчах Євро-2016. Спочатку знову подивимося структуру відповіді сервера для вибраного методу «competitions». Для прикладу розглянемо структуру інформації про матчі, що завершився серією пенальті (це максимально складний склад даних з можливих для цього методу).
games_url = 'http://api.football-data.org/v1/competitions/{competition_id}/fixtures'
response = requests.get(games_url.format(competition_id=424))
games = json.loads(response.text)['fixtures']
# Для прикладу розглянемо структуру інформації про матчі, що завершився серією пенальті.
games_selected = [game for game games in if 'extraTime' in game['result']]
games_selected[0]

У відповіді отримуємо наступне:
{'_links': {'awayTeam': {'href': 'http://api.football-data.org/v1/teams/794'},
'competition': {'href': 'http://api.football-data.org/v1/competitions/424'},
'homeTeam': {'href': 'http://api.football-data.org/v1/teams/788'},
'self': {'href': 'http://api.football-data.org/v1/fixtures/150457'}},
'awayTeamName': 'Poland',
'date': '2016-06-25T13:00:00Z',
'homeTeamName': 'Switzerland',
'matchday': 4,
'result': {'extraTime': {'goalsAwayTeam': 1, 'goalsHomeTeam': 1},
'goalsAwayTeam': 1,
'goalsHomeTeam': 1,
'halfTime': {'goalsAwayTeam': 1, 'goalsHomeTeam': 0},
'penaltyShootout': {'goalsAwayTeam': 5, 'goalsHomeTeam': 4}},
'status': 'FINISHED'}

Для полегшення роботи з даними і процесу завантаження їх в DataFrame напишемо невелику функцію, яка в якості атрибута приймає словник з інформацією про матчі і повертає словник зручного для нас вигляду.
Функція обробки словника матчу
def handle_game(game):
date = game['date']
team_1 = game['homeTeamName']
team_2 = game['awayTeamName']
matchday = game['matchday']
team_1_goals_main = game['result']['goalsHomeTeam']
team_2_goals_main = game['result']['goalsAwayTeam']
status = game['status']
if 'extraTime' in game['result']:
team_1_goals_extra = game['result']['extraTime']['goalsHomeTeam']
team_2_goals_extra = game['result']['extraTime']['goalsAwayTeam']
if 'penaltyShootout' in game['result']:
team_1_goals_penalty = game['result']['penaltyShootout']['goalsHomeTeam']
team_2_goals_penalty = game['result']['penaltyShootout']['goalsAwayTeam']
else:
team_1_goals_penalty = team_2_goals_penalty = 0
else:
team_1_goals_extra = team_2_goals_extra = team_1_goals_penalty = team_2_goals_penalty = 0
team_1_goals = team_1_goals_main + team_1_goals_extra
team_2_goals = team_2_goals_main + team_2_goals_extra
if (team_1_goals + team_1_goals_penalty) > (team_2_goals + team_2_goals_penalty):
team_1_win = 1
team_2_win = 0
draw = 0
elif (team_1_goals + team_1_goals_penalty) < (team_2_goals + team_2_goals_penalty):
team_1_win = 0
team_2_win = 1
draw = 0
else:
team_1_win = team_2_win = 0
draw = 1
game = dict(date=date, team_1=team_1, team_2=team_2, matchday=matchday, status=status,
team_1_goals=team_1_goals, team_2_goals=team_2_goals,
team_1_goals_extra=team_1_goals_extra, team_2_goals_extra=team_2_goals_extra,
team_1_win=team_1_win, team_2_win=team_2_win, draw=draw,
team_1_goals_penalty=team_1_goals_penalty, team_2_goals_penalty=team_2_goals_penalty)
return game

# Відразу спробуємо використовувати функцію на прикладі нашої запису з пенальті
game = handle_game(games_selected[0])
print(game)


Ось як виглядає зворотний словник:
{'date': '2016-06-25T13:00:00Z',
'draw': 0,
'matchday': 4,
'status': 'FINISHED',
'team_1': 'Switzerland',
'team_1_goals': 2,
'team_1_goals_extra': 1,
'team_1_goals_penalty': 4,
'team_1_win': 0,
'team_2': 'Poland',
'team_2_goals': 2,
'team_2_goals_extra': 1,
'team_2_goals_penalty': 5,
'team_2_win': 1}

Тепер у нас все готово для завантаження даних про всіх матчах Євро-2016 в DataFrame.
games_url = 'http://api.football-data.org/v1/competitions/{competition_id}/fixtures'
response = requests.get(games_url.format(competition_id=424))
games = json.loads(response.text)['fixtures']
GAMES = pd.DataFrame([handle_game(g) for g in games])
GAMES.to_csv(os.path.join(DIR, 'input', 'games_euro2016.csv'))
GAMES.head()






date draw matchday status team_1 team_1_goals team_1_goals_extra team_1_goals_penalty team_1_win team_2 team_2_goals team_2_goals_extra team_2_goals_penalty team_2_win 0 2016-06-10T19:00:00Z 0 1 FINISHED France 2 0 0 1 Romania 1 0 0 0 1 2016-06-11T13:00:00Z 0 1 FINISHED Albania 0 0 0 0 Switzerland 1 0 0 1
Відмінно, дані у нас завантажені в DataFrame, але така структура не підходить для аналізу даних. Керуючись принципами документа «Tidy Data, Hadley Wickham (2014)», скоригуємо структуру DataFrame таким чином, щоб одна змінна (команда) була тотожна одному рядку — фактично кількість рядків Dataframe ми збільшимо вдвічі. Також скоригуємо назви стовпців, щоб з ними було простіше працювати.
# Крім усього іншого наведемо стовпець дати до формату дати.
GAMES['date'] = pd.to_datetime(GAMES.date)
# Для початку розташуємо стовпці в зручному порядку і скорегуємо їх назви (скоротимо)
GAMES = GAMES.reindex(columns=['date', 'matchday', 'status',
'team_1', 'team_1_win', 'team_1_goals', 'team_1_goals_extra', 'team_1_goals_penalty',
'team_2', 'team_2_win', 'team_2_goals', 'team_2_goals_extra', 'team_2_goals_penalty',
'draw'])
new_columns = ['date', 'mday', 'status', 't1', 't1w', 't1g', 't1ge', 't1gp', 't2', 't2w', 't2g', 't2ge', 't2gp', 'draw']
GAMES.columns = new_columns
GAMES.head()

# Тепер створимо підсумковий DataFrame
# Створимо копію DataFrame з іграми, перетасувавши стовпці, і об'єднаємо його з оригінальним DataFrame.
GAMES_2 = GAMES.ix[:,[0, 1, 2, 8, 9, 10, 11, 12, 3, 4, 5, 6, 7, 13]]
GAMES_2.columns = new_columns
GAMES_F = pd.concat([GAMES, GAMES_2])
GAMES_F.sort(['date'], оперативне=True)
GAMES_F.head()







date mday status t1 t1w t1g t1ge t1gp t2 t2w t2g t2ge t2gp draw 0 2016-06-10 19:00:00 1 FINISHED France 1 2 0 0 Romania 0 1 0 0 0 0 2016-06-10 19:00:00 1 FINISHED Romania 0 1 0 0 France 1 2 0 0 0 1 2016-06-11 13:00:00 1 FINISHED Albania 0 0 0 0 Switzerland 1 1 0 0 0
Така структура куди краще, але для подальшої роботи нам знадобиться ще кілька невеликих змін.
# Для зручності відображення інформації в таблиці додамо ще один стовпець - "g" - загальна кількість забитих голів за матч
GAMES_F['g'] = GAMES_F.t1g + GAMES_F.t2g
GAMES_F['idx'] = GAMES_F.index

# Для зручності відображення деяких графіків додамо ще дані про стадії того чи іншого матчу.
# Ми знаємо, що всього матчів було 51, з них 15 матчів плей-офф, решта - груповий етап.
# Давайте додамо цю інформацію в DataFrame
TP = pd.DataFrame({'typ': ['Групи']*36 + ['1/8']*8 + ['1/4']*4 + ['1/2']*2 + ['Фінал']*1,
'idx': range(0, 51)})

# Сформуємо підсумковий DataFrame
GAMES_F= pd.merge(GAMES_F, TP, how='left', left_on=GAMES_F.idx, right_on=TP.idx)
GAMES_F.head()







date mday status t1 t1w t1g t1ge t1gp t2 t2w t2g t2ge t2gp draw g idx_x idx_y typ 0 2016-06-10 19:00:00 1 FINISHED France 1 2 0 0 Romania 0 1 0 0 0 3 0 0 Групи 1 2016-06-10 19:00:00 1 FINISHED Romania 0 1 0 0 France 1 2 0 0 0 3 0 0 Групи 2 2016-06-11 13:00:00 1 FINISHED Albania 0 0 0 0 Switzerland 1 1 0 0 0 1 1 1 Групи
2. Візуалізація даних за допомогою Python
Для візуалізації даних ми будемо використовувати бібліотеку matplotlib. На цьому етапі ми фактично підготуємо графіки в форматі .svg для їх подальшої обробки в векторному редакторі.
Для початку підготуємо графік-таймлайн всіх матчів Євро-2016. Мені здалося, що він буде гідно і зручно виглядати у формі горизонтальних стовпчиків.
GAMES_W = GAMES_F[(GAMES_F.t1w==1) | (GAMES_F.draw==1)]
GAMES_W['dt'] =[d.strftime('%d.%m.%Y') for d in GAMES_W.date]
# Отформатируем підписи осей
GAMES_W['l1'] = (GAMES_W.idx_x + 1).astype(str) + ' - ' + GAMES_W.dt + '' + (GAMES_W.typ)
GAMES_W['l2'] = GAMES_W.t2 + '' + GAMES_W.t2g.astype(str) + ':' + GAMES_W.t1g.astype(str) + '' + GAMES_W.t1

fig, ax1 = plt.subplots(figsize=[10, 30])
ax1.barh(GAMES_W.idx_x, GAMES_W.t1g, color='#01aae8')
ax1.set_yticks(GAMES_W.idx_x + 0.5)
ax1.set_yticklabels(GAMES_W.l1.values)
ax2 = ax1.twinx()
ax2.barh(GAMES_W.idx_x, -GAMES_W.t2g, color='#f79744')
ax2.set_yticks(GAMES_W.idx_x + 0.5)
ax2.set_yticklabels(GAMES_W.l2.values)
# Добре. Тепер ми завдали майже достатньо інформації для редагування в редакторі - нам залишилося лише 
# нанести інформацію для матчів, які закінчилися серією пенальті.
ax3 = ax1.twinx()
ax3.barh(GAMES_W.idx_x, GAMES_W.t2gp, 0.5, alpha=0.2, color='blue')
ax3.barh(GAMES_W.idx_x, -GAMES_W.t1gp, 0.5, alpha=0.2, color='orange')
ax1.grid(False)
ax2.grid(False)
ax1.set_xlim(-6, 6)
ax2.set_xlim(-6, 6)
# Тепер можна зберегти у файл для обробки
plt.show()
fig.savefig(os.path.join(DIR, 'output', 'barh_1.svg'))

На виході ми отримаємо ось такий непоказний графік-таймлайн.
Всі матчі Євро-2016 (Python)

Тепер Давайте дізнаємося, яка ж збірна забила найбільше м'ячів. Для цього створимо группированный DataFrame з існуючого і розрахуємо деякі агреговані показники.
GAMES_GR = GAMES_F.groupby(['t1'])
GAMES_AGG = GAMES_GR.agg({'t1g': np.sum})
GAMES_AGG.sort(['t1g'], ascending=False).head()

teams goals
France 13
Wales 10
Portugal 10
Belgium 9
Iceland 8

Цікаво, але давайте підрахуємо ще декілька показників:
  • Кількість зіграних матчів
  • Кол-во перемог/нічиїх/поразок
  • К-ть забитих/пропущених голів
  • Кількість матчів з додатковим часом
  • К-ть матчів, закончившехся серією пенальті
GAMES_F['n'] = 1
GAMES_GR = GAMES_F.groupby(['t1'])
GAMES_AGG = GAMES_GR.agg({'n': np.sum, 'draw': np.sum, 't1w': np.sum, 't2w':np.sum,
't1g': np.sum, 't2g': np.sum})
GAMES_AGG['games_extra'] = GAMES_GR.apply(lambda x: x[x['t1ge']>0]['n'].count())
GAMES_AGG['games_penalty'] = GAMES_GR.apply(lambda x: x[x['t1gp']>0]['n'].count())
GAMES_AGG.reset_index(оперативне=True)
GAMES_AGG.columns = ['team', 'draw', 'goals_lose', 'lose', 'win', 'goals',
'ігри', 'games_extra', 'games_penalty']
GAMES_AGG = GAMES_AGG.reindex(columns = ['team', 'games', 'games_extra', 'games_penalty',
'win', 'lose', 'draw', 'goals', 'goals_lose'])

Тепер пропонуємо ці розподілу:
GAMES_P = GAMES_AGG
GAMES_P.sort(['goals'], ascending=False, оперативне=True)
GAMES_P.reset_index(оперативне=True)

bar_width = 0.6
fig, ax = plt.subplots(figsize=[16, 6])
ax.bar(GAMES_P.index + 0.2, GAMES_P.goals, bar_width, color='#01aae8', label='Забито')
ax.bar(GAMES_P.index + 0.2, -GAMES_P.goals_lose, bar_width, color='#f79744', label='Пропущено')
ax.set_xticks(GAMES_P.index)
ax.set_xticklabels(GAMES_P.team, rotation=45)
ax.set_ylabel('Голи')
ax.set_xlabel('Команди')
ax.set_title('Топ команд за кількістю забитих м'ячів')
ax.legend()
plt.grid(False)
plt.show()
fig.savefig(os.path.join(DIR, 'output', 'bar_1.svg'))

На виході ми отримуємо такий графік
Розподіл забитих/пропущених м'ячів Євро-2016 (Python)

Намалюємо ще один графік розподілу перемог, поразок і нічиїх на Євро.
GAMES_P = GAMES_AGG
GAMES_P.sort(['win'], ascending=False, оперативне=True)
GAMES_P.reset_index(оперативне=True)

bar_width = 0.6
fig, ax = plt.subplots(figsize=[16, 6])
ax.bar(GAMES_P.index + 0.2, GAMES_P.win, bar_width, color='#01aae8', label='Перемоги')
ax.bar(GAMES_P.index + 0.2, -GAMES_P.lose, bar_width/2, color='#f79744', label='Ураження')
ax.bar(GAMES_P.index + 0.5, -GAMES_P.draw, bar_width/2, color='#99b286', label='Нічиї')
ax.set_xticks(GAMES_P.index)
ax.set_xticklabels(GAMES_P.team, rotation=45)
ax.set_ylabel('Матчі')
ax.set_xlabel('Команда')
ax.set_title('Топ команд за кількістю перемог')
ax.set_ylim(-GAMES_P.lose.max()*1.2, GAMES_P.win.max()*1.2)
ax.legend(ncol=3)
plt.grid(False)
plt.show()
fig.savefig(os.path.join(DIR, 'output', 'bar_2.svg'))

На виході ми отримуємо такий графік
Розподіл перемог/нічиїх/поразок Євро-2016 (Python)

Користуючись нагодою, хочеться відзначити, що лідер за кількістю забитих м'ячів і кількістю перемог Франція не став переможцем чемпіонату, а Португалія, з 3 матчами в нічию, меншою кількістю забитих м'ячів і більшим, ніж у Франції кількістю пропущених, в результаті отримала кубок.

3. Редагування векторної графіки Inkscape
Тепер давайте відредагуємо отримані у нас графіки у векторному редакторі Inkscape. Вибір припав на цей редактор з однієї причини — він безкоштовний. Відразу хотілося б зазначити, що до ресурсів системи він також вкрай вимогливий — досвідченим шляхом було визначено, що мінімальна кількість оперативної пам'яті для роботи — 8гб. У мене було 4гб, так що до кінця правки однією з ілюстрацій я вже починав нервувати від тривалих очікувань. Для комфортної роботи в редакторі рекомендується ознайомитися з вступними туториалами на сайті.
Основним завданням редагування графіків у векторному редакторі є можливість зробити красиві візуалізації за допомогою вбудованих функцій редактора. При цьому ми можемо:
  • Обирати, змінювати і переміщати будь-який елемент графіка, створений за допомогою python
  • Як завгодно доповнювати відмальовані дані із зовнішніх джерел, наприклад — вставляти прапори країн
  • Експортувати дані в якому завгодно дозвіл більшість графічних форматів
Я не буду довго описувати перетворення, які я зробив у Inkscape, а просто перерахую їх і представлю підсумкові отримані графіки. Зміни:
  • Відредаговані підписи на осях
  • Прибрані зайві заливки
  • Додано прапори країн для більш зрозумілою візуалізації
  • Зроблені кольорові виділення
  • Додана символіка Євро-2016
  • Додано футбольні атрибути (наприклад, м'ячі, відображають кількість забитих м'ячів на таймлайні)
Ось що вийшло в результаті:
Розподіл забитих/пропущених м'ячів Євро-2016 (Inkscape)

Розподіл перемог/поразок/нічиїх Євро-2016 (Inkscape)

Всі матчі Євро-2016 (Inkscape)


Дякую за увагу.
Джерело: Хабрахабр

0 коментарів

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