Skip to main content

Overview

The mapping module provides data normalization for leagues, markets, and betting nomenclature across different data sources. It ensures consistency and enables accurate bet grading by mapping human-readable market names to API stat types.

Module Structure

SharedServices/mapping/
├── __init__.py                    # Exports LeagueNormalizer, MarketResultMapper
├── league_normalizer.py           # League name normalization
├── market_mapper.py               # Market to API stat mapping
├── canonical_markets.py           # Canonical market definitions
├── market_result_mapper.py        # Legacy result mapping
└── fixture_matching_utils.py      # Fixture matching utilities

League Normalization

The LeagueNormalizer handles league name variations from different data sources.

Problem

Different APIs and bookmakers use different league names:
  • “LaLiga” vs “La Liga”
  • “Liga Portugal” vs “Primeira Liga”
  • “Admiral Bundesliga” vs “Bundesliga”
  • “Division Profesional” vs “Liga De Futbol Prof”
This creates duplicate leagues in dashboards and analytics.

Solution

SharedServices/mapping/league_normalizer.py
from PROPPR.SharedServices import LeagueNormalizer

# Normalize league names
league = LeagueNormalizer.normalize("LaLiga")
print(league)  # Output: "La Liga"

league = LeagueNormalizer.normalize("Liga Portugal")
print(league)  # Output: "Primeira Liga"

league = LeagueNormalizer.normalize("Admiral Bundesliga")
print(league)  # Output: "Bundesliga"

Normalization Map

The normalization map was auto-generated by analyzing 295,058 alerts:
SharedServices/mapping/league_normalizer.py
LEAGUE_NORMALIZATION_MAP = {
    # Spain
    "LaLiga": "La Liga",
    "LaLiga 2": "La Liga 2",
    
    # Portugal
    "Liga Portugal": "Primeira Liga",
    
    # Greece
    "Super League Greece": "Super League",
    
    # Europe
    "UEFA Conference League": "Europa Conference League",
    
    # Austria
    "Admiral Bundesliga": "Bundesliga",
    
    # England
    "Enterprise National League": "National League",
    
    # Argentina
    "Liga Profesional de Futbol": "Liga Profesional",
    
    # Bolivia (user-reported issue)
    "Division Profesional": "Liga De Futbol Prof",
    "División Profesional": "Liga De Futbol Prof",
    
    # And more...
}

Market Mapping

The UnifiedMarketMapper maps market names to API stat types for both player and team bets.

Key Features

  • Unified Interface: Single class for both player and team markets
  • Case-Insensitive Lookup: Handles name variations
  • Threshold Normalization: Removes “0.5+”, “1.5+” suffixes
  • Result Calculation: Determines won/lost/refund/half-win/half-loss

Player Market Mapping

SharedServices/mapping/market_mapper.py
from PROPPR.SharedServices.mapping import UnifiedMarketMapper

# Get market mapping for player bets
mapping = UnifiedMarketMapper.get_market_mapping(
    market_name="Player Goals",
    is_player_bet=True
)

print(mapping)
# {
#     'type_id': 52,
#     'stat_type': 'goals'
# }

Team Market Mapping

SharedServices/mapping/market_mapper.py
# Get market mapping for team bets
mapping = UnifiedMarketMapper.get_market_mapping(
    market_name="Total Corners",
    is_player_bet=False
)

print(mapping)
# {
#     'type_id': 34,
#     'location': 'total',
#     'stat_type': 'corners'
# }

Supported Player Markets

  • Player Goals (Anytime Goalscorer)
  • Player Score 2+ Goals
  • Player Score 3+ Goals
  • To Score First
  • Player Goal From Outside Box
  • Player Goal From Header
  • Player Shots Total
  • Player Shots On Target
  • Player Headed Shots
  • Player Headed Shots On Target
  • Player SOT Outside Box
  • Player Yellow Card (To Be Booked)
  • Player Red Card
  • Player First Card (To Be Booked First)
  • Player Assists
  • Player Score or Assist
  • Player Fouls Committed
  • Player Fouls Won
  • Player Tackles
  • Player Passes
  • Goalkeeper Saves

Supported Team Markets

  • Team Total Goals
  • Team Total Home
  • Team Total Away
  • Match Goals
  • Asian Handicap
  • Total Corners
  • Team Corners (Home/Away)
  • Corner Handicap
  • Corners Spread
  • Total Cards
  • Team Cards (Home/Away)
  • Card Handicap
  • Bookings Spread
  • Total Shots
  • Total Shots On Target
  • Team Shots (Home/Away)
  • Total Tackles
  • Total Offsides
  • Total Fouls

Market Name Normalization

The mapper automatically normalizes market names:
# Remove threshold suffixes
UnifiedMarketMapper.normalize_market_name("Player Goals 0.5+")
# Returns: "Player Goals"

UnifiedMarketMapper.normalize_market_name("Match Total - Corners")
# Returns: "Corners"

Result Calculation

The mapper calculates bet results based on actual values and thresholds:

Standard Over/Under

result = UnifiedMarketMapper.calculate_bet_result(
    actual_value=2.0,
    threshold=1.5,
    direction='over',
    market_name='Player Goals'
)
print(result)  # Output: "won"

result = UnifiedMarketMapper.calculate_bet_result(
    actual_value=1.5,
    threshold=1.5,
    direction='over'
)
print(result)  # Output: "refund" (exact hit)

Asian Handicap

# Handicap markets use tuple input (home_score, away_score)
result = UnifiedMarketMapper.calculate_bet_result(
    actual_value=(2, 1),  # Home 2 - Away 1
    threshold=-0.5,       # Away +0.5
    direction='away',
    market_name='Corner Handicap'
)
print(result)  # Output: "won" (Away covered)

Quarter Handicaps

Asian handicaps with quarter lines (0.25, 0.75, 1.25, 1.75) split into two bets:
# 1.25 handicap splits into 1.0 and 1.5
result = UnifiedMarketMapper.calculate_bet_result(
    actual_value=(3, 1),  # Home 3 - Away 1
    threshold=1.25,
    direction='home',
    market_name='Corners Spread'
)
print(result)  # Output: "won" (both 1.0 and 1.5 covered)

# Partial win scenario
result = UnifiedMarketMapper.calculate_bet_result(
    actual_value=(2, 1),  # Home 2 - Away 1
    threshold=1.25,
    direction='home'
)
print(result)  # Output: "half_win" (1.0 won, 1.5 push)

Canonical Markets

The canonical_markets module defines the single source of truth for player market names.

Canonical Player Markets

SharedServices/mapping/canonical_markets.py
from PROPPR.SharedServices.mapping.canonical_markets import (
    CANONICAL_PLAYER_MARKETS,
    normalize_market_name,
    is_canonical_market
)

# 23 canonical player markets
print(len(CANONICAL_PLAYER_MARKETS))  # 23

# Check if market is canonical
is_canonical_market("Player Goals")  # True
is_canonical_market("Anytime Goalscorer")  # False (variant)

# Normalize to canonical
canonical = normalize_market_name("Anytime Goalscorer")
print(canonical)  # "Player Goals"

Market Name Variations

The normalization map handles all variations:
MARKET_NAME_NORMALIZATION = {
    # Goals
    "Anytime Goalscorer": "Player Goals",
    "Multi Scorers": "Player Goals",
    
    # Assists
    "Player To Assist": "Player Assists",
    
    # Score or Assist
    "Player to Score or Assist": "Player Score or Assist",
    
    # Cards
    "Anytime Booked": "Player Yellow Card",
    "To Be Booked First": "Player First Card",
    
    # And more...
}

Market Support Checking

from PROPPR.SharedServices.mapping import UnifiedMarketMapper

# Check if market is supported
is_supported = UnifiedMarketMapper.is_supported_market(
    market_name="Player Goals",
    is_player_bet=True
)
print(is_supported)  # True

# Get all supported markets
player_markets = UnifiedMarketMapper.get_supported_markets(is_player_bet=True)
team_markets = UnifiedMarketMapper.get_supported_markets(is_player_bet=False)

print(f"Player markets: {len(player_markets)}")
print(f"Team markets: {len(team_markets)}")

Untrackable Markets

Some markets cannot be tracked via API:
# Previously untrackable (now all trackable via shotmap/events):
# - First/Last Goalscorer (via get_first/last_goalscorer_id)
# - Inside/Outside Box (via shotmap isFromInsideBox)
# - Header (via shotmap shotType)
# - Penalty/Free Kick/Corner (via shotmap situation)

UNTRACKABLE_MARKETS = []  # Empty - all markets now trackable

Location Mapping

Team markets use location to determine which team’s stats to use:
'location' values:
- 'home': Home team stat
- 'away': Away team stat
- 'total': Combined (home + away)
- 'dynamic': Determined from alert metadata
- 'handicap_comparison': Handicap calculation

Best Practices

Use LeagueNormalizer.normalize() before storing or comparing leagues.
Use is_supported_market() before processing alerts.
Store canonical market names in database for consistency.
Remember quarter lines return half_win/half_loss, not just won/lost.

Next Steps

Bet Grading

See how market mappings are used in grading

API Clients

Learn about FotMob stat extraction

Build docs developers (and LLMs) love