The proliferation of Large Language Models (LLMs) heightens challenges in protecting Personal Identifiable Information (PII), particularly Quasi-Identifiers (QIs), in unstructured text. QIs enable re-identification when combined and pose significant privacy risks, highlighted by their use in security verification. Current approaches face limitations: large LLMs offer flexibility for detecting diverse QIs but are often hindered by high computational costs, while traditional supervised NER models require domain-specific labeled data and fail to generalize to heterogeneous, unseen QI types. Furthermore, evaluating QI identification methods is hampered by the lack of diverse benchmarks. To address this need for evaluation resources, we present the Quasi-Identifier Benchmark (QIB), a new corpus with 1750 examples across 35 diverse QI categories (e.g., personal preferences, security answers) designed to assess model robustness against QI heterogeneity. To facilitate the application of flexible identification methods on such diverse data, we also introduce ZINK (Zero-shot INK), a Python package providing a unified framework for applying existing zero-shot NER models to QI identification and anonymization, simplifying model integration and offering configurable redaction and replacement.
Evaluation using ZINK on QIB shows strong performance, achieving an F4-score of 0.9206. This result outperforms both supervised models like BERT (0.6109) and paid large language models like GPT-4-Nano (0.9007), while remaining competitive with top-tier models like GPT-4 (0.9726). QIB and ZINK provide valuable resources enabling standardized evaluation and development of flexible, practical solutions for quasi-identifier anonymization in text.
Install the package using uv or pip.
CPU Support (Recommended for most users):
uv add "zink[cpu]"
# or
pip install "zink[cpu]"GPU Support:
pip install "zink[gpu]"Get started with ZINK in just a few lines of code. The redact function replaces identified entities with [LABEL]_REDACTED.
import zink as zn
text = "John works as a doctor and plays football after work and drives a toyota."
labels = ("person", "profession", "sport", "car")
result = zn.redact(text, labels)
print(result.anonymized_text)Output:
person_REDACTED works as a profession_REDACTED and plays sport_REDACTED after work and drives a car_REDACTED.
Instead of simple redaction, you can replace entities with realistic synthetic data using the replace function. ZINK uses the Faker library to generate context-aware replacements.
import zink as zn
text = "John Doe dialled his mother at 992-234-3456 and then went out for a walk."
labels = ("person", "phone number", "relationship")
result = zn.replace(text, labels)
print(result.anonymized_text)Possible Output:
Warren Buffet dialled his Uncle at 2347789287 and then went out for a walk.
You can provide your own dictionary of replacements for specific labels. This is useful for consistent mapping or using domain-specific datasets.
import zink as zn
text = "Melissa works at Google and drives a Tesla."
labels = ("person", "company", "car")
custom_replacements = {
"person": "Alice",
"company": "OpenAI",
"car": ("Honda", "Toyota")
}
result = zn.replace_with_my_data(text, labels, user_replacements=custom_replacements)
print(result.anonymized_text)Possible Output:
Alice works at OpenAI and drives a Honda.
Protect sensitive data in your RAG pipelines or API calls using the @zink.shield decorator. It automatically anonymizes inputs before they reach the function and re-identifies the output, creating a secure "shield" around your logic.
import zink as zn
# This mock function simulates calling an external API (like OpenAI or Gemini)
@zn.shield(target_arg='prompt', labels=('person', 'company'))
def call_sensitive_api(prompt: str):
# The prompt received here is already anonymized.
# e.g., "Report for person_REDACTED from company_REDACTED."
return f"Analysis for {prompt} is complete."
# The original, sensitive text
sensitive_data = "Report for John Doe from Acme Inc."
# Call the function normally. The decorator handles anonymization and re-identification.
final_result = call_sensitive_api(prompt=sensitive_data)
print(final_result)Output:
Analysis for John Doe from Acme Inc. is complete.
To ensure consistent redaction across multiple sessions or calls, you can use the numbered_entities=True flag. This will automatically use a persistent mapping file stored in your home directory (~/.zink/mapping.json).
import zink as zn
# First call: Generates IDs and saves them to default mapping file
text1 = "My name is Alice."
result1 = zn.redact(text1, labels=["person"], numbered_entities=True)
print(result1.anonymized_text)
# Output: person_1234_REDACTED ...
# Second call: Reuses the SAME ID for 'Alice'
text2 = "Alice is here again."
result2 = zn.redact(text2, labels=["person"], numbered_entities=True)
print(result2.anonymized_text)
# Output: person_1234_REDACTED ...
# Check where the mapping file is stored
print(zn.where_mapping_file())
# Clear the mapping file to start fresh
zn.refresh_mapping_file()You can run zink easily using Docker. The image comes with the model pre-installed and exposes a REST API.
Run this command in the root of the repository:
docker build -t zink .Start the API server on port 8000:
docker run --rm -p 8000:8000 zinkTip: If port 8000 is already in use (e.g., you see a "Bind for 0.0.0.0:8000 failed" error), map it to a different port like 8001:
docker run --rm -p 8001:8000 zink
Once the container is running, you can send requests to the /redact endpoint.
Example Request:
curl -X POST "http://localhost:8000/redact" \
-H "Content-Type: application/json" \
-d '{"text": "John Doe lives in New York.", "labels": ["person", "location"]}'(Note: If you used port 8001, replace localhost:8000 with localhost:8001)
Example Response:
{
"original_text": "John Doe lives in New York.",
"anonymized_text": "<PERSON> lives in <LOCATION>.",
"replacements": [
{
"label": "person",
"original": "John Doe",
"pseudonym": "<PERSON>",
"start": 0,
"end": 8,
"score": 0.99
},
{
"label": "location",
"original": "New York",
"pseudonym": "<LOCATION>",
"start": 18,
"end": 26,
"score": 0.99
}
]
}To verify the installation, you can run the test suite inside the container:
docker run --rm zink pytest zink/testsGLiNER is a Named Entity Recognition (NER) model capable of identifying any entity type using a bidirectional transformer encoder (BERT-like). It provides a practical alternative to traditional NER models, which are limited to predefined entities, and Large Language Models (LLMs) that, despite their flexibility, are costly and large for resource-constrained scenarios.
NuNerZero is a compact, zero-shot Named Entity Recognition model that leverages the robust GLiNER architecture for efficient token classification. It requires lower-cased labels and processes inputs as a concatenation of entity types and text, enabling it to detect arbitrarily long entities.
Faker & Training Data
Zink leverages both the Faker library and the extensive training data from the GLiNER model to generate realistic, synthetic replacements for sensitive information.
- GLiNER Training Data: Access to thousands of label categories from the model's training set for diverse and accurate replacements.
- Dynamic Data Generation: Faker generates context-aware values (e.g., names, addresses).
- Location Handling: Swaps countries/cities with valid alternatives.
- Date Replacement: Handles various date formats intelligently.
- Roles: Differentiates between roles (e.g., doctor vs. patient) for appropriate naming.
Here is a comparison of ZINK against other models on Quasi Identifier Benchmark ([QIB])(https://huggingface.co/datasets/deepanwa/QIB)
| Model | Overall Recall | Overall Precision | Overall F4_SCORE | True Positives (TP) | False Negatives (FN) | Total Redaction Markers |
|---|---|---|---|---|---|---|
| gpt_41_nano | 0.8971 | 0.962 | 0.9007 | 1570 | 180 | 1632 |
| gpt_41 | 0.9726 | 0.9737 | 0.9726 | 1702 | 48 | 1748 |
| zink_single | 0.9 | 0.8858 | 0.8992 | 1575 | 175 | 1778 |
| zink_topic | 0.9126 | 0.6502 | 0.8914 | 1597 | 153 | 2456 |
| zink_human (run 1) | 0.9371 | 0.6597 | 0.9145 | 1640 | 110 | 2486 |
| zink_human (run 2) | 0.9446 | 0.6544 | 0.9206 | 1653 | 97 | 2526 |
| tars_topic | 0.5983 | 0.762 | 0.6059 | 1047 | 703 | 1374 |
| bert | 0.628 | 0.4255 | 0.6109 | 1099 | 651 | 2583 |
To run the tests, navigate to the project directory and execute:
pytestContributions are welcome! Please feel free to submit pull requests or open issues to suggest improvements or report bugs.
- Fork the repository.
- Create a new branch:
git checkout -b feature/your-feature - Make your changes.
- Commit your changes:
git commit -m 'Add your feature' - Push to the branch:
git push origin feature/your-feature - Submit a pull request.
If you are using this package for your work/research, use the below citation:
Wadhwa, D. (2025). ZINK: Zero-shot anonymization in unstructured text. (v0.2.1). Zenodo. https://doi.org/10.5281/zenodo.15035072
This project is licensed under the Apache 2.0 License.