Skip to content
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

Anomaly Detection: Add Daily Season with Hourly Interval to HoltWinter #546

Merged
merged 3 commits into from
Mar 10, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import collection.mutable.ListBuffer
object HoltWinters {

object SeriesSeasonality extends Enumeration {
val Weekly, Yearly: Value = Value
val Daily, Weekly, Yearly: Value = Value
}

object MetricInterval extends Enumeration {
val Daily, Monthly: Value = Value
val Hourly, Daily, Monthly: Value = Value
}

private[seasonal] case class ModelResults(
Expand All @@ -48,29 +48,30 @@ object HoltWinters {

}

/**
* Detects anomalies based on additive Holt-Winters model. The methods has two
* parameters, one for the metric frequency, as in how often the metric of interest
* is computed (e.g. daily) and one for the expected metric seasonality which
* defines the longest cycle in series. This quantity is also referred to as periodicity.
*
* For example, if a metric is produced daily and repeats itself every Monday, then the
* model should be created with a Daily metric interval and a Weekly seasonality parameter.
*
* @param metricsInterval: How often a metric is available
* @param seasonality: Cycle length (or periodicity) of the metric
*/
class HoltWinters(
metricsInterval: HoltWinters.MetricInterval.Value,
seasonality: HoltWinters.SeriesSeasonality.Value)
class HoltWinters(seriesPeriodicity: Int)
extends AnomalyDetectionStrategy {

import HoltWinters._

private val seriesPeriodicity = seasonality -> metricsInterval match {
case (SeriesSeasonality.Weekly, MetricInterval.Daily) => 7
case (SeriesSeasonality.Yearly, MetricInterval.Monthly) => 12
}
/**
* Detects anomalies based on additive Holt-Winters model. The methods has two
* parameters, one for the metric frequency, as in how often the metric of interest
* is computed (e.g. daily) and one for the expected metric seasonality which
* defines the longest cycle in series. This quantity is also referred to as periodicity.
*
* For example, if a metric is produced daily and repeats itself every Monday, then the
* model should be created with a Daily metric interval and a Weekly seasonality parameter.
*
* @param metricsInterval : How often a metric is available
* @param seasonality : Cycle length (or periodicity) of the metric
*/
def this(metricsInterval: HoltWinters.MetricInterval.Value,
seasonality: HoltWinters.SeriesSeasonality.Value) =
this(seasonality -> metricsInterval match {
case (HoltWinters.SeriesSeasonality.Daily, HoltWinters.MetricInterval.Hourly) => 24
case (HoltWinters.SeriesSeasonality.Weekly, HoltWinters.MetricInterval.Daily) => 7
case (HoltWinters.SeriesSeasonality.Yearly, HoltWinters.MetricInterval.Monthly) => 12
})

/**
* Triple exponential smoothing with additive trend and seasonality
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,71 @@ class HoltWintersTest extends AnyWordSpec with Matchers {
anomalies should have size 3
}
}

"work on hourly data with daily seasonality" in {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a test for custom seriesPeriodicity as well? The two tests are testing the original constructor and not the new constructor.

// https://www.kaggle.com/datasets/fedesoriano/traffic-prediction-dataset
val hourlyTrafficData = Vector[Double](
15, 13, 10, 7, 9, 6, 9, 8, 11, 12, 15, 17, 16, 15, 16, 12, 12, 16, 17, 20, 17, 19, 20, 15,
14, 12, 14, 12, 12, 11, 13, 14, 12, 22, 32, 31, 35, 26, 34, 30, 27, 27, 24, 26, 29, 32, 30, 27,
21, 18, 19, 13, 11, 11, 11, 14, 15, 29, 33, 32, 32, 29, 27, 26, 28, 26, 25, 29, 26, 24, 25, 20,
18, 18, 13, 13, 10, 12, 13, 11, 13, 22, 26, 27, 31, 24, 23, 26, 26, 24, 23, 25, 26, 24, 26, 24,
19, 20, 18, 13, 13, 9, 12, 12, 15, 16, 23, 24, 25, 24, 26, 22, 20, 20, 22, 26, 22, 21, 21, 21,
16, 18, 19, 14, 12, 13, 14, 14, 13, 20, 22, 26, 26, 21, 23, 23, 19, 19, 20, 24, 18, 19, 16, 17,
16, 16, 10, 9, 8, 7, 9, 8, 12, 13, 17, 14, 14, 14, 14, 11, 15, 13, 12, 17, 18, 17, 16, 15, 13
)

val strategy = new HoltWinters(
HoltWinters.MetricInterval.Hourly,
HoltWinters.SeriesSeasonality.Daily)

val nDaysTrain = 6
val nDaysTest = 1
val trainSize = nDaysTrain * 24
val testSize = nDaysTest * 24
val nTotal = trainSize + testSize

val anomalies = strategy.detect(
hourlyTrafficData.take(nTotal),
trainSize -> nTotal
)

anomalies should have size 2
}

"work on monthly data with yearly seasonality using custom seriesPeriodicity" in {
// https://datamarket.com/data/set/22ox/monthly-milk-production-pounds-per-cow-jan-62-dec-75
val monthlyMilkProduction = Vector[Double](
589, 561, 640, 656, 727, 697, 640, 599, 568, 577, 553, 582,
600, 566, 653, 673, 742, 716, 660, 617, 583, 587, 565, 598,
628, 618, 688, 705, 770, 736, 678, 639, 604, 611, 594, 634,
658, 622, 709, 722, 782, 756, 702, 653, 615, 621, 602, 635,
677, 635, 736, 755, 811, 798, 735, 697, 661, 667, 645, 688,
713, 667, 762, 784, 837, 817, 767, 722, 681, 687, 660, 698,
717, 696, 775, 796, 858, 826, 783, 740, 701, 706, 677, 711,
734, 690, 785, 805, 871, 845, 801, 764, 725, 723, 690, 734,
750, 707, 807, 824, 886, 859, 819, 783, 740, 747, 711, 751,
804, 756, 860, 878, 942, 913, 869, 834, 790, 800, 763, 800,
826, 799, 890, 900, 961, 935, 894, 855, 809, 810, 766, 805,
821, 773, 883, 898, 957, 924, 881, 837, 784, 791, 760, 802,
828, 778, 889, 902, 969, 947, 908, 867, 815, 812, 773, 813,
834, 782, 892, 903, 966, 937, 896, 858, 817, 827, 797, 843
)

val strategy = new HoltWinters(12)

val nYearsTrain = 3
val nYearsTest = 1
val trainSize = nYearsTrain * 12
val testSize = nYearsTest * 12
val nTotal = trainSize + testSize

val anomalies = strategy.detect(
monthlyMilkProduction.take(nTotal),
trainSize -> nTotal
)

anomalies should have size 7
}
}

object HoltWintersTest {
Expand Down
Loading