When to Use
Invoke this when building a cross-sectional equity factor from corporate patent filings. The
engine turns a stream of patent records into a point-in-time Innovation Quality Score (IQS) and
its z-scored factor, keyed on an explicit as_of cut-off.
Balance-sheet R&D expense measures spending, not productivity, and the published result is about the ratio between them: Hirshleifer, Hsu & Li (2013) find that "innovative efficiency (IE), patents or citations scaled by research and development expenditures, is a strong positive predictor of future returns after controlling for firm characteristics and risk." Note what that sentence does and does not say — the predictor is patents scaled by R&D, the sample is US firms 1981–2006, and the authors attribute the effect substantially to mispricing and limited investor attention rather than to a risk premium. A raw patent count is mostly a firm-size proxy.
Three things make this harder than counting patents, and the module exists for all three:
-
A filing is not public on its filing date. 35 U.S.C. 122(a) holds applications "in confidence." Publication comes later, and the gap is where look-ahead bias enters.
-
Forward citations are a forward-looking quantity by construction. Hall, Jaffe & Trajtenberg (2001) put a number on it: if the lag distribution were stable, "patents granted in year 2,000 will receive just half of their citations by 2,010, 75% by 2,020, and even by 2,050 they will still be receiving some." Scoring a 2016 cross-section with a citation count pulled from today's database therefore imports roughly a decade of future information.
-
Raw counts are dominated by scale, and the two obvious components are the same component. A sum of citations over a firm's patents grows with the patent count, so weighting "velocity" against "citation total" weights firm size against firm size.
When NOT to Use
-
Without per-date citation snapshots, for a historical backtest. Every record must carry
citations_observed_asof, and the engine raisesPatentDataErrorwhen that date is afteras_of. This is the point of the module, not an obstacle to route around: if you only have today's cumulative counts, you can score today's cross-section honestly and nothing earlier. Do not "fix" the exception by wideningas_ofor stripping the observation date. -
As an innovative-efficiency replication. Hirshleifer, Hsu & Li scale by an R&D capital stock built from a weighted 5-year expense history and use grant-year cohorts of USPTO subcategories. This engine takes one flat
innovation_inputper asset and builds cohorts from the availability year. It is the same idea, not the same estimator, and it will not reproduce their numbers. -
On a universe too small to standardise. The factor is a cross-sectional z-score. With population sigma and N assets, the largest attainable |z| is sqrt(N−1), so the ±3 winsorisation is inert below N = 10 and z-scores in a handful of names describe universe size more than innovation. Read
universe_below_recommended_sizeandwinsorisation_can_bindbefore ranking. -
For patent valuation. Counts and citations proxy technological impact, not economic value. A factor built here says nothing about whether a patent is enforceable, licensed, or worth anything.
-
Across jurisdictions without checking the availability rule. The 18-month rule is common to 35 U.S.C. 122(b)(1)(A) and Article 93(1)(a) EPC, but the EPC clock runs from the filing or priority date, and other offices differ. Supply observed dates; do not compute them.
-
As a compliance control over alternative data. For MNPI and vendor-governance questions see
insider-trading-controls-for-alternative-data-usageandalternative-data-vendor-due-diligence-checklist.
Prerequisites
- Per patent:
asset_id,patent_id,filing_date, and both availability dates where they exist —pre_grant_publication_dateandgrant_date.patent_idmust be unique; assignee disambiguation joins routinely emit one row per assignee per patent. forward_citationswith thecitations_observed_asofdate the count was read. Required whenever the count is non-zero; an undated cumulative count cannot be checked for look-ahead.technology_class(CPC/IPC/USPC subcategory). Together with the availability year this forms the citation cohort. Records with no class fall intoUNCLASSIFIEDand are adjusted against each other, which is weaker than a real classification.- Optional but strongly recommended:
innovation_inputs, an{asset_id: R&D spend}mapping in your own units. Without it the velocity term is a raw count and the factor carries a firm-size component; the report says so inwarnings. - A data source. The USPTO Open Data Portal (
data.uspto.gov) is the current first-party route and requires a USPTO.gov account with a linked ID.me account to obtain an API key; rate limits are published atdata.uspto.gov/apis/api-rate-limits— read them there rather than assuming a number. The legacy PatentsView API (api.patentsview.org) was discontinued on 1 May 2025 and returns HTTP 410, andpatentsview.orgnow redirects to the Open Data Portal.
Workflow
-
Establish each patent's public availability date — never its filing date.
public_availability_dateis the earlier ofpre_grant_publication_dateandgrant_date.- Decision point — this is a
min(), not "filing + 18 months". Under 35 U.S.C. 122(b)(1)(A) an application publishes "promptly after the expiration of a period of 18 months from the earliest filing date for which a benefit is sought", and MPEP 1120 puts the projected date at the later of that or roughly 14 weeks from the filing receipt, issuing weekly on Thursdays. But a non-publication request under 35 U.S.C. 122(b)(2)(B)(i) is available where the invention "has not been and will not be the subject of an application filed in another country ... that requires publication", and a secrecy order under 35 U.S.C. 181 suppresses publication entirely. Those applications become public only at grant. A synthesisedfiling_date + 18 monthsis wrong for exactly the filings a non-publication request was chosen to hide. - A pending, unpublished application has no availability date and carries no tradable
information. It is counted in
not_yet_public_excluded, not silently dropped.
- Decision point — this is a
-
Evaluate as of an explicit instant.
compute_patent_innovation_signals(records, as_of=...)scores only patents public at or beforeas_ofand inside the rollinglookback_yearswindow (default 5, inclusive at both ends). Pass the whole history and rollas_offorward.- Decision point — a citation count observed after
as_ofis rejected, not clipped. The engine cannot know which of those citations predate the cut-off, so it refuses rather than guessing. Re-read the count as of the evaluation date. - Counts read across a wide span of dates cover different exposure windows and are not
cross-comparable even when each individually passes the cut-off; the span is reported in
citation_observation_span_daysand warned on abovemax_citation_observation_span_days.
- Decision point — a citation count observed after
-
Deduplicate on
patent_idbefore counting anything. One patent arriving as several assignee rows inflates velocity for precisely the largest, most-joined issuers. Duplicates are counted induplicate_patent_ids_dropped. -
Remove citation truncation with cohort scaling. Each patent's count is divided by the mean count of its
(technology_class, availability_year)cohort. This is Hall, Jaffe & Trajtenberg's "fixed-effects approach ... scaling citation counts by dividing them by the average citation count for a group of patents to which the patent of interest belongs"; Hirshleifer, Hsu & Li apply the same construction because it "helps control for citation propensity attributed to differences in technology fields, grant year, and citing year."- Decision point — a cohort below
min_cohort_sizeis not trusted. Those patents keep their raw, still-truncation-biased counts and are counted incohorts_below_min_size. A cohort of one would adjust that patent to exactly 1.0 and tell you nothing. - A cohort in which nothing has been cited yet has mean 0 and is left unadjusted rather than divided by.
- Decision point — a cohort below
-
Build two components that are not the same component.
- Velocity $V_i$ = patents available in the window, divided by
innovation_inputwhen supplied. - Quality $Q_i$ = the mean cohort-adjusted ratio per patent — scale-free by construction, so it is not a second copy of the count. Optionally compressed with $\ln(1+x)$ so one mega-cited patent cannot carry an issuer.
- Velocity $V_i$ = patents available in the window, divided by
-
Standardise each component, then weight. $$Z^V_i = \frac{V_i - \mu_V}{\sigma_V}, \qquad Z^Q_i = \frac{Q_i - \mu_Q}{\sigma_Q}$$ $$IQS_i = \frac{w_{\text{vel}} Z^V_i + w_{\text{cite}} Z^Q_i}{w_{\text{vel}} + w_{\text{cite}}}$$
- Decision point — the order matters and is the whole reason the weights mean anything. Weighting a raw count against a sum of logs, as $w_v V_i + w_c C_i$ does, hands the factor to whichever term happens to carry the larger numeric scale, regardless of the weights. After standardisation the factor is invariant to the units of either component.
- The composite is re-standardised and then winsorised to ±
winsorize_z. Winsorisation is applied last, so the delivered factor is unit-variance but only approximately mean-zero once any name is clipped.
-
Reconcile before using the output.
records_suppliedequalspatents_scoredplus the three exclusion counters exactly;report.reconciles()checks it. Readwarningsand thestatusfield —SIGNALS_GENERATED,EMPTY_UNIVERSE,INSUFFICIENT_UNIVERSE(no peers to standardise against) orNO_DISPERSION(every asset identical, nothing to rank). On the last three,top_innovatoris"NONE"and is not a key inz_scores.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
-
Dating a patent by its filing date. The application was confidential then (35 U.S.C. 122(a)). Hall, Jaffe & Trajtenberg recommend the application date as "the relevant time placer" for measuring when invention happened — that is a question about innovation, not about what a portfolio could have known. For a tradable signal the date is publication.
-
Dating it by the grant date instead, and assuming that is conservative. For most applications the 18-month A-publication comes first, so grant-dating discards roughly a year and a half of genuinely public information. The correct rule is the earlier of the two, per patent.
-
Using today's cumulative citation count in a historical cross-section. This is the largest single source of look-ahead in patent factors, and it does not look like a bug: the counts are real, they are just measured too late. Half of a patent's lifetime citations arrive in its first decade (HJT 2001).
-
Comparing raw citation counts across technology fields or filing cohorts. A biotech patent from 2015 and a software patent from 2022 have incomparable expected counts. Cohort-scale first.
-
Summing citations instead of averaging them. A sum is a count in disguise and reintroduces the size factor you were trying to escape.
-
Treating the ±3 winsorisation as an active control. With population sigma it cannot bind until N ≥ 10; below that it is decorative.
-
Letting a NaN citation count become a zero.
max(0, float('nan'))evaluates to0in Python, so a failed upstream join silently becomes "this patent was never cited." The engine raises instead. -
Counting one patent once per assignee row. Disambiguated assignee tables are one-to-many.
Verification
PatentFilingRecord("AAA", "a1", filing_date=..., grant_date=2023-05-01, pre_grant_publication_date=2021-07-01).public_availability_date$\implies$2021-07-01; withpre_grant_publication_date=None$\implies$ the grant date; with neither $\implies$None.- Three issuers — AAA with 10 patents × 1 citation, BBB with 2 × 50, CCC with 4 × 5, one cohort of 16 whose mean is 8.125 $\implies$ quality terms 0.123077 / 6.153846 / 0.615385 and a factor of +0.807947 / +0.601221 / −1.409168. Multiplying every citation count by 10 $\implies$ an identical factor (unit invariance).
- Supply a patent granted after
as_of$\implies$not_yet_public_excluded == 1and it is absent from the scored count. Supplycitations_observed_asof > as_of$\implies$PatentDataError. - Supply
forward_citations=float("nan")$\implies$PatentDataError, not a zero-citation patent. - Empty input $\implies$
status == "EMPTY_UNIVERSE"; one asset $\implies$INSUFFICIENT_UNIVERSEwith no scores emitted; identical assets $\implies$NO_DISPERSION. - Run
python -m unittest discover -s skills/patent-filing-data-for-innovation-signal-research/scripts.