update doc
This commit is contained in:
parent
bab805c8d6
commit
fb2d294459
2 changed files with 247 additions and 225 deletions
107
README.md
107
README.md
|
|
@ -1,9 +1,9 @@
|
||||||
# Летняя практика — Турнирные таблицы КХЛ
|
# Летняя практика — Турнирные таблицы КХЛ
|
||||||
|
|
||||||
Проект анализирует результаты матчей регулярного чемпионата КХЛ за сезоны
|
Проект анализирует результаты матчей регулярного чемпионата КХЛ за сезоны
|
||||||
**2018/19 – 2021/22** и строит по ним турнирные таблицы и графики.
|
**2018/19 – 2021/22** и строит по ним турнирные таблицы и график.
|
||||||
Основной инструмент — библиотека **pandas** (работа с таблицами) и
|
Основные инструменты — библиотека **pandas** (работа с таблицами) и
|
||||||
**matplotlib** (графики).
|
**matplotlib** (график).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -12,11 +12,9 @@
|
||||||
| Файл / папка | Назначение |
|
| Файл / папка | Назначение |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `main.py` | Основная программа: строит полную турнирную таблицу чемпионата и таблицы по конференциям (Запад / Восток). |
|
| `main.py` | Основная программа: строит полную турнирную таблицу чемпионата и таблицы по конференциям (Запад / Восток). |
|
||||||
| `individual_1.py` | Индивидуальное задание: гистограмма набранных очков по командам. |
|
| `divisions.py` | Отдельный модуль: добавляет деление на **дивизионы** и **правило посева** победителей дивизионов. Логику расчёта берёт из `main.py`, сам `main.py` не меняет. |
|
||||||
| `individual_4.py` | Индивидуальное задание: гистограмма всего заброшенных шайб за сезон. |
|
| `3.py` | Задание №3: **график** разницы заброшенных и пропущенных шайб одной команды по ходу сезона. |
|
||||||
| `individual_5.py` | Индивидуальное задание: гистограмма разницы шайб конкретной команды в заданном периоде по датам. |
|
| `khl_3/` | Исходные данные — CSV-файлы матчей (4 сезона, кодировка UTF-8). |
|
||||||
| `khl_3/` | Исходные данные — CSV-файлы матчей (кодировка UTF-8). |
|
|
||||||
| `khl_3_win/` | Те же данные в кодировке UTF-8 **с BOM** — открываются в Excel без «кракозябр». |
|
|
||||||
|
|
||||||
### Формат исходных данных (CSV)
|
### Формат исходных данных (CSV)
|
||||||
|
|
||||||
|
|
@ -28,6 +26,8 @@
|
||||||
* `Команда_1` — хозяева, `Команда_2` — гости.
|
* `Команда_1` — хозяева, `Команда_2` — гости.
|
||||||
* `Период_1..3`, `Овертайм`, `Буллиты` — счёт в формате `хозяева:гости`.
|
* `Период_1..3`, `Овертайм`, `Буллиты` — счёт в формате `хозяева:гости`.
|
||||||
* Пустой `:` означает, что период не игрался (нет овертайма/буллитов).
|
* Пустой `:` означает, что период не игрался (нет овертайма/буллитов).
|
||||||
|
* **Технические результаты** записаны знаками: `+ — -` (техпобеда хозяев) и
|
||||||
|
`- — +` (техпобеда гостей). Встречаются в сезоне 2020/21.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -39,18 +39,16 @@ pip install pandas numpy matplotlib
|
||||||
# Полная турнирная таблица + таблицы по конференциям
|
# Полная турнирная таблица + таблицы по конференциям
|
||||||
python3 main.py khl_3/khl_2018_19.csv
|
python3 main.py khl_3/khl_2018_19.csv
|
||||||
|
|
||||||
# Гистограмма очков (по желанию — сохранить в файл)
|
# Таблицы по конференциям с дивизионами и посевом победителей дивизионов
|
||||||
python3 individual_1.py khl_3/khl_2018_19.csv [output.png]
|
python3 divisions.py khl_3/khl_2020_21.csv
|
||||||
|
|
||||||
# Гистограмма заброшенных шайб
|
# График разницы шайб одной команды по датам (сохраняется в diff_<команда>.png)
|
||||||
python3 individual_4.py khl_3/khl_2018_19.csv
|
python3 3.py khl_3/khl_2018_19.csv "ЦСКА"
|
||||||
|
|
||||||
# Разница шайб команды в N-м периоде по датам
|
|
||||||
python3 individual_5.py khl_3/khl_2018_19.csv 2 "Ак Барс" [output.png]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> Имя файла важно: `main.py` достаёт код сезона (`2018_19`) из имени `khl_2018_19.csv`,
|
> Имя файла важно: программы достают код сезона (`2018_19`) из имени
|
||||||
> чтобы понять, какие команды в какой конференции играли.
|
> `khl_2018_19.csv`, чтобы понять, какие команды в какой конференции (и дивизионе)
|
||||||
|
> играли.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -61,7 +59,8 @@ python3 individual_5.py khl_3/khl_2018_19.csv 2 "Ак Барс" [output.png]
|
||||||
|
|
||||||
**Шаг 1. `parse_score(df, col)`** — разбирает строку счёта `"3:1"` на два числа.
|
**Шаг 1. `parse_score(df, col)`** — разбирает строку счёта `"3:1"` на два числа.
|
||||||
`str.split(':', expand=True)` раскладывает столбец в два, `to_numeric(..., errors='coerce')`
|
`str.split(':', expand=True)` раскладывает столбец в два, `to_numeric(..., errors='coerce')`
|
||||||
превращает текст в числа, а пустой период (`:`) через `fillna(0)` считается как `0:0`.
|
превращает текст в числа, а пустой период (`:`) или знаки техрезультата через
|
||||||
|
`fillna(0)` считаются как `0`.
|
||||||
|
|
||||||
**Шаг 2. `build_standings(df, conf_map)`** — превращает список матчей в таблицу команд:
|
**Шаг 2. `build_standings(df, conf_map)`** — превращает список матчей в таблицу команд:
|
||||||
|
|
||||||
|
|
@ -69,14 +68,17 @@ python3 individual_5.py khl_3/khl_2018_19.csv 2 "Ак Барс" [output.png]
|
||||||
2. Определяет **тип исхода** каждого матча сравнением столбцов:
|
2. Определяет **тип исхода** каждого матча сравнением столбцов:
|
||||||
победа в основное время / в овертайме / по буллитам — и аналогично поражения.
|
победа в основное время / в овертайме / по буллитам — и аналогично поражения.
|
||||||
Победителю по буллитам добавляется ровно 1 решающая шайба.
|
Победителю по буллитам добавляется ровно 1 решающая шайба.
|
||||||
3. **«Разворачивает» матч в две строки** — взгляд хозяев и взгляд гостей —
|
3. Отдельно обрабатывает **технические результаты** (`+ — -` / `- — +`):
|
||||||
|
засчитывает их как победу/поражение в основное время, но шайбы не начисляет (0:0).
|
||||||
|
4. **«Разворачивает» матч в две строки** — взгляд хозяев и взгляд гостей —
|
||||||
и склеивает их (`pd.concat`). Теперь у каждой команды по строке на матч.
|
и склеивает их (`pd.concat`). Теперь у каждой команды по строке на матч.
|
||||||
4. `groupby('Команда').sum()` суммирует все показатели по командам.
|
5. `groupby('Команда').sum()` суммирует все показатели по командам.
|
||||||
5. Считает производные показатели:
|
6. Считает производные показатели:
|
||||||
* **И** — игры, **О** — очки (победа = 2, поражение в ОТ/буллитах = 1),
|
* **И** — игры, **О** — очки (победа = 2, поражение в ОТ/буллитах = 1),
|
||||||
* **Р** — разница шайб, **%О** — процент набранных очков.
|
* **Р** — разница шайб, **%О** — процент набранных очков.
|
||||||
* `%О` считается с **отбрасыванием** знаков (`np.floor`), как на сайте КХЛ, а не округлением.
|
* `%О` считается **в целых числах** (`* 10000 // (2*И) / 100`) с отбрасыванием
|
||||||
6. Сортирует по регламенту КХЛ: `%О → В → ВО → ВБ → Р → ГЗ`.
|
знаков, как на сайте КХЛ, — без округления и без ошибок float (57.00 не съезжает в 56.99).
|
||||||
|
7. Сортирует по регламенту КХЛ: `%О → В → ВО → ВБ → Р → ГЗ`.
|
||||||
|
|
||||||
**Шаг 3. `show_table` и `main`** — печатают полную таблицу, затем отдельно
|
**Шаг 3. `show_table` и `main`** — печатают полную таблицу, затем отдельно
|
||||||
конференции Запад и Восток (фильтр по столбцу `Конф`).
|
конференции Запад и Восток (фильтр по столбцу `Конф`).
|
||||||
|
|
@ -90,23 +92,40 @@ python3 individual_5.py khl_3/khl_2018_19.csv 2 "Ак Барс" [output.png]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Индивидуальные задания
|
## 4. Дополнительные файлы
|
||||||
|
|
||||||
* **`individual_1.py`** — переиспользует `parse_score` из `main.py`, считает очки
|
### `divisions.py` — дивизионы и посев
|
||||||
каждой команды и строит **столбчатую диаграмму очков**. Поддерживает сохранение в PNG.
|
|
||||||
* **`individual_4.py`** — суммирует все заброшенные шайбы каждой команды за сезон
|
В части сезонов КХЛ (2018/19–2020/21) действовало правило: **победители двух
|
||||||
и строит гистограмму с подписями значений над столбцами.
|
дивизионов получают места 1 и 2 в конференции**, даже если по очкам они не в топ-2.
|
||||||
* **`individual_5.py`** — фильтрует матчи одной команды, считает **разницу шайб
|
Например, в 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. Материал к защите
|
## 5. Материал к защите
|
||||||
|
|
||||||
**О чём проект.** По «сырым» результатам матчей КХЛ автоматически строится
|
**О чём проект.** По «сырым» результатам матчей КХЛ автоматически строится
|
||||||
официальная турнирная таблица и набор графиков. Цель — освоить обработку
|
официальная турнирная таблица, таблицы с дивизионами и график. Цель — освоить
|
||||||
табличных данных в pandas и визуализацию в matplotlib.
|
обработку табличных данных в pandas и визуализацию в matplotlib.
|
||||||
|
|
||||||
**Ключевые идеи, которые стоит назвать на защите:**
|
**Ключевые идеи, которые стоит назвать на защите:**
|
||||||
|
|
||||||
|
|
@ -118,17 +137,21 @@ python3 individual_5.py khl_3/khl_2018_19.csv 2 "Ак Барс" [output.png]
|
||||||
3. **Булевы маски.** Тип исхода (победа/поражение и как именно) — это столбцы `True/False`,
|
3. **Булевы маски.** Тип исхода (победа/поражение и как именно) — это столбцы `True/False`,
|
||||||
которые переводятся в `1/0` через `.astype(int)` и суммируются.
|
которые переводятся в `1/0` через `.astype(int)` и суммируются.
|
||||||
4. **Точное соответствие регламенту КХЛ.** Система очков (2/1/0), добавление шайбы
|
4. **Точное соответствие регламенту КХЛ.** Система очков (2/1/0), добавление шайбы
|
||||||
за буллиты, отбрасывание (а не округление) процента очков, порядок критериев сортировки.
|
за буллиты, технические результаты, целочисленный (без округления) процент очков,
|
||||||
5. **Переиспользование кода.** `individual_1.py` импортирует функции из `main.py`,
|
порядок критериев сортировки, посев победителей дивизионов.
|
||||||
чтобы не дублировать логику разбора счёта.
|
5. **Переиспользование кода.** `divisions.py` и `3.py` опираются на логику из `main.py`,
|
||||||
|
чтобы не дублировать разбор счёта и расчёт таблицы.
|
||||||
|
|
||||||
**Возможные вопросы и ответы:**
|
**Возможные вопросы и ответы:**
|
||||||
|
|
||||||
* *Почему пустой период = 0:0?* — Овертайм/буллиты есть не во всех матчах; `fillna(0)`
|
* *Почему пустой период = 0:0?* — Овертайм/буллиты есть не во всех матчах; `fillna(0)`
|
||||||
делает расчёт единообразным.
|
делает расчёт единообразным.
|
||||||
* *Почему `np.floor`, а не `round`?* — Сайт КХЛ отбрасывает знаки после второго
|
* *Почему %О считается в целых числах, а не через `round`?* — Сайт КХЛ отбрасывает
|
||||||
(72.448 → 72.44), а не округляет.
|
знаки после второго (а не округляет), и целочисленная арифметика не даёт ошибок
|
||||||
* *Зачем папка `khl_3_win`?* — то же содержимое, но с BOM, чтобы Excel правильно
|
float (57.00 вместо 56.99...).
|
||||||
показывал кириллицу.
|
* *Как обрабатываются технические результаты?* — `+ — -` / `- — +` засчитываются как
|
||||||
* *Как добавить новый сезон?* — добавить CSV в `khl_3/` и описать конференции
|
победа/поражение в основное время, но без шайб (0:0), как в КХЛ.
|
||||||
в словаре `CONFERENCES` внутри `main.py`.
|
* *Почему дивизионы в отдельном файле?* — чтобы не усложнять `main.py`; `divisions.py`
|
||||||
|
переиспользует его функции, ничего в нём не меняя.
|
||||||
|
* *Как добавить новый сезон?* — добавить CSV в `khl_3/`, описать конференции в
|
||||||
|
`CONFERENCES` (в `main.py`) и при необходимости дивизионы в `DIVISIONS` (в `divisions.py`).
|
||||||
|
|
|
||||||
365
РАЗБОР_КОДА.md
365
РАЗБОР_КОДА.md
|
|
@ -1,7 +1,8 @@
|
||||||
# Разбор кода построчно (для защиты)
|
# Разбор кода построчно (для защиты)
|
||||||
|
|
||||||
Здесь разобрана **каждая строка** всех файлов и **каждая функция из библиотек**
|
Здесь разобраны три файла проекта — **`main.py`**, **`divisions.py`**, **`3.py`** —
|
||||||
(pandas, numpy, matplotlib, sys, os). Читается сверху вниз — можно отвечать прямо по тексту.
|
и **каждая функция из библиотек** (pandas, numpy, matplotlib, sys, os).
|
||||||
|
Читается сверху вниз — можно отвечать прямо по тексту.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -11,45 +12,45 @@
|
||||||
|
|
||||||
### `sys` и `os` (стандартная библиотека Python)
|
### `sys` и `os` (стандартная библиотека Python)
|
||||||
* `sys.argv` — список аргументов командной строки. `sys.argv[0]` — имя скрипта, `sys.argv[1]` — первый аргумент и т.д.
|
* `sys.argv` — список аргументов командной строки. `sys.argv[0]` — имя скрипта, `sys.argv[1]` — первый аргумент и т.д.
|
||||||
* `sys.exit(1)` — завершить программу с кодом ошибки 1 (ненулевой код = «завершилось с ошибкой»).
|
|
||||||
* `os.path.basename(path)` — берёт из пути `khl_3/khl_2018_19.csv` только имя файла `khl_2018_19.csv`.
|
* `os.path.basename(path)` — берёт из пути `khl_3/khl_2018_19.csv` только имя файла `khl_2018_19.csv`.
|
||||||
* `os.path.isfile(path)` — возвращает `True`, если файл существует.
|
* `os.path.abspath(path)` / `os.path.dirname(path)` — полный путь к файлу / папка, в которой он лежит. Нужны в `divisions.py`, чтобы найти `main.py` рядом с собой.
|
||||||
|
|
||||||
### `pandas` (сокращённо `pd`) — работа с таблицами
|
### `pandas` (сокращённо `pd`) — работа с таблицами
|
||||||
* `pd.read_csv(path)` — читает CSV-файл в **DataFrame** (таблицу).
|
* `pd.read_csv(path)` — читает CSV-файл в **DataFrame** (таблицу).
|
||||||
* **DataFrame** — двумерная таблица (строки × столбцы). `df['Столбец']` — это один столбец (объект **Series**).
|
* **DataFrame** — двумерная таблица (строки × столбцы). `df['Столбец']` — это один столбец (объект **Series**).
|
||||||
* **Series** — один столбец таблицы: массив значений с индексом (номерами/подписями строк).
|
* **Series** — один столбец таблицы: массив значений с индексом (номерами/подписями строк).
|
||||||
* `df[col].str.split(':', expand=True)` — у строкового столбца разбивает каждую ячейку по `:`; `expand=True` раскладывает результат в несколько столбцов.
|
* `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` (пусто), если значение не число.
|
* `pd.to_numeric(series, errors='coerce')` — превращает текст в числа; `errors='coerce'` ставит `NaN` (пусто), если значение не число.
|
||||||
* `series.fillna(0)` — заменяет пустые значения `NaN` на 0.
|
* `series.fillna(0)` — заменяет пустые значения `NaN` на 0.
|
||||||
|
* `series.where(условие, 0)` — оставляет значение, где условие `True`, иначе ставит 0.
|
||||||
* `pd.DataFrame({...})` — создаёт новую таблицу из словаря `{'имя столбца': данные}`.
|
* `pd.DataFrame({...})` — создаёт новую таблицу из словаря `{'имя столбца': данные}`.
|
||||||
* `pd.concat([df1, df2], ignore_index=True)` — склеивает таблицы одну под другой; `ignore_index=True` перенумеровывает строки заново.
|
* `pd.concat([df1, df2], ignore_index=True)` — склеивает таблицы одну под другой; `ignore_index=True` перенумеровывает строки заново.
|
||||||
* `series.astype(int)` — переводит тип в целые числа (в т.ч. `True/False → 1/0`).
|
* `series.astype(int)` — переводит тип в целые числа (в т.ч. `True/False → 1/0`).
|
||||||
* `df.groupby('Команда').sum()` — группирует строки по командам и складывает числа внутри каждой группы.
|
* `df.groupby('Команда').sum()` — группирует строки по командам и складывает числа внутри каждой группы.
|
||||||
|
* `df.groupby('Див', sort=False).head(1)` — берёт первую строку каждой группы (`sort=False` — не пересортировывать группы).
|
||||||
* `df[cols].sum(axis=1)` — сумма по строке (вдоль столбцов). `axis=0` — по столбцу.
|
* `df[cols].sum(axis=1)` — сумма по строке (вдоль столбцов). `axis=0` — по столбцу.
|
||||||
* `df.sort_values(by=[...], ascending=False)` — сортирует строки по указанным столбцам (по убыванию).
|
* `df.sort_values(by=[...], ascending=False)` — сортирует строки по указанным столбцам (по убыванию).
|
||||||
* `series.map(словарь)` — заменяет каждое значение по словарю (ключ → значение).
|
* `series.map(словарь)` — заменяет каждое значение по словарю (ключ → значение).
|
||||||
* `df.copy()` — делает независимую копию таблицы (чтобы не менять оригинал).
|
* `df.copy()` — делает независимую копию таблицы (чтобы не менять оригинал).
|
||||||
|
* `df.drop(индексы)` — выбрасывает строки с указанными индексами.
|
||||||
* `df.insert(позиция, 'имя', данные)` — вставляет новый столбец на заданную позицию.
|
* `df.insert(позиция, 'имя', данные)` — вставляет новый столбец на заданную позицию.
|
||||||
* `df.to_string(index=False)` — превращает таблицу в текст для печати; `index=False` — без столбца индекса.
|
* `df.to_string(index=False)` — превращает таблицу в текст для печати; `index=False` — без столбца индекса.
|
||||||
* `df.index` — подписи строк (здесь — названия команд после `groupby`).
|
* `df.index` — подписи строк (здесь — названия команд после `groupby`).
|
||||||
|
* `pd.to_datetime(series)` — превращает текст с датой в настоящую дату (чтобы ось X шла по времени).
|
||||||
|
|
||||||
### `numpy` (сокращённо `np`) — числовые операции
|
### `numpy` (сокращённо `np`) — числовые операции
|
||||||
* `np.floor(x)` — округление **вниз** (отбрасывание дробной части): `72.99 → 72`.
|
* `np.arange(1, N+1)` — массив чисел `[1, 2, ..., N]` (для нумерации мест).
|
||||||
* `np.arange(1, N+1)` — массив чисел `[1, 2, ..., N]`.
|
|
||||||
* `np.sign(x)` — знак числа: `-1`, `0` или `1`.
|
|
||||||
|
|
||||||
### `matplotlib.pyplot` (сокращённо `plt`) — графики
|
### `matplotlib.pyplot` (сокращённо `plt`) — графики
|
||||||
* `plt.figure(figsize=(ш, в))` — создаёт новый холст графика заданного размера в дюймах.
|
* `plt.subplots(figsize=(ш, в))` — создаёт холст (`fig`) и оси (`ax`) заданного размера в дюймах.
|
||||||
* `plt.bar(x, y, color=...)` — столбчатая диаграмма.
|
* `ax.plot(x, y, marker='o')` — линия с точками-маркерами.
|
||||||
* `plt.title / plt.xlabel / plt.ylabel` — заголовок и подписи осей.
|
* `ax.axhline(0, ...)` — горизонтальная линия на уровне 0.
|
||||||
* `plt.xticks(rotation=..., ha=...)` — настройка подписей оси X (поворот, выравнивание).
|
* `ax.set_title / set_xlabel / set_ylabel` — заголовок и подписи осей.
|
||||||
* `plt.yticks(range(...))` — какие деления показывать на оси Y.
|
* `ax.grid(True, alpha=...)` — координатная сетка (`alpha` — прозрачность).
|
||||||
* `plt.grid(axis='y', alpha=...)` — координатная сетка (`alpha` — прозрачность).
|
* `fig.autofmt_xdate()` — автоматически поворачивает подписи дат, чтобы не слипались.
|
||||||
* `plt.axhline(0, ...)` — горизонтальная линия на уровне 0.
|
* `fig.savefig(path, dpi=150, bbox_inches='tight')` — сохранить график в файл (`tight` — обрезать поля).
|
||||||
* `plt.text(x, y, текст, ...)` — подпись в точке графика.
|
|
||||||
* `plt.tight_layout()` — автоматически подбирает отступы, чтобы подписи не обрезались.
|
|
||||||
* `plt.savefig(path, dpi=200)` — сохранить график в файл.
|
|
||||||
* `plt.show()` — показать график в окне.
|
* `plt.show()` — показать график в окне.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -60,7 +61,7 @@
|
||||||
import sys # доступ к аргументам командной строки (sys.argv)
|
import sys # доступ к аргументам командной строки (sys.argv)
|
||||||
import os # работа с путями файлов (os.path.basename)
|
import os # работа с путями файлов (os.path.basename)
|
||||||
import pandas as pd # таблицы (DataFrame); pd — общепринятое сокращение
|
import pandas as pd # таблицы (DataFrame); pd — общепринятое сокращение
|
||||||
import numpy as np # числовые операции (np.floor, np.arange)
|
import numpy as np # числовые операции (np.arange для нумерации мест)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Функция `make_conf` — собирает словарь «команда → конференция»
|
### Функция `make_conf` — собирает словарь «команда → конференция»
|
||||||
|
|
@ -92,12 +93,12 @@ CONFERENCES = {
|
||||||
```python
|
```python
|
||||||
def parse_score(df, col):
|
def parse_score(df, col):
|
||||||
parts = df[col].str.split(':', expand=True) # "3:1" -> два столбца: ["3"], ["1"]
|
parts = df[col].str.split(':', expand=True) # "3:1" -> два столбца: ["3"], ["1"]
|
||||||
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)# левое число -> голы хозяев; пусто -> 0
|
home = pd.to_numeric(parts[0], errors='coerce').fillna(0)# левое число -> голы хозяев; пусто/знак -> 0
|
||||||
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)# правое число -> голы гостей; пусто -> 0
|
away = pd.to_numeric(parts[1], errors='coerce').fillna(0)# правое число -> голы гостей; пусто/знак -> 0
|
||||||
return home, away # возвращаем два столбца (Series)
|
return home, away # возвращаем два столбца (Series)
|
||||||
```
|
```
|
||||||
* `parts[0]` / `parts[1]` — первый и второй столбцы результата split.
|
* `parts[0]` / `parts[1]` — первый и второй столбцы результата split.
|
||||||
* Пустой период `:` после split даёт пустые строки → `to_numeric` делает `NaN` → `fillna(0)` ставит 0.
|
* Пустой период `:` или знаки техрезультата (`+ — -`) после split не превращаются в число → `to_numeric` делает `NaN` → `fillna(0)` ставит 0.
|
||||||
|
|
||||||
### Функция `build_standings` — главный расчёт таблицы
|
### Функция `build_standings` — главный расчёт таблицы
|
||||||
|
|
||||||
|
|
@ -127,19 +128,37 @@ def build_standings(df, conf_map):
|
||||||
```python
|
```python
|
||||||
win_home_ot = tie & (oth > ota) # была ничья И хозяева забили в ОТ -> победа в овертайме
|
win_home_ot = tie & (oth > ota) # была ничья И хозяева забили в ОТ -> победа в овертайме
|
||||||
win_away_ot = tie & (ota > oth) # ничья И гости забили в ОТ
|
win_away_ot = tie & (ota > oth) # ничья И гости забили в ОТ
|
||||||
```
|
|
||||||
`&` — поэлементное «И» для булевых столбцов (обе маски должны быть True в одной строке).
|
|
||||||
|
|
||||||
```python
|
|
||||||
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) # буллиты выиграли гости
|
||||||
```
|
```
|
||||||
|
`&` — поэлементное «И» для булевых столбцов (обе маски должны быть 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
|
```python
|
||||||
goals_home = reg_h + oth + win_home_so.astype(int) # все шайбы хозяев: основное + ОТ + 1 за выигр. буллиты
|
goals_home = reg_h + oth + win_home_so.astype(int) # все шайбы хозяев: основное + ОТ + 1 за выигр. буллиты
|
||||||
goals_away = reg_a + ota + win_away_so.astype(int) # все шайбы гостей
|
goals_away = reg_a + ota + win_away_so.astype(int) # все шайбы гостей
|
||||||
```
|
```
|
||||||
`win_home_so.astype(int)` — `True/False` превращаем в `1/0`, чтобы прибавить как число.
|
`win_home_so.astype(int)` — `True/False` превращаем в `1/0`, чтобы прибавить как число. У технического матча все слагаемые равны 0, поэтому шайб нет.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
home = pd.DataFrame({ # таблица «глазами хозяев»: одна строка = один матч
|
home = pd.DataFrame({ # таблица «глазами хозяев»: одна строка = один матч
|
||||||
|
|
@ -160,16 +179,10 @@ def build_standings(df, conf_map):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
both = pd.concat([home, away], ignore_index=True) # склеиваем обе таблицы в одну
|
both = pd.concat([home, away], ignore_index=True) # склеиваем обе таблицы в одну
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
flag_cols = ['В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П'] # столбцы-исходы
|
flag_cols = ['В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П'] # столбцы-исходы
|
||||||
both[flag_cols] = both[flag_cols].astype(int) # True/False -> 1/0 (чтобы суммировать)
|
both[flag_cols] = both[flag_cols].astype(int) # True/False -> 1/0 (чтобы суммировать)
|
||||||
both[['ГЗ', 'ГП']] = both[['ГЗ', 'ГП']].astype(int)# голы делаем целыми (после fillna были дробными)
|
both[['ГЗ', 'ГП']] = both[['ГЗ', 'ГП']].astype(int)# голы делаем целыми (после fillna были дробными)
|
||||||
```
|
table = both.groupby('Команда').sum() # группируем по команде и складываем все числа
|
||||||
|
|
||||||
```python
|
|
||||||
table = both.groupby('Команда').sum() # группируем по команде и складываем все числа
|
|
||||||
```
|
```
|
||||||
После этого индекс таблицы — названия команд, а в ячейках — суммы за сезон.
|
После этого индекс таблицы — названия команд, а в ячейках — суммы за сезон.
|
||||||
|
|
||||||
|
|
@ -180,9 +193,9 @@ def build_standings(df, conf_map):
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
table['%О'] = np.floor(table['О'] / (2 * table['И']) * 10000) / 100
|
table['%О'] = (table['О'] * 10000 // (2 * table['И'])) / 100
|
||||||
```
|
```
|
||||||
Процент очков = очки / максимум (2 за игру). `np.floor(...*10000)/100` **отбрасывает** знаки до сотых (как сайт КХЛ), а не округляет.
|
Процент очков = очки / максимум (2 за игру). Считается **в целых числах**: умножаем на 10000, делим **нацело** (`//`) на `2*И` — дробная часть отбрасывается (как на сайте КХЛ), потом делим на 100. Целочисленная арифметика не даёт ошибок float, поэтому ровные значения (57.00) не съезжают в 56.99.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)
|
table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)
|
||||||
|
|
@ -237,89 +250,115 @@ main() # запуск програ
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `individual_1.py` — гистограмма очков
|
## `divisions.py` — дивизионы и посев победителей дивизионов
|
||||||
|
|
||||||
|
Зачем нужен: в сезонах 2018/19–2020/21 победители двух дивизионов занимали места 1–2
|
||||||
|
в конференции, даже если по очкам были ниже. `main.py` этого не делает, поэтому правило
|
||||||
|
вынесено в **отдельный файл**, а `main.py` не трогается.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import os, sys
|
import sys
|
||||||
import matplotlib.pyplot as plt
|
import os
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import main as mn # импортируем main.py как модуль mn -> переиспользуем parse_score
|
import numpy as np
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Переиспользование логики из `main.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def calculate_team_points(filename):
|
_main_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'main.py')
|
||||||
df = pd.read_csv(filename)
|
_src = open(_main_path, encoding='utf-8').read() # читаем исходник main.py как текст
|
||||||
h1,a1 = mn.parse_score(df,'Период_1') # тот же разбор счёта, что в main.py
|
_src = _src.replace('\nmain()\n', '\n') # убираем последнюю строку с автозапуском
|
||||||
h2,a2 = mn.parse_score(df,'Период_2')
|
_ns = {}
|
||||||
h3,a3 = mn.parse_score(df,'Период_3')
|
exec(compile(_src, 'main.py', 'exec'), _ns) # выполняем код -> получаем его функции в _ns
|
||||||
oth,ota = mn.parse_score(df,'Овертайм')
|
build_standings = _ns['build_standings'] # забираем готовый расчёт таблицы
|
||||||
soh,soa = mn.parse_score(df,'Буллиты')
|
CONFERENCES = _ns['CONFERENCES'] # и словарь конференций
|
||||||
|
|
||||||
home_goals = h1+h2+h3 # голы хозяев в основное время
|
|
||||||
away_goals = a1+a2+a3 # голы гостей
|
|
||||||
tie = home_goals == away_goals # маска ничьей в основное время
|
|
||||||
|
|
||||||
home_win = (home_goals > away_goals) | (tie & ((oth>ota) | ((oth==ota)&(soh>soa)))) # победа хозяев любым способом
|
|
||||||
away_win = (away_goals > home_goals) | (tie & ((ota>oth) | ((oth==ota)&(soa>soh)))) # победа гостей
|
|
||||||
home_overtime_loss = tie & ((ota>oth) | ((oth==ota)&(soa>soh))) # хозяева проиграли в ОТ/буллитах -> 1 очко
|
|
||||||
away_overtime_loss = tie & ((oth>ota) | ((oth==ota)&(soh>soa))) # гости проиграли в ОТ/буллитах
|
|
||||||
|
|
||||||
home_points = 2*home_win.astype(int) + home_overtime_loss.astype(int) # очки хозяев за каждый матч
|
|
||||||
away_points = 2*away_win.astype(int) + away_overtime_loss.astype(int) # очки гостей
|
|
||||||
|
|
||||||
points = pd.concat([ # снова приём «две строки на матч»
|
|
||||||
pd.DataFrame({'Команда': df['Команда_1'], 'О': home_points}),
|
|
||||||
pd.DataFrame({'Команда': df['Команда_2'], 'О': away_points}),
|
|
||||||
])
|
|
||||||
return points.groupby('Команда')['О'].sum().sort_values(ascending=False) # сумма очков по командам, по убыванию
|
|
||||||
```
|
```
|
||||||
* `|` — поэлементное «ИЛИ» для масок.
|
* `main.py` в конце сам вызывает `main()`, поэтому обычный `import main` тут же попытался бы прочитать аргументы и упал. Поэтому читаем текст, убираем строку `main()` и выполняем.
|
||||||
* `['О']` после groupby — берём только столбец очков.
|
* `exec(compile(...), _ns)` — выполнить код, сложив все его имена (функции, словари) в словарь `_ns`. Так получаем `build_standings` и `CONFERENCES`, ничего не запуская и не печатая.
|
||||||
|
* **Главное:** логика расчёта не дублируется — единственный источник правды остаётся в `main.py`.
|
||||||
|
|
||||||
|
### Функция `make_div` — словарь «команда → дивизион»
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def show_points_histogram(filename, output=None):
|
def make_div(bobrov, tarasov, kharlamov, chernyshev):
|
||||||
points = calculate_team_points(filename)
|
d = {}
|
||||||
plt.figure(figsize=(12,7)) # холст 12x7 дюймов
|
d.update({t: 'Боброва' for t in bobrov}) # Запад
|
||||||
plt.bar(points.index, points.values, color='steelblue') # столбцы: X=названия, Y=очки
|
d.update({t: 'Тарасова' for t in tarasov}) # Запад
|
||||||
plt.title('Гистограмма набранных командами очков')
|
d.update({t: 'Харламова' for t in kharlamov}) # Восток
|
||||||
plt.xlabel('Команда'); plt.ylabel('Очки')
|
d.update({t: 'Чернышёва' for t in chernyshev}) # Восток
|
||||||
plt.xticks(rotation=75, ha='right') # подписи команд повернуть на 75°, прижать вправо
|
return d
|
||||||
plt.grid(axis='y', alpha=0.3) # горизонтальная сетка, полупрозрачная
|
|
||||||
plt.tight_layout() # подобрать отступы
|
|
||||||
if output: # если задан файл — сохранить, иначе показать
|
|
||||||
plt.savefig(output, dpi=200)
|
|
||||||
print(f'Гистограмма сохранена в файл: {output}')
|
|
||||||
else:
|
|
||||||
plt.show()
|
|
||||||
```
|
```
|
||||||
* `points.index` — названия команд, `points.values` — числа очков.
|
Аналог `make_conf`, только делений не два (Запад/Восток), а четыре дивизиона.
|
||||||
* `f'...{output}'` — f-строка, подставляет значение переменной в текст.
|
|
||||||
|
### Словари `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
|
```python
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) not in (2, 3): # проверка: 1 или 2 аргумента (+имя скрипта)
|
path = sys.argv[1]
|
||||||
print('Использование: python3 individual_1.py <csv-файл> [файл_для_сохранения]')
|
season = os.path.basename(path).replace('khl_', '').replace('.csv', '')
|
||||||
sys.exit(1)
|
df = pd.read_csv(path)
|
||||||
filename = sys.argv[1]
|
table = build_standings(df, CONFERENCES[season]) # таблица из логики main.py
|
||||||
output = sys.argv[2] if len(sys.argv)==3 else None # второй аргумент необязателен
|
table['Див'] = pd.Series(table.index, index=table.index).map(DIVISIONS[season]) # приписываем дивизион
|
||||||
if not os.path.isfile(filename): # проверка существования файла
|
use_seeding = SEED_BY_DIVISION[season]
|
||||||
raise FileNotFoundError(f'Файл не найден: {filename}')
|
for conf in ['Запад', 'Восток']:
|
||||||
show_points_histogram(filename, output)
|
part = table[table['Конф'] == conf]
|
||||||
|
if use_seeding:
|
||||||
|
part = seed_conference(part) # если в этом сезоне посев был — применяем
|
||||||
|
show_table(part, 'Конференция ' + conf + ' ' + season + ...)
|
||||||
|
|
||||||
if __name__ == '__main__': # запуск только если файл запущен напрямую,
|
main()
|
||||||
main() # а не импортирован как модуль
|
|
||||||
```
|
```
|
||||||
* `if __name__ == '__main__':` — стандартная защита: код запускается только при прямом вызове `python3 individual_1.py`, но не при `import`. (Важно, т.к. этот файл сам импортирует `main`.)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `individual_4.py` — гистограмма заброшенных шайб
|
## `3.py` — график разницы шайб команды по датам
|
||||||
|
|
||||||
`parse_score` — копия из main.py (тот же разбор счёта). Далее:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def task4(filename):
|
import sys
|
||||||
df = pd.read_csv(filename)
|
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')
|
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,'Буллиты')
|
oth,ota = parse_score(df,'Овертайм'); soh,soa = parse_score(df,'Буллиты')
|
||||||
|
|
||||||
|
|
@ -331,100 +370,58 @@ def task4(filename):
|
||||||
goals_home = reg_h + oth + win_home_so.astype(int) # все шайбы хозяев (+1 за буллиты)
|
goals_home = reg_h + oth + win_home_so.astype(int) # все шайбы хозяев (+1 за буллиты)
|
||||||
goals_away = reg_a + ota + win_away_so.astype(int) # все шайбы гостей
|
goals_away = reg_a + ota + win_away_so.astype(int) # все шайбы гостей
|
||||||
|
|
||||||
home = pd.DataFrame({'Команда': df['Команда_1'], 'ГЗ': goals_home}) # шайбы хозяев
|
p1 = df['Период_1'].astype(str).str.strip()
|
||||||
away = pd.DataFrame({'Команда': df['Команда_2'], 'ГЗ': goals_away}) # шайбы гостей
|
tech = p1.str.startswith('+') | p1.str.endswith('+')# технический результат
|
||||||
both = pd.concat([home, away], ignore_index=True) # склеиваем
|
goals_home = goals_home.where(~tech, 0) # у техрезультата шайб нет (0:0)
|
||||||
|
goals_away = goals_away.where(~tech, 0)
|
||||||
totals = both.groupby('Команда')['ГЗ'].sum().astype(int) # сумма шайб по командам
|
return goals_home, goals_away
|
||||||
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])
|
|
||||||
```
|
```
|
||||||
* `enumerate(totals.values)` — даёт пары `(номер, значение)`; номер `i` — позиция столбца по X.
|
* Та же логика подсчёта шайб, что в `main.py`.
|
||||||
* `plt.text(i, value, str(value), ha='center', va='bottom')` — подпись по центру столбца, над ним (`va='bottom'` — низ текста на уровне значения).
|
* `series.where(~tech, 0)` — оставить значение, где матч НЕ технический, иначе 0. Так разница у такого матча будет 0, как на сайте КХЛ.
|
||||||
|
|
||||||
---
|
### Функция `plot_team_diff` — построение и сохранение графика
|
||||||
|
|
||||||
## `individual_5.py` — разница шайб команды в периоде по датам
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import os, sys
|
def plot_team_diff(team, filename):
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
def parse_score(df, col): # тот же разбор счёта (с docstring-описанием в тройных кавычках)
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
def calculate_period_goals_diff(filename, period_number, team_name):
|
|
||||||
df = pd.read_csv(filename)
|
df = pd.read_csv(filename)
|
||||||
team_df = df[(df['Команда_1']==team_name) | (df['Команда_2']==team_name)].copy() # только матчи нужной команды
|
goals_home, goals_away = match_goals(df) # голы хозяев/гостей в каждом матче
|
||||||
team_df = team_df.sort_values(by='Дата') # по возрастанию дат
|
|
||||||
period_col = f'Период_{period_number}' # имя столбца, напр. 'Период_2'
|
|
||||||
h, a = parse_score(team_df, period_col) # счёт в этом периоде
|
|
||||||
|
|
||||||
is_home = team_df['Команда_1'] == team_name # маска: была ли команда хозяином
|
home = pd.DataFrame({'Дата': df['Дата'], 'Команда': df['Команда_1'], 'ГЗ': goals_home, 'ГП': goals_away})
|
||||||
deltas = (h - a) * (2 * is_home.astype(int) - 1) # разница «своих минус чужих» с учётом стороны
|
away = pd.DataFrame({'Дата': df['Дата'], 'Команда': df['Команда_2'], 'ГЗ': goals_away, 'ГП': goals_home})
|
||||||
result = pd.DataFrame({'Дата': team_df['Дата'], 'Разница': deltas.astype(int)})
|
both = pd.concat([home, away], ignore_index=True) # тот же приём «две строки на матч»
|
||||||
return result
|
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()
|
||||||
```
|
```
|
||||||
**Трюк со знаком:** `2*is_home - 1` даёт `+1`, если команда хозяин, и `-1`, если гость.
|
* `'ГП'` = пропущено = забито соперником, поэтому в `away` поля `ГЗ`/`ГП` меняются местами.
|
||||||
Тогда `(h - a)` (разница хозяев) автоматически переворачивается для гостевых матчей — получается разница именно нашей команды.
|
* `pd.to_datetime` нужен, чтобы matplotlib расставил матчи по оси времени правильно.
|
||||||
|
|
||||||
|
### Функция `main`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def show_period_goals_diff_histogram(filename, period_number, team_name, output_path=None):
|
def main():
|
||||||
data = calculate_period_goals_diff(filename, period_number, team_name)
|
filename = sys.argv[1] # путь к CSV
|
||||||
dates = data['Дата']; deltas = data['Разница']
|
team = sys.argv[2] # название команды
|
||||||
|
plot_team_diff(team, filename)
|
||||||
|
|
||||||
plt.figure(figsize=(14,7))
|
main()
|
||||||
color_map = {-1:'#e74c3c', 0:'#95a5a6', 1:'#2ecc71'} # минус=красный, ноль=серый, плюс=зелёный
|
|
||||||
signs = np.sign(deltas) # знак каждой разницы: -1/0/1
|
|
||||||
colors = [color_map[s] for s in signs] # список цветов под каждый столбец
|
|
||||||
|
|
||||||
plt.bar(dates, deltas, color=colors, edgecolor='black', alpha=0.85)
|
|
||||||
plt.title(f"Разница шайб в {period_number}-м периоде у команды '{team_name}' по датам", fontsize=14, fontweight='bold')
|
|
||||||
plt.xlabel('Дата матча', fontsize=12); plt.ylabel('Разница забитых и пропущенных шайб', fontsize=12)
|
|
||||||
|
|
||||||
min_val = int(deltas.min()); max_val = int(deltas.max()) # границы значений
|
|
||||||
plt.yticks(range(min_val-1, max_val+2)) # деления оси Y — только целые числа
|
|
||||||
plt.xticks(rotation=90, fontsize=8) # даты вертикально, мелким шрифтом
|
|
||||||
plt.axhline(0, color='black', linewidth=1.2, linestyle='--') # пунктирная линия нуля
|
|
||||||
plt.grid(axis='y', linestyle=':', alpha=0.6) # точечная сетка по Y
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
actions = { # выбор действия без if: словарь функций
|
|
||||||
True: lambda: plt.savefig(output_path, dpi=200),
|
|
||||||
False: lambda: plt.show()
|
|
||||||
}
|
|
||||||
actions[output_path is not None]() # есть путь -> сохранить, иначе показать
|
|
||||||
```
|
```
|
||||||
* `np.sign(deltas)` — массив знаков; по нему выбираем цвет каждого столбца.
|
Запуск: `python3 3.py khl_3/khl_2018_19.csv "ЦСКА"`.
|
||||||
* `[color_map[s] for s in signs]` — **списковое включение**: строит список цветов.
|
|
||||||
* `lambda: ...` — короткая безымянная функция; `actions[...]()` вызывает выбранную.
|
|
||||||
|
|
||||||
```python
|
|
||||||
args = sys.argv + [None] * (5 - len(sys.argv)) # дополняем аргументы None-ами до 5 штук, чтобы не было IndexError
|
|
||||||
show_period_goals_diff_histogram(args[1], int(args[2]), args[3], args[4]) # запуск
|
|
||||||
```
|
|
||||||
* `[None] * n` — список из `n` элементов `None`.
|
|
||||||
* `int(args[2])` — номер периода из строки в число.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -434,5 +431,7 @@ show_period_goals_diff_histogram(args[1], int(args[2]), args[3], args[4]) # з
|
||||||
* **Почему без циклов?** Векторные операции pandas быстрее и короче: одно действие сразу над всем столбцом.
|
* **Почему без циклов?** Векторные операции pandas быстрее и короче: одно действие сразу над всем столбцом.
|
||||||
* **Что такое булева маска?** Столбец True/False из сравнения; по нему фильтруют строки и считают суммы через `.astype(int)`.
|
* **Что такое булева маска?** Столбец True/False из сравнения; по нему фильтруют строки и считают суммы через `.astype(int)`.
|
||||||
* **Зачем `pd.concat` хозяев и гостей?** Чтобы у каждой команды была своя строка на каждый матч и сработал `groupby`.
|
* **Зачем `pd.concat` хозяев и гостей?** Чтобы у каждой команды была своя строка на каждый матч и сработал `groupby`.
|
||||||
* **Почему `np.floor` для %очков?** КХЛ отбрасывает знаки, а не округляет.
|
* **Как считается %очков?** В целых числах с отбрасыванием знаков (`*10000 // (2*И) /100`) — как сайт КХЛ, без округления и без ошибок float.
|
||||||
* **Что делает `if __name__ == '__main__'`?** Запускает код только при прямом вызове файла, не при импорте.
|
* **Как обрабатываются технические результаты?** `+ — -` / `- — +` — это победа/поражение в основное время, но без шайб (0:0).
|
||||||
|
* **Почему дивизионы в отдельном файле?** Чтобы не менять `main.py`; `divisions.py` переиспользует его функции через чтение и `exec` исходника без автозапуска.
|
||||||
|
* **Зачем посев победителей дивизионов?** В 2018/19–2020/21 они получали места 1–2 в конференции независимо от очков; в 2021/22 правило отменили.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue