[{"data":1,"prerenderedAt":648},["ShallowReactive",2],{"content-query-0CM9e8LWV7":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"draft":6,"tags":11,"thumbnail":17,"slug":18,"body":19,"_type":642,"_id":643,"_source":644,"_file":645,"_stem":646,"_extension":647},"/posts/lyubishchev-pypi-benchmarks","posts",false,"","We Put Lyubishchev on PyPI with interesting benchmarks","A 1943 Soviet manuscript. A Python library. And benchmarks showing where regularized-by-default QDA holds up where sklearn's own implementation falls over.","2026-06-07T00:00:00.000Z",[12,13,14,15,16],"python","machine learning","sklearn","taxonomy","open source","/img/lyubishchev_benchmark.png","lyubishchev-pypi-benchmarks",{"type":20,"children":21,"toc":631},"root",[22,47,52,56,63,68,79,92,95,101,326,329,335,340,351,356,359,365,370,407,412,421,441,459,462,468,478,490,493,499,526,535,540,549,552,558,567,581,601,604,610,615,620,623],{"type":23,"tag":24,"props":25,"children":26},"element","p",{},[27,30,37,39,45],{"type":28,"value":29},"text","Last week I published a library called ",{"type":23,"tag":31,"props":32,"children":34},"code",{"className":33},[],[35],{"type":28,"value":36},"lyubishchev",{"type":28,"value":38}," on PyPI. The previous article explained why — Alexander Lyubishchev developed multivariate classification methods in 1943, twenty years before the researchers whose names ended up in ",{"type":23,"tag":31,"props":40,"children":42},{"className":41},[],[43],{"type":28,"value":44},"scipy.spatial.distance",{"type":28,"value":46},". The library was a citation as much as a tool",{"type":23,"tag":24,"props":48,"children":49},{},[50],{"type":28,"value":51},"Then I ran actual benchmarks. The results were more interesting than I expected",{"type":23,"tag":53,"props":54,"children":55},"hr",{},[],{"type":23,"tag":57,"props":58,"children":60},"h2",{"id":59},"the-setup",[61],{"type":28,"value":62},"The Setup",{"type":23,"tag":24,"props":64,"children":65},{},[66],{"type":28,"value":67},"Three standard sklearn datasets. Four classifiers. Five-fold stratified cross-validation",{"type":23,"tag":69,"props":70,"children":74},"pre",{"className":71,"code":73,"language":12,"meta":7},[72],"language-python","from sklearn.datasets import load_iris, load_wine, load_breast_cancer\nfrom sklearn.model_selection import cross_val_score, StratifiedKFold\nfrom sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis\nfrom sklearn.discriminant_analysis import LinearDiscriminantAnalysis\nfrom sklearn.naive_bayes import GaussianNB\nfrom lyubishchev import LyubishchevClassifier\n\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n",[75],{"type":23,"tag":31,"props":76,"children":77},{"__ignoreMap":7},[78],{"type":28,"value":73},{"type":23,"tag":24,"props":80,"children":81},{},[82,84,90],{"type":28,"value":83},"No hyperparameter tuning. Default settings for everything except ",{"type":23,"tag":31,"props":85,"children":87},{"className":86},[],[88],{"type":28,"value":89},"LyubishchevClassifier(standardize=True)",{"type":28,"value":91}," — standardization on because the method requires features on comparable scales, same as any distance-based approach",{"type":23,"tag":53,"props":93,"children":94},{},[],{"type":23,"tag":57,"props":96,"children":98},{"id":97},"the-results",[99],{"type":28,"value":100},"The Results",{"type":23,"tag":102,"props":103,"children":107},"div",{"className":104},[105,106],"overflow-x-auto","my-8",[108,110],{"type":28,"value":109},"\n  ",{"type":23,"tag":111,"props":112,"children":117},"table",{"className":113},[114,115,116],"text-sm","border-collapse","w-full",[118,120,179,180,325],{"type":28,"value":119},"\n    ",{"type":23,"tag":121,"props":122,"children":123},"thead",{},[124,126,178],{"type":28,"value":125},"\n      ",{"type":23,"tag":127,"props":128,"children":132},"tr",{"className":129},[130,131],"border-b-2","border-gray-200",[133,135,147,148,156,157,163,164,170,171,177],{"type":28,"value":134},"\n        ",{"type":23,"tag":136,"props":137,"children":144},"th",{"className":138},[139,140,141,142,143],"text-left","py-3","pr-8","font-semibold","whitespace-nowrap",[145],{"type":28,"value":146},"Dataset",{"type":28,"value":134},{"type":23,"tag":136,"props":149,"children":153},{"className":150},[151,140,152,142,143],"text-right","px-6",[154],{"type":28,"value":155},"Lyubishchev",{"type":28,"value":134},{"type":23,"tag":136,"props":158,"children":160},{"className":159},[151,140,152,142,143],[161],{"type":28,"value":162},"QDA",{"type":28,"value":134},{"type":23,"tag":136,"props":165,"children":167},{"className":166},[151,140,152,142,143],[168],{"type":28,"value":169},"LDA",{"type":28,"value":134},{"type":23,"tag":136,"props":172,"children":174},{"className":173},[151,140,152,142,143],[175],{"type":28,"value":176},"GaussianNB",{"type":28,"value":125},{"type":28,"value":119},{"type":28,"value":119},{"type":23,"tag":181,"props":182,"children":183},"tbody",{},[184,185,230,231,270,271,324],{"type":28,"value":125},{"type":23,"tag":127,"props":186,"children":190},{"className":187},[188,189],"border-b","border-gray-100",[191,192,202,203,209,210,215,216,222,223,229],{"type":28,"value":134},{"type":23,"tag":193,"props":194,"children":199},"td",{"className":195},[196,141,197,198,143],"py-4","text-gray-500","text-xs",[200],{"type":28,"value":201},"Iris · 150 samples · 4 features",{"type":28,"value":134},{"type":23,"tag":193,"props":204,"children":206},{"className":205},[151,196,152,142,143],[207],{"type":28,"value":208},"0.9800 ± 0.0267",{"type":28,"value":134},{"type":23,"tag":193,"props":211,"children":213},{"className":212},[151,196,152,142,143],[214],{"type":28,"value":208},{"type":28,"value":134},{"type":23,"tag":193,"props":217,"children":219},{"className":218},[151,196,152,197,143],[220],{"type":28,"value":221},"0.9733 ± 0.0389",{"type":28,"value":134},{"type":23,"tag":193,"props":224,"children":226},{"className":225},[151,196,152,197,143],[227],{"type":28,"value":228},"0.9467 ± 0.0400",{"type":28,"value":125},{"type":28,"value":125},{"type":23,"tag":127,"props":232,"children":234},{"className":233},[188,189],[235,236,242,243,249,250,255,256,262,263,269],{"type":28,"value":134},{"type":23,"tag":193,"props":237,"children":239},{"className":238},[196,141,197,198,143],[240],{"type":28,"value":241},"Wine · 178 samples · 13 features",{"type":28,"value":134},{"type":23,"tag":193,"props":244,"children":246},{"className":245},[151,196,152,142,143],[247],{"type":28,"value":248},"0.9889 ± 0.0136",{"type":28,"value":134},{"type":23,"tag":193,"props":251,"children":253},{"className":252},[151,196,152,142,143],[254],{"type":28,"value":248},{"type":28,"value":134},{"type":23,"tag":193,"props":257,"children":259},{"className":258},[151,196,152,197,143],[260],{"type":28,"value":261},"0.9830 ± 0.0139",{"type":28,"value":134},{"type":23,"tag":193,"props":264,"children":266},{"className":265},[151,196,152,197,143],[267],{"type":28,"value":268},"0.9719 ± 0.0252",{"type":28,"value":125},{"type":28,"value":125},{"type":23,"tag":127,"props":272,"children":273},{},[274,275,281,282,294,295,301,302,316,317,323],{"type":28,"value":134},{"type":23,"tag":193,"props":276,"children":278},{"className":277},[196,141,197,198,143],[279],{"type":28,"value":280},"Breast Cancer · 569 samples · 30 features",{"type":28,"value":134},{"type":23,"tag":193,"props":283,"children":285},{"className":284},[151,196,152,143],[286,288],{"type":28,"value":287},"0.9543 ± ",{"type":23,"tag":289,"props":290,"children":291},"strong",{},[292],{"type":28,"value":293},"0.0035",{"type":28,"value":134},{"type":23,"tag":193,"props":296,"children":298},{"className":297},[151,196,152,142,143],[299],{"type":28,"value":300},"0.9561 ± 0.0055",{"type":28,"value":134},{"type":23,"tag":193,"props":303,"children":305},{"className":304},[151,196,152,142,143],[306,308],{"type":28,"value":307},"0.9561 ± ",{"type":23,"tag":309,"props":310,"children":313},"span",{"className":311},[312],"text-red-400",[314],{"type":28,"value":315},"0.0200",{"type":28,"value":134},{"type":23,"tag":193,"props":318,"children":320},{"className":319},[151,196,152,197,143],[321],{"type":28,"value":322},"0.9385 ± 0.0235",{"type":28,"value":125},{"type":28,"value":119},{"type":28,"value":109},{"type":23,"tag":53,"props":327,"children":328},{},[],{"type":23,"tag":57,"props":330,"children":332},{"id":331},"run-it-yourself",[333],{"type":28,"value":334},"Run It Yourself",{"type":23,"tag":24,"props":336,"children":337},{},[338],{"type":28,"value":339},"Three commands. You need Python installed, nothing else unusual",{"type":23,"tag":69,"props":341,"children":346},{"className":342,"code":344,"language":345,"meta":7},[343],"language-bash","# Install the libraries\npip install lyubishchev scikit-learn\n\n# Run the benchmark\npython3 \u003C\u003C 'EOF'\nimport warnings\nimport numpy as np\nfrom sklearn.datasets import load_iris, load_wine, load_breast_cancer\nfrom sklearn.model_selection import cross_val_score, StratifiedKFold\nfrom sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis, LinearDiscriminantAnalysis\nfrom sklearn.naive_bayes import GaussianNB\nfrom lyubishchev import LyubishchevClassifier\n\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n\ndatasets = {\n    \"Iris          \": load_iris(return_X_y=True),\n    \"Wine          \": load_wine(return_X_y=True),\n    \"Breast Cancer \": load_breast_cancer(return_X_y=True),\n}\n\nclassifiers = {\n    \"LyubishchevClassifier\": LyubishchevClassifier(standardize=True),\n    \"QDA (sklearn)        \": QuadraticDiscriminantAnalysis(),\n    \"LDA (sklearn)        \": LinearDiscriminantAnalysis(),\n    \"GaussianNB (sklearn) \": GaussianNB(),\n}\n\nfor ds_name, (X, y) in datasets.items():\n    print(f\"\\n{ds_name.strip()}\")\n    for clf_name, clf in classifiers.items():\n        try:\n            with warnings.catch_warnings():\n                warnings.simplefilter(\"ignore\")\n                scores = cross_val_score(clf, X, y, cv=cv, scoring=\"accuracy\")\n            print(f\"  {clf_name} {scores.mean():.4f} ± {scores.std():.4f}\")\n        except Exception:\n            print(f\"  {clf_name} CRASH\")\nEOF\n","bash",[347],{"type":23,"tag":31,"props":348,"children":349},{"__ignoreMap":7},[350],{"type":28,"value":344},{"type":23,"tag":24,"props":352,"children":353},{},[354],{"type":28,"value":355},"You will see QDA complete all three datasets on sklearn 1.6+. The numbers should match the table above exactly — same random seed, same splits",{"type":23,"tag":53,"props":357,"children":358},{},[],{"type":23,"tag":57,"props":360,"children":362},{"id":361},"what-is-actually-happening-on-breast-cancer",[363],{"type":28,"value":364},"What Is Actually Happening on Breast Cancer",{"type":23,"tag":24,"props":366,"children":367},{},[368],{"type":28,"value":369},"Breast Cancer is the interesting row. 30 features, 569 samples, many correlated measurements. This is where real data lives",{"type":23,"tag":24,"props":371,"children":372},{},[373,375,381,383,389,391,397,399,405],{"type":28,"value":374},"On this dataset all three discriminant classifiers score within two tenths of a percent of each other on accuracy. The difference worth noting is that ",{"type":23,"tag":31,"props":376,"children":378},{"className":377},[],[379],{"type":28,"value":380},"LyubishchevClassifier",{"type":28,"value":382}," has the lowest standard deviation — ",{"type":23,"tag":31,"props":384,"children":386},{"className":385},[],[387],{"type":28,"value":388},"± 0.0035",{"type":28,"value":390}," versus QDA's ",{"type":23,"tag":31,"props":392,"children":394},{"className":393},[],[395],{"type":28,"value":396},"± 0.0055",{"type":28,"value":398}," and LDA's ",{"type":23,"tag":31,"props":400,"children":402},{"className":401},[],[403],{"type":28,"value":404},"± 0.0200",{"type":28,"value":406},". More consistent predictions across folds",{"type":23,"tag":24,"props":408,"children":409},{},[410],{"type":28,"value":411},"More importantly: this dataset exposes a version-dependent fragility in sklearn's own QDA",{"type":23,"tag":69,"props":413,"children":416},{"className":414,"code":415,"language":12,"meta":7},[72],"from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis\nqda_unsafe = QuadraticDiscriminantAnalysis(reg_param=0.0)\n",[417],{"type":23,"tag":31,"props":418,"children":419},{"__ignoreMap":7},[420],{"type":28,"value":415},{"type":23,"tag":24,"props":422,"children":423},{},[424,426,431,433,439],{"type":28,"value":425},"On sklearn 1.6, ",{"type":23,"tag":31,"props":427,"children":429},{"className":428},[],[430],{"type":28,"value":162},{"type":28,"value":432}," applies a small silent regularization and runs. On sklearn 1.9+, it raises ",{"type":23,"tag":31,"props":434,"children":436},{"className":435},[],[437],{"type":28,"value":438},"ValueError: covariance matrix is not full rank",{"type":28,"value":440}," and crashes entirely. The behavior depends on which version you have installed — and you may not know which behaviour you are relying on until it breaks in production",{"type":23,"tag":24,"props":442,"children":443},{},[444,449,451,457],{"type":23,"tag":31,"props":445,"children":447},{"className":446},[],[448],{"type":28,"value":380},{"type":28,"value":450}," applies ",{"type":23,"tag":31,"props":452,"children":454},{"className":453},[],[455],{"type":28,"value":456},"reg_covar",{"type":28,"value":458}," explicitly and by design. The regularization is not a patch — it is part of the method. Lyubishchev's 1943 formulation required a well-conditioned covariance matrix, so the diagonal ridge was structural from the start. The result is deterministic behaviour across sklearn versions: it fits, it predicts, and it does not change behaviour silently when you upgrade",{"type":23,"tag":53,"props":460,"children":461},{},[],{"type":23,"tag":57,"props":463,"children":465},{"id":464},"what-this-is-and-is-not",[466],{"type":28,"value":467},"What This Is and Is Not",{"type":23,"tag":24,"props":469,"children":470},{},[471,476],{"type":23,"tag":31,"props":472,"children":474},{"className":473},[],[475],{"type":28,"value":380},{"type":28,"value":477}," is mathematically equivalent to regularized QDA. It is not magic. On Iris and Wine — clean, low-feature datasets — it produces identical results to sklearn's QDA down to four decimal places, because the two methods are doing the same thing when the covariance is non-singular",{"type":23,"tag":24,"props":479,"children":480},{},[481,483,488],{"type":28,"value":482},"The difference is robustness. Real datasets are not Iris. They have correlated features, more features than samples in some classes, and measurement scales that vary by orders of magnitude. ",{"type":23,"tag":31,"props":484,"children":486},{"className":485},[],[487],{"type":28,"value":380},{"type":28,"value":489}," was built for that reality, because that is the reality Lyubishchev was working in — flea beetle measurements with complex correlation structure, identified by a field entomologist with a ruler",{"type":23,"tag":53,"props":491,"children":492},{},[],{"type":23,"tag":57,"props":494,"children":496},{"id":495},"the-api",[497],{"type":28,"value":498},"The API",{"type":23,"tag":24,"props":500,"children":501},{},[502,504,510,512,518,520],{"type":28,"value":503},"It is fully sklearn-compatible. Passes ",{"type":23,"tag":31,"props":505,"children":507},{"className":506},[],[508],{"type":28,"value":509},"check_estimator",{"type":28,"value":511},". Works inside ",{"type":23,"tag":31,"props":513,"children":515},{"className":514},[],[516],{"type":28,"value":517},"Pipeline",{"type":28,"value":519}," and ",{"type":23,"tag":31,"props":521,"children":523},{"className":522},[],[524],{"type":28,"value":525},"GridSearchCV",{"type":23,"tag":69,"props":527,"children":530},{"className":528,"code":529,"language":12,"meta":7},[72],"from lyubishchev import LyubishchevClassifier\nimport numpy as np\n\nclf = LyubishchevClassifier(standardize=True, reg_covar=1e-6)\nclf.fit(X_train, y_train)\n\nlabels = clf.predict(X_test)\nproba  = clf.predict_proba(X_test)\nscore  = clf.score(X_test, y_test)\n\n# Works in a Pipeline\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.preprocessing import StandardScaler\n\npipe = Pipeline([\n    ('scaler', StandardScaler()),\n    ('clf', LyubishchevClassifier()),\n])\npipe.fit(X_train, y_train)\n",[531],{"type":23,"tag":31,"props":532,"children":533},{"__ignoreMap":7},[534],{"type":28,"value":529},{"type":23,"tag":24,"props":536,"children":537},{},[538],{"type":28,"value":539},"The lower-level functions from the original release are still there:",{"type":23,"tag":69,"props":541,"children":544},{"className":542,"code":543,"language":12,"meta":7},[72],"from lyubishchev import divergence_coefficient, scatter_ellipse, classify, transgression\n",[545],{"type":23,"tag":31,"props":546,"children":547},{"__ignoreMap":7},[548],{"type":28,"value":543},{"type":23,"tag":53,"props":550,"children":551},{},[],{"type":23,"tag":57,"props":553,"children":555},{"id":554},"install",[556],{"type":28,"value":557},"Install",{"type":23,"tag":69,"props":559,"children":562},{"className":560,"code":561,"language":345,"meta":7},[343],"pip install lyubishchev\n",[563],{"type":23,"tag":31,"props":564,"children":565},{"__ignoreMap":7},[566],{"type":28,"value":561},{"type":23,"tag":24,"props":568,"children":569},{},[570,572],{"type":28,"value":571},"Source: ",{"type":23,"tag":573,"props":574,"children":578},"a",{"href":575,"rel":576},"https://github.com/AkzhanBerdi/lyubishchev",[577],"nofollow",[579],{"type":28,"value":580},"github.com/AkzhanBerdi/lyubishchev",{"type":23,"tag":24,"props":582,"children":583},{},[584,586,592,594],{"type":28,"value":585},"Primary source: Lyubishchev, A.A. (1943). ",{"type":23,"tag":587,"props":588,"children":589},"em",{},[590],{"type":28,"value":591},"Programma obshchey sistematiki",{"type":28,"value":593},". Manuscript, 22 November 1943. ",{"type":23,"tag":573,"props":595,"children":598},{"href":596,"rel":597},"http://www.zin.ru/animalia/coleoptera/rus/lyubis05.htm",[577],[599],{"type":28,"value":600},"zin.ru/animalia/coleoptera/rus/lyubis05.htm",{"type":23,"tag":53,"props":602,"children":603},{},[],{"type":23,"tag":57,"props":605,"children":607},{"id":606},"what-is-next",[608],{"type":28,"value":609},"What Is Next",{"type":23,"tag":24,"props":611,"children":612},{},[613],{"type":28,"value":614},"The library is listed under scikit-learn-contrib (#81) — the official sklearn ecosystem for compatible packages. An R version has been submitted to CRAN. A proposal to add lyubishchev_divergence to scipy.spatial.distance is open at scipy #25335, tagged by a SciPy maintainer",{"type":23,"tag":24,"props":616,"children":617},{},[618],{"type":28,"value":619},"If you have used the library, hit an issue, or want to contribute — the repo is open",{"type":23,"tag":53,"props":621,"children":622},{},[],{"type":23,"tag":24,"props":624,"children":625},{},[626],{"type":23,"tag":587,"props":627,"children":628},{},[629],{"type":28,"value":630},"Yours, Bad Dog",{"title":7,"searchDepth":632,"depth":632,"links":633},2,[634,635,636,637,638,639,640,641],{"id":59,"depth":632,"text":62},{"id":97,"depth":632,"text":100},{"id":331,"depth":632,"text":334},{"id":361,"depth":632,"text":364},{"id":464,"depth":632,"text":467},{"id":495,"depth":632,"text":498},{"id":554,"depth":632,"text":557},{"id":606,"depth":632,"text":609},"markdown","content:posts:lyubishchev-pypi-benchmarks.md","content","posts/lyubishchev-pypi-benchmarks.md","posts/lyubishchev-pypi-benchmarks","md",1781008746439]