UFC Sell-Through Project - Card Optimizer
Overview
The card optimizer builds optimal UFC fight cards using:
- Greedy Selection - Start with highest-scoring fights
- 2-opt Local Search - Iteratively improve by swapping fights
Data Structures
Fight Class
class Fight:
def __init__(self, fight_id, fighter1, fighter2, weight_class, is_title=False):
self.fight_id = fight_id
self.fighter1 = fighter1 # dict with name and stats
self.fighter2 = fighter2
self.weight_class = weight_class
self.is_title = is_title
self.score = 0.0
def __repr__(self):
title_str = " [TITLE]" if self.is_title else ""
return f"{self.fighter1['name']} vs {self.fighter2['name']} ({self.weight_class}){title_str}"
FightCard Class
class FightCard:
def __init__(self, fights=None):
self.fights = fights or []
self.predicted_sellthrough = 0.0
def add_fight(self, fight):
self.fights.append(fight)
def remove_fight(self, fight):
self.fights.remove(fight)
def get_all_fighters(self):
fighters = set()
for f in self.fights:
fighters.add(f.fighter1['name'])
fighters.add(f.fighter2['name'])
return fighters
def get_weight_classes(self):
return {f.weight_class for f in self.fights}
def count_title_fights(self):
return sum(1 for f in self.fights if f.is_title)
def is_valid(self, min_fights=10, max_fights=14, min_weight_classes=3):
# Right number of fights?
if len(self.fights) < min_fights or len(self.fights) > max_fights:
return False
# Enough variety?
if len(self.get_weight_classes()) < min_weight_classes:
return False
# No fighter appears twice?
fighters = []
for f in self.fights:
if f.fighter1['name'] in fighters or f.fighter2['name'] in fighters:
return False
fighters.extend([f.fighter1['name'], f.fighter2['name']])
return True
Fight Scoring
def score_fight(fight, current_card_fights):
"""Score a fight based on attendance potential."""
f1 = fight.fighter1
f2 = fight.fighter2
score = 0.0
# Win rate - successful fighters have fans
win_rate_1 = f1.get('win_rate', 0.5)
win_rate_2 = f2.get('win_rate', 0.5)
score += (win_rate_1 + win_rate_2) * 0.2
# Experience - established fighters have fanbases
exp_1 = min(f1.get('total_fights', 0) / 20, 1.0)
exp_2 = min(f2.get('total_fights', 0) / 20, 1.0)
score += (exp_1 + exp_2) * 0.15
# Finish rate - exciting fighters
finish_1 = f1.get('finish_rate', 0.3)
finish_2 = f2.get('finish_rate', 0.3)
score += (finish_1 + finish_2) * 0.15
# Title fights are HUGE draws
if fight.is_title:
score += 0.3
# Competitive matchups are more interesting
competitiveness = 1.0 - abs(win_rate_1 - win_rate_2)
score += competitiveness * 0.1
# Bonus for new weight class (variety)
current_weights = {f.weight_class for f in current_card_fights}
if fight.weight_class not in current_weights:
score += 0.1
return score
Sell-Through Prediction
def calculate_card_sellthrough(card):
"""Estimate sell-through from fight scores."""
if not card.fights:
return 0.5
total_score = sum(f.score for f in card.fights)
# Base sell-through + quality bonus
base = 0.75 # Most UFC events sell 70-80%
bonus = min(total_score / 15, 0.25) # Cap at 25%
# Title fight bonus
title_bonus = card.count_title_fights() * 0.02
predicted = base + bonus + title_bonus
return max(0.5, min(1.0, predicted))
Greedy Algorithm
def greedy_build_card(available_fights, min_fights=10, max_fights=14):
"""Build initial card using greedy selection."""
card = FightCard()
used_fighters = set()
# Score all fights
for fight in available_fights:
fight.score = score_fight(fight, [])
# Sort by score descending
sorted_fights = sorted(available_fights, key=lambda f: f.score, reverse=True)
# Greedily add best non-conflicting fights
for fight in sorted_fights:
if fight.fighter1['name'] in used_fighters:
continue
if fight.fighter2['name'] in used_fighters:
continue
card.add_fight(fight)
used_fighters.add(fight.fighter1['name'])
used_fighters.add(fight.fighter2['name'])
if len(card.fights) >= max_fights:
break
card.predicted_sellthrough = calculate_card_sellthrough(card)
return card
2-opt Local Search
def two_opt_improve(card, available_fights, max_iterations=100):
"""Improve card by swapping fights."""
best_card = card
best_score = calculate_card_sellthrough(card)
used_fighters = card.get_all_fighters()
# Get unused fights
unused_fights = [f for f in available_fights if f not in card.fights]
unused_fights = [f for f in unused_fights
if f.fighter1['name'] not in used_fighters
and f.fighter2['name'] not in used_fighters]
improved = True
iteration = 0
while improved and iteration < max_iterations:
improved = False
iteration += 1
for i, current_fight in enumerate(best_card.fights):
for new_fight in unused_fights:
# Check if swap is valid
test_fighters = used_fighters.copy()
test_fighters.remove(current_fight.fighter1['name'])
test_fighters.remove(current_fight.fighter2['name'])
if (new_fight.fighter1['name'] in test_fighters or
new_fight.fighter2['name'] in test_fighters):
continue
# Try the swap
test_card = FightCard(best_card.fights.copy())
test_card.fights[i] = new_fight
if test_card.is_valid():
new_score = calculate_card_sellthrough(test_card)
if new_score > best_score:
best_card = test_card
best_score = new_score
improved = True
# Update used/unused lists
unused_fights.append(current_fight)
unused_fights.remove(new_fight)
used_fighters = best_card.get_all_fighters()
break
if improved:
break
best_card.predicted_sellthrough = best_score
return best_card
Running the Optimizer
def optimize_card(available_fights):
"""Full optimization pipeline."""
print("Step 1: Greedy selection...")
initial_card = greedy_build_card(available_fights)
print(f" Initial card: {len(initial_card.fights)} fights")
print(f" Initial sell-through: {initial_card.predicted_sellthrough:.1%}")
print("\nStep 2: 2-opt improvement...")
optimized_card = two_opt_improve(initial_card, available_fights)
print(f" Final sell-through: {optimized_card.predicted_sellthrough:.1%}")
print("\nOptimized Fight Card:")
for i, fight in enumerate(optimized_card.fights, 1):
print(f" {i}. {fight}")
return optimized_card
Demo
python src/optimizer/card_optimizer.py
Constraints
| Constraint | Default | Description |
|---|---|---|
min_fights |
10 | Minimum fights on card |
max_fights |
14 | Maximum fights on card |
min_weight_classes |
3 | Variety requirement |
| No duplicate fighters | - | Fighter can only appear once |