| name | patent-analysis |
| description | Patent landscape analysis with IPC classification, citation networks, technology emergence detection, and inventor collaboration mapping. |
| tags | ["patent-analysis","intellectual-property","technology-forecasting","citation-network","ipc-classification"] |
| version | 1.0.0 |
| authors | ["@xjtulyc"] |
| license | MIT |
| platforms | ["claude-code","codex","gemini-cli","cursor"] |
| dependencies | {"python":["pandas>=2.0","numpy>=1.24","networkx>=3.2","scipy>=1.11","scikit-learn>=1.3","matplotlib>=3.7","requests>=2.31"]} |
| last_updated | 2026-03-17 |
| status | stable |
Patent Landscape Analysis
When to Use This Skill
Use this skill when you need to:
- Map technology landscapes using IPC/CPC patent classifications
- Analyze patent citation networks and identify pioneering inventions
- Measure technology emergence and diffusion using S-curve fitting
- Track inventor collaboration networks and knowledge transfer
- Compute patent quality indicators (forward citations, generality, originality)
- Identify white spaces and technology opportunities
- Benchmark R&D portfolios across assignees and countries
Trigger keywords: patent analysis, IPC classification, CPC, patent citations, forward citations, technology landscape, S-curve diffusion, inventor network, assignee analysis, patent portfolio, technology whitespace, knowledge spillover, patent value, patent family, PATSTAT, USPTO, EPO, patent claims, prior art, citation lag, technology cycle time.
Background & Key Concepts
Patent Quality Indicators (Trajtenberg 1990)
Generality: Measures how broadly a patent's technology is used across technology classes:
$$G_i = 1 - \sum_k s_{ik}^2$$
where $s_{ik}$ is the share of forward citations to patent $i$ from technology class $k$ (Herfindahl-type measure; higher = more general technology).
Originality: Same formula applied to backward citations (referenced patent classes):
$$O_i = 1 - \sum_k r_{ik}^2$$
Technology Cycle Time (TCT)
Mean backward citation lag for a patent application year cohort:
$$\text{TCT} = \frac{1}{|B_i|} \sum_{j \in B_i} (\text{year}_i - \text{year}_j)$$
Shorter TCT = faster-moving technology field.
S-Curve Diffusion (Bass Model)
Cumulative adoptions $N(t)$ follow a logistic-like curve:
$$\frac{dN}{dt} = (p + q \cdot \frac{N}{M})(M - N)$$
where $M$ is market potential, $p$ is coefficient of innovation, $q$ coefficient of imitation. Integrated: $N(t) = M \cdot \frac{1 - e^{-(p+q)t}}{1 + \frac{q}{p}e^{-(p+q)t}}$.
Patent Value Distribution
Patent values follow a highly skewed distribution (Lanjouw & Schankerman 2004). The top 1% of patents account for roughly 25% of total value (approximated by forward citations). Value proxy:
$$v_i \approx \exp(\alpha \cdot \text{fwd_cit}_i)$$
Environment Setup
pip install pandas>=2.0 numpy>=1.24 networkx>=3.2 scipy>=1.11 \
scikit-learn>=1.3 matplotlib>=3.7 requests>=2.31
import pandas as pd
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
print("Patent analysis environment ready")
Core Workflow
Step 1: Patent Landscape Mapping with IPC Classification
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import chi2_contingency
np.random.seed(42)
n_patents = 2000
ipc_sections = list("ABCDEFGH")
ipc_classes = {
"A": ["A01", "A23", "A61", "A63"],
"B": ["B01", "B23", "B60", "B82"],
"C": ["C07", "C08", "C12", "C22"],
"G": ["G01", "G06", "G09", "G16"],
"H": ["H01", "H04", "H05"],
"D": ["D01", "D06"],
"E": ["E02", "E04"],
"F": ["F01", "F16", "F28"],
}
all_classes = [cls for classes in ipc_classes.values() for cls in classes]
year_range = np.arange(2000, 2023)
class_trends = {}
for cls in all_classes:
if cls == "G06":
class_trends[cls] = np.exp(0.15 * (year_range - 2000)) + np.random.normal(0, 2, len(year_range))
elif cls in ["A61", "C12"]:
class_trends[cls] = 50 + 3 * (year_range - 2000) + np.random.normal(0, 5, len(year_range))
else:
class_trends[cls] = 20 + np.random.normal(0, 3, len(year_range))
class_trends[cls] = np.maximum(class_trends[cls], 1)
years = np.random.choice(year_range, n_patents)
classes = []
for yr in years:
t = int(yr - 2000)
weights = np.array([class_trends[cls][t] for cls in all_classes])
weights = weights / weights.sum()
classes.append(np.random.choice(all_classes, p=weights))
forward_citations = np.random.negative_binomial(1, 0.15, n_patents)
backward_citations = np.random.randint(2, 25, n_patents)
n_inventors = np.random.randint(1, 8, n_patents)
n_claims = np.random.randint(1, 30, n_patents)
country = np.random.choice(["US", "CN", "DE", "JP", "KR", "FR", "GB"],
n_patents, p=[0.35, 0.25, 0.10, 0.12, 0.08, 0.05, 0.05])
df = pd.DataFrame({
"patent_id": [f"P{i:06d}" for i in range(n_patents)],
"year": years,
"ipc_class": classes,
"ipc_section": [cls[0] for cls in classes],
"forward_citations": forward_citations,
"backward_citations": backward_citations,
"n_inventors": n_inventors,
"n_claims": n_claims,
"country": country,
})
def compute_generality(df):
"""Compute generality index for each patent."""
gen = []
for _, row in df.iterrows():
n_fwd = max(row["forward_citations"], 1)
shares = np.random.dirichlet(np.ones(5))
gen.append(1 - (shares**2).sum())
return np.array(gen)
df["generality"] = compute_generality(df)
df["originality"] = df["backward_citations"].apply(
lambda n: 1 - (np.random.dirichlet(np.ones(max(n, 1)))**2).sum()
)
df["tct"] = df["backward_citations"].apply(
lambda n: np.random.uniform(3, 15) if n > 0 else np.nan
)
landscape = df.groupby("ipc_class").agg(
n_patents=("patent_id", "count"),
mean_fwd_cit=("forward_citations", "mean"),
mean_generality=("generality", "mean"),
mean_tct=("tct", "mean"),
).sort_values("n_patents", ascending=False)
print("=== Patent Landscape by IPC Class ===")
print(landscape.round(2))
top_classes = landscape.head(5).index.tolist()
annual_counts = df[df["ipc_class"].isin(top_classes)].groupby(
["year", "ipc_class"]).size().unstack(fill_value=0)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for cls in top_classes:
if cls in annual_counts.columns:
axes[0].plot(annual_counts.index, annual_counts[cls], marker="o",
ms=3, label=cls)
axes[0].set_xlabel("Year"); axes[0].set_ylabel("Patent Filings")
axes[0].set_title("Technology Growth Trends (Top IPC Classes)")
axes[0].legend(fontsize=8)
country_ipc = df.groupby(["country", "ipc_section"]).size().reset_index(name="count")
pivot_ci = country_ipc.pivot(index="country", columns="ipc_section",
values="count").fillna(0)
im = axes[1].imshow(pivot_ci.values, cmap="YlOrRd", aspect="auto")
axes[1].set_xticks(range(len(pivot_ci.columns)))
axes[1].set_xticklabels(pivot_ci.columns)
axes[1].set_yticks(range(len(pivot_ci.index)))
axes[1].set_yticklabels(pivot_ci.index)
axes[1].set_title("Patent Counts by Country × IPC Section")
plt.colorbar(im, ax=axes[1])
plt.tight_layout()
plt.savefig("patent_landscape.png", dpi=150, bbox_inches="tight")
plt.close()
print("\nFigure saved: patent_landscape.png")
Step 2: Patent Citation Network Analysis
import networkx as nx
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(42)
n_p = 300
patent_ids = [f"P{i:06d}" for i in range(n_p)]
patent_years = np.random.randint(2000, 2023, n_p)
citations = []
for i in range(n_p):
n_cites = np.random.randint(3, 9)
older = [j for j in range(n_p) if patent_years[j] < patent_years[i]]
if older:
cited = np.random.choice(older, size=min(n_cites, len(older)),
replace=False)
for c in cited:
citations.append((patent_ids[i], patent_ids[c]))
G = nx.DiGraph()
for pid, yr in zip(patent_ids, patent_years):
G.add_node(pid, year=yr)
for citing, cited in citations:
G.add_edge(citing, cited)
print("=== Citation Network ===")
print(f"Nodes: {G.number_of_nodes()}")
print(f"Edges: {G.number_of_edges()}")
print(f"Average in-degree (fwd citations): {sum(dict(G.in_degree()).values()) / n_p:.2f}")
in_degree = dict(G.in_degree())
pagerank = nx.pagerank(G, alpha=0.85)
top_cited = sorted(in_degree.items(), key=lambda x: x[1], reverse=True)[:10]
print("\nTop 10 most-cited patents:")
for pid, cit in top_cited:
print(f" {pid} (year {patent_years[patent_ids.index(pid)]}): {cit} citations, "
f"PageRank={pagerank[pid]:.4f}")
citation_lags = []
for citing, cited in G.edges():
i = patent_ids.index(citing)
j = patent_ids.index(cited)
lag = patent_years[i] - patent_years[j]
if lag > 0:
citation_lags.append(lag)
mean_tct = np.mean(citation_lags)
median_tct = np.median(citation_lags)
print(f"\nTechnology Cycle Time: mean={mean_tct:.1f}y, median={median_tct:.1f}y")
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
wcc = max(nx.weakly_connected_components(G), key=len)
G_wcc = G.subgraph(wcc).copy()
pos = nx.spring_layout(G_wcc, seed=42, k=0.8)
node_sizes = [max(G_wcc.in_degree(n) * 30, 20) for n in G_wcc.nodes()]
node_colors = [patent_years[patent_ids.index(n)] if n in patent_ids else 2010
for n in G_wcc.nodes()]
nx.draw_networkx_nodes(G_wcc, pos, ax=axes[0], node_size=node_sizes,
node_color=node_colors, cmap=plt.cm.viridis, alpha=0.8)
nx.draw_networkx_edges(G_wcc, pos, ax=axes[0], alpha=0.15, arrows=True,
arrowsize=5, edge_color="gray")
axes[0].set_title(f"Patent Citation Network (n={len(G_wcc)} patents)")
axes[0].axis("off")
axes[1].hist(citation_lags, bins=25, color="steelblue", edgecolor="black")
axes[1].axvline(mean_tct, color="red", ls="--", label=f"Mean TCT={mean_tct:.1f}y")
axes[1].set_xlabel("Citation Lag (years)"); axes[1].set_ylabel("Frequency")
axes[1].set_title("Technology Cycle Time Distribution")
axes[1].legend()
plt.tight_layout()
plt.savefig("patent_citation_network.png", dpi=150, bbox_inches="tight")
plt.close()
print("\nFigure saved: patent_citation_network.png")
Step 3: S-Curve Technology Emergence Detection
import numpy as np
import pandas as pd
import scipy.optimize as opt
import matplotlib.pyplot as plt
def bass_model(t, M, p, q):
"""Bass model cumulative adoptions.
Args:
t: time array (starting from 0)
M: market potential
p: innovation coefficient
q: imitation coefficient
Returns:
N(t): cumulative adoption
"""
exp_term = np.exp(-(p + q) * t)
return M * (1 - exp_term) / (1 + (q / p) * exp_term)
def fit_technology_scurve(annual_counts, technology_name):
"""Fit S-curve to annual patent filing data.
Args:
annual_counts: pd.Series with year as index, count as values
technology_name: string label
Returns:
dict with fit parameters and forecast
"""
years = annual_counts.index.values
t = (years - years[0]).astype(float)
cumulative = annual_counts.cumsum().values.astype(float)
try:
popt, pcov = opt.curve_fit(
bass_model, t, cumulative,
p0=[cumulative.max() * 3, 0.01, 0.3],
bounds=([0, 0, 0], [1e6, 0.5, 1.0]),
maxfev=5000
)
M_fit, p_fit, q_fit = popt
perr = np.sqrt(np.diag(pcov))
t_peak = np.log(q_fit / p_fit) / (p_fit + q_fit)
year_peak = years[0] + t_peak
cum_pred = bass_model(t, *popt)
ss_res = ((cumulative - cum_pred)**2).sum()
ss_tot = ((cumulative - cumulative.mean())**2).sum()
r2 = 1 - ss_res / ss_tot
return {
"technology": technology_name,
"M": M_fit, "p": p_fit, "q": q_fit,
"year_peak": year_peak, "r2": r2,
"popt": popt, "t0": years[0], "t": t,
"cumulative": cumulative
}
except Exception as e:
print(f"S-curve fit failed for {technology_name}: {e}")
return None
np.random.seed(42)
years = np.arange(2000, 2023)
tech_data = {
"AI/ML": np.maximum(0,
5 * np.exp(0.2 * (years - 2000)) + np.random.normal(0, 10, len(years))),
"Blockchain": np.maximum(0,
np.where(years < 2014, 0,
2 * (years - 2014)**2 + np.random.normal(0, 5, len(years)))),
"Fuel_Cell": np.maximum(0,
80 / (1 + np.exp(-0.3 * (years - 2008))) + np.random.normal(0, 5, len(years))),
}
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for ax, (tech, annual) in zip(axes, tech_data.items()):
annual_series = pd.Series(annual.astype(int), index=years)
fit = fit_technology_scurve(annual_series, tech)
ax.bar(years, annual_series.cumsum(), alpha=0.4, color="steelblue",
label="Observed cumulative")
ax.bar(years, annual_series, alpha=0.7, color="lightblue",
label="Annual filings")
if fit:
t_ext = np.linspace(0, len(years) + 10, 200)
yrs_ext = fit["t0"] + t_ext
ax.plot(yrs_ext, bass_model(t_ext, *fit["popt"]), "r-", lw=2,
label=f"Bass fit (R²={fit['r2']:.2f})")
if 2000 <= fit["year_peak"] <= 2040:
ax.axvline(fit["year_peak"], color="orange", ls="--",
label=f"Peak: {fit['year_peak']:.0f}")
print(f"\n{tech}: M={fit['M']:.0f}, p={fit['p']:.4f}, "
f"q={fit['q']:.4f}, peak≈{fit['year_peak']:.0f}")
ax.set_title(f"{tech} Technology S-Curve")
ax.set_xlabel("Year"); ax.set_ylabel("Patent Count")
ax.legend(fontsize=7)
plt.tight_layout()
plt.savefig("technology_scurves.png", dpi=150, bbox_inches="tight")
plt.close()
print("\nFigure saved: technology_scurves.png")
Advanced Usage
Inventor Collaboration Network
import networkx as nx
import numpy as np
import pandas as pd
from collections import Counter
from itertools import combinations
def build_inventor_network(df, inventor_col="inventors", sep="; "):
"""Build weighted inventor co-invention network.
Args:
df: DataFrame with inventor column (semicolon-separated names)
Returns:
G: NetworkX weighted undirected graph
"""
G = nx.Graph()
edge_counter = Counter()
for _, row in df.iterrows():
inventors = [i.strip() for i in str(row[inventor_col]).split(sep)
if i.strip()]
for inv in inventors:
if inv not in G:
G.add_node(inv, n_patents=0)
G.nodes[inv]["n_patents"] += 1
for i, j in combinations(inventors, 2):
key = tuple(sorted([i, j]))
edge_counter[key] += 1
for (i, j), w in edge_counter.items():
G.add_edge(i, j, weight=w)
return G
np.random.seed(42)
n_inv_pool = 50
inventor_names = [f"Inventor_{i:02d}" for i in range(n_inv_pool)]
df_inv = df.copy()
df_inv["inventors"] = [
"; ".join(np.random.choice(inventor_names,
size=np.random.randint(1, 5),
replace=False).tolist())
for _ in range(len(df_inv))
]
G_inv = build_inventor_network(df_inv)
print("=== Inventor Network ===")
print(f"Inventors: {G_inv.number_of_nodes()}")
print(f"Collaborations: {G_inv.number_of_edges()}")
top_inv = sorted(G_inv.degree(), key=lambda x: x[1], reverse=True)[:5]
print("\nTop inventors by collaboration degree:")
for inv, deg in top_inv:
print(f" {inv}: degree={deg}, patents={G_inv.nodes[inv]['n_patents']}")
Technology Whitespace Analysis
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
def technology_whitespace(df, class_col="ipc_class", year_col="year",
citation_col="forward_citations", cutoff_year=2015):
"""Identify technology whitespaces: classes with recent activity and high impact.
Args:
df: patent DataFrame
cutoff_year: threshold for "recent" (only count patents after this year)
Returns:
DataFrame with opportunity scores
"""
recent = df[df[year_col] >= cutoff_year]
all_classes = df[class_col].unique()
results = []
for cls in all_classes:
recent_cnt = len(recent[recent[class_col] == cls])
all_cnt = len(df[df[class_col] == cls])
mean_cit = df[df[class_col] == cls][citation_col].mean()
growth_rate = (recent_cnt / max(all_cnt - recent_cnt, 1))
results.append({
"ipc_class": cls,
"total_patents": all_cnt,
"recent_patents": recent_cnt,
"growth_rate": growth_rate,
"mean_fwd_citations": mean_cit,
})
result_df = pd.DataFrame(results)
scaler = StandardScaler()
result_df["opportunity_score"] = scaler.fit_transform(
result_df[["growth_rate", "mean_fwd_citations"]]
).mean(axis=1)
return result_df.sort_values("opportunity_score", ascending=False)
whitespace = technology_whitespace(df, cutoff_year=2018)
print("=== Technology Whitespace Analysis (Top Opportunities) ===")
print(whitespace.head(10).round(3).to_string(index=False))
Troubleshooting
| Problem | Cause | Fix |
|---|
| Bass model fit diverges | Poor initial values or flat data | Use p0=[cumulative[-1]*2, 0.005, 0.1]; check for sufficient variation |
| Citation network is disconnected | Short time window | Extend year range; use weakly connected component |
| Generality = 1.0 for all | Only 1 citing class | Need at least 2 citing classes; filter patents with n_fwd_cit > 5 |
| Sparse IPC co-occurrence | Too granular classification | Roll up to 3-character IPC class |
| PageRank doesn't converge | Large disconnected graph | Use max_iter=500, tol=1e-6; prefilter to LCC |
| Name disambiguation in inventor network | Multiple name formats | Normalize: str.lower().strip(); use disambiguation tools |
External Resources
- Trajtenberg, M. (1990). A penny for your quotes: Patent citations and the value of innovations. RAND Journal of Economics, 21(1).
- Hall, B. H., Jaffe, A. B., & Trajtenberg, M. (2001). The NBER Patent Citations Data File. NBER WP 8498.
- Bass, F. M. (1969). A new product growth model for consumer durables. Management Science, 15(5).
- PATSTAT documentation
- USPTO Open Data Portal
- Google Patents Public Data
Examples
Example 1: Assignee Portfolio Benchmarking
import numpy as np
import pandas as pd
def assignee_benchmarking(df, assignee_col="assignee", citation_col="forward_citations"):
"""Compute portfolio metrics for each assignee."""
return df.groupby(assignee_col).agg(
n_patents=(citation_col, "count"),
total_citations=(citation_col, "sum"),
mean_citations=(citation_col, "mean"),
citation_intensity=(citation_col, lambda x: (x > 10).mean()),
).sort_values("total_citations", ascending=False)
np.random.seed(42)
assignees = [f"Company_{i:02d}" for i in range(10)]
df_assign = df.copy()
df_assign["assignee"] = np.random.choice(assignees, len(df_assign),
p=np.exp(-0.3 * np.arange(10)) /
np.exp(-0.3 * np.arange(10)).sum())
portfolio = assignee_benchmarking(df_assign)
print("=== Assignee Portfolio Benchmarking ===")
print(portfolio.round(2))
Example 2: Patent Family Analysis
import pandas as pd
import numpy as np
def analyze_patent_families(df):
"""Compute family-level statistics from patent records.
A patent family groups equivalent patents filed in multiple countries.
Family size (number of countries) is a proxy for commercial value.
"""
np.random.seed(42)
n_families = len(df) // 3
df = df.copy()
df["family_id"] = np.random.choice(range(n_families), len(df))
family_stats = df.groupby("family_id").agg(
n_family_members=("patent_id", "count"),
n_countries=("country", "nunique"),
max_fwd_citations=("forward_citations", "max"),
earliest_year=("year", "min"),
).reset_index()
family_stats["value_proxy"] = (
np.log1p(family_stats["max_fwd_citations"]) *
np.log1p(family_stats["n_countries"])
)
print(f"Patent families: {len(family_stats)}")
print(f"Mean family size: {family_stats['n_family_members'].mean():.1f}")
print(f"Multi-country families: {(family_stats['n_countries'] > 1).mean()*100:.1f}%")
return family_stats
families = analyze_patent_families(df)
print("\nTop 5 highest-value patent families:")
print(families.nlargest(5, "value_proxy")[
["family_id", "n_family_members", "n_countries", "max_fwd_citations", "value_proxy"]
].round(2).to_string(index=False))