<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Murat Genç</title>
    <description>The latest articles on DEV Community by Murat Genç (@gencmurat).</description>
    <link>https://dev.to/gencmurat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F345901%2Fbfbf95cb-0966-4fe4-a436-61f1f800b6b1.jpeg</url>
      <title>DEV Community: Murat Genç</title>
      <link>https://dev.to/gencmurat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gencmurat"/>
    <language>en</language>
    <item>
      <title>You've Seen the Pipeline. Now Meet the Matrix: The One `Vec&lt;f64&gt;` Behind the 400 Shrink</title>
      <dc:creator>Murat Genç</dc:creator>
      <pubDate>Sun, 02 Aug 2026 06:18:39 +0000</pubDate>
      <link>https://dev.to/gencmurat/youve-seen-the-pipeline-now-meet-the-matrix-the-one-vec-behind-the-400x-shrink-385g</link>
      <guid>https://dev.to/gencmurat/youve-seen-the-pipeline-now-meet-the-matrix-the-one-vec-behind-the-400x-shrink-385g</guid>
      <description>&lt;p&gt;&lt;em&gt;How a single contiguous allocation — and a type system that won't let you feed strings to a scaler — is the real reason datarust fits in 2.3 megabytes.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the last post I showed you the whole datarust workflow: impute, scale, one-hot, train a logistic regression, evaluate, and save it as JSON — all without a Python runtime in sight. The Docker image shrank from ~900 MB to ~8 MB, and the binary was 2.3 MB.&lt;/p&gt;

&lt;p&gt;But I skimmed over something important. I kept saying &lt;em&gt;"the flat memory layout"&lt;/em&gt; as if it were a detail. It isn't. It's the whole bet.&lt;/p&gt;

&lt;p&gt;Every scaler, every encoder, every model, every metric in datarust runs on top of one data structure. If you understand that structure — why it looks the way it does and what it refuses to let you do — the rest of the library stops being magic.&lt;/p&gt;

&lt;p&gt;So let's zoom in. Meet &lt;code&gt;Matrix&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two containers, on purpose
&lt;/h2&gt;

&lt;p&gt;Real data is mixed. Numbers in one column, strings in the next. In Python, everything flows through one giant &lt;code&gt;numpy.ndarray&lt;/code&gt; or a &lt;code&gt;pandas.DataFrame&lt;/code&gt;, and the type system just... shrugs. A string column next to a float column gets coerced into &lt;code&gt;object&lt;/code&gt; dtype. You'll find out at training time, in the form of an error message three frames deep.&lt;/p&gt;

&lt;p&gt;datarust does the opposite. It splits your data into two types at the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StrMatrix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;85.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;24.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;70.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;31.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;95.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;45.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;categorical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;StrMatrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MonthToMonth"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OneYear"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MonthToMonth"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Matrix&lt;/code&gt; is &lt;code&gt;f64&lt;/code&gt; only. &lt;code&gt;StrMatrix&lt;/code&gt; is strings only. They are &lt;em&gt;different types&lt;/em&gt;, and the compiler will refuse to compile a program that hands a string column to a scaler. Not at runtime — at compile time. In the last post I called this "putting on glasses for the first time." Let me show you what it actually buys you.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ColumnTransformer&lt;/code&gt; API is built on that split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="nf"&gt;.add_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scaled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nn"&gt;TransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;          &lt;span class="c1"&gt;// OK&lt;/span&gt;
&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="nf"&gt;.add_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encoded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nn"&gt;CategoricalTransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt; &lt;span class="c1"&gt;// OK&lt;/span&gt;
&lt;span class="c1"&gt;// ct.add_numeric("nope", vec![0],&lt;/span&gt;
&lt;span class="c1"&gt;//     CategoricalTransformerKind::OneHotEncoder(...));               // does not compile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;OneHotEncoder&lt;/code&gt; expects strings. The type system makes that a compile error on a numeric column. sklearn can't do this — every column name is just a string, and &lt;code&gt;OneHotEncoder&lt;/code&gt; will happily run on floats if you don't read the docs carefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Construction is a lie detector
&lt;/h2&gt;

&lt;p&gt;Here's the thing that surprised me most when I actually read the source: &lt;code&gt;Matrix::new&lt;/code&gt; doesn't just store your data. It &lt;em&gt;validates&lt;/em&gt; it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ShapeMismatch { expected: "3 columns", actual: "2 columns at row 1" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a panic. Not &lt;code&gt;NaN&lt;/code&gt; silently appearing in column 3. Not a jagged array that flows downstream and poisons your model. A precise, recoverable error, at the moment of construction.&lt;/p&gt;

&lt;p&gt;In pandas, a ragged column silently becomes &lt;code&gt;object&lt;/code&gt; dtype. In numpy, you get a cryptic error deep in some array-conversion path. In datarust, the constructor is the bouncer, and it's the only place the check needs to happen. From then on, every function that takes a &lt;code&gt;&amp;amp;Matrix&lt;/code&gt; can trust the shape without re-checking.&lt;/p&gt;

&lt;p&gt;There's a matching &lt;code&gt;from_flat&lt;/code&gt; for when you already have a contiguous buffer, and it validates the element count matches the declared shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_flat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.nrows&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.ncols&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even the allocation is checked: &lt;code&gt;rows.checked_mul(cols)&lt;/code&gt; — so you can't ask for a matrix so large that &lt;code&gt;rows * cols&lt;/code&gt; overflows &lt;code&gt;usize&lt;/code&gt; and wraps around into a buffer too small. I know that sounds paranoid. It's also the difference between a library that panics in production and one that returns &lt;code&gt;Err&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one allocation
&lt;/h2&gt;

&lt;p&gt;Now for the part I waved my hands at last time. Here's the internal layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Matrix&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// one contiguous buffer, row-major&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Not &lt;code&gt;Vec&amp;lt;Vec&amp;lt;f64&amp;gt;&amp;gt;&lt;/code&gt; — no allocation per row, no pointers-to-pointers. Element &lt;code&gt;(i, j)&lt;/code&gt; lives at &lt;code&gt;data[i * cols + j]&lt;/code&gt;. Your 50,000×200 matrix is one contiguous &lt;code&gt;Vec&amp;lt;f64&amp;gt;&lt;/code&gt; of ten million floats, in one heap allocation.&lt;/p&gt;

&lt;p&gt;Why does that matter? Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache locality.&lt;/strong&gt; When a scaler walks a row, every element is adjacent in memory. With &lt;code&gt;Vec&amp;lt;Vec&amp;lt;f64&amp;gt;&amp;gt;&lt;/code&gt;, each row is a separate allocation, and walking rows means jumping between unrelated memory pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-vectorization.&lt;/strong&gt; &lt;code&gt;as_slice()&lt;/code&gt; hands the compiler a plain &lt;code&gt;&amp;amp;[f64]&lt;/code&gt;. Modern CPUs get to use SIMD, and LLVM gets to prove the loop has no aliasing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Python boundary doesn't exist.&lt;/strong&gt; This is the quiet killer. In a numpy pipeline, every &lt;code&gt;scaler.transform()&lt;/code&gt; crosses from Python into C and back, materializing Python float objects for the results. In datarust, &lt;code&gt;transform&lt;/code&gt; is a Rust function operating on a Rust buffer. The whole pipeline never leaves native code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This wasn't always the case. Pre-0.3, &lt;code&gt;Matrix&lt;/code&gt; was &lt;code&gt;Vec&amp;lt;Vec&amp;lt;f64&amp;gt;&amp;gt;&lt;/code&gt;. When we switched to the flat layout, the same workloads got dramatically faster &lt;em&gt;with no algorithm changes&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload (50,000 × 200)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Vec&amp;lt;Vec&amp;lt;f64&amp;gt;&amp;gt;&lt;/code&gt; (v0.2)&lt;/th&gt;
&lt;th&gt;flat (v0.3, default)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StandardScaler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;115 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.4 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MinMaxScaler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;81 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RobustScaler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;459 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;137 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline (3 scalers)&lt;/td&gt;
&lt;td&gt;662 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;152 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PCA&lt;/td&gt;
&lt;td&gt;1056 ms&lt;/td&gt;
&lt;td&gt;1008 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OneHotEncoder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;88 ms&lt;/td&gt;
&lt;td&gt;98 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;RobustScaler&lt;/code&gt; got 3.4× faster, &lt;code&gt;Pipeline&lt;/code&gt; 4.4×. Same math, same data — only the memory layout changed. Flat beats fancy.&lt;/p&gt;

&lt;p&gt;The hot loops are designed around that layout. You almost never call &lt;code&gt;get(i, j)&lt;/code&gt; in an inner loop; you grab a row slice and iterate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.iter_rows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;      &lt;span class="c1"&gt;// yields &amp;amp;[f64], zero-copy&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;row(i)&lt;/code&gt; returns a &lt;code&gt;&amp;amp;[f64]&lt;/code&gt; slice, and &lt;code&gt;as_slice()&lt;/code&gt; exposes the whole buffer for the tightest loops. &lt;code&gt;get&lt;/code&gt; exists for the times you need it — and it's bounds-checked in release mode, with a precise panic message. If you want to be defensive, &lt;code&gt;checked_get&lt;/code&gt; returns &lt;code&gt;Option&amp;lt;f64&amp;gt;&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  NaN is a type problem, so we treat it like one
&lt;/h2&gt;

&lt;p&gt;Python has a lovely tradition of letting &lt;code&gt;NaN&lt;/code&gt; flow through a pipeline until it silently infects your coefficients. You train, your model returns &lt;code&gt;nan&lt;/code&gt;, and you spend an afternoon bisecting which column did it.&lt;/p&gt;

&lt;p&gt;datarust's answer is three validators, each with a distinct contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.validate_no_nan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// rejects NaN, allows ±inf — for data that must be complete&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.validate_no_infinite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// rejects ±inf, allows NaN — for imputers&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="nf"&gt;.validate_finite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// rejects both — for models and metrics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subtle one is the middle: &lt;code&gt;validate_no_infinite&lt;/code&gt;. Imputers like &lt;code&gt;SimpleImputer&lt;/code&gt; and &lt;code&gt;KnnImputer&lt;/code&gt; are &lt;em&gt;allowed&lt;/em&gt; to see &lt;code&gt;NaN&lt;/code&gt; — that's the missing-value marker they exist to fix. But an infinity is not a missing value, it's a broken number. So imputers call &lt;code&gt;validate_no_infinite&lt;/code&gt;, not &lt;code&gt;validate_no_nan&lt;/code&gt;. The distinction is baked into the API, and it means the error message tells you &lt;em&gt;which kind&lt;/em&gt; of dirty data you're dealing with.&lt;/p&gt;

&lt;p&gt;When the validation fails, the error tells you exactly where:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InvalidInput("NaN value at position (2, 0)")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Row 2, column 0. Not "somewhere." You go straight to the bad cell.&lt;/p&gt;

&lt;p&gt;And this is exactly why the preprocessing order matters. The &lt;code&gt;Matrix&lt;/code&gt; in the previous post had a missing tenure value in row 2 — a &lt;code&gt;NaN&lt;/code&gt; that a scaler would refuse to touch. You can't skip the imputer and hope the scaler tolerates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;imputer&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;ImputeStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimpleImputer&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;imputed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SimpleImputer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ImputeStrategy&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.fit_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imputed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;categorical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ColumnTransformer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.remainder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Remainder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.add_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scaled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nn"&gt;TransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="nf"&gt;.add_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encoded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nn"&gt;CategoricalTransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.handle_unknown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HandleUnknown&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ignore&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="nf"&gt;.fit_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// imputed → scaled → encoded, one step&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the imputer, that last line fails loudly — &lt;code&gt;invalid input: non-finite value NaN at position (2, 0)&lt;/code&gt; — instead of silently training a model on broken numbers. The validation isn't there to annoy you. It's there to force the pipeline to be honest about what it handles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sparse sibling
&lt;/h2&gt;

&lt;p&gt;One-hot encoding a high-cardinality column produces a lot of zeros. Storing all of them as &lt;code&gt;f64&lt;/code&gt; is waste. So there's a third container: &lt;code&gt;SparseMatrix&lt;/code&gt;, a Compressed Sparse Row matrix mirroring &lt;code&gt;scipy.sparse.csr_matrix&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SparseMatrix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SparseMatrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_triplets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice something: I passed &lt;code&gt;(0, 0)&lt;/code&gt; twice. datarust sums duplicate coordinates (&lt;code&gt;1.0 + 2.0 = 3.0&lt;/code&gt;), drops entries that sum to zero, and sorts each row by column index — so the invariants a reader expects are always true. And if you hand it a malformed CSR array, it rejects it with a specific message instead of indexing out of bounds later. The constructor validates &lt;code&gt;indptr&lt;/code&gt;, column ranges, and per-row ordering. Bad data is rejected here, not at the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do (yet)
&lt;/h2&gt;

&lt;p&gt;Honesty section, because the tone of these posts is "here's what I learned," not "buy my perfect library."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;StrMatrix&lt;/code&gt; is not flat.&lt;/strong&gt; It's still &lt;code&gt;Vec&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt;&lt;/code&gt;, one heap allocation per string. It's fine for categorical columns (which are wide and short), but it's a deliberate asymmetry, and it shows: string-heavy workloads don't get the same cache wins as numeric ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No views.&lt;/strong&gt; &lt;code&gt;select_rows&lt;/code&gt; and &lt;code&gt;select_columns&lt;/code&gt; copy. In numpy you'd get a view with fancy indexing; here, correctness and simplicity win over aliasing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;f32&lt;/code&gt;.&lt;/strong&gt; It's &lt;code&gt;f64&lt;/code&gt; everywhere. An &lt;code&gt;f32&lt;/code&gt; mode would halve memory for big datasets, but it's a sweeping refactor across every estimator. It's on the roadmap, not in the crate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No NumPy interop.&lt;/strong&gt; The &lt;code&gt;serde&lt;/code&gt; feature preserves a nested-JSON wire format for &lt;code&gt;Matrix&lt;/code&gt;, so fitted pipelines round-trip through &lt;code&gt;save_json&lt;/code&gt;/&lt;code&gt;load_json&lt;/code&gt; — but there's no &lt;code&gt;.npy&lt;/code&gt; reader yet. That's explicitly listed as under consideration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is the core feature
&lt;/h2&gt;

&lt;p&gt;Every number in every example in the first article passed through &lt;code&gt;Matrix&lt;/code&gt;. The 179–620× speedups against sklearn's &lt;code&gt;ColumnTransformer&lt;/code&gt;? Partly the flat buffer, partly never crossing the Python/C boundary. The type-safety that caught a &lt;code&gt;OneHotEncoder&lt;/code&gt; on a numeric column before it could run? That's &lt;code&gt;Matrix&lt;/code&gt; vs &lt;code&gt;StrMatrix&lt;/code&gt; being different types. The JSON model that loads in a WASM module or an ARM embedded target? That's a buffer you can serialize and a shape you can trust.&lt;/p&gt;

&lt;p&gt;In other words: the 400× Docker shrink isn't the story. The one &lt;code&gt;Vec&amp;lt;f64&amp;gt;&lt;/code&gt; that makes it possible — and the constructor that refuses to let you put garbage in it — is the story.&lt;/p&gt;

&lt;p&gt;If you want to see the data structure that everything else is built on, the full source is in &lt;code&gt;crates/datarust/src/matrix.rs&lt;/code&gt; — 1,400 lines including tests, and the tests are where the invariants are documented. Or just run the pipeline from the last post and know that every number in it started as a flat buffer that checked its own shape.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;datarust is MIT-licensed and available on &lt;a href="https://crates.io/crates/datarust" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;, with docs at &lt;a href="https://datarust.dev" rel="noopener noreferrer"&gt;datarust.dev&lt;/a&gt;. The roadmap — including the &lt;code&gt;f32&lt;/code&gt; refactor and the &lt;code&gt;.npy&lt;/code&gt; reader — lives at &lt;a href="https://github.com/genc-murat/datarust" rel="noopener noreferrer"&gt;github.com/genc-murat/datarust&lt;/a&gt;. If you want to help make &lt;code&gt;Matrix&lt;/code&gt; generic over float types, now's a good time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>rust</category>
      <category>datarust</category>
    </item>
    <item>
      <title>I Replaced My sklearn Pipeline With Pure Rust. The Docker Image Shrank 400x</title>
      <dc:creator>Murat Genç</dc:creator>
      <pubDate>Sat, 01 Aug 2026 14:37:07 +0000</pubDate>
      <link>https://dev.to/gencmurat/i-replaced-my-sklearn-pipeline-with-pure-rust-the-docker-image-shrank-400x-1deg</link>
      <guid>https://dev.to/gencmurat/i-replaced-my-sklearn-pipeline-with-pure-rust-the-docker-image-shrank-400x-1deg</guid>
      <description>&lt;p&gt;&lt;em&gt;How datarust gives you StandardScaler, OneHotEncoder, LogisticRegression, and JSON model persistence — without BLAS, Python, or the guilt of a 900MB Docker image.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There's a moment in every ML project where the notebook stops being fun and the deployment starts being painful.&lt;/p&gt;

&lt;p&gt;You've got your &lt;code&gt;Pipeline&lt;/code&gt;. You've got your metrics. You hand it to the platform team, and they ask: &lt;em&gt;"Why does this need 900 megabytes?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because scikit-learn pulls in NumPy, SciPy, joblib, threadpoolctl, and a Fortran BLAS library. That's why.&lt;/p&gt;

&lt;p&gt;I've been in that meeting. I've been the person who built the beautiful sklearn pipeline and then watched it drown in Docker layers. So I started wondering: &lt;strong&gt;what if the entire preprocessing + classical ML workflow existed as a pure Rust library — zero BLAS, zero Python runtime — that compiled to a single static binary?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That library is &lt;a href="https://crates.io/crates/datarust" rel="noopener noreferrer"&gt;&lt;strong&gt;datarust&lt;/strong&gt;&lt;/a&gt;. It just hit v0.6, and I want to show you the same workflow you'd write at work — mixed data types, preprocessing, training, evaluation, serialization — but without the Python tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: two commands, zero configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo new churn_classifier &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;churn_classifier
cargo add datarust &lt;span class="nt"&gt;--features&lt;/span&gt; datasets,serde
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No &lt;code&gt;pip install&lt;/code&gt;. No virtual environment. No fighting with BLAS backends. The default build has &lt;strong&gt;zero external dependencies&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data: mixed types, real problems
&lt;/h2&gt;

&lt;p&gt;Real data is messy. You've got numbers (&lt;code&gt;tenure&lt;/code&gt;, &lt;code&gt;monthly_charge&lt;/code&gt;), categories (&lt;code&gt;contract_type&lt;/code&gt;), missing values, and different scales. In datarust, data lives in two containers: &lt;code&gt;Matrix&lt;/code&gt; for numbers and &lt;code&gt;StrMatrix&lt;/code&gt; for strings.&lt;/p&gt;

&lt;p&gt;They're deliberately separate types — the compiler won't let you accidentally feed a string column to a scaler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StrMatrix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Numeric: tenure (months), monthly_charge ($), age&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;85.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;24.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;70.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;31.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;95.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;45.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// missing tenure&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;48.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;55.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;52.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Categorical: contract_type&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;categorical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;StrMatrix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MonthToMonth"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OneYear"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MonthToMonth"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TwoYear"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Target: 1 = churned, 0 = stayed&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Already, something nice has happened: &lt;code&gt;Matrix::new&lt;/code&gt; validates that all rows have the same length. No silent broadcasting bugs. If your data is ragged, you find out at construction, not three functions deep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preprocessing: the part that usually hurts
&lt;/h2&gt;

&lt;p&gt;This is where most Rust ML libraries tap out. They give you a model — maybe a linear regression — but ask you to handle scaling, encoding, and imputation yourself. datarust takes the sklearn approach: these are first-class citizens.&lt;/p&gt;

&lt;p&gt;I want to do four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Impute&lt;/strong&gt; the missing tenure value (replace with the mean)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale&lt;/strong&gt; the numeric columns to zero mean / unit variance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-hot encode&lt;/strong&gt; the contract type&lt;/li&gt;
&lt;li&gt;Do all of the above in a single, composable step&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the &lt;code&gt;ColumnTransformer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;ColumnTransformer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Remainder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;HandleUnknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;imputer&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;ImputeStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimpleImputer&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;scaler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;transformer_kind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TransformerKind&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CategoricalTransformerKind&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;categorical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ColumnTransformer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.remainder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Remainder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.add_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"tenure_imputed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;           &lt;span class="c1"&gt;// column 0 of numeric matrix&lt;/span&gt;
        &lt;span class="nn"&gt;TransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;SimpleImputer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SimpleImputer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ImputeStrategy&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Mean&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.add_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"features_scaled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;        &lt;span class="c1"&gt;// columns 1, 2 of numeric matrix&lt;/span&gt;
        &lt;span class="nn"&gt;TransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.add_categorical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"contract_encoded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;           &lt;span class="c1"&gt;// column 0 of categorical matrix&lt;/span&gt;
        &lt;span class="nn"&gt;CategoricalTransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.handle_unknown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HandleUnknown&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ignore&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="nf"&gt;.fit_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The output &lt;code&gt;x&lt;/code&gt; is a single numeric &lt;code&gt;Matrix&lt;/code&gt; — imputed, scaled, encoded — ready to feed into any model.&lt;/p&gt;

&lt;p&gt;There's one design choice here that I genuinely love: &lt;strong&gt;the type system enforces correctness&lt;/strong&gt;. &lt;code&gt;add_numeric&lt;/code&gt; takes a &lt;code&gt;TransformerKind&lt;/code&gt;, &lt;code&gt;add_categorical&lt;/code&gt; takes a &lt;code&gt;CategoricalTransformerKind&lt;/code&gt;. You can't accidentally put a &lt;code&gt;OneHotEncoder&lt;/code&gt; (which expects strings) on a numeric column. The compiler catches it.&lt;/p&gt;

&lt;p&gt;After years of sklearn's everything-is-a-string-column-name dynamism, this feels like putting on glasses for the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training the model
&lt;/h2&gt;

&lt;p&gt;Now for the classifier. datarust v0.6 ships &lt;code&gt;LogisticRegression&lt;/code&gt; with both binary and multiclass support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;linear_model&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogisticSolver&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;traits&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Predictor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.with_solver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LogisticSolver&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Svd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.with_max_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;probabilities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.predict_proba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// (n, 2) matrix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model uses Newton-Raphson (IRLS for binary, full multinomial for multiclass) under the hood — the same algorithm sklearn uses, implemented in pure Rust with a Cholesky or SVD linear solver. No BLAS. No LAPACK.&lt;/p&gt;

&lt;p&gt;And here's something I didn't expect to care about: &lt;strong&gt;it's fast&lt;/strong&gt;. On benchmarks comparing against scikit-learn 1.6 on an M-series Mac, datarust's &lt;code&gt;ColumnTransformer&lt;/code&gt; was 179–620× faster than sklearn's on the same data. Part of that is the flat memory layout (a single contiguous &lt;code&gt;Vec&amp;lt;f64&amp;gt;&lt;/code&gt; instead of Python objects), and part is simply not crossing the Python/C boundary on every operation.&lt;/p&gt;

&lt;p&gt;I'm not claiming datarust beats sklearn everywhere — sklearn's PCA (which calls LAPACK) is dramatically faster on large matrices. But for preprocessing and linear models, pure Rust holds its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation: don't stop at accuracy
&lt;/h2&gt;

&lt;p&gt;A common mistake is to stop at accuracy. For churn — where maybe 20% of customers leave — accuracy is misleading. A model that predicts "nobody churns" is 80% accurate and completely useless.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;classification&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;accuracy_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;prec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;precision_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recall_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;f1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f1_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;auc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;roc_auc_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;probabilities&lt;/span&gt;&lt;span class="nf"&gt;.column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;log_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;probabilities&lt;/span&gt;&lt;span class="nf"&gt;.column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;1e-15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this churn problem, I care most about &lt;strong&gt;recall&lt;/strong&gt; — how many of the customers who &lt;em&gt;will&lt;/em&gt; churn did I catch? A false positive (offering a discount to someone who wasn't leaving) costs a little; a false negative (losing a customer I could have saved) costs a lot.&lt;/p&gt;

&lt;p&gt;And if this were a multiclass problem, the same metrics auto-detect the label count and switch to macro-averaging. No &lt;code&gt;average='macro'&lt;/code&gt; parameter needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-validate properly
&lt;/h2&gt;

&lt;p&gt;Evaluating on training data is the oldest sin in machine learning. Let's do it properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;model_selection&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StratifiedKFold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;skf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;StratifiedKFold&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.with_n_splits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.with_shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.with_random_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;fold_accuracies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;skf&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x_train&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="nf"&gt;.select_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;train_idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;x_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="nf"&gt;.select_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;test_idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_idx&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;test_idx&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;fold_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.with_solver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LogisticSolver&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Svd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;fold_model&lt;/span&gt;&lt;span class="nf"&gt;.fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fold_pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fold_model&lt;/span&gt;&lt;span class="nf"&gt;.predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;fold_accuracies&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;accuracy_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;fold_pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stratified means each fold preserves the churn/stay ratio — critical for imbalanced data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save the model: JSON, not pickle
&lt;/h2&gt;

&lt;p&gt;You trained the model. Now you need to deploy it. In Python, you'd &lt;code&gt;joblib.dump&lt;/code&gt; a pickle file — a binary blob that only Python can read, tied to the exact library versions that created it.&lt;/p&gt;

&lt;p&gt;datarust uses JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scaler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;TransformerKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="nf"&gt;.with_estimator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="nf"&gt;.fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Save to disk&lt;/span&gt;
&lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;save_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"churn_model.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... later, in the serving binary ...&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SupervisedPipeline&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LogisticRegression&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;datarust&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"churn_model.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Already fitted — predict immediately&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="nf"&gt;.predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;new_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The saved model is human-readable JSON. You can &lt;code&gt;cat&lt;/code&gt; it. You can diff it. You can load it in a Rust service, a CLI tool, or a WASM module — anything that can parse JSON and link the datarust crate.&lt;/p&gt;

&lt;p&gt;Try that with a pickle file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deployment payoff
&lt;/h2&gt;

&lt;p&gt;The entire workflow above — preprocessing, training, evaluation, cross-validation, serialization — runs in a single Rust binary with &lt;strong&gt;zero external dependencies&lt;/strong&gt; by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cargo build --release
$ ls -lh target/release/churn_service
-rwxr-xr-x  2.3MB  churn_service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 2.3-megabyte binary contains the entire ML pipeline.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-compile&lt;/strong&gt; it to any target Rust supports (Linux, macOS, Windows, WASM, ARM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it in the browser&lt;/strong&gt; via WASM — load the JSON model, call &lt;code&gt;predict&lt;/code&gt;, return the result&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed it in a microservice&lt;/strong&gt; written in Go, Node, or Python via FFI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship it as a CLI tool&lt;/strong&gt; that data scientists can run locally without installing Python&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare that to a Python sklearn Docker image:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;datarust&lt;/th&gt;
&lt;th&gt;Python + sklearn&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binary size&lt;/td&gt;
&lt;td&gt;2.3 MB&lt;/td&gt;
&lt;td&gt;~30 MB (venv)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker image&lt;/td&gt;
&lt;td&gt;~8 MB&lt;/td&gt;
&lt;td&gt;~900 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WASM support&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;td&gt;pyodide (~30 MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External deps&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;NumPy, SciPy, joblib&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's not there yet
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this library does everything sklearn does. Here's what's missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No trees or ensembles.&lt;/strong&gt; No &lt;code&gt;RandomForest&lt;/code&gt;, no &lt;code&gt;GradientBoosting&lt;/code&gt;. This is the biggest gap, planned for v0.7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SVM.&lt;/strong&gt; No &lt;code&gt;SVC&lt;/code&gt; with RBF kernels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No deep learning.&lt;/strong&gt; That's deliberate — &lt;a href="https://github.com/huggingface/candle" rel="noopener noreferrer"&gt;candle&lt;/a&gt; and &lt;a href="https://github.com/tracel-ai/burn" rel="noopener noreferrer"&gt;burn&lt;/a&gt; own that space in Rust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PCA is slower than sklearn's&lt;/strong&gt; on large matrices. sklearn calls LAPACK; datarust uses a pure-Rust Jacobi eigensolver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No CSV reader built-in.&lt;/strong&gt; You construct the &lt;code&gt;Matrix&lt;/code&gt; yourself. (A &lt;code&gt;csv&lt;/code&gt; feature is planned.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workflow absolutely needs &lt;code&gt;RandomForest&lt;/code&gt; or a transformer, datarust isn't there yet. But if you're doing logistic regression, linear models, clustering, or — especially — &lt;strong&gt;preprocessing pipelines that need to run outside Python&lt;/strong&gt;, it's ready today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full feature set
&lt;/h2&gt;

&lt;p&gt;Here's what v0.6 gives you, all with zero dependencies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preprocessing:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;StandardScaler&lt;/code&gt;, &lt;code&gt;MinMaxScaler&lt;/code&gt;, &lt;code&gt;RobustScaler&lt;/code&gt;, &lt;code&gt;MaxAbsScaler&lt;/code&gt;, &lt;code&gt;Normalizer&lt;/code&gt;, &lt;code&gt;Binarizer&lt;/code&gt;, &lt;code&gt;KBinsDiscretizer&lt;/code&gt;, &lt;code&gt;QuantileTransformer&lt;/code&gt;, &lt;code&gt;PowerTransformer&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encoding:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;OneHotEncoder&lt;/code&gt;, &lt;code&gt;OrdinalEncoder&lt;/code&gt;, &lt;code&gt;LabelEncoder&lt;/code&gt;, &lt;code&gt;TargetEncoder&lt;/code&gt;, &lt;code&gt;FrequencyEncoder&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imputation:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SimpleImputer&lt;/code&gt; (mean/median/most_frequent/constant), &lt;code&gt;KnnImputer&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature engineering:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;PolynomialFeatures&lt;/code&gt;, &lt;code&gt;VarianceThreshold&lt;/code&gt;, &lt;code&gt;SelectKBest&lt;/code&gt;, &lt;code&gt;PCA&lt;/code&gt;, &lt;code&gt;TruncatedSVD&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Models:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;LinearRegression&lt;/code&gt;, &lt;code&gt;Ridge&lt;/code&gt;, &lt;code&gt;Lasso&lt;/code&gt;, &lt;code&gt;LogisticRegression&lt;/code&gt;, &lt;code&gt;KMeans&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics:&lt;/strong&gt;&lt;br&gt;
MSE, MAE, RMSE, R², accuracy, precision, recall, F1, ROC-AUC, PR-AUC, log loss, confusion matrix, Cohen's kappa, Matthews correlation, silhouette score&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Utilities:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;Pipeline&lt;/code&gt;, &lt;code&gt;ColumnTransformer&lt;/code&gt;, &lt;code&gt;KFold&lt;/code&gt;, &lt;code&gt;StratifiedKFold&lt;/code&gt;, &lt;code&gt;cross_val_score&lt;/code&gt;, &lt;code&gt;train_test_split&lt;/code&gt;, JSON serialization&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add datarust &lt;span class="nt"&gt;--features&lt;/span&gt; datasets,serde
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever wanted to run an sklearn-style pipeline without the Python tax, give it a try.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;datarust is MIT-licensed and available on &lt;a href="https://crates.io/crates/datarust" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;, with full documentation at &lt;a href="https://genc-murat.github.io/datarust/" rel="noopener noreferrer"&gt;genc-murat.github.io/datarust&lt;/a&gt;. The roadmap lives at &lt;a href="https://github.com/genc-murat/datarust" rel="noopener noreferrer"&gt;github.com/genc-murat/datarust&lt;/a&gt; — if you want to contribute a &lt;code&gt;DecisionTree&lt;/code&gt; or a &lt;code&gt;CountVectorizer&lt;/code&gt;, now's a good time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>rust</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
