Model or dataset
dcaribou/transfermarkt-datasets avatar
dcaribou/transfermarkt-datasets

transfermarkt-datasets turns Transfermarkt into twelve joinable football tables

⚽️ Extract, prepare and publish Transfermarkt datasets.

521 stars100 forksPythonCC0-1.0

At a glance

What is it?
The transfermarkt-datasets project scrapes Transfermarkt, transforms the result into a clean relational dataset of 12 tables with 88,000 games, 50,000 players and 1.89 million appearances, and publishes it as CSV, a DuckDB file and a Kaggle dataset, with the whole pipeline built on dbt, DVC, DuckDB and Streamlit. It is CC0 licensed, and the collection pipeline stopped completing in July 2026, so the published data is frozen at 6 July 2026.
Who is it for?
transfermarkt-datasets suits a football analytics project that wants match, appearance, transfer and market value data without writing a scraper and a cleaning pipeline from scratch, since the tables are documented, joinable and already published in three formats.
Can I use it commercially?
Yes. CC0-1.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 48 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the dataset actually is

The project describes itself as a clean, structured football dataset built from Transfermarkt data, with 88,000+ games, 50,000+ players, 1,890,000+ appearances and more, across 12 joinable tables.

The twelve tables cover competitions, games, clubs, players, appearances, player valuations, club games, game events, game lineups, transfers, countries and national teams, and each table contains the attributes of the entity plus IDs that can be used to join them together.

The scale of each table is given, and the numbers tell you what the data is good for. competitions has 65 rows, covering leagues, tournaments and national team competitions. clubs has 790+, with club details, squad size and market value. players has 50,000+, with player profiles, positions, market values and international caps. games has 88,000+, with match results, lineups and attendance. appearances has 1,890,000+, one row per player per game played. player_valuations has 650,000+ historical market value records. club_games has 177,000+, a per-club view of each game. game_events has 1,270,000+ for goals, cards and substitutions. game_lineups has 3,170,000+ for starting and bench lineups. transfers has 175,000+. And countries and national_teams have 124 rows each, with confederation membership and FIFA ranking.

That shape is the important design decision. appearances at one row per player per game is the fact table, and everything else hangs off it, which is why there are 1.89 million appearances but only 88,000 games. A dataset with one row per game would have been smaller and would have thrown away the individual player contributions that most football analysis is actually about.

The licence is CC0-1.0, which is the most permissive thing a dataset can be under and means you can use it without attribution or restriction.

The homepage points at the Kaggle dataset, and the downloads are offered as a ZIP archive of the whole dataset, as a single DuckDB file, and through a Codespaces link that opens the project with a quickstart so you can query it without installing anything.

The pause, and the dates that matter

The most important thing in the readme is a warning, and it is at the top of the file.

Updates are paused. The dataset is current to 6 July 2026. The automated collection pipeline stopped completing successfully in mid-July 2026, and there is no estimated date for updates to resume.

The warning then gives the per-table cut-offs, which is the detail that saves you an afternoon of confused analysis. The games table contains nothing after 2026-07-06, appearances nothing after 2026-06-28, player_valuations nothing after 2026-06-12, and 2026/27 squads are not covered.

That is three different dates for three tables in the same dataset, and it means the tables are not internally consistent at the boundary. A query that joins appearances to games and filters on a date in late June can find an appearance with no matching game, and a valuation from the middle of June can be newer than the games it is meant to describe.

The readme asks readers to read the status announcement before opening an issue about missing recent data, which is a reasonable request given that the natural reaction to a truncated season is to file a bug.

Everything published before the cut-off is unaffected and still available to download, and the readme is explicit that the pause does not affect what is there. So the dataset is usable as a historical snapshot, just not a live one.

The pipeline badges at the top of the readme explain the cause rather than just the symptom. There is a Build Status badge for the build workflow, a separate Scraper Pipeline Status badge for the acquire-transfermarkt-scraper workflow, and a third for the acquire-transfermarkt-api workflow. The presence of two acquisition paths suggests the project moved from scraping towards an API-based collection, and the readme does not say which of the two is failing.

On the positive side, the World Cup 2026 group stage is in the dataset, with results and player appearances for all 48 nations, up to the July cut-off, and the dataset covers all major national team tournaments, World Cup, UEFA Euro, Copa America, AFCON and AFC Asian Cup, plus international_caps, international_goals and current_national_team_id on every player profile.

Querying it with DuckDB

The fastest way to explore the dataset is to download the DuckDB database file, a single file containing all 12 tables, ready to query. The readme notes that this is a snapshot with data up to 6 July 2026 and is not currently being refreshed.

From the command line:

bash
curl -LO https://pub-e682421888d945d684bcae8890b0ec20.r2.dev/data/transfermarkt-datasets.duckdb

Then you open it with the DuckDB CLI, with Python, with R, or with any compatible client.

The readme's example query is worth reading because it shows the join structure in one place. It asks who scored in Brazil's last group game, and it selects from appearances joined to games using game_id, filtering on competition_id of FIWC, on the club name being either the home or away club, and on goals greater than zero, ordering by date descending. The result comes back with Vinicius Junior on two and Matheus Cunha on one.

The query demonstrates three things about the schema at once. The competition identifier is a short code rather than a readable name, so filtering on competitions means you need the code from the competitions table. The join between appearances and games is on game_id alone, which is why there is no club table in the query. And the home and away club names live on the games table, not on clubs, so the club filter is a disjunction over two columns of the same row.

That last point is the kind of thing that trips people up. The club_games table exists as a per-club view of each game, which is the normalised way to query a club's fixtures, and using it avoids the home-or-away disjunction entirely. The entity relationship diagram in the readme, which is drawn as a Mermaid class diagram, shows both clubs to games relationships, one through home/away_club_id and one through the club_games table with club_id and opponent_club_id.

The schema also carries the international dimension explicitly. national_teams join to countries on country_id, and players join to national_teams on current_national_team_id, with international_caps and international_goals on the player record itself, so a national team squad is queryable without a separate appearances-based aggregation.

How the pipeline is put together

The repository is a full data engineering project rather than a set of scraper scripts, and the file layout shows the stages.

There is a dbt/ directory, which is where the transformation logic lives, with the readme carrying a dbt version badge at 1.11.5. There is a .dvc/ directory and a dvc dependency, so data versioning is part of the setup. There is a data/ directory for the raw and intermediate data, a scripts/ directory, and a transfermarkt_datasets/ directory for the package code itself.

The justfile is the task runner, and it is where the pipeline stages are named. Variables at the top set the platform, which is linux/arm64 because that is the mac M1, the branch, a job name of on-cli, a tag of dev, the dbt target, and then the two pipeline components by name: acquirer is transfermarkt-scraper and syncer is sync-kaggle, with the workflow also named sync-kaggle.

That naming tells you the shape of the pipeline. There is an acquisition step that pulls from Transfermarkt, a transformation step in dbt that turns the scraped mess into the twelve clean tables, and a publication step that syncs the result to Kaggle.

The recipes cover the deployment as well as the work. There is a dvc_pull recipe that pulls data from the cloud, which the comment identifies as Cloudflare R2, and docker_login_ecr for Amazon Elastic Container Registry in the eu-west-1 region, plus logins for Docker Hub and Fly.io. The docker_build recipe builds for a platform and tags it with the platform and the tag joined by a dash, and there are push recipes for Docker Hub and for the Fly registry. There is a fly.toml, and the Docker image entrypoint runs a bootstrap script whose default command is master, just, streamlit_local.

The Streamlit part is worth noting. There is a .streamlit/ directory and a streamlit/ directory, so the project includes a browsable web app over the data, not just the files. The dev dependencies also include jupyter, and the main dependencies include streamlit and plotly, which is the charting pair for that app.

The scraping itself comes from a separate project, transfermarkt-scraper, pulled in as a git dependency pinned to its main branch rather than from PyPI, which means the acquisition code evolves alongside the dataset code rather than in a separately released package.

Where the project fits, and where it stops

The comparison worth making is with a commercial football data provider and with writing your own scraper.

Against a commercial provider, the difference is cost and coverage. This dataset is CC0 and free, and it is large enough to build a serious model on. A paid provider will have data this does not, and in particular the readme's own note that player market valuations exist as a historical series is unusual, since Transfermarkt's valuation figures are a distinctive part of what it publishes and a paid feed of comparable history is not straightforward to get.

Against writing your own scraper, the value is everything except the scraping. The tables are documented with an entity relationship diagram, the joins are settled, the cut-offs are published, and the data is already in DuckDB, CSV and Kaggle. A team that wrote its own scraper would spend weeks on exactly the parts this project has already decided: what the keys are, how the home and away club columns work, how the international dimension is modelled.

The limits are mostly about the pause and the source. The collection stopped completing in July 2026 with no estimated restart, so this is a snapshot and not a feed, and for a question about the current season it is the wrong dataset. The three different table cut-off dates mean boundary queries need care. And the licensing position is asymmetric in a way worth being honest about: the project publishes CC0, while the data was extracted from a commercial site whose own terms are a separate matter, and a project built on it for commercial use should take advice on that rather than relying on the CC0 badge alone.

The maintenance picture is otherwise good. The last push was on 2026-09-05, the repository is not archived, and there is a .pre-commit-config.yaml, a .python-version file and a Dockerfile that installs poetry and the just task runner, so the environment is reproducible. There is a CLAUDE.md and a .claude/ directory at the root, a devcontainer, a streamlit app, and a docs/ directory.

For an analyst who wants football data now, the practical sequence is short: download the DuckDB file, check the row counts and the date ranges of the tables you care about, and only then decide whether the snapshot is recent enough for the question. The status announcement is the document to read before assuming the data is stale in a way it is not.

Editorial conclusion

transfermarkt-datasets suits a football analytics project that wants match, appearance, transfer and market value data without writing a scraper and a cleaning pipeline from scratch, since the tables are documented, joinable and already published in three formats. It is the wrong tool if you need data after July 2026, because the acquisition pipeline stopped completing and the readme says there is no estimated date for updates to resume, and it is the wrong tool if you need guaranteed licensing from the source, since the project publishes under CC0 while the underlying site is a commercial one. Before you plan a season of analysis on it, read the status announcement, check which tables are current to which date, and query the DuckDB file first to see whether the cut-off lines up with your question.

Frequently asked questions

What is the transfermarkt-datasets project?

It is a clean, structured football dataset built from Transfermarkt data, with 88,000+ games, 50,000+ players, 1,890,000+ appearances across 12 joinable tables covering competitions, games, clubs, players, appearances, player valuations, club games, game events, game lineups, transfers, countries and national teams. The licence is CC0-1.0.

Is the transfermarkt-datasets project still being updated?

No. The readme carries a warning that updates are paused and the dataset is current to 6 July 2026, because the automated collection pipeline stopped completing successfully in mid-July 2026 with no estimated date for updates to resume. Everything published before then is unaffected and still downloadable, and a status announcement explains the situation.

What dates does the dataset actually cover?

The cut-off differs per table. The games table contains nothing after 2026-07-06, appearances nothing after 2026-06-28, and player_valuations nothing after 2026-06-12, and the 2026/27 squads are not covered. The World Cup 2026 group stage is included for all 48 nations, up to the July cut-off.

How do I download and query the dataset?

The fastest route is the DuckDB database file, a single file containing all 12 tables, downloadable from the button in the readme or with curl -LO from the published URL. You can then open it with the DuckDB CLI, Python, R or any compatible client. There is also a ZIP archive of the whole dataset, a Kaggle dataset, and a GitHub Codespaces link that opens the project with a quickstart.

How is the transfermarkt-datasets pipeline built?

With dbt for the transformations, DVC for data versioning, DuckDB as the warehouse, and a scraper pulled in as a git dependency on the transfermarkt-scraper project. The justfile drives the stages, with named recipes for acquisition, transformation and a sync-kaggle publication step, plus Docker image builds pushed to Docker Hub and Fly.io.

Which tools does the transfermarkt-datasets project use?

Python 3.12 with poetry, pandas, frictionless for data validation, scipy, Streamlit and Plotly for the browsable app, dbt-duckdb and duckdb-engine for the warehouse, boto3, DVC with S3 extras, watchdog for file watching and kaggle for publication. The dev group adds jupyter, autopep8, datacompy and pre-commit; pytest sits in the main dependencies.

Official sources

  1. dcaribou/transfermarkt-datasets on GitHub
  2. Issues
  3. License: CC0-1.0
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dcaribou-transfermarkt-datasets.svg)](https://hysenlabs.com/projects/dcaribou-transfermarkt-datasets)

Was this useful?