Manipulating Network Data
Author: by James Hollway

Skip to Tutorial Content

## Introduction

In the companion tutorial “Making Network Data”, we saw how to identify network data bundled in packages, import it from external files, and coerce it between the many classes used for network analysis in R using the `as_*()` functions.

Collected data is rarely in exactly the shape needed for analysis, though. Perhaps the nodes need naming or anonymising, the ties are directed where they should not be, weights need thresholding, or a two-mode network needs projecting before a one-mode method can be applied. This tutorial is organised around such tasks: each section takes one part of a network — its nodes, its ties, its layers, modes, and waves — and shows how to build it up, change its properties, and understand the consequences, before a final section on narrowing a network down to what matters.

Along the way you will meet three recurring families of functions:

- `is_*()` functions check whether a network has some property, returning `TRUE` or `FALSE`
- `to_*()` functions change that property, returning the modified network (in the same class it arrived in)
- `add_*()`/`delete_*()` and the `{dplyr}`-style verbs (`mutate_*()`, `filter_*()`, `select_*()`, `rename_*()`, `join_*()`) grow and prune networks’ nodes, ties, and attributes

Vocabulary: `{manynet}` distinguishes reformatting — changing a network’s type (direction, weights, signs, etc.) while keeping the same number of nodes — from transforming — changing the network’s order (its number of nodes), as projection does. Both are done by `to_*()` functions, and each section below notes which is happening, because it tells you whether your nodes will survive the operation intact.

### Aims

By the end of this tutorial, you should be able to:

- Examine and subset a network with the `[`, `[[`, and `$` operators
- Label, unlabel, and relabel a network’s nodes
- Change a network’s direction, weights, and signs, and know why you might
- Grow and prune networks by adding, joining, deleting, and filtering nodes, ties, and attributes
- Extract a single layer from a multiplex network
- Project a two-mode network to one of its modes, and understand the consequences
- Split a longitudinal network into waves and apply recorded changes

Choose your own data: As in the “Making Network Data” tutorial, the worked examples use small, clean networks so the output is easy to read, but every “Your turn” and “Free play” box invites you to bring your own. Remember the three flavours as a rough difficulty ladder — Classic (`ison_*`, small & tidy), Fiction (`fict_*`, mid-sized & fun), Real-world (`irps_*`, larger & realistic) — and that every function here works on any of them. Each “Your turn” box suggests datasets that have the right structure for the task (e.g. a weighted network for `to_unweighted()`), but you can always browse the full list with `table_data()`.

## Operating on networks

Before changing anything, it helps to know how to look inside a network object, and this same syntax turns out to offer a do-it-yourself alternative to many of the named functions we meet later. Because the data in the `{manynet}` package are either igraph objects at their core, or stocnet data, we can query (and manipulate) them using the same `[`, `[[`, and `$` operators used for matrices, lists, and data frames elsewhere in R. Run the following code and inspect the two outputs.

    ison_adolescents[1:3, 1:3]
    ison_adolescents[[2]]

The first line returns the corner of the network’s adjacency matrix, where a `1` indicates a tie between the row node and the column node and a `.` indicates no tie (because this is a sparse matrix representation; a ‘normal’ matrix representation would represent absence of a tie as a 0). The second line returns the second node’s neighborhood : the set of nodes to which it is tied.

The `$` operator extracts a named attribute, whether it belongs to the nodes, the ties, or the network itself. `{manynet}` is set to try and autocomplete the attribute name for you, so you can type `ison_adolescents$` and then hit the Tab key to see the available attributes. 1

    ison_adolescents$name

The same operators can also be used to assign new values. Here the first line removes all of the first node’s ties, and the second adds a new nodal attribute (the vector is 8 values long, matching the 8 nodes, which is how `{manynet}` knows it belongs to the nodes):

    test <- ison_adolescents
    test[1, ] <- FALSE
    test$smoker <- c(TRUE, FALSE, FALSE, TRUE, FALSE, TRUE, TRUE, FALSE)
    test[1:3, 1:3]
    test

Note that because this is an undirected network, removing the tie from node 1 to node 2 means there is also now no reciprocal tie from node 2 to node 1.

Throughout this tutorial, keep this assignment syntax in mind: many of the named functions we meet have an assignment equivalent, and we will point these out as we go. The named functions are usually clearer and safer (they check and update the network’s type for you), but the operators are handy for quick, surgical changes.

In brief: For igraph/tidygraph objects, `[` indexes a network like its adjacency matrix, `[[` returns a node’s neighbourhood, and `$` gets or sets a named node, tie, or network attribute. All three can be used with `<-` to modify a network directly.

1. Note though that for igraph/tidygraph objects, since the internal list elements are not named, the autocomplete necessarily braces the attribute names in backticks. This is not the case for stocnet objects, which are named lists.↩︎

## Nodal properties

On this page: Adding & removing · Labels · Attributes

The previous page showed how to read a network with operators. Now we start changing it, one element at a time, beginning with the nodes: which nodes are present (adding and removing them), the names that identify them (labels), and the other variables they carry (attributes).

### Adding & removing

Sometimes you need to change which nodes a network contains. `add_nodes()` adds one or more (unconnected) nodes, while `delete_nodes()` removes nodes by index or by name. Run the code and check the node count in each printout.

    add_nodes(ison_adolescents, 1)
    delete_nodes(ison_adolescents, "Sue")

A freshly added node has no ties yet — you would connect it with `add_ties()`, which we meet in the next section — and `delete_nodes()` also drops any ties attached to the nodes it removes.

### Labels

Whether a network is labelled or not matters for how easily you can read results, but also for research ethics: collected social network data sometimes must be anonymised before it is shared or published. `is_labelled()` checks whether a network is labelled, and `node_labels()` retrieves the labels themselves.

`ison_algebra` records interactions among 16 anonymous students in an algebra class. Since working with indices gets confusing quickly, especially when discussing what we are seeing with others — node 1 is not the same as node 2, and so on — we want to give the nodes names.

The assignment equivalent sets the names directly: `net$label <- letters[1:8]` relabels an 8-node network, because a value with one entry per node is stored as a nodal attribute, and `label` is the reserved attribute for labels.

we can use `to_labelled()` to assign (random) names to the nodes. The function allows your chosen labels to be added, of course, but if called without further arguments it assigns random (U.S.) children’s names. Check whether `ison_algebra` is labelled, and then label it randomly.

    # First check with is_labelled(),
    # then pipe the network to to_labelled() and print the result.

    is_labelled(ison_algebra)
    ison_algebra |> to_labelled()

To check your result: the printout should now say labelled, and the nodes table should have gained a `name` column of alphabetically ordered first names. Since these names are random, they are useful for telling nodes apart, not for identifying real people. Did you notice how the names are drawn in alphabetic sequence?

The reverse operation, `to_unlabelled()`, strips all names from a network. This is a one-step anonymiser. Anonymise `ison_adolescents`, and then relabel it with letters using `$`.

    # Two separate steps:
    # to_unlabelled(_____)
    # Then assign to a copy and relabel:
    # test <- ison_adolescents
    # test$name <- _____
    # (letters[1:8] gives the first eight lowercase letters)

    to_unlabelled(ison_adolescents)
    test <- ison_adolescents
    test$name <- letters[1:8]
    test

Note that `to_unlabelled()` removes the identifying labels but leaves any other attributes in place — full anonymisation of your data may require deleting or coarsening other attributes too, which brings us to attributes just below.

### Attributes

Adding nodal attributes to a given network is relatively straightforward. An ‘attribute’ is just a variable attached to the nodes (or ties) of a network, such as people’s age or the strength of their friendships. `{manynet}` offers a more `{igraph}`-like syntax, e.g. `add_node_attribute()`, as well as a more `{dplyr}`-like syntax, e.g. `mutate_nodes()`, for those already familiar with these tools in R. Run the following code and find the new columns in the printout.

    ison_adolescents |>
      mutate_nodes(colour = "red",
                   degree = 1:8)

One can also rename attributes with `rename_nodes()` (which works like `{dplyr}`’s `rename(new = old)`), and delete them in one of two equivalent ways: the `{dplyr}` way, by assigning `NULL` inside `mutate_nodes()`, or the `{igraph}` way, with `delete_node_attribute()`. Compare the printouts before and after the changes below.

    ison_southern_women
    ison_southern_women |>
      delete_node_attribute("Surname") |>
      rename_nodes(Honorific = Title)

Each of these `_nodes` verbs has a `_ties` counterpart (`mutate_ties()`, `rename_ties()`, `delete_tie_attribute()`, and so on) that does the same for tie attributes; since a tie’s weight and sign are just special tie attributes, we pick these up at the start of the next section on tie properties.

In brief: `add_nodes()`/`delete_nodes()` change which nodes are present, `to_labelled()`/`to_unlabelled()` name and anonymise them (check with `is_labelled()`, or set names directly with `$<-`), and `mutate_nodes()` adds or changes nodal attributes (`rename_nodes()` renames them, and assigning `NULL` deletes them).

## Tie properties

On this page: Adding & removing · Attributes · Direction · Weights · Signs

Having dealt with the nodes, we turn to the ties — first which ties are present (adding and removing them), then the attributes they carry. Like nodes, ties can carry attributes of any kind; and three tie attributes are common and consequential enough to get their own reserved names and verbs — a tie’s direction , its weight , and its sign . These three are reformatting properties — changing them never changes the number of nodes — and for each we also show how to simplify it away, since many methods expect a plain, undirected, unweighted, unsigned network.

### Adding & removing

`add_ties()` adds ties between named or indexed nodes, and `delete_ties()` removes them — by index, or by naming the tie with `|` between its two endpoints. Add a tie between the first and third adolescents, then delete the tie between Carol and Tina.

    add_ties(ison_adolescents, list(1, 3))
    delete_ties(ison_adolescents, "Carol|Tina")

With the ties in place, we can turn to what they carry.

### Attributes

Just as `mutate_nodes()` attaches variables to nodes, `mutate_ties()` attaches them to ties — a strength, a date, a category, whatever your data records (`add_tie_attribute()` is the more `{igraph}`-like alternative). Add a `weight` to each of the adolescents’ 10 friendships.

    ison_adolescents |> mutate_ties(weight = 1:10)

The `_ties` suffix carries across the `{dplyr}`-style verbs (`mutate_ties()`, `rename_ties()`, `filter_ties()`, `select_ties()`), just as `_nodes` does for nodes, and assigning `NULL` deletes a tie attribute. The weight we just added is one of the special tie attributes that `{manynet}` reserves a name and verbs for; the next three subsections cover direction, weight, and sign in turn.

### Direction

Ties either have a direction — who nominated whom, who exports to whom — or they do not. An undirected network treats every tie as mutual, while a directed network distinguishes each arc ’s sender and receiver. This matters practically because many measures and models are only defined for one or the other.

A common task is fixing direction after import. When an edgelist is imported, `{manynet}` cannot always tell whether ties are meant to be directed, and a heuristic used during the import may return a directed network where an undirected one was intended. Import the `data/adols.csv` file with `read_edgelist()`, make it an igraph-class object, and then make it undirected.

    # Chain the three steps with pipes:
    # read the edgelist, then coerce with as_igraph(),
    # then reformat with to_undirected().

    read_edgelist("data/adols.csv") |> as_igraph() |> to_undirected()

To check your result: the printout should describe an undirected network with 8 nodes and 10 ties, matching the original `ison_adolescents`.

`{manynet}` includes a full set of direction verbs:

- `to_undirected()` merges arcs into undirected ties (a tie appears if an arc exists in either direction)
- `to_directed()` makes undirected ties directed (or, if already directed, does nothing)
- `to_redirected()` swaps the direction of every arc — useful when “who asks whom for advice” should become “who gives advice to whom”
- `to_reciprocated()` adds a reciprocal arc for every existing arc, so that all ties become mutual (see reciprocity )
- `to_acyclic()` removes just enough arcs to eliminate all cycles

Direction interacts with tie counts in ways worth seeing for yourself. `ison_networkers` is a directed network of messages among 32 researchers. Check how many (directed) ties it has, and how many remain after `to_undirected()`.

    # net_ties() counts ties; run it on the original network
    # and on its to_undirected() version.

    net_ties(ison_networkers)
    net_ties(to_undirected(ison_networkers))

### Weights

In a weighted network, ties carry a numeric value — a count of messages, a volume of trade, a strength of friendship. `is_weighted()` checks for weights, and `tie_weights()` returns them as a vector, one value per tie.

Many methods expect a binary network though, and even when weights can be used, you may want to concentrate on the strongest ties. `to_unweighted()` binarises (dichotomises) a network: by default it keeps every tie with weight at least 1, but the `threshold` argument lets you choose the cut-off. Explore the weights of `ison_networkers`, then keep only the pairs who exchanged at least 100 messages.

    # Try summary(tie_weights(_____)) to see the distribution,
    # then to_unweighted(_____, threshold = 100).

    summary(tie_weights(ison_networkers))
    to_unweighted(ison_networkers, threshold = 100)

To check your results: the weights range from 2 to 559 messages, and thresholding at 100 leaves a much sparser network of 32 ties — the backbone of heavy correspondents.

Going the other way, `to_weighted()` adds a weight attribute, and weights can also be assigned directly: `mutate_ties(net, weight = ...)` (the general tie-attribute tool from just above) or the operator shorthand `net$weight <- ...`, which works because a value with one entry per tie is stored as a tie attribute.

### Signs

In a signed network, every tie is marked positive or negative. `irps_wwi` is a classic example: the shifting alliances (`+`) and enmities (`-`) among the six great powers of Europe before the First World War. `is_signed()` checks for signs, and `tie_signs()` retrieves them (`1` for positive, `-1` for negative). Print the network and retrieve its signs.

    irps_wwi
    tie_signs(irps_wwi)

Many measures are only defined for unsigned networks, and sometimes the positive and negative ties are best analysed as separate networks — friendship networks and conflict networks often obey quite different logics. `to_unsigned()` extracts one or the other, via its `keep` argument. Split the WWI network into its alliance network and its conflict network, and compare their tie counts.

    # to_unsigned(_____, keep = "positive")
    # to_unsigned(_____, keep = "negative")
    # net_ties() counts each network's ties.

    to_unsigned(irps_wwi, keep = "positive")
    to_unsigned(irps_wwi, keep = "negative")

The reverse, `to_signed()`, adds signs to an unsigned network, either from a logical `mark` vector or (without one) at random.

Your turn: reformatting is easiest to see on a network that already has the property you are changing. Here is one network carrying weight or sign per flavour — check it with `is_weighted()` or `is_signed()`, simplify it with `to_unweighted()` or `to_unsigned()`, and confirm the change with the same check:

Classic | Fiction | Real-world
--- | --- | ---
`ison_networkers` (weighted) | `fict_marvel` (signed) | `irps_wwi` (signed)

After `to_unweighted()`, `is_weighted()` should flip from `TRUE` to `FALSE`; after `to_unsigned()`, `is_signed()` should do the same; and `net_nodes()` should never budge — reformatting never changes the node count. (Signed networks are relatively rare; `fict_marvel` pairs its signs with other complications, so `irps_wwi` is the cleanest one to experiment on.)

In brief: `add_ties()`/`delete_ties()` change which ties are present, `mutate_ties()` attaches attributes to them, `to_undirected()`/`to_directed()`/`to_redirected()`/`to_reciprocated()` reformat tie direction, `to_unweighted()` binarises weighted ties around a chosen `threshold` (and `to_weighted()` adds weights), and `to_unsigned(keep = )` splits a signed network into its positive or negative ties (and `to_signed()` adds signs). Check each property with `is_directed()`/`is_weighted()`/`is_signed()`, and inspect values with `tie_weights()`/`tie_signs()`.

## Multiplex networks

On this page: Joining networks · Layers

So far each network has had a single kind of tie. A multiplex network instead carries several types of tie among the same nodes — friendship and advice, alliance and trade. This page shows one way such networks arise — by joining two networks together — and then how to inspect their layers and pull a single one back out.

### Joining networks

What if the extra ties you want to add are already collected as a second network among the same nodes? `join_ties()` merges another network’s ties into the current one as an additional type of tie. Join a ring network’s ties to the adolescents’ friendships and inspect the resulting tie table.

    ison_adolescents |> join_ties(create_ring(8), attr_name = "ring")

The printout now reports a multiplex network: the `type` column distinguishes the original ties (`orig`) from the ring ties we joined. We look at how to inspect and extract these types of tie in the next subsection, Layers. There are also `bind_*` equivalents (e.g. `bind_ties()`) that stack extra rows onto the existing nodes or ties without creating a new type, and `join_nodes()` for merging in a second network’s nodes.

### Layers

A multiplex network contains several types of tie among the same nodes — remember the multiplex network we just built with `join_ties()` above. `is_multiplex()` checks for multiple tie types, and `layer_names()` lists them. `ison_lawfirm` records three kinds of relationship among the partners and associates of a New England law firm. Find out what they are.

    # is_multiplex(_____) checks, layer_names(_____) lists.

    is_multiplex(ison_lawfirm)
    layer_names(ison_lawfirm)

Most analyses of multiplex networks proceed one layer at a time, and `to_uniplex()` extracts a single layer by name. Extract the friendship layer from `ison_lawfirm`.

    # to_uniplex(_____, tie = "friends")

    to_uniplex(ison_lawfirm, "friends")

To check your result: the friendship layer has 575 of the original network’s 2571 ties, and the printout no longer describes the network as multiplex. Note that the corresponding do-it-yourself route is `filter_ties(ison_lawfirm, type == "friends")` — `to_uniplex()` does this and additionally tidies up the now-redundant type information.

Your turn: try the other layers of `ison_lawfirm`, or explore another multiplex network:

Classic | Fiction | Real-world
--- | --- | ---
`ison_algebra` (3 layers) | `fict_thrones` (marriage/other) | `irps_911` (association/trust)

In brief: `join_ties()` merges a second network’s ties in as a new type, producing a multiplex network. `is_multiplex()` checks for multiple tie types and `layer_names()` lists them, while `to_uniplex(tie = )` extracts a single layer by name (the do-it-yourself equivalent of `filter_ties(type == ...)`).

## Multimodal networks

Where the previous page bundled several kinds of tie among one set of nodes, a multimodal network has more than one kind of node. The commonest case is a two-mode (or bipartite) network, where ties connect two distinct node sets — such as people and the events they attend. Since most network methods assume a single set of nodes, the key task is moving from two modes to one, usually by projection .

### Modes

`is_twomode()` checks whether a network has two modes, and projection transforms it into ties among just one of them: `to_mode1()` projects onto shared ties among the first node set, and `to_mode2()` onto the second. For more information on projection, see for example Knoke et al. (2021). Note that these are transforming functions: projection changes the number of nodes.

Let’s try this out on a classic two-mode network, `ison_southern_women`. Assign and name the transformed networks something sensible using e.g. `women <- to_mode...` so that we can continue working with this data afterwards. To assign and immediately print the result, wrap the line in parentheses.

    # Print the original first, then project it both ways:
    # ison_southern_women
    # (s_women <- to_mode1(_____))
    # (s_events <- to_mode2(_____))

    ison_southern_women
    (s_women <- to_mode1(ison_southern_women))
    (s_events <- to_mode2(ison_southern_women))

Compare the three printouts: the original two-mode network of women attending events has become (1) a one-mode network among the women, where a tie means two women attended one or more of the same events, and (2) a one-mode network among the events, where a tie means two events shared one or more attendees.

Now use describing functions from the “Making Network Data” tutorial on the two projections you have created to find out:

1. how many nodes there are in each of these networks,
2. what the names of the nodes are, and
3. what tie attributes there are in the networks.

    # Recall the naming convention:
    # net_nodes() counts nodes, node_labels() lists names,
    # and tie attributes are listed with net_tie_attributes().

    net_nodes(s_women)
    net_nodes(s_events)
    node_labels(s_women)
    node_labels(s_events)
    net_tie_attributes(s_women)
    net_tie_attributes(s_events)

To check your results: the women projection has 18 nodes and the events projection 14, together the 32 nodes of the original network.

So we can see that the `to_mode*()` functions have created a network of only one of the modes in the network. The ties in these projected networks, representing shared connections to nodes of the other mode, are weighted — which connects back to the Weights section: everything you learned there (`tie_weights()`, `to_unweighted()`, thresholds) applies to projections too.

Retrieve the tie weights from your women projection. Find the average (mean) of this vector of tie weights, and the average (mean) tie weight overall.

    # Fill in the blank (this is a template, not runnable code):
    # tie_weights(_____)

    # Fill in the blank (this is a template, not runnable code):
    # mean(tie_weights(_____))

    # For the overall average, include also the pairs with no tie,
    # by taking the mean over the whole (weighted) adjacency matrix:
    # mean(as_matrix(_____))

    tie_weights(s_women)
    mean(tie_weights(s_women))
    mean(as_matrix(s_women))

Ok, so now we know that projection transforms an (unweighted) two-mode network into a weighted one-mode network and what these weights represent. Note though that counting the frequency of shared ties to nodes in the other mode is just one (albeit the default) option for how ties in the projection are weighted. The `similarity` argument to `to_mode1()`/`to_mode2()` also offers the Jaccard index, Rand simple matching coefficient, Pearson coefficient, and Yule’s Q. These may be of interest if, for example, overlap should be weighted by participation.

Projection is not the only way between modes:

- `to_onemode()` simply forgets the mode distinction, returning all 32 nodes in a single set — useful for methods that need a one-mode object but should keep both node sets
- `to_multilevel()` similarly retains all nodes but keeps the mode information, treating the network as multilevel so that within-mode ties may be added
- `to_twomode()` goes the other way, splitting a one-mode network into two modes according to a logical `mark` vector

Your turn: project a different two-mode network and inspect its modes. `irps_revere` is a famous historical example (Paul Revere’s ties to Boston organisations on the eve of the American Revolution):

Classic | Real-world
--- | ---
`ison_southern_women` (women × events) | `irps_revere` (people × organisations)

Whatever you pick, `is_twomode()` should return `TRUE` before projection and `FALSE` after, and `is_weighted()` should return `TRUE` on the projection. (The fiction two-mode networks such as `fict_actually` are multiplex, so projecting them first needs a single tie type extracted with `to_uniplex()` — you now know how!)

In brief: `is_twomode()` checks for two modes, and `to_mode1()`/`to_mode2()` project a two-mode network into a weighted one-mode network of shared ties. `to_onemode()`, `to_twomode()`, and `to_multilevel()` move between modes without projecting.

## Dynamic networks

On this page: Waves · Changes

Networks are rarely static: friendships form and fade, democracy diffuses, characters die. `{manynet}` distinguishes three ways time can enter a network:

- a longitudinal network is observed in discrete waves or panels, recorded in a `wave` tie attribute — check with `is_longitudinal()`
- a dynamic network has ties appearing and disappearing in continuous time, e.g. with `begin`/`end` attributes — check with `is_dynamic()`
- a changing network is where one or more nodal attributes are changing over time

### Waves

`fict_potter` records support ties among Harry Potter characters across all the books in the series. Print it, check that it is longitudinal, and count its waves with `net_waves()`.

    # fict_potter
    # is_longitudinal(_____)
    # net_waves(_____)

    fict_potter
    is_longitudinal(fict_potter)
    net_waves(fict_potter)

Note the `wave` column in the ties table and the `active` column in the nodes table — the two reserved attributes doing the longitudinal work here — as well as the changes table between them (more on that below).

Two functions extract time-specific data from a longitudinal network:

- `to_time()` returns the network as it stood at one wave — a single, ordinary network you can analyse with everything above (`to_wave()` is an alias, if you prefer the wave-based wording)
- `to_waves()` splits the network into a list of networks, one per wave, handy for comparing or looping over periods

Extract the network as it stood in the third book, and then split the whole series into waves.

    # to_time(fict_potter, 3)
    # pott_waves <- to_waves(fict_potter)
    # length(pott_waves)

    to_time(fict_potter, 3)
    pott_waves <- to_waves(fict_potter)
    length(pott_waves)

To check your results: the wave-3 network has 48 nodes — fewer than the full cast of 64, because only characters active in that book are retained — and `to_waves()` returns a list of 6 networks.

### Changes

Where do those ‘active’ switches come from? Longitudinal `{manynet}` networks can carry a changelog: a table of node changes with columns `time`, `node`, `var`, and `value`, each row recording that a node’s attribute takes a new value from some time on. `fict_starwars` records who converses with whom in the Star Wars films, with changes tracking characters’ comings, goings, and even Anakin’s changing allegiances. The changelog prints between the node and tie tables, and can be manipulated with the `*_changes()` family of verbs (`filter_changes()`, `mutate_changes()`, `select_changes()`, etc.). Print the network, then filter its changes to see Anakin’s trajectory.

    # fict_starwars
    # filter_changes(fict_starwars, node == "Anakin")

    fict_starwars
    filter_changes(fict_starwars, node == "Anakin")

To make the changes take effect, `apply_changes()` returns the network with all changes up to a given time applied to the node attributes (`gather_changes()` shows the same information as a table without applying it, and `to_time()` does the applying and tie-filtering in one step). Apply the changes up to episode 3 and check what Anakin has become.

    # eps3 <- apply_changes(fict_starwars, time = 3)
    # then inspect e.g. the faction attribute:
    # eps3 |> filter_nodes(name == "Anakin")

    eps3 <- apply_changes(fict_starwars, time = 3)
    eps3 |> filter_nodes(name == "Anakin")

Changes can also be added: `add_changes()` and `bind_changes()` record new node changes on a network you are constructing yourself, using the same `time`/`node`/`var`/`value` structure. Together with `to_waves()` this is the foundation for the dynamic and diffusion analyses covered in later tutorials (in the `{netrics}` and `{migraph}` packages).

Going further: In stocnet objects, there is also a ‘globals’ section that carries globally changing variables e.g. a variable that is constant across all nodes/ties in the network, but that changes over time.

In brief: Longitudinal networks store panel observations in a `wave` tie attribute and node changes in a changelog. `net_waves()` counts the waves, `to_time()` extracts the network at one time point, `to_waves()` splits it into a list of per-wave networks, and `filter_changes()`/`apply_changes()` inspect and apply the recorded node changes.

## Modifying networks

On this page: Subgraphs · Isolates

By now you can build up a network with all of its properties — labels and attributes, direction, weights and signs, layers, modes, and dynamics. Analysis often needs the opposite move: taking that comprehensive object and narrowing it down to just the part that matters. The verbs on this page all take a whole network and hand back a smaller or cleaner one.

### Subgraphs

Earlier, `delete_nodes()` and `delete_ties()` removed elements you named. More often you want to keep elements that satisfy a condition, which is what the `filter_*()` verbs and `to_subgraph()` do — usually the more useful direction for analysis. Keep only the Gryffindors from `fict_potter`, a network of support among Harry Potter characters.

    # The nodes have a "house" attribute:
    # filter_nodes(fict_potter, house == "Gryffindor")
    # to_subgraph(fict_potter, house == "Gryffindor") is equivalent.

    fict_potter |> filter_nodes(house == "Gryffindor")

To check your result: 25 of the 64 characters remain, along with only the ties among them (this is called an induced subgraph ). Filtering on tie attributes instead uses `filter_ties()`.

### Isolates

Deleting or filtering ties can leave nodes stranded without any ties. Depending on your question, such isolates may be important actors (who is not integrated?) or clutter. Two transforming functions prune them:

- `to_no_isolates()` drops all nodes without ties
- `to_giant()` keeps only the largest component , dropping smaller disconnected fragments as well as isolates

Betty’s only friendship is with Sue. Delete that tie and watch the node count before and after `to_no_isolates()`.

    # stranded <- delete_ties(ison_adolescents, "Betty|Sue")
    # then compare net_nodes(stranded) with
    # net_nodes(to_no_isolates(stranded))

    stranded <- delete_ties(ison_adolescents, "Betty|Sue")
    net_nodes(stranded)
    net_nodes(to_no_isolates(stranded))

A related cleaning task concerns loops (self-ties): `to_simplex()` removes them, turning a complex network into a simplex one.

In brief: `filter_nodes()`/`filter_ties()` and `to_subgraph()` keep only the elements satisfying a condition, while `to_no_isolates()`, `to_giant()`, and `to_simplex()` prune stranded nodes, minor components, and self-ties — all taking a whole network and returning a narrower or cleaner one.

## Summary

### Putting it together

Real-world data often needs several of this tutorial’s verbs in sequence. `irps_blogs` is a large directed network of 1490 political blogs. Suppose we only want the well-connected core, as a simple undirected network. We can chain reformatting and transforming verbs to get there. Run this and watch the tie and node counts fall as the network is tidied.

    irps_blogs
    irps_blogs |> to_undirected() |> to_giant()

Notice how `to_undirected()` keeps all 1490 nodes but merges reciprocal ties, while `to_giant()` then drops every node outside the main component. This kind of short pipeline — coerce, reformat, transform — is the everyday work of getting real network data into shape.

One last free play: build your own cleaning pipeline. Pick any dataset from `table_data()` and chain at least three verbs from different sections of this tutorial — for example extract a layer, undirect it, threshold its weights, and drop the isolates — checking with `is_*()`, `net_nodes()`, and `net_ties()` at each step that it did what you expected.

### Function overview

Well done — you have completed the tutorial on manipulating network data! Along the way, you have learned to use these functions:

Function | What it does
--- | ---
`[`, `[[`, `$` | examine and assign into a network directly
`add_nodes()`, `add_ties()`, `delete_nodes()`, `delete_ties()` | add/remove nodes and ties
`filter_nodes()`, `filter_ties()`, `to_subgraph()` | keep elements satisfying a condition
`to_no_isolates()`, `to_giant()`/`to_component()`, `to_simplex()` | prune isolates, minor components, self-ties
`to_labelled()`, `to_unlabelled()` | name or anonymise nodes
`mutate_nodes()`/`mutate_ties()`, `rename_nodes()`/`rename_ties()`, `delete_node_attribute()`/`delete_tie_attribute()` | add, change, rename, or delete attributes
`to_undirected()`, `to_directed()`, `to_redirected()`, `to_reciprocated()`, `to_acyclic()` | reformat tie direction
`to_unweighted()`, `to_weighted()` | binarise (around a `threshold`) or weight ties
`to_unsigned(keep = )`, `to_signed()`, `tie_signs()` | split or create signed networks
`join_ties()`, `bind_ties()`, `join_nodes()` | merge another network’s ties or nodes in
`to_uniplex(tie = )`, `layer_names()` | extract a layer from a multiplex network
`to_mode1()`, `to_mode2()`, `to_onemode()`, `to_twomode()`, `to_multilevel()` | move between modes
`net_waves()`, `to_time()`/`to_wave()`, `to_waves()` | count and extract waves of a longitudinal network
`filter_changes()`, `apply_changes()`, `add_changes()` | inspect and apply node changes over time
`is_labelled()`, `is_directed()`, `is_weighted()`, `is_signed()`, `is_multiplex()`, `is_twomode()`, `is_longitudinal()`, … | check any of these properties

From here, you can continue with the tutorials on visualising networks (in the `{autograph}` package) or on measuring and analysing them (in the `{netrics}` package).

### Glossary

Here are some of the terms that we have covered in this tutorial:

Arc
  An ordered pair of nodes indicating a directed tie or edge from a tail to a head.
Changing
  A changing network is one where nodal attributes, including whether the nodes are present or not, can change.
Complex
  A complex network is one that includes or can include loops or self-ties.
Component
  A component is a connected subgraph not part of a larger connected subgraph.
Directed
  A directed network is a network where the ties have a direction, from a sender to a receiver.
Dynamic
  A dynamic network is one where ties appear or disappear at recorded points in continuous time.
Induced
  An induced subgraph comprises all ties in a subset of the nodes in a network.
Isolate
  An isolate is a node with degree equal to zero.
Label
  A labelled network includes unique labels for each node (or ties) in the network.
Longitudinal
  A longitudinal network is one observed in two or more discrete waves or panels over time.
Loop
  A loop is a self-tie with a single node as both endpoints, forming a cycle of length 1.
Multilevel
  A network of more than one set of nodes that includes ties both between and within the different node sets.
Multimodal
  A network that includes more than one set of nodes.
Multiplex
  A network that includes multiple types of tie.
Neighborhood
  The neighborhood of a node is the set of other nodes to which that node is connected.
Order
  The order of a network is the number of its nodes.
Projection
  A projection reduces a two-mode network to a one-mode network of shared ties to the other mode among one set of its nodes.
Reciprocity
  A measure of how often nodes in a directed network are mutually linked.
Signed
  A signed network is one where ties are marked as positive or negative, such as friendship and enmity or alliance and conflict.
Simplex
  A simplex network is one without loops or multiple adjacencies.
Subgraph
  A subgraph comprises a subset of the nodes and ties in a network.
Twomode
  A two-mode (or bipartite) network is a network with two different sets of nodes, where ties connect only nodes from different sets, such as people and the events they attend.
Undirected
  An undirected or line network is one in which tie direction is undefined.
Weighted
  A weighted network is where the ties have been assigned weights.

# Manipulating Network Data

#### by James Hollway