datafc 2.5.0__tar.gz → 2.7.0__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.5.0 → datafc-2.7.0}/PKG-INFO +150 -4
- {datafc-2.5.0 → datafc-2.7.0}/README.md +148 -2
- {datafc-2.5.0 → datafc-2.7.0}/datafc/__init__.py +8 -1
- datafc-2.7.0/datafc/clubelo/__init__.py +18 -0
- datafc-2.7.0/datafc/clubelo/_client.py +112 -0
- datafc-2.7.0/datafc/clubelo/_parsers.py +40 -0
- datafc-2.7.0/datafc/clubelo/fetch_club_history_data.py +34 -0
- datafc-2.7.0/datafc/clubelo/fetch_daily_ranking_data.py +48 -0
- datafc-2.7.0/datafc/clubelo/fetch_fixtures_data.py +27 -0
- datafc-2.7.0/datafc/eloratings/__init__.py +26 -0
- datafc-2.7.0/datafc/eloratings/_client.py +112 -0
- datafc-2.7.0/datafc/eloratings/_parsers.py +212 -0
- datafc-2.7.0/datafc/eloratings/fetch_country_codes_data.py +28 -0
- datafc-2.7.0/datafc/eloratings/fetch_country_matches_data.py +35 -0
- datafc-2.7.0/datafc/eloratings/fetch_teams_data.py +27 -0
- datafc-2.7.0/datafc/eloratings/fetch_tournament_codes_data.py +28 -0
- datafc-2.7.0/datafc/eloratings/fetch_tournament_editions_data.py +30 -0
- datafc-2.7.0/datafc/eloratings/fetch_tournament_groups_data.py +33 -0
- datafc-2.7.0/datafc/eloratings/fetch_world_ranking_data.py +26 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/__init__.py +2 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/_parsers.py +18 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/aio.py +23 -0
- datafc-2.7.0/datafc/sofascore/fetch_formations_data.py +44 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_config.py +62 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/PKG-INFO +150 -4
- {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/SOURCES.txt +17 -0
- {datafc-2.5.0 → datafc-2.7.0}/pyproject.toml +2 -2
- {datafc-2.5.0 → datafc-2.7.0}/LICENSE +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/exceptions.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/_core.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_average_positions_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_coordinates_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_goal_networks_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_incidents_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_league_player_stats_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_lineups_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_details_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_h2h_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_odds_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_match_stats_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_momentum_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_past_matches_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_attribute_overviews_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_career_stats_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_match_log_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_national_team_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_stats_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_player_transfers_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_pregame_form_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_referee_stats_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_search_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_season_rounds_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_seasons_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_shots_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_squad_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_standings_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_substitutions_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_match_history_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_stats_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_team_transfers_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/sofascore/fetch_upcoming_matches_data.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/__init__.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_async_client.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_cache.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_client.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_helpers.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_save_files.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_tournament_info.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc/utils/_validate.py +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/dependency_links.txt +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/requires.txt +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/datafc.egg-info/top_level.txt +0 -0
- {datafc-2.5.0 → datafc-2.7.0}/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.0
|
|
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,16 +24,27 @@ 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.0
|
|
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`.
|
|
35
35
|
> - **Programmatically:** use `search_data("super lig", entity_type="tournament")` to find the tournament ID, then `seasons_data(tournament_id)` to list all available seasons.
|
|
36
36
|
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
If you find <strong>datafc</strong> useful, consider supporting the project ☕<br><br>
|
|
41
|
+
<a href="https://buymeacoffee.com/urazdev">
|
|
42
|
+
<img src="https://img.shields.io/badge/Buy%20Me%20A%20Coffee-urazdev-%23FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee">
|
|
43
|
+
</a>
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
37
48
|
## What Data Can You Access?
|
|
38
49
|
|
|
39
50
|
### Discovery
|
|
@@ -69,6 +80,7 @@ Dynamic: license-file
|
|
|
69
80
|
|---|---|
|
|
70
81
|
| `match_data` | Fixtures for a matchweek: score, status, home/away teams |
|
|
71
82
|
| `match_details_data` | Referee info (name, cards, games) and venue details per match |
|
|
83
|
+
| `formations_data` | Home and away formation for each match |
|
|
72
84
|
| `match_stats_data` | Aggregate team stats per match (possession, shots, fouls, …) |
|
|
73
85
|
| `match_odds_data` | Pre-match 1/X/2 betting odds |
|
|
74
86
|
| `match_h2h_data` | All-time H2H record between the two teams |
|
|
@@ -100,6 +112,26 @@ Dynamic: license-file
|
|
|
100
112
|
|---|---|
|
|
101
113
|
| `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
|
|
102
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
|
+
|
|
103
135
|
> **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
|
|
104
136
|
|
|
105
137
|
## Installation
|
|
@@ -580,6 +612,22 @@ Dependencies: `match_data`
|
|
|
580
612
|
|
|
581
613
|
---
|
|
582
614
|
|
|
615
|
+
#### `formations_data`
|
|
616
|
+
|
|
617
|
+
Fetch home and away formation for each match.
|
|
618
|
+
|
|
619
|
+
```python
|
|
620
|
+
from datafc import formations_data
|
|
621
|
+
|
|
622
|
+
df = formations_data(match_df=match_df)
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Columns: `country`, `tournament`, `season`, `week`, `game_id`, `team` (`home` / `away`), `formation`.
|
|
626
|
+
|
|
627
|
+
Dependencies: `match_data`
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
583
631
|
#### `match_stats_data`
|
|
584
632
|
|
|
585
633
|
Fetch aggregate team statistics (possession, shots, passes, etc.) for each match.
|
|
@@ -933,8 +981,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
|
|
|
933
981
|
|
|
934
982
|
---
|
|
935
983
|
|
|
984
|
+
### ClubElo
|
|
985
|
+
|
|
986
|
+
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.
|
|
987
|
+
|
|
988
|
+
```python
|
|
989
|
+
from datafc import clubelo
|
|
990
|
+
|
|
991
|
+
# Full ranking on a given day
|
|
992
|
+
df = clubelo.daily_ranking_data("2026-06-10")
|
|
993
|
+
|
|
994
|
+
# A single club's full Elo history (use the spelling shown in daily_ranking_data)
|
|
995
|
+
hist = clubelo.club_history_data("Man City")
|
|
996
|
+
|
|
997
|
+
# Upcoming matches with per-goal-difference and exact-result probabilities
|
|
998
|
+
fix = clubelo.fixtures_data()
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
|
|
1002
|
+
|
|
1003
|
+
> **Notes:**
|
|
1004
|
+
> - 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.
|
|
1005
|
+
> - Values before 1960 are considered provisional by ClubElo.
|
|
1006
|
+
> - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
|
|
1007
|
+
|
|
1008
|
+
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:
|
|
1009
|
+
|
|
1010
|
+
```python
|
|
1011
|
+
from datafc import clubelo
|
|
1012
|
+
clubelo.set_clubelo_base_url("http://api.clubelo.com")
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
`clubelo.reset_clubelo_base_url()` restores the default.
|
|
1016
|
+
|
|
1017
|
+
---
|
|
1018
|
+
|
|
1019
|
+
### EloRatings (national teams)
|
|
1020
|
+
|
|
1021
|
+
[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.
|
|
1022
|
+
|
|
1023
|
+
```python
|
|
1024
|
+
from datafc import eloratings
|
|
1025
|
+
|
|
1026
|
+
# Current global ranking of all national teams (30 columns)
|
|
1027
|
+
df = eloratings.world_ranking_data()
|
|
1028
|
+
|
|
1029
|
+
# Full match history for a single national team
|
|
1030
|
+
matches = eloratings.country_matches_data("Spain")
|
|
1031
|
+
|
|
1032
|
+
# Code reference tables (handy for joining onto the above)
|
|
1033
|
+
countries = eloratings.country_codes_data() # country_code → country_name
|
|
1034
|
+
tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
|
|
1035
|
+
groups = eloratings.tournament_groups_data() # specific code → group code (long format)
|
|
1036
|
+
editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
|
|
1037
|
+
|
|
1038
|
+
# Legacy/historical country code → ISO code mapping
|
|
1039
|
+
teams = eloratings.teams_data()
|
|
1040
|
+
|
|
1041
|
+
# Example: enrich Spain's matches with full tournament names
|
|
1042
|
+
spain = (
|
|
1043
|
+
eloratings.country_matches_data("Spain")
|
|
1044
|
+
.merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
# Example: every World Cup final tournament (filter editions on the WC group)
|
|
1048
|
+
wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
|
|
1052
|
+
|
|
1053
|
+
> **Column names are verified against the site's JavaScript source.**
|
|
1054
|
+
> 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.
|
|
1055
|
+
|
|
1056
|
+
> **Country argument is a page slug, not an ISO code.**
|
|
1057
|
+
> `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"`).
|
|
1058
|
+
|
|
1059
|
+
Override the base URL the same way as ClubElo if needed:
|
|
1060
|
+
|
|
1061
|
+
```python
|
|
1062
|
+
from datafc import eloratings
|
|
1063
|
+
eloratings.set_eloratings_base_url("http://www.eloratings.net")
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
---
|
|
1067
|
+
|
|
936
1068
|
## What's new
|
|
937
1069
|
|
|
1070
|
+
### v2.7.0
|
|
1071
|
+
|
|
1072
|
+
- **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`.
|
|
1073
|
+
- **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`.
|
|
1074
|
+
- **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.
|
|
1075
|
+
|
|
1076
|
+
---
|
|
1077
|
+
|
|
1078
|
+
### v2.6.0
|
|
1079
|
+
|
|
1080
|
+
- **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`).
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
938
1084
|
### v2.5.0
|
|
939
1085
|
|
|
940
1086
|
- **`player_data` now returns `position_detailed`.** New column containing Sofascore's detailed position list (e.g. `["AM", "MC", "RW"]`) alongside the existing single `position` field.
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
# datafc v2.
|
|
1
|
+
# datafc v2.7.0
|
|
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`.
|
|
9
9
|
> - **Programmatically:** use `search_data("super lig", entity_type="tournament")` to find the tournament ID, then `seasons_data(tournament_id)` to list all available seasons.
|
|
10
10
|
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
If you find <strong>datafc</strong> useful, consider supporting the project ☕<br><br>
|
|
15
|
+
<a href="https://buymeacoffee.com/urazdev">
|
|
16
|
+
<img src="https://img.shields.io/badge/Buy%20Me%20A%20Coffee-urazdev-%23FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee">
|
|
17
|
+
</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
11
22
|
## What Data Can You Access?
|
|
12
23
|
|
|
13
24
|
### Discovery
|
|
@@ -43,6 +54,7 @@
|
|
|
43
54
|
|---|---|
|
|
44
55
|
| `match_data` | Fixtures for a matchweek: score, status, home/away teams |
|
|
45
56
|
| `match_details_data` | Referee info (name, cards, games) and venue details per match |
|
|
57
|
+
| `formations_data` | Home and away formation for each match |
|
|
46
58
|
| `match_stats_data` | Aggregate team stats per match (possession, shots, fouls, …) |
|
|
47
59
|
| `match_odds_data` | Pre-match 1/X/2 betting odds |
|
|
48
60
|
| `match_h2h_data` | All-time H2H record between the two teams |
|
|
@@ -74,6 +86,26 @@
|
|
|
74
86
|
|---|---|
|
|
75
87
|
| `referee_stats_data` | Career stats for a referee: games, cards, and per-game averages |
|
|
76
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
|
+
|
|
77
109
|
> **Coverage:** Any league and season available on Sofascore. For Turkey Super Lig, every season from 1980/81 to the present is accessible.
|
|
78
110
|
|
|
79
111
|
## Installation
|
|
@@ -554,6 +586,22 @@ Dependencies: `match_data`
|
|
|
554
586
|
|
|
555
587
|
---
|
|
556
588
|
|
|
589
|
+
#### `formations_data`
|
|
590
|
+
|
|
591
|
+
Fetch home and away formation for each match.
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
from datafc import formations_data
|
|
595
|
+
|
|
596
|
+
df = formations_data(match_df=match_df)
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Columns: `country`, `tournament`, `season`, `week`, `game_id`, `team` (`home` / `away`), `formation`.
|
|
600
|
+
|
|
601
|
+
Dependencies: `match_data`
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
557
605
|
#### `match_stats_data`
|
|
558
606
|
|
|
559
607
|
Fetch aggregate team statistics (possession, shots, passes, etc.) for each match.
|
|
@@ -907,8 +955,106 @@ Columns: `referee_id`, `referee_name`, `tournament_id`, `tournament_name`, `stat
|
|
|
907
955
|
|
|
908
956
|
---
|
|
909
957
|
|
|
958
|
+
### ClubElo
|
|
959
|
+
|
|
960
|
+
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.
|
|
961
|
+
|
|
962
|
+
```python
|
|
963
|
+
from datafc import clubelo
|
|
964
|
+
|
|
965
|
+
# Full ranking on a given day
|
|
966
|
+
df = clubelo.daily_ranking_data("2026-06-10")
|
|
967
|
+
|
|
968
|
+
# A single club's full Elo history (use the spelling shown in daily_ranking_data)
|
|
969
|
+
hist = clubelo.club_history_data("Man City")
|
|
970
|
+
|
|
971
|
+
# Upcoming matches with per-goal-difference and exact-result probabilities
|
|
972
|
+
fix = clubelo.fixtures_data()
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (any `DiskCache` instance, shared with the Sofascore functions).
|
|
976
|
+
|
|
977
|
+
> **Notes:**
|
|
978
|
+
> - 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.
|
|
979
|
+
> - Values before 1960 are considered provisional by ClubElo.
|
|
980
|
+
> - Dates accept either `"YYYY-MM-DD"` strings or `datetime.date` objects.
|
|
981
|
+
|
|
982
|
+
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:
|
|
983
|
+
|
|
984
|
+
```python
|
|
985
|
+
from datafc import clubelo
|
|
986
|
+
clubelo.set_clubelo_base_url("http://api.clubelo.com")
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
`clubelo.reset_clubelo_base_url()` restores the default.
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
### EloRatings (national teams)
|
|
994
|
+
|
|
995
|
+
[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.
|
|
996
|
+
|
|
997
|
+
```python
|
|
998
|
+
from datafc import eloratings
|
|
999
|
+
|
|
1000
|
+
# Current global ranking of all national teams (30 columns)
|
|
1001
|
+
df = eloratings.world_ranking_data()
|
|
1002
|
+
|
|
1003
|
+
# Full match history for a single national team
|
|
1004
|
+
matches = eloratings.country_matches_data("Spain")
|
|
1005
|
+
|
|
1006
|
+
# Code reference tables (handy for joining onto the above)
|
|
1007
|
+
countries = eloratings.country_codes_data() # country_code → country_name
|
|
1008
|
+
tournaments = eloratings.tournament_codes_data() # tournament_code → tournament_name
|
|
1009
|
+
groups = eloratings.tournament_groups_data() # specific code → group code (long format)
|
|
1010
|
+
editions = eloratings.tournament_editions_data() # every tournament edition + dates + slug
|
|
1011
|
+
|
|
1012
|
+
# Legacy/historical country code → ISO code mapping
|
|
1013
|
+
teams = eloratings.teams_data()
|
|
1014
|
+
|
|
1015
|
+
# Example: enrich Spain's matches with full tournament names
|
|
1016
|
+
spain = (
|
|
1017
|
+
eloratings.country_matches_data("Spain")
|
|
1018
|
+
.merge(tournaments, left_on="tournament", right_on="tournament_code", how="left")
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Example: every World Cup final tournament (filter editions on the WC group)
|
|
1022
|
+
wc_finals = editions[(editions["code"] == "WC") & editions["start_date"].notna()]
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
Common parameters: `rate_limit` (default `2.0` req/s) and `cache` (same `DiskCache` instance as Sofascore/ClubElo).
|
|
1026
|
+
|
|
1027
|
+
> **Column names are verified against the site's JavaScript source.**
|
|
1028
|
+
> 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.
|
|
1029
|
+
|
|
1030
|
+
> **Country argument is a page slug, not an ISO code.**
|
|
1031
|
+
> `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"`).
|
|
1032
|
+
|
|
1033
|
+
Override the base URL the same way as ClubElo if needed:
|
|
1034
|
+
|
|
1035
|
+
```python
|
|
1036
|
+
from datafc import eloratings
|
|
1037
|
+
eloratings.set_eloratings_base_url("http://www.eloratings.net")
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
---
|
|
1041
|
+
|
|
910
1042
|
## What's new
|
|
911
1043
|
|
|
1044
|
+
### v2.7.0
|
|
1045
|
+
|
|
1046
|
+
- **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`.
|
|
1047
|
+
- **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`.
|
|
1048
|
+
- **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.
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
### v2.6.0
|
|
1053
|
+
|
|
1054
|
+
- **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`).
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
912
1058
|
### v2.5.0
|
|
913
1059
|
|
|
914
1060
|
- **`player_data` now returns `position_detailed`.** New column containing Sofascore's detailed position list (e.g. `["AM", "MC", "RW"]`) alongside the existing single `position` field.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.7.0"
|
|
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,
|
|
@@ -36,6 +38,7 @@ __all__ = [
|
|
|
36
38
|
# Matchweek
|
|
37
39
|
"match_data",
|
|
38
40
|
"match_details_data",
|
|
41
|
+
"formations_data",
|
|
39
42
|
"match_stats_data",
|
|
40
43
|
"match_odds_data",
|
|
41
44
|
"match_h2h_data",
|
|
@@ -60,6 +63,10 @@ __all__ = [
|
|
|
60
63
|
"referee_stats_data",
|
|
61
64
|
# Async API
|
|
62
65
|
"aio",
|
|
66
|
+
# ClubElo subpackage
|
|
67
|
+
"clubelo",
|
|
68
|
+
# EloRatings subpackage
|
|
69
|
+
"eloratings",
|
|
63
70
|
# Exceptions
|
|
64
71
|
"DataFCError",
|
|
65
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)
|