Scenario

We want to identify policies expiring in 30 days and prepare features for a future non-renewal model.

Step 1: Define the prediction moment

For historical training:

PredictionDate = ExpiryDate minus 30 days

For current production scoring:

PredictionDate = Today
Eligible policies = Policies expiring 30 days from today

Step 2: Build a point-in-time SQL feature query

DECLARE @PredictionDate DATE = CAST(GETDATE() AS DATE);

SELECT
    p.PolicyId,
    p.CustomerId,
    p.PolicyType,

    DATEDIFF(
        YEAR,
        c.DateOfBirth,
        @PredictionDate
    ) AS CustomerAge,

    CAST(p.PremiumAmount AS FLOAT) AS PremiumAmount,

    DATEDIFF(
        MONTH,
        c.CreatedDate,
        @PredictionDate
    ) AS CustomerTenureMonths,

    (
        SELECT COUNT(*)
        FROM Claims cl
        WHERE cl.CustomerId = p.CustomerId
          AND cl.ClaimDate < @PredictionDate
          AND cl.ClaimDate >= DATEADD(MONTH, -12, @PredictionDate)
    ) AS ClaimsLast12Months,

    (
        SELECT COUNT(*)
        FROM Complaints co
        WHERE co.CustomerId = p.CustomerId
          AND co.ComplaintDate < @PredictionDate
          AND co.ComplaintDate >= DATEADD(DAY, -90, @PredictionDate)
    ) AS ComplaintsLast90Days,

    (
        SELECT COUNT(*)
        FROM Payments py
        WHERE py.CustomerId = p.CustomerId
          AND py.PaymentDate < @PredictionDate
          AND py.WasDelayed = 1
    ) AS PreviousDelayedPayments,

    (
        SELECT COUNT(*)
        FROM Policies previous
        WHERE previous.CustomerId = p.CustomerId
          AND previous.PolicyId <> p.PolicyId
          AND previous.ExpiryDate < @PredictionDate
          AND previous.Renewed = 1
    ) AS PreviousRenewalCount

FROM Policies p
INNER JOIN Customers c
    ON c.CustomerId = p.CustomerId

WHERE p.ExpiryDate >= DATEADD(DAY, 30, @PredictionDate)
  AND p.ExpiryDate <  DATEADD(DAY, 31, @PredictionDate);

This query performs two jobs:

1. Select policies reaching the prediction moment. 
2. Calculate features using earlier historical data.

The AI model has not yet made a prediction.

Step 3: Create the ML.NET input class

using Microsoft.ML.Data;

public sealed class PolicyRiskInput
{
    public float CustomerAge { get; set; }
    public float PremiumAmount { get; set; }
    public float CustomerTenureMonths { get; set; }
    public float ClaimsLast12Months { get; set; }
    public float ComplaintsLast90Days { get; set; }
    public float PreviousDelayedPayments { get; set; }
    public float PreviousRenewalCount { get; set; }
    public string PolicyType { get; set; } = string.Empty;

    [ColumnName("Label")]
    public bool DidNotRenew { get; set; }
}

PolicyId and CustomerId may be retained for traceability, but should not automatically enter the feature vector.

Step 4: Create a runnable ML.NET transformation pipeline

Create the project:

dotnet new console -n FeatureEngineeringDemo
cd FeatureEngineeringDemo 
dotnet add package Microsoft.ML

Use this Program.cs:

using Microsoft.ML;
using Microsoft.ML.Data;

namespace FeatureEngineeringDemo;

public sealed class PolicyRiskInput
{
    public float CustomerAge { get; set; }
    public float PremiumAmount { get; set; }
    public float CustomerTenureMonths { get; set; }
    public float ClaimsLast12Months { get; set; }
    public float ComplaintsLast90Days { get; set; }
    public float PreviousDelayedPayments { get; set; }
    public float PreviousRenewalCount { get; set; }
    public string PolicyType { get; set; } = string.Empty;

    [ColumnName("Label")]
    public bool DidNotRenew { get; set; }
}

internal static class Program
{
    private static void Main()
    {
        var rows = new List<PolicyRiskInput>
        {
            new()
            {
                CustomerAge = 35,
                PremiumAmount = 12500,
                CustomerTenureMonths = 48,
                ClaimsLast12Months = 0,
                ComplaintsLast90Days = 0,
                PreviousDelayedPayments = 0,
                PreviousRenewalCount = 3,
                PolicyType = "Health",
                DidNotRenew = false
            },
            new()
            {
                CustomerAge = 52,
                PremiumAmount = 28500,
                CustomerTenureMonths = 18,
                ClaimsLast12Months = 3,
                ComplaintsLast90Days = 4,
                PreviousDelayedPayments = 2,
                PreviousRenewalCount = 0,
                PolicyType = "Motor",
                DidNotRenew = true
            },
            new()
            {
                CustomerAge = 43,
                PremiumAmount = 45000,
                CustomerTenureMonths = 72,
                ClaimsLast12Months = 1,
                ComplaintsLast90Days = 0,
                PreviousDelayedPayments = 0,
                PreviousRenewalCount = 5,
                PolicyType = "Life",
                DidNotRenew = false
            }
        };

        var mlContext = new MLContext(seed: 42);
        IDataView trainingData =
            mlContext.Data.LoadFromEnumerable(rows);

        string[] numericColumns =
        {
            nameof(PolicyRiskInput.CustomerAge),
            nameof(PolicyRiskInput.PremiumAmount),
            nameof(PolicyRiskInput.CustomerTenureMonths),
            nameof(PolicyRiskInput.ClaimsLast12Months),
            nameof(PolicyRiskInput.ComplaintsLast90Days),
            nameof(PolicyRiskInput.PreviousDelayedPayments),
            nameof(PolicyRiskInput.PreviousRenewalCount)
        };

        var pipeline =
            mlContext.Transforms.Categorical.OneHotEncoding(
                outputColumnName: "PolicyTypeEncoded",
                inputColumnName: nameof(PolicyRiskInput.PolicyType))
            .Append(
                mlContext.Transforms.Concatenate(
                    "NumericFeatures",
                    numericColumns))
            .Append(
                mlContext.Transforms.NormalizeMinMax(
                    "NormalizedNumericFeatures",
                    "NumericFeatures"))
            .Append(
                mlContext.Transforms.Concatenate(
                    "Features",
                    "NormalizedNumericFeatures",
                    "PolicyTypeEncoded"));

        ITransformer featureTransformer =
            pipeline.Fit(trainingData);

        IDataView transformedData =
            featureTransformer.Transform(trainingData);

        var featureVectors = transformedData
            .GetColumn<float[]>("Features")
            .ToArray();

        for (int index = 0; index < featureVectors.Length; index++)
        {
            Console.WriteLine(
                $"Row {index + 1}: " +
                string.Join(", ",
                    featureVectors[index]
                        .Select(value => value.ToString("0.000"))));
        }
    }
}
Run:
dotnet run

The pipeline performs:

PolicyType
    ↓
One-hot encoding

Numeric columns
    ↓
Concatenation
    ↓
Min-max normalization

Normalized numbers + encoded policy type
    ↓
Final Features vector

We have transformed the data, but we have not trained the classification model yet.

Step 5: Understand Fit and Transform

pipeline.Fit(trainingData);

Fit learns required transformation information from training data, such as:

  • Known policy categories
  • Minimum values
  • Maximum values
featureTransformer.Transform(newData);

Transform applies the same learned rules to validation, test and production records.

Correct lifecycle:

Fit feature pipeline on training data
                ↓
Transform training data
Transform validation data
Transform test data
Transform production data

Never independently recalculate production normalization using one incoming policy.