Tayler Erbe · Technical Documentation · Part 1 of 2 · 2026

Legislative Data
& Prediction Pipeline

The server-side half of the Legislative Intelligence Platform: how raw bill text becomes structured intelligence and a calibrated floor-vote probability. Ingestion, LLM feature extraction, a formal model selection evaluation, a procedural taxonomy built from nothing, and a three-stage survival model validated at 0.9944 AUC-ROC across 117,667 bills — running unattended every day.

Part 2 — The Web Application →
Status
Production · delivered to IGPA Aug 2026
Role
Sole builder · data engineering through production
Timeline
Feb 2026 POC → Aug 2026 production
Cadence
Autonomous daily run
01Ingestion & Corpus 02Model Selection 03Feature Extraction 04Procedural Taxonomy 05Prediction System 06Leakage Control 07Production Pipeline 08Defects & Limits

Getting the corpus, and keeping it current

Two sources, neither designed for this. LegiScan provides a structured API; the Illinois General Assembly publishes bulk files over FTP. Neither exposes a change feed, so the pipeline computes its own: a content hash per artifact, compared against the last known hash, with download conditional on a mismatch. A daily run that finds nothing new does almost no work.

Bill text arrives in wildly uneven lengths — a one-line technical amendment and a 400-page appropriations act enter the same pipeline. Text is normalized and chunked before extraction so the downstream model sees consistent input regardless of source length.

Corpus, application
46,074 bills across 4 General Assembly sessions (101st–104th)
Active session
104th GA (2025–2026) · 12,022 bills · 9,734 active
Corpus, model training
117,667 bills spanning nine General Assembly sessions, 2009–2026
Ingest sources
LegiScan API · ILGA FTP bulk
Change detection
Content-hash comparison, conditional download
Why two different corpus sizes

The application serves four sessions because that is the window stakeholders actually reason about. The model trains and validates across nine, because a survival model needs to see enough terminal outcomes across enough political regimes to be trusted. Generative work — personalized impact scoring, briefings, digests — runs only against the current session, since scoring a closed session against a live role profile produces analysis nobody can act on.

Choosing a model before spending a week of compute

Extraction across the backfill corpus was an 11,673-bill, roughly 155-hour unattended job. A wrong model choice there is not a quick re-run; it is a week of GPU time and a corpus of low-quality structured output that everything downstream inherits. So the choice was made as a formal evaluation with the acceptance criteria written down first.

Design
15-cell grid — 3 models × 5 chunk sizes, 2 repeats per cell, ~15 hours unattended
Service levels
Declared before execution rather than fitted to results
Resource analysis
USE method applied to the GPU — utilization, saturation, errors sampled alongside throughput
Qualitative review
Blind three-criterion rubric, authored for this task, with a review-companion tool
Deliverables
Formal decision record, stakeholder deck, consolidated metrics, reproducible artifact bundle

Two findings did the real work. First, a reproducible saturation signature appeared in one model-and-chunk-size cell — visible only because resource metrics were sampled concurrently with throughput rather than averaged afterward. Second, output quality degraded past roughly 2,000 tokens regardless of model. That second finding is what made the decision tractable: chunk size became separable from model choice, so the two could be decided independently instead of as a joint search.

The original grid selected Mistral 7B at 500 tokens, with cross-configuration variance used as the tiebreaker rather than peak score. For an unattended multi-night job, a model that behaves predictably across configurations is worth more than one that wins a single cell and misbehaves elsewhere.

Re-evaluated when the serving stack changed

The original evaluation ran on the previous inference backend. After the migration to vLLM (documented separately in the throughput case study), the comparison no longer held — different batching behaviour, different effective cost per token. Rather than carry forward a stale decision, the evaluation was re-run over the weekend of 17 May 2026 against a contemporary quantized lineup: Mistral 7B v0.3 AWQ, Qwen 2.5 7B AWQ, and Llama 3.1 8B AWQ.

Production moved to Llama 3.1 8B AWQ, upgrading from the 3B-class model the pipeline had been running. The larger model became affordable specifically because the serving migration had recovered the headroom to pay for it.

From bill text to structured fields

Each bill is reduced to a set of standardized attributes an analyst can filter, sort, and reason over: what the bill does, who it affects, what it changes, what it expands or contracts, its policy domain, and a university impact assessment. These are the fields every downstream surface reads — search, categories, impact scoring, and digests all resolve against them.

Two properties matter more than the field list. Raw model output is retained alongside the standardized field. Keyword matching runs against the raw text as ground truth and falls back to the standardized field only when a raw value is absent, so normalization never silently destroys a match. And extraction is decoupled from scoring: bill features are computed once per bill, while per-user impact scoring runs separately against each user's own prompts. Adding a user does not re-extract the corpus.

Bill detail view showing probability trail and per-action model scores
A scored bill: extracted summary, university impact assessment, policy domain, the full action history, and the model score after each procedural action.

The training data did not exist

Predicting passage requires knowing where a bill sits procedurally, and legislative action text is free-form. "Placed on Calendar Order of 3rd Reading", "Rule 19(a) / Re-referred to Rules Committee", "Do Pass Licensed Activities; 008-000-000" — each carries stage, activity type, and semantic meaning, none of it labelled.

So the labels were built. A 47-category two-tier taxonomy reaching 99.0% coverage, applied to 1,388,400 individual action rows with procedural stage, activity type, and semantic stage at 99.65% coverage.

An implementation constraint that changed the design. A row-wise labeler over 1.39M rows was not runnable at this scale within the pipeline's daily window. The labeler was written as a two-pass vectorized operation instead — broad pattern classes first, then refinement within class. This is the difference between a labeling step that completes inside a nightly run and one that does not.
Taxonomy
47 categories, two-tier · 99.0% coverage
Action rows labelled
1,388,400 · 99.65% coverage
Labels per row
Procedural stage, activity type, semantic stage
Method
Two-pass vectorized labeler
Retained
Raw labelled history kept as an audit artifact

A survival problem, not a classification problem

Most bills die. They die at different procedural stages, for different reasons, and the honest answer to "will this pass" depends entirely on when you ask. A single classifier over final outcomes throws that structure away. The system is staged instead, so each model answers the question that is actually answerable at its point in the lifecycle.

STAGE 1Introduction-time prediction
Scores a bill at the moment of introduction, before any procedural history exists. The hardest stage and the least certain, because at introduction the only signal is the bill itself — sponsor, subject, chamber, drafting characteristics.
STAGE 2Survival to floor vote · core production model
The model in production. Given a bill's accumulated procedural history, what is the likelihood it reaches a floor vote? This is the number surfaced throughout the application as floor-vote likelihood, and it is rescored after every new action.
STAGE 3Per-legislator vote prediction
Conditional on reaching a vote, how do individual members vote? Built on public roll-call record.

Stage 2 validation

MeasureResultNote
Discrimination (AUC-ROC)0.99442025 holdout, never seen during training
95% confidence interval0.9908 – 0.9971Bootstrap over the holdout
Precision @ operating threshold0.9602Threshold chosen for the production surface
Recall @ operating threshold0.9659
Year-by-year stability0.9961 ± 0.0018Sixteen years, across speaker and rule changes
Validation corpus117,667 bills2009 – 2026, nine General Assembly sessions

The stability figure matters more than the headline. A model that scores well on one holdout may simply have learned one legislature's habits. Holding within ±0.0018 across sixteen years spanning multiple speakers, rule changes, and political transitions says the signal is procedural rather than incidental to a particular regime.

Probability trail for a tracked bill showing model score after each legislative action
The probability trail. Each point is the model score after a specific action — a bill's forecast as a path rather than a single number, with the action that moved it named alongside.
Why the trail is exposed, not just the score

A stakeholder asked to act on a number will ask why it changed. Showing the score after every action makes the model's reasoning inspectable without requiring anyone to read a model: a bill that jumps from 46% to 85% on "Do Pass" is legible. A single current-value display would have been simpler to build and considerably less useful, and it would have concealed the defect described in Section 08.

Cross-state expansion

The system was extended to California to test whether the approach generalized past one legislature. The first framing produced a suspiciously easy task — the target definition was leaking. After correction, Stage 2 validated at 0.8512 AUC across 31,798 California bills. That is the published number. The flattering pre-correction figure is not reported anywhere, because it was wrong.

A number this high should invite suspicion

0.9944 on passage prediction is the kind of result that should make a reviewer distrust the pipeline before they believe the model, and it made me distrust mine. Procedural history leaks by construction: the actions that record a bill passing live in the same table as the actions that predict it. Without a guard, the model learns to read the answer.

Guard placement
Applied at a single point, before aggregation — one place to inspect, one place to break
Split boundary
Bill level, not row level, so a bill's own history cannot span train and test
Terminal outcomes
Audited across all nine terminal-outcome terms
Audit artifact
Raw labelled history retained so the guard can be inspected rather than trusted
Holdout
Full calendar year (2025) withheld entirely from training

The last row is the one I would defend hardest. A leakage guard that cannot be independently checked is a claim, not a control. Keeping the raw labelled history means a reviewer can re-derive what the guard excluded rather than take my word for it.

One scheduled command, no human in the loop

The daily run executes as a single scheduled command on the institutional Linux analytics server. Every stage writes structured logs against the standardized logging contract used across all pipelines on that server, which means this pipeline is visible in the cross-pipeline job execution monitor without any bespoke monitoring code.

Hash check
Conditional download
Normalize & chunk
Relabel actions
Rescore
Snapshot
Cloud upload
Cadence
Daily, unattended
Scope
Nine General Assembly sessions · 50,501 active bills across the full corpus
Generative scope
Impact scoring and briefings run against the current session only
Inference
Hybrid — on-premises vLLM for bulk scoring, cloud LLM for user-facing generation
Observability
Structured Parquet logs into the shared job execution monitor
The hybrid split was forced, then made deliberate

Database writes worked locally and failed from the deployed application. Not credentials, not permissions — the campus network was terminating the connection at the TLS layer, and only for traffic originating inside the institutional network. Rather than fight the network, the scoring workload was re-architected around it: bulk inference stays on-premises where the constraint lives, user-facing generation runs in the cloud where it does not. The constraint became an architectural decision instead of an outage.

What went wrong, and what the system still cannot do

Every defect below was found internally, before a stakeholder encountered it. That is the part worth documenting — not that the system had faults, but which mechanism surfaced each one.

DEFECT 01Join-key failure · ID-space mismatch
A production join silently failed because two upstream sources used different identifier spaces for what appeared to be the same key. Diagnosed to the mismatch, rebuilt from source, and given a stable secondary key so the join cannot degrade to a partial match again.

Surfaced by: row-count validation between stages, which is why the stage boundaries write counts rather than just passing frames along.
DEFECT 02Monotonicity fault in the probability trail
The per-bill probability trail moved in ways the procedural sequence could not justify — a bill's score falling on an action that should have raised it. Aggregate AUC concealed this completely. A model can order bills near-perfectly while still producing an individually incoherent path, and the headline metric will never say so.

The model-side fix was specified. A display-layer patch was applied in the interim and documented as insufficient rather than allowed to stand in for the real correction.

Surfaced by: exposing the trail in the interface. A single current-value display would have hidden this indefinitely.
DEFECT 03Unfalsifiable success signal
A reliability audit of the pipeline's own health check found it reported success under conditions where it could not have detected failure. It was structurally incapable of returning no. Separately, a six-day repeating alarm traced to a single malformed record.

The check was rewritten to be falsifiable. Terminology was also standardized after the audit found the prediction being described five different ways across five surfaces.

Surfaced by: auditing my own monitoring rather than assuming a green signal meant green.

Known limitations

Stage 2 predicts reaching a floor vote, not becoming law. These are different questions. Bills reach votes and fail; bills pass one chamber and die in the other. The number is labelled floor-vote likelihood throughout the interface for that reason, and the standardization pass that fixed five competing descriptions existed specifically to stop this from being misread.
The advance-warning framework is specified but not live. The intended validation surface answers a better question than AUC does: how many days before passage did the model first reach certainty? A model that becomes confident the morning of the vote is accurate and useless. That framework requires prospective tracking data the system has not yet accumulated, so the surface exists in the application as a defined destination and is not populated. It is reported here as pending rather than shipped.
Cross-state generalization is demonstrated, not established. One additional state at 0.8512 shows the approach transfers. It does not show it transfers everywhere, and the gap between the Illinois and California figures is itself the interesting result — procedural signal is legislature-specific in ways a single number cannot capture.

The other half

Everything above produces a scored, structured corpus. It becomes useful only when a specific person can ask it a question in their own terms — which is the subject of Part 2: the authenticated multi-service application, the per-user personalization layer, team deliberation, and the cloud architecture behind it.

Part 2 — The Web Application →
← Back to Portfolio