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.
- demetrapy-0.1.0/PKG-INFO +180 -0
- demetrapy-0.1.0/README.md +151 -0
- demetrapy-0.1.0/pyproject.toml +43 -0
- demetrapy-0.1.0/setup.cfg +4 -0
- demetrapy-0.1.0/src/demetrapy/__init__.py +28 -0
- demetrapy-0.1.0/src/demetrapy/cli.py +154 -0
- demetrapy-0.1.0/src/demetrapy/config.py +72 -0
- demetrapy-0.1.0/src/demetrapy/dashboard.py +425 -0
- demetrapy-0.1.0/src/demetrapy/dataframe.py +239 -0
- demetrapy-0.1.0/src/demetrapy/engine.py +893 -0
- demetrapy-0.1.0/src/demetrapy/interactive.py +144 -0
- demetrapy-0.1.0/src/demetrapy/plotting.py +142 -0
- demetrapy-0.1.0/src/demetrapy.egg-info/PKG-INFO +180 -0
- demetrapy-0.1.0/src/demetrapy.egg-info/SOURCES.txt +23 -0
- demetrapy-0.1.0/src/demetrapy.egg-info/dependency_links.txt +1 -0
- demetrapy-0.1.0/src/demetrapy.egg-info/entry_points.txt +3 -0
- demetrapy-0.1.0/src/demetrapy.egg-info/requires.txt +14 -0
- demetrapy-0.1.0/src/demetrapy.egg-info/top_level.txt +1 -0
- demetrapy-0.1.0/tests/test_cli.py +170 -0
- demetrapy-0.1.0/tests/test_dashboard.py +74 -0
- demetrapy-0.1.0/tests/test_dataframe.py +156 -0
- demetrapy-0.1.0/tests/test_engine.py +278 -0
- demetrapy-0.1.0/tests/test_interactive.py +47 -0
- demetrapy-0.1.0/tests/test_plotting.py +74 -0
- demetrapy-0.1.0/tests/test_synthetic_properties.py +170 -0
demetrapy-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](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
|
+
[](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,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
|
+
]
|