# Master Data Management for AI

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

---

## Introduksjon

Master Data Management (MDM) skaper en enkelt kilde til sannhet for kritiske forretningsenheter som kunder, produkter, ansatte og lokasjoner. For AI-losninger er kvaliteten pa stamdata direkte avgjorede -- en ML-modell trent pa inkonsistente kundedata vil produsere upaalitelige prediksjoner, og en RAG-losning med dupliserte dokumenter vil gi motstridende svar.

Microsoft tilbyr MDM gjennom flere tjenester: Microsoft Purview for data governance og MDM-integrasjoner, Dataverse som operativt masterdatasystem for Dynamics 365 og Power Platform, og Azure-tjenester som Data Factory for datakvalitet og deduplisering. For AI-pipelines i Fabric er det kritisk a sikre at referansedata og enhetsmapping er konsistent pa tvers av domener.

For norsk offentlig sektor er MDM spesielt viktig: Folkeregisteret, Enhetsregisteret og Matrikkelen er eksempler pa nasjonale masterdatakilder. Integrasjon med disse via Digdir sine felleslosninger, kombinert med intern MDM i Dataverse eller Fabric, sikrer at AI-losninger opererer pa palitelig grunn.

---

## Golden Record Creation and Reconciliation

### Hva er en Golden Record?

En golden record er den autoritative, konsoliderte versjonen av en enhet som kombinerer data fra flere kilder:

```
Kilde A: {navn: "Ola Nordmann", epost: "ola@firma.no", tlf: null}
Kilde B: {navn: "O. Nordmann", epost: null, tlf: "+47 90000000"}
Kilde C: {navn: "Ola Nordmann", epost: "ola.nordmann@firma.no", tlf: "+47 90000000"}

Golden Record: {
    navn: "Ola Nordmann",           -- Mest komplett, hoyest tillit
    epost: "ola.nordmann@firma.no", -- Fra kilde C (nyeste, mest komplett)
    tlf: "+47 90000000",            -- Fra kilde B/C (konsistent)
    kilder: ["A", "B", "C"],
    tillit_score: 0.95
}
```

### Survivorship-regler

| Regel | Beskrivelse | Eksempel |
|-------|-------------|---------|
| **Most Recent** | Nyeste verdi vinner | Sist oppdatert adresse |
| **Most Complete** | Mest utfylt felt vinner | Lengste navn-streng |
| **Most Trusted Source** | Autoritativ kilde vinner | Folkeregisteret > CRM |
| **Frequency** | Hyppigste verdi vinner | 3 av 4 kilder sier "Oslo" |
| **Manual Override** | Manuell beslutning | Datakurator velger |

### Implementering i Fabric

```python
from pyspark.sql import functions as F
from pyspark.sql.window import Window

def create_golden_records(sources: dict, entity_key: str, rules: dict):
    """
    Opprett golden records fra flere kilder med survivorship-regler.

    Args:
        sources: Dict med {kildenavn: DataFrame}
        entity_key: Nokkelkolonne for matching
        rules: Dict med {kolonne: survivorship_regel}
    """
    # 1. Kombiner alle kilder med kildemerking
    combined = None
    for source_name, df in sources.items():
        tagged = df.withColumn("_source", F.lit(source_name)) \
                    .withColumn("_load_time", F.current_timestamp())
        combined = combined.unionByName(tagged, allowMissingColumns=True) \
                   if combined else tagged

    # 2. Dedupliser og velg vinnere per kolonne
    golden = combined.groupBy(entity_key)

    agg_exprs = []
    for col_name, rule in rules.items():
        if rule == "most_recent":
            agg_exprs.append(
                F.last(col_name, ignorenulls=True).alias(col_name)
            )
        elif rule == "most_complete":
            agg_exprs.append(
                F.max(F.when(F.col(col_name).isNotNull(), F.col(col_name)))
                .alias(col_name)
            )
        elif rule == "most_trusted":
            # Sorter etter kildeprioritering
            agg_exprs.append(
                F.first(col_name, ignorenulls=True).alias(col_name)
            )

    golden_df = golden.agg(*agg_exprs)
    return golden_df

# Bruk
sources = {
    "crm": df_crm,
    "erp": df_erp,
    "folkeregisteret": df_folkereg
}

rules = {
    "full_name": "most_trusted",       # Folkeregisteret prioritert
    "address": "most_recent",          # Siste oppdatering
    "phone": "most_complete",          # Mest utfylt
    "organization_number": "most_trusted"  # Enhetsregisteret
}

golden_customers = create_golden_records(sources, "person_id", rules)
```

---

## Entity Resolution and Deduplication

### Duplikatdeteksjon i Dataverse

Microsoft Dataverse har innebygd duplikatdeteksjon for aktive poster som kontoer og kontakter:

| Funksjon | Beskrivelse |
|----------|-------------|
| **Duplikatdeteksjonsregler** | Definer matchingkriterier per entitet |
| **Sanntidssjekk** | Sjekker ved opprettelse/oppdatering |
| **Bulk-deteksjon** | Kjor deteksjon pa eksisterende data |
| **Merge** | Kombiner duplikater med valg av primaer-post |

### Fuzzy Matching i Fabric

For mer avansert entity resolution i AI-pipelines:

```python
from pyspark.sql import functions as F
from pyspark.ml.feature import NGram, HashingTF, MinHashLSH

def fuzzy_match_entities(df, match_columns, threshold=0.7):
    """
    Fuzzy matching med MinHash LSH for skalerbar deduplisering.
    """
    # 1. Kombiner matchkolonner til en tekststreng
    df = df.withColumn(
        "match_text",
        F.lower(F.concat_ws(" ", *match_columns))
    )

    # 2. Tokeniser til n-grams
    df = df.withColumn(
        "tokens",
        F.split(F.col("match_text"), " ")
    )

    # 3. Hashing for dimensjonsreduksjon
    hashingTF = HashingTF(inputCol="tokens", outputCol="features", numFeatures=1024)
    df_hashed = hashingTF.transform(df)

    # 4. MinHash LSH for approximate nearest neighbors
    mh = MinHashLSH(inputCol="features", outputCol="hashes", numHashTables=5)
    model = mh.fit(df_hashed)

    # 5. Finn lignende par
    similar_pairs = model.approxSimilarityJoin(
        df_hashed, df_hashed, threshold, distCol="distance"
    )

    return similar_pairs.filter(F.col("datasetA.id") < F.col("datasetB.id"))

# Eksempel: Dedupliser organisasjoner
duplicates = fuzzy_match_entities(
    df_organizations,
    match_columns=["org_name", "address", "postal_code"],
    threshold=0.3  # Lav avstand = hoy likhet
)
```

### Deterministic vs. Probabilistic Matching

| Tilnaerming | Bruksomrade | Presisjon | Recall |
|-------------|-------------|-----------|--------|
| **Deterministic** | Eksakt match pa unike IDer | Hoy | Lav |
| **Rule-based** | Kombinasjon av felt med toleranser | Moderat-hoy | Moderat |
| **Probabilistic** | Fuzzy matching med ML-modeller | Moderat | Hoy |
| **Hybrid** | Deterministic forst, deretter probabilistic | Hoy | Hoy |

```python
# Hybrid-tilnaerming
def hybrid_entity_resolution(df_source, df_reference):
    """
    Trinn 1: Eksakt match pa organisasjonsnummer
    Trinn 2: Fuzzy match pa navn + adresse for resterende
    """
    # Trinn 1: Deterministic match
    exact_matches = df_source.join(
        df_reference,
        df_source["org_number"] == df_reference["org_number"],
        "inner"
    )

    unmatched = df_source.join(
        df_reference,
        df_source["org_number"] == df_reference["org_number"],
        "left_anti"
    )

    # Trinn 2: Fuzzy match for umatchede
    fuzzy_matches = fuzzy_match_entities(
        unmatched.unionByName(df_reference, allowMissingColumns=True),
        match_columns=["org_name", "address"],
        threshold=0.4
    )

    return exact_matches, fuzzy_matches
```

---

## MDM Integration with Dataverse

### Dataverse som Master Data System

Dataverse fungerer som operativt masterdatasystem for Dynamics 365-apper:

```
+-------------------+     Service Bus     +------------------+
| Dataverse         |------ Queue ------->| Logic App        |
| (Master Data)     |                     | (Transform to    |
|                   |                     |  Canonical Model)|
| - Accounts        |                     +--------+---------+
| - Contacts        |                              |
| - Products        |                     +--------v---------+
| - Addresses       |                     | Service Bus Topic|
+-------------------+                     | (Canonical Data) |
                                          +--------+---------+
                                                   |
                                          +--------v---------+
                                          | Consumers:       |
                                          | - Fabric Lakehouse|
                                          | - Azure SQL      |
                                          | - Power BI       |
                                          +------------------+
```

### Synkronisering mellom Dataverse og Fabric

```python
# Dataverse til Fabric via Azure Synapse Link
# Konfigureres i Power Platform Admin Center

# Alternativt: Dataverse connector i Fabric Data Pipeline
# Pipeline -> Copy Activity -> Source: Dataverse -> Sink: Lakehouse

# Eksempel: Les masterdata fra Dataverse via Spark
df_accounts = spark.read \
    .format("com.microsoft.cdm") \
    .option("entity", "account") \
    .option("dataverseUrl", "https://org.crm.dynamics.com") \
    .load()

# Opprett referansetabell i Lakehouse
df_accounts.select(
    "accountid",
    "name",
    "address1_city",
    "accountnumber",
    "industrycode"
).write \
    .format("delta") \
    .mode("overwrite") \
    .saveAsTable("lakehouse.default.ref_organizations")
```

### Duplikatdeteksjon i Dataverse

```
# Dataverse duplikatdeteksjonsregler:
#
# 1. Opprett regel i Power Platform Admin Center
#    - Entitet: Account
#    - Matchkriterier: accountname (fuzzy match)
#                      + address1_city (eksakt match)
#
# 2. Aktiver sanntidssjekk
#    - Ved opprettelse av ny post
#    - Ved oppdatering av eksisterende post
#
# 3. Bulk-deteksjon
#    - Kjor pa eksisterende data
#    - Planlegg regelmessig kjoring
```

---

## Reference Data Versioning

### Versjonerte referansetabeller

```python
# Implementer SCD Type 2 for referansedata i Fabric

from delta.tables import DeltaTable

def upsert_reference_data(ref_table_name, new_data_df, key_columns, tracked_columns):
    """
    SCD Type 2 upsert for referansedata.
    Bevarer historikk for endrede verdier.
    """
    ref_table = DeltaTable.forName(spark, ref_table_name)

    # Bygg match-betingelse
    match_condition = " AND ".join(
        [f"target.{col} = source.{col}" for col in key_columns]
    )

    # Bygg endringsbetingelse
    change_condition = " OR ".join(
        [f"target.{col} != source.{col}" for col in tracked_columns]
    )

    # Merk utgaatte rader og sett inn nye
    ref_table.alias("target").merge(
        new_data_df.alias("source"),
        match_condition
    ).whenMatchedUpdate(
        condition=change_condition,
        set={
            "is_current": F.lit(False),
            "valid_to": F.current_timestamp()
        }
    ).whenNotMatchedInsertAll().execute()

    # Sett inn oppdaterte rader som nye
    changed = new_data_df.alias("s").join(
        ref_table.toDF().filter("is_current = false").alias("t"),
        [F.col(f"s.{c}") == F.col(f"t.{c}") for c in key_columns],
        "inner"
    ).select("s.*") \
     .withColumn("is_current", F.lit(True)) \
     .withColumn("valid_from", F.current_timestamp()) \
     .withColumn("valid_to", F.lit(None).cast("timestamp"))

    changed.write.format("delta").mode("append").saveAsTable(ref_table_name)

# Eksempel: Oppdater kommuner-referanse (relevant ved kommunesammenslaainger)
upsert_reference_data(
    ref_table_name="lakehouse.default.ref_municipalities",
    new_data_df=df_new_municipalities,
    key_columns=["municipality_code"],
    tracked_columns=["municipality_name", "county_code", "county_name"]
)
```

### Referansedata fra nasjonale registre

| Register | Eier | Bruksomrade for AI |
|----------|------|-------------------|
| **Folkeregisteret** | Skatteetaten | Personentiteter i NER, chatbots |
| **Enhetsregisteret** | Bronnoysundregistrene | Organisasjonsdata for bedriftsanalyse |
| **Matrikkelen** | Kartverket | Eiendomsdata for geospatial AI |
| **NVDB** | Statens vegvesen | Veidata for trafikkmodeller |
| **Kommuneregisteret** | SSB | Geografisk referanse |

```python
# Eksempel: Last inn kommuneregister fra SSB API
import requests
import json

def load_ssb_municipality_register():
    """Last inn offisiell kommuneliste fra SSB."""
    url = "https://data.ssb.no/api/klass/v1/classifications/131/codes"
    params = {"from": "2026-01-01", "to": "2026-12-31"}

    response = requests.get(url, params=params)
    data = response.json()

    # Konverter til Spark DataFrame
    df = spark.createDataFrame([
        {
            "code": item["code"],
            "name": item["name"],
            "valid_from": item["validFrom"],
            "valid_to": item.get("validTo")
        }
        for item in data["codes"]
    ])

    return df
```

---

## Data Quality SLAs for MDM Entities

### Kvalitetsdimensjoner for masterdata

| Dimensjon | Definisjon | Malemetode | Eksempel SLA |
|-----------|-----------|------------|-------------|
| **Completeness** | Andel utfylte felt | % ikke-null verdier | >= 98% |
| **Uniqueness** | Ingen duplikater | Antall duplikater / totalt | = 100% |
| **Accuracy** | Korrekthet mot kilde | Stikkprovekontroll | >= 99% |
| **Timeliness** | Ferskhet pa data | Tid siden siste oppdatering | < 24 timer |
| **Consistency** | Samsvar mellom systemer | Cross-system validering | >= 99.5% |
| **Validity** | Overholder forretningsregler | Regelvalidering | >= 99% |

### Implementere DQ-SLAer i Fabric

```python
from datetime import datetime, timedelta

def check_mdm_quality_slas(table_name: str, slas: dict) -> dict:
    """
    Sjekk datakvalitet mot definerte SLAer for en MDM-tabell.
    """
    df = spark.table(table_name)
    total_rows = df.count()
    results = {}

    # Completeness
    if "completeness" in slas:
        for col_name, threshold in slas["completeness"].items():
            non_null = df.filter(F.col(col_name).isNotNull()).count()
            pct = (non_null / total_rows) * 100
            results[f"completeness_{col_name}"] = {
                "actual": round(pct, 2),
                "threshold": threshold,
                "passed": pct >= threshold
            }

    # Uniqueness
    if "uniqueness" in slas:
        key_cols = slas["uniqueness"]["columns"]
        distinct = df.select(key_cols).distinct().count()
        is_unique = distinct == total_rows
        results["uniqueness"] = {
            "distinct": distinct,
            "total": total_rows,
            "duplicates": total_rows - distinct,
            "passed": is_unique
        }

    # Timeliness
    if "timeliness" in slas:
        max_age = slas["timeliness"]["max_age_hours"]
        timestamp_col = slas["timeliness"]["column"]
        latest = df.agg(F.max(timestamp_col)).collect()[0][0]
        age_hours = (datetime.now() - latest).total_seconds() / 3600
        results["timeliness"] = {
            "latest_update": str(latest),
            "age_hours": round(age_hours, 1),
            "threshold_hours": max_age,
            "passed": age_hours <= max_age
        }

    # Validity
    if "validity" in slas:
        for rule_name, rule in slas["validity"].items():
            valid_count = df.filter(rule["condition"]).count()
            pct = (valid_count / total_rows) * 100
            results[f"validity_{rule_name}"] = {
                "actual": round(pct, 2),
                "threshold": rule["threshold"],
                "passed": pct >= rule["threshold"]
            }

    return results

# Eksempel: SLA-sjekk for organisasjons-masterdata
slas = {
    "completeness": {
        "org_name": 100.0,
        "org_number": 100.0,
        "address": 95.0,
        "contact_email": 80.0
    },
    "uniqueness": {
        "columns": ["org_number"]
    },
    "timeliness": {
        "column": "last_updated",
        "max_age_hours": 24
    },
    "validity": {
        "valid_org_number": {
            "condition": "LENGTH(org_number) = 9 AND org_number RLIKE '^[0-9]+$'",
            "threshold": 100.0
        }
    }
}

results = check_mdm_quality_slas("lakehouse.default.ref_organizations", slas)
```

### Alerting ved SLA-brudd

```python
# Integrer med Power Automate for varsling
def alert_on_sla_breach(results: dict, webhook_url: str):
    """Send varsel via Power Automate webhook ved SLA-brudd."""
    breaches = {k: v for k, v in results.items() if not v.get("passed", True)}

    if breaches:
        payload = {
            "title": "MDM Quality SLA Breach",
            "summary": f"{len(breaches)} SLA-brudd oppdaget",
            "details": json.dumps(breaches, indent=2, default=str)
        }
        requests.post(webhook_url, json=payload)
```

---

## Referanser

- [Master data management in Microsoft Purview](https://learn.microsoft.com/en-us/purview/data-governance-master-data-management) -- MDM-oversikt i Purview
- [Detect duplicate records and merge](https://learn.microsoft.com/en-us/power-platform/admin/detect-duplicate-records) -- Duplikatdeteksjon i Dataverse
- [Dataverse as a master data system](https://learn.microsoft.com/en-us/dynamics365/guidance/reference-architectures/dataverse-master-data-system) -- Referansearkitektur for Dataverse MDM
- [Master data management](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/cloud-scale-analytics/govern-master-data) -- MDM i Cloud Adoption Framework
- [CluedIn integration for MDM](https://learn.microsoft.com/en-us/purview/data-governance-master-data-management-cluedin) -- Eventual connectivity for MDM
- [Profisee integration for MDM](https://learn.microsoft.com/en-us/purview/data-governance-master-data-management-profisee) -- MDM med Azure-integrasjon
- [Set up duplicate detection rules](https://learn.microsoft.com/en-us/power-platform/admin/set-up-duplicate-detection-rules-keep-data-clean) -- Konfigurere duplikatregler i Dataverse

---

## For Cosmo

- **Bruk denne referansen** naar kunder trenger a sikre datakvalitet i stamdata for bruk i AI/ML-modeller, eller naar de planlegger MDM-strategi for Microsoft-plattformen.
- **Dataverse er det naturlige valget** for operativ MDM i organisasjoner som allerede bruker Dynamics 365 eller Power Platform. For analytisk MDM, bruk Fabric Lakehouse med SCD Type 2.
- **Entity resolution er kritisk for AI**: Uten riktig deduplisering vil ML-modeller laere fra inkonsistente data. Anbefal hybrid-tilnaerming: deterministic match forst, deretter fuzzy match.
- **For norsk offentlig sektor**: Integrer med nasjonale registre (Folkeregisteret, Enhetsregisteret) som autoritative kilder. Disse bor ha hoyest prioritet i survivorship-regler.
- **SLA-monitorering bor automatiseres**: Sett opp kvalitetssjekker som kjorer daglig og varsler ved brudd, spesielt for data som inngaar i AI-treningspipelines.
