From d2cf5cd57cc2d426c65dc1097ebd3cfa5aa12543 Mon Sep 17 00:00:00 2001 From: q Date: Mon, 29 Jun 2026 11:57:15 +0300 Subject: [PATCH] swag update --- РАЗБОР_КОДА.md | 438 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 РАЗБОР_КОДА.md diff --git a/РАЗБОР_КОДА.md b/РАЗБОР_КОДА.md new file mode 100644 index 0000000..1d022f2 --- /dev/null +++ b/РАЗБОР_КОДА.md @@ -0,0 +1,438 @@ +# Разбор кода построчно (для защиты) + +Здесь разобрана **каждая строка** всех файлов и **каждая функция из библиотек** +(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 [файл_для_сохранения]') + 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__'`?** Запускает код только при прямом вызове файла, не при импорте.