Redes escolares

Modified

May 10, 2024

Nota de Traducción

Esta versión del capítulo fue traducida de manera automática utilizando IA. El capítulo aún no ha sido revisado por un humano.

Este capítulo proporciona un ejemplo de principio a fin para procesar datos tipo encuesta en R. El capítulo presenta el conjunto de datos del Estudio de Redes Sociales [SNS]. Puedes descargar los datos para este capítulo aquí, y el libro de códigos para los datos proporcionados aquí está en el apéndice.

Los objetivos para este capítulo son:

  1. Leer los datos en R.

  2. Crear una red con ellos.

  3. Calcular estadísticas descriptivas.

  4. Visualizar la red.

Preprocesamiento de datos

Leyendo los datos en R

R tiene varias formas de leer datos. Tus datos pueden ser archivos de texto plano como CSV, delimitados por tabulaciones, o especificados por ancho de columna. Para leer datos de texto plano, puedes usar el paquete readr (Wickham, Hester, and Bryan 2024). En el caso de archivos binarios, como archivos de Stata, Octave, o SPSS, puedes usar el paquete de R foreign (R Core Team 2023). Si tus datos están formateados como hojas de cálculo de Microsoft, el paquete de R readxl (Wickham and Bryan 2023) es la alternativa a usar. En nuestro caso, los datos para esta sesión están en formato Stata:

library(foreign)

# Leyendo los datos
dat <- foreign::read.dta("03-sns.dta")

# Echando un vistazo a las primeras 5 columnas y 5 filas de los datos
dat[1:5, 1:10]
  photoid school hispanic female1 female2 female3 female4 grades1 grades2
1       1    111        1      NA      NA       0       0      NA      NA
2       2    111        1       0      NA      NA       0     3.0      NA
3       7    111        0       1       1       1       1     5.0     4.5
4      13    111        1       1       1       1       1     2.5     2.5
5      14    111        1       1       1       1      NA     3.0     3.5
  grades3
1     3.5
2      NA
3     4.0
4     2.5
5     3.5

Creando un id único para cada participante

Debemos crear un id único usando la escuela y el id de foto. Dado que ambas variables son numéricas, codificar el id es una buena forma de hacer esto. Por ejemplo, los últimos tres números son el photoid, y los primeros números son el id de la escuela. Para hacer esto, necesitamos tomar en cuenta el rango de las variables:

(photo_id_ran <- range(dat$photoid))
[1]    1 2074

Como la variable se extiende hasta 2074, necesitamos establecer las últimas 4 unidades de la variable para almacenar el photoid. Usaremos dplyr (Wickham et al. 2023) para crear esta variable y la llamaremos id:

library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
# Creando la variable
dat <- dat |>
  mutate(id = school*10000 + photoid)

# Primeras filas
dat |>
  head() |>
  select(school, photoid, id)
  school photoid      id
1    111       1 1110001
2    111       2 1110002
3    111       7 1110007
4    111      13 1110013
5    111      14 1110014
6    111      15 1110015

¡Vaya, qué pasó en las últimas líneas de código! ¿Qué es ese |>? Bueno, ese es el operador pipe1, y es una forma atractiva de escribir llamadas a funciones anidadas. En este caso, en lugar de escribir algo como:

dat_filtered$id <- dat_filtered$school*10000 + dat_filtered$photoid
subset(head(dat_filtered), select = c(school, photoid, id))

Creando una red

  • Queremos construir una red social. Para eso, usamos una matriz de adyacencia o una lista de enlaces.

  • Cada individuo de los datos SNS nominó 19 amigos de la escuela. Usaremos esas nominaciones para crear la red social.

  • En este caso, crearemos la red coercionando el conjunto de datos en una lista de enlaces.

De encuesta a lista de enlaces

Comencemos cargando un par de paquetes útiles de R. Cargaremos tidyr (Wickham, Vaughan, and Girlich 2024) y stringr (Wickham 2023). Usaremos el primero, tidyr, para remodelar los datos. El segundo, stringr, nos ayudará a procesar cadenas usando expresiones regulares2.

library(tidyr)
library(stringr)

Opcionalmente, podemos usar el tipo de objeto tibble, una alternativa al data.frame actual. Este objeto proporciona métodos más eficientes para matrices y marcos de datos.

dat <- as_tibble(dat)

Lo que me gusta de los tibbles es que cuando los imprimes en la consola, estos se ven bien:

dat
# A tibble: 2,164 × 100
   photoid school hispanic female1 female2 female3 female4 grades1 grades2
     <int>  <int>    <dbl>   <int>   <int>   <int>   <int>   <dbl>   <dbl>
 1       1    111        1      NA      NA       0       0    NA      NA  
 2       2    111        1       0      NA      NA       0     3      NA  
 3       7    111        0       1       1       1       1     5       4.5
 4      13    111        1       1       1       1       1     2.5     2.5
 5      14    111        1       1       1       1      NA     3       3.5
 6      15    111        1       0       0       0       0     2.5     2.5
 7      20    111        1       1       1       1       1     2.5     2.5
 8      22    111        1      NA      NA       0       0    NA      NA  
 9      25    111        0       1       1      NA       1     4.5     3.5
10      27    111        1       0      NA       0       0     3.5    NA  
# ℹ 2,154 more rows
# ℹ 91 more variables: grades3 <dbl>, grades4 <dbl>, eversmk1 <int>,
#   eversmk2 <int>, eversmk3 <int>, eversmk4 <int>, everdrk1 <int>,
#   everdrk2 <int>, everdrk3 <int>, everdrk4 <int>, home1 <int>, home2 <int>,
#   home3 <int>, home4 <int>, sch_friend11 <int>, sch_friend12 <int>,
#   sch_friend13 <int>, sch_friend14 <int>, sch_friend15 <int>,
#   sch_friend16 <int>, sch_friend17 <int>, sch_friend18 <int>, …
# Tal vez demasiados pipes... ¡pero es genial!
net <- dat |> 
  select(id, school, starts_with("sch_friend")) |>
  gather(key = "varname", value = "content", -id, -school) |>
  filter(!is.na(content)) |>
  mutate(
    friendid = school*10000 + content,
    year     = as.integer(str_extract(varname, "(?<=[a-z])[0-9]")),
    nnom     = as.integer(str_extract(varname, "(?<=[a-z][0-9])[0-9]+"))
  )

Veamos esto paso a paso:

  1. Primero, subconjuntamos los datos: Queremos mantener id, school, sch_friend*. Para este último, usamos la función starts_with (del paquete tidyselect). Este último nos permite seleccionar todas las variables que comienzan con la palabra “sch_friend”, lo que significa que sch_friend11, sch_friend12, ... serán seleccionadas.

    dat |> 
      select(id, school, starts_with("sch_friend"))
    # A tibble: 2,164 × 78
            id school sch_friend11 sch_friend12 sch_friend13 sch_friend14
         <dbl>  <int>        <int>        <int>        <int>        <int>
     1 1110001    111           NA           NA           NA           NA
     2 1110002    111          424          423          426          289
     3 1110007    111          629          505           NA           NA
     4 1110013    111          232          569           NA           NA
     5 1110014    111          582          134           41          592
     6 1110015    111           26          488           81          138
     7 1110020    111          528           NA          492          395
     8 1110022    111           NA           NA           NA           NA
     9 1110025    111          135          185          553           84
    10 1110027    111          346          168          559            5
    # ℹ 2,154 more rows
    # ℹ 72 more variables: sch_friend15 <int>, sch_friend16 <int>,
    #   sch_friend17 <int>, sch_friend18 <int>, sch_friend19 <int>,
    #   sch_friend110 <int>, sch_friend111 <int>, sch_friend112 <int>,
    #   sch_friend113 <int>, sch_friend114 <int>, sch_friend115 <int>,
    #   sch_friend116 <int>, sch_friend117 <int>, sch_friend118 <int>,
    #   sch_friend119 <int>, sch_friend21 <int>, sch_friend22 <int>, …
  2. Luego, lo remodelamos a formato largo: Transponiendo todos los sch_friend* a formato largo. Hacemos esto usando la función gather (del paquete tidyr); una alternativa a la función reshape, que encuentro más fácil de usar. Veamos cómo funciona:

    dat |> 
      select(id, school, starts_with("sch_friend")) |>
      gather(key = "varname", value = "content", -id, -school)
    # A tibble: 164,464 × 4
            id school varname      content
         <dbl>  <int> <chr>          <int>
     1 1110001    111 sch_friend11      NA
     2 1110002    111 sch_friend11     424
     3 1110007    111 sch_friend11     629
     4 1110013    111 sch_friend11     232
     5 1110014    111 sch_friend11     582
     6 1110015    111 sch_friend11      26
     7 1110020    111 sch_friend11     528
     8 1110022    111 sch_friend11      NA
     9 1110025    111 sch_friend11     135
    10 1110027    111 sch_friend11     346
    # ℹ 164,454 more rows

    En este caso, el parámetro key establece el nombre de la variable que contendrá el nombre de la variable que fue remodelada, mientras que value es el nombre de la variable que contendrá el contenido de los datos (por eso los nombré así). El bit -id, -school le dice a la función que “elimine” esas variables antes de remodelar. En otras palabras, “remodela todo excepto id y school.

    También, nota que pasamos de 2164 filas a 19 (nominaciones) * 2164 (sujetos) * 4 (ondas) = 164464 filas, como se esperaba.

  3. Como los datos de nominación pueden estar vacíos para algunas celdas, necesitamos cuidar esos casos, los NAs, así que filtramos los datos:

    dat |> 
      select(id, school, starts_with("sch_friend")) |>
      gather(key = "varname", value = "content", -id, -school) |>
      filter(!is.na(content))
    # A tibble: 39,561 × 4
            id school varname      content
         <dbl>  <int> <chr>          <int>
     1 1110002    111 sch_friend11     424
     2 1110007    111 sch_friend11     629
     3 1110013    111 sch_friend11     232
     4 1110014    111 sch_friend11     582
     5 1110015    111 sch_friend11      26
     6 1110020    111 sch_friend11     528
     7 1110025    111 sch_friend11     135
     8 1110027    111 sch_friend11     346
     9 1110029    111 sch_friend11     369
    10 1110030    111 sch_friend11     462
    # ℹ 39,551 more rows
  4. Y finalmente, creamos tres nuevas variables de este conjunto de datos: friendid,, year, y nom_num (número de nominación). Todo usando expresiones regulares:

    dat |> 
      select(id, school, starts_with("sch_friend")) |>
      gather(key = "varname", value = "content", -id, -school) |>
      filter(!is.na(content)) |>
      mutate(
        friendid = school*10000 + content,
        year     = as.integer(str_extract(varname, "(?<=[a-z])[0-9]")),
        nnom     = as.integer(str_extract(varname, "(?<=[a-z][0-9])[0-9]+"))
        )
    # A tibble: 39,561 × 7
            id school varname      content friendid  year  nnom
         <dbl>  <int> <chr>          <int>    <dbl> <int> <int>
     1 1110002    111 sch_friend11     424  1110424     1     1
     2 1110007    111 sch_friend11     629  1110629     1     1
     3 1110013    111 sch_friend11     232  1110232     1     1
     4 1110014    111 sch_friend11     582  1110582     1     1
     5 1110015    111 sch_friend11      26  1110026     1     1
     6 1110020    111 sch_friend11     528  1110528     1     1
     7 1110025    111 sch_friend11     135  1110135     1     1
     8 1110027    111 sch_friend11     346  1110346     1     1
     9 1110029    111 sch_friend11     369  1110369     1     1
    10 1110030    111 sch_friend11     462  1110462     1     1
    # ℹ 39,551 more rows

    La expresión regular (?<=[a-z]) coincide con una cadena precedida por cualquier letra de a a z. En contraste, la expresión [0-9] coincide con un solo número. Por lo tanto, de la cadena "sch_friend12", la expresión regular solo coincidirá con el 1, ya que es el único número seguido por una letra. La expresión (?<=[a-z][0-9]) coincide con una cadena precedida por una letra minúscula y un número de un dígito. Finalmente, la expresión [0-9]+ coincide con una cadena de números–así que podría ser más de uno. Por lo tanto, de la cadena "sch_friend12", obtendremos 2:

    str_extract("sch_friend12", "(?<=[a-z])[0-9]")
    [1] "1"
    str_extract("sch_friend12", "(?<=[a-z][0-9])[0-9]+")
    [1] "2"

Y finalmente, la función as.integer coerciona el valor de retorno de la función str_extract de character a integer. Ahora que tenemos esta lista de enlaces, podemos crear un objeto igraph

Red igraph

Para coercionar la lista de enlaces en un objeto igraph, usaremos la función graph_from_data_frame en igraph (Csárdi et al. 2024). Esta función recibe los siguientes argumentos: un marco de datos donde las dos primeras columnas son “source” (ego) y “target” (alter), un indicador de si la red es dirigida o no, y un marco de datos opcional con vértices, en cuya primera columna debería contener los ids de vértice.

Usar el argumento opcional vertices es una buena práctica: Le dice a la función qué ids debería esperar. Usando el conjunto de datos original, crearemos un marco de datos con vértices de nombre:

vertex_attrs <- dat |> 
  select(id, school, hispanic, female1, starts_with("eversmk"))

Ahora, usemos la función graph_from_data_frame para crear un objeto igraph:

library(igraph)

ig_year1 <- net |>
  filter(year == "1") |> 
  select(id, friendid, nnom) |>
  graph_from_data_frame(
    vertices = vertex_attrs
  )
Error in graph_from_data_frame(select(filter(net, year == "1"), id, friendid, : Some vertex names in edge list are not listed in vertex data frame

¡Ups! Parece que los individuos están nominando a otros estudiantes no incluidos en la encuesta. ¿Cómo resolver eso? Bueno, ¡todo depende de lo que necesites hacer! En este caso, iremos por la estrategia de elimínalos-silenciosamente-y-no-digas-nada:

library(igraph)

ig_year1 <- net |>
  filter(year == "1") |>
  
  # Línea extra, todas las nominaciones deben estar en ego también.
  filter(friendid %in% id) |> 
  
  select(id, friendid, nnom) |>
  graph_from_data_frame(
    vertices = vertex_attrs
    )

ig_year1
IGRAPH 29b5fab DN-- 2164 9514 -- 
+ attr: name (v/c), school (v/n), hispanic (v/n), female1 (v/n),
| eversmk1 (v/n), eversmk2 (v/n), eversmk3 (v/n), eversmk4 (v/n), nnom
| (e/n)
+ edges from 29b5fab (vertex names):
 [1] 1110007->1110629 1110013->1110232 1110014->1110582 1110015->1110026
 [5] 1110025->1110135 1110027->1110346 1110029->1110369 1110035->1110034
 [9] 1110040->1110390 1110041->1110557 1110044->1110027 1110046->1110030
[13] 1110050->1110086 1110057->1110263 1110069->1110544 1110071->1110167
[17] 1110072->1110289 1110073->1110014 1110075->1110352 1110084->1110305
[21] 1110086->1110206 1110093->1110040 1110094->1110483 1110095->1110043
+ ... omitted several edges

Así que tenemos nuestra red con 2164 nodos y 9514 enlaces. Los siguientes pasos: obtener algunas estadísticas descriptivas y visualizar nuestra red.

Estadísticas descriptivas de red

Aunque podríamos hacer todas las redes a la vez, en esta parte, nos enfocaremos en calcular algunas estadísticas de red para una sola escuela. Comenzamos por la escuela 111. La primera pregunta que deberías estar haciéndote ahora es, “¿cómo puedo obtener esa información del objeto igraph?.” Los atributos de vértices y enlaces se pueden acceder a través de las funciones V y E, respectivamente; además, podemos listar qué atributos de vértice/enlace están disponibles:

vertex_attr_names(ig_year1)
[1] "name"     "school"   "hispanic" "female1"  "eversmk1" "eversmk2" "eversmk3"
[8] "eversmk4"
edge_attr_names(ig_year1) 
[1] "nnom"

Tal como haríamos con marcos de datos, acceder a atributos de vértice se hace a través del operador signo de dólar $. Junto con la función V; por ejemplo, acceder a los primeros diez elementos de la variable hispanic se puede hacer de la siguiente manera:

V(ig_year1)$hispanic[1:10]
 [1] 1 1 0 1 1 1 1 1 0 1

Ahora que sabes cómo acceder a atributos de vértice, podemos obtener la red correspondiente a la escuela 111 identificando qué vértices son parte de ella y pasar esa información a la función induced_subgraph:

# ¿Qué ids son de la escuela 111?
school111ids <- which(V(ig_year1)$school == 111)

# Creando un subgrafo
ig_year1_111 <- induced_subgraph(
  graph = ig_year1,
  vids  = school111ids
)

La función which en R devuelve un vector de índices indicando qué elementos pasan la prueba, devolviendo verdadero y falso, de lo contrario. En nuestro caso, resultará en un vector de índices de los vértices que tienen el atributo school igual a 111. Con el subgrafo, podemos calcular diferentes medidas de centralidad3 para cada vértice y almacenarlas en el objeto igraph mismo:

# Calculando medidas de centralidad para cada vértice
V(ig_year1_111)$indegree   <- degree(ig_year1_111, mode = "in")
V(ig_year1_111)$outdegree  <- degree(ig_year1_111, mode = "out")
V(ig_year1_111)$closeness  <- closeness(ig_year1_111, mode = "total")
V(ig_year1_111)$betweeness <- betweenness(ig_year1_111, normalized = TRUE)

Desde aquí, podemos volver a nuestros viejos hábitos y obtener el conjunto de atributos de vértice como un marco de datos para que podamos calcular algunas estadísticas de resumen sobre las medidas de centralidad que acabamos de obtener

# Extrayendo cada característica de vértice como un data.frame
stats <- as_data_frame(ig_year1_111, what = "vertices")

# Calculando cuantiles para cada variable
stats_degree <- with(stats, {
 cbind(
   indegree   = quantile(indegree, c(.025, .5, .975), na.rm = TRUE),
   outdegree  = quantile(outdegree, c(.025, .5, .975), na.rm = TRUE),
   closeness  = quantile(closeness, c(.025, .5, .975), na.rm = TRUE),
   betweeness = quantile(betweeness, c(.025, .5, .975), na.rm = TRUE)
 )
})

stats_degree
      indegree outdegree    closeness  betweeness
2.5%         0         0 0.0005915148 0.000000000
50%          4         4 0.0007487833 0.001879006
97.5%       16        16 0.0008838413 0.016591048

La función with es algo similar a lo que dplyr nos permite hacer cuando queremos trabajar con el conjunto de datos pero sin mencionar su nombre cada vez que pedimos una variable. Sin usar la función with, lo anterior podría haberse hecho de la siguiente manera:

stats_degree <- 
 cbind(
   indegree   = quantile(stats$indegree, c(.025, .5, .975), na.rm = TRUE),
   outdegree  = quantile(stats$outdegree, c(.025, .5, .975), na.rm = TRUE),
   closeness  = quantile(stats$closeness, c(.025, .5, .975), na.rm = TRUE),
   betweeness = quantile(stats$betweeness, c(.025, .5, .975), na.rm = TRUE)
 )

A continuación, calcularemos algunas estadísticas a nivel de grafo:

cbind(
  size    = vcount(ig_year1_111),
  nedges  = ecount(ig_year1_111),
  density = edge_density(ig_year1_111),
  recip   = reciprocity(ig_year1_111),
  centr   = centr_betw(ig_year1_111)$centralization,
  pathLen = mean_distance(ig_year1_111)
  )
     size nedges     density     recip      centr pathLen
[1,]  533   2638 0.009303277 0.3731513 0.02179154 4.23678

Censo triádico

triadic <- triad_census(ig_year1_111)
triadic
 [1] 24059676   724389   290849     3619     3383     4401     3219     2997
 [9]      407       33      836      235      163      137      277       85

Para obtener una vista más agradable de esto, podemos usar una tabla que recuperé de ?triad_census. Además, podemos normalizar el objeto triadic por su suma en lugar de mirar conteos en bruto. De esa manera, obtenemos proporciones en su lugar4

knitr::kable(cbind(
  Pcent = triadic/sum(triadic)*100,
  read.csv("triadic_census.csv")
  ), digits = 2)
Pcent code description
95.88 003 A,B,C, the empty graph.
2.89 012 A->B, C, the graph with a single directed edge.
1.16 102 A<->B, C, the graph with a mutual connection between two vertices.
0.01 021D A<-B->C, the out-star.
0.01 021U A->B<-C, the in-star.
0.02 021C A->B->C, directed line.
0.01 111D A<->B<-C.
0.01 111U A<->B->C.
0.00 030T A->B<-C, A->C.
0.00 030C A<-B<-C, A->C.
0.00 201 A<->B<->C.
0.00 120D A<-B->C, A<->C.
0.00 120U A->B<-C, A<->C.
0.00 120C A->B->C, A<->C.
0.00 210 A->B<->C, A<->C.
0.00 300 A<->B<->C, A<->C, the complete graph.

Graficando la red en igraph

Gráfico único

Echemos un vistazo a cómo se ve nuestra red cuando usamos los parámetros predeterminados en el método plot del objeto igraph:

plot(ig_year1)

Un gráfico de red no muy agradable. Esto es lo que obtenemos con los parámetros predeterminados en igraph.

No muy agradable, ¿verdad? Un par de cosas con este gráfico:

  1. Estamos viendo todas las escuelas simultáneamente, lo que no tiene sentido. Así que, en lugar de graficar ig_year1, nos enfocaremos en ig_year1_111.

  2. Todos los vértices tienen el mismo tamaño y se están solapando. En lugar de usar el tamaño predeterminado, dimensionaremos los vértices por indegree usando la función degree y pasando el vector de grados a vertex.size.5

  3. Dado el número de vértices en estas redes, las etiquetas no son útiles aquí. Así que las eliminaremos estableciendo vertex.label = NA. Además, reduciremos el tamaño de la punta de las flechas estableciendo edge.arrow.size = 0.25.

  4. Y finalmente, estableceremos el color de cada vértice para que sea una función de si el individuo es hispano o no. Para esta última parte necesitamos ir un poco más de programación:

col_hispanic <- V(ig_year1_111)$hispanic + 1
col_hispanic <- coalesce(col_hispanic, 3) 
col_hispanic <- c("steelblue", "tomato", "white")[col_hispanic]

Línea por línea, hicimos lo siguiente:

  1. La primera línea agregó uno a todos los valores no NA para que los 0s (no hispanos) se convirtieran en 1s y los 1s (hispanos) se convirtieran en 2s.

  2. La segunda línea reemplazó todos los NAs con el número tres para que nuestro vector col_hispanic ahora vaya de uno a tres sin NAs en él.

  3. En la última línea, creamos un vector de colores. Esencialmente, lo que estamos haciendo aquí es decirle a R que cree un vector de longitud length(col_hispanic) seleccionando elementos por índice del vector c("steelblue", "tomato", "white"). De esta manera, si, por ejemplo, el primer elemento del vector col_hispanic fuera un 3, nuestro nuevo vector de colores tendría un "white" en él.

Para asegurarnos de que sabemos que estamos en lo correcto, imprimamos los primeros 10 elementos de nuestro nuevo vector de colores junto con la columna original hispanic:

cbind(
  original = V(ig_year1_111)$hispanic[1:10],
  colors   = col_hispanic[1:10]
  )
      original colors     
 [1,] "1"      "tomato"   
 [2,] "1"      "tomato"   
 [3,] "0"      "steelblue"
 [4,] "1"      "tomato"   
 [5,] "1"      "tomato"   
 [6,] "1"      "tomato"   
 [7,] "1"      "tomato"   
 [8,] "1"      "tomato"   
 [9,] "0"      "steelblue"
[10,] "1"      "tomato"   

Con nuestro agradable vector de colores, ahora podemos pasarlo a plot.igraph (que llamamos implícitamente simplemente llamando plot), a través del argumento vertex.color:

# Gráfico elegante
set.seed(1)
plot(
  ig_year1_111,
  vertex.size     = degree(ig_year1_111)/10 +1,
  vertex.label    = NA,
  edge.arrow.size = .25,
  vertex.color    = col_hispanic
  )

Red de amigos en tiempo 1 para la escuela 111.

¡Agradable! Así que se ve mejor. El único problema es que tenemos muchos aislados. Intentemos de nuevo dibujando el mismo gráfico sin aislados. Para hacer eso, necesitamos filtrar el grafo, para lo cual usaremos la función induced_subgraph

# ¿Qué vértices no son aislados?
which_ids <- which(degree(ig_year1_111, mode = "total") > 0)

# Obteniendo el subgrafo
ig_year1_111_sub <- induced_subgraph(ig_year1_111, which_ids)

# Necesitamos obtener el mismo subconjunto en col_hispanic
col_hispanic <- col_hispanic[which_ids]
# Gráfico elegante
set.seed(1)
plot(
  ig_year1_111_sub,
  vertex.size     = degree(ig_year1_111_sub)/5 +1,
  vertex.label    = NA,
  edge.arrow.size = .25,
  vertex.color    = col_hispanic
  )

Red de amigos en tiempo 1 para la escuela 111. El grafo excluye aislados.

¡Ahora eso está mejor! Un patrón interesante que surge es que los individuos parecen agruparse por si son hispanos o no.

Podemos escribir esto como una función para evitar copiar y pegar el código n veces (suponiendo que queremos crear un gráfico similar a este n veces). Hacemos esto último en la siguiente subsección.

Múltiples gráficos

Cuando te estás repitiendo repetidamente, es una buena idea escribir una secuencia de comandos como una función. En este caso, dado que ejecutaremos el mismo tipo de gráfico para todas las escuelas/ondas, escribimos una función en la que las únicas cosas que cambian son: (a) el id de la escuela, y (b) el color de los nodos.

myplot <- function(
  net,
  schoolid,
  mindgr = 1,
  vcol   = "tomato",
  ...) {
  
  # Creando un subgrafo
  subnet <- induced_subgraph(
    net,
    which(degree(net, mode = "all") >= mindgr & V(net)$school == schoolid)
  )
  
  # Gráfico elegante
  set.seed(1)
  plot(
    subnet,
    vertex.size     = degree(subnet)/5,
    vertex.label    = NA,
    edge.arrow.size = .25,
    vertex.color    = vcol,
    ...
    )
}

La definición de la función:

  1. El myplot <- function([argumentos]) {[cuerpo de la función]} le dice a R que vamos a crear una función llamada myplot.

  2. Declaramos cuatro argumentos específicos: net, schoolid, mindgr, y vcol. Estos son un objeto igraph, el id de la escuela, el grado mínimo que los vértices deben tener para ser incluidos en la figura, y el color de los vértices. Observa que, comparado con otros lenguajes de programación, R no requiere declarar los tipos de datos.

  3. El objeto de puntos suspensivos, ..., es un objeto especial en R que nos permite pasar otros argumentos sin especificar cuáles. Si echas un vistazo al bit plot en el cuerpo de la función, verás que también agregamos .... Usamos los puntos suspensivos para pasar argumentos extra (diferentes de los que definimos explícitamente) directamente a plot. En la práctica, esto implica que podemos, por ejemplo, establecer el argumento edge.arrow.size al llamar myplot, ¡incluso aunque no lo incluimos en la definición de la función! (Ver ?dotsMethods en R para más detalles).

En las siguientes líneas de código, usando nuestra nueva función, graficaremos la red de cada escuela en el mismo dispositivo de graficado (ventana) con la ayuda de la función par, y agregaremos leyenda con el legend:

# Graficando todos juntos
oldpar <- par(no.readonly = TRUE)
par(mfrow = c(2, 3), mai = rep(0, 4), oma= c(1, 0, 0, 0))
myplot(ig_year1, 111, vcol = "tomato")
myplot(ig_year1, 112, vcol = "steelblue")
myplot(ig_year1, 113, vcol = "black")
myplot(ig_year1, 114, vcol = "gold")
myplot(ig_year1, 115, vcol = "white")
par(oldpar)

# Una leyenda elegante
legend(
  "bottomright",
  legend = c(111, 112, 113, 114, 115),
  pt.bg  = c("tomato", "steelblue", "black", "gold", "white"),
  pch    = 21,
  cex    = 1,
  bty    = "n",
  title  = "Escuela"
  )

Las 5 escuelas en tiempo 1. Nuevamente, los grafos excluyen aislados.

Entonces, ¿qué pasó aquí?

  • oldpar <- par(no.readonly = TRUE) Esta línea almacena los parámetros actuales para graficar. Dado que vamos a estar cambiándolos, ¡más vale asegurarnos de que podemos volver!.

  • par(mfrow = c(2, 3), mai = rep(0, 4), oma=rep(0, 4)) Aquí estamos estableciendo varias cosas al mismo tiempo. mfrow especifica cuántas figuras se dibujarán, y en qué orden. En particular, estamos pidiendo al dispositivo de graficado que haga espacio para 2*3 = 6 figuras organizadas en dos filas y tres columnas dibujadas por fila.

    mai especifica el tamaño de los márgenes en pulgadas, establecer todos los márgenes iguales a cero (que es lo que estamos haciendo ahora) da más espacio al gráfico. Lo mismo es cierto para oma. Ver ?par para más información.

  • myplot(ig_year1, ...) Esto es simplemente llamar nuestra función de graficado. La parte elegante de esto es que, dado que establecimos mfrow = c(2, 3), R se encarga de distribuir los gráficos en el dispositivo.

  • par(oldpar) Esta línea nos permite restaurar los parámetros de graficado.

Pruebas estadísticas

¿Está correlacionado el número de nominación con el indegree?

Hipótesis: Los individuos que, en promedio, están entre las primeras nominaciones de sus pares son más populares

# Obteniendo todos los datos en formato largo
edgelist <- as_long_data_frame(ig_year1) |>
  as_tibble()

# Calculando indegree (de nuevo) y número promedio de nominación
# Incluir "En una escala del uno al cinco qué tan cerca te sientes"
# También para amigos egocéntricos (A. Amigos)
indeg_nom_cor <- group_by(edgelist, to, to_name, to_school) |>
  summarise(
    indeg   = length(nnom),
    nom_avg = 1/mean(nnom)
  ) |>
  rename(
    school = to_school
  )
`summarise()` has grouped output by 'to', 'to_name'. You can override using the
`.groups` argument.
indeg_nom_cor
# A tibble: 1,561 × 5
# Groups:   to, to_name [1,561]
      to to_name school indeg nom_avg
   <dbl> <chr>    <int> <int>   <dbl>
 1     2 1110002    111    22   0.222
 2     3 1110007    111     7   0.175
 3     4 1110013    111     6   0.171
 4     5 1110014    111    19   0.134
 5     6 1110015    111     3   0.15 
 6     7 1110020    111     6   0.154
 7     9 1110025    111     6   0.214
 8    10 1110027    111    13   0.220
 9    11 1110029    111    14   0.131
10    12 1110030    111     6   0.222
# ℹ 1,551 more rows
# Usando correlación de Pearson
with(indeg_nom_cor, cor.test(indeg, nom_avg))

    Pearson's product-moment correlation

data:  indeg and nom_avg
t = -12.254, df = 1559, p-value < 2.2e-16
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
 -0.3409964 -0.2504653
sample estimates:
       cor 
-0.2963965 
save.image("03.rda")

  1. Introducido en R versión 4.1.0, el operador pipe de R base |> funciona de manera similar al pipe de magrittr %>%. Las diferencias clave entre estos dos se explican en https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/.↩︎

  2. Por favor, consulta el archivo de ayuda ?'regular expression' en R. El paquete de R rex (Ushey, Hester, and Krzyzanowski 2021) es un compañero amigable para escribir expresiones regulares. También hay un complemento de RStudio ordenado (pero experimental) que puede ser muy útil para entender cómo funcionan las expresiones regulares, el complemento regexplain.↩︎

  3. Para más información sobre las diferentes medidas de centralidad, por favor echa un vistazo al artículo “Centrality” en Wikipedia.↩︎

  4. Durante nuestro taller, la Prof. De la Haye sugirió usar {n \choose 3} como una constante normalizadora. ¡Resulta que sum(triadic) = choose(n, 3)! Así que cualquier enfoque es correcto.↩︎

  5. Descubrir cuál es el tamaño de vértice óptimo es un poco complicado. Sin ponerse demasiado técnico, no hay otra forma de obtener un tamaño de vértice agradable que no sea simplemente jugar con diferentes valores de él. Una solución agradable a esto es usar netdiffuseR::igraph_vertex_rescale que reescala los vértices para que estos mantengan su relación de aspecto a una proporción predefinida de la pantalla.↩︎