History of soccer_passing
The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.
[entries] Add the date when each entry was added, and remove icon_hash beff7c9
The dates are those of the commits that first added the entries' files (2020-08-11 for the entries of the initial commit); renamed entries keep the date of their first name. Changed: none
diff --git a/entries/soccer_passing/__init__.py b/entries/soccer_passing/__init__.py
index 48c43ba..d1ba1b4 100644
--- a/entries/soccer_passing/__init__.py
+++ b/entries/soccer_passing/__init__.py
@@ -55,7 +55,7 @@ The networks have the following properties:
tags = ['Social', 'Offline', 'Sports', 'Weighted', 'Multigraph', 'Timestamps', 'Metadata']
url = 'https://doi.org/10.6084/m9.figshare.c.4415000'
citation = [('L. Pappalardo, P. Cintia, A. Rossi, E. Massucco, P. Ferragina, D. Pedreschi, F. Giannotti, "A public data set of spatio-temporal match events in soccer competitions." Scientific Data 6, 236 (2019)', 'https://doi.org/10.1038/s41597-019-0247-7')]
-icon_hash = None
+date_added = '2026-10-27'
upstream_license = ("CC BY 4.0", "https://creativecommons.org/licenses/by/4.0/")
# figshare files, by id: events (14464685, zip), matches (14464622, zip),
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) bc3dbbd
The lists of properties at the end of the descriptions marked edge properties with "Edge" but left node properties unmarked, or grouped them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property was often unclear. Every item now starts with (Node), (Edge) or (Graph), followed by the property names, a colon and the explanation, one item per property or group of properties; qualifications (kinds of nodes, networks) follow the names in parentheses. Properties that were only named got a short explanation, and a few items that named the wrong kind of property were corrected.
diff --git a/entries/soccer_passing/__init__.py b/entries/soccer_passing/__init__.py
index fdb110d..48c43ba 100644
--- a/entries/soccer_passing/__init__.py
+++ b/entries/soccer_passing/__init__.py
@@ -38,8 +38,20 @@ Wyscout records who made each pass and whether it was accurate (i.e. reached a t
The networks have the following properties:
-- Nodes: `player_id`, the Wyscout id of the player; `name`, his short name; `first_name` and `last_name`; `role`, his role (goalkeeper, defender, midfielder or forward); `team`, the team(s) for which he made or received passes in the competition (several, separated by semicolons, for players transferred during the season); `nationality`, the country of his passport; `birth_date`; `height`, in centimeters, and `weight_kg`, his body weight in kilograms (0 if unknown); `foot`, his preferred foot (empty if unknown).
-- Edges: `weight`, the number of completed passes; `match_id`, the Wyscout id of the match; `time`, the starting time of the match (Unix time); `match`, the label of the match (teams and final score); `team`, the team of the two players."""
+- (Node) `player_id`: the Wyscout id of the player.
+- (Node) `name`: his short name.
+- (Node) `first_name` and `last_name`.
+- (Node) `role`: his role (goalkeeper, defender, midfielder or forward).
+- (Node) `team`: the team(s) for which he made or received passes in the competition (several, separated by semicolons, for players transferred during the season).
+- (Node) `nationality`: the country of his passport.
+- (Node) `birth_date`.
+- (Node) `height`: in centimeters, and `weight_kg`, his body weight in kilograms (0 if unknown).
+- (Node) `foot`: his preferred foot (empty if unknown).
+- (Edge) `weight`: the number of completed passes.
+- (Edge) `match_id`: the Wyscout id of the match.
+- (Edge) `time`: the starting time of the match (Unix time).
+- (Edge) `match`: the label of the match (teams and final score).
+- (Edge) `team`: the team of the two players."""
tags = ['Social', 'Offline', 'Sports', 'Weighted', 'Multigraph', 'Timestamps', 'Metadata']
url = 'https://doi.org/10.6084/m9.figshare.c.4415000'
citation = [('L. Pappalardo, P. Cintia, A. Rossi, E. Massucco, P. Ferragina, D. Pedreschi, F. Giannotti, "A public data set of spatio-temporal match events in soccer competitions." Scientific Data 6, 236 (2019)', 'https://doi.org/10.1038/s41597-019-0247-7')]
[entries] Remove unneeded code from the entries 2c69f19
Unneeded "global files" statements, unused imports and variables, f-strings without placeholders, duplicate keys in foodweb_little_rock (keeping the order and the values used), and the second definition of get_lines() in twitter_higgs. Nothing changes in the networks or descriptions. Changed: none
diff --git a/entries/soccer_passing/__init__.py b/entries/soccer_passing/__init__.py
index dc2cdf0..fdb110d 100644
--- a/entries/soccer_passing/__init__.py
+++ b/entries/soccer_passing/__init__.py
@@ -78,7 +78,6 @@ def fetch_upstream(force=False):
@coerce_props()
@annotate()
def parse(alts=None):
- global files
name = __name__.split(".")[-1]
for fnames, alt, fmt in files:
if alts is not None and alt not in alts:
[entries] New entry: soccer_passing 6df3808
diff --git a/entries/soccer_passing/__init__.py b/entries/soccer_passing/__init__.py
new file mode 100644
index 0000000..dc2cdf0
--- /dev/null
+++ b/entries/soccer_passing/__init__.py
@@ -0,0 +1,170 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from .. import *
+from datetime import timezone
+import re
+
+title = "Soccer passing networks (Wyscout, 2016-2018)"
+description = """Passing networks of soccer (association football) players, built from the match event data ("soccer-logs") collected by Wyscout and published by Pappalardo et al.: all the 1,941 matches of seven competitions, one network per competition:
+
+- `england`: English Premier League, season 2017/2018 (380 matches);
+- `france`: French Ligue 1, season 2017/2018 (380 matches);
+- `germany`: German Bundesliga, season 2017/2018 (306 matches);
+- `italy`: Italian Serie A, season 2017/2018 (380 matches);
+- `spain`: Spanish La Liga, season 2017/2018 (380 matches);
+- `euro_2016`: UEFA European Championship 2016 (51 matches);
+- `world_cup_2018`: FIFA World Cup 2018 (64 matches).
+
+Each node is a player, and each directed edge goes from a player to a teammate who received at least one of his completed passes in a given match, weighted by the number of such passes; the network of a competition is thus a multigraph, with one edge per pair of players and match, and the passing network of a single team in a single match is the subgraph of the edges with the corresponding `match_id` and `team`. Passes include the free kicks that are passes (corners, free kicks, free kick crosses, goal kicks and throw-ins), but not shots and penalties.
+
+Wyscout records who made each pass and whether it was accurate (i.e. reached a teammate), but not who received it. As usual for these data, the recipient of an accurate pass is taken to be the player of the next event of the same team in the same period of the match (events of the opposing team in between, e.g. duels, are skipped). Of the 1,855,914 passes in the data, 1,478,167 are included: left out are the 341,770 passes that were not accurate, and the accurate passes for which no recipient can be determined in this way (7,416 for which the next event of the team is by the same player, 28,547 for which it has no player, and 14 that are the last events of the team in a period). Only players who made or received at least one included pass appear in the networks.
+
+The networks have the following properties:
+
+- Nodes: `player_id`, the Wyscout id of the player; `name`, his short name; `first_name` and `last_name`; `role`, his role (goalkeeper, defender, midfielder or forward); `team`, the team(s) for which he made or received passes in the competition (several, separated by semicolons, for players transferred during the season); `nationality`, the country of his passport; `birth_date`; `height`, in centimeters, and `weight_kg`, his body weight in kilograms (0 if unknown); `foot`, his preferred foot (empty if unknown).
+- Edges: `weight`, the number of completed passes; `match_id`, the Wyscout id of the match; `time`, the starting time of the match (Unix time); `match`, the label of the match (teams and final score); `team`, the team of the two players."""
+tags = ['Social', 'Offline', 'Sports', 'Weighted', 'Multigraph', 'Timestamps', 'Metadata']
+url = 'https://doi.org/10.6084/m9.figshare.c.4415000'
+citation = [('L. Pappalardo, P. Cintia, A. Rossi, E. Massucco, P. Ferragina, D. Pedreschi, F. Giannotti, "A public data set of spatio-temporal match events in soccer competitions." Scientific Data 6, 236 (2019)', 'https://doi.org/10.1038/s41597-019-0247-7')]
+icon_hash = None
+upstream_license = ("CC BY 4.0", "https://creativecommons.org/licenses/by/4.0/")
+
+# figshare files, by id: events (14464685, zip), matches (14464622, zip),
+# players (15073721), teams (15073697)
+upstream_prefix = 'https://api.figshare.com/v2/file/download'
+competitions = [('England', 'england'), ('France', 'france'),
+ ('Germany', 'germany'), ('Italy', 'italy'), ('Spain', 'spain'),
+ ('European_Championship', 'euro_2016'),
+ ('World_Cup', 'world_cup_2018')]
+files = [((f'14464685:zip:events_{c}.json', f'14464622:zip:matches_{c}.json',
+ '15073721', '15073697'), alt, None) for c, alt in competitions]
+
+# Wyscout event ids: passes, and the free kicks that are passes (corners,
+# free kicks, free kick crosses, goal kicks and throw-ins; not shots and
+# penalties)
+PASS_EVENT = 8
+PASS_FREE_KICKS = {30, 31, 32, 34, 36}
+ACCURATE_TAG = 1801
+
+def unescape(s):
+ """Decode the escape sequences (e.g. "\\u00e9") that are left in some
+ strings of the upstream files (e.g. the player names)."""
+ if not isinstance(s, str):
+ return s
+ return re.sub(r"\\u([0-9a-fA-F]{4})", lambda m: chr(int(m[1], 16)), s)
+
+def fetch_upstream(force=False):
+ return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+ force)
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+ global files
+ name = __name__.split(".")[-1]
+ for fnames, alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ with open_upstream_file(name, fnames[2], "rb") as f:
+ players = {p["wyId"]: p for p in json.load(f)}
+ with open_upstream_file(name, fnames[3], "rb") as f:
+ teams = {t["wyId"]: t for t in json.load(f)}
+ with open_upstream_file(name, fnames[1], "rb") as f:
+ matches = {m["wyId"]: m for m in json.load(f)}
+ with open_upstream_file(name, fnames[0], "rb") as f:
+ events = json.load(f)
+
+ # The recipient of a completed pass is the player of the next event
+ # of the same team in the same period of the match
+ counts = defaultdict(int)
+ for i, ev in enumerate(events):
+ if not (ev["eventId"] == PASS_EVENT or
+ ev["subEventId"] in PASS_FREE_KICKS):
+ continue
+ if not any(t["id"] == ACCURATE_TAG for t in ev["tags"]):
+ continue
+ key = (ev["matchId"], ev["matchPeriod"])
+ rec = None
+ for j in range(i + 1, len(events)):
+ nev = events[j]
+ if (nev["matchId"], nev["matchPeriod"]) != key:
+ break
+ if nev["teamId"] == ev["teamId"]:
+ rec = nev
+ break
+ if (rec is None or rec["playerId"] == ev["playerId"] or
+ rec["playerId"] == 0 or ev["playerId"] == 0):
+ continue
+ counts[(ev["matchId"], ev["teamId"], ev["playerId"],
+ rec["playerId"])] += 1
+ del events
+
+ def match_time(mid):
+ d = datetime.strptime(matches[mid]["dateutc"], "%Y-%m-%d %H:%M:%S")
+ return int(d.replace(tzinfo=timezone.utc).timestamp())
+
+ g = Graph(directed=True)
+ vprops = [("player_id", "int"), ("name", "string"),
+ ("first_name", "string"), ("last_name", "string"),
+ ("role", "string"), ("team", "string"),
+ ("nationality", "string"), ("birth_date", "string"),
+ ("height", "int"), ("weight_kg", "int"), ("foot", "string")]
+ for p, t in vprops:
+ g.vp[p] = g.new_vp(t)
+ eprops = [("weight", "int"), ("match_id", "int"), ("time", "int64_t"),
+ ("match", "string"), ("team", "string")]
+ for p, t in eprops:
+ g.ep[p] = g.new_ep(t)
+
+ keys = sorted(counts, key=lambda k: (match_time(k[0]), k))
+ vs = {}
+ vteams = defaultdict(list)
+ def get_v(pid, tid):
+ if tid not in vteams[pid]:
+ vteams[pid].append(tid)
+ v = vs.get(pid)
+ if v is None:
+ v = vs[pid] = int(g.add_vertex())
+ return v
+ edges = []
+ for k in keys:
+ mid, tid, s, t = k
+ edges.append((get_v(s, tid), get_v(t, tid), counts[k], mid,
+ match_time(mid), unescape(matches[mid]["label"]),
+ unescape(teams[tid]["name"])))
+ g.add_edge_list(edges, eprops=[g.ep[p] for p, t in eprops])
+
+ for pid, v in vs.items():
+ p = players.get(pid, {})
+ g.vp.player_id[v] = pid
+ g.vp.name[v] = unescape(p.get("shortName", ""))
+ g.vp.first_name[v] = unescape(p.get("firstName", ""))
+ g.vp.last_name[v] = unescape(p.get("lastName", ""))
+ g.vp.role[v] = p.get("role", {}).get("name", "")
+ g.vp.team[v] = "; ".join(unescape(teams[t]["name"])
+ for t in vteams[pid])
+ area = p.get("passportArea") or {}
+ g.vp.nationality[v] = unescape(area.get("name", ""))
+ g.vp.birth_date[v] = p.get("birthDate") or ""
+ g.vp.height[v] = p.get("height") or 0
+ g.vp.weight_kg[v] = p.get("weight") or 0
+ g.vp.foot[v] = p.get("foot") or ""
+ yield alt, g