Skip to content

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_matrix with a planar, TSPLIB 95 or great-circle metric (and the shorthands euclidean_matrix / haversine_matrix);
  • a long table of (origin, destination, value) rows — pairs_to_matrix, the order-independent replacement of 1.0's dfcolumn_to_dict;
  • a dict of dicts {i: {j: value}} — from_dict_of_dicts and to_dict_of_dicts;
  • the road network — travel_time_matrix asks 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.GoogleDistanceMatrix is the Google client it delegates to (optional googlemaps extra, billed to your account);
  • the world itself — geocode turns an address into coordinates (Nominatim or Google) and fetch_pois lists 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 "euclidean", "manhattan" and the planar TSPLIB metrics the columns are (x, y) exactly as in a TSPLIB NODE_COORD_SECTION. For "tsplib_geo" the columns are (latitude, longitude) in the TSPLIB DDD.MM degrees-and-minutes notation; for "haversine" they are decimal degrees (latitude, longitude).

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 EUC_2D, CEIL_2D, MAN_2D, ATT and GEO exactly (see Notes).

"euclidean"
block_size int

Number of rows computed per block. Bounds the peak memory of the temporaries to a few block_size x n arrays instead of n x n; the result itself is always the full (n, n) matrix.

2048

Returns:

Type Description
ndarray of shape (n, n), dtype float64

C-contiguous, symmetric, with a zero diagonal.

Warns:

Type Description
UserWarning

When n > 20_000: the dense matrix alone needs more than 3.2 GB. The warning is attributed to the caller outside scikit-route, also through the shorthands and TSPBunch.distance_matrix.

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 t
  • tsplib_geo: each DDD.MM coordinate is converted with ``deg = int(x); m = x
  • deg; rad = PI * (deg + 5 m / 3) / 180wherePI = 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

euclidean_matrix(coords: ArrayLike) -> NDArray[float64]

Plain Euclidean distance matrix; shorthand for distance_matrix(coords, "euclidean").

Parameters:

Name Type Description Default
coords array-like of shape (n, 2)

Planar (x, y) coordinates.

required

Returns:

Type Description
ndarray of shape (n, n), dtype float64

Examples:

>>> from skroute.preprocessing import euclidean_matrix
>>> euclidean_matrix([[0.0, 0.0], [0.0, 2.0]]).tolist()
[[0.0, 2.0], [2.0, 0.0]]

haversine_matrix

haversine_matrix(latlon: ArrayLike) -> NDArray[float64]

Great-circle distances in kilometres; shorthand for distance_matrix(latlon, "haversine").

Parameters:

Name Type Description Default
latlon array-like of shape (n, 2)

(latitude, longitude) in decimal degrees.

required

Returns:

Type Description
ndarray of shape (n, n), dtype float64

Kilometres on a sphere of radius 6371.0088 km.

Examples:

>>> from skroute.preprocessing import haversine_matrix
>>> madrid, barcelona = (40.4168, -3.7038), (41.3874, 2.1686)
>>> D = haversine_matrix([madrid, barcelona])
>>> bool(500 < D[0, 1] < 510)  # about 505 km
True

tsplib_nint

tsplib_nint(x: ArrayLike) -> NDArray[float64] | float

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 float; an array returns an array of the same shape.

required

Returns:

Type Description
ndarray of float64 or float

floor(x + 0.5) as floating-point integers (the dtype stays float64 so the result can be stored straight into a cost matrix).

Examples:

>>> from skroute.preprocessing import tsplib_nint
>>> tsplib_nint(2.5), tsplib_nint(-2.5), tsplib_nint(2.49)
(3.0, -2.0, 2.0)
>>> tsplib_nint([0.5, 1.5, 2.5]).tolist()
[1.0, 2.0, 3.0]

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 an entry (i, j) that is absent from the table takes the value of (j, i) when that one is present. Entries given in both directions are kept as given (the result is then only symmetric if the data is).

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 labels.

None
fill float

Value for the entries that are neither in the table nor (with symmetric=True) mirrored. Without it, such an entry raises ValueError. The diagonal is exempt: a missing (i, i) is 0.

None

Returns:

Name Type Description
matrix ndarray of shape (n, n), dtype float64

Dense matrix in the order of labels.

labels ndarray of shape (n,), dtype int64 or object

The labels, in the order used for the matrix and exactly as given (a label 1 next to a label "a" stays the integer 1): int64 when every label is an integer, object otherwise -- the rule of every label array in scikit-route (skroute.utils.validation.coerce_labels).

Raises:

Type Description
ValueError

If the three sequences differ in length, a label is missing from labels, or an off-diagonal entry is missing without fill.

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

to_dict_of_dicts(matrix: ArrayLike, labels: ArrayLike | None = None) -> dict[Any, dict[Any, float]]

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 0..n-1.

None

Returns:

Type Description
dict

Outer and inner keys in the order of labels; values are Python floats.

Raises:

Type Description
ValueError

If the matrix is not square, or labels has the wrong length or repeats a label (two equal keys would silently collapse two rows into one).

Examples:

>>> from skroute.preprocessing import to_dict_of_dicts
>>> to_dict_of_dicts([[0, 5], [5, 0]], labels=["a", "b"])
{'a': {'a': 0.0, 'b': 5.0}, 'b': {'a': 5.0, 'b': 0.0}}

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 matrix_to_dict dropped zeros); any other missing entry raises ValueError, and so does an inner key that is not an outer key.

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: int64 when all are integers, object otherwise (strings, tuples, mixed types), as pairs_to_matrix.

Examples:

>>> from skroute.preprocessing import from_dict_of_dicts
>>> M, labels = from_dict_of_dicts({1: {2: 5.0}, 2: {1: 5.0}})
>>> M.tolist(), labels.tolist()
([[0.0, 5.0], [5.0, 0.0]], [1, 2])

Coordinates

normalize_coords

normalize_coords(coords: ArrayLike) -> NDArray[float64]

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 [0, 1]**2; all zeros when every point coincides.

Raises:

Type Description
ValueError

For a shape other than (n, 2), an empty input or a non-finite coordinate -- the same checks and messages as distance_matrix.

Examples:

>>> from skroute.preprocessing import normalize_coords
>>> normalize_coords([[10.0, 10.0], [30.0, 20.0]]).tolist()
[[0.0, 0.0], [1.0, 0.5]]

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)

(latitude, longitude) pairs in decimal degrees (finite, latitudes in [-90, 90], longitudes in [-180, 180]). Note the order: latitude first, as everywhere in scikit-route; the OSRM URLs are built lon,lat internally.

required
provider ('osrm', 'google')

"osrm" calls the table service of an OSRM server; "google" delegates to skroute.preprocessing.google.GoogleDistanceMatrix (needs api_key and the google extra; billed to your account).

"osrm"
mode str

For OSRM, the profile segment of the URL (/table/v1/<mode>/): the public demo server only serves "driving"; a self-hosted server names its profiles freely ("car", "bike", "foot"...). For Google, one of "driving", "walking", "bicycling", "transit".

"driving"
units ('min', 's', 'h')

Unit of the returned time matrix. Distances are always in metres.

"min"
api_key str

Google Cloud key with the Distance Matrix API enabled; required by "google", ignored by "osrm".

None
departure_time ('now', datetime or int)

Google only: asks for duration_in_traffic (preferred over duration when the answer carries it). OSRM has no traffic model: a value is ignored with a UserWarning.

None
base_url str

OSRM server, default https://router.project-osrm.org (the public demo: car profile, limited table size, one request per second is polite). Ignored by "google" (the googlemaps client knows its endpoint).

None
chunk_size int

OSRM: the points are split into ceil(n / chunk_size) blocks of nearly equal size and the table is requested block by block (sources = one block, destinations = another), so a request never carries more than 2 * chunk_size coordinates; default 50 (100 coordinates, the demo server's limit). Google: the batch_size of GoogleDistanceMatrix (1..10, default 10).

None
user_agent str

User-Agent header; default scikit-route/<version> (+repository URL). Ignored by "google" (the googlemaps client sends its own).

None
pause float

Seconds to wait between consecutive OSRM requests (0 for a server of yours). Ignored by "google" (its quota is per account, not per second).

1.0
timeout float

Seconds to wait for each HTTP answer; for "google", the timeout of the googlemaps client.

60

Returns:

Type Description
Bunch

time and distance: float64 (n, n) arrays (row = origin, column = destination; asymmetric when the roads are; diagonal 0); units: {"time": units, "distance": "m"}; provider; coords: the validated float64 (n, 2) array.

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 nan in both matrices (complete them before solving; every solver needs finite matrices). With "google", GoogleDistanceMatrix additionally logs the details (per block and a summary) on the skroute logger at WARNING. Also once when an OSRM server returns no distances table (annotations=distance unsupported): time is complete and distance is nan off the diagonal for those blocks.

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. "Calle Ramón y Cajal 18, Leganés".

required
provider ('nominatim', 'google')

"nominatim" uses the public OpenStreetMap geocoder (no key; usage policy: a descriptive User-Agent and at most one request per second -- both enforced here, the throttle by sleeping between consecutive calls); "google" uses the Geocoding API (needs api_key; billed to your account).

"nominatim"
api_key str

Google Cloud key with the Geocoding API enabled; required by "google".

None
user_agent str

User-Agent header; default scikit-route/<version> (+repository URL). For heavy use, Nominatim asks for one that identifies your application.

None
timeout float

Seconds to wait for the HTTP answer.

10
base_url str

Server: default https://nominatim.openstreetmap.org (a self-hosted Nominatim has the same /search endpoint) or the Google Geocoding JSON endpoint.

None

Returns:

Type Description
Bunch

lat and lon (floats, decimal degrees), display_name (the address the service resolved) and raw (the first result as returned by the service).

Raises:

Type Description
ValueError

Empty query, unknown provider, missing key, or no result for the query ("no result for ...").

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 (area["boundary"="administrative"]["name"=area]): "Comunidad de Madrid", "Leganés", "Catalunya"...

required
brand str

Exact brand tag ("Burger King").

None
name str

Case-insensitive regular expression on the name tag ("burger").

None
amenity str

Exact amenity tag ("fast_food", "pharmacy", "townhall").

None
wikidata str

Exact brand:wikidata tag -- the most reliable brand filter, immune to spelling variants ("Q177054" is Burger King).

None
provider 'overpass'

Only the Overpass API is implemented.

"overpass"
base_url str

Overpass endpoint, default https://overpass-api.de/api/interpreter.

None
timeout float

Seconds the server may spend on the query (its [timeout:], rounded up to a whole second); the HTTP socket waits this plus OVERPASS_HTTP_MARGIN (10 s), so the server's own remark arrives before the client gives up and retries.

90
user_agent str

User-Agent header; default scikit-route/<version> (+repository URL).

None

Returns:

Type Description
Bunch

coords: float64 (n, 2) array of (lat, lon); labels: object ndarray of OSM ids "node/123" / "way/123" / "relation/123" (the dtype every label array in scikit-route gives string labels, so a boolean mask indexes it like coords); names: list of str (the name tag, else brand, else ""); addresses: list of str ("<street> <housenumber>, <postcode> <city>" from the addr:* tags, "" when unknown); tags: list of dicts (all the tags of each element); DESCR: the query, the server, the count and the ODbL attribution. Elements are sorted by type and id -- i.e. by label -- so the order is deterministic.

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 remark (a query timeout or memory limit).

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 name tag of an administrative boundary.

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

MapServiceError(message: str, *, status: int | None = None, url: str = '', body: str = '')

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 when the server could not be reached.

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 batch_size x batch_size per request (the API caps a request at 100 elements, hence 1 <= batch_size <= 10). The 1.0 CostScraper issued one request per pair -- 18 336 requests for the 192 Qatar nodes; with batch_size=10 that is 400 requests.

10
departure_time ('now', datetime or int)

Forwarded to the API when given ("now", a datetime or a Unix timestamp): the answer then carries duration_in_traffic, which fetch prefers over duration for the time matrix. Traffic-aware requests are billed at the higher "Advanced" rate; mode must be "driving" for Google to honour it.

None
timeout float

Seconds the googlemaps client waits for each HTTP answer (its timeout, connect and read combined). None leaves the client's default (no limit) and sends no timeout at all; skroute.preprocessing.travel_time_matrix passes its own timeout here.

None

Attributes:

Name Type Description
addresses_ list of str

After fetch, the address Google resolved for each node (from the origin_addresses of the responses; "" when unresolved).

n_requests_ int

After fetch, the number of requests issued.

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)

(latitude, longitude) in decimal degrees.

required
labels sequence of length n

Unique node labels for the returned labels field; defaults to 0..n-1.

None

Returns:

Type Description
Bunch

distance (metres) and time (hours; duration_in_traffic when the answer carries it, else duration) as float64 (n, n) arrays, labels (int64 when every label is an integer, object otherwise -- the rule of skroute.utils.validation.coerce_labels) and units == {"distance": "m", "time": "h"}. The matrices are directional (Google's durations are not symmetric).

CostScraper

CostScraper(api: str, nodes: list[tuple[Any, ...]], mode: str = 'driving')

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.

scrap

scrap() -> Bunch

Fetch the matrices (billed to your account); the Bunch of GoogleDistanceMatrix.fetch.

Stored under result_ as well.

pandas

pandas() -> Any

The 1.0 long table as a DataFrame (requires pandas; calls scrap first if needed).

to_pickle

to_pickle(filename: str | None = None) -> None

Removed in 2.0.