Skip to content

Large data with credgate

Bank, insurance and fintech data is usually too big for a data frame. This guide shows how to find and remove personal data in large tables with credgate, in Python, in R and on the command line. Every example uses synthetic data; runnable versions are in examples/.

The approach

  1. Scan first. scan_pii() tells you which columns hold personal data and how much, from a sample. It reads big tables one column at a time and samples exactly as it does in memory, so the report is the same at any size.
  2. Treat structured columns as a whole. A column that is a PAN or a mobile number (cust_pan, mobile) is best dropped, or tokenised as a column; it does not need detection row by row.
  3. Redact free text in batches. Notes, complaints and chat logs are redacted in batches (Arrow record batches, database chunks), so memory use does not grow with the data.

What works where

Python R Command line
In-memory columns pandas / Polars Series, Arrow arrays character vectors, Arrow arrays text files, standard input
Polars pl.col("notes").credgate.redact() - -
Arrow tables, datasets credgate.arrow, scan_pii(dataset) scan_pii(arrow::open_dataset(...)) -
Parquet larger than memory credgate.arrow.redact_parquet() redact_parquet() credgate redact --parquet
DuckDB SQL: cg_redact(notes), cg_has_pii(notes) redact_query(), scan_pii(dplyr::tbl(con, ...)) -
Other databases (DBI) - redact_query(), scan_pii() on dbplyr tables -
Whole databases (inventory) credgate.database.inventory_database() inventory_database() -
BigQuery (scan, inventory) credgate.bigquery bq_scan_pii(), bq_pii_inventory() -
Spark (scan, inventory) scan_pii(df), pii_inventory(df, ...) scan_pii(tbl), pii_inventory(tbl, ...) (sparklyr) -
Scan a file scan_pii() scan_pii() credgate scan file.parquet

Numbered tokens ([PAN_1]) are available for in-memory columns. Keyed tokens (Vault(key) in Python, cg_vault(key) in R) give the same token for the same identifier in every vault with the same key, so parallel workers agree and their vaults can be merged; tokenising inside Polars, DuckDB and Spark with them is planned. Until then, use redaction inside those engines.

Spark tables are sampled inside the cluster: a seeded random sample of up to sample rows (Spark has no stable row order, so not evenly spaced), of which only the sample reaches the driver. Redacting inside Spark is planned.

Python

pip install 'credgate[polars,duckdb]'      # or [arrow] for Parquet only
import credgate, polars as pl
import credgate.polars                       # adds .credgate to expressions

calls = pl.scan_parquet("calls.parquet")     # lazy: nothing loaded yet
credgate.scan_pii(calls)                     # one column at a time
calls.with_columns(pl.col("notes").credgate.redact(min_confidence=0.5)) \
     .drop("cust_pan", "mobile").sink_parquet("calls_safe.parquet")
import duckdb, credgate.duckdb

con = duckdb.connect()
credgate.duckdb.register(con, min_confidence=0.5)
con.sql("COPY (SELECT call_id, cg_redact(notes) AS notes FROM 'calls.parquet') "
        "TO 'calls_safe.parquet'")

Examples: pandas_scan_redact.py, polars_redact.py, duckdb_sql_redact.py, arrow_parquet_stream.py.

R

install.packages(c("arrow", "duckdb", "DBI", "dbplyr", "dplyr"))  # the ones you use
library(credgate)

scan_pii(arrow::open_dataset("calls.parquet"))
redact_parquet("calls.parquet", "calls_safe.parquet", columns = "notes", min_confidence = 0.5)

con <- DBI::dbConnect(duckdb::duckdb())
scan_pii(dplyr::tbl(con, dplyr::sql("SELECT * FROM 'calls.parquet'")))
redact_query(con, "SELECT call_id, notes FROM 'calls.parquet'", "calls_safe")

The same works on any database with a DBI driver (Postgres, SQL Server via odbc, Oracle, Snowflake, ...): redact_query() needs only DBI, and scan_pii() takes a dbplyr table. For example, Postgres:

con <- DBI::dbConnect(RPostgres::Postgres(), dbname = "lending", host = "db.internal")
scan_pii(dplyr::tbl(con, "loan_notes"))                     # needs dbplyr
redact_query(con, "SELECT id, notes FROM loan_notes", "loan_notes_safe")

Tested with DuckDB, SQLite and Postgres (a real Postgres server runs in the test suite). scan_pii() reads each column twice (to count, then to sample), so the sample matches the in-memory one only if the database returns rows in the same order both times, as these do for plain tables. Postgres and SQLite cannot write on a connection while a query is open on it, so with one connection redact_query() keeps redacted chunks in temporary files until the query has been read and then writes them in one transaction; give it a second connection (to_con) to write as it reads.

The vignette vignette("large-data", package = "credgate") walks through both. Examples: arrow_parquet.R, duckdb_dbplyr.R.

Command line

pip install 'credgate[arrow]'
credgate scan calls.parquet
credgate redact --parquet calls.parquet --column notes --min-confidence 0.5 -o calls_safe.parquet

Example: parquet_pipeline.sh.

Speed

On one core of a 2019 laptop credgate processes about 0.4 MB of text per second in Python and 0.2-0.3 MB in R (throughput). Detection is independent per row, so it scales with cores and machines. Redaction inside Spark and faster matching are on the roadmap.