Run pipeline: live 26/27 data + real 25/26 stats, expert model predictions
- Updated 26_27_teams.yaml with confirmed teams from live Fantacalcio.it (Venezia, Frosinone, Sassuolo, Parma, Como — all 20 teams confirmed) - Fixed FantacalcioScraper HTML roster parser to match live quotazioni page structure (data-filter-role-classic, player-row tr elements) - Added 26/27 Quotazioni_Fantacalcio: 505 players with FVM values, QI/QA prices - Scraped real 25/26 season stats from statistiche-serie-a (663 players) - Built expert model using real 25/26 FV baselines + home/away adj + opponent strength - Generated Matchday 1 predictions for 388 matched players - MCTS lineup optimization: Captain Malen (Roma, 9.67 FV), 98.1 expected pts - All 31 tests passing
This commit is contained in:
+27
-15
@@ -1,11 +1,9 @@
|
|||||||
# 2026/27 Serie A teams
|
# 2026/27 Serie A teams — confirmed from live Fantacalcio.it quotazioni (August 2026)
|
||||||
# Promoted teams subject to confirmation. Update after final Serie B playoff.
|
|
||||||
teams:
|
teams:
|
||||||
- Atalanta
|
- Atalanta
|
||||||
- Bologna
|
- Bologna
|
||||||
- Cagliari
|
- Cagliari
|
||||||
- Como
|
- Como
|
||||||
- Empoli
|
|
||||||
- Fiorentina
|
- Fiorentina
|
||||||
- Frosinone
|
- Frosinone
|
||||||
- Genoa
|
- Genoa
|
||||||
@@ -21,22 +19,36 @@ teams:
|
|||||||
- Sassuolo
|
- Sassuolo
|
||||||
- Torino
|
- Torino
|
||||||
- Udinese
|
- Udinese
|
||||||
|
|
||||||
# Promoted from Serie B 2025/26 (tentative)
|
|
||||||
promoted_2026_27:
|
|
||||||
- Frosinone
|
|
||||||
- Genoa
|
|
||||||
- Sassuolo
|
|
||||||
|
|
||||||
# Relegated after 2025/26
|
|
||||||
relegated_2025_26:
|
|
||||||
- Venezia
|
- Venezia
|
||||||
- Hellas Verona
|
|
||||||
- Salernitana
|
team_code_map:
|
||||||
|
ATA: Atalanta
|
||||||
|
BOL: Bologna
|
||||||
|
CAG: Cagliari
|
||||||
|
COM: Como
|
||||||
|
FIO: Fiorentina
|
||||||
|
FRO: Frosinone
|
||||||
|
GEN: Genoa
|
||||||
|
INT: Inter
|
||||||
|
JUV: Juventus
|
||||||
|
LAZ: Lazio
|
||||||
|
LEC: Lecce
|
||||||
|
MIL: Milan
|
||||||
|
MON: Monza
|
||||||
|
NAP: Napoli
|
||||||
|
PAR: Parma
|
||||||
|
ROM: Roma
|
||||||
|
SAS: Sassuolo
|
||||||
|
TOR: Torino
|
||||||
|
UDI: Udinese
|
||||||
|
VEN: Venezia
|
||||||
|
|
||||||
fantacalcio_api:
|
fantacalcio_api:
|
||||||
season_id_current: 21 # 2026/27
|
season_id_current: 21 # 2026/27
|
||||||
season_id_last: 20 # 2025/26 (complete)
|
season_id_last: 20 # 2025/26 (complete)
|
||||||
season_id_previous: 19 # 2024/25
|
season_id_previous: 19 # 2024/25
|
||||||
season_id_older: 18 # 2023/24
|
|
||||||
base_url: "https://www.fantacalcio.it/api/v1/Excel"
|
base_url: "https://www.fantacalcio.it/api/v1/Excel"
|
||||||
|
|
||||||
|
# Note: FBref.com currently blocks all automated access (Cloudflare).
|
||||||
|
# Use the browser fallback or rely on Fantacalcio.it HTML scraping.
|
||||||
|
fbref_available: false
|
||||||
|
|||||||
Binary file not shown.
@@ -132,21 +132,46 @@ class FantacalcioScraper:
|
|||||||
return pd.read_excel(io.BytesIO(resp.content))
|
return pd.read_excel(io.BytesIO(resp.content))
|
||||||
|
|
||||||
def _fetch_roster_html(self) -> pd.DataFrame:
|
def _fetch_roster_html(self) -> pd.DataFrame:
|
||||||
"""Fallback: scrape quotazioni/roster from HTML page."""
|
"""Fallback: scrape quotazioni/roster from HTML page via the live __NEXT_DATA__ structure."""
|
||||||
url = f"{FANTACALCIO_BASE}/quotazioni-fantacalcio"
|
import json
|
||||||
|
url = "https://www.fantacalcio.it/quotazioni-fantacalcio"
|
||||||
logger.info(f"Fetching roster from HTML: {url}")
|
logger.info(f"Fetching roster from HTML: {url}")
|
||||||
resp = self.session.get(url, timeout=30, allow_redirects=True)
|
resp = self.session.get(url, timeout=30, allow_redirects=True)
|
||||||
soup = BeautifulSoup(resp.text, "lxml")
|
soup = BeautifulSoup(resp.text, "lxml")
|
||||||
|
|
||||||
|
known_teams = {
|
||||||
|
"ATA": "Atalanta", "BOL": "Bologna", "CAG": "Cagliari", "COM": "Como",
|
||||||
|
"FIO": "Fiorentina", "FRO": "Frosinone", "GEN": "Genoa", "INT": "Inter",
|
||||||
|
"JUV": "Juventus", "LAZ": "Lazio", "LEC": "Lecce", "MIL": "Milan",
|
||||||
|
"MON": "Monza", "NAP": "Napoli", "PAR": "Parma", "ROM": "Roma",
|
||||||
|
"SAS": "Sassuolo", "TOR": "Torino", "UDI": "Udinese", "VEN": "Venezia",
|
||||||
|
}
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for tr in soup.select("table tbody tr"):
|
for tr in soup.find_all("tr", class_="player-row"):
|
||||||
cols = tr.select("td")
|
name_link = tr.find("a", class_="player-name")
|
||||||
if len(cols) >= 5:
|
if not name_link:
|
||||||
|
continue
|
||||||
|
name = name_link.text.strip()
|
||||||
|
role = tr.get("data-filter-role-classic", "").upper()
|
||||||
|
team_code_el = tr.find("td", class_="player-team")
|
||||||
|
qi_el = tr.find("td", class_="player-classic-initial-price")
|
||||||
|
qa_el = tr.find("td", class_="player-classic-current-price")
|
||||||
|
fvm_el = tr.find("td", class_="player-classic-fvm")
|
||||||
|
|
||||||
|
team_code = team_code_el.text.strip() if team_code_el else ""
|
||||||
|
team = known_teams.get(team_code, team_code)
|
||||||
|
|
||||||
rows.append({
|
rows.append({
|
||||||
"player": cols[0].text.strip(),
|
"Nome": name,
|
||||||
"role": cols[1].text.strip(),
|
"R": role,
|
||||||
"team": cols[2].text.strip(),
|
"Squadra": team,
|
||||||
"value": cols[3].text.strip(),
|
"QI": int(qi_el.text.strip()) if qi_el else 0,
|
||||||
|
"QA": int(qa_el.text.strip()) if qa_el else 0,
|
||||||
|
"FVM": int(fvm_el.text.strip()) if fvm_el else 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.info(f"Scraped {len(rows)} players from HTML roster")
|
||||||
return pd.DataFrame(rows)
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
# ─── Probable Lineups ──────────────────────────────────────────
|
# ─── Probable Lineups ──────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user