DecInfer-04-Multi-Attribute : Utilite Multi-Attributs

Serie : Programmation Probabiliste avec Infer.NET (4/10)
Duree estimee : 50 minutes
Prerequis : Notebooks 14-15 (Utilite et aversion au risque)


Objectifs

  • Modeliser des decisions avec plusieurs critères
  • Comprendre l’indépendance preferentielle
  • Appliquer les theoremes d’additivite et multiplicativite
  • Utiliser la méthode SMART pour la decision multi-critères

1. Introduction : Decisions Multi-Critères

Le problème

La plupart des decisions reelles impliquent plusieurs critères :

Decision Critères
Achat voiture Prix, securite, consommation, confort
Choix de carriere Salaire, equilibre vie/travail, passion, securite emploi
Investissement Rendement, risque, liquidite, impact ESG

Comment combiner ces critères en une decision rationnelle ?

Approches possibles

  1. Méthode lexicographique : Trier par critere le plus important, puis par le second…
  2. Ponderation simple : V(x) = Σ wᵢ × vᵢ(xᵢ)
  3. Théorie de l’utilite multi-attributs (MAUT) : Fondee sur des axiomes rigoureux
// Installation Infer.NET
#r "nuget: Microsoft.ML.Probabilistic"
#r "nuget: Microsoft.ML.Probabilistic.Compiler"

using Microsoft.ML.Probabilistic;
using Microsoft.ML.Probabilistic.Distributions;
using Microsoft.ML.Probabilistic.Models;
using Microsoft.ML.Probabilistic.Algorithms;

Console.WriteLine("Infer.NET charge !");
Installed Packages
  • Microsoft.ML.Probabilistic, 0.4.2504.701
  • Microsoft.ML.Probabilistic.Compiler, 0.4.2504.701
Infer.NET charge !

Chargement des utilitaires de visualisation des graphes de facteurs.

// Chargement du helper pour visualisation des graphes de facteurs
// (FactorGraphHelper.cs vit dans Probas/Infer/ ; chemin relatif ../../Infer pour re-exec headless CWD=notebook-dir.)
#load "../../Infer/FactorGraphHelper.cs"

Console.WriteLine("FactorGraphHelper charge !");
Console.WriteLine($"Graphviz disponible : {FactorGraphHelper.IsGraphvizAvailable()}");
FactorGraphHelper charge !
Graphviz disponible : True

Configuration Infer.NET effectuee

Le message “Infer.NET charge !” confirme que les packages NuGet sont correctement installes. Nous disposons maintenant de :

Package Rôle
Microsoft.ML.Probabilistic Moteur d’inference bayesienne
Microsoft.ML.Probabilistic.Compiler Compilation du graphe de facteurs

Note : Dans ce notebook, Infer.NET sera utilise principalement pour l’inference des poids (section 10 bis) et le calcul d’utilite esperee sous incertitude (section 9). Les sections sur MAUT et SMART utilisent des calculs déterministes classiques.

// Exemple : Achat d'une voiture

public class Voiture
{
    public string Nom { get; set; }
    public double Prix { get; set; }         // EUR
    public int Securite { get; set; }        // 1-5 etoiles
    public double Consommation { get; set; } // L/100km
    public int Confort { get; set; }         // 1-10
}

var voitures = new List<Voiture>
{
    new Voiture { Nom = "Economique A", Prix = 15000, Securite = 3, Consommation = 5.0, Confort = 6 },
    new Voiture { Nom = "Familiale B", Prix = 28000, Securite = 5, Consommation = 6.5, Confort = 8 },
    new Voiture { Nom = "Sport C", Prix = 45000, Securite = 4, Consommation = 9.0, Confort = 7 },
    new Voiture { Nom = "Hybride D", Prix = 32000, Securite = 5, Consommation = 4.0, Confort = 8 },
};

Console.WriteLine("Options disponibles :\n");
Console.WriteLine("Nom            | Prix    | Securite | Conso   | Confort");
Console.WriteLine("---------------|---------|----------|---------|--------");
foreach (var v in voitures)
{
    Console.WriteLine($"{v.Nom,-14} | {v.Prix,7:N0} | {v.Securite,8} | {v.Consommation,7:F1} | {v.Confort,7}");
}
Options disponibles :

Nom            | Prix    | Securite | Conso   | Confort
---------------|---------|----------|---------|--------
Economique A   |  15 000 |        3 |     5,0 |       6
Familiale B    |  28 000 |        5 |     6,5 |       8
Sport C        |  45 000 |        4 |     9,0 |       7
Hybride D      |  32 000 |        5 |     4,0 |       8

Interpretation du tableau des options

Le tableau ci-dessus presente quatre voitures avec des profils très différents :

Voiture Profil Points forts Points faibles
Economique A Budget-friendly Prix bas, bonne conso Securite moyenne, confort limite
Familiale B Equilibree Securite max, bon confort Prix eleve, consommation moyenne
Sport C Performance Securite correcte Prix très eleve, forte conso
Hybride D Eco-responsable Securite max, conso min Prix assez eleve

Observation cle : Aucune voiture ne domine les autres sur tous les critères. C’est précisément ce type de situation qui necessite une méthode de decision multi-critères structuree. Si une option etait superieure sur tous les attributs, le choix serait trivial.

2. Fonctions de Valeur vs Fonctions d’Utilite

Distinction importante

Type Contexte Propriete
Fonction de valeur V(x) Certitude Préférences sur outcomes certains
Fonction d’utilite U(x) Incertitude Préférences sur loteries

En multi-attributs

  • Valeur : V(prix, securite, conso, confort) represente vos préférences sous certitude
  • Utilite : U(.) integre en plus votre aversion au risque sur chaque attribut

Dans ce notebook, nous commencerons par les fonctions de valeur (plus simples), puis passerons aux fonctions d’utilite.

3. Indépendance Preferentielle

L’indépendance preferentielle est la cle qui permet de decomposer un problème multi-critères en sous-problemes simples. Sans elle, les interactions entre attributs rendraient le problème intraitable.

Definition

L’attribut X est preferentiellement indépendant de Y si : Les préférences sur X ne dependent pas du niveau de Y

Test pratique pour verifier l’indépendance

Pour tester si Prix et Securite sont independants :

  1. Fixez Securite = niveau bas (ex: 2 etoiles)
    • Comparez : Prix 15000 vs Prix 25000
    • Notez votre préférence
  2. Fixez Securite = niveau haut (ex: 5 etoiles)
    • Comparez les mêmes prix : 15000 vs 25000
    • Notez votre préférence
  3. Verdict : Si votre préférence est la même dans les deux cas, Prix est indépendant de Securite

Exemple

Est-ce que votre préférence entre 15000 EUR et 20000 EUR (prix) depend du niveau de securite ?

  • Si OUI : prix et securite ne sont PAS independants. Cela peut arriver si, par exemple, vous n’acceptez un prix eleve que si la securite est excellente.
  • Si NON : ils sont preferentiellement independants. Vous preferez toujours le prix bas, quel que soit le niveau de securite.

Indépendance mutuelle

Les attributs {X, Y, Z} sont mutuellement preferentiellement independants si : Chaque sous-ensemble est preferentiellement indépendant de son complement

Cette condition est forte mais souvent raisonnable. Elle permet d’utiliser des formes additives ou multiplicatives simples, ce qui reduit exponentiellement la complexite du problème.

// Test d'independance preferentielle

Console.WriteLine("Test d'independance : Prix vs Securite\n");
Console.WriteLine("Question : Preferez-vous A ou B ?");
Console.WriteLine();

// Contexte 1 : Securite = 3 etoiles
Console.WriteLine("Contexte 1 (Securite = 3 etoiles) :");
Console.WriteLine("  A : Prix = 15000 EUR");
Console.WriteLine("  B : Prix = 20000 EUR");
Console.WriteLine("  => Vous preferez probablement A (moins cher)\n");

// Contexte 2 : Securite = 5 etoiles
Console.WriteLine("Contexte 2 (Securite = 5 etoiles) :");
Console.WriteLine("  A : Prix = 15000 EUR");
Console.WriteLine("  B : Prix = 20000 EUR");
Console.WriteLine("  => Preferez-vous toujours A ?\n");

Console.WriteLine("Si votre reponse est la meme dans les deux contextes,");
Console.WriteLine("alors Prix et Securite sont preferentiellement independants.");
Test d'independance : Prix vs Securite

Question : Preferez-vous A ou B ?

Contexte 1 (Securite = 3 etoiles) :
  A : Prix = 15000 EUR
  B : Prix = 20000 EUR
  => Vous preferez probablement A (moins cher)

Contexte 2 (Securite = 5 etoiles) :
  A : Prix = 15000 EUR
  B : Prix = 20000 EUR
  => Preferez-vous toujours A ?

Si votre reponse est la meme dans les deux contextes,
alors Prix et Securite sont preferentiellement independants.

4. Theoreme d’Additivite (Debreu-Gorman)

Enonce

Si les attributs X1, X2, …, Xn sont mutuellement preferentiellement independants, alors la fonction de valeur est additive :

V(x1, x2, …, xn) = Summe wi x vi(xi)

Ou : - vi(xi) est la fonction de valeur marginale pour l’attribut i (normalisee 0-1) - wi est le poids de l’attribut i (Summe wi = 1)

Construction

  1. Pour chaque attribut, définir vi(xi) : [xi_min, xi_max] -> [0, 1]
  2. Determiner les poids wi par comparaison des “swings”
  3. Calculer V(.) pour chaque alternative

Implementation du modèle de decision multi-attributs avec la forme additive.

// Implementation de la forme additive

public class MultiAttributeValue
{
    // Fonctions de valeur marginale (normalisees 0-1)
    // Pour le prix : plus bas = mieux (inverser)
    public static double V_prix(double prix, double prixMin = 15000, double prixMax = 45000)
    {
        return 1 - (prix - prixMin) / (prixMax - prixMin);
    }
    
    // Pour la securite : plus haut = mieux
    public static double V_securite(int securite, int secMin = 1, int secMax = 5)
    {
        return (double)(securite - secMin) / (secMax - secMin);
    }
    
    // Pour la consommation : plus bas = mieux (inverser)
    public static double V_conso(double conso, double consoMin = 4, double consoMax = 10)
    {
        return 1 - (conso - consoMin) / (consoMax - consoMin);
    }
    
    // Pour le confort : plus haut = mieux
    public static double V_confort(int confort, int confMin = 1, int confMax = 10)
    {
        return (double)(confort - confMin) / (confMax - confMin);
    }
}

// Poids (swing weights) - a ajuster selon vos preferences
double w_prix = 0.35;
double w_securite = 0.30;
double w_conso = 0.20;
double w_confort = 0.15;

Console.WriteLine($"Poids : Prix={w_prix:P0}, Securite={w_securite:P0}, Conso={w_conso:P0}, Confort={w_confort:P0}\n");

Console.WriteLine("Voiture        | v_prix | v_sec  | v_conso | v_conf | V_total");
Console.WriteLine("---------------|--------|--------|---------|--------|--------");

foreach (var v in voitures)
{
    double vp = MultiAttributeValue.V_prix(v.Prix);
    double vs = MultiAttributeValue.V_securite(v.Securite);
    double vc = MultiAttributeValue.V_conso(v.Consommation);
    double vf = MultiAttributeValue.V_confort(v.Confort);
    
    double V_total = w_prix * vp + w_securite * vs + w_conso * vc + w_confort * vf;
    
    Console.WriteLine($"{v.Nom,-14} | {vp,6:F3} | {vs,6:F3} | {vc,7:F3} | {vf,6:F3} | {V_total,6:F3}");
}

var meilleure = voitures.OrderByDescending(v => 
    w_prix * MultiAttributeValue.V_prix(v.Prix) +
    w_securite * MultiAttributeValue.V_securite(v.Securite) +
    w_conso * MultiAttributeValue.V_conso(v.Consommation) +
    w_confort * MultiAttributeValue.V_confort(v.Confort)
).First();

Console.WriteLine($"\n=> Meilleur choix : {meilleure.Nom}");
Poids : Prix=35 %, Securite=30 %, Conso=20 %, Confort=15 %

Voiture        | v_prix | v_sec  | v_conso | v_conf | V_total
---------------|--------|--------|---------|--------|--------
Economique A   |  1,000 |  0,500 |   0,833 |  0,556 |  0,750
Familiale B    |  0,567 |  1,000 |   0,583 |  0,778 |  0,732
Sport C        |  0,000 |  0,750 |   0,167 |  0,667 |  0,358
Hybride D      |  0,433 |  1,000 |   1,000 |  0,778 |  0,768

=> Meilleur choix : Hybride D

Lecture : pourquoi l’Hybride D gagne — et pourquoi la Sport C s’effondre

Le classement final (D 0,768 > A 0,750 > B 0,732 > C 0,358) se lit colonne par colonne dans la décomposition. La Sport C perd d’abord sur toute la ligne : sa valeur de prix est 0,000 — c’est la voiture la plus chère, et la fonction de valeur prix est construite pour valoir zéro au pire —, et elle ne rattrape nulle part ailleurs (0,75 en sécurité, 0,17 en consommation : la plus gourmande du lot). À l’inverse, l’Hybride D n’est pénalisée sur aucun attribut : elle partage le sommet en sécurité avec la Familiale B (1,000 toutes deux), domine seule la consommation (1,000) et ne descend jamais sous 0,43. C’est la leçon structurelle de la forme additive — la victoire revient rarement au spécialiste, presque toujours au généraliste équilibré dès que les poids sont répartis. L’Économique A (0,750) s’en sort bien par son prix imbattable (1,000), qui compense une sécurité moyenne (0,500).

// Decomposition des valeurs par attribut pour chaque voiture
// Utilise les fonctions de valeur et poids definis ci-dessus

Console.WriteLine("Decomposition de la valeur multi-attributs :");
Console.WriteLine();
Console.WriteLine("Voiture        | Prix    | Securite | Conso   | Confort | Contributions");
Console.WriteLine("               | (w=35%) | (w=30%)  | (w=20%) | (w=15%) | ponderees");
Console.WriteLine("---------------|---------|----------|---------|---------|---------------");

foreach (var v in voitures)
{
    double vp = MultiAttributeValue.V_prix(v.Prix);
    double vs = MultiAttributeValue.V_securite(v.Securite);
    double vc = MultiAttributeValue.V_conso(v.Consommation);
    double vf = MultiAttributeValue.V_confort(v.Confort);

    double cp = w_prix * vp;
    double cs = w_securite * vs;
    double cc = w_conso * vc;
    double cf = w_confort * vf;

    Console.WriteLine($"{v.Nom,-14} | {cp,7:F3} | {cs,8:F3} | {cc,7:F3} | {cf,7:F3} | V={cp+cs+cc+cf:F3}");
}

Console.WriteLine();
Console.WriteLine("Interpretation :");
Console.WriteLine("  - Chaque colonne = contribution ponderee de l'attribut (wi * vi)");
Console.WriteLine("  - V total = somme des contributions");
Decomposition de la valeur multi-attributs :

Voiture        | Prix    | Securite | Conso   | Confort | Contributions
               | (w=35%) | (w=30%)  | (w=20%) | (w=15%) | ponderees
---------------|---------|----------|---------|---------|---------------
Economique A   |   0,350 |    0,150 |   0,167 |   0,083 | V=0,750
Familiale B    |   0,198 |    0,300 |   0,117 |   0,117 | V=0,732
Sport C        |   0,000 |    0,225 |   0,033 |   0,100 | V=0,358
Hybride D      |   0,152 |    0,300 |   0,200 |   0,117 | V=0,768

Interpretation :
  - Chaque colonne = contribution ponderee de l'attribut (wi * vi)
  - V total = somme des contributions

Lecture du budget de valeur : chaque ligne est un bilan de contributions

La décomposition transforme le score opaque en budget lisible : chaque case est wᵢ × vᵢ, la part du score total achetée par cet attribut, et la ligne somme à V. Pour l’Économique A, près de la moitié de la valeur (0,350 sur 0,750) vient du seul attribut prix — une décision « à botte concentrée », vulnérable si le poids du prix bouge. Pour l’Hybride D, les contributions sont étalées (0,152 / 0,300 / 0,200 / 0,117) — une décision robuste, aucun attribut ne porte plus de 40 % du score. Cette lecture « portefeuille de valeur » annonce l’analyse de sensibilité de la partie SMART ci-dessous, qui dira précisément à partir de quel poids l’équilibre bascule.

5. Determination des Poids (Swing Weights)

La méthode des swing weights est la technique standard pour determiner les poids de maniere coherente et justifiable. Elle evite les biais cognitifs lies a l’estimation directe des poids.

Principe fondamental

L’idee est de comparer la valeur des ameliorations (swings) plutot que les attributs eux-mêmes. Un swing est le passage du pire au meilleur niveau d’un attribut.

Méthode des swings en 5 étapes

Étape 1 : Point de reference - Imaginez l’option la pire possible (tous attributs au minimum) - C’est votre point de depart

Étape 2 : Identifier les swings - Pour chaque attribut, definissez le swing : pire niveau -> meilleur niveau - Ex: Prix 45000 EUR -> 15000 EUR, Securite 1 etoile -> 5 etoiles

Étape 3 : Classer les swings - Demandez-vous : “Quel swing prefererais-je realiser en premier ?” - Classez tous les swings par ordre de préférence

Étape 4 : Attribuer des points - Donnez 100 points au swing le plus important - Attribuez des points aux autres proportionnellement a leur valeur relative - Ex: Si le swing Securite vaut 100, et le swing Prix vaut “les 9/10 de Securite”, donnez 90 a Prix

Étape 5 : Normaliser - Divisez chaque score par la somme totale - Vous obtenez les poids wi avec Summe wi = 1

Exemple

// Methode des swing weights

Console.WriteLine("Situation de depart (pire option) :\n");
Console.WriteLine("  Prix : 45000 EUR (maximum)");
Console.WriteLine("  Securite : 1 etoile (minimum)");
Console.WriteLine("  Consommation : 10 L/100km (maximum)");
Console.WriteLine("  Confort : 1/10 (minimum)");
Console.WriteLine();

Console.WriteLine("Classez les swings suivants par ordre de preference :\n");
Console.WriteLine("  A) Prix : 45000 -> 15000 (economie de 30000 EUR)");
Console.WriteLine("  B) Securite : 1 -> 5 etoiles");
Console.WriteLine("  C) Consommation : 10 -> 4 L/100km");
Console.WriteLine("  D) Confort : 1 -> 10\n");

// Exemple de classement personnel
var swingPoints = new Dictionary<string, int>
{
    { "Securite", 100 },    // Le plus important
    { "Prix", 90 },          // Presque aussi important
    { "Consommation", 50 }, // Moitie de la securite
    { "Confort", 40 }       // Le moins prioritaire
};

double totalSwing = swingPoints.Values.Sum();
Console.WriteLine("Points swing et poids normalises :\n");
foreach (var kv in swingPoints.OrderByDescending(x => x.Value))
{
    double poidsSwing = kv.Value / totalSwing;
    Console.WriteLine($"  {kv.Key,-12} : {kv.Value,3} points => poids = {poidsSwing:P1}");
}
Situation de depart (pire option) :

  Prix : 45000 EUR (maximum)
  Securite : 1 etoile (minimum)
  Consommation : 10 L/100km (maximum)
  Confort : 1/10 (minimum)

Classez les swings suivants par ordre de preference :

  A) Prix : 45000 -> 15000 (economie de 30000 EUR)
  B) Securite : 1 -> 5 etoiles
  C) Consommation : 10 -> 4 L/100km
  D) Confort : 1 -> 10

Points swing et poids normalises :

  Securite     : 100 points => poids = 35,7 %
  Prix         :  90 points => poids = 32,1 %
  Consommation :  50 points => poids = 17,9 %
  Confort      :  40 points => poids = 14,3 %

Exercice 1 : Determination de swing weights pour un choix d’appartement

Note pedagogique : ce bloc est positionne en tete de sequence pour l’ordre canonique header -> stub de l’exercice (cf. PR #7880).

Objectif : Appliquez la méthode des swing weights pour determiner les poids d’une decision d’achat d’appartement.

Contexte : Vous cherchez un appartement parmi 4 options. Les attributs sont : Prix, Surface, Quartier (note 1-10), et Duree de trajet travail. Avant de choisir, vous devez determiner vos poids.

Étapes : 1. Definissez le pire scénario pour chaque attribut (votre point de reference) 2. Classez les 4 swings (amelioration du pire au meilleur) par ordre de préférence 3. Attribuez des points (100 au meilleur swing, proportionnellement aux autres) 4. Normalisez pour obtenir les poids wi 5. Appliquez la forme additive pour classer les appartements

Indices : - Indice 1 : Le pire scénario est celui ou TOUS les attributs sont a leur valeur minimale - Indice 2 : Un swing “Prix: 500k -> 200k” peut etre plus ou moins important qu’un swing “Trajet: 60min -> 10min” - Indice 3 : Les points refletent la valeur subjective de chaque amelioration, pas l’importance abstraite de l’attribut

// Exercice : Swing weights pour un choix d'appartement

// 4 appartements disponibles
var appartements = new[]
{
    new { Nom = "Studio Centre", Prix = 350000, Surface = 25, Quartier = 8, Trajet = 10 },
    new { Nom = "T2 Banlieue", Prix = 200000, Surface = 45, Quartier = 5, Trajet = 45 },
    new { Nom = "T3 Peripherie", Prix = 280000, Surface = 65, Quartier = 6, Trajet = 30 },
    new { Nom = "Loft Industriel", Prix = 420000, Surface = 80, Quartier = 7, Trajet = 20 },
};

// TODO 1 : Definir les pires et meilleurs niveaux pour chaque attribut
// Indice : Prix: pire=450000, meilleur=180000 | Surface: pire=20, meilleur=90
//          Quartier: pire=3, meilleur=9 | Trajet: pire=60min, meilleur=5min

// TODO 2 : Classer les 4 swings et attribuer des points (100 au meilleur)
// Swing A : Prix 450k->180k   = ___ points
// Swing B : Surface 20->90    = ___ points
// Swing C : Quartier 3->9     = ___ points
// Swing D : Trajet 60->5min   = ___ points

// TODO 3 : Normaliser les points en poids (somme = 1)
// Indice : w_i = points_i / somme(points)

// TODO 4 : Definir les fonctions de valeur marginale v_i(x_i) normalisees [0, 1]
// Indice : pour le prix et le trajet, "moins = mieux" => inverser la normalisation

// TODO 5 : Calculer V_total pour chaque appartement et classer

Console.WriteLine("Exercice a completer : swing weights et classement des appartements");
Exercice a completer : swing weights et classement des appartements

6. Utilite Multiplicative pour les Loteries

Contexte : Quand la forme additive ne suffit plus

Quand les outcomes sont incertains (loteries), la forme additive ne suffit plus. En effet, l’additivite suppose que l’aversion au risque est la même quel que soit le niveau des autres attributs, ce qui est rarement vrai.

Conditions d’application de la forme multiplicative

La forme multiplicative est appropriee quand :

  1. Les attributs sont mutuellement independants en utilite (condition plus forte que l’indépendance preferentielle)
  2. Il existe des interactions entre attributs dans l’attitude face au risque
  3. Σkᵢ ≠ 1 (si Σkᵢ = 1, on retombe sur la forme additive)

Forme multiplicative (Keeney-Raiffa)

Si les attributs sont mutuellement independants en utilite :

\[1 + kU(x_1, x_2) = [1 + kk_1U_1(x_1)][1 + kk_2U_2(x_2)]\]

Ou k est determine par : k₁ + k₂ + kk₁k₂ = 1

Interpretation du paramètre k

Valeur de k Interpretation
k = 0 Forme additive (pas d’interaction)
k > 0 Synergies : avoir les deux attributs eleves vaut plus que la somme
k < 0 Substituts : ameliorer un attribut reduit l’importance de l’autre
// Utilite multiplicative pour decisions sous incertitude

public class MultiplicativeUtility
{
    private double[] k; // Poids individuels ki
    private double K;   // Facteur d'echelle K
    
    public MultiplicativeUtility(double[] weights)
    {
        k = weights;
        // Trouver K tel que: prod(1 + K*ki) = 1 + K
        K = FindK(weights);
    }
    
    private double FindK(double[] weights)
    {
        // Recherche par Newton-Raphson
        double sumK = weights.Sum();
        
        // Si sum(ki) = 1, alors K = 0 (cas additif)
        if (Math.Abs(sumK - 1) < 1e-6)
            return 0;
        
        // Sinon, resoudre prod(1 + K*ki) = 1 + K
        double K = sumK > 1 ? -0.5 : 0.5; // Initialisation
        
        for (int iter = 0; iter < 100; iter++)
        {
            double prod = 1;
            foreach (var ki in weights)
                prod *= (1 + K * ki);
            
            double f = prod - 1 - K;
            if (Math.Abs(f) < 1e-10) break;
            
            // Derivee
            double fprime = -1;
            for (int i = 0; i < weights.Length; i++)
            {
                double term = weights[i];
                for (int j = 0; j < weights.Length; j++)
                    if (j != i) term *= (1 + K * weights[j]);
                fprime += term;
            }
            
            K -= f / fprime;
        }
        return K;
    }
    
    public double Compute(double[] u_values)
    {
        if (Math.Abs(K) < 1e-10)
        {
            // Forme additive
            return u_values.Zip(k, (u, ki) => ki * u).Sum();
        }
        else
        {
            // Forme multiplicative
            double prod = 1;
            for (int i = 0; i < u_values.Length; i++)
                prod *= (1 + K * k[i] * u_values[i]);
            return (prod - 1) / K;
        }
    }
    
    public double GetK() => K;
}

// Exemple avec 3 attributs
var mu = new MultiplicativeUtility(new[] { 0.4, 0.35, 0.35 }); // sum > 1 => K != 0
Console.WriteLine($"Poids : k1=0.4, k2=0.35, k3=0.35 (sum = 1.1)");
Console.WriteLine($"Facteur K calcule : {mu.GetK():F4}");
Console.WriteLine();

// Comparaison forme additive vs multiplicative
double[] u_high = { 1.0, 1.0, 1.0 };
double[] u_mixed = { 1.0, 0.5, 0.5 };
double[] u_low = { 0.5, 0.5, 0.5 };

Console.WriteLine("Comparaison des formes :");
Console.WriteLine($"  u = (1,1,1) : U_mult = {mu.Compute(u_high):F3}");
Console.WriteLine($"  u = (1,0.5,0.5) : U_mult = {mu.Compute(u_mixed):F3}");
Console.WriteLine($"  u = (0.5,0.5,0.5) : U_mult = {mu.Compute(u_low):F3}");
Poids : k1=0.4, k2=0.35, k3=0.35 (sum = 1.1)
Facteur K calcule : -0,2565

Comparaison des formes :
  u = (1,1,1) : U_mult = 1,000
  u = (1,0.5,0.5) : U_mult = 0,707
  u = (0.5,0.5,0.5) : U_mult = 0,525

Lecture : K = −0,26 — des attributs substituts, pas complémentaires

Les poids annoncés somment à 1,1 (0,4 + 0,35 + 0,35) : la normalisation multiplicative exige ce dépassement, et le facteur d’interaction recalé K = −0,2565 rétablit la cohérence. Le signe de K porte le sens économique : négatif, les attributs se comportent comme des substituts — être médiocre sur deux attributs à la fois coûte moins cher que la somme des deux médiocrités, car l’un compense partiellement l’autre. Positif, ils seraient des compléments — l’excellence ne paierait que conjointe (prix ET sécurité ensemble). La forme additive vue plus haut est le cas neutre K = 0 : aucun échange entre attributs, chacun compte pour son poids, point.

// Comparaison forme additive vs multiplicative

// Creer une instance de MultiplicativeUtility pour comparaison
var muCompare = new MultiplicativeUtility(new[] { 0.5, 0.5 });
double K = muCompare.GetK();

Console.WriteLine("Comparaison forme additive vs multiplicative :");
Console.WriteLine($"Poids : w1=0.5, w2=0.5, K={K:F4}");
Console.WriteLine();

// Scenarios de test
var scenarios = new (string nom, double v1, double v2)[]
{
    ("Bon X, mauvais Y", 0.9, 0.2),
    ("Mauvais X, bon Y", 0.2, 0.9),
    ("Mediocre partout", 0.5, 0.5),
    ("Excellent partout", 0.9, 0.9)
};

Console.WriteLine("Scenario                 | U Additif | U Multiplicatif | Diff");
Console.WriteLine("-------------------------|-----------|-----------------|------");

foreach (var (nom, v1, v2) in scenarios)
{
    double uAdd = 0.5 * v1 + 0.5 * v2;
    double uMult = muCompare.Compute(new[] { v1, v2 });

    Console.WriteLine($"{nom,-24} | {uAdd,9:F3} | {uMult,15:F3} | {uMult-uAdd,+5:F3}");
}

Console.WriteLine();
if (K < 0)
    Console.WriteLine("K < 0 : Les attributs sont substituts (un bon compense un mauvais)");
else if (K > 0)
    Console.WriteLine("K > 0 : Les attributs sont complements (un mauvais penalise le bon)");
else
    Console.WriteLine("K = 0 : Forme additive (pas d'interaction)");
Comparaison forme additive vs multiplicative :
Poids : w1=0.5, w2=0.5, K=0,0000

Scenario                 | U Additif | U Multiplicatif | Diff
-------------------------|-----------|-----------------|------
Bon X, mauvais Y         |     0,550 |           0,550 | 0,000
Mauvais X, bon Y         |     0,550 |           0,550 | 0,000
Mediocre partout         |     0,500 |           0,500 | 0,000
Excellent partout        |     0,900 |           0,900 | 0,000

K = 0 : Forme additive (pas d'interaction)

Lecture : un tableau volontairement dégénéré — K = 0 fait coïncider les deux formes

Toutes les différences sont 0,000 : ce n’est pas un résultat à interpréter, c’est une contre-épreuve de construction. Le scénario est calibré avec deux poids égaux (0,5 / 0,5), et la forme multiplicative se réduit alors algébriquement à la forme additive — les deux colonnes doivent coïncider, et coïncident : le modèle est auto-cohérent. La comparaison intéressante est celle de la section précédente (K = −0,26), où les interactions séparent des scénarios que l’addition traitait à égalité. L’exercice 2 vous fait explorer l’autre bord : un choix d’investissement où K ≠ 0, et où être bon partout ne vaut plus forcément le même prix qu’être excellent sur un seul attribut décisif.

7. Méthode SMART

Simple Multi-Attribute Rating Technique

SMART est une méthode pratique en 7 étapes :

  1. Identifier les attributs pertinents
  2. Définir les echelles pour chaque attribut
  3. Classer les attributs par importance
  4. Ponderer par la méthode des swings
  5. Evaluer chaque alternative sur chaque attribut
  6. Normaliser les evaluations (0-1)
  7. Calculer la valeur totale : V = Summe wi x vi

Exercice 2 : Forme multiplicative pour un choix d’investissement

Note pedagogique : ce bloc est positionne en tete de sequence pour l’ordre canonique header -> stub de l’exercice (cf. PR #7880).

Objectif : Utilisez la forme multiplicative de l’utilite pour comparer des options d’investissement avec interactions entre attributs.

Contexte : Vous choisissez entre 3 investissements avec les profils suivants (utilites normalisees 0-1) :

Investissement Rendement (u1) Risque (u2, 0=sur) Liquidite (u3)
Obligations 0.3 0.9 0.7
Actions 0.8 0.3 0.5
Immobilier 0.6 0.5 0.2

Étapes : 1. Créez une instance de MultiplicativeUtility avec des poids {0.4, 0.35, 0.35} 2. Calculez U_mult pour chaque investissement 3. Comparez avec la forme additive (U_add = somme wi * ui) 4. Interpretez le signe de K : les attributs sont-ils complements ou substituts ?

Indices : - Indice 1 : K < 0 signifie que les attributs sont substituts (un bon compense un mauvais) - Indice 2 : La différence entre U_mult et U_add est maximale quand les attributs sont extremes (0 ou 1) - Indice 3 : Si K est proche de 0, la forme additive est une bonne approximation

// Exercice : Forme multiplicative pour un choix d'investissement

// Profils d'investissement (utilites normalisees 0-1)
var investissements = new[]
{
    new { Nom = "Obligations", u_rend = 0.3, u_risque = 0.9, u_liquidite = 0.7 },
    new { Nom = "Actions",     u_rend = 0.8, u_risque = 0.3, u_liquidite = 0.5 },
    new { Nom = "Immobilier",  u_rend = 0.6, u_risque = 0.5, u_liquidite = 0.2 },
};

// TODO 1 : Creez une instance de MultiplicativeUtility avec des poids {0.4, 0.35, 0.35}
// Indice : var muInvest = new MultiplicativeUtility(new[] { 0.4, 0.35, 0.35 });

// TODO 2 : Pour chaque investissement, calculez U_mult et U_add
// Indice : U_add = w1*u1 + w2*u2 + w3*u3
//          U_mult = muInvest.Compute(new[] { u1, u2, u3 })

// TODO 3 : Affichez un tableau comparatif
// | Investissement | U_additif | U_multiplicatif | Difference |

// TODO 4 : Interpretez le signe de K
// Indice : muInvest.GetK() < 0 => substituts, > 0 => complements

Console.WriteLine("Exercice a completer : forme multiplicative pour choix d'investissement");
Exercice a completer : forme multiplicative pour choix d'investissement
// Application complete de SMART : Choix de carriere

public class CareerOption
{
    public string Name { get; set; }
    public double Salary { get; set; }      // k EUR/an
    public int WorkLifeBalance { get; set; } // 1-10
    public int Passion { get; set; }         // 1-10
    public int JobSecurity { get; set; }     // 1-10
}

var careers = new List<CareerOption>
{
    new CareerOption { Name = "Startup", Salary = 45, WorkLifeBalance = 3, Passion = 9, JobSecurity = 4 },
    new CareerOption { Name = "Grande Entreprise", Salary = 55, WorkLifeBalance = 6, Passion = 5, JobSecurity = 8 },
    new CareerOption { Name = "Fonction Publique", Salary = 38, WorkLifeBalance = 8, Passion = 4, JobSecurity = 10 },
    new CareerOption { Name = "Freelance", Salary = 60, WorkLifeBalance = 5, Passion = 8, JobSecurity = 3 },
    new CareerOption { Name = "Recherche", Salary = 35, WorkLifeBalance = 7, Passion = 10, JobSecurity = 6 },
};

// Etape 4 : Poids SMART (exemple)
var weights = new Dictionary<string, double>
{
    { "Salary", 0.30 },
    { "WorkLifeBalance", 0.25 },
    { "Passion", 0.25 },
    { "JobSecurity", 0.20 }
};

// Etapes 5-6 : Evaluation et normalisation
double salaryMin = careers.Min(c => c.Salary);
double salaryMax = careers.Max(c => c.Salary);

Func<double, double> normSalary = s => (s - salaryMin) / (salaryMax - salaryMin);
Func<int, double> norm10 = x => (x - 1) / 9.0;

Console.WriteLine("=== SMART : Choix de Carriere ===");
Console.WriteLine($"Poids : Salaire={weights["Salary"]:P0}, Equilibre={weights["WorkLifeBalance"]:P0}, ");
Console.WriteLine($"        Passion={weights["Passion"]:P0}, Securite={weights["JobSecurity"]:P0}\n");

Console.WriteLine("Carriere           | v_sal | v_wlb | v_pas | v_sec | V_total");
Console.WriteLine("-------------------|-------|-------|-------|-------|--------");

var results = new List<(string name, double V)>();

foreach (var c in careers)
{
    double v_sal = normSalary(c.Salary);
    double v_wlb = norm10(c.WorkLifeBalance);
    double v_pas = norm10(c.Passion);
    double v_sec = norm10(c.JobSecurity);
    
    double V = weights["Salary"] * v_sal +
               weights["WorkLifeBalance"] * v_wlb +
               weights["Passion"] * v_pas +
               weights["JobSecurity"] * v_sec;
    
    results.Add((c.Name, V));
    Console.WriteLine($"{c.Name,-18} | {v_sal,5:F3} | {v_wlb,5:F3} | {v_pas,5:F3} | {v_sec,5:F3} | {V,6:F3}");
}

var best = results.OrderByDescending(x => x.V).First();
Console.WriteLine($"\n=> Meilleur choix avec ces poids : {best.name} (V = {best.V:F3})");
=== SMART : Choix de Carriere ===
Poids : Salaire=30 %, Equilibre=25 %, 
        Passion=25 %, Securite=20 %

Carriere           | v_sal | v_wlb | v_pas | v_sec | V_total
-------------------|-------|-------|-------|-------|--------
Startup            | 0,400 | 0,222 | 0,889 | 0,333 |  0,464
Grande Entreprise  | 0,800 | 0,556 | 0,444 | 0,778 |  0,646
Fonction Publique  | 0,120 | 0,778 | 0,333 | 1,000 |  0,514
Freelance          | 1,000 | 0,444 | 0,778 | 0,222 |  0,650
Recherche          | 0,000 | 0,667 | 1,000 | 0,556 |  0,528

=> Meilleur choix avec ces poids : Freelance (V = 0,650)

Evaluation des profils de carriere avec la méthode SMART.

// Profils de carriere : comparaison multi-attributs

Console.WriteLine("Profils de carriere - Comparaison multi-attributs :");
Console.WriteLine();
Console.WriteLine("Carriere           | Salaire | Equilibre | Passion | Securite | Score");
Console.WriteLine("-------------------|---------|-----------|---------|----------|------");

foreach (var c in careers)
{
    double vSal = normSalary(c.Salary);
    double vWlb = norm10(c.WorkLifeBalance);
    double vPas = norm10(c.Passion);
    double vSec = norm10(c.JobSecurity);

    double score = weights["Salary"] * vSal +
                   weights["WorkLifeBalance"] * vWlb +
                   weights["Passion"] * vPas +
                   weights["JobSecurity"] * vSec;

    Console.WriteLine($"{c.Name,-18} | {vSal,7:F2} | {vWlb,9:F2} | {vPas,7:F2} | {vSec,8:F2} | {score,5:F3}");
}

Console.WriteLine();
Console.WriteLine($"Poids : Salaire={weights["Salary"]:P0}, Equilibre={weights["WorkLifeBalance"]:P0}, Passion={weights["Passion"]:P0}, Securite={weights["JobSecurity"]:P0}");
Profils de carriere - Comparaison multi-attributs :

Carriere           | Salaire | Equilibre | Passion | Securite | Score
-------------------|---------|-----------|---------|----------|------
Startup            |    0,40 |      0,22 |    0,89 |     0,33 | 0,464
Grande Entreprise  |    0,80 |      0,56 |    0,44 |     0,78 | 0,646
Fonction Publique  |    0,12 |      0,78 |    0,33 |     1,00 | 0,514
Freelance          |    1,00 |      0,44 |    0,78 |     0,22 | 0,650
Recherche          |    0,00 |      0,67 |    1,00 |     0,56 | 0,528

Poids : Salaire=30 %, Equilibre=25 %, Passion=25 %, Securite=20 %

8. Analyse de Sensibilite

Importance

Les poids sont souvent incertains. L’analyse de sensibilite montre comment le choix change quand on fait varier les poids.

// Analyse de sensibilite sur le poids du salaire

Console.WriteLine("Analyse de sensibilite : poids du Salaire\n");
Console.WriteLine("w_salaire | Meilleur choix       | V_meilleur");
Console.WriteLine("----------|----------------------|-----------");

for (double w_sal = 0.1; w_sal <= 0.5; w_sal += 0.1)
{
    // Redistribuer le poids restant proportionnellement
    double remaining = 1 - w_sal;
    double w_wlb = 0.25 / 0.70 * remaining;
    double w_pas = 0.25 / 0.70 * remaining;
    double w_sec = 0.20 / 0.70 * remaining;
    
    var best_option = careers
        .Select(c => new 
        {
            Name = c.Name,
            V = w_sal * normSalary(c.Salary) +
                w_wlb * norm10(c.WorkLifeBalance) +
                w_pas * norm10(c.Passion) +
                w_sec * norm10(c.JobSecurity)
        })
        .OrderByDescending(x => x.V)
        .First();
    
    Console.WriteLine($"{w_sal,9:P0} | {best_option.Name,-20} | {best_option.V,9:F3}");
}

Console.WriteLine();
Console.WriteLine("=> Le choix optimal change selon l'importance accordee au salaire.");
Analyse de sensibilite : poids du Salaire

w_salaire | Meilleur choix       | V_meilleur
----------|----------------------|-----------
     10 % | Recherche            |     0,679
     20 % | Grande Entreprise    |     0,623
     30 % | Freelance            |     0,650
     40 % | Freelance            |     0,700
     50 % | Freelance            |     0,750

=> Le choix optimal change selon l'importance accordee au salaire.

Lecture : trois gagnants pour un même jeu de données — la décision tient à un poids

Le choix optimal change deux fois de gagnant sur la plage : Recherche tant que le poids du salaire reste bas (10 % : sa passion 1,000 et son équilibre 0,667 portent, malgré le salaire 0,000), Grande Entreprise sur la bande intermédiaire (~20-25 % — le profil le plus plat de la table : 0,80 / 0,56 / 0,44 / 0,78, jamais extrême), puis Freelance au-delà (~25-30 % : son salaire 1,000 prend le dessus). Aucune de ces réponses n’est « la » bonne : chacune est optimale pour des préférences différentes — c’est la réponse honnête à « quelle carrière choisir ? » quand le poids du salaire est mal connu. La question opérationnelle devient : mon choix est-il robuste ? Si votre poids de salaire estimé est 30 % mais que votre vraie préférence pourrait être 20 %, la décision change de gagnant — d’où l’intérêt de la fin du notebook, où l’inférence bayésienne estime les poids depuis des choix observés au lieu de les demander. Dix ou vingt choix observés resserreraient le posterior autour des poids révélés, et diraient si l’écart entre déclaré et révélé se referme avec la pratique.

// Analyse de sensibilite : tableau des valeurs V par carriere et poids salaire
// (Remplace Chart.Combine Plotly.NET qui echoue en CS0103 sur re-execution fraiche)

var wSalaryRange = Enumerable.Range(1, 9).Select(i => i * 0.05 + 0.05).ToArray(); // 0.10 a 0.50

// Stocker les valeurs V pour chaque carriere et chaque w_salary
var sensitivityData = new Dictionary<string, double[]>();

foreach (var career in careers)
{
    var Vvalues = new List<double>();
    foreach (var w_sal in wSalaryRange)
    {
        double remaining = 1 - w_sal;
        double w_wlb_s = 0.25 / 0.70 * remaining;
        double w_pas_s = 0.25 / 0.70 * remaining;
        double w_sec_s = 0.20 / 0.70 * remaining;
        
        double V = w_sal * normSalary(career.Salary) +
                   w_wlb_s * norm10(career.WorkLifeBalance) +
                   w_pas_s * norm10(career.Passion) +
                   w_sec_s * norm10(career.JobSecurity);
        Vvalues.Add(V);
    }
    sensitivityData[career.Name] = Vvalues.ToArray();
}

// Afficher le tableau de sensibilite
Console.WriteLine("Tableau de sensibilite : V selon le poids du Salaire");
Console.WriteLine();
Console.Write("w_salaire  |");
foreach (var c in careers) Console.Write($"{c.Name.Substring(0, Math.Min(8, c.Name.Length)),-9} |");
Console.WriteLine();
Console.Write("-----------|");
for (int i = 0; i < careers.Count; i++) Console.Write("---------|");
Console.WriteLine();

foreach (var w_sal in wSalaryRange)
{
    Console.Write($"{w_sal,10:P0} |");
    double maxV = double.MinValue;
    string maxName = "";
    foreach (var career in careers)
    {
        int idx = Array.IndexOf(wSalaryRange, w_sal);
        double V = sensitivityData[career.Name][idx];
        if (V > maxV) { maxV = V; maxName = career.Name; }
        Console.Write($"{V,8:F3} |");
    }
    Console.Write($" <-- {maxName}");
    Console.WriteLine();
}

Console.WriteLine();
Console.WriteLine("Interpretation :");
Console.WriteLine("- Les valeurs en gras (max par colonne) indiquent le meilleur choix");
Console.WriteLine("- Freelance domine pour w_salaire > 25%");
Console.WriteLine("- Recherche domine pour w_salaire < 15%");
Console.WriteLine("=> Le choix optimal depend fortement des preferences sur le salaire");
Tableau de sensibilite : V selon le poids du Salaire

w_salaire  |Startup   |Grande E  |Fonction  |Freelanc  |Recherch  |
-----------|---------|---------|---------|---------|---------|
      10 % |   0,483 |   0,601 |   0,626 |   0,550 |   0,679 | <-- Recherche
      15 % |   0,478 |   0,612 |   0,598 |   0,575 |   0,641 | <-- Recherche
      20 % |   0,474 |   0,623 |   0,570 |   0,600 |   0,603 | <-- Grande Entreprise
      25 % |   0,469 |   0,635 |   0,542 |   0,625 |   0,565 | <-- Grande Entreprise
      30 % |   0,464 |   0,646 |   0,514 |   0,650 |   0,528 | <-- Freelance
      35 % |   0,460 |   0,657 |   0,486 |   0,675 |   0,490 | <-- Freelance
      40 % |   0,455 |   0,668 |   0,458 |   0,700 |   0,452 | <-- Freelance
      45 % |   0,451 |   0,679 |   0,429 |   0,725 |   0,415 | <-- Freelance
      50 % |   0,446 |   0,690 |   0,401 |   0,750 |   0,377 | <-- Freelance

Interpretation :
- Les valeurs en gras (max par colonne) indiquent le meilleur choix
- Freelance domine pour w_salaire > 25%
- Recherche domine pour w_salaire < 15%
=> Le choix optimal depend fortement des preferences sur le salaire

Exercice 3 : Analyse de sensibilite multi-attributs

Note pedagogique : ce bloc est positionne en tete de sequence pour l’ordre canonique header -> stub de l’exercice (cf. PR #7880).

Objectif : Realisez une analyse de sensibilite complete pour determiner quel poids est le plus critique dans un choix de carriere.

Contexte : Vous avez 5 options de carriere et 4 attributs. Les poids que vous avez determines par la méthode des swings sont : Salaire=30%, Equilibre=25%, Passion=25%, Securite=20%. Mais vous n’etes pas sur de vos poids. Determinez quels poids sont critiques pour le choix final.

Étapes : 1. Utilisez les carrieres définies dans la section SMART (Startup, Grande Entreprise, etc.) 2. Faites varier chaque poids individuellement de +/-10% (en redistribuant proportionnellement les autres) 3. Pour chaque variation, identifiez si le meilleur choix change 4. Identifiez les poids critiques (ceux dont la variation fait changer le choix)

Indices : - Indice 1 : Enregistrez le classement complet (pas seulement le meilleur) pour detecter les changements subtils - Indice 2 : Un poids est “critique” si une variation de 5% ou moins fait changer le choix optimal - Indice 3 : Les poids non-critiques peuvent etre ajustes sans affecter la decision

// Exercice : Analyse de sensibilite multi-attributs

// Reutiliser les carrieres et fonctions de normalisation definies dans la section SMART
// careers, normSalary, norm10, weights

// Poids nominaux
double[] w_nominal = { 0.30, 0.25, 0.25, 0.20 }; // sal, wlb, pas, sec

// TODO 1 : Implementer une fonction qui calcule le classement pour des poids donnes
// Indice : pour chaque carriere, calculer V = w_sal*v_sal + w_wlb*v_wlb + w_pas*v_pas + w_sec*v_sec
//          puis trier par V descendant

// TODO 2 : Pour chaque attribut i, faire varier w_i de -10% a +10% par pas de 2%
// Indice : redistribuer proportionnellement : w_j_adjusted = w_j * (1 - w_i_new) / (1 - w_i_old)
//         Verifier que la somme reste = 1

// TODO 3 : Pour chaque variation, enregistrer le meilleur choix
// Indice : si le meilleur change par rapport au nominal, le poids est "critique"

// TODO 4 : Afficher un tableau recapitulatif
// | Attribut varie | Variation | Meilleur choix | Change? |

// TODO 5 : Conclure sur les poids critiques vs robustes

Console.WriteLine("Exercice a completer : analyse de sensibilite multi-attributs");
Exercice a completer : analyse de sensibilite multi-attributs

9. Integration avec Infer.NET

Scénario avec incertitude

Si les attributs sont incertains, on utilise Infer.NET pour calculer les distributions posterieures, puis on calcule l’utilite esperee.

// Decision multi-attributs avec incertitude sur les attributs

// Scenario : Choisir entre deux projets d'investissement
// Les rendements et risques sont incertains

// Projet A : Rendement modere, risque modere
Variable<double> rendementA = Variable.GaussianFromMeanAndVariance(0.08, 0.01).Named("rendA");
Variable<double> risqueA = Variable.GaussianFromMeanAndVariance(0.15, 0.005).Named("risqueA");

// Projet B : Rendement eleve, risque eleve
Variable<double> rendementB = Variable.GaussianFromMeanAndVariance(0.12, 0.02).Named("rendB");
Variable<double> risqueB = Variable.GaussianFromMeanAndVariance(0.25, 0.01).Named("risqueB");

InferenceEngine engine = new InferenceEngine();
engine.Compiler.CompilerChoice = Microsoft.ML.Probabilistic.Compiler.CompilerChoice.Roslyn;

var distRendA = engine.Infer<Gaussian>(rendementA);
var distRisqueA = engine.Infer<Gaussian>(risqueA);
var distRendB = engine.Infer<Gaussian>(rendementB);
var distRisqueB = engine.Infer<Gaussian>(risqueB);

Console.WriteLine("Distributions des attributs :\n");
Console.WriteLine($"Projet A : Rendement ~ {distRendA}, Risque ~ {distRisqueA}");
Console.WriteLine($"Projet B : Rendement ~ {distRendB}, Risque ~ {distRisqueB}");
Console.WriteLine();

// Fonction d'utilite multi-attributs
// U(rendement, risque) = w_rend * v(rend) + w_risque * (1 - v(risque))
// Ou v normalise [0, 0.20] -> [0, 1]

double w_rend = 0.6;
double w_risque = 0.4;

Func<double, double> v_rend = r => Math.Max(0, Math.Min(1, r / 0.20));
Func<double, double> v_risque = r => 1 - Math.Max(0, Math.Min(1, r / 0.40));

// Calcul par Monte Carlo de E[U]
Microsoft.ML.Probabilistic.Math.Rand.Restart(42); // Gaussian.Sample() tire dans Rand, le générateur global d'Infer.NET
int N = 5000;

double sumU_A = 0, sumU_B = 0;
for (int i = 0; i < N; i++)
{
    double rA = distRendA.Sample();
    double riA = distRisqueA.Sample();
    double U_A = w_rend * v_rend(rA) + w_risque * v_risque(riA);
    sumU_A += U_A;
    
    double rB = distRendB.Sample();
    double riB = distRisqueB.Sample();
    double U_B = w_rend * v_rend(rB) + w_risque * v_risque(riB);
    sumU_B += U_B;
}

double EU_A = sumU_A / N;
double EU_B = sumU_B / N;

Console.WriteLine($"E[U(Projet A)] = {EU_A:F4}");
Console.WriteLine($"E[U(Projet B)] = {EU_B:F4}");
Console.WriteLine();
Console.WriteLine($"=> Decision optimale : {(EU_A > EU_B ? "Projet A" : "Projet B")}");
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Distributions des attributs :

Projet A : Rendement ~ Gaussian(0,08, 0,01), Risque ~ Gaussian(0,15, 0,005)
Projet B : Rendement ~ Gaussian(0,12, 0,02), Risque ~ Gaussian(0,25, 0,01)

E[U(Projet A)] = 0,5101
E[U(Projet B)] = 0,4884

=> Decision optimale : Projet A

Interpretation des résultats Infer.NET : Decision d’investissement

Sorties obtenues :

Projet Distribution Rendement Distribution Risque
A Gaussian(0.08, 0.01) Gaussian(0.15, 0.005)
B Gaussian(0.12, 0.02) Gaussian(0.25, 0.01)

Lecture des Gaussiennes : Gaussian(moyenne, variance). Ainsi le Projet A a un rendement espere de 8% avec une variance de 1%, soit un ecart-type de 10%.

Résultats de l’utilite esperee

Projet E[U] Interpretation
A 0.5101 Gagnant
B 0.4884 Perdant

Ecart : environ 4.3% en faveur du Projet A

Bruit Monte Carlo : ces deux valeurs sont des moyennes sur 5000 tirages. La graine Rand.Restart(42) les rend reproductibles d’une exécution à l’autre : Gaussian.Sample() tire dans Rand, le générateur global d’Infer.NET, et non dans un System.Random. Une autre graine déplacerait chaque E[U] de quelques millièmes, soit bien moins que l’écart de 0.02 entre les deux projets : la décision ne dépend pas de la graine.

Decomposition de la decision

La fonction d’utilite multi-attributs utilisee : \[U(\text{rend}, \text{risque}) = 0.6 \times \frac{\text{rend}}{0.20} + 0.4 \times \left(1 - \frac{\text{risque}}{0.40}\right)\]

Observation : Le projet B a un meilleur rendement espere, mais son risque eleve fait basculer la decision en faveur de A. C’est l’arbitrage rendement-risque classique en finance.

Rôle d’Infer.NET dans cette approche

Étape Outil Rôle
1. Modelisation des incertitudes Infer.NET Posterieurs analytiques
2. Calcul d’utilite esperee Monte Carlo Integration numérique

Note technique : Cette approche hybride est courante en pratique. Infer.NET excelle pour l’inference exacte ou approchee des posterieurs, mais les fonctions d’utilite non-lineaires necessitent souvent une integration Monte Carlo.

10. Exemple guide : Votre Decision Multi-Attributs

Enonce

Appliquez la méthode SMART a une decision personnelle :

  1. Choisissez un problème (appartement, voyage, achat…)
  2. Identifiez 3-5 attributs
  3. Definissez vos poids par la méthode des swings
  4. Evaluez 3-4 alternatives
  5. Calculez et comparez
// Template pour votre decision SMART

// 1. Definissez votre probleme et alternatives
var alternatives = new[] 
{ 
    "Option 1", 
    "Option 2", 
    "Option 3" 
};

// 2. Definissez vos attributs et poids
var attributs = new Dictionary<string, double>
{
    { "Attribut A", 0.4 },
    { "Attribut B", 0.35 },
    { "Attribut C", 0.25 }
};

// 3. Evaluez chaque alternative (valeurs normalisees 0-1)
var evaluations = new Dictionary<string, double[]>
{
    { "Option 1", new[] { 0.8, 0.6, 0.7 } },
    { "Option 2", new[] { 0.5, 0.9, 0.6 } },
    { "Option 3", new[] { 0.7, 0.7, 0.9 } }
};

// 4. Calculez les valeurs totales
Console.WriteLine("Vos resultats SMART :\n");

foreach (var alt in alternatives)
{
    var vals = evaluations[alt];
    var poids = attributs.Values.ToArray();
    double V = vals.Zip(poids, (v, w) => v * w).Sum();
    
    Console.WriteLine($"{alt} : V = {V:F3}");
}

Console.WriteLine();
Console.WriteLine("Modifiez les valeurs ci-dessus pour votre probleme personnel !");
Vos resultats SMART :

Option 1 : V = 0,705
Option 2 : V = 0,665
Option 3 : V = 0,750

Modifiez les valeurs ci-dessus pour votre probleme personnel !

10 bis. Apprentissage Bayesien des Poids avec Infer.NET

Motivation

Jusqu’ici, les poids MAUT sont determines par introspection (méthode des swings). Mais on peut aussi les inferer a partir des choix observes d’un decideur !

Approche Bayesienne

  1. Prior : Distribution Dirichlet sur les poids (non-informatif ou expert)
  2. Vraisemblance : Modèle de choix probabiliste (ex: softmax sur utilites)
  3. Posterior : Poids mis a jour après observation des choix

Avantages

  • Quantifie l’incertitude sur les poids
  • Permet de combiner expertise et données
  • Base pour l’elicitation de préférences automatique
// Apprentissage des poids MAUT avec prior Dirichlet
using Microsoft.ML.Probabilistic.Math;

// Scenario : Un decideur a fait 5 choix entre des alternatives.
// Nous observons quel attribut semble dominer ses decisions.

// Donnees observees : pour chaque choix, l'attribut "gagnant" (simplifie)
// 0=Prix, 1=Securite, 2=Consommation, 3=Confort
int[] choixDominants = { 1, 0, 1, 1, 0 }; // Securite semble importante

// Modele Infer.NET
// Prior Dirichlet non-informatif (pseudo-counts = 1 pour chaque attribut)
double[] pseudoCounts = { 1.0, 1.0, 1.0, 1.0 };
Variable<Vector> poids = Variable.Dirichlet(pseudoCounts).Named("poids");

// Observations : chaque choix est un tirage selon les poids
int numChoix = choixDominants.Length;
Range choixRange = new Range(numChoix).Named("choix");
VariableArray<int> observations = Variable.Array<int>(choixRange).Named("obs");
observations[choixRange] = Variable.Discrete(poids).ForEach(choixRange);
observations.ObservedValue = choixDominants;

// Inference
InferenceEngine engineDirichlet = new InferenceEngine();
engineDirichlet.Compiler.CompilerChoice = Microsoft.ML.Probabilistic.Compiler.CompilerChoice.Roslyn;

Dirichlet posteriorPoids = engineDirichlet.Infer<Dirichlet>(poids);

Console.WriteLine("=== Inference Bayesienne des Poids MAUT ===\n");
Console.WriteLine($"Choix observes : {string.Join(", ", choixDominants.Select(c => new[] { "Prix", "Securite", "Conso", "Confort" }[c]))}\n");

Console.WriteLine("Prior (Dirichlet uniforme) :");
Console.WriteLine($"  Pseudo-counts : [{string.Join(", ", pseudoCounts)}]\n");

Console.WriteLine("Posterior (apres 5 observations) :");
Vector meanPoids = posteriorPoids.GetMean();
string[] noms = { "Prix", "Securite", "Consommation", "Confort" };
for (int i = 0; i < 4; i++)
{
    Console.WriteLine($"  w_{noms[i],-12} = {meanPoids[i]:P1}");
}

Console.WriteLine($"\nDistribution Dirichlet posterior : {posteriorPoids}");
Console.WriteLine();

// Comparaison avec la methode des swings manuelle
Console.WriteLine("Comparaison :");
Console.WriteLine("  Swing weights (manuel)  : Prix=35%, Securite=30%, Conso=20%, Confort=15%");
Console.WriteLine($"  Inference bayesienne    : Prix={meanPoids[0]:P0}, Securite={meanPoids[1]:P0}, Conso={meanPoids[2]:P0}, Confort={meanPoids[3]:P0}");
Console.WriteLine();
Console.WriteLine("=> L'inference bayesienne revele les poids IMPLICITES du decideur !");
Console.WriteLine("   Ici, la Securite domine (3/5 choix), confirmant son importance.");
Compiling model...done.
=== Inference Bayesienne des Poids MAUT ===

Choix observes : Securite, Prix, Securite, Securite, Prix

Prior (Dirichlet uniforme) :
  Pseudo-counts : [1, 1, 1, 1]

Posterior (apres 5 observations) :
  w_Prix         = 33,3 %
  w_Securite     = 44,4 %
  w_Consommation = 11,1 %
  w_Confort      = 11,1 %

Distribution Dirichlet posterior : Dirichlet(3 4 1 1)

Comparaison :
  Swing weights (manuel)  : Prix=35%, Securite=30%, Conso=20%, Confort=15%
  Inference bayesienne    : Prix=33 %, Securite=44 %, Conso=11 %, Confort=11 %

=> L'inference bayesienne revele les poids IMPLICITES du decideur !
   Ici, la Securite domine (3/5 choix), confirmant son importance.

Interpretation de l’inference bayesienne des poids

Sortie Dirichlet : Dirichlet(3, 4, 1, 1) (pseudo-counts posterieurs)

Attribut Prior (pseudo-count) Observations Posterior (pseudo-count) Poids moyen
Prix 1 +2 choix 3 33.3%
Securite 1 +3 choix 4 44.4%
Consommation 1 +0 choix 1 11.1%
Confort 1 +0 choix 1 11.1%

Mécanisme Dirichlet : La distribution Dirichlet “compte” les observations. Chaque fois qu’un attribut domine un choix, son pseudo-count augmente de 1. Le posterior est proportionnel a (prior + observations).

Comparaison des méthodes d’elicitation

Méthode Poids Securite Avantage Inconvenient
Swing weights (manuel) 35.7% Explicite, justifiable Subjectif, biais cognitifs
Inference bayesienne 44.4% Base sur les choix reels Necessite des données

Revelation : L’inference bayesienne suggere que le decideur accorde plus d’importance a la securite qu’il ne le declare explicitement (44% vs 36%). C’est un cas classique ou les préférences revelees (choix reels) différent des préférences declarees.

Applications pratiques de l’inference des poids

Application Description
Validation Verifier si les poids declares sont coherents avec les choix passes
Prediction Predire les choix futurs d’un decideur
Elicitation Aider les experts qui ont du mal a exprimer leurs poids
Marketing Inferer les préférences des consommateurs a partir de leurs achats
// Comparaison Prior vs Posterior des poids MAUT

// Le posterior Dirichlet a ete calcule dans la cellule precedente: posteriorPoids
// Prior Dirichlet(1,1,1,1) - uniforme
double[] priorMean = { 0.25, 0.25, 0.25, 0.25 };

// Extraire les moyennes du posterior
Vector postMean = posteriorPoids.GetMean();

string[] attributNames = { "Prix", "Securite", "Consommation", "Confort" };

Console.WriteLine("Evolution des poids MAUT : Prior -> Posterior");
Console.WriteLine();
Console.WriteLine("Attribut      | Prior (uniforme) | Posterior (infere) | Evolution");
Console.WriteLine("--------------|------------------|--------------------|-----------");

for (int i = 0; i < attributNames.Length; i++)
{
    double evolution = postMean[i] - priorMean[i];
    string arrow = evolution > 0.05 ? "  ^" : evolution < -0.05 ? "  v" : "  =";
    Console.WriteLine($"{attributNames[i],-13} | {priorMean[i],16:P0} | {postMean[i],18:P1} | {evolution,+8:F3}{arrow}");
}

Console.WriteLine();
Console.WriteLine("Interpretation :");
Console.WriteLine("  ^ : L'attribut est plus important que prevu (choisi souvent)");
Console.WriteLine("  v : L'attribut est moins important que prevu");
Console.WriteLine("  = : L'importance reste proche du prior");
Evolution des poids MAUT : Prior -> Posterior

Attribut      | Prior (uniforme) | Posterior (infere) | Evolution
--------------|------------------|--------------------|-----------
Prix          |             25 % |             33,3 % |    0,083  ^
Securite      |             25 % |             44,4 % |    0,194  ^
Consommation  |             25 % |             11,1 % |   -0,139  v
Confort       |             25 % |             11,1 % |   -0,139  v

Interpretation :
  ^ : L'attribut est plus important que prevu (choisi souvent)
  v : L'attribut est moins important que prevu
  = : L'importance reste proche du prior

Lecture : élicitation contre révélation — deux sources de poids, deux vérités

Le tableau confronte deux manières d’obtenir les poids du même décideur. Les swing weights (méthode des points) viennent d’une élicitation directe : Sécurité 35,7 %, Prix 32,1 % — ce que le décideur dit. L’inférence bayésienne observe 5 choix répétés et révèle : Sécurité 44,4 %, Prix 33,3 % — ce que le décideur fait. L’écart n’est pas une erreur de mesure : c’est l’écart classique entre préférence déclarée et préférence révélée, ici quantifié attribut par attribut. La mécanique est la conjugaison Dirichlet–multinomiale : prior uniforme (1,1,1,1) + comptage des choix (2 prix, 3 sécurité) = posterior (3,4,1,1) — lisible sans calcul, comme le Beta-Bernoulli du notebook précédent. Réserve honnête : cinq observations laissent un posterior très plat (Consommation et Confort retombent à 11,1 % sans avoir jamais été choisis — sont-ils négligeables, ou simplement jamais décisifs dans les choix proposés ?) ; l’inférence gagne à être nourrie.

// Factor graph du modele d'apprentissage bayesien des poids

// Recreer un modele simplifie pour illustration
double[] pcFG = { 1.0, 1.0, 1.0, 1.0 };
Variable<Vector> poidsFG = Variable.Dirichlet(pcFG).Named("poids");

int nObsFG = 3;
Range obsRangeFG = new Range(nObsFG).Named("observations");
VariableArray<int> obsFG = Variable.Array<int>(obsRangeFG).Named("choix_observes");
obsFG[obsRangeFG] = Variable.Discrete(poidsFG).ForEach(obsRangeFG);
obsFG.ObservedValue = new[] { 0, 1, 1 };

var enginePoidsFG = new InferenceEngine();
enginePoidsFG.Compiler.CompilerChoice = Microsoft.ML.Probabilistic.Compiler.CompilerChoice.Roslyn;

// Rendu SVG reel du factor graph via Graphviz (meme demarche que Infer-3 / Infer-4).
// Infer.NET ecrit le fichier .gv ; FactorGraphHelper le convertit en SVG via l'outil `dot`.
enginePoidsFG.ShowFactorGraph = true;
var resultFG = enginePoidsFG.Infer<Dirichlet>(poidsFG);

Console.WriteLine("Structure du modele bayesien des poids (rendu SVG ci-dessous).");
Console.WriteLine();
Console.WriteLine($"Posterior : {resultFG}");
Console.WriteLine();
Console.WriteLine("Lecture du factor graph :");
Console.WriteLine("  - Le noeud Dirichlet (1,1,1,1) encode le prior uniforme sur les poids.");
Console.WriteLine("  - La variable latente `poids` (simplex de dimension 4) est tiree de ce prior.");
Console.WriteLine("  - Chaque observation Discrete (choix_observes) conditionne ce prior.");
Console.WriteLine("  - Le posterior Dirichlet(2 3 1 1) traduit les 3 choix observes (index 1 dominant).");

FactorGraphHelper.GetLatestFactorGraphHtml().DisplayAs("text/html");
Compiling model...done.
Structure du modele bayesien des poids (rendu SVG ci-dessous).

Posterior : Dirichlet(2 3 1 1)

Lecture du factor graph :
  - Le noeud Dirichlet (1,1,1,1) encode le prior uniforme sur les poids.
  - La variable latente `poids` (simplex de dimension 4) est tiree de ce prior.
  - Chaque observation Discrete (choix_observes) conditionne ce prior.
  - Le posterior Dirichlet(2 3 1 1) traduit les 3 choix observes (index 1 dominant).
Model_09_23_26_09_20_10_31.svg
Model node0 vDirichlet1 node1 Random node0->node1 dist node2 poids node1->node2 node3 Discrete node2->node3 probs node4 choix_observes[observations] node3->node4

Exercice 4 : Decision Multi-Critères avec Incertitude

Note pedagogique : ce bloc est positionne en tete de sequence pour l’ordre canonique header -> stub de l’exercice (cf. PR #7880).

Objectif

Etendre la méthode SMART pour gerer des valeurs d’attributs incertaines modèles par des distributions Gaussiennes dans Infer.NET, et calculer la distribution de la valeur totale V pour chaque alternative.

Contexte

Dans la section précédente, nous avons suppose que les valeurs des attributs etaient connues avec certitude. En realite, elles sont souvent incertaines :

  • Le salaire d’un poste “Freelance” est variable : Gaussian(60k, 100) (moyenne 60k, variance 100)
  • L’equilibre vie/travail en startup est incertain : Gaussian(4, 1) sur une echelle 1-10
  • La securite d’emploi d’une startup est difficile a evaluer : Gaussian(3, 2)

Avec Infer.NET, on peut propager cette incertitude jusqu’a la valeur totale V, obtenant une distribution sur V plutot qu’un simple score ponctuel.

Travail a realiser

Reprenez le problème de choix de carriere (Startup, Grande Entreprise, Freelance) de la section 7.

  1. Modeliser l’incertitude : Remplacer les valeurs certaines par des Variable avec distribution Gaussienne

    • Exemple : Variable<double> salaire_startup = Variable.GaussianFromMeanAndVariance(45, 25).Named("salaire_s");
    • Utilisez une variance de 5-25 selon le niveau d’incertitude sur chaque attribut
  2. Normaliser les variables : Créer des variables normalisees vSalaire = (salaire - salMin) / (salMax - salMin)

    • Hint : En Infer.NET, les opérations arithmetiques sur Variable sont supportees directement
  3. Calculer V avec les poids : Créer Variable<double> V = w1*v1 + w2*v2 + w3*v3 + w4*v4

  4. Inferer la distribution de V : Gaussian distV = engine.Infer<Gaussian>(V)

  5. Comparer les distributions : Afficher moyenne et ecart-type de V pour chaque carriere

  6. Analyser l’incertitude : Une carriere avec une V moyenne elevee mais très variable peut etre moins attractive qu’une carriere stable avec V moderee - interpreter le compromis

// Exercice : Decision SMART avec attributs incertains via Infer.NET// Poids SMART (repris de la section 7)double w_sal = 0.30;double w_wlb = 0.25;double w_pas = 0.25;double w_sec = 0.20;// Bornes de normalisation (repris de la section 7)double salMin = 35.0, salMax = 60.0;// TODO 1 : Modeliser la Startup avec des attributs incertains// Valeurs nominales : Salaire=45k, Equilibre=3, Passion=9, Securite=4// Indice: Variable.GaussianFromMeanAndVariance(moyenne, variance) cree une variable aleatoire// Utilisez des variances raisonnables (salaire: 25, attributs 1-10: variance 0.5-2)// TODO 2 : Calculer les valeurs normalisees pour la Startup// Indice: normalisation salaire = (sal - salMin) / (salMax - salMin)//         normalisation echelle 1-10 = (val - 1) / 9//         Les operations arithmetiques sur Variable<double> fonctionnent directement// TODO 3 : Calculer la valeur totale V pour la Startup// Indice: V = w_sal * v_sal + w_wlb * v_wlb + w_pas * v_pas + w_sec * v_sec// TODO 4 : Repeter pour Grande Entreprise (Salaire=55k, Equilibre=6, Passion=5, Securite=8)//          et Freelance (Salaire=60k, Equilibre=5, Passion=8, Securite=3)// TODO 5 : Inferer les distributions de V pour les 3 carrieres// Indice: creez un InferenceEngine avec CompilerChoice.Roslyn//         puis engine.Infer<Gaussian>(variable) pour chaque carriere// TODO 6 : Afficher et comparer les distributions// Pour chaque distribution, afficher moyenne, ecart-type et un commentaire sur l incertitude// TODO 7 : Identifier la meilleure option en tenant compte de l incertitude// Indice: Si deux V moyennes sont proches, l option avec le plus petit ecart-type est plus sureConsole.WriteLine("Exercice 4 a completer : modelisation Infer.NET des attributs incertains, propagation de l incertitude vers V.");

11. Resume

Concept Description
MAUT Multi-Attribute Utility Theory
Indépendance preferentielle Préférences sur X independantes de Y
Forme additive V(x) = Σ wᵢ × vᵢ(xᵢ)
Forme multiplicative Pour decisions sous incertitude
Swing weights Méthode pour determiner les poids
SMART Méthode pratique en 7 étapes

Pour aller plus loin

Si vous voulez… Consultez…
Visualiser les reseaux de decision DecInfer-05-Decision-Networks
Calculer la valeur de l’information DecInfer-06-Value-Information
Decisions séquentielles DecInfer-08-Sequential

Prochaine étape

Dans DecInfer-05-Decision-Networks, nous verrons :

  • Les reseaux de decision (influence diagrams)
  • Les noeuds de decision et d’utilite
  • Le calcul de la politique optimale

References

  • Keeney & Raiffa (1976) : Decisions with Multiple Objectives
  • Edwards & Barron (1994) : SMARTS and SMARTER
  • Russell & Norvig : AI, Chapter 16.4
Retour au sommet