1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! VALUE-parity divergence suite for `IterativeImputer` against scikit-learn
//! 1.5.2 `class IterativeImputer` (`sklearn/impute/_iterative.py`).
//!
//! EXPERIMENTAL upstream — imported in oracles via
//! `from sklearn.experimental import enable_iterative_imputer`.
//!
//! These tests pin the ITERATED IMPUTED-VALUE divergences that the existing
//! `divergence_iterative_imputer.rs` suite explicitly carved out (its REQ-4
//! note: "ferrolearn Ridge(alpha=1) != sklearn default BayesianRidge ... NOT
//! asserted here"). Per goal.md R-DEFER-2/3/5 there is no third "carve-out"
//! status: every value divergence on `main` is real work we owe and must be
//! pinned with a FAILING test against the LIVE sklearn oracle.
//!
//! ROOT CAUSE: `iterative_imputer.rs` hard-codes its own closed-form
//! `ridge_fit` with `alpha = F::one()` (`fn fit`, `let alpha = F::one()`),
//! whereas sklearn's default round-robin estimator is `BayesianRidge`
//! (`_iterative.py:732-735`: `if self.estimator is None: ... self._estimator =
//! BayesianRidge()`). ferrolearn-preprocess ALREADY depends on ferrolearn-linear
//! (which ships `BayesianRidge` in `bayesian_ridge.rs`), so the divergence is
//! fixable by routing the round-robin step through the real BayesianRidge.
//! Secondary contributors: default imputation_order (sklearn 'ascending' vs
//! ferrolearn column/'roman'), inf-norm tol vs ferrolearn L2-relative tol, and
//! no min/max clip.
//!
//! All expected values are the LIVE sklearn 1.5.2 oracle (run from /tmp with the
//! experimental gate), captured in each test's doc comment — never copied from
//! ferrolearn (R-CHAR-3).
use FitTransform;
use ;
use ;
const TOL: f64 = 1e-6;
// ===========================================================================
// DIV-VAL-1 — core round-robin imputed VALUES (small fixture)
// ===========================================================================
/// Divergence DIV-VAL-1: `IterativeImputer::fit_transform` diverges from
/// `sklearn/impute/_iterative.py:732-735` (default estimator `BayesianRidge()`)
/// + `:454` (`imputed_values = estimator.predict(X_test)`).
///
/// Oracle (sklearn 1.5.2, EXPERIMENTAL gate, run from /tmp):
/// X = [[1,2],[3,nan],[nan,6]]
/// IterativeImputer(random_state=0).fit_transform(X) =
/// [[1.0, 2.0],
/// [3.0, 4.000002999996018],
/// [4.999994000015464, 6.0]]
/// n_iter_ = 2
///
/// sklearn imputes [1,1] -> 4.000002999996018 and [2,0] -> 4.999994000015464
/// (BayesianRidge effectively recovers the y=2x relation).
/// ferrolearn's Ridge(alpha=1) round-robin imputes [1,1] ~= 3.346 and
/// [2,0] ~= 5.034 — both differ from sklearn by >> 1e-6.
///
/// Tracking #1405 (root: ferrolearn-linear BayesianRidge not wired in).
// ===========================================================================
// DIV-VAL-2 — round-robin imputed VALUES + ascending order (3-feature fixture)
// ===========================================================================
/// Divergence DIV-VAL-2: `IterativeImputer::fit_transform` diverges from
/// `sklearn/impute/_iterative.py:732-735` (default `BayesianRidge()`) and
/// `:533-535`/`:769` (default `imputation_order='ascending'`, fewest-missing
/// first) for a multi-feature fixture.
///
/// Oracle (sklearn 1.5.2, EXPERIMENTAL gate, run from /tmp):
/// X = [[1,2,3],[2,nan,5],[3,6,nan],[4,8,9],[nan,10,11],[6,12,13]]
/// IterativeImputer(random_state=0, max_iter=10, tol=1e-3).fit_transform(X) =
/// [[1.0, 2.0, 3.0],
/// [2.0, 4.000010357521532, 5.0],
/// [3.0, 6.0, 6.999997942723164],
/// [4.0, 8.0, 9.0],
/// [4.999999113051894, 10.0, 11.0],
/// [6.0, 12.0, 13.0]]
/// n_iter_ = 3
///
/// ferrolearn (Ridge(alpha=1), roman order, L2-relative tol) imputes:
/// [1,1] ~= 4.047, [2,2] ~= 7.005, [4,0] ~= 4.984 — all differ >> 1e-6.
///
/// Tracking #1405 (estimator) and #1407 (default order ascending vs roman).
// ===========================================================================
// DIV-VAL-3 — min_value / max_value clip not expressible / not applied
// ===========================================================================
/// Divergence DIV-VAL-3: ferrolearn `IterativeImputer` has NO `min_value`/
/// `max_value` parameter and never clips, diverging from
/// `sklearn/impute/_iterative.py:455-457`
/// (`imputed_values = np.clip(imputed_values, self._min_value[feat_idx],
/// self._max_value[feat_idx])`).
///
/// Oracle (sklearn 1.5.2, EXPERIMENTAL gate, run from /tmp):
/// X = [[1,2,3],[2,nan,5],[3,6,nan],[4,8,9],[nan,10,11],[6,12,13]]
/// IterativeImputer(random_state=0, max_iter=10, tol=1e-3,
/// min_value=5.0, max_value=7.0).fit_transform(X) =
/// [[1.0, 2.0, 3.0],
/// [2.0, 5.0, 5.0],
/// [3.0, 6.0, 6.999999523809311],
/// [4.0, 8.0, 9.0],
/// [5.0, 10.0, 11.0],
/// [6.0, 12.0, 13.0]]
/// Every imputed cell is clipped into [5.0, 7.0]: [1,1]->5.0 (clipped up),
/// [4,0]->5.0 (clipped up), [2,2]->~7.0 (clipped down).
///
/// ferrolearn has no clip parameter at all, so its imputed values for the SAME
/// cells fall OUTSIDE [5.0, 7.0] (e.g. [1,1] ~= 4.047 < 5.0). This test asserts
/// the sklearn CLIP bound on those cells; ferrolearn (unclipped) violates it.
///
/// Tracking #1408 (min_value/max_value clip missing).