Benchmarks¶
This page compares bulanik with other open-source libraries and with public data. It is there
to show how the library behaves, including where it differs and where it does poorly. The tables are
read from the result files in tests/benchmarks/data/ when the docs are built. The scripts that
produce them are in benchmarks/; its README
gives the commands. Offline tests in tests/benchmarks/ keep the frozen values honest.
In short
- On matching formulas, every crisp MCDM method gives the same scores as pymcdm and pyrepo-mcda, to rounding, on 209 matrices. Most real differences are choices of variant; pymcdm's COPRAS uses another formula.
- bulanik is stricter than the libraries about rounded weights, and it has no min-max option for TOPSIS.
- Mamdani output equals simpful's and differs from an exact integral by a discretisation error.
- ANFIS gets nearly all of its fit from the least-squares step. At default settings the premise step changes almost nothing on any dataset here, so gradient training adds little. This is the weakest result on the page.
1. MCDM against pymcdm and pyrepo-mcda¶
Method. bulanik and each library run on the same inputs: nine decision matrices taken from the
studies named in the libraries' test suites, 200 random matrices from a seeded generator (4 to 12
alternatives, 3 to 8 criteria, mixed benefit and cost), and for AHP 100 random reciprocal matrices on
Saaty's scale plus one from a study. A few of the study weight vectors were adjusted in the last entry so
that they sum to 1 and every library accepts them. Ranks are compared with a tie tolerance of \(10^{-9}\).
The /vector and /sum rows set a variant so that the two formulas match.
Data and licence. Matrices and expected values come from the pymcdm 1.4.0 and pyrepo-mcda 0.1.15
source distributions (both MIT; see benchmarks/THIRD_PARTY_NOTICES.md).
Produced with bulanik 0.3.0, pymcdm 1.4.0, pyrepo-mcda 0.1.15, numpy 2.5.4, scipy 1.18.1, Python 3.12.8; seed 20261105.
| Method | Library | Matrices | Max abs. score diff. | Min. Spearman | Identical rankings | Classification |
|---|---|---|---|---|---|---|
| ahp | pymcdm | 101 | 6.63e-13 | 1.0000 | 101 of 101 | agree |
| ahp | pyrepo-mcda | 101 | 6.62e-13 | 1.0000 | 101 of 101 | agree |
| cocoso | pymcdm | 208 | 7.11e-15 | 1.0000 | 208 of 208 | agree |
| cocoso | pyrepo-mcda | 208 | 7.11e-15 | 1.0000 | 208 of 208 | agree |
| codas | pymcdm | 209 | 1.78e-15 | 1.0000 | 209 of 209 | agree |
| codas | pyrepo-mcda | 209 | 1.78e-15 | 1.0000 | 209 of 209 | agree |
| copras | pymcdm | 209 | 8.66e-01 | -1.0000 | 4 of 209 | formula form |
| copras | pyrepo-mcda | 209 | 4.44e-16 | 1.0000 | 209 of 209 | agree |
| copras/sum | pymcdm | 209 | 3.33e-16 | 1.0000 | 209 of 209 | agree |
| critic | pymcdm | 209 | 1.67e-16 | 1.0000 | 209 of 209 | agree |
| critic | pyrepo-mcda | 209 | 1.67e-16 | 1.0000 | 209 of 209 | agree |
| critic/cost-types | pymcdm | 209 | 2.50e-01 | -1.0000 | 8 of 209 | parameter default |
| critic/cost-types | pyrepo-mcda | 209 | 2.50e-01 | -1.0000 | 8 of 209 | parameter default |
| edas | pymcdm | 209 | 3.33e-16 | 1.0000 | 209 of 209 | agree |
| edas | pyrepo-mcda | 209 | 3.33e-16 | 1.0000 | 209 of 209 | agree |
| entropy | pymcdm | 209 | 6.99e-15 | 1.0000 | 209 of 209 | agree |
| entropy | pyrepo-mcda | 209 | 2.34e-14 | 1.0000 | 209 of 209 | agree |
| marcos | pymcdm | 209 | 3.33e-16 | 1.0000 | 209 of 209 | agree |
| marcos | pyrepo-mcda | 209 | 3.33e-16 | 1.0000 | 209 of 209 | agree |
| topsis | pymcdm | 209 | 2.26e-01 | -0.2000 | 47 of 209 | normalisation variant |
| topsis | pyrepo-mcda | 209 | 2.26e-01 | -0.2000 | 47 of 209 | normalisation variant |
| topsis/vector | pymcdm | 209 | 2.22e-16 | 1.0000 | 209 of 209 | agree |
| topsis/vector | pyrepo-mcda | 209 | 2.22e-16 | 1.0000 | 209 of 209 | agree |
| vikor | pymcdm | 209 | 3.33e-16 | 1.0000 | 209 of 209 | agree |
| vikor | pyrepo-mcda | 209 | 3.33e-16 | 1.0000 | 209 of 209 | agree |
| waspas | pymcdm | 209 | 1.11e-16 | 1.0000 | 209 of 209 | agree |
| waspas | pyrepo-mcda | 209 | 1.11e-16 | 1.0000 | 209 of 209 | agree |
Known differences
- topsis, pymcdm (normalisation variant). pymcdm's default is min-max; bulanik always uses vector normalisation and has no option for min-max. The topsis/vector row sets pymcdm to vector normalisation and agrees.
- copras, pymcdm (formula form). pymcdm 1.4.0 computes Q = S+ + (min(S-) * S-) / (S- * (min(S-) / S-)), which simplifies to S+ + S-: a larger cost raises the score. bulanik and pyrepo-mcda use Q = S+ + sum(S-) / (S- * sum(1/S-)). The copras/sum row rebuilds pymcdm's form from bulanik's extras (S_plus + S_minus, divided by its maximum) and agrees.
- topsis, pyrepo-mcda (normalisation variant). pyrepo-mcda's default is min-max; bulanik always uses vector normalisation and has no option for min-max. The topsis/vector row sets pyrepo-mcda to vector normalisation and agrees.
- critic/cost-types, pymcdm (parameter default). bulanik can flip cost columns in the min-max step (criteria_types=...), which changes the sign of their correlations; both libraries ignore criterion types here. Left at its default (all benefit) bulanik agrees, see the critic rows.
- cocoso, pymcdm. bulanik refused one random matrix, which is why the row has 208 matrices: random-150: CoCoSo: alternative 'A2' is worst on every criterion, so k_b divides by zero
- AHP consistency ratio, pymcdm (parameter default). Largest gap 0.0209; 1.0e-12 with the same random index. pymcdm uses another random-index table (0.5799, 0.8921, ... ); bulanik and pyrepo-mcda use Saaty's 0.58, 0.90, ... With the same table the ratios agree.
- Ranks of tied alternatives (tie handling). For scores [0.437495, 0.437495, 0.953267, 0.440563]: bulanik's positions [3, 4, 1, 2], pymcdm [3.5, 3.5, 1.0, 2.0], pyrepo-mcda [3, 3, 1, 2]. bulanik returns a strict order in which ties keep input order; pymcdm gives tied alternatives their average rank; pyrepo-mcda gives them the same integer rank and does not skip the next one.
The formulas of the pymcdm COPRAS row come from reading its source; the study its test cites was not read here, so this page does not say which form the study prints or which is right.
Expected values from the libraries' test suites. bulanik against the numbers printed in those
tests, at the rounding each test uses. "Normalised" means bulanik rejected the rounded weights until
normalize_weights=True was set. The studies are the ones the test suites name; they were not read here.
| Fixture | Study named by the test | Decimals | Max abs. diff. | Within rounding | Note |
|---|---|---|---|---|---|
pymcdm-cocoso |
Yazdani, Zarate, Zavadskas & Turskis (2019), Management Decision 57(9), 2501-2519 | 3 | 0.00035 | yes | |
pymcdm-codas |
Badi, Shetwan & Abdulshahed (2017), Proc. 1st ICMNEE, 395-407 | 4 | 0.00005 | yes | |
pymcdm-copras-1 |
Zavadskas, Kaklauskas, Peldschus & Turskis (2007), Baltic J. Road Bridge Eng. 2(4), 195-203 | 4 | 0.07218 | no | pymcdm's test expects its S+ + S- form; see the copras row. |
pymcdm-copras-2 |
Zavadskas, Kaklauskas, Peldschus & Turskis (2007), Baltic J. Road Bridge Eng. 2(4), 195-203 | 4 | 0.12504 | no | pymcdm's test expects its S+ + S- form; see the copras row. |
pymcdm-edas |
Yazdani, Torkayesh, Santibanez-Gonzalez & Otaghsara (2020), Sustainable Operations and Computers 1, 35-42 | 3 | 0.00038 | yes | Normalised. |
pymcdm-marcos |
Ulutas, Karabasevic, Popovic, Stanujkic, Nguyen & Karakoy (2020), Mathematics 8(10), 1672 | 4 | 0.00005 | yes | Normalised. |
pymcdm-topsis |
Opricovic & Tzeng (2004), EJOR 156(2), 445-455 | 3 | 0.26214 | no | pymcdm's test uses its min-max default; bulanik uses vector normalisation. |
pymcdm-vikor-yang |
Yang & Wu (2020), IEEE Access 8, 21261-21271 | 4 | 0.00003 | yes | |
pymcdm-vikor-opricovic |
Opricovic & Tzeng (2004), EJOR 156(2), 445-455 | 9 | 0.00000 | yes | |
pymcdm-waspas |
Chakraborty, Zavadskas & Antucheviciene (2015), ECECSR 49(1), 5-22 | 4 | 0.00591 | no | The fixture's weights sum to 1.0155 and pymcdm's test turns validation off. bulanik rejects them; with normalize_weights=True the scores shrink by up to 0.0059 and the ranking stays the same. |
pymcdm-entropy |
Elsayed, Dawood & Karthikeyan (2017), Int. J. Eng. Trends Technol. 46(2), 60-66 | 6 | 0.00000 | yes | |
pymcdm-critic |
Tus & Aytac Adali (2019), Opsearch 56(2), 528-538 | 3 | 0.00049 | yes | |
pymcdm-ahp |
Wang & Chin (2011), Information Sciences 181(23), 5240-5248 | 4 | 0.00003 | yes | |
pyrepo-vikor |
Papathanasiou & Ploskas (2018), Multiple Criteria Decision Aid, 31-55 | 3 | 0.00041 | yes | |
pyrepo-topsis |
Ersoy (2021), LogForum 17(3) | 2 | 0.00075 | yes | |
pyrepo-codas-1 |
Badi, Abdulshahed & Shetwan (2018), Decision Making: Appl. Manag. Eng. 1(1), 1-12 | 4 | 0.00005 | yes | |
pyrepo-codas-2 |
Badi, Ballem & Shetwan (2018), Int. J. Quality Research 12(3), 609 | 4 | 0.00005 | yes | |
pyrepo-edas |
Ersoy (2021), LogForum 17(3) | 3 | 0.00034 | yes | |
pyrepo-marcos |
Stevic, Pamucar, Puska & Chatterjee (2020), Computers & Industrial Engineering 140, 106231 | 3 | 0.00050 | yes | Normalised. |
pyrepo-copras |
Goswami & Mitra (2020), Int. J. Data and Network Science 4(1), 27-42 | 5 | 0.00000 | yes | |
pyrepo-cocoso |
Yazdani, Zarate, Zavadskas & Turskis (2019), Management Decision 57(9), 2501-2519 | 1 | 0.00131 | yes | |
pyrepo-entropy-xu |
Xu (2004), EJOR 156(2), 530-532 | 4 | 0.00005 | yes | |
pyrepo-entropy-podvezko |
Zavadskas & Podvezko (2016), IJITDM 15(2), 267-283 | 4 | 0.00004 | yes | |
pyrepo-entropy-ersoy |
Ersoy (2021), LogForum 17(3) | 3 | 0.00043 | yes | |
pyrepo-entropy-lee |
Lee & Chang (2018), Renewable and Sustainable Energy Reviews 92, 883-896 | 3 | 0.00046 | yes | |
pyrepo-critic |
Tus & Aytac Adali (2019), Opsearch 56(2), 528-538 | 3 | 0.00049 | yes |
Edge cases. The libraries and bulanik differ most at the edges. These rows show only the cases where the three do not give the same outcome. Numbers are scores; a COPRAS row that differs only as above is left out.
| Case | Method | bulanik | pymcdm | pyrepo-mcda |
|---|---|---|---|---|
| constant column | vikor | 0.511, 0.000, 1.000, 0.785 | raises ValueError | 0.511, 0.000, 1.000, 0.785 |
| constant column | cocoso | 1.691, 3.630, 1.534, 1.957 | 1.705, 2.662, 1.648, 1.865 | 1.705, 2.662, 1.648, 1.865 |
| constant column | critic | 0.492, 0.000, 0.508 | returns non-finite | returns non-finite |
| zero entry in a benefit column | codas | -2.229, 2.710, -0.549, 0.068 | raises ValueError | -2.229, 2.710, -0.549, 0.068 |
| zero entry in a benefit column | waspas | raises ValueError | raises ValueError | 0.112, 0.953, 0.441, 0.558 |
| zero entry in a benefit column | marcos | raises ValueError | 0.218, 0.926, 0.460, 0.547 | 0.218, 0.926, 0.460, 0.547 |
| zero entry in a benefit column | entropy | 0.729, 0.053, 0.217 | raises ValueError | 0.729, 0.053, 0.217 |
| alternative worst on every criterion | cocoso | raises ValueError | returns non-finite | returns non-finite |
| benefit criteria only | copras | 0.802, 1.000, 0.739, 0.647 | raises ValueError | returns non-finite |
| all alternatives identical | topsis | 0.500, 0.500, 0.500 | returns non-finite | returns non-finite |
| all alternatives identical | vikor | 0.000, 0.000, 0.000 | raises ValueError | -0.600, -0.600, -0.600 |
| all alternatives identical | edas | 0.500, 0.500, 0.500 | returns non-finite | returns non-finite |
| all alternatives identical | cocoso | raises ValueError | 1.985, 1.985, 1.985 | 1.985, 1.985, 1.985 |
| all alternatives identical | entropy | raises ValueError | 0.333, 0.333, 0.333 | 0.333, 0.333, 0.333 |
| all alternatives identical | critic | raises ValueError | returns non-finite | returns non-finite |
Where bulanik is stricter: it refuses a zero in MARCOS, which both libraries accept, and in WASPAS, which pyrepo-mcda accepts and pymcdm refuses. For all-identical alternatives it refuses CoCoSo and entropy, which the libraries score. Where an alternative is worst on every CoCoSo criterion it raises an error, and the libraries return non-finite scores. On a constant column CoCoSo sets the normalised value to 0 where both libraries set it to 1, which changes the scores (not the order, in this case).
2. Human Development Index¶
Method. The HDI of a country is the geometric mean of three dimension indices: health, education and income. The published file holds the raw indicators and not the indices, so they are computed from life expectancy, expected and mean years of schooling, and GNI per capita, as \((LE - 20) / 65\), \((\min(EYS, 18) / 18 + \min(MYS, 15) / 15) / 2\) and \((\ln \min(GNI, 75000) - \ln 100) / (\ln 75000 - \ln 100)\). These goalposts were not read from UNDP's technical notes for this benchmark: they are checked by reproducing the published HDI (below).
Countries are then ranked by waspas with equal weights and lam=0, the
product form, and compared with UNDP's rank. WASPAS first divides each column by its maximum, so
its product form is the HDI divided by a constant, and it should match nearly exactly by construction.
This checks the implementation against a published ranking. It does not show that WASPAS learns anything
about countries. The sum form (lam=1) and the default blend show how far other forms drift.
Data and licence. HDR 2025 composite indices,
United Nations Development Programme, used under
CC BY 3.0 IGO as stated in its
terms of use. A 2023 extract with computed index columns is
committed at tests/benchmarks/data/hdi_2023_extract.csv; benchmarks/THIRD_PARTY_NOTICES.md lists the changes.
UNDP does not endorse this work.
193 countries for 2023; left out for missing data: Korea (Democratic People's Rep. of), Monaco. Recomputed HDI equals the published value at 3 decimals for 193 of 193 countries.
| Form | lam | Spearman with UNDP rank | Same rank, unrounded | Largest rank gap, unrounded | Same rank after rounding to 3 decimals |
|---|---|---|---|---|---|
| product | 0.0 | 0.999982 | 159 of 193 | 2.0 | 193 of 193 |
| blend_0.5 | 0.5 | 0.999902 | 118 of 193 | 4.0 | 125 of 193 |
| sum | 1.0 | 0.999765 | 86 of 193 | 5.0 | 88 of 193 |
Known differences. UNDP ranks on the HDI rounded to 3 decimals, so 34 countries share a rank with another (1, 2, 2, 4, ...). Unrounded scores split those ties, which accounts for the whole gap of the product form: once its values are rounded to 3 decimals, its ranks are UNDP's.
3. ANFIS on Mackey-Glass¶
Method. The series is generated here by RK4 from
\(\dot x = 0.2\,x(t-17) / (1 + x(t-17)^{10}) - 0.1\,x(t)\) with step 0.1, \(x(0) = 1.2\) and \(x(t) = 0\) for
\(t < 0\). Inputs are \(x(t-18), x(t-12), x(t-6), x(t)\), the target is \(x(t+6)\), with 500 training points from
\(t = 118\) and the next 500 for testing. Two terms per input give 16 rules. Training is
fit, the hybrid method; it is pure Python and needs no extra (the sezgi extra is only for the
optional derivative-free train_anfis). Training is deterministic, so there is no seed. Every step
size tried is listed; none was selected. No figure from the literature is quoted, because
none was read for this benchmark and the series could differ.
Data and licence. Generated by tests/benchmarks/_bench_support.py; no external data.
| Model | lr | Epochs | Train RMSE | Test RMSE | Fit seconds |
|---|---|---|---|---|---|
| Last value carried forward | 0.18550 | 0.18476 | |||
| Linear least squares | 0.09516 | 0.09830 | |||
| ANFIS gaussian, 16 rules | 0.01 (default) | 1 | 0.00217 | 0.00300 | 0.17 |
| ANFIS gaussian, 16 rules | 0.01 (default) | 500 | 0.00217 | 0.00300 | 50.33 |
| ANFIS bell, 16 rules | 0.01 (default) | 1 | 0.00281 | 0.00359 | 0.18 |
| ANFIS bell, 16 rules | 0.01 (default) | 500 | 0.00281 | 0.00359 | 52.48 |
| ANFIS gaussian, 16 rules | 0.1 | 1 | 0.00217 | 0.00300 | 0.17 |
| ANFIS gaussian, 16 rules | 0.1 | 500 | 0.00217 | 0.00300 | 50.37 |
| ANFIS bell, 16 rules | 0.1 | 1 | 0.00281 | 0.00359 | 0.17 |
| ANFIS bell, 16 rules | 0.1 | 500 | 0.00279 | 0.00359 | 51.07 |
| ANFIS gaussian, 16 rules | 1 | 1 | 0.00217 | 0.00300 | 0.19 |
| ANFIS gaussian, 16 rules | 1 | 500 | 0.00217 | 0.00300 | 52.11 |
| ANFIS bell, 16 rules | 1 | 1 | 0.00281 | 0.00359 | 0.18 |
| ANFIS bell, 16 rules | 1 | 500 | 0.00267 | 0.00352 | 51.8 |
| ANFIS gaussian, 16 rules | 10 | 1 | 0.00217 | 0.00300 | 0.2 |
| ANFIS gaussian, 16 rules | 10 | 500 | 0.00217 | 0.00300 | 52.89 |
| ANFIS bell, 16 rules | 10 | 1 | 0.00281 | 0.00359 | 0.2 |
| ANFIS bell, 16 rules | 10 | 500 | 0.00270 | 0.00354 | 56.55 |
| ANFIS gaussian, 16 rules | 100 | 1 | 0.00217 | 0.00300 | 0.24 |
| ANFIS gaussian, 16 rules | 100 | 500 | 0.00217 | 0.00300 | 54.22 |
| ANFIS bell, 16 rules | 100 | 1 | 0.00281 | 0.00359 | 0.23 |
| ANFIS bell, 16 rules | 100 | 500 | 0.00268 | 0.00353 | 56.36 |
What this shows. At the default lr=0.01, the fit after one epoch and after 500 are the same to
five decimals: the least-squares consequents do the work and the premise barely moves. The premise gradient
is correct (an offline test checks it against finite differences on a 100-point subset, as the gradient of the
mean squared error), but its entries there are around \(10^{-7}\) once the error is this small, so a small step
does nothing. Larger values of lr help the bell terms a little and the Gaussian terms not at all, and the
Gaussian rows have the best test RMSE at every lr. The test set
goes slightly outside the training range, so the extrapolation warning fires on every run.
4. Mamdani and Sugeno against simpful and scikit-fuzzy¶
Method. Two rule bases written for this benchmark (tests/benchmarks/_inference_cases.py), with
triangular, trapezoidal and Gaussian terms, are evaluated over a grid of input pairs. All libraries use
AND as stated, OR max, NOT \(1-\mu\), clip implication and max aggregation.
Scope of scikit-fuzzy. skfuzzy.control imports networkx, which scikit-fuzzy 0.5.0 does not declare and
this suite does not install, so its control engine was not run. The scikit-fuzzy column uses the library's
membership functions and defuzz, with firing strengths and aggregation written in the benchmark script. No
file in the package mentions Sugeno, so it has no Sugeno row.
Data and licence. No dataset. simpful and scikit-fuzzy outputs are frozen for the offline test.
Produced with bulanik 0.3.0, simpful 2.12.0, scikit-fuzzy 0.5.0.
| Mamdani rule base | AND | Resolution | Grid points | Max diff. vs simpful | Max diff. vs scikit-fuzzy primitives | Seconds: bulanik, simpful, scikit-fuzzy |
|---|---|---|---|---|---|---|
| climate | min | 101 | 441 | 4.26e-14 | 4.85e-01 | 0.036, 0.245, 0.09 |
| climate | min | 1001 | 441 | 1.71e-13 | 4.87e-02 | 0.301, 2.015, 0.226 |
| climate | min | 10001 | 25 | 1.56e-13 | 4.38e-03 | 0.144, 1.102, 0.089 |
| climate | prod | 1001 | 441 | 2.77e-13 | 4.84e-02 | 0.303, 1.999, 0.226 |
| mixed | min | 1001 | 441 | 2.42e-13 | 8.06e-02 | 0.272, 1.166, 0.2 |
| Defuzzification (climate, resolution 1001) | Max diff. vs scikit-fuzzy | Mean diff. |
|---|---|---|
| bisector | 3.20e-01 | 3.43e-02 |
| mom | 1.42e-14 | 2.71e-15 |
| som | 0.00e+00 | 0.00e+00 |
| lom | 0.00e+00 | 0.00e+00 |
| Sugeno rule base | AND | Max diff. vs simpful | Seconds: bulanik, simpful |
|---|---|---|---|
| climate | min | 2.84e-14 | 0.0062, 0.0617 |
| climate | prod | 2.84e-14 | 0.006, 0.0598 |
| mixed | min | 2.67e+01 | 0.0043, 0.0387 |
| mixed, weights removed | min | 2.84e-14 | 0.0042, 0.0388 |
Known differences
- Mamdani against simpful. Same scores within rounding: same operators, same sampled centroid.
- Centroid against scikit-fuzzy (formula form (quadrature)). bulanik and simpful take sum(x mu) / sum(mu) over the samples, a rectangle rule that counts the universe endpoints in full; skfuzzy integrates the piecewise-linear set exactly. The gap shrinks about tenfold for each tenfold rise in resolution. Applying the rectangle rule to the aggregate set built with scikit-fuzzy's functions gives bulanik's value to 2.8e-14.
- Bisector against scikit-fuzzy. formula form (quadrature): bulanik returns the first sample at which the running sum reaches half; skfuzzy solves for the exact area split. On a low flat stretch the endpoint weight moves the split by more than one grid step.
- Weighted Sugeno against simpful (formula form). With rule weights, bulanik computes sum(w a f) / sum(w a), simpful sum(w a f) / sum(a): simpful weights the numerator only. Without weights they agree (the row 'mixed, weights removed'). bulanik has no option for simpful's form; rebuilding it from bulanik's rule activations reproduces simpful. Largest gap of the rebuilt form: 1.4e-14.
- When no rule fires. bulanik: raises NoRuleFiredError: no rule fired for output 'fan' and no default was given; the error's .. simpful: returns 0.0 (prints a warning). scikit-fuzzy's defuzz: raises EmptyMembershipError: The membership function area is empty..
For bulanik's centroid the error against the exact integral is largest when a term has non-zero membership
at the end of the output universe. At a resolution of 1001 it reached about 0.08 on a universe of width 100
here. Raise resolution if that matters.
5. ANFIS on public regression data¶
Method. Five fixed folds from a seeded shuffle, the same for all models, on raw inputs. ANFIS uses
defaults: Gaussian terms, lr=0.01, 20 epochs; ranges come from the training fold. The inputs were
chosen before running: for Airfoil, frequency, suction-side displacement thickness and attack angle,
plus a run with all five; for Auto MPG, weight, horsepower and model year. scikit-learn's models use all
inputs and default settings. Nothing was tuned. The script runs only in benchmarks/.
The plain ANFIS rows are the result. The rows marked "inputs scaled" (inputs mapped to \([0, 1]\) by the
training fold's range) and "lr 1e-12" (premise frozen, least squares only) are diagnostics. They were
added after seeing the first results, which exposed a bug in fit, since fixed; the figures below are
from the fixed code.
Data and licence. Both CC BY 4.0, downloaded at run time and not committed: Brooks, Pope & Marcolini (1989), Airfoil Self-Noise, and Quinlan (1993), Auto MPG, UCI Machine Learning Repository. Auto MPG rows with unknown horsepower are dropped.
airfoil (1503 rows)
| Model | Mean RMSE | Std. over folds | Worst fold | Mean fit seconds | Folds with extrapolation warning |
|---|---|---|---|---|---|
| ANFIS 3 inputs, 2 terms (8 rules) | 3.680 | 0.188 | 3.811 | 1.833 | 0 |
| ANFIS 3 inputs, 2 terms (8 rules), inputs scaled | 3.680 | 0.188 | 3.811 | 1.75 | 0 |
| ANFIS 3 inputs, 2 terms (8 rules), lr 1e-12 | 3.680 | 0.188 | 3.811 | 1.483 | 0 |
| ANFIS 3 inputs, 3 terms (27 rules) | 4.277 | 2.502 | 8.737 | 8.592 | 0 |
| ANFIS 3 inputs, 3 terms (27 rules), inputs scaled | 4.277 | 2.501 | 8.735 | 8.365 | 0 |
| ANFIS 3 inputs, 3 terms (27 rules), lr 1e-12 | 4.277 | 2.502 | 8.737 | 7.615 | 0 |
| ANFIS 5 inputs, 2 terms (32 rules) | 3.293 | 0.152 | 3.423 | 19.704 | 0 |
| ANFIS 5 inputs, 2 terms (32 rules), inputs scaled | 3.293 | 0.152 | 3.423 | 19.388 | 0 |
| ANFIS 5 inputs, 2 terms (32 rules), lr 1e-12 | 3.293 | 0.152 | 3.423 | 18.58 | 0 |
| scikit-learn LinearRegression (all inputs) | 4.824 | 0.171 | 5.027 | 0.001 | 0 |
| scikit-learn GradientBoostingRegressor (all inputs) | 2.636 | 0.112 | 2.767 | 0.043 | 0 |
auto_mpg (392 rows)
| Model | Mean RMSE | Std. over folds | Worst fold | Mean fit seconds | Folds with extrapolation warning |
|---|---|---|---|---|---|
| ANFIS 3 inputs, 2 terms (8 rules) | 3.332 | 1.437 | 5.897 | 0.403 | 3 |
| ANFIS 3 inputs, 2 terms (8 rules), inputs scaled | 3.332 | 1.438 | 5.899 | 0.456 | 3 |
| ANFIS 3 inputs, 2 terms (8 rules), lr 1e-12 | 3.332 | 1.438 | 5.899 | 0.435 | 3 |
| ANFIS 3 inputs, 3 terms (27 rules) | 5.183 | 2.703 | 9.706 | 2.23 | 3 |
| ANFIS 3 inputs, 3 terms (27 rules), inputs scaled | 5.137 | 2.599 | 9.457 | 2.392 | 3 |
| ANFIS 3 inputs, 3 terms (27 rules), lr 1e-12 | 5.187 | 2.705 | 9.710 | 2.331 | 3 |
| scikit-learn LinearRegression (all inputs) | 3.372 | 0.142 | 3.586 | 0.0 | 0 |
| scikit-learn GradientBoostingRegressor (all inputs) | 2.853 | 0.377 | 3.389 | 0.026 | 0 |
The premise step adds little. The plain rows are within 0.005 of the "lr 1e-12" rows, so
the fit comes from least squares. A premise step is kept only if it does not raise the training error, so on
data like this, with a large target and a small input range, lr is shrunk rather than harming the model. A
small case shows it:
import math
from bulanik.anfis import ANFIS
X = [[0.05 * i / 199] for i in range(200)]
y = [125 + 5 * math.sin(120 * x[0]) for x in X]
history = ANFIS(1, 2).fit(X, y, epochs=10)
print("RMSE per epoch, defaults:", [round(v, 2) for v in history])
print("least squares only (lr=1e-12):", round(ANFIS(1, 2).fit(X, y, epochs=1, lr=1e-12)[0], 3))
RMSE per epoch, defaults: [0.14, 0.14, 0.14, 0.14, 0.14, 0.14, 0.13, 0.13, 0.13, 0.13]
least squares only (lr=1e-12): 0.136
The least-squares step itself looks sound: in a one-off check on three
Airfoil folds, it gave the same train and test RMSE as numpy.linalg.solve on the same design matrix, to 9 decimals.
Against scikit-learn. Gradient boosting beats every ANFIS row on both datasets. The best ANFIS rows do beat linear regression on Airfoil; on Auto MPG they are about level with it, and their worst fold is well above its worst.
Known differences. This is not a like-for-like contest: gradient boosting sees every input, and the ANFIS grid cannot afford to, since rules multiply with each input. Runtime is a result too: the five-input grid takes seconds per fold where scikit-learn's models take hundredths of a second or less.