# Feature Stores and Feature Engineering

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

---

## Introduksjon

Feature stores er et sentralt mønster i moderne MLOps som løser problemet med feature-gjenbruk, konsistens mellom trening og inferens, og operasjonalisering av feature-pipelines. Azure Machine Learning Managed Feature Store og Microsoft Fabric Data Science gir en komplett plattform for å definere, materialisere, dele og overvåke features på tvers av ML-prosjekter.

For norsk offentlig sektor innebærer feature store-tilnærmingen at data science-team kan dele beregninger på tvers av prosjekter -- for eksempel kan trafikkdata-features brukes både for ulykkesprediksjonsmodeller og køvarslingsmodeller uten redundant feature engineering. Dette reduserer kostnader, forbedrer konsistens og forkorter tid fra eksperimentering til produksjon.

Denne referansen dekker feature-definisjon og lagring, point-in-time lookups for trening, feature-oppdateringsstrategier, Data Wrangler for utforskende feature engineering, og overvåking av feature-kvalitet og drift.

---

## Feature Definition and Storage in Silver Layer

### Feature Store Arkitektur

```
┌──────────────────────────────────────────────────────────────┐
│                    Azure ML Managed Feature Store              │
│  ┌──────────────┐  ┌──────────────────┐  ┌───────────────┐  │
│  │ Feature Set  │  │ Materialization   │  │ Feature       │  │
│  │ Specification│  │ Store (ADLS Gen2) │  │ Retrieval     │  │
│  │              │  │ Offline + Online  │  │ Component     │  │
│  └──────┬───────┘  └────────┬─────────┘  └───────┬───────┘  │
│         │                   │                     │          │
│         └───────────────────┴─────────────────────┘          │
└──────────────────────────────┬───────────────────────────────┘
                               │
┌──────────────────────────────▼───────────────────────────────┐
│                    Microsoft Fabric                            │
│  ┌──────────┐  ┌──────────────┐  ┌────────────────────────┐ │
│  │ Bronze   │  │ Silver Layer │  │ Gold Layer              │ │
│  │ (raw)    │──│ (features)   │──│ (training datasets)    │ │
│  └──────────┘  └──────────────┘  └────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```

### Feature Set Specification

En feature set-spesifikasjon definerer features og valgfri transformasjonslogikk:

```python
# Feature set specification (YAML)
# feature_set_spec/transactions/spec.yaml
"""
name: transactions
version: "1"
description: "Customer transaction features"
entities:
  - name: customer
    version: "1"
    join_keys:
      - customer_id
source:
  type: parquet
  path: "abfss://silver@onelake.dfs.fabric.microsoft.com/transactions"
  timestamp_column: transaction_date
features:
  - name: transaction_7day_count
    type: integer
    description: "Number of transactions in last 7 days"
  - name: transaction_7day_sum
    type: double
    description: "Total transaction amount in last 7 days"
  - name: transaction_30day_avg
    type: double
    description: "Average transaction amount in last 30 days"
"""
```

### Feature Transformations med PySpark

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

# Kildedataer fra Silver layer
transactions = spark.read.format("delta").table("silver.transactions")

# Definer vindus-spesifikasjoner
window_7d = (
    Window
    .partitionBy("customer_id")
    .orderBy(F.col("transaction_date").cast("long"))
    .rangeBetween(-7 * 86400, 0)  # 7 dager i sekunder
)

window_30d = (
    Window
    .partitionBy("customer_id")
    .orderBy(F.col("transaction_date").cast("long"))
    .rangeBetween(-30 * 86400, 0)
)

# Beregn features
customer_features = (
    transactions
    .withColumn("txn_7d_count", F.count("*").over(window_7d))
    .withColumn("txn_7d_sum", F.sum("amount").over(window_7d))
    .withColumn("txn_30d_avg", F.avg("amount").over(window_30d))
    .withColumn("txn_30d_max", F.max("amount").over(window_30d))
    .withColumn("days_since_last_txn",
        F.datediff(F.current_date(), F.max("transaction_date").over(
            Window.partitionBy("customer_id"))))
)

# Lagre features i Silver layer
customer_features.write.format("delta") \
    .mode("overwrite") \
    .saveAsTable("silver.customer_transaction_features")
```

### Feature-lagring i Medallion Architecture

| Lag | Innhold | Oppdateringsfrekvens |
|---|---|---|
| **Bronze** | Råtransaksjoner fra Dataverse/kildesystemer | Sanntid / daglig |
| **Silver** | Feature-beregninger (aggregater, vindus-funksjoner) | Daglig / per time |
| **Gold** | Ferdige treningsdatasett (features + labels) | Ved behov |
| **Feature Store** | Registrerte, versjonerte features | Materialisert etter plan |

---

## Point-in-Time Lookups for Training

### Temporal Joins (tidsreise-joiner)

Point-in-time lookups er kritisk for å unngå datalekasje i ML-trening:

```python
# FEIL: Standard join inkluderer fremtidige data (data leakage!)
# features_at_prediction_time = features.join(labels, "customer_id")

# RIKTIG: Point-in-time join
from pyspark.sql.functions import col

# Observations: tidspunkter der vi vil ha features
observations = spark.createDataFrame([
    ("C001", "2026-01-15"),
    ("C002", "2026-01-20"),
    ("C003", "2026-02-01")
], ["customer_id", "observation_date"])

# Features: tidsseriedata
features = spark.read.format("delta").table("silver.customer_transaction_features")

# Point-in-time join: hent features som var gjeldende PÅ observation_date
pit_features = (
    observations.alias("obs")
    .join(
        features.alias("feat"),
        (col("obs.customer_id") == col("feat.customer_id")) &
        (col("feat.feature_date") <= col("obs.observation_date")),
        "left"
    )
    .withColumn("rank", F.row_number().over(
        Window
        .partitionBy("obs.customer_id", "obs.observation_date")
        .orderBy(F.desc("feat.feature_date"))
    ))
    .filter(col("rank") == 1)  # Siste feature-verdi FØR observation_date
    .drop("rank")
)
```

### Azure ML Feature Retrieval Component

```python
# Deklarativ feature retrieval i Azure ML pipeline
from azure.ai.ml import MLClient
from azure.ai.ml.entities import FeatureRetrievalSpec

# Definer feature retrieval spec
feature_retrieval_spec = FeatureRetrievalSpec(
    feature_store_name="my-feature-store",
    features=[
        {
            "feature_set": "transactions:1",
            "features": ["txn_7d_count", "txn_7d_sum", "txn_30d_avg"]
        },
        {
            "feature_set": "demographics:1",
            "features": ["age_group", "region", "income_band"]
        }
    ]
)

# Feature retrieval støtter automatisk point-in-time joins
# basert på timestamp-kolonne i feature set specification
```

---

## Feature Freshness and Refresh Cadences

### Materialiseringsstrategi

| Feature-type | Oppdateringsfrekvens | Materialiseringsmetode |
|---|---|---|
| **Statiske** (demografi) | Ukentlig / månedlig | Batch materialisering |
| **Langsom endring** (score) | Daglig | Scheduled materialisering |
| **Rask endring** (transaksjoner) | Per time / sanntid | Streaming + backfill |
| **Sanntid** (lokasjon) | Kontinuerlig | Online store (Redis) |

### Materialiserings-oppsett

```python
from azure.ai.ml.entities import (
    MaterializationSettings,
    MaterializationComputeResource,
    RecurrenceTrigger
)

# Konfigurer materialisering for en feature set
materialization = MaterializationSettings(
    schedule=RecurrenceTrigger(
        interval=1,
        frequency="Day",
        time_of_day="02:00"  # Kjør kl 02:00 UTC
    ),
    resource=MaterializationComputeResource(
        instance_type="standard_e4s_v3"
    ),
    spark_configuration={
        "spark.driver.cores": 4,
        "spark.driver.memory": "36g",
        "spark.executor.cores": 4,
        "spark.executor.memory": "36g"
    }
)

# Backfill for historisk data
from azure.ai.ml import MLClient

fs_client = MLClient(credential, subscription_id, resource_group, feature_store_name)
poller = fs_client.feature_sets.begin_backfill(
    name="transactions",
    version="1",
    feature_window_start_time="2025-01-01T00:00:00Z",
    feature_window_end_time="2026-02-11T00:00:00Z",
    data_status=["None", "Incomplete"]
)

# Stream jobb-logger
fs_client.jobs.stream(poller.result().job_ids[0])
```

### Online vs. Offline Materialization

| Aspekt | Offline Store (ADLS Gen2) | Online Store (Redis) |
|---|---|---|
| **Bruksområde** | Trening, batch-inferens | Real-time inferens |
| **Latens** | Sekunder-minutter | Millisekunder |
| **Volum** | Ubegrenset | Begrenset av Redis-minne |
| **Kostnad** | Lav (lagring) | Høyere (compute) |
| **Format** | Delta/Parquet | Key-value |

---

## Data Wrangler for Exploratory Feature Engineering

### Data Wrangler i Fabric

Data Wrangler er et notebook-basert verktøy for visuell datautforsking og feature engineering:

```python
# Steg 1: Last data i Notebook
import pandas as pd

df = spark.read.format("delta").table("silver.customer_data").toPandas()

# Steg 2: Start Data Wrangler
# Klikk "Data" > "Launch Data Wrangler" i Notebook-menyen
# Velg DataFrame "df"

# Steg 3: Data Wrangler UI tilbyr:
# - Grid-visning med statistikk per kolonne
# - Innebygde visualiseringer (histogrammer, scatter plots)
# - Over 300 transformasjoner
# - AI-drevne forslag (PROSE)
# - Copilot for naturlig språk → kode

# Steg 4: Eksporter kode tilbake til Notebook
```

### Vanlige feature engineering-operasjoner i Data Wrangler

| Operasjon | Eksempel | Autogenerert kode |
|---|---|---|
| **One-hot encoding** | Kategoriske variabler | `pd.get_dummies(df, columns=[...])` |
| **Binning** | Aldersgrupper | `pd.cut(df['age'], bins=[...])` |
| **Missing values** | Imputering | `df['col'].fillna(df['col'].median())` |
| **Standardisering** | Z-score | `(df['col'] - mean) / std` |
| **Feature crossing** | Kombinasjoner | `df['new'] = df['a'] * df['b']` |
| **Dato-features** | Dag, uke, måned | `df['month'] = df['date'].dt.month` |

### PySpark Feature Engineering Templates

```python
from pyspark.sql import functions as F
from pyspark.ml.feature import VectorAssembler, StandardScaler, StringIndexer

# Kategorisk encoding
indexer = StringIndexer(inputCol="region", outputCol="region_index")

# Numerisk standardisering
assembler = VectorAssembler(
    inputCols=["age", "income", "txn_count"],
    outputCol="features_raw"
)
scaler = StandardScaler(
    inputCol="features_raw",
    outputCol="features_scaled",
    withStd=True,
    withMean=True
)

# Dato-baserte features
df_features = (
    df
    .withColumn("day_of_week", F.dayofweek("event_date"))
    .withColumn("month", F.month("event_date"))
    .withColumn("is_weekend", F.when(
        F.dayofweek("event_date").isin([1, 7]), 1).otherwise(0))
    .withColumn("hour_of_day", F.hour("event_timestamp"))
    .withColumn("days_since_registration",
        F.datediff(F.current_date(), "registration_date"))
)
```

---

## Feature Monitoring and Drift Detection

### Feature Drift-typer

| Drift-type | Beskrivelse | Deteksjonsmetode |
|---|---|---|
| **Data drift** | Endring i feature-distribusjon | KS-test, PSI |
| **Concept drift** | Endring i forholdet mellom features og target | Modell-ytelse over tid |
| **Schema drift** | Endring i datastruktur | Schema-validering |
| **Freshness drift** | Data er ikke oppdatert | Timestamp-sjekk |

### Monitoring i Azure ML Feature Store

```python
from azure.ai.ml.entities import (
    FeatureSetMonitoringSpec,
    MonitorSignal
)

# Konfigurer feature-monitoring
monitoring = FeatureSetMonitoringSpec(
    signal=MonitorSignal(
        feature_data_type_override={
            "txn_7d_count": "numerical",
            "region": "categorical"
        },
        metric_thresholds={
            "numerical": {
                "jensen_shannon_distance": 0.1,
                "population_stability_index": 0.2
            },
            "categorical": {
                "jensen_shannon_distance": 0.1
            }
        }
    ),
    notification_emails=["team@example.no"]
)
```

### Manuell drift-deteksjon i Fabric Notebook

```python
from scipy.stats import ks_2samp
import numpy as np

def detect_feature_drift(reference_df, current_df, features, threshold=0.05):
    """Detekter feature drift mellom referanse- og nåværende data."""
    drift_report = {}

    for feature in features:
        ref_values = reference_df[feature].dropna().values
        curr_values = current_df[feature].dropna().values

        # Kolmogorov-Smirnov test
        stat, p_value = ks_2samp(ref_values, curr_values)

        # Population Stability Index (PSI)
        psi = calculate_psi(ref_values, curr_values, buckets=10)

        drift_report[feature] = {
            "ks_statistic": round(stat, 4),
            "ks_p_value": round(p_value, 4),
            "psi": round(psi, 4),
            "drifted": p_value < threshold or psi > 0.2
        }

    return drift_report

def calculate_psi(reference, current, buckets=10):
    """Beregn Population Stability Index."""
    breakpoints = np.linspace(
        min(reference.min(), current.min()),
        max(reference.max(), current.max()),
        buckets + 1
    )
    ref_counts = np.histogram(reference, breakpoints)[0] / len(reference)
    curr_counts = np.histogram(current, breakpoints)[0] / len(current)

    # Unngå log(0)
    ref_counts = np.clip(ref_counts, 0.001, None)
    curr_counts = np.clip(curr_counts, 0.001, None)

    psi = np.sum((curr_counts - ref_counts) * np.log(curr_counts / ref_counts))
    return psi
```

---

## Referanser

- [What is managed feature store?](https://learn.microsoft.com/en-us/azure/machine-learning/concept-what-is-managed-feature-store) -- Konseptoversikt
- [What is a Feature Store? (AI Playbook)](https://learn.microsoft.com/en-us/ai/playbook/capabilities/model-development/feature-management/) -- Arkitektur og implementasjon
- [Tutorial 1: Develop and register a feature set](https://learn.microsoft.com/en-us/azure/machine-learning/tutorial-get-started-with-feature-store) -- Hands-on tutorial
- [Tutorial 4: Enable online materialization](https://learn.microsoft.com/en-us/azure/machine-learning/tutorial-online-materialization-inference) -- Online feature serving
- [Manage access control for managed feature store](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-setup-access-control-feature-store) -- RBAC og sikkerhet
- [Accelerate data prep with Data Wrangler](https://learn.microsoft.com/en-us/fabric/data-science/data-wrangler) -- Data Wrangler guide
- [Automated ML in Fabric](https://learn.microsoft.com/en-us/fabric/data-science/automated-ml-fabric) -- AutoML med feature engineering

---

## For Cosmo

- **Bruk denne referansen** når brukeren planlegger ML-infrastruktur, trenger feature-gjenbruk på tvers av prosjekter, eller ønsker å operasjonalisere feature engineering.
- Anbefal **Azure ML Managed Feature Store** for organisasjoner med flere ML-team som trenger å dele features. For enkeltprosjekter er **Delta-tabeller i Silver layer** ofte tilstrekkelig.
- **Point-in-time lookups er ikke-forhandlingsbart** for tidsserie-features -- uten dette vil modeller lekke fremtidig informasjon og vise urealistisk god ytelse i testing.
- For norsk offentlig sektor: Feature stores muliggjør **sentral styring** av beregninger som brukes på tvers av etater -- Direktoratet for digital tjenesteutvikling kan dele trafikkfeatures med andre transportetater via feature store-deling.
- Start med **Data Wrangler** for utforskende feature engineering, deretter formaliser i feature set-spesifikasjoner når features er validert og skal til produksjon.
