Profiles / AI & prediction
ML

Machine learning engineer

Density clustering, graph search and data quality gates, in memory on the server.

10 recipes on this page · open in the interactive view

Recipes
  1. Customer segmentation with density clustering
  2. Assign a new customer and update the segments
  3. Compare search algorithms on a delivery network
  4. Data quality gate before training
  5. Audit a database column with a custom rule
  6. Customer segments straight from a SQL table
  7. Linear regression solved by QR decomposition
  8. Is the new model really better? Mann-Whitney on CV folds
  9. Class imbalance audit and balanced class weights
  10. PCA: how much variance does the first component keep?
executed on MentDB

Customer segmentation with density clustering

Group 14 customers by annual spend and visit frequency with a DBSCAN-like clustering (radius 1.0, at least 3 neighbours), then report the size and centroid of each segment, the number of outliers and the distance between the low and high spend centroids.

Pipeline
Script
admin
#1. Customers: annual spend in kEUR, store visits per month;
-> "[cust]" "[[1.2, 1.0], [1.5, 1.2], [1.1, 0.8], [1.4, 1.5], [1.3, 1.1], [4.8, 3.9], [5.1, 4.2], [4.6, 4.0], [5.3, 3.7], [9.5, 2.0], [9.9, 2.3], [10.2, 1.8], [9.7, 2.1], [7.0, 9.0]]";
#2. Density clustering: radius 1.0, at least 3 customers per segment;
ml cluster load_from_json "rx_seg" 1.0 3 [cust];
-> "[k]" (ml cluster nb "rx_seg");
-> "[report]" "";
-> "[clustered]" 0;
#3. Size and centroid of each segment;
for (-> "[i]" 0) (< [i] [k]) (++ "[i]") {
  -> "[n]" (ml cluster nb_point "rx_seg" [i]);
  -> "[sx]" 0; -> "[sy]" 0;
  for (-> "[j]" 0) (< [j] [n]) (++ "[j]") {
    json load "rx_pt" (ml cluster point_get "rx_seg" [i] [j]);
    -> "[sx]" (+ [sx] (json select "rx_pt" "/[0]"));
    -> "[sy]" (+ [sy] (json select "rx_pt" "/[1]"));
  };
  -> "[clustered]" (+ [clustered] [n]);
  -> (concat "[cx" [i] "]") (math round (/ [sx] [n]) 2);
  -> (concat "[cy" [i] "]") (math round (/ [sy] [n]) 2);
  -> "[report]" (concat [report] "segment " [i] ": " [n] " customers, centroid " (env get var (concat "[cx" [i] "]")) " kEUR / " (env get var (concat "[cy" [i] "]")) " visits; ");
};
json unload "rx_pt";
#4. Distance between the low and high spend centroids, and noise;
-> "[gap]" (math round (ml cluster distance "rx_seg" [cx0] [cy0] [cx2] [cy2]) 2);
ml cluster close "rx_seg";
concat [k] " segments | " [report] "outliers: " (- 14 [clustered]) " | low-spend to high-spend distance: " [gap]
Result
mentdb
3 segments | segment 0: 5 customers, centroid 1.3 kEUR / 1.12 visits; segment 1: 4 customers, centroid 4.95 kEUR / 3.95 visits; segment 2: 4 customers, centroid 9.83 kEUR / 2.05 visits; outliers: 1 | low-spend to high-spend distance: 8.58
Go further
executed on MentDB

Assign a new customer and update the segments

Keep last month segments in memory: assign a new customer to the segment with the smallest mean distance, move a customer whose spend grew, remove a churned customer, then draw the segments.

Pipeline
Script
admin
#1. Reload last month segments if they are not in memory;
if (== (ml cluster exist "rx_seg2") 0) {
  ml cluster load_from_json "rx_seg2" 1.0 3 "[[1.2, 1.0], [1.5, 1.2], [1.1, 0.8], [1.4, 1.5], [1.3, 1.1], [4.8, 3.9], [5.1, 4.2], [4.6, 4.0], [5.3, 3.7], [9.5, 2.0], [9.9, 2.3], [10.2, 1.8], [9.7, 2.1]]";
};
#2. New customer: nearest segment by mean distance to its members;
-> "[nx]" 5.6; -> "[ny]" 3.4;
-> "[best]" -1; -> "[dmin]" 1000000;
for (-> "[i]" 0) (< [i] (ml cluster nb "rx_seg2")) (++ "[i]") {
  json load "rx_pts" (ml cluster points "rx_seg2" [i]);
  -> "[s]" 0;
  for (-> "[j]" 0) (< [j] (json count "rx_pts" /)) (++ "[j]") {
    -> "[s]" (+ [s] (ml cluster distance "rx_seg2" [nx] [ny] (json select "rx_pts" (concat "/[" [j] "]/[0]")) (json select "rx_pts" (concat "/[" [j] "]/[1]"))));
  };
  -> "[d]" (/ [s] (json count "rx_pts" /));
  if (< [d] [dmin]) {-> "[dmin]" [d]; -> "[best]" [i];};
};
json unload "rx_pts";
ml cluster point_add "rx_seg2" [best] [nx] [ny];
#3. A customer moved up, another one churned;
ml cluster point_update "rx_seg2" 0 3 2.1 1.9;
ml cluster point_delete "rx_seg2" 2 1;
#4. Draw the updated segments;
ml cluster xy_scatter "rx_seg2"
Result
mentdb
The scatter chart opens in a window of the editor.
Go further
executed on MentDB

Data quality gate before training

Before training a model on CRM rows, check each cell against the rules of the MentDB data quality catalogue (e-mail, integer, valid date) and list the rejected cells and the rows kept.

Pipeline
Script
admin
#1. Raw training rows exported from the CRM;
json load "rx_rows" "[{\"email\": \"ana@shop.com\", \"age\": \"34\", \"signup\": \"2026-03-14\"}, {\"email\": \"bob@shop\", \"age\": \"41\", \"signup\": \"2026-02-30\"}, {\"email\": \"\", \"age\": \"2x\", \"signup\": \"2026-05-02\"}, {\"email\": \"li@mail.org\", \"age\": \"27\", \"signup\": \"2026-06-21\"}, {\"email\": \"zoe@mail.org\", \"age\": \"-\", \"signup\": \"21/06/2026\"}]";
#2. Rule per column, taken from the data quality catalogue;
json load "rx_rules" "{\"email\": \"is_email\", \"age\": \"is_integer\", \"signup\": \"is_date\"}";
json load "rx_fields" (json fields "rx_rules" /);
json load "rx_cat" (dq algorithm show);
json load "rx_report" "{}";
json iobject "rx_report" / "rules_in_catalogue" (json count "rx_cat" /) NUM;
#3. Run each rule on each cell, the rule reads the value in [VAR];
-> "[keep]" 0;
for (-> "[r]" 0) (< [r] (json count "rx_rows" /)) (++ "[r]") {
  -> "[ok]" 1;
  for (-> "[f]" 0) (< [f] (json count "rx_fields" /)) (++ "[f]") {
    -> "[field]" (json select "rx_fields" (concat "/[" [f] "]"));
    -> "[algo]" (json select "rx_rules" (concat "/" [field]));
    if (== (dq algorithm exist [algo]) 0) {exception (1) (concat "Unknown rule " [algo])};
    -> "[VAR]" (json select "rx_rows" (concat "/[" [r] "]/" [field]));
    if (== (eval (dq algorithm get [algo])) 0) {-> "[ok]" 0; json iobject "rx_report" / (concat "row" [r] "_" [field]) [VAR] STR;};
  };
  -> "[keep]" (+ [keep] [ok]);
};
json iobject "rx_report" / "rows_kept" [keep] NUM;
json iobject "rx_report" / "rows_rejected" (- (json count "rx_rows" /) [keep]) NUM;
json unload "rx_rows"; json unload "rx_rules"; json unload "rx_fields"; json unload "rx_cat";
json doc "rx_report"
Result
mentdb
{
  "row2_email": "",
  "rules_in_catalogue": 24.0,
  "row1_email": "bob@shop",
  "rows_rejected": 3.0,
  "row4_age": "-",
  "row4_signup": "21/06/2026",
  "row1_signup": "2026-02-30",
  "row2_age": "2x",
  "rows_kept": 2.0
}
Go further
needs your systems

Audit a database column with a custom rule

Add a company rule for SKU codes, generate the audit script of a MySQL column, run the analysis and drill down on the rows that break the rule. Needs a database connection (demo_cm_mysql), so it is not executed here.

Pipeline
Script
admin
#1. Add a company rule to the catalogue: SKU codes like AB-12345;
dq algorithm set "is_sku" (mql {
	type is_matches_regex [VAR] "[A-Z]{2}-[0-9]{5}";
});
dq algorithm exist "is_sku";
dq algorithm get "is_sku";
#2. Generate the audit script of the column sku of the products table;
-> "[script]" (dq generate "demo_cm_mysql" "products" "sku" "[\"is_sku\", \"is_null_or_empty\"]" "select * from products limit 0, 500");
#3. The rule was only needed to generate the script;
dq algorithm remove "is_sku";
#4. Run the analysis: the script ends with dq analyse;
eval [script];
#5. Drill down on the rows that break the SKU rule;
dq analyse show "demo_cm_mysql" (json doc "dq") "is_sku" "sku" "products" "select * from products limit 0, 500";
Result
This recipe uses your own connections (database, SFTP, SMTP...). Create them with the CM functions, then run it on your server.
Go further
executed on MentDB

Customer segments straight from a SQL table

The customer features live in a database table. The script loads them into DBSCAN with one SQL query (radius 8, at least 2 neighbours), then counts the segments, the customers per segment and the noise points.

Pipeline
Script
admin
#1. A local H2 database plays the customer warehouse;
cm set "tuto_p4h_crm" {execute "db.h2.embedded.config.get"
	"[database]" "./tmp/tuto_p4h_db/crm"
	"[user]" "sa"
	"[password]" "sa"
;};
sql connect "rx_p4h_crm" {cm get "tuto_p4h_crm";};
sql dml "rx_p4h_crm" "create table customers (id int primary key, orders_year double, basket_eur double)";
sql dml "rx_p4h_crm" "insert into customers values (1, 2, 35), (2, 3, 38), (3, 2, 41), (4, 3, 36), (5, 12, 62), (6, 13, 60), (7, 11, 65), (8, 12, 58), (9, 25, 140), (10, 26, 135), (11, 24, 142), (12, 40, 20)";
#2. DBSCAN straight from the SQL query: radius 8, at least 2 neighbours;
ml cluster load "rx_p4h_seg" "tuto_p4h_crm" "orders_year" "basket_eur" 8 2 "select * from customers";
-> "[nb]" (ml cluster nb "rx_p4h_seg");
-> "[sizes]" "";
for (-> "[i]" 0) (< [i] [nb]) (++ "[i]") {
	-> "[sizes]" (concat [sizes] " " (ml cluster nb_point "rx_p4h_seg" [i]));
};
-> "[loaded]" (string contains (ml cluster show) "rx_p4h_seg");
#3. Clean up: model, table, connection, files;
ml cluster close "rx_p4h_seg";
sql dml "rx_p4h_crm" "drop table customers";
sql disconnect "rx_p4h_crm";
cm remove "tuto_p4h_crm";
file delete "tmp/tuto_p4h_db/crm.mv.db";
file delete "tmp/tuto_p4h_db";
concat "model listed in ml cluster show: " [loaded] " | segments: " [nb] " | customers per segment:" [sizes] " | noise: " (- 12 (math eval (concat "0" (string replace [sizes] " " "+")) "{}"))
Result
mentdb
model listed in ml cluster show: 1 | segments: 3 | customers per segment: 4 4 3 | noise: 1
Go further
executed on MentDB

Linear regression solved by QR decomposition

A house price model price = b0 + b1.area + b2.rooms is fitted on 6 sales. The script solves the least squares with a Householder QR (numerically stable), checks it against the built-in solver and gives the condition number of X.

Pipeline
Script
admin
#1. Design matrix X = [1, area m2, rooms] and the prices in kEUR;
-> "[X]" "[[1, 52, 2], [1, 75, 3], [1, 98, 4], [1, 60, 2], [1, 120, 5], [1, 85, 3]]";
-> "[y]" "[210, 290, 365, 228, 450, 318]";
#2. Householder QR: X = Q.R, then R1.beta = Q1t.y on the first 3 rows;
json load "rx_p4h_qr" (matrix qr [X]);
-> "[qty]" (matrix mul (matrix transpose (json select "rx_p4h_qr" "/Q")) [y]);
json load "rx_p4h_r" (json select "rx_p4h_qr" "/R");
json load "rx_p4h_b" [qty];
-> "[R1]" (concat "[" (json select "rx_p4h_r" "/[0]") "," (json select "rx_p4h_r" "/[1]") "," (json select "rx_p4h_r" "/[2]") "]");
-> "[b1]" (concat "[" (json select "rx_p4h_b" "/[0]") "," (json select "rx_p4h_b" "/[1]") "," (json select "rx_p4h_b" "/[2]") "]");
-> "[beta]" (matrix solve [R1] [b1]);
#3. Check against the direct least squares solver and get the condition number;
concat "beta by QR: " [beta] " | beta by matrix solve: " (matrix solve [X] [y]) " | cond X: " (math round (matrix cond [X]) 1)
Result
mentdb
beta by QR: [30.523408732246203,2.7154129405576017,18.279326670173575] | beta by matrix solve: [30.52340873224634,2.715412940557595,18.279326670173713] | cond X: 566.6
Go further
executed on MentDB

Is the new model really better? Mann-Whitney on CV folds

Two models (gradient boosting and random forest) were scored on 7 cross-validation folds (MAE). The script runs a non-parametric Mann-Whitney U test, the difference of medians and the spread, then gives a decision.

Pipeline
Script
admin
#1. MAE of two models on 7 cross-validation folds;
-> "[gbm]" "[3.1, 2.8, 3.4, 2.9, 3.0, 2.7, 3.2]";
-> "[rf]" "[3.6, 3.3, 3.9, 3.5, 3.1, 3.8, 3.4]";
#2. Non-parametric test: the fold errors are not assumed normal;
json load "rx_p4h_mw" (stat mann_whitney [gbm] [rf]);
-> "[p]" (json select "rx_p4h_mw" "/p_value");
#3. Effect size: difference of medians and spread;
-> "[dmed]" (- (stat median [rf]) (stat median [gbm]));
concat "U=" (json select "rx_p4h_mw" "/u") " p=" (math round [p] 4) " (" (json select "rx_p4h_mw" "/method") ") | median gain " (math round [dmed] 2) " | sd gbm " (math round (stat stddev [gbm]) 3) " sd rf " (math round (stat stddev [rf]) 3) " | " (if (< [p] 0.05) {"keep the gradient boosting"} {"no significant difference"})
Result
mentdb
U=4 p=0.0104 (asymptotic) | median gain 0.5 | sd gbm 0.241 sd rf 0.279 | keep the gradient boosting
Go further
executed on MentDB

Class imbalance audit and balanced class weights

Before training a fraud classifier, the script builds the frequency table of the labels, finds the majority class, the imbalance ratio and the balanced class weights w = N / (k . count) to pass to the loss function.

Pipeline
Script
admin
#1. Labels of the training set: 0 = legit, 1 = fraud, 2 = chargeback;
-> "[labels]" "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,0,0,0,0,0,0,0,0,1,0]";
#2. Frequency table and majority class;
json load "rx_p4h_freq" (stat frequency [labels]);
-> "[n]" (json count "rx_p4h_freq" /);
-> "[weights]" "";
#3. Balanced class weights: w = N / (k * count);
for (-> "[i]" 0) (< [i] [n]) (++ "[i]") {
	-> "[c]" (json select "rx_p4h_freq" (concat "/[" [i] "]/count"));
	-> "[weights]" (concat [weights] " class " (json select "rx_p4h_freq" (concat "/[" [i] "]/value")) "=" (math round (math calc "30/([n]*[c])") 2));
};
-> "[cmax]" (json select "rx_p4h_freq" "/[0]/count");
-> "[cmin]" (json select "rx_p4h_freq" (concat "/[" (- [n] 1) "]/count"));
concat "majority class: " (stat mode [labels]) " | imbalance ratio " (/ [cmax] [cmin]) ":1 | weights:" [weights]
Result
mentdb
majority class: [0] | imbalance ratio 25:1 | weights: class 0=0.4 class 1=2.5 class 2=10
Go further
  • Above 10:1, prefer the precision-recall curve to the accuracy.
  • Oversample the minority class with stat dist_random noise around its points.
  • Check that the train and test splits keep the same frequencies with stat chi2_test.
executed on MentDB

PCA: how much variance does the first component keep?

Three correlated sensor features are standardised, their correlation matrix is built and diagonalised. The script gives the eigenvalues, the share of variance of PC1 and checks the decomposition C = V.D.Vt.

Pipeline
Script
admin
#1. Three sensor features, standardised column by column;
-> "[f1]" (stat zscore "[2.5, 0.5, 2.2, 1.9, 3.1, 2.3, 2.0, 1.0, 1.5, 1.1]");
-> "[f2]" (stat zscore "[2.4, 0.7, 2.9, 2.2, 3.0, 2.7, 1.6, 1.1, 1.6, 0.9]");
-> "[f3]" (stat zscore "[1.2, 0.3, 1.0, 1.1, 1.4, 0.9, 1.3, 0.2, 0.8, 0.4]");
#2. Correlation matrix of the standardised data;
-> "[c12]" (stat covariance [f1] [f2]);
-> "[c13]" (stat covariance [f1] [f3]);
-> "[c23]" (stat covariance [f2] [f3]);
-> "[C]" (concat "[[1," [c12] "," [c13] "],[" [c12] ",1," [c23] "],[" [c13] "," [c23] ",1]]");
#3. Eigen decomposition, check C = V D Vt with a diagonal matrix;
json load "rx_p4h_eig" (matrix eigen [C]);
-> "[vals]" (json select "rx_p4h_eig" "/values");
-> "[Vt]" (json select "rx_p4h_eig" "/vectors");
-> "[rebuilt]" (matrix mul (matrix mul (matrix transpose [Vt]) (matrix diag [vals])) [Vt]);
-> "[pc1]" (math round (* 100 (/ (json select "rx_p4h_eig" "/values/[0]") (stat sum [vals]))) 1);
concat "eigenvalues " [vals] " | PC1 explains " [pc1] "% of the variance | rebuild error " (math round (matrix norm_frobenius (matrix sub [rebuilt] [C])) 12)
Result
mentdb
eigenvalues [
  2.7428778970219247,
  0.216649742645597,
  0.040472360332478426
] | PC1 explains 91.4% of the variance | rebuild error 0
Go further
  • Keep the components until 95% of the variance (cumulative sum of the eigenvalues).
  • Project the data on PC1 with matrix mul to get one compact feature.
  • matrix svd gives the same axes without building the correlation matrix.

Also in AI & prediction

DL
Deep learning engineer
Text classifiers, feed-forward networks on CSV data and image networks.
10 recipes
FE
Forecaster & econometrician
Linear and multiple regressions, confidence intervals, ANOVA and polynomial trends.
10 recipes
CB
Conversational bot designer
Bots, intents, users, rights and releases between servers.
10 recipes