ufc-fight-predictor / scripts /predict_fight.py
benjamintia's picture
Upload scripts/predict_fight.py with huggingface_hub
8bc1dc0 verified
Raw
History Blame Contribute Delete
16.3 kB
"""
UFC Fight Predictor CLI
Predict the outcome of a specific UFC matchup using the trained ensemble model.
Usage:
python scripts/predict_fight.py --fighter_a "Islam Makhachev" --fighter_b "Charles Oliveira"
python scripts/predict_fight.py -a "Jon Jones" -b "Tom Aspinall" --verbose
python scripts/predict_fight.py --list-fighters
"""
import os
import sys
import argparse
import json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import numpy as np
DATA_DIR = Path(__file__).parent.parent / "data"
KNOWN_FIGHTERS = {
"Jon Jones": {"slpm": 4.29, "sapm": 2.22, "td_avg": 1.93, "td_def": 95, "strike_acc": 57, "strike_def": 65,
"sub_avg": 0.5, "win_rate": 0.95, "height_inches": 76, "reach_inches": 84.5, "num_fights": 28,
"momentum": 0.8, "sentiment": 0.6},
"Islam Makhachev": {"slpm": 3.51, "sapm": 1.45, "td_avg": 3.17, "td_def": 89, "strike_acc": 62, "strike_def": 67,
"sub_avg": 0.7, "win_rate": 0.94, "height_inches": 70, "reach_inches": 72.0, "num_fights": 26,
"momentum": 0.9, "sentiment": 0.7},
"Alexander Volkanovski": {"slpm": 6.23, "sapm": 3.35, "td_avg": 1.72, "td_def": 72, "strike_acc": 58, "strike_def": 63,
"sub_avg": 0.4, "win_rate": 0.89, "height_inches": 66, "reach_inches": 71.5, "num_fights": 29,
"momentum": 0.3, "sentiment": 0.4},
"Israel Adesanya": {"slpm": 4.93, "sapm": 3.77, "td_avg": 0.08, "td_def": 77, "strike_acc": 51, "strike_def": 61,
"sub_avg": 0.1, "win_rate": 0.83, "height_inches": 76, "reach_inches": 80.0, "num_fights": 27,
"momentum": -0.2, "sentiment": -0.2},
"Alex Pereira": {"slpm": 5.26, "sapm": 4.33, "td_avg": 0.00, "td_def": 70, "strike_acc": 62, "strike_def": 54,
"sub_avg": 0.0, "win_rate": 0.81, "height_inches": 76, "reach_inches": 79.0, "num_fights": 11,
"momentum": 0.7, "sentiment": 0.6},
"Charles Oliveira": {"slpm": 3.45, "sapm": 3.41, "td_avg": 2.88, "td_def": 63, "strike_acc": 54, "strike_def": 51,
"sub_avg": 1.4, "win_rate": 0.79, "height_inches": 70, "reach_inches": 74.0, "num_fights": 40,
"momentum": 0.4, "sentiment": 0.3},
"Leon Edwards": {"slpm": 2.74, "sapm": 2.89, "td_avg": 1.60, "td_def": 68, "strike_acc": 48, "strike_def": 55,
"sub_avg": 0.3, "win_rate": 0.85, "height_inches": 74, "reach_inches": 74.0, "num_fights": 22,
"momentum": 0.5, "sentiment": 0.5},
"Max Holloway": {"slpm": 7.24, "sapm": 4.75, "td_avg": 0.20, "td_def": 84, "strike_acc": 51, "strike_def": 60,
"sub_avg": 0.1, "win_rate": 0.79, "height_inches": 71, "reach_inches": 69.0, "num_fights": 30,
"momentum": 0.6, "sentiment": 0.5},
"Dustin Poirier": {"slpm": 5.57, "sapm": 4.38, "td_avg": 1.70, "td_def": 62, "strike_acc": 50, "strike_def": 52,
"sub_avg": 0.7, "win_rate": 0.77, "height_inches": 69, "reach_inches": 72.0, "num_fights": 35,
"momentum": 0.2, "sentiment": 0.2},
"Justin Gaethje": {"slpm": 7.70, "sapm": 7.99, "td_avg": 0.11, "td_def": 75, "strike_acc": 55, "strike_def": 53,
"sub_avg": 0.1, "win_rate": 0.75, "height_inches": 71, "reach_inches": 70.0, "num_fights": 28,
"momentum": 0.3, "sentiment": 0.1},
"Sean O'Malley": {"slpm": 6.79, "sapm": 3.75, "td_avg": 0.00, "td_def": 70, "strike_acc": 62, "strike_def": 62,
"sub_avg": 0.0, "win_rate": 0.88, "height_inches": 71, "reach_inches": 72.0, "num_fights": 18,
"momentum": 0.7, "sentiment": 0.7},
"Ilia Topuria": {"slpm": 4.91, "sapm": 3.47, "td_avg": 1.36, "td_def": 74, "strike_acc": 52, "strike_def": 56,
"sub_avg": 0.6, "win_rate": 1.0, "height_inches": 67, "reach_inches": 69.0, "num_fights": 15,
"momentum": 0.95, "sentiment": 0.9},
"Sean Strickland": {"slpm": 5.94, "sapm": 4.03, "td_avg": 0.82, "td_def": 74, "strike_acc": 41, "strike_def": 68,
"sub_avg": 0.0, "win_rate": 0.75, "height_inches": 73, "reach_inches": 76.0, "num_fights": 30,
"momentum": -0.1, "sentiment": 0.0},
"Jiri Prochazka": {"slpm": 5.55, "sapm": 5.08, "td_avg": 0.33, "td_def": 60, "strike_acc": 54, "strike_def": 44,
"sub_avg": 0.3, "win_rate": 0.88, "height_inches": 76, "reach_inches": 80.0, "num_fights": 31,
"momentum": 0.5, "sentiment": 0.4},
"Khamzat Chimaev": {"slpm": 4.82, "sapm": 1.92, "td_avg": 4.29, "td_def": 60, "strike_acc": 62, "strike_def": 50,
"sub_avg": 0.4, "win_rate": 1.0, "height_inches": 74, "reach_inches": 75.0, "num_fights": 13,
"momentum": 0.7, "sentiment": 0.6},
"Tom Aspinall": {"slpm": 7.63, "sapm": 3.67, "td_avg": 2.42, "td_def": 100, "strike_acc": 53, "strike_def": 63,
"sub_avg": 0.6, "win_rate": 0.92, "height_inches": 77, "reach_inches": 78.0, "num_fights": 18,
"momentum": 0.85, "sentiment": 0.8},
"Ciryl Gane": {"slpm": 5.08, "sapm": 2.51, "td_avg": 0.76, "td_def": 63, "strike_acc": 60, "strike_def": 62,
"sub_avg": 0.3, "win_rate": 0.84, "height_inches": 76, "reach_inches": 83.0, "num_fights": 15,
"momentum": 0.1, "sentiment": 0.2},
"Colby Covington": {"slpm": 4.16, "sapm": 3.35, "td_avg": 4.12, "td_def": 72, "strike_acc": 36, "strike_def": 56,
"sub_avg": 0.4, "win_rate": 0.79, "height_inches": 71, "reach_inches": 72.0, "num_fights": 20,
"momentum": -0.3, "sentiment": -0.3},
"Robert Whittaker": {"slpm": 4.78, "sapm": 3.62, "td_avg": 1.62, "td_def": 85, "strike_acc": 42, "strike_def": 61,
"sub_avg": 0.1, "win_rate": 0.81, "height_inches": 72, "reach_inches": 73.5, "num_fights": 30,
"momentum": 0.2, "sentiment": 0.1},
"Arman Tsarukyan": {"slpm": 4.42, "sapm": 2.86, "td_avg": 3.62, "td_def": 72, "strike_acc": 46, "strike_def": 58,
"sub_avg": 0.5, "win_rate": 0.83, "height_inches": 67, "reach_inches": 72.0, "num_fights": 23,
"momentum": 0.6, "sentiment": 0.5},
"Brandon Moreno": {"slpm": 4.41, "sapm": 3.59, "td_avg": 1.71, "td_def": 64, "strike_acc": 43, "strike_def": 55,
"sub_avg": 0.7, "win_rate": 0.72, "height_inches": 67, "reach_inches": 70.0, "num_fights": 27,
"momentum": -0.1, "sentiment": 0.0},
"Shavkat Rakhmonov": {"slpm": 4.20, "sapm": 2.38, "td_avg": 1.63, "td_def": 100, "strike_acc": 51, "strike_def": 58,
"sub_avg": 1.0, "win_rate": 1.0, "height_inches": 73, "reach_inches": 77.0, "num_fights": 18,
"momentum": 0.9, "sentiment": 0.8},
"Petr Yan": {"slpm": 5.62, "sapm": 3.99, "td_avg": 1.77, "td_def": 88, "strike_acc": 47, "strike_def": 60,
"sub_avg": 0.1, "win_rate": 0.70, "height_inches": 67, "reach_inches": 66.5, "num_fights": 20,
"momentum": -0.2, "sentiment": 0.0},
"Cory Sandhagen": {"slpm": 5.49, "sapm": 3.85, "td_avg": 1.40, "td_def": 64, "strike_acc": 41, "strike_def": 66,
"sub_avg": 0.3, "win_rate": 0.76, "height_inches": 71, "reach_inches": 70.0, "num_fights": 19,
"momentum": 0.3, "sentiment": 0.2},
"Gilbert Burns": {"slpm": 2.72, "sapm": 3.32, "td_avg": 2.55, "td_def": 42, "strike_acc": 45, "strike_def": 52,
"sub_avg": 0.7, "win_rate": 0.72, "height_inches": 69, "reach_inches": 71.0, "num_fights": 27,
"momentum": -0.2, "sentiment": 0.0},
"Belal Muhammad": {"slpm": 4.66, "sapm": 3.41, "td_avg": 1.94, "td_def": 100, "strike_acc": 51, "strike_def": 56,
"sub_avg": 0.1, "win_rate": 0.80, "height_inches": 70, "reach_inches": 72.0, "num_fights": 24,
"momentum": 0.8, "sentiment": 0.6},
}
FIGHTER_WEIGHT_CLASS = {
"Jon Jones": 9, "Islam Makhachev": 5, "Alexander Volkanovski": 4, "Israel Adesanya": 7,
"Alex Pereira": 8, "Charles Oliveira": 5, "Leon Edwards": 6, "Max Holloway": 4,
"Dustin Poirier": 5, "Justin Gaethje": 5, "Sean O'Malley": 3, "Ilia Topuria": 4,
"Sean Strickland": 7, "Jiri Prochazka": 8, "Khamzat Chimaev": 7, "Tom Aspinall": 9,
"Ciryl Gane": 9, "Colby Covington": 6, "Robert Whittaker": 7, "Arman Tsarukyan": 5,
"Brandon Moreno": 2, "Shavkat Rakhmonov": 6, "Petr Yan": 3, "Cory Sandhagen": 3,
"Gilbert Burns": 6, "Belal Muhammad": 6, "Conor McGregor": 5, "Kamaru Usman": 6,
"Francis Ngannou": 9, "Stipe Miocic": 9, "Deiveson Figueiredo": 3, "Henry Cejudo": 3,
"Aljamain Sterling": 4, "Merab Dvalishvili": 3, "Tony Ferguson": 5, "Michael Chandler": 5,
"Kevin Holland": 6, "Stephen Thompson": 6, "Derrick Lewis": 9, "Sergei Pavlovich": 9,
"Jailton Almeida": 9, "Magomed Ankalaev": 8, "Jan Blachowicz": 8, "Marlon Vera": 3,
"Yair Rodriguez": 4, "Arnold Allen": 4, "Calvin Kattar": 4, "Brian Ortega": 4,
"Josh Emmett": 4, "Jack Della Maddalena": 6, "Geoff Neal": 6, "Alexandre Pantoja": 2,
"Manon Fiorot": 2, "Erin Blanchfield": 2, "Rose Namajunas": 2, "Zhang Weili": 1,
"Valentina Shevchenko": 2, "Alexa Grasso": 2, "Jared Cannonier": 7, "Paulo Costa": 7,
"Marvin Vettori": 7, "Dominick Reyes": 8, "Curtis Blaydes": 9, "Tai Tuivasa": 9,
"Beneil Dariush": 5, "Mateusz Gamrot": 5, "Rafael Fiziev": 5, "Dan Hooker": 5,
"Neil Magny": 6, "Derek Brunson": 7, "Anthony Smith": 8, "Aleksandar Rakic": 8,
}
def list_fighters():
"""Print all known fighters."""
print(f"\n Available Fighters ({len(KNOWN_FIGHTERS)}):")
print(" " + "-" * 60)
for name, stats in sorted(KNOWN_FIGHTERS.items()):
momentum = stats.get("momentum", 0)
momentum_indicator = "+" if momentum > 0.3 else "-" if momentum < -0.3 else "~"
record = f"{stats.get('win_rate', 0)*100:.0f}% win rate" if stats.get("win_rate") else ""
print(f" {momentum_indicator} {name:<25} {record}")
print()
def format_prediction(result, fighter_a, fighter_b):
"""Pretty-print prediction results."""
print("\n" + "=" * 60)
print(" UFC FIGHT PREDICTION")
print("=" * 60)
print(f"\n {fighter_a} vs {fighter_b}")
print(f" {'-' * 40}")
prob_a = result["fighter_a_win_probability"]
prob_b = result["fighter_b_win_probability"]
bar_width = 36
a_bars = int(bar_width * prob_a)
b_bars = bar_width - a_bars
bar_a = "#" * a_bars
bar_b = "#" * b_bars
print(f"\n {fighter_a:<22} {bar_a} {prob_a:.1%}")
print(f" {fighter_b:<22} {bar_b} {prob_b:.1%}")
print(f"\n {'Predicted Winner:':<22} {result['predicted_winner']}")
print(f" {'Confidence:':<22} {result['confidence']:.1%}")
individual = result.get("individual_predictions", {})
if individual:
print(f"\n Individual Model Predictions (Fighter A win probability):")
print(f" {'XGBoost:':<22} {individual['xgb_fighter_a_prob']:.1%}")
print(f" {'LightGBM:':<22} {individual['lgb_fighter_a_prob']:.1%}")
print(f" {'Neural Net:':<22} {individual['nn_fighter_a_prob']:.1%}")
agreement = result.get("model_agreement", 0)
print(f"\n {'Model Agreement:':<22} {agreement:.1%} ({'High' if agreement > 0.85 else 'Moderate' if agreement > 0.7 else 'Low - models disagree'})")
print("\n" + "=" * 60)
def get_fighter_features(fighter_name):
"""Get feature dict for a fighter, with graceful fuzzy matching."""
if fighter_name in KNOWN_FIGHTERS:
features = KNOWN_FIGHTERS[fighter_name].copy()
features["weight_class"] = FIGHTER_WEIGHT_CLASS.get(fighter_name, 5)
return features
name_lower = fighter_name.lower()
for known_name, stats in KNOWN_FIGHTERS.items():
if name_lower in known_name.lower() or known_name.lower() in name_lower:
print(f" (Matched '{fighter_name}' to '{known_name}')")
return stats
print(f" WARNING: '{fighter_name}' not found in database. Using generic fighter profile.")
return {
"slpm": 3.5, "sapm": 3.5, "td_avg": 1.5, "td_def": 65,
"strike_acc": 48, "strike_def": 55, "sub_avg": 0.3,
"win_rate": 0.70, "height_inches": 71, "reach_inches": 72.0,
"num_fights": 20, "momentum": 0.0, "sentiment": 0.0,
"weight_class": 5,
}
def main():
parser = argparse.ArgumentParser(
description="UFC Fight Predictor - Predict fight outcomes using ML ensemble",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python predict_fight.py -a "Islam Makhachev" -b "Charles Oliveira"
python predict_fight.py -a "Jon Jones" -b "Tom Aspinall" --verbose --json
python predict_fight.py --list-fighters
""",
)
parser.add_argument("-a", "--fighter_a", type=str, help="Name of Fighter A")
parser.add_argument("-b", "--fighter_b", type=str, help="Name of Fighter B")
parser.add_argument("--list-fighters", action="store_true", help="List all available fighters")
parser.add_argument("--verbose", action="store_true", help="Show detailed output")
parser.add_argument("--json", action="store_true", help="Output result as JSON")
parser.add_argument("--model-dir", type=str, default=None, help="Custom model directory path")
args = parser.parse_args()
if args.list_fighters:
list_fighters()
return
if not args.fighter_a or not args.fighter_b:
parser.print_help()
print("\n Please specify both fighters. Examples:")
print(' python scripts/predict_fight.py -a "Islam Makhachev" -b "Charles Oliveira"')
print(' python scripts/predict_fight.py --list-fighters')
return
features_a = get_fighter_features(args.fighter_a)
features_b = get_fighter_features(args.fighter_b)
try:
from inference import UFCPredictor
predictor = UFCPredictor(model_dir=args.model_dir)
except (FileNotFoundError, ImportError, ModuleNotFoundError):
print("\n Models not found or dependencies missing. Using fallback heuristic.")
print(" For full ML predictions, run: conda activate ufc_predictor && pip install -r requirements.txt")
features_a["name"] = args.fighter_a
features_b["name"] = args.fighter_b
a_score = (
features_a.get("slpm", 3.5) * 0.15
- features_a.get("sapm", 3.5) * 0.10
+ features_a.get("td_avg", 1.5) * 0.15
+ features_a.get("strike_acc", 48) * 0.10
+ features_a.get("strike_def", 55) * 0.10
+ features_a.get("td_def", 65) * 0.10
+ features_a.get("sub_avg", 0.3) * 0.10
+ features_a.get("win_rate", 0.7) * 0.10
+ features_a.get("momentum", 0) * 0.10
)
b_score = (
features_b.get("slpm", 3.5) * 0.15
- features_b.get("sapm", 3.5) * 0.10
+ features_b.get("td_avg", 1.5) * 0.15
+ features_b.get("strike_acc", 48) * 0.10
+ features_b.get("strike_def", 55) * 0.10
+ features_b.get("td_def", 65) * 0.10
+ features_b.get("sub_avg", 0.3) * 0.10
+ features_b.get("win_rate", 0.7) * 0.10
+ features_b.get("momentum", 0) * 0.10
)
total = a_score + b_score + 1e-9
prob_a = a_score / total
result = {
"fighter_a_win_probability": prob_a,
"fighter_b_win_probability": 1 - prob_a,
"predicted_winner": args.fighter_a if prob_a > 0.5 else args.fighter_b,
"confidence": abs(prob_a - 0.5) * 2,
"individual_predictions": {},
"model_agreement": 0.0,
}
else:
result = predictor.predict_proba(args.fighter_a, args.fighter_b, features_a, features_b)
if args.json:
print(json.dumps(result, indent=2))
else:
format_prediction(result, args.fighter_a, args.fighter_b)
if __name__ == "__main__":
main()