
Hr Network Analyst
- 121 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Analyze HR and professional network graphs to map influence, collaboration patterns, retention risks, and org-structure gaps for people-ops and workforce planning.
About
Provides structured HR network analysis for organizational graphs, helping interpret who connects to whom, where silos form, and which nodes drive influence or risk. Useful for people-ops leaders, workforce planners, and internal tools that visualize employee or candidate relationship data.
- Maps influence and collaboration clusters
- Surfaces retention and silo risks
- Supports restructuring and hiring plans
- Works from relationship or org-graph data
- Turns networks into actionable HR insights
Hr Network Analyst by the numbers
- 121 all-time installs (skills.sh)
- Ranked #765 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill hr-network-analystAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 121 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Analyze HR and professional network graphs to map influence, collaboration patterns, retention risks, and org-structure gaps for people-ops and workforce planning.
Files
HR Network Analyst
Applies graph theory and network science to professional relationship mapping. Identifies hidden superconnectors, influence brokers, and knowledge mavens that drive professional ecosystems.
Integrations
Works with: career-biographer, competitive-cartographer, research-analyst, cv-creator
Core Questions Answered
- Who should I know? (optimal networking targets)
- Who knows everyone? (superconnectors for referrals)
- Who bridges worlds? (cross-domain brokers)
- How does influence flow? (information/opportunity pathways)
- Where are structural holes? (untapped connection opportunities)
Quick Start
User: "Who are the key connectors in AI safety research?"
Process:
1. Define boundary: AI safety researchers, 2020-2024
2. Identify sources: arXiv, NeurIPS workshops, Twitter clusters
3. Compute centrality: betweenness (bridges), eigenvector (influence)
4. Classify by archetype: Connector, Maven, Broker
5. Output: Ranked list with network position rationaleKey principle: Most valuable people aren't always most famous—they connect otherwise disconnected worlds.
Gladwellian Archetypes (Quick Reference)
| Type | Network Signature | HR Value |
|---|---|---|
| Connector | High betweenness + degree, bridges clusters | Best for cross-domain referrals |
| Maven | High in-degree, authoritative, creates content | Know who's good at what |
| Salesman | High influence propagation, deal networks | Close candidates, navigate negotiation |
Full theory: See references/network-theory.md
Centrality Metrics (Quick Reference)
| Metric | Meaning | When to Use |
|---|---|---|
| Betweenness | Controls information flow | Finding gatekeepers, brokers |
| Degree | Raw connection count | Maximizing referral reach |
| Eigenvector | Quality over quantity | Access to power, rising stars |
| PageRank | Endorsed by important others | Thought leaders |
| Closeness | Can reach anyone quickly | Information spreading |
Analysis Workflows
1. Find Superconnectors for Referrals
- Define target domain → Seed network → Expand → Compute betweenness + degree → Rank
2. Map Domain Influence
- Define boundaries → Multi-source construction → Community detection → Identify brokers
3. Optimize Personal Networking
- Map current network → Map target domain → Find shortest paths → Identify structural holes
4. Organizational Network Analysis (ONA)
- Collect data (surveys, Slack metadata) → Construct graph → Find informal vs formal structure
Detailed workflows: See references/data-sources-implementation.md
Data Sources
| Source | Signal Strength | What to Extract |
|---|---|---|
| Co-authorship | Very strong | Publication collaborations |
| Conference co-panel | Strong | Speaking relationships |
| GitHub co-repo | Medium-strong | Code collaboration |
| LinkedIn connection | Medium | Professional links |
| Twitter mutual | Weak | Social association |
Multi-source fusion: Weight and combine signals for robust network
When NOT to Use
- Surveillance: Tracking individuals without consent
- Discrimination: Using network position to exclude
- Manipulation: Engineering social influence for harm
- Privacy violation: Accessing non-public data
- Speculation without data: Guessing network structure
Anti-Patterns
Anti-Pattern: Degree Obsession
What it looks like: Only looking at who has most connections Why wrong: High degree often = noise; connectors differ from popular Instead: Use betweenness for bridging, eigenvector for influence quality
Anti-Pattern: Static Network Assumption
What it looks like: Treating 5-year-old connections as current Why wrong: Networks evolve; old edges may be dead Instead: Recency-weight edges, verify currency
Anti-Pattern: Single-Source Reliance
What it looks like: Using only LinkedIn data Why wrong: Missing relationships not on LinkedIn Instead: Multi-source fusion with source-appropriate weighting
Anti-Pattern: Ignoring Context
What it looks like: High betweenness = valuable, regardless of domain Why wrong: Bridging irrelevant communities isn't useful Instead: Constrain analysis to relevant domain boundaries
Ethical Guidelines
Acceptable:
- Analyzing public data (conference speakers, publications)
- Aggregate pattern analysis
- Opt-in organizational analysis
- Academic research with proper IRB
NOT Acceptable:
- Scraping private profiles without consent
- Building surveillance systems
- Selling individual data
- Discrimination based on network position
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Can't find data | Domain small/private | Snowball sampling, surveys, adjacent communities |
| False edges | Over-weighting weak signals | Require multiple signals, threshold weights |
| Too large | Unconstrained boundary | K-core filtering, high-weight only |
| Entity resolution | Same person, different names | Unique IDs (ORCID), manual verification |
Reference Files
references/algorithms.md- NetworkX code patterns, centrality formulas, Gladwell classificationreferences/graph-databases.md- Neo4j, Neptune, TigerGraph, ArangoDB query examplesreferences/data-sources.md- LinkedIn network data acquisition strategies, APIs, scraping, legal considerations
---
Core insight: Advantage comes from bridging otherwise disconnected groups, not from connections within dense clusters. — Ron Burt, Structural Holes Theory
Changelog
[2.0.0] - 2024-01-XX
Changed
- BREAKING: Restructured from monolithic 694-line file to progressive disclosure architecture
- Fixed frontmatter format:
tools:→allowed-tools:(comma-separated) - Added NOT clause to description for precise activation boundaries
- Reduced SKILL.md from 694 lines to 148 lines (79% reduction)
Added
references/network-theory.md- Betweenness centrality, structural holes, Gladwell archetypesreferences/data-sources-implementation.md- Network construction, data extraction, Python code- Anti-patterns section with "What it looks like / Why wrong / Instead" format
- Quick reference for Gladwell archetypes (Connectors, Mavens, Salesmen)
- Compact analysis workflow table
Removed
- Verbose network science explanations (moved to references)
- Inline Python implementations (moved to references)
- Redundant metric calculations
Migration Guide
Reference files are now in /references/ directory. Import patterns:
- Network theory background →
references/network-theory.md - Python implementations →
references/data-sources-implementation.md
Network Analysis Algorithms Reference
Centrality Measures
Betweenness Centrality
Mathematical Definition:
CB(v) = Σ σst(v) / σst
s≠v≠tWhere:
- σst = total number of shortest paths from node s to node t
- σst(v) = number of those paths passing through v
Interpretation: Measures how often a node acts as a bridge along shortest paths.
NetworkX Implementation:
import networkx as nx
# Basic betweenness
bc = nx.betweenness_centrality(G)
# Weighted (edges with 'weight' attribute)
bc_weighted = nx.betweenness_centrality(G, weight='weight')
# Normalized (default) vs unnormalized
bc_unnorm = nx.betweenness_centrality(G, normalized=False)
# Approximate (faster for large graphs)
bc_approx = nx.betweenness_centrality(G, k=100) # sample k nodesComplexity: O(VE) for unweighted, O(VE + V² log V) for weighted
---
Degree Centrality
Mathematical Definition:
CD(v) = deg(v) / (n-1)Interpretation: Simple count of connections, normalized.
NetworkX Implementation:
# Undirected
dc = nx.degree_centrality(G)
# Directed
in_dc = nx.in_degree_centrality(G)
out_dc = nx.out_degree_centrality(G)---
Eigenvector Centrality
Mathematical Definition:
xi = (1/λ) Σ Aij xj
jWhere A is the adjacency matrix and λ is the largest eigenvalue.
Interpretation: A node is important if connected to other important nodes (recursive).
NetworkX Implementation:
ec = nx.eigenvector_centrality(G)
# With weights
ec_weighted = nx.eigenvector_centrality(G, weight='weight')
# NumPy version (faster)
ec_numpy = nx.eigenvector_centrality_numpy(G)---
PageRank
Mathematical Definition:
PR(v) = (1-d)/N + d Σ PR(u)/L(u)
u∈Bin(v)Where:
- d = damping factor (typically 0.85)
- N = total nodes
- Bin(v) = nodes linking to v
- L(u) = outgoing links from u
Interpretation: Probability of random walker landing on node.
NetworkX Implementation:
pr = nx.pagerank(G)
# Custom damping
pr = nx.pagerank(G, alpha=0.9)
# With weights
pr = nx.pagerank(G, weight='weight')---
Closeness Centrality
Mathematical Definition:
CC(v) = (n-1) / Σ d(v,u)
u≠vInterpretation: Inverse of average distance to all other nodes.
NetworkX Implementation:
cc = nx.closeness_centrality(G)
# For disconnected graphs
cc = nx.closeness_centrality(G, wf_improved=True)---
Structural Holes (Burt)
Constraint
Mathematical Definition:
Ci = Σ cij²
j
cij = (pij + Σ piq × pqj)²
qWhere pij = proportion of i's network invested in j.
Interpretation: How constrained is a node by its network? Low constraint = spanning structural holes.
NetworkX Implementation:
constraint = nx.constraint(G)
# With weights
constraint_w = nx.constraint(G, weight='weight')
# For specific nodes
constraint_node = nx.constraint(G, nodes=['Alice', 'Bob'])Effective Size
Mathematical Definition:
ES(i) = Σ [1 - Σ piq × mjq]
j q≠j
mjq = pjq / max(pkq for all k)Interpretation: Redundancy-adjusted network size.
NetworkX Implementation:
eff_size = nx.effective_size(G)---
Community Detection
Louvain Algorithm
Implementation:
from networkx.algorithms.community import louvain_communities
communities = louvain_communities(G)
# With resolution parameter
communities = louvain_communities(G, resolution=1.5)
# Get partition as dict
partition = {}
for i, comm in enumerate(communities):
for node in comm:
partition[node] = iLabel Propagation
from networkx.algorithms.community import label_propagation_communities
communities = label_propagation_communities(G)Modularity Score
from networkx.algorithms.community import modularity
Q = modularity(G, communities)---
Network Statistics
Basic Properties
# Number of nodes and edges
n = G.number_of_nodes()
m = G.number_of_edges()
# Density
density = nx.density(G)
# Average clustering coefficient
avg_clustering = nx.average_clustering(G)
# Transitivity (global clustering)
transitivity = nx.transitivity(G)
# Average shortest path (for connected graphs)
if nx.is_connected(G):
avg_path = nx.average_shortest_path_length(G)
# Diameter
if nx.is_connected(G):
diameter = nx.diameter(G)K-Core Decomposition
# Find k-core (subgraph where all nodes have degree >= k)
k_core = nx.k_core(G, k=5)
# Core number of each node
core_numbers = nx.core_number(G)---
Useful Patterns
Multi-Layer Network Fusion
def fuse_networks(networks, weights):
"""
Combine multiple network sources with weights.
Args:
networks: dict of {source_name: networkx.Graph}
weights: dict of {source_name: float}
Returns:
Fused network with combined edge weights
"""
G_fused = nx.Graph()
for source, G_source in networks.items():
w = weights.get(source, 1.0)
for u, v, data in G_source.edges(data=True):
edge_weight = data.get('weight', 1.0) * w
if G_fused.has_edge(u, v):
G_fused[u][v]['weight'] += edge_weight
G_fused[u][v]['sources'].append(source)
else:
G_fused.add_edge(u, v, weight=edge_weight, sources=[source])
return G_fusedTemporal Decay Weighting
from datetime import datetime
import math
def apply_temporal_decay(G, date_attr='date', half_life_days=365):
"""
Apply exponential decay to edge weights based on recency.
"""
now = datetime.now()
for u, v, data in G.edges(data=True):
if date_attr in data:
edge_date = data[date_attr]
days_old = (now - edge_date).days
decay = math.exp(-math.log(2) * days_old / half_life_days)
data['weight'] = data.get('weight', 1.0) * decay
return GGladwell Classification
def classify_gladwell(G, metrics=None):
"""
Classify nodes into Gladwell archetypes.
Returns dict mapping node -> archetype
"""
if metrics is None:
metrics = {
'betweenness': nx.betweenness_centrality(G),
'degree': nx.degree_centrality(G),
'eigenvector': nx.eigenvector_centrality(G),
}
try:
metrics['constraint'] = nx.constraint(G)
except:
metrics['constraint'] = {n: 0.5 for n in G.nodes()}
classifications = {}
# Compute thresholds (top percentile)
bc_threshold = sorted(metrics['betweenness'].values())[-int(len(G)*0.1)]
dc_threshold = sorted(metrics['degree'].values())[-int(len(G)*0.1)]
ec_threshold = sorted(metrics['eigenvector'].values())[-int(len(G)*0.1)]
for node in G.nodes():
bc = metrics['betweenness'][node]
dc = metrics['degree'][node]
ec = metrics['eigenvector'][node]
constraint = metrics['constraint'].get(node, 0.5)
if bc >= bc_threshold and dc >= dc_threshold:
classifications[node] = 'connector'
elif ec >= ec_threshold and dc < dc_threshold:
classifications[node] = 'maven'
elif dc >= dc_threshold and constraint < 0.3:
classifications[node] = 'salesman'
else:
classifications[node] = 'standard'
return classifications---
Data Source APIs
Semantic Scholar
import requests
def get_author_collaborators(author_id):
"""Get co-authors from Semantic Scholar."""
url = f"https://api.semanticscholar.org/graph/v1/author/{author_id}"
params = {
'fields': 'papers.authors'
}
resp = requests.get(url, params=params)
data = resp.json()
coauthors = set()
for paper in data.get('papers', []):
for author in paper.get('authors', []):
if author['authorId'] != author_id:
coauthors.add((author['authorId'], author['name']))
return coauthorsGitHub
import requests
def get_repo_contributors(owner, repo, token=None):
"""Get contributors to a GitHub repo."""
headers = {}
if token:
headers['Authorization'] = f'token {token}'
url = f"https://api.github.com/repos/{owner}/{repo}/contributors"
resp = requests.get(url, headers=headers)
return [(c['login'], c['contributions']) for c in resp.json()]
def build_github_network(repos, token=None):
"""Build collaboration network from list of repos."""
G = nx.Graph()
for owner, repo in repos:
contributors = get_repo_contributors(owner, repo, token)
# Add edges between all contributors to same repo
for i, (user1, contrib1) in enumerate(contributors):
for user2, contrib2 in contributors[i+1:]:
if G.has_edge(user1, user2):
G[user1][user2]['weight'] += 1
G[user1][user2]['repos'].append(f"{owner}/{repo}")
else:
G.add_edge(user1, user2, weight=1, repos=[f"{owner}/{repo}"])
return G---
Visualization
Interactive HTML Network
from pyvis.network import Network
def visualize_network(G, metrics, output='network.html'):
"""Create interactive HTML visualization."""
net = Network(height='800px', width='100%', bgcolor='#1a1a2e')
# Color map for Gladwell types
colors = {
'connector': '#e94560',
'maven': '#0f3460',
'salesman': '#16c79a',
'standard': '#666666'
}
classifications = classify_gladwell(G, metrics)
for node in G.nodes():
bc = metrics['betweenness'][node]
size = 10 + 100 * bc
color = colors[classifications[node]]
title = f"{node}<br>Type: {classifications[node]}<br>BC: {bc:.4f}"
net.add_node(node, size=size, color=color, title=title)
for u, v, data in G.edges(data=True):
weight = data.get('weight', 1)
net.add_edge(u, v, value=weight)
net.show_buttons(filter_=['physics'])
net.save_graph(output)Static Matplotlib
import matplotlib.pyplot as plt
def plot_network_static(G, metrics, figsize=(12, 12)):
"""Create static network visualization."""
fig, ax = plt.subplots(figsize=figsize)
# Layout
pos = nx.spring_layout(G, k=2, iterations=50)
# Node sizes by betweenness
node_sizes = [1000 * metrics['betweenness'][n] + 50 for n in G.nodes()]
# Node colors by eigenvector centrality
node_colors = [metrics['eigenvector'][n] for n in G.nodes()]
nx.draw_networkx(
G, pos, ax=ax,
node_size=node_sizes,
node_color=node_colors,
cmap=plt.cm.viridis,
with_labels=True,
font_size=8,
alpha=0.8
)
plt.colorbar(plt.cm.ScalarMappable(cmap=plt.cm.viridis),
label='Eigenvector Centrality', ax=ax)
plt.tight_layout()
return figData Sources & Implementation Reference
Network construction from multiple professional data sources.
Primary Data Sources
LinkedIn Analysis
Extract: Connection overlaps, shared experiences, endorsement patterns, group memberships, comment networks
Ethical considerations: Respect rate limits/ToS, public data only, aggregate patterns
from networkx import bipartite
people_projection = bipartite.projected_graph(B, people_nodes)Conference & Event Networks
Edge weights:
- Co-speaking at same event → strong
- Same session/track → medium
- Same conference → weak
- Panel co-participation → very strong
High-value by domain:
- Tech: Strange Loop, QCon, domain-specific (RustConf)
- AI/ML: NeurIPS, ICML, ICLR workshops
- Data: Strata, dbt Coalesce
Publication & Co-authorship
Sources: Semantic Scholar (open), Google Scholar, arXiv, DBLP, PubMed
Edge weighting:
- Co-authorship count (repeated = trust)
- Citation flows
- Author list position (first/last = more weight)
GitHub & Open Source
Extract: Repo collaboration, review relationships, org membership, sponsorship, issues
Quality signals:
- Sustained > one-off contribution
- Cross-project = broader network
- Maintainer = trust indicator
Twitter/X Analysis
Extract: Follow graphs, mutual follows, quote-tweet/reply networks, list memberships
Reddit & Community
Extract: Cross-subreddit posting (bridges), comment interactions, moderator networks
Multi-Layer Network Fusion
edge_weights = {
'coauthor': 1.0, # Strongest
'conference_copanel': 0.8,
'linkedin_connection': 0.5,
'github_corepo': 0.6,
'twitter_mutual': 0.3,
}
G_unified = nx.Graph()
for source, weight in edge_weights.items():
for u, v in source_graphs[source].edges():
if G_unified.has_edge(u, v):
G_unified[u][v]['weight'] += weight
else:
G_unified.add_edge(u, v, weight=weight)Entity Resolution
Challenge: Same person across sources
- "Jane Smith" (LinkedIn)
- "J. Smith" (papers)
- "@janesmith" (Twitter)
- "jsmith" (GitHub)
Approaches:
- Email as unique identifier
- ORCID for researchers
- LinkedIn URL as canonical
- Fuzzy matching with verification
Analysis Implementation
import networkx as nx
import pandas as pd
from pyvis.network import Network
def analyze_professional_network(edges_df):
G = nx.from_pandas_edgelist(edges_df, 'source', 'target', ['weight'])
metrics = {
'betweenness': nx.betweenness_centrality(G, weight='weight'),
'degree': nx.degree_centrality(G),
'eigenvector': nx.eigenvector_centrality(G, weight='weight'),
'pagerank': nx.pagerank(G, weight='weight'),
}
constraint = nx.constraint(G, weight='weight')
communities = nx.community.louvain_communities(G)
def classify_gladwell(node):
bc = metrics['betweenness'][node]
dc = metrics['degree'][node]
ec = metrics['eigenvector'][node]
if bc > 0.1 and dc > 0.1:
return 'connector'
elif ec > 0.1 and dc < 0.05:
return 'maven'
elif dc > 0.05 and constraint.get(node, 1) < 0.3:
return 'salesman'
return 'standard'
return {
'metrics': metrics,
'constraint': constraint,
'communities': communities,
'classifications': {n: classify_gladwell(n) for n in G.nodes()}
}Visualization
def visualize_network_html(G, metrics, output_path='network.html'):
net = Network(height='800px', width='100%', bgcolor='#222222')
for node in G.nodes():
size = 10 + 50 * metrics['betweenness'][node]
net.add_node(node, size=size, title=f"BC: {metrics['betweenness'][node]:.3f}")
for edge in G.edges():
net.add_edge(edge[0], edge[1])
net.show(output_path)Temporal Considerations
- Recency-weight edges (recent > old)
- Track rising stars (centrality trajectory)
- Identify fading connections
- Seasonal patterns (conference cycles)
Professional Network Data Acquisition Guide
Executive Summary
This document provides a comprehensive guide to acquiring professional network data for graph analysis, with a focus on identifying superconnectors, influence brokers, and knowledge mavens. We cover official APIs, third-party data providers, scraping tools, and alternative network reconstruction strategies—along with legal considerations and practical implementation code.
Key Insight: For network analysis purposes, reconstructing professional networks from public sources (publications, conferences, GitHub) often yields higher-quality relationship data than LinkedIn scraping, which captures connections rather than actual collaboration.
---
Table of Contents
1. Data Source Hierarchy 2. Official LinkedIn Routes 3. Third-Party Data Providers 4. Scraping Tools 5. Alternative Network Reconstruction 6. Legal Considerations 7. Recommended Strategy
---
Data Source Hierarchy
Quality vs. Accessibility Matrix
| Data Source | Relationship Quality | Accessibility | Cost | Legal Risk |
|---|---|---|---|---|
| Co-authorship (papers) | ★★★★★ | High | Free | None |
| GitHub collaboration | ★★★★☆ | High | Free | None |
| Conference co-speaking | ★★★★☆ | Medium | Free | None |
| LinkedIn connections | ★★☆☆☆ | Low | $$$ | Medium |
| Email/calendar data | ★★★★★ | Very Low | N/A | High |
Why LinkedIn connections are low quality for network analysis:
- People accept connections from strangers
- No indication of relationship strength
- No collaboration signal
- Includes recruiters, salespeople, random requests
Why co-authorship is gold:
- Months of collaboration required
- Trust signal (putting your name on shared work)
- Repeated co-authorship = strong relationship
- Position on author list indicates role
---
Official LinkedIn Routes
1. Personal Data Export
Access: Settings → Data Privacy → Get a copy of your data
What you get:
Connections.csv: Name, company, position, connected dateMessages.csv: Message historyInvitations.csv: Sent/received invitations
Limitations:
- Only YOUR 1st-degree network
- No 2nd-degree visibility
- No relationship strength indicators
Use case: Analyzing your own network position, finding paths to targets
import pandas as pd
# Load your LinkedIn export
connections = pd.read_csv('Connections.csv', skiprows=3)
# Basic analysis
print(f"Total connections: {len(connections)}")
print(f"Companies represented: {connections['Company'].nunique()}")
# Find potential bridges (people at companies you have few connections to)
company_counts = connections['Company'].value_counts()
rare_companies = company_counts[company_counts <= 2].index
bridges = connections[connections['Company'].isin(rare_companies)]2. LinkedIn Sales Navigator
Pricing: $99.99/mo (Core) to $179.99/mo (Advanced)
Capabilities:
- Advanced search filters (industry, company size, seniority)
- Lead lists and saved searches
- InMail credits
- Account mapping (org charts)
Export options:
- Lead lists to CSV (limited fields)
- CRM integrations (Salesforce, HubSpot)
For network analysis:
- Can identify target individuals
- No graph structure data
- Best combined with enrichment tools
3. LinkedIn APIs (Enterprise)
Available APIs:
- Marketing API (ad targeting, company pages)
- Talent Solutions API (recruiting)
- Learning API (course completions)
- Consumer API (deprecated for most uses)
Access requirements:
- Enterprise partnership agreement
- Significant annual spend ($50K+)
- Compliance review
Reality: Not accessible for most network analysis use cases.
---
Third-Party Data Providers
Tier 1: Full-Service B2B Data
Apollo.io
Data: 275M+ contacts, 73M+ companies
Pricing:
- Free: 50 credits/month
- Basic: $49/mo (900 credits)
- Professional: $99/mo (unlimited emails)
Best for: Lead generation, email finding, basic enrichment
API Example:
import requests
APOLLO_API_KEY = 'your_key'
def search_people(domain, title_keywords):
"""Search Apollo for people at a company."""
response = requests.post(
'https://api.apollo.io/v1/mixed_people/search',
headers={'X-Api-Key': APOLLO_API_KEY},
json={
'q_organization_domains': domain,
'person_titles': title_keywords,
'page': 1,
'per_page': 25
}
)
return response.json()['people']
# Find ML engineers at top AI labs
for domain in ['anthropic.com', 'openai.com', 'deepmind.com']:
people = search_people(domain, ['Machine Learning', 'Research'])
print(f"{domain}: {len(people)} people found")ZoomInfo
Data: 100M+ business profiles, org charts, intent data
Pricing: Enterprise (contact sales, typically $15K+/year)
Best for: Enterprise recruiting, account-based marketing
Network analysis value: Org charts can reveal internal influence structures
Clearbit (now Breeze by HubSpot)
Data: Company + person enrichment
Pricing: Per-lookup ($0.05-0.20 per enrichment)
API Example:
import clearbit
clearbit.key = 'your_key'
# Enrich a person by email
person = clearbit.Person.find(email='elon@tesla.com', stream=True)
print(f"Name: {person['name']['fullName']}")
print(f"Role: {person['employment']['title']}")
print(f"Company: {person['employment']['name']}")
print(f"LinkedIn: {person['linkedin']['handle']}")Tier 2: LinkedIn-Specific APIs
Proxycurl
What it does: API wrapper for LinkedIn profile data
Pricing:
- $0.01 per profile lookup
- $0.003 per company lookup
- Bulk discounts available
Data returned:
- Full profile (experience, education, skills)
- Company data
- Job postings
API Example:
import requests
PROXYCURL_API_KEY = 'your_key'
def get_linkedin_profile(linkedin_url):
"""Fetch full LinkedIn profile data."""
response = requests.get(
'https://nubela.co/proxycurl/api/v2/linkedin',
params={'url': linkedin_url},
headers={'Authorization': f'Bearer {PROXYCURL_API_KEY}'}
)
return response.json()
# Get profile data
profile = get_linkedin_profile('https://linkedin.com/in/satlokomern')
# Extract for network analysis
person = {
'name': profile['full_name'],
'current_company': profile['experiences'][0]['company'] if profile['experiences'] else None,
'past_companies': [exp['company'] for exp in profile['experiences']],
'education': [edu['school'] for edu in profile['education']],
'connections': profile.get('connections') # Often not available
}People Data Labs
What it does: Bulk access to 1.5B+ person records
Pricing: API credits, starting ~$0.01/record
Best for: Large-scale network reconstruction
Key advantage: Employment history allows you to infer "worked together" relationships
import requests
PDL_API_KEY = 'your_key'
def find_coworkers(company, year_range):
"""Find people who worked at a company during a time period."""
response = requests.get(
'https://api.peopledatalabs.com/v5/person/search',
headers={'X-Api-Key': PDL_API_KEY},
params={
'query': f"experience.company.name:{company} AND experience.start_date:[{year_range[0]} TO {year_range[1]}]",
'size': 100
}
)
return response.json()['data']
# Find people who worked at Stripe 2018-2022
stripe_alumni = find_coworkers('Stripe', ('2018', '2022'))
# People with overlapping tenure likely know each other
# This is MUCH better than LinkedIn connections for inferring real relationships---
Scraping Tools
Browser Automation Tools
These tools automate YOUR logged-in LinkedIn session:
Phantombuster
Pricing: Free tier (2 hours/day) to $900/mo (enterprise)
Capabilities:
- Profile visitor (triggers profile views)
- Profile scraper (export profile data)
- Search export (save search results)
- Connection automation
Rate limits: ~80 profiles/day safely
Example workflow:
1. Sales Navigator search → Save to CSV
2. Phantombuster profile scraper → Enrich with full data
3. Export to Google Sheets or Airtable
4. NetworkX analysisEvaboot
Pricing: $49-99/mo
Specialization: Sales Navigator export specifically
What it does: One-click export of Sales Navigator searches
Dux-Soup / Octopus CRM / Waalaxy
Pricing: $15-100/mo range
Capabilities: Similar browser automation, varying features
Scraping Risks & Mitigation
Account ban risk factors:
- Too many profile views (>100/day)
- Automated patterns (regular intervals)
- Scraping from non-premium account
- Using headless browsers
Mitigation strategies:
import random
import time
def human_like_delay():
"""Randomized delays to avoid detection."""
base_delay = random.uniform(30, 90) # 30-90 seconds
jitter = random.gauss(0, 10) # Normal distribution jitter
return max(15, base_delay + jitter)
def scrape_with_delays(profile_urls):
"""Scrape with human-like patterns."""
results = []
for i, url in enumerate(profile_urls):
# Don't scrape more than ~50/day
if i >= 50:
print("Daily limit reached")
break
result = scrape_profile(url)
results.append(result)
delay = human_like_delay()
print(f"Waiting {delay:.1f}s before next request...")
time.sleep(delay)
# Take breaks
if i > 0 and i % 10 == 0:
long_break = random.uniform(300, 600) # 5-10 minute break
print(f"Taking {long_break/60:.1f} minute break...")
time.sleep(long_break)
return results---
Alternative Network Reconstruction
This is the recommended approach for finding superconnectors.
1. Publication Co-authorship Networks
Why this is gold: Co-authorship requires months of collaboration and trust.
Semantic Scholar API (Free, Excellent)
import requests
from collections import defaultdict
import networkx as nx
class SemanticScholarNetwork:
BASE_URL = 'https://api.semanticscholar.org/graph/v1'
def __init__(self):
self.G = nx.Graph()
self.author_cache = {}
def search_papers(self, query, limit=100):
"""Search for papers by topic."""
response = requests.get(
f'{self.BASE_URL}/paper/search',
params={
'query': query,
'limit': limit,
'fields': 'title,authors,year,citationCount'
}
)
return response.json().get('data', [])
def get_author_papers(self, author_id):
"""Get all papers by an author."""
response = requests.get(
f'{self.BASE_URL}/author/{author_id}',
params={'fields': 'papers.authors,papers.title,papers.year'}
)
return response.json()
def build_coauthorship_network(self, papers):
"""Build network from list of papers."""
for paper in papers:
authors = paper.get('authors', [])
author_ids = [a['authorId'] for a in authors if a.get('authorId')]
# Add nodes
for author in authors:
if author.get('authorId'):
self.G.add_node(
author['authorId'],
name=author.get('name', 'Unknown')
)
# Add edges between all co-authors
for i, a1 in enumerate(author_ids):
for a2 in author_ids[i+1:]:
if self.G.has_edge(a1, a2):
self.G[a1][a2]['weight'] += 1
self.G[a1][a2]['papers'].append(paper.get('title'))
else:
self.G.add_edge(a1, a2, weight=1, papers=[paper.get('title')])
return self.G
def find_superconnectors(self, top_n=20):
"""Find authors with highest betweenness centrality."""
bc = nx.betweenness_centrality(self.G, weight='weight')
sorted_bc = sorted(bc.items(), key=lambda x: x[1], reverse=True)
results = []
for author_id, score in sorted_bc[:top_n]:
results.append({
'author_id': author_id,
'name': self.G.nodes[author_id].get('name'),
'betweenness': score,
'degree': self.G.degree(author_id),
'collaborators': list(self.G.neighbors(author_id))
})
return results
# Usage Example: Find AI Safety superconnectors
network = SemanticScholarNetwork()
# Search for AI safety papers
papers = network.search_papers('AI safety alignment', limit=500)
print(f"Found {len(papers)} papers")
# Build co-authorship network
G = network.build_coauthorship_network(papers)
print(f"Network: {G.number_of_nodes()} authors, {G.number_of_edges()} collaborations")
# Find superconnectors
superconnectors = network.find_superconnectors(top_n=10)
for sc in superconnectors:
print(f"{sc['name']}: BC={sc['betweenness']:.4f}, {sc['degree']} collaborators")arXiv API (Free)
import arxiv
from collections import defaultdict
def build_arxiv_network(query, max_results=500):
"""Build co-authorship network from arXiv papers."""
search = arxiv.Search(
query=query,
max_results=max_results,
sort_by=arxiv.SortCriterion.SubmittedDate
)
G = nx.Graph()
for paper in search.results():
authors = [a.name for a in paper.authors]
# Add nodes
for author in authors:
if author not in G:
G.add_node(author, papers=1)
else:
G.nodes[author]['papers'] += 1
# Add edges
for i, a1 in enumerate(authors):
for a2 in authors[i+1:]:
if G.has_edge(a1, a2):
G[a1][a2]['weight'] += 1
else:
G.add_edge(a1, a2, weight=1)
return G
# Example: LLM research network
G = build_arxiv_network('cat:cs.CL AND (large language model OR LLM)', max_results=1000)2. GitHub Collaboration Networks
import requests
from collections import defaultdict
class GitHubNetwork:
def __init__(self, token):
self.token = token
self.headers = {'Authorization': f'token {token}'}
self.G = nx.Graph()
def get_repo_contributors(self, owner, repo):
"""Get all contributors to a repository."""
contributors = []
page = 1
while True:
response = requests.get(
f'https://api.github.com/repos/{owner}/{repo}/contributors',
headers=self.headers,
params={'page': page, 'per_page': 100}
)
if response.status_code != 200:
break
data = response.json()
if not data:
break
contributors.extend(data)
page += 1
return contributors
def get_pr_reviewers(self, owner, repo, limit=100):
"""Get PR review relationships (high trust signal)."""
response = requests.get(
f'https://api.github.com/repos/{owner}/{repo}/pulls',
headers=self.headers,
params={'state': 'all', 'per_page': limit}
)
reviews = defaultdict(lambda: defaultdict(int))
for pr in response.json():
author = pr['user']['login']
# Get reviewers for this PR
review_response = requests.get(
pr['url'] + '/reviews',
headers=self.headers
)
for review in review_response.json():
reviewer = review['user']['login']
if reviewer != author:
reviews[author][reviewer] += 1
return reviews
def build_network_from_repos(self, repos):
"""Build collaboration network from list of repos."""
for owner, repo in repos:
contributors = self.get_repo_contributors(owner, repo)
# Add nodes
for c in contributors:
login = c['login']
if login not in self.G:
self.G.add_node(login, contributions=c['contributions'])
else:
self.G.nodes[login]['contributions'] += c['contributions']
# Add edges between contributors (same repo = collaboration)
logins = [c['login'] for c in contributors]
for i, u1 in enumerate(logins):
for u2 in logins[i+1:]:
if self.G.has_edge(u1, u2):
self.G[u1][u2]['weight'] += 1
self.G[u1][u2]['repos'].append(f"{owner}/{repo}")
else:
self.G.add_edge(u1, u2, weight=1, repos=[f"{owner}/{repo}"])
return self.G
# Example: Build ML framework contributor network
github = GitHubNetwork('your_token')
ml_repos = [
('pytorch', 'pytorch'),
('tensorflow', 'tensorflow'),
('huggingface', 'transformers'),
('langchain-ai', 'langchain'),
('anthropics', 'anthropic-sdk-python'),
]
G = github.build_network_from_repos(ml_repos)
print(f"Network: {G.number_of_nodes()} developers, {G.number_of_edges()} collaborations")3. Conference Speaker Networks
import requests
from bs4 import BeautifulSoup
from collections import defaultdict
class ConferenceNetwork:
def __init__(self):
self.G = nx.Graph()
self.speakers = defaultdict(list) # speaker -> conferences
def scrape_neurips_speakers(self, year):
"""Scrape NeurIPS speaker list."""
# Note: Actual implementation depends on conference website structure
# This is a template
url = f'https://neurips.cc/{year}/Schedule'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
sessions = []
# Parse session data...
return sessions
def add_conference_edges(self, conference_name, sessions):
"""Add edges for co-presenters at same session."""
for session in sessions:
speakers = session.get('speakers', [])
# Add nodes
for speaker in speakers:
if speaker not in self.G:
self.G.add_node(speaker, conferences=[conference_name])
else:
if conference_name not in self.G.nodes[speaker]['conferences']:
self.G.nodes[speaker]['conferences'].append(conference_name)
# Same panel/session = strong edge
for i, s1 in enumerate(speakers):
for s2 in speakers[i+1:]:
if self.G.has_edge(s1, s2):
self.G[s1][s2]['weight'] += 2 # Co-panel is strong signal
else:
self.G.add_edge(s1, s2, weight=2, type='co_panel')
# Same conference = weak edge (add later in batch)
def add_same_conference_edges(self):
"""Add weak edges between all speakers at same conference."""
conference_speakers = defaultdict(list)
for node in self.G.nodes():
for conf in self.G.nodes[node].get('conferences', []):
conference_speakers[conf].append(node)
for conf, speakers in conference_speakers.items():
for i, s1 in enumerate(speakers):
for s2 in speakers[i+1:]:
if not self.G.has_edge(s1, s2):
self.G.add_edge(s1, s2, weight=0.5, type='same_conference')4. Multi-Source Fusion
def fuse_professional_networks(networks, weights):
"""
Combine multiple network sources with configurable weights.
Args:
networks: dict of {source_name: nx.Graph}
weights: dict of {source_name: float}
Returns:
Unified nx.Graph with combined edge weights
"""
G_unified = nx.Graph()
# Entity resolution: normalize names across sources
name_mapping = {} # Maps variations to canonical name
for source, G_source in networks.items():
source_weight = weights.get(source, 1.0)
for u, v, data in G_source.edges(data=True):
# Normalize names
u_canonical = name_mapping.get(u, u)
v_canonical = name_mapping.get(v, v)
edge_weight = data.get('weight', 1.0) * source_weight
if G_unified.has_edge(u_canonical, v_canonical):
G_unified[u_canonical][v_canonical]['weight'] += edge_weight
G_unified[u_canonical][v_canonical]['sources'].append(source)
else:
G_unified.add_edge(
u_canonical, v_canonical,
weight=edge_weight,
sources=[source]
)
return G_unified
# Example usage
networks = {
'semantic_scholar': coauthorship_network,
'github': github_network,
'conferences': conference_network,
}
weights = {
'semantic_scholar': 1.0, # Strongest signal
'github': 0.8, # Strong signal
'conferences': 0.6, # Medium signal
}
unified = fuse_professional_networks(networks, weights)
# Now analyze the unified network
bc = nx.betweenness_centrality(unified, weight='weight')
superconnectors = sorted(bc.items(), key=lambda x: x[1], reverse=True)[:20]---
Legal Considerations
LinkedIn Terms of Service
Prohibited activities (Section 8.2):
- Scraping or copying profiles
- Using bots or automated tools
- Circumventing access restrictions
Consequences:
- Account suspension/termination
- Legal action (rare but possible)
hiQ Labs v. LinkedIn (2022)
Key ruling: Scraping publicly available LinkedIn data does not violate the Computer Fraud and Abuse Act (CFAA).
What this means:
- Criminal liability unlikely for public data scraping
- LinkedIn can still enforce ToS (ban accounts)
- Civil liability less clear
Limitations:
- Does not apply to logged-in scraping
- Does not override other laws (GDPR, CCPA)
- LinkedIn may use technical measures
GDPR Considerations (EU)
If processing EU residents' data:
- Need legal basis (legitimate interest, consent)
- Data minimization required
- Right to erasure must be honored
- Document your processing activities
Best Practices for Compliance
1. Prefer public APIs (Semantic Scholar, GitHub, arXiv) 2. Use official exports for your own data 3. Buy from legitimate providers who handle compliance 4. Document business purpose for any scraping 5. Don't store unnecessary PII 6. Honor opt-out requests
---
Recommended Strategy
For Finding Superconnectors
Don't start with LinkedIn. Start with better data:
Week 1: Define Target Domain
├── List key conferences
├── Identify top journals/venues
├── Find major open source projects
└── Note industry thought leaders (seed nodes)
Week 2: Build Publication Network
├── Semantic Scholar API for papers
├── arXiv for preprints
├── Google Scholar for citations
└── Identify high-betweenness authors
Week 3: Add Collaboration Signals
├── GitHub contributor networks
├── Conference speaker lists
├── Podcast guest appearances
└── Fuse with publication network
Week 4: Targeted Enrichment
├── Proxycurl for top 50 candidates
├── Apollo for contact info
├── LinkedIn manual research
└── Prioritized outreach listCost-Effective Stack
| Purpose | Tool | Monthly Cost |
|---|---|---|
| Publication data | Semantic Scholar API | Free |
| Code collaboration | GitHub API | Free |
| Conference data | Web scraping | Free |
| Contact enrichment | Apollo.io | $49 |
| LinkedIn profiles | Proxycurl | ~$20 (pay per use) |
| Total | ~$70/month |
For Organizational Network Analysis
Internal data sources (requires proper authorization):
- Slack/Teams message patterns
- Meeting co-attendance (calendar)
- Email headers (not content)
- Document collaboration
- Code review assignments
Survey-based ONA:
- "Who do you go to for advice?"
- "Who do you collaborate with most?"
- "Who is influential in decisions?"
This is the gold standard for ONA—explicit relationship mapping with consent.
---
Conclusion
For professional network analysis aimed at finding superconnectors:
1. LinkedIn is overrated for this purpose—connections ≠ relationships 2. Co-authorship and code collaboration are stronger signals 3. Public data sources are legally safer and often higher quality 4. Targeted enrichment of top candidates is more cost-effective than bulk scraping 5. Multi-source fusion produces the most accurate network maps
The best superconnector identification comes from asking: "Who has actually worked with people across multiple communities?"—and that question is best answered by publication, code, and conference data rather than LinkedIn connection counts.
Graph Database Analysis Reference
Neo4j
Neo4j is the most popular graph database for professional network analysis. It uses Cypher query language and has built-in graph data science algorithms.
Setup
// Create constraint for unique person IDs
CREATE CONSTRAINT person_id IF NOT EXISTS
FOR (p:Person) REQUIRE p.id IS UNIQUE;
// Create index for faster lookups
CREATE INDEX person_name IF NOT EXISTS
FOR (p:Person) ON (p.name);Data Model for Professional Networks
// Node types
(:Person {id, name, email, role, company, linkedin_url})
(:Company {id, name, industry, size})
(:Conference {id, name, year, location})
(:Publication {id, title, year, venue, doi})
(:Project {id, name, repo_url, tech_stack})
// Relationship types
(:Person)-[:WORKS_AT {since, role}]->(:Company)
(:Person)-[:SPOKE_AT {talk_title, track}]->(:Conference)
(:Person)-[:COAUTHORED {position}]->(:Publication)
(:Person)-[:CONTRIBUTED_TO {commits, role}]->(:Project)
(:Person)-[:KNOWS {strength, source, since}]->(:Person)
(:Person)-[:COLLABORATED_WITH {project, duration}]->(:Person)Loading Data
// Load from CSV
LOAD CSV WITH HEADERS FROM 'file:///people.csv' AS row
MERGE (p:Person {id: row.id})
SET p.name = row.name,
p.email = row.email,
p.role = row.role;
// Load edges
LOAD CSV WITH HEADERS FROM 'file:///connections.csv' AS row
MATCH (a:Person {id: row.source})
MATCH (b:Person {id: row.target})
MERGE (a)-[r:KNOWS]->(b)
SET r.strength = toFloat(row.strength),
r.source = row.data_source;Centrality Algorithms (Graph Data Science Library)
// First, create a graph projection
CALL gds.graph.project(
'professional-network',
'Person',
{
KNOWS: {
orientation: 'UNDIRECTED',
properties: ['strength']
}
}
);
// Betweenness Centrality
CALL gds.betweenness.stream('professional-network')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC
LIMIT 20;
// Write back to nodes
CALL gds.betweenness.write('professional-network', {
writeProperty: 'betweenness'
});
// PageRank
CALL gds.pageRank.stream('professional-network', {
dampingFactor: 0.85,
maxIterations: 20
})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC
LIMIT 20;
// Eigenvector Centrality
CALL gds.eigenvector.stream('professional-network', {
maxIterations: 100
})
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC;
// Degree Centrality
CALL gds.degree.stream('professional-network')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC;
// Closeness Centrality
CALL gds.closeness.stream('professional-network')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC;Community Detection
// Louvain community detection
CALL gds.louvain.stream('professional-network')
YIELD nodeId, communityId
RETURN gds.util.asNode(nodeId).name AS name, communityId
ORDER BY communityId;
// Write communities to nodes
CALL gds.louvain.write('professional-network', {
writeProperty: 'community'
});
// Label Propagation
CALL gds.labelPropagation.stream('professional-network')
YIELD nodeId, communityId
RETURN communityId, collect(gds.util.asNode(nodeId).name) AS members
ORDER BY size(members) DESC;
// Weakly Connected Components
CALL gds.wcc.stream('professional-network')
YIELD nodeId, componentId
RETURN componentId, count(*) AS size
ORDER BY size DESC;Finding Bridges and Superconnectors
// Find people who bridge communities
MATCH (p:Person)
WHERE p.betweenness > 0.1
RETURN p.name, p.betweenness, p.community
ORDER BY p.betweenness DESC;
// Find people connected to multiple communities
MATCH (p:Person)-[:KNOWS]-(other:Person)
WITH p, collect(DISTINCT other.community) AS connected_communities
WHERE size(connected_communities) >= 3
RETURN p.name, connected_communities, size(connected_communities) AS bridge_score
ORDER BY bridge_score DESC;
// Find structural hole spanners
MATCH (p:Person)-[:KNOWS]-(a:Person)
MATCH (p)-[:KNOWS]-(b:Person)
WHERE a.community <> b.community AND NOT (a)-[:KNOWS]-(b)
WITH p, count(DISTINCT [a.community, b.community]) AS holes_spanned
WHERE holes_spanned > 5
RETURN p.name, holes_spanned
ORDER BY holes_spanned DESC;Gladwell Classification Query
// Classify nodes by Gladwell archetype
MATCH (p:Person)
WITH p,
percentileDisc(p.betweenness, 0.9) OVER () AS bc_threshold,
percentileDisc(p.degree, 0.9) OVER () AS dc_threshold,
percentileDisc(p.eigenvector, 0.9) OVER () AS ec_threshold
RETURN p.name,
CASE
WHEN p.betweenness >= bc_threshold AND p.degree >= dc_threshold
THEN 'connector'
WHEN p.eigenvector >= ec_threshold AND p.degree < dc_threshold
THEN 'maven'
WHEN p.degree >= dc_threshold
THEN 'salesman'
ELSE 'standard'
END AS gladwell_type,
p.betweenness, p.degree, p.eigenvector
ORDER BY p.betweenness DESC;Path Finding
// Shortest path between two people
MATCH path = shortestPath(
(a:Person {name: 'Alice'})-[:KNOWS*]-(b:Person {name: 'Bob'})
)
RETURN path, length(path) AS degrees_of_separation;
// All shortest paths
MATCH paths = allShortestPaths(
(a:Person {name: 'Alice'})-[:KNOWS*]-(b:Person {name: 'Bob'})
)
RETURN paths;
// Find connectors who can introduce you
MATCH (me:Person {name: 'Alice'})
MATCH (target:Person {name: 'Bob'})
MATCH path = shortestPath((me)-[:KNOWS*2..4]-(target))
WITH nodes(path) AS path_nodes
UNWIND range(1, size(path_nodes)-2) AS i
WITH path_nodes[i] AS connector
RETURN connector.name, connector.betweenness
ORDER BY connector.betweenness DESC;Multi-Source Network Fusion
// Create weighted relationships from multiple sources
MATCH (a:Person)-[r:KNOWS]-(b:Person)
WITH a, b, collect(r) AS rels
SET a.connection_weight = reduce(w = 0.0, r IN rels |
w + CASE r.source
WHEN 'coauthorship' THEN 1.0
WHEN 'conference' THEN 0.8
WHEN 'linkedin' THEN 0.5
WHEN 'github' THEN 0.6
ELSE 0.3
END
);---
Amazon Neptune
Neptune is AWS's managed graph database, compatible with Gremlin and SPARQL.
Gremlin Queries
// Betweenness-like analysis (Gremlin doesn't have native betweenness)
// Count paths through each node
g.V().hasLabel('Person')
.project('name', 'pathsThrough')
.by('name')
.by(
__.as('p')
.both('KNOWS').as('start')
.repeat(__.both('KNOWS').simplePath())
.until(__.loops().is(3))
.path()
.filter(__.unfold().is('p'))
.count()
)
.order().by('pathsThrough', desc)
.limit(20)
// Degree centrality
g.V().hasLabel('Person')
.project('name', 'degree')
.by('name')
.by(__.both('KNOWS').count())
.order().by('degree', desc)
.limit(20)
// Find bridges between communities
g.V().hasLabel('Person')
.where(
__.both('KNOWS').values('community').dedup().count().is(gte(3))
)
.project('name', 'communities')
.by('name')
.by(__.both('KNOWS').values('community').dedup().fold())---
TigerGraph
TigerGraph is optimized for deep-link analytics on large graphs.
GSQL Queries
-- Create schema
CREATE VERTEX Person (
PRIMARY_ID id STRING,
name STRING,
email STRING,
role STRING
)
CREATE DIRECTED EDGE KNOWS (
FROM Person,
TO Person,
strength FLOAT,
source STRING
)
-- Betweenness Centrality
CREATE QUERY betweenness_centrality() FOR GRAPH professional_network {
MapAccum<VERTEX<Person>, FLOAT> @@bc_scores;
Start = {Person.*};
// Run shortest paths from each node
FOREACH src IN Start DO
paths = SELECT t
FROM Start:s -(KNOWS:e)- Person:t
WHERE s == src
ACCUM @@bc_scores += (t -> 1.0);
END;
PRINT @@bc_scores;
}
-- PageRank
CREATE QUERY pagerank(FLOAT damping = 0.85, INT max_iter = 20)
FOR GRAPH professional_network {
MaxAccum<FLOAT> @pr_score = 1.0;
SumAccum<FLOAT> @new_score;
Start = {Person.*};
INT num_vertices = Start.size();
FOREACH i IN RANGE[1, max_iter] DO
Start = SELECT s
FROM Start:s -(KNOWS:e)- Person:t
ACCUM t.@new_score += s.@pr_score / s.outdegree("KNOWS")
POST-ACCUM
s.@pr_score = (1 - damping) / num_vertices + damping * s.@new_score,
s.@new_score = 0;
END;
PRINT Start[Start.@pr_score];
}
-- Find superconnectors
CREATE QUERY find_superconnectors(INT top_k = 20)
FOR GRAPH professional_network {
SumAccum<INT> @degree;
MaxAccum<FLOAT> @betweenness;
Start = {Person.*};
// Calculate degree
connected = SELECT s
FROM Start:s -(KNOWS:e)- Person:t
ACCUM s.@degree += 1;
// Return top by combined score
Result = SELECT s FROM Start:s
ORDER BY s.@degree DESC
LIMIT top_k;
PRINT Result;
}---
ArangoDB
ArangoDB is a multi-model database with graph capabilities.
AQL Queries
// Betweenness Centrality (using Pregel)
WITH "professional_network"
LET result = PREGEL_RUN("betweenness", "professional_network", {
maxIterations: 100,
resultField: "betweenness"
})
FOR doc IN Person
SORT doc.betweenness DESC
LIMIT 20
RETURN {name: doc.name, betweenness: doc.betweenness}
// PageRank
WITH "professional_network"
LET result = PREGEL_RUN("pagerank", "professional_network", {
maxIterations: 100,
dampingFactor: 0.85,
resultField: "pagerank"
})
FOR doc IN Person
SORT doc.pagerank DESC
LIMIT 20
RETURN {name: doc.name, pagerank: doc.pagerank}
// Shortest path
FOR v, e IN OUTBOUND SHORTEST_PATH
'Person/alice' TO 'Person/bob'
GRAPH 'professional_network'
RETURN v.name
// K-hop neighbors
FOR v, e, p IN 1..3 ANY 'Person/alice'
GRAPH 'professional_network'
RETURN DISTINCT v.name
// Find bridges
FOR person IN Person
LET neighbors = (
FOR v IN 1..1 ANY person GRAPH 'professional_network'
RETURN DISTINCT v.community
)
FILTER LENGTH(neighbors) >= 3
SORT LENGTH(neighbors) DESC
RETURN {
name: person.name,
communities_bridged: neighbors,
bridge_score: LENGTH(neighbors)
}---
DGraph
DGraph is a horizontally scalable graph database with GraphQL support.
DQL Queries
# Schema
type Person {
id: ID!
name: String! @index(term)
email: String @index(exact)
knows: [Person] @reverse
betweenness: Float
pagerank: Float
}
# Query high-centrality people
{
superconnectors(func: ge(betweenness, 0.1), orderdesc: betweenness, first: 20) {
name
betweenness
pagerank
knows {
name
}
}
}
# Shortest path
{
path as shortest(from: 0x1, to: 0x2) {
name
}
}
# Find all paths up to depth 3
{
var(func: eq(name, "Alice")) {
knows @recurse(depth: 3) {
uid
name
}
}
}---
Python Integration Patterns
Neo4j with Python
from neo4j import GraphDatabase
import pandas as pd
class ProfessionalNetworkAnalyzer:
def __init__(self, uri, user, password):
self.driver = GraphDatabase.driver(uri, auth=(user, password))
def close(self):
self.driver.close()
def get_superconnectors(self, limit=20):
with self.driver.session() as session:
result = session.run("""
CALL gds.betweenness.stream('professional-network')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC
LIMIT $limit
""", limit=limit)
return pd.DataFrame([dict(r) for r in result])
def find_path_to_target(self, source_name, target_name):
with self.driver.session() as session:
result = session.run("""
MATCH path = shortestPath(
(a:Person {name: $source})-[:KNOWS*]-(b:Person {name: $target})
)
RETURN [n IN nodes(path) | n.name] AS path,
length(path) AS degrees
""", source=source_name, target=target_name)
record = result.single()
if record:
return record['path'], record['degrees']
return None, None
def classify_by_gladwell(self):
with self.driver.session() as session:
result = session.run("""
MATCH (p:Person)
WHERE p.betweenness IS NOT NULL
WITH p,
percentileDisc(p.betweenness, 0.9) OVER () AS bc_thresh,
percentileDisc(p.degree, 0.9) OVER () AS dc_thresh,
percentileDisc(p.eigenvector, 0.9) OVER () AS ec_thresh
RETURN p.name AS name,
CASE
WHEN p.betweenness >= bc_thresh AND p.degree >= dc_thresh
THEN 'connector'
WHEN p.eigenvector >= ec_thresh AND p.degree < dc_thresh
THEN 'maven'
WHEN p.degree >= dc_thresh
THEN 'salesman'
ELSE 'standard'
END AS archetype
""")
return pd.DataFrame([dict(r) for r in result])
# Usage
analyzer = ProfessionalNetworkAnalyzer(
"bolt://localhost:7687",
"neo4j",
"password"
)
superconnectors = analyzer.get_superconnectors()
print(superconnectors.head(10))
path, degrees = analyzer.find_path_to_target("Alice", "Bob")
print(f"Path: {' -> '.join(path)} ({degrees} degrees)")
classifications = analyzer.classify_by_gladwell()
print(classifications[classifications['archetype'] == 'connector'])
analyzer.close()Bulk Loading Pattern
from neo4j import GraphDatabase
def bulk_load_network(driver, nodes_df, edges_df, batch_size=5000):
"""Efficiently load network data into Neo4j."""
with driver.session() as session:
# Load nodes in batches
for i in range(0, len(nodes_df), batch_size):
batch = nodes_df.iloc[i:i+batch_size].to_dict('records')
session.run("""
UNWIND $batch AS row
MERGE (p:Person {id: row.id})
SET p.name = row.name,
p.email = row.email,
p.role = row.role
""", batch=batch)
print(f"Loaded {min(i+batch_size, len(nodes_df))} nodes")
# Load edges in batches
for i in range(0, len(edges_df), batch_size):
batch = edges_df.iloc[i:i+batch_size].to_dict('records')
session.run("""
UNWIND $batch AS row
MATCH (a:Person {id: row.source})
MATCH (b:Person {id: row.target})
MERGE (a)-[r:KNOWS]->(b)
SET r.strength = row.strength,
r.source = row.data_source
""", batch=batch)
print(f"Loaded {min(i+batch_size, len(edges_df))} edges")
# Usage
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
bulk_load_network(driver, people_df, connections_df)
driver.close()Network Theory Reference
Theoretical foundations for professional network analysis.
Gladwellian Archetypes (The Tipping Point)
Connectors
Definition: People who know an extraordinary number across diverse social worlds.
Network Signature:
- Very high degree centrality (many connections)
- High betweenness centrality (bridge between clusters)
- Diverse cluster membership (not siloed)
- Power-law distribution: rare but disproportionately connected
Identification Signals:
- Multiple conference speaker lists across domains
- Co-authored with 5+ different institutions
- LinkedIn spans 10+ distinct industries
- Referenced by people who don't otherwise interact
HR Value: Best for referrals across domains, accelerate hiring in new markets
Mavens
Definition: Information specialists who accumulate knowledge and love sharing it.
Network Signature:
- High in-degree (people seek them out)
- Central in knowledge-sharing networks
- High PageRank (authoritative)
- Create content others reference
Identification Signals:
- Prolific writers/speakers on specific topics
- Run newsletters, podcasts, educational content
- Tagged in "who should I follow for X?" threads
- High engagement-to-follower ratio
HR Value: Know who's good at what, validate candidate quality
Salesmen
Definition: Persuaders with natural ability to get agreement.
Network Signature:
- High influence propagation
- Strong reciprocal relationships
- Central in deal-making networks
- Bridge between decision-makers
Identification Signals:
- Track record of successful introductions
- Referenced in "how I got my job" stories
- Active in investor/founder/hiring circles
- High response rate to outreach
HR Value: Close candidates on fence, navigate negotiations
Network Centrality Metrics
Betweenness Centrality
Formula: BC(v) = Σ (σst(v) / σst) for all s,t pairs Meaning: How often node lies on shortest paths between others HR Interpretation: "Gatekeeper" - controls information flow
import networkx as nx
bc = nx.betweenness_centrality(G)When it matters: Finding people who can introduce to unreachable networks
Degree Centrality
Formula: DC(v) = degree(v) / (n-1) Meaning: Raw count of connections, normalized HR Interpretation: "Popular" - knows many directly
When it matters: Maximizing referral reach, event organizing
Eigenvector Centrality
Formula: Recursive: centrality depends on neighbors' centrality Meaning: Connected to other well-connected people HR Interpretation: "Influential" - quality over quantity
When it matters: Access to power, rising stars, influence hierarchies
Closeness Centrality
Formula: CC(v) = (n-1) / Σ d(v,u) Meaning: Average shortest path to all others HR Interpretation: "Accessible" - can reach anyone quickly
When it matters: Information spreading, optimal hire positioning
PageRank
Formula: Iterative probability of random walk Meaning: Weighted by quality of incoming connections HR Interpretation: "Authoritative" - endorsed by important others
When it matters: Thought leaders vs merely prolific
Structural Holes Theory (Burt)
Core Insight: Advantage comes from bridging disconnected groups, not dense cluster connections.
Key Metrics:
- Constraint: How concentrated in one group
- Effective Size: Redundancy-adjusted network size
- Hierarchy: Constraint concentration across contacts
constraint = nx.constraint(G)
low_constraint = {k: v for k, v in constraint.items() if v < 0.5}
# These are broker opportunitiesHR Applications:
- Candidates bridging groups bring diverse information
- Mix connectors and specialists in teams
- Target structural holes, not cluster centers