Choreo.DecisionTree (Choreo v0.7.1)

Copy Markdown View Source

Decision-tree builder on top of Yog.

Choreo.DecisionTree models classification and choice trees where internal nodes are decisions (tests on features) and leaf nodes are outcomes (class labels or actions). Paths from root to leaf represent complete decision chains.

When to use

Use Choreo.DecisionTree when you need to document, validate, or visualize decision logic — business rules, ML classification trees, troubleshooting guides, or configuration selectors. It enforces tree invariants and detects logically inconsistent paths automatically.

The builder enforces tree invariants:

  • exactly one root node
  • every non-root node has exactly one parent
  • no cycles

Node types

  • :root — the starting decision node (exactly one per tree)
  • :decision — internal node testing a feature / attribute
  • :outcome — terminal leaf with a class label or action

Further reading

Quick Start

tree =
  Choreo.DecisionTree.new()
  |> Choreo.DecisionTree.set_root(:color, feature: "color")
  |> Choreo.DecisionTree.add_outcome(:red_stop, label: "Stop")
  |> Choreo.DecisionTree.add_outcome(:green_go, label: "Go")
  |> Choreo.DecisionTree.branch(:color, :red_stop, "red")
  |> Choreo.DecisionTree.branch(:color, :green_go, "green")

Choreo.DecisionTree.Analysis.decide(tree, %{"color" => "red"})
#=> {:ok, [:color, :red_stop], "Stop"}

dot = Choreo.DecisionTree.to_dot(tree)

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; color [label="color", penwidth="2.0", fillcolor="#8b5cf6", shape="diamond"]; red_stop [label="Stop", fillcolor="#10b981", style="rounded,filled", shape="box"]; green_go [label="Go", fillcolor="#10b981", style="rounded,filled", shape="box"]; color -> red_stop [fontcolor="#64748b", color="#64748b", label="red"]; color -> green_go [fontcolor="#64748b", color="#64748b", label="green"]; { rank=same; green_go; red_stop; } }

Summary

Functions

Adds an internal decision node.

Adds a leaf / outcome node.

Creates a branch from parent to child with a condition label.

Returns all branches as {parent, child, condition} tuples.

Returns the condition label on the branch from parent to child.

Returns all decision (internal) node IDs.

Creates a new empty decision tree.

Returns all node IDs in the tree.

Returns all outcome (leaf) node IDs.

Returns the root node id, or nil if not set.

Sets the root decision node of the tree.

Renders the decision tree to DOT format.

Returns the raw Yog.Graph struct underpinning the tree.

Types

t()

@type t() :: %Choreo.DecisionTree{
  edge_meta: %{optional({Yog.node_id(), Yog.node_id()}) => map()},
  graph: Yog.graph(),
  root: Yog.node_id() | nil
}

Functions

add_decision(tree, id, opts \\ [])

@spec add_decision(t(), Yog.node_id(), keyword()) :: t()

Adds an internal decision node.

Options

Examples

iex> tree = Choreo.DecisionTree.new()
iex> tree = Choreo.DecisionTree.add_decision(tree, :temp, feature: "temp")
iex> :temp in Choreo.DecisionTree.nodes(tree)
true
iex> Yog.node(tree.graph, :temp).node_type
:decision

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; temp [label="temp", fillcolor="#3b82f6", shape="diamond"]; }

add_outcome(tree, id, opts \\ [])

@spec add_outcome(t(), Yog.node_id(), keyword()) :: t()

Adds a leaf / outcome node.

Options

Examples

iex> tree = Choreo.DecisionTree.new()
iex> tree = Choreo.DecisionTree.add_outcome(tree, :play, label: "Play", class: "yes")
iex> :play in Choreo.DecisionTree.nodes(tree)
true
iex> Yog.node(tree.graph, :play).node_type
:outcome
iex> Yog.node(tree.graph, :play).class
"yes"

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; play [label="Play", fillcolor="#10b981", style="rounded,filled", shape="box"]; }

branch(tree, parent, child, condition)

@spec branch(t(), Yog.node_id(), Yog.node_id(), String.t()) :: t()

Creates a branch from parent to child with a condition label.

Enforces tree invariants:

  • child must not already have a parent
  • adding the edge must not create a cycle

Returns the updated tree. Raises ArgumentError on invariant violation.

Examples

iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...>   |> Choreo.DecisionTree.set_root(:color, feature: "color")
...>   |> Choreo.DecisionTree.add_outcome(:stop)
...>   |> Choreo.DecisionTree.branch(:color, :stop, "red")
iex> Yog.has_edge?(tree.graph, :color, :stop)
true
iex> Choreo.DecisionTree.condition(tree, :color, :stop)
"red"

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; stop [label="", fillcolor="#10b981", style="rounded,filled", shape="box"]; color [label="color", penwidth="2.0", fillcolor="#8b5cf6", shape="diamond"]; color -> stop [fontcolor="#64748b", color="#64748b", label="red"]; }
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.branch(tree, :y, :x, "2")
** (ArgumentError) Node :x already has a parent

branches(decision_tree)

@spec branches(t()) :: [{Yog.node_id(), Yog.node_id(), number()}]

Returns all branches as {parent, child, condition} tuples.

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, "yes")
iex> Choreo.DecisionTree.branches(tree)
[{:a, :x, "yes"}]

condition(decision_tree, parent, child)

@spec condition(t(), Yog.node_id(), Yog.node_id()) :: String.t() | nil

Returns the condition label on the branch from parent to child.

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, "yes")
iex> Choreo.DecisionTree.condition(tree, :a, :x)
"yes"
iex> Choreo.DecisionTree.condition(tree, :a, :missing)
nil

decisions(decision_tree)

@spec decisions(t()) :: [Yog.node_id()]

Returns all decision (internal) node IDs.

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, :b, "1")
...>   |> Choreo.DecisionTree.branch(:b, :x, "2")
iex> Enum.sort(Choreo.DecisionTree.decisions(tree))
[:a, :b]

new()

@spec new() :: t()

Creates a new empty decision tree.

Examples

iex> tree = Choreo.DecisionTree.new()
iex> Choreo.DecisionTree.nodes(tree)
[]
iex> Choreo.DecisionTree.root(tree)
nil

nodes(decision_tree)

@spec nodes(t()) :: [Yog.node_id()]

Returns all node IDs in the tree.

Examples

iex> tree = Choreo.DecisionTree.new()
iex> tree = tree
...>   |> Choreo.DecisionTree.set_root(:a, feature: "a")
...>   |> Choreo.DecisionTree.add_outcome(:x)
iex> Enum.sort(Choreo.DecisionTree.nodes(tree))
[:a, :x]

outcomes(decision_tree)

@spec outcomes(t()) :: [Yog.node_id()]

Returns all outcome (leaf) node IDs.

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> Enum.sort(Choreo.DecisionTree.outcomes(tree))
[:x, :y]

root(decision_tree)

@spec root(t()) :: Yog.node_id() | nil

Returns the root node id, or nil if not set.

Examples

iex> tree = Choreo.DecisionTree.new()
iex> Choreo.DecisionTree.root(tree)
nil
iex> tree = Choreo.DecisionTree.set_root(tree, :a, feature: "a")
iex> Choreo.DecisionTree.root(tree)
:a

set_root(tree, id, opts \\ [])

@spec set_root(t(), Yog.node_id(), keyword()) :: t()

Sets the root decision node of the tree.

Can only be called once. Raises ArgumentError if the tree already has a root.

Options

Examples

iex> tree = Choreo.DecisionTree.new()
iex> tree = Choreo.DecisionTree.set_root(tree, :weather, feature: "weather")
iex> Choreo.DecisionTree.root(tree)
:weather
iex> Yog.node(tree.graph, :weather).node_type
:root
iex> Yog.node(tree.graph, :weather).feature
"weather"

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.7, ranksep=1.2]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; weather [label="weather", penwidth="2.0", fillcolor="#8b5cf6", shape="diamond"]; }
iex> tree = Choreo.DecisionTree.new() |> Choreo.DecisionTree.set_root(:a, feature: "x")
iex> Choreo.DecisionTree.set_root(tree, :b, feature: "y")
** (ArgumentError) Tree already has a root

theme(name \\ :default, overrides \\ [])

@spec theme(
  atom(),
  keyword()
) :: Choreo.Theme.t()

Returns a theme for Choreo.DecisionTree.

Examples

iex> theme = Choreo.DecisionTree.theme(:default, graph_rankdir: :lr)
iex> theme.graph_rankdir
:lr

to_dot(tree, opts \\ [])

@spec to_dot(
  t(),
  keyword()
) :: String.t()

Renders the decision tree to DOT format.

Options

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> dot = Choreo.DecisionTree.to_dot(tree)
iex> String.contains?(dot, "digraph")
true
iex> String.contains?(dot, "red")
true
iex> String.contains?(dot, "green")
true

to_graph(decision_tree)

@spec to_graph(t()) :: Yog.graph()

Returns the raw Yog.Graph struct underpinning the tree.

Examples

iex> tree = Choreo.DecisionTree.new()
iex> graph = Choreo.DecisionTree.to_graph(tree)
iex> graph.kind
:directed