datafc 2.6.0__tar.gz → 2.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {datafc-2.6.0 → datafc-2.7.1}/PKG-INFO +137 -8
- {datafc-2.6.0 → datafc-2.7.1}/README.md +135 -6
- {datafc-2.6.0 → datafc-2.7.1}/datafc/__init__.py +7 -1
- datafc-2.7.1/datafc/clubelo/__init__.py +18 -0
- datafc-2.7.1/datafc/clubelo/_client.py +112 -0
- datafc-2.7.1/datafc/clubelo/_parsers.py +40 -0
- datafc-2.7.1/datafc/clubelo/fetch_club_history_data.py +34 -0
- datafc-2.7.1/datafc/clubelo/fetch_daily_ranking_data.py +48 -0
- datafc-2.7.1/datafc/clubelo/fetch_fixtures_data.py +27 -0
- datafc-2.7.1/datafc/eloratings/__init__.py +26 -0
- datafc-2.7.1/datafc/eloratings/_client.py +112 -0
- datafc-2.7.1/datafc/eloratings/_parsers.py +212 -0
- datafc-2.7.1/datafc/eloratings/fetch_country_codes_data.py +28 -0
- datafc-2.7.1/datafc/eloratings/fetch_country_matches_data.py +35 -0
- datafc-2.7.1/datafc/eloratings/fetch_teams_data.py +27 -0
- datafc-2.7.1/datafc/eloratings/fetch_tournament_codes_data.py +28 -0
- datafc-2.7.1/datafc/eloratings/fetch_tournament_editions_data.py +30 -0
- datafc-2.7.1/datafc/eloratings/fetch_tournament_groups_data.py +33 -0
- datafc-2.7.1/datafc/eloratings/fetch_world_ranking_data.py +26 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_formations_data.py +44 -44
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_config.py +67 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/PKG-INFO +137 -8
- {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/SOURCES.txt +16 -0
- {datafc-2.6.0 → datafc-2.7.1}/pyproject.toml +2 -2
- {datafc-2.6.0 → datafc-2.7.1}/LICENSE +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/exceptions.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/__init__.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/_core.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/_parsers.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/aio.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_average_positions_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_coordinates_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_goal_networks_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_incidents_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_league_player_stats_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_lineups_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_details_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_h2h_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_odds_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_match_stats_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_momentum_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_past_matches_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_attribute_overviews_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_career_stats_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_match_log_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_national_team_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_stats_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_player_transfers_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_pregame_form_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_referee_stats_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_search_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_season_rounds_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_seasons_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_shots_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_squad_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_standings_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_substitutions_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_match_history_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_stats_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_team_transfers_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/sofascore/fetch_upcoming_matches_data.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/__init__.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_async_client.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_cache.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_client.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_helpers.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_save_files.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_tournament_info.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc/utils/_validate.py +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/dependency_links.txt +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/requires.txt +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/datafc.egg-info/top_level.txt +0 -0
- {datafc-2.6.0 → datafc-2.7.1}/setup.cfg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datafc
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.1
|
|
4
4
|
Summary: Fetch, process, and export structured football data.
|
|
5
5
|
Author-email: Uraz Akgül <urazdev@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/urazakgul/datafc
|
|
8
8
|
Project-URL: Repository, https://github.com/urazakgul/datafc
|
|
9
9
|
Project-URL: Issues, https://github.com/urazakgul/datafc/issues
|
|
10
|
-
Keywords: football,soccer,data,analytics,sofascore
|
|
10
|
+
Keywords: football,soccer,data,analytics,sofascore,clubelo,eloratings,elo
|
|
11
11
|
Requires-Python: >=3.8
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
@@ -24,11 +24,11 @@ Requires-Dist: pytest>=8.0; extra == "dev"
|
|
|
24
24
|
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
|
-
# datafc v2.
|
|
27
|
+
# datafc v2.7.1
|
|
28
28
|
|
|
29
29
|
## Overview
|
|
30
30
|
|
|
31
|
-
`datafc` fetches, processes, and exports structured football data. It provides **
|
|
31
|
+
`datafc` fetches, processes, and exports structured football data. It provides **45 functions** covering tournament metadata, standings, squad rosters, match fixtures, shots, lineups, player heatmaps, odds, club & national-team Elo ratings, and more — all returning clean `pandas` DataFrames ready for analysis. Sofascore, ClubElo, and eloratings.net are the supported data sources.
|
|
32
32
|
|
|
33
33
|
> **Finding IDs:** `tournament_id` and `season_id` can be discovered two ways:
|
|
34
34
|
> - **From the URL:** navigating to a league page on Sofascore (e.g. `sofascore.com/.../trendyol-super-lig/52#id:63814`) shows `tournament_id=52` and `season_id=63814`.
|
|
@@ -112,6 +112,26 @@ Dynamic: license-file
|
|
|
112
112
|
|---|---|
|
|
113
113
|
| `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
|
|
114
114
|
|
|
115
|
+
### ClubElo
|
|
116
|
+
|
|
117
|
+
| Function | What it returns |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `clubelo.daily_ranking_data` | Full ClubElo ranking for a given calendar day (since 1939) |
|
|
120
|
+
| `clubelo.club_history_data` | Complete Elo rating history for a single club |
|
|
121
|
+
| `clubelo.fixtures_data` | Per-goal-difference and exact-result probabilities for upcoming matches |
|
|
122
|
+
|
|
123
|
+
### EloRatings (national teams)
|
|
124
|
+
|
|
125
|
+
| Function | What it returns |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `eloratings.world_ranking_data` | Current global Elo ranking for all national teams |
|
|
128
|
+
| `eloratings.country_matches_data` | Complete international match history for a national team |
|
|
129
|
+
| `eloratings.country_codes_data` | Country code → English country name reference |
|
|
130
|
+
| `eloratings.tournament_codes_data` | Tournament code → English tournament name reference |
|
|
131
|
+
| `eloratings.tournament_editions_data` | Every tournament edition with start/end dates and page slug |
|
|
132
|
+
| `eloratings.tournament_groups_data` | Specific tournament code → broader group code (e.g. all WC qualifier variants → `WQT`) |
|
|
133
|
+
| `eloratings.teams_data` | Legacy/historical country code → ISO code mapping |
|
|
134
|
+
|
|
115
135
|
> **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
|
|
116
136
|
|
|
117
137
|
## Installation
|
|
@@ -560,17 +580,28 @@ wc_group_df = match_data(
|
|
|
560
580
|
tournament_type="world_cup",
|
|
561
581
|
tournament_stage="group_stage_week",
|
|
562
582
|
)
|
|
583
|
+
|
|
584
|
+
# Domestic league promotion playoff (e.g. TFF 1. Lig, EFL Championship):
|
|
585
|
+
# use season_rounds_data to discover the round_number for each stage.
|
|
586
|
+
playoff_df = match_data(
|
|
587
|
+
tournament_id=98,
|
|
588
|
+
season_id=<season_id>,
|
|
589
|
+
week_number=<round_number>,
|
|
590
|
+
tournament_type="league_playoff",
|
|
591
|
+
tournament_stage="final",
|
|
592
|
+
)
|
|
563
593
|
```
|
|
564
594
|
|
|
565
595
|
Parameters:
|
|
566
596
|
|
|
567
597
|
- `tournament_id` (int)
|
|
568
598
|
- `season_id` (int)
|
|
569
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
570
|
-
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup. `None` assumes a domestic league.
|
|
599
|
+
- `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
|
|
600
|
+
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs. `None` assumes a domestic league.
|
|
571
601
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set.
|
|
572
602
|
- `"uefa"` options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
|
|
573
603
|
- `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
|
|
604
|
+
- `"league_playoff"` options: `quarterfinals`, `semifinals`, `final`.
|
|
574
605
|
|
|
575
606
|
Columns: `country`, `tournament`, `season`, `week`, `game_id`, `home_team`, `home_team_id`, `away_team`, `away_team_id`, `injury_time_1`, `injury_time_2`, `start_timestamp`, `status`, `home_score_current`, `home_score_display`, `home_score_period1`, `home_score_period2`, `home_score_normaltime`, `away_score_current`, `away_score_display`, `away_score_period1`, `away_score_period2`, `away_score_normaltime`.
|
|
576
607
|
|
|
@@ -823,8 +854,8 @@ Parameters:
|
|
|
823
854
|
|
|
824
855
|
- `tournament_id` (int)
|
|
825
856
|
- `season_id` (int)
|
|
826
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
827
|
-
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup.
|
|
857
|
+
- `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
|
|
858
|
+
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs.
|
|
828
859
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
|
|
829
860
|
|
|
830
861
|
Same columns as `match_data`.
|
|
@@ -961,8 +992,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
|
|
|
961
992
|
|
|
962
993
|
---
|
|
963
994
|
|
|
995
|
+
### ClubElo
|
|
996
|
+
|
|
997
|
+
ClubElo (<https://clubelo.com>) maintains daily Elo ratings for European clubs going back to 1939. Three functions wrap the public API; all return `pandas` DataFrames.
|
|
998
|
+
|
|
999
|
+
```python
|
|
1000
|
+
from datafc import clubelo
|
|
1001
|
+
|
|
1002
|
+
# Full ranking on a given day
|
|
1003
|
+
df = clubelo.daily_ranking_data("2026-06-10")
|
|
1004
|
+
|
|
1005
|
+
# A single club's full Elo history (use the spelling shown in daily_ranking_data)
|
|
1006
|
+
hist = clubelo.club_history_data("Man City")
|
|
1007
|
+
|
|
1008
|
+
# Upcoming matches with per-goal-difference and exact-result probabilities
|
|
1009
|
+
fix = clubelo.fixtures_data()
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
|
|
1013
|
+
|
|
1014
|
+
> **Notes:**
|
|
1015
|
+
> - Club names must match ClubElo's spelling exactly (e.g. `"Man City"`, `"Real Madrid"`, `"Paris SG"`). Run `daily_ranking_data` first and check the `club` column.
|
|
1016
|
+
> - Values before 1960 are considered provisional by ClubElo.
|
|
1017
|
+
> - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
|
|
1018
|
+
|
|
1019
|
+
If the default `https://api.clubelo.com` is unreachable from your network (some corporate firewalls block port 443 to this host while leaving port 80 open), override the base URL once at startup:
|
|
1020
|
+
|
|
1021
|
+
```python
|
|
1022
|
+
from datafc import clubelo
|
|
1023
|
+
clubelo.set_clubelo_base_url("http://api.clubelo.com")
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
`clubelo.reset_clubelo_base_url()` restores the default.
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
### EloRatings (national teams)
|
|
1031
|
+
|
|
1032
|
+
[eloratings.net](https://www.eloratings.net) maintains daily Elo ratings for national teams going back to 1872. Seven functions wrap the site's underlying `.tsv` data files; all return `pandas` DataFrames.
|
|
1033
|
+
|
|
1034
|
+
```python
|
|
1035
|
+
from datafc import eloratings
|
|
1036
|
+
|
|
1037
|
+
# Current global ranking of all national teams (30 columns)
|
|
1038
|
+
df = eloratings.world_ranking_data()
|
|
1039
|
+
|
|
1040
|
+
# Full match history for a single national team
|
|
1041
|
+
matches = eloratings.country_matches_data("Spain")
|
|
1042
|
+
|
|
1043
|
+
# Code reference tables (handy for joining onto the above)
|
|
1044
|
+
countries = eloratings.country_codes_data() # country_code → country_name
|
|
1045
|
+
tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
|
|
1046
|
+
groups = eloratings.tournament_groups_data() # specific code → group code (long format)
|
|
1047
|
+
editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
|
|
1048
|
+
|
|
1049
|
+
# Legacy/historical country code → ISO code mapping
|
|
1050
|
+
teams = eloratings.teams_data()
|
|
1051
|
+
|
|
1052
|
+
# Example: enrich Spain's matches with full tournament names
|
|
1053
|
+
spain = (
|
|
1054
|
+
eloratings.country_matches_data("Spain")
|
|
1055
|
+
.merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# Example: every World Cup final tournament (filter editions on the WC group)
|
|
1059
|
+
wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
|
|
1063
|
+
|
|
1064
|
+
> **Column names are verified against the site's JavaScript source.**
|
|
1065
|
+
> eloratings.net does not publish a documented API; the `.tsv` files have no header row. Column names in `world_ranking_data` are derived from index→field assignments in `scripts/ratings.js`; the remaining files are cross-checked against the live site's display. The ``WORLD_COLUMNS`` / ``COUNTRY_MATCH_COLUMNS`` lists in `datafc/eloratings/_parsers.py` document each position. If the site changes its layout, columns may be silently mis-labelled — please open an issue if you spot a discrepancy.
|
|
1066
|
+
|
|
1067
|
+
> **Country argument is a page slug, not an ISO code.**
|
|
1068
|
+
> `country_matches_data("Spain")` calls `https://www.eloratings.net/Spain.tsv`. Use the same capitalisation and spelling shown in the URL of the country page on eloratings.net (e.g. `"Czech_Republic"`, `"United_States"`).
|
|
1069
|
+
|
|
1070
|
+
Override the base URL the same way as ClubElo if needed:
|
|
1071
|
+
|
|
1072
|
+
```python
|
|
1073
|
+
from datafc import eloratings
|
|
1074
|
+
eloratings.set_eloratings_base_url("http://www.eloratings.net")
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
964
1079
|
## What's new
|
|
965
1080
|
|
|
1081
|
+
### v2.7.1
|
|
1082
|
+
|
|
1083
|
+
- **New `tournament_type="league_playoff"`** for `match_data` and `past_matches_data`. Covers domestic promotion playoffs (e.g. TFF 1. Lig, EFL Championship) with `quarterfinals`, `semifinals`, and `final` stages. Use `season_rounds_data` to discover the `week_number` for each stage.
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
### v2.7.0
|
|
1088
|
+
|
|
1089
|
+
- **Club Elo ratings via ClubElo.** Pull daily Elo rankings of European clubs (going back to 1939), a single club's full rating history, and probability-based predictions for upcoming matches — all as `pandas` DataFrames. Three functions under `datafc.clubelo`: `daily_ranking_data`, `club_history_data`, `fixtures_data`.
|
|
1090
|
+
- **National-team Elo ratings via eloratings.net.** Pull the current global ranking of every national team, a country's full international match history (back to 1872), and the site's reference tables for resolving the two-letter country codes and tournament codes into full names. Seven functions under `datafc.eloratings`, including `world_ranking_data`, `country_matches_data`, `country_codes_data`, and `tournament_codes_data`.
|
|
1091
|
+
- **Shared caching across all sources.** ClubElo and eloratings.net responses use the same `DiskCache` you already pass to the Sofascore functions, so a single cache directory keeps every fetch persistent across sessions.
|
|
1092
|
+
|
|
1093
|
+
---
|
|
1094
|
+
|
|
966
1095
|
### v2.6.0
|
|
967
1096
|
|
|
968
1097
|
- **New function: `formations_data`** — fetches home and away formation for each match. Returns two rows per match (`team`: `home` / `away`) with the formation string (e.g. `4-3-3`).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# datafc v2.
|
|
1
|
+
# datafc v2.7.1
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
`datafc` fetches, processes, and exports structured football data. It provides **
|
|
5
|
+
`datafc` fetches, processes, and exports structured football data. It provides **45 functions** covering tournament metadata, standings, squad rosters, match fixtures, shots, lineups, player heatmaps, odds, club & national-team Elo ratings, and more — all returning clean `pandas` DataFrames ready for analysis. Sofascore, ClubElo, and eloratings.net are the supported data sources.
|
|
6
6
|
|
|
7
7
|
> **Finding IDs:** `tournament_id` and `season_id` can be discovered two ways:
|
|
8
8
|
> - **From the URL:** navigating to a league page on Sofascore (e.g. `sofascore.com/.../trendyol-super-lig/52#id:63814`) shows `tournament_id=52` and `season_id=63814`.
|
|
@@ -86,6 +86,26 @@
|
|
|
86
86
|
|---|---|
|
|
87
87
|
| `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
|
|
88
88
|
|
|
89
|
+
### ClubElo
|
|
90
|
+
|
|
91
|
+
| Function | What it returns |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `clubelo.daily_ranking_data` | Full ClubElo ranking for a given calendar day (since 1939) |
|
|
94
|
+
| `clubelo.club_history_data` | Complete Elo rating history for a single club |
|
|
95
|
+
| `clubelo.fixtures_data` | Per-goal-difference and exact-result probabilities for upcoming matches |
|
|
96
|
+
|
|
97
|
+
### EloRatings (national teams)
|
|
98
|
+
|
|
99
|
+
| Function | What it returns |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `eloratings.world_ranking_data` | Current global Elo ranking for all national teams |
|
|
102
|
+
| `eloratings.country_matches_data` | Complete international match history for a national team |
|
|
103
|
+
| `eloratings.country_codes_data` | Country code → English country name reference |
|
|
104
|
+
| `eloratings.tournament_codes_data` | Tournament code → English tournament name reference |
|
|
105
|
+
| `eloratings.tournament_editions_data` | Every tournament edition with start/end dates and page slug |
|
|
106
|
+
| `eloratings.tournament_groups_data` | Specific tournament code → broader group code (e.g. all WC qualifier variants → `WQT`) |
|
|
107
|
+
| `eloratings.teams_data` | Legacy/historical country code → ISO code mapping |
|
|
108
|
+
|
|
89
109
|
> **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
|
|
90
110
|
|
|
91
111
|
## Installation
|
|
@@ -534,17 +554,28 @@ wc_group_df = match_data(
|
|
|
534
554
|
tournament_type="world_cup",
|
|
535
555
|
tournament_stage="group_stage_week",
|
|
536
556
|
)
|
|
557
|
+
|
|
558
|
+
# Domestic league promotion playoff (e.g. TFF 1. Lig, EFL Championship):
|
|
559
|
+
# use season_rounds_data to discover the round_number for each stage.
|
|
560
|
+
playoff_df = match_data(
|
|
561
|
+
tournament_id=98,
|
|
562
|
+
season_id=<season_id>,
|
|
563
|
+
week_number=<round_number>,
|
|
564
|
+
tournament_type="league_playoff",
|
|
565
|
+
tournament_stage="final",
|
|
566
|
+
)
|
|
537
567
|
```
|
|
538
568
|
|
|
539
569
|
Parameters:
|
|
540
570
|
|
|
541
571
|
- `tournament_id` (int)
|
|
542
572
|
- `season_id` (int)
|
|
543
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
544
|
-
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup. `None` assumes a domestic league.
|
|
573
|
+
- `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
|
|
574
|
+
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs. `None` assumes a domestic league.
|
|
545
575
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set.
|
|
546
576
|
- `"uefa"` options: `preliminary_semifinals`, `preliminary_final`, `qualification_round`, `qualification_playoff`, `group_stage_week`, `playoff_round`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
|
|
547
577
|
- `"world_cup"` options: `group_stage_week`, `round_of_32`, `round_of_16`, `quarterfinals`, `semifinals`, `match_for_3rd_place`, `final`.
|
|
578
|
+
- `"league_playoff"` options: `quarterfinals`, `semifinals`, `final`.
|
|
548
579
|
|
|
549
580
|
Columns: `country`, `tournament`, `season`, `week`, `game_id`, `home_team`, `home_team_id`, `away_team`, `away_team_id`, `injury_time_1`, `injury_time_2`, `start_timestamp`, `status`, `home_score_current`, `home_score_display`, `home_score_period1`, `home_score_period2`, `home_score_normaltime`, `away_score_current`, `away_score_display`, `away_score_period1`, `away_score_period2`, `away_score_normaltime`.
|
|
550
581
|
|
|
@@ -797,8 +828,8 @@ Parameters:
|
|
|
797
828
|
|
|
798
829
|
- `tournament_id` (int)
|
|
799
830
|
- `season_id` (int)
|
|
800
|
-
- `week_number` (int, optional): Required for league rounds, UEFA stages,
|
|
801
|
-
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup.
|
|
831
|
+
- `week_number` (int, optional): Required for league rounds, UEFA stages, `world_cup` + `group_stage_week`, and all `league_playoff` stages. Not needed for other `world_cup` stages.
|
|
832
|
+
- `tournament_type` (str, optional): `"uefa"` for UEFA competitions, `"world_cup"` for FIFA World Cup, `"league_playoff"` for domestic promotion playoffs.
|
|
802
833
|
- `tournament_stage` (str, optional): Required when `tournament_type` is set. Same options as `match_data`.
|
|
803
834
|
|
|
804
835
|
Same columns as `match_data`.
|
|
@@ -935,8 +966,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
|
|
|
935
966
|
|
|
936
967
|
---
|
|
937
968
|
|
|
969
|
+
### ClubElo
|
|
970
|
+
|
|
971
|
+
ClubElo (<https://clubelo.com>) maintains daily Elo ratings for European clubs going back to 1939. Three functions wrap the public API; all return `pandas` DataFrames.
|
|
972
|
+
|
|
973
|
+
```python
|
|
974
|
+
from datafc import clubelo
|
|
975
|
+
|
|
976
|
+
# Full ranking on a given day
|
|
977
|
+
df = clubelo.daily_ranking_data("2026-06-10")
|
|
978
|
+
|
|
979
|
+
# A single club's full Elo history (use the spelling shown in daily_ranking_data)
|
|
980
|
+
hist = clubelo.club_history_data("Man City")
|
|
981
|
+
|
|
982
|
+
# Upcoming matches with per-goal-difference and exact-result probabilities
|
|
983
|
+
fix = clubelo.fixtures_data()
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
|
|
987
|
+
|
|
988
|
+
> **Notes:**
|
|
989
|
+
> - Club names must match ClubElo's spelling exactly (e.g. `"Man City"`, `"Real Madrid"`, `"Paris SG"`). Run `daily_ranking_data` first and check the `club` column.
|
|
990
|
+
> - Values before 1960 are considered provisional by ClubElo.
|
|
991
|
+
> - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
|
|
992
|
+
|
|
993
|
+
If the default `https://api.clubelo.com` is unreachable from your network (some corporate firewalls block port 443 to this host while leaving port 80 open), override the base URL once at startup:
|
|
994
|
+
|
|
995
|
+
```python
|
|
996
|
+
from datafc import clubelo
|
|
997
|
+
clubelo.set_clubelo_base_url("http://api.clubelo.com")
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
`clubelo.reset_clubelo_base_url()` restores the default.
|
|
1001
|
+
|
|
1002
|
+
---
|
|
1003
|
+
|
|
1004
|
+
### EloRatings (national teams)
|
|
1005
|
+
|
|
1006
|
+
[eloratings.net](https://www.eloratings.net) maintains daily Elo ratings for national teams going back to 1872. Seven functions wrap the site's underlying `.tsv` data files; all return `pandas` DataFrames.
|
|
1007
|
+
|
|
1008
|
+
```python
|
|
1009
|
+
from datafc import eloratings
|
|
1010
|
+
|
|
1011
|
+
# Current global ranking of all national teams (30 columns)
|
|
1012
|
+
df = eloratings.world_ranking_data()
|
|
1013
|
+
|
|
1014
|
+
# Full match history for a single national team
|
|
1015
|
+
matches = eloratings.country_matches_data("Spain")
|
|
1016
|
+
|
|
1017
|
+
# Code reference tables (handy for joining onto the above)
|
|
1018
|
+
countries = eloratings.country_codes_data() # country_code → country_name
|
|
1019
|
+
tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
|
|
1020
|
+
groups = eloratings.tournament_groups_data() # specific code → group code (long format)
|
|
1021
|
+
editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
|
|
1022
|
+
|
|
1023
|
+
# Legacy/historical country code → ISO code mapping
|
|
1024
|
+
teams = eloratings.teams_data()
|
|
1025
|
+
|
|
1026
|
+
# Example: enrich Spain's matches with full tournament names
|
|
1027
|
+
spain = (
|
|
1028
|
+
eloratings.country_matches_data("Spain")
|
|
1029
|
+
.merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
# Example: every World Cup final tournament (filter editions on the WC group)
|
|
1033
|
+
wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
|
|
1037
|
+
|
|
1038
|
+
> **Column names are verified against the site's JavaScript source.**
|
|
1039
|
+
> eloratings.net does not publish a documented API; the `.tsv` files have no header row. Column names in `world_ranking_data` are derived from index→field assignments in `scripts/ratings.js`; the remaining files are cross-checked against the live site's display. The ``WORLD_COLUMNS`` / ``COUNTRY_MATCH_COLUMNS`` lists in `datafc/eloratings/_parsers.py` document each position. If the site changes its layout, columns may be silently mis-labelled — please open an issue if you spot a discrepancy.
|
|
1040
|
+
|
|
1041
|
+
> **Country argument is a page slug, not an ISO code.**
|
|
1042
|
+
> `country_matches_data("Spain")` calls `https://www.eloratings.net/Spain.tsv`. Use the same capitalisation and spelling shown in the URL of the country page on eloratings.net (e.g. `"Czech_Republic"`, `"United_States"`).
|
|
1043
|
+
|
|
1044
|
+
Override the base URL the same way as ClubElo if needed:
|
|
1045
|
+
|
|
1046
|
+
```python
|
|
1047
|
+
from datafc import eloratings
|
|
1048
|
+
eloratings.set_eloratings_base_url("http://www.eloratings.net")
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
---
|
|
1052
|
+
|
|
938
1053
|
## What's new
|
|
939
1054
|
|
|
1055
|
+
### v2.7.1
|
|
1056
|
+
|
|
1057
|
+
- **New `tournament_type="league_playoff"`** for `match_data` and `past_matches_data`. Covers domestic promotion playoffs (e.g. TFF 1. Lig, EFL Championship) with `quarterfinals`, `semifinals`, and `final` stages. Use `season_rounds_data` to discover the `week_number` for each stage.
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
### v2.7.0
|
|
1062
|
+
|
|
1063
|
+
- **Club Elo ratings via ClubElo.** Pull daily Elo rankings of European clubs (going back to 1939), a single club's full rating history, and probability-based predictions for upcoming matches — all as `pandas` DataFrames. Three functions under `datafc.clubelo`: `daily_ranking_data`, `club_history_data`, `fixtures_data`.
|
|
1064
|
+
- **National-team Elo ratings via eloratings.net.** Pull the current global ranking of every national team, a country's full international match history (back to 1872), and the site's reference tables for resolving the two-letter country codes and tournament codes into full names. Seven functions under `datafc.eloratings`, including `world_ranking_data`, `country_matches_data`, `country_codes_data`, and `tournament_codes_data`.
|
|
1065
|
+
- **Shared caching across all sources.** ClubElo and eloratings.net responses use the same `DiskCache` you already pass to the Sofascore functions, so a single cache directory keeps every fetch persistent across sessions.
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
940
1069
|
### v2.6.0
|
|
941
1070
|
|
|
942
1071
|
- **New function: `formations_data`** — fetches home and away formation for each match. Returns two rows per match (`team`: `home` / `away`) with the formation string (e.g. `4-3-3`).
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.7.1"
|
|
2
2
|
|
|
3
3
|
from .sofascore import *
|
|
4
|
+
from . import clubelo
|
|
5
|
+
from . import eloratings
|
|
4
6
|
from .exceptions import (
|
|
5
7
|
DataFCError,
|
|
6
8
|
InvalidParameterError,
|
|
@@ -61,6 +63,10 @@ __all__ = [
|
|
|
61
63
|
"referee_stats_data",
|
|
62
64
|
# Async API
|
|
63
65
|
"aio",
|
|
66
|
+
# ClubElo subpackage
|
|
67
|
+
"clubelo",
|
|
68
|
+
# EloRatings subpackage
|
|
69
|
+
"eloratings",
|
|
64
70
|
# Exceptions
|
|
65
71
|
"DataFCError",
|
|
66
72
|
"InvalidParameterError",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from datafc.utils._config import (
|
|
2
|
+
get_clubelo_base_url,
|
|
3
|
+
reset_clubelo_base_url,
|
|
4
|
+
set_clubelo_base_url,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .fetch_club_history_data import club_history_data
|
|
8
|
+
from .fetch_daily_ranking_data import daily_ranking_data
|
|
9
|
+
from .fetch_fixtures_data import fixtures_data
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"daily_ranking_data",
|
|
13
|
+
"club_history_data",
|
|
14
|
+
"fixtures_data",
|
|
15
|
+
"get_clubelo_base_url",
|
|
16
|
+
"set_clubelo_base_url",
|
|
17
|
+
"reset_clubelo_base_url",
|
|
18
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from curl_cffi import requests as cf_requests
|
|
7
|
+
|
|
8
|
+
from datafc.exceptions import APIError, RateLimitError, ServerError
|
|
9
|
+
from datafc.utils._cache import get_default_cache
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClubEloClient:
|
|
15
|
+
"""
|
|
16
|
+
HTTP client for the ClubElo public API. Responses are CSV text.
|
|
17
|
+
|
|
18
|
+
Cached payloads are stored as ``{"csv": "<text>"}`` so they share the same
|
|
19
|
+
DiskCache backend used by SofascoreClient.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
rate_limit: Maximum requests per second. Defaults to 2.0.
|
|
23
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
24
|
+
retries: Number of retry attempts on transient errors. Defaults to 3.
|
|
25
|
+
cache: Optional DiskCache. Falls back to the module-level default cache.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_class_lock: threading.Lock = threading.Lock()
|
|
29
|
+
_class_last_request_time: float = 0.0
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
rate_limit: float = 2.0,
|
|
34
|
+
timeout: int = 30,
|
|
35
|
+
retries: int = 3,
|
|
36
|
+
cache=None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._min_interval = 1.0 / rate_limit if rate_limit > 0 else 0.0
|
|
39
|
+
self._timeout = timeout
|
|
40
|
+
self._retries = retries
|
|
41
|
+
self._cache = cache if cache is not None else get_default_cache()
|
|
42
|
+
self._session = cf_requests.Session(impersonate="chrome124")
|
|
43
|
+
|
|
44
|
+
def _rate_limit_wait(self) -> None:
|
|
45
|
+
with ClubEloClient._class_lock:
|
|
46
|
+
elapsed = time.monotonic() - ClubEloClient._class_last_request_time
|
|
47
|
+
if elapsed < self._min_interval:
|
|
48
|
+
time.sleep(self._min_interval - elapsed)
|
|
49
|
+
ClubEloClient._class_last_request_time = time.monotonic()
|
|
50
|
+
|
|
51
|
+
def get(self, url: str) -> str:
|
|
52
|
+
"""Perform a GET, return the response body as CSV text."""
|
|
53
|
+
if self._cache is not None:
|
|
54
|
+
cached = self._cache.get(url)
|
|
55
|
+
if isinstance(cached, dict) and "csv" in cached:
|
|
56
|
+
logger.debug("Cache hit: %s", url)
|
|
57
|
+
return cached["csv"]
|
|
58
|
+
|
|
59
|
+
last_exc: Optional[Exception] = None
|
|
60
|
+
|
|
61
|
+
for attempt in range(1, self._retries + 1):
|
|
62
|
+
self._rate_limit_wait()
|
|
63
|
+
try:
|
|
64
|
+
response = self._session.get(url, timeout=self._timeout)
|
|
65
|
+
|
|
66
|
+
if response.status_code == 200:
|
|
67
|
+
text = response.text
|
|
68
|
+
if self._cache is not None:
|
|
69
|
+
self._cache.set(url, {"csv": text})
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
if response.status_code == 429:
|
|
73
|
+
wait = 2 ** attempt
|
|
74
|
+
logger.warning(
|
|
75
|
+
"Rate limited (429). Waiting %ds before retry %d/%d. URL: %s",
|
|
76
|
+
wait, attempt, self._retries, url,
|
|
77
|
+
)
|
|
78
|
+
time.sleep(wait)
|
|
79
|
+
last_exc = RateLimitError(429, url)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if response.status_code in (500, 502, 503, 504):
|
|
83
|
+
wait = 2 ** attempt
|
|
84
|
+
logger.warning(
|
|
85
|
+
"Server error %d. Waiting %ds before retry %d/%d. URL: %s",
|
|
86
|
+
response.status_code, wait, attempt, self._retries, url,
|
|
87
|
+
)
|
|
88
|
+
time.sleep(wait)
|
|
89
|
+
last_exc = ServerError(response.status_code, url)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
raise APIError(response.status_code, url)
|
|
93
|
+
|
|
94
|
+
except (APIError, RateLimitError, ServerError):
|
|
95
|
+
raise
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.warning("Request failed (attempt %d/%d): %s", attempt, self._retries, exc)
|
|
98
|
+
last_exc = exc
|
|
99
|
+
time.sleep(2 ** attempt)
|
|
100
|
+
|
|
101
|
+
if isinstance(last_exc, (RateLimitError, ServerError, APIError)):
|
|
102
|
+
raise last_exc
|
|
103
|
+
raise APIError(0, url, f"All {self._retries} attempts failed") from last_exc
|
|
104
|
+
|
|
105
|
+
def close(self) -> None:
|
|
106
|
+
self._session.close()
|
|
107
|
+
|
|
108
|
+
def __enter__(self) -> "ClubEloClient":
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def __exit__(self, *_) -> None:
|
|
112
|
+
self.close()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from datafc.exceptions import DataNotAvailableError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _read_csv(text: str, *, context: str) -> pd.DataFrame:
|
|
9
|
+
if not text or not text.strip():
|
|
10
|
+
raise DataNotAvailableError(f"Empty response from ClubElo for {context}.")
|
|
11
|
+
df = pd.read_csv(StringIO(text))
|
|
12
|
+
if df.empty:
|
|
13
|
+
raise DataNotAvailableError(f"No rows returned from ClubElo for {context}.")
|
|
14
|
+
return df
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
18
|
+
df.columns = [str(c).strip().lower() for c in df.columns]
|
|
19
|
+
return df
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_daily_ranking(text: str, date: str) -> pd.DataFrame:
|
|
23
|
+
"""Columns: rank, club, country, level, elo, from, to."""
|
|
24
|
+
df = _normalize_columns(_read_csv(text, context=f"date={date}"))
|
|
25
|
+
df.insert(0, "query_date", date)
|
|
26
|
+
return df
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_club_history(text: str, club: str) -> pd.DataFrame:
|
|
30
|
+
"""Columns: rank, club, country, level, elo, from, to (rank may be 'None')."""
|
|
31
|
+
df = _normalize_columns(_read_csv(text, context=f"club={club}"))
|
|
32
|
+
if "rank" in df.columns:
|
|
33
|
+
df["rank"] = pd.to_numeric(df["rank"], errors="coerce")
|
|
34
|
+
df.insert(0, "query_club", club)
|
|
35
|
+
return df
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_fixtures(text: str) -> pd.DataFrame:
|
|
39
|
+
"""Upcoming fixtures with per-goal-difference and exact-result probabilities."""
|
|
40
|
+
return _normalize_columns(_read_csv(text, context="fixtures"))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from datafc.clubelo._client import ClubEloClient
|
|
6
|
+
from datafc.clubelo._parsers import parse_club_history
|
|
7
|
+
from datafc.exceptions import InvalidParameterError
|
|
8
|
+
from datafc.utils._config import get_clubelo_base_url
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from datafc.utils._cache import DiskCache
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def club_history_data(
|
|
15
|
+
club: str,
|
|
16
|
+
rate_limit: float = 2.0,
|
|
17
|
+
cache: Optional["DiskCache"] = None,
|
|
18
|
+
) -> pd.DataFrame:
|
|
19
|
+
"""Fetches the full Elo rating history for one club.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
club: Club name exactly as ClubElo spells it (use a daily ranking to
|
|
23
|
+
confirm the spelling, e.g. ``"Barcelona"``, ``"ManCity"``).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
DataFrame with columns: ``query_club, rank, club, country, level, elo, from, to``.
|
|
27
|
+
"""
|
|
28
|
+
if not isinstance(club, str) or not club.strip():
|
|
29
|
+
raise InvalidParameterError("club must be a non-empty string.")
|
|
30
|
+
club_clean = club.strip()
|
|
31
|
+
url = f"{get_clubelo_base_url()}/{club_clean}"
|
|
32
|
+
with ClubEloClient(rate_limit=rate_limit, cache=cache) as client:
|
|
33
|
+
text = client.get(url)
|
|
34
|
+
return parse_club_history(text, club_clean)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from datetime import date as _date_cls
|
|
2
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from datafc.clubelo._client import ClubEloClient
|
|
7
|
+
from datafc.clubelo._parsers import parse_daily_ranking
|
|
8
|
+
from datafc.exceptions import InvalidParameterError
|
|
9
|
+
from datafc.utils._config import get_clubelo_base_url
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from datafc.utils._cache import DiskCache
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _coerce_date(value: Union[str, _date_cls]) -> str:
|
|
16
|
+
if isinstance(value, _date_cls):
|
|
17
|
+
return value.strftime("%Y-%m-%d")
|
|
18
|
+
if isinstance(value, str):
|
|
19
|
+
try:
|
|
20
|
+
return _date_cls.fromisoformat(value).strftime("%Y-%m-%d")
|
|
21
|
+
except ValueError as exc:
|
|
22
|
+
raise InvalidParameterError(
|
|
23
|
+
f"date must be 'YYYY-MM-DD' or a datetime.date; got {value!r}."
|
|
24
|
+
) from exc
|
|
25
|
+
raise InvalidParameterError(
|
|
26
|
+
f"date must be a string or datetime.date; got {type(value).__name__}."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def daily_ranking_data(
|
|
31
|
+
date: Union[str, _date_cls],
|
|
32
|
+
rate_limit: float = 2.0,
|
|
33
|
+
cache: Optional["DiskCache"] = None,
|
|
34
|
+
) -> pd.DataFrame:
|
|
35
|
+
"""Fetches the full ClubElo ranking for the given calendar day.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
date: 'YYYY-MM-DD' string or ``datetime.date``. Values before 1960 are
|
|
39
|
+
considered provisional by ClubElo.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
DataFrame with columns: ``query_date, rank, club, country, level, elo, from, to``.
|
|
43
|
+
"""
|
|
44
|
+
date_str = _coerce_date(date)
|
|
45
|
+
url = f"{get_clubelo_base_url()}/{date_str}"
|
|
46
|
+
with ClubEloClient(rate_limit=rate_limit, cache=cache) as client:
|
|
47
|
+
text = client.get(url)
|
|
48
|
+
return parse_daily_ranking(text, date_str)
|