Python and DirectSharp for data analysis, ML and AI
Compare the result contract and numeric assumptions, not a hand-translated loop. Pandas grouping, model inference and service orchestration have different appropriate idioms. See readability under real business rules for policy branches, initialization and coordination.
Python is a natural choice for notebooks, pandas/Polars, NumPy, scikit-learn, PyTorch and experimentation with AI tooling. DirectSharp targets typed .NET applications. A useful comparison asks where data exploration ends and an application boundary begins, rather than pretending the two ecosystems have identical libraries.
This page compares a telemetry summary, then trains a small classifier in Python and evaluates the same numeric model in DirectSharp. The complete examples are executable comparison sources.
Data processing: service health summaries
Input rows have Service (String), LatencyMs (Double) and Failed (Boolean). Service identifiers in this comparison contain ASCII letters and ordinary spaces. Normalize them with trim and uppercase. Reject absent rows, missing/blank service names, and negative or nonfinite latency. Every valid row contributes to its service's request count, mean latency and error count. Empty input produces an empty result; group order is irrelevant. The compared workloads have counts within Integer range and finite latency sums.
The linked DirectSharp source includes ordinary TelemetryRow and ServiceSummary classes. Record updates counts and the running latency sum; MeanLatency divides by the request count. The Python dataclass declares the matching input shape. These are already decoded typed inputs, not a claim that annotations validate arbitrary JSON.
DirectSharp
// Data workflow: preserve the stated grouping, precision and model-boundary contract.
import System
import System.Collections.Generic
public class TelemetryAnalytics
static action Summarize(rows as IEnumerable <of TelemetryRow>)
returns Dictionary <of String, ServiceSummary>
require rows is not nothing otherwise new ArgumentNullException("rows")
let summaries = new Dictionary <of String, ServiceSummary>(StringComparer.Ordinal)
for_each row in rows
require row is not nothing otherwise new ArgumentException("A row is absent", "rows")
require not String.IsNullOrWhiteSpace(row.Service) otherwise new ArgumentException("Service is required", "rows")
require Double.IsFinite(row.LatencyMs ?? 0) and row.LatencyMs >= 0 otherwise new ArgumentException("Latency must be finite and nonnegative", "rows")
let service = row.Service.Trim().ToUpperInvariant()
summaries[service] ??= new ServiceSummary()
summaries[service].Record(row.LatencyMs ?? 0, row.Failed ?? false)
end for_each
return summaries
end action
end class
The loop consumes its source once and retains one summary object per service. The dictionary assignment ??= creates a summary only when the key is missing or its value is nothing; later rows update the existing object. StringComparer.Ordinal supplies the dictionary's data-key policy independently of case-insensitive program names. This ordinary assignment does not make concurrent updates atomic.
Python
# Data workflow: preserve the stated grouping, precision and model-boundary contract.
from dataclasses import dataclass
from math import isfinite
from collections.abc import Iterable
import pandas as pd
@dataclass(frozen=True)
class TelemetryRow:
service: str
latency_ms: float
failed: bool
def summarize(rows: Iterable[TelemetryRow]) -> dict:
if rows is None:
raise ValueError("rows are required")
records = []
for row in rows:
if row is None:
raise ValueError("A row is absent")
if row.service is None or not row.service.strip():
raise ValueError("Service is required")
if not isfinite(row.latency_ms) or row.latency_ms < 0:
raise ValueError("Latency must be finite and nonnegative")
records.append({"service": row.service.strip().upper(),
"latency_ms": row.latency_ms, "failed": row.failed})
if not records:
return {}
frame = pd.DataFrame.from_records(records)
return frame.groupby("service", sort=False).agg(
requests=("latency_ms", "size"),
mean_latency_ms=("latency_ms", "mean"),
errors=("failed", "sum"),
).to_dict(orient="index")
Pandas makes grouping and column-oriented transformations compact. This version intentionally materializes normalized records before grouping. The guards precede DataFrame construction so invalid latency cannot disappear through missing-value or aggregation behavior.
For rows (" api ", 100, false), ("API", 300, true) and ("worker", 50, false), both produce API: 2 requests, mean 200 ms, 1 error; WORKER: 1 request, mean 50 ms, 0 errors. The executable comparison checks those values, empty/single-pass input and the invalid boundaries.
This is a semantic comparison, not a speed test. The DirectSharp version retains group summaries; the Python version builds a DataFrame as well. Pandas is particularly useful when subsequent work needs joins, column selection, time-series operations or notebook visualization. A streaming .NET service may prefer a one-pass loop when it only needs these counters. Floating-point reductions can differ in the last bits; tests use a stated numeric tolerance rather than claiming bit-identical results on every runtime.
When the data already lives in a database
SQL is another useful comparison. For a table whose ingestion/schema has already enforced the same input contract, the grouping can stay with the data:
SELECT upper(trim(service)) AS service,
count(*) AS requests,
avg(latency_ms) AS mean_latency_ms,
sum(CASE WHEN failed THEN 1 ELSE 0 END) AS errors
FROM telemetry
GROUP BY upper(trim(service));
The comparison executes this query with SQLite against the same valid rows and empty input. The query alone does not reproduce the DirectSharp/Python input guards; those belong to the table's constraints or ingestion boundary. Database collation, trimming, numeric types and query plans must be checked for the actual provider. Fetching an entire table into either application language solely to perform this grouping may be unnecessary.
ML: train in Python, evaluate the same model in .NET
A telemetry classifier uses two features in a fixed order: error_rate and latency_seconds. Its positive class means “inspect.” Training chooses the weights and intercept. Serving computes the probability for a new row. These are different jobs.
Train and export with scikit-learn
# Data workflow: preserve the stated grouping, precision and model-boundary contract.
import json
from pathlib import Path
import numpy as np
from sklearn.linear_model import LogisticRegression
# Columns: error_rate, latency_seconds. Labels: 0 = ordinary, 1 = inspect.
# Synthetic demonstration data, not evidence of predictive quality.
features = np.array([[0.00, 0.1], [0.01, 0.2], [0.02, 0.3],
[0.30, 2.0], [0.50, 3.0], [0.80, 5.0]], dtype=np.float64)
labels = np.array([0, 0, 0, 1, 1, 1])
model = LogisticRegression(random_state=0, max_iter=1000).fit(features, labels)
assert model.classes_.tolist() == [0, 1]
artifact = {"feature_names": ["error_rate", "latency_seconds"],
"positive_label": 1, "weights": model.coef_[0].tolist(),
"intercept": float(model.intercept_[0])}
Path("inspection-model.json").write_text(json.dumps(artifact), encoding="utf-8")
The JSON artifact contains ordinary numbers and ordered feature names. It does not contain executable Python or an arbitrary pickled estimator. The synthetic training data makes the example reproducible; it establishes no predictive quality. A real model needs representative training/held-out data, appropriate metrics and a deliberate update process.
Score with the same numeric contract
These classes evaluate a binary logistic model using a stable sigmoid. They copy the supplied weights, reject empty/nonfinite parameters, check feature count and reject nonfinite inputs or an overflowing score. No preprocessing is silently inferred.
DirectSharp
// Data workflow: preserve the stated grouping, precision and model-boundary contract.
import System
import System.Collections.Generic
import System.Linq
public class LinearClassifier
private readonly weights as List <of Double>
readonly Intercept as Double
readonly FeatureCount as Integer
constructor(weights as IEnumerable <of Double>, intercept as Double)
require arguments.weights is not nothing otherwise new ArgumentNullException("weights")
let copied = arguments.weights.ToList()
require copied.Count > 0 otherwise new ArgumentException("Weights are required", "weights")
for_each weight in copied
require Double.IsFinite(weight) otherwise new ArgumentException("Weights must be finite", "weights")
end for_each
require Double.IsFinite(arguments.intercept) otherwise new ArgumentException("Intercept must be finite", "intercept")
self.weights = copied
self.Intercept = arguments.intercept
self.FeatureCount = copied.Count ?? 0
end constructor
action Probability(features as IReadOnlyList <of Double>)
returns Double
require features is not nothing otherwise new ArgumentNullException("features")
require features.Count = FeatureCount otherwise new ArgumentException("Feature count differs", "features")
let score = Intercept
let index = 0
for_each value in features
require Double.IsFinite(value) otherwise new ArgumentException("Features must be finite", "features")
score += weights[index] * value
index += 1
end for_each
require Double.IsFinite(score) otherwise new ArgumentException("Score must be finite", "features")
if score >= 0 then
return 1 / (1 + Math.Exp(-score))
end if
let exponential = Math.Exp(score)
return exponential / (1 + exponential)
end action
end class
Python
# Data workflow: preserve the stated grouping, precision and model-boundary contract.
from math import exp, isfinite
from collections.abc import Iterable, Sequence
class LinearClassifier:
def __init__(self, weights: Iterable[float], intercept: float):
if weights is None:
raise ValueError("Weights are required")
self.weights = tuple(weights)
if not self.weights or not all(isfinite(value) for value in self.weights):
raise ValueError("Weights must be present and finite")
if not isfinite(intercept):
raise ValueError("Intercept must be finite")
self.intercept = intercept
def probability(self, features: Sequence[float]) -> float:
if features is None or len(features) != len(self.weights):
raise ValueError("Feature count differs")
score = self.intercept
for weight, value in zip(self.weights, features):
if not isfinite(value):
raise ValueError("Features must be finite")
score += weight * value
if not isfinite(score):
raise ValueError("Score must be finite")
if score >= 0:
return 1 / (1 + exp(-score))
exponential = exp(score)
return exponential / (1 + exponential)
C#
// Data workflow: preserve the stated grouping, precision and model-boundary contract.
using System;
using System.Collections.Generic;
using System.Linq;
public class LinearClassifier
{
private readonly List<double> weights;
public double Intercept { get; }
public int FeatureCount { get; }
public LinearClassifier(IEnumerable<double> weights, double intercept)
{
ArgumentNullException.ThrowIfNull(weights);
var copied = weights.ToList();
if (copied.Count == 0) throw new ArgumentException("Weights are required", nameof(weights));
if (copied.Any(weight => !double.IsFinite(weight)))
throw new ArgumentException("Weights must be finite", nameof(weights));
if (!double.IsFinite(intercept)) throw new ArgumentException("Intercept must be finite", nameof(intercept));
this.weights = copied;
Intercept = intercept;
FeatureCount = copied.Count;
}
public double Probability(IReadOnlyList<double> features)
{
ArgumentNullException.ThrowIfNull(features);
if (features.Count != FeatureCount) throw new ArgumentException("Feature count differs", nameof(features));
double score = Intercept;
for (int index = 0; index < features.Count; index++)
{
double value = features[index];
if (!double.IsFinite(value)) throw new ArgumentException("Features must be finite", nameof(features));
score += weights[index] * value;
}
if (!double.IsFinite(score)) throw new ArgumentException("Score must be finite", nameof(features));
if (score >= 0) return 1 / (1 + Math.Exp(-score));
double exponential = Math.Exp(score);
return exponential / (1 + exponential);
}
}
The test host loads the exported weights/intercept and checks all three scorers against scikit-learn's predict_proba on separate probe inputs, with absolute tolerance 1e-12. It checks the feature-name order and positive label before constructing the model. The scorer does not replace the training process or predict from raw telemetry automatically: callers must supply the same features, units and preprocessing used during training.
In an ordinary Python service, calling the trained model's predict_proba directly is often the natural choice. The manual Python scorer here makes the small numeric contract comparable with the CLR implementations. The Python tuple copies the input but does not prevent later attribute reassignment; the DirectSharp/C# private readonly fields enforce that restriction through their type systems.
This numeric interchange covers this binary linear model. It is not an exporter for arbitrary sklearn pipelines, neural networks, tokenizers or preprocessing graphs. For a broader compatible model format, evaluate ONNX Runtime; for .NET-native learning, evaluate ML.NET. Those are ordinary library options, and their full compiler interoperability is not established by this example.
AI application boundaries
| Work | Python fit | DirectSharp/.NET fit |
|---|---|---|
| Explore data and compare models | Notebooks, NumPy, pandas/Polars and established plotting/statistics packages | Integrate results into an existing typed application; .NET also has data libraries, with different coverage |
| Train or fine-tune | Broad scikit-learn/PyTorch ecosystem and hardware integrations | Use an appropriate .NET training library or a separate training service; Python training APIs are not native CLR APIs |
| Serve a compatible model | Python model server or compatible inference runtime | Ordinary CLR scorer/runtime, typed inputs, application validation and existing service hosting |
| Call an LLM or embedding endpoint | A supported Python SDK or HTTP client | A compatible .NET SDK or HttpClient; same remote model, explicit authentication, cancellation and response validation |
| Retrieval and business decisions | Python can orchestrate the whole application | Existing .NET identity, storage and domain services can consume retrieved/generated data |
| Browser interaction | A separate browser client is still needed | A TypeScript client can call the same HTTP boundary; DirectSharp is .NET-first |
Calling a remote model does not require a language-level AI keyword. Both languages can send the same request. The meaningful design choices are where model execution happens, how the request/response schema is checked, and which application rules remain deterministic. A generated answer does not itself approve an order or alter a credit limit; the domain operation still enforces its rule.
Python is often the better place to begin an ML experiment. Keeping a Python training or serving component beside a .NET business application can be a sensible architecture. DirectSharp's opportunity is readable application and integration code, not an assertion that it already replaces Python's scientific ecosystem.
Domain and HTTP boundaries · Other workload choices · Earlier decimal import comparison