# Data Anonymization and Privacy Compliance

**Last updated:** 2026-02
**Status:** GA
**Category:** Data Engineering for AI

---

## Introduksjon

Personvern og databeskyttelse er fundamentale krav for enhver AI-losning som behandler personopplysninger. GDPR (og den norske Personopplysningsloven) stiller strenge krav til hvordan persondata samles inn, behandles og beskyttes. For AI-systemer er dette spesielt utfordrende: ML-modeller kan utilsiktet memorere persondata fra treningsdatasettet, og RAG-systemer kan eksponere sensitiv informasjon i svar.

Microsoft tilbyr flere verktoy for personvernbeskyttelse: Azure Language PII-deteksjon for a identifisere og maskere personopplysninger, Microsoft Purview for dataklassifisering og governance, og SmartNoise for differensiell personvern. Disse verktoyene kan integreres i datapipelines i Fabric for a sikre at AI-modeller trenes pa korrekt anonymiserte data.

For norsk offentlig sektor, som er underlagt bade GDPR, Personopplysningsloven og sektorspesifikke krav (f.eks. Helseregisterloven, Politiregisterloven), er systematisk anonymisering og personvernbeskyttelse ikke bare god praksis -- det er lovpalagt.

---

## Differential Privacy Techniques

### Hva er differensiell personvern?

Differensiell personvern (DP) garanterer matematisk at ingen enkeltperson kan identifiseres fra resultatet av en dataanalyse. Prinsippet: tilforing av kontrollert stoy gjor det umulig a avgjore om en spesifikk person er i datasettet.

```
Formell definisjon:
For alle datasett D1 og D2 som skiller seg med maks 1 rad,
og alle mulige resultater S:

  Pr[M(D1) i S] <= e^epsilon * Pr[M(D2) i S]

epsilon (privacy budget): Lavere = sterkere personvern, mer stoy
```

### Privacy Budget (epsilon)

| Epsilon | Personvernniva | Bruksomrade |
|---------|---------------|-------------|
| 0.1 | Svart sterkt | Helsedata, sensitiv forskning |
| 1.0 | Sterkt | Generell offentlig statistikk |
| 3.0 | Moderat | Intern analyse, dashboards |
| 10.0 | Svakt | Testing, lav-risiko data |

### SmartNoise-implementering

```python
# SmartNoise - Microsoft/OpenDP-prosjektet for differensiell personvern
# pip install opendp smartnoise-sql

from opendp.measurements import make_laplace
from opendp.domains import atom_domain
from opendp.metrics import absolute_distance

def dp_count(true_count: int, epsilon: float = 1.0) -> float:
    """
    Legg til Laplace-stoy for differensielt privat telling.
    """
    sensitivity = 1  # En person kan endre tellingen med maks 1
    scale = sensitivity / epsilon

    import numpy as np
    noise = np.random.laplace(0, scale)
    return max(0, true_count + noise)  # Aldri negativ telling

def dp_mean(values, epsilon: float = 1.0, lower_bound: float = 0, upper_bound: float = 100):
    """
    Beregn differensielt privat gjennomsnitt.
    """
    import numpy as np
    n = len(values)
    true_mean = np.mean(values)

    sensitivity = (upper_bound - lower_bound) / n
    scale = sensitivity / epsilon

    noise = np.random.laplace(0, scale)
    return true_mean + noise

# Eksempel: Privat gjennomsnitt av inntektsdata
incomes = [450000, 520000, 380000, 620000, 490000]
private_mean = dp_mean(incomes, epsilon=1.0, lower_bound=0, upper_bound=2000000)
print(f"Differensielt privat gjennomsnitt: {private_mean:,.0f} NOK")
```

### Differensiell personvern i ML-trening

```python
# DP-SGD (Differentially Private Stochastic Gradient Descent)
# For trening av modeller med personverngarantier

# Med opacus (PyTorch)
# pip install opacus

"""
from opacus import PrivacyEngine

model = YourModel()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

privacy_engine = PrivacyEngine()
model, optimizer, dataloader = privacy_engine.make_private_with_epsilon(
    module=model,
    optimizer=optimizer,
    data_loader=dataloader,
    epochs=10,
    target_epsilon=3.0,  # Privacy budget
    target_delta=1e-5,
    max_grad_norm=1.0
)

# Tren som vanlig - opacus handterer stoy-tilforing automatisk
for epoch in range(10):
    for batch in dataloader:
        optimizer.zero_grad()
        loss = criterion(model(batch), labels)
        loss.backward()
        optimizer.step()

# Sjekk faktisk privacy-forbruk
epsilon = privacy_engine.get_epsilon(delta=1e-5)
print(f"Faktisk epsilon: {epsilon:.2f}")
"""
```

---

## K-Anonymity and L-Diversity

### K-Anonymitet

K-anonymitet sikrer at hver kombinasjon av quasi-identifikatorer forekommer i minst k rader:

```python
def check_k_anonymity(df, quasi_identifiers: list, k: int = 5) -> dict:
    """
    Sjekk om et datasett oppfyller k-anonymitet.

    Args:
        df: DataFrame
        quasi_identifiers: Kolonner som kan brukes til re-identifisering
        k: Minimum gruppestorrelse
    """
    # Grupper etter quasi-identifikatorer
    groups = df.groupBy(quasi_identifiers).count()

    # Finn grupper med faerre enn k elementer
    violating = groups.filter(F.col("count") < k)
    total_groups = groups.count()
    violating_groups = violating.count()

    min_group_size = groups.agg(F.min("count")).collect()[0][0]

    return {
        "k_anonymous": violating_groups == 0,
        "k_value": k,
        "total_groups": total_groups,
        "violating_groups": violating_groups,
        "min_group_size": min_group_size,
        "recommendation": f"Oek generalisering" if min_group_size < k else "OK"
    }

# Sjekk k-anonymitet for helsedatasett
result = check_k_anonymity(
    df_health_data,
    quasi_identifiers=["age_group", "postal_area", "gender"],
    k=5
)
```

### Generaliseringsstrategier for k-anonymitet

```python
def generalize_for_k_anonymity(df, generalizations: dict):
    """
    Generaliser quasi-identifikatorer for a oppna k-anonymitet.

    Args:
        generalizations: {kolonne: generaliseringsfunksjon}
    """
    result = df
    for col_name, gen_func in generalizations.items():
        result = result.withColumn(col_name, gen_func(F.col(col_name)))
    return result

# Generaliseringsfunksjoner
generalizations = {
    # Alder -> aldersgruppe (5-aarsintervaller)
    "age": lambda c: (F.floor(c / 5) * 5).cast("int"),

    # Postnummer -> postomrade (forste 2 siffer)
    "postal_code": lambda c: F.substring(c, 1, 2),

    # Fodselsdato -> fodeselsar
    "birth_date": lambda c: F.year(c),

    # Kommune -> fylke
    "municipality": lambda c: F.substring(c, 1, 2)  # Forste 2 siffer = fylke
}

df_generalized = generalize_for_k_anonymity(df_sensitive, generalizations)

# Verifiser
result = check_k_anonymity(df_generalized, ["age", "postal_code", "gender"], k=5)
print(f"K-anonym: {result['k_anonymous']}, Min gruppestorrelse: {result['min_group_size']}")
```

### L-Diversitet

L-diversitet utvider k-anonymitet ved a kreve at sensitive attributter har minst l forskjellige verdier i hver gruppe:

```python
def check_l_diversity(df, quasi_identifiers: list, sensitive_column: str, l: int = 3):
    """
    Sjekk om et datasett oppfyller l-diversitet.
    """
    # Tell unike verdier av sensitiv attributt per gruppe
    diversity = df.groupBy(quasi_identifiers).agg(
        F.countDistinct(sensitive_column).alias("distinct_sensitive"),
        F.count("*").alias("group_size")
    )

    violating = diversity.filter(F.col("distinct_sensitive") < l)
    min_diversity = diversity.agg(F.min("distinct_sensitive")).collect()[0][0]

    return {
        "l_diverse": violating.count() == 0,
        "l_value": l,
        "min_diversity": min_diversity,
        "violating_groups": violating.count()
    }

# Sjekk l-diversitet for diagnosekoder
result = check_l_diversity(
    df_health_data,
    quasi_identifiers=["age_group", "postal_area"],
    sensitive_column="diagnosis_code",
    l=3
)
```

---

## PII Detection and Masking

### Azure Language PII Detection

Azure Language tilbyr avansert PII-deteksjon med stotte for 50+ kategorier:

```python
from azure.ai.textanalytics import TextAnalyticsClient
from azure.core.credentials import AzureKeyCredential

def detect_and_redact_pii(text: str, endpoint: str, key: str,
                          categories_to_redact: list = None) -> dict:
    """
    Detekter og masker PII i tekst med Azure Language.

    Args:
        text: Tekst a analysere
        categories_to_redact: Spesifikke PII-kategorier a maskere
    """
    client = TextAnalyticsClient(
        endpoint=endpoint,
        credential=AzureKeyCredential(key)
    )

    response = client.recognize_pii_entities(
        documents=[text],
        categories_filter=categories_to_redact,
        language="no"  # Norsk
    )

    result = response[0]

    return {
        "original_text": text,
        "redacted_text": result.redacted_text,
        "entities": [
            {
                "text": entity.text,
                "category": entity.category,
                "subcategory": entity.subcategory,
                "confidence": entity.confidence_score,
                "offset": entity.offset,
                "length": entity.length
            }
            for entity in result.entities
        ]
    }

# Eksempel
result = detect_and_redact_pii(
    text="Ola Nordmann bor i Storgata 15, 0184 Oslo. Hans personnummer er 01019012345.",
    endpoint="https://myservice.cognitiveservices.azure.com/",
    key="your-api-key",
    categories_to_redact=["Person", "Address", "NorwayIdentityNumber"]
)
# Output: "***** bor i *****, ***** Oslo. Hans personnummer er *****."
```

### PII-deteksjon i Fabric-pipelines

```python
# Batch PII-deteksjon i PySpark
from pyspark.sql import functions as F
from pyspark.sql.types import ArrayType, StructType, StructField, StringType, DoubleType

def batch_pii_detection(df, text_column: str, endpoint: str, key: str):
    """
    Kjor PII-deteksjon pa en hel DataFrame-kolonne.
    """
    @F.udf(returnType=StringType())
    def redact_pii_udf(text):
        if not text:
            return text

        from azure.ai.textanalytics import TextAnalyticsClient
        from azure.core.credentials import AzureKeyCredential

        client = TextAnalyticsClient(
            endpoint=endpoint,
            credential=AzureKeyCredential(key)
        )

        try:
            response = client.recognize_pii_entities(
                documents=[text], language="no"
            )
            return response[0].redacted_text
        except Exception:
            return text  # Returner original ved feil

    return df.withColumn(f"{text_column}_redacted", redact_pii_udf(F.col(text_column)))

# Bruk pa treningsdata for RAG
df_documents = spark.table("lakehouse.default.raw_documents")
df_redacted = batch_pii_detection(
    df_documents,
    text_column="content",
    endpoint=endpoint,
    key=api_key
)

# Lagre redaktert versjon for AI-trening
df_redacted.select("doc_id", "content_redacted", "metadata") \
    .write.format("delta").mode("overwrite") \
    .saveAsTable("lakehouse.default.training_documents_anonymized")
```

### PII-kategorier relevant for norsk offentlig sektor

| Kategori | Azure-kode | Eksempler |
|----------|-----------|----------|
| Personnummer | NorwayIdentityNumber | 01019012345 |
| Personnavn | Person | Ola Nordmann |
| Adresse | Address | Storgata 15, 0184 Oslo |
| Telefonnummer | PhoneNumber | +47 90000000 |
| E-post | Email | ola@firma.no |
| Bankkonto | InternationalBankingAccountNumber | NO9386011117947 |
| Organisasjonsnummer | Organization | 123 456 789 |
| Helseinfo (PHI) | HealthcareEntities | Diagnose, medisin |

---

## Right-to-Be-Forgotten Implementation

### GDPR Artikkel 17: Retten til sletting

```python
from delta.tables import DeltaTable

class GDPRDeletionService:
    """
    Implementer rett til sletting (GDPR Art. 17) i Delta Lake.
    """

    def __init__(self, tables_with_personal_data: list):
        self.tables = tables_with_personal_data
        self.deletion_log_table = "lakehouse.default.gdpr_deletion_log"

    def process_deletion_request(self, person_id: str, request_id: str):
        """
        Slett alle personopplysninger for en person pa tvers av tabeller.
        """
        results = {}

        for table_config in self.tables:
            table_name = table_config["table"]
            id_column = table_config["id_column"]
            strategy = table_config.get("strategy", "hard_delete")

            try:
                delta_table = DeltaTable.forName(spark, table_name)

                if strategy == "hard_delete":
                    # Slett raden helt
                    delta_table.delete(f"{id_column} = '{person_id}'")

                elif strategy == "anonymize":
                    # Anonymiser i stedet for a slette
                    anon_columns = table_config.get("anonymize_columns", [])
                    update_set = {col: F.lit("SLETTET") for col in anon_columns}
                    update_set["is_anonymized"] = F.lit(True)
                    update_set["anonymized_date"] = F.current_timestamp()

                    delta_table.update(
                        condition=f"{id_column} = '{person_id}'",
                        set=update_set
                    )

                elif strategy == "pseudonymize":
                    # Erstatt med pseudonym
                    import hashlib
                    pseudonym = hashlib.sha256(
                        f"{person_id}_{request_id}".encode()
                    ).hexdigest()[:12]

                    delta_table.update(
                        condition=f"{id_column} = '{person_id}'",
                        set={id_column: F.lit(f"PSEUDO_{pseudonym}")}
                    )

                results[table_name] = {"status": "OK", "strategy": strategy}

            except Exception as e:
                results[table_name] = {"status": "ERROR", "error": str(e)}

        # Logg slettingen
        self._log_deletion(request_id, person_id, results)

        return results

    def _log_deletion(self, request_id, person_id, results):
        """Logg slettingsforesporselen for compliance-formaal."""
        log_entry = spark.createDataFrame([{
            "request_id": request_id,
            "person_id_hash": hashlib.sha256(person_id.encode()).hexdigest(),
            "timestamp": datetime.now().isoformat(),
            "tables_processed": len(results),
            "all_successful": all(r["status"] == "OK" for r in results.values()),
            "details": json.dumps(results)
        }])
        log_entry.write.format("delta").mode("append") \
            .saveAsTable(self.deletion_log_table)

    def vacuum_after_deletion(self, retention_hours: int = 0):
        """
        Kjor VACUUM for a fysisk fjerne slettede data.
        ADVARSEL: Setter retensjon til 0 timer = ingen tidsreise mulig.
        """
        spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "false")

        for table_config in self.tables:
            spark.sql(f"VACUUM {table_config['table']} RETAIN {retention_hours} HOURS")

        spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "true")

# Konfigurasjon
tables_config = [
    {"table": "lakehouse.default.customers", "id_column": "person_id", "strategy": "hard_delete"},
    {"table": "lakehouse.default.transactions", "id_column": "customer_id", "strategy": "anonymize",
     "anonymize_columns": ["customer_name", "email", "phone"]},
    {"table": "lakehouse.default.ml_features", "id_column": "entity_id", "strategy": "pseudonymize"},
    {"table": "lakehouse.default.embeddings", "id_column": "source_person_id", "strategy": "hard_delete"}
]

gdpr_service = GDPRDeletionService(tables_config)
result = gdpr_service.process_deletion_request("12345678901", "REQ-2026-001")
```

### TTL (Time-to-Live) for automatisk sletting

```python
# Implementer TTL for partisjonerte Delta-tabeller
def enforce_ttl(table_name: str, partition_column: str, retention_days: int):
    """
    Slett partisjoner eldre enn retention_days.
    Nyttig for a overholde GDPR-krav om minimering av lagringstid.
    """
    cutoff_date = (datetime.now() - timedelta(days=retention_days)).strftime("%Y-%m-%d")

    delta_table = DeltaTable.forName(spark, table_name)
    delta_table.delete(f"{partition_column} < '{cutoff_date}'")

    # VACUUM for a fysisk fjerne filene
    spark.sql(f"VACUUM {table_name}")

    print(f"Slettet data eldre enn {cutoff_date} fra {table_name}")

# Kjor daglig: Slett persondata eldre enn 13 maaneder
enforce_ttl("lakehouse.default.customer_interactions", "interaction_date", retention_days=395)
```

---

## Privacy Impact Assessments

### DPIA-rammeverk for AI-systemer

| Fase | Aktivitet | Verktoy |
|------|----------|---------|
| **1. Kartlegging** | Identifiser persondata i AI-systemet | Microsoft Purview Data Map |
| **2. Vurdering** | Vurder noodvendighet og proporsjonalitet | DPIA-mal fra Datatilsynet |
| **3. Risikoanalyse** | Identifiser risiko for de registrerte | Risk Assessment Framework |
| **4. Tiltak** | Implementer tekniske og organisatoriske tiltak | Anonymisering, tilgangsstyring |
| **5. Dokumentasjon** | Dokumenter vurderingen | Protokoll, behandlingsregister |
| **6. Konsultasjon** | Konsulter personvernombud / Datatilsynet | Ved hoy risiko |

### Automatisert personvern-sjekk i CI/CD

```python
def privacy_check_before_deployment(model_artifacts_path: str) -> dict:
    """
    Automatisert personvernsjekk for ML-modeller.
    Kjores som del av CI/CD-pipeline.
    """
    checks = {}

    # 1. Sjekk at treningsdata er anonymisert
    training_data_path = f"{model_artifacts_path}/training_data_manifest.json"
    manifest = json.load(open(training_data_path))

    checks["anonymized_training_data"] = manifest.get("anonymization_applied", False)

    # 2. Sjekk at modellen ikke memorerer PII
    # (Sample inference med kjente PII-verdier)
    checks["pii_leakage_test"] = run_pii_leakage_test(model_artifacts_path)

    # 3. Sjekk at DPIA er utfylt
    checks["dpia_completed"] = os.path.exists(f"{model_artifacts_path}/dpia_signed.pdf")

    # 4. Sjekk at personvernombud er konsultert
    checks["dpo_approved"] = manifest.get("dpo_approval_date") is not None

    # 5. Sjekk retensjonspolicy
    checks["retention_policy_defined"] = manifest.get("data_retention_days") is not None

    all_passed = all(checks.values())

    return {
        "passed": all_passed,
        "checks": checks,
        "recommendation": "DEPLOY" if all_passed else "BLOKKER - Personvernkrav ikke oppfylt"
    }
```

---

## Referanser

- [What is Azure Language PII detection?](https://learn.microsoft.com/en-us/azure/ai-services/language-service/personally-identifiable-information/overview) -- PII-deteksjon og maskering
- [PII filter in Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/content-filter-personal-information) -- PII-filtrering for LLM-er
- [Responsible AI - Privacy and security](https://learn.microsoft.com/en-us/azure/machine-learning/concept-responsible-ai) -- SmartNoise og Counterfit
- [Data privacy for cloud-scale analytics](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/cloud-scale-analytics/secure-data-privacy) -- Dataklassifisering og konfidensialitetsskjema
- [PII entity categories](https://learn.microsoft.com/en-us/azure/ai-services/language-service/personally-identifiable-information/concepts/entity-categories) -- Alle stottede PII-kategorier
- [Transparency note for PII](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/language-service/transparency-note-personally-identifiable-information) -- Bruksomrader og begrensninger
- [Data governance with Microsoft Purview](https://learn.microsoft.com/en-us/purview/data-governance-master-data-management) -- Purview for dataklassifisering

---

## For Cosmo

- **Bruk denne referansen** naar kunder behandler personopplysninger i AI-systemer og trenger anonymiserings- og personvernstrategier.
- **Azure Language PII-deteksjon er forstevalget** for a identifisere og maskere personopplysninger i tekst -- bade for treningsdata og RAG-dokumenter. Stotter norsk sprak.
- **GDPR-sletting i Delta Lake krever VACUUM**: Delta Lake sin tidsreise betyr at slettede data forblir tilgjengelige i transaksjonsloggen til VACUUM kjores. Planlegg VACUUM i trad med slettekrav.
- **K-anonymitet er minimum for publisering**: For datasett som deles utenfor organisasjonen, krev minimum k=5 anonymitet. For helsedata, bruk l-diversitet i tillegg.
- **For norsk offentlig sektor**: Datatilsynets DPIA-mal er obligatorisk for AI-systemer med hoy risiko. Integrer personvernsjekk i CI/CD-pipeline for a sikre at ingen modell deployes uten godkjent DPIA.
