За да следвате този урок, трябва да имате следното:
- Python 3.7 или по-нова.
- Arctype
- Основно разбиране на SQL.
- Текстов редактор.
Инсталиране на необходимите библиотеки
Библиотеките, необходими за този урок, са както следва:
- numpy — основен пакет за научни изчисления с Python
- pandas — библиотека, предоставяща високопроизводителни, лесни за използване структури от данни и инструменти за анализ на данни
- requests — е единствената Non-GMO HTTP библиотека за Python, безопасна за човешка консумация. (обичам този ред от официалните документи :D)
- BeautifulSoup — библиотека на Python за извличане на данни от HTML и XML файлове.
За да инсталирате библиотеките, необходими за този урок, изпълнете следните команди по-долу:
pip install numpy
pip install pandas
pip install requests
pip install bs4
Изграждане на Python Web Scraper
Сега, след като имаме инсталирани всички необходими библиотеки, нека се заемем с изграждането на нашия уеб скрепер.
Импортиране на библиотеките на Python
import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup
import json
Извършване на проучване на място
Първата стъпка във всеки проект за уеб скрейп е проучване на уеб страницата, която искате да скрапирате, и да научите как работи. Това е от решаващо значение за намирането къде да получите данните от сайта. За този урок ще използваме http://understat.com.
Виждаме на началната страница, че сайтът има данни за шест европейски лиги. Ние обаче ще извличаме данни само за първите 5 лиги (отбори с изключение на RFPL).
Можем също да забележим, че данните на сайта започват от 2014/2015 до 2020/2021. Нека създадем променливи, които да обработват само данните, от които се нуждаем.
# create urls for all seasons of all leagues
base_url = 'https://understat.com/league'
leagues = ['La_liga', 'EPL', 'Bundesliga', 'Serie_A', 'Ligue_1']
seasons = ['2016', '2017', '2018', '2019', '2020']
Следващата стъпка е да разберете къде се съхраняват данните на уеб страницата. За да направите това, отворете Инструменти за разработчици в Chrome, отворете раздела Мрежа, намерете файла с данни (в този пример 2018) и изберете раздела „Отговор“. След като изпълним заявките, това ще получим.
След като прегледахме съдържанието на уеб страницата, открихме, че данните се записват под елемента „script“ в променливата teamsData
и са JSON кодирани. В резултат на това ще трябва да проследим този таг, да извлечем JSON от него и да го конвертираме в четима от Python структура от данни.
Декодиране на JSON данни с Python
season_data = dict()
for season in seasons:
url = base_url+'/'+league+'/'+season
res = requests.get(url)
soup = BeautifulSoup(res.content, "lxml")
# Based on the structure of the webpage, I found that data is in the JSON variable, under <script> tags
scripts = soup.find_all('script')
string_with_json_obj = ''
# Find data for teams
for el in scripts:
if 'teamsData' in str(el):
string_with_json_obj = str(el).strip()
# strip unnecessary symbols and get only JSON data
ind_start = string_with_json_obj.index("('")+2
ind_end = string_with_json_obj.index("')")
json_data = string_with_json_obj[ind_start:ind_end]
json_data = json_data.encode('utf8').decode('unicode_escape')
#print(json_data)
След като стартирате кода на Python по-горе, трябва да получите куп данни, които сме изчистили.
Разбиране на данните на скрепера
Когато започнем да разглеждаме данните, разбираме, че това е речник от речници с три ключа: id, title и history. Идентификаторите също се използват като ключове в началния слой на речника.
Следователно можем да заключим, че историята има информация за всеки мач, който отбор е играл в собствената си лига (мачовете за Купата на лигата или Шампионската лига не са включени).
След като прегледахме речника на първия слой, можем да започнем да съставяме списък с имена на екипи.
# Get teams and their relevant ids and put them into separate dictionary
teams = {}
for id in data.keys():
teams[id] = data[id]['title']
Виждаме, че често се появяват имена на колони; затова ги поставяме в отделен списък. Освен това вижте как се показват примерните стойности.
columns = []
# Check the sample of values per each column
values = []
for id in data.keys():
columns = list(data[id]['history'][0].keys())
values = list(data[id]['history'][0].values())
break
Сега нека вземем данни за всички отбори. Разкоментирайте оператора за печат в кода по-долу, за да отпечатате данните на вашата конзола.
# Getting data for all teams
dataframes = {}
for id, team in teams.items():
teams_data = []
for row in data[id]['history']:
teams_data.append(list(row.values()))
df = pd.DataFrame(teams_data, columns=columns)
dataframes[team] = df
# print('Added data for {}.'.format(team))
След като завършите този код, ще имаме речник на DataFrames, като ключът е името на отбора, а стойността е DataFrame, съдържаща всички игри на отбора.
Манипулиране на таблицата с данни
Когато разгледаме съдържанието на DataFrame, можем да видим, че показатели като PPDA и OPPDA (ppda и разрешени ppda) са представени като общи суми от атакуващи/защитни действия.
Те обаче са показани като коефициенти в оригиналната таблица. Нека изчистим това.
for team, df in dataframes.items():
dataframes[team]['ppda_coef'] = dataframes[team]['ppda'].apply(lambda x: x['att']/x['def'] if x['def'] != 0 else 0)
dataframes[team]['oppda_coef'] = dataframes[team]['ppda_allowed'].apply(lambda x: x['att']/x['def'] if x['def'] != 0 else 0)
Вече имаме всичките си номера, но за всяка игра. Общите резултати за отбора са това, което изискваме. Нека да разгледаме колоните, които трябва да съберем. За да направим това, ние се върнахме към оригиналната таблица на уебсайта и открихме, че всички мерки трябва да бъдат добавени заедно, като само PPDA и OPPDA остават като средства в крайна сметка. Първо, нека дефинираме колоните, които трябва да сумираме и означаваме.
cols_to_sum = ['xG', 'xGA', 'npxG', 'npxGA', 'deep', 'deep_allowed', 'scored', 'missed', 'xpts', 'wins', 'draws', 'loses', 'pts', 'npxGD']
cols_to_mean = ['ppda_coef', 'oppda_coef']
И накрая, нека изчислим сумите и средните стойности.
for team, df in dataframes.items():
sum_data = pd.DataFrame(df[cols_to_sum].sum()).transpose()
mean_data = pd.DataFrame(df[cols_to_mean].mean()).transpose()
final_df = sum_data.join(mean_data)
final_df['team'] = team
final_df['matches'] = len(df)
frames.append(final_df)
full_stat = pd.concat(frames)
full_stat = full_stat[['team', 'matches', 'wins', 'draws', 'loses', 'scored', 'missed', 'pts', 'xG', 'npxG', 'xGA', 'npxGA', 'npxGD', 'ppda_coef', 'oppda_coef', 'deep', 'deep_allowed', 'xpts']]
full_stat.sort_values('pts', ascending=False, inplace=True)
full_stat.reset_index(inplace=True, drop=True)
full_stat['position'] = range(1,len(full_stat)+1)
full_stat['xG_diff'] = full_stat['xG'] - full_stat['scored']
full_stat['xGA_diff'] = full_stat['xGA'] - full_stat['missed']
full_stat['xpts_diff'] = full_stat['xpts'] - full_stat['pts']
cols_to_int = ['wins', 'draws', 'loses', 'scored', 'missed', 'pts', 'deep', 'deep_allowed']
full_stat[cols_to_int] = full_stat[cols_to_int].astype(int)
В кода по-горе пренаредихме колоните за по-добра четливост, сортирахме редове въз основа на точки, нулирахме индекса и добавихме колона „позиция“.
Добавихме и разликите между очакваните показатели и реалните показатели.
И накрая, ние преобразувахме числата с плаваща степен в цели числа, където е подходящо.
Разкрасяване на крайния изход на Dataframe
И накрая, нека разкрасим нашите данни, за да станат подобни на данните за сайта в изображението по-горе. За да направите това, изпълнете кода на Python по-долу.
python
col_order = ['position', 'team', 'matches', 'wins', 'draws', 'loses', 'scored', 'missed', 'pts', 'xG', 'xG_diff', 'npxG', 'xGA', 'xGA_diff', 'npxGA', 'npxGD', 'ppda_coef', 'oppda_coef', 'deep', 'deep_allowed', 'xpts', 'xpts_diff']
full_stat = full_stat[col_order]
full_stat = full_stat.set_index('position')
# print(full_stat.head(20))
За да отпечатате част от разкрасените данни, разкоментирайте оператора за печат в кода по-горе.
Компилиране на окончателния код за агрегатор на данни на Python
За да получим всички данни, трябва да преминем през всички лиги и сезони, след което да ги манипулираме, за да могат да бъдат експортирани като CSV файл.
import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup
import json
# create urls for all seasons of all leagues
base_url = 'https://understat.com/league'
leagues = ['La_liga', 'EPL', 'Bundesliga', 'Serie_A', 'Ligue_1']
seasons = ['2016', '2017', '2018', '2019', '2020']
full_data = dict()
for league in leagues:
season_data = dict()
for season in seasons:
url = base_url+'/'+league+'/'+season
res = requests.get(url)
soup = BeautifulSoup(res.content, "lxml")
# Based on the structure of the webpage, I found that data is in the JSON variable, under <script> tags
scripts = soup.find_all('script')
string_with_json_obj = ''
# Find data for teams
for el in scripts:
if 'teamsData' in str(el):
string_with_json_obj = str(el).strip()
# print(string_with_json_obj)
# strip unnecessary symbols and get only JSON data
ind_start = string_with_json_obj.index("('")+2
ind_end = string_with_json_obj.index("')")
json_data = string_with_json_obj[ind_start:ind_end]
json_data = json_data.encode('utf8').decode('unicode_escape')
# convert JSON data into Python dictionary
data = json.loads(json_data)
# Get teams and their relevant ids and put them into separate dictionary
teams = {}
for id in data.keys():
teams[id] = data[id]['title']
# EDA to get a feeling of how the JSON is structured
# Column names are all the same, so we just use first element
columns = []
# Check the sample of values per each column
values = []
for id in data.keys():
columns = list(data[id]['history'][0].keys())
values = list(data[id]['history'][0].values())
break
# Getting data for all teams
dataframes = {}
for id, team in teams.items():
teams_data = []
for row in data[id]['history']:
teams_data.append(list(row.values()))
df = pd.DataFrame(teams_data, columns=columns)
dataframes[team] = df
# print('Added data for {}.'.format(team))
for team, df in dataframes.items():
dataframes[team]['ppda_coef'] = dataframes[team]['ppda'].apply(lambda x: x['att']/x['def'] if x['def'] != 0 else 0)
dataframes[team]['oppda_coef'] = dataframes[team]['ppda_allowed'].apply(lambda x: x['att']/x['def'] if x['def'] != 0 else 0)
cols_to_sum = ['xG', 'xGA', 'npxG', 'npxGA', 'deep', 'deep_allowed', 'scored', 'missed', 'xpts', 'wins', 'draws', 'loses', 'pts', 'npxGD']
cols_to_mean = ['ppda_coef', 'oppda_coef']
frames = []
for team, df in dataframes.items():
sum_data = pd.DataFrame(df[cols_to_sum].sum()).transpose()
mean_data = pd.DataFrame(df[cols_to_mean].mean()).transpose()
final_df = sum_data.join(mean_data)
final_df['team'] = team
final_df['matches'] = len(df)
frames.append(final_df)
full_stat = pd.concat(frames)
full_stat = full_stat[['team', 'matches', 'wins', 'draws', 'loses', 'scored', 'missed', 'pts', 'xG', 'npxG', 'xGA', 'npxGA', 'npxGD', 'ppda_coef', 'oppda_coef', 'deep', 'deep_allowed', 'xpts']]
full_stat.sort_values('pts', ascending=False, inplace=True)
full_stat.reset_index(inplace=True, drop=True)
full_stat['position'] = range(1,len(full_stat)+1)
full_stat['xG_diff'] = full_stat['xG'] - full_stat['scored']
full_stat['xGA_diff'] = full_stat['xGA'] - full_stat['missed']
full_stat['xpts_diff'] = full_stat['xpts'] - full_stat['pts']
cols_to_int = ['wins', 'draws', 'loses', 'scored', 'missed', 'pts', 'deep', 'deep_allowed']
full_stat[cols_to_int] = full_stat[cols_to_int].astype(int)
col_order = ['position', 'team', 'matches', 'wins', 'draws', 'loses', 'scored', 'missed', 'pts', 'xG', 'xG_diff', 'npxG', 'xGA', 'xGA_diff', 'npxGA', 'npxGD', 'ppda_coef', 'oppda_coef', 'deep', 'deep_allowed', 'xpts', 'xpts_diff']
full_stat = full_stat[col_order]
full_stat = full_stat.set_index('position')
# print(full_stat.head(20))
season_data[season] = full_stat
df_season = pd.concat(season_data)
full_data[league] = df_season
За да анализираме нашите данни в Arctype, трябва да експортираме данните в CSV файл. За да направите това, копирайте и поставете кода по-долу.
python
data = pd.concat(full_data)
data.to_csv('understat.com.csv')
Анализиране на данни от Scraper с MySQL
Сега, когато имаме чист CSV файл, съдържащ нашите футболни данни, нека създадем някои визуализации. Първо ще трябва да импортираме CSV файла в MySQL таблица.
Импортиране на CSV данни в MySQL
За да използваме извлечените от нас данни, трябва да импортираме CSV данните като таблица в нашата база данни. За да направите това, следвайте стъпките по-долу:
Стъпка 1
В менюто на базата данни щракнете върху иконата с три точки и изберете „Импортиране на таблица“. Кликнете върху „приемам“, за да приемете схемата.
Стъпка 2
Въведете името на таблицата като „soccer_data“, след което преименувайте първите две колони на „league“ и „year“. Оставете всички други настройки и щракнете върху бутона „Импортиране на CSV“, както се вижда на изображението по-долу.
След като следвате стъпките по-горе, таблицата „soccer_data“ трябва да бъде попълнена с данни от CSV файла, както се вижда на изображението по-долу.
Сега, след като импортирахме нашите данни, съхранени в CSV файл, можем да сравним различни данни и да ги визуализираме в диаграми с данни.
Използвайте Dynamic SQL, за да създадете обобщена таблица и лентова диаграма
Ще анализираме данните за вкарани и пропуснати удари за една лига през всички години, за да изчислим съотношението удари за гол на всеки отбор. Перфектното първенство за провеждане на този анализ е „Бундеслигата“, тъй като те са първенство, известно с много удари извън кутията.
Създаване на обобщена таблица за удари за гол
За тази визуализация ще се нуждаем от нашите резултати в таблица с обобщен стил с уникална колона за всеки сезон в набора от данни. Това е основната логика на нашето запитване:
SELECT
year,
SUM(
CASE
WHEN year = '2020' THEN (scored + missed) / scored
ELSE NULL
END
) AS `2020 Season`
FROM
soccer_data
WHERE
league = 'Bundesliga'
GROUP BY
team
ORDER BY
team;
По този начин съотношението удари за гол на всеки отбор през сезон 2020 се извежда в колона, наречена 2020 Season
. Но какво, ако искаме пет отделни колони, които правят едно и също нещо в продължение на пет сезона? Разбира се, можем да дефинираме всеки един ръчно или можем да използваме GROUP CONCAT()
и потребителски променливи, за да направим това динамично. Единственият динамичен компонент на нашата заявка са сезонните колони в нашия SELECT
израз, така че нека започнем с SELECT
вписване на този низ на заявката в променлива (@sql).
SELECT
GROUP_CONCAT(
DISTINCT CONCAT(
'SUM(case when year = ''',
year,
''' then (scored + missed) / scored ELSE NULL END) AS `',
year,
' Season`'
)
ORDER BY
year ASC
) INTO @sql
FROM
soccer_data;
Тук DISTINCT CONCAT()
генерира дефиниция на SUM(CASE WHEN year=...)
колона за всяка отделна стойност в year
колона на нашата таблица. Ако искате да видите точния резултат, просто добавете SELECT @sql
; на нов ред и изпълнете заявката.
Сега, когато имаме динамичната част от нашия низ за заявка, просто трябва да добавим всичко останало около него по следния начин:
SET
@sql = CONCAT(
'WITH pivot_data AS (SELECT team, ',
@sql,
'FROM understat_com
WHERE league=''Bundesliga''
GROUP BY team
ORDER BY team)
SELECT *
FROM pivot_data
WHERE `2019 Season` IS NOT NULL
AND `2020 Season` IS NOT NULL;'
);
И накрая, просто трябва да подготвим SQL оператор от низа в @sql и да го изпълним:
PREPARE stmt FROM @sql;
EXECUTE stmt;
Стартирайте, преименувайте и запазете цялата заявка по-горе.
Визуализиране на съотношението гол за удар със стълбова диаграма
Отличен начин за визуализиране на заявката по-горе е използването на лентова диаграма. Използвайте колоната team
за оста x и всяка от нашите Season
колони за оста y. Това трябва да доведе до стълбовидна диаграма като тази по-долу:
Създайте кръгова диаграма „Топ 5 срещу останалите“, като използвате CTE
Ще анализираме данни за една лига от различни години, за да сравним общите победи на първите пет отбора с останалите в лигата. Идеалното първенство за провеждане на този анализ е „Серия А“, тъй като те записват много победи.
Как да разделите топ 5 отбора с помощта на оператор WITH
За тази визуализация по същество искаме резултатите от нашата заявка да изглеждат така:
Имайки това предвид, първо ще се съсредоточим върху редовете за първите пет отбора:
WITH top5 AS(
SELECT
team,
SUM(wins) as wins
FROM
soccer_data
WHERE
league='Serie_A'
GROUP BY
1
ORDER BY
2 DESC
LIMIT 5
)
SELECT * FROM top5
Тук използваме клауза WITH, за да създадем израз на обща таблица (CTE), наречен top5
с името на отбора и общите победи за първите 5 отбора. След това избираме всичко в top5
.
След като имаме първите пет отбора, нека използваме UNION, за да добавим останалите:
UNION
SELECT
'Other' as team,
SUM(wins) as wins
FROM
soccer_data
WHERE
league='Serie_A'
AND team NOT IN (SELECT team FROM top5)
Стартирайте, преименувайте и запазете цялата заявка по-горе.
Визуализиране на топ 5 срещу останалите с кръгова диаграма
Отличен начин за визуализиране на заявката по-горе е използването на кръгова диаграма. Използвайте колоната team
за „категория“ и wins
за „стойности“. След като добавим колоните, трябва да имаме кръгова диаграма като тази на изображението по-долу. Както можете да видите, първите пет отбора съставляват близо 50% от победите в лигата Серия А:
Сравняване на коефициентите печалба-загуба с CTE и динамичен SQL
За тази заявка ще използваме CTE и динамичен SQL, за да сравним съотношенията победи и загуби на първите три отбора в Серия А с всички останали отбори. Ще искаме наборът от резултати да изглежда така:
Основната логика на заявката трябва да изглежда така:
SELECT
year,
MAX(CASE
WHEN team = 'team1' THEN wins / losses
ELSE NULL
END) AS `team 1`
AVG(CASE
WHEN team NOT in ('team1','team2','team3') THEN wins / losses
ELSE NULL
END) AS `other`
FROM
soccer_stats
WHERE
league = 'Serie_A'
GROUP BY
team
Разбира се, това няма да работи без малко MySQL магия.
Разделяне на топ 3 отбора
Първо, нека разделим нашите три най-добри отбора с помощта на CTE:
WITH top3 AS(
SELECT
team,
AVG(wins / loses) as wins_to_losses
FROM
soccer_data
WHERE
league = 'Serie_A'
GROUP BY
team
ORDER BY
2 DESC
LIMIT
3
)
Генериране на динамични SQL низове в CTE
Тъй като всеки от тези екипи ще се нуждае от собствена колона, ще трябва да използваме динамичен SQL, за да генерираме някои специални CASE оператори. Ще трябва също така да генерираме оператор CASE за нашата колона „Други“. За това ще използваме динамичен SQL вътре в CTE:
variable_definitions AS(
SELECT
(
GROUP_CONCAT(
CONCAT(
'''',
team,
''''
)
)
) as team_names,
(
GROUP_CONCAT(
DISTINCT CONCAT(
'MAX(case when team = ''',
team,
''' then wins / loses ELSE NULL END) AS `',
team,
'`'
)
)
) as column_definitions
FROM top3
)
След това нека вземем низовете team_names
и column_definitions
и ги INSERT
в променливи:
SELECT
team_names,
column_definitions
INTO
@teams,
@sql
FROM
variable_definitions;
В този момент трябва да имаме списък с първите три отбора в низов формат, записан в @teams
, и нашите отчети за случай на колона за първите три отбора, запазени в @sql
. Просто трябва да изградим крайната заявка:
SET
@sql = CONCAT(
'SELECT year, ',
@sql,
', AVG(CASE WHEN team NOT IN (',
@teams,
') THEN wins / loses ELSE NULL END) AS `Others` ',
'FROM soccer_data WHERE league = ''Serie_A'' GROUP BY year;'
);
prepare stmt FROM @sql;
EXECUTE stmt;
Можете да намерите пълната заявка в долната част на тази статия.
Визуализиране на съотношението победа-загуба с площна диаграма
Отличен начин за визуализиране на заявката по-горе е използването на диаграма с площи. За да създадете тази областна диаграма, използвайте year
за оста x и всички други колони за оста y. Вашата диаграма трябва да изглежда по следния начин:
Тъй като използваме динамичен SQL, можем лесно да добавим още екипни колони, като променим LIMIT 3
в top3
CTE на LIMIT 5
:
Заключение
В тази статия научихте как да извличате спортни данни с Python от уебсайт и да използвате разширени MySQL операции, за да ги анализирате и визуализирате с Arctype. Освен това видяхте колко лесно е да изпълнявате SQL заявки към вашата база данни с помощта на Arctype и получихте шанса да проучите някои от нейните основни характеристики и функционалности.
Изходният код на скрипта на Python, CSV файлът и други данни са достъпни в Github. Ако имате въпроси, не се колебайте да се свържете с мен в Twitter: @LordChuks3.
Окончателна SQL заявка:
WITH top3 AS(
SELECT
team,
AVG(wins / loses) as wins_to_losses
FROM
soccer_data
WHERE
league = 'Serie_A'
GROUP BY
team
ORDER BY
2 DESC
LIMIT
3
), variable_definitions AS(
SELECT
(GROUP_CONCAT(CONCAT('''', team, ''''))) as team_names,
(
GROUP_CONCAT(
DISTINCT CONCAT(
'MAX(case when team = ''',
team,
''' then wins / loses ELSE NULL END) AS `',
team,
'`'
)
)
) as column_definitions
FROM
top3
)
SELECT
team_names,
column_definitions
INTO
@teams,
@sql
FROM
variable_definitions;
SET
@sql = CONCAT(
'SELECT year, ',
@sql,
', AVG(CASE WHEN team NOT IN (',
@teams,
') THEN wins / loses ELSE NULL END) AS `Others` ',
'FROM soccer_data WHERE league = ''Serie_A'' GROUP BY year;'
);
prepare stmt FROM @sql;
EXECUTE stmt;
Повече съдържание на plainenglish.io