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.
@@ -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")
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
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 {})))