diff --git a/otava/analysis.py b/otava/analysis.py index 21bb33c1..54cfdb9a 100644 --- a/otava/analysis.py +++ b/otava/analysis.py @@ -104,26 +104,6 @@ def change_point( return ChangePoint.from_candidate(candidate, stats) -def fill_missing(data: Sequence[SupportsFloat]): - """ - Forward-fills None occurrences with nearest previous non-None values. - Initial None values are back-filled with the nearest future non-None value. - - TODO: Remove this. - """ - prev = None - for i in range(len(data)): - if data[i] is None and prev is not None: - data[i] = prev - prev = data[i] - - prev = None - for i in reversed(range(len(data))): - if data[i] is None and prev is not None: - data[i] = prev - prev = data[i] - - def merge( change_points: TtestCPList, series: Sequence[SupportsFloat], max_pvalue: float, min_magnitude: float ) -> TtestCPList: diff --git a/otava/series.py b/otava/series.py index 23f4af60..e3d9e828 100644 --- a/otava/series.py +++ b/otava/series.py @@ -24,7 +24,6 @@ TTestStats, compute_change_points, compute_change_points_orig, - fill_missing, ) from otava.change_point_divisive.base import ( ChangePoint, @@ -166,7 +165,6 @@ def __compute_change_points( weak_change_points = ChangePointsByMetric() for metric in series.data.keys(): values = series.data[metric].copy() - fill_missing(values) if options.orig_edivisive: change_points, _ = compute_change_points_orig( values, @@ -327,8 +325,6 @@ def append(self, time, new_data, attributes): attributes=self.__series.attributes_at(cp.index), ) ) - # TODO: Remove this. It should not be a requirement that metrics have the same history. - fill_missing(self.__series.data[metric]) r = ChangePointsByMetric.from_dict(result) w = ChangePointsByMetric.from_dict(weak_change_points) diff --git a/tests/analysis_test.py b/tests/analysis_test.py index ef548a77..6c6df88d 100644 --- a/tests/analysis_test.py +++ b/tests/analysis_test.py @@ -21,23 +21,10 @@ TTestSignificanceTester, compute_change_points, compute_change_points_orig, - fill_missing, ) from otava.change_point_divisive.base import CandidateChangePoint -def test_fill_missing(): - list1 = [None, None, 1.0, 1.2, 0.5] - list2 = [1.0, 1.2, None, None, 4.3] - list3 = [1.0, 1.2, 0.5, None, None] - fill_missing(list1) - fill_missing(list2) - fill_missing(list3) - assert list1 == [1.0, 1.0, 1.0, 1.2, 0.5] - assert list2 == [1.0, 1.2, 1.2, 1.2, 4.3] - assert list3 == [1.0, 1.2, 0.5, 0.5, 0.5] - - def test_single_series(): series = [ 1.02, diff --git a/tests/series_test.py b/tests/series_test.py index 28ad2a90..cab900a9 100644 --- a/tests/series_test.py +++ b/tests/series_test.py @@ -149,11 +149,11 @@ def test_div_by_zero(): def test_change_point_detection_performance(): - timestamps = range(0, 90) # 3 months of data + timestamps = range(90) # 3 months of data series = [random() for x in timestamps] start_time = time.process_time() - for run in range(0, 10): # 10 series + for run in range(10): # 10 series test = Series( "test", branch=None, @@ -360,3 +360,50 @@ def test_orig_edivisive(): # assert len(change_points) == 2 # assert change_points[0].index == 4 # assert change_points[1].index == 6 + + +def test_series_sparse_commits(): + """Verify series processes sparse commit sequences without artificial padding.""" + name = "sparse_commit_test" + branch = "main" + time_points = [1000, 1001, 1005, 1008] + metrics = {} + data = {"latency": [100.5, 101.0, 105.2, 98.7]} + # Match length of attributes list to len(time_points) == 4 + attributes = {"env": ["ci", "ci", "ci", "ci"]} + + series = Series(name, branch, time_points, metrics, data, attributes) + + assert len(series.time) == 4 + assert series.time == [1000, 1001, 1005, 1008] + assert series.data["latency"] == [100.5, 101.0, 105.2, 98.7] + + +def test_series_irregular_timestamps(): + """Verify change detection operates on unpadded time intervals.""" + name = "irregular_ts_test" + branch = "main" + time_points = [1600000000, 1600000010, 1600000300, 1600003600] + metrics = {} + data = {"duration": [50.0, 51.0, 50.5, 95.0]} + attributes = {} + + series = Series(name, branch, time_points, metrics, data, attributes) + + assert len(series.time) == 4 + assert series.data["duration"] == [50.0, 51.0, 50.5, 95.0] + + +def test_series_raw_initialization(): + """Verify Series initialization operates directly on raw unpadded input data.""" + name = "raw_init_test" + branch = "main" + time_points = [1, 2, 3] + metrics = {} + data = {"throughput": [10.0, 12.0, 11.5]} + attributes = {} + + series = Series(name, branch, time_points, metrics, data, attributes) + + assert len(series.time) == 3 + assert series.data["throughput"] == [10.0, 12.0, 11.5]