From 438e5c7177514de414791a5f58163fa4959f2f90 Mon Sep 17 00:00:00 2001 From: ielbadisy Date: Fri, 3 Jul 2026 09:47:46 +0100 Subject: [PATCH] Add TabFM backend and rename package to tfmr --- DESCRIPTION | 10 +- NAMESPACE | 13 + R/TabFM-constructor.R | 19 ++ R/TabFM-fit.R | 311 ++++++++++++++++++++++ R/TabFM-predict.R | 67 +++++ R/TabPFN-fit.R | 2 +- R/control-tab-fm.R | 205 ++++++++++++++ R/fit-tabfm.R | 17 +- R/misc.R | 25 ++ R/zzz.R | 22 ++ README.Rmd | 28 +- README.md | 28 +- man/control_tab_fm.Rd | 119 +++++++++ man/fit_tabfm.Rd | 23 -- man/fit_tfmr.Rd | 26 ++ man/is_tab_fm_installed.Rd | 14 + man/predict.tab_fm.Rd | 34 +++ man/tab_fm.Rd | 53 ++++ man/tab_pfn.Rd | 2 +- man/{tabfm-package.Rd => tfmr-package.Rd} | 14 +- tests/testthat.R | 4 +- tests/testthat/helper.R | 9 + tests/testthat/test-TabPFN-fit.R | 8 +- tests/testthat/test-classification.R | 2 +- tests/testthat/test-misc.R | 2 +- tests/testthat/test-regression.R | 6 +- tests/testthat/test-tabfm-wrapper.R | 68 +++++ 27 files changed, 1049 insertions(+), 82 deletions(-) create mode 100644 R/TabFM-constructor.R create mode 100644 R/TabFM-fit.R create mode 100644 R/TabFM-predict.R create mode 100644 R/control-tab-fm.R create mode 100644 man/control_tab_fm.Rd delete mode 100644 man/fit_tabfm.Rd create mode 100644 man/fit_tfmr.Rd create mode 100644 man/is_tab_fm_installed.Rd create mode 100644 man/predict.tab_fm.Rd create mode 100644 man/tab_fm.Rd rename man/{tabfm-package.Rd => tfmr-package.Rd} (61%) create mode 100644 tests/testthat/test-tabfm-wrapper.R diff --git a/DESCRIPTION b/DESCRIPTION index 364b1d5..c91775f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,4 +1,4 @@ -Package: tabfm +Package: tfmr Title: Foundation Model Wrappers for Tabular Data in R Version: 0.2.0.9000 Authors@R: c( @@ -9,11 +9,11 @@ Authors@R: c( comment = c(ROR = "03wc8by49")) ) Description: Provides a consistent R API for classification and regression with - tabular foundation models, including 'TabPFN' and 'TabICLv2'. The - calculations are served via 'Python' through 'reticulate'. + tabular foundation models, including 'TabPFN', 'TabICLv2', and 'TabFM'. + The calculations are served via 'Python' through 'reticulate'. License: Apache License (>= 2) -URL: https://github.com/ielbadisy/tabfm -BugReports: https://github.com/ielbadisy/tabfm/issues +URL: https://github.com/ielbadisy/tfmr +BugReports: https://github.com/ielbadisy/tfmr/issues Depends: R (>= 4.1.0) Imports: diff --git a/NAMESPACE b/NAMESPACE index 0bf3fa4..137064e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,9 @@ # Generated by roxygen2: do not edit by hand +S3method(augment,tab_fm) S3method(augment,tab_icl) S3method(augment,tab_pfn) +S3method(predict,tab_fm) S3method(predict,tab_icl) S3method(predict,tab_icl_mock) S3method(predict,tab_pfn) @@ -11,10 +13,17 @@ S3method(predict,tabicl._sklearn.classifier.TabICLClassifier) S3method(predict,tabicl._sklearn.regressor.TabICLRegressor) S3method(predict,tabpfn.classifier.TabPFNClassifier) S3method(predict,tabpfn.regressor.TabPFNRegressor) +S3method(print,control_tab_fm) S3method(print,control_tab_icl) S3method(print,control_tab_pfn) +S3method(print,tab_fm) S3method(print,tab_icl) S3method(print,tab_pfn) +S3method(tab_fm,data.frame) +S3method(tab_fm,default) +S3method(tab_fm,formula) +S3method(tab_fm,matrix) +S3method(tab_fm,recipe) S3method(tab_icl,data.frame) S3method(tab_icl,default) S3method(tab_icl,formula) @@ -26,11 +35,15 @@ S3method(tab_pfn,formula) S3method(tab_pfn,matrix) S3method(tab_pfn,recipe) export(augment) +export(control_tab_fm) export(control_tab_icl) export(control_tab_pfn) export(fit_tabfm) +export(fit_tfmr) +export(is_tab_fm_installed) export(is_tab_icl_installed) export(is_tab_pfn_installed) +export(tab_fm) export(tab_icl) export(tab_pfn) export(tabpfn_download_models) diff --git a/R/TabFM-constructor.R b/R/TabFM-constructor.R new file mode 100644 index 0000000..f2aef64 --- /dev/null +++ b/R/TabFM-constructor.R @@ -0,0 +1,19 @@ +new_tab_fm <- function( + fit, + levels, + training, + logging, + blueprint, + call = NULL +) { + check_character(levels, allow_null = TRUE) + + hardhat::new_model( + fit = fit, + levels = levels, + training = training, + logging = logging, + blueprint = blueprint, + class = "tab_fm" + ) +} diff --git a/R/TabFM-fit.R b/R/TabFM-fit.R new file mode 100644 index 0000000..c04a674 --- /dev/null +++ b/R/TabFM-fit.R @@ -0,0 +1,311 @@ +#' Fit a TabFM model +#' +#' `tab_fm()` fits the Python `tabfm` classifier or regressor through +#' `reticulate` and returns an R object with `predict()` and `augment()` +#' methods. +#' +#' @param x A data frame, matrix, recipe, or formula. +#' @param y Outcome vector for the data-frame and matrix interfaces. +#' @param data Data frame for formula and recipe interfaces. +#' @param formula Formula for the formula interface. +#' @param training_set_limit Maximum number of training rows retained before +#' fitting. Use `Inf` to disable downsampling. +#' @param control A list from [control_tab_fm()]. +#' @param ... Not currently used. +#' @return A `tab_fm` model object. +#' @export +tab_fm <- function(x, ...) { + UseMethod("tab_fm") +} + +#' @export +#' @rdname tab_fm +tab_fm.default <- function(x, ...) { + cli::cli_abort("{.fn tab_fm} is not defined for {obj_type_friendly(x)}.") +} + +#' @export +#' @rdname tab_fm +tab_fm.data.frame <- function( + x, + y, + training_set_limit = Inf, + control = control_tab_fm(), + ... +) { + processed <- hardhat::mold(x, y) + processed <- limit_training_set(processed, training_set_limit) + tab_fm_bridge(processed, control, ...) +} + +#' @export +#' @rdname tab_fm +tab_fm.matrix <- function( + x, + y, + training_set_limit = Inf, + control = control_tab_fm(), + ... +) { + processed <- hardhat::mold(x, y) + processed <- limit_training_set(processed, training_set_limit) + tab_fm_bridge(processed, control, ...) +} + +#' @export +#' @rdname tab_fm +tab_fm.formula <- function( + formula, + data, + training_set_limit = Inf, + control = control_tab_fm(), + ... +) { + bp <- hardhat::default_formula_blueprint( + intercept = FALSE, + allow_novel_levels = FALSE, + indicators = "none", + composition = "tibble" + ) + processed <- hardhat::mold(formula, data, blueprint = bp) + processed <- limit_training_set(processed, training_set_limit) + tab_fm_bridge(processed, control, ...) +} + +#' @export +#' @rdname tab_fm +tab_fm.recipe <- function( + x, + data, + training_set_limit = Inf, + control = control_tab_fm(), + ... +) { + processed <- hardhat::mold(x, data) + processed <- limit_training_set(processed, training_set_limit) + tab_fm_bridge(processed, control, ...) +} + +tab_fm_bridge <- function(processed, options, ...) { + rlang::check_dots_empty() + + predictors <- processed$predictors + outcome <- processed$outcomes[[1]] + res <- tab_fm_impl(predictors, outcome, options) + + new_tab_fm( + fit = res$fit, + levels = res$lvls, + training = res$train, + logging = res$logging, + blueprint = processed$blueprint + ) +} + +tabfm_require_backend <- function(backend) { + req <- switch( + backend, + auto = "tabfm[pytorch]", + jax = "tabfm[jax]", + pytorch = "tabfm[pytorch]" + ) + try(reticulate::py_require(req), silent = TRUE) + invisible(NULL) +} + +tabfm_loader_module <- function(backend) { + tabfm_require_backend(backend) + + pkg <- import_tabfm() + if (is.null(pkg)) { + pkg <- reticulate::import( + "tabfm", + delay_load = list( + on_error = function(e) { + cli::cli_abort("The {.pkg tabfm} Python package is not installed.") + }, + before_load = check_libomp + ) + ) + } + + if (backend == "auto") { + if (reticulate::py_has_attr(pkg, "tabfm_v1_0_0")) { + return(list(module = pkg$tabfm_v1_0_0, backend = "jax")) + } + if (reticulate::py_has_attr(pkg, "tabfm_v1_0_0_pytorch")) { + return(list(module = pkg$tabfm_v1_0_0_pytorch, backend = "pytorch")) + } + if (reticulate::py_has_attr(pkg, "tabfm_v1_0_0_jax")) { + return(list(module = pkg$tabfm_v1_0_0_jax, backend = "jax")) + } + cli::cli_abort("Could not find a TabFM loader in the installed Python package.") + } + + module_name <- switch( + backend, + jax = "tabfm.src.jax.tabfm_v1_0_0", + pytorch = "tabfm.src.pytorch.tabfm_v1_0_0" + ) + list(module = reticulate::import(module_name), backend = backend) +} + +tabfm_model_type <- function(y) { + if (is.factor(y)) { + "classification" + } else if (is.numeric(y)) { + "regression" + } else { + cli::cli_abort("`y` must be numeric for regression or a factor for classification.") + } +} + +tabfm_estimator_args <- function(kind, options) { + args <- list( + n_estimators = options$n_estimators, + norm_methods = options$norm_methods, + feat_shuffle_method = options$feat_shuffle_method, + permute_categorical = options$permute_categorical, + outlier_threshold = options$outlier_threshold, + max_num_features = options$max_num_features, + max_num_rows = options$max_num_rows, + use_amp = options$use_amp, + batch_size = options$batch_size, + random_state = options$random_state, + verbose = options$verbose, + cat_encoder_mode = options$cat_encoder_mode, + num_folds_for_cv = options$num_folds_for_cv, + n_feature_crosses = options$n_feature_crosses, + n_svd_features = options$n_svd_features, + total_svd_pool = options$total_svd_pool, + enable_nnls = options$enable_nnls, + nnls_beta = options$nnls_beta, + min_rows_for_single_val_split = options$min_rows_for_single_val_split + ) + + if (kind == "classification") { + args$class_shift <- options$class_shift + args$softmax_temperature <- options$softmax_temperature + args$average_logits <- options$average_logits + args$binary_calibration_method <- options$binary_calibration_method + args$multiclass_calibration_method <- options$multiclass_calibration_method + args$calibration_lambda <- options$calibration_lambda + } + + args +} + +tabfm_load_model <- function(loader, kind, options, backend) { + if (backend == "pytorch") { + return(loader$load( + model_type = kind, + checkpoint_path = options$checkpoint_path, + device = options$device, + use_cache = options$use_cache + )) + } + + if (backend == "jax") { + return(loader$load( + model_type = kind, + checkpoint_path = options$checkpoint_path, + step = options$step, + col_attention_impl = options$col_attention_impl, + row_attention_impl = options$row_attention_impl, + icl_attention_impl = options$icl_attention_impl, + use_cache = options$use_cache + )) + } + + cli::cli_abort("Could not load a TabFM model for backend {.val {options$backend}}.") +} + +tab_fm_impl <- function(x, y, opts) { + kind <- tabfm_model_type(y) + + if (kind == "classification" && is.factor(y) && length(levels(y)) > 10) { + cli::cli_abort( + c( + i = "There are {length(levels(y))} classes in the outcome.", + x = "TabFM supports at most 10 classes." + ) + ) + } + + backend_request <- opts$backend + if (backend_request == "auto") { + if (!is.null(opts$device)) { + backend_request <- "pytorch" + } else if ( + !is.null(opts$step) || + !identical(opts$col_attention_impl, "flash") || + !identical(opts$row_attention_impl, "jax") || + !identical(opts$icl_attention_impl, "flash") + ) { + backend_request <- "jax" + } + } + + loader_info <- tabfm_loader_module(backend_request) + loader <- loader_info$module + backend <- loader_info$backend + tabfm_pkg <- import_tabfm() + if (is.null(tabfm_pkg)) { + tabfm_pkg <- reticulate::import("tabfm") + } + + model <- tabfm_load_model(loader, kind, opts, backend) + + class_ctor <- if (kind == "classification") { + tabfm_pkg$TabFMClassifier + } else { + tabfm_pkg$TabFMRegressor + } + + est_args <- tabfm_estimator_args(kind, opts) + est_call <- rlang::call2(class_ctor, model = model, !!!est_args) + est <- rlang::eval_bare(est_call) + + if (kind == "classification") { + y_fit <- as.character(y) + lvls <- levels(y) + } else { + y_fit <- y + lvls <- NULL + } + + captured <- capture_python_or_r(model_fit <- try(est$fit(x, y_fit), silent = TRUE)) + py_msg <- captured$output + + if (inherits(model_fit, "try-error")) { + msgs <- as.character(model_fit) + cli::cli_abort("Model failed: {msgs}") + } else { + msgs <- character(0) + } + + res <- list( + fit = model_fit, + lvls = lvls, + train = dim(x), + logging = c(r = msgs, py = py_msg) + ) + class(res) <- c("tab_fm") + res +} + +#' @export +print.tab_fm <- function(x, ...) { + type <- ifelse(is.null(x$levels), "Regression", "Classification") + cli::cli_inform("TabFM {type} Model") + cat("\n") + cli::cli_inform("Training set\n\n") + cli::cli_inform(c(i = "{x$training[1]} data point{?s}")) + cli::cli_inform(c(i = "{x$training[2]} predictor{?s}")) + + if (!is.null(x$levels)) { + cli::cli_inform(c(i = "class levels: {.val {x$levels}}")) + } + + invisible(x) +} diff --git a/R/TabFM-predict.R b/R/TabFM-predict.R new file mode 100644 index 0000000..1c6a470 --- /dev/null +++ b/R/TabFM-predict.R @@ -0,0 +1,67 @@ +#' Predict using `TabFM` +#' +#' @param object,x A `tab_fm` object. +#' +#' @param new_data A data frame or matrix of new predictors. +#' +#' @param type The type of prediction. For classification, can be `"class"` or +#' `"prob"`. Defaults to `NULL` which gives all prediction types possible. +#' +#' @param ... Not used, but required for extensibility. +#' +#' @return +#' +#' [predict()] returns a tibble of predictions and [augment()] appends the +#' columns in `new_data`. In either case, the number of rows in the tibble is +#' guaranteed to be the same as the number of rows in `new_data`. +#' +#' For regression data, the prediction is in the column `.pred`. For +#' classification, the class predictions are in `.pred_class` and the +#' probability estimates are in columns with the pattern `.pred_{level}` where +#' `level` is the levels of the outcome factor vector. +#' +#' @export +predict.tab_fm <- function(object, new_data, type = NULL, ...) { + rlang::check_dots_empty() + forged <- hardhat::forge(new_data, object$blueprint)$predictors + + if (is.null(object$levels)) { + out <- try(object$fit$predict(forged), silent = TRUE) + if (inherits(out, "try-error")) { + cli::cli_abort("Prediction failed: {as.character(out)}") + } + return(tibble::tibble(.pred = as.vector(out))) + } + + out <- try(object$fit$predict_proba(forged), silent = TRUE) + if (inherits(out, "try-error")) { + cli::cli_abort("Prediction failed: {as.character(out)}") + } + + out <- as.matrix(out) + cls <- as.character(object$fit$classes_) + colnames(out) <- paste0(".pred_", cls) + cls_ind <- apply(out, 1, which.max) + res <- tibble::as_tibble(out) + res$.pred_class <- factor(cls[cls_ind], levels = object$levels) + + if (!is.null(type)) { + type <- rlang::arg_match(type, c("class", "prob")) + if (type == "class") { + res <- res[, ".pred_class"] + } else if (type == "prob") { + res <- res[, names(res) != ".pred_class"] + } + } + + res +} + +#' @rdname predict.tab_fm +#' @export +augment.tab_fm <- function(x, new_data, type = NULL, ...) { + new_data <- tibble::new_tibble(new_data) + res <- predict(x, new_data, type = type) + res <- cbind(res, new_data) + tibble::new_tibble(res) +} diff --git a/R/TabPFN-fit.R b/R/TabPFN-fit.R index 3342db8..ed4db5c 100644 --- a/R/TabPFN-fit.R +++ b/R/TabPFN-fit.R @@ -108,7 +108,7 @@ #' packages. That process would look like this: #' #' \preformatted{ -#' > library(tabfm) +#' > library(tfmr) #' > #' > predictors <- mtcars[, -1] #' > outcome <- mtcars[, 1] diff --git a/R/control-tab-fm.R b/R/control-tab-fm.R new file mode 100644 index 0000000..a33d55a --- /dev/null +++ b/R/control-tab-fm.R @@ -0,0 +1,205 @@ +#' Controlling TabFM execution +#' +#' @param backend Backend selector. Use `"auto"` to prefer JAX when available +#' and otherwise fall back to PyTorch. +#' @param checkpoint_path Optional local checkpoint directory. +#' @param step Optional checkpoint step for the JAX loader. +#' @param device Optional PyTorch device such as `"cpu"` or `"cuda"`. +#' @param use_cache Reuse a process-wide cached pretrained model when possible. +#' @param col_attention_impl JAX column-attention implementation. +#' @param row_attention_impl JAX row-attention implementation. +#' @param icl_attention_impl JAX ICL attention implementation. +#' @param n_estimators Number of ensemble members. +#' @param norm_methods Normalization methods passed to the Python constructor. +#' @param feat_shuffle_method Feature permutation strategy. +#' @param class_shift Whether to apply class-label shifts during classification. +#' @param permute_categorical Whether to randomly permute categorical values. +#' @param outlier_threshold Z-score threshold for outlier clipping. +#' @param max_num_features Maximum number of features per ensemble member. +#' @param max_num_rows Maximum number of rows per ensemble member. +#' @param softmax_temperature Temperature for classification probabilities. +#' @param average_logits Whether to average logits before probabilities. +#' @param use_amp Automatic mixed precision setting. +#' @param batch_size Number of ensemble members to process together. +#' @param random_state Random seed. +#' @param verbose Whether to print Python-side training details. +#' @param cat_encoder_mode Categorical encoding mode. +#' @param binary_calibration_method Binary calibration method. +#' @param multiclass_calibration_method Multiclass calibration method. +#' @param num_folds_for_cv Number of folds for out-of-fold calibration. +#' @param n_feature_crosses Number of feature crosses or `"sqrt"`. +#' @param n_svd_features Number of SVD features or `"sqrt"`. +#' @param total_svd_pool Pool size for SVD features. +#' @param enable_nnls Enable NNLS ensemble blending. +#' @param nnls_beta Blend weight for NNLS. +#' @param calibration_lambda L2 regularization strength for calibration. +#' @param min_rows_for_single_val_split Minimum rows for a single validation split. +#' @param ... Additional named arguments passed directly to the Python constructor. +#' @return A list with class `"control_tab_fm"`. +#' @export +control_tab_fm <- function( + backend = c("auto", "jax", "pytorch"), + checkpoint_path = NULL, + step = NULL, + device = NULL, + use_cache = TRUE, + col_attention_impl = "flash", + row_attention_impl = "jax", + icl_attention_impl = "flash", + n_estimators = 32L, + norm_methods = NULL, + feat_shuffle_method = "random", + class_shift = TRUE, + permute_categorical = FALSE, + outlier_threshold = 4.0, + max_num_features = 500L, + max_num_rows = NULL, + softmax_temperature = 0.9, + average_logits = TRUE, + use_amp = TRUE, + batch_size = 1L, + random_state = 42L, + verbose = FALSE, + cat_encoder_mode = "appearance", + binary_calibration_method = NULL, + multiclass_calibration_method = NULL, + num_folds_for_cv = 5L, + n_feature_crosses = 0, + n_svd_features = 0, + total_svd_pool = NULL, + enable_nnls = FALSE, + nnls_beta = 0.75, + calibration_lambda = 1e-2, + min_rows_for_single_val_split = 2000L, + ... +) { + backend <- rlang::arg_match(backend) + + check_string(feat_shuffle_method) + check_string(col_attention_impl) + check_string(row_attention_impl) + check_string(icl_attention_impl) + check_number_whole(n_estimators, min = 1) + check_number_decimal(outlier_threshold, min = 0) + check_number_whole(max_num_features, min = 1) + check_number_decimal(softmax_temperature, min = .Machine$double.eps) + check_logical(class_shift) + check_logical(permute_categorical) + check_logical(average_logits) + check_logical(use_amp) + check_logical(enable_nnls) + check_number_whole(batch_size, min = 1) + check_number_whole(random_state) + check_logical(verbose) + check_string(cat_encoder_mode) + check_number_whole(num_folds_for_cv, min = 2) + check_number_whole(min_rows_for_single_val_split, min = 0) + if (!is.null(checkpoint_path)) check_string(checkpoint_path) + if (!is.null(step)) check_number_whole(step, min = 0) + if (!is.null(device)) check_string(device) + if (!is.null(max_num_rows)) check_number_whole(max_num_rows, min = 1) + if (!is.null(total_svd_pool)) check_number_whole(total_svd_pool, min = 1) + if (!is.null(binary_calibration_method)) check_string(binary_calibration_method) + if (!is.null(multiclass_calibration_method)) check_string(multiclass_calibration_method) + + reserved <- c( + "backend", + "checkpoint_path", + "step", + "device", + "use_cache", + "col_attention_impl", + "row_attention_impl", + "icl_attention_impl", + "n_estimators", + "norm_methods", + "feat_shuffle_method", + "class_shift", + "permute_categorical", + "outlier_threshold", + "max_num_features", + "max_num_rows", + "softmax_temperature", + "average_logits", + "use_amp", + "batch_size", + "random_state", + "verbose", + "cat_encoder_mode", + "binary_calibration_method", + "multiclass_calibration_method", + "num_folds_for_cv", + "n_feature_crosses", + "n_svd_features", + "total_svd_pool", + "enable_nnls", + "nnls_beta", + "calibration_lambda", + "min_rows_for_single_val_split" + ) + + dot_args <- rlang::list2(...) + conflicts <- intersect(names(dot_args), reserved) + if (length(conflicts) > 0) { + cli::cli_abort( + "Argument{?s} {.arg {conflicts}} must be passed as named argument{?s}, not via {.code ...}." + ) + } + + args <- c( + list( + backend = backend, + checkpoint_path = checkpoint_path, + step = step, + device = device, + use_cache = use_cache, + col_attention_impl = col_attention_impl, + row_attention_impl = row_attention_impl, + icl_attention_impl = icl_attention_impl, + n_estimators = as.integer(n_estimators), + norm_methods = norm_methods, + feat_shuffle_method = feat_shuffle_method, + class_shift = class_shift, + permute_categorical = permute_categorical, + outlier_threshold = outlier_threshold, + max_num_features = as.integer(max_num_features), + max_num_rows = max_num_rows, + softmax_temperature = softmax_temperature, + average_logits = average_logits, + use_amp = use_amp, + batch_size = as.integer(batch_size), + random_state = as.integer(random_state), + verbose = verbose, + cat_encoder_mode = cat_encoder_mode, + binary_calibration_method = binary_calibration_method, + multiclass_calibration_method = multiclass_calibration_method, + num_folds_for_cv = as.integer(num_folds_for_cv), + n_feature_crosses = n_feature_crosses, + n_svd_features = n_svd_features, + total_svd_pool = total_svd_pool, + enable_nnls = enable_nnls, + nnls_beta = nnls_beta, + calibration_lambda = calibration_lambda, + min_rows_for_single_val_split = as.integer(min_rows_for_single_val_split) + ), + dot_args + ) + + args <- args[!vapply(args, is.null, logical(1))] + class(args) <- "control_tab_fm" + args +} + +#' @export +print.control_tab_fm <- function(x, ...) { + cli::cli_inform("control object for {.fn tab_fm}") + cat("\n") + lst <- purrr::map2( + names(x), + x, + ~ cli::format_inline("{.arg {.x}}: {.val {.y}}") + ) + names(lst) <- rep("*", length(lst)) + cli::cli_bullets(lst) + invisible(x) +} diff --git a/R/fit-tabfm.R b/R/fit-tabfm.R index 6219c5c..243252f 100644 --- a/R/fit-tabfm.R +++ b/R/fit-tabfm.R @@ -1,19 +1,24 @@ #' Fit a tabular foundation model #' -#' `fit_tabfm()` is a small convenience wrapper over [tab_pfn()] and -#' [tab_icl()]. Use the explicit backend functions when you need backend-specific -#' arguments. +#' `fit_tfmr()` is a small convenience wrapper over [tab_pfn()], +#' [tab_icl()], and [tab_fm()]. Use the explicit backend functions when you +#' need backend-specific arguments. #' #' @param x A data frame, matrix, recipe, or formula. #' @param ... Arguments passed to the selected backend. -#' @param engine One of `"tabpfn"` or `"tabicl"`. +#' @param engine One of `"tabpfn"`, `"tabicl"`, or `"tabfm"`. #' @return A fitted foundation-model object. #' @export -fit_tabfm <- function(x, ..., engine = c("tabpfn", "tabicl")) { +fit_tfmr <- function(x, ..., engine = c("tabpfn", "tabicl", "tabfm")) { engine <- match.arg(engine) switch( engine, tabpfn = tab_pfn(x, ...), - tabicl = tab_icl(x, ...) + tabicl = tab_icl(x, ...), + tabfm = tab_fm(x, ...) ) } + +#' @rdname fit_tfmr +#' @export +fit_tabfm <- fit_tfmr diff --git a/R/misc.R b/R/misc.R index 5486028..2302551 100644 --- a/R/misc.R +++ b/R/misc.R @@ -21,6 +21,17 @@ msg_tabicl_not_available <- function(cnd) { ) } +msg_tabfm_not_available <- function(cnd) { + c( + x = "The {.pkg tabfm} Python package is not installed in the discovered Python installation ({.file {reticulate::py_exe()}}).", + i = 'Allow reticulate to automatically configure an ephemeral Python environment by + removing the Python installation from the order of discovery and restarting the R session. + See {.href [Order of Discovery](https://rstudio.github.io/reticulate/dev/articles/versions.html#order-of-discovery)} for more info.', + i = 'Or install the {.pkg tabfm} Python package plus a backend extra such as + {.code tabfm[pytorch]} or {.code tabfm[jax]} in the selected Python environment.' + ) +} + check_libomp <- function() { os_info <- Sys.info()[["sysname"]] if (os_info != "Darwin") { @@ -185,6 +196,20 @@ is_tab_icl_installed <- function() { !inherits(res, "try-error") } +#' Check the TabFM Python package installation +#' +#' Attempts to import the Python package. +#' @return A single logical. +#' @export +is_tab_fm_installed <- function() { + suppressWarnings( + res <- import_tabfm() |> + reticulate::py_has_attr("noexists") |> + try(silent = TRUE) + ) + !inherits(res, "try-error") +} + check_model_version <- function(x, call = rlang::caller_env()) { valid_versions <- tabpfn_list_versions() diff --git a/R/zzz.R b/R/zzz.R index 2c58d74..140fc11 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -2,6 +2,7 @@ .pkg_env <- new.env() .pkg_env$tab_pfn <- NULL .pkg_env$tab_icl <- NULL +.pkg_env$tab_fm <- NULL .onLoad <- function(...) { # Set PyTorch TorchInductor cache to R's temp directory @@ -50,6 +51,23 @@ cli::cli_warn(msg_tabicl_not_available(e)) } ) + + tryCatch( + .pkg_env$tab_fm <- reticulate::import( + "tabfm", + delay_load = list( + on_error = function(e) { + cli::cli_abort(msg_tabfm_not_available(e)) + }, + before_load = function() { + check_libomp() + } + ) + ), + python.builtin.ModuleNotFoundError = function(e) { + cli::cli_warn(msg_tabfm_not_available(e)) + } + ) } .onUnload <- function(libpath) { @@ -87,4 +105,8 @@ import_tabicl <- function() { .pkg_env$tab_icl } +import_tabfm <- function() { + .pkg_env$tab_fm +} + # nocov end diff --git a/README.Rmd b/README.Rmd index dbe1d0e..20445f7 100644 --- a/README.Rmd +++ b/README.Rmd @@ -11,15 +11,15 @@ knitr::opts_chunk$set( ) ``` -# tabpfn +# tfmr -[![CRAN status](https://www.r-pkg.org/badges/version/tabpfn)](https://CRAN.R-project.org/package=tabpfn) -[![R-CMD-check](https://github.com/tidymodels/tabpfn/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/tidymodels/tabpfn/actions/workflows/R-CMD-check.yaml) -[![Codecov test coverage](https://codecov.io/gh/tidymodels/tabpfn/branch/main/graph/badge.svg)](https://app.codecov.io/gh/tidymodels/tabpfn?branch=main) +[![CRAN status](https://www.r-pkg.org/badges/version/tfmr)](https://CRAN.R-project.org/package=tfmr) +[![R-CMD-check](https://github.com/ielbadisy/tfmr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ielbadisy/tfmr/actions/workflows/R-CMD-check.yaml) +[![Codecov test coverage](https://codecov.io/gh/ielbadisy/tfmr/branch/main/graph/badge.svg)](https://app.codecov.io/gh/ielbadisy/tfmr?branch=main) -tabpfn, meaning prior fitted networks for tabular data, is a deep-learning model. See: +tfmr, meaning tabular foundation model wrappers for tabular data, is a package that exposes deep-learning models. See: - [_Transformers Can Do Bayesian Inference_](https://arxiv.org/abs/2112.10510) (arXiv, 2021) - [_TabPFN: A Transformer That Solves Small Tabular Classification Problems in a Second_](https://arxiv.org/abs/2207.01848) (arXiv, 2022) @@ -33,21 +33,21 @@ You can download the package from CRAN via: ```{r} #| eval: false -install.packages("tabpfn") +install.packages("tfmr") ``` -or you can install the development version of tabpfn like so: +or you can install the development version of tfmr like so: ```{r} #| eval: false require(pak) -pak(c("tidymodels/tabpfn"), ask = FALSE) +pak(c("ielbadisy/tfmr"), ask = FALSE) ``` -You'll need a Python virtual environment to access the underlying library. After installing the R package, tabpfn will install the required Python bits when you first fit a model: +You'll need a Python virtual environment to access the underlying library. After installing the R package, tfmr will install the required Python bits when you first fit a model: ``` -> library(tabpfn) +> library(tfmr) > > predictors <- mtcars[, -1] > outcome <- mtcars[, 1] @@ -67,7 +67,7 @@ Downloading numpy (4.9MiB) Downloading torch Installed 58 packages in 350ms > mod -tabpfn Regression Model +TabPFN Regression Model Training set i 32 data points @@ -81,7 +81,7 @@ After loading the package: ```{r} #| label: tab-start-up -library(tabpfn) +library(tfmr) ``` we can fit a model via the standard x/y interface. @@ -102,7 +102,7 @@ Prediction follows the usual S3 `predict()` method: predict(reg_mod, mtcars[26:32, -1]) ``` -tabpfn follows the tidymodels prediction convention: a data frame is always returned with a standard set of column names. +tfmr follows the tidymodels prediction convention: a data frame is always returned with a standard set of column names. For a classification model, the outcome should always be a factor vector. For example, using these data from the modeldata package: @@ -162,4 +162,4 @@ Also, the model is most effective when a GPU is available (by an order of magnit ## Code of Conduct -Please note that the tabpfn project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. +Please note that the tfmr project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. diff --git a/README.md b/README.md index abfe630..883d506 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# tabpfn +# tfmr [![CRAN -status](https://www.r-pkg.org/badges/version/tabpfn)](https://CRAN.R-project.org/package=tabpfn) -[![R-CMD-check](https://github.com/tidymodels/tabpfn/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/tidymodels/tabpfn/actions/workflows/R-CMD-check.yaml) +status](https://www.r-pkg.org/badges/version/tfmr)](https://CRAN.R-project.org/package=tfmr) +[![R-CMD-check](https://github.com/ielbadisy/tfmr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ielbadisy/tfmr/actions/workflows/R-CMD-check.yaml) [![Codecov test -coverage](https://codecov.io/gh/tidymodels/tabpfn/branch/main/graph/badge.svg)](https://app.codecov.io/gh/tidymodels/tabpfn?branch=main) +coverage](https://codecov.io/gh/ielbadisy/tfmr/branch/main/graph/badge.svg)](https://app.codecov.io/gh/ielbadisy/tfmr?branch=main) -tabpfn, meaning prior fitted networks for tabular data, is a +tfmr, meaning tabular foundation model wrappers for tabular data, is a deep-learning model. See: - [*Transformers Can Do Bayesian @@ -30,21 +30,21 @@ idiomatic R syntax using standard S3 methods. You can download the package from CRAN via: ``` r -install.packages("tabpfn") +install.packages("tfmr") ``` -or you can install the development version of tabpfn like so: +or you can install the development version of tfmr like so: ``` r require(pak) -pak(c("tidymodels/tabpfn"), ask = FALSE) +pak(c("ielbadisy/tfmr"), ask = FALSE) ``` You’ll need a Python virtual environment to access the underlying -library. After installing the R package, tabpfn will install the +library. After installing the R package, tfmr will install the required Python bits when you first fit a model: - > library(tabpfn) + > library(tfmr) > > predictors <- mtcars[, -1] > outcome <- mtcars[, 1] @@ -64,7 +64,7 @@ required Python bits when you first fit a model: Downloading torch Installed 58 packages in 350ms > mod - tabpfn Regression Model + TabPFN Regression Model Training set i 32 data points @@ -75,7 +75,7 @@ required Python bits when you first fit a model: After loading the package: ``` r -library(tabpfn) +library(tfmr) ``` we can fit a model via the standard x/y interface. @@ -108,7 +108,7 @@ predict(reg_mod, mtcars[26:32, -1]) #> 7 22.5 ``` -tabpfn follows the tidymodels prediction convention: a data frame is +tfmr follows the tidymodels prediction convention: a data frame is always returned with a standard set of column names. For a classification model, the outcome should always be a factor @@ -198,7 +198,7 @@ strictly working with traditional tabular data models. ## Code of Conduct -Please note that the tabpfn project is released with a [Contributor Code +Please note that the tfmr project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. diff --git a/man/control_tab_fm.Rd b/man/control_tab_fm.Rd new file mode 100644 index 0000000..a27731a --- /dev/null +++ b/man/control_tab_fm.Rd @@ -0,0 +1,119 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/control-tab-fm.R +\name{control_tab_fm} +\alias{control_tab_fm} +\title{Controlling TabFM execution} +\usage{ +control_tab_fm( + backend = c("auto", "jax", "pytorch"), + checkpoint_path = NULL, + step = NULL, + device = NULL, + use_cache = TRUE, + col_attention_impl = "flash", + row_attention_impl = "jax", + icl_attention_impl = "flash", + n_estimators = 32L, + norm_methods = NULL, + feat_shuffle_method = "random", + class_shift = TRUE, + permute_categorical = FALSE, + outlier_threshold = 4, + max_num_features = 500L, + max_num_rows = NULL, + softmax_temperature = 0.9, + average_logits = TRUE, + use_amp = TRUE, + batch_size = 1L, + random_state = 42L, + verbose = FALSE, + cat_encoder_mode = "appearance", + binary_calibration_method = NULL, + multiclass_calibration_method = NULL, + num_folds_for_cv = 5L, + n_feature_crosses = 0, + n_svd_features = 0, + total_svd_pool = NULL, + enable_nnls = FALSE, + nnls_beta = 0.75, + calibration_lambda = 0.01, + min_rows_for_single_val_split = 2000L, + ... +) +} +\arguments{ +\item{backend}{Backend selector. Use \code{"auto"} to prefer JAX when available +and otherwise fall back to PyTorch.} + +\item{checkpoint_path}{Optional local checkpoint directory.} + +\item{step}{Optional checkpoint step for the JAX loader.} + +\item{device}{Optional PyTorch device such as \code{"cpu"} or \code{"cuda"}.} + +\item{use_cache}{Reuse a process-wide cached pretrained model when possible.} + +\item{col_attention_impl}{JAX column-attention implementation.} + +\item{row_attention_impl}{JAX row-attention implementation.} + +\item{icl_attention_impl}{JAX ICL attention implementation.} + +\item{n_estimators}{Number of ensemble members.} + +\item{norm_methods}{Normalization methods passed to the Python constructor.} + +\item{feat_shuffle_method}{Feature permutation strategy.} + +\item{class_shift}{Whether to apply class-label shifts during classification.} + +\item{permute_categorical}{Whether to randomly permute categorical values.} + +\item{outlier_threshold}{Z-score threshold for outlier clipping.} + +\item{max_num_features}{Maximum number of features per ensemble member.} + +\item{max_num_rows}{Maximum number of rows per ensemble member.} + +\item{softmax_temperature}{Temperature for classification probabilities.} + +\item{average_logits}{Whether to average logits before probabilities.} + +\item{use_amp}{Automatic mixed precision setting.} + +\item{batch_size}{Number of ensemble members to process together.} + +\item{random_state}{Random seed.} + +\item{verbose}{Whether to print Python-side training details.} + +\item{cat_encoder_mode}{Categorical encoding mode.} + +\item{binary_calibration_method}{Binary calibration method.} + +\item{multiclass_calibration_method}{Multiclass calibration method.} + +\item{num_folds_for_cv}{Number of folds for out-of-fold calibration.} + +\item{n_feature_crosses}{Number of feature crosses or \code{"sqrt"}.} + +\item{n_svd_features}{Number of SVD features or \code{"sqrt"}.} + +\item{total_svd_pool}{Pool size for SVD features.} + +\item{enable_nnls}{Enable NNLS ensemble blending.} + +\item{nnls_beta}{Blend weight for NNLS.} + +\item{calibration_lambda}{L2 regularization strength for calibration.} + +\item{min_rows_for_single_val_split}{Minimum rows for a single validation split.} + +\item{...}{Additional named arguments passed directly to the Python constructor.} +} +\value{ +A list with class \code{"control_tab_fm"}. +} +\description{ +Controlling TabFM execution +} diff --git a/man/fit_tabfm.Rd b/man/fit_tabfm.Rd deleted file mode 100644 index 6b6af2d..0000000 --- a/man/fit_tabfm.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/fit-tabfm.R -\name{fit_tabfm} -\alias{fit_tabfm} -\title{Fit a tabular foundation model} -\usage{ -fit_tabfm(x, ..., engine = c("tabpfn", "tabicl")) -} -\arguments{ -\item{x}{A data frame, matrix, recipe, or formula.} - -\item{...}{Arguments passed to the selected backend.} - -\item{engine}{One of \code{"tabpfn"} or \code{"tabicl"}.} -} -\value{ -A fitted foundation-model object. -} -\description{ -\code{fit_tabfm()} is a small convenience wrapper over \code{\link[=tab_pfn]{tab_pfn()}} and -\code{\link[=tab_icl]{tab_icl()}}. Use the explicit backend functions when you need backend-specific -arguments. -} diff --git a/man/fit_tfmr.Rd b/man/fit_tfmr.Rd new file mode 100644 index 0000000..56a85a0 --- /dev/null +++ b/man/fit_tfmr.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/fit-tabfm.R +\name{fit_tfmr} +\alias{fit_tfmr} +\alias{fit_tabfm} +\title{Fit a tabular foundation model} +\usage{ +fit_tfmr(x, ..., engine = c("tabpfn", "tabicl", "tabfm")) + +fit_tabfm(x, ..., engine = c("tabpfn", "tabicl", "tabfm")) +} +\arguments{ +\item{x}{A data frame, matrix, recipe, or formula.} + +\item{...}{Arguments passed to the selected backend.} + +\item{engine}{One of \code{"tabpfn"}, \code{"tabicl"}, or \code{"tabfm"}.} +} +\value{ +A fitted foundation-model object. +} +\description{ +\code{fit_tfmr()} is a small convenience wrapper over \code{\link[=tab_pfn]{tab_pfn()}}, +\code{\link[=tab_icl]{tab_icl()}}, and \code{\link[=tab_fm]{tab_fm()}}. Use the explicit backend functions when you +need backend-specific arguments. +} diff --git a/man/is_tab_fm_installed.Rd b/man/is_tab_fm_installed.Rd new file mode 100644 index 0000000..5babb90 --- /dev/null +++ b/man/is_tab_fm_installed.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/misc.R +\name{is_tab_fm_installed} +\alias{is_tab_fm_installed} +\title{Check the TabFM Python package installation} +\usage{ +is_tab_fm_installed() +} +\value{ +A single logical. +} +\description{ +Attempts to import the Python package. +} diff --git a/man/predict.tab_fm.Rd b/man/predict.tab_fm.Rd new file mode 100644 index 0000000..16ab484 --- /dev/null +++ b/man/predict.tab_fm.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/TabFM-predict.R +\name{predict.tab_fm} +\alias{predict.tab_fm} +\alias{augment.tab_fm} +\title{Predict using \code{TabFM}} +\usage{ +\method{predict}{tab_fm}(object, new_data, type = NULL, ...) + +\method{augment}{tab_fm}(x, new_data, type = NULL, ...) +} +\arguments{ +\item{object, x}{A \code{tab_fm} object.} + +\item{new_data}{A data frame or matrix of new predictors.} + +\item{type}{The type of prediction. For classification, can be \code{"class"} or +\code{"prob"}. Defaults to \code{NULL} which gives all prediction types possible.} + +\item{...}{Not used, but required for extensibility.} +} +\value{ +\code{\link[=predict]{predict()}} returns a tibble of predictions and \code{\link[=augment]{augment()}} appends the +columns in \code{new_data}. In either case, the number of rows in the tibble is +guaranteed to be the same as the number of rows in \code{new_data}. + +For regression data, the prediction is in the column \code{.pred}. For +classification, the class predictions are in \code{.pred_class} and the +probability estimates are in columns with the pattern \verb{.pred_\{level\}} where +\code{level} is the levels of the outcome factor vector. +} +\description{ +Predict using \code{TabFM} +} diff --git a/man/tab_fm.Rd b/man/tab_fm.Rd new file mode 100644 index 0000000..f1853e2 --- /dev/null +++ b/man/tab_fm.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/TabFM-fit.R +\name{tab_fm} +\alias{tab_fm} +\alias{tab_fm.default} +\alias{tab_fm.data.frame} +\alias{tab_fm.matrix} +\alias{tab_fm.formula} +\alias{tab_fm.recipe} +\title{Fit a TabFM model} +\usage{ +tab_fm(x, ...) + +\method{tab_fm}{default}(x, ...) + +\method{tab_fm}{data.frame}(x, y, training_set_limit = Inf, control = control_tab_fm(), ...) + +\method{tab_fm}{matrix}(x, y, training_set_limit = Inf, control = control_tab_fm(), ...) + +\method{tab_fm}{formula}( + formula, + data, + training_set_limit = Inf, + control = control_tab_fm(), + ... +) + +\method{tab_fm}{recipe}(x, data, training_set_limit = Inf, control = control_tab_fm(), ...) +} +\arguments{ +\item{x}{A data frame, matrix, recipe, or formula.} + +\item{...}{Not currently used.} + +\item{y}{Outcome vector for the data-frame and matrix interfaces.} + +\item{training_set_limit}{Maximum number of training rows retained before +fitting. Use \code{Inf} to disable downsampling.} + +\item{control}{A list from \code{\link[=control_tab_fm]{control_tab_fm()}}.} + +\item{formula}{Formula for the formula interface.} + +\item{data}{Data frame for formula and recipe interfaces.} +} +\value{ +A \code{tab_fm} model object. +} +\description{ +\code{tab_fm()} fits the Python \code{tabfm} classifier or regressor through +\code{reticulate} and returns an R object with \code{predict()} and \code{augment()} +methods. +} diff --git a/man/tab_pfn.Rd b/man/tab_pfn.Rd index 93debf2..15fd82b 100644 --- a/man/tab_pfn.Rd +++ b/man/tab_pfn.Rd @@ -190,7 +190,7 @@ create an ephemeral environment and automatically install the required packages. That process would look like this: \preformatted{ - > library(tabfm) + > library(tfmr) > > predictors <- mtcars[, -1] > outcome <- mtcars[, 1] diff --git a/man/tabfm-package.Rd b/man/tfmr-package.Rd similarity index 61% rename from man/tabfm-package.Rd rename to man/tfmr-package.Rd index 52ac230..22aa8ad 100644 --- a/man/tabfm-package.Rd +++ b/man/tfmr-package.Rd @@ -1,18 +1,18 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/TabPFN-package.R \docType{package} -\name{tabfm-package} -\alias{tabfm} -\alias{tabfm-package} -\title{tabfm: Foundation Model Wrappers for Tabular Data in R} +\name{tfmr-package} +\alias{tfmr} +\alias{tfmr-package} +\title{tfmr: Foundation Model Wrappers for Tabular Data in R} \description{ -Provides a consistent R API for classification and regression with tabular foundation models, including 'TabPFN' and 'TabICLv2'. The calculations are served via 'Python' through 'reticulate'. +Provides a consistent R API for classification and regression with tabular foundation models, including 'TabPFN', 'TabICLv2', and 'TabFM'. The calculations are served via 'Python' through 'reticulate'. } \seealso{ Useful links: \itemize{ - \item \url{https://github.com/ielbadisy/tabfm} - \item Report bugs at \url{https://github.com/ielbadisy/tabfm/issues} + \item \url{https://github.com/ielbadisy/tfmr} + \item Report bugs at \url{https://github.com/ielbadisy/tfmr/issues} } } diff --git a/tests/testthat.R b/tests/testthat.R index e3f6734..8569b1c 100644 --- a/tests/testthat.R +++ b/tests/testthat.R @@ -21,6 +21,6 @@ # ) library(testthat) -library(tabfm) +library(tfmr) -test_check("tabfm") +test_check("tfmr") diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 40a62e0..afb989e 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -7,6 +7,15 @@ skip_if_no_tabpfn <- function() { } exp_cls <- c("tab_pfn", "hardhat_model", "hardhat_scalar") +exp_cls_fm <- c("tab_fm", "hardhat_model", "hardhat_scalar") + +skip_if_no_tabfm <- function() { + skip_if( + !is_tab_fm_installed(), + message = "TabFM Python library is not installed" + ) + skip_on_cran() +} predictors <- mtcars[, -1] outcome <- mtcars[, 1] diff --git a/tests/testthat/test-TabPFN-fit.R b/tests/testthat/test-TabPFN-fit.R index b0a6849..5c00771 100644 --- a/tests/testthat/test-TabPFN-fit.R +++ b/tests/testthat/test-TabPFN-fit.R @@ -10,7 +10,7 @@ test_that("check_data_constraints errors when too many rows", { x <- matrix(0, nrow = 50001, ncol = 2) y <- factor(rep(c("a", "b"), length.out = 50001)) expect_error( - tabfm:::check_data_constraints(x, y, control_tab_pfn()), + tfmr:::check_data_constraints(x, y, control_tab_pfn()), "50,000" ) }) @@ -29,7 +29,7 @@ test_that("check_data_constraints errors when too many columns", { x <- matrix(0, nrow = 10, ncol = 2001) y <- factor(rep(c("a", "b"), length.out = 10)) expect_error( - tabfm:::check_data_constraints(x, y, control_tab_pfn()), + tfmr:::check_data_constraints(x, y, control_tab_pfn()), "2000" ) }) @@ -38,7 +38,7 @@ test_that("check_data_constraints errors when too many classes", { x <- matrix(0, nrow = 11, ncol = 2) y <- factor(letters[1:11]) expect_error( - tabfm:::check_data_constraints(x, y, control_tab_pfn()), + tfmr:::check_data_constraints(x, y, control_tab_pfn()), "classes" ) }) @@ -46,7 +46,7 @@ test_that("check_data_constraints errors when too many classes", { test_that("sample_indicies handles numeric outcomes", { set.seed(1) molded <- list(outcomes = data.frame(outcome = rnorm(50001))) - result <- tabfm:::sample_indicies(molded) + result <- tfmr:::sample_indicies(molded) expect_length(result, 50000) expect_true(all(result >= 1 & result <= 50001)) }) diff --git a/tests/testthat/test-classification.R b/tests/testthat/test-classification.R index 7666df0..d48c7da 100644 --- a/tests/testthat/test-classification.R +++ b/tests/testthat/test-classification.R @@ -83,7 +83,7 @@ test_that('classification models - recipes', { reticulate::import("torch") - library(tabfm) + library(tfmr) suppressPackageStartupMessages(library(recipes)) #----------------------------------------------------------------------------- diff --git a/tests/testthat/test-misc.R b/tests/testthat/test-misc.R index 43ed2e2..092f009 100644 --- a/tests/testthat/test-misc.R +++ b/tests/testthat/test-misc.R @@ -8,6 +8,6 @@ test_that("check_model_version validates correctly", { }) test_that("msg_tabpfn_not_available returns correct structure", { - msg <- tabfm:::msg_tabpfn_not_available() + msg <- tfmr:::msg_tabpfn_not_available() expect_named(msg, c("x", "i", "i")) }) diff --git a/tests/testthat/test-regression.R b/tests/testthat/test-regression.R index 3441859..28100c8 100644 --- a/tests/testthat/test-regression.R +++ b/tests/testthat/test-regression.R @@ -1,14 +1,14 @@ test_that("show_env_var returns value when set", { withr::with_envvar( new = c(TABPFN_TEST_VAR = "hello"), - expect_equal(tabfm:::show_env_var("TABPFN_TEST_VAR"), "hello") + expect_equal(tfmr:::show_env_var("TABPFN_TEST_VAR"), "hello") ) }) test_that("show_env_var returns not set when empty", { withr::with_envvar( new = c(TABPFN_TEST_VAR = ""), - expect_equal(tabfm:::show_env_var("TABPFN_TEST_VAR"), "") + expect_equal(tfmr:::show_env_var("TABPFN_TEST_VAR"), "") ) }) @@ -148,7 +148,7 @@ test_that('regression models - recipes', { reticulate::import("torch") - library(tabfm) + library(tfmr) library(recipes) data(Chicago, package = "modeldata") diff --git a/tests/testthat/test-tabfm-wrapper.R b/tests/testthat/test-tabfm-wrapper.R new file mode 100644 index 0000000..a45eedd --- /dev/null +++ b/tests/testthat/test-tabfm-wrapper.R @@ -0,0 +1,68 @@ +test_that("tab_fm regression wrapper fits and predicts through sklearn-style API", { + fake_fit <- structure( + list( + predict = function(newdata) rep(1.5, nrow(newdata)) + ), + class = "tab_fm_mock" + ) + fake_regressor <- function(...) { + structure( + list(fit = function(x, y) fake_fit), + class = "tab_fm_mock" + ) + } + fake_loader <- list(load = function(...) "fake_model") + fake_module <- list(TabFMRegressor = fake_regressor) + + local_mocked_bindings( + import_tabfm = function() fake_module, + tabfm_loader_module = function(...) list(module = fake_loader, backend = "pytorch") + ) + + mod <- tab_fm(mpg ~ wt + hp, data = mtcars[1:10, ]) + expect_s3_class(mod, "tab_fm") + pred <- predict(mod, mtcars[11:12, ]) + expect_equal(pred, tibble::tibble(.pred = c(1.5, 1.5))) + expect_s3_class(fit_tfmr(mpg ~ wt + hp, data = mtcars[1:10, ], engine = "tabfm"), "tab_fm") +}) + +test_that("tab_fm classification wrapper returns class and probability predictions", { + fake_fit <- structure( + list( + classes_ = c("no", "yes"), + predict_proba = function(newdata) matrix( + c(0.8, 0.2, 0.3, 0.7), + nrow = nrow(newdata), + byrow = TRUE + ) + ), + class = "tab_fm_mock" + ) + fake_classifier <- function(...) { + structure( + list(fit = function(x, y) fake_fit), + class = "tab_fm_mock" + ) + } + fake_loader <- list(load = function(...) "fake_model") + fake_module <- list(TabFMClassifier = fake_classifier) + + local_mocked_bindings( + import_tabfm = function() fake_module, + tabfm_loader_module = function(...) list(module = fake_loader, backend = "pytorch") + ) + + dat <- data.frame( + y = factor(c("no", "yes", "no", "yes")), + x1 = 1:4, + x2 = c(1, 1, 0, 0) + ) + mod <- tab_fm(y ~ ., data = dat) + pred <- predict(mod, dat[1:2, ]) + expect_equal(names(pred), c(".pred_no", ".pred_yes", ".pred_class")) + expect_equal(as.character(pred$.pred_class), c("no", "yes")) + expect_equal( + predict(mod, dat[1:2, ], type = "prob")[0, ], + tibble::tibble(.pred_no = numeric(), .pred_yes = numeric()) + ) +})