easyclassifier 0.8.1__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {easyclassifier-0.8.1/src/easyclassifier.egg-info → easyclassifier-0.8.2}/PKG-INFO +16 -4
  2. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/README.md +15 -3
  3. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/pyproject.toml +1 -1
  4. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/__init__.py +5 -3
  5. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/distances.py +12 -3
  6. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/evaluation.py +1 -1
  7. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/help_texts.py +4 -2
  8. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/latex_report.py +52 -23
  9. easyclassifier-0.8.2/src/easyclassifier/references.py +284 -0
  10. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/wizard.py +31 -10
  11. {easyclassifier-0.8.1 → easyclassifier-0.8.2/src/easyclassifier.egg-info}/PKG-INFO +16 -4
  12. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier.egg-info/SOURCES.txt +2 -0
  13. easyclassifier-0.8.2/tests/test_references.py +77 -0
  14. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/tests/test_report.py +21 -0
  15. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/LICENSE +0 -0
  16. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/setup.cfg +0 -0
  17. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/__main__.py +0 -0
  18. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/dataset.py +0 -0
  19. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/demo_data.py +0 -0
  20. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/diagnostics.py +0 -0
  21. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/figures.py +0 -0
  22. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/importance.py +0 -0
  23. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/logbook.py +0 -0
  24. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/models.py +0 -0
  25. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/preprocessing.py +0 -0
  26. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/recommend.py +0 -0
  27. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/reporting.py +0 -0
  28. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/target.py +0 -0
  29. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier/ui.py +0 -0
  30. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier.egg-info/dependency_links.txt +0 -0
  31. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier.egg-info/entry_points.txt +0 -0
  32. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier.egg-info/requires.txt +0 -0
  33. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/src/easyclassifier.egg-info/top_level.txt +0 -0
  34. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/tests/test_figures.py +0 -0
  35. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/tests/test_first_run.py +0 -0
  36. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/tests/test_leakage.py +0 -0
  37. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/tests/test_selection.py +0 -0
  38. {easyclassifier-0.8.1 → easyclassifier-0.8.2}/tests/test_target.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyclassifier
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Machine learning classification without programming - a guided, menu-driven wizard.
5
5
  Author-email: Ahmad Hassanat <ahmad.hassanat@gmail.com>
6
6
  License-Expression: MIT
@@ -40,6 +40,7 @@ Dynamic: license-file
40
40
  [![PyPI](https://img.shields.io/pypi/v/easyclassifier.svg)](https://pypi.org/project/easyclassifier/)
41
41
  [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue.svg)](https://www.python.org/)
42
42
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/ahmadhassant/easyclassifier/blob/main/LICENSE)
43
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23122901.svg)](https://doi.org/10.5281/zenodo.23122901)
43
44
 
44
45
  **Machine Learning without Programming.**
45
46
 
@@ -48,6 +49,11 @@ evaluate machine learning classification models from a CSV or Excel file —
48
49
  no Python code, no scripting, and no machine learning jargon required
49
50
  (unless you want it).
50
51
 
52
+ Regression, time-series forecasting, signal classification and a Windows
53
+ desktop application that runs all four tasks are in
54
+ [EasyResearch](https://github.com/ahmadhassant/easyresearch), which is built
55
+ on EasyClassifier and follows the same rules.
56
+
51
57
  **New to Python or the terminal? Read the step-by-step
52
58
  [User Guide](https://github.com/ahmadhassant/easyclassifier/blob/main/docs/USER_GUIDE.md).** It covers installing Python, installing
53
59
  EasyClassifier, running it, what each question means, and what the results
@@ -222,8 +228,14 @@ The report says whether the normal form (all values >= 0) or the signed form
222
228
  (negative values present) of the formula was applied, and the citation is
223
229
  shown on screen and saved to `citations.txt`:
224
230
 
225
- > Hassanat, A. B. (2014). Dimensionality Invariant Similarity Measure.
226
- > Journal of American Science, 10(8). arXiv:1409.0923.
231
+ > Hassanat, A. B., Alkafaween, E., Tarawneh, A. S., & Elmougy, S. (2022).
232
+ > Applications review of Hassanat distance metric. In *2022 International
233
+ > Conference on Emerging Trends in Computing and Engineering Applications
234
+ > (ETCEA)*, Karak, Jordan (pp. 1–6). IEEE.
235
+ > https://doi.org/10.1109/ETCEA57049.2022.10009844
236
+ >
237
+ > Hassanat, A. B. (2014). Dimensionality invariant similarity measure.
238
+ > arXiv preprint arXiv:1409.0923. https://arxiv.org/abs/1409.0923
227
239
 
228
240
  ## Benchmarks
229
241
 
@@ -254,7 +266,7 @@ can see the whole workflow immediately.
254
266
  ## Citing
255
267
 
256
268
  If you use EasyClassifier in published work, please cite it (GitHub's
257
- "Cite this repository" button uses [`CITATION.cff`](CITATION.cff)); each
269
+ "Cite this repository" button uses [`CITATION.cff`](https://github.com/ahmadhassant/easyclassifier/blob/main/CITATION.cff)); each
258
270
  run also writes the exact references to `citations.txt`.
259
271
 
260
272
  ## Contributing
@@ -4,6 +4,7 @@
4
4
  [![PyPI](https://img.shields.io/pypi/v/easyclassifier.svg)](https://pypi.org/project/easyclassifier/)
5
5
  [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue.svg)](https://www.python.org/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/ahmadhassant/easyclassifier/blob/main/LICENSE)
7
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23122901.svg)](https://doi.org/10.5281/zenodo.23122901)
7
8
 
8
9
  **Machine Learning without Programming.**
9
10
 
@@ -12,6 +13,11 @@ evaluate machine learning classification models from a CSV or Excel file —
12
13
  no Python code, no scripting, and no machine learning jargon required
13
14
  (unless you want it).
14
15
 
16
+ Regression, time-series forecasting, signal classification and a Windows
17
+ desktop application that runs all four tasks are in
18
+ [EasyResearch](https://github.com/ahmadhassant/easyresearch), which is built
19
+ on EasyClassifier and follows the same rules.
20
+
15
21
  **New to Python or the terminal? Read the step-by-step
16
22
  [User Guide](https://github.com/ahmadhassant/easyclassifier/blob/main/docs/USER_GUIDE.md).** It covers installing Python, installing
17
23
  EasyClassifier, running it, what each question means, and what the results
@@ -186,8 +192,14 @@ The report says whether the normal form (all values >= 0) or the signed form
186
192
  (negative values present) of the formula was applied, and the citation is
187
193
  shown on screen and saved to `citations.txt`:
188
194
 
189
- > Hassanat, A. B. (2014). Dimensionality Invariant Similarity Measure.
190
- > Journal of American Science, 10(8). arXiv:1409.0923.
195
+ > Hassanat, A. B., Alkafaween, E., Tarawneh, A. S., & Elmougy, S. (2022).
196
+ > Applications review of Hassanat distance metric. In *2022 International
197
+ > Conference on Emerging Trends in Computing and Engineering Applications
198
+ > (ETCEA)*, Karak, Jordan (pp. 1–6). IEEE.
199
+ > https://doi.org/10.1109/ETCEA57049.2022.10009844
200
+ >
201
+ > Hassanat, A. B. (2014). Dimensionality invariant similarity measure.
202
+ > arXiv preprint arXiv:1409.0923. https://arxiv.org/abs/1409.0923
191
203
 
192
204
  ## Benchmarks
193
205
 
@@ -218,7 +230,7 @@ can see the whole workflow immediately.
218
230
  ## Citing
219
231
 
220
232
  If you use EasyClassifier in published work, please cite it (GitHub's
221
- "Cite this repository" button uses [`CITATION.cff`](CITATION.cff)); each
233
+ "Cite this repository" button uses [`CITATION.cff`](https://github.com/ahmadhassant/easyclassifier/blob/main/CITATION.cff)); each
222
234
  run also writes the exact references to `citations.txt`.
223
235
 
224
236
  ## Contributing
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "easyclassifier"
7
- version = "0.8.1"
7
+ version = "0.8.2"
8
8
  description = "Machine learning classification without programming - a guided, menu-driven wizard."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,12 +4,14 @@ A guided, menu-driven wizard that lets non-programmers build and evaluate
4
4
  classification models from a CSV file.
5
5
  """
6
6
 
7
- __version__ = "0.8.1"
7
+ __version__ = "0.8.2"
8
8
  __author__ = "Ahmad Hassanat"
9
9
 
10
+ # As in CITATION.cff and the user guide (Zenodo DOI).
10
11
  CITATION = (
11
- "Hassanat, A. (2026). EasyClassifier: Machine Learning without "
12
- "Programming (Version {version}) [Software].".format(version=__version__)
12
+ "Hassanat, A. B. A. (2026). EasyClassifier: Machine Learning without "
13
+ "Programming (Version {version}) [Software]. Zenodo. "
14
+ "https://doi.org/10.5281/zenodo.23122901".format(version=__version__)
13
15
  )
14
16
 
15
17
  from .wizard import Wizard # noqa: E402
@@ -23,12 +23,21 @@ from sklearn.base import BaseEstimator, ClassifierMixin
23
23
  from sklearn.neighbors import KNeighborsClassifier
24
24
 
25
25
 
26
+ # The Hassanat distance is cited with the IEEE conference paper that reviews
27
+ # its applications, and the arXiv preprint that introduced it.
26
28
  HASSANAT_CITATION = (
27
- "Hassanat, A. B. (2014). Dimensionality Invariant Similarity Measure. "
28
- "Journal of American Science, 10(8). arXiv:1409.0923."
29
+ "Hassanat, A. B., Alkafaween, E., Tarawneh, A. S., & Elmougy, S. (2022). "
30
+ "Applications review of Hassanat distance metric. In 2022 International "
31
+ "Conference on Emerging Trends in Computing and Engineering Applications "
32
+ "(ETCEA), Karak, Jordan (pp. 1-6). IEEE. "
33
+ "https://doi.org/10.1109/ETCEA57049.2022.10009844"
34
+ )
35
+ HASSANAT_ARXIV_CITATION = (
36
+ "Hassanat, A. B. (2014). Dimensionality invariant similarity measure. "
37
+ "arXiv preprint arXiv:1409.0923. https://arxiv.org/abs/1409.0923"
29
38
  )
30
39
 
31
- HASSANAT_CITATIONS: List[str] = [HASSANAT_CITATION]
40
+ HASSANAT_CITATIONS: List[str] = [HASSANAT_CITATION, HASSANAT_ARXIV_CITATION]
32
41
 
33
42
  # key -> (menu label, sklearn metric name or None for custom)
34
43
  DISTANCES: Dict[str, tuple] = {
@@ -32,7 +32,7 @@ METRICS = {
32
32
  "precision": "Precision",
33
33
  "recall": "Recall",
34
34
  "f1": "F1",
35
- "specificity": "Specificity",
35
+ "specificity": "Macro specificity",
36
36
  "balanced_accuracy": "Balanced Accuracy",
37
37
  "mcc": "MCC",
38
38
  "cohen_kappa": "Cohen Kappa",
@@ -14,8 +14,10 @@ GLOSSARY = {
14
14
  "items the model successfully found.",
15
15
  "f1": "F1 is a single score that balances precision and recall. It is "
16
16
  "high only when both are high.",
17
- "specificity": "Specificity measures how many of the real negative items "
18
- "the model correctly labeled negative.",
17
+ "specificity": "Macro specificity: for each class, the share of items "
18
+ "from the other classes that the model correctly did not "
19
+ "put in that class, averaged over all classes. With two "
20
+ "classes it equals balanced accuracy.",
19
21
  "sensitivity": "Sensitivity is another name for recall: how many real "
20
22
  "positives the model found.",
21
23
  "mcc": "Matthews Correlation Coefficient summarises the whole confusion "
@@ -25,15 +25,14 @@ from typing import Dict, List, Optional, Tuple
25
25
 
26
26
  import pandas as pd
27
27
 
28
- from .distances import HASSANAT_CITATION
28
+ from . import references as refs
29
29
  from .evaluation import METRICS, SELECTION_LABEL, VALIDATION
30
30
  from .help_texts import explain
31
+ from .references import cite
31
32
 
32
- SKLEARN_CITATION = ("Pedregosa, F., et al. (2011). Scikit-learn: Machine "
33
- "Learning in Python. Journal of Machine Learning "
34
- "Research, 12, 2825-2830.")
35
- BREIMAN_CITATION = ("Breiman, L. (2001). Random Forests. Machine Learning, "
36
- "45, 5-32.")
33
+ # Kept for the EasyResearch cores, which import them from here.
34
+ SKLEARN_CITATION = refs.REFERENCES["sklearn"]
35
+ BREIMAN_CITATION = refs.REFERENCES["breiman2001"]
37
36
 
38
37
  PERCENT_METRICS = {"accuracy", "precision", "recall", "f1",
39
38
  "balanced_accuracy", "specificity"}
@@ -76,6 +75,11 @@ class ReportContext:
76
75
  importance: Optional[pd.DataFrame] = None
77
76
  comparison_text: str = ""
78
77
  learning_text: str = ""
78
+ # Registry keys of the classifiers (same order as ``classifiers``) and the
79
+ # KNN distance, so that each method is cited.
80
+ classifier_keys: List[str] = field(default_factory=list)
81
+ knn_distance: Optional[str] = None
82
+ hassanat_signed: bool = False # signed form applied (negative values)
79
83
 
80
84
 
81
85
  # --------------------------------------------------------------------------- #
@@ -195,7 +199,8 @@ def _methods_paragraph(c: ReportContext) -> str:
195
199
  parts = [
196
200
  f"Classification was carried out with EasyClassifier version "
197
201
  f"{tex(c.version)} \\cite{{easyclassifier}}, which is built on "
198
- f"scikit-learn \\cite{{sklearn}}.",
202
+ f"scikit-learn \\cite{{sklearn}}, NumPy \\cite{{numpy}} and pandas "
203
+ f"\\cite{{pandas}}.",
199
204
  f"The dataset ({tex(c.dataset)}) contained {c.rows_loaded} rows and "
200
205
  f"{c.cols_loaded} columns. The outcome to predict was "
201
206
  f"\\emph{{{tex(c.target)}}} with {k} classes.",
@@ -243,14 +248,23 @@ def _methods_paragraph(c: ReportContext) -> str:
243
248
  "separately within every validation split, so that no "
244
249
  "information from test data entered model training.")
245
250
 
251
+ if len(c.classifier_keys) == len(c.classifiers):
252
+ named = [tex(n) + cite(refs.CLASSIFIER_REFERENCES.get(k, []))
253
+ for n, k in zip(c.classifiers, c.classifier_keys)]
254
+ else:
255
+ named = [tex(n) for n in c.classifiers]
246
256
  parts.append(
247
257
  "The following classifiers were used with their default parameters, "
248
- "without hyperparameter tuning: " + tex(_join(c.classifiers)) + ".")
258
+ "without hyperparameter tuning: " + _join(named) + ".")
249
259
  if c.knn_text:
250
- cite = " \\cite{hassanat2014}" if c.hassanat_used else ""
251
- parts.append(tex(c.knn_text) + cite + ".")
260
+ keys = refs.DISTANCE_REFERENCES.get(c.knn_distance or "", [])
261
+ if c.hassanat_used:
262
+ keys = refs.DISTANCE_REFERENCES["hassanat"] + (
263
+ refs.HASSANAT_SIGNED_REFERENCES if c.hassanat_signed else [])
264
+ parts.append(tex(c.knn_text) + cite(keys) + ".")
252
265
  parts.append(f"Performance was estimated with "
253
- f"{tex(VALIDATION[c.validation].lower())}.")
266
+ f"{tex(VALIDATION[c.validation].lower())}"
267
+ f"{cite(refs.VALIDATION_REFERENCES.get(c.validation, []))}.")
254
268
 
255
269
  if c.final is not None and c.selection == "final_test":
256
270
  parts.append(
@@ -259,7 +273,8 @@ def _methods_paragraph(c: ReportContext) -> str:
259
273
  f"{c.n_test} rows (20\\%, stratified by class) were set aside "
260
274
  f"before any analysis; classifiers were compared on the "
261
275
  f"remaining {c.n_dev} rows only, and the selected classifier was "
262
- f"evaluated once on the untouched test rows.")
276
+ f"evaluated once on the untouched test rows"
277
+ f"{cite(refs.SELECTION_REFERENCES['final_test'])}.")
263
278
  elif c.final is not None and c.selection == "nested":
264
279
  parts.append(
265
280
  f"The classifier with the highest {SELECTION_LABEL.lower()} was "
@@ -267,7 +282,7 @@ def _methods_paragraph(c: ReportContext) -> str:
267
282
  f"performance was estimated with nested 5-fold cross-validation, "
268
283
  f"in which the complete comparison was repeated within each "
269
284
  f"outer training fold and the winner was evaluated on the outer "
270
- f"test fold.")
285
+ f"test fold{cite(refs.SELECTION_REFERENCES['nested'])}.")
271
286
  if c.importance is not None:
272
287
  parts.append(
273
288
  "The contribution of each predictor was estimated by permutation "
@@ -275,6 +290,21 @@ def _methods_paragraph(c: ReportContext) -> str:
275
290
  f"{SELECTION_LABEL.lower()} when that column's values were "
276
291
  "randomly shuffled, measured on the "
277
292
  f"{tex(c.importance.attrs.get('measured_on', 'held-out rows'))}.")
293
+ measures = [tex(METRICS[m]) + cite(refs.METRIC_REFERENCES.get(m, []))
294
+ for m in c.metrics if m in METRICS]
295
+ parts.append(
296
+ f"Classifiers were ranked by {SELECTION_LABEL.lower()}"
297
+ f"{cite(refs.METRIC_REFERENCES['balanced_accuracy'])}"
298
+ + (f"; the measures reported were {_join(measures)}" if measures
299
+ else "") + ".")
300
+ if c.figures:
301
+ curves = [f"{label}{cite(refs.FIGURE_REFERENCES[k])}"
302
+ for k, label in (("roc_curve", "ROC curves"),
303
+ ("pr_curve", "precision-recall curves"))
304
+ if k in c.figures]
305
+ parts.append("Figures were drawn with Matplotlib \\cite{matplotlib}"
306
+ + (", including " + _join(curves) if curves else "")
307
+ + ".")
278
308
  return " ".join(parts)
279
309
 
280
310
 
@@ -471,16 +501,15 @@ def build_tex(c: ReportContext) -> str:
471
501
  "\\texttt{log.txt} in the Results folder.\n"
472
502
  "\\end{itemize}\n")
473
503
 
474
- # 6. References --------------------------------------------------------- #
475
- L.append("\\begin{thebibliography}{9}\n")
476
- L.append(f"\\bibitem{{easyclassifier}} {tex(c.software_citation)}\n")
477
- L.append(f"\\bibitem{{sklearn}} {tex(SKLEARN_CITATION)}\n")
478
- if c.hassanat_used:
479
- L.append(f"\\bibitem{{hassanat2014}} {tex(HASSANAT_CITATION)}\n")
480
- if c.importance is not None:
481
- L.append(f"\\bibitem{{breiman2001}} {tex(BREIMAN_CITATION)}\n")
482
- L.append("\\end{thebibliography}\n\\end{document}\n")
483
- return "".join(L)
504
+ # 6. References: every reference cited above, in order of citation ----- #
505
+ body = "".join(L)
506
+ return (body + refs.bibliography(body, report_entries(c), escape=tex)
507
+ + "\\end{document}\n")
508
+
509
+
510
+ def report_entries(c: ReportContext) -> Dict[str, str]:
511
+ """Report-specific bibliography entries (the software itself)."""
512
+ return {"easyclassifier": c.software_citation}
484
513
 
485
514
 
486
515
  # --------------------------------------------------------------------------- #
@@ -0,0 +1,284 @@
1
+ """References for every method a report can mention.
2
+
3
+ Each report cites, in its Methods paragraph, the software and every method
4
+ that was actually used: the classifiers (or other models), the KNN distance,
5
+ the evaluation design, the measures and the figures that need a source. The
6
+ bibliography and ``citations.txt`` are generated from the ``\\cite{...}``
7
+ commands in the finished report, so a reference is listed exactly when it is
8
+ cited, and nothing that is cited can be missing.
9
+
10
+ The EasyResearch cores (regression, forecasting, signal classification)
11
+ extend this list with the references of their own models.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from typing import Dict, Iterable, List, Optional
18
+
19
+ from .distances import HASSANAT_ARXIV_CITATION, HASSANAT_CITATION
20
+
21
+ # --------------------------------------------------------------------------- #
22
+ # Reference texts
23
+ # --------------------------------------------------------------------------- #
24
+
25
+ REFERENCES: Dict[str, str] = {
26
+ # Software
27
+ "sklearn": "Pedregosa, F., et al. (2011). Scikit-learn: Machine "
28
+ "Learning in Python. Journal of Machine Learning Research, "
29
+ "12, 2825-2830.",
30
+ "numpy": "Harris, C. R., Millman, K. J., van der Walt, S. J., et al. "
31
+ "(2020). Array programming with NumPy. Nature, 585(7825), "
32
+ "357-362. https://doi.org/10.1038/s41586-020-2649-2",
33
+ "pandas": "McKinney, W. (2010). Data structures for statistical "
34
+ "computing in Python. In Proceedings of the 9th Python in "
35
+ "Science Conference (pp. 56-61). "
36
+ "https://doi.org/10.25080/Majora-92bf1922-00a",
37
+ "matplotlib": "Hunter, J. D. (2007). Matplotlib: A 2D graphics "
38
+ "environment. Computing in Science & Engineering, 9(3), "
39
+ "90-95. https://doi.org/10.1109/MCSE.2007.55",
40
+ # Classifiers
41
+ "breiman1984": "Breiman, L., Friedman, J. H., Olshen, R. A., & Stone, "
42
+ "C. J. (1984). Classification and Regression Trees. "
43
+ "Wadsworth.",
44
+ "breiman2001": "Breiman, L. (2001). Random Forests. Machine Learning, "
45
+ "45, 5-32. https://doi.org/10.1023/A:1010933404324",
46
+ "cortes1995": "Cortes, C., & Vapnik, V. (1995). Support-vector "
47
+ "networks. Machine Learning, 20(3), 273-297. "
48
+ "https://doi.org/10.1007/BF00994018",
49
+ "platt1999": "Platt, J. C. (1999). Probabilistic outputs for support "
50
+ "vector machines and comparisons to regularized likelihood "
51
+ "methods. In A. J. Smola, P. Bartlett, B. Schölkopf, & "
52
+ "D. Schuurmans (Eds.), Advances in Large Margin Classifiers "
53
+ "(pp. 61-74). MIT Press.",
54
+ "cox1958": "Cox, D. R. (1958). The regression analysis of binary "
55
+ "sequences. Journal of the Royal Statistical Society: Series "
56
+ "B, 20(2), 215-242. "
57
+ "https://doi.org/10.1111/j.2517-6161.1958.tb00292.x",
58
+ "cover1967": "Cover, T., & Hart, P. (1967). Nearest neighbor pattern "
59
+ "classification. IEEE Transactions on Information Theory, "
60
+ "13(1), 21-27. https://doi.org/10.1109/TIT.1967.1053964",
61
+ "hand2001": "Hand, D. J., & Yu, K. (2001). Idiot's Bayes - not so "
62
+ "stupid after all? International Statistical Review, 69(3), "
63
+ "385-398. "
64
+ "https://doi.org/10.1111/j.1751-5823.2001.tb00465.x",
65
+ "chen2016": "Chen, T., & Guestrin, C. (2016). XGBoost: A scalable tree "
66
+ "boosting system. In Proceedings of the 22nd ACM SIGKDD "
67
+ "International Conference on Knowledge Discovery and Data "
68
+ "Mining (pp. 785-794). "
69
+ "https://doi.org/10.1145/2939672.2939785",
70
+ "ke2017": "Ke, G., Meng, Q., Finley, T., Wang, T., Chen, W., Ma, W., "
71
+ "Ye, Q., & Liu, T.-Y. (2017). LightGBM: A highly efficient "
72
+ "gradient boosting decision tree. In Advances in Neural "
73
+ "Information Processing Systems 30 (pp. 3146-3154).",
74
+ "rumelhart1986": "Rumelhart, D. E., Hinton, G. E., & Williams, R. J. "
75
+ "(1986). Learning representations by back-propagating "
76
+ "errors. Nature, 323(6088), 533-536. "
77
+ "https://doi.org/10.1038/323533a0",
78
+ "kingma2015": "Kingma, D. P., & Ba, J. (2015). Adam: A method for "
79
+ "stochastic optimization. In International Conference on "
80
+ "Learning Representations (ICLR). arXiv:1412.6980.",
81
+ # Distances
82
+ "hassanat2022": HASSANAT_CITATION,
83
+ "hassanat2014": HASSANAT_ARXIV_CITATION,
84
+ "alaydaa2026": "Alaydaa, M. S., Alotibi, G. N., Tarawneh, A. S., & "
85
+ "Hassanat, A. B. (2026). A sign-symmetric reformulation "
86
+ "of the Hassanat distance for data with negative feature "
87
+ "values. Symmetry, 18(7), 1225. "
88
+ "https://doi.org/10.3390/sym18071225",
89
+ "lance1966": "Lance, G. N., & Williams, W. T. (1966). Computer programs "
90
+ "for hierarchical polythetic classification (\"similarity "
91
+ "analyses\"). The Computer Journal, 9(1), 60-64. "
92
+ "https://doi.org/10.1093/comjnl/9.1.60",
93
+ "deza2009": "Deza, M. M., & Deza, E. (2009). Encyclopedia of Distances. "
94
+ "Springer. https://doi.org/10.1007/978-3-642-00234-2",
95
+ # Evaluation
96
+ "kohavi1995": "Kohavi, R. (1995). A study of cross-validation and "
97
+ "bootstrap for accuracy estimation and model selection. In "
98
+ "Proceedings of the 14th International Joint Conference on "
99
+ "Artificial Intelligence (IJCAI) (pp. 1137-1143).",
100
+ "lachenbruch1968": "Lachenbruch, P. A., & Mickey, M. R. (1968). "
101
+ "Estimation of error rates in discriminant analysis. "
102
+ "Technometrics, 10(1), 1-11. "
103
+ "https://doi.org/10.1080/00401706.1968.10490530",
104
+ "varma2006": "Varma, S., & Simon, R. (2006). Bias in error estimation "
105
+ "when using cross-validation for model selection. BMC "
106
+ "Bioinformatics, 7, 91. "
107
+ "https://doi.org/10.1186/1471-2105-7-91",
108
+ "cawley2010": "Cawley, G. C., & Talbot, N. L. C. (2010). On over-fitting "
109
+ "in model selection and subsequent selection bias in "
110
+ "performance evaluation. Journal of Machine Learning "
111
+ "Research, 11, 2079-2107.",
112
+ # Measures
113
+ "brodersen2010": "Brodersen, K. H., Ong, C. S., Stephan, K. E., & "
114
+ "Buhmann, J. M. (2010). The balanced accuracy and its "
115
+ "posterior distribution. In 20th International "
116
+ "Conference on Pattern Recognition (pp. 3121-3124). "
117
+ "IEEE. https://doi.org/10.1109/ICPR.2010.764",
118
+ "matthews1975": "Matthews, B. W. (1975). Comparison of the predicted and "
119
+ "observed secondary structure of T4 phage lysozyme. "
120
+ "Biochimica et Biophysica Acta - Protein Structure, "
121
+ "405(2), 442-451. "
122
+ "https://doi.org/10.1016/0005-2795(75)90109-9",
123
+ "cohen1960": "Cohen, J. (1960). A coefficient of agreement for nominal "
124
+ "scales. Educational and Psychological Measurement, 20(1), "
125
+ "37-46. https://doi.org/10.1177/001316446002000104",
126
+ "fawcett2006": "Fawcett, T. (2006). An introduction to ROC analysis. "
127
+ "Pattern Recognition Letters, 27(8), 861-874. "
128
+ "https://doi.org/10.1016/j.patrec.2005.10.010",
129
+ "davis2006": "Davis, J., & Goadrich, M. (2006). The relationship between "
130
+ "precision-recall and ROC curves. In Proceedings of the "
131
+ "23rd International Conference on Machine Learning "
132
+ "(pp. 233-240). https://doi.org/10.1145/1143844.1143874",
133
+ }
134
+
135
+ # What each reference is cited for (shown in citations.txt).
136
+ TOPICS: Dict[str, str] = {
137
+ "sklearn": "scikit-learn (models, preprocessing and evaluation)",
138
+ "numpy": "NumPy (numerical computing)",
139
+ "pandas": "pandas (data handling)",
140
+ "matplotlib": "Matplotlib (figures)",
141
+ "breiman1984": "Decision tree (CART)",
142
+ "breiman2001": "Random Forest; permutation importance",
143
+ "cortes1995": "Support vector machine",
144
+ "platt1999": "SVM probability estimates (Platt scaling)",
145
+ "cox1958": "Logistic regression",
146
+ "cover1967": "K-nearest neighbours",
147
+ "hand2001": "Naive Bayes",
148
+ "chen2016": "XGBoost",
149
+ "ke2017": "LightGBM; histogram-based gradient boosting",
150
+ "rumelhart1986": "Neural network (multilayer perceptron, backpropagation)",
151
+ "kingma2015": "Adam optimiser (neural network training)",
152
+ "hassanat2022": "Hassanat distance",
153
+ "hassanat2014": "Hassanat distance (original preprint)",
154
+ "alaydaa2026": "Signed form of the Hassanat distance (negative values)",
155
+ "lance1966": "Canberra distance",
156
+ "deza2009": "Distance measures (Euclidean, Manhattan, Chebyshev, cosine)",
157
+ "kohavi1995": "Stratified k-fold cross-validation",
158
+ "lachenbruch1968": "Leave-one-out validation",
159
+ "varma2006": "Nested cross-validation",
160
+ "cawley2010": "Selection bias and separate final evaluation",
161
+ "brodersen2010": "Balanced accuracy",
162
+ "matthews1975": "Matthews correlation coefficient (MCC)",
163
+ "cohen1960": "Cohen's kappa",
164
+ "fawcett2006": "ROC curves and ROC AUC",
165
+ "davis2006": "Precision-recall curves",
166
+ }
167
+
168
+ # --------------------------------------------------------------------------- #
169
+ # Which references go with which choice
170
+ # --------------------------------------------------------------------------- #
171
+
172
+ SOFTWARE = ["sklearn", "numpy", "pandas"]
173
+
174
+ CLASSIFIER_REFERENCES: Dict[str, List[str]] = {
175
+ "decision_tree": ["breiman1984"],
176
+ "random_forest": ["breiman2001"],
177
+ "svm": ["cortes1995", "platt1999"],
178
+ "logistic_regression": ["cox1958"],
179
+ "knn": ["cover1967"],
180
+ "naive_bayes": ["hand2001"],
181
+ "xgboost": ["chen2016"],
182
+ "lightgbm": ["ke2017"],
183
+ "neural_network": ["rumelhart1986", "kingma2015"],
184
+ }
185
+
186
+ DISTANCE_REFERENCES: Dict[str, List[str]] = {
187
+ "hassanat": ["hassanat2022", "hassanat2014"],
188
+ "euclidean": ["deza2009"],
189
+ "manhattan": ["deza2009"],
190
+ "chebyshev": ["deza2009"],
191
+ "canberra": ["lance1966"],
192
+ "cosine": ["deza2009"],
193
+ }
194
+
195
+ # Cited in addition when the signed form of the Hassanat formula was applied
196
+ # (some values negative).
197
+ HASSANAT_SIGNED_REFERENCES = ["alaydaa2026"]
198
+
199
+ VALIDATION_REFERENCES: Dict[str, List[str]] = {
200
+ "holdout": [],
201
+ "kfold5": ["kohavi1995"],
202
+ "kfold10": ["kohavi1995"],
203
+ "stratified": ["kohavi1995"],
204
+ "loo": ["lachenbruch1968"],
205
+ }
206
+
207
+ SELECTION_REFERENCES: Dict[str, List[str]] = {
208
+ "nested": ["varma2006", "cawley2010"],
209
+ "final_test": ["cawley2010"],
210
+ }
211
+
212
+ # Measures that need a source (accuracy, precision, recall, F1, specificity
213
+ # and log loss are standard definitions).
214
+ METRIC_REFERENCES: Dict[str, List[str]] = {
215
+ "balanced_accuracy": ["brodersen2010"],
216
+ "mcc": ["matthews1975"],
217
+ "cohen_kappa": ["cohen1960"],
218
+ "roc_auc": ["fawcett2006"],
219
+ }
220
+
221
+ FIGURE_REFERENCES: Dict[str, List[str]] = {
222
+ "roc_curve": ["fawcett2006"],
223
+ "pr_curve": ["davis2006"],
224
+ }
225
+
226
+ # --------------------------------------------------------------------------- #
227
+ # Helpers
228
+ # --------------------------------------------------------------------------- #
229
+
230
+ _CITE = re.compile(r"\\cite\{([^}]*)\}")
231
+
232
+
233
+ def cite(keys: Iterable[str]) -> str:
234
+ """`` \\cite{a,b}`` for the given keys (in order, without repeats), or an
235
+ empty string when there are none."""
236
+ keys = list(dict.fromkeys(k for k in keys if k))
237
+ return f" \\cite{{{','.join(keys)}}}" if keys else ""
238
+
239
+
240
+ def cited_keys(text: str) -> List[str]:
241
+ """Every key cited in a LaTeX text, in order of first citation."""
242
+ keys: List[str] = []
243
+ for group in _CITE.findall(text):
244
+ for k in group.split(","):
245
+ k = k.strip()
246
+ if k and k not in keys:
247
+ keys.append(k)
248
+ return keys
249
+
250
+
251
+ def lookup(key: str, extra: Optional[Dict[str, str]] = None) -> str:
252
+ if extra and key in extra:
253
+ return extra[key]
254
+ if key in REFERENCES:
255
+ return REFERENCES[key]
256
+ raise KeyError(f"no reference text for \\cite{{{key}}}")
257
+
258
+
259
+ def bibliography(text: str, extra: Optional[Dict[str, str]] = None,
260
+ escape=lambda s: s) -> str:
261
+ """The ``thebibliography`` block for every key cited in ``text``, in
262
+ order of first citation. ``extra`` holds report-specific entries (the
263
+ software itself, the dataset). A cited key without a text raises an
264
+ error, so a report can never cite a reference it does not list."""
265
+ keys = cited_keys(text)
266
+ lines = ["\\begin{thebibliography}{99}\n"]
267
+ lines += [f"\\bibitem{{{k}}} {escape(lookup(k, extra))}\n" for k in keys]
268
+ lines.append("\\end{thebibliography}\n")
269
+ return "".join(lines)
270
+
271
+
272
+ def citation_lines(text: str, extra: Optional[Dict[str, str]] = None,
273
+ topics: Optional[Dict[str, str]] = None) -> List[str]:
274
+ """``citations.txt``: every reference cited in the report, with what it
275
+ is cited for."""
276
+ topics = dict(TOPICS, **(topics or {}))
277
+ lines = ["Please cite the following if you publish these results.",
278
+ "They are the references of the report (report.tex), in the "
279
+ "order they are cited there.", ""]
280
+ for i, k in enumerate(cited_keys(text), 1):
281
+ what = topics.get(k)
282
+ lines.append(f"[{i}] " + (f"{what}:" if what else ""))
283
+ lines.append(" " + lookup(k, extra))
284
+ return lines
@@ -29,7 +29,9 @@ from .diagnostics import (
29
29
  learning_curve_data,
30
30
  )
31
31
  from .importance import honest_importance
32
- from .latex_report import ReportContext, write_and_compile
32
+ from .latex_report import (ReportContext, build_tex, report_entries,
33
+ write_and_compile)
34
+ from .references import citation_lines
33
35
  from .help_texts import explain
34
36
  from .logbook import LogBook
35
37
  from .models import build_registry
@@ -1076,14 +1078,22 @@ class Wizard:
1076
1078
  saved.append(rep.save_model(
1077
1079
  model, os.path.join(out_dir, "trained_model.pkl")))
1078
1080
 
1081
+ # citations.txt lists exactly the references cited in the report.
1082
+ try:
1083
+ ctx = self._report_context(best, results, final, X, y,
1084
+ fig_files, importance)
1085
+ except Exception as exc: # noqa: BLE001
1086
+ ctx = None
1087
+ self.log.add(f"Report context FAILED: {exc}")
1079
1088
  saved.append(self.save_citations(os.path.join(out_dir,
1080
- "citations.txt")))
1089
+ "citations.txt"), ctx))
1081
1090
 
1082
1091
  # Report: report.tex, compiled to report.pdf if LaTeX is installed.
1083
1092
  ui.info("Writing the report ...")
1084
1093
  try:
1085
- ctx = self._report_context(best, results, final, X, y,
1086
- fig_files, importance)
1094
+ if ctx is None:
1095
+ raise RuntimeError("the report information could not be "
1096
+ "collected (see the log)")
1087
1097
  tex_path, pdf_path, msg = write_and_compile(ctx, out_dir)
1088
1098
  saved.append(tex_path)
1089
1099
  if pdf_path:
@@ -1212,6 +1222,10 @@ class Wizard:
1212
1222
  comparison_text=self.comparison_text,
1213
1223
  learning_text=self.learning_text,
1214
1224
  importance=importance,
1225
+ classifier_keys=list(self._classifiers),
1226
+ knn_distance=self.knn_distance,
1227
+ hassanat_signed=bool(self.hassanat_form
1228
+ and self.hassanat_form["form"] == "signed"),
1215
1229
  )
1216
1230
 
1217
1231
  def _pipeline_spec(self, key: str, X):
@@ -1223,13 +1237,20 @@ class Wizard:
1223
1237
  factory=lambda: pp.build_pipeline(X, cfg, spec.factory(), scale),
1224
1238
  )
1225
1239
 
1226
- def save_citations(self, path: str) -> str:
1227
- lines = ["Please cite the following if you publish these results:",
1228
- "", "Software:", " " + CITATION]
1240
+ def save_citations(self, path: str, ctx=None) -> str:
1241
+ if ctx is not None:
1242
+ lines = citation_lines(build_tex(ctx), report_entries(ctx),
1243
+ {"easyclassifier": "EasyClassifier "
1244
+ "(this software)"})
1245
+ else:
1246
+ lines = ["Please cite the following if you publish these "
1247
+ "results:", "", "Software:", " " + CITATION]
1229
1248
  if self.hassanat_form:
1230
- lines += ["", "Hassanat distance (used by KNN):"]
1231
- lines += [" " + c for c in HASSANAT_CITATIONS]
1232
- lines += ["", "Formula form applied: " + self.hassanat_form["text"]]
1249
+ if ctx is None:
1250
+ lines += ["", "Hassanat distance (used by KNN):"]
1251
+ lines += [" " + c for c in HASSANAT_CITATIONS]
1252
+ lines += ["", "Hassanat distance, formula form applied: "
1253
+ + self.hassanat_form["text"]]
1233
1254
  with open(path, "w", encoding="utf-8") as fh:
1234
1255
  fh.write("\n".join(lines) + "\n")
1235
1256
  return path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyclassifier
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Machine learning classification without programming - a guided, menu-driven wizard.
5
5
  Author-email: Ahmad Hassanat <ahmad.hassanat@gmail.com>
6
6
  License-Expression: MIT
@@ -40,6 +40,7 @@ Dynamic: license-file
40
40
  [![PyPI](https://img.shields.io/pypi/v/easyclassifier.svg)](https://pypi.org/project/easyclassifier/)
41
41
  [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue.svg)](https://www.python.org/)
42
42
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/ahmadhassant/easyclassifier/blob/main/LICENSE)
43
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23122901.svg)](https://doi.org/10.5281/zenodo.23122901)
43
44
 
44
45
  **Machine Learning without Programming.**
45
46
 
@@ -48,6 +49,11 @@ evaluate machine learning classification models from a CSV or Excel file —
48
49
  no Python code, no scripting, and no machine learning jargon required
49
50
  (unless you want it).
50
51
 
52
+ Regression, time-series forecasting, signal classification and a Windows
53
+ desktop application that runs all four tasks are in
54
+ [EasyResearch](https://github.com/ahmadhassant/easyresearch), which is built
55
+ on EasyClassifier and follows the same rules.
56
+
51
57
  **New to Python or the terminal? Read the step-by-step
52
58
  [User Guide](https://github.com/ahmadhassant/easyclassifier/blob/main/docs/USER_GUIDE.md).** It covers installing Python, installing
53
59
  EasyClassifier, running it, what each question means, and what the results
@@ -222,8 +228,14 @@ The report says whether the normal form (all values >= 0) or the signed form
222
228
  (negative values present) of the formula was applied, and the citation is
223
229
  shown on screen and saved to `citations.txt`:
224
230
 
225
- > Hassanat, A. B. (2014). Dimensionality Invariant Similarity Measure.
226
- > Journal of American Science, 10(8). arXiv:1409.0923.
231
+ > Hassanat, A. B., Alkafaween, E., Tarawneh, A. S., & Elmougy, S. (2022).
232
+ > Applications review of Hassanat distance metric. In *2022 International
233
+ > Conference on Emerging Trends in Computing and Engineering Applications
234
+ > (ETCEA)*, Karak, Jordan (pp. 1–6). IEEE.
235
+ > https://doi.org/10.1109/ETCEA57049.2022.10009844
236
+ >
237
+ > Hassanat, A. B. (2014). Dimensionality invariant similarity measure.
238
+ > arXiv preprint arXiv:1409.0923. https://arxiv.org/abs/1409.0923
227
239
 
228
240
  ## Benchmarks
229
241
 
@@ -254,7 +266,7 @@ can see the whole workflow immediately.
254
266
  ## Citing
255
267
 
256
268
  If you use EasyClassifier in published work, please cite it (GitHub's
257
- "Cite this repository" button uses [`CITATION.cff`](CITATION.cff)); each
269
+ "Cite this repository" button uses [`CITATION.cff`](https://github.com/ahmadhassant/easyclassifier/blob/main/CITATION.cff)); each
258
270
  run also writes the exact references to `citations.txt`.
259
271
 
260
272
  ## Contributing
@@ -16,6 +16,7 @@ src/easyclassifier/logbook.py
16
16
  src/easyclassifier/models.py
17
17
  src/easyclassifier/preprocessing.py
18
18
  src/easyclassifier/recommend.py
19
+ src/easyclassifier/references.py
19
20
  src/easyclassifier/reporting.py
20
21
  src/easyclassifier/target.py
21
22
  src/easyclassifier/ui.py
@@ -29,6 +30,7 @@ src/easyclassifier.egg-info/top_level.txt
29
30
  tests/test_figures.py
30
31
  tests/test_first_run.py
31
32
  tests/test_leakage.py
33
+ tests/test_references.py
32
34
  tests/test_report.py
33
35
  tests/test_selection.py
34
36
  tests/test_target.py
@@ -0,0 +1,77 @@
1
+ """Every method the tool can use is cited in the report, and the
2
+ bibliography lists exactly the cited references."""
3
+
4
+ import re
5
+
6
+ from easyclassifier import references as refs
7
+ from easyclassifier.distances import DISTANCES
8
+ from easyclassifier.evaluation import METRICS, SELECTION, VALIDATION
9
+ from easyclassifier.latex_report import build_tex
10
+ from easyclassifier.models import build_registry
11
+
12
+ # Measures with a standard definition that need no source.
13
+ STANDARD_MEASURES = {"accuracy", "precision", "recall", "f1", "specificity",
14
+ "log_loss"}
15
+
16
+
17
+ def test_every_choice_has_its_references():
18
+ assert set(build_registry()) <= set(refs.CLASSIFIER_REFERENCES)
19
+ assert all(refs.CLASSIFIER_REFERENCES[k] for k in build_registry())
20
+ assert set(DISTANCES) == set(refs.DISTANCE_REFERENCES)
21
+ assert set(VALIDATION) == set(refs.VALIDATION_REFERENCES)
22
+ assert set(SELECTION) - {"auto"} == set(refs.SELECTION_REFERENCES)
23
+ assert set(METRICS) == set(refs.METRIC_REFERENCES) | STANDARD_MEASURES
24
+ tables = [refs.CLASSIFIER_REFERENCES, refs.DISTANCE_REFERENCES,
25
+ refs.VALIDATION_REFERENCES, refs.SELECTION_REFERENCES,
26
+ refs.METRIC_REFERENCES, refs.FIGURE_REFERENCES]
27
+ keys = {k for t in tables for v in t.values() for k in v} | set(refs.SOFTWARE)
28
+ assert keys <= set(refs.REFERENCES)
29
+ assert set(refs.REFERENCES) <= set(refs.TOPICS)
30
+ assert not any("Journal of American Science" in r
31
+ for r in refs.REFERENCES.values())
32
+
33
+
34
+ def test_bibliography_lists_exactly_the_cited_references():
35
+ text = r"A \cite{sklearn,breiman2001} B \cite{own} C \cite{breiman2001}."
36
+ bib = refs.bibliography(text, {"own": "Own, A. (2026). Data."})
37
+ assert re.findall(r"\\bibitem\{([^}]*)\}", bib) == \
38
+ ["sklearn", "breiman2001", "own"]
39
+ try:
40
+ refs.bibliography(r"\cite{nowhere}")
41
+ except KeyError as exc:
42
+ assert "nowhere" in str(exc)
43
+ else:
44
+ raise AssertionError("an unknown citation must not pass silently")
45
+ lines = refs.citation_lines(text, {"own": "Own, A. (2026). Data."})
46
+ assert sum(line.startswith("[") for line in lines) == 3
47
+
48
+
49
+ def test_report_cites_each_classifier_measure_and_evaluation():
50
+ from test_report import _context
51
+ c = _context()
52
+ c.classifiers = ["Decision Tree", "XGBoost", "LightGBM",
53
+ "KNN (Canberra, k=5)"]
54
+ c.classifier_keys = ["decision_tree", "xgboost", "lightgbm", "knn"]
55
+ c.knn_text, c.knn_distance = "K-nearest neighbours used k = 5", "canberra"
56
+ c.metrics = ["accuracy", "balanced_accuracy", "mcc", "cohen_kappa"]
57
+ c.selection = "nested"
58
+ c.final = c.results[0]
59
+ doc = build_tex(c)
60
+ cited = refs.cited_keys(doc.split(r"\begin{thebibliography}")[0])
61
+ for key in ("breiman1984", "chen2016", "ke2017", "cover1967", "lance1966",
62
+ "kohavi1995", "varma2006", "brodersen2010", "matthews1975",
63
+ "cohen1960", "sklearn", "numpy", "pandas"):
64
+ assert key in cited, key
65
+ assert re.findall(r"\\bibitem\{([^}]*)\}", doc) == cited
66
+
67
+
68
+ def test_signed_hassanat_form_cites_the_sign_symmetric_paper():
69
+ from test_report import _context
70
+ for signed in (False, True):
71
+ c = _context()
72
+ c.classifiers, c.classifier_keys = ["KNN (Hassanat, k=5)"], ["knn"]
73
+ c.knn_text, c.knn_distance = "K-nearest neighbours used k = 5", "hassanat"
74
+ c.hassanat_used, c.hassanat_signed = True, signed
75
+ cited = refs.cited_keys(build_tex(c))
76
+ assert {"hassanat2022", "hassanat2014"} <= set(cited)
77
+ assert ("alaydaa2026" in cited) == signed
@@ -108,3 +108,24 @@ def test_report_compiles_to_pdf(tmp_path):
108
108
  assert pdf_path is not None, msg
109
109
  assert (tmp_path / "report.pdf").stat().st_size > 10_000
110
110
  assert not (tmp_path / "report.aux").exists() # cleaned up
111
+
112
+
113
+ def test_specificity_is_macro_average():
114
+ """Reported specificity is one-versus-rest specificity averaged over
115
+ classes; with two classes it equals balanced accuracy."""
116
+ import numpy as np
117
+ from sklearn.metrics import balanced_accuracy_score
118
+ from easyclassifier.evaluation import METRICS, _specificity
119
+ from easyclassifier.help_texts import GLOSSARY
120
+
121
+ y_true = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1, 1])
122
+ y_pred = np.array([0, 0, 1, 1, 1, 1, 1, 1, 1, 0])
123
+ assert np.isclose(_specificity(y_true, y_pred, [0, 1]),
124
+ balanced_accuracy_score(y_true, y_pred))
125
+
126
+ y3 = np.array([0, 0, 1, 1, 2, 2])
127
+ p3 = np.array([0, 1, 1, 1, 2, 0])
128
+ # one-vs-rest specificities: class 0: 3/4, class 1: 3/4, class 2: 4/4
129
+ assert np.isclose(_specificity(y3, p3, [0, 1, 2]), (0.75 + 0.75 + 1) / 3)
130
+ assert METRICS["specificity"] == "Macro specificity"
131
+ assert "averaged over all classes" in GLOSSARY["specificity"]
File without changes
File without changes