Nothing
#' @title
#' R6 class for a formal context
#'
#' @description
#' This class implements the data structure and methods for formal contexts.
#'
#' @examples
#' # Build and print the formal context
#' fc_planets <- FormalContext$new(planets)
#' print(fc_planets)
#'
#' # Define a set of attributes
#' S <- Set$new(attributes = fc_planets$attributes)
#' S$assign(moon = 1, large = 1)
#'
#' # Compute the closure of S
#' Sc <- fc_planets$closure(S)
#' # Is Sc a closed set?
#' fc_planets$is_closed(Sc)
#'
#' # Clarify and reduce the formal context
#' fc2 <- fc_planets$reduce(TRUE)
#'
#' # Find implications
#' fc_planets$find_implications()
#'
#' # Read a formal context from CSV
#' filename <- system.file("contexts", "airlines.csv", package = "fcaR")
#' fc <- FormalContext$new(filename)
#'
#' # Read a formal context from a CXT file
#' filename <- system.file("contexts", "lives_in_water.cxt", package = "fcaR")
#' fc <- FormalContext$new(filename)
#'
#' @references
#'
#' Guigues J, Duquenne V (1986). “Familles minimales d'implications informatives résultant d'un tableau de données binaires.” _Mathématiques et Sciences humaines_, *95*, 5-18.
#'
#' Ganter B, Wille R (1999). _Formal concept analysis : mathematical foundations_. Springer. ISBN 3540627715.
#'
#' Belohlavek R (2002). “Algorithms for fuzzy concept lattices.” In _Proc. Fourth Int. Conf. on Recent Advances in Soft Computing_. Nottingham, United Kingdom, 200-205.
#'
#' Hahsler M, Grun B, Hornik K (2005). “arules - a computational environment for mining association rules and frequent item sets.” _J Stat Softw_, *14*, 1-25.
#'
#' @export
FormalContext <- R6::R6Class(
classname = "FormalContext",
public = list(
#' @field I The table of the formal context as a matrix.
I = NULL,
#' @field attributes The attributes of the formal context.
attributes = NULL,
#' @field objects The objects of the formal context.
objects = NULL,
#' @field grades_set The set of degrees (in \[0, 1\]) the whole set of attributes can take.
grades_set = NULL,
#' @field expanded_grades_set The set of degrees (in \[0, 1\]) each attribute can take.
expanded_grades_set = NULL,
#' @field concepts The concept lattice associated to the formal context as a \code{\link{ConceptLattice}}.
concepts = NULL,
#' @field implications A set of implications on the formal context as an \code{\link{ImplicationSet}}.
implications = NULL,
#' @field description An optional description of the dataset
description = character(0),
#' @description
#' Creator for the Formal Context class
#'
#' @param I (numeric matrix) The table of the formal context.
#' @param filename (character) Path of a file to import.
#' @param remove_const (logical) If \code{TRUE}, remove constant columns. The default is \code{FALSE}.
#'
#' @details
#' Columns of \code{I} should be named, since they are the names of the attributes of the formal context.
#'
#' If no \code{I} is used, the resulting \code{FormalContext} will be empty and not usable unless for loading a previously saved one. In this case, one can provide a \code{filename} to import. Only RDS, CSV and CXT files are currently supported.
#'
#' If the file is not present, the fcarepository.org is looked for coincidences. If so, the corresponding context is loaded.
#'
#' @return An object of the \code{FormalContext} class.
#' @export
#'
initialize = function(I, filename, remove_const = FALSE) {
if (missing(I)) {
if (!missing(filename) && file.exists(filename)) {
self$load(filename)
}
return(invisible(self))
}
if ((length(I) == 1) && is.character(I) && file.exists(I)) {
self$load(I)
return(invisible(self))
}
if ((length(I) == 1) && is.character(I) && !file.exists(I)) {
stop(
glue::glue(
"File '{I}' not found. If you want to download it from the repository, please use `fcaR::fetch_context('{I}')` instead."
),
call. = FALSE
)
}
# version
if (!capabilities()["long.double"] & getRversion() < "4.1.0") {
private$can_plot <- FALSE
}
# Transform the formal context to sparse
if (inherits(I, "transactions")) {
check_needed_pkg("arules", "process 'transactions' objects")
# If it comes from the arules package
attributes <- I@itemInfo$labels
I <- convert_to_sparse(I@data)
objects <- paste0(seq_len(ncol(I)))
dimnames(I) <- list(attributes, objects)
} else {
# Or if it comes from a numeric table
if (length(colnames(I)) > 0) {
attributes <- colnames(I)
} else {
attributes <- paste0("A", seq_len(ncol(I)))
}
if (length(rownames(I)) > 0) {
objects <- rownames(I)
} else {
objects <- paste0("O", seq_len(nrow(I)))
}
private$is_many_valued <- check_many_valued(I)
if (!private$is_many_valued) {
I <- as.matrix(I)
# Remove the constant columns
if (remove_const) {
constant_cols <- which(apply(I, 2, max) == apply(I, 2, min))
if (length(constant_cols) > 0) {
str <- paste0(
"Removed constant columns: ",
stringr::str_flatten(attributes[constant_cols], collapse = ", ")
)
message(stringr::str_wrap(str, exdent = 2, width = 75))
I <- I[, -constant_cols]
attributes <- attributes[-constant_cols]
}
}
# # TODO: save tI???
# tI <- new_spm(I)
# I <- new_spm(t(I))
I <- convert_to_sparse(
Matrix::Matrix(t(I), sparse = TRUE)
)
}
}
self$objects <- objects
self$attributes <- attributes
if (!private$is_many_valued) {
# Assign everything to its corresponding field
expanded_grades_set <- compute_grades(Matrix::t(I))
grades_set <- sort(unique(unlist(expanded_grades_set)))
self$I <- I
self$grades_set <- unique(c(0, grades_set, 1))
self$expanded_grades_set <- expanded_grades_set
colnames(self$I) <- self$objects
rownames(self$I) <- self$attributes
# Is the FormalContext binary?
private$is_binary <- length(self$grades_set) == 2
} else {
# browser()
private$many_valued_I <- I
# colnames(private$many_valued_I) <- self$objects
# rownames(private$many_valued_I) <- self$attributes
}
# Create a new empty implication set inside
self$implications <- ImplicationSet$new(
attributes = attributes,
I = self$I
)
# Create a new empty ConceptLattice inside
self$concepts <- ConceptLattice$new(
extents = NULL,
intents = NULL,
objects = self$objects,
attributes = self$attributes,
I = self$I
)
},
#' @description
#' Check if the \code{FormalContext} is empty
#'
#' @return \code{TRUE} if the \code{FormalContext} is empty, that is, has not been provided with a matrix, and \code{FALSE} otherwise.
#'
#' @export
is_empty = function() {
return(is.null(self$I) && is.null(private$many_valued_I))
},
#' @description
#' Scale the context
#'
#' @param attributes The attributes to scale
#' @param type Type of scaling.
#'
#' @param ...
#'
#' @details
#' The types of scaling are implemented in a registry,
#' so that \code{scalingRegistry$get_entries()} returns
#' all types.
#'
#' In the dots argument, the user can supply the value for \code{bg} (logical), which, if set to \code{TRUE}, indicates to compute background knowledge as implications on the scales; if \code{FALSE}, no implications will be computed on the scales.
#'
#' @return The scaled formal context
#' @export
#' @examples
#' filename <- system.file("contexts", "aromatic.csv", package = "fcaR")
#' fc <- FormalContext$new(filename)
#' fc$scale("nitro", "ordinal", comparison = `>=`, values = 1:3)
#' fc$scale("OS", "nominal", c("O", "S"))
#' fc$scale(attributes = "ring", type = "nominal")
scale = function(attributes, type, ...) {
# TODO: Check that the attributes are in self$attributes
if (missing(attributes)) {
attributes <- self$attributes
}
if (is.function(attributes)) {
I <- self$incidence()
attributes <- sapply(self$attributes, \(n) attributes(I[, n])) |>
which() |>
names()
}
bg <- FALSE
dots <- list(...)
if ("bg" %in% names(dots)) {
bg <- dots$bg
dots$bg <- NULL
}
I <- self$incidence()
for (att in attributes) {
scaled <-
scale_context(I, column = att, type = type, bg = bg, dots)
I <- scaled$derived
private$scales <- c(
private$scales,
scaled$scale
)
names(private$scales)[length(private$scales)] <- att
if (bg) {
# Add implications to the bg_implications
private$bg_implications <- combine_implications(
private$bg_implications,
scaled$bg_implications
)
# if (scaled$bg_implications$cardinality() > 0) {
#
# private$bg_implications$to_basis()
#
# }
}
}
self$initialize(I)
},
#' @description
#' Scales applied to the formal context
#'
#' @param attributes (character) Name of the attributes for which scales
#' (if applied) are returned.
#'
#' @return The scales that have been applied to the specified attributes
#' of the formal context. If no \code{attributes} are passed,
#' then all applied scales are returned.
#'
#' @export
#' @examples
#' filename <- system.file("contexts", "aromatic.csv", package = "fcaR")
#' fc <- FormalContext$new(filename)
#' fc$scale("nitro", "ordinal", comparison = `>=`, values = 1:3)
#' fc$scale("OS", "nominal", c("O", "S"))
#' fc$scale(attributes = "ring", type = "nominal")
#' fc$get_scales()
get_scales = function(attributes = names(private$scales)) {
# TODO: Get the scales applied to only one or some
# of the self$attributes.
attributes <- attributes[attributes %in% names(private$scales)]
res <- private$scales[attributes]
if (length(res) > 0) {
if (length(res) > 1) {
return(res)
} else {
return(res[[1]])
}
} else {
message("No scaling has been performed on these attributes.")
}
},
#' @description
#' Background knowledge of a scaled formal context
#'
#' @return
#' An \code{ImplicationSet} with the implications
#' extracted from the application of scales.
#'
#' @export
#'
#' @examples
#' filename <- system.file("contexts", "aromatic.csv", package = "fcaR")
#' fc <- FormalContext$new(filename)
#' fc$scale("nitro", "ordinal", comparison = `>=`, values = 1:3)
#' fc$scale("OS", "nominal", c("O", "S"))
#' fc$scale(attributes = "ring", type = "nominal")
#' fc$background_knowledge()
background_knowledge = function() {
if (!is.null(private$bg_implications)) {
if (is.null(private$bg_implications_basis)) {
private$bg_implications_basis <- private$bg_implications$clone()
# private$bg_implications_basis$to_basis()
suppressMessages(private$bg_implications_basis$apply_rules(c(
"comp",
"simp",
"rsimp"
)))
}
private$bg_implications_basis$clone()
# private$bg_implications$clone()
} else {
ImplicationSet$new(attributes = self$attributes)
}
},
#' @description
#' Get the dual formal context
#'
#' @return A \code{FormalContext} where objects and attributes have interchanged their roles.
#'
#' @export
dual = function() {
if (private$is_many_valued) {
return(FormalContext$new(t(private$many_valued_I)))
} else {
I <- Matrix::as.matrix(self$I)
colnames(I) <- self$objects
rownames(I) <- self$attributes
return(FormalContext$new(I))
}
},
#' @description
#' Get the intent of a fuzzy set of objects
#'
#' @param ... A \code{Set} of objects, or object names.
#'
#' @return A \code{Set} with the intent.
#'
#' @export
intent = function(...) {
if (private$is_many_valued) {
error_many_valued()
}
dots <- list(...)
if (length(dots) == 1 && (inherits(dots[[1]], "Set") || is.numeric(dots[[1]]) || inherits(dots[[1]], "Matrix") || inherits(dots[[1]], "matrix"))) {
S <- dots[[1]]
} else {
S <- object_set(self, ...)
}
if (inherits(S, "Set")) {
if (all(S$get_attributes() == self$objects)) {
S <- S$get_vector()
} else {
S <- match_attributes(S, self$objects)
S <- S$get_vector()
warn <- c(
"The attributes in the input set are not the same",
" that in the formal context. Attempting to match",
" attribute names gives ",
.set_to_string(S, self$objects)
) |>
stringr::str_flatten()
warning(warn, call. = FALSE, immediate. = TRUE)
# stop("It is not a set of the required type (set of objects).", call. = FALSE)
}
}
if (length(S) == length(self$objects)) {
R <- compute_intent(
S,
Matrix::as.matrix(Matrix::t(self$I)),
connection = private$connection,
name = private$logic
)
if (length(R@i) > 0) {
# Non-empty set:
R <- Matrix::sparseMatrix(
i = R@i + 1,
j = rep(1, length(R@i)),
x = R@x,
dims = c(length(self$attributes), 1)
)
R <- Set$new(
attributes = self$attributes,
M = R
)
} else {
# Empty intent
R <- Set$new(attributes = self$attributes)
}
return(R)
} else {
stop(
"It is not a set of the required type (set of objects).",
call. = FALSE
)
}
},
#' @description
#' Get the intent of a fuzzy set of objects
#'
#' @param S (\code{Set}) The set of objects to compute the intent for.
#'
#' @return A \code{Set} with the intent.
#'
#' @export
uparrow = function(S) {
self$intent(S)
},
#' @description
#' Get the extent of a fuzzy set of attributes
#'
#' @param ... A \code{Set} of attributes, or attribute names.
#'
#' @return A \code{Set} with the intent.
#'
#' @export
extent = function(...) {
# TODO: Apply scales to Sets.
if (private$is_many_valued) {
error_many_valued()
}
dots <- list(...)
if (length(dots) == 1 && (inherits(dots[[1]], "Set") || is.numeric(dots[[1]]) || inherits(dots[[1]], "Matrix") || inherits(dots[[1]], "matrix"))) {
S <- dots[[1]]
} else {
S <- attribute_set(self, ...)
}
if (inherits(S, "Set")) {
if (all(S$get_attributes() == self$attributes)) {
S <- S$get_vector()
} else {
S <- match_attributes(S, self$attributes)
S <- S$get_vector()
warn <- c(
"The attributes in the input set are not the same",
" that in the formal context. Attempting to match",
" attribute names gives ",
.set_to_string(S, self$attributes)
) |>
stringr::str_flatten()
warning(warn, call. = FALSE, immediate. = TRUE)
# stop("It is not a set of the required type (set of attributes).", call. = FALSE)
}
}
if (length(S) == length(self$attributes)) {
R <- compute_extent(
S,
Matrix::as.matrix(Matrix::t(self$I)),
connection = private$connection,
name = private$logic
)
if (length(R@i) > 0) {
# Non-empty set:
R <- Matrix::sparseMatrix(
i = R@i + 1,
j = rep(1, length(R@i)),
x = R@x,
dims = c(length(self$objects), 1)
)
R <- Set$new(
attributes = self$objects,
M = R
)
} else {
# Empty extent
R <- Set$new(attributes = self$objects)
}
return(R)
} else {
stop(
"It is not a set of the required type (set of objects).",
call. = FALSE
)
}
},
#' @description
#' Get the extent of a fuzzy set of attributes
#'
#' @param S (\code{Set}) The set of attributes to compute the extent for.
#'
#' @return A \code{Set} with the intent.
#'
#' @export
downarrow = function(S) {
self$extent(S)
},
#' @description
#' Get the closure of a fuzzy set of attributes
#'
#' @param ... A \code{Set} of attributes, or attribute names.
#'
#' @return A \code{Set} with the closure.
#'
#' @export
closure = function(...) {
if (private$is_many_valued) {
error_many_valued()
}
dots <- list(...)
if (length(dots) == 1 && (inherits(dots[[1]], "Set") || is.numeric(dots[[1]]) || inherits(dots[[1]], "Matrix") || inherits(dots[[1]], "matrix"))) {
S <- dots[[1]]
} else {
S <- attribute_set(self, ...)
}
if (inherits(S, "Set")) {
if (all(S$get_attributes() == self$attributes)) {
S <- S$get_vector()
} else {
S <- match_attributes(S, self$attributes)
S <- S$get_vector()
warn <- c(
"The attributes in the input set are not the same",
" that in the formal context. Attempting to match",
" attribute names gives ",
.set_to_string(S, self$attributes)
) |>
stringr::str_flatten()
warning(warn, call. = FALSE, immediate. = TRUE)
# stop("It is not a set of the required type (set of attributes).", call. = FALSE)
}
}
if (length(S) == length(self$attributes)) {
R <- compute_closure(
S,
Matrix::as.matrix(Matrix::t(self$I)),
connection = private$connection,
name = private$logic
)
if (length(R@i) > 0) {
# Non-empty set:
R <- Matrix::sparseMatrix(
i = R@i + 1,
j = rep(1, length(R@i)),
x = R@x,
dims = c(length(self$attributes), 1)
)
R <- Set$new(
attributes = self$attributes,
M = R
)
} else {
# Empty closure
R <- Set$new(attributes = self$attributes)
}
return(R)
} else {
stop(
"It is not a set of the required type (set of objects).",
call. = FALSE
)
}
},
#' @description
#' Object Concept
#'
#' @param object (character) Name of the object to compute its associated concept
#'
#' @return
#' The object concept associated to the object given.
#'
#' @export
obj_concept = function(object) {
if (private$is_many_valued) {
error_many_valued()
}
S <- Set$new(attributes = self$objects)
S$assign(attributes = object, values = 1)
B <- self$intent(S)
A <- self$extent(B)
C <- Concept$new(extent = A, intent = B)
return(C)
},
#' @description
#' Attribute Concept
#'
#' @param attribute (character) Name of the attribute to compute its associated concept
#'
#' @return
#' The attribute concept associated to the attribute given.
#'
#' @export
att_concept = function(attribute) {
if (private$is_many_valued) {
error_many_valued()
}
S <- Set$new(attributes = self$attributes)
S$assign(attributes = attribute, values = 1)
A <- self$extent(S)
B <- self$intent(A)
C <- Concept$new(extent = A, intent = B)
return(C)
},
#' @description
#' Is a Concept?
#'
#' @param C A \code{Concept} object
#'
#' @return
#' \code{TRUE} if \code{C} is a concept.
#'
#' @export
is_concept = function(C) {
O <- C$get_extent()
A <- C$get_intent()
# O should be the extent of A, and A should be intent of O
return((self$extent(A) %==% O) && (self$intent(O) %==% A))
},
#' @description
#' Testing closure of attribute sets
#'
#' @param ... A \code{Set} of attributes, or attribute names.
#'
#' @return
#' \code{TRUE} if the set \code{S} is closed in this formal context.
#' @export
is_closed = function(...) {
dots <- list(...)
if (length(dots) == 1 && (inherits(dots[[1]], "Set") || is.numeric(dots[[1]]) || inherits(dots[[1]], "Matrix") || inherits(dots[[1]], "matrix"))) {
S <- dots[[1]]
} else {
S <- attribute_set(self, ...)
}
Sc <- self$closure(S)
return(S %==% Sc)
},
#' @description
#' Clarify a formal context
#'
#' @param copy (logical) If \code{TRUE}, a new \code{FormalContext} object is created with the clarified context, otherwise the current one is overwritten.
#'
#' @return The clarified \code{FormalContext}.
#'
#' @export
clarify = function(copy = FALSE) {
if (private$is_many_valued) {
error_many_valued()
}
# Redundant attributes
my_I <- .clarify_matrix(
Matrix::t(self$I),
rows = self$objects,
cols = self$attributes
)
# And redundant objects
my_I <- .clarify_matrix(
Matrix::t(my_I),
rows = colnames(my_I),
cols = self$objects
)
my_I <- Matrix::as.matrix(Matrix::t(my_I))
if (copy) {
fc2 <- FormalContext$new(my_I)
return(fc2)
} else {
self$initialize(my_I)
return(invisible(self))
}
},
#' @description
#' Reduce a formal context
#'
#' @param copy (logical) If \code{TRUE}, a new \code{FormalContext} object is created with the clarified and reduced context, otherwise the current one is overwritten.
#' @param method (character) The method to use for reduction. One of \code{"arrows"} (default, uses arrow relations, only for binary contexts) or \code{"concepts"} (uses irreducible concepts, works for fuzzy).
#'
#' @return The clarified and reduced \code{FormalContext}.
#'
#' @export
reduce = function(copy = FALSE, method = c("arrows", "concepts")) {
private$check_empty()
if (private$is_many_valued) {
error_many_valued()
}
method <- match.arg(method)
if (method == "arrows") {
if (!private$is_binary) {
stop("Arrow relations are only defined for binary contexts. Use method = 'concepts' for fuzzy contexts.", call. = FALSE)
}
res <- self$reduce_arrows()
} else {
if (self$concepts$is_empty()) {
stop("Concepts must be computed before reducing by 'concepts'.")
}
if (self$concepts$is_empty()) {
stop("Concept lattice is empty. Compute concepts before reducing.")
}
join_irr <- self$concepts$join_irreducibles()$to_list()
meet_irr <- self$concepts$meet_irreducibles()$to_list()
nj <- length(join_irr)
nm <- length(meet_irr)
I_new <- matrix(0, nrow = nj, ncol = nm)
for (i in seq(nj)) {
for (j in seq(nm)) {
I_new[i, j] <- ifelse(join_irr[[i]] %<=% meet_irr[[j]], 1, 0)
}
}
colnames(I_new) <- paste0("M", seq(nm))
rownames(I_new) <- paste0("J", seq(nj))
res <- FormalContext$new(I_new)
}
if (copy) {
return(res)
} else {
self$initialize(res$incidence())
return(invisible(self))
}
},
#' @description
#' Build the Standard Context
#'
#' @details
#' This is a wrapper around \code{reduce(copy = TRUE, method = "arrows")} for binary contexts,
#' or \code{reduce(copy = TRUE, method = "concepts")} for fuzzy contexts.
#'
#' @return
#' The standard context using the join- and meet- irreducible elements.
#' @export
standardize = function() {
method <- if (private$is_binary) "arrows" else "concepts"
return(self$reduce(copy = TRUE, method = method))
},
#' @description
#' Use Ganter Algorithm to compute concepts
#'
#' @param method (string) The name of a method for the computation of concepts. Available options can be listed with \code{conceptRegistry$get_entries()}.
#' @param verbose (logical) TRUE will provide a verbose output.
#'
#' @return A list with all the concepts in the formal context.
#'
#' @export
find_concepts = function(method = "InClose", verbose = FALSE) {
private$check_empty()
if (private$is_many_valued) {
error_many_valued()
}
# my_I <- Matrix::as.matrix(Matrix::t(self$I))
if (all(self$I@x == 1) && (method == "InClose")) {
my_I <- methods::as(Matrix::t(self$I), "dgCMatrix")
# Extracción explícita de vectores
sp_i <- my_I@i
sp_p <- my_I@p
sp_dim <- my_I@Dim
L <- InClose_Reorder(
sp_i_sexp = sp_i,
sp_p_sexp = sp_p,
dim_sexp = sp_dim,
verbose = verbose
)
# L <- InClose_Reorder(
# I = my_I,
# # attrs = self$attributes,
# verbose = verbose
# )
} else {
my_I <- Matrix::as.matrix(Matrix::t(self$I))
method <- conceptRegistry$get_entry(method[1])
if (is.null(method)) {
avail_methods <- conceptRegistry$get_entry_names()
stop("Unrecognized method.")
}
fun <- method$fun
if (method$method == "NextClosure") {
grades_set <- rep(list(self$grades_set), length(self$attributes))
} else {
grades_set <- self$grades_set
}
attrs <- self$attributes
if (verbose) {
glue::glue("You have chosen {method$method}\n") |>
cli::cat_line()
}
# L <- next_closure_concepts(
L <- method$fun(
I = my_I,
grades_set = grades_set,
attrs = attrs,
connection = private$connection,
name = private$logic,
verbose = verbose
)
}
# Since the previous function gives the list of intents of
# the computed concepts, now we will compute the corresponding
# extents.
if (length(self$attributes) == 1) {
my_intents <- Matrix::Matrix(t(as.vector(L$intents)), sparse = TRUE)
my_extents <- Matrix::Matrix(t(as.vector(L$extents)), sparse = TRUE)
} else {
my_intents <- L$intents
my_extents <- L$extents
}
self$concepts <- ConceptLattice$new(
extents = my_extents,
intents = my_intents,
objects = self$objects,
attributes = self$attributes,
I = self$I
)
if (verbose) {
cat("Number of closures", L$closure_count, "\n")
}
# return(invisible(self$concepts))
return(invisible(self))
},
#' @description
#' Use modified Ganter algorithm to compute both concepts and implications
#'
#' @param save_concepts (logical) \code{TRUE} will also compute and save the concept lattice. \code{FALSE} is usually faster, since it only computes implications.
#' @param verbose (logical) \code{TRUE} will provide a verbose output.
#' @param method (character) Algorithm to use for binary contexts. One of \code{"LinCbO"} (default, Janostik, Konecny, Krajca) or \code{"NextClosure"} (Ganter's algorithm). For non-binary (fuzzy) contexts, NextClosure is always used.
#'
#' @return Nothing, just updates the internal fields \code{concepts} and \code{implications}.
#'
#'
#' @export
find_implications = function(
save_concepts = TRUE,
verbose = FALSE,
method = c("LinCbO", "NextClosure")
) {
private$check_empty()
if (private$is_many_valued) {
error_many_valued()
}
method <- match.arg(method)
if (all(self$I@x == 1)) {
I <- self$incidence()
mode(I) <- "integer"
if (method == "LinCbO") {
L <- binary_lincbo_implications(
I,
save_concepts = save_concepts,
verbose = verbose
)
} else {
L <- binary_next_closure_implications(
I,
verbose = verbose
)
}
} else {
if (method == "LinCbO") {
message(
"LinCbO is only available for binary contexts. Falling back to NextClosure.",
call. = FALSE
)
}
my_I <- Matrix::as.matrix(Matrix::t(self$I))
grades_set <- rep(list(self$grades_set), length(self$attributes))
attrs <- self$attributes
L <- next_closure_implications(
I = my_I,
grades_set = grades_set,
attrs = attrs,
save_concepts = save_concepts,
connection = private$connection,
name = private$logic,
verbose = verbose
)
}
if (
!is.null(private$bg_implications) &&
private$bg_implications$cardinality() > 0
) {
private$bg_implications <- reorder_attributes(
private$bg_implications,
self$attributes
)
bg <- private$bg_implications$clone()
suppressMessages(bg$apply_rules(c("simp", "rsimp")))
# print(private$bg_implications %~% bg)
lhs_bg <- bg$get_LHS_matrix()
rhs_bg <- bg$get_RHS_matrix()
L1 <- complete_rhs(LHS = lhs_bg, RHS = rhs_bg)
lhs_bg <- L1$lhs
rhs_bg <- L1$rhs
L2 <- .imp_to_basis_bg(
lhs_bg = lhs_bg,
rhs_bg = rhs_bg,
LHS = L$LHS,
RHS = L$RHS,
attributes = self$attributes
)
L3 <- .clean(L2$lhs, L2$rhs)
L$LHS <- L3$lhs
L$RHS <- L3$rhs
}
if (save_concepts) {
my_intents <- L$concepts
my_extents <- L$extents
}
if (save_concepts) {
self$concepts <- ConceptLattice$new(
extents = my_extents,
intents = my_intents,
objects = self$objects,
attributes = self$attributes,
I = self$I
)
}
# Now, add the computed implications
# TODO: check what happens with only 1 implication.
if (ncol(L$LHS) > 0) {
# There are implications (the first one is dummy
# emptyset -> emptyset )
my_LHS <- convert_to_sparse(L$LHS)
my_RHS <- convert_to_sparse(L$RHS)
extracted_implications <- ImplicationSet$new(
attributes = self$attributes,
lhs = my_LHS,
rhs = my_RHS,
I = self$I
)
} else {
extracted_implications <- ImplicationSet$new(
attributes = self$attributes,
I = self$I
)
}
self$implications <- extracted_implications
self$implications$use_logic(self$get_logic())
return(invisible(self))
},
#' @description
#' Find protoconcepts
#'
#' @param verbose (logical) Show verbose output.
#'
#' @return A list of protoconcepts, where each protoconcept is a list of two Sets (extent and intent).
#' @export
find_protoconcepts = function(verbose = FALSE) {
private$check_empty()
my_I <- Matrix::as.matrix(Matrix::t(self$I))
L <- find_protoconcepts_cpp(
I = my_I,
connection = private$connection,
name = private$logic,
verbose = verbose
)
res <- vector("list", length(L))
for (i in seq_along(L)) {
elt <- L[[i]]
# elt[[1]] is extent (S4), elt[[2]] is intent (S4)
S_ext <- Set$new(attributes = self$objects, M = elt[[1]])
S_int <- Set$new(attributes = self$attributes, M = elt[[2]])
res[[i]] <- list(extent = S_ext, intent = S_int)
}
return(res)
},
#' @description
#' Find causal rules
#'
#' @param response_var (character) The name of the response variable.
#' @param min_support (numeric) Minimum support for the premise attributes.
#' @param confidence_level (numeric) Confidence level for the causality test.
#' @param max_length (integer) Maximum length of the premise.
#' @param verbose (logical) Show verbose output.
#'
#' @return A \code{RuleSet} object containing the discovered causal rules and their quality metrics.
#' @export
find_causal_rules = function(
response_var,
min_support = 0.1,
confidence_level = 0.95,
max_length = 3,
verbose = FALSE
) {
df <- find_causal_rules(
self,
response_var,
min_support,
confidence_level,
max_length,
verbose
)
if (nrow(df) == 0) {
return(RuleSet$new(attributes = self$attributes))
}
# Convert data frame to RuleSet
n_rules <- nrow(df)
n_attrs <- length(self$attributes)
# Create sparse matrices
# Using triplet format for efficiency if many rules, but dgCMatrix construction is fine
lhs_i <- integer(0)
lhs_j <- integer(0)
rhs_i <- integer(0)
rhs_j <- integer(0)
for (k in seq_len(n_rules)) {
# Parse premise
if (df$premise[k] != "") {
p_attrs <- unlist(strsplit(df$premise[k], ", "))
p_idx <- match(p_attrs, self$attributes)
lhs_i <- c(lhs_i, p_idx)
lhs_j <- c(lhs_j, rep(k, length(p_idx)))
}
# Conclusion
c_attrs <- df$conclusion[k]
c_idx <- match(c_attrs, self$attributes)
rhs_i <- c(rhs_i, c_idx)
rhs_j <- c(rhs_j, rep(k, length(c_idx)))
}
lhs_mat <- Matrix::sparseMatrix(
i = lhs_i,
j = lhs_j,
dims = c(n_attrs, n_rules)
)
rhs_mat <- Matrix::sparseMatrix(
i = rhs_i,
j = rhs_j,
dims = c(n_attrs, n_rules)
)
qual <- df[, setdiff(names(df), c("premise", "conclusion")), drop = FALSE]
rs <- RuleSet$new(
attributes = self$attributes,
lhs = lhs_mat,
rhs = rhs_mat,
quality = qual
)
return(rs)
},
#' @description
#' Factorize the formal context using Boolean Matrix Factorization (BMF) algorithms.
#' Note: Fuzzy contexts are currently not supported and will result in an error.
#'
#' @param method (character) The algorithm to use. Supported algorithms:
#' "RSF", "RSF-ES", "GreConD", "GreEss", "ASSO", "PaNDa+ MDL", "PaNDa+ ASSO-style",
#' "PaNDa+ Weighted", "Hyper", "Hyper+".
#' @param ... Additional arguments depending on the method:
#' \itemize{
#' \item \code{GreConD}, \code{PaNDa+ *}, \code{ASSO}: \code{k} (integer) maximum factors to extract.
#' \item \code{ASSO}: \code{threshold} (default 0.6), \code{w_pos} (default 1.0), \code{w_neg} (default 1.0).
#' \item \code{PaNDa+ Weighted}: \code{rho} (default 1.0).
#' \item \code{Hyper}, \code{Hyper+}: \code{min_support} (ratio, default 0.05).
#' \item \code{Hyper+}: \code{beta} (default 0.1).
#' }
#'
#' @return A list with two \code{FormalContext} objects:
#' \itemize{
#' \item \code{object_factor}: The context mapping Objects to Factors (Matrix A / U).
#' \item \code{factor_attribute}: The context mapping Factors to Attributes (Matrix B / V).
#' }
#' @export
factorize = function(method = "GreConD", ...) {
if (self$is_empty()) {
stop("Context is empty.")
}
I_mat <- as.matrix(self$incidence())
# Verificamos si la matriz es difusa (tiene valores > 0 y < 1)
if (any(I_mat > 0 & I_mat < 1)) {
stop("Current context is fuzzy. Factorization is currently only supported for Boolean contexts. Please binarize the context first.")
}
# Forzamos conversión a matriz lógica para Rcpp
storage.mode(I_mat) <- "logical"
# Estrategia de transposición para optimizar rendimiento:
# Si hay más atributos que objetos, operamos sobre la matriz transpuesta.
needs_transpose <- nrow(I_mat) < ncol(I_mat)
if (needs_transpose) {
I_mat <- t(I_mat)
}
dots <- list(...)
res <- NULL
# Enrutamiento de algoritmos
if (method == "RSF") {
res <- rsf_attr_cpp(I_mat)
} else if (method == "RSF-ES") {
res <- rsf_es_attr_cpp(I_mat)
} else if (method == "GreConD") {
k <- ifelse(is.null(dots$k), -1L, as.integer(dots$k))
res <- grecond_cpp(I_mat, no_of_factors = k)
} else if (method == "GreEss") {
res <- greess_cpp(I_mat)
} else if (method == "ASSO") {
k <- ifelse(is.null(dots$k), 5000L, as.integer(dots$k))
threshold <- ifelse(is.null(dots$threshold), 0.6, as.numeric(dots$threshold))
w_pos <- ifelse(is.null(dots$w_pos), 1.0, as.numeric(dots$w_pos))
w_neg <- ifelse(is.null(dots$w_neg), 1.0, as.numeric(dots$w_neg))
res <- asso_bitwise_cpp(I_mat, k_max = k, threshold = threshold, w_pos = w_pos, w_neg = w_neg)
} else if (method == "PaNDa+ MDL") {
k <- ifelse(is.null(dots$k), 5000L, as.integer(dots$k))
res <- panda_plus_jp_cpp(I_mat, k_max = k)
} else if (method == "PaNDa+ ASSO-style") {
k <- ifelse(is.null(dots$k), 5000L, as.integer(dots$k))
res <- panda_plus_ja_cpp(I_mat, k_max = k)
} else if (method == "PaNDa+ Weighted") {
k <- ifelse(is.null(dots$k), 5000L, as.integer(dots$k))
rho <- ifelse(is.null(dots$rho), 1.0, as.numeric(dots$rho))
res <- panda_plus_jprho_cpp(I_mat, k_max = k, rho = rho)
} else if (method == "Hyper" || method == "Hyper+") {
min_support <- ifelse(is.null(dots$min_support), 0.05, as.numeric(dots$min_support))
min_support_count <- max(1L, as.integer(floor(nrow(I_mat) * min_support)))
res_h <- hyper_inclose_cpp(I_mat, min_support = min_support_count)
if (method == "Hyper") {
res <- res_h
} else {
beta <- ifelse(is.null(dots$beta), 0.1, as.numeric(dots$beta))
res <- hyper_plus_optimized_cpp(I_mat, res_h, beta = beta)
}
} else {
stop(sprintf("Unknown factorization method: '%s'. Supported methods are: RSF, RSF-ES, GreConD, GreEss, ASSO, PaNDa+ MDL, PaNDa+ ASSO-style, PaNDa+ Weighted, Hyper, Hyper+.", method))
}
# Comprobamos validez de la salida
if (is.null(res) || ncol(res$U) == 0) {
warning("No factors found.")
return(NULL)
}
# Restaurar las dimensiones originales si se aplicó transposición
if (needs_transpose) {
tmp <- t(res$U)
res$U <- t(res$V)
res$V <- tmp
}
# Construimos los objetos FormalContext usando sparse matrices por eficiencia
# Convertimos a numérico primero porque lgCMatrix -> ngCMatrix falla en versiones recientes de Matrix
U_num <- matrix(as.numeric(res$U), nrow = nrow(res$U), ncol = ncol(res$U))
V_num <- matrix(as.numeric(res$V), nrow = nrow(res$V), ncol = ncol(res$V))
U_sparse <- methods::as(Matrix::Matrix(U_num, sparse = TRUE), "nMatrix")
V_sparse <- methods::as(Matrix::Matrix(V_num, sparse = TRUE), "nMatrix")
n_factors <- ncol(U_sparse)
factor_names <- paste0("F", seq_len(n_factors))
rownames(U_sparse) <- self$objects
colnames(U_sparse) <- factor_names
rownames(V_sparse) <- factor_names
colnames(V_sparse) <- self$attributes
ctx_A <- FormalContext$new(U_sparse)
ctx_B <- FormalContext$new(V_sparse)
return(list(
object_factor = ctx_A,
factor_attribute = ctx_B
))
},
#' @description
#' Convert the formal context to object of class \code{transactions} from the \code{arules} package
#'
#' @return A \code{transactions} object.
#'
#' @export
to_transactions = function() {
check_needed_pkg("arules", "exporting to transactions format")
private$check_empty()
if (private$is_many_valued) {
error_many_valued()
}
return(methods::as(methods::as(self$I, "nMatrix"), "transactions"))
# return(to_transactions.SpM(self$I))
},
#' @description
#' Save a \code{FormalContext} to RDS or CXT format
#'
#' @param filename (character) Path of the file where to store the \code{FormalContext}.
#'
#' @return Invisibly the current \code{FormalContext}.
#'
#' @details The format is inferred from the extension of the filename.
#'
#' @export
save = function(filename = tempfile(fileext = ".rds")) {
private$check_empty()
pattern <- "(?<!^|[.]|/)[.]([^.]+)$"
extension <- filename |>
stringr::str_extract_all(pattern) |>
unlist() |>
tolower()
if (extension == ".cxt") {
if (!private$is_many_valued) {
to_cxt(
I = self$incidence(),
objects = self$objects,
attributes = self$attributes,
filename
)
}
return(invisible(self))
}
if (!self$concepts$is_empty()) {
extents <- self$concepts$extents()
intents <- self$concepts$intents()
} else {
extents <- NULL
intents <- NULL
}
if (private$is_many_valued) {
L <- list(
I = self$I,
many_valued_I = private$many_valued_I,
extents = NULL,
intents = NULL,
attributes = self$attributes,
objects = self$objects,
expanded_grades_set = NULL,
grades_set = NULL,
implications = self$implications
)
} else {
L <- list(
I = self$I,
extents = extents,
intents = intents,
attributes = self$attributes,
objects = self$objects,
expanded_grades_set = self$expanded_grades_set,
grades_set = self$grades_set,
implications = self$implications
)
}
saveRDS(L, file = filename)
return(invisible(self))
},
#' @description
#' Load a \code{FormalContext} from a file
#'
#' @param filename (character) Path of the file to load the \code{FormalContext} from.
#'
#' @details Currently, only RDS, CSV and CXT files are supported.
#'
#' @return The loaded \code{FormalContext}.
#'
#' @export
load = function(filename) {
pattern <- "(?<!^|[.]|/)[.]([^.]+)$"
extension <- filename |>
stringr::str_extract_all(pattern) |>
unlist() |>
tolower()
if (extension == ".rds") {
L <- readRDS(filename)
if ("many_valued_I" %in% names(L)) {
self$initialize(L$many_valued_I)
return()
}
self$I <- L$I
self$attributes <- L$attributes
self$objects <- L$objects
self$expanded_grades_set <- L$expanded_grades_set
self$grades_set <- L$grades_set
self$implications <- ImplicationSet$new(
attributes = L$attributes,
I = L$I,
lhs = L$implications$get_LHS_matrix(),
rhs = L$implications$get_RHS_matrix()
)
if (!is.null(L$extents)) {
self$concepts <- ConceptLattice$new(
extents = L$extents,
intents = L$intents,
objects = L$objects,
attributes = L$attributes,
I = L$I
)
}
}
if (extension == ".csv") {
I <- read.csv(filename)
if (is.character(I[[1]]) || is.factor(I[[1]])) {
objects <- as.character(I[[1]])
I <- I[, -1]
rownames(I) <- objects
}
I <- as.matrix(I)
self$initialize(I)
}
if (extension == ".cxt") {
txt <- readLines(filename)
n_objects <- txt[3] |> as.numeric()
n_attributes <- txt[4] |> as.numeric()
obj_idx <- seq(6, 6 + n_objects - 1)
att_idx <- seq(
6 + n_objects,
6 + n_objects + n_attributes - 1
)
matrix_idx <- seq(
6 + n_objects + n_attributes,
6 + 2 * n_objects + n_attributes - 1
)
objects <- txt[obj_idx]
attributes <- txt[att_idx]
matrix <- txt[matrix_idx] |>
stringr::str_replace_all(
pattern = "[X|x]",
replacement = "1"
) |>
stringr::str_replace_all(
pattern = stringr::fixed("."),
replacement = "0"
) |>
as.list() |>
stringr::str_split(pattern = "") |>
purrr::map(as.numeric)
I <- do.call(rbind, matrix)
rownames(I) <- objects
colnames(I) <- attributes
self$initialize(I)
}
return(invisible(self))
},
#' @description
#' Dimensions of the formal context
#'
#' @return A vector with (number of objects, number of attributes).
#'
#' @export
dim = function() {
return(c(length(self$objects), length(self$attributes)))
},
#' @description
#' Export the formal context to JSON
#'
#' @param file (character) The path of the file to save the JSON to.
#' @return A JSON string representing the formal context.
#' @export
to_json = function(file = NULL) {
check_needed_pkg("jsonlite", "exporting to JSON")
# We export the transpose of I so that it is row-major (Objects x Attributes)
# Helper to convert sparse matrix
mat_to_list <- function(M) {
if (is.null(M)) {
warning("mat_to_list received NULL matrix")
return(list(
indices = integer(0),
columns = integer(0),
values = numeric(0)
))
}
# Use summary to get triplets (works for dgCMatrix)
s <- summary(M)
# If summary returns a table-like object (e.g. for ddiMatrix), force conversion
if (!all(c("i", "j", "x") %in% names(s))) {
# Fallback: try to convert to TsparseMatrix if summary didn't give triplets
T <- as(M, "TsparseMatrix")
s <- summary(T)
}
if (nrow(s) == 0) {
return(list(
indices = integer(0),
columns = integer(0),
values = numeric(0)
))
}
# Convert to 0-based indexing for JSON
res <- list(
indices = as.integer(s$j) - 1L,
columns = as.integer(s$i) - 1L,
values = as.numeric(s$x)
)
return(res)
}
tr_I <- Matrix::t(self$I)
I_data <- mat_to_list(tr_I)
# Concepts
concepts_data <- NULL
if (inherits(self$concepts, "ConceptLattice")) {
concepts_data <- self$concepts$to_json(return_list = TRUE)
}
# Implications
implications_data <- NULL
if (inherits(self$implications, "ImplicationSet")) {
implications_data <- self$implications$to_json(return_list = TRUE)
}
context_data <- list(
type = "FormalContext",
objects = self$objects,
attributes = self$attributes,
I = I_data,
grades_set = self$grades_set,
description = self$description,
is_many_valued = private$is_many_valued,
concepts = concepts_data,
implications = implications_data
)
json_string <- jsonlite::toJSON(context_data, auto_unbox = TRUE)
if (!is.null(file)) {
writeLines(json_string, file)
return(invisible(json_string))
} else {
return(json_string)
}
},
#' @description
#' Prints the formal context
#'
#' @return Prints information regarding the formal context.
#' @export
print = function() {
if (self$is_empty()) {
cat("Empty FormalContext.\n")
return(invisible(self))
}
dims <- self$dim()
if (!private$is_many_valued) {
I <- Matrix::as.matrix(Matrix::t(self$I))
if (private$is_binary) {
if (!is.null(private$arrow_rels)) {
I <- .print_arrows(I, private$arrow_rels, latex = FALSE)
} else {
I <- .print_binary(I, latex = FALSE)
}
}
objects <- self$objects
if (nrow(I) > 10) {
I <- I[1:10, ]
objects <- objects[1:10]
}
matp <- .print_matrix(
I,
objects = objects,
attributes = self$attributes
)
M <- matp$mat
ids <- matp$att_id
last_attribute <- max(ids) - 1
str <- paste0(
"FormalContext with ",
dims[1],
" objects and ",
dims[2],
" attributes."
) |>
stringr::str_wrap(width = getOption("width"))
cat(str)
cat("\n")
cat(M)
cat("\n")
if (last_attribute < length(self$attributes)) {
remaining <- self$attributes[-seq(last_attribute)]
if (length(remaining) > 6) {
remaining <- c(remaining[1:6], "...")
}
remaining <- remaining |>
stringr::str_flatten(", ")
str <- paste0("Other attributes are: ", remaining) |>
stringr::str_wrap(width = getOption("width"))
cat(str, "\n")
}
} else {
str <- paste0(
"FormalContext with ",
dims[1],
" objects and ",
dims[2],
" attributes."
) |>
stringr::str_wrap(width = getOption("width"))
cat(str)
cat("\n")
print_many_valued(private$many_valued_I)
# print(tibble::tibble(private$many_valued_I))
}
},
#' @description
#' Calculate arrow relations for a binary context.
#'
#' @details
#' This method computes the arrow relations (swarrow, nearrow, and double arrow).
#'
#' @return Nothing, updates the internal state.
#' @export
calculate_arrow_relations = function() {
private$check_empty()
if (!private$is_binary) {
stop("Arrow relations are only defined for binary contexts.",
call. = FALSE)
}
I_mat <- self$incidence()
# Ensure it is integer binary matrix for C++
I_mat <- matrix(as.integer(I_mat > 0),
nrow = nrow(I_mat),
ncol = ncol(I_mat))
private$arrow_rels <- compute_arrow_relations_cpp(I_mat)
invisible(self)
},
#' @description
#' Get arrow relations
#'
#' @return An integer matrix with the arrow relations.
#' @export
get_arrow_relations = function() {
if (is.null(private$arrow_rels)) {
self$calculate_arrow_relations()
}
res <- private$arrow_rels
dimnames(res) <- list(self$objects, self$attributes)
return(res)
},
#' @description
#' Get irreducible objects
#'
#' @return A character vector with the names of the irreducible objects.
#' @export
get_irreducible_objects = function() {
if (is.null(private$arrow_rels)) {
self$calculate_arrow_relations()
}
# Irreducible objects: those with at least one arrow swarrow (1) or double (3)
irr <- rowSums(private$arrow_rels == 1 | private$arrow_rels == 3) > 0
return(self$objects[irr])
},
#' @description
#' Get irreducible attributes
#'
#' @return A character vector with the names of the irreducible attributes.
#' @export
get_irreducible_attributes = function() {
if (is.null(private$arrow_rels)) {
self$calculate_arrow_relations()
}
# Irreducible attributes: those with at least one arrow nearrow (2) or double (3)
irr <- colSums(private$arrow_rels == 2 | private$arrow_rels == 3) > 0
return(self$attributes[irr])
},
#' @description
#' Check if the context (and its lattice) is distributive
#'
#' @return Logical: \code{TRUE} if the lattice is distributive.
#' @export
is_distributive = function() {
private$check_empty()
if (self$concepts$is_empty()) {
self$find_concepts()
}
return(self$concepts$is_distributive())
},
#' @description
#' Reduce the formal context using arrow relations
#'
#' @return A new \code{FormalContext} object that is clarified and reduced.
#' @export
reduce_arrows = function() {
private$check_empty()
# 1. Clarify (removes redundant duplicates)
fc <- self$clarify(copy = TRUE)
# 2. Calculate arrows on clarified context
fc$calculate_arrow_relations()
# 3. Identify irreducibles
objs <- fc$get_irreducible_objects()
atts <- fc$get_irreducible_attributes()
# 4. Extract submatrix
return(fc$subcontext(objects = objs, attributes = atts))
},
#' @description
#' Get the Core of the Formal Context
#'
#' @details
#' The core is the minimal subcontext that generates the same concept lattice.
#' For binary contexts, this is equivalent to \code{standardize()}.
#'
#' @return A new \code{FormalContext} object containing only irreducible rows and columns.
#' @export
get_core = function() {
return(self$reduce(copy = TRUE, method = "arrows"))
},
#' @description
#' Write the context in LaTeX format
#'
#' @param table (logical) If \code{TRUE}, surrounds everything between \code{\\begin{table}} and \code{\\end{table}}.
#' @param label (character) The label for the table environment.
#' @param caption (character) The caption of the table.
#' @param fraction (character) If \code{none}, no fractions are produced. Otherwise, if it is \code{frac}, \code{dfrac} or \code{sfrac}, decimal numbers are represented as fractions with the corresponding LaTeX typesetting.
#'
#' @return
#' A table environment in LaTeX.
#'
#' @export
#' @importFrom glue glue
#'
to_latex = function(table = TRUE, label = "", caption = "") {
# TODO: export a many-valued context to LaTeX
if (private$is_many_valued) {
error_many_valued()
}
fraction <- fcaR_options("latex_fraction")
I <- Matrix::as.matrix(Matrix::t(self$I))
if (private$is_binary) {
if (!is.null(private$arrow_rels)) {
I <- .print_arrows(I, private$arrow_rels, latex = TRUE)
} else {
I <- .print_binary(I, latex = TRUE)
}
} else {
if (fraction != "none") {
I <- .to_fraction(I, latex = TRUE, type = fraction)
} else {
decimal_places <- fcaR_options("decimal_places")
I[] <- I |>
formatC(digits = decimal_places) |>
stringr::str_replace_all("\\s*", "")
}
}
if (fcaR_options("use_mathrm")) {
objects <- glue::glue("\\mathrm{{{self$objects}}}")
attributes <- glue::glue("\\mathrm{{{self$attributes}}}")
} else {
objects <- self$objects
attributes <- self$attributes
}
str <- context_to_latex(
I,
objects = objects,
attributes = attributes
)
if (table) {
my_caption <- glue::glue(
"\\caption{{{caption}}}\\label{{{label}}}"
)
# my_caption <- paste0("\\caption{,
# \\label{",
# label, "}",
# caption, "}")
str <- c(
"\\begin{table}",
my_caption,
"\\centering",
str,
"\\end{table}"
)
}
cat(str)
return(invisible(str))
},
#' @description
#' Incidence matrix of the formal context
#'
#' @return The incidence matrix of the formal context
#' @export
#'
#' @examples
#' fc <- FormalContext$new(planets)
#' fc$incidence()
incidence = function() {
if (private$is_many_valued) {
return(private$many_valued_I)
} else {
I <- Matrix::as.matrix(Matrix::t(self$I))
dimnames(I) <- list(self$objects, self$attributes)
return(I)
}
},
#' @description
#' Generates a new FormalContext restricted to a subset of objects and/or attributes.
#'
#' @param objects (character or integer vector) The names or indices of the objects to keep. If NULL, keeps all.
#' @param attributes (character or integer vector) The names or indices of the attributes to keep. If NULL, keeps all.
#'
#' @return A new \code{FormalContext} object representing the subcontext.
#'
#' @export
#'
#' @examples
#' fc <- FormalContext$new(planets)
#' fc$subcontext(attributes = c("moon", "no_moon"))
subcontext = function(objects, attributes) {
if (missing(objects)) {
objects <- self$objects
}
if (missing(attributes)) {
attributes <- self$attributes
}
# --- 1. Resolver Índices de Objetos ---
if (is.null(objects)) {
# Si es NULL, mantenemos todos los índices
idx_objs <- seq_len(self$dim()[1])
} else if (is.character(objects)) {
# Si son nombres, buscamos sus índices
idx_objs <- match(objects, self$objects)
# Filtramos NAs (nombres que no existen)
idx_objs <- idx_objs[!is.na(idx_objs)]
} else {
# Asumimos numérico/lógico
idx_objs <- objects
}
# --- 2. Resolver Índices de Atributos ---
if (is.null(attributes)) {
idx_attrs <- seq_len(self$dim()[2])
} else if (is.character(attributes)) {
idx_attrs <- match(attributes, self$attributes)
idx_attrs <- idx_attrs[!is.na(idx_attrs)]
} else {
idx_attrs <- attributes
}
# --- 3. Subconjunto de la Matriz (CRÍTICO) ---
I_sub <- self$incidence()[idx_objs, idx_attrs, drop = FALSE]
# rownames(I_sub) <- self$objects[idx_objs]
# rownames(I_sub) <- self$attributes[idx_attrs]
# --- 4. Crear el Nuevo Contexto ---
# FormalContext$new es capaz de ingerir una matriz dispersa directamente.
# Esto reinicia automáticamente conceptos e implicaciones a NULL (lo correcto).
new_fc <- FormalContext$new(I_sub)
# --- 5. Preservar Metadatos Vitales ---
# El subcontexto debe operar bajo las mismas reglas matemáticas que el padre.
# Copiamos la lógica difusa actual.
new_fc$use_logic(private$logic)
# (Opcional) Si hubiera Background Knowledge cargado, ¿debería copiarse?
# Generalmente no, porque las escalas dependen de los atributos originales.
# Si se han borrado atributos, el BG knowledge podría ser inválido.
# Por seguridad, es mejor dejarlo limpio.
return(new_fc)
},
# subcontext = function(objects,
# attributes) {
# if (missing(objects)) objects <- self$objects
# if (missing(attributes)) attributes <- self$attributes
#
# if (is.numeric(objects)) {
#
# final_objects <- self$objects[objects[objects > 0 & objects < length(self$objects)]]
# } else {
# final_objects <- intersect(objects, self$objects)
# }
#
# if (is.numeric(attributes)) {
# final_attributes <- self$attributes[attributes[attributes > 0 & attributes < length(self$attributes)]]
# } else {
# final_attributes <- intersect(attributes, self$attributes)
# }
#
# if (length(objects) == 0) objects <- self$objects
# if (length(attributes) == 0) attributes <- self$attributes
#
# I <- self$incidence()[final_objects, final_attributes]
# I <- matrix(I,
# nrow = length(final_objects),
# ncol = length(final_attributes)
# )
# rownames(I) <- final_objects
# colnames(I) <- final_attributes
#
# return(FormalContext$new(I))
# },
#' @description
#' Subcontext of the formal context
#'
#' @param objects (character array) Name of the objects to
#' keep.
#' @param attributes (character array) Names of the attributes
#' to keep.
#'
#' @details
#' A warning will be issued if any of the names is not present
#' in the list of objects or attributes of the formal context.
#'
#' If \code{objects} or \code{attributes} is empty, then it is
#' assumed to represent the whole set of objects or attributes
#' of the original formal context.
#'
#' @return Another \code{FormalContext} that is a subcontext
#' of the original one, with only the objects and attributes
#' selected.
#' @export
#'
#' @examples
#' fc <- FormalContext$new(planets)
#' fc[, c("moon", "no_moon")]
`[` = function(objects, attributes) {
self$subcontext(objects, attributes)
},
#' @description
#' Plot the formal context table
#'
#' @param to_latex (logical) If \code{TRUE}, export the plot as a \code{tikzpicture} environment that can be included in a \code{LaTeX} file.
#' @param ... Other parameters to be passed to the \code{tikzDevice} that renders the lattice in \code{LaTeX}, or for the figure caption. See \code{Details}.
#'
#' @details
#' Particular parameters that control the size of the \code{tikz} output are: \code{width}, \code{height} (both in inches), and \code{pointsize} (in points), that should be set to the font size used in the \code{documentclass} header in the \code{LaTeX} file where the code is to be inserted.
#'
#' If a \code{caption} is provided, the whole \code{tikz} picture will be wrapped by a \code{figure} environment and the caption set.
#'
#' @return If \code{to_latex} is \code{FALSE}, it returns nothing, just plots the graph of the formal context. Otherwise, this function returns the \code{LaTeX} code to reproduce the formal context plot.
#'
#' @export
plot = function(to_latex = FALSE, ...) {
if (private$is_many_valued) {
error_many_valued()
}
private$check_empty()
if (!private$can_plot) {
warning(
"The R system has not the needed capabilities to plot.",
call. = FALSE
)
return(invisible(FALSE))
}
plot_context(self$I, to_latex, ...)
},
#' @description
#' Sets the logic to use
#'
#'
#' @param name The name of the logic to use. To see the available names, run \code{available_logics()}.
#'
#' @export
use_logic = function(name = available_logics()) {
name <- match.arg(name)
if (name %in% available_logics()) {
private$logic <- name
}
},
#' @description
#' Gets the logic used
#'
#' @return A string with the name of the logic.
#'
#' @export
get_logic = function() {
private$logic
},
#' @description
#' Sets the name of the Galois connection to use
#'
#' @param connection The name of the Galois connection. Available connections are "standard" (antitone), "benevolent1" and "benevolent2" (isotone)
#'
#' @export
use_connection = function(connection) {
if (connection %in% c("standard", "benevolent1", "benevolent2")) {
private$connection <- connection
}
},
#' @description
#' Gets the name of the Galois connection
#'
#' @return A string with the name of the Galois connection
#'
#' @export
get_connection = function() {
private$connection
}
),
private = list(
logic = "Godel",
arrow_rels = NULL,
connection = "standard",
is_binary = FALSE,
is_many_valued = FALSE,
can_plot = TRUE,
many_valued_I = NULL,
scales = list(),
bg_implications = NULL,
bg_implications_basis = NULL,
check_empty = function() {
if (self$is_empty()) {
stop(
"The formal context is empty. The only allowed method is 'load' to import from a previously saved RDS file.",
call. = FALSE
)
}
return(invisible(NULL))
}
)
)
#' @title Import FormalContext from JSON
#' @description Reconstructs a FormalContext object from a JSON string.
#' @param json_str A JSON string generated by \code{to_json()}.
#' @return A \code{FormalContext} object.
#' @export
context_from_json <- function(json_str) {
check_needed_pkg("jsonlite", "import from JSON")
data <- jsonlite::fromJSON(json_str, simplifyVector = FALSE)
if (data$type != "FormalContext") {
stop("Invalid JSON: type must be 'FormalContext'")
}
objects <- unlist(data$objects)
attributes <- unlist(data$attributes)
# Reconstruct I
i <- unlist(data$I$columns)
j <- unlist(data$I$indices)
x <- unlist(data$I$values)
if (length(i) > 0) {
I <- Matrix::sparseMatrix(
i = i + 1,
j = j + 1,
x = x,
dims = c(length(objects), length(attributes)),
dimnames = list(objects, attributes)
)
} else {
I <- Matrix::Matrix(
0,
nrow = length(objects),
ncol = length(attributes),
sparse = TRUE
)
dimnames(I) <- list(objects, attributes)
}
fc <- FormalContext$new(I)
# Restore other fields
if (!is.null(data$grades_set)) {
fc$grades_set <- unlist(data$grades_set)
}
if (!is.null(data$description)) {
fc$description <- unlist(data$description)
}
# Restore Concepts
if (!is.null(data$concepts)) {
# Reconstruct lattice from list data
# We can invoke lattice_from_json by passing the sub-json
# parsing sub-json from data object is tricky as we have list
# Use toJSON to get string? Or refactor lattice_from_json to accept list?
# Refactoring lattice_from_json to accept list is cleaner.
# But for now, convert list to JSON string is easier.
concepts_json <- jsonlite::toJSON(data$concepts, auto_unbox = TRUE)
cl <- lattice_from_json(concepts_json)
fc$concepts <- cl
}
# Restore Implications
if (!is.null(data$implications)) {
implications_json <- jsonlite::toJSON(data$implications, auto_unbox = TRUE)
imp <- implications_from_json(implications_json)
fc$implications <- imp
}
return(fc)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.