Skip to content

Pseudonymization

SMDT provides a built-in Pseudonymizer to process a source database into a destination database, applying hashing to identifiers and redaction to free-text fields. The same real identifier always maps to the same pseudonym, so network graphs and cross-table joins survive the transformation intact.

Quickstart

python
from smdt.pseudonymizer import Pseudonymizer, PseudonymizeConfig, Algorithm, DEFAULT_POLICY
from smdt.config import PseudonymizationVariables

pseudo_vars = PseudonymizationVariables()

cfg = PseudonymizeConfig(
    src_db_name="my_dataset",
    dst_db_name="my_dataset_pseudo",
    pepper=pseudo_vars.pepper,
    algorithm=Algorithm.SHA256,
    ask_reinit=True,
    chunk_rows=5_000,
)

Pseudonymizer(cfg, DEFAULT_POLICY).run()

That is all you need. The default policy handles the standard SMDT schema: it hashes all identifier columns, redacts body and bio columns, blanks profile image URLs, and copies everything else unchanged.

How It Works

The pseudonymizer reads from your source database and writes to a new destination database with the same schema. Each column in each table gets one of five treatments:

ActionEffectExample
HashReplaced with a pepper-keyed SHA-256 hashaccount_id, username, post_id
RedactMentions, emails, and URLs are scrubbedbody, bio
BlankSet to NULLprofile_image_url
DropColumn removed entirelyNot used by default
KeepCopied unchanged (default)created_at, platform, counts

Customizing the Policy

If you need different per-column rules, create a custom PseudonymPolicy:

python
from smdt.pseudonymizer import PseudonymPolicy, DEFAULT_POLICY

custom_policy = PseudonymPolicy(
    hash_cols={
        "accounts": {"account_id", "username"},
        "posts": {"account_id", "post_id", "conversation_id"},
    },
    redact_cols={
        "posts": {"body"},
        "accounts": {"bio"},
    },
    blank_cols={
        "accounts": {"profile_image_url", "location"},
    },
    drop_cols={},
)

Pseudonymizer(cfg, custom_policy).run()

When a column appears in multiple action sets, priority is: Drop > Hash > Redact > Blank > Keep.

Optional: PII Detection with Presidio

By default, free-text redaction uses a dependency-free regex engine that catches @mentions, emails, and URLs. Installing the pii extra enables Presidio-based detection of broader PII: phone numbers, credit cards, and personal names. This is opt-in and requires an NLP model.

bash
pip install 'smdt[pii]'
python -m spacy download en_core_web_lg
python
from smdt.pseudonymizer import PiiPolicy, PiiAction, PiiRule, DEFAULT_PII_POLICY

pii_policy = DEFAULT_PII_POLICY  # covers accounts.bio, posts.body, communities.bio

cfg = PseudonymizeConfig(
    src_db_name="my_dataset",
    dst_db_name="my_dataset_pseudo",
    pepper=pseudo_vars.pepper,
    pii_policy=pii_policy,
    nlp_configuration={
        "nlp_engine_name": "spacy",
        "models": [{"lang_code": "en", "model_name": "en_core_web_lg"}],
    },
)

Pseudonymizer(cfg, DEFAULT_POLICY).run()

Each entity type gets its own action: mentions are hashed for consistency with identifiers, phone numbers and credit cards become [PHONE_NUMBER] and [CREDIT_CARD], URLs are reduced to their domain. See the PiiPolicy documentation for custom rules.

Optional: Parallel Processing

For large tables with substantial free-text content, the CPU-bound hashing and redaction can be distributed across worker processes. Database reads and writes stay in the main process.

python
cfg = PseudonymizeConfig(
    src_db_name="my_dataset",
    dst_db_name="my_dataset_pseudo",
    pepper=pseudo_vars.pepper,
    num_workers=8,
    transform_chunk_size=500,
)
Optional: GDPR Erasure

Eraser handles deletion requests by recomputing the pseudonym for a given real identity and locating their data. It supports hard deletion or in-place scrubbing across the source database, the pseudonymized database, or both.

python
from smdt.pseudonymizer import Eraser, ErasureTarget, ErasureMode

eraser = Eraser(
    targets=[
        ErasureTarget(db_name="my_dataset", mode=ErasureMode.DELETE,
                      is_pseudonymized=False),
        ErasureTarget(db_name="my_dataset_pseudo", mode=ErasureMode.SCRUB,
                      is_pseudonymized=True),
    ],
    pepper=pseudo_vars.pepper,
)

report = eraser.erase("real_account_id_123", identity_column="account_id")

Scrubbing nulls personal columns but keeps posts in place so other users' replies and interactions are not orphaned. There is no reverse mapping table; erasure works by forward recomputation only.

Prerequisites

  • A database ingested with SMDT (see Standardizing Twitter v2)
  • A PEPPER environment variable set (the secret used for keyed hashing)
  • PiiPolicy users also need pip install 'smdt[pii]' and a spaCy model