#' Time Series Analysis (Smart Aggregation for Date & Numeric) #' #' @param dataset Dataset name #' @param var_num The numeric variable to analyze (Y) #' @param var_date The time variable (X). Can be Date or Numeric. #' @param agg_level Aggregation level ("day", "week", "month", "quarter", "year") #' @param agg_fun Aggregation function ("mean", "sum", "count") #' @param ma_n Window size for Moving Average #' @param fc_periods Number of periods to forecast #' @param methods Vector of methods: "ma", "ets", "acf" #' @param data_filter Filter expression #' @param envir Environment #' #' @return A list of results #' @export time_series <- function(dataset, var_num, var_date = NULL, agg_level = "month", agg_fun = "mean", ma_n = 3, fc_periods = 0, methods = c("ma"), data_filter = "", envir = parent.frame()) { # === 1. 数据准备 === vars <- c(var_num) if (!is.null(var_date) && var_date != "") vars <- c(vars, var_date) df_raw <- get_data(dataset, vars, filt = data_filter, na.rm = TRUE, envir = envir) # === 2. 智能聚合逻辑 === # 检查 X 轴数据类型 is_date_col <- !is.null(var_date) && var_date != "" && inherits(df_raw[[var_date]], c("Date", "POSIXt")) is_num_col <- !is.null(var_date) && var_date != "" && is.numeric(df_raw[[var_date]]) df <- df_raw # 默认回退 if (is_date_col || is_num_col) { requireNamespace("dplyr") if (is_date_col) { # A. 如果是日期:使用 lubridate 对齐 requireNamespace("lubridate") df_raw$ts_group_col <- lubridate::floor_date(df_raw[[var_date]], unit = agg_level) step_num <- 1 # 仅用于后续判断 } else if (is_num_col) { # B. 如果是数字:执行分箱 (Binning) # 将 "Month" 映射为 "30个单位", "Week" -> "7个单位" step_num <- switch(agg_level, "week" = 7, "month" = 30, "quarter" = 90, "year" = 365, 1) # default day=1 # 核心算法:向下取整分箱 df_raw$ts_group_col <- floor(df_raw[[var_date]] / step_num) * step_num } # 执行聚合 df <- df_raw %>% dplyr::group_by(ts_group_col) %>% dplyr::summarise( y_agg = if (agg_fun == "count") dplyr::n() else if (agg_fun == "sum") sum(.data[[var_num]], na.rm = TRUE) else mean(.data[[var_num]], na.rm = TRUE) ) %>% dplyr::arrange(ts_group_col) %>% dplyr::rename(!!var_date := ts_group_col, !!var_num := y_agg) %>% as.data.frame() } else { # 如果没选变量,生成 Index if (is.null(var_date) || var_date == "") { var_date <- "Index" df$Index <- 1:nrow(df) step_num <- 1 } } y_val <- df[[var_num]] res_list <- list() metrics <- data.frame(Method = character(), RMSE = numeric(), MAE = numeric(), stringsAsFactors = FALSE) get_accuracy <- function(actual, fitted, name) { err <- actual - fitted c(name, round(sqrt(mean(err^2, na.rm=TRUE)), 3), round(mean(abs(err), na.rm=TRUE), 3)) } # === 3. 计算模块 === # --- Moving Average --- if ("ma" %in% methods) { # 确保窗口不大于数据长度 safe_ma_n <- min(ma_n, length(y_val)) ma_weights <- rep(1/safe_ma_n, safe_ma_n) df$ma_line <- as.numeric(stats::filter(y_val, ma_weights, sides = 1)) valid <- !is.na(df$ma_line) if (sum(valid) > 0) { metrics[nrow(metrics)+1,] <- get_accuracy(y_val[valid], df$ma_line[valid], "Moving Average") } } # --- ETS (Holt-Winters) --- if ("ets" %in% methods) { # 尝试根据聚合粒度设定频率 freq <- 1 if (is_date_col) { freq <- switch(agg_level, "month"=12, "quarter"=4, "week"=52, 7) } else if (is_num_col) { # 对于数值型,尝试推断周期,默认无季节性 freq <- 1 } if (length(y_val) < 2 * freq) freq <- 1 ts_obj <- ts(y_val, frequency = freq) ets_model <- tryCatch(HoltWinters(ts_obj), error = function(e) tryCatch(HoltWinters(ts_obj, gamma=FALSE), error = function(e) NULL)) if (!is.null(ets_model)) { fitted_raw <- as.numeric(ets_model$fitted[, 1]) pad <- rep(NA, length(y_val) - length(fitted_raw)) df$ets_line <- c(pad, fitted_raw) # 记录参数 res_list$ets_params <- list(alpha = ets_model$alpha, beta = ets_model$beta, gamma = ets_model$gamma) # 记录误差 metrics[nrow(metrics)+1,] <- get_accuracy(y_val[!is.na(df$ets_line)], df$ets_line[!is.na(df$ets_line)], "Exp. Smoothing") # === 预测 (Forecast) === if (fc_periods > 0) { fc_out <- predict(ets_model, n.ahead = fc_periods, prediction.interval = TRUE, level = 0.95) last_time <- tail(df[[var_date]], 1) # 生成未来时间轴 (区分日期和数字) if (inherits(last_time, "Date") || inherits(last_time, "POSIXt")) { future_dates <- seq(from = last_time, by = agg_level, length.out = fc_periods + 1)[-1] } else { # 对于数字,按 step_num 递增 # 如果之前判定是数字列,使用之前的 step_num step <- if(exists("step_num")) step_num else 1 future_dates <- seq(from = last_time + step, by = step, length.out = fc_periods) } res_list$forecast <- data.frame( date = future_dates, fit = as.numeric(fc_out[, "fit"]), upr = as.numeric(fc_out[, "upr"]), lwr = as.numeric(fc_out[, "lwr"]) ) } } } # --- ACF --- if ("acf" %in% methods) { # 增加 tryCatch 防止数据太少报错 acf_out <- tryCatch(acf(y_val, plot = FALSE, na.action = na.pass), error = function(e) NULL) if (!is.null(acf_out)) { res_list$acf_df <- data.frame(lag = acf_out$lag, acf = acf_out$acf) } } # === 4. 返回 === res_list$dataset <- df res_list$metrics <- metrics res_list$var_num <- var_num res_list$var_date <- var_date res_list$agg_level <- agg_level res_list$agg_fun <- agg_fun res_list$methods <- methods res_list$df_name <- if (is_string(dataset)) dataset else deparse(substitute(dataset)) as.list(res_list) %>% add_class("time_series") } #' Summary method #' @export summary.time_series <- function(object, ...) { cat("Time Series Analysis\n") cat("Data :", object$df_name, "\n") cat("Variable :", object$var_num, "\n") cat("Time Var :", object$var_date, "\n") cat("Aggregation:", object$agg_level, "(", object$agg_fun, ")\n\n") if (nrow(object$metrics) > 0) { cat("Model Accuracy (Goodness of Fit):\n") print(object$metrics, row.names = FALSE) cat("---\n") } if ("ets" %in% object$methods && !is.null(object$ets_params)) { p <- object$ets_params cat("Exp. Smoothing Parameters:\n") cat(sprintf("Alpha: %.3f, Beta: %.3f, Gamma: %.3f\n", p$alpha, ifelse(is.null(p$beta), 0, p$beta), ifelse(is.null(p$gamma), 0, p$gamma))) } if (!is.null(object$forecast)) { cat("\nForecast (Next", nrow(object$forecast), "periods):\n") print(head(object$forecast, 5)) } } #' Plot method #' @export plot.time_series <- function(x, plots = c("original", "ma", "ets"), shiny = FALSE, custom = FALSE, ...) { plot_list <- list() if (any(c("original", "ma", "ets") %in% plots)) { p <- ggplot(x$dataset, aes(x = .data[[x$var_date]], y = .data[[x$var_num]])) + labs(title = paste("Time Series:", x$var_num), subtitle = paste("Aggregated by", x$agg_level)) + theme_minimal() if ("original" %in% plots) { p <- p + geom_line(color = "black", alpha = 0.3) + geom_point(color = "black", alpha = 0.3, size = 1.5) } if ("ma" %in% plots && "ma_line" %in% names(x$dataset)) { p <- p + geom_line(aes(y = ma_line, color = "Moving Avg"), linewidth = 0.8) } if ("ets" %in% plots && "ets_line" %in% names(x$dataset)) { p <- p + geom_line(aes(y = ets_line, color = "Exp Smoothing"), linewidth = 0.8) } if ("ets" %in% plots && !is.null(x$forecast)) { p <- p + geom_ribbon(data = x$forecast, aes(x = date, ymin = lwr, ymax = upr, y = fit), fill = "red", alpha = 0.15, inherit.aes = FALSE) + geom_line(data = x$forecast, aes(x = date, y = fit, color = "Forecast"), linetype = "dashed", linewidth = 0.8, inherit.aes = FALSE) } p <- p + scale_color_manual(name = "Legend", values = c("Moving Avg" = "blue", "Exp Smoothing" = "red", "Forecast" = "red")) plot_list[[1]] <- p } if ("acf" %in% plots && "acf" %in% x$methods && !is.null(x$acf_df)) { ci <- 0.95 clim <- qnorm((1 + ci)/2) / sqrt(nrow(x$dataset)) p_acf <- ggplot(x$acf_df, aes(x = lag, y = acf)) + geom_segment(aes(xend = lag, yend = 0), color = "darkblue", linewidth = 1) + geom_hline(yintercept = c(clim, -clim), linetype = "dashed", color = "blue") + geom_hline(yintercept = 0) + labs(title = "Autocorrelation (ACF)", y = "ACF", x = "Lag") + theme_minimal() plot_list[[length(plot_list) + 1]] <- p_acf } if (length(plot_list) > 0) { patchwork::wrap_plots(plot_list, ncol = 1) %>% (function(obj) if (shiny) obj else print(obj)) } }