forked from zovos/summer_practice
Compare commits
10 commits
individual
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6710f2e54f | ||
| 71dc7ee569 | |||
|
|
ccb4ae6b3f | ||
|
|
fb2d294459 | ||
|
|
bab805c8d6 | ||
|
|
d2cf5cd57c | ||
|
|
7775a1b504 | ||
| 30235ebf7c | |||
| 4335a229cb | |||
|
|
e680cd5131 |
6 changed files with 1285 additions and 24 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()
|
||||
157
README.md
157
README.md
|
|
@ -1,2 +1,157 @@
|
|||
# summer_practice
|
||||
# Летняя практика — Турнирные таблицы КХЛ
|
||||
|
||||
Проект анализирует результаты матчей регулярного чемпионата КХЛ за сезоны
|
||||
**2018/19 – 2021/22** и строит по ним турнирные таблицы и график.
|
||||
Основные инструменты — библиотека **pandas** (работа с таблицами) и
|
||||
**matplotlib** (график).
|
||||
|
||||
---
|
||||
|
||||
## 1. Структура проекта
|
||||
|
||||
| Файл / папка | Назначение |
|
||||
|---|---|
|
||||
| `main.py` | Основная программа: строит полную турнирную таблицу чемпионата и таблицы по конференциям (Запад / Восток). |
|
||||
| `divisions.py` | Отдельный модуль: добавляет деление на **дивизионы** и **правило посева** победителей дивизионов. Логику расчёта берёт из `main.py`, сам `main.py` не меняет. |
|
||||
| `3.py` | Задание №3: **график** разницы заброшенных и пропущенных шайб одной команды по ходу сезона. |
|
||||
| `khl_3/` | Исходные данные — CSV-файлы матчей (4 сезона, кодировка UTF-8). |
|
||||
|
||||
### Формат исходных данных (CSV)
|
||||
|
||||
```
|
||||
,Номер,Дата,Команда_1,Команда_2,Период_1,Период_2,Период_3,Овертайм,Буллиты
|
||||
0,1,2018-09-01,Ак Барс,СКА,0:1,1:2,0:3,:,:
|
||||
```
|
||||
|
||||
* `Команда_1` — хозяева, `Команда_2` — гости.
|
||||
* `Период_1..3`, `Овертайм`, `Буллиты` — счёт в формате `хозяева:гости`.
|
||||
* Пустой `:` означает, что период не игрался (нет овертайма/буллитов).
|
||||
* **Технические результаты** записаны знаками: `+ — -` (техпобеда хозяев) и
|
||||
`- — +` (техпобеда гостей). Встречаются в сезоне 2020/21.
|
||||
|
||||
---
|
||||
|
||||
## 2. Установка и запуск
|
||||
|
||||
```bash
|
||||
pip install pandas numpy matplotlib
|
||||
|
||||
# Полная турнирная таблица + таблицы по конференциям
|
||||
python3 main.py khl_3/khl_2018_19.csv
|
||||
|
||||
# Таблицы по конференциям с дивизионами и посевом победителей дивизионов
|
||||
python3 divisions.py khl_3/khl_2020_21.csv
|
||||
|
||||
# График разницы шайб одной команды по датам (сохраняется в diff_<команда>.png)
|
||||
python3 3.py khl_3/khl_2018_19.csv "ЦСКА"
|
||||
```
|
||||
|
||||
> Имя файла важно: программы достают код сезона (`2018_19`) из имени
|
||||
> `khl_2018_19.csv`, чтобы понять, какие команды в какой конференции (и дивизионе)
|
||||
> играли.
|
||||
|
||||
---
|
||||
|
||||
## 3. Как работает `main.py` (ядро проекта)
|
||||
|
||||
Логика построена на **векторных операциях pandas**: вычисления делаются сразу
|
||||
над целым столбцом, без циклов по строкам.
|
||||
|
||||
**Шаг 1. `parse_score(df, col)`** — разбирает строку счёта `"3:1"` на два числа.
|
||||
`str.split(':', expand=True)` раскладывает столбец в два, `to_numeric(..., errors='coerce')`
|
||||
превращает текст в числа, а пустой период (`:`) или знаки техрезультата через
|
||||
`fillna(0)` считаются как `0`.
|
||||
|
||||
**Шаг 2. `build_standings(df, conf_map)`** — превращает список матчей в таблицу команд:
|
||||
|
||||
1. Считает голы в основное время (сумма трёх периодов).
|
||||
2. Определяет **тип исхода** каждого матча сравнением столбцов:
|
||||
победа в основное время / в овертайме / по буллитам — и аналогично поражения.
|
||||
Победителю по буллитам добавляется ровно 1 решающая шайба.
|
||||
3. Отдельно обрабатывает **технические результаты** (`+ — -` / `- — +`):
|
||||
засчитывает их как победу/поражение в основное время, но шайбы не начисляет (0:0).
|
||||
4. **«Разворачивает» матч в две строки** — взгляд хозяев и взгляд гостей —
|
||||
и склеивает их (`pd.concat`). Теперь у каждой команды по строке на матч.
|
||||
5. `groupby('Команда').sum()` суммирует все показатели по командам.
|
||||
6. Считает производные показатели:
|
||||
* **И** — игры, **О** — очки (победа = 2, поражение в ОТ/буллитах = 1),
|
||||
* **Р** — разница шайб, **%О** — процент набранных очков.
|
||||
* `%О` считается **в целых числах** (`* 10000 // (2*И) / 100`) с отбрасыванием
|
||||
знаков, как на сайте КХЛ, — без округления и без ошибок float (57.00 не съезжает в 56.99).
|
||||
7. Сортирует по регламенту КХЛ: `%О → В → ВО → ВБ → Р → ГЗ`.
|
||||
|
||||
**Шаг 3. `show_table` и `main`** — печатают полную таблицу, затем отдельно
|
||||
конференции Запад и Восток (фильтр по столбцу `Конф`).
|
||||
|
||||
### Обозначения столбцов
|
||||
|
||||
`И` — игры · `В` — победы в основное время · `ВО` — победы в овертайме ·
|
||||
`ВБ` — победы по буллитам · `ПБ` — поражения по буллитам · `ПО` — поражения в овертайме ·
|
||||
`П` — поражения в основное время · `ГЗ` — голы забитые · `ГП` — голы пропущенные ·
|
||||
`Р` — разница · `О` — очки · `%О` — процент очков.
|
||||
|
||||
---
|
||||
|
||||
## 4. Дополнительные файлы
|
||||
|
||||
### `divisions.py` — дивизионы и посев
|
||||
|
||||
В части сезонов КХЛ (2018/19–2020/21) действовало правило: **победители двух
|
||||
дивизионов получают места 1 и 2 в конференции**, даже если по очкам они не в топ-2.
|
||||
Например, в 2020/21 на Западе СКА (82 очка) стоит выше Динамо М (84) и
|
||||
Локомотива (83) — СКА выиграл дивизион Боброва. В сезоне 2021/22 это правило
|
||||
отменили (чистый порядок по %О).
|
||||
|
||||
`divisions.py`:
|
||||
* переиспользует `build_standings` и `CONFERENCES` из `main.py` (не дублируя логику и
|
||||
**не изменяя** `main.py`);
|
||||
* словарь `DIVISIONS` задаёт состав четырёх дивизионов (Боброва, Тарасова — Запад;
|
||||
Харламова, Чернышёва — Восток) по каждому сезону;
|
||||
* `SEED_BY_DIVISION` включает посев только там, где он применялся;
|
||||
* `seed_conference` поднимает победителей дивизионов на места 1–2;
|
||||
* выводит таблицы конференций с дополнительным столбцом `Див`.
|
||||
|
||||
### `3.py` — график разницы шайб
|
||||
|
||||
Задание №3: строит **график** разницы заброшенных и пропущенных шайб одной команды
|
||||
по датам матчей. Считает голы в каждом матче (`match_goals`, та же логика подсчёта,
|
||||
что в `main.py`, включая зануление технических результатов), разносит матч на взгляд
|
||||
хозяев и гостей, оставляет нужную команду, рисует линию «забито − пропущено» по дате
|
||||
и сохраняет картинку `diff_<команда>.png`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Материал к защите
|
||||
|
||||
**О чём проект.** По «сырым» результатам матчей КХЛ автоматически строится
|
||||
официальная турнирная таблица, таблицы с дивизионами и график. Цель — освоить
|
||||
обработку табличных данных в pandas и визуализацию в matplotlib.
|
||||
|
||||
**Ключевые идеи, которые стоит назвать на защите:**
|
||||
|
||||
1. **Векторизация вместо циклов.** Все расчёты — операции над столбцами целиком
|
||||
(сложение, сравнение, маски `True/False`). Это и быстрее, и короче, чем `for` по строкам.
|
||||
2. **Приём «одна строка матча → две строки команд».** Чтобы посчитать статистику
|
||||
команды, каждый матч дублируется: одна запись со стороны хозяев, другая — гостей.
|
||||
После этого `groupby` сводит всё в таблицу команд.
|
||||
3. **Булевы маски.** Тип исхода (победа/поражение и как именно) — это столбцы `True/False`,
|
||||
которые переводятся в `1/0` через `.astype(int)` и суммируются.
|
||||
4. **Точное соответствие регламенту КХЛ.** Система очков (2/1/0), добавление шайбы
|
||||
за буллиты, технические результаты, целочисленный (без округления) процент очков,
|
||||
порядок критериев сортировки, посев победителей дивизионов.
|
||||
5. **Переиспользование кода.** `divisions.py` и `3.py` опираются на логику из `main.py`,
|
||||
чтобы не дублировать разбор счёта и расчёт таблицы.
|
||||
|
||||
**Возможные вопросы и ответы:**
|
||||
|
||||
* *Почему пустой период = 0:0?* — Овертайм/буллиты есть не во всех матчах; `fillna(0)`
|
||||
делает расчёт единообразным.
|
||||
* *Почему %О считается в целых числах, а не через `round`?* — Сайт КХЛ отбрасывает
|
||||
знаки после второго (а не округляет), и целочисленная арифметика не даёт ошибок
|
||||
float (57.00 вместо 56.99...).
|
||||
* *Как обрабатываются технические результаты?* — `+ — -` / `- — +` засчитываются как
|
||||
победа/поражение в основное время, но без шайб (0:0), как в КХЛ.
|
||||
* *Почему дивизионы в отдельном файле?* — чтобы не усложнять `main.py`; `divisions.py`
|
||||
переиспользует его функции, ничего в нём не меняя.
|
||||
* *Как добавить новый сезон?* — добавить CSV в `khl_3/`, описать конференции в
|
||||
`CONFERENCES` (в `main.py`) и при необходимости дивизионы в `DIVISIONS` (в `divisions.py`).
|
||||
|
|
|
|||
57
individual_4.py
Normal file
57
individual_4.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import sys
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
# разбираем счёт "3:1" на два числа, пустой период считаем как 0
|
||||
def parse_score(df, col):
|
||||
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 task4(filename):
|
||||
df = pd.read_csv(filename)
|
||||
|
||||
# счёт по периодам, овертайму и буллитам
|
||||
h1, a1 = parse_score(df, 'Период_1')
|
||||
h2, a2 = parse_score(df, 'Период_2')
|
||||
h3, a3 = parse_score(df, 'Период_3')
|
||||
oth, ota = parse_score(df, 'Овертайм')
|
||||
soh, soa = parse_score(df, 'Буллиты')
|
||||
|
||||
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)
|
||||
|
||||
goals_home = reg_h + oth + win_home_so.astype(int)
|
||||
goals_away = reg_a + ota + win_away_so.astype(int)
|
||||
|
||||
# собираем шайбы обеих команд и суммируем по каждой
|
||||
home = pd.DataFrame({'Команда': df['Команда_1'], 'ГЗ': goals_home})
|
||||
away = pd.DataFrame({'Команда': df['Команда_2'], 'ГЗ': goals_away})
|
||||
both = pd.concat([home, away], ignore_index=True)
|
||||
|
||||
totals = both.groupby('Команда')['ГЗ'].sum().astype(int)
|
||||
totals = totals.sort_values(ascending=False)
|
||||
|
||||
# рисуем гистограмму
|
||||
plt.figure(figsize=(12, 6))
|
||||
plt.bar(totals.index, totals.values, color='steelblue')
|
||||
plt.title('Всего заброшено шайб за сезон')
|
||||
plt.xlabel('Команда')
|
||||
plt.ylabel('Заброшено шайб')
|
||||
plt.xticks(rotation=90)
|
||||
for i, value in enumerate(totals.values):
|
||||
plt.text(i, value, str(value), ha='center', va='bottom')
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
task4(sys.argv[1])
|
||||
127
main.py
127
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 os # чтобы достать имя файла из пути
|
||||
import pandas as pd
|
||||
|
|
@ -5,6 +27,14 @@ import numpy as np
|
|||
|
||||
|
||||
def make_conf(west, east):
|
||||
'''
|
||||
Собрать словарь {название команды -> конференция} из двух списков команд.
|
||||
|
||||
Принимает: west, east - списки названий команд Западной и Восточной конференций.
|
||||
Возвращает: dict, где каждой команде сопоставлена строка 'Запад' или 'Восток'.
|
||||
|
||||
Нужен, чтобы позже одной операцией .map() приписать каждой команде её конференцию.
|
||||
'''
|
||||
d = {}
|
||||
d.update({team: 'Запад' for team in west})
|
||||
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):
|
||||
'''
|
||||
Шаг 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)
|
||||
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)
|
||||
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)
|
||||
return home, away
|
||||
|
||||
|
||||
# Шаг 2. Функция, которая из таблицы матчей строит таблицу команд.
|
||||
|
||||
|
||||
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 периоде
|
||||
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_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 за каждую игру).
|
||||
По этому проценту КХЛ расставляет места, когда команды сыграли разное число игр.
|
||||
Сайт КХЛ НЕ округляет, а отбрасывает знаки после второго (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(словарь) проходит по индексу
|
||||
# (названиям команд) и подставляет значение из словаря.
|
||||
table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)
|
||||
|
|
@ -178,8 +243,18 @@ def build_standings(df, conf_map):
|
|||
return table
|
||||
|
||||
|
||||
# Вывод таблицы текстом
|
||||
def show_table(table, title):
|
||||
'''
|
||||
Напечатать турнирную таблицу в консоль в виде текста.
|
||||
|
||||
Принимает: table - готовая (уже отсортированная) таблица команд;
|
||||
title - заголовок над таблицей.
|
||||
Ничего не возвращает - только печатает.
|
||||
|
||||
Что делает: оставляет нужные столбцы в порядке, как на сайте КХЛ, добавляет
|
||||
слева колонку "Место" (1..N по порядку строк) и колонку "Команда"
|
||||
(название берётся из индекса таблицы), затем выводит всё через to_string.
|
||||
'''
|
||||
# Колонки в том же порядке и с теми же подписями, что на сайте КХЛ.
|
||||
cols = ['И', 'В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П', 'ГЗ', 'ГП', 'Р', 'О', '%О']
|
||||
view = table[cols].copy() # берём только нужные столбцы (копия, чтобы не трогать оригинал)
|
||||
|
|
@ -195,6 +270,12 @@ def show_table(table, title):
|
|||
|
||||
|
||||
def main():
|
||||
'''
|
||||
Точка входа. Связывает все шаги вместе:
|
||||
читает путь к CSV из аргумента командной строки -> по имени файла определяет
|
||||
сезон -> строит таблицу через build_standings -> печатает общую таблицу и две
|
||||
таблицы по конференциям (Запад/Восток), отфильтрованные по столбцу 'Конф'.
|
||||
'''
|
||||
path = sys.argv[1] # получаем путь до csv
|
||||
name = os.path.basename(path) # из khl_3/khl_*.csv берём только khl_*.csv
|
||||
season = name.replace('khl_', '').replace('.csv', '')
|
||||
|
|
@ -212,5 +293,5 @@ def main():
|
|||
show_table(west, 'Конференция Запад ' + season)
|
||||
show_table(east, 'Конференция Восток ' + season)
|
||||
|
||||
|
||||
main()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
379
new_main.py
Normal file
379
new_main.py
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
'''
|
||||
new_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. Для таблиц конференций учитываются дивизионы: в нужных сезонах победители
|
||||
дивизионов получают места 1 и 2 внутри своей конференции.
|
||||
6. show_table(...) печатает таблицу: общую по чемпионату и отдельно по двум
|
||||
конференциям (Запад / Восток).
|
||||
|
||||
ВХОДНЫЕ ДАННЫЕ (столбцы CSV):
|
||||
Команда_1 - хозяева, Команда_2 - гости;
|
||||
Период_1/2/3 - счёт по периодам в формате "3:1" (хозяева:гости);
|
||||
Овертайм, Буллиты - счёт за овертайм и серию буллитов (или ":" если их не было).
|
||||
|
||||
Зависимости: pandas (таблицы), numpy (нумерация мест).
|
||||
'''
|
||||
|
||||
import sys # для аргументов командной строки
|
||||
import os # чтобы достать имя файла из пути
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
def make_conf(west, east):
|
||||
'''
|
||||
Собрать словарь {название команды -> конференция} из двух списков команд.
|
||||
|
||||
Принимает: west, east - списки названий команд Западной и Восточной конференций.
|
||||
Возвращает: dict, где каждой команде сопоставлена строка 'Запад' или 'Восток'.
|
||||
|
||||
Нужен, чтобы позже одной операцией .map() приписать каждой команде её конференцию.
|
||||
'''
|
||||
d = {}
|
||||
d.update({team: 'Запад' for team in west})
|
||||
d.update({team: 'Восток' for team in east})
|
||||
return d
|
||||
|
||||
|
||||
# ключ - это код сезона (например, 2018_19)
|
||||
CONFERENCES = {
|
||||
'2018_19': make_conf(
|
||||
west=['ЦСКА', 'СКА', 'Локомотив', 'Йокерит', 'Динамо М', 'ХК Сочи',
|
||||
'Спартак', 'Витязь', 'Динамо Р', 'Северсталь', 'Динамо Мн', 'Слован'],
|
||||
east=['Автомобилист', 'Барыс', 'Металлург Мг', 'Авангард', 'Ак Барс',
|
||||
'Салават Юлаев', 'Торпедо НН', 'Трактор', 'Сибирь', 'Нефтехимик',
|
||||
'Куньлунь РС', 'Адмирал', 'Амур'],
|
||||
),
|
||||
'2019_20': make_conf(
|
||||
west=['ЦСКА', 'СКА', 'Йокерит', 'Динамо М', 'Спартак', 'Локомотив',
|
||||
'Витязь', 'Торпедо НН', 'ХК Сочи', 'Северсталь', 'Динамо Р', 'Динамо Мн'],
|
||||
east=['Ак Барс', 'Барыс', 'Авангард', 'Автомобилист', 'Сибирь',
|
||||
'Салават Юлаев', 'Металлург Мг', 'Нефтехимик', 'Амур', 'Куньлунь РС',
|
||||
'Трактор', 'Адмирал'],
|
||||
),
|
||||
'2020_21': make_conf(
|
||||
west=['ЦСКА', 'СКА', 'Динамо М', 'Локомотив', 'Йокерит', 'Северсталь',
|
||||
'Динамо Мн', 'Спартак', 'Витязь', 'ХК Сочи', 'Динамо Р'],
|
||||
east=['Ак Барс', 'Авангард', 'Металлург Мг', 'Салават Юлаев', 'Трактор',
|
||||
'Барыс', 'Автомобилист', 'Торпедо НН', 'Сибирь', 'Амур', 'Нефтехимик',
|
||||
'Куньлунь РС'],
|
||||
),
|
||||
'2021_22': make_conf(
|
||||
west=['СКА', 'Йокерит', 'ЦСКА', 'Динамо М', 'Северсталь', 'Локомотив',
|
||||
'Спартак', 'Динамо Мн', 'Торпедо', 'Витязь', 'ХК Сочи', 'Динамо Р'],
|
||||
east=['Металлург Мг', 'Трактор', 'Салават Юлаев', 'Ак Барс', 'Авангард',
|
||||
'Сибирь', 'Нефтехимик', 'Барыс', 'Автомобилист', 'Амур', 'Адмирал',
|
||||
'Куньлунь РС'],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def make_div(bobrov, tarasov, kharlamov, chernyshev):
|
||||
'''
|
||||
Собрать словарь {название команды -> дивизион} из четырёх списков команд.
|
||||
|
||||
Дивизионы Боброва и Тарасова относятся к Западу, Харламова и Чернышёва -
|
||||
к Востоку. Нужны для финального посева победителей дивизионов в таблицах
|
||||
конференций.
|
||||
'''
|
||||
d = {}
|
||||
d.update({team: 'Боброва' for team in bobrov})
|
||||
d.update({team: 'Тарасова' for team in tarasov})
|
||||
d.update({team: 'Харламова' for team in kharlamov})
|
||||
d.update({team: 'Чернышёва' for team in chernyshev})
|
||||
return d
|
||||
|
||||
|
||||
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=['Авангард', 'Амур', 'Барыс', 'Сибирь', 'Салават Юлаев', 'Адмирал'],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
SEED_BY_DIVISION = {
|
||||
'2018_19': True,
|
||||
'2019_20': True,
|
||||
'2020_21': True,
|
||||
'2021_22': False,
|
||||
}
|
||||
|
||||
|
||||
def seed_conference(conf_table):
|
||||
'''
|
||||
Поставить победителей двух дивизионов на места 1 и 2 в конференции.
|
||||
|
||||
На вход приходит таблица одной конференции, уже отсортированная по основным
|
||||
правилам КХЛ. Поэтому первая команда каждого дивизиона - победитель
|
||||
дивизиона, а порядок между двумя победителями уже правильный.
|
||||
'''
|
||||
winners = conf_table.groupby('Див', sort=False).head(1)
|
||||
rest = conf_table.drop(winners.index)
|
||||
return pd.concat([winners, rest])
|
||||
|
||||
|
||||
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)
|
||||
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)
|
||||
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)
|
||||
return home, away
|
||||
|
||||
|
||||
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 периоде
|
||||
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 # сколько гости забили за три периода
|
||||
|
||||
# Определяем ТИП исхода каждого матча через сравнение столбцов
|
||||
# (любое сравнение возвращает столбец False/True)
|
||||
|
||||
tie = (reg_h == reg_a) # True там, где после 3 периодов была ничья
|
||||
|
||||
win_home_reg = (reg_h > reg_a) # хозяева победили в основное время
|
||||
win_away_reg = (reg_a > reg_h) # гости победили в основное время
|
||||
|
||||
win_home_ot = tie & (oth > ota) # ничья в основное + хозяева забили в ОТ = победа в ОТ
|
||||
win_away_ot = tie & (ota > oth) # ничья в основное + гости забили в ОТ
|
||||
|
||||
win_home_so = tie & (oth == ota) & (soh > soa) # ОТ ничейный -> буллиты выиграли хозяева
|
||||
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
|
||||
|
||||
'''
|
||||
Считаем заброшенные шайбы для итоговой таблицы.
|
||||
К голам в основное время прибавляем голы овертайма, а победителю серии буллитов
|
||||
добавляем ровно 1 решающую шайбу. astype(int) переводит True/False в 1/0.
|
||||
'''
|
||||
goals_home = reg_h + oth + win_home_so.astype(int) # все шайбы хозяев
|
||||
goals_away = reg_a + ota + win_away_so.astype(int) # все шайбы гостей
|
||||
|
||||
'''
|
||||
Конвертируем "по одной строке на матч" -> "по две строки на матч".
|
||||
В каждом матче участвует 2 команды. Делаем две таблицы:
|
||||
home - взгляд со стороны хозяев, away - со стороны гостей,
|
||||
а потом склеиваем и группируем по названию команды.
|
||||
Столбцы В, ВО, ВБ, ПБ, ПО, П - это булевы маски (выиграл/проиграл).
|
||||
'''
|
||||
home = pd.DataFrame({
|
||||
'Команда': df['Команда_1'], # имя хозяев
|
||||
'В': win_home_reg, # победа в основное время
|
||||
'ВО': win_home_ot, # победа в овертайме
|
||||
'ВБ': win_home_so, # победа по буллитам
|
||||
'ПБ': win_away_so, # поражение по буллитам (гости выиграли буллиты)
|
||||
'ПО': win_away_ot, # поражение в овертайме
|
||||
'П': win_away_reg, # поражение в основное время
|
||||
'ГЗ': goals_home, # голы, забитые хозяевами
|
||||
'ГП': goals_away, # голы, пропущенные (забитые гостями)
|
||||
})
|
||||
away = pd.DataFrame({
|
||||
'Команда': df['Команда_2'],
|
||||
'В': win_away_reg,
|
||||
'ВО': win_away_ot,
|
||||
'ВБ': win_away_so,
|
||||
'ПБ': win_home_so,
|
||||
'ПО': win_home_ot,
|
||||
'П': win_home_reg,
|
||||
'ГЗ': goals_away,
|
||||
'ГП': goals_home,
|
||||
})
|
||||
|
||||
# pd.concat склеивает две таблицы одну под другой (как два списка матчей подряд).
|
||||
# ignore_index=True перенумеровывает строки заново, чтобы номера не повторялись.
|
||||
both = pd.concat([home, away], ignore_index=True)
|
||||
|
||||
# Булевы столбцы (True/False) переводим в числа (1/0), чтобы их можно было суммировать.
|
||||
flag_cols = ['В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П']
|
||||
both[flag_cols] = both[flag_cols].astype(int)
|
||||
|
||||
# Голы тоже переводим в целые числа (после fillna(0) они стали дробными).
|
||||
both[['ГЗ', 'ГП']] = both[['ГЗ', 'ГП']].astype(int)
|
||||
|
||||
# Суммируем всё по каждой команде.
|
||||
# groupby('Команда').sum() - сгруппируй строки по названию команды и сложи числа.
|
||||
table = both.groupby('Команда').sum()
|
||||
|
||||
# Производные показатели:
|
||||
|
||||
# И - количество игр = сумма всех исходов (каждый матч - ровно один из шести исходов)
|
||||
table['И'] = table[flag_cols].sum(axis=1) # axis=1 - складывать вдоль строки
|
||||
# О - очки по системе КХЛ: любая победа = 2, поражение в ОТ/буллитах = 1, в осн. время = 0
|
||||
table['О'] = 2 * (table['В'] + table['ВО'] + table['ВБ']) + (table['ПБ'] + table['ПО'])
|
||||
# Р - разница шайб (дополнительный критерий при равенстве)
|
||||
table['Р'] = table['ГЗ'] - table['ГП']
|
||||
|
||||
'''
|
||||
%О - процент набранных очков = очки / максимально возможные (2 за каждую игру).
|
||||
По этому проценту КХЛ расставляет места, когда команды сыграли разное число игр.
|
||||
Сайт КХЛ НЕ округляет, а отбрасывает знаки после второго (72.448 -> 72.44),
|
||||
поэтому считаем в целых числах: умножаем на 10000 и делим нацело (//) на 2*И -
|
||||
остаток (дробная часть) отбрасывается, затем делим на 100. Целочисленная
|
||||
арифметика без float, поэтому ровные значения (57.00) не съезжают в 56.99.
|
||||
'''
|
||||
table['%О'] = (table['О'] * 10000 // (2 * table['И'])) / 100
|
||||
# Подписываем каждой команде её конференцию. .map(словарь) проходит по индексу
|
||||
# (названиям команд) и подставляет значение из словаря.
|
||||
table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)
|
||||
|
||||
'''
|
||||
Сортировка: по проценту очков, затем при равенстве - больше побед в основное время,
|
||||
затем в овертайме, затем по буллитам, затем лучше разница шайб, затем больше забито.
|
||||
'''
|
||||
table = table.sort_values(
|
||||
by=['%О', 'В', 'ВО', 'ВБ', 'Р', 'ГЗ'],
|
||||
ascending=False,
|
||||
)
|
||||
return table
|
||||
|
||||
|
||||
def show_table(table, title):
|
||||
'''
|
||||
Напечатать турнирную таблицу в консоль в виде текста.
|
||||
|
||||
Принимает: table - готовая (уже отсортированная) таблица команд;
|
||||
title - заголовок над таблицей.
|
||||
Ничего не возвращает - только печатает.
|
||||
|
||||
Что делает: оставляет нужные столбцы в порядке, как на сайте КХЛ, добавляет
|
||||
слева колонку "Место" (1..N по порядку строк) и колонку "Команда"
|
||||
(название берётся из индекса таблицы), затем выводит всё через to_string.
|
||||
'''
|
||||
# Колонки в том же порядке и с теми же подписями, что на сайте КХЛ.
|
||||
cols = ['И', 'В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П', 'ГЗ', 'ГП', 'Р', 'О', '%О']
|
||||
view = table[cols].copy() # берём только нужные столбцы (копия, чтобы не трогать оригинал)
|
||||
# Место по порядку строк. np.arange(1, N+1) создаёт массив [1..N].
|
||||
view.insert(0, 'Место', np.arange(1, len(view) + 1))
|
||||
# Имя команды (индекс таблицы) ставим отдельным столбцом, чтобы оно было видно при выводе.
|
||||
view.insert(1, 'Команда', view.index)
|
||||
|
||||
print('\n' + '=' * 80)
|
||||
print(title)
|
||||
print('=' * 80)
|
||||
print(view.to_string(index=False))
|
||||
|
||||
|
||||
def main():
|
||||
'''
|
||||
Точка входа. Связывает все шаги вместе:
|
||||
читает путь к CSV из аргумента командной строки -> по имени файла определяет
|
||||
сезон -> строит таблицу через build_standings -> печатает общую таблицу и две
|
||||
таблицы по конференциям (Запад/Восток), отфильтрованные по столбцу 'Конф'.
|
||||
Для сезонов с правилом посева победители дивизионов поднимаются на места 1-2.
|
||||
'''
|
||||
path = sys.argv[1] # получаем путь до csv
|
||||
name = os.path.basename(path) # из khl_3/khl_*.csv берём только khl_*.csv
|
||||
season = name.replace('khl_', '').replace('.csv', '')
|
||||
conf_map = CONFERENCES[season]
|
||||
div_map = DIVISIONS[season]
|
||||
|
||||
df = pd.read_csv(path)
|
||||
table = build_standings(df, conf_map)
|
||||
table['Див'] = pd.Series(table.index, index=table.index).map(div_map)
|
||||
|
||||
# Итоговая таблица чемпионата
|
||||
show_table(table, 'Итоговая таблица чемпионата ' + season)
|
||||
|
||||
# Итоговые таблицы по конференциям (фильтруем по столбцу Конф)
|
||||
west = table[table['Конф'] == 'Запад']
|
||||
east = table[table['Конф'] == 'Восток']
|
||||
if SEED_BY_DIVISION[season]:
|
||||
west = seed_conference(west)
|
||||
east = seed_conference(east)
|
||||
|
||||
show_table(west, 'Конференция Запад ' + season)
|
||||
show_table(east, 'Конференция Восток ' + season)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
437
РАЗБОР_КОДА.md
Normal file
437
РАЗБОР_КОДА.md
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
# Разбор кода построчно (для защиты)
|
||||
|
||||
Здесь разобраны три файла проекта — **`main.py`**, **`divisions.py`**, **`3.py`** —
|
||||
и **каждая функция из библиотек** (pandas, numpy, matplotlib, sys, os).
|
||||
Читается сверху вниз — можно отвечать прямо по тексту.
|
||||
|
||||
---
|
||||
|
||||
## Справочник функций из библиотек
|
||||
|
||||
Чтобы не повторять одно и то же, вот что делает каждая чужая функция/метод в проекте:
|
||||
|
||||
### `sys` и `os` (стандартная библиотека Python)
|
||||
* `sys.argv` — список аргументов командной строки. `sys.argv[0]` — имя скрипта, `sys.argv[1]` — первый аргумент и т.д.
|
||||
* `os.path.basename(path)` — берёт из пути `khl_3/khl_2018_19.csv` только имя файла `khl_2018_19.csv`.
|
||||
* `os.path.abspath(path)` / `os.path.dirname(path)` — полный путь к файлу / папка, в которой он лежит. Нужны в `divisions.py`, чтобы найти `main.py` рядом с собой.
|
||||
|
||||
### `pandas` (сокращённо `pd`) — работа с таблицами
|
||||
* `pd.read_csv(path)` — читает CSV-файл в **DataFrame** (таблицу).
|
||||
* **DataFrame** — двумерная таблица (строки × столбцы). `df['Столбец']` — это один столбец (объект **Series**).
|
||||
* **Series** — один столбец таблицы: массив значений с индексом (номерами/подписями строк).
|
||||
* `df[col].str.split(':', expand=True)` — у строкового столбца разбивает каждую ячейку по `:`; `expand=True` раскладывает результат в несколько столбцов.
|
||||
* `df[col].astype(str).str.strip()` — приводит столбец к тексту и убирает пробелы по краям.
|
||||
* `series.str.startswith('+')` / `series.str.endswith('+')` — маска `True/False`: начинается/заканчивается ли строка на `+`.
|
||||
* `pd.to_numeric(series, errors='coerce')` — превращает текст в числа; `errors='coerce'` ставит `NaN` (пусто), если значение не число.
|
||||
* `series.fillna(0)` — заменяет пустые значения `NaN` на 0.
|
||||
* `series.where(условие, 0)` — оставляет значение, где условие `True`, иначе ставит 0.
|
||||
* `pd.DataFrame({...})` — создаёт новую таблицу из словаря `{'имя столбца': данные}`.
|
||||
* `pd.concat([df1, df2], ignore_index=True)` — склеивает таблицы одну под другой; `ignore_index=True` перенумеровывает строки заново.
|
||||
* `series.astype(int)` — переводит тип в целые числа (в т.ч. `True/False → 1/0`).
|
||||
* `df.groupby('Команда').sum()` — группирует строки по командам и складывает числа внутри каждой группы.
|
||||
* `df.groupby('Див', sort=False).head(1)` — берёт первую строку каждой группы (`sort=False` — не пересортировывать группы).
|
||||
* `df[cols].sum(axis=1)` — сумма по строке (вдоль столбцов). `axis=0` — по столбцу.
|
||||
* `df.sort_values(by=[...], ascending=False)` — сортирует строки по указанным столбцам (по убыванию).
|
||||
* `series.map(словарь)` — заменяет каждое значение по словарю (ключ → значение).
|
||||
* `df.copy()` — делает независимую копию таблицы (чтобы не менять оригинал).
|
||||
* `df.drop(индексы)` — выбрасывает строки с указанными индексами.
|
||||
* `df.insert(позиция, 'имя', данные)` — вставляет новый столбец на заданную позицию.
|
||||
* `df.to_string(index=False)` — превращает таблицу в текст для печати; `index=False` — без столбца индекса.
|
||||
* `df.index` — подписи строк (здесь — названия команд после `groupby`).
|
||||
* `pd.to_datetime(series)` — превращает текст с датой в настоящую дату (чтобы ось X шла по времени).
|
||||
|
||||
### `numpy` (сокращённо `np`) — числовые операции
|
||||
* `np.arange(1, N+1)` — массив чисел `[1, 2, ..., N]` (для нумерации мест).
|
||||
|
||||
### `matplotlib.pyplot` (сокращённо `plt`) — графики
|
||||
* `plt.subplots(figsize=(ш, в))` — создаёт холст (`fig`) и оси (`ax`) заданного размера в дюймах.
|
||||
* `ax.plot(x, y, marker='o')` — линия с точками-маркерами.
|
||||
* `ax.axhline(0, ...)` — горизонтальная линия на уровне 0.
|
||||
* `ax.set_title / set_xlabel / set_ylabel` — заголовок и подписи осей.
|
||||
* `ax.grid(True, alpha=...)` — координатная сетка (`alpha` — прозрачность).
|
||||
* `fig.autofmt_xdate()` — автоматически поворачивает подписи дат, чтобы не слипались.
|
||||
* `fig.savefig(path, dpi=150, bbox_inches='tight')` — сохранить график в файл (`tight` — обрезать поля).
|
||||
* `plt.show()` — показать график в окне.
|
||||
|
||||
---
|
||||
|
||||
## `main.py` — построчно
|
||||
|
||||
```python
|
||||
import sys # доступ к аргументам командной строки (sys.argv)
|
||||
import os # работа с путями файлов (os.path.basename)
|
||||
import pandas as pd # таблицы (DataFrame); pd — общепринятое сокращение
|
||||
import numpy as np # числовые операции (np.arange для нумерации мест)
|
||||
```
|
||||
|
||||
### Функция `make_conf` — собирает словарь «команда → конференция»
|
||||
|
||||
```python
|
||||
def make_conf(west, east):
|
||||
d = {} # пустой словарь
|
||||
d.update({team: 'Запад' for team in west}) # каждой команде с Запада ставим метку 'Запад'
|
||||
d.update({team: 'Восток' for team in east}) # каждой команде с Востока — 'Восток'
|
||||
return d # отдаём готовый словарь
|
||||
```
|
||||
* `{team: 'Запад' for team in west}` — **словарное включение** (dict comprehension): пробегает по списку команд и строит словарь `{'ЦСКА': 'Запад', ...}`.
|
||||
* `d.update(...)` — добавляет пары в словарь `d`.
|
||||
|
||||
### Словарь `CONFERENCES`
|
||||
|
||||
```python
|
||||
CONFERENCES = {
|
||||
'2018_19': make_conf(west=[...], east=[...]), # для каждого сезона свой состав конференций
|
||||
'2019_20': make_conf(...),
|
||||
...
|
||||
}
|
||||
```
|
||||
* Ключ — код сезона (`'2018_19'`), значение — словарь «команда → конференция».
|
||||
* Зачем: составы конференций по годам менялись, поэтому для каждого сезона свой список.
|
||||
|
||||
### Функция `parse_score` — разбор счёта «3:1»
|
||||
|
||||
```python
|
||||
def parse_score(df, col):
|
||||
parts = df[col].str.split(':', expand=True) # "3:1" -> два столбца: ["3"], ["1"]
|
||||
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)# левое число -> голы хозяев; пусто/знак -> 0
|
||||
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)# правое число -> голы гостей; пусто/знак -> 0
|
||||
return home, away # возвращаем два столбца (Series)
|
||||
```
|
||||
* `parts[0]` / `parts[1]` — первый и второй столбцы результата split.
|
||||
* Пустой период `:` или знаки техрезультата (`+ — -`) после split не превращаются в число → `to_numeric` делает `NaN` → `fillna(0)` ставит 0.
|
||||
|
||||
### Функция `build_standings` — главный расчёт таблицы
|
||||
|
||||
```python
|
||||
def build_standings(df, conf_map):
|
||||
h1, a1 = parse_score(df, 'Период_1') # голы хозяев(h)/гостей(a) в 1-м периоде
|
||||
h2, a2 = parse_score(df, 'Период_2') # во 2-м
|
||||
h3, a3 = parse_score(df, 'Период_3') # в 3-м
|
||||
oth, ota = parse_score(df, 'Овертайм') # в овертайме (0/0, если его не было)
|
||||
soh, soa = parse_score(df, 'Буллиты') # в серии буллитов (0/0, если её не было)
|
||||
```
|
||||
Здесь `h1, a1 = ...` — **распаковка кортежа**: функция вернула два значения, кладём их в две переменные.
|
||||
|
||||
```python
|
||||
reg_h = h1 + h2 + h3 # голы хозяев в основное время (поэлементная сумма столбцов)
|
||||
reg_a = a1 + a2 + a3 # голы гостей в основное время
|
||||
```
|
||||
Сложение двух Series складывает их **построчно** (матч за матчем).
|
||||
|
||||
```python
|
||||
tie = (reg_h == reg_a) # столбец True/False: True там, где в основное время ничья
|
||||
win_home_reg = (reg_h > reg_a)# хозяева выиграли в основное время
|
||||
win_away_reg = (reg_a > reg_h)# гости выиграли в основное время
|
||||
```
|
||||
Сравнение столбцов (`==`, `>`) возвращает **столбец булевых значений** (маску).
|
||||
|
||||
```python
|
||||
win_home_ot = tie & (oth > ota) # была ничья И хозяева забили в ОТ -> победа в овертайме
|
||||
win_away_ot = tie & (ota > oth) # ничья И гости забили в ОТ
|
||||
win_home_so = tie & (oth == ota) & (soh > soa) # ничья + ОТ ничейный -> буллиты выиграли хозяева
|
||||
win_away_so = tie & (oth == ota) & (soa > soh) # буллиты выиграли гости
|
||||
```
|
||||
`&` — поэлементное «И» для булевых столбцов (обе маски должны быть True в одной строке).
|
||||
|
||||
**Технические результаты** (неявка/отмена матча):
|
||||
|
||||
```python
|
||||
p1 = df['Период_1'].astype(str).str.strip()
|
||||
tech_home = p1.str.startswith('+') # "+ — -": техническая победа хозяев
|
||||
tech_away = p1.str.endswith('+') # "- — +": техническая победа гостей
|
||||
tech = tech_home | tech_away
|
||||
```
|
||||
В файле такой матч записан не цифрами, а знаками, поэтому обычная классификация выше сочла его «ничьёй 0:0». Перекрываем её:
|
||||
|
||||
```python
|
||||
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
|
||||
```
|
||||
* `~tech` — поэлементное «НЕ» (инвертирует маску): «это НЕ технический матч».
|
||||
* КХЛ засчитывает техрезультат как победу/поражение в основное время (2 и 0 очков), но шайбы не начисляет — поэтому ниже голы у такого матча останутся 0:0.
|
||||
|
||||
```python
|
||||
goals_home = reg_h + oth + win_home_so.astype(int) # все шайбы хозяев: основное + ОТ + 1 за выигр. буллиты
|
||||
goals_away = reg_a + ota + win_away_so.astype(int) # все шайбы гостей
|
||||
```
|
||||
`win_home_so.astype(int)` — `True/False` превращаем в `1/0`, чтобы прибавить как число. У технического матча все слагаемые равны 0, поэтому шайб нет.
|
||||
|
||||
```python
|
||||
home = pd.DataFrame({ # таблица «глазами хозяев»: одна строка = один матч
|
||||
'Команда': df['Команда_1'],# имя команды-хозяина
|
||||
'В': win_home_reg, 'ВО': win_home_ot, 'ВБ': win_home_so, # её победы (маски)
|
||||
'ПБ': win_away_so, 'ПО': win_away_ot, 'П': win_away_reg, # её поражения (= победы соперника)
|
||||
'ГЗ': goals_home, # забила
|
||||
'ГП': goals_away, # пропустила
|
||||
})
|
||||
away = pd.DataFrame({ # та же логика «глазами гостей», поля зеркальные
|
||||
'Команда': df['Команда_2'],
|
||||
'В': win_away_reg, 'ВО': win_away_ot, 'ВБ': win_away_so,
|
||||
'ПБ': win_home_so, 'ПО': win_home_ot, 'П': win_home_reg,
|
||||
'ГЗ': goals_away, 'ГП': goals_home,
|
||||
})
|
||||
```
|
||||
**Главный приём:** один матч превращаем в две записи — статистику хозяев и статистику гостей.
|
||||
|
||||
```python
|
||||
both = pd.concat([home, away], ignore_index=True) # склеиваем обе таблицы в одну
|
||||
flag_cols = ['В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П'] # столбцы-исходы
|
||||
both[flag_cols] = both[flag_cols].astype(int) # True/False -> 1/0 (чтобы суммировать)
|
||||
both[['ГЗ', 'ГП']] = both[['ГЗ', 'ГП']].astype(int)# голы делаем целыми (после fillna были дробными)
|
||||
table = both.groupby('Команда').sum() # группируем по команде и складываем все числа
|
||||
```
|
||||
После этого индекс таблицы — названия команд, а в ячейках — суммы за сезон.
|
||||
|
||||
```python
|
||||
table['И'] = table[flag_cols].sum(axis=1) # игры = сумма всех 6 исходов (axis=1 = вдоль строки)
|
||||
table['О'] = 2*(table['В']+table['ВО']+table['ВБ']) + (table['ПБ']+table['ПО']) # очки: победа=2, пораж. в ОТ/буллитах=1
|
||||
table['Р'] = table['ГЗ'] - table['ГП'] # разница шайб
|
||||
```
|
||||
|
||||
```python
|
||||
table['%О'] = (table['О'] * 10000 // (2 * table['И'])) / 100
|
||||
```
|
||||
Процент очков = очки / максимум (2 за игру). Считается **в целых числах**: умножаем на 10000, делим **нацело** (`//`) на `2*И` — дробная часть отбрасывается (как на сайте КХЛ), потом делим на 100. Целочисленная арифметика не даёт ошибок float, поэтому ровные значения (57.00) не съезжают в 56.99.
|
||||
|
||||
```python
|
||||
table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)
|
||||
```
|
||||
Берём названия команд (индекс), и `.map(conf_map)` подставляет каждой её конференцию из словаря.
|
||||
|
||||
```python
|
||||
table = table.sort_values(by=['%О', 'В', 'ВО', 'ВБ', 'Р', 'ГЗ'], ascending=False)
|
||||
return table
|
||||
```
|
||||
Сортировка по регламенту: сначала по %очков, при равенстве — больше побед в осн. время, затем в ОТ, по буллитам, разница шайб, забитые. `ascending=False` = по убыванию.
|
||||
|
||||
### Функция `show_table` — печать таблицы
|
||||
|
||||
```python
|
||||
def show_table(table, title):
|
||||
cols = ['И','В','ВО','ВБ','ПБ','ПО','П','ГЗ','ГП','Р','О','%О'] # порядок столбцов как на сайте КХЛ
|
||||
view = table[cols].copy() # берём только эти столбцы (копия, чтобы не портить оригинал)
|
||||
view.insert(0, 'Место', np.arange(1, len(view) + 1)) # столбец «Место» = 1,2,3,... (строки уже отсортированы)
|
||||
view.insert(1, 'Команда', view.index) # имя команды из индекса делаем видимым столбцом
|
||||
print('\n' + '=' * 80) # разделитель из 80 знаков «=»
|
||||
print(title) # заголовок
|
||||
print('=' * 80)
|
||||
print(view.to_string(index=False)) # печатаем таблицу без служебного индекса
|
||||
```
|
||||
* `len(view)` — количество строк (команд).
|
||||
* `'=' * 80` — повтор строки 80 раз.
|
||||
|
||||
### Функция `main` — точка входа
|
||||
|
||||
```python
|
||||
def main():
|
||||
path = sys.argv[1] # путь к csv из командной строки
|
||||
name = os.path.basename(path) # только имя файла: khl_2018_19.csv
|
||||
season = name.replace('khl_', '').replace('.csv', '') # -> '2018_19'
|
||||
conf_map = CONFERENCES[season] # берём конференции нужного сезона
|
||||
|
||||
df = pd.read_csv(path) # читаем матчи в таблицу
|
||||
table = build_standings(df, conf_map) # строим турнирную таблицу
|
||||
|
||||
show_table(table, 'Итоговая таблица чемпионата ' + season) # печатаем общую таблицу
|
||||
|
||||
west = table[table['Конф'] == 'Запад'] # строки только западной конференции
|
||||
east = table[table['Конф'] == 'Восток'] # только восточной
|
||||
show_table(west, 'Конференция Запад ' + season)
|
||||
show_table(east, 'Конференция Восток ' + season)
|
||||
|
||||
main() # запуск программы
|
||||
```
|
||||
* `str.replace('a','')` — удаляет подстроку (заменяет на пустую).
|
||||
* `table[table['Конф'] == 'Запад']` — **фильтрация по маске**: оставляем строки, где условие True.
|
||||
|
||||
---
|
||||
|
||||
## `divisions.py` — дивизионы и посев победителей дивизионов
|
||||
|
||||
Зачем нужен: в сезонах 2018/19–2020/21 победители двух дивизионов занимали места 1–2
|
||||
в конференции, даже если по очкам были ниже. `main.py` этого не делает, поэтому правило
|
||||
вынесено в **отдельный файл**, а `main.py` не трогается.
|
||||
|
||||
```python
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
```
|
||||
|
||||
### Переиспользование логики из `main.py`
|
||||
|
||||
```python
|
||||
_main_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'main.py')
|
||||
_src = open(_main_path, encoding='utf-8').read() # читаем исходник main.py как текст
|
||||
_src = _src.replace('\nmain()\n', '\n') # убираем последнюю строку с автозапуском
|
||||
_ns = {}
|
||||
exec(compile(_src, 'main.py', 'exec'), _ns) # выполняем код -> получаем его функции в _ns
|
||||
build_standings = _ns['build_standings'] # забираем готовый расчёт таблицы
|
||||
CONFERENCES = _ns['CONFERENCES'] # и словарь конференций
|
||||
```
|
||||
* `main.py` в конце сам вызывает `main()`, поэтому обычный `import main` тут же попытался бы прочитать аргументы и упал. Поэтому читаем текст, убираем строку `main()` и выполняем.
|
||||
* `exec(compile(...), _ns)` — выполнить код, сложив все его имена (функции, словари) в словарь `_ns`. Так получаем `build_standings` и `CONFERENCES`, ничего не запуская и не печатая.
|
||||
* **Главное:** логика расчёта не дублируется — единственный источник правды остаётся в `main.py`.
|
||||
|
||||
### Функция `make_div` — словарь «команда → дивизион»
|
||||
|
||||
```python
|
||||
def make_div(bobrov, tarasov, kharlamov, chernyshev):
|
||||
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
|
||||
```
|
||||
Аналог `make_conf`, только делений не два (Запад/Восток), а четыре дивизиона.
|
||||
|
||||
### Словари `DIVISIONS` и `SEED_BY_DIVISION`
|
||||
|
||||
```python
|
||||
DIVISIONS = {
|
||||
'2018_19': make_div(bobrov=[...], tarasov=[...], kharlamov=[...], chernyshev=[...]),
|
||||
...
|
||||
}
|
||||
SEED_BY_DIVISION = {'2018_19': True, '2019_20': True, '2020_21': True, '2021_22': False}
|
||||
```
|
||||
* `DIVISIONS` — состав четырёх дивизионов по каждому сезону (имена команд ровно как в CSV).
|
||||
* `SEED_BY_DIVISION` — в каких сезонах посев победителей применялся. В 2021/22 — `False` (КХЛ перешла к чистому порядку по %О).
|
||||
|
||||
### Функция `seed_conference` — посев победителей дивизионов
|
||||
|
||||
```python
|
||||
def seed_conference(conf_table):
|
||||
winners = conf_table.groupby('Див', sort=False).head(1) # лучший из каждого дивизиона = его победитель
|
||||
rest = conf_table.drop(winners.index) # все остальные
|
||||
return pd.concat([winners, rest]) # победители наверх, остальные следом
|
||||
```
|
||||
* Таблица уже отсортирована по %О, поэтому `head(1)` внутри дивизиона = победитель дивизиона.
|
||||
* `drop(winners.index)` убирает этих двоих из остального списка, чтобы они не повторились.
|
||||
* `concat` ставит победителей на места 1–2, затем — остальные в прежнем порядке.
|
||||
|
||||
### Функция `show_table` — печать с дивизионом
|
||||
|
||||
То же, что в `main.py`, но в `cols` добавлен столбец `'Див'`, чтобы было видно, почему
|
||||
победители дивизионов оказались наверху.
|
||||
|
||||
### Функция `main`
|
||||
|
||||
```python
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
season = os.path.basename(path).replace('khl_', '').replace('.csv', '')
|
||||
df = pd.read_csv(path)
|
||||
table = build_standings(df, CONFERENCES[season]) # таблица из логики main.py
|
||||
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) # если в этом сезоне посев был — применяем
|
||||
show_table(part, 'Конференция ' + conf + ' ' + season + ...)
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `3.py` — график разницы шайб команды по датам
|
||||
|
||||
```python
|
||||
import sys
|
||||
import pandas as pd
|
||||
import numpy as np # импортирован, но напрямую не используется
|
||||
import matplotlib.pyplot as plt # построение графика
|
||||
```
|
||||
|
||||
### Функция `parse_score`
|
||||
|
||||
Точная копия из `main.py` (тот же разбор счёта «3:1» на два числовых столбца) — правило
|
||||
подсчёта шайб одно и то же.
|
||||
|
||||
### Функция `match_goals` — голы хозяев и гостей в каждом матче
|
||||
|
||||
```python
|
||||
def match_goals(df):
|
||||
h1,a1 = parse_score(df,'Период_1'); h2,a2 = parse_score(df,'Период_2'); h3,a3 = parse_score(df,'Период_3')
|
||||
oth,ota = parse_score(df,'Овертайм'); soh,soa = parse_score(df,'Буллиты')
|
||||
|
||||
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)# гости выиграли буллиты
|
||||
|
||||
goals_home = reg_h + oth + win_home_so.astype(int) # все шайбы хозяев (+1 за буллиты)
|
||||
goals_away = reg_a + ota + win_away_so.astype(int) # все шайбы гостей
|
||||
|
||||
p1 = df['Период_1'].astype(str).str.strip()
|
||||
tech = p1.str.startswith('+') | p1.str.endswith('+')# технический результат
|
||||
goals_home = goals_home.where(~tech, 0) # у техрезультата шайб нет (0:0)
|
||||
goals_away = goals_away.where(~tech, 0)
|
||||
return goals_home, goals_away
|
||||
```
|
||||
* Та же логика подсчёта шайб, что в `main.py`.
|
||||
* `series.where(~tech, 0)` — оставить значение, где матч НЕ технический, иначе 0. Так разница у такого матча будет 0, как на сайте КХЛ.
|
||||
|
||||
### Функция `plot_team_diff` — построение и сохранение графика
|
||||
|
||||
```python
|
||||
def plot_team_diff(team, filename):
|
||||
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)
|
||||
|
||||
rows = both[both['Команда'] == team].copy() # только матчи нужной команды
|
||||
rows['Дата'] = pd.to_datetime(rows['Дата']) # текст -> дата (для оси X по времени)
|
||||
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()
|
||||
```
|
||||
* `'ГП'` = пропущено = забито соперником, поэтому в `away` поля `ГЗ`/`ГП` меняются местами.
|
||||
* `pd.to_datetime` нужен, чтобы matplotlib расставил матчи по оси времени правильно.
|
||||
|
||||
### Функция `main`
|
||||
|
||||
```python
|
||||
def main():
|
||||
filename = sys.argv[1] # путь к CSV
|
||||
team = sys.argv[2] # название команды
|
||||
plot_team_diff(team, filename)
|
||||
|
||||
main()
|
||||
```
|
||||
Запуск: `python3 3.py khl_3/khl_2018_19.csv "ЦСКА"`.
|
||||
|
||||
---
|
||||
|
||||
## Что точно спросят на защите — короткие ответы
|
||||
|
||||
* **Что такое DataFrame/Series?** Таблица и её отдельный столбец в pandas.
|
||||
* **Почему без циклов?** Векторные операции pandas быстрее и короче: одно действие сразу над всем столбцом.
|
||||
* **Что такое булева маска?** Столбец True/False из сравнения; по нему фильтруют строки и считают суммы через `.astype(int)`.
|
||||
* **Зачем `pd.concat` хозяев и гостей?** Чтобы у каждой команды была своя строка на каждый матч и сработал `groupby`.
|
||||
* **Как считается %очков?** В целых числах с отбрасыванием знаков (`*10000 // (2*И) /100`) — как сайт КХЛ, без округления и без ошибок float.
|
||||
* **Как обрабатываются технические результаты?** `+ — -` / `- — +` — это победа/поражение в основное время, но без шайб (0:0).
|
||||
* **Почему дивизионы в отдельном файле?** Чтобы не менять `main.py`; `divisions.py` переиспользует его функции через чтение и `exec` исходника без автозапуска.
|
||||
* **Зачем посев победителей дивизионов?** В 2018/19–2020/21 они получали места 1–2 в конференции независимо от очков; в 2021/22 правило отменили.
|
||||
Loading…
Reference in a new issue