Multi-Criteria Analysis — Advanced Usage

This notebook complements analyze_multicriteria.ipynb with advanced patterns:

  1. Continuous scoring via Input(..., score_routine=...)

  2. Mixed operators and threshold formats

  3. 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()
../_images/tutorials_analyze_multicriteria_advanced_8_0.png

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()
../_images/tutorials_analyze_multicriteria_advanced_11_0.png

5. Notes

  • Keep custom routines deterministic and vectorized.

  • Return a np.ma.MaskedArray with 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 by Input objects.