pkg/degradation-detector/statistic/changeDetector.go (107 lines of code) (raw):

package statistic import ( "math" "slices" ) func GetChangePointIndexes(data []int, minDistance int) []int { n := len(data) if n <= 2 { return []int{} } if minDistance < 1 || minDistance > n { panic("minDistance should be in range from 1 to len(data)") } penalty := 3 * math.Log(float64(n)) k := int(math.Min(float64(n), math.Ceil(4*math.Log(float64(n))))) partialSums := getPartialSums(data, k) cost := func(tau1, tau2 int) float64 { return getSegmentCost(partialSums, tau1, tau2, k, n) } bestCost := make([]float64, n+1) bestCost[0] = -penalty for currentTau := minDistance; currentTau < 2*minDistance; currentTau++ { bestCost[currentTau] = cost(0, currentTau) } previousChangePointIndex := make([]int, n+1) previousTaus := []int{0, minDistance} var costForPreviousTau []float64 for currentTau := 2 * minDistance; currentTau < n+1; currentTau++ { costForPreviousTau = costForPreviousTau[:0] for _, previousTau := range previousTaus { costForPreviousTau = append(costForPreviousTau, bestCost[previousTau]+cost(previousTau, currentTau)+penalty) } bestPreviousTauIndex := whichMin(costForPreviousTau) bestCost[currentTau] = costForPreviousTau[bestPreviousTauIndex] previousChangePointIndex[currentTau] = previousTaus[bestPreviousTauIndex] newPreviousTaus := make([]int, 0, len(previousTaus)) for i, cost2 := range costForPreviousTau { if cost2 < bestCost[currentTau]+penalty { newPreviousTaus = append(newPreviousTaus, previousTaus[i]) } } previousTaus = newPreviousTaus previousTaus = append(previousTaus, currentTau-(minDistance-1)) } var changePointIndexes []int currentIndex := previousChangePointIndex[n] for currentIndex != 0 { changePointIndexes = append(changePointIndexes, currentIndex) currentIndex = previousChangePointIndex[currentIndex] } slices.Reverse(changePointIndexes) return changePointIndexes } func getPartialSums(data []int, k int) [][]int { n := len(data) partialSums := make([][]int, k) for i := range partialSums { partialSums[i] = make([]int, n+1) } sortedData := make([]int, n) copy(sortedData, data) slices.Sort(sortedData) for i := range k { z := -1 + (2*float64(i)+1.0)/float64(k) p := 1.0 / (1 + math.Pow(2*float64(n)-1, -z)) t := sortedData[int(math.Trunc(float64(n-1)*p))] for tau := 1; tau <= n; tau++ { partialSums[i][tau] = partialSums[i][tau-1] if data[tau-1] < t { partialSums[i][tau] += 2 // Using doubled value (2) instead of original 1.0 } if data[tau-1] == t { partialSums[i][tau]++ } } } return partialSums } func getSegmentCost(partialSums [][]int, tau1, tau2, k, n int) float64 { var sum float64 tauDiff := float64(tau2 - tau1) tauDiffDoubled := (tau2 - tau1) * 2 for i := range k { // actualSum is (count(data[j] < t) * 2 + count(data[j] == t) * 1) for j=tau1..tau2-1 actualSum := partialSums[i][tau2] - partialSums[i][tau1] if actualSum != 0 && actualSum != tauDiffDoubled { fit := float64(actualSum) / float64(tauDiffDoubled) lnp := tauDiff * (fit*math.Log(fit) + (1-fit)*math.Log1p(-fit)) sum += lnp } } c := -math.Log(float64(2*n - 1)) // Constant from Lemma 3.1 in [Haynes2017] return 2.0 * c / float64(k) * sum // See Section 3.1 "Discrete approximation" in [Haynes2017] } func whichMin(values []float64) int { if len(values) == 0 { panic("slice should contain elements") } minValue := values[0] minIndex := 0 for i := 1; i < len(values); i++ { if values[i] < minValue { minValue = values[i] minIndex = i } } return minIndex }