From fc2f82463a741a61276cfdb123373d614ba3a1d9 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Tue, 26 Mar 2024 09:49:50 +0100 Subject: [PATCH 01/38] included rsast --- .../shapelet_based/_rsast_classifier.py | 200 +++++ .../collection/shapelet_based/_rsast.py | 685 ++++++++++++++++++ 2 files changed, 885 insertions(+) create mode 100644 aeon/classification/shapelet_based/_rsast_classifier.py create mode 100644 aeon/transformations/collection/shapelet_based/_rsast.py diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py new file mode 100644 index 0000000000..f71e03328e --- /dev/null +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -0,0 +1,200 @@ +"""Random Scalable and Accurate Subsequence Transform (RSAST). + +Pipeline classifier using the RSAST transformer and an sklearn classifier. +""" + +__maintainer__ = [] +__all__ = ["RSASTClassifier"] + +from operator import itemgetter + +import numpy as np +from sklearn.linear_model import RidgeClassifierCV +from sklearn.pipeline import make_pipeline + +from aeon.base._base import _clone_estimator +from aeon.classification import BaseClassifier +from aeon.transformations.collection.shapelet_based import RSAST +from aeon.utils.numba.general import z_normalise_series + + +class RSASTClassifier(BaseClassifier): + """Classification pipeline using RSAST [1]_ transformer and an sklean classifier. + + Parameters + ---------- + n_random_points: int default = 10 the number of initial random points to extract + len_method: string default="both" the type of statistical tool used to get the length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS + nb_inst_per_class : int default = 10 + the number of reference time series to select per class + seed : int, default = None + the seed of the random generator + classifier : sklearn compatible classifier, default = None + if None, a RidgeClassifierCV(alphas=np.logspace(-3, 3, 10)) is used. + n_jobs : int, default -1 + Number of threads to use for the transform. + + + Reference + --------- + .. [1] ... + "..." + ... + + Examples + -------- + >>> from aeon.classification.shapelet_based import SASTClassifier + >>> 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") + >>> clf = SASTClassifier() + >>> clf.fit(X_train, y_train) + SASTClassifier(...) + >>> y_pred = clf.predict(X_test) + """ + + _tags = { + "capability:multithreading": True, + "capability:multivariate": False, + "algorithm_type": "subsequence", + } + + def __init__( + self, + n_random_points=10, + len_method="both", + nb_inst_per_class=10, + seed=None, + classifier=None, + n_jobs=-1, + ): + super().__init__() + self.n_random_points=n_random_points, + self.len_method=len_method, + self.nb_inst_per_class = nb_inst_per_class + self.n_jobs = n_jobs + self.seed = seed + + self.classifier = classifier + + def _fit(self, X, y): + """Fit RSASTClassifier to the training data. + + Parameters + ---------- + X: np.ndarray shape (n_cases, n_channels, n_timepoints) + The training input samples. + y: array-like or list + The class values for X. + + Return + ------ + self : RSASTClassifier + This pipeline classifier + + """ + self._transformer = RSAST( + self.n_random_points=n_random_points, + self.len_method=len_method, + self.nb_inst_per_class, + self.seed, + self.n_jobs, + ) + + self._classifier = _clone_estimator( + ( + RidgeClassifierCV(alphas=np.logspace(-3, 3, 10)) + if self.classifier is None + else self.classifier + ), + self.seed, + ) + + self._pipeline = make_pipeline(self._transformer, self._classifier) + + self._pipeline.fit(X, y) + + return self + + def _predict(self, X): + """Predict labels for the input. + + Parameters + ---------- + X: np.ndarray shape (n_cases, n_channels, n_timepoints) + The training input samples. + + Return + ------ + array-like or list + Predicted class labels. + """ + return self._pipeline.predict(X) + + def _predict_proba(self, X): + """Predict labels probabilities for the input. + + Parameters + ---------- + X: np.ndarray shape (n_cases, n_channels, n_timepoints) + The training input samples. + + Return + ------ + dists : np.ndarray shape (n_cases, n_timepoints) + Predicted class probabilities. + """ + m = getattr(self._classifier, "predict_proba", None) + if callable(m): + dists = self._pipeline.predict_proba(X) + else: + dists = np.zeros((X.shape[0], self.n_classes_)) + preds = self._pipeline.predict(X) + for i in range(0, X.shape[0]): + dists[i, np.where(self.classes_ == preds[i])] = 1 + return dists + + def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): + """Plot the most important features on ts. + + Parameters + ---------- + ts : float[:] + The time series + feature_importance : float[:] + The importance of each feature in the transformed data + limit : int, default = 5 + The maximum number of features to plot + + Returns + ------- + fig : plt.figure + The figure + """ + import matplotlib.pyplot as plt + + features = zip(self._transformer._kernel_orig, feature_importance) + sorted_features = sorted(features, key=itemgetter(1), reverse=True) + + max_ = min(limit, len(sorted_features)) + + fig, axes = plt.subplots( + 1, max_, sharey=True, figsize=(3 * max_, 3), tight_layout=True + ) + + for f in range(max_): + kernel, _ = sorted_features[f] + znorm_kernel = z_normalise_series(kernel) + d_best = np.inf + for i in range(ts.size - kernel.size): + s = ts[i : i + kernel.size] + s = z_normalise_series(s) + d = np.sum((s - znorm_kernel) ** 2) + if d < d_best: + d_best = d + start_pos = i + axes[f].plot(range(start_pos, start_pos + kernel.size), kernel, linewidth=5) + axes[f].plot(range(ts.size), ts, linewidth=2) + axes[f].set_title(f"feature: {f+1}") + + return fig diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py new file mode 100644 index 0000000000..8a7a26782e --- /dev/null +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -0,0 +1,685 @@ +# -*- coding: utf-8 -*- +""" +Spyder Editor + +This is a temporary script file. +""" + +import numpy as np + +from sklearn.base import BaseEstimator, ClassifierMixin, clone +from sklearn.utils.validation import check_array, check_X_y, check_is_fitted + +from sklearn.ensemble import RandomForestClassifier, VotingClassifier + +from sklearn.linear_model import RidgeClassifierCV, LogisticRegressionCV, LogisticRegression, RidgeClassifier + + +from sklearn.linear_model._base import LinearClassifierMixin +from sklearn.pipeline import Pipeline + +#from sktime.utils.data_processing import from_2d_array_to_nested +#from sktime.transformations.panel.rocket import Rocket + +from numba import njit, prange + +#from mass_ts import * + +import pandas as pd + +from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning +from statsmodels.tsa.stattools import acf, pacf + +import time + +import os +from operator import itemgetter + + + +from utils_sast import from_2d_array_to_nested, znormalize_array, load_dataset, format_dataset, plot_most_important_features, plot_most_important_feature_on_ts, plot_most_important_feature_sast_on_ts +from aeon.classification.shapelet_based import RDSTClassifier +#from sktime.datasets import load_UCR_UEA_dataset + + + + + +@njit(fastmath=False) +def apply_kernel(ts, arr): + d_best = np.inf # sdist + m = ts.shape[0] + kernel = arr[~np.isnan(arr)] # ignore nan + + # profile = mass2(ts, kernel) + # d_best = np.min(profile) + + l = kernel.shape[0] + for i in range(m - l + 1): + d = np.sum((znormalize_array(ts[i:i+l]) - kernel)**2) + if d < d_best: + d_best = d + + return d_best + + +@njit(parallel=True, fastmath=True) +def apply_kernels(X, kernels): + nbk = len(kernels) + out = np.zeros((X.shape[0], nbk), dtype=np.float32) + for i in prange(nbk): + k = kernels[i] + for t in range(X.shape[0]): + ts = X[t] + out[t][i] = apply_kernel(ts, k) + return out + + +class SAST(BaseEstimator, ClassifierMixin): + + def __init__(self, cand_length_list, shp_step=1, nb_inst_per_class=1, random_state=None, classifier=None): + super(SAST, self).__init__() + self.cand_length_list = cand_length_list + self.shp_step = shp_step + self.nb_inst_per_class = nb_inst_per_class + self.kernels_ = None + self.kernel_orig_ = None # not z-normalized kernels + self.kernels_generators_ = {} + self.random_state = np.random.RandomState(random_state) if not isinstance( + random_state, np.random.RandomState) else random_state + + self.classifier = classifier + + def get_params(self, deep=True): + return { + 'cand_length_list': self.cand_length_list, + 'shp_step': self.shp_step, + 'nb_inst_per_class': self.nb_inst_per_class, + 'classifier': self.classifier + } + + def init_sast(self, X, y): + + self.cand_length_list = np.array(sorted(self.cand_length_list)) + + assert self.cand_length_list.ndim == 1, 'Invalid shapelet length list: required list or tuple, or a 1d numpy array' + + if self.classifier is None: + self.classifier = RandomForestClassifier( + min_impurity_decrease=0.05, max_features=None) + + classes = np.unique(y) + self.num_classes = classes.shape[0] + + candidates_ts = [] + for c in classes: + X_c = X[y == c] + + # convert to int because if self.nb_inst_per_class is float, the result of np.min() will be float + cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) + choosen = self.random_state.permutation(X_c.shape[0])[:cnt] + candidates_ts.append(X_c[choosen]) + self.kernels_generators_[c] = X_c[choosen] + + candidates_ts = np.concatenate(candidates_ts, axis=0) + + self.cand_length_list = self.cand_length_list[self.cand_length_list <= X.shape[1]] + + max_shp_length = max(self.cand_length_list) + + n, m = candidates_ts.shape + + n_kernels = n * np.sum([m - l + 1 for l in self.cand_length_list]) + + self.kernels_ = np.full( + (n_kernels, max_shp_length), dtype=np.float32, fill_value=np.nan) + self.kernel_orig_ = [] + + k = 0 + + for shp_length in self.cand_length_list: + for i in range(candidates_ts.shape[0]): + for j in range(0, candidates_ts.shape[1] - shp_length + 1, self.shp_step): + end = j + shp_length + can = np.squeeze(candidates_ts[i][j: end]) + self.kernel_orig_.append(can) + self.kernels_[k, :shp_length] = znormalize_array(can) + + k += 1 + + def fit(self, X, y): + + X, y = check_X_y(X, y) # check the shape of the data + + # randomly choose reference time series and generate kernels + self.init_sast(X, y) + + # subsequence transform of X + X_transformed = apply_kernels(X, self.kernels_) + + self.classifier.fit(X_transformed, y) # fit the classifier + + return self + + def predict(self, X): + + check_is_fitted(self) # make sure the classifier is fitted + + X = check_array(X) # validate the shape of X + + # subsequence transform of X + X_transformed = apply_kernels(X, self.kernels_) + + return self.classifier.predict(X_transformed) + + def predict_proba(self, X): + check_is_fitted(self) # make sure the classifier is fitted + + X = check_array(X) # validate the shape of X + + # subsequence transform of X + X_transformed = apply_kernels(X, self.kernels_) + + if isinstance(self.classifier, LinearClassifierMixin): + return self.classifier._predict_proba_lr(X_transformed) + return self.classifier.predict_proba(X_transformed) + + +class SASTEnsemble(BaseEstimator, ClassifierMixin): + + def __init__(self, cand_length_list, shp_step=1, nb_inst_per_class=1, random_state=None, classifier=None, weights=None, n_jobs=None): + super(SASTEnsemble, self).__init__() + self.cand_length_list = cand_length_list + self.shp_step = shp_step + self.nb_inst_per_class = nb_inst_per_class + self.classifier = classifier + self.random_state = random_state + self.n_jobs = n_jobs + + self.saste = None + + self.weights = weights + + assert isinstance(self.classifier, BaseEstimator) + + self.init_ensemble() + + def init_ensemble(self): + estimators = [] + for i, candidate_lengths in enumerate(self.cand_length_list): + clf = clone(self.classifier) + sast = SAST(cand_length_list=candidate_lengths, + nb_inst_per_class=self.nb_inst_per_class, + random_state=self.random_state, + shp_step=self.shp_step, + classifier=clf) + estimators.append((f'sast{i}', sast)) + + self.saste = VotingClassifier( + estimators=estimators, voting='soft', n_jobs=self.n_jobs, weights=self.weights) + + def fit(self, X, y): + self.saste.fit(X, y) + return self + + def predict(self, X): + return self.saste.predict(X) + + def predict_proba(self, X): + return self.saste.predict_proba(X) + + + +class RSAST(BaseEstimator, ClassifierMixin): + + def __init__(self,n_random_points=10, nb_inst_per_class=10, len_method="both", random_state=None, classifier=None, sel_inst_wrepl=False,sel_randp_wrepl=False, half_instance=False, half_len=False,n_shapelet_samples=None ): + super(RSAST, self).__init__() + self.n_random_points = n_random_points + self.nb_inst_per_class = nb_inst_per_class + self.len_method = len_method + self.random_state = np.random.RandomState(random_state) if not isinstance( + random_state, np.random.RandomState) else random_state + self.classifier = classifier + self.cand_length_list = None + self.kernels_ = None + self.kernel_orig_ = None # not z-normalized kernels + self.kernel_permutated_ = None + self.kernels_generators_ = None + self.class_generators_ = None + self.sel_inst_wrepl=sel_inst_wrepl + self.sel_randp_wrepl=sel_randp_wrepl + self.half_instance=half_instance + self.half_len=half_len + self.time_calculating_weights = None + self.time_creating_subsequences = None + self.time_transform_dataset = None + self.time_classifier = None + self.n_shapelet_samples =n_shapelet_samples + + def get_params(self, deep=True): + return { + 'len_method': self.len_method, + 'n_random_points': self.n_random_points, + 'nb_inst_per_class': self.nb_inst_per_class, + 'sel_inst_wrepl':self.sel_inst_wrepl, + 'sel_randp_wrepl':self.sel_randp_wrepl, + 'half_instance':self.half_instance, + 'half_len':self.half_len, + 'classifier': self.classifier, + 'cand_length_list': self.cand_length_list + } + + def init_sast(self, X, y): + #0- initialize variables and convert values in "y" to string + start = time.time() + y=np.asarray([str(x_s) for x_s in y]) + + self.cand_length_list = {} + self.kernel_orig_ = [] + self.kernels_generators_ = [] + self.class_generators_ = [] + + list_kernels =[] + + + + n = [] + classes = np.unique(y) + self.num_classes = classes.shape[0] + m_kernel = 0 + + #1--calculate ANOVA per each time t throught the lenght of the TS + for i in range (X.shape[1]): + statistic_per_class= {} + for c in classes: + assert len(X[np.where(y==c)[0]][:,i])> 0, 'Time t without values in TS' + + statistic_per_class[c]=X[np.where(y==c)[0]][:,i] + #print("statistic_per_class- i:"+str(i)+', c:'+str(c)) + #print(statistic_per_class[c].shape) + + + #print('Without pd series') + #print(statistic_per_class) + + statistic_per_class=pd.Series(statistic_per_class) + #statistic_per_class = list(statistic_per_class.values()) + # Calculate t-statistic and p-value + + try: + t_statistic, p_value = f_oneway(*statistic_per_class) + except DegenerateDataWarning or ConstantInputWarning: + p_value=np.nan + # Interpretation of the results + # if p_value < 0.05: " The means of the populations are significantly different." + #print('pvalue', str(p_value)) + if np.isnan(p_value): + n.append(0) + else: + n.append(1-p_value) + end = time.time() + self.time_calculating_weights = end-start + + + #2--calculate PACF and ACF for each TS chossen in each class + start = time.time() + for i, c in enumerate(classes): + X_c = X[y == c] + if self.half_instance==True: + cnt = np.max([X_c.shape[0]//2, 1]).astype(int) + self.nb_inst_per_class=cnt + else: + cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) + #set if the selection of instances is with replacement (if false it is not posible to select the same intance more than one) + if self.sel_inst_wrepl ==False: + choosen = self.random_state.permutation(X_c.shape[0])[:cnt] + else: + choosen = self.random_state.choice(X_c.shape[0], cnt) + + + + + for rep, idx in enumerate(choosen): + self.cand_length_list[c+","+str(idx)+","+str(rep)] = [] + non_zero_acf=[] + if (self.len_method == "both" or self.len_method == "ACF" or self.len_method == "Max ACF") : + #2.1-- Compute Autorrelation per object + acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) + prev_acf=0 + for j, conf in enumerate(acf_confint): + + if(3<=j and (0 < acf_confint[j][0] <= acf_confint[j][1] or acf_confint[j][0] <= acf_confint[j][1] < 0) ): + #Consider just the maximum ACF value + if prev_acf!=0 and self.len_method == "Max ACF": + non_zero_acf.remove(prev_acf) + self.cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_acf) + non_zero_acf.append(j) + self.cand_length_list[c+","+str(idx)+","+str(rep)].append(j) + prev_acf=j + + non_zero_pacf=[] + if (self.len_method == "both" or self.len_method == "PACF" or self.len_method == "Max PACF"): + #2.2 Compute Partial Autorrelation per object + pacf_val, pacf_confint = pacf(X_c[idx], method="ols", nlags=(len(X_c[idx])//2) - 1, alpha=.05) + prev_pacf=0 + for j, conf in enumerate(pacf_confint): + + if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0) ): + #Consider just the maximum PACF value + if prev_pacf!=0 and self.len_method == "Max PACF": + non_zero_pacf.remove(prev_pacf) + self.cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_pacf) + + non_zero_pacf.append(j) + self.cand_length_list[c+","+str(idx)+","+str(rep)].append(j) + prev_pacf=j + + if (self.len_method == "all"): + self.cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3,1+ len(X_c[idx]))) + + #2.3-- Save the maximum autocorralated lag value as shapelet lenght + + if len(self.cand_length_list[c+","+str(idx)+","+str(rep)])==0: + #chose a random lenght using the lenght of the time series (added 1 since the range start in 0) + rand_value= self.random_state.choice(len(X_c[idx]), 1)[0]+1 + self.cand_length_list[c+","+str(idx)+","+str(rep)].extend([max(3,rand_value)]) + #elif len(non_zero_acf)==0: + #print("There is no AC in TS", idx, " of class ",c) + #elif len(non_zero_pacf)==0: + #print("There is no PAC in TS", idx, " of class ",c) + #else: + #print("There is AC and PAC in TS", idx, " of class ",c) + + #print("Kernel lenght list:",self.cand_length_list[c+","+str(idx)],"") + + #remove duplicates for the list of lenghts + self.cand_length_list[c+","+str(idx)+","+str(rep)]=list(set(self.cand_length_list[c+","+str(idx)+","+str(rep)])) + #print("Len list:"+str(self.cand_length_list[c+","+str(idx)+","+str(rep)])) + for max_shp_length in self.cand_length_list[c+","+str(idx)+","+str(rep)]: + + #2.4-- Choose randomly n_random_points point for a TS + #2.5-- calculate the weights of probabilities for a random point in a TS + if sum(n) == 0 : + # Determine equal weights of a random point point in TS is there are no significant points + # print('All p values in One way ANOVA are equal to 0') + weights = [1/len(n) for i in range(len(n))] + weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) + else: + # Determine the weights of a random point point in TS (excluding points after n-l+1) + weights = n / np.sum(n) + weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) + + if self.half_len==True: + self.n_random_points=np.max([len(X_c[idx])//2, 1]).astype(int) + + + if self.n_random_points > len(X_c[idx])-max_shp_length+1 and self.sel_randp_wrepl==False: + #set a upper limit for the posible of number of random points when selecting without replacement + limit_rpoint=len(X_c[idx])-max_shp_length+1 + rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p=weights, replace=self.sel_randp_wrepl) + #print("limit_rpoint:"+str(limit_rpoint)) + else: + rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p=weights, replace=self.sel_randp_wrepl) + #print("n_random_points:"+str(self.n_random_points)) + + #print("rpoints:"+str(rand_point_ts)) + + for i in rand_point_ts: + #2.6-- Extract the subsequence with that point + kernel = X_c[idx][i:i+max_shp_length].reshape(1,-1) + #print("kernel:"+str(kernel)) + if m_kernel n_samples (intances) + self.classifier=RidgeClassifierCV() + print("RidgeClassifierCV:"+str("size training")+str(X_transformed.shape[0])+"<="+" kernels"+str(X_transformed.shape[1])) + else: + print("LogisticRegression:"+str("size training")+str(X_transformed.shape[0])+">"+" kernels"+str(X_transformed.shape[1])) + self.classifier=LogisticRegression() + #self.classifier = RandomForestClassifier(min_impurity_decrease=0.05, max_features=None) + + start = time.time() + #print('X_transformed shape') + #print(X_transformed.shape) + #print('X_transformed') + #print(X_transformed) + + self.classifier.fit(X_transformed, y) # fit the classifier + end = time.time() + self.time_classifier = end-start + + return self + + def predict(self, X): + + check_is_fitted(self) # make sure the classifier is fitted + + X = check_array(X) # validate the shape of X + + # subsequence transform of X + X_transformed = apply_kernels(X, self.kernels_) + + return self.classifier.predict(X_transformed) + + def predict_proba(self, X): + check_is_fitted(self) # make sure the classifier is fitted + + X = check_array(X) # validate the shape of X + + # subsequence transform of X + X_transformed = apply_kernels(X, self.kernels_) + + if isinstance(self.classifier, LinearClassifierMixin): + return self.classifier._predict_proba_lr(X_transformed) + return self.classifier.predict_proba(X_transformed) + + +if __name__ == "__main__": + + ds='Chinatown' # Chosing a dataset from # Number of classes to consider + + rtype="numpy2D" + + #X_train, y_train = load_UCR_UEA_dataset(name=ds, split="train",extract_path="data", return_type=rtype) + + + #X_train=np.nan_to_num(X_train) + #y_train=np.nan_to_num(y_train) + + #X_test, y_test = load_UCR_UEA_dataset(name=ds, split="test", extract_path="data", return_type=rtype) + + #X_test=np.nan_to_num(X_test) + #y_test=np.nan_to_num(y_test) + #print('Format: load_UCR_UEA_dataset') + #print(X_train.shape) + #print(X_test.shape) + #print(y_train.shape) + #print(y_test.shape) + + + #y_train = list(map(int, y_train)) + #y_test =list(map(int, y_test)) + #print(X_train[0]) + + """ + print("ds:"+ds) + X_train_mod=[] + for i , element in enumerate(X_train): + element=np.array(element[0]) + print("TS N:"+str(i)+" len:"+str(element.shape)) + #print(element) + X_train_mod.append(element) + + X_train_mod= np.array(X_train_mod) + print(X_train_mod.shape) + + X_train_mod=np.nan_to_num(X_train_mod) + """ + + path=r"C:\Users\Surface pro\random_sast\sast\data" + ds_train_lds , ds_test_lds = load_dataset(ds_folder=path,ds_name=ds,shuffle=False) + X_test_lds, y_test_lds = format_dataset(ds_test_lds) + X_train_lds, y_train_lds = format_dataset(ds_train_lds) + + X_train_lds=np.nan_to_num(X_train_lds) + y_train_lds=np.nan_to_num(y_train_lds) + X_test_lds=np.nan_to_num(X_test_lds) + y_test_lds=np.nan_to_num(y_test_lds) + + print('Format: load_dataset') + print(X_train_lds.shape) + print(X_train_lds[0].shape) + print(X_train_lds[1].shape) + print(X_test_lds.shape) + + + print(y_train_lds.shape) + print(y_test_lds.shape) + + + + + start = time.time() + random_state = None + rsast_ridge = RSAST(n_random_points=10, nb_inst_per_class=10, len_method="both") + rsast_ridge.fit(X_train_lds, y_train_lds) + end = time.time() + print('rsast score :', rsast_ridge.score(X_test_lds, y_test_lds)) + print('duration:', end-start) + print('params:', rsast_ridge.get_params()) + + #print('classifier:',rsast_ridge.classifier.coef_[0]) + + #fname = f'images/chinatown-rf-class{c}-top5-features-on-ref-ts.jpg' + #print(f"ts.shape{pd.array(rsast_ridge.kernels_generators_).shape}") + #print(f"kernel_d.shape{pd.array(rsast_ridge.kernel_orig_).shape}") + + plot_most_important_feature_on_ts(set_ts=rsast_ridge.kernels_generators_, labels=rsast_ridge.class_generators_, features=rsast_ridge.kernel_orig_, scores=rsast_ridge.classifier.coef_[0], limit=3, offset=0,znormalized=False) + + plot_most_important_features(rsast_ridge.kernel_orig_, rsast_ridge.classifier.coef_[0], limit=3,scale_color=False) + + X_train = X_train_lds[:, np.newaxis, :] + X_test = X_test_lds[:, np.newaxis, :] + y_train=np.asarray([int(x_s) for x_s in y_train_lds]) + y_test=np.asarray([int(x_s) for x_s in y_test_lds]) + start = time.time() + + rdst = RDSTClassifier( + max_shapelets=4, + shapelet_lengths=[7], + proba_normalization=0, + save_transformed_data=True + ) + rdst = RDSTClassifier(proba_normalization=0) + rdst.fit(X_train, y_train) + end = time.time() + + + + print('rdst score :', rdst.score(X_test, y_test)) + print('duration:', end-start) + print('params:', rdst.get_params()) + """ + for i, shp in enumerate(rdst._transformer.shapelets_[0].squeeze()): + print('rdst shapelet values:',str(i+1)," shape:", shp.shape," shapelet:", shp ) + + for i, dilation in enumerate(rdst._transformer.shapelets_[2].squeeze()): + print('rdst dilation parameter:',str(i+1)," shape:", shp.shape," dilation:", dilation ) + + for i, treshold in enumerate(rdst._transformer.shapelets_[3].squeeze()): + print('rdst treshold parameter:',str(i+1)," shape:", shp.shape," treshold:", treshold ) + + for i, normalization in enumerate(rdst._transformer.shapelets_[4].squeeze()): + print('rdst normalization parameter:',str(i+1)," shape:", shp.shape," normalization:", normalization ) + + for i, coef in enumerate(rdst._estimator["ridgeclassifiercv"].coef_): + print('rdst coef:',str(i+1)," shape:", coef.shape," coef:", coef ) + """ + + features_cl=rdst._transformer.shapelets_[0].squeeze() + dilations_cl=rdst._transformer.shapelets_[2].squeeze() + + coef_cl=rdst._estimator["ridgeclassifiercv"].coef_[0] + features_cl=[a for a in features_cl for i in range(3)] + dilations_cl=[a for a in dilations_cl for i in range(3)] + type_features_cl=["min","argmin","SO"]*len(features_cl) + + for l in pd.unique(rsast_ridge.class_generators_): + + all=zip(rsast_ridge.kernels_generators_,rsast_ridge.class_generators_) + + ts_cl=list(filter(lambda x: x[1]==l,all))[0][0] + ts_cl=[ts_cl for i in range(len(features_cl))] + labels=[l for i in range(len(features_cl))] + plot_most_important_feature_on_ts(set_ts=ts_cl, labels=labels, features=features_cl, scores=coef_cl,dilations=dilations_cl,type_features=type_features_cl, limit=3, offset=0,znormalized=False) + plot_most_important_features(features_cl, coef_cl, dilations=dilations_cl, limit=3, scale_color=False) + """ + min_shp_length = 3 + max_shp_length = X_train_lds.shape[1] + candidate_lengths = np.arange(min_shp_length, max_shp_length+1) + # candidate_lengths = (3, 7, 9, 11) + nb_inst_per_class = 1 + ridge = RidgeClassifierCV(alphas = np.logspace(-3, 3, 10)) + + start = time.time() + random_state = None + sast_ridge = SAST(cand_length_list=candidate_lengths, + nb_inst_per_class=nb_inst_per_class, + random_state=random_state, classifier=ridge) + sast_ridge.fit(X_train_lds, y_train_lds) + end = time.time() + print('sast score :', sast_ridge.score(X_test_lds, y_test_lds)) + print('duration:', end-start) + print('params:', sast_ridge.get_params()) + #print('classifier:',rsast_ridge.classifier.coef_[0]) + + #fname = f'images/chinatown-rf-class{c}-top5-features-on-ref-ts.jpg' + #print(f"ts.shape{pd.array(rsast_ridge.kernels_generators_).shape}") + #print(f"kernel_d.shape{pd.array(rsast_ridge.kernel_orig_).shape}") + for c, ts in sast_ridge.kernels_generators_.items(): + plot_most_important_feature_sast_on_ts(ts.squeeze(), c, sast_ridge.kernel_orig_, sast_ridge.classifier.coef_[0], limit=3, offset=0) # plot only the first model one-vs-all model's features + """ From deeddbf8e9e8b6b8bf95d283428c46f603ae3d91 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Fri, 29 Mar 2024 13:48:02 +0100 Subject: [PATCH 02/38] updated transformer and classifier --- .../classification/shapelet_based/__init__.py | 2 + .../shapelet_based/_rsast_classifier.py | 22 +- .../collection/shapelet_based/__init__.py | 3 +- .../collection/shapelet_based/_rsast.py | 428 +++--------------- 4 files changed, 81 insertions(+), 374 deletions(-) diff --git a/aeon/classification/shapelet_based/__init__.py b/aeon/classification/shapelet_based/__init__.py index 6f5edda5ec..f810ae0259 100644 --- a/aeon/classification/shapelet_based/__init__.py +++ b/aeon/classification/shapelet_based/__init__.py @@ -5,9 +5,11 @@ "ShapeletTransformClassifier", "RDSTClassifier", "SASTClassifier", + "RSASTClassifier", ] from aeon.classification.shapelet_based._mrsqm import MrSQMClassifier from aeon.classification.shapelet_based._rdst import RDSTClassifier from aeon.classification.shapelet_based._sast_classifier import SASTClassifier +from aeon.classification.shapelet_based._rsast_classifier import RSASTClassifier from aeon.classification.shapelet_based._stc import ShapeletTransformClassifier diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index f71e03328e..4d7b800a44 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -37,19 +37,18 @@ class RSASTClassifier(BaseClassifier): Reference --------- - .. [1] ... - "..." - ... - + .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for Time Series Classification. + https://hal.science/hal-04311309/ + Examples -------- - >>> from aeon.classification.shapelet_based import SASTClassifier + >>> from aeon.classification.shapelet_based import RSASTClassifier >>> 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") - >>> clf = SASTClassifier() + >>> clf = RSASTClassifier() >>> clf.fit(X_train, y_train) - SASTClassifier(...) + RSASTClassifier(...) >>> y_pred = clf.predict(X_test) """ @@ -69,12 +68,11 @@ def __init__( n_jobs=-1, ): super().__init__() - self.n_random_points=n_random_points, - self.len_method=len_method, + self.n_random_points = n_random_points, + self.len_method = len_method, self.nb_inst_per_class = nb_inst_per_class self.n_jobs = n_jobs self.seed = seed - self.classifier = classifier def _fit(self, X, y): @@ -94,8 +92,8 @@ def _fit(self, X, y): """ self._transformer = RSAST( - self.n_random_points=n_random_points, - self.len_method=len_method, + self.n_random_points, + self.len_method, self.nb_inst_per_class, self.seed, self.n_jobs, diff --git a/aeon/transformations/collection/shapelet_based/__init__.py b/aeon/transformations/collection/shapelet_based/__init__.py index 11ba7222a0..5b134c2c56 100644 --- a/aeon/transformations/collection/shapelet_based/__init__.py +++ b/aeon/transformations/collection/shapelet_based/__init__.py @@ -1,11 +1,12 @@ """Shapelet based transformers.""" -__all__ = ["RandomShapeletTransform", "RandomDilatedShapeletTransform", "SAST"] +__all__ = ["RandomShapeletTransform", "RandomDilatedShapeletTransform", "SAST", "RSAST" ] from aeon.transformations.collection.shapelet_based._dilated_shapelet_transform import ( RandomDilatedShapeletTransform, ) from aeon.transformations.collection.shapelet_based._sast import SAST +from aeon.transformations.collection.shapelet_based._rsast import RSAST from aeon.transformations.collection.shapelet_based._shapelet_transform import ( RandomShapeletTransform, ) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 8a7a26782e..cb4c247003 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -1,47 +1,9 @@ -# -*- coding: utf-8 -*- -""" -Spyder Editor - -This is a temporary script file. -""" - import numpy as np +from numba import get_num_threads, njit, prange, set_num_threads -from sklearn.base import BaseEstimator, ClassifierMixin, clone -from sklearn.utils.validation import check_array, check_X_y, check_is_fitted - -from sklearn.ensemble import RandomForestClassifier, VotingClassifier - -from sklearn.linear_model import RidgeClassifierCV, LogisticRegressionCV, LogisticRegression, RidgeClassifier - - -from sklearn.linear_model._base import LinearClassifierMixin -from sklearn.pipeline import Pipeline - -#from sktime.utils.data_processing import from_2d_array_to_nested -#from sktime.transformations.panel.rocket import Rocket - -from numba import njit, prange - -#from mass_ts import * - -import pandas as pd - -from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning -from statsmodels.tsa.stattools import acf, pacf - -import time - -import os -from operator import itemgetter - - - -from utils_sast import from_2d_array_to_nested, znormalize_array, load_dataset, format_dataset, plot_most_important_features, plot_most_important_feature_on_ts, plot_most_important_feature_sast_on_ts -from aeon.classification.shapelet_based import RDSTClassifier -#from sktime.datasets import load_UCR_UEA_dataset - - +from aeon.transformations.collection import BaseCollectionTransformer +from aeon.utils.numba.general import z_normalise_series +from aeon.utils.validation import check_n_jobs @@ -56,7 +18,7 @@ def apply_kernel(ts, arr): l = kernel.shape[0] for i in range(m - l + 1): - d = np.sum((znormalize_array(ts[i:i+l]) - kernel)**2) + d = np.sum((z_normalise_series(ts[i:i+l]) - kernel)**2) if d < d_best: d_best = d @@ -75,163 +37,73 @@ def apply_kernels(X, kernels): return out -class SAST(BaseEstimator, ClassifierMixin): - - def __init__(self, cand_length_list, shp_step=1, nb_inst_per_class=1, random_state=None, classifier=None): - super(SAST, self).__init__() - self.cand_length_list = cand_length_list - self.shp_step = shp_step - self.nb_inst_per_class = nb_inst_per_class - self.kernels_ = None - self.kernel_orig_ = None # not z-normalized kernels - self.kernels_generators_ = {} - self.random_state = np.random.RandomState(random_state) if not isinstance( - random_state, np.random.RandomState) else random_state - - self.classifier = classifier - - def get_params(self, deep=True): - return { - 'cand_length_list': self.cand_length_list, - 'shp_step': self.shp_step, - 'nb_inst_per_class': self.nb_inst_per_class, - 'classifier': self.classifier - } - - def init_sast(self, X, y): - - self.cand_length_list = np.array(sorted(self.cand_length_list)) - - assert self.cand_length_list.ndim == 1, 'Invalid shapelet length list: required list or tuple, or a 1d numpy array' - - if self.classifier is None: - self.classifier = RandomForestClassifier( - min_impurity_decrease=0.05, max_features=None) - - classes = np.unique(y) - self.num_classes = classes.shape[0] - - candidates_ts = [] - for c in classes: - X_c = X[y == c] - - # convert to int because if self.nb_inst_per_class is float, the result of np.min() will be float - cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) - choosen = self.random_state.permutation(X_c.shape[0])[:cnt] - candidates_ts.append(X_c[choosen]) - self.kernels_generators_[c] = X_c[choosen] - - candidates_ts = np.concatenate(candidates_ts, axis=0) - - self.cand_length_list = self.cand_length_list[self.cand_length_list <= X.shape[1]] - - max_shp_length = max(self.cand_length_list) - - n, m = candidates_ts.shape - - n_kernels = n * np.sum([m - l + 1 for l in self.cand_length_list]) - - self.kernels_ = np.full( - (n_kernels, max_shp_length), dtype=np.float32, fill_value=np.nan) - self.kernel_orig_ = [] - - k = 0 - - for shp_length in self.cand_length_list: - for i in range(candidates_ts.shape[0]): - for j in range(0, candidates_ts.shape[1] - shp_length + 1, self.shp_step): - end = j + shp_length - can = np.squeeze(candidates_ts[i][j: end]) - self.kernel_orig_.append(can) - self.kernels_[k, :shp_length] = znormalize_array(can) - - k += 1 - - def fit(self, X, y): - - X, y = check_X_y(X, y) # check the shape of the data - - # randomly choose reference time series and generate kernels - self.init_sast(X, y) - - # subsequence transform of X - X_transformed = apply_kernels(X, self.kernels_) - - self.classifier.fit(X_transformed, y) # fit the classifier +class RSAST(BaseCollectionTransformer): + """Random Scalable and Accurate Subsequence Transform (SAST). - return self - - def predict(self, X): - - check_is_fitted(self) # make sure the classifier is fitted - - X = check_array(X) # validate the shape of X - - # subsequence transform of X - X_transformed = apply_kernels(X, self.kernels_) - - return self.classifier.predict(X_transformed) - - def predict_proba(self, X): - check_is_fitted(self) # make sure the classifier is fitted - - X = check_array(X) # validate the shape of X - - # subsequence transform of X - X_transformed = apply_kernels(X, self.kernels_) - - if isinstance(self.classifier, LinearClassifierMixin): - return self.classifier._predict_proba_lr(X_transformed) - return self.classifier.predict_proba(X_transformed) - - -class SASTEnsemble(BaseEstimator, ClassifierMixin): + RSAST [1] is based on SAST, it uses a stratified sampling strategy for subsequences selection but additionally takes into account certain + statistical criteria such as ANOVA, ACF, and PACF to further reduce the search space of shapelets. + + RSAST starts with the pre-computation of a list of weights, using ANOVA, which helps in the selection of initial points for + subsequences. Then randomly select k time series per class, which are used with an ACF and PACF, obtaining a set of highly correlated + lagged values. These values are used as potential lengths for the shapelets. Lastly, with a pre-defined number of admissible starting + points to sample, the shapelets are extracted and used to transform the original dataset, replacing each time series by the vector of + its distance to each subsequence. + + Parameters + ---------- + n_random_points: int default = 10 the number of initial random points to extract + len_method: string default="both" the type of statistical tool used to get the length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS + nb_inst_per_class : int default = 10 + the number of reference time series to select per class + seed : int, default = None + the seed of the random generator + classifier : sklearn compatible classifier, default = None + if None, a RidgeClassifierCV(alphas=np.logspace(-3, 3, 10)) is used. + n_jobs : int, default -1 + Number of threads to use for the transform. + + Reference + --------- + .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for Time Series Classification. + https://hal.science/hal-04311309/ + + + Examples + -------- + >>> from aeon.transformations.collection.shapelet_based import RSAST + >>> 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") + >>> rsast = RSAST() + >>> rsast.fit(X_train, y_train) + RSAST() + >>> X_train = rsast.transform(X_train) + >>> X_test = rsast.transform(X_test) - def __init__(self, cand_length_list, shp_step=1, nb_inst_per_class=1, random_state=None, classifier=None, weights=None, n_jobs=None): - super(SASTEnsemble, self).__init__() - self.cand_length_list = cand_length_list - self.shp_step = shp_step + """ + + _tags = { + "output_data_type": "Tabular", + "capability:multivariate": False, + "algorithm_type": "subsequence", + } + + def __init__( + self, + n_random_points=10, + len_method="both", + nb_inst_per_class=10, + seed=None, + n_jobs=-1, + ): + super().__init__() + self.n_random_points = n_random_points, + self.len_method = len_method, self.nb_inst_per_class = nb_inst_per_class - self.classifier = classifier - self.random_state = random_state self.n_jobs = n_jobs + self.seed = seed - self.saste = None - - self.weights = weights - - assert isinstance(self.classifier, BaseEstimator) - - self.init_ensemble() - - def init_ensemble(self): - estimators = [] - for i, candidate_lengths in enumerate(self.cand_length_list): - clf = clone(self.classifier) - sast = SAST(cand_length_list=candidate_lengths, - nb_inst_per_class=self.nb_inst_per_class, - random_state=self.random_state, - shp_step=self.shp_step, - classifier=clf) - estimators.append((f'sast{i}', sast)) - - self.saste = VotingClassifier( - estimators=estimators, voting='soft', n_jobs=self.n_jobs, weights=self.weights) - - def fit(self, X, y): - self.saste.fit(X, y) - return self - - def predict(self, X): - return self.saste.predict(X) - - def predict_proba(self, X): - return self.saste.predict_proba(X) - - - -class RSAST(BaseEstimator, ClassifierMixin): - + def __init__(self,n_random_points=10, nb_inst_per_class=10, len_method="both", random_state=None, classifier=None, sel_inst_wrepl=False,sel_randp_wrepl=False, half_instance=False, half_len=False,n_shapelet_samples=None ): super(RSAST, self).__init__() self.n_random_points = n_random_points @@ -517,169 +389,3 @@ def predict_proba(self, X): return self.classifier._predict_proba_lr(X_transformed) return self.classifier.predict_proba(X_transformed) - -if __name__ == "__main__": - - ds='Chinatown' # Chosing a dataset from # Number of classes to consider - - rtype="numpy2D" - - #X_train, y_train = load_UCR_UEA_dataset(name=ds, split="train",extract_path="data", return_type=rtype) - - - #X_train=np.nan_to_num(X_train) - #y_train=np.nan_to_num(y_train) - - #X_test, y_test = load_UCR_UEA_dataset(name=ds, split="test", extract_path="data", return_type=rtype) - - #X_test=np.nan_to_num(X_test) - #y_test=np.nan_to_num(y_test) - #print('Format: load_UCR_UEA_dataset') - #print(X_train.shape) - #print(X_test.shape) - #print(y_train.shape) - #print(y_test.shape) - - - #y_train = list(map(int, y_train)) - #y_test =list(map(int, y_test)) - #print(X_train[0]) - - """ - print("ds:"+ds) - X_train_mod=[] - for i , element in enumerate(X_train): - element=np.array(element[0]) - print("TS N:"+str(i)+" len:"+str(element.shape)) - #print(element) - X_train_mod.append(element) - - X_train_mod= np.array(X_train_mod) - print(X_train_mod.shape) - - X_train_mod=np.nan_to_num(X_train_mod) - """ - - path=r"C:\Users\Surface pro\random_sast\sast\data" - ds_train_lds , ds_test_lds = load_dataset(ds_folder=path,ds_name=ds,shuffle=False) - X_test_lds, y_test_lds = format_dataset(ds_test_lds) - X_train_lds, y_train_lds = format_dataset(ds_train_lds) - - X_train_lds=np.nan_to_num(X_train_lds) - y_train_lds=np.nan_to_num(y_train_lds) - X_test_lds=np.nan_to_num(X_test_lds) - y_test_lds=np.nan_to_num(y_test_lds) - - print('Format: load_dataset') - print(X_train_lds.shape) - print(X_train_lds[0].shape) - print(X_train_lds[1].shape) - print(X_test_lds.shape) - - - print(y_train_lds.shape) - print(y_test_lds.shape) - - - - - start = time.time() - random_state = None - rsast_ridge = RSAST(n_random_points=10, nb_inst_per_class=10, len_method="both") - rsast_ridge.fit(X_train_lds, y_train_lds) - end = time.time() - print('rsast score :', rsast_ridge.score(X_test_lds, y_test_lds)) - print('duration:', end-start) - print('params:', rsast_ridge.get_params()) - - #print('classifier:',rsast_ridge.classifier.coef_[0]) - - #fname = f'images/chinatown-rf-class{c}-top5-features-on-ref-ts.jpg' - #print(f"ts.shape{pd.array(rsast_ridge.kernels_generators_).shape}") - #print(f"kernel_d.shape{pd.array(rsast_ridge.kernel_orig_).shape}") - - plot_most_important_feature_on_ts(set_ts=rsast_ridge.kernels_generators_, labels=rsast_ridge.class_generators_, features=rsast_ridge.kernel_orig_, scores=rsast_ridge.classifier.coef_[0], limit=3, offset=0,znormalized=False) - - plot_most_important_features(rsast_ridge.kernel_orig_, rsast_ridge.classifier.coef_[0], limit=3,scale_color=False) - - X_train = X_train_lds[:, np.newaxis, :] - X_test = X_test_lds[:, np.newaxis, :] - y_train=np.asarray([int(x_s) for x_s in y_train_lds]) - y_test=np.asarray([int(x_s) for x_s in y_test_lds]) - start = time.time() - - rdst = RDSTClassifier( - max_shapelets=4, - shapelet_lengths=[7], - proba_normalization=0, - save_transformed_data=True - ) - rdst = RDSTClassifier(proba_normalization=0) - rdst.fit(X_train, y_train) - end = time.time() - - - - print('rdst score :', rdst.score(X_test, y_test)) - print('duration:', end-start) - print('params:', rdst.get_params()) - """ - for i, shp in enumerate(rdst._transformer.shapelets_[0].squeeze()): - print('rdst shapelet values:',str(i+1)," shape:", shp.shape," shapelet:", shp ) - - for i, dilation in enumerate(rdst._transformer.shapelets_[2].squeeze()): - print('rdst dilation parameter:',str(i+1)," shape:", shp.shape," dilation:", dilation ) - - for i, treshold in enumerate(rdst._transformer.shapelets_[3].squeeze()): - print('rdst treshold parameter:',str(i+1)," shape:", shp.shape," treshold:", treshold ) - - for i, normalization in enumerate(rdst._transformer.shapelets_[4].squeeze()): - print('rdst normalization parameter:',str(i+1)," shape:", shp.shape," normalization:", normalization ) - - for i, coef in enumerate(rdst._estimator["ridgeclassifiercv"].coef_): - print('rdst coef:',str(i+1)," shape:", coef.shape," coef:", coef ) - """ - - features_cl=rdst._transformer.shapelets_[0].squeeze() - dilations_cl=rdst._transformer.shapelets_[2].squeeze() - - coef_cl=rdst._estimator["ridgeclassifiercv"].coef_[0] - features_cl=[a for a in features_cl for i in range(3)] - dilations_cl=[a for a in dilations_cl for i in range(3)] - type_features_cl=["min","argmin","SO"]*len(features_cl) - - for l in pd.unique(rsast_ridge.class_generators_): - - all=zip(rsast_ridge.kernels_generators_,rsast_ridge.class_generators_) - - ts_cl=list(filter(lambda x: x[1]==l,all))[0][0] - ts_cl=[ts_cl for i in range(len(features_cl))] - labels=[l for i in range(len(features_cl))] - plot_most_important_feature_on_ts(set_ts=ts_cl, labels=labels, features=features_cl, scores=coef_cl,dilations=dilations_cl,type_features=type_features_cl, limit=3, offset=0,znormalized=False) - plot_most_important_features(features_cl, coef_cl, dilations=dilations_cl, limit=3, scale_color=False) - """ - min_shp_length = 3 - max_shp_length = X_train_lds.shape[1] - candidate_lengths = np.arange(min_shp_length, max_shp_length+1) - # candidate_lengths = (3, 7, 9, 11) - nb_inst_per_class = 1 - ridge = RidgeClassifierCV(alphas = np.logspace(-3, 3, 10)) - - start = time.time() - random_state = None - sast_ridge = SAST(cand_length_list=candidate_lengths, - nb_inst_per_class=nb_inst_per_class, - random_state=random_state, classifier=ridge) - sast_ridge.fit(X_train_lds, y_train_lds) - end = time.time() - print('sast score :', sast_ridge.score(X_test_lds, y_test_lds)) - print('duration:', end-start) - print('params:', sast_ridge.get_params()) - #print('classifier:',rsast_ridge.classifier.coef_[0]) - - #fname = f'images/chinatown-rf-class{c}-top5-features-on-ref-ts.jpg' - #print(f"ts.shape{pd.array(rsast_ridge.kernels_generators_).shape}") - #print(f"kernel_d.shape{pd.array(rsast_ridge.kernel_orig_).shape}") - for c, ts in sast_ridge.kernels_generators_.items(): - plot_most_important_feature_sast_on_ts(ts.squeeze(), c, sast_ridge.kernel_orig_, sast_ridge.classifier.coef_[0], limit=3, offset=0) # plot only the first model one-vs-all model's features - """ From 2c0a41dc96f4ac04ffcdb4db239b8189242f0262 Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Mon, 1 Apr 2024 17:51:50 +0200 Subject: [PATCH 03/38] included transformer and classifier --- .../collection/shapelet_based/_rsast.py | 215 +++---- .../shapelet_based (RSAST).ipynb | 523 ++++++++++++++++++ 2 files changed, 597 insertions(+), 141 deletions(-) create mode 100644 examples/classification/shapelet_based (RSAST).ipynb diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index cb4c247003..8ab7ad23f4 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -5,20 +5,20 @@ from aeon.utils.numba.general import z_normalise_series from aeon.utils.validation import check_n_jobs +from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning +from statsmodels.tsa.stattools import acf, pacf +import pandas as pd @njit(fastmath=False) -def apply_kernel(ts, arr): +def _apply_kernel(ts, arr): d_best = np.inf # sdist m = ts.shape[0] kernel = arr[~np.isnan(arr)] # ignore nan - # profile = mass2(ts, kernel) - # d_best = np.min(profile) - - l = kernel.shape[0] - for i in range(m - l + 1): - d = np.sum((z_normalise_series(ts[i:i+l]) - kernel)**2) + kernel_len = kernel.shape[0] + for i in range(m - kernel_len + 1): + d = np.sum((z_normalise_series(ts[i : i + kernel_len]) - kernel) ** 2) if d < d_best: d_best = d @@ -26,14 +26,14 @@ def apply_kernel(ts, arr): @njit(parallel=True, fastmath=True) -def apply_kernels(X, kernels): +def _apply_kernels(X, kernels): nbk = len(kernels) out = np.zeros((X.shape[0], nbk), dtype=np.float32) for i in prange(nbk): k = kernels[i] for t in range(X.shape[0]): ts = X[t] - out[t][i] = apply_kernel(ts, k) + out[t][i] = _apply_kernel(ts, k) return out @@ -90,11 +90,11 @@ class RSAST(BaseCollectionTransformer): def __init__( self, - n_random_points=10, - len_method="both", - nb_inst_per_class=10, - seed=None, - n_jobs=-1, + n_random_points = 10, + len_method = "both", + nb_inst_per_class = 10, + seed = None, + n_jobs = -1, ): super().__init__() self.n_random_points = n_random_points, @@ -102,48 +102,29 @@ def __init__( self.nb_inst_per_class = nb_inst_per_class self.n_jobs = n_jobs self.seed = seed - - - def __init__(self,n_random_points=10, nb_inst_per_class=10, len_method="both", random_state=None, classifier=None, sel_inst_wrepl=False,sel_randp_wrepl=False, half_instance=False, half_len=False,n_shapelet_samples=None ): - super(RSAST, self).__init__() - self.n_random_points = n_random_points - self.nb_inst_per_class = nb_inst_per_class - self.len_method = len_method - self.random_state = np.random.RandomState(random_state) if not isinstance( - random_state, np.random.RandomState) else random_state - self.classifier = classifier - self.cand_length_list = None - self.kernels_ = None - self.kernel_orig_ = None # not z-normalized kernels - self.kernel_permutated_ = None - self.kernels_generators_ = None - self.class_generators_ = None - self.sel_inst_wrepl=sel_inst_wrepl - self.sel_randp_wrepl=sel_randp_wrepl - self.half_instance=half_instance - self.half_len=half_len - self.time_calculating_weights = None - self.time_creating_subsequences = None - self.time_transform_dataset = None - self.time_classifier = None - self.n_shapelet_samples =n_shapelet_samples - - def get_params(self, deep=True): - return { - 'len_method': self.len_method, - 'n_random_points': self.n_random_points, - 'nb_inst_per_class': self.nb_inst_per_class, - 'sel_inst_wrepl':self.sel_inst_wrepl, - 'sel_randp_wrepl':self.sel_randp_wrepl, - 'half_instance':self.half_instance, - 'half_len':self.half_len, - 'classifier': self.classifier, - 'cand_length_list': self.cand_length_list - } - - def init_sast(self, X, y): - #0- initialize variables and convert values in "y" to string - start = time.time() + self._kernels = None # z-normalized subsequences + self._kernel_orig = None # non z-normalized subsequences + self._kernels_generators = {} # Reference time series + self._cand_length_list = None + + def _fit(self, X, y): + """Select reference time series and generate subsequences from them. + + Parameters + ---------- + X: np.ndarray shape (n_cases, n_channels, n_timepoints) + The training input samples. + y: array-like or list + The class values for X. + + Return + ------ + self : RSAST + This transformer + + """ + #0- initialize variables and convert values in "y" to string + y=np.asarray([str(x_s) for x_s in y]) self.cand_length_list = {} @@ -189,27 +170,19 @@ def init_sast(self, X, y): n.append(0) else: n.append(1-p_value) - end = time.time() - self.time_calculating_weights = end-start + + #2--calculate PACF and ACF for each TS chossen in each class - start = time.time() + for i, c in enumerate(classes): X_c = X[y == c] - if self.half_instance==True: - cnt = np.max([X_c.shape[0]//2, 1]).astype(int) - self.nb_inst_per_class=cnt - else: - cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) + + cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) #set if the selection of instances is with replacement (if false it is not posible to select the same intance more than one) - if self.sel_inst_wrepl ==False: - choosen = self.random_state.permutation(X_c.shape[0])[:cnt] - else: - choosen = self.random_state.choice(X_c.shape[0], cnt) - - - + + choosen = self.random_state.permutation(X_c.shape[0])[:cnt] for rep, idx in enumerate(choosen): self.cand_length_list[c+","+str(idx)+","+str(rep)] = [] @@ -281,20 +254,18 @@ def init_sast(self, X, y): weights = n / np.sum(n) weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) - if self.half_len==True: - self.n_random_points=np.max([len(X_c[idx])//2, 1]).astype(int) - if self.n_random_points > len(X_c[idx])-max_shp_length+1 and self.sel_randp_wrepl==False: + if self.n_random_points > len(X_c[idx])-max_shp_length+1 : #set a upper limit for the posible of number of random points when selecting without replacement limit_rpoint=len(X_c[idx])-max_shp_length+1 - rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p=weights, replace=self.sel_randp_wrepl) + rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p=weights, replace=False) #print("limit_rpoint:"+str(limit_rpoint)) else: - rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p=weights, replace=self.sel_randp_wrepl) - #print("n_random_points:"+str(self.n_random_points)) + rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p=weights, replace=False) + + - #print("rpoints:"+str(rand_point_ts)) for i in rand_point_ts: #2.6-- Extract the subsequence with that point @@ -309,83 +280,45 @@ def init_sast(self, X, y): print("total kernels:"+str(len(self.kernel_orig_))) - if self.n_shapelet_samples!=None: - print("Truncated to:"+str(self.n_shapelet_samples)) - - self.kernel_permutated_ = self.random_state.permutation(self.kernel_orig_)[:self.n_shapelet_samples] - else: - self.kernel_permutated_ = self.kernel_orig_ + #3--save the calculated subsequences - n_kernels = len (self.kernel_permutated_) + n_kernels = len (self.kernel_orig_) self.kernels_ = np.full( (n_kernels, m_kernel), dtype=np.float32, fill_value=np.nan) - for k, kernel in enumerate(self.kernel_permutated_): - self.kernels_[k, :len(kernel)] = znormalize_array(kernel) - - end = time.time() - self.time_creating_subsequences = end-start - - def fit(self, X, y): - - X, y = check_X_y(X, y) # check the shape of the data - - # randomly choose reference time series and generate kernels - self.init_sast(X, y) - - start = time.time() - # subsequence transform of X - X_transformed = apply_kernels(X, self.kernels_) - end = time.time() - self.transform_dataset = end-start - - if self.classifier is None: - - if X_transformed.shape[0]<=X_transformed.shape[1]: #n_features (kernels) > n_samples (intances) - self.classifier=RidgeClassifierCV() - print("RidgeClassifierCV:"+str("size training")+str(X_transformed.shape[0])+"<="+" kernels"+str(X_transformed.shape[1])) - else: - print("LogisticRegression:"+str("size training")+str(X_transformed.shape[0])+">"+" kernels"+str(X_transformed.shape[1])) - self.classifier=LogisticRegression() - #self.classifier = RandomForestClassifier(min_impurity_decrease=0.05, max_features=None) - - start = time.time() - #print('X_transformed shape') - #print(X_transformed.shape) - #print('X_transformed') - #print(X_transformed) - - self.classifier.fit(X_transformed, y) # fit the classifier - end = time.time() - self.time_classifier = end-start + for k, kernel in enumerate(self.kernel_orig_): + self.kernels_[k, :len(kernel)] = z_normalise_series(kernel) return self + + def _transform(self, X, y=None): + """Transform the input X using the generated subsequences. - def predict(self, X): - - check_is_fitted(self) # make sure the classifier is fitted - - X = check_array(X) # validate the shape of X - - # subsequence transform of X - X_transformed = apply_kernels(X, self.kernels_) - - return self.classifier.predict(X_transformed) + Parameters + ---------- + X: np.ndarray shape (n_cases, n_channels, n_timepoints) + The training input samples. + y: array-like or list + Ignored argument, interface compatibility - def predict_proba(self, X): - check_is_fitted(self) # make sure the classifier is fitted + Return + ------ + X_transformed: np.ndarray shape (n_cases, n_timepoints), + The transformed data + """ + X_ = np.reshape(X, (X.shape[0], X.shape[-1])) - X = check_array(X) # validate the shape of X + prev_threads = get_num_threads() - # subsequence transform of X - X_transformed = apply_kernels(X, self.kernels_) + n_jobs = check_n_jobs(self.n_jobs) - if isinstance(self.classifier, LinearClassifierMixin): - return self.classifier._predict_proba_lr(X_transformed) - return self.classifier.predict_proba(X_transformed) + set_num_threads(n_jobs) + X_transformed = _apply_kernels(X_, self._kernels) # subsequence transform of X + set_num_threads(prev_threads) + return X_transformed diff --git a/examples/classification/shapelet_based (RSAST).ipynb b/examples/classification/shapelet_based (RSAST).ipynb new file mode 100644 index 0000000000..afcdedc67b --- /dev/null +++ b/examples/classification/shapelet_based (RSAST).ipynb @@ -0,0 +1,523 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "# Shapelet based time series machine learning\n", + "\n", + "Shapelets a subsections of times series taken from the train data that are a useful for time series machine learning. They were first proposed ia primitive for machine learning [1][2] and were embedded in a decision tree for classification. The Shapelet Transform Classifier (STC)[3,4] is a pipeline classifier which searches the training data for shapelets, transforms series to vectors of distances to a filtered set of selected shapelets based on information gain, then builds a classifier on the latter.\n", + "\n", + "Finding shapelets involves selecting and evaluating shapelets. The original shapelet tree and STC performed a full enumeration of all possible shapelets before keeping the best ones. This is computationally inefficient, and modern shapelet based machine learning algorithms randomise the search." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append(r'C:\\Users\\nicol\\aeon')" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[('MrSQMClassifier',\n", + " aeon.classification.shapelet_based._mrsqm.MrSQMClassifier),\n", + " ('RDSTClassifier', aeon.classification.shapelet_based._rdst.RDSTClassifier),\n", + " ('ShapeletTransformClassifier',\n", + " aeon.classification.shapelet_based._stc.ShapeletTransformClassifier)]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import warnings\n", + "\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "from aeon.datasets import load_basic_motions\n", + "from aeon.registry import all_estimators\n", + "from aeon.transformations.collection.shapelet_based import RandomShapeletTransform\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "all_estimators(\"classifier\", filter_tags={\"algorithm_type\": \"shapelet\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Shapelet Transform for Classification\n", + "\n", + "The `RandomShapeletTransform` transformer takes a set of labelled training time series in the `fit` function, randomly samples `n_shapelet_samples` shapelets, keeping the best `max_shapelets`. The resulting shapelets are used in the `transform` function to create a new tabular dataset, where each row represents a time series instance, and each column stores the distance from a time series to a shapelet. The resulting tabular data can be used by any scikit learn compatible classifier. In this notebook we will explain these terms and describe how the algorithm works. But first we show it in action. We will use the BasicMotions data as an example. This data set contains time series of motion traces for the activities \"running\", \"walking\", \"standing\" and \"badminton\". The learning problem is to predict the activity given the time series. Each time series has six channels: x, y, z position and x, y, z accelerometer of the wrist. Data was recorded on a smart watch." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Shape of transformed data = (40, 8)\n", + " Distance of second series to third shapelet = 1.302772121165026\n", + " Shapelets + random forest acc = 0.95\n" + ] + } + ], + "source": [ + "X, y = load_basic_motions(split=\"train\")\n", + "rst = RandomShapeletTransform(n_shapelet_samples=100, max_shapelets=10, random_state=42)\n", + "st = rst.fit_transform(X, y)\n", + "print(\" Shape of transformed data = \", st.shape)\n", + "print(\" Distance of second series to third shapelet = \", st[1][2])\n", + "testX, testy = load_basic_motions(split=\"test\")\n", + "tr_test = rst.transform(testX)\n", + "rf = RandomForestClassifier(random_state=10)\n", + "rf.fit(st, y)\n", + "preds = rf.predict(tr_test)\n", + "print(\" Shapelets + random forest acc = \", accuracy_score(preds, testy))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Visualising Shapelets\n", + "The first column of the transformed data represents the distance from the first shapelet to each time series. The shapelets are sorted, so the first shapelet is the one we estimate is the best (using the calculation described below). You can recover the shapelets from the transform. Each shapelet is a 7-tuple, storing the following information:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Quality = 0.81127812\n", + "Length = 39\n", + "position = 55\n", + "Channel = 0\n", + "Origin Instance Index = 11\n", + "Class label = running\n", + "Shapelet = [-0.85667017 -1.88711152 -0.8751295 0.80633757 1.10838333 0.69810992\n", + " 0.85713394 1.23190921 0.01801365 -1.29683966 -1.94694259 -0.37487726\n", + " -0.37487726 1.39471462 0.74922685 0.74922685 0.22343376 0.22343376\n", + " -0.7730703 -1.37591995 -0.80376393 1.32758071 0.99778845 0.6013481\n", + " 0.83711118 0.93684593 0.93684593 -1.30429475 -1.64522057 -0.56312308\n", + " 0.96855713 0.56796251 0.35714242 0.62066541 0.65135287 -0.80531237\n", + " -1.49170075 -1.18512797 0.69685753]\n" + ] + } + ], + "source": [ + "running_shapelet = rst.shapelets[0]\n", + "print(\"Quality = \", running_shapelet[0])\n", + "print(\"Length = \", running_shapelet[1])\n", + "print(\"position = \", running_shapelet[2])\n", + "print(\"Channel = \", running_shapelet[3])\n", + "print(\"Origin Instance Index = \", running_shapelet[4])\n", + "print(\"Class label = \", running_shapelet[5])\n", + "print(\"Shapelet = \", running_shapelet[6])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "We can directly extract shapelets and inspect them. These are the the two shapelets that are best at discriminating badminton and running against other activities. All shapelets are normalised to provide scale invariance." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Badminton shapelet from channel 0 (x-dimension) (0.65194393, 74, 7, 1, 1, 'standing', array([-5.27667376, -0.94911454, 0.90433173, 1.26316864, 2.34760078,\n", + " 1.84408 , 0.9192852 , 0.9192852 , -1.29868372, -1.29868372,\n", + " -1.5476774 , -1.03000413, 0.27593674, -0.70184658, 0.37460295,\n", + " 1.27398121, 1.02881837, 0.64543662, -0.0669839 , -0.54373096,\n", + " -0.55716134, -0.56605101, -0.08611633, 0.31270572, 0.25642625,\n", + " 0.5512744 , 0.78929504, 0.73385326, 0.73385326, -0.26777726,\n", + " -0.63967737, -0.63967737, -0.5539071 , -0.5539071 , 0.3867047 ,\n", + " 0.3867047 , 0.88832979, 0.85074214, 0.46901267, 0.0925433 ,\n", + " -0.34444436, -0.72498936, -0.83763127, -0.53034818, -0.05869122,\n", + " 0.46600593, 1.02537238, 0.81800526, 0.51709059, 0.17497366,\n", + " -0.31072836, -0.64876695, -0.89102368, -0.60834799, -0.0627886 ,\n", + " 0.42532723, 0.95696668, 0.91077086, 0.77491818, 0.14283377,\n", + " 0.14283377, -1.08722874, -1.08722874, -0.65706914, -0.65706914,\n", + " 0.28210933, 0.74159654, 0.8064869 , 0.8064869 , 0.19889294,\n", + " -0.16601048, -0.78706337, -0.76364317, -0.63789726]))\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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uRnvvfeeR9eXy+Wt/n6vvfZamx/3lClToFQq8dprr9ndbsu/pzsrV660q0/asWMHtm/fLq4WEj71txx7y/uRy+WYOnUqVq5cicLCQvH8o0ePYs2aNV6Nyd+c/Y9deumlMJlMeP311+3Of+mll8BxnPickMCijEgn88svv4jRfVlZGT777DOcOHECixYtQmRkJAC+FuHOO+/E0qVLsW/fPlx88cVQKpU4ceIEvvrqK7zyyiu45ppr8OGHH+LNN9/EVVddhdzcXNTV1eG9995DZGSkeJALCQlBnz598MUXX6BHjx6IiYlBv379xOWwLfXp0wcTJ07E0KFDERMTg127duHrr7/GggULvH6s06dPx3//+19MmzYN//d//4eysjK88cYb6Natm8O6k379+mHq1Kl2y3cBiJ1hAeDZZ5/Fxo0bMXLkSMybNw99+vRBZWUl9uzZg3Xr1qGystLpeN544w2MGzcO/fv3x7x585CTk4Nz585h69atOHPmDPbv3w+ADy7kcjn+85//oKamBmq1GhdeeCE+++wzt8+3NwYOHIjZs2fj3XffRXV1NSZMmIAdO3bgww8/xIwZMzBp0iSvb9MTt99+O+666y7MnDkTF110Efbv3481a9YgLi7OL/fnK3feeSdef/113HjjjbjvvvuQnJyMTz/9VGyQ5i6Tddlll+Hjjz+GVqtFnz59sHXrVqxbt67NyzyF3jnCktdLL70Ue/fuxS+//OLVc9mtWzeMGzcOd999N3Q6HV5++WXExsbi4YcfBgBERkZi/PjxeO6552AwGJCamopff/0VeXl5rW5ryZIlWL16NS644ALcc889MBqNeO2119C3b1+H/3NSEYrS7733XkydOhVyuRw33HADLr/8ckyaNAn/+Mc/kJ+fj4EDB+LXX3/F999/j/vvv9+uMJUEUMDX6RC/cLR8V6PRsEGDBrG33nrLbvmf4N1332VDhw5lISEhLCIigvXv3589/PDDrKSkhDHG2J49e9iNN97IMjIymFqtZgkJCeyyyy6zWwLLGGNbtmxhQ4cOZSqVyu1S3qeffpqNGDGCRUVFsZCQENarVy/2zDPPML1eL17G0VJUxhh78sknWy3J++CDD1j37t2ZWq1mvXr1YsuWLXN4OQBs/vz57JNPPhEvP3jwYIdLMs+dO8fmz5/P0tPTmVKpZElJSWzy5Mns3Xff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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "badminton_shapelet = rst.shapelets[4]\n", + "print(\" Badminton shapelet from channel 0 (x-dimension)\", badminton_shapelet)\n", + "plt.title(\"Best shapelets for running and badminton\")\n", + "plt.plot(badminton_shapelet[6], label=\"Badminton\")\n", + "plt.plot(running_shapelet[6], label=\"Running\")\n", + "plt.legend()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Both shapelets are in the x-axis, so represent side to side motion. Badminton is characterised by sa single large peak in one direction, capturing the drawing of the hand back and quickly hittig the shuttlcock. Running is chaaracterised by a longer repetition of side to side motions, with a sharper peak representing bringing the arm forward accross the body in a running motion." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Performance on the UCR univariate datasets\n", + "\n", + "You can find the interval based classifiers as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MrSQMClassifier\n", + "RDSTClassifier\n", + "ShapeletTransformClassifier\n" + ] + } + ], + "source": [ + "from aeon.registry import all_estimators\n", + "\n", + "est = [\"MrSQMClassifier\", \"RDSTClassifier\", \"ShapeletTransformClassifier\"]\n", + "for c in est:\n", + " print(c)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(112, 3)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from aeon.benchmarking import get_estimator_results_as_array\n", + "from aeon.datasets.tsc_data_lists import univariate\n", + "\n", + "names = [t.replace(\"Classifier\", \"\") for t in est]\n", + "results, present_names = get_estimator_results_as_array(\n", + " names, univariate, include_missing=False\n", + ")\n", + "results.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(
, )" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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c+fO1dChQzV9+nStXbtWnp6eatmypT788EOVK1cuSR1vb29t2rRJU6ZM0Zw5c/THH39oy5YtKlCggLy9vdWtWzc1adLEIdynpnz58vrjjz80YcIEzZ8/X3/88Yc2btyoQoUKycfHR3369FHz5s0d6ri5uWnp0qV69913tXHjRu3atUvx8fGqXbt2loT0fPnyadu2bRo4cKCWLl2q//3vfypYsKAaNGiggQMHav369Zm2rVy5cmnChAlas2aNduzYoeXLlys6Olre3t5q0aKFunfvrnr16qWrzYIFC2rDhg0aM2aM5syZo2XLlik+Pl4BAQHq0qWL+vTpk+yd4AAA4N5o3769hg8frr1792rYsGH69NNP7evOnTunqVOnSrqdgfLkySNvb2+1a9dODRs2VIsWLVLMXevXr9eKFSv066+/6tChQ1q5cqWuXbumPHnyKCQkRA0bNlT37t1VqFChe7KfmclmMjI1ikUdP35cAQEB8vPzs1/ZBwBAuv2NsJubW4ZnsgCArMQ8XAAAAIDFENIBAAAAiyGkAwAAABbzQI5JBwAgJYxJB3A/4Eo6AABAImvXrlVoaKi8vb1ls9n0008/3bHO+PHjVbZsWXl4eKh06dKaNm2aw/o9e/aoZcuW8vf3l81m0+eff56kjatXr6pnz57y8/OTh4eHatSooa1btyYpt2/fPjVp0kR58+aVp6enQkJC7A+SxIODkA4AAJDI9evXVbFiRY0fPz5N5b/88ku9/fbbGjRokPbs2aMPPvhAr7/+uhYuXGgvExUVpRIlSmjIkCEqWrRosu288sorWr58uaZPn64///xTDRo0UP369RUREWEvc+TIEdWqVUtlypTR6tWr9ccff+i9995L89TQuH8w3AUA8FBhuAvSw2azaf78+WrWrFmKZWrUqKGaNWvan4wpSb1799bmzZuTfdaIv7+/evbsqZ49e9qX3bhxQ7lz59bPP/+s559/3r68cuXKevbZZ/XRRx9Juv00d1dXV02fPj3jOwdL40o6AABABty6dSvJlWwPDw9t2bJFMTExaWojNjZWcXFxybaTEPTj4+O1aNEiPfbYY2rYsKG8vLxUrVq1NA3Hwf2HkA4AAJABDRs21DfffKPff/9dxhht27ZN33zzjWJiYnThwoU0tZE7d25Vr15dgwcP1unTpxUXF6cZM2Zo48aNOnPmjCTp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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from aeon.visualisation import plot_boxplot_median, plot_critical_difference\n", + "\n", + "plot_critical_difference(results, names)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(
, )" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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AJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGXJKofuee+5RfX29/H6/lixZoi1btpxw3wceeECGYfR7+P3+E+7/yU9+UoZhaP369adSGgAAAAAAOSPt0P3II49ozZo1uv3227Vt2zbNnz9fK1asUFNT0wmPKSws1KFDh/oee/bsGXS/n//853r++edVU1OTblkAAAAAAOSctEP33Xffreuuu06rV6/WnDlztGHDBgWDQd1///0nPMYwDFVVVfU9KisrB+xz4MAB3XjjjXrooYfk8XhOWkM8HlckEun3AAAAAAAg16QVuhOJhLZu3arly5cfPYFpavny5dq8efMJj+vq6lJdXZ1qa2t1+eWX65VXXun3um3buvrqq3XzzTdr7ty571jHunXrFAqF+h61tbXpfAwAAAAAAEZFWqG7paVFlmUNaKmurKxUQ0PDoMfMnDlT999/v375y1/qwQcflG3bWrZsmfbv39+3zze/+U253W59+tOfHlIda9euVTgc7nvs27cvnY8BAAAAAMCocGf6DZYuXaqlS5f2PV+2bJlmz56tH/3oR7rrrru0detWfe9739O2bdtkGMaQzunz+eTz+TJVMgAAAAAAIyKtlu6ysjK5XC41Njb2297Y2KiqqqohncPj8eiss87Sm2++KUl65pln1NTUpEmTJsntdsvtdmvPnj363Oc+p/r6+nTKAwAAAAAgp6QVur1erxYuXKhNmzb1bbNtW5s2berXmn0ylmVpx44dqq6uliRdffXVeumll7R9+/a+R01NjW6++Wb99re/Tac8AAAAAAByStrdy9esWaNVq1Zp0aJFWrx4sdavX6/u7m6tXr1aknTNNddowoQJWrdunSTpzjvv1Lnnnqtp06apo6ND3/72t7Vnzx5de+21kqTS0lKVlpb2ew+Px6OqqirNnDlzuJ8PAAAAAICsSTt0r1y5Us3NzbrtttvU0NCgBQsWaOPGjX2Tq+3du1emebQBvb29Xdddd50a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wmyXAUhiyTAAAAAA48zU+qnZLgHoQ0s3AAAAAAAZQugGAAAAACBDCN0AAAAAAGQIoRsAAAAAgAwhdAMAAAAAkCGEbgAAAAAAMoTQDQAAAABAhhC6AQAAAADIEEI3AAAAAAAZQugGAAAAACBDTil033PPPaqvr5ff79eSJUu0ZcuWIR338MMPyzAMXXHFFSfc55Of/KQMw9D69etPpTQAAAAAAHJG2qH7kUce0Zo1a3T77bdr27Ztmj9/vlasWKGmpqaTHrd7927ddNNNOv/880+4z89//nM9//zzqqmpSbcsAAAAAAByTtqh++6779Z1112n1atXa86cOdqwYYOCwaDuv//+Ex5jWZY+8pGP6I477tCUKVMG3efAgQO68cYb9dBDD8nj8Zy0hng8rkgk0u8BAAAAAECuSSt0JxIJbd26VcuXLz96AtPU8uXLtXnz5hMed+edd6qiokKf+MQnBn3dtm1dffXVuvnmmzV37tx3rGPdunUKhUJ9j9ra2nQ+BgAAAAAAoyKt0N3S0iLLslRZWdlve2VlpRoaGgY95tlnn9V9992ne++994Tn/eY3vym3261Pf/rTQ6pj7dq1CofDfY99+/YN/UMAAAAAADBK3Jk8eWdnp66++mrde++9KisrG3SfrVu36nvf+562bdsmwzCGdF6fzyefzzeSpQIAAAAAMOLSCt1lZWVyuVxqbGzst72xsVFVVVUD9t+1a5d2796tSy+9tG+bbdu9b+x2a+fOnXrmmWfU1NSkSZMm9e1jWZY+97nPaf369dq9e3c6JQIAAAAAkDPSCt1er1cLFy7Upk2b+pb9sm1bmzZt0g033DBg/1mzZmnHjh39tn35y19WZ2envve976m2tlZXX311vzHikrRixQpdffXVWr16dZofBwAAAACA3JF29/I1a9Zo1apVWrRokRYvXqz169eru7u7LyBfc801mjBhgtatWye/36958+b1O76oqEiS+raXlpaqtLS03z4ej0dVVVWaOXPmqXwmAAAAAAByQtqhe+XKlWpubtZtt92mhoYGLViwQBs3buybXG3v3r0yzbRXIgMAAAAAYNwxHMdxsl3EcEUiEYVCIYXDYRUWFma7HAAAAADAODfUHEqTNAAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0A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9llt3LhRP/nJT/T0008rFovpi1/8on71q1/JsqwsVgm8M0I3TlvHtmDfddddCoVCikQiuueee/TTn/5U/+t//a++/QjeAHLZyy+/rEWLFulzn/ucHnjgAf34xz9WZ2enLrroIvX09Mg0TYI3gDHFtu1+12olJSX6wAc+oOXLl2vhwoX6/e9/r/z8fK1bt06PPfYYwRs5jdCN086XvvQlXX311XrqqafU0tIiSfJ4PJo+fbpefvllXXXVVbrvvvv005/+VJ/61KckiS7mAHJaa2urbr/9dt11112SpPnz5+vhhx9WV1eXLrzwQoI3gDHnyBjub37zm7r00kt1xRVXaNeuXX2v5+fn65e//KUKCgr0zW9+U//1X//FdxxyFqEbp5UtW7Zo3bp1euihh3T//ffrPe95jx599FHZtq1bbrlF9913n5599ln90z/9U1/wXrlyZbbLBoBB7dmzR3fffbfq6ur05S9/WdLR7pjz58/X//2//5fgDWBMObZ34b/+67/qrrvu0ty5c+X1evWnP/1JP/zhD5VKpSQdDd7d3d3atGlTX1AHcg3/MnFamT59ur75zW/K6/WqpqZG119/vW677TZdfvnl+ulPf6r3ve99euKJJyRJV155pb73ve+poKAgy1UDwEAvv/yyVqxYoS1btujpp5/u235sz5xjg/e73/1uRaNRLkoB5LQj32FPPPGE3nrrLf3nf/6nvvGNb2jLli06//zz9cgjj+g//uM/+m4g5uXl6fnnn9ePfvSjbJYNnBSzl+O009HRoe9///v6yle+oscff1wLFy7U008/ra9+9at66aWX9J73vEe//vWv5XK5lEql5Hb3rqzHTOYAcsWrr76q888/X5/4xCf02c9+VlVVVSfd/6WXXtJ73/tezZ07V5s2bRqlKgFg6I5daeG3v/2t1q5dq8bGRv3sZz/rWx6sra1N//zP/6x9+/bpmmuu0Sc+8Yl+M5ezWgNyFaEb414kElFHR4dSqZSmTJkiqfdL+dZbb9U3vvEN/fjHP9bVV1+tnp4ePf3001q0aJFKS0uzXDUADC4Wi+maa65RZWWlfvCDH/Tb3tnZqfb2ds2YMWPAcS+//LICgYCmTp06muUCQFr+4z/+Q8FgUFu2bNFDDz2kf/zHf9Q999zT93p7e7tuvPFGbdmyRd/61rd0xRVXZK9YYIjc2S4AyKTvfOc7evLJJ/XUU08pPz9fM2fO1A033KD3v//9+vrXvy7TNLVq1SrF43Fde+21WrFihaTeGTPpggkgF1mWpZ07d+qCCy7o2/a73/1Ojz32mB566CElEgldddVVuvPOO1VVVdXXS2fevHlZrBoABnfsNde//uu/6qabbtKrr76q9773vfJ4PHriiSd055136rbbbpMkFRcX63vf+55+8IMf6NJLL81m6cCQEboxbt1888366U9/qq985Sv68Ic/rFgspu9+97u67rrrdPPNN2vNmjW64447ZJqmPvWpT8nj8WjVqlWSROAGkLMsy1Jtba1efPFF/e1vf9Njjz2m//2//7fmz5+v22+/XRMmTNCHP/xhzZw5U5/73OcYFgMgpx255nruuecUjUb17//+76qrq5Mk3XLLLbJtW48//rgMw9Ctt94qSSotLdVXvvIVSXQpx9hA6Ma49PDDD+uRRx7pG7N9xKpVq/R3f/d3uvvuuzV16lStXLlSN910k0zT1OrVqzV16lSdd955WawcAE6usLBQf/d3f6d///d/1yWXXKJ4PK5vfvObeve7393Xdfzv//7v9fzzz2e5UgAYmldffbXv+uvIsBnHcVRaWqovfvGL+vrXv67f/OY3ikQi+va3v93vWAI3xgLGdGNcOdKN8uabb1ZjY6N+8pOf9N0BTSaT8ng8SqVSWrRokfLy8vSnP/1JktTV1aXf//73uvLKK7P8CQCgv87OTnV0dGjHjh0KBAK6+OKLJUm7du1SS0uLJk+erIqKir79Y7GYPvShD2nx4sV93TEBINf98pe/1NVXX61LL71UGzZsUEFBQd91XWtrq77whS/INE396Ec/ogcPxhxCN8al9773vSoqKtJ//ud/9ut2dGQ28v/+7//W//pf/0tbtmwZMKkQ47kB5IqdO3dq7dq12rVrl1555RXZtq13vetd+vKXv6yLL75YXq+33/6WZekrX/mKfvzjH+uPf/yjpk2blqXKAWBwJ7vOeuSRR/TRj35Un/3sZ/X1r39dbre7L3iHw2EVFhbKMAxWlMGYQ/dyjEvl5eV64YUXJPV2OzryBX9k+a9QKKRwODzolz6BG0AuePHFF7VixQp96EMf0sc//nFNnz5dr7zyim655RZde+21+pd/+Rf9wz/8Q99Nxccee0yPPfaYfv7zn2vjxo0EbgA559jAvWHDBr3y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4okntH37dkns4QbgmPLz8xUdHS0XFxe1bNlS27ZtU2Jiol588UUdPnxYJ06cUEJCgqxWq/r27auUlBR+ywA4tN///vfasGGD4uLi1LVrV1vzw2q16uTJkxo1apSKioqUm5srLy8vO1cLPByEbji0+fPna+3atZo9e7aGDh2q0NBQZWZmqkePHtq5c6ekf/1ntbKyUosWLdKuXbvUpUsXxcfH33UcABxRXl6exo8fr71792ru3LmaOnXqXcdLSkq0e/du+fr6ytvb205VAsB/lpOToyFDhujDDz9U9+7dZTabVVZWpmPHjunZZ59VixYtdPLkSQ0cOFDPPvustmzZYu+SgYeC0A2HVVBQoF69eqmiokIvvfSSRo4cqZdfflmTJk1Senq6Jk2apPHjx9/V6b5w4YLKysrk6+sricANoG44e/asIiMjZTKZNHPmTPn7+0vSPat5AMCRpaena+LEiTp37py++uorbdmyRampqbpw4YL69etne6LM2bNn1a5dO6aUo8EgjcBhtWnTRiEhIbJYLHJ1dVV8fLx27Nih2NhY9e3bVxs3btTq1atlsVhs72nbtq0tcFutVgI3gDqhQ4cOWrp0qaxWq+bNm2fb403gBuCoampq7nmta9euKikpUZcuXRQYGKgrV65ozpw5ys3NVVZWlo4ePSrp1m8eU8rRkDCpAA6pqqpKLi4uioyM1KVLl9S1a1ft3btXCxYskNFo1PLlyzVu3DglJydLksaMGXPP4A32PQKoS7y9vbVkyRJNmTJFU6dOVWxsrLp162bvsgDgHneuJMzNzdWjjz4qNzc3dejQQbm5udqwYYO6deumnj17ysPDQ1VVVfLz87vnXo1ONxoK2oBwKGfOnJEkubi4SJI8PT1148YNlZeXa9WqVWrVqpXef/99ffbZZ1qxYoV8fHy0ePFiff755/YsGwAeCG9vby1atEht2rRR69at7V0OAPyk24F7+vTpCg4OVp8+ffTGG2/o448/VseOHfXHP/5RAwYMkJubm0pKShQcHCyLxaJ+/frZuXLAPtjTDYcxb948xcfHKygoSDNmzFCLFi3UqlUrHTx4UKGhocrIyFCjRo00ffp0lZSUaPr06erTp4+Sk5M1atQoe5cPAA/M7dU+AOBI7nwazMGDBzVkyBAlJyfr22+/VXZ2tj799FPNnz9fw4YNk9ls1oYNG7Rm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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_boxplot_median(results, names)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Scalable and Accurate Subsequence Transform (SAST)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": false, + "pycharm": { + "is_executing": true + } + }, + "outputs": [], + "source": [ + "\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "\n", + "from aeon.datasets import load_basic_motions,load_classification\n", + "\n", + "from aeon.transformations.collection.shapelet_based import SAST\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Shape of transformed data = (20, 504)\n", + " Distance of second series to third shapelet = 0.0004470423\n", + " Shapelets + random forest acc = 1.0\n" + ] + } + ], + "source": [ + "#X, y = load_basic_motions(split=\"train\")\n", + "X, y = load_classification(name=\"Chinatown\",split=\"train\")\n", + "sast = SAST(lengths=None,\n", + " stride=1,\n", + " nb_inst_per_class=1,\n", + " seed=42,\n", + " n_jobs=-1)\n", + "st = sast.fit_transform(X, y)\n", + "print(\" Shape of transformed data = \", st.shape)\n", + "print(\" Distance of second series to third shapelet = \", st[1][2])\n", + "#testX, testy = load_basic_motions(split=\"test\")\n", + "testX, testy = load_classification(name=\"Chinatown\",split=\"train\")\n", + "tr_test = sast.transform(testX)\n", + "rf = RandomForestClassifier(random_state=10)\n", + "rf.fit(st, y)\n", + "preds = rf.predict(tr_test)\n", + "print(\" Shapelets + random forest acc = \", accuracy_score(preds, testy))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Random Scalable and Accurate Subsequence Transform (RSAST)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append(r'C:\\Users\\nicol\\aeon')" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false, + "pycharm": { + "is_executing": true + } + }, + "outputs": [], + "source": [ + "from sklearn.ensemble import RandomForestClassifier\n", + "\n", + "from aeon.datasets import load_basic_motions,load_classification\n", + "\n", + "from aeon.transformations.collection.shapelet_based import RSAST\n", + "\n", + "from sklearn.metrics import accuracy_score" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[3], line 8\u001b[0m\n\u001b[0;32m 2\u001b[0m X, y \u001b[38;5;241m=\u001b[39m load_classification(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mChinatown\u001b[39m\u001b[38;5;124m\"\u001b[39m,split\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtrain\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 3\u001b[0m sast \u001b[38;5;241m=\u001b[39m RSAST(n_random_points\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m10\u001b[39m,\n\u001b[0;32m 4\u001b[0m len_method\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mboth\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 5\u001b[0m nb_inst_per_class\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m10\u001b[39m,\n\u001b[0;32m 6\u001b[0m seed\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m 7\u001b[0m n_jobs\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m----> 8\u001b[0m st \u001b[38;5;241m=\u001b[39m \u001b[43msast\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfit_transform\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 9\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m Shape of transformed data = \u001b[39m\u001b[38;5;124m\"\u001b[39m, st\u001b[38;5;241m.\u001b[39mshape)\n\u001b[0;32m 10\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m Distance of second series to third shapelet = \u001b[39m\u001b[38;5;124m\"\u001b[39m, st[\u001b[38;5;241m1\u001b[39m][\u001b[38;5;241m2\u001b[39m])\n", + "File \u001b[1;32m~\\aeon\\aeon\\transformations\\collection\\base.py:162\u001b[0m, in \u001b[0;36mBaseCollectionTransformer.fit_transform\u001b[1;34m(self, X, y)\u001b[0m\n\u001b[0;32m 159\u001b[0m X_inner \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_preprocess_collection(X)\n\u001b[0;32m 160\u001b[0m y_inner \u001b[38;5;241m=\u001b[39m y\n\u001b[1;32m--> 162\u001b[0m Xt \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_fit_transform\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mX_inner\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43my_inner\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 164\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_is_fitted \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m 166\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Xt\n", + "File \u001b[1;32m~\\aeon\\aeon\\transformations\\collection\\base.py:328\u001b[0m, in \u001b[0;36mBaseCollectionTransformer._fit_transform\u001b[1;34m(self, X, y)\u001b[0m\n\u001b[0;32m 309\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Fit to data, then transform it.\u001b[39;00m\n\u001b[0;32m 310\u001b[0m \n\u001b[0;32m 311\u001b[0m \u001b[38;5;124;03mFits the transformer to X and y and returns a transformed version of X.\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 324\u001b[0m \u001b[38;5;124;03mtransformed version of X.\u001b[39;00m\n\u001b[0;32m 325\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 326\u001b[0m \u001b[38;5;66;03m# Non-optimized default implementation; override when a better\u001b[39;00m\n\u001b[0;32m 327\u001b[0m \u001b[38;5;66;03m# method is possible for a given algorithm.\u001b[39;00m\n\u001b[1;32m--> 328\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_fit\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 329\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_transform(X, y)\n", + "File \u001b[1;32m~\\aeon\\aeon\\transformations\\collection\\shapelet_based\\_rsast.py:169\u001b[0m, in \u001b[0;36mRSAST._fit\u001b[1;34m(self, X, y)\u001b[0m\n\u001b[0;32m 165\u001b[0m p_value\u001b[38;5;241m=\u001b[39mnp\u001b[38;5;241m.\u001b[39mnan\n\u001b[0;32m 166\u001b[0m \u001b[38;5;66;03m# Interpretation of the results\u001b[39;00m\n\u001b[0;32m 167\u001b[0m \u001b[38;5;66;03m# if p_value < 0.05: \" The means of the populations are significantly different.\"\u001b[39;00m\n\u001b[0;32m 168\u001b[0m \u001b[38;5;66;03m#print('pvalue', str(p_value))\u001b[39;00m\n\u001b[1;32m--> 169\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m np\u001b[38;5;241m.\u001b[39misnan(p_value):\n\u001b[0;32m 170\u001b[0m n\u001b[38;5;241m.\u001b[39mappend(\u001b[38;5;241m0\u001b[39m)\n\u001b[0;32m 171\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n", + "\u001b[1;31mValueError\u001b[0m: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()" + ] + } + ], + "source": [ + "#X, y = load_basic_motions(split=\"train\")\n", + "X, y = load_classification(name=\"Chinatown\",split=\"train\")\n", + "sast = RSAST(n_random_points=10,\n", + " len_method=\"both\",\n", + " nb_inst_per_class=10,\n", + " seed=None,\n", + " n_jobs=-1)\n", + "st = sast.fit_transform(X, y)\n", + "print(\" Shape of transformed data = \", st.shape)\n", + "print(\" Distance of second series to third shapelet = \", st[1][2])\n", + "#testX, testy = load_basic_motions(split=\"test\")\n", + "testX, testy = load_classification(name=\"Chinatown\",split=\"train\")\n", + "tr_test = sast.transform(testX)\n", + "rf = RandomForestClassifier(random_state=10)\n", + "rf.fit(st, y)\n", + "preds = rf.predict(tr_test)\n", + "print(\" Shapelets + random forest acc = \", accuracy_score(preds, testy))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From ddd830a0da10382e6417461245a803bc2737f0ef Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Tue, 2 Apr 2024 00:16:31 +0200 Subject: [PATCH 04/38] updated rsast tranformer --- .../collection/shapelet_based/_rsast.py | 83 +++++++++++++++---- .../shapelet_based (RSAST).ipynb | 28 +++---- 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 8ab7ad23f4..3bca11fef6 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -1,3 +1,6 @@ +import sys +sys.path.append(r'C:\Users\nicol\aeon') + import numpy as np from numba import get_num_threads, njit, prange, set_num_threads @@ -96,9 +99,8 @@ def __init__( seed = None, n_jobs = -1, ): - super().__init__() - self.n_random_points = n_random_points, - self.len_method = len_method, + self.n_random_points = n_random_points + self.len_method = len_method self.nb_inst_per_class = nb_inst_per_class self.n_jobs = n_jobs self.seed = seed @@ -106,6 +108,9 @@ def __init__( self._kernel_orig = None # non z-normalized subsequences self._kernels_generators = {} # Reference time series self._cand_length_list = None + super().__init__() + + def _fit(self, X, y): """Select reference time series and generate subsequences from them. @@ -123,9 +128,21 @@ def _fit(self, X, y): This transformer """ - #0- initialize variables and convert values in "y" to string - y=np.asarray([str(x_s) for x_s in y]) + #0- initialize variables and convert values in "y" to string + X_ = np.reshape(X, (X.shape[0], X.shape[-1])) + + self._random_state = ( + np.random.RandomState(self.seed) + if not isinstance(self.seed, np.random.RandomState) + else self.seed + ) + + classes = np.unique(y) + self._num_classes = classes.shape[0] + + candidates_ts = [] + y = np.asarray([str(x_s) for x_s in y]) self.cand_length_list = {} self.kernel_orig_ = [] @@ -142,12 +159,12 @@ def _fit(self, X, y): m_kernel = 0 #1--calculate ANOVA per each time t throught the lenght of the TS - for i in range (X.shape[1]): + for i in range (X_.shape[1]): statistic_per_class= {} for c in classes: - assert len(X[np.where(y==c)[0]][:,i])> 0, 'Time t without values in TS' + assert len(X_[np.where(y==c)[0]][:,i])> 0, 'Time t without values in TS' - statistic_per_class[c]=X[np.where(y==c)[0]][:,i] + statistic_per_class[c]=X_[np.where(y==c)[0]][:,i] #print("statistic_per_class- i:"+str(i)+', c:'+str(c)) #print(statistic_per_class[c].shape) @@ -162,7 +179,9 @@ def _fit(self, X, y): try: t_statistic, p_value = f_oneway(*statistic_per_class) except DegenerateDataWarning or ConstantInputWarning: - p_value=np.nan + p_value = np.nan + + #print('statistic_per_class', str(statistic_per_class)) # Interpretation of the results # if p_value < 0.05: " The means of the populations are significantly different." #print('pvalue', str(p_value)) @@ -177,12 +196,12 @@ def _fit(self, X, y): #2--calculate PACF and ACF for each TS chossen in each class for i, c in enumerate(classes): - X_c = X[y == c] + X_c = X_[y == c] cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) #set if the selection of instances is with replacement (if false it is not posible to select the same intance more than one) - choosen = self.random_state.permutation(X_c.shape[0])[:cnt] + choosen = self._random_state.permutation(X_c.shape[0])[:cnt] for rep, idx in enumerate(choosen): self.cand_length_list[c+","+str(idx)+","+str(rep)] = [] @@ -226,7 +245,7 @@ def _fit(self, X, y): if len(self.cand_length_list[c+","+str(idx)+","+str(rep)])==0: #chose a random lenght using the lenght of the time series (added 1 since the range start in 0) - rand_value= self.random_state.choice(len(X_c[idx]), 1)[0]+1 + rand_value= self._random_state.choice(len(X_c[idx]), 1)[0]+1 self.cand_length_list[c+","+str(idx)+","+str(rep)].extend([max(3,rand_value)]) #elif len(non_zero_acf)==0: #print("There is no AC in TS", idx, " of class ",c) @@ -255,14 +274,14 @@ def _fit(self, X, y): weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) - + if self.n_random_points > len(X_c[idx])-max_shp_length+1 : #set a upper limit for the posible of number of random points when selecting without replacement limit_rpoint=len(X_c[idx])-max_shp_length+1 - rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p=weights, replace=False) + rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p=weights, replace=False) #print("limit_rpoint:"+str(limit_rpoint)) else: - rand_point_ts = self.random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p=weights, replace=False) + rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p=weights, replace=False) @@ -271,7 +290,7 @@ def _fit(self, X, y): #2.6-- Extract the subsequence with that point kernel = X_c[idx][i:i+max_shp_length].reshape(1,-1) #print("kernel:"+str(kernel)) - if m_kernel 8\u001b[0m st \u001b[38;5;241m=\u001b[39m \u001b[43msast\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfit_transform\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 9\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m Shape of transformed data = \u001b[39m\u001b[38;5;124m\"\u001b[39m, st\u001b[38;5;241m.\u001b[39mshape)\n\u001b[0;32m 10\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m Distance of second series to third shapelet = \u001b[39m\u001b[38;5;124m\"\u001b[39m, st[\u001b[38;5;241m1\u001b[39m][\u001b[38;5;241m2\u001b[39m])\n", - "File \u001b[1;32m~\\aeon\\aeon\\transformations\\collection\\base.py:162\u001b[0m, in \u001b[0;36mBaseCollectionTransformer.fit_transform\u001b[1;34m(self, X, y)\u001b[0m\n\u001b[0;32m 159\u001b[0m X_inner \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_preprocess_collection(X)\n\u001b[0;32m 160\u001b[0m y_inner \u001b[38;5;241m=\u001b[39m y\n\u001b[1;32m--> 162\u001b[0m Xt \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_fit_transform\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mX_inner\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43my_inner\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 164\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_is_fitted \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m 166\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Xt\n", - "File \u001b[1;32m~\\aeon\\aeon\\transformations\\collection\\base.py:328\u001b[0m, in \u001b[0;36mBaseCollectionTransformer._fit_transform\u001b[1;34m(self, X, y)\u001b[0m\n\u001b[0;32m 309\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Fit to data, then transform it.\u001b[39;00m\n\u001b[0;32m 310\u001b[0m \n\u001b[0;32m 311\u001b[0m \u001b[38;5;124;03mFits the transformer to X and y and returns a transformed version of X.\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 324\u001b[0m \u001b[38;5;124;03mtransformed version of X.\u001b[39;00m\n\u001b[0;32m 325\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 326\u001b[0m \u001b[38;5;66;03m# Non-optimized default implementation; override when a better\u001b[39;00m\n\u001b[0;32m 327\u001b[0m \u001b[38;5;66;03m# method is possible for a given algorithm.\u001b[39;00m\n\u001b[1;32m--> 328\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_fit\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 329\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_transform(X, y)\n", - "File \u001b[1;32m~\\aeon\\aeon\\transformations\\collection\\shapelet_based\\_rsast.py:169\u001b[0m, in \u001b[0;36mRSAST._fit\u001b[1;34m(self, X, y)\u001b[0m\n\u001b[0;32m 165\u001b[0m p_value\u001b[38;5;241m=\u001b[39mnp\u001b[38;5;241m.\u001b[39mnan\n\u001b[0;32m 166\u001b[0m \u001b[38;5;66;03m# Interpretation of the results\u001b[39;00m\n\u001b[0;32m 167\u001b[0m \u001b[38;5;66;03m# if p_value < 0.05: \" The means of the populations are significantly different.\"\u001b[39;00m\n\u001b[0;32m 168\u001b[0m \u001b[38;5;66;03m#print('pvalue', str(p_value))\u001b[39;00m\n\u001b[1;32m--> 169\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m np\u001b[38;5;241m.\u001b[39misnan(p_value):\n\u001b[0;32m 170\u001b[0m n\u001b[38;5;241m.\u001b[39mappend(\u001b[38;5;241m0\u001b[39m)\n\u001b[0;32m 171\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n", - "\u001b[1;31mValueError\u001b[0m: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()" + "name": "stdout", + "output_type": "stream", + "text": [ + "total kernels:570\n", + " Shape of transformed data = (20, 570)\n", + " Distance of second series to third shapelet = 0.16883065\n", + " Shapelets + random forest acc = 1.0\n" ] } ], "source": [ "#X, y = load_basic_motions(split=\"train\")\n", "X, y = load_classification(name=\"Chinatown\",split=\"train\")\n", - "sast = RSAST(n_random_points=10,\n", + "rsast = RSAST(n_random_points=10,\n", " len_method=\"both\",\n", " nb_inst_per_class=10,\n", " seed=None,\n", " n_jobs=-1)\n", - "st = sast.fit_transform(X, y)\n", + "st = rsast.fit_transform(X, y)\n", "print(\" Shape of transformed data = \", st.shape)\n", "print(\" Distance of second series to third shapelet = \", st[1][2])\n", "#testX, testy = load_basic_motions(split=\"test\")\n", "testX, testy = load_classification(name=\"Chinatown\",split=\"train\")\n", - "tr_test = sast.transform(testX)\n", + "tr_test = rsast.transform(testX)\n", "rf = RandomForestClassifier(random_state=10)\n", "rf.fit(st, y)\n", "preds = rf.predict(tr_test)\n", From 73d8eac21a436d0ae5c2ee502924bd4da57b2403 Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sun, 7 Apr 2024 13:40:49 +0200 Subject: [PATCH 05/38] deleted example --- .../shapelet_based/_rsast_classifier.py | 14 +- .../collection/shapelet_based/_rsast.py | 80 +-- .../shapelet_based (RSAST).ipynb | 519 ------------------ 3 files changed, 32 insertions(+), 581 deletions(-) delete mode 100644 examples/classification/shapelet_based (RSAST).ipynb diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 4d7b800a44..b374b4053a 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -6,6 +6,7 @@ __maintainer__ = [] __all__ = ["RSASTClassifier"] + from operator import itemgetter import numpy as np @@ -16,6 +17,7 @@ from aeon.classification import BaseClassifier from aeon.transformations.collection.shapelet_based import RSAST from aeon.utils.numba.general import z_normalise_series +import matplotlib.pyplot as plt class RSASTClassifier(BaseClassifier): @@ -68,8 +70,8 @@ def __init__( n_jobs=-1, ): super().__init__() - self.n_random_points = n_random_points, - self.len_method = len_method, + self.n_random_points = n_random_points + self.len_method = len_method self.nb_inst_per_class = nb_inst_per_class self.n_jobs = n_jobs self.seed = seed @@ -152,7 +154,8 @@ def _predict_proba(self, X): dists[i, np.where(self.classes_ == preds[i])] = 1 return dists - def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): + def plot_most_important_feature_on_ts(self, ts,feature_importance, limit=5): + """Plot the most important features on ts. Parameters @@ -169,8 +172,6 @@ def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): fig : plt.figure The figure """ - import matplotlib.pyplot as plt - features = zip(self._transformer._kernel_orig, feature_importance) sorted_features = sorted(features, key=itemgetter(1), reverse=True) @@ -195,4 +196,5 @@ def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): axes[f].plot(range(ts.size), ts, linewidth=2) axes[f].set_title(f"feature: {f+1}") - return fig + #return fig + diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 3bca11fef6..2ee2b343e0 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -1,6 +1,3 @@ -import sys -sys.path.append(r'C:\Users\nicol\aeon') - import numpy as np from numba import get_num_threads, njit, prange, set_num_threads @@ -105,9 +102,10 @@ def __init__( self.n_jobs = n_jobs self.seed = seed self._kernels = None # z-normalized subsequences - self._kernel_orig = None # non z-normalized subsequences + self._cand_length_list = {} + self._kernel_orig = [] self._kernels_generators = {} # Reference time series - self._cand_length_list = None + super().__init__() @@ -141,15 +139,8 @@ def _fit(self, X, y): classes = np.unique(y) self._num_classes = classes.shape[0] - candidates_ts = [] - y = np.asarray([str(x_s) for x_s in y]) - self.cand_length_list = {} - self.kernel_orig_ = [] - self.kernels_generators_ = [] - self.class_generators_ = [] - - list_kernels =[] + y = np.asarray([str(x_s) for x_s in y]) @@ -203,8 +194,10 @@ def _fit(self, X, y): choosen = self._random_state.permutation(X_c.shape[0])[:cnt] + self._kernels_generators[c] = [] + for rep, idx in enumerate(choosen): - self.cand_length_list[c+","+str(idx)+","+str(rep)] = [] + self._cand_length_list[c+","+str(idx)+","+str(rep)] = [] non_zero_acf=[] if (self.len_method == "both" or self.len_method == "ACF" or self.len_method == "Max ACF") : #2.1-- Compute Autorrelation per object @@ -216,9 +209,9 @@ def _fit(self, X, y): #Consider just the maximum ACF value if prev_acf!=0 and self.len_method == "Max ACF": non_zero_acf.remove(prev_acf) - self.cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_acf) + self._cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_acf) non_zero_acf.append(j) - self.cand_length_list[c+","+str(idx)+","+str(rep)].append(j) + self._cand_length_list[c+","+str(idx)+","+str(rep)].append(j) prev_acf=j non_zero_pacf=[] @@ -232,21 +225,21 @@ def _fit(self, X, y): #Consider just the maximum PACF value if prev_pacf!=0 and self.len_method == "Max PACF": non_zero_pacf.remove(prev_pacf) - self.cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_pacf) + self._cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_pacf) non_zero_pacf.append(j) - self.cand_length_list[c+","+str(idx)+","+str(rep)].append(j) + self._cand_length_list[c+","+str(idx)+","+str(rep)].append(j) prev_pacf=j if (self.len_method == "all"): - self.cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3,1+ len(X_c[idx]))) + self._cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3,1+ len(X_c[idx]))) #2.3-- Save the maximum autocorralated lag value as shapelet lenght - if len(self.cand_length_list[c+","+str(idx)+","+str(rep)])==0: + if len(self._cand_length_list[c+","+str(idx)+","+str(rep)])==0: #chose a random lenght using the lenght of the time series (added 1 since the range start in 0) rand_value= self._random_state.choice(len(X_c[idx]), 1)[0]+1 - self.cand_length_list[c+","+str(idx)+","+str(rep)].extend([max(3,rand_value)]) + self._cand_length_list[c+","+str(idx)+","+str(rep)].extend([max(3,rand_value)]) #elif len(non_zero_acf)==0: #print("There is no AC in TS", idx, " of class ",c) #elif len(non_zero_pacf)==0: @@ -257,9 +250,9 @@ def _fit(self, X, y): #print("Kernel lenght list:",self.cand_length_list[c+","+str(idx)],"") #remove duplicates for the list of lenghts - self.cand_length_list[c+","+str(idx)+","+str(rep)]=list(set(self.cand_length_list[c+","+str(idx)+","+str(rep)])) + self._cand_length_list[c+","+str(idx)+","+str(rep)]=list(set(self._cand_length_list[c+","+str(idx)+","+str(rep)])) #print("Len list:"+str(self.cand_length_list[c+","+str(idx)+","+str(rep)])) - for max_shp_length in self.cand_length_list[c+","+str(idx)+","+str(rep)]: + for max_shp_length in self._cand_length_list[c+","+str(idx)+","+str(rep)]: #2.4-- Choose randomly n_random_points point for a TS #2.5-- calculate the weights of probabilities for a random point in a TS @@ -288,29 +281,29 @@ def _fit(self, X, y): for i in rand_point_ts: #2.6-- Extract the subsequence with that point - kernel = X_c[idx][i:i+max_shp_length].reshape(1,-1) + kernel = X_c[idx][i:i+max_shp_length].reshape(1,-1).copy() #print("kernel:"+str(kernel)) if m_kernel < max_shp_length: m_kernel = max_shp_length - list_kernels.append(kernel) - self.kernel_orig_.append(np.squeeze(kernel)) - self.kernels_generators_.append(np.squeeze(X_c[idx].reshape(1,-1))) - self.class_generators_.append(c) + + self._kernel_orig.append(np.squeeze(kernel)) + self._kernels_generators[c].extend(X_c[idx].reshape(1,-1)) + + - print("total kernels:"+str(len(self.kernel_orig_))) #3--save the calculated subsequences - n_kernels = len (self.kernel_orig_) + n_kernels = len (self._kernel_orig) self._kernels = np.full( (n_kernels, m_kernel), dtype=np.float32, fill_value=np.nan) - for k, kernel in enumerate(self.kernel_orig_): + for k, kernel in enumerate(self._kernel_orig): self._kernels[k, :len(kernel)] = z_normalise_series(kernel) return self @@ -344,28 +337,3 @@ def _transform(self, X, y=None): return X_transformed -if __name__ == "__main__": - - from sklearn.ensemble import RandomForestClassifier - - from aeon.datasets import load_basic_motions,load_classification - - from aeon.transformations.collection.shapelet_based import RSAST - - from sklearn.metrics import accuracy_score - - #X, y = load_basic_motions(split="train") - X, y = load_classification(name="Chinatown",split="train") - rsast = RSAST( ) - - rsast.fit(X, y) - st = rsast.transform(X, y) - print(" Shape of transformed data = ", st.shape) - print(" Distance of second series to third shapelet = ", st[1][2]) - #testX, testy = load_basic_motions(split="test") - testX, testy = load_classification(name="Chinatown",split="train") - tr_test = rsast.transform(testX) - rf = RandomForestClassifier(random_state=10) - rf.fit(st, y) - preds = rf.predict(tr_test) - print(" Shapelets + random forest acc = ", accuracy_score(preds, testy)) \ No newline at end of file diff --git a/examples/classification/shapelet_based (RSAST).ipynb b/examples/classification/shapelet_based (RSAST).ipynb deleted file mode 100644 index 06586b10d6..0000000000 --- a/examples/classification/shapelet_based (RSAST).ipynb +++ /dev/null @@ -1,519 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "# Shapelet based time series machine learning\n", - "\n", - "Shapelets a subsections of times series taken from the train data that are a useful for time series machine learning. They were first proposed ia primitive for machine learning [1][2] and were embedded in a decision tree for classification. The Shapelet Transform Classifier (STC)[3,4] is a pipeline classifier which searches the training data for shapelets, transforms series to vectors of distances to a filtered set of selected shapelets based on information gain, then builds a classifier on the latter.\n", - "\n", - "Finding shapelets involves selecting and evaluating shapelets. The original shapelet tree and STC performed a full enumeration of all possible shapelets before keeping the best ones. This is computationally inefficient, and modern shapelet based machine learning algorithms randomise the search." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "sys.path.append(r'C:\\Users\\nicol\\aeon')" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[('MrSQMClassifier',\n", - " aeon.classification.shapelet_based._mrsqm.MrSQMClassifier),\n", - " ('RDSTClassifier', aeon.classification.shapelet_based._rdst.RDSTClassifier),\n", - " ('ShapeletTransformClassifier',\n", - " aeon.classification.shapelet_based._stc.ShapeletTransformClassifier)]" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import warnings\n", - "\n", - "from sklearn.ensemble import RandomForestClassifier\n", - "from sklearn.metrics import accuracy_score\n", - "\n", - "from aeon.datasets import load_basic_motions\n", - "from aeon.registry import all_estimators\n", - "from aeon.transformations.collection.shapelet_based import RandomShapeletTransform\n", - "\n", - "warnings.filterwarnings(\"ignore\")\n", - "all_estimators(\"classifier\", filter_tags={\"algorithm_type\": \"shapelet\"})" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "### Shapelet Transform for Classification\n", - "\n", - "The `RandomShapeletTransform` transformer takes a set of labelled training time series in the `fit` function, randomly samples `n_shapelet_samples` shapelets, keeping the best `max_shapelets`. The resulting shapelets are used in the `transform` function to create a new tabular dataset, where each row represents a time series instance, and each column stores the distance from a time series to a shapelet. The resulting tabular data can be used by any scikit learn compatible classifier. In this notebook we will explain these terms and describe how the algorithm works. But first we show it in action. We will use the BasicMotions data as an example. This data set contains time series of motion traces for the activities \"running\", \"walking\", \"standing\" and \"badminton\". The learning problem is to predict the activity given the time series. Each time series has six channels: x, y, z position and x, y, z accelerometer of the wrist. Data was recorded on a smart watch." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Shape of transformed data = (40, 8)\n", - " Distance of second series to third shapelet = 1.302772121165026\n", - " Shapelets + random forest acc = 0.95\n" - ] - } - ], - "source": [ - "X, y = load_basic_motions(split=\"train\")\n", - "rst = RandomShapeletTransform(n_shapelet_samples=100, max_shapelets=10, random_state=42)\n", - "st = rst.fit_transform(X, y)\n", - "print(\" Shape of transformed data = \", st.shape)\n", - "print(\" Distance of second series to third shapelet = \", st[1][2])\n", - "testX, testy = load_basic_motions(split=\"test\")\n", - "tr_test = rst.transform(testX)\n", - "rf = RandomForestClassifier(random_state=10)\n", - "rf.fit(st, y)\n", - "preds = rf.predict(tr_test)\n", - "print(\" Shapelets + random forest acc = \", accuracy_score(preds, testy))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "### Visualising Shapelets\n", - "The first column of the transformed data represents the distance from the first shapelet to each time series. The shapelets are sorted, so the first shapelet is the one we estimate is the best (using the calculation described below). You can recover the shapelets from the transform. Each shapelet is a 7-tuple, storing the following information:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Quality = 0.81127812\n", - "Length = 39\n", - "position = 55\n", - "Channel = 0\n", - "Origin Instance Index = 11\n", - "Class label = running\n", - "Shapelet = [-0.85667017 -1.88711152 -0.8751295 0.80633757 1.10838333 0.69810992\n", - " 0.85713394 1.23190921 0.01801365 -1.29683966 -1.94694259 -0.37487726\n", - " -0.37487726 1.39471462 0.74922685 0.74922685 0.22343376 0.22343376\n", - " -0.7730703 -1.37591995 -0.80376393 1.32758071 0.99778845 0.6013481\n", - " 0.83711118 0.93684593 0.93684593 -1.30429475 -1.64522057 -0.56312308\n", - " 0.96855713 0.56796251 0.35714242 0.62066541 0.65135287 -0.80531237\n", - " -1.49170075 -1.18512797 0.69685753]\n" - ] - } - ], - "source": [ - "running_shapelet = rst.shapelets[0]\n", - "print(\"Quality = \", running_shapelet[0])\n", - "print(\"Length = \", running_shapelet[1])\n", - "print(\"position = \", running_shapelet[2])\n", - "print(\"Channel = \", running_shapelet[3])\n", - "print(\"Origin Instance Index = \", running_shapelet[4])\n", - "print(\"Class label = \", running_shapelet[5])\n", - "print(\"Shapelet = \", running_shapelet[6])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "We can directly extract shapelets and inspect them. These are the the two shapelets that are best at discriminating badminton and running against other activities. All shapelets are normalised to provide scale invariance." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Badminton shapelet from channel 0 (x-dimension) (0.65194393, 74, 7, 1, 1, 'standing', array([-5.27667376, -0.94911454, 0.90433173, 1.26316864, 2.34760078,\n", - " 1.84408 , 0.9192852 , 0.9192852 , -1.29868372, -1.29868372,\n", - " -1.5476774 , -1.03000413, 0.27593674, -0.70184658, 0.37460295,\n", - " 1.27398121, 1.02881837, 0.64543662, -0.0669839 , -0.54373096,\n", - " -0.55716134, -0.56605101, -0.08611633, 0.31270572, 0.25642625,\n", - " 0.5512744 , 0.78929504, 0.73385326, 0.73385326, -0.26777726,\n", - " -0.63967737, -0.63967737, -0.5539071 , -0.5539071 , 0.3867047 ,\n", - " 0.3867047 , 0.88832979, 0.85074214, 0.46901267, 0.0925433 ,\n", - " -0.34444436, -0.72498936, -0.83763127, -0.53034818, -0.05869122,\n", - " 0.46600593, 1.02537238, 0.81800526, 0.51709059, 0.17497366,\n", - " -0.31072836, -0.64876695, -0.89102368, -0.60834799, -0.0627886 ,\n", - " 0.42532723, 0.95696668, 0.91077086, 0.77491818, 0.14283377,\n", - " 0.14283377, -1.08722874, -1.08722874, -0.65706914, -0.65706914,\n", - " 0.28210933, 0.74159654, 0.8064869 , 0.8064869 , 0.19889294,\n", - " -0.16601048, -0.78706337, -0.76364317, -0.63789726]))\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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uRnvvfeeR9eXy+Wt/n6vvfZamx/3lClToFQq8dprr9ndbsu/pzsrV660q0/asWMHtm/fLq4WEj71txx7y/uRy+WYOnUqVq5cicLCQvH8o0ePYs2aNV6Nyd+c/Y9deumlMJlMeP311+3Of+mll8BxnPickMCijEgn88svv4jRfVlZGT777DOcOHECixYtQmRkJAC+FuHOO+/E0qVLsW/fPlx88cVQKpU4ceIEvvrqK7zyyiu45ppr8OGHH+LNN9/EVVddhdzcXNTV1eG9995DZGSkeJALCQlBnz598MUXX6BHjx6IiYlBv379xOWwLfXp0wcTJ07E0KFDERMTg127duHrr7/GggULvH6s06dPx3//+19MmzYN//d//4eysjK88cYb6Natm8O6k379+mHq1Kl2y3cBiJ1hAeDZZ5/Fxo0bMXLkSMybNw99+vRBZWUl9uzZg3Xr1qGystLpeN544w2MGzcO/fv3x7x585CTk4Nz585h69atOHPmDPbv3w+ADy7kcjn+85//oKamBmq1GhdeeCE+++wzt8+3NwYOHIjZs2fj3XffRXV1NSZMmIAdO3bgww8/xIwZMzBp0iSvb9MTt99+O+666y7MnDkTF110Efbv3481a9YgLi7OL/fnK3feeSdef/113HjjjbjvvvuQnJyMTz/9VGyQ5i6Tddlll+Hjjz+GVqtFnz59sHXrVqxbt67NyzyF3jnCktdLL70Ue/fuxS+//OLVc9mtWzeMGzcOd999N3Q6HV5++WXExsbi4YcfBgBERkZi/PjxeO6552AwGJCamopff/0VeXl5rW5ryZIlWL16NS644ALcc889MBqNeO2119C3b1+H/3NSEYrS7733XkydOhVyuRw33HADLr/8ckyaNAn/+Mc/kJ+fj4EDB+LXX3/F999/j/vvv9+uMJUEUMDX6RC/cLR8V6PRsEGDBrG33nrLbvmf4N1332VDhw5lISEhLCIigvXv3589/PDDrKSkhDHG2J49e9iNN97IMjIymFqtZgkJCeyyyy6zWwLLGGNbtmxhQ4cOZSqVyu1S3qeffpqNGDGCRUVFsZCQENarVy/2zDPPML1eL17G0VJUxhh78sknWy3J++CDD1j37t2ZWq1mvXr1YsuWLXN4OQBs/vz57JNPPhEvP3jwYIdLMs+dO8fmz5/P0tPTmVKpZElJSWzy5Mns3Xff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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "badminton_shapelet = rst.shapelets[4]\n", - "print(\" Badminton shapelet from channel 0 (x-dimension)\", badminton_shapelet)\n", - "plt.title(\"Best shapelets for running and badminton\")\n", - "plt.plot(badminton_shapelet[6], label=\"Badminton\")\n", - "plt.plot(running_shapelet[6], label=\"Running\")\n", - "plt.legend()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "Both shapelets are in the x-axis, so represent side to side motion. Badminton is characterised by sa single large peak in one direction, capturing the drawing of the hand back and quickly hittig the shuttlcock. Running is chaaracterised by a longer repetition of side to side motions, with a sharper peak representing bringing the arm forward accross the body in a running motion." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false - }, - "source": [ - "## Performance on the UCR univariate datasets\n", - "\n", - "You can find the interval based classifiers as follows." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MrSQMClassifier\n", - "RDSTClassifier\n", - "ShapeletTransformClassifier\n" - ] - } - ], - "source": [ - "from aeon.registry import all_estimators\n", - "\n", - "est = [\"MrSQMClassifier\", \"RDSTClassifier\", \"ShapeletTransformClassifier\"]\n", - "for c in est:\n", - " print(c)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(112, 3)" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from aeon.benchmarking import get_estimator_results_as_array\n", - "from aeon.datasets.tsc_data_lists import univariate\n", - "\n", - "names = [t.replace(\"Classifier\", \"\") for t in est]\n", - "results, present_names = get_estimator_results_as_array(\n", - " names, univariate, include_missing=False\n", - ")\n", - "results.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(
, )" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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c+fO1dChQzV9+nStXbtWnp6eatmypT788EOVK1cuSR1vb29t2rRJU6ZM0Zw5c/THH39oy5YtKlCggLy9vdWtWzc1adLEIdynpnz58vrjjz80YcIEzZ8/X3/88Yc2btyoQoUKycfHR3369FHz5s0d6ri5uWnp0qV69913tXHjRu3atUvx8fGqXbt2loT0fPnyadu2bRo4cKCWLl2q//3vfypYsKAaNGiggQMHav369Zm2rVy5cmnChAlas2aNduzYoeXLlys6Olre3t5q0aKFunfvrnr16qWrzYIFC2rDhg0aM2aM5syZo2XLlik+Pl4BAQHq0qWL+vTpk+yd4AAA4N5o3769hg8frr1792rYsGH69NNP7evOnTunqVOnSrqdgfLkySNvb2+1a9dODRs2VIsWLVLMXevXr9eKFSv066+/6tChQ1q5cqWuXbumPHnyKCQkRA0bNlT37t1VqFChe7KfmclmMjI1ikUdP35cAQEB8vPzs1/ZBwBAuv2NsJubW4ZnsgCArMQ8XAAAAIDFENIBAAAAiyGkAwAAABbzQI5JBwAgJYxJB3A/4Eo6AABAImvXrlVoaKi8vb1ls9n0008/3bHO+PHjVbZsWXl4eKh06dKaNm2aw/o9e/aoZcuW8vf3l81m0+eff56kjatXr6pnz57y8/OTh4eHatSooa1btyYpt2/fPjVp0kR58+aVp6enQkJC7A+SxIODkA4AAJDI9evXVbFiRY0fPz5N5b/88ku9/fbbGjRokPbs2aMPPvhAr7/+uhYuXGgvExUVpRIlSmjIkCEqWrRosu288sorWr58uaZPn64///xTDRo0UP369RUREWEvc+TIEdWqVUtlypTR6tWr9ccff+i9995L89TQuH8w3AUA8FBhuAvSw2azaf78+WrWrFmKZWrUqKGaNWvan4wpSb1799bmzZuTfdaIv7+/evbsqZ49e9qX3bhxQ7lz59bPP/+s559/3r68cuXKevbZZ/XRRx9Juv00d1dXV02fPj3jOwdL40o6AABABty6dSvJlWwPDw9t2bJFMTExaWojNjZWcXFxybaTEPTj4+O1aNEiPfbYY2rYsKG8vLxUrVq1NA3Hwf2HkA4AAJABDRs21DfffKPff/9dxhht27ZN33zzjWJiYnThwoU0tZE7d25Vr15dgwcP1unTpxUXF6cZM2Zo48aNOnPmjCTp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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from aeon.visualisation import plot_boxplot_median, plot_critical_difference\n", - "\n", - "plot_critical_difference(results, names)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(
, )" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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AJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGXJKofuee+5RfX29/H6/lixZoi1btpxw3wceeECGYfR7+P3+E+7/yU9+UoZhaP369adSGgAAAAAAOSPt0P3II49ozZo1uv3227Vt2zbNnz9fK1asUFNT0wmPKSws1KFDh/oee/bsGXS/n//853r++edVU1OTblkAAAAAAOSctEP33Xffreuuu06rV6/WnDlztGHDBgWDQd1///0nPMYwDFVVVfU9KisrB+xz4MAB3XjjjXrooYfk8XhOWkM8HlckEun3AAAAAAAg16QVuhOJhLZu3arly5cfPYFpavny5dq8efMJj+vq6lJdXZ1qa2t1+eWX65VXXun3um3buvrqq3XzzTdr7ty571jHunXrFAqF+h61tbXpfAwAAAAAAEZFWqG7paVFlmUNaKmurKxUQ0PDoMfMnDlT999/v375y1/qwQcflG3bWrZsmfbv39+3zze/+U253W59+tOfHlIda9euVTgc7nvs27cvnY8BAAAAAMCocGf6DZYuXaqlS5f2PV+2bJlmz56tH/3oR7rrrru0detWfe9739O2bdtkGMaQzunz+eTz+TJVMgAAAAAAIyKtlu6ysjK5XC41Njb2297Y2KiqqqohncPj8eiss87Sm2++KUl65pln1NTUpEmTJsntdsvtdmvPnj363Oc+p/r6+nTKAwAAAAAgp6QVur1erxYuXKhNmzb1bbNtW5s2berXmn0ylmVpx44dqq6uliRdffXVeumll7R9+/a+R01NjW6++Wb99re/Tac8AAAAAAByStrdy9esWaNVq1Zp0aJFWrx4sdavX6/u7m6tXr1aknTNNddowoQJWrdunSTpzjvv1Lnnnqtp06apo6ND3/72t7Vnzx5de+21kqTS0lKVlpb2ew+Px6OqqirNnDlzuJ8PAAAAAICsSTt0r1y5Us3NzbrtttvU0NCgBQsWaOPGjX2Tq+3du1emebQBvb29Xdddd50a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wmyXAUhiyTAAAAAA48zU+qnZLgHoQ0s3AAAAAAAZQugGAAAAACBDCN0AAAAAAGQIoRsAAAAAgAwhdAMAAAAAkCGEbgAAAAAAMoTQDQAAAABAhhC6AQAAAADIEEI3AAAAAAAZQugGAAAAACBDTil033PPPaqvr5ff79eSJUu0ZcuWIR338MMPyzAMXXHFFSfc55Of/KQMw9D69etPpTQAAAAAAHJG2qH7kUce0Zo1a3T77bdr27Ztmj9/vlasWKGmpqaTHrd7927ddNNNOv/880+4z89//nM9//zzqqmpSbcsAAAAAAByTtqh++6779Z1112n1atXa86cOdqwYYOCwaDuv//+Ex5jWZY+8pGP6I477tCUKVMG3efAgQO68cYb9dBDD8nj8Zy0hng8rkgk0u8BAAAAAECuSSt0JxIJbd26VcuXLz96AtPU8uXLtXnz5hMed+edd6qiokKf+MQnBn3dtm1dffXVuvnmmzV37tx3rGPdunUKhUJ9j9ra2nQ+BgAAAAAAoyKt0N3S0iLLslRZWdlve2VlpRoaGgY95tlnn9V9992ne++994Tn/eY3vym3261Pf/rTQ6pj7dq1CofDfY99+/YN/UMAAAAAADBK3Jk8eWdnp66++mrde++9KisrG3SfrVu36nvf+562bdsmwzCGdF6fzyefzzeSpQIAAAAAMOLSCt1lZWVyuVxqbGzst72xsVFVVVUD9t+1a5d2796tSy+9tG+bbdu9b+x2a+fOnXrmmWfU1NSkSZMm9e1jWZY+97nPaf369dq9e3c6JQIAAAAAkDPSCt1er1cLFy7Upk2b+pb9sm1bmzZt0g033DBg/1mzZmnHjh39tn35y19WZ2envve976m2tlZXX311vzHikrRixQpdffXVWr16dZofBwAAAACA3JF29/I1a9Zo1apVWrRokRYvXqz169eru7u7LyBfc801mjBhgtatWye/36958+b1O76oqEiS+raXlpaqtLS03z4ej0dVVVWaOXPmqXwmAAAAAAByQtqhe+XKlWpubtZtt92mhoYGLViwQBs3buybXG3v3r0yzbRXIgMAAAAAYNwxHMdxsl3EcEUiEYVCIYXDYRUWFma7HAAAAADAODfUHEqTNAAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIacUui+5557VF9fL7/fryVLlmjLli1DOu7hhx+WYRi64oor+rYlk0l94Qtf0BlnnKG8vDzV1NTommuu0cGDB0+lNAAAAAAAckbaofuRRx7RmjVrdPvtt2vbtm2aP3++VqxYoaamppMet3v3bt100006//zz+22PRqPatm2bbr31Vm3btk0/+9nPtHPnTl122WXplgYAAAAAQE4xHMdx0jlgyZIlOuecc/TDH/5QkmTbtmpra3XjjTfqlltuGfQYy7J0wQUX6OMf/7ieeeYZdXR06Be/+MUJ3+Mvf/mLFi9erD179mjSpEnvWFMkElEoFFI4HFZhYWE6HwcAAAAAgLQNNYem1dKdSCS0detWLV++/OgJTFPLly/X5s2bT3jcnXfeqYqKCn3iE58Y0vuEw2EZhqGioqJBX4/H44pEIv0eAAAAAADkmrRCd0tLiyzLUmVlZb/tlZWVamhoGPSYZ599Vvfdd5/uvffeIb1HLBbTF77wBV111VUnvFuwbt06hUKhvkdtbW06HwMAAAAAgFGR0dnLOzs7dfXVV+vee+9VWVnZO+6fTCb1j//4j3IcR//2b/92wv3Wrl2rcDjc99i3b99Ilg0AAAAAwIhwp7NzWVmZXC6XGhsb+21vbGxUVVXVgP137dql3bt369JLL+3bZtt27xu73dq5c6emTp0q6Wjg3rNnj/74xz+etE+8z+eTz+dLp3QAAAAAAEZdWi3dXq9XCxcu1KZNm/q22batTZs2aenSpQP2nzVrlnbs2KHt27f3PS677DJdfPHF2r59e1+38COB+4033tAf/vAHlZaWDvNjAQAAAACQfWm1dEvSmjVrtGrVKi1atEiLFy/W+vXr1d3drdWrV0uSrrnmGk2YMEHr1q2T3+/XvHnz+h1/ZHK0I9uTyaQ+9KEPadu2bXrsscdkWVbf+PCSkhJ5vd7hfD4AAAAAALIm7dC9cuVKNTc367bbblNDQ4MWLFigjRs39k2utnfvXpnm0BvQDxw4oEcffVSStGDBgn6vPfHEE7rooovSLREAAAAAgJyQ9jrduYh1ugEAAAAAoykj63QDAAAAAIChI3QDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkCKEbAAAAAIAMIXQDAAAAAJAhhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAAAAAAGULoBgAAAAAgQwjdAAAAAABkyCmF7nvuuUf19fXy+/1asmSJtmzZMqTjHn74YRmGoSuuuKLfdsdxdNttt6m6ulqBQEDLly/XG2+8cSqlAQAAAACQM9IO3Y888ojWrFmj22+/Xdu2bdP8+fO1YsUKNTU1nfS43bt366abbtL5558/4LVvfetb+v73v68NGzboz3/+s/Ly8rRixQrFYrF0ywMAAAAAIGcYjuM46RywZMkSnXPOOfrhD38oSbJtW7W1tbrxxht1yy23DHqMZVm64IIL9PGPf1zPPPOMOjo69Itf/EJSbyt3TU2NPve5z+mmm26SJIXDYVVWVuqBBx7Qhz/84QHni8fjisfjfc/D4bAmTZqkffv2qbCwMJ2PAwAAAABA2iKRiGpra9XR0aFQKHTC/dzpnDSRSGjr1q1au3Zt3zbTNLV8+XJt3rz5hMfdeeedqqio0Cc+8Qk988wz/V57++231dDQoOXLl/dtC4VCWrJkiTZv3jxo6F63bp3uuOOOAdtra2vT+TgAAAAAAAxLZ2fnyIXulpYWWZalysrKftsrKyv12muvDXrMs88+q/vuu0/bt28f9PWGhoa+cxx/ziOvHW/t2rVas2ZN33PbttXW1qbS0lIZhjHUjwOk7cjdLHpVABgv+F4DMN7wvYbR4jiOOjs7VVNTc9L90grd6ers7NTVV1+te++9V2VlZSN2Xp/PJ5/P129bUVHRiJ0feCeFhYV8iQMYV/heAzDe8L2G0XCyFu4j0grdZWVlcrlcamxs7Le9sbFRVVVVA/bftWuXdu/erUsvvbRvm23bvW/sdmvnzp19xzU2Nqq6urrfORcsWJBOeQAAAAAA5JS0Zi/3er1auHChNm3a1LfNtm1t2rRJS5cuHbD/rFmztGPHDm3fvr3vcdlll+niiy/W9u3bVVtbq8mTJ6uqqqrfOSORiP785z8Pek4AAAAAAMaKtLuXr1mzRqtWrdKiRYu0ePFirV+/Xt3d3Vq9erUk6ZprrtGECRO0bt06+f1+zZs3r9/xR7qBH7v9M5/5jL761a9q+vTpmjx5sm699VbV1NQMWM8byDafz6fbb799wPAGABir+F4DMN7wvYZck3boXrlypZqbm3XbbbepoaFBCxYs0MaNG/smQtu7d69MM73lvz//+c+ru7tb119/vTo6OnTeeedp48aN8vv96ZYHZJTP59NXvvKVbJcBACOG7zUA4w3fa8g1aa/TDQAAAAAAhia9JmkAAAAAADBkhG4AAAAAADKE0A0AAAAAQIYQugEAAAAAyBBCNwAA41Q0Gs12CQAAnPYI3cBxmNAfwHjw0ksv6aMf/aj27t2b7VIAADitEboB9a4V/5nPfEaSZBgGwRvAmPbiiy/q7LPP1rx58zRp0qRslwMAw2bbdrZLAE6ZO9sFANm2bds2fec73+l7vn79+r7gbRhGFisDgPS99tprWrp0qW6//Xbdeuut2S4HAIbNtm2ZZm9b4fbt2xWPx7VkyZIsVwUMneHQpIfTXGNjo6699loVFhZq48aNuuKKK3TfffdJEsEbwJjy0ksv6ZJLLlE0Gu0bz21ZllwuV5YrA4Dh+/znP68HH3xQ4XBYixcv1pe//GVddNFFfMch59G9HKetI/ebKisrVV9fr1dffVX33nuvfv7zn+v666+XRFdzAGPHiy++qHPPPVcf+MAHNGPGDC1evFjRaFQul4tumQDGpGOvwZ599llt3LhRP/nJT/T0008rFovpi1/8on71q1/JsqwsVgm8M0I3TlvHtmDfddddCoVCikQiuueee/TTn/5U/+t//a++/QjeAHLZyy+/rEWLFulzn/ucHnjgAf34xz9WZ2enLrroIvX09Mg0TYI3gDHFtu1+12olJSX6wAc+oOXLl2vhwoX6/e9/r/z8fK1bt06PPfYYwRs5jdCN086XvvQlXX311XrqqafU0tIiSfJ4PJo+fbpefvllXXXVVbrvvvv005/+VJ/61KckiS7mAHJaa2urbr/9dt11112SpPnz5+vhhx9WV1eXLrzwQoI3gDHnyBjub37zm7r00kt1xRVXaNeuXX2v5+fn65e//KUKCgr0zW9+U//1X//FdxxyFqEbp5UtW7Zo3bp1euihh3T//ffrPe95jx599FHZtq1bbrlF9913n5599ln90z/9U1/wXrlyZbbLBoBB7dmzR3fffbfq6ur05S9/WdLR7pjz58/X//2//5fgDWBMObZ34b/+67/qrrvu0ty5c+X1evWnP/1JP/zhD5VKpSQdDd7d3d3atGlTX1AHcg3/MnFamT59ur75zW/K6/WqpqZG119/vW677TZdfvnl+ulPf6r3ve99euKJJyRJV155pb73ve+poKAgy1UDwEAvv/yyVqxYoS1btujpp5/u235sz5xjg/e73/1uRaNRLkoB5LQj32FPPPGE3nrrLf3nf/6nvvGNb2jLli06//zz9cgjj+g//uM/+m4g5uXl6fnnn9ePfvSjbJYNnBSzl+O009HRoe9///v6yle+oscff1wLFy7U008/ra9+9at66aWX9J73vEe//vWv5XK5lEql5Hb3rqzHTOYAcsWrr76q888/X5/4xCf02c9+VlVVVSfd/6WXXtJ73/tezZ07V5s2bRqlKgFg6I5daeG3v/2t1q5dq8bGRv3sZz/rWx6sra1N//zP/6x9+/bpmmuu0Sc+8Yl+M5ezWgNyFaEb414kElFHR4dSqZSmTJkiqfdL+dZbb9U3vvEN/fjHP9bVV1+tnp4ePf3001q0aJFKS0uzXDUADC4Wi+maa65RZWWlfvCDH/Tb3tnZqfb2ds2YMWPAcS+//LICgYCmTp06muUCQFr+4z/+Q8FgUFu2bNFDDz2kf/zHf9Q999zT93p7e7tuvPFGbdmyRd/61rd0xRVXZK9YYIjc2S4AyKTvfOc7evLJJ/XUU08pPz9fM2fO1A033KD3v//9+vrXvy7TNLVq1SrF43Fde+21WrFihaTeGTPpggkgF1mWpZ07d+qCCy7o2/a73/1Ojz32mB566CElEgldddVVuvPOO1VVVdXXS2fevHlZrBoABnfsNde//uu/6qabbtKrr76q9773vfJ4PHriiSd055136rbbbpMkFRcX63vf+55+8IMf6NJLL81m6cCQEboxbt1888366U9/qq985Sv68Ic/rFgspu9+97u67rrrdPPNN2vNmjW64447ZJqmPvWpT8nj8WjVqlWSROAGkLMsy1Jtba1efPFF/e1vf9Njjz2m//2//7fmz5+v22+/XRMmTNCHP/xhzZw5U5/73OcYFgMgpx255nruuecUjUb17//+76qrq5Mk3XLLLbJtW48//rgMw9Ctt94qSSotLdVXvvIVSXQpx9hA6Ma49PDDD+uRRx7pG7N9xKpVq/R3f/d3uvvuuzV16lStXLlSN910k0zT1OrVqzV16lSdd955WawcAE6usLBQf/d3f6d///d/1yWXXKJ4PK5vfvObeve7393Xdfzv//7v9fzzz2e5UgAYmldffbXv+uvIsBnHcVRaWqovfvGL+vrXv67f/OY3ikQi+va3v93vWAI3xgLGdGNcOdKN8uabb1ZjY6N+8pOf9N0BTSaT8ng8SqVSWrRokfLy8vSnP/1JktTV1aXf//73uvLKK7P8CQCgv87OTnV0dGjHjh0KBAK6+OKLJUm7du1SS0uLJk+erIqKir79Y7GYPvShD2nx4sV93TEBINf98pe/1NVXX61LL71UGzZsUEFBQd91XWtrq77whS/INE396Ec/ogcPxhxCN8al9773vSoqKtJ//ud/9ut2dGQ28v/+7//W//pf/0tbtmwZMKkQ47kB5IqdO3dq7dq12rVrl1555RXZtq13vetd+vKXv6yLL75YXq+33/6WZekrX/mKfvzjH+uPf/yjpk2blqXKAWBwJ7vOeuSRR/TRj35Un/3sZ/X1r39dbre7L3iHw2EVFhbKMAxWlMGYQ/dyjEvl5eV64YUXJPV2OzryBX9k+a9QKKRwODzolz6BG0AuePHFF7VixQp96EMf0sc//nFNnz5dr7zyim655RZde+21+pd/+Rf9wz/8Q99Nxccee0yPPfaYfv7zn2vjxo0EbgA559jAvWHDBr3y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4okntH37dkns4QbgmPLz8xUdHS0XFxe1bNlS27ZtU2Jiol588UUdPnxYJ06cUEJCgqxWq/r27auUlBR+ywA4tN///vfasGGD4uLi1LVrV1vzw2q16uTJkxo1apSKioqUm5srLy8vO1cLPByEbji0+fPna+3atZo9e7aGDh2q0NBQZWZmqkePHtq5c6ekf/1ntbKyUosWLdKuXbvUpUsXxcfH33UcABxRXl6exo8fr71792ru3LmaOnXqXcdLSkq0e/du+fr6ytvb205VAsB/lpOToyFDhujDDz9U9+7dZTabVVZWpmPHjunZZ59VixYtdPLkSQ0cOFDPPvustmzZYu+SgYeC0A2HVVBQoF69eqmiokIvvfSSRo4cqZdfflmTJk1Senq6Jk2apPHjx9/V6b5w4YLKysrk6+sricANoG44e/asIiMjZTKZNHPmTPn7+0vSPat5AMCRpaena+LEiTp37py++uorbdmyRampqbpw4YL69etne6LM2bNn1a5dO6aUo8EgjcBhtWnTRiEhIbJYLHJ1dVV8fLx27Nih2NhY9e3bVxs3btTq1atlsVhs72nbtq0tcFutVgI3gDqhQ4cOWrp0qaxWq+bNm2fb403gBuCoampq7nmta9euKikpUZcuXRQYGKgrV65ozpw5ys3NVVZWlo4ePSrp1m8eU8rRkDCpAA6pqqpKLi4uioyM1KVLl9S1a1ft3btXCxYskNFo1PLlyzVu3DglJydLksaMGXPP4A32PQKoS7y9vbVkyRJNmTJFU6dOVWxsrLp162bvsgDgHneuJMzNzdWjjz4qNzc3dejQQbm5udqwYYO6deumnj17ysPDQ1VVVfLz87vnXo1ONxoK2oBwKGfOnJEkubi4SJI8PT1148YNlZeXa9WqVWrVqpXef/99ffbZZ1qxYoV8fHy0ePFiff755/YsGwAeCG9vby1atEht2rRR69at7V0OAPyk24F7+vTpCg4OVp8+ffTGG2/o448/VseOHfXHP/5RAwYMkJubm0pKShQcHCyLxaJ+/frZuXLAPtjTDYcxb948xcfHKygoSDNmzFCLFi3UqlUrHTx4UKGhocrIyFCjRo00ffp0lZSUaPr06erTp4+Sk5M1atQoe5cPAA/M7dU+AOBI7nwazMGDBzVkyBAlJyfr22+/VXZ2tj799FPNnz9fw4YNk9ls1oYNG7Rm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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_boxplot_median(results, names)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Scalable and Accurate Subsequence Transform (SAST)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": false, - "pycharm": { - "is_executing": true - } - }, - "outputs": [], - "source": [ - "\n", - "from sklearn.ensemble import RandomForestClassifier\n", - "\n", - "from aeon.datasets import load_basic_motions,load_classification\n", - "\n", - "from aeon.transformations.collection.shapelet_based import SAST\n" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Shape of transformed data = (20, 504)\n", - " Distance of second series to third shapelet = 0.0004470423\n", - " Shapelets + random forest acc = 1.0\n" - ] - } - ], - "source": [ - "#X, y = load_basic_motions(split=\"train\")\n", - "X, y = load_classification(name=\"Chinatown\",split=\"train\")\n", - "sast = SAST(lengths=None,\n", - " stride=1,\n", - " nb_inst_per_class=1,\n", - " seed=42,\n", - " n_jobs=-1)\n", - "st = sast.fit_transform(X, y)\n", - "print(\" Shape of transformed data = \", st.shape)\n", - "print(\" Distance of second series to third shapelet = \", st[1][2])\n", - "#testX, testy = load_basic_motions(split=\"test\")\n", - "testX, testy = load_classification(name=\"Chinatown\",split=\"train\")\n", - "tr_test = sast.transform(testX)\n", - "rf = RandomForestClassifier(random_state=10)\n", - "rf.fit(st, y)\n", - "preds = rf.predict(tr_test)\n", - "print(\" Shapelets + random forest acc = \", accuracy_score(preds, testy))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Random Scalable and Accurate Subsequence Transform (RSAST)" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "sys.path.append(r'C:\\Users\\nicol\\aeon')" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false, - "pycharm": { - "is_executing": true - } - }, - "outputs": [], - "source": [ - "from sklearn.ensemble import RandomForestClassifier\n", - "\n", - "from aeon.datasets import load_basic_motions,load_classification\n", - "\n", - "from aeon.transformations.collection.shapelet_based import RSAST\n", - "\n", - "from sklearn.metrics import accuracy_score" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "total kernels:570\n", - " Shape of transformed data = (20, 570)\n", - " Distance of second series to third shapelet = 0.16883065\n", - " Shapelets + random forest acc = 1.0\n" - ] - } - ], - "source": [ - "#X, y = load_basic_motions(split=\"train\")\n", - "X, y = load_classification(name=\"Chinatown\",split=\"train\")\n", - "rsast = RSAST(n_random_points=10,\n", - " len_method=\"both\",\n", - " nb_inst_per_class=10,\n", - " seed=None,\n", - " n_jobs=-1)\n", - "st = rsast.fit_transform(X, y)\n", - "print(\" Shape of transformed data = \", st.shape)\n", - "print(\" Distance of second series to third shapelet = \", st[1][2])\n", - "#testX, testy = load_basic_motions(split=\"test\")\n", - "testX, testy = load_classification(name=\"Chinatown\",split=\"train\")\n", - "tr_test = rsast.transform(testX)\n", - "rf = RandomForestClassifier(random_state=10)\n", - "rf.fit(st, y)\n", - "preds = rf.predict(tr_test)\n", - "print(\" Shapelets + random forest acc = \", accuracy_score(preds, testy))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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.12.2" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} From cf6582dddf8ab2df7fb2338344d66427c1557f23 Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sun, 7 Apr 2024 13:47:43 +0200 Subject: [PATCH 06/38] updated init --- aeon/classification/shapelet_based/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/classification/shapelet_based/__init__.py b/aeon/classification/shapelet_based/__init__.py index f810ae0259..afd5fff3fa 100644 --- a/aeon/classification/shapelet_based/__init__.py +++ b/aeon/classification/shapelet_based/__init__.py @@ -5,7 +5,7 @@ "ShapeletTransformClassifier", "RDSTClassifier", "SASTClassifier", - "RSASTClassifier", + "RSASTClassifier" ] from aeon.classification.shapelet_based._mrsqm import MrSQMClassifier From f5eab2d73037939567e081401bdf6efeb5601661 Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sun, 7 Apr 2024 15:47:24 +0200 Subject: [PATCH 07/38] included LearningShapeletClassifier --- aeon/classification/shapelet_based/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aeon/classification/shapelet_based/__init__.py b/aeon/classification/shapelet_based/__init__.py index afd5fff3fa..2c24102207 100644 --- a/aeon/classification/shapelet_based/__init__.py +++ b/aeon/classification/shapelet_based/__init__.py @@ -5,9 +5,10 @@ "ShapeletTransformClassifier", "RDSTClassifier", "SASTClassifier", - "RSASTClassifier" + "RSASTClassifier", + "LearningShapeletClassifier", ] - +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._sast_classifier import SASTClassifier From 2ec5ab398d5ae7da13eaa70166e988eaa47439d1 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 10:25:54 +0200 Subject: [PATCH 08/38] updated format comments --- .../shapelet_based/_rsast_classifier.py | 8 +- .../collection/shapelet_based/__init__.py | 2 +- .../collection/shapelet_based/_rsast.py | 86 +++++-------------- 3 files changed, 27 insertions(+), 69 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index b374b4053a..61a12119ba 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -26,7 +26,9 @@ class RSASTClassifier(BaseClassifier): Parameters ---------- n_random_points: int default = 10 the number of initial random points to extract - len_method: string default="both" the type of statistical tool used to get the length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS + len_method: string default="both" the type of statistical tool used to get the + length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, + "None"=Extract randomly any length from the TS nb_inst_per_class : int default = 10 the number of reference time series to select per class seed : int, default = None @@ -154,7 +156,7 @@ def _predict_proba(self, X): dists[i, np.where(self.classes_ == preds[i])] = 1 return dists - def plot_most_important_feature_on_ts(self, ts,feature_importance, limit=5): + def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): """Plot the most important features on ts. @@ -196,5 +198,5 @@ def plot_most_important_feature_on_ts(self, ts,feature_importance, limit=5): axes[f].plot(range(ts.size), ts, linewidth=2) axes[f].set_title(f"feature: {f+1}") - #return fig + diff --git a/aeon/transformations/collection/shapelet_based/__init__.py b/aeon/transformations/collection/shapelet_based/__init__.py index 5b134c2c56..e7851a5dbe 100644 --- a/aeon/transformations/collection/shapelet_based/__init__.py +++ b/aeon/transformations/collection/shapelet_based/__init__.py @@ -1,6 +1,6 @@ """Shapelet based transformers.""" -__all__ = ["RandomShapeletTransform", "RandomDilatedShapeletTransform", "SAST", "RSAST" ] +__all__ = ["RandomShapeletTransform", "RandomDilatedShapeletTransform", "SAST", "RSAST"] from aeon.transformations.collection.shapelet_based._dilated_shapelet_transform import ( RandomDilatedShapeletTransform, diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 2ee2b343e0..53611d9eb0 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -127,7 +127,7 @@ def _fit(self, X, y): """ - #0- initialize variables and convert values in "y" to string + # 0- initialize variables and convert values in "y" to string X_ = np.reshape(X, (X.shape[0], X.shape[-1])) self._random_state = ( @@ -139,58 +139,41 @@ def _fit(self, X, y): classes = np.unique(y) self._num_classes = classes.shape[0] - y = np.asarray([str(x_s) for x_s in y]) - - n = [] classes = np.unique(y) self.num_classes = classes.shape[0] m_kernel = 0 - #1--calculate ANOVA per each time t throught the lenght of the TS + # 1--calculate ANOVA per each time t throught the lenght of the TS for i in range (X_.shape[1]): statistic_per_class= {} for c in classes: assert len(X_[np.where(y==c)[0]][:,i])> 0, 'Time t without values in TS' - statistic_per_class[c]=X_[np.where(y==c)[0]][:,i] - #print("statistic_per_class- i:"+str(i)+', c:'+str(c)) - #print(statistic_per_class[c].shape) - - - #print('Without pd series') - #print(statistic_per_class) statistic_per_class=pd.Series(statistic_per_class) - #statistic_per_class = list(statistic_per_class.values()) # Calculate t-statistic and p-value - try: t_statistic, p_value = f_oneway(*statistic_per_class) except DegenerateDataWarning or ConstantInputWarning: p_value = np.nan - - #print('statistic_per_class', str(statistic_per_class)) + # Interpretation of the results # if p_value < 0.05: " The means of the populations are significantly different." - #print('pvalue', str(p_value)) if np.isnan(p_value): n.append(0) else: n.append(1-p_value) - - - - #2--calculate PACF and ACF for each TS chossen in each class + # 2--calculate PACF and ACF for each TS chossen in each class for i, c in enumerate(classes): X_c = X_[y == c] cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) - #set if the selection of instances is with replacement (if false it is not posible to select the same intance more than one) + # set if the selection of instances is with replacement (if false it is not posible to select the same intance more than one) choosen = self._random_state.permutation(X_c.shape[0])[:cnt] @@ -200,13 +183,13 @@ def _fit(self, X, y): self._cand_length_list[c+","+str(idx)+","+str(rep)] = [] non_zero_acf=[] if (self.len_method == "both" or self.len_method == "ACF" or self.len_method == "Max ACF") : - #2.1-- Compute Autorrelation per object + # 2.1-- Compute Autorrelation per object acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) prev_acf=0 for j, conf in enumerate(acf_confint): if(3<=j and (0 < acf_confint[j][0] <= acf_confint[j][1] or acf_confint[j][0] <= acf_confint[j][1] < 0) ): - #Consider just the maximum ACF value + # Consider just the maximum ACF value if prev_acf!=0 and self.len_method == "Max ACF": non_zero_acf.remove(prev_acf) self._cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_acf) @@ -216,13 +199,13 @@ def _fit(self, X, y): non_zero_pacf=[] if (self.len_method == "both" or self.len_method == "PACF" or self.len_method == "Max PACF"): - #2.2 Compute Partial Autorrelation per object + # 2.2 Compute Partial Autorrelation per object pacf_val, pacf_confint = pacf(X_c[idx], method="ols", nlags=(len(X_c[idx])//2) - 1, alpha=.05) prev_pacf=0 for j, conf in enumerate(pacf_confint): if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0) ): - #Consider just the maximum PACF value + # Consider just the maximum PACF value if prev_pacf!=0 and self.len_method == "Max PACF": non_zero_pacf.remove(prev_pacf) self._cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_pacf) @@ -234,72 +217,47 @@ def _fit(self, X, y): if (self.len_method == "all"): self._cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3,1+ len(X_c[idx]))) - #2.3-- Save the maximum autocorralated lag value as shapelet lenght - + # 2.3-- Save the maximum autocorralated lag value as shapelet lenght if len(self._cand_length_list[c+","+str(idx)+","+str(rep)])==0: - #chose a random lenght using the lenght of the time series (added 1 since the range start in 0) + # chose a random lenght using the lenght of the time series (added 1 since the range start in 0) rand_value= self._random_state.choice(len(X_c[idx]), 1)[0]+1 self._cand_length_list[c+","+str(idx)+","+str(rep)].extend([max(3,rand_value)]) - #elif len(non_zero_acf)==0: - #print("There is no AC in TS", idx, " of class ",c) - #elif len(non_zero_pacf)==0: - #print("There is no PAC in TS", idx, " of class ",c) - #else: - #print("There is AC and PAC in TS", idx, " of class ",c) - - #print("Kernel lenght list:",self.cand_length_list[c+","+str(idx)],"") - - #remove duplicates for the list of lenghts + self._cand_length_list[c+","+str(idx)+","+str(rep)]=list(set(self._cand_length_list[c+","+str(idx)+","+str(rep)])) - #print("Len list:"+str(self.cand_length_list[c+","+str(idx)+","+str(rep)])) + for max_shp_length in self._cand_length_list[c+","+str(idx)+","+str(rep)]: - - #2.4-- Choose randomly n_random_points point for a TS - #2.5-- calculate the weights of probabilities for a random point in a TS + # 2.4-- Choose randomly n_random_points point for a TS + # 2.5-- calculate the weights of probabilities for a random point in a TS if sum(n) == 0 : # Determine equal weights of a random point point in TS is there are no significant points - # print('All p values in One way ANOVA are equal to 0') weights = [1/len(n) for i in range(len(n))] weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) else: # Determine the weights of a random point point in TS (excluding points after n-l+1) weights = n / np.sum(n) weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) - - if self.n_random_points > len(X_c[idx])-max_shp_length+1 : - #set a upper limit for the posible of number of random points when selecting without replacement + # set a upper limit for the posible of number of random points when selecting without replacement limit_rpoint=len(X_c[idx])-max_shp_length+1 rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p=weights, replace=False) - #print("limit_rpoint:"+str(limit_rpoint)) + else: rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p=weights, replace=False) - - - - + for i in rand_point_ts: - #2.6-- Extract the subsequence with that point + # 2.6-- Extract the subsequence with that point kernel = X_c[idx][i:i+max_shp_length].reshape(1,-1).copy() - #print("kernel:"+str(kernel)) + if m_kernel < max_shp_length: m_kernel = max_shp_length self._kernel_orig.append(np.squeeze(kernel)) self._kernels_generators[c].extend(X_c[idx].reshape(1,-1)) - - - - - - - #3--save the calculated subsequences - + # 3--save the calculated subsequences n_kernels = len (self._kernel_orig) - self._kernels = np.full( (n_kernels, m_kernel), dtype=np.float32, fill_value=np.nan) @@ -310,7 +268,6 @@ def _fit(self, X, y): def _transform(self, X, y=None): """Transform the input X using the generated subsequences. - Parameters ---------- X: np.ndarray shape (n_cases, n_channels, n_timepoints) @@ -331,7 +288,6 @@ def _transform(self, X, y=None): set_num_threads(n_jobs) - X_transformed = _apply_kernels(X_, self._kernels) # subsequence transform of X set_num_threads(prev_threads) From c08038be5ef1d9fb7e6cd2040b35de0a6bab86a1 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 11:02:53 +0200 Subject: [PATCH 09/38] corrected spaces --- .../shapelet_based/_rsast_classifier.py | 20 +++---- .../collection/shapelet_based/_rsast.py | 55 ++++++++++--------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 61a12119ba..62d06b8359 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -41,7 +41,8 @@ class RSASTClassifier(BaseClassifier): Reference --------- - .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for Time Series Classification. + .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling + Shapelets for Time Series Classification. https://hal.science/hal-04311309/ Examples @@ -64,12 +65,12 @@ class RSASTClassifier(BaseClassifier): def __init__( self, - n_random_points=10, - len_method="both", - nb_inst_per_class=10, - seed=None, - classifier=None, - n_jobs=-1, + n_random_points = 10, + len_method = "both", + nb_inst_per_class = 10, + seed = None, + classifier = None, + n_jobs = -1, ): super().__init__() self.n_random_points = n_random_points @@ -97,7 +98,7 @@ def _fit(self, X, y): """ self._transformer = RSAST( self.n_random_points, - self.len_method, + self.len_method, self.nb_inst_per_class, self.seed, self.n_jobs, @@ -197,6 +198,3 @@ def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): axes[f].plot(range(start_pos, start_pos + kernel.size), kernel, linewidth=5) axes[f].plot(range(ts.size), ts, linewidth=2) axes[f].set_title(f"feature: {f+1}") - - - diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 53611d9eb0..c729aff96d 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -40,19 +40,23 @@ def _apply_kernels(X, kernels): class RSAST(BaseCollectionTransformer): """Random Scalable and Accurate Subsequence Transform (SAST). - RSAST [1] is based on SAST, it uses a stratified sampling strategy for subsequences selection but additionally takes into account certain - statistical criteria such as ANOVA, ACF, and PACF to further reduce the search space of shapelets. + RSAST [1] is based on SAST, it uses a stratified sampling strategy for subsequences selection but + additionally takes into account certain statistical criteria such as ANOVA, ACF, and PACF to + further reduce the search space of shapelets. - RSAST starts with the pre-computation of a list of weights, using ANOVA, which helps in the selection of initial points for - subsequences. Then randomly select k time series per class, which are used with an ACF and PACF, obtaining a set of highly correlated - lagged values. These values are used as potential lengths for the shapelets. Lastly, with a pre-defined number of admissible starting - points to sample, the shapelets are extracted and used to transform the original dataset, replacing each time series by the vector of - its distance to each subsequence. + RSAST starts with the pre-computation of a list of weights, using ANOVA, which helps in the + selection of initial points for subsequences. Then randomly select k time series per class, + which are used with an ACF and PACF, obtaining a set of highly correlated lagged values. + These values are used as potential lengths for the shapelets. Lastly, with a pre-defined + number of admissible starting points to sample, the shapelets are extracted and used to + transform the original dataset, replacing each time series by the vector of its distance + to each subsequence. Parameters ---------- n_random_points: int default = 10 the number of initial random points to extract - len_method: string default="both" the type of statistical tool used to get the length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS + len_method: string default="both" the type of statistical tool used to get the length of + shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS nb_inst_per_class : int default = 10 the number of reference time series to select per class seed : int, default = None @@ -64,7 +68,8 @@ class RSAST(BaseCollectionTransformer): Reference --------- - .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for Time Series Classification. + .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for + Time Series Classification. https://hal.science/hal-04311309/ @@ -108,8 +113,6 @@ def __init__( super().__init__() - - def _fit(self, X, y): """Select reference time series and generate subsequences from them. @@ -148,10 +151,10 @@ def _fit(self, X, y): # 1--calculate ANOVA per each time t throught the lenght of the TS for i in range (X_.shape[1]): - statistic_per_class= {} + statistic_per_class = {} for c in classes: - assert len(X_[np.where(y==c)[0]][:,i])> 0, 'Time t without values in TS' - statistic_per_class[c]=X_[np.where(y==c)[0]][:,i] + assert len(X_[np.where(y == c)[0]][:,i]) > 0, 'Time t without values in TS' + statistic_per_class[c] = X_[np.where(y == c)[0]][:,i] statistic_per_class=pd.Series(statistic_per_class) # Calculate t-statistic and p-value @@ -181,7 +184,7 @@ def _fit(self, X, y): for rep, idx in enumerate(choosen): self._cand_length_list[c+","+str(idx)+","+str(rep)] = [] - non_zero_acf=[] + non_zero_acf = [] if (self.len_method == "both" or self.len_method == "ACF" or self.len_method == "Max ACF") : # 2.1-- Compute Autorrelation per object acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) @@ -197,11 +200,11 @@ def _fit(self, X, y): self._cand_length_list[c+","+str(idx)+","+str(rep)].append(j) prev_acf=j - non_zero_pacf=[] + non_zero_pacf = [] if (self.len_method == "both" or self.len_method == "PACF" or self.len_method == "Max PACF"): # 2.2 Compute Partial Autorrelation per object - pacf_val, pacf_confint = pacf(X_c[idx], method="ols", nlags=(len(X_c[idx])//2) - 1, alpha=.05) - prev_pacf=0 + pacf_val, pacf_confint = pacf(X_c[idx], method = "ols", nlags=(len(X_c[idx])//2) - 1, alpha = .05) + prev_pacf = 0 for j, conf in enumerate(pacf_confint): if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0) ): @@ -218,12 +221,12 @@ def _fit(self, X, y): self._cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3,1+ len(X_c[idx]))) # 2.3-- Save the maximum autocorralated lag value as shapelet lenght - if len(self._cand_length_list[c+","+str(idx)+","+str(rep)])==0: + if len(self._cand_length_list[c+","+str(idx)+","+str(rep)]) == 0: # chose a random lenght using the lenght of the time series (added 1 since the range start in 0) - rand_value= self._random_state.choice(len(X_c[idx]), 1)[0]+1 + rand_value = self._random_state.choice(len(X_c[idx]), 1)[0]+1 self._cand_length_list[c+","+str(idx)+","+str(rep)].extend([max(3,rand_value)]) - self._cand_length_list[c+","+str(idx)+","+str(rep)]=list(set(self._cand_length_list[c+","+str(idx)+","+str(rep)])) + self._cand_length_list[c+","+str(idx)+","+str(rep)] = list(set(self._cand_length_list[c+","+str(idx)+","+str(rep)])) for max_shp_length in self._cand_length_list[c+","+str(idx)+","+str(rep)]: # 2.4-- Choose randomly n_random_points point for a TS @@ -239,11 +242,11 @@ def _fit(self, X, y): if self.n_random_points > len(X_c[idx])-max_shp_length+1 : # set a upper limit for the posible of number of random points when selecting without replacement - limit_rpoint=len(X_c[idx])-max_shp_length+1 - rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p=weights, replace=False) + limit_rpoint = len(X_c[idx])-max_shp_length+1 + rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p = weights, replace = False) else: - rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p=weights, replace=False) + rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p = weights, replace = False) for i in rand_point_ts: # 2.6-- Extract the subsequence with that point @@ -259,14 +262,14 @@ def _fit(self, X, y): n_kernels = len (self._kernel_orig) self._kernels = np.full( - (n_kernels, m_kernel), dtype=np.float32, fill_value=np.nan) + (n_kernels, m_kernel), dtype = np.float32, fill_value = np.nan) for k, kernel in enumerate(self._kernel_orig): self._kernels[k, :len(kernel)] = z_normalise_series(kernel) return self - def _transform(self, X, y=None): + def _transform(self, X, y = None): """Transform the input X using the generated subsequences. Parameters ---------- From 8728d92fb2f25049e44f41b86ef023723bd2abdf Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 13:26:14 +0200 Subject: [PATCH 10/38] corrected identation --- .../shapelet_based/_rsast_classifier.py | 12 ++-- .../collection/shapelet_based/_rsast.py | 56 ++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 62d06b8359..2bbaef9eae 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -65,12 +65,12 @@ class RSASTClassifier(BaseClassifier): def __init__( self, - n_random_points = 10, - len_method = "both", - nb_inst_per_class = 10, - seed = None, - classifier = None, - n_jobs = -1, + n_random_points=10, + len_method="both", + nb_inst_per_class=10, + seed=None, + classifier=None, + n_jobs=-1, ): super().__init__() self.n_random_points = n_random_points diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index c729aff96d..2eddbcdd21 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -40,23 +40,27 @@ def _apply_kernels(X, kernels): class RSAST(BaseCollectionTransformer): """Random Scalable and Accurate Subsequence Transform (SAST). - RSAST [1] is based on SAST, it uses a stratified sampling strategy for subsequences selection but - additionally takes into account certain statistical criteria such as ANOVA, ACF, and PACF to - further reduce the search space of shapelets. + RSAST [1] is based on SAST, it uses a stratified sampling strategy + for subsequences selection but additionally takes into account certain + statistical criteria such as ANOVA, ACF, and PACF to further reduce + the search space of shapelets. - RSAST starts with the pre-computation of a list of weights, using ANOVA, which helps in the - selection of initial points for subsequences. Then randomly select k time series per class, - which are used with an ACF and PACF, obtaining a set of highly correlated lagged values. - These values are used as potential lengths for the shapelets. Lastly, with a pre-defined - number of admissible starting points to sample, the shapelets are extracted and used to - transform the original dataset, replacing each time series by the vector of its distance - to each subsequence. + RSAST starts with the pre-computation of a list of weights, using ANOVA, + which helps in the selection of initial points for subsequences. Then + randomly select k time series per class, which are used with an ACF and PACF, + obtaining a set of highly correlated lagged values. These values are used as + potential lengths for the shapelets. Lastly, with a pre-defined number of + admissible starting points to sample, the shapelets are extracted and used to + transform the original dataset, replacing each time series by the vector of its + distance to each subsequence. Parameters ---------- n_random_points: int default = 10 the number of initial random points to extract - len_method: string default="both" the type of statistical tool used to get the length of - shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS + len_method: string default="both" the type of statistical tool used to get + the length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, + "None"=Extract randomly any length from the TS + nb_inst_per_class : int default = 10 the number of reference time series to select per class seed : int, default = None @@ -68,8 +72,8 @@ class RSAST(BaseCollectionTransformer): Reference --------- - .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for - Time Series Classification. + .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). + RSAST: Sampling Shapelets for Time Series Classification. https://hal.science/hal-04311309/ @@ -95,11 +99,11 @@ class RSAST(BaseCollectionTransformer): def __init__( self, - n_random_points = 10, - len_method = "both", - nb_inst_per_class = 10, - seed = None, - n_jobs = -1, + n_random_points=10, + len_method="both", + nb_inst_per_class=10, + seed=None, + n_jobs=-1, ): self.n_random_points = n_random_points self.len_method = len_method @@ -130,7 +134,7 @@ def _fit(self, X, y): """ - # 0- initialize variables and convert values in "y" to string + # 0- initialize variables and convert values in "y" to string X_ = np.reshape(X, (X.shape[0], X.shape[-1])) self._random_state = ( @@ -150,13 +154,15 @@ def _fit(self, X, y): m_kernel = 0 # 1--calculate ANOVA per each time t throught the lenght of the TS - for i in range (X_.shape[1]): + for i in range(X_.shape[1]): statistic_per_class = {} for c in classes: - assert len(X_[np.where(y == c)[0]][:,i]) > 0, 'Time t without values in TS' - statistic_per_class[c] = X_[np.where(y == c)[0]][:,i] + assert len( + X_[np.where(y == c)[0]][:, i] + ) > 0, 'Time t without values in TS' + statistic_per_class[c] = X_[np.where(y == c)[0]][:, i] - statistic_per_class=pd.Series(statistic_per_class) + statistic_per_class = pd.Series(statistic_per_class) # Calculate t-statistic and p-value try: t_statistic, p_value = f_oneway(*statistic_per_class) @@ -218,7 +224,7 @@ def _fit(self, X, y): prev_pacf=j if (self.len_method == "all"): - self._cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3,1+ len(X_c[idx]))) + self._cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3, 1+ len(X_c[idx]))) # 2.3-- Save the maximum autocorralated lag value as shapelet lenght if len(self._cand_length_list[c+","+str(idx)+","+str(rep)]) == 0: From 1dc790348c2291458c4610a2604f5e79368b7ad7 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 13:49:53 +0200 Subject: [PATCH 11/38] updated identation --- .../collection/shapelet_based/_rsast.py | 93 +++++++++++++------ 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 2eddbcdd21..dee23146b7 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -166,11 +166,12 @@ def _fit(self, X, y): # Calculate t-statistic and p-value try: t_statistic, p_value = f_oneway(*statistic_per_class) - except DegenerateDataWarning or ConstantInputWarning: + except (DegenerateDataWarning, ConstantInputWarning): p_value = np.nan # Interpretation of the results - # if p_value < 0.05: " The means of the populations are significantly different." + # if p_value < 0.05: " The means of the populations are + # significantly different." if np.isnan(p_value): n.append(0) else: @@ -182,7 +183,6 @@ def _fit(self, X, y): X_c = X_[y == c] cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) - # set if the selection of instances is with replacement (if false it is not posible to select the same intance more than one) choosen = self._random_state.permutation(X_c.shape[0])[:cnt] @@ -191,78 +191,113 @@ def _fit(self, X, y): for rep, idx in enumerate(choosen): self._cand_length_list[c+","+str(idx)+","+str(rep)] = [] non_zero_acf = [] - if (self.len_method == "both" or self.len_method == "ACF" or self.len_method == "Max ACF") : + if (self.len_method == "both" or + self.len_method == "ACF" or + self.len_method == "Max ACF"): # 2.1-- Compute Autorrelation per object - acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) + acf_val, acf_confint = acf(X_c[idx], + nlags=len(X_c[idx])-1, alpha=.05) prev_acf=0 for j, conf in enumerate(acf_confint): - if(3<=j and (0 < acf_confint[j][0] <= acf_confint[j][1] or acf_confint[j][0] <= acf_confint[j][1] < 0) ): + if(3<=j and (0 < acf_confint[j][0] <= acf_confint[j][1] or + acf_confint[j][0] <= acf_confint[j][1] < 0) ): # Consider just the maximum ACF value if prev_acf!=0 and self.len_method == "Max ACF": non_zero_acf.remove(prev_acf) - self._cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_acf) + self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ].remove(prev_acf) non_zero_acf.append(j) - self._cand_length_list[c+","+str(idx)+","+str(rep)].append(j) + self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ].append(j) prev_acf=j non_zero_pacf = [] - if (self.len_method == "both" or self.len_method == "PACF" or self.len_method == "Max PACF"): + if (self.len_method == "both" or + self.len_method == "PACF" or self.len_method == "Max PACF"): # 2.2 Compute Partial Autorrelation per object - pacf_val, pacf_confint = pacf(X_c[idx], method = "ols", nlags=(len(X_c[idx])//2) - 1, alpha = .05) + pacf_val, pacf_confint = pacf(X_c[idx], method = "ols", + nlags=(len(X_c[idx])//2) - 1, + alpha = .05) prev_pacf = 0 for j, conf in enumerate(pacf_confint): - if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0) ): + if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or + pacf_confint[j][0] <= pacf_confint[j][1] < 0) ): # Consider just the maximum PACF value if prev_pacf!=0 and self.len_method == "Max PACF": non_zero_pacf.remove(prev_pacf) - self._cand_length_list[c+","+str(idx)+","+str(rep)].remove(prev_pacf) + self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ].remove(prev_pacf) non_zero_pacf.append(j) - self._cand_length_list[c+","+str(idx)+","+str(rep)].append(j) + self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ].append(j) prev_pacf=j if (self.len_method == "all"): - self._cand_length_list[c+","+str(idx)+","+str(rep)].extend(np.arange(3, 1+ len(X_c[idx]))) + self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ].extend(np.arange(3, 1+ len(X_c[idx]))) - # 2.3-- Save the maximum autocorralated lag value as shapelet lenght + # 2.3-- Save the maximum autocorralated lag value as shapelet lenght if len(self._cand_length_list[c+","+str(idx)+","+str(rep)]) == 0: - # chose a random lenght using the lenght of the time series (added 1 since the range start in 0) + # chose a random lenght using the lenght of the time series + # (added 1 since the range start in 0) rand_value = self._random_state.choice(len(X_c[idx]), 1)[0]+1 - self._cand_length_list[c+","+str(idx)+","+str(rep)].extend([max(3,rand_value)]) + self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ].extend([max(3, rand_value)]) - self._cand_length_list[c+","+str(idx)+","+str(rep)] = list(set(self._cand_length_list[c+","+str(idx)+","+str(rep)])) + self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ] = list(set(self._cand_length_list[c+","+str(idx)+","+str(rep)])) - for max_shp_length in self._cand_length_list[c+","+str(idx)+","+str(rep)]: + for max_shp_length in self._cand_length_list[ + c+","+str(idx)+","+str(rep) + ]: # 2.4-- Choose randomly n_random_points point for a TS - # 2.5-- calculate the weights of probabilities for a random point in a TS + # 2.5-- calculate the weights of probabilities for a random point + # in a TS if sum(n) == 0 : - # Determine equal weights of a random point point in TS is there are no significant points + # Determine equal weights of a random point point in TS is + # there are no significant points weights = [1/len(n) for i in range(len(n))] - weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) + weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum( + weights[:len(X_c[idx])-max_shp_length+1]) else: - # Determine the weights of a random point point in TS (excluding points after n-l+1) + # Determine the weights of a random point point in TS + # (excluding points after n-l+1) weights = n / np.sum(n) - weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum(weights[:len(X_c[idx])-max_shp_length+1]) + weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum( + weights[:len(X_c[idx])-max_shp_length+1]) if self.n_random_points > len(X_c[idx])-max_shp_length+1 : - # set a upper limit for the posible of number of random points when selecting without replacement + # set a upper limit for the posible of number of random + # points when selecting without replacement limit_rpoint = len(X_c[idx])-max_shp_length+1 - rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, limit_rpoint, p = weights, replace = False) + rand_point_ts = self._random_state.choice( + len(X_c[idx])-max_shp_length+1, limit_rpoint, + p = weights, replace = False) else: - rand_point_ts = self._random_state.choice(len(X_c[idx])-max_shp_length+1, self.n_random_points, p = weights, replace = False) + rand_point_ts = self._random_state.choice( + len(X_c[idx])-max_shp_length+1, self.n_random_points, + p = weights, replace = False) for i in rand_point_ts: # 2.6-- Extract the subsequence with that point - kernel = X_c[idx][i:i+max_shp_length].reshape(1,-1).copy() + kernel = X_c[idx][i:i+max_shp_length].reshape(1, -1).copy() if m_kernel < max_shp_length: m_kernel = max_shp_length self._kernel_orig.append(np.squeeze(kernel)) - self._kernels_generators[c].extend(X_c[idx].reshape(1,-1)) + self._kernels_generators[c].extend(X_c[idx].reshape(1, -1)) # 3--save the calculated subsequences n_kernels = len (self._kernel_orig) From d9dfda537fd8c61f21508ffc8c75dcd4287cb467 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 14:16:17 +0200 Subject: [PATCH 12/38] updated identation --- .../collection/shapelet_based/_rsast.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index dee23146b7..abc766a1c7 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -103,8 +103,7 @@ def __init__( len_method="both", nb_inst_per_class=10, seed=None, - n_jobs=-1, - ): + n_jobs=-1,): self.n_random_points = n_random_points self.len_method = len_method self.nb_inst_per_class = nb_inst_per_class @@ -140,8 +139,7 @@ def _fit(self, X, y): self._random_state = ( np.random.RandomState(self.seed) if not isinstance(self.seed, np.random.RandomState) - else self.seed - ) + else self.seed) classes = np.unique(y) self._num_classes = classes.shape[0] @@ -158,8 +156,9 @@ def _fit(self, X, y): statistic_per_class = {} for c in classes: assert len( - X_[np.where(y == c)[0]][:, i] - ) > 0, 'Time t without values in TS' + X_[ + np.where(y == c)[0] + ][:, i]) > 0, 'Time t without values in TS' statistic_per_class[c] = X_[np.where(y == c)[0]][:, i] statistic_per_class = pd.Series(statistic_per_class) @@ -191,19 +190,20 @@ def _fit(self, X, y): for rep, idx in enumerate(choosen): self._cand_length_list[c+","+str(idx)+","+str(rep)] = [] non_zero_acf = [] - if (self.len_method == "both" or - self.len_method == "ACF" or + + if (self.len_method == "both" or self.len_method == "ACF" or self.len_method == "Max ACF"): - # 2.1-- Compute Autorrelation per object + + # 2.1 -- Compute Autorrelation per object acf_val, acf_confint = acf(X_c[idx], - nlags=len(X_c[idx])-1, alpha=.05) - prev_acf=0 - for j, conf in enumerate(acf_confint): + nlags=len(X_c[idx])-1, alpha=.05) + prev_acf = 0 + for j in range(len(acf_confint)): - if(3<=j and (0 < acf_confint[j][0] <= acf_confint[j][1] or - acf_confint[j][0] <= acf_confint[j][1] < 0) ): + if(3 <= j and (0 < acf_confint[j][0] <= acf_confint[j][1] or + acf_confint[j][0] <= acf_confint[j][1] < 0)): # Consider just the maximum ACF value - if prev_acf!=0 and self.len_method == "Max ACF": + if prev_acf != 0 and self.len_method == "Max ACF": non_zero_acf.remove(prev_acf) self._cand_length_list[ c+","+str(idx)+","+str(rep) @@ -212,8 +212,8 @@ def _fit(self, X, y): self._cand_length_list[ c+","+str(idx)+","+str(rep) ].append(j) - prev_acf=j - + prev_acf = j + non_zero_pacf = [] if (self.len_method == "both" or self.len_method == "PACF" or self.len_method == "Max PACF"): @@ -222,12 +222,12 @@ def _fit(self, X, y): nlags=(len(X_c[idx])//2) - 1, alpha = .05) prev_pacf = 0 - for j, conf in enumerate(pacf_confint): + for j in range(len(pacf_confint)): if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or - pacf_confint[j][0] <= pacf_confint[j][1] < 0) ): + pacf_confint[j][0] <= pacf_confint[j][1] < 0)): # Consider just the maximum PACF value - if prev_pacf!=0 and self.len_method == "Max PACF": + if prev_pacf != 0 and self.len_method == "Max PACF": non_zero_pacf.remove(prev_pacf) self._cand_length_list[ c+","+str(idx)+","+str(rep) @@ -282,12 +282,12 @@ def _fit(self, X, y): limit_rpoint = len(X_c[idx])-max_shp_length+1 rand_point_ts = self._random_state.choice( len(X_c[idx])-max_shp_length+1, limit_rpoint, - p = weights, replace = False) + p=weights, replace=False) else: rand_point_ts = self._random_state.choice( len(X_c[idx])-max_shp_length+1, self.n_random_points, - p = weights, replace = False) + p=weights, replace=False) for i in rand_point_ts: # 2.6-- Extract the subsequence with that point @@ -300,17 +300,17 @@ def _fit(self, X, y): self._kernels_generators[c].extend(X_c[idx].reshape(1, -1)) # 3--save the calculated subsequences - n_kernels = len (self._kernel_orig) + n_kernels = len(self._kernel_orig) self._kernels = np.full( - (n_kernels, m_kernel), dtype = np.float32, fill_value = np.nan) + (n_kernels, m_kernel), dtype=np.float32, fill_value=np.nan) for k, kernel in enumerate(self._kernel_orig): self._kernels[k, :len(kernel)] = z_normalise_series(kernel) return self - def _transform(self, X, y = None): + def _transform(self, X, y=None): """Transform the input X using the generated subsequences. Parameters ---------- From 0c21b5f366ba02675dfd18b94a047cf18b0a7b43 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 14:45:58 +0200 Subject: [PATCH 13/38] corrected identation --- .../collection/shapelet_based/_rsast.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index abc766a1c7..75b7a31a1f 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -1,15 +1,12 @@ import numpy as np from numba import get_num_threads, njit, prange, set_num_threads - from aeon.transformations.collection import BaseCollectionTransformer from aeon.utils.numba.general import z_normalise_series from aeon.utils.validation import check_n_jobs - from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning from statsmodels.tsa.stattools import acf, pacf import pandas as pd - @njit(fastmath=False) def _apply_kernel(ts, arr): d_best = np.inf # sdist @@ -19,12 +16,10 @@ def _apply_kernel(ts, arr): kernel_len = kernel.shape[0] for i in range(m - kernel_len + 1): d = np.sum((z_normalise_series(ts[i : i + kernel_len]) - kernel) ** 2) - if d < d_best: + if d < d_best: d_best = d - return d_best - @njit(parallel=True, fastmath=True) def _apply_kernels(X, kernels): nbk = len(kernels) @@ -103,7 +98,8 @@ def __init__( len_method="both", nb_inst_per_class=10, seed=None, - n_jobs=-1,): + n_jobs=-1, + ): self.n_random_points = n_random_points self.len_method = len_method self.nb_inst_per_class = nb_inst_per_class @@ -113,7 +109,6 @@ def __init__( self._cand_length_list = {} self._kernel_orig = [] self._kernels_generators = {} # Reference time series - super().__init__() def _fit(self, X, y): @@ -191,10 +186,11 @@ def _fit(self, X, y): self._cand_length_list[c+","+str(idx)+","+str(rep)] = [] non_zero_acf = [] - if (self.len_method == "both" or self.len_method == "ACF" or - self.len_method == "Max ACF"): - - # 2.1 -- Compute Autorrelation per object + if ( + self.len_method == "both" or + self.len_method == "ACF" or self.len_method == "Max ACF" + ): + # 2.1 -- Compute Autorrelation per object acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) prev_acf = 0 @@ -218,12 +214,11 @@ def _fit(self, X, y): if (self.len_method == "both" or self.len_method == "PACF" or self.len_method == "Max PACF"): # 2.2 Compute Partial Autorrelation per object - pacf_val, pacf_confint = pacf(X_c[idx], method = "ols", + pacf_val, pacf_confint = pacf(X_c[idx], method="ols", nlags=(len(X_c[idx])//2) - 1, - alpha = .05) + alpha=.05) prev_pacf = 0 for j in range(len(pacf_confint)): - if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0)): # Consider just the maximum PACF value @@ -263,7 +258,7 @@ def _fit(self, X, y): # 2.4-- Choose randomly n_random_points point for a TS # 2.5-- calculate the weights of probabilities for a random point # in a TS - if sum(n) == 0 : + if sum(n) == 0: # Determine equal weights of a random point point in TS is # there are no significant points weights = [1/len(n) for i in range(len(n))] @@ -276,7 +271,7 @@ def _fit(self, X, y): weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum( weights[:len(X_c[idx])-max_shp_length+1]) - if self.n_random_points > len(X_c[idx])-max_shp_length+1 : + if self.n_random_points > len(X_c[idx])-max_shp_length+1: # set a upper limit for the posible of number of random # points when selecting without replacement limit_rpoint = len(X_c[idx])-max_shp_length+1 From 54dafa84a3f30510d2d38a1467323af54ca016f4 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 15:04:19 +0200 Subject: [PATCH 14/38] updated identation --- .../collection/shapelet_based/_rsast.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 75b7a31a1f..c601a4dae7 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -20,6 +20,7 @@ def _apply_kernel(ts, arr): d_best = d return d_best + @njit(parallel=True, fastmath=True) def _apply_kernels(X, kernels): nbk = len(kernels) @@ -183,39 +184,37 @@ def _fit(self, X, y): self._kernels_generators[c] = [] for rep, idx in enumerate(choosen): - self._cand_length_list[c+","+str(idx)+","+str(rep)] = [] + self._cand_length_list[c + "," + str(idx) + "," + str(rep)] = [] non_zero_acf = [] - if ( - self.len_method == "both" or - self.len_method == "ACF" or self.len_method == "Max ACF" - ): + if (self.len_method == "both" or + self.len_method == "ACF" or self.len_method == "Max ACF"): # 2.1 -- Compute Autorrelation per object acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) prev_acf = 0 for j in range(len(acf_confint)): - if(3 <= j and (0 < acf_confint[j][0] <= acf_confint[j][1] or acf_confint[j][0] <= acf_confint[j][1] < 0)): # Consider just the maximum ACF value if prev_acf != 0 and self.len_method == "Max ACF": non_zero_acf.remove(prev_acf) self._cand_length_list[ - c+","+str(idx)+","+str(rep) + c + "," + str(idx) + "," + str(rep) ].remove(prev_acf) non_zero_acf.append(j) self._cand_length_list[ - c+","+str(idx)+","+str(rep) + c + "," + str(idx) + "," + str(rep) ].append(j) prev_acf = j non_zero_pacf = [] + if (self.len_method == "both" or self.len_method == "PACF" or self.len_method == "Max PACF"): # 2.2 Compute Partial Autorrelation per object pacf_val, pacf_confint = pacf(X_c[idx], method="ols", - nlags=(len(X_c[idx])//2) - 1, + nlags=(len(X_c[idx]) // 2) - 1, alpha=.05) prev_pacf = 0 for j in range(len(pacf_confint)): @@ -225,35 +224,35 @@ def _fit(self, X, y): if prev_pacf != 0 and self.len_method == "Max PACF": non_zero_pacf.remove(prev_pacf) self._cand_length_list[ - c+","+str(idx)+","+str(rep) + c + "," + str(idx)+"," + str(rep) ].remove(prev_pacf) non_zero_pacf.append(j) self._cand_length_list[ - c+","+str(idx)+","+str(rep) + c + "," + str(idx) + "," + str(rep) ].append(j) - prev_pacf=j + prev_pacf = j if (self.len_method == "all"): self._cand_length_list[ - c+","+str(idx)+","+str(rep) - ].extend(np.arange(3, 1+ len(X_c[idx]))) + c + ","+str(idx) + "," + str(rep) + ].extend(np.arange(3, 1 + len(X_c[idx]))) # 2.3-- Save the maximum autocorralated lag value as shapelet lenght - if len(self._cand_length_list[c+","+str(idx)+","+str(rep)]) == 0: + if len(self._cand_length_list[c + "," + str(idx) + "," + str(rep)]) == 0: # chose a random lenght using the lenght of the time series # (added 1 since the range start in 0) - rand_value = self._random_state.choice(len(X_c[idx]), 1)[0]+1 + rand_value = self._random_state.choice(len(X_c[idx]), 1)[0] + 1 self._cand_length_list[ - c+","+str(idx)+","+str(rep) + c + "," + str(idx) + "," + str(rep) ].extend([max(3, rand_value)]) self._cand_length_list[ - c+","+str(idx)+","+str(rep) - ] = list(set(self._cand_length_list[c+","+str(idx)+","+str(rep)])) + c + "," + str(idx) + "," + str(rep) + ] = list(set(self._cand_length_list[c + "," + str(idx) + "," + str(rep)])) for max_shp_length in self._cand_length_list[ - c+","+str(idx)+","+str(rep) + c + ","+str(idx) + "," + str(rep) ]: # 2.4-- Choose randomly n_random_points point for a TS # 2.5-- calculate the weights of probabilities for a random point @@ -262,31 +261,30 @@ def _fit(self, X, y): # Determine equal weights of a random point point in TS is # there are no significant points weights = [1/len(n) for i in range(len(n))] - weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum( - weights[:len(X_c[idx])-max_shp_length+1]) + weights = weights[:len(X_c[idx]) - max_shp_length + 1]/np.sum( + weights[:len(X_c[idx]) - max_shp_length + 1]) else: # Determine the weights of a random point point in TS # (excluding points after n-l+1) weights = n / np.sum(n) - weights = weights[:len(X_c[idx])-max_shp_length +1]/np.sum( - weights[:len(X_c[idx])-max_shp_length+1]) + weights = weights[:len(X_c[idx]) - max_shp_length + 1]/np.sum( + weights[:len(X_c[idx]) - max_shp_length + 1]) if self.n_random_points > len(X_c[idx])-max_shp_length+1: # set a upper limit for the posible of number of random # points when selecting without replacement - limit_rpoint = len(X_c[idx])-max_shp_length+1 + limit_rpoint = len(X_c[idx]) - max_shp_length + 1 rand_point_ts = self._random_state.choice( - len(X_c[idx])-max_shp_length+1, limit_rpoint, + len(X_c[idx]) - max_shp_length + 1, limit_rpoint, p=weights, replace=False) - else: rand_point_ts = self._random_state.choice( - len(X_c[idx])-max_shp_length+1, self.n_random_points, + len(X_c[idx]) - max_shp_length + 1, self.n_random_points, p=weights, replace=False) for i in rand_point_ts: # 2.6-- Extract the subsequence with that point - kernel = X_c[idx][i:i+max_shp_length].reshape(1, -1).copy() + kernel = X_c[idx][i : i + max_shp_length].reshape(1, -1).copy() if m_kernel < max_shp_length: m_kernel = max_shp_length From a1d1ecea972522446e25c0a2e5f95f51ff063a71 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 15:12:49 +0200 Subject: [PATCH 15/38] updated identation --- .../collection/shapelet_based/_rsast.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index c601a4dae7..6b34d50654 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -187,14 +187,17 @@ def _fit(self, X, y): self._cand_length_list[c + "," + str(idx) + "," + str(rep)] = [] non_zero_acf = [] - if (self.len_method == "both" or - self.len_method == "ACF" or self.len_method == "Max ACF"): + if ( + self.len_method == "both" or + self.len_method == "ACF" or + self.len_method == "Max ACF" + ): # 2.1 -- Compute Autorrelation per object acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) prev_acf = 0 for j in range(len(acf_confint)): - if(3 <= j and (0 < acf_confint[j][0] <= acf_confint[j][1] or + if (3 <= j and (0 < acf_confint[j][0] <= acf_confint[j][1] or acf_confint[j][0] <= acf_confint[j][1] < 0)): # Consider just the maximum ACF value if prev_acf != 0 and self.len_method == "Max ACF": @@ -211,14 +214,16 @@ def _fit(self, X, y): non_zero_pacf = [] if (self.len_method == "both" or - self.len_method == "PACF" or self.len_method == "Max PACF"): + self.len_method == "PACF" or + self.len_method == "Max PACF" + ): # 2.2 Compute Partial Autorrelation per object pacf_val, pacf_confint = pacf(X_c[idx], method="ols", nlags=(len(X_c[idx]) // 2) - 1, alpha=.05) prev_pacf = 0 for j in range(len(pacf_confint)): - if(3<=j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or + if (3 <= j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0)): # Consider just the maximum PACF value if prev_pacf != 0 and self.len_method == "Max PACF": @@ -239,7 +244,8 @@ def _fit(self, X, y): ].extend(np.arange(3, 1 + len(X_c[idx]))) # 2.3-- Save the maximum autocorralated lag value as shapelet lenght - if len(self._cand_length_list[c + "," + str(idx) + "," + str(rep)]) == 0: + if len(self._cand_length_list[ + c + "," + str(idx) + "," + str(rep)]) == 0: # chose a random lenght using the lenght of the time series # (added 1 since the range start in 0) rand_value = self._random_state.choice(len(X_c[idx]), 1)[0] + 1 @@ -249,7 +255,8 @@ def _fit(self, X, y): self._cand_length_list[ c + "," + str(idx) + "," + str(rep) - ] = list(set(self._cand_length_list[c + "," + str(idx) + "," + str(rep)])) + ] = list(set(self._cand_length_list[ + c + "," + str(idx) + "," + str(rep)])) for max_shp_length in self._cand_length_list[ c + ","+str(idx) + "," + str(rep) From 7bc3df10a07ea07189bb2d60958ac984baacf52a Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 15:19:25 +0200 Subject: [PATCH 16/38] updated identation --- .../collection/shapelet_based/_rsast.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 6b34d50654..498b19d35c 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -197,8 +197,10 @@ def _fit(self, X, y): nlags=len(X_c[idx])-1, alpha=.05) prev_acf = 0 for j in range(len(acf_confint)): - if (3 <= j and (0 < acf_confint[j][0] <= acf_confint[j][1] or - acf_confint[j][0] <= acf_confint[j][1] < 0)): + if ( + 3 <= j and + (0 < acf_confint[j][0] <= acf_confint[j][1] or + acf_confint[j][0] <= acf_confint[j][1] < 0)): # Consider just the maximum ACF value if prev_acf != 0 and self.len_method == "Max ACF": non_zero_acf.remove(prev_acf) @@ -215,16 +217,17 @@ def _fit(self, X, y): if (self.len_method == "both" or self.len_method == "PACF" or - self.len_method == "Max PACF" - ): + self.len_method == "Max PACF"): # 2.2 Compute Partial Autorrelation per object pacf_val, pacf_confint = pacf(X_c[idx], method="ols", nlags=(len(X_c[idx]) // 2) - 1, alpha=.05) prev_pacf = 0 for j in range(len(pacf_confint)): - if (3 <= j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or - pacf_confint[j][0] <= pacf_confint[j][1] < 0)): + if ( + 3 <= j and + (0 < pacf_confint[j][0] <= pacf_confint[j][1] or + pacf_confint[j][0] <= pacf_confint[j][1] < 0)): # Consider just the maximum PACF value if prev_pacf != 0 and self.len_method == "Max PACF": non_zero_pacf.remove(prev_pacf) @@ -244,8 +247,7 @@ def _fit(self, X, y): ].extend(np.arange(3, 1 + len(X_c[idx]))) # 2.3-- Save the maximum autocorralated lag value as shapelet lenght - if len(self._cand_length_list[ - c + "," + str(idx) + "," + str(rep)]) == 0: + if len(self._cand_length_list[c + "," + str(idx) + "," + str(rep)]) == 0: # chose a random lenght using the lenght of the time series # (added 1 since the range start in 0) rand_value = self._random_state.choice(len(X_c[idx]), 1)[0] + 1 From f86c4654486666b596d428c3e87e126bd87fd805 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 15:24:59 +0200 Subject: [PATCH 17/38] excluded max acf and max pacf --- .../collection/shapelet_based/_rsast.py | 56 +++++++------------ 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 498b19d35c..b0d5c57feb 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -189,57 +189,43 @@ def _fit(self, X, y): if ( self.len_method == "both" or - self.len_method == "ACF" or - self.len_method == "Max ACF" - ): + self.len_method == "ACF"): # 2.1 -- Compute Autorrelation per object acf_val, acf_confint = acf(X_c[idx], nlags=len(X_c[idx])-1, alpha=.05) - prev_acf = 0 + for j in range(len(acf_confint)): if ( 3 <= j and (0 < acf_confint[j][0] <= acf_confint[j][1] or acf_confint[j][0] <= acf_confint[j][1] < 0)): - # Consider just the maximum ACF value - if prev_acf != 0 and self.len_method == "Max ACF": - non_zero_acf.remove(prev_acf) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].remove(prev_acf) + non_zero_acf.append(j) self._cand_length_list[ c + "," + str(idx) + "," + str(rep) ].append(j) - prev_acf = j + non_zero_pacf = [] if (self.len_method == "both" or - self.len_method == "PACF" or - self.len_method == "Max PACF"): - # 2.2 Compute Partial Autorrelation per object - pacf_val, pacf_confint = pacf(X_c[idx], method="ols", - nlags=(len(X_c[idx]) // 2) - 1, - alpha=.05) - prev_pacf = 0 - for j in range(len(pacf_confint)): - if ( - 3 <= j and - (0 < pacf_confint[j][0] <= pacf_confint[j][1] or - pacf_confint[j][0] <= pacf_confint[j][1] < 0)): - # Consider just the maximum PACF value - if prev_pacf != 0 and self.len_method == "Max PACF": - non_zero_pacf.remove(prev_pacf) - self._cand_length_list[ - c + "," + str(idx)+"," + str(rep) - ].remove(prev_pacf) - - non_zero_pacf.append(j) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].append(j) - prev_pacf = j + self.len_method == "PACF"): + # 2.2 Compute Partial Autorrelation per object + pacf_val, pacf_confint = pacf(X_c[idx], method="ols", + nlags=(len(X_c[idx]) // 2) - 1, + alpha=.05) + + for j in range(len(pacf_confint)): + if ( + 3 <= j and + (0 < pacf_confint[j][0] <= pacf_confint[j][1] or + pacf_confint[j][0] <= pacf_confint[j][1] < 0)): + + non_zero_pacf.append(j) + self._cand_length_list[ + c + "," + str(idx) + "," + str(rep) + ].append(j) + if (self.len_method == "all"): self._cand_length_list[ From 2d278ec9ce49b0976eaf3656730cc7d598474c0f Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 15:28:58 +0200 Subject: [PATCH 18/38] updated identation --- .../collection/shapelet_based/_rsast.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index b0d5c57feb..f16232be98 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -185,29 +185,27 @@ def _fit(self, X, y): for rep, idx in enumerate(choosen): self._cand_length_list[c + "," + str(idx) + "," + str(rep)] = [] + non_zero_acf = [] - if ( self.len_method == "both" or self.len_method == "ACF"): - # 2.1 -- Compute Autorrelation per object - acf_val, acf_confint = acf(X_c[idx], - nlags=len(X_c[idx])-1, alpha=.05) - - for j in range(len(acf_confint)): - if ( - 3 <= j and - (0 < acf_confint[j][0] <= acf_confint[j][1] or - acf_confint[j][0] <= acf_confint[j][1] < 0)): - - non_zero_acf.append(j) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].append(j) + # 2.1 -- Compute Autorrelation per object + acf_val, acf_confint = acf(X_c[idx], + nlags=len(X_c[idx])-1, alpha=.05) + + for j in range(len(acf_confint)): + if ( + 3 <= j and + (0 < acf_confint[j][0] <= acf_confint[j][1] or + acf_confint[j][0] <= acf_confint[j][1] < 0)): + non_zero_acf.append(j) + self._cand_length_list[ + c + "," + str(idx) + "," + str(rep) + ].append(j) non_zero_pacf = [] - if (self.len_method == "both" or self.len_method == "PACF"): # 2.2 Compute Partial Autorrelation per object @@ -220,7 +218,7 @@ def _fit(self, X, y): 3 <= j and (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0)): - + non_zero_pacf.append(j) self._cand_length_list[ c + "," + str(idx) + "," + str(rep) From 246b0ad7abc7f75d7f49452c7e01d9f2980e71a0 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 15:47:54 +0200 Subject: [PATCH 19/38] updated identation --- .../collection/shapelet_based/_rsast.py | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index f16232be98..b877e5432f 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -187,44 +187,36 @@ def _fit(self, X, y): self._cand_length_list[c + "," + str(idx) + "," + str(rep)] = [] non_zero_acf = [] - if ( - self.len_method == "both" or - self.len_method == "ACF"): - # 2.1 -- Compute Autorrelation per object - acf_val, acf_confint = acf(X_c[idx], - nlags=len(X_c[idx])-1, alpha=.05) - - for j in range(len(acf_confint)): - if ( - 3 <= j and - (0 < acf_confint[j][0] <= acf_confint[j][1] or - acf_confint[j][0] <= acf_confint[j][1] < 0)): - - non_zero_acf.append(j) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].append(j) + if (self.len_method == "both" or self.len_method == "ACF"): + # 2.1 -- Compute Autorrelation per object + acf_val, acf_confint = acf(X_c[idx], + nlags=len(X_c[idx])-1, alpha=.05) + + for j in range(len(acf_confint)): + if (3 <= j and + (0 < acf_confint[j][0] <= acf_confint[j][1] or + acf_confint[j][0] <= acf_confint[j][1] < 0)): + non_zero_acf.append(j) + self._cand_length_list[ + c + "," + str(idx) + "," + str(rep) + ].append(j) non_zero_pacf = [] - if (self.len_method == "both" or - self.len_method == "PACF"): - # 2.2 Compute Partial Autorrelation per object - pacf_val, pacf_confint = pacf(X_c[idx], method="ols", - nlags=(len(X_c[idx]) // 2) - 1, - alpha=.05) - - for j in range(len(pacf_confint)): - if ( - 3 <= j and - (0 < pacf_confint[j][0] <= pacf_confint[j][1] or - pacf_confint[j][0] <= pacf_confint[j][1] < 0)): - - non_zero_pacf.append(j) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].append(j) + if (self.len_method == "both" or self.len_method == "PACF"): + # 2.2 Compute Partial Autorrelation per object + pacf_val, pacf_confint = pacf(X_c[idx], method="ols", + nlags=(len(X_c[idx]) // 2) - 1, + alpha=.05) + + for j in range(len(pacf_confint)): + if (3 <= j and + (0 < pacf_confint[j][0] <= pacf_confint[j][1] or + pacf_confint[j][0] <= pacf_confint[j][1] < 0)): + non_zero_pacf.append(j) + self._cand_length_list[ + c + "," + str(idx) + "," + str(rep) + ].append(j) - if (self.len_method == "all"): self._cand_length_list[ c + ","+str(idx) + "," + str(rep) From ca59b2c71b722b5f6f6b8448d91df4efea5e9ad1 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 15:56:07 +0200 Subject: [PATCH 20/38] updated identation --- .../collection/shapelet_based/_rsast.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index b877e5432f..dbad1e5561 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -189,8 +189,9 @@ def _fit(self, X, y): non_zero_acf = [] if (self.len_method == "both" or self.len_method == "ACF"): # 2.1 -- Compute Autorrelation per object - acf_val, acf_confint = acf(X_c[idx], - nlags=len(X_c[idx])-1, alpha=.05) + acf_val, acf_confint = acf( + X_c[idx], nlags=len(X_c[idx]) - 1, + alpha=.05) for j in range(len(acf_confint)): if (3 <= j and @@ -204,9 +205,9 @@ def _fit(self, X, y): non_zero_pacf = [] if (self.len_method == "both" or self.len_method == "PACF"): # 2.2 Compute Partial Autorrelation per object - pacf_val, pacf_confint = pacf(X_c[idx], method="ols", - nlags=(len(X_c[idx]) // 2) - 1, - alpha=.05) + pacf_val, pacf_confint = pacf( + X_c[idx], method="ols", nlags=(len(X_c[idx]) // 2) - 1, + alpha=.05) for j in range(len(pacf_confint)): if (3 <= j and @@ -237,8 +238,7 @@ def _fit(self, X, y): c + "," + str(idx) + "," + str(rep)])) for max_shp_length in self._cand_length_list[ - c + ","+str(idx) + "," + str(rep) - ]: + c + ","+str(idx) + "," + str(rep)]: # 2.4-- Choose randomly n_random_points point for a TS # 2.5-- calculate the weights of probabilities for a random point # in a TS From 52a1d3372319b49982a79d4fd9eccf001b99b19a Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 16:06:27 +0200 Subject: [PATCH 21/38] updated identation --- .../collection/shapelet_based/_rsast.py | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index dbad1e5561..2957af0699 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -175,6 +175,8 @@ def _fit(self, X, y): # 2--calculate PACF and ACF for each TS chossen in each class for i, c in enumerate(classes): + + idx_len_list = c + ","+str(idx) + "," + str(rep) X_c = X_[y == c] cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) @@ -184,7 +186,7 @@ def _fit(self, X, y): self._kernels_generators[c] = [] for rep, idx in enumerate(choosen): - self._cand_length_list[c + "," + str(idx) + "," + str(rep)] = [] + self._cand_length_list[idx_len_list] = [] non_zero_acf = [] if (self.len_method == "both" or self.len_method == "ACF"): @@ -198,9 +200,7 @@ def _fit(self, X, y): (0 < acf_confint[j][0] <= acf_confint[j][1] or acf_confint[j][0] <= acf_confint[j][1] < 0)): non_zero_acf.append(j) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].append(j) + self._cand_length_list[idx_len_list].append(j) non_zero_pacf = [] if (self.len_method == "both" or self.len_method == "PACF"): @@ -214,31 +214,23 @@ def _fit(self, X, y): (0 < pacf_confint[j][0] <= pacf_confint[j][1] or pacf_confint[j][0] <= pacf_confint[j][1] < 0)): non_zero_pacf.append(j) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].append(j) + self._cand_length_list[idx_len_list].append(j) if (self.len_method == "all"): - self._cand_length_list[ - c + ","+str(idx) + "," + str(rep) - ].extend(np.arange(3, 1 + len(X_c[idx]))) + self._cand_length_list[idx_len_list].extend( + np.arange(3, 1 + len(X_c[idx]))) # 2.3-- Save the maximum autocorralated lag value as shapelet lenght - if len(self._cand_length_list[c + "," + str(idx) + "," + str(rep)]) == 0: + if len(self._cand_length_list[idx_len_list]) == 0: # chose a random lenght using the lenght of the time series # (added 1 since the range start in 0) rand_value = self._random_state.choice(len(X_c[idx]), 1)[0] + 1 - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ].extend([max(3, rand_value)]) + self._cand_length_list[idx_len_list].extend([max(3, rand_value)]) - self._cand_length_list[ - c + "," + str(idx) + "," + str(rep) - ] = list(set(self._cand_length_list[ - c + "," + str(idx) + "," + str(rep)])) + self._cand_length_list[idx_len_list] = list(set( + self._cand_length_list[idx_len_list])) - for max_shp_length in self._cand_length_list[ - c + ","+str(idx) + "," + str(rep)]: + for max_shp_length in self._cand_length_list[idx_len_list]: # 2.4-- Choose randomly n_random_points point for a TS # 2.5-- calculate the weights of probabilities for a random point # in a TS From 2f7e3b45a2330753a1cebb0107513549433a9b6c Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 16:13:44 +0200 Subject: [PATCH 22/38] updated identation --- aeon/transformations/collection/shapelet_based/_rsast.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 2957af0699..d676d89055 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -91,6 +91,7 @@ class RSAST(BaseCollectionTransformer): "output_data_type": "Tabular", "capability:multivariate": False, "algorithm_type": "subsequence", + "python_dependencies": "statsmodels", } def __init__( @@ -176,7 +177,6 @@ def _fit(self, X, y): for i, c in enumerate(classes): - idx_len_list = c + ","+str(idx) + "," + str(rep) X_c = X_[y == c] cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) @@ -186,6 +186,9 @@ def _fit(self, X, y): self._kernels_generators[c] = [] for rep, idx in enumerate(choosen): + + idx_len_list = c + ","+str(idx) + "," + str(rep) # defining indices for length list + self._cand_length_list[idx_len_list] = [] non_zero_acf = [] From 395ece6c4821e6970fbc83ab928f25c72b7b55f1 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 16:22:22 +0200 Subject: [PATCH 23/38] updated identation --- aeon/transformations/collection/shapelet_based/_rsast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index d676d89055..9e74a7ebf5 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -186,8 +186,8 @@ def _fit(self, X, y): self._kernels_generators[c] = [] for rep, idx in enumerate(choosen): - - idx_len_list = c + ","+str(idx) + "," + str(rep) # defining indices for length list + # defining indices for length list + idx_len_list = c + ","+str(idx) + "," + str(rep) self._cand_length_list[idx_len_list] = [] From 846472f719ee91f7695a2da7ed46d2147e107515 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 16:22:42 +0200 Subject: [PATCH 24/38] updated identation --- aeon/classification/shapelet_based/_rsast_classifier.py | 1 + aeon/transformations/collection/shapelet_based/_rsast.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 2bbaef9eae..925691c6ed 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -61,6 +61,7 @@ class RSASTClassifier(BaseClassifier): "capability:multithreading": True, "capability:multivariate": False, "algorithm_type": "subsequence", + "python_dependencies": ["statsmodels"], } def __init__( diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 9e74a7ebf5..ac47e9b0ba 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -91,7 +91,7 @@ class RSAST(BaseCollectionTransformer): "output_data_type": "Tabular", "capability:multivariate": False, "algorithm_type": "subsequence", - "python_dependencies": "statsmodels", + "python_dependencies": ["statsmodels"], } def __init__( From 013af532e7446b23f6568466c820a968d4fd7240 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Mon, 8 Apr 2024 16:27:33 +0200 Subject: [PATCH 25/38] update packages --- aeon/classification/shapelet_based/_rsast_classifier.py | 2 +- aeon/transformations/collection/shapelet_based/_rsast.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 925691c6ed..0aa35b7b7d 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -61,7 +61,7 @@ class RSASTClassifier(BaseClassifier): "capability:multithreading": True, "capability:multivariate": False, "algorithm_type": "subsequence", - "python_dependencies": ["statsmodels"], + "python_dependencies": "statsmodels", } def __init__( diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index ac47e9b0ba..8f2aea9b69 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -91,7 +91,6 @@ class RSAST(BaseCollectionTransformer): "output_data_type": "Tabular", "capability:multivariate": False, "algorithm_type": "subsequence", - "python_dependencies": ["statsmodels"], } def __init__( From b7ad0a77e6397974ed0c01ad8c60e8d50a7b6caf Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sat, 13 Apr 2024 23:07:05 +0200 Subject: [PATCH 26/38] included tag in transformer --- aeon/classification/shapelet_based/_rsast_classifier.py | 4 ++-- aeon/transformations/collection/shapelet_based/_rsast.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 0aa35b7b7d..766d739e6a 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -17,7 +17,7 @@ from aeon.classification import BaseClassifier from aeon.transformations.collection.shapelet_based import RSAST from aeon.utils.numba.general import z_normalise_series -import matplotlib.pyplot as plt + class RSASTClassifier(BaseClassifier): @@ -159,7 +159,7 @@ def _predict_proba(self, X): return dists def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): - + import matplotlib.pyplot as plt """Plot the most important features on ts. Parameters diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 8f2aea9b69..42735d4086 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -3,8 +3,8 @@ from aeon.transformations.collection import BaseCollectionTransformer from aeon.utils.numba.general import z_normalise_series from aeon.utils.validation import check_n_jobs -from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning -from statsmodels.tsa.stattools import acf, pacf + + import pandas as pd @njit(fastmath=False) @@ -86,11 +86,14 @@ class RSAST(BaseCollectionTransformer): >>> X_test = rsast.transform(X_test) """ + from statsmodels.tsa.stattools import acf, pacf + from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning _tags = { "output_data_type": "Tabular", "capability:multivariate": False, "algorithm_type": "subsequence", + "python_dependencies": "statsmodels", } def __init__( From 1182a3ad6206ddcfe0c560be80e2ced77620e24e Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sat, 13 Apr 2024 23:11:58 +0200 Subject: [PATCH 27/38] moved the import libraries --- aeon/transformations/collection/shapelet_based/_rsast.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 42735d4086..66353daec8 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -86,9 +86,8 @@ class RSAST(BaseCollectionTransformer): >>> X_test = rsast.transform(X_test) """ - from statsmodels.tsa.stattools import acf, pacf - from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning - + + _tags = { "output_data_type": "Tabular", "capability:multivariate": False, @@ -96,6 +95,9 @@ class RSAST(BaseCollectionTransformer): "python_dependencies": "statsmodels", } + from statsmodels.tsa.stattools import acf, pacf + from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning + def __init__( self, n_random_points=10, From e1227fd1ea5db8a2e4ea08fdba9a4a49c3f66fe0 Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sat, 13 Apr 2024 23:23:33 +0200 Subject: [PATCH 28/38] included brackets --- aeon/classification/shapelet_based/_rsast_classifier.py | 2 +- aeon/transformations/collection/shapelet_based/_rsast.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 766d739e6a..f85b8b9024 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -61,7 +61,7 @@ class RSASTClassifier(BaseClassifier): "capability:multithreading": True, "capability:multivariate": False, "algorithm_type": "subsequence", - "python_dependencies": "statsmodels", + "python_dependencies": ["statsmodels"], } def __init__( diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 66353daec8..56c710c602 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -92,12 +92,12 @@ class RSAST(BaseCollectionTransformer): "output_data_type": "Tabular", "capability:multivariate": False, "algorithm_type": "subsequence", - "python_dependencies": "statsmodels", + "python_dependencies": ["statsmodels"], } from statsmodels.tsa.stattools import acf, pacf from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning - + def __init__( self, n_random_points=10, From 85bd62cf1e0ab2569de5e2386a05c3b33f0f2fce Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sat, 13 Apr 2024 23:27:46 +0200 Subject: [PATCH 29/38] moved libraries to fit function --- aeon/transformations/collection/shapelet_based/_rsast.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 56c710c602..08bdc652f5 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -95,8 +95,7 @@ class RSAST(BaseCollectionTransformer): "python_dependencies": ["statsmodels"], } - from statsmodels.tsa.stattools import acf, pacf - from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning + def __init__( self, @@ -118,6 +117,10 @@ def __init__( super().__init__() def _fit(self, X, y): + + from statsmodels.tsa.stattools import acf, pacf + from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning + """Select reference time series and generate subsequences from them. Parameters From 5996a92118e992a3e9f4c485fbec6c03eacc2f9d Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sat, 13 Apr 2024 23:31:58 +0200 Subject: [PATCH 30/38] deleted spaces --- aeon/transformations/collection/shapelet_based/_rsast.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 08bdc652f5..5ae9b9c104 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -86,8 +86,6 @@ class RSAST(BaseCollectionTransformer): >>> X_test = rsast.transform(X_test) """ - - _tags = { "output_data_type": "Tabular", "capability:multivariate": False, @@ -95,8 +93,6 @@ class RSAST(BaseCollectionTransformer): "python_dependencies": ["statsmodels"], } - - def __init__( self, n_random_points=10, From c5539f605e3a243b651f0523b27105e4e0973e62 Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sat, 13 Apr 2024 23:43:51 +0200 Subject: [PATCH 31/38] updated spaces --- aeon/classification/shapelet_based/_rsast_classifier.py | 1 + aeon/classification/shapelet_based/_sast_classifier.py | 1 + aeon/transformations/collection/shapelet_based/_rsast.py | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index f85b8b9024..08b89b75f5 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -199,3 +199,4 @@ def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): axes[f].plot(range(start_pos, start_pos + kernel.size), kernel, linewidth=5) axes[f].plot(range(ts.size), ts, linewidth=2) axes[f].set_title(f"feature: {f+1}") + \ No newline at end of file diff --git a/aeon/classification/shapelet_based/_sast_classifier.py b/aeon/classification/shapelet_based/_sast_classifier.py index a2bdca6fad..990366e79d 100644 --- a/aeon/classification/shapelet_based/_sast_classifier.py +++ b/aeon/classification/shapelet_based/_sast_classifier.py @@ -18,6 +18,7 @@ from aeon.utils.numba.general import z_normalise_series + class SASTClassifier(BaseClassifier): """Classification pipeline using SAST [1]_ transformer and an sklean classifier. diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 5ae9b9c104..43cfaa023f 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -3,10 +3,9 @@ from aeon.transformations.collection import BaseCollectionTransformer from aeon.utils.numba.general import z_normalise_series from aeon.utils.validation import check_n_jobs - - import pandas as pd + @njit(fastmath=False) def _apply_kernel(ts, arr): d_best = np.inf # sdist @@ -86,6 +85,7 @@ class RSAST(BaseCollectionTransformer): >>> X_test = rsast.transform(X_test) """ + _tags = { "output_data_type": "Tabular", "capability:multivariate": False, @@ -312,4 +312,3 @@ def _transform(self, X, y=None): set_num_threads(prev_threads) return X_transformed - From b525d88a9c0c6f7a6acd1cc9867842e1db0cb21f Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sat, 13 Apr 2024 23:49:44 +0200 Subject: [PATCH 32/38] updated identation --- aeon/classification/shapelet_based/_rsast_classifier.py | 1 - aeon/transformations/collection/shapelet_based/_rsast.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 08b89b75f5..f85b8b9024 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -199,4 +199,3 @@ def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): axes[f].plot(range(start_pos, start_pos + kernel.size), kernel, linewidth=5) axes[f].plot(range(ts.size), ts, linewidth=2) axes[f].set_title(f"feature: {f+1}") - \ No newline at end of file diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 43cfaa023f..e9e137c344 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -111,7 +111,7 @@ def __init__( self._kernel_orig = [] self._kernels_generators = {} # Reference time series super().__init__() - + def _fit(self, X, y): from statsmodels.tsa.stattools import acf, pacf @@ -285,7 +285,7 @@ def _fit(self, X, y): self._kernels[k, :len(kernel)] = z_normalise_series(kernel) return self - + def _transform(self, X, y=None): """Transform the input X using the generated subsequences. Parameters From fa80c266e3de01002a945d31b4cd51a1ca1af4f8 Mon Sep 17 00:00:00 2001 From: Nicolas Rojas Varela Date: Sun, 14 Apr 2024 11:43:30 +0200 Subject: [PATCH 33/38] included library statmodel in rsast classifier --- aeon/classification/shapelet_based/_rsast_classifier.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index f85b8b9024..0e7bf7122f 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -97,6 +97,9 @@ def _fit(self, X, y): This pipeline classifier """ + from statsmodels.tsa.stattools import acf, pacf + from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning + self._transformer = RSAST( self.n_random_points, self.len_method, From a7fe63c76f4a073205dac63c844b8722a5f58292 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Tue, 16 Apr 2024 15:22:33 +0200 Subject: [PATCH 34/38] applied in Classifier: doctest: +SKIP --- .../shapelet_based/_rsast_classifier.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 0e7bf7122f..f88aec0727 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -18,8 +18,6 @@ from aeon.transformations.collection.shapelet_based import RSAST from aeon.utils.numba.general import z_normalise_series - - class RSASTClassifier(BaseClassifier): """Classification pipeline using RSAST [1]_ transformer and an sklean classifier. @@ -51,10 +49,10 @@ class RSASTClassifier(BaseClassifier): >>> 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") - >>> clf = RSASTClassifier() - >>> clf.fit(X_train, y_train) + >>> clf = RSASTClassifier() # doctest: +SKIP + >>> clf.fit(X_train, y_train) # doctest: +SKIP RSASTClassifier(...) - >>> y_pred = clf.predict(X_test) + >>> y_pred = clf.predict(X_test) # doctest: +SKIP """ _tags = { @@ -97,8 +95,7 @@ def _fit(self, X, y): This pipeline classifier """ - from statsmodels.tsa.stattools import acf, pacf - from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning + self._transformer = RSAST( self.n_random_points, From 2f5ad63333cf1bec9542482bcf9f6d88d7802da8 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Tue, 16 Apr 2024 15:47:35 +0200 Subject: [PATCH 35/38] skip in # doctest: +SKIP --- aeon/transformations/collection/shapelet_based/_rsast.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index e9e137c344..255e406120 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -78,11 +78,11 @@ class RSAST(BaseCollectionTransformer): >>> 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") - >>> rsast = RSAST() - >>> rsast.fit(X_train, y_train) + >>> rsast = RSAST() # doctest: +SKIP + >>> rsast.fit(X_train, y_train) # doctest: +SKIP RSAST() - >>> X_train = rsast.transform(X_train) - >>> X_test = rsast.transform(X_test) + >>> X_train = rsast.transform(X_train) # doctest: +SKIP + >>> X_test = rsast.transform(X_test) # doctest: +SKIP """ From 859c4feb17c2be978fd326a57dae781fbe9fb8a0 Mon Sep 17 00:00:00 2001 From: nirojasva Date: Wed, 17 Apr 2024 09:07:47 +0200 Subject: [PATCH 36/38] using pre-commit --- .../classification/shapelet_based/__init__.py | 2 +- .../shapelet_based/_rsast_classifier.py | 12 +- .../shapelet_based/_sast_classifier.py | 1 - .../collection/shapelet_based/__init__.py | 2 +- .../collection/shapelet_based/_rsast.py | 176 ++++++++++-------- 5 files changed, 105 insertions(+), 88 deletions(-) diff --git a/aeon/classification/shapelet_based/__init__.py b/aeon/classification/shapelet_based/__init__.py index b687d93413..f8c45a242a 100644 --- a/aeon/classification/shapelet_based/__init__.py +++ b/aeon/classification/shapelet_based/__init__.py @@ -12,6 +12,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._sast_classifier import SASTClassifier from aeon.classification.shapelet_based._rsast_classifier import RSASTClassifier +from aeon.classification.shapelet_based._sast_classifier import SASTClassifier from aeon.classification.shapelet_based._stc import ShapeletTransformClassifier diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index f88aec0727..78c08d92d9 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -18,14 +18,15 @@ from aeon.transformations.collection.shapelet_based import RSAST from aeon.utils.numba.general import z_normalise_series + class RSASTClassifier(BaseClassifier): """Classification pipeline using RSAST [1]_ transformer and an sklean classifier. Parameters ---------- n_random_points: int default = 10 the number of initial random points to extract - len_method: string default="both" the type of statistical tool used to get the - length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, + len_method: string default="both" the type of statistical tool used to get the + length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS nb_inst_per_class : int default = 10 the number of reference time series to select per class @@ -39,10 +40,10 @@ class RSASTClassifier(BaseClassifier): Reference --------- - .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling + .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for Time Series Classification. https://hal.science/hal-04311309/ - + Examples -------- >>> from aeon.classification.shapelet_based import RSASTClassifier @@ -95,8 +96,6 @@ def _fit(self, X, y): This pipeline classifier """ - - self._transformer = RSAST( self.n_random_points, self.len_method, @@ -160,6 +159,7 @@ def _predict_proba(self, X): def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): import matplotlib.pyplot as plt + """Plot the most important features on ts. Parameters diff --git a/aeon/classification/shapelet_based/_sast_classifier.py b/aeon/classification/shapelet_based/_sast_classifier.py index 990366e79d..a2bdca6fad 100644 --- a/aeon/classification/shapelet_based/_sast_classifier.py +++ b/aeon/classification/shapelet_based/_sast_classifier.py @@ -18,7 +18,6 @@ from aeon.utils.numba.general import z_normalise_series - class SASTClassifier(BaseClassifier): """Classification pipeline using SAST [1]_ transformer and an sklean classifier. diff --git a/aeon/transformations/collection/shapelet_based/__init__.py b/aeon/transformations/collection/shapelet_based/__init__.py index e7851a5dbe..23990ad520 100644 --- a/aeon/transformations/collection/shapelet_based/__init__.py +++ b/aeon/transformations/collection/shapelet_based/__init__.py @@ -5,8 +5,8 @@ from aeon.transformations.collection.shapelet_based._dilated_shapelet_transform import ( RandomDilatedShapeletTransform, ) -from aeon.transformations.collection.shapelet_based._sast import SAST from aeon.transformations.collection.shapelet_based._rsast import RSAST +from aeon.transformations.collection.shapelet_based._sast import SAST from aeon.transformations.collection.shapelet_based._shapelet_transform import ( RandomShapeletTransform, ) diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 255e406120..3ce7ce0277 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -1,9 +1,10 @@ import numpy as np +import pandas as pd from numba import get_num_threads, njit, prange, set_num_threads + from aeon.transformations.collection import BaseCollectionTransformer from aeon.utils.numba.general import z_normalise_series from aeon.utils.validation import check_n_jobs -import pandas as pd @njit(fastmath=False) @@ -15,7 +16,7 @@ def _apply_kernel(ts, arr): kernel_len = kernel.shape[0] for i in range(m - kernel_len + 1): d = np.sum((z_normalise_series(ts[i : i + kernel_len]) - kernel) ** 2) - if d < d_best: + if d < d_best: d_best = d return d_best @@ -35,27 +36,27 @@ def _apply_kernels(X, kernels): class RSAST(BaseCollectionTransformer): """Random Scalable and Accurate Subsequence Transform (SAST). - RSAST [1] is based on SAST, it uses a stratified sampling strategy + RSAST [1] is based on SAST, it uses a stratified sampling strategy for subsequences selection but additionally takes into account certain - statistical criteria such as ANOVA, ACF, and PACF to further reduce + statistical criteria such as ANOVA, ACF, and PACF to further reduce the search space of shapelets. - + RSAST starts with the pre-computation of a list of weights, using ANOVA, - which helps in the selection of initial points for subsequences. Then - randomly select k time series per class, which are used with an ACF and PACF, - obtaining a set of highly correlated lagged values. These values are used as - potential lengths for the shapelets. Lastly, with a pre-defined number of - admissible starting points to sample, the shapelets are extracted and used to - transform the original dataset, replacing each time series by the vector of its + which helps in the selection of initial points for subsequences. Then + randomly select k time series per class, which are used with an ACF and PACF, + obtaining a set of highly correlated lagged values. These values are used as + potential lengths for the shapelets. Lastly, with a pre-defined number of + admissible starting points to sample, the shapelets are extracted and used to + transform the original dataset, replacing each time series by the vector of its distance to each subsequence. Parameters ---------- n_random_points: int default = 10 the number of initial random points to extract - len_method: string default="both" the type of statistical tool used to get - the length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, + len_method: string default="both" the type of statistical tool used to get + the length of shapelets. "both"=ACF&PACF, "ACF"=ACF, "PACF"=PACF, "None"=Extract randomly any length from the TS - + nb_inst_per_class : int default = 10 the number of reference time series to select per class seed : int, default = None @@ -67,7 +68,7 @@ class RSAST(BaseCollectionTransformer): Reference --------- - .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). + .. [1] Varela, N. R., Mbouopda, M. F., & Nguifo, E. M. (2023). RSAST: Sampling Shapelets for Time Series Classification. https://hal.science/hal-04311309/ @@ -113,10 +114,10 @@ def __init__( super().__init__() def _fit(self, X, y): - + + from scipy.stats import ConstantInputWarning, DegenerateDataWarning, f_oneway from statsmodels.tsa.stattools import acf, pacf - from scipy.stats import f_oneway, DegenerateDataWarning, ConstantInputWarning - + """Select reference time series and generate subsequences from them. Parameters @@ -132,20 +133,21 @@ def _fit(self, X, y): This transformer """ - + # 0- initialize variables and convert values in "y" to string X_ = np.reshape(X, (X.shape[0], X.shape[-1])) self._random_state = ( np.random.RandomState(self.seed) if not isinstance(self.seed, np.random.RandomState) - else self.seed) + else self.seed + ) classes = np.unique(y) self._num_classes = classes.shape[0] y = np.asarray([str(x_s) for x_s in y]) - + n = [] classes = np.unique(y) self.num_classes = classes.shape[0] @@ -155,10 +157,9 @@ def _fit(self, X, y): for i in range(X_.shape[1]): statistic_per_class = {} for c in classes: - assert len( - X_[ - np.where(y == c)[0] - ][:, i]) > 0, 'Time t without values in TS' + assert ( + len(X_[np.where(y == c)[0]][:, i]) > 0 + ), "Time t without values in TS" statistic_per_class[c] = X_[np.where(y == c)[0]][:, i] statistic_per_class = pd.Series(statistic_per_class) @@ -169,125 +170,142 @@ def _fit(self, X, y): p_value = np.nan # Interpretation of the results - # if p_value < 0.05: " The means of the populations are + # if p_value < 0.05: " The means of the populations are # significantly different." if np.isnan(p_value): n.append(0) else: - n.append(1-p_value) + n.append(1 - p_value) # 2--calculate PACF and ACF for each TS chossen in each class - + for i, c in enumerate(classes): - + X_c = X_[y == c] cnt = np.min([self.nb_inst_per_class, X_c.shape[0]]).astype(int) choosen = self._random_state.permutation(X_c.shape[0])[:cnt] - + self._kernels_generators[c] = [] for rep, idx in enumerate(choosen): # defining indices for length list - idx_len_list = c + ","+str(idx) + "," + str(rep) + idx_len_list = c + "," + str(idx) + "," + str(rep) self._cand_length_list[idx_len_list] = [] - + non_zero_acf = [] - if (self.len_method == "both" or self.len_method == "ACF"): + if self.len_method == "both" or self.len_method == "ACF": # 2.1 -- Compute Autorrelation per object acf_val, acf_confint = acf( - X_c[idx], nlags=len(X_c[idx]) - 1, - alpha=.05) + X_c[idx], nlags=len(X_c[idx]) - 1, alpha=0.05 + ) for j in range(len(acf_confint)): - if (3 <= j and - (0 < acf_confint[j][0] <= acf_confint[j][1] or - acf_confint[j][0] <= acf_confint[j][1] < 0)): + if 3 <= j and ( + 0 < acf_confint[j][0] <= acf_confint[j][1] + or acf_confint[j][0] <= acf_confint[j][1] < 0 + ): non_zero_acf.append(j) self._cand_length_list[idx_len_list].append(j) non_zero_pacf = [] - if (self.len_method == "both" or self.len_method == "PACF"): + if self.len_method == "both" or self.len_method == "PACF": # 2.2 Compute Partial Autorrelation per object pacf_val, pacf_confint = pacf( - X_c[idx], method="ols", nlags=(len(X_c[idx]) // 2) - 1, - alpha=.05) + X_c[idx], + method="ols", + nlags=(len(X_c[idx]) // 2) - 1, + alpha=0.05, + ) for j in range(len(pacf_confint)): - if (3 <= j and - (0 < pacf_confint[j][0] <= pacf_confint[j][1] or - pacf_confint[j][0] <= pacf_confint[j][1] < 0)): + if 3 <= j and ( + 0 < pacf_confint[j][0] <= pacf_confint[j][1] + or pacf_confint[j][0] <= pacf_confint[j][1] < 0 + ): non_zero_pacf.append(j) self._cand_length_list[idx_len_list].append(j) - - if (self.len_method == "all"): + + if self.len_method == "all": self._cand_length_list[idx_len_list].extend( - np.arange(3, 1 + len(X_c[idx]))) - + np.arange(3, 1 + len(X_c[idx])) + ) + # 2.3-- Save the maximum autocorralated lag value as shapelet lenght if len(self._cand_length_list[idx_len_list]) == 0: - # chose a random lenght using the lenght of the time series + # chose a random lenght using the lenght of the time series # (added 1 since the range start in 0) rand_value = self._random_state.choice(len(X_c[idx]), 1)[0] + 1 self._cand_length_list[idx_len_list].extend([max(3, rand_value)]) - self._cand_length_list[idx_len_list] = list(set( - self._cand_length_list[idx_len_list])) + self._cand_length_list[idx_len_list] = list( + set(self._cand_length_list[idx_len_list]) + ) for max_shp_length in self._cand_length_list[idx_len_list]: - # 2.4-- Choose randomly n_random_points point for a TS - # 2.5-- calculate the weights of probabilities for a random point + # 2.4-- Choose randomly n_random_points point for a TS + # 2.5-- calculate the weights of probabilities for a random point # in a TS if sum(n) == 0: - # Determine equal weights of a random point point in TS is + # Determine equal weights of a random point point in TS is # there are no significant points - weights = [1/len(n) for i in range(len(n))] - weights = weights[:len(X_c[idx]) - max_shp_length + 1]/np.sum( - weights[:len(X_c[idx]) - max_shp_length + 1]) - else: - # Determine the weights of a random point point in TS + weights = [1 / len(n) for i in range(len(n))] + weights = weights[ + : len(X_c[idx]) - max_shp_length + 1 + ] / np.sum(weights[: len(X_c[idx]) - max_shp_length + 1]) + else: + # Determine the weights of a random point point in TS # (excluding points after n-l+1) weights = n / np.sum(n) - weights = weights[:len(X_c[idx]) - max_shp_length + 1]/np.sum( - weights[:len(X_c[idx]) - max_shp_length + 1]) + weights = weights[ + : len(X_c[idx]) - max_shp_length + 1 + ] / np.sum(weights[: len(X_c[idx]) - max_shp_length + 1]) - if self.n_random_points > len(X_c[idx])-max_shp_length+1: - # set a upper limit for the posible of number of random + if self.n_random_points > len(X_c[idx]) - max_shp_length + 1: + # set a upper limit for the posible of number of random # points when selecting without replacement limit_rpoint = len(X_c[idx]) - max_shp_length + 1 rand_point_ts = self._random_state.choice( - len(X_c[idx]) - max_shp_length + 1, limit_rpoint, - p=weights, replace=False) + len(X_c[idx]) - max_shp_length + 1, + limit_rpoint, + p=weights, + replace=False, + ) else: rand_point_ts = self._random_state.choice( - len(X_c[idx]) - max_shp_length + 1, self.n_random_points, - p=weights, replace=False) - - for i in rand_point_ts: + len(X_c[idx]) - max_shp_length + 1, + self.n_random_points, + p=weights, + replace=False, + ) + + for i in rand_point_ts: # 2.6-- Extract the subsequence with that point kernel = X_c[idx][i : i + max_shp_length].reshape(1, -1).copy() - + if m_kernel < max_shp_length: - m_kernel = max_shp_length - + m_kernel = max_shp_length + self._kernel_orig.append(np.squeeze(kernel)) self._kernels_generators[c].extend(X_c[idx].reshape(1, -1)) - + # 3--save the calculated subsequences n_kernels = len(self._kernel_orig) - + self._kernels = np.full( - (n_kernels, m_kernel), dtype=np.float32, fill_value=np.nan) - + (n_kernels, m_kernel), dtype=np.float32, fill_value=np.nan + ) + for k, kernel in enumerate(self._kernel_orig): - self._kernels[k, :len(kernel)] = z_normalise_series(kernel) - + self._kernels[k, : len(kernel)] = z_normalise_series(kernel) + return self def _transform(self, X, y=None): """Transform the input X using the generated subsequences. + Parameters ---------- X: np.ndarray shape (n_cases, n_channels, n_timepoints) @@ -307,7 +325,7 @@ def _transform(self, X, y=None): n_jobs = check_n_jobs(self.n_jobs) set_num_threads(n_jobs) - + X_transformed = _apply_kernels(X_, self._kernels) # subsequence transform of X set_num_threads(prev_threads) From 60e68e2ae4fd5f3458b2ff19cc51b2035ac4862b Mon Sep 17 00:00:00 2001 From: nirojasva Date: Wed, 17 Apr 2024 09:07:59 +0200 Subject: [PATCH 37/38] using pre-commit --- aeon/classification/shapelet_based/_rsast_classifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 78c08d92d9..1b3d736244 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -20,7 +20,7 @@ class RSASTClassifier(BaseClassifier): - """Classification pipeline using RSAST [1]_ transformer and an sklean classifier. + """Classification pipeline using RSAST [1]_ transformer and an sklearn classifier. Parameters ---------- From f4167b32be9ff0b717007366c00a59519dac5abc Mon Sep 17 00:00:00 2001 From: nirojasva Date: Wed, 24 Apr 2024 09:49:37 +0200 Subject: [PATCH 38/38] updated changes requested for PR --- .../shapelet_based/_rsast_classifier.py | 59 +++---------------- .../collection/shapelet_based/_rsast.py | 6 +- 2 files changed, 11 insertions(+), 54 deletions(-) diff --git a/aeon/classification/shapelet_based/_rsast_classifier.py b/aeon/classification/shapelet_based/_rsast_classifier.py index 1b3d736244..5b591a48fe 100644 --- a/aeon/classification/shapelet_based/_rsast_classifier.py +++ b/aeon/classification/shapelet_based/_rsast_classifier.py @@ -3,12 +3,9 @@ Pipeline classifier using the RSAST transformer and an sklearn classifier. """ -__maintainer__ = [] +__maintainer__ = ["nirojasva"] __all__ = ["RSASTClassifier"] - -from operator import itemgetter - import numpy as np from sklearn.linear_model import RidgeClassifierCV from sklearn.pipeline import make_pipeline @@ -16,11 +13,14 @@ from aeon.base._base import _clone_estimator from aeon.classification import BaseClassifier from aeon.transformations.collection.shapelet_based import RSAST -from aeon.utils.numba.general import z_normalise_series class RSASTClassifier(BaseClassifier): - """Classification pipeline using RSAST [1]_ transformer and an sklearn classifier. + """RSASTClassifier. + + Classification pipeline using + Random Scalable and Accurate Subsequence Transform (RSAST) [1]_ transformer + and an sklearn classifier. Parameters ---------- @@ -59,8 +59,8 @@ class RSASTClassifier(BaseClassifier): _tags = { "capability:multithreading": True, "capability:multivariate": False, - "algorithm_type": "subsequence", - "python_dependencies": ["statsmodels"], + "algorithm_type": "shapelet", + "python_dependencies": "statsmodels", } def __init__( @@ -156,46 +156,3 @@ def _predict_proba(self, X): for i in range(0, X.shape[0]): dists[i, np.where(self.classes_ == preds[i])] = 1 return dists - - def plot_most_important_feature_on_ts(self, ts, feature_importance, limit=5): - import matplotlib.pyplot as plt - - """Plot the most important features on ts. - - Parameters - ---------- - ts : float[:] - The time series - feature_importance : float[:] - The importance of each feature in the transformed data - limit : int, default = 5 - The maximum number of features to plot - - Returns - ------- - fig : plt.figure - The figure - """ - features = zip(self._transformer._kernel_orig, feature_importance) - sorted_features = sorted(features, key=itemgetter(1), reverse=True) - - max_ = min(limit, len(sorted_features)) - - fig, axes = plt.subplots( - 1, max_, sharey=True, figsize=(3 * max_, 3), tight_layout=True - ) - - for f in range(max_): - kernel, _ = sorted_features[f] - znorm_kernel = z_normalise_series(kernel) - d_best = np.inf - for i in range(ts.size - kernel.size): - s = ts[i : i + kernel.size] - s = z_normalise_series(s) - d = np.sum((s - znorm_kernel) ** 2) - if d < d_best: - d_best = d - start_pos = i - axes[f].plot(range(start_pos, start_pos + kernel.size), kernel, linewidth=5) - axes[f].plot(range(ts.size), ts, linewidth=2) - axes[f].set_title(f"feature: {f+1}") diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py index 3ce7ce0277..e0e7f8abd2 100644 --- a/aeon/transformations/collection/shapelet_based/_rsast.py +++ b/aeon/transformations/collection/shapelet_based/_rsast.py @@ -34,7 +34,7 @@ def _apply_kernels(X, kernels): class RSAST(BaseCollectionTransformer): - """Random Scalable and Accurate Subsequence Transform (SAST). + """Random Scalable and Accurate Subsequence Transform (RSAST). RSAST [1] is based on SAST, it uses a stratified sampling strategy for subsequences selection but additionally takes into account certain @@ -90,8 +90,8 @@ class RSAST(BaseCollectionTransformer): _tags = { "output_data_type": "Tabular", "capability:multivariate": False, - "algorithm_type": "subsequence", - "python_dependencies": ["statsmodels"], + "algorithm_type": "shapelet", + "python_dependencies": "statsmodels", } def __init__(