import pandas as pd
from fastcore.test import test_eq
from trufl.optimizer import Optimizer
from trufl.sampler import rank_to_sampleHow trufl ranks areas
trufl ranks areas by sampling priority with multiple-criteria decision-making (MCDM). It scores each area against criteria and weights that you choose, then ranks the areas by score. Rank 1 is the highest sampling priority. Why trufl explains why trufl takes this approach.
The examples on this page use the six criteria and weights of Round II in Getting started, on a state table of four areas small enough to read in full.
Criteria
Each column of the state that a State returns is one criterion. You choose the columns through the callbacks you pass to State. Round II uses these six:
| Criterion | Callback | Type |
|---|---|---|
| Maximum sample value | MaxCB() |
Benefit |
| Minimum sample value | MinCB() |
Benefit |
| Standard deviation | StdCB() |
Benefit |
| Sample count | CountCB() |
Cost |
| Moran’s I | MoranICB(k=5) |
Cost |
| Prior estimate | PriorCB() |
Benefit |
A high value of a benefit criterion raises the priority of an area. A low value of a cost criterion raises it. In Round II, an area with few samples therefore ranks higher than a well-sampled one. An area whose values show no spatial trend, with a Moran’s I close to 0, ranks higher than one with a clear gradient, with a Moran’s I close to 1.
The state can hold any other quantitative variable as a criterion, such as population or whether an area has been remediated.
Here is a state for four areas. Areas 0 and 1 have high values, but area 0 has two samples and area 1 has twelve. Areas 2 and 3 have low values, with three and ten samples:
state = pd.DataFrame({'Max': [9.0, 9.5, 3.0, 2.5], 'Min': [2.0, 1.5, 0.5, 1.0],
'Standard Deviation': [2.5, 2.0, 0.8, 0.3], 'Count': [2, 12, 3, 10],
'Moran.I': [0.1, 0.6, 0.2, 0.7], 'Prior': [6.0, 7.0, 2.0, 1.5]},
index=pd.Index(range(4), name='loc_id'))
is_benefit = [True, True, True, False, False, True]
state| Max | Min | Standard Deviation | Count | Moran.I | Prior | |
|---|---|---|---|---|---|---|
| loc_id | ||||||
| 0 | 9.0 | 2.0 | 2.5 | 2 | 0.1 | 6.0 |
| 1 | 9.5 | 1.5 | 2.0 | 12 | 0.6 | 7.0 |
| 2 | 3.0 | 0.5 | 0.8 | 3 | 0.2 | 2.0 |
| 3 | 2.5 | 1.0 | 0.3 | 10 | 0.7 | 1.5 |
Weights
The weight vector sets the importance of each criterion relative to the others. The weights must sum to 1. If you pass w_vector=None, get_rank computes the weights from the data with w_method instead. mcdm describes the weighting methods.
With the Round II weights, area 0 ranks first. Area 1 has similar values but many more samples, and ranks second:
optimizer = Optimizer(state)
ranks = optimizer.get_rank(is_benefit_x=is_benefit, w_vector=[0.2, 0.1, 0.1, 0.2, 0.2, 0.2],
n_method='LINEAR1', s_method='CP')
test_eq(ranks['rank'].to_dict(), {0: 1, 1: 2, 2: 3, 3: 4})
ranks| rank | |
|---|---|
| loc_id | |
| 0 | 1 |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
Raise the weight of the sample count to 0.6 and the areas with few samples move up. Area 2, with low values but only three samples, now ranks above area 1:
count_heavy = [0.1, 0.05, 0.05, 0.6, 0.1, 0.1]
ranks = optimizer.get_rank(is_benefit_x=is_benefit, w_vector=count_heavy, n_method='LINEAR1', s_method='CP')
test_eq(ranks['rank'].to_dict(), {0: 1, 2: 2, 3: 3, 1: 4})
ranks| rank | |
|---|---|
| loc_id | |
| 0 | 1 |
| 2 | 2 |
| 3 | 3 |
| 1 | 4 |
Normalization
The criteria have different units and ranges, such as a number of samples and a contamination level. Before scoring, get_rank rescales every criterion with n_method. Round II uses LINEAR1. It divides each benefit criterion by its maximum, and divides the minimum of each cost criterion by its values. Every criterion then lies in [0, 1], and a higher value always means a higher priority. mcdm describes the other normalization methods.
Scoring: CP and TOPSIS
Both scoring methods compare each area with the ideal area, which has the best value of every criterion. Compromise programming (CP) scores each area by its weighted distance to the ideal area. The lowest score ranks first. TOPSIS also measures the distance to the worst possible area. It scores each area by its relative closeness to the ideal area, from 0 to 1. The highest score ranks first.
With the Round II weights, both methods rank the four areas in the same order. With the count-heavy weights, they disagree on the last place. TOPSIS ranks area 1 third and area 3 last:
ranks = optimizer.get_rank(is_benefit_x=is_benefit, w_vector=count_heavy, n_method='LINEAR1', s_method='TOPSIS')
test_eq(ranks['rank'].to_dict(), {0: 1, 2: 2, 1: 3, 3: 4})
ranks| rank | |
|---|---|
| loc_id | |
| 0 | 1 |
| 2 | 2 |
| 1 | 3 |
| 3 | 4 |
From ranks to sample counts
rank_to_sample turns the ranks into a number of samples for each area. It takes the ranks in loc_id order. The default Weighted policy gives each area a share of the budget proportional to 1/rank. For the TOPSIS ranks above and a budget of 20 samples:
ranks_by_loc = ranks['rank'].sort_index().values
rank_to_sample(ranks_by_loc, budget=20)array([10, 3, 5, 2])
The quantiles policy, which the tutorial uses, splits the areas into four quartiles by rank. The first quartile shares 50% of the budget, the second 30%, the third 20% and the last none. With four areas, each quartile holds one area:
rank_to_sample(ranks_by_loc, budget=20, policy='quantiles')array([10, 4, 6, 0])
sampler describes both policies and the min parameter.