api design

Design rationale

Where flow-scikit's API deliberately differs from scikit-learn's, why, and what is still missing.

This document records the API positions flow-scikit takes. Most of the behavior documented here falls out of the Flow language having no default arguments. It is an accident of the toolchain. The language makes hidden defaults impossible, and the project keeps it that way on purpose.

1. No hidden regularization

The scikit-learn LogisticRegression class applies L2 regularization with C=1.0 by default. The class named after logistic regression does not perform plain logistic regression. The blog post "Scikit-learn's Defaults are Wrong" by ryxcommar (2019-08-30) documents this behavior. Independent threads on Hacker News and r/datascience raise the same issue.

In flow-scikit, estimators require an explicit penalty argument. The caller writes penalty_none() or penalty_l2(alpha) explicitly.

export function linear_regression_fit(X: Matrix, y: ptr<f32>, penalty: Penalty) -> LinearRegression

export function logistic_regression_fit(X: Matrix, y: ptr<f32>, n_classes: i32, epochs: i32, lr: f32, penalty: Penalty) -> LogisticRegression

2. Regularization strength

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.

The flow-scikit library uses penalty_l2(alpha). This is a partial answer. As ryxcommar notes, alpha itself is one step removed from lambda.

alpha and C are not reciprocals of each other. scikit-learn's LogisticRegression minimises the sum of the per-sample losses plus 0.5 * ||w||^2 / C. logistic_regression_fit minimises the mean loss plus 0.5 * alpha * ||w||^2. Putting the two in the same units leaves alpha = 1 / (C * n_samples), so the sample count is part of the conversion. penalty_l2_from_c(C, n_samples) in lib/scikit/linear.flow performs it.

Issue #408 recorded what happens when the conversion is skipped. The canonical benchmark passed a flat penalty_l2(0.001) on both iris and digits against scikit-learn's C=1.0. 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.

The intercept is not penalized on either side. logistic_regression_fit augments theta with the bias at index n and applies the L2 term over 0 to n, which leaves the bias out.

3. Fit returns a value

Estimator fitting returns a value. Hacker News commenter zeec123 argues fit should return a function from input space to output space and avoid modifying internal state.

Flow structs pass by value. Every estimator fit function returns a fitted struct.

Pipeline was the one exception until recently: pipeline_fit 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 Pipeline, so the rule holds without exception.

4. No hidden threshold

The predict functions return probabilities. The decide functions take an explicit threshold.

The codebase contains nine *_decide functions:

5. Structured validation errors

The ValidationResult struct in lib/scikit/validation.flow carries a numeric code and a string message. The ensure_* validation functions share a consistent naming convention. G2 reviewers raised unhelpful error messages in both 2019 and 2024. The structured approach provides clear diagnostics.

Known gaps

Recorded here so this page is not one-sided.

Closed since this page was written