# Data Sampling and Labeling Strategies

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

---

## Introduksjon

Kvaliteten pa treningsdata er den viktigste faktoren for ytelsen til ML-modeller. Effektiv datasampling sikrer at treningsdatasettet er representativt og balansert, mens systematisk datamerking (labeling) gir modellene de korrekte signalene a laere fra. Azure Machine Learning tilbyr en komplett plattform for datamerking med stotte for bade bilde- og tekstdata, inkludert ML-assistert merking som akselererer prosessen vesentlig.

For norsk offentlig sektor, der data ofte er ubalansert (f.eks. svaert fa svindeltilfeller vs. legitime transaksjoner, eller sjaeldne hendelser i trafikkdata), er stratifisert sampling og aktiv laering spesielt viktig. Riktig sampling reduserer merkebehovet med 50-80%, noe som sparer bade tid og kostnader i prosjekter med stramme budsjetter.

Denne referansen dekker hele livssyklusen fra datautvalg gjennom merkeprosesser til kvalitetskontroll, med fokus pa teknikker som er relevante for Microsoft AI-stakken og Azure Machine Learning.

---

## Stratified Sampling for Class Balance

### Problemet med ubalanserte datasett

| Scenario | Positiv klasse | Negativ klasse | Ubalanse-ratio |
|----------|---------------|----------------|----------------|
| Svindeldeteksjon | 0.1% svindel | 99.9% legitim | 1:1000 |
| Ulykkesprediksjon | 2% ulykker | 98% normal trafikk | 1:50 |
| Dokumentklassifisering | 5% sensitiv | 95% ikke-sensitiv | 1:19 |
| Feildeteksjon (IoT) | 0.5% feil | 99.5% normal | 1:200 |

### Stratifisert sampling i PySpark

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

def stratified_sample(df, label_column, sample_fractions, seed=42):
    """
    Utfor stratifisert sampling for a balansere klasser.

    Args:
        df: Input DataFrame
        label_column: Kolonnen som inneholder klassen
        sample_fractions: Dict med {klasse: samplingandel}
        seed: Random seed for reproduserbarhet
    """
    sampled = df.sampleBy(label_column, fractions=sample_fractions, seed=seed)
    return sampled

# Eksempel: Balanser svindeldatasett
# Original: 99.9% legitim, 0.1% svindel
sample_fractions = {
    "legitimate": 0.01,   # Sample 1% av legitime (reduser fra 99.9k til ~1k)
    "fraud": 1.0          # Behold alle svindeltilfeller (~100)
}

balanced_df = stratified_sample(
    df_transactions,
    label_column="transaction_type",
    sample_fractions=sample_fractions
)

print(f"Original: {df_transactions.count()} rader")
print(f"Balansert: {balanced_df.count()} rader")
print("Klassefordeling:")
balanced_df.groupBy("transaction_type").count().show()
```

### Oversampling og undersampling-teknikker

```python
def oversample_minority_class(df, label_column, minority_class, target_ratio=0.5):
    """
    Oversample minoritetsklassen ved a duplisere rader.

    Args:
        target_ratio: Onsket andel av minoritetsklassen
    """
    class_counts = df.groupBy(label_column).count().collect()
    counts = {row[label_column]: row["count"] for row in class_counts}

    minority_count = counts[minority_class]
    majority_count = sum(v for k, v in counts.items() if k != minority_class)

    # Beregn hvor mange ganger minoritetsklassen ma dupliseres
    desired_minority = int(majority_count * target_ratio / (1 - target_ratio))
    oversample_factor = desired_minority / minority_count

    # Oversample
    minority_df = df.filter(F.col(label_column) == minority_class)
    majority_df = df.filter(F.col(label_column) != minority_class)

    oversampled = minority_df.sample(
        withReplacement=True,
        fraction=oversample_factor,
        seed=42
    )

    return majority_df.unionByName(oversampled)

# Bruk
balanced = oversample_minority_class(
    df_training,
    label_column="incident_type",
    minority_class="accident",
    target_ratio=0.3  # 30% ulykker i treningsdatasettet
)
```

### SMOTE-lignende syntetisk oversampling

```python
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.clustering import KMeans
import numpy as np

def synthetic_oversampling(df, feature_columns, label_column, minority_class, n_synthetic):
    """
    Generer syntetiske minoritetseksempler basert pa naeromrade-interpolering.
    """
    minority_df = df.filter(F.col(label_column) == minority_class)

    # Vektoriser features
    assembler = VectorAssembler(inputCols=feature_columns, outputCol="features")
    vectorized = assembler.transform(minority_df)

    # For hver minoritetsrad: finn naermeste nabo og interpolder
    # Forenklet implementering med KMeans for cluster-sentre
    kmeans = KMeans(k=min(n_synthetic, minority_df.count()), seed=42)
    model = kmeans.fit(vectorized)

    # Bruk cluster-sentrene som syntetiske punkter
    centers = model.clusterCenters()

    synthetic_rows = []
    for center in centers:
        row = {col: float(center[i]) for i, col in enumerate(feature_columns)}
        row[label_column] = minority_class
        row["_synthetic"] = True
        synthetic_rows.append(row)

    synthetic_df = spark.createDataFrame(synthetic_rows)
    return df.withColumn("_synthetic", F.lit(False)).unionByName(synthetic_df, allowMissingColumns=True)
```

---

## Active Learning and Uncertainty Sampling

### Prinsippet bak aktiv laering

Aktiv laering velger de mest informative eksemplene for merking, i stedet for a merke tilfeldig:

```
+-- Umerkede data (pool) --+
|                           |
| [Hogt sikker] -> Hopp over, allerede laert
| [Moderat sikker] -> Hopp over
| [Usikker] -> MERK DENNE! <-- Mest laererik
| [Veldig usikker] -> MERK DENNE! <-- Hoyest prioritet
|                           |
+---------------------------+
```

### Usikkerhetssamplings-strategier

| Strategi | Formel | Best for |
|----------|--------|----------|
| **Least Confidence** | 1 - max(P(y\|x)) | Generell klassifisering |
| **Margin Sampling** | P(y1\|x) - P(y2\|x) | Naere beslutningsgrenser |
| **Entropy Sampling** | -sum(P(y\|x) * log P(y\|x)) | Multi-class problemer |
| **Query-by-Committee** | Uenighet mellom modeller | Ensemble-basert |

### Implementering av aktiv laering

```python
import numpy as np
from sklearn.ensemble import RandomForestClassifier

class ActiveLearner:
    """
    Aktiv laering med usikkerhetssampling for iterativ datamerking.
    """

    def __init__(self, model=None, strategy="entropy"):
        self.model = model or RandomForestClassifier(n_estimators=100)
        self.strategy = strategy
        self.labeled_indices = []
        self.labels = []

    def initial_sample(self, X, n_initial=100):
        """Velg et tilfeldig initialt treningssett."""
        indices = np.random.choice(len(X), n_initial, replace=False)
        self.labeled_indices = list(indices)
        return indices

    def query(self, X, n_samples=50):
        """Velg de mest informative eksemplene for merking."""
        # Prediksjonssannsynligheter for umerkede data
        unlabeled_mask = np.ones(len(X), dtype=bool)
        unlabeled_mask[self.labeled_indices] = False
        unlabeled_indices = np.where(unlabeled_mask)[0]

        if len(unlabeled_indices) == 0:
            return np.array([])

        X_unlabeled = X[unlabeled_indices]
        probas = self.model.predict_proba(X_unlabeled)

        # Beregn usikkerhetsscorer
        if self.strategy == "entropy":
            scores = -np.sum(probas * np.log(probas + 1e-10), axis=1)
        elif self.strategy == "least_confidence":
            scores = 1 - np.max(probas, axis=1)
        elif self.strategy == "margin":
            sorted_probas = np.sort(probas, axis=1)
            scores = 1 - (sorted_probas[:, -1] - sorted_probas[:, -2])

        # Velg top-n mest usikre
        top_indices = np.argsort(scores)[-n_samples:]
        return unlabeled_indices[top_indices]

    def teach(self, X, y, indices, labels):
        """Oppdater modellen med nylig merkede data."""
        self.labeled_indices.extend(indices)
        self.labels.extend(labels)

        X_labeled = X[self.labeled_indices]
        y_labeled = np.array(self.labels)

        self.model.fit(X_labeled, y_labeled)

# Brukseksempel
learner = ActiveLearner(strategy="entropy")

# Runde 1: Tilfeldig initialt sett
initial_idx = learner.initial_sample(X_pool, n_initial=100)
initial_labels = get_labels_from_labelers(X_pool[initial_idx])
learner.teach(X_pool, None, initial_idx, initial_labels)

# Runde 2-N: Aktiv laering
for round_num in range(10):
    query_idx = learner.query(X_pool, n_samples=50)
    new_labels = get_labels_from_labelers(X_pool[query_idx])
    learner.teach(X_pool, None, query_idx, new_labels)
    print(f"Runde {round_num + 1}: Totalt merket = {len(learner.labeled_indices)}")
```

---

## Crowdsourcing and Labeling Platforms

### Azure Machine Learning Data Labeling

Azure ML tilbyr en komplett merkeplattform med stotte for:

| Funksjon | Beskrivelse |
|----------|-------------|
| **Bildeklassifisering** | Multi-class og multi-label |
| **Objektdeteksjon** | Bounding boxes |
| **Instanssegmentering** | Polygoner |
| **Semantisk segmentering** | Piksel-niva (preview) |
| **Tekstklassifisering** | Single og multi-label |
| **Named Entity Recognition** | Tekst-span-merking |

### Opprette et merkeprosjekt

```python
# Azure ML SDK v2 - Opprett bildeklassifiseringsprosjekt
from azure.ai.ml import MLClient
from azure.ai.ml.entities import DataLabelingJob

# Opprett klient
ml_client = MLClient(credential, subscription_id, resource_group, workspace_name)

# Definer merkeprosjekt
labeling_job = DataLabelingJob(
    display_name="traffic-sign-classification",
    description="Klassifiser trafikkskilt fra vegkamera",
    labeling_job_type="ImageClassificationMulticlass",
    data={"uri": "azureml://datastores/images/paths/traffic_signs/"},
    labels={
        "classes": [
            {"name": "speed_limit", "display_name": "Fartsgrense"},
            {"name": "stop", "display_name": "Stopp"},
            {"name": "yield", "display_name": "Vikeplikt"},
            {"name": "no_entry", "display_name": "Innkjoring forbudt"},
            {"name": "pedestrian", "display_name": "Fotgjenger"},
            {"name": "construction", "display_name": "Veiarbeid"},
            {"name": "other", "display_name": "Annet"}
        ]
    },
    properties={
        "ml_assist_enabled": True,  # Aktiver ML-assistert merking
        "consensus_labeling_enabled": True,  # Krev konsensus
        "min_label_count": 2  # Minimum 2 merkere per bilde
    }
)

# Opprett prosjekt
created_job = ml_client.labeling_jobs.begin_create_or_update(labeling_job)
```

### ML-Assisted Labeling

ML-assistert merking i Azure ML akselererer prosessen gjennom to faser:

```
Fase 1: CLUSTERING
+-- Merkere merker ~300 bilder manuelt
+-- ML-modell grupperer lignende bilder
+-- Merkere ser klynger av like bilder (raskere merking)

Fase 2: PRE-LABELING
+-- Modell trenes pa merkede data
+-- Modell foreslaar etiketter for umerkede bilder
+-- Merkere bekrefter/korrigerer forhondsetiketter
+-- Prosessen gjentas iterativt
```

**Viktige hensyn:**
- Transfer learning akselererer opplaering: Noen ganger trengs kun 300 merkede eksempler
- Konsensus-etiketter brukes for trening naar aktivert
- Bilder shuffles tilfeldig for a redusere bias
- Tekstinnholdet begrenses til ~128 ord for treningseffektivitet

---

## Quality Control and Inter-Rater Agreement

### Konsensus-merking

```python
# Implementer konsensusbasert kvalitetskontroll
from collections import Counter

def calculate_inter_rater_agreement(labels_per_item: dict) -> dict:
    """
    Beregn inter-rater agreement (IRA) for merkede data.

    Args:
        labels_per_item: {item_id: [label_from_rater1, label_from_rater2, ...]}

    Returns:
        Statistikk over enighet
    """
    agreements = []
    disagreements = []

    for item_id, labels in labels_per_item.items():
        counter = Counter(labels)
        most_common_label, most_common_count = counter.most_common(1)[0]
        total_raters = len(labels)

        agreement_ratio = most_common_count / total_raters

        if agreement_ratio >= 0.8:
            agreements.append({
                "item_id": item_id,
                "consensus_label": most_common_label,
                "agreement": agreement_ratio
            })
        else:
            disagreements.append({
                "item_id": item_id,
                "labels": dict(counter),
                "agreement": agreement_ratio
            })

    total = len(labels_per_item)
    return {
        "total_items": total,
        "agreed": len(agreements),
        "disagreed": len(disagreements),
        "agreement_rate": round(len(agreements) / max(total, 1) * 100, 1),
        "disagreed_items": disagreements[:10]  # Vis topp 10 uenigheter
    }
```

### Cohens Kappa for kvalitetsmalinger

```python
from sklearn.metrics import cohen_kappa_score

def evaluate_labeler_quality(rater1_labels, rater2_labels):
    """
    Beregn Cohens Kappa mellom to merkere.

    Tolkning:
    - < 0.20: Darlig enighet
    - 0.21-0.40: Moderat enighet
    - 0.41-0.60: Moderat enighet
    - 0.61-0.80: Substansiell enighet
    - 0.81-1.00: Naer perfekt enighet
    """
    kappa = cohen_kappa_score(rater1_labels, rater2_labels)

    if kappa < 0.40:
        quality = "LAV - Gjennomga retningslinjer og gi oppfolging"
    elif kappa < 0.60:
        quality = "MODERAT - Akseptabel for screening, ikke for endelig trening"
    elif kappa < 0.80:
        quality = "GOD - Akseptabel for de fleste ML-oppgaver"
    else:
        quality = "UTMERKET - Hoy kvalitet for treningsdata"

    return {"kappa": round(kappa, 3), "quality": quality}

# Eksempel
result = evaluate_labeler_quality(
    rater1_labels=["positive", "negative", "positive", "neutral", "positive"],
    rater2_labels=["positive", "negative", "neutral", "neutral", "positive"]
)
```

### Kvalitetskontrollpipeline

```
1. Initial merking (2-3 merkere per element)
        |
2. Beregn inter-rater agreement (IRA)
        |
3. IRA >= 80%? --> Bruk konsensus-etikett
        |
4. IRA < 80%? --> Send til ekspert-merker (adjudicator)
        |
5. Ekspert avgjer endelig etikett
        |
6. Oppdater merkeretningslinjer basert pa uenigheter
        |
7. Re-tren ML-assist-modell med nye etiketter
```

---

## Feedback Loops for Continuous Labeling

### Produksjonsdata tilbake til merking

```python
def identify_candidates_for_relabeling(model, new_data_df, confidence_threshold=0.6):
    """
    Identifiser prediksjoner med lav konfidens for manuell gjennomgang.
    """
    predictions = model.predict_proba(new_data_df)

    low_confidence = new_data_df.filter(
        F.col("prediction_confidence") < confidence_threshold
    )

    # Prioriter etter usikkerhet
    candidates = low_confidence.orderBy(F.col("prediction_confidence").asc())

    # Legg til i merke-ko
    candidates.select(
        "record_id", "features", "prediction", "prediction_confidence"
    ).write.format("delta").mode("append") \
        .saveAsTable("lakehouse.default.labeling_queue")

    return candidates.count()

# Kjor daglig
n_candidates = identify_candidates_for_relabeling(
    model=production_model,
    new_data_df=todays_predictions,
    confidence_threshold=0.6
)
print(f"{n_candidates} nye elementer lagt til merke-koen")
```

### Drift-deteksjon og re-merking

```python
def detect_label_drift(historical_labels_df, recent_labels_df, columns):
    """
    Oppdager endringer i etikettdistribusjon over tid.
    """
    from scipy.stats import chi2_contingency

    for col in columns:
        hist_dist = historical_labels_df.groupBy(col).count().toPandas()
        recent_dist = recent_labels_df.groupBy(col).count().toPandas()

        # Chi-kvadrat-test
        contingency = hist_dist.merge(recent_dist, on=col, suffixes=["_hist", "_recent"])
        chi2, p_value, dof, expected = chi2_contingency(
            contingency[["count_hist", "count_recent"]].values.T
        )

        if p_value < 0.05:
            print(f"DRIFT OPPDAGET i '{col}': p={p_value:.4f}")
            print(f"  Historisk: {dict(zip(hist_dist[col], hist_dist['count']))}")
            print(f"  Nylig:    {dict(zip(recent_dist[col], recent_dist['count']))}")
```

---

## Referanser

- [Set up an image labeling project](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-create-image-labeling-projects) -- Bildedatamerking i Azure ML
- [Set up a text labeling project](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-create-text-labeling-projects) -- Tekstdatamerking i Azure ML
- [Labeling images and text documents](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-label-data) -- Merkerverktoy og ML-assistert merking
- [Prepare data for computer vision tasks](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-prepare-datasets-for-automl-images) -- Data for AutoML-bildemodeller
- [Label text data for training](https://learn.microsoft.com/en-us/azure/ai-services/language-service/custom-text-classification/how-to/tag-data) -- Merking for Custom Language Models
- [Create and explore datasets with labels](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-use-labeled-dataset) -- Bruk av merkede datasett i Azure ML

---

## For Cosmo

- **Bruk denne referansen** naar kunder planlegger datamerkings-prosjekter for ML-modeller, eller naar de trenger strategier for a hondtere ubalanserte datasett.
- **Azure ML Data Labeling er forstevalget** for merkingsprosjekter i Microsoft-stakken. ML-assistert merking kan redusere manuelt arbeid med 50-80% etter initiell opplaering.
- **Aktiv laering bor alltid vurderes** for store umerkede datasett -- det reduserer merkekostnader dramatisk ved a prioritere de mest informative eksemplene.
- **Kvalitetskontroll er ikke valgfritt**: Krev konsensus mellom merkere (minimum 2), mal inter-rater agreement, og ha en ekspert-adjudicator for uenigheter.
- **For norsk offentlig sektor**: Vurder personvernaspekter ved merking av data som kan inneholde personopplysninger. Bruk PII-deteksjon for a fjerne sensitiv informasjon for merkerne ser dataene.
