Migrating from 1.0.0a2 to 2.0¶
scikit-route 2.0 is a rewrite. The eight solver classes of 1.0 keep their names, but the way you hand them a problem, the way you read the result and the meaning of the multi-trip objective all changed. This page maps every public name of 1.0.0a2 to its 2.0 counterpart and shows each change with a before/after snippet.
The matrix order is reversed and time_matrix is keyword-only
1.0's fit(route_example, time_matrix, cost_matrix) becomes
fit(cost_matrix, time_matrix=time_matrix, depot=route_example[0]).
Passing the time matrix positionally raises TypeError, on purpose: a migrated
call that kept the old order would otherwise swap two square matrices silently
and price hours as euros.
1. Installation and imports¶
pip install --upgrade scikit-route # Python >= 3.11; wheels for Linux, macOS, Windows
pip install "scikit-route[pandas]" # DataFrame inputs and as_frame=True loaders
The 2020 alpha reached PyPI under the distribution name skroute (1.0.0a1); 2.0 is published as scikit-route, the name the repository has always used, so pip install --upgrade skroute does not find it — install scikit-route instead. The import name is skroute in both.
TensorFlow, scikit-learn, googlemaps and tqdm are no longer dependencies. The old
import paths still work for the 2.x series and emit a DeprecationWarning:
| 1.0.0a2 | 2.0 | notes |
|---|---|---|
skroute.heuristics.brute.BruteForce |
skroute.exact.BruteForce |
shim re-exports with DeprecationWarning |
skroute.heuristics.NRBS.NRBS |
skroute.construction.NRBS |
the five exponents now default to 1.0 and must be >= 0 (1.0 validated only the type); distance_weigth → distance_weight; fit(start, ids, cost) → fit(cost, depot=start) |
skroute.metaheuristics.genetics.Genetic |
skroute.metaheuristics.Genetic |
p_c→p_crossover, p_m→p_mutation, pop→pop_size, gen→n_generations, k→tournament_size, early_stopping→patience |
skroute.metaheuristics.genetics.EnsembleGenetic |
skroute.ensemble.EnsembleGenetic |
n_genetics kept; or use MultiStart(Genetic(...)) |
skroute.metaheuristics.simulated_annealing.SimulatedAnnealing |
skroute.metaheuristics.SimulatedAnnealing |
temp→t0, delta→alpha (1.0 silently rescaled it into 0.9–1), tol→t_min, neighbours→n_moves |
skroute.metaheuristics.simulated_annealing.EnsembleSimulatedAnnealing |
skroute.ensemble.EnsembleSimulatedAnnealing |
n_simulateds kept (default 20 → 10) |
skroute.metaheuristics.tabu_search.TabuSearch |
skroute.metaheuristics.TabuSearch |
searchs→n_iter, tabu_length/tabu_var→tenure, p_m dropped |
skroute.metaheuristics.som.SOM |
skroute.metaheuristics.SOM |
units→n_units, lr→learning_rate, fit(nodes, epochs) → fit(X, coords=...) with n_iter in __init__ |
Every solver is also importable directly from skroute.
2. fit before and after¶
In 1.0 the problem data (max_time_work, extra_cost, people) lived in
__init__ and fit took a "route example" whose first element was the depot. In 2.0
the algorithm knobs stay in __init__ and everything that describes the instance
goes to fit:
# 1.0.0a2
ga = Genetic(p_m=0.3, pop=400, gen=2000, k=5, early_stopping=100, max_time_work=6, extra_cost=12.83)
cost, route = ga.fit(route_example, time_matrix, cost_matrix)
# 2.0
from skroute import Genetic
ga = Genetic(
p_mutation=0.3, pop_size=400, n_generations=2000, tournament_size=5, patience=100, random_state=0
)
ga.fit(cost_matrix, time_matrix=time_matrix, depot=route_example[0], max_time_work=6.0, extra_cost=12.83)
ga.cost_, ga.route_
The same shape applies to SimulatedAnnealing, TabuSearch, BruteForce and the
two Ensemble* wrappers. NRBS.fit(start_point_id, ids_route, cost_matrix) becomes
NRBS().fit(cost_matrix, depot=start_point_id). SOM.fit(nodes, epochs) becomes
SOM(n_iter=epochs).fit(cost_matrix, coords=xy) where xy is the (n, 2) array of
coordinates (2.0 evaluates every solution on a dense cost matrix; see section 9).
3. The return value became attributes¶
fit returns the estimator. The result lives in trailing-underscore attributes:
| attribute | meaning |
|---|---|
tour_ |
the open giant tour, depot first (labels) — the warm-start format |
route_ |
the route as driven: depot, trip 1, depot, trip 2, …, depot |
trips_ |
one closed array per trip |
cost_ |
the objective, always recomputed from route_ |
n_trips_, trip_costs_, trip_times_ |
per-trip figures (trip_times_ only with a time matrix) |
history_, n_iter_, stop_reason_ |
iterative solvers only |
is_optimal_ |
exact solvers only |
1.0's history_ held raw costs per generation; 2.0's history_ is the best cost so
far after each outer iteration, so it never increases.
4. route_example is gone¶
1.0 needed a list of node ids whose first element was the depot. 2.0 identifies nodes
by the rows of the matrix (or by the labels of a DataFrame/dict, or by labels=),
takes the depot with depot=<label> (default: the first node) and takes an optional
starting tour with init= in __init__: "nearest_neighbour" (default), "random",
or the tour_/route_ of another solver.
5. The objective changed in three ways¶
- A trip may never exceed
max_time_work, including the return leg. 1.0 checked the elapsed time before adding a leg and could overshoot the budget by one leg and never checked the final return. peoplemultiplies onlyextra_cost(as 1.0's docstring said; its code also multiplied travel cost, which never changed the argmin but changed reported costs).- Two decoders of the giant tour into trips are available:
split="greedy"(default; the corrected successor of the 1.0 rule) andsplit="optimal"(the minimum-cost partition into feasible trips, Prins 2004).optimalis never worse thangreedyfor the same tour.
Because of 1 and 2, costs reported by 2.0 differ from 1.0 on the same route; the problem model page works the Barcelona example through.
6. Hyper-parameters renamed and defaults that changed¶
Renames are in the table of section 1. Results obtained with default parameters differ from 1.0 because the defaults changed:
| class | 1.0 default | 2.0 default |
|---|---|---|
BruteForce, TabuSearch |
max_time_work=8, extra_cost, people in __init__ |
plain TSP unless max_time_work= is passed to fit |
Genetic |
p_c=0.6, p_m=0.4, pop=400, gen=1600, k=3 |
p_crossover=0.9, p_mutation=0.2, pop_size=100, n_generations=500, tournament_size=3, patience=100 |
EnsembleGenetic |
pop=400, gen=1000 |
pop_size=100, n_generations=500 |
SimulatedAnnealing |
temp=12.0, neighbours=250, delta=0.78, tol=1.29 |
t0="auto", n_moves=10·n, alpha=0.995, t_min="auto", patience=None |
EnsembleSimulatedAnnealing |
n_simulateds=20 |
n_simulateds=10 |
TabuSearch |
searchs=1250, p_m=0.6, tabu_length=45, tabu_var=10 |
n_iter=1000, tenure="auto" (p_m dropped) |
SOM |
radius_decay=0.9991, lr_decay=0.9991, fit(..., epochs=10_000) |
radius_decay=0.9997, lr_decay=0.99997, n_iter=100_000 |
NRBS |
no defaults (distance_weigth), negative exponents accepted |
all five 1.0 (distance_weight), exponents >= 0 |
7. Every public name of 1.0.0a2¶
| 1.0 name | status | 2.0 replacement | reason |
|---|---|---|---|
| the 8 classes of section 1 | kept (moved) | see section 1 | — |
EnsembleGenetic, EnsembleSimulatedAnnealing |
kept (moved) | skroute.ensemble.*; or MultiStart(Genetic(...)) |
explicit-parameter wrappers over MultiStart |
preprocessing.dfcolumn_to_dict |
replaced | pairs_to_matrix |
the result depended on row order |
preprocessing.matrix_to_dict |
replaced | to_dict_of_dicts |
referenced an undefined variable |
preprocessing.normalize(df, lat, lon) |
replaced | normalize_coords(coords) |
numpy in, numpy out |
preprocessing.df_to_tuple |
removed | Bunch.coords/labels, or df[[...]].to_numpy() |
one-line pandas idiom |
preprocessing.matrix_parse |
removed | pairs_to_matrix + distance_matrix |
superseded |
preprocessing.DataLossWarning |
removed | — | nothing emits it any more |
preprocessing.CostScraper |
deprecated wrapper | GoogleDistanceMatrix(api_key).fetch(coords, labels) |
removed in 3.0 |
cluster.KMeansTruncate |
removed | sklearn.cluster.KMeans(max_iter=1) |
scikit-learn is no longer a dependency |
cluster.{KMeans, DBSCAN, AffinityPropagation, GaussianMixture} |
removed | import from scikit-learn | same |
27 load_<country> + 5 cost loaders |
kept | same names; return a Bunch |
"DataFrame" → frame (with as_frame=True), "feature_names" dropped, DESCR kept |
load_costs_qatar |
deprecated alias | load_qatar_costs |
1.0 loaded Valencia |
mode= in TSP loaders |
deprecated | n_nodes= |
1.0 returned 0 rows for wi29 "small" |
fit(route_example, time_matrix, cost_matrix) |
changed | fit(cost, time_matrix=..., depot=..., init=...) |
see the warning at the top |
8. Datasets¶
Loaders return a Bunch (a dict with attribute access) instead of a dict with a
"DataFrame" key. The cost datasets expose square matrices ready for fit:
from skroute.datasets import load_barcelona
bcn = load_barcelona() # cost (EUR), time (h), distance (m), coords, labels, depot, units, DESCR
bcn.cost.shape, bcn.depot # (19, 19), 10000007
Loader matrices are plain arrays: pass labels=bcn.labels to fit so that
depot=bcn.depot and the returned route_ use the dataset's ids, or call
load_barcelona(as_frame=True) to get labelled DataFrames (requires pandas).
The 27 national instances are loaded with load_tsp("wi29") (or the old
load_sahara() names) and carry coords, labels, optimal_tour_length and a
cached distance_matrix() method; mode="small"|"medium"|"big" still works with a
DeprecationWarning — use n_nodes= instead.
9. SOM needs coordinates and a matrix¶
1.0's SOM took a tuple of (id, lat, lon) tuples and TensorFlow. 2.0's SOM is numpy
only and, like every other solver, evaluates its tours on the dense cost matrix:
SOM(random_state=0).fit(C, coords=xy). That matrix is the practical ceiling of 2.0
(about 20 000 nodes, 3.2 GB); the four bundled instances above it can be read and
subsampled with load_tsp(name, n_nodes=5000) but not solved whole. Coordinate-only
fitting is planned for 2.1.
10. Ensemble* → MultiStart¶
EnsembleGenetic and EnsembleSimulatedAnnealing remain as thin wrappers, but the
general tool is MultiStart(estimator, n_restarts=10, n_jobs=None), which runs any
stochastic solver from independent seeds in parallel (threads by default, since the
kernels release the GIL) and keeps the best result. Results do not depend on n_jobs.
11. Extras and dropped dependencies¶
| 1.0 | 2.0 |
|---|---|
| tensorflow (SOM) | gone — numpy implementation |
scikit-learn (cluster re-exports) |
gone — import scikit-learn yourself |
| googlemaps (hard) | optional: pip install "scikit-route[google]" |
| pandas (hard) | optional: pip install "scikit-route[pandas]" |
| tqdm | gone — progress goes to the skroute logger (skroute.set_log_level("INFO")) |
12. CostScraper → GoogleDistanceMatrix¶
GoogleDistanceMatrix(api_key, mode="driving").fetch(coords, labels) batches the
requests (1.0 issued one request per pair — 18 336 for Qatar) and returns a Bunch
with distance (m), time (h), labels and units. CostScraper still accepts the
1.0 constructor and scrap()/pandas() but warns; to_pickle() is gone. Both live
behind the google extra.
A worked, deterministic example¶
The dict-of-dicts input of 1.0 is still accepted; the depot is the first key, as
route_example[0] used to be:
>>> from skroute import BruteForce
>>> cost = {1: {1: 0, 2: 5, 3: 9, 4: 10}, 2: {1: 5, 2: 0, 3: 4, 4: 8},
... 3: {1: 9, 2: 4, 3: 0, 4: 3}, 4: {1: 10, 2: 8, 3: 3, 4: 0}}
>>> hours = {1: {1: 0, 2: 1, 3: 2, 4: 2}, 2: {1: 1, 2: 0, 3: 1, 4: 2},
... 3: {1: 2, 2: 1, 3: 0, 4: 1}, 4: {1: 2, 2: 2, 3: 1, 4: 0}}
>>> bf = BruteForce().fit(cost, time_matrix=hours, max_time_work=4.0, extra_cost=3.0)
>>> bf.route_.tolist(), bf.cost_, bf.n_trips_
([1, 2, 3, 1, 4, 1], 41.0, 2)
The single trip 1-2-3-4-1 would need 5 hours, so the tour breaks after node 3:
trips [1, 2, 3, 1] (cost 18) and [1, 4, 1] (cost 20) plus one extra-trip charge
of 3.0 give 41.0.