defmodule Descisionex.PaymentMatrix do @moduledoc """ https://en.wikipedia.org/wiki/Decision-matrix_method """ alias Descisionex.{PaymentMatrix, Helper} defstruct matrix: [], variants: [], variants_num: 0, possible_steps: [], possible_steps_num: 0, probabilities: [], hurwitz_additional_value: 0.5, generalized_additional_value: 0.5, bayes_criterion: %{}, wald_criterion: %{}, maximax_criterion: %{}, laplace_criterion: %{}, savage_criterion: %{}, hurwitz_criterion: %{}, generalized_criterion: %{} @doc """ Set variants (states of nature) for payment matrix. ## Examples iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_variants(["some", "variants"]) %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: ["some", "variants"], variants_num: 2, wald_criterion: %{} } """ def set_variants(%PaymentMatrix{} = data, variants) do data |> Map.put(:variants, variants) |> Map.put(:variants_num, Enum.count(variants)) end @doc """ Set steps (strategies/decisions) for payment matrix. ## Examples iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_steps(["some", "steps"]) %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [], maximax_criterion: %{}, possible_steps: ["some", "steps"], possible_steps_num: 2, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } """ def set_steps(%PaymentMatrix{} = data, steps) do data |> Map.put(:possible_steps, steps) |> Map.put(:possible_steps_num, Enum.count(steps)) end @doc """ Set Hurwitz additional value for payment matrix (range from 0.1 to 0.9), defaults to 0.5. ## Examples iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_hurwitz_additional_value(0.3) %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.3, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_hurwitz_additional_value(0) ** (ArgumentError) Hurwitz additional value incorrect (number range must be from 0.1 to 0.9) """ def set_hurwitz_additional_value(%PaymentMatrix{} = data, value) do if 0.1 <= value && value <= 0.9 do Map.put(data, :hurwitz_additional_value, value) else raise ArgumentError, message: "Hurwitz additional value incorrect (number range must be from 0.1 to 0.9)" end end @doc """ Set Generalized additional value for payment matrix (range from 0.1 to 0.9), defaults to 0.5. ## Examples iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_generalized_additional_value(0.3) %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.3, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_generalized_additional_value(0) ** (ArgumentError) Generalized additional value incorrect (number range must be from 0.1 to 0.9) """ def set_generalized_additional_value(%PaymentMatrix{} = data, value) do if 0.1 <= value && value <= 0.9 do Map.put(data, :generalized_additional_value, value) else raise ArgumentError, message: "Generalized additional value incorrect (number range must be from 0.1 to 0.9)" end end @doc """ Set probabilities for states of nature (must sum to 1.0). Required for Bayes criterion. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.set_probabilities([0.4, 0.6]) %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [0.4, 0.6], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_probabilities([]) ** (ArgumentError) Probabilities must be not empty! iex> %Descisionex.PaymentMatrix{} |> Descisionex.PaymentMatrix.set_probabilities([0.3, 0.3]) ** (ArgumentError) Probabilities must sum to 1.0! """ def set_probabilities(%PaymentMatrix{} = _data, []) do raise ArgumentError, message: "Probabilities must be not empty!" end def set_probabilities(%PaymentMatrix{} = data, probabilities) do sum = Enum.sum(probabilities) unless abs(sum - 1.0) < 0.001 do raise ArgumentError, message: "Probabilities must sum to 1.0!" end Map.put(data, :probabilities, probabilities) end @doc """ Calculates Wald criterion (maximin) for payment matrix. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.calculate_wald_criterion() %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{criterion: 3, strategy_index: 1, strategy_name: nil} } """ def calculate_wald_criterion(%PaymentMatrix{} = data) do if data.matrix == [], do: raise(ArgumentError, message: "Matrix must be set!") all_criteria = Enum.map(data.matrix, fn row -> Enum.min(row) end) {wald_criterion, strategy_index} = Helper.find_max_criteria(all_criteria) Map.put(data, :wald_criterion, %{ criterion: wald_criterion, strategy_index: strategy_index, strategy_name: resolve_strategy_name(data, strategy_index) }) end @doc """ Calculates Maximax criterion (optimism) for payment matrix. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.calculate_maximax_criterion() %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{criterion: 4, strategy_index: 1, strategy_name: nil}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } """ def calculate_maximax_criterion(%PaymentMatrix{} = data) do if data.matrix == [], do: raise(ArgumentError, message: "Matrix must be set!") all_criteria = Enum.map(data.matrix, fn row -> Enum.max(row) end) {maximax_criterion, strategy_index} = Helper.find_max_criteria(all_criteria) Map.put(data, :maximax_criterion, %{ criterion: maximax_criterion, strategy_index: strategy_index, strategy_name: resolve_strategy_name(data, strategy_index) }) end @doc """ Calculates Laplace criterion (equal probability) for payment matrix. If variants are not explicitly set, the column count is inferred from the matrix. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.calculate_laplace_criterion() %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{criterion: 3.5, strategy_index: 1, strategy_name: nil}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } """ def calculate_laplace_criterion(%PaymentMatrix{} = data) do if data.matrix == [], do: raise(ArgumentError, message: "Matrix must be set!") variant_count = if data.variants_num > 0, do: data.variants_num, else: data.matrix |> List.first() |> length() all_criteria = data.matrix |> Enum.map(fn row -> Enum.map(row, fn element -> Float.round(element / variant_count, 3) end) end) |> Enum.map(fn row -> Enum.sum(row) end) {laplace_criterion, strategy_index} = Helper.find_max_criteria(all_criteria) Map.put(data, :laplace_criterion, %{ criterion: laplace_criterion, strategy_index: strategy_index, strategy_name: resolve_strategy_name(data, strategy_index) }) end @doc """ Calculates Hurwitz criterion (pessimism-optimism) for payment matrix. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.calculate_hurwitz_criterion() %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{criterion: 3.5, strategy_index: 1, strategy_name: nil}, laplace_criterion: %{}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } """ def calculate_hurwitz_criterion(%PaymentMatrix{} = data) do if data.matrix == [], do: raise(ArgumentError, message: "Matrix must be set!") additional_value = data.hurwitz_additional_value max = data.matrix |> Enum.map(fn row -> Enum.max(row) end) |> Enum.map(fn element -> num = element * additional_value if is_float(num), do: Float.round(num, 3), else: num end) |> Enum.with_index() min = data.matrix |> Enum.map(fn row -> Enum.min(row) end) |> Enum.map(fn element -> num = element * (1 - additional_value) if is_float(num), do: Float.round(num, 3), else: num end) {hurwitz_criterion, strategy_index} = max |> Enum.map(fn {element, index} -> element + Enum.at(min, index) end) |> Helper.find_max_criteria() Map.put(data, :hurwitz_criterion, %{ criterion: hurwitz_criterion, strategy_index: strategy_index, strategy_name: resolve_strategy_name(data, strategy_index) }) end @doc """ Calculates Savage criterion (minimax regret) for payment matrix. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.calculate_savage_criterion() %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{criterion: 0, strategy_index: 1, strategy_name: nil}, variants: [], variants_num: 0, wald_criterion: %{} } """ def calculate_savage_criterion(%PaymentMatrix{} = data) do if data.matrix == [], do: raise(ArgumentError, message: "Matrix must be set!") matrix = data.matrix max = matrix |> Matrix.transpose() |> Enum.map(fn row -> Enum.max(row) end) all_criteria = matrix |> Enum.map(fn row -> Enum.zip(max, row) |> Enum.map(fn {risk, elem} -> num = risk - elem if is_float(num), do: Float.round(num, 3), else: num end) end) |> Enum.map(fn row -> Enum.max(row) end) {savage_criterion, strategy_index} = Helper.find_min_criteria(all_criteria) Map.put(data, :savage_criterion, %{ criterion: savage_criterion, strategy_index: strategy_index, strategy_name: resolve_strategy_name(data, strategy_index) }) end @doc """ Calculates generalized criterion for payment matrix. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.calculate_generalized_criterion() %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{criterion: 1.5, strategy_index: 0, strategy_name: nil}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } """ def calculate_generalized_criterion(%PaymentMatrix{} = data) do if data.matrix == [], do: raise(ArgumentError, message: "Matrix must be set!") additional_value = data.generalized_additional_value max = data.matrix |> Enum.map(fn row -> Enum.max(row) end) |> Enum.map(fn element -> num = element * additional_value if is_float(num), do: Float.round(num, 3), else: num end) |> Enum.with_index() min = data.matrix |> Enum.map(fn row -> Enum.min(row) end) |> Enum.map(fn element -> num = element * additional_value if is_float(num), do: Float.round(num, 3), else: num end) {generalized_criterion, strategy_index} = max |> Enum.map(fn {element, index} -> element + Enum.at(min, index) end) |> Helper.find_min_criteria() Map.put(data, :generalized_criterion, %{ criterion: generalized_criterion, strategy_index: strategy_index, strategy_name: resolve_strategy_name(data, strategy_index) }) end @doc """ Calculates Bayes criterion (decision under risk) for payment matrix (probabilities must be set). ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.calculate_bayes_criterion() ** (ArgumentError) For Bayes criterion probabilities must be set! iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.set_probabilities([0.4, 0.6]) |> Descisionex.PaymentMatrix.calculate_bayes_criterion() %Descisionex.PaymentMatrix{ bayes_criterion: %{criterion: 3.6, strategy_index: 1, strategy_name: nil}, generalized_additional_value: 0.5, generalized_criterion: %{}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{}, laplace_criterion: %{}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{}, possible_steps: [], possible_steps_num: 0, probabilities: [0.4, 0.6], savage_criterion: %{}, variants: [], variants_num: 0, wald_criterion: %{} } """ def calculate_bayes_criterion(%PaymentMatrix{} = data) do if data.matrix == [], do: raise(ArgumentError, message: "Matrix must be set!") if data.probabilities == [], do: raise(ArgumentError, message: "For Bayes criterion probabilities must be set!") all_criteria = data.matrix |> Enum.map(fn row -> Enum.zip(data.probabilities, row) |> Enum.map(fn {prob, val} -> prob * val end) |> Enum.sum() |> then(fn sum -> if is_float(sum), do: Float.round(sum, 3), else: sum end) end) {bayes_criterion, strategy_index} = Helper.find_max_criteria(all_criteria) Map.put(data, :bayes_criterion, %{ criterion: bayes_criterion, strategy_index: strategy_index, strategy_name: resolve_strategy_name(data, strategy_index) }) end @doc """ Calculates all criteria for payment matrix. Includes Bayes criterion if probabilities are set. ## Examples iex> %Descisionex.PaymentMatrix{matrix: [[1, 2], [3, 4]]} |> Descisionex.PaymentMatrix.set_variants(["some", "variants"]) |> Descisionex.PaymentMatrix.calculate_criteria() %Descisionex.PaymentMatrix{ bayes_criterion: %{}, generalized_additional_value: 0.5, generalized_criterion: %{criterion: 1.5, strategy_index: 0, strategy_name: nil}, hurwitz_additional_value: 0.5, hurwitz_criterion: %{criterion: 3.5, strategy_index: 1, strategy_name: nil}, laplace_criterion: %{criterion: 3.5, strategy_index: 1, strategy_name: nil}, matrix: [[1, 2], [3, 4]], maximax_criterion: %{criterion: 4, strategy_index: 1, strategy_name: nil}, possible_steps: [], possible_steps_num: 0, probabilities: [], savage_criterion: %{criterion: 0, strategy_index: 1, strategy_name: nil}, variants: ["some", "variants"], variants_num: 2, wald_criterion: %{criterion: 3, strategy_index: 1, strategy_name: nil} } """ def calculate_criteria(%PaymentMatrix{} = data) do result = data |> calculate_wald_criterion() |> calculate_maximax_criterion() |> calculate_savage_criterion() |> calculate_laplace_criterion() |> calculate_hurwitz_criterion() |> calculate_generalized_criterion() if result.probabilities != [], do: calculate_bayes_criterion(result), else: result end defp resolve_strategy_name(data, strategy_index) do if data.possible_steps != [], do: Enum.at(data.possible_steps, strategy_index), else: nil end end