29 KiB
Разбор кода построчно (для защиты)
Здесь разобрана каждая строка всех файлов и каждая функция из библиотек (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делаетNaN→fillna(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__'? Запускает код только при прямом вызове файла, не при импорте.