latu_ml follows Semantic Versioning. Before 1.0, a minor version may
rename or remove; each such change is listed here with the migration in one line.
0.2.0 — 2026-09-08
No API change. One dependency change, which is what makes this a minor.
The latu requirement is now ~> 0.4, raised from ~> 0.3. Nothing here needs a Latu 0.4
feature. What the floor buys is honesty: it names the Latu this package is developed and tested
against rather than the oldest one that happens to work. Note what it does not buy. A
two-component ~> caps at the next major, so ~> 0.4 still resolves any Latu below 1.0.0,
exactly as ~> 0.3 did. Capping a minor would need ~> 0.4.0, and that is deliberately not
what is here: it would mean a release of this package for every Latu minor. If you are below
Latu 0.4.0, upgrade it alongside this.
Three docstrings were unreadable on hexdocs. Latu.ML.attribute/2, summary/1 and
operator/1 each carried a duplicated first line with a stray heredoc opener between the two
copies. It is a legal escape inside a docstring, so it compiled, formatted and passed
mix docs --warnings-as-errors without complaint.
Latu.ML.load/4 could orphan a cache entry. Loading a search reads its estimator before its
evaluator, and a failure on the second left the first in the server's cache with nothing owning
it. Fixing that turned up the larger half. The code that decides what to give back had no
clause for an unfitted pipeline, whose stages may include a model that is already fitted, so
such a pipeline was invisible to every release path and not only to this one.
The documentation was rewritten. The README, the four guides and usage-rules.md are the
same facts in a different voice, about a fifth shorter. The README now says on its first screen
what shape MLlib is: DataFrame in, DataFrame out, with no fit(x, y) on tensors.
Internals, with no surface change: the load and save folds share two helpers, so the rule that an error path gives back what it cached lives in one place instead of five; a failed second persist in a grid search no longer leaves the first one persisted; and the save layout's three parallel tables over the same six kinds became one.
0.1.0 — 2026-09-07
First release, against Spark 4.2.0, and a companion to latu ~> 0.3.
Spark MLlib from Elixir over Spark Connect: fit, transform, evaluate, model and summary
attributes, save/load in Spark's own on-disk format, the model cache and its bracket, the
ConnectHelper route, pipelines and grid search. The operator surface is generated: 111
rows carrying 991 params, extracted from PySpark's own classes and the server's own
ALLOWED_ATTRIBUTES rather than transcribed. That gives 68 constructors and 623 accessors
across 64 model and summary modules. All 66 runnable operators have been run against a
live server.
Every place the API departs from pyspark.ml is in docs/deviations.md, with why.
- Operators and models.
Latu.ML.fit/2andtransform/2,attribute/2andattribute_frame/2for what a fitted model knows, and a generated accessor module per model and summary class. A model is a handle into a server-side cache, not a value.with_model/3brackets one anddelete/1gives it back. - Reading a
Vectorcolumn.Latu.ML.Functions.vector_to_array/2andarray_to_vector/1, which is whatLatu.collect/2refuses. Spark keeps both in an internal function registry rather than the builtin one, so they are unreachable from SQL and reachable over Connect. The wire form is a plainUnresolvedFunctionwithis_internalleft unset.Summarizertakes the same route and is not wrapped: reading one metric out of the struct it answers with needs field access Latu does not build. - Evaluation and persistence.
Latu.ML.evaluate/2runs an evaluator, andLatu.ML.save/3andLatu.ML.load/3carry a model or an unfitted operator through Spark's own on-disk format. All six evaluators are:probedas a result. PySpark reads what this writes and the reverse;dev/README.mdhas the interop dance. - The
ConnectHelperroute.Latu.ML.Stat's three statistical tests,Latu.ML.assign_clusters/2andfind_frequent_sequential_patterns/2for the two operators that are neither fitted nor applied,from_labels/3and its siblings generated onto their model modules,Latu.ML.Feature.load_default_stop_words/2, andLatu.ML.helper/3under all of them. - Pipelines and tuning.
Latu.ML.pipeline/1builds a pipeline andLatu.ML.param_grid/1,2builds a grid as data;cross_validator/1andtrain_validation_split/1search one. None of them has a wire form in any client, so the folds are this package's own, cut withrand(seed)and a range as Spark does it. A sub-model is released as its metric is read, so a five-by-six search holds one cache entry rather than thirty.collect_sub_models: truekeeps them.larger_better?/1andbest_index/1are public. A saved search carries its encoded grid, andsub_models: truewrites the fold models and asks for them back. A plain load leaves them on disk, which is where this deviates from PySpark. - Documentation. A quick start and a
cookbook, a recipe per model family, both executed by
test/integration/guides_test.exslike the two "coming from" guides.usage-rules.mdships for agents in theusage_rulesconvention, anddocs/cheatsheet.cheatmdputs every hand-written verb on one page withtest/latu/cheatsheet_test.exsfailing on either kind of drift.CONTRIBUTING.mdcarries the extractors, the oracle, the probe and the release checklist.