Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total)
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* FIFA World Cup 2026 reference data — June 11–July 19, 2026.
|
||||
*
|
||||
* Static, hand-curated. The poller pulls fixtures/standings/squads from
|
||||
* APIs (football-data.org + worldcup2026 OSS); this file holds the
|
||||
* intelligence the APIs don't carry: venue altitudes, host-continent
|
||||
* teams, designated penalty/set-piece takers, and historical tournament
|
||||
* performers.
|
||||
*
|
||||
* Updated manually as the tournament progresses (squad confirmations,
|
||||
* penalty-taker shifts, etc.). Not consumed during normal cache misses —
|
||||
* the feature extractor reads this once per process load.
|
||||
*
|
||||
* Venue altitudes sourced from public elevation data (NOAA / Google
|
||||
* Earth, ft above sea level). Estadio Akron is in Zapopan (Guadalajara
|
||||
* metro) at ~5,100 ft; Estadio Azteca's stadium floor sits at ~7,349 ft
|
||||
* — the highest altitude in tournament history for the host venue.
|
||||
*/
|
||||
|
||||
const VENUES = Object.freeze({
|
||||
// United States (11)
|
||||
'AT&T Stadium': { city: 'Arlington, TX', altitude_ft: 600, climate: 'hot', country: 'USA' },
|
||||
'Mercedes-Benz Stadium': { city: 'Atlanta, GA', altitude_ft: 1050, climate: 'hot_humid', country: 'USA' },
|
||||
'Gillette Stadium': { city: 'Foxborough, MA', altitude_ft: 260, climate: 'temperate', country: 'USA' },
|
||||
'NRG Stadium': { city: 'Houston, TX', altitude_ft: 80, climate: 'hot_humid', country: 'USA' },
|
||||
'Arrowhead Stadium': { city: 'Kansas City, MO', altitude_ft: 820, climate: 'temperate', country: 'USA' },
|
||||
'SoFi Stadium': { city: 'Inglewood, CA', altitude_ft: 100, climate: 'temperate', country: 'USA' },
|
||||
'Hard Rock Stadium': { city: 'Miami Gardens, FL', altitude_ft: 10, climate: 'hot_humid', country: 'USA' },
|
||||
'MetLife Stadium': { city: 'East Rutherford, NJ', altitude_ft: 7, climate: 'temperate', country: 'USA' },
|
||||
'Lincoln Financial Field': { city: 'Philadelphia, PA', altitude_ft: 30, climate: 'temperate', country: 'USA' },
|
||||
"Levi's Stadium": { city: 'Santa Clara, CA', altitude_ft: 33, climate: 'temperate', country: 'USA' },
|
||||
'Lumen Field': { city: 'Seattle, WA', altitude_ft: 15, climate: 'cool', country: 'USA' },
|
||||
// Canada (2)
|
||||
'BMO Field': { city: 'Toronto, ON', altitude_ft: 250, climate: 'temperate', country: 'Canada' },
|
||||
'BC Place': { city: 'Vancouver, BC', altitude_ft: 33, climate: 'cool', country: 'Canada' },
|
||||
// Mexico (3)
|
||||
'Estadio Azteca': { city: 'Mexico City', altitude_ft: 7349, climate: 'altitude', country: 'Mexico' },
|
||||
'Estadio BBVA': { city: 'Monterrey', altitude_ft: 1765, climate: 'hot', country: 'Mexico' },
|
||||
'Estadio Akron': { city: 'Guadalajara (Zapopan)', altitude_ft: 5138, climate: 'altitude', country: 'Mexico' },
|
||||
});
|
||||
|
||||
// Host-continent teams (CONCACAF) — historically benefit from reduced
|
||||
// travel, fan support, and altitude acclimation. The 2026 hosts (USA,
|
||||
// Canada, Mexico) auto-qualified; the rest qualified through CONCACAF.
|
||||
const CONCACAF_TEAMS = Object.freeze([
|
||||
'USA', 'Canada', 'Mexico', 'Costa Rica', 'Jamaica', 'Honduras',
|
||||
'Panama', 'El Salvador', 'Haiti', 'Trinidad and Tobago',
|
||||
'Guatemala', 'Curacao', 'Suriname',
|
||||
]);
|
||||
|
||||
// CONMEBOL teams travel less than European/African squads and are
|
||||
// historically strong in 2026-style climates. Used as a softer secondary
|
||||
// modifier (not full home-continent advantage).
|
||||
const CONMEBOL_TEAMS = Object.freeze([
|
||||
'Argentina', 'Brazil', 'Uruguay', 'Colombia', 'Ecuador', 'Paraguay',
|
||||
'Peru', 'Chile', 'Venezuela', 'Bolivia',
|
||||
]);
|
||||
|
||||
// Designated penalty takers — primary plus secondary fallback if primary
|
||||
// is off the pitch. Sourced from each team's most recent qualifier or
|
||||
// pre-tournament friendly; updated as confirmations come in.
|
||||
//
|
||||
// Penalty-taker status adds ~0.15 goals per 90 to the player's base rate.
|
||||
// Keys are team names matching football-data.org's `team.name` field;
|
||||
// values are arrays in preference order.
|
||||
const PENALTY_TAKERS = Object.freeze({
|
||||
'Argentina': ['Lionel Messi', 'Lautaro Martínez'],
|
||||
'Brazil': ['Vinicius Junior', 'Neymar'],
|
||||
'France': ['Kylian Mbappé', 'Antoine Griezmann'],
|
||||
'England': ['Harry Kane', 'Bukayo Saka'],
|
||||
'Portugal': ['Cristiano Ronaldo', 'Bruno Fernandes'],
|
||||
'Spain': ['Álvaro Morata', 'Mikel Merino'],
|
||||
'Germany': ['Kai Havertz', 'İlkay Gündoğan'],
|
||||
'Netherlands': ['Memphis Depay', 'Cody Gakpo'],
|
||||
'Belgium': ['Romelu Lukaku', 'Kevin De Bruyne'],
|
||||
'Italy': ['Jorginho', 'Lorenzo Pellegrini'],
|
||||
'Croatia': ['Luka Modrić', 'Andrej Kramarić'],
|
||||
'Uruguay': ['Darwin Núñez', 'Federico Valverde'],
|
||||
'Colombia': ['James Rodríguez', 'Luis Díaz'],
|
||||
'Mexico': ['Raúl Jiménez', 'Hirving Lozano'],
|
||||
'USA': ['Christian Pulisic', 'Folarin Balogun'],
|
||||
'Canada': ['Jonathan David', 'Alphonso Davies'],
|
||||
'Morocco': ['Hakim Ziyech', 'Achraf Hakimi'],
|
||||
'Senegal': ['Sadio Mané', 'Ismaïla Sarr'],
|
||||
'Japan': ['Takefusa Kubo', 'Wataru Endō'],
|
||||
'South Korea': ['Son Heung-min', 'Hwang Hee-chan'],
|
||||
'Australia': ['Jackson Irvine', 'Mitchell Duke'],
|
||||
'Switzerland': ['Granit Xhaka', 'Xherdan Shaqiri'],
|
||||
'Poland': ['Robert Lewandowski', 'Piotr Zieliński'],
|
||||
'Denmark': ['Christian Eriksen', 'Pierre-Emile Højbjerg'],
|
||||
'Serbia': ['Aleksandar Mitrović', 'Dušan Vlahović'],
|
||||
});
|
||||
|
||||
// Designated corner takers (set-piece delivery role). Multi-name arrays
|
||||
// reflect rotation; the first is the most common deliverer. Corner-taker
|
||||
// status meaningfully boosts assist probability for headed goals.
|
||||
const CORNER_TAKERS = Object.freeze({
|
||||
'Argentina': ['Lionel Messi', 'Ángel Di María'],
|
||||
'Brazil': ['Lucas Paquetá', 'Bruno Guimarães'],
|
||||
'France': ['Antoine Griezmann', 'Kylian Mbappé'],
|
||||
'England': ['Bukayo Saka', 'Phil Foden', 'Trent Alexander-Arnold'],
|
||||
'Portugal': ['Bruno Fernandes', 'João Cancelo'],
|
||||
'Spain': ['Dani Olmo', 'Mikel Merino'],
|
||||
'Germany': ['Joshua Kimmich', 'Toni Kroos'],
|
||||
'Netherlands': ['Frenkie de Jong', 'Cody Gakpo'],
|
||||
'Belgium': ['Kevin De Bruyne', 'Yannick Carrasco'],
|
||||
'Italy': ['Lorenzo Pellegrini', 'Federico Chiesa'],
|
||||
'Croatia': ['Luka Modrić', 'Mateo Kovačić'],
|
||||
'Uruguay': ['Federico Valverde', 'Giorgian de Arrascaeta'],
|
||||
'Mexico': ['Andrés Guardado', 'Hirving Lozano'],
|
||||
'USA': ['Christian Pulisic', 'Gio Reyna'],
|
||||
'Canada': ['Stephen Eustáquio', 'Tajon Buchanan'],
|
||||
});
|
||||
|
||||
// Direct free-kick specialists. These players take long-range and
|
||||
// dangerous-area free kicks. Boosts both goal AND assist probability
|
||||
// when a foul is drawn in shooting range.
|
||||
const FREE_KICK_TAKERS = Object.freeze({
|
||||
'Argentina': ['Lionel Messi'],
|
||||
'Brazil': ['Neymar', 'Vinicius Junior'],
|
||||
'France': ['Kylian Mbappé'],
|
||||
'England': ['Trent Alexander-Arnold', 'Bukayo Saka'],
|
||||
'Portugal': ['Cristiano Ronaldo', 'Bruno Fernandes'],
|
||||
'Spain': ['Dani Olmo'],
|
||||
'Germany': ['Joshua Kimmich'],
|
||||
'Belgium': ['Kevin De Bruyne'],
|
||||
'Italy': ['Federico Chiesa'],
|
||||
'Croatia': ['Luka Modrić'],
|
||||
'Colombia': ['James Rodríguez'],
|
||||
'Mexico': ['Raúl Jiménez'],
|
||||
'USA': ['Christian Pulisic'],
|
||||
'Morocco': ['Hakim Ziyech'],
|
||||
'South Korea': ['Son Heung-min'],
|
||||
'Poland': ['Piotr Zieliński'],
|
||||
'Serbia': ['Dušan Tadić'],
|
||||
});
|
||||
|
||||
// "Tournament players" — historical World Cup performers with three or
|
||||
// more career WC goals. These names lift the prior on big-game scoring.
|
||||
// Threshold: >=3 career WC goals OR >=2 in the most recent WC.
|
||||
const TOURNAMENT_PLAYERS = Object.freeze({
|
||||
'Lionel Messi': { wc_goals_career: 13, wc_appearances: 26 },
|
||||
'Cristiano Ronaldo': { wc_goals_career: 8, wc_appearances: 22 },
|
||||
'Kylian Mbappé': { wc_goals_career: 12, wc_appearances: 14 },
|
||||
'Harry Kane': { wc_goals_career: 8, wc_appearances: 11 },
|
||||
'Neymar': { wc_goals_career: 8, wc_appearances: 16 },
|
||||
'Olivier Giroud': { wc_goals_career: 5, wc_appearances: 18 },
|
||||
'Antoine Griezmann': { wc_goals_career: 6, wc_appearances: 17 },
|
||||
'Romelu Lukaku': { wc_goals_career: 5, wc_appearances: 11 },
|
||||
'Luka Modrić': { wc_goals_career: 2, wc_appearances: 18 }, // captain bias
|
||||
'Robert Lewandowski': { wc_goals_career: 2, wc_appearances: 8 },
|
||||
'Karim Benzema': { wc_goals_career: 3, wc_appearances: 11 },
|
||||
'Edinson Cavani': { wc_goals_career: 4, wc_appearances: 14 },
|
||||
'Luis Suárez': { wc_goals_career: 7, wc_appearances: 14 },
|
||||
'Andrés Guardado': { wc_goals_career: 1, wc_appearances: 20 }, // captain bias
|
||||
'Thomas Müller': { wc_goals_career: 10, wc_appearances: 16 },
|
||||
'Eden Hazard': { wc_goals_career: 3, wc_appearances: 11 },
|
||||
'Hirving Lozano': { wc_goals_career: 2, wc_appearances: 7 },
|
||||
'Sadio Mané': { wc_goals_career: 1, wc_appearances: 5 },
|
||||
});
|
||||
|
||||
// Lookup helpers — case-insensitive on player names, exact-match on
|
||||
// team names. Each returns a primitive so the feature extractor can
|
||||
// drop the result straight into the feature vector.
|
||||
|
||||
function isPenaltyTaker(playerName, teamName) {
|
||||
if (!playerName || !teamName) return false;
|
||||
const takers = PENALTY_TAKERS[teamName];
|
||||
if (!Array.isArray(takers)) return false;
|
||||
const p = String(playerName).toLowerCase();
|
||||
return takers.some((t) => t.toLowerCase() === p);
|
||||
}
|
||||
|
||||
function isCornerTaker(playerName, teamName) {
|
||||
if (!playerName || !teamName) return false;
|
||||
const takers = CORNER_TAKERS[teamName];
|
||||
if (!Array.isArray(takers)) return false;
|
||||
const p = String(playerName).toLowerCase();
|
||||
return takers.some((t) => t.toLowerCase() === p);
|
||||
}
|
||||
|
||||
function isFreeKickTaker(playerName, teamName) {
|
||||
if (!playerName || !teamName) return false;
|
||||
const takers = FREE_KICK_TAKERS[teamName];
|
||||
if (!Array.isArray(takers)) return false;
|
||||
const p = String(playerName).toLowerCase();
|
||||
return takers.some((t) => t.toLowerCase() === p);
|
||||
}
|
||||
|
||||
function getTournamentHistory(playerName) {
|
||||
if (!playerName) return null;
|
||||
// Exact match first, then case-insensitive scan.
|
||||
if (TOURNAMENT_PLAYERS[playerName]) return TOURNAMENT_PLAYERS[playerName];
|
||||
const p = String(playerName).toLowerCase();
|
||||
for (const [name, history] of Object.entries(TOURNAMENT_PLAYERS)) {
|
||||
if (name.toLowerCase() === p) return history;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isHomeContinent(teamName) {
|
||||
if (!teamName) return false;
|
||||
return CONCACAF_TEAMS.includes(teamName);
|
||||
}
|
||||
|
||||
function getVenue(venueName) {
|
||||
if (!venueName) return null;
|
||||
return VENUES[venueName] || null;
|
||||
}
|
||||
|
||||
// Classify altitude impact for non-acclimatized teams. The historical
|
||||
// goal-output reduction kicks in around 1,500 ft and gets material above
|
||||
// 4,000 ft (per CSIC studies on player physiology at altitude).
|
||||
function altitudeImpact(altitudeFt) {
|
||||
if (!Number.isFinite(altitudeFt)) return 'none';
|
||||
if (altitudeFt >= 4000) return 'high';
|
||||
if (altitudeFt >= 1500) return 'moderate';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
VENUES,
|
||||
CONCACAF_TEAMS,
|
||||
CONMEBOL_TEAMS,
|
||||
PENALTY_TAKERS,
|
||||
CORNER_TAKERS,
|
||||
FREE_KICK_TAKERS,
|
||||
TOURNAMENT_PLAYERS,
|
||||
isPenaltyTaker,
|
||||
isCornerTaker,
|
||||
isFreeKickTaker,
|
||||
getTournamentHistory,
|
||||
isHomeContinent,
|
||||
getVenue,
|
||||
altitudeImpact,
|
||||
};
|
||||
Reference in New Issue
Block a user