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.
linear_regression_fit(lib/scikit/linear.flow, line 323):
export function linear_regression_fit(X: Matrix, y: ptr<f32>, penalty: Penalty) -> LinearRegression
logistic_regression_fit(lib/scikit/linear.flow, line 443):
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:
random_forest_classifier_decidelogistic_decidesgd_classifier_decidemulticlass_logistic_decideknn_classifier_decidemlp_classifier_decidepipeline_decidekernel_svc_decidelinear_svc_decide
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.
- 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
glmuses, which matters when porting a model between languages.
Closed since this page was written
- Issue #355: a bootstrap cross-validator now exists.
BootstrapOOBinlib/scikit/model_selection.flowimplements plain out-of-bag bootstrap and names the variant, since scikit-learn removed its ownBootstrapclass for inventing non-standard semantics under a misleading name. - Issue #356:
pipeline_fitnow returns a fittedPipelineinstead of mutating.