easyresearch 0.1.0__py3-none-any.whl
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.
- easyresearch/__init__.py +28 -0
- easyresearch/__main__.py +3 -0
- easyresearch/cli.py +165 -0
- easyresearch/common/__init__.py +1 -0
- easyresearch/common/hassanat.py +70 -0
- easyresearch/common/output.py +156 -0
- easyresearch/common/references.py +235 -0
- easyresearch/regression/__init__.py +12 -0
- easyresearch/regression/analysis.py +371 -0
- easyresearch/regression/data.py +114 -0
- easyresearch/regression/evaluation.py +298 -0
- easyresearch/regression/figures.py +190 -0
- easyresearch/regression/models.py +124 -0
- easyresearch/regression/report.py +316 -0
- easyresearch/timeseries/__init__.py +4 -0
- easyresearch/timeseries/classify.py +430 -0
- easyresearch/timeseries/data.py +224 -0
- easyresearch/timeseries/deep.py +208 -0
- easyresearch/timeseries/evaluation.py +242 -0
- easyresearch/timeseries/figures.py +209 -0
- easyresearch/timeseries/forecast.py +302 -0
- easyresearch/timeseries/models.py +257 -0
- easyresearch/timeseries/report.py +268 -0
- easyresearch/timeseries/signal_data.py +146 -0
- easyresearch/timeseries/signal_models.py +379 -0
- easyresearch-0.1.0.dist-info/METADATA +198 -0
- easyresearch-0.1.0.dist-info/RECORD +31 -0
- easyresearch-0.1.0.dist-info/WHEEL +5 -0
- easyresearch-0.1.0.dist-info/entry_points.txt +2 -0
- easyresearch-0.1.0.dist-info/licenses/LICENSE +21 -0
- easyresearch-0.1.0.dist-info/top_level.txt +1 -0
easyresearch/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""EasyResearch - machine learning without programming.
|
|
2
|
+
|
|
3
|
+
Analysis cores that share EasyClassifier's rules (leakage-free preparation,
|
|
4
|
+
default parameters with fixed seeds, honest final scores, plain-language
|
|
5
|
+
reports):
|
|
6
|
+
|
|
7
|
+
* ``easyresearch.regression`` - predict a number from a table
|
|
8
|
+
* ``easyresearch.timeseries`` - forecast a series; classify recordings
|
|
9
|
+
(ECG, EEG, sensor signals)
|
|
10
|
+
* classification is provided by the EasyClassifier package (0.8.2)
|
|
11
|
+
|
|
12
|
+
The cores have no user interaction: settings go in, results and files come
|
|
13
|
+
out, so a terminal wizard, the desktop application or a web server can drive
|
|
14
|
+
them in the same way.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__author__ = "Ahmad Hassanat"
|
|
19
|
+
|
|
20
|
+
CITATION = (
|
|
21
|
+
"Hassanat, A. B. A. (2026). EasyResearch: Machine Learning without "
|
|
22
|
+
"Programming (Version {version}) [Software]. "
|
|
23
|
+
"https://github.com/ahmadhassant/easyresearch".format(version=__version__)
|
|
24
|
+
)
|
|
25
|
+
EASYCLASSIFIER_CITATION = (
|
|
26
|
+
"Hassanat, A. B. A. (2026). EasyClassifier: Machine Learning without "
|
|
27
|
+
"Programming (Version 0.8.2) [Software]. Zenodo. "
|
|
28
|
+
"https://doi.org/10.5281/zenodo.23122901")
|
easyresearch/__main__.py
ADDED
easyresearch/cli.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Command line for the EasyResearch cores (no programming needed beyond one
|
|
2
|
+
command). Examples::
|
|
3
|
+
|
|
4
|
+
easyresearch forecast sales.csv --value Sales --horizon 12
|
|
5
|
+
easyresearch forecast --demo
|
|
6
|
+
easyresearch regress houses.xlsx --target Price
|
|
7
|
+
easyresearch signals ECG200_TRAIN.tsv --label label
|
|
8
|
+
easyresearch models forecast # list the available models
|
|
9
|
+
easyresearch regress --demo --figures residuals,comparison --figure-format png+pdf
|
|
10
|
+
|
|
11
|
+
Results are written to a new folder inside ./Results (or --out).
|
|
12
|
+
A terminal wizard and the desktop/web interfaces will drive the same cores.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _models(arg, registry):
|
|
26
|
+
if not arg:
|
|
27
|
+
return None
|
|
28
|
+
if arg == "all":
|
|
29
|
+
return [k for k, s in registry.items() if getattr(s, "available", True)]
|
|
30
|
+
return [m.strip() for m in arg.split(",") if m.strip()]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _figure_settings(st, a):
|
|
34
|
+
"""Figure colours, file formats and the figures to draw (default: the
|
|
35
|
+
task's usual set)."""
|
|
36
|
+
st.figure_theme, st.figure_format = a.figure_theme, a.figure_format
|
|
37
|
+
if a.figures:
|
|
38
|
+
st.figure_keys = [k.strip() for k in a.figures.split(",") if k.strip()]
|
|
39
|
+
return st
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(argv=None) -> int:
|
|
43
|
+
from easyclassifier.figures import DEFAULT_THEME, FORMATS, THEMES
|
|
44
|
+
p = argparse.ArgumentParser(prog="easyresearch",
|
|
45
|
+
description="Machine learning without "
|
|
46
|
+
"programming - analysis cores.")
|
|
47
|
+
p.add_argument("--version", action="version",
|
|
48
|
+
version=f"EasyResearch {__version__}")
|
|
49
|
+
sub = p.add_subparsers(dest="task", required=True)
|
|
50
|
+
|
|
51
|
+
for name, help_ in (("forecast", "forecast one time series"),
|
|
52
|
+
("regress", "predict a number from a table"),
|
|
53
|
+
("signals", "classify recordings (ECG, EEG, ...)")):
|
|
54
|
+
s = sub.add_parser(name, help=help_)
|
|
55
|
+
s.add_argument("data", nargs="?", help="CSV, Excel or UCR .tsv file")
|
|
56
|
+
s.add_argument("--demo", action="store_true",
|
|
57
|
+
help="use the built-in example data")
|
|
58
|
+
s.add_argument("--models", help="comma-separated model ids, or 'all'"
|
|
59
|
+
" (default: the fast set)")
|
|
60
|
+
s.add_argument("--out", default=".", help="folder for Results/")
|
|
61
|
+
s.add_argument("--no-figures", action="store_true")
|
|
62
|
+
s.add_argument("--figures", help="comma-separated figure names "
|
|
63
|
+
"(default: the usual set; see the report's list)")
|
|
64
|
+
s.add_argument("--figure-theme", default=DEFAULT_THEME,
|
|
65
|
+
choices=list(THEMES), help="figure colours")
|
|
66
|
+
s.add_argument("--figure-format", default="png", choices=list(FORMATS),
|
|
67
|
+
help="figure files: png, png+pdf or png+svg")
|
|
68
|
+
if name == "forecast":
|
|
69
|
+
s.add_argument("--value", help="column to forecast")
|
|
70
|
+
s.add_argument("--time", help="date/time column (auto-detected)")
|
|
71
|
+
s.add_argument("--horizon", default="auto",
|
|
72
|
+
help="steps ahead (default: automatic)")
|
|
73
|
+
s.add_argument("--metric", default="mase",
|
|
74
|
+
choices=["mase", "rmse", "mae", "mhsp"],
|
|
75
|
+
help="measure used to choose the model")
|
|
76
|
+
elif name == "regress":
|
|
77
|
+
s.add_argument("--target", help="numeric column to predict")
|
|
78
|
+
else:
|
|
79
|
+
s.add_argument("--label", help="column holding the class")
|
|
80
|
+
lm = sub.add_parser("models", help="list the models of a task")
|
|
81
|
+
lm.add_argument("which", choices=["forecast", "regress", "signals"])
|
|
82
|
+
a = p.parse_args(argv)
|
|
83
|
+
|
|
84
|
+
if a.task == "models":
|
|
85
|
+
if a.which == "forecast":
|
|
86
|
+
from .timeseries.models import build_registry
|
|
87
|
+
elif a.which == "regress":
|
|
88
|
+
from .regression.models import build_registry
|
|
89
|
+
else:
|
|
90
|
+
from .timeseries.signal_models import build_registry
|
|
91
|
+
for k, s in build_registry().items():
|
|
92
|
+
mark = "*" if getattr(s, "default_selected", False) else " "
|
|
93
|
+
print(f" {mark} {k:20s} {s.name}")
|
|
94
|
+
print("\n * = in the default (fast) set")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
if not a.demo and not a.data:
|
|
98
|
+
p.error("give a data file or --demo")
|
|
99
|
+
out = os.path.abspath(a.out)
|
|
100
|
+
try:
|
|
101
|
+
if a.task == "forecast":
|
|
102
|
+
from .timeseries import ForecastSettings, run_forecast
|
|
103
|
+
from .timeseries import data as td
|
|
104
|
+
from .timeseries.models import build_registry
|
|
105
|
+
df = td.load_demo() if a.demo else _load(a.data)
|
|
106
|
+
value = a.value or ("CO2_ppm" if a.demo else td.suggest_value(df))
|
|
107
|
+
st = ForecastSettings(time_column=a.time, figures=not a.no_figures,
|
|
108
|
+
selection_metric=a.metric,
|
|
109
|
+
horizon=a.horizon if a.horizon == "auto"
|
|
110
|
+
else int(a.horizon))
|
|
111
|
+
st.models = _models(a.models, build_registry()) or st.models
|
|
112
|
+
_figure_settings(st, a)
|
|
113
|
+
r = run_forecast(df, value, st, out,
|
|
114
|
+
td.DEMO_NAME if a.demo else Path(a.data).name,
|
|
115
|
+
"demo_co2" if a.demo else Path(a.data).stem,
|
|
116
|
+
td.DEMO_CITATION if a.demo else "")
|
|
117
|
+
head = f"Selected: {r['best'].name}"
|
|
118
|
+
elif a.task == "regress":
|
|
119
|
+
from .regression import Settings, run_analysis
|
|
120
|
+
from .regression import data as rd
|
|
121
|
+
from .regression.models import build_registry
|
|
122
|
+
df = rd.load_demo() if a.demo else _load(a.data)
|
|
123
|
+
target = a.target or rd.suggest_target(df)
|
|
124
|
+
reg = build_registry()
|
|
125
|
+
models = _models(a.models, reg) or [
|
|
126
|
+
k for k, s in reg.items() if s.default_selected]
|
|
127
|
+
st = _figure_settings(Settings(models=models,
|
|
128
|
+
figures=not a.no_figures), a)
|
|
129
|
+
r = run_analysis(df, target, st, out, rd.DEMO_NAME if a.demo else
|
|
130
|
+
Path(a.data).name, "demo_diabetes" if a.demo
|
|
131
|
+
else Path(a.data).stem,
|
|
132
|
+
rd.DEMO_CITATION if a.demo else "")
|
|
133
|
+
head = f"Selected: {r['best'].name}"
|
|
134
|
+
else:
|
|
135
|
+
from .timeseries import signal_data as sd
|
|
136
|
+
from .timeseries.classify import (SignalSettings,
|
|
137
|
+
run_signal_classification)
|
|
138
|
+
from .timeseries.signal_models import build_registry
|
|
139
|
+
df = sd.load_demo() if a.demo else sd.load_table(a.data)
|
|
140
|
+
label = a.label or sd.suggest_label(df)
|
|
141
|
+
st = SignalSettings(figures=not a.no_figures)
|
|
142
|
+
st.models = _models(a.models, build_registry()) or st.models
|
|
143
|
+
_figure_settings(st, a)
|
|
144
|
+
r = run_signal_classification(
|
|
145
|
+
df, label, st, out, sd.DEMO_NAME if a.demo else
|
|
146
|
+
Path(a.data).name, "demo_heartbeats" if a.demo
|
|
147
|
+
else Path(a.data).stem)
|
|
148
|
+
head = f"Selected: {r['best'].classifier_name}"
|
|
149
|
+
except (ValueError, RuntimeError, FileNotFoundError) as exc:
|
|
150
|
+
print(f"\nCould not run the analysis: {exc}", file=sys.stderr)
|
|
151
|
+
return 1
|
|
152
|
+
print("\n" + head)
|
|
153
|
+
for line in r["summary"]:
|
|
154
|
+
print(" " + line)
|
|
155
|
+
print(f"\nAll results: {r['out']}")
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _load(path):
|
|
160
|
+
from easyclassifier.dataset import clean_path, load_csv
|
|
161
|
+
return load_csv(clean_path(path))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pieces shared by all EasyResearch cores."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Hassanat similarity and the MHSP measure.
|
|
2
|
+
|
|
3
|
+
Hassanat similarity between two values a and b (per dimension of the
|
|
4
|
+
Hassanat distance, which is 1 minus this similarity):
|
|
5
|
+
|
|
6
|
+
if min(a, b) >= 0: S = (1 + min) / (1 + max)
|
|
7
|
+
if min(a, b) < 0: S = (1 + min + |min|) / (1 + max + |min|)
|
|
8
|
+
|
|
9
|
+
S is 1 when a == b and approaches 0 as they move apart, so a single large
|
|
10
|
+
error cannot dominate an average of similarities.
|
|
11
|
+
|
|
12
|
+
MHSP (mean Hassanat similarity percentage) of a set of predictions is
|
|
13
|
+
|
|
14
|
+
MHSP = 100 * mean_i S(actual_i, predicted_i)
|
|
15
|
+
|
|
16
|
+
100% means every prediction equals its actual value; higher is better.
|
|
17
|
+
It is computed on the values as they are (no scaling), as defined in
|
|
18
|
+
Hassanat et al. (2024), Mathematics 12(22), 3623.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
# The paper that introduced MHSP as an outlier-robust accuracy measure for
|
|
26
|
+
# regression; cited wherever MHSP is reported.
|
|
27
|
+
MHSP_CITATION = (
|
|
28
|
+
"Hassanat, A. B., Alqaralleh, M. K., Tarawneh, A. S., Almohammadi, K., "
|
|
29
|
+
"Alamri, M., Alzahrani, A., Altarawneh, G. A., & Alhalaseh, R. (2024). "
|
|
30
|
+
"A novel outlier-robust accuracy measure for machine learning regression "
|
|
31
|
+
"using a non-convex distance metric. Mathematics, 12(22), 3623. "
|
|
32
|
+
"https://doi.org/10.3390/math12223623")
|
|
33
|
+
|
|
34
|
+
# Cited for the Hassanat distance when KNN uses it: the IEEE conference paper
|
|
35
|
+
# that reviews its applications, and the arXiv preprint that introduced it
|
|
36
|
+
# (the same two as EasyClassifier).
|
|
37
|
+
HASSANAT_CITATIONS = [
|
|
38
|
+
"Hassanat, A. B., Alkafaween, E., Tarawneh, A. S., & Elmougy, S. (2022). "
|
|
39
|
+
"Applications review of Hassanat distance metric. In 2022 International "
|
|
40
|
+
"Conference on Emerging Trends in Computing and Engineering Applications "
|
|
41
|
+
"(ETCEA), Karak, Jordan (pp. 1-6). IEEE. "
|
|
42
|
+
"https://doi.org/10.1109/ETCEA57049.2022.10009844",
|
|
43
|
+
"Hassanat, A. B. (2014). Dimensionality invariant similarity measure. "
|
|
44
|
+
"arXiv preprint arXiv:1409.0923. https://arxiv.org/abs/1409.0923",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def hassanat_similarity(a, b) -> np.ndarray:
|
|
49
|
+
"""Element-wise Hassanat similarity in [0, 1]."""
|
|
50
|
+
a = np.asarray(a, dtype=float)
|
|
51
|
+
b = np.asarray(b, dtype=float)
|
|
52
|
+
mn = np.minimum(a, b)
|
|
53
|
+
mx = np.maximum(a, b)
|
|
54
|
+
shift = np.where(mn < 0, -mn, 0.0)
|
|
55
|
+
return (1.0 + mn + shift) / (1.0 + mx + shift)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def mhsp(actual, predicted) -> float:
|
|
59
|
+
"""Mean Hassanat similarity percentage (0-100, higher is better)."""
|
|
60
|
+
return float(100.0 * np.mean(hassanat_similarity(actual, predicted)))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
MHSP_EXPLANATION = (
|
|
64
|
+
"MHSP (mean Hassanat similarity percentage): for each prediction, the "
|
|
65
|
+
"Hassanat similarity (1 + smaller value) / (1 + larger value) between "
|
|
66
|
+
"the actual and predicted value, averaged and shown as a percentage. "
|
|
67
|
+
"100% means perfect predictions. Each prediction contributes at most "
|
|
68
|
+
"its share, so one very large error cannot dominate it. It depends on "
|
|
69
|
+
"the units of the values: errors on large values count as relatively "
|
|
70
|
+
"small.")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Small helpers shared by the cores: number formatting, output folders,
|
|
2
|
+
LaTeX compilation, and the record of settings and software versions that
|
|
3
|
+
every run folder contains."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
import datetime as _dt
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import sys
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import Dict, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from easyclassifier.latex_report import find_engine
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fmt(v: float) -> str:
|
|
20
|
+
"""A number with sensible precision for any unit."""
|
|
21
|
+
a = abs(v)
|
|
22
|
+
if a >= 1000:
|
|
23
|
+
return f"{v:,.0f}"
|
|
24
|
+
if a >= 1:
|
|
25
|
+
return f"{v:.2f}"
|
|
26
|
+
return f"{v:.3g}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def results_folder(out_root: str, source_stem: str) -> str:
|
|
30
|
+
"""A new folder for every run: <out_root>/Results/<data>_<date>_<time>."""
|
|
31
|
+
stamp = _dt.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
32
|
+
safe = "".join(ch if ch.isalnum() or ch in "-_" else "_"
|
|
33
|
+
for ch in source_stem)[:40] or "data"
|
|
34
|
+
out = os.path.join(out_root, "Results", f"{safe}_{stamp}")
|
|
35
|
+
os.makedirs(os.path.join(out, "figures"), exist_ok=True)
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compile_tex(tex_text: str, out_dir: str,
|
|
40
|
+
timeout: int = 180) -> Tuple[str, Optional[str], str]:
|
|
41
|
+
"""Write report.tex and compile it to report.pdf when LaTeX is found."""
|
|
42
|
+
tex_path = os.path.join(out_dir, "report.tex")
|
|
43
|
+
with open(tex_path, "w", encoding="utf-8") as fh:
|
|
44
|
+
fh.write(tex_text)
|
|
45
|
+
engine = find_engine()
|
|
46
|
+
if engine is None:
|
|
47
|
+
return tex_path, None, (
|
|
48
|
+
"No LaTeX program was found, so only report.tex was written. "
|
|
49
|
+
"Upload it with the 'figures' folder to an online editor such as "
|
|
50
|
+
"Overleaf, or install MiKTeX to get report.pdf.")
|
|
51
|
+
pdf_path = os.path.join(out_dir, "report.pdf")
|
|
52
|
+
if os.path.exists(pdf_path):
|
|
53
|
+
os.remove(pdf_path)
|
|
54
|
+
runs = ([["tectonic", "report.tex"]] if engine == "tectonic" else
|
|
55
|
+
[[engine, "-interaction=nonstopmode", "-halt-on-error",
|
|
56
|
+
"report.tex"]] * 2)
|
|
57
|
+
proc = None
|
|
58
|
+
try:
|
|
59
|
+
for run in runs:
|
|
60
|
+
proc = subprocess.run(run, cwd=out_dir, capture_output=True,
|
|
61
|
+
timeout=timeout)
|
|
62
|
+
if proc.returncode != 0:
|
|
63
|
+
break
|
|
64
|
+
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
65
|
+
return tex_path, None, f"LaTeX ({engine}) did not finish: {exc}."
|
|
66
|
+
finally:
|
|
67
|
+
for ext in (".aux", ".out", ".toc"):
|
|
68
|
+
p = os.path.join(out_dir, "report" + ext)
|
|
69
|
+
if os.path.exists(p):
|
|
70
|
+
os.remove(p)
|
|
71
|
+
log = os.path.join(out_dir, "report.log")
|
|
72
|
+
if proc is not None and proc.returncode == 0 and os.path.exists(pdf_path):
|
|
73
|
+
if os.path.exists(log):
|
|
74
|
+
os.remove(log)
|
|
75
|
+
return tex_path, pdf_path, f"PDF created with {engine}."
|
|
76
|
+
if os.path.exists(log):
|
|
77
|
+
os.replace(log, os.path.join(out_dir, "report_latex_errors.log"))
|
|
78
|
+
return tex_path, None, (f"LaTeX ({engine}) could not build the PDF; see "
|
|
79
|
+
"report_latex_errors.log. report.tex can still be "
|
|
80
|
+
"opened in Overleaf.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
VERSION_PACKAGES = ("easyresearch", "easyclassifier", "numpy", "pandas",
|
|
84
|
+
"scikit-learn", "scipy", "matplotlib", "joblib",
|
|
85
|
+
"statsmodels", "torch", "openpyxl", "xgboost", "lightgbm")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def software_versions() -> Dict[str, str]:
|
|
89
|
+
"""Python, operating system and the installed analysis packages."""
|
|
90
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
91
|
+
found = {"Python": platform.python_version(),
|
|
92
|
+
"Operating system": f"{platform.system()} {platform.release()} "
|
|
93
|
+
f"({platform.machine()})"}
|
|
94
|
+
for name in VERSION_PACKAGES:
|
|
95
|
+
try:
|
|
96
|
+
found[name] = version(name)
|
|
97
|
+
except PackageNotFoundError:
|
|
98
|
+
pass
|
|
99
|
+
return found
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_run_record(out_dir: str, task: str, data: str, column: str,
|
|
103
|
+
settings, resolved: Dict) -> None:
|
|
104
|
+
"""settings.json (what was asked and what was used) and versions.txt
|
|
105
|
+
(the software that produced the results), so a run can be repeated and
|
|
106
|
+
reported exactly."""
|
|
107
|
+
asked = (dataclasses.asdict(settings) if dataclasses.is_dataclass(settings)
|
|
108
|
+
else dict(settings))
|
|
109
|
+
record = {"task": task, "data": data, "column": column,
|
|
110
|
+
"settings": asked, "used": resolved,
|
|
111
|
+
"created": _dt.datetime.now().isoformat(timespec="seconds")}
|
|
112
|
+
with open(os.path.join(out_dir, "settings.json"), "w",
|
|
113
|
+
encoding="utf-8") as fh:
|
|
114
|
+
json.dump(record, fh, ensure_ascii=False, indent=2, default=str)
|
|
115
|
+
lines = [f"{k}: {v}" for k, v in software_versions().items()]
|
|
116
|
+
with open(os.path.join(out_dir, "versions.txt"), "w",
|
|
117
|
+
encoding="utf-8") as fh:
|
|
118
|
+
fh.write("Software used for these results\n\n" + "\n".join(lines)
|
|
119
|
+
+ f"\n\nPython executable: {sys.executable}\n")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def figure_style(settings, known) -> Tuple[dict, Optional[list]]:
|
|
123
|
+
"""The figure theme and file formats chosen in the settings (as keyword
|
|
124
|
+
arguments for the figure maker), and the figures to draw (None = the
|
|
125
|
+
task's usual set). Unknown choices are refused in plain words."""
|
|
126
|
+
from easyclassifier.figures import FORMATS, THEMES
|
|
127
|
+
theme = getattr(settings, "figure_theme", "colorblind")
|
|
128
|
+
formats = getattr(settings, "figure_format", "png")
|
|
129
|
+
keys = getattr(settings, "figure_keys", None)
|
|
130
|
+
if theme not in THEMES:
|
|
131
|
+
raise ValueError(f"Unknown figure theme '{theme}'; choose one of: "
|
|
132
|
+
+ ", ".join(THEMES) + ".")
|
|
133
|
+
if formats not in FORMATS:
|
|
134
|
+
raise ValueError(f"Unknown figure format '{formats}'; choose one of: "
|
|
135
|
+
+ ", ".join(FORMATS) + ".")
|
|
136
|
+
if keys is not None:
|
|
137
|
+
unknown = [k for k in keys if k not in known]
|
|
138
|
+
if unknown:
|
|
139
|
+
raise ValueError("Unknown figure(s): " + ", ".join(unknown) + ".")
|
|
140
|
+
keys = [k for k in known if k in keys] # report order
|
|
141
|
+
return dict(theme=theme, formats=formats), keys
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def save_model(obj, path: str) -> Optional[str]:
|
|
145
|
+
"""Save the trained model; on failure, remove the partial file and return
|
|
146
|
+
a plain message instead of stopping the analysis (all other results are
|
|
147
|
+
still saved)."""
|
|
148
|
+
import joblib
|
|
149
|
+
try:
|
|
150
|
+
joblib.dump(obj, path)
|
|
151
|
+
return None
|
|
152
|
+
except Exception as exc: # noqa: BLE001
|
|
153
|
+
if os.path.exists(path):
|
|
154
|
+
os.remove(path)
|
|
155
|
+
return (f"The trained model could not be saved ({type(exc).__name__}: "
|
|
156
|
+
f"{exc}); all other results were saved.")
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""References for every method the EasyResearch cores can use.
|
|
2
|
+
|
|
3
|
+
Extends EasyClassifier's reference list (classifiers, distances, evaluation,
|
|
4
|
+
measures, software) with the models and methods of regression, forecasting
|
|
5
|
+
and signal classification. Each report cites the methods it used; the
|
|
6
|
+
bibliography and ``citations.txt`` are generated from those citations (see
|
|
7
|
+
``easyclassifier.references``), so a reference is listed exactly when it is
|
|
8
|
+
cited.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from easyclassifier.references import (
|
|
16
|
+
CLASSIFIER_REFERENCES,
|
|
17
|
+
FIGURE_REFERENCES,
|
|
18
|
+
HASSANAT_SIGNED_REFERENCES,
|
|
19
|
+
METRIC_REFERENCES,
|
|
20
|
+
REFERENCES as _CLASSIFICATION_REFERENCES,
|
|
21
|
+
SELECTION_REFERENCES,
|
|
22
|
+
TOPICS as _CLASSIFICATION_TOPICS,
|
|
23
|
+
VALIDATION_REFERENCES,
|
|
24
|
+
cite,
|
|
25
|
+
cited_keys,
|
|
26
|
+
)
|
|
27
|
+
from easyclassifier import references as _base
|
|
28
|
+
|
|
29
|
+
from .hassanat import MHSP_CITATION
|
|
30
|
+
|
|
31
|
+
__all__ = ["REFERENCES", "TOPICS", "CLASSIFIER_REFERENCES", "FIGURE_REFERENCES",
|
|
32
|
+
"HASSANAT_SIGNED_REFERENCES",
|
|
33
|
+
"METRIC_REFERENCES", "SELECTION_REFERENCES",
|
|
34
|
+
"VALIDATION_REFERENCES", "REGRESSION_MODEL_REFERENCES",
|
|
35
|
+
"FORECAST_MODEL_REFERENCES", "SIGNAL_MODEL_REFERENCES",
|
|
36
|
+
"bibliography", "citation_lines", "cite", "cited_keys",
|
|
37
|
+
"model_cites"]
|
|
38
|
+
|
|
39
|
+
REFERENCES: Dict[str, str] = dict(_CLASSIFICATION_REFERENCES)
|
|
40
|
+
REFERENCES.update({
|
|
41
|
+
"mhsp": MHSP_CITATION,
|
|
42
|
+
# Software
|
|
43
|
+
"statsmodels": "Seabold, S., & Perktold, J. (2010). Statsmodels: "
|
|
44
|
+
"Econometric and statistical modeling with Python. "
|
|
45
|
+
"Proceedings of the 9th Python in Science Conference, "
|
|
46
|
+
"92-96.",
|
|
47
|
+
"pytorch": "Paszke, A., et al. (2019). PyTorch: An imperative style, "
|
|
48
|
+
"high-performance deep learning library. NeurIPS 2019, "
|
|
49
|
+
"8024-8035.",
|
|
50
|
+
# Regression models
|
|
51
|
+
"hastie2009": "Hastie, T., Tibshirani, R., & Friedman, J. (2009). The "
|
|
52
|
+
"Elements of Statistical Learning: Data Mining, Inference, "
|
|
53
|
+
"and Prediction (2nd ed.). Springer. "
|
|
54
|
+
"https://doi.org/10.1007/978-0-387-84858-7",
|
|
55
|
+
"hoerl1970": "Hoerl, A. E., & Kennard, R. W. (1970). Ridge regression: "
|
|
56
|
+
"Biased estimation for nonorthogonal problems. "
|
|
57
|
+
"Technometrics, 12(1), 55-67. "
|
|
58
|
+
"https://doi.org/10.1080/00401706.1970.10488634",
|
|
59
|
+
"friedman2001": "Friedman, J. H. (2001). Greedy function approximation: "
|
|
60
|
+
"A gradient boosting machine. The Annals of Statistics, "
|
|
61
|
+
"29(5), 1189-1232. "
|
|
62
|
+
"https://doi.org/10.1214/aos/1013203451",
|
|
63
|
+
"drucker1997": "Drucker, H., Burges, C. J. C., Kaufman, L., Smola, A., & "
|
|
64
|
+
"Vapnik, V. (1997). Support vector regression machines. "
|
|
65
|
+
"In Advances in Neural Information Processing Systems 9 "
|
|
66
|
+
"(pp. 155-161). MIT Press.",
|
|
67
|
+
"altman1992": "Altman, N. S. (1992). An introduction to kernel and "
|
|
68
|
+
"nearest-neighbor nonparametric regression. The American "
|
|
69
|
+
"Statistician, 46(3), 175-185. "
|
|
70
|
+
"https://doi.org/10.1080/00031305.1992.10475879",
|
|
71
|
+
# Forecasting
|
|
72
|
+
"hyndman2021": "Hyndman, R. J., & Athanasopoulos, G. (2021). "
|
|
73
|
+
"Forecasting: Principles and Practice (3rd ed.). OTexts. "
|
|
74
|
+
"https://otexts.com/fpp3/",
|
|
75
|
+
"hyndman2002": "Hyndman, R. J., Koehler, A. B., Snyder, R. D., & Grose, "
|
|
76
|
+
"S. (2002). A state space framework for automatic "
|
|
77
|
+
"forecasting using exponential smoothing methods. "
|
|
78
|
+
"International Journal of Forecasting, 18(3), 439-454.",
|
|
79
|
+
"assimakopoulos2000": "Assimakopoulos, V., & Nikolopoulos, K. (2000). "
|
|
80
|
+
"The theta model: a decomposition approach to "
|
|
81
|
+
"forecasting. International Journal of "
|
|
82
|
+
"Forecasting, 16(4), 521-530.",
|
|
83
|
+
"bontempi2013": "Bontempi, G., Ben Taieb, S., & Le Borgne, Y.-A. (2013). "
|
|
84
|
+
"Machine learning strategies for time series "
|
|
85
|
+
"forecasting. In M.-A. Aufaure & E. Zimányi (Eds.), "
|
|
86
|
+
"Business Intelligence (eBISS 2012), Lecture Notes in "
|
|
87
|
+
"Business Information Processing 138 (pp. 62-77). "
|
|
88
|
+
"Springer. https://doi.org/10.1007/978-3-642-36318-4_3",
|
|
89
|
+
"tashman2000": "Tashman, L. J. (2000). Out-of-sample tests of "
|
|
90
|
+
"forecasting accuracy: an analysis and review. "
|
|
91
|
+
"International Journal of Forecasting, 16(4), 437-450.",
|
|
92
|
+
"hyndman2006": "Hyndman, R. J., & Koehler, A. B. (2006). Another look at "
|
|
93
|
+
"measures of forecast accuracy. International Journal of "
|
|
94
|
+
"Forecasting, 22(4), 679-688.",
|
|
95
|
+
"makridakis1993": "Makridakis, S. (1993). Accuracy measures: theoretical "
|
|
96
|
+
"and practical concerns. International Journal of "
|
|
97
|
+
"Forecasting, 9(4), 527-529. "
|
|
98
|
+
"https://doi.org/10.1016/0169-2070(93)90079-3",
|
|
99
|
+
"cleveland1990": "Cleveland, R. B., Cleveland, W. S., McRae, J. E., & "
|
|
100
|
+
"Terpenning, I. (1990). STL: A seasonal-trend "
|
|
101
|
+
"decomposition procedure based on loess. Journal of "
|
|
102
|
+
"Official Statistics, 6(1), 3-73.",
|
|
103
|
+
# Deep networks
|
|
104
|
+
"hochreiter1997": "Hochreiter, S., & Schmidhuber, J. (1997). Long "
|
|
105
|
+
"short-term memory. Neural Computation, 9(8), "
|
|
106
|
+
"1735-1780.",
|
|
107
|
+
"cho2014": "Cho, K., et al. (2014). Learning phrase representations "
|
|
108
|
+
"using RNN encoder-decoder for statistical machine "
|
|
109
|
+
"translation. EMNLP 2014, 1724-1734.",
|
|
110
|
+
"bai2018": "Bai, S., Kolter, J. Z., & Koltun, V. (2018). An empirical "
|
|
111
|
+
"evaluation of generic convolutional and recurrent networks "
|
|
112
|
+
"for sequence modeling. arXiv:1803.01271.",
|
|
113
|
+
"oreshkin2020": "Oreshkin, B. N., Carpov, D., Chapados, N., & Bengio, Y. "
|
|
114
|
+
"(2020). N-BEATS: Neural basis expansion analysis for "
|
|
115
|
+
"interpretable time series forecasting. ICLR 2020.",
|
|
116
|
+
"vaswani2017": "Vaswani, A., et al. (2017). Attention is all you need. "
|
|
117
|
+
"NeurIPS 2017, 5998-6008.",
|
|
118
|
+
# Signal classification
|
|
119
|
+
"dempster2020": "Dempster, A., Petitjean, F., & Webb, G. I. (2020). "
|
|
120
|
+
"ROCKET: exceptionally fast and accurate time series "
|
|
121
|
+
"classification using random convolutional kernels. "
|
|
122
|
+
"Data Mining and Knowledge Discovery, 34(5), 1454-1495.",
|
|
123
|
+
"fulcher2014": "Fulcher, B. D., & Jones, N. S. (2014). Highly "
|
|
124
|
+
"comparative feature-based time-series classification. "
|
|
125
|
+
"IEEE Transactions on Knowledge and Data Engineering, "
|
|
126
|
+
"26(12), 3026-3037. "
|
|
127
|
+
"https://doi.org/10.1109/TKDE.2014.2316504",
|
|
128
|
+
"wang2017": "Wang, Z., Yan, W., & Oates, T. (2017). Time series "
|
|
129
|
+
"classification from scratch with deep neural networks: a "
|
|
130
|
+
"strong baseline. IJCNN 2017, 1578-1585.",
|
|
131
|
+
"he2016": "He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual "
|
|
132
|
+
"learning for image recognition. In Proceedings of the IEEE "
|
|
133
|
+
"Conference on Computer Vision and Pattern Recognition (CVPR) "
|
|
134
|
+
"(pp. 770-778). https://doi.org/10.1109/CVPR.2016.90",
|
|
135
|
+
"ismailfawaz2020": "Ismail Fawaz, H., et al. (2020). InceptionTime: "
|
|
136
|
+
"finding AlexNet for time series classification. "
|
|
137
|
+
"Data Mining and Knowledge Discovery, 34(6), "
|
|
138
|
+
"1936-1962.",
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
TOPICS: Dict[str, str] = dict(_CLASSIFICATION_TOPICS)
|
|
142
|
+
TOPICS.update({
|
|
143
|
+
"software": "EasyResearch (this software)",
|
|
144
|
+
"easyclassifier": "EasyClassifier (data preparation and evaluation)",
|
|
145
|
+
"dataset": "The example dataset",
|
|
146
|
+
"mhsp": "Mean Hassanat similarity percentage (MHSP)",
|
|
147
|
+
"statsmodels": "statsmodels (statistical forecasting models)",
|
|
148
|
+
"pytorch": "PyTorch (deep networks)",
|
|
149
|
+
"hastie2009": "Linear regression (least squares)",
|
|
150
|
+
"hoerl1970": "Ridge regression",
|
|
151
|
+
"friedman2001": "Gradient boosting",
|
|
152
|
+
"drucker1997": "Support vector regression",
|
|
153
|
+
"altman1992": "K-nearest-neighbour regression",
|
|
154
|
+
"hyndman2021": "Naive and seasonal naive forecasts; prediction intervals",
|
|
155
|
+
"hyndman2002": "Exponential smoothing (ETS)",
|
|
156
|
+
"assimakopoulos2000": "Theta method",
|
|
157
|
+
"bontempi2013": "Machine-learning forecasting from past values "
|
|
158
|
+
"(direct multi-step strategy)",
|
|
159
|
+
"tashman2000": "Rolling-origin evaluation",
|
|
160
|
+
"hyndman2006": "MASE",
|
|
161
|
+
"makridakis1993": "sMAPE",
|
|
162
|
+
"cleveland1990": "STL decomposition",
|
|
163
|
+
"hochreiter1997": "LSTM",
|
|
164
|
+
"cho2014": "GRU",
|
|
165
|
+
"bai2018": "Temporal convolutional network (TCN)",
|
|
166
|
+
"oreshkin2020": "N-BEATS",
|
|
167
|
+
"vaswani2017": "Transformer",
|
|
168
|
+
"dempster2020": "ROCKET",
|
|
169
|
+
"fulcher2014": "Feature-based time-series classification",
|
|
170
|
+
"wang2017": "FCN and ResNet for time-series classification",
|
|
171
|
+
"he2016": "Residual networks (ResNet)",
|
|
172
|
+
"ismailfawaz2020": "InceptionTime",
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
HASSANAT = ["hassanat2022", "hassanat2014"]
|
|
176
|
+
|
|
177
|
+
REGRESSION_MODEL_REFERENCES: Dict[str, List[str]] = {
|
|
178
|
+
"linear_regression": ["hastie2009"],
|
|
179
|
+
"ridge": ["hoerl1970"],
|
|
180
|
+
"decision_tree": ["breiman1984"],
|
|
181
|
+
"random_forest": ["breiman2001"],
|
|
182
|
+
# scikit-learn's histogram-based gradient boosting follows LightGBM.
|
|
183
|
+
"gradient_boosting": ["friedman2001", "ke2017"],
|
|
184
|
+
"svr": ["drucker1997"],
|
|
185
|
+
"knn": ["altman1992"] + HASSANAT,
|
|
186
|
+
"neural_network": ["rumelhart1986", "kingma2015"],
|
|
187
|
+
"xgboost": ["chen2016"],
|
|
188
|
+
"lightgbm": ["ke2017"],
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
FORECAST_MODEL_REFERENCES: Dict[str, List[str]] = {
|
|
192
|
+
"naive": ["hyndman2021"],
|
|
193
|
+
"seasonal_naive": ["hyndman2021"],
|
|
194
|
+
"ets": ["hyndman2002"],
|
|
195
|
+
"theta": ["assimakopoulos2000"],
|
|
196
|
+
"ridge": ["hoerl1970"],
|
|
197
|
+
"random_forest": ["breiman2001"],
|
|
198
|
+
"gradient_boosting": ["friedman2001", "ke2017"],
|
|
199
|
+
"knn": ["altman1992"] + HASSANAT,
|
|
200
|
+
"mlp": ["rumelhart1986"],
|
|
201
|
+
"lstm": ["hochreiter1997"],
|
|
202
|
+
"gru": ["cho2014"],
|
|
203
|
+
"tcn": ["bai2018"],
|
|
204
|
+
"nbeats": ["oreshkin2020"],
|
|
205
|
+
"transformer": ["vaswani2017"],
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
SIGNAL_MODEL_REFERENCES: Dict[str, List[str]] = {
|
|
209
|
+
"majority": [],
|
|
210
|
+
"knn": ["cover1967"] + HASSANAT,
|
|
211
|
+
"rocket": ["dempster2020"],
|
|
212
|
+
"features_rf": ["fulcher2014", "breiman2001"],
|
|
213
|
+
"fcn": ["wang2017"],
|
|
214
|
+
"resnet": ["wang2017", "he2016"],
|
|
215
|
+
"inception": ["ismailfawaz2020"],
|
|
216
|
+
"lstm": ["hochreiter1997"],
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def model_cites(keys, table: Dict[str, List[str]]) -> List[str]:
|
|
221
|
+
"""All references of the given models, in order, without repeats."""
|
|
222
|
+
return list(dict.fromkeys(r for k in keys for r in table.get(k, [])))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def bibliography(text: str, extra: Optional[Dict[str, str]] = None,
|
|
226
|
+
escape=lambda s: s) -> str:
|
|
227
|
+
"""As in EasyClassifier, with the references of all EasyResearch cores
|
|
228
|
+
(``extra`` holds the report's own entries: software, dataset)."""
|
|
229
|
+
return _base.bibliography(text, dict(REFERENCES, **(extra or {})), escape)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def citation_lines(text: str, extra: Optional[Dict[str, str]] = None,
|
|
233
|
+
topics: Optional[Dict[str, str]] = None) -> List[str]:
|
|
234
|
+
return _base.citation_lines(text, dict(REFERENCES, **(extra or {})),
|
|
235
|
+
dict(TOPICS, **(topics or {})))
|