Chapter 5 Ego-network structure

5.1 Overview

Ego-network structure refers to the distribution of ties among alters. As usual, we first illustrate analyses on one ego-network, then replicate them on many ego-networks at once.

This chapter covers the following topics:

  • Calculating measures of ego-network structure.
  • R lists and how they can be used to represent many ego-networks.
  • Split-apply-combine on lists with purrr to analyze the structure of many ego-networks at once.

5.2 Measures of ego-network structure

  • Ego-network structure can be described using different measures, either at the alter level (e.g., alter centrality measures) or at the ego level (e.g., ego-network density).
  • Some of these structural measures are calculated on the alter-alter network excluding the ego. Other measures (e.g., ego betweenness, constraint) are calculated on the ego-network including the ego. We will see examples of both.
  • Certain measures combine information about both structure and composition: they are based on data about both the distribution of alter-alter ties and the distribution of alter attributes. An example is the average degree centrality (network structure) of alters who are family members (network composition).
  • Note that whenever ego-network structure is considered (whether by itself or in combination with composition), we need data on alter-alter ties. Unlike in Chapter 4, just the alter attribute data frame will not be sufficient.
    • So in this section we need to work with the igraph objects containing the alter-alter tie information for our ego-networks (and possibly also incorporate alter attribute data frames if our measures are a combination of structure and composition).
  • What we do in the following code.
    • Consider ego-network structural measures based on the distribution of alter-alter ties, with the ego excluded: density, number of components, average alter degree, maximum alter betweenness, number of isolates. Calculate these using igraph functions.
    • Consider structural measures that require the ego to be included in the igraph object: ego betweenness, constraint. Calculate these with igraph.
    • Calculate measures combining ego-network structure and composition: type of relationship between ego and the most between-central alter; average degree centrality of alters who are family members; density of ties among alters in certain categories (e.g., alters who live in Sri Lanka).
# Load packages.
library(tidyverse)
library(igraph)

# Load data.
load("./Data/data.rda")

# For structural measures we need the ego-network as an igraph.

# The data.rda file, which we loaded earlier, includes the igraph object of
# ego ID 28's network.
gr.28
## IGRAPH 6e06ab7 UNW- 45 259 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from 6e06ab7 (vertex names):
##  [1] 2801--2802 2801--2803 2801--2804 2801--2805 2801--2806 2801--2807
##  [7] 2801--2808 2801--2809 2801--2810 2801--2811 2801--2812 2801--2813
## [13] 2801--2814 2801--2815 2801--2818 2801--2820 2801--2823 2801--2825
## [19] 2801--2827 2801--2828 2801--2829 2801--2831 2801--2840 2801--2841
## [25] 2802--2803 2802--2804 2802--2805 2802--2806 2802--2807 2802--2808
## + ... omitted several edges
# Let's reassign it to a new, generic object name. This makes the code more 
# generic and more easily re-usable on any ego-network igraph object 
# (of any ego ID).
gr <- gr.28

# Measures based only on the structure of alter-alter ties                  ----
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

# Structural characteristics of the network.

# Network density.
edge_density(gr)
## [1] 0.2616162
# Number of components.
components(gr)
## $membership
## 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 
##    1    1    1    1    1    1    1    1    1    1    1    1    1    1    1    1 
## 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 
##    1    1    1    1    1    1    1    1    1    1    1    1    1    1    1    1 
## 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 
##    1    1    1    1    1    1    1    1    1    1    1    1    1 
## 
## $csize
## [1] 45
## 
## $no
## [1] 1
# This is a list, we only need its 3rd element (number of components).
components(gr)$no
## [1] 1
# Summarization of structural characteristics of the alters.

# Average alter degree.
degree(gr) |> mean()
## [1] 11.51111
# Max alter betweenness.
betweenness(gr, weights = NA) |> max()
## [1] 257.0168
# Number of "isolate" alters.

# Check out the alter degree vector.
degree(gr)
## 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 
##   24   17   13   12   12   13   13   12   10    9   11    8   18   14   16    9 
## 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 
##    9   10    2    2    9   18   27   13   10    8    4    3    3    3   15   16 
## 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 
##   16   12   21    3   10   14   14   16   13    7   12    5   12
# Count the number of isolates, i.e. alters with degree==0
sum(degree(gr)==0)
## [1] 0
# All sorts of more complicated structural analyses can be run on an ego-network
# using igraph or statnet functions. For example, here are the results of the
# Girvan-Newman community-detection algorithm on the ego-network.
cluster_edge_betweenness(gr, weights= NA)
## IGRAPH clustering edge betweenness, groups: 10, mod: 0.41
## + groups:
##   $`1`
##    [1] "2801" "2802" "2803" "2804" "2805" "2806" "2807" "2808" "2809" "2810"
##   [11] "2811" "2812" "2840" "2841"
##   
##   $`2`
##   [1] "2813" "2814" "2815" "2816" "2817" "2818" "2824" "2831"
##   
##   $`3`
##   [1] "2819" "2836"
##   
##   + ... omitted several groups/vertices
# What if we want to calculate the same structural measure (e.g. density) on all
# ego-networks? We can take the list of all ego-networks and run the same 
# function on every list element with the purrr package in tidyverse.

# List that contains all our ego-networks.
class(gr.list)
## [1] "list"
length(gr.list)
## [1] 102
head(gr.list)
## $`28`
## IGRAPH 6e06ab7 UNW- 45 259 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from 6e06ab7 (vertex names):
##  [1] 2801--2802 2801--2803 2801--2804 2801--2805 2801--2806 2801--2807
##  [7] 2801--2808 2801--2809 2801--2810 2801--2811 2801--2812 2801--2813
## [13] 2801--2814 2801--2815 2801--2818 2801--2820 2801--2823 2801--2825
## [19] 2801--2827 2801--2828 2801--2829 2801--2831 2801--2840 2801--2841
## [25] 2802--2803 2802--2804 2802--2805 2802--2806 2802--2807 2802--2808
## + ... omitted several edges
## 
## $`29`
## IGRAPH d815457 UNW- 45 202 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from d815457 (vertex names):
##  [1] 2901--2902 2901--2904 2901--2906 2901--2907 2901--2908 2901--2909
##  [7] 2901--2910 2901--2911 2901--2915 2901--2916 2901--2917 2901--2918
## [13] 2901--2919 2901--2927 2901--2928 2901--2929 2901--2930 2901--2931
## [19] 2901--2936 2901--2942 2901--2945 2902--2903 2902--2904 2902--2905
## [25] 2902--2906 2902--2907 2902--2908 2902--2909 2902--2910 2902--2911
## + ... omitted several edges
## 
## $`33`
## IGRAPH 52ba8e8 UNW- 45 207 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from 52ba8e8 (vertex names):
##  [1] 3301--3302 3301--3303 3301--3304 3301--3305 3301--3306 3301--3307
##  [7] 3301--3308 3301--3309 3301--3310 3301--3311 3301--3312 3301--3313
## [13] 3301--3314 3301--3315 3301--3316 3301--3317 3301--3318 3301--3319
## [19] 3301--3320 3301--3321 3301--3322 3301--3323 3302--3303 3302--3304
## [25] 3302--3305 3302--3306 3302--3307 3302--3308 3302--3309 3302--3310
## + ... omitted several edges
## 
## $`35`
## IGRAPH dcca465 UNW- 45 221 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from dcca465 (vertex names):
##  [1] 3501--3502 3501--3503 3501--3504 3501--3505 3501--3506 3501--3507
##  [7] 3501--3508 3501--3509 3501--3510 3501--3512 3501--3513 3501--3514
## [13] 3501--3516 3501--3517 3501--3522 3501--3523 3501--3524 3501--3525
## [19] 3501--3526 3501--3527 3501--3528 3501--3529 3501--3530 3501--3532
## [25] 3501--3533 3501--3534 3501--3535 3501--3536 3501--3540 3501--3543
## + ... omitted several edges
## 
## $`39`
## IGRAPH 2467484 UNW- 45 92 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from 2467484 (vertex names):
##  [1] 3901--3902 3901--3903 3901--3904 3901--3905 3901--3906 3901--3907
##  [7] 3901--3911 3901--3913 3901--3920 3901--3924 3901--3925 3901--3931
## [13] 3901--3932 3901--3933 3901--3937 3901--3938 3901--3940 3901--3941
## [19] 3902--3903 3902--3904 3902--3905 3902--3906 3902--3907 3902--3908
## [25] 3902--3909 3902--3910 3902--3913 3902--3924 3902--3925 3902--3931
## + ... omitted several edges
## 
## $`40`
## IGRAPH 0d62906 UNW- 45 255 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from 0d62906 (vertex names):
##  [1] 4001--4003 4001--4007 4001--4008 4001--4009 4001--4036 4001--4040
##  [7] 4001--4045 4002--4003 4002--4004 4002--4006 4002--4008 4002--4009
## [13] 4002--4010 4002--4011 4002--4012 4002--4013 4002--4014 4002--4015
## [19] 4002--4016 4002--4017 4002--4021 4002--4029 4002--4036 4002--4037
## [25] 4002--4038 4002--4040 4002--4041 4002--4042 4002--4043 4002--4044
## + ... omitted several edges
# We can use purrr::map() to run the same function on every element of the list.
purrr::map_dbl(gr.list, edge_density)
##         28         29         33         35         39         40         45 
## 0.26161616 0.20404040 0.20909091 0.22323232 0.09292929 0.25757576 0.18484848 
##         46         47         48         49         51         52         53 
## 0.26969697 0.53737374 0.20303030 0.42828283 0.16969697 0.12929293 0.16565657 
##         55         56         57         58         59         60         61 
## 0.23838384 0.22828283 0.60000000 0.20505051 0.50505051 0.28888889 0.37676768 
##         62         64         65         66         68         69         71 
## 0.30202020 0.28282828 0.36060606 0.30909091 0.23333333 0.23636364 0.47777778 
##         73         74         78         79         80         81         82 
## 0.40707071 0.42525253 0.17575758 0.29292929 0.33434343 0.29797980 0.35353535 
##         83         84         85         86         87         88         90 
## 0.38888889 0.27575758 0.12525253 0.18989899 0.24444444 0.40101010 0.45353535 
##         91         92         93         94         95         97         99 
## 0.47171717 0.43535354 0.14141414 0.26767677 0.36767677 0.35050505 0.24040404 
##        102        104        105        107        108        109        110 
## 0.27777778 0.22929293 0.26666667 0.16262626 0.10606061 0.33535354 0.48686869 
##        112        113        114        115        116        118        119 
## 0.13939394 0.23636364 0.28787879 0.33232323 0.22929293 0.37676768 0.33131313 
##        120        121        122        123        124        125        126 
## 0.20101010 0.23838384 0.35959596 0.39090909 0.61111111 0.26464646 0.72828283 
##        127        128        129        130        131        132        133 
## 0.28080808 0.26262626 0.22727273 0.22020202 0.27979798 0.29090909 0.27575758 
##        135        136        138        139        140        141        142 
## 0.22525253 0.28686869 0.19797980 0.31515152 0.29292929 0.26868687 0.21010101 
##        144        146        147        149        151        152        153 
## 0.24949495 0.14747475 0.23030303 0.20707071 0.27272727 0.25151515 0.34242424 
##        154        155        156        157        158        159        160 
## 0.31818182 0.39393939 0.24646465 0.37777778 0.23737374 0.37979798 0.36767677 
##        161        162        163        164 
## 0.41010101 0.41111111 0.35454545 0.32222222
# We'll talk more about this and show more examples in the section about 
# multiple ego-networks.

# Structural measures requiring ego in the network                          ----
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

# We now need the ego-network with ego included. The file data.rda, loaded earlier,
# includes this ego-network for ego ID 28. 
gr.ego.28
## IGRAPH 673bb5f UNW- 46 304 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from 673bb5f (vertex names):
##  [1] 2801--2802 2801--2803 2801--2804 2801--2805 2801--2806 2801--2807
##  [7] 2801--2808 2801--2809 2801--2810 2801--2811 2801--2812 2801--2813
## [13] 2801--2814 2801--2815 2801--2818 2801--2820 2801--2823 2801--2825
## [19] 2801--2827 2801--2828 2801--2829 2801--2831 2801--2840 2801--2841
## [25] 2802--2803 2802--2804 2802--2805 2802--2806 2802--2807 2802--2808
## + ... omitted several edges
# Let's reassign it to another, generic object name.
gr.ego <- gr.ego.28

# Ego's betweenness centrality.
# weights = NA so the function doesn't use the $weight edge attribute to
# calculate weighted betweenness.
betweenness(gr.ego, v = "ego", weights = NA)
##      ego 
## 434.0271
# Ego's constraint.
constraint(gr.ego, nodes= "ego")
## ego 
##  NA
# Measures combining composition and structure: From structure to composition ----
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

# Non-network attribute of alters selected based on structural characteristics.

# Type of relationship with alter with max betweenness.

# Logical index for alter with max betweenness.
ind <- betweenness(gr, weights = NA) == max(betweenness(gr, weights = NA))

# Get that alter.
V(gr)[ind]
## + 1/45 vertex, named, from 6e06ab7:
## [1] 2801
# Get type of relation of that alter.
V(gr)[ind]$alter.rel
## [1] "Close family"
# Note that there might be multiple alters with the same (maximum) value of 
# betweenness. For that case, we'll need more complicated code (see exercise).

# Measures combining composition and structure: From composition to structure ----
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

# Structural characteristics of alters selected based on composition.

# Average degree of Close family.

# Vertex sequence of Close family members.
(clo.fam.vs <- V(gr)[alter.rel=="Close family"])
## + 5/45 vertices, named, from 6e06ab7:
## [1] 2801 2803 2804 2805 2806
# Get their average degree.
gr |>
  degree(v = clo.fam.vs) |> 
  mean()
## [1] 14.8
# Count of ties between alters who live in Sri Lanka.

# First get the vertex sequence of alters who live in Sri Lanka.
(alters.sl <- V(gr)[alter.res=="Sri Lanka"])
## + 17/45 vertices, named, from 6e06ab7:
##  [1] 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2827 2828 2829
## [16] 2840 2841
# Then get the edges among them.
E(gr)[alters.sl %--% alters.sl]
## + 88/259 edges from 6e06ab7 (vertex names):
##  [1] 2801--2802 2801--2803 2801--2804 2801--2805 2801--2806 2801--2807
##  [7] 2801--2808 2801--2809 2801--2810 2801--2811 2801--2812 2801--2827
## [13] 2801--2828 2801--2829 2801--2840 2801--2841 2802--2803 2802--2804
## [19] 2802--2805 2802--2806 2802--2807 2802--2808 2802--2809 2802--2810
## [25] 2802--2811 2802--2812 2802--2840 2802--2841 2803--2804 2803--2805
## [31] 2803--2806 2803--2807 2803--2808 2803--2809 2803--2810 2803--2811
## [37] 2803--2812 2803--2840 2803--2841 2804--2805 2804--2806 2804--2807
## [43] 2804--2808 2804--2809 2804--2810 2804--2811 2804--2840 2804--2841
## [49] 2805--2806 2805--2807 2805--2808 2805--2809 2805--2810 2805--2811
## [55] 2805--2840 2805--2841 2806--2807 2806--2808 2806--2809 2806--2810
## + ... omitted several edges
# How many edges are there between alters who live in Sri Lanka?
E(gr)[alters.sl %--% alters.sl] |> 
  length()
## [1] 88
# Density between alters who live in Sri Lanka.

# Get subgraph of relevant alters
induced_subgraph(gr, vids = alters.sl)
## IGRAPH 9153f74 UNW- 17 88 -- 
## + attr: .egoID (g/n), ego_ID (g/c), name (v/c), alter_num (v/n),
## | alter.sex (v/c), alter.age.cat (v/c), alter.rel (v/c), alter.nat
## | (v/c), alter.res (v/c), alter.clo (v/n), alter.loan (v/c), alter.fam
## | (v/c), alter.age (v/n), weight (e/n)
## + edges from 9153f74 (vertex names):
##  [1] 2801--2802 2801--2803 2802--2803 2801--2804 2802--2804 2803--2804
##  [7] 2801--2805 2802--2805 2803--2805 2804--2805 2801--2806 2802--2806
## [13] 2803--2806 2804--2806 2805--2806 2801--2807 2802--2807 2803--2807
## [19] 2804--2807 2805--2807 2806--2807 2801--2808 2802--2808 2803--2808
## [25] 2804--2808 2805--2808 2806--2808 2807--2808 2801--2809 2802--2809
## + ... omitted several edges
# Get the density of this subgraph.
induced_subgraph(gr, vids = alters.sl) |> 
  edge_density()
## [1] 0.6470588

5.3 R lists

  • A lists is simply a collection of objects. It can contain any kind of object, with no restriction. A list can contain other lists.
  • Lists have list as type and class.
  • Data frames are a type of list. Other complex objects in R are also stored as lists (have list as type), although their class is not list: for example, results from statistical estimations or network community detection procedures.
  • Use str(list) to display the types and lengths of elements in a list.
  • List may be named, that is, have element names. You can view or assign names with the names function (base R) or the set_names function (tidyverse).
  • Three different notations to index lists:
    1. [ ] notation, e.g. my.list[3].
    2. [[ ]] notation, e.g. my.list[[3]] or my.list[["element.name"]].
    3. The $ notation. This only works for named lists. E.g., list$element.name. This is the same as the [[ ]] notation: list$element.name is the same as list[["element.name"]] or list[[i]] (where i is the position of the element called element.name in the list).
  • These three indexing methods work in exactly the same way as for data frames (see Section 2.5.1).
  • What we do in the following code.
    • Create, display, and index a list.
# Let's get some objects to put in a list.

# A simple numeric vector.
(num <- 1:10)
##  [1]  1  2  3  4  5  6  7  8  9 10
# A matrix.
(mat <- matrix(1:4, nrow=2, ncol=2))
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
# A character vector.
(char <- colors()[1:5])
## [1] "white"         "aliceblue"     "antiquewhite"  "antiquewhite1"
## [5] "antiquewhite2"
# Create a list that contains all these objects.
L <- list(num, mat, char)

# Display it
L
## [[1]]
##  [1]  1  2  3  4  5  6  7  8  9 10
## 
## [[2]]
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
## 
## [[3]]
## [1] "white"         "aliceblue"     "antiquewhite"  "antiquewhite1"
## [5] "antiquewhite2"
# Create a named list
L <- list(numbers= num, matrix= mat, colors= char)  

# Display it
L
## $numbers
##  [1]  1  2  3  4  5  6  7  8  9 10
## 
## $matrix
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
## 
## $colors
## [1] "white"         "aliceblue"     "antiquewhite"  "antiquewhite1"
## [5] "antiquewhite2"
# Type and class
typeof(L)
## [1] "list"
class(L)
## [1] "list"
# Extract the first element of L
# [ ] notation (result is a list containing the element).
L[1]
## $numbers
##  [1]  1  2  3  4  5  6  7  8  9 10
# [[ ]] notation (result is the element itself, no longer in a list).
L[[1]]
##  [1]  1  2  3  4  5  6  7  8  9 10
# $ notation (result is the element itself, no longer in a list).
L$numbers
##  [1]  1  2  3  4  5  6  7  8  9 10
# Name indexing.
L[["numbers"]]
##  [1]  1  2  3  4  5  6  7  8  9 10
# Types of elements in L.
str(L)
## List of 3
##  $ numbers: int [1:10] 1 2 3 4 5 6 7 8 9 10
##  $ matrix : int [1:2, 1:2] 1 2 3 4
##  $ colors : chr [1:5] "white" "aliceblue" "antiquewhite" "antiquewhite1" ...

5.4 Analyzing the structure of many ego-networks

  • In base R, functions of the apply family, such as lapply and sapply, are the traditional way to take a function and apply it to every element of a list. In tidyverse, the purrr package does the same thing but in more efficient ways and with more readable code.
  • We use purrr to apply the same function to each element of a list of ego-networks, that is, to each ego-network. Depending on the function, the result for each ego-network may be a single number or a more complex object (for example, a data frame).
  • purrr provides type-stable functions, which always return the same type of output:
    • map always returns a list. (This is the equivalent of lapply in base R).
    • map_dbl always returns a vector of double-precision numbers. map_int returns a vector of integer numbers. map_chr returns a character vector. map_lgl returns a logical vector.
    • map_dfr returns a data frame. It assumes that you are applying an operation to each list element, which returns one data frame row for that element. It then binds together the data frame rows obtained for each element, combining them into a single data frame.
  • In addition to type-stable output, the map functions also offer a convenient formula syntax: ~ f(.x), where: (1) .x represents each element of the input list; (2) f is the function to be executed on that element.
    • For example, map(L, ~ .x * 2) takes each element of the list L (represented by .x) and multiplies it times 2.
  • The map functions preserve list names in their output. If we have a list of ego-networks whose names are the ego IDs, this means that ego IDs will be preserved in result lists and vectors.
  • map is also useful when you want to run functions that take a list element as argument (e.g. an igraph ego-network), and return another list element as output (e.g. another igraph object). This is the case whenever you want to manipulate the ego-networks (for example, only keep a certain type of ties or vertices in the networks) and store the results in a new list.
  • map_dbl is useful when you want to run functions that take a list element as argument (e.g. an igraph ego-network), and return a number as output. This is the case whenever you want to run a structural measure such as tie density or centralization on each network.
  • What we do in the following code.
    • Use purrr functions to calculate the same structural measures on every ego-network in the data.
# A simple structural measure such as network density can be applied to every
# ego-network (i.e., every element of our list), and returns a scalar for each
# network. All the scalars can be put in a vector.
gr.list |>
  purrr::map_dbl(edge_density) 
##         28         29         33         35         39         40         45 
## 0.26161616 0.20404040 0.20909091 0.22323232 0.09292929 0.25757576 0.18484848 
##         46         47         48         49         51         52         53 
## 0.26969697 0.53737374 0.20303030 0.42828283 0.16969697 0.12929293 0.16565657 
##         55         56         57         58         59         60         61 
## 0.23838384 0.22828283 0.60000000 0.20505051 0.50505051 0.28888889 0.37676768 
##         62         64         65         66         68         69         71 
## 0.30202020 0.28282828 0.36060606 0.30909091 0.23333333 0.23636364 0.47777778 
##         73         74         78         79         80         81         82 
## 0.40707071 0.42525253 0.17575758 0.29292929 0.33434343 0.29797980 0.35353535 
##         83         84         85         86         87         88         90 
## 0.38888889 0.27575758 0.12525253 0.18989899 0.24444444 0.40101010 0.45353535 
##         91         92         93         94         95         97         99 
## 0.47171717 0.43535354 0.14141414 0.26767677 0.36767677 0.35050505 0.24040404 
##        102        104        105        107        108        109        110 
## 0.27777778 0.22929293 0.26666667 0.16262626 0.10606061 0.33535354 0.48686869 
##        112        113        114        115        116        118        119 
## 0.13939394 0.23636364 0.28787879 0.33232323 0.22929293 0.37676768 0.33131313 
##        120        121        122        123        124        125        126 
## 0.20101010 0.23838384 0.35959596 0.39090909 0.61111111 0.26464646 0.72828283 
##        127        128        129        130        131        132        133 
## 0.28080808 0.26262626 0.22727273 0.22020202 0.27979798 0.29090909 0.27575758 
##        135        136        138        139        140        141        142 
## 0.22525253 0.28686869 0.19797980 0.31515152 0.29292929 0.26868687 0.21010101 
##        144        146        147        149        151        152        153 
## 0.24949495 0.14747475 0.23030303 0.20707071 0.27272727 0.25151515 0.34242424 
##        154        155        156        157        158        159        160 
## 0.31818182 0.39393939 0.24646465 0.37777778 0.23737374 0.37979798 0.36767677 
##        161        162        163        164 
## 0.41010101 0.41111111 0.35454545 0.32222222
# Note that the vector names (taken from gr.list names) are the ego IDs.

# If you want the same result as a nice data frame with ego IDs, use enframe().
gr.list |>
  map_dbl(edge_density) |> 
  enframe(name = "ego_ID", value = "density")
## # A tibble: 102 × 2
##    ego_ID density
##    <chr>    <dbl>
##  1 28      0.262 
##  2 29      0.204 
##  3 33      0.209 
##  4 35      0.223 
##  5 39      0.0929
##  6 40      0.258 
##  7 45      0.185 
##  8 46      0.270 
##  9 47      0.537 
## 10 48      0.203 
## # ℹ 92 more rows
# Same thing, with number of components in each ego network. Note the ~ .x 
# syntax
gr.list |>
  map_dbl(~ components(.x)$no) |> 
  enframe()
## # A tibble: 102 × 2
##    name  value
##    <chr> <dbl>
##  1 28        1
##  2 29        4
##  3 33        7
##  4 35        2
##  5 39       20
##  6 40        2
##  7 45        1
##  8 46        2
##  9 47        1
## 10 48        1
## # ℹ 92 more rows
# With map_dfr() we can calculate multiple structural measures at once on every 
# ego-network, and return the results as a single ego-level data frame.

# This assumes that we are applying an operation that returns one data frame row
# for each ego-network. For example, take one ego-network from our list.
gr <- gr.list[[10]]

# Apply an operation to that ego-network, which returns one data frame row.
tibble(dens = edge_density(gr),
       mean.deg = mean(igraph::degree(gr)),
       mean.bet = mean(igraph::betweenness(gr, weights = NA)),
       deg.centr = centr_degree(gr)$centralization)
## # A tibble: 1 × 4
##    dens mean.deg mean.bet deg.centr
##   <dbl>    <dbl>    <dbl>     <dbl>
## 1 0.203     8.93     33.8     0.365
# Now we can do the same thing but for all ego-networks at once. The result is a 
# single ego-level data frame, with one row for each ego. 
# Note the .id argument in map_dfr().
gr.list |>
  map_dfr(~ tibble(dens= edge_density(.x),
                   mean.deg= mean(igraph::degree(.x)),
                   mean.bet= mean(igraph::betweenness(.x, weights = NA)),
                   deg.centr= centr_degree(.x)$centralization),
          .id = "ego_ID")
## # A tibble: 102 × 5
##    ego_ID   dens mean.deg mean.bet deg.centr
##    <chr>   <dbl>    <dbl>    <dbl>     <dbl>
##  1 28     0.262     11.5     22.9      0.352
##  2 29     0.204      8.98    23.9      0.273
##  3 33     0.209      9.2      4.56     0.291
##  4 35     0.223      9.82    22.0      0.481
##  5 39     0.0929     4.09     7.18     0.316
##  6 40     0.258     11.3     21.3      0.492
##  7 45     0.185      8.13    20.6      0.679
##  8 46     0.270     11.9     19.6      0.503
##  9 47     0.537     23.6     10.5      0.372
## 10 48     0.203      8.93    33.8      0.365
## # ℹ 92 more rows
# ***** EXERCISES
#
# (1) Get the graph for ego ID 28 from gr.list. Get the max betweenness of
# Italian alters on this graph. Based on this code, use map() to get the same
# measure that for all egos.
#
# (2) Given a personal network gr, the subgraph of close family in the personal
# network is induced_subgraph(gr, V(gr)[alter.rel=="Close family"]). Use
# purrr::map() to get the family subgraph for every personal network in gr.list,
# and put the results in a new list called gr.list.family. Use the formula
# notation (~ .x).
#
# *****