| Title: | Tidy Temporal Network Analysis |
|---|---|
| Description: | Building, analysis and visualisation of temporal networks. Supports several formats of relational data: interval data with explicit start and end times, contact data of instantaneous events, threaded interactions such as forum or chat data, and co-presence logs. Every analysis returns a tidy data frame carrying proper vertex labels. Metrics cover time-varying centrality, graph-level structure, edge formation and dissolution, burstiness, time-respecting paths, reachability and mixing. Offers a wide variety of network visualisations and animations, as well as plots of temporal network metrics across time. |
| Authors: | Mohammed Saqr [aut, cre, cph] |
| Maintainer: | Mohammed Saqr <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.7.3 |
| Built: | 2026-10-18 15:01:41 UTC |
| Source: | https://github.com/mohsaqr/Dynet |
The same operation as add_ties(), with one extra guarantee: the network
must already be directed, so from and to keep the direction the caller
means. Adding an arc to an undirected network raises a condition of class
dynet_needs_directed rather than silently recording a symmetric tie.
add_arcs(dn, data, loops = FALSE)add_arcs(dn, data, loops = FALSE)
dn |
A temporal network from |
data |
A nonempty data frame with |
loops |
Whether added self-loops are permitted. |
A new directed dynet object carrying the added spells, with the
same structure as the input.
add_ties(), which does not require a directed network.
dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) dn <- add_nodes(dn, "C") add_arcs(dn, data.frame(from = "B", to = "C", start = 1, end = 2))dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) dn <- add_nodes(dn, "C") add_arcs(dn, data.frame(from = "B", to = "C", start = 1, end = 2))
Add nodes to a temporal network
add_nodes(dn, data)add_nodes(dn, data)
dn |
A temporal network from |
data |
A character vector of new node names or a data frame containing
a |
Existing nodes and attributes are unchanged. Missing attribute
values are filled with typed NA. If the source has a cograph grouping,
each new node must supply its group through a groups column in data
or through the source attribute from which that grouping was derived;
otherwise a condition of class dynet_missing_group is raised.
A new dynet object, of the same class and structure as the input,
with the added nodes represented as implicit always-active isolates until
ties or vertex activity are supplied. The input is unchanged.
dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) add_nodes(dn, "C")dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) add_nodes(dn, "C")
Add temporal ties
add_ties(dn, data, loops = FALSE)add_ties(dn, data, loops = FALSE)
dn |
A temporal network from |
data |
A nonempty data frame with |
loops |
Whether added self-loops are permitted. |
Added times use the existing network clock. A sessioned network requires an existing session label on every row; mutation does not create a new session scheme. Implicit observation support expands to include the new raw ties, while explicit observation support remains fixed. Added raw rows are interval-format tie identities even when the source was originally a contact, threaded, or co-presence log.
A new dynet object, of the same class and structure as the input.
The input is unchanged; canonical temporal ties and every flattened
cograph field are rebuilt together.
dn <- dynet(data.frame( from = "A", to = "B", start = 0, end = 1 )) dn <- add_nodes(dn, "C") add_ties(dn, data.frame(from = "B", to = "C", start = 1, end = 2))dn <- dynet(data.frame( from = "A", to = "B", start = 0, end = 1 )) dn <- add_nodes(dn, "C") add_ties(dn, data.frame(from = "B", to = "C", start = 1, end = 2))
Add declared vertex-activity spells
add_vertex_spells(dn, data)add_vertex_spells(dn, data)
dn |
A temporal network. |
data |
A nonempty vertex-spell data frame with |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), with the network's existing
activity and data canonicalised together, so an added spell that
overlaps or abuts an existing one for the same vertex is merged into it
and canonical spell identifiers may change. Raises dynet_unknown_node
for a vertex the network does not have, and dynet_bad_input when data
is not a nonempty data frame, and dynet_incompatible_vertex_spells when
session is supplied to a network with no session scheme – the same
refusal update_vertex_spells() makes, rather than dropping the label.
dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) extended <- add_vertex_spells(enrolled, data.frame(node = "Cara", start = 5, end = 20)) as.data.frame(extended, what = "vertex_spells")dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) extended <- add_vertex_spells(enrolled, data.frame(node = "Cara", start = 5, end = 20)) as.data.frame(extended, what = "vertex_spells")
Draws the network bin by bin over the measurement grid and writes the
frames to an animated GIF or a video. The grid is the same four arguments
every measuring verb takes, so an animation shows exactly what
snapshots() tabulates and what plot(dn, type = "snapshots") draws as a
filmstrip, with the bins joined by motion.
animate( dn, start = NULL, end = NULL, step = NULL, window = NULL, sessions = c("bounded", "collapse"), layout = "spring", measure = NULL, tween = 6L, fps = 12, file = tempfile(fileext = ".gif"), loop = TRUE, width = 800L, height = 800L, res = 120, palette = "okabe", tie_states = TRUE, timeline = TRUE, absent = c("fade", "away", "hide"), isolates = c("fade", "show", "hide"), ease = c("dwell", "continuous"), max_displacement = 0.08, anchor_strength = 1, layout_args = list(), seed = 42L, ... )animate( dn, start = NULL, end = NULL, step = NULL, window = NULL, sessions = c("bounded", "collapse"), layout = "spring", measure = NULL, tween = 6L, fps = 12, file = tempfile(fileext = ".gif"), loop = TRUE, width = 800L, height = 800L, res = 120, palette = "okabe", tie_states = TRUE, timeline = TRUE, absent = c("fade", "away", "hide"), isolates = c("fade", "show", "hide"), ease = c("dwell", "continuous"), max_displacement = 0.08, anchor_strength = 1, layout_args = list(), seed = 42L, ... )
dn |
A temporal network from |
start, end, step, window
|
The measurement grid, as in |
sessions |
How to treat sessions, as in |
layout |
|
measure |
What node size follows. |
tween |
Frames drawn per bin. One positive whole number, |
fps |
Frames per second. One positive number, |
file |
Path to write to, ending in |
loop |
For a GIF: |
width, height
|
Frame size in pixels, |
res |
Resolution passed to |
palette |
Palette specification, as in |
tie_states |
Whether to draw forming, persisting and dissolving ties
differently. |
timeline |
Whether to draw the timeline strip. |
absent |
How a vertex is drawn in a bin where it is not present.
|
isolates |
How a vertex that is present but has no tie in a bin is
drawn. |
ease |
|
max_displacement |
How far a vertex may move between bins under
|
anchor_strength |
How strongly a vertex is pulled back towards its
previous position under |
layout_args |
A named list of further arguments for
|
seed |
Seed for the spring layouts, so |
... |
Passed to |
Frames. Each bin is drawn tween times. Between one bin and the next
the vertices glide to their new positions, a tie that is about to appear
fades in and one that is about to vanish fades out, and a vertex whose
measure changes grows or shrinks. Under ease = "dwell", the default,
the motion follows the smoothstep curve, so each bin holds still before
it starts to change and the bins can be read one by one. Under
ease = "continuous" nothing holds still: positions follow a Catmull-Rom
spline through the bins, so a vertex moving across several bins traces
one smooth path, and fades are linear. tween = 1 gives one frame per
bin with hard cuts. A film that feels episodic usually has a grid whose
bins do not overlap; a sliding window, step smaller than window,
smooths the data itself, since a tie then persists across several bins.
Layouts. Under every layout but "relaxed" a vertex keeps one position
for the whole animation, so the only thing that moves is the ties; those
are the layouts to read structure from. "spring", the default, lays out
the union of every frame once with cograph::layout_spring(), so pairs
that met often sit close. "circle" and "oval" are rings, in vertex
order. "groups" puts each partition on its own ring and needs a network
built with groups = . "relaxed" lays each frame out again, seeded from
the previous one and held near it by max_displacement and
anchor_strength, then smooths every vertex's path with a centred
triangular kernel over one bin each side; clusters can form and dissolve
without vertices jumping, and no vertex moves further than
max_displacement between consecutive bins. Each relaxed frame is framed
by its active vertices: a vertex that is absent or has no tie in the bin
is held at the border of that frame rather than drifting outward under
repulsion, which would shrink the picture. layout_args tunes the spring
layout for both. A data frame with columns name (or node), x and
y fixes the positions yourself.
What is drawn. Tie width follows weight on one scale fixed across the
whole animation, so a tie of the same weight has the same width in a quiet
frame and a busy one. With tie_states = TRUE a tie forming during a
transition is dotted and green, one persisting is solid and grey, and one
dissolving is dashed and vermilion, so the distinction survives without
colour. With measure given, node size follows that measure, again on
one scale across every frame, with the area of the circle proportional to
the measure's position in its range; see the argument for the three forms
it takes.
Absence and idleness. A vertex that is not present in a bin, under
declared vertex activity or observation bounds, is drawn as absent
says: faded in place, parked out of sight at the edge of the layout and
gliding in when it arrives and out when it leaves, or hidden in place. A
network built without vertex activity has every vertex present in every
bin; set_vertex_spells(dn, "ties") declares each vertex present from
its first tie to its last. A vertex that is present but has no tie in a
bin is drawn as isolates says. With timeline = TRUE a strip under the
network shows the grid with a marker at the current time and the key to
the drawing.
Files. The extension of file chooses the encoder: .gif is written
by the gifski package, .mp4 and .webm by the av package. A video needs
even pixel dimensions. Writing the file is the point of the verb, but the
tidy bin table is still what comes back, so the animation can be described
without opening it.
An object of class "dynet_animation": a tidy data frame with one
row per bin and columns bin, frame (the first rendered frame of the
bin), time (the bin's label on the network's time scale),
window_start and window_end (its bounds), nodes (vertices present),
idle (of those, vertices with no tie), ties (ties drawn), forming (ties not active in the previous bin, NA
for the first), dissolving (ties not active in the next bin, NA for
the last), and file (the same on every row). These are counts of what
the picture shows, not the risk-set accounting of events(). The
rendered-frame schedule is available through
as.data.frame(x, what = "frames"). Returned invisibly, since writing
the file is the verb's purpose.
Raises dynet_unknown_format for a file extension other than .gif,
.mp4 or .webm; dynet_needs_gifski or dynet_needs_av when the
encoder that extension needs is not installed; dynet_needs_cograph when
cograph is not; dynet_empty_result when the grid holds no bin that can be
drawn; dynet_unknown_attribute for layout = "groups" on a network
without a partition or a labels naming no vertex attribute;
dynet_missing_column and dynet_unknown_node for a
coordinate table that is incomplete; dynet_unknown_measure for a
measure that is neither a measure centrality_series() offers nor a
numeric vertex attribute, and whatever centrality_series() raises for one it
refuses; dynet_bad_input for a centrality_series() result that is not
node-level or lands on none of the bins, and a warning of class
dynet_partial_measure when it lands on only some; and dynet_bad_input for a non-positive
fps, tween, width, height or res, an odd video size, a loop
that is neither logical nor a positive whole number, or a negative
max_displacement or anchor_strength.
snapshots() for the same grid as a table, plot.dynet() with
type = "snapshots" for it as a static filmstrip, and centrality_series()
for the measures node size can follow.
if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn = dn, end = 8, step = 4, window = 4, tween = 2) frames summary(frames) } if (requireNamespace("av", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) file <- tempfile(fileext = ".mp4") video <- animate(dn = dn, end = 8, step = 4, tween = 2, measure = "degree", file = file) summary(video) }if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn = dn, end = 8, step = 4, window = 4, tween = 2) frames summary(frames) } if (requireNamespace("av", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) file <- tempfile(fileext = ".mp4") video <- animate(dn = dn, end = 8, step = 4, tween = 2, measure = "degree", file = file) summary(video) }
Imports a temporal network held in another R representation, so that every
Dynet verb applies to it. A method is supplied for networkDynamic
objects; the method for dynet is the identity.
as_dynet(x, ...) ## S3 method for class 'dynet' as_dynet(x, ...)as_dynet(x, ...) ## S3 method for class 'dynet' as_dynet(x, ...)
x |
An object representing a temporal network. |
... |
Passed to a class-specific method. |
A dynet() temporal network: an object of class
c("dynet", "netobject", "cograph_network") carrying the tie ledger, the
node table and the construction metadata. The dynet method is the
identity, returning x unchanged, so as_dynet() is safe to call on an
object that is already a temporal network.
as_dynet.networkDynamic(), which imports a networkDynamic
object.
dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) as_dynet(dn) dn <- dynet(school_contacts) same <- as_dynet(dn) samedn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) as_dynet(dn) dn <- dynet(school_contacts) same <- as_dynet(dn) same
Converts the edge-activity spell ledger, observation support, explicit
vertex activity, weights, static vertex attributes, and static edge
attributes of a networkDynamic object. Integer vertex identifiers are
replaced by a complete unique vertex attribute. By default Dynet tries
Name, Label, and then vertex.names, in that order.
## S3 method for class 'networkDynamic' as_dynet( x, name_attribute = NULL, group_attribute = NULL, weight_attribute = "weight", session_attribute = NULL, interval = NULL, active_default = TRUE, import_edge_attributes = TRUE, ... )## S3 method for class 'networkDynamic' as_dynet( x, name_attribute = NULL, group_attribute = NULL, weight_attribute = "weight", session_attribute = NULL, interval = NULL, active_default = TRUE, import_edge_attributes = TRUE, ... )
x |
A |
name_attribute |
Optional vertex attribute holding the public node
names. |
group_attribute |
Optional static vertex attribute used as the cograph grouping variable. |
weight_attribute |
Static edge attribute used as spell weight,
|
session_attribute |
Optional static edge attribute used as the spell
session label. |
interval |
Positive measurement interval. |
active_default |
Whether legacy edges with no explicit activity spell
are active over the observation period, matching the same argument in
|
import_edge_attributes |
Whether to retain compatible static legacy
edge attributes on the raw Dynet spell ledger. |
... |
Ignored. |
Dynamic edge attributes other than activity itself are not part of the
networkDynamic spell-list interface and are therefore not imported.
Ordinary per-edge attributes are repeated onto every imported spell of the
corresponding aggregate edge, matched on the canonical endpoint order
dynet() stores, so an undirected pair carries its own attributes.
A dynet() temporal network: an object of class
c("dynet", "netobject", "cograph_network") whose spell table has one row
per imported edge-activity spell, with the legacy vertex and edge
attributes, observation support, vertex activity and censor flags carried
across. Its metadata additionally carries legacy_source = "networkDynamic", the chosen legacy_name_attribute, the retained
legacy_edge_attributes and any legacy_edge_attribute_renames.
as_dynet(), the generic.
if (requireNamespace("networkDynamic", quietly = TRUE) && requireNamespace("network", quietly = TRUE)) { spells <- data.frame(onset = c(0, 1), terminus = c(2, 3), tail = c(1, 2), head = c(2, 3)) legacy <- networkDynamic::networkDynamic(edge.spells = spells) network::set.vertex.attribute(legacy, "Name", c("A", "B", "C")) dn <- as_dynet(legacy) as.data.frame(dn) }if (requireNamespace("networkDynamic", quietly = TRUE) && requireNamespace("network", quietly = TRUE)) { spells <- data.frame(onset = c(0, 1), terminus = c(2, 3), tail = c(1, 2), head = c(2, 3)) legacy <- networkDynamic::networkDynamic(edge.spells = spells) network::set.vertex.attribute(legacy, "Name", c("A", "B", "C")) dn <- as_dynet(legacy) as.data.frame(dn) }
Tidy tables from a temporal network
## S3 method for class 'dynet' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("edges", "nodes", "bins", "network", "observations", "observed_edges", "vertex_spells"), measure = NULL, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, ... )## S3 method for class 'dynet' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("edges", "nodes", "bins", "network", "observations", "observed_edges", "vertex_spells"), measure = NULL, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, ... )
x |
A temporal network from |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
what |
Which table to return: |
measure |
Optional centrality measures to annotate the vertex table
with, valid only for |
sessions |
How sessions are treated while |
start, end
|
Measurement bounds passed to |
... |
Ignored. |
A plain data.frame, one row per whatever what names.
"edges": one row per unchanged raw spell, with from, to, start,
end, duration and weight. A session column is present when the
network was built with sessions, onset_censored and terminus_censored
when interval censoring was declared explicitly, and any column the
construction carried through – thread for a threaded log, group for
a co-presence log.
"observations": one row per canonical observation component, with
observation, start, end, duration and instant.
"observed_edges": one row per derived observation fragment, with
raw_spell, observation and fragment locating it, from, to,
start, end (clipped to the observation), raw_start, raw_end (as
supplied), weight, instant, the strict
left_observation_censored and right_observation_censored flags, and
duration.
session and the explicit onset_censored/terminus_censored flags are
copied unchanged from the raw spell when the network carries them.
"vertex_spells": one row per maximal declared activity component, with
vertex_spell, node, start, end, duration, instant, session,
onset_censored and terminus_censored. Half-open positive spells and
exact points are both representable. Undeclared vertices are implicitly
always active and receive no synthetic rows, so this table is empty for a
network with no declared vertex activity.
"nodes": one row per vertex, with name, any static attributes
supplied at construction, and one column per measure named in measure.
"bins": one row per measurement window, with bin, lo, hi, time
(the bin's representative time) and closed (whether the upper bound is
included). Bins are component-qualified under discontinuous observation.
"network": one row per aggregate vertex pair, with from, to and the
summed weight cograph renders.
dn <- dynet(school_contacts) spells <- as.data.frame(dn) head(spells) as.data.frame(dn, what = "nodes") as.data.frame(dn, what = "vertex_spells") # Annotate the vertex table so it can be filtered without a second call. busy <- as.data.frame(dn, what = "nodes", measure = c("degree", "indegree", "outdegree")) subset(busy, degree > 15)dn <- dynet(school_contacts) spells <- as.data.frame(dn) head(spells) as.data.frame(dn, what = "nodes") as.data.frame(dn, what = "vertex_spells") # Annotate the vertex table so it can be filtered without a second call. busy <- as.data.frame(dn, what = "nodes", measure = c("degree", "indegree", "outdegree")) subset(busy, degree > 15)
Coerce an animation to a data frame
## S3 method for class 'dynet_animation' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("bins", "frames"), ... )## S3 method for class 'dynet_animation' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("bins", "frames"), ... )
x |
A |
row.names, optional
|
As in |
what |
|
... |
Ignored. |
A plain data.frame.
if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn, step = 6, window = 6, tween = 2) as.data.frame(frames) as.data.frame(frames, what = "frames") }if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn, step = 6, window = 6, tween = 2) as.data.frame(frames) as.data.frame(frames, what = "frames") }
Tidy tables from a collapsed temporal network
## S3 method for class 'dynet_collapsed' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("edges", "nodes"), ... )## S3 method for class 'dynet_collapsed' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("edges", "nodes"), ... )
x |
A network returned by |
row.names, optional
|
Ignored; present for compatibility. |
what |
|
... |
Ignored. |
A plain data.frame. For "edges", one row per collapsed vertex
pair carrying every weighting side by side: from, to, binary,
union_duration, total_duration, duration_fraction, spell_count,
weight_sum, weighted_duration, latest_weight, first, last, and
the activity.duration and activity.count aliases. For "nodes", one
row per vertex with name, any static vertex attributes the network
carries, activity_duration and its activity.duration alias. See
collapse_network() for what each weighting means.
dn <- dynet(school_contacts) collapsed <- collapse_network(dn) as.data.frame(collapsed) as.data.frame(collapsed, what = "nodes")dn <- dynet(school_contacts) collapsed <- collapse_network(dn) as.data.frame(collapsed) as.data.frame(collapsed, what = "nodes")
Stacks the per-session edge tables into one tidy frame with a session
key, so a session-separated collapse reads like every other verb's result.
Without this the base method flattened the list sideways into a single row
of s1.from, s2.from, ... columns.
## S3 method for class 'dynet_collapsed_list' as.data.frame(x, row.names = NULL, optional = FALSE, session = NULL, ...)## S3 method for class 'dynet_collapsed_list' as.data.frame(x, row.names = NULL, optional = FALSE, session = NULL, ...)
x |
A |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
session |
Optional session name. Supply one to get that session's
table alone, without the |
... |
Ignored. |
A plain data.frame, one row per collapsed pair per session, with
session first and then the columns
as.data.frame.dynet_collapsed() returns.
dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 0), end = c(2, 3), session = c("s1", "s2") ), session = "session") by_session <- collapse_network(dn, sessions = "separate") as.data.frame(by_session) as.data.frame(by_session, session = "s1")dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 0), end = c(2, 3), session = c("s1", "s2") ), session = "session") by_session <- collapse_network(dn, sessions = "separate") as.data.frame(by_session) as.data.frame(by_session, session = "s1")
Tidy data frame of a temporal community partition
## S3 method for class 'dynet_communities' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("membership", "sizes", "runs", "events"), ... )## S3 method for class 'dynet_communities' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("membership", "sizes", "runs", "events"), ... )
x |
A |
row.names, optional
|
Ignored, for method consistency. |
what |
|
... |
Ignored. |
A plain data frame.
Tidy tables from an event graph
## S3 method for class 'dynet_event_graph' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("events", "adjacencies"), ... )## S3 method for class 'dynet_event_graph' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("events", "adjacencies"), ... )
x |
An event graph returned by |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
what |
|
... |
Ignored. |
A plain data frame; the columns are listed under event_graph().
dn <- dynet(school_contacts) eg <- event_graph(dn, delta = 1) as.data.frame(eg) as.data.frame(eg, what = "adjacencies")dn <- dynet(school_contacts) eg <- event_graph(dn, delta = 1) as.data.frame(eg) as.data.frame(eg, what = "adjacencies")
Tidy data frame of a temporal measure
## S3 method for class 'dynet_metric' as.data.frame( x, row.names = NULL, optional = FALSE, layout = c("long", "wide"), what = c("values", "diagnostics"), top = NULL, ... )## S3 method for class 'dynet_metric' as.data.frame( x, row.names = NULL, optional = FALSE, layout = c("long", "wide"), what = c("values", "diagnostics"), top = NULL, ... )
x |
A |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
layout |
|
what |
|
top |
Keep only the |
... |
Ignored. |
A plain data.frame. Long layout carries measure and value
with one row per observation, alongside whichever columns say what was
measured: session when the network has sessions, time for anything
measured on a grid of bins, node for a vertex-level quantity, from
and to for a pair-level one, raw_spell for per-spell edge durations
and vertex_spell with implicit for per-spell vertex durations from
durations(), and from_group and to_group for mixing(). A
graph-level series carries time, measure and value alone.
Wide layout puts the identifying columns first and spreads what varies
across the rest. A measure taken on a grid of bins spreads time: one
column per bin, named t followed by the bin's time, leaving one row per
vertex and measure. A measure with no time axis, such as reachability,
spreads the measures instead: one column per measure, leaving one row per
vertex. A measure with no time axis and only one measure is already wide
and comes back unchanged.
dn <- dynet(school_contacts) degree <- centrality_series(dn, measure = "degree") as.data.frame(degree) as.data.frame(degree, layout = "wide") as.data.frame(degree, top = 5) as.data.frame(degree, what = "diagnostics")dn <- dynet(school_contacts) degree <- centrality_series(dn, measure = "degree") as.data.frame(degree) as.data.frame(degree, layout = "wide") as.data.frame(degree, top = 5) as.data.frame(degree, what = "diagnostics")
Tidy data frame of a multislice modularity result
## S3 method for class 'dynet_modularity' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("total", "bins"), ... )## S3 method for class 'dynet_modularity' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("total", "bins"), ... )
x |
A |
row.names, optional
|
Ignored, for method consistency. |
what |
|
... |
Passed on for |
A plain data frame.
Tidy frame of a temporal motif census
## S3 method for class 'dynet_motifs' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'dynet_motifs' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
A |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
... |
Ignored. |
A plain data.frame carrying the same rows and columns as x:
motif, family, pattern and count, preceded by node for a local
census and by session when the census is session-local.
Coerce surrogate networks to a data frame
## S3 method for class 'dynet_null' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'dynet_null' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
A |
row.names |
Passed to the data frame method. |
optional |
Passed to the data frame method. |
... |
Ignored. |
A base data frame, one row per surrogate spell per replicate.
Tidy tables from a temporal path-union network
## S3 method for class 'dynet_path_network' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("edges", "nodes"), ... )## S3 method for class 'dynet_path_network' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("edges", "nodes"), ... )
x |
A network returned by |
row.names, optional
|
Ignored. |
what |
|
... |
Ignored. |
A plain data.frame. For "edges", one row per hop used by an
optimal route, with from, to, weight, first_time, last_time and
n_endpoints. For "nodes", one row per reached vertex, with name,
arrival_time, latency, n_hops, n_paths and groups. See
path_network() for what each column means.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") union_network <- path_network(routes) as.data.frame(union_network, what = "edges") as.data.frame(union_network, what = "nodes")dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") union_network <- path_network(routes) as.data.frame(union_network, what = "edges") as.data.frame(union_network, what = "nodes")
Tidy table of a temporal trajectory tree
## S3 method for class 'dynet_path_trajectories' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'dynet_path_trajectories' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
A result from |
row.names, optional
|
Ignored. |
... |
Ignored. |
A plain data frame with one row per tree node, with the columns
described in path_trajectories() and none of its attributes.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) as.data.frame(trajectories)dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) as.data.frame(trajectories)
Tidy data frame of time-respecting paths
## S3 method for class 'dynet_paths' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("paths", "steps"), ... )## S3 method for class 'dynet_paths' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("paths", "steps"), ... )
x |
A |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
what |
|
... |
Ignored. |
A plain data.frame. For "paths", one row per endpoint vertex,
the source included, with the columns paths() documents: node,
reachable, arrival_time, attained, latency, n_hops and
n_paths, plus path_session and n_best_sessions under
sessions = "bounded", and session and origin under
sessions = "separate". For "steps", one row per step of every
reconstructed optimal route, with endpoint (the vertex the route ends
at), path_id (which of the tied optimal routes to that endpoint),
path_session, step (position along the route, starting at the
source), node (the vertex occupied at that step), time (when it was
reached) and attained.
dn <- dynet(school_contacts) reach <- paths(dn, from = "Ana") as.data.frame(reach) as.data.frame(reach, what = "steps")dn <- dynet(school_contacts) reach <- paths(dn, from = "Ana") as.data.frame(reach) as.data.frame(reach, what = "steps")
Tidy data frame of ranked pathways
## S3 method for class 'dynet_pathways' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("routes", "steps"), ... )## S3 method for class 'dynet_pathways' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("routes", "steps"), ... )
x |
A |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
what |
|
... |
Ignored. |
A plain data.frame with the columns described in pathways(),
most frequent first, or the per-step table when what = "steps".
dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") as.data.frame(routes) as.data.frame(routes, what = "steps")dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") as.data.frame(routes) as.data.frame(routes, what = "steps")
Tidy data frame of detected temporal phases
## S3 method for class 'dynet_phases' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("bins", "profile", "phases"), ... )## S3 method for class 'dynet_phases' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("bins", "profile", "phases"), ... )
x |
A |
row.names, optional
|
Ignored, for method consistency. |
what |
|
... |
Ignored. |
A plain data frame.
Tidy tables from a time-projected network
## S3 method for class 'dynet_projection' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("vertices", "edges"), ... )## S3 method for class 'dynet_projection' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("vertices", "edges"), ... )
x |
A projection returned by |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
what |
|
... |
Ignored. |
A plain data frame. Vertex rows contain state, optional session
and observation, slice, time, start, end, closed, node,
active, and copied node attributes. Edge rows contain from_state,
to_state, from_node, to_node, optional session, from_slice,
to_slice, from_time, to_time, edge_type, weight, n_spells,
and lag.
dn <- dynet(school_contacts) projected <- projection(dn, step = 4, window = 4) as.data.frame(projected) as.data.frame(projected, what = "edges")dn <- dynet(school_contacts) projected <- projection(dn, step = 4, window = 4) as.data.frame(projected) as.data.frame(projected, what = "edges")
Tidy data frame of participation shift counts
## S3 method for class 'dynet_pshifts' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'dynet_pshifts' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
A |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
... |
Ignored. |
A plain data.frame carrying the same rows and columns as x. For
a result built with output = "final" that is one row per shift type –
shift, family and count – preceded by session when the result is
session-local; the thirteen Gibson shift types are always present,
including those with a count of zero. For output = "cumulative" it is
thirteen rows per classified turn, adding sequence, event, time,
speaker, target and group ahead of shift, family and count.
dn <- dynet(school_contacts) shifts <- pshifts(dn) as.data.frame(shifts)dn <- dynet(school_contacts) shifts <- pshifts(dn) as.data.frame(shifts)
Coerce a permutation test to a data frame
## S3 method for class 'dynet_significance' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'dynet_significance' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
A |
row.names |
Passed to the data frame method. |
optional |
Passed to the data frame method. |
... |
Ignored. |
A base data frame, one row per tested cell.
Tidy table of time-bin similarity
## S3 method for class 'dynet_similarity' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'dynet_similarity' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
A result from |
row.names, optional
|
Ignored; present for compatibility with the generic. |
... |
Ignored. |
A plain data.frame, one row per ordered pair of time bins, with
columns time, other, measure and value.
dn <- dynet(school_contacts) resemblance <- similarity(dn, step = 4, window = 4) as.data.frame(resemblance)dn <- dynet(school_contacts) resemblance <- similarity(dn, step = 4, window = 4) as.data.frame(resemblance)
Tidy table of snapshot edges
## S3 method for class 'dynet_snapshot' as.data.frame(x, row.names = NULL, optional = FALSE, ...)## S3 method for class 'dynet_snapshot' as.data.frame(x, row.names = NULL, optional = FALSE, ...)
x |
A |
row.names |
Ignored; present for compatibility with the generic. |
optional |
Ignored; present for compatibility with the generic. |
... |
Ignored. |
A plain data.frame with the same rows and columns.
dn <- dynet(school_contacts) bins <- snapshots(dn) bins_table <- as.data.frame(bins) head(bins_table)dn <- dynet(school_contacts) bins <- snapshots(dn) bins_table <- as.data.frame(bins) head(bins_table)
Tidy data frame of community trajectories
## S3 method for class 'dynet_trajectory' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("node", "time", "global", "allegiance"), ... )## S3 method for class 'dynet_trajectory' as.data.frame( x, row.names = NULL, optional = FALSE, what = c("node", "time", "global", "allegiance"), ... )
x |
A |
row.names, optional
|
Ignored, for method consistency. |
what |
|
... |
Ignored. |
A plain data frame.
Whether a vertex acts in bursts or at a steady pace. Burstiness compares
the spread of the gaps between a vertex's events with their average: it
approaches 1 for increasingly heterogeneous sequences, has theoretical
reference value 0 for a Poisson process, and is -1 for a metronome. The
memory coefficient asks a different question – whether a short gap tends
to be followed by another short gap.
Two vertices can post the same number of times and differ entirely on both.
burstiness( dn, measure = c("burstiness", "memory", "events"), sessions = c("bounded", "collapse", "separate"), plot = FALSE )burstiness( dn, measure = c("burstiness", "memory", "events"), sessions = c("bounded", "collapse", "separate"), plot = FALSE )
dn |
A temporal network from |
measure |
One or more of |
sessions |
How to treat sessions: |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
One raw spell row contributes its start time once to each distinct incident vertex. A self-loop is one event, equal-time rows remain distinct events, direction does not alter incidence, and interval ends and weights are ignored. Sorted equal times therefore create legitimate zero gaps. Explicitly onset-censored limits are not observed onset events and are excluded; terminus censoring does not affect this onset sequence.
If the usable interevent gaps are , burstiness is
where is their mean and
is the population standard
deviation of the equal-mass empirical gap distribution. mean_gap needs at
least one gap. Burstiness needs at least two and is NA if every usable gap
is zero. Its finite-sample range is [-1, 1).
Memory is the ordinary Pearson correlation between consecutive gaps. It
needs at least two adjacent-gap pairs and nonzero variation on both sides;
otherwise it is NA. In sessions = "bounded", primitive gaps and adjacent
pairs are formed within each session and then pooled, so no cross-session
gap is introduced. Collapse includes calendar gaps after erasing labels;
separate returns session-local blocks over the fixed vertex universe.
A dynet_metric at node level with no time column: one row per
vertex and measure. Attributes record the event identity, dispersion,
memory, loop, weight, and session-gap conventions as
event_identity = "incident_spell_start", dispersion = "population",
memory = "lag1_pearson", loop_contribution = "one_event",
weights = "ignored", and mode-specific session_gaps.
Goh, K.-I., & Barabasi, A.-L. (2008). Burstiness and memory in complex systems. Europhysics Letters, 81(4), 48002, equations 1 and 4. doi:10.1209/0295-5075/81/48002
dn <- dynet(school_contacts) burstiness(dn)dn <- dynet(school_contacts) burstiness(dn)
Centrality for every vertex at every time point. Ask for several measures in one call and they arrive stacked in a single tidy frame, one row per vertex, time point and measure.
Each value measures the network as it stands in one time bin, so the
result is a trajectory of ordinary centrality. Order within a bin is not
used: every tie active in the bin counts as present. Centrality computed
from time-respecting paths across the whole period is path_centrality().
centrality_series( dn, measure = "degree", sessions = c("bounded", "collapse", "separate"), sample = NULL, damping = 0.85, mode = c("all", "out", "in"), start = NULL, end = NULL, step = NULL, window = NULL, exponent = 1, prestige = "indegree", rescale = FALSE, lambda = 1, groups = NULL, plot = FALSE )centrality_series( dn, measure = "degree", sessions = c("bounded", "collapse", "separate"), sample = NULL, damping = 0.85, mode = c("all", "out", "in"), start = NULL, end = NULL, step = NULL, window = NULL, exponent = 1, prestige = "indegree", rescale = FALSE, lambda = 1, groups = NULL, plot = FALSE )
dn |
A temporal network from |
measure |
One or more of |
sessions |
How to treat sessions: |
sample |
Deprecated. |
damping |
Damping factor for PageRank; a single number strictly
between zero and one, |
mode |
Which edges count on a directed network: |
start, end
|
First and last time at which to measure. Default to the observed range. A network built from dates may be addressed with dates. |
step |
How often to measure. Defaults to the interval the network was built with. |
window |
How much time each measurement covers. Defaults to |
exponent |
Attenuation factor for Bonacich |
prestige |
Prestige definition, |
rescale |
Whether to divide prestige by its total independently
inside every reported time/session block; |
lambda |
Nonnegative multiplier for |
groups |
For |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
step and window are separate on purpose. step is how often you look;
window is how much of the timeline each look takes in. Setting them equal
partitions the period; setting window larger than step is a rolling
window, which keeps the resolution of the smaller step while smoothing over
the noise of a sparse bin. The arguments match tsna::tSnaStats(), where
they are called time.interval and aggregate.dur.
Snapshot "degree" counts distinct active binary dyads, so duplicate,
split and overlapping spells do not multiply it; mode = "all" on a
directed snapshot is in-degree plus out-degree. "strength" is the same
margin taken over spell weights rather than over binary dyads. In a
positive window each spell contributes its weight in proportion to the
share of its duration that falls inside the window, weight * overlap / duration, so a spell straddling two tiled windows splits its weight
between them and the pieces add back to the whole. A point contact has no
duration to split and contributes its full weight to the window holding
it. With window = 0 every active spell contributes its full weight at
that instant. The share uses the spell's recorded duration, so the part of
a spell outside the observation period is not reassigned to observed
windows. snapshots() and the network plots keep full weights per bin, so
their weight column is not the input to this strength.
Snapshot "closeness" is not Freeman's , which
is
undefined once a snapshot is disconnected – and a time bin almost always
is. It is the reciprocal of the mean geodesic distance to the vertices a
vertex can actually reach: with the reachable nonself set,
which is zero for an isolate and equals Freeman's normalised closeness
on a connected snapshot. "harary" is the
reciprocal of
eccentricity, zero for a vertex that cannot reach everything.
"eigenvector", "hub" and "authority" are certified the way eigenvector
prestige is: a snapshot whose spectral radius is zero (no cycle) or whose
Perron root is repeated (components of equal weight) has no single answer,
and every vertex of that block is NA under a warning of class
dynet_eigen_undefined.
"eigenvector" is uniquely determined when the Perron eigenvalue has a
one-dimensional eigenspace; strong connectivity is a sufficient condition.
Disconnected snapshots with equally dominant components can have more than
one correct eigenvector, so read the result as a within-snapshot ranking
rather than an automatically comparable number across the whole series.
Indegree prestige is the column sum of the directed binary active-dyad
adjacency matrix. It is exactly snapshot degree with mode = "in":
duplicate, split, and overlapping spells and edge weights do not multiply
the result, while an explicitly retained directed loop contributes once.
With rescale = TRUE, the column sums are divided by their block total. A
zero total is mathematically undefined and is returned as literal NaN.
Row-normalised indegree prestige first converts every nonzero binary
adjacency row to sum one; zero rows remain all zero. Its column sums are the
received sender-nomination mass, so their total is the number of active
senders. rescale = TRUE divides again by that block total. This closed-form
transform is the sna::prestige(cmode = "indegree.rownorm") definition on
binary matrices. Dynet deliberately ignores edge weights, whereas sna
uses their magnitudes on valued matrices.
Row-column-normalised prestige uses deterministic Sinkhorn–Knopp scaling
only when the full binary vertex matrix has total support: every active dyad
must belong to a perfect matching. It preserves all binary dyads and does
not remove isolates or unsupported edges. Infeasible blocks return NA for
every vertex with a classed warning. A feasible transform has every incoming
column sum equal to one, so raw prestige is uniformly one and rescaled
prestige uniformly 1 / n; this definition is a transform diagnostic, not
a vertex ranking. Dynet uses fixed-order sweeps, maximum absolute row/column
residual 1e-12, and at most 10,000 sweeps. It never returns a partial
iterate. This deliberately differs from the randomised loose-tolerance
annealer in sna 2.8.
Domain prestige is incoming indegree in the directed reachability graph
after excluding its reflexive diagonal. If H[i,j] records whether i = j
or a directed path from i to j exists, then
p[j] = sum(H[,j]) - 1. Every distinct reaching vertex counts once,
regardless of path length or multiplicity. Loops cannot add self credit,
isolates score zero, and a zero-total rescaling returns literal NaN.
Closure is computed on the binary active snapshot for each reporting block,
not on chronologically ordered temporal journeys through the raw spells.
Domain-proximity prestige additionally uses the shortest incoming hop
distances. For the nonself domain D[j], let r[j] be its size and s[j]
the sum of its finite distances into j. The score is zero when r[j] = 0
and otherwise r[j]^2 / ((n - 1) * s[j]): the incoming domain fraction
divided by mean hop distance. Unreachable vertices are omitted before the
distance sum. This deliberately fixes an arithmetic artefact in sna 2.8,
whose FALSE * Inf operation incorrectly zeros partial nonempty domains.
Eigenvector prestige solves t(B) %*% p = rho * p for the nonnegative
Perron ray of binary adjacency B. It requires positive spectral radius and
a one-dimensional Perron eigenspace. Raw scores have Euclidean norm one;
rescale = TRUE makes their sum one. Zero-radius or nonunique blocks return
all NA with a classed warning and diagnostics. Periodic cycles remain
valid even when negative or complex roots share the spectral radius. Dynet
uses direct eigenvalues plus an SVD nullity/residual check at tolerance
1e-10, orients the ray as nonnegative, and never applies elementwise
absolute value.
Row-normalised eigenvector prestige first forms binary adjacency B and
divides each nonzero sender row by its number of distinct outgoing dyads;
zero rows remain exactly zero. It then solves the certified incoming Perron
equation for the transpose of that row-stochastic matrix. Thus each active
sender distributes one unit of recursive nomination mass, with no
teleportation or dangling-row imputation. Binary session union and retained
loop policy occur before row normalisation. The positive-radius, geometric-
uniqueness, nonnegative-sign, L2/sum-scale, warning, and diagnostic rules
are otherwise exactly those of ordinary eigenvector prestige.
Column-normalised eigenvector prestige divides each nonzero binary receiver column by its number of distinct incoming dyads; zero columns remain zero. It solves the incoming Perron equation only after that transform. If every vertex has positive indegree, the transformed transpose is row-stochastic and every certified score is necessarily uniform. Nonuniform defined scores therefore require a zero-indegree vertex. Binary union and retained-loop policy precede normalisation; certification and scaling remain those above.
Row-column-normalised eigenvector prestige composes the total-support and
deterministic Sinkhorn–Knopp contract with the certified Perron contract.
Infeasible support and nonconvergent balancing terminate before the spectral
solve. A completed doubly stochastic transform always has the all-ones
Perron ray, but reducible transforms have several such rays and remain
undefined. Every fully certified score is therefore exactly uniform:
1 / sqrt(n) raw or 1 / n rescaled. This selector diagnoses support,
balance, and irreducibility; it is not a vertex ranking.
Declared vertex activity induces the eligible vertex population before any
kernel is evaluated. Positive windows independently use any-time vertex and
edge unions before induction, while window = 0 evaluates the exact state.
Results remain rectangular over the fixed vertex universe: inactive
vertices receive typed NA, while eligible isolates keep the centrality
kernel's ordinary static result.
A dynet_metric: a tidy data frame with one row per vertex, time
point and measure. Columns are session (only under
sessions = "separate", which is the only mode that keeps session labels
apart), time, node, measure and value.
Print it, summary() it, plot() it, or take the plain frame with
as.data.frame(). Prestige stores its mathematical choices as direct
attributes for a prestige-only result and as named records under
measure_metadata otherwise. When a prestige
variant is structurally undefined or fails to converge, the affected
values are NA, a warning says how many reporting blocks were affected,
and a record naming the stage and reason for each comes out through
as.data.frame(x, what = "diagnostics").
Errors: dynet_unknown_measure (a measure not listed above),
dynet_needs_directed ("prestige", "hub", "authority",
"indegree" or "outdegree" on an undirected network),
dynet_no_sessions (sessions = "separate" without a session column),
dynet_outside_observation (the requested range misses observed support;
it also carries dynet_bad_input),
and dynet_bad_input for every other broken contract – dn not a
dynet, an unknown mode, an out-of-range damping, exponent,
lambda, prestige, rescale, start, end, step or window, and
rescale = TRUE without measure = "prestige".
Warnings: dynet_deprecated (measure = "indegree"/"outdegree", or the
retired sample argument), dynet_eigen_undefined and
dynet_kernel_singular (both also carrying dynet_measure_undefined) when
a snapshot's eigenvector, hub, authority, Bonacich power or information
kernel has no unique answer, and dynet_prestige_infeasible,
dynet_prestige_nonconvergence and dynet_prestige_eigen_undefined when a
prestige variant is structurally undefined or fails to converge.
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125.
Wasserman, S., & Faust, K. (1994). Social Network Analysis: Methods and Applications. Cambridge University Press, Chapter 5.
Butts, C. T. (2024). sna: Tools for Social Network Analysis, version 2.8. doi:10.32614/CRAN.package.sna.
Lin, N. (1976). Foundations of Social Research. McGraw-Hill.
Freeman, L. C. (1979). Centrality in social networks: conceptual clarification. Social Networks, 1(3), 215-239. doi:10.1016/0378-8733(78)90021-7
Brandes, U. (2001). A faster algorithm for betweenness centrality. Journal of Mathematical Sociology, 25(2), 163-177.
Bonacich, P. (1987). Power and centrality: a family of measures. American Journal of Sociology, 92(5), 1170-1182.
Hage, P., & Harary, F. (1995). Eccentricity and centrality in networks. Social Networks, 17(1), 57-63.
Stephenson, K., & Zelen, M. (1989). Rethinking centrality. Social Networks, 11(1), 1-37.
Goh, K.-I., Kahng, B., & Kim, D. (2001). Universal behavior of load distribution in scale-free networks. Physical Review Letters, 87(27), 278701.
Freeman, L. C., Borgatti, S. P., & White, D. R. (1991). Centrality in valued graphs. Social Networks, 13(2), 141-154.
Page, L., Brin, S., Motwani, R., & Winograd, T. (1999). The PageRank citation ranking: bringing order to the web. Technical Report 1999-66, Stanford InfoLab.
Kleinberg, J. M. (1999). Authoritative sources in a hyperlinked environment. Journal of the ACM, 46(5), 604-632. doi:10.1145/324133.324140.
Seidman, S. B. (1983). Network structure and minimum degree. Social Networks, 5(3), 269-287. doi:10.1016/0378-8733(83)90028-X.
Burt, R. S. (1992). Structural Holes: The Social Structure of Competition. Harvard University Press.
Kundu, S., Murthy, C. A., & Pal, S. K. (2011). A new centrality measure for influence maximization in social networks. In Pattern Recognition and Machine Intelligence, Lecture Notes in Computer Science 6744 (pp. 242-247). Springer. doi:10.1007/978-3-642-21786-9_40.
Bonacich, P. (1972). Factoring and weighting approaches to status scores and clique identification. Journal of Mathematical Sociology, 2, 113-120. doi:10.1080/0022250X.1972.9989806.
Berman, A., & Plemmons, R. J. (1994). Nonnegative Matrices in the Mathematical Sciences. SIAM. doi:10.1137/1.9781611971262.
Sinkhorn, R. (1964). A relationship between arbitrary positive matrices and doubly stochastic matrices. Annals of Mathematical Statistics, 35, 876-879. doi:10.1214/aoms/1177703591.
Sinkhorn, R., & Knopp, P. (1967). Concerning nonnegative matrices and doubly stochastic matrices. Pacific Journal of Mathematics, 21, 343-348. doi:10.2140/pjm.1967.21.343.
Knight, P. A. (2008). The Sinkhorn-Knopp algorithm: convergence and applications. SIAM Journal on Matrix Analysis and Applications, 30, 261-275. doi:10.1137/060659624.
path_centrality() for closeness and betweenness on
time-respecting paths; reachability() for temporal reach.
dn <- dynet(school_contacts) centrality_series(dn, measure = "degree") centrality_series(dn, measure = c("degree", "betweenness")) centrality_series(dn, measure = "prestige", rescale = TRUE) centrality_series(dn, measure = "prestige", prestige = "indegree.rownorm") centrality_series(dn, measure = "prestige", prestige = "domain") centrality_series(dn, measure = "prestige", prestige = "domain.proximity") centrality_series(dn, measure = "prestige", prestige = "eigenvector") centrality_series(dn, measure = "prestige", prestige = "eigenvector.rownorm") centrality_series(dn, measure = "prestige", prestige = "eigenvector.colnorm") centrality_series(dn, measure = "prestige", prestige = "eigenvector.rowcolnorm") # A seven-day window, stepped one day at a time. centrality_series(dn, measure = "degree", step = 1, window = 7) degree <- centrality_series(dn, measure = "degree") summary(degree)dn <- dynet(school_contacts) centrality_series(dn, measure = "degree") centrality_series(dn, measure = c("degree", "betweenness")) centrality_series(dn, measure = "prestige", rescale = TRUE) centrality_series(dn, measure = "prestige", prestige = "indegree.rownorm") centrality_series(dn, measure = "prestige", prestige = "domain") centrality_series(dn, measure = "prestige", prestige = "domain.proximity") centrality_series(dn, measure = "prestige", prestige = "eigenvector") centrality_series(dn, measure = "prestige", prestige = "eigenvector.rownorm") centrality_series(dn, measure = "prestige", prestige = "eigenvector.colnorm") centrality_series(dn, measure = "prestige", prestige = "eigenvector.rowcolnorm") # A seven-day window, stepped one day at a time. centrality_series(dn, measure = "degree", step = 1, window = 7) degree <- centrality_series(dn, measure = "degree") summary(degree)
Restore implicit observation support
clear_observations(dn)clear_observations(dn)
dn |
A temporal network. |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), observed continuously from
its earliest raw start through its latest raw end. Every explicit
observation field is dropped from the metadata and the bin count is
recomputed over the raw range; spells and attributes are untouched. Safe
on a network that never had explicit observations, which is returned with
only its recorded call changed.
dn <- dynet(school_contacts) first_week <- set_observations(dn, start = 0, end = 7) restored <- clear_observations(first_week) restoreddn <- dynet(school_contacts) first_week <- set_observations(dn, start = 0, end = 7) restored <- clear_observations(first_week) restored
Creates a static cograph network from the exact observed, endpoint-valid activity in a requested time range. Every collapsed edge retains all common duration summaries, so choosing one weighting does not discard the others.
collapse_network( dn, start = NULL, end = NULL, weight = c("binary", "union_duration", "total_duration", "duration_fraction", "spell_count", "weight_sum", "weighted_duration", "latest_weight"), sessions = c("bounded", "collapse", "separate"), censored = c("include", "exclude") )collapse_network( dn, start = NULL, end = NULL, weight = c("binary", "union_duration", "total_duration", "duration_fraction", "spell_count", "weight_sum", "weighted_duration", "latest_weight"), sessions = c("bounded", "collapse", "separate"), censored = c("include", "exclude") )
dn |
A temporal network from |
start, end
|
Collapse bounds. Default to the observed range. Positive
intervals are clipped to |
weight |
Edge field used as the cograph weight: |
sessions |
Session handling. |
censored |
Whether raw edge and vertex identities carrying an explicit
censor flag are |
A dynet_collapsed cograph netobject, whose two tidy tables are
reached with as.data.frame(x, what = "edges") and
as.data.frame(x, what = "nodes"). With sessions = "separate", a named
dynet_collapsed_list of such objects, one per session.
The edge table carries one row per collapsed pair and every weighting at
once, so choosing one does not discard the others: from, to,
binary (1 for a pair that was ever active), union_duration (time the
pair was active, overlaps counted once), total_duration (summed spell
lengths, overlaps counted twice), duration_fraction (union_duration
over the pair's joint activity opportunity, NA when that opportunity is
zero), spell_count, weight_sum, weighted_duration (weight times
duration, summed), latest_weight (the weight of the last spell to end),
first and last (the pair's earliest onset and latest terminus), and
the activity.duration and activity.count aliases for compatibility
with networkDynamic::network.collapse().
The node table carries one row per vertex, with name, any static vertex
attributes the network was built with, activity_duration (time the
vertex was active, overlaps counted once) and its activity.duration
alias.
dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 1), end = c(2, 3) )) flat <- collapse_network(dn, weight = "union_duration") as.data.frame(flat)dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 1), end = c(2, 3) )) flat <- collapse_network(dn, weight = "union_duration") as.data.frame(flat)
"Who is with whom" and "did the structure reorganise, and when" are different questions. This one compares each bin's partition with another bin's by a label-invariant statistic, so it says how much changed without needing to know which community became which.
Because every statistic here is invariant to relabelling,
match_communities() is not a prerequisite. That is a common
misunderstanding and it is worth stating plainly: matching is needed to
follow a community, not to measure change.
community_change( x, measure = c("nmi", "ami", "ari", "vi", "split_join", "jaccard", "omega_index", "uami", "iami"), against = c("previous", "first", "all") )community_change( x, measure = c("nmi", "ami", "ari", "vi", "split_join", "jaccard", "omega_index", "uami", "iami"), against = c("previous", "first", "all") )
x |
A |
measure |
One or more of |
against |
|
Every statistic is built from the contingency table of the two labellings,
restricted to the vertices active in both bins. A vertex that merely
goes quiet must not read as a reorganisation, so it is dropped from both
sides rather than counted as having left its community. When fewer than two
vertices are shared, nothing is defined and the value is NA – a real
absence, not a zero.
The first bin has no predecessor, so under against = "previous" its value
is NA by construction.
What each measures, briefly. "nmi" is shared information normalised by
the mean entropy, in , high for agreement; it is not
chance-corrected, so two unrelated partitions score above zero. "ami" is
the same quantity with the chance level subtracted, and is the one to reach
for. "ari" is chance-corrected pair agreement, centred on zero for
unrelated partitions. "vi" is a true metric in nats, zero for agreement
and unbounded above. "split_join" counts the vertices that would have to
move, so it is an integer in . "jaccard" scores agreement
over co-classified pairs. "omega_index" is chance-corrected pair
agreement; for the disjoint partitions this package produces it coincides
exactly with "ari", and it is offered because it is what multinet
reports and because it generalises to overlapping communities.
"uami" and "iami" are Zhong et al.'s (2025) answer to a problem every
temporal network has: the vertex set moves between bins, and ordinary
mutual information assumes it does not. "iami" compares over the
intersection, the vertices both bins have. "uami" compares over the
union, giving each partition one extra virtual community holding the
vertices it does not have, so a vertex arriving or leaving is itself
information about the change rather than something discarded. The two
answer different questions: "iami" asks whether the vertices that stayed
kept their company, "uami" asks whether the whole structure held. Since
every other measure here is already restricted to the shared vertices,
"iami" is numerically identical to "ami" in this package, and is
offered under both names because the literature uses both.
Two notes on the sources, because both cost time to discover. Adjusted
mutual information admits more than one normaliser: this package divides by
the arithmetic mean of the two entropies, matching "nmi" above,
sklearn's default, and the stated intent of Zhong et al.; aricode::AMI
divides by the maximum instead, so the two disagree by convention and
not by error. And the expression printed for the adjusted measures in Zhong
et al. subtracts the chance term once in a denominator that needs it twice,
which scores two identical partitions at 0.949 rather than 1; the corrected
form is used here.
A dynet_metric at graph level. For "previous" and "first",
one row per bin per measure with columns session, time, measure,
value; for "all", one row per ordered pair with an other column
giving the bin compared against, in the same long shape similarity()
uses so both plot with the same code. The n_compared attribute records
how many vertices each comparison could actually use.
Danon, L., Diaz-Guilera, A., Duch, J., & Arenas, A. (2005). Comparing community structure identification. Journal of Statistical Mechanics, P09008.
Hubert, L., & Arabie, P. (1985). Comparing partitions. Journal of Classification, 2, 193-218.
Meila, M. (2007). Comparing clusterings – an information based distance. Journal of Multivariate Analysis, 98(5), 873-895.
van Dongen, S. (2000). Performance criteria for graph clustering and Markov cluster experiments. CWI Technical Report INS-R0012.
Collins, L. M., & Dent, C. W. (1988). Omega: a general formulation of the Rand index of cluster recovery suitable for non-disjoint solutions. Multivariate Behavioral Research, 23(2), 231-242.
Gates, A. J., & Ahn, Y.-Y. (2019). Element-centric clustering comparison unifies overlaps and hierarchy. Scientific Reports, 9, 8574.
Vinh, N. X., Epps, J., & Bailey, J. (2010). Information theoretic measures for clusterings comparison. JMLR, 11, 2837-2854.
Zhong, P., Ba, C., Mondragon, R., & Clegg, R. (2025). Quantifying community evolution in temporal networks. Scientific Reports, 15, 45373.
dn <- dynet(school_contacts) found <- temporal_communities(dn, step = 5, window = 5, seeds = 1:5) community_change(found) community_change(found, measure = c("ami", "ari")) community_change(found, measure = c("iami", "uami"))dn <- dynet(school_contacts) found <- temporal_communities(dn, step = 5, window = 5, seeds = 1:5) community_change(found) community_change(found, measure = c("ami", "ari")) community_change(found, measure = c("iami", "uami"))
Once labels mean the same thing in every bin, the interesting quantities are about vertices rather than communities: how often one changes group, how many groups it has belonged to, how reliably it stays put, how often two vertices are found together, and how tightly a vertex sticks to its own reference group rather than visiting others.
community_trajectory( x, measure = c("flexibility", "promiscuity", "persistence"), reference = NULL )community_trajectory( x, measure = c("flexibility", "promiscuity", "persistence"), reference = NULL )
x |
A |
measure |
One or more of |
reference |
For |
Let be the community of vertex in bin , over
bins.
Flexibility is the share of consecutive bins in which a vertex changed
community, .
Promiscuity is how much of the whole community structure a vertex visited: its distinct-label count minus one, over the global distinct-label count minus one. A vertex that never moves scores zero; one that visits every community scores one. The denominator is global, not per vertex; that is teneto's convention and it is what makes the measure comparable across vertices.
Persistence is the complement of flexibility, and comes at three
granularities: per vertex, per bin, and one number for the whole series.
The per-bin form is NA in the first bin, which has no predecessor.
Allegiance is how
often two vertices were in the same community. It divides by the number of
bins, not by the bins in which both were active – teneto's convention,
replicated here so the numbers agree.
Recruitment is a vertex's mean allegiance to the other members of its
own reference group; integration is its mean allegiance to vertices
outside it.
Why the labels must be matched first. All of these read a change of label as a change of group. If the labels are per-bin arbitrary – which they are whenever the slices were solved independently – then flexibility measures relabelling noise and nothing else. This verb therefore refuses to run on an unmatched partition rather than returning a number that looks fine.
Inactive vertices. A vertex inactive in a bin still carries the label its
identity arc brought it, matching projection()'s waiting convention, so
it is neither dropped nor treated as having left. n_inactive_states
records how many states that covers, so the reader can judge.
A dynet_metric at node level, one row per vertex per measure,
with columns session (only when the network has sessions), node,
measure and value. Every measure here summarises the whole series, so
there is no time column; the per-bin and whole-series forms of
persistence, and the pairwise allegiance table, are reached by
as.data.frame(x, what = ).
Bassett, D. S., Wymbs, N. F., Porter, M. A., Mucha, P. J., Carlson, J. M., & Grafton, S. T. (2011). Dynamic reconfiguration of human brain networks during learning. PNAS, 108(18), 7641-7646.
Papadopoulos, L., Puckett, J. G., Daniels, K. E., & Bassett, D. S. (2016). Evolution of network architecture in a granular material under compression. Physical Review E, 94(3), 032908.
Bassett, D. S., Porter, M. A., Wymbs, N. F., Grafton, S. T., Carlson, J. M., & Mucha, P. J. (2013). Robust detection of dynamic community structure in networks. Chaos, 23(1), 013142.
Bassett, D. S., Yang, M., Wymbs, N. F., & Grafton, S. T. (2015). Learning-induced autonomy of sensorimotor systems. Nature Neuroscience, 18(5), 744-751.
Thompson, W. H., Brantefors, P., & Fransson, P. (2017). From static to temporal network theory. Network Neuroscience, 1(2), 69-99.
dn <- dynet(school_contacts) found <- temporal_communities(dn, step = 5, window = 5, seeds = 1:5) community_trajectory(found) as.data.frame(community_trajectory(found), what = "time")dn <- dynet(school_contacts) found <- temporal_communities(dn, step = 5, window = 5, seeds = 1:5) community_trajectory(found) as.data.frame(community_trajectory(found), what = "time")
Pair unit returns one row per vertex pair and measure, summarising every retained raw spell they shared. Spell unit returns each retained raw edge identity. Vertex-activity unit returns fixed-universe vertex summaries; vertex-spell unit returns canonical vertex-activity components. Duration is what separates an interval network from a contact network: a pair that met fifty times briefly and a pair that met once at length have the same edge weight in a static network and nothing else in common.
durations( dn, measure = c("events", "total", "mean"), sessions = c("bounded", "collapse", "separate"), censored = c("include", "exclude"), unit = c("pair", "spell", "vertex_activity", "vertex_spell", "node_ties"), mode = c("out", "in", "all"), plot = FALSE )durations( dn, measure = c("events", "total", "mean"), sessions = c("bounded", "collapse", "separate"), censored = c("include", "exclude"), unit = c("pair", "spell", "vertex_activity", "vertex_spell", "node_ties"), mode = c("out", "in", "all"), plot = FALSE )
dn |
A temporal network from |
measure |
For pair unit, one or more of |
sessions |
How to treat sessions: |
censored |
Whether to |
unit |
|
mode |
For |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
Positive spell duration is the observed time during which both endpoints
are eligible. Genuine eligible point contacts are retained with duration
zero. Pair total sums these raw-spell durations, so overlapping identities
intentionally multiply time; pair union counts binary calendar occupancy
once. Consequently union <= total, and union cannot exceed the pair's
eligible opportunity time. Pair events counts retained raw identities.
Formally, if retained raw spell i has endpoint-valid fragments
F[i], then duration[i] = sum((b - a) for [a,b) in F[i]).
For pair p, total[p] = sum(duration[i]), while
union[p] is the Lebesgue measure of the calendar union of every positive
fragment belonging to p. Point contacts therefore count as events and
spells but contribute zero duration. These conventions follow the spell and
dyad distinction in tsna::edgeDuration() (Butts, 2024,
doi:10.32614/CRAN.package.tsna), with Dynet additionally applying its
observation and endpoint-eligibility contract.
Collapse erases edge and vertex session labels before gating. Bounded gates within each session and then pools spell identities while unioning overlapping pair occupancy once on the shared calendar. Separate returns local blocks. Weights and vertex censor flags do not affect durations. Excluding raw edge censoring removes the whole identity, never only an observed fragment.
For a retained canonical vertex component k, let S[k] be its observed
support, d[k] its total positive width, and f[k] and l[k] its extrema.
Vertex total is sum(d[k]), while union is the measure of the calendar
union of every positive S[k]; therefore 0 <= union <= total. Points count
as identities with zero duration. A declared vertex with no retained support
has zero events/total/union and missing mean/median/first/last. A wholly
undeclared vertex has one measurement-only implicit always-active identity
over observed support. This is stream-graph node presence duration as in
Latapy, Viard, and Magnien (2018), doi:10.1007/s13278-018-0537-7, and agrees
with tsna::vertexDuration() only under matching continuous-observation,
non-session conventions.
Node-ties uses the same endpoint-valid raw spell supports as pair/spell
duration. Directed out credits the tail, in credits the head, and all adds
both endpoint stubs. A retained loop therefore contributes once to out,
once to in, and twice to additive all-mode events/total; undirected results
use the same two-stub rule. In contrast, node-tie union Boolean-unions all
positive incident fragments, so loops, reciprocal overlap, duplicate rows,
and simultaneous neighbours occupy calendar time only once. Consequently
union <= total, and directed all equals out plus in only for events and
total. Formally, for endpoint-stub multiplicity c[v,i,m], retained raw
identity duration d[i], and positive support F[i], node-tie events are
sum(c[v,i,m]), total is sum(c[v,i,m] * d[i]), and union is the measure
of the calendar union of all F[i] having positive multiplicity. These
union values cannot exceed the corresponding eligible vertex-activity
union. Isolates, inactive vertices, and loopless singletons receive exact
zeros for every node-tie measure. The additive quantities match
tsna::tiedDuration() only for continuous observation, static eligible
endpoints, uncensored matched spells, and no sessions; tsna is not an
oracle for union, gaps/points, endpoint schedules, source censor filtering,
or session policies (Bender-deMoll and Morris, 2025,
doi:10.32614/CRAN.package.tsna).
A dynet_metric. Pair and edge-spell units are edge-level: pair has
columns from, to, measure, and value, while spell additionally has
raw_spell.
Administrative observation and endpoint-activity fragments are recombined
by raw spell before the censored policy is applied. Vertex-activity unit
has node, measure, and value; vertex-spell additionally has
vertex_spell and implicit; both vertex units are node-level metrics.
Node-ties is also node-level and has the fixed schema node, measure,
and value (plus session only for separate mode).
dn <- dynet(school_contacts) durations(dn) durations(dn, measure = "union") durations(dn, unit = "spell", measure = "duration") durations(dn, unit = "vertex_activity") durations(dn, unit = "vertex_spell") durations(dn, unit = "node_ties", mode = "all") tie_durations <- durations(dn) summary(tie_durations, by = "measure")dn <- dynet(school_contacts) durations(dn) durations(dn, measure = "union") durations(dn, unit = "spell", measure = "duration") durations(dn, unit = "vertex_activity") durations(dn, unit = "vertex_spell") durations(dn, unit = "node_ties", mode = "all") tie_durations <- durations(dn) summary(tie_durations, by = "measure")
Builds a temporal network from a relational log. One constructor covers the four shapes relational data actually arrives in, and the shape is inferred from the arguments you name:
Each row is an edge active over [start, end). Used when
the data carry an end time or a duration.
Each row is an instantaneous event with a time and no duration – a message, a click, a citation.
Forum, chat or email data. An edge is treated as active
from its own post until the last post in the same thread, following
Saqr and Nouri (2020). Name the thread argument to select this.
Two-mode attendance data. Actors sharing a group become
connected for the span of that group. Name actor and group.
A log of who acted when, with no recipient, such as coded
chat or collaboration actions. Within each group, every action takes
up the one before it: a directed contact from the previous actor to
the current one, at the current action's time. Name actor, time
and (optionally) group, and set format = "turns".
The same kind of log, read as every action being
addressed to the whole group: a directed contact from the actor to
every other member of the group (every actor with an action in it), at
the action's time. Name actor, group and time, and set
format = "broadcast". event_graph(events = "messages") then treats
each action as one event.
Every other column of data is kept as a tie attribute: it appears in
as.data.frame() and can be selected on with induce_subgraph(ties = ).
The exceptions are the canonical spell fields themselves – duration,
weight, session, thread, onset_censored and terminus_censored –
which are dropped even when they were never named as arguments, because the
spell table owns those names. A non-atomic column raises
dynet_bad_tie_attribute, and a factor is carried as character.
Co-presence logs keep none, because their rows are memberships rather than
ties.
Column names are resolved case-insensitively from a table of aliases, so
Sender/Receiver, source/target and onset/terminus are all
understood without being spelled out. Times may be numeric, Date,
POSIXct or character date-time strings; character and date-time input is
converted to elapsed time since the first event in a readable unit.
Vertices are addressed by name everywhere in this package. Integer vertex indices are used internally for speed but are never part of any result.
Explicit observation bounds are administrative measurement limits, not a
destructive filter. A positive spell [s,e) contributes the half-open
intersection [max(s,L),min(e,U)) when it has positive duration, while a
genuine instantaneous event is retained at either L or U. Original
endpoints remain the only formation and dissolution events, censoring is
never inferred from equality with a limit, and temporal paths must both
start and finish inside the declared interval.
Vertex activity is a separate declaration. A vertex with at least one row
in vertex_spells is eligible only on the union of those half-open positive
spells and exact points; a vertex with no row remains eligible at all times.
Snapshot measurements independently union eligible vertices and active
edges over each positive window and then induce on eligible endpoints;
point snapshots evaluate both at the exact time. Explicit vertex censor
flags describe raw outer-boundary state and are never inferred from
observation limits or used to alter eligibility.
dynet( data, from = NULL, to = NULL, start = NULL, end = NULL, duration = NULL, time = NULL, thread = NULL, actor = NULL, group = NULL, session = NULL, weight = NULL, nodes = NULL, groups = NULL, format = c("auto", "interval", "contact", "threaded", "copresence", "turns", "broadcast"), thread_clock = c("absolute", "relative"), directed = TRUE, interval = 1, time_unit = "auto", observation_start = NULL, observation_end = NULL, observation_spells = NULL, loops = FALSE, min_thread_posts = 1L, onset_censored = NULL, terminus_censored = NULL, vertex_spells = NULL )dynet( data, from = NULL, to = NULL, start = NULL, end = NULL, duration = NULL, time = NULL, thread = NULL, actor = NULL, group = NULL, session = NULL, weight = NULL, nodes = NULL, groups = NULL, format = c("auto", "interval", "contact", "threaded", "copresence", "turns", "broadcast"), thread_clock = c("absolute", "relative"), directed = TRUE, interval = 1, time_unit = "auto", observation_start = NULL, observation_end = NULL, observation_spells = NULL, loops = FALSE, min_thread_posts = 1L, onset_censored = NULL, terminus_censored = NULL, vertex_spells = NULL )
data |
Data frame holding one relational event per row. |
from, to
|
Column names for the source and target vertex. Auto-detected
from |
start, end
|
Column names for the start and end of an edge spell.
Auto-detected from |
duration |
Column name for a spell duration, used in place of |
time |
Column name for an event time, used in place of |
thread |
Column name identifying a conversation thread. Naming it selects the threaded format. |
actor, group
|
Column names for the actor and the shared group. Naming
both selects the co-presence format, unless |
session |
Column name for a session or period grouping. Sessions act as walls that time-respecting paths do not cross. |
weight |
Column name for event multiplicity. |
nodes |
Optional data frame of vertex attributes. The vertex key is
auto-detected ( |
groups |
Name of a column in |
format |
One of |
thread_clock |
For a threaded log, |
directed |
Whether edges are directed, |
interval |
Width of one time bin, in the network's time unit. Defaults
to |
time_unit |
Unit for converting |
observation_start, observation_end
|
Optional bounds of the continuous
observation interval. Supply numeric values in the network's internal
time scale, or |
observation_spells |
Optional data frame with exactly two columns,
|
loops |
Whether to keep self-loops. |
min_thread_posts |
For a threaded log, the smallest number of posts a
thread must hold, after self-loops have been dropped, for its posts to
enter the network. The default |
onset_censored, terminus_censored
|
Optional logical column names for explicit raw interval-boundary censor state. These selectors are available only for interval input, are never auto-detected, and may not flag a zero-duration point. |
vertex_spells |
Optional tidy vertex-activity table: one row per
period in which a vertex is present, with a node column ( |
An object of class c("dynet", "netobject", "cograph_network").
It is a cograph network, so cograph::splot() draws it directly and
every cograph rendering argument applies. Use as.data.frame() for the
tidy spell table, as.data.frame(x, what = "nodes") for the vertex
table, as.data.frame(x, what = "network") for the aggregate edge list,
summary() for the description and plot() for a picture. Nothing in
this package requires you to reach into the object.
Saqr, M., & Nouri, J. (2020). High resolution temporal network analysis to understand and improve collaborative learning. Proceedings of the Tenth International Conference on Learning Analytics & Knowledge, 314-319.
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125.
Butts, C. T. (2008). network: a package for managing relational data in R. Journal of Statistical Software, 24(2), 1-36.
# An interval log: each row carries its own start and end dynet(school_contacts) # A threaded log: edge stays active until its thread falls silent dynet(forum_posts, thread = "thread") # A co-presence log: actors sharing a group become connected dynet(seminar_attendance, actor = "student", group = "seminar") # A turns log: each action takes up the one before it in its group chat <- data.frame( student = c("Ana", "Ben", "Ana", "Cy", "Dee", "Eli"), team = c("t1", "t1", "t1", "t1", "t2", "t2"), minute = c(1, 2, 4, 5, 1, 3), action = c("plan", "monitor", "discuss", "plan", "plan", "adapt") ) dynet(chat, actor = "student", group = "team", time = "minute", format = "turns") # The same log as broadcasts: each action reaches the whole team dynet(chat, actor = "student", group = "team", time = "minute", format = "broadcast") # Declare observation time without rewriting the source spell. bounded <- dynet(data.frame( from = "A", to = "B", start = -2, end = 8 ), observation_start = 0, observation_end = 5) as.data.frame(bounded) # Declare changing vertex eligibility without altering edge spells. scheduled <- dynet(data.frame( from = "A", to = "B", start = 0, end = 10 ), vertex_spells = data.frame( node = c("A", "A"), start = c(0, 7), end = c(4, 10) )) as.data.frame(scheduled, what = "vertex_spells")# An interval log: each row carries its own start and end dynet(school_contacts) # A threaded log: edge stays active until its thread falls silent dynet(forum_posts, thread = "thread") # A co-presence log: actors sharing a group become connected dynet(seminar_attendance, actor = "student", group = "seminar") # A turns log: each action takes up the one before it in its group chat <- data.frame( student = c("Ana", "Ben", "Ana", "Cy", "Dee", "Eli"), team = c("t1", "t1", "t1", "t1", "t2", "t2"), minute = c(1, 2, 4, 5, 1, 3), action = c("plan", "monitor", "discuss", "plan", "plan", "adapt") ) dynet(chat, actor = "student", group = "team", time = "minute", format = "turns") # The same log as broadcasts: each action reaches the whole team dynet(chat, actor = "student", group = "team", time = "minute", format = "broadcast") # Declare observation time without rewriting the source spell. bounded <- dynet(data.frame( from = "A", to = "B", start = -2, end = 8 ), observation_start = 0, observation_end = 5) as.data.frame(bounded) # Declare changing vertex eligibility without altering edge spells. scheduled <- dynet(data.frame( from = "A", to = "B", start = 0, end = 10 ), vertex_spells = data.frame( node = c("A", "A"), start = c(0, 7), end = c(4, 10) )) as.data.frame(scheduled, what = "vertex_spells")
Credits every optimal time-respecting journey to the contacts that carried it, giving a betweenness score per contact rather than per vertex. This is the measure intervention questions actually ask: not which people matter, but which meetings did.
The row unit is one canonical contact, not one pair. The same A -> B pair
active in two disjoint spells is two rows, because a journey uses one of
them and not the other.
edge_centrality( dn, measure = "betweenness", criterion = c("foremost_then_shortest", "min_hops", "foremost", "fastest"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0 )edge_centrality( dn, measure = "betweenness", criterion = c("foremost_then_shortest", "min_hops", "foremost", "fastest"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0 )
dn |
A temporal network from |
measure |
Currently |
criterion |
Which optimisation problem the credited journeys solve, as
in |
sessions |
Session aggregation policy. |
start, end
|
First and last time to search. |
traversal_time |
Nonnegative duration charged for every hop. |
The score is not normalised, and its range is the same [0, (n-1)(n-2)] as
the vertex measure.
An exact identity ties this to path_centrality(): because every optimal
journey is vertex-simple, it enters each vertex through exactly one contact,
so the scores of the contacts arriving at a vertex sum to that vertex's
temporal betweenness plus the number of sources that reach it. The tests
assert it, which makes this verb checkable without any external reference.
A dynet_metric at edge level, one row per contact, with columns
from, to, start, end, measure and value. A contact used by no
optimal journey is present with value zero rather than dropped, so the
result is a complete census.
Oettershagen, L., and Mutzel, P. (2022). TGLib: an open-source library for temporal graph analysis. ICDM Workshops. arXiv:2209.12587.
Brandes, U. (2001). A faster algorithm for betweenness centrality. Journal of Mathematical Sociology, 25(2), 163-177.
path_centrality() for the vertex form.
dn <- dynet(school_contacts) edge_centrality(dn)dn <- dynet(school_contacts) edge_centrality(dn)
Builds the event graph: every spell becomes a vertex, and an arc joins two
events that share a vertex and follow one another in time there. A
directed path in it is a time-respecting chain of events, so questions
about temporal reachability, waiting times and cascades become questions
about an ordinary static directed acyclic graph. It needs no time grid.
The time-expanded graph of projection() is the other static
representation; it has one vertex per vertex and time slice instead.
event_graph( dn, sessions = c("bounded", "collapse", "separate"), delta = Inf, adjacency = c("all", "next"), direction = c("respect", "ignore"), loops = FALSE, events = c("ties", "messages") )event_graph( dn, sessions = c("bounded", "collapse", "separate"), delta = Inf, adjacency = c("all", "next"), direction = c("respect", "ignore"), loops = FALSE, events = c("ties", "messages") )
dn |
A temporal network from |
sessions |
How to treat sessions: |
delta |
The longest admissible wait at the shared vertex, in the
network's time unit: an arc requires the later event to start no more
than |
adjacency |
|
direction |
On a directed network, |
loops |
Whether self-loop spells take part. |
events |
|
Events are ordered by start, then end, from and to, and numbered in
that order. Event e1 precedes e2 at a shared vertex v when e2
starts strictly after e1 ends, and no more than delta later. An event
therefore occupies its whole spell, as in the event graphs of Kivela et
al. (2018): something carried by it is available at its endpoints once it
has ended. Because the later event must start strictly after, two
instantaneous events at the same instant are never adjacent, nor is an
event that starts at the very instant another ends; every wait is
positive, the graph is acyclic, and numbering events by start is a
topological order. This is the rule of Reticula (Badie-Modiri and
Kivela, 2023), the reference implementation of the construct.
Two events that share both endpoints are joined once per shared vertex,
so they produce two adjacency rows with different via. This is correct:
each row is a different way the second event can follow the first.
With adjacency = "all" every arc is an admissible hop and every chain of
admissible hops is a path, so a path from an event leaving u to an event
arriving at w exists exactly when w can be reached from u through
events taken one after another. On contact data (instantaneous events),
that is the reachability of paths() with a traversal_time shorter than
the smallest gap between distinct contact times. It is not the
reachability of paths() on interval spells, which may be entered at any
instant while they are active rather than only as a whole. "next"
keeps the earliest successors only, the consecutive-event adjacency of
Kovanen et al. (2011) used for temporal motifs. It is much sparser, and
with delta = Inf on an undirected network of instantaneous contacts it
reaches the same events as "all". Otherwise it can lose reachability: on
a directed network the earliest departure from a vertex need not lead
where a later one does, and with interval spells the earliest successor
can end after a later one has started.
No R package computes this construct, so it is checked against Dynet's own path search and against direct enumeration of event pairs.
An object of class dynet_event_graph. Take its tables with
as.data.frame(x) for the events, one row per spell (or per message),
with event, session (under sessions = "separate"), from, to
(for a message, its targets joined by commas), n_targets (messages
only), start, end, duration, weight, spell (the spell the
event came from, or a message's first spell, numbered in the order
dynet() built them) and the tie attributes of that spell, except one
whose name is already among these columns; and as.data.frame(x, what = "adjacencies") for the
arcs, one row per pair of events and shared vertex, with from_event,
to_event (the numbers of the two events in the event table), via
(the shared vertex), first and second (the two events written as
from->to, or from--to on an undirected network; a message to
several targets as from->9 targets), from_time (when
the earlier event ends), to_time (when the later one starts), wait,
under sessions = "separate" session, and each tie attribute of the
two events as first_<name> and second_<name>. summary() gives one row per event
and plot() draws the graph.
Errors: dynet_bad_input for a dn that is not a dynet, a delta that
is not one non-negative number, or a loops that is not one logical
value; dynet_no_sessions for sessions = "separate" without a session
column; dynet_empty_network when no spell remains once self-loops are
dropped.
Kivela, M., Cambe, J., Saramaki, J., & Karsai, M. (2018). Mapping temporal- network percolation to weighted, static event graphs. Scientific Reports, 8, 12357. doi:10.1038/s41598-018-29577-2
Mellor, A. (2018). The temporal event graph. Journal of Complex Networks, 6(4), 639-659. doi:10.1093/comnet/cnx048
Badie-Modiri, A., & Kivela, M. (2023). Reticula: A temporal network and hypergraph analysis software package. SoftwareX, 21, 101301. doi:10.1016/j.softx.2022.101301
Kovanen, L., Karsai, M., Kaski, K., Kertesz, J., & Saramaki, J. (2011). Temporal motifs in time-dependent networks. Journal of Statistical Mechanics: Theory and Experiment, P11005. doi:10.1088/1742-5468/2011/11/P11005
projection() for the time-expanded graph, paths() for
time-respecting paths between vertices.
dn <- dynet(school_contacts) eg <- event_graph(dn, delta = 1) eg as.data.frame(eg, what = "adjacencies") summary(eg) # Only the earliest successors, as for temporal motifs sparse <- event_graph(dn, delta = 1, adjacency = "next") sparsedn <- dynet(school_contacts) eg <- event_graph(dn, delta = 1) eg as.data.frame(eg, what = "adjacencies") summary(eg) # Only the earliest successors, as for temporal motifs sparse <- event_graph(dn, delta = 1, adjacency = "next") sparse
When relationships are born and when they die. In a static network every edge is present at once; here the turnover itself is the finding. A course typically shows formation front-loaded and dissolution piling up at the end, and a group that never dissolves an edge is behaving differently from one that constantly re-forms them.
events( dn, measure = c("formation", "dissolution"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )events( dn, measure = c("formation", "dissolution"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )
dn |
A temporal network from |
measure |
One or more of |
sessions |
How to treat sessions: |
start, end
|
First and last time at which to measure. Default to the observed range. A network built from dates may be addressed with dates. |
step |
How often to measure. Defaults to the interval the network was built with. |
window |
How much time each measurement covers. Defaults to |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
Formation and dissolution are counted inside each window, so overlapping
windows (window > step) count the same event more than once by design –
that is what a rolling total is. Setting window equal to step, the
default, gives disjoint counts that sum to the total turnover.
Explicitly onset-censored raw limits are not formations, and explicitly
terminus-censored limits are not dissolutions. A left-censored observed tie
is prior evidence for new_pairs; raw censor state never changes activity.
Formation fraction is defined only with window = 0. For a positive
half-open interval [s,e), its pre-batch state at t is
s < t <= e and its post-batch state is s <= t < e. These predicates are
binary-unioned per nonloop ordered pair or undirected dyad after the entire
timestamp batch. Points are absent on both sides. A pair enters risk only
when observation and both endpoints are eligible immediately before and
after t and the pair is inactive before. A formation is confirmed when it
is active after and at least one contributing positive raw spell has a known
onset at t. The ratio is in [0,1]; zero risk returns NA.
Duplicate, overlapping, or adjacent raw spells cannot multiply pair-state transitions. Observation and vertex boundaries are excluded by two-sided eligibility. Onset censoring suppresses confirmation but not state; terminus censoring, weights, loops, and point contacts do not contribute. Collapse erases labels, bounded authorises within sessions before unioning each calendar pair, and separate returns session-local fractions.
Dissolution fraction is the dual exact-time quantity. For each nonloop pair,
let E- and E+ be binary-union state on the symbolic one-sided limits and
let L mean at least one positive raw spell ends exactly at the timestamp
with a known terminus. The numerator is Z * E- * (1 - E+) * L, where Z
requires two-sided observation and endpoint eligibility; the denominator is
sum(Z * E-), including pairs that remain active. Zero risk returns
NA_real_, while positive risk with no confirmed dissolution returns zero.
Censor flags do not change state: one known terminus confirms a disappearance
but an all-censored disappearance is unconfirmed. Duplicate, overlapping,
adjacent, and tied rows are unioned; points, loops, weights, onset censoring,
and administrative observation/activity boundaries do not create transitions.
Collapse erases labels, bounded unions authorised session-local states, and
separate reports local rows. Positive windows are rejected because
"dissolution_rate" owns
dissolution rates.
Dissolution rate is the active-risk dual over a positive window. Its
numerator sums confirmed binary pair dissolutions at included timestamp
batches; its denominator integrates exact eligible active nonloop pair-time
over observation, vertex, edge, and window change cells. Right-censored
termini retain state and exposure but do not confirm an event, while one
known duplicate suffices. Zero active exposure returns NA_real_; positive
exposure without a confirmed dissolution is zero. The unit is inverse
network time. It is not raw terminus intensity, spell-duration sum, or an
average of instantaneous fractions; positive windows are required, and this
is the rate "dissolution_rate" reports.
Formation rate is the positive-window counterpart. Its numerator sums the
confirmed binary pair formations at each included timestamp, while its
denominator integrates exact inactive eligible nonloop pair-time over
change-point cells cut by the window, observation components, vertex
activity, and edge state. It is not an average of instantaneous fractions,
a raw-onset intensity, or an ever-observed-pair quantity. Zero exposure
returns NA_real_; positive exposure with no confirmed formation returns
zero. The unit is inverse network time and scales inversely with positive
time scaling. Points have zero exposure, onset censoring suppresses only
confirmation, and gap/boundary, duplicate, overlap, adjacency, loop,
weight, and session rules follow the same ledger as
"formation_fraction". window = 0 is rejected because that measure owns
the instantaneous fractions.
A dynet_metric at graph level, one row per time point and
measure.
Andersen, P. K., & Gill, R. D. (1982). Cox's regression model for counting processes: a large sample study. Annals of Statistics, 10, 1100-1120. doi:10.1214/aos/1176345976
Butts, C. T., Leslie-Cook, A., Krivitsky, P. N., & Bender-deMoll, S. (2024). networkDynamic: Dynamic Extensions for Network Objects, version 0.11.5. doi:10.32614/CRAN.package.networkDynamic
dn <- dynet(school_contacts) events(dn) turnover <- events(dn, measure = c("formation", "dissolution")) plot(turnover) events(dn, measure = "formation_fraction", start = 1, end = 1, window = 0) events(dn, measure = "dissolution_fraction", start = 1, end = 1, window = 0) events(dn, measure = "formation_rate", start = 1, end = 2, window = 1) events(dn, measure = "dissolution_rate", start = 1, end = 2, window = 1)dn <- dynet(school_contacts) events(dn) turnover <- events(dn, measure = c("formation", "dissolution")) plot(turnover) events(dn, measure = "formation_fraction", start = 1, end = 1, window = 0) events(dn, measure = "dissolution_fraction", start = 1, end = 1, window = 0) events(dn, measure = "formation_rate", start = 1, end = 2, window = 1) events(dn, measure = "dissolution_rate", start = 1, end = 2, window = 1)
Vertex attributes for forum_posts. Passed to dynet() through its nodes
argument, these become available to mixing().
forum_peopleforum_people
A data.frame with 20 rows and 3 columns:
Character. Matches the sender and receiver names in forum_posts.
Character. "Student" (16), "Teacher" (3) or
"Facilitator" (1).
Character. "High", "Middle" or "Low" for
students; NA for the four staff.
Simulated, not observed. Generated deterministically under a fixed
seed by data-raw/make-data.R.
dn <- dynet(forum_posts, thread = "thread", nodes = forum_people) mixing(dn, attribute = "role")dn <- dynet(forum_posts, thread = "thread", nodes = forum_people) mixing(dn, attribute = "role")
Posts in a course discussion forum over roughly eight weeks. Each row is one
post directed at an earlier poster in the same thread. Because a post has a
timestamp but no end, the duration of the tie has to be derived: dynet()
treats a post as active until the last post in its thread, so a message that
provoked a long argument stays live longer than one that fell flat.
forum_postsforum_posts
A data.frame with 241 rows and 4 columns:
Character. Who wrote the post.
Character. Who the post replied to.
POSIXct (UTC). When the post was made. Posts run from
2024-09-02 to 2024-10-27, just under eight weeks.
Character. The discussion thread it belongs to; 62 threads.
Pairs with forum_people, which carries the roles used for mixing analysis.
Simulated, not observed. Generated deterministically under a fixed
seed by data-raw/make-data.R.
dynet(forum_posts, thread = "thread", nodes = forum_people)dynet(forum_posts, thread = "thread", nodes = forum_people)
Every interval between one event and the next, as a tidy table with one row
per gap. burstiness() computes these gaps to reach B and M and then
reports only their mean; this returns the distribution itself, so it can be
plotted, fitted, or tested against a Poisson null.
gaps( dn, unit = c("node", "pair"), sessions = c("bounded", "collapse", "separate"), censored = c("exclude", "include"), plot = FALSE )gaps( dn, unit = c("node", "pair"), sessions = c("bounded", "collapse", "separate"), censored = c("exclude", "include"), plot = FALSE )
dn |
A temporal network from |
unit |
|
sessions |
How to treat sessions, as in |
censored |
|
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
A gap of exactly zero is legitimate and is kept: it is the signature of two
distinct raw spells incident to the same vertex at one instant. Dropping
those would silently raise the mean above the "mean_gap" that
burstiness() reports from the same numbers.
A dynet_metric data frame with one row per gap and no
aggregation, level = "node" under unit = "node" and level = "edge"
under unit = "pair". Columns are time, the onset of the later event of
the pair, so the row sits at the moment the gap closed; node, or from
and to under unit = "pair"; index, the 1-based rank of the gap
within that vertex's or pair's ordered sequence, restarting at one in each
session under "bounded" and "separate"; measure, the constant
"gap"; and value, the gap in the network's time unit. A leading
session column is present under sessions = "separate". A vertex or
pair with fewer than two usable events contributes no rows rather than a
missing one. Print it, summary() it, plot() it, or take the plain
frame with as.data.frame().
Errors: dynet_bad_input when dn is not a dynet, and
dynet_no_sessions when sessions = "separate" is asked of a network with
no session column. An unmatched unit, sessions or censored is
rejected by match.arg() and is a plain error, not a classed one.
Goh, K.-I., & Barabasi, A.-L. (2008). Burstiness and memory in complex systems. Europhysics Letters, 81(4), 48002. doi:10.1209/0295-5075/81/48002
Karsai, M., Kaski, K., Barabasi, A.-L., & Kertesz, J. (2012). Universal features of correlated bursty behaviour. Scientific Reports, 2, 397. doi:10.1038/srep00397
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125. doi:10.1016/j.physrep.2012.03.001
burstiness() for the summary statistics of these gaps, and
durations() for how long a tie was up rather than how long a pair was
quiet.
dn <- dynet(school_contacts) quiet <- gaps(dn) quiet summary(quiet) dyads <- gaps(dn, unit = "pair") dyadsdn <- dynet(school_contacts) quiet <- gaps(dn) quiet summary(quiet) dyads <- gaps(dn, unit = "pair") dyads
Truncates the rows without rewriting what the measure is. The printed
header still describes the series the rows came from, and a first n of N rows line records the truncation.
## S3 method for class 'dynet_metric' head(x, n = 6L, ...)## S3 method for class 'dynet_metric' head(x, n = 6L, ...)
x |
A |
n |
Number of rows to keep. Defaults to six. |
... |
Passed to the default method. |
A dynet_metric with at most n rows, carrying the source counts
so its header stays true to the series.
dn <- dynet(school_contacts) degree <- centrality_series(dn, step = 4, window = 4) head(degree)dn <- dynet(school_contacts) degree <- centrality_series(dn, step = 4, window = 4) head(degree)
Extract an induced temporal subgraph
induce_subgraph(dn, nodes = NULL, ties = NULL, keep_isolates = FALSE)induce_subgraph(dn, nodes = NULL, ties = NULL, keep_isolates = FALSE)
dn |
A temporal network. |
nodes |
Which vertices to keep. Either a condition on the vertex
table, evaluated the way |
ties |
Which ties to keep. Either a condition on the spell table,
evaluated the way |
keep_isolates |
Whether named nodes without a selected tie remain.
Default |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), carrying only the selected
spells, the vertices they touch (plus any isolate named in nodes when
keep_isolates = TRUE), those vertices' activity spells, and all static
node and tie attributes. Metadata is rebuilt, so the observed range and
the canonical spell identifiers describe the subgraph, not the parent.
Raises dynet_unknown_node for a name that is not a vertex,
dynet_empty_network when the selection leaves no vertex or no tie, and
dynet_bad_input when neither nodes nor ties is supplied or a
selection is malformed.
dn <- dynet(school_contacts) # A condition on the vertex table, in one call. induce_subgraph(dn, degree > 16) # Names still work, as does anything carrying them. induce_subgraph(dn, nodes = c("Ana", "Ben", "Cara"))dn <- dynet(school_contacts) # A condition on the vertex table, in one call. induce_subgraph(dn, degree > 16) # Names still work, as does anything carrying them. induce_subgraph(dn, nodes = c("Ana", "Ben", "Cara"))
A community label is arbitrary within a bin. If bin 3's "community 2" is bin 4's "community 1", then flexibility, persistence and allegiance measure relabelling noise and nothing else. This verb walks the bins in order and gives a community the label of whichever earlier community it most overlaps with, so a label means the same group throughout.
You usually do not need this. When temporal_communities() runs with
omega > 0, the interlayer coupling is the matching, performed inside
the objective rather than as a post-hoc heuristic, and the labels are
already consistent. Matching is for omega = 0, for per-bin detection, for
comparing runs at different gamma, and for a partition computed
elsewhere.
match_communities( x, method = c("hungarian", "greedy"), overlap = c("jaccard", "intersection"), threshold = 0.1 )match_communities( x, method = c("hungarian", "greedy"), overlap = c("jaccard", "intersection"), threshold = 0.1 )
x |
A |
method |
|
overlap |
How to score a candidate pairing: |
threshold |
Minimum overlap for a community to inherit an earlier
label rather than start a new one. In |
Overlaps are computed only over the vertices active in both bins, so a community whose members merely go quiet is not read as dissolving.
The event taxonomy is Greene, Doyle and Cunningham's (2010), with an
optimal assignment step in place of their greedy one. A bin-
community that overlaps two or more bin- communities above
threshold has split: the assigned one inherits the label and the
others are born, and every state involved is marked "split". The mirror
case, two earlier communities feeding one later one, is a merge. When
both descriptions fit, "split" is reported.
Labels are never recycled. A community that dissolves at bin 10 and an unrelated one born at bin 40 cannot share a label, so a long series does not silently reconnect two different groups.
A dynet_communities frame with the same rows as x and three
guarantees: community now carries the matched, time-consistent label;
community_raw keeps the original per-bin label so the matching is
auditable; and event records what happened to this state's community at
this bin, one of "born", "persist", "split" or "merge". The
community-level lifecycle table, which also carries "dissolve", is
as.data.frame(x, what = "events").
Kuhn, H. W. (1955). The Hungarian method for the assignment problem. Naval Research Logistics Quarterly, 2(1-2), 83-97.
Jonker, R., & Volgenant, A. (1987). A shortest augmenting path algorithm for dense and sparse linear assignment problems. Computing, 38, 325-340.
Greene, D., Doyle, D., & Cunningham, P. (2010). Tracking the evolution of communities in dynamic social networks. ASONAM 2010, 176-183.
Palla, G., Barabasi, A.-L., & Vicsek, T. (2007). Quantifying social group evolution. Nature, 446, 664-667.
Cazabet, R., & Rossetti, G. (2019). Challenges in community discovery on temporal networks. In Temporal Network Theory, Springer, 181-197.
dn <- dynet(school_contacts) loose <- temporal_communities(dn, omega = 0, step = 5, window = 5, seeds = 1:3) match_communities(loose)dn <- dynet(school_contacts) loose <- temporal_communities(dn, omega = 0, step = 5, window = 5, seeds = 1:3) match_communities(loose)
Graph properties measured on each time bin, returned as a time series. This is where a temporal network earns its keep: a single density for the whole course tells you nothing about a group that was dense in week two and silent in week five.
metrics( dn, measure = "density", sessions = c("bounded", "collapse", "separate"), sample = NULL, start = NULL, end = NULL, step = NULL, window = NULL, basis = c("hops", "latency"), traversal_time = 0, plot = FALSE )metrics( dn, measure = "density", sessions = c("bounded", "collapse", "separate"), sample = NULL, start = NULL, end = NULL, step = NULL, window = NULL, basis = c("hops", "latency"), traversal_time = 0, plot = FALSE )
dn |
A temporal network from |
measure |
One or more measure names, |
sessions |
How to treat sessions, as in |
sample |
Deprecated. |
start, end
|
First and last time at which to measure. Default to the observed range. A network built from dates may be addressed with dates. |
step |
How often to measure. Defaults to the interval the network was built with. |
window |
How much time each measurement covers. Defaults to |
basis |
For |
traversal_time |
For |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
"density" counts the any-time union of realised edges against eligible
possible edges in each bin. The four temporal selectors instead integrate
exact state over positive observed time inside every reporting window.
"temporal_density" is binary occupied pair-time divided by all eligible
nonloop ordered-pair time (directed) or dyad time (undirected).
"observed_pair_density" uses the same numerator but restricts opportunity
to pairs having endpoint-valid evidence anywhere in the complete stored
history. That cohort is not reset by reporting windows or observation gaps.
summary() reports the first quantity over the pooled full history.
If Y[r](t) is exact simultaneous endpoint eligibility, E[r](t) is
binary edge presence, and H is the ever-observed pair set, the exposure
ledgers are R = sum(r) integral(Y[r](t) dt),
O = sum(r) integral(Y[r](t) E[r](t) dt), and
R_H = sum(r in H) integral(Y[r](t) dt). The two occupancies are O/R
and O/R_H and lie in [0, 1]. Loops, weights, duplicates, and censor
flags cannot multiply occupancy. Integration stops at the observation
period, which defaults to the span of the data, so a last window reaching
past the final spell is measured over its observed part only, and tiled
windows pool to the whole-period value. tsna::tEdgeDensity() uses the
same observation-period rule.
"onset_intensity" and "observed_pair_onset_intensity" divide the
number of known raw spell starts by R and R_H. Each nonloop raw row,
including a point contact, contributes once when its start is observed,
not onset-censored, and has exactly eligible endpoints. Termini are not
events; overlaps and duplicates remain distinct onsets. Intensities are
nonnegative, unbounded, and measured in inverse network time. A zero
denominator gives NA for every temporal selector, even when a point event
exists. A positive denominator with a zero numerator gives zero. Therefore
window = 0 makes all four temporal selectors undefined while snapshot
measures retain their exact point-state meanings.
step and window are separate on purpose: step is how often you look,
window is how much of the timeline each look takes in. A seven-day window
stepped one day at a time smooths a noisy series without giving up daily
resolution. They match time.interval and aggregate.dur in
tsna::tSnaStats().
When vertex activity was declared in dynet(), every measure is computed
on the endpoint-induced eligible vertex set for the window. Positive
windows independently use the any-time vertex and edge unions before
induction; window = 0 evaluates the exact state. Density and census
opportunities, components, isolate counts, largest-component shares, and
Freeman denominators therefore use eligible rather than fixed order.
"assortativity" is Newman's degree assortativity computed with the
total degree (in plus out) at both ends of every arc, on directed and
undirected snapshots alike. It is not the directed out-degree-to-in-degree
variant igraph::assortativity_degree(directed = TRUE) reports, and the
two disagree on directed data.
"centralization_closeness" is a Freeman centralisation of this package's
own closeness (reachable vertices divided by reachable distance, so it is
defined on a disconnected snapshot), with the theoretical maximum of that
score: n - 1 for a directed snapshot (a single arc from the centre) and
n - 2 for an undirected one (one isolated dyad). It matches
sna::centralization(closeness) on connected snapshots and not on
disconnected ones, where sna's closeness is zero everywhere.
Krackhardt's four indices – "connectedness", "efficiency",
"hierarchy" and "lubness" – describe how far a directed network
departs from a pure out-tree. "hierarchy" and "lubness" are undefined
on some graphs (no connected pair, no component of three) and report NaN
rather than a number that would mislead.
Triad census cost grows with the cube of the vertex count. On a network of a few hundred vertices it is the slowest measure here by a wide margin.
The lightweight structural selectors use the binary, loop-free induced
snapshot. Directed total degree is in-degree plus out-degree;
"degree_variance" is the sample variance across eligible vertices.
"mean_degree" is the mean out-degree for a directed graph (identically,
the mean in-degree) and the ordinary mean degree for an undirected graph;
this matches ERGM's meandeg statistic. "indegree_1_5" and
"outdegree_1_5" sum the corresponding vertex degrees raised to 1.5.
Directed "triangles" is the sum of cyclic and transitive triples, while
an undirected triangle is counted once.
A concurrent vertex has relations to at least two distinct neighbours
active at the same instant, so a reciprocal dyad still supplies only one
neighbour. Unlike the other structural selectors, concurrency is not read
from the window's union snapshot: with a positive window, two ties that
fall in the same window without overlapping in time do not make their
shared vertex concurrent. A vertex counts in a window when it is
concurrent at any instant inside it; "concurrent_nodes" is the number of
such vertices and "concurrent_share" divides it by the window's eligible
vertices. With window = 0 the snapshot is itself an instant, so both
readings coincide. Half-open spells that only meet at a boundary do not
overlap, and a point contact is concurrent with every relation active at
its timestamp. mixing() answers a different question – which groups are
connected somewhere in the window – and requires no simultaneity.
"in_2stars" and "out_2stars"
sum choose(degree, 2) over directed in- and out-degrees. Directed
"two_paths" counts ordered i -> j -> k paths with i != k;
undirected two-paths count each unordered wedge once. Empty eligible
snapshots return zero for all selectors.
A dynet_metric at graph level: one row per time point and
measure, with columns session (only under sessions = "separate", the
one mode that keeps session labels apart), time, measure and value.
"triads" contributes sixteen rows per time point, whose measure
entries are triad_003, triad_012, ..., triad_300. Print it,
summary() it, plot() it, or take the plain frame with
as.data.frame().
Errors: dynet_unknown_measure (a name outside the forty above),
dynet_needs_directed (one of the eight direction-reading selectors on an
undirected network), dynet_no_sessions (sessions = "separate" without a
session column), dynet_outside_observation (the requested range misses
observed support; it also carries dynet_bad_input), and
dynet_bad_input for every other broken contract –
dn not a dynet, a non-character measure, an empty measure, an
out-of-range start, end, step or window, end before start, and
step combined with window = "all".
Warning: dynet_deprecated for the retired sample argument.
Freeman, L. C. (1979). Centrality in social networks: conceptual clarification. Social Networks, 1, 215-239. doi:10.1016/0378-8733(78)90021-7
Butts, C. T., Leslie-Cook, A., Krivitsky, P. N., & Bender-deMoll, S. (2024). networkDynamic: Dynamic Extensions for Network Objects, version 0.11.5. doi:10.32614/CRAN.package.networkDynamic
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125. doi:10.1016/j.physrep.2012.03.001
Latapy, M., Viard, T., & Magnien, C. (2018). Stream graphs and link streams for the modeling of interactions over time. Social Network Analysis and Mining, 8, 61. doi:10.1007/s13278-018-0537-7
Andersen, P. K., & Gill, R. D. (1982). Cox's regression model for counting processes: a large sample study. Annals of Statistics, 10, 1100-1120. doi:10.1214/aos/1176345976
Krackhardt, D. (1994). Graph theoretical dimensions of informal organizations. In Computational Organization Theory (pp. 89-111). Lawrence Erlbaum.
Newman, M. E. J. (2002). Assortative mixing in networks. Physical Review Letters, 89, 208701. doi:10.1103/PhysRevLett.89.208701
Morris, M., & Kretzschmar, M. (1997). Concurrent partnerships and the spread of HIV. AIDS, 11(5), 641-648. doi:10.1097/00002030-199705000-00012
Holland, P. W., & Leinhardt, S. (1976). Local structure in social networks. Sociological Methodology, 7, 1-45. doi:10.2307/270703
Wasserman, S., & Faust, K. (1994). Social Network Analysis: Methods and Applications. Cambridge University Press.
dn <- dynet(school_contacts) metrics(dn, measure = "density") metrics(dn, measure = c("density", "reciprocity", "transitivity")) metrics(dn, measure = "density", step = 1, window = 3) dyads <- metrics(dn, measure = c("mutual", "asymmetric")) plot(dyads)dn <- dynet(school_contacts) metrics(dn, measure = "density") metrics(dn, measure = c("density", "reciprocity", "transitivity")) metrics(dn, measure = "density", step = 1, window = 3) dyads <- metrics(dn, measure = c("mutual", "asymmetric")) plot(dyads)
How much each kind of vertex interacted with each other kind, in every time bin. This is the question a temporal network answers that a static one cannot: not whether high and low achievers mixed, but when they did, and whether the pattern held or decayed.
The grouping variable comes from the vertex attributes supplied to
dynet() through its nodes argument.
mixing( dn, attribute, sessions = c("bounded", "collapse", "separate"), sample = NULL, start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )mixing( dn, attribute, sessions = c("bounded", "collapse", "separate"), sample = NULL, start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )
dn |
A temporal network from |
attribute |
Name of a column in the vertex table. A name the network
does not carry raises an error of class |
sessions |
How to treat sessions, as in |
sample |
Deprecated. |
start, end
|
First and last time at which to measure. Default to the observed range. A network built from dates may be addressed with dates. |
step |
How often to measure. Defaults to the interval the network was built with. |
window |
How much time each measurement covers. Defaults to |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
Each cell is a raw count of distinct active binary vertex dyads. Repeated, overlapping, or split spells and edge weights do not multiply a dyad. Retained self-loops count once. For directed networks, every ordered group pair is reported and
The row and column margins are grouped outdegree and indegree, and the table sum is the active directed edge count including retained loops.
Undirected networks report one lexicographically canonical cell for each
unordered group pair, with display labels such as "A -- B". A within-group
edge or loop contributes once to its diagonal cell. The group stub margin is
so the margins sum to twice the table total. These are unnormalised counts, not Newman's mixing proportions.
Missing attribute values are retained as a collision-safe explicit group ordered after observed labels. Bounded and collapsed modes both use the binary calendar union: a dyad active in two sessions at the same time counts once. Separate mode returns session-local tables over the fixed group universe. Every supported cell is emitted, including zeros. Declared vertex activity first induces the endpoint-valid snapshot. The complete group-cell universe remains fixed, but inactive vertices and eligible isolates contribute no dyad.
A dynet_metric at graph level with one row per time point and
group pair. The columns are session (only under
sessions = "separate", the one mode that keeps session labels apart),
time, measure, value, from_group and to_group. Directed
measure labels use "A -> B"; undirected labels use "A -- B".
value is the active binary-dyad count, and the authoritative
from_group and to_group columns identify the cell. Attributes record
unit, pair-domain, normalisation, weight, loop, missing-group, and
session-aggregation conventions.
Errors: dynet_unknown_attribute (no such vertex attribute),
dynet_no_sessions (sessions = "separate" without a session column),
dynet_outside_observation (the requested range misses observed support;
it also carries dynet_bad_input),
and dynet_bad_input for every other broken contract – dn not a
dynet, an attribute that is not a single column name, and an
out-of-range start, end, step or window.
Warning: dynet_deprecated for the retired sample argument.
Newman, M. E. J. (2003). Mixing patterns in networks. Physical Review E, 67, 026126. doi:10.1103/PhysRevE.67.026126
Morris, M., Handcock, M. S., & Hunter, D. R. (2008). Specification of exponential-family random graph models: terms and computational aspects. Journal of Statistical Software, 24(4). doi:10.18637/jss.v024.i04
dn <- dynet(forum_posts, thread = "thread", nodes = forum_people) role_mixing <- mixing(dn, attribute = "role") role_mixing plot(role_mixing)dn <- dynet(forum_posts, thread = "thread", nodes = forum_people) role_mixing <- mixing(dn, attribute = "role") role_mixing plot(role_mixing)
The participant table accompanying mooc_posts: every person who appears as a sender or a receiver there, with the self-reported experience level as the chapter's integer code and as the label it recodes the code into and uses as the mixing attribute.
mooc_peoplemooc_people
A data frame with 445 rows and 3 columns:
Character. Participant identifier, matching the sender
and receiver columns of mooc_posts.
Integer. Self-reported experience level: 1 expert,
2 student, 3 teacher.
Character. The same level as Expert, Student
or Teacher.
As mooc_posts: Saqr (2024), doi:10.1007/978-3-031-54464-4_17.
mooc_posts; vignette("ch17-temporal-networks").
participants <- dynet(mooc_posts, from = "sender", to = "receiver", time = "timestamp", thread = "discussion", nodes = mooc_people) as.data.frame(participants, what = "nodes")participants <- dynet(mooc_posts, from = "sender", to = "receiver", time = "timestamp", thread = "discussion", nodes = mooc_people) as.data.frame(participants, what = "nodes")
The discussion log of chapter 17 of Learning Analytics Methods and
Tutorials (Saqr, 2024), one row per post in the Digital Learning
Transition MOOC, April to June 2013. A post names the participant who
wrote it and the participant it answers, so a tie runs from sender to
receiver; discussion is the thread the post belongs to, which is what
makes the log threaded in the sense dynet() means by thread =.
mooc_postsmooc_posts
A data frame with 2529 rows and 4 columns:
Character. The participant who wrote the post.
Character. The participant the post answers.
POSIXct (UTC). When the post was made, from 2013-04-04 16:32 to 2013-06-16 17:12.
Character. Thread title; 338 distinct threads.
Only the four columns the chapter's analysis reads are kept; the category hierarchy and comment identifiers of the published file are dropped.
Saqr, M. (2024). Temporal network analysis: Introduction, methods
and analysis with R. In M. Saqr & S. López-Pernas (Eds.), Learning
Analytics Methods and Tutorials. Springer.
doi:10.1007/978-3-031-54464-4_17. Data from
https://github.com/lamethods/data, directory 6_snaMOOC, prepared by
data-raw/mooc_forum.R.
mooc_people for the participants, and
vignette("ch17-temporal-networks") for the chapter's analysis.
dn <- dynet(mooc_posts, from = "sender", to = "receiver", time = "timestamp", thread = "discussion") dndn <- dynet(mooc_posts, from = "sender", to = "receiver", time = "timestamp", thread = "discussion") dn
Counts the 40 Paranjape three-edge, three-node temporal motif classes, in the class ordering of raphtory 0.17.0 so a census can be checked against an independent implementation index by index.
motifs( dn, delta, output = c("global", "local"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL )motifs( dn, delta, output = c("global", "local"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL )
dn |
A directed temporal network from |
delta |
Window width, in the network's time unit. A triple of events
is a motif instance when its last event is no later than |
output |
|
sessions |
How to treat sessions, as in |
start, end
|
Optional inclusive query limits. |
The definition. Three incompatible motif families exist in the
literature. This adopts Paranjape, Benson & Leskovec (2017): exactly three
edge events on at most three distinct vertices, with
t_last - t_first <= delta over the three times in sorted order. Kovanen et
al. (2011) instead allow arbitrary size and bound consecutive gaps, which
is a strictly weaker condition and agrees only for two-edge patterns; that
family's instance count is unbounded and does not reduce to a fixed census.
The three edges are three distinct events; the same pair may repeat, which is exactly what the two-node classes are. Loops are excluded, and instances may share edges – every qualifying triple is counted, as in the reference implementations. Edge-disjoint counting is a different and much harder problem, and is not what any reference computes.
Local counting is not uniform across families, and this is raphtory's rule, adopted verbatim. For a star only the centre counts the instance; for a triangle all three vertices do; for a two-node motif the global census itself already records one count at each endpoint's class. So the local counts sum to the global counts for stars and two-node motifs, and to three times the global counts for triangles.
Simultaneous events are ordered by the deterministic key
(time, from, to, spell). Paranjape assumes distinct timestamps; this is
Dynet's choice, so that a census never depends on input row order.
The enumeration is the direct one, quadratic in the number of events
sharing a window. Paranjape's incremental algorithm is linear; this is not
it, and a very large delta on a bursty network will be slow.
A dynet_motifs data frame. Under output = "global" the columns
are motif (the 1-based raphtory index), family, pattern and
count; under "local" a node column precedes them. A leading
session column is present under sessions = "separate". All 40 classes
are always present, zeros included – a census with rows silently missing
is not a census. Print it, summary() it, plot() it, or take the plain
frame with as.data.frame().
Errors: dynet_needs_directed on an undirected network, since the 40-class
taxonomy is defined by edge directions and has no honest undirected
reading; dynet_bad_input when delta is missing, not a single finite
non-negative number, or when dn is not a dynet; dynet_no_sessions
under sessions = "separate" without a session column;
dynet_count_overflow when a class exceeds 2^53.
Paranjape, A., Benson, A. R., & Leskovec, J. (2017). Motifs in temporal networks. WSDM '17, 601-610. doi:10.1145/3018661.3018731
Kovanen, L., Karsai, M., Kaski, K., Kertesz, J., & Saramaki, J. (2011). Temporal motifs in time-dependent networks. Journal of Statistical Mechanics, P11005. doi:10.1088/1742-5468/2011/11/P11005
gaps() for choosing delta, and pshifts() for the dyadic
turn-taking census, which counts the same events with a different arity.
dn <- dynet(school_contacts) census <- motifs(dn, delta = 2) censusdn <- dynet(school_contacts) census <- motifs(dn, delta = 2) census
The Mucha et al. (2010) quality function: how much better a partition
explains the network than a degree-preserving null does, computed slice by
slice with the slices tied together by the interlayer coupling omega.
Score a partition you already have – a class roster, a set of research
groups, an externally computed clustering – or score the one
temporal_communities() found. It is the objective that verb maximises, so
the two round-trip with no reshaping by the caller.
multislice_modularity( dn, membership = NULL, gamma = 1, omega = 1, coupling = c("ordinal", "categorical"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL )multislice_modularity( dn, membership = NULL, gamma = 1, omega = 1, coupling = c("ordinal", "categorical"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL )
dn |
A temporal network from |
membership |
A data frame with columns |
gamma |
Resolution. Values above one favour more, smaller communities; values below one favour fewer, larger ones. |
omega |
Interlayer coupling: how much a vertex is rewarded for keeping its community from one slice to the next. Zero scores the slices independently. |
coupling |
Which slices are coupled, |
sessions |
How to treat sessions: |
start, end
|
First and last slice times. Default to observed support. |
step |
Spacing between slice starts. |
window |
Width represented by each slice. |
With the weight of edge – in slice ,
its slice-local strength, the slice total,
the coupling of vertex between slices
and , and ,
Three things in that formula are not the static null, and getting any of them wrong gives a different objective that still looks plausible:
The null uses , and from slice
alone, never the supra-degree. That is what lets a sparse bin
and a dense bin be compared at all.
The null is multiplied by , so no null is subtracted
from the interlayer arcs. Identity arcs are not edges to be explained
away; they assert that a vertex is the same vertex.
The normaliser includes the coupling strength, which is
why does not diverge as omega grows.
Running igraph::cluster_louvain() on the supra-adjacency matrix gets all
three wrong: it applies the static Newman–Girvan null to the whole
supra-graph. That is a different objective function, not a different
implementation of this one.
Two exact reductions follow from the formula and are pinned by tests. With
one slice and omega = 0, is ordinary Newman–Girvan modularity.
With omega = 0 and any number of slices, is the -
weighted mean of the per-slice Newman–Girvan modularities.
An edgeless slice has and would divide by zero; its null
contribution is defined as exactly zero, and n_empty_slices reports how
many such slices there were. A network with no edges and no coupling has
and no defined at all, which raises
dynet_empty_result rather than returning NaN.
A directed network is averaged with its transpose before scoring, because
the published quality function is defined for symmetric slices. The result
records this in its symmetrised attribute.
A dynet_metric at graph level, one row per component of the
decomposition: q (the multislice modularity), q_intra and q_inter
(its within-slice and interlayer parts, which sum to q), two_mu (the
normaliser), n_communities and n_empty_slices. Columns are measure
and value; there is no time column because is a property of
the whole series, not of a bin. Attributes carry gamma, omega,
coupling and symmetrised.
Mucha, P. J., Richardson, T., Macon, K., Porter, M. A., & Onnela, J.-P. (2010). Community structure in time-dependent, multiscale, and multiplex networks. Science, 328(5980), 876-878.
Newman, M. E. J., & Girvan, M. (2004). Finding and evaluating community structure in networks. Physical Review E, 69(2), 026113.
Reichardt, J., & Bornholdt, S. (2006). Statistical mechanics of community detection. Physical Review E, 74(1), 016110.
Bazzi, M., Porter, M. A., Williams, S., McDonald, M., Fenn, D. J., & Howison, S. D. (2016). Community detection in temporal multilayer networks, with an application to correlation networks. Multiscale Modeling & Simulation, 14(1), 1-41.
temporal_communities(), which maximises this quantity, and
projection(), which builds the time-expanded network it is defined on.
dn <- dynet(school_contacts) multislice_modularity(dn, step = 5, window = 5) multislice_modularity(dn, membership = temporal_communities( dn, step = 5, window = 5, seeds = 1:3 ), step = 5, window = 5)dn <- dynet(school_contacts) multislice_modularity(dn, step = 5, window = 5) multislice_modularity(dn, membership = temporal_communities( dn, step = 5, window = 5, seeds = 1:3 ), step = 5, window = 5)
Centrality computed from the time-respecting paths that paths() finds,
or from the temporal walks of the contact stream, taken across the whole
observation period (or the start-to-end window). A path may only
continue along a tie that is available after it arrives, so these values
cannot be inflated by ties that occur in the wrong order, as a flattened
network is. The result is one value per vertex, not a series: for
centrality that changes from window to window, use centrality_series();
for the number of vertices a vertex can reach, use reachability().
path_centrality( dn, measure = "closeness", sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0, criterion = c("foremost_then_shortest", "min_hops", "foremost", "fastest", "shortest"), cost = c("hops", "weight"), top = NULL, mode = c("out", "in"), beta = 0.1, decay = 0, damping = 0.85, transition = 1, rescale = TRUE, plot = FALSE )path_centrality( dn, measure = "closeness", sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0, criterion = c("foremost_then_shortest", "min_hops", "foremost", "fastest", "shortest"), cost = c("hops", "weight"), top = NULL, mode = c("out", "in"), beta = 0.1, decay = 0, damping = 0.85, transition = 1, rescale = TRUE, plot = FALSE )
dn |
A temporal network from |
measure |
One or more of the path measures |
sessions |
How to treat sessions: |
start, end
|
Inclusive path-traversal bounds. Default to the observed range. A network built from dates may be addressed with dates. |
traversal_time |
Nonnegative duration charged for every hop, in the
network's time unit; |
criterion |
Which optimisation problem
the journeys solve. |
cost |
For |
top |
For |
mode |
For |
beta |
For |
decay |
For |
damping |
For |
transition |
For temporal |
rescale |
For |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
Betweenness is the raw dependency sum over reachable forward
ordered pairs. For each source-target pair, its unit dependency is divided
equally over every canonical shortest-foremost journey, and an internal
vertex receives the fraction of those journeys that contain it. Sources and
targets receive no endpoint credit. This ordered-pair convention also
applies to undirected contacts because temporal reach is generally
asymmetric. The result is not normalised; its fixed range is
[0, (n - 1) * (n - 2)].
Closeness is inverse mean forward latency over reachable vertices:
if is the set of reachable vertices other than source ,
where is the foremost arrival time and is the source's
resolved origin: the traversal window's lower bound, or – when vertex
activity was declared – the source's first presence inside that window.
Every reachable endpoint is included once, regardless
of how many optimal paths reach it. A source with no reachable nonself
endpoints has value zero. If all reachable endpoints have zero latency, the
value is Inf; zero-latency endpoints remain in the numerator when mixed
with positive latencies. The measure therefore has inverse-time units, is
invariant to translating the time axis, and scales inversely when time is
rescaled.
Both measures use paths() traversal semantics: nondecreasing
times, unlimited waiting, half-open interval spells, and a separate exact
timestamp rule for point events. Positive traversal_time requires an
interval traversal to finish within continuous pair activity; a point event
triggers at its timestamp and reaches its endpoint after that duration.
start and end bound every measure. In separate-session output, a
session outside a one-sided bound contributes zero rows.
Efficiency is the mean reciprocal temporal distance from a vertex to every
other vertex (mode = "out"), or to it from every other (mode = "in"),
with the distance the criterion optimises. Unreachable vertices
contribute zero rather than being dropped, so it is defined on a
disconnected network; it is therefore not the reciprocal of closeness,
which averages over reachable vertices only.
Temporal Katz centrality is the attenuated, time-decayed count of temporal
walks ending at each vertex (Beres et al., 2018): each contact (u, v, t)
passes beta times whatever had reached u, plus one for the walk that
is that contact alone.
Temporal PageRank scores a vertex by the damped, transition-weighted count
of temporal walks that end at it, following Rozenshtein and Gionis (2016).
It is computed by their Algorithm 1 in one pass over the contact stream: a
contact (u, v, t) starts a fresh walk at u worth 1 - damping, then
carries damping times whatever mass is waiting at u across to v. With
transition < 1 the mass left behind at u is multiplied by transition
for each contact it declines, so a walk\'s next step follows a geometric
distribution over the outgoing contacts that pass. transition = 1 is a
distinct rule rather than the limit of that one: the waiting mass leaves
entirely on the first passing contact.
Two consequences are worth stating plainly. The raw score grows linearly
with the number of contacts, so it is comparable only within one window;
rescale = TRUE, the default for this measure, divides by the total and is
what makes two windows comparable. And the walk does not restart uniformly:
a walk begins at u once per outgoing contact of u, so the implied
personalization is the out-degree distribution. Under repeated uniform
sampling from a fixed digraph and transition = 1, the rescaled scores
converge to static PageRank personalized by weighted out-degree; on a
digraph of constant out-degree that is exactly the uniform-teleport
PageRank reported at snapshot scope. There is no teleportation matrix and so
no dangling-mass correction: a vertex with no outgoing contact simply holds
its mass. A vertex that never appears as a contact endpoint scores exactly
zero, and a block with no eligible contact at all scores NaN throughout
once rescaled.
"walk" is temporal walk centrality (Oettershagen, Mutzel and Kriege,
2022): a vertex scores by the walks that arrive at it and the walks that
leave it afterwards, sum over t1 < t2 of W_in(v, t1) * W_out(v, t2) * exp(-decay * (t2 - t1)),
where W_in(v, t) is the summed weight of temporal walks ending at v
at t and W_out(v, t) of those starting there. It is the one measure
of the family that scores brokerage in time, obtaining then
distributing, rather than accumulated arrivals; it costs two passes over
the contact stream. The pairing is strict: an arrival and a departure at
the same instant are two contacts that cannot chain, so they are not
paired. A vertex with no incoming or no outgoing contact scores zero. On
an undirected network every contact runs both ways. The result records
attenuation, decay, walk_weight, waiting_weight, walk_rule and
pairing.
Simultaneous contacts are batched strictly for the stream measures,
"katz", "pagerank" and "walk": every contact sharing a timestamp reads the
active mass as it stood before that instant, and the arrivals it produces
are only available afterwards. This deliberately differs from paths(),
which composes equal-time contacts at traversal_time = 0, and from a
literal reading of Algorithm 1, which would let one contact continue a walk
another contact delivered in the same instant. Without the batch rule the
answer would depend on the arbitrary order of rows inside one instant, and
therefore on the vertex labelling. On a stream whose timestamps are
distinct and which carries no self-contact, the batched recurrence reduces
to Algorithm 1 exactly.
Declared vertex activity gates the exact source anchor and every hop. Waiting after a valid anchor may cross inactivity; interval traversal requires both endpoints through completion, while a point trigger requires the receiver again after any traversal delay. Fixed node rows and full-network denominators are retained.
A node-level dynet_metric: a tidy data frame with one row per
vertex and measure, columns node, measure and value, preceded by
session under sessions = "separate". There is no time column. A
single-measure result stores its mathematical choices as direct
attributes; a two-measure result stores named records under
measure_metadata.
Errors: dynet_unknown_measure (a measure not listed under measure),
dynet_intractable_criterion (betweenness under criterion = "foremost";
it also carries dynet_bad_input), dynet_no_sessions (sessions = "separate" without a
session column), dynet_outside_observation (the requested range misses
observed support; it also carries dynet_bad_input), and
dynet_bad_input for every other broken contract – dn not a dynet, a
malformed measure, an out-of-range start, end or traversal_time,
an argument named with a measure it does not apply to, betweenness under
criterion = "fastest", and top outside closeness under "min_hops" or
"shortest".
Pan, R. K., & Saramaki, J. (2011). Path lengths, correlations, and centrality in temporal networks. Physical Review E, 84(1), 016105.
Tang, J., Musolesi, M., Mascolo, C., Latora, V., & Nicosia, V. (2010). Analysing information flows and key mediators through temporal centrality metrics. Proceedings of SNS '10.
Buss, S., Molter, H., Niedermeier, R., & Rymar, M. (2024). Algorithmic aspects of temporal betweenness. Network Science, 12(2), 160-188.
Nicosia, V., Tang, J., Mascolo, C., Musolesi, M., Russo, G., & Latora, V. (2013). Graph metrics for temporal networks. In Temporal Networks (pp. 15-40). Springer.
Beres, F., Palovics, R., Olah, A., & Benczur, A. A. (2018). Temporal walk based centrality metric for graph streams. Applied Network Science, 3, 32.
Oettershagen, L., Mutzel, P., and Kriege, N. M. (2022). Temporal walk centrality: ranking nodes in evolving networks. Proceedings of the ACM Web Conference 2022, 1640-1650. doi:10.1145/3485447.3512210
Rozenshtein, P., & Gionis, A. (2016). Temporal PageRank. In Machine Learning and Knowledge Discovery in Databases (ECML PKDD 2016), LNCS 9852, 674-689.
paths(), reachability(), centrality_series().
# Every ordered pair is searched, so the cost grows steeply with the # vertex count; a subgraph keeps the example quick. dn <- dynet(school_contacts) few <- induce_subgraph(dn, nodes = c("Ana", "Ben", "Cara", "Dan", "Eve", "Finn", "Gita", "Hugo")) path_centrality(few) path_centrality(few, measure = c("closeness", "betweenness"), start = 0, end = 10) # The leaders only, searching just what the ranking needs path_centrality(few, criterion = "min_hops", top = 3) # Stream measures need no path search, so they run on the whole network. path_centrality(dn, measure = "katz") path_centrality(dn, measure = "walk", decay = 0.1) path_centrality(dn, measure = "pagerank", transition = 0.5)# Every ordered pair is searched, so the cost grows steeply with the # vertex count; a subgraph keeps the example quick. dn <- dynet(school_contacts) few <- induce_subgraph(dn, nodes = c("Ana", "Ben", "Cara", "Dan", "Eve", "Finn", "Gita", "Hugo")) path_centrality(few) path_centrality(few, measure = c("closeness", "betweenness"), start = 0, end = 10) # The leaders only, searching just what the ranking needs path_centrality(few, criterion = "min_hops", top = 3) # Stream measures need no path search, so they run on the whole network. path_centrality(dn, measure = "katz") path_centrality(dn, measure = "walk", decay = 0.1) path_centrality(dn, measure = "pagerank", transition = 0.5)
paths() uses an endpoint-local foremost-then-shortest criterion, so
its routes need not form one predecessor tree. This function therefore
returns the honest union of all expanded optimal route hops. Edge weight
is the number of endpoint/path families using the hop; first_time and
last_time retain its temporal range.
path_network(x)path_network(x)
x |
A result from |
A static dynet_path_network cograph netobject, whose two tidy
tables are reached with as.data.frame(x, what = "edges") and
as.data.frame(x, what = "nodes"). The edge table has one row per hop
used by at least one optimal route, with from, to, weight (how many
endpoint/path families use the hop), first_time and last_time (the
hop's temporal range) and n_endpoints (how many distinct endpoints it
serves). The node table has one row per vertex the source actually
reaches, the source included, with name, arrival_time, latency,
n_hops, n_paths and groups (hop count as a grouping label for
plotting). Unreachable vertices are absent, not present with NA.
The network is always directed, because a route hop has an orientation
even when the temporal network does not; hops of a backward path result
still point the way time runs, from the sender towards the queried
target, and its arrival_time is that vertex's latest-departure
supremum, as in paths().
A result that is not from paths() raises dynet_bad_input; a path
result with no reachable vertex raises dynet_empty_result.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") union_network <- path_network(routes) as.data.frame(union_network) as.data.frame(union_network, what = "nodes")dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") union_network <- path_network(routes) as.data.frame(union_network) as.data.frame(union_network, what = "nodes")
Turns the optimal route family returned by paths() into a tidy
prefix tree. Every row is one tree node: a route prefix reaching vertex
at time, used by count optimal routes. A named vertex reached through
a different temporal history is a separate row, so branches never create
the misleading crossings of a path-union graph and two routes that differ
only in when a hop fires stay separate.
Forward routes grow away from the queried source. Backward routes are reversed, so the queried target is the root and possible senders branch away from it.
path_trajectories(x, min_count = 1L, plot = FALSE)path_trajectories(x, min_count = 1L, plot = FALSE)
x |
A result from |
min_count |
Keep only branches used by at least this many optimal
routes. The default of |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
A dynet_path_trajectories data frame with one row per tree node
and columns node (the route prefix, written as vertex@time steps
joined by arrows), parent, depth, count, probability, vertex,
time, session and branch. depth is the hop number from the
queried vertex, probability is the branching fraction of the parent's
routes that continue along this branch and is missing at the root, which
has no parent, and branch is the node's
placement across the tree. The synthetic (start) root is dropped when
it has a single child, which is the usual case; it is kept when it
genuinely branches, as under sessions = "separate", where it carries
one subtree per session, has no vertex or time, a missing
probability, and pushes every other node one hop deeper.
A result that is not from paths(), or a min_count that is not one
positive whole number, raises dynet_bad_input; a path result with no
route step, or a min_count no prefix reaches, raises
dynet_empty_result.
plot_path_trajectories() to draw the tree, path_network() for
the route union as a network.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") path_trajectories(routes)dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") path_trajectories(routes)
Follows every time-respecting path out of (or into) one vertex and reports where it gets to, when, and through whom. A path may only use edges whose timing runs forward, so unlike a path in a flattened network it can never travel back in time.
The source vertex is named, not numbered. paths(dn, from = "Ana")
works; there is no vertex index to look up first.
At the default zero traversal duration, forward paths use nondecreasing hop times, so relations active at the same instant may form a multi-hop chain. Waiting is allowed. Interval spells are onset-inclusive and terminus-exclusive; point events trigger at their exact timestamp through a distinct event rule. A positive duration separates a hop's trigger or entry from its completion, as detailed below. Reach and arrival do not depend on edge-row order or duplicate spell rows.
paths( dn, from, at = NULL, direction = c("forward", "backward"), criterion = c("foremost_then_shortest", "min_hops", "foremost", "fastest", "latest_departure", "shortest"), cost = c("hops", "weight"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0, max_states = 1e+05, plot = FALSE )paths( dn, from, at = NULL, direction = c("forward", "backward"), criterion = c("foremost_then_shortest", "min_hops", "foremost", "fastest", "latest_departure", "shortest"), cost = c("hops", "weight"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0, max_states = 1e+05, plot = FALSE )
dn |
A temporal network from |
from |
Name of the one vertex the search is anchored on: the source of a forward search, the target of a backward one. |
at |
Forward source-availability time or backward arrival deadline.
An explicit |
direction |
|
criterion |
Which optimisation problem to solve.
|
cost |
For |
sessions |
How to treat sessions, as in |
start, end
|
Inclusive lower and upper traversal-time bounds. Interval
spells remain terminus-exclusive. When these are supplied, use them
instead of |
traversal_time |
Nonnegative duration charged for every hop, in the
network's time unit. A calendar network also accepts a scalar |
max_states |
For |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
A valid forward journey has distinct vertices, hop-entry times x, and
completion times y = x + traversal_time. The source is ready at the
resolved origin, each later entry is no earlier than the preceding
completion, and final completion is at or before end. At zero duration,
entry and completion coincide, recovering the nondecreasing hop times
described above. The empty journey reaches the source at the origin. With
at, that value is both the origin and the window bound: start for
forward paths or end for backward paths. Cycles are unnecessary for
reach and earliest
arrival because deleting a repeated-vertex section and waiting at that
vertex preserves every later hop.
The origin is anchored at the source's own presence. Without at, a vertex
with declared spells starts at the first instant it is present inside the
window, or at the last instant when searching backward, so a vertex that
enters the network late is never scored from a time before it existed; a
vertex with no declared spells starts at the window bound. A vertex that is
never present inside the window has no valid anchor, so every row of its
result, the source row included, is unreachable. The resolved origin is
reported in the printed header; under sessions = "separate", where every
session resolves its own, it is reported in the origin column instead.
start and end form a closed bound on the complete journey: entry may
equal start and completion may equal end. This does not close interval
activity on the right. At zero duration, an event or interval onset at
end is eligible while an interval terminating there cannot be entered.
With positive duration, no nonempty hop can both enter and complete at
end; start = end therefore leaves only the empty journey.
Declared vertex activity gates traversal appearances. The anchor must be
valid: the forward source must be active exactly at the resolved origin,
and the backward target either active there or leaving exactly there, since
a spell's terminus is the last instant that vertex exists even though
presence is half-open. An invalid anchor – which an explicit at outside
the source's own spells produces – leaves every fixed-universe row,
including the anchor row itself, unreachable.
After a valid anchor, waiting may cross inactive periods. A zero-duration
hop requires both endpoints at its time. A positive-duration interval hop
requires both endpoints continuously on the closed traversal from entry
through completion. A delayed point contact requires both endpoints at its
trigger and the receiver again at completion, but creates no continuous
edge or tail occupancy. Several activity-created timing domains of one
canonical contact remain one path atom and cannot multiply n_paths.
For backward paths, arrival_time is the latest-departure supremum for a
journey ending at the named target by the resolved end, and latency is
end minus that value. A supremum at an interval's excluded terminus need
not itself be an attainable departure. Such an endpoint is still reachable
and still reports its route family: n_hops, n_paths and the steps of
the routes that approach the supremum are those of the family, and
attained = FALSE records that the instant itself is not realised.
A latest-departure query is solved by time reversal: the latest departure
from from into a vertex z by end is the label a backward search rooted
at z assigns to from, so one backward search per vertex answers it with
every session, activity and attainment rule inherited unchanged. In
particular each target inherits the backward anchor rule and must be active
at end; the empty journey departs from from at end itself. The
optimality attribute records "maximum" for this criterion and
"minimum" for the others, and deadline records the resolved end.
A shortest query with weight costs is solved by settling vertex
appearances in order of accumulated cost (Wu et al., 2016): every cost is
positive, so the cheapest open appearance is final when it is settled and
every predecessor of an appearance is settled before it, which is what
makes the journey count exact. Two journeys tie when their summed weights
agree within a relative tolerance of sqrt(.Machine$double.eps).
A fastest query is a sweep over candidate departures: for a fixed
source-ready time the problem is prefix-optimal again, so the minimum
duration is the minimum over departures d of the earliest arrival from
d minus d. The candidates are the atom-domain endpoints (shifted by
whole multiples of traversal_time), each taken exactly and from below,
at which the source can depart; one forward search runs per candidate.
With sessions = "bounded", each endpoint is optimised across complete
session-specific searches. A unique winner is named in path_session; ties
leave it missing and are counted in n_best_sessions. No merged predecessor
tree is exposed. The steps accessor retains a complete route from every tied
best session, so each route stays inside one session. With
sessions = "separate", every session contributes a complete vertex block
and resolves its own default origin. In the steps table, time is the
optimal search label at that route vertex. For backward interval paths it
can be an unattained supremum, as indicated by attained = FALSE.
With positive traversal_time, an interval hop entered at x arrives at
x + traversal_time and must fit within continuous activity for that pair;
overlapping or touching interval spells form one component. Completion
exactly at the component terminus is allowed. A point event triggers at its
timestamp and arrives after the same duration; it does not represent
continued edge activity. The query end bounds completion, not only entry.
Optimal forward journeys are shortest foremost: final completion is
minimised first (foremost) and hop count second (shortest). Backward
journeys mirror it, maximising the departure time first and minimising hop
count second. There is no criterion argument: this is the only criterion
paths() offers, and it is recorded on the result as
"foremost_then_shortest". A fastest journey, which minimises elapsed time
rather than arrival time, is a different optimum and is not computed here.
Journey identity is the ordered sequence of canonical oriented contacts.
Duplicate points, overlapping or
touching interval segmentation, weights, and waiting schedules do not
multiply paths; genuinely recurrent contacts do. n_paths is exact through
2^53, after which a dynet_path_overflow condition is raised. The empty
journey has one path and an unreachable endpoint has none.
Failures are classed. An unknown from raises dynet_unknown_vertex; a
from that is not one name, a negative traversal_time, combining at
with start or end, or a window that cannot hold a journey, raises
dynet_bad_input; a window disjoint from explicit observation raises
dynet_outside_observation; a count beyond 2^53 raises
dynet_path_overflow; and expanding more than a million routes through
as.data.frame(x, what = "steps") raises
dynet_path_expansion_too_large, which the compact n_paths column
answers instead.
An object of class "dynet_paths": a tidy data frame with one row
per vertex and columns node, reachable, arrival_time, attained
(whether that optimum itself is realised), latency (elapsed time
between the origin and arrival_time, in either direction), n_hops,
and the exact count n_paths. Under
criterion = "latest_departure" a departure_time column follows
arrival_time, holding the latest departure supremum from from, and a
duration column; arrival_time, n_hops and n_paths then describe
the journeys that depart exactly then (earliest arrival, then fewest
hops, within that family). Under criterion = "fastest" the same two
columns hold the fastest journey's departure and its duration. In both
cases an unattained optimum keeps its limiting departure, arrival and
duration but has n_hops = NA and n_paths = 0. Under
criterion = "shortest" a path_cost column follows n_paths: the
summed cost of the cheapest journey, n_hops is the fewest hops among
the cheapest journeys (NA when they differ), and arrival_time and
n_paths describe the cheapest journeys that arrive earliest. Bounded
mode adds path_session and n_best_sessions; separate mode adds session and
origin, one complete vertex block per session. Use
as.data.frame(x, what = "steps") for every reconstructed optimal route:
one row per vertex visited, with endpoint, path_id (endpoint-local,
distinguishing tied atom sequences), path_session, step, node,
time and attained, preceded by session in separate mode.
Kempe, D., Kleinberg, J., & Kumar, A. (2002). Connectivity and inference problems for temporal networks. Journal of Computer and System Sciences, 64(4), 820-842.
Bui-Xuan, B., Ferreira, A., & Jarry, A. (2003). Computing shortest, fastest, and foremost journeys in dynamic networks. International Journal of Foundations of Computer Science, 14(2), 267-285.
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125.
Casteigts, A., Corsini, A., & Sarkar, W. (2024). Simple, strict, proper, happy: A study of reachability in temporal graphs. Theoretical Computer Science, 991, 114434.
Buss, S., Molter, H., Niedermeier, R., & Rymar, M. (2024). Algorithmic aspects of temporal betweenness. Network Science, 12(2), 160-188.
Wu, H., Cheng, J., Huang, S., Ke, Y., Lu, Y., & Xu, Y. (2014). Path problems in temporal graphs. Proceedings of the VLDB Endowment, 7(9), 721-732.
Bui-Xuan, B., Ferreira, A., & Jarry, A. (2003). Computing shortest, fastest, and foremost journeys in dynamic networks. International Journal of Foundations of Computer Science, 14(2), 267-285.
dn <- dynet(school_contacts) # Every earliest-arrival journey, not only the shortest ones. The other # criteria search exhaustively, so they are shown on a small network. few <- dynet(data.frame(from = c("A", "A", "B", "C"), to = c("D", "B", "C", "D"), time = c(4, 1, 2, 4)), format = "contact", directed = TRUE) paths(few, from = "A", criterion = "foremost") paths(few, from = "A", criterion = "fastest") paths(few, from = "A", criterion = "latest_departure", end = 4) routes <- paths(dn, from = "Ana") routes summary(routes) paths(dn, from = "Ana", start = 0, end = 10) paths(dn, from = "Ana", direction = "backward")dn <- dynet(school_contacts) # Every earliest-arrival journey, not only the shortest ones. The other # criteria search exhaustively, so they are shown on a small network. few <- dynet(data.frame(from = c("A", "A", "B", "C"), to = c("D", "B", "C", "D"), time = c(4, 1, 2, 4)), format = "contact", directed = TRUE) paths(few, from = "A", criterion = "foremost") paths(few, from = "A", criterion = "fastest") paths(few, from = "A", criterion = "latest_departure", end = 4) routes <- paths(dn, from = "Ana") routes summary(routes) paths(dn, from = "Ana", start = 0, end = 10) paths(dn, from = "Ana", direction = "backward")
pathways() reports whole journeys rather than per-vertex summaries: one
row per distinct route, ranked by how many optimal routes follow it. It
answers "which pathways does this network actually use", where
path_trajectories() answers "where do the routes diverge" and paths()
answers "who is reachable".
pathways(dn, from = NULL, top = NULL, min_hops = 1L, ..., plot = FALSE)pathways(dn, from = NULL, top = NULL, min_hops = 1L, ..., plot = FALSE)
dn |
A temporal network from |
from |
Optional source vertex. A name gives the routes leaving that
vertex, and several names give the routes leaving each of them. The
default, |
top |
Optional number of routes to keep, most frequent first. The default keeps all of them. |
min_hops |
Shortest route to report. Defaults to one, which drops the zero-hop route from a vertex to itself. |
... |
Passed to |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
The result is already ordered and already carries the share of the total,
so a caller never sorts or subsets it; top limits it in the call.
Routes are keyed on their vertex sequence. The trajectory tree keys a node on vertex and time, so one sequence realised through different contacts appears there as several branches; those are one pathway here and their counts are added. Under the foremost criterion this loses nothing: only earliest-arrival routes survive to be counted, so duplicates of a sequence necessarily share an arrival time, and a test asserts it.
An object of class dynet_pathways, a data frame with one row per
distinct route, most frequent first: route, the vertex sequence joined
by arrows; endpoint, where it lands; count, how many optimal routes
follow it; share, its fraction of every counted route, so the shares of
a result limited by top do not sum to one; n_hops; and
arrival_time, the earliest time the route lands. Pooling over every
source adds from as the first column. Use as.data.frame() for a plain
frame and as.data.frame(x, what = "steps") for the per-hop timing of
the routes that were kept.
An unknown from raises dynet_unknown_node; a top or min_hops that
is not one finite number in range raises dynet_bad_input; and a query
that leaves no route of at least min_hops hops raises
dynet_empty_result. Conditions raised by paths() on the arguments
passed through ... reach the caller unchanged.
paths() for reachability, path_trajectories() for the prefix
tree those routes share.
dn <- dynet(school_contacts) pathways(dn, from = "Ana") pathways(dn, from = c("Ana", "Ben", "Kira"), top = 5)dn <- dynet(school_contacts) pathways(dn, from = "Ana") pathways(dn, from = c("Ana", "Ben", "Kira"), top = 5)
Topological overlap: the share of a vertex's ties that survive from one time bin to the next, normalised by the geometric mean of its degrees in the two bins. Averaged over a vertex's transitions it is that vertex's persistence; averaged again over vertices it is the network's temporal correlation coefficient (Tang et al., 2010).
persistence( dn, scope = c("pertime", "node", "overall"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )persistence( dn, scope = c("pertime", "node", "overall"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )
dn |
A temporal network from |
scope |
|
sessions |
How to treat sessions, as in |
start, end, step, window
|
The measurement grid, as in |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
The measure is defined on undirected neighbourhoods, so a directed network is read as a contact network here: each layer is folded onto its transpose before the overlap is taken. Weights and spell counts are ignored – a pair is tied in a bin or it is not – and loops are excluded.
A vertex isolated in either bin of a transition scores zero rather than a
missing value. That is a substantive claim, not an arithmetic accident: the
ratio is genuinely 0/0, and zero is the convention teneto uses. A reader who
wants isolation and genuine turnover distinguished should read the degree
alongside, which centrality_series() gives.
A dynet_metric. Under scope = "pertime" it is level = "node"
with columns time, node, measure and value, one row per vertex per
transition, where time is the earlier bin of the pair; the final bin
opens no transition and so contributes no rows at all, rather than a row
whose value is structurally undefined. Under scope = "node" it is
level = "node" with node, measure and value, one row per vertex.
Under scope = "overall" it is level = "graph" with measure and
value, one row per session. A leading session column is present under
sessions = "separate". measure is "topological_overlap",
"average_topological_overlap" and "temporal_correlation" respectively,
and value lies in [0, 1] at every scope. Print it, summary() it,
plot() it, or take the plain frame with as.data.frame().
Errors: dynet_empty_result when the grid yields fewer than two bins, since
a single bin opens no transition and the measure is undefined;
dynet_no_sessions when sessions = "separate" is asked of a network with
no session column; and dynet_bad_input when dn is not a dynet.
Tang, J., Scellato, S., Musolesi, M., Mascolo, C., & Latora, V. (2010). Small-world behavior in time-varying graphs. Physical Review E, 81(5), 055101(R). doi:10.1103/PhysRevE.81.055101
Nicosia, V., Tang, J., Mascolo, C., Musolesi, M., Russo, G., & Latora, V. (2013). Graph metrics for temporal networks. In P. Holme & J. Saramaki (Eds.), Temporal Networks (pp. 15-40). Springer. doi:10.1007/978-3-642-36461-7_2
Clauset, A., & Eagle, N. (2007). Persistence and periodicity in a dynamic proximity network. DIMACS Workshop on Computational Methods for Dynamic Interaction Networks.
similarity(), which compares whole edge sets between every pair
of bins and answers a different question, and turnover() for the
complementary view of what changed.
dn <- dynet(school_contacts) kept <- persistence(dn) kept per_node <- persistence(dn, scope = "node") per_node overall <- persistence(dn, scope = "overall") overalldn <- dynet(school_contacts) kept <- persistence(dn) kept per_node <- persistence(dn, scope = "node") per_node overall <- persistence(dn, scope = "overall") overall
A similarity heatmap invites the question "so how many regimes are there, and where does each begin". This answers it: cluster the bins by how alike their networks are, and report the blocks.
It is the other way of thinking about temporal structure from
temporal_communities(). That one asks which vertices group together and
lets the grouping change over time; this one asks which moments group
together and does not look at vertices at all.
phases( dn, k = NULL, method = c("jaccard", "overlap", "hamming", "cosine", "pearson"), linkage = c("ward.D2", "average", "complete"), contiguous = TRUE, k_max = 10L, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL )phases( dn, k = NULL, method = c("jaccard", "overlap", "hamming", "cosine", "pearson"), linkage = c("ward.D2", "average", "complete"), contiguous = TRUE, k_max = 10L, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL )
dn |
A temporal network from |
k |
Number of phases. |
method |
How to compare two bins, passed to |
linkage |
Agglomeration method for |
contiguous |
|
k_max |
Largest number of phases to consider when |
sessions |
How to treat sessions. |
start, end
|
First and last bin times. |
step |
Spacing between bin starts. |
window |
Width represented by each bin. |
The distance between two bins is 1 - similarity for every method except
"hamming", which already reports disagreement and is instead divided by
the number of vertex pairs that could carry a tie. Getting that one
backwards would silently invert the answer, so it is handled explicitly.
"pearson" similarity can be negative, so its distance runs in
rather than ; that is a valid distance, but do not
expect the unit scale.
With contiguous = TRUE the bins are partitioned by Fisher's (1958)
dynamic program, which minimises total within-phase scatter over all
contiguous partitions. That is exact and deterministic. Cutting a
dendrogram is neither: it is a greedy agglomeration and it can put bin 3
and bin 40 in a phase that bin 20 is not in, which is not a regime.
Nothing here is random. temporal_communities() needs seeds because
modularity maximisation is a heuristic on a near-degenerate landscape; this
verb needs none because both of its steps are exact. Two calls return
identical results and the caller's random stream is never touched. The
difference is deliberate.
For the Bayesian changepoint approach to the same question – a different
paradigm, with MCMC and its own diagnostics – see the NetworkChange
package, which this deliberately does not reimplement.
A dynet_phases frame, one row per time bin: session (only when
the network has sessions), time, phase, boundary (is this the first
bin of its phase) and silhouette (this bin's width under the chosen
k). Print it, plot() it, or take the plain frame with
as.data.frame(), which also serves what = "profile" for the
-by- sensitivity table and what = "phases" for one row
per phase.
Fisher, W. D. (1958). On grouping for maximum homogeneity. Journal of the American Statistical Association, 53(284), 789-798.
Rousseeuw, P. J. (1987). Silhouettes: a graphical aid to the interpretation and validation of cluster analysis. Journal of Computational and Applied Mathematics, 20, 53-65.
Murtagh, F., & Legendre, P. (2014). Ward's hierarchical agglomerative clustering method: which algorithms implement Ward's criterion? Journal of Classification, 31, 274-295.
Lucas, M., Morris, A., Townsend-Teague, A., Tichit, L., Habermann, B. H., & Barrat, A. (2023). Inferring cell cycle phases from a partially temporal network of protein interactions. Cell Reports Methods, 3(3), 100397.
Park, J. H., & Sohn, Y. (2020). Detecting structural changes in longitudinal network data. Bayesian Analysis, 15(1), 133-157.
similarity(), whose matrix this clusters, and
temporal_communities() for the vertex-side question.
dn <- dynet(school_contacts) phases(dn, step = 2, window = 2) as.data.frame(phases(dn, step = 2, window = 2), what = "profile")dn <- dynet(school_contacts) phases(dn, step = 2, window = 2) as.data.frame(phases(dn, step = 2, window = 2), what = "profile")
Draws the optimal route family returned by paths() using the
trajectory-tree grammar ported from the transitiontrees package: leaves
stacked in depth-first order, parents centred on their children, and
branches carried by a cosine smoothstep. Nodes follow that package's
horizontal phylogram rather than its capsule style – a count-sized filled
circle with its label set below it. Node size and branch width always show
how many optimal routes use a branch. Every node also prints the value of
the chosen measure beside its vertex name, so nothing is encoded by
colour alone: the default frequency view is free to fill each node with its
vertex's own colour, while the "time" and "predictability" views fill
from a ramp with a colour bar.
Forward paths grow away from the queried source. Backward paths are flipped so the queried target is the root and possible senders branch away from it. A named vertex repeats whenever it is reached under a different temporal history.
plot_path_trajectories( x, measure = c("frequency", "time", "predictability"), orientation = c("horizontal", "vertical"), min_count = 1L, base_size = 11, palette = "okabe" )plot_path_trajectories( x, measure = c("frequency", "time", "predictability"), orientation = c("horizontal", "vertical"), min_count = 1L, base_size = 11, palette = "okabe" )
x |
A result from |
measure |
What each node reports, and what fills it. |
orientation |
|
min_count |
Draw only branches used by at least this many optimal
routes. Defaults to |
base_size |
Base text size, as in |
palette |
Palette for the vertex colours of the frequency view, as in
|
A ggplot object. A tree with no branch to draw raises
dynet_empty_result; an x that is neither a paths() nor a
path_trajectories() result, or a base_size that is not one positive
number, raises dynet_bad_input.
path_trajectories() for the tidy tree behind the plot.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") plot_path_trajectories(routes) plot_path_trajectories(routes, measure = "time", orientation = "vertical")dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") plot_path_trajectories(routes) plot_path_trajectories(routes, measure = "time", orientation = "vertical")
Nine views, each answering a different question.
"events"Every contact as a link drawn at the moment it fires, with actors on the vertical axis. A link leaves its source in the source's colour and arrives in the target's.
"timeline"Edge activity as an intensity heatmap, one row per pair. This is the view a static network cannot give you: it shows at a glance whether the network was busy throughout or concentrated in a few bursts.
"activity"Edges forming and dissolving over time.
"network"The network as a node-link diagram, drawn by
cograph::splot(). With no at, the whole window is flattened into one
picture – useful as a reference point, and as a reminder of how much it
overstates, since every tie appears simultaneous. With at, only that
time bin is drawn.
"snapshots"Small multiples, one cograph::splot() per time
bin, laid out on shared coordinates so positions are comparable across
panels.
"layers"The multilayer view: one network per time slice, drawn
as a stack of layers in which each vertex appears once per slice and is
joined to its own copy in the next by an identity arc of weight
omega.
"heatmap"The matrix counterpart of "layers": each slice is a
tilted heatmap plane rather than a node-link diagram.
"stack"The same slices projected as a node-link stack, each vertex keeping one colour through the whole stack so it can be followed between planes.
"proximity"Vertices placed on a vertical line at each time point according to how close they are in the network, and joined through time. Clusters appear as bands of lines travelling together.
All node-link rendering is cograph's. A dynet object is a cograph
netobject, so cograph::splot(dn) works directly and every one of its
rendering arguments is available here through ....
## S3 method for class 'dynet' plot( x, type = c("timeline", "events", "activity", "network", "snapshots", "proximity", "layers", "heatmap", "stack"), at = NULL, start = NULL, end = NULL, top = 40L, step = NULL, omega = 1, bins = NULL, link = c("hook", "arc", "chevron", "wave", "bracket"), time = c("bin", "event", "clock"), aggregate = TRUE, nest = c("pair", "column"), split = 0.8, blend = FALSE, weight = TRUE, node_size = NULL, node_shape = NULL, node_fill = NULL, node_border_color = NULL, node_border_width = NULL, node_alpha = NULL, edge_color = NULL, edge_alpha = NULL, edge_width = NULL, edge_width_range = NULL, edge_style = NULL, edge_start_style = NULL, edge_start_length = NULL, curvature = NULL, curve_pivot = NULL, label_size = NULL, label_color = NULL, label_fontface = NULL, panels = 9L, measure = "degree", phases = NULL, networks = TRUE, events = TRUE, labels = TRUE, highlight = NULL, slices = 120L, window = NULL, flow = 2L, palette = "okabe", default_dist = 2, base_size = 12, style = .dyn_style(), ... )## S3 method for class 'dynet' plot( x, type = c("timeline", "events", "activity", "network", "snapshots", "proximity", "layers", "heatmap", "stack"), at = NULL, start = NULL, end = NULL, top = 40L, step = NULL, omega = 1, bins = NULL, link = c("hook", "arc", "chevron", "wave", "bracket"), time = c("bin", "event", "clock"), aggregate = TRUE, nest = c("pair", "column"), split = 0.8, blend = FALSE, weight = TRUE, node_size = NULL, node_shape = NULL, node_fill = NULL, node_border_color = NULL, node_border_width = NULL, node_alpha = NULL, edge_color = NULL, edge_alpha = NULL, edge_width = NULL, edge_width_range = NULL, edge_style = NULL, edge_start_style = NULL, edge_start_length = NULL, curvature = NULL, curve_pivot = NULL, label_size = NULL, label_color = NULL, label_fontface = NULL, panels = 9L, measure = "degree", phases = NULL, networks = TRUE, events = TRUE, labels = TRUE, highlight = NULL, slices = 120L, window = NULL, flow = 2L, palette = "okabe", default_dist = 2, base_size = 12, style = .dyn_style(), ... )
x |
A temporal network from |
type |
One of |
at |
For |
start, end
|
Window the plot to |
top |
For the timeline, draw only the |
step |
Width of one time bin, in the network's time unit. For
|
omega |
For |
bins |
Number of equal time bins for |
link |
Link glyph for |
time |
Time axis for |
aggregate |
For |
nest |
For |
split |
For |
blend |
For |
weight |
For |
node_size, node_shape, node_fill, node_border_color, node_border_width, node_alpha
|
Node aesthetics, named as in |
edge_color, edge_alpha, edge_width, edge_width_range, edge_style
|
Link
aesthetics, named as in |
edge_start_style, edge_start_length
|
How the origin of each link is
marked, named as in |
curvature, curve_pivot
|
Bow geometry, as in |
label_size, label_color, label_fontface
|
Axis label aesthetics, named as
in |
panels |
For snapshots, the maximum number of panels to draw, 9 by default. Bins are sampled evenly across the window and the choice is reported. |
measure |
For the proximity view, the node-level measure that line
thickness follows, |
phases |
For the proximity view, how many phases to split the window
into for the network panels. |
networks |
Whether the proximity view draws a network panel per phase,
|
events |
Whether the proximity view marks the times edges formed,
|
labels |
Whether vertices are named, |
highlight |
Vertex names to draw in colour in the proximity view, with
the rest in grey. |
slices |
How many times the proximity view measures the network across
the window, 120 by default. Smoothness comes from measuring often, never
from interpolation. |
window |
Width of each proximity slice. |
flow |
How many corner-cutting passes round each proximity line, 2 by
default. Rounding only ever takes convex combinations of measurements, so
it softens the joints without letting the curve overshoot one. |
palette |
Colours for vertices and lines: |
default_dist |
Distance assumed between vertices with no path between
them, in the proximity view. |
base_size |
Base font size for the |
style |
Style constants for the proximity view's base-graphics panel:
a list holding |
... |
Passed to the renderer the chosen view uses: |
The node-link views set a few of cograph::splot()'s defaults before
handing over: they draw no edge labels and no edge-colour legend
(legend_edge_colors = FALSE, against cograph::splot()'s own TRUE),
colour edges a neutral grey, and size nodes and arrowheads from the vertex
count. Naming any of those through ... overrides it.
Failures are classed conditions. dynet_bad_input covers every malformed
argument, dynet_unknown_plot_arg a name in ... no view can read,
dynet_bad_palette an unusable palette, and dynet_empty_result a
window, an at or a step that leaves nothing to draw. The proximity view
adds dynet_unknown_measure and dynet_needs_directed. With cograph
absent, the node-link views raise dynet_needs_cograph and the
"layers", "heatmap" and "stack" views dynet_missing_package.
For "timeline", "events", "activity" and "heatmap", a
ggplot object, which prints itself when the call is not assigned. For
"network", "snapshots", "layers", "stack" and "proximity", the
figure is drawn on the current device and the network is returned
invisibly – x itself, or the windowed network when start or end
was given.
Okabe, M., & Ito, K. (2008). Color universal design: how to make figures and presentations that are friendly to colorblind people.
Chaikin, G. M. (1974). An algorithm for high-speed curve generation. Computer Graphics and Image Processing, 3(4), 346-349.
dn <- dynet(school_contacts) plot(dn) plot(dn, type = "proximity") plot(dn, type = "proximity", measure = "betweenness", phases = 4) plot(dn, type = "network", node_fill = "#56B4E9")dn <- dynet(school_contacts) plot(dn) plot(dn, type = "proximity") plot(dn, type = "proximity", measure = "betweenness", phases = 4) plot(dn, type = "network", node_fill = "#56B4E9")
The union of a network's ties over a window, as a node-link diagram with
Dynet's rendering defaults; any cograph::splot() argument overrides them.
## S3 method for class 'dynet_collapsed' plot(x, palette = "okabe", ...)## S3 method for class 'dynet_collapsed' plot(x, palette = "okabe", ...)
x |
A result from |
palette |
Palette specification, as in |
... |
Passed to |
x, invisibly.
dn <- dynet(school_contacts) flat <- collapse_network(dn) plot(flat, layout = "oval")dn <- dynet(school_contacts) flat <- collapse_network(dn) plot(flat, layout = "oval")
One band per vertex per bin, stacked by community, so a community reads as a block and a vertex changing community reads as a band crossing between blocks. Communities are labelled directly on the plot as well as coloured, because colour alone is not a channel every reader has.
## S3 method for class 'dynet_communities' plot(x, base_size = 12, ...)## S3 method for class 'dynet_communities' plot(x, base_size = 12, ...)
x |
A |
base_size |
Base text size. |
... |
Ignored. |
A ggplot object.
type = "storyline", the default, draws the event graph as a storyline
(Tanahashi and Ma, 2012): every actor
is a line through time, and every event is a column that gathers the lines
of its two endpoints in a light crimson capsule. The stretch of an actor's
line between two of its events is solid when the event graph joins them
through that actor, so something the actor received in the first could be
passed on in the second, and dashed when it does not, because the wait
exceeds delta or the direction of the events forbids it. On a directed
network an arrow in each column runs from the source to the target.
## S3 method for class 'dynet_event_graph' plot( x, type = c("storyline", "events"), top = 8L, start = NULL, end = NULL, rows = c("actor", "chain"), color_by = NULL, labels = FALSE, palette = "okabe", node_size = 2.5, node_shape = c(16, 17, 15, 18, 8, 4, 3, 1, 0), line_width = 0.9, line_alpha = 1, relay_style = "solid", no_relay_style = "22", capsule_color = "#DC143C", capsule_alpha = 0.16, capsule_width = 6, arrows = TRUE, arrow_color = "grey40", arrow_width = 0.4, arrow_size = 0.12, base_size = 12, label_size = 7, ... )## S3 method for class 'dynet_event_graph' plot( x, type = c("storyline", "events"), top = 8L, start = NULL, end = NULL, rows = c("actor", "chain"), color_by = NULL, labels = FALSE, palette = "okabe", node_size = 2.5, node_shape = c(16, 17, 15, 18, 8, 4, 3, 1, 0), line_width = 0.9, line_alpha = 1, relay_style = "solid", no_relay_style = "22", capsule_color = "#DC143C", capsule_alpha = 0.16, capsule_width = 6, arrows = TRUE, arrow_color = "grey40", arrow_width = 0.4, arrow_size = 0.12, base_size = 12, label_size = 7, ... )
x |
An event graph returned by |
type |
|
top |
The number of actors drawn, those in the most events (ties by
name); |
start, end
|
Draw only events starting in this period. Default to the whole network. |
rows |
For |
color_by |
For |
labels |
For |
palette |
Actor line colours, or under |
node_size |
Size of the points where a line meets its events; |
node_shape |
Point shapes, recycled over the actors (or the
|
line_width, line_alpha
|
Width and opacity of the actor lines, or of
the arcs under |
relay_style, no_relay_style
|
Line types of a stretch that is an
adjacency of the event graph and of one that is not; |
capsule_color, capsule_alpha, capsule_width
|
Colour, opacity and width
of the capsule gathering the endpoints of an event; a light crimson
( |
arrows |
Whether to draw the source-to-target arrow in each column of
a directed network; |
arrow_color, arrow_width, arrow_size
|
Colour, line width and head
length (in centimetres) of those arrows; |
base_size |
Base font size of the theme; |
label_size |
Size, in points, of the time labels under the columns,
or of the |
... |
Ignored. |
Columns are events in time order, spaced by order rather than by elapsed
time, and labelled with their start. Lines are ordered by the barycentre
rule (Sugiyama et al., 1981) over repeated sweeps, keeping the ordering
with the fewest crossings, as in the storyline of the hypergraphs
package. Each actor has its own colour and point shape, so no actor is
told by colour alone.
type = "events" draws the events themselves as the vertices. Each event
is a point at its start time, and each adjacency is a line from the
earlier event to the later one. With rows = "actor", the default, every
source has its own row, in the order of its first event, so a line runs
from the row of the actor whose event was taken further to the row of the
actor who took it. With rows = "chain", events are grouped into relay
chains, the weakly connected components of the event graph among the
events drawn, every chain has its own row ordered by its first event, and
adjacencies are arcs above the row; a row of one point is an event that
relays nothing and continues nothing within delta. color_by colours
and shapes the points by a column of the event table, such as a tie
attribute. An event graph of messages (events = "messages") is drawn
this way by default, because a message gathers several actors at once
and has no storyline.
A ggplot object, so titles, themes and scales can be added with
+. Raises dynet_bad_input for a malformed top, start, end,
color_by, labels or styling argument, dynet_bad_palette for an unusable palette,
capsule_color or arrow_color, and dynet_empty_result when no event
starts in the period.
Tanahashi, Y., & Ma, K.-L. (2012). Design considerations for optimizing storyline visualizations. IEEE Transactions on Visualization and Computer Graphics, 18(12), 2679-2688. doi:10.1109/TVCG.2012.212
Sugiyama, K., Tagawa, S., & Toda, M. (1981). Methods for visual understanding of hierarchical system structures. IEEE Transactions on Systems, Man, and Cybernetics, 11(2), 109-125. doi:10.1109/TSMC.1981.4308636
dn <- dynet(data.frame( from = c("A", "B", "C", "A", "B"), to = c("B", "C", "D", "C", "D"), time = c(1, 2, 3, 4, 5) )) eg <- event_graph(dn, delta = 1.5) plot(eg) plot(eg, type = "events", labels = TRUE)dn <- dynet(data.frame( from = c("A", "B", "C", "A", "B"), to = c("B", "C", "D", "C", "D"), time = c(1, 2, 3, 4, 5) )) eg <- event_graph(dn, delta = 1.5) plot(eg) plot(eg, type = "events", labels = TRUE)
Draws the quantity against time. Node-level measures are drawn as one line per vertex; graph-level measures as one line per measure. Distinctions are carried by colour and line type together, never by colour alone.
## S3 method for class 'dynet_metric' plot( x, type = c("line", "heatmap", "ridge"), highlight = NULL, top = NULL, palette = "okabe", base_size = 12, ... )## S3 method for class 'dynet_metric' plot( x, type = c("line", "heatmap", "ridge"), highlight = NULL, top = NULL, palette = "okabe", base_size = 12, ... )
x |
A |
type |
|
highlight |
Optional character vector naming the series to draw in
colour, with everything else in grey: vertex names for a node-level
measure, measure names for a graph-level one. For a |
top |
How many rows to draw. For a measure taken over time, the |
palette |
Colours for the series: |
base_size |
Base font size. |
... |
Ignored. |
A measure with no time axis, such as reachability or durations(), has no
trajectory to draw and is shown as a bar panel instead, one bar per vertex
or pair and one facet per measure. type and highlight have nothing to
act on there and are ignored.
A ggplot object. Drawing happens when that object is printed, so
the plot is the return value here rather than a side effect.
dn <- dynet(school_contacts) degree <- centrality_series(dn, measure = "degree") plot(degree, top = 5) plot(degree, palette = "extended")dn <- dynet(school_contacts) degree <- centrality_series(dn, measure = "degree") plot(degree, top = 5) plot(degree, palette = "extended")
Plot a temporal motif census
## S3 method for class 'dynet_motifs' plot(x, ...)## S3 method for class 'dynet_motifs' plot(x, ...)
x |
A |
... |
Passed to |
x, invisibly; a bar plot is drawn as a side effect.
Plot the surrogate activity profile against the observed one
## S3 method for class 'dynet_null' plot(x, ...)## S3 method for class 'dynet_null' plot(x, ...)
x |
A |
... |
Ignored. |
A ggplot object showing events per time bin for each surrogate
with the observed profile overplotted.
The hops used by a set of optimal temporal paths, as a node-link diagram
with Dynet's rendering defaults; any cograph::splot() argument overrides
them.
## S3 method for class 'dynet_path_network' plot(x, palette = "okabe", ...)## S3 method for class 'dynet_path_network' plot(x, palette = "okabe", ...)
x |
A result from |
palette |
Palette specification, as in |
... |
Passed to |
x, invisibly.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") route_network <- path_network(routes) plot(route_network, layout = "oval")dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") route_network <- path_network(routes) plot(route_network, layout = "oval")
A method wrapper so plot() works on the result directly. It draws exactly
what plot_path_trajectories() draws; that function remains the place where
the appearance arguments are documented.
## S3 method for class 'dynet_path_trajectories' plot(x, ...)## S3 method for class 'dynet_path_trajectories' plot(x, ...)
x |
A |
... |
Passed to |
A ggplot object.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) plot(trajectories)dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) plot(trajectories)
A result from paths() records the optimality criterion it was found
under, and such a result is drawn as a trajectory tree by
plot_path_trajectories(). That is the right picture for an
endpoint-local family: the routes need not share prefix-optimal
subpaths, so a vertex reached under two different temporal histories
appears twice rather than being forced into one predecessor tree.
Older serialised results carry no criterion. Those are drawn by the legacy
predecessor-tree renderer, which only defines a tree when the sessions were
collapsed; a bounded or separate-session result of that vintage raises
dynet_unsupported_plot instead of implying a tree the criterion never
promised.
## S3 method for class 'dynet_paths' plot(x, palette = "okabe", ...)## S3 method for class 'dynet_paths' plot(x, palette = "okabe", ...)
x |
A |
palette |
Palette specification, as in |
... |
Passed to |
A ggplot object for a result carrying criterion metadata, which
is every result paths() returns. A legacy result without it is drawn on
the current device by cograph::splot() and x is returned invisibly.
A legacy result raises dynet_unsupported_plot when its sessions were
not collapsed, dynet_empty_result when the source reaches no other
vertex, dynet_bad_palette for an unusable palette, and
dynet_needs_cograph when cograph is not installed.
dn <- dynet(school_contacts) journeys <- paths(dn, from = "Ana") plot(journeys)dn <- dynet(school_contacts) journeys <- paths(dn, from = "Ana") plot(journeys)
One row per route, drawn along real time: a point at each vertex placed at the moment the route reaches it, joined by a segment. The horizontal gap between two points is the waiting time at that vertex, so the drawing is time-respecting in the way a bar of a route string is not – a route that waits is visibly slower than one that does not, and where routes arrive relative to each other is read off the axis.
## S3 method for class 'dynet_pathways' plot(x, top = 12L, labels = TRUE, base_size = 12, ...)## S3 method for class 'dynet_pathways' plot(x, top = 12L, labels = TRUE, base_size = 12, ...)
x |
A |
top |
Number of routes to draw, most frequent first. Defaults to twelve, which keeps the vertex labels legible. |
labels |
Whether to name the vertex at each step. |
base_size |
Base font size, as in |
... |
Ignored. |
Routes are ordered by frequency, most used at the top, with the count and share to the right of each. Fill marks the endpoint, which the row already names, so colour never carries a distinction alone.
A ggplot object. A top, labels or base_size that is not one
valid value raises dynet_bad_input.
dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") plot(routes)dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") plot(routes)
The heatmap plot.dynet_similarity() draws, with the phase blocks outlined
on it, so the reader can see whether the blocks the algorithm found are the
blocks the eye finds.
## S3 method for class 'dynet_phases' plot(x, base_size = 12, ...)## S3 method for class 'dynet_phases' plot(x, base_size = 12, ...)
x |
A |
base_size |
Base text size. |
... |
Ignored. |
A ggplot object.
One horizontal bar per shift type, grouped and coloured by family. Families are distinguished by a direct axis grouping as well as by fill, so the figure does not rely on colour alone.
## S3 method for class 'dynet_pshifts' plot(x, ...)## S3 method for class 'dynet_pshifts' plot(x, ...)
x |
A |
... |
Ignored. |
A ggplot object.
dn <- dynet(school_contacts) shifts <- pshifts(dn) plot(shifts)dn <- dynet(school_contacts) shifts <- pshifts(dn) plot(shifts)
Plot a permutation test
## S3 method for class 'dynet_significance' plot(x, type = c("null", "series", "z"), ...)## S3 method for class 'dynet_significance' plot(x, type = c("null", "series", "z"), ...)
x |
A |
type |
|
... |
Ignored. |
A ggplot object.
Draw time-bin similarity as a heatmap
## S3 method for class 'dynet_similarity' plot(x, base_size = 12, ...)## S3 method for class 'dynet_similarity' plot(x, base_size = 12, ...)
x |
A result from |
base_size |
Base text size. Defaults to twelve. |
... |
Ignored. |
A ggplot object. Drawing happens when that object is printed, so
the plot is the return value here rather than a side effect.
dn <- dynet(school_contacts) bin_similarity <- similarity(dn, step = 5, window = 5) plot(bin_similarity)dn <- dynet(school_contacts) bin_similarity <- similarity(dn, step = 5, window = 5) plot(bin_similarity)
Plot how many ties each snapshot holds
## S3 method for class 'dynet_snapshot' plot(x, base_size = 12, palette = "okabe", ...)## S3 method for class 'dynet_snapshot' plot(x, base_size = 12, palette = "okabe", ...)
x |
A |
base_size |
Base font size. |
palette |
Palette specification, as in |
... |
Ignored. |
A ggplot object, faceted by session when the result carries one.
A result in which no tie is active raises an error of class
dynet_empty_result rather than drawing an empty panel.
dn <- dynet(school_contacts) bins <- snapshots(dn) plot(bins)dn <- dynet(school_contacts) bins <- snapshots(dn) plot(bins)
Print a temporal network
## S3 method for class 'dynet' print(x, ...)## S3 method for class 'dynet' print(x, ...)
x |
A temporal network from |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) dndn <- dynet(school_contacts) dn
Print an animation's bin table
## S3 method for class 'dynet_animation' print(x, n = 12L, ...)## S3 method for class 'dynet_animation' print(x, n = 12L, ...)
x |
A |
n |
Largest number of bins to list, |
... |
Ignored. |
x, invisibly.
if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn, step = 6, window = 6, tween = 2) print(frames, n = 3) }if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn, step = 6, window = 6, tween = 2) print(frames, n = 3) }
Print a collapsed temporal network
## S3 method for class 'dynet_collapsed' print(x, ...)## S3 method for class 'dynet_collapsed' print(x, ...)
x |
A network returned by |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) collapsed <- collapse_network(dn) collapseddn <- dynet(school_contacts) collapsed <- collapse_network(dn) collapsed
Print session-specific collapsed networks
## S3 method for class 'dynet_collapsed_list' print(x, ...)## S3 method for class 'dynet_collapsed_list' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly.
dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 0), end = c(2, 3), session = c("s1", "s2") ), session = "session") collapse_network(dn, sessions = "separate")dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 0), end = c(2, 3), session = c("s1", "s2") ), session = "session") collapse_network(dn, sessions = "separate")
Print a temporal community partition
## S3 method for class 'dynet_communities' print(x, n = 12L, ...)## S3 method for class 'dynet_communities' print(x, n = 12L, ...)
x |
A |
n |
Number of rows to show. |
... |
Ignored. |
x, invisibly. Called for the printed output.
Print an event graph
## S3 method for class 'dynet_event_graph' print(x, ...)## S3 method for class 'dynet_event_graph' print(x, ...)
x |
An event graph returned by |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) event_graph(dn, delta = 1)dn <- dynet(school_contacts) event_graph(dn, delta = 1)
Print a temporal measure
## S3 method for class 'dynet_metric' print(x, n = 12L, ...)## S3 method for class 'dynet_metric' print(x, n = 12L, ...)
x |
A |
n |
Number of rows to show. Defaults to twelve. |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) degree <- centrality_series(dn, step = 4, window = 4) degree print(degree, n = 4)dn <- dynet(school_contacts) degree <- centrality_series(dn, step = 4, window = 4) degree print(degree, n = 4)
Print a temporal motif census
## S3 method for class 'dynet_motifs' print(x, n = 10L, ...)## S3 method for class 'dynet_motifs' print(x, n = 10L, ...)
x |
A |
n |
Number of rows to show; the non-zero classes come first. |
... |
Ignored. |
x, invisibly.
Print surrogate networks
## S3 method for class 'dynet_null' print(x, ...)## S3 method for class 'dynet_null' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly.
Print a temporal trajectory tree
## S3 method for class 'dynet_path_trajectories' print(x, ...)## S3 method for class 'dynet_path_trajectories' print(x, ...)
x |
A result from |
... |
Passed to the data frame print method. |
x, invisibly. Called for the side effect of printing a header
naming the direction, anchor, node count, depth and number of routes,
followed by the tidy table.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) print(trajectories)dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) print(trajectories)
Print time-respecting paths
## S3 method for class 'dynet_paths' print(x, n = 12L, ...)## S3 method for class 'dynet_paths' print(x, n = 12L, ...)
x |
A |
n |
Number of rows to show. Defaults to twelve. |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") routes print(routes, n = 4)dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") routes print(routes, n = 4)
Print ranked pathways
## S3 method for class 'dynet_pathways' print(x, n = 12L, ...)## S3 method for class 'dynet_pathways' print(x, n = 12L, ...)
x |
A |
n |
Number of routes to show. Defaults to twelve. |
... |
Ignored. |
x, invisibly. Called for the side effect of printing a header
giving the number of distinct routes and of optimal routes counted,
followed by the first n rows.
dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") print(routes)dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") print(routes)
Print detected temporal phases
## S3 method for class 'dynet_phases' print(x, ...)## S3 method for class 'dynet_phases' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly. Called for the printed output.
Print a time-projected network
## S3 method for class 'dynet_projection' print(x, ...)## S3 method for class 'dynet_projection' print(x, ...)
x |
A projection returned by |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) projected <- projection(dn, step = 4, window = 4) projecteddn <- dynet(school_contacts) projected <- projection(dn, step = 4, window = 4) projected
Print participation shift counts
## S3 method for class 'dynet_pshifts' print(x, ...)## S3 method for class 'dynet_pshifts' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) shifts <- pshifts(dn) shiftsdn <- dynet(school_contacts) shifts <- pshifts(dn) shifts
Print a permutation test
## S3 method for class 'dynet_significance' print(x, ...)## S3 method for class 'dynet_significance' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly.
Print time-bin similarity
## S3 method for class 'dynet_similarity' print(x, ...)## S3 method for class 'dynet_similarity' print(x, ...)
x |
A result from |
... |
Passed to the data frame print method. |
x, invisibly.
dn <- dynet(school_contacts) resemblance <- similarity(dn, step = 4, window = 4) resemblancedn <- dynet(school_contacts) resemblance <- similarity(dn, step = 4, window = 4) resemblance
Print snapshot edges
## S3 method for class 'dynet_snapshot' print(x, n = 10L, ...)## S3 method for class 'dynet_snapshot' print(x, n = 10L, ...)
x |
A |
n |
Number of rows to show; ten by default. |
... |
Ignored. |
x, invisibly.
dn <- dynet(school_contacts) bins <- snapshots(dn, at = 3) binsdn <- dynet(school_contacts) bins <- snapshots(dn, at = 3) bins
projection() discretises a temporal network into snapshot slices and
connects each vertex state to its realisation in the next slice. Within a
slice it uses the same independently aggregated, endpoint-induced snapshot
as snapshots(). Identity arcs always point forward and carry the coupling
weight omega. The result is a tidy projection object rather than a bare
matrix.
projection( dn, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, omega = 1, coupling = c("ordinal", "categorical") )projection( dn, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, omega = 1, coupling = c("ordinal", "categorical") )
dn |
A temporal network from |
sessions |
Session handling: |
start, end
|
First and last slice times. Defaults to observed support. |
step |
Spacing between slice starts. |
window |
Width represented by each slice. |
omega |
Weight on the identity arcs that carry a vertex from one slice
to the next, that is, the interlayer coupling of the time-expanded
network. One, the default, keeps an identity arc as heavy as a unit
contact; zero leaves the slices uncoupled. Must be a single finite
non-negative number, or a |
coupling |
Which slices an identity arc may join. |
Every fixed-universe vertex receives one state in every emitted slice.
active records whether the vertex was eligible in that slice. Identity
arcs are retained through inactive slices because Dynet permits waiting
through vertex inactivity; inactive states have no incident
endpoint-induced within-slice edge. Consecutive observed slices are also
linked across an observation gap, matching Dynet's calendar-time waiting
convention.
Directed source edges produce one within-slice arc. An undirected nonloop
edge produces reciprocal arcs, while an undirected loop is emitted once.
Parallel active spells are one within-slice pair whose weight is their
summed weight and whose n_spells records their count. Identity arcs have
weight = omega and n_spells = 0, and meta$identity_weight reports the
same value.
An object of class dynet_projection. Use
as.data.frame(x, what = "vertices") for vertex states and
as.data.frame(x, what = "edges") for directed projected arcs.
Bender-deMoll, S., & Moody, J. timeProjectedNetwork() in the tsna
package, version 0.3.6.
Butts, C. T., Leslie-Cook, A., Krivitsky, P. N., & Bender-deMoll, S. (2024). networkDynamic: Dynamic Extensions for Network Objects, version 0.11.5. doi:10.32614/CRAN.package.networkDynamic
dn <- dynet(data.frame( from = c("A", "B", "C"), to = c("B", "C", "A"), start = 0:2, end = 1:3 ), observation_start = 0, observation_end = 3) projected <- projection(dn, step = 1, window = 1) as.data.frame(projected, what = "vertices") as.data.frame(projected, what = "edges")dn <- dynet(data.frame( from = c("A", "B", "C"), to = c("B", "C", "A"), start = 0:2, end = 1:3 ), observation_start = 0, observation_end = 3) projected <- projection(dn, step = 1, window = 1) as.data.frame(projected, what = "vertices") as.data.frame(projected, what = "edges")
Gibson participation shifts from raw temporal turns
pshifts( dn, sessions = c("bounded", "collapse", "separate"), output = c("final", "cumulative"), start = NULL, end = NULL, group_events = c("simultaneous", "none"), plot = FALSE )pshifts( dn, sessions = c("bounded", "collapse", "separate"), output = c("final", "cumulative"), start = NULL, end = NULL, group_events = c("simultaneous", "none"), plot = FALSE )
dn |
A directed temporal network from |
sessions |
Session aggregation policy: |
output |
|
start, end
|
Optional inclusive query limits; each query is a fresh sequence and never uses a predecessor outside the range. A network built from dates may be addressed with dates. |
group_events |
Infer one group-directed turn from simultaneous distinct
recipients ( |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
Only uncensored raw spell onsets inside the observed query and
observation components are turns; duration, weights, fragments and
terminus censoring are ignored. Consecutive turns are classified using
Gibson's fixed thirteen labels. Session and component walls, loops, ties,
duplicate multiplicity, and simultaneous-recipient group inference are
retained in metadata. output = "final" emits one typed row per class;
output = "cumulative" emits the running class vector for each turn.
A dynet_pshifts data frame whose shape follows output, carrying
the measure/value pair every other measurement verb returns, so a
participation-shift census composes with the verbs that consume one;
measure is the constant "count" and value is the integer count.
"final" gives one row per shift class – thirteen rows, always all
thirteen even when a class never occurred – with columns shift (the
Gibson label), family (the label's group), measure and value.
"cumulative" gives one row per turn and class, that is thirteen rows
per classified turn, with sequence and event locating the turn in its
sequence, time, speaker, target and group describing the turn,
and shift, family, measure and value carrying the running total
of that class up to and including the turn. Either shape gains a leading
session column under sessions = "separate", which reports each
session on its own rows; "bounded" and "collapse" carry no session
column. Print it, summary() it, plot() it, or take the plain frame
with as.data.frame().
Errors: dynet_needs_directed (an undirected network; the class vector is
c("dynet_needs_directed", "dynet_bad_input")), dynet_no_sessions
(sessions = "separate" without a session column),
dynet_outside_observation (the requested range misses observed support;
it also carries dynet_bad_input),
and dynet_bad_input for every other broken contract – dn not a
dynet, and a start or end that is not a single finite time. An
unmatched sessions, output or group_events is rejected by
match.arg() and is a plain error, not a classed one.
Gibson, D. R. (2003). Participation shifts: Order and differentiation in group conversation. Social Forces, 81, 1335–1380. doi:10.1353/sof.2003.0055
dn <- dynet(data.frame( from = c("A", "B"), to = c("B", "A"), start = c(1, 2), end = c(1, 2) )) pshifts(dn)dn <- dynet(data.frame( from = c("A", "B"), to = c("B", "A"), start = c(1, 2), end = c(1, 2) )) pshifts(dn)
Generates a temporal network with a known generating process, so a measure
can be checked against an answer that is known in advance rather than only
against another implementation. A Poisson process has burstiness exactly
zero; a binomial network has expected per-slice density exactly p; a block
network carries its planted partition as a node attribute.
random_dynet( nodes = 20L, times = 20L, model = c("binomial", "poisson", "block", "activation"), p = 0.1, birth = NULL, persist = NULL, rate = 1, blocks = 2L, p_within = 0.3, p_between = 0.02, block_switch = 0, waiting = c("exponential", "weibull", "lognormal"), shape = 1, directed = TRUE, interval = 1, loops = FALSE, seed = NULL )random_dynet( nodes = 20L, times = 20L, model = c("binomial", "poisson", "block", "activation"), p = 0.1, birth = NULL, persist = NULL, rate = 1, blocks = 2L, p_within = 0.3, p_between = 0.02, block_switch = 0, waiting = c("exponential", "weibull", "lognormal"), shape = 1, directed = TRUE, interval = 1, loops = FALSE, seed = NULL )
nodes |
A vertex count, or a character vector of names. A count generates zero-padded names so that lexical and numeric order agree. |
times |
Number of slices for the slice models, or the length of the observation window for the event models. |
model |
|
p |
Per-slice edge probability for |
birth, persist
|
The two-state Markov variant of |
rate |
Mean events per unit time for the event models. |
blocks |
A block count, or one block label per vertex. |
p_within, p_between
|
Per-slice edge probability inside and between blocks. |
block_switch |
Per-slice probability a vertex changes block, so the planted partition drifts. Zero gives a static partition. |
waiting, shape
|
The renewal gap distribution for |
directed, interval, loops
|
Passed through to |
seed |
A single whole number for a reproducible draw, or |
The slice models draw a dyad-by-slice activity matrix and then collapse each maximal run of consecutive active slices into one spell. The generated network therefore has fewer, longer spells than the model has active slices, which is what makes it a spell network rather than a stack of snapshots.
"poisson" and "activation" use exponential waiting times, so they are
genuine renewal processes. teneto's rand_poisson draws integer gaps that
can be zero, which is not a Poisson process and is not numerically
comparable with these.
Reading burstiness off a generated network. burstiness() is a
node-level measure: it pools the events of every dyad incident to a vertex.
Superposing independent renewal processes drives the pooled process toward
Poisson, so a vertex with many partners scores near zero however bursty each
of its links is. Measured here at shape = 0.5, whose per-link burstiness is
0.382: a vertex with one incident process scored 0.36, with three 0.26, with
seven 0.19 and with fifteen 0.14. To calibrate against the per-link value,
generate nodes = 2, directed = FALSE, which gives each vertex exactly one
process. This is a property of the measure, not of the generator.
Weibull waiting has a closed-form burstiness,
with
and , giving 0.381966 at
shape = 0.5, 0 at shape = 1 and -0.313436 at shape = 2. The generated
network approaches these from a finite window; shape = 0.5 converges most
slowly because its gaps are heavy-tailed.
A dynet, so every verb, plot and accessor applies with no special
casing. "binomial" and "block" produce interval networks;
"poisson" and "activation" produce contact networks. For "block"
the planted partition is written into the node table as block, so
mixing(dn, attribute = "block") reads it with no further argument.
Erdos, P., and Renyi, A. (1959). On random graphs I. Publicationes Mathematicae, 6, 290-297.
Holland, P. W., Laskey, K. B., and Leinhardt, S. (1983). Stochastic blockmodels: first steps. Social Networks, 5(2), 109-137.
Goh, K.-I., and Barabasi, A.-L. (2008). Burstiness and memory in complex systems. Europhysics Letters, 81(4), 48002.
Vazquez, A., Oliveira, J. G., Dezso, Z., Goh, K.-I., Kondor, I., and Barabasi, A.-L. (2006). Modeling bursts and heavy tails in human dynamics. Physical Review E, 73, 036127.
randomise() for destroying structure in an observed network
rather than generating it.
random_dynet(nodes = 10, times = 12, model = "binomial", p = 0.2, seed = 1) random_dynet(nodes = 8, times = 50, model = "poisson", rate = 0.5, seed = 1) random_dynet(nodes = 12, times = 15, model = "block", blocks = 2, seed = 1)random_dynet(nodes = 10, times = 12, model = "binomial", p = 0.2, seed = 1) random_dynet(nodes = 8, times = 50, model = "poisson", rate = 0.5, seed = 1) random_dynet(nodes = 12, times = 15, model = "block", blocks = 2, seed = 1)
Produces networks that keep some of the observed structure and destroy the
rest, so that a measured value can be read against what an unstructured
network would have shown. Every measure in this package is a point estimate
until it is compared with a null; significance() does that comparison.
Which structure each method holds fixed is the whole content of the choice,
so print() states it and the @details below tabulate it.
randomise( dn, method = c("times", "timeline", "edges", "targets", "labels", "reversal"), n = 99L, within = c("network", "sender", "session"), transpose = FALSE, swaps = 10, max_tries = 100L, keep = c("networks", "spells"), respect = c("activity", "observation"), seed = NULL )randomise( dn, method = c("times", "timeline", "edges", "targets", "labels", "reversal"), n = 99L, within = c("network", "sender", "session"), transpose = FALSE, swaps = 10, max_tries = 100L, keep = c("networks", "spells"), respect = c("activity", "observation"), seed = NULL )
dn |
A temporal network from |
method |
The null model. |
n |
Number of surrogates. Forced to one for |
within |
Scope a shuffle is confined to: the whole network, within each sender, or within each session. |
transpose |
For |
swaps |
For |
max_tries |
Iteration cap on the self-loop repair in |
keep |
Retain the surrogate networks so |
respect |
Constraints a surrogate must satisfy on a network that
declares them: |
seed |
A single whole number for a reproducible draw, or |
"reversal"Preserves every spell duration, every pair, each pair's event count and all degrees. Destroys the arrow of time.
"times"Preserves the multiset of start and end times, so the activity profile and duration distribution exactly. Destroys the coupling between topology and timing, and each pair's burstiness.
"timeline"Preserves each pair's event sequence intact, so per-link burstiness and memory. Destroys which pair owns which timeline.
"edges"Preserves the aggregate degree sequence, in and out separately when directed. Destroys topology above the degree sequence.
"targets"Preserves each sender's event times. Destroys who was reached, so reciprocity and triadic closure.
"labels"Preserves everything structural: the surrogate is
isomorphic to the original. It is therefore a null for
attribute-dependent quantities only, such as mixing(). Every
structural measure is exactly invariant under it, which makes it a free
correctness test rather than a weak null.
On a network that declares vertex activity or observation spells, an
unconstrained shuffle can place an event while an endpoint is ineligible or
inside an unobserved gap, and the snapshot machinery would then induce it
away, biasing the null downward. respect enforces those constraints by
rejecting and redrawing infeasible spells. The result is a feasible
surrogate rather than a uniform draw from the feasible set; summary()
reports how many proposals were rejected so the constraint's bite is
visible.
A dynet_null data frame with one row per surrogate spell per
replicate and columns replicate, from, to, start, end,
duration and weight, plus session when the network has sessions.
The surrogate networks themselves are carried on the object and are
reached through significance(), never by hand.
Gauvin, L., Genois, M., Karsai, M., Kivela, M., Takaguchi, T., Valdano, E., and Vestergaard, C. L. (2022). Randomized reference models for temporal networks. SIAM Review, 64(4), 763-830.
Holme, P., and Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125.
Maslov, S., and Sneppen, K. (2002). Specificity and stability in topology of protein networks. Science, 296(5569), 910-913.
significance() to turn a null into an interval and a p-value.
dn <- dynet(school_contacts) randomise(dn, method = "times", n = 9, seed = 1) randomise(dn, method = "reversal")dn <- dynet(school_contacts) randomise(dn, method = "times", n = 9, seed = 1) randomise(dn, method = "reversal")
The number or share of other vertices each vertex can reach along time-respecting paths, and the number or share that can reach it. Reachability is the temporal replacement for component membership: in a static network two vertices in the same component reach each other by definition, whereas in a temporal network reach depends on whether the timing lines up.
reachability( dn, direction = c("both", "forward", "backward"), at = NULL, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0, measure = "reach", criterion = c("foremost_then_shortest", "min_hops"), plot = FALSE )reachability( dn, direction = c("both", "forward", "backward"), at = NULL, sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, traversal_time = 0, measure = "reach", criterion = c("foremost_then_shortest", "min_hops"), plot = FALSE )
dn |
A temporal network from |
direction |
|
at |
Forward source-availability time or backward arrival deadline,
defaulting to the beginning or end of each observed period respectively.
Unlike in |
sessions |
How to treat sessions, as in |
start, end
|
Inclusive lower and upper traversal-time bounds. Interval spells remain terminus-exclusive. |
traversal_time |
Nonnegative duration charged for every hop, in the
network's time unit. A calendar network also accepts a scalar |
measure |
One or more of |
criterion |
Which optimisation problem the journeys solve. Reach and
reach count are identical under every criterion, because they depend on
the feasible set rather than on which journey wins; |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
Reachability uses paths() traversal semantics: nondecreasing times,
unlimited waiting, half-open interval spells, and a separate exact timestamp
rule for point events. Positive traversal_time requires interval occupancy
to finish within continuous pair activity and delays a point-trigger arrival.
Declared vertex activity additionally requires active hop endpoints and a
valid anchor, and every vertex is anchored at its own presence: each search
starts at that vertex's first instant inside the window, or its last
instant searching backward, rather than at the window bound. A vertex never
present inside the window reaches nothing, which is reported as zero rather
than as a missing row. Waiting after a valid anchor may cross vertex
inactivity; interval traversal requires both endpoints continuously through
completion, while a delayed point requires the receiver again at completion.
For backward reachability, the resolved end is a common deadline and
latest-departure suprema determine whether a vertex can reach the target.
The canonical start and end bounds apply one closed traversal-time window
to both forward and backward queries.
The source is excluded: a count is the number of distinct other vertices in the reachable set, not the number of journeys. A proportion divides that count by the full network size minus one. It is defined as zero for a singleton network. In separate-session output the same full-network denominator is retained in every session block.
In separate-session output, a session entirely outside a one-sided bound contributes zero-reach rows rather than aborting the complete result. Its missing implicit bound is clamped to the supplied bound, producing the empty journey at that boundary and no eligible hop.
Failures are classed. An unrecognised measure raises
dynet_unknown_measure; a malformed measure, a negative
traversal_time, at combined with start or end, or a window that
cannot hold a journey raises dynet_bad_input; and a window disjoint from
explicit observation raises dynet_outside_observation.
A dynet_metric at node level: a tidy data frame with one row per
vertex per requested measure, columns node, measure and value,
preceded by session under sessions = "separate". Proportion
measures are named forward_reach and backward_reach; counts are named
forward_reach_count and backward_reach_count. as.data.frame()
returns the plain frame.
Holme, P. (2005). Network reachability of real-world contact sequences. Physical Review E, 71(4), 046119.
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125.
# Reachability searches every ordered pair, so the example uses a small # inline network to stay fast. The verb takes any `dynet`. dn <- dynet(data.frame( from = c("A", "B", "C", "A"), to = c("B", "C", "D", "D"), start = c(0, 1, 2, 3), end = c(1, 2, 3, 4) )) reachability(dn) reachability(dn, direction = "forward") reachability(dn, start = 0, end = 2)# Reachability searches every ordered pair, so the example uses a small # inline network to stay fast. The verb takes any `dynet`. dn <- dynet(data.frame( from = c("A", "B", "C", "A"), to = c("B", "C", "D", "D"), start = c(0, 1, 2, 3), end = c(1, 2, 3, 4) )) reachability(dn) reachability(dn, direction = "forward") reachability(dn, start = 0, end = 2)
The same operation as remove_ties(), with one extra guarantee: the
network must already be directed, so a from/to pair names one arc and
not both orientations. Removing an arc from an undirected network raises a
condition of class dynet_needs_directed.
remove_arcs( dn, ties = NULL, from = NULL, to = NULL, start = NULL, end = NULL, session = NULL )remove_arcs( dn, ties = NULL, from = NULL, to = NULL, start = NULL, end = NULL, session = NULL )
dn |
A temporal network from |
ties |
Which ties to remove: a condition on the spell table,
evaluated the way |
from, to, start, end, session
|
Optional selectors combined by conjunction.
|
A new directed dynet object without the matched spells, with the
same structure as the input.
remove_ties(), which does not require a directed network.
dn <- dynet(data.frame( from = c("A", "B"), to = c("B", "C"), start = c(0, 1), end = c(1, 2) )) remove_arcs(dn, ties = 1)dn <- dynet(data.frame( from = c("A", "B"), to = c("B", "C"), start = c(0, 1), end = c(1, 2) )) remove_arcs(dn, ties = 1)
Remove nodes from a temporal network
remove_nodes(dn, nodes, cascade = FALSE)remove_nodes(dn, nodes, cascade = FALSE)
dn |
A temporal network from |
nodes |
Character node names. |
cascade |
Whether to remove every incident temporal tie and vertex
activity spell. The safe default, |
A new internally consistent dynet object, of the same class and
structure as the input, without the named vertices and – under
cascade = TRUE – without their ties and vertex-activity spells. At
least one temporal tie must remain.
dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) dn <- add_nodes(dn, "C") remove_nodes(dn, "C")dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) dn <- add_nodes(dn, "C") remove_nodes(dn, "C")
Remove temporal ties
remove_ties( dn, ties = NULL, from = NULL, to = NULL, start = NULL, end = NULL, session = NULL )remove_ties( dn, ties = NULL, from = NULL, to = NULL, start = NULL, end = NULL, session = NULL )
dn |
A temporal network from |
ties |
Which ties to remove: a condition on the spell table,
evaluated the way |
from, to, start, end, session
|
Optional selectors combined by conjunction.
|
A new internally consistent dynet object, of the same class and
structure as the input, without the matched spells. At least one temporal
tie must remain. A request that matches nothing raises a condition of
class dynet_tie_not_found.
dn <- dynet(data.frame( from = c("A", "B"), to = c("B", "C"), start = c(0, 1), end = c(1, 2) )) remove_ties(dn, ties = 1)dn <- dynet(data.frame( from = c("A", "B"), to = c("B", "C"), start = c(0, 1), end = c(1, 2) )) remove_ties(dn, ties = 1)
Remove declared vertex-activity components
remove_vertex_spells(dn, spells)remove_vertex_spells(dn, spells)
dn |
A temporal network. |
spells |
Integer positions or a logical mask over
|
A new dynet object, class
c("dynet", "netobject", "cograph_network"), with the selected activity
components dropped and the rest canonicalised again, so the remaining
spell identifiers renumber. A node with no remaining declaration becomes
implicitly always active over observation support. Raises
dynet_bad_input when spells is not a valid selection.
dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) trimmed <- remove_vertex_spells(enrolled, spells = 1) as.data.frame(trimmed, what = "vertex_spells")dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) trimmed <- remove_vertex_spells(enrolled, spells = 1) as.data.frame(trimmed, what = "vertex_spells")
Rename nodes everywhere in a temporal network
rename_nodes(dn, mapping)rename_nodes(dn, mapping)
dn |
A temporal network. |
mapping |
A named character vector whose names are old node names and
values are replacements, a two-column data frame named |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), with edge endpoints, node
attributes, vertex activity, cograph labels, and groups renamed together.
Raises dynet_unknown_node when an old name is not a vertex,
dynet_duplicate_node when a replacement collides with a name that is
being kept, dynet_unknown_attribute when mapping names a column that
is not a vertex attribute, and dynet_bad_input otherwise.
dn <- dynet(school_contacts) renamed <- rename_nodes(dn, c(Ana = "Anna", Ben = "Benjamin")) renameddn <- dynet(school_contacts) renamed <- rename_nodes(dn, c(Ana = "Anna", Ben = "Benjamin")) renamed
Rename session walls
rename_sessions(dn, mapping)rename_sessions(dn, mapping)
dn |
A sessioned temporal network. |
mapping |
A named character vector from old to new labels, or an
|
A new dynet object, class
c("dynet", "netobject", "cograph_network"), with edge and vertex
session labels renamed together and the session scheme in its metadata
updated. Labels absent from mapping are left alone. Raises
dynet_unknown_session when an old label is not a session, and
dynet_bad_input when the network has no session scheme, when mapping
is malformed, or when the renaming would produce duplicate labels.
dn <- dynet(school_contacts) weeks <- with(school_contacts, ifelse(start < 7, "week_1", "later")) labelled <- set_tie_sessions(dn, session = weeks) renamed <- rename_sessions(labelled, c(week_1 = "opening")) renameddn <- dynet(school_contacts) weeks <- with(school_contacts, ifelse(start < 7, "week_1", "later")) labelled <- set_tie_sessions(dn, session = weeks) renamed <- rename_sessions(labelled, c(week_1 = "opening")) renamed
Face-to-face contacts among fourteen students over three weeks. Each row is one contact with an explicit start and end, which makes this an interval log: duration carries information, and two students who met once at length are distinguishable from two who met briefly many times. The students fall into three loosely-connected friendship clusters and overall activity rises through the second week before falling away.
school_contactsschool_contacts
A data.frame with 240 rows and 4 columns:
Character. The student initiating the contact.
Character. The other student.
Numeric. Day the contact began, counted from day zero.
Numeric. Day the contact ended.
The fourteen students are Ana, Ben, Cara, Dan, Eve, Finn,
Gita, Hugo, Iris, Jonas, Kira, Leo, Mira and Nils; times
run from day 0 to day 21.52.
Simulated, not observed. Generated deterministically under a fixed
seed by data-raw/make-data.R.
dynet(school_contacts)dynet(school_contacts)
Fransson's SID: for each time bin, how much more tied a network's communities are internally than to each other, both normalised by the number of pairs available. Positive means segregation exceeds integration.
segregation( dn, communities, scope = c("pertime", "overall"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )segregation( dn, communities, scope = c("pertime", "overall"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )
dn |
A temporal network from |
communities |
Either a vector named by vertex name, or a single
string naming a column of the node table, as |
scope |
|
sessions |
How to treat sessions, as in |
start, end, step, window
|
The measurement grid, as in |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
A community of one vertex makes the within-community normaliser
2 / (N_a (N_a - 1)) a division by zero. Rather than drop the vertex,
which would change every other community's denominator, the bin returns
NA_real_ and a dynet_singleton_community warning names the offending
community.
The measure is defined on undirected networks, and a directed one is read
by folding each layer onto its transpose, as persistence() does. Weights
and spell counts are ignored: a pair is tied in a bin or it is not.
A dynet_metric at level = "graph" with columns time (under
scope = "pertime"), measure, which is the constant "sid", and
value. A leading session column is present under
sessions = "separate". The value is signed and unbounded: it is a
difference of two normalised sums, not a proportion, so there is no range
to check it against. Print it, summary() it, plot() it, or take the
plain frame with as.data.frame().
Errors: dynet_bad_communities when the partition is unnamed, names a
vertex the network does not have, or leaves a vertex unassigned;
dynet_no_sessions under sessions = "separate" without a session column;
dynet_bad_input when dn is not a dynet. Warns with
dynet_singleton_community when a community has one member.
Fransson, P., Thompson, W. H., Skiold, B., et al. (2018). Brain network segregation and integration during an epoch-related working memory fMRI experiment. NeuroImage, 178, 147-161. doi:10.1016/j.neuroimage.2018.05.040
Thompson, W. H., Brantefors, P., & Fransson, P. (2017). From static to temporal network theory. Network Neuroscience, 1(2), 69-99. doi:10.1162/NETN_a_00011
mixing() for who ties to whom by attribute, and persistence()
for whether those ties survive.
dn <- dynet(school_contacts) groups <- stats::setNames( rep(c("a", "b"), length.out = nrow(dn$nodes)), dn$nodes$name ) sid <- segregation(dn, communities = groups) siddn <- dynet(school_contacts) groups <- stats::setNames( rep(c("a", "b"), length.out = nrow(dn$nodes)), dn$nodes$name ) sid <- segregation(dn, communities = groups) sid
Which students attended which weekly seminar over one term. This is
two-mode data: students are not linked to each other directly, only to the
seminars they turned up to. dynet() projects it, connecting every pair of
students who shared a room in the week they shared it.
seminar_attendanceseminar_attendance
A data.frame with 104 rows and 3 columns:
Character. Student identifier, s01 to s24.
Character. Which weekly seminar, week_01 to week_12.
Date. When the seminar was held, 2024-09-03 to 2024-11-19.
Simulated, not observed. Generated deterministically under a fixed
seed by data-raw/make-data.R.
dynet(seminar_attendance, actor = "student", group = "seminar")dynet(seminar_attendance, actor = "student", group = "seminar")
Replace observation support
set_observations(dn, data = NULL, start = NULL, end = NULL)set_observations(dn, data = NULL, start = NULL, end = NULL)
dn |
A temporal network. |
data |
Optional data frame with |
start, end
|
Optional scalar continuous bounds used instead of |
A new dynet object, class
c("dynet", "netobject", "cograph_network"). Raw edge and vertex spells
are unchanged – as.data.frame(x) still returns the originals – and
only the non-destructive measurement view is replaced, so every verb now
clips exposure and path horizons to this support. Read the components
back with as.data.frame(x, what = "observations"), one row per
component with observation, start, end, duration and instant.
Raises dynet_bad_input when neither or both of data and the bounds
are given.
dn <- dynet(school_contacts) first_week <- set_observations(dn, start = 0, end = 7) as.data.frame(first_week, what = "observations")dn <- dynet(school_contacts) first_week <- set_observations(dn, start = 0, end = 7) as.data.frame(first_week, what = "observations")
Assign or remove tie sessions
set_tie_sessions(dn, session = NULL, breaks = NULL, labels = NULL)set_tie_sessions(dn, session = NULL, breaks = NULL, labels = NULL)
dn |
A temporal network. |
session |
A complete, nonempty character vector of length one or the
raw tie count; a length-one value labels every spell. Default A vector of the full length is matched positionally against the spell
table, not against the data frame the network was built from.
|
breaks |
Optional increasing numeric vector of cut points on the
network's time axis. A spell belongs to the session of the interval its
|
labels |
Optional character vector naming the |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), with a session column on
the spell table and the session scheme recorded in its metadata, or with
both removed when neither session nor breaks is given. Raises
dynet_bad_input when session has neither length one nor the raw tie
count, or carries NA or blank labels; when session and breaks are
both given; when breaks is not increasing and finite; or when labels
does not have one more element than breaks.
dn <- dynet(school_contacts) weeks <- set_tie_sessions(dn, breaks = c(7, 14), labels = c("week_1", "week_2", "week_3")) weeksdn <- dynet(school_contacts) weeks <- set_tie_sessions(dn, breaks = c(7, 14), labels = c("week_1", "week_2", "week_3")) weeks
Replace declared vertex activity
set_vertex_spells(dn, data = NULL)set_vertex_spells(dn, data = NULL)
dn |
A temporal network. |
data |
A vertex-spell data frame with |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), whose declared vertex
activity is exactly data and whose edge spells, node attributes and
metadata are those of dn. Overlapping or adjacent spells are
canonicalised exactly as in dynet(), so canonical spell identifiers may
change. Read the result back with
as.data.frame(x, what = "vertex_spells"). Raises dynet_unknown_node
when data names a vertex the network does not have, and
dynet_bad_input for a string other than "ties".
dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) as.data.frame(enrolled, what = "vertex_spells") # Present from the first contact to the last, vertex by vertex. spanned <- set_vertex_spells(dn, "ties") as.data.frame(spanned, what = "vertex_spells")dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) as.data.frame(enrolled, what = "vertex_spells") # Present from the first contact to the last, vertex by vertex. spanned <- set_vertex_spells(dn, "ties") as.data.frame(spanned, what = "vertex_spells")
Runs a measurement verb on the observed network and on every surrogate from a null model, then reports the observed value beside the null distribution, a percentile interval and a permutation p-value. This is what turns any of this package's measures from a point estimate into a result.
Works with every verb that returns a measure and a value column, which
is all of them: metrics(), centrality_series(), path_centrality(),
events(), burstiness(), durations(), reachability(), mixing(),
similarity() and pshifts().
significance( x, statistic, ..., method = c("times", "timeline", "edges", "targets", "labels"), n = 999L, alternative = c("two.sided", "greater", "less"), conf_level = 0.95, p_adjust = "BH", within = c("network", "sender", "session"), seed = NULL )significance( x, statistic, ..., method = c("times", "timeline", "edges", "targets", "labels"), n = 999L, alternative = c("two.sided", "greater", "less"), conf_level = 0.95, p_adjust = "BH", within = c("network", "sender", "session"), seed = NULL )
x |
A temporal network from |
statistic |
A measurement verb, passed as a function. |
... |
Passed to |
method, n, within
|
Null model settings, as in |
alternative |
|
conf_level |
Width of the reported percentile interval of the null. |
p_adjust |
Multiplicity correction, passed to |
seed |
A single whole number for a reproducible draw, or |
The p-value carries the standard plus-one correction,
, so with 999 surrogates the smallest reportable
value is 0.001 and never zero. p_mcse is the Monte-Carlo standard error on
p itself, computed from the same draws, so the precision of the p-value is
visible beside it.
A cell present in the observed result but absent from a surrogate counts as
missing, never as zero: a bin with no eligible pairs did not have zero
density, it had no density. n_null reports how many surrogates actually
produced each cell.
Under method = "labels" every structural measure is exactly invariant, so
every p is exactly one. That is correct, and it is a useful check that the
machinery is aligned rather than a failure.
n_ties counts surrogates numerically equal to the observed value. Read it:
a discrete statistic on a small network, such as density where every value
is a count over a fixed number of pairs, can put most of the null mass
exactly on the observed value. The p-value is then decided by how ties are
counted rather than by the tail, and it does not stabilise as n grows.
Measured on school_contacts, the density p-value varied by 0.61 across
five seeds at both 199 and 999 surrogates, while the continuous burstiness
p-value tightened from 0.19 to 0.07 over the same increase. A large
n_ties relative to n_null means more surrogates will not help; a
statistic with finer resolution will.
A dynet_significance data frame with one row per cell of the
observed statistic. It carries every key column the statistic produced,
then observed, null_mean, null_sd, null_lo, null_hi, z, p,
p_adj, p_mcse, n_ties and n_null. null_lo and null_hi bound
the null distribution, not the observed value, which is why they are not
named as a confidence interval for it.
Davison, A. C., and Hinkley, D. V. (1997). Bootstrap Methods and Their Application. Cambridge University Press.
North, B. V., Curtis, D., and Sham, P. C. (2002). A note on the calculation of empirical P values from Monte Carlo procedures. American Journal of Human Genetics, 71(2), 439-441.
Benjamini, Y., and Hochberg, Y. (1995). Controlling the false discovery rate. Journal of the Royal Statistical Society B, 57(1), 289-300.
randomise() for the null models and what each one holds fixed.
dn <- dynet(school_contacts) significance(dn, statistic = metrics, measure = "density", n = 99, seed = 1)dn <- dynet(school_contacts) significance(dn, statistic = metrics, measure = "density", n = 99, seed = 1)
Compares the edge set at every time bin with the edge set at every other,
giving the pairwise similarity matrix as a tidy frame. This answers how
much the network at one moment resembles the network at another, which no
single-bin measure reports and which the formation and dissolution
quantities in events() only address between neighbouring bins.
Coefficients are computed by cograph::layer_similarity().
similarity( dn, method = c("jaccard", "overlap", "hamming", "cosine", "pearson"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )similarity( dn, method = c("jaccard", "overlap", "hamming", "cosine", "pearson"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )
dn |
A temporal network from |
method |
One of |
sessions |
How to treat sessions when the layers are built:
|
start, end
|
First and last time to measure. Default to the observed range. |
step |
How often to measure. Defaults to the interval the network was built with. |
window |
How much time each measurement covers. Defaults to |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
A dynet_similarity data frame with one row per ordered pair of
time bins and columns time, other, measure and value. The
diagonal is included and is one for every coefficient except
"hamming", where identical layers differ in nothing and score zero.
"pearson" reaches one only to floating-point accuracy, so compare it
with a tolerance rather than with ==. The frame is returned invisibly
when plot = TRUE has drawn the figure.
The coefficients come from cograph, which is a hard dependency of Dynet;
a namespace that cannot be loaded raises dynet_needs_cograph.
snapshots() for the networks being compared, events() for
formation and dissolution between neighbouring bins.
dn <- dynet(school_contacts) similarity(dn) similarity(dn, method = "cosine")dn <- dynet(school_contacts) similarity(dn) similarity(dn, method = "cosine")
The edges alive in each time bin, as one tidy table. Useful for exporting a slice, for feeding a layout routine, or for checking by eye what the metric verbs are seeing.
snapshots( dn, at = NULL, sessions = c("bounded", "collapse", "separate"), sample = NULL, start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )snapshots( dn, at = NULL, sessions = c("bounded", "collapse", "separate"), sample = NULL, start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )
dn |
A temporal network from |
at |
Optional single time, narrowing the result to the bins that
cover it. A network built from dates may be addressed with a date.
With the default disjoint tiling that is one bin; with an
overlapping |
sessions |
How to treat sessions, as in |
sample |
Deprecated. |
start, end
|
First and last time at which to measure. Default to the observed range. A network built from dates may be addressed with dates. |
step |
How often to measure. Defaults to the interval the network was built with. |
window |
How much time each measurement covers. Defaults to |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
A dynet_snapshot data frame with one row per active edge per bin:
session (when the network has sessions), observation (when
observation is discontinuous, naming the observed component the bin falls
in), time, from, to, weight and n_spells.
print() shows a header and the first rows, summary() collapses to one
row per bin, plot() draws how many ties each bin holds, and
as.data.frame() returns the plain table. A pair
joined by more than one spell in the same bin is one edge, with
n_spells recording how many spells were collapsed – so the edge counts
here agree with those from metrics(). weight is the sum of those
spells' full weights: a spell counts its whole weight in every bin it
touches, as networkDynamic::network.collapse() does. Snapshot
"strength" in centrality_series() instead splits a spell's weight by
the share of its duration inside the bin. Eligible isolates have no
synthetic edge row; use centrality_series() or metrics() when the
eligible population itself is required.
Errors: dynet_no_sessions (sessions = "separate" without a session
column), dynet_outside_observation (the requested range misses observed
support; it also carries dynet_bad_input), and dynet_bad_input for
every other broken contract – dn not
a dynet, an at, start or end that is not a single finite time, and
an out-of-range step or window.
Warning: dynet_deprecated for the retired sample argument.
dn <- dynet(school_contacts) snapshots(dn, at = 3)dn <- dynet(school_contacts) snapshots(dn, at = 3)
A tidy description of the whole network, one row per property. Two densities are reported and they answer different questions. Snapshot density is the mean over time bins of realised against possible edges. Temporal density is the proportion of all possible relational exposure occupied during the observation window. Overlapping and duplicate spells for the same ordered pair, or dyad in an undirected network, are unioned before their duration is counted.
## S3 method for class 'dynet' summary(object, temporal_density = FALSE, ...)## S3 method for class 'dynet' summary(object, temporal_density = FALSE, ...)
object |
A temporal network from |
temporal_density |
Whether to compute the temporal-density row.
|
... |
Ignored. |
Let indicate that both endpoints of relational opportunity
are eligible at positive observed time , and let
indicate binary union edge activity. Temporal density is
Directed opportunities are ordered; undirected opportunities are unordered.
The integrals are evaluated exactly over observation, vertex, and edge change
points. Self-loops, weights, session labels, duplicate spells, genuine
points, and observation gaps do not add exposure. A network with no positive
time containing two coeligible distinct vertices has undefined temporal
density and reports NA.
This is an occupancy definition. Unlike summing spell durations, it remains
in [0, 1] when the same relation has overlapping or duplicated spells.
A data.frame with columns property and value, one row per
property.
Bender-deMoll, S., & Morris, M. (2025). tsna: Tools for Temporal Social Network Analysis. R package version 0.3.6.
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125.
Latapy, M., Viard, T., & Magnien, C. (2018). Stream graphs and link streams for the modeling of interactions over time. Social Network Analysis and Mining, 8, 61.
dn <- dynet(school_contacts) summary(dn) # The temporal density is opt-in, since it is quadratic in the vertex count. summary(dn, temporal_density = TRUE)dn <- dynet(school_contacts) summary(dn) # The temporal density is opt-in, since it is quadratic in the vertex count. summary(dn, temporal_density = TRUE)
Summarise an animation
## S3 method for class 'dynet_animation' summary(object, ...)## S3 method for class 'dynet_animation' summary(object, ...)
object |
A |
... |
Ignored. |
A one-row plain data.frame with bins, frames, fps,
seconds, tween, ease, layout, format, measure (what node
size follows, NA when it is constant), first_time, last_time,
min_ties, max_ties, turnover (over the bins after the first that
hold a tie, the median share of a bin's ties that were not active in the
bin before: near 0 the film flows, near 1 every bin is a new picture;
NA with a single bin) and file.
if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn, step = 6, window = 6, tween = 2) summary(frames) }if (requireNamespace("gifski", quietly = TRUE) && requireNamespace("cograph", quietly = TRUE)) { dn <- dynet(school_contacts) frames <- animate(dn, step = 6, window = 6, tween = 2) summary(frames) }
Summarise session-specific collapsed networks
## S3 method for class 'dynet_collapsed_list' summary(object, ...)## S3 method for class 'dynet_collapsed_list' summary(object, ...)
object |
A |
... |
Ignored. |
A plain data.frame, one row per session: session, the number of
collapsed pairs, the nodes those pairs span, and the summed
union_duration and total_duration.
dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 0), end = c(2, 3), session = c("s1", "s2") ), session = "session") by_session <- collapse_network(dn, sessions = "separate") summary(by_session)dn <- dynet(data.frame( from = c("A", "A"), to = c("B", "B"), start = c(0, 0), end = c(2, 3), session = c("s1", "s2") ), session = "session") by_session <- collapse_network(dn, sessions = "separate") summary(by_session)
Summarize a temporal community partition
## S3 method for class 'dynet_communities' summary(object, ...)## S3 method for class 'dynet_communities' summary(object, ...)
object |
A |
... |
Ignored. |
A data frame with one row per community: community, n_states,
n_nodes, first_time, last_time, n_bins and persistence, the
share of the bins it spans in which it is actually present.
Summarise an event graph
## S3 method for class 'dynet_event_graph' summary(object, ...)## S3 method for class 'dynet_event_graph' summary(object, ...)
object |
An event graph returned by |
... |
Ignored. |
A plain data.frame, one row per event: event, its session
(under sessions = "separate"), its start time, its endpoints from and to, in_degree (the distinct events
it follows), out_degree (the distinct events that follow it), and
mean_wait, the mean wait before those successors start, followed by the
event's tie attributes. mean_wait is NA for an event with no
successor, where no wait is defined.
dn <- dynet(school_contacts) summary(event_graph(dn, delta = 1))dn <- dynet(school_contacts) summary(event_graph(dn, delta = 1))
Collapses the time dimension. Node-level measures are summarised one row per vertex and measure; graph-level measures one row per measure. The peak time is reported alongside, because when a quantity peaked is usually the question a temporal network is being asked.
## S3 method for class 'dynet_metric' summary(object, by = NULL, ...)## S3 method for class 'dynet_metric' summary(object, by = NULL, ...)
object |
A |
by |
Grouping for the summary: |
... |
Ignored. |
A data.frame with the grouping columns plus n, mean, sd,
min, max and, when time is available, peak_time. n counts the
measured values the statistics were computed from, so a vertex that was
inactive for part of the calendar reports fewer than the number of time
points.
dn <- dynet(school_contacts) degree <- centrality_series(dn, measure = "degree") summary(degree) summary(degree, by = "time")dn <- dynet(school_contacts) degree <- centrality_series(dn, measure = "degree") summary(degree) summary(degree, by = "time")
Summarise a temporal motif census by family
## S3 method for class 'dynet_motifs' summary(object, by = c("family", "node"), ...)## S3 method for class 'dynet_motifs' summary(object, by = c("family", "node"), ...)
object |
A |
by |
|
... |
Ignored. |
A plain data.frame with one row per group: the group, its
count, its share of all instances, and the top_motif index within
it.
Summarise surrogate networks
## S3 method for class 'dynet_null' summary(object, ...)## S3 method for class 'dynet_null' summary(object, ...)
object |
A |
... |
Ignored. |
A data frame with one row per replicate and columns replicate,
n_events, n_pairs, t_min, t_max and mean_duration, so the
conservation each method claims is visible.
Summarise path trajectories
## S3 method for class 'dynet_path_trajectories' summary(object, ...)## S3 method for class 'dynet_path_trajectories' summary(object, ...)
object |
A |
... |
Ignored. |
A plain data.frame, one row per depth in increasing order:
depth, the number of distinct branches reaching it, the number of
distinct vertices they land on, the summed
count of routes through it, and mean_branching, the average branching
fraction of those routes. Note probability in the underlying table is
CONDITIONAL on each parent, so it is averaged rather than summed: adding
conditional fractions across siblings would not be a probability at all.
Depth zero is the queried vertex itself and has no parent, so its
mean_branching is NA.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) summary(trajectories)dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") trajectories <- path_trajectories(routes) summary(trajectories)
Summarise time-respecting paths
## S3 method for class 'dynet_paths' summary(object, ...)## S3 method for class 'dynet_paths' summary(object, ...)
object |
A |
... |
Ignored. |
A data.frame with columns property and value, one row per
property, both character so the table prints as one block. The eight
properties are source, direction, reachable, reachable share,
median latency, max latency, median hops and max hops; the source
is excluded from every count and share. Under sessions = "separate" a
leading session column is added and the eight properties are repeated
for each session.
dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") summary(routes)dn <- dynet(school_contacts) routes <- paths(dn, from = "Ana") summary(routes)
Summarise ranked pathways
## S3 method for class 'dynet_pathways' summary(object, ...)## S3 method for class 'dynet_pathways' summary(object, ...)
object |
A |
... |
Ignored. |
A plain data.frame, one row per endpoint: endpoint, the number
of distinct routes reaching it, their summed count and share, the
min_hops of the shortest, and first_arrival, the earliest time any
route lands there. Ordered by count.
dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") summary(routes)dn <- dynet(school_contacts) routes <- pathways(dn, from = "Ana") summary(routes)
Summarise a time projection
## S3 method for class 'dynet_projection' summary(object, ...)## S3 method for class 'dynet_projection' summary(object, ...)
object |
A |
... |
Ignored. |
A plain data.frame, one row per slice: slice, its time, the
number of active vertex states, the within_slice arcs induced in it,
and the identity_arcs leaving it for the next slice. The final slice
emits no identity arcs, so its count is zero.
dn <- dynet(school_contacts) slices <- projection(dn, step = 5, window = 5) summary(slices)dn <- dynet(school_contacts) slices <- projection(dn, step = 5, window = 5) summary(slices)
Summarise participation shifts by family
## S3 method for class 'dynet_pshifts' summary(object, ...)## S3 method for class 'dynet_pshifts' summary(object, ...)
object |
A |
... |
Ignored. |
A plain data.frame, one row per shift family, ordered by
descending count: family, its count, the share of all classified
transitions it accounts for, and top_shift, the single most frequent
shift type within it. share is NaN when nothing was classified. The
family totals are sums of the count column as it stands, so they are
transition counts for an output = "final" result.
dn <- dynet(school_contacts) shifts <- pshifts(dn) summary(shifts)dn <- dynet(school_contacts) shifts <- pshifts(dn) summary(shifts)
Summarise a permutation test
## S3 method for class 'dynet_significance' summary(object, ...)## S3 method for class 'dynet_significance' summary(object, ...)
object |
A |
... |
Ignored. |
A data frame with one row per measure and columns measure,
tested, significant and median_z.
Summarise snapshot similarity
## S3 method for class 'dynet_similarity' summary(object, ...)## S3 method for class 'dynet_similarity' summary(object, ...)
object |
A |
... |
Ignored. |
A plain data.frame, one row per bin: time, the mean, min
and max similarity to every other bin, and nearest, the time of the
most similar other bin. The self-comparison is excluded throughout, so a
bin with no comparable neighbour reports NaN and NA.
dn <- dynet(school_contacts) bin_similarity <- similarity(dn, step = 5, window = 5) summary(bin_similarity)dn <- dynet(school_contacts) bin_similarity <- similarity(dn, step = 5, window = 5) summary(bin_similarity)
Summarise snapshot edges by time bin
## S3 method for class 'dynet_snapshot' summary(object, ...)## S3 method for class 'dynet_snapshot' summary(object, ...)
object |
A |
... |
Ignored. |
A plain data.frame with one row per bin and columns session
(when present), time, ties, nodes and weight.
dn <- dynet(school_contacts) bins <- snapshots(dn) summary(bins)dn <- dynet(school_contacts) bins <- snapshots(dn) summary(bins)
A synthetic temporal network of how one interaction code follows another in asynchronous discussion. Vertices are the ten codes a message can carry, so a vertex is a category, never a person, and a tie runs from the code of a message to the code of the message it replies to.
synthdatasynthdata
A data.frame with 101 rows and 5 columns:
Character. Code of the replying message.
Character. Code of the message replied to.
Numeric. When the spell opens, 0 to 2.10.
Numeric. When it closes, 0.03 to 5.80.
Numeric, whole-valued. Message pairs the spell represents, 1 to 5881.
The ten codes are Acceptance, Argument, Composing, Disagreement,
Evaluation, Group_regulation, Question, Sharing, Socioemotional
and T.Regulation; 14 rows are self-loops.
This is a stand-in for the network analysed in the Trees of Thought study, whose own data is not redistributable. It was built by drawing a random 70% subset of that study's edge spells and resampling that subset with replacement to 90% of the original spell count, so it omits about a third of the real spells and repeats others. No row of it should be read as a finding about the study, and figures computed from it will not match the published ones. It exists so the analysis can be run and taught end to end.
Spells are weighted by how many message pairs they represent, overlap in
time, and include self-loops, because a code following itself is a real and
common transition. Build with loops = TRUE to keep them; the weight
column is picked up as the tie weight.
Synthesised from the Trees of Thought code-transition network by
the resampling described above; see data-raw/synthdata.R.
dynet(synthdata, directed = TRUE, loops = TRUE, weight = "weight")dynet(synthdata, directed = TRUE, loops = TRUE, weight = "weight")
The counterpart of head.dynet_metric(); the header still describes the
series and a last n of N rows line records the truncation.
## S3 method for class 'dynet_metric' tail(x, n = 6L, ...)## S3 method for class 'dynet_metric' tail(x, n = 6L, ...)
x |
A |
n |
Number of rows to keep. Defaults to six. |
... |
Passed to the default method. |
A dynet_metric with at most n rows, carrying the source counts
so its header stays true to the series.
dn <- dynet(school_contacts) degree <- centrality_series(dn, step = 4, window = 4) tail(degree)dn <- dynet(school_contacts) degree <- centrality_series(dn, step = 4, window = 4) tail(degree)
Which vertices form a group, and how that group survives, splits or dissolves as time passes. The partition is found by maximising the multislice modularity of Mucha et al. (2010) over the time-expanded network, so the slices are solved together rather than one at a time and a community keeps its identity across bins by construction.
Two knobs decide what you get. gamma sets the resolution: how dense a
group has to be to count as one. omega sets how much a vertex is
rewarded for staying put, so it trades a partition that tracks each bin's
structure exactly against one that persists and is readable as a story.
temporal_communities( dn, gamma = 1, omega = 1, method = c("louvain", "consensus"), seeds = 1:10, coupling = c("ordinal", "categorical"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, max_passes = 20L, tol = 1e-10 )temporal_communities( dn, gamma = 1, omega = 1, method = c("louvain", "consensus"), seeds = 1:10, coupling = c("ordinal", "categorical"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, max_passes = 20L, tol = 1e-10 )
dn |
A temporal network from |
gamma |
Resolution. Above one favours more, smaller communities; below one favours fewer, larger ones. |
omega |
Interlayer coupling. Zero detects each bin independently and
the labels are then matched by |
method |
|
seeds |
The seeds to run from, one optimisation each. Modularity landscapes are near-degenerate, so a single run is not a result; the default runs ten and reports how much they agreed. |
coupling |
|
sessions |
How to treat sessions. |
start, end
|
First and last slice times. Default to observed support. |
step |
Spacing between slice starts. |
window |
Width represented by each slice. |
max_passes |
Cap on aggregation passes per run. |
tol |
Smallest improvement that counts as a move. |
The objective is multislice_modularity(), and the two round-trip:
multislice_modularity(dn, membership = temporal_communities(dn)) returns
the q this verb reports.
Why several seeds. Louvain visits vertices in some order and takes the
first improving move it finds, so its answer depends on that order, and
modularity landscapes are near-degenerate: very many partitions sit within
a hair of the maximum (Good, de Montjoye & Clauset 2010). One run is a
sample from that plateau, not the answer. This verb therefore runs once per
seed, reports the best, and reports how much the runs agreed –
stability_ari, the mean pairwise adjusted Rand index across runs, and a
per-state stability column. A stability_ari near one means the labels
can be read; near zero means they cannot, whatever the modularity says.
Asking for a single seed raises a dynet_single_seed warning for that
reason.
What is not implemented. The Leiden refinement (Traag, Waltman & van Eck
2019) is not, so a community this verb reports can in principle be
internally disconnected – the known defect of Louvain. Running several
seeds and reading stability_ari is the mitigation on offer here.
Inactive vertices. A vertex with no activity in a bin still has its
identity arcs, because projection() lets a vertex wait through
inactivity. It is therefore carried along by the coupling and receives a
label rather than being dropped, which would break the chain. The active
column marks those states.
Reproducibility. The result is a deterministic function of seeds: two
calls with the same seeds return identical frames. The caller's random
stream is saved and restored, so running this verb never changes what the
next sample() produces.
A dynet_communities frame, one row per vertex per time bin:
session (only when the network has sessions), time, node,
community (an integer label, meaning the same group in every bin),
active (was the vertex eligible in this bin) and stability (in
, how much of the run-to-run variation this state survived).
Print it, summary() it, plot() it, or take the plain frame with
as.data.frame(), which also serves what = "runs", "sizes" and,
after match_communities(), "events".
Mucha, P. J., Richardson, T., Macon, K., Porter, M. A., & Onnela, J.-P. (2010). Community structure in time-dependent, multiscale, and multiplex networks. Science, 328(5980), 876-878.
Jeub, L. G. S., Bazzi, M., Jutla, I. S., & Mucha, P. J. (2011-2019). A generalized Louvain method for community detection implemented in MATLAB.
Blondel, V. D., Guillaume, J.-L., Lambiotte, R., & Lefebvre, E. (2008). Fast unfolding of communities in large networks. Journal of Statistical Mechanics, P10008.
Good, B. H., de Montjoye, Y.-A., & Clauset, A. (2010). Performance of modularity maximization in practical contexts. Physical Review E, 81(4), 046106.
Lancichinetti, A., & Fortunato, S. (2012). Consensus clustering in complex networks. Scientific Reports, 2, 336.
Bassett, D. S., Porter, M. A., Wymbs, N. F., Grafton, S. T., Carlson, J. M., & Mucha, P. J. (2013). Robust detection of dynamic community structure in networks. Chaos, 23(1), 013142.
Traag, V. A., Waltman, L., & van Eck, N. J. (2019). From Louvain to Leiden: guaranteeing well-connected communities. Scientific Reports, 9, 5233.
multislice_modularity() for the objective, match_communities()
for labels from an uncoupled run, community_change() for how much the
partition moved, community_trajectory() for what each vertex did, and
phases() for regimes found without communities at all.
dn <- dynet(school_contacts) found <- temporal_communities(dn, step = 5, window = 5, seeds = 1:5) found summary(found) as.data.frame(found, what = "sizes")dn <- dynet(school_contacts) found <- temporal_communities(dn, step = 5, window = 5, seeds = 1:5) found summary(found) as.data.frame(found, what = "sizes")
A table of reply links between the codes of messages in coded asynchronous discussions, based on the Trees of Thought study. Each row is one link from the code of a message to the code of the message it replies to. About 20 percent of the study's records were removed, dates and rates were changed and anonymised, and the data were augmented by simulation, so the table is not the study's data, and the participant, group, course and time values do not identify anyone.
Thursdays and Fridays do not occur. Some rows repeat exactly (8,122
duplicates); aggregating the log counts them as weight. A code answering
itself is a self-link (9,452 rows); dynet() drops these unless
loops = TRUE. The course column is recognised as the session column,
so the five courses become sessions unless session = says otherwise.
thought_chainsthought_chains
A data.frame with 23,017 rows and 7 columns:
Character. Code of the replying message, one of the nine
codes Approving, Arguing, Coordinating, Drafting, Inquiring,
Objecting, Resourcing, Socialising, Tutoring.
Character. Code of the message replied to, same set.
POSIXct (UTC) time of the replying message, 2006-09-23 to
2011-11-02; changed and anonymised, not the study's dates.
Character. Author label, P001 to P240.
Integer discussion (thread) id, 1 to 1169, all present.
Character. Course group, A_01 style; 29 groups.
Character. Course, A to E.
Based on the Trees of Thought study of coded asynchronous discussions, with about 20 percent of the records removed, dates and rates changed and anonymised, and the data augmented by simulation: Saqr, M., López-Pernas, S. and Törmänen, T. (2026). A temporal network approach to reveal the longitudinal dynamics of CSCL group regulation and productive collaboration. International Journal of Computer-Supported Collaborative Learning, 21, 237-270. doi:10.1007/s11412-025-09464-5
dynet(thought_chains, time = "time", loops = TRUE) dynet(thought_chains, thread = "discussion")dynet(thought_chains, time = "time", loops = TRUE) dynet(thought_chains, thread = "discussion")
"volatility" is the proportion of eligible pairs whose tie state changed
between one time bin and the next. "fluctuability" is the number of
distinct pairs ever active divided by the total number of pair-bin
activations, so it is low when the same few pairs recur and high when
activity is spread thin.
turnover( dn, measure = c("volatility", "fluctuability"), scope = c("pertime", "overall"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )turnover( dn, measure = c("volatility", "fluctuability"), scope = c("pertime", "overall"), sessions = c("bounded", "collapse", "separate"), start = NULL, end = NULL, step = NULL, window = NULL, plot = FALSE )
dn |
A temporal network from |
measure |
|
scope |
|
sessions |
How to treat sessions, as in |
start, end, step, window
|
The measurement grid, as in |
plot |
Whether to draw the result as well as return it. Drawing is a
side effect in the manner of |
Volatility is the proportion Hamming distance over the eligible non-loop
pair domain: the strict upper triangle for an undirected network, every
ordered off-diagonal pair for a directed one. This is not the scale
cograph::layer_similarity(method = "hamming") uses, which is a raw count
over the full symmetric matrix and is therefore 2 * choose(n, 2) times
larger on an undirected network.
Fluctuability is grid-dependent and comparable only across networks
measured on the same grid. Its denominator sums per-bin active-pair
counts, so halving step roughly doubles it and roughly halves the result.
That is a property of the published definition rather than a defect; teneto's
own documentation concedes the measure is not normalised in a way that makes
comparisons across very different networks meaningful.
Weights and spell counts are ignored throughout: a pair is tied in a bin or it is not. Loops are excluded from the domain.
A dynet_metric at level = "graph". Under scope = "pertime" the
columns are time, measure and value, one row per transition, where
time is the earlier bin of the pair; the final bin opens no
transition and contributes no row. Under scope = "overall" they are
measure and value, one row per requested measure. A leading session
column is present under sessions = "separate". volatility lies in
[0, 1] and fluctuability in (0, 1], or is NA_real_ when no pair is
active anywhere in the grid. Print it, summary() it, plot() it, or
take the plain frame with as.data.frame().
Errors: dynet_incompatible_scope when "fluctuability" is asked for at
scope = "pertime"; dynet_empty_result when the grid yields fewer than two
bins, or when the network has fewer than two vertices and the pair domain is
empty; dynet_no_sessions when sessions = "separate" is asked of a network
with no session column; and dynet_bad_input when dn is not a dynet.
Thompson, W. H., Brantefors, P., & Fransson, P. (2017). From static to temporal network theory: applications to functional brain connectivity. Network Neuroscience, 1(2), 69-99. doi:10.1162/NETN_a_00011
Holme, P., & Saramaki, J. (2012). Temporal networks. Physics Reports, 519(3), 97-125. doi:10.1016/j.physrep.2012.03.001
persistence() for the node-level view of what survived, and
events() for raw formation and dissolution counts.
dn <- dynet(school_contacts) churn <- turnover(dn) churn summarised <- turnover(dn, measure = c("volatility", "fluctuability"), scope = "overall") summariseddn <- dynet(school_contacts) churn <- turnover(dn) churn summarised <- turnover(dn, measure = c("volatility", "fluctuability"), scope = "overall") summarised
Update static node attributes
update_nodes(dn, data)update_nodes(dn, data)
dn |
A temporal network. |
data |
A nonempty data frame with a |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), with the same spells,
vertex activity and metadata as dn and the supplied attributes added to
or replaced on the named vertices. Unnamed vertices keep their existing
values, gaining NA in any column the network did not already have.
Raises dynet_unknown_node when a name is not a vertex, and
dynet_bad_input when data is malformed or names a cograph structural
column (id, label, x, y).
dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) update_nodes(dn, data.frame(name = "A", role = "initiator"))dn <- dynet(data.frame(from = "A", to = "B", start = 0, end = 1)) update_nodes(dn, data.frame(name = "A", role = "initiator"))
Update temporal ties and their attributes
update_ties(dn, ties, data, loops = FALSE)update_ties(dn, ties, data, loops = FALSE)
dn |
A temporal network. |
ties |
Which ties to update: a condition on the spell table,
evaluated the way |
data |
A data frame with one row or one row per selected tie. Columns may be canonical tie fields or arbitrary atomic spell attributes. |
loops |
Whether an endpoint update may introduce a new self-loop.
Existing loops may always be retained. Default |
A new dynet object, class
c("dynet", "netobject", "cograph_network"), holding the unselected
spells unchanged and the selected spells with the supplied values
substituted. Because the edited spells are rebuilt together with the
rest, spell order and the canonical spell identifiers may change. Raises
dynet_loop_not_allowed when an endpoint update would create a new
self-loop without loops = TRUE, and dynet_bad_input when the
selection or data is malformed, or names the read-only derived columns
duration or .raw_spell.
dn <- dynet(school_contacts) marked <- update_ties(dn, ties = end - start > 1, data = data.frame(kind = "long")) markeddn <- dynet(school_contacts) marked <- update_ties(dn, ties = end - start > 1, data = data.frame(kind = "long")) marked
Update declared vertex-activity components
update_vertex_spells(dn, spells, data)update_vertex_spells(dn, spells, data)
dn |
A temporal network. |
spells |
Integer positions or a logical mask over canonical vertex
activity, as returned by |
data |
A data frame with one row, or one row per selected component,
containing the fields to replace: |
A new dynet object, class
c("dynet", "netobject", "cograph_network"). Updated components are
canonicalised with the retained components, so overlaps can merge and
spell identifiers can change. Raises dynet_bad_input when spells is
not a valid selection, when data is malformed or of the wrong height,
or when it names the read-only derived columns vertex_spell,
duration or instant.
dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) extended <- update_vertex_spells(enrolled, spells = 1, data = data.frame(end = 12)) as.data.frame(extended, what = "vertex_spells")dn <- dynet(school_contacts) present <- data.frame(node = c("Ana", "Ben"), start = 0, end = 10) enrolled <- set_vertex_spells(dn, present) extended <- update_vertex_spells(enrolled, spells = 1, data = data.frame(end = 12)) as.data.frame(extended, what = "vertex_spells")