Task1_of_summer_practice/РАЗБОР_КОДА.md
2026-06-29 11:57:15 +03:00

438 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Разбор кода построчно (для защиты)
Здесь разобрана **каждая строка** всех файлов и **каждая функция из библиотек**
(pandas, numpy, matplotlib, sys, os). Читается сверху вниз — можно отвечать прямо по тексту.
---
## Справочник функций из библиотек
Чтобы не повторять одно и то же, вот что делает каждая чужая функция/метод в проекте:
### `sys` и `os` (стандартная библиотека Python)
* `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.isfile(path)` — возвращает `True`, если файл существует.
### `pandas` (сокращённо `pd`) — работа с таблицами
* `pd.read_csv(path)` — читает CSV-файл в **DataFrame** (таблицу).
* **DataFrame** — двумерная таблица (строки × столбцы). `df['Столбец']` — это один столбец (объект **Series**).
* **Series** — один столбец таблицы: массив значений с индексом (номерами/подписями строк).
* `df[col].str.split(':', expand=True)`у строкового столбца разбивает каждую ячейку по `:`; `expand=True` раскладывает результат в несколько столбцов.
* `pd.to_numeric(series, errors='coerce')` — превращает текст в числа; `errors='coerce'` ставит `NaN` (пусто), если значение не число.
* `series.fillna(0)` — заменяет пустые значения `NaN` на 0.
* `pd.DataFrame({...})` — создаёт новую таблицу из словаря `{'имя столбца': данные}`.
* `pd.concat([df1, df2], ignore_index=True)` — склеивает таблицы одну под другой; `ignore_index=True` перенумеровывает строки заново.
* `series.astype(int)` — переводит тип в целые числа (в т.ч. `True/False → 1/0`).
* `df.groupby('Команда').sum()` — группирует строки по командам и складывает числа внутри каждой группы.
* `df[cols].sum(axis=1)` — сумма по строке (вдоль столбцов). `axis=0` — по столбцу.
* `df.sort_values(by=[...], ascending=False)` — сортирует строки по указанным столбцам (по убыванию).
* `series.map(словарь)` — заменяет каждое значение по словарю (ключ → значение).
* `df.copy()` — делает независимую копию таблицы (чтобы не менять оригинал).
* `df.insert(позиция, 'имя', данные)` — вставляет новый столбец на заданную позицию.
* `df.to_string(index=False)` — превращает таблицу в текст для печати; `index=False` — без столбца индекса.
* `df.index` — подписи строк (здесь — названия команд после `groupby`).
### `numpy` (сокращённо `np`) — числовые операции
* `np.floor(x)` — округление **вниз** (отбрасывание дробной части): `72.99 → 72`.
* `np.arange(1, N+1)` — массив чисел `[1, 2, ..., N]`.
* `np.sign(x)` — знак числа: `-1`, `0` или `1`.
### `matplotlib.pyplot` (сокращённо `plt`) — графики
* `plt.figure(figsize=(ш, в))` — создаёт новый холст графика заданного размера в дюймах.
* `plt.bar(x, y, color=...)` — столбчатая диаграмма.
* `plt.title / plt.xlabel / plt.ylabel` — заголовок и подписи осей.
* `plt.xticks(rotation=..., ha=...)` — настройка подписей оси X (поворот, выравнивание).
* `plt.yticks(range(...))` — какие деления показывать на оси Y.
* `plt.grid(axis='y', alpha=...)` — координатная сетка (`alpha` — прозрачность).
* `plt.axhline(0, ...)` — горизонтальная линия на уровне 0.
* `plt.text(x, y, текст, ...)` — подпись в точке графика.
* `plt.tight_layout()` — автоматически подбирает отступы, чтобы подписи не обрезались.
* `plt.savefig(path, dpi=200)` — сохранить график в файл.
* `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.floor, 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) # ничья И гости забили в ОТ
```
`&` — поэлементное «И» для булевых столбцов (обе маски должны быть True в одной строке).
```python
win_home_so = tie & (oth == ota) & (soh > soa) # ничья + ОТ ничейный -> буллиты выиграли хозяева
win_away_so = tie & (oth == ota) & (soa > soh) # буллиты выиграли гости
```
```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`, чтобы прибавить как число.
```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) # склеиваем обе таблицы в одну
```
```python
flag_cols = ['В', 'ВО', 'ВБ', 'ПБ', 'ПО', 'П'] # столбцы-исходы
both[flag_cols] = both[flag_cols].astype(int) # True/False -> 1/0 (чтобы суммировать)
both[['ГЗ', 'ГП']] = both[['ГЗ', 'ГП']].astype(int)# голы делаем целыми (после fillna были дробными)
```
```python
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['%О'] = np.floor(table['О'] / (2 * table['И']) * 10000) / 100
```
Процент очков = очки / максимум (2 за игру). `np.floor(...*10000)/100` **отбрасывает** знаки до сотых (как сайт КХЛ), а не округляет.
```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.
---
## `individual_1.py` — гистограмма очков
```python
import os, sys
import matplotlib.pyplot as plt
import pandas as pd
import main as mn # импортируем main.py как модуль mn -> переиспользуем parse_score
```
```python
def calculate_team_points(filename):
df = pd.read_csv(filename)
h1,a1 = mn.parse_score(df,ериод_1') # тот же разбор счёта, что в main.py
h2,a2 = mn.parse_score(df,ериод_2')
h3,a3 = mn.parse_score(df,ериод_3')
oth,ota = mn.parse_score(df,'Овертайм')
soh,soa = mn.parse_score(df,'Буллиты')
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) # сумма очков по командам, по убыванию
```
* `|` — поэлементное «ИЛИ» для масок.
* `['О']` после groupby — берём только столбец очков.
```python
def show_points_histogram(filename, output=None):
points = calculate_team_points(filename)
plt.figure(figsize=(12,7)) # холст 12x7 дюймов
plt.bar(points.index, points.values, color='steelblue') # столбцы: X=названия, Y=очки
plt.title('Гистограмма набранных командами очков')
plt.xlabel('Команда'); plt.ylabel('Очки')
plt.xticks(rotation=75, ha='right') # подписи команд повернуть на 75°, прижать вправо
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` — числа очков.
* `f'...{output}'` — f-строка, подставляет значение переменной в текст.
```python
def main():
if len(sys.argv) not in (2, 3): # проверка: 1 или 2 аргумента (+имя скрипта)
print('Использование: python3 individual_1.py <csv-файл> [файл_для_сохранения]')
sys.exit(1)
filename = sys.argv[1]
output = sys.argv[2] if len(sys.argv)==3 else None # второй аргумент необязателен
if not os.path.isfile(filename): # проверка существования файла
raise FileNotFoundError(f'Файл не найден: {filename}')
show_points_histogram(filename, output)
if __name__ == '__main__': # запуск только если файл запущен напрямую,
main() # а не импортирован как модуль
```
* `if __name__ == '__main__':` — стандартная защита: код запускается только при прямом вызове `python3 individual_1.py`, но не при `import`. (Важно, т.к. этот файл сам импортирует `main`.)
---
## `individual_4.py` — гистограмма заброшенных шайб
`parse_score` — копия из main.py (тот же разбор счёта). Далее:
```python
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) # все шайбы хозяев (+1 за буллиты)
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])
```
* `enumerate(totals.values)` — даёт пары `(номер, значение)`; номер `i` — позиция столбца по X.
* `plt.text(i, value, str(value), ha='center', va='bottom')` — подпись по центру столбца, над ним (`va='bottom'` — низ текста на уровне значения).
---
## `individual_5.py` — разница шайб команды в периоде по датам
```python
import os, sys
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)
team_df = df[(df['Команда_1']==team_name) | (df['Команда_2']==team_name)].copy() # только матчи нужной команды
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 # маска: была ли команда хозяином
deltas = (h - a) * (2 * is_home.astype(int) - 1) # разница «своих минус чужих» с учётом стороны
result = pd.DataFrame({'Дата': team_df['Дата'], 'Разница': deltas.astype(int)})
return result
```
**Трюк со знаком:** `2*is_home - 1` даёт `+1`, если команда хозяин, и `-1`, если гость.
Тогда `(h - a)` (разница хозяев) автоматически переворачивается для гостевых матчей — получается разница именно нашей команды.
```python
def show_period_goals_diff_histogram(filename, period_number, team_name, output_path=None):
data = calculate_period_goals_diff(filename, period_number, team_name)
dates = data['Дата']; deltas = data['Разница']
plt.figure(figsize=(14,7))
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)` — массив знаков; по нему выбираем цвет каждого столбца.
* `[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])` — номер периода из строки в число.
---
## Что точно спросят на защите — короткие ответы
* **Что такое DataFrame/Series?** Таблица и её отдельный столбец в pandas.
* **Почему без циклов?** Векторные операции pandas быстрее и короче: одно действие сразу над всем столбцом.
* **Что такое булева маска?** Столбец True/False из сравнения; по нему фильтруют строки и считают суммы через `.astype(int)`.
* **Зачем `pd.concat` хозяев и гостей?** Чтобы у каждой команды была своя строка на каждый матч и сработал `groupby`.
* **Почему `np.floor` для %очков?** КХЛ отбрасывает знаки, а не округляет.
* **Что делает `if __name__ == '__main__'`?** Запускает код только при прямом вызове файла, не при импорте.