import pandas as pd from collections import deque ...

Created on: December 14, 2025

Answered using GPT-5.2 Thinking by Chat01

Question

import pandas as pd
from collections import deque
from sympy import symbols, Eq, solve
from datetime import datetime, timedelta

Словарь со значениями пороговых коэффициентов для каждой лиги

league_thresholds = {
"Albania Superiore": (2.52, 2.73),
"Andorra Primera": (2.42, 2.75),
"Armenia Premier": (2.32, 3.20),
"Bosnia WWin Liga": (2.00, 3.20),
"Croatia HNL": (2.29, 3.25),
"Cyprus League": (2.25, 3.20),
"Cyprus Division 2": (2.46, 2.70),
"Czech Chance Liga": (2.25, 3.30),
"Denmark Superliga": (2.40, 3.00),
"Greece Super League": (2.36, 3.40),
"Greece Super League 2": (2.25, 3.30),
"Hungary OTP Bank Liga": (2.17, 3.40),
"Hungary Merkantil Bank Liga": (2.38, 3.30),
"Luxembourg BGL Ligue": (2.20, 3.00),
"Malta Premier League": (2.75, 2.60), # Обратите внимание, что здесь home > away
"Montenegro Prva Liga": (2.50, 3.20),
"North Macedonia 1 MFL": (2.13, 3.60),
"Slovakia Nike Liga": (2.30, 3.00),
"Slovenia Prva Liga": (2.47, 3.00),
"Algeria Ligue 2": (1.87, 5.00),
"Mexico Liga de Expansion MX": (2.00, 4.00),
"South Africa Premiership": (2.31, 3.45),
"Croatia Prva NL": (2.40, 2.80),
"India ISL": (2.17, 3.33),
"Indonesia Liga 1": (2.27, 3.45),
"Bulgaria Vtora liga": (2.27, 3.45),
"Zambia Super League": (2.47, 3.10),
"Romania Liga 3": (2.30, 2.94),
"Myanmar National League": (2.50, 2.50),
"Tanzania Ligi Kuu Bara": (2.20, 3.30),
"France Nacional": (2.32, 3.30),
"Nigeria NPFL": (1.50, 6.00),
"Turkey 1.Lig": (2.27, 3.20),
"Algeria Ligue 1": (2.05, 4.30),
"Italy Serie C - A": (2.38, 3.30),
"Italy Serie B": (2.46, 3.50),
"Algeria Ligue 2 - 21": (2.00, 3.20),
"England National League South": (2.38, 3.00),
"Brazil Paulista A4": (2.44, 3.00),
"Ethiopia": (3.12, 2.85),
"Spain LaLiga": (2.29, 3.61),
"Australia NPL": (2.22, 3.00),
"Bulgaria Vtora Liga": (2.08, 3.70),
"Italy Serie C - C": (2.40, 3.20),
"Spain Segunda": (2.11, 4.02),
"France Ligue 1": (2.30, 3.60),
"Brazil Serie A": (2.05, 4.00),
"Brazil Serie B": (2.05, 4.00),
"Brazil Serie C": (2.15, 3.70),
"Iceland Besta Delid": (2.12, 3.10),
"Australia NPL Northern": (2.47, 2.30),
"Norway Division 2": (2.08, 2.94),
"USA MLS": (2.27, 3.20),
"Argentina": (2.22, 3.80),
"Mexico": (2.08, 3.80),
"Bundesliga - 2": (2.16, 3.55),
"France - 2": (2.16, 3.55)

}

Функция для очистки имен команд от лишних пробелов

def clean_team_name(name):
if isinstance(name, str):
return ' '.join(name.strip().split())
return name

Чтение данных из Excel файла

excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx"
df = pd.read_excel(excel_file_path)

Преобразуем каждую строку в кортеж с учётом лиги (колонка A)

matches = []
for _, row in df.iterrows():
match = (
row[0], # Лига (колонка A)
row[1], # Дата (колонка B)
clean_team_name(row[2]), # Домашняя команда (колонка C)
clean_team_name(row[3]), # Гостевая команда (колонка D)
row[4], # Коэффициент 1 (колонка E)
row[5], # Коэффициент на ничью (колонка F)
row[6] # Коэффициент 2 (колонка G)
)
matches.append(match)

Функция для поиска всех цепочек между двумя командами

def find_chains(matches, start_team, end_team, min_depth=1, max_depth=4):
# Создание графа игр (по принципу «команда – список (сосед, матч)»)
graph = {}
for match in matches:
league, date, home_team, away_team, odds1, odds_draw, odds2 = match

text
if home_team not in graph: graph[home_team] = [] if away_team not in graph: graph[away_team] = [] # Включаем в граф связь home_team -> (away_team, match) и наоборот graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) # Поиск в ширину для нахождения всех цепочек from collections import deque chains = [] queue = deque([(start_team, [], set())]) while queue: current_team, path, visited = queue.popleft() if current_team == end_team and path: if min_depth <= len(path) <= max_depth: chains.append(path) continue if len(path) >= max_depth: continue for neighbor, match in graph.get(current_team, []): if match not in visited: new_visited = visited.copy() new_visited.add(match) queue.append((neighbor, path + [match], new_visited)) return chains

Запрос ввода команд от пользователя с очисткой ввода

start_team = input("Введите название стартовой команды: ").strip()
end_team = input("Введите название команды назначения: ").strip()

Поиск цепочек между двумя заданными командами

chains = find_chains(matches, start_team, end_team)

Удаляем дубликаты цепочек

unique_chains = []
seen_chains = set()

for chain in chains:
chain_tuple = tuple(chain)
if chain_tuple not in seen_chains:
seen_chains.add(chain_tuple)
unique_chains.append(chain)

chains = unique_chains

Класс для расчета силы команд

class TeamStrengthCalculator:
def init(self):
self.games = []
self.equations_method_1 = []
self.equations_method_2 = []
self.teams = set()

text
def add_games(self, games): """ games – список кортежей: (league, date, home_team, away_team, odds1, odds_draw, odds2) """ for g in games: league, date, home_team, away_team, home_odds, draw_odds, away_odds = g self.games.append({ 'league': league, 'date': date, 'home_team': clean_team_name(home_team), 'away_team': clean_team_name(away_team), 'home_odds': float(str(home_odds).replace(',', '.')), 'draw_odds': float(str(draw_odds).replace(',', '.')), 'away_odds': float(str(away_odds).replace(',', '.')) }) self.teams.add(clean_team_name(home_team)) self.teams.add(clean_team_name(away_team)) def calculate_strength_difference_method_1(self): """ Метод 1. Для каждого матча берём индивидуальные пороги (threshold_home, threshold_away) и формируем &laquo;уравнения&raquo; (строки), которые потом решаются в simplify_with_sympy. """ for game in self.games: league_name = game['league'] home_team = game['home_team'] away_team = game['away_team'] home_odds = game['home_odds'] away_odds = game['away_odds'] # Берём пороги для данной лиги (или по умолчанию 2.40 и 3.00) threshold_home, threshold_away = league_thresholds.get(league_name, (2.40, 3.00)) if home_odds < threshold_home and away_odds > threshold_away: # формируем "уравнение": "HomeTeam = AwayTeam + 0.123" home_strength = (100 / threshold_home) - (100 / home_odds) away_strength = (100 / away_odds) - (100 / threshold_away) strength_difference = (abs(home_strength) + abs(away_strength)) / 2 self.equations_method_1.append( f"{home_team} = {away_team} + {strength_difference:.3f}" ) elif home_odds > threshold_home and away_odds < threshold_away: home_strength = (100 / home_odds) - (100 / threshold_home) away_strength = (100 / threshold_away) - (100 / away_odds) strength_difference = (abs(home_strength) + abs(away_strength)) / 2 self.equations_method_1.append( f"{away_team} = {home_team} + {strength_difference:.3f}" ) else: # Игнорируем для метода 1, если не подошли под условия pass def calculate_strength_difference_method_2(self): """ Метод 2. Аналогично, но с другой логикой. """ for game in self.games: league_name = game['league'] home_team = game['home_team'] away_team = game['away_team'] home_odds = game['home_odds'] away_odds = game['away_odds'] threshold_home, threshold_away = league_thresholds.get(league_name, (2.40, 3.00)) if home_odds < away_odds: # хозяева считаются фаворитом if home_odds < threshold_home: home_strength = abs((100 / threshold_home) - (100 / home_odds)) self.equations_method_2.append( f"{home_team} = {away_team} + {home_strength:.3f}" ) else: home_strength = abs((100 / home_odds) - (100 / threshold_home)) self.equations_method_2.append( f"{away_team} = {home_team} + {home_strength:.3f}" ) else: # гости считаются фаворитом if away_odds < threshold_away: away_strength = abs((100 / threshold_away) - (100 / away_odds)) self.equations_method_2.append( f"{away_team} = {home_team} + {away_strength:.3f}" ) else: away_strength = abs((100 / away_odds) - (100 / threshold_away)) self.equations_method_2.append( f"{home_team} = {away_team} + {away_strength:.3f}" ) def simplify_with_sympy(self, start_team, end_team): """ Решаем сформированные &laquo;уравнения&raquo; и вычисляем разницу сил между start_team и end_team. В этом методе также берём пороги исходя из лиги первого матча в self.games (если в цепочке несколько лиг – используем первую). """ if not self.games: print("Нет матчей для расчета.") return # Лига первого матча (если цепочка состоит из нескольких лиг, берём первую) first_league = self.games[0]['league'] threshold_home, threshold_away = league_thresholds.get(first_league, (2.40, 3.00)) # Создаём символы для каждой команды team_symbols = {} for team in self.teams: if team == start_team: # Фиксируем силу стартовой команды = 0 team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) # Преобразуем equations_method_1 в список уравнений SymPy sympy_equations_method_1 = [] for equation in self.equations_method_1: parts = equation.split('=') if len(parts) == 2 and '+' in parts[1]: left_team, right = parts[0].strip(), parts[1].strip() right_team, strength_diff = right.split('+') right_team = right_team.strip() strength_diff = float(strength_diff.strip()) left_sym = team_symbols[left_team] right_sym = team_symbols[right_team] sympy_equations_method_1.append(Eq(left_sym, right_sym + strength_diff)) # Преобразуем equations_method_2 в список уравнений SymPy sympy_equations_method_2 = [] for equation in self.equations_method_2: parts = equation.split('=') if len(parts) == 2 and '+' in parts[1]: left_team, right = parts[0].strip(), parts[1].strip() right_team, strength_diff = right.split('+') right_team = right_team.strip() strength_diff = float(strength_diff.strip()) left_sym = team_symbols[left_team] right_sym = team_symbols[right_team] sympy_equations_method_2.append(Eq(left_sym, right_sym + strength_diff)) # Формируем список символов (исключая те, что зафиксированы = 0) symbols_list = [sym for sym in team_symbols.values() if sym != 0] # Решение уравнений solution_1 = solve(sympy_equations_method_1, symbols_list, dict=True) if sympy_equations_method_1 else [] solution_2 = solve(sympy_equations_method_2, symbols_list, dict=True) if sympy_equations_method_2 else [] # Ищем символ для end_team end_symbol = team_symbols.get(end_team) final_results = [] # --- Обработка результатов метода 1 --- if solution_1 and end_symbol is not None: sol = solution_1[0] if end_symbol in sol: end_value = sol[end_symbol] try: end_value_numeric = float(end_value.evalf()) # сила start_team = 0, значит разница (start_team - end_team) = -end_value_numeric strength_diff = -end_value_numeric new_odds_start = 100 / ((100 / threshold_home) + strength_diff) new_odds_end = 100 / ((100 / threshold_home) - strength_diff) final_results.append( f"Method 1: {start_team} = {end_team} + {strength_diff:.3f}; " f"{start_team} odds = {new_odds_start:.3f}; {end_team} odds = {new_odds_end:.3f}" ) except (TypeError, ValueError): print(f"Не удалось вычислить численное значение силы {end_team} в методе 1.") else: print(f"Команда {end_team} не найдена в решении метода 1.") else: print("Нет решения для метода 1.") # --- Обработка результатов метода 2 --- if solution_2 and end_symbol is not None: sol = solution_2[0] if end_symbol in sol: end_value = sol[end_symbol] try: end_value_numeric = float(end_value.evalf()) strength_diff = -end_value_numeric new_odds_start = 100 / ((100 / threshold_home) + strength_diff) new_odds_end = 100 / ((100 / threshold_home) - strength_diff) final_results.append( f"Method 2: {start_team} = {end_team} + {strength_diff:.3f}; " f"{start_team} odds = {new_odds_start:.3f}; {end_team} odds = {new_odds_end:.3f}" ) except (TypeError, ValueError): print(f"Не удалось вычислить численное значение силы {end_team} в методе 2.") else: print(f"Команда {end_team} не найдена в решении метода 2.") else: print("Нет решения для метода 2.") # Вывод итоговых результатов if final_results: for result in final_results: print(result) else: print("Не удалось вычислить разницу в силе между командами.")

Если цепочки найдены

if chains:
# Группируем цепочки по длине
chain_groups = {}
for chain in chains:
chain_length = len(chain)
if chain_length not in chain_groups:
chain_groups[chain_length] = []

text
# Вычисляем &laquo;среднюю дату&raquo; в цепочке (для сортировки от более новых к старым) dates = [] for match in chain: # match[1] – это дата raw_date = match[1] date_obj = None # Пытаемся аккуратно преобразовать к datetime if isinstance(raw_date, datetime): date_obj = raw_date elif isinstance(raw_date, (float, int)): # Считаем, что это Excel-формат дат (кол-во дней с 30.12.1899). try: date_obj = datetime(1899, 12, 30) + timedelta(days=int(raw_date)) except: date_obj = None elif isinstance(raw_date, str): # Проверяем, есть ли разделитель " - " try: if ' - ' in raw_date: date_part = raw_date.split(' - ')[0].strip() # "dd.mm.yyyy" date_obj = datetime.strptime(date_part, '%d.%m.%Y') else: # Может быть просто "dd.mm.yyyy" date_obj = datetime.strptime(raw_date.strip(), '%d.%m.%Y') except: date_obj = None if date_obj: dates.append(date_obj) else: print(f"Не удалось интерпретировать дату: {raw_date}") if dates: avg_timestamp = sum(d.timestamp() for d in dates) / len(dates) avg_date = datetime.fromtimestamp(avg_timestamp) chain_groups[chain_length].append({'chain': chain, 'avg_date': avg_date}) else: # Если не получилось распарсить ни одной даты, ставим минимальную для сортировки chain_groups[chain_length].append({'chain': chain, 'avg_date': datetime.min}) # Сортируем цепочки в каждой группе по средней дате (от более свежей к более старой) for chain_length in sorted(chain_groups.keys()): chain_infos = chain_groups[chain_length] chain_infos_sorted = sorted(chain_infos, key=lambda x: x['avg_date'], reverse=True) chain_groups[chain_length] = chain_infos_sorted # Выводим результаты chain_index = 1 for chain_length in sorted(chain_groups.keys()): print(f"\n=== Цепочки из {chain_length} WTP ===\n") chain_infos = chain_groups[chain_length] for chain_info in chain_infos: chain = chain_info['chain'] avg_date = chain_info['avg_date'] avg_date_str = avg_date.strftime('%d.%m.%Y') print(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") # Собираем матчи и лиги games = [] used_leagues = set() for match in chain: league, date, home_team, away_team, odds1, odds_draw, odds2 = match # Упрощенный вывод каждого матча print(f"('{date}', '{home_team}', '{away_team}', {odds1}, {odds_draw}, {odds2})") games.append(match) used_leagues.add(league) print("\nИспользуемые лиги в цепочке и их пороги:") for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) print(f" Лига: {lg} &rarr; home={th_home}, away={th_away}") # Расчет и вывод методов calculator = TeamStrengthCalculator() calculator.add_games(games) calculator.calculate_strength_difference_method_1() calculator.calculate_strength_difference_method_2() calculator.simplify_with_sympy(start_team, end_team) print("\n" + "="*75 + "\n") chain_index += 1

else:
print("Не найдены WTP-цепочки между указанными командами.") изучи этот код и скажи есть ли способы лучше посчитать коэффициент между командами в будущей игре чем здесь

Thought

Critiquing code to compute odds for future games

The user’s current approach relies on bookmaker odds as proxies and uses threshold heuristics, but faces issues like inconsistency across leagues, missing draw odds, and overdetermined problems. Improvements could include converting odds to implied probabilities, using Poisson models or Elo rating updates with home advantage, and incorporating market-implied expected goal difference using Skellam.

Improving odds computation through graph and Bayesian methods

To improve odds calculation, I suggest using a graph approach where matches are edges with weights calculated from the log of team odds. Solving for rating differences could be done with regularized least squares, incorporating recency, bookmaker liquidity, and league thresholds. A Bayesian model could estimate team strength, alongside continuous mappings from odds to rating difference for more precise estimation. Also, using regression models with various features can help infer fair odds.

Proposing a pipeline for odds and match prediction

I could suggest a pipeline: First, gather past match data with odds and compute implied probabilities after de-vig. Then, use regularized least squares on equations for each match, considering recency. To predict future matches, use logistic regression with home advantage. For draws, multinomial logistic or Poisson models are options. I’ll note that using BFS chains introduces biases, and global league data should be considered. Additionally, code performance can improve by moving away from sympy and ensuring consistent mapping for team names.

Providing improved suggestions with formulas and methods

I could suggest a few improvements for the user: First, de-vig probabilities (e.g., p1=1/o1). To compute rating differences, log-odds can be used with the equation: s_home - s_away + h = k*d. I’ll propose three methods: A) Elo ratings based on market probabilities, B) Least squares ratings from all matches, and C) Poisson models using expected goals from odds, or without thresholds using continuous mappings.

Thought for 33s

Answer

Да, можно. Текущий код — это скорее эвристика по “цепочкам” + попытка “протянуть” разницу сил через набор локальных уравнений из коэффициентов. Это работает иногда, но у него есть системные проблемы, из-за которых для прогноза коэфов будущей игры он почти всегда будет хуже, чем более “статистические” подходы.

Что в текущем подходе слабое (почему прогноз может быть нестабильным)

  1. Ты используешь только цепочки между двумя командами (max_depth=4).
    Это отбрасывает 90% информации лиги. Оценки силы получаются “локальными” и зависят от того, нашлась ли удачная цепочка.

  2. Пороги (threshold_home/threshold_away) — дискретная логика.
    Команда чуть сильнее/слабее порога → формула резко меняется, хотя реальный эффект непрерывен.

  3. Маржа букмекера не убирается.
    Ты работаешь с “грязными” odds. Правильнее сначала переводить odds → вероятности и “снимать маржу” (devig).

  4. Ничья почти не участвует в силе.
    Хотя для футбола draw — огромная часть структуры рынка. Игнорирование odds_draw ухудшает прогноз.

  5. Sympy solve на уравнениях вида A = B + k:

  • уравнений обычно мало/много, есть циклы → решения могут быть неоднозначными/переопределёнными,
  • нет взвешивания по давности, нет регуляризации, нет оценки ошибки.
  1. В simplify_with_sympy() ты потом переводишь “силу” обратно в odds через порог первой лиги цепочки — это просто невалидно, если в цепочке разные лиги (и даже в одной лиге это сомнительно).

Способы посчитать “коэффициент будущей игры” заметно лучше

Ниже — 3 практичных варианта, которые почти всегда дадут более стабильный и калиброванный результат, чем цепочки+пороги.

1) Рейтинги через линейную алгебру (Massey / Ridge) по всем матчам и odds

Идея: считаем, что рынок даёт нам “сигнал силы” в каждом матче, и из всех матчей лиги оцениваем рейтинги команд.

Шаги:

  1. Переводим odds (1/X/2) в implied probabilities: p1=1/o11/o1+1/oX+1/o2,    pX=1/oX,    p2=1/o2p_1 = \frac{1/o_1}{1/o_1 + 1/o_X + 1/o_2},\;\; p_X = \frac{1/o_X}{\dots},\;\; p_2 = \frac{1/o_2}{\dots} (это снимает маржу простейшей нормализацией).
  2. Делаем “цель” для матча — например лог-отношение вероятностей побед: y=log(p1p2)y = \log\left(\frac{p_1}{p_2}\right)
  3. Строим систему: rhomeraway+hkyr_{home} - r_{away} + h \approx k \cdot y где r_* — рейтинги, h — домашнее преимущество, k — масштаб.
  4. Решаем взвешенным ridge (регуляризация), вес = давность матча (например экспонента).

Почему это лучше твоего:

  • использует всю историю, а не цепочки,
  • решение всегда существует (ridge),
  • можно естественно учитывать “свежесть” и качество линии.

Для прогноза будущей игры:

  • берём diff = r_home - r_away + h,
  • переводим в вероятности (минимально — в 2-way через sigmoid, лучше — 3-way, см. ниже),
  • odds = 1/p.

2) Elo, но обновляемый от market probabilities (а не от фактического результата)

Если у тебя мало данных/хочется проще — отличный компромисс.

  • Убираешь маржу → получаешь p1, px, p2.
  • Конвертишь матч в ожидаемый счёт (например 1 за победу, 0.5 за ничью, 0 за поражение): E=p1+0.5pXE = p_1 + 0.5 \cdot p_X
  • Обновляешь Elo по факту результата, но “ожидание” берёшь из odds (это сильно стабилизирует).

Плюсы:

  • очень просто и быстро,
  • естественно учитывает последовательность и давность,
  • меньше “магии” с порогами.

Минусы:

  • хуже, чем (1) и (3) на больших выборках, но обычно всё равно лучше цепочек.

3) Футбольная классика: Poisson / Dixon–Coles (лучший путь для 3-исходного рынка)

Чтобы прогнозировать 1/X/2 корректно, сильнее всего работают модели, которые моделируют голы.

Базовая идея:

  • λhome=exp(αhomeβaway+H)\lambda_{home} = \exp(\alpha_{home} - \beta_{away} + H)
  • λaway=exp(αawayβhome)\lambda_{away} = \exp(\alpha_{away} - \beta_{home})

Голы ~ Пуассон (или бивариантный), далее считаем вероятности всех счётов → p1, px, p2 → odds.

Можно обучать:

  • по реальным голам (идеально),
  • либо (если голов нет) — “подгонять” параметры так, чтобы модель воспроизводила рыночные p1, px, p2 (хуже, но всё равно лучше порогов).

Плюсы:

  • честно и стабильно работает с ничьей,
  • даёт осмысленные probabilities и тоталы.

Если улучшать именно твой код “минимальными правками”

Если ты пока не хочешь переписывать на рейтинги/пуассон, минимум что резко повысит качество:

  1. Всегда devig odds (нормализация 1/odds).
  2. Убрать пороги и сделать непрерывную разницу силы, например через log(p1/p2) вместо ветвлений threshold.
  3. Не цепочки: оцени силу на всей лиге (или хотя бы на всех матчах команд за период), а потом прогнозируй нужную пару.
  4. Добавить вес по давности (например half-life 60–120 дней).
  5. Учитывать ничью (хотя бы через E = p1 + 0.5*px).

Что бы я выбрал на практике

  • Если у тебя есть голы/результаты: Dixon–Coles / Poisson (вариант 3).
  • Если есть только odds и много матчей: Ridge/Massey на log(p1/p2) (вариант 1).
  • Если хочется быстро и просто: Elo от market probabilities (вариант 2).

Если хочешь, я могу накидать готовую версию под твой формат Excel:

  • чтение,
  • devig,
  • веса по давности,
  • оценка рейтингов ridge,
  • выдача “fair odds” 1/X/2 на будущий матч.

Share this Q&A