This repository contains streaming tools for working with the CFPB complaints dataset without loading the full raw file into memory. The main workflow in the repo is a visualization pipeline for complaints related to credit reporting and student loans, plus a couple of utility scripts for converting and sampling the dataset.
The repo is organized into a small package under src/ plus thin script
wrappers under scripts/.
src/cfpb_analyzer/analysis.pyStreamsdata/complaints.jsonl, normalizesProductandSub-product, filters to credit-reporting and student-loan complaints, and computes aggregate counts in one pass.src/cfpb_analyzer/plotting.pyRenders plots from the aggregated results usingmatplotlibandseaborn.src/cfpb_analyzer/reporting.pyGenerates the Markdown summary with high-level findings and caveats.src/cfpb_analyzer/cleaning.pyFilters, deduplicates, renamesIssue/Sub-issuevalues, and applies a date cutoff. Two execution paths share the same streaming loop: a true in-place rewrite (no extra disk) and a.tmp+atomic-rename rewrite (used when renames may grow row length).src/cfpb_analyzer/cli.pyOrchestrates the end-to-end analysis run for the visualization pipeline.
scripts/visualize_complaints.pyThin wrapper around the package CLI. This is the main entrypoint for the visualization workflow.scripts/complaints_csv_to_jsonl.pyConverts the original CFPB CSV file into JSONL in a streaming way.scripts/clean_complaints_jsonl.pyFilters, dedupes, renames, and date-trims the JSONL. Defaults to dropping pre-2018 complaints and rows whoseProduct/Sub-productaren't credit reporting or student loan.scripts/view_random_jsonl.pyBuilds a sidecar line-offset index and lets you inspect random JSONL entries without loading the full file into memory.
tests/test_analysis.pyUnit tests for normalization, categorization, and aggregation behavior.tests/test_cli.pySmoke test for the visualization CLI and expected output files.tests/test_clean_complaints_jsonl.pyCovers the cleaning pipeline: filter, dedup, rename, date filter, dry-run, and the in-place vs..tmp+rename code paths.
By default the repo expects data in data/:
data/complaints.csvSource CSV, if you start from the raw CFPB export.data/complaints.jsonlStreaming-friendly newline-delimited JSON version of the dataset.
Large data files and generated artifacts are ignored by Git in this repo.
This project uses uv for dependency management.
uv syncThat creates or updates .venv/ with the project dependencies.
Use uv run to execute commands inside the project environment:
uv run python --versionIf you only have the CSV:
uv run python scripts/complaints_csv_to_jsonl.pyWith explicit paths:
uv run python scripts/complaints_csv_to_jsonl.py \
data/complaints.csv \
data/complaints.jsonlUseful flags:
--limit: convert only the first N rows--progress-every: print progress every N rows
Optional preprocessing step that shrinks the dataset and standardizes labels before the visualization run:
uv run python scripts/clean_complaints_jsonl.py \
--issue-map data/issue_map.json --dry-runAlways preview with --dry-run first — it scans without modifying anything
and prints the projected counts. When the numbers look right, drop --dry-run
to apply the changes (you'll be prompted to type yes, or pass --yes).
What the cleaner does:
- Filter — keeps only rows whose
ProductorSub-product(afterstrip().casefold()) containscredit reportingorstudent loan. - Date filter — drops complaints whose
Date receivedis before 2018 (or missing/malformed). Override with--min-year YYYY;--min-year 0disables the filter. - Deduplicate — drops later occurrences of the same
Complaint ID. - Rename — applies the
IssueandSub-issuemappings from the--issue-mapJSON file.
Issue-map file format:
{
"Issue": { "old label": "new label", "...": "..." },
"Sub-issue": { "old label": "new label", "...": "..." }
}Useful flags:
--dry-run: scan and report only; never writes.--out-path NEW.jsonl: write the cleaned output to a new file instead of rewriting in place. Preserves the original.--min-year YYYY: change or disable the date cutoff (default 2018;0disables).--limit N: stop after N scanned rows; only allowed with--dry-runor--out-path.--yes: skip the destructive-write confirmation prompt.
Two execution paths chosen automatically:
- In-place when the rename map is empty — uses a trailing-write-pointer
rewrite (
rb+, seek/write/truncate). Zero extra disk needed during the run. A crash mid-run leaves the file corrupt; recover by re-runningcomplaints_csv_to_jsonl.pyagainst the CSV. .tmp+atomic-rename when the rename map is non-empty — needs ~equal extra disk during the run, but is crash-safe (a partial.tmpis unlinked, the original survives).
To inspect random complaints from a large JSONL file:
uv run python scripts/view_random_jsonl.pyExamples:
uv run python scripts/view_random_jsonl.py --count 5
uv run python scripts/view_random_jsonl.py --interactive
uv run python scripts/view_random_jsonl.py --rawThe first run creates a sidecar index file (*.idx) so later random lookups are
fast.
This is the main workflow:
uv run python scripts/visualize_complaints.pyBy default it:
- reads
data/complaints.jsonl - filters to complaints where
ProductorSub-productcontainscredit reportingorstudent loan - writes plots and a generated summary to
artifacts/credit_student_analysis/
Useful options:
uv run python scripts/visualize_complaints.py data/complaints.jsonl \
--output-dir artifacts/credit_student_analysis \
--top-n 10 \
--progress-every 500000Optional smoke-test mode:
uv run python scripts/visualize_complaints.py \
--limit 10000 \
--output-dir /tmp/cfpb-smokeThe visualization run writes:
summary.mdcategory_share.pngus_state_complaints_map.pngcredit_reporting_monthly_trends.pngstudent_loan_monthly_trends.pngcredit_reporting_yearly_trends.pngstudent_loan_yearly_trends.pngcredit_reporting_top_issues.pngstudent_loan_top_issues.pngcredit_reporting_top_companies.pngstudent_loan_top_companies.pngcredit_reporting_top_states.pngstudent_loan_top_states.pngstudent_loan_subproducts.pngcredit_reporting_response_outcomes.pngstudent_loan_response_outcomes.png
The summary file includes:
- total matched complaint volume
- date range of usable complaints
- category dominance
- an overall US state complaint map with state acronyms
- issue concentration
- company concentration
- state concentration
- year-over-year and month-over-month spikes
- student-loan sub-product mix
- malformed JSON and date caveats
The visualization pipeline is intentionally streaming and aggregation-first:
- It never loads the full raw
jsonlfile into memory. - It scans the file once and updates counters as it goes.
- It keeps only compact aggregate results in memory for plotting.
- Plotting happens after the scan is complete.
Normalization rules:
ProductandSub-productare matched withstrip()andcasefold()- rows are included if either field contains
credit reportingorstudent loan - blank categorical fields are normalized to
Unknown - malformed JSON rows are skipped and counted
- missing or malformed
Date receivedvalues are skipped for time-series charts and counted in the summary
Run the test suite with:
uv run pytestThe tests cover:
- category normalization and filtering
- aggregation correctness
- malformed-row handling
- end-to-end CLI artifact generation
- The plotting code uses a seaborn theme and palette for consistent output.
- State counts are raw complaint totals, not population-adjusted rates.
- The generated charts are static PNGs rather than an interactive dashboard.
- For the full dataset, expect the visualization run to take time because it
scans the raw
jsonlfile directly on each run.