feat(corpus_importer): ingest USPTO PTLITIG patent litigation data#7390
feat(corpus_importer): ingest USPTO PTLITIG patent litigation data#7390neilrubin wants to merge 3 commits into
Conversation
Add a USPTO_PTLITIG source bit to Docket so that dockets contributed from the USPTO Patent Litigation Docket Reports (PTLITIG) can be told apart from RECAP uploads. This follows the precedent of IDB and ANON_2020, where a named external dataset earns its own source value. A new bit is needed rather than reusing RECAP: most PTLITIG cases that lack an existing CourtListener docket are precisely the ones RECAP never captured, so labeling them RECAP would be inaccurate. The source byte was already full (all 256 combinations are enumerated), so this adds the ninth bit (256) and its 256 new combinations to SOURCE_CHOICES, generated the same way the SCANNING_PROJECT bit was layered on. The field is already a SmallIntegerField, so the values 256-511 need no column change -- only the choices -- hence the accompanying migration is a no-op. A matching add_uspto_ptlitig_source() helper and NON_USPTO_PTLITIG_SOURCES() group are added alongside the existing add_*_source helpers, with tests covering the helper and the contiguous source-constant invariant the groups rely on. This is the first of three PRs for the PTLITIG import; on its own it changes no behavior. Refs: freelawproject#664 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a DocketIdentifier model to record external identifiers for the subject matter at issue in a case -- initially the patents asserted in a patent infringement docket. CourtListener has no model for patents today (only the nature-of-suit codes 830/835), so this is the structured home for that data. The model mirrors the typed-identifier shape of Citation: an AbstractDateTimeModel with a CASCADE foreign key to Docket, a small-integer type enum, and the identifier value. The four enum values match the kinds the data distinguishes: granted U.S. patent, application, published application, and foreign patent. The value is stored exactly as found so non-numeric forms survive (design 'Dxxxxxx', reissue 'RExxxxx', application serials 'NN/NNNNNN'), and a (docket, type, value) unique constraint makes ingestion idempotent. Tests cover creation, __str__, and the unique constraint. This is the second of three PRs for the PTLITIG import. It adds the table but nothing writes to it yet. Refs: freelawproject#664 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Import the USPTO Patent Litigation Docket Reports (PTLITIG) -- ~97k U.S. district-court patent cases (1963-2021) the USPTO compiled from PACER docket reports. Because PTLITIG is built from the same PACER docket reports RECAP captures, each case is a flattened sibling of Juriscraper's DocketReport.data, so this reshapes each case into that dict and feeds it through the existing cl.recap.mergers primitives (find_docket_object, update_docket_metadata, add_parties_and_attorneys), the way the SCOTUS and Texas importers do. cl/corpus_importer/ptlitig.py holds the adapter and merge_ptlitig_docket(); the import_uspto_ptlitig management command reads the PTLITIG CSV download and merges each case in two passes (small per-case files in memory, the 2.7 GB documents.csv streamed). A single case that fails on a data problem (an unknown court, a constraint violation) is logged and skipped; other errors surface. Safety choices, so PTLITIG's older snapshot never overwrites richer RECAP data: - Parties/attorneys are added only to dockets that have none (the merge primitive deletes and recreates roles). - Asserted patents become DocketIdentifier rows, always added (idempotent via the unique constraint). - Docket entries are a gap-fill: numbered entries are matched on (docket, entry_number) so only the missing ones are created and existing RECAP entries are untouched (kept even when undated); unnumbered entries, which have no stable key, are added only to dockets that start out empty. Tests use FactoryBoy (no fixtures) and cover the source bit, patent typing, party building, the entry gap-fill, idempotency, and an end-to-end run of the management command over sample CSVs. This is the third of three PRs for the PTLITIG import; it depends on the USPTO_PTLITIG source and the DocketIdentifier model added in the other two. Refs: freelawproject#664 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks Neil! This is a nice one to get done. A couple quick thoughts from me, and then I think somebody from our team will slot this for review:
This is cool, thanks for picking it up! |
|
Hi @mlissner, adding that sort of search and alert functionality is an obvious follow-on but seemed like something your team would have strong opinions about the right approach on. I thought about parsing documents. It seems like a pretty simple problem if you actually have full dockets with all PDFs, but it gets tricky if you want to try to extract the information when you only have partial dockets. Regex logic that matches on The gold standard for what patents are actually asserted is the Form AO 120 that is filed with the complaint. The layout of the form has been the same forever, and most filed in PACER should be vector PDFs with the patent numbers typed in, so extracting the patent numbers from the form should be robust. The problem is that people probably don't download these forms often on PACER (to be picked up on the RECAP extension), and recap.email is going miss most of them, because they are filed before most counsel has made an appearance. You folks would know better than me how often you get AO 120 PDFs for patent NOS cases (or how often you get complaints). It would take some extra work, but AO 120 forms are also available for free from the USPTO ODP API (I have a comprehensive collection). The timing of when the USPTO gets them is a little random (whenever court clerks get around to sending them to the PTO), and the copies in ODP are raster scans, which makes extraction a little harder. |
|
Nice. I think we'd keep this really simple and just use regex to search for patent numbers anywhere, and then make those searchable. If we do it this way, folks can decide for themselves if they want to filter to only certain forms or NOS codes, and we can keep our search engine universal (ie, we can say "if it has a patent number, we find it" instead of "if it has a patent number, and a nos code of XYZ, and, and, and..."). Definitely some details to work out (like, how do you normalize |
|
Agreed on simple and on using regexes. My take on the "'123 Patent" issue is that the use of this abbreviated reference format in patent documents is a feature, rather than a bug. A regex on 7- and 8-digit numbers with commas is going to have false positives, and there are a vast number of ways the full patent number will appear. ("U.S. Patent No. 12,345,678" would be canonical, but there are a bunch of other ways.) In contrast, "'123 Patent" is nearly universal as the abbreviated form, and is also going to have virtually zero false positives. (It's like seeing "Id. at NN" in legal writing is virtually 100% going to be a citation to something.) And there are virtually zero patent litigation documents that don't use the abbreviated form somewhere. There are also very, very few documents that use the abbreviated form without somewhere mentioning the full form. So, the strategy I'd propose is: (1) a very liberal regex on the full patent form (matching any string of digits with commas, plus the RE, D, and PP prefixes) and (2) requiring the presence of a matching short form as a false positive rejecter. If you want to deal with the very rare case where the abbreviated form is not used, you could possibly add a third regex that accepts the canonical form ("U.S. Patent No. 12,345,678"). The only complication that I can see is that the occasional case will have two patents that happen to collide on the last three digits, in which case the most common solution is to expand the abbreviated form to the last four digits. Rare, but it's probably worth handling in the regexes. The nice thing is that you have a large ground-truth set in the form of the PTLITIG set, so it would be pretty easy to test the regexes at scale. In case it's useful, here the USPTO paper describing their methodology: USPTO Economic Working Paper No. 2019-05. Looks like they used NOS codes to identify the cases, and they manually coded the patent numbers (for 97k cases—not my idea of a good time). |
Yeah, that sounds exactly right to me (including doing the third regex). Thanks Neil! |
Hi — I'm a first-time contributor to CourtListener. I've tried to take on the task of addressing Issue #664, importing PACER data from patent cases collected by the USPTO (the PTLITIG dataset), last updated in 2020. For better or worse, this task touches on some other components of the CL system, and I've broken it into 3 PRs as a result.
First, it seems like the PTLITIG dataset should be treated as a new Docket source value, but the current 8-bit field for such values is full. The first PR switches to a nine-bit source field. Others with a better sense of the big picture might prefer a different solution.
Second, the PTLITIG data identifies the patent(s) at issue in each of the cases. Patent numbers are a very important piece of data in analyzing and making use of patent litigation dockets, and there are other similar well-known identifiers (trademark or copyright registrations come to mind, but there are certainly others) that it would make sense for the CourtListener system to handle and to allow searches on. The second PR adds a database table for such identifiers, currently scoped solely to handle the patent values from PTLITIG. While out of scope for this PR, I would suggest scraping patent numbers from complaints and/or AO120 forms of patent cases to populate this table for more recent cases would be a good follow-on project.
The third PR is the actual ingestion process. The data files to be ingested are available at USPTO. As designed, this seeks to merge all PTLITIG docket entries into the current database, without adding duplicates or disturbing existing entries. Since it is PACER data (some from RECAP and some from the USPTO's own crawl), it includes both docket entries with entry numbers and those without entry numbers (e.g., minute entries). To avoid duplication or difficult merge logic on the minute entries, I chose to not import minute entries at all for items without entry numbers, if there were any existing docket entries for the case in the CL database. The thinking is that cases that currently have docket entries in CL are likely to have dockets at least as complete as those in a 2020 crawl and that minute entries for old cases are of relatively low value anyway, such that they are not worth dealing with the merge logic.
This is one of four PRs that I'm submitting today as a first-time contributor to the repo. While I'm knowledgeable concerning the PACER data, RECAP, and the CL data offerings, and I'm a capable Python coder, I am not intimately familiar with the internals of the repo code. As full disclosure, the code was entirely written using Claude Code, as was the entirety of this .md file, aside from the first six paragraphs of this text. While I have attempted to carefully think through the design, make sure it is correct and consistent with the existing code, and make sure that it passes all CI tests, this is the work of a first-timer.
I'm aware that FLP has some sort of policies and guidance on the use of tools such as Claude Code for development, but I have not been able to review them. See Issue #7383. To the extent this PR is inconsistent with those policies and guidance, I am happy to revise once I'm able to see them.
Fixes
Fixes: #664
Refs: #448
PR 3 of 3 for the PTLITIG import; builds on PR 1/3, #7388 (the
USPTO_PTLITIGsource bit) and PR 2/3, #7389 (theDocketIdentifiermodel).Please merge in order: #7388, then #7389, then this PR.
Summary
Imports the USPTO Patent Litigation Docket Reports (PTLITIG) — ~97k U.S.
district-court patent cases (1963–2021) the USPTO compiled from PACER docket
reports.
Because PTLITIG is built from the same PACER docket reports RECAP captures, each
case is a flattened sibling of Juriscraper's
DocketReport.data. So rather thanwrite new normalization, this reshapes each case into that dict and feeds it
through the existing
cl.recap.mergersprimitives (find_docket_object,update_docket_metadata,add_parties_and_attorneys) — the way the SCOTUS andTexas importers do.
cl/corpus_importer/ptlitig.pyholds the adapter andmerge_ptlitig_docket(); theimport_uspto_ptlitigmanagement command reads thePTLITIG CSV download and merges each case in two passes (the small per-case files
in memory, the 2.7 GB
documents.csvstreamed).Safety choices, so PTLITIG's older snapshot never overwrites richer RECAP
data:
primitive deletes and recreates roles).
DocketIdentifierrows, always added (idempotentvia the unique constraint).
(docket, entry_number)so only the missing ones are created and existingRECAP entries are left untouched (kept even when undated); unnumbered entries,
which have no stable key, are added only to dockets that start out empty.
violation) is logged and skipped; other errors surface.
Tests use FactoryBoy (no fixtures) and cover the source bit, patent typing,
party building, the entry gap-fill, idempotency, and an end-to-end run of the
command over sample CSVs.
Operational note for whoever runs it: this is a slow, one-time job. Per-case
writes dominate and the full
documents.csvis streamed, so a complete ~97k-caserun takes hours, and saving the dockets/entries fires the usual ES indexing. It's
meant to be run once, off-peak, by a maintainer — not on a schedule.
Deployment
This PR should:
skip-deploy(skips everything below)skip-web-deployskip-celery-deployskip-cronjob-deployskip-daemon-deployAdds a management command and supporting code; nothing is deployed or scheduled —
the command is run manually against the PTLITIG CSV download.