Writing

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.

insider university person B company prospect four hops, two shared affiliations
The path is the intro: the insider asks person B, whom they know from university, and person B knows the prospect through a company.

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.

FeatureThe questionAlgorithmStatus
Warm introWho connects my insider to this prospect?Shortest pathBuilt
Capacity scoreHow much could they give, and how connected are they?Net worth plus degree centralityBuilt
Prospect discoveryWho sits near the people who already give?Personalized PageRank from existing donorsNext
Affinity clustersWhich 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:

  1. Normalise names and addresses, and expand nicknames.
  2. Block candidates, for example by last name and region, so you don't compare every record with every other.
  3. Score each pair on name, geography, employer and shared graph neighbours.
  4. Sort pairs into three buckets: merge, keep apart, and a grey zone.
  5. 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

Keep reading