build_htna() accepts data in three
interchangeable shapes: 1. a named list of per-actor-type nodes
2. a single combined frame with an actor-type column 3. a single
combined frame with a node-to-actor-type dictionary.
This vignette walks through each form, shows that all three produce
the same network, and then covers two extra parameters that compose with
any input form: disambiguate (for code-label collisions)
and group (for per-cohort networks).
To keep the examples concrete, htna ships four bundled
objects covering every shape the vignette needs:
-
human_simplifiedandai_simplified: per-actor long datasets, one per actor type. These feed Form 1 directly. -
human_ai: the same data row-bound into one long frame with anactor_typecolumn (and a pre-computedphasecohort tag for the grouped example at the end). This feeds Form 2 and Form 3. -
human_ai_codebook— a tidy two-column code → actor-type lookup, ready to pass to Form 3.
Setup
All three long frames share the canonical schema htna
expects: actor/session_id, code,
order_in_session. Within each session,
order_in_session is a shared time index
across both actors.
Form 1 — Named list of per-actor frames
The most direct form. Each entry is one actor’s long frame; the list
name becomes the actor-type label on the network. The bundled
human_simplified and ai_simplified frames
already have this shape — one per actor type, sharing the canonical
schema.
net_list <- build_htna(list(Human = human_simplified, AI = ai_simplified),
actor = "session_id")
summary(net_list)
#> <htna network>
#> Method: relative
#> Sessions: 429 (max 287 timesteps)
#> Nodes: 12
#> Edges: 135 / 144 (non-zero)
#>
#> Actor types (2):
#> Human (6 nodes): Check, Frustrate, Inquire, Refine, Request, Specify
#> AI (6 nodes): Ask, Delegate, Execute, Plan, Repair, Report
#>
#> Edge counts by actor (rows = source, cols = target):
#> Human AI
#> Human 35 36
#> AI 36 28
plot_htna(net_list)
Use this form when each actor’s data lives in a separate frame (the typical case when the two streams were prepared independently).
Form 2 — Single combined frame with actor_type
When the data already comes as one long table, tag each row with the
actor-type and pass the column name to actor_type =.
net_actor_type <- build_htna(human_ai,
actor_type = "actor_type",
actor = "session_id",
order = "order_in_session")The actor-type order on the resulting network follows the factor levels (or, for character vectors, the order of first appearance), which is how the actor groups will be ordered in legends and plots.
Form 3 — Single combined frame with node_groups
When the combined frame has no actor-type column,
declare the node-to-actor partition directly via
node_groups. node_groups accepts two
interchangeable shapes:
3a. Named list of code vectors
Extract the per-actor code sets from the bundled per-actor frames and pass them as a named list. The list names become the actor-type labels on the network.
human_codes <- c("Specify", "Request", "Frustrate", "Check", "Inquire", "Refine")
ai_codes <- c("Delegate", "Plan", "Execute", "Ask", "Repair", "Report")
net_node_groups <- build_htna(
human_ai,
actor = "session_id",
node_groups = list(Human = human_codes, AI = ai_codes)
)
net_node_groups
#> Transition Network (relative probabilities) [directed]
#> Weights: [0.002, 0.611] | mean: 0.089
#>
#> Weight matrix:
#> Ask Check Delegate Execute Frustrate Inquire Plan Refine Repair
#> Ask 0.018 0.063 0.000 0.022 0.116 0.063 0.409 0.051 0.004
#> Check 0.117 0.050 0.015 0.411 0.058 0.005 0.008 0.042 0.033
#> Delegate 0.000 0.039 0.000 0.011 0.110 0.032 0.611 0.014 0.000
#> Execute 0.061 0.087 0.000 0.074 0.143 0.088 0.107 0.069 0.002
#> Frustrate 0.194 0.104 0.039 0.119 0.114 0.060 0.004 0.098 0.029
#> Inquire 0.251 0.069 0.027 0.285 0.039 0.033 0.009 0.016 0.042
#> Plan 0.000 0.146 0.000 0.015 0.215 0.083 0.003 0.086 0.005
#> Refine 0.143 0.045 0.006 0.206 0.008 0.014 0.013 0.000 0.025
#> Repair 0.241 0.012 0.028 0.391 0.043 0.047 0.016 0.024 0.004
#> Report 0.102 0.114 0.000 0.009 0.124 0.102 0.050 0.058 0.039
#> Request 0.148 0.055 0.018 0.292 0.016 0.010 0.008 0.009 0.007
#> Specify 0.269 0.017 0.040 0.273 0.096 0.011 0.015 0.002 0.013
#> Report Request Specify
#> Ask 0.028 0.173 0.052
#> Check 0.055 0.049 0.156
#> Delegate 0.018 0.131 0.035
#> Execute 0.010 0.293 0.065
#> Frustrate 0.021 0.135 0.085
#> Inquire 0.170 0.028 0.029
#> Plan 0.026 0.325 0.095
#> Refine 0.023 0.105 0.413
#> Repair 0.103 0.071 0.020
#> Report 0.077 0.257 0.066
#> Request 0.034 0.067 0.336
#> Specify 0.037 0.126 0.101
#>
#> Initial probabilities:
#> Specify 0.818 ████████████████████████████████████████
#> Request 0.156 ████████
#> Frustrate 0.023 █
#> Refine 0.002
#> Ask 0.000
#> Check 0.000
#> Delegate 0.000
#> Execute 0.000
#> Inquire 0.000
#> Plan 0.000
#> Repair 0.000
#> Report 0.0003b. Two-column data frame (codebook)
The same partition expressed as a tidy codebook — one row per code,
one column for the action and one for the actor type. The action column
must match the action parameter (default
"code"); the other column is treated as the actor-type tag.
Column order is irrelevant.
The bundled human_ai_codebook already has this shape, so
it can be passed straight through:
head(human_ai_codebook)
#> code actor_type
#> 1 Specify Human
#> 2 Request Human
#> 3 Frustrate Human
#> 4 Check Human
#> 5 Inquire Human
#> 6 Refine Human
net_node_groups_df <- build_htna(
human_ai,
session = "session_id",
node_groups = human_ai_codebook
)The actor-type ordering follows the column’s factor levels (if a factor) or the order of first appearance (if a character vector). Use this form when your code/actor mapping already lives in a tidy lookup table.
All forms produce the same network
build_htna() resolves every input shape to the same
combined sequence and forwards to
Nestimate::build_network(). The four networks below are
drawn side by side — the layout, colours, and edges are
indistinguishable:
forms <- list(
list_form = net_list,
actor_type_form = net_actor_type,
node_groups_list = net_node_groups,
node_groups_df = net_node_groups_df
)
op <- par(mfrow = c(2, 2), mar = c(1, 1, 3, 1))
for (nm in names(forms)) plot_htna(forms[[nm]], title = nm)
par(op)Handling code-label collisions with disambiguate =
If both actor types share a code label (e.g. both Human and AI have a
code called Ask and another code called
Reply), build_htna() errors by default —
keeping such states distinct is usually what you want. Pass
disambiguate = TRUE to prefix every code with its
actor-type label so the two Ask nodes become
Human:Ask and AI:Ask.
overlap <- data.frame(
session_id = rep(paste0("s", 1:3), each = 6),
code = rep(c("Ask", "Reply", "Ask", "Reply", "Ask", "Reply"), 3),
actor_type = rep(c("Human", "Human", "AI", "AI", "Human", "AI"), 3),
order_in_session = rep(1:6, 3),
stringsAsFactors = FALSE
)
# Without disambiguate=, this errors:
try(build_htna(overlap, actor_type = "actor_type"))
#> Error : Code(s) appear in more than one actor group: Ask, Reply. Pass `disambiguate = TRUE` to prefix codes with the actor label.
# With disambiguate=, the two `Ask`s become distinct nodes:
net_dis <- build_htna(overlap, actor_type = "actor_type",
disambiguate = TRUE)
net_dis$nodes$label
#> [1] "AI:Ask" "AI:Reply" "Human:Ask" "Human:Reply"
plot(net_dis)
Splitting into cohorts with group =
Any of the three input forms can additionally be split into
per-cohort networks by passing group = — the result is an
htna_group, one htna network per level of the
grouping column. human_ai already ships with a
phase column tagging each session as Early or
Late (a chronological split: first-half of sessions =
early, rest = late), so the cohort build is a one-liner:
grp <- build_htna(human_ai, actor_type = "actor_type", group = "phase")
class(grp)
#> [1] "htna_group" "netobject_group" "list"
names(grp)
#> [1] "Late" "Early"
plot_htna(grp)

Cheat sheet
| Form | When to use |
|---|---|
list(Actor1 = a1, Actor2 = a2) |
Each actor’s data is in its own frame |
df, actor_type = "actor_type" |
One combined frame, actor identity in a row-level column |
df, node_groups = list(Actor1 = ..., Actor2 = ...) |
One combined frame; partition declared as a named list of codes |
df, node_groups = data.frame(code = ..., actor_type = ...) |
One combined frame; partition declared as a tidy codebook |
+ actor = "user_id" |
Identify the individual actor that performed each event (forwarded
to Nestimate::build_network()) |
+ disambiguate = TRUE |
Same code label appears in more than one actor type |
+ group = "phase" |
Build one network per cohort/phase |
All three input shapes are interchangeable; pick whichever matches the shape your data already has.
