Why marketers should care about friend graphs

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)

Original work: my personal social network

For Week 9 Lab, I built a real social network from family, coworkers/friends at Olas, and a bridge friend (Jessie) who also knows my boss and owns Revolution Surf Shop (~13 people).

People in the network

Circle People
Family Jeffrey (me), Susan (mom), Dennis (dad), Danielle (sister), Danny (brother-in-law), Jackson & Owen (nephews)
Olas (work / friends) Carlos (boss), Zak, Cody, Megan, Cheyanne
Bridge Jessie (friend; knows Carlos; owns Revolution Surf Shop)
  • Vertices (nodes): the people listed above.
  • Edges: family ties, workplace friendships, and the Jeffrey–Jessie–Carlos bridge.
# Personal network: family + Olas + Jessie bridge
my_network <- graph_from_literal(
  # Family cluster
  Jeffrey-Susan-Dennis, Jeffrey-Dennis,
  Jeffrey-Danielle-Danny,
  Danielle-Jackson:Owen, Danny-Jackson:Owen, Jackson-Owen,
  Jeffrey-Jackson:Owen,
  # Olas workplace / friend cluster
  Jeffrey-Carlos,
  Jeffrey-Zak:Cody:Megan:Cheyanne,
  Carlos-Zak:Cody:Megan:Cheyanne,
  Zak-Cody, Cody-Megan, Megan-Cheyanne,
  # Bridge across work worlds
  Jeffrey-Jessie-Carlos
)

# Brighter colors by circle
family_nodes <- c("Susan", "Dennis", "Danielle", "Danny", "Jackson", "Owen")
olas_nodes   <- c("Carlos", "Zak", "Cody", "Megan", "Cheyanne")

V(my_network)$color <- dplyr::case_when(
  V(my_network)$name == "Jeffrey"      ~ "#FF5A36", # coral — me (hub)
  V(my_network)$name %in% family_nodes ~ "#00C2A8", # teal — family
  V(my_network)$name %in% olas_nodes   ~ "#FFD166", # gold — Olas
  V(my_network)$name == "Jessie"       ~ "#7B61FF", # violet — bridge
  TRUE                                 ~ "#AAAAAA"
)

set.seed(580)
plot(
  my_network,
  layout = layout_with_fr(my_network),
  vertex.size = 28,
  vertex.label.cex = 0.75,
  vertex.label.color = "black",
  vertex.color = V(my_network)$color,
  vertex.frame.color = "white",
  edge.color = "#4A90E2",
  edge.width = 2,
  main = "Jeffrey Deems — family (teal), Olas (gold), Jessie (violet)"
)

degree_tbl <- tibble(
  person = names(degree(my_network)),
  degree = as.integer(degree(my_network))
) %>%
  arrange(desc(degree))

degree_tbl %>%
  kable(caption = "Degree centrality in my personal network") %>%
  kable_styling(latex_options = "striped")
Degree centrality in my personal network
person degree
Jeffrey 11
Carlos 6
Danielle 4
Jackson 4
Owen 4
Cody 4
Megan 4
Danny 3
Zak 3
Cheyanne 3
Susan 2
Dennis 2
Jessie 2

What the graph shows

  • Jeffrey-Susan-Dennis is a chain (and Jeffrey-Dennis closes a parent triangle): how you’d trace introductions.
  • Jeffrey-Zak:Cody:Megan:Cheyanne uses : shorthand to fan out to several Olas friends at once.
  • Family triangles (Danielle–Danny–nephews) and Olas coworker ties create closed loops — tightly-knit clusters.
  • Jeffrey-Jessie-Carlos creates multiple paths between Jeffrey and Carlos (direct boss edge and through Jessie).
  • Jeffrey has the highest degree; Carlos is the strongest hub inside the Olas cluster.
In my_network, which vertex has the highest degree?



Brief interpretation of my findings

Two clear communities appear: a family cluster (Susan, Dennis, Danielle, Danny, Jackson, Owen) and an Olas work/friend cluster (Carlos, Zak, Cody, Megan, Cheyanne). I sit between them, so outreach that starts with me can reach both worlds. Carlos and Jessie matter for influence inside and across work: Carlos is well connected at Olas, and Jessie links me to Carlos while also representing Revolution Surf Shop — a natural warm-introduction path (friend-of-friend style) between coworker friends and another local business network. Triangle-dense family ties are the kind of “bring the whole group” clusters marketers target with shared offers; the Olas friendships are a workplace community ready for group campaigns.

What is the Facebook Circles dataset?

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.

  • Vertices (users): id, birthday, hometown, employer_id, school_id
  • Edges (friendships): src, dst

Step 1: Simulating the vertices (user) table

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")
First 5 rows of the simulated user (vertex) table
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

Step 2: Simulating the edges (friendship) table

# 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")
First 5 rows of the simulated friendship (edge) table
src dst
1 2
1 3
2 3
1 4
2 4
Which table stores the friendship relationships between users?


Step 3: Building the graph

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)
Sample ‘triplets’: an edge plus both users’ attributes
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

Step 4: Graph analysis — algorithms and marketing use cases

1. Finding users with the same birthday

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")
Friend pairs who share a birthday
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.

2. Counting friendship triangles

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")
Users embedded in the most friendship triangles
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.

A high triangle count for a user most likely indicates…


3. Friends of friends: expanding social circles

edge_list <- as_data_frame(fb_graph, what = "edges") %>%
  mutate(from = as.integer(from), to = as.integer(to)) %>%
  transmute(a = pmin(from, to), b = pmax(from, to))

friends_of_friends <- edge_list %>%
  rename(a = a, b_mid = b) %>%
  inner_join(edge_list, by = c("b_mid" = "a"), suffix = c("", "_2")) %>%
  rename(b = b) %>%
  filter(a != b) %>%
  anti_join(edge_list, by = c("a", "b")) %>%
  distinct(a, b) %>%
  left_join(vertices %>% select(id, school_id), by = c("a" = "id")) %>%
  left_join(vertices %>% select(id, school_id), by = c("b" = "id"), suffix = c("_a", "_b")) %>%
  filter(school_id_a == school_id_b)

friends_of_friends %>%
  slice_head(n = 5) %>%
  kable(caption = "Friend-of-friend pairs who also attended the same school") %>%
  kable_styling(latex_options = "striped")
Friend-of-friend pairs who also attended the same school
a b school_id_a school_id_b
1 87 5 5
2 87 5 5
1 55 5 5
1 63 5 5
1 93 5 5

Marketing takeaway: mutual friend + same school is a strong “People You May Know” / warm-intro signal (same idea as LinkedIn).

4. Identifying influencers using PageRank

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")
Top 5 most influential users by PageRank
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.

PageRank ranks a user highly mainly because of…


5. Bonus: PageRank on a website (csub.edu)

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")
csub.edu pages ranked by PageRank
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.

Why do pages like Library rank highly even though we never crawled them directly?


6. Making it interactive: D3.js with networkD3

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
)
What does wrapping the graph in networkD3 add?


Real-world applications for marketing teams

  1. Targeted marketing — birthday personalization; group offers for triangle-dense clusters.
  2. Event / networking — FoF + shared employer/school introductions.
  3. Influencer marketing — PageRank shortlists, not just follower count.
  4. Community detection — support existing clusters instead of forcing new ones.

Monetizing the insights

  1. Network-analytics consulting for brands
  2. FoF-style recommendation engines
  3. Event matchmaking platforms
  4. Targeting packages based on triangle density / PageRank

Conclusion

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).

Which technique is closest to LinkedIn’s ‘People You May Know’?



References

  • Leskovec, J., & Krevl, A. (2014). SNAP Datasets. Stanford Network Analysis Project.
  • Csardi, G., & Nepusz, T. (2006). The igraph software package for complex network research.
  • Page, L., Brin, S., Motwani, R., & Winograd, T. (1999). The PageRank Citation Ranking.
  • CSUB.edu pages (Home, Admissions, BPA), accessed July 23, 2026.
  • Xu, Z. “Jimmy” — Social network analysis / Six Degrees of Connection tutorials on RPubs.
  • Bostock et al. (2011). D3: Data-Driven Documents. Gandrud et al. networkD3.