From fdaaa86aae9f1bb0e5c313a3cbfef4bbff5bed1c Mon Sep 17 00:00:00 2001 From: Paige Rubendall Date: Tue, 14 May 2024 16:52:35 -0400 Subject: [PATCH] geting percent difference rh-pre-commit.version: 2.2.0 rh-pre-commit.check-secrets: ENABLED Signed-off-by: Auto User --- README.md | 7 +- orion.py | 12 +++- percentdiff.jpg | Bin 0 -> 28176 bytes pkg/algorithms/algorithmFactory.py | 3 + pkg/algorithms/cmr/__init__.py | 4 ++ pkg/algorithms/cmr/cmr.py | 111 +++++++++++++++++++++++++++++ pkg/constants.py | 1 + pkg/runTest.py | 24 +++++-- pkg/utils.py | 21 +++++- 9 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 percentdiff.jpg create mode 100644 pkg/algorithms/cmr/__init__.py create mode 100644 pkg/algorithms/cmr/cmr.py diff --git a/README.md b/README.md index f6e9d02..22e144a 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,11 @@ Additionally, users can specify a custom path for the output CSV file using the Orion now supports anomaly detection for your data. Use the ```--anomaly-detection``` command to start the anomaly detection process. + +To be able to find significant percent differences in workload runs, use the ```--cmr``` command. This will compare the most recent run with any previous matching runs or baseline UUIDs. If more than 1 other run is found from the most recent, the values will be meaned together and then compared with the previous run. Use with *direction: 0* (set in the config) when using ```-o json``` format to see percent differences + +![cmr percent difference](percentdiff.jpg) + You can now constrain your look-back period using the ```--lookback``` option. The format for look-back is ```XdYh```, where X represents the number of days and Y represents the number of hours. To specify how many runs to look back, you can use the ```--lookback-size``` option. By default, this option is set to 10000. @@ -156,7 +161,7 @@ This is similar to how car manufacturers warranty plays out such as 5years or 60 You can open the match requirement by using the ```--node-count``` option to find any matching uuid based on the metadata and not have to have the same jobConfig.jobIterations. This variable is a ```True``` or ```False```, defaulted to False. -**_NOTE:_** The ```--hunter-analyze``` and ```--anomaly-detection``` flags are mutually exclusive. They cannot be used together because they represent different algorithms designed for distinct use cases. +**_NOTE:_** The ```cmr```, ```--hunter-analyze``` and ```--anomaly-detection``` flags are mutually exclusive. They cannot be used together because they represent different algorithms designed for distinct use cases. ### Daemon mode The core purpose of Daemon mode is to operate Orion as a self-contained server, dedicated to handling incoming requests. By sending a POST request accompanied by a test name of predefined tests, users can trigger change point detection on the provided metadata and metrics. Following the processing, the response is formatted in JSON, providing a structured output for seamless integration and analysis. To trigger daemon mode just use the following commands diff --git a/orion.py b/orion.py index 4935372..2a66e66 100644 --- a/orion.py +++ b/orion.py @@ -69,6 +69,14 @@ def cli(max_content_width=120): # pylint: disable=unused-argument # pylint: disable=too-many-locals @cli.command(name="cmd") +@click.option( + "--cmr", + is_flag=True, + help="Generate percent difference in comparison", + cls=MutuallyExclusiveOption, + mutually_exclusive=["anomaly_detection","hunter_analyze"], +) +@click.option("--filter", is_flag=True, help="Generate percent difference in comparison") @click.option("--config", default="config.yaml", help="Path to the configuration file") @click.option( "--save-data-path", default="data.csv", help="Path to save the output file" @@ -79,7 +87,7 @@ def cli(max_content_width=120): # pylint: disable=unused-argument is_flag=True, help="run hunter analyze", cls=MutuallyExclusiveOption, - mutually_exclusive=["anomaly_detection"], + mutually_exclusive=["anomaly_detection","cmr"], ) @click.option("--anomaly-window", type=int, callback=validate_anomaly_options, help="set window size for moving average for anomaly-detection") @click.option("--min-anomaly-percent", type=int, callback=validate_anomaly_options, help="set minimum percentage difference from moving average for data point to be detected as anomaly") @@ -88,7 +96,7 @@ def cli(max_content_width=120): # pylint: disable=unused-argument is_flag=True, help="run anomaly detection algorithm powered by isolation forest", cls=MutuallyExclusiveOption, - mutually_exclusive=["hunter_analyze"], + mutually_exclusive=["hunter_analyze","cmr"], ) @click.option( "-o", diff --git a/percentdiff.jpg b/percentdiff.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c66352bda405332140f7c0ff3ecdbbe001a88b2b GIT binary patch literal 28176 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 Ma{N!e{yqD@0OD(QC;$Ke literal 0 HcmV?d00001 diff --git a/pkg/algorithms/algorithmFactory.py b/pkg/algorithms/algorithmFactory.py index a4d47f8..720a35d 100644 --- a/pkg/algorithms/algorithmFactory.py +++ b/pkg/algorithms/algorithmFactory.py @@ -6,6 +6,7 @@ import pkg.constants as cnsts from .edivisive import EDivisive from .isolationforest import IsolationForestWeightedMean +from .cmr import CMR class AlgorithmFactory: # pylint: disable= too-few-public-methods, too-many-arguments, line-too-long @@ -30,4 +31,6 @@ def instantiate_algorithm(self, algorithm: str, matcher: Matcher, dataframe:pd.D return EDivisive(matcher, dataframe, test, options, metrics_config) if algorithm == cnsts.ISOLATION_FOREST: return IsolationForestWeightedMean(matcher, dataframe, test, options, metrics_config) + if algorithm == cnsts.CMR: + return CMR(matcher, dataframe, test, options, metrics_config) raise ValueError("Invalid algorithm called") diff --git a/pkg/algorithms/cmr/__init__.py b/pkg/algorithms/cmr/__init__.py new file mode 100644 index 0000000..f93d771 --- /dev/null +++ b/pkg/algorithms/cmr/__init__.py @@ -0,0 +1,4 @@ +""" +Init for CMR Algorithm +""" +from .cmr import CMR diff --git a/pkg/algorithms/cmr/cmr.py b/pkg/algorithms/cmr/cmr.py new file mode 100644 index 0000000..5025691 --- /dev/null +++ b/pkg/algorithms/cmr/cmr.py @@ -0,0 +1,111 @@ +"""CMR - Comparing Mean Responses Algorithm""" + +# pylint: disable = line-too-long +from typing import List +import pandas as pd +import numpy + +from fmatch.logrus import SingletonLogger +from hunter.series import ChangePoint, ComparativeStats +from pkg.algorithms.algorithm import Algorithm + + +class CMR(Algorithm): + """Implementation of the CMR algorithm + Will Combine metrics into 2 lines and compare with a tolerancy to set pass fail + + Args: + Algorithm (Algorithm): Inherits + """ + + + def _analyze(self): + """Analyze the dataframe with meaning any previous data and generate percent change with a current uuid + + Returns: + series: data series that contains attributes and full dataframe + change_points_by_metric: list of ChangePoints + """ + logger_instance = SingletonLogger.getLogger("Orion") + logger_instance.info("Starting analysis using CMR") + self.dataframe["timestamp"] = pd.to_datetime(self.dataframe["timestamp"]) + self.dataframe["timestamp"] = self.dataframe["timestamp"].astype(int) // 10**9 + + if len(self.dataframe.index) == 1: + series= self.setup_series() + series.data = self.dataframe + return series, {} + # if larger than 2 rows, need to get the mean of 0 through -2 + self.dataframe = self.combine_and_average_runs(self.dataframe) + + series= self.setup_series() + + df, change_points_by_metric = self.run_cmr(self.dataframe) + series.data= df + return series, change_points_by_metric + + + def run_cmr(self, dataframe_list: pd.DataFrame): + """ + Generate the percent difference in a 2 row dataframe + + Args: + dataframe_list (pd.DataFrame): data frame of all data to compare on + + Returns: + pd.Dataframe, dict[metric_name, ChangePoint]: Returned data frame and change points + """ + metric_columns = self.metrics_config.keys() + change_points_by_metric={ k:[] for k in metric_columns } + max_date_time = pd.Timestamp.max.to_pydatetime() + max_time = max_date_time.timestamp() + + for column in metric_columns: + + change_point = ChangePoint(metric=column, + index=1, + time=max_time, + stats=ComparativeStats( + mean_1=dataframe_list[column][0], + mean_2=dataframe_list[column][1], + std_1=0, + std_2=0, + pvalue=1 + )) + change_points_by_metric[column].append(change_point) + + # based on change point generate pass/fail + return dataframe_list, change_points_by_metric + + def combine_and_average_runs(self, dataFrame: pd.DataFrame): + """ + If more than 1 previous run, mean data together into 1 single row + Combine with current run into 1 data frame (current run being -1 index) + + Args: + dataFrame (pd.DataFrame): data to combine into 2 rows + + Returns: + pd.Dataframe: data frame of most recent run and averaged previous runs + """ + i = 0 + + last_row = dataFrame.tail(1) + dF = dataFrame[:-1] + data2 = {} + + metric_columns = list(dataFrame.columns) + for column in metric_columns: + + if isinstance(dF.loc[0, column], (numpy.float64, numpy.int64)): + mean = dF[column].mean() + data2[column] = [mean] + else: + column_list = dF[column].tolist() + non_numeric_joined_list = ','.join(column_list) + data2[column] = [non_numeric_joined_list] + i += 1 + df2 = pd.DataFrame(data2) + + result = pd.concat([df2, last_row], ignore_index=True) + return result diff --git a/pkg/constants.py b/pkg/constants.py index 87813a3..ea9a770 100644 --- a/pkg/constants.py +++ b/pkg/constants.py @@ -6,3 +6,4 @@ JSON="json" TEXT="text" JUNIT="junit" +CMR="cmr" diff --git a/pkg/runTest.py b/pkg/runTest.py index 5948651..990bfde 100644 --- a/pkg/runTest.py +++ b/pkg/runTest.py @@ -9,7 +9,24 @@ import pkg.constants as cnsts from pkg.utils import get_datasource, process_test, get_subtracted_timestamp +def get_algorithm_type(kwargs): + """Switch Case of getting algorithm name + Args: + kwargs (dict): passed command line arguments + + Returns: + str: algorithm name + """ + if kwargs["hunter_analyze"]: + algorithm_name = cnsts.EDIVISIVE + elif kwargs["anomaly_detection"]: + algorithm_name = cnsts.ISOLATION_FOREST + elif kwargs['cmr']: + algorithm_name = cnsts.CMR + else: + algorithm_name = None + return algorithm_name def run(**kwargs: dict[str, Any]) -> dict[str, Any]: #pylint: disable = R0914 """run method to start the tests @@ -48,11 +65,8 @@ def run(**kwargs: dict[str, Any]) -> dict[str, Any]: #pylint: disable = R0914 if fingerprint_matched_df is None: sys.exit(3) # No data present - if kwargs["hunter_analyze"]: - algorithm_name = cnsts.EDIVISIVE - elif kwargs["anomaly_detection"]: - algorithm_name = cnsts.ISOLATION_FOREST - else: + algorithm_name = get_algorithm_type(kwargs) + if algorithm_name is None: return None, None algorithmFactory = AlgorithmFactory() diff --git a/pkg/utils.py b/pkg/utils.py index e4add5e..94cf457 100644 --- a/pkg/utils.py +++ b/pkg/utils.py @@ -258,10 +258,12 @@ def process_test( shortener = pyshorteners.Shortener(timeout=10) merged_df["buildUrl"] = merged_df["uuid"].apply( lambda uuid: ( - shortener.tinyurl.short(buildUrls[uuid]) + shorten_url(shortener, buildUrls[uuid]) if options["convert_tinyurl"] else buildUrls[uuid] - ) # pylint: disable = cell-var-from-loop + ) + + # pylint: disable = cell-var-from-loop ) merged_df=merged_df.reset_index(drop=True) #save the dataframe @@ -269,6 +271,21 @@ def process_test( match.save_results(merged_df, csv_file_path=output_file_path) return merged_df, metrics_config +def shorten_url(shortener: any, uuids: str) -> str: + """Shorten url if there is a list of buildUrls + + Args: + shortener (any): shortener object to use tinyrl.short on + uuids (List[str]): List of uuids to shorten + + Returns: + str: a combined string of shortened urls + """ + short_url_list = [] + for buildUrl in uuids.split(","): + short_url_list.append(shortener.tinyurl.short(buildUrl)) + short_url = ','.join(short_url_list) + return short_url def get_metadata_with_uuid(uuid: str, match: Matcher) -> Dict[Any, Any]: """Gets metadata of the run from each test