History of swissrailnet
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] New entry: swissrailnet 566b337
diff --git a/entries/swissrailnet/__init__.py b/entries/swissrailnet/__init__.py
new file mode 100644
index 0000000..a385caf
--- /dev/null
+++ b/entries/swissrailnet/__init__.py
@@ -0,0 +1,136 @@
+#! /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 *
+
+title = "Swiss railway network, four representations (2025)"
+description = """Network representations of the Swiss railway network, built by M. Sterchi, E. Raubach and L. Hilfiker from the open data of Swiss public transport (opentransportdata.swiss): the timetable of all scheduled stops on Wednesday, March 5, 2025 ("Actual Data"), the list of service points valid on that day, and the numbers of passengers boarding and alighting at each station. The authors define four representations; in all of them, the nodes are the same 1,626 train stations (including a few stations in Italy and Germany operated by Swiss companies), and they differ in the definition of the edges:
+
+- `space_of_changes`: a directed edge from station $i$ to station $j$ if some train serves $j$ after $i$ on the same trip, so that one can travel from $i$ to $j$ without changing trains. The edges are weighted by the number of such connections on that day and their average duration.
+- `space_of_stops`: a directed edge from station $i$ to station $j$ if some train stops at $j$ immediately after $i$ (a non-stop connection), with the same weights.
+- `space_of_stations`: an undirected edge between geographically adjacent stations, i.e. stations next to each other along the railway tracks. It was derived from the non-stop connections and validated manually: 11 edges, e.g. of long tunnels and high-speed lines served without stop, were added, and 11 edges between stations that are not adjacent were removed.
+- `space_of_stations_raw`: the same as `space_of_stations`, but also including the 11 edges that were removed manually, which the authors published in a separate file, marked by an edge property.
+- `temporal`: every individual connection of the day, as in `space_of_changes` but without aggregation: a directed edge from $i$ to $j$ for each train trip that serves $j$ after $i$, with the time of departure from $i$ and the duration of the journey. There are many parallel edges.
+
+The `temporal` network also has 83 self-loops, the connections of trips that return to their station (at two stations, presumably on loop lines), which do not appear in the aggregated networks. Times are rounded to the minute; connections taking less than a minute have a duration of 0. The dataset has two versions on figshare (July and October 2025), which contain the same nodes and edges; the second adds the node property `traffic_valid`, and its files are used here.
+
+The networks have the following properties:
+
+- Nodes: `name`, the name of the station; `bpuic`, its official number (Swiss/European station code); `canton` and `municipality`, where it is located (empty for the stations abroad); `company`, the company responsible for it; `latitude`, `longitude` and `elevation` (m), its position; `avg_daily_traffic`, `avg_daily_traffic_weekdays` and `avg_daily_traffic_weekends`, the average daily numbers of railway passengers boarding and alighting at the station (over all days, Monday to Friday, and weekends and public holidays; NaN where not available; the value 49 indicates an unreliable measurement); `traffic_valid`, whether these numbers are available and reliable.
+- Edges of `space_of_changes` and `space_of_stops`: `num_connections`, the number of trains per day connecting the two stations; `avg_duration`, the average duration of the connection (minutes).
+- Edges of `space_of_stations` and `space_of_stations_raw`: `distance_geodesic`, the geodesic distance between the two stations (km); `distance_exact`, the distance along the tracks (km), available only for the lines of the Swiss Federal Railways (NaN otherwise); `manually_added`, whether the edge was added manually; in `space_of_stations_raw` also `manually_removed`, whether the edge was removed manually (and is therefore absent from `space_of_stations`).
+- Edges of `temporal`: `time`, the departure time (Unix time; the times run from midnight of March 5, 2025, Central European Time, into the morning of March 6); `start`, the departure time in minutes after midnight of March 5, as given in the data; `duration`, the duration of the connection (minutes).
+
+Only the `space_of_stations` and `space_of_stations_raw` networks are laid out as spatial networks (without the partition of the stochastic block model)."""
+tags = ['Transportation', 'Public Transport', 'Spatial', 'Weighted', 'Multigraph', 'Timestamps', 'Metadata']
+url = ['https://doi.org/10.6084/m9.figshare.29609615', 'https://opentransportdata.swiss/en/']
+citation = [('M. Sterchi, E. Raubach, L. Hilfiker, "SwissRailNet: Four representations of a spatially embedded railway network dataset." Data in Brief 63, 112266 (2025)', 'https://doi.org/10.1016/j.dib.2025.112266'),
+ ('M. Sterchi, E. Raubach, L. Hilfiker, "Four representations of the Swiss railway network." figshare, dataset, version 2 (2025)', 'https://doi.org/10.6084/m9.figshare.29609615.v2')]
+upstream_license = ("Creative Commons Attribution 4.0 International (CC BY 4.0)",
+ "https://creativecommons.org/licenses/by/4.0/")
+icon_hash = None
+# figshare files (version 2 of the dataset), by id:
+# nodelist.csv, edgelist_SoCha.csv, edgelist_SoSto.csv, edgelist_SoSta.csv,
+# edges_added_SoSta.csv, edgelist_temporal.csv, edges_removed_SoSta.csv
+upstream_prefix = 'https://api.figshare.com/v2/file/download'
+files = [(("58905211", "58905214"), "space_of_changes", None),
+ (("58905211", "58905220"), "space_of_stops", None),
+ (("58905211", "58905217", "56423534"), "space_of_stations", None),
+ (("58905211", "58905223"), "temporal", None),
+ (("58905211", "58905217", "56423534", "56423540"),
+ "space_of_stations_raw", None)]
+
+# Midnight of March 5, 2025 (Central European Time), as Unix time
+T0 = 1741129200
+
+def fetch_upstream(force=False):
+ return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+ force)
+
+def read_table(name, fname):
+ with open_upstream_file(name, fname, "r") as f:
+ return list(csv.DictReader(f, delimiter=";"))
+
+def to_float(x):
+ return float(x) if x != "" else numpy.nan
+
+@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
+ nodes = read_table(name, fnames[0])
+ idx = {r["BPUIC"]: i for i, r in enumerate(nodes)}
+ g = Graph(directed=not alt.startswith("space_of_stations"))
+ g.add_vertex(len(nodes))
+ g.vp.name = g.new_vp("string", vals=[r["STATION_NAME"] for r in nodes])
+ g.vp.bpuic = g.new_vp("int", vals=[int(r["BPUIC"]) for r in nodes])
+ for k in ["canton", "municipality", "company"]:
+ g.vp[k] = g.new_vp("string", vals=[r[k.upper()] for r in nodes])
+ for k in ["latitude", "longitude", "elevation", "avg_daily_traffic",
+ "avg_daily_traffic_weekdays", "avg_daily_traffic_weekends"]:
+ g.vp[k] = g.new_vp("double", vals=[to_float(r[k.upper()])
+ for r in nodes])
+ g.vp.traffic_valid = g.new_vp("bool", vals=[r["TRAFFIC_VALID"] == "True"
+ for r in nodes])
+ edges = read_table(name, fnames[1])
+ if alt == "space_of_stations_raw":
+ removed = read_table(name, fnames[3])
+ edges += removed
+ g.add_edge_list([(idx[r["BPUIC1"]], idx[r["BPUIC2"]]) for r in edges])
+ if alt in ["space_of_changes", "space_of_stops"]:
+ g.ep.num_connections = g.new_ep("int", vals=[int(r["NUM_CONNECTIONS"])
+ for r in edges])
+ g.ep.avg_duration = g.new_ep("double", vals=[float(r["AVG_DURATION"])
+ for r in edges])
+ elif alt.startswith("space_of_stations"):
+ g.ep.distance_geodesic = g.new_ep("double", vals=[to_float(r["DISTANCE_GEODESIC"])
+ for r in edges])
+ g.ep.distance_exact = g.new_ep("double", vals=[to_float(r["DISTANCE_EXACT"])
+ for r in edges])
+ added = set((r["BPUIC1"], r["BPUIC2"])
+ for r in read_table(name, fnames[2]))
+ g.ep.manually_added = g.new_ep("bool", vals=[(r["BPUIC1"], r["BPUIC2"]) in added
+ for r in edges])
+ if g.ep.manually_added.a.sum() != len(added):
+ raise ValueError("manually added edges not found")
+ if alt == "space_of_stations_raw":
+ g.ep.manually_removed = g.new_ep("bool")
+ g.ep.manually_removed.a[-len(removed):] = True
+ else:
+ start = numpy.array([int(r["START"]) for r in edges])
+ g.ep.time = g.new_ep("int64_t", vals=T0 + 60 * start)
+ g.ep.start = g.new_ep("int", vals=start)
+ g.ep.duration = g.new_ep("int", vals=[int(r["DURATION"])
+ for r in edges])
+ yield alt, g
+
+
+def get_pos(g, sbm_fit):
+ # Only the space-of-stations networks are spatial (nearly planar): it is
+ # laid out with the default parameters of cfd_layout(), without the
+ # partition of the SBM fit; the others are laid out as usual, grouped by
+ # the hierarchical partition of the SBM fit
+ if "space_of_stations" in g.gp.get("name", ""):
+ return cfd_layout(g)
+ bs = sbm_fit[0] if sbm_fit is not None else None
+ return cfd_layout(g, b=bs)