Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions docs/design-rationale.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ <h2>1. No hidden regularization</h2>
<h2>2. Regularization strength</h2>
<p>The parameter for regularization strength is alpha. The scikit-learn API uses C, the inverse of the penalty. The ryxcommar post complains that C is two steps removed from the textbook lambda.</p>
<p>The flow-scikit library uses <code>penalty_l2(alpha)</code>. This is a partial answer. As ryxcommar notes, alpha itself is one step removed from lambda.</p>
<p>alpha and C are not reciprocals of each other. scikit-learn's <code>LogisticRegression</code> minimises the sum of the per-sample losses plus <code>0.5 * ||w||^2 / C</code>. <code>logistic_regression_fit</code> minimises the mean loss plus <code>0.5 * alpha * ||w||^2</code>. Putting the two in the same units leaves <code>alpha = 1 / (C * n_samples)</code>, so the sample count is part of the conversion. <code>penalty_l2_from_c(C, n_samples)</code> in <code>lib/scikit/linear.flow</code> performs it.</p>
<p>Issue #408 recorded what happens when the conversion is skipped. The canonical benchmark passed a flat <code>penalty_l2(0.001)</code> on both iris and digits against scikit-learn's <code>C=1.0</code>. On iris that is 8.3x weaker than the model it was published beside, and the Flow fit carried a coefficient Frobenius norm of 9.16 against scikit-learn's 4.52 at the same accuracy. A library that asks for the penalty explicitly and then ships a benchmark that picks a number with no stated relationship to the comparison is doing the thing this page objects to.</p>
<p>alpha and C are not reciprocals of each other. scikit-learn&#x27;s <code>LogisticRegression</code> minimises the sum of the per-sample losses plus <code>0.5 * ||w||^2 / C</code>. <code>logistic_regression_fit</code> minimises the mean loss plus <code>0.5 * alpha * ||w||^2</code>. Putting the two in the same units leaves</p>
<p>```</p>
<p>alpha = 1 / (C * n_samples)</p>
<p>```</p>
<p>so the sample count is part of the conversion. <code>penalty_l2_from_c(C, n_samples)</code> in <code>lib/scikit/linear.flow</code> performs it.</p>
<p><a href="https://github.com/godofecht/flow-scikit/issues/408">Issue <a href="https://github.com/godofecht/flow-scikit/issues/408">#408</a></a> recorded what happens when the conversion is skipped. The canonical benchmark passed a flat <code>penalty_l2(0.001)</code> on both iris and digits against scikit-learn&#x27;s <code>C=1.0</code>. On iris that is 8.3x weaker than the model it was published beside, and the Flow fit carried a coefficient Frobenius norm of 9.16 against scikit-learn&#x27;s 4.52 at the same accuracy. A library that asks for the penalty explicitly and then ships a benchmark that picks a number with no stated relationship to the comparison is doing the thing this page objects to.</p>
<p>The intercept is not penalized on either side. <code>logistic_regression_fit</code> augments theta with the bias at index <code>n</code> and applies the L2 term over <code>0 to n</code>, which leaves the bias out.</p>
<h2>3. Fit returns a value</h2>
<p>Estimator fitting returns a value. Hacker News commenter zeec123 argues <code>fit</code> should return a function from input space to output space and avoid modifying internal state.</p>
<p>Flow structs pass by value. Every estimator fit function returns a fitted struct.</p>
<p><code>Pipeline</code> was the one exception until recently: <code>pipeline_fit</code> returned void and mutated in place, which is the single case where the criticism landed against this library rather than against scikit-learn. It now returns a fitted <code>Pipeline</code>, so the rule holds without exception.</p>
<h2>4. No hidden threshold</h2>
<p>The <code>predict</code> functions return probabilities. The <code>decide</code> functions take an explicit threshold.</p>
<p>For a binary target the probability is P(<code>classes[1]</code>), which is the column scikit-learn&#x27;s <code>predict_proba</code> puts it in, so raising the threshold makes <code>classes[1]</code> rarer.</p>
<p>The codebase contains nine <code>*_decide</code> functions:</p>
<ul>
<li><code>random_forest_classifier_decide</code></li>
Expand All @@ -38,15 +43,18 @@ <h2>4. No hidden threshold</h2>
<h2>5. Structured validation errors</h2>
<p>The <code>ValidationResult</code> struct in <code>lib/scikit/validation.flow</code> carries a numeric code and a string message. The <code>ensure_*</code> validation functions share a consistent naming convention. G2 reviewers raised unhelpful error messages in both 2019 and 2024. The structured approach provides clear diagnostics.</p>
<h2>Known gaps</h2>
<p>Recorded here so this page is not one-sided.</p>
<ul>
<li><a href="https://github.com/godofecht/flow-scikit/issues/353">Issue <a href="https://github.com/godofecht/flow-scikit/issues/353">#353</a></a>: no coefficient inference. No standard errors, no p-values, no model summary.</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/354">Issue <a href="https://github.com/godofecht/flow-scikit/issues/354">#354</a></a>: decision trees cannot handle categorical features natively. A caller must label-encode, which imposes false ordinality, or one-hot encode, which inflates cardinality.</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/357">Issue <a href="https://github.com/godofecht/flow-scikit/issues/357">#357</a></a>: logistic regression offers LBFGS only. Newton&#x27;s method is the textbook default and is what R&#x27;s <code>glm</code> uses, which matters when porting a model between languages.</li>
</ul>
<p>A hardcoded list here went stale twice, so it no longer lives in prose. The</p>
<p>current gaps are the <a href="https://github.com/godofecht/flow-scikit/issues">open issues</a>,</p>
<p>each carrying verified claims and a named fix direction. This page records</p>
<p>positions and their reasoning; the tracker records what is missing.</p>
<h2>Closed since this page was written</h2>
<ul>
<li><a href="https://github.com/godofecht/flow-scikit/issues/353">Issue <a href="https://github.com/godofecht/flow-scikit/issues/353">#353</a></a>: OLS coefficient inference exists. <code>OLSInference</code> exposes the residual standard error, coefficient covariance, standard errors and t statistics, computed from the QR factorization the fit already performs and checked against hand-derived values. It refuses penalized and rank-deficient fits rather than reporting a covariance that does not mean what the name promises. Logistic inference remains open for the same honesty reason: the Newton Hessian carries the penalty term.</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/354">Issue <a href="https://github.com/godofecht/flow-scikit/issues/354">#354</a></a>: decision trees handle categorical features natively. A per-feature flag routes a column through Breiman&#x27;s optimal subset split (sort levels by mean response, evaluate the k-1 contiguous partitions) with the left set stored as a 128-bit mask in the node. On a parity-of-level dataset the categorical path reaches accuracy 1.0 where any threshold on a label encoding tops out at 0.75. Forest and Bagging builders remain numeric-only for now.</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/357">Issue <a href="https://github.com/godofecht/flow-scikit/issues/357">#357</a></a>: logistic regression offers an explicit solver choice. Newton/IRLS is available and reaches the optimum in 2 to 6 iterations where LBFGS takes 5 to 27 on low-dimensional problems; LBFGS stays the default because Newton pays O(d^2) memory and O(d^3) per iteration on wide data. On a singular Hessian the solve zeroes the dependent coordinate and takes the genuine Newton step for the reduced problem.</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/355">Issue <a href="https://github.com/godofecht/flow-scikit/issues/355">#355</a></a>: a bootstrap cross-validator now exists. <code>BootstrapOOB</code> in <code>lib/scikit/model_selection.flow</code> implements plain out-of-bag bootstrap and names the variant, since scikit-learn removed its own <code>Bootstrap</code> class for inventing non-standard semantics under a misleading name.</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/356">Issue <a href="https://github.com/godofecht/flow-scikit/issues/356">#356</a></a>: <code>pipeline_fit</code> now returns a fitted <code>Pipeline</code> instead of mutating.</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/435">Issue <a href="https://github.com/godofecht/flow-scikit/issues/435">#435</a></a>: <code>logistic_predict_proba</code> returned P(<code>classes[0]</code>) while <code>logistic_decide</code> labelled a probability above the threshold <code>classes[1]</code>, so the two composed to an inverted binary label. The probability is now P(<code>classes[1]</code>).</li>
<li><a href="https://github.com/godofecht/flow-scikit/issues/449">Issue <a href="https://github.com/godofecht/flow-scikit/issues/449">#449</a></a>: seven call sites in <code>model_selection.flow</code> and <code>pipeline.flow</code> handed <code>logistic_predict</code>&#x27;s argmax class labels to a <code>decide</code> function, which thresholds them as probabilities. For classes {0, 1} that threshold is the identity and every fixture in the repository used 0/1 labels. For classes {2, 7} both labels exceed 0.5 and every prediction came out as 7. Cross-validation now scores <code>logistic_predict</code>&#x27;s labels directly, and <code>pipeline_predict_proba</code> returns P(<code>classes[1]</code>) for a binary classifier pipeline and null where no single column is the probability.</li>
</ul>
</section></main><footer><a class="wordmark" href="index.html">flow<span>~</span>scikit</a><p>Compiled classical ML in Flow, with reproducible sklearn comparisons.</p></footer></body></html>
12 changes: 7 additions & 5 deletions docs/design-rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ The `ValidationResult` struct in `lib/scikit/validation.flow` carries a numeric

## Known gaps

Recorded here so this page is not one-sided.

- Issue #353: no coefficient inference. No standard errors, no p-values, no model summary.
- Issue #354: decision trees cannot handle categorical features natively. A caller must label-encode, which imposes false ordinality, or one-hot encode, which inflates cardinality.
- Issue #357: logistic regression offers LBFGS only. Newton's method is the textbook default and is what R's `glm` uses, which matters when porting a model between languages.
A hardcoded list here went stale twice, so it no longer lives in prose. The
current gaps are the [open issues](https://github.com/godofecht/flow-scikit/issues),
each carrying verified claims and a named fix direction. This page records
positions and their reasoning; the tracker records what is missing.

## Closed since this page was written

- Issue #353: OLS coefficient inference exists. `OLSInference` exposes the residual standard error, coefficient covariance, standard errors and t statistics, computed from the QR factorization the fit already performs and checked against hand-derived values. It refuses penalized and rank-deficient fits rather than reporting a covariance that does not mean what the name promises. Logistic inference remains open for the same honesty reason: the Newton Hessian carries the penalty term.
- Issue #354: decision trees handle categorical features natively. A per-feature flag routes a column through Breiman's optimal subset split (sort levels by mean response, evaluate the k-1 contiguous partitions) with the left set stored as a 128-bit mask in the node. On a parity-of-level dataset the categorical path reaches accuracy 1.0 where any threshold on a label encoding tops out at 0.75. Forest and Bagging builders remain numeric-only for now.
- Issue #357: logistic regression offers an explicit solver choice. Newton/IRLS is available and reaches the optimum in 2 to 6 iterations where LBFGS takes 5 to 27 on low-dimensional problems; LBFGS stays the default because Newton pays O(d^2) memory and O(d^3) per iteration on wide data. On a singular Hessian the solve zeroes the dependent coordinate and takes the genuine Newton step for the reduced problem.
- Issue #355: a bootstrap cross-validator now exists. `BootstrapOOB` in `lib/scikit/model_selection.flow` implements plain out-of-bag bootstrap and names the variant, since scikit-learn removed its own `Bootstrap` class for inventing non-standard semantics under a misleading name.
- Issue #356: `pipeline_fit` now returns a fitted `Pipeline` instead of mutating.
- Issue #435: `logistic_predict_proba` returned P(`classes[0]`) while `logistic_decide` labelled a probability above the threshold `classes[1]`, so the two composed to an inverted binary label. The probability is now P(`classes[1]`).
Expand Down
Loading