---
title: "Working with edgelists and nodelists"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Working with edgelists and nodelists}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup, message = FALSE}
library(networkformat)
```

`edgelist()` and `nodelist()` are the core functions of **networkformat**.
They extract network structure from vectors, data frames, lists, and tree
models as plain data frames, giving you full control over filtering, transforming,
and passing the results to igraph or any other tool.

For quick visualization without touching the raw data, see
`vignette("visualization")`.

# Decision tree

## edgelist()

```{r tree-edges, eval = requireNamespace("tree", quietly = TRUE)}
tr <- tree::tree(Species ~ Sepal.Length + Sepal.Width, data = iris)
edges <- edgelist(tr)
edges
```

Columns:

| Column | Description |
|--------|-------------|
| `from` | Parent node ID (binary heap index) |
| `to` | Child node ID (binary heap index) |
| `label` | Full split condition (e.g. `"Sepal.Length <5.45"`) |
| `split_var` | Variable name |
| `split_op` | `"<"` or `">"` (NA for categorical splits) |
| `split_point` | Numeric threshold (NA for categorical) |

The parsed columns let you filter or restyle without regex:

```{r tree-filter, eval = requireNamespace("tree", quietly = TRUE)}
# All edges that split on Sepal.Length
edges[edges$split_var == "Sepal.Length", ]

# Thresholds above 5
edges[!is.na(edges$split_point) & edges$split_point > 5, ]
```

## nodelist()

```{r tree-nodes, eval = requireNamespace("tree", quietly = TRUE)}
nodes <- nodelist(tr)
nodes
```

Columns:

| Column | Description |
|--------|-------------|
| `name` | Node ID (matches edgelist `from`/`to`) |
| `var` | Split variable, or `"<leaf>"` |
| `n` | Observation count |
| `dev` | Deviance |
| `yval` | Predicted value |
| `is_leaf` | Logical |
| `label` | Display label: `"<var>\nn=<n>"` or `"<yval>\nn=<n>"` |

The `label` column is ready for plotting --- no manual `ifelse()` needed.

## Pairing edges and nodes for igraph

Node IDs line up, so you can pass them straight to
`igraph::graph_from_data_frame()`:

```{r tree-igraph, eval = requireNamespace("tree", quietly = TRUE) && requireNamespace("igraph", quietly = TRUE)}
library(igraph)
g <- graph_from_data_frame(edges, directed = TRUE, vertices = nodes)
cat("Nodes:", vcount(g), " Edges:", ecount(g), "\n")
```

Or use `as.igraph(tr)` for the same result in one step.

## Visualizing the edgelist pipeline

The full workflow --- `edgelist()` -> `nodelist()` -> `graph_from_data_frame()` -> ggraph --- gives you control at every step. For a complete visualization example, see `vignette("visualization")`.

# Random forest

## edgelist()

```{r rf-edges, eval = requireNamespace("randomForest", quietly = TRUE), message = FALSE}
rf <- randomForest::randomForest(Species ~ ., data = iris, ntree = 3, maxnodes = 5)

# All trees
rf_edges <- edgelist(rf)
head(rf_edges)
```

Columns:

| Column | Description |
|--------|-------------|
| `from` | Parent node index |
| `to` | Child node index |
| `split_var` | Numeric variable index |
| `split_point` | Split threshold |
| `prediction` | Predicted value at child node |
| `direction` | Branch direction: `"left"` or `"right"` |
| `treenum` | Tree number |
| `split_var_name` | Human-readable variable name |

### The `treenum` argument

Extract specific trees without subsetting:

```{r rf-treenum, eval = requireNamespace("randomForest", quietly = TRUE)}
# Single tree
t1 <- edgelist(rf, treenum = 1)
nrow(t1)

# Trees 1 and 3
t13 <- edgelist(rf, treenum = c(1, 3))
table(t13$treenum)
```

## nodelist()

```{r rf-nodes, eval = requireNamespace("randomForest", quietly = TRUE)}
rf_nodes <- nodelist(rf, treenum = 1)
rf_nodes
```

Columns:

| Column | Description |
|--------|-------------|
| `name` | Node ID (matches edgelist `from`/`to`) |
| `is_leaf` | Logical |
| `split_var` | Numeric variable index (NA for leaves) |
| `split_var_name` | Variable name (NA for leaves) |
| `split_point` | Split threshold (NA for leaves) |
| `prediction` | Predicted value (NA for internal nodes) |
| `treenum` | Tree number |
| `label` | Variable name for internal nodes, predicted value for leaves |

# Data frame

## edgelist()

When your data is already tabular, `edgelist()` reshapes it into
from-to format. Columns are specified with
[tidyselect](https://tidyselect.r-lib.org/reference/language.html)
syntax --- bare names, strings, numbers, or helpers like `starts_with()`.

```{r df-basic}
courses

edgelist(courses, source_cols = course, target_cols = prereq)
```

### Multiple target columns

When you pass multiple `target_cols`, each produces a block of edges.
The `to_col` column identifies which relationship each edge came from:

```{r df-multi}
edgelist(courses, source_cols = course,
         target_cols = c(prereq, prereq2, crosslist))
```

### `na.rm` --- handling missing values

By default, rows where from or to is NA are removed. Set
`na.rm = FALSE` to keep them:

```{r df-narm}
# Default: NAs removed
nrow(edgelist(courses, source_cols = course,
              target_cols = c(prereq, prereq2, crosslist)))

# Keep NAs
nrow(edgelist(courses, source_cols = course,
              target_cols = c(prereq, prereq2, crosslist),
              na.rm = FALSE))
```

### `attr_cols` --- selecting edge attributes

By default, all columns not used as source or target are carried along.
Use `attr_cols` to select specific columns, or `c()` for none:

```{r df-attrs}
# Only from, to, metadata
edgelist(courses, source_cols = course, target_cols = prereq,
         attr_cols = c())

# Specific columns
edgelist(courses, source_cols = course, target_cols = prereq,
         attr_cols = c(dept, credits))
```

### `symmetric_cols` --- marking undirected relationships

Some target columns represent symmetric relationships (crosslists,
co-authorships). Use `symmetric_cols` to add a `directed` column and
automatically deduplicate symmetric edges:

```{r df-sym}
edgelist(courses, source_cols = course,
         target_cols = c(prereq, prereq2, crosslist),
         attr_cols = c(),
         symmetric_cols = crosslist)
```

Prerequisite edges get `directed = TRUE`; crosslist edges get
`directed = FALSE`. Symmetric edges are automatically deduped (only
`from <= to` kept). Set `dedupe = FALSE` to preserve both directions.

## nodelist()

For data frames, `nodelist()` simply reorders columns so the ID column
comes first --- convenient for `igraph::graph_from_data_frame()`:

```{r df-nodelist}
nodelist(courses, id_col = course)
```

## Note on tidygraph

tidygraph provides its own `as_tbl_graph.data.frame()` method that
auto-detects edge data frames with `from`/`to` columns. Since
networkformat already uses `from`/`to`, you can pass an edgelist
directly to tidygraph:

```{r tidygraph-note, eval = FALSE}
edges <- edgelist(courses, source_cols = course, target_cols = prereq)
tg <- tidygraph::as_tbl_graph(edges)
```

networkformat does not add its own `as_tbl_graph.data.frame()` method
since tidygraph already covers this case.

## Putting it together: course network

For a complete course network visualization, see `vignette("visualization")`.

Here we build the graph and compute statistics:

```{r courses-graph, eval = requireNamespace("igraph", quietly = TRUE), message = FALSE}
library(igraph)

all_edges <- edgelist(courses, source_cols = course,
                      target_cols = c(prereq, prereq2, crosslist),
                      attr_cols = c(),
                      symmetric_cols = crosslist)

# Reverse prereq arrows so they point from prereq -> dependent course
directed_rows <- all_edges$directed
all_edges[directed_rows, c("from", "to")] <- all_edges[directed_rows, c("to", "from")]

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

### Graph statistics

Once the data is in igraph, you can compute standard graph metrics:

```{r courses-stats, eval = requireNamespace("igraph", quietly = TRUE)}
cat("Nodes:", vcount(g), "\n")
cat("Edges:", ecount(g), "\n")
cat("Components:", count_components(g), "\n")

# In-degree: how many prerequisites each course has
deg_in <- degree(g, mode = "in")
cat("\nMost prerequisites:\n")
print(sort(deg_in[deg_in > 0], decreasing = TRUE))

# Out-degree: how many courses depend on this one
deg_out <- degree(g, mode = "out")
cat("\nMost depended-on:\n")
print(sort(deg_out[deg_out > 0], decreasing = TRUE))
```

# Lists

Lists produce a recursive parent-child edgelist. Each element becomes a node,
and nested lists create deeper edges. Path-style IDs (e.g. `root/a/b`) ensure
uniqueness.

## edgelist()

```{r list-edges}
edgelist(list(a = list(b = 1, c = 2), d = 3))
```

Columns:

| Column | Description |
|--------|-------------|
| `from` | Parent node path-style ID |
| `to` | Child node path-style ID |
| `depth` | Integer depth of the child node (root children are 1) |

Unnamed elements use positional indices:

```{r list-unnamed}
edgelist(list(1, 2, list(3, 4)))
```

Use `max_depth` to limit depth (root = 0, children = 1, ...):

```{r list-maxdepth}
edgelist(list(a = list(b = list(c = 1))), max_depth = 2)
```

## nodelist()

```{r list-nodes}
nodelist(list(a = list(b = 1, c = 2), d = 3))
```

Columns:

| Column | Description |
|--------|-------------|
| `name` | Path-style node ID |
| `depth` | Integer depth (root is 0) |
| `type` | Element class (`"numeric"`, `"character"`, `"list"`, etc.) |
| `n_children` | Number of direct children (0 for leaves) |
| `label` | Element name or positional index |

## S3 object fallthrough

S3 objects without a dedicated method (e.g. `lm`) are decomposed as plain
lists with a diagnostic message:

```{r list-s3, eval = FALSE}
fit <- lm(Sepal.Length ~ Sepal.Width, data = iris)
edgelist(fit)
#> No edgelist method for class 'lm'; treating as a plain list.
```

# Output reference

## edgelist()

| Input class | Columns returned |
|-------------|-----------------|
| atomic vector | `from`, `to`, [`weight`] |
| `list` | `from`, `to`, `depth` |
| `data.frame` | `from`, `to`, `from_col`, `to_col`, [`directed`], [`weight`], `<attr_cols>` |
| `tree` | `from`, `to`, `label`, `split_var`, `split_op`, `split_point` |
| `randomForest` | `from`, `to`, `split_var`, `split_point`, `prediction`, `direction`, `treenum`, `split_var_name` |
| `rpart` | `from`, `to`, `label`, `split_var`, `split_op`, `split_point` |
| `xgb.Booster` | `from`, `to`, `feature`, `split`, `quality`, `cover`, `treenum` |
| `gbm` | `from`, `to`, `split_var`, `split_point`, `prediction`, `treenum`, `split_var_name` |

> **Note:** rpart uses `<` and `>=` for split operators, while tree uses `<` and `>`.

## nodelist()

| Input class | Columns returned |
|-------------|-----------------|
| atomic vector | `name`, `n` |
| `list` | `name`, `depth`, `type`, `n_children`, `label` |
| `data.frame` | Reordered input (ID column first) |
| `tree` | `name`, `var`, `n`, `dev`, `yval`, `is_leaf`, `label` |
| `randomForest` | `name`, `is_leaf`, `split_var`, `split_var_name`, `split_point`, `prediction`, `treenum`, `label` |
| `rpart` | `name`, `var`, `n`, `dev`, `yval`, `is_leaf`, `label` |
| `xgb.Booster` | `name`, `is_leaf`, `feature`, `split`, `quality`, `cover`, `treenum`, `label` |
| `gbm` | `name`, `is_leaf`, `split_var`, `split_var_name`, `split_point`, `prediction`, `treenum`, `label` |

## as.igraph() / as_tbl_graph()

| Input class | `treenum` | Returns |
|-------------|-----------|---------|
| `tree` | --- | single graph |
| `randomForest` | `NULL` (default) | single graph (all trees as disconnected components) |
| `randomForest` | `1` | single graph (one tree) |
| `rpart` | --- | single graph |
| `xgb.Booster` | `NULL` (default) | single graph (all trees, globally unique string IDs) |
| `xgb.Booster` | `1` | single graph (one tree) |
| `gbm` | `NULL` (default) | single graph (all trees as disconnected components) |
| `gbm` | `1` | single graph (one tree) |
