demetrapy 0.1.0__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.
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: demetrapy
3
+ Version: 0.1.0
4
+ Summary: A Python CLI for seasonal adjustment with JDemetra+ core
5
+ Project-URL: Documentation, https://github.com/SermetPekin/seasonal-pri/blob/main/USAGE.md
6
+ Project-URL: Issues, https://github.com/SermetPekin/seasonal-pri/issues
7
+ Project-URL: Repository, https://github.com/SermetPekin/seasonal-pri
8
+ Keywords: seasonal-adjustment,jdemetra,x13,tramo-seats,time-series
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Financial and Insurance Industry
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: JPype1<2,>=1.5
19
+ Provides-Extra: examples
20
+ Requires-Dist: pandas<4,>=2; extra == "examples"
21
+ Provides-Extra: plots
22
+ Requires-Dist: pandas<4,>=2; extra == "plots"
23
+ Requires-Dist: matplotlib<4,>=3.8; extra == "plots"
24
+ Provides-Extra: dashboard
25
+ Requires-Dist: pandas<4,>=2; extra == "dashboard"
26
+ Requires-Dist: matplotlib<4,>=3.8; extra == "dashboard"
27
+ Requires-Dist: plotly<7,>=6; extra == "dashboard"
28
+ Requires-Dist: streamlit<2,>=1.40; extra == "dashboard"
29
+
30
+ # demetrapy
31
+
32
+ [![CI](https://github.com/SermetPekin/seasonal-pri/actions/workflows/ci.yml/badge.svg)](https://github.com/SermetPekin/seasonal-pri/actions/workflows/ci.yml)
33
+
34
+ A Python command-line interface for seasonal adjustment with
35
+ [JDemetra+ core](https://github.com/jdemetra/jdemetra-core). It calls the real
36
+ X13 and TRAMO/SEATS implementations through JPype and does not require Maven or
37
+ a Demetra+ desktop installation.
38
+
39
+ ## Requirements
40
+
41
+ - Python 3.9 or newer
42
+ - Java 8 or newer
43
+
44
+ ## Install
45
+
46
+ Windows users should follow the [Windows usage guide](WINDOWS_USAGE.md), which
47
+ also covers proxy-restricted and fully offline JAR installation.
48
+
49
+ ```bash
50
+ python -m venv .venv
51
+ source .venv/bin/activate
52
+ python -m pip install -e .
53
+ ```
54
+
55
+ The pinned `demetra-tstoolkit` 2.2.6 JAR is downloaded from Maven Central on
56
+ the first run and cached in `~/.cache/demetrapy`. Set `DEMETRAPY_JAR` to
57
+ use a local JAR instead.
58
+
59
+ ## Use
60
+
61
+ Input is a regular monthly, quarterly, half-yearly, or yearly CSV series:
62
+
63
+ ```csv
64
+ date,value
65
+ 2019-01-01,101.2
66
+ 2019-02-01,103.8
67
+ ```
68
+
69
+ Run with defaults (`Monthly`, `RSA4`):
70
+
71
+ ```bash
72
+ demetrapy input.csv --output adjusted.csv
73
+ ```
74
+
75
+ The equivalent fully named form is:
76
+
77
+ ```bash
78
+ demetrapy --data input.csv --config examples/config.json --output adjusted.csv
79
+ ```
80
+
81
+ Or provide a JSON configuration. The full example includes TRAMO/SEATS,
82
+ calendar effects, user regressors, outliers, interventions, and ramps:
83
+
84
+ ```bash
85
+ demetrapy input.csv --config examples/config.json --output adjusted.csv
86
+ demetrapy input.csv --config examples/full_config.json --output adjusted.csv
87
+ ```
88
+
89
+ See the [usage guide](USAGE.md) for the complete input, configuration, output,
90
+ and Python API reference, or the [Windows usage guide](WINDOWS_USAGE.md) for
91
+ Command Prompt instructions.
92
+
93
+ The supported runtime matrix and result stability policy are documented in
94
+ [COMPATIBILITY.md](COMPATIBILITY.md).
95
+ See [CONFIGURATION.md](CONFIGURATION.md) for processing order, compatible
96
+ option groups, defaults, ARIMA controls, calendars, X11, SEATS, and detailed
97
+ result semantics.
98
+
99
+ The result contains the original (`y`), seasonally adjusted (`sa`), trend
100
+ (`t`), seasonal (`s`), and irregular (`i`) series.
101
+
102
+ Configuration supports X13 and TRAMO/SEATS presets, preprocessing and
103
+ decomposition overrides, built-in and custom calendars, CSV-backed user
104
+ variables, prespecified and automatically detected outliers, interventions,
105
+ ramps, and fixed coefficients.
106
+
107
+ The same engine is available from Python:
108
+
109
+ ```python
110
+ from demetrapy import adjust
111
+
112
+ result = adjust(values, frequency="Monthly", start_year=2019, spec="RSA4")
113
+ seasonally_adjusted = result["sa"]
114
+ ```
115
+
116
+ Opt into the complete JDemetra result dictionary, scalar diagnostics, and
117
+ processing messages with `detailed=True`. Returned time series retain their
118
+ own frequency and starting period, including forecasts and backcasts.
119
+ Detailed results also expose the fitted ARIMA orders and whether automatic
120
+ model selection was used through `result.arima_model`.
121
+ See [automatic_arima_example.py](examples/automatic_arima_example.py) and
122
+ [explicit_arima_example.py](examples/explicit_arima_example.py) for runnable
123
+ examples with both processing engines.
124
+ The [full TRAMO/SEATS UserDefined calendar example](examples/full_tramoseats_user_calendar_example.py)
125
+ combines a separate calendar pool, explicit seasonal ARIMA model, all supported
126
+ TRAMO estimation controls, outlier detection, forecasts, and SEATS options.
127
+ The [quarterly example](examples/quarterly_example.py) demonstrates frequency
128
+ inference and compares X13 with TRAMO/SEATS using a seasonal period of four.
129
+
130
+ ```python
131
+ detailed = adjust(
132
+ values,
133
+ frequency="Monthly",
134
+ start_year=2019,
135
+ forecast_horizon=12,
136
+ detailed=True,
137
+ )
138
+ forecast = detailed.series["final.sa_f"]
139
+ ```
140
+
141
+ See [examples/dataframe_user_variables_example.py](examples/dataframe_user_variables_example.py)
142
+ for a pandas example that keeps observations and a broad user-defined calendar
143
+ pool in separate DataFrames. `adjust_dataframe()` infers their frequency and
144
+ domains, validates coverage, and lets each target select different calendar
145
+ columns using the same semantics as GUI `Trading Days > UserDefined`.
146
+ The [retail operations case study](examples/RETAIL_CASE_STUDY.md) turns that
147
+ example into a reproducible multi-target adjustment and forecasting workflow.
148
+
149
+ To run X13 and TRAMO/SEATS against the same deterministic series, compare every
150
+ compact component, and write aligned results to `method_comparison.csv`:
151
+
152
+ ```bash
153
+ python examples/compare_methods.py
154
+ ```
155
+
156
+ ## Plots and Dashboard
157
+
158
+ Install optional visualization support:
159
+
160
+ ```bash
161
+ python -m pip install -e ".[plots]"
162
+ demetrapy --data input.csv --plot-output adjustment.png
163
+ ```
164
+
165
+ For an interactive local interface with CSV uploads, multi-series controls,
166
+ calendar mappings, Plotly charts, diagnostics, messages, and downloads:
167
+
168
+ ```bash
169
+ python -m pip install -e ".[dashboard]"
170
+ demetrapy-dashboard
171
+ ```
172
+
173
+ ## Test
174
+
175
+ ```bash
176
+ python -m unittest discover -s tests
177
+ ```
178
+
179
+ CI runs the complete suite on Linux, Windows, and macOS with representative
180
+ Python 3.9-3.13 and Java 11/17 combinations.
@@ -0,0 +1,151 @@
1
+ # demetrapy
2
+
3
+ [![CI](https://github.com/SermetPekin/seasonal-pri/actions/workflows/ci.yml/badge.svg)](https://github.com/SermetPekin/seasonal-pri/actions/workflows/ci.yml)
4
+
5
+ A Python command-line interface for seasonal adjustment with
6
+ [JDemetra+ core](https://github.com/jdemetra/jdemetra-core). It calls the real
7
+ X13 and TRAMO/SEATS implementations through JPype and does not require Maven or
8
+ a Demetra+ desktop installation.
9
+
10
+ ## Requirements
11
+
12
+ - Python 3.9 or newer
13
+ - Java 8 or newer
14
+
15
+ ## Install
16
+
17
+ Windows users should follow the [Windows usage guide](WINDOWS_USAGE.md), which
18
+ also covers proxy-restricted and fully offline JAR installation.
19
+
20
+ ```bash
21
+ python -m venv .venv
22
+ source .venv/bin/activate
23
+ python -m pip install -e .
24
+ ```
25
+
26
+ The pinned `demetra-tstoolkit` 2.2.6 JAR is downloaded from Maven Central on
27
+ the first run and cached in `~/.cache/demetrapy`. Set `DEMETRAPY_JAR` to
28
+ use a local JAR instead.
29
+
30
+ ## Use
31
+
32
+ Input is a regular monthly, quarterly, half-yearly, or yearly CSV series:
33
+
34
+ ```csv
35
+ date,value
36
+ 2019-01-01,101.2
37
+ 2019-02-01,103.8
38
+ ```
39
+
40
+ Run with defaults (`Monthly`, `RSA4`):
41
+
42
+ ```bash
43
+ demetrapy input.csv --output adjusted.csv
44
+ ```
45
+
46
+ The equivalent fully named form is:
47
+
48
+ ```bash
49
+ demetrapy --data input.csv --config examples/config.json --output adjusted.csv
50
+ ```
51
+
52
+ Or provide a JSON configuration. The full example includes TRAMO/SEATS,
53
+ calendar effects, user regressors, outliers, interventions, and ramps:
54
+
55
+ ```bash
56
+ demetrapy input.csv --config examples/config.json --output adjusted.csv
57
+ demetrapy input.csv --config examples/full_config.json --output adjusted.csv
58
+ ```
59
+
60
+ See the [usage guide](USAGE.md) for the complete input, configuration, output,
61
+ and Python API reference, or the [Windows usage guide](WINDOWS_USAGE.md) for
62
+ Command Prompt instructions.
63
+
64
+ The supported runtime matrix and result stability policy are documented in
65
+ [COMPATIBILITY.md](COMPATIBILITY.md).
66
+ See [CONFIGURATION.md](CONFIGURATION.md) for processing order, compatible
67
+ option groups, defaults, ARIMA controls, calendars, X11, SEATS, and detailed
68
+ result semantics.
69
+
70
+ The result contains the original (`y`), seasonally adjusted (`sa`), trend
71
+ (`t`), seasonal (`s`), and irregular (`i`) series.
72
+
73
+ Configuration supports X13 and TRAMO/SEATS presets, preprocessing and
74
+ decomposition overrides, built-in and custom calendars, CSV-backed user
75
+ variables, prespecified and automatically detected outliers, interventions,
76
+ ramps, and fixed coefficients.
77
+
78
+ The same engine is available from Python:
79
+
80
+ ```python
81
+ from demetrapy import adjust
82
+
83
+ result = adjust(values, frequency="Monthly", start_year=2019, spec="RSA4")
84
+ seasonally_adjusted = result["sa"]
85
+ ```
86
+
87
+ Opt into the complete JDemetra result dictionary, scalar diagnostics, and
88
+ processing messages with `detailed=True`. Returned time series retain their
89
+ own frequency and starting period, including forecasts and backcasts.
90
+ Detailed results also expose the fitted ARIMA orders and whether automatic
91
+ model selection was used through `result.arima_model`.
92
+ See [automatic_arima_example.py](examples/automatic_arima_example.py) and
93
+ [explicit_arima_example.py](examples/explicit_arima_example.py) for runnable
94
+ examples with both processing engines.
95
+ The [full TRAMO/SEATS UserDefined calendar example](examples/full_tramoseats_user_calendar_example.py)
96
+ combines a separate calendar pool, explicit seasonal ARIMA model, all supported
97
+ TRAMO estimation controls, outlier detection, forecasts, and SEATS options.
98
+ The [quarterly example](examples/quarterly_example.py) demonstrates frequency
99
+ inference and compares X13 with TRAMO/SEATS using a seasonal period of four.
100
+
101
+ ```python
102
+ detailed = adjust(
103
+ values,
104
+ frequency="Monthly",
105
+ start_year=2019,
106
+ forecast_horizon=12,
107
+ detailed=True,
108
+ )
109
+ forecast = detailed.series["final.sa_f"]
110
+ ```
111
+
112
+ See [examples/dataframe_user_variables_example.py](examples/dataframe_user_variables_example.py)
113
+ for a pandas example that keeps observations and a broad user-defined calendar
114
+ pool in separate DataFrames. `adjust_dataframe()` infers their frequency and
115
+ domains, validates coverage, and lets each target select different calendar
116
+ columns using the same semantics as GUI `Trading Days > UserDefined`.
117
+ The [retail operations case study](examples/RETAIL_CASE_STUDY.md) turns that
118
+ example into a reproducible multi-target adjustment and forecasting workflow.
119
+
120
+ To run X13 and TRAMO/SEATS against the same deterministic series, compare every
121
+ compact component, and write aligned results to `method_comparison.csv`:
122
+
123
+ ```bash
124
+ python examples/compare_methods.py
125
+ ```
126
+
127
+ ## Plots and Dashboard
128
+
129
+ Install optional visualization support:
130
+
131
+ ```bash
132
+ python -m pip install -e ".[plots]"
133
+ demetrapy --data input.csv --plot-output adjustment.png
134
+ ```
135
+
136
+ For an interactive local interface with CSV uploads, multi-series controls,
137
+ calendar mappings, Plotly charts, diagnostics, messages, and downloads:
138
+
139
+ ```bash
140
+ python -m pip install -e ".[dashboard]"
141
+ demetrapy-dashboard
142
+ ```
143
+
144
+ ## Test
145
+
146
+ ```bash
147
+ python -m unittest discover -s tests
148
+ ```
149
+
150
+ CI runs the complete suite on Linux, Windows, and macOS with representative
151
+ Python 3.9-3.13 and Java 11/17 combinations.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "demetrapy"
7
+ version = "0.1.0"
8
+ description = "A Python CLI for seasonal adjustment with JDemetra+ core"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = ["JPype1>=1.5,<2"]
12
+ keywords = ["seasonal-adjustment", "jdemetra", "x13", "tramo-seats", "time-series"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Financial and Insurance Industry",
17
+ "Intended Audience :: Science/Research",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Scientific/Engineering :: Information Analysis",
21
+ ]
22
+
23
+ [project.urls]
24
+ Documentation = "https://github.com/SermetPekin/seasonal-pri/blob/main/USAGE.md"
25
+ Issues = "https://github.com/SermetPekin/seasonal-pri/issues"
26
+ Repository = "https://github.com/SermetPekin/seasonal-pri"
27
+
28
+ [project.optional-dependencies]
29
+ examples = ["pandas>=2,<4"]
30
+ plots = ["pandas>=2,<4", "matplotlib>=3.8,<4"]
31
+ dashboard = [
32
+ "pandas>=2,<4",
33
+ "matplotlib>=3.8,<4",
34
+ "plotly>=6,<7",
35
+ "streamlit>=1.40,<2",
36
+ ]
37
+
38
+ [project.scripts]
39
+ demetrapy = "demetrapy.cli:main"
40
+ demetrapy-dashboard = "demetrapy.dashboard:launch"
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ """Python access to JDemetra+ seasonal adjustment."""
2
+
3
+ from .engine import (
4
+ COMPACT_COMPONENTS,
5
+ RESULT_SCHEMA_VERSION,
6
+ AdjustmentResult,
7
+ ArimaModel,
8
+ OutputSeries,
9
+ ProcessingMessage,
10
+ adjust,
11
+ )
12
+ from .dataframe import DataFrameAdjustmentResult, adjust_dataframe
13
+ from .interactive import plot_adjustment_interactive
14
+ from .plotting import plot_adjustment
15
+
16
+ __all__ = [
17
+ "AdjustmentResult",
18
+ "ArimaModel",
19
+ "COMPACT_COMPONENTS",
20
+ "DataFrameAdjustmentResult",
21
+ "OutputSeries",
22
+ "ProcessingMessage",
23
+ "RESULT_SCHEMA_VERSION",
24
+ "adjust",
25
+ "adjust_dataframe",
26
+ "plot_adjustment",
27
+ "plot_adjustment_interactive",
28
+ ]
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import csv
5
+ import sys
6
+ from dataclasses import replace
7
+ from datetime import date
8
+ from pathlib import Path
9
+ from typing import Sequence, TextIO
10
+
11
+ from .config import AdjustmentConfig
12
+ from .engine import COMPACT_COMPONENTS, AdjustmentResult, adjust
13
+ from .plotting import plot_adjustment
14
+
15
+ PERIODS_PER_YEAR = {"Monthly": 12, "Quarterly": 4, "HalfYearly": 2, "Yearly": 1}
16
+
17
+
18
+ def _read_csv(
19
+ path: Path, config: AdjustmentConfig
20
+ ) -> tuple[list[str], list[float], dict[str, list[float]]]:
21
+ with path.open(newline="", encoding="utf-8-sig") as stream:
22
+ reader = csv.DictReader(stream)
23
+ columns = reader.fieldnames or []
24
+ variable_columns = config.user_variable_columns()
25
+ required = {config.date_column, config.value_column, *variable_columns}
26
+ if not required.issubset(columns):
27
+ raise ValueError(f"CSV must contain columns: {', '.join(sorted(required))}")
28
+ dates: list[str] = []
29
+ values: list[float] = []
30
+ user_values = {column: [] for column in variable_columns}
31
+ for row_number, row in enumerate(reader, start=2):
32
+ dates.append(row[config.date_column])
33
+ try:
34
+ values.append(float(row[config.value_column]))
35
+ for column in variable_columns:
36
+ user_values[column].append(float(row[column]))
37
+ except (TypeError, ValueError) as error:
38
+ raise ValueError(f"invalid number on CSV row {row_number}") from error
39
+ if not values:
40
+ raise ValueError("CSV contains no observations")
41
+ return dates, values, user_values
42
+
43
+
44
+ def _start(dates: Sequence[str], frequency: str) -> tuple[int, int]:
45
+ try:
46
+ first = date.fromisoformat(dates[0])
47
+ periods = PERIODS_PER_YEAR[frequency]
48
+ except (ValueError, KeyError) as error:
49
+ raise ValueError(
50
+ "the first date must be ISO YYYY-MM-DD and frequency must be "
51
+ f"one of {', '.join(PERIODS_PER_YEAR)}"
52
+ ) from error
53
+ return first.year, ((first.month - 1) * periods // 12) + 1
54
+
55
+
56
+ def _write_csv(
57
+ stream: TextIO, dates: Sequence[str], result: dict[str, list[float]]
58
+ ) -> None:
59
+ writer = csv.writer(stream)
60
+ names = list(result)
61
+ writer.writerow(["date", *names])
62
+ writer.writerows(
63
+ [current_date, *(result[name][index] for name in names)]
64
+ for index, current_date in enumerate(dates)
65
+ )
66
+
67
+
68
+ def _parser() -> argparse.ArgumentParser:
69
+ parser = argparse.ArgumentParser(
70
+ prog="demetrapy",
71
+ description="Seasonally adjust a regular CSV series with JDemetra+ core.",
72
+ )
73
+ parser.add_argument(
74
+ "input", nargs="?", type=Path, help="CSV containing date and value columns"
75
+ )
76
+ parser.add_argument(
77
+ "-d", "--data", type=Path, help="CSV data file (alternative to positional input)"
78
+ )
79
+ parser.add_argument("-c", "--config", type=Path, help="JSON adjustment configuration")
80
+ parser.add_argument("-o", "--output", type=Path, help="output CSV (default: stdout)")
81
+ parser.add_argument("--method", choices=("x13", "tramoseats"), help="processing method")
82
+ parser.add_argument("--spec", help="JDemetra+ preset, such as RSA4 or RSAfull")
83
+ parser.add_argument(
84
+ "--frequency", choices=tuple(PERIODS_PER_YEAR), help="observation frequency"
85
+ )
86
+ parser.add_argument("--date-column", help="CSV date column")
87
+ parser.add_argument("--value-column", help="CSV value column")
88
+ parser.add_argument("--plot", action="store_true", help="display an overview plot")
89
+ parser.add_argument("--plot-output", type=Path, help="save an overview plot as PNG")
90
+ return parser
91
+
92
+
93
+ def run(argv: Sequence[str] | None = None) -> int:
94
+ args = _parser().parse_args(argv)
95
+ try:
96
+ if args.input and args.data:
97
+ raise ValueError("provide the data file either positionally or with --data, not both")
98
+ input_path = args.data or args.input
99
+ if input_path is None:
100
+ raise ValueError("a data file is required; use --data FILE or a positional path")
101
+ config = AdjustmentConfig.load(args.config)
102
+ overrides = {
103
+ name: getattr(args, name)
104
+ for name in ("method", "spec", "frequency", "date_column", "value_column")
105
+ if getattr(args, name) is not None
106
+ }
107
+ if overrides:
108
+ config = replace(config, **overrides)
109
+ dates, values, user_values = _read_csv(input_path, config)
110
+ start_year, start_period = _start(dates, config.frequency)
111
+ wants_plot = args.plot or args.plot_output is not None
112
+ engine_options = config.engine_options(user_values)
113
+ if wants_plot:
114
+ engine_options["detailed"] = True
115
+ engine_result = adjust(
116
+ values,
117
+ start_year=start_year,
118
+ start_period=start_period,
119
+ **engine_options,
120
+ )
121
+ if isinstance(engine_result, AdjustmentResult):
122
+ result = {
123
+ name: list(engine_result.series[f"final.{name}"].values)
124
+ for name in COMPACT_COMPONENTS
125
+ }
126
+ else:
127
+ result = engine_result
128
+ if any(len(series) != len(dates) for series in result.values()):
129
+ raise RuntimeError("JDemetra+ returned an unexpected output length")
130
+ if args.output:
131
+ with args.output.open("w", newline="", encoding="utf-8") as stream:
132
+ _write_csv(stream, dates, result)
133
+ else:
134
+ _write_csv(sys.stdout, dates, result)
135
+ if wants_plot:
136
+ figure = plot_adjustment(
137
+ engine_result,
138
+ index=dates,
139
+ title=input_path.stem,
140
+ )
141
+ if args.plot_output:
142
+ figure.savefig(args.plot_output, dpi=150)
143
+ if args.plot:
144
+ import matplotlib.pyplot as plt
145
+
146
+ plt.show()
147
+ return 0
148
+ except (ImportError, OSError, ValueError, RuntimeError) as error:
149
+ print(f"demetrapy: {error}", file=sys.stderr)
150
+ return 2
151
+
152
+
153
+ def main() -> None:
154
+ raise SystemExit(run())
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class AdjustmentConfig:
11
+ frequency: str = "Monthly"
12
+ method: str = "x13"
13
+ spec: str = "RSA4"
14
+ date_column: str = "date"
15
+ value_column: str = "value"
16
+ decomposition_mode: str | None = None
17
+ seasonal_filter: str | None = None
18
+ henderson_filter_length: int | None = None
19
+ lower_sigma: float | None = None
20
+ upper_sigma: float | None = None
21
+ forecast_horizon: int | None = None
22
+ backcast_horizon: int | None = None
23
+ benchmarking: bool = False
24
+ calendar: dict[str, Any] | None = None
25
+ user_variables: list[dict[str, Any]] = field(default_factory=list)
26
+ outliers: list[dict[str, Any]] = field(default_factory=list)
27
+ interventions: list[dict[str, Any]] = field(default_factory=list)
28
+ ramps: list[dict[str, Any]] = field(default_factory=list)
29
+ fixed_coefficients: dict[str, float | list[float]] = field(default_factory=dict)
30
+ preprocessing: dict[str, Any] | None = None
31
+ outlier_detection: dict[str, Any] | None = None
32
+ seats: dict[str, Any] | None = None
33
+
34
+ @classmethod
35
+ def load(cls, path: str | Path | None) -> "AdjustmentConfig":
36
+ if path is None:
37
+ return cls()
38
+ with Path(path).open(encoding="utf-8") as stream:
39
+ payload: Any = json.load(stream)
40
+ if not isinstance(payload, dict):
41
+ raise ValueError("config must be a JSON object")
42
+ try:
43
+ return cls(**payload)
44
+ except TypeError as error:
45
+ raise ValueError(f"invalid config: {error}") from error
46
+
47
+ def engine_options(
48
+ self, user_values: dict[str, list[float]] | None = None
49
+ ) -> dict[str, object]:
50
+ options = asdict(self)
51
+ options.pop("date_column")
52
+ options.pop("value_column")
53
+ variables = []
54
+ for definition in options["user_variables"]:
55
+ variable = dict(definition)
56
+ column = variable.pop("column", None)
57
+ if "values" not in variable:
58
+ if column is None or user_values is None or column not in user_values:
59
+ raise ValueError(
60
+ f"user variable '{variable.get('name', '')}' needs a CSV column or values"
61
+ )
62
+ variable["values"] = user_values[column]
63
+ variables.append(variable)
64
+ options["user_variables"] = variables
65
+ return options
66
+
67
+ def user_variable_columns(self) -> list[str]:
68
+ return [
69
+ str(variable["column"])
70
+ for variable in self.user_variables
71
+ if "values" not in variable and "column" in variable
72
+ ]