DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on Originally published at prepstack.co.in

No Python, No PhD: Train Real ML Models in C# with ML.NET (Regression, Classification, Clustering)

Mattrx ran its predictive features on a Python scikit-learn microservice for two years. We replaced it with ML.NET running in-process inside the existing .NET 9 app — and trained real regression, classification, and clustering models in C# with no separate service, no second language in production, and no implementing gradient descent by hand.

If you're a .NET team that "does ML" by shipping a Python sidecar, you're paying a tax most teams never question: a second runtime, a second deploy pipeline, a second on-call surface, a cross-process hop on every prediction, and a data contract that drifts between two languages. For classical ML — regression, classification, clustering — you usually don't need any of it.

Before vs after

Dimension Before (Python sidecar) After (ML.NET in-process)
Languages in production C# and Python C# only
Deploy pipelines 2 1
Prediction path HTTP to Flask -> scikit-learn in-memory call
Prediction p95 45 ms 2.8 ms
Model artifact pickle in a container image 2 MB .zip, loaded by the app
On-call surface app + ML service app only
Infra cost +$160/mo for the ML service $0 (decommissioned)

The one mental shift

The reason .NET teams reach for Python isn't the models — it's a belief that "real ML needs Python and a math background." For deep-learning research, fair. For the bread-and-butter business ML that 90% of products ship — predict a number, predict a category, group similar things — it's not true.

You don't implement algorithms; you compose a pipeline. You describe data -> transforms -> trainer -> metrics, call Fit(), call Evaluate(). You never write gradient descent, a tree split, or a k-means iteration. The skill that matters is data preparation and honest evaluation — and that's language-agnostic.

Regression — forecast campaign conversions

var pipeline = ml.Transforms.Categorical.OneHotEncoding("ChannelEnc", "Channel")
    .Append(ml.Transforms.Categorical.OneHotEncoding("VerticalEnc", "Vertical"))
    .Append(ml.Transforms.Concatenate("Features",
        "Impressions", "Week1Clicks", "AudienceSize", "ChannelEnc", "VerticalEnc"))
    .Append(ml.Transforms.NormalizeMinMax("Features"))
    .Append(ml.Regression.Trainers.FastTree(labelColumnName: "Label",
        featureColumnName: "Features"));

ITransformer model = pipeline.Fit(split.TrainSet);
var metrics = ml.Regression.Evaluate(model.Transform(split.TestSet));
Enter fullscreen mode Exit fullscreen mode

FastTree is the gradient-boosted tree trainer — you just select it. Result:0.78, RMSE 41 on campaigns averaging ~600 conversions. Same accuracy band as the old scikit-learn model, now with no service to call.

Classification — predict tenant churn

The catch every real churn model hits: class imbalance. Most tenants don't churn, so a model that always predicts "no" looks 94% accurate and is useless. Evaluate on AUC / precision / recall — never raw accuracy.

var m = ml.BinaryClassification.Evaluate(model.Transform(split.TestSet));
// AUC, PositivePrecision, PositiveRecall, F1

// CS can only call ~30 tenants/week -> tune the threshold for PRECISION:
bool flag = prediction.Probability >= 0.62;   // from the PR curve, not 0.5
Enter fullscreen mode Exit fullscreen mode

Result: AUC 0.86, precision 0.71 at the threshold CS actually works. The weekly at-risk list is model-ranked instead of a brittle if.

Clustering — segment tenants (no labels)

var pipeline = ml.Transforms.Concatenate("Features",
        "CampaignsPerMonth", "AvgAudienceSize", "ReportDownloads",
        "SeatUtilization", "ApiCallsPerDay")
    .Append(ml.Transforms.NormalizeMinMax("Features"))   // critical: k-means is scale-sensitive
    .Append(ml.Clustering.Trainers.KMeans("Features", numberOfClusters: 5));
Enter fullscreen mode Exit fullscreen mode

Result: 5 clusters, silhouette 0.52 — distinct enough the product team named them ("power users," "dormant SMBs," "report-only"). The old size-based SQL never surfaced "report-only," a high-churn group hiding inside "enterprise."

Serving in production (the part tutorials skip)

A trained ITransformer is not thread-safe to predict from directly. Use PredictionEnginePool — thread-safe, fast, hot-reloadable:

builder.Services.AddPredictionEnginePool<ChurnInput, ChurnPrediction>()
    .FromFile(modelName: "churn", filePath: "Models/churn.zip", watchForChanges: true);
Enter fullscreen mode Exit fullscreen mode

The nightly retrain job trains, evaluates, and gates on a metric floor before swapping the file — never ship a regression:

if (auc >= 0.80) ml.Model.Save(model, trainSet.Schema, "Models/churn.zip"); // pool hot-reloads
Enter fullscreen mode Exit fullscreen mode

Result: prediction p95 45 ms -> 2.8 ms, zero-downtime model promotion.

The classic mistake: data leakage

Catching one leaked feature (a final_invoice_flag that only existed after churn) dropped offline AUC from a too-good 0.97 to an honest 0.86 — and the honest model is the one that works on live tenants. Every feature must be knowable at prediction time. A suspiciously high AUC is a leak until proven otherwise.

When ML.NET is the wrong call

Deep learning, transformers, LLMs, computer vision belong in Python (or an API) — ML.NET can consume an ONNX model but won't train a state-of-the-art net. If your data scientists live in Python daily, don't fight that. ML.NET wins when the engineering team owns the model and the problem is classical.

The closing mental model

Classical ML is data engineering with an evaluation step — pick the language your app is already in. Regression, classification, and clustering are data -> transforms -> trainer -> metrics. ML.NET gives a .NET team all four in C#, in-process, with no second runtime to operate.

The full guide has the before/after architecture diagrams, every pipeline in full, the trainer cheat-sheet, the pre-ship checklist, and the aggregate Mattrx metrics:

https://prepstack.co.in/blog/no-python-no-phd-train-ml-models-csharp-mlnet

Originally published on PrepStack.

Top comments (0)