peltwtcn
A faithful Python implementation of the PELT → wavelet → deep-learning carbon price forecasting framework, with the replication audit the paper needs.
Implements
Ren, R., Li, J., Li, Y., Huang, S., Shen, J., Li, W., Le, J. and Wang, S. (2025) "A Hybrid Deep Learning based Carbon Price Forecasting Framework with Structural Breakpoints Detection and Signal Denoising", arXiv:2511.04988.
Author of this package: Dr Merwan Roudane — merwanroudane920@gmail.com — github.com/merwanroudane
Everything is built on the real EU ETS dataset — 6,113 daily EUA observations from 2007-09-10 to 2024-06-04, matching the paper exactly. Nothing here is simulated.
Table of contents
- What this package does
- Installation
- Sixty-second quick start
- The three protocols
- Results
- What the replication found
- The real dataset
- Module map
- Documentation
- Testing
- Citation
- Author
1. What this package does
The paper's pipeline, end to end, in one call — plus the diagnostics it omits.
- Three structural break detectors — PELT (Killick et al., 2012), ICSS (Inclán & Tiao, 1994), Bai–Perron (2003), and the combined BP&ICSS baseline of Lin & Zhang (2022).
- Wavelet denoising in the paper's two-sided form and a leakage-free rolling form.
- LSTM, GRU and TCN in Keras, with the paper's exact hyper-parameters. The TCN's dilated causal convolution is written from scratch so it runs on a CPU and is verified for strict causality.
- Live data loaders for the EUA price and its drivers, with a bundled cache.
- Publication-quality tables and figures — LaTeX/booktabs, Markdown, HTML, CSV, Excel; every figure of the paper at 300 dpi.
- Diebold–Mariano and Model Confidence Set tests, plus a random-walk benchmark and Theil's U. The paper reports none of these.
- The published Table 1 as a checkable constant, so any run can be diffed against it.
2. Installation
git clone https://github.com/merwanroudane/peltwtcn.git
cd peltwtcn
pip install -e ".[all]"
Extras: [deep] TensorFlow · [data] yfinance + openpyxl · [plots] seaborn ·
[test] pytest. Without TensorFlow everything except the three networks still
works.
Requires Python ≥ 3.9.
3. Sixty-second quick start
import peltwtcn as pw
# 1. the real EUA carbon price plus 13 exogenous drivers
df = pw.load_paper_dataset()
print(df.shape) # (6113, 14)
# 2. the paper's best model, in a configuration that works out of sample
pipe = pw.PELTWTPipeline(model="tcn", stationary=True).fit(df)
print(pipe.summary())
# 3. all five specifications of Table 1
res = pw.run_experiment(df, stationary=True)
print(pw.results_table(res.table, fmt="markdown"))
# 4. how does it compare with the published numbers?
print(pw.compare_with_paper(res.table))
# 5. every figure, at 300 dpi
pw.set_journal_style()
pw.save_all_figures(res, df, outdir="assets")
The full replication, all three protocols, tables and figures:
python examples/run_full_replication.py
4. The three protocols
The paper's specification does not survive an honest chronological split. Rather than quietly fix it, this package implements all three variants and reports them side by side.
| Protocol | Call | What it is |
|---|---|---|
| A level | mode="paper", stationary=False |
The faithful replication — the paper's specification, literally. Does not work out of sample. |
| B stationary | mode="paper", stationary=True |
Same models, same hyper-parameters, applied to the first difference. Levels rebuilt as last value + predicted change, so metrics stay in EUR. |
| C causal | mode="causal", stationary=True |
Adds a causal wavelet, a training-only scaler and a raw target. The only protocol whose numbers are an honest out-of-sample claim. |
mode="causal" forces the leak-free settings, so you cannot leave one in by
accident.
5. Results
Protocol B on the real dataset, the paper's own settings (50 epochs, early stopping), sorted by RMSE:
| Model | MAE | RMSE | MAPE (%) | R² | Theil U | Train (s) |
|---|---|---|---|---|---|---|
| Random walk | 0.8457 | 1.2230 | 1.1571 | 0.9934 | 1.0000 | 0.0 |
| PELT-WT-GRU | 0.8484 | 1.2361 | 1.1611 | 0.9933 | 1.0107 | 79.1 |
| PELT-WT-LSTM (multi) | 0.8930 | 1.2615 | 1.2292 | 0.9930 | 1.0315 | 64.9 |
| BP&ICSS-WT-LSTM | 0.9210 | 1.2847 | 1.2583 | 0.9928 | 1.0505 | 76.7 |
| PELT-WT-LSTM (uni) | 1.0078 | 1.3459 | 1.4313 | 0.9920 | 1.1005 | 271.1 |
| PELT-WT-TCN | 1.6752 | 2.1012 | 2.2983 | 0.9806 | 1.7180 | 46.8 |
Two results worth stating plainly. Every one of the five models has Theil's U above 1, so not one of them beats a random walk. And the paper's ranking does not survive: it puts PELT-WT-TCN first, whereas here the TCN comes last by a clear margin. The rest of the order does reproduce — GRU, then multivariate LSTM, then BP&ICSS, then univariate LSTM.
As published in the paper (Table 1, p. 22):
| Model | MAE | RMSE | MAPE (%) | R² |
|---|---|---|---|---|
| BP&ICSS-WT-LSTM | 4.6345 | 5.3878 | 5.8731 | 0.8712 |
| PELT-WT-LSTM (uni) | 2.3627 | 2.7488 | 3.0582 | 0.9664 |
| PELT-WT-LSTM (multi) | 1.8192 | 2.2967 | 2.3267 | 0.9765 |
| PELT-WT-GRU | 1.3308 | 1.6987 | 1.7401 | 0.9872 |
| PELT-WT-TCN | 1.1855 | 1.5866 | 1.6451 | 0.9888 |
Available in code as pw.PAPER_TABLE1.
RMSE under all three protocols, so the effect of each change is visible:
| Model | Paper | A level | B stationary | C causal |
|---|---|---|---|---|
| BP&ICSS-WT-LSTM | 5.3878 | 48.8677 | 1.2847 | 1.7253 |
| PELT-WT-LSTM (uni) | 2.7488 | 43.3046 | 1.3459 | 1.7221 |
| PELT-WT-LSTM (multi) | 2.2967 | 38.2507 | 1.2615 | 1.7215 |
| PELT-WT-GRU | 1.6987 | 55.1775 | 1.2361 | 1.7269 |
| PELT-WT-TCN | 1.5866 | 37.2711 | 2.1012 | 2.2677 |
| Random walk | — | 1.2230 | 1.2230 | 1.7217 |
Under protocol A, the paper's specification applied literally, every model lands between RMSE 37 and 55 with R² between −5.1 and −12.4, and the forecast collapses to a flat band around EUR 20 against a truth of EUR 33–98. That is not a bug in this implementation — §6 explains why, and the reasoning is reproducible.
Under protocol C, with every look-ahead removed, all four recurrent models land within 0.3 % of the random walk and of each other. Theil's U is 1.000 to three decimals. The formal tests then settle it:
- Diebold–Mariano, best model vs the random walk: DM = −0.122, p = 0.9027. Not significant. No pairwise difference among the four recurrent models is significant either.
- Model Confidence Set at α = 0.10: five of six models survive, including the random walk. The only specification rejected is the paper's own preferred one, PELT-WT-TCN — rejected for being significantly worse (p = 0.0000).
Once the leak is removed there is no evidence that any of the paper's five architectures forecasts the EUA price better than assuming tomorrow's price equals today's.
Full numbers, and the 34 figures, land in results/ and assets/.
A few of the figures
Structural breaks in the EUA price, matched to the policy chronology (the paper's Figure 7):
Every model's forecast against the realised price on the test window (Figure 14):
Pairwise Diebold–Mariano p-values — the test the paper does not run. Pale cells are pairs that cannot be distinguished:
6. What the replication found
Four findings, all reproducible. Details and derivations in
docs/REPLICATION_NOTES.md.
The models cannot extrapolate past the training range
The EUA price trends hard: the 80 % training window tops out at EUR 35.14
while the test window reaches EUR 98.01 — 2.79× higher. An LSTM or GRU
squashes its state through tanh, so once inputs leave the range seen in
training the state saturates and the forecast flattens. Measured, 25 epochs:
| Configuration | RMSE | R² | Prediction range |
|---|---|---|---|
| one-hot regimes + exogenous (the paper's spec) | 55.18 | −12.37 | EUR 15.4 – 25.9 |
| exogenous only | 24.01 | −1.53 | EUR 32.2 – 69.2 |
| denoised price only | 4.42 | 0.914 | EUR 34.3 – 90.5 |
The more faithful the configuration, the worse it does. stationary=True
fixes it by modelling the change instead of the level.
One-hot regime dummies cannot describe a future regime
PELT finds 11 breaks, so 12 regimes. After an 80/20 split, regimes 7–11 occur
only in the test window: five of the twelve one-hot columns are identically
zero for every training row, so no network can learn a weight for them. This is
structural to the paper's e_t encoding — the last regime always begins after
the last training observation.
The wavelet filter looks ahead
wavedec/waverec over the whole series is two-sided. Perturbing the price from
t = 3000 onwards changes the denoised value as early as t = 2994 — a
six-observation look-ahead. For a one-step-ahead forecast, one would already
be too many. Use denoise_mode="causal".
The paper never benchmarks against a random walk
On this data the no-change forecast gives RMSE 1.223, R² 0.9934 — better than every model in the paper's Table 1, including the winner (1.5866). Plain OLS on the same 30-step windows reaches RMSE 0.8189. A high R² on a near-unit-root series in levels is not evidence of skill.
Neither Diebold–Mariano nor a Model Confidence Set appears in the paper, so none of its reported differences is shown to be significant. Both are provided here.
Two errata
- The abstract's headline "22.35 % RMSE / 18.63 % MAE" improvement cannot be
recovered from the paper's own Table 1. The true figures are 70.55 % / 74.42 %
against
BP&ICSS-WT-LSTMand 6.60 % / 10.92 % againstPELT-WT-GRU; no pair of rows gives 22.35 % / 18.63 %. Check it withpw.improvement_table. - Training times are reported twice and disagree (Figure 16 vs the Section
4.3 text). Both are kept in
pw.PAPER_TRAIN_TIMES.
7. The real dataset
load_paper_dataset() returns 6,113 rows × 14 columns, exactly the paper's
sample size, cached in data/.
| Block | Columns |
|---|---|
| Target | Carbon_Price (EUA spot, EUR/tCO₂) |
| Energy | Europe_Coal, TTF_Natural_Gas, Henry_Hub_Gas, Brent_Crude |
| Equity / FX | Euro_Stoxx_50, DAX, VIX, EURUSD |
| Uncertainty | EPU_US, EPU_UK, GPR, EU_10Y_Yield |
| Policy | Policy (reconstructed — see below) |
Three features named in the paper have no free feed and are listed in
pw.UNAVAILABLE_FEATURES: Epex Spot Germany (commercial licence), the Citi
Economic Surprise Index (Bloomberg/Citi), and 1-week Euribor (EMMI licence).
Epex is the paper's second-ranked driver, so its absence is material.
The Policy feature is never defined in the paper, despite being its most
important predictor (Extra-Trees importance > 0.5). build_policy_features
reconstructs it transparently from the twelve dated events the paper itself
lists in Section 4.1, as a signed exponentially-decaying impulse plus the EU ETS
phase number. It is documented as a reconstruction, not the original series.
Your own data works too — any dated frame with a target column:
pipe = pw.PELTWTPipeline(stationary=True).fit(my_df, price_col="Price")
8. Module map
| Module | Contents |
|---|---|
peltwtcn/datasets.py |
live loaders, caching, the policy reconstruction, POLICY_EVENTS |
peltwtcn/breaks.py |
PELT, ICSS, Bai–Perron, BP&ICSS, regime encoding |
peltwtcn/wavelet.py |
decomposition, denoising, causal denoising, thresholds |
peltwtcn/features.py |
z_t design matrix, sliding windows, scalers |
peltwtcn/models.py |
LSTM, GRU, TCN, weight-normalised dilated causal conv |
peltwtcn/pipeline.py |
PipelineConfig, PELTWTPipeline, run_experiment |
peltwtcn/metrics.py |
MAE/RMSE/MAPE/R²/Theil U, Diebold–Mariano, MCS |
peltwtcn/tables.py |
Table 1, descriptives, break inventory, paper comparison |
peltwtcn/plots.py |
every figure of the paper, journal styling |
9. Documentation
Start with the tutorial. It is a single script you can run today, and the guide is its narrative twin.
| Document | What it covers |
|---|---|
examples/tutorial_step_by_step.py |
Start here. A runnable 13-step tutorial, ~5 minutes, printing what every stage produced |
docs/TUTORIAL_OUTPUT.md |
The verbatim output of that script, so you can check your own run against it |
docs/STEP_BY_STEP_GUIDE.md |
How to write the code, stage by stage — 15 steps, every block runnable, with the reasoning |
docs/SYNTAX.md |
Complete API reference: every function, argument and return value |
docs/REPLICATION_NOTES.md |
What matches the paper, what cannot, and the errata |
examples/run_full_replication.py |
All three protocols end to end at the paper's own 50-epoch settings |
python examples/tutorial_step_by_step.py # learn it (~5 min)
python examples/run_full_replication.py # replicate it (~30 min)
10. Testing
pytest # everything
pytest -m "not slow" # skip the network fits
pytest -m "not network" # skip the live downloads
200 fast tests, 13 more that fit networks. Highlights of what is actually verified, rather than merely asserted:
- the TCN's dilated convolution is strictly causal — perturbing
x_tfort ≥ 20leaves every output beforet = 20bit-identical — and matches a hand-rolled reference convolution to float32 precision; - the wavelet reconstruction identity
A + ΣD = f; - the paper's filter does leak and the causal one does not;
- PELT recovers known break locations to within 5 observations, and ICSS finds a variance shift while ignoring a mean shift;
- Theil's U of a random walk is exactly 1;
- the bundled dataset is 6,113 rows spanning the paper's exact dates;
- the 70.55 % and 6.60 % improvement figures, pinned as regression tests.
11. Citation
If you use this software, please cite both the implementation and the original
paper. A CITATION.cff is included.
@software{roudane2026peltwtcn,
author = {Roudane, Merwan},
title = {peltwtcn: Hybrid deep-learning carbon price forecasting with
PELT structural-break detection and wavelet denoising},
year = {2026},
version = {1.0.0},
url = {https://github.com/merwanroudane/peltwtcn},
license = {MIT}
}
@article{ren2025hybrid,
author = {Ren, Runsheng and Li, Jing and Li, Yanxiu and Huang, Shixun and
Shen, Jun and Li, Wanqing and Le, John and Wang, Sheng},
title = {A Hybrid Deep Learning based Carbon Price Forecasting Framework
with Structural Breakpoints Detection and Signal Denoising},
journal = {arXiv preprint arXiv:2511.04988},
year = {2025},
url = {https://arxiv.org/abs/2511.04988}
}
Key methodological references
| Reference | DOI |
|---|---|
| Bai & Perron (2003), J. Applied Econometrics | 10.1002/jae.659 |
| Inclán & Tiao (1994), JASA | 10.2307/2290916 |
| Killick, Fearnhead & Eckley (2012), JRSS-B | 10.1111/j.1467-9868.2011.01004.x |
| Mallat (1989), IEEE TPAMI | 10.1109/34.192463 |
| Lin & Zhang (2022), Process Safety and Env. Protection | 10.1016/j.psep.2022.08.011 |
| Liu et al. (2012), Energy Economics | 10.1016/j.eneco.2011.09.002 |
| Geurts, Ernst & Wehenkel (2006), Machine Learning | 10.1007/s10994-006-6226-1 |
| Cho et al. (2014), EMNLP | 10.3115/v1/D14-1179 |
| Bai, Kolter & Koltun (2018) | arXiv:1803.01271 |
| Hochreiter & Schmidhuber (1997), Neural Computation | 10.1162/neco.1997.9.8.1735 |
12. Author
Dr Merwan Roudane
- Email: merwanroudane920@gmail.com
- GitHub: @merwanroudane
Other packages: QuantileOnQuantile, mqqr, qqkrls, mqqcause (CRAN).
Licence
MIT — see LICENSE.
The implementation is original work. The methodology is due to Ren et al. (2025); please cite them. Redistribution of the underlying price series is subject to each provider's own terms.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file peltwtcn-1.0.0.tar.gz.
File metadata
- Download URL: peltwtcn-1.0.0.tar.gz
- Upload date:
- Size: 116.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bfca119686dcbc691850949fb11f00248244c1aca3e93724a6d7c6e45d1faab9
|
|
| MD5 |
389baa048604b0b9268f5fb03c2cc02b
|
|
| BLAKE2b-256 |
fc427895c9e50e8e010cf68f9e75add374b9619dfd30a39673b1f8287d538c75
|
File details
Details for the file peltwtcn-1.0.0-py3-none-any.whl.
File metadata
- Download URL: peltwtcn-1.0.0-py3-none-any.whl
- Upload date:
- Size: 63.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f4a9ecabd45a5a878365fff78d4200df74e66a423a4ec08273854065a3252b9
|
|
| MD5 |
34864dd34391dcd9365e6f9d91a432ed
|
|
| BLAKE2b-256 |
a7b6633ee6d231e6d01114cf9534d24e9ae8262c59fa353d9c7c5fef0c6f41b6
|