Why marketers should care about friend graphs

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. Two customers who share a dozen mutual friends are far more likely to respond to the same campaign than two random strangers. A customer who sits at the center of many friendship triangles is probably someone other people listen to. A pair of customers who aren’t friends yet, but share three mutual connections, is a warm introduction waiting to happen.

The original version of this walkthrough used GraphFrames, a graph library that runs on top of Apache Spark. Spark is built for networks with billions of edges — genuinely “big data” scale. For a graduate marketing analytics class, though, we rarely need that much horsepower, and installing a Spark cluster is a poor use of a three-hour lab session. igraph in R gives us the same core ideas — building a graph from vertices and edges, counting triangles, computing PageRank, expanding friend-of-a-friend circles — on a single laptop, with syntax that’s much closer to the dplyr workflows you already know.

This tutorial rebuilds every analysis from the original article in R, translates the code line-by-line where it’s useful to see the mapping, and reframes each technique around a marketing decision rather than a computer-science exercise.

If you’d like a gentler on-ramp before diving into the Facebook Circles data, my earlier walkthrough, Social network analysis using R, covers the same igraph fundamentals — building a graph, basic centrality measures, and simple visualization — on a smaller, simpler example. It’s a good warm-up read if any of the igraph syntax below feels unfamiliar.

library(tidyverse)   # data wrangling
library(igraph)      # graph construction and algorithms
library(kableExtra)  # nicer tables for the report
library(checkdown)   # in-line knowledge checks
library(scales)       # color/size scaling for the network plot
library(networkD3)    # interactive D3.js force-directed graphs

First, the basics: a network you already know by heart

Before we simulate thousands of Facebook users, it helps to see the core vocabulary of network analysis on a network small enough to hold in your head — your own family.

A network (or graph) is just two things:

  • Vertices (also called nodes): the things being connected. Here, that’s people — Jimmy, his mother, father, wife, and a handful of friends or relatives labeled A, B, E, G, S.
  • Edges: the connections between vertices. Here, that’s a family or friendship tie.

igraph’s graph_from_literal() lets you sketch a small graph directly in R code using a simple text notation: a dash (-) draws an edge between two vertices, and a colon (:) lets one vertex connect to several others at once without repeating the dash.

g5 <- graph_from_literal(Jimmy-Mother-Farther-Wife, Jimmy-B:S, Jimmy-A, A-B, Jimmy-G-E, Jimmy-E)

plot(g5)

A few things worth noticing in this tiny graph:

  • Jimmy-Mother-Farther-Wife is a chain: Jimmy connects to Mother, who connects to Farther, who connects to Wife. Reading a chain like this is exactly how you’d trace a path of introductions in a customer network — “who introduced whom to whom.”
  • Jimmy-B:S is shorthand for two edges at once — Jimmy-B and Jimmy-S — the : operator fans one vertex out to several others in a single statement.
  • A-B closes a loop: Jimmy connects to both A and B (via Jimmy-A and Jimmy-B), and now A and B connect to each other too. That closed loop is a triangle — the same building block we’ll use later to spot tightly-knit friend clusters in the Facebook Circles data.
  • Jimmy-G-E and Jimmy-E together mean Jimmy reaches E two different ways: directly, and through G. Multiple paths between the same two people is a sign of a strong tie — exactly the kind of signal that makes friend-of-friend recommendations reliable rather than noisy.
  • Jimmy himself sits at the middle of every one of these connections — in network terms, he has the highest degree (number of direct connections) in this graph. In a customer network, a high-degree person is your most-connected customer: the one whose referral or share reaches the most other people directly.

Every concept in this section — vertices, edges, chains, triangles, multiple paths, and degree — reappears at Facebook-Circles scale later in this tutorial. The only thing that changes is size: 150 (or 4,039) users instead of 8 family members, and marketing questions instead of family trivia.

In g5, which vertex has the highest degree (the most direct connections)?



What is the Facebook Circles dataset?

The Facebook Circles dataset comes from the Stanford Network Analysis Project (SNAP). It’s a real anonymized snapshot of Facebook friendships: roughly 4,039 users (vertices) and about 88,235 friendship links (edges), along with attributes like each user’s birthday, hometown, employer, and school.

Because the real SNAP file lives on an external server we can’t reach from this classroom environment, we’ll simulate a smaller Facebook-Circles-style network with the same shape: users as vertices, friendships as edges, and the same four marketing-relevant attributes (birthday, hometown, employer, school). Everything you learn on the simulated version transfers directly — just swap in the real CSVs from SNAP if you want to scale up outside of class.

Dataset breakdown (what we’re recreating):

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

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

Each row is one user, just like the vertices DataFrame in the original PySpark version. Having birthday, employer_id, and school_id on hand is what lets us later ask marketing questions like “which users share an employer?” or “who has the same birth month?”

Step 2: Simulating the edges (friendship) table

# Barabasi-Albert graph: a small number of well-connected "hub" users,
# similar to the hub-and-spoke pattern real social networks show
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

sample_pa() builds a preferential-attachment graph — new users are more likely to befriend already-popular users, which is exactly the “a few people have tons of friends, most people have a handful” pattern real social networks show. That’s what will let triangle-counting and PageRank produce meaningful results later instead of a flat, uniform graph.

Which table stores the friendship relationships between users?


Step 3: Building the graph

In PySpark, a GraphFrame is built from a vertices DataFrame and an edges DataFrame. igraph works the same way — graph_from_data_frame() takes an edge list and an optional vertex table and stitches them into one graph object.

fb_graph <- graph_from_data_frame(
  d = edges,
  vertices = vertices,
  directed = FALSE
)

fb_graph
## IGRAPH daefdb6 UN-- 150 444 -- 
## + attr: name (v/c), birthday (v/n), hometown (v/c), employer_id (v/n),
## | school_id (v/n)
## + edges from daefdb6 (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
# The equivalent of GraphFrame's `graph.triplets.show()`:
# each edge, alongside the attributes of both endpoints
# (igraph stores vertex names as character, so we cast back to integer
# before joining against the vertices table)
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
  • graph_from_data_frame() combines the two tables, the same way GraphFrame(vertices, edges) does in PySpark.
  • Joining the edge list back to vertices twice (once per endpoint) reproduces GraphFrame’s triplets view, letting us see both users’ attributes side-by-side on every friendship.

Step 4: Graph analysis — algorithms and marketing use cases

1. Finding users with the same birthday

A shared birthday is a lightweight, low-cost personalization hook — useful for birthday-bundle promotions or “double celebration” event invitations.

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
NA NA NA
–: –: :—————
  • We join the edge list to the vertex table twice — once for each side of the friendship — the same role graph.find("(a)-[]->(b)") plays in GraphFrames.
  • The filter() step is a direct stand-in for the .filter("a.birthday = b.birthday") line in the original.

Marketing takeaway: a company running a loyalty program could automatically flag friend pairs who share a birthday and offer a “celebrate together” discount code redeemable by both users — a cheap way to turn an existing friendship into two redemptions instead of one.

2. Counting friendship triangles

A triangle is three users who are all mutually connected. A user who sits inside many triangles belongs to a tight-knit friend group — exactly the kind of person whose recommendation carries weight with the people around them.

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
  • count_triangles() is igraph‘s direct equivalent of GraphFrames’ triangleCount() — one number per user, counting how many closed friend-triangles they belong to.

Marketing takeaway: tight-knit groups are good candidates for community-building campaigns — think “bring your whole friend group” offers — because the group is already primed to act together.

A high triangle count for a user most likely indicates…


3. Friends of friends: expanding social circles

Friends-of-friends (FoF) finds pairs of users who share a mutual friend but aren’t directly connected yet — the same logic LinkedIn uses for “People You May Know.”

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
  • The chained inner_join() recreates GraphFrames’ motif syntax "(a)-[]->(b); (b)-[]->(c); !(a)-[]->(c)": walk from a to a mutual friend, then out to c, and drop any pair that’s already directly connected.
  • Filtering on school_id_a == school_id_b reproduces the original’s a.education_school_id = c.education_school_id condition.

Marketing takeaway: if two users share a mutual friend and an alma mater, that’s a stronger “suggest a connection” signal than either fact alone — useful for alumni-network apps or B2B platforms like LinkedIn suggesting a warm introduction instead of a cold one.

4. Identifying influencers using PageRank

PageRank scores each user by how well-connected their friends are, not just how many friends they have — someone with 20 friends who are themselves highly connected outranks someone with 50 friends who are all isolated.

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
  • page_rank()‘s damping argument plays the same role as GraphFrames’ resetProbability — it’s the “restart” probability that keeps the algorithm from getting stuck endlessly circling one dense cluster. (resetProbability = 0.15 in the original is the same idea as damping = 0.85, just expressed as the complement.)

Marketing takeaway: PageRank is a principled way to shortlist influencer-marketing candidates — people whose endorsement reaches other well-connected users, not just a large but disengaged audience.

PageRank ranks a user highly mainly because of…


5. Bonus: PageRank in its original habitat — ranking website popularity

Before Google’s founders ever thought about friend graphs, they used this exact algorithm to rank web pages. Think of every hyperlink as a “vote”: a page that gets linked to by many other well-linked pages ranks higher than a page with the same number of links from obscure sites. It’s the identical math we just used to rank influential Facebook users — only here the “friendship” is a hyperlink, and it only goes one direction.

That makes this a directed graph instead of an undirected one, which is a useful contrast for a marketing student: friendships are mutual, but a backlink isn’t — Site A can link to Site B without Site B linking back.

To make this concrete, here’s a real link graph, hand-captured from three actual pages on csub.edu (fetched directly on July 23, 2026): the CSUB homepage, the Office of Admissions page, and the College of Business and Public Administration (BPA) page. Every edge below is a hyperlink that genuinely appears on one of those three pages — nothing here is simulated.

# A real (partial) crawl of csub.edu: every edge is an actual hyperlink
# found on one of three fetched pages (Home, Admissions, BPA), captured
# 2026-07-23. Destination pages weren't themselves crawled, so their
# outbound links aren't represented -- this is a snapshot, not a full
# site crawl.
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 (real crawl, 3 source pages)",
        digits = 4) %>%
  kable_styling(latex_options = "striped")
csub.edu pages ranked by PageRank (real crawl, 3 source pages)
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
Home 0.0801
Admissions 0.0801
BPA Academic Programs 0.0725
MBA Program 0.0725

The table tells you the ranking, but the graph makes the reason for the ranking visible: which pages sit in the middle of the traffic pattern versus off to the side.

# Size and shade each node by its PageRank score so the plot visually
# reinforces the table above -- bigger, darker circles are pages that
# rank higher.
V(csub_graph)$pagerank <- csub_pr[V(csub_graph)$name]

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("Blues", 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 = "grey60",
  main = "csub.edu link graph (node size/shade = PageRank)"
)

Home, Admissions, and BPA sit near the center of the layout with arrows pointing both in and out of them — that mutual linking is what pulls them together visually and pushes their PageRank up. Library, Academics/Degrees, Financial Aid, Registrar, News, and Future Students sit further out with arrows only pointing in: they’re still shaded fairly dark, since three separate pages all vote for them, but they never send a vote back out. BPA Academic Programs and MBA Program sit at the outer edge, each with a single incoming arrow from BPA and nothing else — visually, the periphery of the network is exactly where the lowest PageRank scores end up.

  • directed = TRUE matters here more than ever — the CSUB homepage links out to Admissions, but that doesn’t obligate Admissions to link back (it does, in this case, purely because CSUB’s site template repeats the same global navigation menu on every page).
  • Home, Admissions, and BPA all rank near the top because they link to each other in addition to being linked to — a small, mutually-reinforcing cluster.
  • Library, Academics/Degrees, Financial Aid, Registrar, News, and Future Students all show up as inbound-heavy pages: every one of the three source pages links to them (because they sit in CSUB’s shared header/footer navigation), but we didn’t crawl those pages ourselves, so the graph shows no outbound edges from them. That’s a real limitation of a partial crawl, not a flaw in PageRank — a full site crawl would very likely rank Home even higher, since site-wide navigation templates are exactly the mechanism that concentrates PageRank on a handful of hub pages.
  • BPA Academic Programs and MBA Program sit lower — they’re only reachable from the single BPA page we crawled, with no other page (in this sample) pointing to them.

Marketing takeaway: this is the algorithmic backbone of SEO and digital PR — and the CSUB example makes the internal-linking version of that lesson concrete. A university’s homepage and its shared global navigation act like a bank of backlinks that every page automatically receives; a specific program page (like the MBA page) only accumulates that same authority when something outside the template — a college page, a news story, another department — chooses to link to it directly. For a real marketing or digital-strategy team, that’s the practical version of “earn better backlinks”: don’t just rely on the template nav, get other high-authority pages to link to the page you’re trying to promote.

In the CSUB example, why do pages like Library and Academics/Degrees rank highly even though we never crawled them directly?


6. Making it interactive: the same graph in D3.js

Everything so far has been a static image — good for a printed report, but not for exploring. D3.js (“Data-Driven Documents”) is the JavaScript library behind most of the interactive network graphics you’ve probably seen in The New York Times or The Pulitzer Center’s investigative pieces: nodes you can drag around, hover over, and zoom into, all running live in a web browser.

You don’t need to write JavaScript to get this. The networkD3 package wraps D3.js as an R htmlwidget — you hand it a nodes table and an edges table, and it renders a fully interactive D3.js force-directed graph directly inside your knitted HTML report.

library(networkD3)

# networkD3 wants nodes indexed from 0, so we build a lookup table first
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
)
  • nodes_d3 and links_d3 are just the same csub_graph data, reshaped into the two flat tables forceNetwork() expects — a nodes table and a 0-indexed edge list.
  • Nodesize = "pagerank" reuses the exact PageRank scores from the table and static plot above, so the same “bigger circle = higher rank” logic carries over — except now a student can drag Home away from Admissions and watch the layout physically resist it, which makes the “mutually reinforcing cluster” idea from the static plot much more tangible.
  • arrows = TRUE keeps the directionality visible, just like the static igraph plot.

Marketing takeaway: static charts are fine for a slide deck, but an interactive network like this is what you’d actually hand to a stakeholder exploring a customer graph, a site map, or a partner network — letting them drag, zoom, and hover into the specific nodes they care about instead of squinting at a fixed image.

What does wrapping the graph in networkD3 add that the static igraph plot didn’t have?


Real-world applications for marketing teams

1. Targeted marketing and advertising

  • Personalized offers: use shared attributes like birthdays to trigger automated, low-cost personalization at scale.
  • Group campaigns: target whole friend clusters (high triangle count) with a shared discount code, so the campaign spreads through an already-existing social bond instead of relying on paid reach.

2. Event planning and networking

  • Suggest events based on mutual connections, shared employers, or shared schools — the same signal that powers FoF recommendations.
  • Build professional or alumni networks by proposing introductions between people who share a background but haven’t met.

3. Influencer marketing

  • Use PageRank to shortlist candidates for brand partnerships — people whose reach compounds through the network rather than stopping at their own follower count.
  • Track how a message spreads outward from an influencer to gauge real campaign lift, not just impressions.

4. Community detection and social dynamics

  • Triangle-dense clusters reveal existing communities worth supporting rather than trying to manufacture from scratch.
  • Studying how these clusters shift over time can reveal how trends and information move through a customer base.

Monetizing the insights

  1. Social network analytics services — offer network-analysis consulting to brands who want to understand how their customers are actually connected to each other, not just to the brand.
  2. Custom recommendation engines — build FoF-style “people/products you may know” features for a client’s own app or CRM.
  3. Event and community platforms — charge for premium matchmaking between attendees who share mutual friends, employers, or schools.
  4. Advertising targeting tools — sell targeting packages built on triangle-density or PageRank scores instead of demographics alone.

Conclusion

The math didn’t change when we moved from PySpark to R — triangles are still triangles, and PageRank still rewards being connected to well-connected people. What changed is the barrier to entry: with igraph, a marketing analytics student can build, query, and visualize a friendship network on a laptop, using dplyr-style joins they already know, without standing up a Spark cluster first.

The techniques here — shared-attribute matching, triangle counting, friend-of-friend expansion, and PageRank — map onto four concrete marketing decisions: who to personalize for, who to run group campaigns against, who to introduce to whom, and who to sign as an influencer. That’s the real payoff of social network analysis for a marketing team: turning a graph of relationships into a shortlist of people worth acting on.

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



References

  • Leskovec, J., & Krevl, A. (2014). SNAP Datasets: Stanford Large Network Dataset Collection. Stanford Network Analysis Project.
  • Csardi, G., & Nepusz, T. (2006). The igraph software package for complex network research. InterJournal, Complex Systems, 1695.
  • Page, L., Brin, S., Motwani, R., & Winograd, T. (1999). The PageRank Citation Ranking: Bringing Order to the Web. Stanford InfoLab.
  • California State University, Bakersfield. Home, Admissions, and College of Business and Public Administration pages, csub.edu (accessed July 23, 2026) — source of the real link graph used in the website popularity example.
  • Original PySpark/GraphFrames walkthrough: Scaibu, Facebook Circles: A Deeper Dive into Social Network Analysis Using GraphFrames, Medium, Nov 18, 2024.
  • Bostock, M., Ogievetsky, V., & Heer, J. (2011). D3: Data-Driven Documents. IEEE Transactions on Visualization and Computer Graphics.
  • Gandrud, C., Allaire, J.J., & Russell, K. networkD3: D3 JavaScript Network Graphs from R. R package.
  • Xu, Z. “Jimmy” (2018). Social network analysis using R. RPubs. https://rpubs.com/utjimmyx/network1 — an earlier igraph walkthrough covering the fundamentals this tutorial builds on.