@@ -152,18 +152,19 @@ AnomalyDetectionTs <- function(x, max_anoms = 0.10, direction = 'pos',
152152 # Although we derive this in S-H-ESD, we also need it to be minutley later on so we do it here first.
153153 gran <- get_gran(x , 1 )
154154
155- if (gran == " day" ){
155+ if (gran == " day" ) {
156156 num_days_per_line <- 7
157- if (is.character(only_last ) && only_last == ' hr' ){
157+ if (is.character(only_last ) && only_last == ' hr' ) {
158158 only_last <- ' day'
159159 }
160160 } else {
161161 num_days_per_line <- 1
162162 }
163163
164164 # Aggregate data to minutely if secondly
165- if (gran == " sec" ){
166- x <- format_timestamp(aggregate(x [2 ], format(x [1 ], " %Y-%m-%d %H:%M:00" ), eval(parse(text = " sum" ))))
165+ if (gran == " sec" ) {
166+ x <- format_timestamp(aggregate(x [2 ], format(x [1 ], " %Y-%m-%d %H:%M:00" ),
167+ eval(parse(text = " sum" ))))
167168 }
168169
169170 period = switch (gran ,
@@ -173,17 +174,17 @@ AnomalyDetectionTs <- function(x, max_anoms = 0.10, direction = 'pos',
173174 day = 7 )
174175 num_obs <- length(x [[2 ]])
175176
176- if (max_anoms < 1 / num_obs ){
177+ if (max_anoms < 1 / num_obs ) {
177178 max_anoms <- 1 / num_obs
178179 }
179180
180181 # -- Setup for longterm time series
181182
182183 # If longterm is enabled, break the data into subset data frames and store in all_data
183- if (longterm ){
184+ if (longterm ) {
184185 # Pre-allocate list with size equal to the number of piecewise_median_period_weeks chunks in x + any left over chunk
185186 # handle edge cases for daily and single column data period lengths
186- if (gran == " day" ){
187+ if (gran == " day" ) {
187188 # STL needs 2*period + 1 observations
188189 num_obs_in_period <- period * piecewise_median_period_weeks + 1
189190 num_days_in_period <- (7 * piecewise_median_period_weeks ) + 1
@@ -195,62 +196,62 @@ AnomalyDetectionTs <- function(x, max_anoms = 0.10, direction = 'pos',
195196 # Store last date in time series
196197 last_date <- x [[1 ]][num_obs ]
197198
198- all_data <- vector(mode = " list" , length = ceiling(length(x [[1 ]])/ (num_obs_in_period )))
199+ all_data <- vector(mode = " list" , length = ceiling(length(x [[1 ]])/ (num_obs_in_period )))
199200 # Subset x into piecewise_median_period_weeks chunks
200- for (j in seq(1 ,length(x [[1 ]]), by = num_obs_in_period )){
201+ for (j in seq(1 , length(x [[1 ]]), by = num_obs_in_period )) {
201202 start_date <- x [[1 ]][j ]
202203 end_date <- min(start_date + lubridate :: days(num_days_in_period ), x [[1 ]][length(x [[1 ]])])
203204 # if there is at least 14 days left, subset it, otherwise subset last_date - 14days
204- if (difftime(end_date , start_date , units = " days" ) == as.difftime(num_days_in_period , units = " days" )){
205+ if (difftime(end_date , start_date , units = " days" ) == as.difftime(num_days_in_period , units = " days" )) {
205206 all_data [[ceiling(j / (num_obs_in_period ))]] <- subset(x , x [[1 ]] > = start_date & x [[1 ]] < end_date )
206- }else {
207- all_data [[ceiling(j / (num_obs_in_period ))]] <- subset(x , x [[1 ]] > (last_date - lubridate :: days(num_days_in_period )) & x [[1 ]] < = last_date )
207+ } else {
208+ all_data [[ceiling(j / (num_obs_in_period ))]] <- subset(x , x [[1 ]] > (last_date - lubridate :: days(num_days_in_period )) & x [[1 ]] < = last_date )
208209 }
209210 }
210- }else {
211+ } else {
211212 # If longterm is not enabled, then just overwrite all_data list with x as the only item
212213 all_data <- list (x )
213214 }
214215
215216 # Create empty data frames to store all anoms and seasonal+trend component from decomposition
216- all_anoms <- data.frame (timestamp = numeric (0 ), count = numeric (0 ))
217- seasonal_plus_trend <- data.frame (timestamp = numeric (0 ), count = numeric (0 ))
217+ all_anoms <- data.frame (timestamp = numeric (0 ), count = numeric (0 ))
218+ seasonal_plus_trend <- data.frame (timestamp = numeric (0 ), count = numeric (0 ))
218219
219220 # Detect anomalies on all data (either entire data in one-pass, or in 2 week blocks if longterm=TRUE)
220- for (i in 1 : length(all_data )) {
221+ for (i in 1 : length(all_data )) {
221222
222223 anomaly_direction = switch (direction ,
223- " pos" = data.frame (one_tail = TRUE , upper_tail = TRUE ), # upper-tail only (positive going anomalies)
224- " neg" = data.frame (one_tail = TRUE , upper_tail = FALSE ), # lower-tail only (negative going anomalies)
225- " both" = data.frame (one_tail = FALSE , upper_tail = TRUE )) # Both tails. Tail direction is not actually used.
224+ " pos" = data.frame (one_tail = TRUE , upper_tail = TRUE ), # upper-tail only (positive going anomalies)
225+ " neg" = data.frame (one_tail = TRUE , upper_tail = FALSE ), # lower-tail only (negative going anomalies)
226+ " both" = data.frame (one_tail = FALSE , upper_tail = TRUE )) # Both tails. Tail direction is not actually used.
226227
227228 # detect_anoms actually performs the anomaly detection and returns the results in a list containing the anomalies
228229 # as well as the decomposed components of the time series for further analysis.
229- s_h_esd_timestamps <- detect_anoms(all_data [[i ]], k = max_anoms , alpha = alpha , num_obs_per_period = period , use_decomp = TRUE , use_esd = FALSE ,
230- one_tail = anomaly_direction $ one_tail , upper_tail = anomaly_direction $ upper_tail , verbose = verbose )
230+ s_h_esd_timestamps <- detect_anoms(all_data [[i ]], k = max_anoms , alpha = alpha , num_obs_per_period = period , use_decomp = TRUE , use_esd = FALSE ,
231+ one_tail = anomaly_direction $ one_tail , upper_tail = anomaly_direction $ upper_tail , verbose = verbose )
231232
232233 # store decomposed components in local variable and overwrite s_h_esd_timestamps to contain only the anom timestamps
233234 data_decomp <- s_h_esd_timestamps $ stl
234235 s_h_esd_timestamps <- s_h_esd_timestamps $ anoms
235236
236237 # -- Step 3: Use detected anomaly timestamps to extract the actual anomalies (timestamp and value) from the data
237- if (! is.null(s_h_esd_timestamps )){
238+ if (! is.null(s_h_esd_timestamps )) {
238239 anoms <- subset(all_data [[i ]], (all_data [[i ]][[1 ]] %in% s_h_esd_timestamps ))
239240 } else {
240- anoms <- data.frame (timestamp = numeric (0 ), count = numeric (0 ))
241+ anoms <- data.frame (timestamp = numeric (0 ), count = numeric (0 ))
241242 }
242243
243244 # Filter the anomalies using one of the thresholding functions if applicable
244- if (threshold != " None" ){
245+ if (threshold != " None" ) {
245246 # Calculate daily max values
246- periodic_maxs <- tapply(x [[2 ]],as.Date(x [[1 ]]),FUN = max )
247+ periodic_maxs <- tapply(x [[2 ]], as.Date(x [[1 ]]), FUN = max )
247248
248249 # Calculate the threshold set by the user
249- if (threshold == ' med_max' ){
250+ if (threshold == ' med_max' ) {
250251 thresh <- median(periodic_maxs )
251- }else if (threshold == ' p95' ){
252+ } else if (threshold == ' p95' ) {
252253 thresh <- quantile(periodic_maxs , .95 )
253- }else if (threshold == ' p99' ){
254+ } else if (threshold == ' p99' ) {
254255 thresh <- quantile(periodic_maxs , .99 )
255256 }
256257 # Remove any anoms below the threshold
@@ -265,20 +266,20 @@ AnomalyDetectionTs <- function(x, max_anoms = 0.10, direction = 'pos',
265266 seasonal_plus_trend <- seasonal_plus_trend [! duplicated(seasonal_plus_trend [[1 ]]), ]
266267
267268 # -- If only_last was set by the user, create subset of the data that represent the most recent day
268- if (! is.null(only_last )){
269- start_date <- x [[1 ]][num_obs ]- lubridate :: days(7 )
270- start_anoms <- x [[1 ]][num_obs ]- lubridate :: days(1 )
271- if (gran == " day" ){
269+ if (! is.null(only_last )) {
270+ start_date <- x [[1 ]][num_obs ] - lubridate :: days(7 )
271+ start_anoms <- x [[1 ]][num_obs ] - lubridate :: days(1 )
272+ if (gran == " day" ) {
272273 # TODO: This might be better set up top at the gran check
273274 breaks <- 3 * 12
274275 num_days_per_line <- 7
275276 } else {
276- if (only_last == ' day' ){
277+ if (only_last == ' day' ) {
277278 breaks <- 12
278- }else {
279+ } else {
279280 # We need to change start_date and start_anoms for the hourly only_last option
280- start_date <- lubridate :: floor_date(x [[1 ]][num_obs ]- lubridate :: days(2 ), " day" )
281- start_anoms <- x [[1 ]][num_obs ]- lubridate :: hours(1 )
281+ start_date <- lubridate :: floor_date(x [[1 ]][num_obs ] - lubridate :: days(2 ), " day" )
282+ start_anoms <- x [[1 ]][num_obs ] - lubridate :: hours(1 )
282283 breaks <- 3
283284 }
284285 }
@@ -295,70 +296,87 @@ AnomalyDetectionTs <- function(x, max_anoms = 0.10, direction = 'pos',
295296 anom_pct <- (length(all_anoms [[2 ]]) / num_obs ) * 100
296297
297298 # If there are no anoms, then let's exit
298- if (anom_pct == 0 ){
299- if (verbose ) message(" No anomalies detected." )
300- return (list (" anoms" = data.frame (), " plot" = plot.new()))
299+ if (anom_pct == 0 ) {
300+ if (verbose ) message(" No anomalies detected." )
301+ return (list (" anoms" = data.frame (), " plot" = plot.new()))
301302 }
302303
303- if (plot ){
304+ if (plot ) {
304305 # -- Build title for plots utilizing parameters set by user
305- plot_title <- paste(title , round(anom_pct , digits = 2 ), " % Anomalies (alpha=" , alpha , " , direction=" , direction ," )" , sep = " " )
306- if (longterm ){
307- plot_title <- paste(plot_title , " , longterm=T" , sep = " " )
306+ plot_title <- paste(title , round(anom_pct , digits = 2 ), " % Anomalies (alpha=" , alpha , " , direction=" , direction ," )" , sep = " " )
307+ if (longterm ) {
308+ plot_title <- paste(plot_title , " , longterm=T" , sep = " " )
308309 }
309310
310311 # -- Plot raw time series data
311- color_name <- paste(" \" " , title , " \" " , sep = " " )
312+ color_name <- paste(" \" " , title , " \" " , sep = " " )
312313 alpha <- 0.8
313- if (! is.null(only_last )){
314- xgraph <- ggplot2 :: ggplot(x_subset_week , ggplot2 :: aes_string(x = " timestamp" , y = " count" )) + ggplot2 :: theme_bw() + ggplot2 :: theme(panel.grid.major = ggplot2 :: element_blank(), panel.grid.minor = ggplot2 :: element_blank(), text = ggplot2 :: element_text(size = 14 ))
315- xgraph <- xgraph + ggplot2 :: geom_line(data = x_subset_week , ggplot2 :: aes_string(colour = color_name ), alpha = alpha * .33 ) + ggplot2 :: geom_line(data = x_subset_single_day , ggplot2 :: aes_string(color = color_name ), alpha = alpha )
316- week_rng = get_range(x_subset_week , index = 2 , y_log = y_log )
317- day_rng = get_range(x_subset_single_day , index = 2 , y_log = y_log )
318- yrange = c(min(week_rng [1 ],day_rng [1 ]), max(week_rng [2 ],day_rng [2 ]))
319- xgraph <- add_day_labels_datetime(xgraph , breaks = breaks , start = as.POSIXlt(min(x_subset_week [[1 ]]), tz = " UTC" ), end = as.POSIXlt(max(x_subset_single_day [[1 ]]), tz = " UTC" ), days_per_line = num_days_per_line )
320- xgraph <- xgraph + ggplot2 :: labs(x = xlabel , y = ylabel , title = plot_title )
321- }else {
322- xgraph <- ggplot2 :: ggplot(x , ggplot2 :: aes_string(x = " timestamp" , y = " count" )) + ggplot2 :: theme_bw() + ggplot2 :: theme(panel.grid.major = ggplot2 :: element_line(colour = " gray60" ), panel.grid.major.y = ggplot2 :: element_blank(), panel.grid.minor = ggplot2 :: element_blank(), text = ggplot2 :: element_text(size = 14 ))
323- xgraph <- xgraph + ggplot2 :: geom_line(data = x , ggplot2 :: aes_string(colour = color_name ), alpha = alpha )
324- yrange <- get_range(x , index = 2 , y_log = y_log )
325- xgraph <- xgraph + ggplot2 :: scale_x_datetime(labels = function (x ) ifelse(as.POSIXlt(x , tz = " UTC" )$ hour != 0 ,strftime(x , format = " %kh" , tz = " UTC" ), strftime(x , format = " %b %e" , tz = " UTC" )),
326- expand = c(0 ,0 ))
327- xgraph <- xgraph + ggplot2 :: labs(x = xlabel , y = ylabel , title = plot_title )
314+ if (! is.null(only_last )) {
315+ xgraph <- ggplot2 :: ggplot(x_subset_week , ggplot2 :: aes_string(x = " timestamp" , y = " count" )) +
316+ ggplot2 :: theme_bw() +
317+ ggplot2 :: theme(panel.grid.major = ggplot2 :: element_blank(),
318+ panel.grid.minor = ggplot2 :: element_blank(),
319+ text = ggplot2 :: element_text(size = 14 ))
320+ xgraph <- xgraph +
321+ ggplot2 :: geom_line(data = x_subset_week , ggplot2 :: aes_string(colour = color_name ), alpha = alpha * .33 ) +
322+ ggplot2 :: geom_line(data = x_subset_single_day , ggplot2 :: aes_string(color = color_name ), alpha = alpha )
323+ week_rng <- get_range(x_subset_week , index = 2 , y_log = y_log )
324+ day_rng <- get_range(x_subset_single_day , index = 2 , y_log = y_log )
325+ yrange <- c(min(week_rng [1 ], day_rng [1 ]), max(week_rng [2 ], day_rng [2 ]))
326+ xgraph <- add_day_labels_datetime(xgraph , breaks = breaks , start = as.POSIXlt(min(x_subset_week [[1 ]]), tz = " UTC" ), end = as.POSIXlt(max(x_subset_single_day [[1 ]]), tz = " UTC" ), days_per_line = num_days_per_line )
327+ xgraph <- xgraph +
328+ ggplot2 :: labs(x = xlabel , y = ylabel , title = plot_title )
329+ } else {
330+ xgraph <- ggplot2 :: ggplot(x , ggplot2 :: aes_string(x = " timestamp" , y = " count" )) +
331+ ggplot2 :: theme_bw() +
332+ ggplot2 :: theme(panel.grid.major = ggplot2 :: element_line(colour = " gray60" ),
333+ panel.grid.major.y = ggplot2 :: element_blank(),
334+ panel.grid.minor = ggplot2 :: element_blank(),
335+ text = ggplot2 :: element_text(size = 14 ))
336+ xgraph <- xgraph +
337+ ggplot2 :: geom_line(data = x , ggplot2 :: aes_string(colour = color_name ), alpha = alpha )
338+ yrange <- get_range(x , index = 2 , y_log = y_log )
339+ xgraph <- xgraph +
340+ ggplot2 :: scale_x_datetime(labels = function (x ) ifelse(as.POSIXlt(x , tz = " UTC" )$ hour != 0 , strftime(x , format = " %kh" , tz = " UTC" ), strftime(x , format = " %b %e" , tz = " UTC" )),
341+ expand = c(0 ,0 ))
342+ xgraph <- xgraph +
343+ ggplot2 :: labs(x = xlabel , y = ylabel , title = plot_title )
328344 }
329345
330346 # Add anoms to the plot as circles.
331347 # We add zzz_ to the start of the name to ensure that the anoms are listed after the data sets.
332- xgraph <- xgraph + ggplot2 :: geom_point(data = all_anoms , ggplot2 :: aes_string(color = paste(" \" zzz_" ,title ," \" " ,sep = " " )), size = 3 , shape = 1 )
348+ xgraph <- xgraph +
349+ ggplot2 :: geom_point(data = all_anoms , ggplot2 :: aes_string(color = paste(" \" zzz_" , title , " \" " , sep = " " )), size = 3 , shape = 1 )
333350
334351 # Hide legend
335- xgraph <- xgraph + ggplot2 :: theme(legend.position = " none" )
352+ xgraph <- xgraph +
353+ ggplot2 :: theme(legend.position = " none" )
336354
337355 # Use log scaling if set by user
338- xgraph <- xgraph + add_formatted_y( yrange , y_log = y_log )
339-
356+ xgraph <- xgraph +
357+ add_formatted_y( yrange , y_log = y_log )
340358 }
341359
342360 # Fix to make sure date-time is correct and that we retain hms at midnight
343- all_anoms [[1 ]] <- format(all_anoms [[1 ]], format = " %Y-%m-%d %H:%M:%S" )
361+ all_anoms [[1 ]] <- format(all_anoms [[1 ]], format = " %Y-%m-%d %H:%M:%S" )
344362
345363 # Store expected values if set by user
346- if (e_value ) {
347- anoms <- data.frame (timestamp = all_anoms [[1 ]], anoms = all_anoms [[2 ]],
348- expected_value = subset(seasonal_plus_trend [[2 ]], as.POSIXlt(seasonal_plus_trend [[1 ]], tz = " UTC" ) %in% all_anoms [[1 ]]),
349- stringsAsFactors = FALSE )
364+ if (e_value ) {
365+ anoms <- data.frame (timestamp = all_anoms [[1 ]], anoms = all_anoms [[2 ]],
366+ expected_value = subset(seasonal_plus_trend [[2 ]], as.POSIXlt(seasonal_plus_trend [[1 ]], tz = " UTC" ) %in% all_anoms [[1 ]]),
367+ stringsAsFactors = FALSE )
350368 } else {
351- anoms <- data.frame (timestamp = all_anoms [[1 ]], anoms = all_anoms [[2 ]], stringsAsFactors = FALSE )
369+ anoms <- data.frame (timestamp = all_anoms [[1 ]], anoms = all_anoms [[2 ]], stringsAsFactors = FALSE )
352370 }
353371
354- # Make sure we're still a valid POSIXlt datetime.
372+ # Make sure we're still a valid POSIXct datetime.
355373 # TODO: Make sure we keep original datetime format and timezone.
356- anoms $ timestamp <- as.POSIXlt (anoms $ timestamp , tz = " UTC" )
374+ anoms $ timestamp <- as.POSIXct (anoms $ timestamp , tz = " UTC" )
357375
358376 # Lastly, return anoms and optionally the plot if requested by the user
359- if (plot ){
360- return (list (anoms = anoms , plot = xgraph ))
377+ if (plot ) {
378+ return (list (anoms = anoms , plot = xgraph ))
361379 } else {
362- return (list (anoms = anoms , plot = plot.new()))
380+ return (list (anoms = anoms , plot = plot.new()))
363381 }
364382}
0 commit comments