Multi-Criteria Analysis — Advanced Usage
This notebook complements analyze_multicriteria.ipynb with advanced patterns:
Continuous scoring via
Input(..., score_routine=...)Mixed operators and threshold formats
Weighted aggregation with
MulticriteriAnalysis(..., method=Operator.WEIGHTED_SUM)
The examples use synthetic arrays for reproducibility.
[1]:
import numpy as np
import matplotlib.pyplot as plt
from wolfhece.multicriteria.analysis import (
Criteria,
Format,
Input,
MulticriteriAnalysis,
Operations,
Operator,
Scores,
)
1. Build reproducible test layers
[2]:
rng = np.random.default_rng(123)
shape = (8, 8)
depth = np.ma.MaskedArray(rng.uniform(0.0, 2.0, shape).astype(np.float32), mask=False)
velocity = np.ma.MaskedArray(rng.uniform(0.0, 1.5, shape).astype(np.float32), mask=False)
hazard = np.ma.MaskedArray((depth * velocity).astype(np.float32), mask=False)
print('depth range :', float(depth.min()), float(depth.max()))
print('velocity range:', float(velocity.min()), float(velocity.max()))
print('hazard range :', float(hazard.min()), float(hazard.max()))
depth range : 0.006362218409776688 1.8548145294189453
velocity range: 0.008835050277411938 1.4408305883407593
hazard range : 0.0035901775117963552 2.406481981277466
2. Define continuous scoring routines
A custom routine receives (values, criteria) and returns a masked array with the same shape/mask.
Below, we use a linear ramp in [0, 1].
[3]:
def linear_ramp(values: np.ma.MaskedArray, criteria: Criteria) -> np.ma.MaskedArray:
if not isinstance(criteria.threshold, tuple):
raise ValueError('linear_ramp expects threshold as tuple: (low, high)')
low, high = criteria.threshold
if high <= low:
raise ValueError('Invalid threshold tuple: high must be > low')
out = (values - low) / (high - low)
out = np.ma.clip(out, 0.0, 1.0)
return out.astype(np.float32)
def inverse_linear_ramp(values: np.ma.MaskedArray, criteria: Criteria) -> np.ma.MaskedArray:
return (1.0 - linear_ramp(values, criteria)).astype(np.float32)
3. Create advanced inputs
depth_score: continuous score increases with depth in [0.2, 1.5]velocity_score: continuous score decreases with velocity in [0.3, 1.2]hazard_binary: classic binary criterion
[4]:
depth_input = Input(name='depth_score',
array=depth,
condition=Operator.BETWEEN,
threshold=(0.2, 1.5),
score_routine=linear_ramp)
velocity_input = Input(name='velocity_score',
array=velocity,
condition=Operator.BETWEEN,
threshold=(0.3, 1.2),
score_routine=inverse_linear_ramp)
hazard_input = Input(name='hazard_binary',
array=hazard,
condition=Operator.SUPERIOR_OR_EQUAL,
threshold=0.9)
score_depth = depth_input.score.score
score_velocity = velocity_input.score.score
score_hazard = hazard_input.score.score
print('depth score dtype :', score_depth.dtype)
print('velocity score dtype:', score_velocity.dtype)
print('hazard score dtype :', score_hazard.dtype)
depth score dtype : float32
velocity score dtype: float32
hazard score dtype : int32
[5]:
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
for ax, arr, title, vmax in [
(axes[0], score_depth, 'Continuous depth score', 1.0),
(axes[1], score_velocity, 'Continuous velocity score', 1.0),
(axes[2], score_hazard, 'Binary hazard score', 1.0),
]:
im = ax.imshow(arr.T, origin='lower', vmin=0.0, vmax=vmax, cmap='viridis')
ax.set_title(title)
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
plt.tight_layout()
plt.show()
4. Combine scores: standard MCDA, product, and weighted sum
MulticriteriAnalysis now supports: SUM, PRODUCT, WEIGHTED_SUM, AVERAGE, PERCENTAGE.
For WEIGHTED_SUM, pass an explicit weight dictionary where keys are the Input objects (not strings).
[6]:
inputs = [depth_input, velocity_input, hazard_input]
mcda_avg = MulticriteriAnalysis(inputs=inputs, method=Operator.AVERAGE)
avg_result = mcda_avg.results.array
mcda_product = MulticriteriAnalysis(inputs=inputs, method=Operator.PRODUCT)
product_result = mcda_product.results.array
weights_by_input = {depth_input: 0.5,
velocity_input: 0.3,
hazard_input: 0.2,
}
# New API: weight keys are Input objects in MulticriteriAnalysis.
# Fallback keeps compatibility with older installed versions.
try:
mcda_weighted = MulticriteriAnalysis(inputs=inputs,
method=Operator.WEIGHTED_SUM,
weight=weights_by_input,
dtype=Format.FLOAT32.value,
)
weighted = mcda_weighted.results.array
except TypeError:
scores = Scores({i.name: i.score for i in inputs})
weights_by_name = {depth_input.name: 0.5,
velocity_input.name: 0.3,
hazard_input.name: 0.2,
}
ops = Operations(scores=scores, weight=weights_by_name)
weighted = ops.weighted_sum()
print('avg_result dtype :', avg_result.dtype)
print('product_result dtype:', product_result.dtype)
print('weighted_sum dtype :', weighted.dtype)
print('weighted min/max :', float(weighted.min()), float(weighted.max()))
WARNING:root:No weight provided. Setting weight to equal distribution.
WARNING:root:No weight provided. Setting weight to equal distribution.
WARNING:root:No weight provided. Setting weight to equal distribution.
WARNING:root:No weight provided. Setting weight to equal distribution.
avg_result dtype : float32
product_result dtype: float32
weighted_sum dtype : float32
weighted min/max : 0.02992250956594944 0.8957045078277588
[7]:
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
im0 = axes[0].imshow(avg_result.T, origin='lower', cmap='magma')
axes[0].set_title('MCDA average result')
plt.colorbar(im0, ax=axes[0], fraction=0.046, pad=0.04)
im1 = axes[1].imshow(weighted.T, origin='lower', cmap='cividis')
axes[1].set_title('Weighted sum result')
plt.colorbar(im1, ax=axes[1], fraction=0.046, pad=0.04)
plt.tight_layout()
plt.show()
5. Notes
Keep custom routines deterministic and vectorized.
Return a
np.ma.MaskedArraywith same shape/mask as input.Use tuple thresholds for ramp-like routines.
You can mix binary and continuous inputs in the same analysis.
With
WEIGHTED_SUM, provide an explicit weight dictionary keyed byInputobjects.