# Football age chart: methodology and limitations

This chart compares the recorded season ages of players receiving domestic league minutes across five European leagues.

## Result

| Club-season | Recorded age weighted by minutes | Player-minutes | Age × minutes sum |
|---|---:|---:|---:|
| Strasbourg, 2024/25 | 20.6 years | 33,693 | 695,697 |
| Lille, 2017/18 | 22.0 years | 37,469 | 826,141 |
| Chievo, 2016/17 | 30.7 years | 37,611 | 1,154,581 |

Strasbourg is the youngest and Chievo the oldest among all 2,814 eligible club-seasons in this file. Lille is the next youngest. This is a descriptive result for the stated snapshot and period, not an all-time European record or a claim that youth causes better results.

## Input and coverage

Input: the existing `datasets/european-football/output/v3_club_stats.csv` under the local data-shop project. SHA-256:

`93ce3f7e6286fd0b9ddf1d29de43c2d024a60ee2021b769429d83e4cda02bccb`

The file contains **465,479** rows, including 250,393 domestic-cup rows, 154,598 league rows and 60,488 European-competition rows. Its 24-column schema has season, player, club, competition, appearances, minutes and recorded integer age. Position is blank throughout, so this analysis makes no position or forward classification.

The analysis uses only these five competition codes: `GB1` Premier League, `ES1` LaLiga, `L1` Bundesliga, `IT1` Serie A and `FR1` Ligue 1. Season-start years **1996 through 2024** correspond to **1996/97–2024/25**. No national-team file is read. The three other leagues in the source are outside this chart's cohort. Season-start 2025 is excluded because the archived snapshot's 2025/26 season is incomplete; its existence does not establish a full additional season.

Within the selected leagues and years, there are 99,380 rows, of which 77,738 have positive minutes. They contribute 102,346,927 player-minutes to 2,814 club-seasons across 229 distinct club IDs. The 2024/25 subset includes 96 clubs. Coverage counts and minute totals are checked against the available file, rather than asserting that all upstream football history is complete.

## Exact filters and grouping

1. `competition_type == "league"`.
2. `competition_code in {GB1, ES1, L1, IT1, FR1}`.
3. `league_id == competition_code`.
4. `1996 <= season_start_year <= 2024` and `season_end_year == season_start_year + 1`.
5. Require nonnegative minutes and appearances, and minutes no greater than 120 × appearances. Keep positive-minute rows for the calculation. Zero-minute entries receive zero weight.
6. Check positive-minute rows have integer recorded ages between 14 and 50. All do; no active row is removed for invalid age.
7. Check uniqueness of `(player_id, club_id, competition_code, season_start_year)`. There are no duplicate selected keys. The script stops if a future input introduces duplicates rather than silently retaining a potentially wrong record.
8. Group by season, domestic competition and club ID. Keep club-seasons with at least **25,000 valid player-minutes**. This is about 25 full-team matches at 11 × 90 minutes, so a brief fragment of a team-season cannot create the outlier. All 2,814 club-seasons pass; the smallest has 26,732 minutes. The threshold was fixed before the chart was rendered and does not select clubs according to age.

Calculation: `sum(recorded_age * minutes_played) / sum(minutes_played)`. A 3,000-minute player has 30 times the influence of a 100-minute player. A player's contributions after a transfer remain assigned to the relevant club-season. Goalkeepers are included. No player receives extra weight for being famous, having many appearances, or being a highlighted club member.

The plotted line is the median of the eligible **club values** in each season, giving each club equal influence on that reference. It is not the average age of every player or a pooled league minute-weighted mean. The scatter uses true season positions with no horizontal or vertical jitter. Highlighted points are selected by minimum, second minimum and maximum of the full aggregate, rather than hand-picked names.

## Age limitations and checks

The source supplies a recorded age for each season; it has no birthdate or match-date field. The chart therefore uses **recorded season age**, not exact chronological age at every appearance. The retained pipeline does not contain the v3 age calculation, so its intended reference date cannot be quoted from source code. A supplementary check against the existing local Transfermarkt `players.csv` birthdates strongly supports a **July 1 of the season-start year** convention: 50,111 of 50,114 matched positive-minute rows agree . All 30 Strasbourg 2024/25 contributors agree, covering 100% of its 33,693 minutes. Display labels are rounded to one decimal from the full-precision weighted result. The CSV retains exact numerators and denominators.

Three rows with available birthdates differ from the July 1 calculation by one year. None affects the highlighted clubs. These checks use a separate stored birthdate table, not ages inferred from the chart input. No live official-page birthdate verification is claimed. This public method note includes aggregate check results, without individual birthdates or player records.

The 2024/25 Strasbourg record also has full-season indicators: The two recorded goalkeepers together give **34 goalkeeper appearances and 3,060 goalkeeper minutes**, exactly 34 × 90. Total player-minutes are 33,693, compared with a nominal 34 × 11 × 90 = 33,660, a difference of just 33 minutes (0.1%). The 18 Ligue 1 clubs in that season have totals from 33,434 to 33,694 minutes. This supports a completed 34-match league season rather than a partial-season outlier; it is not a match-by-match reconstruction.

Seven players have a one-year change in `age - season_start_year` across their records. These are retained as recorded and flagged in the private analysis audit. None contributes to Strasbourg 2024/25, Lille 2017/18 or Chievo 2016/17. No source age was silently corrected.

Playing time is a measure of player use, not the whole registered squad. Injuries, substitution patterns, goalkeeper usage and transfers influence it. League formats and numbers of clubs change over time; the chart displays club-seasons and weights each club's ages by its own minutes. It does not identify a fixed panel of clubs or infer why a particular club is young. Source match records are not available here, so completeness is not independently proved at match level.

Strasbourg's 33,693 minutes are spread across 30 contributors. Players recorded below 23 account for 30,306 minutes, or **90%**. This supplementary figure uses the same recorded-age convention; it does not mean those players stayed under 23 on every matchday.

## Provenance and public links

The local README attributes older seasons to Transfermarkt collection and later seasons to the davidcariboo football dataset. Live upstream pages verified on 2026-10-08:

- [Football Data from Transfermarkt on Kaggle](https://www.kaggle.com/datasets/davidcariboo/player-scores).
- [dcaribou/transfermarkt-datasets](https://github.com/dcaribou/transfermarkt-datasets).

These identify the documented upstream source. They are not advertised as downloads of the exact local v3 compilation. The archived `clean.py` writes a different filename/schema from the existing v3 CSV, and no v3 build script, raw season files or Kaggle inputs are retained in this dataset directory. Accordingly, this package reproduces the analysis from the existing v3 file or the supplied derived aggregate, not the historical collection pipeline. The current upstream release has changed since the local snapshot.

The chart-source page supplies the derived club-season tables and analysis code. The historical source CSV is excluded. These downloads reproduce the chart from the aggregate without promising access to the original compilation or reconstructing its collection pipeline. Source attribution, Meplar affiliation and AI assistance are disclosed on the page.

## Reproduce and verify

Create a Python environment and install `requirements.txt`, then run:

```bash
python build_chart.py --input /path/to/v3_club_stats.csv --output .
python validate_independently.py --input /path/to/v3_club_stats.csv --output .
```

To redraw using the delivered aggregate without the original CSV:

```bash
python build_chart.py --aggregate club_season_age.csv --output .
```

The independent script uses the Python standard library's CSV reader, integer sums, Decimal division and `statistics.median`; it does not import the pandas chart script. It verifies every one of the 2,814 aggregates and all 29 median values, plus four explicit club-season spot checks. Maximum CSV rounding error is below one trillionth of a year. Input SHA-256 before and after the analysis matches. The full-size image and a reduced mobile preview were visually inspected. Text bounding boxes are checked against the export canvas; all annotation connectors terminate at their referenced data points.

## Tools and affiliation

Python, pandas, NumPy and Matplotlib were used. ChatGPT/Codex assisted with the analysis, code, checks and copy. I run Meplar. This is a new analysis and chart made with AI assistance.
