|
1 | 1 | # Databricks notebook source |
2 | 2 | # MAGIC %md |
3 | 3 | # 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. |
5 | 5 | # 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. |
9 | 7 |
|
10 | 8 | # COMMAND ---------- |
11 | 9 |
|
12 | 10 | # 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]` |
17 | 16 |
|
18 | 17 | # COMMAND ---------- |
19 | 18 |
|
20 | 19 | dbutils.widgets.text("test_library_ref", "", "Test Library Ref") |
21 | 20 |
|
22 | 21 | 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")}' |
24 | 23 | else: |
25 | | - %pip install databricks-labs-dqx |
26 | | - |
27 | | -%pip install presidio_analyzer numpy==1.23.5 |
| 24 | + %pip install databricks-labs-dqx[pii] |
28 | 25 |
|
29 | 26 | # COMMAND ---------- |
30 | 27 |
|
31 | 28 | dbutils.library.restartPython() |
32 | 29 |
|
33 | 30 | # COMMAND ---------- |
34 | 31 |
|
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 |
41 | 32 | from databricks.sdk import WorkspaceClient |
42 | 33 | from databricks.labs.dqx.engine import DQEngine |
43 | 34 | 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 |
45 | 37 |
|
46 | 38 | # COMMAND ---------- |
47 | 39 |
|
48 | 40 | # 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. |
51 | 43 |
|
52 | 44 | # COMMAND ---------- |
53 | 45 |
|
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) |
88 | 71 |
|
89 | 72 | # COMMAND ---------- |
90 | 73 |
|
91 | 74 | # 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/) |
94 | 81 |
|
95 | 82 | # COMMAND ---------- |
96 | 83 |
|
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) |
102 | 145 |
|
103 | 146 | # COMMAND ---------- |
104 | 147 |
|
105 | 148 | # 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.* |
108 | 153 |
|
109 | 154 | # COMMAND ---------- |
110 | 155 |
|
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 | +} |
126 | 161 |
|
127 | | -# Define the DQX rule: |
128 | 162 | 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 | + ), |
130 | 171 | ] |
131 | 172 |
|
132 | 173 | # Initialize the DQX engine: |
133 | 174 | dq_engine = DQEngine(WorkspaceClient()) |
134 | 175 |
|
135 | 176 | # Create some sample data: |
136 | 177 | 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], |
140 | 182 | ] |
141 | | -df = spark.createDataFrame(data, 'val string') |
| 183 | +df = spark.createDataFrame(data, "val string") |
142 | 184 |
|
143 | 185 | # Run the checks and display the output: |
144 | 186 | checked_df = dq_engine.apply_checks(df, checks) |
|
0 commit comments