437 lines
30 KiB
Markdown
437 lines
30 KiB
Markdown
# Разбор кода построчно (для защиты)
|
||
|
||
Здесь разобраны три файла проекта — **`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 правило отменили.
|