Skip to content

Commit 859cd6f

Browse files
ghansemwojtyczkaCopilot
authored
Add built-in methods for PII detection (#486)
## Changes <!-- Summary of your changes that are easy to understand. Add screenshots when necessary --> This PR introduces the `pii` module which users can install to access built-in checks for Personally Identifiable Information (PII). These built-in checks use the [Presidio framework](https://github.com/microsoft/presidio). #### Installation Users install PII detection capabilities as an extras module with additional dependencies: ``` pip install databricks-labs-dqx[pii] ``` #### Usage Users can define checks using a new built-in function, `contains_pii`. ``` from databricks.labs.dqx.rule import DQRowRule from databricks.labs.dqx.pii.pii_detection_funcs import contains_pii checks = [ # PII detection check using stanford-deidentifier-base as a named entity recognizer DQRowRule( name="col_description_contains_pii", criticality="error", column="description", check_func=contains_pii ), ] ``` #### Configuration Users can configure the underlying model by passing parameters to `contains_pii`. This includes NLP entity recognition configuration which can be passed as a Python dictionary or as an `NLPEngineConfig`. In the future, we can add `NLPEngineConfig` values to support different named entity recognizers. **TO-DO:** - [x] Introduce methods to configure the underlying named entity recognizer - [x] Add tests - [x] Add documentation - [x] Update demos to use the built-in check ### Linked issues <!-- DOC: Link issue with a keyword: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved. See https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword --> Resolves #419 ### Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [x] manually tested - [x] added unit tests - [x] added integration tests --------- Co-authored-by: Marcin Wojtyczka <marcin.wojtyczka@databricks.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0b0943a commit 859cd6f

19 files changed

Lines changed: 995 additions & 241 deletions

.github/workflows/acceptance.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
with:
3939
cache: 'pip'
4040
cache-dependency-path: '**/pyproject.toml'
41-
python-version: '3.10'
41+
python-version: '3.11'
4242

4343
- name: Install hatch
4444
run: pip install hatch==1.9.4
@@ -90,7 +90,7 @@ jobs:
9090
with:
9191
cache: 'pip'
9292
cache-dependency-path: '**/pyproject.toml'
93-
python-version: '3.10'
93+
python-version: '3.11'
9494

9595
- name: Install hatch
9696
run: pip install hatch==1.9.4
@@ -122,7 +122,7 @@ jobs:
122122
with:
123123
cache: 'pip'
124124
cache-dependency-path: '**/pyproject.toml'
125-
python-version: '3.10'
125+
python-version: '3.11'
126126

127127
- name: Install hatch
128128
run: pip install hatch==1.9.4
@@ -156,7 +156,7 @@ jobs:
156156
with:
157157
cache: 'pip'
158158
cache-dependency-path: '**/pyproject.toml'
159-
python-version: '3.10'
159+
python-version: '3.11'
160160

161161
- name: Install hatch
162162
run: pip install hatch==1.9.4

.github/workflows/downstreams.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
with:
4040
cache: 'pip'
4141
cache-dependency-path: '**/pyproject.toml'
42-
python-version: '3.10'
42+
python-version: '3.11'
4343

4444
- name: Install toolchain
4545
run: |

.github/workflows/nightly.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
with:
3030
cache: 'pip'
3131
cache-dependency-path: '**/pyproject.toml'
32-
python-version: '3.10'
32+
python-version: '3.11'
3333

3434
- name: Install hatch
3535
run: pip install hatch==1.9.4
@@ -78,7 +78,7 @@ jobs:
7878
with:
7979
cache: 'pip'
8080
cache-dependency-path: '**/pyproject.toml'
81-
python-version: '3.10'
81+
python-version: '3.11'
8282

8383
- name: Install hatch
8484
run: pip install hatch==1.9.4
@@ -111,7 +111,7 @@ jobs:
111111
with:
112112
cache: 'pip'
113113
cache-dependency-path: '**/pyproject.toml'
114-
python-version: '3.10'
114+
python-version: '3.11'
115115

116116
- name: Install hatch
117117
run: pip install hatch==1.9.4

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
with:
2424
cache: 'pip'
2525
cache-dependency-path: '**/pyproject.toml'
26-
python-version: '3.10'
26+
python-version: '3.11'
2727

2828
- name: Build wheels
2929
run: |

demos/dqx_demo_pii_detection.py

Lines changed: 127 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,186 @@
11
# Databricks notebook source
22
# MAGIC %md
33
# MAGIC # Using DQX for PII Detection
4-
# MAGIC Increased regulation makes Databricks customers responsible for any Personally Identifiable Information (PII) stored in Unity Catalog. While [Lakehouse Monitoring](https://docs.databricks.com/aws/en/lakehouse-monitoring/data-classification#discover-sensitive-data) can identify sensitive data in-place, many customers need to proactively quarantine or anonymize PII before writing the data to Delta.
4+
# MAGIC Increased regulation makes Databricks customers responsible for any Personally Identifiable Information (PII) stored in Unity Catalog. Companies need to be able to perform PII detection for data at-rest and in-transit to proactively quarantine or anonymize PII before persisting the data.
55
# MAGIC
6-
# MAGIC [Databricks Labs' DQX project](https://databrickslabs.github.io/dqx/) provides in-flight data quality monitoring for Spark `DataFrames`. Customers can apply checks, get row-level metadata, and quarantine failing records. Workloads can use DQX's built-in checks or custom user-defined functions.
7-
# MAGIC
8-
# MAGIC In this notebook, we'll use DQX with a custom function to detect PII in JSON strings.
6+
# MAGIC DQX provides in-flight data quality monitoring for Spark `DataFrames`. You can apply checks, get row-level metadata, and quarantine failing records. Workloads can also use DQX's built-in functions to check `DataFrames` for PII.
97

108
# COMMAND ----------
119

1210
# MAGIC %md
13-
# MAGIC ## Prerequisites
14-
# MAGIC This notebook uses [Presidio](https://microsoft.github.io/presidio/) to detect PII in strings. To run this notebook:
15-
# MAGIC - Use DBR 15.4LTS
16-
# MAGIC - Install [SpaCy](https://spacy.io/usage/models#download) as a cluster-scoped library
11+
# MAGIC # Install DQX with PII extras
12+
# MAGIC
13+
# MAGIC To enable PII detection quality checking, DQX has to be installed with `pii` extras:
14+
# MAGIC
15+
# MAGIC `%pip install databricks-labs-dqx[pii]`
1716

1817
# COMMAND ----------
1918

2019
dbutils.widgets.text("test_library_ref", "", "Test Library Ref")
2120

2221
if dbutils.widgets.get("test_library_ref") != "":
23-
%pip install '{dbutils.widgets.get("test_library_ref")}'
22+
%pip install 'databricks-labs-dqx[pii] @ {dbutils.widgets.get("test_library_ref")}'
2423
else:
25-
%pip install databricks-labs-dqx
26-
27-
%pip install presidio_analyzer numpy==1.23.5
24+
%pip install databricks-labs-dqx[pii]
2825

2926
# COMMAND ----------
3027

3128
dbutils.library.restartPython()
3229

3330
# COMMAND ----------
3431

35-
import json
36-
import pandas as pd
37-
38-
from pyspark.sql.functions import concat_ws, col, lit, pandas_udf
39-
from pyspark.sql import Column
40-
from presidio_analyzer import AnalyzerEngine
4132
from databricks.sdk import WorkspaceClient
4233
from databricks.labs.dqx.engine import DQEngine
4334
from databricks.labs.dqx.rule import DQRowRule
44-
from databricks.labs.dqx.check_funcs import make_condition
35+
from databricks.labs.dqx.pii.nlp_engine_config import NLPEngineConfig
36+
from databricks.labs.dqx.pii.pii_detection_funcs import does_not_contain_pii
4537

4638
# COMMAND ----------
4739

4840
# MAGIC %md
49-
# MAGIC ## Creating the Presidio analyzer
50-
# MAGIC First, we'll use Presidio's `AnalyzerEngine` to define a function that checks values for PII. For any PII detected, the `entity_mapping` will contain the type of PII identified and a confidence score.
41+
# MAGIC ## Detecting PII with DQX
42+
# MAGIC DQX supports built-in PII detection using Presidio's `AnalyzerEngine` to define a function that checks values for PII. For any PII detected, the `entity_mapping` will contain the type of PII identified and a confidence score.
5143

5244
# COMMAND ----------
5345

54-
# Create the Presidio analyzer:
55-
analyzer = AnalyzerEngine()
56-
57-
# Get the list of entities to download the model:
58-
entities = analyzer.get_supported_entities()
59-
60-
# Create a wrapper function to generate the entity mapping results:
61-
def get_entity_mapping(data: str) -> str | None:
62-
if data:
63-
# Run the Presidio analyzer to detect PII in the string:
64-
results = analyzer.analyze(
65-
text=data,
66-
entities=["PERSON", "EMAIL_ADDRESS"],
67-
language='en',
68-
score_threshold=0.5,
69-
)
70-
if results != []:
71-
output = []
72-
# Validate and return the results:
73-
for result in results:
74-
# Ignore if the result is low confidence:
75-
if result.score < 0.5:
76-
continue
77-
# Append the result to the output:
78-
output.append({
79-
"entity_type": result.entity_type,
80-
"start": int(result.start),
81-
"end": int(result.end),
82-
"score": float(result.score),
83-
})
84-
if output != []:
85-
# Return the results as JSON:
86-
return json.dumps(output)
87-
return None
46+
# Define the DQX rule:
47+
checks = [
48+
DQRowRule(
49+
criticality="error",
50+
check_func=does_not_contain_pii,
51+
column="val",
52+
name="does_not_contain_pii",
53+
)
54+
]
55+
56+
# Initialize the DQX engine:
57+
dq_engine = DQEngine(WorkspaceClient())
58+
59+
# Create some sample data:
60+
data = [
61+
["My name is John Smith"],
62+
["The sky is blue, road runner"],
63+
["Jane Smith sent an email to sara@info.com"],
64+
[None],
65+
]
66+
df = spark.createDataFrame(data, "val string")
67+
68+
# Run the checks and display the output:
69+
checked_df = dq_engine.apply_checks(df, checks)
70+
display(checked_df)
8871

8972
# COMMAND ----------
9073

9174
# MAGIC %md
92-
# MAGIC ## Creating a Pandas UDF
93-
# MAGIC We can call `get_entity_mapping` on DataFrame rows using a Pandas user-defined function. This provides good performance with batched execution over the arriving records.
75+
# MAGIC ## Configuring the PII detection settings
76+
# MAGIC DQX supports several configurable settings which control PII detection:
77+
# MAGIC - `threshold` controls the specificity of the PII detection (higher values give more specificity with less sensitivity)
78+
# MAGIC - `entities` specifies which [entity types](https://microsoft.github.io/presidio/supported_entities/) are marked as PII
79+
# MAGIC - `language` sets the detection language
80+
# MAGIC - `nlp_engine_config` sets various properties of the Presidio analyzer's [named entity recognition model](https://microsoft.github.io/presidio/samples/python/ner_model_configuration/)
9481

9582
# COMMAND ----------
9683

97-
# Register a pandas UDF to run the analyzer:
98-
@pandas_udf('string')
99-
def contains_pii(batch: pd.Series) -> pd.Series:
100-
# Apply `get_entity_mapping` to each value:
101-
return batch.map(get_entity_mapping)
84+
# Use a built-in NLP configuration for detecting PII:
85+
nlp_engine_config = NLPEngineConfig.SPACY_MEDIUM
86+
87+
checks = [
88+
# Define a PII check with a lower threshold (more sensitivity):
89+
DQRowRule(
90+
criticality="error",
91+
check_func=does_not_contain_pii,
92+
check_func_kwargs={"threshold": 0.5},
93+
column="val",
94+
name="does_not_contain_pii_lower_threshold",
95+
),
96+
# Define a PII check with a subset of named entities:
97+
DQRowRule(
98+
criticality="error",
99+
check_func=does_not_contain_pii,
100+
check_func_kwargs={
101+
"entities": ["EMAIL_ADDRESS"],
102+
},
103+
column="val",
104+
name="contains_email_address_data",
105+
),
106+
# Define a PII check with a built-in named-entity recognizer (SpaCy medium):
107+
DQRowRule(
108+
criticality="error",
109+
check_func=does_not_contain_pii,
110+
check_func_kwargs={
111+
"entities": ["PERSON", "LOCATION"],
112+
"nlp_engine_config": NLPEngineConfig.SPACY_MEDIUM
113+
},
114+
column="val",
115+
name="contains_person_or_address_data",
116+
),
117+
# Define a PII check with a built-in named-entity recognizer (SpaCy medium):
118+
DQRowRule(
119+
criticality="error",
120+
check_func=does_not_contain_pii,
121+
check_func_kwargs={
122+
"entities": ["PERSON", "LOCATION"],
123+
"nlp_engine_config": NLPEngineConfig.SPACY_MEDIUM
124+
},
125+
column="val",
126+
name="contains_person_or_address_data",
127+
),
128+
]
129+
130+
# Initialize the DQX engine:
131+
dq_engine = DQEngine(WorkspaceClient())
132+
133+
# Create some sample data:
134+
data = [
135+
["My name is John Smith and I live at 123 Main St New York, NY 07008"],
136+
["The sky is blue, road runner"],
137+
["Jane Smith sent an email to sara@info.com"],
138+
[None],
139+
]
140+
df = spark.createDataFrame(data, "val string")
141+
142+
# Run the checks and display the output:
143+
checked_df = dq_engine.apply_checks(df, checks)
144+
display(checked_df)
102145

103146
# COMMAND ----------
104147

105148
# MAGIC %md
106-
# MAGIC ## Making and applying a DQX condition
107-
# MAGIC Once our Presidio algorithm can be called as a Spark UDF, we can use DQX's `make_condition` to implement a custom check that identifies PII and generates row-level metadata about the `json` keys and types of PII identified.
149+
# MAGIC ## Using a custom named entity recognizer
150+
# MAGIC DQX supports custom named entity recognizers passed as Python dictionaries. All dependencies must be pre-loaded for use with DQX's built-in PII detection checks.
151+
# MAGIC
152+
# MAGIC ***WARNING:** Using custom named entity recognizers can significantly degrade performance of quality checking at scale. Sample data or use smaller models for best performance. Run checks on non-serverless compute when using large named entity recognizers.*
108153

109154
# COMMAND ----------
110155

111-
def does_not_contain_pii(column: str) -> Column:
112-
# Define a PII detection expression calling the pandas UDF:
113-
pii_info = contains_pii(col(column))
114-
115-
# Return the DQX condition that uses the PII detection expression:
116-
return make_condition(
117-
pii_info.isNotNull(),
118-
concat_ws(
119-
' ',
120-
lit(column),
121-
lit('contains pii with the following info:'),
122-
pii_info
123-
),
124-
f'{column}_contains_pii'
125-
)
156+
# Define the NLP configuration:
157+
nlp_engine_config = {
158+
"nlp_engine_name": "spacy",
159+
"models": [{"lang_code": "en", "model_name": "en_core_web_md"}]
160+
}
126161

127-
# Define the DQX rule:
128162
checks = [
129-
DQRowRule(criticality='error', check_func=does_not_contain_pii, column='val')
163+
# Define a PII check with a custom named-entity recognizer (Stanford De-Identifier Base):
164+
DQRowRule(
165+
criticality="error",
166+
check_func=does_not_contain_pii,
167+
check_func_kwargs={"nlp_engine_config": nlp_engine_config},
168+
column="val",
169+
name="contains_pii_custom_recognizer",
170+
),
130171
]
131172

132173
# Initialize the DQX engine:
133174
dq_engine = DQEngine(WorkspaceClient())
134175

135176
# Create some sample data:
136177
data = [
137-
['My name is John Smith'],
138-
['The sky is blue, road runner'],
139-
['Jane Smith sent an email to sara@info.com']
178+
["My name is John Smith and I live at 123 Main St New York, NY 07008"],
179+
["The sky is blue, road runner"],
180+
["Jane Smith sent an email to sara@info.com"],
181+
[None],
140182
]
141-
df = spark.createDataFrame(data, 'val string')
183+
df = spark.createDataFrame(data, "val string")
142184

143185
# Run the checks and display the output:
144186
checked_df = dq_engine.apply_checks(df, checks)

demos/dqx_streaming_demo_diy.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@
180180

181181
from databricks.labs.dqx.engine import DQEngine
182182
from databricks.sdk import WorkspaceClient
183-
from pyspark.sql import DataFrame
184183

185184
dq_engine = DQEngine(WorkspaceClient())
186185

0 commit comments

Comments
 (0)