Skip to content

Commit 0aafa82

Browse files
committed
return as.POSIXct instead of as.POSIXlt and format code
1 parent a2c6dd7 commit 0aafa82

1 file changed

Lines changed: 92 additions & 74 deletions

File tree

R/ts_anom_detection.R

Lines changed: 92 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)