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

29 KiB
Raw Blame History

Разбор кода построчно (для защиты)

Здесь разобрана каждая строка всех файлов и каждая функция из библиотек (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 — построчно

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 — собирает словарь «команда → конференция»

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

CONFERENCES = {
    '2018_19': make_conf(west=[...], east=[...]),  # для каждого сезона свой состав конференций
    '2019_20': make_conf(...),
    ...
}
  • Ключ — код сезона ('2018_19'), значение — словарь «команда → конференция».
  • Зачем: составы конференций по годам менялись, поэтому для каждого сезона свой список.

Функция parse_score — разбор счёта «3:1»

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 делает NaNfillna(0) ставит 0.

Функция build_standings — главный расчёт таблицы

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 = ...распаковка кортежа: функция вернула два значения, кладём их в две переменные.

    reg_h = h1 + h2 + h3   # голы хозяев в основное время (поэлементная сумма столбцов)
    reg_a = a1 + a2 + a3   # голы гостей в основное время

Сложение двух Series складывает их построчно (матч за матчем).

    tie = (reg_h == reg_a)        # столбец True/False: True там, где в основное время ничья
    win_home_reg = (reg_h > reg_a)# хозяева выиграли в основное время
    win_away_reg = (reg_a > reg_h)# гости выиграли в основное время

Сравнение столбцов (==, >) возвращает столбец булевых значений (маску).

    win_home_ot = tie & (oth > ota)  # была ничья И хозяева забили в ОТ -> победа в овертайме
    win_away_ot = tie & (ota > oth)  # ничья И гости забили в ОТ

& — поэлементное «И» для булевых столбцов (обе маски должны быть True в одной строке).

    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)  # все шайбы гостей

win_home_so.astype(int)True/False превращаем в 1/0, чтобы прибавить как число.

    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,
    })

Главный приём: один матч превращаем в две записи — статистику хозяев и статистику гостей.

    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()  # группируем по команде и складываем все числа

После этого индекс таблицы — названия команд, а в ячейках — суммы за сезон.

    table['И'] = table[flag_cols].sum(axis=1)  # игры = сумма всех 6 исходов (axis=1 = вдоль строки)
    table['О'] = 2*(table['В']+table['ВО']+table['ВБ']) + (table['ПБ']+table['ПО'])  # очки: победа=2, пораж. в ОТ/буллитах=1
    table['Р'] = table['ГЗ'] - table['ГП']     # разница шайб
    table['%О'] = np.floor(table['О'] / (2 * table['И']) * 10000) / 100

Процент очков = очки / максимум (2 за игру). np.floor(...*10000)/100 отбрасывает знаки до сотых (как сайт КХЛ), а не округляет.

    table['Конф'] = pd.Series(table.index, index=table.index).map(conf_map)

Берём названия команд (индекс), и .map(conf_map) подставляет каждой её конференцию из словаря.

    table = table.sort_values(by=['%О', 'В', 'ВО', 'ВБ', 'Р', 'ГЗ'], ascending=False)
    return table

Сортировка по регламенту: сначала по %очков, при равенстве — больше побед в осн. время, затем в ОТ, по буллитам, разница шайб, забитые. ascending=False = по убыванию.

Функция show_table — печать таблицы

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 — точка входа

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 — гистограмма очков

import os, sys
import matplotlib.pyplot as plt
import pandas as pd
import main as mn                # импортируем main.py как модуль mn -> переиспользуем parse_score
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 — берём только столбец очков.
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-строка, подставляет значение переменной в текст.
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 (тот же разбор счёта). Далее:

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 — разница шайб команды в периоде по датам

import os, sys
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
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
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) (разница хозяев) автоматически переворачивается для гостевых матчей — получается разница именно нашей команды.

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[...]() вызывает выбранную.
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__'? Запускает код только при прямом вызове файла, не при импорте.