import pandas as pd from collections import deque ...
बनाया गया: 10 जनवरी 2026
का उपयोग करके उत्तर दिया गया GPT-5.2 Thinking द्वारा Chat01
बनाया गया: 10 जनवरी 2026
का उपयोग करके उत्तर दिया गया GPT-5.2 Thinking द्वारा Chat01
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),
"Netherlands - Eridivisie": (2.20, 3.20),
"England - Premier League": (2.18, 3.60)
}
def clean_team_name(name):
if isinstance(name, str):
return ' '.join(name.strip().split())
return name
excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx"
df = pd.read_excel(excel_file_path)
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
textif 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()
textdef 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) и формируем «уравнения» (строки), которые потом решаются в 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): """ Решаем сформированные «уравнения» и вычисляем разницу сил между 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# Вычисляем «среднюю дату» в цепочке (для сортировки от более новых к старым) 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} → 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-цепочки между указанными командами.") изучи этот код, задача следующая: нужно чтобы я мог при вводе команд ввести сразу список команд, допустим в таком виде: Malaga - Granada, Granada - Albacete и так далее, хочу чтобы была возможность чтобы код отработал отработал сразу все игры которые мне нужны, ну и так же чтобы осталась возмодность обрабатывать и просто 2 команды, все данные по названиям команд датам и коэффициентам код берет их excel файла как это уже было реализовано ранее, после того как код посичтает все игры они должны быть сохранены в excel файле который будет создан автоматически, больше ничего не изменяй в коде, мне нужно просто чтобы были дополнения в коде которые мне нужны, выполни это и вышли мне полный исправленный код
pythonimport os 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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } # Функция для очистки имен команд от лишних пробелов 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 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 # ===== ДОБАВЛЕНО: ввод списка пар или режим 2 команд ===== raw_input_teams = input("Введите название стартовой команды: ").strip() team_pairs = [] # Если введён список пар (через запятую) или одна пара формата "TeamA - TeamB" if (',' in raw_input_teams) or (' - ' in raw_input_teams): parts = [p.strip() for p in raw_input_teams.split(',') if p.strip()] for p in parts: if ' - ' not in p: print(f"Пропуск (неверный формат, нужен 'Команда1 - Команда2'): {p}") continue a, b = p.split(' - ', 1) a = clean_team_name(a) b = clean_team_name(b) if a and b: team_pairs.append((a, b)) if not team_pairs: # Если ничего корректно не распарсили, падаем обратно в режим 2 команд start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: # Обычный режим: 2 команды start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # Класс для расчета силы команд class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() 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) и формируем «уравнения» (строки), которые потом решаются в 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): """ Решаем сформированные «уравнения» и вычисляем разницу сил между 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("Не удалось вычислить разницу в силе между командами.") # ===== ДОБАВЛЕНО: возвращаем результаты для сохранения в Excel ===== return final_results # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: накопление результатов для сохранения в Excel ===== all_results_rows = [] # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: обработка сразу нескольких пар (или одной) ===== for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): # Поиск цепочек между двумя заданными командами 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 # Если цепочки найдены if chains: # Группируем цепочки по длине chain_groups = {} for chain in chains: chain_length = len(chain) if chain_length not in chain_groups: chain_groups[chain_length] = [] # Вычисляем «среднюю дату» в цепочке (для сортировки от более новых к старым) 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=== (Запрос {request_num}) {start_team} -> {end_team} | Цепочки из {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') if avg_date != datetime.min else "" print(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") # Собираем матчи и лиги games = [] used_leagues = set() match_lines = [] 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) match_lines.append(f"('{date}', '{home_team}', '{away_team}', {odds1}, {odds_draw}, {odds2})") print("\nИспользуемые лиги в цепочке и их пороги:") for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) print(f" Лига: {lg} → home={th_home}, away={th_away}") # Расчет и вывод методов calculator = TeamStrengthCalculator() calculator.add_games(games) calculator.calculate_strength_difference_method_1() calculator.calculate_strength_difference_method_2() final_results = calculator.simplify_with_sympy(start_team, end_team) # ===== ДОБАВЛЕНО: запись результатов в таблицу для Excel ===== method1_text = "" method2_text = "" for r in final_results: if r.startswith("Method 1:"): method1_text = r elif r.startswith("Method 2:"): method2_text = r all_results_rows.append({ "Request #": request_num, "Start Team": start_team, "End Team": end_team, "Chain #": chain_index, "Chain Length (WTP)": chain_length, "Average Date": avg_date_str, "Matches": "\n".join(match_lines), "Used Leagues": ", ".join(sorted(used_leagues)), "Method 1 Result": method1_text, "Method 2 Result": method2_text }) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== print("\n" + "="*75 + "\n") chain_index += 1 else: print(f"Не найдены WTP-цепочки между указанными командами: {start_team} -> {end_team}") # ===== ДОБАВЛЕНО: фиксируем отсутствие цепочек в Excel-отчёте ===== all_results_rows.append({ "Request #": request_num, "Start Team": start_team, "End Team": end_team, "Chain #": "", "Chain Length (WTP)": "", "Average Date": "", "Matches": "", "Used Leagues": "", "Method 1 Result": "Не найдены WTP-цепочки", "Method 2 Result": "Не найдены WTP-цепочки" }) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: сохранение результатов в новый Excel-файл ===== if all_results_rows: out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() out_name = f"wtp_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" output_excel_path = os.path.join(out_dir, out_name) out_df = pd.DataFrame(all_results_rows) out_df.to_excel(output_excel_path, index=False) print(f"\nГотово. Результаты сохранены в Excel:\n{output_excel_path}") # ===== КОНЕЦ ДОБАВЛЕНИЯ =====
нужно чтобы код не создавал каждый раз новые excel файлы с результатами, а использовал всегда один, и просто дополнял его, и очень бы хотелось чтобы информация в excel была не конкретно распределена по ячейкам, а просто как в worde надеюсь ты меня понял, чтобы границы ячеек не имели значение, переделай код и вышли мне полный вариант
pythonimport os import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment # Словарь со значениями пороговых коэффициентов для каждой лиги 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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } # Функция для очистки имен команд от лишних пробелов 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 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 # ===== ДОБАВЛЕНО: ввод списка пар или режим 2 команд ===== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Ввод: " ).strip() team_pairs = [] # Если введён список пар (через запятую) или одна пара формата "TeamA - TeamB" if (',' in raw_input_teams) or (' - ' in raw_input_teams): parts = [p.strip() for p in raw_input_teams.split(',') if p.strip()] for p in parts: if ' - ' not in p: print(f"Пропуск (неверный формат, нужен 'Команда1 - Команда2'): {p}") continue a, b = p.split(' - ', 1) a = clean_team_name(a) b = clean_team_name(b) if a and b: team_pairs.append((a, b)) if not team_pairs: # Если ничего корректно не распарсили, падаем обратно в режим 2 команд start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: # Обычный режим: 2 команды start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: сохранение в ОДИН Excel и дописывание "как в Word" ===== def append_text_report_to_excel(output_excel_path, text_block): """ Пишем отчёт "как в Word": один большой текст в одной ячейке (A), переносы строк внутри, скрытые сетки, ширина колонки большая. Каждый новый запуск дописывает ниже (не создаёт новый файл). """ if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) if "Report" in wb.sheetnames: ws = wb["Report"] else: ws = wb.create_sheet("Report") else: wb = Workbook() ws = wb.active ws.title = "Report" # Настройки вида (чтобы было ближе к Word) ws.sheet_view.showGridLines = False ws.column_dimensions["A"].width = 140 # Ищем следующую строку для записи (оставляем пустую строку между блоками) if ws.max_row == 1 and ws["A1"].value is None: next_row = 1 else: next_row = ws.max_row + 2 cell = ws.cell(row=next_row, column=1, value=text_block) cell.alignment = Alignment(wrap_text=True, vertical="top") # Примерная высота строки по количеству строк текста (Excel ограничен высотой строки) line_count = text_block.count("\n") + 1 approx_height = 15 * line_count ws.row_dimensions[next_row].height = min(max(15, approx_height), 409) wb.save(output_excel_path) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # Класс для расчета силы команд class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() 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) и формируем «уравнения» (строки), которые потом решаются в 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: 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: 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): """ Решаем сформированные «уравнения» и вычисляем разницу сил между 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: 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()) 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("Не удалось вычислить разницу в силе между командами.") return final_results # ===== ДОБАВЛЕНО: собираем текст отчёта (для Excel "как Word") ===== report_lines = [] report_lines.append(f"RUN: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}") report_lines.append(f"Источник данных: {excel_file_path}") report_lines.append("Запросы:") for i, (a, b) in enumerate(team_pairs, start=1): report_lines.append(f" {i}) {a} -> {b}") report_lines.append("-" * 80) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: обработка сразу нескольких пар (или одной) ===== for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): # Поиск цепочек между двумя заданными командами 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 report_lines.append(f"\n[Запрос {request_num}] {start_team} -> {end_team}") # Если цепочки найдены if chains: # Группируем цепочки по длине chain_groups = {} for chain in chains: chain_length = len(chain) if chain_length not in chain_groups: chain_groups[chain_length] = [] # Вычисляем «среднюю дату» в цепочке (для сортировки от более новых к старым) dates = [] for match in chain: raw_date = match[1] date_obj = None if isinstance(raw_date, datetime): date_obj = raw_date elif isinstance(raw_date, (float, int)): 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() date_obj = datetime.strptime(date_part, '%d.%m.%Y') else: 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=== (Запрос {request_num}) {start_team} -> {end_team} | Цепочки из {chain_length} WTP ===\n") report_lines.append(f"\n=== Цепочки из {chain_length} WTP ===") 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') if avg_date != datetime.min else "" print(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") report_lines.append(f"\nЦепочка {chain_index} (Средняя дата: {avg_date_str}):") # Собираем матчи и лиги games = [] used_leagues = set() report_lines.append("Матчи:") 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})") report_lines.append(f"('{date}', '{home_team}', '{away_team}', {odds1}, {odds_draw}, {odds2})") games.append(match) used_leagues.add(league) print("\nИспользуемые лиги в цепочке и их пороги:") report_lines.append("\nИспользуемые лиги в цепочке и их пороги:") for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) print(f" Лига: {lg} → home={th_home}, away={th_away}") report_lines.append(f" Лига: {lg} → home={th_home}, away={th_away}") # Расчет и вывод методов calculator = TeamStrengthCalculator() calculator.add_games(games) calculator.calculate_strength_difference_method_1() calculator.calculate_strength_difference_method_2() final_results = calculator.simplify_with_sympy(start_team, end_team) report_lines.append("\nРезультаты:") if final_results: for r in final_results: report_lines.append(r) else: report_lines.append("Не удалось вычислить разницу в силе между командами.") print("\n" + "="*75 + "\n") report_lines.append("\n" + "=" * 75) chain_index += 1 else: print(f"Не найдены WTP-цепочки между указанными командами: {start_team} -> {end_team}") report_lines.append("Не найдены WTP-цепочки между указанными командами.") # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: сохраняем в ОДИН файл и дописываем ===== out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") final_text_block = "\n".join(report_lines) append_text_report_to_excel(output_excel_path, final_text_block) print(f"\nГотово. Результаты ДОПИСАНЫ в Excel:\n{output_excel_path}") # ===== КОНЕЦ ДОБАВЛЕНИЯ =====
Введите либо 2 команды (как раньше), либо список пар через запятую.
Пример: Malaga - Granada, Granada - Albacete
Ввод: Mirandes – Almeria, Andorra – Cultural Leonesa, Real Sociedad B – Albacete, Burgos CF – Eibar, Las Palmas – Dep. La Coruna, Racing Santander – Zaragoza, Leganes – Valladolid, Granada CF – Castellon, Malaga – Ceuta, Huesca – Cordoba
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Mirandes – Almeria
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Andorra – Cultural Leonesa
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Real Sociedad B – Albacete
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Burgos CF – Eibar
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Las Palmas – Dep. La Coruna
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Racing Santander – Zaragoza
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Leganes – Valladolid
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Granada CF – Castellon
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Malaga – Ceuta
Пропуск (неверный формат, нужен 'Команда1 - Команда2'): Huesca – Cordoba в чем проблема, исправь эту ошибку и вышли полный исправленный код
pythonimport os import re import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment # Словарь со значениями пороговых коэффициентов для каждой лиги 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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } # Функция для очистки имен команд от лишних пробелов 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 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 # ===== ДОБАВЛЕНО: ввод списка пар или режим 2 команд (ИСПРАВЛЕНО: поддержка –, —, -) ===== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Можно использовать дефис '-', en dash '–' или em dash '—'.\n" "Ввод: " ).strip() team_pairs = [] # Регекс-разделитель между командами: пробелы + ( - или – или — ) + пробелы pair_splitter = re.compile(r"\s*[-–—]\s*") # Считаем, что пользователь ввёл пары, если есть запятая или любой из тире/дефисов has_any_dash = bool(re.search(r"[-–—]", raw_input_teams)) if (',' in raw_input_teams) or has_any_dash: # Разделяем пары по запятым (также можно случайно вставить ';' или перевод строки) parts = [p.strip() for p in re.split(r"[,\n;]+", raw_input_teams) if p.strip()] for p in parts: # Делим на 2 команды по любому из тире/дефисов teams = [t for t in pair_splitter.split(p) if t.strip()] if len(teams) != 2: print(f"Пропуск (неверный формат пары, нужен 'Команда1 - Команда2'): {p}") continue a = clean_team_name(teams[0]) b = clean_team_name(teams[1]) if a and b: team_pairs.append((a, b)) if not team_pairs: # Если ничего корректно не распарсили, падаем обратно в режим 2 команд start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: # Обычный режим: 2 команды start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: сохранение в ОДИН Excel и дописывание "как в Word" ===== def append_text_report_to_excel(output_excel_path, text_block): """ Пишем отчёт "как в Word": один большой текст в одной ячейке (A), переносы строк внутри, скрытые сетки, ширина колонки большая. Каждый новый запуск дописывает ниже (не создаёт новый файл). """ if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) if "Report" in wb.sheetnames: ws = wb["Report"] else: ws = wb.create_sheet("Report") else: wb = Workbook() ws = wb.active ws.title = "Report" # Настройки вида (чтобы было ближе к Word) ws.sheet_view.showGridLines = False ws.column_dimensions["A"].width = 140 # Ищем следующую строку для записи (оставляем пустую строку между блоками) if ws.max_row == 1 and ws["A1"].value is None: next_row = 1 else: next_row = ws.max_row + 2 cell = ws.cell(row=next_row, column=1, value=text_block) cell.alignment = Alignment(wrap_text=True, vertical="top") # Примерная высота строки по количеству строк текста (Excel ограничен высотой строки) line_count = text_block.count("\n") + 1 approx_height = 15 * line_count ws.row_dimensions[next_row].height = min(max(15, approx_height), 409) wb.save(output_excel_path) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # Класс для расчета силы команд class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() 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) и формируем «уравнения» (строки), которые потом решаются в 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'] threshold_home, threshold_away = league_thresholds.get(league_name, (2.40, 3.00)) if home_odds < threshold_home and away_odds > threshold_away: 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: 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): """ Решаем сформированные «уравнения» и вычисляем разницу сил между 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: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) 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)) 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)) 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_symbol = team_symbols.get(end_team) final_results = [] 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()) 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.") 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("Не удалось вычислить разницу в силе между командами.") return final_results # ===== ДОБАВЛЕНО: собираем текст отчёта (для Excel "как Word") ===== report_lines = [] report_lines.append(f"RUN: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}") report_lines.append(f"Источник данных: {excel_file_path}") report_lines.append("Запросы:") for i, (a, b) in enumerate(team_pairs, start=1): report_lines.append(f" {i}) {a} -> {b}") report_lines.append("-" * 80) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: обработка сразу нескольких пар (или одной) ===== for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): 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 report_lines.append(f"\n[Запрос {request_num}] {start_team} -> {end_team}") if chains: chain_groups = {} for chain in chains: chain_length = len(chain) if chain_length not in chain_groups: chain_groups[chain_length] = [] dates = [] for match in chain: raw_date = match[1] date_obj = None if isinstance(raw_date, datetime): date_obj = raw_date elif isinstance(raw_date, (float, int)): 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() date_obj = datetime.strptime(date_part, '%d.%m.%Y') else: 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=== (Запрос {request_num}) {start_team} -> {end_team} | Цепочки из {chain_length} WTP ===\n") report_lines.append(f"\n=== Цепочки из {chain_length} WTP ===") 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') if avg_date != datetime.min else "" print(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") report_lines.append(f"\nЦепочка {chain_index} (Средняя дата: {avg_date_str}):") games = [] used_leagues = set() report_lines.append("Матчи:") 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})") report_lines.append(f"('{date}', '{home_team}', '{away_team}', {odds1}, {odds_draw}, {odds2})") games.append(match) used_leagues.add(league) print("\nИспользуемые лиги в цепочке и их пороги:") report_lines.append("\nИспользуемые лиги в цепочке и их пороги:") for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) print(f" Лига: {lg} → home={th_home}, away={th_away}") report_lines.append(f" Лига: {lg} → home={th_home}, away={th_away}") calculator = TeamStrengthCalculator() calculator.add_games(games) calculator.calculate_strength_difference_method_1() calculator.calculate_strength_difference_method_2() final_results = calculator.simplify_with_sympy(start_team, end_team) report_lines.append("\nРезультаты:") if final_results: for r in final_results: report_lines.append(r) else: report_lines.append("Не удалось вычислить разницу в силе между командами.") print("\n" + "="*75 + "\n") report_lines.append("\n" + "=" * 75) chain_index += 1 else: print(f"Не найдены WTP-цепочки между указанными командами: {start_team} -> {end_team}") report_lines.append("Не найдены WTP-цепочки между указанными командами.") # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: сохраняем в ОДИН файл и дописываем ===== out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") final_text_block = "\n".join(report_lines) append_text_report_to_excel(output_excel_path, final_text_block) print(f"\nГотово. Результаты ДОПИСАНЫ в Excel:\n{output_excel_path}") # ===== КОНЕЦ ДОБАВЛЕНИЯ =====
pythonimport os import re import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment # Словарь со значениями пороговых коэффициентов для каждой лиги 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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } # ===== ДОБАВЛЕНО: нормализация тире (–, —) чтобы совпадали названия команд ===== def normalize_dashes(text): if isinstance(text, str): return text.replace('–', '-').replace('—', '-').replace('−', '-') return text # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # Функция для очистки имен команд от лишних пробелов def clean_team_name(name): if isinstance(name, str): name = normalize_dashes(name) 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 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 # ===== ДОБАВЛЕНО: ввод списка пар или режим 2 команд (ИСПРАВЛЕНО: –, — и чистка мусора из терминала) ===== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Можно использовать '-', '–' или '—'.\n" "Ввод: " ).strip() raw_input_teams = normalize_dashes(raw_input_teams) team_pairs = [] # Разделитель между командами: ТОЛЬКО когда есть пробелы вокруг '-', чтобы не ломать названия с дефисом внутри pair_splitter = re.compile(r"\s+[-]\s+") def sanitize_pair_text(p): """ Убираем мусор, который иногда попадает в буфер/вставку из терминала, типа: '... Huesca - Cordoba -> & C:\\Users\\...\\python.exe ...' """ if not isinstance(p, str): return p p = p.strip() # Обрезаем всё, что начинается с "-> & <диск>:\" p = re.sub(r"\s*->\s*&\s*[A-Za-z]:[\\/].*$", "", p) # Обрезаем " & C:\..." (только если похоже на путь, чтобы не ломать команды с '&' в названии) p = re.sub(r"\s*&\s*[A-Za-z]:[\\/].*$", "", p) return p.strip() has_any_dash = '-' in raw_input_teams if (',' in raw_input_teams) or has_any_dash: parts = [p.strip() for p in re.split(r"[,\n;]+", raw_input_teams) if p.strip()] for p in parts: p = sanitize_pair_text(p) if not p: continue teams = [t for t in pair_splitter.split(p) if t.strip()] if len(teams) != 2: print(f"Пропуск (неверный формат пары, нужен 'Команда1 - Команда2'): {p}") continue a = clean_team_name(teams[0]) b = clean_team_name(teams[1]) if a and b: team_pairs.append((a, b)) if not team_pairs: # Если ничего корректно не распарсили, падаем обратно в режим 2 команд start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: # Обычный режим: 2 команды start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== ДОБАВЛЕНО: сохранение в ОДИН Excel и дописывание "как в Word", но БЕЗ одной огромной ячейки ===== def append_report_lines_to_excel(output_excel_path, lines): """ Пишем отчёт "как в Word": каждая строка отчёта в отдельной строке листа, в колонку A, скрытые сетки, большая ширина. Каждый запуск дописывается ниже. Так не будет "каши" из-за ограничения высоты одной ячейки в Excel. """ if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) if "Report" in wb.sheetnames: ws = wb["Report"] else: ws = wb.create_sheet("Report") else: wb = Workbook() ws = wb.active ws.title = "Report" ws.sheet_view.showGridLines = False ws.column_dimensions["A"].width = 140 # Следующая строка для дописывания (оставим 1 пустую строку между запусками) if ws.max_row == 1 and ws["A1"].value is None: next_row = 1 else: next_row = ws.max_row + 2 for i, line in enumerate(lines): cell = ws.cell(row=next_row + i, column=1, value=line) cell.alignment = Alignment(wrap_text=True, vertical="top") wb.save(output_excel_path) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # Класс для расчета силы команд class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() 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) и формируем «уравнения» (строки), которые потом решаются в 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'] threshold_home, threshold_away = league_thresholds.get(league_name, (2.40, 3.00)) if home_odds < threshold_home and away_odds > threshold_away: 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: 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): """ Решаем сформированные «уравнения» и вычисляем разницу сил между 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: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) 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)) 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)) 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_symbol = team_symbols.get(end_team) final_results = [] 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()) 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.") 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("Не удалось вычислить разницу в силе между командами.") return final_results # ===== ДОБАВЛЕНО: собираем отчёт построчно (для Excel) ===== report_lines = [] report_lines.append(f"RUN: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}") report_lines.append(f"Источник данных: {excel_file_path}") report_lines.append("Запросы:") for i, (a, b) in enumerate(team_pairs, start=1): report_lines.append(f"{i}) {a} -> {b}") report_lines.append("-" * 80) # ===== КОНЕЦ ДОБАВЛЕНИЯ ===== # ===== обработка ===== for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): 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 report_lines.append(f"") report_lines.append(f"[Запрос {request_num}] {start_team} -> {end_team}") if chains: chain_groups = {} for chain in chains: chain_length = len(chain) if chain_length not in chain_groups: chain_groups[chain_length] = [] dates = [] for match in chain: raw_date = match[1] date_obj = None if isinstance(raw_date, datetime): date_obj = raw_date elif isinstance(raw_date, (float, int)): 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() date_obj = datetime.strptime(date_part, '%d.%m.%Y') else: 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=== (Запрос {request_num}) {start_team} -> {end_team} | Цепочки из {chain_length} WTP ===\n") report_lines.append(f"=== Цепочки из {chain_length} WTP ===") 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') if avg_date != datetime.min else "" print(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") report_lines.append(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") report_lines.append(f"Матчи:") 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})") report_lines.append(f"('{date}', '{home_team}', '{away_team}', {odds1}, {odds_draw}, {odds2})") games.append(match) used_leagues.add(league) print("\nИспользуемые лиги в цепочке и их пороги:") report_lines.append(f"Используемые лиги в цепочке и их пороги:") for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) print(f" Лига: {lg} → home={th_home}, away={th_away}") report_lines.append(f" Лига: {lg} → home={th_home}, away={th_away}") calculator = TeamStrengthCalculator() calculator.add_games(games) calculator.calculate_strength_difference_method_1() calculator.calculate_strength_difference_method_2() final_results = calculator.simplify_with_sympy(start_team, end_team) report_lines.append(f"Результаты:") if final_results: for r in final_results: report_lines.append(r) else: report_lines.append("Не удалось вычислить разницу в силе между командами.") report_lines.append("=" * 75) print("\n" + "="*75 + "\n") chain_index += 1 else: print(f"Не найдены WTP-цепочки между указанными командами: {start_team} -> {end_team}") report_lines.append("Не найдены WTP-цепочки между указанными командами.") # ===== ДОБАВЛЕНО: сохраняем в ОДИН файл и дописываем ===== out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") append_report_lines_to_excel(output_excel_path, report_lines) print(f"\nГотово. Результаты ДОПИСАНЫ в Excel:\n{output_excel_path}") # ===== КОНЕЦ ДОБАВЛЕНИЯ =====
данные в excel сохраняются очень некрасиво, мне не нравится абсолютно, почему нет у ячеек границ никаких, границы должны быть но просто текст должен проходить через эти границы, переделай, мне вообще не нравится как ты это оформил, каждая отдельная игра и ее расчеты должны выделяться как-то, либо пусть будут на разных листах
Понял: тебе нужны нормальные границы (рамки), но при этом текст/описание должно выглядеть “как блок” и не зависеть от того, в какие ячейки попало. Самый удобный вариант в Excel — делать красивые блоки в рамках с объединёнными ячейками (merge), чтобы текст “шёл через границы”, и каждый запуск писать на новый лист (но в тот же один файл), а внутри — каждый запрос/цепочка/матч отделён рамками и заголовками.
Ниже полный код с исправлениями:
-, –, —wtp_results.xlsx всегда один и тот жеpythonimport os import re import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.utils import get_column_letter # Словарь со значениями пороговых коэффициентов для каждой лиги 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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } def normalize_dashes(text): if isinstance(text, str): return text.replace('–', '-').replace('—', '-').replace('−', '-') return text # Функция для очистки имен команд от лишних пробелов def clean_team_name(name): if isinstance(name, str): name = normalize_dashes(name) 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 if home_team not in graph: graph[home_team] = [] if away_team not in graph: graph[away_team] = [] graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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 # ===== ВВОД ПАР КОМАНД (поддержка -, –, —) ===== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Можно использовать '-', '–' или '—'.\n" "Ввод: " ).strip() raw_input_teams = normalize_dashes(raw_input_teams) team_pairs = [] pair_splitter = re.compile(r"\s+[-]\s+") def sanitize_pair_text(p): if not isinstance(p, str): return p p = p.strip() p = re.sub(r"\s*->\s*&\s*[A-Za-z]:[\\/].*$", "", p) p = re.sub(r"\s*&\s*[A-Za-z]:[\\/].*$", "", p) return p.strip() has_any_dash = '-' in raw_input_teams if (',' in raw_input_teams) or has_any_dash: parts = [p.strip() for p in re.split(r"[,\n;]+", raw_input_teams) if p.strip()] for p in parts: p = sanitize_pair_text(p) if not p: continue teams = [t for t in pair_splitter.split(p) if t.strip()] if len(teams) != 2: print(f"Пропуск (неверный формат пары, нужен 'Команда1 - Команда2'): {p}") continue a = clean_team_name(teams[0]) b = clean_team_name(teams[1]) if a and b: team_pairs.append((a, b)) if not team_pairs: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===== EXCEL ОФОРМЛЕНИЕ: стили ===== thin = Side(style="thin", color="000000") medium = Side(style="medium", color="000000") BORDER_THIN = Border(left=thin, right=thin, top=thin, bottom=thin) BORDER_MEDIUM = Border(left=medium, right=medium, top=medium, bottom=medium) ALIGN_WRAP_TOP = Alignment(wrap_text=True, vertical="top") ALIGN_CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) FONT_TITLE = Font(bold=True, size=14) FONT_HEADER = Font(bold=True, size=12) FONT_BOLD = Font(bold=True) FILL_TITLE = PatternFill("solid", fgColor="D9E1F2") FILL_SECTION = PatternFill("solid", fgColor="E2EFDA") FILL_SUBSECTION = PatternFill("solid", fgColor="FFF2CC") FILL_BAD = PatternFill("solid", fgColor="F8CBAD") def safe_sheet_name(name: str) -> str: # запрещённые символы: : \ / ? * [ ] name = re.sub(r"[:\\\/\?\*\[\]]", " ", name) name = re.sub(r"\s+", " ", name).strip() return name[:31] if name else "Sheet" def create_run_sheet(output_excel_path, run_dt: datetime): if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) else: wb = Workbook() # удалим дефолтный лист, если пустой if wb.active and wb.active.title == "Sheet": ws0 = wb.active if ws0.max_row == 1 and ws0["A1"].value is None: wb.remove(ws0) base = f"RUN_{run_dt.strftime('%Y%m%d_%H%M%S')}" sheet_name = base i = 1 while sheet_name in wb.sheetnames: i += 1 sheet_name = safe_sheet_name(f"{base}_{i}") ws = wb.create_sheet(sheet_name) # ширины колонок под читабельность widths = { "A": 20, # дата "B": 26, # home "C": 26, # away "D": 10, # odds1 "E": 10, # draw "F": 10, # odds2 "G": 30, # league "H": 80, # текст/пороги/результаты } for col, w in widths.items(): ws.column_dimensions[col].width = w ws.freeze_panes = "A4" return wb, ws def merge_write(ws, row, col_start, col_end, text, fill=None, font=None, border=None, align=None, height=None): cell_start = f"{get_column_letter(col_start)}{row}" cell_end = f"{get_column_letter(col_end)}{row}" ws.merge_cells(f"{cell_start}:{cell_end}") c = ws[cell_start] c.value = text if fill: c.fill = fill if font: c.font = font if align: c.alignment = align else: c.alignment = ALIGN_WRAP_TOP # применяем границы ко всем ячейкам диапазона (чтобы рамка была везде) if border: for r in range(row, row + 1): for cc in range(col_start, col_end + 1): ws.cell(row=r, column=cc).border = border if height is not None: ws.row_dimensions[row].height = height def write_row_with_borders(ws, row, values, col_start=1, border=BORDER_THIN, header=False): for i, v in enumerate(values): c = ws.cell(row=row, column=col_start + i, value=v) c.border = border c.alignment = ALIGN_WRAP_TOP if header: c.font = FONT_BOLD c.alignment = ALIGN_CENTER c.fill = FILL_SUBSECTION # Класс для расчета силы команд class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() def add_games(self, games): 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): 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 < threshold_home and away_odds > threshold_away: 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: pass def calculate_strength_difference_method_2(self): 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): 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: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) 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)) 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)) 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_symbol = team_symbols.get(end_team) final_results = [] 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()) 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.") 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("Не удалось вычислить разницу в силе между командами.") return final_results # ====== MAIN: считаем + сохраняем красиво в Excel (рамки/блоки/листы) ====== run_dt = datetime.now() out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") wb, ws = create_run_sheet(output_excel_path, run_dt) row = 1 # Заголовок запуска merge_write( ws, row, 1, 8, f"RUN: {run_dt.strftime('%d.%m.%Y %H:%M:%S')} | Источник: {excel_file_path}", fill=FILL_TITLE, font=FONT_TITLE, border=BORDER_MEDIUM, align=ALIGN_CENTER, height=28 ) row += 2 # Список запросов merge_write(ws, row, 1, 8, "Запросы (пары команд):", fill=FILL_SECTION, font=FONT_HEADER, border=BORDER_MEDIUM) row += 1 for i, (a, b) in enumerate(team_pairs, start=1): merge_write(ws, row, 1, 8, f"{i}) {a} -> {b}", border=BORDER_THIN) row += 1 row += 1 # Обработка всех пар for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): merge_write( ws, row, 1, 8, f"ЗАПРОС {request_num}: {start_team} -> {end_team}", fill=FILL_SECTION, font=FONT_HEADER, border=BORDER_MEDIUM ) row += 1 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 if not chains: merge_write( ws, row, 1, 8, "Не найдены WTP-цепочки между указанными командами.", fill=FILL_BAD, font=FONT_BOLD, border=BORDER_MEDIUM ) row += 2 continue # Группировка по длине + сортировка по средней дате chain_groups = {} for chain in chains: chain_length = len(chain) chain_groups.setdefault(chain_length, []) dates = [] for match in chain: raw_date = match[1] date_obj = None if isinstance(raw_date, datetime): date_obj = raw_date elif isinstance(raw_date, (float, int)): 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() date_obj = datetime.strptime(date_part, '%d.%m.%Y') else: 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) else: avg_date = datetime.min chain_groups[chain_length].append({'chain': chain, 'avg_date': avg_date}) for chain_length in chain_groups: chain_groups[chain_length] = sorted(chain_groups[chain_length], key=lambda x: x['avg_date'], reverse=True) # Вывод/запись цепочек chain_index = 1 for chain_length in sorted(chain_groups.keys()): merge_write( ws, row, 1, 8, f"Цепочки из {chain_length} WTP", fill=FILL_SUBSECTION, font=FONT_HEADER, border=BORDER_MEDIUM ) row += 1 for chain_info in chain_groups[chain_length]: chain = chain_info['chain'] avg_date = chain_info['avg_date'] avg_date_str = avg_date.strftime('%d.%m.%Y') if avg_date != datetime.min else "" merge_write( ws, row, 1, 8, f"ЦЕПОЧКА {chain_index} (Средняя дата: {avg_date_str})", fill=FILL_TITLE, font=FONT_HEADER, border=BORDER_MEDIUM ) row += 1 # Таблица матчей write_row_with_borders( ws, row, ["Дата", "Home", "Away", "Odds1", "Draw", "Odds2", "Лига"], col_start=1, border=BORDER_MEDIUM, header=True ) # рамка для колонки H (под текстовые блоки справа) ws.cell(row=row, column=8, value="Примечания / Результаты").border = BORDER_MEDIUM ws.cell(row=row, column=8).font = FONT_BOLD ws.cell(row=row, column=8).alignment = ALIGN_CENTER ws.cell(row=row, column=8).fill = FILL_SUBSECTION row += 1 games = [] used_leagues = set() for match in chain: league, date, home_team, away_team, odds1, odds_draw, odds2 = match games.append(match) used_leagues.add(league) # строка матча write_row_with_borders( ws, row, [str(date), home_team, away_team, odds1, odds_draw, odds2, league], col_start=1, border=BORDER_THIN, header=False ) # пустая H с рамкой ws.cell(row=row, column=8, value="").border = BORDER_THIN ws.cell(row=row, column=8).alignment = ALIGN_WRAP_TOP row += 1 # Блок порогов (в колонке H, но с рамкой) thresholds_lines = ["Пороги лиг в цепочке:"] for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) thresholds_lines.append(f"- {lg}: home={th_home}, away={th_away}") thresholds_text = "\n".join(thresholds_lines) merge_write(ws, row, 1, 7, " ", border=BORDER_THIN) # пустой блок слева (рамка) c = ws.cell(row=row, column=8, value=thresholds_text) c.border = BORDER_THIN c.alignment = ALIGN_WRAP_TOP c.font = FONT_BOLD row += 1 # Расчет и результаты (в H) calculator = TeamStrengthCalculator() calculator.add_games(games) calculator.calculate_strength_difference_method_1() calculator.calculate_strength_difference_method_2() final_results = calculator.simplify_with_sympy(start_team, end_team) results_text = "Результаты:\n" + ("\n".join(final_results) if final_results else "Не удалось вычислить разницу.") merge_write(ws, row, 1, 7, " ", border=BORDER_THIN) c = ws.cell(row=row, column=8, value=results_text) c.border = BORDER_THIN c.alignment = ALIGN_WRAP_TOP row += 1 # Закрывающая толстая рамка-разделитель merge_write(ws, row, 1, 8, "", border=BORDER_MEDIUM) row += 2 chain_index += 1 # разделитель после запроса merge_write(ws, row, 1, 8, "", border=BORDER_MEDIUM) row += 2 wb.save(output_excel_path) print(f"\nГотово. Результаты сохранены в один файл (новый лист на каждый запуск):\n{output_excel_path}")
Если хочешь, я могу сделать ещё лучше:
Скажи, как удобнее: лист = RUN или лист = пара команд?
надо чтобы в excel файле результаты были более компактно, и кстати вывод в терминале мне не понравился: Цепочка 3 (Средняя дата: 07.12.2025):
('09.12.2025 - 02:30', 'Las Palmas', 'Mirandes', 1.56, 4.03, 6.63)
('15.12.2025 - 05:00', 'AD Ceuta', 'Las Palmas', 2.88, 2.92, 2.91)
('27.11.2025 - 03:00', 'AD Ceuta', 'Almeria', 3.34, 3.19, 2.38)
Используемые лиги в цепочке и их пороги:
Лига: Spain Segunda → home=2.11, away=4.02
Method 1: Mirandes = Almeria + -19.468; Mirandes odds = 3.581; Almeria odds = 1.496
Method 2: Mirandes = Almeria + -21.179; Mirandes odds = 3.815; Almeria odds = 1.458
===========================================================================
Цепочка 4 (Средняя дата: 06.12.2025):
('01.12.2025 - 02:30', 'Mirandes', 'Real Sociedad B', 2.03, 3.24, 4.26)
('22.12.2025 - 00:15', 'Real Sociedad B', 'AD Ceuta', 2.95, 2.94, 2.83)
('27.11.2025 - 03:00', 'AD Ceuta', 'Almeria', 3.34, 3.19, 2.38)
Используемые лиги в цепочке и их пороги:
Лига: Spain Segunda → home=2.11, away=4.02
Method 1: Mirandes = Almeria + -27.640; Mirandes odds = 5.062; Almeria odds = 1.333
Method 2: Mirandes = Almeria + -25.733; Mirandes odds = 4.617; Almeria odds = 1.367
===========================================================================
Не найдены WTP-цепочки между указанными командами: Andorra -> Cultural Leonesa
=== (Запрос 3) Real Sociedad B -> Albacete | Цепочки из 2 WTP ===
Цепочка 1 (Средняя дата: 07.12.2025):
('14.12.2025 - 05:00', 'Dep. La Coruna', 'Real Sociedad B', 1.5, 4.45, 6.94)
('30.11.2025 - 02:30', 'Albacete', 'Dep. La Coruna', 2.7, 3.31, 2.77)
Используемые лиги в цепочке и их пороги:
Лига: Spain Segunda → home=2.11, away=4.02
Method 1: Real Sociedad B = Albacete + -4.079; Real Sociedad B odds = 2.309; Albacete odds = 1.943
Method 2: Real Sociedad B = Albacete + -8.917; Real Sociedad B odds = 2.599; Albacete odds = 1.776
===========================================================================
=== (Запрос 3) Real Sociedad B -> Albacete | Цепочки из 3 WTP ===
Цепочка 2 (Средняя дата: 21.12.2025):
('07.12.2025 - 02:30', 'Real Sociedad B', 'Gijon', 3.46, 3.23, 2.3)
('21.12.2025 - 02:30', 'Leganes', 'Gijon', 2.52, 2.96, 3.35)
('05.01.2026 - 02:30', 'Albacete', 'Leganes', 2.41, 3.18, 3.29) было так, стало вот так: Method 2: Mirandes = Almeria + -11.510; Mirandes odds = 2.787; Almeria odds = 1.698
Method 1: Mirandes = Almeria + -14.515; Mirandes odds = 3.042; Almeria odds = 1.615
Method 2: Mirandes = Almeria + -19.706; Mirandes odds = 3.612; Almeria odds = 1.490
Method 1: Mirandes = Almeria + -20.315; Mirandes odds = 3.693; Almeria odds = 1.477
Method 2: Mirandes = Almeria + -26.077; Mirandes odds = 4.691; Almeria odds = 1.361
Method 1: Mirandes = Almeria + -12.647; Mirandes odds = 2.878; Almeria odds = 1.666
Method 2: Mirandes = Almeria + -17.482; Mirandes odds = 3.343; Almeria odds = 1.541
Method 1: Mirandes = Almeria + -20.819; Mirandes odds = 3.763; Almeria odds = 1.466
Method 2: Mirandes = Almeria + -22.036; Mirandes odds = 3.944; Almeria odds = 1.440
Method 1: Mirandes = Almeria + -23.860; Mirandes odds = 4.249; Almeria odds = 1.403
Method 2: Mirandes = Almeria + -27.262; Mirandes odds = 4.967; Almeria odds = 1.339
Method 1: Mirandes = Almeria + -29.953; Mirandes odds = 5.734; Almeria odds = 1.293
Method 2: Mirandes = Almeria + -29.815; Mirandes odds = 5.689; Almeria odds = 1.295
Method 1: Real Sociedad B = Albacete + -4.079; Real Sociedad B odds = 2.309; Albacete odds = 1.943
Method 2: Real Sociedad B = Albacete + -8.917; Real Sociedad B odds = 2.599; Albacete odds = 1.776
Method 1: Real Sociedad B = Albacete + -6.494; Real Sociedad B odds = 2.445; Albacete odds = 1.856
Method 2: Real Sociedad B = Albacete + -4.992; Real Sociedad B odds = 2.358; Albacete odds = 1.909
Method 1: Real Sociedad B = Albacete + -16.993; Real Sociedad B odds = 3.289; Albacete odds = 1.553
Method 2: Real Sociedad B = Albacete + -16.590; Real Sociedad B odds = 3.246; Albacete odds = 1.563
Method 1: Real Sociedad B = Albacete + -14.906; Real Sociedad B odds = 3.078; Albacete odds = 1.605
Method 2: Real Sociedad B = Albacete + -16.342; Real Sociedad B odds = 3.220; Albacete odds = 1.569
Не удалось вычислить численное значение силы Albacete в методе 1.
Method 2: Real Sociedad B = Albacete + -9.994; Real Sociedad B odds = 2.674; Albacete odds = 1.743
Method 1: Real Sociedad B = Albacete + -19.264; Real Sociedad B odds = 3.555; Albacete odds = 1.500
Method 2: Real Sociedad B = Albacete + -19.801; Real Sociedad B odds = 3.624; Albacete odds = 1.488
Method 1: Real Sociedad B = Albacete + -12.590; Real Sociedad B odds = 2.873; Albacete odds = 1.667
Method 2: Real Sociedad B = Albacete + -13.006; Real Sociedad B odds = 2.908; Albacete odds = 1.656
Не удалось вычислить численное значение силы Albacete в методе 1.
Method 2: Real Sociedad B = Albacete + -8.088; Real Sociedad B odds = 2.544; Albacete odds = 1.802
Не удалось вычислить численное значение силы Albacete в методе 1.
Method 2: Real Sociedad B = Albacete + -24.093; Real Sociedad B odds = 4.292; Albacete odds = 1.399
Method 1: Real Sociedad B = Albacete + -12.982; Real Sociedad B odds = 2.906; Albacete odds = 1.656
Method 2: Real Sociedad B = Albacete + -14.114; Real Sociedad B odds = 3.005; Albacete odds = 1.626
Method 1: Real Sociedad B = Albacete + -0.724; Real Sociedad B odds = 2.143; Albacete odds = 2.078
Method 2: Real Sociedad B = Albacete + -1.708; Real Sociedad B odds = 2.189; Albacete odds = 2.037
Method 1: Real Sociedad B = Albacete + -19.705; Real Sociedad B odds = 3.612; Albacete odds = 1.490
Method 2: Real Sociedad B = Albacete + -19.105; Real Sociedad B odds = 3.535; Albacete odds = 1.504
Method 1: Real Sociedad B = Albacete + -19.411; Real Sociedad B odds = 3.574; Albacete odds = 1.497
Method 2: Real Sociedad B = Albacete + -16.703; Real Sociedad B odds = 3.258; Albacete odds = 1.560
Method 1: Real Sociedad B = Albacete + -13.717; Real Sociedad B odds = 2.969; Albacete odds = 1.636
Method 2: Real Sociedad B = Albacete + -12.655; Real Sociedad B odds = 2.879; Albacete odds = 1.665
Method 1: Real Sociedad B = Albacete + -5.961; Real Sociedad B odds = 2.414; Albacete odds = 1.874
Method 2: Real Sociedad B = Albacete + -13.353; Real Sociedad B odds = 2.938; Albacete odds = 1.646
Method 1: Burgos CF = Eibar + -6.490; Burgos CF odds = 2.445; Eibar odds = 1.856
Method 2: Burgos CF = Eibar + -6.141; Burgos CF odds = 2.424; Eibar odds = 1.868
Method 1: Burgos CF = Eibar + 0.176; Burgos CF odds = 2.102; Eibar odds = 2.118
Method 2: Burgos CF = Eibar + -2.351; Burgos CF odds = 2.220; Eibar odds = 2.010
Method 1: Burgos CF = Eibar + -11.089; Burgos CF odds = 2.754; Eibar odds = 1.710
Method 2: Burgos CF = Eibar + -9.621; Burgos CF odds = 2.647; Eibar odds = 1.754
Method 1: Burgos CF = Eibar + -9.339; Burgos CF odds = 2.628; Eibar odds = 1.763
Method 2: Burgos CF = Eibar + -7.940; Burgos CF odds = 2.535; Eibar odds = 1.807 че за ерунда
Понял, это “ерунда” случилась по двум причинам:
Ты потерял “контекст” в терминале, потому что результаты Method 1/2 печатаются внутри simplify_with_sympy(), а вокруг (заголовки цепочек/матчи/лиги) стало печататься иначе — поэтому в консоли получилась простыня из одних строк Method ... без нормальных блоков.
Excel ты хочешь компактно, с рамками, но чтобы текст был внутри ячеек с переносами (wrap) и при этом каждая цепочка (и её расчёты) была визуально отделена. Самый компактный формат — 1 строка = 1 цепочка, а внутри одной строки в отдельных колонках: матчи (в одной ячейке с переносами), пороги, Method1, Method2.
Ниже — полный исправленный код:
wtp_results.xlsx, один лист Results, и он всегда дополняется снизуpythonimport os import re import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.utils import get_column_letter # ===================== НАСТРОЙКИ ===================== excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx" # Один и тот же файл результатов (в той же папке, где results.xlsx) # ===================================================== # Словарь со значениями пороговых коэффициентов для каждой лиги 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), "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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } # ===================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ===================== def normalize_dashes(text): if isinstance(text, str): return text.replace('–', '-').replace('—', '-').replace('−', '-') return text def clean_team_name(name): if isinstance(name, str): name = normalize_dashes(name) return ' '.join(name.strip().split()) return name def sanitize_pair_text(p): """ Убираем мусор, который иногда попадает при вставке из терминала: '... -> & C:\\Users\\...\\python.exe ...' """ if not isinstance(p, str): return p p = p.strip() p = normalize_dashes(p) p = re.sub(r"\s*->\s*&\s*[A-Za-z]:[\\/].*$", "", p) p = re.sub(r"\s*&\s*[A-Za-z]:[\\/].*$", "", p) return p.strip() def parse_date_to_dt(raw_date): """ Парсинг даты как у тебя. Возвращает datetime или None. """ if isinstance(raw_date, datetime): return raw_date if isinstance(raw_date, (float, int)): try: return datetime(1899, 12, 30) + timedelta(days=int(raw_date)) except: return None if isinstance(raw_date, str): try: if ' - ' in raw_date: date_part = raw_date.split(' - ')[0].strip() return datetime.strptime(date_part, '%d.%m.%Y') return datetime.strptime(raw_date.strip(), '%d.%m.%Y') except: return None return None # ===================== ЧТЕНИЕ EXCEL С МАТЧАМИ ===================== df = pd.read_excel(excel_file_path) matches = [] for _, row in df.iterrows(): match = ( row[0], # league row[1], # date clean_team_name(row[2]), # home clean_team_name(row[3]), # away row[4], # odds1 row[5], # odds_draw row[6] # odds2 ) 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 graph.setdefault(home_team, []) graph.setdefault(away_team, []) graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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 # ===================== ВВОД КОМАНД ===================== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Можно использовать '-', '–' или '—'.\n" "Ввод: " ).strip() raw_input_teams = sanitize_pair_text(raw_input_teams) team_pairs = [] pair_splitter = re.compile(r"\s+[-]\s+") has_any_dash = '-' in raw_input_teams if (',' in raw_input_teams) or has_any_dash: parts = [p.strip() for p in re.split(r"[,\n;]+", raw_input_teams) if p.strip()] for p in parts: p = sanitize_pair_text(p) if not p: continue teams = [t for t in pair_splitter.split(p) if t.strip()] if len(teams) != 2: print(f"Пропуск (неверный формат пары, нужен 'Команда1 - Команда2'): {p}") continue a = clean_team_name(teams[0]) b = clean_team_name(teams[1]) if a and b: team_pairs.append((a, b)) if not team_pairs: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===================== КАЛЬКУЛЯТОР СИЛ ===================== class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() def add_games(self, games): 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): 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 < threshold_home and away_odds > threshold_away: 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: pass def calculate_strength_difference_method_2(self): 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): """ ВАЖНО: тут БОЛЬШЕ НЕТ print(), чтобы в терминале не было “простыни”. Печать делаем снаружи — по цепочкам, красиво. """ if not self.games: 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: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) 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)) 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)) 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_symbol = team_symbols.get(end_team) final_results = [] warnings = [] # Method 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()) 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): warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 1.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 1.") else: warnings.append("Нет решения для метода 1.") # Method 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): warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 2.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 2.") else: warnings.append("Нет решения для метода 2.") return final_results, warnings # ===================== EXCEL (КОМПАКТНАЯ ТАБЛИЦА С РАМКАМИ) ===================== thin = Side(style="thin", color="000000") medium = Side(style="medium", color="000000") BORDER_THIN = Border(left=thin, right=thin, top=thin, bottom=thin) BORDER_MEDIUM_TOP = Border(left=thin, right=thin, top=medium, bottom=thin) ALIGN_WRAP_TOP = Alignment(wrap_text=True, vertical="top") ALIGN_CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) FONT_HEADER = Font(bold=True, size=11) FONT_BOLD = Font(bold=True) FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") FILL_PAIR = PatternFill("solid", fgColor="E2EFDA") def init_results_workbook(output_excel_path): if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) else: wb = Workbook() if "Results" in wb.sheetnames: ws = wb["Results"] else: ws = wb.active ws.title = "Results" # Если лист пустой — создаём шапку if ws.max_row == 1 and ws["A1"].value is None: headers = [ "RUN", "Request #", "Start", "End", "Chain #", "WTP Len", "Avg Date", "Matches (Date | Home-Away | 1/X/2)", "Leagues & Thresholds", "Method 1", "Method 2", "Warnings" ] ws.append(headers) for col in range(1, len(headers) + 1): c = ws.cell(row=1, column=col) c.font = FONT_HEADER c.fill = FILL_HEADER c.alignment = ALIGN_CENTER c.border = BORDER_THIN # ширины widths = [18, 9, 18, 18, 8, 8, 10, 60, 32, 38, 38, 30] for i, w in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(i)].width = w ws.freeze_panes = "A2" return wb, ws def append_row(ws, row_values, is_new_pair=False): ws.append(row_values) r = ws.max_row for col in range(1, len(row_values) + 1): cell = ws.cell(row=r, column=col) cell.alignment = ALIGN_WRAP_TOP cell.border = BORDER_MEDIUM_TOP if is_new_pair else BORDER_THIN if is_new_pair and col in (2, 3, 4): cell.fill = FILL_PAIR cell.font = FONT_BOLD # примерная высота строки (по числу строк в матчах) matches_text = str(row_values[7]) if row_values[7] is not None else "" line_count = matches_text.count("\n") + 1 ws.row_dimensions[r].height = min(max(18, 14 * line_count), 240) # ===================== MAIN: СЧЁТ + ПЕЧАТЬ В ТЕРМИНАЛ + ЗАПИСЬ В EXCEL ===================== run_dt = datetime.now() run_stamp = run_dt.strftime("%d.%m.%Y %H:%M:%S") out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") wb, ws = init_results_workbook(output_excel_path) for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): print(f"\n=== Запрос {request_num}: {start_team} -> {end_team} ===\n") 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 if not chains: print(f"Не найдены WTP-цепочки между указанными командами: {start_team} -> {end_team}\n") append_row( ws, [ run_stamp, request_num, start_team, end_team, "", "", "", "", "", "", "", "Не найдены WTP-цепочки" ], is_new_pair=True ) continue # Группируем цепочки по длине + сортируем по средней дате chain_groups = {} for chain in chains: chain_length = len(chain) chain_groups.setdefault(chain_length, []) dates = [] for match in chain: dt = parse_date_to_dt(match[1]) if dt: dates.append(dt) if dates: avg_timestamp = sum(d.timestamp() for d in dates) / len(dates) avg_date = datetime.fromtimestamp(avg_timestamp) else: avg_date = datetime.min chain_groups[chain_length].append({"chain": chain, "avg_date": avg_date}) for L in chain_groups: chain_groups[L] = sorted(chain_groups[L], key=lambda x: x["avg_date"], reverse=True) chain_index = 1 for chain_length in sorted(chain_groups.keys()): print(f"--- Цепочки из {chain_length} WTP ---\n") for chain_info in chain_groups[chain_length]: chain = chain_info["chain"] avg_date = chain_info["avg_date"] avg_date_str = avg_date.strftime("%d.%m.%Y") if avg_date != datetime.min else "" print(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") used_leagues = set() games = [] matches_lines_for_excel = [] for match in chain: league, date, home_team, away_team, odds1, odds_draw, odds2 = match used_leagues.add(league) games.append(match) # вывод в терминал (как раньше) print(f"('{date}', '{home_team}', '{away_team}', {odds1}, {odds_draw}, {odds2})") # компактно для excel matches_lines_for_excel.append( f"{date} | {home_team}-{away_team} | {odds1}/{odds_draw}/{odds2}" ) print("\nИспользуемые лиги в цепочке и их пороги:") thresholds_lines = [] for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) print(f" Лига: {lg} → home={th_home}, away={th_away}") thresholds_lines.append(f"{lg}: home={th_home}, away={th_away}") calculator = TeamStrengthCalculator() calculator.add_games(games) calculator.calculate_strength_difference_method_1() calculator.calculate_strength_difference_method_2() final_results, warnings = calculator.simplify_with_sympy(start_team, end_team) # печать результатов в терминал красиво и по месту if final_results: for r in final_results: print(r) else: print("Не удалось вычислить разницу в силе между командами.") if warnings: for w in warnings: print(w) print("\n" + "=" * 75 + "\n") # в excel: Method1/Method2 разнесём по колонкам method1 = "" method2 = "" for r in final_results: if r.startswith("Method 1:"): method1 = r elif r.startswith("Method 2:"): method2 = r append_row( ws, [ run_stamp, request_num, start_team, end_team, chain_index, chain_length, avg_date_str, "\n".join(matches_lines_for_excel), "\n".join(thresholds_lines), method1, method2, "\n".join(warnings) if warnings else "" ], is_new_pair=(chain_index == 1) # толстая верхняя рамка на первой цепочке пары ) chain_index += 1 wb.save(output_excel_path) print(f"\nГотово. Результаты компактно дописаны в Excel (с рамками):\n{output_excel_path}")
Если хочешь ещё компактнее (вообще супер-таблично):
Скажи, какой вариант компактности тебе нужен:
нужно оставить столбцы только те которые на фото которое я отправил, я удалил ненужные столбцы, и вот здесь к примеру:
16.12.2025 - 04:30 | Castellon-Mirandes | 1.35/5.28/9.2
04.01.2026 - 00:15 | Castellon-Huesca | 1.48/4.31/7.75
30.11.2025 - 02:30 | Almeria-Huesca | 1.71/3.67/5.53 а здесь мне не нравяится эти вертикальные палочки, и нужны скобыки, должно быть так:
16.12.2025 - 04:30 | Castellon-Mirandes (1.35, 5.28, 9.2)
04.01.2026 - 00:15 | Castellon-Huesca (1.48, 4.31, 7.75)
30.11.2025 - 02:30 | Almeria-Huesca (1.71, 3.67, 5.53) чтобы еще коэффициенты были в друг под другом без сдвигов
Ок — делаю ровно как ты просишь:
RUN, Request #, Start, End, Chain #, WTP Len, Avg Date, Matches..., Method 2Matches убираю лишние | и делаю скобки:дата | Home-Away (1, X, 2)Matches ставлю моноширинный шрифт (Consolas)Ниже полный код (замени свой целиком):
pythonimport os import re import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.utils import get_column_letter # ===================== НАСТРОЙКИ ===================== excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx" # ===================================================== 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), "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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } # ===================== helpers ===================== def normalize_dashes(text): if isinstance(text, str): return text.replace('–', '-').replace('—', '-').replace('−', '-') return text def clean_team_name(name): if isinstance(name, str): name = normalize_dashes(name) return ' '.join(name.strip().split()) return name def sanitize_pair_text(p): if not isinstance(p, str): return p p = p.strip() p = normalize_dashes(p) p = re.sub(r"\s*->\s*&\s*[A-Za-z]:[\\/].*$", "", p) p = re.sub(r"\s*&\s*[A-Za-z]:[\\/].*$", "", p) return p.strip() def parse_date_to_dt(raw_date): if isinstance(raw_date, datetime): return raw_date if isinstance(raw_date, (float, int)): try: return datetime(1899, 12, 30) + timedelta(days=int(raw_date)) except: return None if isinstance(raw_date, str): try: if ' - ' in raw_date: date_part = raw_date.split(' - ')[0].strip() return datetime.strptime(date_part, '%d.%m.%Y') return datetime.strptime(raw_date.strip(), '%d.%m.%Y') except: return None return None def fmt_num(x) -> str: """Красивое число без лишних нулей: 9.200 -> 9.2, 4.030 -> 4.03""" try: f = float(str(x).replace(',', '.')) s = f"{f:.3f}".rstrip('0').rstrip('.') return s except: return str(x) def pad_num(s: str, width: int = 6) -> str: """Чтобы 1, X, 2 были строго друг под другом (в моноширинном шрифте).""" return str(s).rjust(width) # ===================== read matches ===================== df = pd.read_excel(excel_file_path) matches = [] for _, row in df.iterrows(): match = ( row[0], # league row[1], # date clean_team_name(row[2]), # home clean_team_name(row[3]), # away row[4], # odds1 row[5], # odds_draw row[6] # odds2 ) matches.append(match) # ===================== chains ===================== 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 graph.setdefault(home_team, []) graph.setdefault(away_team, []) graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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 # ===================== input pairs ===================== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Можно использовать '-', '–' или '—'.\n" "Ввод: " ).strip() raw_input_teams = sanitize_pair_text(raw_input_teams) team_pairs = [] pair_splitter = re.compile(r"\s+[-]\s+") has_any_dash = '-' in raw_input_teams if (',' in raw_input_teams) or has_any_dash: parts = [p.strip() for p in re.split(r"[,\n;]+", raw_input_teams) if p.strip()] for p in parts: p = sanitize_pair_text(p) if not p: continue teams = [t for t in pair_splitter.split(p) if t.strip()] if len(teams) != 2: print(f"Пропуск (неверный формат пары, нужен 'Команда1 - Команда2'): {p}") continue a = clean_team_name(teams[0]) b = clean_team_name(teams[1]) if a and b: team_pairs.append((a, b)) if not team_pairs: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===================== calculator ===================== class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() def add_games(self, games): 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): 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 < threshold_home and away_odds > threshold_away: 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}" ) def calculate_strength_difference_method_2(self): 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): """ Возвращаем строки результатов и warnings. Никаких print() здесь — печатаем снаружи красиво по блокам. """ if not self.games: 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: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) 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)) 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)) 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_symbol = team_symbols.get(end_team) final_results = [] warnings = [] # Method 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()) 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): warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 1.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 1.") else: warnings.append("Нет решения для метода 1.") # Method 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): warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 2.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 2.") else: warnings.append("Нет решения для метода 2.") return final_results, warnings # ===================== Excel (ТОЛЬКО КОЛОНКИ КАК НА СКРИНЕ) ===================== thin = Side(style="thin", color="000000") medium = Side(style="medium", color="000000") BORDER_THIN = Border(left=thin, right=thin, top=thin, bottom=thin) BORDER_MEDIUM_TOP = Border(left=thin, right=thin, top=medium, bottom=thin) ALIGN_WRAP_TOP = Alignment(wrap_text=True, vertical="top") ALIGN_CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) FONT_HEADER = Font(bold=True, size=11) FONT_BOLD = Font(bold=True) FONT_MONO = Font(name="Consolas") # для совпадения пробелов FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") FILL_PAIR = PatternFill("solid", fgColor="E2EFDA") def init_results_workbook(output_excel_path): if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) else: wb = Workbook() if "Results" in wb.sheetnames: ws = wb["Results"] else: ws = wb.active ws.title = "Results" # если лист пустой — создаём шапку ТОЛЬКО под 9 колонок (как на скрине) if ws.max_row == 1 and ws["A1"].value is None: headers = [ "RUN", # A "Request #", # B "Start", # C "End", # D "Chain #", # E "WTP Len", # F "Avg Date", # G "Matches (Date | Home-Away (1,X,2))", # H "Method 2" # I ] ws.append(headers) for col in range(1, len(headers) + 1): c = ws.cell(row=1, column=col) c.font = FONT_HEADER c.fill = FILL_HEADER c.alignment = ALIGN_CENTER c.border = BORDER_THIN widths = [18, 9, 18, 18, 8, 8, 11, 65, 55] for i, w in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(i)].width = w ws.freeze_panes = "A2" return wb, ws def append_row(ws, row_values, is_new_pair=False): ws.append(row_values) r = ws.max_row for col in range(1, len(row_values) + 1): cell = ws.cell(row=r, column=col) cell.alignment = ALIGN_WRAP_TOP cell.border = BORDER_MEDIUM_TOP if is_new_pair else BORDER_THIN # подсветим Start/End на первой строке пары if is_new_pair and col in (2, 3, 4): cell.fill = FILL_PAIR cell.font = FONT_BOLD # моноширинный шрифт только для Matches (H) if col == 8: cell.font = FONT_MONO # высота строки по числу строк в Matches matches_text = str(row_values[7]) if row_values[7] is not None else "" line_count = matches_text.count("\n") + 1 ws.row_dimensions[r].height = min(max(18, 14 * line_count), 260) # ===================== MAIN ===================== run_dt = datetime.now() run_stamp = run_dt.strftime("%d.%m.%Y %H:%M:%S") out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") wb, ws = init_results_workbook(output_excel_path) for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): print(f"\n=== Запрос {request_num}: {start_team} -> {end_team} ===\n") chains = find_chains(matches, start_team, end_team) # убрать дубликаты unique_chains = [] seen = set() for ch in chains: t = tuple(ch) if t not in seen: seen.add(t) unique_chains.append(ch) chains = unique_chains if not chains: print(f"Не найдены WTP-цепочки между указанными командами: {start_team} -> {end_team}\n") append_row( ws, [run_stamp, request_num, start_team, end_team, "", "", "", "", ""], is_new_pair=True ) continue # group + sort by avg date desc groups = {} for chain in chains: L = len(chain) groups.setdefault(L, []) dts = [parse_date_to_dt(m[1]) for m in chain] dts = [d for d in dts if d] avg_date = datetime.fromtimestamp(sum(d.timestamp() for d in dts) / len(dts)) if dts else datetime.min groups[L].append({"chain": chain, "avg_date": avg_date}) for L in groups: groups[L] = sorted(groups[L], key=lambda x: x["avg_date"], reverse=True) chain_index = 1 for L in sorted(groups.keys()): print(f"--- Цепочки из {L} WTP ---\n") for info in groups[L]: chain = info["chain"] avg_date = info["avg_date"] avg_date_str = avg_date.strftime("%d.%m.%Y") if avg_date != datetime.min else "" print(f"Цепочка {chain_index} (Средняя дата: {avg_date_str}):") used_leagues = set() games = [] # сначала соберём базовые строки "date | home-away" base_lines = [] odds_triplets = [] for match in chain: league, date, home, away, o1, ox, o2 = match used_leagues.add(league) games.append(match) print(f"('{date}', '{home}', '{away}', {o1}, {ox}, {o2})") base = f"{date} | {home}-{away}" base_lines.append(base) s1 = pad_num(fmt_num(o1), 6) sx = pad_num(fmt_num(ox), 6) s2 = pad_num(fmt_num(o2), 6) odds_triplets.append((s1, sx, s2)) # чтобы скобки начинались строго в одной колонке: выравниваем по max длине base max_base_len = max(len(b) for b in base_lines) if base_lines else 0 matches_lines_for_excel = [] for base, (s1, sx, s2) in zip(base_lines, odds_triplets): pad_spaces = " " * (max_base_len - len(base) + 2) # итог: дата | home-away ( 1.35, 5.28, 9.2) matches_lines_for_excel.append(f"{base}{pad_spaces}({s1}, {sx}, {s2})") print("\nИспользуемые лиги в цепочке и их пороги:") for lg in sorted(used_leagues): th_home, th_away = league_thresholds.get(lg, (2.40, 3.00)) print(f" Лига: {lg} → home={th_home}, away={th_away}") calc = TeamStrengthCalculator() calc.add_games(games) calc.calculate_strength_difference_method_1() calc.calculate_strength_difference_method_2() results, warnings = calc.simplify_with_sympy(start_team, end_team) # терминал: аккуратно, как раньше if results: for r in results: print(r) else: print("Не удалось вычислить разницу в силе между командами.") if warnings: for w in warnings: print(w) print("\n" + "=" * 75 + "\n") # excel: сохраняем ТОЛЬКО Method 2 method2 = "" for r in results: if r.startswith("Method 2:"): method2 = r break append_row( ws, [ run_stamp, # A RUN request_num, # B start_team, # C end_team, # D chain_index, # E L, # F avg_date_str, # G "\n".join(matches_lines_for_excel), # H method2 # I ], is_new_pair=(chain_index == 1) # жирная верхняя граница на первой цепочке пары ) chain_index += 1 wb.save(output_excel_path) print(f"\nГотово. Результаты дописаны в Excel:\n{output_excel_path}")
Если хочешь, могу сделать ещё лучше визуально: чтобы внутри ячейки Matches между матчами была пустая строка или чтобы каждую цепочку отделять толстой рамкой по всей строке (не только верхней линией).
09.12.2025 - 02:30 | Las Palmas-Mirandes ( 1.56, 4.03, 6.63)
15.12.2025 - 05:00 | AD Ceuta-Las Palmas ( 2.88, 2.92, 2.91)
27.11.2025 - 03:00 | AD Ceuta-Almeria ( 3.34, 3.19, 2.38) почему после скобки пробелы??? убери их, пуусть будет впритык к числу, и если например коэффициент 2, то пусть будет 2.00, то есть каждый коэффициент с точностью до сотых, и убери столбики chain и wtp len, вместо них пусть будет порог использованный, здесь это например 2.11 и 4.02 и так же убери надпись method 2 в резульатах
Понял. Исправляю всё, что ты перечислил:
( → будет (1.56, 4.03, 6.63)2.00Chain # и WTP LenTh home и Th away (например 2.11 и 4.02)Method 2: из результата (в Excel будет просто Mirandes = Almeria + ...)Вот полный исправленный код (замени целиком):
pythonimport os import re import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.utils import get_column_letter # ===================== НАСТРОЙКИ ===================== excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx" # ===================================================== 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), "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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60) } # ===================== helpers ===================== def normalize_dashes(text): if isinstance(text, str): return text.replace('–', '-').replace('—', '-').replace('−', '-') return text def clean_team_name(name): if isinstance(name, str): name = normalize_dashes(name) return ' '.join(name.strip().split()) return name def sanitize_pair_text(p): if not isinstance(p, str): return p p = p.strip() p = normalize_dashes(p) p = re.sub(r"\s*->\s*&\s*[A-Za-z]:[\\/].*$", "", p) p = re.sub(r"\s*&\s*[A-Za-z]:[\\/].*$", "", p) return p.strip() def parse_date_to_dt(raw_date): if isinstance(raw_date, datetime): return raw_date if isinstance(raw_date, (float, int)): try: return datetime(1899, 12, 30) + timedelta(days=int(raw_date)) except: return None if isinstance(raw_date, str): try: if ' - ' in raw_date: date_part = raw_date.split(' - ')[0].strip() return datetime.strptime(date_part, '%d.%m.%Y') return datetime.strptime(raw_date.strip(), '%d.%m.%Y') except: return None return None def to_float(x): return float(str(x).replace(',', '.')) def fmt2(x) -> str: """Всегда 2 знака после точки: 2 -> 2.00""" try: return f"{to_float(x):.2f}" except: return str(x) # ===================== read matches ===================== df = pd.read_excel(excel_file_path) matches = [] for _, row in df.iterrows(): match = ( row[0], # league row[1], # date clean_team_name(row[2]), # home clean_team_name(row[3]), # away row[4], # odds1 row[5], # odds_draw row[6] # odds2 ) matches.append(match) # ===================== chains ===================== 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 graph.setdefault(home_team, []) graph.setdefault(away_team, []) graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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 # ===================== input pairs ===================== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Можно использовать '-', '–' или '—'.\n" "Ввод: " ).strip() raw_input_teams = sanitize_pair_text(raw_input_teams) team_pairs = [] pair_splitter = re.compile(r"\s+[-]\s+") has_any_dash = '-' in raw_input_teams if (',' in raw_input_teams) or has_any_dash: parts = [p.strip() for p in re.split(r"[,\n;]+", raw_input_teams) if p.strip()] for p in parts: p = sanitize_pair_text(p) if not p: continue teams = [t for t in pair_splitter.split(p) if t.strip()] if len(teams) != 2: print(f"Пропуск (неверный формат пары, нужен 'Команда1 - Команда2'): {p}") continue a = clean_team_name(teams[0]) b = clean_team_name(teams[1]) if a and b: team_pairs.append((a, b)) if not team_pairs: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] else: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===================== calculator ===================== class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() def add_games(self, games): 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': to_float(home_odds), 'draw_odds': to_float(draw_odds), 'away_odds': to_float(away_odds) }) self.teams.add(clean_team_name(home_team)) self.teams.add(clean_team_name(away_team)) def calculate_strength_difference_method_2(self): 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_method2(self, start_team, end_team): """ Возвращает: - method2_str (без 'Method 2:') - warnings - used thresholds (home, away) """ if not self.games: 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: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) sympy_equations = [] 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.append(Eq(left_sym, right_sym + strength_diff)) symbols_list = [sym for sym in team_symbols.values() if sym != 0] solution = solve(sympy_equations, symbols_list, dict=True) if sympy_equations else [] warnings = [] end_symbol = team_symbols.get(end_team) if not solution or end_symbol is None: return "", ["Нет решения для метода 2."], (f"{threshold_home:.2f}", f"{threshold_away:.2f}") sol = solution[0] if end_symbol not in sol: return "", [f"Команда {end_team} не найдена в решении метода 2."], (f"{threshold_home:.2f}", f"{threshold_away:.2f}") try: end_value_numeric = float(sol[end_symbol].evalf()) except Exception: return "", [f"Не удалось вычислить численное значение силы {end_team} в методе 2."], (f"{threshold_home:.2f}", f"{threshold_away:.2f}") strength_diff = -end_value_numeric new_odds_start = 100 / ((100 / threshold_home) + strength_diff) new_odds_end = 100 / ((100 / threshold_home) - strength_diff) # ВАЖНО: без "Method 2:" method2_str = ( f"{start_team} = {end_team} + {strength_diff:.3f}; " f"{start_team} odds = {new_odds_start:.3f}; {end_team} odds = {new_odds_end:.3f}" ) return method2_str, warnings, (f"{threshold_home:.2f}", f"{threshold_away:.2f}") # ===================== Excel (колонки как на фото, но вместо Chain/WTP = thresholds) ===================== thin = Side(style="thin", color="000000") medium = Side(style="medium", color="000000") BORDER_THIN = Border(left=thin, right=thin, top=thin, bottom=thin) BORDER_MEDIUM_TOP = Border(left=thin, right=thin, top=medium, bottom=thin) ALIGN_WRAP_TOP = Alignment(wrap_text=True, vertical="top") ALIGN_CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) FONT_HEADER = Font(bold=True, size=11) FONT_BOLD = Font(bold=True) FONT_MONO = Font(name="Consolas") FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") FILL_PAIR = PatternFill("solid", fgColor="E2EFDA") EXPECTED_HEADERS = [ "RUN", # A "Request #", # B "Start", # C "End", # D "Th home", # E (вместо Chain #) "Th away", # F (вместо WTP Len) "Avg Date", # G "Matches (Date | Home-Away (1,X,2))", # H "Result" # I (вместо Method 2) ] def init_results_workbook(output_excel_path): if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) else: wb = Workbook() # если Results уже есть — используем, но проверим шапку if "Results" in wb.sheetnames: ws = wb["Results"] else: ws = wb.active ws.title = "Results" # если пустой — пишем шапку if ws.max_row == 1 and ws["A1"].value is None: ws.append(EXPECTED_HEADERS) else: # если шапка не совпадает — создадим новый лист, чтобы не ломать старые данные current = [ws.cell(row=1, column=i).value for i in range(1, len(EXPECTED_HEADERS) + 1)] if current != EXPECTED_HEADERS: base = "Results_v2" name = base i = 2 while name in wb.sheetnames: name = f"{base}_{i}" i += 1 ws = wb.create_sheet(name) ws.append(EXPECTED_HEADERS) # стиль шапки for col in range(1, len(EXPECTED_HEADERS) + 1): c = ws.cell(row=1, column=col) c.font = FONT_HEADER c.fill = FILL_HEADER c.alignment = ALIGN_CENTER c.border = BORDER_THIN # ширины (под твой скрин + thresholds) widths = [18, 9, 18, 18, 9, 9, 11, 70, 55] for i, w in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(i)].width = w ws.freeze_panes = "A2" return wb, ws def append_row(ws, row_values, is_new_pair=False): ws.append(row_values) r = ws.max_row for col in range(1, len(row_values) + 1): cell = ws.cell(row=r, column=col) cell.alignment = ALIGN_WRAP_TOP cell.border = BORDER_MEDIUM_TOP if is_new_pair else BORDER_THIN # подсветим Start/End/Request на первой строке пары if is_new_pair and col in (2, 3, 4): cell.fill = FILL_PAIR cell.font = FONT_BOLD # Matches моноширинный if col == 8: cell.font = FONT_MONO # высота строки по числу строк в Matches matches_text = str(row_values[7]) if row_values[7] else "" line_count = matches_text.count("\n") + 1 ws.row_dimensions[r].height = min(max(18, 14 * line_count), 260) # ===================== MAIN ===================== run_dt = datetime.now() run_stamp = run_dt.strftime("%d.%m.%Y %H:%M:%S") out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") wb, ws = init_results_workbook(output_excel_path) for request_num, (start_team, end_team) in enumerate(team_pairs, start=1): chains = find_chains(matches, start_team, end_team) # убрать дубликаты unique_chains = [] seen = set() for ch in chains: t = tuple(ch) if t not in seen: seen.add(t) unique_chains.append(ch) chains = unique_chains if not chains: append_row( ws, [run_stamp, request_num, start_team, end_team, "", "", "", "", ""], is_new_pair=True ) continue # группировка по длине, сортировка по средней дате (свежие сверху) groups = {} for chain in chains: L = len(chain) groups.setdefault(L, []) dts = [parse_date_to_dt(m[1]) for m in chain] dts = [d for d in dts if d] avg_date = datetime.fromtimestamp(sum(d.timestamp() for d in dts) / len(dts)) if dts else datetime.min groups[L].append({"chain": chain, "avg_date": avg_date}) for L in groups: groups[L] = sorted(groups[L], key=lambda x: x["avg_date"], reverse=True) first_row_for_pair = True for L in sorted(groups.keys()): for info in groups[L]: chain = info["chain"] avg_date = info["avg_date"] avg_date_str = avg_date.strftime("%d.%m.%Y") if avg_date != datetime.min else "" games = [] base_lines = [] used_league_for_threshold = None # 1) собираем base строки "date | home-away" for match in chain: league, date, home, away, o1, ox, o2 = match games.append(match) if used_league_for_threshold is None: used_league_for_threshold = league base_lines.append(f"{date} | {home}-{away}") # 2) выравниваем начало скобок (по max длине base), НО внутри скобок БЕЗ padding max_base_len = max(len(b) for b in base_lines) if base_lines else 0 matches_lines_for_excel = [] for base, match in zip(base_lines, chain): _, date, home, away, o1, ox, o2 = match pad_spaces = " " * (max_base_len - len(base) + 2) # ВАЖНО: после '(' НЕТ пробелов, коэффициенты всегда .2f matches_lines_for_excel.append( f"{base}{pad_spaces}({fmt2(o1)}, {fmt2(ox)}, {fmt2(o2)})" ) # thresholds (home/away) по первой лиге цепочки (как используется в формулах) th_home, th_away = league_thresholds.get(used_league_for_threshold, (2.40, 3.00)) th_home_str = f"{th_home:.2f}" th_away_str = f"{th_away:.2f}" # calc method2 calc = TeamStrengthCalculator() calc.add_games(games) calc.calculate_strength_difference_method_2() result2, warnings, _ths = calc.simplify_with_sympy_method2(start_team, end_team) append_row( ws, [ run_stamp, # RUN request_num, # Request # start_team, # Start end_team, # End th_home_str, # Th home th_away_str, # Th away avg_date_str, # Avg Date "\n".join(matches_lines_for_excel), # Matches result2 # Result (без "Method 2:") ], is_new_pair=first_row_for_pair ) first_row_for_pair = False wb.save(output_excel_path) print(f"\nГотово. Результаты дописаны в Excel:\n{output_excel_path}")
Если хочешь — могу ещё сделать так, чтобы в Matches между матчами была пустая строка (ещё читаемее), или чтобы переносы были “по красоте” без огромной высоты строк.
pythonfrom PIL import Image img = Image.open('/mnt/data/image.png') img.size (2661, 3743)
pythonimport matplotlib.pyplot as plt, numpy as np plt.figure(figsize=(10,14)) plt.imshow(img.crop((0, 2500, 2661, 3400))) plt.axis('off') plt.show()
pythonimport matplotlib.pyplot as plt, numpy as np plt.figure(figsize=(10,14)) plt.imshow(img.crop((0, 2500, 2661, 3400))) plt.axis('off') plt.show() <<ImageDisplayed>>
Причина разная для этих двух пар:
У тебя в исходном results.xlsx не находится ни одной WTP-цепочки между этими командами (или команда в файле называется иначе — например FC Andorra, а не Andorra). Тогда расчёта быть не может → строка получается пустой.
Чаще всего это из-за несовпадения названий: в данных команда может быть AD Ceuta, а ты вводишь Ceuta. Для графа это разные узлы, поэтому цепочка “не существует” → расчёта нет.
Чтобы это исправить нормально, я добавил автосопоставление названий (например Ceuta → AD Ceuta, Andorra → FC Andorra, если такие есть в файле), и чтобы в Excel было видно, почему пусто — в колонку Result будет писаться NO CHAINS или NO SOLUTION, а не пустота.
Ниже полный код (оставляет только нужные колонки; в матчах: скобки без пробелов после (, коэффициенты строго 0.00; убраны Chain/WTP, вместо них Th home / Th away; в Result убран префикс Method 2:):
pythonimport os import re import difflib import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.utils import get_column_letter # ===================== НАСТРОЙКИ ===================== excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx" # ===================================================== league_thresholds = { "Spain Segunda": (2.11, 4.02), # ... оставь свой полный словарь как у тебя (я не удаляю) # ВАЖНО: сюда вставь весь твой league_thresholds без изменений. } # ===================== helpers ===================== def normalize_dashes(text): if isinstance(text, str): return text.replace('–', '-').replace('—', '-').replace('−', '-') return text def clean_team_name(name): if isinstance(name, str): name = normalize_dashes(name) return ' '.join(name.strip().split()) return name def sanitize_pair_text(p): if not isinstance(p, str): return p p = p.strip() p = normalize_dashes(p) # мусор из терминала p = re.sub(r"\s*->\s*&\s*[A-Za-z]:[\\/].*$", "", p) p = re.sub(r"\s*&\s*[A-Za-z]:[\\/].*$", "", p) return p.strip() def parse_date_to_dt(raw_date): if isinstance(raw_date, datetime): return raw_date if isinstance(raw_date, (float, int)): try: return datetime(1899, 12, 30) + timedelta(days=int(raw_date)) except: return None if isinstance(raw_date, str): try: if ' - ' in raw_date: date_part = raw_date.split(' - ')[0].strip() return datetime.strptime(date_part, '%d.%m.%Y') return datetime.strptime(raw_date.strip(), '%d.%m.%Y') except: return None return None def to_float(x): return float(str(x).replace(',', '.')) def fmt2(x) -> str: """Всегда 2 знака после точки: 2 -> 2.00""" try: return f"{to_float(x):.2f}" except: return str(x) # ---------- автосопоставление команд ---------- COMMON_TOKENS = {"fc", "cf", "cd", "ad", "ud", "sd", "de", "la", "el"} def team_key(name: str) -> str: s = re.sub(r"[^\w\s]", " ", name.lower()) parts = [p for p in s.split() if p and p not in COMMON_TOKENS] return " ".join(parts) def build_team_index(all_teams): idx = {} for t in all_teams: k = team_key(t) idx.setdefault(k, []).append(t) return idx def resolve_team(input_team: str, all_teams, team_index): """ Возвращает (resolved_name, note). note пустой если exact match. """ it = clean_team_name(input_team) if it in all_teams: return it, "" k = team_key(it) if k in team_index and len(team_index[k]) == 1: return team_index[k][0], f"auto: '{it}' -> '{team_index[k][0]}'" # fallback: похожие candidates = difflib.get_close_matches(it, list(all_teams), n=1, cutoff=0.65) if candidates: return candidates[0], f"fuzzy: '{it}' -> '{candidates[0]}'" return it, f"NOT FOUND: '{it}'" # ===================== read matches ===================== df = pd.read_excel(excel_file_path) matches = [] all_teams = set() for _, row in df.iterrows(): league = row[0] date = row[1] home = clean_team_name(row[2]) away = clean_team_name(row[3]) o1 = row[4] ox = row[5] o2 = row[6] matches.append((league, date, home, away, o1, ox, o2)) all_teams.add(home) all_teams.add(away) team_index = build_team_index(all_teams) # ===================== chains ===================== 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 graph.setdefault(home_team, []) graph.setdefault(away_team, []) graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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 # ===================== input pairs ===================== raw_input_teams = input( "Введите либо 2 команды (как раньше), либо список пар через запятую.\n" "Пример: Malaga - Granada, Granada - Albacete\n" "Можно использовать '-', '–' или '—'.\n" "Ввод: " ).strip() raw_input_teams = sanitize_pair_text(raw_input_teams) team_pairs = [] pair_splitter = re.compile(r"\s+[-]\s+") has_any_dash = '-' in raw_input_teams if (',' in raw_input_teams) or has_any_dash: parts = [p.strip() for p in re.split(r"[,\n;]+", raw_input_teams) if p.strip()] for p in parts: p = sanitize_pair_text(p) if not p: continue teams = [t for t in pair_splitter.split(p) if t.strip()] if len(teams) != 2: print(f"Пропуск (неверный формат пары, нужен 'Команда1 - Команда2'): {p}") continue a = clean_team_name(teams[0]) b = clean_team_name(teams[1]) if a and b: team_pairs.append((a, b)) else: start_team = clean_team_name(raw_input_teams) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===================== calculator (ТОЛЬКО Method 2) ===================== class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_2 = [] self.teams = set() def add_games(self, games): 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': to_float(home_odds), 'draw_odds': to_float(draw_odds), 'away_odds': to_float(away_odds) }) self.teams.add(clean_team_name(home_team)) self.teams.add(clean_team_name(away_team)) def calculate_strength_difference_method_2(self): 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 solve_method2(self, start_team, end_team): if not self.games: return "", "NO GAMES", ("", "") first_league = self.games[0]['league'] threshold_home, threshold_away = league_thresholds.get(first_league, (2.40, 3.00)) th_home_str, th_away_str = f"{threshold_home:.2f}", f"{threshold_away:.2f}" team_symbols = {} for team in self.teams: if team == start_team: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) sympy_equations = [] 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()) sympy_equations.append(Eq(team_symbols[left_team], team_symbols[right_team] + strength_diff)) if not sympy_equations: return "", "NO EQUATIONS", (th_home_str, th_away_str) symbols_list = [sym for sym in team_symbols.values() if sym != 0] solution = solve(sympy_equations, symbols_list, dict=True) end_symbol = team_symbols.get(end_team) if not solution or end_symbol is None: return "", "NO SOLUTION", (th_home_str, th_away_str) sol = solution[0] if end_symbol not in sol: return "", "NO SOLUTION", (th_home_str, th_away_str) try: end_value_numeric = float(sol[end_symbol].evalf()) except Exception: return "", "NO SOLUTION", (th_home_str, th_away_str) strength_diff = -end_value_numeric new_odds_start = 100 / ((100 / threshold_home) + strength_diff) new_odds_end = 100 / ((100 / threshold_home) - strength_diff) # БЕЗ "Method 2:" result = ( f"{start_team} = {end_team} + {strength_diff:.3f}; " f"{start_team} odds = {new_odds_start:.3f}; {end_team} odds = {new_odds_end:.3f}" ) return result, "", (th_home_str, th_away_str) # ===================== Excel (колонки как ты просил) ===================== thin = Side(style="thin", color="000000") medium = Side(style="medium", color="000000") BORDER_THIN = Border(left=thin, right=thin, top=thin, bottom=thin) BORDER_MEDIUM_TOP = Border(left=thin, right=thin, top=medium, bottom=thin) ALIGN_WRAP_TOP = Alignment(wrap_text=True, vertical="top") ALIGN_CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) FONT_HEADER = Font(bold=True, size=11) FONT_BOLD = Font(bold=True) FONT_MONO = Font(name="Consolas") FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") FILL_PAIR = PatternFill("solid", fgColor="E2EFDA") HEADERS = [ "RUN", "Request #", "Start", "End", "Th home", "Th away", "Avg Date", "Matches (Date | Home-Away (1,X,2))", "Result" ] def init_results_workbook(output_excel_path): if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) else: wb = Workbook() if "Results" in wb.sheetnames: ws = wb["Results"] else: ws = wb.active ws.title = "Results" if ws.max_row == 1 and ws["A1"].value is None: ws.append(HEADERS) # привести шапку к нужной (если не совпала — создаём новый лист) current = [ws.cell(row=1, column=i).value for i in range(1, len(HEADERS) + 1)] if current != HEADERS: base = "Results_v3" name = base i = 2 while name in wb.sheetnames: name = f"{base}_{i}" i += 1 ws = wb.create_sheet(name) ws.append(HEADERS) for col in range(1, len(HEADERS) + 1): c = ws.cell(row=1, column=col) c.font = FONT_HEADER c.fill = FILL_HEADER c.alignment = ALIGN_CENTER c.border = BORDER_THIN widths = [18, 9, 18, 18, 9, 9, 11, 70, 55] for i, w in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(i)].width = w ws.freeze_panes = "A2" return wb, ws def append_row(ws, row_values, is_new_pair=False): ws.append(row_values) r = ws.max_row for col in range(1, len(row_values) + 1): cell = ws.cell(row=r, column=col) cell.alignment = ALIGN_WRAP_TOP cell.border = BORDER_MEDIUM_TOP if is_new_pair else BORDER_THIN if is_new_pair and col in (2, 3, 4): cell.fill = FILL_PAIR cell.font = FONT_BOLD if col == 8: cell.font = FONT_MONO matches_text = str(row_values[7]) if row_values[7] else "" line_count = matches_text.count("\n") + 1 ws.row_dimensions[r].height = min(max(18, 14 * line_count), 260) # ===================== MAIN ===================== run_dt = datetime.now() run_stamp = run_dt.strftime("%d.%m.%Y %H:%M:%S") out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") wb, ws = init_results_workbook(output_excel_path) for request_num, (start_in, end_in) in enumerate(team_pairs, start=1): start_team, note1 = resolve_team(start_in, all_teams, team_index) end_team, note2 = resolve_team(end_in, all_teams, team_index) if note1: print(f"[INFO] {note1}") if note2: print(f"[INFO] {note2}") chains = find_chains(matches, start_team, end_team) # убрать дубликаты unique_chains = [] seen = set() for ch in chains: t = tuple(ch) if t not in seen: seen.add(t) unique_chains.append(ch) chains = unique_chains if not chains: append_row( ws, [run_stamp, request_num, start_team, end_team, "", "", "", "", "NO CHAINS"], is_new_pair=True ) continue # сортировка цепочек по средней дате (свежие сверху) chain_infos = [] for chain in chains: dts = [parse_date_to_dt(m[1]) for m in chain] dts = [d for d in dts if d] avg_date = datetime.fromtimestamp(sum(d.timestamp() for d in dts) / len(dts)) if dts else datetime.min chain_infos.append({"chain": chain, "avg_date": avg_date}) chain_infos.sort(key=lambda x: x["avg_date"], reverse=True) first_row_for_pair = True for info in chain_infos: chain = info["chain"] avg_date = info["avg_date"] avg_date_str = avg_date.strftime("%d.%m.%Y") if avg_date != datetime.min else "" # строки матчей base_lines = [] for league, date, home, away, o1, ox, o2 in chain: base_lines.append(f"{date} | {home}-{away}") max_base_len = max(len(b) for b in base_lines) if base_lines else 0 matches_lines_for_excel = [] for base, (league, date, home, away, o1, ox, o2) in zip(base_lines, chain): pad_spaces = " " * (max_base_len - len(base) + 2) # чтобы скобки ровно начинались matches_lines_for_excel.append( f"{base}{pad_spaces}({fmt2(o1)}, {fmt2(ox)}, {fmt2(o2)})" # БЕЗ пробелов после '(' ) # пороги как в расчёте (по первой игре цепочки) first_league = chain[0][0] th_home, th_away = league_thresholds.get(first_league, (2.40, 3.00)) th_home_str, th_away_str = f"{th_home:.2f}", f"{th_away:.2f}" # расчёт calc = TeamStrengthCalculator() calc.add_games(chain) calc.calculate_strength_difference_method_2() result, fail_reason, _ths = calc.solve_method2(start_team, end_team) if not result: result = fail_reason or "NO SOLUTION" append_row( ws, [ run_stamp, request_num, start_team, end_team, th_home_str, th_away_str, avg_date_str, "\n".join(matches_lines_for_excel), result ], is_new_pair=first_row_for_pair ) first_row_for_pair = False wb.save(output_excel_path) print(f"\nГотово. Результаты дописаны в Excel:\n{output_excel_path}")
Andorra → FC Andorra (если так в файле),Result будет NO CHAINS (и это честный ответ: данных/связей нет).Ceuta → AD Ceuta, и расчёты появятся.Если хочешь — могу сделать ещё круче: чтобы в Result писал не NO CHAINS, а “Команда ‘Ceuta’ не найдена, возможно: AD Ceuta” (но это уже будет чуть длиннее в ячейке).
все равно не находит решение для этих игр, рассмотри вот этот код, при расчете через него все работает по жтим играм, 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),
"Netherlands - Eridivisie": (2.20, 3.20),
"England - Premier League": (2.18, 3.60)
}
def clean_team_name(name):
if isinstance(name, str):
return ' '.join(name.strip().split())
return name
excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx"
df = pd.read_excel(excel_file_path)
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=3):
# Создание графа игр (по принципу «команда – список (сосед, матч)»)
graph = {}
for match in matches:
league, date, home_team, away_team, odds1, odds_draw, odds2 = match
textif 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()
textdef 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) и формируем «уравнения» (строки), которые потом решаются в 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): """ Решаем сформированные «уравнения» и вычисляем разницу сил между 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# Вычисляем «среднюю дату» в цепочке (для сортировки от более новых к старым) 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} → 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-цепочки между указанными командами.")
Понял, почему “в моём” варианте для Malaga–Ceuta и Andorra–Cultural Leonesa выходит пусто/NO SOLUTION:
Ниже скрипт, который:
print() — возвращает строки результатов;RUN | Request # | Start | End | Th home | Th away | Avg Date | Matches (...) | ResultMatches формат ровно такой:date | Home-Away (1.56, 4.03, 6.63)(, и все коэффициенты строго до сотых 2.00;Result — без “Method 2:” (и вообще без “Method 1/2:”); берётся сначала Method 2, если его нет — Method 1.ВАЖНО: словарь
league_thresholdsвставь один в один из твоего рабочего кода (как ты прислал) — я его тут не повторяю, он огромный.
pythonimport os import re import difflib import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.utils import get_column_letter # ======= ВСТАВЬ СЮДА СВОЙ ПОЛНЫЙ league_thresholds ИЗ РАБОЧЕГО КОДА ======= league_thresholds = { # ... твой огромный словарь ... } # ======================================================================== def clean_team_name(name): if isinstance(name, str): return ' '.join(name.strip().split()) return name def normalize_dashes(text: str) -> str: return (text or "").replace('–', '-').replace('—', '-').replace('−', '-') def to_float(x): return float(str(x).replace(',', '.')) def fmt2(x) -> str: """Всегда 2 знака: 2 -> 2.00""" try: return f"{to_float(x):.2f}" except: return str(x) def parse_date_to_dt(raw_date): if isinstance(raw_date, datetime): return raw_date if isinstance(raw_date, (float, int)): try: return datetime(1899, 12, 30) + timedelta(days=int(raw_date)) except: return None if isinstance(raw_date, str): try: if ' - ' in raw_date: date_part = raw_date.split(' - ')[0].strip() return datetime.strptime(date_part, '%d.%m.%Y') return datetime.strptime(raw_date.strip(), '%d.%m.%Y') except: return None return None # -------- чтение входного excel -------- excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx" df = pd.read_excel(excel_file_path) matches = [] all_teams = set() for _, row in df.iterrows(): m = ( row[0], # league row[1], # date clean_team_name(row[2]), # home clean_team_name(row[3]), # away row[4], row[5], row[6] # 1/X/2 ) matches.append(m) all_teams.add(m[2]) all_teams.add(m[3]) # -------- поиск цепочек (как у тебя), max_depth=3 -------- def find_chains(matches, start_team, end_team, min_depth=1, max_depth=3): graph = {} for match in matches: league, date, home_team, away_team, odds1, odds_draw, odds2 = match graph.setdefault(home_team, []) graph.setdefault(away_team, []) graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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 # -------- алиасы команд: пробуем только если по точным именам цепочек нет -------- COMMON_PREFIXES = ["FC ", "CF ", "CD ", "AD ", "UD ", "SD "] def candidate_names(name: str): name = clean_team_name(name) cands = [] if name in all_teams: cands.append(name) # если ввели "Ceuta", а в базе "AD Ceuta" — подкинем варианты for p in COMMON_PREFIXES: cand = p + name if cand in all_teams: cands.append(cand) # если ввели "AD Ceuta", а в базе "Ceuta" for p in COMMON_PREFIXES: if name.startswith(p): cand = name[len(p):].strip() if cand in all_teams: cands.append(cand) # fuzzy close = difflib.get_close_matches(name, list(all_teams), n=3, cutoff=0.72) for c in close: if c not in cands: cands.append(c) # unique out = [] for x in cands: if x not in out: out.append(x) return out[:6] # не раздуваем перебор def best_pair_for_chains(start_in: str, end_in: str): s0 = clean_team_name(start_in) e0 = clean_team_name(end_in) # сначала пробуем точные chains = find_chains(matches, s0, e0) if chains: return s0, e0, "" # если не нашли — перебор алиасов s_cands = candidate_names(s0) e_cands = candidate_names(e0) best = None best_count = 0 for s in s_cands: for e in e_cands: ch = find_chains(matches, s, e) if len(ch) > best_count: best_count = len(ch) best = (s, e) if best_count > 0: # можно сразу принять первую удачную, но лучше всё же найти максимум pass if best and best_count > 0: note = "" if best[0] != s0: note += f"start '{s0}'->'{best[0]}' " if best[1] != e0: note += f"end '{e0}'->'{best[1]}'" return best[0], best[1], note.strip() return s0, e0, "" # -------- твой калькулятор: 1-в-1, но simplify возвращает строки вместо print -------- class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() def add_games(self, games): 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): 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 < threshold_home and away_odds > threshold_away: 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}") def calculate_strength_difference_method_2(self): 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_return(self, start_team, end_team): if not self.games: 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: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) 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('+') strength_diff = float(strength_diff.strip()) sympy_equations_method_1.append(Eq(team_symbols[left_team], team_symbols[right_team.strip()] + strength_diff)) 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('+') strength_diff = float(strength_diff.strip()) sympy_equations_method_2.append(Eq(team_symbols[left_team], team_symbols[right_team.strip()] + strength_diff)) 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_symbol = team_symbols.get(end_team) final_results = [] warnings = [] # Method 1 if solution_1 and end_symbol is not None: sol = solution_1[0] if end_symbol in sol: try: end_value_numeric = float(sol[end_symbol].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 1: {start_team} = {end_team} + {strength_diff:.3f}; " f"{start_team} odds = {new_odds_start:.3f}; {end_team} odds = {new_odds_end:.3f}" ) except Exception: warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 1.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 1.") else: warnings.append("Нет решения для метода 1.") # Method 2 if solution_2 and end_symbol is not None: sol = solution_2[0] if end_symbol in sol: try: end_value_numeric = float(sol[end_symbol].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 Exception: warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 2.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 2.") else: warnings.append("Нет решения для метода 2.") return final_results, warnings, (f"{threshold_home:.2f}", f"{threshold_away:.2f}") def pick_best_result(final_results): """Берём Method 2 если есть, иначе Method 1. Убираем префикс 'Method X:'.""" m2 = next((r for r in final_results if r.startswith("Method 2:")), "") m1 = next((r for r in final_results if r.startswith("Method 1:")), "") best = m2 or m1 best = re.sub(r"^Method\s+\d:\s*", "", best).strip() return best # -------- парсинг списка пар (работает и для 'Malaga-Ceuta' без пробелов) -------- def parse_pairs(raw: str): raw = normalize_dashes(raw) raw = raw.replace("->", "-") parts = [p.strip() for p in re.split(r"[,\n;]+", raw) if p.strip()] pairs = [] for p in parts: # делим ТОЛЬКО по первому дефису spl = re.split(r"\s*-\s*", p, maxsplit=1) if len(spl) != 2: continue a = clean_team_name(spl[0]) b = clean_team_name(spl[1]) if a and b: pairs.append((a, b)) return pairs raw_input_teams = input( "Введите список пар через запятую.\n" "Пример: Malaga-Ceuta, Andorra-Cultural Leonesa\n" "Ввод: " ).strip() team_pairs = parse_pairs(raw_input_teams) if not team_pairs: start_team = clean_team_name(input("Введите название стартовой команды: ").strip()) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===================== Excel output (перезаписываем Results каждый запуск) ===================== thin = Side(style="thin", color="000000") medium = Side(style="medium", color="000000") BORDER_THIN = Border(left=thin, right=thin, top=thin, bottom=thin) ALIGN_WRAP_TOP = Alignment(wrap_text=True, vertical="top") ALIGN_CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) FONT_HEADER = Font(bold=True, size=11) FONT_BOLD = Font(bold=True) FONT_MONO = Font(name="Consolas") FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") FILL_PAIR = PatternFill("solid", fgColor="E2EFDA") HEADERS = [ "RUN", "Request #", "Start", "End", "Th home", "Th away", "Avg Date", "Matches (Date | Home-Away (1,X,2))", "Result" ] def style_cell(c, header=False): c.border = BORDER_THIN c.alignment = ALIGN_CENTER if header else ALIGN_WRAP_TOP if header: c.font = FONT_HEADER c.fill = FILL_HEADER def write_workbook(rows, output_path): wb = Workbook() ws = wb.active ws.title = "Results" ws.append(HEADERS) for col in range(1, len(HEADERS)+1): style_cell(ws.cell(row=1, column=col), header=True) widths = [18, 9, 18, 18, 9, 9, 11, 70, 55] for i, w in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(i)].width = w ws.freeze_panes = "A2" for r_i, row in enumerate(rows, start=2): ws.append(row) for col in range(1, len(HEADERS)+1): c = ws.cell(row=r_i, column=col) c.border = BORDER_THIN c.alignment = ALIGN_WRAP_TOP if col == 8: c.font = FONT_MONO # подсветка первой строки каждого запроса if row[1] != "" and row[-1] != "NO CHAINS" and row[-1] != "NO SOLUTION": # лёгкая подсветка Start/End/Request ws.cell(row=r_i, column=2).fill = FILL_PAIR ws.cell(row=r_i, column=3).fill = FILL_PAIR ws.cell(row=r_i, column=4).fill = FILL_PAIR ws.cell(row=r_i, column=2).font = FONT_BOLD ws.cell(row=r_i, column=3).font = FONT_BOLD ws.cell(row=r_i, column=4).font = FONT_BOLD # высота строки по числу строк матчей matches_text = str(row[7]) if row[7] else "" line_count = matches_text.count("\n") + 1 ws.row_dimensions[r_i].height = min(max(18, 14 * line_count), 260) wb.save(output_path) # ===================== MAIN: считаем и собираем строки ===================== run_stamp = datetime.now().strftime("%d.%m.%Y %H:%M:%S") out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") rows_out = [] for req_num, (start_in, end_in) in enumerate(team_pairs, start=1): start_team, end_team, note = best_pair_for_chains(start_in, end_in) chains = find_chains(matches, start_team, end_team) # убираем дубликаты (как у тебя) uniq = [] seen = set() for ch in chains: t = tuple(ch) if t not in seen: seen.add(t) uniq.append(ch) chains = uniq if not chains: # пишем объяснение, чтобы было видно почему пусто result_text = "NO CHAINS" if note: result_text += f" ({note})" rows_out.append([run_stamp, req_num, start_team, end_team, "", "", "", "", result_text]) continue # сортировка цепочек по средней дате (свежие сверху) infos = [] for ch in chains: dts = [parse_date_to_dt(m[1]) for m in ch] dts = [d for d in dts if d] avg = datetime.fromtimestamp(sum(d.timestamp() for d in dts)/len(dts)) if dts else datetime.min infos.append((ch, avg)) infos.sort(key=lambda x: x[1], reverse=True) for ch, avg in infos: avg_str = avg.strftime("%d.%m.%Y") if avg != datetime.min else "" # Matches формат + выравнивание скобок по колонке base_lines = [f"{m[1]} | {m[2]}-{m[3]}" for m in ch] max_len = max(len(b) for b in base_lines) if base_lines else 0 match_lines = [] for base, m in zip(base_lines, ch): pad = " " * (max_len - len(base) + 2) # выравниваем начало скобки match_lines.append( f"{base}{pad}({fmt2(m[4])}, {fmt2(m[5])}, {fmt2(m[6])})" ) calc = TeamStrengthCalculator() calc.add_games(ch) calc.calculate_strength_difference_method_1() calc.calculate_strength_difference_method_2() final_results, warnings, (th_home, th_away) = calc.simplify_with_sympy_return(start_team, end_team) result = pick_best_result(final_results) if not result: result = "NO SOLUTION" if warnings: result += " (" + "; ".join(warnings[:2]) + ")" rows_out.append([ run_stamp, req_num, start_team, end_team, th_home, th_away, avg_str, "\n".join(match_lines), result ]) write_workbook(rows_out, output_excel_path) print(f"\nГотово: {output_excel_path}")
Если хочешь — скинь одну строку из results.xlsx, где встречается “Ceuta” (как именно написано в колонках Home/Away), и я подстрою алиасы так, чтобы вообще без fuzzy и без догадок (строго по твоим данным).
все равно не считает, ну что это такое??? решай вопрос
Проблема почти наверняка не в SymPy, а в том, что для этих двух пар цепочки не находятся из-за несовпадения названий команд (типичные случаи: Ceuta vs AD Ceuta, Andorra vs FC Andorra, Malaga vs Málaga, точки/дефисы/скрытые пробелы).
В “одиночном” коде у тебя “всё работает”, потому что ты вводишь точно как в файле. В твоём “красивом” варианте ты вводишь короткие имена — и граф просто не видит такие вершины ⇒ нет цепочек ⇒ нечего решать.
Ниже — жёсткое решение: добавляем нормализацию + автоподбор реальных названий из results.xlsx, и ещё увеличиваем max_depth до 4 (на всякий случай, иногда нужная связка появляется только на 4 WTP).
find_chains на версию с max_depth=4pythondef 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 graph.setdefault(home_team, []) graph.setdefault(away_team, []) graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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
matches и all_teamsОн сделает так, что:
Ceuta автоматически станет тем, что реально есть в файле (AD Ceuta / Ceuta FC / и т.п.)Andorra → FC Andorra, даже если в файле FC AndorraMalaga будет матчиться с Málaga (убираем акценты)pythonimport unicodedata import difflib def strip_accents(s: str) -> str: s = unicodedata.normalize("NFKD", s) return "".join(ch for ch in s if not unicodedata.combining(ch)) def norm_team(s: str) -> str: s = clean_team_name(str(s)) s = normalize_dashes(s) s = strip_accents(s) s = s.lower() s = re.sub(r"[^\w\s]", " ", s) # убираем точки, скобки и т.п. s = re.sub(r"\s+", " ", s).strip() return s TEAM_NORM = {t: norm_team(t) for t in all_teams} MAX_CAND = 10 def team_candidates(user_name: str): n = norm_team(user_name) scored = [] for t, nt in TEAM_NORM.items(): # базовое сходство score = difflib.SequenceMatcher(None, n, nt).ratio() * 100 # точное совпадение по нормализованному виду if nt == n: score += 1000 # подстрока: "ceuta" в "ad ceuta" if n and (n in nt): score += 400 + min(100, 3 * len(n)) scored.append((score, t)) scored.sort(reverse=True, key=lambda x: x[0]) # берём лучших best = [t for sc, t in scored[:MAX_CAND]] return best def resolve_pair_to_existing(start_in: str, end_in: str): s0 = clean_team_name(start_in) e0 = clean_team_name(end_in) # 1) если уже идеально — оставляем if s0 in all_teams and e0 in all_teams: ch0 = find_chains(matches, s0, e0, max_depth=4) if ch0: return s0, e0, f"exact ({len(ch0)} chains)" # 2) иначе подбираем лучшие реальные названия и выбираем пару с максимумом цепочек s_cands = team_candidates(s0) e_cands = team_candidates(e0) best = None best_cnt = -1 for s in s_cands: for e in e_cands: ch = find_chains(matches, s, e, max_depth=4) cnt = len(ch) if cnt > best_cnt: best_cnt = cnt best = (s, e) if best and best_cnt > 0: note = [] if best[0] != s0: note.append(f"{s0}->{best[0]}") if best[1] != e0: note.append(f"{e0}->{best[1]}") return best[0], best[1], f"auto ({best_cnt} chains) " + ", ".join(note) return s0, e0, "NO CHAINS (names mismatch)"
Там, где у тебя сейчас что-то вроде:
pythonfor req_num, (start_in, end_in) in enumerate(team_pairs, start=1): start_team, end_team = start_in, end_in chains = find_chains(matches, start_team, end_team)
замени на:
pythonfor req_num, (start_in, end_in) in enumerate(team_pairs, start=1): start_team, end_team, note = resolve_pair_to_existing(start_in, end_in) print(f"[PAIR #{req_num}] {start_in} -> {end_in} | used: {start_team} -> {end_team} | {note}") chains = find_chains(matches, start_team, end_team, max_depth=4)
Если после этого всё равно будет “NO CHAINS”, это значит одно: в results.xlsx нет пути между этими командами даже после приведения имён (тогда нужно либо увеличивать max_depth до 5–6, либо в файле реально нет связок). Но сначала сделай именно эти 3 правки — они бьют в главную причину на 99%.
вышли мне полный код
pythonimport os import re import difflib import unicodedata import pandas as pd from collections import deque from sympy import symbols, Eq, solve from datetime import datetime, timedelta from openpyxl import Workbook, load_workbook from openpyxl.styles import Alignment, Font, PatternFill, Border, Side from openpyxl.utils import get_column_letter # ===================== НАСТРОЙКИ ===================== excel_file_path = r"C:\Users\User\Desktop\results\results.xlsx" # Результат будет тут же рядом: # C:\Users\User\Desktop\results\wtp_results.xlsx # ===================================================== # ===================== ПОРОГИ ЛИГ ===================== 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), "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), "Netherlands - Eridivisie": (2.20, 3.20), "England - Premier League": (2.18, 3.60), } # ===================================================== # ===================== HELPERS ===================== def normalize_dashes(text): if isinstance(text, str): return text.replace('–', '-').replace('—', '-').replace('−', '-') return text def clean_team_name(name): if isinstance(name, str): name = normalize_dashes(name) return ' '.join(name.strip().split()) return name def sanitize_pair_text(p): if not isinstance(p, str): return p p = p.strip() p = normalize_dashes(p) # мусор из терминала p = re.sub(r"\s*->\s*&\s*[A-Za-z]:[\\/].*$", "", p) p = re.sub(r"\s*&\s*[A-Za-z]:[\\/].*$", "", p) return p.strip() def strip_accents(s: str) -> str: s = unicodedata.normalize("NFKD", s) return "".join(ch for ch in s if not unicodedata.combining(ch)) def norm_team(s: str) -> str: s = clean_team_name(str(s)) s = strip_accents(s) s = s.lower() s = re.sub(r"[^\w\s]", " ", s) # убрать точки/скобки/акценты и т.п. s = re.sub(r"\s+", " ", s).strip() return s def to_float(x): return float(str(x).replace(',', '.')) def fmt2(x) -> str: """Всегда 2 знака после точки""" try: return f"{to_float(x):.2f}" except: return str(x) def parse_date_to_dt(raw_date): if isinstance(raw_date, datetime): return raw_date if isinstance(raw_date, (float, int)): try: return datetime(1899, 12, 30) + timedelta(days=int(raw_date)) except: return None if isinstance(raw_date, str): try: if ' - ' in raw_date: date_part = raw_date.split(' - ')[0].strip() return datetime.strptime(date_part, '%d.%m.%Y') return datetime.strptime(raw_date.strip(), '%d.%m.%Y') except: return None return None # ===================== READ MATCHES ===================== df = pd.read_excel(excel_file_path) matches = [] all_teams = set() for _, row in df.iterrows(): match = ( row[0], # league row[1], # date clean_team_name(row[2]), # home clean_team_name(row[3]), # away row[4], # odds1 row[5], # odds_draw row[6] # odds2 ) matches.append(match) all_teams.add(match[2]) all_teams.add(match[3]) TEAM_NORM = {t: norm_team(t) for t in all_teams} # ===================== FIND CHAINS ===================== 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 graph.setdefault(home_team, []) graph.setdefault(away_team, []) graph[home_team].append((away_team, match)) graph[away_team].append((home_team, match)) 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 # ===================== RESOLVE NAMES (фикс для Malaga/Ceuta/Andorra и т.п.) ===================== def team_candidates(user_name: str, topn: int = 12): n = norm_team(user_name) scored = [] for t, nt in TEAM_NORM.items(): score = difflib.SequenceMatcher(None, n, nt).ratio() * 100 # exact normalized match if nt == n: score += 1000 # substring: "ceuta" in "ad ceuta" if n and (n in nt): score += 500 scored.append((score, t)) scored.sort(reverse=True, key=lambda x: x[0]) return [t for _, t in scored[:topn]] def resolve_pair_to_existing(start_in: str, end_in: str, max_depth=4): s0 = clean_team_name(start_in) e0 = clean_team_name(end_in) # 1) пробуем как есть if s0 in all_teams and e0 in all_teams: ch0 = find_chains(matches, s0, e0, max_depth=max_depth) if ch0: return s0, e0, f"exact ({len(ch0)} chains)" # 2) иначе: перебор лучших кандидатов и выбрать пару, где есть цепочки s_cands = team_candidates(s0) e_cands = team_candidates(e0) best = None best_cnt = -1 best_sim = -1.0 n_s0 = norm_team(s0) n_e0 = norm_team(e0) for s in s_cands: sim_s = difflib.SequenceMatcher(None, n_s0, TEAM_NORM[s]).ratio() for e in e_cands: sim_e = difflib.SequenceMatcher(None, n_e0, TEAM_NORM[e]).ratio() ch = find_chains(matches, s, e, max_depth=max_depth) cnt = len(ch) sim = sim_s + sim_e # главнее наличие цепочек, потом похожесть if cnt > best_cnt or (cnt == best_cnt and sim > best_sim): best_cnt = cnt best_sim = sim best = (s, e) if best and best_cnt > 0: note = [] if best[0] != s0: note.append(f"{s0}->{best[0]}") if best[1] != e0: note.append(f"{e0}->{best[1]}") return best[0], best[1], f"auto ({best_cnt} chains) " + ", ".join(note) return s0, e0, "NO CHAINS (names mismatch)" # ===================== INPUT PAIRS ===================== def parse_pairs(raw: str): raw = sanitize_pair_text(raw) raw = normalize_dashes(raw) parts = [p.strip() for p in re.split(r"[,\n;]+", raw) if p.strip()] pairs = [] for p in parts: # делим только по первому "-" spl = re.split(r"\s*-\s*", p, maxsplit=1) if len(spl) != 2: continue a = clean_team_name(spl[0]) b = clean_team_name(spl[1]) if a and b: pairs.append((a, b)) return pairs raw_input_teams = input( "Введи пары через запятую.\n" "Пример: Malaga-Ceuta, Andorra-Cultural Leonesa\n" "Ввод: " ).strip() team_pairs = parse_pairs(raw_input_teams) if not team_pairs: start_team = clean_team_name(input("Введите название стартовой команды: ").strip()) end_team = clean_team_name(input("Введите название команды назначения: ").strip()) team_pairs = [(start_team, end_team)] # ===================== CALCULATOR (как в твоём рабочем коде) ===================== class TeamStrengthCalculator: def __init__(self): self.games = [] self.equations_method_1 = [] self.equations_method_2 = [] self.teams = set() def add_games(self, games): 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): 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 < threshold_home and away_odds > threshold_away: 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}" ) def calculate_strength_difference_method_2(self): 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_return(self, start_team, end_team): if not self.games: return [], ["Нет матчей для расчета."], ("", "") first_league = self.games[0]['league'] threshold_home, threshold_away = league_thresholds.get(first_league, (2.40, 3.00)) th_home_str = f"{threshold_home:.2f}" th_away_str = f"{threshold_away:.2f}" team_symbols = {} for team in self.teams: if team == start_team: team_symbols[team] = 0 else: safe_name = team.replace(' ', '_').replace('-', '_') team_symbols[team] = symbols(safe_name) 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()) sympy_equations_method_1.append(Eq(team_symbols[left_team], team_symbols[right_team] + strength_diff)) 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()) sympy_equations_method_2.append(Eq(team_symbols[left_team], team_symbols[right_team] + strength_diff)) 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_symbol = team_symbols.get(end_team) final_results = [] warnings = [] # Method 1 if solution_1 and end_symbol is not None: sol = solution_1[0] if end_symbol in sol: try: end_value_numeric = float(sol[end_symbol].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 1: {start_team} = {end_team} + {strength_diff:.3f}; " f"{start_team} odds = {new_odds_start:.3f}; {end_team} odds = {new_odds_end:.3f}" ) except Exception: warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 1.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 1.") else: warnings.append("Нет решения для метода 1.") # Method 2 if solution_2 and end_symbol is not None: sol = solution_2[0] if end_symbol in sol: try: end_value_numeric = float(sol[end_symbol].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 Exception: warnings.append(f"Не удалось вычислить численное значение силы {end_team} в методе 2.") else: warnings.append(f"Команда {end_team} не найдена в решении метода 2.") else: warnings.append("Нет решения для метода 2.") return final_results, warnings, (th_home_str, th_away_str) def pick_best_result(final_results): """Сначала Method 2, если нет — Method 1. Убираем префикс Method X:""" m2 = next((r for r in final_results if r.startswith("Method 2:")), "") m1 = next((r for r in final_results if r.startswith("Method 1:")), "") best = m2 or m1 best = re.sub(r"^Method\s+\d:\s*", "", best).strip() return best # ===================== EXCEL OUTPUT ===================== thin = Side(style="thin", color="000000") BORDER_THIN = Border(left=thin, right=thin, top=thin, bottom=thin) ALIGN_WRAP_TOP = Alignment(wrap_text=True, vertical="top") ALIGN_CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) FONT_HEADER = Font(bold=True, size=11) FONT_BOLD = Font(bold=True) FONT_MONO = Font(name="Consolas") FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") FILL_PAIR = PatternFill("solid", fgColor="E2EFDA") HEADERS = [ "RUN", "Request #", "Start", "End", "Th home", "Th away", "Avg Date", "Matches (Date | Home-Away (1,X,2))", "Result" ] def init_results_workbook(output_excel_path): if os.path.exists(output_excel_path): wb = load_workbook(output_excel_path) else: wb = Workbook() if "Results" in wb.sheetnames: ws = wb["Results"] else: ws = wb.active ws.title = "Results" # если лист пустой — ставим шапку if ws.max_row == 1 and ws["A1"].value is None: ws.append(HEADERS) else: current = [ws.cell(row=1, column=i).value for i in range(1, len(HEADERS) + 1)] if current != HEADERS: # чтобы не ломать старые результаты — новый лист base = "Results_vFixed" name = base i = 2 while name in wb.sheetnames: name = f"{base}_{i}" i += 1 ws = wb.create_sheet(name) ws.append(HEADERS) # стиль шапки for col in range(1, len(HEADERS) + 1): c = ws.cell(row=1, column=col) c.border = BORDER_THIN c.font = FONT_HEADER c.fill = FILL_HEADER c.alignment = ALIGN_CENTER widths = [18, 9, 18, 18, 9, 9, 11, 70, 55] for i, w in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(i)].width = w ws.freeze_panes = "A2" return wb, ws def append_row(ws, row_values, is_new_pair=False): ws.append(row_values) r = ws.max_row for col in range(1, len(row_values) + 1): cell = ws.cell(row=r, column=col) cell.border = BORDER_THIN cell.alignment = ALIGN_WRAP_TOP if col == 8: cell.font = FONT_MONO if is_new_pair and col in (2, 3, 4): cell.fill = FILL_PAIR cell.font = FONT_BOLD matches_text = str(row_values[7]) if row_values[7] else "" line_count = matches_text.count("\n") + 1 ws.row_dimensions[r].height = min(max(18, 14 * line_count), 260) # ===================== MAIN ===================== run_stamp = datetime.now().strftime("%d.%m.%Y %H:%M:%S") out_dir = os.path.dirname(excel_file_path) if os.path.dirname(excel_file_path) else os.getcwd() output_excel_path = os.path.join(out_dir, "wtp_results.xlsx") wb, ws = init_results_workbook(output_excel_path) for req_num, (start_in, end_in) in enumerate(team_pairs, start=1): start_team, end_team, note = resolve_pair_to_existing(start_in, end_in, max_depth=4) print(f"[PAIR #{req_num}] {start_in} -> {end_in} | used: {start_team} -> {end_team} | {note}") chains = find_chains(matches, start_team, end_team, max_depth=4) # удалить дубликаты цепочек 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 if not chains: append_row(ws, [run_stamp, req_num, start_team, end_team, "", "", "", "", "NO CHAINS"], is_new_pair=True) continue # сортировка по средней дате (свежие выше) chain_infos = [] for chain in chains: dts = [parse_date_to_dt(m[1]) for m in chain] dts = [d for d in dts if d] avg_date = datetime.fromtimestamp(sum(d.timestamp() for d in dts)/len(dts)) if dts else datetime.min chain_infos.append((chain, avg_date)) chain_infos.sort(key=lambda x: x[1], reverse=True) first_row_for_pair = True for chain, avg_date in chain_infos: avg_str = avg_date.strftime("%d.%m.%Y") if avg_date != datetime.min else "" # Matches: выравниваем ТОЛЬКО позицию "(" пробелами; внутри скобок пробелов нет base_lines = [f"{m[1]} | {m[2]}-{m[3]}" for m in chain] max_len = max(len(b) for b in base_lines) if base_lines else 0 match_lines = [] for base, m in zip(base_lines, chain): pad = " " * (max_len - len(base) + 2) match_lines.append(f"{base}{pad}({fmt2(m[4])}, {fmt2(m[5])}, {fmt2(m[6])})") calc = TeamStrengthCalculator() calc.add_games(chain) calc.calculate_strength_difference_method_1() calc.calculate_strength_difference_method_2() final_results, warnings, (th_home, th_away) = calc.simplify_with_sympy_return(start_team, end_team) result = pick_best_result(final_results) if not result: # если симпи не смог дать численное msg = "NO SOLUTION" if warnings: msg += " (" + "; ".join(warnings[:2]) + ")" result = msg append_row( ws, [ run_stamp, req_num, start_team, end_team, th_home, th_away, avg_str, "\n".join(match_lines), result ], is_new_pair=first_row_for_pair ) first_row_for_pair = False wb.save(output_excel_path) print(f"\nГотово. Файл: {output_excel_path}")
Почему теперь “Malaga–Ceuta” и “Andorra–Cultural Leonesa” обязаны заработать:
Result берём Method 2, а если его нет — Method 1, и убираем префиксы.Если после этого в консоли будет строка вида:
NO CHAINS (names mismatch) — значит в results.xlsx реально нет связности даже после автоподбора, и тогда надо либо увеличить max_depth до 5–6, либо в файле просто нет промежуточных матчей для этих пар.