From fe951d811e1ac35001d213de1fd2c271447be0fe Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 13:38:37 +0200 Subject: [PATCH 01/47] New function in plot: plot_power_distribution --- nilmth/utils/plot.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/nilmth/utils/plot.py b/nilmth/utils/plot.py index e9e4a9f..584b0bf 100644 --- a/nilmth/utils/plot.py +++ b/nilmth/utils/plot.py @@ -1,5 +1,5 @@ -import matplotlib.pyplot as plt import numpy as np +from matplotlib import pyplot as plt def plot_real_data( @@ -66,3 +66,34 @@ def plot_real_vs_prediction( else: plt.savefig(savefig) plt.close() + + +def plot_power_distribution( + ser: np.array, app: str, bins: int = 20, figsize: tuple = (3, 3) +): + """Plot the histogram of power distribution + + Parameters + ---------- + ser : numpy.array + Contains all the power values + app : label + Name of the appliance + bins : int, optional + Histogram splits, by default 20 + figsize : tuple, optional + Figure size, by default (3, 3) + + Returns + ------- + matplotlib.figure.Figure + matplotlib.axes._subplots.AxesSubplot + + """ + fig, ax = plt.subplots(figsize=figsize) + ax.hist(ser, bins=bins) + ax.set_title(app.capitalize().replace("_", " ")) + ax.set_xlabel("Power (watts)") + ax.set_ylabel("Frequency") + ax.grid() + return fig, ax From 223a027e0abd7370e21de8daec87943242072cf4 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 14:07:51 +0200 Subject: [PATCH 02/47] Threshold returns info when called --- nilmth/data/threshold.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nilmth/data/threshold.py b/nilmth/data/threshold.py index a53e35e..39bd2aa 100644 --- a/nilmth/data/threshold.py +++ b/nilmth/data/threshold.py @@ -41,6 +41,10 @@ def __init__( self._status_fun = self._compute_status self._initialize_params() + def __repr__(self): + """This message is returned any time the object is called""" + return f"Threshold | Method: {self.method} | Statuses: {self.num_status}" + def _initialize_params(self): """ Given the method name and list of appliances, From 98bd9ba673412ea30c7c8aaf9ff1538fb2d78169 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 14:08:17 +0200 Subject: [PATCH 03/47] Dataset returns info when called, add datapoints property --- nilmth/data/dataset.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/nilmth/data/dataset.py b/nilmth/data/dataset.py index 217c69b..8d18ffd 100644 --- a/nilmth/data/dataset.py +++ b/nilmth/data/dataset.py @@ -11,7 +11,6 @@ class DataSet(data.Dataset): files: list = list() - datapoints: int = 0 appliances: list = list() num_apps: int = 0 status: list = list() @@ -50,6 +49,17 @@ def __init__( f"Dataset received extra kwargs, not used:\n {', '.join(kwargs.keys())}" ) + @property + def datapoints(self) -> int: + return len(self.files) + + def __len__(self): + return self.datapoints + + def __repr__(self): + """This message is returned any time the object is called""" + return f"Dataset | Data points: {self.datapoints} | Input length: {self.length}" + @staticmethod def _open_file(path_file: str) -> pd.DataFrame: """Opens a csv as a pandas.DataFrame""" @@ -85,7 +95,6 @@ def _list_files(self, path_data: str): files += files_of_building[test_idx:] # Update the class parameters self.files = files - self.datapoints = len(files) logger.info(f"{self.datapoints} data points found for {self.subset}") def _get_parameters_from_file(self): @@ -160,6 +169,3 @@ def __getitem__(self, index: int) -> tuple: except KeyError: s = self.power_to_status(y) return x, y, s - - def __len__(self): - return self.datapoints From a1390ca6ee4fa293f123ec6e479060a7110571b3 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 14:08:41 +0200 Subject: [PATCH 04/47] Dataloader returns info when called, add threshold and appliances properties --- nilmth/data/dataloader.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/nilmth/data/dataloader.py b/nilmth/data/dataloader.py index 986b387..c35482c 100644 --- a/nilmth/data/dataloader.py +++ b/nilmth/data/dataloader.py @@ -4,6 +4,7 @@ import torch.utils.data as data from nilmth.data.dataset import DataSet +from nilmth.data.threshold import Threshold from nilmth.utils.config import ConfigError from nilmth.utils.logging import logger @@ -28,11 +29,23 @@ def __init__( self.path_threshold = path_threshold self.compute_thresholds() + @property + def threshold(self) -> Threshold: + return self.dataset.threshold + + @property + def appliances(self) -> list: + return self.dataset.appliances + + def __repr__(self): + """This message is returned any time the object is called""" + return f"Dataloader > {self.dataset}" + def get_appliance_power_series(self, app: Union[str, int]) -> np.array: """Returns the full series of power of an appliance""" if type(app) == str: try: - app_idx = self.dataset.appliances.index(app) + app_idx = self.appliances.index(app) except ValueError: raise ValueError(f"Appliance not found: {app}") else: @@ -48,7 +61,7 @@ def compute_thresholds(self): """Compute the thresholds of each appliance""" # First try to load the thresholds try: - self.dataset.threshold.read_config(self.path_threshold) + self.threshold.read_config(self.path_threshold) return # If not possible, compute them except ConfigError: @@ -59,12 +72,12 @@ def compute_thresholds(self): ) logger.debug("Threshold values not found. Computing them...") # Loop through each appliance - for app_idx, app in enumerate(self.dataset.appliances): + for app_idx, app in enumerate(self.appliances): ser = self.get_appliance_power_series(app_idx) # Concatenate all values and update the threshold - self.dataset.threshold.update_appliance_threshold(ser, app) + self.threshold.update_appliance_threshold(ser, app) # Write the config file - self.dataset.threshold.write_config(self.path_threshold) + self.threshold.write_config(self.path_threshold) def next(self): """Returns the next data batch""" From 8f22e365ee0974d63d01129e649b3600f6658158 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 14:10:25 +0200 Subject: [PATCH 05/47] Dataset add property status --- nilmth/data/dataloader.py | 4 ++++ nilmth/data/temporal.py | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nilmth/data/dataloader.py b/nilmth/data/dataloader.py index c35482c..f2a955b 100644 --- a/nilmth/data/dataloader.py +++ b/nilmth/data/dataloader.py @@ -37,6 +37,10 @@ def threshold(self) -> Threshold: def appliances(self) -> list: return self.dataset.appliances + @property + def status(self): + return self.dataset.status + def __repr__(self): """This message is returned any time the object is called""" return f"Dataloader > {self.dataset}" diff --git a/nilmth/data/temporal.py b/nilmth/data/temporal.py index ff11dd6..0abf9de 100644 --- a/nilmth/data/temporal.py +++ b/nilmth/data/temporal.py @@ -44,9 +44,7 @@ def generate_temporal_data(loader: DataLoader, path: str = "data_temp"): for m in mat: df = pd.DataFrame( m, - columns=["aggregate"] - + loader.dataset.appliances - + loader.dataset.status, + columns=["aggregate"] + loader.appliances + loader.status, ) path_file = os.path.join(path, f"{file_num:04}.csv") df.to_csv(path_file) From bd90c52a39eba6b94f58d7e8c8057646e0aafa62 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 14:14:03 +0200 Subject: [PATCH 06/47] Dataset add properties border and files --- nilmth/data/dataloader.py | 10 +++++++++- nilmth/data/dataset.py | 2 +- nilmth/data/temporal.py | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/nilmth/data/dataloader.py b/nilmth/data/dataloader.py index f2a955b..f548015 100644 --- a/nilmth/data/dataloader.py +++ b/nilmth/data/dataloader.py @@ -38,9 +38,17 @@ def appliances(self) -> list: return self.dataset.appliances @property - def status(self): + def status(self) -> list: return self.dataset.status + @property + def files(self) -> list: + return self.dataset.files + + @property + def border(self) -> int: + return self.dataset.border + def __repr__(self): """This message is returned any time the object is called""" return f"Dataloader > {self.dataset}" diff --git a/nilmth/data/dataset.py b/nilmth/data/dataset.py index 8d18ffd..7b99cdc 100644 --- a/nilmth/data/dataset.py +++ b/nilmth/data/dataset.py @@ -52,7 +52,7 @@ def __init__( @property def datapoints(self) -> int: return len(self.files) - + def __len__(self): return self.datapoints diff --git a/nilmth/data/temporal.py b/nilmth/data/temporal.py index 0abf9de..ce4815b 100644 --- a/nilmth/data/temporal.py +++ b/nilmth/data/temporal.py @@ -22,14 +22,14 @@ def generate_temporal_data(loader: DataLoader, path: str = "data_temp"): os.mkdir(path) # Initialize the file number and the list of files file_num = 0 - files = loader.dataset.files.copy() + files = loader.files.copy() # Iterate through the whole dataloader for data, target_power, target_status in iter(loader): data = data.cpu().detach().numpy() target_power = target_power.cpu().detach().numpy() target_status = target_status.cpu().detach().numpy() # Add the border for appliance power and status - npad = ((0, 0), (loader.dataset.border, loader.dataset.border), (0, 0)) + npad = ((0, 0), (loader.border, loader.border), (0, 0)) target_power = np.pad( target_power, pad_width=npad, mode="constant", constant_values=0 ) From fc263f9bf735b1fb705b0bef2a0581612868869e Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 15:54:27 +0200 Subject: [PATCH 07/47] get_appliance_power_series to get_appliance_series --- nilmth/data/dataloader.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/nilmth/data/dataloader.py b/nilmth/data/dataloader.py index f548015..e90811e 100644 --- a/nilmth/data/dataloader.py +++ b/nilmth/data/dataloader.py @@ -53,8 +53,23 @@ def __repr__(self): """This message is returned any time the object is called""" return f"Dataloader > {self.dataset}" - def get_appliance_power_series(self, app: Union[str, int]) -> np.array: - """Returns the full series of power of an appliance""" + def get_appliance_series( + self, app: Union[str, int], target: str = "power" + ) -> np.array: + """Returns the full series of power of an appliance + + Parameters + ---------- + app : str + Appliance label + target : str, optional + Target value (power or status), by default "power" + + Returns + ------- + numpy.array + + """ if type(app) == str: try: app_idx = self.appliances.index(app) @@ -65,8 +80,14 @@ def get_appliance_power_series(self, app: Union[str, int]) -> np.array: # Initialize list ser = [0] * self.__len__() # Loop through the set and extract all the values - for idx, (_, meters, _) in enumerate(self): - ser[idx] = meters[:, :, app_idx].flatten() + if target.lower() == "power": + for idx, (_, meters, _) in enumerate(self): + ser[idx] = meters[:, :, app_idx].flatten() + elif target.lower() == "status": + for idx, (_, _, meters) in enumerate(self): + ser[idx] = meters[:, :, app_idx].flatten() + else: + raise ValueError(f"Target not available: {target}") return np.concatenate(ser) def compute_thresholds(self): @@ -85,7 +106,7 @@ def compute_thresholds(self): logger.debug("Threshold values not found. Computing them...") # Loop through each appliance for app_idx, app in enumerate(self.appliances): - ser = self.get_appliance_power_series(app_idx) + ser = self.get_appliance_series(app_idx) # Concatenate all values and update the threshold self.threshold.update_appliance_threshold(ser, app) # Write the config file From 5ddb3e9bd56a85046bf6c2ae7581b87177e78814 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 16:11:47 +0200 Subject: [PATCH 08/47] get_appliance_series can return reconstructed power --- nilmth/data/dataloader.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/nilmth/data/dataloader.py b/nilmth/data/dataloader.py index e90811e..f19631c 100644 --- a/nilmth/data/dataloader.py +++ b/nilmth/data/dataloader.py @@ -37,6 +37,10 @@ def threshold(self) -> Threshold: def appliances(self) -> list: return self.dataset.appliances + @property + def num_apps(self) -> int: + return self.dataset.num_apps + @property def status(self) -> list: return self.dataset.status @@ -56,14 +60,15 @@ def __repr__(self): def get_appliance_series( self, app: Union[str, int], target: str = "power" ) -> np.array: - """Returns the full series of power of an appliance + """Returns the full series of an appliance, being it power, status or + reconstructed power Parameters ---------- app : str Appliance label target : str, optional - Target value (power or status), by default "power" + Target value (power, status or reconstructed), by default "power" Returns ------- @@ -79,16 +84,30 @@ def get_appliance_series( app_idx = app # Initialize list ser = [0] * self.__len__() - # Loop through the set and extract all the values - if target.lower() == "power": + # Set target string to lowercase + target = target.lower() + # Choose the target, then loop through the set and extract all the values + # Power + if target.startswith("p"): for idx, (_, meters, _) in enumerate(self): ser[idx] = meters[:, :, app_idx].flatten() - elif target.lower() == "status": + return np.concatenate(ser) + # Status or reconstructed power (taken from status) + elif target.startswith("s") | target.startswith("r"): for idx, (_, _, meters) in enumerate(self): ser[idx] = meters[:, :, app_idx].flatten() + # If status, return it directly + if target.startswith("s"): + return np.concatenate(ser) + # If reconstructed power, compute its value from status + else: + ser = np.concatenate(ser) + ser = np.repeat( + np.expand_dims(ser, axis=1), repeats=self.num_apps, axis=1 + ) + return self.dataset.status_to_power(ser)[:, app_idx] else: raise ValueError(f"Target not available: {target}") - return np.concatenate(ser) def compute_thresholds(self): """Compute the thresholds of each appliance""" From 086f4668a602c5ac5b6317ec4be6c1e70a8e914e Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 17:05:12 +0200 Subject: [PATCH 09/47] Threshold method to set threshold and centroid values --- nilmth/data/threshold.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/nilmth/data/threshold.py b/nilmth/data/threshold.py index 39bd2aa..0514b0a 100644 --- a/nilmth/data/threshold.py +++ b/nilmth/data/threshold.py @@ -20,6 +20,7 @@ class Threshold: + num_status: int = 2 thresholds: np.array = None # (appliance, status) centroids: np.array = None # (appliance, status) use_std: bool = False @@ -314,3 +315,29 @@ def write_config(self, path_config: str): config = dict_config store_config(path_config, config) logging.debug(f"Config stored at {path_config}\n") + + def set_thresholds_and_centroids( + self, thresholds: np.array, centroids: np.array + ): + """Change threshold and centroid values to given ones + + Parameters + ---------- + thresholds : numpy.array + New threshold values + centroids : numpy.array + New centroid values + + """ + assert len(thresholds.shape), "Array must have two dimensions" + assert ( + thresholds.shape[0] == self.num_apps + ), f"Axis 0 must have length {self.num_apps}" + assert ( + thresholds.shape == centroids.shape + ), "Both arrays must have same dimension" + self.num_status = thresholds.shape[1] + self.thresholds = thresholds + self.centroids = centroids + self.method = "custom" + self._status_fun = self._compute_status From 510e54fcf187e74d287cc8ea4a3647fde53ed8ed Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 17:18:29 +0200 Subject: [PATCH 10/47] Change param name length to len_series --- nilmth/data/dataset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nilmth/data/dataset.py b/nilmth/data/dataset.py index 7b99cdc..11d6703 100644 --- a/nilmth/data/dataset.py +++ b/nilmth/data/dataset.py @@ -32,7 +32,7 @@ def __init__( ): self.subset = subset self.border = border - self.length = input_len + self.len_series = input_len self.buildings = {} if buildings is None else buildings[subset] self.train_size = train_size self.validation_size = valid_size @@ -58,7 +58,7 @@ def __len__(self): def __repr__(self): """This message is returned any time the object is called""" - return f"Dataset | Data points: {self.datapoints} | Input length: {self.length}" + return f"Dataset | Data points: {self.datapoints} | Input length: {self.len_series}" @staticmethod def _open_file(path_file: str) -> pd.DataFrame: @@ -105,9 +105,9 @@ def _get_parameters_from_file(self): self.appliances = sorted(appliances) self.num_apps = len(appliances) self.status = [app + "_status" for app in self.appliances] - self.length = df.shape[0] + self.len_series = df.shape[0] self._idx_start = self.border - self._idx_end = self.length - self.border + self._idx_end = self.len_series - self.border def power_to_status(self, ser: np.array) -> np.array: """Computes the status assigned to each power value From 3f7dc85163df172acf44e059a5aaede95bfa2299 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 17:48:33 +0200 Subject: [PATCH 11/47] Optimize status_to_power method --- nilmth/data/dataset.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nilmth/data/dataset.py b/nilmth/data/dataset.py index 11d6703..fed8179 100644 --- a/nilmth/data/dataset.py +++ b/nilmth/data/dataset.py @@ -140,10 +140,7 @@ def status_to_power(self, ser: np.array) -> np.array: """ # Get power values from status - power = np.multiply(np.ones(ser.shape), self.threshold.centroids[:, 0]) - power_on = np.multiply(np.ones(ser.shape), self.threshold.centroids[:, 1]) - power[ser == 1] = power_on[ser == 1] - return power + return np.take_along_axis(self.threshold.centroids, ser.T, axis=1).T def __getitem__(self, index: int) -> tuple: """Returns an element of the data loader From 39589e5b0fa967ccbc74c6c805696d1c6ba9e9e4 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 20 May 2021 18:02:33 +0200 Subject: [PATCH 12/47] Dataset takes the list of appliances as input --- nilmth/data/dataset.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/nilmth/data/dataset.py b/nilmth/data/dataset.py index fed8179..56ba9bb 100644 --- a/nilmth/data/dataset.py +++ b/nilmth/data/dataset.py @@ -12,13 +12,13 @@ class DataSet(data.Dataset): files: list = list() appliances: list = list() - num_apps: int = 0 status: list = list() threshold: Threshold = None def __init__( self, path_data: str, + appliances: list = None, subset: str = "train", input_len: int = 510, border: int = 15, @@ -31,6 +31,7 @@ def __init__( **kwargs, ): self.subset = subset + self.appliances = appliances self.border = border self.len_series = input_len self.buildings = {} if buildings is None else buildings[subset] @@ -53,12 +54,19 @@ def __init__( def datapoints(self) -> int: return len(self.files) + @property + def num_apps(self) -> int: + return len(self.appliances) + def __len__(self): return self.datapoints def __repr__(self): """This message is returned any time the object is called""" - return f"Dataset | Data points: {self.datapoints} | Input length: {self.len_series}" + return ( + f"Dataset | Data points: {self.datapoints} | " + f"Input length: {self.len_series}" + ) @staticmethod def _open_file(path_file: str) -> pd.DataFrame: @@ -100,10 +108,16 @@ def _list_files(self, path_data: str): def _get_parameters_from_file(self): """Updates class parameters from sample csv file""" df = self._open_file(self.files[0]) + # List appliances in file appliances = [t for t in df.columns if not t.endswith("_status")] - appliances.remove("aggregate") - self.appliances = sorted(appliances) - self.num_apps = len(appliances) + # Ensure our list of appliances is contained in the dataset + # If we have no list, take the whole dataset + if self.appliances is None: + self.appliances = appliances + else: + for app in self.appliances: + assert app in appliances, f"Appliance missing in dataset: {app}" + # List the status columns self.status = [app + "_status" for app in self.appliances] self.len_series = df.shape[0] self._idx_start = self.border From 9e9a4ba433c79f2838dd9ed238da2765fb8dff68 Mon Sep 17 00:00:00 2001 From: Daniel Precioso Date: Tue, 1 Jun 2021 17:08:13 +0200 Subject: [PATCH 13/47] Rename class according to script. Make abstract method --- nilmth/data/preprocessing.py | 8 +++++--- nilmth/data/ukdale.py | 6 +++--- nilmth/preprocess.py | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/nilmth/data/preprocessing.py b/nilmth/data/preprocessing.py index 519eccb..18ef06e 100644 --- a/nilmth/data/preprocessing.py +++ b/nilmth/data/preprocessing.py @@ -1,5 +1,6 @@ import logging import os +from abc import abstractmethod import pandas as pd @@ -8,8 +9,8 @@ logging.basicConfig(level=logging.DEBUG, format="%(message)s") -class PreprocessWrapper: - dataset: str = "wrapper" +class Preprocessing: + dataset: str = "Wrapper" def __init__( self, @@ -64,9 +65,10 @@ def __init__( f" {', '.join(kwargs.keys())}\n" ) + @abstractmethod def load_house_meters(self, house: int) -> pd.DataFrame: """Placeholder function, this should load the household meters and status""" - return pd.DataFrame() + pass def store_preprocessed_data(self, path_output: str): """Stores preprocessed data in output folder""" diff --git a/nilmth/data/ukdale.py b/nilmth/data/ukdale.py index 6604374..a2e6fb8 100644 --- a/nilmth/data/ukdale.py +++ b/nilmth/data/ukdale.py @@ -4,16 +4,16 @@ from pandas import Series from pandas.io.pytables import HDFStore -from nilmth.data.preprocessing import PreprocessWrapper +from nilmth.data.preprocessing import Preprocessing from nilmth.utils.string import APPLIANCE_NAMES, homogenize_string -class UkdalePreprocess(PreprocessWrapper): +class Ukdale(Preprocessing): dataset: str = "ukdale" datastore: HDFStore = None def __init__(self, path_h5: str, path_labels: str, **kwargs): - super(UkdalePreprocess, self).__init__(**kwargs) + super(Ukdale, self).__init__(**kwargs) self._path_h5 = path_h5 self._path_labels = path_labels # Load the datastore diff --git a/nilmth/preprocess.py b/nilmth/preprocess.py index e745c27..b6df9b7 100644 --- a/nilmth/preprocess.py +++ b/nilmth/preprocess.py @@ -2,7 +2,7 @@ import typer -from nilmth.data.ukdale import UkdalePreprocess +from nilmth.data.ukdale import Ukdale from nilmth.utils.config import load_config from nilmth.utils.logging import logger @@ -13,7 +13,7 @@ def preprocess_ukdale(path_data: str, path_output: str, config: dict): path_h5 = path_ukdale + ".h5" - prep = UkdalePreprocess(path_h5, path_ukdale, **config) + prep = Ukdale(path_h5, path_ukdale, **config) prep.store_preprocessed_data(path_output) From b8492d10c388e0b3d33b28b32965a683d6778a9d Mon Sep 17 00:00:00 2001 From: Daniel Precioso Date: Tue, 1 Jun 2021 17:18:56 +0200 Subject: [PATCH 14/47] Homogenize label outside ukdale --- nilmth/data/ukdale.py | 8 +++----- nilmth/utils/string.py | 6 ++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/nilmth/data/ukdale.py b/nilmth/data/ukdale.py index a2e6fb8..67147bb 100644 --- a/nilmth/data/ukdale.py +++ b/nilmth/data/ukdale.py @@ -5,7 +5,7 @@ from pandas.io.pytables import HDFStore from nilmth.data.preprocessing import Preprocessing -from nilmth.utils.string import APPLIANCE_NAMES, homogenize_string +from nilmth.utils.string import homogenize_label class Ukdale(Preprocessing): @@ -76,16 +76,14 @@ def _datastore_to_series(self, house: int, label: str) -> pd.Series: ).to_dict()[1] # Homogenize input label - label = homogenize_string(label) - label = APPLIANCE_NAMES.get(label, label) + label = homogenize_label(label) # Series placeholder s = None # Iterate through all the existing labels, searching for the input label for i in labels: - lab = homogenize_string(labels[i]) - lab = APPLIANCE_NAMES.get(lab, lab) + lab = homogenize_label(labels[i]) # When we find the input label, we load the meter records if lab == label: s = self._load_meter(house, i) diff --git a/nilmth/utils/string.py b/nilmth/utils/string.py index 8e814ec..fc04488 100644 --- a/nilmth/utils/string.py +++ b/nilmth/utils/string.py @@ -72,3 +72,9 @@ def homogenize_string(string, remove_dieresis_u=True): string_low_deaccent = deaccent( string_low, remove_dieresis_u=remove_dieresis_u) return string_low_deaccent + + +def homogenize_label(label: str) -> str: + """Homogenizes the input label (which is the name of an appliance or meter)""" + label = homogenize_string(label) + return APPLIANCE_NAMES.get(label, label) From a170a7274054a65a133424538a816481cfcede93 Mon Sep 17 00:00:00 2001 From: Daniel Precioso Date: Tue, 1 Jun 2021 17:21:31 +0200 Subject: [PATCH 15/47] Optimize ukdale for loop --- nilmth/data/ukdale.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nilmth/data/ukdale.py b/nilmth/data/ukdale.py index 67147bb..638ba71 100644 --- a/nilmth/data/ukdale.py +++ b/nilmth/data/ukdale.py @@ -78,17 +78,14 @@ def _datastore_to_series(self, house: int, label: str) -> pd.Series: # Homogenize input label label = homogenize_label(label) - # Series placeholder - s = None - # Iterate through all the existing labels, searching for the input label for i in labels: lab = homogenize_label(labels[i]) # When we find the input label, we load the meter records if lab == label: s = self._load_meter(house, i) - - if s is None: + break + else: raise ValueError( f"Label {label} not found on house {house}\n" f"Valid labels are: {list(labels.values())}" From d5a6345594efc1bcee7dcdad031e56751d9d386d Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 2 Jun 2021 13:04:59 +0200 Subject: [PATCH 16/47] Hierarchical clustering class --- nilmth/data/clustering.py | 112 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 nilmth/data/clustering.py diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py new file mode 100644 index 0000000..0d078a1 --- /dev/null +++ b/nilmth/data/clustering.py @@ -0,0 +1,112 @@ +import itertools + +import matplotlib.pyplot as plt +import numpy as np +from scipy.cluster.hierarchy import dendrogram, linkage, fcluster, cophenet +from scipy.spatial.distance import pdist + +from nilmth.utils.plot import plot_power_distribution + + +class HierarchicalClustering: + x: np.array = None + z: np.array = None + thresh: np.array = None + centroids: np.array = None + dendrogram: dict = None + + def __init__( + self, method: str = "average", n_cluster: int = 2, criterion: str = "maxclust" + ): + self.method = method + self.n_cluster = n_cluster + self.criterion = criterion + + def perform_clustering(self, ser: np.array, method: str = None): + """Performs the actual clustering, using the linkage function + + Returns + ------- + numpy.array + Z[i] will tell us which clusters were merged in the i-th iteration + """ + if method is not None: + self.method = method + # The shape of our X matrix must be (n, m) + # n = samples, m = features + self.x = np.expand_dims(ser, axis=1) + self.z = linkage(self.x, method=self.method) + + @property + def cophenet(self): + # Cophenet correlation coefficient + c, coph_dists = cophenet(self.z, pdist(self.x)) + return c + + def plot_dendrogram(self, p=6, max_d=None, figsize=(3, 3)): + fig, ax = plt.subplots(figsize=figsize) + self.dendrogram = dendrogram( + self.z, + p=p, + orientation="right", + truncate_mode="lastp", + labels=self.x[:, 0], + ax=ax, + ) + if max_d is not None: + ax.axvline(x=max_d, c="k") + return fig, ax + + @property + def dendrogram_distance(self): + return sorted(set(itertools.chain(*self.dendrogram["dcoord"])), reverse=True) + + def plot_dendrogram_distance(self, figsize=(10, 3)): + # Initialize plots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) + # Dendrogram distance + ax1.scatter( + range(2, len(self.dendrogram_distance) + 1), self.dendrogram_distance[:-1] + ) + ax1.set_ylabel("Distance") + ax1.set_xlabel("Number of clusters") + ax1.grid() + # Dendrogram distance difference + diff = np.divide( + -np.diff(self.dendrogram_distance), self.dendrogram_distance[:-1] + ) + ax2.scatter(range(3, len(self.dendrogram_distance) + 1), diff[:-1]) + ax2.set_ylabel("Gradient") + ax2.set_xlabel("Number of clusters") + ax2.grid() + return fig, (ax1, ax2) + + def compute_thresholds_and_centroids( + self, n_cluster: int = None, criterion: str = None, centroid: str = "median" + ): + if n_cluster is not None: + self.n_cluster = n_cluster + if criterion is not None: + self.criterion = criterion + clusters = fcluster(self.z, self.n_cluster, self.criterion) + # Get centroids + if centroid == "median": + fun = np.median + elif centroid == "mean": + fun = np.mean + self.centroids = np.array( + sorted([fun(self.x[clusters == (c + 1)]) for c in range(self.n_cluster)]) + ) + # Sort clusters by power + x_max = sorted( + [np.max(self.x[clusters == (c + 1)]) for c in range(self.n_cluster)] + ) + x_min = sorted( + [np.min(self.x[clusters == (c + 1)]) for c in range(self.n_cluster)] + ) + self.thresh = np.divide(np.array(x_min[1:]) + np.array(x_max[:-1]), 2) + + def plot_cluster_distribution(self, label="", bins=100): + fig, ax = plot_power_distribution(self.x, label, bins=bins) + [ax.axvline(t, color="r", linestyle="--") for t in self.thresh] + return fig, ax From 2c853f39bad3d96702f53b547cc9f60dc2b3c841 Mon Sep 17 00:00:00 2001 From: daniprec Date: Tue, 3 Aug 2021 17:18:44 +0200 Subject: [PATCH 17/47] Ignore ipynb files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cb7a515..b81b090 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ notebooks/ plots/ output*/ *.csv +*.ipynb # Log log.out From 3ca8e417f84dd0352de9a2046f154cbf504201af Mon Sep 17 00:00:00 2001 From: daniprec Date: Tue, 3 Aug 2021 18:01:49 +0200 Subject: [PATCH 18/47] Docstring --- README.md | 8 ++-- nilmth/data/clustering.py | 79 +++++++++++++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6d415bd..7266014 100644 --- a/README.md +++ b/README.md @@ -106,19 +106,19 @@ If you want to use your own set of parameters, duplicate the aforementioned configuration file and modify the paremeters you want to change (without deleting any parameter). You can then use that config file with the following command: - ``` +``` python nilmth/train.py --path_config ``` For more information about the script, run: - ``` +``` python nilmth/train.py --help ``` Once the models are trained, test them with: - ``` +``` python nilmth/test.py --path_config ``` @@ -136,7 +136,7 @@ models are stored. Then, the script `train.py` will be called, using each configuration each. This will store the model weights, which will be used again during the test phase: - ``` +``` nohup sh test_sequential.sh > log.out & ``` diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index 0d078a1..48dd071 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -1,11 +1,11 @@ import itertools +from typing import Optional, Tuple import matplotlib.pyplot as plt import numpy as np -from scipy.cluster.hierarchy import dendrogram, linkage, fcluster, cophenet -from scipy.spatial.distance import pdist - from nilmth.utils.plot import plot_power_distribution +from scipy.cluster.hierarchy import cophenet, dendrogram, fcluster, linkage +from scipy.spatial.distance import pdist class HierarchicalClustering: @@ -22,16 +22,24 @@ def __init__( self.n_cluster = n_cluster self.criterion = criterion - def perform_clustering(self, ser: np.array, method: str = None): + def perform_clustering( + self, ser: np.array, method: Optional[str] = None + ) -> np.array: """Performs the actual clustering, using the linkage function + Parameters + ---------- + ser : np.array + Series of points to group in clusters + method : str, optional + Clustering method, by default None (takes the one from the class) + Returns ------- - numpy.array + np.array Z[i] will tell us which clusters were merged in the i-th iteration """ - if method is not None: - self.method = method + self.method = method if method is not None else self.method # The shape of our X matrix must be (n, m) # n = samples, m = features self.x = np.expand_dims(ser, axis=1) @@ -43,7 +51,20 @@ def cophenet(self): c, coph_dists = cophenet(self.z, pdist(self.x)) return c - def plot_dendrogram(self, p=6, max_d=None, figsize=(3, 3)): + def plot_dendrogram( + self, p: int = 6, max_d: Optional[float] = None, figsize: Tuple[int] = (3, 3) + ): + """Plots the dendrogram + + Parameters + ---------- + p : int, optional + Last split, by default 6 + max_d : Optional[float], optional + Maximum distance between splits, by default None + figsize : Tuple[int], optional + Figure size, by default (3, 3) + """ fig, ax = plt.subplots(figsize=figsize) self.dendrogram = dendrogram( self.z, @@ -61,7 +82,14 @@ def plot_dendrogram(self, p=6, max_d=None, figsize=(3, 3)): def dendrogram_distance(self): return sorted(set(itertools.chain(*self.dendrogram["dcoord"])), reverse=True) - def plot_dendrogram_distance(self, figsize=(10, 3)): + def plot_dendrogram_distance(self, figsize: Tuple[int] = (10, 3)): + """Plots the dendrogram distances + + Parameters + ---------- + figsize : Tuple[int], optional + Size of the figure, by default (10, 3) + """ # Initialize plots fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) # Dendrogram distance @@ -82,12 +110,24 @@ def plot_dendrogram_distance(self, figsize=(10, 3)): return fig, (ax1, ax2) def compute_thresholds_and_centroids( - self, n_cluster: int = None, criterion: str = None, centroid: str = "median" + self, + n_cluster: Optional[int] = None, + criterion: Optional[str] = None, + centroid: str = "median", ): - if n_cluster is not None: - self.n_cluster = n_cluster - if criterion is not None: - self.criterion = criterion + """Computes the thresholds and centroids of each group + + Parameters + ---------- + n_cluster : Optional[int], optional + Number of clusters, by default None + criterion : Optional[str], optional + Criterion used to compute the clusters, by default None + centroid : str, optional + Method to compute the centroids (median or mean), by default "median" + """ + self.n_cluster = n_cluster if n_cluster is not None else self.n_cluster + self.criterion = criterion if criterion is not None else self.criterion clusters = fcluster(self.z, self.n_cluster, self.criterion) # Get centroids if centroid == "median": @@ -106,7 +146,16 @@ def compute_thresholds_and_centroids( ) self.thresh = np.divide(np.array(x_min[1:]) + np.array(x_max[:-1]), 2) - def plot_cluster_distribution(self, label="", bins=100): + def plot_cluster_distribution(self, label: str = "", bins: int = 100): + """Plots the power distribution, and the lines splitting each cluster + + Parameters + ---------- + label : str, optional + Label of the distribution, by default "" + bins : int, optional + Number of bins, by default 100 + """ fig, ax = plot_power_distribution(self.x, label, bins=bins) [ax.axvline(t, color="r", linestyle="--") for t in self.thresh] return fig, ax From e4207dd8c5407e3c91ed3c9aa3f2fed2745b6ffc Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 4 Aug 2021 12:48:30 +0200 Subject: [PATCH 19/47] Move plot_cluster_distribution to plot --- nilmth/data/clustering.py | 14 -------------- nilmth/utils/plot.py | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index 48dd071..026fc84 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -145,17 +145,3 @@ def compute_thresholds_and_centroids( [np.min(self.x[clusters == (c + 1)]) for c in range(self.n_cluster)] ) self.thresh = np.divide(np.array(x_min[1:]) + np.array(x_max[:-1]), 2) - - def plot_cluster_distribution(self, label: str = "", bins: int = 100): - """Plots the power distribution, and the lines splitting each cluster - - Parameters - ---------- - label : str, optional - Label of the distribution, by default "" - bins : int, optional - Number of bins, by default 100 - """ - fig, ax = plot_power_distribution(self.x, label, bins=bins) - [ax.axvline(t, color="r", linestyle="--") for t in self.thresh] - return fig, ax diff --git a/nilmth/utils/plot.py b/nilmth/utils/plot.py index 584b0bf..999a690 100644 --- a/nilmth/utils/plot.py +++ b/nilmth/utils/plot.py @@ -1,3 +1,4 @@ +from typing import Iterable import numpy as np from matplotlib import pyplot as plt @@ -69,7 +70,7 @@ def plot_real_vs_prediction( def plot_power_distribution( - ser: np.array, app: str, bins: int = 20, figsize: tuple = (3, 3) + ser: np.array, app: str = "", bins: int = 20, figsize: tuple = (3, 3) ): """Plot the histogram of power distribution @@ -77,8 +78,8 @@ def plot_power_distribution( ---------- ser : numpy.array Contains all the power values - app : label - Name of the appliance + app : str, optional + Name of the appliance, by default "" bins : int, optional Histogram splits, by default 20 figsize : tuple, optional @@ -97,3 +98,30 @@ def plot_power_distribution( ax.set_ylabel("Frequency") ax.grid() return fig, ax + + +def plot_cluster_distribution( + ser: np.array, + thresh: Iterable[float], + app: str = "", + bins: int = 20, + figsize: tuple = (3, 3), +): + """Plots the power distribution, and the lines splitting each cluster + + Parameters + ---------- + ser : numpy.array + Contains all the power values + thresh : Iterable[float] + Contains all the threshold values + app : str, optional + Name of the appliance, by default "" + bins : int, optional + Histogram splits, by default 20 + figsize : tuple, optional + Figure size, by default (3, 3) + """ + fig, ax = plot_power_distribution(ser, app=app, bins=bins, figsize=figsize) + [ax.axvline(t, color="r", linestyle="--") for t in thresh] + return fig, ax From b5be502c85aa433c8aa4af7d46bd75467af3c516 Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 4 Aug 2021 12:48:44 +0200 Subject: [PATCH 20/47] Script to compute hie clust --- nilmth/hierarchical_clustering.py | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 nilmth/hierarchical_clustering.py diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py new file mode 100644 index 0000000..7dcc1ee --- /dev/null +++ b/nilmth/hierarchical_clustering.py @@ -0,0 +1,101 @@ +import os + +import matplotlib.pyplot as plt +import numpy as np +import typer + +from nilmth.data.clustering import HierarchicalClustering +from nilmth.data.dataloader import DataLoader +from nilmth.utils.config import load_config +from nilmth.utils.scores import regression_scores_dict + +LIST_APPLIANCES = ["dish_washer", "fridge", "washing_machine"] +LIST_CLUSTER = [2, 3, 4, 5, 6] +LIST_LINKAGE = [ + "average", + "weighted", + "centroid", + "median", + "ward", # Ward variance minimization algorithm +] + + +def main( + limit: int = 20000, + path_data: str = "data-prep", + path_threshold: str = "threshold.toml", + path_config: str = "nilmth/config.toml", + path_output: str = "outputs/hieclust", +): + + # Read config file + config = load_config(path_config, "model") + + # Create output path + if not os.path.exists(path_output): + os.mkdir(path_output) + + for app in LIST_APPLIANCES: + config["appliances"] = [app] + + # Prepare data loader with train data + dl = DataLoader( + path_data=path_data, + subset="train", + shuffle=False, + path_threshold=path_threshold, + **config, + ) + + # Take an appliance series + ser = dl.get_appliance_series(app)[:limit] + # Name + appliance = app.capitalize().replace("_", " ") + + for method in LIST_LINKAGE: + # Clustering + hie = HierarchicalClustering() + hie.perform_clustering(ser, method=method) + # Initialize the list of intrinsic error + # per number of clusters + intr_error = [0] * len(LIST_CLUSTER) + # Compute thresholds per number of clusters + for idx, n_cluster in enumerate(LIST_CLUSTER): + hie.compute_thresholds_and_centroids(n_cluster=n_cluster) + # Update thresholds and centroids + thresh = np.insert(np.expand_dims(hie.thresh, axis=0), 0, 0, axis=1) + centroids = np.expand_dims(hie.centroids, axis=0) + dl.threshold.set_thresholds_and_centroids(thresh, centroids) + # Create the dictionary of power series + power = np.expand_dims(ser, axis=1) + sta = dl.dataset.power_to_status(power) + recon = dl.dataset.status_to_power(sta) + dict_app = {appliance: {"power": power, "power_pred": recon}} + # Compute the scores + dict_scores = regression_scores_dict(dict_app) + intr_error[idx] = dict_scores[appliance]["nde"] + # Initialize plots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) + fig.suptitle(f"{appliance}, Linkage: {method}") + # Plot intrinsic error + ax1.plot(LIST_CLUSTER, intr_error, ".--") + ax1.set_title(f"Intrinsic Error") + ax1.set_ylabel("NDE") + ax1.set_xlabel("Number of status") + ax1.grid() + # Plot proportional reduction + rel_error = -100 * np.divide(np.diff(intr_error), intr_error[:-1]) + ax2.plot(LIST_CLUSTER[1:], rel_error, ".--") + ax2.set_title("Reduction of Intrinsic Error") + ax2.set_ylabel("Reduction (%)") + ax2.set_xlabel("Number of status") + ax2.set_ylim(0, 100) + ax2.grid() + # Save and close the figure + path_fig = os.path.join(path_output, f"{app}_{method}.png") + fig.savefig(path_fig) + plt.close(fig) + + +if __name__ == "__main__": + typer.run(main) From e26a0bfd0be85c9494a798530d914c703f302474 Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 4 Aug 2021 13:18:45 +0200 Subject: [PATCH 21/47] Move plot to hieclust script --- nilmth/hierarchical_clustering.py | 88 +++++++++++++++++++++++++------ nilmth/utils/plot.py | 28 ---------- 2 files changed, 72 insertions(+), 44 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index 7dcc1ee..13964ec 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -1,8 +1,11 @@ import os +from typing import Iterable import matplotlib.pyplot as plt import numpy as np import typer +from matplotlib import pyplot as plt +from matplotlib.axes import Axes from nilmth.data.clustering import HierarchicalClustering from nilmth.data.dataloader import DataLoader @@ -20,6 +23,70 @@ ] +def plot_intrinsic_error(intr_error: Iterable[float], ax: Axes): + """Plots the intrinsic error depending on the number of splits + + Parameters + ---------- + intr_error : Iterable[float] + List of intrinsic error values + ax : Axes + Axes where the graph is plotted + """ + ax.plot(LIST_CLUSTER, intr_error, ".--") + ax.set_ylabel("Intrinsic Error (NDE)") + ax.set_xlabel("Number of status") + ax.grid() + + +def plot_error_reduction(intr_error: Iterable[float], ax: Axes): + """Plots the intrinsic error reduction depending on the number of splits + + Parameters + ---------- + intr_error : Iterable[float] + List of intrinsic error values + ax : Axes + Axes where the graph is plotted + """ + rel_error = -100 * np.divide(np.diff(intr_error), intr_error[:-1]) + ax.plot(LIST_CLUSTER[1:], rel_error, ".--") + ax.set_ylabel("Reduction of Intrinsic Error (%)") + ax.set_xlabel("Number of status") + ax.set_ylim(0, 100) + ax.grid() + + +def plot_cluster_distribution( + ser: np.array, + thresh: Iterable[float], + ax: Axes, + app: str = "", + bins: int = 100, +): + """Plots the power distribution, and the lines splitting each cluster + + Parameters + ---------- + ser : numpy.array + Contains all the power values + thresh : Iterable[float] + Contains all the threshold values + ax : Axes + Axes where the graph is plotted + app : str, optional + Name of the appliance, by default "" + bins : int, optional + Histogram splits, by default 100 + """ + ax.hist(ser, bins=bins) + ax.set_title(app.capitalize().replace("_", " ")) + ax.set_xlabel("Power (watts)") + ax.set_ylabel("Frequency") + ax.grid() + [ax.axvline(t, color="r", linestyle="--") for t in thresh] + + def main( limit: int = 20000, path_data: str = "data-prep", @@ -27,7 +94,6 @@ def main( path_config: str = "nilmth/config.toml", path_output: str = "outputs/hieclust", ): - # Read config file config = load_config(path_config, "model") @@ -75,22 +141,12 @@ def main( dict_scores = regression_scores_dict(dict_app) intr_error[idx] = dict_scores[appliance]["nde"] # Initialize plots - fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) + fig, axis = plt.subplots(2, 2, figsize=(12, 8)) fig.suptitle(f"{appliance}, Linkage: {method}") - # Plot intrinsic error - ax1.plot(LIST_CLUSTER, intr_error, ".--") - ax1.set_title(f"Intrinsic Error") - ax1.set_ylabel("NDE") - ax1.set_xlabel("Number of status") - ax1.grid() - # Plot proportional reduction - rel_error = -100 * np.divide(np.diff(intr_error), intr_error[:-1]) - ax2.plot(LIST_CLUSTER[1:], rel_error, ".--") - ax2.set_title("Reduction of Intrinsic Error") - ax2.set_ylabel("Reduction (%)") - ax2.set_xlabel("Number of status") - ax2.set_ylim(0, 100) - ax2.grid() + # Plots + plot_cluster_distribution(hie.x, hie.thresh, axis[0, 0]) + plot_intrinsic_error(intr_error, axis[1, 0]) + plot_error_reduction(intr_error, axis[1, 1]) # Save and close the figure path_fig = os.path.join(path_output, f"{app}_{method}.png") fig.savefig(path_fig) diff --git a/nilmth/utils/plot.py b/nilmth/utils/plot.py index 999a690..9a120a4 100644 --- a/nilmth/utils/plot.py +++ b/nilmth/utils/plot.py @@ -1,4 +1,3 @@ -from typing import Iterable import numpy as np from matplotlib import pyplot as plt @@ -98,30 +97,3 @@ def plot_power_distribution( ax.set_ylabel("Frequency") ax.grid() return fig, ax - - -def plot_cluster_distribution( - ser: np.array, - thresh: Iterable[float], - app: str = "", - bins: int = 20, - figsize: tuple = (3, 3), -): - """Plots the power distribution, and the lines splitting each cluster - - Parameters - ---------- - ser : numpy.array - Contains all the power values - thresh : Iterable[float] - Contains all the threshold values - app : str, optional - Name of the appliance, by default "" - bins : int, optional - Histogram splits, by default 20 - figsize : tuple, optional - Figure size, by default (3, 3) - """ - fig, ax = plot_power_distribution(ser, app=app, bins=bins, figsize=figsize) - [ax.axvline(t, color="r", linestyle="--") for t in thresh] - return fig, ax From ff1b687999acc2b432e96a2d6af267468be04dbd Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 4 Aug 2021 13:39:48 +0200 Subject: [PATCH 22/47] Improve plots and add docstring --- nilmth/hierarchical_clustering.py | 33 ++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index 13964ec..76a7002 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -79,12 +79,14 @@ def plot_cluster_distribution( bins : int, optional Histogram splits, by default 100 """ - ax.hist(ser, bins=bins) + y, x, _ = ax.hist(ser, bins=bins) ax.set_title(app.capitalize().replace("_", " ")) ax.set_xlabel("Power (watts)") ax.set_ylabel("Frequency") ax.grid() - [ax.axvline(t, color="r", linestyle="--") for t in thresh] + for idx, t in enumerate(thresh): + ax.axvline(t, color="r", linestyle="--") + ax.text(t + 0.01 * x.max(), y.max(), idx, rotation=0, color="r") def main( @@ -94,6 +96,23 @@ def main( path_config: str = "nilmth/config.toml", path_output: str = "outputs/hieclust", ): + """Performs the hierarchical clustering on the given list of appliances, + testing different linkaged methods and number of splits. For each combination, + outputs an image with several informative graphs. + + Parameters + ---------- + limit : int, optional + Number of data points to use, by default 20000 + path_data : str, optional + Path to the preprocessed data folder, by default "data-prep" + path_threshold : str, optional + Path to the threshold configuration, by default "threshold.toml" + path_config : str, optional + Path to the config file, by default "nilmth/config.toml" + path_output : str, optional + Path to the outputs folder, by default "outputs/hieclust" + """ # Read config file config = load_config(path_config, "model") @@ -122,9 +141,10 @@ def main( # Clustering hie = HierarchicalClustering() hie.perform_clustering(ser, method=method) - # Initialize the list of intrinsic error - # per number of clusters + # Initialize the list of intrinsic error per number of clusters intr_error = [0] * len(LIST_CLUSTER) + # Initialize the empty list of thresholds (sorted) + thresh_sorted = [] # Compute thresholds per number of clusters for idx, n_cluster in enumerate(LIST_CLUSTER): hie.compute_thresholds_and_centroids(n_cluster=n_cluster) @@ -140,11 +160,14 @@ def main( # Compute the scores dict_scores = regression_scores_dict(dict_app) intr_error[idx] = dict_scores[appliance]["nde"] + # Update the sorted list of thresholds + thresh = list(set(hie.thresh) - set(thresh_sorted)) + thresh_sorted.append(thresh[0]) # Initialize plots fig, axis = plt.subplots(2, 2, figsize=(12, 8)) fig.suptitle(f"{appliance}, Linkage: {method}") # Plots - plot_cluster_distribution(hie.x, hie.thresh, axis[0, 0]) + plot_cluster_distribution(hie.x, thresh_sorted, axis[0, 0]) plot_intrinsic_error(intr_error, axis[1, 0]) plot_error_reduction(intr_error, axis[1, 1]) # Save and close the figure From 8fc1470381bd88e9888c06363337a549da1b5a50 Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 3 Sep 2021 13:18:43 +0200 Subject: [PATCH 23/47] Update README --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++------ images/logo.png | Bin 0 -> 108608 bytes 2 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 images/logo.png diff --git a/README.md b/README.md index 6d415bd..c0d7bcc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,66 @@ -# NILM: classification VS regression + + + + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![LinkedIn][linkedin-shield]][linkedin-url] + + +
+

+ + Logo + + +

NILM: classification VS regression

+

+ + + +
+ Table of Contents +
    +
  1. + About The Project +
  2. +
  3. + Getting Started + +
  4. +
  5. + Data + + +
  6. +
  7. + Train + +
  8. +
  9. Publications
  10. +
  11. Contact
  12. +
  13. Acknowledgements
  14. +
+
+ +## About the project Non-Intrusive Load Monitoring (NILM) aims to predict the status or consumption of domestic appliances in a household only by knowing @@ -14,10 +76,10 @@ deep learning state-of-the-art architectures on both the regression and classification problems, introducing criteria to select the most convenient thresholding method. -Source: [see publications](#publications) +## Getting started +### Create the Environment -## Set up -### Create the environment using Conda +To create the environment using Conda: 1. Install miniconda @@ -122,7 +184,7 @@ Once the models are trained, test them with: python nilmth/test.py --path_config ``` -#### Reproduce paper +### Reproduce the Paper To reproduce the results shown in [our paper](#publications), activate the environment and then run: @@ -140,7 +202,7 @@ models are stored. Then, the script `train.py` will be called, using each nohup sh test_sequential.sh > log.out & ``` -### Thresholding methods +### Thresholding Methods There are three threshold methods available. Read [our paper](#publications) to understand how each threshold works. @@ -151,13 +213,32 @@ to understand how each threshold works. ## Publications -[NILM as a regression versus classification problem: +* [NILM as a regression versus classification problem: the importance of thresholding](https://www.researchgate.net/project/Non-Intrusive-Load-Monitoring-6) -## Contact information +## Contact -Author: Daniel Precioso, PhD student at Universidad de Cádiz -- Email: daniel.precioso@uca.es -- [Github](https://github.com/daniprec) -- [LinkedIn](https://www.linkedin.com/in/daniel-precioso-garcelan/) -- [ResearchGate](https://www.researchgate.net/profile/Daniel_Precioso_Garcelan) +Daniel Precioso - [daniprec](https://github.com/daniprec) - daniel.precioso@uca.es + +Project link: [https://github.com/UCA-Datalab/nilm-thresholding](https://github.com/UCA-Datalab/nilm-thresholding) + +ResearhGate link: [https://www.researchgate.net/project/NILM-classification-VS-regression](https://www.researchgate.net/project/NILM-classification-VS-regression) + +## Acknowledgements + +* [UCA DataLab](http://datalab.uca.es/) +* [David Gómez-Ullate](https://www.linkedin.com/in/david-g%C3%B3mez-ullate-oteiza-87a820b/?originalSubdomain=en) + + + + +[contributors-shield]: https://img.shields.io/github/contributors/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[contributors-url]: https://github.com/UCA-Datalab/nilm-thresholding/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[forks-url]: https://github.com/UCA-Datalab/nilm-thresholding/network/members +[stars-shield]: https://img.shields.io/github/stars/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[stars-url]: https://github.com/UCA-Datalab/nilm-thresholding/stargazers +[issues-shield]: https://img.shields.io/github/issues/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[issues-url]: https://github.com/UCA-Datalab/nilm-thresholding/issues +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 +[linkedin-url]: https://www.linkedin.com/in/daniel-precioso-garcelan/ \ No newline at end of file diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..07dbbaff4d7aa56dbefd08c6af8389fc60a98976 GIT binary patch literal 108608 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�fr=fx)|jG>XgRLEUY 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 literal 0 HcmV?d00001 From 17df2749aed3291e3cd8f1caa31e6dbf6bcf44b6 Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 3 Sep 2021 13:24:21 +0200 Subject: [PATCH 24/47] Update README --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c0d7bcc..62e8b4e 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,14 @@

  • - Data + Datasets +
  • Preprocess the Data
  • Train @@ -105,12 +106,10 @@ To create the environment using Conda: conda activate nilm-thresholding ``` -## Data +## Datasets ### UK-DALE -#### Download UK-DALE - UK-DALE dataset is hosted on the following link: [https://data.ukedc.rl.ac.uk/browse/edc/efficiency/residential /EnergyConsumption/Domestic/UK-DALE-2017/UK-DALE-FULL-disaggregated](https://data.ukedc.rl.ac.uk/browse/edc/efficiency/residential/EnergyConsumption/Domestic/UK-DALE-2017/UK-DALE-FULL-disaggregated) @@ -131,7 +130,15 @@ nilm-thresholding Credit: [Jack Kelly](https://jack-kelly.com/data/) -### Preprocess +### Pecan Street Dataport + +We are aiming to include this dataset in a future release. You can check the issue here: [https://github.com/UCA-Datalab/nilm-thresholding/issues/8](https://github.com/UCA-Datalab/nilm-thresholding/issues/8) + +Any help and suggestions are welcome! + +Credit: [Pecan Street](https://dataport.pecanstreet.org/) + +## Preprocess the Data Once downloaded the raw data from any of the sources above, you must preprocess it. From cd0b6227fc07978d20b6b2153d13bf16e3e1b6b9 Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 22 Sep 2021 12:59:22 +0200 Subject: [PATCH 25/47] Change url of image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62e8b4e..84181c6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

    - + Logo From 33f7d9c93eda3b8d4a75d938b27ca04cd246b3bf Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 23 Sep 2021 13:54:58 +0200 Subject: [PATCH 26/47] Move part of main function to new function --- nilmth/hierarchical_clustering.py | 110 ++++++++++++++++++------------ 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index 76a7002..cc2cd88 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -1,5 +1,5 @@ import os -from typing import Iterable +from typing import Iterable, Optional, Tuple import matplotlib.pyplot as plt import numpy as np @@ -23,32 +23,36 @@ ] -def plot_intrinsic_error(intr_error: Iterable[float], ax: Axes): +def plot_intrinsic_error(intr_error: Iterable[float], ax: Optional[Axes] = None): """Plots the intrinsic error depending on the number of splits Parameters ---------- intr_error : Iterable[float] List of intrinsic error values - ax : Axes + ax : Axes, optional Axes where the graph is plotted """ + if ax is None: + ax = plt.gca() ax.plot(LIST_CLUSTER, intr_error, ".--") ax.set_ylabel("Intrinsic Error (NDE)") ax.set_xlabel("Number of status") ax.grid() -def plot_error_reduction(intr_error: Iterable[float], ax: Axes): +def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None): """Plots the intrinsic error reduction depending on the number of splits Parameters ---------- intr_error : Iterable[float] List of intrinsic error values - ax : Axes + ax : Axes, optional Axes where the graph is plotted """ + if ax is None: + ax = plt.gca() rel_error = -100 * np.divide(np.diff(intr_error), intr_error[:-1]) ax.plot(LIST_CLUSTER[1:], rel_error, ".--") ax.set_ylabel("Reduction of Intrinsic Error (%)") @@ -60,7 +64,7 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Axes): def plot_cluster_distribution( ser: np.array, thresh: Iterable[float], - ax: Axes, + ax: Optional[Axes] = None, app: str = "", bins: int = 100, ): @@ -72,13 +76,15 @@ def plot_cluster_distribution( Contains all the power values thresh : Iterable[float] Contains all the threshold values - ax : Axes + ax : Axes, optional Axes where the graph is plotted app : str, optional Name of the appliance, by default "" bins : int, optional Histogram splits, by default 100 """ + if ax is None: + ax = plt.gca() y, x, _ = ax.hist(ser, bins=bins) ax.set_title(app.capitalize().replace("_", " ")) ax.set_xlabel("Power (watts)") @@ -89,6 +95,52 @@ def plot_cluster_distribution( ax.text(t + 0.01 * x.max(), y.max(), idx, rotation=0, color="r") +def plot_clustering_results(ser: np.array, dl: DataLoader, method: str = "average"): + """Plots the results of applying a certain clustering method + on the given series + + Parameters + ---------- + ser : np.array + Contains all the power values + dl : DataLoader + Required to apply the thresholds on the series + method : str, optional + Clustering method, by default "average" + """ + # Clustering + hie = HierarchicalClustering() + hie.perform_clustering(ser, method=method) + # Initialize the list of intrinsic error per number of clusters + intr_error = [0] * len(LIST_CLUSTER) + # Initialize the empty list of thresholds (sorted) + thresh_sorted = [] + # Compute thresholds per number of clusters + for idx, n_cluster in enumerate(LIST_CLUSTER): + hie.compute_thresholds_and_centroids(n_cluster=n_cluster) + # Update thresholds and centroids + thresh = np.insert(np.expand_dims(hie.thresh, axis=0), 0, 0, axis=1) + centroids = np.expand_dims(hie.centroids, axis=0) + dl.threshold.set_thresholds_and_centroids(thresh, centroids) + # Create the dictionary of power series + power = np.expand_dims(ser, axis=1) + sta = dl.dataset.power_to_status(power) + recon = dl.dataset.status_to_power(sta) + dict_app = {"app": {"power": power, "power_pred": recon}} + # Compute the scores + dict_scores = regression_scores_dict(dict_app) + intr_error[idx] = dict_scores["app"]["nde"] + # Update the sorted list of thresholds + thresh = list(set(hie.thresh) - set(thresh_sorted)) + thresh_sorted.append(thresh[0]) + # Initialize plots + fig, axis = plt.subplots(2, 2, figsize=(12, 8)) + # Plots + plot_cluster_distribution(power, thresh_sorted, ax=axis[0, 0]) + plot_intrinsic_error(intr_error, ax=axis[1, 0]) + plot_error_reduction(intr_error, ax=axis[1, 1]) + + def main( limit: int = 20000, path_data: str = "data-prep", @@ -120,6 +172,7 @@ def main( if not os.path.exists(path_output): os.mkdir(path_output) + # Loop through the list of appliances for app in LIST_APPLIANCES: config["appliances"] = [app] @@ -134,46 +187,17 @@ def main( # Take an appliance series ser = dl.get_appliance_series(app)[:limit] - # Name + # Appliance name appliance = app.capitalize().replace("_", " ") - + # Loop through methods for method in LIST_LINKAGE: - # Clustering - hie = HierarchicalClustering() - hie.perform_clustering(ser, method=method) - # Initialize the list of intrinsic error per number of clusters - intr_error = [0] * len(LIST_CLUSTER) - # Initialize the empty list of thresholds (sorted) - thresh_sorted = [] - # Compute thresholds per number of clusters - for idx, n_cluster in enumerate(LIST_CLUSTER): - hie.compute_thresholds_and_centroids(n_cluster=n_cluster) - # Update thresholds and centroids - thresh = np.insert(np.expand_dims(hie.thresh, axis=0), 0, 0, axis=1) - centroids = np.expand_dims(hie.centroids, axis=0) - dl.threshold.set_thresholds_and_centroids(thresh, centroids) - # Create the dictionary of power series - power = np.expand_dims(ser, axis=1) - sta = dl.dataset.power_to_status(power) - recon = dl.dataset.status_to_power(sta) - dict_app = {appliance: {"power": power, "power_pred": recon}} - # Compute the scores - dict_scores = regression_scores_dict(dict_app) - intr_error[idx] = dict_scores[appliance]["nde"] - # Update the sorted list of thresholds - thresh = list(set(hie.thresh) - set(thresh_sorted)) - thresh_sorted.append(thresh[0]) - # Initialize plots - fig, axis = plt.subplots(2, 2, figsize=(12, 8)) - fig.suptitle(f"{appliance}, Linkage: {method}") - # Plots - plot_cluster_distribution(hie.x, thresh_sorted, axis[0, 0]) - plot_intrinsic_error(intr_error, axis[1, 0]) - plot_error_reduction(intr_error, axis[1, 1]) + plot_clustering_results(ser, dl, method=method) + # Place title in figure + plt.gcf().suptitle(f"{appliance}, Linkage: {method}") # Save and close the figure path_fig = os.path.join(path_output, f"{app}_{method}.png") - fig.savefig(path_fig) - plt.close(fig) + plt.savefig(path_fig) + plt.close() if __name__ == "__main__": From 0fecba9969c916fb841e72bc08cda9ee4ae1a4ec Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 23 Sep 2021 16:03:12 +0200 Subject: [PATCH 27/47] Include new plot and improve all --- nilmth/hierarchical_clustering.py | 114 ++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index cc2cd88..f2a8fa1 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -1,5 +1,5 @@ import os -from typing import Iterable, Optional, Tuple +from typing import Iterable, Optional import matplotlib.pyplot as plt import numpy as np @@ -23,6 +23,75 @@ ] +def plot_thresholds_on_distribution( + ser: np.array, + thresh: Iterable[float], + ax: Optional[Axes] = None, + app: str = "", + bins: int = 100, +): + """Plots the power distribution, and the lines splitting each cluster + + Parameters + ---------- + ser : numpy.array + Contains all the power values + thresh : Iterable[float] + Contains all the threshold values + ax : Axes, optional + Axes where the graph is plotted + app : str, optional + Name of the appliance, by default "" + bins : int, optional + Histogram splits, by default 100 + """ + if ax is None: + ax = plt.gca() + y, x, _ = ax.hist(ser, bins=bins) + ax.set_title(app.capitalize().replace("_", " ")) + ax.set_xlabel("Power (watts)") + ax.set_ylabel("Frequency") + ax.set_title("Thresholds on power distribution") + ax.grid() + # Plot the thresholds + for idx, t in enumerate(thresh): + ax.axvline(t, color="r", linestyle="--") + ax.text(t + 0.01 * x.max(), y.max(), idx + 1, rotation=0, color="r") + + +def plot_thresholds_on_series( + ser: np.array, thresh: Iterable[float], size: int = 500, ax: Optional[Axes] = None +): + """Plots the thresholds on a sample time series + + Parameters + ---------- + ser : numpy.array + Contains all the power values + thresh : Iterable[float] + Contains all the threshold values + ax : Axes, optional + Axes where the graph is plotted + """ + if ax is None: + ax = plt.gca() + # Take sample of the series and plot it + idx = np.argmax(ser) + idx_min = int(max(0, idx - size / 2)) + idx_max = int(min(len(ser), idx + size / 2)) + power = ser[idx_min:idx_max] + time = np.arange(0, len(power)) * 6 + ax.plot(time, power) + # Plot the thresholds + for idx, thresh in enumerate(thresh): + ax.axhline(thresh, color="r", linestyle="--") + ax.text(time.max(), thresh + power.max() * 0.01, idx + 1, rotation=0, color="r") + ax.set_xlabel("Time (s)") + ax.set_ylabel("Power (watts)") + ax.set_title("Thresholds on sample time series") + ax.grid() + + def plot_intrinsic_error(intr_error: Iterable[float], ax: Optional[Axes] = None): """Plots the intrinsic error depending on the number of splits @@ -38,6 +107,7 @@ def plot_intrinsic_error(intr_error: Iterable[float], ax: Optional[Axes] = None) ax.plot(LIST_CLUSTER, intr_error, ".--") ax.set_ylabel("Intrinsic Error (NDE)") ax.set_xlabel("Number of status") + ax.set_title("Intrinsic Error depending on splits") ax.grid() @@ -55,46 +125,13 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None) ax = plt.gca() rel_error = -100 * np.divide(np.diff(intr_error), intr_error[:-1]) ax.plot(LIST_CLUSTER[1:], rel_error, ".--") + ax.set_ylim(0, 100) ax.set_ylabel("Reduction of Intrinsic Error (%)") ax.set_xlabel("Number of status") - ax.set_ylim(0, 100) + ax.set_title("Error reduction with each subsequent split") ax.grid() -def plot_cluster_distribution( - ser: np.array, - thresh: Iterable[float], - ax: Optional[Axes] = None, - app: str = "", - bins: int = 100, -): - """Plots the power distribution, and the lines splitting each cluster - - Parameters - ---------- - ser : numpy.array - Contains all the power values - thresh : Iterable[float] - Contains all the threshold values - ax : Axes, optional - Axes where the graph is plotted - app : str, optional - Name of the appliance, by default "" - bins : int, optional - Histogram splits, by default 100 - """ - if ax is None: - ax = plt.gca() - y, x, _ = ax.hist(ser, bins=bins) - ax.set_title(app.capitalize().replace("_", " ")) - ax.set_xlabel("Power (watts)") - ax.set_ylabel("Frequency") - ax.grid() - for idx, t in enumerate(thresh): - ax.axvline(t, color="r", linestyle="--") - ax.text(t + 0.01 * x.max(), y.max(), idx, rotation=0, color="r") - - def plot_clustering_results(ser: np.array, dl: DataLoader, method: str = "average"): """Plots the results of applying a certain clustering method on the given series @@ -136,9 +173,12 @@ def plot_clustering_results(ser: np.array, dl: DataLoader, method: str = "averag # Initialize plots fig, axis = plt.subplots(2, 2, figsize=(12, 8)) # Plots - plot_cluster_distribution(power, thresh_sorted, ax=axis[0, 0]) + plot_thresholds_on_distribution(power, thresh_sorted, ax=axis[0, 0]) + plot_thresholds_on_series(power, thresh_sorted, ax=axis[0, 1]) plot_intrinsic_error(intr_error, ax=axis[1, 0]) plot_error_reduction(intr_error, ax=axis[1, 1]) + # Set the space between subplots + fig.tight_layout(rect=[0, 0.03, 1, 0.95]) def main( From a40d96a34d9de1f49789891836111d4b60eb0a65 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 23 Sep 2021 17:48:26 +0200 Subject: [PATCH 28/47] Change the range of power distribution --- nilmth/hierarchical_clustering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index f2a8fa1..0a1d294 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -47,11 +47,11 @@ def plot_thresholds_on_distribution( """ if ax is None: ax = plt.gca() - y, x, _ = ax.hist(ser, bins=bins) + y, x, _ = ax.hist(ser, bins=bins, range=(3, ser.max())) ax.set_title(app.capitalize().replace("_", " ")) ax.set_xlabel("Power (watts)") ax.set_ylabel("Frequency") - ax.set_title("Thresholds on power distribution") + ax.set_title("Thresholds on power distribution (>=3 watts)") ax.grid() # Plot the thresholds for idx, t in enumerate(thresh): From 3d24f8558f9ad8bfd97f736bfca225e919093567 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 23 Sep 2021 17:53:03 +0200 Subject: [PATCH 29/47] Typo in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 84181c6..fcf4fc8 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ If you want to use your own set of parameters, duplicate the aforementioned configuration file and modify the paremeters you want to change (without deleting any parameter). You can then use that config file with the following command: - ``` +``` python nilmth/train.py --path_config ``` @@ -187,7 +187,7 @@ python nilmth/train.py --help Once the models are trained, test them with: - ``` +``` python nilmth/test.py --path_config ``` @@ -205,7 +205,7 @@ models are stored. Then, the script `train.py` will be called, using each configuration each. This will store the model weights, which will be used again during the test phase: - ``` +``` nohup sh test_sequential.sh > log.out & ``` From 191ab0cc38e05c885a918fc129cee81eb0c93321 Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 3 Sep 2021 13:18:43 +0200 Subject: [PATCH 30/47] Update README --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++------ images/logo.png | Bin 0 -> 108608 bytes 2 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 images/logo.png diff --git a/README.md b/README.md index 7266014..d6a8f59 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,66 @@ -# NILM: classification VS regression + + + + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![LinkedIn][linkedin-shield]][linkedin-url] + + +
    +

    + + Logo + + +

    NILM: classification VS regression

    +

    + + + +
    + Table of Contents +
      +
    1. + About The Project +
    2. +
    3. + Getting Started + +
    4. +
    5. + Data + + +
    6. +
    7. + Train + +
    8. +
    9. Publications
    10. +
    11. Contact
    12. +
    13. Acknowledgements
    14. +
    +
    + +## About the project Non-Intrusive Load Monitoring (NILM) aims to predict the status or consumption of domestic appliances in a household only by knowing @@ -14,10 +76,10 @@ deep learning state-of-the-art architectures on both the regression and classification problems, introducing criteria to select the most convenient thresholding method. -Source: [see publications](#publications) +## Getting started +### Create the Environment -## Set up -### Create the environment using Conda +To create the environment using Conda: 1. Install miniconda @@ -122,7 +184,7 @@ Once the models are trained, test them with: python nilmth/test.py --path_config ``` -#### Reproduce paper +### Reproduce the Paper To reproduce the results shown in [our paper](#publications), activate the environment and then run: @@ -140,7 +202,7 @@ models are stored. Then, the script `train.py` will be called, using each nohup sh test_sequential.sh > log.out & ``` -### Thresholding methods +### Thresholding Methods There are three threshold methods available. Read [our paper](#publications) to understand how each threshold works. @@ -151,13 +213,32 @@ to understand how each threshold works. ## Publications -[NILM as a regression versus classification problem: +* [NILM as a regression versus classification problem: the importance of thresholding](https://www.researchgate.net/project/Non-Intrusive-Load-Monitoring-6) -## Contact information +## Contact -Author: Daniel Precioso, PhD student at Universidad de Cádiz -- Email: daniel.precioso@uca.es -- [Github](https://github.com/daniprec) -- [LinkedIn](https://www.linkedin.com/in/daniel-precioso-garcelan/) -- [ResearchGate](https://www.researchgate.net/profile/Daniel_Precioso_Garcelan) +Daniel Precioso - [daniprec](https://github.com/daniprec) - daniel.precioso@uca.es + +Project link: [https://github.com/UCA-Datalab/nilm-thresholding](https://github.com/UCA-Datalab/nilm-thresholding) + +ResearhGate link: [https://www.researchgate.net/project/NILM-classification-VS-regression](https://www.researchgate.net/project/NILM-classification-VS-regression) + +## Acknowledgements + +* [UCA DataLab](http://datalab.uca.es/) +* [David Gómez-Ullate](https://www.linkedin.com/in/david-g%C3%B3mez-ullate-oteiza-87a820b/?originalSubdomain=en) + + + + +[contributors-shield]: https://img.shields.io/github/contributors/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[contributors-url]: https://github.com/UCA-Datalab/nilm-thresholding/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[forks-url]: https://github.com/UCA-Datalab/nilm-thresholding/network/members +[stars-shield]: https://img.shields.io/github/stars/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[stars-url]: https://github.com/UCA-Datalab/nilm-thresholding/stargazers +[issues-shield]: https://img.shields.io/github/issues/UCA-Datalab/nilm-thresholding.svg?style=for-the-badge +[issues-url]: https://github.com/UCA-Datalab/nilm-thresholding/issues +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 +[linkedin-url]: https://www.linkedin.com/in/daniel-precioso-garcelan/ \ No newline at end of file diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..07dbbaff4d7aa56dbefd08c6af8389fc60a98976 GIT binary patch literal 108608 zcmb5Wc{tSn*FTO3BT-q(5-KHY$&!7jD9S#>7)xbu$U63|q^y-hWZ!2P$!^9FSt3gq zlNoCghB0FujO91)&wYQd>;B_@-`D*9FcTc$f2n}p0~1!xA>E9(wO&~xHL3TA$yDJm zxV-_WYL9j(TG@9F%>m0MGa%z;v-xGuW?@m_o#*=P^v^Y1PUF*IyT zdGdyDKNFKU)4kiemO=I_lboMKKuw2h79g!ut(AmJ9nu^uEHQvfLHwo@vkFB1afyTi zLA}doPTyvoefcysO6LB!VHXTF}`AwE`u+Vs(j%j?W_+1N4bWlLH*zS2V{ zv0$-_ZDSz#`berXg+n}wiG`7mqT6jo-*28h&cd-NaqaQn3YTdrn6JQdP18HHj!?yuuBDi=Zj-&XPX;bF`ec`6MjmP+25bi;b#Zv5wKHEvUxjwGxPAfcJu?+p8mEG!ft;$lGKOo5< zpd~BdPyySK#PxR|xT0ZsU5Fuh|MT)BDc}zBly$aRsc|0Z{kVn{CGfM~R5vKYOO@@n zzGY0{JRo2#gLdYN@Q*(gRlRjGPMV?ei3QGkZ+7a=JCr+4iQxr*fYmaVH_o&20-2Z@ z`B=XU+>!T|=?znqyyiDneuh6p!)mrZWk>gOdGB~9;j0yVRFc1bnXCMCSIwcaykH>c z8?7d%)ozZq*1qdE{-Ivsw!Hykuj1{gql?x~K_S(sum`EVWv-8o~`W>!?)?bsMK_?j! z9bLrHXaFy!Z@E{o%98U(hMH#>FKZX#I}#)Ew=AUuClO=C|0+u0Ci;~ zH2bmc$vi}w&Bnm)bnb~w^-n2``Z4eWE6236d)--27gm=6^{sF?N5ij;{cNM+eM{6b zlv(zLEBL4WK;`RpiYMNd`eU1I!=6aeLce7A1y*eI7X-|og-Np+@3g-@T>M#RGOSYL z3Qsa)l1s;~hO}2%ey!onfzLoncOxTa_nUaO{Iv%e*8zyG0~?51SGIcexf`xYW{Xde z#{*+xhop6DN4b?|na9zj&rr9m9o<+;$&Xx(g_kc-^Xm+w&8Ve#nY*|DOQ10u-uPon zc?>Mw12KNl&}s3#x@TEDY*`*5ec0(|y*Tjw@x=vn%KeO+K1=ACI_3|f*> z9$n4H?fgsZAk1v!ddr>_8eqDm^`We~%oX?98hssUX3cQXtGPTR4X59!fSH53pO6e4 z24M@RP1o`KW6_UyvRV1i1l!p!V$wkkCL-vXAI)u~tc@ivZYvYOBS(ihE|4W|DDpO@fWGxJUX0P8?X`C&I1ksn7&SWa zp&BN_!1mJ)84k3*0_JsCu{6d6Z^$(m;<3!mf)en{W29re8)-AA27p!#|3{b#4 z6?)k4n7glVPC!bD2L=yXAAKj+B|cxyOst6-ZH8Go*LFmrwO zgQ<-3it^VAp??nn@p4w3sLNS^Srk!j5kVMKsVo(r(kF-Erf?I1%W`I0Av1;yi{jEn z;5_PyU6Y3~YwhHU+k^S1gH(eLh}!ulE=wO@vQgqYjk%N~n#;?VW;PQO-!hs4{-eQJ zR1P$VjhWyT_ztgK{OD~wjB}a?Fg?CqQz}Dg)NP0O1T&Il{KqR_$^v)1dS2LxSFu1# ze@q$rX^cxx`CO85#F7n*-*Lq_s%quDd+Kc`%CO<)bMaz?n!1J^i-2)oUeaE~)*xmcK!XpvN`vry}3EGHPPUd_hn1 z_aN(2n+#&E_%$VZt+|nUb#OnDu4;DM|1bOOSeU$sUA6S~|txHS><>Ho52B z`H}mA>B(?WeJl#Eu%nJ8cOj^QqYO8ey5u0ALv$ zR!@>O2xoiBZTb4yvC8kdTcVsT45tm1aL4=Qkblba)W%83QXvF>4o9x;`IG zif3jNXq9T|cKgyk;_{Jt^mD$pjTJS*_WME%%i35CT(4}71i0-5n*KSY!EmUl;ujC= z7LAT#b@}m0+1z2f8V<9h#|ODnSHl)|4=)3LlhSg`uAgR556rBeE{1hOqM}&&R`FI2 zfgC95N~d?HnYr|iN9A`syDYToe~)MpXQz9ZGS9R?j~T@!|w?aozTS zY*acim<5c|wpV3Z!tF`G<~xY2z|UVDmyyI#RfOtb^q(K;H||JvXAPuK%1t z{~GF{8?aFD*2hrUA$*{r?pI{C+#lzpix8IifbqKy4aT^hv)l|S0eF#)Y@UCxuB+8i zF|g%w=$#$;Y}FT)MtQe9Yc13bdde8Sq8a^)7m@0I5iM<(oz8l>NGQ~2hig4JXsxP9 z7;~ZRrMx`FctDUeWnzie7o>zo8l6kL5SBim@s;K6Jp2W?)bB+ifI(NvJH=KOhI@q= z_~dXg?-{NU33;IP#9Z3+Gqa&oBIjx}Qw5?M z*Rzio0%Dh!)-q}n6MPuGpHe;jVO{o-F4K~qqx~#fjJEoD?-+kYnF$ptpJdqFiSqO+ ze$}Xri71!X3j<8w8K_s8zgr3nCXCMtO}_F^e#3AMrIblOkUrSwFCn|FXEo()uxs8)mG2j;5bMp6t-mG;A-kf~mQbDbnz)o7>n;M(v2V48@d2CSq7m zh_kWy*eECab4iJ~FMqQw4*hsmlrgg&y7rHUxRif%Q(%$o3rIsCwtR&Yuts@k?YDb8 z%gV?1?l2mC93gb@oZ#VB$aBOIUmEl)EbyR)0?!jG zZnosQ#qvmm;lq>nkV{J7V0r5Yd586y}}0>yB8=S%pl9q`-5qtETNU?&ZT9O zIhs1>!<~V#+obAUzlC4w411cmDU4Zq8XL}e7TChZw;FQ)D^iei5*sQRvf3(w2`yT< zK~Fjv^tfm|or6JCsUeEJk0*9$CDP<^3MKn<(lRm@2ID=+?rn95Di&&@N~9r)!CpgdMZMd)4NM<-5$@MHoLc z2Ul*#;}Za{$wf=IffdsJ_y{!IypEy?D~BtT}H8xVaB(wYr*r|$@> zFC;ySu&|Cr)jeLis$)H1xTlNz_!eFp>r@&ODr3jHPk?G3ig^(WT9-Rp%MKCFdsg{s1jDh+cYgAASHuK$aNTQo$A_sKEn5K) zW$!Eey|fJY%y{R~oPP2lsy#@R=z)_dg2jOq=ZdwF6Q30jzbCUXWS!EXNqvEx-*;1@ z*d(xC7FdmKR^!1h*BH!y@@DBN3ur?iXb(JUj4d7?;90fcC0qCILFUFP_ES-t3E9|BAt#Z} zgv_ZzrYY2ThzbNzz9n^)H`t*kql_U}kz;IJh5U$n? zO7j6qPhpI@PlMU?VkH6)`bXx3(64B}M(dGkC5E$XAWOjA7xUi^8r9gW&#q~68iRUQ zfzYcOa724HDbc3%Go-4v{!C?Mqx71MvbIJ$#VP41bZ-xe`?;55KRv~yFhKW$h|Is< zY}hAFcL=NR3nN$rFcxRcgMa>}@}o5>A};2;GKee8tW#2L^)=o~O9vmnJFI=u*?Vs2 zl3jF-a0r*Oe$@~r=OKuzbE;H&S>MY~k&U(<4uV$Xv}VTyl`eht@gn|~sRgaF0cN!( zG=LEfbY`jbxbCwmGCRubKwfnDDYIs*c;tuVpVt5kAIF|wDjoQ|$brQ|fK)g$Y2v?X z!fzMaDqX~HcQ0Q<gVU?O!F^8r1R5n#n(G!qmQtu28=jzWd)Iisok$tQcgHz6y8fdem^K- zyfMU12r;qs*m&(~+8RR09v3z?l2glGCNxEx%wgAB04z^0pKmQ~73}K_{)Y^VX}33! ziYl#ior8NeWmpSbaRLuWhfyy=)k5zGbPr}QN@K6f(diG&7>roZVQ0FNqz4$!!=tz&2Y{FI<)rA!~=Ob{dXDkmSG>d=4nZm%lmm;7?_5mTeiy4Z>H(GUdvr zzMq!$Ww8gB`w>SLl>0PYt{#=%4I;0re}6yNw{k32Y-`H|9tJb&jq9T@st+McItaqJ zbew?C%IH})UT@n**U^c8Im71eV_78{2;3HKkIW;@lobXnn7kp>d2B^Wz9h+SQ4 zW^oM8vcLTIF|(#9{WLl-(BO-bxk$5H3?ZtbDoqA!nCH;7qoOea6bw%DP5QkW^2WOrPI!DhELb) z^!L?%QA+WaW^K|mI_6Tr=qr;Xf%McS)G#!GD~1gJJ!vT@?dI%+(8s&f>rm0b@8bhq z5;kJ`o@~b+qU8_^Qaf9<3&9T7eMCM*)m1~R2Ade!l4k$+a$6L`QZ*EJD;p0~9ly#2 zGtF2rj3pn+Ou3A;Td^XWQ;EK>^=SBtyx+QBRnIjWHogf@PDkYS4L_r;Xv{|FqIEc8 zuQG?>X>7+APRK%|~zlxtPUp|&(wpwZQ8f1ripMqO~-94C0SNKU}ozoGbf zHAbli*VFPI>X)YCo!H?abpI+COFHrYW&!ru{1)uq;&m0TF^uzLr`*uJFbjNxDPVA^|4KHPu}O_Y0Ya~v*BXQdj- zzy`~S5JY`e9n@8gVH@jt0i1h5KB)tqz2=>~@to%j8nNE8 zOBAT>;PbLuDXAx0&z~noGCcJc%O}?z~^g98~&l_r|TRn)- zTA*n&qb1}&gHBmuQ3?ERnvt*4f0Gym4~|hg@v`8Qt;J`(1+M{9ki}1`ipEnmriq1n zEIv#M{>Y))*YDiHO|K7IYF2cFZMXL7eR$~g)&gr-$?sZ^q`Tfq&jha~&<~{j9cD{AZ&^o@W$VFs=_+bSOQ7*7mo&7H zXV74Hx53pQ>flOZ&f5!&*1Htj=rB5PeOc@np#E)^lKnPvIm|kXHvI`S`-3fZcCWdG zuyLT{3$tq|QOU-Yq=UVi)Fwi6!>e%HHDEU`tQOxlndwKCZOdM4W5>w_i#}S~=3h~S zW{s2u#tl0j7|}uZTvc=Y>S?Gdp+HNXa|*kcT+5WxM2oHDYXj$WlU(`RWL9g)FB}pA z?RfbDt$$xbMwJ5XE9)#K9VCNFtNIGA)yBhAi$dB?Fg}M6!SN@EU_*|y<6W86tIr~J zhrgM)3Ly*{({JZ}51G0L38weHtKP?Vo~h*~HAv@c)aQi6|deWo`g94fQsB1#=f#XymYQr+SZ|wa~ zp8S-lwE@_EFAo625=7fmFN<>3;4v@;ufv%h1;w2QQ&zX{%St`J%^)oS%oAhT<(d{% z>GSI_=*I@uoUwa&Aqba}CBA0S%<{(DM|UJ?HF^0kEhsWzaVDiI0@Yog$cqq`!@EOFfNAE*5G+a_!inq#d26dZ7 zMjLLS#Lt#r!kJqxUg9b9oMg1SMGfb+JC&yqw0-Wz6H zt$Eq_{eiJ~T-^?jOyUy`Q2cH&xOx zLD8GhC*Ja{c&T!kGRfh5FKg?UUSEC*q5SttnOlP`MRG_7={sJXTr!jJ4;JaMS2F>( z7=?wqcf}oMQRN9!5~b7SNmlsR6o{T`f1bSOvYDGN(h`m5$Az^+lw_#IoF<=CTCRod zPzJ9?!Qk+(P{=T~-qg-#aCg&g7u!fzm1$K+_ko0!&CidDN8ACVGJR+>2_}Tl{C)~S! z*r24NU?q47z-%xakbYOE4&zpwnG3A~OUI^2g{U^v)NH(?M>=ycrBENUzT+JLFB7@W z+n@#limy3)zTqQF&;#2GMyI50T<1x@UZk3L+yG?8!tw6_CvhI4X$Tk&V zG!e_wg}|xYTF$kk&YnqaU;G!>u_gcyquHmT^qXAD~e_uSPod* zb<#pEu~a(5=Vn#SqM;#C4ip(Vw1KN2`l}{FA=_alxcOZ#)@1EISpP-+J|8-5Cd?67 z>kl#?i8Z{39Cbb73fU1WzU<&HIAAx~&mY8KKcXxXsPOh3e9J70>w(SaUzSLW;j>d{ zMAA-MX!ZNls!Qg4TKV-U@M$e|O2QMG=s&IN+gNh)h-`_}iw3A2sK347+)ZghpT|D( zp)B&}P9$=zK_NrcARo7RUS`lU*l4E7gxIla8oLq*Vp1@bR8K1&$>^WfX4|T3h?+EY zah=svIE*wFGlMv3f;wxw$Rjn@p7*VjZ@Q09zZ^C|PHPm0e~Jmjn`=%Gg1^e!RzUfX z1WUj3k|RJa+}6TFH=VmbJ;IOB1CjT2(d>1|$T78srU7MYQ-W({Z{3;HTZ{l|bSBrI zi(#tnUpWL>RlhMOy`ez*$pz8=ZtgXCd5VvV2T{pO(;_|K4gXboC$?Vdl7CUVu7Qkt z?eqD#VYFyvQk6(EIKLYw{fhb^iApt`>WrNp4ysVEnuqK~WfkR%Ra82tR!>Iohb&9G zl?oEqW(6PH44F76T`o1vOLA|*9K`0>ukZA&z9eB9W$Q|m!3*EHREM}>d3zd`(1LIE zL$2uU4gRFMo66&&sd6joa#N~}n_qHhsHt@>wsoY{A~h$o|9LV)p;{crvelDoDdyi> zK$Y%aH-ZE`6zRDOe*xMum-)l=?-a2~fiFcVd<8KF-#~qbc=ctC^zhs4E*kf{Y(9bLu*bUtBlRxw$ zrTG^FNbKkVl0ama@GTz1_+&0K=3R2&i{gb%OztxK3QKIH1*H1=G*|x$BNy+91M)1z zCO)8(Q=NdP_x*~EV_@zM&DPJaD_phP@M3dZy-$1Nc^#7qCcIQuRwf}Z6~j2%XuSom z!e?Il;_;g+KF;a=m%2cl6E-@<=8bmIC@^*qOWeip`VHROQu$i-iSWhwD{Mq*>aiur z>$!4#fbHy!KDXk7=K~S4zj<>MA7$r*^UWenB+#)JOXNbuUk4aZDqk5CTiq`y^!GEd zAe1B8A;Ata9+mcC3LN7xLHY1mHR>^~hl}!VEjj_`PjE04Bo!szY~MGWew8YaXqE9> z0BY7K10LA!Pm8}H>uyO8G!LIz?07jzcfBvrrL$9ALIYDC0_|C4z=@Oi^#FfKe;mXa zeQ4Pm8tyiTsZz>{D7Ta*s++*8G&Z+XLx_9)GFB6`q^fM!t7W6-2M4@4^;J=Ye2rNo znitJ*Q9gm~8tx^8=%DNRzIx znn9wfK`d>UH-*nW;?=z7Wkl(0xlK2NBg?>6wl89stY$Zz{%xE`$8Rl>)DzInth6(s z^sy78JPPFDejQ7Y*fYWY`vHvzT+^lfNAyY0iy$Vl9H4`gkK8tY=;f+4W#yncO1-!* z{_2KHnj?fNp~*EKz&&cO5Jy$t+L6?vy=V`lE;nfeBoVd_rgOu$$0HrWoDAA;AO_|^ zBrkRlN&{?7E9y4`H*^^YJcljEf7`WeMF{1bcL1N5diXsL?5M6>D=?ZIdlk!I%6ft3Y4aknIO zu2{ia$~JPWLk$;f+7>zM4Cw6LBYL`A-|zl68MIIrX2@LEghvKsVF8mMK&G?T1+5b| zb5iVuT|krzRI>S^u~^{n7p~BoT3{0~9q&dNrQC8|6Kk2WJ-cx2R_^ilTzc|pt3x+0 zHzkkl$BNVhz^>UcQn@N#d_$s4`nb}N?xRQLAwSvav#x)0mW9fk?Zb2|>&iR$0-F>c z7n+cswbDldt4yVK(hP$DxI2C63Oov^KLw-7!Fk6*Bb@qPnf0&wJ3~#zLgQ@eP?`%~ zG9b$PQ!-bL$E%x1veioh#@C~pi~sI_uLlLqBKCH5RHmdgxy!#d0_@S;B=fYFn(cD# zEo#d4Cs-NsKXaLX4nDfAdNpvvwazC1@##Ij_`z=5@7rcI@CW>Z0fL{NN=Tn%-Q#;0 z;MJE^Yq5v}t*y>^z9*R9X|!V}ON*yT2X)keYwGh3pw|<%HGUU$pC_J?gIuWoavKFgI3 z87yfCinu4(GWj4=%%7%ia^Z7wRQ<=v>l_m)SuE5wP%l*3kTSqg@gk6q30ni1Yqa>A zY5Db8liy=pb5^r=w%vz1{$iTXe$+LRcs31ULm#s+xgPNIHusJ!bO8^VSM29H%TB3a zv3Dw1t*>e<9DF*1lG}m_9Al&e%R{n^b5D!HjCFkp2;WSQ2}0&VHji!_HO~EDO7n#b zWlV%iDyShV2Bu?5@Tyxkbsu<&^*tvW)u_*ge-9wosiez9QFCm!b@DJJwN+Jb>k*j~ zA%JOT*Hd$HZU^7Jeq|h~=I#0_GGtRqiNL@PSnS^|{y%QtEi($8ybuw_qk5@1Ub=$> zOfGq*_W0?C!Bab=n6~gzAO%SPHHa6mQNk2X&Olti8-byZT5>nh3R2&;2O2Z&tcFun z29Tz(n$yV+KB)v%r0T^3hN^Sm9_trj%m%tY?a!|~&t>yKw)@kS*xXY#o$WWH8bFZ) ze?N7ddRQHl@PMxw^$?n#1_4*74E$lgHe$%6KxvWVw?td=Q--%{l!_1J_Lz_KMBWYH zof`9tg}F3jW_Ow@xO3MSB4vQ<|7b_e`v%BNXy8N5_vuTow|HI2PYE;6xL_-Gr{BNU zbzN@n6NIY$pdR>fVW_L1Y%AF$aA5<#QZ5J}rLwyUg-5{-`x3fSNThu{=5O{)jN=VK z7dO;6?${l`jGF5B1Dp3q$GPS1?^K2YhpAumi8yk>gIZBX3zz!q6}!VOc^$oD$#2KL zlwVUkcG2aPGVj?_FDm<^sUOp_KMYvzAdiQ;v~Zai%J5r01)zD#Y-c{EM?@U#nYbI$ zvbJ{jr;&VdxG$j(pAGTYDn)nHXmIK$Gc=ZSkK>72Si{?T8Oik2ZxAztXBR?{y<*Jb zn}>g8b8+q_z>P5a+_D84=&DwA=O^m064d!!dgf{vti2R=_Tb}T%Z;x!n&dNpN~nOh zS{638A`#_DnCO`J6MOZp^5sSsJ0XUU`uHW&vqR(V>T2tJG2)@=C6ecM9@a*DXxTtJ z?Hv^!KRlFfeLRc?Mh}>IJofi=Umm#0`^#?Cx$ZdaOBb!6{kDRdAo7`!u^IeTp)E_* zCym1gfGIgeswtI?(0}s&HdgPQeGU1}hEcqAJ9W z3wviRw_=08qLi~ezi3N#G1h;>p6c=18MyNpeya^cq|3#bf47;Za8_BYd(F?FG0$BX zw16LpEuPFKlgnQ82e}evn#*K@1n=l-$`4w|sdy)0Fl8ILKh;v2uac-&6E_3}(++>f zd!7#v9JIICQ2SHVA$&zy4O^{wi%^2W)@MlusW!$cH_#UmlAo@3#obX|=mFEvw$43J*hV=(8k5bEyu6(+jyr}c{Qt)$16HWkRkl|VXk-q#Re-^IEIp}w(y8xVy&hDLF-G9ah zd#{C8!Gor5wph;=w0HC2M@FXnkip-w>9ZKin>*>N^B`E0jEkS&Nk?TWKA?uj9W`2L z9e~gBw3^M z+j%m2JN*$TE)Xt2vX|Gfw~yGb#n^`GlkU-)_x5e-j=GtN8T5t5e=lar>i+>*L+$ws zqh=)j{=ZwA2Y4i+-OhY2S1B?a)THq+UB?hA2C5dyCk8-f)ww3$!`vdnDt5cqnOoN{)Fy>kkRicI&Xuz(@rL5dtUt2;BtV47X~fcG-Ss# z5D{La49ln0PYWtTh^pH7G-FD%Xk~|Idy|ouwj?pd( z%vEOFkge_hKetVu=!tjCqVDe~_F!0>W4yk1R$f*(OWJQg1?Uem{{FF{^SeC4%39jv ziKj*2a-w+^^=4*{{YXH3y-X#f3v6;Ay3+ppEBrI8MlZ-Az%yRQC;dmFXR{C5EN{l3 z+Y>bos~CRAiw-Rubj+zH!m^8dlHAs(MEEC_Admk#B5#yYm6V}Hj?2mTI-%REd0wtryg6fHA_2F3z}!eP54*RI|#D4>jxi*0U0SWEnk8rg67*pz#If7cU@w)0cbXN_ zQvvgV3GG4qa~1kc!Wmco9WAe({s;BdSngkKetTT!{Y_mpd~ie@yyKuMbUrP&k^D(z zN3QsCLn|1!2RDbHI`dJcxGK=%>K2FRtstK-Ps3v=#<18DPI~|89DcGCdEG5!CN#3&}d(-)g(xwUP_m`~-I*_G{7+7f((txsFf&3cka| zW!-A)ibQ9e_~)=~rfhujBY_L6S{`ENv{!hQTP-fhQ{HJuQ)XTcqd39&EpYh3N4yFD zw@`TlKWFLxBUgFz=sy|C!kqMNk#T`n>DSizJ`is4M9k*#M;nK5b{RnsLIhe?)=Fms?-i2wfX({Da@jyj{XjlL-ew- z_$k$m-gnL>!j`L&3+9wRFDaj$IKIY{JGtS$vx^veF}M3o&|+RRg#xLwYdZO#yLqzm zi3^up(JtFQr;PAbJzWsNnTWMHs+x%RMeC+S76qoo1es8m0iRv~yIB2mTbsgiOQC<_ zhzqN;wcPP*|7NcDIG?D7Ed;||c+;(j@wYBXehWzDFMHvqm!`9U21=lBLHI;wv{5_0 zf*&xe>MokU7MPO%tKD&@vU`zBx^s` zbj1e#_c(M}d=!h)Hv2+^hd?8(C^Ke7e%}!50-AH zTuo6Z?s)w0Uu+i`z(%h+5(=*H*W2FlEp1l4Jy}(+@}gAE_M)mWbKMAEJ4#mqeS^Q9 z@Rxf|Set0cMs_L3XXk!|%0rXi4x5_YG~WPM$hoD2bfA6rzx6px4*w2}+qFO6S6B?2 zEkIt#&kYIoNr1YhPdMfF1y7uw_{--C(8|hvRQTUS*$F#fG@l9Sz(Z#Ql@2fWt?@j! zlNQu1&N?;{S^}TEdXzPK(xRsFj~Z`JK@`-eCD@St@N!b0Ue&)5ZNOU`Phf;`sW5sQ zcmdm0;+ysJWn=j1Pfwf^y5B)Zh~CA4%`pnyGB04oAuXeow!sa;+P{M;kCuEW1LoIl zr1J&XDZk}p4dA`g)*xoGJ5)Cm8dFf-RBIhSRq<;Qg_DZxDfwAvBY>J5laenHd*8yM z@Wn$R((tn@l+#Nmg>&09#YniI?poNr$tq+w@8W+JI>k{efM9OiVv1!}$@bXFz$Mj- zxp^NS)_4Rh5SMrH@3vznek8wEifJo)Z!+8kd(hk6^Ka8+(QppB4_XuLRx{%iEU-0| zpZFzu%_(qcBb>Nk`HNA#Sc;}Y8YL#R4!Zo)Yli~nvnNiO#Vux*u;pEqUs3*XwO&&FMRtw9^P4rpH zkJC$vHJTiY_LF)WJC3~z>k`7H5ji<`cW<6M&%$v@{GYEdC=3|Sz4C0SNL~$gIHAV9 z#`7qZ*(7e#Hjg=_G_kVEv! zOR{_p#FD=@?i2aJrLQB?^8=J%A44Mv)H_9moQ8b8ATcwJDny9ghck9Ql5)jEF(pvt zbD%cQqsC>oa5XxUv^-=TQP-JLaJNE%K7PMqo0CUCKumL zwPNjk?Q0_xaaAKA?#~FV-|{!Bji2X%|H*)(D>$V5AGP`)#@J@zU)r359ZMOhFVeb0 zx>Pxd+YG?y-5ON+%x+=Q&{i#E|7KO9YfeKSYTVoXovv^3bIi2qtRtC^Mpt~XMrWF& z1@ACLdvH)}%(?F%>Dgt{g*I20!V0C0+u~}>+6M1x=6kut^B^2I!fww(*2c1JPC7Ve zw94PAs;FZluk>`_!6*CmVs$Cn{oVB!`bhcC!ljagnw}3e$_|fdX4hc~zNoSd(ivgk zpAT2>?%m(7Ip;IQ?BS0E6fei!6Q_? z>WZaq?PY*F_IF@@$`jTV>J@>N*T-Tj;SsQG__Sn$oSR}QooNgoB;}7|uAYn@uT}Gq zb{wysW%EwdR-l@7!z71;yEo_Q^-}28E;`(+p0(-)x?@;o&*lJp!D|<@h*NGsEMMy+ zBL@44L*7EowIn%m#RzcSv?bu(N`=R`=}DT!fV8^}!5nH(*D@M-w)YPE?v&srX1Z*A zTJF3V82Mpzd)$a!t?Nf?rN}iky)$Y?P{a?DSDcCbU(>PLrFB{$2L&@pP1?dm5sp!? zVWYEJw;|MDm6*3{bx4s@13>A7EB zPY!>4(s{nAj_*zL8@(vG#$}2AAGQxt==pu-&{(f>c9)|D%3>AAv)!L`s69TJ^FDgD zuQGu294kBHnszr;^E|FoPSZd3?`V$>&pn}ToYl=`$ewRbX83$hdRs~6KrFrMqG862 z2Cpk?xuohwSI~ENhw!cZdi3+~ z)9ZHO$#>@zB>#5Bo&t(fSpZ+jz_p6|EZi_Z*6sF`xsx+Z1^TCy13(MG{ti zxPvgtF(qk`{ZfWpt~*h&g}I$n`C{br1@4iq4>;W!+BUr$%k@E6L?I~ZYLfN$Vz485EVs{@C{X#cb z0G$VV&%;spj~>{Gfpt{#RH^@g;Em^RtD~jw{w^0qI89dt+I77gd=_{4S>jYg)!326 zEz}9JzN0CN0TI&wJjC^2O6fj?@L}R##Xro>1sFB8;4&UYvNr2! zTOD}CtS%kf+xSrNsqn1$i;f9hK3Eu>rX*xyskrwz{Pjk-pHkigG^>EBM>!u3Qatf7eH{9sMh zXD9hsG@9dmg=11fYkkfG6>-olh1iyQZa@y;m9x?-&@IFz6nv!sUPZV(p0b=#eIGm+ z(Y=cgQR#~_F9e*@E)u5FY;ul2Heib0WZxEuE zl+L?@=I|Ohc(y$NF6zsyYH0QU6wc)J^OFModnPaAr{&RmT8ORr&TLly`?#aHUJvID1O7B86e%u-NAmnCJ z>8Xd-j;i5)sMRc<_84Fq+aVa&7lQ5AWaU7hEA0Md6mb2*8+x(UooHPvu2ZlMvG+3i zdy4BtJR;ajbwL%Er)Tr!ugrkw8s857+USc{Q<9#OIIJ9vJ|is8G4T*|@qR>TtrYrh znn>hA%w?m4EoHPn`aHSc6XsE-11?Y~Cr)xLojqGfPe$~4O_|8nnG45`eOlHEnT}`1 z#EpdVng;c@2ew+(h+m2fSo-?96vh7wIEYuNEiLyU`x|{rea_p{_o1?z=g}(q-}d?L zwg9!;Vb}dOFG96w{vsJYhHV$O8rY9jWC2C?tMlG9e8jApH~3hVKZdGm+`mDDkF`}v zAd1v$`=pMmnFnyB@=VKptk%o@eq(cY-_q`$yiD#OB2P_&(%G zbJO z8Pwm#yoX{7>%QwOJ_l-Cw?piq5fT$W_pD=A@uQxu#ACy2gW2%S*kXsWy+23IzjfX@ z@P_k!T*(znV(!_x$5l6j`Dprjr4x*yh}tGE|xSKXs8V!u}TJ5=WS8`$aK zSc1z#tZjIcFfm`un$^Ef1T7C)E|$z1hd_MstY0H#cBm!|V;EAcIP~sA^EvK7nNSMyGSf zSdy+jJ7OyB-~M%s6-@OfoVO(TYf;1NPTgwc^S(2)#X|mhrh#}UbhIb9b3Piw>OXyA zL`~rLj}afsVEf?AGk*Vq9>)dCAd~`uAy50=ns!4Si@MLYoBCoLmt;)o%Wr@N9}58& zbkXA{Y-DuB%FWD`_OT=4Jn{Kj=BU3?@vsORML}#^uwAqS>7lou%umvYb2n zpFFCHKgcCYT!EVsW~{&^#ri0+yZJ8ZzA5kByN(7B>hVcWZ{x*1JC#ohoT&;# zZCJR#ErGdC^tU{nAANXJHti5u>B0juvL%;~xGny(?eK~%5c~`Wr)OyDyh+x_eH9l1 zIi*f0o2MYnH8@h~n7g&*Ugz4^e|Wfy&OFF=0yT|0NLJtr4i0M9;~Jrr@S{WDLad7A z$h5`;8WC^ce6bAi*jNmzLC2^y6PzP3ux` zS{+_gge+O9hW$tZ9jr=OOOG`pEzXlq{k2qOK9C=Y(arJJ9j+GAcjrX%p0rwrc=Rpt8G~E?@r>rfX+0-SfvA~DZ+`l?G;ouW-O%MB*c&05O5>qt0oYMYz z2h2vk?1>mR@Q~Gm{fD6l;5Fswjd|Q^Nhm!zi9;D3oLgD7q{ZQNu;=9)BBjsPf608F zT9bBZS`TVeM;Z+@CEO!b&Uf#^zfEb-8z^C^_orkQP@y_$*1`}V^}CAVP4Uu`>>d%| zV2b}AdEXA{L=z*lM z3I{E0_|*W~O<71^R>$6Y8oQ~K8MLit)BZ^@?wE8l#ur6LxHqDvb}GRzy@` zC#ElE#SxW`H}}76^AU2fKZ-k%gw^nNV)DMqvsCNsMBKyb%l zSfF_!YXB=0C&Yc%thRqC44>=*Q*aX!E(52Dujm@~gb z+OEGzHE$hri9ZvyqS zN<0~RXnV44>tH}AH{WMmprIj+fUBVS)>ToLW@*?J1ie#EaN?&RN9?BMo_DXSsQta+ z&pau-mjKrYT(maa;!pmmIe2$@!2&*iR^)#V4@Jh}xw9INliRDB-)TG~aAF>SlA7xU z?u$kqJJBO;lxFBEGvk|f9^H|KrV^{6c-2{}aR zsInfxKD!2I5H2bFBX#UvC-JcDr;BLkJM0XmNKh?oeEc6-sd_ z#oZ+o*9M0I#idZ3Kyi0>DDLiF+~4%R&-4Fu&b!uc1I(= zJ2DRd&1&cF7jqKd*vrrbZ3@YMKLMW5zXK79<{~BG3ZFiJP8C7~L)AV*d-A#!2arR@$zrpGiB9AWNQv*Yf+ z(@1|&^%*ezI{D*BMBc4#?G@{K(`$AtssG-D`xlA%VMu&x*&@3*W|+g#XJPQJt%+)U zx~<8$5T^R}biPruYUt!DNC@&MH@phmA3G#@*xo%|n!4u33!XQSXvRG7vT0&=q|Isvp`%-`U$;aAdIT=&_ruIn$pE{NW8hc;DzJOZ5E z@6Bpvi)Jr9aYPF)z8^ezedIkI`{G>yYnrF=xgPk}C2x4G`YTrlw@KkRgD7jwf#=y1 zwPhBl$!;UQ_h`RI z%gHIU{@3abYxdZe7qvQ*-bqw9Wp&2Qf(2hyDhve&x-V%uGR1+ueL6>vMa-)i!r84t z8|32&!c=6FsOC zuf1}xAcW4Slwr;|yhhl&3*2YlJ1QGP^jiFh7!c@z-(STnOEaB%sq+KVy-p#Efc^0N z0(B8eDvp^R{!hH9!-rI{p816=JQHQ)Jl$Q>)ZC$_FUL&?(()ZS=hbJG1ZCMkZ)UU} zzKWc_EA296_YU1T(;C@NHEmiyu)-e~HF~)mtTl#SITZ+1L(@Hq-TDq3zU{R555~B~ z7AXFAbb>YbKX-Y^PEULZs?8ELpel2jz7(_6yX<@Ru!}B6)z)SmXVWRbUpMl zPkrz2zY1L{C;cxcMgzD}sr$+)xW}RJz~Jiz9!Bnw=2g;CpmD2x0i!p}DjaL1gDQSq z8n_w#@Rre=QN@ba_ybF(M`?1zr^e{?THBBv=ekZ0Cn~Av1+?xv?L};%NOFjXg$C$9 z6Q2Ef{*p>rlim3ij?R8l$*WMfnLg0UEAJ~)_m&kiJ#>nq>+be))me|y@)f^QFAuPR zoW$-GEG2#&%~H#nEt`q7f%@xKc3D?H2ev*i$O#W#P<8$XHh`V7x+1AUmmHb#|lC)V%w|+o~y*Ut_Avyx;n&3B5Q}w+-zsvuR|e61ytl zO5Lw!S3y?u@T_2BlDrCf^dFC}Kf}LM5!w2~r?I(iRydP@3ZK`MO`Iks@24H@9u#U$ zEmeh!GYY=vwOt&QooA*ot0Jl6!u#0k+*Xn&3bhqvC8^X9O#8LeioUHDc}dltMEUOs zNU3TU(2KH~vh;1Ao%xnqCm>%~UmUKSnO~g_yn7di)>U2~y`QAH-B@n8HKtq6>!e#U z0RTHmvVDWJ%VS2jE%#*qrxv`<-2?`Qw|K2QHqAqQ)4pgdgWzQwJc@vZ1NmgCB?* zEPZ&>DTDsyT#~21?p*$tR|^xCf3xQmBQ*bXOJNQp+wc7C^X3Vu+RmDR!g&^sTR+q*% z9BLO|E@xN~;r?f7Q?@Pul{M;jjc;+SL`_-BncH*G;Y$+0IDRTqvA#UCrPaZo*Sz z@&&DRgFmhWt{H`=avkpfS2t`Z?RfIKFwLRmQv?9~6*eg+rZ25BTHInk;F@~u9 z6h7QVm6)miB=j$7p|pYz;d@*D7DKTyi&_6g#$eMiL_hkYR}t#$CJq8tY=g#iB{SHM zxP9@!?V!)?6NPU1=zd*3;HvP81@5Cgf0e!m$%4J1$|V!bgBcv^`S0^+luUnz|5Gfr z;7;R<7p}FmERE3?o|nvEW@_>H-46xJkz>@A5$)-$I*j}D5BvB8R%uFyb=Vl(wc&x8 z4=v2kqOo;MC6TLS#jWmj8lNLP-t5ThwgWH)Bqzio@p_OdEK; zY9h7Ox;cpQVuqVBSTp-IyXAxMI`$>^%aC7@kAuGhv~PcR|CwAbCVIb$51_jmcd*Z0 zvwzIn@+cvOrr5N9gD*O}e6gIadsTqJ?)CAhQy=TU3;tO0AF?HWE47@EmeT!SF94Q% z^$Jvauunth_{N51Ee>=*KCEy1ptY-6WNnmosDC9>XWE;3^1f`Nil7#%ighgfs_MAZ zuAJn%P@+=PRED4P$%a*KzNoo|q3?$?7ycUNmCNatgr_||owM~B?)1Z?(_ z)=`=8Z-4e`#`v4n;hkJoeSV44EcgE|-r^1gsS zx;*Nm{r7K;SQWN$#B4%XH%lF{`CakewM^crW-mM=0BnGC?XbFO=h-DZh2cat+4kuo zD=~V80j6*Sf)D4x2G07tY^V{bYo4ef3+ys~JcFN~Qs>xH?>H5`eUAIglAWs-4sI}e z#-9YwG4}tR&I=$$VT`?$aC0+`Y9vwO$KOl+=|8)_ZT%sr0qOLewz$m&o10UDt}X)j zdi5v*$9(ON8`ikRoZi%Nw~(t()X4+EmjkWx<$~Ggg~>w{F|mrHZ;t%>YGz${Ozl-Mwr%n7enAd6^l-8p1MMUC>PsF728 zy@z-J8&&qC*iNU0-i_oP7g^slBAzoVUOC>~So}UMnHCxE(}d-A!)DTJ#fcx87q(3T z(Lh86HPbdFLoV}slMEDh^()1(WI>iQk+Iq=VrXsVLg;l%s*jjL){yc&A?Pw5cmHBw z$6T);3hElK<@0S_IyzT`EitM3@abRj;Y^+F*ejblNXL{J{X`Sq_1gIEU9Y>Nf8K0wNR#lSnhlzrtpa(it@P>WXE2_Et1$sa9AEDpIsV>Dq zRJ6eu`TQg%4=3IX`!AvyK#MFD2B^Eopf7^SzGf5m>C?*alGWAzNCxqu{A48%t00Xt zf2+W=P{>!p4(mu5-TgU5qAJ|KnAPDTBAapk^rM;n>o>XKzv2pF;ZXkP&M4rQ(G>3AjByD`@|X!0rNDQ5gS?e@;C`iX+n%Wr!yF>Ay+IeMT^ z5t<-4h=68fqXKW?$R0-k zgl@%Fn1RJ{Q}Hs4-OEjfcJb-;M?B-H_>Nsbq}$;tt_KlfChTqh*JA{P4?h(`n@q>1 z5U@~s%)>1KW8I|r1=(pmSX8c=Nq_hre%8&;WT$tMPX%nkeKriXGk=*R${&wm(6jeQ zAAE7a35!qH9D1$SDLOBNFpo%6!E1T5PEpIpJ2h&noOdI4-Bdi@_d!}nDU5i;$%6WD z88t&cl&}o>Xr3BB<}_V_-Xny=o+NpJBCx>_j1gk6xa-Yyl$6Gzqn0t5%36vrnyD3b zcrP!bm$p?l5`LlUc$8aQT+A;Q)3WB7e!&|3f;)ds$!AUlk3$KMGoAq_P4v(qJ5T?y zk2*yYGr)3-i7YBtIRu5${(__ReuEX8Y+utf;`gNL?w&c}@wxPxqkc_v0n1R+-gjeZ zJAqx3;Sjlr>DFzx-F@qIYVWg^tPr5Q|Nne08N|;Geh?a@4KODZ*>%XqO5qWriNNLY&V+4EI>q!sx|)k zyoBWaqL8F@DV1q;V>$;p^7U(pQ?PV z@5#@2m-Wrlj0PGpCZwouE<2I>!P@8n3TN-J(5{1xM)ctgWx>eTMKd=)u!vDlh7vh6 zrmRlmDCkK@tYOR1rk*y-n&@kDcnuYcKxSX=4T5|dzm}$c4Sib|Xb9+m% z<=llK3(=BE4-%ld5Yw8Wu;Xt~JK=(Dpj!{>VRQ0otKI%5X^%h2qK3##2ONMHNMA;I ziH)@(p{V^MS^wZoQc*m^n?JuP%Kwc>P;fh)rQ4>s?wfM8yUsy1EZA}YckN*Y4$?Qa zA;k*dDI$qS=*A=9s&`an&FKlnv?l|Z@z>?6QZ@F3@l!H^8{(-0oU-%7DTIz9jO#zA zj%|=ivcIAJ-=BbXQ1$U4f%e6BUXg-lPKJ4B;2%aZeL`jN!-X|U8(9}r1VOF`By)bX+#bPi z7au|eVr@cwZn@m?TN=N8Fj_4f8vj=6vDRp`KbVYz4Og4EY&l$ieucFniq!WE_`K)| zUXo0=4OuJmac`nT^e~VZxyr!Zq^oEV$h_%ruSf3H_rN&vL-HM#&} z?PGN`f5nd^&%8^B=Na2*TT_CKw}{qlKi?4R|4;PMW{bkFRI~F+&qjY|^R@R-$#QOs zhfqL{Jc687z27s$6^5n)tqJbZk!1Rys@%rxOI_cUY(9J&z>MQP(S)3qx$R61X;zsh z3SRk%BQ>|qvHvml_gDmmFu(0O^Jimdc1fpa{240XBKz1Q#WJJ+O`+-42z0-z80Q2t zjcx9owwaNsrA9aR;z%p<`7)l5oFCS~OcBZ6C7vK+3;HHb?NSABGSw+ z1sNJVg{%O`zAs@BBG= z*~R9!$@aq!!#%32W4E6L79nroy{!~|KV3=~aO;(v&9Nv-+xNOT$XUF*ZZI9gt2q2H z<*?=Sr<G$NZKKQ1PXuSBwb!-T%NYWidE zS^PX>7G%J$uu{LepYGg~l~H_PKxHb7y-_ZeuI(M9B-_9v4bBN1Kv`+NRzGoTc-s3krmm8Pmxr^JNJ>uJ22HST-B(;Wc7sP+> zGGiPZkWg;(o2X(lYvg2k5XV+HhmbMFINAwuX~=1 z=0j0Sy>K?~L@}QfWZz@ML$}u#oY}Ee9&+Al+e5~?rz3DNFa4Gup zC^|K(w%SgL`A(%=dDT*qszz?C5R^gh|bXN)_n%|+Z&2v;wP}8)pbyPY@WQIoR6~IYGP!OnogyL3H)17}PB7nq>Pf=cO*)RM%d2HLPzp-9`=q9)EN`PX6h# zuoaOW4zw3)Bu*^r~j%HkI0aByq(5tO<9>m&jF=0*Zq~0D?eNg&mG)tur}+8Vy)eXSp&DS<1A5m zIz_*4k|3hq1V(q4xJYJ*SY|#yKku?=)n_4xL3gCszIQN=kgWv35_>n3Tx7grZ%_QSBR&CW9 zFqojnqZJdGZw?Q>c`V)`DWcwpVTvNxJ_Gv4?vf*XsXUS7{KG}!Yg=z*_U6!gu7ij| zb_v!?tJZkWNT4fC_5dkvN9h#V#hFz|l*3y6(d!H51GK_1?nN!~43K;>)Mf!6tMeFxD%rd0i2-c*6>7!8y&-TX*9~439TTx)Npbyz=*oN_f@V8_Jy#m!~+UYFTMh5?|=-T zZ0Xj+anUB~ds=kGS^cS$6#-tl@!_~TxL_XJ3r#6^EBNuawFtL1BxGSDsTLzlc)zga{cGfCg^^^uT ztu2IT@bcblVwSKt;$F`iFK&4L3lwzNgrQaZZ}!92-haoei^2V@aAr_}oCw6)DTn4&c4u3(U%UxIVx8`8W3IOexw4lL1q!sz#YM|i467es)D!pLB> zMjAh+P^?qJE@g!8wsR-=YJ?q+oe(nkAOyN9D5P$dx~8*3TU`x)O&oq22oSXh_7+w-xEfvCw~?~tg$w*eM%f;?z}xbQ zs~Q?JJJW5VK`(45%bSEZQ$sKZwg04I$v^hg zPgsM4@GN?gXm+mJ&eRyBg}JSG_*)e8P5)ah|NTE0C-Kief+VVf>tg|$?eYuU3 z6GlKn7a(?OAWS*&Wz1>fsh$5WAVa5lECU9UOCdE&RUjUd-HgM^rIK(t5pGUK&F0$DYrZ1gl*4awb&LYJkSm_yOq^&JvdBJCkS{zD+3Hf%`W&t6 z8s>zymybhDCsCW0f}H*+6J-n%D6Pn7PzB-}cG~F@U0-~_*%OC5UEY#SH|1HS&}ijp z;w~QL9~8bF9|(Bm^w}O6@?q3zrJhN0^pBTjKC`#yaS;7hTp1i zRllW$LEw-#;y^E~uAr9iw=t*fRAP8XsS41S1N02_z7u~wNqRv1Hm%|=D+ttR{bdp4 zA8_j6O}_ei^$wlK!KLbuD$*r$P*dK-{G$Z)cdd^!a}8DHnjL6bLCo^>O!?N8J$)9T ze0D9l8yT0t>d$X`?o4R=n4*(D*PIVh-iRF78}rJf`8tf-3y4Tjbx7edn*Ie2tPfCO zFo~E|FogqBW4DJiBkctHIR$ej-s4LS=%{e<&NsNzI=E9)XC$V6rf69asJgc0KVCvL zupXnu5ey}39JQT1uSc)aUK}@^PgTGyC%XROjldrpXzM{DY3a^M{6Z&ff%k1+Md72g zM~T$PUBc_y>h+R)x{n6KXt!917OoJQDwfjJFEh$QeKwmyCrrk z`ffo^xVY3;>Qg+wFrjzICYDlaz){8n)|dCGpH$-Kk7E3!Ns5EVJEJz5oCEu9xPg`Y zL)&Tla=8lhC8M>jHDSrOVBOk5zSNktJG(*;=Le;<&eE^!*cm2i{;j6+-xtu4s5|3r zt8R><6KT9>X$q?Yf%23b0DbNZdJb^1jk%Fjl zzKW%)aD8WVB7bLos?#6n!R{Kd+2LGJ%Nh{K4#Zw5hDE#WEz5z~Xgc z?>7IC!`!GxX6c1Z4g`zA<%X>BlBD|z`y6cblw^%%{e>=nU^tC`so|f1^Rrson9^2CfRJyW__I+JB2~@mBQqHpPWr310Ggb7z%Ue`MGlX$|Ui z>+ZT;r!&B9CtXCe7$Eo+BVkZ=$WkgAkjA|2GF!IiJrX99|)sow&{PJ_jMA2`L=!&_Q!5M3>-XFV?MrcgsP?BvQzr3+><{E2 zr&~LRqbfN$z1X}2?w2E`T`W4046|z=urJJtX_F3SHh9(O#X|%MYJQS!dDwIvR|wj?v0-nvQ5MpwsjoPY+ z<80v=?TOj|3HU0H*k*AifS^(9yOVSxy1petYlH!#VwuK0ypyO^@b4@CR->ma-wnqo0uWvL~9+q%8Gu;k?ODFcfFTc*H|K>AS83k8U6b z)b#l#JewLX)7fYW7fCPODRXUi=Vrpc4`r;~aBJ@hIJ4JO{7p2S`;4&a5FSIsZ&;gq zhY;*R;*vK1L9QxoVk_JT`*B7#W4Xzr%}zFw)MvfAD83=%eUX6YTa;Mwk0|y)SB5@r{^)fdlD8ArsN(S@z zChCokfBe`?gwX;LTNe<%%$)ym4Roy1KN&rW(arLiS}~7$MG9K{se(9*Bx3j~V7!ia zQE^g|vR!b8Wy@e9b?Tv2gnT!-95PRcQuq&Dde-Om=^5x%qJmW`v+_=4=XeEV zQD(byRA=U-#gW+fr$B)|O_PF+{Gyw)a|Im6r1m8>Hmp9;!{IUt%=0Dke(yTuV3&ey zLllV>Dexovlq*GUn!{Nm#59$v*rwNIz^IO}oUPNdTxj6C*yhr^`ey;isk_J0j8BGO z7lRsD%idqeaMj@`$Mx!$Ok2>IJ*%|ENPpl>i)`iQz9t3sx&bK#i#SVacW|isj8(@l?FxB%PodXdh zE?@oDaK=pUeT6I0!8m~aB5w~n89gbD$H|Lqt|E=Tc0BSCNSm7w&b2zGLbde(JQm%s zNTaKDlMUOwfc>ClyYdCi`i_|DOCU2q1m6!-`qi91=M7+UNNx7IK&RH27=E;(oqC3X zO%X{X#l%Fd33&#WgdQFT6a_as+^%{fH>A&v&@~k5M&0g4`^(ai3e!$za=|iKgvQ>)kan3fbOMk zqXBYGIp~rkP$kDYmOh3<9B}X>BA;)p^a=e-e&}HEq6Hl_^?Xmx2WhZYfUO5uAD(JM z;4FE&?vzG0Dy(ng9BrdwPG;Y=Q8L)%^>U(|&TW7hJ(?kPoB*-dO-TKDHB&r&vyS0V zbLw9xETyL`T4CY&bH=}9ubFFUX@uI_wTl&Ih^#(3*-py2ZaKxkTM=SjP2=xWN#?sG zft^5E={7i1^}zJULT)|mj)DK34z&s#|1c&na5YEaJKy&;(52)gLH0B8@V3-xQU7nc zPYOmA>98bh3IAP?4<2uIx>>w($goEcpV-S?8qkWpmby(jY{Tfrnz!F|tln+y683m#M%xH&r?em4J2*l=;=e-q`0B~gcXD_E z)<}IPpC$uNiHbRY&#P$zIeFV&+ayKV+_ZRlCJxrZj_G5NB-LE~4rNnb z&v+l}0+1b`2EnhKSYE%!T6;nTi1<40;hOMC!5j)qcn}NiVV5AW=jHo3=RDc#@z*{e zr2r<2+>Jx*!3fA{C{tY^(iTlNv)-aQR#8jZSTA^1oTZ6S$PuL*hP!s@Zm6n zf7*_4C)#{*+^Q-jf4b3$fM zkN};^Mw6?fsA(fquBd?t)-G|d4c$prDU3-LLT+j_K$#qPR9YsQbj=u z0RDjguq2=atGymffovK9AX6XYW!WmNoYT{pWp1Tdrr(+V{tZp=%egZNF zOuW-6qtowHx1;pGQ8~jn~fN7(nZknFjC!^CFRZky1}?lB7FueusISm7Rm~4u=Pmn z&ka=aVA1`ig7BC&SLGY$J-2NmfmDkM>M+mFWa;E!TC$$e>Mr4-w>3l4H6>cYVSl7} zz7-~4HUD0!@J~v@@&sUglt-#qCl07bkc=GDS71sWrjW(Kp}77% zu>#AFZSB13ZooOe3D}9fj$%H-ZHNw57Ri2w$(9knHdW%?1#0R7O;8vhp9pAzd|3zY7zuArX@qxLC74bNi^)$-D(EIA!4ixiYa^KmD$)~j1##fun3HP+#&Otd81Ik`PP56!{v2Kw zr!N-?5uL@R=}cIaAFgOQ8)PYLlgK-JBSt?h2hB1W_AdAWsqn+$h=ubFxRTHNP=keG z7maS1Rxr6Q^zj|p0&WeC#YR;=f2dgR53IxH>KWjJ%9?cy*>a*y7~n~Jz__%<*=PLPnU*gweE{&VhF;Nx)!DQ@ zK+C;y$3>`U`O^TY8Hyd`iM2*1YW6!=hK{F0{hymB3)r>Zn&QPO-=i-64MJ14dJktp z8xIdEvctJaIw>MfHWJV4M1Oz+ zStd$0DR&bIk%M7IAay%0H)CZ$%@8j)N44$TUpi+vz|<6d2;sV15$YwywF`V~@eZ%U z2W#{LOEXPguZD=E&XJ)cD;RCKp2;;~AwPTr$!&7!7R;C=;isP9OHr3OFnO4VLTRxi*Q2CUHu_1gX zTkr^dmgkhcNN-0e9Q?vSw10xRB;1D`ki~qSQP1FQMuLq$1X}YcE;Zq5z1Q2(eZ3TC zXKE;p?)SA%OuFMUuFn=JH>dULd^30E69Q*5Fb0>ZGhR6JZ|Z=@SxY6NTv|)pNhK07 zxmE?jj#Wnd_s#1vn)}`z;UU;Py$B{zJ4EM5qAycu9~A6$h;>ee*+;GlMtkGyD32Qp z2v?X`i=qe><91+D^of}mM>PlR>6+SFAvQL)6glj<0h5Q_hS{IEd&Bn|3kDOt@{hN0 zhW57B5-K;-BuZu6gqq}wft{%yXRt3cf&6_uNUXs8$g-}5 zbyyGGtGw^Lh2<+Nu|7ACs85S$6ET#Qj zlyZ#{EW(fYoCObPAXavs%IDTNlmVlEE4|1_r@jVrVgfEN^c^8%W!8r1J+i^i9Pe9$ zmDV#>oS02B+*~3znk3hEY-q9602UZ;8A8P{FiTHnj#VJl�fr=fx)|jG>XgRLEUY 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 literal 0 HcmV?d00001 From 2161a00b81d05fb1f2f8c5f92bb18ba8409db120 Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 3 Sep 2021 13:24:21 +0200 Subject: [PATCH 31/47] Update README --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d6a8f59..f343685 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,14 @@

  • - Data + Datasets +
  • Preprocess the Data
  • Train @@ -105,12 +106,10 @@ To create the environment using Conda: conda activate nilm-thresholding ``` -## Data +## Datasets ### UK-DALE -#### Download UK-DALE - UK-DALE dataset is hosted on the following link: [https://data.ukedc.rl.ac.uk/browse/edc/efficiency/residential /EnergyConsumption/Domestic/UK-DALE-2017/UK-DALE-FULL-disaggregated](https://data.ukedc.rl.ac.uk/browse/edc/efficiency/residential/EnergyConsumption/Domestic/UK-DALE-2017/UK-DALE-FULL-disaggregated) @@ -131,7 +130,15 @@ nilm-thresholding Credit: [Jack Kelly](https://jack-kelly.com/data/) -### Preprocess +### Pecan Street Dataport + +We are aiming to include this dataset in a future release. You can check the issue here: [https://github.com/UCA-Datalab/nilm-thresholding/issues/8](https://github.com/UCA-Datalab/nilm-thresholding/issues/8) + +Any help and suggestions are welcome! + +Credit: [Pecan Street](https://dataport.pecanstreet.org/) + +## Preprocess the Data Once downloaded the raw data from any of the sources above, you must preprocess it. From 5262887c92f21f8abcf3b82deec7b7fbd5d71e9b Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 22 Sep 2021 12:59:22 +0200 Subject: [PATCH 32/47] Change url of image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f343685..b07ee9a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

    - + Logo From e8a01f5ea7a994a3bb42ec7606eb510794fac69c Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 23 Sep 2021 18:44:50 +0200 Subject: [PATCH 33/47] Typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fcf4fc8..b07ee9a 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ python nilmth/train.py --path_config For more information about the script, run: - ``` +``` python nilmth/train.py --help ``` From 6b0149f4022f25e2ddf0804d0625b93d8b8d22d3 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 23 Sep 2021 18:45:47 +0200 Subject: [PATCH 34/47] Change plot_power_distribution argument --- nilmth/utils/plot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nilmth/utils/plot.py b/nilmth/utils/plot.py index 584b0bf..9a120a4 100644 --- a/nilmth/utils/plot.py +++ b/nilmth/utils/plot.py @@ -69,7 +69,7 @@ def plot_real_vs_prediction( def plot_power_distribution( - ser: np.array, app: str, bins: int = 20, figsize: tuple = (3, 3) + ser: np.array, app: str = "", bins: int = 20, figsize: tuple = (3, 3) ): """Plot the histogram of power distribution @@ -77,8 +77,8 @@ def plot_power_distribution( ---------- ser : numpy.array Contains all the power values - app : label - Name of the appliance + app : str, optional + Name of the appliance, by default "" bins : int, optional Histogram splits, by default 20 figsize : tuple, optional From 16c357bd98f6b27fd61e4fdb3f17d998ff95ace9 Mon Sep 17 00:00:00 2001 From: daniprec Date: Tue, 3 Aug 2021 18:01:49 +0200 Subject: [PATCH 35/47] Docstring --- nilmth/data/clustering.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index 026fc84..48dd071 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -145,3 +145,17 @@ def compute_thresholds_and_centroids( [np.min(self.x[clusters == (c + 1)]) for c in range(self.n_cluster)] ) self.thresh = np.divide(np.array(x_min[1:]) + np.array(x_max[:-1]), 2) + + def plot_cluster_distribution(self, label: str = "", bins: int = 100): + """Plots the power distribution, and the lines splitting each cluster + + Parameters + ---------- + label : str, optional + Label of the distribution, by default "" + bins : int, optional + Number of bins, by default 100 + """ + fig, ax = plot_power_distribution(self.x, label, bins=bins) + [ax.axvline(t, color="r", linestyle="--") for t in self.thresh] + return fig, ax From 829b334d888927ef5d391bbbc99310f568a3265f Mon Sep 17 00:00:00 2001 From: daniprec Date: Wed, 4 Aug 2021 12:48:30 +0200 Subject: [PATCH 36/47] Move plot_cluster_distribution to plot --- nilmth/data/clustering.py | 14 -------------- nilmth/utils/plot.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index 48dd071..026fc84 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -145,17 +145,3 @@ def compute_thresholds_and_centroids( [np.min(self.x[clusters == (c + 1)]) for c in range(self.n_cluster)] ) self.thresh = np.divide(np.array(x_min[1:]) + np.array(x_max[:-1]), 2) - - def plot_cluster_distribution(self, label: str = "", bins: int = 100): - """Plots the power distribution, and the lines splitting each cluster - - Parameters - ---------- - label : str, optional - Label of the distribution, by default "" - bins : int, optional - Number of bins, by default 100 - """ - fig, ax = plot_power_distribution(self.x, label, bins=bins) - [ax.axvline(t, color="r", linestyle="--") for t in self.thresh] - return fig, ax diff --git a/nilmth/utils/plot.py b/nilmth/utils/plot.py index 9a120a4..999a690 100644 --- a/nilmth/utils/plot.py +++ b/nilmth/utils/plot.py @@ -1,3 +1,4 @@ +from typing import Iterable import numpy as np from matplotlib import pyplot as plt @@ -97,3 +98,30 @@ def plot_power_distribution( ax.set_ylabel("Frequency") ax.grid() return fig, ax + + +def plot_cluster_distribution( + ser: np.array, + thresh: Iterable[float], + app: str = "", + bins: int = 20, + figsize: tuple = (3, 3), +): + """Plots the power distribution, and the lines splitting each cluster + + Parameters + ---------- + ser : numpy.array + Contains all the power values + thresh : Iterable[float] + Contains all the threshold values + app : str, optional + Name of the appliance, by default "" + bins : int, optional + Histogram splits, by default 20 + figsize : tuple, optional + Figure size, by default (3, 3) + """ + fig, ax = plot_power_distribution(ser, app=app, bins=bins, figsize=figsize) + [ax.axvline(t, color="r", linestyle="--") for t in thresh] + return fig, ax From 8b7a4308f89a5d065ae368fb16bd8f7834a65984 Mon Sep 17 00:00:00 2001 From: daniprec Date: Thu, 23 Sep 2021 19:53:56 +0200 Subject: [PATCH 37/47] Threshold class can change between status and powe --- nilmth/data/dataset.py | 4 ++-- nilmth/data/threshold.py | 23 +++++++++++++++++++---- nilmth/hierarchical_clustering.py | 13 +++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/nilmth/data/dataset.py b/nilmth/data/dataset.py index 56ba9bb..e5aceb1 100644 --- a/nilmth/data/dataset.py +++ b/nilmth/data/dataset.py @@ -137,7 +137,7 @@ def power_to_status(self, ser: np.array) -> np.array: shape [output len, num appliances] """ - return self.threshold.get_status(ser) + return self.threshold.power_to_status(ser) def status_to_power(self, ser: np.array) -> np.array: """Computes the power assigned to each status @@ -154,7 +154,7 @@ def status_to_power(self, ser: np.array) -> np.array: """ # Get power values from status - return np.take_along_axis(self.threshold.centroids, ser.T, axis=1).T + return self.threshold.status_to_power(ser) def __getitem__(self, index: int) -> tuple: """Returns an element of the data loader diff --git a/nilmth/data/threshold.py b/nilmth/data/threshold.py index 0514b0a..a6f698f 100644 --- a/nilmth/data/threshold.py +++ b/nilmth/data/threshold.py @@ -262,7 +262,7 @@ def _compute_status_by_activation_time(self, ser: np.array): ser_bin = np.stack(ser_bin).T return ser_bin - def get_status(self, ser: np.array) -> np.array: + def power_to_status(self, ser: np.array) -> np.array: """ Parameters @@ -284,6 +284,23 @@ def get_status(self, ser: np.array) -> np.array: return ser_bin + def status_to_power(self, ser: np.array) -> np.array: + """Computes the power assigned to each status + + Parameters + ---------- + ser : numpy.array + shape [output len, num appliances] + + Returns + ------- + numpy.array + shape [output len, num appliances] + + """ + # Get power values from status + return np.take_along_axis(self.centroids, ser.T, axis=1).T + @property def config_key(self): """Key that contains the relevant values of the config file""" @@ -316,9 +333,7 @@ def write_config(self, path_config: str): store_config(path_config, config) logging.debug(f"Config stored at {path_config}\n") - def set_thresholds_and_centroids( - self, thresholds: np.array, centroids: np.array - ): + def set_thresholds_and_centroids(self, thresholds: np.array, centroids: np.array): """Change threshold and centroid values to given ones Parameters diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index 0a1d294..fb4e4e4 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -9,6 +9,7 @@ from nilmth.data.clustering import HierarchicalClustering from nilmth.data.dataloader import DataLoader +from nilmth.data.threshold import Threshold from nilmth.utils.config import load_config from nilmth.utils.scores import regression_scores_dict @@ -132,7 +133,7 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None) ax.grid() -def plot_clustering_results(ser: np.array, dl: DataLoader, method: str = "average"): +def plot_clustering_results(ser: np.array, th: Threshold, method: str = "average"): """Plots the results of applying a certain clustering method on the given series @@ -140,7 +141,7 @@ def plot_clustering_results(ser: np.array, dl: DataLoader, method: str = "averag ---------- ser : np.array Contains all the power values - dl : DataLoader + th : Threshold Required to apply the thresholds on the series method : str, optional Clustering method, by default "average" @@ -158,11 +159,11 @@ def plot_clustering_results(ser: np.array, dl: DataLoader, method: str = "averag # Update thresholds and centroids thresh = np.insert(np.expand_dims(hie.thresh, axis=0), 0, 0, axis=1) centroids = np.expand_dims(hie.centroids, axis=0) - dl.threshold.set_thresholds_and_centroids(thresh, centroids) + th.set_thresholds_and_centroids(thresh, centroids) # Create the dictionary of power series power = np.expand_dims(ser, axis=1) - sta = dl.dataset.power_to_status(power) - recon = dl.dataset.status_to_power(sta) + sta = th.power_to_status(power) + recon = th.status_to_power(sta) dict_app = {"app": {"power": power, "power_pred": recon}} # Compute the scores dict_scores = regression_scores_dict(dict_app) @@ -231,7 +232,7 @@ def main( appliance = app.capitalize().replace("_", " ") # Loop through methods for method in LIST_LINKAGE: - plot_clustering_results(ser, dl, method=method) + plot_clustering_results(ser, dl.threshold, method=method) # Place title in figure plt.gcf().suptitle(f"{appliance}, Linkage: {method}") # Save and close the figure From 14fe2fff820c44db46ee4def4d1d9391776ecf66 Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 24 Sep 2021 10:40:14 +0200 Subject: [PATCH 38/47] No need to give Threshold --- nilmth/data/threshold.py | 2 +- nilmth/hierarchical_clustering.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nilmth/data/threshold.py b/nilmth/data/threshold.py index a6f698f..4639697 100644 --- a/nilmth/data/threshold.py +++ b/nilmth/data/threshold.py @@ -34,7 +34,7 @@ def __init__( num_status: int = 2, ): # Set thresholding method parameters - self.appliances = [] if appliances is None else sorted(to_list(appliances)) + self.appliances = ["App"] if appliances is None else sorted(to_list(appliances)) self.num_apps = len(self.appliances) self.method = method self.num_status = num_status diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index fb4e4e4..bd9048b 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -133,7 +133,7 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None) ax.grid() -def plot_clustering_results(ser: np.array, th: Threshold, method: str = "average"): +def plot_clustering_results(ser: np.array, method: str = "average"): """Plots the results of applying a certain clustering method on the given series @@ -141,8 +141,6 @@ def plot_clustering_results(ser: np.array, th: Threshold, method: str = "average ---------- ser : np.array Contains all the power values - th : Threshold - Required to apply the thresholds on the series method : str, optional Clustering method, by default "average" """ @@ -151,6 +149,8 @@ def plot_clustering_results(ser: np.array, th: Threshold, method: str = "average hie.perform_clustering(ser, method=method) # Initialize the list of intrinsic error per number of clusters intr_error = [0] * len(LIST_CLUSTER) + # Initialize threshold class + th = Threshold() # Initialize the empty list of thresholds (sorted) thresh_sorted = [] # Compute thresholds per number of clusters @@ -232,7 +232,7 @@ def main( appliance = app.capitalize().replace("_", " ") # Loop through methods for method in LIST_LINKAGE: - plot_clustering_results(ser, dl.threshold, method=method) + plot_clustering_results(ser, method=method) # Place title in figure plt.gcf().suptitle(f"{appliance}, Linkage: {method}") # Save and close the figure From c76099681e9daf3ab02db6b40d89ac320ad3341e Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 24 Sep 2021 11:18:41 +0200 Subject: [PATCH 39/47] Allow to choose centroid computation method --- nilmth/hierarchical_clustering.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index bd9048b..734bd2d 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -133,7 +133,9 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None) ax.grid() -def plot_clustering_results(ser: np.array, method: str = "average"): +def plot_clustering_results( + ser: np.array, method: str = "average", centroid: str = "mean" +): """Plots the results of applying a certain clustering method on the given series @@ -143,19 +145,21 @@ def plot_clustering_results(ser: np.array, method: str = "average"): Contains all the power values method : str, optional Clustering method, by default "average" + centroid : str, optional + Method to compute the centroids (median or mean), by default "mean" """ # Clustering hie = HierarchicalClustering() hie.perform_clustering(ser, method=method) # Initialize the list of intrinsic error per number of clusters intr_error = [0] * len(LIST_CLUSTER) - # Initialize threshold class - th = Threshold() # Initialize the empty list of thresholds (sorted) thresh_sorted = [] # Compute thresholds per number of clusters for idx, n_cluster in enumerate(LIST_CLUSTER): - hie.compute_thresholds_and_centroids(n_cluster=n_cluster) + hie.compute_thresholds_and_centroids(n_cluster=n_cluster, centroid=centroid) + # Initialize threshold class + th = Threshold(method="custom") # Update thresholds and centroids thresh = np.insert(np.expand_dims(hie.thresh, axis=0), 0, 0, axis=1) centroids = np.expand_dims(hie.centroids, axis=0) @@ -216,7 +220,6 @@ def main( # Loop through the list of appliances for app in LIST_APPLIANCES: config["appliances"] = [app] - # Prepare data loader with train data dl = DataLoader( path_data=path_data, @@ -225,7 +228,6 @@ def main( path_threshold=path_threshold, **config, ) - # Take an appliance series ser = dl.get_appliance_series(app)[:limit] # Appliance name From 45248d746693c7c21ec93726fef5dc9abdbead81 Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 24 Sep 2021 11:40:21 +0200 Subject: [PATCH 40/47] Rename linkage method to distance --- nilmth/data/clustering.py | 14 +++++++------- nilmth/hierarchical_clustering.py | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index 026fc84..72a8070 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -16,14 +16,14 @@ class HierarchicalClustering: dendrogram: dict = None def __init__( - self, method: str = "average", n_cluster: int = 2, criterion: str = "maxclust" + self, distance: str = "average", n_cluster: int = 2, criterion: str = "maxclust" ): - self.method = method + self.distance = distance self.n_cluster = n_cluster self.criterion = criterion def perform_clustering( - self, ser: np.array, method: Optional[str] = None + self, ser: np.array, distance: Optional[str] = None ) -> np.array: """Performs the actual clustering, using the linkage function @@ -31,19 +31,19 @@ def perform_clustering( ---------- ser : np.array Series of points to group in clusters - method : str, optional - Clustering method, by default None (takes the one from the class) + distance : str, optional + Clustering distance criteria, by default None (takes the one from the class) Returns ------- np.array Z[i] will tell us which clusters were merged in the i-th iteration """ - self.method = method if method is not None else self.method + self.distance = distance if distance is not None else self.distance # The shape of our X matrix must be (n, m) # n = samples, m = features self.x = np.expand_dims(ser, axis=1) - self.z = linkage(self.x, method=self.method) + self.z = linkage(self.x, method=self.distance) @property def cophenet(self): diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index 734bd2d..b864a94 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -15,7 +15,7 @@ LIST_APPLIANCES = ["dish_washer", "fridge", "washing_machine"] LIST_CLUSTER = [2, 3, 4, 5, 6] -LIST_LINKAGE = [ +LIST_DISTANCE = [ "average", "weighted", "centroid", @@ -134,7 +134,7 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None) def plot_clustering_results( - ser: np.array, method: str = "average", centroid: str = "mean" + ser: np.array, distance: str = "average", centroid: str = "median" ): """Plots the results of applying a certain clustering method on the given series @@ -143,14 +143,14 @@ def plot_clustering_results( ---------- ser : np.array Contains all the power values - method : str, optional - Clustering method, by default "average" + distance : str, optional + Clustering linkage criteria, by default "average" centroid : str, optional Method to compute the centroids (median or mean), by default "mean" """ # Clustering hie = HierarchicalClustering() - hie.perform_clustering(ser, method=method) + hie.perform_clustering(ser, distance=distance) # Initialize the list of intrinsic error per number of clusters intr_error = [0] * len(LIST_CLUSTER) # Initialize the empty list of thresholds (sorted) @@ -194,7 +194,7 @@ def main( path_output: str = "outputs/hieclust", ): """Performs the hierarchical clustering on the given list of appliances, - testing different linkaged methods and number of splits. For each combination, + testing different linkage methods and number of splits. For each combination, outputs an image with several informative graphs. Parameters @@ -232,13 +232,13 @@ def main( ser = dl.get_appliance_series(app)[:limit] # Appliance name appliance = app.capitalize().replace("_", " ") - # Loop through methods - for method in LIST_LINKAGE: - plot_clustering_results(ser, method=method) + # Loop through distances + for distance in LIST_DISTANCE: + plot_clustering_results(ser, distance=distance) # Place title in figure - plt.gcf().suptitle(f"{appliance}, Linkage: {method}") + plt.gcf().suptitle(f"{appliance}, Linkage: {distance}") # Save and close the figure - path_fig = os.path.join(path_output, f"{app}_{method}.png") + path_fig = os.path.join(path_output, f"{app}_{distance}.png") plt.savefig(path_fig) plt.close() From 75b16eda98fef20389a53b49643e3593fc153fba Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 24 Sep 2021 11:41:12 +0200 Subject: [PATCH 41/47] Remove unused import --- nilmth/data/clustering.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index 72a8070..d732305 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -3,7 +3,6 @@ import matplotlib.pyplot as plt import numpy as np -from nilmth.utils.plot import plot_power_distribution from scipy.cluster.hierarchy import cophenet, dendrogram, fcluster, linkage from scipy.spatial.distance import pdist From 7e14e34a92d76bf16de1c48348668327920d3612 Mon Sep 17 00:00:00 2001 From: daniprec Date: Fri, 24 Sep 2021 17:52:30 +0200 Subject: [PATCH 42/47] Include synthetic series --- nilmth/hierarchical_clustering.py | 79 ++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index b864a94..c41d253 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -1,10 +1,10 @@ import os +import random from typing import Iterable, Optional import matplotlib.pyplot as plt import numpy as np import typer -from matplotlib import pyplot as plt from matplotlib.axes import Axes from nilmth.data.clustering import HierarchicalClustering @@ -14,7 +14,7 @@ from nilmth.utils.scores import regression_scores_dict LIST_APPLIANCES = ["dish_washer", "fridge", "washing_machine"] -LIST_CLUSTER = [2, 3, 4, 5, 6] +LIST_CLUSTER = [2, 3, 4, 5] LIST_DISTANCE = [ "average", "weighted", @@ -133,6 +133,25 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None) ax.grid() +def plot_series_reconstruction(power: np.array, recon: np.array): + """Plots the original and reconstructed series + + Parameters + ---------- + power : np.array + Original series + recon : np.array + Reconstructed series + """ + time = np.arange(0, len(power)) * 6 + plt.plot(time, power, label="Original") + plt.plot(time, recon, alpha=0.8, label="Reconstructed") + plt.grid() + plt.legend() + plt.ylabel("Power (watts)") + plt.xlabel("Time (s)") + + def plot_clustering_results( ser: np.array, distance: str = "average", centroid: str = "median" ): @@ -175,6 +194,12 @@ def plot_clustering_results( # Update the sorted list of thresholds thresh = list(set(hie.thresh) - set(thresh_sorted)) thresh_sorted.append(thresh[0]) + # Plot + plt.figure(figsize=(12, 4)) + plot_series_reconstruction(power, recon) + plt.title(f"Series reconstruction from {n_cluster} statuses") + plt.show() + plt.close() # Initialize plots fig, axis = plt.subplots(2, 2, figsize=(12, 8)) # Plots @@ -186,6 +211,56 @@ def plot_clustering_results( fig.tight_layout(rect=[0, 0.03, 1, 0.95]) +def build_synthetic_series( + size: int = 1500, + list_power: Optional[Iterable] = None, + period_min: int = 50, + period_max: int = 100, + noise_mean: float = 0, + noise_std: float = 10, +) -> np.array: + """Builds a synthetic series + + Parameters + ---------- + size : int, optional + Size of the series, by default 1500 + list_power : Optional[Iterable], optional + List of allowed power (statuses), by default None + period_min : int, optional + Minimum time period, by default 50 + period_max : int, optional + Maximum time period, by default 100 + noise_mean : float, optional + Mean of the noise, by default 0 + noise_std : float, optional + Standard deviation of the noise, by default 10 + + Returns + ------- + np.array + synthetic series + """ + list_power = [0, 30, 90] if list_power is None else list_power + # Initialize series and indexes + ser = np.empty(size) + t_start = 0 + t_end = 0 + idx = 0 + while t_end < size: + t_start = t_end + t_end = min(size, t_end + random.randint(period_min, period_max)) + ser[t_start:t_end] = list_power[idx] + idx += 1 + if idx >= len(list_power): + idx = 0 + random.shuffle(list_power) + # Add noise + ser += np.random.normal(noise_mean, noise_std, size) + ser[ser < 0] = 0 + return ser + + def main( limit: int = 20000, path_data: str = "data-prep", From 216a7c8f83b1a6918ee3fa7b08b1656ed3255cfd Mon Sep 17 00:00:00 2001 From: daniprec Date: Mon, 27 Sep 2021 16:34:31 +0200 Subject: [PATCH 43/47] Allow to choose error metric --- nilmth/data/clustering.py | 3 +- nilmth/hierarchical_clustering.py | 47 ++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index d732305..c91db57 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -143,4 +143,5 @@ def compute_thresholds_and_centroids( x_min = sorted( [np.min(self.x[clusters == (c + 1)]) for c in range(self.n_cluster)] ) - self.thresh = np.divide(np.array(x_min[1:]) + np.array(x_max[:-1]), 2) + thresh = np.divide(np.array(x_min[1:]) + np.array(x_max[:-1]), 2) + self.thresh = np.insert(thresh, 0, 0, axis=0) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index c41d253..e990cc1 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -13,12 +13,13 @@ from nilmth.utils.config import load_config from nilmth.utils.scores import regression_scores_dict + LIST_APPLIANCES = ["dish_washer", "fridge", "washing_machine"] -LIST_CLUSTER = [2, 3, 4, 5] +LIST_CLUSTER = [1, 2, 3, 4, 5, 6] LIST_DISTANCE = [ "average", - "weighted", - "centroid", + # "weighted", + # "centroid", "median", "ward", # Ward variance minimization algorithm ] @@ -71,6 +72,8 @@ def plot_thresholds_on_series( Contains all the power values thresh : Iterable[float] Contains all the threshold values + size : int, optional + Length of the sample series array, by default 500 ax : Axes, optional Axes where the graph is plotted """ @@ -106,6 +109,8 @@ def plot_intrinsic_error(intr_error: Iterable[float], ax: Optional[Axes] = None) if ax is None: ax = plt.gca() ax.plot(LIST_CLUSTER, intr_error, ".--") + ymax = min(max(intr_error), 1) + ax.set_ylim(0, ymax) ax.set_ylabel("Intrinsic Error (NDE)") ax.set_xlabel("Number of status") ax.set_title("Intrinsic Error depending on splits") @@ -153,7 +158,11 @@ def plot_series_reconstruction(power: np.array, recon: np.array): def plot_clustering_results( - ser: np.array, distance: str = "average", centroid: str = "median" + ser: np.array, + distance: str = "average", + centroid: str = "median", + error: str = "nde", + plot_recon: bool = False, ): """Plots the results of applying a certain clustering method on the given series @@ -165,7 +174,11 @@ def plot_clustering_results( distance : str, optional Clustering linkage criteria, by default "average" centroid : str, optional - Method to compute the centroids (median or mean), by default "mean" + Method to compute the centroids (median or mean), by default "mean" + error : str, optional + Error metric (mse, mae, rmse, sae, nde), by default "nde" + plot_recon : bool, optional + If True, plots the reconstructed series, by default False """ # Clustering hie = HierarchicalClustering() @@ -180,8 +193,9 @@ def plot_clustering_results( # Initialize threshold class th = Threshold(method="custom") # Update thresholds and centroids - thresh = np.insert(np.expand_dims(hie.thresh, axis=0), 0, 0, axis=1) + thresh = np.expand_dims(hie.thresh, axis=0) centroids = np.expand_dims(hie.centroids, axis=0) + centroids[centroids < 1] += 1e-1 th.set_thresholds_and_centroids(thresh, centroids) # Create the dictionary of power series power = np.expand_dims(ser, axis=1) @@ -190,16 +204,17 @@ def plot_clustering_results( dict_app = {"app": {"power": power, "power_pred": recon}} # Compute the scores dict_scores = regression_scores_dict(dict_app) - intr_error[idx] = dict_scores["app"]["nde"] + intr_error[idx] = dict_scores["app"][error] # Update the sorted list of thresholds thresh = list(set(hie.thresh) - set(thresh_sorted)) thresh_sorted.append(thresh[0]) # Plot - plt.figure(figsize=(12, 4)) - plot_series_reconstruction(power, recon) - plt.title(f"Series reconstruction from {n_cluster} statuses") - plt.show() - plt.close() + if plot_recon: + plt.figure(figsize=(12, 4)) + plot_series_reconstruction(power, recon) + plt.title(f"Series reconstruction from {n_cluster} statuses") + plt.show() + plt.close() # Initialize plots fig, axis = plt.subplots(2, 2, figsize=(12, 8)) # Plots @@ -263,6 +278,7 @@ def build_synthetic_series( def main( limit: int = 20000, + error: str = "nde", path_data: str = "data-prep", path_threshold: str = "threshold.toml", path_config: str = "nilmth/config.toml", @@ -276,6 +292,8 @@ def main( ---------- limit : int, optional Number of data points to use, by default 20000 + error : str, optional + Error metric (mse, mae, rmse, sae, nde), by default "nde" path_data : str, optional Path to the preprocessed data folder, by default "data-prep" path_threshold : str, optional @@ -294,6 +312,7 @@ def main( # Loop through the list of appliances for app in LIST_APPLIANCES: + print(f"=====\nAppliance: {app}") config["appliances"] = [app] # Prepare data loader with train data dl = DataLoader( @@ -305,11 +324,13 @@ def main( ) # Take an appliance series ser = dl.get_appliance_series(app)[:limit] + # Appliance name appliance = app.capitalize().replace("_", " ") # Loop through distances for distance in LIST_DISTANCE: - plot_clustering_results(ser, distance=distance) + print(f"-----\nAppliance: {app} | Distance: {distance}") + plot_clustering_results(ser, distance=distance, error=error) # Place title in figure plt.gcf().suptitle(f"{appliance}, Linkage: {distance}") # Save and close the figure From e6a09d929bfebc13ffa157b4540db034f21bb499 Mon Sep 17 00:00:00 2001 From: daniprec Date: Tue, 5 Oct 2021 15:45:21 +0200 Subject: [PATCH 44/47] Choose number of states --- nilmth/hierarchical_clustering.py | 62 ++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/hierarchical_clustering.py index e990cc1..de029b4 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/hierarchical_clustering.py @@ -15,7 +15,6 @@ LIST_APPLIANCES = ["dish_washer", "fridge", "washing_machine"] -LIST_CLUSTER = [1, 2, 3, 4, 5, 6] LIST_DISTANCE = [ "average", # "weighted", @@ -29,6 +28,7 @@ def plot_thresholds_on_distribution( ser: np.array, thresh: Iterable[float], ax: Optional[Axes] = None, + power_min: int = 0, app: str = "", bins: int = 100, ): @@ -49,16 +49,19 @@ def plot_thresholds_on_distribution( """ if ax is None: ax = plt.gca() - y, x, _ = ax.hist(ser, bins=bins, range=(3, ser.max())) + y, x, _ = ax.hist(ser, bins=bins, range=(power_min, ser.max())) ax.set_title(app.capitalize().replace("_", " ")) ax.set_xlabel("Power (watts)") ax.set_ylabel("Frequency") - ax.set_title("Thresholds on power distribution (>=3 watts)") + title = "Thresholds on power distribution" + if power_min > 0: + title += f" (>={power_min} watts)" + ax.set_title(title) ax.grid() # Plot the thresholds for idx, t in enumerate(thresh): ax.axvline(t, color="r", linestyle="--") - ax.text(t + 0.01 * x.max(), y.max(), idx + 1, rotation=0, color="r") + ax.text(t + 0.01 * x.max(), y.max(), idx, rotation=0, color="r") def plot_thresholds_on_series( @@ -89,18 +92,22 @@ def plot_thresholds_on_series( # Plot the thresholds for idx, thresh in enumerate(thresh): ax.axhline(thresh, color="r", linestyle="--") - ax.text(time.max(), thresh + power.max() * 0.01, idx + 1, rotation=0, color="r") + ax.text(time.max(), thresh + power.max() * 0.01, idx, rotation=0, color="r") ax.set_xlabel("Time (s)") ax.set_ylabel("Power (watts)") ax.set_title("Thresholds on sample time series") ax.grid() -def plot_intrinsic_error(intr_error: Iterable[float], ax: Optional[Axes] = None): +def plot_intrinsic_error( + list_states: Iterable[int], intr_error: Iterable[float], ax: Optional[Axes] = None +): """Plots the intrinsic error depending on the number of splits Parameters ---------- + list_states : Iterable[int] + List of states (number of clusters) intr_error : Iterable[float] List of intrinsic error values ax : Axes, optional @@ -108,20 +115,22 @@ def plot_intrinsic_error(intr_error: Iterable[float], ax: Optional[Axes] = None) """ if ax is None: ax = plt.gca() - ax.plot(LIST_CLUSTER, intr_error, ".--") - ymax = min(max(intr_error), 1) - ax.set_ylim(0, ymax) - ax.set_ylabel("Intrinsic Error (NDE)") + ax.plot(list_states, intr_error, ".--") + ax.set_ylabel("Intrinsic Error") ax.set_xlabel("Number of status") ax.set_title("Intrinsic Error depending on splits") ax.grid() -def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None): +def plot_error_reduction( + list_states: Iterable[int], intr_error: Iterable[float], ax: Optional[Axes] = None +): """Plots the intrinsic error reduction depending on the number of splits Parameters ---------- + list_states : Iterable[int] + List of states (number of clusters) intr_error : Iterable[float] List of intrinsic error values ax : Axes, optional @@ -130,7 +139,7 @@ def plot_error_reduction(intr_error: Iterable[float], ax: Optional[Axes] = None) if ax is None: ax = plt.gca() rel_error = -100 * np.divide(np.diff(intr_error), intr_error[:-1]) - ax.plot(LIST_CLUSTER[1:], rel_error, ".--") + ax.plot(list_states[1:], rel_error, ".--") ax.set_ylim(0, 100) ax.set_ylabel("Reduction of Intrinsic Error (%)") ax.set_xlabel("Number of status") @@ -159,9 +168,10 @@ def plot_series_reconstruction(power: np.array, recon: np.array): def plot_clustering_results( ser: np.array, + max_clusters: int = 10, distance: str = "average", centroid: str = "median", - error: str = "nde", + error: str = "mae", plot_recon: bool = False, ): """Plots the results of applying a certain clustering method @@ -171,12 +181,14 @@ def plot_clustering_results( ---------- ser : np.array Contains all the power values + max_clusters : int, optional + Maximum number of clusters, by default 10 distance : str, optional Clustering linkage criteria, by default "average" centroid : str, optional Method to compute the centroids (median or mean), by default "mean" error : str, optional - Error metric (mse, mae, rmse, sae, nde), by default "nde" + Error metric (mse, mae, rmse, sae, nde), by default "mae" plot_recon : bool, optional If True, plots the reconstructed series, by default False """ @@ -184,11 +196,12 @@ def plot_clustering_results( hie = HierarchicalClustering() hie.perform_clustering(ser, distance=distance) # Initialize the list of intrinsic error per number of clusters - intr_error = [0] * len(LIST_CLUSTER) + intr_error = [0] * max_clusters + list_states = [n + 1 for n in range(max_clusters)] # Initialize the empty list of thresholds (sorted) thresh_sorted = [] # Compute thresholds per number of clusters - for idx, n_cluster in enumerate(LIST_CLUSTER): + for idx, n_cluster in enumerate(list_states): hie.compute_thresholds_and_centroids(n_cluster=n_cluster, centroid=centroid) # Initialize threshold class th = Threshold(method="custom") @@ -216,12 +229,12 @@ def plot_clustering_results( plt.show() plt.close() # Initialize plots - fig, axis = plt.subplots(2, 2, figsize=(12, 8)) + fig, axis = plt.subplots(2, 2, figsize=(12, 8), dpi=150) # Plots plot_thresholds_on_distribution(power, thresh_sorted, ax=axis[0, 0]) plot_thresholds_on_series(power, thresh_sorted, ax=axis[0, 1]) - plot_intrinsic_error(intr_error, ax=axis[1, 0]) - plot_error_reduction(intr_error, ax=axis[1, 1]) + plot_intrinsic_error(list_states, intr_error, ax=axis[1, 0]) + plot_error_reduction(list_states, intr_error, ax=axis[1, 1]) # Set the space between subplots fig.tight_layout(rect=[0, 0.03, 1, 0.95]) @@ -278,7 +291,8 @@ def build_synthetic_series( def main( limit: int = 20000, - error: str = "nde", + max_clusters: int = 10, + error: str = "mae", path_data: str = "data-prep", path_threshold: str = "threshold.toml", path_config: str = "nilmth/config.toml", @@ -292,8 +306,10 @@ def main( ---------- limit : int, optional Number of data points to use, by default 20000 + max_clusters : int, optional + Maximum number of clusters, by default 10 error : str, optional - Error metric (mse, mae, rmse, sae, nde), by default "nde" + Error metric (mse, mae, rmse, sae, nde), by default "mae" path_data : str, optional Path to the preprocessed data folder, by default "data-prep" path_threshold : str, optional @@ -330,7 +346,9 @@ def main( # Loop through distances for distance in LIST_DISTANCE: print(f"-----\nAppliance: {app} | Distance: {distance}") - plot_clustering_results(ser, distance=distance, error=error) + plot_clustering_results( + ser, max_clusters=max_clusters, distance=distance, error=error + ) # Place title in figure plt.gcf().suptitle(f"{appliance}, Linkage: {distance}") # Save and close the figure From bc65add961015704d878ce778752209b2c604a65 Mon Sep 17 00:00:00 2001 From: daniprec Date: Tue, 5 Oct 2021 15:54:34 +0200 Subject: [PATCH 45/47] Move functions to plot_clustering --- .../plot_clustering.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename nilmth/{hierarchical_clustering.py => plot/plot_clustering.py} (98%) diff --git a/nilmth/hierarchical_clustering.py b/nilmth/plot/plot_clustering.py similarity index 98% rename from nilmth/hierarchical_clustering.py rename to nilmth/plot/plot_clustering.py index de029b4..847ad0a 100644 --- a/nilmth/hierarchical_clustering.py +++ b/nilmth/plot/plot_clustering.py @@ -293,10 +293,10 @@ def main( limit: int = 20000, max_clusters: int = 10, error: str = "mae", - path_data: str = "data-prep", - path_threshold: str = "threshold.toml", - path_config: str = "nilmth/config.toml", - path_output: str = "outputs/hieclust", + path_data: str = "../data-prep", + path_threshold: str = "../threshold.toml", + path_config: str = "../nilmth/config.toml", + path_output: str = "../outputs/hieclust", ): """Performs the hierarchical clustering on the given list of appliances, testing different linkage methods and number of splits. For each combination, From 98357aacecdb40ddf0bd6f25236eb60f0b6f2837 Mon Sep 17 00:00:00 2001 From: daniprec Date: Tue, 5 Oct 2021 16:08:37 +0200 Subject: [PATCH 46/47] Remove glob attributes from HierarchicalClustering --- nilmth/data/clustering.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/nilmth/data/clustering.py b/nilmth/data/clustering.py index c91db57..780e182 100644 --- a/nilmth/data/clustering.py +++ b/nilmth/data/clustering.py @@ -8,19 +8,37 @@ class HierarchicalClustering: - x: np.array = None - z: np.array = None - thresh: np.array = None - centroids: np.array = None - dendrogram: dict = None - def __init__( self, distance: str = "average", n_cluster: int = 2, criterion: str = "maxclust" ): + """This object is able to perform Hierarchical Clustering on a given set of points + + Parameters + ---------- + distance : str, optional + Clustering distance criteria, by default "average" + n_cluster : int, optional + Number of clusters to form, by default 2 + criterion : str, optional + Criterion used to compute the clusters, by default "maxclust" + """ self.distance = distance self.n_cluster = n_cluster self.criterion = criterion + # Attributes filled with `perform_clustering` + self.x = np.empty(0) # Set of data points + self.z = np.empty(0) # The hierarchical clustering encoded as a linkage matrix + # z[i] will tell us which clusters were merged in the i-th iteration + + # Attributes filled with `plot_dendogram` + self.dendrogram = {} + # A dictionary of data structures computed to render the dendrogram + + # Attributes filled with `compute_thresholds_and_centroids` + self.thresh = np.empty(0) + self.centroids = np.empty(0) + def perform_clustering( self, ser: np.array, distance: Optional[str] = None ) -> np.array: @@ -32,11 +50,6 @@ def perform_clustering( Series of points to group in clusters distance : str, optional Clustering distance criteria, by default None (takes the one from the class) - - Returns - ------- - np.array - Z[i] will tell us which clusters were merged in the i-th iteration """ self.distance = distance if distance is not None else self.distance # The shape of our X matrix must be (n, m) From 0c72a0a049e1cd2b95e5836cb25272ba01165980 Mon Sep 17 00:00:00 2001 From: daniprec Date: Tue, 5 Oct 2021 16:31:08 +0200 Subject: [PATCH 47/47] Remove global attributes from objects --- nilmth/data/dataloader.py | 2 -- nilmth/data/dataset.py | 12 +++++++----- nilmth/data/preprocessing.py | 6 ++++-- nilmth/data/threshold.py | 21 ++++++++++++--------- nilmth/data/ukdale.py | 16 +++++++++++----- 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/nilmth/data/dataloader.py b/nilmth/data/dataloader.py index f19631c..4c5595f 100644 --- a/nilmth/data/dataloader.py +++ b/nilmth/data/dataloader.py @@ -10,8 +10,6 @@ class DataLoader(data.DataLoader): - dataset: DataSet = None - def __init__( self, path_data: str, diff --git a/nilmth/data/dataset.py b/nilmth/data/dataset.py index e5aceb1..a6f0910 100644 --- a/nilmth/data/dataset.py +++ b/nilmth/data/dataset.py @@ -10,11 +10,6 @@ class DataSet(data.Dataset): - files: list = list() - appliances: list = list() - status: list = list() - threshold: Threshold = None - def __init__( self, path_data: str, @@ -39,6 +34,13 @@ def __init__( self.validation_size = valid_size self.random_split = random_split self.random_seed = random_seed + + # Attributes filled by `_list_files` + self.files = list() # List of files + + # Attributes filled by `_get_parameters_from_file` + self.status = list() # List of status columns + self._list_files(path_data) self._get_parameters_from_file() diff --git a/nilmth/data/preprocessing.py b/nilmth/data/preprocessing.py index 18ef06e..94ffb61 100644 --- a/nilmth/data/preprocessing.py +++ b/nilmth/data/preprocessing.py @@ -10,10 +10,9 @@ class Preprocessing: - dataset: str = "Wrapper" - def __init__( self, + dataset: str = "Wrapper", appliances: list = None, buildings: dict = None, dates: dict = None, @@ -28,6 +27,8 @@ def __init__( Parameters ---------- + dataset : str, optional + Name of the dataset, by default "Wrapper" appliances : list, optional List of appliances, by default empty buildings : dict, optional @@ -49,6 +50,7 @@ def __init__( kwargs Additional arguments, not taken into account """ + self.dataset = dataset # Read parameters from config files self.appliances = [] if appliances is None else sorted(to_list(appliances)) self.buildings = [] if buildings is None else buildings[self.dataset] diff --git a/nilmth/data/threshold.py b/nilmth/data/threshold.py index 4639697..ea9a02c 100644 --- a/nilmth/data/threshold.py +++ b/nilmth/data/threshold.py @@ -20,13 +20,6 @@ class Threshold: - num_status: int = 2 - thresholds: np.array = None # (appliance, status) - centroids: np.array = None # (appliance, status) - use_std: bool = False - min_on: list = None - min_off: list = None - def __init__( self, appliances: list = None, @@ -40,6 +33,18 @@ def __init__( self.num_status = num_status # Set the default status function self._status_fun = self._compute_status + + # Attributes filled by `_initialize_params` + self.thresholds = np.zeros( + (self.num_apps, self.num_status) + ) # (appliance, status) + self.centroids = np.zeros( + (self.num_apps, self.num_status) + ) # (appliance, status) + self.use_std = False + self.min_on = list() + self.min_off = list() + self._initialize_params() def __repr__(self): @@ -51,8 +56,6 @@ def _initialize_params(self): Given the method name and list of appliances, this function defines the necessary parameters to use the method """ - self.thresholds = np.zeros((self.num_apps, self.num_status)) - self.centroids = np.zeros((self.num_apps, self.num_status)) if self.method == "vs": # Variance-Sensitive threshold self.use_std = True diff --git a/nilmth/data/ukdale.py b/nilmth/data/ukdale.py index 638ba71..11e649f 100644 --- a/nilmth/data/ukdale.py +++ b/nilmth/data/ukdale.py @@ -2,21 +2,27 @@ import pandas as pd from pandas import Series -from pandas.io.pytables import HDFStore from nilmth.data.preprocessing import Preprocessing from nilmth.utils.string import homogenize_label class Ukdale(Preprocessing): - dataset: str = "ukdale" - datastore: HDFStore = None - def __init__(self, path_h5: str, path_labels: str, **kwargs): - super(Ukdale, self).__init__(**kwargs) + """UK-DALE preprocessing object + + Parameters + ---------- + path_h5 : str + Path to the h5 file + path_labels : str + Path to the labels folder + """ + super(Ukdale, self).__init__(dataset="ukdale", **kwargs) self._path_h5 = path_h5 self._path_labels = path_labels # Load the datastore + self.datastore = None # HDFStore self._load_datastore() def _load_datastore(self):