-
Notifications
You must be signed in to change notification settings - Fork 30
Add --deterministic-edivisive variation #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
8435a57
3710938
d6a2ce1
6228b91
d025635
5114231
ed271bd
6af684c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| TTestSignificanceTester, | ||
| TTestStats, | ||
| compute_change_points, | ||
| compute_change_points_deterministic, | ||
| compute_change_points_orig, | ||
| fill_missing, | ||
| ) | ||
|
|
@@ -37,19 +38,25 @@ class AnalysisOptions: | |
| max_pvalue: float | ||
| min_magnitude: float | ||
| orig_edivisive: bool | ||
| deterministic_edivisive: bool | ||
| split_edivisive: bool | ||
|
|
||
| def __init__(self): | ||
| self.window_len = 50 | ||
| self.max_pvalue = 0.001 | ||
| self.min_magnitude = 0.0 | ||
| self.orig_edivisive = False | ||
| self.deterministic_edivisive = False | ||
| self.split_edivisive = True | ||
|
|
||
| def to_json(self): | ||
| return { | ||
| "window_len": self.window_len, | ||
| "max_pvalue": self.max_pvalue, | ||
| "min_magnitude": self.min_magnitude, | ||
| "orig_edivisive": self.orig_edivisive | ||
| "orig_edivisive": self.orig_edivisive, | ||
| "deterministic_edivisive": self.deterministic_edivisive, | ||
| "split_edivisive": self.split_edivisive, | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -259,6 +266,20 @@ def __compute_change_points( | |
| time=series.time[cp_ttest.index], metric=metric, stats=cp_ttest.stats | ||
| ) | ||
| ) | ||
| elif options.deterministic_edivisive: | ||
| # weak_change_points == change_points when min_magnitude == 0. | ||
| # when min_magnitude > 0 then change_points is the subset where the change was >= min_magnitude. | ||
| change_points, weak_cps = compute_change_points_deterministic(values, max_pvalue=options.max_pvalue, min_magnitude=options.min_magnitude) | ||
| tester = TTestSignificanceTester(options.max_pvalue) | ||
| intervals = tester.get_intervals(change_points) | ||
| for c in change_points: | ||
| cp_ttest = tester.change_point(c.to_candidate(), values, intervals) | ||
| result[metric].append( | ||
| ChangePoint( | ||
| index=cp_ttest.index, qhat=cp_ttest.qhat, | ||
| time=series.time[cp_ttest.index], metric=metric, stats=cp_ttest.stats | ||
| ) | ||
| ) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Arguably this is too much copy paste from above and should be refactored into a separate method. The reason I don't do that is that this transformation from one ChangePoint to another is the more fundamental problem that we need to discuss and fix. I've opened #151 for that discussion. In the mean time I prefer to keep this code ugly and visible so we don't forget to fix it.Trying to make it look better via refactoring but not solving the fundamental issue is IMO counter productive. |
||
| else: | ||
| change_points, weak_cps = compute_change_points( | ||
| values, | ||
|
|
@@ -278,8 +299,7 @@ def __compute_change_points( | |
| index=c.index, qhat=0.0, time=series.time[c.index], metric=metric, stats=c.stats | ||
| ) | ||
| ) | ||
| # If you got an exception and are wondering about the next row... | ||
| # weak_cps is an optimization which you can ignore | ||
|
|
||
| return result, weak_change_points | ||
|
|
||
| @staticmethod | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
| import csv | ||
| import tempfile | ||
| import textwrap | ||
| import unittest | ||
| from datetime import datetime, timedelta | ||
| from pathlib import Path | ||
| from unittest.mock import patch | ||
|
|
||
| import otava.series | ||
| from otava.main import script_main | ||
|
|
||
|
|
||
| class CliOptionsTest(unittest.TestCase): | ||
|
|
||
| # Test --deterministic-edivisive in various ways | ||
| def test_no_cli_option(self): | ||
| with patch('otava.series.compute_change_points') as mock_split: | ||
| script_main(args=[]) | ||
| mock_split.assert_not_called() | ||
|
|
||
| def test_default_cli_option(self): | ||
| with patch('otava.series.compute_change_points') as mock_split: | ||
| mock_split.return_value = ([], []) | ||
| with tempfile.TemporaryDirectory() as td: | ||
| td_path = Path(td) | ||
| csv_path, timestamps, config_path, test_name = _create_files_in_temp_dir(td_path) | ||
| # _uv_run(td_path, test_name) | ||
| config_path_str = "" + str(config_path) | ||
| script_main(args=["analyze", "--config", config_path_str, test_name]) | ||
|
|
||
| assert otava.series.compute_change_points.call_count == 2 | ||
|
|
||
| def test_split_cli_option(self): | ||
| with patch('otava.series.compute_change_points') as mock_split: | ||
| mock_split.return_value = ([], []) | ||
| with tempfile.TemporaryDirectory() as td: | ||
| td_path = Path(td) | ||
| csv_path, timestamps, config_path, test_name = _create_files_in_temp_dir(td_path) | ||
| # _uv_run(td_path, test_name) | ||
| config_path_str = "" + str(config_path) | ||
| script_main(args=["analyze", "--config", config_path_str, "--split-edivisive", test_name]) | ||
|
|
||
| assert otava.series.compute_change_points.call_count == 2 | ||
|
|
||
| def test_orig_cli_option(self): | ||
| with patch('otava.series.compute_change_points_orig') as mock_orig: | ||
| mock_orig.return_value = ([], None) | ||
| with tempfile.TemporaryDirectory() as td: | ||
| td_path = Path(td) | ||
| csv_path, timestamps, config_path, test_name = _create_files_in_temp_dir(td_path) | ||
| # _uv_run(td_path, test_name) | ||
| config_path_str = "" + str(config_path) | ||
| script_main(args=["analyze", "--config", config_path_str, "--orig-edivisive", test_name]) | ||
|
|
||
| assert otava.series.compute_change_points_orig.call_count == 2 | ||
|
|
||
| def test_deterministic_cli_option(self): | ||
| with patch('otava.series.compute_change_points_deterministic') as mock_deterministic: | ||
| mock_deterministic.return_value = ([], None) | ||
| with tempfile.TemporaryDirectory() as td: | ||
| td_path = Path(td) | ||
| csv_path, timestamps, config_path, test_name = _create_files_in_temp_dir(td_path) | ||
| # _uv_run(td_path, test_name) | ||
| config_path_str = "" + str(config_path) | ||
| script_main(args=["analyze", "--config", config_path_str, "--deterministic-edivisive", test_name]) | ||
|
|
||
| assert otava.series.compute_change_points_deterministic.call_count == 2 | ||
|
|
||
|
|
||
| def _create_files_in_temp_dir(td_path: Path): | ||
| data_dir = td_path / "data" | ||
| data_dir.mkdir(parents=True, exist_ok=True) | ||
|
|
||
| csv_path, timestamps = _create_csv_data_file_for_test(td_path) | ||
| config_path, test_name = _create_csv_config_file_for_test(td_path) | ||
|
|
||
| return csv_path, timestamps, config_path, test_name | ||
|
|
||
|
|
||
| def _create_csv_data_file_for_test(td_path: Path): | ||
| # create data directory and write CSV | ||
| data_dir = td_path / "data" | ||
| csv_path = data_dir / "local_sample.csv" | ||
|
|
||
| # Generate some CSV content | ||
| now = datetime.now() | ||
| n = 10 | ||
| timestamps = [now - timedelta(days=i) for i in range(n)] | ||
| metrics1 = [154023, 138455, 143112, 149190, 132098, 151344, 155145, 148889, 149466, 148209] | ||
| metrics2 = [10.43, 10.23, 10.29, 10.91, 10.34, 10.69, 9.23, 9.11, 9.13, 9.03] | ||
| data_points = [] | ||
| for i in range(n): | ||
| data_points.append( | ||
| ( | ||
| timestamps[i].strftime("%Y.%m.%d %H:%M:%S %z"), # time | ||
| "aaa" + str(i), # commit | ||
| metrics1[i], | ||
| metrics2[i], | ||
| ) | ||
| ) | ||
|
|
||
| with open(csv_path, "w", newline="") as f: | ||
| writer = csv.writer(f) | ||
| writer.writerow(["time", "commit", "metric1", "metric2"]) | ||
| writer.writerows(data_points) | ||
|
|
||
| return csv_path, timestamps | ||
|
|
||
|
|
||
| def _create_csv_config_file_for_test(td_path: Path): | ||
| data_dir = td_path / "data" | ||
| csv_path = str(data_dir / "local_sample.csv") | ||
|
|
||
| config_content = textwrap.dedent( | ||
| """\ | ||
| tests: | ||
| sample_for_test: | ||
| type: csv | ||
| file: """ + csv_path + """ | ||
| time_column: time | ||
| attributes: [commit] | ||
| metrics: [metric1, metric2] | ||
| csv_options: | ||
| delimiter: "," | ||
| quotechar: "'" | ||
| """ | ||
| ) | ||
| config_path = td_path / "otava.yaml" | ||
| config_path.write_text(config_content, encoding="utf-8") | ||
| return config_path, "sample_for_test" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could also have used the argparse
choicestype. Did this for backward compatibility.Also one could argue that creating different variations like this is the wrong direction and we should instead just expose all of the sub-features as options the user can use to compose their own combination. My argument against this is that most users want one authoritative solution. And half of our users are not capable of understanding what the math is doing anyway, and the other half don't want to understand. (I'm myself in the latter group, if not the former, even :- )