The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Visualizing models as networks

library(networkformat)
library(ggraph)
library(tidygraph)

networkformat converts R objects into graph-ready format for visualization with ggraph. For tree models, the quickest path is:

For vectors and data frames, use edgelist() and nodelist() to build the edge/node data, then pass them to igraph or tidygraph. If you want to work with the raw edgelists and nodelists directly — filtering edges, computing graph statistics, or building custom igraph objects — see vignette("edgelist-nodelist").

Decision tree

tr <- tree::tree(Species ~ Sepal.Length + Sepal.Width, data = iris)
tg <- as_tbl_graph(tr)

The graph carries all the attributes you need for plotting — node labels, split variables, leaf predictions, observation counts, and parsed edge thresholds.

cat("Nodes:", igraph::vcount(tg), " Edges:", igraph::ecount(tg),
    " Depth:", max(igraph::distances(tg, mode = "out")[1, ]), "\n")
#> Nodes: 15  Edges: 14  Depth: 4
# Add a fill column: split variable for internal nodes, predicted class for leaves
tg <- tg %>%
  mutate(fill_var = ifelse(is_leaf, yval, var))

# Build a palette: one color per variable + one per class
node_df <- as.data.frame(tg, what = "vertices")
vars <- setdiff(unique(node_df$var), "<leaf>")
classes <- unique(node_df$yval[node_df$is_leaf])
var_pal <- c(
  setNames(c("#3B8EA5", "#D4A373"), vars[seq_len(min(length(vars), 2))]),
  setNames(c("#66c2a5", "#fc8d62", "#8da0cb"), classes[seq_len(min(length(classes), 3))])
)

ggraph(tg, layout = "tree") +
  geom_edge_diagonal(colour = "grey60", width = 0.5) +
  geom_edge_diagonal(
    aes(label = ifelse(is.na(split_point), "", paste0(split_op, split_point))),
    angle_calc = "along", label_dodge = unit(3, "mm"),
    label_size = 2.5, label_colour = "grey30", colour = NA) +
  geom_node_label(
    aes(label = label, fill = fill_var),
    size = 2.5, colour = "white", fontface = "bold",
    label.padding = unit(0.25, "lines"), label.r = unit(0.2, "lines")) +
  scale_fill_manual(values = var_pal, name = NULL) +
  theme_graph(base_family = "sans") +
  theme(legend.position = "bottom") +
  labs(title = "Iris classification tree",
       subtitle = "Split on Sepal.Length and Sepal.Width")

Internal nodes are colored by the split variable. Leaf nodes show the predicted class. Edge labels show the split condition (e.g. <5.45).

Random forest

as_tbl_graph(rf) combines all trees into a single graph with disconnected components — one per tree. Each node carries a treenum attribute so you can distinguish them.

rf <- randomForest::randomForest(Species ~ ., data = iris, ntree = 3, maxnodes = 5)

# All 3 trees in one graph
tg_rf <- as_tbl_graph(rf)
cat("Nodes:", igraph::vcount(tg_rf), " Edges:", igraph::ecount(tg_rf),
    " Components:", igraph::count_components(tg_rf), "\n")
#> Nodes: 27  Edges: 24  Components: 3
# Map integer prediction indices to class names
tg_rf <- tg_rf %>%
  mutate(
    pred_class = rf$classes[prediction],
    display_label = ifelse(
      is_leaf, pred_class,
      paste0(split_var_name, "\n< ", round(split_point, 1))
    ),
    fill_var = ifelse(is_leaf, pred_class, split_var_name)
  )

# Palette: one color per predictor variable + one per predicted class
node_df <- as.data.frame(tg_rf, what = "vertices")
predictor_vars <- sort(unique(na.omit(node_df$split_var_name)))
leaf_classes <- sort(unique(na.omit(node_df$pred_class)))
var_colors <- c("#3B8EA5", "#D4A373", "#7B68AE", "#E07B54")
class_colors <- c("#66c2a5", "#fc8d62", "#8da0cb")
var_pal_rf <- c(
  setNames(var_colors[seq_along(predictor_vars)], predictor_vars),
  setNames(class_colors[seq_along(leaf_classes)], leaf_classes)
)

ggraph(tg_rf, layout = "tree") +
  geom_edge_link(
    arrow = arrow(length = unit(1.5, "mm"), type = "closed"),
    end_cap = circle(5, "mm"),
    colour = "grey70", width = 0.3) +
  geom_node_label(
    aes(label = display_label, fill = fill_var),
    size = 2.5, colour = "black", fontface = "bold",
    label.padding = unit(0.2, "lines"), label.r = unit(0.15, "lines"),
    alpha = 0.85) +
  scale_fill_manual(values = var_pal_rf, name = NULL) +
  theme_graph(base_family = "sans") +
  theme(legend.position = "bottom") +
  labs(title = "Random forest (3 trees)",
       subtitle = "Each disconnected component is one tree")

All three trees are visible in a single plot. Internal nodes are colored by split variable; leaf nodes are colored by predicted class.

Single tree from a forest

For a closer look at one tree:

tg1 <- as_tbl_graph(rf, treenum = 1) %>%
  mutate(
    pred_class = rf$classes[prediction],
    display_label = ifelse(
      is_leaf, pred_class,
      paste0(split_var_name, "\n< ", round(split_point, 1))
    ),
    fill_var = ifelse(is_leaf, pred_class, split_var_name)
  )

ggraph(tg1, layout = "dendrogram") +
  geom_edge_elbow(colour = "grey65", width = 0.4) +
  geom_node_label(
    aes(label = display_label, fill = fill_var),
    colour = "black", size = 2.5, fontface = "bold",
    label.padding = unit(0.25, "lines"), label.r = unit(0.2, "lines"),
    label.size = 0.4, alpha = 0.2) +
  geom_node_text(
    aes(label = display_label),
    colour = "black", size = 2.5, fontface = "bold") +
  scale_fill_manual(values = var_pal_rf, name = NULL) +
  theme_graph(base_family = "sans") +
  theme(legend.position = "bottom") +
  labs(title = "Tree 1 from the random forest")
#> Warning in geom_node_label(aes(label = display_label, fill = fill_var), :
#> Ignoring unknown parameters: `label.size`

Course prerequisite network

networkformat also handles tabular data. The bundled courses dataset contains 13 courses across 4 departments, with prerequisite and crosslist relationships.

library(igraph)

# Prereqs as source so arrows naturally point prereq -> course;
# crosslist is undirected, so its direction doesn't matter
all_edges <- edgelist(courses,
                      source_cols = c(prereq, prereq2, crosslist),
                      target_cols = course)
all_edges$directed <- all_edges$from_col != "crosslist"
all_edges <- all_edges[all_edges$directed | all_edges$from < all_edges$to, ]

nodes <- nodelist(courses, id_col = course)
g <- graph_from_data_frame(all_edges, vertices = nodes)
tg_courses <- as_tbl_graph(g)

cat("Nodes:", vcount(g), " Edges:", ecount(g), "\n")
#> Nodes: 13  Edges: 19

dept_pal <- c(STAT = "#5B8DB8", MATH = "#E07B54", DATA = "#6BAF6B", CS = "#9B6BB0")

ggraph(tg_courses, layout = "stress") +
  geom_edge_link(
    aes(filter = directed),
    arrow = arrow(length = unit(2, "mm"), type = "closed"),
    end_cap = circle(8, "mm"),
    width = 0.5, colour = "grey45") +
  geom_edge_link(
    aes(filter = !directed),
    end_cap = circle(6, "mm"), start_cap = circle(6, "mm"),
    width = 0.8, colour = "#9B59B6", linetype = "dashed") +
  geom_node_label(
    aes(label = name, fill = dept),
    colour = "white", fontface = "bold", size = 3,
    label.padding = unit(0.3, "lines"), label.r = unit(0.2, "lines")) +
  scale_fill_manual(values = dept_pal, name = "Department") +
  theme_graph(base_family = "sans") +
  theme(legend.position = "bottom") +
  labs(title = "Course prerequisite network",
       subtitle = "Arrows point from prerequisite to dependent course  |  Dashed = crosslisted")

For the full data frame workflow — column selection, na.rm, attr_cols, symmetric_cols, and graph statistics — see vignette("edgelist-nodelist").

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.