Repository: alan-turing-institute/sktime
Pull request #3248 (2022-08-12 16:32:06)

[ENH] Channel selection for multivariate time series classification.
<!--
Thanks for contributing a pull request! Please ensure you have taken a look
at our contribution guide: https://github.com/alan-turing-institute/sktime/blob/main/CONTRIBUTING.md
-->

#### Reference Issues/PRs
#2934
Partially related to mentioned PR. The methods are implemented from [this](https://link.springer.com/chapter/10.1007/978-3-030-91445-5_3) paper. 

<!--
Example: Fixes #1234. See also #3456.

Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->


#### What does this implement/fix? Explain your changes.
<!--
A clear and concise description of what you have implemented.
-->

#### Does your contribution introduce a new dependency? If yes, which one?

<!--
If your contribution does add a new hard dependency, we may suggest to initially develop your contribution in a separate companion package in https://github.com/sktime/ to keep external dependencies of the core sktime package to a minimum.
-->

#### What should a reviewer concentrate their feedback on?

<!-- This section is particularly useful if you have a pull request that is still in development. You can guide the reviews to focus on the parts that are ready for their comments. We suggest using bullets (indicated by * or -) and filled checkboxes [x] here -->

#### Any other comments?
<!--
Please be aware that we are a loose team of volunteers so patience is necessary; assistance handling other issues is very welcome. We value all user contributions, no matter how minor they are. If we are slow to review, either the pull request needs some benchmarking, tinkering, convincing, etc. or more likely the reviewers are simply busy. In either case, we ask for your understanding during the review process.
-->

#### PR checklist
<!--
Please go through the checklist below. Please feel free to remove points if they are not applicable.
-->

##### For all contributions
- [X] I've added myself to the [list of contributors](https://github.com/alan-turing-institute/sktime/blob/main/.all-contributorsrc).
- [X] Optionally, I've updated sktime's [CODEOWNERS](https://github.com/alan-turing-institute/sktime/blob/main/CODEOWNERS) to receive notifications about future changes to these files.
- [X] I've added unit tests and made sure they pass locally.
- [X] The PR title starts with either [ENH], [MNT], [DOC], or [BUG] indicating whether the PR topic is related to enhancement, maintenance, documentation, or bug.

##### For new estimators
- [ ] I've added the estimator to the online documentation.
- [ ] I've updated the existing example notebooks or provided a new one to showcase how my estimator works.


<!--
Thanks for contributing!
-->


review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/alan-turing-institute/sktime/pull/3248"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
fkiraly: Hm, it might actually be `FlatDist` (from this PR https://github.com/alan-turing-institute/sktime/pull/3249) instead of `AggrDist`, i.e., `FlatDist(ScipyDist())`
fkiraly: Also, your doctests are failing. The code in there must run with `sktime` installed, and it does not, e.g., there is a type where you confused `sktime` and `sklearn`.

To test whether the docstring is running, easy way is to copy paste it into a visual studio code notebook and run it. You can copy paste with `>>>` and `...` and it will still run.
haskarb: Thanks for leaving comments.

So in the paper, we have used euclidean to save computation time, so we have fixed euclidean distance. We also tried to use 'dtw' in another [paper](https://arxiv.org/pdf/2206.09274.pdf); it is under review. I will update/contribute the code once the review process is over.

Also, I can't find FlatDist [here](https://github.com/alan-turing-institute/sktime/blob/main/sktime/dists_kernels/compose_tab_to_panel.py)
fkiraly: > In the paper, we have used euclidean to save computation time, so we have fixed euclidean distance. 

Well, just leave that as the default then, and we keep the distance modular.
If you don't want to do that, I can do that in a separate PR later, if you don't mind?
fkiraly: > Also, I can't find FlatDist [here](https://github.com/alan-turing-institute/sktime/blob/main/sktime/dists_kernels/compose_tab_to_panel.py)

Still in a PR, so to use it , you would have to merge it in.

Question: is it euclidean distance on flattened series, or is it average of euclidean distances between time pts?
haskarb: > Or, if you don't want to do that, I can do that in a separate PR later, if you don't mind?

Yes, I'm okay with that.

> Question: is it euclidean distance on flattened series, or is it average of euclidean distances between time pts?

It is euclidean distance between two vectors (or two univariate time-series)


fkiraly: > Also, I see some of the tests failing, I'm curious what's going wrong?

Do you know how to see the failing tests, and how to get the traceback? Go on "details" next to the test.
haskarb: Ah, the channel selection was not defined. Thanks, think I found the problem.
fkiraly: still failing - try to copy-paste the example block in a visual studio code and run it, to see what fails
fkiraly: (if unclear what I'm saying, ping me on slack and I can show you when I'm online - or ask in the contributors channel)
haskarb: Sure, thanks. I will try to work on this tomorrow.

On Sat 13 Aug 2022, 10:51 Franz Király, ***@***.***> wrote:

> (if unclear what I'm saying, ping me on slack and I can show you when I'm
> online - or ask in the contributors channel)
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/alan-turing-institute/sktime/pull/3248#issuecomment-1214120674>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AE4NEH3W2TR4WWZW2OXGLGLVY5VZVANCNFSM56MG4UFA>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

fkiraly: Fixed it for you because it's non-obvious.

If you have an expression going over multiple lines in a docstring, the second line needs to start with triple dots and a space (see what I fixed).
fkiraly: Re docstring: I fixed only the first one, you should be able to do the 2nd by following the pattern.

Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 8aca5c8e5..b641b5a29 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1747,7 +1747,17 @@
       "contributions": [
         "bug"
       ]
-    }
+    },
+    {
+      "login": "haskarb",
+      "name": "Bhaskar Dhariyal",
+      "avatar_url": "https://avatars.githubusercontent.com/u/20501023?v=4",
+      "profile": "https://haskarb.github.io/",
+      "contributions": [
+        "code",
+        "test"
+      ]
+    },
   ],
   "projectName": "sktime",
   "projectOwner": "alan-turing-institute",
diff --git a/CODEOWNERS b/CODEOWNERS
index 63a8a64ea..de3020a15 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -36,6 +36,7 @@ sktime/transformations/series/kalman_filter.py @NoaBenAmi
 sktime/transformations/panel/augmenter.py @MrPr3ntice @iljamaurer
 sktime/transformations/multiplex.py @miraep8
 sktime/transformations/tests/test_multiplexer.py @miraep8
+sktime/transformations/panel/channel_selection.py @haskarb @a-pasos-ruiz @TonyBagnall
 
 sktime/forecasting/base/ @fkiraly @mloning @aiwalter
 sktime/forecasting/base/adapters/_statsforecast.py @FedericoGarza
diff --git a/examples/channel_selection.ipynb b/examples/channel_selection.ipynb
new file mode 100644
index 000000000..7c5a799e3
--- /dev/null
+++ b/examples/channel_selection.ipynb
@@ -0,0 +1,534 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "89a363cd-8944-475a-88ea-4401785218c5",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import warnings\n",
+    "\n",
+    "warnings.filterwarnings(\"ignore\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b45cbeda-65bc-4b10-9f9c-9afd43a54d20",
+   "metadata": {},
+   "source": [
+    "# Channel Selection in Multivariate Time Series Classification \n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "08decb5b-8dfb-4666-a66b-3a2349960956",
+   "metadata": {},
+   "source": [
+    "## Overview"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "da743484-17d3-4cec-8eaa-8d8293ce6f35",
+   "metadata": {},
+   "source": [
+    "Sometimes every channel is not required to perform classification; only a few are useful.  The [1] proposed a fast channel selection technique for Multivariate Time Classification. "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "dcbe2174-a691-4093-ab80-d796edb5121d",
+   "metadata": {},
+   "source": [
+    "[1] : Fast Channel Selection for Scalable Multivariate Time Series Classification [Link](https://www.researchgate.net/publication/354445008_Fast_Channel_Selection_for_Scalable_Multivariate_Time_Series_Classification)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "d1779970-eefb-4577-9c4e-e0a19ceadcc1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from sklearn.linear_model import RidgeClassifierCV\n",
+    "from sklearn.pipeline import make_pipeline\n",
+    "\n",
+    "from sktime.datasets import load_UCR_UEA_dataset\n",
+    "from sktime.transformations.panel import channel_selection\n",
+    "from sktime.transformations.panel.rocket import Rocket"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0437ca7a-5b5a-4e28-b565-0b2df4eac60d",
+   "metadata": {},
+   "source": [
+    "# 1 Initialise the Pipeline"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "830137a3-10c3-49b9-9a98-7062dc7ab1d8",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# cs = channel_selection.ElbowClassSum()  # ECS\n",
+    "cs = channel_selection.ElbowClassPairwise()  # ECP"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "89443793-7cf0-4a4c-a4b1-d928a46a1bb2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "rocket_pipeline = make_pipeline(cs, Rocket(), RidgeClassifierCV())"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5a268cc1-d5bf-4b02-916b-f3417c1cd3ff",
+   "metadata": {},
+   "source": [
+    "# 2 Load and Fit the Training Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "68f508e3-ecc9-4b3a-b4de-7073cd1dfb90",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "data = \"BasicMotions\"\n",
+    "X_train, y_train = load_UCR_UEA_dataset(data, split=\"train\", return_X_y=True)\n",
+    "X_test, y_test = load_UCR_UEA_dataset(data, split=\"test\", return_X_y=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "94f421bc-e384-4b98-89af-111a4d8c378b",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Pipeline(steps=[('elbowclasspairwise', ElbowClassPairwise()),\n",
+       "                ('rocket', Rocket()),\n",
+       "                ('ridgeclassifiercv',\n",
+       "                 RidgeClassifierCV(alphas=array([ 0.1,  1. , 10. ])))])"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "rocket_pipeline.fit(X_train, y_train)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0c867fdb-5126-44f6-b7f0-f999d3f60457",
+   "metadata": {},
+   "source": [
+    "# 3 Classify the Test Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "04573f4d-0b61-4ab8-8355-79f0aa1ca04f",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "1.0"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "rocket_pipeline.score(X_test, y_test)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d18ac8bc-a83a-4dd7-b577-aefc25d7bed6",
+   "metadata": {},
+   "source": [
+    "# 4 Identify channels"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "35a44d68-7bce-44b0-baf3-e4f11606001c",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[0, 1]"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "rocket_pipeline.steps[0][1].channels_selected_"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "358ab28f-edbe-49f4-95f5-8a7d0fb5d166",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Centroid_badminton_running</th>\n",
+       "      <th>Centroid_badminton_standing</th>\n",
+       "      <th>Centroid_badminton_walking</th>\n",
+       "      <th>Centroid_running_standing</th>\n",
+       "      <th>Centroid_running_walking</th>\n",
+       "      <th>Centroid_standing_walking</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>39.594679</td>\n",
+       "      <td>55.752785</td>\n",
+       "      <td>48.440779</td>\n",
+       "      <td>63.610220</td>\n",
+       "      <td>57.247383</td>\n",
+       "      <td>10.717044</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>57.681767</td>\n",
+       "      <td>24.390543</td>\n",
+       "      <td>27.770269</td>\n",
+       "      <td>60.458125</td>\n",
+       "      <td>62.339120</td>\n",
+       "      <td>16.370347</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>20.175911</td>\n",
+       "      <td>24.126969</td>\n",
+       "      <td>22.331621</td>\n",
+       "      <td>25.671979</td>\n",
+       "      <td>22.991555</td>\n",
+       "      <td>4.897452</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>12.546212</td>\n",
+       "      <td>12.439152</td>\n",
+       "      <td>12.741854</td>\n",
+       "      <td>6.317654</td>\n",
+       "      <td>6.695743</td>\n",
+       "      <td>3.585273</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>10.101196</td>\n",
+       "      <td>8.865871</td>\n",
+       "      <td>9.221908</td>\n",
+       "      <td>6.520172</td>\n",
+       "      <td>6.715702</td>\n",
+       "      <td>1.299989</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>23.464251</td>\n",
+       "      <td>14.568685</td>\n",
+       "      <td>13.953445</td>\n",
+       "      <td>18.878429</td>\n",
+       "      <td>19.768549</td>\n",
+       "      <td>7.228389</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Centroid_badminton_running  Centroid_badminton_standing  \\\n",
+       "0                   39.594679                    55.752785   \n",
+       "1                   57.681767                    24.390543   \n",
+       "2                   20.175911                    24.126969   \n",
+       "3                   12.546212                    12.439152   \n",
+       "4                   10.101196                     8.865871   \n",
+       "5                   23.464251                    14.568685   \n",
+       "\n",
+       "   Centroid_badminton_walking  Centroid_running_standing  \\\n",
+       "0                   48.440779                  63.610220   \n",
+       "1                   27.770269                  60.458125   \n",
+       "2                   22.331621                  25.671979   \n",
+       "3                   12.741854                   6.317654   \n",
+       "4                    9.221908                   6.520172   \n",
+       "5                   13.953445                  18.878429   \n",
+       "\n",
+       "   Centroid_running_walking  Centroid_standing_walking  \n",
+       "0                 57.247383                  10.717044  \n",
+       "1                 62.339120                  16.370347  \n",
+       "2                 22.991555                   4.897452  \n",
+       "3                  6.695743                   3.585273  \n",
+       "4                  6.715702                   1.299989  \n",
+       "5                 19.768549                   7.228389  "
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "rocket_pipeline.steps[0][1].distance_frame_"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c75f99ea-3966-483b-9f08-89e3f0dbffeb",
+   "metadata": {},
+   "source": [
+    "# 5 Standalone"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "82607728-1095-4f15-a06e-d2463bb5c642",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "ElbowClassPairwise()"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cs.fit(X_train, y_train)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a1f3ec36-ce86-4388-b7b5-b3b2a087b43a",
+   "metadata": {},
+   "source": [
+    "# 6 Distance Matrix"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "f4a19774-368e-43d4-a45a-d8109ae2d17f",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Centroid_badminton_running</th>\n",
+       "      <th>Centroid_badminton_standing</th>\n",
+       "      <th>Centroid_badminton_walking</th>\n",
+       "      <th>Centroid_running_standing</th>\n",
+       "      <th>Centroid_running_walking</th>\n",
+       "      <th>Centroid_standing_walking</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>39.594679</td>\n",
+       "      <td>55.752785</td>\n",
+       "      <td>48.440779</td>\n",
+       "      <td>63.610220</td>\n",
+       "      <td>57.247383</td>\n",
+       "      <td>10.717044</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>57.681767</td>\n",
+       "      <td>24.390543</td>\n",
+       "      <td>27.770269</td>\n",
+       "      <td>60.458125</td>\n",
+       "      <td>62.339120</td>\n",
+       "      <td>16.370347</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>20.175911</td>\n",
+       "      <td>24.126969</td>\n",
+       "      <td>22.331621</td>\n",
+       "      <td>25.671979</td>\n",
+       "      <td>22.991555</td>\n",
+       "      <td>4.897452</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>12.546212</td>\n",
+       "      <td>12.439152</td>\n",
+       "      <td>12.741854</td>\n",
+       "      <td>6.317654</td>\n",
+       "      <td>6.695743</td>\n",
+       "      <td>3.585273</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>10.101196</td>\n",
+       "      <td>8.865871</td>\n",
+       "      <td>9.221908</td>\n",
+       "      <td>6.520172</td>\n",
+       "      <td>6.715702</td>\n",
+       "      <td>1.299989</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>5</th>\n",
+       "      <td>23.464251</td>\n",
+       "      <td>14.568685</td>\n",
+       "      <td>13.953445</td>\n",
+       "      <td>18.878429</td>\n",
+       "      <td>19.768549</td>\n",
+       "      <td>7.228389</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "   Centroid_badminton_running  Centroid_badminton_standing  \\\n",
+       "0                   39.594679                    55.752785   \n",
+       "1                   57.681767                    24.390543   \n",
+       "2                   20.175911                    24.126969   \n",
+       "3                   12.546212                    12.439152   \n",
+       "4                   10.101196                     8.865871   \n",
+       "5                   23.464251                    14.568685   \n",
+       "\n",
+       "   Centroid_badminton_walking  Centroid_running_standing  \\\n",
+       "0                   48.440779                  63.610220   \n",
+       "1                   27.770269                  60.458125   \n",
+       "2                   22.331621                  25.671979   \n",
+       "3                   12.741854                   6.317654   \n",
+       "4                    9.221908                   6.520172   \n",
+       "5                   13.953445                  18.878429   \n",
+       "\n",
+       "   Centroid_running_walking  Centroid_standing_walking  \n",
+       "0                 57.247383                  10.717044  \n",
+       "1                 62.339120                  16.370347  \n",
+       "2                 22.991555                   4.897452  \n",
+       "3                  6.695743                   3.585273  \n",
+       "4                  6.715702                   1.299989  \n",
+       "5                 19.768549                   7.228389  "
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cs.distance_frame_"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "a29b0ece",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "13"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cs.train_time_"
+   ]
+  }
+ ],
+ "metadata": {
+  "interpreter": {
+   "hash": "30ff7f6bb2505d289b6e6022e217e794dc64e9153f959b8a264cb3c597a35999"
+  },
+  "kernelspec": {
+   "display_name": "Python 3.7.5 ('sktime-test')",
+   "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.7.5"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/sktime/forecasting/compose/_reduce.py b/sktime/forecasting/compose/_reduce.py
index 3ec20ac87..ee54baa99 100644
--- a/sktime/forecasting/compose/_reduce.py
+++ b/sktime/forecasting/compose/_reduce.py
@@ -1315,6 +1315,7 @@ class DirectReductionForecaster(BaseForecaster):
 
     _tags = {
         "requires-fh-in-fit": True,  # is the forecasting horizon required in fit?
+        "ignores-exogeneous-X": False,
         "X_inner_mtype": "pd.DataFrame",
         "y_inner_mtype": "pd.DataFrame",
     }
diff --git a/sktime/transformations/panel/channel_selection.py b/sktime/transformations/panel/channel_selection.py
new file mode 100644
index 000000000..103cbe002
--- /dev/null
+++ b/sktime/transformations/panel/channel_selection.py
@@ -0,0 +1,332 @@
+# -*- coding: utf-8 -*-
+"""Channel Selection techniques for Multivariate Time Series Classification.
+
+A transformer that selects a subset of channels/dimensions for time series
+classification using a scoring system with an elbow point method.
+"""
+
+__author__ = ["haskarb", "a-pasos-ruiz", "TonyBagnall"]
+__all__ = ["ElbowClassSum", "ElbowClassPairwise"]
+
+
+import itertools
+import time
+
+import numpy as np
+import pandas as pd
+from scipy.spatial.distance import euclidean
+from sklearn.neighbors import NearestCentroid
+
+from sktime.datatypes._panel._convert import from_3d_numpy_to_nested
+from sktime.transformations.base import BaseTransformer
+
+
+def _eu_dist(x, y):
+    """Calculate the euclidean distance."""
+    return euclidean(x, y)
+
+
+def _detect_knee_point(values, indices):
+    """Find elbow point."""
+    n_points = len(values)
+    all_coords = np.vstack((range(n_points), values)).T
+    first_point = all_coords[0]
+    line_vec = all_coords[-1] - all_coords[0]
+    line_vec_norm = line_vec / np.sqrt(np.sum(line_vec**2))
+    vec_from_first = all_coords - first_point
+    scalar_prod = np.sum(vec_from_first * np.tile(line_vec_norm, (n_points, 1)), axis=1)
+    vec_from_first_parallel = np.outer(scalar_prod, line_vec_norm)
+    vec_to_line = vec_from_first - vec_from_first_parallel
+    dist_to_line = np.sqrt(np.sum(vec_to_line**2, axis=1))
+    knee_idx = np.argmax(dist_to_line)
+    knee = values[knee_idx]
+    best_dims = [idx for (elem, idx) in zip(values, indices) if elem > knee]
+    if len(best_dims) == 0:
+        return [knee_idx], knee_idx
+
+    return (best_dims,)
+
+
+class _distance_matrix:
+    """Create distance matrix."""
+
+    def distance(self, centroid_frame):
+        """Fuction to create DM."""
+        distance_pair = list(
+            itertools.combinations(range(0, centroid_frame.shape[0]), 2)
+        )
+        # exit()
+
+        map_cls = centroid_frame.class_vals.to_dict()
+        distance_frame = pd.DataFrame()
+        for class_ in distance_pair:
+
+            class_pair = []
+            # calculate the distance of centroid here
+            for _, (q, t) in enumerate(
+                zip(
+                    centroid_frame.drop(["class_vals"], axis=1).iloc[class_[0], :],
+                    centroid_frame.iloc[class_[1], :],
+                )
+            ):
+                # print(eu_dist(q.values, t.values))
+                class_pair.append(_eu_dist(q.values, t.values))
+                dict_ = {
+                    f"Centroid_{map_cls[class_[0]]}_{map_cls[class_[1]]}": class_pair
+                }
+                # print(class_[0])
+
+            distance_frame = pd.concat([distance_frame, pd.DataFrame(dict_)], axis=1)
+
+        return distance_frame
+
+
+class _shrunk_centroid:
+    """Create centroid."""
+
+    def __init__(self, shrink=0):
+        self.shrink = shrink
+
+    def create_centroid(self, X, y):
+        """Create the centroid for each class."""
+        _, ncols, _ = X.shape
+        cols = ["dim_" + str(i) for i in range(ncols)]
+        ts = X
+        centroids = []
+
+        # le = LabelEncoder()
+        # y_ind = le.fit_transform(y)
+
+        for dim in range(ts.shape[1]):
+            train = ts[:, dim, :]
+            clf = NearestCentroid(train)
+            clf = NearestCentroid(shrink_threshold=self.shrink)
+            clf.fit(train, y)
+            centroids.append(clf.centroids_)
+
+        centroid_frame = from_3d_numpy_to_nested(
+            np.stack(centroids, axis=1), column_names=cols
+        )
+        centroid_frame["class_vals"] = clf.classes_
+
+        return centroid_frame.reset_index(drop=True)
+
+
+class ElbowClassSum(BaseTransformer):
+    """Elbow Class Sum (ECS) transformer to select a subset of channels.
+
+    Overview: From the input of multivariate time series data, create a distance
+    matrix [1] by calculating the distance between each class centroid. The
+    ECS selects the subset of channels using the elbow method, which maximizes the
+    distance between the class centroids by aggregating the distance for every
+    class pair across each channel.
+
+    Note: Channels, variables, dimensions, features are used interchangeably in
+    literature.
+
+    Attributes
+    ----------
+    channels_selected_ : list of integers; integer being the index of the channel
+        List of channels selected by the ECS.
+    distance_frame_ : DataFrame
+        distance matrix of the class centroids pair and channels.
+            ``shape = [n_channels, n_class_centroids_pairs]``
+        Table 1 provides an illustration in [1].
+    train_time_ : int
+        Time taken to train the ECS.
+
+    Notes
+    -----
+    Original repository: https://github.com/mlgig/Channel-Selection-MTSC
+
+    References
+    ----------
+    ..[1]: Bhaskar Dhariyal et al. “Fast Channel Selection for Scalable Multivariate
+    Time Series Classification.” AALTD, ECML-PKDD, Springer, 2021
+
+    Examples
+    --------
+    >>> from sktime.transformations.panel.channel_selection import ElbowClassSum
+    >>> from sktime.datasets import load_UCR_UEA_dataset
+    >>> cs = ElbowClassSum()
+    >>> X_train, y_train = load_UCR_UEA_dataset(
+    ...     "Cricket", split="train", return_X_y=True
+    ... )
+    >>> cs.fit(X_train, y_train)
+    ElbowClassSum(...)
+    >>> Xt = cs.transform(X_train)
+    """
+
+    _tags = {
+        "scitype:transform-input": "Series",
+        # what is the scitype of X: Series, or Panel
+        # "scitype:transform-output": "Primitives",
+        # what scitype is returned: Primitives, Series, Panel
+        "scitype:instancewise": True,  # is this an instance-wise transform?
+        "univariate-only": False,  # can the transformer handle multivariate X?
+        "X_inner_mtype": "numpy3D",  # which mtypes do _fit/_predict support for X?
+        "y_inner_mtype": "numpy1D",  # which mtypes do _fit/_predict support for y?
+        "requires_y": True,  # does y need to be passed in fit?
+        "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
+        "skip-inverse-transform": True,  # is inverse-transform skipped when called?
+        "capability:unequal_length": False,
+        # can the transformer handle unequal length time series (if passed Panel)?
+    }
+
+    def __init__(self):
+
+        super(ElbowClassSum, self).__init__()
+
+    def _fit(self, X, y):
+        """Fit ECS to a specified X and y.
+
+        Parameters
+        ----------
+        X: pandas DataFrame or np.ndarray
+            The training input samples.
+        y: array-like or list
+            The class values for X.
+
+        Returns
+        -------
+        self : reference to self.
+
+        """
+        self.channels_selected_ = []
+        start = int(round(time.time() * 1000))
+        centroid_obj = _shrunk_centroid(0)
+        centroids = centroid_obj.create_centroid(X.copy(), y)
+        distances = _distance_matrix()
+        self.distance_frame_ = distances.distance(centroids)
+        distance = self.distance_frame_.sum(axis=1).sort_values(ascending=False).values
+        indices = self.distance_frame_.sum(axis=1).sort_values(ascending=False).index
+        self.channels_selected_.extend(_detect_knee_point(distance, indices)[0])
+        self.train_time_ = int(round(time.time() * 1000)) - start
+        return self
+
+    def _transform(self, X, y=None):
+        """
+        Transform X and return a transformed version.
+
+        Parameters
+        ----------
+        X : pandas DataFrame or np.ndarray
+            The input data to transform.
+
+        Returns
+        -------
+        output : pandas DataFrame
+            X with a subset of channels
+        """
+        return X[:, self.channels_selected_, :]
+
+
+class ElbowClassPairwise(BaseTransformer):
+    """Elbow Class Pairwise (ECP) transformer to select a subset of channels.
+
+    Overview: From the input of multivariate time series data, create a distance
+    matrix [1] by calculating the distance between each class centroid. The ECP
+    selects the subset of channels using the elbow method that maximizes the
+    distance between each class centroids pair across all channels.
+
+    Note: Channels, variables, dimensions, features are used interchangeably in
+    literature.
+
+    Attributes
+    ----------
+    channels_selected_ : list of integers; integer being the index of the channel
+        List of channels selected by the ECS.
+    distance_frame_ : DataFrame
+        distance matrix of the class centroids pair and channels.
+            ``shape = [n_channels, n_class_centroids_pairs]``
+        Table 1 provides an illustration in [1].
+    train_time_ : int
+        Time taken to train the ECP.
+
+    Notes
+    -----
+    Original repository: https://github.com/mlgig/Channel-Selection-MTSC
+
+    References
+    ----------
+    ..[1]: Bhaskar Dhariyal et al. “Fast Channel Selection for Scalable Multivariate
+    Time Series Classification.” AALTD, ECML-PKDD, Springer, 2021
+
+    Examples
+    --------
+    >>> from sktime.transformations.panel.channel_selection import ElbowClassPairwise
+    >>> from sktime.datasets import load_UCR_UEA_dataset
+    >>> cs = ElbowClassPairwise()
+    >>> X_train, y_train = load_UCR_UEA_dataset(
+    ...     "Cricket", split="train", return_X_y=True
+    ... )
+    >>> cs.fit(X_train, y_train)
+    ElbowClassPairwise(...)
+    >>> Xt = cs.transform(X_train)
+    """
+
+    _tags = {
+        "scitype:transform-input": "Series",
+        # what is the scitype of X: Series, or Panel
+        # "scitype:transform-output": "Primitives",
+        # what scitype is returned: Primitives, Series, Panel
+        "scitype:instancewise": True,  # is this an instance-wise transform?
+        "univariate-only": False,  # can the transformer handle multivariate X?
+        "X_inner_mtype": "numpy3D",  # which mtypes do _fit/_predict support for X?
+        "y_inner_mtype": "numpy1D",  # which mtypes do _fit/_predict support for y?
+        "requires_y": True,  # does y need to be passed in fit?
+        "fit_is_empty": False,  # is fit empty and can be skipped? Yes = True
+        "skip-inverse-transform": True,  # is inverse-transform skipped when called?
+        "capability:unequal_length": False,
+        # can the transformer handle unequal length time series (if passed Panel)?
+    }
+
+    def __init__(self):
+        super(ElbowClassPairwise, self).__init__()
+
+    def _fit(self, X, y):
+        """Fit ECP to a specified X and y.
+
+        Parameters
+        ----------
+        X: pandas DataFrame or np.ndarray
+            The training input samples.
+        y: array-like or list
+            The class values for X.
+
+        Returns
+        -------
+        self : reference to self.
+
+        """
+        self.channels_selected_ = []
+        start = int(round(time.time() * 1000))
+        centroid_obj = _shrunk_centroid(0)
+        df = centroid_obj.create_centroid(X.copy(), y)
+        obj = _distance_matrix()
+        self.distance_frame_ = obj.distance(df)
+
+        for pairdistance in self.distance_frame_.iteritems():
+            distance = pairdistance[1].sort_values(ascending=False).values
+            indices = pairdistance[1].sort_values(ascending=False).index
+            self.channels_selected_.extend(_detect_knee_point(distance, indices)[0])
+            self.channels_selected_ = list(set(self.channels_selected_))
+        self.train_time_ = int(round(time.time() * 1000)) - start
+        # self._is_fitted = True
+        return self
+
+    def _transform(self, X, y=None):
+        """
+        Transform X and return a transformed version.
+
+        Parameters
+        ----------
+        X : pandas DataFrame or np.ndarray
+            The input data to transform.
+
+        Returns
+        -------
+        output : pandas DataFrame
+            X with a subset of channels
+        """
+        return X[:, self.channels_selected_, :]
diff --git a/sktime/transformations/series/lag.py b/sktime/transformations/series/lag.py
index 1fa39932f..88d7644b3 100644
--- a/sktime/transformations/series/lag.py
+++ b/sktime/transformations/series/lag.py
@@ -259,13 +259,10 @@ class Lag(BaseTransformer):
 
             Xt_list.append(Xt)
 
-        if len(Xt_list) == 1:
-            Xt = Xt_list[0]
-        else:
-            lag_names = self._yield_shift_param_names()
-            Xt = pd.concat(Xt_list, axis=1, keys=lag_names, names=["lag", "variable"])
-            if self.flatten_transform_index:
-                Xt.columns = flatten_multiindex(Xt.columns)
+        lag_names = self._yield_shift_param_names()
+        Xt = pd.concat(Xt_list, axis=1, keys=lag_names, names=["lag", "variable"])
+        if self.flatten_transform_index:
+            Xt.columns = flatten_multiindex(Xt.columns)
 
         # some pandas versions do not sort index automatically after concat
         # so removing will break specific pandas versions


Test patch:
diff --git a/sktime/transformations/panel/tests/test_channel_selection.py b/sktime/transformations/panel/tests/test_channel_selection.py
new file mode 100644
index 000000000..9af05f459
--- /dev/null
+++ b/sktime/transformations/panel/tests/test_channel_selection.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+"""Channel selection test code."""
+from sktime.datasets import load_basic_motions
+from sktime.transformations.panel.channel_selection import ElbowClassPairwise
+
+
+def test_cs_basic_motions():
+    """Test channel selection on basic motions dataset."""
+    X, y = load_basic_motions(split="train", return_X_y=True)
+
+    ecp = ElbowClassPairwise()
+
+    ecp.fit(X, y)
+
+    # transform the training data
+
+    ecp.transform(X, y)
+
+    # test shape pf transformed data should be (n_samples, n_channels_selected)
+    assert ecp.transform(X, y).shape == (X.shape[0], len(ecp.channels_selected_))
+
+    # test shape of transformed data should be (n_samples, n_channels_selected)
+
+    X_test, y_test = load_basic_motions(split="test", return_X_y=True)
+
+    assert ecp.transform(X_test).shape == (X_test.shape[0], len(ecp.channels_selected_))
diff --git a/sktime/transformations/series/tests/test_featureizer.py b/sktime/transformations/series/tests/test_featureizer.py
index 53a00eb38..1f7149e1b 100644
--- a/sktime/transformations/series/tests/test_featureizer.py
+++ b/sktime/transformations/series/tests/test_featureizer.py
@@ -28,4 +28,4 @@ def test_featurized_values():
     exp_transformer = ExponentTransformer()
     expected_len = lags + len(y_test)
     y_hat = exp_transformer.fit_transform(y[-expected_len:])
-    assert_array_equal(X_hat["TOTEMP"].values, y_hat.values)
+    assert_array_equal(X_hat[f"lag_{lags}__TOTEMP"].values, y_hat.values)
diff --git a/sktime/transformations/series/tests/test_lag.py b/sktime/transformations/series/tests/test_lag.py
index 02819643b..e496f72db 100644
--- a/sktime/transformations/series/tests/test_lag.py
+++ b/sktime/transformations/series/tests/test_lag.py
@@ -4,6 +4,8 @@
 
 __author__ = ["fkiraly"]
 
+import itertools
+
 import pandas as pd
 import pytest
 
@@ -63,3 +65,31 @@ def test_lag_fit_transform_columns(X, index_out, lag):
             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/sktime/transformations/series/tests/test_optional_passthrough.py b/sktime/transformations/series/tests/test_optional_passthrough.py
new file mode 100644
index 000000000..490d5d5a3
--- /dev/null
+++ b/sktime/transformations/series/tests/test_optional_passthrough.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Tests for OptionalPassthrough transformer."""
+
+import pytest
+from pandas.testing import assert_series_equal
+
+from sktime.transformations.series.boxcox import BoxCoxTransformer
+from sktime.transformations.series.compose import OptionalPassthrough
+from sktime.utils._testing.series import _make_series
+
+
+@pytest.mark.parametrize("passthrough", [True, False])
+def test_passthrough(passthrough):
+    """Test that passthrough works as expected."""
+    y = _make_series(n_columns=1)
+
+    optional_passthourgh = OptionalPassthrough(
+        BoxCoxTransformer(), passthrough=passthrough
+    )
+    box_cox = BoxCoxTransformer()
+
+    y_hat_passthrough = optional_passthourgh.fit_transform(y)
+    y_inv_passthrough = optional_passthourgh.inverse_transform(y_hat_passthrough)
+
+    y_hat_boxcox = box_cox.fit_transform(y)
+    y_inv_boxcox = box_cox.inverse_transform(y_hat_boxcox)
+
+    assert_series_equal(y, y_inv_passthrough)
+
+    if passthrough:
+        assert_series_equal(y, y_hat_passthrough)
+        with pytest.raises(AssertionError):
+            assert_series_equal(y_hat_boxcox, y_hat_passthrough)
+    else:
+        assert_series_equal(y_hat_passthrough, y_hat_boxcox)
+        assert_series_equal(y_inv_passthrough, y_inv_boxcox)
+
+        with pytest.raises(AssertionError):
+            assert_series_equal(y, y_hat_passthrough)