Compute normalized distances from each stored solution in a
solutionset-class object to the observed ideal and/or nadir
point in objective space.
Usage
frontier_distances(
x,
objectives = NULL,
reference = "ideal",
metric = c("euclidean", "manhattan", "chebyshev")
)Arguments
- x
A
solutionset-classobject returned bysolve.- objectives
Optional character vector of objective names to use. If
NULL, all available objective-value columns are used.- reference
Character vector indicating the reference point or points to use. Allowed values are
"ideal"and"nadir". If both are supplied, distances and ranks for both reference points are returned. Default is"ideal".- metric
Character. Distance metric to use. One of
"euclidean","manhattan", or"chebyshev".
Value
A data.frame with one row per stored solution having complete
values for the selected objectives.
The table contains:
run_idandsolution_id;the original objective values;
normalized objective values prefixed with
norm_;distance and rank columns for the requested reference points.
The returned table also contains the following attributes:
"ideal": observed ideal point in the original objective scales;"nadir": observed nadir point in the original objective scales;"ranges": observed absolute ranges in the original objective scales;"objectives": objective names used;"sense": optimization sense of each objective;"metric": distance metric used;"reference": requested reference point or points;"normalized": whether normalized values were used;"space": objective space used for distance calculations.
Details
This function supports the interpretation and ranking of trade-offs among
solutions stored in a SolutionSet.
The calculations are based on the solutions contained in the supplied
object. Therefore, the observed ideal point, observed nadir point, objective
ranges, normalized values, and distances may change when the input
SolutionSet is filtered.
To calculate distances using only non-dominated solutions, first use:
x_nd <- solution_filter(x, nondominated = TRUE)
frontier_distances(x_nd)Objective values are internally transformed to a common minimization space
using the objective senses registered in
get_objective_specs. Objectives with
sense = "min" are kept unchanged, whereas objectives with
sense = "max" are multiplied by \(-1\). In this transformed space,
lower values are always better.
The observed ideal point is defined by the best observed value for each objective:
$$ z_j^{ideal} = \min_i z_{ij}, $$
where \(z_{ij}\) is the transformed value of solution \(i\) for objective \(j\).
The observed nadir point is defined by the worst observed value for each objective:
$$ z_j^{nadir} = \max_i z_{ij}. $$
These reference points are empirical bounds derived from the supplied solutions. They are not necessarily the true ideal and nadir points of the complete feasible objective space.
Objective values are normalized to the interval \([0,1]\) using:
$$ \tilde{z}_{ij} = \frac{z_{ij} - z_j^{ideal}} {z_j^{nadir} - z_j^{ideal}}. $$
After normalization:
0represents the best observed value for an objective;1represents the worst observed value for an objective.
This interpretation is independent of whether the original objective was minimized or maximized.
If an objective has zero observed range, all normalized values for that objective are set to zero. The objective therefore does not contribute to the calculated distances.
For distances to the ideal point, smaller values are preferred and
rank_to_ideal = 1 identifies the closest solution.
For distances to the nadir point, larger values are preferred and
rank_from_nadir = 1 identifies the solution farthest from the
observed nadir point.
Examples
pu <- data.frame(
id = 1:4,
cost = c(1, 2, 3, 4)
)
features <- data.frame(
id = 1:2,
name = c("sp1", "sp2")
)
dist_features <- data.frame(
pu = c(1, 1, 2, 3, 4),
feature = c(1, 2, 2, 1, 2),
amount = c(5, 2, 3, 4, 1)
)
actions <- data.frame(
id = c("conservation", "restoration")
)
effects <- data.frame(
action = rep(actions$id, each = 2),
feature = rep(features$id, times = 2),
multiplier = c(
1.0, 1.0,
1.5, 1.5
)
)
problem <- create_problem(
pu = pu,
features = features,
dist_features = dist_features,
cost = "cost"
) |>
add_actions(
actions = actions,
cost = c(
conservation = 1,
restoration = 2
)
) |>
add_effects(
effects = effects,
effect_type = "after"
) |>
add_constraint_targets_relative(0.05) |>
add_objective_min_cost(alias = "cost") |>
add_objective_max_benefit(alias = "benefit") |>
set_method_weighted_sum(
aliases = c("cost", "benefit"),
runs = run_grid(
n = 5,
include_extremes = TRUE
),
normalize_weights = TRUE
)
if (requireNamespace("rcbc", quietly = TRUE)) {
problem <- set_solver_cbc(
problem,
verbose = FALSE
)
solutions <- solve(problem)
# Normalized Euclidean distance to the observed ideal point
frontier_distances(solutions)
# Distances to both observed ideal and nadir points
distances <- frontier_distances(
solutions,
reference = c("ideal", "nadir")
)
# Use only selected objectives
frontier_distances(
solutions,
objectives = c("cost", "benefit")
)
# Use Manhattan distance
frontier_distances(
solutions,
metric = "manhattan"
)
# Use Chebyshev distance
frontier_distances(
solutions,
metric = "chebyshev"
)
# Inspect observed reference points and objective ranges
attr(distances, "ideal")
attr(distances, "nadir")
attr(distances, "ranges")
# Calculate distances only over non-dominated solutions
if (requireNamespace("moocore", quietly = TRUE)) {
nondominated_solutions <- solution_filter(
solutions,
feasible_only = TRUE,
nondominated = TRUE
)
frontier_distances(
nondominated_solutions,
reference = c("ideal", "nadir")
)
}
}
#> run_id solution_id cost benefit norm_cost norm_benefit distance_to_ideal
#> 1 1 s1 2 0.0 0.0000 1.00000000 1.0000000
#> 2 2 s2 3 3.5 0.0625 0.53333333 0.5369830
#> 3 4 s4 12 7.0 0.6250 0.06666667 0.6285455
#> 4 5 s5 18 7.5 1.0000 0.00000000 1.0000000
#> rank_to_ideal distance_to_nadir rank_from_nadir
#> 1 3 1.000000 3
#> 2 1 1.047227 1
#> 3 2 1.005851 2
#> 4 3 1.000000 3
