Skip to main content Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill openalx-bibliometricsThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository
Related occupations SOC
Based on SOC occupation classification
name openalx-bibliometrics description Bibliometric analysis using the OpenAlex API covering co-authorship networks, citation analysis, h-index, and research trend mapping. tags ["bibliometrics","openalx","citation-analysis","research-networks","scientometrics"] version 1.0.0 authors ["@xjtulyc"] license MIT platforms ["claude-code","codex","gemini-cli","cursor"] dependencies {"python":["requests>=2.31","pandas>=2.0","numpy>=1.24","networkx>=3.2","matplotlib>=3.7","scipy>=1.11"]} last_updated 2026-03-17 status stable
OpenAlex Bibliometrics
When to Use This Skill
Use this skill when you need to:
Retrieve publication metadata from OpenAlex API (open scholarly metadata)
Compute h-index, g-index, i10-index for authors or institutions
Build and analyze co-authorship networks
Map citation networks and identify key papers
Track research trends using publication counts and concept co-occurrence
Compare institutional research output and collaboration patterns
Perform systematic mapping studies without manual database downloads
Trigger keywords : OpenAlex, bibliometrics, h-index, citation count, co-authorship network, research output, institutional collaboration, VOSviewer, bibliographic coupling, co-citation analysis, journal impact, Altmetrics, systematic mapping, PRISMA, research front, scholarly network, scientometrics.
Background & Key Concepts
h-index (Hirsch 2005)
The h-index is the maximum $h$ such that the researcher has at least $h$ papers with $\geq h$ citations each:
$$h = \max{h : |{p : c_p \geq h}| \geq h}$$
g-index (Egghe 2006)
The g-index is the largest $g$ such that the top $g$ papers together have at least $g^2$ citations:
$$g = \max{g : \sum_{p=1}^{g} c_p \geq g^2}$$
Bibliographic Coupling
Two papers are bibliographically coupled if they share common references. Coupling strength between papers $A$ and $B$:
$$\text{BC}(A,B) = \frac{|R_A \cap R_B|}{|R_A \cup R_B|}$$
(Jaccard similarity of reference sets).
Co-citation Strength
Two papers are co-cited when both appear in the reference list of a third paper. Co-citation strength:
$$\text{CC}(A,B) = \frac{|\text{papers citing both } A \text{ and } B|}{\sqrt{c_A \cdot c_B}}$$
(Salton's cosine normalization).
Bradford's Law of Scattering
A small core of journals contributes a disproportionate fraction of articles on a subject. If journals are ranked by productivity:
$$\log r = a + b \log n$$
The Bradford multiplier $b$ describes how quickly productivity falls across zones.
Environment Setup
pip install requests>=2.31 pandas>=2.0 numpy>=1.24 networkx>=3.2 \
matplotlib>=3.7 scipy>=1.11
import requests
import pandas as pd
import numpy as np
import networkx nx
matplotlib.pyplot plt
( )
as
import
as
print
"Bibliometrics environment ready"
Core Workflow
Step 1: Retrieve Publications from OpenAlex API import requests
import pandas as pd
import numpy as np
import time
import os
OPENALEX_EMAIL = os.getenv("OPENALEX_EMAIL" , "" )
def openalex_works (query, max_results=200 , email=OPENALEX_EMAIL ):
"""Retrieve works from OpenAlex API with pagination.
Args:
query: search string
max_results: maximum number of results to fetch
email: optional email for polite pool (higher rate limit)
Returns:
list of work dicts
"""
base_url = "https://api.openalex.org/works"
params = {
"search" : query,
"per-page" : min (max_results, 200 ),
"cursor" : "*" ,
"select" : ("id,title,publication_year,cited_by_count,"
"authorships,concepts,referenced_works,doi" ),
}
if email:
params["mailto" ] = email
all_works = []
fetched = 0
while fetched < max_results:
try :
response = requests.get(base_url, params=params, timeout=30 )
response.raise_for_status()
data = response.json()
except Exception as e:
print (f"API call failed: {e} " )
break
results = data.get("results" , [])
if not results:
break
all_works.extend(results)
fetched += len (results)
next_cursor = data.get("meta" , {}).get("next_cursor" )
if not next_cursor or fetched >= max_results:
break
params["cursor" ] = next_cursor
time.sleep(0.1 )
return all_works[:max_results]
def works_to_dataframe (works ):
"""Flatten OpenAlex work objects to a tidy DataFrame."""
rows = []
for w in works:
authors = [a.get("author" , {}).get("display_name" , "" )
for a in w.get("authorships" , [])]
institutions = list ({
inst.get("institution" , {}).get("display_name" , "Unknown" )
for a in w.get("authorships" , [])
for inst in a.get("institutions" , [])
})
top_concepts = sorted (w.get("concepts" , []),
key=lambda c: c.get("score" , 0 ), reverse=True )[:3 ]
rows.append({
"id" : w.get("id" , "" ),
"title" : w.get("title" , "" ),
"year" : w.get("publication_year" ),
"citations" : w.get("cited_by_count" , 0 ),
"doi" : w.get("doi" , "" ),
"n_authors" : len (authors),
"authors" : "; " .join(authors[:5 ]),
"institutions" : "; " .join(institutions[:3 ]),
"concepts" : "; " .join(c["display_name" ] for c in top_concepts),
"n_references" : len (w.get("referenced_works" , [])),
})
return pd.DataFrame(rows)
try :
works = openalex_works("machine learning climate science" , max_results=100 )
if len (works) < 5 :
raise ValueError("Too few results returned" )
df = works_to_dataframe(works)
print (f"Fetched {len (df)} papers from OpenAlex" )
DATA_SOURCE = "API"
except Exception as e:
print (f"API unavailable ({e} ), using synthetic data" )
np.random.seed(42 )
n_papers = 100
years = np.random.randint(2010 , 2024 , n_papers)
citations = np.random.zipf(1.8 , n_papers) * 5
n_authors = np.random.randint(1 , 12 , n_papers)
df = pd.DataFrame({
"id" : [f"W{i:06d} " for i in range (n_papers)],
"title" : [f"Paper on ML and Climate Science #{i} " for i in range (n_papers)],
"year" : years,
"citations" : citations,
"n_authors" : n_authors,
"authors" : [f"Author_{i%30 } ; Author_{(i+1 )%30 } " for i in range (n_papers)],
"institutions" : [f"Univ_{i%10 } " for i in range (n_papers)],
"concepts" : "Machine Learning; Climate Science" ,
"n_references" : np.random.randint(20 , 80 , n_papers),
})
DATA_SOURCE = "Synthetic"
print (f"\nData source: {DATA_SOURCE} " )
print (df[["year" , "citations" , "n_authors" ]].describe().round (1 ))
Step 2: Bibliometric Indicators import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
def h_index (citations ):
"""Compute h-index from citation counts."""
c = np.sort(citations)[::-1 ]
h = 0
for i, ci in enumerate (c):
if ci >= i + 1 :
h = i + 1
else :
break
return h
def g_index (citations ):
"""Compute g-index from citation counts."""
c = np.sort(citations)[::-1 ]
cumsum = np.cumsum(c)
g = 0
for i in range (len (c)):
if cumsum[i] >= (i + 1 )**2 :
g = i + 1
return g
def i10_index (citations ):
"""Count papers with >= 10 citations."""
return int ((np.array(citations) >= 10 ).sum ())
def compute_indices (citation_series ):
"""Compute all standard bibliometric indices."""
c = citation_series.values
return {
"n_papers" : len (c),
"total_citations" : int (c.sum ()),
"mean_citations" : float (c.mean()),
"median_citations" : float (np.median(c)),
"h_index" : h_index(c),
"g_index" : g_index(c),
"i10_index" : i10_index(c),
"max_citations" : int (c.max ()),
}
metrics = compute_indices(df["citations" ])
print ("=== Bibliometric Indices ===" )
for k, v in metrics.items():
print (f" {k} : {v:.1 f} " if isinstance (v, float ) else f" {k} : {v} " )
annual_counts = df.groupby("year" ).agg(
n_papers=("id" , "count" ),
total_cit=("citations" , "sum" ),
mean_cit=("citations" , "mean" )
).reset_index()
from scipy.stats import linregress
slope, intercept, r, p, se = linregress(annual_counts["year" ], annual_counts["n_papers" ])
print (f"\nPublication growth trend: {slope:.2 f} papers/year (p={p:.3 f} )" )
np.random.seed(42 )
journal_pool = [f"Journal_{i:02d} " for i in range (20 )]
journal_weights = np.exp(-0.3 * np.arange(20 ))
journal_weights /= journal_weights.sum ()
df["journal" ] = np.random.choice(journal_pool, len (df), p=journal_weights)
journal_counts = df.groupby("journal" ).size().reset_index(name="n_papers" )
journal_counts = journal_counts.sort_values("n_papers" , ascending=False )
journal_counts["rank" ] = range (1 , len (journal_counts) + 1 )
journal_counts["cumsum" ] = journal_counts["n_papers" ].cumsum()
total_papers = len (df)
one_third = total_papers // 3
zone1 = journal_counts[journal_counts["cumsum" ] <= one_third]
zone2 = journal_counts[(journal_counts["cumsum" ] > one_third) &
(journal_counts["cumsum" ] <= 2 * one_third)]
zone3 = journal_counts[journal_counts["cumsum" ] > 2 * one_third]
print (f"\nBradford's Law: Core={len (zone1)} , Zone2={len (zone2)} , Zone3={len (zone3)} journals" )
fig, axes = plt.subplots(2 , 2 , figsize=(12 , 10 ))
axes[0 , 0 ].hist(df["citations" ] + 1 , bins=30 , log=True , color="steelblue" , edgecolor="k" )
h_val = metrics["h_index" ]
axes[0 , 0 ].axvline(h_val, color="red" , ls="--" , label=f"h-index = {h_val} " )
axes[0 , 0 ].set_xlabel("Citations + 1 (log scale)" )
axes[0 , 0 ].set_ylabel("Frequency (log)" )
axes[0 , 0 ].set_title("Citation Distribution" )
axes[0 , 0 ].legend()
axes[0 , 1 ].bar(annual_counts["year" ], annual_counts["n_papers" ],
color="steelblue" , alpha=0.7 )
trend_y = intercept + slope * annual_counts["year" ]
axes[0 , 1 ].plot(annual_counts["year" ], trend_y, "r-" , lw=2 , label=f"Trend ({slope:+.2 f} /yr)" )
axes[0 , 1 ].set_xlabel("Year" ); axes[0 , 1 ].set_ylabel("Publications" )
axes[0 , 1 ].set_title("Annual Publication Count" )
axes[0 , 1 ].legend()
sorted_cit = np.sort(df["citations" ].values)[::-1 ]
x_papers = np.arange(1 , len (sorted_cit) + 1 )
axes[1 , 0 ].bar(x_papers[:50 ], sorted_cit[:50 ], color="steelblue" , alpha=0.8 )
axes[1 , 0 ].plot([0 , 50 ], [0 , 50 ], "r-" , lw=1.5 , label="y=x line" )
axes[1 , 0 ].axvline(h_val, color="orange" , ls="--" , label=f"h={h_val} " )
axes[1 , 0 ].set_xlabel("Paper Rank" ); axes[1 , 0 ].set_ylabel("Citations" )
axes[1 , 0 ].set_title("Hirsch Plot (top 50 papers)" )
axes[1 , 0 ].legend()
axes[1 , 1 ].plot(np.log(journal_counts["rank" ]),
journal_counts["n_papers" ], "o-" , color="steelblue" , ms=4 )
axes[1 , 1 ].set_xlabel("ln(Journal Rank)" )
axes[1 , 1 ].set_ylabel("Papers" )
axes[1 , 1 ].set_title("Bradford's Law (Journal Productivity)" )
for zone_label, cutoff in [("Zone 1" , len (zone1)), ("Zone 2" , len (zone1)+len (zone2))]:
axes[1 , 1 ].axvline(np.log(cutoff + 1 ), color="red" , ls="--" , alpha=0.5 ,
label=zone_label)
axes[1 , 1 ].legend()
plt.tight_layout()
plt.savefig("bibliometric_analysis.png" , dpi=150 , bbox_inches="tight" )
plt.close()
print ("\nFigure saved: bibliometric_analysis.png" )
Step 3: Co-authorship Network Analysis import networkx as nx
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from collections import Counter
def build_coauthorship_network (df, author_col="authors" , sep="; " ):
"""Build weighted co-authorship graph.
Args:
df: DataFrame with author column (semicolon-separated)
author_col: column name
sep: separator between authors
Returns:
G: NetworkX weighted graph
"""
G = nx.Graph()
edge_weights = Counter()
for _, row in df.iterrows():
authors = [a.strip() for a in str (row[author_col]).split(sep) if a.strip()]
for a in authors:
if G.has_node(a):
G.nodes[a]["papers" ] = G.nodes[a].get("papers" , 0 ) + 1
else :
G.add_node(a, papers=1 )
for i in range (len (authors)):
for j in range (i + 1 , len (authors)):
key = tuple (sorted ([authors[i], authors[j]]))
edge_weights[key] += 1
for (a, b), w in edge_weights.items():
G.add_edge(a, b, weight=w)
return G
G = build_coauthorship_network(df)
print ("=== Co-authorship Network ===" )
print (f"Nodes (authors): {G.number_of_nodes()} " )
print (f"Edges (collaborations): {G.number_of_edges()} " )
print (f"Average degree: {sum (dict (G.degree()).values()) / G.number_of_nodes():.2 f} " )
print (f"Density: {nx.density(G):.4 f} " )
components = list (nx.connected_components(G))
largest_cc = max (components, key=len )
print (f"Connected components: {len (components)} " )
print (f"Largest component: {len (largest_cc)} authors "
f"({len (largest_cc)/G.number_of_nodes()*100 :.1 f} %)" )
G_lcc = G.subgraph(largest_cc).copy()
degree_cent = nx.degree_centrality(G_lcc)
betw_cent = nx.betweenness_centrality(G_lcc, weight="weight" )
eigv_cent = nx.eigenvector_centrality(G_lcc, weight="weight" , max_iter=200 )
def top_n (centrality_dict, n=5 ):
return sorted (centrality_dict.items(), key=lambda x: x[1 ], reverse=True )[:n]
print ("\nTop 5 by degree centrality:" )
for a, c in top_n(degree_cent): print (f" {a} : {c:.3 f} " )
print ("\nTop 5 by betweenness centrality:" )
for a, c in top_n(betw_cent): print (f" {a} : {c:.3 f} " )
communities = nx.community.greedy_modularity_communities(G_lcc, weight="weight" )
modularity = nx.community.modularity(G_lcc, communities, weight="weight" )
print (f"\nLouvain communities: {len (communities)} , Modularity = {modularity:.4 f} " )
fig, axes = plt.subplots(1 , 2 , figsize=(14 , 7 ))
pos = nx.spring_layout(G_lcc, seed=42 , k=1.5 )
node_sizes = [G_lcc.nodes[n].get("papers" , 1 ) * 50 for n in G_lcc.nodes()]
community_map = {}
for c_idx, comm in enumerate (communities):
for author in comm:
community_map[author] = c_idx
node_colors = [community_map.get(n, 0 ) for n in G_lcc.nodes()]
nx.draw_networkx_nodes(G_lcc, pos, ax=axes[0 ], node_size=node_sizes,
node_color=node_colors, cmap=plt.cm.Set1, alpha=0.8 )
edges = G_lcc.edges(data=True )
weights = [e[2 ].get("weight" , 1 ) for e in edges]
nx.draw_networkx_edges(G_lcc, pos, ax=axes[0 ], alpha=0.2 ,
width=[w * 0.5 for w in weights])
if len (G_lcc) < 30 :
nx.draw_networkx_labels(G_lcc, pos, ax=axes[0 ], font_size=6 )
axes[0 ].set_title(f"Co-authorship Network\n({G_lcc.number_of_nodes()} nodes, "
f"{len (communities)} communities)" )
axes[0 ].axis("off" )
degrees = [d for _, d in G.degree()]
axes[1 ].hist(degrees, bins=20 , color="steelblue" , edgecolor="black" , log=True )
axes[1 ].set_xlabel("Degree (collaborators)" )
axes[1 ].set_ylabel("Frequency (log)" )
axes[1 ].set_title("Degree Distribution of Co-authorship Network" )
plt.tight_layout()
plt.savefig("coauthorship_network.png" , dpi=150 , bbox_inches="tight" )
plt.close()
print ("\nFigure saved: coauthorship_network.png" )
Advanced Usage
Concept Co-occurrence Analysis import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from collections import Counter
from itertools import combinations
def concept_cooccurrence_matrix (df, concept_col="concepts" , sep="; " , top_n=15 ):
"""Build concept co-occurrence matrix.
Args:
df: DataFrame with concept column
top_n: number of most frequent concepts to include
Returns:
co_matrix (DataFrame), top_concepts (list)
"""
all_concepts = []
concept_lists = []
for _, row in df.iterrows():
concepts = [c.strip() for c in str (row[concept_col]).split(sep) if c.strip()]
concept_lists.append(concepts)
all_concepts.extend(concepts)
freq = Counter(all_concepts)
top_concepts = [c for c, _ in freq.most_common(top_n)]
co_matrix = pd.DataFrame(0 , index=top_concepts, columns=top_concepts)
for concepts in concept_lists:
filtered = [c for c in concepts if c in top_concepts]
for c1, c2 in combinations(filtered, 2 ):
co_matrix.loc[c1, c2] += 1
co_matrix.loc[c2, c1] += 1
for c in filtered:
co_matrix.loc[c, c] += 1
return co_matrix, top_concepts
co_matrix, top_concepts = concept_cooccurrence_matrix(df, top_n=10 )
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8 , 7 ))
im = ax.imshow(co_matrix.values, cmap="Blues" , aspect="auto" )
ax.set_xticks(range (len (top_concepts)))
ax.set_xticklabels(top_concepts, rotation=45 , ha="right" , fontsize=8 )
ax.set_yticks(range (len (top_concepts)))
ax.set_yticklabels(top_concepts, fontsize=8 )
ax.set_title("Concept Co-occurrence Matrix" )
plt.colorbar(im, ax=ax, label="Co-occurrence count" )
plt.tight_layout()
plt.savefig("concept_cooccurrence.png" , dpi=150 , bbox_inches="tight" )
plt.close()
print ("Figure saved: concept_cooccurrence.png" )
Institutional Collaboration Network import networkx as nx
import pandas as pd
from collections import Counter
from itertools import combinations
def institution_network (df, inst_col="institutions" , sep="; " ):
"""Build inter-institutional collaboration network."""
G = nx.Graph()
edge_w = Counter()
for _, row in df.iterrows():
insts = [i.strip() for i in str (row[inst_col]).split(sep) if i.strip()]
insts = list (set (insts))
for inst in insts:
if inst not in G:
G.add_node(inst, papers=0 )
G.nodes[inst]["papers" ] = G.nodes[inst].get("papers" , 0 ) + 1
for i, j in combinations(insts, 2 ):
edge_w[tuple (sorted ([i, j]))] += 1
for (a, b), w in edge_w.items():
G.add_edge(a, b, weight=w)
return G
G_inst = institution_network(df)
print ("\n=== Institutional Collaboration Network ===" )
print (f"Institutions: {G_inst.number_of_nodes()} " )
print (f"Collaborations: {G_inst.number_of_edges()} " )
top_insts = sorted (G_inst.nodes(data=True ),
key=lambda x: x[1 ].get("papers" , 0 ), reverse=True )[:5 ]
for inst, attrs in top_insts:
print (f" {inst} : {attrs.get('papers' , 0 )} papers, "
f"degree={G_inst.degree(inst)} " )
Troubleshooting Problem Cause Fix OpenAlex API 429 rate limit Too many requests Add time.sleep(0.1) between requests; use email in params Empty API results Query too specific Broaden search terms; check OpenAlex concept IDs h-index lower than expected Incomplete citation counts OpenAlex may lag Scopus/WoS; check cited_by_count Network too large to visualize Many authors Filter to largest component; use degree threshold Self-citations inflate h-index All citations included Filter out self-citations using author IDs Duplicate authors (name variations) Name disambiguation Use OpenAlex author IDs, not display names
External Resources
Hirsch, J. E. (2005). An index to quantify an individual's scientific research output. PNAS , 102(46).
Egghe, L. (2006). Theory and practise of the g-index. Scientometrics , 69(1).
OpenAlex documentation
PyAlex Python wrapper
VOSviewer — visualization of bibliometric networks
Bradford, S. C. (1934). Sources of information on specific subjects. Engineering , 137, 85-86.
Examples
Example 1: Compare Two Research Fields import numpy as np
import pandas as pd
def compare_fields (field_a_papers, field_b_papers, field_a_name, field_b_name ):
"""Compare bibliometric profiles of two research fields."""
metrics_a = compute_indices_simple(field_a_papers)
metrics_b = compute_indices_simple(field_b_papers)
comparison = pd.DataFrame({
field_a_name: metrics_a,
field_b_name: metrics_b
})
return comparison
def compute_indices_simple (citations ):
"""Compute bibliometric indices from citation array."""
c = np.sort(citations)[::-1 ]
h = sum (1 for i, ci in enumerate (c) if ci >= i + 1 )
cumsum = np.cumsum(c)
g = sum (1 for i in range (len (c)) if cumsum[i] >= (i + 1 )**2 )
return {
"n_papers" : len (c),
"total_citations" : int (c.sum ()),
"mean_citations" : float (c.mean()),
"h_index" : h,
"g_index" : g,
"i10_index" : int ((c >= 10 ).sum ()),
}
np.random.seed(42 )
citations_a = np.random.zipf(1.6 , 150 ) * 8
citations_b = np.random.zipf(1.4 , 80 ) * 12
comparison = compare_fields(citations_a, citations_b, "Field_A" , "Field_B" )
print (comparison)
Example 2: Research Trend Forecasting import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
np.random.seed(42 )
years = np.arange(2000 , 2024 )
pub_counts = (50 * np.exp(0.12 * (years - 2000 ))
+ np.random.normal(0 , 10 , len (years))).astype(int )
t = years - 2000
coeff = np.polyfit(t, pub_counts, deg=2 )
poly = np.poly1d(coeff)
forecast_years = np.arange(2024 , 2028 )
forecast_t = forecast_years - 2000
forecast = poly(forecast_t)
print ("=== Research Trend Forecast ===" )
print (f"Trend equation: {coeff[0 ]:.2 f} t² + {coeff[1 ]:.2 f} t + {coeff[2 ]:.2 f} " )
for yr, fc in zip (forecast_years, forecast):
print (f" {yr} : {int (fc)} papers (projected)" )