Lab note: This workbook follows Jimmy Xu’s Six Degrees of Connection tutorial as the course starting point, with Section 1 replaced by my own family / Olas / Jessie network (original work for Week 9 Lab).
Every “like,” friend request, and tagged photo on a platform like Facebook leaves behind a trace of who is connected to whom. That trace is a network — and networks are one of the richest sources of customer insight a marketing team can get their hands on.
igraph in R lets us build a graph from
vertices and edges, count triangles, compute PageRank, and expand
friend-of-a-friend circles on a laptop, with syntax close to
dplyr.
Starting-point tutorial: Social network analysis using R and the Six Degrees walkthrough on RPubs / course materials.
library(tidyverse)
library(igraph)
library(kableExtra)
library(scales)
library(networkD3)
The Facebook Circles dataset (SNAP) is a real anonymized snapshot of Facebook friendships (~4,039 users, ~88,235 edges). In class we simulate a smaller version with the same shape and attributes.
set.seed(580)
n_users <- 150
vertices <- tibble(
id = 1:n_users,
birthday = sample(seq(as.Date("1985-01-01"), as.Date("2005-12-31"), by = "day"),
n_users, replace = TRUE),
hometown = sample(paste("City", LETTERS[1:8]), n_users, replace = TRUE),
employer_id = sample(1:12, n_users, replace = TRUE),
school_id = sample(1:10, n_users, replace = TRUE)
)
vertices %>%
slice_head(n = 5) %>%
kable(caption = "First 5 rows of the simulated user (vertex) table") %>%
kable_styling(latex_options = "striped")
| id | birthday | hometown | employer_id | school_id |
|---|---|---|---|---|
| 1 | 1989-08-29 | City B | 4 | 5 |
| 2 | 1991-01-04 | City B | 12 | 5 |
| 3 | 1998-04-19 | City A | 1 | 4 |
| 4 | 1986-02-27 | City H | 10 | 9 |
| 5 | 1988-04-09 | City H | 5 | 2 |
# Preferential attachment: a few hub users get many friends
g_sim <- sample_pa(n_users, power = 1.1, m = 3, directed = FALSE)
edges <- as_data_frame(g_sim, what = "edges") %>%
rename(src = from, dst = to) %>%
as_tibble()
edges %>%
slice_head(n = 5) %>%
kable(caption = "First 5 rows of the simulated friendship (edge) table") %>%
kable_styling(latex_options = "striped")
| src | dst |
|---|---|
| 1 | 2 |
| 1 | 3 |
| 2 | 3 |
| 1 | 4 |
| 2 | 4 |
fb_graph <- graph_from_data_frame(
d = edges,
vertices = vertices,
directed = FALSE
)
fb_graph
## IGRAPH 3a58e24 UN-- 150 444 --
## + attr: name (v/c), birthday (v/n), hometown (v/c), employer_id (v/n),
## | school_id (v/n)
## + edges from 3a58e24 (vertex names):
## [1] 1 --2 1 --3 2 --3 1 --4 2 --4 3 --4 3 --5 2 --5 4 --5 5 --6
## [11] 2 --6 3 --6 1 --7 2 --7 6 --7 2 --8 1 --8 5 --8 3 --9 5 --9
## [21] 2 --9 4 --10 3 --10 7 --10 6 --11 2 --11 5 --11 3 --12 9 --12 6 --12
## [31] 12--13 2 --13 1 --13 4 --14 13--14 12--14 4 --15 2 --15 8 --15 5 --16
## [41] 2 --16 12--16 2 --17 4 --17 13--17 12--18 1 --18 8 --18 15--19 5 --19
## [51] 18--19 4 --20 12--20 3 --20 5 --21 17--21 2 --21 3 --22 14--22 5 --22
## [61] 15--23 2 --23 16--23 16--24 13--24 8 --24 8 --25 11--25 16--25 4 --26
## + ... omitted several edges
as_data_frame(fb_graph, what = "edges") %>%
mutate(from = as.integer(from), to = as.integer(to)) %>%
left_join(vertices, by = c("from" = "id")) %>%
rename_with(~ paste0("a_", .), .cols = -c(from, to)) %>%
left_join(vertices, by = c("to" = "id")) %>%
rename_with(~ paste0("b_", .), .cols = c(birthday, hometown, employer_id, school_id)) %>%
slice_head(n = 3) %>%
kable(caption = "Sample 'triplets': an edge plus both users' attributes") %>%
kable_styling(latex_options = "striped", font_size = 9)
| from | to | a_birthday | a_hometown | a_employer_id | a_school_id | b_birthday | b_hometown | b_employer_id | b_school_id |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 2 | 1989-08-29 | City B | 4 | 5 | 1991-01-04 | City B | 12 | 5 |
| 1 | 3 | 1989-08-29 | City B | 4 | 5 | 1998-04-19 | City A | 1 | 4 |
| 2 | 3 | 1991-01-04 | City B | 12 | 5 | 1998-04-19 | City A | 1 | 4 |
same_birthday <- as_data_frame(fb_graph, what = "edges") %>%
mutate(from = as.integer(from), to = as.integer(to)) %>%
left_join(vertices, by = c("from" = "id")) %>%
left_join(vertices, by = c("to" = "id"), suffix = c("_a", "_b")) %>%
filter(birthday_a == birthday_b) %>%
select(a = from, b = to, shared_birthday = birthday_a)
same_birthday %>%
slice_head(n = 5) %>%
kable(caption = "Friend pairs who share a birthday") %>%
kable_styling(latex_options = "striped")
| a | b | shared_birthday |
|---|---|---|
| character(0) | c(“–:”, “–:”, “:—————”) | character(0) |
Marketing takeaway: flag friend pairs who share a birthday and offer a “celebrate together” discount — one friendship, two redemptions.
triangle_counts <- tibble(
id = as.integer(V(fb_graph)$name),
triangle_count = count_triangles(fb_graph)
)
triangle_counts %>%
arrange(desc(triangle_count)) %>%
slice_head(n = 5) %>%
kable(caption = "Users embedded in the most friendship triangles") %>%
kable_styling(latex_options = "striped")
| id | triangle_count |
|---|---|
| 2 | 36 |
| 1 | 23 |
| 3 | 23 |
| 5 | 19 |
| 4 | 16 |
Marketing takeaway: high triangle-count users are good for “bring your whole friend group” campaigns.
pr <- page_rank(fb_graph, damping = 0.85)$vector
influencers <- tibble(
id = as.integer(names(pr)),
pagerank = pr
) %>%
arrange(desc(pagerank))
influencers %>%
slice_head(n = 5) %>%
kable(caption = "Top 5 most influential users by PageRank", digits = 4) %>%
kable_styling(latex_options = "striped")
| id | pagerank |
|---|---|
| 1 | 0.0304 |
| 12 | 0.0281 |
| 2 | 0.0264 |
| 4 | 0.0250 |
| 5 | 0.0217 |
Marketing takeaway: PageRank shortlists influencers whose friends are themselves well connected — reach that compounds.
Friendships are undirected; hyperlinks are directed. Below is a real partial crawl of csub.edu pages (Home, Admissions, BPA).
csub_links <- tribble(
~from, ~to, ~url_to,
"Home", "About CSUB", "csub.edu/about/index.shtml",
"Home", "Academics/Degrees", "csub.edu/academics/degrees.shtml",
"Home", "Admissions", "csub.edu/admissions/index.shtml",
"Home", "Financial Aid", "csub.edu/financial-aid/index.shtml",
"Home", "Registrar", "csub.edu/registrar/index.shtml",
"Home", "Library", "library.csub.edu",
"Home", "News", "news.csub.edu",
"Home", "Future Students", "csub.edu/future-students.shtml",
"Home", "BPA", "csub.edu/bpa/index.shtml",
"Admissions", "Home", "csub.edu/",
"Admissions", "About CSUB", "csub.edu/about/index.shtml",
"Admissions", "Academics/Degrees", "csub.edu/academics/degrees.shtml",
"Admissions", "Financial Aid", "csub.edu/financial-aid/index.shtml",
"Admissions", "Registrar", "csub.edu/registrar/index.shtml",
"Admissions", "Library", "library.csub.edu",
"Admissions", "News", "news.csub.edu",
"Admissions", "Future Students", "csub.edu/future-students.shtml",
"Admissions", "BPA", "csub.edu/bpa/index.shtml",
"BPA", "Home", "csub.edu/",
"BPA", "About CSUB", "csub.edu/about/index.shtml",
"BPA", "Academics/Degrees", "csub.edu/academics/degrees.shtml",
"BPA", "Admissions", "csub.edu/admissions/index.shtml",
"BPA", "Financial Aid", "csub.edu/financial-aid/index.shtml",
"BPA", "Registrar", "csub.edu/registrar/index.shtml",
"BPA", "Library", "library.csub.edu",
"BPA", "News", "news.csub.edu",
"BPA", "Future Students", "csub.edu/future-students.shtml",
"BPA", "BPA Academic Programs","csub.edu/bpa/academic-programs.shtml",
"BPA", "MBA Program", "csub.edu/bpa/master-business-administration-mba/index.shtml"
)
csub_graph <- graph_from_data_frame(
csub_links %>% select(from, to), directed = TRUE
)
csub_pr <- page_rank(csub_graph, damping = 0.85)$vector
csub_rank <- tibble(
page = names(csub_pr),
pagerank = csub_pr
) %>%
arrange(desc(pagerank))
csub_rank %>%
kable(caption = "csub.edu pages ranked by PageRank", digits = 4) %>%
kable_styling(latex_options = "striped")
| page | pagerank |
|---|---|
| About CSUB | 0.0876 |
| Academics/Degrees | 0.0876 |
| Financial Aid | 0.0876 |
| Registrar | 0.0876 |
| Library | 0.0876 |
| News | 0.0876 |
| Future Students | 0.0876 |
| BPA | 0.0814 |
| Admissions | 0.0801 |
| Home | 0.0801 |
| BPA Academic Programs | 0.0725 |
| MBA Program | 0.0725 |
V(csub_graph)$pagerank <- csub_pr[V(csub_graph)$name]
set.seed(580)
plot(
csub_graph,
layout = layout_with_fr(csub_graph),
vertex.size = 15 + scales::rescale(V(csub_graph)$pagerank, to = c(0, 35)),
vertex.color = scales::col_numeric(
c("#FFD166", "#FF5A36", "#7B61FF"),
domain = NULL
)(V(csub_graph)$pagerank),
vertex.label.color = "black",
vertex.label.cex = 0.75,
vertex.frame.color = "white",
edge.arrow.size = 0.4,
edge.color = "#4A90E2",
main = "csub.edu link graph (brighter colors; size = PageRank)"
)
Marketing takeaway: SEO / internal linking — hub pages with many quality inbound links accumulate authority; deep pages need intentional links from those hubs.
nodes_d3 <- tibble(
name = V(csub_graph)$name,
pagerank = V(csub_graph)$pagerank,
group = 1
)
links_d3 <- as_data_frame(csub_graph, what = "edges") %>%
mutate(
source = match(from, nodes_d3$name) - 1,
target = match(to, nodes_d3$name) - 1,
value = 1
)
forceNetwork(
Links = as.data.frame(links_d3),
Nodes = as.data.frame(nodes_d3),
Source = "source",
Target = "target",
Value = "value",
NodeID = "name",
Group = "group",
Nodesize = "pagerank",
radiusCalculation = JS("Math.sqrt(d.nodesize) * 40 + 6"),
linkDistance = 130,
opacity = 0.9,
zoom = TRUE,
arrows = TRUE,
fontSize = 14,
bounded = TRUE
)
Triangles, FoF, and PageRank turn a graph of relationships into a shortlist of people (or pages) worth acting on — who to personalize for, who to run group campaigns against, who to introduce, and who to sign as an influencer.
On my personal network, that shortlist starts with me as the bridge, Carlos as the Olas hub, and Jessie as the warm intro to another business circle (Revolution Surf Shop).
networkD3.