# `Choreo.DecisionTree.Analysis`
[🔗](https://github.com/code-shoily/choreo/blob/v0.14.1/lib/choreo/decision_tree/analysis.ex#L1)

Analysis functions for `Choreo.DecisionTree`.

Provides path enumeration, evaluation, rule extraction, test-case
generation, completeness checks, depth metrics, and pruning.

## Further reading

  * [Decision tree learning (Wikipedia)](https://en.wikipedia.org/wiki/Decision_tree_learning)
  * [Decision Tree Pruning](https://en.wikipedia.org/wiki/Decision_tree_pruning)

# `breadth`

```elixir
@spec breadth(Choreo.DecisionTree.t()) :: non_neg_integer()
```

Returns the number of leaf / outcome nodes.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop)
    ...>   |> Choreo.DecisionTree.add_outcome(:go)
    ...>   |> Choreo.DecisionTree.add_outcome(:caution)
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    ...>   |> Choreo.DecisionTree.branch(:color, :caution, "yellow")
    iex> Choreo.DecisionTree.Analysis.breadth(tree)
    3

This analysis answers the question: "How many leaf outcomes exist?"

# `cyclic?`

```elixir
@spec cyclic?(Choreo.DecisionTree.t()) :: boolean()
```

Checks whether the decision tree contains a directed cycle.

In a well-formed tree, cycles are prohibited.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:a, feature: "a")
    ...>   |> Choreo.DecisionTree.add_outcome(:x)
    ...>   |> Choreo.DecisionTree.branch(:a, :x, "1")
    iex> Choreo.DecisionTree.Analysis.cyclic?(tree)
    false

This analysis answers the question: "Does the decision tree contain any cycles?"

# `dead_ends`

```elixir
@spec dead_ends(Choreo.DecisionTree.t()) :: [Yog.node_id()]
```

Returns nodes that cannot reach any outcome (terminal leaf) node.

In a well-formed tree, every decision node should eventually lead to an
outcome. Nodes with no path to an outcome represent dead-end logic.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:a, feature: "a")
    ...>   |> Choreo.DecisionTree.add_decision(:b, feature: "b")
    ...>   |> Choreo.DecisionTree.add_outcome(:x)
    ...>   |> Choreo.DecisionTree.branch(:a, :x, "1")
    ...>   |> Choreo.DecisionTree.branch(:a, :b, "2")
    iex> Choreo.DecisionTree.Analysis.dead_ends(tree)
    [:b]

This analysis answers the question: "Which decision paths lead nowhere?"

# `decide`

```elixir
@spec decide(Choreo.DecisionTree.t(), %{required(String.t()) =&gt; String.t()}) ::
  {:ok, [Yog.node_id()], String.t()} | {:error, String.t()}
```

Evaluates the decision tree against a map of feature values.

Walks from the root, at each decision node reading the corresponding
feature value and following the branch whose condition matches.

Returns `{:ok, path, outcome_label}` or `{:error, reason}`.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, label: "Stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go, label: "Go")
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> Choreo.DecisionTree.Analysis.decide(tree, %{"color" => "red"})
    {:ok, [:color, :stop], "Stop"}
    iex> Choreo.DecisionTree.Analysis.decide(tree, %{"color" => "blue"})
    {:error, "No branch for 'blue' from node :color"}
    iex> Choreo.DecisionTree.Analysis.decide(Choreo.DecisionTree.new(), %{})
    {:error, "Tree has no root"}

This analysis answers the question: "Given feature values, what outcome does the tree predict?"

# `depth`

```elixir
@spec depth(Choreo.DecisionTree.t()) :: non_neg_integer()
```

Returns the maximum depth of the tree (number of edges from root
to deepest leaf).

A single-node tree has depth 0.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:a, feature: "a")
    ...>   |> Choreo.DecisionTree.add_decision(:b, feature: "b")
    ...>   |> Choreo.DecisionTree.add_outcome(:x)
    ...>   |> Choreo.DecisionTree.add_outcome(:y)
    ...>   |> Choreo.DecisionTree.branch(:a, :b, "1")
    ...>   |> Choreo.DecisionTree.branch(:b, :x, "2")
    ...>   |> Choreo.DecisionTree.branch(:b, :y, "3")
    iex> Choreo.DecisionTree.Analysis.depth(tree)
    2
    iex> Choreo.DecisionTree.Analysis.depth(Choreo.DecisionTree.new())
    0

This analysis answers the question: "How deep is the tree?"

# `feature_importance`

```elixir
@spec feature_importance(Choreo.DecisionTree.t()) :: %{
  required(String.t()) =&gt; non_neg_integer()
}
```

Returns a map of feature frequencies across all decision nodes.

Useful for understanding which features drive the most splits.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:weather, feature: "weather")
    ...>   |> Choreo.DecisionTree.add_decision(:wind, feature: "wind")
    ...>   |> Choreo.DecisionTree.add_outcome(:play)
    ...>   |> Choreo.DecisionTree.branch(:weather, :wind, "cloudy")
    ...>   |> Choreo.DecisionTree.branch(:wind, :play, "calm")
    iex> Choreo.DecisionTree.Analysis.feature_importance(tree)
    %{"weather" => 1, "wind" => 1}

This analysis answers the question: "Which features drive the most splits?"

# `generate_test_cases`

```elixir
@spec generate_test_cases(Choreo.DecisionTree.t()) :: [
  %{required(String.t()) =&gt; String.t()}
]
```

Generates feature maps that cover every reachable leaf path.

Each generated map can be passed to `decide/2` to reach a distinct
outcome. This is useful for testing or for validating that every
rule in the tree is exercisable.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, label: "Stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go, label: "Go")
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> test_cases = Choreo.DecisionTree.Analysis.generate_test_cases(tree)
    iex> length(test_cases)
    2
    iex> %{"color" => "red"} in test_cases
    true
    iex> %{"color" => "green"} in test_cases
    true

This analysis answers the question: "What inputs exercise every path?"

# `inconsistent_paths`

```elixir
@spec inconsistent_paths(Choreo.DecisionTree.t()) :: [{[Yog.node_id()], [String.t()]}]
```

Finds logically impossible paths where a feature is checked against
mutually exclusive conditions.

Returns a list of tuples `{path, [features_with_conflicts]}`.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_decision(:shade, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, label: "Stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go1, label: "Go")
    ...>   |> Choreo.DecisionTree.add_outcome(:go2, label: "Go")
    ...>   |> Choreo.DecisionTree.branch(:color, :shade, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go1, "green")
    ...>   |> Choreo.DecisionTree.branch(:shade, :stop, "dark")
    ...>   |> Choreo.DecisionTree.branch(:shade, :go2, "light")
    iex> inconsistencies = Choreo.DecisionTree.Analysis.inconsistent_paths(tree)
    iex> length(inconsistencies)
    2
    iex> Enum.any?(inconsistencies, fn {_path, features} -> "color" in features end)
    true

This analysis answers the question: "Are there logically impossible paths?"

# `missing_branches`

```elixir
@spec missing_branches(Choreo.DecisionTree.t(), %{
  required(String.t()) =&gt; [String.t()]
}) :: [
  {Yog.node_id(), String.t(), [String.t()]}
]
```

Finds decision nodes whose outgoing branches do not cover an expected
set of feature values.

Accepts a map of `feature => [expected_values]`. For each decision node
testing that feature, returns `{node_id, feature, missing_values}` when
values are absent.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop)
    ...>   |> Choreo.DecisionTree.add_outcome(:go)
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> Choreo.DecisionTree.Analysis.missing_branches(tree, %{"color" => ["red", "green", "blue"]})
    [{:color, "color", ["blue"]}]

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:a, feature: "a")
    ...>   |> Choreo.DecisionTree.add_outcome(:x)
    ...>   |> Choreo.DecisionTree.branch(:a, :x, "1")
    iex> Choreo.DecisionTree.Analysis.missing_branches(tree, %{"a" => ["1"]})
    []

This analysis answers the question: "Which expected branches are missing?"

# `orphan_nodes`

```elixir
@spec orphan_nodes(Choreo.DecisionTree.t()) :: [Yog.node_id()]
```

Returns nodes that are not reachable from the root.

In a well-formed tree every declared node should be reachable. Nodes
added without a connecting branch are reported as orphans.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:a, feature: "a")
    ...>   |> Choreo.DecisionTree.add_outcome(:x)
    ...>   |> Choreo.DecisionTree.add_outcome(:y)
    ...>   |> Choreo.DecisionTree.branch(:a, :x, "1")
    iex> Choreo.DecisionTree.Analysis.orphan_nodes(tree)
    [:y]

This analysis answers the question: "Which declared nodes are unreachable?"

# `outcome_distribution`

```elixir
@spec outcome_distribution(Choreo.DecisionTree.t()) :: %{
  required(String.t()) =&gt; non_neg_integer()
}
```

Returns the distribution of reachable outcome classes across all paths.

Counts how many reachable leaf outcomes correspond to each class (or label
if no class is set).

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, class: "stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:slow, class: "stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go, class: "go")
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :slow, "yellow")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> Choreo.DecisionTree.Analysis.outcome_distribution(tree)
    %{"go" => 1, "stop" => 2}

This analysis answers the question: "How are reachable outcomes distributed across classes?"

# `paths`

```elixir
@spec paths(Choreo.DecisionTree.t()) :: [[Yog.node_id()]]
```

Enumerates all root-to-leaf paths.

Each path is a list of node IDs from root to outcome.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, label: "Stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go, label: "Go")
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> Enum.sort(Choreo.DecisionTree.Analysis.paths(tree))
    [[:color, :go], [:color, :stop]]

This analysis answers the question: "What are all possible root-to-leaf paths?"

# `paths_with_conditions`

```elixir
@spec paths_with_conditions(Choreo.DecisionTree.t()) :: [
  {[Yog.node_id()], [{Yog.node_id(), Yog.node_id(), String.t()}]}
]
```

Returns all root-to-leaf paths with their branch conditions.

Each result is `{path, [{parent, child, condition}]}`.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, label: "Stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go, label: "Go")
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> paths = Choreo.DecisionTree.Analysis.paths_with_conditions(tree)
    iex> {[:color, :stop], [{:color, :stop, "red"}]} in paths
    true
    iex> {[:color, :go], [{:color, :go, "green"}]} in paths
    true

This analysis answers the question: "What are all paths with their branch conditions?"

# `prune_redundant`

```elixir
@spec prune_redundant(Choreo.DecisionTree.t()) :: Choreo.DecisionTree.t()
```

Prunes redundant decision nodes.

A decision is redundant when **all** of its descendant leaves share
the same class label. The decision node is replaced by an outcome
node with that label.

Returns a new tree.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_decision(:shade, feature: "shade")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop_light, label: "Stop", class: "stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop_dark, label: "Stop", class: "stop")
    ...>   |> Choreo.DecisionTree.branch(:color, :shade, "red")
    ...>   |> Choreo.DecisionTree.branch(:shade, :stop_light, "light")
    ...>   |> Choreo.DecisionTree.branch(:shade, :stop_dark, "dark")
    iex> pruned = Choreo.DecisionTree.Analysis.prune_redundant(tree)
    iex> Choreo.DecisionTree.outcomes(pruned)
    [:color]
    iex> :color in Choreo.DecisionTree.decisions(pruned)
    false

This analysis answers the question: "Which decision nodes can be simplified?"

# `reachable_outcomes`

```elixir
@spec reachable_outcomes(Choreo.DecisionTree.t()) :: [String.t()]
```

Returns the unique set of all possible outcome classes the tree can produce.

Only considers outcomes that are actually reachable from the root.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, class: "stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go, class: "go")
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> Enum.sort(Choreo.DecisionTree.Analysis.reachable_outcomes(tree))
    ["go", "stop"]

This analysis answers the question: "What are all possible outcome classes?"

# `rules`

```elixir
@spec rules(Choreo.DecisionTree.t()) :: [
  %{
    conditions: %{required(String.t()) =&gt; String.t()},
    outcome: %{class: String.t() | nil, label: String.t() | nil}
  }
]
```

Extracts IF-THEN rules from the decision tree.

Each rule maps the conditions along a root-to-leaf path to the outcome
reached at that leaf.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop, label: "Stop", class: "stop")
    ...>   |> Choreo.DecisionTree.add_outcome(:go, label: "Go", class: "go")
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> rules = Choreo.DecisionTree.Analysis.rules(tree)
    iex> length(rules)
    2
    iex> Enum.find(rules, fn r -> r.outcome.class == "stop" end)
    %{conditions: %{"color" => "red"}, outcome: %{class: "stop", label: "Stop"}}

This analysis answers the question: "What IF-THEN rules does the tree encode?"

# `validate`

```elixir
@spec validate(Choreo.DecisionTree.t()) :: [{:error | :warning, String.t()}]
```

Validates tree completeness and structural invariants.

Checks for:
  * missing root
  * cycles in the hierarchy
  * multiple parents (converging branches)
  * decision nodes with no branches
  * outcome nodes with branches (should be leaves)
  * duplicate conditions from the same parent
  * orphan nodes (not reachable from root)

Returns a list of `{severity, message}` tuples.

## Examples

    iex> tree = Choreo.DecisionTree.new()
    iex> tree = tree
    ...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
    ...>   |> Choreo.DecisionTree.add_outcome(:stop)
    ...>   |> Choreo.DecisionTree.add_outcome(:go)
    ...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
    ...>   |> Choreo.DecisionTree.branch(:color, :go, "green")
    iex> Choreo.DecisionTree.Analysis.validate(tree)
    []

    iex> tree = Choreo.DecisionTree.new()
    iex> Choreo.DecisionTree.Analysis.validate(tree)
    [{:error, "Tree has no root"}]

This analysis answers the question: "Is the tree structurally valid?"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
