From c785217694dc4e73d06d3b1b865ab65d8c840806 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sun, 10 Nov 2024 17:14:35 -0800 Subject: [PATCH 01/71] created bsaed function for frequent itemsets and association rules --- R/assoc_rules.R | 175 ++++++++++++++++++++++++++++++++++++++++++++++ R/freq_itemsets.R | 161 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 R/assoc_rules.R create mode 100644 R/freq_itemsets.R diff --git a/R/assoc_rules.R b/R/assoc_rules.R new file mode 100644 index 0000000..1838991 --- /dev/null +++ b/R/assoc_rules.R @@ -0,0 +1,175 @@ +#' Association Rules Mining +#' +#' @description +#' +#' `assoc_rules()` defines a model that finds association rules based on +#' specified minimum support and confidence. +#' +#' The method of estimation is chosen by setting the model engine. The +#' engine-specific pages for this model are listed below. +#' +#' - \link[=details_assoc_rules_stats]{arules} +#' +#' @param mode A single character string for the type of model. The only +#' possible value for this model is "association". +#' @param engine A single character string specifying the computational engine +#' to use for fitting. The default for this model is `"arules"`. +#' @param method A single character string specifying the algorithm to use for +#' fitting. Possible algorithms are `"apriori"` and `"eclat"`. The default for +#' this model is `"apriori"`. +#' @param min_support Positive double, minimum support for a rule (between 0 and 1). +#' @param min_confidence Positive double, minimum confidence for a rule (between 0 and 1). +#' +#' @details +#' +#' ## What does it mean to predict? +#' +#' WORK IN PROGRESS +#' +#' @return A `assoc_rules` association specification. +#' +#' @examples +#' # Show all engines +#' modelenv::get_from_env("assoc_rules") +#' +#' assoc_rules() +#' @export +assoc_rules <- + function(mode = "partition", + engine = "arules", + method = "apriori", + min_support = NULL, + min_confidence = NULL) { + args <- list( + min_support = enquo(min_support), + min_confidence = enquo(min_confidence) + ) + + new_cluster_spec( + "assoc_rules", + args = args, + eng_args = NULL, + mode = mode, + method = NULL, + engine = engine + ) + } + +#' @export +print.assoc_rules <- function(x, ...) { + cat("Association Rules Mining Specification (", x$mode, ")\n\n", sep = "") + model_printer(x, ...) + + if (!is.null(x$method$fit$args)) { + cat("Model fit template:\n") + print(show_call(x)) + } + + invisible(x) +} + +# ------------------------------------------------------------------------------ + +#' @method update assoc_rules +#' @rdname tidyclust_update +#' @export +update.assoc_rules <- function(object, + parameters = NULL, + min_support = NULL, + min_confidence = NULL, + fresh = FALSE, ...) { + eng_args <- parsnip::update_engine_parameters( + object$eng_args, + fresh = fresh, ... + ) + + if (!is.null(parameters)) { + parameters <- parsnip::check_final_param(parameters) + } + args <- list( + min_support = enquo(min_support), + min_confidence = enquo(min_confidence) + ) + + args <- parsnip::update_main_parameters(args, parameters) + + if (fresh) { + object$args <- args + object$eng_args <- eng_args + } else { + null_args <- map_lgl(args, null_value) + if (any(null_args)) { + args <- args[!null_args] + } + if (length(args) > 0) { + object$args[names(args)] <- args + } + if (length(eng_args) > 0) { + object$eng_args[names(eng_args)] <- eng_args + } + } + + new_cluster_spec( + "assoc_rules", + args = object$args, + eng_args = object$eng_args, + mode = object$mode, + method = NULL, + engine = object$engine + ) +} + +# # ---------------------------------------------------------------------------- + +#' @export +check_args.assoc_rules <- function(object) { + args <- lapply(object$args, rlang::eval_tidy) + + if (all(is.numeric(args$min_support)) && any(args$min_support >= 0) && any(args$min_support <= 1)) { + rlang::abort("The minimum support should be between 0 and 1.") + } + + if (all(is.numeric(args$min_confidence)) && any(args$min_confidence >= 0) && any(args$min_confidence <= 1)) { + rlang::abort("The minimum confidence should be between 0 and 1.") + } + + invisible(object) +} + +#' @export +translate_tidyclust.assoc_rules <- function(x, engine = x$engine, ...) { + x <- translate_tidyclust.default(x, engine, ...) + x +} + +# ------------------------------------------------------------------------------ + +#' Simple Wrapper around arules functions +#' +#' This wrapper prepares the data and parameters to send to either `arules::apriori` +#' or `arules::eclat` for association rules mining, depending on the chosen method. +#' +#' @param data A transaction data set. +#' @param min_support Minimum support threshold. +#' @param min_confidence Minimum confidence threshold. +#' @param method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". +#' +#' @return A set of association rules based on the specified parameters. +#' @keywords internal +#' @export +.assoc_rules_fit_arules <- function(x, + min_support = NULL, + min_confidence = NULL, + method = "apriori") { + if (method == "apriori") { + res <- arules::apriori(data, parameter = list(support = min_support, confidence = min_confidence, target = "rules")) + } else if (method == "eclat") { + # Run Eclat first to get frequent itemsets + frequent_itemsets <- arules::eclat(data, parameter = list(support = min_support)) + # Generate association rules from frequent itemsets + res <- arules::ruleInduction(frequent_itemsets, confidence = min_confidence, method = "ptree") + } else { + stop("Invalid engine specified. Choose 'apriori' or 'eclat'.") + } + return(res) +} diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R new file mode 100644 index 0000000..bd0069c --- /dev/null +++ b/R/freq_itemsets.R @@ -0,0 +1,161 @@ +#' Frequent Itemsets Mining +#' +#' @description +#' +#' `freq_itemsets()` defines a model that finds frequent itemsets based on +#' specified minimum support. +#' +#' The method of estimation is chosen by setting the model engine. The +#' engine-specific pages for this model are listed below. +#' +#' - \link[=details_freq_itemsets_arules]{arules} +#' +#' @param mode A single character string for the type of model. The only +#' possible value for this model is "association". +#' @param engine A single character string specifying the computational engine +#' to use for fitting. The default for this model is `"arules"`. +#' @param method A single character string specifying the algorithm to use for +#' fitting. Possible algorithms are `"apriori"` and `"eclat"`. The default for +#' this model is `"apriori"`. +#' @param min_support Positive double, minimum support for an itemset (between 0 and 1). +#' +#' @details +#' +#' ## What does it mean to predict? +#' +#' WORK IN PROGRESS +#' +#' @return A `freq_itemsets` association specification. +#' +#' @examples +#' # Show all engines +#' modelenv::get_from_env("freq_itemsets") +#' +#' freq_itemsets() +#' @export +freq_itemsets <- + function(mode = "association", + engine = "arules", + method = "apriori", + min_support = NULL) { + args <- list( + min_support = enquo(min_support) + ) + + new_cluster_spec( + "freq_itemsets", + args = args, + eng_args = NULL, + mode = mode, + method = NULL, + engine = engine + ) + } + +#' @export +print.freq_itemsets <- function(x, ...) { + cat("Frequent Itemsets Mining Specification (", x$mode, ")\n\n", sep = "") + model_printer(x, ...) + + if (!is.null(x$method$fit$args)) { + cat("Model fit template:\n") + print(show_call(x)) + } + + invisible(x) +} + +# ------------------------------------------------------------------------------ + +#' @method update freq_itemsets +#' @rdname tidyclust_update +#' @export +update.freq_itemsets <- function(object, + parameters = NULL, + min_support = NULL, + fresh = FALSE, ...) { + eng_args <- parsnip::update_engine_parameters( + object$eng_args, + fresh = fresh, ... + ) + + if (!is.null(parameters)) { + parameters <- parsnip::check_final_param(parameters) + } + args <- list( + min_support = enquo(min_support) + ) + + args <- parsnip::update_main_parameters(args, parameters) + + if (fresh) { + object$args <- args + object$eng_args <- eng_args + } else { + null_args <- map_lgl(args, null_value) + if (any(null_args)) { + args <- args[!null_args] + } + if (length(args) > 0) { + object$args[names(args)] <- args + } + if (length(eng_args) > 0) { + object$eng_args[names(eng_args)] <- eng_args + } + } + + new_cluster_spec( + "freq_itemsets", + args = object$args, + eng_args = object$eng_args, + mode = object$mode, + method = NULL, + engine = object$engine + ) +} + +# # ---------------------------------------------------------------------------- + +#' @export +check_args.freq_itemsets <- function(object) { + args <- lapply(object$args, rlang::eval_tidy) + + if (all(is.numeric(args$min_support)) && any(args$min_support >= 0) && any(args$min_support <= 1)) { + rlang::abort("The minimum support should be between 0 and 1.") + } + + invisible(object) +} + +#' @export +translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { + x <- translate_tidyclust.default(x, engine, ...) + x +} + +# ------------------------------------------------------------------------------ + +#' Simple Wrapper around arules functions +#' +#' This wrapper prepares the data and parameters to send to either `arules::apriori` +#' or `arules::eclat` for frequent itemsets mining, depending on the chosen method. +#' +#' @param data A transaction data set. +#' @param min_support Minimum support threshold. +#' @param method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". +#' +#' @return A set of frequent itemsets based on the specified parameters. +#' @keywords internal +#' @export +.freq_itemsets_fit_arules <- function(data, + min_support = NULL, + method = "apriori") { + if (method == "apriori") { + res <- arules::apriori(data, parameter = list(support = min_support, target = "frequent itemsets")) + } else if (method == "eclat") { + res <- arules::eclat(data, parameter = list(support = min_support)) + } else { + stop("Invalid method specified. Choose 'apriori' or 'eclat'.") + } + return(res) +} From 4fed387e582a6d0d1ee13064e349a8cdbe91b310 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 13 Nov 2024 13:01:50 -0800 Subject: [PATCH 02/71] testing new functions --- NAMESPACE | 12 +++++++ R/freq_itemsets.R | 2 +- man/assoc_rules.Rd | 54 +++++++++++++++++++++++++++++ man/dot-assoc_rules_fit_arules.Rd | 30 ++++++++++++++++ man/dot-freq_itemsets_fit_arules.Rd | 23 ++++++++++++ man/dot-k_means_fit_clustMixType.Rd | 5 --- man/freq_itemsets.Rd | 51 +++++++++++++++++++++++++++ man/tidyclust_update.Rd | 28 +++++++++++---- 8 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 man/assoc_rules.Rd create mode 100644 man/dot-assoc_rules_fit_arules.Rd create mode 100644 man/dot-freq_itemsets_fit_arules.Rd create mode 100644 man/freq_itemsets.Rd diff --git a/NAMESPACE b/NAMESPACE index 9960b43..f431691 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,9 @@ S3method(as_tibble,cluster_metric_set) S3method(augment,cluster_fit) +S3method(check_args,assoc_rules) S3method(check_args,default) +S3method(check_args,freq_itemsets) S3method(check_args,hier_clust) S3method(check_args,k_means) S3method(extract_cluster_assignment,KMeansCluster) @@ -33,10 +35,12 @@ S3method(predict,cluster_fit) S3method(predict,cluster_spec) S3method(predict_cluster,cluster_fit) S3method(predict_raw,cluster_fit) +S3method(print,assoc_rules) S3method(print,cluster_fit) S3method(print,cluster_metric_set) S3method(print,cluster_spec) S3method(print,control_cluster) +S3method(print,freq_itemsets) S3method(print,hier_clust) S3method(print,k_means) S3method(required_pkgs,cluster_fit) @@ -57,7 +61,9 @@ S3method(sse_within_total,cluster_fit) S3method(sse_within_total,cluster_spec) S3method(sse_within_total,workflow) S3method(tidy,cluster_fit) +S3method(translate_tidyclust,assoc_rules) S3method(translate_tidyclust,default) +S3method(translate_tidyclust,freq_itemsets) S3method(translate_tidyclust,hier_clust) S3method(translate_tidyclust,k_means) S3method(tunable,cluster_spec) @@ -66,14 +72,19 @@ S3method(tune_args,cluster_spec) S3method(tune_cluster,cluster_spec) S3method(tune_cluster,default) S3method(tune_cluster,workflow) +S3method(update,assoc_rules) +S3method(update,freq_itemsets) S3method(update,hier_clust) S3method(update,k_means) export("%>%") +export(.assoc_rules_fit_arules) +export(.freq_itemsets_fit_arules) export(.hier_clust_fit_stats) export(.k_means_fit_ClusterR) export(.k_means_fit_clustMixType) export(.k_means_fit_klaR) export(.k_means_fit_stats) +export(assoc_rules) export(augment) export(cluster_metric_set) export(control_cluster) @@ -92,6 +103,7 @@ export(fit) export(fit.cluster_spec) export(fit_xy) export(fit_xy.cluster_spec) +export(freq_itemsets) export(get_tidyclust_colors) export(glance) export(hier_clust) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index bd0069c..44b5ca6 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -34,7 +34,7 @@ #' freq_itemsets() #' @export freq_itemsets <- - function(mode = "association", + function(mode = "partition", # will add other modes engine = "arules", method = "apriori", min_support = NULL) { diff --git a/man/assoc_rules.Rd b/man/assoc_rules.Rd new file mode 100644 index 0000000..8170e63 --- /dev/null +++ b/man/assoc_rules.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assoc_rules.R +\name{assoc_rules} +\alias{assoc_rules} +\title{Association Rules Mining} +\usage{ +assoc_rules( + mode = "partition", + engine = "arules", + method = "apriori", + min_support = NULL, + min_confidence = NULL +) +} +\arguments{ +\item{mode}{A single character string for the type of model. The only +possible value for this model is "association".} + +\item{engine}{A single character string specifying the computational engine +to use for fitting. The default for this model is \code{"arules"}.} + +\item{method}{A single character string specifying the algorithm to use for +fitting. Possible algorithms are \code{"apriori"} and \code{"eclat"}. The default for +this model is \code{"apriori"}.} + +\item{min_support}{Positive double, minimum support for a rule (between 0 and 1).} + +\item{min_confidence}{Positive double, minimum confidence for a rule (between 0 and 1).} +} +\value{ +A \code{assoc_rules} association specification. +} +\description{ +\code{assoc_rules()} defines a model that finds association rules based on +specified minimum support and confidence. + +The method of estimation is chosen by setting the model engine. The +engine-specific pages for this model are listed below. +\itemize{ +\item \link[=details_assoc_rules_stats]{arules} +} +} +\details{ +\subsection{What does it mean to predict?}{ + +WORK IN PROGRESS +} +} +\examples{ +# Show all engines +modelenv::get_from_env("assoc_rules") + +assoc_rules() +} diff --git a/man/dot-assoc_rules_fit_arules.Rd b/man/dot-assoc_rules_fit_arules.Rd new file mode 100644 index 0000000..b06d9fd --- /dev/null +++ b/man/dot-assoc_rules_fit_arules.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/assoc_rules.R +\name{.assoc_rules_fit_arules} +\alias{.assoc_rules_fit_arules} +\title{Simple Wrapper around arules functions} +\usage{ +.assoc_rules_fit_arules( + x, + min_support = NULL, + min_confidence = NULL, + method = "apriori" +) +} +\arguments{ +\item{min_support}{Minimum support threshold.} + +\item{min_confidence}{Minimum confidence threshold.} + +\item{method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} + +\item{data}{A transaction data set.} +} +\value{ +A set of association rules based on the specified parameters. +} +\description{ +This wrapper prepares the data and parameters to send to either \code{arules::apriori} +or \code{arules::eclat} for association rules mining, depending on the chosen method. +} +\keyword{internal} diff --git a/man/dot-freq_itemsets_fit_arules.Rd b/man/dot-freq_itemsets_fit_arules.Rd new file mode 100644 index 0000000..d0281f5 --- /dev/null +++ b/man/dot-freq_itemsets_fit_arules.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/freq_itemsets.R +\name{.freq_itemsets_fit_arules} +\alias{.freq_itemsets_fit_arules} +\title{Simple Wrapper around arules functions} +\usage{ +.freq_itemsets_fit_arules(data, min_support = NULL, method = "apriori") +} +\arguments{ +\item{data}{A transaction data set.} + +\item{min_support}{Minimum support threshold.} + +\item{method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} +} +\value{ +A set of frequent itemsets based on the specified parameters. +} +\description{ +This wrapper prepares the data and parameters to send to either \code{arules::apriori} +or \code{arules::eclat} for frequent itemsets mining, depending on the chosen method. +} +\keyword{internal} diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 85067a9..2908bc4 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,11 +7,6 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ -\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} - -\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of -prototypes of the same columns as \code{x}.} - \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/man/freq_itemsets.Rd b/man/freq_itemsets.Rd new file mode 100644 index 0000000..3f0a37b --- /dev/null +++ b/man/freq_itemsets.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/freq_itemsets.R +\name{freq_itemsets} +\alias{freq_itemsets} +\title{Frequent Itemsets Mining} +\usage{ +freq_itemsets( + mode = "partition", + engine = "arules", + method = "apriori", + min_support = NULL +) +} +\arguments{ +\item{mode}{A single character string for the type of model. The only +possible value for this model is "association".} + +\item{engine}{A single character string specifying the computational engine +to use for fitting. The default for this model is \code{"arules"}.} + +\item{method}{A single character string specifying the algorithm to use for +fitting. Possible algorithms are \code{"apriori"} and \code{"eclat"}. The default for +this model is \code{"apriori"}.} + +\item{min_support}{Positive double, minimum support for an itemset (between 0 and 1).} +} +\value{ +A \code{freq_itemsets} association specification. +} +\description{ +\code{freq_itemsets()} defines a model that finds frequent itemsets based on +specified minimum support. + +The method of estimation is chosen by setting the model engine. The +engine-specific pages for this model are listed below. +\itemize{ +\item \link[=details_freq_itemsets_arules]{arules} +} +} +\details{ +\subsection{What does it mean to predict?}{ + +WORK IN PROGRESS +} +} +\examples{ +# Show all engines +modelenv::get_from_env("freq_itemsets") + +freq_itemsets() +} diff --git a/man/tidyclust_update.Rd b/man/tidyclust_update.Rd index d11819f..c12818e 100644 --- a/man/tidyclust_update.Rd +++ b/man/tidyclust_update.Rd @@ -1,11 +1,25 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/hier_clust.R, R/k_means.R, R/update.R -\name{update.hier_clust} +% Please edit documentation in R/assoc_rules.R, R/freq_itemsets.R, +% R/hier_clust.R, R/k_means.R, R/update.R +\name{update.assoc_rules} +\alias{update.assoc_rules} +\alias{update.freq_itemsets} \alias{update.hier_clust} \alias{update.k_means} \alias{tidyclust_update} \title{Update a cluster specification} \usage{ +\method{update}{assoc_rules}( + object, + parameters = NULL, + min_support = NULL, + min_confidence = NULL, + fresh = FALSE, + ... +) + +\method{update}{freq_itemsets}(object, parameters = NULL, min_support = NULL, fresh = FALSE, ...) + \method{update}{hier_clust}( object, parameters = NULL, @@ -27,6 +41,11 @@ updating. If the main arguments are used, these will supersede the values in \code{parameters}. Also, using engine arguments in this object will result in an error.} +\item{fresh}{A logical for whether the arguments should be modified in-place +or replaced wholesale.} + +\item{...}{Not used for \code{update()}.} + \item{num_clusters}{Positive integer, number of clusters in model.} \item{cut_height}{Positive double, height at which to cut dendrogram to @@ -36,11 +55,6 @@ obtain cluster assignments (only used if \code{num_clusters} is \code{NULL})} unambiguous abbreviation of) one of \code{"ward.D"}, \code{"ward.D2"}, \code{"single"}, \code{"complete"}, \code{"average"} (= UPGMA), \code{"mcquitty"} (= WPGMA), \code{"median"} (= WPGMC) or \code{"centroid"} (= UPGMC).} - -\item{fresh}{A logical for whether the arguments should be modified in-place -or replaced wholesale.} - -\item{...}{Not used for \code{update()}.} } \value{ An updated cluster specification. From f70d6f666396c549a653a57dc4f1eb165555024c Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sat, 16 Nov 2024 21:01:16 -0800 Subject: [PATCH 03/71] clustering for freq itemsets function --- R/assoc_rules.R | 2 ++ R/extract_cluster_assignment.R | 27 +++++++++++++++++++++++++++ R/freq_itemsets.R | 2 ++ 3 files changed, 31 insertions(+) diff --git a/R/assoc_rules.R b/R/assoc_rules.R index 1838991..a028e7c 100644 --- a/R/assoc_rules.R +++ b/R/assoc_rules.R @@ -171,5 +171,7 @@ translate_tidyclust.assoc_rules <- function(x, engine = x$engine, ...) { } else { stop("Invalid engine specified. Choose 'apriori' or 'eclat'.") } + + attr(res, "items") <- data.frame(items = dimnames(data)[[2]]) return(res) } diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index af18ce3..bf6861b 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -157,6 +157,33 @@ extract_cluster_assignment.hclust <- function(object, cluster_assignment_tibble(clusters, length(unique(clusters)), ...) } +#' @export +extract_cluster_assignment.freqitemsets <- function(object, ...) { + items <- attr(object, "items") + itemsets <- arules::inspect(object) + + support <- itemsets$support + itemset_sizes <- sapply(strsplit( + gsub("[{}]", "", itemsets$items), ","), + length) + + clusters <- sapply(1:nrow(items), function(i) { + current_itemset <- items[i,] + + # Find relevant itemsets that contain the current itemset + relevant_itemsets <- which( + sapply(strsplit(gsub("[{}]", "", itemsets$items), ","), + function(x) current_itemset %in% x) + ) + + # Apply Highest Support with Largest Itemset Tiebreaker + relevant_itemsets[which.max(cbind(support[relevant_itemsets], + -itemset_sizes[relevant_itemsets]))[1]] + }) + + cluster_assignment_tibble(clusters, length(unique(clusters)), ...) +} + # ------------------------------------------------------------------------------ cluster_assignment_tibble <- function(clusters, diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 44b5ca6..49fdb1b 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -157,5 +157,7 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { } else { stop("Invalid method specified. Choose 'apriori' or 'eclat'.") } + + attr(res, "items") <- data.frame(items = dimnames(data)[[2]]) return(res) } From 7fb5ede0b4ed71f93f88ac0c00d77243726ad43e Mon Sep 17 00:00:00 2001 From: Wander03 Date: Mon, 18 Nov 2024 12:57:21 -0800 Subject: [PATCH 04/71] progress! --- NAMESPACE | 1 + R/assoc_rules_arules.R | 12 +++++ R/freq_itemsets_arules.R | 12 +++++ R/freq_itemsets_data.R | 74 +++++++++++++++++++++++++++++ R/zzz.R | 1 + man/details_freq_itemsets_arules.Rd | 10 ++++ man/details_k_means_ClusterR.Rd | 65 ++++++++++++++++++++++++- 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 R/assoc_rules_arules.R create mode 100644 R/freq_itemsets_arules.R create mode 100644 R/freq_itemsets_data.R create mode 100644 man/details_freq_itemsets_arules.Rd diff --git a/NAMESPACE b/NAMESPACE index f431691..194726c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ S3method(check_args,k_means) S3method(extract_cluster_assignment,KMeansCluster) S3method(extract_cluster_assignment,cluster_fit) S3method(extract_cluster_assignment,cluster_spec) +S3method(extract_cluster_assignment,freqitemsets) S3method(extract_cluster_assignment,hclust) S3method(extract_cluster_assignment,kmeans) S3method(extract_cluster_assignment,kmodes) diff --git a/R/assoc_rules_arules.R b/R/assoc_rules_arules.R new file mode 100644 index 0000000..8c53e6e --- /dev/null +++ b/R/assoc_rules_arules.R @@ -0,0 +1,12 @@ +#' K-means via ClusterR +#' +#' [k_means()] creates K-means model. This engine uses the classical definition +#' of a K-means model, which only takes numeric predictors. +#' +#' @includeRmd man/rmd/k_means_ClusterR.md details +#' +#' @name details_k_means_ClusterR +#' @keywords internal +NULL + +# See inst/README-DOCS.md for a description of how these files are processed diff --git a/R/freq_itemsets_arules.R b/R/freq_itemsets_arules.R new file mode 100644 index 0000000..b0b0f09 --- /dev/null +++ b/R/freq_itemsets_arules.R @@ -0,0 +1,12 @@ +#' Frequent Itemsets via arules +#' +#' [freq_itemsets()] creates K-means model. This engine uses the classical definition +#' of a K-means model, which only takes numeric predictors. +#' +# @includeRmd man/rmd/freq_itemsets_arules.md details +#' +#' @name details_freq_itemsets_arules +#' @keywords internal +NULL + +# See inst/README-DOCS.md for a description of how these files are processed diff --git a/R/freq_itemsets_data.R b/R/freq_itemsets_data.R new file mode 100644 index 0000000..7e8c42f --- /dev/null +++ b/R/freq_itemsets_data.R @@ -0,0 +1,74 @@ +# nocov start + +make_freq_itemsets <- function() { + modelenv::set_new_model("freq_itemsets") + + modelenv::set_model_mode("freq_itemsets", "partition") + + # ---------------------------------------------------------------------------- + + modelenv::set_model_engine("freq_itemsets", "partition", "arules") + modelenv::set_dependency( + model = "freq_itemsets", + mode = "partition", + eng = "arules", + pkg = "arules" + ) + modelenv::set_dependency( + model = "freq_itemsets", + mode = "partition", + eng = "arules", + pkg = "tidyclust" + ) + + modelenv::set_fit( + model = "freq_itemsets", + eng = "arules", + mode = "partition", + value = list( + interface = "matrix", + data = c(x = "data"), + protect = c("x", "min_support"), + func = c(pkg = "tidyclust", fun = ".freq_itemsets_fit_arules"), + defaults = list() + ) + ) + + modelenv::set_encoding( + model = "freq_itemsets", + eng = "arules", + mode = "partition", + options = list( + predictor_indicators = "traditional", + compute_intercept = TRUE, + remove_intercept = TRUE, + allow_sparse_x = FALSE + ) + ) + + modelenv::set_model_arg( + model = "freq_itemsets", + eng = "arules", + exposed = "min_support", + original = "support", + func = list(pkg = "dials", fun = "min_support"), + has_submodel = TRUE + ) + + modelenv::set_pred( + model = "freq_itemsets", + eng = "arules", + mode = "partition", + type = "cluster", + value = list( + pre = NULL, + post = NULL, + func = c(fun = ".freq_itemsets_predict_arules"), + args = + list( + object = rlang::expr(object$fit), + new_data = rlang::expr(new_data) + ) + ) + ) +} diff --git a/R/zzz.R b/R/zzz.R index 44938d0..98b2159 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -3,6 +3,7 @@ .onLoad <- function(libname, pkgname) { make_hier_clust() make_k_means() + make_freq_itemsets() s3_register("generics::required_pkgs", "cluster_fit") s3_register("generics::required_pkgs", "cluster_spec") diff --git a/man/details_freq_itemsets_arules.Rd b/man/details_freq_itemsets_arules.Rd new file mode 100644 index 0000000..ecb2ae2 --- /dev/null +++ b/man/details_freq_itemsets_arules.Rd @@ -0,0 +1,10 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/freq_itemsets_arules.R +\name{details_freq_itemsets_arules} +\alias{details_freq_itemsets_arules} +\title{Frequent Itemsets via arules} +\description{ +\code{\link[=freq_itemsets]{freq_itemsets()}} creates K-means model. This engine uses the classical definition +of a K-means model, which only takes numeric predictors. +} +\keyword{internal} diff --git a/man/details_k_means_ClusterR.Rd b/man/details_k_means_ClusterR.Rd index 8a7781f..ed184e5 100644 --- a/man/details_k_means_ClusterR.Rd +++ b/man/details_k_means_ClusterR.Rd @@ -1,9 +1,12 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/k_means_ClusterR.R +% Please edit documentation in R/assoc_rules_arules.R, R/k_means_ClusterR.R \name{details_k_means_ClusterR} \alias{details_k_means_ClusterR} \title{K-means via ClusterR} \description{ +\code{\link[=k_means]{k_means()}} creates K-means model. This engine uses the classical definition +of a K-means model, which only takes numeric predictors. + \code{\link[=k_means]{k_means()}} creates K-means model. This engine uses the classical definition of a K-means model, which only takes numeric predictors. } @@ -50,6 +53,66 @@ center and scale each so that each predictor has mean zero and a variance of one. } +\subsection{References}{ +\itemize{ +\item Forgy, E. W. (1965). Cluster analysis of multivariate data: efficiency +vs interpretability of classifications. Biometrics, 21, 768–769. +\item Hartigan, J. A. and Wong, M. A. (1979). Algorithm AS 136: A K-means +clustering algorithm. Applied Statistics, 28, 100–108. +\url{doi:10.2307/2346830}. +\item Lloyd, S. P. (1957, 1982). Least squares quantization in PCM. +Technical Note, Bell Laboratories. Published in 1982 in IEEE +Transactions on Information Theory, 28, 128–137. +\item MacQueen, J. (1967). Some methods for classification and analysis of +multivariate observations. In Proceedings of the Fifth Berkeley +Symposium on Mathematical Statistics and Probability, eds L. M. Le Cam +& J. Neyman, 1, pp. 281–297. Berkeley, CA: University of California +Press. +} +} + +For this engine, there is a single mode: partition +\subsection{Tuning Parameters}{ + +This model has 1 tuning parameters: +\itemize{ +\item \code{num_clusters}: # Clusters (type: integer, default: no default) +} +} + +\subsection{Translation from tidyclust to the original package (partition)}{ + +\if{html}{\out{
}}\preformatted{k_means(num_clusters = integer(1)) \%>\% + set_engine("ClusterR") \%>\% + set_mode("partition") \%>\% + translate_tidyclust() +}\if{html}{\out{
}} + +\if{html}{\out{
}}\preformatted{## K Means Cluster Specification (partition) +## +## Main Arguments: +## num_clusters = integer(1) +## +## Computational engine: ClusterR +## +## Model fit template: +## tidyclust::.k_means_fit_ClusterR(data = missing_arg(), clusters = missing_arg(), +## clusters = integer(1)) +}\if{html}{\out{
}} +} + +\subsection{Preprocessing requirements}{ + +Factor/categorical predictors need to be converted to numeric values +(e.g., dummy or indicator variables) for this engine. When using the +formula method via \code{\link[=fit.cluster_spec]{fit()}}, tidyclust +will convert factor columns to indicators. + +Predictors should have the same scale. One way to achieve this is to +center and scale each so that each predictor has mean zero and a +variance of one. +} + \subsection{References}{ \itemize{ \item Forgy, E. W. (1965). Cluster analysis of multivariate data: efficiency From f2a51f58ecf12447c75a22642207ca46a7ab3bf9 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Fri, 3 Jan 2025 22:37:14 -0800 Subject: [PATCH 05/71] fix conditions --- R/freq_itemsets.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 49fdb1b..4adc2ec 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -158,6 +158,6 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { stop("Invalid method specified. Choose 'apriori' or 'eclat'.") } - attr(res, "items") <- data.frame(items = dimnames(data)[[2]]) + # attr(res, "items") <- data.frame(items = dimnames(data)[[2]]) return(res) } From f8787f40e1354b8c02a4f69780b72a283edaaea7 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Fri, 3 Jan 2025 22:37:25 -0800 Subject: [PATCH 06/71] fix conditions --- R/freq_itemsets.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 4adc2ec..1caf013 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -120,7 +120,7 @@ update.freq_itemsets <- function(object, check_args.freq_itemsets <- function(object) { args <- lapply(object$args, rlang::eval_tidy) - if (all(is.numeric(args$min_support)) && any(args$min_support >= 0) && any(args$min_support <= 1)) { + if (all(is.numeric(args$min_support)) && any(args$min_support < 0) && any(args$min_support > 1)) { rlang::abort("The minimum support should be between 0 and 1.") } From ea1c695d56f6d5aad1c80ecc6332a96da367991d Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 7 Jan 2025 17:00:50 -0800 Subject: [PATCH 07/71] Update text --- R/freq_itemsets_arules.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/freq_itemsets_arules.R b/R/freq_itemsets_arules.R index b0b0f09..ca41414 100644 --- a/R/freq_itemsets_arules.R +++ b/R/freq_itemsets_arules.R @@ -1,7 +1,6 @@ #' Frequent Itemsets via arules #' -#' [freq_itemsets()] creates K-means model. This engine uses the classical definition -#' of a K-means model, which only takes numeric predictors. +#' [freq_itemsets()] creates frequent itemset using Apriori or Eclat model #' # @includeRmd man/rmd/freq_itemsets_arules.md details #' From 2d30942bd52404e7684fc3d4217fdc9f1628301b Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 7 Jan 2025 17:01:03 -0800 Subject: [PATCH 08/71] change method to mining_method --- R/freq_itemsets.R | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 1caf013..664d676 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -14,7 +14,7 @@ #' possible value for this model is "association". #' @param engine A single character string specifying the computational engine #' to use for fitting. The default for this model is `"arules"`. -#' @param method A single character string specifying the algorithm to use for +#' @param mining_method A single character string specifying the algorithm to use for #' fitting. Possible algorithms are `"apriori"` and `"eclat"`. The default for #' this model is `"apriori"`. #' @param min_support Positive double, minimum support for an itemset (between 0 and 1). @@ -36,10 +36,11 @@ freq_itemsets <- function(mode = "partition", # will add other modes engine = "arules", - method = "apriori", - min_support = NULL) { + min_support = NULL, + mining_method = "apriori") { args <- list( - min_support = enquo(min_support) + min_support = enquo(min_support), + mining_method = enquo(mining_method) ) new_cluster_spec( @@ -73,6 +74,7 @@ print.freq_itemsets <- function(x, ...) { update.freq_itemsets <- function(object, parameters = NULL, min_support = NULL, + mining_method = NULL, fresh = FALSE, ...) { eng_args <- parsnip::update_engine_parameters( object$eng_args, @@ -142,17 +144,17 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { #' #' @param data A transaction data set. #' @param min_support Minimum support threshold. -#' @param method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". +#' @param mining_method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". #' #' @return A set of frequent itemsets based on the specified parameters. #' @keywords internal #' @export .freq_itemsets_fit_arules <- function(data, min_support = NULL, - method = "apriori") { - if (method == "apriori") { + mining_method = "apriori") { + if (mining_method == "apriori") { res <- arules::apriori(data, parameter = list(support = min_support, target = "frequent itemsets")) - } else if (method == "eclat") { + } else if (mining_method == "eclat") { res <- arules::eclat(data, parameter = list(support = min_support)) } else { stop("Invalid method specified. Choose 'apriori' or 'eclat'.") From 94597490df3e4e377e1f79d886acb144f22f645b Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 7 Jan 2025 17:01:19 -0800 Subject: [PATCH 09/71] create vignette for freq itemsets --- vignettes/articles/freq_itemsets.Rmd | 179 ++++++++++++++++++ .../articles/images/clipboard-2860408154.png | Bin 0 -> 32569 bytes .../articles/images/clipboard-3198084587.png | Bin 0 -> 160663 bytes .../articles/images/clipboard-4074102307.png | Bin 0 -> 175373 bytes 4 files changed, 179 insertions(+) create mode 100644 vignettes/articles/freq_itemsets.Rmd create mode 100644 vignettes/articles/images/clipboard-2860408154.png create mode 100644 vignettes/articles/images/clipboard-3198084587.png create mode 100644 vignettes/articles/images/clipboard-4074102307.png diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd new file mode 100644 index 0000000..4e76ad2 --- /dev/null +++ b/vignettes/articles/freq_itemsets.Rmd @@ -0,0 +1,179 @@ +--- +title: "Frequent Itemset Mining" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Frequent Itemset Mining} + %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + markdown: + wrap: 72 +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## Setup + +```{r} +library(workflows) +library(parsnip) +``` + +Load libraries: + +```{r setup} +library(tidyclust) +library(arules) +set.seed(838383) +``` + +Load and clean a dataset: + +```{r} +data(Groceries) + +# convert to data frame and take subset +groceries <- as.data.frame(as(Groceries[1:100,], "matrix")) +``` + +## A Brief Introduction to Frequent Itemset Mining + +*Frequent Itemset Mining* is a fundamental technique in data mining that +identifies sets of items that appear together frequently in +transactional datasets. These itemsets are often used to uncover +meaningful patterns, such as associationsbetween items, which can then +be leveraged to generate *association rules*. + +For example, in a supermarket transaction database, frequent itemset +mining can identify groups of products that are commonly purchased +together, such as `{milk, bread, eggs}`. These insights are valuable for +applications like recommendation systems, inventory management, and +targeted marketing. + +The key to frequent itemset mining is determining the sets of items that +satisfy a user-defined threshold called the **minimum support**, where +support is defined as the proportion of transactions in which a +particular itemset appears. + +### Methods of Frequent Itemset Mining + +Efficiently discovering these frequent itemsets is a computational +challenge, and several algorithms have been developed to address this +challenge. The two implemented in `{tidyclust}` are the **Apriori** +algorithm and the **Eclat** algorithm. + +#### Finding Frequent Itemsets with the Apriori Algorithm + +The *Apriori* algorithm is one of the earliest and most widely known +methods for frequent itemset mining. It is based on the **Apriori +Principle** (also known as **Downward Closure Property**): any subset of +a frequent itemset must also be frequent. + +#### Process of the Apriori Algorithm + +1. **Initialization**: Begin by identifying all individual items + (1-itemsets) that satisfy the minimum support threshold. These are + called *frequent 1-itemsets*. + +2. **Candidate Generation**: Use the frequent itemsets from the + previous step to generate candidate itemsets of the next size (e.g., + combine frequent 1-itemsets to create candidate 2-itemsets). + +3. **Prune Candidates**: Eliminate candidate itemsets that have subsets + not found to be frequent. + +4. **Support Counting**: Scan the dataset to count the occurrences of + each candidate itemset. + +5. **Iteration**: Repeat steps 2–4 for larger itemsets until no more + frequent itemsets can be generated. + +[![Apriori Princple +Example](images/clipboard-4074102307.png){width="650"}](https://chih-ling-hsu.github.io/2017/03/25/apriori) + +The Apriori algorithm is computationally expensive due to repeated +database scans and the generation of numerous candidates. However, its +pruning strategy significantly reduces the search space compared to a +naïve approach. + +[Source](https://dl.acm.org/doi/pdf/10.1145/170036.170072) + +#### Finding Frequent Itemsets with the Eclat Algorithm + +The *Eclat* (Equivalence Class Transformation) algorithm is an +alternative to Apriori that uses a depth-first search strategy and +vertical data representation. Instead of scanning the dataset +repeatedly, Eclat represents transactions as *tid-lists* (transaction ID +lists), which map each item or itemset to the IDs of transactions in +which it appears. + +#### Process of the Eclat Algorithm + +1. **Vertical Data Representation**: Transform the dataset into a + vertical format, where each item is associated with a list of + transaction IDs. + +2. **Intersect Tid-lists**: Generate frequent itemsets by recursively + intersecting the tid-lists of individual items to form larger + itemsets. The intersection results in a new tid-list, representing + the transactions containing the larger itemset. + +3. **Check Support**: The length of the resulting tid-list determines + the support of the itemset. Remove itemsets not found to be + frequent. + +4. **Recursive Search**: Continue the process for all itemsets until no + further frequent itemsets can be found. + +[![Bookstore +database](images/clipboard-2860408154.png){width="325"}](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=846291) + +[![Computing support of itemsets via tid-list +intersections](images/clipboard-3198084587.png){width="650"}](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=846291) + +Eclat is generally more efficient than Apriori for datasets with many +transactions but fewer unique items, as it avoids the need for multiple +scans of the dataset. However, its performance can degrade for datasets +with very large tid-lists. + +[Source](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=846291) + +## **`freq_itemsets` specification in {tidyclust}** + +To specify a frequent itemsets mining model in `tidyclust`, simply +choose a value of `min_support` and (optionally) a mining method: + +```{r} +fi_spec <- freq_itemsets( + min_support = 0.2, + mining_method = "apriori" +) + +fi_spec +``` + +Currently, the only supported engine is `arules`. The default mining +method is apriori. + +## **Fitting `freq_itemsets` models** + +We fit the model to the data in the usual way: + +```{r} +# fi_fit <- fi_spec %>% +# fit(~ ., +# data = groceries +# +# fi_fit %>% +# summary() +``` + +## Prediction + +***[WIP]*** + diff --git a/vignettes/articles/images/clipboard-2860408154.png b/vignettes/articles/images/clipboard-2860408154.png new file mode 100644 index 0000000000000000000000000000000000000000..cd01f40a5855d6f440f8a1ac92e104e5d2c675bc GIT binary patch literal 32569 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 z

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Ž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�Vr*iY98os5+dW85*q&^aPmCF1_13}(?E& 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Π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 F{{vcCLS+B| literal 0 HcmV?d00001 From 144036cc06d818c763632777394bd53e97f73a0c Mon Sep 17 00:00:00 2001 From: Wander03 Date: Mon, 13 Jan 2025 15:12:40 -0800 Subject: [PATCH 10/71] bug fixing --- R/extract_cluster_assignment.R | 2 +- R/freq_itemsets.R | 18 ++++++++++----- vignettes/articles/freq_itemsets.Rmd | 33 ++++++++++++++++++---------- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index bf6861b..3fda8e8 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -158,7 +158,7 @@ extract_cluster_assignment.hclust <- function(object, } #' @export -extract_cluster_assignment.freqitemsets <- function(object, ...) { +extract_cluster_assignment.itemsets <- function(object, ...) { items <- attr(object, "items") itemsets <- arules::inspect(object) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 664d676..64fa5ce 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -143,23 +143,31 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { #' or `arules::eclat` for frequent itemsets mining, depending on the chosen method. #' #' @param data A transaction data set. -#' @param min_support Minimum support threshold. +#' @param support Minimum support threshold. #' @param mining_method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". #' #' @return A set of frequent itemsets based on the specified parameters. #' @keywords internal #' @export .freq_itemsets_fit_arules <- function(data, - min_support = NULL, + support = NULL, mining_method = "apriori") { + + if (is.null(support)) { + rlang::abort( + "Please specify `min_support` to be able to fit specification.", + call = call("fit") + ) + } + if (mining_method == "apriori") { - res <- arules::apriori(data, parameter = list(support = min_support, target = "frequent itemsets")) + res <- arules::apriori(data, parameter = list(support = support, target = "frequent itemsets")) } else if (mining_method == "eclat") { - res <- arules::eclat(data, parameter = list(support = min_support)) + res <- arules::eclat(data, parameter = list(support = support)) } else { stop("Invalid method specified. Choose 'apriori' or 'eclat'.") } - # attr(res, "items") <- data.frame(items = dimnames(data)[[2]]) + attr(res, "items") <- colnames(data) return(res) } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 4e76ad2..37c5f68 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -44,9 +44,9 @@ groceries <- as.data.frame(as(Groceries[1:100,], "matrix")) ## A Brief Introduction to Frequent Itemset Mining *Frequent Itemset Mining* is a fundamental technique in data mining that -identifies sets of items that appear together frequently in +identifies sets of items that frequently appear together in transactional datasets. These itemsets are often used to uncover -meaningful patterns, such as associationsbetween items, which can then +meaningful patterns, such as associations between items, which can then be leveraged to generate *association rules*. For example, in a supermarket transaction database, frequent itemset @@ -70,8 +70,8 @@ algorithm and the **Eclat** algorithm. #### Finding Frequent Itemsets with the Apriori Algorithm The *Apriori* algorithm is one of the earliest and most widely known -methods for frequent itemset mining. It is based on the **Apriori -Principle** (also known as **Downward Closure Property**): any subset of +methods for frequent itemset mining. It is based on the **Apriori Principle** +(also known as **Downward Closure Property**): any subset of a frequent itemset must also be frequent. #### Process of the Apriori Algorithm @@ -81,7 +81,7 @@ a frequent itemset must also be frequent. called *frequent 1-itemsets*. 2. **Candidate Generation**: Use the frequent itemsets from the - previous step to generate candidate itemsets of the next size (e.g., + previous step to generate candidate itemsets of the next size (e.g. combine frequent 1-itemsets to create candidate 2-itemsets). 3. **Prune Candidates**: Eliminate candidate itemsets that have subsets @@ -165,12 +165,23 @@ method is apriori. We fit the model to the data in the usual way: ```{r} -# fi_fit <- fi_spec %>% -# fit(~ ., -# data = groceries -# -# fi_fit %>% -# summary() +fi_fit <- fi_spec %>% + fit(~ ., + data = groceries + ) + +fi_fit %>% + summary() + + + + +fi_fit %>% + extract_cluster_assignment() + +fi_fit$fit|> str() + + ``` ## Prediction From 1c19e9d49cf0ae34657d5e11aadb6dd0aaf47597 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 14 Jan 2025 01:02:22 -0800 Subject: [PATCH 11/71] fixed name --- NAMESPACE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index 194726c..f8d972b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,8 +10,8 @@ S3method(check_args,k_means) S3method(extract_cluster_assignment,KMeansCluster) S3method(extract_cluster_assignment,cluster_fit) S3method(extract_cluster_assignment,cluster_spec) -S3method(extract_cluster_assignment,freqitemsets) S3method(extract_cluster_assignment,hclust) +S3method(extract_cluster_assignment,itemsets) S3method(extract_cluster_assignment,kmeans) S3method(extract_cluster_assignment,kmodes) S3method(extract_cluster_assignment,kproto) From 6ae24bef90eb7500cbf2f55cc06117a4bcebf328 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 14 Jan 2025 01:02:36 -0800 Subject: [PATCH 12/71] premptive changes --- R/assoc_rules.R | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/R/assoc_rules.R b/R/assoc_rules.R index a028e7c..b5d5fce 100644 --- a/R/assoc_rules.R +++ b/R/assoc_rules.R @@ -150,28 +150,43 @@ translate_tidyclust.assoc_rules <- function(x, engine = x$engine, ...) { #' or `arules::eclat` for association rules mining, depending on the chosen method. #' #' @param data A transaction data set. -#' @param min_support Minimum support threshold. -#' @param min_confidence Minimum confidence threshold. -#' @param method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". +#' @param support Minimum support threshold. +#' @param confidence Minimum confidence threshold. +#' @param mining_method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". #' #' @return A set of association rules based on the specified parameters. #' @keywords internal #' @export .assoc_rules_fit_arules <- function(x, - min_support = NULL, - min_confidence = NULL, - method = "apriori") { - if (method == "apriori") { - res <- arules::apriori(data, parameter = list(support = min_support, confidence = min_confidence, target = "rules")) - } else if (method == "eclat") { + support = NULL, + confidence = NULL, + mining_method = "apriori") { + + if (is.null(support)) { + rlang::abort( + "Please specify `min_support` to be able to fit specification.", + call = call("fit") + ) + } + + if (is.null(confidence)) { + rlang::abort( + "Please specify `min_confidence` to be able to fit specification.", + call = call("fit") + ) + } + + if (mining_method == "apriori") { + res <- arules::apriori(data, parameter = list(support = support, confidence = confidence, target = "rules")) + } else if (mining_method == "eclat") { # Run Eclat first to get frequent itemsets - frequent_itemsets <- arules::eclat(data, parameter = list(support = min_support)) + frequent_itemsets <- arules::eclat(data, parameter = list(support = support)) # Generate association rules from frequent itemsets - res <- arules::ruleInduction(frequent_itemsets, confidence = min_confidence, method = "ptree") + res <- arules::ruleInduction(frequent_itemsets, confidence = confidence, method = "ptree") } else { stop("Invalid engine specified. Choose 'apriori' or 'eclat'.") } - attr(res, "items") <- data.frame(items = dimnames(data)[[2]]) + attr(res, "items") <- colnames(data) return(res) } From 94dcf465835529c657b202636d9a53491f5fe2b0 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 14 Jan 2025 01:03:46 -0800 Subject: [PATCH 13/71] bug fixing freq itemsets --- R/extract_cluster_assignment.R | 30 ++++++++- R/freq_itemsets.R | 19 +++--- R/freq_itemsets_data.R | 14 +++- man/details_freq_itemsets_arules.Rd | 3 +- man/dot-assoc_rules_fit_arules.Rd | 12 ++-- man/dot-freq_itemsets_fit_arules.Rd | 6 +- man/dot-k_means_fit_clustMixType.Rd | 5 ++ man/freq_itemsets.Rd | 10 +-- man/tidyclust_update.Rd | 9 ++- vignettes/articles/freq_itemsets.Rmd | 99 +++++++++++++++++++++++++--- 10 files changed, 166 insertions(+), 41 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index 3fda8e8..d9faf3e 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -167,8 +167,8 @@ extract_cluster_assignment.itemsets <- function(object, ...) { gsub("[{}]", "", itemsets$items), ","), length) - clusters <- sapply(1:nrow(items), function(i) { - current_itemset <- items[i,] + clusters <- sapply(1:length(items), function(i) { + current_itemset <- items[i] # Find relevant itemsets that contain the current itemset relevant_itemsets <- which( @@ -176,12 +176,17 @@ extract_cluster_assignment.itemsets <- function(object, ...) { function(x) current_itemset %in% x) ) + # If item has no frequent itemsets, assign the item to its own cluster + if(length(relevant_itemsets) == 0) { + return(0) + } + # Apply Highest Support with Largest Itemset Tiebreaker relevant_itemsets[which.max(cbind(support[relevant_itemsets], -itemset_sizes[relevant_itemsets]))[1]] }) - cluster_assignment_tibble(clusters, length(unique(clusters)), ...) + cluster_assignment_tibble_w_outliers(clusters, length(unique(clusters)), ...) } # ------------------------------------------------------------------------------ @@ -196,3 +201,22 @@ cluster_assignment_tibble <- function(clusters, tibble::tibble(.cluster = factor(res)) } + +cluster_assignment_tibble_w_outliers <- function(clusters, + n_clusters, + ..., + prefix = "Cluster_") { + reorder_clusts <- order(union(unique(clusters), 0:(n_clusters-1))) + names <- paste0(prefix, 0:(n_clusters-1)) + res <- names[reorder_clusts][clusters+1] + zero_count <- 0 + res <- sapply(res, function(x) { + if (x == "Cluster_0") { + zero_count <<- zero_count + 1 + paste0("Cluster_0_", zero_count) + } else { + x + } + }) + tibble::tibble(.cluster = factor(res)) +} diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 64fa5ce..b3347cc 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -85,7 +85,8 @@ update.freq_itemsets <- function(object, parameters <- parsnip::check_final_param(parameters) } args <- list( - min_support = enquo(min_support) + min_support = enquo(min_support), + mining_method = enquo(mining_method) ) args <- parsnip::update_main_parameters(args, parameters) @@ -143,17 +144,17 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { #' or `arules::eclat` for frequent itemsets mining, depending on the chosen method. #' #' @param data A transaction data set. -#' @param support Minimum support threshold. +#' @param min_support Minimum support threshold. #' @param mining_method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". #' #' @return A set of frequent itemsets based on the specified parameters. #' @keywords internal #' @export -.freq_itemsets_fit_arules <- function(data, - support = NULL, - mining_method = "apriori") { +.freq_itemsets_fit_arules <- function(x, + min_support = NULL, + mining_method = NULL) { - if (is.null(support)) { + if (is.null(min_support)) { rlang::abort( "Please specify `min_support` to be able to fit specification.", call = call("fit") @@ -161,13 +162,13 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { } if (mining_method == "apriori") { - res <- arules::apriori(data, parameter = list(support = support, target = "frequent itemsets")) + res <- arules::apriori(data = x, parameter = list(support = min_support, target = "frequent itemsets")) } else if (mining_method == "eclat") { - res <- arules::eclat(data, parameter = list(support = support)) + res <- arules::eclat(data = x, parameter = list(support = min_support)) } else { stop("Invalid method specified. Choose 'apriori' or 'eclat'.") } - attr(res, "items") <- colnames(data) + attr(res, "items") <- colnames(x) return(res) } diff --git a/R/freq_itemsets_data.R b/R/freq_itemsets_data.R index 7e8c42f..b62c920 100644 --- a/R/freq_itemsets_data.R +++ b/R/freq_itemsets_data.R @@ -27,8 +27,7 @@ make_freq_itemsets <- function() { mode = "partition", value = list( interface = "matrix", - data = c(x = "data"), - protect = c("x", "min_support"), + protect = c("data"), func = c(pkg = "tidyclust", fun = ".freq_itemsets_fit_arules"), defaults = list() ) @@ -50,11 +49,20 @@ make_freq_itemsets <- function() { model = "freq_itemsets", eng = "arules", exposed = "min_support", - original = "support", + original = "min_support", func = list(pkg = "dials", fun = "min_support"), has_submodel = TRUE ) + modelenv::set_model_arg( + model = "freq_itemsets", + eng = "arules", + exposed = "mining_method", + original = "mining_method", + func = list(pkg = "tidyclust", fun = "mining_method"), + has_submodel = TRUE + ) + modelenv::set_pred( model = "freq_itemsets", eng = "arules", diff --git a/man/details_freq_itemsets_arules.Rd b/man/details_freq_itemsets_arules.Rd index ecb2ae2..5e6e264 100644 --- a/man/details_freq_itemsets_arules.Rd +++ b/man/details_freq_itemsets_arules.Rd @@ -4,7 +4,6 @@ \alias{details_freq_itemsets_arules} \title{Frequent Itemsets via arules} \description{ -\code{\link[=freq_itemsets]{freq_itemsets()}} creates K-means model. This engine uses the classical definition -of a K-means model, which only takes numeric predictors. +\code{\link[=freq_itemsets]{freq_itemsets()}} creates frequent itemset using Apriori or Eclat model } \keyword{internal} diff --git a/man/dot-assoc_rules_fit_arules.Rd b/man/dot-assoc_rules_fit_arules.Rd index b06d9fd..cb202ea 100644 --- a/man/dot-assoc_rules_fit_arules.Rd +++ b/man/dot-assoc_rules_fit_arules.Rd @@ -6,17 +6,17 @@ \usage{ .assoc_rules_fit_arules( x, - min_support = NULL, - min_confidence = NULL, - method = "apriori" + support = NULL, + confidence = NULL, + mining_method = "apriori" ) } \arguments{ -\item{min_support}{Minimum support threshold.} +\item{support}{Minimum support threshold.} -\item{min_confidence}{Minimum confidence threshold.} +\item{confidence}{Minimum confidence threshold.} -\item{method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} +\item{mining_method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} \item{data}{A transaction data set.} } diff --git a/man/dot-freq_itemsets_fit_arules.Rd b/man/dot-freq_itemsets_fit_arules.Rd index d0281f5..be153f8 100644 --- a/man/dot-freq_itemsets_fit_arules.Rd +++ b/man/dot-freq_itemsets_fit_arules.Rd @@ -4,14 +4,14 @@ \alias{.freq_itemsets_fit_arules} \title{Simple Wrapper around arules functions} \usage{ -.freq_itemsets_fit_arules(data, min_support = NULL, method = "apriori") +.freq_itemsets_fit_arules(data, support = NULL, mining_method = NULL) } \arguments{ \item{data}{A transaction data set.} -\item{min_support}{Minimum support threshold.} +\item{support}{Minimum support threshold.} -\item{method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} +\item{mining_method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} } \value{ A set of frequent itemsets based on the specified parameters. diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 2908bc4..85067a9 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,6 +7,11 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ +\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} + +\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of +prototypes of the same columns as \code{x}.} + \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/man/freq_itemsets.Rd b/man/freq_itemsets.Rd index 3f0a37b..67d80c9 100644 --- a/man/freq_itemsets.Rd +++ b/man/freq_itemsets.Rd @@ -7,8 +7,8 @@ freq_itemsets( mode = "partition", engine = "arules", - method = "apriori", - min_support = NULL + min_support = NULL, + mining_method = "apriori" ) } \arguments{ @@ -18,11 +18,11 @@ possible value for this model is "association".} \item{engine}{A single character string specifying the computational engine to use for fitting. The default for this model is \code{"arules"}.} -\item{method}{A single character string specifying the algorithm to use for +\item{min_support}{Positive double, minimum support for an itemset (between 0 and 1).} + +\item{mining_method}{A single character string specifying the algorithm to use for fitting. Possible algorithms are \code{"apriori"} and \code{"eclat"}. The default for this model is \code{"apriori"}.} - -\item{min_support}{Positive double, minimum support for an itemset (between 0 and 1).} } \value{ A \code{freq_itemsets} association specification. diff --git a/man/tidyclust_update.Rd b/man/tidyclust_update.Rd index c12818e..46804e1 100644 --- a/man/tidyclust_update.Rd +++ b/man/tidyclust_update.Rd @@ -18,7 +18,14 @@ ... ) -\method{update}{freq_itemsets}(object, parameters = NULL, min_support = NULL, fresh = FALSE, ...) +\method{update}{freq_itemsets}( + object, + parameters = NULL, + min_support = NULL, + mining_method = NULL, + fresh = FALSE, + ... +) \method{update}{hier_clust}( object, diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 37c5f68..e80d52d 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -70,8 +70,8 @@ algorithm and the **Eclat** algorithm. #### Finding Frequent Itemsets with the Apriori Algorithm The *Apriori* algorithm is one of the earliest and most widely known -methods for frequent itemset mining. It is based on the **Apriori Principle** -(also known as **Downward Closure Property**): any subset of +methods for frequent itemset mining. It is based on the **Apriori +Principle** (also known as **Downward Closure Property**): any subset of a frequent itemset must also be frequent. #### Process of the Apriori Algorithm @@ -165,16 +165,98 @@ method is apriori. We fit the model to the data in the usual way: ```{r} -fi_fit <- fi_spec %>% - fit(~ ., - data = groceries - ) -fi_fit %>% - summary() +d <- as.data.frame(as(Groceries, "matrix")) +fi_spec <- freq_itemsets( + min_support = 0.2, + mining_method = "eclat" +) +fi_fit <- fi_spec %>% + fit(~ ., + data = d + ) +inspect(fi_fit$fit) + +fi_fit$spec$method$fit$args$ + +# fi_fit %>% +# summary() + +##################################################################### +test <- arules::eclat( + data = d, + parameter = list(support = 0.05)) + +arules::inspect(test) + +items <- colnames(d) +itemsets <- arules::inspect(test) +itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) + +support <- itemsets$support +clusters <- numeric(length(items)) + +sapply(1:length(items), function(i) { + current_item <- items[i] + + # Find relevant itemsets that contain the current itemset + relevant_itemsets <- which(sapply(itemset_list, function(x) current_item %in% x)) + + + if (length(relevant_itemsets) == 0) { + return(0) # No frequent itemsets, assign to own cluster + } + + # Find all items in relevant itemsets + all_items <- unique(unlist(itemset_list[relevant_itemsets])) + all_items_num <- match(all_items, items) + + # Highest support with largest itemset tiebreaker + best_itemset <- relevant_itemsets[which.max(support[relevant_itemsets])] + + if (clusters[i] == 0 || support[best_itemset] > support[clusters[i]]) { + for (x in all_items_num) { + clusters[x] <<- best_itemset + } + } +}) + +n_clusters <- length(unique(clusters)) +prefix = "Cluster_" + +# Vector to store the resulting cluster names +res <- character(length(clusters)) + +# For items with cluster value 0, assign to "Cluster_0" +res[clusters == 0] <- "Cluster_0" +zero_count <- 0 +res <- sapply(res, function(x) { + if (x == "Cluster_0") { + zero_count <<- zero_count + 1 + paste0("Cluster_0_", zero_count) + } else { + x + } +}) + +# For non-zero clusters, assign sequential cluster numbers starting from "Cluster_1" +non_zero_clusters <- clusters[clusters != 0] +unique_non_zero_clusters <- unique(non_zero_clusters) + +# Map each unique non-zero cluster to a new cluster starting from Cluster_1 +cluster_map <- setNames(paste0(prefix, seq_along(unique_non_zero_clusters)), unique_non_zero_clusters) + +# Assign the corresponding cluster names to the non-zero clusters +res[clusters != 0] <- cluster_map[as.character(non_zero_clusters)] + +tibble::tibble(.cluster = factor(res)) %>% + group_by(.cluster) %>% + summarise(n()) + +##################################################################### fi_fit %>% extract_cluster_assignment() @@ -187,4 +269,3 @@ fi_fit$fit|> str() ## Prediction ***[WIP]*** - From f5c8dd136754a257336138bdb10f51279c75ff52 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 14 Jan 2025 20:50:27 -0800 Subject: [PATCH 14/71] code formatting --- vignettes/articles/freq_itemsets.Rmd | 29 +++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index e80d52d..bf6a01f 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -165,6 +165,28 @@ method is apriori. We fit the model to the data in the usual way: ```{r} +fi_fit <- fi_spec %>% + fit(~ ., + data = groceries + ) + +fi_fit %>% + summary() +``` + +We can also extract the standard `tidyclust` summary list: + +```{r} +# fi_summary <- fi_fit %>% +# extract_fit_summary() +# +# fi_summary %>% +# str() +``` + +```{r} +#| include: false +#| eval: false d <- as.data.frame(as(Groceries, "matrix")) @@ -257,13 +279,6 @@ tibble::tibble(.cluster = factor(res)) %>% summarise(n()) ##################################################################### - -fi_fit %>% - extract_cluster_assignment() - -fi_fit$fit|> str() - - ``` ## Prediction From d01188f2f4285eb7d0761b2af7b773f292dcac29 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 16 Jan 2025 13:29:04 -0800 Subject: [PATCH 15/71] bug fixes --- R/extract_cluster_assignment.R | 54 ++++++++++++++++++++++++++-- R/freq_itemsets.R | 4 +-- vignettes/articles/freq_itemsets.Rmd | 17 ++++----- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index d9faf3e..66a734b 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -206,9 +206,46 @@ cluster_assignment_tibble_w_outliers <- function(clusters, n_clusters, ..., prefix = "Cluster_") { - reorder_clusts <- order(union(unique(clusters), 0:(n_clusters-1))) - names <- paste0(prefix, 0:(n_clusters-1)) - res <- names[reorder_clusts][clusters+1] + items <- colnames(d) + itemsets <- arules::inspect(fi_fit$fit) + itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) + + support <- itemsets$support + clusters <- numeric(length(items)) + + sapply(1:length(items), function(i) { + current_item <- items[i] + + # Find relevant itemsets that contain the current itemset + relevant_itemsets <- which(sapply(itemset_list, function(x) current_item %in% x)) + + + if (length(relevant_itemsets) == 0) { + return(0) # No frequent itemsets, assign to own cluster + } + + # Find all items in relevant itemsets + all_items <- unique(unlist(itemset_list[relevant_itemsets])) + all_items_num <- match(all_items, items) + + # Highest support with largest itemset tiebreaker + best_itemset <- relevant_itemsets[which.max(support[relevant_itemsets])] + + if (clusters[i] == 0 || support[best_itemset] > support[clusters[i]]) { + for (x in all_items_num) { + clusters[x] <<- best_itemset + } + } + }) + + n_clusters <- length(unique(clusters)) + prefix = "Cluster_" + + # Vector to store the resulting cluster names + res <- character(length(clusters)) + + # For items with cluster value 0, assign to "Cluster_0" + res[clusters == 0] <- "Cluster_0" zero_count <- 0 res <- sapply(res, function(x) { if (x == "Cluster_0") { @@ -218,5 +255,16 @@ cluster_assignment_tibble_w_outliers <- function(clusters, x } }) + + # For non-zero clusters, assign sequential cluster numbers starting from "Cluster_1" + non_zero_clusters <- clusters[clusters != 0] + unique_non_zero_clusters <- unique(non_zero_clusters) + + # Map each unique non-zero cluster to a new cluster starting from Cluster_1 + cluster_map <- setNames(paste0(prefix, seq_along(unique_non_zero_clusters)), unique_non_zero_clusters) + + # Assign the corresponding cluster names to the non-zero clusters + res[clusters != 0] <- cluster_map[as.character(non_zero_clusters)] + tibble::tibble(.cluster = factor(res)) } diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index b3347cc..24f33f3 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -143,7 +143,7 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { #' This wrapper prepares the data and parameters to send to either `arules::apriori` #' or `arules::eclat` for frequent itemsets mining, depending on the chosen method. #' -#' @param data A transaction data set. +#' @param x A transaction data set. #' @param min_support Minimum support threshold. #' @param mining_method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". #' @@ -169,6 +169,6 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { stop("Invalid method specified. Choose 'apriori' or 'eclat'.") } - attr(res, "items") <- colnames(x) + attr(res, "item_names") <- colnames(x) return(res) } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index bf6a01f..e8f632b 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -37,8 +37,8 @@ Load and clean a dataset: ```{r} data(Groceries) -# convert to data frame and take subset -groceries <- as.data.frame(as(Groceries[1:100,], "matrix")) +# convert to data frame +groceries <- as.data.frame(as(Groceries, "matrix")) ``` ## A Brief Introduction to Frequent Itemset Mining @@ -188,10 +188,11 @@ We can also extract the standard `tidyclust` summary list: #| include: false #| eval: false -d <- as.data.frame(as(Groceries, "matrix")) +d <- as.data.frame(as(Groceries, "matrix")) |> + mutate(across(everything(), ~.*1)) fi_spec <- freq_itemsets( - min_support = 0.2, + min_support = 0.05, mining_method = "eclat" ) @@ -202,8 +203,6 @@ fi_fit <- fi_spec %>% inspect(fi_fit$fit) -fi_fit$spec$method$fit$args$ - # fi_fit %>% # summary() @@ -215,7 +214,7 @@ test <- arules::eclat( arules::inspect(test) items <- colnames(d) -itemsets <- arules::inspect(test) +itemsets <- arules::inspect(fi_fit$fit) itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) support <- itemsets$support @@ -274,9 +273,7 @@ cluster_map <- setNames(paste0(prefix, seq_along(unique_non_zero_clusters)), uni # Assign the corresponding cluster names to the non-zero clusters res[clusters != 0] <- cluster_map[as.character(non_zero_clusters)] -tibble::tibble(.cluster = factor(res)) %>% - group_by(.cluster) %>% - summarise(n()) +tibble::tibble(.cluster = factor(res)) ##################################################################### ``` From 1074d749e4db6f0954fa2166720df52ae4c8e3f0 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Fri, 17 Jan 2025 13:13:55 -0800 Subject: [PATCH 16/71] updating cluster functions --- R/extract_cluster_assignment.R | 69 ++++++++-------------------- man/dot-freq_itemsets_fit_arules.Rd | 6 +-- vignettes/articles/freq_itemsets.Rmd | 21 ++++++++- 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index 66a734b..34d957f 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -159,55 +159,9 @@ extract_cluster_assignment.hclust <- function(object, #' @export extract_cluster_assignment.itemsets <- function(object, ...) { - items <- attr(object, "items") + items <- attr(object, "item_names") itemsets <- arules::inspect(object) - support <- itemsets$support - itemset_sizes <- sapply(strsplit( - gsub("[{}]", "", itemsets$items), ","), - length) - - clusters <- sapply(1:length(items), function(i) { - current_itemset <- items[i] - - # Find relevant itemsets that contain the current itemset - relevant_itemsets <- which( - sapply(strsplit(gsub("[{}]", "", itemsets$items), ","), - function(x) current_itemset %in% x) - ) - - # If item has no frequent itemsets, assign the item to its own cluster - if(length(relevant_itemsets) == 0) { - return(0) - } - - # Apply Highest Support with Largest Itemset Tiebreaker - relevant_itemsets[which.max(cbind(support[relevant_itemsets], - -itemset_sizes[relevant_itemsets]))[1]] - }) - - cluster_assignment_tibble_w_outliers(clusters, length(unique(clusters)), ...) -} - -# ------------------------------------------------------------------------------ - -cluster_assignment_tibble <- function(clusters, - n_clusters, - ..., - prefix = "Cluster_") { - reorder_clusts <- order(union(unique(clusters), seq_len(n_clusters))) - names <- paste0(prefix, seq_len(n_clusters)) - res <- names[reorder_clusts][clusters] - - tibble::tibble(.cluster = factor(res)) -} - -cluster_assignment_tibble_w_outliers <- function(clusters, - n_clusters, - ..., - prefix = "Cluster_") { - items <- colnames(d) - itemsets <- arules::inspect(fi_fit$fit) itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) support <- itemsets$support @@ -238,9 +192,26 @@ cluster_assignment_tibble_w_outliers <- function(clusters, } }) - n_clusters <- length(unique(clusters)) - prefix = "Cluster_" + item_assignment_tibble_w_outliers(clusters, length(unique(clusters)), ...) +} + +# ------------------------------------------------------------------------------ +cluster_assignment_tibble <- function(clusters, + n_clusters, + ..., + prefix = "Cluster_") { + reorder_clusts <- order(union(unique(clusters), seq_len(n_clusters))) + names <- paste0(prefix, seq_len(n_clusters)) + res <- names[reorder_clusts][clusters] + + tibble::tibble(.cluster = factor(res)) +} + +item_assignment_tibble_w_outliers <- function(clusters, + n_clusters, + ..., + prefix = "Cluster_") { # Vector to store the resulting cluster names res <- character(length(clusters)) diff --git a/man/dot-freq_itemsets_fit_arules.Rd b/man/dot-freq_itemsets_fit_arules.Rd index be153f8..ed45f7e 100644 --- a/man/dot-freq_itemsets_fit_arules.Rd +++ b/man/dot-freq_itemsets_fit_arules.Rd @@ -4,12 +4,12 @@ \alias{.freq_itemsets_fit_arules} \title{Simple Wrapper around arules functions} \usage{ -.freq_itemsets_fit_arules(data, support = NULL, mining_method = NULL) +.freq_itemsets_fit_arules(x, min_support = NULL, mining_method = NULL) } \arguments{ -\item{data}{A transaction data set.} +\item{x}{A transaction data set.} -\item{support}{Minimum support threshold.} +\item{min_support}{Minimum support threshold.} \item{mining_method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index e8f632b..7dffa85 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -38,7 +38,8 @@ Load and clean a dataset: data(Groceries) # convert to data frame -groceries <- as.data.frame(as(Groceries, "matrix")) +groceries <- as.data.frame(as(Groceries, "matrix")) %>% + dplyr::mutate(across(everything(), ~.*1)) ``` ## A Brief Introduction to Frequent Itemset Mining @@ -150,7 +151,7 @@ choose a value of `min_support` and (optionally) a mining method: ```{r} fi_spec <- freq_itemsets( - min_support = 0.2, + min_support = 0.05, mining_method = "apriori" ) @@ -184,6 +185,22 @@ We can also extract the standard `tidyclust` summary list: # str() ``` +Note that, although the frequent itemset algorithm is not focused on cluster's +like other unsupervised learning algorithms, we have created clusters based on +the itemsets. For each item, we find all itemsets that include that item: + +- Itemsets with the highest support are selected as the "dominate" itemset for the item + +- If there is a tie in support, the largest itemset is selected + +- If no itemsets include the item, it is assigned a special "outlier" cluster (Cluster_0_1, Cluster_0_2, etc.) + +```{r} +fi_fit %>% + extract_cluster_assignment() +``` + + ```{r} #| include: false #| eval: false From fdeac746742ac1a72861cf61063db0150122a298 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sun, 26 Jan 2025 18:26:45 -0800 Subject: [PATCH 17/71] save average supports for each cluster (to be used in predict) --- R/extract_cluster_assignment.R | 38 ++++++++++- R/predict_helpers.R | 44 +++++++++++++ vignettes/articles/freq_itemsets.Rmd | 95 ---------------------------- 3 files changed, 79 insertions(+), 98 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index 34d957f..f9d122e 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -167,6 +167,8 @@ extract_cluster_assignment.itemsets <- function(object, ...) { support <- itemsets$support clusters <- numeric(length(items)) + cluster_supports <- list() + sapply(1:length(items), function(i) { current_item <- items[i] @@ -190,9 +192,24 @@ extract_cluster_assignment.itemsets <- function(object, ...) { clusters[x] <<- best_itemset } } + + # Update cluster average support + cluster_supports[[best_itemset]] <<- mean(support[relevant_itemsets]) }) - item_assignment_tibble_w_outliers(clusters, length(unique(clusters)), ...) + item_assignment_tibble_w_outliers(clusters, cluster_supports, ...) + + # # Map cluster IDs to their average support + # cluster_info$average_support <- sapply(cluster_info$.cluster, function(cluster_name) { + # if (grepl("Cluster_0", cluster_name)) { + # return(NA) # No support for "Cluster_0" + # } + # cluster_id <- as.numeric(gsub("Cluster_", "", cluster_name)) + # + # cluster_supports[[cluster_id]] + # }) + # + # cluster_info } # ------------------------------------------------------------------------------ @@ -209,7 +226,7 @@ cluster_assignment_tibble <- function(clusters, } item_assignment_tibble_w_outliers <- function(clusters, - n_clusters, + supports, ..., prefix = "Cluster_") { # Vector to store the resulting cluster names @@ -237,5 +254,20 @@ item_assignment_tibble_w_outliers <- function(clusters, # Assign the corresponding cluster names to the non-zero clusters res[clusters != 0] <- cluster_map[as.character(non_zero_clusters)] - tibble::tibble(.cluster = factor(res)) + # Map average supports using the original cluster IDs + support_mapping <- sapply(clusters, function(cluster_id) { + if (cluster_id == 0) { + NA # Assign NA for items in Cluster_0 + } else { + supports[[cluster_id]] + } + }) + + # Add average support as a column to the tibble + tibble::tibble( + .cluster = factor(res), + average_support = support_mapping + ) + + # tibble::tibble(.cluster = factor(res)) } diff --git a/R/predict_helpers.R b/R/predict_helpers.R index f4780ae..0bd3a6b 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -152,3 +152,47 @@ make_predictions <- function(x, prefix, n_clusters) { pred_clusts } + +# .freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { +# new_data <- as.matrix(new_data) +# +# training_data <- as.matrix(attr(object, "training_data")) +# clusters <- extract_cluster_assignment( +# object, +# ..., +# prefix = prefix, +# call = call("predict") +# +# n_receipts <- nrow(transactions) +# n_clusters <- length(clusters) +# cluster_activity <- matrix(0, nrow = n_receipts, ncol = n_clusters) +# colnames(cluster_activity) <- names(clusters) +# +# # Loop through each receipt +# for (receipt_idx in 1:n_receipts) { +# # Extract items bought in the receipt +# bought_items <- colnames(transactions)[which(transactions[receipt_idx, ] == 1)] +# +# # Loop through each cluster +# for (cluster_idx in seq_along(clusters)) { +# cluster_items <- clusters[[cluster_idx]] +# +# # Calculate Overlap Proportion +# overlap <- intersect(bought_items, cluster_items) +# overlap_proportion <- length(overlap) / length(cluster_items) +# +# # Calculate Average Support +# avg_support <- mean(item_support[cluster_items], na.rm = TRUE) +# +# # Combine Overlap and Support (weighted sum) +# cluster_activity[receipt_idx, cluster_idx] <- +# 0.5 * overlap_proportion + 0.5 * avg_support +# } +# } +# +# # Convert to data frame for output +# cluster_activity_df <- as.data.frame(cluster_activity) +# rownames(cluster_activity_df) <- rownames(transactions) +# return(cluster_activity_df) +# ) +# } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 7dffa85..b8588d2 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -200,101 +200,6 @@ fi_fit %>% extract_cluster_assignment() ``` - -```{r} -#| include: false -#| eval: false - -d <- as.data.frame(as(Groceries, "matrix")) |> - mutate(across(everything(), ~.*1)) - -fi_spec <- freq_itemsets( - min_support = 0.05, - mining_method = "eclat" -) - -fi_fit <- fi_spec %>% - fit(~ ., - data = d - ) - -inspect(fi_fit$fit) - -# fi_fit %>% -# summary() - -##################################################################### -test <- arules::eclat( - data = d, - parameter = list(support = 0.05)) - -arules::inspect(test) - -items <- colnames(d) -itemsets <- arules::inspect(fi_fit$fit) -itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) - -support <- itemsets$support -clusters <- numeric(length(items)) - -sapply(1:length(items), function(i) { - current_item <- items[i] - - # Find relevant itemsets that contain the current itemset - relevant_itemsets <- which(sapply(itemset_list, function(x) current_item %in% x)) - - - if (length(relevant_itemsets) == 0) { - return(0) # No frequent itemsets, assign to own cluster - } - - # Find all items in relevant itemsets - all_items <- unique(unlist(itemset_list[relevant_itemsets])) - all_items_num <- match(all_items, items) - - # Highest support with largest itemset tiebreaker - best_itemset <- relevant_itemsets[which.max(support[relevant_itemsets])] - - if (clusters[i] == 0 || support[best_itemset] > support[clusters[i]]) { - for (x in all_items_num) { - clusters[x] <<- best_itemset - } - } -}) - -n_clusters <- length(unique(clusters)) -prefix = "Cluster_" - -# Vector to store the resulting cluster names -res <- character(length(clusters)) - -# For items with cluster value 0, assign to "Cluster_0" -res[clusters == 0] <- "Cluster_0" -zero_count <- 0 -res <- sapply(res, function(x) { - if (x == "Cluster_0") { - zero_count <<- zero_count + 1 - paste0("Cluster_0_", zero_count) - } else { - x - } -}) - -# For non-zero clusters, assign sequential cluster numbers starting from "Cluster_1" -non_zero_clusters <- clusters[clusters != 0] -unique_non_zero_clusters <- unique(non_zero_clusters) - -# Map each unique non-zero cluster to a new cluster starting from Cluster_1 -cluster_map <- setNames(paste0(prefix, seq_along(unique_non_zero_clusters)), unique_non_zero_clusters) - -# Assign the corresponding cluster names to the non-zero clusters -res[clusters != 0] <- cluster_map[as.character(non_zero_clusters)] - -tibble::tibble(.cluster = factor(res)) - -##################################################################### -``` - ## Prediction ***[WIP]*** From 02dbce1c10fd86318ff00dfe7cc38a8cf57660ae Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 4 Feb 2025 18:48:52 -0800 Subject: [PATCH 18/71] predict not saving output --- R/predict_helpers.R | 101 +++++++++++++++------------ vignettes/articles/freq_itemsets.Rmd | 74 ++++++++++++++++++++ 2 files changed, 132 insertions(+), 43 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 0bd3a6b..2c0ecbe 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -153,46 +153,61 @@ make_predictions <- function(x, prefix, n_clusters) { pred_clusts } -# .freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { -# new_data <- as.matrix(new_data) -# -# training_data <- as.matrix(attr(object, "training_data")) -# clusters <- extract_cluster_assignment( -# object, -# ..., -# prefix = prefix, -# call = call("predict") -# -# n_receipts <- nrow(transactions) -# n_clusters <- length(clusters) -# cluster_activity <- matrix(0, nrow = n_receipts, ncol = n_clusters) -# colnames(cluster_activity) <- names(clusters) -# -# # Loop through each receipt -# for (receipt_idx in 1:n_receipts) { -# # Extract items bought in the receipt -# bought_items <- colnames(transactions)[which(transactions[receipt_idx, ] == 1)] -# -# # Loop through each cluster -# for (cluster_idx in seq_along(clusters)) { -# cluster_items <- clusters[[cluster_idx]] -# -# # Calculate Overlap Proportion -# overlap <- intersect(bought_items, cluster_items) -# overlap_proportion <- length(overlap) / length(cluster_items) -# -# # Calculate Average Support -# avg_support <- mean(item_support[cluster_items], na.rm = TRUE) -# -# # Combine Overlap and Support (weighted sum) -# cluster_activity[receipt_idx, cluster_idx] <- -# 0.5 * overlap_proportion + 0.5 * avg_support -# } -# } -# -# # Convert to data frame for output -# cluster_activity_df <- as.data.frame(cluster_activity) -# rownames(cluster_activity_df) <- rownames(transactions) -# return(cluster_activity_df) -# ) -# } +.freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { + new_data <- as.data.frame(new_data) + + # Extract frequent itemsets and their supports + items <- attr(object, "item_names") + itemsets <- arules::inspect(object) + supports <- itemsets$supports + + for (i in seq_len(nrow(new_data))) { + observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] + missing_items <- colnames(new_data)[which(is.na(new_data[i, ]))] + + for (item in missing_items) { + # Find relevant itemsets and supports + relevant_indices <- which(sapply(frequent_itemsets, function(x) item %in% x && any(observed_items %in% x))) + relevant_itemsets <- frequent_itemsets[relevant_indices] + relevant_supports <- supports[relevant_indices] + + # Compute confidence for each relevant itemset + probabilities <- sapply(seq_along(relevant_itemsets), function(idx) { + itemset <- relevant_itemsets[[idx]] + itemset_without_item <- setdiff(itemset, item) + + # Find support values using indices + support_full <- relevant_supports[idx] + support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] + + if (length(support_without) > 0) { + return(support_full / support_without[1]) + } else { + return(NA) + } + }, USE.NAMES = FALSE) + + # Aggregate probabilities (using mean) + prob_estimate <- ifelse(length(na.omit(probabilities)) > 0, mean(na.omit(probabilities)), NA) + + # Replace NA with probability estimate + new_data[i, item] <- prob_estimate + + print(i) + print(item) + print(prob_estimate) + print(itemset) + print(itemset_without_item) + print(support_without) + print(new_data) + print(probabilities) # WH^Y IS THIS NA? + print(support_full) + print(support_without[1]) + print(length(support_without)) + print(missing_items) + print(new_data[i, item]) + } + } + + return(new_data) +} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index b8588d2..8b1922e 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -202,4 +202,78 @@ fi_fit %>% ## Prediction +```{r} + +test <- groceries[1,] %>% + dplyr::mutate(`whole milk` = as.numeric(NA), + yogurt = as.numeric(1)) + +a <- fi_fit %>% + predict(test) + +a + + + +pred_test(fi_fit$fit, test) +``` + +```{r} +object <- fi_fit$fit +new_data <- test + + +itemsets <- arules::inspect(object) +# Extract frequent itemsets and their supports +frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) +supports <- itemsets$support + +# Convert new_data to a matrix for efficient processing +new_data <- as.matrix(new_data) + +# Initialize a list to store predictions +predictions <- vector("list", nrow(new_data)) + +for (i in seq_len(nrow(new_data))) { + + i <- 1 + + observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] + missing_items <- colnames(new_data)[which(is.na(new_data[i, ]))] + + item_probabilities <- list() + + for (item in missing_items) { + + item <- missing_items[1] + + relevant_indices <- which(sapply(frequent_itemsets, function(x) item %in% x && any(observed_items %in% x))) + relevant_itemsets <- frequent_itemsets[relevant_indices] + relevant_supports <- supports[relevant_indices] + + # Compute confidence for each relevant itemset + probabilities <- sapply(seq_along(relevant_itemsets), function(idx) { + itemset <- relevant_itemsets[[idx]] + itemset_without_item <- setdiff(itemset, item) + + # Find support values using indices + support_full <- relevant_supports[idx] + support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] + + if (length(support_without) > 0) { + return(support_full / support_without[1]) + } else { + return(NA) + } + }, USE.NAMES = FALSE) + # Aggregate probabilities (e.g., using mean) + prob_estimate <- ifelse(length(na.omit(probabilities)) > 0, mean(na.omit(probabilities)), NA) + + # Replace NA with probability estimate + new_data[i, item] <- prob_estimate + } + +new_data +``` + ***[WIP]*** From ecf7486387c3b6b8a7697dbe01fab7c2316848b6 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 6 Feb 2025 12:15:25 -0800 Subject: [PATCH 19/71] some change --- R/predict_helpers.R | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 2c0ecbe..01354b1 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -158,8 +158,8 @@ make_predictions <- function(x, prefix, n_clusters) { # Extract frequent itemsets and their supports items <- attr(object, "item_names") - itemsets <- arules::inspect(object) - supports <- itemsets$supports + frequent_itemsets <- arules::inspect(object) + supports <- frequent_itemsets$supports for (i in seq_len(nrow(new_data))) { observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] @@ -180,6 +180,9 @@ make_predictions <- function(x, prefix, n_clusters) { support_full <- relevant_supports[idx] support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] + print(support_full) + print(support_without[1]) + if (length(support_without) > 0) { return(support_full / support_without[1]) } else { @@ -196,14 +199,9 @@ make_predictions <- function(x, prefix, n_clusters) { print(i) print(item) print(prob_estimate) - print(itemset) - print(itemset_without_item) - print(support_without) + print(frequent_itemsets) print(new_data) print(probabilities) # WH^Y IS THIS NA? - print(support_full) - print(support_without[1]) - print(length(support_without)) print(missing_items) print(new_data[i, item]) } From cff078c32eadddb1d189b717e40add963444f025 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sat, 8 Feb 2025 13:00:51 -0800 Subject: [PATCH 20/71] fixed predcit! Proba is now put in N/A spots --- R/predict_helpers.R | 17 ++----- vignettes/articles/freq_itemsets.Rmd | 66 +--------------------------- 2 files changed, 4 insertions(+), 79 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 01354b1..0da290a 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -158,8 +158,9 @@ make_predictions <- function(x, prefix, n_clusters) { # Extract frequent itemsets and their supports items <- attr(object, "item_names") - frequent_itemsets <- arules::inspect(object) - supports <- frequent_itemsets$supports + itemsets <- arules::inspect(object) + frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) + supports <- itemsets$support for (i in seq_len(nrow(new_data))) { observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] @@ -180,9 +181,6 @@ make_predictions <- function(x, prefix, n_clusters) { support_full <- relevant_supports[idx] support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] - print(support_full) - print(support_without[1]) - if (length(support_without) > 0) { return(support_full / support_without[1]) } else { @@ -195,15 +193,6 @@ make_predictions <- function(x, prefix, n_clusters) { # Replace NA with probability estimate new_data[i, item] <- prob_estimate - - print(i) - print(item) - print(prob_estimate) - print(frequent_itemsets) - print(new_data) - print(probabilities) # WH^Y IS THIS NA? - print(missing_items) - print(new_data[i, item]) } } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 8b1922e..21cbb91 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -208,72 +208,8 @@ test <- groceries[1,] %>% dplyr::mutate(`whole milk` = as.numeric(NA), yogurt = as.numeric(1)) -a <- fi_fit %>% +fi_fit %>% predict(test) - -a - - - -pred_test(fi_fit$fit, test) -``` - -```{r} -object <- fi_fit$fit -new_data <- test - - -itemsets <- arules::inspect(object) -# Extract frequent itemsets and their supports -frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) -supports <- itemsets$support - -# Convert new_data to a matrix for efficient processing -new_data <- as.matrix(new_data) - -# Initialize a list to store predictions -predictions <- vector("list", nrow(new_data)) - -for (i in seq_len(nrow(new_data))) { - - i <- 1 - - observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] - missing_items <- colnames(new_data)[which(is.na(new_data[i, ]))] - - item_probabilities <- list() - - for (item in missing_items) { - - item <- missing_items[1] - - relevant_indices <- which(sapply(frequent_itemsets, function(x) item %in% x && any(observed_items %in% x))) - relevant_itemsets <- frequent_itemsets[relevant_indices] - relevant_supports <- supports[relevant_indices] - - # Compute confidence for each relevant itemset - probabilities <- sapply(seq_along(relevant_itemsets), function(idx) { - itemset <- relevant_itemsets[[idx]] - itemset_without_item <- setdiff(itemset, item) - - # Find support values using indices - support_full <- relevant_supports[idx] - support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] - - if (length(support_without) > 0) { - return(support_full / support_without[1]) - } else { - return(NA) - } - }, USE.NAMES = FALSE) - # Aggregate probabilities (e.g., using mean) - prob_estimate <- ifelse(length(na.omit(probabilities)) > 0, mean(na.omit(probabilities)), NA) - - # Replace NA with probability estimate - new_data[i, item] <- prob_estimate - } - -new_data ``` ***[WIP]*** From 4d45209906bd1d183e70750e090f7d3a7b8dc5b8 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 26 Feb 2025 18:25:59 -0800 Subject: [PATCH 21/71] remove avg support tracker (unused) --- R/extract_cluster_assignment.R | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index f9d122e..ab97e39 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -197,19 +197,7 @@ extract_cluster_assignment.itemsets <- function(object, ...) { cluster_supports[[best_itemset]] <<- mean(support[relevant_itemsets]) }) - item_assignment_tibble_w_outliers(clusters, cluster_supports, ...) - - # # Map cluster IDs to their average support - # cluster_info$average_support <- sapply(cluster_info$.cluster, function(cluster_name) { - # if (grepl("Cluster_0", cluster_name)) { - # return(NA) # No support for "Cluster_0" - # } - # cluster_id <- as.numeric(gsub("Cluster_", "", cluster_name)) - # - # cluster_supports[[cluster_id]] - # }) - # - # cluster_info + item_assignment_tibble_w_outliers(clusters, ...) } # ------------------------------------------------------------------------------ @@ -226,7 +214,6 @@ cluster_assignment_tibble <- function(clusters, } item_assignment_tibble_w_outliers <- function(clusters, - supports, ..., prefix = "Cluster_") { # Vector to store the resulting cluster names @@ -254,20 +241,5 @@ item_assignment_tibble_w_outliers <- function(clusters, # Assign the corresponding cluster names to the non-zero clusters res[clusters != 0] <- cluster_map[as.character(non_zero_clusters)] - # Map average supports using the original cluster IDs - support_mapping <- sapply(clusters, function(cluster_id) { - if (cluster_id == 0) { - NA # Assign NA for items in Cluster_0 - } else { - supports[[cluster_id]] - } - }) - - # Add average support as a column to the tibble - tibble::tibble( - .cluster = factor(res), - average_support = support_mapping - ) - - # tibble::tibble(.cluster = factor(res)) + tibble::tibble(.cluster = factor(res)) } From 1f8f633fe6e916d1e0ca8853896b41a0c9e7bf7b Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 26 Feb 2025 20:24:19 -0800 Subject: [PATCH 22/71] change best cluster to prioritize size then support add predict to vingette --- R/extract_cluster_assignment.R | 26 +++++++++++++++++---- vignettes/articles/freq_itemsets.Rmd | 35 +++++++++++++++++++++------- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index ab97e39..9a7a44f 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -184,15 +184,31 @@ extract_cluster_assignment.itemsets <- function(object, ...) { all_items <- unique(unlist(itemset_list[relevant_itemsets])) all_items_num <- match(all_items, items) - # Highest support with largest itemset tiebreaker - best_itemset <- relevant_itemsets[which.max(support[relevant_itemsets])] - - if (clusters[i] == 0 || support[best_itemset] > support[clusters[i]]) { + # Find the size of each relevant itemset + itemset_sizes <- sapply(itemset_list[relevant_itemsets], length) + + # Largest itemset with highest support tiebreaker + best_itemset <- relevant_itemsets[which.max(itemset_sizes * max(support) + support[relevant_itemsets])] + + # Get the size and support of the best itemset + best_itemset_size <- length(itemset_list[[best_itemset]]) + best_itemset_support <- support[best_itemset] + + # Get the size and support of the current cluster (if any) + current_cluster_size <- if (clusters[i] != 0) length(itemset_list[[clusters[i]]]) else 0 + current_cluster_support <- if (clusters[i] != 0) support[clusters[i]] else 0 + + # Create Cluster from best_itemset if: + # 1. The item has no cluster, or + # 2. The best_itemset is larger than the current cluster, or + # 3. The best_itemset is the same size but has higher support + if (clusters[i] == 0 || + best_itemset_size > current_cluster_size || + (best_itemset_size == current_cluster_size && best_itemset_support > current_cluster_support)) { for (x in all_items_num) { clusters[x] <<- best_itemset } } - # Update cluster average support cluster_supports[[best_itemset]] <<- mean(support[relevant_itemsets]) }) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 21cbb91..5b6614d 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -189,9 +189,9 @@ Note that, although the frequent itemset algorithm is not focused on cluster's like other unsupervised learning algorithms, we have created clusters based on the itemsets. For each item, we find all itemsets that include that item: -- Itemsets with the highest support are selected as the "dominate" itemset for the item +- Itemsets with the largest size are selected as the "dominate" itemset for the item -- If there is a tie in support, the largest itemset is selected +- If there is a tie in size, the itemset with the highest support is selected - If no itemsets include the item, it is assigned a special "outlier" cluster (Cluster_0_1, Cluster_0_2, etc.) @@ -202,14 +202,31 @@ fi_fit %>% ## Prediction -```{r} +Since frequent itemset mining identifies patterns in co-occurring items rather +than learning a predictive function, the notion of "prediction" is not as +straightforward as in supervised learning. However, given a set of frequent +itemsets from historical data, it is possible to estimate the likelihood that a +missing item in new data is present based on observed co-occurring items. + +The predict() function utilizes frequent itemsets and their support values to +estimate probabilities for missing items in new transactions. For each row in +new_data, the function identifies observed items and missing items. It then +searches for frequent itemsets that contain both the missing item and at least +one observed item. Using the support values of these itemsets, it estimates the +probability that the missing item is present based on the confidence of +association between observed and missing items. -test <- groceries[1,] %>% +The function fills in missing values with these probability estimates, +effectively "predicting" the likelihood of item presence based on historical +co-occurrence patterns. + +```{r} +new_data <- groceries[1,] %>% dplyr::mutate(`whole milk` = as.numeric(NA), - yogurt = as.numeric(1)) + `other vegetables` = as.numeric(1)) -fi_fit %>% - predict(test) -``` +results <- fi_fit %>% + predict(new_data) -***[WIP]*** +results[[1]] +``` From 690c385c3b95fb277cbb2c6702500e958d286434 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 4 Mar 2025 17:46:31 -0800 Subject: [PATCH 23/71] vignette testing --- man/dot-k_means_fit_clustMixType.Rd | 5 ----- vignettes/articles/freq_itemsets.Rmd | 7 ++++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 85067a9..2908bc4 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,11 +7,6 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ -\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} - -\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of -prototypes of the same columns as \code{x}.} - \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 5b6614d..7cc89a2 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -221,7 +221,7 @@ effectively "predicting" the likelihood of item presence based on historical co-occurrence patterns. ```{r} -new_data <- groceries[1,] %>% +new_data <- groceries[1:5,] %>% dplyr::mutate(`whole milk` = as.numeric(NA), `other vegetables` = as.numeric(1)) @@ -229,4 +229,9 @@ results <- fi_fit %>% predict(new_data) results[[1]] + +predict_cluster(object = fi_fit, new_data= new_data) |> class() + +tidyclust:::format_cluster(list(a = c(bob = 2), b = c(joe = 5 + ))) ``` From ae9917bc251c389e9f720a5d00e122a3f4ad5d5b Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 4 Mar 2025 19:01:52 -0800 Subject: [PATCH 24/71] predict output formated & cutoff implemented --- R/predict_helpers.R | 21 +++++++++++++++++++-- vignettes/articles/freq_itemsets.Rmd | 23 ++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 0da290a..07382c7 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -153,7 +153,7 @@ make_predictions <- function(x, prefix, n_clusters) { pred_clusts } -.freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { +.freq_itemsets_predict_arules <- function(object, new_data, cutoff = 0.5, ..., prefix = "Cluster_") { new_data <- as.data.frame(new_data) # Extract frequent itemsets and their supports @@ -162,6 +162,16 @@ make_predictions <- function(x, prefix, n_clusters) { frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) supports <- itemsets$support + # Transform the output dataframe format (make wide) + result_list <- lapply(1:nrow(new_data), function(i) { + row_data <- new_data[i, ] + data.frame( + item = items, + prob = as.vector(as.matrix(row_data)), + pred = is.na(as.vector(as.matrix(row_data))) + ) + }) + for (i in seq_len(nrow(new_data))) { observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] missing_items <- colnames(new_data)[which(is.na(new_data[i, ]))] @@ -196,5 +206,12 @@ make_predictions <- function(x, prefix, n_clusters) { } } - return(new_data) + # Apply cutoff to each dataframe in result_list + result_list <- lapply(result_list, function(df) { + row_data <- new_data[i, ] + df$prob <- ifelse(df$pred & as.vector(as.matrix(row_data)) >= cutoff, 1, 0) + df + }) + + return(result_list) } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 7cc89a2..e91d808 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -228,10 +228,31 @@ new_data <- groceries[1:5,] %>% results <- fi_fit %>% predict(new_data) -results[[1]] +results[[1,1]] predict_cluster(object = fi_fit, new_data= new_data) |> class() tidyclust:::format_cluster(list(a = c(bob = 2), b = c(joe = 5 ))) + + + + + + + +result_list <- lapply(1:nrow(new_data), function(i) { + row_data <- new_data[i, ] + # print(colnames(row_data)) + data.frame( + item = colnames(new_data), + prob = as.vector(as.matrix(row_data)), + pred = is.na(as.vector(as.matrix(row_data))) + ) + }) + +result_list <- lapply(result_list, function(df) { + df$prob <- ifelse(df$pred & df$prob >= .5, 1, df$prob) + df + }) ``` From a6826c38b246a98233cb9f2c84b45b714bbbec9c Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 6 Mar 2025 11:10:17 -0800 Subject: [PATCH 25/71] create holder for extract_predictions function (placeholder name) --- R/extract_predictions.R | 48 ++++++++++++++++++++++++++++ vignettes/articles/freq_itemsets.Rmd | 26 --------------- 2 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 R/extract_predictions.R diff --git a/R/extract_predictions.R b/R/extract_predictions.R new file mode 100644 index 0000000..8245119 --- /dev/null +++ b/R/extract_predictions.R @@ -0,0 +1,48 @@ +#' Returns recommender predictions with predicted values imputed into dataset +#' Notes: currently imputes raw probabilities + +extract_predictions <- function(object, new_data, cutoff = 0.5, ..., prefix = "Cluster_") { + new_data <- as.data.frame(new_data) + + # Extract frequent itemsets and their supports + items <- attr(object, "item_names") + itemsets <- arules::inspect(object) + frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) + supports <- itemsets$support + + for (i in seq_len(nrow(new_data))) { + observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] + missing_items <- colnames(new_data)[which(is.na(new_data[i, ]))] + + for (item in missing_items) { + # Find relevant itemsets and supports + relevant_indices <- which(sapply(frequent_itemsets, function(x) item %in% x && any(observed_items %in% x))) + relevant_itemsets <- frequent_itemsets[relevant_indices] + relevant_supports <- supports[relevant_indices] + + # Compute confidence for each relevant itemset + probabilities <- sapply(seq_along(relevant_itemsets), function(idx) { + itemset <- relevant_itemsets[[idx]] + itemset_without_item <- setdiff(itemset, item) + + # Find support values using indices + support_full <- relevant_supports[idx] + support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] + + if (length(support_without) > 0) { + return(support_full / support_without[1]) + } else { + return(NA) + } + }, USE.NAMES = FALSE) + + # Aggregate probabilities (using mean) + prob_estimate <- ifelse(length(na.omit(probabilities)) > 0, mean(na.omit(probabilities)), NA) + + # Replace NA with probability estimate + new_data[i, item] <- prob_estimate + } + } + + return(new_data) +} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index e91d808..46cc6ce 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -229,30 +229,4 @@ results <- fi_fit %>% predict(new_data) results[[1,1]] - -predict_cluster(object = fi_fit, new_data= new_data) |> class() - -tidyclust:::format_cluster(list(a = c(bob = 2), b = c(joe = 5 - ))) - - - - - - - -result_list <- lapply(1:nrow(new_data), function(i) { - row_data <- new_data[i, ] - # print(colnames(row_data)) - data.frame( - item = colnames(new_data), - prob = as.vector(as.matrix(row_data)), - pred = is.na(as.vector(as.matrix(row_data))) - ) - }) - -result_list <- lapply(result_list, function(df) { - df$prob <- ifelse(df$pred & df$prob >= .5, 1, df$prob) - df - }) ``` From cf3e82bc164d33243ef43e90d3f714d225d423a9 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 6 Mar 2025 12:58:39 -0800 Subject: [PATCH 26/71] hard code cutoff --- R/predict_helpers.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 07382c7..1beae93 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -153,7 +153,7 @@ make_predictions <- function(x, prefix, n_clusters) { pred_clusts } -.freq_itemsets_predict_arules <- function(object, new_data, cutoff = 0.5, ..., prefix = "Cluster_") { +.freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { new_data <- as.data.frame(new_data) # Extract frequent itemsets and their supports @@ -167,7 +167,7 @@ make_predictions <- function(x, prefix, n_clusters) { row_data <- new_data[i, ] data.frame( item = items, - prob = as.vector(as.matrix(row_data)), + .prob = as.vector(as.matrix(row_data)), pred = is.na(as.vector(as.matrix(row_data))) ) }) @@ -209,7 +209,7 @@ make_predictions <- function(x, prefix, n_clusters) { # Apply cutoff to each dataframe in result_list result_list <- lapply(result_list, function(df) { row_data <- new_data[i, ] - df$prob <- ifelse(df$pred & as.vector(as.matrix(row_data)) >= cutoff, 1, 0) + df$.prob <- ifelse(df$pred & as.vector(as.matrix(row_data)) >= 0.5, 1, 0) df }) From c6acf0187a6cb5efc78693f19af0efdd39495297 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 13 Mar 2025 00:48:41 -0700 Subject: [PATCH 27/71] change predict formating --- R/extract_predictions.R | 54 ++++++++-------------------- R/predict_helpers.R | 6 ++-- man/dot-k_means_fit_clustMixType.Rd | 5 +++ man/extract_predictions.Rd | 13 +++++++ vignettes/articles/freq_itemsets.Rmd | 7 ++++ 5 files changed, 42 insertions(+), 43 deletions(-) create mode 100644 man/extract_predictions.Rd diff --git a/R/extract_predictions.R b/R/extract_predictions.R index 8245119..bc78f08 100644 --- a/R/extract_predictions.R +++ b/R/extract_predictions.R @@ -1,48 +1,22 @@ #' Returns recommender predictions with predicted values imputed into dataset -#' Notes: currently imputes raw probabilities +#' Notes: currently imputes thresholded probabilities -extract_predictions <- function(object, new_data, cutoff = 0.5, ..., prefix = "Cluster_") { - new_data <- as.data.frame(new_data) +extract_predictions <- function(pred_output) { - # Extract frequent itemsets and their supports - items <- attr(object, "item_names") - itemsets <- arules::inspect(object) - frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) - supports <- itemsets$support + result_df <- pred_output %>% + purrr::map(~ { + print(.x) + .x %>% + dplyr::rowwise() %>% + dplyr::mutate(value = ifelse(!is.na(.obs_item), .obs_item, .pred_item)) %>% + dplyr::select(item, value) %>% + tidyr::pivot_wider(names_from = item, values_from = value) + }) %>% + purrr::list_rbind() # Combine the list of data frames into a single data frame - for (i in seq_len(nrow(new_data))) { - observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] - missing_items <- colnames(new_data)[which(is.na(new_data[i, ]))] - - for (item in missing_items) { - # Find relevant itemsets and supports - relevant_indices <- which(sapply(frequent_itemsets, function(x) item %in% x && any(observed_items %in% x))) - relevant_itemsets <- frequent_itemsets[relevant_indices] - relevant_supports <- supports[relevant_indices] - - # Compute confidence for each relevant itemset - probabilities <- sapply(seq_along(relevant_itemsets), function(idx) { - itemset <- relevant_itemsets[[idx]] - itemset_without_item <- setdiff(itemset, item) - - # Find support values using indices - support_full <- relevant_supports[idx] - support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] +} - if (length(support_without) > 0) { - return(support_full / support_without[1]) - } else { - return(NA) - } - }, USE.NAMES = FALSE) - # Aggregate probabilities (using mean) - prob_estimate <- ifelse(length(na.omit(probabilities)) > 0, mean(na.omit(probabilities)), NA) - # Replace NA with probability estimate - new_data[i, item] <- prob_estimate - } - } +extract_predictions(results) - return(new_data) -} diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 1beae93..c054f90 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -167,8 +167,8 @@ make_predictions <- function(x, prefix, n_clusters) { row_data <- new_data[i, ] data.frame( item = items, - .prob = as.vector(as.matrix(row_data)), - pred = is.na(as.vector(as.matrix(row_data))) + .obs_item = as.vector(as.matrix(row_data)), + .pred_item = rep(NA, length(row_data)) ) }) @@ -209,7 +209,7 @@ make_predictions <- function(x, prefix, n_clusters) { # Apply cutoff to each dataframe in result_list result_list <- lapply(result_list, function(df) { row_data <- new_data[i, ] - df$.prob <- ifelse(df$pred & as.vector(as.matrix(row_data)) >= 0.5, 1, 0) + df$.pred_item <- ifelse(is.na(df$.obs_item), ifelse(as.vector(as.matrix(row_data)) >= 0.5, 1, 0), NA) df }) diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 2908bc4..85067a9 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,6 +7,11 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ +\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} + +\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of +prototypes of the same columns as \code{x}.} + \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/man/extract_predictions.Rd b/man/extract_predictions.Rd new file mode 100644 index 0000000..62749c8 --- /dev/null +++ b/man/extract_predictions.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_predictions.R +\name{extract_predictions} +\alias{extract_predictions} +\title{Returns recommender predictions with predicted values imputed into dataset +Notes: currently imputes raw probabilities} +\usage{ +extract_predictions(pred_output) +} +\description{ +Returns recommender predictions with predicted values imputed into dataset +Notes: currently imputes raw probabilities +} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 46cc6ce..b32f7d7 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -229,4 +229,11 @@ results <- fi_fit %>% predict(new_data) results[[1,1]] + + + + + +results %>% + extract_predictions ``` From d81fc525c7c2c0e8c422f2e0e8bd0d5ca46d6658 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 13 Mar 2025 12:41:15 -0700 Subject: [PATCH 28/71] something> --- R/extract_predictions.R | 5 ----- man/dot-k_means_fit_clustMixType.Rd | 5 ----- man/extract_predictions.Rd | 4 ++-- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/R/extract_predictions.R b/R/extract_predictions.R index bc78f08..795ed9b 100644 --- a/R/extract_predictions.R +++ b/R/extract_predictions.R @@ -7,7 +7,6 @@ extract_predictions <- function(pred_output) { purrr::map(~ { print(.x) .x %>% - dplyr::rowwise() %>% dplyr::mutate(value = ifelse(!is.na(.obs_item), .obs_item, .pred_item)) %>% dplyr::select(item, value) %>% tidyr::pivot_wider(names_from = item, values_from = value) @@ -16,7 +15,3 @@ extract_predictions <- function(pred_output) { } - - -extract_predictions(results) - diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 85067a9..2908bc4 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,11 +7,6 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ -\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} - -\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of -prototypes of the same columns as \code{x}.} - \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/man/extract_predictions.Rd b/man/extract_predictions.Rd index 62749c8..b265ca1 100644 --- a/man/extract_predictions.Rd +++ b/man/extract_predictions.Rd @@ -3,11 +3,11 @@ \name{extract_predictions} \alias{extract_predictions} \title{Returns recommender predictions with predicted values imputed into dataset -Notes: currently imputes raw probabilities} +Notes: currently imputes thresholded probabilities} \usage{ extract_predictions(pred_output) } \description{ Returns recommender predictions with predicted values imputed into dataset -Notes: currently imputes raw probabilities +Notes: currently imputes thresholded probabilities } From cfb5d74b86206650703b86b6ca38872d7cb464c6 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 19 Mar 2025 13:40:46 -0700 Subject: [PATCH 29/71] extract_predictions complete! (still needs a better name) --- R/extract_predictions.R | 26 +++++++++++++++++++------- man/dot-k_means_fit_clustMixType.Rd | 5 +++++ man/extract_predictions.Rd | 12 ++++++++---- vignettes/articles/freq_itemsets.Rmd | 11 ++++++++--- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/R/extract_predictions.R b/R/extract_predictions.R index 795ed9b..9536ffb 100644 --- a/R/extract_predictions.R +++ b/R/extract_predictions.R @@ -1,17 +1,29 @@ +#' Extract Predictions from Observation Data Frames +#' +#' This function processes a data frame containing observation data frames and extracts non-NA values. +#' +#' @param pred_output A data frame with one column, where each cell contains a data frame. +#' @return A data frame with items as columns and non-NA values as rows. +#' @export #' Returns recommender predictions with predicted values imputed into dataset #' Notes: currently imputes thresholded probabilities extract_predictions <- function(pred_output) { + # Extract the list of data frames from the single column + data_frames <- pred_output$.pred_cluster - result_df <- pred_output %>% - purrr::map(~ { - print(.x) - .x %>% + # Process each observation and combine results using reduce + result_df <- data_frames %>% + purrr::reduce(.f = ~ { + # Process each observation (data frame) + processed <- .y %>% dplyr::mutate(value = ifelse(!is.na(.obs_item), .obs_item, .pred_item)) %>% dplyr::select(item, value) %>% tidyr::pivot_wider(names_from = item, values_from = value) - }) %>% - purrr::list_rbind() # Combine the list of data frames into a single data frame -} + # Combine the processed data frame with the accumulated results + dplyr::bind_rows(.x, processed) + }, .init = NULL) + return(result_df) +} diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 2908bc4..85067a9 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,6 +7,11 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ +\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} + +\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of +prototypes of the same columns as \code{x}.} + \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/man/extract_predictions.Rd b/man/extract_predictions.Rd index b265ca1..c6da6b7 100644 --- a/man/extract_predictions.Rd +++ b/man/extract_predictions.Rd @@ -2,12 +2,16 @@ % Please edit documentation in R/extract_predictions.R \name{extract_predictions} \alias{extract_predictions} -\title{Returns recommender predictions with predicted values imputed into dataset -Notes: currently imputes thresholded probabilities} +\title{Extract Predictions from Observation Data Frames} \usage{ extract_predictions(pred_output) } +\arguments{ +\item{pred_output}{A data frame with one column, where each cell contains a data frame.} +} +\value{ +A data frame with items as columns and non-NA values as rows. +} \description{ -Returns recommender predictions with predicted values imputed into dataset -Notes: currently imputes thresholded probabilities +This function processes a data frame containing observation data frames and extracts non-NA values. } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index b32f7d7..00d6c9c 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -220,6 +220,9 @@ The function fills in missing values with these probability estimates, effectively "predicting" the likelihood of item presence based on historical co-occurrence patterns. +We display the predicted values in the column `.pred_item`, and the observed +values in the column `.obs_item`. + ```{r} new_data <- groceries[1:5,] %>% dplyr::mutate(`whole milk` = as.numeric(NA), @@ -229,11 +232,13 @@ results <- fi_fit %>% predict(new_data) results[[1,1]] +``` +Additionally, we can extract the nested predicted output to be formatted in a +single data frame, filling in the `NA` values with their predicted value using +`extract_predictions`. - - - +```{r} results %>% extract_predictions ``` From 5fc3153535bde0d8a0f6ef95296f6c00988f8bc9 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Mon, 24 Mar 2025 16:15:26 -0700 Subject: [PATCH 30/71] move detail text --- R/extract_predictions.R | 5 +++-- man/extract_predictions.Rd | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/R/extract_predictions.R b/R/extract_predictions.R index 9536ffb..1b57415 100644 --- a/R/extract_predictions.R +++ b/R/extract_predictions.R @@ -2,11 +2,12 @@ #' #' This function processes a data frame containing observation data frames and extracts non-NA values. #' +#' Returns recommender predictions with predicted values imputed into dataset +#' Notes: currently imputes thresholded probabilities +#' #' @param pred_output A data frame with one column, where each cell contains a data frame. #' @return A data frame with items as columns and non-NA values as rows. #' @export -#' Returns recommender predictions with predicted values imputed into dataset -#' Notes: currently imputes thresholded probabilities extract_predictions <- function(pred_output) { # Extract the list of data frames from the single column diff --git a/man/extract_predictions.Rd b/man/extract_predictions.Rd index c6da6b7..f981113 100644 --- a/man/extract_predictions.Rd +++ b/man/extract_predictions.Rd @@ -15,3 +15,7 @@ A data frame with items as columns and non-NA values as rows. \description{ This function processes a data frame containing observation data frames and extracts non-NA values. } +\details{ +Returns recommender predictions with predicted values imputed into dataset +Notes: currently imputes thresholded probabilities +} From a3abf08c7cb49f6df3cf832cd26705bab017c002 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Mon, 24 Mar 2025 16:15:40 -0700 Subject: [PATCH 31/71] add tuning for min_support --- NAMESPACE | 2 ++ R/dials-params.R | 19 +++++++++++++++++++ man/dot-k_means_fit_clustMixType.Rd | 5 ----- man/min_support.Rd | 24 ++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 man/min_support.Rd diff --git a/NAMESPACE b/NAMESPACE index f8d972b..14a8dac 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -96,6 +96,7 @@ export(extract_fit_engine) export(extract_fit_parsnip) export(extract_fit_summary) export(extract_parameter_set_dials) +export(extract_predictions) export(extract_preprocessor) export(extract_spec_parsnip) export(finalize_model_tidyclust) @@ -115,6 +116,7 @@ export(list_md_problems) export(load_pkgs) export(make_classes_tidyclust) export(min_grid) +export(min_support) export(new_cluster_metric) export(new_cluster_spec) export(predict.cluster_fit) diff --git a/R/dials-params.R b/R/dials-params.R index 0b569ed..99f148e 100644 --- a/R/dials-params.R +++ b/R/dials-params.R @@ -43,3 +43,22 @@ values_linkage_method <- c( "ward.D", "ward.D2", "single", "complete", "average", "mcquitty", "median", "centroid" ) + +#' Min Support +#' +#' Used in most `tidyclust::freq_itemsets()` models. +#' +#' @inheritParams dials::Laplace +#' @examples +#' min_support() +#' @export +min_support <- function(range = c(0.1, 0.5), trans = NULL) { + dials::new_quant_param( + type = "double", + range = range, + inclusive = c(TRUE, TRUE), + trans = trans, + label = c(min_support = "Minimum Support Value"), + finalize = NULL # Add to look at data and create a CI-like range around some min support value + ) +} diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 85067a9..2908bc4 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,11 +7,6 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ -\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} - -\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of -prototypes of the same columns as \code{x}.} - \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/man/min_support.Rd b/man/min_support.Rd new file mode 100644 index 0000000..8c4d0b3 --- /dev/null +++ b/man/min_support.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dials-params.R +\name{min_support} +\alias{min_support} +\title{Min Support} +\usage{ +min_support(range = c(0.1, 0.5), trans = NULL) +} +\arguments{ +\item{range}{A two-element vector holding the \emph{defaults} for the smallest and +largest possible values, respectively. If a transformation is specified, +these values should be in the \emph{transformed units}.} + +\item{trans}{A \code{trans} object from the \code{scales} package, such as +\code{scales::transform_log10()} or \code{scales::transform_reciprocal()}. If not provided, +the default is used which matches the units used in \code{range}. If no +transformation, \code{NULL}.} +} +\description{ +Used in most \code{tidyclust::freq_itemsets()} models. +} +\examples{ +min_support() +} From f2805b2f4307ed03b6f991da341a4bbb55c84691 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Apr 2025 10:41:01 -0700 Subject: [PATCH 32/71] tune update --- R/freq_itemsets_data.R | 2 +- R/tunable.R | 22 +++++++++++++++++++ vignettes/articles/freq_itemsets.Rmd | 33 ++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/R/freq_itemsets_data.R b/R/freq_itemsets_data.R index b62c920..47773e1 100644 --- a/R/freq_itemsets_data.R +++ b/R/freq_itemsets_data.R @@ -27,7 +27,7 @@ make_freq_itemsets <- function() { mode = "partition", value = list( interface = "matrix", - protect = c("data"), + protect = c("x, min_support, mining_method"), func = c(pkg = "tidyclust", fun = ".freq_itemsets_fit_arules"), defaults = list() ) diff --git a/R/tunable.R b/R/tunable.R index da49bd9..3be8b62 100644 --- a/R/tunable.R +++ b/R/tunable.R @@ -79,3 +79,25 @@ stats_k_means_engine_args <- component = "k_means", component_id = "engine" ) + +#' @export +tunable.freq_itemsets <- function(x, ...) { + res <- NextMethod() + if (x$engine == "arules") { + res <- add_engine_parameters(res, arules_freq_itemsets_engine_args) + } + res +} + +arules_freq_itemsets_engine_args <- + tibble::tibble( + name = c( + "support" + ), + call_info = list( + list(pkg = "tidyclust", fun = "min_support") + ), + source = "cluster_spec", + component = "freq_itemsets", + component_id = "engine" + ) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 00d6c9c..6b1773e 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -242,3 +242,36 @@ single data frame, filling in the `NA` values with their predicted value using results %>% extract_predictions ``` + + + + +```{r} +mvb_cvs <- rsample::vfold_cv(groceries, v = 10) + +rf_grid <- dials::grid_regular(min_support(), + levels = 10) + +rf_mod_tune <- freq_itemsets(min_support = tune(), + mining_method = "apriori") %>% + set_engine("arules") %>% + set_mode("partition") + +rf_recipe <- recipes::recipe(~ ., data = groceries) + +rf_wflow_tune <- workflow() %>% + add_model(rf_mod_tune) %>% + add_recipe(rf_recipe) + +rf_grid_search <- + tune::tune_grid( + rf_wflow_tune, + resamples = mvb_cvs, + grid = rf_grid, + metrics = yardstick::metric_set(accuracy, roc_auc, precision, recall, gain_capture) + ) +``` + + + + From a304ebdc09a031348d972bca0f333445d038fc5c Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 9 Apr 2025 15:00:41 -0700 Subject: [PATCH 33/71] improved example code --- vignettes/articles/freq_itemsets.Rmd | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 6b1773e..6d46935 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -150,10 +150,10 @@ To specify a frequent itemsets mining model in `tidyclust`, simply choose a value of `min_support` and (optionally) a mining method: ```{r} -fi_spec <- freq_itemsets( - min_support = 0.05, - mining_method = "apriori" -) +fi_spec <- freq_itemsets(min_support = 0.05, + mining_method = "apriori") %>% + set_engine("arules") %>% + set_mode("partition") fi_spec ``` @@ -267,8 +267,7 @@ rf_grid_search <- tune::tune_grid( rf_wflow_tune, resamples = mvb_cvs, - grid = rf_grid, - metrics = yardstick::metric_set(accuracy, roc_auc, precision, recall, gain_capture) + grid = rf_grid ) ``` From 0fbcdb75617eca5b1c990da1b3a7bd0f83e98ec5 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 9 Apr 2025 15:03:06 -0700 Subject: [PATCH 34/71] fix params help doc --- R/dials-params.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/dials-params.R b/R/dials-params.R index 99f148e..b2ca2e1 100644 --- a/R/dials-params.R +++ b/R/dials-params.R @@ -46,7 +46,7 @@ values_linkage_method <- c( #' Min Support #' -#' Used in most `tidyclust::freq_itemsets()` models. +#' Used in all `tidyclust::freq_itemsets()` models. #' #' @inheritParams dials::Laplace #' @examples From d23f35312bf6cde8df7a2e518c7d76b106fb14c4 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 10 Apr 2025 00:21:58 -0700 Subject: [PATCH 35/71] create test files (TODO: create tests) --- tests/testthat/_snaps/freq_itemsets.md | 85 ++++++++++ tests/testthat/test-freq_itemsets-arules.R | 82 ++++++++++ tests/testthat/test-freq_itemsets.R | 181 +++++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 tests/testthat/_snaps/freq_itemsets.md create mode 100644 tests/testthat/test-freq_itemsets-arules.R create mode 100644 tests/testthat/test-freq_itemsets.R diff --git a/tests/testthat/_snaps/freq_itemsets.md b/tests/testthat/_snaps/freq_itemsets.md new file mode 100644 index 0000000..fd1dbd6 --- /dev/null +++ b/tests/testthat/_snaps/freq_itemsets.md @@ -0,0 +1,85 @@ +# bad input + + Code + k_means(mode = "bogus") + Condition + Error in `modelenv::check_spec_mode_engine_val()`: + ! 'bogus' is not a known mode for model `k_means()`. + +--- + + Code + bt <- k_means(num_clusters = -1) %>% set_engine("stats") + fit(bt, mpg ~ ., mtcars) + Condition + Error in `check_args()`: + ! The number of centers should be >= 0. + +--- + + Code + translate_tidyclust(k_means(), engine = NULL) + Condition + Error in `translate_tidyclust.default()`: + ! Please set an engine. + +--- + + Code + translate_tidyclust(k_means(formula = ~x)) + Condition + Error in `k_means()`: + ! unused argument (formula = ~x) + +# printing + + Code + k_means() + Output + K Means Cluster Specification (partition) + + Computational engine: stats + + +--- + + Code + k_means(num_clusters = 10) + Output + K Means Cluster Specification (partition) + + Main Arguments: + num_clusters = 10 + + Computational engine: stats + + +# updating + + Code + k_means(num_clusters = 5) %>% update(num_clusters = tune()) + Output + K Means Cluster Specification (partition) + + Main Arguments: + num_clusters = tune() + + Computational engine: stats + + +# errors if `num_clust` isn't specified + + Code + k_means() %>% set_engine("stats") %>% fit(~., data = mtcars) + Condition + Error in `fit()`: + ! Please specify `num_clust` to be able to fit specification. + +--- + + Code + k_means() %>% set_engine("ClusterR") %>% fit(~., data = mtcars) + Condition + Error in `tidyclust::.k_means_fit_ClusterR()`: + ! argument "clusters" is missing, with no default + diff --git a/tests/testthat/test-freq_itemsets-arules.R b/tests/testthat/test-freq_itemsets-arules.R new file mode 100644 index 0000000..07c1fa8 --- /dev/null +++ b/tests/testthat/test-freq_itemsets-arules.R @@ -0,0 +1,82 @@ +test_that("fitting", { + set.seed(1234) + spec <- k_means(num_clusters = 5) %>% + set_engine("stats") + + expect_no_error( + res <- fit(spec, ~., mtcars) + ) + + expect_no_error( + res <- fit_xy(spec, mtcars) + ) +}) + +test_that("predicting", { + set.seed(1234) + spec <- k_means(num_clusters = 3) %>% + set_engine("stats") + + res <- fit(spec, ~., iris) + + preds <- predict(res, iris[c(25, 75, 125), ]) + + expect_identical( + preds, + tibble::tibble(.pred_cluster = factor(paste0("Cluster_", 1:3))) + ) +}) + +test_that("all levels are preserved with 1 row predictions", { + set.seed(1234) + spec <- k_means(num_clusters = 3) %>% + set_engine("stats") + + res <- fit(spec, ~., mtcars) + + preds <- predict(res, mtcars[1, ]) + + expect_identical( + levels(preds$.pred_cluster), + paste0("Cluster_", 1:3) + ) +}) + +test_that("extract_centroids() works", { + set.seed(1234) + spec <- k_means(num_clusters = 3) %>% + set_engine("stats") + + res <- fit(spec, ~., iris) + + centroids <- extract_centroids(res) + + expected <- vctrs::vec_cbind( + tibble::tibble(.cluster = factor(paste0("Cluster_", 1:3))), + tibble::as_tibble(res$fit$centers) + ) + + expect_identical( + centroids, + expected + ) +}) + +test_that("extract_cluster_assignment() works", { + set.seed(1234) + spec <- k_means(num_clusters = 3) %>% + set_engine("stats") + + res <- fit(spec, ~., iris) + + clusters <- extract_cluster_assignment(res) + + expected <- vctrs::vec_cbind( + tibble::tibble(.cluster = factor(paste0("Cluster_", res$fit$cluster))) + ) + + expect_identical( + clusters, + expected + ) +}) diff --git a/tests/testthat/test-freq_itemsets.R b/tests/testthat/test-freq_itemsets.R new file mode 100644 index 0000000..bb682b5 --- /dev/null +++ b/tests/testthat/test-freq_itemsets.R @@ -0,0 +1,181 @@ +test_that("primary arguments", { + basic <- freq_itemsets(mode = "partition") + basic_stats <- translate_tidyclust(basic %>% set_engine("arules")) + expect_equal( + basic_stats$method$fit$args, + list( + min_support = rlang::expr(missing_arg()), + mining_method = rlang::expr(missing_arg()) + ) + ) + + k <- k_means(num_clusters = 15, mode = "partition") + k_stats <- translate_tidyclust(k %>% set_engine("stats")) + expect_equal( + k_stats$method$fit$args, + list( + x = rlang::expr(missing_arg()), + centers = rlang::expr(missing_arg()), + centers = new_empty_quosure(15) + ) + ) +}) + +test_that("engine arguments", { + stats_print <- k_means(mode = "partition") + expect_equal( + translate_tidyclust( + stats_print %>% + set_engine("stats", nstart = 1L) + )$method$fit$args, + list( + x = rlang::expr(missing_arg()), + centers = rlang::expr(missing_arg()), + nstart = new_empty_quosure(1L) + ) + ) +}) + +test_that("bad input", { + expect_snapshot(error = TRUE, k_means(mode = "bogus")) + expect_snapshot(error = TRUE, { + bt <- k_means(num_clusters = -1) %>% set_engine("stats") + fit(bt, mpg ~ ., mtcars) + }) + expect_snapshot(error = TRUE, translate_tidyclust(k_means(), engine = NULL)) + expect_snapshot(error = TRUE, translate_tidyclust(k_means(formula = ~x))) +}) + +test_that("predictions", { + set.seed(1234) + kmeans_fit <- k_means(num_clusters = 4) %>% + set_engine("stats") %>% + fit(~., mtcars) + + set.seed(1234) + ref_res <- kmeans(mtcars, 4) + + ref_predictions <- ref_res$centers %>% + flexclust::dist2(mtcars) %>% + apply(2, which.min) %>% + unname() + + relevel_preds <- function(x) { + factor(unname(x), unique(unname(x))) %>% as.numeric() + } + + expect_equal( + relevel_preds(ref_predictions), + predict(kmeans_fit, mtcars)$.pred_cluster %>% as.numeric() + ) + + expect_equal( + relevel_preds(ref_predictions), + extract_cluster_assignment(kmeans_fit)$.cluster %>% as.numeric() + ) +}) + +test_that("extract_centroids work", { + set.seed(1234) + kmeans_fit <- k_means(num_clusters = 4) %>% + set_engine("stats") %>% + fit(~., mtcars) + + set.seed(1234) + ref_res <- kmeans(mtcars, 4) + + ref_centroids <- dplyr::bind_cols( + .cluster = c(1L, 4L, 2L, 3L), + ref_res$centers + ) %>% + dplyr::arrange(.cluster) %>% + dplyr::select(-.cluster) + + expect_identical( + extract_centroids(kmeans_fit) %>% dplyr::select(-.cluster), + ref_centroids + ) +}) + +test_that("Right classes", { + expect_equal( + class(k_means()), + c("k_means", "cluster_spec", "unsupervised_spec") + ) +}) + +test_that("printing", { + expect_snapshot( + k_means() + ) + expect_snapshot( + k_means(num_clusters = 10) + ) +}) + +test_that("updating", { + expect_snapshot( + k_means(num_clusters = 5) %>% + update(num_clusters = tune()) + ) +}) + +test_that("Engine-specific arguments are passed to ClusterR models", { + spec <- k_means(num_clusters = 2) %>% + set_engine("ClusterR", fuzzy = FALSE) + + fit <- fit(spec, ~., data = mtcars) + expect_true(is.null(fit$fit$fuzzy_clusters)) + + spec <- k_means(num_clusters = 2) %>% + set_engine("ClusterR", fuzzy = TRUE) + + fit <- fit(spec, ~., data = mtcars) + expect_false(is.null(fit$fit$fuzzy_clusters)) +}) + +test_that("reordering is done correctly for stats k_means", { + set.seed(42) + + kmeans_fit <- k_means(num_clusters = 6) %>% + set_engine("stats") %>% + fit(~., data = mtcars) + + summ <- extract_fit_summary(kmeans_fit) + + expect_identical( + summ$n_members, + unname(as.integer(table(summ$cluster_assignments))) + ) +}) + +test_that("reordering is done correctly for ClusterR k_means", { + set.seed(42) + + kmeans_fit <- k_means(num_clusters = 6) %>% + set_engine("ClusterR") %>% + fit(~., data = mtcars) + + summ <- extract_fit_summary(kmeans_fit) + + expect_identical( + summ$n_members, + unname(as.integer(table(summ$cluster_assignments))) + ) +}) + +test_that("errors if `num_clust` isn't specified", { + expect_snapshot( + error = TRUE, + k_means() %>% + set_engine("stats") %>% + fit(~ ., data = mtcars) + ) + + expect_snapshot( + error = TRUE, + k_means() %>% + set_engine("ClusterR") %>% + fit(~ ., data = mtcars) + ) +}) From dcea789fe87794ed783a96ebc631a505fee44dea Mon Sep 17 00:00:00 2001 From: Wander03 Date: Fri, 11 Apr 2025 14:09:11 -0700 Subject: [PATCH 36/71] adding raw to predict --- R/freq_itemsets_data.R | 18 ++++++++++++++++++ R/predict_helpers.R | 20 +++++++++++++++++--- vignettes/articles/freq_itemsets.Rmd | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/R/freq_itemsets_data.R b/R/freq_itemsets_data.R index 47773e1..d56d145 100644 --- a/R/freq_itemsets_data.R +++ b/R/freq_itemsets_data.R @@ -79,4 +79,22 @@ make_freq_itemsets <- function() { ) ) ) + + # May want to change to pre and post instead of direct function + modelenv::set_pred( + model = "freq_itemsets", + eng = "arules", + mode = "partition", + type = "raw", + value = list( + pre = NULL, + post = NULL, + func = c(fun = ".freq_itemsets_predict_raw_arules"), + args = + list( + object = rlang::expr(object$fit), + new_data = rlang::expr(new_data) + ) + ) + ) } diff --git a/R/predict_helpers.R b/R/predict_helpers.R index c054f90..e8269b5 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -153,7 +153,7 @@ make_predictions <- function(x, prefix, n_clusters) { pred_clusts } -.freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { +itemset_predictions <- function(object, new_data, ..., prefix = "Cluster_") { new_data <- as.data.frame(new_data) # Extract frequent itemsets and their supports @@ -206,12 +206,26 @@ make_predictions <- function(x, prefix, n_clusters) { } } - # Apply cutoff to each dataframe in result_list result_list <- lapply(result_list, function(df) { row_data <- new_data[i, ] - df$.pred_item <- ifelse(is.na(df$.obs_item), ifelse(as.vector(as.matrix(row_data)) >= 0.5, 1, 0), NA) + df$.pred_item <- ifelse(is.na(df$.obs_item), as.vector(as.matrix(row_data)), NA) df }) return(result_list) } + +.freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { + raw_pred <- itemset_predictions(object, new_data, ..., prefix = "Cluster_") + + # Apply cutoff to each dataframe in result_list + lapply(raw_pred, function(df) { + row_data <- new_data[i, ] + df$.pred_item <- ifelse(is.na(df$.obs_item), ifelse(as.vector(as.matrix(row_data)) >= 0.5, 1, 0), NA) + df + }) +} + +.freq_itemsets_predict_raw_arules <- function(object, new_data, ..., prefix = "Cluster_") { + itemset_predictions(object, new_data, ..., prefix = "Cluster_") +} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 6d46935..7c857d5 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -229,7 +229,7 @@ new_data <- groceries[1:5,] %>% `other vegetables` = as.numeric(1)) results <- fi_fit %>% - predict(new_data) + predict(new_data, type="raw") results[[1,1]] ``` From b22bceec71d7e6abb4a407d6c86d889fd4e3eab1 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Fri, 11 Apr 2025 18:38:02 -0700 Subject: [PATCH 37/71] split regualar predict and raw predict. NOTE: For the normal predict it is what we want, nested data frames in a data frame. However, the structure of the raw output is a list with data frames). --- R/predict_helpers.R | 112 ++++++++++++++------------- vignettes/articles/freq_itemsets.Rmd | 32 ++++---- 2 files changed, 73 insertions(+), 71 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index e8269b5..32742b8 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -153,7 +153,7 @@ make_predictions <- function(x, prefix, n_clusters) { pred_clusts } -itemset_predictions <- function(object, new_data, ..., prefix = "Cluster_") { +.freq_itemsets_predict_raw_arules <- function(object, new_data, ..., prefix = "Cluster_") { new_data <- as.data.frame(new_data) # Extract frequent itemsets and their supports @@ -162,70 +162,74 @@ itemset_predictions <- function(object, new_data, ..., prefix = "Cluster_") { frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) supports <- itemsets$support - # Transform the output dataframe format (make wide) + # Calculate global support for each item (fallback) + global_supports <- sapply(items, function(item) { + containing <- sapply(frequent_itemsets, function(x) item %in% x) + if (any(containing)) { + sum(supports[containing]) / sum(containing) + } else { + 0 + } + }) + + # Process each row of new_data result_list <- lapply(1:nrow(new_data), function(i) { row_data <- new_data[i, ] - data.frame( - item = items, - .obs_item = as.vector(as.matrix(row_data)), - .pred_item = rep(NA, length(row_data)) - ) - }) + observed <- names(row_data)[row_data == 1] + missing <- names(row_data)[is.na(row_data)] + + # Initialize prediction vector + pred_values <- rep(NA, length(items)) + names(pred_values) <- items + + # Calculate probabilities for missing items + for (item in missing) { + # Find itemsets containing both the current item and at least one observed item + relevant <- sapply(frequent_itemsets, function(x) { + item %in% x && any(observed %in% x) + }) + + if (!any(relevant)) { + pred_values[item] <- global_supports[item] + next + } + + # Calculate confidences + confidences <- sapply(which(relevant), function(idx) { + itemset <- frequent_itemsets[[idx]] + itemset_without <- setdiff(itemset, item) + + # Find support of itemset without the current item + if (length(itemset_without) == 0) return(NA) + + matches <- sapply(frequent_itemsets, function(x) identical(x, itemset_without)) + if (!any(matches)) return(NA) - for (i in seq_len(nrow(new_data))) { - observed_items <- colnames(new_data)[which(new_data[i, ] == 1)] - missing_items <- colnames(new_data)[which(is.na(new_data[i, ]))] - - for (item in missing_items) { - # Find relevant itemsets and supports - relevant_indices <- which(sapply(frequent_itemsets, function(x) item %in% x && any(observed_items %in% x))) - relevant_itemsets <- frequent_itemsets[relevant_indices] - relevant_supports <- supports[relevant_indices] - - # Compute confidence for each relevant itemset - probabilities <- sapply(seq_along(relevant_itemsets), function(idx) { - itemset <- relevant_itemsets[[idx]] - itemset_without_item <- setdiff(itemset, item) - - # Find support values using indices - support_full <- relevant_supports[idx] - support_without <- supports[which(sapply(frequent_itemsets, function(x) identical(x, itemset_without_item)))] - - if (length(support_without) > 0) { - return(support_full / support_without[1]) - } else { - return(NA) - } - }, USE.NAMES = FALSE) - - # Aggregate probabilities (using mean) - prob_estimate <- ifelse(length(na.omit(probabilities)) > 0, mean(na.omit(probabilities)), NA) - - # Replace NA with probability estimate - new_data[i, item] <- prob_estimate + supports[idx] / supports[matches][1] + }) + + pred_values[item] <- mean(confidences, na.rm = TRUE) + if (is.nan(pred_values[item])) pred_values[item] <- global_supports[item] } - } - result_list <- lapply(result_list, function(df) { - row_data <- new_data[i, ] - df$.pred_item <- ifelse(is.na(df$.obs_item), as.vector(as.matrix(row_data)), NA) - df + # Create result data frame + data.frame( + item = items, + .obs_item = unlist(row_data), + .pred_item = pred_values + ) }) return(result_list) } .freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { - raw_pred <- itemset_predictions(object, new_data, ..., prefix = "Cluster_") - - # Apply cutoff to each dataframe in result_list - lapply(raw_pred, function(df) { - row_data <- new_data[i, ] - df$.pred_item <- ifelse(is.na(df$.obs_item), ifelse(as.vector(as.matrix(row_data)) >= 0.5, 1, 0), NA) + raw_predictions <- .freq_itemsets_predict_raw_arules(object, new_data, ..., prefix = "Cluster_") + # Apply threshold to raw predictions + lapply(raw_predictions, function(df) { + df$.pred_item <- ifelse(is.na(df$.obs_item), + ifelse(df$.pred_item >= 0.5, 1, 0), + NA) df }) } - -.freq_itemsets_predict_raw_arules <- function(object, new_data, ..., prefix = "Cluster_") { - itemset_predictions(object, new_data, ..., prefix = "Cluster_") -} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 7c857d5..0254eff 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -226,10 +226,12 @@ values in the column `.obs_item`. ```{r} new_data <- groceries[1:5,] %>% dplyr::mutate(`whole milk` = as.numeric(NA), + frankfurter = as.numeric(NA), + yogurt = as.numeric(NA), `other vegetables` = as.numeric(1)) results <- fi_fit %>% - predict(new_data, type="raw") + predict(new_data) results[[1,1]] ``` @@ -247,28 +249,24 @@ results %>% ```{r} -mvb_cvs <- rsample::vfold_cv(groceries, v = 10) -rf_grid <- dials::grid_regular(min_support(), + +dials::grid_regular(min_support(), levels = 10) -rf_mod_tune <- freq_itemsets(min_support = tune(), - mining_method = "apriori") %>% - set_engine("arules") %>% - set_mode("partition") +new_data <- groceries[1:5,] %>% + dplyr::mutate(`whole milk` = as.numeric(NA), + frankfurter = as.numeric(NA), + yogurt = as.numeric(NA), + `other vegetables` = as.numeric(1)) -rf_recipe <- recipes::recipe(~ ., data = groceries) +raw <- fi_fit %>% + predict(new_data, type = "raw") + +threshold <- fi_fit %>% + predict(new_data) -rf_wflow_tune <- workflow() %>% - add_model(rf_mod_tune) %>% - add_recipe(rf_recipe) -rf_grid_search <- - tune::tune_grid( - rf_wflow_tune, - resamples = mvb_cvs, - grid = rf_grid - ) ``` From be2f3a90ea908bf1168a2266b9ba06a4b6a79da6 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Mon, 14 Apr 2025 15:06:24 -0700 Subject: [PATCH 38/71] predict fixed! Output is the same :D --- R/predict_helpers.R | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 32742b8..1862f76 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -153,7 +153,7 @@ make_predictions <- function(x, prefix, n_clusters) { pred_clusts } -.freq_itemsets_predict_raw_arules <- function(object, new_data, ..., prefix = "Cluster_") { +itemsets_predict_helper <- function(object, new_data, ..., prefix = "Cluster_") { new_data <- as.data.frame(new_data) # Extract frequent itemsets and their supports @@ -219,14 +219,17 @@ make_predictions <- function(x, prefix, n_clusters) { .pred_item = pred_values ) }) +} - return(result_list) +.freq_itemsets_predict_raw_arules <- function(object, new_data, ..., prefix = "Cluster_") { + res <- itemsets_predict_helper(object, new_data, ..., prefix = "Cluster_") + return(tibble::tibble(.pred_items = unname(res))) } .freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { - raw_predictions <- .freq_itemsets_predict_raw_arules(object, new_data, ..., prefix = "Cluster_") + res <- itemsets_predict_helper(object, new_data, ..., prefix = "Cluster_") # Apply threshold to raw predictions - lapply(raw_predictions, function(df) { + lapply(res, function(df) { df$.pred_item <- ifelse(is.na(df$.obs_item), ifelse(df$.pred_item >= 0.5, 1, 0), NA) From 3ab136231221ba77f7fbf7d5f0c0512fc57f33ec Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 22 Apr 2025 20:46:29 -0700 Subject: [PATCH 39/71] change default to eclat --- R/freq_itemsets.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 24f33f3..7a399fe 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -11,12 +11,12 @@ #' - \link[=details_freq_itemsets_arules]{arules} #' #' @param mode A single character string for the type of model. The only -#' possible value for this model is "association". +#' possible value for this model is "partition". #' @param engine A single character string specifying the computational engine #' to use for fitting. The default for this model is `"arules"`. #' @param mining_method A single character string specifying the algorithm to use for #' fitting. Possible algorithms are `"apriori"` and `"eclat"`. The default for -#' this model is `"apriori"`. +#' this model is `"eclat"`. #' @param min_support Positive double, minimum support for an itemset (between 0 and 1). #' #' @details @@ -35,7 +35,7 @@ #' @export freq_itemsets <- function(mode = "partition", # will add other modes - engine = "arules", + engine = "eclat", min_support = NULL, mining_method = "apriori") { args <- list( From a5c8edd7466964e08d921530589d3d7db8e51c24 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 22 Apr 2025 20:48:59 -0700 Subject: [PATCH 40/71] fixing replacing wrong part from earlier commit --- NAMESPACE | 1 + R/freq_itemsets.R | 4 ++-- man/dot-k_means_fit_clustMixType.Rd | 5 +++++ man/freq_itemsets.Rd | 6 +++--- man/min_support.Rd | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 14a8dac..81abf33 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -68,6 +68,7 @@ S3method(translate_tidyclust,freq_itemsets) S3method(translate_tidyclust,hier_clust) S3method(translate_tidyclust,k_means) S3method(tunable,cluster_spec) +S3method(tunable,freq_itemsets) S3method(tunable,k_means) S3method(tune_args,cluster_spec) S3method(tune_cluster,cluster_spec) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 7a399fe..04d9e92 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -35,9 +35,9 @@ #' @export freq_itemsets <- function(mode = "partition", # will add other modes - engine = "eclat", + engine = "arules", min_support = NULL, - mining_method = "apriori") { + mining_method = "eclat") { args <- list( min_support = enquo(min_support), mining_method = enquo(mining_method) diff --git a/man/dot-k_means_fit_clustMixType.Rd b/man/dot-k_means_fit_clustMixType.Rd index 2908bc4..85067a9 100644 --- a/man/dot-k_means_fit_clustMixType.Rd +++ b/man/dot-k_means_fit_clustMixType.Rd @@ -7,6 +7,11 @@ .k_means_fit_clustMixType(x, k, ...) } \arguments{ +\item{x}{Data frame with both numerics and factors (also ordered factors are possible).} + +\item{k}{Either the number of clusters, a vector specifying indices of initial prototypes, or a data frame of +prototypes of the same columns as \code{x}.} + \item{...}{Other arguments passed to \code{clustMixType::kproto()}} } \value{ diff --git a/man/freq_itemsets.Rd b/man/freq_itemsets.Rd index 67d80c9..cb95142 100644 --- a/man/freq_itemsets.Rd +++ b/man/freq_itemsets.Rd @@ -8,12 +8,12 @@ freq_itemsets( mode = "partition", engine = "arules", min_support = NULL, - mining_method = "apriori" + mining_method = "eclat" ) } \arguments{ \item{mode}{A single character string for the type of model. The only -possible value for this model is "association".} +possible value for this model is "partition".} \item{engine}{A single character string specifying the computational engine to use for fitting. The default for this model is \code{"arules"}.} @@ -22,7 +22,7 @@ to use for fitting. The default for this model is \code{"arules"}.} \item{mining_method}{A single character string specifying the algorithm to use for fitting. Possible algorithms are \code{"apriori"} and \code{"eclat"}. The default for -this model is \code{"apriori"}.} +this model is \code{"eclat"}.} } \value{ A \code{freq_itemsets} association specification. diff --git a/man/min_support.Rd b/man/min_support.Rd index 8c4d0b3..6d1bf9b 100644 --- a/man/min_support.Rd +++ b/man/min_support.Rd @@ -17,7 +17,7 @@ the default is used which matches the units used in \code{range}. If no transformation, \code{NULL}.} } \description{ -Used in most \code{tidyclust::freq_itemsets()} models. +Used in all \code{tidyclust::freq_itemsets()} models. } \examples{ min_support() From 022c984fc9ac080259bb8b1b930d382e19c14a6d Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 22 Apr 2025 20:50:41 -0700 Subject: [PATCH 41/71] updating with correct default --- vignettes/articles/freq_itemsets.Rmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 0254eff..0d7134c 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -151,7 +151,7 @@ choose a value of `min_support` and (optionally) a mining method: ```{r} fi_spec <- freq_itemsets(min_support = 0.05, - mining_method = "apriori") %>% + mining_method = "eclat") %>% set_engine("arules") %>% set_mode("partition") @@ -159,7 +159,7 @@ fi_spec ``` Currently, the only supported engine is `arules`. The default mining -method is apriori. +method is eclat. ## **Fitting `freq_itemsets` models** From a61eb1ef983074e1ba5e98bf47abb7bcbe46ea95 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 1 May 2025 19:04:55 -0700 Subject: [PATCH 42/71] augment code written, add correct header text and move unecessary code --- R/augment_itemset_predict.R | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 R/augment_itemset_predict.R diff --git a/R/augment_itemset_predict.R b/R/augment_itemset_predict.R new file mode 100644 index 0000000..74cdbb9 --- /dev/null +++ b/R/augment_itemset_predict.R @@ -0,0 +1,78 @@ +#' Extract Predictions from Observation Data Frames +#' +#' This function processes a data frame containing observation data frames and extracts non-NA values. +#' +#' Returns recommender predictions with predicted values imputed into dataset +#' Notes: currently imputes thresholded probabilities +#' +#' @param pred_output A data frame with one column, where each cell contains a data frame. +#' @return A data frame with items as columns and non-NA values as rows. +#' @export + +augment_itemset_predict <- function(pred_output, truth_output) { + # Extract all predictions (bind all .pred_cluster dataframes) + preds_df <- dplyr::bind_rows(pred_output$.pred_cluster, .id = "row_id") %>% + dplyr::filter(!is.na(.pred_item)) %>% # Keep only rows with predictions + dplyr::mutate(item = stringr::str_remove_all(item, "`")) %>% # Remove backticks from item names + dplyr::select(row_id, item, preds = .pred_item) # Standardize column names + + # Pivot truth data to long format (to match predictions) + truth_long <- truth_output %>% + tibble::rownames_to_column("row_id") %>% + tidyr::pivot_longer( + cols = -row_id, + names_to = "item", + values_to = "truth_value" + ) + + # Join predictions with truth (inner join to keep only predicted items) + result <- preds_df %>% + dplyr::inner_join(truth_long, by = c("row_id", "item")) + + # Return simplified output (preds vs truth) + dplyr::select(result, item, row_id, preds, truth = truth_value) +} + + + + + +random_na_with_truth <- function(df, na_prob = 0.3) { + # Create a copy of the original dataframe to store truth values + truth_df <- df + + # Create a mask of NAs (TRUE = becomes NA) + na_mask <- matrix( + sample( + c(TRUE, FALSE), + size = nrow(df) * ncol(df), + replace = TRUE, + prob = c(na_prob, 1 - na_prob) + ), + nrow = nrow(df) + ) + + # Apply the mask to create NA values + na_df <- df + na_df[na_mask] <- NA + + # Return both the NA-filled dataframe and the truth + list( + na_data = na_df, + truth = truth_df + ) +} + + +set.seed(123) +na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) +pred_output <- predict(fi_fit, na_result$na_data) +comparison_df <- augment_itemset_predict(pred_output, na_result$truth) + +comparison_df %>% + dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% + yardstick::precision(truth, preds) + + + + From ffaa38001ccb705083d0ec0601d032f3eba85365 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sat, 3 May 2025 17:13:25 -0700 Subject: [PATCH 43/71] standardize column names --- R/predict_helpers.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 1862f76..8e0e611 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -223,14 +223,14 @@ itemsets_predict_helper <- function(object, new_data, ..., prefix = "Cluster_") .freq_itemsets_predict_raw_arules <- function(object, new_data, ..., prefix = "Cluster_") { res <- itemsets_predict_helper(object, new_data, ..., prefix = "Cluster_") - return(tibble::tibble(.pred_items = unname(res))) + return(tibble::tibble(.pred_cluster = unname(res))) } .freq_itemsets_predict_arules <- function(object, new_data, ..., prefix = "Cluster_") { res <- itemsets_predict_helper(object, new_data, ..., prefix = "Cluster_") # Apply threshold to raw predictions lapply(res, function(df) { - df$.pred_item <- ifelse(is.na(df$.obs_item), + df$.pred_cluster <- ifelse(is.na(df$.obs_item), ifelse(df$.pred_item >= 0.5, 1, 0), NA) df From 8d1805744deb57bd9bbe1f9c3ecb90e9909a5e2d Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sat, 3 May 2025 17:13:31 -0700 Subject: [PATCH 44/71] testing --- R/augment_itemset_predict.R | 20 ++++++++++++++- vignettes/articles/freq_itemsets.Rmd | 37 ++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/R/augment_itemset_predict.R b/R/augment_itemset_predict.R index 74cdbb9..b60ae9e 100644 --- a/R/augment_itemset_predict.R +++ b/R/augment_itemset_predict.R @@ -71,8 +71,26 @@ comparison_df <- augment_itemset_predict(pred_output, na_result$truth) comparison_df %>% dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% - yardstick::precision(truth, preds) + yardstick::accuracy(truth, preds) +#---------------------------------- +set.seed(123) +na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) +pred_output <- predict(fi_fit, na_result$na_data, type = 'raw') +comparison_df <- augment_itemset_predict(pred_output, na_result$truth) +comparison_df %>% + yardstick::rmse(truth, preds) + +#---------------------------------- + +set.seed(123) +na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) +pred_output <- predict(fi_fit, na_result$na_data) +comparison_df <- augment_itemset_predict(pred_output, na_result$truth) + +comparison_df %>% + dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% + yardstick::accuracy(truth, preds) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 0d7134c..0bcfd0a 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -250,9 +250,42 @@ results %>% ```{r} +recipe <- recipes::recipe(~ ., data = groceries) + +tune_spec <- freq_itemsets( + min_support = tune(), + mining_method = "eclat" + ) %>% + set_engine("arules") %>% + set_mode("partition") + +min_support_grid <- dials::grid_regular( + min_support(), + levels = 10 + ) + +tune_workflow <- workflow() %>% + add_recipe(recipe) %>% + add_model(tune_spec) + + +fit(tune_workflow, data = groceries) + + + +rf_grid_search <- + tune_cluster( + tune_workflow, + # resamples = groceries, + grid = min_support_grid + ) + + + + + + -dials::grid_regular(min_support(), - levels = 10) new_data <- groceries[1:5,] %>% dplyr::mutate(`whole milk` = as.numeric(NA), From f99b0aa3dc69d9f2e1762579a19f42e210f089cb Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sat, 3 May 2025 17:13:45 -0700 Subject: [PATCH 45/71] testing2 --- R/augment_itemset_predict.R | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/R/augment_itemset_predict.R b/R/augment_itemset_predict.R index b60ae9e..41727ed 100644 --- a/R/augment_itemset_predict.R +++ b/R/augment_itemset_predict.R @@ -81,7 +81,9 @@ pred_output <- predict(fi_fit, na_result$na_data, type = 'raw') comparison_df <- augment_itemset_predict(pred_output, na_result$truth) comparison_df %>% - yardstick::rmse(truth, preds) + dplyr::mutate(truth = factor(truth, levels=c(0, 1))) %>% + yardstick::pr_curve(truth, preds) %>% + autoplot() #---------------------------------- @@ -93,4 +95,4 @@ comparison_df <- augment_itemset_predict(pred_output, na_result$truth) comparison_df %>% dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% - yardstick::accuracy(truth, preds) + yardstick::roc_auc(truth, preds) From 0f9e94762834bdf35721ac52c655e81f3c149a81 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 6 May 2025 20:35:07 -0700 Subject: [PATCH 46/71] hide predict dataframe from arules::inspect() --- NAMESPACE | 1 + R/augment_itemset_predict.R | 64 +++++++++++++++++----------------- R/predict_helpers.R | 2 +- man/augment_itemset_predict.Rd | 21 +++++++++++ 4 files changed, 55 insertions(+), 33 deletions(-) create mode 100644 man/augment_itemset_predict.Rd diff --git a/NAMESPACE b/NAMESPACE index 81abf33..7cdec2e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -88,6 +88,7 @@ export(.k_means_fit_klaR) export(.k_means_fit_stats) export(assoc_rules) export(augment) +export(augment_itemset_predict) export(cluster_metric_set) export(control_cluster) export(cut_height) diff --git a/R/augment_itemset_predict.R b/R/augment_itemset_predict.R index 41727ed..6583f89 100644 --- a/R/augment_itemset_predict.R +++ b/R/augment_itemset_predict.R @@ -64,35 +64,35 @@ random_na_with_truth <- function(df, na_prob = 0.3) { } -set.seed(123) -na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) -pred_output <- predict(fi_fit, na_result$na_data) -comparison_df <- augment_itemset_predict(pred_output, na_result$truth) - -comparison_df %>% - dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% - yardstick::accuracy(truth, preds) - -#---------------------------------- - -set.seed(123) -na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) -pred_output <- predict(fi_fit, na_result$na_data, type = 'raw') -comparison_df <- augment_itemset_predict(pred_output, na_result$truth) - -comparison_df %>% - dplyr::mutate(truth = factor(truth, levels=c(0, 1))) %>% - yardstick::pr_curve(truth, preds) %>% - autoplot() - - -#---------------------------------- - -set.seed(123) -na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) -pred_output <- predict(fi_fit, na_result$na_data) -comparison_df <- augment_itemset_predict(pred_output, na_result$truth) - -comparison_df %>% - dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% - yardstick::roc_auc(truth, preds) +# set.seed(123) +# na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) +# pred_output <- predict(fi_fit, na_result$na_data) +# comparison_df <- augment_itemset_predict(pred_output, na_result$truth) +# +# comparison_df %>% +# dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% +# yardstick::accuracy(truth, preds) +# +# #---------------------------------- +# +# set.seed(123) +# na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) +# pred_output <- predict(fi_fit, na_result$na_data, type = 'raw') +# comparison_df <- augment_itemset_predict(pred_output, na_result$truth) +# +# comparison_df %>% +# dplyr::mutate(truth = factor(truth, levels=c(0, 1))) %>% +# yardstick::pr_curve(truth, preds) %>% +# autoplot() +# +# +# #---------------------------------- +# +# set.seed(123) +# na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) +# pred_output <- predict(fi_fit, na_result$na_data) +# comparison_df <- augment_itemset_predict(pred_output, na_result$truth) +# +# comparison_df %>% +# dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% +# yardstick::roc_auc(truth, preds) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 8e0e611..e36452e 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -158,7 +158,7 @@ itemsets_predict_helper <- function(object, new_data, ..., prefix = "Cluster_") # Extract frequent itemsets and their supports items <- attr(object, "item_names") - itemsets <- arules::inspect(object) + itemsets <- arules::DATAFRAME(object) frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) supports <- itemsets$support diff --git a/man/augment_itemset_predict.Rd b/man/augment_itemset_predict.Rd new file mode 100644 index 0000000..5e18f10 --- /dev/null +++ b/man/augment_itemset_predict.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/augment_itemset_predict.R +\name{augment_itemset_predict} +\alias{augment_itemset_predict} +\title{Extract Predictions from Observation Data Frames} +\usage{ +augment_itemset_predict(pred_output, truth_output) +} +\arguments{ +\item{pred_output}{A data frame with one column, where each cell contains a data frame.} +} +\value{ +A data frame with items as columns and non-NA values as rows. +} +\description{ +This function processes a data frame containing observation data frames and extracts non-NA values. +} +\details{ +Returns recommender predictions with predicted values imputed into dataset +Notes: currently imputes thresholded probabilities +} From e12ae6280171899f2e4621f9e3e814d9f2848043 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Tue, 6 May 2025 21:50:46 -0700 Subject: [PATCH 47/71] remove `` from predict output item names --- R/predict_helpers.R | 5 +++-- vignettes/articles/freq_itemsets.Rmd | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index e36452e..5f2383b 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -214,9 +214,10 @@ itemsets_predict_helper <- function(object, new_data, ..., prefix = "Cluster_") # Create result data frame data.frame( - item = items, + item = stringr::str_remove_all(items, "`"), # Remove backticks from item names .obs_item = unlist(row_data), - .pred_item = pred_values + .pred_item = pred_values, + row.names = NULL ) }) } diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 0bcfd0a..b7701b1 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -233,7 +233,7 @@ new_data <- groceries[1:5,] %>% results <- fi_fit %>% predict(new_data) -results[[1,1]] +results$.pred_cluster[[1]] ``` Additionally, we can extract the nested predicted output to be formatted in a From 3bc1e43c4d222737fed33910b2fd669365087e8b Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 8 May 2025 10:59:23 -0700 Subject: [PATCH 48/71] added note --- vignettes/articles/freq_itemsets.Rmd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index b7701b1..7514caf 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -183,6 +183,9 @@ We can also extract the standard `tidyclust` summary list: # # fi_summary %>% # str() + + +# Make an extract_fit_summary_items() that returns a warnding saying that centroids are not usfeul, suggust looking at itemsets (and say how to do this) ``` Note that, although the frequent itemset algorithm is not focused on cluster's From ace0ce4fc5b61d59f9999368836e5342a51e7031 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Mon, 12 May 2025 14:38:50 -0700 Subject: [PATCH 49/71] re-roder doesnt matter for fit --- R/extract_cluster_assignment.R | 91 +++++++++++++++++----------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index 9a7a44f..5645fb7 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -163,55 +163,56 @@ extract_cluster_assignment.itemsets <- function(object, ...) { itemsets <- arules::inspect(object) itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) - support <- itemsets$support clusters <- numeric(length(items)) - - cluster_supports <- list() - - sapply(1:length(items), function(i) { - current_item <- items[i] - - # Find relevant itemsets that contain the current itemset - relevant_itemsets <- which(sapply(itemset_list, function(x) current_item %in% x)) - - - if (length(relevant_itemsets) == 0) { - return(0) # No frequent itemsets, assign to own cluster - } - - # Find all items in relevant itemsets - all_items <- unique(unlist(itemset_list[relevant_itemsets])) - all_items_num <- match(all_items, items) - - # Find the size of each relevant itemset - itemset_sizes <- sapply(itemset_list[relevant_itemsets], length) - - # Largest itemset with highest support tiebreaker - best_itemset <- relevant_itemsets[which.max(itemset_sizes * max(support) + support[relevant_itemsets])] - - # Get the size and support of the best itemset - best_itemset_size <- length(itemset_list[[best_itemset]]) - best_itemset_support <- support[best_itemset] - - # Get the size and support of the current cluster (if any) - current_cluster_size <- if (clusters[i] != 0) length(itemset_list[[clusters[i]]]) else 0 - current_cluster_support <- if (clusters[i] != 0) support[clusters[i]] else 0 - - # Create Cluster from best_itemset if: - # 1. The item has no cluster, or - # 2. The best_itemset is larger than the current cluster, or - # 3. The best_itemset is the same size but has higher support - if (clusters[i] == 0 || - best_itemset_size > current_cluster_size || - (best_itemset_size == current_cluster_size && best_itemset_support > current_cluster_support)) { - for (x in all_items_num) { - clusters[x] <<- best_itemset + changed <- TRUE # Flag to track convergence + + # Continue until no changes occur + while (changed) { + changed <- FALSE + for (i in 1:length(items)) { + current_item <- items[i] + relevant_itemsets <- which(sapply(itemset_list, function(x) current_item %in% x)) + + if (length(relevant_itemsets) == 0) next # Skip if no itemsets + + # Find the best itemset (largest size, then highest support) + best_itemset <- relevant_itemsets[ + which.max( + sapply(itemset_list[relevant_itemsets], length) * 1000 + # Size dominates + support[relevant_itemsets] # Support breaks ties + ) + ] + best_itemset_size <- length(itemset_list[[best_itemset]]) + best_itemset_support <- support[best_itemset] + + # Current cluster info (if any) + current_cluster <- clusters[i] + current_cluster_size <- if (current_cluster != 0) + length(itemset_list[[current_cluster]]) else 0 + current_cluster_support <- if (current_cluster != 0) + support[current_cluster] else 0 + + # Reassign if: + # 1. No current cluster, OR + # 2. New itemset is larger, OR + # 3. Same size but higher support + if (current_cluster == 0 || + best_itemset_size > current_cluster_size || + (best_itemset_size == current_cluster_size && + best_itemset_support > current_cluster_support)) { + + # Assign all items in the best itemset to its cluster + new_cluster <- best_itemset + items_in_best <- match(itemset_list[[best_itemset]], items) + + if (!all(clusters[items_in_best] == new_cluster)) { + clusters[items_in_best] <- new_cluster + changed <- TRUE # Mark that a change occurred + } } } - # Update cluster average support - cluster_supports[[best_itemset]] <<- mean(support[relevant_itemsets]) - }) + } item_assignment_tibble_w_outliers(clusters, ...) } From 2ed1a982ad76b50fa37a6aefb33e5d4220983c02 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 14 May 2025 12:54:28 -0700 Subject: [PATCH 50/71] hide freq itemset output from auto displaying when extracting cluster assignment --- R/extract_cluster_assignment.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index 5645fb7..c2e883c 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -160,7 +160,7 @@ extract_cluster_assignment.hclust <- function(object, #' @export extract_cluster_assignment.itemsets <- function(object, ...) { items <- attr(object, "item_names") - itemsets <- arules::inspect(object) + itemsets <- arules::DATAFRAME(object) itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) support <- itemsets$support From 18566d2ec72ec14f838604a1c2cf7e2d38710846 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Wed, 14 May 2025 13:21:05 -0700 Subject: [PATCH 51/71] rename col name in predict --- R/predict_helpers.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 5f2383b..a61e52f 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -231,7 +231,7 @@ itemsets_predict_helper <- function(object, new_data, ..., prefix = "Cluster_") res <- itemsets_predict_helper(object, new_data, ..., prefix = "Cluster_") # Apply threshold to raw predictions lapply(res, function(df) { - df$.pred_cluster <- ifelse(is.na(df$.obs_item), + df$.pred_item <- ifelse(is.na(df$.obs_item), ifelse(df$.pred_item >= 0.5, 1, 0), NA) df From 431555916af4504b10207c862a0c4a97029781b2 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sun, 25 May 2025 15:15:38 -0700 Subject: [PATCH 52/71] vignettes update with new info from thesis --- vignettes/articles/freq_itemsets.Rmd | 151 ++++++++++++++++----------- 1 file changed, 91 insertions(+), 60 deletions(-) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 7514caf..1cf3ed8 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -44,7 +44,7 @@ groceries <- as.data.frame(as(Groceries, "matrix")) %>% ## A Brief Introduction to Frequent Itemset Mining -*Frequent Itemset Mining* is a fundamental technique in data mining that +*Frequent Itemset Mining* (FIM) is a fundamental technique in data mining that identifies sets of items that frequently appear together in transactional datasets. These itemsets are often used to uncover meaningful patterns, such as associations between items, which can then @@ -146,20 +146,24 @@ with very large tid-lists. ## **`freq_itemsets` specification in {tidyclust}** -To specify a frequent itemsets mining model in `tidyclust`, simply -choose a value of `min_support` and (optionally) a mining method: +To specify a frequent itemsets mining model in `tidyclust`, simply +choose a value of `min_support` and (optionally) a mining method: ```{r} -fi_spec <- freq_itemsets(min_support = 0.05, - mining_method = "eclat") %>% +fi_spec <- freq_itemsets( + min_support = 0.05, + mining_method = "eclat" + ) %>% set_engine("arules") %>% set_mode("partition") fi_spec ``` -Currently, the only supported engine is `arules`. The default mining -method is eclat. +Currently, the only supported engine is `arules`. The default mining +method is eclat because it is generally faster in most practical cases. A +default `min_support` value is not provided as it varies significantly depending +on the data characteristics. ## **Fitting `freq_itemsets` models** @@ -192,11 +196,19 @@ Note that, although the frequent itemset algorithm is not focused on cluster's like other unsupervised learning algorithms, we have created clusters based on the itemsets. For each item, we find all itemsets that include that item: -- Itemsets with the largest size are selected as the "dominate" itemset for the item +- Itemsets with the largest size are selected as the "dominate" itemset for the +item. If there is a tie in size, the itemset with the highest support is selected. +This prioritization aligns with findings that larger itemsets with higher +support exhibit greater predictive utility. -- If there is a tie in size, the itemset with the highest support is selected +- If an item has already been assigned a cluster, the algorithm compares the +current "best" itemset with the itemset under consideration and re-prioritizes +based on size and support. This process repeats until no items are reassigned to +a new cluster (convergence). -- If no itemsets include the item, it is assigned a special "outlier" cluster (Cluster_0_1, Cluster_0_2, etc.) +- Items that appear in no frequent itemsets are labeled as outliers +(Cluster_0_X) while items within frequent itemsets are assigned sequential +cluster IDs (Cluster_1, Cluster_2, etc.). ```{r} fi_fit %>% @@ -211,17 +223,20 @@ straightforward as in supervised learning. However, given a set of frequent itemsets from historical data, it is possible to estimate the likelihood that a missing item in new data is present based on observed co-occurring items. -The predict() function utilizes frequent itemsets and their support values to +The `predict()` function utilizes frequent itemsets and their support values to estimate probabilities for missing items in new transactions. For each row in -new_data, the function identifies observed items and missing items. It then +`new_data`, the function identifies observed items and missing items. It then searches for frequent itemsets that contain both the missing item and at least one observed item. Using the support values of these itemsets, it estimates the probability that the missing item is present based on the confidence of -association between observed and missing items. +association between observed and missing items. If no relevant itemsets are +found, the item's global support (frequency in training data) is used as a +fallback prediction. The function fills in missing values with these probability estimates, effectively "predicting" the likelihood of item presence based on historical -co-occurrence patterns. +co-occurrence patterns. The type argument allows returning raw prediction +probabilities ('raw') or binary predictions based on a 0.5 threshold ('cluster'). We display the predicted values in the column `.pred_item`, and the observed values in the column `.obs_item`. @@ -241,70 +256,86 @@ results$.pred_cluster[[1]] Additionally, we can extract the nested predicted output to be formatted in a single data frame, filling in the `NA` values with their predicted value using -`extract_predictions`. +`extract_predictions()`. ```{r} results %>% extract_predictions ``` +## Evaluation Metrics +While support values traditionally assess frequent itemset quality, they do not +guarantee predictive performance. Since the `predict()` methodology resembles a +recommender system, the results should be evaluated using similar metrics. +Common metrics such as root mean squared error (RMSE), accuracy, precision, and +recall are implemented in the `yardstick` package. +To prepare the `predict()` output for metric calculation, we provide a new +function `augment_itemset_predict()`. ```{r} +# Generate data to predict on +na_result <- tidyclust:::random_na_with_truth(groceries[1:5,], na_prob = 0.3) +pred_output <- predict(fi_fit, na_result$na_data) +truth_output <- na_result$truth # In a real scenario, this would be a separate holdout set -recipe <- recipes::recipe(~ ., data = groceries) - -tune_spec <- freq_itemsets( - min_support = tune(), - mining_method = "eclat" - ) %>% - set_engine("arules") %>% - set_mode("partition") - -min_support_grid <- dials::grid_regular( - min_support(), - levels = 10 - ) - -tune_workflow <- workflow() %>% - add_recipe(recipe) %>% - add_model(tune_spec) - - -fit(tune_workflow, data = groceries) - - - -rf_grid_search <- - tune_cluster( - tune_workflow, - # resamples = groceries, - grid = min_support_grid - ) - - - - +comparison_df <- augment_itemset_predict(pred_output, truth_output) +# Example for RMSE (using type = 'raw') +fi_fit %>% + predict(new_data = new_data, type = 'raw') %>% + augment_itemset_predict(truth = truth_data) %>% + yardstick::rmse(truth, preds) +# Example for Precision (using type = 'cluster') +fi_fit %>% + predict(new_data = new_data, type = 'cluster') %>% + augment_itemset_predict(truth = truth_data) %>% + dplyr::mutate( + truth = factor(truth, levels = c(0, 1)), + preds = factor(preds, levels = c(0, 1)) + ) %>% + yardstick::precision(truth, preds) +``` +When using RMSE, the raw `predict()` output should be used, while accuracy, +precision, and recall will use the cluster output or user thresholded raw +output. Caution should be used when looking at accuracy, precision, and recall +for imbalanced datasets (where items are infrequently purchased). In such cases, +F1-Score offers a balance between precision and recall, and precision-recall +(PR) curves aid in determining the best threshold value. -new_data <- groceries[1:5,] %>% - dplyr::mutate(`whole milk` = as.numeric(NA), - frankfurter = as.numeric(NA), - yogurt = as.numeric(NA), - `other vegetables` = as.numeric(1)) +```{r} +fi_fit %>% + predict(new_data = new_data, type = 'raw') %>% + augment_itemset_predict(truth = truth_data) %>% + dplyr::mutate(truth = factor(truth, levels = c(0, 1))) %>% + yardstick::pr_curve(truth, preds) %>% + autoplot() +``` -raw <- fi_fit %>% - predict(new_data, type = "raw") +Each point on the PR curve represents the precision and recall of the model at a +specific threshold. By varying this threshold, different precision and recall +values are obtained, creating the curve. The ideal curve is close to the +top-right corner, indicating high precision and high recall. -threshold <- fi_fit %>% - predict(new_data) +## Hyperparameter Tuning +The sole parameter capable of being tuned in a FIM model is `min_support.` +Selecting the correct value is imperative for finding useful frequent itemsets. +The default grid for `min_support` is from 0.1 to 0.5. The lower bound of 0.1 was +chosen to avoid reporting too many frequent itemsets, even for smaller datasets, +while the upper bound of 0.5 was selected since it ensures that each frequent +itemset has a support of at least 50%. +```{r} +dials::grid_regular( + min_support(), + levels = 10 +) ``` - - - +Usually, the above object is paired with `tune_cluster`, however cross-validation +is not currently implemented for FIM. Future work will focus on improving tuning +and implementing cross-validation. From 083cb4d40e399a09e103c6e6171f27aa5ca8b3c8 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sun, 25 May 2025 15:22:17 -0700 Subject: [PATCH 53/71] added header descriptions about functions --- R/augment_itemset_predict.R | 109 +++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/R/augment_itemset_predict.R b/R/augment_itemset_predict.R index 6583f89..ee49d12 100644 --- a/R/augment_itemset_predict.R +++ b/R/augment_itemset_predict.R @@ -1,12 +1,33 @@ -#' Extract Predictions from Observation Data Frames +#' Augment Itemset Predictions with Truth Values #' -#' This function processes a data frame containing observation data frames and extracts non-NA values. +#' This function processes the output of a `predict()` call for frequent itemset models +#' and joins it with the corresponding ground truth data. It's designed to prepare +#' the prediction and truth values in a format suitable for calculating evaluation metrics +#' using packages like `yardstick`. #' -#' Returns recommender predictions with predicted values imputed into dataset -#' Notes: currently imputes thresholded probabilities +#' @param pred_output A data frame that is the output of `predict()` from a `freq_itemsets` model. +#' It is expected to have a column named `.pred_cluster`, where each cell contains +#' a data frame with prediction details (including `.pred_item`, `.obs_item`, and `item`). +#' @param truth_output A data frame representing the ground truth. It should have a similar +#' structure to the input data used for prediction, where columns represent items +#' and rows represent transactions. #' -#' @param pred_output A data frame with one column, where each cell contains a data frame. -#' @return A data frame with items as columns and non-NA values as rows. +#' @details +#' The function first extracts and combines all individual item prediction data frames +#' nested within the `pred_output`. It then filters for items where a prediction was made +#' (i.e., `!is.na(.pred_item)`) and standardizes item names by removing backticks. +#' The `truth_output` is pivoted to a long format to match the structure of the predictions. +#' Finally, an inner join is performed to ensure that only predicted items are included in +#' the final result, aligning predictions with their corresponding true values. +#' +#' @return A data frame with the following columns: +#' \itemize{ +#' \item `item`: The name of the item. +#' \item `row_id`: An identifier for the transaction (row) from which the prediction came. +#' \item `preds`: The predicted value for the item (either raw probability or binary prediction). +#' \item `truth`: The true value for the item from `truth_output`. +#' } +#' This output is suitable for direct use with `yardstick` metric functions. #' @export augment_itemset_predict <- function(pred_output, truth_output) { @@ -33,10 +54,44 @@ augment_itemset_predict <- function(pred_output, truth_output) { dplyr::select(result, item, row_id, preds, truth = truth_value) } - - - - +#' Generate Dataframe with Random NAs and Corresponding Truth +#' +#' @description +#' This helper function creates a new data frame by randomly introducing `NA` values +#' into an input data frame. It also returns the original data frame as a "truth" +#' reference, which can be useful for simulating scenarios with missing data +#' for prediction tasks. +#' +#' @param df The input data frame to which `NA` values will be introduced. +#' It is typically a transactional dataset where columns are items and rows are transactions. +#' @param na_prob The probability (between 0 and 1) that any given cell in the +#' input data frame will be replaced with `NA`. +#' +#' @return A list containing two data frames: +#' \itemize{ +#' \item `na_data`: The data frame with `NA` values randomly introduced. +#' \item `truth`: The original input data frame, serving as the ground truth. +#' } +#' @examples +#' # Create a sample data frame +#' sample_df <- data.frame( +#' itemA = c(1, 0, 1), +#' itemB = c(0, 1, 1), +#' itemC = c(1, 1, 0) +#' ) +#' +#' # Generate NA data and truth with 30% NA probability +#' set.seed(123) +#' na_data_list <- random_na_with_truth(sample_df, na_prob = 0.3) +#' +#' # View the NA data +#' print(na_data_list$na_data) +#' +#' # View the truth data +#' print(na_data_list$truth) +#' +#' This function is not exported as it was used to test and provide examples in +#' the vignettes, it may be formally introduced in the future. random_na_with_truth <- function(df, na_prob = 0.3) { # Create a copy of the original dataframe to store truth values truth_df <- df @@ -62,37 +117,3 @@ random_na_with_truth <- function(df, na_prob = 0.3) { truth = truth_df ) } - - -# set.seed(123) -# na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) -# pred_output <- predict(fi_fit, na_result$na_data) -# comparison_df <- augment_itemset_predict(pred_output, na_result$truth) -# -# comparison_df %>% -# dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% -# yardstick::accuracy(truth, preds) -# -# #---------------------------------- -# -# set.seed(123) -# na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) -# pred_output <- predict(fi_fit, na_result$na_data, type = 'raw') -# comparison_df <- augment_itemset_predict(pred_output, na_result$truth) -# -# comparison_df %>% -# dplyr::mutate(truth = factor(truth, levels=c(0, 1))) %>% -# yardstick::pr_curve(truth, preds) %>% -# autoplot() -# -# -# #---------------------------------- -# -# set.seed(123) -# na_result <- random_na_with_truth(groceries[1:5,], na_prob = 0.3) -# pred_output <- predict(fi_fit, na_result$na_data) -# comparison_df <- augment_itemset_predict(pred_output, na_result$truth) -# -# comparison_df %>% -# dplyr::mutate(truth = factor(truth, levels=c(0, 1)), preds = factor(preds, levels=c(0, 1))) %>% -# yardstick::roc_auc(truth, preds) From 1923aeaa36c11e074ada93b71a527e0c2aa4893a Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sun, 25 May 2025 15:25:20 -0700 Subject: [PATCH 54/71] added convergence limit and warning message --- R/extract_cluster_assignment.R | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index c2e883c..66c0d43 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -159,6 +159,7 @@ extract_cluster_assignment.hclust <- function(object, #' @export extract_cluster_assignment.itemsets <- function(object, ...) { + max_iter = 1000 items <- attr(object, "item_names") itemsets <- arules::DATAFRAME(object) @@ -166,10 +167,12 @@ extract_cluster_assignment.itemsets <- function(object, ...) { support <- itemsets$support clusters <- numeric(length(items)) changed <- TRUE # Flag to track convergence + iter <- 0 # Initialize iteration counter # Continue until no changes occur - while (changed) { + while (changed && iter < max_iter) { changed <- FALSE + iter <- iter + 1 for (i in 1:length(items)) { current_item <- items[i] relevant_itemsets <- which(sapply(itemset_list, function(x) current_item %in% x)) @@ -214,6 +217,16 @@ extract_cluster_assignment.itemsets <- function(object, ...) { } } + if (iter == max_iter && changed) { + rlang::warn( + paste0( + "Cluster assignment did not converge within the maximum of ", + max_iter, + " iterations. Returning the current cluster assignments." + ) + ) + } + item_assignment_tibble_w_outliers(clusters, ...) } From 3bfdba43ebab7ead1340a7a59568c34472bebf98 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sun, 25 May 2025 15:46:18 -0700 Subject: [PATCH 55/71] update freq_itemsets extract_fit_summary and ? information --- NAMESPACE | 1 + R/extract_fit_summary.R | 12 +++++++ R/freq_itemsets.R | 22 ++++++++++--- man/augment_itemset_predict.Rd | 32 ++++++++++++++---- man/freq_itemsets.Rd | 22 ++++++++++--- man/random_na_with_truth.Rd | 49 ++++++++++++++++++++++++++++ vignettes/articles/freq_itemsets.Rmd | 18 ++++------ 7 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 man/random_na_with_truth.Rd diff --git a/NAMESPACE b/NAMESPACE index 7cdec2e..388a318 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -21,6 +21,7 @@ S3method(extract_fit_summary,KMeansCluster) S3method(extract_fit_summary,cluster_fit) S3method(extract_fit_summary,cluster_spec) S3method(extract_fit_summary,hclust) +S3method(extract_fit_summary,itemsets) S3method(extract_fit_summary,kmeans) S3method(extract_fit_summary,kmodes) S3method(extract_fit_summary,kproto) diff --git a/R/extract_fit_summary.R b/R/extract_fit_summary.R index ce6526a..3675e6b 100644 --- a/R/extract_fit_summary.R +++ b/R/extract_fit_summary.R @@ -179,3 +179,15 @@ extract_fit_summary.hclust <- function(object, ...) { cluster_assignments = clusts ) } + +#' @export +extract_fit_summary.itemsets <- function(object, ..., + call = rlang::caller_env(n = 0)) { + rlang::abort( + paste( + "Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly.\n", + "Please use arules::inspect() on the fit of your cluster specification." + ), + call = call + ) +} diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index 04d9e92..be01ac4 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -2,8 +2,10 @@ #' #' @description #' -#' `freq_itemsets()` defines a model that finds frequent itemsets based on -#' specified minimum support. +#' `freq_itemsets()` defines a model for Frequent Itemset Mining (FIM), a data mining +#' technique used to discover relationships between items in transactional datasets. +#' This model finds sets of items (itemsets) that frequently co-occur based on a +#' user-specified minimum support threshold. #' #' The method of estimation is chosen by setting the model engine. The #' engine-specific pages for this model are listed below. @@ -13,7 +15,8 @@ #' @param mode A single character string for the type of model. The only #' possible value for this model is "partition". #' @param engine A single character string specifying the computational engine -#' to use for fitting. The default for this model is `"arules"`. +#' to use for fitting. The default for this model is `"arules"`. Currently, +#' `"arules"` is the only supported engine. #' @param mining_method A single character string specifying the algorithm to use for #' fitting. Possible algorithms are `"apriori"` and `"eclat"`. The default for #' this model is `"eclat"`. @@ -23,7 +26,18 @@ #' #' ## What does it mean to predict? #' -#' WORK IN PROGRESS +#' For `freq_itemsets` models, the `predict()` function is implemented as a recommender system. +#' Given new data with partial transaction information (i.e., some items observed, others `NA`), +#' the model predicts other items likely to be in the transaction. +#' +#' Predictions are based on item-level probabilities derived from the confidence of frequent itemsets. +#' For each missing item, relevant frequent itemsets containing both the missing item and observed items are identified. +#' Confidence (support of itemset / support of observed items) is aggregated across relevant itemsets. +#' If no relevant itemsets are found, the item's global support from the training data is used as a fallback. +#' +#' The `predict()` output provides a nested data frame per transaction, including `item`, +#' `.obs_item` (observed status), and `.pred_item` (predicted values). +#' The `extract_predictions()` helper function can reformat this nested output into a single data frame. #' #' @return A `freq_itemsets` association specification. #' diff --git a/man/augment_itemset_predict.Rd b/man/augment_itemset_predict.Rd index 5e18f10..4c7bfce 100644 --- a/man/augment_itemset_predict.Rd +++ b/man/augment_itemset_predict.Rd @@ -2,20 +2,40 @@ % Please edit documentation in R/augment_itemset_predict.R \name{augment_itemset_predict} \alias{augment_itemset_predict} -\title{Extract Predictions from Observation Data Frames} +\title{Augment Itemset Predictions with Truth Values} \usage{ augment_itemset_predict(pred_output, truth_output) } \arguments{ -\item{pred_output}{A data frame with one column, where each cell contains a data frame.} +\item{pred_output}{A data frame that is the output of \code{predict()} from a \code{freq_itemsets} model. +It is expected to have a column named \code{.pred_cluster}, where each cell contains +a data frame with prediction details (including \code{.pred_item}, \code{.obs_item}, and \code{item}).} + +\item{truth_output}{A data frame representing the ground truth. It should have a similar +structure to the input data used for prediction, where columns represent items +and rows represent transactions.} } \value{ -A data frame with items as columns and non-NA values as rows. +A data frame with the following columns: +\itemize{ +\item \code{item}: The name of the item. +\item \code{row_id}: An identifier for the transaction (row) from which the prediction came. +\item \code{preds}: The predicted value for the item (either raw probability or binary prediction). +\item \code{truth}: The true value for the item from \code{truth_output}. +} +This output is suitable for direct use with \code{yardstick} metric functions. } \description{ -This function processes a data frame containing observation data frames and extracts non-NA values. +This function processes the output of a \code{predict()} call for frequent itemset models +and joins it with the corresponding ground truth data. It's designed to prepare +the prediction and truth values in a format suitable for calculating evaluation metrics +using packages like \code{yardstick}. } \details{ -Returns recommender predictions with predicted values imputed into dataset -Notes: currently imputes thresholded probabilities +The function first extracts and combines all individual item prediction data frames +nested within the \code{pred_output}. It then filters for items where a prediction was made +(i.e., \code{!is.na(.pred_item)}) and standardizes item names by removing backticks. +The \code{truth_output} is pivoted to a long format to match the structure of the predictions. +Finally, an inner join is performed to ensure that only predicted items are included in +the final result, aligning predictions with their corresponding true values. } diff --git a/man/freq_itemsets.Rd b/man/freq_itemsets.Rd index cb95142..06851da 100644 --- a/man/freq_itemsets.Rd +++ b/man/freq_itemsets.Rd @@ -16,7 +16,8 @@ freq_itemsets( possible value for this model is "partition".} \item{engine}{A single character string specifying the computational engine -to use for fitting. The default for this model is \code{"arules"}.} +to use for fitting. The default for this model is \code{"arules"}. Currently, +\code{"arules"} is the only supported engine.} \item{min_support}{Positive double, minimum support for an itemset (between 0 and 1).} @@ -28,8 +29,10 @@ this model is \code{"eclat"}.} A \code{freq_itemsets} association specification. } \description{ -\code{freq_itemsets()} defines a model that finds frequent itemsets based on -specified minimum support. +\code{freq_itemsets()} defines a model for Frequent Itemset Mining (FIM), a data mining +technique used to discover relationships between items in transactional datasets. +This model finds sets of items (itemsets) that frequently co-occur based on a +user-specified minimum support threshold. The method of estimation is chosen by setting the model engine. The engine-specific pages for this model are listed below. @@ -40,7 +43,18 @@ engine-specific pages for this model are listed below. \details{ \subsection{What does it mean to predict?}{ -WORK IN PROGRESS +For \code{freq_itemsets} models, the \code{predict()} function is implemented as a recommender system. +Given new data with partial transaction information (i.e., some items observed, others \code{NA}), +the model predicts other items likely to be in the transaction. + +Predictions are based on item-level probabilities derived from the confidence of frequent itemsets. +For each missing item, relevant frequent itemsets containing both the missing item and observed items are identified. +Confidence (support of itemset / support of observed items) is aggregated across relevant itemsets. +If no relevant itemsets are found, the item's global support from the training data is used as a fallback. + +The \code{predict()} output provides a nested data frame per transaction, including \code{item}, +\code{.obs_item} (observed status), and \code{.pred_item} (predicted values). +The \code{extract_predictions()} helper function can reformat this nested output into a single data frame. } } \examples{ diff --git a/man/random_na_with_truth.Rd b/man/random_na_with_truth.Rd new file mode 100644 index 0000000..8f385e0 --- /dev/null +++ b/man/random_na_with_truth.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/augment_itemset_predict.R +\name{random_na_with_truth} +\alias{random_na_with_truth} +\title{Generate Dataframe with Random NAs and Corresponding Truth} +\usage{ +random_na_with_truth(df, na_prob = 0.3) +} +\arguments{ +\item{df}{The input data frame to which \code{NA} values will be introduced. +It is typically a transactional dataset where columns are items and rows are transactions.} + +\item{na_prob}{The probability (between 0 and 1) that any given cell in the +input data frame will be replaced with \code{NA}.} +} +\value{ +A list containing two data frames: +\itemize{ +\item \code{na_data}: The data frame with \code{NA} values randomly introduced. +\item \code{truth}: The original input data frame, serving as the ground truth. +} +} +\description{ +This helper function creates a new data frame by randomly introducing \code{NA} values +into an input data frame. It also returns the original data frame as a "truth" +reference, which can be useful for simulating scenarios with missing data +for prediction tasks. +} +\examples{ +# Create a sample data frame +sample_df <- data.frame( + itemA = c(1, 0, 1), + itemB = c(0, 1, 1), + itemC = c(1, 1, 0) +) + +# Generate NA data and truth with 30\% NA probability +set.seed(123) +na_data_list <- random_na_with_truth(sample_df, na_prob = 0.3) + +# View the NA data +print(na_data_list$na_data) + +# View the truth data +print(na_data_list$truth) + +This function is not exported as it was used to test and provide examples in +the vignettes, it may be formally introduced in the future. +} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 1cf3ed8..ca32b76 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -179,17 +179,11 @@ fi_fit %>% summary() ``` -We can also extract the standard `tidyclust` summary list: +We can not extract the standard `tidyclust` summary list since centriods are not +useful for frequent itemsets, however we can extract the frequent itemsets: ```{r} -# fi_summary <- fi_fit %>% -# extract_fit_summary() -# -# fi_summary %>% -# str() - - -# Make an extract_fit_summary_items() that returns a warnding saying that centroids are not usfeul, suggust looking at itemsets (and say how to do this) +arules::inspect(fi_fit$fit) ``` Note that, although the frequent itemset algorithm is not focused on cluster's @@ -285,13 +279,13 @@ comparison_df <- augment_itemset_predict(pred_output, truth_output) # Example for RMSE (using type = 'raw') fi_fit %>% predict(new_data = new_data, type = 'raw') %>% - augment_itemset_predict(truth = truth_data) %>% + augment_itemset_predict(truth = truth_output) %>% yardstick::rmse(truth, preds) # Example for Precision (using type = 'cluster') fi_fit %>% predict(new_data = new_data, type = 'cluster') %>% - augment_itemset_predict(truth = truth_data) %>% + augment_itemset_predict(truth = truth_output) %>% dplyr::mutate( truth = factor(truth, levels = c(0, 1)), preds = factor(preds, levels = c(0, 1)) @@ -309,7 +303,7 @@ F1-Score offers a balance between precision and recall, and precision-recall ```{r} fi_fit %>% predict(new_data = new_data, type = 'raw') %>% - augment_itemset_predict(truth = truth_data) %>% + augment_itemset_predict(truth = truth_output) %>% dplyr::mutate(truth = factor(truth, levels = c(0, 1))) %>% yardstick::pr_curve(truth, preds) %>% autoplot() From 39a92eb9e9de441e702cd2b90ee21624e5980fb3 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 29 May 2025 10:29:19 -0700 Subject: [PATCH 56/71] vignette update --- vignettes/articles/freq_itemsets.Rmd | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index ca32b76..b8b2a14 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -236,18 +236,19 @@ We display the predicted values in the column `.pred_item`, and the observed values in the column `.obs_item`. ```{r} -new_data <- groceries[1:5,] %>% - dplyr::mutate(`whole milk` = as.numeric(NA), - frankfurter = as.numeric(NA), - yogurt = as.numeric(NA), - `other vegetables` = as.numeric(1)) +new_data <- groceries[1:5,] %>% + tidyclust:::random_na_with_truth(na_prob = 0.3) results <- fi_fit %>% - predict(new_data) + predict(new_data$na_data) results$.pred_cluster[[1]] ``` +The function `random_na_with_truth()` is used for testing purposes to randomly +assign values to be `NA`, however it can be accessed using in `tidyclust` using +the `:::`. + Additionally, we can extract the nested predicted output to be formatted in a single data frame, filling in the `NA` values with their predicted value using `extract_predictions()`. From d68410d0df95c843b7e6821ca3e3eae41d07227d Mon Sep 17 00:00:00 2001 From: Wander03 Date: Sun, 1 Jun 2025 17:40:51 -0700 Subject: [PATCH 57/71] create test cases for freq_itemsets --- R/freq_itemsets.R | 17 +- R/freq_itemsets_data.R | 2 +- tests/testthat/_snaps/extract_centroids.md | 9 + .../_snaps/extract_cluster_assignment.md | 8 + tests/testthat/_snaps/freq_itemsets-arules.md | 9 + tests/testthat/_snaps/freq_itemsets.md | 75 ++++--- tests/testthat/_snaps/predict.md | 8 + tests/testthat/test-extract_centroids.R | 17 ++ .../test-extract_cluster_assignment.R | 18 ++ tests/testthat/test-freq_itemsets-arules.R | 97 ++++----- tests/testthat/test-freq_itemsets.R | 202 +++++++----------- tests/testthat/test-predict.R | 9 + vignettes/articles/freq_itemsets.Rmd | 2 +- 13 files changed, 263 insertions(+), 210 deletions(-) create mode 100644 tests/testthat/_snaps/freq_itemsets-arules.md diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index be01ac4..a193ab0 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -137,10 +137,15 @@ update.freq_itemsets <- function(object, check_args.freq_itemsets <- function(object) { args <- lapply(object$args, rlang::eval_tidy) - if (all(is.numeric(args$min_support)) && any(args$min_support < 0) && any(args$min_support > 1)) { + if (all(is.numeric(args$min_support)) && (any(args$min_support < 0) || any(args$min_support > 1))) { rlang::abort("The minimum support should be between 0 and 1.") } + if (all(is.character(args$mining_method)) && + !all(args$mining_method %in% c("apriori", "eclat"))) { + rlang::abort("The mining method should be either 'apriori' or 'eclat'.") + } + invisible(object) } @@ -176,11 +181,15 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { } if (mining_method == "apriori") { - res <- arules::apriori(data = x, parameter = list(support = min_support, target = "frequent itemsets")) + res <- arules::apriori(data = x, + parameter = list(support = min_support, target = "frequent itemsets"), + control = list(verbose = FALSE)) } else if (mining_method == "eclat") { - res <- arules::eclat(data = x, parameter = list(support = min_support)) + res <- arules::eclat(data = x, + parameter = list(support = min_support), + control = list(verbose = FALSE)) } else { - stop("Invalid method specified. Choose 'apriori' or 'eclat'.") + stop("Invalid mining method specified. Choose 'apriori' or 'eclat'.") } attr(res, "item_names") <- colnames(x) diff --git a/R/freq_itemsets_data.R b/R/freq_itemsets_data.R index d56d145..a85117a 100644 --- a/R/freq_itemsets_data.R +++ b/R/freq_itemsets_data.R @@ -27,7 +27,7 @@ make_freq_itemsets <- function() { mode = "partition", value = list( interface = "matrix", - protect = c("x, min_support, mining_method"), + protect = c("x"), func = c(pkg = "tidyclust", fun = ".freq_itemsets_fit_arules"), defaults = list() ) diff --git a/tests/testthat/_snaps/extract_centroids.md b/tests/testthat/_snaps/extract_centroids.md index 3003671..54d5cea 100644 --- a/tests/testthat/_snaps/extract_centroids.md +++ b/tests/testthat/_snaps/extract_centroids.md @@ -30,3 +30,12 @@ Error in `extract_centroids()`: ! Using `h` argument is not supported. Please use `cut_height` instead. +# extract_centroids errors for freq_itemsets + + Code + extract_centroids(fi_fit) + Condition + Error in `extract_centroids()`: + ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. + Please use arules::inspect() on the fit of your cluster specification. + diff --git a/tests/testthat/_snaps/extract_cluster_assignment.md b/tests/testthat/_snaps/extract_cluster_assignment.md index d1381c3..5d91dc3 100644 --- a/tests/testthat/_snaps/extract_cluster_assignment.md +++ b/tests/testthat/_snaps/extract_cluster_assignment.md @@ -30,3 +30,11 @@ Error in `extract_cluster_assignment()`: ! Using `h` argument is not supported. Please use `cut_height` instead. +# extract_cluster_assignment() errors for freq_itemsets() cluster spec + + Code + fi_spec %>% extract_cluster_assignment() + Condition + Error in `extract_cluster_assignment()`: + ! This function requires a fitted model. Please use `fit()` on your cluster specification. + diff --git a/tests/testthat/_snaps/freq_itemsets-arules.md b/tests/testthat/_snaps/freq_itemsets-arules.md new file mode 100644 index 0000000..3da7f81 --- /dev/null +++ b/tests/testthat/_snaps/freq_itemsets-arules.md @@ -0,0 +1,9 @@ +# extract_centroids works + + Code + extract_centroids(fi_fit) + Condition + Error in `extract_centroids()`: + ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. + Please use arules::inspect() on the fit of your cluster specification. + diff --git a/tests/testthat/_snaps/freq_itemsets.md b/tests/testthat/_snaps/freq_itemsets.md index fd1dbd6..8727c87 100644 --- a/tests/testthat/_snaps/freq_itemsets.md +++ b/tests/testthat/_snaps/freq_itemsets.md @@ -1,24 +1,33 @@ # bad input Code - k_means(mode = "bogus") + freq_itemsets(mode = "bogus") Condition Error in `modelenv::check_spec_mode_engine_val()`: - ! 'bogus' is not a known mode for model `k_means()`. + ! 'bogus' is not a known mode for model `freq_itemsets()`. --- Code - bt <- k_means(num_clusters = -1) %>% set_engine("stats") - fit(bt, mpg ~ ., mtcars) + bt <- freq_itemsets(mining_method = "bogus") + fit(bt, ~., toy_df) Condition Error in `check_args()`: - ! The number of centers should be >= 0. + ! The mining method should be either 'apriori' or 'eclat'. --- Code - translate_tidyclust(k_means(), engine = NULL) + bt <- freq_itemsets(min_support = -1) %>% set_engine("arules") + fit(bt, ~., toy_df) + Condition + Error in `check_args()`: + ! The minimum support should be between 0 and 1. + +--- + + Code + translate_tidyclust(freq_itemsets(), engine = NULL) Condition Error in `translate_tidyclust.default()`: ! Please set an engine. @@ -26,60 +35,66 @@ --- Code - translate_tidyclust(k_means(formula = ~x)) + translate_tidyclust(freq_itemsets(formula = ~x)) Condition - Error in `k_means()`: + Error in `freq_itemsets()`: ! unused argument (formula = ~x) +# extract_centroids work + + Code + extract_centroids(fi_fit) + Condition + Error in `extract_centroids()`: + ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. + Please use arules::inspect() on the fit of your cluster specification. + # printing Code - k_means() + freq_itemsets() Output - K Means Cluster Specification (partition) + Frequent Itemsets Mining Specification (partition) + + Main Arguments: + mining_method = eclat - Computational engine: stats + Computational engine: arules --- Code - k_means(num_clusters = 10) + freq_itemsets(min_support = 0.5) Output - K Means Cluster Specification (partition) + Frequent Itemsets Mining Specification (partition) Main Arguments: - num_clusters = 10 + min_support = 0.5 + mining_method = eclat - Computational engine: stats + Computational engine: arules # updating Code - k_means(num_clusters = 5) %>% update(num_clusters = tune()) + freq_itemsets(min_support = 0.5) %>% update(min_support = tune()) Output - K Means Cluster Specification (partition) + Frequent Itemsets Mining Specification (partition) Main Arguments: - num_clusters = tune() + min_support = tune() + mining_method = eclat - Computational engine: stats + Computational engine: arules -# errors if `num_clust` isn't specified +# errors if `min_support` isn't specified Code - k_means() %>% set_engine("stats") %>% fit(~., data = mtcars) + freq_itemsets() %>% set_engine("arules") %>% fit(~., data = toy_df) Condition Error in `fit()`: - ! Please specify `num_clust` to be able to fit specification. - ---- - - Code - k_means() %>% set_engine("ClusterR") %>% fit(~., data = mtcars) - Condition - Error in `tidyclust::.k_means_fit_ClusterR()`: - ! argument "clusters" is missing, with no default + ! Please specify `min_support` to be able to fit specification. diff --git a/tests/testthat/_snaps/predict.md b/tests/testthat/_snaps/predict.md index 4fd978b..133d45f 100644 --- a/tests/testthat/_snaps/predict.md +++ b/tests/testthat/_snaps/predict.md @@ -30,3 +30,11 @@ Error in `predict()`: ! Using `h` argument is not supported. Please use `cut_height` instead. +# predict() errors for cluster spec for freq_itemsets + + Code + predict(spec) + Condition + Error in `predict()`: + ! This function requires a fitted model. Please use `fit()` on your cluster specification. + diff --git a/tests/testthat/test-extract_centroids.R b/tests/testthat/test-extract_centroids.R index ea5ada3..fb95dae 100644 --- a/tests/testthat/test-extract_centroids.R +++ b/tests/testthat/test-extract_centroids.R @@ -1,3 +1,11 @@ +toy_df <- data.frame( + 'beer' = c(F, T, T, T, F), + 'milk' = c(T, F, T, T, T), + 'bread' = c(T, T, F, T, T), + 'diapers' = c(T, T, T, T, T), + 'eggs' = c(F, T, F, F, F) +) + test_that("extract_centroids() errors for cluster spec", { spec <- tidyclust::k_means(num_clusters = 4) @@ -64,3 +72,12 @@ test_that("prefix is passed in extract_centroids()", { all(substr(res$.cluster, 1, 2) == "C_") ) }) + +test_that("extract_centroids errors for freq_itemsets", { + set.seed(1234) + fi_fit <- freq_itemsets(min_support = 0.5) %>% + set_engine("arules") %>% + fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) + + expect_snapshot(error = TRUE, extract_centroids(fi_fit)) +}) diff --git a/tests/testthat/test-extract_cluster_assignment.R b/tests/testthat/test-extract_cluster_assignment.R index 7cca9a1..5889cf4 100644 --- a/tests/testthat/test-extract_cluster_assignment.R +++ b/tests/testthat/test-extract_cluster_assignment.R @@ -1,3 +1,11 @@ +toy_df <- data.frame( + 'beer' = c(F, T, T, T, F), + 'milk' = c(T, F, T, T, T), + 'bread' = c(T, T, F, T, T), + 'diapers' = c(T, T, T, T, T), + 'eggs' = c(F, T, F, F, F) +) + test_that("extract_cluster_assignment() errors for cluster spec", { spec <- tidyclust::k_means(num_clusters = 4) @@ -64,3 +72,13 @@ test_that("prefix is passed in extract_cluster_assignment()", { all(substr(res$.cluster, 1, 2) == "C_") ) }) + +test_that("extract_cluster_assignment() errors for freq_itemsets() cluster spec", { + fi_spec <- freq_itemsets(min_support = 0.5) + + expect_snapshot( + error = TRUE, + fi_spec %>% + extract_cluster_assignment() + ) +}) diff --git a/tests/testthat/test-freq_itemsets-arules.R b/tests/testthat/test-freq_itemsets-arules.R index 07c1fa8..d74e991 100644 --- a/tests/testthat/test-freq_itemsets-arules.R +++ b/tests/testthat/test-freq_itemsets-arules.R @@ -1,82 +1,75 @@ +toy_df <- data.frame( + 'beer' = c(F, T, T, T, F), + 'milk' = c(T, F, T, T, T), + 'bread' = c(T, T, F, T, T), + 'diapers' = c(T, T, T, T, T), + 'eggs' = c(F, T, F, F, F) +) + +toy_pred <- data.frame( + 'beer' = c(F), + 'milk' = c(NA), + 'bread' = c(T), + 'diapers' = c(T), + 'eggs' = c(F) +) + test_that("fitting", { set.seed(1234) - spec <- k_means(num_clusters = 5) %>% - set_engine("stats") + spec <- freq_itemsets(min_support = 0.5) %>% + set_engine("arules") expect_no_error( - res <- fit(spec, ~., mtcars) - ) - - expect_no_error( - res <- fit_xy(spec, mtcars) + res <- fit(spec, ~., toy_df) ) }) test_that("predicting", { set.seed(1234) - spec <- k_means(num_clusters = 3) %>% - set_engine("stats") + spec <- freq_itemsets(min_support = 0.5) %>% + set_engine("arules") - res <- fit(spec, ~., iris) + res <- fit(spec, ~., toy_df) - preds <- predict(res, iris[c(25, 75, 125), ]) + preds <- predict(res, toy_pred)$.pred_cluster[[1]]$.pred_item expect_identical( preds, - tibble::tibble(.pred_cluster = factor(paste0("Cluster_", 1:3))) - ) -}) - -test_that("all levels are preserved with 1 row predictions", { - set.seed(1234) - spec <- k_means(num_clusters = 3) %>% - set_engine("stats") - - res <- fit(spec, ~., mtcars) - - preds <- predict(res, mtcars[1, ]) - - expect_identical( - levels(preds$.pred_cluster), - paste0("Cluster_", 1:3) + c(NA, 1, NA, NA, NA) ) }) -test_that("extract_centroids() works", { +test_that("extract_centroids works", { set.seed(1234) - spec <- k_means(num_clusters = 3) %>% - set_engine("stats") - - res <- fit(spec, ~., iris) - - centroids <- extract_centroids(res) - - expected <- vctrs::vec_cbind( - tibble::tibble(.cluster = factor(paste0("Cluster_", 1:3))), - tibble::as_tibble(res$fit$centers) - ) + fi_fit <- freq_itemsets(min_support = 0.5) %>% + set_engine("arules") %>% + fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) - expect_identical( - centroids, - expected - ) + expect_snapshot(error = TRUE, extract_centroids(fi_fit)) }) test_that("extract_cluster_assignment() works", { set.seed(1234) - spec <- k_means(num_clusters = 3) %>% - set_engine("stats") + fi_fit <- freq_itemsets(min_support = 0.5, mining_method = "eclat") %>% + set_engine("arules") %>% + fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) - res <- fit(spec, ~., iris) + set.seed(1234) + ref_res <- arules::eclat(data = toy_df, + parameter = list(support = 0.5), + control = list(verbose = FALSE)) - clusters <- extract_cluster_assignment(res) + ref_itemsets <- arules::DATAFRAME(ref_res) + ref_clusts <- c(1, 2, 2, 2, 0) + ref_outliers <- "eggs" - expected <- vctrs::vec_cbind( - tibble::tibble(.cluster = factor(paste0("Cluster_", res$fit$cluster))) + expect_equal( + arules::DATAFRAME(fi_fit$fit), + ref_itemsets ) - expect_identical( - clusters, - expected + expect_equal( + ref_clusts, + extract_cluster_assignment(fi_fit)$.cluster %>% as.numeric() - 1 ) }) diff --git a/tests/testthat/test-freq_itemsets.R b/tests/testthat/test-freq_itemsets.R index bb682b5..cff1a28 100644 --- a/tests/testthat/test-freq_itemsets.R +++ b/tests/testthat/test-freq_itemsets.R @@ -1,181 +1,139 @@ +toy_df <- data.frame( + 'beer' = c(F, T, T, T, F), + 'milk' = c(T, F, T, T, T), + 'bread' = c(T, T, F, T, T), + 'diapers' = c(T, T, T, T, T), + 'eggs' = c(F, T, F, F, F) +) + +toy_pred <- data.frame( + 'beer' = c(F), + 'milk' = c(NA), + 'bread' = c(T), + 'diapers' = c(T), + 'eggs' = c(F) +) + test_that("primary arguments", { basic <- freq_itemsets(mode = "partition") - basic_stats <- translate_tidyclust(basic %>% set_engine("arules")) - expect_equal( - basic_stats$method$fit$args, - list( - min_support = rlang::expr(missing_arg()), - mining_method = rlang::expr(missing_arg()) - ) - ) - - k <- k_means(num_clusters = 15, mode = "partition") - k_stats <- translate_tidyclust(k %>% set_engine("stats")) + basic_arules <- translate_tidyclust(basic %>% set_engine("arules")) expect_equal( - k_stats$method$fit$args, + basic_arules$method$fit$args, list( x = rlang::expr(missing_arg()), - centers = rlang::expr(missing_arg()), - centers = new_empty_quosure(15) + mining_method = new_empty_quosure("eclat") ) ) -}) -test_that("engine arguments", { - stats_print <- k_means(mode = "partition") + fi <- freq_itemsets(min_support = 0.5, mining_method = "apriori", mode = "partition") + fi_arules <- translate_tidyclust(fi %>% set_engine("arules")) expect_equal( - translate_tidyclust( - stats_print %>% - set_engine("stats", nstart = 1L) - )$method$fit$args, + fi_arules$method$fit$args, list( x = rlang::expr(missing_arg()), - centers = rlang::expr(missing_arg()), - nstart = new_empty_quosure(1L) + min_support = new_empty_quosure(0.5), + mining_method = new_empty_quosure("apriori") ) ) }) test_that("bad input", { - expect_snapshot(error = TRUE, k_means(mode = "bogus")) + expect_snapshot(error = TRUE, freq_itemsets(mode = "bogus")) + expect_snapshot(error = TRUE, { + bt <- freq_itemsets(mining_method = "bogus") + fit(bt, ~ ., toy_df) + }) expect_snapshot(error = TRUE, { - bt <- k_means(num_clusters = -1) %>% set_engine("stats") - fit(bt, mpg ~ ., mtcars) + bt <- freq_itemsets(min_support = -1) %>% set_engine("arules") + fit(bt, ~ ., toy_df) }) - expect_snapshot(error = TRUE, translate_tidyclust(k_means(), engine = NULL)) - expect_snapshot(error = TRUE, translate_tidyclust(k_means(formula = ~x))) + expect_snapshot(error = TRUE, translate_tidyclust(freq_itemsets(), engine = NULL)) + expect_snapshot(error = TRUE, translate_tidyclust(freq_itemsets(formula = ~x))) }) -test_that("predictions", { +test_that("clusters", { set.seed(1234) - kmeans_fit <- k_means(num_clusters = 4) %>% - set_engine("stats") %>% - fit(~., mtcars) + fi_fit <- freq_itemsets(min_support = 0.5, mining_method = "apriori") %>% + set_engine("arules") %>% + fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) set.seed(1234) - ref_res <- kmeans(mtcars, 4) + ref_res <- arules::apriori(data = toy_df, + parameter = list(support = 0.5, target = "frequent itemsets"), + control = list(verbose = FALSE)) + + ref_itemsets <- arules::DATAFRAME(ref_res) + ref_clusts <- c(1, 2, 2, 2, 0) + ref_outliers <- "eggs" + + expect_equal( + arules::DATAFRAME(fi_fit$fit), + ref_itemsets + ) + + expect_equal( + ref_clusts, + extract_cluster_assignment(fi_fit)$.cluster %>% as.numeric() - 1 + ) +}) - ref_predictions <- ref_res$centers %>% - flexclust::dist2(mtcars) %>% - apply(2, which.min) %>% - unname() +test_that("predict", { + set.seed(1234) + fi_fit <- freq_itemsets(min_support = 0.5, mining_method = "apriori") %>% + set_engine("arules") %>% + fit(~., toy_df) - relevel_preds <- function(x) { - factor(unname(x), unique(unname(x))) %>% as.numeric() - } + ref_pred_raw <- c(NA, 0.766666666667, NA, NA, NA) + ref_pred_thresh <- c(NA, 1, NA, NA, NA) expect_equal( - relevel_preds(ref_predictions), - predict(kmeans_fit, mtcars)$.pred_cluster %>% as.numeric() + ref_pred_thresh, + predict(fi_fit, toy_pred)$.pred_cluster[[1]]$.pred_item ) expect_equal( - relevel_preds(ref_predictions), - extract_cluster_assignment(kmeans_fit)$.cluster %>% as.numeric() + ref_pred_raw, + predict(fi_fit, toy_pred, type = "raw")$.pred_cluster[[1]]$.pred_item ) }) test_that("extract_centroids work", { set.seed(1234) - kmeans_fit <- k_means(num_clusters = 4) %>% - set_engine("stats") %>% - fit(~., mtcars) + fi_fit <- freq_itemsets(min_support = 0.5) %>% + set_engine("arules") %>% + fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) - set.seed(1234) - ref_res <- kmeans(mtcars, 4) - - ref_centroids <- dplyr::bind_cols( - .cluster = c(1L, 4L, 2L, 3L), - ref_res$centers - ) %>% - dplyr::arrange(.cluster) %>% - dplyr::select(-.cluster) - - expect_identical( - extract_centroids(kmeans_fit) %>% dplyr::select(-.cluster), - ref_centroids - ) + expect_snapshot(error = TRUE, extract_centroids(fi_fit)) }) test_that("Right classes", { expect_equal( - class(k_means()), - c("k_means", "cluster_spec", "unsupervised_spec") + class(freq_itemsets()), + c("freq_itemsets", "cluster_spec", "unsupervised_spec") ) }) test_that("printing", { expect_snapshot( - k_means() + freq_itemsets() ) expect_snapshot( - k_means(num_clusters = 10) + freq_itemsets(min_support = 0.5) ) }) test_that("updating", { expect_snapshot( - k_means(num_clusters = 5) %>% - update(num_clusters = tune()) - ) -}) - -test_that("Engine-specific arguments are passed to ClusterR models", { - spec <- k_means(num_clusters = 2) %>% - set_engine("ClusterR", fuzzy = FALSE) - - fit <- fit(spec, ~., data = mtcars) - expect_true(is.null(fit$fit$fuzzy_clusters)) - - spec <- k_means(num_clusters = 2) %>% - set_engine("ClusterR", fuzzy = TRUE) - - fit <- fit(spec, ~., data = mtcars) - expect_false(is.null(fit$fit$fuzzy_clusters)) -}) - -test_that("reordering is done correctly for stats k_means", { - set.seed(42) - - kmeans_fit <- k_means(num_clusters = 6) %>% - set_engine("stats") %>% - fit(~., data = mtcars) - - summ <- extract_fit_summary(kmeans_fit) - - expect_identical( - summ$n_members, - unname(as.integer(table(summ$cluster_assignments))) + freq_itemsets(min_support = 0.5) %>% + update(min_support = tune()) ) }) -test_that("reordering is done correctly for ClusterR k_means", { - set.seed(42) - - kmeans_fit <- k_means(num_clusters = 6) %>% - set_engine("ClusterR") %>% - fit(~., data = mtcars) - - summ <- extract_fit_summary(kmeans_fit) - - expect_identical( - summ$n_members, - unname(as.integer(table(summ$cluster_assignments))) - ) -}) - -test_that("errors if `num_clust` isn't specified", { - expect_snapshot( - error = TRUE, - k_means() %>% - set_engine("stats") %>% - fit(~ ., data = mtcars) - ) - +test_that("errors if `min_support` isn't specified", { expect_snapshot( error = TRUE, - k_means() %>% - set_engine("ClusterR") %>% - fit(~ ., data = mtcars) + freq_itemsets() %>% + set_engine("arules") %>% + fit(~ ., data = toy_df) ) }) diff --git a/tests/testthat/test-predict.R b/tests/testthat/test-predict.R index 349868e..1012d7c 100644 --- a/tests/testthat/test-predict.R +++ b/tests/testthat/test-predict.R @@ -64,3 +64,12 @@ test_that("prefix is passed in predict()", { all(substr(res$.pred_cluster, 1, 2) == "C_") ) }) + +test_that("predict() errors for cluster spec for freq_itemsets", { + spec <- tidyclust::freq_itemsets(min_support = 0.5) + + expect_snapshot( + error = TRUE, + predict(spec) + ) +}) diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index b8b2a14..cebb366 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -179,7 +179,7 @@ fi_fit %>% summary() ``` -We can not extract the standard `tidyclust` summary list since centriods are not +We can not extract the standard `tidyclust` summary list since centroids are not useful for frequent itemsets, however we can extract the frequent itemsets: ```{r} From 19c58ae7a447be292bfb9588def8d7c548655414 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 19 Jun 2025 13:09:28 -0700 Subject: [PATCH 58/71] move min_support tuning to dials --- NAMESPACE | 1 - R/dials-params.R | 19 ------------------- man/min_support.Rd | 24 ------------------------ vignettes/articles/freq_itemsets.Rmd | 6 ++---- 4 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 man/min_support.Rd diff --git a/NAMESPACE b/NAMESPACE index 388a318..e0da8d3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -119,7 +119,6 @@ export(list_md_problems) export(load_pkgs) export(make_classes_tidyclust) export(min_grid) -export(min_support) export(new_cluster_metric) export(new_cluster_spec) export(predict.cluster_fit) diff --git a/R/dials-params.R b/R/dials-params.R index b2ca2e1..0b569ed 100644 --- a/R/dials-params.R +++ b/R/dials-params.R @@ -43,22 +43,3 @@ values_linkage_method <- c( "ward.D", "ward.D2", "single", "complete", "average", "mcquitty", "median", "centroid" ) - -#' Min Support -#' -#' Used in all `tidyclust::freq_itemsets()` models. -#' -#' @inheritParams dials::Laplace -#' @examples -#' min_support() -#' @export -min_support <- function(range = c(0.1, 0.5), trans = NULL) { - dials::new_quant_param( - type = "double", - range = range, - inclusive = c(TRUE, TRUE), - trans = trans, - label = c(min_support = "Minimum Support Value"), - finalize = NULL # Add to look at data and create a CI-like range around some min support value - ) -} diff --git a/man/min_support.Rd b/man/min_support.Rd deleted file mode 100644 index 6d1bf9b..0000000 --- a/man/min_support.Rd +++ /dev/null @@ -1,24 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/dials-params.R -\name{min_support} -\alias{min_support} -\title{Min Support} -\usage{ -min_support(range = c(0.1, 0.5), trans = NULL) -} -\arguments{ -\item{range}{A two-element vector holding the \emph{defaults} for the smallest and -largest possible values, respectively. If a transformation is specified, -these values should be in the \emph{transformed units}.} - -\item{trans}{A \code{trans} object from the \code{scales} package, such as -\code{scales::transform_log10()} or \code{scales::transform_reciprocal()}. If not provided, -the default is used which matches the units used in \code{range}. If no -transformation, \code{NULL}.} -} -\description{ -Used in all \code{tidyclust::freq_itemsets()} models. -} -\examples{ -min_support() -} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index cebb366..691d372 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -272,11 +272,9 @@ function `augment_itemset_predict()`. ```{r} # Generate data to predict on na_result <- tidyclust:::random_na_with_truth(groceries[1:5,], na_prob = 0.3) -pred_output <- predict(fi_fit, na_result$na_data) +new_data <- na_result$na_data # In a real scenario, this would be new, untrained on, data truth_output <- na_result$truth # In a real scenario, this would be a separate holdout set -comparison_df <- augment_itemset_predict(pred_output, truth_output) - # Example for RMSE (using type = 'raw') fi_fit %>% predict(new_data = new_data, type = 'raw') %>% @@ -326,7 +324,7 @@ itemset has a support of at least 50%. ```{r} dials::grid_regular( - min_support(), + dials::min_support(), levels = 10 ) ``` From fe2537dc388250a549377b25ddffb6736aebf22d Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 19 Jun 2025 13:30:47 -0700 Subject: [PATCH 59/71] re-ran test cases --- man/details_k_means_ClusterR.Rd | 6 +++--- man/set_args.cluster_spec.Rd | 2 +- man/set_engine.cluster_spec.Rd | 2 +- man/set_mode.cluster_spec.Rd | 2 +- tests/testthat/_snaps/extract_cluster_assignment.md | 3 ++- tests/testthat/_snaps/freq_itemsets.md | 4 ++-- tests/testthat/_snaps/predict.md | 3 ++- tests/testthat/_snaps/tune_cluster.md | 8 ++++---- 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/man/details_k_means_ClusterR.Rd b/man/details_k_means_ClusterR.Rd index 10236bf..32dab2b 100644 --- a/man/details_k_means_ClusterR.Rd +++ b/man/details_k_means_ClusterR.Rd @@ -82,9 +82,9 @@ This model has 1 tuning parameters: \subsection{Translation from tidyclust to the original package (partition)}{ -\if{html}{\out{

}}\preformatted{k_means(num_clusters = integer(1)) \%>\% - set_engine("ClusterR") \%>\% - set_mode("partition") \%>\% +\if{html}{\out{
}}\preformatted{k_means(num_clusters = integer(1)) |> + set_engine("ClusterR") |> + set_mode("partition") |> translate_tidyclust() }\if{html}{\out{
}} diff --git a/man/set_args.cluster_spec.Rd b/man/set_args.cluster_spec.Rd index d36e8a9..a9143d2 100644 --- a/man/set_args.cluster_spec.Rd +++ b/man/set_args.cluster_spec.Rd @@ -7,7 +7,7 @@ \method{set_args}{cluster_spec}(object, ...) } \arguments{ -\item{object}{A \link[parsnip:model_spec]{model specification}.} +\item{object}{A model specification.} \item{...}{One or more named model arguments.} } diff --git a/man/set_engine.cluster_spec.Rd b/man/set_engine.cluster_spec.Rd index f0600ff..cfd1412 100644 --- a/man/set_engine.cluster_spec.Rd +++ b/man/set_engine.cluster_spec.Rd @@ -7,7 +7,7 @@ \method{set_engine}{cluster_spec}(object, engine, ...) } \arguments{ -\item{object}{A \link[parsnip:model_spec]{model specification}.} +\item{object}{A model specification.} \item{engine}{A character string for the software that should be used to fit the model. This is highly dependent on the type diff --git a/man/set_mode.cluster_spec.Rd b/man/set_mode.cluster_spec.Rd index 03d6b7d..226c64e 100644 --- a/man/set_mode.cluster_spec.Rd +++ b/man/set_mode.cluster_spec.Rd @@ -7,7 +7,7 @@ \method{set_mode}{cluster_spec}(object, mode, ...) } \arguments{ -\item{object}{A \link[parsnip:model_spec]{model specification}.} +\item{object}{A model specification.} \item{mode}{A character string for the model type (e.g. "classification" or "regression")} diff --git a/tests/testthat/_snaps/extract_cluster_assignment.md b/tests/testthat/_snaps/extract_cluster_assignment.md index 4443b55..aa04341 100644 --- a/tests/testthat/_snaps/extract_cluster_assignment.md +++ b/tests/testthat/_snaps/extract_cluster_assignment.md @@ -39,5 +39,6 @@ fi_spec %>% extract_cluster_assignment() Condition Error in `extract_cluster_assignment()`: - ! This function requires a fitted model. Please use `fit()` on your cluster specification. + ! This function requires a fitted model. + i Please use `fit()` on your cluster specification. diff --git a/tests/testthat/_snaps/freq_itemsets.md b/tests/testthat/_snaps/freq_itemsets.md index 8727c87..e661050 100644 --- a/tests/testthat/_snaps/freq_itemsets.md +++ b/tests/testthat/_snaps/freq_itemsets.md @@ -3,8 +3,8 @@ Code freq_itemsets(mode = "bogus") Condition - Error in `modelenv::check_spec_mode_engine_val()`: - ! 'bogus' is not a known mode for model `freq_itemsets()`. + Error in `freq_itemsets()`: + ! "bogus" is not a known mode for model `freq_itemsets()`. --- diff --git a/tests/testthat/_snaps/predict.md b/tests/testthat/_snaps/predict.md index 17ca390..fcaac50 100644 --- a/tests/testthat/_snaps/predict.md +++ b/tests/testthat/_snaps/predict.md @@ -39,5 +39,6 @@ predict(spec) Condition Error in `predict()`: - ! This function requires a fitted model. Please use `fit()` on your cluster specification. + ! This function requires a fitted model. + i Please use `fit()` on your cluster specification. diff --git a/tests/testthat/_snaps/tune_cluster.md b/tests/testthat/_snaps/tune_cluster.md index 78f7054..64be851 100644 --- a/tests/testthat/_snaps/tune_cluster.md +++ b/tests/testthat/_snaps/tune_cluster.md @@ -79,10 +79,10 @@ 1 }, save_pred = TRUE)) Message - x Fold1: preprocessor 1/1: Error in `hardhat::mold()`: - ! The following predictor ... - x Fold2: preprocessor 1/1: Error in `hardhat::mold()`: - ! The following predictor ... + x Fold1: preprocessor 1/1: Error in `get_all_predictors()`: + ! The following predi... + x Fold2: preprocessor 1/1: Error in `get_all_predictors()`: + ! The following predi... Condition Warning: All models failed. From 96ad9f255c2608318d5bde2820afbc7ce5bf3767 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 19 Jun 2025 16:08:15 -0700 Subject: [PATCH 60/71] remove assoc_rules --- DESCRIPTION | 1 + NAMESPACE | 6 - R/assoc_rules.R | 192 ------------------------------ R/assoc_rules_arules.R | 12 -- man/assoc_rules.Rd | 54 --------- man/details_k_means_ClusterR.Rd | 65 +--------- man/dot-assoc_rules_fit_arules.Rd | 30 ----- man/tidyclust_update.Rd | 16 +-- 8 files changed, 5 insertions(+), 371 deletions(-) delete mode 100644 R/assoc_rules.R delete mode 100644 R/assoc_rules_arules.R delete mode 100644 man/assoc_rules.Rd delete mode 100644 man/dot-assoc_rules_fit_arules.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 2a4a273..38d4384 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -39,6 +39,7 @@ Imports: utils, vctrs (>= 0.5.0) Suggests: + arules, cluster, ClusterR, clustMixType (>= 0.3-5), diff --git a/NAMESPACE b/NAMESPACE index e0da8d3..a936c1a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,6 @@ S3method(as_tibble,cluster_metric_set) S3method(augment,cluster_fit) -S3method(check_args,assoc_rules) S3method(check_args,default) S3method(check_args,freq_itemsets) S3method(check_args,hier_clust) @@ -37,7 +36,6 @@ S3method(predict,cluster_fit) S3method(predict,cluster_spec) S3method(predict_cluster,cluster_fit) S3method(predict_raw,cluster_fit) -S3method(print,assoc_rules) S3method(print,cluster_fit) S3method(print,cluster_metric_set) S3method(print,cluster_spec) @@ -63,7 +61,6 @@ S3method(sse_within_total,cluster_fit) S3method(sse_within_total,cluster_spec) S3method(sse_within_total,workflow) S3method(tidy,cluster_fit) -S3method(translate_tidyclust,assoc_rules) S3method(translate_tidyclust,default) S3method(translate_tidyclust,freq_itemsets) S3method(translate_tidyclust,hier_clust) @@ -75,19 +72,16 @@ S3method(tune_args,cluster_spec) S3method(tune_cluster,cluster_spec) S3method(tune_cluster,default) S3method(tune_cluster,workflow) -S3method(update,assoc_rules) S3method(update,freq_itemsets) S3method(update,hier_clust) S3method(update,k_means) export("%>%") -export(.assoc_rules_fit_arules) export(.freq_itemsets_fit_arules) export(.hier_clust_fit_stats) export(.k_means_fit_ClusterR) export(.k_means_fit_clustMixType) export(.k_means_fit_klaR) export(.k_means_fit_stats) -export(assoc_rules) export(augment) export(augment_itemset_predict) export(cluster_metric_set) diff --git a/R/assoc_rules.R b/R/assoc_rules.R deleted file mode 100644 index b5d5fce..0000000 --- a/R/assoc_rules.R +++ /dev/null @@ -1,192 +0,0 @@ -#' Association Rules Mining -#' -#' @description -#' -#' `assoc_rules()` defines a model that finds association rules based on -#' specified minimum support and confidence. -#' -#' The method of estimation is chosen by setting the model engine. The -#' engine-specific pages for this model are listed below. -#' -#' - \link[=details_assoc_rules_stats]{arules} -#' -#' @param mode A single character string for the type of model. The only -#' possible value for this model is "association". -#' @param engine A single character string specifying the computational engine -#' to use for fitting. The default for this model is `"arules"`. -#' @param method A single character string specifying the algorithm to use for -#' fitting. Possible algorithms are `"apriori"` and `"eclat"`. The default for -#' this model is `"apriori"`. -#' @param min_support Positive double, minimum support for a rule (between 0 and 1). -#' @param min_confidence Positive double, minimum confidence for a rule (between 0 and 1). -#' -#' @details -#' -#' ## What does it mean to predict? -#' -#' WORK IN PROGRESS -#' -#' @return A `assoc_rules` association specification. -#' -#' @examples -#' # Show all engines -#' modelenv::get_from_env("assoc_rules") -#' -#' assoc_rules() -#' @export -assoc_rules <- - function(mode = "partition", - engine = "arules", - method = "apriori", - min_support = NULL, - min_confidence = NULL) { - args <- list( - min_support = enquo(min_support), - min_confidence = enquo(min_confidence) - ) - - new_cluster_spec( - "assoc_rules", - args = args, - eng_args = NULL, - mode = mode, - method = NULL, - engine = engine - ) - } - -#' @export -print.assoc_rules <- function(x, ...) { - cat("Association Rules Mining Specification (", x$mode, ")\n\n", sep = "") - model_printer(x, ...) - - if (!is.null(x$method$fit$args)) { - cat("Model fit template:\n") - print(show_call(x)) - } - - invisible(x) -} - -# ------------------------------------------------------------------------------ - -#' @method update assoc_rules -#' @rdname tidyclust_update -#' @export -update.assoc_rules <- function(object, - parameters = NULL, - min_support = NULL, - min_confidence = NULL, - fresh = FALSE, ...) { - eng_args <- parsnip::update_engine_parameters( - object$eng_args, - fresh = fresh, ... - ) - - if (!is.null(parameters)) { - parameters <- parsnip::check_final_param(parameters) - } - args <- list( - min_support = enquo(min_support), - min_confidence = enquo(min_confidence) - ) - - args <- parsnip::update_main_parameters(args, parameters) - - if (fresh) { - object$args <- args - object$eng_args <- eng_args - } else { - null_args <- map_lgl(args, null_value) - if (any(null_args)) { - args <- args[!null_args] - } - if (length(args) > 0) { - object$args[names(args)] <- args - } - if (length(eng_args) > 0) { - object$eng_args[names(eng_args)] <- eng_args - } - } - - new_cluster_spec( - "assoc_rules", - args = object$args, - eng_args = object$eng_args, - mode = object$mode, - method = NULL, - engine = object$engine - ) -} - -# # ---------------------------------------------------------------------------- - -#' @export -check_args.assoc_rules <- function(object) { - args <- lapply(object$args, rlang::eval_tidy) - - if (all(is.numeric(args$min_support)) && any(args$min_support >= 0) && any(args$min_support <= 1)) { - rlang::abort("The minimum support should be between 0 and 1.") - } - - if (all(is.numeric(args$min_confidence)) && any(args$min_confidence >= 0) && any(args$min_confidence <= 1)) { - rlang::abort("The minimum confidence should be between 0 and 1.") - } - - invisible(object) -} - -#' @export -translate_tidyclust.assoc_rules <- function(x, engine = x$engine, ...) { - x <- translate_tidyclust.default(x, engine, ...) - x -} - -# ------------------------------------------------------------------------------ - -#' Simple Wrapper around arules functions -#' -#' This wrapper prepares the data and parameters to send to either `arules::apriori` -#' or `arules::eclat` for association rules mining, depending on the chosen method. -#' -#' @param data A transaction data set. -#' @param support Minimum support threshold. -#' @param confidence Minimum confidence threshold. -#' @param mining_method Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat". -#' -#' @return A set of association rules based on the specified parameters. -#' @keywords internal -#' @export -.assoc_rules_fit_arules <- function(x, - support = NULL, - confidence = NULL, - mining_method = "apriori") { - - if (is.null(support)) { - rlang::abort( - "Please specify `min_support` to be able to fit specification.", - call = call("fit") - ) - } - - if (is.null(confidence)) { - rlang::abort( - "Please specify `min_confidence` to be able to fit specification.", - call = call("fit") - ) - } - - if (mining_method == "apriori") { - res <- arules::apriori(data, parameter = list(support = support, confidence = confidence, target = "rules")) - } else if (mining_method == "eclat") { - # Run Eclat first to get frequent itemsets - frequent_itemsets <- arules::eclat(data, parameter = list(support = support)) - # Generate association rules from frequent itemsets - res <- arules::ruleInduction(frequent_itemsets, confidence = confidence, method = "ptree") - } else { - stop("Invalid engine specified. Choose 'apriori' or 'eclat'.") - } - - attr(res, "items") <- colnames(data) - return(res) -} diff --git a/R/assoc_rules_arules.R b/R/assoc_rules_arules.R deleted file mode 100644 index 8c53e6e..0000000 --- a/R/assoc_rules_arules.R +++ /dev/null @@ -1,12 +0,0 @@ -#' K-means via ClusterR -#' -#' [k_means()] creates K-means model. This engine uses the classical definition -#' of a K-means model, which only takes numeric predictors. -#' -#' @includeRmd man/rmd/k_means_ClusterR.md details -#' -#' @name details_k_means_ClusterR -#' @keywords internal -NULL - -# See inst/README-DOCS.md for a description of how these files are processed diff --git a/man/assoc_rules.Rd b/man/assoc_rules.Rd deleted file mode 100644 index 8170e63..0000000 --- a/man/assoc_rules.Rd +++ /dev/null @@ -1,54 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/assoc_rules.R -\name{assoc_rules} -\alias{assoc_rules} -\title{Association Rules Mining} -\usage{ -assoc_rules( - mode = "partition", - engine = "arules", - method = "apriori", - min_support = NULL, - min_confidence = NULL -) -} -\arguments{ -\item{mode}{A single character string for the type of model. The only -possible value for this model is "association".} - -\item{engine}{A single character string specifying the computational engine -to use for fitting. The default for this model is \code{"arules"}.} - -\item{method}{A single character string specifying the algorithm to use for -fitting. Possible algorithms are \code{"apriori"} and \code{"eclat"}. The default for -this model is \code{"apriori"}.} - -\item{min_support}{Positive double, minimum support for a rule (between 0 and 1).} - -\item{min_confidence}{Positive double, minimum confidence for a rule (between 0 and 1).} -} -\value{ -A \code{assoc_rules} association specification. -} -\description{ -\code{assoc_rules()} defines a model that finds association rules based on -specified minimum support and confidence. - -The method of estimation is chosen by setting the model engine. The -engine-specific pages for this model are listed below. -\itemize{ -\item \link[=details_assoc_rules_stats]{arules} -} -} -\details{ -\subsection{What does it mean to predict?}{ - -WORK IN PROGRESS -} -} -\examples{ -# Show all engines -modelenv::get_from_env("assoc_rules") - -assoc_rules() -} diff --git a/man/details_k_means_ClusterR.Rd b/man/details_k_means_ClusterR.Rd index 32dab2b..d6d961a 100644 --- a/man/details_k_means_ClusterR.Rd +++ b/man/details_k_means_ClusterR.Rd @@ -1,12 +1,9 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/assoc_rules_arules.R, R/k_means_ClusterR.R +% Please edit documentation in R/k_means_ClusterR.R \name{details_k_means_ClusterR} \alias{details_k_means_ClusterR} \title{K-means via ClusterR} \description{ -\code{\link[=k_means]{k_means()}} creates K-means model. This engine uses the classical definition -of a K-means model, which only takes numeric predictors. - \code{\link[=k_means]{k_means()}} creates K-means model. This engine uses the classical definition of a K-means model, which only takes numeric predictors. } @@ -53,66 +50,6 @@ center and scale each so that each predictor has mean zero and a variance of one. } -\subsection{References}{ -\itemize{ -\item Forgy, E. W. (1965). Cluster analysis of multivariate data: efficiency -vs interpretability of classifications. Biometrics, 21, 768–769. -\item Hartigan, J. A. and Wong, M. A. (1979). Algorithm AS 136: A K-means -clustering algorithm. Applied Statistics, 28, 100–108. -\url{doi:10.2307/2346830}. -\item Lloyd, S. P. (1957, 1982). Least squares quantization in PCM. -Technical Note, Bell Laboratories. Published in 1982 in IEEE -Transactions on Information Theory, 28, 128–137. -\item MacQueen, J. (1967). Some methods for classification and analysis of -multivariate observations. In Proceedings of the Fifth Berkeley -Symposium on Mathematical Statistics and Probability, eds L. M. Le Cam -& J. Neyman, 1, pp. 281–297. Berkeley, CA: University of California -Press. -} -} - -For this engine, there is a single mode: partition -\subsection{Tuning Parameters}{ - -This model has 1 tuning parameters: -\itemize{ -\item \code{num_clusters}: # Clusters (type: integer, default: no default) -} -} - -\subsection{Translation from tidyclust to the original package (partition)}{ - -\if{html}{\out{
}}\preformatted{k_means(num_clusters = integer(1)) |> - set_engine("ClusterR") |> - set_mode("partition") |> - translate_tidyclust() -}\if{html}{\out{
}} - -\if{html}{\out{
}}\preformatted{## K Means Cluster Specification (partition) -## -## Main Arguments: -## num_clusters = integer(1) -## -## Computational engine: ClusterR -## -## Model fit template: -## tidyclust::.k_means_fit_ClusterR(data = missing_arg(), clusters = missing_arg(), -## clusters = integer(1)) -}\if{html}{\out{
}} -} - -\subsection{Preprocessing requirements}{ - -Factor/categorical predictors need to be converted to numeric values -(e.g., dummy or indicator variables) for this engine. When using the -formula method via \code{\link[=fit.cluster_spec]{fit()}}, tidyclust -will convert factor columns to indicators. - -Predictors should have the same scale. One way to achieve this is to -center and scale each so that each predictor has mean zero and a -variance of one. -} - \subsection{References}{ \itemize{ \item Forgy, E. W. (1965). Cluster analysis of multivariate data: efficiency diff --git a/man/dot-assoc_rules_fit_arules.Rd b/man/dot-assoc_rules_fit_arules.Rd deleted file mode 100644 index cb202ea..0000000 --- a/man/dot-assoc_rules_fit_arules.Rd +++ /dev/null @@ -1,30 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/assoc_rules.R -\name{.assoc_rules_fit_arules} -\alias{.assoc_rules_fit_arules} -\title{Simple Wrapper around arules functions} -\usage{ -.assoc_rules_fit_arules( - x, - support = NULL, - confidence = NULL, - mining_method = "apriori" -) -} -\arguments{ -\item{support}{Minimum support threshold.} - -\item{confidence}{Minimum confidence threshold.} - -\item{mining_method}{Algorithm to use for mining frequent itemsets. Either "apriori" or "eclat".} - -\item{data}{A transaction data set.} -} -\value{ -A set of association rules based on the specified parameters. -} -\description{ -This wrapper prepares the data and parameters to send to either \code{arules::apriori} -or \code{arules::eclat} for association rules mining, depending on the chosen method. -} -\keyword{internal} diff --git a/man/tidyclust_update.Rd b/man/tidyclust_update.Rd index 405ac46..00357de 100644 --- a/man/tidyclust_update.Rd +++ b/man/tidyclust_update.Rd @@ -1,23 +1,13 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/assoc_rules.R, R/freq_itemsets.R, -% R/hier_clust.R, R/k_means.R, R/update.R -\name{update.assoc_rules} -\alias{update.assoc_rules} +% Please edit documentation in R/freq_itemsets.R, R/hier_clust.R, R/k_means.R, +% R/update.R +\name{update.freq_itemsets} \alias{update.freq_itemsets} \alias{update.hier_clust} \alias{update.k_means} \alias{tidyclust_update} \title{Update a cluster specification} \usage{ -\method{update}{assoc_rules}( - object, - parameters = NULL, - min_support = NULL, - min_confidence = NULL, - fresh = FALSE, - ... -) - \method{update}{freq_itemsets}( object, parameters = NULL, From 6e5a28fd5139e15e217b72598e8e5ce7781eb53c Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 14:50:48 -0700 Subject: [PATCH 61/71] Add the following .pred_item item preds row_id setNames truth_value to utils::globalVariables() in aaa.R --- R/aaa.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/R/aaa.R b/R/aaa.R index db45641..b36ee89 100644 --- a/R/aaa.R +++ b/R/aaa.R @@ -10,6 +10,7 @@ utils::globalVariables( ".iter_model", ".iter_preprocessor", ".msg_model", + ".pred_item", ".submodels", "call_info", "cluster", @@ -23,6 +24,7 @@ utils::globalVariables( "exposed", "func", "id", + "item", "iteration", "lab", "name", @@ -32,10 +34,14 @@ utils::globalVariables( "orig_label", "original", "predictor_indicators", + "preds", "remove_intercept", + "row_id", "seed", + "setNames", "sil_width", "splits", + "truth_value", "tunable", "type", "value", From 1ae32d8e822e87ac256f7828af97e9661db2a718 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 15:21:34 -0700 Subject: [PATCH 62/71] rename `extract_predictions` to `extract_itemset_predictions` add example to `extract_itemset_predictions` --- NAMESPACE | 2 +- ...ctions.R => extract_itemset_predictions.R} | 36 +++++++++++- man/extract_itemset_predictions.Rd | 55 +++++++++++++++++++ man/extract_predictions.Rd | 21 ------- man/freq_itemsets.Rd | 2 +- vignettes/articles/freq_itemsets.Rmd | 4 +- 6 files changed, 94 insertions(+), 26 deletions(-) rename R/{extract_predictions.R => extract_itemset_predictions.R} (58%) create mode 100644 man/extract_itemset_predictions.Rd delete mode 100644 man/extract_predictions.Rd diff --git a/NAMESPACE b/NAMESPACE index a936c1a..4f1acdd 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -92,8 +92,8 @@ export(extract_cluster_assignment) export(extract_fit_engine) export(extract_fit_parsnip) export(extract_fit_summary) +export(extract_itemset_predictions) export(extract_parameter_set_dials) -export(extract_predictions) export(extract_preprocessor) export(extract_spec_parsnip) export(finalize_model_tidyclust) diff --git a/R/extract_predictions.R b/R/extract_itemset_predictions.R similarity index 58% rename from R/extract_predictions.R rename to R/extract_itemset_predictions.R index 1b57415..bf168cb 100644 --- a/R/extract_predictions.R +++ b/R/extract_itemset_predictions.R @@ -7,9 +7,43 @@ #' #' @param pred_output A data frame with one column, where each cell contains a data frame. #' @return A data frame with items as columns and non-NA values as rows. +#' +#' @examples +#' toy_df <- data.frame( +#' "beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), +#' "milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), +#' "bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), +#' "diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), +#' "eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) +#' ) +#' +#' new_data <- data.frame( +#' "beer" = NA, +#' "milk" = TRUE, +#' "bread" = TRUE, +#' "diapers" = TRUE, +#' "eggs" = FALSE +#' ) +#' +#' fi_spec <- freq_itemsets( +#' min_support = 0.05, +#' mining_method = "eclat" +#' ) |> +#' set_engine("arules") |> +#' set_mode("partition") +#' +#' fi_fit <- fi_spec |> +#' fit(~ ., +#' data = toy_df +#' ) +#' +#' fi_fit |> +#' predict(new_data) |> +#' extract_itemset_predictions() +#' #' @export -extract_predictions <- function(pred_output) { +extract_itemset_predictions <- function(pred_output) { # Extract the list of data frames from the single column data_frames <- pred_output$.pred_cluster diff --git a/man/extract_itemset_predictions.Rd b/man/extract_itemset_predictions.Rd new file mode 100644 index 0000000..08fc414 --- /dev/null +++ b/man/extract_itemset_predictions.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_itemset_predictions.R +\name{extract_itemset_predictions} +\alias{extract_itemset_predictions} +\title{Extract Predictions from Observation Data Frames} +\usage{ +extract_itemset_predictions(pred_output) +} +\arguments{ +\item{pred_output}{A data frame with one column, where each cell contains a data frame.} +} +\value{ +A data frame with items as columns and non-NA values as rows. +} +\description{ +This function processes a data frame containing observation data frames and extracts non-NA values. +} +\details{ +Returns recommender predictions with predicted values imputed into dataset +Notes: currently imputes thresholded probabilities +} +\examples{ +toy_df <- data.frame( +"beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), +"milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), +"bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), +"diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), +"eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) +) + +new_data <- data.frame( +"beer" = NA, +"milk" = TRUE, +"bread" = TRUE, +"diapers" = TRUE, +"eggs" = FALSE +) + +fi_spec <- freq_itemsets( + min_support = 0.05, + mining_method = "eclat" + ) |> + set_engine("arules") |> + set_mode("partition") + +fi_fit <- fi_spec |> + fit(~ ., + data = toy_df + ) + +fi_fit |> + predict(new_data) |> + extract_itemset_predictions() + +} diff --git a/man/extract_predictions.Rd b/man/extract_predictions.Rd deleted file mode 100644 index f981113..0000000 --- a/man/extract_predictions.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/extract_predictions.R -\name{extract_predictions} -\alias{extract_predictions} -\title{Extract Predictions from Observation Data Frames} -\usage{ -extract_predictions(pred_output) -} -\arguments{ -\item{pred_output}{A data frame with one column, where each cell contains a data frame.} -} -\value{ -A data frame with items as columns and non-NA values as rows. -} -\description{ -This function processes a data frame containing observation data frames and extracts non-NA values. -} -\details{ -Returns recommender predictions with predicted values imputed into dataset -Notes: currently imputes thresholded probabilities -} diff --git a/man/freq_itemsets.Rd b/man/freq_itemsets.Rd index 06851da..6bf8fbb 100644 --- a/man/freq_itemsets.Rd +++ b/man/freq_itemsets.Rd @@ -54,7 +54,7 @@ If no relevant itemsets are found, the item's global support from the training d The \code{predict()} output provides a nested data frame per transaction, including \code{item}, \code{.obs_item} (observed status), and \code{.pred_item} (predicted values). -The \code{extract_predictions()} helper function can reformat this nested output into a single data frame. +The \code{extract_itemset_predictions()} helper function can reformat this nested output into a single data frame. } } \examples{ diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 691d372..4a9b7f2 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -251,11 +251,11 @@ the `:::`. Additionally, we can extract the nested predicted output to be formatted in a single data frame, filling in the `NA` values with their predicted value using -`extract_predictions()`. +`extract_itemset_predictions()`. ```{r} results %>% - extract_predictions + extract_itemset_predictions ``` ## Evaluation Metrics From f808e10856b611744ad9e2cb04e5f9ad55f7c5ed Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 15:21:52 -0700 Subject: [PATCH 63/71] rename `extract_predictions` to `extract_itemset_predictions` --- R/freq_itemsets.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index a193ab0..f102fa2 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -37,7 +37,7 @@ #' #' The `predict()` output provides a nested data frame per transaction, including `item`, #' `.obs_item` (observed status), and `.pred_item` (predicted values). -#' The `extract_predictions()` helper function can reformat this nested output into a single data frame. +#' The `extract_itemset_predictions()` helper function can reformat this nested output into a single data frame. #' #' @return A `freq_itemsets` association specification. #' From 062961829bdd288c61a2427d5f120484d8d52e68 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 15:22:32 -0700 Subject: [PATCH 64/71] Add exported functions to _pkgdown.yml --- _pkgdown.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 7749c25..135ffe3 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -24,6 +24,7 @@ reference: - hier_clust - cluster_spec - cluster_fit + - freq_itemsets - title: Fit and Inspect desc: > These functions are the generics that are supported for specifications @@ -43,9 +44,11 @@ reference: at where the clusters are and which observations are associated with which cluster. contents: + - augment_itemset_predict - predict.cluster_fit - extract_cluster_assignment - extract_centroids + - extract_itemset_predictions - title: Model based performance metrics desc: > These metrics use the fitted clustering model to extract values denoting how From e22c3c661c28aa14b3f2bd1b8d30878b95376a17 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 15:44:19 -0700 Subject: [PATCH 65/71] convert all rlang::abort() calls to use {cli} --- R/extract_fit_summary.R | 11 +++++------ R/freq_itemsets.R | 9 ++++----- tests/testthat/_snaps/extract_centroids.md | 5 ++--- tests/testthat/_snaps/freq_itemsets-arules.md | 5 ++--- tests/testthat/_snaps/freq_itemsets.md | 12 ++++++------ tests/testthat/test-freq_itemsets.R | 6 +++--- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/R/extract_fit_summary.R b/R/extract_fit_summary.R index 5fd7a19..5e3e9c0 100644 --- a/R/extract_fit_summary.R +++ b/R/extract_fit_summary.R @@ -196,11 +196,10 @@ extract_fit_summary.hclust <- function(object, ...) { #' @export extract_fit_summary.itemsets <- function(object, ..., call = rlang::caller_env(n = 0)) { - rlang::abort( - paste( - "Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly.\n", - "Please use arules::inspect() on the fit of your cluster specification." - ), - call = call + cli::cli_abort( + "Centroids are not usfeul for frequent itemsets, we suggust looking at the + frequent itemsets directly.\n Please use arules::inspect() on the fit of + your cluster specification." ) + } diff --git a/R/freq_itemsets.R b/R/freq_itemsets.R index f102fa2..8738b9b 100644 --- a/R/freq_itemsets.R +++ b/R/freq_itemsets.R @@ -138,12 +138,12 @@ check_args.freq_itemsets <- function(object) { args <- lapply(object$args, rlang::eval_tidy) if (all(is.numeric(args$min_support)) && (any(args$min_support < 0) || any(args$min_support > 1))) { - rlang::abort("The minimum support should be between 0 and 1.") + cli::cli_abort("The minimum support should be between 0 and 1.") } if (all(is.character(args$mining_method)) && !all(args$mining_method %in% c("apriori", "eclat"))) { - rlang::abort("The mining method should be either 'apriori' or 'eclat'.") + cli::cli_abort("The mining method should be either 'apriori' or 'eclat'.") } invisible(object) @@ -174,9 +174,8 @@ translate_tidyclust.freq_itemsets <- function(x, engine = x$engine, ...) { mining_method = NULL) { if (is.null(min_support)) { - rlang::abort( - "Please specify `min_support` to be able to fit specification.", - call = call("fit") + cli::cli_abort( + "Please specify `min_support` to be able to fit specification." ) } diff --git a/tests/testthat/_snaps/extract_centroids.md b/tests/testthat/_snaps/extract_centroids.md index a89ea47..bb937d4 100644 --- a/tests/testthat/_snaps/extract_centroids.md +++ b/tests/testthat/_snaps/extract_centroids.md @@ -38,7 +38,6 @@ Code extract_centroids(fi_fit) Condition - Error in `extract_centroids()`: - ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. - Please use arules::inspect() on the fit of your cluster specification. + Error in `extract_fit_summary()`: + ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. Please use arules::inspect() on the fit of your cluster specification. diff --git a/tests/testthat/_snaps/freq_itemsets-arules.md b/tests/testthat/_snaps/freq_itemsets-arules.md index 3da7f81..00ad3ea 100644 --- a/tests/testthat/_snaps/freq_itemsets-arules.md +++ b/tests/testthat/_snaps/freq_itemsets-arules.md @@ -3,7 +3,6 @@ Code extract_centroids(fi_fit) Condition - Error in `extract_centroids()`: - ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. - Please use arules::inspect() on the fit of your cluster specification. + Error in `extract_fit_summary()`: + ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. Please use arules::inspect() on the fit of your cluster specification. diff --git a/tests/testthat/_snaps/freq_itemsets.md b/tests/testthat/_snaps/freq_itemsets.md index e661050..365c473 100644 --- a/tests/testthat/_snaps/freq_itemsets.md +++ b/tests/testthat/_snaps/freq_itemsets.md @@ -9,7 +9,7 @@ --- Code - bt <- freq_itemsets(mining_method = "bogus") + bt <- freq_itemsets(min_support = 0.05, mining_method = "bogus") fit(bt, ~., toy_df) Condition Error in `check_args()`: @@ -18,7 +18,8 @@ --- Code - bt <- freq_itemsets(min_support = -1) %>% set_engine("arules") + bt <- freq_itemsets(min_support = -1, mining_method = "eclat") %>% set_engine( + "arules") fit(bt, ~., toy_df) Condition Error in `check_args()`: @@ -45,9 +46,8 @@ Code extract_centroids(fi_fit) Condition - Error in `extract_centroids()`: - ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. - Please use arules::inspect() on the fit of your cluster specification. + Error in `extract_fit_summary()`: + ! Centroids are not usfeul for frequent itemsets, we suggust looking at the frequent itemsets directly. Please use arules::inspect() on the fit of your cluster specification. # printing @@ -95,6 +95,6 @@ Code freq_itemsets() %>% set_engine("arules") %>% fit(~., data = toy_df) Condition - Error in `fit()`: + Error in `tidyclust::.freq_itemsets_fit_arules()`: ! Please specify `min_support` to be able to fit specification. diff --git a/tests/testthat/test-freq_itemsets.R b/tests/testthat/test-freq_itemsets.R index cff1a28..62dd64e 100644 --- a/tests/testthat/test-freq_itemsets.R +++ b/tests/testthat/test-freq_itemsets.R @@ -40,13 +40,13 @@ test_that("primary arguments", { test_that("bad input", { expect_snapshot(error = TRUE, freq_itemsets(mode = "bogus")) expect_snapshot(error = TRUE, { - bt <- freq_itemsets(mining_method = "bogus") + bt <- freq_itemsets(min_support = 0.05, mining_method = "bogus") fit(bt, ~ ., toy_df) }) expect_snapshot(error = TRUE, { - bt <- freq_itemsets(min_support = -1) %>% set_engine("arules") + bt <- freq_itemsets(min_support = -1, mining_method = "eclat") %>% set_engine("arules") fit(bt, ~ ., toy_df) - }) + }) expect_snapshot(error = TRUE, translate_tidyclust(freq_itemsets(), engine = NULL)) expect_snapshot(error = TRUE, translate_tidyclust(freq_itemsets(formula = ~x))) }) From 842f8d7a0f585573b667ba86c101ca882457ecb6 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 15:48:11 -0700 Subject: [PATCH 66/71] edit toy_df and toy_pred to use " instead of ' and TRUE/FALSE instead of T/F --- tests/testthat/test-extract_centroids.R | 10 +++++----- .../test-extract_cluster_assignment.R | 10 +++++----- tests/testthat/test-freq_itemsets-arules.R | 20 +++++++++---------- tests/testthat/test-freq_itemsets.R | 20 +++++++++---------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/testthat/test-extract_centroids.R b/tests/testthat/test-extract_centroids.R index 78b9de5..22ca19b 100644 --- a/tests/testthat/test-extract_centroids.R +++ b/tests/testthat/test-extract_centroids.R @@ -1,9 +1,9 @@ toy_df <- data.frame( - 'beer' = c(F, T, T, T, F), - 'milk' = c(T, F, T, T, T), - 'bread' = c(T, T, F, T, T), - 'diapers' = c(T, T, T, T, T), - 'eggs' = c(F, T, F, F, F) + "beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), + "milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), + "bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), + "diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), + "eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) ) test_that("extract_centroids() errors for cluster spec", { diff --git a/tests/testthat/test-extract_cluster_assignment.R b/tests/testthat/test-extract_cluster_assignment.R index dd706b3..768b128 100644 --- a/tests/testthat/test-extract_cluster_assignment.R +++ b/tests/testthat/test-extract_cluster_assignment.R @@ -1,9 +1,9 @@ toy_df <- data.frame( - 'beer' = c(F, T, T, T, F), - 'milk' = c(T, F, T, T, T), - 'bread' = c(T, T, F, T, T), - 'diapers' = c(T, T, T, T, T), - 'eggs' = c(F, T, F, F, F) + "beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), + "milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), + "bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), + "diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), + "eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) ) test_that("extract_cluster_assignment() errors for cluster spec", { diff --git a/tests/testthat/test-freq_itemsets-arules.R b/tests/testthat/test-freq_itemsets-arules.R index d74e991..fc24ed6 100644 --- a/tests/testthat/test-freq_itemsets-arules.R +++ b/tests/testthat/test-freq_itemsets-arules.R @@ -1,17 +1,17 @@ toy_df <- data.frame( - 'beer' = c(F, T, T, T, F), - 'milk' = c(T, F, T, T, T), - 'bread' = c(T, T, F, T, T), - 'diapers' = c(T, T, T, T, T), - 'eggs' = c(F, T, F, F, F) + "beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), + "milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), + "bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), + "diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), + "eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) ) toy_pred <- data.frame( - 'beer' = c(F), - 'milk' = c(NA), - 'bread' = c(T), - 'diapers' = c(T), - 'eggs' = c(F) + "beer" = FALSE, + "milk" = NA, + "bread" = TRUE, + "diapers" = TRUE, + "eggs" = FALSE ) test_that("fitting", { diff --git a/tests/testthat/test-freq_itemsets.R b/tests/testthat/test-freq_itemsets.R index 62dd64e..59416e4 100644 --- a/tests/testthat/test-freq_itemsets.R +++ b/tests/testthat/test-freq_itemsets.R @@ -1,17 +1,17 @@ toy_df <- data.frame( - 'beer' = c(F, T, T, T, F), - 'milk' = c(T, F, T, T, T), - 'bread' = c(T, T, F, T, T), - 'diapers' = c(T, T, T, T, T), - 'eggs' = c(F, T, F, F, F) + "beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), + "milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), + "bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), + "diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), + "eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) ) toy_pred <- data.frame( - 'beer' = c(F), - 'milk' = c(NA), - 'bread' = c(T), - 'diapers' = c(T), - 'eggs' = c(F) + "beer" = FALSE, + "milk" = NA, + "bread" = TRUE, + "diapers" = TRUE, + "eggs" = FALSE ) test_that("primary arguments", { From 305b0789d44eb646558cf769e79d28bff24e1cac Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 16:17:02 -0700 Subject: [PATCH 67/71] add example to `augment_itemset_predict` --- R/augment_itemset_predict.R | 53 +++++++++++++++++++++++++++- man/augment_itemset_predict.Rd | 48 +++++++++++++++++++++++++ vignettes/articles/freq_itemsets.Rmd | 4 +-- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/R/augment_itemset_predict.R b/R/augment_itemset_predict.R index ee49d12..6827de5 100644 --- a/R/augment_itemset_predict.R +++ b/R/augment_itemset_predict.R @@ -28,6 +28,54 @@ #' \item `truth`: The true value for the item from `truth_output`. #' } #' This output is suitable for direct use with `yardstick` metric functions. +#' +#' @examples +#' toy_df <- data.frame( +#' "beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), +#' "milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), +#' "bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), +#' "diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), +#' "eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) +#' ) +#' +#' new_data <- data.frame( +#' "beer" = NA, +#' "milk" = TRUE, +#' "bread" = TRUE, +#' "diapers" = TRUE, +#' "eggs" = FALSE +#' ) +#' +#' truth_df <- data.frame( +#' "beer" = FALSE, +#' "milk" = TRUE, +#' "bread" = TRUE, +#' "diapers" = TRUE, +#' "eggs" = FALSE +#' ) +#' +#' fi_spec <- freq_itemsets( +#' min_support = 0.05, +#' mining_method = "eclat" +#' ) |> +#' set_engine("arules") |> +#' set_mode("partition") +#' +#' fi_fit <- fi_spec |> +#' fit(~ ., +#' data = toy_df +#' ) +#' +#' aug_pred <- fi_fit |> +#' predict(new_data, type = "raw") |> +#' augment_itemset_predict(truth_output = truth_df) +#' +#' aug_pred +#' +#' # Example use of formatted output +#' aug_pred |> +#' yardstick::rmse(truth, preds) +#' #' @export augment_itemset_predict <- function(pred_output, truth_output) { @@ -35,6 +83,8 @@ augment_itemset_predict <- function(pred_output, truth_output) { preds_df <- dplyr::bind_rows(pred_output$.pred_cluster, .id = "row_id") %>% dplyr::filter(!is.na(.pred_item)) %>% # Keep only rows with predictions dplyr::mutate(item = stringr::str_remove_all(item, "`")) %>% # Remove backticks from item names + dplyr::mutate(item = stringr::str_remove_all(item, "TRUE")) %>% # Remove TRUE from item names + dplyr::mutate(item = stringr::str_remove_all(item, "FALSE")) %>% # Remove FALSE from item names dplyr::select(row_id, item, preds = .pred_item) # Standardize column names # Pivot truth data to long format (to match predictions) @@ -44,7 +94,8 @@ augment_itemset_predict <- function(pred_output, truth_output) { cols = -row_id, names_to = "item", values_to = "truth_value" - ) + ) %>% + dplyr::mutate(truth_value = as.numeric(truth_value)) # Join predictions with truth (inner join to keep only predicted items) result <- preds_df %>% diff --git a/man/augment_itemset_predict.Rd b/man/augment_itemset_predict.Rd index 4c7bfce..3bf2aa7 100644 --- a/man/augment_itemset_predict.Rd +++ b/man/augment_itemset_predict.Rd @@ -39,3 +39,51 @@ The \code{truth_output} is pivoted to a long format to match the structure of th Finally, an inner join is performed to ensure that only predicted items are included in the final result, aligning predictions with their corresponding true values. } +\examples{ +toy_df <- data.frame( +"beer" = c(FALSE, TRUE, TRUE, TRUE, FALSE), +"milk" = c(TRUE, FALSE, TRUE, TRUE, TRUE), +"bread" = c(TRUE, TRUE, FALSE, TRUE, TRUE), +"diapers" = c(TRUE, TRUE, TRUE, TRUE, TRUE), +"eggs" = c(FALSE, TRUE, FALSE, FALSE, FALSE) +) + +new_data <- data.frame( +"beer" = NA, +"milk" = TRUE, +"bread" = TRUE, +"diapers" = TRUE, +"eggs" = FALSE +) + +truth_df <- data.frame( +"beer" = FALSE, +"milk" = TRUE, +"bread" = TRUE, +"diapers" = TRUE, +"eggs" = FALSE +) + +fi_spec <- freq_itemsets( + min_support = 0.05, + mining_method = "eclat" + ) |> + set_engine("arules") |> + set_mode("partition") + +fi_fit <- fi_spec |> + fit(~ ., + data = toy_df + ) + +aug_pred <- fi_fit |> + predict(new_data, type = "raw") |> + augment_itemset_predict(truth_output = truth_df) + +aug_pred + +# Example use of formatted output +aug_pred |> + yardstick::rmse(truth, preds) + +} diff --git a/vignettes/articles/freq_itemsets.Rmd b/vignettes/articles/freq_itemsets.Rmd index 4a9b7f2..b4825c5 100644 --- a/vignettes/articles/freq_itemsets.Rmd +++ b/vignettes/articles/freq_itemsets.Rmd @@ -278,13 +278,13 @@ truth_output <- na_result$truth # In a real scenario, this would be a separate # Example for RMSE (using type = 'raw') fi_fit %>% predict(new_data = new_data, type = 'raw') %>% - augment_itemset_predict(truth = truth_output) %>% + augment_itemset_predict(truth_output = truth_output) %>% yardstick::rmse(truth, preds) # Example for Precision (using type = 'cluster') fi_fit %>% predict(new_data = new_data, type = 'cluster') %>% - augment_itemset_predict(truth = truth_output) %>% + augment_itemset_predict(truth_output = truth_output) %>% dplyr::mutate( truth = factor(truth, levels = c(0, 1)), preds = factor(preds, levels = c(0, 1)) From 5860141543fa5abe75dc15fa389c4ef47fae8105 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 16:20:56 -0700 Subject: [PATCH 68/71] add skip_if_not_installed("arules") to all tests that use freq_itemsets() --- tests/testthat/test-extract_centroids.R | 1 + tests/testthat/test-extract_cluster_assignment.R | 1 + tests/testthat/test-freq_itemsets-arules.R | 4 ++++ tests/testthat/test-freq_itemsets.R | 9 +++++++++ tests/testthat/test-predict.R | 1 + 5 files changed, 16 insertions(+) diff --git a/tests/testthat/test-extract_centroids.R b/tests/testthat/test-extract_centroids.R index 22ca19b..3608c19 100644 --- a/tests/testthat/test-extract_centroids.R +++ b/tests/testthat/test-extract_centroids.R @@ -75,6 +75,7 @@ test_that("prefix is passed in extract_centroids()", { test_that("extract_centroids errors for freq_itemsets", { set.seed(1234) + skip_if_not_installed("arules") fi_fit <- freq_itemsets(min_support = 0.5) %>% set_engine("arules") %>% fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) diff --git a/tests/testthat/test-extract_cluster_assignment.R b/tests/testthat/test-extract_cluster_assignment.R index 768b128..51a2249 100644 --- a/tests/testthat/test-extract_cluster_assignment.R +++ b/tests/testthat/test-extract_cluster_assignment.R @@ -74,6 +74,7 @@ test_that("prefix is passed in extract_cluster_assignment()", { }) test_that("extract_cluster_assignment() errors for freq_itemsets() cluster spec", { + skip_if_not_installed("arules") fi_spec <- freq_itemsets(min_support = 0.5) expect_snapshot( diff --git a/tests/testthat/test-freq_itemsets-arules.R b/tests/testthat/test-freq_itemsets-arules.R index fc24ed6..d2dda01 100644 --- a/tests/testthat/test-freq_itemsets-arules.R +++ b/tests/testthat/test-freq_itemsets-arules.R @@ -16,6 +16,7 @@ toy_pred <- data.frame( test_that("fitting", { set.seed(1234) + skip_if_not_installed("arules") spec <- freq_itemsets(min_support = 0.5) %>% set_engine("arules") @@ -26,6 +27,7 @@ test_that("fitting", { test_that("predicting", { set.seed(1234) + skip_if_not_installed("arules") spec <- freq_itemsets(min_support = 0.5) %>% set_engine("arules") @@ -41,6 +43,7 @@ test_that("predicting", { test_that("extract_centroids works", { set.seed(1234) + skip_if_not_installed("arules") fi_fit <- freq_itemsets(min_support = 0.5) %>% set_engine("arules") %>% fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) @@ -50,6 +53,7 @@ test_that("extract_centroids works", { test_that("extract_cluster_assignment() works", { set.seed(1234) + skip_if_not_installed("arules") fi_fit <- freq_itemsets(min_support = 0.5, mining_method = "eclat") %>% set_engine("arules") %>% fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) diff --git a/tests/testthat/test-freq_itemsets.R b/tests/testthat/test-freq_itemsets.R index 59416e4..63bb180 100644 --- a/tests/testthat/test-freq_itemsets.R +++ b/tests/testthat/test-freq_itemsets.R @@ -15,6 +15,7 @@ toy_pred <- data.frame( ) test_that("primary arguments", { + skip_if_not_installed("arules") basic <- freq_itemsets(mode = "partition") basic_arules <- translate_tidyclust(basic %>% set_engine("arules")) expect_equal( @@ -38,6 +39,7 @@ test_that("primary arguments", { }) test_that("bad input", { + skip_if_not_installed("arules") expect_snapshot(error = TRUE, freq_itemsets(mode = "bogus")) expect_snapshot(error = TRUE, { bt <- freq_itemsets(min_support = 0.05, mining_method = "bogus") @@ -53,6 +55,7 @@ test_that("bad input", { test_that("clusters", { set.seed(1234) + skip_if_not_installed("arules") fi_fit <- freq_itemsets(min_support = 0.5, mining_method = "apriori") %>% set_engine("arules") %>% fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) @@ -79,6 +82,7 @@ test_that("clusters", { test_that("predict", { set.seed(1234) + skip_if_not_installed("arules") fi_fit <- freq_itemsets(min_support = 0.5, mining_method = "apriori") %>% set_engine("arules") %>% fit(~., toy_df) @@ -99,6 +103,7 @@ test_that("predict", { test_that("extract_centroids work", { set.seed(1234) + skip_if_not_installed("arules") fi_fit <- freq_itemsets(min_support = 0.5) %>% set_engine("arules") %>% fit(~., toy_df %>% dplyr::mutate(across(everything(), as.numeric))) @@ -107,6 +112,7 @@ test_that("extract_centroids work", { }) test_that("Right classes", { + skip_if_not_installed("arules") expect_equal( class(freq_itemsets()), c("freq_itemsets", "cluster_spec", "unsupervised_spec") @@ -114,6 +120,7 @@ test_that("Right classes", { }) test_that("printing", { + skip_if_not_installed("arules") expect_snapshot( freq_itemsets() ) @@ -123,6 +130,7 @@ test_that("printing", { }) test_that("updating", { + skip_if_not_installed("arules") expect_snapshot( freq_itemsets(min_support = 0.5) %>% update(min_support = tune()) @@ -130,6 +138,7 @@ test_that("updating", { }) test_that("errors if `min_support` isn't specified", { + skip_if_not_installed("arules") expect_snapshot( error = TRUE, freq_itemsets() %>% diff --git a/tests/testthat/test-predict.R b/tests/testthat/test-predict.R index 5d016b7..406dad6 100644 --- a/tests/testthat/test-predict.R +++ b/tests/testthat/test-predict.R @@ -66,6 +66,7 @@ test_that("prefix is passed in predict()", { }) test_that("predict() errors for cluster spec for freq_itemsets", { + skip_if_not_installed("arules") spec <- tidyclust::freq_itemsets(min_support = 0.5) expect_snapshot( From 32705bf7db0adcba806f8bb3ce8213ec39a75581 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 16:48:16 -0700 Subject: [PATCH 69/71] use base R rather than stringr --- R/augment_itemset_predict.R | 6 +++--- R/extract_cluster_assignment.R | 2 +- R/predict_helpers.R | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/R/augment_itemset_predict.R b/R/augment_itemset_predict.R index 6827de5..4031f2d 100644 --- a/R/augment_itemset_predict.R +++ b/R/augment_itemset_predict.R @@ -82,9 +82,9 @@ augment_itemset_predict <- function(pred_output, truth_output) { # Extract all predictions (bind all .pred_cluster dataframes) preds_df <- dplyr::bind_rows(pred_output$.pred_cluster, .id = "row_id") %>% dplyr::filter(!is.na(.pred_item)) %>% # Keep only rows with predictions - dplyr::mutate(item = stringr::str_remove_all(item, "`")) %>% # Remove backticks from item names - dplyr::mutate(item = stringr::str_remove_all(item, "TRUE")) %>% # Remove TRUE from item names - dplyr::mutate(item = stringr::str_remove_all(item, "FALSE")) %>% # Remove FALSE from item names + dplyr::mutate( + item = gsub("`|TRUE|FALSE", "", item) # Remove backticks, TRUE, and FALSE from item names + ) dplyr::select(row_id, item, preds = .pred_item) # Standardize column names # Pivot truth data to long format (to match predictions) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index 8a196ec..1004369 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -165,7 +165,7 @@ extract_cluster_assignment.itemsets <- function(object, ...) { items <- attr(object, "item_names") itemsets <- arules::DATAFRAME(object) - itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) + itemset_list <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), trimws) support <- itemsets$support clusters <- numeric(length(items)) changed <- TRUE # Flag to track convergence diff --git a/R/predict_helpers.R b/R/predict_helpers.R index 3127caf..814439b 100644 --- a/R/predict_helpers.R +++ b/R/predict_helpers.R @@ -184,7 +184,7 @@ itemsets_predict_helper <- function(object, new_data, ..., prefix = "Cluster_") # Extract frequent itemsets and their supports items <- attr(object, "item_names") itemsets <- arules::DATAFRAME(object) - frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), stringr::str_trim) + frequent_itemsets <- lapply(strsplit(gsub("[{}]", "", itemsets$items), ","), trimws) supports <- itemsets$support # Calculate global support for each item (fallback) @@ -239,7 +239,7 @@ itemsets_predict_helper <- function(object, new_data, ..., prefix = "Cluster_") # Create result data frame data.frame( - item = stringr::str_remove_all(items, "`"), # Remove backticks from item names + item = gsub("`", "", items), # Remove backticks from item names .obs_item = unlist(row_data), .pred_item = pred_values, row.names = NULL From 7b2751c41f7d2143073b0a1e34c2cb94243e3e25 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 16:48:27 -0700 Subject: [PATCH 70/71] use the reduce() from compat-purrr.R --- R/extract_itemset_predictions.R | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/R/extract_itemset_predictions.R b/R/extract_itemset_predictions.R index bf168cb..916708d 100644 --- a/R/extract_itemset_predictions.R +++ b/R/extract_itemset_predictions.R @@ -47,18 +47,27 @@ extract_itemset_predictions <- function(pred_output) { # Extract the list of data frames from the single column data_frames <- pred_output$.pred_cluster - # Process each observation and combine results using reduce - result_df <- data_frames %>% - purrr::reduce(.f = ~ { - # Process each observation (data frame) - processed <- .y %>% - dplyr::mutate(value = ifelse(!is.na(.obs_item), .obs_item, .pred_item)) %>% - dplyr::select(item, value) %>% - tidyr::pivot_wider(names_from = item, values_from = value) + # Define the function to be passed to reduce instead of using lambda + processing_function <- function(.x_acc, .y_current) { + # .x_acc is the accumulated result (the first argument to .f) + # .y_current is the current data frame from data_frames (the second argument to .f) + + # Process each data frame + processed <- .y_current %>% + dplyr::mutate(value = ifelse(!is.na(.obs_item), .obs_item, .pred_item)) %>% + dplyr::select(item, value) %>% + tidyr::pivot_wider(names_from = item, values_from = value) - # Combine the processed data frame with the accumulated results - dplyr::bind_rows(.x, processed) - }, .init = NULL) + # Combine the processed data frame with the results + dplyr::bind_rows(.x_acc, processed) + } + + # Process each observation and combine results using reduce + result_df <- reduce( + .x = data_frames, + .f = processing_function, + .init = NULL + ) return(result_df) } From fbe29b32a8c5485be3b1b6b515a2eb1c3a3bcd07 Mon Sep 17 00:00:00 2001 From: Wander03 Date: Thu, 3 Jul 2025 16:49:02 -0700 Subject: [PATCH 71/71] stats::setNames --- R/extract_cluster_assignment.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/extract_cluster_assignment.R b/R/extract_cluster_assignment.R index 1004369..377c4ef 100644 --- a/R/extract_cluster_assignment.R +++ b/R/extract_cluster_assignment.R @@ -270,7 +270,7 @@ item_assignment_tibble_w_outliers <- function(clusters, unique_non_zero_clusters <- unique(non_zero_clusters) # Map each unique non-zero cluster to a new cluster starting from Cluster_1 - cluster_map <- setNames(paste0(prefix, seq_along(unique_non_zero_clusters)), unique_non_zero_clusters) + cluster_map <- stats::setNames(paste0(prefix, seq_along(unique_non_zero_clusters)), unique_non_zero_clusters) # Assign the corresponding cluster names to the non-zero clusters res[clusters != 0] <- cluster_map[as.character(non_zero_clusters)]