Preprocessing¶
Every solver in scikit-route consumes a dense (n, n) cost matrix.
skroute.preprocessing builds such matrices from what you usually have:
- coordinates —
distance_matrixwith a planar, TSPLIB 95 or great-circle metric (and the shorthandseuclidean_matrix/haversine_matrix); - a long table of
(origin, destination, value)rows —pairs_to_matrix, the order-independent replacement of 1.0'sdfcolumn_to_dict; - a dict of dicts
{i: {j: value}}—from_dict_of_dictsandto_dict_of_dicts; - the road network —
travel_time_matrixasks an OSRM server (the public demo by default, no key) or the Google Distance Matrix API for real driving times and distances between(lat, lon)points;skroute.preprocessing.google.GoogleDistanceMatrixis the Google client it delegates to (optionalgooglemapsextra, billed to your account); - the world itself —
geocodeturns an address into coordinates (Nominatim or Google) andfetch_poislists the OpenStreetMap points of interest of an area matching a brand, a name or an amenity (Overpass). The user guide walks through them.
normalize_coords rescales planar coordinates into the unit square while
preserving their aspect ratio (what the SOM feeds its ring with).
>>> from skroute.preprocessing import distance_matrix, pairs_to_matrix
>>> xy = [[0.0, 0.0], [3.0, 4.0], [6.0, 8.0]]
>>> distance_matrix(xy).tolist()
[[0.0, 5.0, 10.0], [5.0, 0.0, 5.0], [10.0, 5.0, 0.0]]
>>> M, labels = pairs_to_matrix([1, 1, 2], [2, 3, 3], [5.0, 9.0, 4.0])
>>> labels.tolist(), M[0].tolist()
([1, 2, 3], [0.0, 5.0, 9.0])
TSPLIB metrics¶
The tsplib_* metrics reproduce the TSPLIB 95 edge-weight types exactly, with
nint(x) = floor(x + 0.5) (never numpy.rint, which rounds half to even and
evaluates the published optimum of qa194 to 9351 instead of 9352):
| Metric | TSPLIB type | Distance |
|---|---|---|
tsplib_euc_2d |
EUC_2D |
nint(sqrt(dx² + dy²)) |
tsplib_ceil_2d |
CEIL_2D |
ceil(sqrt(dx² + dy²)) |
tsplib_man_2d |
MAN_2D |
nint(abs(dx) + abs(dy)) |
tsplib_att |
ATT |
r = sqrt((dx² + dy²) / 10); t = nint(r); t + 1 if t < r else t |
tsplib_geo |
GEO |
great circle on a sphere of radius 6378.388 km from DDD.MM coordinates, truncated plus one |
haversine |
— | great circle in km on a sphere of radius 6371.0088 km from decimal degrees |
GEO converts each DDD.MM coordinate with deg = int(x); m = x - deg;
rad = PI * (deg + 5 m / 3) / 180 where PI = 3.141592 and int truncates
(the convention of Concorde and of the published optima: ulysses16.opt.tour
evaluates to 6859 this way, 6917 with nint), then
d = int(RRR * acos(0.5 * ((1 + q1) q2 - (1 - q1) q3)) + 1) with
RRR = 6378.388, q1 = cos(lon_i - lon_j), q2 = cos(lat_i - lat_j),
q3 = cos(lat_i + lat_j).
Distances from coordinates¶
distance_matrix ¶
distance_matrix(coords: ArrayLike, metric: str = 'euclidean', *, block_size: int = 2048) -> NDArray[float64]
Dense (n, n) distance matrix of a set of points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords
|
array-like of shape (n, 2)
|
One row per node. For |
required |
metric
|
('euclidean', 'manhattan', 'tsplib_euc_2d', 'tsplib_ceil_2d', 'tsplib_man_2d', 'tsplib_att', 'tsplib_geo', 'haversine')
|
Distance definition; the TSPLIB metrics reproduce the TSPLIB 95 edge-weight
types |
"euclidean"
|
block_size
|
int
|
Number of rows computed per block. Bounds the peak memory of the temporaries
to a few |
2048
|
Returns:
| Type | Description |
|---|---|
ndarray of shape (n, n), dtype float64
|
C-contiguous, symmetric, with a zero diagonal. |
Warns:
| Type | Description |
|---|---|
UserWarning
|
When |
Notes
Every TSPLIB metric follows the TSPLIB 95 definitions with
nint(x) = floor(x + 0.5) (tsplib_nint, never numpy.rint):
tsplib_euc_2d:nint(sqrt(dx**2 + dy**2))tsplib_ceil_2d:ceil(sqrt(dx**2 + dy**2))tsplib_man_2d:nint(|dx| + |dy|)tsplib_att:r = sqrt((dx**2 + dy**2) / 10); t = nint(r); d = t + 1 if t < r else ttsplib_geo: eachDDD.MMcoordinate is converted with ``deg = int(x); m = x- deg; rad = PI * (deg + 5 m / 3) / 180
wherePI = 3.141592(intis truncation, the convention of Concorde and of the published optima: the TSPLIB booklet printsnintthere, which evaluatesulysses16.opt.tourto 6917 instead of 6859); then withRRR = 6378.388,q1 = cos(lon_i - lon_j),q2 = cos(lat_i - lat_j),q3 = cos(lat_i + lat_j)the distance isint(RRR * acos(0.5 * ((1 + q1) q2 - (1 - q1) q3)) + 1)-- truncation plus one, notnint``.
haversine uses the great-circle formula on a sphere of radius 6371.0088 km and
returns kilometres. The diagonal is set to zero for every metric (GEO would
otherwise give 1 there); no solver ever reads it.
Complexity is O(n**2) time and memory for the result.
References
G. Reinelt, TSPLIB 95, Universitaet Heidelberg, 1995, section 2.1.
Examples:
>>> import numpy as np
>>> from skroute.preprocessing import distance_matrix
>>> xy = np.array([[0.0, 0.0], [3.0, 4.0], [6.0, 8.0]])
>>> distance_matrix(xy).tolist()
[[0.0, 5.0, 10.0], [5.0, 0.0, 5.0], [10.0, 5.0, 0.0]]
>>> distance_matrix(xy, metric="manhattan")[0].tolist()
[0.0, 7.0, 14.0]
>>> float(distance_matrix([[0.0, 0.0], [1.0, 1.0]], metric="tsplib_euc_2d")[0, 1]) # nint(1.414...)
1.0
>>> float(distance_matrix([[0.0, 0.0], [1.0, 1.0]], metric="tsplib_ceil_2d")[0, 1])
2.0
euclidean_matrix ¶
Plain Euclidean distance matrix; shorthand for distance_matrix(coords, "euclidean").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords
|
array-like of shape (n, 2)
|
Planar |
required |
Returns:
| Type | Description |
|---|---|
ndarray of shape (n, n), dtype float64
|
|
Examples:
haversine_matrix ¶
Great-circle distances in kilometres; shorthand for distance_matrix(latlon, "haversine").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
latlon
|
array-like of shape (n, 2)
|
|
required |
Returns:
| Type | Description |
|---|---|
ndarray of shape (n, n), dtype float64
|
Kilometres on a sphere of radius 6371.0088 km. |
Examples:
tsplib_nint ¶
Round to the nearest integer the TSPLIB 95 way: nint(x) = floor(x + 0.5).
Half-integers round up (2.5 -> 3.0), unlike numpy.rint, which rounds
half to even (2.5 -> 2.0). The difference is not academic: with np.rint the
Waterloo instance qa194 evaluates its published optimal tour to 9351 instead of
9352 (SPEC D15).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array-like of float
|
Values to round. A scalar returns a Python |
required |
Returns:
| Type | Description |
|---|---|
ndarray of float64 or float
|
|
Examples:
Long tables and dicts¶
The labels array these functions return follows the rule of every label array
in scikit-route: int64 when every label is an integer, object otherwise, so
mixed labels such as [1, "a"] keep their Python types (numpy.asarray alone
would turn the 1 into "1" and depot=1 would never match).
pairs_to_matrix ¶
pairs_to_matrix(origin: ArrayLike, destination: ArrayLike, value: ArrayLike, *, symmetric: bool = True, labels: ArrayLike | None = None, fill: float | None = None) -> tuple[NDArray[float64], NDArray[Any]]
Pivot a long table of (origin, destination, value) rows into a dense matrix.
This is the 2.0 replacement of dfcolumn_to_dict, whose result depended on the
row order of the table. Here every row is placed by its labels, so the order of
the rows is irrelevant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origin
|
sequence of hashable, length m
|
Node labels of each row (lists, arrays or DataFrame columns of equal length). |
required |
destination
|
sequence of hashable, length m
|
Node labels of each row (lists, arrays or DataFrame columns of equal length). |
required |
value
|
sequence of float, length m
|
The quantity of each row (cost, seconds, metres, ...). |
required |
symmetric
|
bool
|
When |
True
|
labels
|
sequence of hashable
|
Order of the rows/columns of the matrix. Defaults to the order of first
appearance while scanning the rows (origin, then destination). Every
origin and destination must be in |
None
|
fill
|
float
|
Value for the entries that are neither in the table nor (with
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
matrix |
ndarray of shape (n, n), dtype float64
|
Dense matrix in the order of |
labels |
ndarray of shape (n,), dtype int64 or object
|
The labels, in the order used for the matrix and exactly as given (a label
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the three sequences differ in length, a label is missing from |
Notes
When the same (origin, destination) pair appears several times the last
row wins.
Examples:
>>> from skroute.preprocessing import pairs_to_matrix
>>> M, labels = pairs_to_matrix([1, 1, 2], [2, 3, 3], [5.0, 9.0, 4.0])
>>> labels.tolist()
[1, 2, 3]
>>> M.tolist()
[[0.0, 5.0, 9.0], [5.0, 0.0, 4.0], [9.0, 4.0, 0.0]]
>>> M, _ = pairs_to_matrix(["a", "b"], ["b", "a"], [1.0, 2.0], symmetric=False)
>>> M.tolist()
[[0.0, 1.0], [2.0, 0.0]]
to_dict_of_dicts ¶
Dense matrix -> {label_i: {label_j: value}} (the legacy dict-of-dicts input format).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
array-like of shape (n, n)
|
Square matrix. |
required |
labels
|
sequence of hashable of length n
|
Keys of the dictionaries, unique; defaults to |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Outer and inner keys in the order of |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the matrix is not square, or |
Examples:
from_dict_of_dicts ¶
from_dict_of_dicts(d: Mapping[Hashable, Mapping[Hashable, float]]) -> tuple[NDArray[float64], NDArray[Any]]
{label_i: {label_j: value}} -> (matrix, labels) in the order of the outer keys.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
d
|
mapping of mapping
|
Outer keys are the node labels; each inner mapping gives the values of that
row. A missing diagonal entry defaults to 0 (1.0's |
required |
Returns:
| Name | Type | Description |
|---|---|---|
matrix |
ndarray of shape (n, n), dtype float64
|
|
labels |
ndarray of shape (n,), dtype int64 or object
|
The outer keys as given: |
Examples:
Coordinates¶
normalize_coords ¶
Aspect-preserving min-max scaling of planar coordinates into the unit square.
Both axes are shifted to start at 0 and divided by the same factor, the
larger of the two ranges, so the longer side of the bounding box spans
[0, 1] and the shorter one [0, r] with r <= 1. Distances are
scaled uniformly and the geometry of the instance is preserved (this is what
SOM feeds its ring with).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords
|
array-like of shape (n, 2)
|
Finite coordinates, at least one row. |
required |
Returns:
| Type | Description |
|---|---|
ndarray of shape (n, 2), dtype float64
|
Coordinates in |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a shape other than |
Examples:
Map services¶
skroute.preprocessing.maps talks to the services with the standard library only
(urllib.request + json): every function honours timeout= (travel_time_matrix
forwards it to the googlemaps client as well) and retries a failed call up to three
times with back-off (0.5 s, 1 s, 2 s) on 429/5xx and connection errors — a
connection dropped while the answer was being read included — raising
MapServiceError afterwards, with the
status, the request URL (API keys redacted) and the first 200 characters of the body.
Coordinates are (lat, lon) everywhere in the API — the OSRM URLs are built lon,lat
internally. Results from OpenStreetMap (Nominatim, Overpass, most OSRM servers) are
ODbL data: show "© OpenStreetMap contributors" with them.
travel_time_matrix ¶
travel_time_matrix(coords: ArrayLike, *, provider: str = 'osrm', mode: str = 'driving', units: str = 'min', api_key: str | None = None, departure_time: Any = None, base_url: str | None = None, chunk_size: int | None = None, user_agent: str | None = None, pause: float = 1.0, timeout: float = 60) -> Bunch
Road travel times and distances between (lat, lon) points, from OSRM or Google.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords
|
array-like of shape (n, 2)
|
|
required |
provider
|
('osrm', 'google')
|
|
"osrm"
|
mode
|
str
|
For OSRM, the profile segment of the URL ( |
"driving"
|
units
|
('min', 's', 'h')
|
Unit of the returned |
"min"
|
api_key
|
str
|
Google Cloud key with the Distance Matrix API enabled; required by |
None
|
departure_time
|
('now', datetime or int)
|
Google only: asks for |
None
|
base_url
|
str
|
OSRM server, default |
None
|
chunk_size
|
int
|
OSRM: the points are split into |
None
|
user_agent
|
str
|
|
None
|
pause
|
float
|
Seconds to wait between consecutive OSRM requests ( |
1.0
|
timeout
|
float
|
Seconds to wait for each HTTP answer; for |
60
|
Returns:
| Type | Description |
|---|---|
Bunch
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Bad coordinates, units, provider, chunk size, or a Google request without a key. |
MapServiceError
|
The server could not be reached after the retries or answered an error. |
Warns:
| Type | Description |
|---|---|
RuntimeWarning
|
Once, when some pairs could not be routed: they are |
Notes
Each block request is GET {base_url}/table/v1/{mode}/{lon,lat;...}?sources=0;1;..&
destinations=..&annotations=duration,distance; the coordinate list is the row block
followed by the column block (just the block for a diagonal one). n = 120 points
with the default chunk_size mean 3 x 3 = 9 requests and 8 pauses. A diagonal
block of one point is never requested (its only cell is the diagonal 0, and OSRM
rejects a one-coordinate table): n = 3 with chunk_size=2 is 3 requests, with
chunk_size=1 6; a single point needs no request at all, with either provider.
Distances the server does not return stay nan in distance without making the
pair unroutable.
Examples:
>>> from skroute.preprocessing import travel_time_matrix
>>> office = (40.3272, -3.7635) # Leganés
>>> sol = (40.4168, -3.7038) # Puerta del Sol
>>> res = travel_time_matrix([office, sol])
>>> res
Bunch(coords, distance, provider, time, units)
>>> res.units, res.time.round(1).tolist()
({'time': 'min', 'distance': 'm'}, [[0.0, 23.8], [20.9, 0.0]])
>>> res.distance.round().tolist()
[[0.0, 16994.0], [14460.0, 0.0]]
geocode ¶
geocode(query: str, *, provider: str = 'nominatim', api_key: str | None = None, user_agent: str | None = None, timeout: float = 10, base_url: str | None = None) -> Bunch
Coordinates of one address or place name, from Nominatim (OpenStreetMap) or Google.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Free-text address or place, e.g. |
required |
provider
|
('nominatim', 'google')
|
|
"nominatim"
|
api_key
|
str
|
Google Cloud key with the Geocoding API enabled; required by |
None
|
user_agent
|
str
|
|
None
|
timeout
|
float
|
Seconds to wait for the HTTP answer. |
10
|
base_url
|
str
|
Server: default |
None
|
Returns:
| Type | Description |
|---|---|
Bunch
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Empty query, unknown provider, missing key, or no result for the query
( |
MapServiceError
|
The server could not be reached after the retries or answered an error. |
Notes
Nominatim is queried as GET {base_url}/search?format=jsonv2&limit=1&q=<query>
and the first hit is returned (Nominatim ranks by importance); Google as
GET {base_url}?address=<query>&key=<api_key> with results[0].geometry.location.
Results from Nominatim are OpenStreetMap data (ODbL): keep the attribution
"© OpenStreetMap contributors" when you show them.
Examples:
>>> from skroute.preprocessing import geocode
>>> office = geocode("Calle Ramón y Cajal 18, Leganés")
>>> office
Bunch(display_name, lat, lon, raw)
>>> round(office.lat, 4), round(office.lon, 4)
(40.3296, -3.7373)
>>> office.display_name
'18, Calle de Ramón y Cajal, ..., Leganés, Comunidad de Madrid, 28916, España'
fetch_pois ¶
fetch_pois(area: str, *, brand: str | None = None, name: str | None = None, amenity: str | None = None, wikidata: str | None = None, provider: str = 'overpass', base_url: str | None = None, timeout: float = 90, user_agent: str | None = None) -> Bunch
Points of interest from OpenStreetMap: every element matching some tags inside an area.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
area
|
str
|
Name of an administrative area exactly as tagged in OpenStreetMap
( |
required |
brand
|
str
|
Exact |
None
|
name
|
str
|
Case-insensitive regular expression on the |
None
|
amenity
|
str
|
Exact |
None
|
wikidata
|
str
|
Exact |
None
|
provider
|
'overpass'
|
Only the Overpass API is implemented. |
"overpass"
|
base_url
|
str
|
Overpass endpoint, default |
None
|
timeout
|
float
|
Seconds the server may spend on the query (its |
90
|
user_agent
|
str
|
|
None
|
Returns:
| Type | Description |
|---|---|
Bunch
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Empty area, no filter given, an empty filter value, or unknown provider. |
MapServiceError
|
The server could not be reached after the retries, answered an error, or
returned a |
Warns:
| Type | Description |
|---|---|
RuntimeWarning
|
Elements without coordinates (a relation Overpass could not centre) are skipped. |
UserWarning
|
Nothing matched: usually the area name does not match the OSM |
Notes
One Overpass QL query, sent as GET {base_url}?data=<query> (the public server
answers 406 to form-encoded POSTs)::
[out:json][timeout:90];
area["boundary"="administrative"]["name"="Comunidad de Madrid"]->.a;
nwr["amenity"="fast_food"]["brand:wikidata"="Q177054"](area.a);
out center;
out center makes ways and relations (a restaurant mapped as a building) points at
the centre of their bounding box; nodes keep their own position. Near-duplicates
are not removed: a shop mapped both as a building way and as a node inside it
appears twice. Drop them yourself when it matters, e.g. keep only nodes
(mask = [l.startswith("node/") for l in res.labels], then res.coords[mask]
and res.labels[mask]) or merge elements closer than a few metres with
skroute.preprocessing.haversine_matrix. The data is OpenStreetMap's (ODbL): show
"© OpenStreetMap contributors" with it.
Examples:
>>> from skroute.preprocessing import fetch_pois
>>> bk = fetch_pois("Leganés", amenity="fast_food", wikidata="Q177054")
>>> bk
Bunch(DESCR, addresses, coords, labels, names, tags)
>>> bk.coords.shape, bk.labels[:2].tolist(), bk.names[0]
((6, 2), ['node/2613719490', 'node/2631338026'], 'Burger King')
>>> print(bk.DESCR.splitlines()[-1])
Data © OpenStreetMap contributors, licensed under the Open Database License (ODbL): https://www.openstreetmap.org/copyright
MapServiceError ¶
Bases: RuntimeError
A map service answered with an error, malformed content, or could not be reached.
Raised after the retries of a failed HTTP call are exhausted (429 and 5xx are
retried, other statuses are not), when a service answers something that is not JSON,
or when the JSON reports an error ("code" != "Ok" from OSRM, a non-OK status
from Google, a remark from Overpass).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
Human-readable description. |
required |
status
|
int or None
|
HTTP status, |
None
|
url
|
str
|
The request URL (API keys redacted). |
""
|
body
|
str
|
The first 200 characters of the answer. |
""
|
Examples:
>>> from skroute.preprocessing.maps import MapServiceError
>>> err = MapServiceError("HTTP 429 from https://example.org/table: rate limited", status=429)
>>> err.status, str(err)
(429, 'HTTP 429 from https://example.org/table: rate limited')
Google Distance Matrix API¶
Install the extra with pip install scikit-route[google]. Requests are batched
batch_size × batch_size origins × destinations (the API caps a request at 100
elements); the 1.0 CostScraper issued one request per pair. departure_time= asks
for traffic-aware durations (duration_in_traffic, preferred over duration when
present); timeout= bounds each HTTP call (the library default is no limit);
travel_time_matrix(provider="google") wraps the client into the same Bunch as
OSRM and passes its timeout on.
GoogleDistanceMatrix ¶
GoogleDistanceMatrix(api_key: str, mode: str = 'driving', *, batch_size: int = 10, departure_time: Any = None, timeout: float | None = None)
Fetch road distances and travel times between coordinates from the Google Distance Matrix API.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
api_key
|
str
|
Credential of a Google Cloud project with the Distance Matrix API enabled. |
required |
mode
|
('driving', 'walking', 'bicycling', 'transit')
|
Travel mode passed to the API. |
"driving"
|
batch_size
|
int
|
Origins and destinations are sent |
10
|
departure_time
|
('now', datetime or int)
|
Forwarded to the API when given ( |
None
|
timeout
|
float
|
Seconds the |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
addresses_ |
list of str
|
After |
n_requests_ |
int
|
After |
Notes
googlemaps is imported lazily in __init__; install it with
pip install scikit-route[google]. Progress is logged to the skroute logger
at INFO (one record per request); enable it with
logging.basicConfig(level=logging.INFO) or skroute.set_log_level("INFO").
Elements the API cannot route (status != "OK", or an "OK" element without
a distance or duration value) become nan and are logged at WARNING;
complete them before solving (every solver needs finite matrices).
Examples:
>>> from skroute.preprocessing.google import GoogleDistanceMatrix
>>> gdm = GoogleDistanceMatrix("<your key>")
>>> res = gdm.fetch([(41.3874, 2.1686), (41.5518, 2.2473)], labels=[10000007, 1])
>>> res.distance.shape, res.units
((2, 2), {'distance': 'm', 'time': 'h'})
fetch ¶
fetch(coords: ArrayLike, labels: ArrayLike | None = None) -> Bunch
Request the full (n, n) distance and duration matrices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords
|
array-like of shape (n, 2)
|
|
required |
labels
|
sequence of length n
|
Unique node labels for the returned |
None
|
Returns:
| Type | Description |
|---|---|
Bunch
|
|
CostScraper ¶
Deprecated 1.0 interface over GoogleDistanceMatrix; removed in 3.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
api
|
str
|
API key. |
required |
nodes
|
list of (id, latitude, longitude)
|
One tuple per node, as in 1.0. |
required |
mode
|
str
|
Travel mode. |
"driving"
|
Notes
Emits DeprecationWarning on construction. scrap() returns the Bunch of
GoogleDistanceMatrix.fetch; pandas() builds the 1.0 long table (one
row per unordered pair, meters/seconds taken from the origin -> destination
direction); to_pickle() raises NotImplementedError.