summer_practice/РАЗБОР_КОДА.md
2026-06-30 09:54:59 +03:00

30 KiB
Raw Permalink Blame History

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

Здесь разобраны три файла проекта — 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 — построчно

import sys          # доступ к аргументам командной строки (sys.argv)
import os           # работа с путями файлов (os.path.basename)
import pandas as pd # таблицы (DataFrame); pd — общепринятое сокращение
import numpy as np  # числовые операции (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)  # ничья И гости забили в ОТ
    win_home_so = tie & (oth == ota) & (soh > soa)  # ничья + ОТ ничейный -> буллиты выиграли хозяева
    win_away_so = tie & (oth == ota) & (soa > soh)  # буллиты выиграли гости

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

Технические результаты (неявка/отмена матча):

    p1 = df[ериод_1'].astype(str).str.strip()
    tech_home = p1.str.startswith('+')  # "+ — -": техническая победа хозяев
    tech_away = p1.str.endswith('+')    # "- — +": техническая победа гостей
    tech = tech_home | tech_away

В файле такой матч записан не цифрами, а знаками, поэтому обычная классификация выше сочла его «ничьёй 0:0». Перекрываем её:

    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.
    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, поэтому шайб нет.

    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['%О'] = (table['О'] * 10000 // (2 * table['И'])) / 100

Процент очков = очки / максимум (2 за игру). Считается в целых числах: умножаем на 10000, делим нацело (//) на 2*И — дробная часть отбрасывается (как на сайте КХЛ), потом делим на 100. Целочисленная арифметика не даёт ошибок float, поэтому ровные значения (57.00) не съезжают в 56.99.

    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.

divisions.py — дивизионы и посев победителей дивизионов

Зачем нужен: в сезонах 2018/192020/21 победители двух дивизионов занимали места 12 в конференции, даже если по очкам были ниже. main.py этого не делает, поэтому правило вынесено в отдельный файл, а main.py не трогается.

import sys
import os
import pandas as pd
import numpy as np

Переиспользование логики из main.py

_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 — словарь «команда → дивизион»

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

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 — посев победителей дивизионов

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 ставит победителей на места 12, затем — остальные в прежнем порядке.

Функция show_table — печать с дивизионом

То же, что в main.py, но в cols добавлен столбец 'Див', чтобы было видно, почему победители дивизионов оказались наверху.

Функция main

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

import sys
import pandas as pd
import numpy as np                # импортирован, но напрямую не используется
import matplotlib.pyplot as plt   # построение графика

Функция parse_score

Точная копия из main.py (тот же разбор счёта «3:1» на два числовых столбца) — правило подсчёта шайб одно и то же.

Функция match_goals — голы хозяев и гостей в каждом матче

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 — построение и сохранение графика

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

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/192020/21 они получали места 12 в конференции независимо от очков; в 2021/22 правило отменили.