diff --git a/.all-contributorsrc b/.all-contributorsrc index d5b5d8d9c5..7476a2db82 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -2437,6 +2437,43 @@ "contributions": [ "doc" ] + }, + { + "login": "IRKnyazev", + "name": "Ivan Knyazev", + "avatar_url": "https://avatars.githubusercontent.com/u/105492484?v=4", + "profile": "https://github.com/IRKnyazev", + "contributions": [ + "doc" + ] + }, + { + "login": "Cyril-Meyer", + "name": "Cyril Meyer", + "avatar_url": "https://avatars.githubusercontent.com/u/69190238?v=4", + "profile": "https://github.com/Cyril-Meyer", + "contributions": [ + "test" + ] + }, + { + "login": "Moonzyyy", + "name": "Daniele Carli", + "avatar_url": "https://avatars.githubusercontent.com/u/47296443?v=4", + "profile": "https://github.com/Moonzyyy", + "contributions": [ + "doc" + ] + }, + { + "login": "adm-unl", + "name": "Adam Unal", + "avatar_url": "https://avatars.githubusercontent.com/u/143117979?v=4", + "profile": "https://github.com/adm-unl", + "contributions": [ + "doc" + ] } - ] + ], + "commitType": "docs" } diff --git a/.github/workflows/pr_precommit.yml b/.github/workflows/pr_precommit.yml index d9d60bdd03..a81fc60e14 100644 --- a/.github/workflows/pr_precommit.yml +++ b/.github/workflows/pr_precommit.yml @@ -17,8 +17,19 @@ jobs: runs-on: ubuntu-20.04 steps: + - name: Create app token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.PR_APP_ID }} + private-key: ${{ secrets.PR_APP_KEY }} + - name: Checkout uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + token: ${{ steps.app-token.outputs.token }} - name: Setup Python 3.10 uses: actions/setup-python@v5 @@ -45,22 +56,6 @@ jobs: extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }} # push fixes if pre-commit fails and PR is eligible - - if: ${{ failure() && github.event_name == 'pull_request_target' && !github.event.pull_request.draft && !contains(github.event.pull_request.labels.*.name, 'stop pre-commit fixes') }} - name: Create app token - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ vars.PR_APP_ID }} - private-key: ${{ secrets.PR_APP_KEY }} - - - if: ${{ failure() && github.event_name == 'pull_request_target' && !github.event.pull_request.draft && !contains(github.event.pull_request.labels.*.name, 'stop pre-commit fixes') }} - name: Checkout - uses: actions/checkout@v4 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.head_ref }} - token: ${{ steps.app-token.outputs.token }} - - if: ${{ failure() && github.event_name == 'pull_request_target' && !github.event.pull_request.draft && !contains(github.event.pull_request.labels.*.name, 'stop pre-commit fixes') }} name: Push pre-commit fixes uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index afeee5ffdd..e0f95c2b04 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -27,7 +27,7 @@ jobs: persist-credentials: false - name: Run analysis - uses: ossf/scorecard-action@v2.3.3 + uses: ossf/scorecard-action@v2.4.0 with: results_file: results.sarif results_format: sarif diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2dec990167..e5991439bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,13 +29,13 @@ repos: args: [ "--create", "--python-folders", "aeon" ] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.2 + rev: v0.5.5 hooks: - id: ruff args: [ "--fix"] - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: [ "--py38-plus" ] diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0a2b990a23..21372faf43 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,7 +1,7 @@ # Contributors -[![All Contributors](https://img.shields.io/badge/all_contributors-236-orange.svg)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-240-orange.svg)](#contributors) This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! @@ -20,10 +20,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Aaron Margolese-Malin
Aaron Margolese-Malin

πŸ› Aaron Smith
Aaron Smith

πŸ’» Abhishek Pathak
Abhishek Pathak

πŸ› + Adam Unal
Adam Unal

πŸ“– Afzal Ansari
Afzal Ansari

πŸ’» πŸ“– - Ahmed Bilal
Ahmed Bilal

πŸ“– + Ahmed Bilal
Ahmed Bilal

πŸ“– AidenRushbrooke
AidenRushbrooke

πŸ’» ⚠️ Akshat Nayak
Akshat Nayak

πŸ’» Akshat Rampuria
Akshat Rampuria

πŸ“– @@ -31,9 +32,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Alex Hawkins-Hooker
Alex Hawkins-Hooker

πŸ’» Alexandra Amidon
Alexandra Amidon

πŸ“ πŸ“– πŸ€” Ali Ismail-Fawaz
Ali Ismail-Fawaz

πŸ’» πŸ› πŸ“– ⚠️ 🚧 - Ali Teeney
Ali Teeney

πŸ’» + Ali Teeney
Ali Teeney

πŸ’» Ali Yazdizadeh
Ali Yazdizadeh

πŸ“– Alwin
Alwin

πŸ“– πŸ’» 🚧 An Hoang
An Hoang

πŸ› πŸ’» @@ -41,9 +42,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d AndrΓ© Guarnier De Mitri
AndrΓ© Guarnier De Mitri

πŸ’» Angus Dempster
Angus Dempster

πŸ’» ⚠️ βœ… Antoine Guillaume
Antoine Guillaume

πŸ’» πŸ“– - Antoni Baum
Antoni Baum

πŸ’» + Antoni Baum
Antoni Baum

πŸ’» Aparna Sakshi
Aparna Sakshi

πŸ’» Arelo Tanoh
Arelo Tanoh

πŸ“– Arepalli Yashwanth Reddy
Arepalli Yashwanth Reddy

πŸ’» πŸ› πŸ“– @@ -51,9 +52,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Arnav
Arnav

πŸ’» Ayushmaan Seth
Ayushmaan Seth

πŸ’» πŸ‘€ ⚠️ πŸ“– πŸ“‹ βœ… BANDASAITEJAREDDY
BANDASAITEJAREDDY

πŸ’» πŸ“– - Badr-Eddine Marani
Badr-Eddine Marani

πŸ’» + Badr-Eddine Marani
Badr-Eddine Marani

πŸ’» Benedikt Heidrich
Benedikt Heidrich

πŸ’» Benjamin Bluhm
Benjamin Bluhm

πŸ’» πŸ“– πŸ’‘ Bhaskar Dhariyal
Bhaskar Dhariyal

πŸ’» ⚠️ @@ -61,9 +62,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Bohan Zhang
Bohan Zhang

πŸ’» Bouke Postma
Bouke Postma

πŸ’» πŸ› πŸ€” Brian Murphy
Brian Murphy

πŸ“– - Carlos Borrajo
Carlos Borrajo

πŸ’» πŸ“– + Carlos Borrajo
Carlos Borrajo

πŸ’» πŸ“– Carlos Ramos CarreΓ±o
Carlos Ramos CarreΓ±o

πŸ“– Chang Wei Tan
Chang Wei Tan

πŸ’» Cheuk Ting Ho
Cheuk Ting Ho

πŸ’» @@ -71,239 +72,242 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Christopher Dahlin
Christopher Dahlin

πŸ’» Christopher Lo
Christopher Lo

πŸ’» πŸ€” Ciaran Gilbert
Ciaran Gilbert

πŸ› πŸ’» πŸ“– ⚠️ πŸ€” - ClaudiaSanches
ClaudiaSanches

πŸ’» ⚠️ + ClaudiaSanches
ClaudiaSanches

πŸ’» ⚠️ Corvin Paul
Corvin Paul

πŸ“– + Cyril Meyer
Cyril Meyer

⚠️ Daniel Burkhardt Cerigo
Daniel Burkhardt Cerigo

πŸ’» Daniel MartΓ­n MartΓ­nez
Daniel MartΓ­n MartΓ­nez

πŸ“– πŸ› + Daniele Carli
Daniele Carli

πŸ“– Dave Hirschfeld
Dave Hirschfeld

πŸš‡ David Buchaca Prats
David Buchaca Prats

πŸ’» + + David Guijo-Rubio
David Guijo-Rubio

πŸ’» πŸ€” Divya Tiwari
Divya Tiwari

πŸ’» πŸ”£ Dmitriy Valetov
Dmitriy Valetov

πŸ’» βœ… - - Doug Ollerenshaw
Doug Ollerenshaw

πŸ“– Drishti Bhasin
Drishti Bhasin

πŸ’» Dylan Sherry
Dylan Sherry

πŸš‡ Emilia Rose
Emilia Rose

πŸ’» ⚠️ Er Jie Yong
Er Jie Yong

πŸ› πŸ’» + + Evan Miller
Evan Miller

βœ… Eyal Shafran
Eyal Shafran

πŸ’» Federico Garza
Federico Garza

πŸ’» πŸ’‘ - - Felix Claessen
Felix Claessen

πŸ’» πŸ“– ⚠️ πŸ› Florian Stinner
Florian Stinner

πŸ’» ⚠️ Franz Kiraly
Franz Kiraly

πŸ› πŸ’Ό πŸ’» πŸ“– 🎨 πŸ“‹ πŸ’‘ πŸ’΅ πŸ” πŸ€” 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬ πŸ‘€ πŸ“’ ⚠️ βœ… πŸ“Ή Freddy A Boulton
Freddy A Boulton

πŸš‡ ⚠️ Futuer
Futuer

πŸ“– + + Gabriel Riegner
Gabriel Riegner

πŸ“– Galina Chernikova
Galina Chernikova

πŸ’» George Oastler
George Oastler

πŸ’» ⚠️ πŸ“¦ πŸ’‘ πŸ“– - - Gilberto Barbosa
Gilberto Barbosa

πŸ’» Grace Gao
Grace Gao

πŸ’» πŸ› Guilherme Arcencio
Guilherme Arcencio

πŸ’» ⚠️ Guzal Bulatova
Guzal Bulatova

πŸ› πŸ’» πŸ“‹ πŸ§‘β€πŸ« πŸ“† πŸ‘€ ⚠️ HYang1996
HYang1996

πŸ’» ⚠️ πŸ“– βœ… + + Harshitha Sudhakar
Harshitha Sudhakar

πŸ“– πŸ’» Hedeer El Showk
Hedeer El Showk

πŸ› πŸ“– πŸ’» Huayi Wei
Huayi Wei

βœ… - - Ifeanyi30
Ifeanyi30

πŸ’» Ilja Maurer
Ilja Maurer

πŸ’» Ilyas Moutawwakil
Ilyas Moutawwakil

πŸ’» πŸ“– Ireoluwatomiwa
Ireoluwatomiwa

πŸ“– Ishan Nangia
Ishan Nangia

πŸ€” + + + Ivan Knyazev
Ivan Knyazev

πŸ“– Jack Russon
Jack Russon

πŸ’» James Large
James Large

πŸ’» πŸ“– ⚠️ πŸš‡ 🚧 James Morrill
James Morrill

πŸ’» - - Jasmine Liaw
Jasmine Liaw

πŸ’» Jason Lines
Jason Lines

πŸ’» πŸ’Ό πŸ“– 🎨 πŸ“‹ πŸ” πŸ€” πŸ“† πŸ’¬ πŸ‘€ πŸ“’ πŸ’‘ Jason Mok
Jason Mok

πŸ“– Jason Pong
Jason Pong

πŸ’» ⚠️ + + Jaume Mateu
Jaume Mateu

πŸ’» JonathanBechtel
JonathanBechtel

πŸ’» πŸ€” ⚠️ Joren Hammudoglu
Joren Hammudoglu

πŸš‡ Juan Orduz
Juan Orduz

βœ… πŸ“– - - Julian Cooper
Julian Cooper

πŸ’» πŸ€” Juliana
Juliana

πŸ’» Justin Shenk
Justin Shenk

πŸ“– Kai Lion
Kai Lion

πŸ’» ⚠️ πŸ“– + + Kavin Anand
Kavin Anand

πŸ“– Kejsi Take
Kejsi Take

πŸ’» Kevin Lam
Kevin Lam

πŸ’» πŸ’‘ ⚠️ Kirstie Whitaker
Kirstie Whitaker

πŸ€” πŸ” - - Kishan Manani
Kishan Manani

πŸ’» πŸ“– ⚠️ πŸ› πŸ€” Krum Arnaudov
Krum Arnaudov

πŸ› πŸ’» Kutay Koralturk
Kutay Koralturk

πŸ’» πŸ› Leonidas Tsaprounis
Leonidas Tsaprounis

πŸ’» πŸ› πŸ§‘β€πŸ« πŸ‘€ + + Lielle Ravid
Lielle Ravid

πŸ’» πŸ“– Logan Duffy
Logan Duffy

πŸ’» πŸ“– ⚠️ πŸ› πŸ€” Lorena Pantano
Lorena Pantano

πŸ€” Lorenzo Toniazzi
Lorenzo Toniazzi

πŸ’» - - Lovkush
Lovkush

πŸ’» ⚠️ πŸ€” πŸ§‘β€πŸ« πŸ“† Luca Bennett
Luca Bennett

πŸ’» πŸ“– ⚠️ Luis Ventura
Luis Ventura

πŸ’» Luis Zugasti
Luis Zugasti

πŸ“– + + Lukasz Mentel
Lukasz Mentel

πŸ’» πŸ“– πŸš‡ ⚠️ πŸ› 🚧 πŸ§‘β€πŸ« Marcelo Trylesinski
Marcelo Trylesinski

πŸ“– Marco Gorelli
Marco Gorelli

πŸš‡ Margaret Gorlin
Margaret Gorlin

πŸ’» πŸ’‘ ⚠️ - - Mariam Jabara
Mariam Jabara

πŸ’» Marielle
Marielle

πŸ“– πŸ’» πŸ€” Markus LΓΆning
Markus LΓΆning

πŸ’» ⚠️ 🚧 πŸ“¦ πŸ‘€ πŸš‡ πŸ’‘ πŸ› βœ… πŸ’Ό πŸ“– 🎨 πŸ“‹ πŸ” πŸ€” πŸ“† πŸ’¬ πŸ“’ πŸ§‘β€πŸ« πŸ“Ή Martin Walter
Martin Walter

πŸ’» πŸ› πŸ“† πŸ” πŸ§‘β€πŸ« πŸ€” 🎨 πŸ‘€ πŸ“– πŸ“’ + + Martina G. Vilas
Martina G. Vilas

πŸ‘€ πŸ€” Matthew Middlehurst
Matthew Middlehurst

πŸ› πŸ’» πŸ”£ πŸ“– 🎨 πŸ’‘ πŸ€” πŸš‡ 🚧 πŸ§‘β€πŸ« πŸ“£ πŸ’¬ πŸ”¬ πŸ‘€ ⚠️ βœ… πŸ“’ Max Patzelt
Max Patzelt

πŸ’» Miao Cai
Miao Cai

πŸ› πŸ’» - - Michael F. Mbouopda
Michael F. Mbouopda

πŸ’» πŸ› πŸ“– Michael Feil
Michael Feil

πŸ’» ⚠️ πŸ€” Michal Chromcak
Michal Chromcak

πŸ’» πŸ“– ⚠️ βœ… Mirae Parker
Mirae Parker

πŸ’» ⚠️ + + Mohammed Saif Kazamel
Mohammed Saif Kazamel

πŸ› Morad :)
Morad :)

πŸ’» ⚠️ πŸ“– Multivin12
Multivin12

πŸ’» ⚠️ MΓ‘rcio A. Freitas Jr
MΓ‘rcio A. Freitas Jr

πŸ“– - - Niek van der Laan
Niek van der Laan

πŸ’» Nikhil Gupta
Nikhil Gupta

πŸ’» πŸ› πŸ“– Nikola Shahpazov
Nikola Shahpazov

πŸ“– Nilesh Kumar
Nilesh Kumar

πŸ’» + + Nima Nooshiri
Nima Nooshiri

πŸ“– Ninnart Fuengfusin
Ninnart Fuengfusin

πŸ’» Noa Ben Ami
Noa Ben Ami

πŸ’» ⚠️ πŸ“– Oleksandr Shchur
Oleksandr Shchur

πŸ› πŸ’» - - Oleksii Kachaiev
Oleksii Kachaiev

πŸ’» ⚠️ Oliver Matthews
Oliver Matthews

πŸ’» Patrick Rockenschaub
Patrick Rockenschaub

πŸ’» 🎨 πŸ€” ⚠️ Patrick SchΓ€fer
Patrick SchΓ€fer

πŸ’» βœ… + + Paul
Paul

πŸ“– Paul Rabich
Paul Rabich

πŸ’» Paul Yim
Paul Yim

πŸ’» πŸ’‘ ⚠️ Philipp Kortmann
Philipp Kortmann

πŸ’» πŸ“– - - Piyush Gade
Piyush Gade

πŸ’» πŸ‘€ Pulkit Verma
Pulkit Verma

πŸ“– Quaterion
Quaterion

πŸ› Rafael AyllΓ³n-GavilΓ‘n
Rafael AyllΓ³n-GavilΓ‘n

πŸ’» + + Rakshitha Godahewa
Rakshitha Godahewa

πŸ’» πŸ“– RavenRudi
RavenRudi

πŸ’» Raya Chakravarty
Raya Chakravarty

πŸ“– Rick van Hattem
Rick van Hattem

πŸš‡ - - Rishabh Bali
Rishabh Bali

πŸ’» Rishav Kumar Sinha
Rishav Kumar Sinha

πŸ“– Rishi Kumar Ray
Rishi Kumar Ray

πŸš‡ Riya Elizabeth John
Riya Elizabeth John

πŸ’» ⚠️ πŸ“– + + Ronnie Llamado
Ronnie Llamado

πŸ“– Ryan Kuhns
Ryan Kuhns

πŸ’» πŸ“– βœ… πŸ’‘ πŸ€” πŸ‘€ ⚠️ Sagar Mishra
Sagar Mishra

⚠️ Sajaysurya Ganesh
Sajaysurya Ganesh

πŸ’» πŸ“– 🎨 πŸ’‘ πŸ€” ⚠️ βœ… - - Saransh Chopra
Saransh Chopra

πŸ“– πŸš‡ Satya Prakash Pattnaik
Satya Prakash Pattnaik

πŸ“– Saurabh Dasgupta
Saurabh Dasgupta

πŸ’» Sebastiaan Koel
Sebastiaan Koel

πŸ’» πŸ“– + + Sebastian Hagn
Sebastian Hagn

πŸ“– Sebastian Schmidl
Sebastian Schmidl

πŸ› πŸ’» πŸ“– πŸ”¬ ⚠️ πŸ‘€ πŸ”£ Shivansh Subramanian
Shivansh Subramanian

πŸ“– πŸ’» Solomon Botchway
Solomon Botchway

🚧 - - Stanislav Khrapov
Stanislav Khrapov

πŸ’» Stijn Rotman
Stijn Rotman

πŸ’» Svea Marie Meyer
Svea Marie Meyer

πŸ“– πŸ’» Sylvain Combettes
Sylvain Combettes

πŸ’» πŸ› + + TNTran92
TNTran92

πŸ’» Taiwo Owoseni
Taiwo Owoseni

πŸ’» Thach Le Nguyen
Thach Le Nguyen

πŸ’» ⚠️ TheMathcompay Widget Factory Team
TheMathcompay Widget Factory Team

πŸ“– - - Thomas Buckley-Houston
Thomas Buckley-Houston

πŸ› Tom Xu
Tom Xu

πŸ’» πŸ“– Tomasz Chodakowski
Tomasz Chodakowski

πŸ’» πŸ“– πŸ› Tony Bagnall
Tony Bagnall

πŸ’» πŸ’Ό πŸ“– 🎨 πŸ“‹ πŸ” πŸ€” πŸ“† πŸ’¬ πŸ‘€ πŸ“’ πŸ”£ + + Tvisha Vedant
Tvisha Vedant

πŸ’» Utkarsh Kumar
Utkarsh Kumar

πŸ’» πŸ“– Utsav Kumar Tiwari
Utsav Kumar Tiwari

πŸ’» πŸ“– Vedant
Vedant

πŸ“– - - Viktor Dremov
Viktor Dremov

πŸ’» ViktorKaz
ViktorKaz

πŸ’» πŸ“– 🎨 Vyomkesh Vyas
Vyomkesh Vyas

πŸ’» πŸ“– πŸ’‘ ⚠️ Wayne Adams
Wayne Adams

πŸ“– + + William Templier
William Templier

πŸ“– William Zeng
William Zeng

πŸ› William Zheng
William Zheng

πŸ’» ⚠️ Yair Beer
Yair Beer

πŸ’» - - Yash Lamba
Yash Lamba

πŸ’» Yi-Xuan Xu
Yi-Xuan Xu

πŸ’» ⚠️ 🚧 πŸ“– Ziyao Wei
Ziyao Wei

πŸ’» aa25desh
aa25desh

πŸ’» πŸ› + + abandus
abandus

πŸ€” πŸ’» adoherty21
adoherty21

πŸ› bethrice44
bethrice44

πŸ› πŸ’» πŸ‘€ ⚠️ big-o
big-o

πŸ’» ⚠️ 🎨 πŸ€” πŸ‘€ βœ… πŸ§‘β€πŸ« - - bobbys
bobbys

πŸ’» brett koonce
brett koonce

πŸ“– btrtts
btrtts

πŸ“– chizzi25
chizzi25

πŸ“ + + chrisholder
chrisholder

πŸ’» ⚠️ πŸ“– 🎨 πŸ’‘ πŸ› danbartl
danbartl

πŸ› πŸ’» πŸ‘€ πŸ“’ ⚠️ βœ… πŸ“Ή hamzahiqb
hamzahiqb

πŸš‡ hiqbal2
hiqbal2

πŸ“– - - jesellier
jesellier

πŸ’» jschemm
jschemm

πŸ’» julu98
julu98

πŸ› kkoziara
kkoziara

πŸ’» πŸ› + + matteogales
matteogales

πŸ’» 🎨 πŸ€” nileenagp
nileenagp

πŸ’» oleskiewicz
oleskiewicz

πŸ’» πŸ“– ⚠️ pabworks
pabworks

πŸ’» ⚠️ - - patiently pending world peace
patiently pending world peace

πŸ’» raishubham1
raishubham1

πŸ“– simone-pignotti
simone-pignotti

πŸ’» πŸ› sophijka
sophijka

πŸ“– 🚧 + + sri1419
sri1419

πŸ’» tensorflow-as-tf
tensorflow-as-tf

πŸ’» vNtzYy
vNtzYy

πŸ› vedazeren
vedazeren

πŸ’» ⚠️ - - vincent-nich12
vincent-nich12

πŸ’» vollmersj
vollmersj

πŸ“– xiaobenbenecho
xiaobenbenecho

πŸ’» diff --git a/aeon/classification/deep_learning/__init__.py b/aeon/classification/deep_learning/__init__.py index 26ff063855..ba36690cfe 100644 --- a/aeon/classification/deep_learning/__init__.py +++ b/aeon/classification/deep_learning/__init__.py @@ -3,6 +3,7 @@ __all__ = [ "BaseDeepClassifier", "CNNClassifier", + "TimeCNNClassifier", "EncoderClassifier", "FCNClassifier", "InceptionTimeClassifier", @@ -13,7 +14,7 @@ "LITETimeClassifier", "IndividualLITEClassifier", ] -from aeon.classification.deep_learning._cnn import CNNClassifier +from aeon.classification.deep_learning._cnn import CNNClassifier, TimeCNNClassifier from aeon.classification.deep_learning._encoder import EncoderClassifier from aeon.classification.deep_learning._fcn import FCNClassifier from aeon.classification.deep_learning._inception_time import ( diff --git a/aeon/classification/deep_learning/_cnn.py b/aeon/classification/deep_learning/_cnn.py index 15fa496f3a..6f8a13a5ae 100644 --- a/aeon/classification/deep_learning/_cnn.py +++ b/aeon/classification/deep_learning/_cnn.py @@ -1,19 +1,27 @@ -"""Time Convolutional Neural Network (CNN) for classification.""" +"""Time Convolutional Neural Network (CNN) classifier.""" -__maintainer__ = [] -__all__ = ["CNNClassifier"] +__maintainer__ = ["hadifawaz1999"] +__all__ = ["CNNClassifier", "TimeCNNClassifier"] import gc import os import time from copy import deepcopy +from deprecated.sphinx import deprecated from sklearn.utils import check_random_state from aeon.classification.deep_learning.base import BaseDeepClassifier -from aeon.networks import CNNNetwork +from aeon.networks import CNNNetwork, TimeCNNNetwork +# TODO: remove v0.12.0 +@deprecated( + version="0.10.0", + reason="CNNClassifier has been renamed to TimeCNNClassifier" + "and will be removed in 0.12.0.", + category=FutureWarning, +) class CNNClassifier(BaseDeepClassifier): """ Time Convolutional Neural Network (CNN). @@ -76,12 +84,17 @@ class CNNClassifier(BaseDeepClassifier): save_last_model : bool, default = False Whether to save the last model, last epoch trained, using the base class method save_last_model_to_file. + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter is discarded. last_file_name : str, default = "last_model" The name of the file of the last model, if save_last_model is set to False, this parameter is discarded. + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. Notes ----- @@ -120,8 +133,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", verbose=False, loss="mean_squared_error", metrics=None, @@ -144,7 +159,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.verbose = verbose self.loss = loss self.metrics = metrics @@ -243,6 +260,329 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + + if self.verbose: + self.training_model_.summary() + + self.file_name_ = ( + self.best_file_name if self.save_best_model else str(time.time_ns()) + ) + + if self.callbacks is None: + self.callbacks_ = [ + tf.keras.callbacks.ModelCheckpoint( + filepath=self.file_path + self.file_name_ + ".keras", + monitor="loss", + save_best_only=True, + ), + ] + else: + self.callbacks_ = self._get_model_checkpoint_callback( + callbacks=self.callbacks, + file_path=self.file_path, + file_name=self.file_name_, + ) + + self.history = self.training_model_.fit( + X, + y_onehot, + batch_size=self.batch_size, + epochs=self.n_epochs, + verbose=self.verbose, + callbacks=self.callbacks_, + ) + + try: + self.model_ = tf.keras.models.load_model( + self.file_path + self.file_name_ + ".keras", compile=False + ) + if not self.save_best_model: + os.remove(self.file_path + self.file_name_ + ".keras") + except FileNotFoundError: + self.model_ = deepcopy(self.training_model_) + + if self.save_last_model: + self.save_last_model_to_file(file_path=self.file_path) + + gc.collect() + return self + + @classmethod + def get_test_params(cls, parameter_set="default"): + """Return testing parameter settings for the estimator. + + Parameters + ---------- + parameter_set : str, default = "default" + Name of the set of test parameters to return, for use in tests. If no + special parameters are defined for a value, will return "default" set. + For classifiers, a "default" set of parameters should be provided for + general testing, and a "results_comparison" set for comparing against + previously recorded results if the general set does not produce suitable + probabilities to compare against. + + Returns + ------- + params : dict or list of dict, default = {} + Parameters to create testing instances of the class. + Each dict are parameters to construct an "interesting" test instance, i.e., + `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance. + `create_test_instance` uses the first (or only) dictionary in `params`. + """ + param1 = { + "n_epochs": 10, + "batch_size": 4, + "avg_pool_size": 4, + } + + test_params = [param1] + + return test_params + + +class TimeCNNClassifier(BaseDeepClassifier): + """ + Time Convolutional Neural Network (CNN). + + Adapted from the implementation used in [1]_. + + Parameters + ---------- + n_layers : int, default = 2 + The number of convolution layers in the network. + kernel_size : int or list of int, default = 7 + Kernel size of convolution layers, if not a list, the same kernel size + is used for all layer, len(list) should be n_layers. + n_filters : int or list of int, default = [6, 12] + Number of filters for each convolution layer, if not a list, the same n_filters + is used in all layers. + avg_pool_size : int or list of int, default = 3 + The size of the average pooling layer, if not a list, the same + max pooling size is used for all convolution layer. + activation : str or list of str, default = "sigmoid" + Keras activation function used in the model for each layer, if not a list, + the same activation is used for all layers. + padding : str or list of str, default = 'valid' + The method of padding in convolution layers, if not a list, the same padding + used for all convolution layers. + strides : int or list of int, default = 1 + The strides of kernels in the convolution and max pooling layers, if not a + list, the same strides are used for all layers. + dilation_rate : int or list of int, default = 1 + The dilation rate of the convolution layers, if not a list, the same dilation + rate is used all over the network. + use_bias : bool or list of bool, default = True + Condition on whether to use bias values for convolution layers, + if not a list, the same condition is used for all layers. + random_state : int, RandomState instance or None, default=None + If `int`, random_state is the seed used by the random number generator; + If `RandomState` instance, random_state is the random number generator; + If `None`, the random number generator is the `RandomState` instance used + by `np.random`. + Seeded random number generation can only be guaranteed on CPU processing, + GPU processing will be non-deterministic. + n_epochs : int, default = 2000 + The number of epochs to train the model. + batch_size : int, default = 16 + The number of samples per gradient update. + verbose : boolean, default = False + Whether to output extra information. + loss : string, default = "mean_squared_error" + Fit parameter for the keras model. + optimizer : keras.optimizer, default = keras.optimizers.Adam() + metrics : list of strings, default = ["accuracy"] + callbacks : keras.callbacks, default = model_checkpoint + To save best model on training loss. + file_path : file_path for the best model + Only used if checkpoint is used as callback. + save_best_model : bool, default = False + Whether to save the best model, if the modelcheckpoint callback is used by + default, this condition, if True, will prevent the automatic deletion of the + best saved model from file and the user can choose the file name. + save_last_model : bool, default = False + Whether to save the last model, last epoch trained, using the base class method + save_last_model_to_file. + save_init_model : bool, default = False + Whether to save the initialization of the model. + best_file_name : str, default = "best_model" + The name of the file of the best model, if save_best_model is set to False, + this parameter is discarded. + last_file_name : str, default = "last_model" + The name of the file of the last model, if save_last_model is set to False, + this parameter is discarded. + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. + + Notes + ----- + Adapted from the implementation from Fawaz et. al + https://github.com/hfawaz/dl-4-tsc/blob/master/classifiers/cnn.py + + References + ---------- + .. [1] Zhao et. al, Convolutional neural networks for time series classification, + Journal of Systems Engineering and Electronics, 28(1):2017. + + Examples + -------- + >>> from aeon.classification.deep_learning import TimeCNNClassifier + >>> from aeon.datasets import load_unit_test + >>> X_train, y_train = load_unit_test(split="train") + >>> X_test, y_test = load_unit_test(split="test") + >>> cnn = TimeCNNClassifier(n_epochs=20, batch_size=4) # doctest: +SKIP + >>> cnn.fit(X_train, y_train) # doctest: +SKIP + TimeCNNClassifier(...) + """ + + def __init__( + self, + n_layers=2, + kernel_size=7, + n_filters=None, + avg_pool_size=3, + activation="sigmoid", + padding="valid", + strides=1, + dilation_rate=1, + n_epochs=2000, + batch_size=16, + callbacks=None, + file_path="./", + save_best_model=False, + save_last_model=False, + save_init_model=False, + best_file_name="best_model", + last_file_name="last_model", + init_file_name="init_model", + verbose=False, + loss="mean_squared_error", + metrics=None, + random_state=None, + use_bias=True, + optimizer=None, + ): + self.n_layers = n_layers + self.kernel_size = kernel_size + self.n_filters = n_filters + self.padding = padding + self.strides = strides + self.dilation_rate = dilation_rate + self.avg_pool_size = avg_pool_size + self.activation = activation + self.use_bias = use_bias + + self.n_epochs = n_epochs + self.callbacks = callbacks + self.file_path = file_path + self.save_best_model = save_best_model + self.save_last_model = save_last_model + self.save_init_model = save_init_model + self.best_file_name = best_file_name + self.init_file_name = init_file_name + self.verbose = verbose + self.loss = loss + self.metrics = metrics + self.optimizer = optimizer + + self.history = None + + super().__init__( + batch_size=batch_size, + random_state=random_state, + last_file_name=last_file_name, + ) + + self._network = TimeCNNNetwork( + n_layers=self.n_layers, + kernel_size=self.kernel_size, + n_filters=self.n_filters, + avg_pool_size=self.avg_pool_size, + activation=self.activation, + padding=self.padding, + strides=self.strides, + dilation_rate=self.dilation_rate, + use_bias=self.use_bias, + ) + + def build_model(self, input_shape, n_classes, **kwargs): + """Construct a compiled, un-trained, keras model that is ready for training. + + In aeon, time series are stored in numpy arrays of shape (d, m), where d + is the number of dimensions, m is the series length. Keras/tensorflow assume + data is in shape (m, d). This method also assumes (m, d). Transpose should + happen in fit. + + Parameters + ---------- + input_shape : tuple + The shape of the data fed into the input layer, should be (m, d) + n_classes : int + The number of classes, which becomes the size of the output layer + + Returns + ------- + output : a compiled Keras Model + """ + import numpy as np + import tensorflow as tf + + if self.metrics is None: + metrics = ["accuracy"] + else: + metrics = self.metrics + + rng = check_random_state(self.random_state) + self.random_state_ = rng.randint(0, np.iinfo(np.int32).max) + tf.keras.utils.set_random_seed(self.random_state_) + input_layer, output_layer = self._network.build_network(input_shape, **kwargs) + + output_layer = tf.keras.layers.Dense( + units=n_classes, activation=self.activation, use_bias=self.use_bias + )(output_layer) + + self.optimizer_ = ( + tf.keras.optimizers.Adam() if self.optimizer is None else self.optimizer + ) + + model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer) + model.compile( + loss=self.loss, + optimizer=self.optimizer_, + metrics=metrics, + ) + + return model + + def _fit(self, X, y): + """Fit the classifier on the training set (X, y). + + Parameters + ---------- + X : np.ndarray + The training input samples of shape (n_cases, n_channels, n_timepoints) + y : np.ndarray + The training data class labels of shape (n_cases,). + + + Returns + ------- + self : object + """ + import tensorflow as tf + + y_onehot = self.convert_y_to_keras(y) + # Transpose to conform to Keras input style. + X = X.transpose(0, 2, 1) + + self.input_shape = X.shape[1:] + self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/classification/deep_learning/_encoder.py b/aeon/classification/deep_learning/_encoder.py index 7f839456f2..2765c4cbbe 100644 --- a/aeon/classification/deep_learning/_encoder.py +++ b/aeon/classification/deep_learning/_encoder.py @@ -52,6 +52,8 @@ class EncoderClassifier(BaseDeepClassifier): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file. + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -60,6 +62,9 @@ class EncoderClassifier(BaseDeepClassifier): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded. + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -102,8 +107,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", verbose=False, loss="categorical_crossentropy", metrics=None, @@ -124,7 +131,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.n_epochs = n_epochs self.verbose = verbose self.loss = loss @@ -225,6 +234,9 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/classification/deep_learning/_fcn.py b/aeon/classification/deep_learning/_fcn.py index e77457646b..ea14036b87 100644 --- a/aeon/classification/deep_learning/_fcn.py +++ b/aeon/classification/deep_learning/_fcn.py @@ -1,6 +1,6 @@ -"""Fully Convolutional Network (FCN) for classification.""" +"""Fully Convolutional Network (FCN) classifier.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["FCNClassifier"] import gc @@ -69,6 +69,8 @@ class FCNClassifier(BaseDeepClassifier): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file. + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -77,6 +79,10 @@ class FCNClassifier(BaseDeepClassifier): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded. + init_file_name : str, default = "init_model" + The name of the file of the init model, if + save_init_model is set to False, + this parameter is discarded. callbacks : keras.callbacks, default = None Notes @@ -112,8 +118,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", n_epochs=2000, batch_size=16, use_mini_batch_size=False, @@ -145,7 +153,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.history = None @@ -238,6 +248,9 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/classification/deep_learning/_inception_time.py b/aeon/classification/deep_learning/_inception_time.py index d5cc3cec56..6b377d565a 100644 --- a/aeon/classification/deep_learning/_inception_time.py +++ b/aeon/classification/deep_learning/_inception_time.py @@ -1,6 +1,6 @@ -"""InceptionTime classifier.""" +"""InceptionTime and Inception classifiers.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["InceptionTimeClassifier"] import gc @@ -103,6 +103,8 @@ class InceptionTimeClassifier(BaseClassifier): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -111,6 +113,9 @@ class InceptionTimeClassifier(BaseClassifier): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -181,8 +186,10 @@ def __init__( file_path="./", save_last_model=False, save_best_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -218,8 +225,10 @@ def __init__( self.save_last_model = save_last_model self.save_best_model = save_best_model + self.save_init_model = save_init_model self.best_file_name = best_file_name self.last_file_name = last_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.random_state = random_state @@ -229,7 +238,7 @@ def __init__( self.metrics = metrics self.optimizer = optimizer - self.classifers_ = [] + self.classifiers_ = [] super().__init__() @@ -247,7 +256,7 @@ def _fit(self, X, y): ------- self : object """ - self.classifers_ = [] + self.classifiers_ = [] rng = check_random_state(self.random_state) for n in range(0, self.n_classifiers): @@ -269,8 +278,10 @@ def _fit(self, X, y): file_path=self.file_path, save_best_model=self.save_best_model, save_last_model=self.save_last_model, + save_init_model=self.save_init_model, best_file_name=self.best_file_name + str(n), last_file_name=self.last_file_name + str(n), + init_file_name=self.init_file_name + str(n), batch_size=self.batch_size, use_mini_batch_size=self.use_mini_batch_size, n_epochs=self.n_epochs, @@ -282,7 +293,7 @@ def _fit(self, X, y): verbose=self.verbose, ) cls.fit(X, y) - self.classifers_.append(cls) + self.classifiers_.append(cls) gc.collect() return self @@ -323,7 +334,7 @@ def _predict_proba(self, X) -> np.ndarray: """ probs = np.zeros((X.shape[0], self.n_classes_)) - for cls in self.classifers_: + for cls in self.classifiers_: probs += cls._predict_proba(X) probs = probs / self.n_classifiers @@ -437,14 +448,19 @@ class IndividualInceptionClassifier(BaseDeepClassifier): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter - is discarded + is discarded. last_file_name : str, default = "last_model" The name of the file of the last model, if save_last_model is set to False, this parameter - is discarded + is discarded. + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -504,8 +520,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -538,7 +556,9 @@ def __init__( self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.verbose = verbose @@ -652,6 +672,9 @@ def _fit(self, X, y): mini_batch_size = self.batch_size self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/classification/deep_learning/_lite_time.py b/aeon/classification/deep_learning/_lite_time.py index ef2479a949..f16e136d71 100644 --- a/aeon/classification/deep_learning/_lite_time.py +++ b/aeon/classification/deep_learning/_lite_time.py @@ -1,6 +1,6 @@ -"""LITETime classifier.""" +"""LITETime and LITE classifiers.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["LITETimeClassifier"] import gc @@ -60,6 +60,8 @@ class LITETimeClassifier(BaseClassifier): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -68,6 +70,9 @@ class LITETimeClassifier(BaseClassifier): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -120,8 +125,10 @@ def __init__( file_path="./", save_last_model=False, save_best_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -146,8 +153,10 @@ def __init__( self.save_last_model = save_last_model self.save_best_model = save_best_model + self.save_init_model = save_init_model self.best_file_name = best_file_name self.last_file_name = last_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.random_state = random_state @@ -157,7 +166,7 @@ def __init__( self.metrics = metrics self.optimizer = optimizer - self.classifers_ = [] + self.classifiers_ = [] super().__init__() @@ -175,7 +184,7 @@ def _fit(self, X, y): ------- self : object """ - self.classifers_ = [] + self.classifiers_ = [] rng = check_random_state(self.random_state) for n in range(0, self.n_classifiers): @@ -185,8 +194,10 @@ def _fit(self, X, y): file_path=self.file_path, save_best_model=self.save_best_model, save_last_model=self.save_last_model, + save_init_model=self.save_init_model, best_file_name=self.best_file_name + str(n), last_file_name=self.last_file_name + str(n), + init_file_name=self.init_file_name + str(n), batch_size=self.batch_size, use_mini_batch_size=self.use_mini_batch_size, n_epochs=self.n_epochs, @@ -198,7 +209,7 @@ def _fit(self, X, y): verbose=self.verbose, ) cls.fit(X, y) - self.classifers_.append(cls) + self.classifiers_.append(cls) gc.collect() return self @@ -239,7 +250,7 @@ def _predict_proba(self, X) -> np.ndarray: """ probs = np.zeros((X.shape[0], self.n_classes_)) - for cls in self.classifers_: + for cls in self.classifiers_: probs += cls._predict_proba(X) probs = probs / self.n_classifiers @@ -318,6 +329,8 @@ class IndividualLITEClassifier(BaseDeepClassifier): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -326,6 +339,9 @@ class IndividualLITEClassifier(BaseDeepClassifier): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -369,8 +385,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -393,7 +411,9 @@ def __init__( self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.verbose = verbose @@ -495,6 +515,9 @@ def _fit(self, X, y): mini_batch_size = self.batch_size self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/classification/deep_learning/_mlp.py b/aeon/classification/deep_learning/_mlp.py index 1624aa9904..48eb8f711e 100644 --- a/aeon/classification/deep_learning/_mlp.py +++ b/aeon/classification/deep_learning/_mlp.py @@ -1,6 +1,6 @@ -"""Multi Layer Perceptron Network (MLP) for classification.""" +"""Multi Layer Perceptron Network (MLP) classifier.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["MLPClassifier"] import gc @@ -51,6 +51,8 @@ class MLPClassifier(BaseDeepClassifier): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -59,6 +61,9 @@ class MLPClassifier(BaseDeepClassifier): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. optimizer : keras.optimizer, default=keras.optimizers.Adadelta(), metrics : list of strings, default=["accuracy"], activation : string or a tf callable, default="sigmoid" @@ -101,8 +106,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", random_state=None, activation="sigmoid", use_bias=True, @@ -119,7 +126,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.optimizer = optimizer self.history = None @@ -204,6 +213,9 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/classification/deep_learning/_resnet.py b/aeon/classification/deep_learning/_resnet.py index ccd6d11f0d..963faec26b 100644 --- a/aeon/classification/deep_learning/_resnet.py +++ b/aeon/classification/deep_learning/_resnet.py @@ -1,6 +1,6 @@ -"""Residual Network (ResNet) for classification.""" +"""Residual Network (ResNet) classifier.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["ResNetClassifier"] import gc @@ -75,12 +75,17 @@ class ResNetClassifier(BaseDeepClassifier): save_last_model : bool, default = False Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file. + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter is discarded. last_file_name : str, default = "last_model" The name of the file of the last model, if save_last_model is set to False, this parameter is discarded. + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. verbose : boolean, default = False whether to output extra information loss : string, default = "mean_squared_error" @@ -131,8 +136,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", optimizer=None, ): self.n_residual_blocks = n_residual_blocks @@ -153,7 +160,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.optimizer = optimizer self.history = None @@ -250,6 +259,9 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape, self.n_classes_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/classification/deep_learning/_tapnet.py b/aeon/classification/deep_learning/_tapnet.py index 4bfe51e4c2..2ad048827b 100644 --- a/aeon/classification/deep_learning/_tapnet.py +++ b/aeon/classification/deep_learning/_tapnet.py @@ -1,6 +1,6 @@ -"""Time Convolutional Neural Network (CNN) for classification.""" +"""Time series Attentional Prototype Network (TapNet) Classifier.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = [ "TapNetClassifier", ] diff --git a/aeon/classification/deep_learning/base.py b/aeon/classification/deep_learning/base.py index 45974298a0..837945eead 100644 --- a/aeon/classification/deep_learning/base.py +++ b/aeon/classification/deep_learning/base.py @@ -5,7 +5,7 @@ because we can generalise tags, _predict and _predict_proba """ -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["BaseDeepClassifier"] from abc import ABC, abstractmethod diff --git a/aeon/classification/deep_learning/tests/test_deep_classifier_base.py b/aeon/classification/deep_learning/tests/test_deep_classifier_base.py index 203c256eef..e0c4157c7a 100644 --- a/aeon/classification/deep_learning/tests/test_deep_classifier_base.py +++ b/aeon/classification/deep_learning/tests/test_deep_classifier_base.py @@ -10,7 +10,7 @@ from aeon.testing.data_generation import make_example_2d_numpy_collection from aeon.utils.validation._dependencies import _check_soft_dependencies -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] class _DummyDeepClassifier(BaseDeepClassifier): diff --git a/aeon/classification/deep_learning/tests/test_random_state_deep_learning.py b/aeon/classification/deep_learning/tests/test_random_state_deep_learning.py index 4647de8b00..b426169cce 100644 --- a/aeon/classification/deep_learning/tests/test_random_state_deep_learning.py +++ b/aeon/classification/deep_learning/tests/test_random_state_deep_learning.py @@ -12,35 +12,37 @@ __maintainer__ = ["hadifawaz1999"] +_deep_cls_classes = [ + member[1] for member in inspect.getmembers(deep_learning, inspect.isclass) +] + + @pytest.mark.skipif( not _check_soft_dependencies(["tensorflow"], severity="none"), reason="skip test if required soft dependency not available", ) -def test_random_state_deep_learning_cls(): +@pytest.mark.parametrize("deep_cls", _deep_cls_classes) +def test_random_state_deep_learning_cls(deep_cls): """Test Deep Classifier seeding.""" - random_state = 42 - - X, y = make_example_3d_numpy(random_state=random_state) - - deep_cls_classes = [ - member[1] for member in inspect.getmembers(deep_learning, inspect.isclass) - ] - - for i in range(len(deep_cls_classes)): - if ( - "BaseDeepClassifier" in str(deep_cls_classes[i]) - or "InceptionTimeClassifier" in str(deep_cls_classes[i]) - or "LITETimeClassifier" in str(deep_cls_classes[i]) - or "TapNetClassifier" in str(deep_cls_classes[i]) - ): - continue - - deep_cls1 = deep_cls_classes[i](random_state=random_state, n_epochs=4) + if not ( + deep_cls.__name__ + in [ + "BaseDeepClassifier", + "InceptionTimeClassifier", + "LITETimeClassifier", + "TapNetClassifier", + ] + ): + random_state = 42 + + X, y = make_example_3d_numpy(random_state=random_state) + + deep_cls1 = deep_cls(random_state=random_state, n_epochs=4) deep_cls1.fit(X, y) layers1 = deep_cls1.training_model_.layers[1:] - deep_cls2 = deep_cls_classes[i](random_state=random_state, n_epochs=4) + deep_cls2 = deep_cls(random_state=random_state, n_epochs=4) deep_cls2.fit(X, y) layers2 = deep_cls2.training_model_.layers[1:] diff --git a/aeon/classification/deep_learning/tests/test_saving_loading_deep_learning_cls.py b/aeon/classification/deep_learning/tests/test_saving_loading_deep_learning_cls.py new file mode 100644 index 0000000000..d90393a369 --- /dev/null +++ b/aeon/classification/deep_learning/tests/test_saving_loading_deep_learning_cls.py @@ -0,0 +1,83 @@ +"""Unit tests for classifiers deep learners save/load functionalities.""" + +import inspect +import os +import tempfile +import time + +import numpy as np +import pytest + +from aeon.classification import deep_learning +from aeon.testing.data_generation import make_example_3d_numpy +from aeon.utils.validation._dependencies import _check_soft_dependencies + +__maintainer__ = ["hadifawaz1999"] + + +_deep_cls_classes = [ + member[1] for member in inspect.getmembers(deep_learning, inspect.isclass) +] + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="skip test if required soft dependency not available", +) +@pytest.mark.parametrize("deep_cls", _deep_cls_classes) +def test_saving_loading_deep_learning_cls(deep_cls): + """Test Deep Classifier saving.""" + with tempfile.TemporaryDirectory() as tmp: + if not ( + deep_cls.__name__ + in [ + "BaseDeepClassifier", + "InceptionTimeClassifier", + "LITETimeClassifier", + "TapNetClassifier", + ] + ): + if tmp[-1] != "/": + tmp = tmp + "/" + curr_time = str(time.time_ns()) + last_file_name = curr_time + "last" + best_file_name = curr_time + "best" + init_file_name = curr_time + "init" + + X, y = make_example_3d_numpy() + + deep_cls_train = deep_cls( + n_epochs=2, + save_best_model=True, + save_last_model=True, + save_init_model=True, + best_file_name=best_file_name, + last_file_name=last_file_name, + init_file_name=init_file_name, + file_path=tmp, + ) + deep_cls_train.fit(X, y) + + deep_cls_best = deep_cls() + deep_cls_best.load_model( + model_path=os.path.join(tmp, best_file_name + ".keras"), + classes=np.unique(y), + ) + ypred_best = deep_cls_best.predict(X) + assert len(ypred_best) == len(y) + + deep_cls_last = deep_cls() + deep_cls_last.load_model( + model_path=os.path.join(tmp, last_file_name + ".keras"), + classes=np.unique(y), + ) + ypred_last = deep_cls_last.predict(X) + assert len(ypred_last) == len(y) + + deep_cls_init = deep_cls() + deep_cls_init.load_model( + model_path=os.path.join(tmp, init_file_name + ".keras"), + classes=np.unique(y), + ) + ypred_init = deep_cls_init.predict(X) + assert len(ypred_init) == len(y) diff --git a/aeon/classification/dictionary_based/__init__.py b/aeon/classification/dictionary_based/__init__.py index 832eae10a5..29f5970edc 100644 --- a/aeon/classification/dictionary_based/__init__.py +++ b/aeon/classification/dictionary_based/__init__.py @@ -10,10 +10,12 @@ "WEASEL_V2", "MUSE", "REDCOMETS", + "MrSQMClassifier", ] from aeon.classification.dictionary_based._boss import BOSSEnsemble, IndividualBOSS from aeon.classification.dictionary_based._cboss import ContractableBOSS +from aeon.classification.dictionary_based._mrsqm import MrSQMClassifier from aeon.classification.dictionary_based._muse import MUSE from aeon.classification.dictionary_based._redcomets import REDCOMETS from aeon.classification.dictionary_based._tde import ( diff --git a/aeon/classification/shapelet_based/_mrsqm.py b/aeon/classification/dictionary_based/_mrsqm.py similarity index 92% rename from aeon/classification/shapelet_based/_mrsqm.py rename to aeon/classification/dictionary_based/_mrsqm.py index a3b1d4cc57..6c9304a144 100644 --- a/aeon/classification/shapelet_based/_mrsqm.py +++ b/aeon/classification/dictionary_based/_mrsqm.py @@ -1,13 +1,12 @@ """Multiple Representations Sequence Miner (MrSQM) Classifier.""" -__maintainer__ = [] +__maintainer__ = ["TonyBagnall"] __all__ = ["MrSQMClassifier"] from typing import List, Union import numpy as np import pandas as pd -from deprecated.sphinx import deprecated from aeon.classification import BaseClassifier @@ -31,13 +30,6 @@ def _from_numpy3d_to_nested_dataframe(X): return df -# TODO: Move in v0.11.0 -@deprecated( - version="0.10.0", - reason="MrSQMClassifier will be moved to the dictionary_based package in version " - "0.11.0 at the request of the author.", - category=FutureWarning, -) class MrSQMClassifier(BaseClassifier): """ Multiple Representations Sequence Miner (MrSQM) classifier. @@ -46,7 +38,7 @@ class MrSQMClassifier(BaseClassifier): MrSQM is not included in all extras as it requires gcc and fftw (http://www.fftw.org/index.html) to be installed for Windows and some Linux OS. - Overview: MrSQM is an efficient time series classifier utilising symbolic + Overview: MrSQM is a time series classifier utilising symbolic representations of time series. MrSQM implements four different feature selection strategies (R,S,RS,SR) that can quickly select subsequences from multiple symbolic representations of time series data. @@ -92,7 +84,7 @@ class MrSQMClassifier(BaseClassifier): Examples -------- - >>> from aeon.classification.shapelet_based import MrSQMClassifier + >>> from aeon.classification.dictionary_based import MrSQMClassifier >>> from aeon.testing.data_generation import make_example_3d_numpy >>> X, y = make_example_3d_numpy(random_state=0) >>> clf = MrSQMClassifier(random_state=0) # doctest: +SKIP @@ -102,7 +94,7 @@ class MrSQMClassifier(BaseClassifier): """ _tags = { - "X_inner_type": "numpy3D", # we don't like this, but it's the only input! + "X_inner_type": "numpy3D", "algorithm_type": "shapelet", "cant-pickle": True, "python_dependencies": "mrsqm", diff --git a/aeon/classification/shapelet_based/__init__.py b/aeon/classification/shapelet_based/__init__.py index 28702fe52f..3b76ddddec 100644 --- a/aeon/classification/shapelet_based/__init__.py +++ b/aeon/classification/shapelet_based/__init__.py @@ -1,7 +1,6 @@ """Shapelet based time series classifiers.""" __all__ = [ - "MrSQMClassifier", "ShapeletTransformClassifier", "RDSTClassifier", "SASTClassifier", @@ -10,7 +9,6 @@ ] from aeon.classification.shapelet_based._ls import LearningShapeletClassifier -from aeon.classification.shapelet_based._mrsqm import MrSQMClassifier from aeon.classification.shapelet_based._rdst import RDSTClassifier from aeon.classification.shapelet_based._rsast import RSASTClassifier from aeon.classification.shapelet_based._sast import SASTClassifier diff --git a/aeon/clustering/_k_means.py b/aeon/clustering/_k_means.py index c7fadd2f8e..8c46a5df9c 100644 --- a/aeon/clustering/_k_means.py +++ b/aeon/clustering/_k_means.py @@ -326,6 +326,9 @@ def _check_params(self, X: np.ndarray) -> None: # Invalid distance passed for ba so default to dba self._average_params["distance"] = "dtw" + if "random_state" not in self._average_params: + self._average_params["random_state"] = self._random_state + self._averaging_method = _resolve_average_callable(self.averaging_method) if self.n_clusters > X.shape[0]: diff --git a/aeon/clustering/deep_learning/_ae_fcn.py b/aeon/clustering/deep_learning/_ae_fcn.py index 322a614a01..a9f33751ce 100644 --- a/aeon/clustering/deep_learning/_ae_fcn.py +++ b/aeon/clustering/deep_learning/_ae_fcn.py @@ -1,6 +1,6 @@ """Deep Learning Auto-Encoder using FCN Network.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["AEFCNClusterer"] import gc diff --git a/aeon/clustering/deep_learning/_ae_resnet.py b/aeon/clustering/deep_learning/_ae_resnet.py index 19ab56549c..2d1ccf13e0 100644 --- a/aeon/clustering/deep_learning/_ae_resnet.py +++ b/aeon/clustering/deep_learning/_ae_resnet.py @@ -1,6 +1,6 @@ """Residual Network (ResNet) for clustering.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["AEResNetClusterer"] import gc diff --git a/aeon/clustering/deep_learning/tests/test_deep_clusterer_base.py b/aeon/clustering/deep_learning/tests/test_deep_clusterer_base.py index cc8e952e85..421aa69a85 100644 --- a/aeon/clustering/deep_learning/tests/test_deep_clusterer_base.py +++ b/aeon/clustering/deep_learning/tests/test_deep_clusterer_base.py @@ -9,7 +9,7 @@ from aeon.testing.mock_estimators import MockDeepClusterer from aeon.utils.validation._dependencies import _check_soft_dependencies -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] @pytest.mark.skipif( diff --git a/aeon/datasets/_data_loaders.py b/aeon/datasets/_data_loaders.py index 4f16e02eea..19f83ff52b 100644 --- a/aeon/datasets/_data_loaders.py +++ b/aeon/datasets/_data_loaders.py @@ -962,7 +962,7 @@ def load_forecasting(name, extract_path=None, return_metadata=False): extract_path : optional (default = None) Path of the location for the data file. If none, data is written to os.path.dirname(__file__)/data/ - return_metadata : boolean, default = True + return_metadata : boolean, default = False If True, returns a tuple (data, metadata) Returns diff --git a/aeon/forecasting/compose/_reduce.py b/aeon/forecasting/compose/_reduce.py index 05335e5217..8a6a1551cc 100644 --- a/aeon/forecasting/compose/_reduce.py +++ b/aeon/forecasting/compose/_reduce.py @@ -27,7 +27,7 @@ from aeon.forecasting.base._fh import _index_range from aeon.regression.base import BaseRegressor from aeon.transformations._legacy.compose import FeatureUnion -from aeon.transformations.summarize import WindowSummarizer +from aeon.transformations._legacy.summarize import WindowSummarizer from aeon.utils.datetime import _shift from aeon.utils.index_functions import get_time_index from aeon.utils.sklearn import is_sklearn_regressor @@ -1839,7 +1839,7 @@ def _predict(self, X=None, fh=None): def _fit_shifted(self, y, X=None, fh=None): """Fit to training data.""" - from aeon.transformations.lag import Lag, ReducerTransform + from aeon.transformations._legacy.lag import Lag, ReducerTransform impute_method = self.impute_method lags = self._lags @@ -1918,7 +1918,7 @@ def _predict_shifted(self, fh=None, X=None): def _fit_concurrent(self, y, X=None, fh=None): """Fit to training data.""" - from aeon.transformations.lag import Lag, ReducerTransform + from aeon.transformations._legacy.lag import Lag, ReducerTransform impute_method = self.impute_method @@ -1983,7 +1983,7 @@ def _fit_concurrent(self, y, X=None, fh=None): def _predict_concurrent(self, X=None, fh=None): """Fit to training data.""" - from aeon.transformations.lag import Lag + from aeon.transformations._legacy.lag import Lag if X is not None and self._X is not None: X_pool = X.combine_first(self._X) @@ -2178,8 +2178,8 @@ def _fit(self, y, X=None, fh=None): ------- self : reference to self """ - from aeon.transformations._legacy.impute import Imputer - from aeon.transformations.lag import Lag + from aeon.transformations._legacy.lag import Lag + from aeon.transformations.impute import Imputer impute_method = self.impute_method @@ -2265,8 +2265,8 @@ def _predict(self, X=None, fh=None): def _predict_out_of_sample(self, X_pool, fh): """Recursive reducer: predict out of sample (ahead of cutoff).""" # very similar to _predict_concurrent of DirectReductionForecaster - refactor? - from aeon.transformations._legacy.impute import Imputer - from aeon.transformations.lag import Lag + from aeon.transformations._legacy.lag import Lag + from aeon.transformations.impute import Imputer fh_idx = self._get_expected_pred_idx(fh=fh) y_cols = self._y.columns @@ -2332,8 +2332,8 @@ def _predict_out_of_sample(self, X_pool, fh): def _predict_in_sample(self, X_pool, fh): """Recursive reducer: predict out of sample (in past of of cutoff).""" - from aeon.transformations._legacy.impute import Imputer - from aeon.transformations.lag import Lag + from aeon.transformations._legacy.lag import Lag + from aeon.transformations.impute import Imputer fh_idx = self._get_expected_pred_idx(fh=fh) y_cols = self._y.columns diff --git a/aeon/forecasting/compose/tests/test_pipeline.py b/aeon/forecasting/compose/tests/test_pipeline.py index 82e75b36ce..d0bc059a4c 100644 --- a/aeon/forecasting/compose/tests/test_pipeline.py +++ b/aeon/forecasting/compose/tests/test_pipeline.py @@ -19,10 +19,10 @@ from aeon.testing.mock_estimators import MockForecaster, MockTransformer from aeon.testing.utils.estimator_checks import _assert_array_almost_equal from aeon.transformations._legacy.adapt import TabularToSeriesAdaptor -from aeon.transformations._legacy.impute import Imputer +from aeon.transformations._legacy.outlier_detection import HampelFilter from aeon.transformations.detrend import Detrender from aeon.transformations.hierarchical.aggregate import Aggregator -from aeon.transformations.outlier_detection import HampelFilter +from aeon.transformations.impute import Imputer from aeon.utils.index_functions import get_window from aeon.utils.validation._dependencies import _check_soft_dependencies diff --git a/aeon/forecasting/compose/tests/test_reduce_global.py b/aeon/forecasting/compose/tests/test_reduce_global.py deleted file mode 100644 index 042f6a9cfb..0000000000 --- a/aeon/forecasting/compose/tests/test_reduce_global.py +++ /dev/null @@ -1,306 +0,0 @@ -"""Test extraction of features across (shifted) windows.""" - -__maintainer__ = [] - -from aeon.utils.validation._dependencies import _check_soft_dependencies - -# HistGradientBoostingRegressor requires experimental flag in old sklearn versions -if _check_soft_dependencies("sklearn<1.0", severity="none"): - from sklearn.experimental import enable_hist_gradient_boosting # noqa - -import numpy as np -import pandas as pd -import pytest -from sklearn.ensemble import RandomForestRegressor -from sklearn.linear_model import LinearRegression -from sklearn.pipeline import make_pipeline - -from aeon.datasets import load_airline -from aeon.forecasting.base import ForecastingHorizon -from aeon.forecasting.compose import make_reduction -from aeon.forecasting.model_selection import temporal_train_test_split -from aeon.performance_metrics.forecasting import mean_absolute_percentage_error -from aeon.testing.data_generation import _make_hierarchical -from aeon.testing.data_generation._legacy import get_examples -from aeon.transformations.summarize import WindowSummarizer - -# Load data that will be the basis of tests -y = load_airline() -y_multi = get_examples("pd-multiindex")[0] - -# y Train will be univariate data set -y_train, y_test = temporal_train_test_split(y) - -# y_int = y.copy() -# y_int.index = [i for i in range(len(y_int))] - -# y_train_int, y_test_int = temporal_train_test_split(y_int) - -# Create traina nd test panel sample data -mi = pd.MultiIndex.from_product([[0], y_train.index], names=["instances", "timepoints"]) -y_group1 = pd.DataFrame(y_train.values, index=mi, columns=["y"]) - -mi = pd.MultiIndex.from_product([[1], y_train.index], names=["instances", "timepoints"]) -y_group2 = pd.DataFrame(y_train.values, index=mi, columns=["y"]) - -y_train_grp = pd.concat([y_group1, y_group2]) - -mi = pd.MultiIndex.from_product([[0], y_test.index], names=["instances", "timepoints"]) -y_group1 = pd.DataFrame(y_test.values, index=mi, columns=["y"]) - -mi = pd.MultiIndex.from_product([[1], y_test.index], names=["instances", "timepoints"]) -y_group2 = pd.DataFrame(y_test.values, index=mi, columns=["y"]) - -y_test_grp = pd.concat([y_group1, y_group2]) - -# Get hierachical data -y_train_hier = get_examples("pd_multiindex_hier")[0] - -# Create unbalanced hierachical data -X = y_train_hier.reset_index().copy() -X = X[~((X["bar"] == 2) & (X["foo"] == "b"))] -X = X[["foo", "bar"]].drop_duplicates() - -time_names = y_train.index.names[-1] -timeframe = y_train.index.to_frame() - -X2 = X.merge(timeframe, how="cross") - -freq_inferred = y_train.index.freq - -x_names = X.columns -if not isinstance(x_names, list): - x_names = x_names.to_list() - -y_train_reset = y_train.reset_index() - -X3 = X2.merge(y_train_reset, on="Period") - -freq_inferred = y_train.index.freq - -y_train_hier_unequal = X3.groupby(x_names, as_index=True).apply( - lambda df: df.drop(x_names, axis=1).set_index(time_names).asfreq(freq_inferred) -) - -# Create integer index data -y_numeric = y_train.copy() -y_numeric.index = pd.to_numeric(y_numeric.index) - - -# Get different WindowSummarizer functions -kwargs = WindowSummarizer.get_test_params()[0] -kwargs_alternames = WindowSummarizer.get_test_params()[1] -kwargs_variant = WindowSummarizer.get_test_params()[2] - - -def check_eval(test_input, expected): - """Test which columns are returned for different arguments. - - For a detailed description what these arguments do, - and how theyinteract see docstring of DateTimeFeatures. - """ - if test_input is not None: - assert len(test_input) == len(expected) - assert all([a == b for a, b in zip(test_input, expected)]) - else: - assert expected is None - - -@pytest.mark.parametrize( - "y, index_names", - [ - ( - y_train_grp, - ["instances", "timepoints"], - ), - ( - y_train, - [None], - ), - ( - y_numeric, - [None], - ), - ( - y_train_hier_unequal, - ["foo", "bar", "Period"], - ), - ], -) -def test_recursive_reduction(y, index_names): - """Test index columns match input.""" - regressor = make_pipeline( - RandomForestRegressor(random_state=1), - ) - - forecaster2 = make_reduction( - regressor, - scitype="tabular-regressor", - transformers=[WindowSummarizer(**kwargs, n_jobs=1)], - window_length=None, - strategy="recursive", - pooling="global", - ) - - forecaster2.fit(y, fh=[1, 2]) - y_pred = forecaster2.predict(fh=[1, 2, 12]) - check_eval(y_pred.index.names, index_names) - - -@pytest.mark.parametrize( - "y, index_names", - [ - ( - y_train_grp, - ["instances", "timepoints"], - ), - ( - y_train, - [None], - ), - ( - y_numeric, - [None], - ), - ( - y_train_hier_unequal, - ["foo", "bar", "Period"], - ), - ], -) -def test_direct_reduction(y, index_names): - """Test index columns match input.""" - regressor = make_pipeline( - RandomForestRegressor(random_state=1), - ) - - forecaster2 = make_reduction( - regressor, - transformers=[WindowSummarizer(**kwargs, n_jobs=1)], - window_length=None, - strategy="recursive", - pooling="global", - ) - - forecaster2.fit(y, fh=[1, 2]) - y_pred = forecaster2.predict(fh=[1, 2, 12]) - check_eval(y_pred.index.names, index_names) - - -@pytest.mark.parametrize( - "y, index_names", - [ - ( - y_train_grp, - ["instances", "timepoints"], - ), - ( - y_train, - [None], - ), - ( - y_numeric, - [None], - ), - ( - y_train_hier_unequal, - ["foo", "bar", "Period"], - ), - ], -) -def test_list_reduction(y, index_names): - """Test index columns match input.""" - regressor = make_pipeline( - RandomForestRegressor(random_state=1), - ) - - forecaster2 = make_reduction( - regressor, - transformers=[WindowSummarizer(**kwargs), WindowSummarizer(**kwargs_variant)], - window_length=None, - strategy="recursive", - pooling="global", - ) - - forecaster2.fit(y, fh=[1, 2, 12]) - y_pred = forecaster2.predict(fh=[1, 2, 12]) - check_eval(y_pred.index.names, index_names) - - -def test_equality_transfo_nontranso(): - """Test that recursive reducers return same results for global / local forecasts.""" - y = load_airline()[:36] - y_train, y_test = temporal_train_test_split(y, test_size=12) - fh = ForecastingHorizon(y_test.index, is_relative=False) - - lag_vec = list(range(12, 0, -1)) - kwargs = { - "lag_feature": { - "lag": lag_vec, - } - } - - regressor = RandomForestRegressor(random_state=42) - - forecaster = make_reduction(regressor, window_length=12, strategy="recursive") - forecaster.fit(y_train) - y_pred = forecaster.predict(fh) - recursive_without = mean_absolute_percentage_error(y_test, y_pred, symmetric=False) - forecaster = make_reduction( - regressor, - window_length=None, - strategy="recursive", - transformers=[WindowSummarizer(**kwargs, n_jobs=1)], - pooling="global", - ) - - forecaster.fit(y_train) - y_pred = forecaster.predict(fh) - recursive_global = mean_absolute_percentage_error(y_test, y_pred, symmetric=False) - np.testing.assert_almost_equal(recursive_without, recursive_global) - - -def test_nofreq_pass(): - """Test that recursive reducers return same results with / without freq given.""" - regressor = make_pipeline( - LinearRegression(), - ) - - kwargs = { - "lag_feature": { - "lag": [1], - } - } - - forecaster_global = make_reduction( - regressor, - scitype="tabular-regressor", - transformers=[WindowSummarizer(**kwargs, n_jobs=1, truncate="bfill")], - window_length=None, - strategy="recursive", - pooling="global", - ) - - forecaster_global_freq = make_reduction( - regressor, - scitype="tabular-regressor", - transformers=[WindowSummarizer(**kwargs, n_jobs=1, truncate="bfill")], - window_length=None, - strategy="recursive", - pooling="global", - ) - - y = _make_hierarchical( - hierarchy_levels=(100,), min_timepoints=1000, max_timepoints=1000 - ) - - y_no_freq = y.reset_index().set_index(["h0", "time"]) - - forecaster_global.fit(y) - forecaster_global_freq.fit(y_no_freq) - - y_pred_global = forecaster_global.predict(fh=[1, 2]) - y_pred_nofreq = forecaster_global_freq.predict(fh=[1, 2]) - np.testing.assert_almost_equal( - y_pred_global["c0"].values, y_pred_nofreq["c0"].values - ) diff --git a/aeon/networks/__init__.py b/aeon/networks/__init__.py index b7ec3ca75a..df56152dea 100644 --- a/aeon/networks/__init__.py +++ b/aeon/networks/__init__.py @@ -4,6 +4,7 @@ "BaseDeepNetwork", "BaseDeepLearningNetwork", "CNNNetwork", + "TimeCNNNetwork", "EncoderNetwork", "FCNNetwork", "InceptionNetwork", @@ -18,7 +19,7 @@ from aeon.networks._ae_bgru import AEBiGRUNetwork from aeon.networks._ae_fcn import AEFCNNetwork from aeon.networks._ae_resnet import AEResNetNetwork -from aeon.networks._cnn import CNNNetwork +from aeon.networks._cnn import CNNNetwork, TimeCNNNetwork from aeon.networks._encoder import EncoderNetwork from aeon.networks._fcn import FCNNetwork from aeon.networks._inception import InceptionNetwork diff --git a/aeon/networks/_ae_bgru.py b/aeon/networks/_ae_bgru.py index 7fc2616f5f..5e2e78a71e 100644 --- a/aeon/networks/_ae_bgru.py +++ b/aeon/networks/_ae_bgru.py @@ -1,4 +1,6 @@ -"""Implement Auto-Encoder based on Bidirectional GRUs.""" +"""Auto-Encoder using Bidirectional GRU Network (AEBiGRUNetwork).""" + +__maintainer__ = ["aadya940", "hadifawaz1999"] from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_ae_fcn.py b/aeon/networks/_ae_fcn.py index 37bbdf1aa5..1cef7d5c31 100644 --- a/aeon/networks/_ae_fcn.py +++ b/aeon/networks/_ae_fcn.py @@ -1,6 +1,6 @@ """Auto-Encoder using Fully Convolutional Network (FCN).""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] import numpy as np diff --git a/aeon/networks/_ae_resnet.py b/aeon/networks/_ae_resnet.py index 3b817efe8e..4578dd8092 100644 --- a/aeon/networks/_ae_resnet.py +++ b/aeon/networks/_ae_resnet.py @@ -1,4 +1,7 @@ -"""Residual Network (ResNet) (minus the final output layer).""" +"""Auto-Encoder using Residual Network (AEResNetNetwork).""" + +__maintainer__ = ["hadifawaz1999"] + import numpy as np diff --git a/aeon/networks/_cnn.py b/aeon/networks/_cnn.py index cb40b8f373..aafb9875c4 100644 --- a/aeon/networks/_cnn.py +++ b/aeon/networks/_cnn.py @@ -1,10 +1,19 @@ -"""Time Convolutional Neural Network (CNN) (minus the final output layer).""" +"""Time Convolutional Neural Network (TimeCNNNetwork).""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] + +from deprecated.sphinx import deprecated from aeon.networks.base import BaseDeepLearningNetwork +# TODO: remove v0.12.0 +@deprecated( + version="0.10.0", + reason="CNNNetwork has been renamed to TimeCNNNetwork" + "and will be removed in 0.12.0.", + category=FutureWarning, +) class CNNNetwork(BaseDeepLearningNetwork): """Establish the network structure for a CNN. @@ -167,3 +176,167 @@ def build_network(self, input_shape, **kwargs): flatten_layer = tf.keras.layers.Flatten()(conv) return input_layer, flatten_layer + + +class TimeCNNNetwork(BaseDeepLearningNetwork): + """Establish the network structure for a CNN. + + Adapted from the implementation used in [1]_. + + Parameters + ---------- + n_layers : int, default = 2 + The number of convolution layers in the network. + kernel_size : int or list of int, default = 7 + Kernel size of convolution layers, if not a list, the same kernel size is + used for all layer, len(list) should be n_layers. + n_filters : int or list of int, default = [6, 12] + Number of filters for each convolution layer, if not a list, the same + `n_filters` is used in all layers. + avg_pool_size : int or list of int, default = 3 + The size of the average pooling layer, if not a list, the same max pooling + size is used for all convolution layer. + activation : str or list of str, default = "sigmoid" + Keras activation function used in the model for each layer, if not a list, + the same activation is used for all layers. + padding : str or list of str, default = "valid" + The method of padding in convolution layers, if not a list, the same padding + used for all convolution layers. + strides : int or list of int, default = 1 + The strides of kernels in the convolution and max pooling layers, if not a list, + the same strides are used for all layers. + dilation_rate : int or list of int, default = 1 + The dilation rate of the convolution layers, if not a list, the same dilation + rate is used all over the network. + use_bias : bool or list of bool, default = True + Condition on whether or not to use bias values for convolution layers, if not + a list, the same condition is used for all layers. + + Notes + ----- + Adapted from source code + https://github.com/hfawaz/dl-4-tsc/blob/master/classifiers/cnn.py + + References + ---------- + .. [1] Zhao et al. Convolutional neural networks for time series classification, + Journal of Systems Engineering and Electronics 28(1), 162--169, 2017 + """ + + def __init__( + self, + n_layers=2, + kernel_size=7, + n_filters=None, + avg_pool_size=3, + activation="sigmoid", + padding="valid", + strides=1, + dilation_rate=1, + use_bias=True, + ): + self.n_layers = n_layers + self.n_filters = n_filters + self.kernel_size = kernel_size + self.avg_pool_size = avg_pool_size + self.activation = activation + self.padding = padding + self.strides = strides + self.dilation_rate = dilation_rate + self.use_bias = use_bias + + super().__init__() + + def build_network(self, input_shape, **kwargs): + """ + Construct a network and return its input and output layers. + + Parameters + ---------- + input_shape : tuple + The shape of the data fed into the input layer. + + Returns + ------- + input_layer : a keras layer + output_layer : a keras layer + """ + import tensorflow as tf + + self._n_filters_ = [6, 12] if self.n_filters is None else self.n_filters + + if isinstance(self.kernel_size, list): + assert len(self.kernel_size) == self.n_layers + self._kernel_size = self.kernel_size + else: + self._kernel_size = [self.kernel_size] * self.n_layers + + if isinstance(self._n_filters_, list): + assert len(self._n_filters_) == self.n_layers + self._n_filters = self._n_filters_ + else: + self._n_filters = [self._n_filters_] * self.n_layers + + if isinstance(self.avg_pool_size, list): + assert len(self.avg_pool_size) == self.n_layers + self._avg_pool_size = self.avg_pool_size + else: + self._avg_pool_size = [self.avg_pool_size] * self.n_layers + + if isinstance(self.activation, list): + assert len(self.activation) == self.n_layers + self._activation = self.activation + else: + self._activation = [self.activation] * self.n_layers + + if isinstance(self.padding, list): + assert len(self.padding) == self.n_layers + self._padding = self.padding + else: + self._padding = [self.padding] * self.n_layers + + if isinstance(self.strides, list): + assert len(self.strides) == self.n_layers + self._strides = self.strides + else: + self._strides = [self.strides] * self.n_layers + + if isinstance(self.dilation_rate, list): + assert len(self.dilation_rate) == self.n_layers + self._dilation_rate = self.dilation_rate + else: + self._dilation_rate = [self.dilation_rate] * self.n_layers + + if isinstance(self.use_bias, list): + assert len(self.use_bias) == self.n_layers + self._use_bias = self.use_bias + else: + self._use_bias = [self.use_bias] * self.n_layers + + input_layer = tf.keras.layers.Input(input_shape) + + if input_shape[0] < 60: + self._padding = ["same"] * self.n_layers + + x = input_layer + + for i in range(self.n_layers): + conv = tf.keras.layers.Conv1D( + filters=self._n_filters[i], + kernel_size=self._kernel_size[i], + strides=self._strides[i], + padding=self._padding[i], + dilation_rate=self._dilation_rate[i], + activation=self._activation[i], + use_bias=self._use_bias[i], + )(x) + + conv = tf.keras.layers.AveragePooling1D(pool_size=self._avg_pool_size[i])( + conv + ) + + x = conv + + flatten_layer = tf.keras.layers.Flatten()(conv) + + return input_layer, flatten_layer diff --git a/aeon/networks/_encoder.py b/aeon/networks/_encoder.py index cc600aa932..e2423fc03c 100644 --- a/aeon/networks/_encoder.py +++ b/aeon/networks/_encoder.py @@ -1,4 +1,4 @@ -"""Encoder Classifier.""" +"""Encoder Network (EncoderNetwork).""" __maintainer__ = ["hadifawaz1999"] @@ -46,7 +46,7 @@ class EncoderNetwork(BaseDeepLearningNetwork): """ _config = { - "python_dependencies": ["tensorflow", "tensorflow-addons"], + "python_dependencies": ["tensorflow"], "python_version": "<3.12", "structure": "encoder", } diff --git a/aeon/networks/_fcn.py b/aeon/networks/_fcn.py index 9b6c10de96..fa4e3f763a 100644 --- a/aeon/networks/_fcn.py +++ b/aeon/networks/_fcn.py @@ -1,6 +1,7 @@ -"""Fully Convolutional Network (FCN) (minus the final output layer).""" +"""Fully Convolutional Network (FCNNetwork).""" + +__maintainer__ = ["hadifawaz1999"] -__maintainer__ = [] from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_inception.py b/aeon/networks/_inception.py index 1b209dd8dc..4c8abaa449 100644 --- a/aeon/networks/_inception.py +++ b/aeon/networks/_inception.py @@ -1,6 +1,6 @@ -"""Inception Network.""" +"""Inception Network (InceptionNetwork).""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_lite.py b/aeon/networks/_lite.py index 64a5961ab4..df19fba0d0 100644 --- a/aeon/networks/_lite.py +++ b/aeon/networks/_lite.py @@ -1,6 +1,7 @@ -"""LITE Network.""" +"""LITE Network (LITENetwork).""" + +__maintainer__ = ["hadifawaz1999"] -__maintainer__ = [] from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_mlp.py b/aeon/networks/_mlp.py index 537fadce0b..84b1570ff7 100644 --- a/aeon/networks/_mlp.py +++ b/aeon/networks/_mlp.py @@ -1,6 +1,7 @@ -"""Multi Layer Perceptron (MLP) (minus the final output layer).""" +"""Multi Layer Perceptron Network (MLPNetwork).""" + +__maintainer__ = ["hadifawaz1999"] -__maintainer__ = [] from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_resnet.py b/aeon/networks/_resnet.py index 91aaf5b154..d1ab38883a 100644 --- a/aeon/networks/_resnet.py +++ b/aeon/networks/_resnet.py @@ -1,6 +1,7 @@ -"""Residual Network (ResNet) (minus the final output layer).""" +"""Residual Network (ResNetNetwork).""" + +__maintainer__ = ["hadifawaz1999"] -__maintainer__ = [] from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_tapnet.py b/aeon/networks/_tapnet.py index 4e06d942c3..720925f2d2 100644 --- a/aeon/networks/_tapnet.py +++ b/aeon/networks/_tapnet.py @@ -1,6 +1,6 @@ -"""Time Convolutional Neural Network (CNN) (minus the final output layer).""" +"""Time series Attentional Prototype Network (TapNetNetwork).""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] import math diff --git a/aeon/networks/tests/test_all_networks.py b/aeon/networks/tests/test_all_networks.py index 37ce19bf07..106a5b8b4f 100644 --- a/aeon/networks/tests/test_all_networks.py +++ b/aeon/networks/tests/test_all_networks.py @@ -35,7 +35,7 @@ def test_all_networks_functionality(network): """Test the functionality of all networks.""" input_shape = (100, 2) - if not (network.__name__ in ["BaseDeepLearningNetwork", "EncoderNetwork"]): + if not (network.__name__ in ["BaseDeepLearningNetwork"]): if _check_soft_dependencies( network._config["python_dependencies"], severity="none" ) and _check_python_version(network._config["python_version"], severity="none"): @@ -62,3 +62,75 @@ def test_all_networks_functionality(network): ) else: pytest.skip(f"{network.__name__} not to be tested since its a base class.") + + +@pytest.mark.parametrize("network", _networks) +def test_all_networks_params(network): + """Test the functionality of all networks.""" + input_shape = (100, 2) + + if network.__name__ in ["BaseDeepLearningNetwork", "EncoderNetwork"]: + pytest.skip(f"{network.__name__} not to be tested since its a base class.") + + if network._config["structure"] == "auto-encoder": + pytest.skip( + f"{network.__name__} not to be tested (AE networks have their own tests)." + ) + + if not ( + _check_soft_dependencies( + network._config["python_dependencies"], severity="none" + ) + and _check_python_version(network._config["python_version"], severity="none") + ): + pytest.skip( + f"{network.__name__} dependencies not satisfied or invalid \ + Python version." + ) + + # check with default parameters + my_network = network() + my_network.build_network(input_shape=input_shape) + + # check with list parameters + params = dict() + for attrname in [ + "kernel_size", + "n_filters", + "avg_pool_size", + "activation", + "padding", + "strides", + "dilation_rate", + "use_bias", + ]: + + # Exceptions to fix + if ( + attrname in ["kernel_size", "padding"] + and network.__name__ == "TapNetNetwork" + ): + continue + # LITENetwork does not seem to work with list args + if network.__name__ == "LITENetwork": + continue + + # Here we use 'None' string as default to differentiate with None values + attr = getattr(my_network, attrname, "None") + if attr != "None": + if attr is None: + attr = 3 + elif isinstance(attr, list): + attr = attr[0] + else: + if network.__name__ in ["ResNetNetwork"]: + attr = [attr] * my_network.n_conv_per_residual_block + elif network.__name__ in ["InceptionNetwork"]: + attr = [attr] * my_network.depth + else: + attr = [attr] * my_network.n_layers + params[attrname] = attr + + if params: + my_network = network(**params) + my_network.build_network(input_shape=input_shape) diff --git a/aeon/networks/tests/test_cnn.py b/aeon/networks/tests/test_cnn.py new file mode 100644 index 0000000000..c859397b34 --- /dev/null +++ b/aeon/networks/tests/test_cnn.py @@ -0,0 +1,22 @@ +"""Tests for the CNN Model.""" + +import pytest + +from aeon.networks import TimeCNNNetwork +from aeon.utils.validation._dependencies import _check_soft_dependencies + +__maintainer__ = [] + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_cnn_input_shape_padding(): + """Test of CNN network with input_shape < 60.""" + input_shape = (40, 2) + network = TimeCNNNetwork() + input_layer, output_layer = network.build_network(input_shape=input_shape) + + assert input_layer is not None + assert output_layer is not None diff --git a/aeon/networks/tests/test_inception.py b/aeon/networks/tests/test_inception.py new file mode 100644 index 0000000000..afd046883f --- /dev/null +++ b/aeon/networks/tests/test_inception.py @@ -0,0 +1,74 @@ +"""Tests for the Inception Model.""" + +import pytest + +from aeon.networks import InceptionNetwork +from aeon.utils.validation._dependencies import _check_soft_dependencies + +__maintainer__ = [] + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_inceptionnetwork_bottleneck(): + """Test of Inception network without bottleneck.""" + input_shape = (100, 2) + inception = InceptionNetwork(use_bottleneck=False) + input_layer, output_layer = inception.build_network(input_shape=input_shape) + + assert input_layer is not None + assert output_layer is not None + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_inceptionnetwork_max_pooling(): + """Test of Inception network without max pooling.""" + input_shape = (100, 2) + inception = InceptionNetwork(use_max_pooling=False) + input_layer, output_layer = inception.build_network(input_shape=input_shape) + + assert input_layer is not None + assert output_layer is not None + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_inceptionnetwork_custom_filters(): + """Test of Inception network with custom filters.""" + input_shape = (100, 2) + inception = InceptionNetwork(use_custom_filters=True) + input_layer, output_layer = inception.build_network(input_shape=input_shape) + + assert input_layer is not None + assert output_layer is not None + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_inceptionnetwork_list_parameters(): + """Test of Inception network with list parameters not in test_all_networks.""" + input_shape = (100, 2) + depth = 6 + n_conv_per_layer = [3] * depth + use_max_pooling = [True] * depth + max_pool_size = [3] * depth + + inception = InceptionNetwork( + depth=depth, + n_conv_per_layer=n_conv_per_layer, + use_max_pooling=use_max_pooling, + max_pool_size=max_pool_size, + ) + input_layer, output_layer = inception.build_network(input_shape=input_shape) + + assert input_layer is not None + assert output_layer is not None diff --git a/aeon/regression/deep_learning/__init__.py b/aeon/regression/deep_learning/__init__.py index 13077e96e8..eb69f1e0f0 100644 --- a/aeon/regression/deep_learning/__init__.py +++ b/aeon/regression/deep_learning/__init__.py @@ -1,7 +1,9 @@ """Deep learning based regressors.""" __all__ = [ + "BaseDeepRegressor", "CNNRegressor", + "TimeCNNRegressor", "FCNRegressor", "InceptionTimeRegressor", "IndividualInceptionRegressor", @@ -13,7 +15,7 @@ "MLPRegressor", ] -from aeon.regression.deep_learning._cnn import CNNRegressor +from aeon.regression.deep_learning._cnn import CNNRegressor, TimeCNNRegressor from aeon.regression.deep_learning._encoder import EncoderRegressor from aeon.regression.deep_learning._fcn import FCNRegressor from aeon.regression.deep_learning._inception_time import ( @@ -27,3 +29,4 @@ from aeon.regression.deep_learning._mlp import MLPRegressor from aeon.regression.deep_learning._resnet import ResNetRegressor from aeon.regression.deep_learning._tapnet import TapNetRegressor +from aeon.regression.deep_learning.base import BaseDeepRegressor diff --git a/aeon/regression/deep_learning/_cnn.py b/aeon/regression/deep_learning/_cnn.py index c5854b2438..988e823c63 100644 --- a/aeon/regression/deep_learning/_cnn.py +++ b/aeon/regression/deep_learning/_cnn.py @@ -1,19 +1,27 @@ -"""Time Convolutional Neural Network (CNN) for regression.""" +"""Time Convolutional Neural Network (TimeCNN) regressor.""" -__maintainer__ = [] -__all__ = ["CNNRegressor"] +__maintainer__ = ["hadifawaz1999"] +__all__ = ["CNNRegressor", "TimeCNNRegressor"] import gc import os import time from copy import deepcopy +from deprecated.sphinx import deprecated from sklearn.utils import check_random_state -from aeon.networks import CNNNetwork +from aeon.networks import CNNNetwork, TimeCNNNetwork from aeon.regression.deep_learning.base import BaseDeepRegressor +# TODO: remove v0.12.0 +@deprecated( + version="0.10.0", + reason="CNNRegressor has been renamed to TimeCNNRegressor" + "and will be removed in 0.12.0.", + category=FutureWarning, +) class CNNRegressor(BaseDeepRegressor): """Time Series Convolutional Neural Network (CNN). @@ -86,6 +94,8 @@ class CNNRegressor(BaseDeepRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -94,6 +104,9 @@ class CNNRegressor(BaseDeepRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. Notes ----- @@ -133,8 +146,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", verbose=False, loss="mse", output_activation="linear", @@ -151,7 +166,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.strides = strides self.dilation_rate = dilation_rate self.callbacks = callbacks @@ -253,6 +270,337 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + + if self.verbose: + self.training_model_.summary() + + self.file_name_ = ( + self.best_file_name if self.save_best_model else str(time.time_ns()) + ) + + if self.callbacks is None: + self.callbacks_ = [ + tf.keras.callbacks.ModelCheckpoint( + filepath=self.file_path + self.file_name_ + ".keras", + monitor="loss", + save_best_only=True, + ), + ] + else: + self.callbacks_ = self._get_model_checkpoint_callback( + callbacks=self.callbacks, + file_path=self.file_path, + file_name=self.file_name_, + ) + + self.history = self.training_model_.fit( + X, + y, + batch_size=self.batch_size, + epochs=self.n_epochs, + verbose=self.verbose, + callbacks=self.callbacks_, + ) + + try: + self.model_ = tf.keras.models.load_model( + self.file_path + self.file_name_ + ".keras", compile=False + ) + if not self.save_best_model: + os.remove(self.file_path + self.file_name_ + ".keras") + except FileNotFoundError: + self.model_ = deepcopy(self.training_model_) + + if self.save_last_model: + self.save_last_model_to_file(file_path=self.file_path) + + gc.collect() + return self + + @classmethod + def get_test_params(cls, parameter_set="default"): + """Return testing parameter settings for the estimator. + + Parameters + ---------- + parameter_set : str, default="default" + Name of the set of test parameters to return, for use in tests. If no + special parameters are defined for a value, will return `"default"` set. + For regressors, a "default" set of parameters should be provided for + general testing, and a "results_comparison" set for comparing against + previously recorded results if the general set does not produce suitable + probabilities to compare against. + + Returns + ------- + params : dict or list of dict, default={} + Parameters to create testing instances of the class. + Each dict are parameters to construct an "interesting" test instance, i.e., + `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance. + `create_test_instance` uses the first (or only) dictionary in `params`. + """ + param = { + "n_epochs": 10, + "batch_size": 4, + "avg_pool_size": 4, + } + + return [param] + + +class TimeCNNRegressor(BaseDeepRegressor): + """Time Series Convolutional Neural Network (CNN). + + Adapted from the implementation used in [1]_. + + Parameters + ---------- + n_layers : int, default = 2, + the number of convolution layers in the network + kernel_size : int or list of int, default = 7, + kernel size of convolution layers, if not a list, the same kernel size + is used for all layer, len(list) should be n_layers + n_filters : int or list of int, default = [6, 12], + number of filters for each convolution layer, if not a list, the same n_filters + is used in all layers. + avg_pool_size : int or list of int, default = 3, + the size of the average pooling layer, if not a list, the same + max pooling size is used + for all convolution layer + output_activation : str, default = "linear", + the output activation for the regressor + activation : str or list of str, default = "sigmoid", + keras activation function used in the model for each layer, + if not a list, the same + activation is used for all layers + padding : str or list of str, default = 'valid', + the method of padding in convolution layers, if not a list, + the same padding used + for all convolution layers + strides : int or list of int, default = 1, + the strides of kernels in the convolution and max pooling layers, + if not a list, the same strides are used for all layers + dilation_rate : int or list of int, default = 1, + the dilation rate of the convolution layers, if not a list, + the same dilation rate is used all over the network + use_bias : bool or list of bool, default = True, + condition on whether or not to use bias values for convolution layers, + if not a list, the same condition is used for all layers + random_state : int, RandomState instance or None, default=None + If `int`, random_state is the seed used by the random number generator; + If `RandomState` instance, random_state is the random number generator; + If `None`, the random number generator is the `RandomState` instance used + by `np.random`. + Seeded random number generation can only be guaranteed on CPU processing, + GPU processing will be non-deterministic. + n_epochs : int, default = 2000 + the number of epochs to train the model + batch_size : int, default = 16 + the number of samples per gradient update. + verbose : boolean, default = False + whether to output extra information + loss : string, default="mean_squared_error" + fit parameter for the keras model + optimizer : keras.optimizer, default=keras.optimizers.Adam(), + metrics : str or list of str, default="mean_squared_error" + The evaluation metrics to use during training. If + a single string metric is provided, it will be + used as the only metric. If a list of metrics are + provided, all will be used for evaluation. + callbacks : keras.callbacks, default=model_checkpoint to save best + model on training loss + file_path : file_path for the best model (if checkpoint is used as callback) + save_best_model : bool, default = False + Whether or not to save the best model, if the + modelcheckpoint callback is used by default, + this condition, if True, will prevent the + automatic deletion of the best saved model from + file and the user can choose the file name + save_last_model : bool, default = False + Whether or not to save the last model, last + epoch trained, using the base class method + save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. + best_file_name : str, default = "best_model" + The name of the file of the best model, if + save_best_model is set to False, this parameter + is discarded + last_file_name : str, default = "last_model" + The name of the file of the last model, if + save_last_model is set to False, this parameter + is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. + + Notes + ----- + Adapted from the implementation from Fawaz et. al + https://github.com/hfawaz/dl-4-tsc/blob/master/classifiers/cnn.py + + References + ---------- + .. [1] Zhao et. al, Convolutional neural networks for time series classification, + Journal of Systems Engineering and Electronics, 28(1):2017. + + Examples + -------- + >>> from aeon.regression.deep_learning import TimeCNNRegressor + >>> from aeon.testing.data_generation import make_example_3d_numpy + >>> X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=12, + ... return_y=True, regression_target=True, + ... random_state=0) + >>> rgs = TimeCNNRegressor(n_epochs=20, bacth_size=4) # doctest: +SKIP + >>> rgs.fit(X, y) # doctest: +SKIP + TimeCNNRegressor(...) + """ + + def __init__( + self, + n_layers=2, + kernel_size=7, + n_filters=None, + avg_pool_size=3, + activation="sigmoid", + padding="valid", + strides=1, + dilation_rate=1, + n_epochs=2000, + batch_size=16, + callbacks=None, + file_path="./", + save_best_model=False, + save_last_model=False, + save_init_model=False, + best_file_name="best_model", + last_file_name="last_model", + init_file_name="init_model", + verbose=False, + loss="mse", + output_activation="linear", + metrics="mean_squared_error", + random_state=None, + use_bias=True, + optimizer=None, + ): + self.n_layers = n_layers + self.avg_pool_size = avg_pool_size + self.padding = padding + self.n_filters = n_filters + self.kernel_size = kernel_size + self.file_path = file_path + self.save_best_model = save_best_model + self.save_last_model = save_last_model + self.save_init_model = save_init_model + self.best_file_name = best_file_name + self.init_file_name = init_file_name + self.strides = strides + self.dilation_rate = dilation_rate + self.callbacks = callbacks + self.n_epochs = n_epochs + self.verbose = verbose + self.loss = loss + self.output_activation = output_activation + self.metrics = metrics + self.random_state = random_state + self.activation = activation + self.use_bias = use_bias + self.optimizer = optimizer + + self.history = None + + super().__init__( + batch_size=batch_size, + last_file_name=last_file_name, + ) + + self._network = TimeCNNNetwork( + n_layers=self.n_layers, + kernel_size=self.kernel_size, + n_filters=self.n_filters, + avg_pool_size=self.avg_pool_size, + activation=self.activation, + padding=self.padding, + strides=self.strides, + dilation_rate=self.dilation_rate, + use_bias=self.use_bias, + ) + + def build_model(self, input_shape, **kwargs): + """Construct a compiled, un-trained, keras model that is ready for training. + + In aeon, time series are stored in numpy arrays of shape (d,m), where d + is the number of dimensions, m is the series length. Keras/tensorflow assume + data is in shape (m,d). This method also assumes (m,d). Transpose should + happen in fit. + + Parameters + ---------- + input_shape : tuple + The shape of the data fed into the input layer, should be (m,d) + + Returns + ------- + output : a compiled Keras Model + """ + import numpy as np + import tensorflow as tf + from tensorflow import keras + + rng = check_random_state(self.random_state) + self.random_state_ = rng.randint(0, np.iinfo(np.int32).max) + tf.keras.utils.set_random_seed(self.random_state_) + input_layer, output_layer = self._network.build_network(input_shape, **kwargs) + + output_layer = keras.layers.Dense(units=1, activation=self.output_activation)( + output_layer + ) + + self.optimizer_ = ( + keras.optimizers.Adam() if self.optimizer is None else self.optimizer + ) + + model = keras.models.Model(inputs=input_layer, outputs=output_layer) + + model.compile( + loss=self.loss, + optimizer=self.optimizer_, + metrics=self._metrics, + ) + return model + + def _fit(self, X, y): + """Fit the regressor on the training set (X, y). + + Parameters + ---------- + X : np.ndarray + The training input samples of shape (n_cases, n_channels, n_timepoints). + y : np.ndarray + The training data target values of shape (n_cases,). + + Returns + ------- + self : object + """ + import tensorflow as tf + + # Transpose to conform to Keras input style. + X = X.transpose(0, 2, 1) + + if isinstance(self.metrics, str): + self._metrics = [self.metrics] + else: + self._metrics = self.metrics + self.input_shape = X.shape[1:] + self.training_model_ = self.build_model(self.input_shape) + + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/regression/deep_learning/_encoder.py b/aeon/regression/deep_learning/_encoder.py index be65733d0f..4b73047c05 100644 --- a/aeon/regression/deep_learning/_encoder.py +++ b/aeon/regression/deep_learning/_encoder.py @@ -1,6 +1,6 @@ """Encoder Regressor.""" -__author__ = ["AnonymousCodes911"] +__author__ = ["AnonymousCodes911", "hadifawaz1999"] __all__ = ["EncoderRegressor"] import gc @@ -54,6 +54,8 @@ class EncoderRegressor(BaseDeepRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file. + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -62,6 +64,9 @@ class EncoderRegressor(BaseDeepRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded. + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. n_epochs: The number of times the entire training dataset will be passed forward and backward @@ -121,8 +126,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", verbose=False, loss="mean_squared_error", metrics="mean_squared_error", @@ -144,7 +151,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.n_epochs = n_epochs self.verbose = verbose self.loss = loss @@ -239,6 +248,9 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/regression/deep_learning/_fcn.py b/aeon/regression/deep_learning/_fcn.py index 7c136cae34..374feb5b93 100644 --- a/aeon/regression/deep_learning/_fcn.py +++ b/aeon/regression/deep_learning/_fcn.py @@ -1,6 +1,6 @@ -"""Fully Convolutional Network (FCN) for regression.""" +"""Fully Convolutional Network (FCN) regressor.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["FCNRegressor"] import gc @@ -75,6 +75,8 @@ class FCNRegressor(BaseDeepRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -83,6 +85,9 @@ class FCNRegressor(BaseDeepRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. callbacks : keras.callbacks, default = None Notes @@ -119,8 +124,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", n_epochs=2000, batch_size=16, use_mini_batch_size=False, @@ -153,7 +160,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.history = None @@ -239,6 +248,9 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/regression/deep_learning/_inception_time.py b/aeon/regression/deep_learning/_inception_time.py index dc14749d01..a5bd459545 100644 --- a/aeon/regression/deep_learning/_inception_time.py +++ b/aeon/regression/deep_learning/_inception_time.py @@ -1,6 +1,6 @@ -"""InceptionTime regressor.""" +"""InceptionTime and Inception regressors.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["InceptionTimeRegressor"] import gc @@ -107,6 +107,8 @@ class InceptionTimeRegressor(BaseRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -115,6 +117,9 @@ class InceptionTimeRegressor(BaseRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -187,8 +192,10 @@ def __init__( file_path="./", save_last_model=False, save_best_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -223,8 +230,10 @@ def __init__( self.save_last_model = save_last_model self.save_best_model = save_best_model + self.save_init_model = save_init_model self.best_file_name = best_file_name self.last_file_name = last_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.random_state = random_state @@ -275,8 +284,10 @@ def _fit(self, X, y): file_path=self.file_path, save_best_model=self.save_best_model, save_last_model=self.save_last_model, + save_init_model=self.save_init_model, best_file_name=self.best_file_name + str(n), last_file_name=self.last_file_name + str(n), + init_file_name=self.init_file_name + str(n), batch_size=self.batch_size, use_mini_batch_size=self.use_mini_batch_size, n_epochs=self.n_epochs, @@ -424,6 +435,8 @@ class IndividualInceptionRegressor(BaseDeepRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -432,6 +445,9 @@ class IndividualInceptionRegressor(BaseDeepRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -492,8 +508,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -526,7 +544,9 @@ def __init__( self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.random_state = random_state @@ -626,6 +646,9 @@ def _fit(self, X, y): mini_batch_size = self.batch_size self.training_model_ = self.build_model(self.input_shape_) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/regression/deep_learning/_lite_time.py b/aeon/regression/deep_learning/_lite_time.py index 8b2e32f200..5a2079df94 100644 --- a/aeon/regression/deep_learning/_lite_time.py +++ b/aeon/regression/deep_learning/_lite_time.py @@ -1,6 +1,6 @@ -"""LITETime Regressor.""" +"""LITETime and LITE regressors.""" -__author__ = ["aadya940"] +__author__ = ["aadya940", "hadifawaz1999"] __all__ = ["IndividualLITERegressor", "LITETimeRegressor"] import gc @@ -61,6 +61,8 @@ class LITETimeRegressor(BaseRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -69,6 +71,9 @@ class LITETimeRegressor(BaseRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -122,8 +127,10 @@ def __init__( file_path="./", save_last_model=False, save_best_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -149,8 +156,10 @@ def __init__( self.save_last_model = save_last_model self.save_best_model = save_best_model + self.save_init_model = save_init_model self.best_file_name = best_file_name self.last_file_name = last_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.random_state = random_state @@ -188,8 +197,10 @@ def _fit(self, X, y): file_path=self.file_path, save_best_model=self.save_best_model, save_last_model=self.save_last_model, + save_init_model=self.save_init_model, best_file_name=self.best_file_name + str(n), last_file_name=self.last_file_name + str(n), + init_file_name=self.init_file_name + str(n), batch_size=self.batch_size, use_mini_batch_size=self.use_mini_batch_size, n_epochs=self.n_epochs, @@ -302,6 +313,8 @@ class IndividualLITERegressor(BaseDeepRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -310,6 +323,9 @@ class IndividualLITERegressor(BaseDeepRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -354,8 +370,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", batch_size=64, use_mini_batch_size=False, n_epochs=1500, @@ -379,7 +397,9 @@ def __init__( self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.callbacks = callbacks self.random_state = random_state @@ -476,6 +496,9 @@ def _fit(self, X, y): mini_batch_size = self.batch_size self.training_model_ = self.build_model(self.input_shape) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/regression/deep_learning/_mlp.py b/aeon/regression/deep_learning/_mlp.py index a8883f3f12..cb9907fe7c 100644 --- a/aeon/regression/deep_learning/_mlp.py +++ b/aeon/regression/deep_learning/_mlp.py @@ -1,6 +1,6 @@ -"""Multi Layer Perceptron Network (MLP) for Regression.""" +"""Multi Layer Perceptron Network (MLP) regressor.""" -__author__ = ["Aadya-Chinubhai"] +__author__ = ["Aadya-Chinubhai", "hadifawaz1999"] __all__ = ["MLPRegressor"] import gc @@ -47,6 +47,8 @@ class MLPRegressor(BaseDeepRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -55,6 +57,9 @@ class MLPRegressor(BaseDeepRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -100,8 +105,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", random_state=None, activation="relu", output_activation="linear", @@ -118,7 +125,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.optimizer = optimizer self.random_state = random_state self.output_activation = output_activation @@ -202,6 +211,9 @@ def _fit(self, X, y): self.training_model_ = self.build_model(self.input_shape) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/regression/deep_learning/_resnet.py b/aeon/regression/deep_learning/_resnet.py index b9a411891c..48f2d3c5f8 100644 --- a/aeon/regression/deep_learning/_resnet.py +++ b/aeon/regression/deep_learning/_resnet.py @@ -1,6 +1,6 @@ -"""Residual Network (ResNet) for regression.""" +"""Residual Network (ResNet) regressor.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = ["ResNetRegressor"] import gc @@ -82,6 +82,8 @@ class ResNetRegressor(BaseDeepRegressor): Whether or not to save the last model, last epoch trained, using the base class method save_last_model_to_file + save_init_model : bool, default = False + Whether to save the initialization of the model. best_file_name : str, default = "best_model" The name of the file of the best model, if save_best_model is set to False, this parameter @@ -90,6 +92,9 @@ class ResNetRegressor(BaseDeepRegressor): The name of the file of the last model, if save_last_model is set to False, this parameter is discarded + init_file_name : str, default = "init_model" + The name of the file of the init model, if save_init_model is set to False, + this parameter is discarded. verbose : boolean, default = False whether to output extra information loss : string, default="mean_squared_error" @@ -147,8 +152,10 @@ def __init__( file_path="./", save_best_model=False, save_last_model=False, + save_init_model=False, best_file_name="best_model", last_file_name="last_model", + init_file_name="init_model", optimizer=None, ): self.n_residual_blocks = n_residual_blocks @@ -171,7 +178,9 @@ def __init__( self.file_path = file_path self.save_best_model = save_best_model self.save_last_model = save_last_model + self.save_init_model = save_init_model self.best_file_name = best_file_name + self.init_file_name = init_file_name self.optimizer = optimizer self.history = None @@ -261,6 +270,9 @@ def _fit(self, X, y): self.input_shape = X.shape[1:] self.training_model_ = self.build_model(self.input_shape) + if self.save_init_model: + self.training_model_.save(self.file_path + self.init_file_name + ".keras") + if self.verbose: self.training_model_.summary() diff --git a/aeon/regression/deep_learning/_tapnet.py b/aeon/regression/deep_learning/_tapnet.py index 380d2ee533..4a03f02c66 100644 --- a/aeon/regression/deep_learning/_tapnet.py +++ b/aeon/regression/deep_learning/_tapnet.py @@ -1,6 +1,6 @@ -"""Time Convolutional Neural Network (CNN) for classification.""" +"""Time series Attentional Prototype Network (TapNet) regressor.""" -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] __all__ = [ "TapNetRegressor", ] diff --git a/aeon/regression/deep_learning/tests/test_deep_regressor_base.py b/aeon/regression/deep_learning/tests/test_deep_regressor_base.py index 000b39173d..9dad88f0b2 100644 --- a/aeon/regression/deep_learning/tests/test_deep_regressor_base.py +++ b/aeon/regression/deep_learning/tests/test_deep_regressor_base.py @@ -10,7 +10,7 @@ from aeon.testing.data_generation import make_example_2d_numpy_collection from aeon.utils.validation._dependencies import _check_soft_dependencies -__maintainer__ = [] +__maintainer__ = ["hadifawaz1999"] class _DummyDeepRegressor(BaseDeepRegressor): diff --git a/aeon/regression/deep_learning/tests/test_saving_loading_deep_learning_cls.py b/aeon/regression/deep_learning/tests/test_saving_loading_deep_learning_cls.py new file mode 100644 index 0000000000..736d99baf3 --- /dev/null +++ b/aeon/regression/deep_learning/tests/test_saving_loading_deep_learning_cls.py @@ -0,0 +1,79 @@ +"""Unit tests for regressors deep learners save/load functionalities.""" + +import inspect +import os +import tempfile +import time + +import pytest + +from aeon.regression import deep_learning +from aeon.testing.data_generation import make_example_3d_numpy +from aeon.utils.validation._dependencies import _check_soft_dependencies + +__maintainer__ = ["hadifawaz1999"] + + +_deep_rgs_classes = [ + member[1] for member in inspect.getmembers(deep_learning, inspect.isclass) +] + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="skip test if required soft dependency not available", +) +@pytest.mark.parametrize("deep_rgs", _deep_rgs_classes) +def test_saving_loading_deep_learning_rgs(deep_rgs): + """Test Deep Regressor saving.""" + with tempfile.TemporaryDirectory() as tmp: + if not ( + deep_rgs.__name__ + in [ + "BaseDeepRegressor", + "InceptionTimeRegressor", + "LITETimeRegressor", + "TapNetRegressor", + ] + ): + if tmp[-1] != "/": + tmp = tmp + "/" + curr_time = str(time.time_ns()) + last_file_name = curr_time + "last" + best_file_name = curr_time + "best" + init_file_name = curr_time + "init" + + X, y = make_example_3d_numpy() + + deep_rgs_train = deep_rgs( + n_epochs=2, + save_best_model=True, + save_last_model=True, + save_init_model=True, + best_file_name=best_file_name, + last_file_name=last_file_name, + init_file_name=init_file_name, + file_path=tmp, + ) + deep_rgs_train.fit(X, y) + + deep_rgs_best = deep_rgs() + deep_rgs_best.load_model( + model_path=os.path.join(tmp, best_file_name + ".keras"), + ) + ypred_best = deep_rgs_best.predict(X) + assert len(ypred_best) == len(y) + + deep_rgs_last = deep_rgs() + deep_rgs_last.load_model( + model_path=os.path.join(tmp, last_file_name + ".keras"), + ) + ypred_last = deep_rgs_last.predict(X) + assert len(ypred_last) == len(y) + + deep_rgs_init = deep_rgs() + deep_rgs_init.load_model( + model_path=os.path.join(tmp, init_file_name + ".keras"), + ) + ypred_init = deep_rgs_init.predict(X) + assert len(ypred_init) == len(y) diff --git a/aeon/testing/expected_results/classifier_results_reproduction.py b/aeon/testing/expected_results/classifier_results_reproduction.py index 06c5231d2e..441a496442 100644 --- a/aeon/testing/expected_results/classifier_results_reproduction.py +++ b/aeon/testing/expected_results/classifier_results_reproduction.py @@ -23,6 +23,7 @@ WEASEL_V2, BOSSEnsemble, ContractableBOSS, + MrSQMClassifier, TemporalDictionaryEnsemble, ) from aeon.classification.distance_based import ( @@ -56,7 +57,6 @@ from aeon.classification.ordinal_classification import OrdinalTDE from aeon.classification.shapelet_based import ( LearningShapeletClassifier, - MrSQMClassifier, SASTClassifier, ShapeletTransformClassifier, ) diff --git a/aeon/testing/expected_results/regressor_results_reproduction.py b/aeon/testing/expected_results/regressor_results_reproduction.py index d60feb2067..5c47340ef7 100644 --- a/aeon/testing/expected_results/regressor_results_reproduction.py +++ b/aeon/testing/expected_results/regressor_results_reproduction.py @@ -7,7 +7,7 @@ from aeon.regression.convolution_based import ( HydraRegressor, MultiRocketHydraRegressor, - RocketRegressor + RocketRegressor, ) from aeon.regression.distance_based import KNeighborsTimeSeriesRegressor from aeon.regression.feature_based import ( @@ -25,9 +25,8 @@ RandomIntervalSpectralEnsembleRegressor, TimeSeriesForestRegressor, ) -from aeon.regression.shapelet_based import ( - RDSTRegressor, -) +from aeon.regression.shapelet_based import RDSTRegressor + def _reproduce_regression_covid_3month(estimator): X_train, y_train = load_covid_3month(split="train") diff --git a/aeon/transformations/lag.py b/aeon/transformations/_legacy/lag.py similarity index 98% rename from aeon/transformations/lag.py rename to aeon/transformations/_legacy/lag.py index 93d14666fe..ff3ef76d0b 100644 --- a/aeon/transformations/lag.py +++ b/aeon/transformations/_legacy/lag.py @@ -6,7 +6,6 @@ import numpy as np import pandas as pd -from deprecated.sphinx import deprecated from pandas.api.types import is_integer_dtype from aeon.transformations.base import BaseTransformer @@ -24,12 +23,6 @@ def _coerce_to_int(obj): return obj -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="Lag will be removed in version 0.11.0.", - category=FutureWarning, -) class Lag(BaseTransformer): """Lagging transformer. Lags time series by one or multiple lags. @@ -298,12 +291,6 @@ def get_test_params(cls, parameter_set="default"): return [params1, params2, params3] -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="ReducerTransform will be removed in version 0.11.0.", - category=FutureWarning, -) class ReducerTransform(BaseTransformer): """Transformer for forecasting reduction. Prepares tabular X/y via lag and trafos. diff --git a/aeon/transformations/outlier_detection.py b/aeon/transformations/_legacy/outlier_detection.py similarity index 93% rename from aeon/transformations/outlier_detection.py rename to aeon/transformations/_legacy/outlier_detection.py index 81c8e8b40a..26b9826aa0 100644 --- a/aeon/transformations/outlier_detection.py +++ b/aeon/transformations/_legacy/outlier_detection.py @@ -7,18 +7,11 @@ import numpy as np import pandas as pd -from deprecated.sphinx import deprecated from aeon.forecasting.model_selection import SlidingWindowSplitter from aeon.transformations.base import BaseTransformer -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="HampelFilter will be removed in version 0.11.0.", - category=FutureWarning, -) class HampelFilter(BaseTransformer): """Use HampelFilter to detect outliers based on a sliding window. @@ -46,14 +39,6 @@ class HampelFilter(BaseTransformer): ---------- .. [1] Hampel F. R., "The influence curve and its role in robust estimation", Journal of the American Statistical Association, 69, 382–393, 1974 - - Examples - -------- - >>> from aeon.transformations.outlier_detection import HampelFilter - >>> from aeon.datasets import load_airline - >>> y = load_airline() - >>> transformer = HampelFilter(window_length=10) - >>> y_hat = transformer.fit_transform(y) """ _tags = { diff --git a/aeon/transformations/_legacy/subset.py b/aeon/transformations/_legacy/subset.py index 48facac3b5..e8dea367d3 100644 --- a/aeon/transformations/_legacy/subset.py +++ b/aeon/transformations/_legacy/subset.py @@ -8,106 +8,6 @@ from aeon.transformations.base import BaseTransformer -class _IndexSubset(BaseTransformer): - r"""Index subsetting transformer. - - In transform, subsets `X` to the indices in `y.index`. - If `y` is None, returns `X` without subsetting. - numpy-based `X` are interpreted as having a RangeIndex starting at n, - where n is the number of numpy rows seen so far through `fit` and `update`. - Non-pandas types are interpreted as having index as after conversion to the - `"pd.DataFrame"` aeon type. - - Parameters - ---------- - index_treatment : str, optional, one of "keep" (default) or "remove" - determines which indices are kept in `Xt = transform(X, y)` - "keep" = all indices in y also appear in Xt. If not present in X, NA is filled. - "remove" = only indices that appear in both X and y are present in Xt. - """ - - _tags = { - "input_data_type": "Series", - # what is the abstract type of X: Series, or Panel - "output_data_type": "Series", - # what abstract type is returned: Primitives, Series, Panel - "instancewise": True, # is this an instance-wise transform? - "X_inner_type": ["pd.DataFrame", "pd.Series"], - "y_inner_type": ["pd.DataFrame", "pd.Series"], - "transform-returns-same-time-index": False, - "fit_is_empty": False, - "capability:multivariate": True, - "capability:inverse_transform": False, - "remember_data": True, # remember all data seen as _X - } - - def __init__(self, index_treatment="keep"): - self.index_treatment = index_treatment - super().__init__() - - def _transform(self, X, y=None): - """Transform X and return a transformed version. - - private _transform containing the core logic, called from transform - - Parameters - ---------- - X : pd.DataFrame or pd.Series - Data to be transformed - y : pd.DataFrame or pd.Series - Additional data, e.g., labels for transformation - - Returns - ------- - Xt : pd.DataFrame or pd.Series, same type as X - transformed version of X - """ - if y is None: - return X - - X = self._X - - index_treatment = self.index_treatment - ind_X_and_y = X.index.intersection(y.index) - - if index_treatment == "remove": - Xt = X.loc[ind_X_and_y] - elif index_treatment == "keep": - Xt = X.loc[ind_X_and_y] - y_idx_frame = type(X)(index=y.index) - Xt = Xt.combine_first(y_idx_frame) - else: - raise ValueError( - f'index_treatment must be one of "remove", "keep", but found' - f' "{index_treatment}"' - ) - return Xt - - @classmethod - def get_test_params(cls, parameter_set="default"): - """Return testing parameter settings for the estimator. - - Parameters - ---------- - parameter_set : str, default="default" - Name of the set of test parameters to return, for use in tests. If no - special parameters are defined for a value, will return `"default"` set. - There are currently no reserved values for transformers. - - Returns - ------- - params : dict or list of dict, default = {} - Parameters to create testing instances of the class - Each dict are parameters to construct an "interesting" test instance, i.e., - `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance. - `create_test_instance` uses the first (or only) dictionary in `params` - """ - params1 = {"index_treatment": "remove"} - params2 = {"index_treatment": "keep"} - - return [params1, params2] - - class _ColumnSelect(BaseTransformer): r"""Column selection transformer. diff --git a/aeon/transformations/summarize.py b/aeon/transformations/_legacy/summarize.py similarity index 91% rename from aeon/transformations/summarize.py rename to aeon/transformations/_legacy/summarize.py index edfbc28b37..3386cb9f68 100644 --- a/aeon/transformations/summarize.py +++ b/aeon/transformations/_legacy/summarize.py @@ -5,19 +5,12 @@ import numpy as np import pandas as pd -from deprecated.sphinx import deprecated from joblib import Parallel, delayed from aeon.transformations.base import BaseTransformer from aeon.utils.multiindex import flatten_multiindex -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="WindowSummarizer will be removed in version 0.11.0.", - category=FutureWarning, -) class WindowSummarizer(BaseTransformer): """ Transformer for extracting time series features. @@ -132,55 +125,6 @@ class WindowSummarizer(BaseTransformer): Contains all transformed columns as well as non-transformed columns. The raw inputs to transformed columns will be dropped. self: reference to self - - Examples - -------- - >>> import pandas as pd - >>> from aeon.transformations.summarize import WindowSummarizer - >>> from aeon.datasets import load_airline, load_longley - >>> from aeon.forecasting.naive import NaiveForecaster - >>> from aeon.forecasting.base import ForecastingHorizon - >>> from aeon.forecasting.compose import ForecastingPipeline - >>> from aeon.forecasting.model_selection import temporal_train_test_split - >>> y = load_airline() - >>> kwargs = { - ... "lag_feature": { - ... "lag": [1], - ... "mean": [[1, 3], [3, 6]], - ... "std": [[1, 4]], - ... } - ... } - >>> transformer = WindowSummarizer(**kwargs) - >>> y_transformed = transformer.fit_transform(y) - - Example with transforming multiple columns of exogeneous features - >>> y, X = load_longley() - >>> y_train, y_test, X_train, X_test = temporal_train_test_split(y, X) - >>> fh = ForecastingHorizon(X_test.index, is_relative=False) - >>> # Example transforming only X - >>> pipe = ForecastingPipeline( - ... steps=[ - ... ("a", WindowSummarizer(n_jobs=1, target_cols=["POP", "GNPDEFL"])), - ... ("b", WindowSummarizer(n_jobs=1, target_cols=["GNP"], **kwargs)), - ... ("forecaster", NaiveForecaster(strategy="drift")), - ... ] - ... ) - >>> pipe_return = pipe.fit(y_train, X_train) - >>> y_pred1 = pipe_return.predict(fh=fh, X=X_test) - - Example with transforming multiple columns of exogeneous features - as well as the y column - >>> Z_train = pd.concat([X_train, y_train], axis=1) - >>> Z_test = pd.concat([X_test, y_test], axis=1) - >>> pipe = ForecastingPipeline( - ... steps=[ - ... ("a", WindowSummarizer(n_jobs=1, target_cols=["POP", "TOTEMP"])), - ... ("b", WindowSummarizer(**kwargs, n_jobs=1, target_cols=["GNP"])), - ... ("forecaster", NaiveForecaster(strategy="drift")), - ... ] - ... ) - >>> pipe_return = pipe.fit(y_train, Z_train) - >>> y_pred2 = pipe_return.predict(fh=fh, X=Z_test) """ _tags = { @@ -594,12 +538,6 @@ def _check_quantiles(quantiles): return quantiles -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="SummaryTransformer will be removed in version 0.11.0.", - category=FutureWarning, -) class SummaryTransformer(BaseTransformer): """Calculate summary value of a time series. @@ -633,14 +571,6 @@ class SummaryTransformer(BaseTransformer): ----- This provides a wrapper around pandas DataFrame and Series agg and quantile methods. - - Examples - -------- - >>> from aeon.transformations.summarize import SummaryTransformer - >>> from aeon.datasets import load_airline - >>> y = load_airline() - >>> transformer = SummaryTransformer() - >>> y_mean = transformer.fit_transform(y) """ _tags = { @@ -742,12 +672,6 @@ def get_test_params(cls, parameter_set="default"): return [params1, params2, params3] -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="PlateauFinder will be removed in version 0.11.0.", - category=FutureWarning, -) class PlateauFinder(BaseTransformer): """ Plateau finder transformer. @@ -833,12 +757,6 @@ def _transform(self, X, y=None): return Xt -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="FittedParamExtractor will be removed in version 0.11.0.", - category=FutureWarning, -) class FittedParamExtractor(BaseTransformer): """Fitted parameter extractor. diff --git a/aeon/transformations/collection/convolution_based/tests/test_all_rockets.py b/aeon/transformations/collection/convolution_based/tests/test_all_rockets.py index 30dceecdad..b9439a1eaa 100644 --- a/aeon/transformations/collection/convolution_based/tests/test_all_rockets.py +++ b/aeon/transformations/collection/convolution_based/tests/test_all_rockets.py @@ -9,6 +9,7 @@ MultiRocket, Rocket, ) +from aeon.transformations.collection.convolution_based._minirocket import _PPV # Data used to test correctness of transform uni_test_data = np.array( @@ -213,3 +214,11 @@ def test_expected_basic_motions(): np.testing.assert_allclose( X2[:5, :5], expected_basic_motions["MiniRocket"], rtol=1e-4 ) + + +def test_ppv(): + """Test uncovered PPV function.""" + a = np.float32(10.0) + b = np.float32(-5.0) + assert _PPV(a, b) == 1 + assert _PPV(b, a) == 0 diff --git a/aeon/transformations/collection/dictionary_based/_sfa.py b/aeon/transformations/collection/dictionary_based/_sfa.py index 9bf638e3d5..df3f51604a 100644 --- a/aeon/transformations/collection/dictionary_based/_sfa.py +++ b/aeon/transformations/collection/dictionary_based/_sfa.py @@ -46,7 +46,7 @@ class SFA(BaseCollectionTransformer): Parameters ---------- word_length: int, default = 8 - length of word to shorten window to (using PAA) + length of word to shorten window to (using DFT) alphabet_size: int, default = 4 number of values to discretise each value to diff --git a/aeon/transformations/collection/dictionary_based/_sfa_fast.py b/aeon/transformations/collection/dictionary_based/_sfa_fast.py index 91d0e326e9..044217158e 100644 --- a/aeon/transformations/collection/dictionary_based/_sfa_fast.py +++ b/aeon/transformations/collection/dictionary_based/_sfa_fast.py @@ -59,7 +59,7 @@ class SFAFast(BaseCollectionTransformer): Parameters ---------- word_length : int, default = 8 - Length of word to shorten window to (using PAA). + Length of word to shorten window to (using DFT). alphabet_size : int, default = 4 Number of values to discretise each value to. window_size : int, default = 12 diff --git a/aeon/transformations/scaledlogit.py b/aeon/transformations/scaledlogit.py deleted file mode 100644 index 3a1f7f1a7d..0000000000 --- a/aeon/transformations/scaledlogit.py +++ /dev/null @@ -1,214 +0,0 @@ -"""Implements the scaled logit transformation.""" - -__maintainer__ = [] -__all__ = ["ScaledLogitTransformer"] - -from copy import deepcopy -from warnings import warn - -import numpy as np -from deprecated.sphinx import deprecated - -from aeon.transformations.base import BaseTransformer - - -# TODO: remove in v0.11.0 -@deprecated( - version="0.10.0", - reason="ScaledLogitTransformer will be removed in version 0.11.0.", - category=FutureWarning, -) -class ScaledLogitTransformer(BaseTransformer): - r"""Scaled logit transform or Log transform. - - If both lower_bound and upper_bound are not None, a scaled logit transform is - applied to the data. Otherwise, the transform applied is a log transform variation - that ensures the resulting values from the inverse transform are bounded - accordingly. The transform is applied to all scalar elements of the input array - individually. - - Combined with an aeon.forecasting.compose.TransformedTargetForecaster, it ensures - that the forecast stays between the specified bounds (lower_bound, upper_bound). - - Default is lower_bound = upper_bound = None, i.e., the identity transform. - - The logarithm transform is obtained for lower_bound = 0, upper_bound = None. - - Parameters - ---------- - lower_bound : float, optional, default=None - lower bound of inverse transform function - upper_bound : float, optional, default=None - upper bound of inverse transform function - - See Also - -------- - aeon.transformations.boxcox._LogTransformer : - Transformer input data using natural log. Can help normalize data and - compress variance of the series. - aeon.transformations.boxcox._BoxCoxTransformer : - Applies Box-Cox power transformation. Can help normalize data and - compress variance of the series. - aeon.transformations.exponent.ExponentTransformer : - Transform input data by raising it to an exponent. Can help compress - variance of series if a fractional exponent is supplied. - aeon.transformations.exponent.SqrtTransformer : - Transform input data by taking its square root. Can help compress - variance of input series. - - Notes - ----- - | The scaled logit transform is applied if both upper_bound and lower_bound are - | not None: - | :math:`log(\frac{x - a}{b - x})`, where a is the lower and b is the upper bound. - - | If upper_bound is None and lower_bound is not None the transform applied is - | a log transform of the form: - | :math:`log(x - a)` - - | If lower_bound is None and upper_bound is not None the transform applied is - | a log transform of the form: - | :math:`- log(b - x)` - - References - ---------- - .. [1] Hyndsight - Forecasting within limits: - https://robjhyndman.com/hyndsight/forecasting-within-limits/ - .. [2] Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and - practice, 3rd edition, OTexts: Melbourne, Australia. OTexts.com/fpp3. - Accessed on January 24th 2022. - - Examples - -------- - >>> import numpy as np - >>> from aeon.datasets import load_airline - >>> from aeon.transformations.scaledlogit import ScaledLogitTransformer - >>> from aeon.forecasting.trend import PolynomialTrendForecaster - >>> from aeon.forecasting.compose import TransformedTargetForecaster - >>> y = load_airline() - >>> fcaster = TransformedTargetForecaster([ - ... ("scaled_logit", ScaledLogitTransformer(0, 650)), - ... ("poly", PolynomialTrendForecaster(degree=2)) - ... ]) - >>> fcaster.fit(y) - TransformedTargetForecaster(...) - >>> y_pred = fcaster.predict(fh = np.arange(32)) - """ - - _tags = { - "input_data_type": "Series", - # what is the abstract type of X: Series, or Panel - "output_data_type": "Series", - # what abstract type is returned: Primitives, Series, Panel - "instancewise": True, # is this an instance-wise transform? - "X_inner_type": "np.ndarray", - "y_inner_type": "None", - "transform-returns-same-time-index": True, - "fit_is_empty": True, - "capability:multivariate": True, - "capability:inverse_transform": True, - "skip-inverse-transform": False, - } - - def __init__(self, lower_bound=None, upper_bound=None): - self.lower_bound = lower_bound - self.upper_bound = upper_bound - - super().__init__() - - def _transform(self, X, y=None): - """Transform X and return a transformed version. - - private _transform containing core logic, called from transform - - Parameters - ---------- - X : 2D np.ndarray - Data to be transformed - y : data structure of type y_inner_type, default=None - Ignored argument for interface compatibility - - Returns - ------- - transformed version of X - """ - if self.upper_bound is not None and np.any(X >= self.upper_bound): - warn( - "X in ScaledLogitTransformer should not have values " - "greater than upper_bound", - RuntimeWarning, - ) - - if self.lower_bound is not None and np.any(X <= self.lower_bound): - warn( - "X in ScaledLogitTransformer should not have values " - "lower than lower_bound", - RuntimeWarning, - ) - - if self.upper_bound and self.lower_bound: - X_transformed = np.log((X - self.lower_bound) / (self.upper_bound - X)) - elif self.upper_bound is not None: - X_transformed = -np.log(self.upper_bound - X) - elif self.lower_bound is not None: - X_transformed = np.log(X - self.lower_bound) - else: - X_transformed = deepcopy(X) - - return X_transformed - - def _inverse_transform(self, X, y=None): - """Inverse transform, inverse operation to transform. - - private _inverse_transform containing core logic, called from inverse_transform - - Parameters - ---------- - X : 2D np.ndarray - Data to be inverse transformed - y : data of y_inner_type, default=None - Ignored argument for interface compatibility - - Returns - ------- - inverse transformed version of X - """ - if self.upper_bound and self.lower_bound: - X_inv_transformed = (self.upper_bound * np.exp(X) + self.lower_bound) / ( - np.exp(X) + 1 - ) - elif self.upper_bound is not None: - X_inv_transformed = self.upper_bound - np.exp(-X) - elif self.lower_bound is not None: - X_inv_transformed = np.exp(X) + self.lower_bound - else: - X_inv_transformed = deepcopy(X) - - return X_inv_transformed - - @classmethod - def get_test_params(cls, parameter_set="default"): - """Return testing parameter settings for the estimator. - - Parameters - ---------- - parameter_set : str, default="default" - Name of the set of test parameters to return, for use in tests. If no - special parameters are defined for a value, will return `"default"` set. - - - Returns - ------- - params : dict or list of dict, default = {} - Parameters to create testing instances of the class - Each dict are parameters to construct an "interesting" test instance, i.e., - `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance. - `create_test_instance` uses the first (or only) dictionary in `params` - """ - test_params = [ - {"lower_bound": None, "upper_bound": None}, - {"lower_bound": -(10**6), "upper_bound": None}, - {"lower_bound": None, "upper_bound": 10**6}, - {"lower_bound": -(10**6), "upper_bound": 10**6}, - ] - return test_params diff --git a/aeon/transformations/tests/test_compose.py b/aeon/transformations/tests/test_compose.py index f25ecba802..c49f12a57d 100644 --- a/aeon/transformations/tests/test_compose.py +++ b/aeon/transformations/tests/test_compose.py @@ -19,13 +19,13 @@ OptionalPassthrough, TransformerPipeline, ) -from aeon.transformations._legacy.impute import Imputer from aeon.transformations._legacy.subset import _ColumnSelect +from aeon.transformations._legacy.summarize import SummaryTransformer from aeon.transformations._legacy.theta import ( _ThetaLinesTransformer as ThetaLinesTransformer, ) from aeon.transformations.collection.pad import PaddingTransformer -from aeon.transformations.summarize import SummaryTransformer +from aeon.transformations.impute import Imputer def test_dunder_mul(): diff --git a/aeon/transformations/tests/test_featureizer.py b/aeon/transformations/tests/test_featureizer.py deleted file mode 100644 index f6b4b3e03c..0000000000 --- a/aeon/transformations/tests/test_featureizer.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Test YtoX.""" - -__maintainer__ = [] - -from numpy.testing import assert_array_equal - -from aeon.datasets import load_longley -from aeon.forecasting.model_selection import temporal_train_test_split -from aeon.testing.mock_estimators import MockTransformer -from aeon.transformations._legacy.compose import YtoX -from aeon.transformations.lag import Lag - -y, X = load_longley() -y_train, y_test, X_train, X_test = temporal_train_test_split(y, X) - - -def test_featurized_values(): - """Test against plain transformation. - - Test to check that the featurized values are same as if transformation - is done without YtoX. - """ - lags = len(y_test) - featurizer = YtoX() * MockTransformer() * Lag(lags) - featurizer.fit(X_train, y_train) - X_hat = featurizer.transform(X_test, y_test) - - exp_transformer = MockTransformer() - expected_len = lags + len(y_test) - y_hat = exp_transformer.fit_transform(y[-expected_len:]) - assert_array_equal(X_hat[f"lag_{lags}__TOTEMP"].values, y_hat.values) diff --git a/aeon/transformations/tests/test_fittedparamextractor.py b/aeon/transformations/tests/test_fittedparamextractor.py deleted file mode 100644 index 524f49255d..0000000000 --- a/aeon/transformations/tests/test_fittedparamextractor.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tests for FittedParamExtractor.""" - -__maintainer__ = [] -__all__ = [] - -import pytest - -from aeon.datasets import load_gunpoint -from aeon.forecasting.exp_smoothing import ExponentialSmoothing -from aeon.transformations.summarize import FittedParamExtractor -from aeon.utils.validation._dependencies import _check_estimator_deps - -X_train, y_train = load_gunpoint("train", return_type="nested_univ") - - -@pytest.mark.skipif( - not _check_estimator_deps(ExponentialSmoothing, severity="none"), - reason="skip test if required soft dependency for hmmlearn not available", -) -@pytest.mark.parametrize("param_names", ["initial_level"]) -def test_fitted_param_extractor(param_names): - forecaster = ExponentialSmoothing() - t = FittedParamExtractor(forecaster=forecaster, param_names=param_names) - Xt = t.fit_transform(X_train) - assert Xt.shape == (X_train.shape[0], len(t._check_param_names(param_names))) - - # check specific value - forecaster.fit(X_train.iloc[47, 0]) - fitted_param = forecaster.get_fitted_params()[param_names] - assert Xt.iloc[47, 0] == fitted_param diff --git a/aeon/transformations/tests/test_lag.py b/aeon/transformations/tests/test_lag.py deleted file mode 100644 index 92e1c8ef78..0000000000 --- a/aeon/transformations/tests/test_lag.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Tests for Lag transformer.""" - -__maintainer__ = [] - -import itertools - -import pandas as pd -import pytest - -from aeon.testing.data_generation._legacy import get_examples, make_series -from aeon.transformations.lag import Lag - -# some examples with range vs time index, univariate vs multivariate (mv) -X_range_idx = get_examples("pd.DataFrame")[0] -X_range_idx_mv = get_examples("pd.DataFrame")[1] -X_time_idx = make_series() -X_time_idx_mv = make_series(n_columns=2) - -# all fixtures -X_fixtures = [X_range_idx, X_range_idx_mv, X_time_idx, X_time_idx_mv] - -# fixtures with time index -X_time_fixtures = [X_time_idx, X_time_idx_mv] - -index_outs = ["original", "extend", "shift"] - - -@pytest.mark.parametrize("X", X_fixtures) -@pytest.mark.parametrize("index_out", index_outs) -def test_lag_fit_transform_out_index(X, index_out): - """Test that index sets of fit_transform output behave as expected.""" - t = Lag(2, index_out=index_out) - Xt = t.fit_transform(X) - - if index_out == "original": - assert Xt.index.equals(X.index) - elif index_out == "extend": - assert X.index.isin(Xt.index).all() - assert len(Xt) == len(X) + 2 - elif index_out == "shift": - assert len(Xt) == len(X) - assert X.index[2:].isin(Xt.index).all() - - -@pytest.mark.parametrize("X", X_fixtures) -@pytest.mark.parametrize("index_out", index_outs) -@pytest.mark.parametrize("lag", [2, [2, 4], [-1, 0, 5]]) -def test_lag_fit_transform_columns(X, index_out, lag): - """Test that columns of fit_transform output behave as expected.""" - t = Lag(lags=lag, index_out=index_out) - Xt = t.fit_transform(X) - - if isinstance(lag, list): - len_lag = len(lag) - else: - len_lag = 1 - - def ncols(obj): - if isinstance(obj, pd.DataFrame): - return len(obj.columns) - else: - return 1 - - assert ncols(Xt) == ncols(X) * len_lag - - -@pytest.mark.parametrize("X", X_fixtures) -@pytest.mark.parametrize("index_out", index_outs) -@pytest.mark.parametrize("lags", [2, [2, 4]]) -def test_lag_fit_transform_column_names(X, index_out, lags): - """Test expected column names.""" - t = Lag(lags=lags, index_out=index_out) - Xt = t.fit_transform(X) - - if isinstance(Xt, pd.DataFrame): - lag_col_names = set(Xt.columns) - - if isinstance(X, pd.DataFrame): - col_names = X.columns - elif isinstance(X, pd.Series): - col_names = [X.name if X.name else 0] - else: - pass - - lags = [lags] if isinstance(lags, int) else lags - expected = { - f"lag_{lag}__{col}" for lag, col in itertools.product(lags, col_names) - } - assert lag_col_names == expected - - elif isinstance(Xt, pd.Series): - assert Xt.name is None diff --git a/aeon/transformations/tests/test_plateaufinder.py b/aeon/transformations/tests/test_plateaufinder.py index d94dac896e..b5ebd53f2b 100644 --- a/aeon/transformations/tests/test_plateaufinder.py +++ b/aeon/transformations/tests/test_plateaufinder.py @@ -4,7 +4,7 @@ import pandas as pd import pytest -from aeon.transformations.summarize import PlateauFinder +from aeon.transformations._legacy.summarize import PlateauFinder @pytest.mark.parametrize("value", [np.nan, -10, 10, -0.5, 0.5]) diff --git a/aeon/transformations/tests/test_rockets.py b/aeon/transformations/tests/test_rockets.py deleted file mode 100644 index cac8aeebb5..0000000000 --- a/aeon/transformations/tests/test_rockets.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Tests for the rocket transformers.""" - -import numpy as np - -from aeon.transformations.collection.convolution_based._minirocket import _PPV - - -def test_ppv(): - """Test uncovered PPV function.""" - a = np.float32(10.0) - b = np.float32(-5.0) - assert _PPV(a, b) == 1 - assert _PPV(b, a) == 0 diff --git a/aeon/transformations/tests/test_summarize.py b/aeon/transformations/tests/test_summarize.py deleted file mode 100644 index 6585b4f79b..0000000000 --- a/aeon/transformations/tests/test_summarize.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Test functionality of summary transformer.""" - -__maintainer__ = [] -import re - -import numpy as np -import pandas as pd -import pytest - -from aeon.testing.data_generation._legacy import make_series -from aeon.transformations.summarize import ALLOWED_SUM_FUNCS, SummaryTransformer - -# Test individual summary functions + lists and tuples of all summary functions -sum_funcs_to_test = [ALLOWED_SUM_FUNCS[0]] + [ALLOWED_SUM_FUNCS] -sum_funcs_to_test.append(tuple(ALLOWED_SUM_FUNCS)) -quantiles_to_test = [0.7, [0.5, 0.25, 0.1], (0.3, 0.0002, 0.99), None] - -# Incorrect inputs to test to ensure they raise expected errors -incorrect_sum_funcs_to_test = [ - "meen", - "md", - 0.25, - ["mean", "medain"], - np.array(["mean", "max"]), -] -incorrect_quantiles_to_test = [25, "0.25", "median", [0.25, 1.25], [0.25, "median"]] - -# Test functionality on pd.Series and pd.DataFrame (uni- and multi-variate) input -y1 = make_series(n_timepoints=75) -y2 = make_series(n_timepoints=75) -y1.name, y2.name = "y1", "y2" -y_df_uni = pd.DataFrame(y1) -y_df_multi = pd.concat([y1, y2], axis=1) -data_to_test = [y1, y_df_uni, y_df_multi] - - -@pytest.mark.parametrize("y", data_to_test) -@pytest.mark.parametrize("summary_arg", sum_funcs_to_test) -@pytest.mark.parametrize("quantile_arg", quantiles_to_test) -def test_summary_transformer_output_type(y, summary_arg, quantile_arg): - """Test whether output is DataFrame of correct dimensions.""" - transformer = SummaryTransformer( - summary_function=summary_arg, quantiles=quantile_arg - ) - transformer.fit(y) - yt = transformer.transform(y) - - output_is_dataframe = isinstance(yt, pd.DataFrame) - assert output_is_dataframe - - # compute number of expected rows and columns - - # all test cases are single series, so single row - expected_instances = 1 - - # expected number of feature types = quantiles plus summaries - expected_sum_features = 1 if isinstance(summary_arg, str) else len(summary_arg) - if quantile_arg is None: - expected_q_features = 0 - elif isinstance(quantile_arg, (int, float)): - expected_q_features = 1 - else: - expected_q_features = len(quantile_arg) - expected_features = expected_sum_features + expected_q_features - - # for multivariate series, columns = no variables * no feature types - if isinstance(y, pd.DataFrame): - expected_features = len(y.columns) * expected_features - - assert yt.shape == (expected_instances, expected_features) - - -@pytest.mark.parametrize("summary_arg", incorrect_sum_funcs_to_test) -def test_summary_transformer_incorrect_summary_function_raises_error(summary_arg): - """Test if correct errors are raised for invalid summary_function input.""" - msg = rf"""`summary_function` must be None, or str or a list or tuple made up of - {ALLOWED_SUM_FUNCS}. - """ - with pytest.raises(ValueError, match=re.escape(msg)): - transformer = SummaryTransformer(summary_function=summary_arg, quantiles=None) - transformer.fit_transform(data_to_test[0]) - - -@pytest.mark.parametrize("quantile_arg", incorrect_quantiles_to_test) -def test_summary_transformer_incorrect_quantile_raises_error(quantile_arg): - """Test if correct errors are raised for invalid quantiles input.""" - msg = """`quantiles` must be None, int, float or a list or tuple made up of - int and float values that are between 0 and 1. - """ - with pytest.raises(ValueError, match=msg): - transformer = SummaryTransformer( - summary_function="mean", quantiles=quantile_arg - ) - transformer.fit_transform(data_to_test[0]) diff --git a/aeon/transformations/tests/test_window_summarizer.py b/aeon/transformations/tests/test_window_summarizer.py deleted file mode 100644 index 636e3c78c6..0000000000 --- a/aeon/transformations/tests/test_window_summarizer.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Test extraction of features across (shifted) windows.""" - -__maintainer__ = [] - -import numpy as np -import pandas as pd -import pytest - -from aeon.datasets import load_airline, load_longley -from aeon.forecasting.model_selection import temporal_train_test_split -from aeon.testing.data_generation._legacy import get_examples -from aeon.transformations.summarize import WindowSummarizer - - -def check_eval(test_input, expected): - """Test which columns are returned for different arguments. - - For a detailed description what these arguments do, - and how theyinteract see docstring of DateTimeFeatures. - """ - if test_input is not None: - assert len(test_input) == len(expected) - assert all([a == b for a, b in zip(test_input, expected)]) - else: - assert expected is None - - -# Load data that will be the basis of tests -y = load_airline() -y_pd = get_examples("pd.DataFrame")[0] -y_series = get_examples("pd.Series")[0] -y_multi = get_examples("pd-multiindex")[0] -# y Train will be univariate data set -y_train, y_test = temporal_train_test_split(y) - -# Create Panel sample data -mi = pd.MultiIndex.from_product([[0], y.index], names=["instances", "timepoints"]) -y_group1 = pd.DataFrame(y.values, index=mi, columns=["y"]) - -mi = pd.MultiIndex.from_product([[1], y.index], names=["instances", "timepoints"]) -y_group2 = pd.DataFrame(y.values, index=mi, columns=["y"]) - -y_grouped = pd.concat([y_group1, y_group2]) - -mi = pd.MultiIndex.from_product([[0], [0], y.index], names=["h1", "h2", "time"]) -y_hier1 = pd.DataFrame(y.values, index=mi, columns=["y"]) - -mi = pd.MultiIndex.from_product([[0], [1], y.index], names=["h1", "h2", "time"]) -y_hier2 = pd.DataFrame(y.values, index=mi, columns=["y"]) - -mi = pd.MultiIndex.from_product([[1], [0], y.index], names=["h1", "h2", "time"]) -y_hier3 = pd.DataFrame(y.values, index=mi, columns=["y"]) - -mi = pd.MultiIndex.from_product([[1], [1], y.index], names=["h1", "h2", "time"]) -y_hier4 = pd.DataFrame(y.values, index=mi, columns=["y"]) - -y_hierarchical = pd.concat([y_hier1, y_hier2, y_hier3, y_hier4]) - -y_ll, X_ll = load_longley() -y_ll_train, _, X_ll_train, X_ll_test = temporal_train_test_split(y_ll, X_ll) - -# Get different WindowSummarizer functions -kwargs = WindowSummarizer.get_test_params()[0] -kwargs_alternames = WindowSummarizer.get_test_params()[1] -kwargs_variant = WindowSummarizer.get_test_params()[2] - - -def count_gt100(x): - """Count how many observations lie above threshold 100.""" - return np.sum((x > 100)[::-1]) - - -# Cannot be pickled in get_test_params, therefore here explicit -kwargs_custom = { - "lag_feature": { - count_gt100: [[3, 2]], - } -} -# Generate named and unnamed y -y_train.name = None -y_train_named = y_train.copy() -y_train_named.name = "y" - -# Target for multivariate extraction -Xtmvar = ["POP_lag_3", "POP_lag_6", "GNP_lag_3", "GNP_lag_6"] -Xtmvar = Xtmvar + ["GNPDEFL", "UNEMP", "ARMED"] -Xtmvar_none = ["GNPDEFL_lag_3", "GNPDEFL_lag_6", "GNP", "UNEMP", "ARMED", "POP"] - - -@pytest.mark.parametrize( - "kwargs, column_names, y, target_cols, truncate", - [ - ( - kwargs, - ["y_lag_1", "y_mean_1_3", "y_mean_1_12", "y_std_1_4"], - y_train_named, - None, - None, - ), - (kwargs_alternames, Xtmvar, X_ll_train, ["POP", "GNP"], None), - (kwargs_alternames, Xtmvar_none, X_ll_train, None, None), - ( - kwargs, - ["y_lag_1", "y_mean_1_3", "y_mean_1_12", "y_std_1_4"], - y_group1, - None, - None, - ), - ( - kwargs, - ["y_lag_1", "y_mean_1_3", "y_mean_1_12", "y_std_1_4"], - y_grouped, - None, - None, - ), - ( - kwargs, - ["y_lag_1", "y_mean_1_3", "y_mean_1_12", "y_std_1_4"], - y_hierarchical, - None, - None, - ), - ( - None, - ["var_0_lag_1", "var_1"], - y_multi, - None, - None, - ), - (None, None, y_train, None, None), - (None, ["a_lag_1"], y_pd, None, None), - (kwargs_custom, ["a_count_gt100_3_2"], y_pd, None, None), - (kwargs_alternames, ["0_lag_3", "0_lag_6"], y_train, None, "bfill"), - ( - kwargs_variant, - ["0_mean_1_7", "0_mean_8_7", "0_cov_1_28"], - y_train, - None, - None, - ), - ], -) -def test_windowsummarizer(kwargs, column_names, y, target_cols, truncate): - """Test columns match kwargs arguments.""" - if kwargs is not None: - transformer = WindowSummarizer( - **kwargs, target_cols=target_cols, truncate=truncate - ) - else: - transformer = WindowSummarizer(target_cols=target_cols, truncate=truncate) - Xt = transformer.fit_transform(y) - if Xt is not None: - if isinstance(Xt, pd.DataFrame): - Xt_columns = Xt.columns.to_list() - else: - Xt_columns = Xt.name - else: - Xt_columns = None - - # check that the index names are preserved - assert y.index.names == Xt.index.names - - check_eval(Xt_columns, column_names) - - -@pytest.mark.xfail(raises=ValueError) -def test_wrong_column(): - """Test mismatch between X column names and target_cols.""" - transformer = WindowSummarizer(target_cols=["dummy"]) - Xt = transformer.fit_transform(X_ll_train) - return Xt diff --git a/docs/api_reference/classification.rst b/docs/api_reference/classification.rst index ab36e9a19b..89a6261c3b 100644 --- a/docs/api_reference/classification.rst +++ b/docs/api_reference/classification.rst @@ -32,7 +32,9 @@ Deep learning :toctree: auto_generated/ :template: class.rst + BaseDeepClassifier CNNClassifier + TimeCNNClassifier EncoderClassifier FCNClassifier InceptionTimeClassifier @@ -56,6 +58,7 @@ Dictionary-based ContractableBOSS IndividualBOSS IndividualTDE + MrSQMClassifier MUSE REDCOMETS TemporalDictionaryEnsemble @@ -133,7 +136,6 @@ Shapelet-based :template: class.rst LearningShapeletClassifier - MrSQMClassifier RDSTClassifier SASTClassifier ShapeletTransformClassifier diff --git a/docs/api_reference/clustering.rst b/docs/api_reference/clustering.rst index 1361f8b1a4..737a585631 100644 --- a/docs/api_reference/clustering.rst +++ b/docs/api_reference/clustering.rst @@ -20,6 +20,7 @@ Deep learning BaseDeepClusterer AEFCNClusterer + AEResNetClusterer Clustering Algorithms --------------------- diff --git a/docs/api_reference/distances.rst b/docs/api_reference/distances.rst index ef79912320..c11ca70151 100644 --- a/docs/api_reference/distances.rst +++ b/docs/api_reference/distances.rst @@ -153,6 +153,9 @@ Shape Dynamic Time Warping (Shape DTW) shape_dtw_pairwise_distance shape_dtw_cost_matrix shape_dtw_alignment_path + _pad_ts_collection_edges + _pad_ts_edges + _transform_subsequences Squared ------- diff --git a/docs/api_reference/networks.rst b/docs/api_reference/networks.rst index 0a33613289..65b8aefa14 100644 --- a/docs/api_reference/networks.rst +++ b/docs/api_reference/networks.rst @@ -15,6 +15,7 @@ Deep learning networks BaseDeepLearningNetwork CNNNetwork + TimeCNNNetwork EncoderNetwork FCNNetwork InceptionNetwork @@ -22,3 +23,6 @@ Deep learning networks ResNetNetwork TapNetNetwork AEFCNNetwork + AEResNetNetwork + LITENetwork + AEBiGRUNetwork diff --git a/docs/api_reference/regression.rst b/docs/api_reference/regression.rst index 2001e5e4b4..bb0ff961c3 100644 --- a/docs/api_reference/regression.rst +++ b/docs/api_reference/regression.rst @@ -51,16 +51,18 @@ Deep learning :toctree: auto_generated/ :template: class.rst + BaseDeepRegressor CNNRegressor + TimeCNNRegressor EncoderRegressor FCNRegressor InceptionTimeRegressor IndividualLITERegressor IndividualInceptionRegressor LITETimeRegressor - LITETimeRegressor ResNetRegressor TapNetRegressor + MLPRegressor Distance-based -------------- diff --git a/docs/api_reference/transformations.rst b/docs/api_reference/transformations.rst index 5f1ea860a4..9752d7c36a 100644 --- a/docs/api_reference/transformations.rst +++ b/docs/api_reference/transformations.rst @@ -74,21 +74,6 @@ features, usually a vector of floats, but can also be categorical. When applied to collections or hierarchical data, the transformation result is a table with as many rows as time series in the collection and a column for each feature. -Summarization -~~~~~~~~~~~~~ - -These transformers extract simple summary features. - -.. currentmodule:: aeon.transformations.summarize - -.. autosummary:: - :toctree: auto_generated/ - :template: class.rst - - SummaryTransformer - WindowSummarizer - FittedParamExtractor - Shapelets, wavelets and convolution ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -172,23 +157,6 @@ These transformers extract larger collections of features. TSFreshFeatureExtractor Catch22 -Series-to-series transformers ------------------------------ - -Series-to-series transformers transform individual time series into another time series. -When applied to collections or hierarchical data, individual series are transformed -through broadcasting. - -Lagging -~~~~~~~ - -.. currentmodule:: aeon.transformations.lag - -.. autosummary:: - :toctree: auto_generated/ - :template: class.rst - - Lag Series transforms ~~~~~~~~~~~~~~~~~~~~~~~ @@ -206,14 +174,6 @@ Depending on the transformer, the transformation parameters can be fitted. BoxCoxTransformer LogTransformer -.. currentmodule:: aeon.transformations.scaledlogit - -.. autosummary:: - :toctree: auto_generated/ - :template: class.rst - - ScaledLogitTransformer - .. currentmodule:: aeon.transformations.exponent .. autosummary:: @@ -386,14 +346,6 @@ Bootstrap transformations Outlier detection, changepoint detection ---------------------------------------- -.. currentmodule:: aeon.transformations.outlier_detection - -.. autosummary:: - :toctree: auto_generated/ - :template: class.rst - - HampelFilter - .. currentmodule:: aeon.transformations.series._clasp .. autosummary:: diff --git a/docs/api_reference/visualisation.rst b/docs/api_reference/visualisation.rst index 1555d6bbaf..4d7be0feb8 100644 --- a/docs/api_reference/visualisation.rst +++ b/docs/api_reference/visualisation.rst @@ -31,3 +31,4 @@ Visualisation plot_time_series_with_change_points plot_time_series_with_profiles plot_cluster_algorithm + plot_network diff --git a/docs/getting_started.md b/docs/getting_started.md index 212c058dca..180d553a38 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -72,15 +72,14 @@ Quarter 1971 Q1 1.897371 1.987154 1.909734 3.657771 -0.1 ``` -We commonly refer to the number of observations for a time series as `n_timepoints` or -`n_timepoints`. If a series is multivariate, we refer to the dimensions as channels +We commonly refer to the number of observations for a time series as `n_timepoints`. If a series is multivariate, we refer to the dimensions as channels (to avoid confusion with the dimensions of array) and in code use `n_channels`. Dimensions may also be referred to as variables. Different parts of `aeon` work with single series or collections of series. The `anomaly detection` and `segmentation` modules will commonly use single series input, while `classification`, `regression` and `clustering` modules will use collections of time -series. Collections of time series may also be referred to a Panels. Collections of +series. Collections of time series may also be referred to as Panels. Collections of time series will often be accompanied by an array of target variables. ```{code-block} python diff --git a/examples/classification/deep_learning.ipynb b/examples/classification/deep_learning.ipynb index e9fc4e2db9..1e9415c32f 100644 --- a/examples/classification/deep_learning.ipynb +++ b/examples/classification/deep_learning.ipynb @@ -201,6 +201,7 @@ "names.remove(\"IndividualInception\")\n", "names.remove(\"IndividualLITE\")\n", "names.remove(\"MLP\")\n", + "names.remove(\"TimeCNN\")\n", "names.remove(\"TapNet\") # Multivariate only\n", "\n", "\n", diff --git a/examples/transformations/transformations.ipynb b/examples/transformations/transformations.ipynb deleted file mode 100644 index 53ff123f8d..0000000000 --- a/examples/transformations/transformations.ipynb +++ /dev/null @@ -1,890 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Transforming time series with aeon\n", - "\n", - "Transformers are objects that transform data from one representation to another. `aeon`\n", - "contains time series specific transformers which can be used in\n", - "pipelines in conjunction with other estimators.\n", - "Note: the term \"transformer\" is used in deep learning to refer to specific neural\n", - "network architectures. `aeon` transformers follow the `scikit-learn` design: they\n", - "have `fit`, `transform` and `fit_transform` methods that combine the two functions.\n", - "Some transformers also have `inverse_transform` that allows you to reverse the change.\n", - "\n", - "`aeon` distinguishes different types of transformer, depending on the input type accepted\n", - "by the `fit` and `transform` methods. The main distinction is whether all series types\n", - "(i.e. single time series, collections of time series, hierarchical time series) are accepted\n", - "and implicitly converted, or whether only a singular input type (i.e. collections) is accepted." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Transformers\n", - "\n", - "General transformers (in the package `aeon/transformations`, `aeon/transformations`,\n", - "`aeon/transformations/bootstrap` and `aeon/transformations/hierarchical`) aim to\n", - "accept all input types, and will attempt to restructure the data or broadcast to multiple transformer\n", - "objects if necessary to fit the input data to the data structure used by the transformer. For example,\n", - "if the class excepts a singular series but is given a collection of series, a separate instance of the\n", - "transformer is applied independently to each series. Transformers all extend the base class\n", - "`BaseTransformer`. General transformations are mostly used for single series tasks such as forecasting\n", - "and annotation, and are best used with `pd.Series` or `pd.DataFrame` input. Other valid data types will\n", - "be accepted but are likely to be converted to another format internally, see the\n", - "[data structures](../datasets/data_structures.ipynb) notebook for clarification of how\n", - "best\n", - "to store\n", - "data with aeon.\n", - "\n", - "Transformers differ in terms of whether they convert time series into different time series\n", - "(series-to-series transformation), or whether they convert series into feature vector(s)\n", - "(series-to-vector transformation).\n", - "\n", - "To illustrate the difference, we compare two single-series transformers with different output:\n", - "\n", - "* the Box-Cox transformer `BoxCoxTransformer`, a series-to-series transformer using the\n", - "[Box Cox power transform](https://en.wikipedia.org/wiki/Power_transform#Box%E2%80%93Cox_transformation).\n", - "* the summary transformer `SummaryTransformer`, a series-to-vector transformer that\n", - "finds summary statistics such as the mean an standard deviation of each series.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "outputs": [], - "source": [ - "import warnings\n", - "\n", - "warnings.filterwarnings(\"ignore\")" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:25.993971Z", - "start_time": "2024-03-01T16:16:25.989981Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": { - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.098690Z", - "start_time": "2024-03-01T16:16:26.088717Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": "pandas.core.series.Series" - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from aeon.datasets import load_airline\n", - "from aeon.transformations._legacy._boxcox import _BoxCoxTransformer as BoxCoxTransformer\n", - "from aeon.transformations.summarize import SummaryTransformer\n", - "from aeon.visualisation import plot_series\n", - "\n", - "boxcox_trans = BoxCoxTransformer()\n", - "summary_trans = SummaryTransformer()\n", - "\n", - "# airline is a single time series stored in a pd.Series\n", - "airline = load_airline()\n", - "\n", - "type(airline)" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "outputs": [ - { - "data": { - "text/plain": "Period\n1949-01 112.0\n1949-02 118.0\n1949-03 132.0\n1949-04 129.0\n1949-05 121.0\nFreq: M, Name: Number of airline passengers, dtype: float64" - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "airline[:5]" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.108664Z", - "start_time": "2024-03-01T16:16:26.102680Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 32, - "outputs": [ - { - "data": { - "text/plain": "(
,\n )" - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": "
", - "image/png": "iVBORw0KGgoAAAANSUhEUgAABSMAAAFfCAYAAAC1GdkVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAAC3WklEQVR4nOzdd1zUh/0/8Ndt4IA7ZaqgIMO9J0KGiJrErGqzml07fokjMa2JRmOMJtHYrKqZbRKbb5smzWpSs0RFDe64N1tUlsg+OG7+/jg+J8gxjtvwej4ePBLuc+N9C+F97yEym81mEBEREREREREREbmY2NMBEBERERERERERUc/AZCQRERERERERERG5BZORRERERERERERE5BZMRhIREREREREREZFbMBlJREREREREREREbsFkJBEREREREREREbkFk5FERERERERERETkFlJPB+ANTCYTioqKEBQUBJFI5OlwiIiIiIiIiIiIfIrZbEZtbS369u0Lsbjt+kcmIwEUFRUhOjra02EQERERERERERH5tAsXLiAqKqrN40xGAggKCgJgebCCg4M9HA0REREREREREZFvqampQXR0tDXP1hYmIwFra3ZwcDCTkURERERERERERF3U0QhELrAhIiIiIiIiIiIit2AykoiIiIiIiIiIiNyCyUgiIiIiIiIiIiJyCyYjiYiIiIiIiIiIyC2YjCQiIiIiIiIiIiK3YDKSiIiIiIiIiIiI3ILJSCIiIiIiIiIiInILJiOJiIiIiIiIiMinaXQG6AwmlNU1QmcwQaMzeDokaoPU0wEQERERERERERF1lVZvxLqMXGzIzEdVgx5qfxkWpsRiSWo8/GQST4dH12AykoiIiIiIiIiIfJJGZ8C6jFysTs+ynlbVoMeqpu8XT42DUs70lzdhmzYREREREREREfkkmViMDZn5No+tz8yHTMzUl7fhM0JERERERERERD6pSqtHVYPe9rEGPaq1to+R5zAZSUREREREREREPkntJ4PaX2b7mL8MKj/bx8hzmIwkIiIiIiIiIiKfpDeZsDAl1uaxBSkx0JtMbo6IOsJkJBERERERERER+SSlXIolqfFYnpZgrZBU+8uwPC0B85NjcbK41sMR0rW4ToiIiIiIiIiIiHyWn0yCpJheeCY1HrWNBvT2l+PghSrc8PYeXKrWYsfjUzCmn8rTYVITVkYSEREREREREZHPqmrQY9bfDyD2pW1QyiSQS8UYF6VCRKACtY0GPPHfkyiva4TOYEJZ0381OoOnw+6xWBlJREREREREREQ+63SppRXbTypGUNPCGj+ZBP99dAIe/ewo3vv1SKzPzMfG3QWoatBD7S/DwpRYLEmNh59M4snQeyQmI4mIiIiIiIiIyGedKrEkI4dFBrU4XeUvwwd3j8Ibu/Lw4tZs6+lVDXqsSs8CACyeGgelnOkxd/J4m/alS5fwwAMPICQkBP7+/hgxYgR++eUX63Gz2YwVK1agT58+8Pf3R1paGrKzs1tcR0VFBe6//34EBwdDrVZj7ty5qKurc/ddISIiIiIiIiIiNzvVVBk5NCKo1TGlXIqNuwtsXm59Zj5kYo+nxnocjz7ilZWVSE5Ohkwmww8//IDTp0/jtddeQ69evaznWbduHdavX493330X+/fvh1KpxMyZM6HVaq3nuf/++3Hq1Cmkp6dj8+bN2LVrF/7whz944i4REREREREREZEbnW6jMhIAqrR6VDXobV6uqkGPaq3tY+Q6Hq1DfeWVVxAdHY2PPvrIelpsbKz1/81mM958800sX74cd9xxBwDg448/RkREBP773//i3nvvxZkzZ/Djjz/i4MGDGD9+PABgw4YNuOWWW/Dqq6+ib9++7r1TRERERERERETkNkJl5DAblZFqPxnU/jKbCUm1vwyqphmT5D4erYz89ttvMX78eNx1110IDw/HmDFj8Le//c16PD8/HyUlJUhLS7OeplKpMGnSJOzduxcAsHfvXqjVamsiEgDS0tIgFouxf/9+m7fb2NiImpqaFl9ERERERERERORbKut1KK5pBGC7TVtvMmFhSmyr0wFgYUos9CaTS+Oj1jyajMzLy8M777yDhIQE/PTTT3jsscewcOFC/OMf/wAAlJSUAAAiIiJaXC4iIsJ6rKSkBOHh4S2OS6VS9O7d23qea61ZswYqlcr6FR0d7ey7RkRERERERERELiZURfZX+yPIr3UDsFIuxZLUeKyYngi1v6UKUu0vw4rpiViSGs/lNR7g0UfcZDJh/PjxePnllwEAY8aMwcmTJ/Huu+/i4YcfdtntLl26FE899ZT1+5qaGiYkiYiIiIiIiIh8jLBJe2hEYJvn8ZNJsHhqHJ5JjUdpbSMighQwmc3wk0ncFSY149HKyD59+mDo0KEtThsyZAgKCwsBAJGRkQCA0tLSFucpLS21HouMjERZWVmL4waDARUVFdbzXEuhUCA4OLjFFxERERERERER+ZZTpXUAgKE2ltc0p5RL8f6+87j9wwP407enWBHpQR5NRiYnJ+PcuXMtTsvKysKAAQMAWJbZREZGYtu2bdbjNTU12L9/P5KSkgAASUlJqKqqwqFDh6zn2b59O0wmEyZNmuSGe0FERERERERERJ5g3aRtY17ktaLV/jhZUotDF6tdHRa1w6Np4EWLFmHKlCl4+eWXcffdd+PAgQN4//338f777wMARCIRnnzySbz44otISEhAbGwsnnvuOfTt2xd33nknAEsl5U033YTf//73ePfdd6HX6zF//nzce++93KRNRERERERERNSNWTdpd1AZCQCJoUoAQNblOpjNZohEIpfGRrZ5NBk5YcIEfP3111i6dClWrVqF2NhYvPnmm7j//vut53n66aeh0Wjwhz/8AVVVVUhJScGPP/4IPz8/63n+9a9/Yf78+Zg2bRrEYjHmzJmD9evXe+IuERERERERERGRG1zR6FBa2/Ym7WvFhSohEgHVWgMu1+kQHqRwdYhkg8hsNps9HYSn1dTUQKVSobq6mvMjiYiIiIiIiIh8wM95V3DD23swoJc/8peldeoysS9txfnKBuyaNwUpsSEujrBn6Wx+zaMzI4mIiIiIiIiIiLrilB3zIgWJYUKrtsYlMVHHmIwkIiIiIiIiIiKfI8yL7GiTdnOJYYEAmIz0JCYjiYiIiIiIiIjI59izSVsgJCOzL9e5JCbqGJORRERERERERETkc+zZpC1gm7bnMRlJREREREREREQ+pVzTiLI6HQBgSHhgpy+XGGo5b84VDYymHr/T2SOYjCQiIiIiIiIiIp8iLK+J7R0ApULa6cv17+UPuUSMRoMJhZUNrgqP2sFkJBERERERERER+ZRTJZaZj0MjOl8VCQASsQjxoQEAgKxyzo30BCYjiYiIiIiIiIjIp1g3aduxvEbAjdqexWQkERERERERERH5FOsmbTuW1wgSQoUlNqyM9AQmI4mIiIiIiIiIyKd0ZZO2QKiMzGZlpEcwGUlERERERERERD6jrLYR5RodRCL7NmkLEsNYGelJTEYSEREREREREbmBRmeAzmBCWV0jdAYTNDqDp0PySadLr27SDpB3fpO2QKiMPF/VAK3e6NTYqGP2P2NERERERERERGQXrd6IdRm52JCZj6oGPdT+MixMicWS1Hj4ySSeDs+nWFu0u7C8BgDCA+VQ+UlRrTUg90p9l1q9qetYGUlERERERERE5EIanQFrtudgdXoWqhr0AICqBj1WpWdh7fYcVkja6VTT8pqhXUwiikSiZhu12artbkxGEhERERERERG5kEwsxobMfJvH1mfmQyZmesYepx2sjASaz43kEht346udiIiIiIiIiMiFqrR6a0Vkq2MNelRrbR+j1sxms7Uy0pH26oRQS2XkOVZGuh2TkURERERERERELqTyk0LtL7N5TO0vg8rP9jFqraxOhyv1eohFwOAubNIWCJWR2UxGuh2TkURERERERERETtBqW3ajAX/ffx4/nbuM+ckxNi+zMCUWepPJvYH6MKEqcmCIEv4OLP5hm7bncJs2EREREREREZGDbG3Lnp8cgwUpsXj006P4v/vHQiwSYT23aTvk6ibtrldFAlfbtC9rdKis16FXgNzh2KhzmIwkIiIiIiIiInKARmfAuoxcrE7Psp5W1aDHi1uzAQCv3TEMaj8pFk+Nw5Jp8SipaURYoCX5xUSkfYTKyCEOLK8BgCA/KfoEK1Bc04jscg0m9mcy0l0cbtM2Go04evQoKisrnREPEREREREREZFPaW9b9sbdBYjtFQCRSASlXIoKjQ63f3gACWu2Qy7h9Dx7WTdpO7C8RpDYVB3JVm33svtV/+STT+KDDz4AYElE3nDDDRg7diyio6OxY8cOZ8dHREREREREROTV7NmW3SfYD/kV9SipbUTelXp3hdgttNik7WBlJAAkWOdGcomNO9mdjPziiy8watQoAMD//vc/5Ofn4+zZs1i0aBGWLVvm9ACJiIiIiIiIiLyZ2k/W6W3ZIpHIujzlHJNgdimpbURlg+ObtAWDwizXkV3Oykh3sjsZWV5ejsjISADA999/j7vuuguJiYn47W9/ixMnTjg9QCIiIiIiIiIib6Y3mbAwJdbmMVvbshObkmDnypiMtEd2eR2GRwZhfJTaKbM2E1kZ6RF2L7CJiIjA6dOn0adPH/z444945513AAD19fWQSDh0lYiIiIiIiIh6FqVciiWp8TDDjA2ZBR1uy7YmIzmrsNM0OgMmRvfCN7+diIhABTQ6A5Ryx/YyC89D1mUNzGYzRCKRM0KlDtj9rD366KO4++67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qqqqEBoais2bN+OJJ57AhQsX0LdvX5w8eRJDhgwBAOzZswfjxo3DtWvXEB4ebvSzGhsb0djYqP9zdXU1IiIiOtQ0bWo7pVqLsKV7Ta63WZqZBi8Z1y4kIiIiIiIiItJpF9O0zSkqKkJpaSlSU1P1rwUGBiIhIQG5ubkAgNzcXAQFBekTkQCQmpoKqVSK48ePmzz2ypUrERgYqP+JiIhw3IlQu6NrpGKMrpEKERERERERERG1ntsmI0tLSwEAoaGhBq+Hhobqt5WWlqJbt24G22UyGYKDg/X7GJORkYGqqir9z9WrV+08emrPuN4mEREREREREZFjdMqsilwuh1wud/UwyI3p1tt8JSXOoBs219skIiIiIiIiImo7t62MDAsLAwCUlZUZvF5WVqbfFhYWhvLycoPtarUat27d0u9D1FZspEJEREREREREZF9um4yMjo5GWFgY9u/fr3+turoax48fR2JiIgAgMTERlZWV+P777/X7ZGdnQ6vVIiEhweljJiIiIiIiIiIiItNcWupVW1uLgoIC/Z+Liopw+vRpBAcHo2fPnpg9ezZWrFiBuLg4REdHY9GiRQgPD9d33O7Tpw/GjBmD6dOn47333oNKpUJ6ejqeeOIJk520iYiIiIiIiIiIyDVcmow8deoURowYof/ziy++CACYOnUqNm/ejLlz56Kurg4zZsxAZWUlkpOTsWfPHnh7e+vf8/HHHyM9PR0pKSmQSqV47LHHsG7dOqefCxEREREREREREZknEUIIVw/C1aqrqxEYGIiqqiooFApXD4eIiIiIiIiIiKhdsTa/5rZrRhIREREREREREVHHwmQkEREREREREREROQWTkUREREREREREROQULm1g4y50y2ZWV1e7eCRERERERERERETtjy6vZqk9DZORAGpqagAAERERLh4JERERERERERFR+1VTU4PAwECT29lNG4BWq8Wvv/6KgIAASCQSVw/H7qqrqxEREYGrV6+yWzg5DeOOnI0xR67AuCNXYNyRszHmyBUYd+QKjDvbCCFQU1OD8PBwSKWmV4ZkZSQAqVSKHj16uHoYDqdQKPiPiZyOcUfOxpgjV2DckSsw7sjZGHPkCow7cgXGXduZq4jUYQMbIiIiIiIiIiIicgomI4mIiIiIiIiIiMgpmIzsBORyOTIzMyGXy109FOpEGHfkbIw5cgXGHbkC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9VWD/e0RdzrXrl3DzZs3DT6b2i9b4u7zzz/HmTNn9PfgDz74AABw+PBhzJw5065xRx2Hrd91xvbp0qUL5HI577Fkkj3irra2Ftu3bzeoigR4jyXT7BF3fI6l1jAVc1FRUQgPD8fFixcN9r906RIiIyMB3ImdyspKgzVJs7OzodVq9clyW2POz88P3bt3R0VFBb777rvOfY91cQOdTq2mpkbk5eWJvLw8AUC8+eabIi8vT/zyyy9CCCG2b98ucnJyRGFhofjyyy9FZGSk+NOf/mT2mDDSDWz16tXixx9/FOfOnRPLli0Tnp6eFjuGXb58Wfj6+oo5c+aICxcuiA0bNggPDw+xZ88eq8dP7sfWmCsoKBDLli0Tp06dEkVFRWLnzp0iJiZGDBs2TL+PWq0W/fv3F2lpaeL06dNiz549IiQkRGRkZJgd29GjR4VMJhNvvPGGuHDhgsjMzBSenp7i7Nmz+n3a8m+CXM8ZcXf27FkREhIinnzySVFSUqL/KS8vNzs2S3FXU1MjXn75ZZGbmyuKiorEvn37xODBg0VcXJxoaGhwwNUie3FG3DWXk5NjVTdta77vbt68KfLy8sQ333wjAIhPPvlE5OXliZKSEtsuDDmMM2Luq6++Eu+//744e/asyM/PF++++67w9fUVixcvNjs23mM7Lmd+133wwQfC29vb4necDu+xHZez4o7PsaRjj9zJ2rVrhUKhEJ999pnIz88XCxcuFN7e3qKgoEC/z5gxY8S9994rjh8/Lo4cOSLi4uLEpEmTzI7Nmpjbs2eP+Pbbb8Xly5fF3r17xcCBA0VCQoJQKpV2vErtC5ORLqR7aGn+M3XqVCGEEG+//bbo0aOH8PT0FD179hQLFy4UjY2NZo9pLBk5YsQIERgYKLy9vUVCQoLYvXu31eMbNGiQ8PLyEjExMWLTpk2tGj+5H1tj7sqVK2LYsGEiODhYyOVyERsbK+bMmSOqqqoMPqe4uFiMHTtW+Pj4iK5du4qXXnpJqFQqi+Pbvn27iI+PF15eXqJfv37im2++Mdjeln8T5HrOiLvMzEyjnxEZGWlxfObirr6+XqSlpYmQkBDh6ekpIiMjxfTp00Vpaandrg85hrO+74x9pjUP6pa+7zZt2mR0/JmZmW25HOQEzoi5b7/9VgwaNEj4+/sLPz8/MXDgQPHee+8JjUZjcXy8x3ZMzvyuS0xMFJMnT27V+HiP7ZicFXd8jiUde+VOVq5cKXr06CF8fX1FYmKiOHz4sMH2mzdvikmTJgl/f3+hUCjEX//6V1FTU2PV+MzF3KeffipiYmKEl5eXCAsLEzNnzhSVlZVtvh4dgUSI3xc4IiIiIiIiIiIiInIgrhlJRERERERERERETsFkJBERERERERERETkFk5FERERERERERETkFExGEhERERERERERkVMwGUlEREREREREREROwWQkEREREREREREROQWTkUREREREREREROQUTEYSERERERERERGRUzAZSURERERERERERE7BZCQRERERERERERE5BZORRERERERERERE5BT/AyV3iOOwr+6LAAAAAElFTkSuQmCC" - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_series(airline)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.291176Z", - "start_time": "2024-03-01T16:16:26.109661Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": { - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.302147Z", - "start_time": "2024-03-01T16:16:26.293172Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": "pandas.core.series.Series" - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# this produces a pandas Series containing the transformed series\n", - "airline_bc = boxcox_trans.fit_transform(airline)\n", - "type(airline_bc)" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "outputs": [ - { - "data": { - "text/plain": "Period\n1949-01 6.827490\n1949-02 6.932822\n1949-03 7.161892\n1949-04 7.114611\n1949-05 6.983787\nFreq: M, dtype: float64" - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "airline_bc[:5]" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.313117Z", - "start_time": "2024-03-01T16:16:26.304142Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 35, - "outputs": [ - { - "data": { - "text/plain": "(
, )" - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": "
", - "image/png": "iVBORw0KGgoAAAANSUhEUgAABRQAAAFfCAYAAAA/NkBUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACrB0lEQVR4nOzdd3TV9f0/8OfdN7lZQEIGCWSzBJShAkFJSIKzuKu1rv6sWgWKtihYdrAofmstYGuXxdFWrQpWsZIEghoEWaLsDDIhg0DmTe6+vz9u7jUh997k7nuT5+OcHA/3M+77fvJJzH3d1xAYjUYjiIiIiIiIiIiIiAZA6OsFEBERERERERERUeBgQJGIiIiIiIiIiIgGjAFFIiIiIiIiIiIiGjAGFImIiIiIiIiIiGjAGFAkIiIiIiIiIiKiAWNAkYiIiIiIiIiIiAaMAUUiIiIiIiIiIiIaMLGvF+AOBoMB58+fR2hoKAQCga+XQ0REREREREREFFCMRiPa29sRFxcHodB+DuKgCCieP38eCQkJvl4GERERERERERFRQKupqUF8fLzdfQZFQDE0NBSA6QWHhYX5eDVERERERERERESBpa2tDQkJCZY4mz2DIqBoLnMOCwtjQJGIiIiIiIiIiMhJA2knyKEsRERERERERERENGAMKBIREREREREREdGAMaBIREREREREREREA8aAIhEREREREREREQ0YA4pEREREREREREQ0YAwoEhERERERERER0YAxoEhEREREREREREQDxoAiERERERERERH5nFKjg0ZnQGOHGhqdAUqNztdLIhvEvl4AERERERERERENbSqtHhuLyrG5uAItXVpEBEmwOCMJy7JSIZeIfL08ugwDikRERERERERE5DNKjQ4bi8qRV1BieaylS4t13f9empkChZQhLH/CkmciIiIiIiIiIvIZiVCIzcUVVrdtKq6ARMjwlb/hd4SIiIiIiIiIiHymRaVFS5fW+rYuLVpV1reR7zCgSEREREREREREPhMhlyAiSGJ9W5AEYXLTNg5t8R8MKBIRERERERERkc9oDQYszkiyum3h7ETsrbiI1i4tNhaVI2ZtPmLW5CNmbT5eLiqHSqv38moJ4FAWIiIiIiIiIiLyIYVUjF/NTYHBaMSWvZW9pjwvnpOEI7Wt+N0X5VhfWGo5hkNbfItXm4iIiIiIiIiI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1eo6fJxxFQDwZngAJBJJG0Ro+zi6k4iIiIiIiMi2saDYjnG0V+vMul+E3fCva1DV1Wu2b8q8gVvVtQh4xAmxg73bKrxOgaM7iYiIiIiIiGwXn/LbucbRXovGBqK8phYuDjLUNjRwtJcBvx7oCV8XB9wor8FXp4swebgv6hsEVn+fBwCY+1QAukg5OpGIiIiIiIiIqDU4QrED4GivlrHrIsWMUH8A/16c5VDuLyivqUN3RxmmPeHXluEREREREREREXVorEyRTZoe4o/tp4uwcEwgVHX1CHTvivzFY5H7SxW6ypn2REREREREREStxcoK2SSPbnIcnhmG1UfyMG1bNsru1sLVUYY5o/rgUXdnvjJORERERERERNRKLCiSTapS12HN93l4Ny1Hs63sbi2SUi8DAOZH9OWr40RERERERERErcA5FMkmyaRSfHT0qs59Hx7Nh0zK1CciIiIiIiIiag1WVcgmldXUouxure59d2tRXqN7HxERERERERERGcaCItkkVwcZXB1luvc5yuDioHsfEREREREREREZxoIi2aTahgbMGdVH5745o/qgtqHByhEREREREREREdkGrkpBNqmrvR0WjukH4N6ciQ+u8rxwTD+u8kxERERERERE1EoSIYRo6yBMVVFRARcXF5SXl0OhULR1ONSOVKnrIJNKUV5TCxcHGWobGri6MxERERERERHRQ1pSX2NlhWxaY/HQ3VkOALDnW/5ERERERERERCZhdYWIiIiIiIiIiIiMxoIiERERERERERERGY0FRSIiIiIiIiIiIjKaRQqKhYWFmDx5Mh555BE4OjoiKCgIJ0+e1Nv+8OHDkEgkTT7FxcWWCI+IiIiIiIiIiIhayeyLsty+fRsjR45EREQE9u3bB3d3d+Tk5KB79+7NfvfSpUtaq8h4eHiYOzwiIiIiIiIiIiIygdkLiitXroSfnx+Sk5M12/r06WPUdz08PODq6mrukIiIiIiIiIiIiMhMzP7K8+7duzFixAi88MIL8PDwwLBhw7Bhwwajvjt06FB4e3vjmWeewbFjx/S2U6lUqKio0PoQERERERERERGR5Zl9hOKVK1fwySefYN68eVi0aBEyMzMxZ84c2NvbY8qUKTq/4+3tjU8//RQjRoyASqXCxo0b8fTTT+P48eMIDg5u0n7FihV45513mmxnYZGIiIiIiIiIiKjlGutqQohm20qEMa1awN7eHiNGjEBGRoZm25w5c5CZmQmlUmn0ccLDw9GrVy9s3ry5yT6VSgWVSqX5u7CwEAMHDjQtcCIiIiIiIiIiok7u+vXr8PX1NdjG7CMUvb29mxT3BgwYgK+//rpFx3nyySdx9OhRnfvkcjnkcrnmb2dnZ1y/fh3dunWDRCJpedAdQEVFBfz8/HD9+nWthWuILIU5R22BeUdtgXlH1saco7bAvKO2wLwja2POmUYIgcrKSvj4+DTb1uwFxZEjR+LSpUta2y5fvgx/f/8WHSc7Oxve3t5GtZVKpc1WTm2FQqHgPwqyKuYctQXmHbUF5h1ZG3OO2gLzjtoC846sjTnXei4uLka1M3tB8Y033kBYWBjee+89vPjiizhx4gQ+++wzfPbZZ5o2CQkJKCwsREpKCgBgzZo16NOnDwYNGoSamhps3LgR6enpOHDggLnDIyIiIiIiIiIiIhOYvaD4xBNPYOfOnUhISEBSUhL69OmDNWvWYNKkSZo2RUVFKCgo0PytVqvx5ptvorCwEE5OTnj88ceRlpaGiIgIc4dHREREREREREREJjB7QREAnnvuOTz33HN692/atEnr7wULFmDBggWWCMVmyOVyJCYmas0dSWRJzDlqC8w7agvMO7I25hy1BeYdtQXmHVkbc856zL7KMxEREREREREREdkuaVsHQERERERERERERB0HC4pERERERERERERkNBYUiYiIiIiIiIiIyGgsKBIREREREREREZHRWFAkIiIiIiIiIiIio7GgaILvv/8ev/rVr+Dj4wOJRIJdu3Zp7S8pKcHUqVPh4+MDJycnxMTEICcnR+exhBAYN26czuMcPHgQYWFh6NatG7y8vPDWW2+hrq6u2fgOHz6M4OBgyOVy9OvXD5s2bWpR/NQ+mSPvnn76aUgkEq3Pa6+9ptWmoKAA48ePh5OTEzw8PDB//nyj8u6rr77CY489BgcHBwQFBWHv3r0tjo/aH2vk3enTpxEXFwc/Pz84OjpiwIABWLt2rVHxNZd3U6dObXLumJiY1l0Mshpr3e8a3bx5E76+vpBIJCgrK2s2vubybseOHYiKisIjjzwCiUSC7Ozslvx8agPWyrmH90skEnz55ZfNxsc+1jZZI+82bdqkM+8kEglKS0sNxsc+1vZY617H51h6kLnqJ0qlEmPGjEHXrl2hUCgwevRo3L17V7P/1q1bmDRpEhQKBVxdXREfH487d+40G19zeVdZWYm5c+fC398fjo6OCAsLQ2ZmZquuha1gQdEEVVVVGDJkCNavX99knxACsbGxuHLlCr755htkZWXB398fkZGRqKqqatJ+zZo1kEgkTbafPn0azz77LGJiYpCVlYVt27Zh9+7dWLhwocHY8vPzMX78eERERCA7Oxtz587FK6+8gu+++86o+Kn9MlfeTZ8+HUVFRZrP+++/r9lXX1+P8ePHQ61WIyMjA3/961+xadMmLF261GBsGRkZiIuLQ3x8PLKyshAbG4vY2FicO3euxfFR+2KNvDt16hQ8PDywZcsWnD9/HosXL0ZCQgLWrVtnMLbm8q5RTEyM1rm3bt1qwhUha7BG3j0oPj4ejz/+uFGxGZN3VVVVGDVqFFauXNmCX01tyZo5l5ycrNUmNjbWYGzsY22XNfLupZde0tpXVFSE6OhohIeHw8PDQ29s7GNtkzVyjs+x9DBz5J1SqURMTAyioqJw4sQJZGZmYvbs2ZBK/13amjRpEs6fP4/U1FTs2bMH33//PV599VWDsRmTd6+88gpSU1OxefNmnD17FlFRUYiMjERhYaEZrk4HJcgsAIidO3dq/r506ZIAIM6dO6fZVl9fL9zd3cWGDRu0vpuVlSV69uwpioqKmhwnISFBjBgxQqv97t27hYODg6ioqNAbz4IFC8SgQYO0tr300ksiOjraqPipY2ht3oWHh4vXX39d73H37t0rpFKpKC4u1mz75JNPhEKhECqVSu/3XnzxRTF+/HitbSEhIWLGjBktio/aN0vlnS4zZ84UERERBts0l3dCCDFlyhQxYcKEFp2b2hdL593HH38swsPDxcGDBwUAcfv2bYPtjcm7Rvn5+QKAyMrKajYOaj8smXOt+e8u9rGdg7X62NLSUiGTyURKSorBduxjbZ+lco7PsWRIa/MuJCRELFmyRO9xL1y4IACIzMxMzbZ9+/YJiUQiCgsL9X6vubyrrq4WXbp0EXv27NFqExwcLBYvXmz4x9owjlC0EJVKBQBwcHDQbJNKpZDL5Th69KhmW3V1NSZOnIj169fDy8tL53EePAYAODo6oqamBqdOndJ7fqVSicjISK1t0dHRUCqVrfo91DEYm3cA8Le//Q1ubm4YPHgwEhISUF1drdmnVCoRFBQET09Pzbbo6GhUVFTg/Pnzes/fXN61JD7qOMyVd7qUl5ejR48eBtsYe787fPgwPDw88Oijj+IPf/gDbt682exvo/bLnHl34cIFJCUlISUlRev/cBvCfrbzMfe9btasWXBzc8OTTz6JL774AkIIg+dnH9s5WaqPTUlJgZOTE377298aPD/72M7HXDnH51hqCWPyrrS0FMePH4eHhwfCwsLg6emJ8PBwrbxUKpVwdXXFiBEjNNsiIyMhlUpx/PhxvedvLu/q6upQX1+vM6c7cx/LgqKFPPbYY+jVqxcSEhJw+/ZtqNVqrFy5Ejdu3EBRUZGm3RtvvIGwsDBMmDBB53Gio6ORkZGBrVu3or6+HoWFhUhKSgIAreM8rLi4WKsYBACenp6oqKjQml+AbIuxeTdx4kRs2bIFhw4dQkJCAjZv3ozJkydr9uvLn8Z9+uj7XuN3jI2POhZz5d3DMjIysG3btmZfUWgu74B7r2KlpKTg4MGDWLlyJY4cOYJx48ahvr6+lb+a2pq58k6lUiEuLg6rVq1Cr169jD6/MXlHtsWc97qkpCRs374dqampeP755zFz5kx89NFHBs/PPrZzslQf+/nnn2PixIlwdHQ0eH72sZ2PuXKOz7HUEsbk3ZUrVwAAy5Ytw/Tp07F//34EBwdj7NixmrkWi4uLm0zjYGdnhx49erTqObYx77p164bQ0FAsX74cP/30E+rr67FlyxYolcpO3ceyoGghMpkMO3bswOXLl9GjRw84OTnh0KFDGDdunGb0w+7du5Geno41a9boPU5UVBRWrVqF1157DXK5HP3798ezzz4LAJrjODs7az76JpqnzsGYvAOAV199FdHR0QgKCsKkSZOQkpKCnTt3Ii8vz6jzFBQUaOXde++9Z9b4qGOxRN6dO3cOEyZMQGJiIqKiogC0Pu8A4He/+x1+/etfIygoCLGxsdizZw8yMzNx+PBhk38/tQ1z5V1CQgIGDBig98HblLwj22LOe93bb7+NkSNHYtiwYXjrrbewYMECrFq1CgD7WNJmiT5WqVTi4sWLiI+P12xjH0uNzJVzfI6lljAm7xoaGgAAM2bMwLRp0zBs2DCsXr0ajz76KL744gujz9XavNu8eTOEEOjZsyfkcjk+/PBDxMXFdeo+1q6tA7Blw4cPR3Z2NsrLy6FWq+Hu7o6QkBDN8Nv09HTk5eXB1dVV63vPP/88nnrqKU0nPG/ePLzxxhsoKipC9+7dcfXqVSQkJCAgIAAAtFaNVCgUAAAvLy+UlJRoHbekpAQKhaLZ/xNJHVtzeadLSEgIACA3Nxd9+/aFl5cXTpw4odWmMZ+8vLzg4+OjlXeNr6Tqy7sHX+dvTXzU/pkj7xpduHABY8eOxauvvoolS5ZotpuSdw8LCAiAm5sbcnNzMXbs2Bb9Vmo/zJF36enpOHv2LP7xj38AgOa1Uzc3NyxevBhvv/222fKOOj5z3usebrN8+XKoVCr2sdSEufNu48aNGDp0KIYPH67Zxj6WHmSunONzLLVEc3nn7e0NABg4cKDW9wYMGICCggIA9/Ln4ZXr6+rqcOvWLc19q7V517dvXxw5cgRVVVWoqKiAt7c3XnrpJU0+d0adt5RqRS4uLnB3d0dOTg5Onjypeb154cKFOHPmDLKzszUfAFi9ejWSk5O1jiGRSODj4wNHR0ds3boVfn5+CA4OBgD069dP82kc3hsaGoqDBw9qHSM1NRWhoaEW/rXUXujLO10ac6/xJh0aGoqzZ89q3YxTU1OhUCgwcOBA2NnZaeVd4390tiTvWhIfdRym5B0AnD9/HhEREZgyZQr+9Kc/abU3R941unHjBm7evKl1buq4TMm7r7/+GqdPn9b0wxs3bgQA/PDDD5g1a5ZZ845sh6n3Ol1tunfvDrlczj6W9DJH3t25cwfbt2/XGp0IsI8l3cyRc3yOpZbSl3e9e/eGj48PLl26pNX+8uXL8Pf3B3Avf8rKyrTm6UxPT0dDQ4Om6G1q3nXt2hXe3t64ffs2vvvuu87dx7bxojAdWmVlpcjKyhJZWVkCgPjggw9EVlaWuHbtmhBCiO3bt4tDhw6JvLw8sWvXLuHv7y9+85vfGDwmdKxS9f7774szZ86Ic+fOiaSkJCGTyZpdyerKlSvCyclJzJ8/X1y8eFGsX79edOnSRezfv9/o+Kl9MjXvcnNzRVJSkjh58qTIz88X33zzjQgICBCjR4/WtKmrqxODBw8WUVFRIjs7W+zfv1+4u7uLhIQEg7EdO3ZM2NnZiT//+c/i4sWLIjExUchkMnH27FlNm9b8u6C2Z428O3v2rHB3dxeTJ08WRUVFmk9paanB2JrLu8rKSvHHP/5RKJVKkZ+fL9LS0kRwcLAIDAwUNTU1FrhaZC7WyLuHHTp0yKhVno253928eVNkZWWJb7/9VgAQX375pcjKyhJFRUWmXRiyGGvk3O7du8WGDRvE2bNnRU5Ojvj444+Fk5OTWLp0qcHY2MfaLmve6zZu3CgcHByavcc1Yh9rm6yVc3yOpQeZo36yevVqoVAoxFdffSVycnLEkiVLhIODg8jNzdW0iYmJEcOGDRPHjx8XR48eFYGBgSIuLs5gbMbk3f79+8W+ffvElStXxIEDB8SQIUNESEiIUKvVZrxKHQsLiiZofOh4+DNlyhQhhBBr164Vvr6+QiaTiV69eoklS5YIlUpl8Ji6CooRERHCxcVFODg4iJCQELF3716j4xs6dKiwt7cXAQEBIjk5uUXxU/tkat4VFBSI0aNHix49egi5XC769esn5s+fL8rLy7XOc/XqVTFu3Djh6Ogo3NzcxJtvvilqa2ubjW/79u2if//+wt7eXgwaNEh8++23Wvtb8++C2p418i4xMVHnOfz9/ZuNz1DeVVdXi6ioKOHu7i5kMpnw9/cX06dPF8XFxWa7PmQZ1rrf6TqnMQ/bzd3vkpOTdcafmJjYmstBVmCNnNu3b58YOnSocHZ2Fl27dhVDhgwRn376qaivr282Pvaxtsma97rQ0FAxceLEFsXHPtb2WCvn+BxLDzJX/WTFihXC19dXODk5idDQUPHDDz9o7b9586aIi4sTzs7OQqFQiGnTponKykqj4jOUd9u2bRMBAQHC3t5eeHl5iVmzZomysrJWXw9bIBHi/mRBRERERERERERERM3gHIpERERERERERERkNBYUiYiIiIiIiIiIyGgsKBIREREREREREZHRWFAkIiIiIiIiIiIio7GgSEREREREREREREZjQZGIiIiIiIiIiIiMxoIiERERERERERERGY0FRSIiIiIiIiIiIjIaC4pERERERERERERkNBYUiYiIiIiIiIiIyGgsKBIREREREREREZHR/h+iRS7KkkUBTQAAAABJRU5ErkJggg==" - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_series(airline_bc)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.492637Z", - "start_time": "2024-03-01T16:16:26.315112Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": { - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.504605Z", - "start_time": "2024-03-01T16:16:26.493634Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": "pandas.core.frame.DataFrame" - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# this produces a pandas.DataFrame containing the feature vector for our single series\n", - "airline_summary = summary_trans.fit_transform(airline)\n", - "type(airline_summary)" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "outputs": [ - { - "data": { - "text/plain": " mean std min max 0.1 0.25 0.5 0.75 0.9\n0 280.298611 119.966317 104.0 622.0 135.3 180.0 265.5 360.5 453.2", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
meanstdminmax0.10.250.50.750.9
0280.298611119.966317104.0622.0135.3180.0265.5360.5453.2
\n
" - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "airline_summary" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.516574Z", - "start_time": "2024-03-01T16:16:26.505602Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "You can get a list of all series-to-series and series-to-vector transformers using\n", - "the output tags. Please consult the API for details on each transformations" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 38, - "outputs": [ - { - "data": { - "text/plain": " name \\\n0 Aggregator \n1 AutoCorrelationTransformer \n2 BKFilter \n3 BoxCoxTransformer \n4 ClaSPTransformer \n5 ClearSky \n6 CollectionToSeriesWrapper \n7 ColumnConcatenator \n8 ColumnSelect \n9 ColumnwiseTransformer \n10 ConditionalDeseasonalizer \n11 CosineTransformer \n12 DOBIN \n13 DateTimeFeatures \n14 Deseasonalizer \n15 Detrender \n16 Differencer \n17 EAgglo \n18 ExponentTransformer \n19 FeatureSelection \n20 FeatureUnion \n21 Filter \n22 FitInTransform \n23 FourierFeatures \n24 FunctionTransformer \n25 HampelFilter \n26 Hidalgo \n27 Id \n28 Imputer \n29 IndexSubset \n30 InvertAugmenter \n31 InvertTransform \n32 KalmanFilterTransformer \n33 Lag \n34 LogTransformer \n35 MatrixProfileSeriesTransformer \n36 MatrixProfileTransformer \n37 MultiplexTransformer \n38 OptionalPassthrough \n39 PCATransformer \n40 PandasTransformAdaptor \n41 PartialAutoCorrelationTransformer \n42 PlateauFinder \n43 RandomSamplesAugmenter \n44 Reconciler \n45 ReducerTransform \n46 ReverseAugmenter \n47 STLTransformer \n48 STRAY \n49 ScaledLogitTransformer \n50 SqrtTransformer \n51 TabularToSeriesAdaptor \n52 ThetaLinesTransformer \n53 TimeBinAggregate \n54 TimeSince \n55 TransformerPipeline \n56 WhiteNoiseAugmenter \n57 WindowSummarizer \n58 YtoX \n\n estimator \n0 \n3 \n6 \n13 \n18 \n22 \n27 \n28 \n29 \n30 \n34 \n40 \n49 ", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
nameestimator
0Aggregator<class 'aeon.transformations.hierarchical.aggr...
1AutoCorrelationTransformer<class 'aeon.transformations.acf.AutoCorrelati...
2BKFilter<class 'aeon.transformations.bkfilter.BKFilter'>
3BoxCoxTransformer<class 'aeon.transformations.boxcox.BoxCoxTran...
4ClaSPTransformer<class 'aeon.transformations.clasp.ClaSPTransf...
5ClearSky<class 'aeon.transformations.clear_sky.ClearSky'>
6CollectionToSeriesWrapper<class 'aeon.transformations.collection._colle...
7ColumnConcatenator<class 'aeon.transformations.compose.ColumnCon...
8ColumnSelect<class 'aeon.transformations.subset.ColumnSele...
9ColumnwiseTransformer<class 'aeon.transformations.compose.Columnwis...
10ConditionalDeseasonalizer<class 'aeon.transformations.detrend._deseason...
11CosineTransformer<class 'aeon.transformations.cos.CosineTransfo...
12DOBIN<class 'aeon.transformations.dobin.DOBIN'>
13DateTimeFeatures<class 'aeon.transformations.date.DateTimeFeat...
14Deseasonalizer<class 'aeon.transformations.detrend._deseason...
15Detrender<class 'aeon.transformations.detrend._detrend....
16Differencer<class 'aeon.transformations.difference.Differ...
17EAgglo<class 'aeon.annotation.eagglo.EAgglo'>
18ExponentTransformer<class 'aeon.transformations.exponent.Exponent...
19FeatureSelection<class 'aeon.transformations.feature_selection...
20FeatureUnion<class 'aeon.transformations.compose.FeatureUn...
21Filter<class 'aeon.transformations.filter.Filter'>
22FitInTransform<class 'aeon.transformations.compose.FitInTran...
23FourierFeatures<class 'aeon.transformations.fourier.FourierFe...
24FunctionTransformer<class 'aeon.transformations.func_transform.Fu...
25HampelFilter<class 'aeon.transformations.outlier_detection...
26Hidalgo<class 'aeon.transformations.hidalgo.Hidalgo'>
27Id<class 'aeon.transformations.compose.Id'>
28Imputer<class 'aeon.transformations.impute.Imputer'>
29IndexSubset<class 'aeon.transformations.subset.IndexSubset'>
30InvertAugmenter<class 'aeon.transformations.augmenter.InvertA...
31InvertTransform<class 'aeon.transformations.compose.InvertTra...
32KalmanFilterTransformer<class 'aeon.transformations.kalman_filter.Kal...
33Lag<class 'aeon.transformations.lag.Lag'>
34LogTransformer<class 'aeon.transformations.boxcox.LogTransfo...
35MatrixProfileSeriesTransformer<class 'aeon.transformations.series._matrix_pr...
36MatrixProfileTransformer<class 'aeon.transformations.matrix_profile.Ma...
37MultiplexTransformer<class 'aeon.transformations.compose.Multiplex...
38OptionalPassthrough<class 'aeon.transformations.compose.OptionalP...
39PCATransformer<class 'aeon.transformations.pca.PCATransformer'>
40PandasTransformAdaptor<class 'aeon.transformations.adapt.PandasTrans...
41PartialAutoCorrelationTransformer<class 'aeon.transformations.acf.PartialAutoCo...
42PlateauFinder<class 'aeon.transformations.summarize.Plateau...
43RandomSamplesAugmenter<class 'aeon.transformations.augmenter.RandomS...
44Reconciler<class 'aeon.transformations.hierarchical.reco...
45ReducerTransform<class 'aeon.transformations.lag.ReducerTransf...
46ReverseAugmenter<class 'aeon.transformations.augmenter.Reverse...
47STLTransformer<class 'aeon.transformations.detrend._deseason...
48STRAY<class 'aeon.anomaly_detection._stray.STRAY'>
49ScaledLogitTransformer<class 'aeon.transformations.scaledlogit.Scale...
50SqrtTransformer<class 'aeon.transformations.exponent.SqrtTran...
51TabularToSeriesAdaptor<class 'aeon.transformations.adapt.TabularToSe...
52ThetaLinesTransformer<class 'aeon.transformations.theta.ThetaLinesT...
53TimeBinAggregate<class 'aeon.transformations.binning.TimeBinAg...
54TimeSince<class 'aeon.transformations.time_since.TimeSi...
55TransformerPipeline<class 'aeon.transformations.compose.Transform...
56WhiteNoiseAugmenter<class 'aeon.transformations.augmenter.WhiteNo...
57WindowSummarizer<class 'aeon.transformations.summarize.WindowS...
58YtoX<class 'aeon.transformations.compose.YtoX'>
\n
" - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from aeon.registry import all_estimators\n", - "\n", - "all_estimators(\n", - " \"transformer\",\n", - " exclude_estimator_types=\"collection-transformer\",\n", - " filter_tags={\n", - " \"output_data_type\": [\"Series\", \"Collection\"],\n", - " },\n", - " as_dataframe=True,\n", - ")" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "is_executing": true - }, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.612317Z", - "start_time": "2024-03-01T16:16:26.517570Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 39, - "outputs": [ - { - "data": { - "text/plain": "Empty DataFrame\nColumns: [name, estimator]\nIndex: []", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n
nameestimator
\n
" - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "all_estimators(\n", - " \"transformer\",\n", - " exclude_estimator_types=\"collection-transformer\",\n", - " filter_tags={\n", - " \"output_data_type\": \"Tabular\",\n", - " },\n", - " as_dataframe=True,\n", - ")" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.686120Z", - "start_time": "2024-03-01T16:16:26.613314Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "If your series is split into training and testing data, you should call `fit` and\n", - "`transform` separately. `BoxCoxTransformer` has a parameter `lambda` that can be\n", - "learned from the train data:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": { - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.695096Z", - "start_time": "2024-03-01T16:16:26.687117Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": "Period\n1958-01 340.0\n1958-02 318.0\n1958-03 362.0\n1958-04 348.0\n1958-05 363.0\nFreq: M, Name: Number of airline passengers, dtype: float64" - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from aeon.forecasting.model_selection import temporal_train_test_split\n", - "\n", - "train, test = temporal_train_test_split(airline)\n", - "boxcox = BoxCoxTransformer(method=\"mle\")\n", - "test[:5]" - ] - }, - { - "cell_type": "markdown", - "source": [ - "You can then apply the model without refitting lambda using just `transform`:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": { - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.709058Z", - "start_time": "2024-03-01T16:16:26.698088Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": "Period\n1958-01 5.597723\n1958-02 5.536036\n1958-03 5.655489\n1958-04 5.619156\n1958-05 5.658029\nFreq: M, dtype: float64" - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# fit the transformer on the training data\n", - "boxcox.fit(train)\n", - "# apply to test data\n", - "test_new = boxcox.transform(test)\n", - "test_new[:5]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Fitted model components of transformers can be found with the `get_fitted_params()`\n", - "method:" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": { - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.716040Z", - "start_time": "2024-03-01T16:16:26.710056Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": "{'lambda': -0.01398297802065717}" - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "boxcox.get_fitted_params()\n", - "# this is a pandas.DataFrame that contains the fitted transformers" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Collection Transformers\n", - "\n", - "Collection transformers inherit from `BaseCollectionTransformer`, itself a subclass\n", - "of `BaseTransformer`. Collection transformers differ from the other transformers in\n", - "`aeon` in that the only accept collections of series, and they are more likely to not\n", - "transform each series independently. A `BaseCollectionTransformer` works best with the same\n", - "data structures used by clusterers, regressors and classifiers: 3D numpy of shape\n", - "`(n_cases, n_channels, n_timepoints)` for equal length series or a list of 2D numpy `[n_cases]`.\n", - " Like before, other valid collection input types can be used.\n", - " See the [data storage notebook](../datasets/data_structures.ipynb) for more\n", - " details.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Arrows shape (n_cases, n_channels, n_timepoints) = (211, 1, 251)\n", - "Motions shape (n_cases, n_channels, n_timepoints) = (80, 6, 100)\n", - "Covid shape (n_cases, n_channels, n_timepoints) = (201, 1, 84)\n" - ] - } - ], - "source": [ - "from aeon.datasets import load_arrow_head, load_basic_motions, load_covid_3month\n", - "\n", - "# univariate classification\n", - "arrows, arrows_labels = load_arrow_head()\n", - "# multivariate classification\n", - "motions, motions_labels = load_basic_motions()\n", - "# univariate regression\n", - "covid, covid_response = load_covid_3month()\n", - "\n", - "print(\"Arrows shape (n_cases, n_channels, n_timepoints) = \", arrows.shape)\n", - "print(\"Motions shape (n_cases, n_channels, n_timepoints) = \", motions.shape)\n", - "print(\"Covid shape (n_cases, n_channels, n_timepoints) = \", covid.shape)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:26.999718Z", - "start_time": "2024-03-01T16:16:26.954838Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Collection transformers can also be series-to-series or series-to-vector. Most transformers will\n", - "always transform a collection of $n$ series into a collection of $n$ series or\n", - "vectors. For example, `Catch22` transforms each channel of each series into 22 summary features.\n" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 49, - "outputs": [ - { - "data": { - "text/plain": "(211, 22)" - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from aeon.transformations.collection.feature_based import Catch22\n", - "\n", - "c22 = Catch22()\n", - "t = c22.fit_transform(arrows)\n", - "t.shape" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:27.121393Z", - "start_time": "2024-03-01T16:16:27.000715Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Series-to-series transformers transform each series into a different series. This can\n", - " mean it has a different number of channels and/or be different length. For example,\n", - " `ElbowClassPairwise` performs a supervised channel selection to reduce\n", - " dimensionality. In the example below, it selects the best two channels from BasicMotions." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 50, - "outputs": [ - { - "data": { - "text/plain": "(80, 2, 100)" - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from aeon.transformations.collection import ElbowClassPairwise\n", - "\n", - "ecp = ElbowClassPairwise()\n", - "t2 = ecp.fit_transform(motions, motions_labels)\n", - "t2.shape" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:27.152310Z", - "start_time": "2024-03-01T16:16:27.122391Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "series-to-vector Collection transformers return array-like objects of shape `(n_cases, n_features)`, so\n", - "they can be used with sklearn classifiers or regressors directly or in a pipeline. The following are equivalent." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 51, - "outputs": [ - { - "data": { - "text/plain": "0.6285714285714286" - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sklearn.linear_model import LogisticRegression\n", - "from sklearn.metrics import accuracy_score\n", - "from sklearn.model_selection import train_test_split\n", - "\n", - "arrows_train, arrows_test, y_train, y_test = train_test_split(\n", - " arrows, arrows_labels, test_size=0.33\n", - ")\n", - "\n", - "c22 = Catch22()\n", - "c22_train = c22.fit_transform(arrows_train, y_train)\n", - "\n", - "lr = LogisticRegression()\n", - "lr.fit(c22_train, y_train)\n", - "\n", - "c22_test = c22.transform(arrows_test, y_test)\n", - "preds = lr.predict(c22_test)\n", - "accuracy_score(y_test, preds)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:27.300647Z", - "start_time": "2024-03-01T16:16:27.153307Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 52, - "outputs": [ - { - "data": { - "text/plain": "0.6285714285714286" - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sklearn.pipeline import Pipeline\n", - "\n", - "pipe = Pipeline(steps=[(\"catch22\", c22), (\"logistic\", lr)])\n", - "pipe.fit(arrows_train, y_train)\n", - "preds = pipe.predict(arrows_test)\n", - "accuracy_score(y_test, preds)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:27.441605Z", - "start_time": "2024-03-01T16:16:27.301645Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Series-to-series collection transformers can be used in an sklearn pipeline with an\n", - "`aeon` classifier or regressor" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 53, - "outputs": [ - { - "data": { - "text/plain": "Pipeline(steps=[('ECP', ElbowClassPairwise()),\n ('knn', KNeighborsTimeSeriesRegressor(distance='euclidean'))])", - "text/html": "
Pipeline(steps=[('ECP', ElbowClassPairwise()),\n                ('knn', KNeighborsTimeSeriesRegressor(distance='euclidean'))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" - }, - "execution_count": 53, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sklearn.metrics import mean_squared_error\n", - "\n", - "from aeon.regression.distance_based import KNeighborsTimeSeriesRegressor\n", - "\n", - "knn = KNeighborsTimeSeriesRegressor(distance=\"euclidean\")\n", - "pipe = Pipeline(steps=[(\"ECP\", ecp), (\"knn\", knn)])\n", - "covid_train, covid_test, y_train, y_test = train_test_split(\n", - " covid, covid_response, test_size=0.75\n", - ")\n", - "pipe.fit(covid_train, y_train)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:30.862948Z", - "start_time": "2024-03-01T16:16:27.442603Z" - } - } - }, - { - "cell_type": "code", - "execution_count": 54, - "outputs": [ - { - "data": { - "text/plain": "0.0030565796424194182" - }, - "execution_count": 54, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "preds = pipe.predict(covid_test)\n", - "mean_squared_error(y_test, preds)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:30.880933Z", - "start_time": "2024-03-01T16:16:30.863946Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "### Wrapping as a general transformer\n", - "### Wrapping as a general transformer\n", - "\n", - "Collection transformers can be wrapped to have to same functionality as a `BaseTransformer` using the `CollectionToSeriesWrapper`." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 1, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'Catch22' is not defined", - "output_type": "error", - "traceback": [ - "\u001B[1;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[1;31mNameError\u001B[0m Traceback (most recent call last)", - "Cell \u001B[1;32mIn[1], line 3\u001B[0m\n\u001B[0;32m 1\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m \u001B[38;5;21;01maeon\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mtransformations\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mcollection\u001B[39;00m \u001B[38;5;28;01mimport\u001B[39;00m CollectionToSeriesWrapper\n\u001B[1;32m----> 3\u001B[0m c22 \u001B[38;5;241m=\u001B[39m \u001B[43mCatch22\u001B[49m()\n\u001B[0;32m 4\u001B[0m wrapper \u001B[38;5;241m=\u001B[39m CollectionToSeriesWrapper(c22) \u001B[38;5;66;03m# wrap transformer to accept single series\u001B[39;00m\n\u001B[0;32m 6\u001B[0m wrapper\u001B[38;5;241m.\u001B[39mfit_transform(airline)\n", - "\u001B[1;31mNameError\u001B[0m: name 'Catch22' is not defined" - ] - } - ], - "source": [ - "from aeon.transformations.collection import CollectionToSeriesWrapper\n", - "\n", - "c22 = Catch22()\n", - "wrapper = CollectionToSeriesWrapper(c22) # wrap transformer to accept single series\n", - "\n", - "wrapper.fit_transform(airline)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-03-01T16:16:30.935785Z", - "start_time": "2024-03-01T16:16:30.881930Z" - } - } - } - ], - "metadata": { - "hide_input": false, - "kernelspec": { - "display_name": "Python 3.8.12 ('aeon-baseobject')", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.12" - }, - "latex_envs": { - "LaTeX_envs_menu_present": true, - "autoclose": false, - "autocomplete": true, - "bibliofile": "biblio.bib", - "cite_by": "apalike", - "current_citInitial": 1, - "eqLabelWithNumbers": true, - "eqNumInitial": 1, - "hotkeys": { - "equation": "Ctrl-E", - "itemize": "Ctrl-I" - }, - "labels_anchors": false, - "latex_user_defs": false, - "report_style_numbering": false, - "user_envs_cfg": false - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - }, - "vscode": { - "interpreter": { - "hash": "ff39becaf9fb8fe58d1d34fc2c63ee411a5dd80719d9cd02520236b9e8461a42" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -}