Qu

Machine learning

ML report

battery health data: tables, an OLS model, and a report

catalog/qu_ml_report.qu

What it exercises

How it works

Setup — a synthetic cell-aging measurement table

N = 200
cycles = randi(0, 2000, N)
batch_id = randi(0, 2, N)                 # 0, 1, 2 -> three production batches
soc = rand(N, 1) * 100
noise = randn(N, 1) * 0.4

# R0 grows with cycling and drifts slightly by batch (mOhm-scale, in Ohm).
R0_true = 0.032 + 0.012 * (1 - exp(-cycles / 800)) + 0.0015 * batch_id
R0_est = R0_true + 0.001 * noise

df = table(cycles = cycles, soc = soc, batch = batch_id, R0_est = R0_est)
print("{nrow(df)} rows: cycles, soc, batch, R0_est")

Explore — filter, sort, and a per-batch summary

good = filter(df, df.soc > 10)             # drop near-empty-cell readings
top5 = head(sort_by(good, "cycles", true), 5)
print("top 5 by cycles:")
print(top5)

summary = group_by_agg(good, "batch",
    (("R0_est", "mean"), ("R0_est", "std"), ("R0_est", "count")))
print(summary)

Train/test split and the OLS fit

n = nrow(good)
idx = argsort(rand(n, 1))       # a random permutation of 0..n-1 via sort-by-random-key
n_train = round(0.8 * n)

gc = good.cycles
gs = good.soc
gc as vector(n, 1)
gs as vector(n, 1)
X = [gc, gs]                                # (n, 2) design matrix
y = good.R0_est

train_idx = idx[0:n_train - 1]
test_idx = idx[n_train:end]

Xtr = X[train_idx, :]
ytr = y[train_idx]
Xte = X[test_idx, :]
yte = y[test_idx]

model = ols_model(Xtr, ytr)
yhat = model.predict(Xte)
rmse = sqrt(mean((yhat - yte)^2))

print("OLS r2 = {model.score(Xte, yte):.4f}, test RMSE = {rmse:.5f} Ohm")

Report figure

subplot(2, 2, 1)
scatter(good.cycles, good.R0_est, marker="o", alpha=0.4, label="measured")
xlabel("cycles")
ylabel("R0 (Ohm)")
title("Cell aging: R0 vs cycles")
legend()

subplot(2, 2, 2)
scatter(test_idx, yte, marker="o", label="actual", color="blue")
scatter(test_idx, yhat, marker="x", label="OLS predicted", color="orange")
xlabel("test row")
ylabel("R0 (Ohm)")
title("Held-out predictions")
legend()

subplot(2, 2, 3)
histogram(yte - yhat, bins=15, label="OLS residuals")
xlabel("prediction error (Ohm)")

subplot(2, 2, 4)
bar(0 to nrow(summary) - 1, summary.mean_R0_est)
xlabel("batch group (row index)")
ylabel("mean R0 (Ohm)")
title("Mean R0 by batch")

show plot
savefig("aging_report.svg")

Results

printed
200 rows: cycles, soc, batch, R0_est
top 5 by cycles:
cycles        soc  batch    R0_est
------  ---------  -----  --------
  1999  31.499422      2  0.046286
  1980  75.491425      0  0.042876
  1978  58.937209      1  0.044548
  1960  14.611375      0  0.043595
  1959  46.207711      1  0.044512
batch  mean_R0_est  std_R0_est  count_R0_est
-----  -----------  ----------  ------------
0         0.040043    0.003195            66
2         0.042796    0.002991            66
1         0.041288    0.003057            51
OLS r2 = 0.7278, test RMSE = 0.00175 Ohm
  note: this figure uses screen styling and may not hold up in print -- call theme("publication") before saving for a paper (said once per session)
  note: fonts render using whatever is installed on the viewer's machine (none are embedded) -- pass embed_fonts=true (fontfamily preset only) or export .tikz for guaranteed appearance
3.2 3.6 4.0 4.4 ×10−2 0 600 1200 1800 x=1650, y=0.043 x=1515, y=0.045 x=1539, y=0.044 x=76, y=0.034 x=897, y=0.043 x=298, y=0.036 x=1011, y=0.043 x=71, y=0.036 x=1157, y=0.041 x=544, y=0.039 x=1024, y=0.045 x=1425, y=0.042 x=988, y=0.043 x=1767, y=0.043 x=1587, y=0.045 x=1978, y=0.045 x=1157, y=0.042 x=698, y=0.042 x=1072, y=0.041 x=1192, y=0.042 x=587, y=0.041 x=479, y=0.041 x=1777, y=0.043 x=1229, y=0.045 x=993, y=0.042 x=882, y=0.042 x=369, y=0.038 x=1934, y=0.043 x=1507, y=0.043 x=278, y=0.037 x=282, y=0.037 x=1851, y=0.045 x=638, y=0.038 x=1641, y=0.044 x=1999, y=0.046 x=1822, y=0.046 x=217, y=0.035 x=1495, y=0.042 x=1456, y=0.044 x=1341, y=0.043 x=79, y=0.037 x=1695, y=0.042 x=249, y=0.039 x=1896, y=0.043 x=621, y=0.041 x=353, y=0.036 x=1163, y=0.044 x=976, y=0.04 x=619, y=0.038 x=1218, y=0.042 x=1744, y=0.044 x=1664, y=0.043 x=1623, y=0.042 x=336, y=0.038 x=1221, y=0.042 x=352, y=0.039 x=1755, y=0.046 x=1446, y=0.046 x=1305, y=0.042 x=1947, y=0.043 x=203, y=0.035 x=1477, y=0.042 x=1496, y=0.044 x=1960, y=0.044 x=347, y=0.037 x=1252, y=0.044 x=1373, y=0.045 x=1008, y=0.044 x=755, y=0.042 x=1680, y=0.046 x=430, y=0.037 x=150, y=0.036 x=684, y=0.042 x=188, y=0.038 x=1598, y=0.042 x=1425, y=0.045 x=899, y=0.041 x=1458, y=0.044 x=1150, y=0.043 x=663, y=0.041 x=32, y=0.032 x=431, y=0.038 x=1882, y=0.046 x=1401, y=0.043 x=755, y=0.04 x=1124, y=0.044 x=852, y=0.043 x=522, y=0.041 x=1500, y=0.042 x=1721, y=0.043 x=1567, y=0.046 x=1540, y=0.044 x=1851, y=0.044 x=1464, y=0.043 x=933, y=0.043 x=879, y=0.044 x=460, y=0.037 x=662, y=0.039 x=859, y=0.043 x=509, y=0.038 x=1234, y=0.044 x=732, y=0.043 x=1049, y=0.041 x=1087, y=0.042 x=651, y=0.04 x=1833, y=0.045 x=1841, y=0.043 x=280, y=0.039 x=397, y=0.038 x=1459, y=0.043 x=1366, y=0.045 x=1795, y=0.045 x=1757, y=0.044 x=103, y=0.037 x=594, y=0.041 x=1809, y=0.043 x=476, y=0.039 x=587, y=0.038 x=1813, y=0.043 x=223, y=0.035 x=113, y=0.033 x=1325, y=0.042 x=231, y=0.037 x=1693, y=0.042 x=1603, y=0.042 x=1902, y=0.045 x=1327, y=0.042 x=1810, y=0.043 x=872, y=0.04 x=498, y=0.038 x=712, y=0.042 x=81, y=0.033 x=1305, y=0.045 x=1179, y=0.043 x=1249, y=0.044 x=1959, y=0.045 x=114, y=0.034 x=1930, y=0.045 x=1084, y=0.042 x=1776, y=0.046 x=395, y=0.036 x=989, y=0.043 x=457, y=0.04 x=1686, y=0.044 x=1932, y=0.045 x=41, y=0.035 x=379, y=0.038 x=1167, y=0.044 x=249, y=0.035 x=495, y=0.037 x=1005, y=0.041 x=69, y=0.036 x=1162, y=0.04 x=632, y=0.038 x=239, y=0.036 x=106, y=0.033 x=1571, y=0.042 x=1676, y=0.046 x=1604, y=0.046 x=1625, y=0.046 x=1701, y=0.045 x=833, y=0.041 x=1754, y=0.044 x=1980, y=0.043 x=645, y=0.042 x=514, y=0.041 x=319, y=0.038 x=1210, y=0.043 x=1324, y=0.042 x=1405, y=0.042 x=1088, y=0.041 x=1153, y=0.044 x=1409, y=0.045 x=694, y=0.041 x=1713, y=0.044 x=1174, y=0.044 x=51, y=0.034 x=43, y=0.036 x=1356, y=0.042 x=868, y=0.043 x=1923, y=0.044 x=1170, y=0.044 x=1082, y=0.041 Cell aging: R0 vs cycles cycles R0 (Ohm) measured 3.4 3.8 4.2 4.6 ×10−2 0 100 x=17, y=0.042 x=113, y=0.037 x=118, y=0.043 x=134, y=0.044 x=115, y=0.043 x=181, y=0.044 x=141, y=0.043 x=33, y=0.044 x=19, y=0.042 x=18, y=0.041 x=27, y=0.043 x=69, y=0.046 x=78, y=0.043 x=123, y=0.042 x=10, y=0.045 x=85, y=0.044 x=119, y=0.035 x=160, y=0.045 x=38, y=0.044 x=11, y=0.042 x=176, y=0.034 x=14, y=0.045 x=64, y=0.037 x=46, y=0.044 x=150, y=0.041 x=104, y=0.04 x=168, y=0.042 x=131, y=0.033 x=109, y=0.043 x=13, y=0.043 x=93, y=0.043 x=29, y=0.037 x=56, y=0.046 x=147, y=0.044 x=174, y=0.044 x=41, y=0.042 x=5, y=0.036 Held-out predictions test row R0 (Ohm) actual OLS predicted 0 1 2 3 4 5 6 -4 -2 0 2 ×10−3 x=-0.004, y=1 x=-0.003, y=0 x=-0.003, y=3 x=-0.002, y=5 x=-0.002, y=1 x=-0.001, y=2 x=$-6.5\times10^{-4}$, y=6 x=$-1.6\times10^{-4}$, y=3 x=$3.2\times10^{-4}$, y=3 x=$8\times10^{-4}$, y=1 x=0.001, y=3 x=0.002, y=5 x=0.002, y=3 x=0.003, y=0 x=0.003, y=1 prediction error (Ohm) 0 1 2 3 4 ×10−2 0 1 2 x=0, y=0.04 x=1, y=0.043 x=2, y=0.041 Mean R0 by batch batch group (row index) mean R0 (Ohm)
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_ml_report.qu

# qu_ml_report.qu — battery health data: tables, an OLS model, and a report

#

# Exercises the real, currently-runnable data-science surface on a

# realistic workflow: build a measurement table, explore it with the

# working Table builtins, fit a statistical (OLS) aging model, and report

# the result. The companion simulation script produces the data; this

# analyzes). A synthetic in-script dataset stands in for a real CSV log so

# the example is fully self-contained (no external data file needed).

#

# Constructs exercised (all real, verified-working builtins):

#   table(...)                 — typed columnar table literal

#   filter(df, cond)           — boolean row filtering

#   sort_by(df, col, desc)     — sort

#   head(df, n)                — row slice

#   group_by_agg(df, col, ...) — per-group aggregate (multi-spec form)

#   ols_model(X, y)            — OLS fit; .predict / .score / .coef / .r2

#   randperm                   — train/test split

#   subplot, scatter, histogram, bar, legend, savefig


backend auto

seed(7)

#%% Setup — a synthetic cell-aging measurement table

N = 200
cycles = randi(0, 2000, N)
batch_id = randi(0, 2, N)                 # 0, 1, 2 -> three production batches

soc = rand(N, 1) * 100
noise = randn(N, 1) * 0.4

# R0 grows with cycling and drifts slightly by batch (mOhm-scale, in Ohm).

R0_true = 0.032 + 0.012 * (1 - exp(-cycles / 800)) + 0.0015 * batch_id
R0_est = R0_true + 0.001 * noise

df = table(cycles = cycles, soc = soc, batch = batch_id, R0_est = R0_est)
print("{nrow(df)} rows: cycles, soc, batch, R0_est")

#%% Explore — filter, sort, and a per-batch summary

good = filter(df, df.soc > 10)             # drop near-empty-cell readings

top5 = head(sort_by(good, "cycles", true), 5)
print("top 5 by cycles:")
print(top5)

summary = group_by_agg(good, "batch",
    (("R0_est", "mean"), ("R0_est", "std"), ("R0_est", "count")))
print(summary)

#%% Train/test split and the OLS fit

n = nrow(good)
idx = argsort(rand(n, 1))       # a random permutation of 0..n-1 via sort-by-random-key

n_train = round(0.8 * n)

gc = good.cycles
gs = good.soc
gc as vector(n, 1)
gs as vector(n, 1)
X = [gc, gs]                                # (n, 2) design matrix

y = good.R0_est

train_idx = idx[0:n_train - 1]
test_idx = idx[n_train:end]

Xtr = X[train_idx, :]
ytr = y[train_idx]
Xte = X[test_idx, :]
yte = y[test_idx]

model = ols_model(Xtr, ytr)
yhat = model.predict(Xte)
rmse = sqrt(mean((yhat - yte)^2))

print("OLS r2 = {model.score(Xte, yte):.4f}, test RMSE = {rmse:.5f} Ohm")

#%% Report figure

subplot(2, 2, 1)
scatter(good.cycles, good.R0_est, marker="o", alpha=0.4, label="measured")
xlabel("cycles")
ylabel("R0 (Ohm)")
title("Cell aging: R0 vs cycles")
legend()

subplot(2, 2, 2)
scatter(test_idx, yte, marker="o", label="actual", color="blue")
scatter(test_idx, yhat, marker="x", label="OLS predicted", color="orange")
xlabel("test row")
ylabel("R0 (Ohm)")
title("Held-out predictions")
legend()

subplot(2, 2, 3)
histogram(yte - yhat, bins=15, label="OLS residuals")
xlabel("prediction error (Ohm)")

subplot(2, 2, 4)
bar(0 to nrow(summary) - 1, summary.mean_R0_est)
xlabel("batch group (row index)")
ylabel("mean R0 (Ohm)")
title("Mean R0 by batch")

show plot
savefig("aging_report.svg")