Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions otava/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.

import copy
from dataclasses import dataclass
from typing import List, Optional, Sequence, SupportsFloat, Tuple

Expand All @@ -24,7 +25,7 @@
from otava.change_point_divisive.base import (
BaseStats,
CandidateChangePoint,
ChangePoint,
ChangePointOtava,
GenericStats,
SignificanceTester,
)
Expand Down Expand Up @@ -97,11 +98,11 @@ def to_json(self):


# Generic Change Point List
GenCPList = List[ChangePoint[GenericStats]]
GenCPList = List[ChangePointOtava[GenericStats]]
# Permutation Change Point List
PermCPList = List[ChangePoint[PermutationStats]]
PermCPList = List[ChangePointOtava[PermutationStats]]
# T-test Change Point List
TtestCPList = List[ChangePoint[TTestStats]]
TtestCPList = List[ChangePointOtava[TTestStats]]


class TTestSignificanceTester(SignificanceTester):
Expand Down Expand Up @@ -130,7 +131,7 @@ def compare(self, left: Sequence[SupportsFloat], right: Sequence[SupportsFloat])

def change_point(
self, candidate: CandidateChangePoint, series: Sequence[SupportsFloat], intervals: List[slice]
) -> ChangePoint[TTestStats]:
) -> ChangePointOtava[TTestStats]:
"""
Computes properties of the change point if the Candidate Change Point based on the provided intervals.

Expand Down Expand Up @@ -167,7 +168,7 @@ def change_point(
left = series[left_interval]
right = series[right_interval]
stats = self.compare(left, right)
return ChangePoint.from_candidate(candidate, stats)
return ChangePointOtava.from_candidate(candidate, stats)


def fill_missing(data: Sequence[SupportsFloat]):
Expand Down Expand Up @@ -323,4 +324,4 @@ def compute_change_points(
"""
first_pass_pvalue = max_pvalue * 10 if max_pvalue < 0.05 else (max_pvalue * 2 if max_pvalue < 0.5 else max_pvalue)
weak_change_points = split(series, window_len, first_pass_pvalue, new_points=new_data, old_cp=old_weak_cp)
return merge(weak_change_points, series, max_pvalue, min_magnitude), weak_change_points
return merge(copy.copy(weak_change_points), series, max_pvalue, min_magnitude), weak_change_points
4 changes: 2 additions & 2 deletions otava/bigquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from google.cloud import bigquery
from google.oauth2 import service_account

from otava.analysis import ChangePoint
from otava.analysis import ChangePointOtava
from otava.test_config import BigQueryTestConfig


Expand Down Expand Up @@ -87,7 +87,7 @@ def insert_change_point(
test: BigQueryTestConfig,
metric_name: str,
attributes: Dict,
change_point: ChangePoint,
change_point: ChangePointOtava,
):
kwargs = {**attributes, **{test.time_column: datetime.utcfromtimestamp(change_point.time)}}
update_stmt = test.update_stmt.format(
Expand Down
14 changes: 7 additions & 7 deletions otava/change_point_divisive/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class BaseStats:


@dataclass
class ChangePoint(CandidateChangePoint, Generic[GenericStats]):
class ChangePointOtava(CandidateChangePoint, Generic[GenericStats]):
'''Change point class, defined by index and signigicance test statistic.'''
stats: GenericStats

Expand All @@ -48,7 +48,7 @@ def __eq__(self, other):
return isinstance(other, self.__class__) and self.index == other.index

@classmethod
def from_candidate(cls, candidate: CandidateChangePoint, stats: GenericStats) -> 'ChangePoint[GenericStats]':
def from_candidate(cls, candidate: CandidateChangePoint, stats: GenericStats) -> 'ChangePointOtava[GenericStats]':
return cls(
index=candidate.index,
qhat=candidate.qhat,
Expand All @@ -67,7 +67,7 @@ class SignificanceTester(Generic[GenericStats]):
def __init__(self, max_pvalue: float):
self.max_pvalue = max_pvalue

def get_intervals(self, change_points: List[ChangePoint[GenericStats]]) -> List[slice]:
def get_intervals(self, change_points: List[ChangePointOtava[GenericStats]]) -> List[slice]:
'''Returns list of slices of the series. Change points must be sorted by index.'''
assert all(
change_points[i].index <= change_points[i + 1].index
Expand All @@ -82,12 +82,12 @@ def get_intervals(self, change_points: List[ChangePoint[GenericStats]]) -> List[
]
return [interval for interval in intervals if interval.start != interval.stop]

def is_significant(self, point: ChangePoint[GenericStats]) -> bool:
'''Compares ChangePoint to level of significance max_pvalue'''
def is_significant(self, point: ChangePointOtava[GenericStats]) -> bool:
'''Compares ChangePointOtava to level of significance max_pvalue'''
return point.stats.pvalue <= self.max_pvalue

def change_point(self, candidate: CandidateChangePoint, series: NDArray, intervals: List[slice]) -> ChangePoint[GenericStats]:
'''Computes stats for a change point candidate and wraps it into ChangePoint class'''
def change_point(self, candidate: CandidateChangePoint, series: NDArray, intervals: List[slice]) -> ChangePointOtava[GenericStats]:
'''Computes stats for a change point candidate and wraps it into ChangePointOtava class'''
...


Expand Down
4 changes: 2 additions & 2 deletions otava/change_point_divisive/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from otava.change_point_divisive.base import (
Calculator,
ChangePoint,
ChangePointOtava,
GenericStats,
SignificanceTester,
)
Expand All @@ -32,7 +32,7 @@ def __init__(self, significance_tester: SignificanceTester, calculator: Type[Cal
self.tester = significance_tester
self.calculator = calculator

def get_change_points(self, series: Sequence[SupportsFloat], start: Optional[int] = None, end: Optional[int] = None) -> List[ChangePoint[GenericStats]]:
def get_change_points(self, series: Sequence[SupportsFloat], start: Optional[int] = None, end: Optional[int] = None) -> List[ChangePointOtava[GenericStats]]:
'''Finds change points in `series[start : end]`.'''
if not isinstance(series, np.ndarray):
series = np.array(series[start : end], dtype=np.float64)
Expand Down
6 changes: 3 additions & 3 deletions otava/change_point_divisive/significance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
BaseStats,
Calculator,
CandidateChangePoint,
ChangePoint,
ChangePointOtava,
SignificanceTester,
)

Expand All @@ -50,7 +50,7 @@ def __init__(self, max_pvalue: float, permutations: int, calculator: Type[Calcul
self.seed = seed
self.rng = np.random.default_rng(seed)

def change_point(self, candidate: CandidateChangePoint, series: NDArray, intervals: List[slice]) -> ChangePoint[PermutationStats]:
def change_point(self, candidate: CandidateChangePoint, series: NDArray, intervals: List[slice]) -> ChangePointOtava[PermutationStats]:
'''Perform permutation test within candidate cluster'''

# 1. Find permutated Qhats
Expand All @@ -74,4 +74,4 @@ def change_point(self, candidate: CandidateChangePoint, series: NDArray, interva
extreme_qhat_perm=extreme_qhat_perm,
n_perm=self.permutations
)
return ChangePoint.from_candidate(candidate, stats)
return ChangePointOtava.from_candidate(candidate, stats)
4 changes: 2 additions & 2 deletions otava/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import pg8000

from otava.analysis import ChangePoint
from otava.analysis import ChangePointOtava
from otava.test_config import PostgresTestConfig


Expand Down Expand Up @@ -88,7 +88,7 @@ def insert_change_point(
test: PostgresTestConfig,
metric_name: str,
attributes: Dict,
change_point: ChangePoint,
change_point: ChangePointOtava,
):
cursor = self.__get_conn().cursor()
kwargs = {**attributes, **{test.time_column: datetime.utcfromtimestamp(change_point.time)}}
Expand Down
28 changes: 14 additions & 14 deletions otava/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
compute_change_points_orig,
fill_missing,
)
from otava.change_point_divisive.base import ChangePoint as _ChangePoint
from otava.change_point_divisive.base import ChangePointOtava


@dataclass
Expand Down Expand Up @@ -72,7 +72,7 @@ def to_json(self):


@dataclass
class ChangePoint(_ChangePoint[TTestStats]):
class ChangePointHunter(ChangePointOtava[TTestStats]):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sowiks Did you have some plan what to do with these almost identical change point classes?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to consolidate these two classes.

"""A change-point for a single metric"""
metric: str
time: int
Expand Down Expand Up @@ -140,7 +140,7 @@ class ChangePointGroup:
prev_time: int
attributes: Dict[str, str]
prev_attributes: Dict[str, str]
changes: List[ChangePoint]
changes: List[ChangePointHunter]

def to_json(self, rounded=False):
return {
Expand Down Expand Up @@ -215,11 +215,11 @@ class AnalyzedSeries:

__series: Series
options: AnalysisOptions
change_points: Dict[str, List[ChangePoint]]
change_points: Dict[str, List[ChangePointHunter]]
change_points_by_time: List[ChangePointGroup]
change_points_timestamp: Any

def __init__(self, series: Series, options: AnalysisOptions, change_points: Dict[str, ChangePoint] = None):
def __init__(self, series: Series, options: AnalysisOptions, change_points: Dict[str, ChangePointHunter] = None):
self.__series = series
self.options = options
self.change_points_timestamp = datetime.now(tz=timezone.utc)
Expand All @@ -235,7 +235,7 @@ def __init__(self, series: Series, options: AnalysisOptions, change_points: Dict
@staticmethod
def __compute_change_points(
series: Series, options: AnalysisOptions
) -> Dict[str, List[ChangePoint]]:
) -> Dict[str, List[ChangePointHunter]]:
result = {}
weak_change_points = {}
for metric in series.data.keys():
Expand All @@ -258,13 +258,13 @@ def __compute_change_points(
)
for c in weak_cps:
weak_change_points[metric].append(
ChangePoint(
ChangePointHunter(
index=c.index, qhat=0.0, time=series.time[c.index], metric=metric, stats=c.stats
)
)
for c in change_points:
result[metric].append(
ChangePoint(
ChangePointHunter(
index=c.index, qhat=0.0, time=series.time[c.index], metric=metric, stats=c.stats
)
)
Expand All @@ -274,9 +274,9 @@ def __compute_change_points(

@staticmethod
def __group_change_points_by_time(
series: Series, change_points: Dict[str, List[ChangePoint]]
series: Series, change_points: Dict[str, List[ChangePointHunter]]
) -> List[ChangePointGroup]:
changes: List[ChangePoint] = []
changes: List[ChangePointHunter] = []
for metric in change_points.keys():
changes += change_points[metric]

Expand Down Expand Up @@ -381,14 +381,14 @@ def append(self, time, new_data, attributes):
result[metric] = []
for c in change_points:
result[metric].append(
ChangePoint(
ChangePointHunter(
index=c.index, qhat=0.0, time=self.__series.time[c.index], metric=metric, stats=c.stats
)
)
weak_change_points[metric] = []
for c in weak_cps:
weak_change_points[metric].append(
ChangePoint(
ChangePointHunter(
index=c.index, qhat=0.0, time=self.__series.time[c.index], metric=metric, stats=c.stats
)
)
Expand Down Expand Up @@ -493,7 +493,7 @@ def from_json(cls, analyzed_json):
pvalue=cp["pvalue"],
)
new_list.append(
ChangePoint(
ChangePointHunter(
index=cp["index"], time=cp["time"], metric=cp["metric"], stats=stat
)
)
Expand All @@ -511,7 +511,7 @@ def from_json(cls, analyzed_json):
pvalue=cp["pvalue"],
)
new_list.append(
ChangePoint(
ChangePointHunter(
index=cp["index"], time=cp["time"], metric=cp["metric"], stats=stat
)
)
Expand Down
14 changes: 7 additions & 7 deletions tests/change_point_divisive_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import pytest

from otava.analysis import TTestSignificanceTester, TTestStats
from otava.change_point_divisive.base import ChangePoint
from otava.change_point_divisive.base import ChangePointOtava
from otava.change_point_divisive.calculator import PairDistanceCalculator
from otava.change_point_divisive.detector import ChangePointDetector
from otava.change_point_divisive.significance_test import PermutationsSignificanceTester
Expand Down Expand Up @@ -124,9 +124,9 @@ def test_get_intervals_requires_sorted_change_points():

# Sorted change points should work
sorted_cps = [
ChangePoint(index=5, qhat=1.0, stats=stats),
ChangePoint(index=10, qhat=1.0, stats=stats),
ChangePoint(index=15, qhat=1.0, stats=stats),
ChangePointOtava(index=5, qhat=1.0, stats=stats),
ChangePointOtava(index=10, qhat=1.0, stats=stats),
ChangePointOtava(index=15, qhat=1.0, stats=stats),
]
intervals = tester.get_intervals(sorted_cps)
assert len(intervals) == 4
Expand All @@ -137,9 +137,9 @@ def test_get_intervals_requires_sorted_change_points():

# Unsorted change points should raise AssertionError
unsorted_cps = [
ChangePoint(index=10, qhat=1.0, stats=stats),
ChangePoint(index=5, qhat=1.0, stats=stats),
ChangePoint(index=15, qhat=1.0, stats=stats),
ChangePointOtava(index=10, qhat=1.0, stats=stats),
ChangePointOtava(index=5, qhat=1.0, stats=stats),
ChangePointOtava(index=15, qhat=1.0, stats=stats),
]
with pytest.raises(AssertionError, match="Change points must be sorted by index"):
tester.get_intervals(unsorted_cps)
Loading
Loading