forked from zovos/summer_practice
fix main.py and add divisions.py
This commit is contained in:
parent
d2cf5cd57c
commit
bab805c8d6
3 changed files with 434 additions and 21 deletions
152
3.py
Normal file
152
3.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
'''
|
||||||
|
3.py - задание №3: график разницы заброшенных и пропущенных шайб одной команды
|
||||||
|
по ходу сезона (по датам матчей).
|
||||||
|
|
||||||
|
ОБЩАЯ ЛОГИКА:
|
||||||
|
1. Запуск: python3 3.py khl_3/khl_2018_19.csv "ЦСКА"
|
||||||
|
2. Читаем CSV с матчами.
|
||||||
|
3. match_goals(df) считает итоговые голы хозяев и гостей в каждом матче.
|
||||||
|
4. plot_team_diff(team, file) оставляет только матчи нужной команды, считает в
|
||||||
|
каждом разницу (забито - пропущено) и рисует её по оси дат; картинка
|
||||||
|
сохраняется в файл diff_<команда>.png.
|
||||||
|
|
||||||
|
Это отдельный сценарий от main.py: тут не строится таблица, а рисуется график
|
||||||
|
для одной команды. Часть кода (parse_score, подсчёт голов) намеренно похожа на
|
||||||
|
main.py, потому что правило подсчёта шайб одно и то же.
|
||||||
|
|
||||||
|
Зависимости: pandas (таблицы), matplotlib (график). numpy не используется напрямую.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys # для аргументов командной строки
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt # для построения графика
|
||||||
|
|
||||||
|
|
||||||
|
def parse_score(df, col):
|
||||||
|
'''
|
||||||
|
Шаг 1. Разобрать столбец со счётом вида "3:1" на два числовых столбца.
|
||||||
|
|
||||||
|
Принимает: df - таблица матчей; col - имя столбца со счётом.
|
||||||
|
Возвращает: (home, away) - голы хозяев и голы гостей (числовые Series).
|
||||||
|
|
||||||
|
split(':') разбивает "3:1" на "3" и "1"; to_numeric(errors='coerce') делает
|
||||||
|
из текста числа, а нечисловое (пустое ":" или знаки) превращает в NaN;
|
||||||
|
fillna(0) ставит 0. Итог: не сыгранный период читается как "0:0".
|
||||||
|
'''
|
||||||
|
parts = df[col].str.split(':', expand=True)
|
||||||
|
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)
|
||||||
|
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)
|
||||||
|
return home, away
|
||||||
|
|
||||||
|
|
||||||
|
def match_goals(df):
|
||||||
|
'''
|
||||||
|
Шаг 2. Посчитать итоговые голы хозяев и гостей в КАЖДОМ матче.
|
||||||
|
|
||||||
|
Принимает: df - таблица матчей.
|
||||||
|
Возвращает: (goals_home, goals_away) - два столбца с числом шайб в каждом матче.
|
||||||
|
|
||||||
|
Правило подсчёта то же, что в основной таблице: голы трёх периодов + голы
|
||||||
|
овертайма + 1 решающая шайба победителю серии буллитов. Отдельно зануляются
|
||||||
|
технические результаты (см. блок ниже).
|
||||||
|
'''
|
||||||
|
h1, a1 = parse_score(df, 'Период_1')
|
||||||
|
h2, a2 = parse_score(df, 'Период_2')
|
||||||
|
h3, a3 = parse_score(df, 'Период_3')
|
||||||
|
oth, ota = parse_score(df, 'Овертайм') # овертайм (0/0, если его не было)
|
||||||
|
soh, soa = parse_score(df, 'Буллиты') # буллиты (0/0, если их не было)
|
||||||
|
|
||||||
|
reg_h = h1 + h2 + h3 # голы хозяев в основное время
|
||||||
|
reg_a = a1 + a2 + a3 # голы гостей в основное время
|
||||||
|
|
||||||
|
tie = (reg_h == reg_a) # ничья после трёх периодов
|
||||||
|
# Победа по буллитам = была ничья в основное И в овертайме, и кто-то выиграл буллиты.
|
||||||
|
win_home_so = tie & (oth == ota) & (soh > soa)
|
||||||
|
win_away_so = tie & (oth == ota) & (soa > soh)
|
||||||
|
|
||||||
|
# astype(int) переводит True/False в 1/0 - так добавляем ровно 1 решающую шайбу.
|
||||||
|
goals_home = reg_h + oth + win_home_so.astype(int)
|
||||||
|
goals_away = reg_a + ota + win_away_so.astype(int)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Технические результаты (неявка/отмена): в файле счёт записан знаками
|
||||||
|
"+ — -" (техпобеда хозяев) или "- — +" (техпобеда гостей), а не цифрами.
|
||||||
|
КХЛ шайбы за такие матчи НЕ начисляет, поэтому оба значения принудительно 0:0
|
||||||
|
(так разница в этом матче будет 0, как на сайте КХЛ).
|
||||||
|
'''
|
||||||
|
p1 = df['Период_1'].astype(str).str.strip()
|
||||||
|
tech = p1.str.startswith('+') | p1.str.endswith('+')
|
||||||
|
goals_home = goals_home.where(~tech, 0)
|
||||||
|
goals_away = goals_away.where(~tech, 0)
|
||||||
|
return goals_home, goals_away
|
||||||
|
|
||||||
|
|
||||||
|
def plot_team_diff(team, filename):
|
||||||
|
'''
|
||||||
|
Шаг 3. Построить и сохранить график разницы шайб одной команды по датам.
|
||||||
|
|
||||||
|
Принимает: team - название команды (как в CSV); filename - путь к CSV.
|
||||||
|
Ничего не возвращает - сохраняет картинку diff_<team>.png и показывает окно.
|
||||||
|
|
||||||
|
Как работает:
|
||||||
|
1) считаем голы в каждом матче через match_goals;
|
||||||
|
2) разносим каждый матч на две строки (взгляд хозяев и взгляд гостей), чтобы
|
||||||
|
"забито/пропущено" были с точки зрения конкретной команды;
|
||||||
|
3) оставляем только строки нужной команды, сортируем по дате;
|
||||||
|
4) для каждого матча считаем "Разница = ГЗ - ГП" и рисуем линию по датам.
|
||||||
|
'''
|
||||||
|
df = pd.read_csv(filename) # читаем файл с матчами
|
||||||
|
goals_home, goals_away = match_goals(df) # голы хозяев/гостей в каждом матче
|
||||||
|
|
||||||
|
# Делаем "по две строки на матч": взгляд хозяев и взгляд гостей.
|
||||||
|
# ГЗ - забито нашей командой, ГП - пропущено (= забито соперником).
|
||||||
|
home = pd.DataFrame({
|
||||||
|
'Дата': df['Дата'],
|
||||||
|
'Команда': df['Команда_1'],
|
||||||
|
'ГЗ': goals_home,
|
||||||
|
'ГП': goals_away,
|
||||||
|
})
|
||||||
|
away = pd.DataFrame({
|
||||||
|
'Дата': df['Дата'],
|
||||||
|
'Команда': df['Команда_2'],
|
||||||
|
'ГЗ': goals_away,
|
||||||
|
'ГП': goals_home,
|
||||||
|
})
|
||||||
|
both = pd.concat([home, away], ignore_index=True)
|
||||||
|
both[['ГЗ', 'ГП']] = both[['ГЗ', 'ГП']].astype(int) # из дробных делаем целые
|
||||||
|
|
||||||
|
# Оставляем только матчи нужной команды (булева маска == team).
|
||||||
|
rows = both[both['Команда'] == team].copy()
|
||||||
|
# Дату-текст превращаем в настоящую дату, чтобы ось X шла по времени.
|
||||||
|
rows['Дата'] = pd.to_datetime(rows['Дата'])
|
||||||
|
rows = rows.sort_values('Дата') # по возрастанию даты
|
||||||
|
rows['Разница'] = rows['ГЗ'] - rows['ГП'] # забито минус пропущено в матче
|
||||||
|
|
||||||
|
# Рисуем график.
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 5))
|
||||||
|
ax.plot(rows['Дата'], rows['Разница'], marker='o') # линия с точками-матчами
|
||||||
|
ax.axhline(0, color='gray', linewidth=1) # горизонтальная линия "ноль" для наглядности
|
||||||
|
ax.set_title('Разница забитых и пропущенных шайб: ' + team)
|
||||||
|
ax.set_xlabel('Дата матча')
|
||||||
|
ax.set_ylabel('Забито - пропущено')
|
||||||
|
ax.grid(True, alpha=0.3) # лёгкая сетка
|
||||||
|
fig.autofmt_xdate() # автоматически поворачивает подписи дат, чтобы не слипались
|
||||||
|
|
||||||
|
fig.savefig('diff_' + team + '.png', dpi=150, bbox_inches='tight') # сохраняем картинку
|
||||||
|
print('График сохранён: diff_' + team + '.png')
|
||||||
|
plt.show() # показываем окно с графиком
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
Точка входа. Берёт из командной строки два аргумента - путь к CSV и название
|
||||||
|
команды - и вызывает построение графика.
|
||||||
|
Пример: python3 3.py khl_3/khl_2018_19.csv "ЦСКА"
|
||||||
|
'''
|
||||||
|
filename = sys.argv[1] # первый аргумент - имя файла
|
||||||
|
team = sys.argv[2] # второй аргумент - имя команды
|
||||||
|
plot_team_diff(team, filename)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
180
divisions.py
Normal file
180
divisions.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import sys # аргументы командной строки
|
||||||
|
import os # путь к main.py и имя csv-файла
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
'''
|
||||||
|
Отдельный модуль: ДИВИЗИОНЫ и ПРАВИЛО ПОСЕВА победителей дивизионов.
|
||||||
|
|
||||||
|
Зачем он нужен.
|
||||||
|
Базовый main.py сортирует команды в конференции строго по проценту очков (%О)
|
||||||
|
и тай-брейкам. Но в части сезонов КХЛ в таблице конференции действовало правило:
|
||||||
|
ПОБЕДИТЕЛИ ДВУХ ДИВИЗИОНОВ получают места 1 и 2 в конференции, даже если по очкам
|
||||||
|
они не входят в топ-2. Из-за этого, например, в сезоне 2020/21 в Западной
|
||||||
|
конференции СКА (82 очка) стоит на сайте выше Динамо М (84) и Локомотива (83):
|
||||||
|
СКА выиграл дивизион Боброва, а те двое - из дивизиона Тарасова, который выиграл ЦСКА.
|
||||||
|
|
||||||
|
main.py этот посев не делает (в нём вообще нет деления на дивизионы, только
|
||||||
|
Запад/Восток), поэтому правило вынесено сюда, в отдельный файл, и main.py не трогается.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Расчёт таблицы берём из main.py, чтобы не дублировать логику (единственный источник
|
||||||
|
правды - там). main.py в конце сам вызывает main(), поэтому импортировать его напрямую
|
||||||
|
нельзя - он бы тут же попытался прочитать аргументы и упал. Поэтому читаем его исходник
|
||||||
|
и выполняем без последней строки `main()`: так получаем готовые функции build_standings
|
||||||
|
и словарь CONFERENCES, ничего при этом не запуская и не печатая.
|
||||||
|
'''
|
||||||
|
_main_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'main.py')
|
||||||
|
_src = open(_main_path, encoding='utf-8').read()
|
||||||
|
_src = _src.replace('\nmain()\n', '\n') # убираем автозапуск
|
||||||
|
_ns = {}
|
||||||
|
exec(compile(_src, 'main.py', 'exec'), _ns)
|
||||||
|
build_standings = _ns['build_standings']
|
||||||
|
CONFERENCES = _ns['CONFERENCES']
|
||||||
|
|
||||||
|
|
||||||
|
def make_div(bobrov, tarasov, kharlamov, chernyshev):
|
||||||
|
'''
|
||||||
|
Собрать словарь {команда -> дивизион} из четырёх списков команд.
|
||||||
|
|
||||||
|
Принимает: четыре списка - составы дивизионов Боброва и Тарасова (Запад),
|
||||||
|
Харламова и Чернышёва (Восток).
|
||||||
|
Возвращает: dict, где каждой команде сопоставлено название её дивизиона.
|
||||||
|
|
||||||
|
Аналог make_conf из main.py, только делений не два (Запад/Восток), а четыре.
|
||||||
|
'''
|
||||||
|
d = {}
|
||||||
|
d.update({t: 'Боброва' for t in bobrov}) # Запад
|
||||||
|
d.update({t: 'Тарасова' for t in tarasov}) # Запад
|
||||||
|
d.update({t: 'Харламова' for t in kharlamov}) # Восток
|
||||||
|
d.update({t: 'Чернышёва' for t in chernyshev}) # Восток
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Состав дивизионов по сезонам (имена команд - ровно как в соответствующем csv).
|
||||||
|
Источники:
|
||||||
|
2020/21 - официальное распределение КХЛ;
|
||||||
|
2021/22 - Википедия "КХЛ в сезоне 2021/2022";
|
||||||
|
2018/19, 2019/20 - стандартная сетка дивизионов тех сезонов.
|
||||||
|
Для посева важен только победитель каждого дивизиона; выходные таблицы ниже
|
||||||
|
сверены с официальным сайтом КХЛ по всем четырём сезонам и совпадают.
|
||||||
|
'''
|
||||||
|
DIVISIONS = {
|
||||||
|
'2018_19': make_div(
|
||||||
|
bobrov=['СКА', 'Йокерит', 'Динамо М', 'Динамо Мн', 'Динамо Р', 'Слован'],
|
||||||
|
tarasov=['ЦСКА', 'Локомотив', 'ХК Сочи', 'Спартак', 'Витязь', 'Северсталь'],
|
||||||
|
kharlamov=['Автомобилист', 'Металлург Мг', 'Ак Барс', 'Трактор',
|
||||||
|
'Нефтехимик', 'Куньлунь РС', 'Адмирал'],
|
||||||
|
chernyshev=['Барыс', 'Авангард', 'Салават Юлаев', 'Сибирь',
|
||||||
|
'Торпедо НН', 'Амур'],
|
||||||
|
),
|
||||||
|
'2019_20': make_div(
|
||||||
|
bobrov=['СКА', 'Йокерит', 'Спартак', 'Динамо М', 'Динамо Мн', 'Динамо Р'],
|
||||||
|
tarasov=['ЦСКА', 'Локомотив', 'Витязь', 'Торпедо НН', 'ХК Сочи', 'Северсталь'],
|
||||||
|
kharlamov=['Ак Барс', 'Металлург Мг', 'Автомобилист', 'Нефтехимик',
|
||||||
|
'Трактор', 'Адмирал'],
|
||||||
|
chernyshev=['Авангард', 'Барыс', 'Салават Юлаев', 'Сибирь',
|
||||||
|
'Амур', 'Куньлунь РС'],
|
||||||
|
),
|
||||||
|
'2020_21': make_div(
|
||||||
|
bobrov=['СКА', 'Йокерит', 'Северсталь', 'Спартак', 'Витязь', 'ХК Сочи'],
|
||||||
|
tarasov=['ЦСКА', 'Динамо М', 'Локомотив', 'Динамо Мн', 'Динамо Р'],
|
||||||
|
kharlamov=['Автомобилист', 'Ак Барс', 'Металлург Мг', 'Нефтехимик',
|
||||||
|
'Трактор', 'Торпедо НН'],
|
||||||
|
chernyshev=['Авангард', 'Амур', 'Барыс', 'Куньлунь РС',
|
||||||
|
'Салават Юлаев', 'Сибирь'],
|
||||||
|
),
|
||||||
|
'2021_22': make_div(
|
||||||
|
bobrov=['Витязь', 'Йокерит', 'СКА', 'ХК Сочи', 'Торпедо', 'Спартак'],
|
||||||
|
tarasov=['Динамо М', 'Динамо Мн', 'Динамо Р', 'Локомотив', 'Северсталь', 'ЦСКА'],
|
||||||
|
kharlamov=['Автомобилист', 'Ак Барс', 'Куньлунь РС', 'Металлург Мг',
|
||||||
|
'Нефтехимик', 'Трактор'],
|
||||||
|
chernyshev=['Авангард', 'Амур', 'Барыс', 'Сибирь', 'Салават Юлаев', 'Адмирал'],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
В каких сезонах правило посева победителей дивизионов отражалось в таблице конференции.
|
||||||
|
2018/19, 2019/20, 2020/21 - да; в 2021/22 КХЛ перешла к чистому порядку по %О
|
||||||
|
(на сайте за 2021/22 победители дивизионов наверх НЕ поднимаются).
|
||||||
|
В сезонах 2018/19 и 2019/20 победители дивизионов и так были в топ-2 по очкам,
|
||||||
|
поэтому правило там виден результат не меняет - но оставляем его включённым,
|
||||||
|
чтобы логика была единой.
|
||||||
|
'''
|
||||||
|
SEED_BY_DIVISION = {
|
||||||
|
'2018_19': True,
|
||||||
|
'2019_20': True,
|
||||||
|
'2020_21': True,
|
||||||
|
'2021_22': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def seed_conference(conf_table):
|
||||||
|
'''
|
||||||
|
conf_table - часть итоговой таблицы по одной конференции, УЖЕ отсортированная
|
||||||
|
по критериям КХЛ (%О, затем В, ВО, ВБ, Р, ГЗ) - так её отдаёт build_standings.
|
||||||
|
|
||||||
|
Берём лучшего из каждого дивизиона (это и есть победитель дивизиона, т.к. таблица
|
||||||
|
отсортирована) и ставим этих двоих на места 1-2; между собой они идут в том же
|
||||||
|
порядке сортировки. Остальные команды - следом, тоже в порядке сортировки.
|
||||||
|
'''
|
||||||
|
# head(1) внутри каждого дивизиона = первая (а значит, лучшая) команда дивизиона.
|
||||||
|
winners = conf_table.groupby('Див', sort=False).head(1)
|
||||||
|
rest = conf_table.drop(winners.index)
|
||||||
|
return pd.concat([winners, rest])
|
||||||
|
|
||||||
|
|
||||||
|
def show_table(table, title):
|
||||||
|
'''
|
||||||
|
Напечатать таблицу конференции в консоль.
|
||||||
|
|
||||||
|
То же, что show_table в main.py, но дополнительно выводит столбец 'Див'
|
||||||
|
(дивизион) - чтобы было видно, почему победители дивизионов оказались наверху.
|
||||||
|
Принимает: table - готовая (уже посеянная) таблица; title - заголовок.
|
||||||
|
Ничего не возвращает - только печатает.
|
||||||
|
'''
|
||||||
|
# Тот же набор колонок, что на сайте КХЛ, плюс столбец дивизиона.
|
||||||
|
cols = ['Див', 'И', 'В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П', 'ГЗ', 'ГП', 'Р', 'О', '%О']
|
||||||
|
view = table[cols].copy()
|
||||||
|
view.insert(0, 'Место', np.arange(1, len(view) + 1))
|
||||||
|
view.insert(1, 'Команда', view.index)
|
||||||
|
|
||||||
|
print('\n' + '=' * 86)
|
||||||
|
print(title)
|
||||||
|
print('=' * 86)
|
||||||
|
print(view.to_string(index=False))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
Точка входа. Шаги:
|
||||||
|
1) определить сезон по имени CSV-файла;
|
||||||
|
2) построить таблицу команд через build_standings (логика из main.py);
|
||||||
|
3) приписать каждой команде дивизион (столбец 'Див');
|
||||||
|
4) для каждой конференции при необходимости применить посев победителей
|
||||||
|
дивизионов (seed_conference) и напечатать результат.
|
||||||
|
Пример: python3 divisions.py khl_3/khl_2020_21.csv
|
||||||
|
'''
|
||||||
|
path = sys.argv[1]
|
||||||
|
name = os.path.basename(path)
|
||||||
|
season = name.replace('khl_', '').replace('.csv', '')
|
||||||
|
|
||||||
|
df = pd.read_csv(path)
|
||||||
|
table = build_standings(df, CONFERENCES[season])
|
||||||
|
|
||||||
|
# Подписываем каждой команде её дивизион.
|
||||||
|
table['Див'] = pd.Series(table.index, index=table.index).map(DIVISIONS[season])
|
||||||
|
|
||||||
|
use_seeding = SEED_BY_DIVISION[season]
|
||||||
|
for conf in ['Запад', 'Восток']:
|
||||||
|
part = table[table['Конф'] == conf]
|
||||||
|
if use_seeding:
|
||||||
|
part = seed_conference(part)
|
||||||
|
suffix = ' (посев по дивизионам)' if use_seeding else ' (по очкам, без посева)'
|
||||||
|
show_table(part, 'Конференция ' + conf + ' ' + season + suffix)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
123
main.py
123
main.py
|
|
@ -1,3 +1,25 @@
|
||||||
|
'''
|
||||||
|
main.py - построение турнирной таблицы КХЛ из файла с результатами матчей.
|
||||||
|
|
||||||
|
ОБЩАЯ ЛОГИКА (что происходит, сверху вниз):
|
||||||
|
1. Запуск: python3 main.py khl_3/khl_2018_19.csv
|
||||||
|
2. По имени файла определяется код сезона (например, "2018_19") и берётся
|
||||||
|
словарь "команда -> конференция" из CONFERENCES.
|
||||||
|
3. CSV читается в таблицу матчей df (одна строка = один матч, счёт по периодам).
|
||||||
|
4. build_standings(df, conf_map) превращает матчи в турнирную таблицу команд:
|
||||||
|
по каждому матчу определяется исход (победа/поражение и каким образом),
|
||||||
|
результаты разносятся на обе команды, суммируются и сортируются по правилам КХЛ.
|
||||||
|
5. show_table(...) печатает таблицу: общую по чемпионату и отдельно по двум
|
||||||
|
конференциям (Запад / Восток).
|
||||||
|
|
||||||
|
ВХОДНЫЕ ДАННЫЕ (столбцы CSV):
|
||||||
|
Команда_1 - хозяева, Команда_2 - гости;
|
||||||
|
Период_1/2/3 - счёт по периодам в формате "3:1" (хозяева:гости);
|
||||||
|
Овертайм, Буллиты - счёт за овертайм и серию буллитов (или ":" если их не было).
|
||||||
|
|
||||||
|
Зависимости: pandas (таблицы), numpy (нумерация мест).
|
||||||
|
'''
|
||||||
|
|
||||||
import sys # для аргументов командной строки
|
import sys # для аргументов командной строки
|
||||||
import os # чтобы достать имя файла из пути
|
import os # чтобы достать имя файла из пути
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
@ -5,6 +27,14 @@ import numpy as np
|
||||||
|
|
||||||
|
|
||||||
def make_conf(west, east):
|
def make_conf(west, east):
|
||||||
|
'''
|
||||||
|
Собрать словарь {название команды -> конференция} из двух списков команд.
|
||||||
|
|
||||||
|
Принимает: west, east - списки названий команд Западной и Восточной конференций.
|
||||||
|
Возвращает: dict, где каждой команде сопоставлена строка 'Запад' или 'Восток'.
|
||||||
|
|
||||||
|
Нужен, чтобы позже одной операцией .map() приписать каждой команде её конференцию.
|
||||||
|
'''
|
||||||
d = {}
|
d = {}
|
||||||
d.update({team: 'Запад' for team in west})
|
d.update({team: 'Запад' for team in west})
|
||||||
d.update({team: 'Восток' for team in east})
|
d.update({team: 'Восток' for team in east})
|
||||||
|
|
@ -43,32 +73,46 @@ CONFERENCES = {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
'''
|
|
||||||
Шаг 1.
|
|
||||||
|
|
||||||
Формат счёта: 3:1 (хозяева забили 3, гости 1).
|
|
||||||
Если период не игрался (нет овертайма или буллитов), в файле стоит ":" - пустые числа.
|
|
||||||
|
|
||||||
df[col].str.split(':', expand=True) применяет строковую операцию split(':')
|
|
||||||
сразу ко всем строкам столбца и раскладывает результат в два столбца (expand=True).
|
|
||||||
|
|
||||||
pd.to_numeric превращает текст "3" в число 3. Параметр errors='coerce' означает:
|
|
||||||
если это не число (например, пустая строка), сделай NaN, а fillna(0) заменяет такие
|
|
||||||
пустые места на 0. В итоге не сыгранный период считается как "0:0".
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def parse_score(df, col):
|
def parse_score(df, col):
|
||||||
|
'''
|
||||||
|
Шаг 1. Разобрать столбец со счётом вида "3:1" на два числовых столбца.
|
||||||
|
|
||||||
|
Принимает: df - таблица матчей; col - имя столбца ('Период_1', 'Овертайм' и т.п.).
|
||||||
|
Возвращает: (home, away) - два числовых столбца (Series): голы хозяев и голы гостей.
|
||||||
|
|
||||||
|
Как работает:
|
||||||
|
- str.split(':', expand=True) разбивает "3:1" на части "3" и "1" сразу по
|
||||||
|
всему столбцу и раскладывает их в два столбца (expand=True);
|
||||||
|
- pd.to_numeric(..., errors='coerce') превращает текст в число, а всё, что
|
||||||
|
числом не является (пустая строка при ":" или знаки "+ — -"), делает NaN;
|
||||||
|
- fillna(0) заменяет NaN на 0. Итог: не сыгранный период читается как "0:0".
|
||||||
|
'''
|
||||||
parts = df[col].str.split(':', expand=True)
|
parts = df[col].str.split(':', expand=True)
|
||||||
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)
|
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)
|
||||||
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)
|
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)
|
||||||
return home, away
|
return home, away
|
||||||
|
|
||||||
|
|
||||||
# Шаг 2. Функция, которая из таблицы матчей строит таблицу команд.
|
|
||||||
|
|
||||||
|
|
||||||
def build_standings(df, conf_map):
|
def build_standings(df, conf_map):
|
||||||
|
'''
|
||||||
|
Шаг 2. Превратить таблицу матчей в турнирную таблицу команд.
|
||||||
|
|
||||||
|
Принимает: df - матчи (одна строка = один матч); conf_map - словарь
|
||||||
|
{команда -> конференция} из make_conf.
|
||||||
|
Возвращает: таблицу (DataFrame), где индекс - название команды, а столбцы -
|
||||||
|
И, В, ВО, ВБ, ПБ, ПО, П, ГЗ, ГП, О, Р, %О, Конф; строки уже
|
||||||
|
отсортированы по правилам КХЛ (лучшая команда сверху).
|
||||||
|
|
||||||
|
Идея целиком:
|
||||||
|
1) разбираем счёт по периодам/овертайму/буллитам;
|
||||||
|
2) по каждому матчу определяем ОДИН из шести исходов для хозяев и для гостей
|
||||||
|
(победа в основное/овертайме/буллитах и зеркальные поражения);
|
||||||
|
3) отдельно учитываем технические результаты ("+ — -" / "- — +");
|
||||||
|
4) считаем заброшенные/пропущенные шайбы;
|
||||||
|
5) разносим каждый матч на две строки (взгляд хозяев и взгляд гостей),
|
||||||
|
склеиваем и группируем по команде - получаем суммы по сезону;
|
||||||
|
6) считаем производные показатели (И, О, Р, %О) и сортируем.
|
||||||
|
'''
|
||||||
# Достаём счёт по периодам и т.д.
|
# Достаём счёт по периодам и т.д.
|
||||||
h1, a1 = parse_score(df, 'Период_1') # голы хозяев/гостей в 1 периоде
|
h1, a1 = parse_score(df, 'Период_1') # голы хозяев/гостей в 1 периоде
|
||||||
h2, a2 = parse_score(df, 'Период_2') # второй
|
h2, a2 = parse_score(df, 'Период_2') # второй
|
||||||
|
|
@ -94,6 +138,25 @@ def build_standings(df, conf_map):
|
||||||
win_home_so = tie & (oth == ota) & (soh > soa) # ОТ ничейный -> буллиты выиграли хозяева
|
win_home_so = tie & (oth == ota) & (soh > soa) # ОТ ничейный -> буллиты выиграли хозяева
|
||||||
win_away_so = tie & (oth == ota) & (soa > soh) # буллиты выиграли гости
|
win_away_so = tie & (oth == ota) & (soa > soh) # буллиты выиграли гости
|
||||||
|
|
||||||
|
'''
|
||||||
|
Технические результаты (неявка/отмена матча). В файле счёт записан не цифрами,
|
||||||
|
а знаками: "+ — -" - техническая победа хозяев, "- — +" - техническая победа гостей.
|
||||||
|
КХЛ засчитывает их как победу/поражение в основное время (2 и 0 очков), но шайбы
|
||||||
|
при этом НЕ начисляются (в графу Ш такой матч идёт как 0:0).
|
||||||
|
'''
|
||||||
|
p1 = df['Период_1'].astype(str).str.strip()
|
||||||
|
tech_home = p1.str.startswith('+') # техническая победа хозяев
|
||||||
|
tech_away = p1.str.endswith('+') # техническая победа гостей
|
||||||
|
tech = tech_home | tech_away
|
||||||
|
|
||||||
|
# Для технических матчей перекрываем обычную классификацию (там сейчас "ничья 0:0").
|
||||||
|
win_home_reg = (win_home_reg & ~tech) | tech_home
|
||||||
|
win_away_reg = (win_away_reg & ~tech) | tech_away
|
||||||
|
win_home_ot = win_home_ot & ~tech
|
||||||
|
win_away_ot = win_away_ot & ~tech
|
||||||
|
win_home_so = win_home_so & ~tech
|
||||||
|
win_away_so = win_away_so & ~tech
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Считаем заброшенные шайбы для итоговой таблицы.
|
Считаем заброшенные шайбы для итоговой таблицы.
|
||||||
К голам в основное время прибавляем голы овертайма, а победителю серии буллитов
|
К голам в основное время прибавляем голы овертайма, а победителю серии буллитов
|
||||||
|
|
@ -160,9 +223,11 @@ def build_standings(df, conf_map):
|
||||||
%О - процент набранных очков = очки / максимально возможные (2 за каждую игру).
|
%О - процент набранных очков = очки / максимально возможные (2 за каждую игру).
|
||||||
По этому проценту КХЛ расставляет места, когда команды сыграли разное число игр.
|
По этому проценту КХЛ расставляет места, когда команды сыграли разное число игр.
|
||||||
Сайт КХЛ НЕ округляет, а отбрасывает знаки после второго (72.448 -> 72.44),
|
Сайт КХЛ НЕ округляет, а отбрасывает знаки после второго (72.448 -> 72.44),
|
||||||
поэтому np.floor (округление вниз): умножаем на 10000, отбрасываем дробь, делим на 100.
|
поэтому считаем в целых числах: умножаем на 10000 и делим нацело (//) на 2*И -
|
||||||
|
остаток (дробная часть) отбрасывается, затем делим на 100. Целочисленная
|
||||||
|
арифметика без float, поэтому ровные значения (57.00) не съезжают в 56.99.
|
||||||
'''
|
'''
|
||||||
table['%О'] = np.floor(table['О'] / (2 * table['И']) * 10000) / 100
|
table['%О'] = (table['О'] * 10000 // (2 * table['И'])) / 100
|
||||||
# Подписываем каждой команде её конференцию. .map(словарь) проходит по индексу
|
# Подписываем каждой команде её конференцию. .map(словарь) проходит по индексу
|
||||||
# (названиям команд) и подставляет значение из словаря.
|
# (названиям команд) и подставляет значение из словаря.
|
||||||
table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)
|
table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)
|
||||||
|
|
@ -178,8 +243,18 @@ def build_standings(df, conf_map):
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
# Вывод таблицы текстом
|
|
||||||
def show_table(table, title):
|
def show_table(table, title):
|
||||||
|
'''
|
||||||
|
Напечатать турнирную таблицу в консоль в виде текста.
|
||||||
|
|
||||||
|
Принимает: table - готовая (уже отсортированная) таблица команд;
|
||||||
|
title - заголовок над таблицей.
|
||||||
|
Ничего не возвращает - только печатает.
|
||||||
|
|
||||||
|
Что делает: оставляет нужные столбцы в порядке, как на сайте КХЛ, добавляет
|
||||||
|
слева колонку "Место" (1..N по порядку строк) и колонку "Команда"
|
||||||
|
(название берётся из индекса таблицы), затем выводит всё через to_string.
|
||||||
|
'''
|
||||||
# Колонки в том же порядке и с теми же подписями, что на сайте КХЛ.
|
# Колонки в том же порядке и с теми же подписями, что на сайте КХЛ.
|
||||||
cols = ['И', 'В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П', 'ГЗ', 'ГП', 'Р', 'О', '%О']
|
cols = ['И', 'В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П', 'ГЗ', 'ГП', 'Р', 'О', '%О']
|
||||||
view = table[cols].copy() # берём только нужные столбцы (копия, чтобы не трогать оригинал)
|
view = table[cols].copy() # берём только нужные столбцы (копия, чтобы не трогать оригинал)
|
||||||
|
|
@ -195,6 +270,12 @@ def show_table(table, title):
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
'''
|
||||||
|
Точка входа. Связывает все шаги вместе:
|
||||||
|
читает путь к CSV из аргумента командной строки -> по имени файла определяет
|
||||||
|
сезон -> строит таблицу через build_standings -> печатает общую таблицу и две
|
||||||
|
таблицы по конференциям (Запад/Восток), отфильтрованные по столбцу 'Конф'.
|
||||||
|
'''
|
||||||
path = sys.argv[1] # получаем путь до csv
|
path = sys.argv[1] # получаем путь до csv
|
||||||
name = os.path.basename(path) # из khl_3/khl_*.csv берём только khl_*.csv
|
name = os.path.basename(path) # из khl_3/khl_*.csv берём только khl_*.csv
|
||||||
season = name.replace('khl_', '').replace('.csv', '')
|
season = name.replace('khl_', '').replace('.csv', '')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue