Warm intros from a graph: shortest path between two people
In short: put people and the organisations they belong to in one graph, and "who can introduce me" becomes a shortest-path query. I built a small demo of this on public Wikidata records for about six…
- published
- read time
- 5 min
- words
- 940
- lang
- en
- filed under
- Engineering
In short: put people and the organisations they belong to in one graph, and "who can introduce me" becomes a shortest-path query. I built a small demo of this on public Wikidata records for about six hundred billionaires. The path is one function call. Deciding when two records are the same person is the actual work.
The problem fundraisers already have
Anyone who raises money, for a charity, a university or a startup, knows a cold email to a wealthy stranger rarely works and an introduction from someone they trust often does. Prospect research teams spend real hours on the question "do we know anyone who knows them". It's usually answered from memory and spreadsheets.
I wanted to see how much of that a graph could answer on its own. So I built a working demo of the three things a prospect research tool does: search a pool of people by filters, write a short research brief on one of them, and find a warm path from someone on your side to someone you want to meet. This post is about the third.
An affiliation graph from public data
The data is real and free. A SPARQL query against Wikidata pulls about six hundred billionaires with their net worth, country, employers, universities and occupations. Wikidata is far from complete, but for public figures it records the affiliations that matter here: where someone studied and where they worked or sat.
The graph has two kinds of node. People are one kind. Organisations and universities are the other. An edge means "this person was affiliated with this place". Two people who share a university are then two hops apart, through the university, and the university node tells you why they might know each other. That "why" is what makes an intro feel warm instead of random.
The query itself
With the graph built, the warm intro is one call to networkx.shortest_path. Every organisation on the path is the reason for the hop next to it.
import networkx as nx
def build_graph(people):
"""people: {person_id: {"name": str, "affiliations": [org_id, ...]}}"""
G = nx.Graph()
for pid, p in people.items():
G.add_node(pid, kind="person", name=p["name"])
for org in p["affiliations"]:
G.add_node(org, kind="org")
G.add_edge(pid, org)
return G
def warm_path(G, insider, prospect):
try:
path = nx.shortest_path(G, insider, prospect)
except (nx.NetworkXNoPath, nx.NodeNotFound):
return None
people = [n for n in path if G.nodes[n]["kind"] == "person"]
return path, people[1:-1] # the people who would make the intro
The same graph gives a second signal almost for free. Degree centrality, the number of affiliations a person has, goes into a rough capacity score next to net worth. It's crude and it's useful for ranking a list.
Here is how the other features map to graph algorithms. Two of them are built, two are the next step.
| Feature | The question | Algorithm | Status |
|---|---|---|---|
| Warm intro | Who connects my insider to this prospect? | Shortest path | Built |
| Capacity score | How much could they give, and how connected are they? | Net worth plus degree centrality | Built |
| Prospect discovery | Who sits near the people who already give? | Personalized PageRank from existing donors | Next |
| Affinity clusters | Which groups of people move together? | Community detection (Louvain) | Next |
Shortest isn't always warmest
An unweighted shortest path treats every shared organisation the same. Two people who went to a university with tens of thousands of alumni are "two hops apart" exactly like two people who sat on the same five-person board. They are not the same kind of connection.
The obvious next change is to weight edges so that small organisations count as stronger ties, and overlapping years count more than decades apart. Then ask for the lightest path, not the shortest. I haven't built that yet, and I'd want a few real fundraisers to tell me whether the ranking it produces matches their instinct before trusting it.
The hard part is knowing who is who
The graph is only as good as its nodes. The same name isn't always the same person, and the same person isn't always the same string: "John A. Smith" and "J. Smith" may be one person, and two John Smiths in two cities are probably not. Once you pull from more than one source, this is most of the job.
The approach in the demo is a sketch, in five steps:
- Normalise names and addresses, and expand nicknames.
- Block candidates, for example by last name and region, so you don't compare every record with every other.
- Score each pair on name, geography, employer and shared graph neighbours.
- Sort pairs into three buckets: merge, keep apart, and a grey zone.
- Send only the grey zone to a language model as a tie-breaker, and to a human when it stays unsure.
The rule behind the thresholds is that the two mistakes don't cost the same. A false split is annoying: you miss a path. A false merge is dangerous: you attribute one person's wealth and connections to someone else, and maybe walk into a meeting with the wrong story. So I tune for high precision on merges and accept more splits.
Try it on your own network
You don't need a vendor to see whether this works for you. Export the affiliations you already have, from a CRM, a board list or an alumni file, as person and organisation pairs. Load them into networkx with the code above, pick one person you want to meet, and ask for the path. If the answer surprises you, check the merges along it before you send the email.
related