weatherlinguist

Making decisions with uncertainty

Recently, I had a discussion with a colleague about using traffic data—such as road temperature and road conditions (e.g., is it snowing or raining)—to make decisions on when to send trucks to clean the roads. This conversation reminded me of a Python library I came across some time ago, called mcda, which is designed for Multi-Criteria Decision Aiding (MCDA). MCDA is a framework for making decisions based on multiple, often conflicting, criteria. I didn’t know much about this topic before, but this piqued my curiosity to dig a bit further into how such a framework can help make well-informed decisions.

MCDA is a structured approach to decision-making that helps individuals or groups evaluate and choose between multiple options when faced with multiple, potentially conflicting, objectives or criteria. It’s particularly useful when dealing with complex decisions where a simple ranking or single criterion isn’t sufficient. This is exactly what’s needed for scenarios like the one I described above.

How MCDA Works

MCDA typically involves several key steps:

MCDA is widely used in fields like environmental assessment, sustainability, healthcare, and risk analysis, where decisions are complex and must balance multiple objectives.

Applying MCDA to Road Maintenance Decisions

Let’s return to the road maintenance scenario. Imagine you have to decide when and where to send snowplows or salt trucks during winter. The decision depends on several factors:

Using MCDA, you can:

  1. Define your criteria: For example, road temperature, precipitation type, traffic volume, and forecast confidence.
  2. Assign weights: Maybe road temperature and precipitation type are most important, so they get higher weights.
  3. Normalize your data: Convert all measurements to a 0–1 scale.
  4. Aggregate scores: Combine the normalized, weighted scores for each road segment.
  5. Analyze sensitivity: See how changes in weights or data uncertainty affect your decisions.

Example: Deciding When to Deploy Road Maintenance Trucks

Suppose you have the following data for three road segments:

Road Segment Temperature (°C) Precipitation (mm/hr) Traffic Volume (cars/hr) Forecast Confidence (%)
A -5 2 500 80
B -2 0 1000 60
C -8 1 300 90

You decide that temperature and precipitation are most important (weight = 0.4 each), traffic volume is less important (weight = 0.15), and forecast confidence is least important (weight = 0.05).


Minimal Working Example with mcda

Below is a minimal, self-contained example using only the mcda library. This example shows how to set up the decision matrix, define scales and weights, normalize the data, aggregate the scores, dump the results to a CSV file, and plot the results.

from mcda import PerformanceTable, normalize
from mcda.scales import QuantitativeScale, MIN, MAX
from mcda.mavt.aggregators import WeightedSum

import pandas as pd
import matplotlib.pyplot as plt

# Define alternatives and criteria
alternatives = ['A', 'B', 'C']
criteria = ['Temperature', 'Precipitation', 'Traffic', 'Confidence']

# Define the performance table (rows: alternatives, columns: criteria)
data = [
    [-5, 2, 500, 80],   # A
    [-2, 0, 1000, 60],  # B
    [-8, 1, 300, 90]    # C
]

# Define scales for each criterion
scales = {
    'Temperature': QuantitativeScale(-10, 0, preference_direction=MIN),  # Lower is better
    'Precipitation': QuantitativeScale(0, 5, preference_direction=MAX),  # Higher is better
    'Traffic': QuantitativeScale(0, 2000, preference_direction=MAX),     # Higher is better
    'Confidence': QuantitativeScale(0, 100, preference_direction=MIN)    # Lower is better
}

# Create the performance table
performance_table = PerformanceTable(
    data,
    alternatives=alternatives,
    criteria=criteria,
    scales=scales
)

# Define weights for each criterion
criteria_weights = {
    'Temperature': 0.4,
    'Precipitation': 0.4,
    'Traffic': 0.15,
    'Confidence': 0.05
}

# Create the weighted sum aggregator
weighted_sum = WeightedSum(criteria_weights)

# Normalize the performance table
normalized_table = normalize(performance_table)

# Compute the scores
scores = weighted_sum(normalized_table)
scores_df = scores.data.reset_index()
scores_df.columns = ['Road_Segment', 'Score']

# Save results to CSV
scores_df.to_csv('mcda_results.csv', index=False)
print("Results saved to mcda_results.csv")

# Plot the results
plt.bar(scores_df['Road_Segment'], scores_df['Score'], color='skyblue')
plt.xlabel('Road Segment')
plt.ylabel('MCDA Score')
plt.title('MCDA Scores for Road Segments')
plt.show()

mcda_road

What this does:

Incorporating Uncertainty in MCDA

In real-world decision-making, our data and weights are rarely exact. For example, weather forecasts come with confidence intervals, sensor measurements have errors, and the importance of each criterion may be debated among stakeholders. MCDA can handle this uncertainty through simulation-based approaches.

Understanding Sources of Uncertainty

Uncertainty can enter the MCDA process in several ways:

Monte Carlo Simulation for Uncertainty Analysis

While the mcda library doesn't have built-in uncertainty modeling, we can implement Monte Carlo simulation by running the MCDA process many times with slightly different input values. This approach helps us understand how robust our decisions are to uncertainty in the inputs.

Here's an enhanced example that incorporates uncertainty in the forecast confidence values:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mcda import PerformanceTable, normalize
from mcda.scales import QuantitativeScale, MIN, MAX
from mcda.mavt.aggregators import WeightedSum

# Set random seed for reproducibility
np.random.seed(42)

# Define alternatives and criteria (same as before)
alternatives = ['A', 'B', 'C']
criteria = ['Temperature', 'Precipitation', 'Traffic', 'Confidence']

# Base data with deterministic values for most criteria
base_data = [
    [-5, 2, 500, 80],   # A
    [-2, 0, 1000, 60],  # B
    [-8, 1, 300, 90]    # C
]

# Define uncertainty in forecast confidence (±10% standard deviation)
confidence_means = [80, 60, 90]
confidence_stds = [8, 6, 9]  # 10% of the mean values

# Define scales and weights (same as before)
scales = {
    'Temperature': QuantitativeScale(-10, 0, preference_direction=MIN),
    'Precipitation': QuantitativeScale(0, 5, preference_direction=MAX),
    'Traffic': QuantitativeScale(0, 2000, preference_direction=MAX),
    'Confidence': QuantitativeScale(0, 100, preference_direction=MIN)
}

criteria_weights = {
    'Temperature': 0.4,
    'Precipitation': 0.4,
    'Traffic': 0.15,
    'Confidence': 0.05
}

# Monte Carlo simulation
n_simulations = 1000
all_scores = []

for _ in range(n_simulations):
    # Sample new confidence values with uncertainty
    sampled_confidence = np.random.normal(confidence_means, confidence_stds)
    # Clip to valid range [0, 100]
    sampled_confidence = np.clip(sampled_confidence, 0, 100)
    
    # Create data with uncertain confidence values
    data_with_uncertainty = [
        [-5, 2, 500, sampled_confidence[0]],   # A
        [-2, 0, 1000, sampled_confidence[1]],  # B
        [-8, 1, 300, sampled_confidence[2]]    # C
    ]
    
    # Run MCDA process
    performance_table = PerformanceTable(
        data_with_uncertainty,
        alternatives=alternatives,
        criteria=criteria,
        scales=scales
    )
    
    weighted_sum = WeightedSum(criteria_weights)
    normalized_table = normalize(performance_table)
    scores = weighted_sum(normalized_table)
    
    # Store results
    all_scores.append(scores.data.values)

# Convert to numpy array for easier analysis
all_scores = np.array(all_scores)  # shape: (n_simulations, n_alternatives)

# Calculate statistics
mean_scores = np.mean(all_scores, axis=0)
std_scores = np.std(all_scores, axis=0)
percentile_5 = np.percentile(all_scores, 5, axis=0)
percentile_95 = np.percentile(all_scores, 95, axis=0)

# Create results DataFrame
results_df = pd.DataFrame({
    'Road_Segment': alternatives,
    'Mean_Score': mean_scores,
    'Std_Score': std_scores,
    'P5_Score': percentile_5,
    'P95_Score': percentile_95
})

print("MCDA Results with Uncertainty:")
print(results_df.round(3))

# Visualization: Boxplots showing uncertainty
fig, ax1 = plt.subplots(1, 1, figsize=(12, 5))

# Boxplot showing score distributions
# Convert to list of arrays, one for each alternative
boxplot_data = [all_scores[:, i] for i in range(len(alternatives))]
ax1.boxplot(boxplot_data, labels=alternatives)
ax1.set_xlabel('Road Segment')
ax1.set_ylabel('MCDA Score')
ax1.set_title('MCDA Scores with Uncertainty\n(Boxplots)')
ax1.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

mcda_uncertainty

Interpreting Uncertainty Results

The uncertainty analysis provides several insights:

Summary

The MCDA approach—and tools like mcda—provide a systematic way to make complex decisions using multiple, possibly uncertain, criteria. While the mcda library doesn't have built-in uncertainty modeling, we can easily implement Monte Carlo simulation to understand how uncertainty affects our decisions.

Whether you're managing road maintenance, choosing a project, or evaluating policy options, combining MCDA with uncertainty analysis helps you make more robust and informed decisions. The visualization of uncertainty through boxplots and error bars provides valuable insights into the reliability of your decision-making process.