R/TestDataCreators.R

Defines functions test_data_drop_rare_feature_levels test_create_synthetic_correlated_bad_outcome_data test_create_synthetic_correlated_one_outcome_data test_create_synthetic_correlated_one_sample_data test_create_synthetic_correlated_one_feature_invariant_data test_create_synthetic_correlated_data test_create_multiple_synthetic_series test_create_synthetic_series_one_feature_all_na_data test_create_synthetic_series_random_na_data test_create_synthetic_series_na_data test_create_synthetic_series_one_feature_invariant_data test_create_synthetic_series_invariant_feature_data test_create_synthetic_series_one_sample_data test_create_synthetic_series_one_outcome test_create_synthetic_series_data test_create_mostly_prospective_data test_create_partially_prospective_data test_create_prospective_data test_create_small_bad_data test_create_bad_data test_create_wide_data test_create_single_feature_two_values_data test_create_single_feature_invariant_data test_create_single_feature_one_sample_data test_create_single_feature_data test_create_all_identical_data test_create_one_sample_data test_create_bootstrapped_data test_create_data_without_feature test_create_empty_data test_create_good_data_random_missing test_create_good_data_few_censored test_create_good_data_one_censored test_create_good_data_without_censoring test_create_invariant_good_data test_create_small_good_data test_create_good_data test_data_package_installed

test_data_package_installed <- function(outcome_type) {
  run_test <- TRUE
  
  data_packages <- list(
    "survival" = "survival",
    "multinomial" = "datasets",
    "binomial" = "MASS",
    "continuous" = "Ecdat",
    "count" = "MASS"
  )
  
  if (!is_package_installed(data_packages[[outcome_type]])) run_test <- FALSE
  if (!rlang::is_installed(data_packages[[outcome_type]])) run_test <- FALSE
  
  if (!run_test) {
    rlang::inform(
      message = paste0(
        "Cannot run test because the ", 
        data_packages[[outcome_type]], 
        " package is not installed."),
      class = "familiar_message_inform_no_test"
    )
  }
  
  return(run_test)
}


test_create_good_data <- function(outcome_type, to_data_object = TRUE) {
  # Suppress NOTES due to non-standard evaluation in data.table
  etype <- median_house_value <- NULL

  if (outcome_type == "survival") {
    # Load colon dataset from the survival package.
    data <- data.table::as.data.table(survival::colon)

    # Focus on recurrence.
    data <- data[etype == 1]
    data$adhere <- factor(
      x = data$adhere, 
      levels = c(0, 1),
      labels = c(FALSE, TRUE), 
      ordered = TRUE)

    # Limit to 150 samples
    data <- data[1:150, ]

    # update sample identifier.
    data[, ":="("id" = .I)]

    if (to_data_object) {
      data <- as_data_object(
        data = data,
        sample_id_column = "id",
        outcome_column = c("time", "status"),
        outcome_type = outcome_type,
        include_features = c("nodes", "rx", "adhere"))
    }
    
  } else if (outcome_type == "multinomial") {
    # Load iris data set.
    data <- data.table::as.data.table(datasets::iris)

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Convert to a data object.
    if (to_data_object) {
      data <- as_data_object(
        data = data,
        sample_id_column = "sample_id",
        outcome_column = "Species",
        outcome_type = outcome_type)
    }
    
  } else if (outcome_type == "binomial") {
    # Load the cancer breast biopsy data set.
    data <- data.table::as.data.table(MASS::biopsy)

    # Rename columns.
    data.table::setnames(
      x = data,
      old = c("ID", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "class"),
      new = c(
        "id", "clump_thickness", "cell_size_uniformity", "cell_shape_uniformity",
        "marginal_adhesion", "epithelial_cell_size", "bare_nuclei",
        "bland_chromatin", "normal_nucleoli", "mitoses", "cell_malignancy"))

    # Keep unique samples. Some samples have the same id, but a different
    # outcome.
    data <- unique(data, by = "id")

    # Limit to 150 samples
    data <- data[1:150, ]

    # update sample identifier.
    data[, ":="("id" = .I)]

    # Convert to a data object. Exclude cell_size_uniformity, as these are
    # correlated and make it difficult to stable establish variable importance.
    if (to_data_object) {
      data <- as_data_object(
        data = data,
        sample_id_column = "id",
        outcome_column = "cell_malignancy",
        outcome_type = outcome_type,
        exclude_features = "cell_size_uniformity",
        class_levels = c("benign", "malignant"))
    }
    
  } else if (outcome_type == "continuous") {
    # Load the California Test Score Data Set
    data <- data.table::data.table(Ecdat::Caschool)

    # Drop distcod, district, county, readscr, mathscr
    data[, ":="(
      "distcod" = NULL, 
      "district" = NULL, 
      "county" = NULL, 
      "readscr" = NULL, 
      "mathscr" = NULL)]

    # Limit to 150 samples
    data <- data[271:420, ]

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Convert to a data object. Exclude mealpct, as this feature is correlated
    # to avginc.
    if (to_data_object) {
      data <- as_data_object(
        data = data,
        sample_id_column = "sample_id",
        outcome_column = "testscr",
        outcome_type = outcome_type,
        exclude_features = "mealpct")
    }
    
  } else if (outcome_type == "count") {
    # Load the Boston Housing data set
    data <- data.table::as.data.table(MASS::Boston)

    # Rename columns
    data.table::setnames(data,
      old = c(
        "crim", "zn", "indus", "chas", "nox", "rm", "age", "dis", "rad",
        "tax", "ptratio", "black", "lstat", "medv"),
      new = c(
        "per_capita_crime", "large_residence_proportion", "industry", "by_charles_river",
        "nox_concentration", "avg_rooms", "residence_before_1940_proportion",
        "distance_to_employment_centres", "radial_highway_accessibility", "property_tax_rate",
        "pupil_teacher_ratio", "african_american_metric", "lower_status_percentage", "median_house_value"
      )
    )

    # Convert by_charles_river to a factor.
    data$by_charles_river <- factor(
      x = data$by_charles_river, 
      levels = c(0, 1), 
      labels = c("no", "yes"))

    # Convert the median_house_value to the actual value.
    data[, "median_house_value" := median_house_value * 1000.0]

    # Limit to 150 samples
    data <- data[1:150, ]

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Convert to a data object.
    if (to_data_object) {
      data <- as_data_object(
        data = data,
        sample_id_column = "sample_id",
        outcome_column = "median_house_value",
        outcome_type = outcome_type)
    }
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  return(data)
}



test_create_small_good_data <- function(outcome_type) {
  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  # Now select a subset of the data.
  data@data <- data@data[fam_sample(
    seq_len(nrow(data@data)),
    size = 30,
    replace = FALSE,
    seed = 1844)]

  return(data)
}



test_create_invariant_good_data <- function(outcome_type) {
  # Create good dataset with one invariant feature.
  
  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  if (outcome_type == "survival") {
    data@data[, "nodes" := 4.0]
  } else if (outcome_type == "binomial") {
    data@data[, "clump_thickness" := 4.0]
  } else if (outcome_type == "multinomial") {
    data@data[, "Sepal_Length" := 3.0]
  } else if (outcome_type == "continuous") {
    data@data[, "calwpct" := 5.0]
  } else if (outcome_type == "count") {
    data@data[, "industry" := 3.0]
  }

  return(data)
}



test_create_good_data_without_censoring <- function(outcome_type) {
  # Dataset where none of the samples is censored.
  
  if (!outcome_type %in% c("survival", "competing_risk")) return(NULL)

  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  # Set all data to event.
  data@data[, "outcome_event" := 1]

  return(data)
}



test_create_good_data_one_censored <- function(outcome_type) {
  # Dataset where just one of the samples is censored.
  
  if (!outcome_type %in% c("survival", "competing_risk")) return(NULL)

  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  # Set all data to event.
  data@data[, "outcome_event" := 1]

  # Set one instance to censored.
  data@data[1L, "outcome_event" := 0]

  return(data)
}



test_create_good_data_few_censored <- function(outcome_type) {
  # Dataset where a minor fraction of the samples is censored.
  if (!outcome_type %in% c("survival", "competing_risk")) return(NULL)

  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  # Set all data to event.
  data@data[, "outcome_event" := 1]

  # Set a few instances to censored.
  data@data[seq_len(4), "outcome_event" := 0]

  return(data)
}



test_create_good_data_random_missing <- function(
    outcome_type,
    n_missing_frac = 0.05,
    seed = 1844,
    rstream_object = NULL) {
  # Some data points are NA, but not instances.
  if (!is.null(seed) && is.null(rstream_object)) {
    rstream_object <- .start_random_number_stream(seed = seed)
  }

  # Create test data.
  data <- test_create_good_data(outcome_type = outcome_type)

  # Get the number of rows and feature columns.
  n_rows <- nrow(data@data)
  feature_columns <- get_feature_columns(data)

  # Select the number of data points to randomise.
  n_randomise <- ceiling(n_missing_frac * n_rows * length(feature_columns))

  # Determine which data points should be randomised.
  random_data <- data.table::data.table(
    "row_id" = fam_sample(
      seq_len(n_rows),
      size = n_randomise,
      replace = TRUE,
      rstream_object = rstream_object),
    "feature" = fam_sample(
      feature_columns,
      size = n_randomise,
      replace = TRUE,
      rstream_object = rstream_object))
  
  # Select only unique data points.
  random_data <- unique(random_data)

  # Split for iteration.
  random_data <- split(
    random_data,
    by = "feature",
    drop = TRUE)

  # Update feature columns.
  for (feature in feature_columns) {
    if (!is.null(random_data[[feature]])) {
      # Split by numeric and factor features.
      if (is.factor(data@data[[feature]])) {
        data@data[random_data[[feature]]$row_id, (feature) := NA]
      } else {
        data@data[random_data[[feature]]$row_id, (feature) := NA_real_]
      }
    }
  }

  return(data)
}


test_create_empty_data <- function(outcome_type) {
  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  # Now empty the data.
  data@data <- head(data@data, n = 0)

  return(data)
}



test_create_data_without_feature <- function(outcome_type) {
  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type)

  # Remove features.
  data@data <- data@data[, mget(get_non_feature_columns(outcome_type))]

  return(data)
}



test_create_bootstrapped_data <- function(outcome_type, to_data_object = TRUE) {
  # Suppress NOTES due to non-standard evaluation in data.table
  sample_id <- NULL

  # Create good dataset first and work from there.
  data <- test_create_good_data(
    outcome_type = outcome_type,
    to_data_object = to_data_object)

  # Now keep only the first sample.
  if (to_data_object) {
    data@data <- data@data[
      fam_sample(seq_len(nrow(data@data)),
      size = nrow(data@data),
      replace = TRUE,
      seed = 1844
    )][order(sample_id)]
    
  } else {
    data@data <- data[
      fam_sample(seq_len(nrow(data)),
      size = nrow(data),
      replace = TRUE,
      seed = 1844
    )][order(sample_id)]
  }

  return(data)
}



test_create_one_sample_data <- function(outcome_type, to_data_object = TRUE) {
  # Create good dataset first and work from there.
  data <- test_create_good_data(
    outcome_type = outcome_type,
    to_data_object = to_data_object)

  # Now keep only the first sample.
  if (to_data_object) {
    data@data <- head(data@data, n = 1)
  } else {
    data <- head(data, n = 1)
  }

  return(data)
}



test_create_all_identical_data <- function(outcome_type) {
  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  # Now keep only the first sample.
  data@data <- head(data@data, n = 1)

  # Fill the dataset with the same sample.
  data@data <- data@data[rep.int(1L, 10)]

  # Set unique subject ids.
  data@data[, "sample_id" := .I]

  return(data)
}



test_create_single_feature_data <- function(outcome_type) {
  # Suppress NOTES due to non-standard evaluation in data.table
  etype <- median_house_value <- NULL

  if (outcome_type == "survival") {
    # Load colon dataset from the survival package
    data <- data.table::as.data.table(survival::colon)

    # Recurrence
    data <- data[etype == 1]

    # Limit to 150 samples
    data <- data[1:150, ]

    # update sample identifier.
    data[, ":="("id" = .I)]

    # Keep only first 150 samples for speed and only id, nodes, rx, extent,
    # adhere and outcome.
    data <- as_data_object(
      data = data,
      sample_id_column = "id",
      outcome_column = c("time", "status"),
      outcome_type = outcome_type,
      include_features = c("nodes"))
    
  } else if (outcome_type == "multinomial") {
    # Load iris data set.
    data <- data.table::as.data.table(datasets::iris)

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "sample_id",
      outcome_column = "Species",
      outcome_type = outcome_type,
      include_features = c("Petal.Length"))
    
  } else if (outcome_type == "binomial") {
    # Load the cancer breast biopsy data set.
    data <- data.table::as.data.table(MASS::biopsy)

    # Rename columns.
    data.table::setnames(data,
      old = c("ID", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "class"),
      new = c(
        "id", "clump_thickness", "cell_size_uniformity", "cell_shape_uniformity",
        "marginal_adhesion", "epithelial_cell_size", "bare_nuclei",
        "bland_chromatin", "normal_nucleoli", "mitoses", "cell_malignancy"
      )
    )

    # Keep unique samples. Some samples have the same id, but a different
    # outcome.
    data <- unique(data, by = "id")

    # Limit to 150 samples
    data <- data[1:150, ]

    # update sample identifier.
    data[, ":="("id" = .I)]

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "id",
      outcome_column = "cell_malignancy",
      outcome_type = outcome_type,
      class_levels = c("benign", "malignant"),
      include_features = "cell_size_uniformity"
    )
  } else if (outcome_type == "continuous") {
    # Load the California Test Score Data Set
    data <- data.table::data.table(Ecdat::Caschool)

    # Drop distcod, district, county, readscr, mathscr
    data[, ":="(
      "distcod" = NULL,
      "district" = NULL,
      "county" = NULL, 
      "readscr" = NULL, 
      "mathscr" = NULL)]

    # Limit to 150 samples
    data <- data[271:420, ]

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "sample_id",
      outcome_column = "testscr",
      outcome_type = outcome_type,
      include_features = "avginc")
    
  } else if (outcome_type == "count") {
    # Load the Boston Housing data set
    data <- data.table::as.data.table(MASS::Boston)

    # Rename columns
    data.table::setnames(data,
      old = c(
        "crim", "zn", "indus", "chas", "nox", "rm", "age", "dis", "rad",
        "tax", "ptratio", "black", "lstat", "medv"),
      new = c(
        "per_capita_crime", "large_residence_proportion", "industry", "by_charles_river",
        "nox_concentration", "avg_rooms", "residence_before_1940_proportion",
        "distance_to_employment_centres", "radial_highway_accessibility", "property_tax_rate",
        "pupil_teacher_ratio", "african_american_metric", "lower_status_percentage", "median_house_value"))

    # Convert by_charles_river to a factor.
    data$by_charles_river <- factor(x = data$by_charles_river, levels = c(0, 1), labels = c("no", "yes"))

    # Convert the median_house_value to the actual value.
    data[, "median_house_value" := median_house_value * 1000.0]

    # Limit to 150 samples
    data <- data[1:150, ]

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "sample_id",
      outcome_column = "median_house_value",
      outcome_type = outcome_type,
      include_features = "lower_status_percentage")
    
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  return(data)
}



test_create_single_feature_one_sample_data <- function(outcome_type) {
  # Create good dataset first and work from there.
  data <- test_create_single_feature_data(outcome_type = outcome_type)

  # Now keep only the first sample.
  data@data <- head(data@data, n = 1)

  return(data)
}



test_create_single_feature_invariant_data <- function(outcome_type) {
  # Create good dataset first and work from there.
  data <- test_create_single_feature_data(outcome_type = outcome_type)

  # Get the feature column
  feature_column <- get_feature_columns(data)

  # Set the feature to a fixed value.
  data@data[, (feature_column) := data@data[[feature_column]][1]]

  return(data)
}



test_create_single_feature_two_values_data <- function(outcome_type) {
  # Create good dataset first.
  data <- test_create_single_feature_data(outcome_type = outcome_type)

  # Get the feature columns
  feature_column <- get_feature_columns(data)

  # Find unique values of the feature and use the first 2.
  feature_values <- head(unique(data@data[[feature_column]]), n = 2L)

  # Fill all the rows while alternating the value.
  data@data[, (feature_column) := rep_len(feature_values, nrow(data@data))]

  return(data)
}



test_create_wide_data <- function(outcome_type) {
  # Suppress NOTES due to non-standard evaluation in data.table
  etype <- median_house_value <- NULL

  # Create random stream object so that the same numbers are produced every
  # time.
  r <- .start_random_number_stream(seed = 1844)

  if (outcome_type == "survival") {
    # Load colon dataset from the survival package
    data <- data.table::as.data.table(survival::colon)

    # Recurrence
    data <- data[etype == 1]

    # Remove superfluous columns
    data[, ":="("study" = NULL, "node4" = NULL, "etype" = NULL)]

    # Refactor columns
    data$sex <- factor(
      x = data$sex, 
      levels = c(0, 1),
      labels = c("female", "male"))
    data$obstruct <- factor(
      x = data$obstruct, 
      levels = c(0, 1),
      labels = c(FALSE, TRUE))
    data$perfor <- factor(
      x = data$perfor,
      levels = c(0, 1), 
      labels = c(FALSE, TRUE))
    data$adhere <- factor(
      x = data$adhere, 
      levels = c(0, 1),
      labels = c(FALSE, TRUE))
    data$differ <- factor(
      x = data$differ,
      levels = c(1, 2, 3), 
      labels = c("well", "moderate", "poor"), 
      ordered = TRUE)
    data$extent <- factor(
      x = data$extent,
      levels = c(1, 2, 3, 4), 
      labels = c("submucosa", "muscle", "serosa", "contiguous_structures"),
      ordered = TRUE)
    data$surg <- factor(
      x = data$surg, 
      levels = c(0, 1),
      labels = c("short", "long"))

    # Make the dataset small and wide (10 features)
    data <- data[1:5, ]
    data$status <- 1

    # update sample identifier.
    data[, ":="("id" = .I)]

    # Add twenty random features
    random_data <- lapply(
      seq_len(20), 
      function(ii, n, r) fam_rnorm(n = n, rstream_object = r),
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_", seq_len(20))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Keep only first 100 samples for speed and only id, nodes, rx, extent and
    # outcome.
    data <- as_data_object(
      data = data,
      sample_id_column = "id",
      outcome_column = c("time", "status"),
      outcome_type = outcome_type)
    
  } else if (outcome_type == "multinomial") {
    # Load iris data set.
    data <- data.table::as.data.table(datasets::iris)

    # Squeeze data
    data <- data[c(1, 2, 3, 80, 81, 82, 148, 149, 150)]

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Add twenty random features
    random_data <- lapply(
      seq_len(20),
      function(ii, n, r) fam_rnorm(n = n, rstream_object = r), 
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_", seq_len(20))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Add another 3 random features
    random_data <- lapply(
      seq_len(3),
      function(ii, n, r) {
        return(factor(fam_sample(
          c("red", "green", "blue"),
          size = n, 
          replace = TRUE, 
          rstream_object = r)))
      }, 
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_categorical_", seq_len(3))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "sample_id",
      outcome_column = "Species",
      outcome_type = outcome_type)
    
  } else if (outcome_type == "binomial") {
    # Load the cancer breast biopsy data set.
    data <- data.table::as.data.table(MASS::biopsy)

    # Rename columns.
    data.table::setnames(data,
      old = c(
        "ID", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "class"),
      new = c(
        "id", "clump_thickness", "cell_size_uniformity", "cell_shape_uniformity",
        "marginal_adhesion", "epithelial_cell_size", "bare_nuclei",
        "bland_chromatin", "normal_nucleoli", "mitoses", "cell_malignancy")
    )

    # Keep unique samples. Some samples have the same id, but a different
    # outcome.
    data <- unique(data, by = "id")

    # Limit to 10 samples
    data <- data[11:20, ]

    # update sample identifier.
    data[, ":="("id" = .I)]

    # Add twenty random features
    random_data <- lapply(
      seq_len(20),
      function(ii, n, r) fam_rnorm(n = n, rstream_object = r),
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_", seq_len(20))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Add another 3 random features
    random_data <- lapply(
      seq_len(3),
      function(ii, n, r) {
        return(factor(fam_sample(
          c("red", "green", "blue"),
          size = n,
          replace = TRUE,
          rstream_object = r)))
        },
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_categorical_", seq_len(3))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "id",
      outcome_column = "cell_malignancy",
      outcome_type = outcome_type,
      class_levels = c("benign", "malignant"))
    
  } else if (outcome_type == "continuous") {
    # Load the California Test Score Data Set
    data <- data.table::data.table(Ecdat::Caschool)

    # Drop distcod, district, county, readscr, mathscr
    data[, ":="(
      "distcod" = NULL, 
      "district" = NULL, 
      "county" = NULL, 
      "readscr" = NULL, 
      "mathscr" = NULL)]

    # Limit to 10 samples
    data <- data[411:420, ]

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Add twenty random features
    random_data <- lapply(
      seq_len(20), 
      function(ii, n, r) fam_rnorm(n = n, rstream_object = r),
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_", seq_len(20))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "sample_id",
      outcome_column = "testscr",
      outcome_type = outcome_type)
    
  } else if (outcome_type == "count") {
    # Load the Boston Housing data set
    data <- data.table::as.data.table(MASS::Boston)

    # Rename columns
    data.table::setnames(data,
      old = c(
        "crim", "zn", "indus", "chas", "nox", "rm", "age", "dis", "rad", 
        "tax", "ptratio", "black", "lstat", "medv"),
      new = c(
        "per_capita_crime", "large_residence_proportion", "industry", "by_charles_river",
        "nox_concentration", "avg_rooms", "residence_before_1940_proportion",
        "distance_to_employment_centres", "radial_highway_accessibility", "property_tax_rate",
        "pupil_teacher_ratio", "african_american_metric", "lower_status_percentage", "median_house_value"))

    # Convert by_charles_river to a factor.
    data$by_charles_river <- factor(
      x = data$by_charles_river,
      levels = c(0, 1),
      labels = c("no", "yes"))

    # Convert the median_house_value to the actual value.
    data[, "median_house_value" := median_house_value * 1000.0]

    # Limit to 10 samples
    data <- data[1:10, ]

    # Add sample identifier.
    data[, ":="("sample_id" = .I)]

    # Add twenty random features
    random_data <- lapply(
      seq_len(20),
      function(ii, n, r) fam_rnorm(n = n, rstream_object = r),
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_", seq_len(20))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Add another 3 random features
    random_data <- lapply(
      seq_len(3),
      function(ii, n, r) {
        return(factor(fam_sample(
          c("red", "green", "blue"),
          size = n,
          replace = TRUE,
          rstream_object = r)))
      },
      n = nrow(data),
      r = r)
    names(random_data) <- paste0("random_categorical_", seq_len(3))

    # Add to dataset
    data <- cbind(data, data.table::as.data.table(random_data))

    # Convert to a data object.
    data <- as_data_object(
      data = data,
      sample_id_column = "sample_id",
      outcome_column = "median_house_value",
      outcome_type = outcome_type)
    
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  return(data)
}



test_create_bad_data <- function(outcome_type, add_na_data = FALSE) {
  # add_na_data argument is intended for integration tests, where we have to
  # circumvent a check on the outcome classes. We do this by keeping these
  # classes in, and assigning NA to rows of one class, causing the data to pass
  # the check, but have the rows be removed afterwards.

  # Suppress NOTES due to non-standard evaluation in data.table
  outcome <- NULL

  # Create good dataset first and work from there.
  data <- test_create_good_data(outcome_type = outcome_type)

  if (outcome_type == "survival") {
    # For survival data it would be really bad if all data are censored.
    data@data[, "outcome_event" := 0]
    
  } else if (outcome_type == "multinomial") {
    # For multinomial data, having not all classes is bad.

    if (add_na_data) {
      # Assign NA to the rows containing the virginica class.

      # Identify the feature columns.
      feature_columns <- get_feature_columns(data)

      # Update feature columns.
      for (feature in feature_columns) {
        if (is.factor(data@data[[feature]])) {
          data@data[outcome == "virginica", (feature) := NA]
        } else {
          data@data[outcome == "virginica", (feature) := NA_real_]
        }
      }
    } else {
      # Select 2 of 3 classes by leaving virginica out.
      data@data <- data@data[outcome %in% c("setosa", "versicolor"), ]
    }
    
  } else if (outcome_type == "binomial") {
    # For binomial data, having a single class is bad.

    if (add_na_data) {
      # Assign NA to the rows containing the malignant class.

      # Identify the feature columns.
      feature_columns <- get_feature_columns(data)

      # Update feature columns.
      for (feature in feature_columns) {
        if (is.factor(data@data[[feature]])) {
          data@data[outcome == "malignant", (feature) := NA]
        } else {
          data@data[outcome == "malignant", (feature) := NA_real_]
        }
      }
    } else {
      # Assign everything to the benign class.
      data@data[, "outcome" := "benign"]
    }
    
  } else if (outcome_type == "continuous") {
    # For continuous data, it would be bad if all outcome values are invariant.
    data@data[, "outcome" := 500.0]
    
  } else if (outcome_type == "count") {
    # For count data it would be bad if all outcome values are invariant
    data@data[, "outcome" := 50000]
    
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  return(data)
}


test_create_small_bad_data <- function(outcome_type) {
  # Create good dataset first and work from there.
  data <- test_create_bad_data(outcome_type = outcome_type)

  # Now select a subset of the data.
  data@data <- data@data[fam_sample(seq_len(nrow(data@data)),
    size = 30,
    replace = FALSE,
    seed = 1844)]

  return(data)
}



test_create_prospective_data <- function(outcome_type) {
  # Prospective data has NA for outcome.
  
  data <- test_create_good_data(outcome_type = outcome_type)
  
  if (outcome_type %in% c("survival", "competing_risk")) {
    data@data[, ":="(
      "outcome_time" = NA,
      "outcome_event" = NA)]
  } else {
    data@data[, ":="("outcome" = NA)]
  }
  
  return(data)
}



test_create_partially_prospective_data <- function(outcome_type) {
  # Prospective data has NA for outcome for a few samples, but not all.

  data <- test_create_good_data(outcome_type = outcome_type)

  if (outcome_type %in% c("survival", "competing_risk")) {
    data@data[c(1, 2), ":="(
      "outcome_time" = NA,
      "outcome_event" = NA)]
  } else {
    data@data[c(1, 2), ":="("outcome" = NA)]
  }

  return(data)
}



test_create_mostly_prospective_data <- function(outcome_type) {
  # Prospective data has NA for all but one sample.

  # Suppress NOTES due to non-standard evaluation in data.table
  sample_id <- NULL

  data <- test_create_good_data(outcome_type = outcome_type)

  if (outcome_type %in% c("survival", "competing_risk")) {
    data@data[sample_id > 1L, ":="(
      "outcome_time" = NA,
      "outcome_event" = NA)]
  } else {
    data@data[sample_id > 1L, ":="("outcome" = NA)]
  }

  return(data)
}



test_create_synthetic_series_data <- function(
    outcome_type,
    n_batch = 3,
    n_samples = 10,
    n_series = 3,
    n_rep = 3,
    n_numeric = 4L,
    rare_outcome = FALSE,
    seed = 1844,
    rstream_object = NULL) {
  # Suppress NOTES due to non-standard evaluation in data.table
  batch_id <- feature_1 <- feature_2 <- feature_3 <- feature_4 <- NULL

  # Create random stream object so that the same numbers are produced every
  # time.
  if (is.null(rstream_object)) {
    r <- .start_random_number_stream(seed = seed)
  } else {
    r <- rstream_object
  }

  # Determine the number of series instances.
  n_series_instances <- n_batch * n_samples * n_series

  # Draw random numbers for three features.
  feature_1 <- fam_runif(n = n_series_instances, min = 0.0, max = 1.0, rstream_object = r)
  feature_2 <- fam_runif(n = n_series_instances, min = 0.0, max = 2.0, rstream_object = r)
  feature_3 <- fam_runif(n = n_series_instances, min = 0.0, max = 2.0, rstream_object = r)
  feature_4 <- fam_runif(n = n_series_instances, min = 0.0, max = 1.0, rstream_object = r)

  # Determine the raw outcome.
  outcome_raw <- feature_1 + feature_2 + feature_3 + feature_4

  if (outcome_type == "binomial") {
    # Convert to 0, 1
    outcome_value <- outcome_raw > 3.0
    outcome_value <- factor(
      x = outcome_value,
      levels = c(FALSE, TRUE),
      labels = c("0", "1"))
    
  } else if (outcome_type == "multinomial") {
    outcome_value <- numeric(n_series_instances)

    # Convert to 0 (x < 2.59), 1 (2.59 < x < 3.41), 2 (3.41 < x < 6)
    outcome_value[outcome_raw < 2.59] <- 0.0
    outcome_value[outcome_raw >= 2.59 & outcome_raw < 3.41] <- 1.0
    outcome_value[outcome_raw >= 3.41 & outcome_raw < 6.00] <- 2.0

    if (rare_outcome) {
      outcome_value[length(outcome_value)] <- 3.0
      outcome_value <- factor(
        x = outcome_value,
        levels = c(0.0, 1.0, 2.0, 3.0),
        labels = c("0", "1", "2", "3"))
      
    } else {
      outcome_value <- factor(
        x = outcome_value,
        levels = c(0.0, 1.0, 2.0),
        labels = c("0", "1", "2"))
      
    }
  } else if (outcome_type == "continuous") {
    outcome_value <- outcome_raw
    
  } else if (outcome_type == "count") {
    outcome_value <- round(outcome_raw * 100)
    
  } else if (outcome_type == "survival") {
    # Outcome follows an exponential distribution.
    outcome_time <- exp(outcome_raw)
    outcome_event <- rep_len(1, length.out = n_series_instances)
    
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  # Create basic table. Sample identifiers are explicitly repeated for different
  # batches.
  data <- data.table::data.table(
    "batch_id" = rep(seq_len(n_batch), each = n_samples * n_series),
    "sample_id" = rep(seq_len(n_samples), each = n_series, times = n_batch),
    "series_id" = rep(seq_len(n_series), times = n_batch * n_samples),
    "feature_1" = feature_1,
    "feature_2" = feature_2,
    "feature_3" = feature_3,
    "feature_4" = feature_4)

  # Add outcome.
  if (outcome_type %in% "survival") {
    data[, ":="(
      "outcome_time" = outcome_time,
      "outcome_event" = outcome_event)]
    outcome_column <- c("outcome_time", "outcome_event")
    
  } else {
    data[, ":="("outcome" = outcome_value)]
    outcome_column <- "outcome"
  }

  # Create batch-offsets
  data[, ":="(
    "feature_1" = feature_1 + batch_id - 1.0,
    "feature_2" = feature_2 + batch_id - 1.0,
    "feature_3" = feature_3 + batch_id - 1.0,
    "feature_4" = feature_4 + batch_id - 1.0)]

  # Create repetitions.
  if (n_rep > 1) {
    repeated_rows <- rep(seq_len(n_series_instances), each = n_rep)
    data <- data[repeated_rows, ]

    # Add some noise to features.
    data[, ":="(
      "feature_1" = feature_1 + fam_rnorm(n = n_rep * n_series_instances, mean = 0.0, sd = 0.125, rstream_object = r),
      "feature_2" = feature_2 + fam_rnorm(n = n_rep * n_series_instances, mean = 0.0, sd = 0.125, rstream_object = r),
      "feature_3" = feature_3 + fam_rnorm(n = n_rep * n_series_instances, mean = 0.0, sd = 0.125, rstream_object = r),
      "feature_4" = feature_4 + fam_rnorm(n = n_rep * n_series_instances, mean = 0.0, sd = 0.125, rstream_object = r))]

    data[feature_1 <= 0.0, "feature_1" := 0.01]
    data[feature_2 <= 0.0, "feature_2" := 0.01]
    data[feature_3 <= 0.0, "feature_3" := 0.01]
    data[feature_4 <= 0.0, "feature_4" := 0.01]
  }

  if (n_numeric < 4) data$feature_1 <- factor(floor(data$feature_1))
  if (n_numeric < 3) data$feature_2 <- factor(floor(data$feature_2))
  if (n_numeric < 2) data$feature_3 <- factor(floor(data$feature_3))
  if (n_numeric < 1) data$feature_4 <- factor(floor(data$feature_4))

  # Convert to a data object.
  data <- as_data_object(
    data = data,
    batch_id_column = "batch_id",
    sample_id_column = "sample_id",
    series_id_column = "series_id",
    outcome_column = outcome_column,
    outcome_type = outcome_type
  )

  return(data)
}



test_create_synthetic_series_one_outcome <- function(
    outcome_type,
    n_numeric = 4L,
    seed = 1844,
    rstream_object = NULL) {
  # Create test data.
  data <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = n_numeric,
    seed = seed,
    rstream_object = rstream_object)

  if (outcome_type %in% c("binomial", "multinomial")) {
    data@data[, "outcome" := "0"]
  } else if (outcome_type %in% c("count", "continuous")) {
    data@data[, "outcome" := 1]
  } else if (outcome_type == "survival") {
    data@data[, ":="("outcome_time" = 1.25, "outcome_event" = 1)]
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  return(data)
}



test_create_synthetic_series_one_sample_data <- function(
    outcome_type,
    n_numeric = 4L,
    seed = 1844,
    rstream_object = NULL) {
  # Create test data.
  data <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = n_numeric,
    seed = seed,
    rstream_object = rstream_object)

  # Select the first instance
  data@data <- head(data@data, n = 1L)

  return(data)
}



test_create_synthetic_series_invariant_feature_data <- function(
    outcome_type,
    n_numeric = 4L,
    seed = 1844,
    rstream_object = NULL) {
  # Create test data.
  data <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = n_numeric,
    seed = seed,
    rstream_object = rstream_object)

  # Select the first instance
  data@data$feature_1 <- data@data$feature_1[1]
  data@data$feature_2 <- data@data$feature_2[1]
  data@data$feature_3 <- data@data$feature_3[1]
  data@data$feature_4 <- data@data$feature_4[1]

  return(data)
}



test_create_synthetic_series_one_feature_invariant_data <- function(
    outcome_type,
    n_numeric = 4L,
    seed = 1844,
    rstream_object = NULL) {
  # Create test data.
  data <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = n_numeric,
    seed = seed,
    rstream_object = rstream_object)

  # Select the first instance for feature 2.
  data@data$feature_2 <- data@data$feature_2[1]

  return(data)
}



test_create_synthetic_series_na_data <- function(
    outcome_type,
    n_numeric = 4L,
    n_missing_frac = 0.1,
    seed = 1844,
    rstream_object = NULL) {
  # Some instances are completely NA.

  # Create test data.
  data <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = n_numeric,
    seed = seed,
    rstream_object = rstream_object)

  # Select which rows will be updated.
  n_rows <- nrow(data@data)
  na_rows <- fam_sample(seq_len(n_rows),
    size = ceiling(n_missing_frac * n_rows),
    replace = FALSE,
    seed = seed,
    rstream_object = rstream_object)

  # Identify the feature columns.
  feature_columns <- get_feature_columns(data)

  # Update feature columns.
  for (feature in feature_columns) {
    if (is.factor(data@data[[feature]])) {
      data@data[na_rows, (feature) := NA]
    } else {
      data@data[na_rows, (feature) := NA_real_]
    }
  }

  return(data)
}



test_create_synthetic_series_random_na_data <- function(
    outcome_type,
    n_numeric = 4L,
    n_missing_frac = 0.1,
    seed = 1844,
    rstream_object = NULL) {
  # Some data points are NA, but not instances.

  if (!is.null(seed) && is.null(rstream_object)) {
    rstream_object <- .start_random_number_stream(seed = seed)
  }

  # Create test data.
  data <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = n_numeric,
    rstream_object = rstream_object
  )

  # Get the number of rows and feature columns.
  n_rows <- nrow(data@data)
  feature_columns <- get_feature_columns(data)

  # Select the number of data points to randomise.
  n_randomise <- ceiling(n_missing_frac * n_rows * length(feature_columns))

  # Determine which data points should be randomised.
  random_data <- data.table::data.table(
    "row_id" = fam_sample(seq_len(n_rows),
      size = n_randomise,
      replace = TRUE,
      rstream_object = rstream_object),
    "feature" = fam_sample(feature_columns,
      size = n_randomise,
      replace = TRUE,
      rstream_object = rstream_object)
  )
  
  # Select only unique data points.
  random_data <- unique(random_data)

  # Split for iteration.
  random_data <- split(random_data, by = "feature", drop = TRUE)

  # Update feature columns.
  for (feature in feature_columns) {
    if (!is.null(random_data[[feature]])) {
      # Split by numeric and factor features.
      if (is.factor(data@data[[feature]])) {
        data@data[random_data[[feature]]$row_id, (feature) := NA]
      } else {
        data@data[random_data[[feature]]$row_id, (feature) := NA_real_]
      }
    }
  }

  return(data)
}



test_create_synthetic_series_one_feature_all_na_data <- function(
    outcome_type,
    n_numeric = 4L,
    seed = 1844,
    rstream_object = NULL) {
  # Suppress NOTES due to non-standard evaluation in data.table
  feature_2 <- NULL

  # Create test data.
  data <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = n_numeric,
    seed = seed,
    rstream_object = rstream_object)

  # Set the first feature column to NA.
  if (is.factor(data@data[["feature_2"]])) {
    data@data[, feature_2 := NA]
  } else {
    data@data[, feature_2 := NA_real_]
  }

  return(data)
}



test_create_multiple_synthetic_series <- function(outcome_type) {
  # The idea here is to create multiple synthetic datasets that together
  # represent extreme variation in data composition.

  ..extend_feature_set <- function(data) {
    # Get feature columns.
    original_feature_columns <- get_feature_columns(data)

    # Find new feature columns for the correlated features
    new_feature_columns <- paste0(
      "feature_", seq_along(original_feature_columns) + length(original_feature_columns))

    # Add in correlated features.
    for (ii in seq_along(original_feature_columns)) {
      data@data[, (new_feature_columns[ii]) := get(original_feature_columns[ii])]
    }

    return(data)
  }

  # Draw the first dataset.
  data_1 <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = 3L,
    n_samples = 20,
    seed = 1)

  # Add correlated features.
  data_1 <- ..extend_feature_set(data_1)

  # Draw the second dataset.
  data_2 <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = 3L,
    n_samples = 20,
    seed = 2)
  # Do not add correlated features to dataset 2.

  # Draw a third dataset.
  data_3 <- test_create_synthetic_series_data(
    outcome_type = outcome_type,
    n_numeric = 3L,
    n_samples = 20,
    seed = 3)

  # Add correlated features, but remove the original features.
  data_3 <- ..extend_feature_set(data_3)
  data_3@data[, ":="(
    "feature_1" = NULL,
    "feature_2" = NULL,
    "feature_3" = NULL,
    "feature_4" = NULL)]

  # Draw a fourth dataset that cannot be used for training, e.g. contains just
  # one sample.
  data_4 <- test_create_synthetic_series_one_outcome(
    outcome_type = outcome_type,
    n_numeric = 3L,
    seed = 4
  )

  # Add correlated features.
  data_4 <- ..extend_feature_set(data_4)

  return(list(
    "set_1" = data_1,
    "set_2" = data_2,
    "set_3" = data_3,
    "set_4" = data_4))
}




test_create_synthetic_correlated_data <- function(
    ...,
    seed = 1844,
    rstream_object = NULL,
    cluster_size = c(1, 1, 1, 1),
    mix_feature_types = TRUE,
    allow_anti_correlation = TRUE) {
  # Create random stream object so that the same numbers are produced every
  # time.
  if (is.null(rstream_object)) {
    r <- .start_random_number_stream(seed = seed)
  } else {
    r <- rstream_object
  }

  # Create basic data.
  base_data <- do.call(
    test_create_synthetic_series_data,
    args = c(
      list("rstream_object" = r),
      list(...)))

  if (length(cluster_size) != get_n_features(base_data)) {
    stop(paste0("The cluster_size argument should match the number of features."))
  }

  # Get feature names prior to extending the dataset.
  original_feature_names <- get_feature_columns(base_data)

  # Create copy of data.
  data <- base_data
  data@data <- data.table::copy(base_data@data)

  for (ii in seq_along(cluster_size)) {
    # Skip if no clusters are required.
    if (cluster_size[ii] == 1) next()

    # Isolate feature data. Force reading this data just in case.
    x <- base_data@data[[original_feature_names[ii]]]

    # Generate clusters.
    for (jj in seq_len(cluster_size[ii])) {
      # Create the name of the new feature.
      new_feature_name <- paste0(original_feature_names[ii], "_", LETTERS[jj])

      if (jj == 1) {
        # The first feature should just be renamed.
        data.table::setnames(data@data,
          old = original_feature_names[ii],
          new = new_feature_name)
        
      } else if (jj == 2 && is.factor(x) && mix_feature_types) {
        # Add numeric feature when mixing data types.
        data@data[, (new_feature_name) := as.numeric(x) * 1.1]
        
      } else if (is.factor(x)) {
        # For categorical features, remix the levels.
        y <- factor(x,
          levels = levels(x),
          labels = levels(x)[fam_sample(
            seq_along(levels(x)),
            size = length(levels(x)),
            replace = FALSE,
            rstream_object = r)])
        
        # Set feature.
        data@data[, (new_feature_name) := y]
        
      } else {
        # For numerical features, introduce an offset and scaling. We scale
        # between 0.5 and 1.5 or -0.5 and -1.5 to avoid multiplying by 0. The
        # negative values are used for strong anti-correlation, which
        if (jj %% 2 == 0 || !allow_anti_correlation) {
          r_scale <- fam_runif(n = 1L, min = 0.5, max = 1.5, rstream_object = r)
        } else {
          r_scale <- fam_runif(n = 1L, min = -1.5, max = -0.5, rstream_object = r)
        }

        r_shift <- fam_runif(n = 1L, min = -1.0, max = 1.0, rstream_object = r)

        y <- x * r_scale + r_shift

        data@data[, (new_feature_name) := y]
      }
    }
  }

  # Order columns nicely.
  data.table::setcolorder(
    x = data@data, 
    neworder = c(
      get_non_feature_columns(data),
      sort(get_feature_columns(data))))

  return(data)
}



test_create_synthetic_correlated_one_feature_invariant_data <- function(
    ...,
    cluster_size = c(1, 1, 1, 1)) {
  # Set the size of the second cluster to 1, always.
  cluster_size[2] <- 1

  # Create test data.
  data <- do.call(
    test_create_synthetic_correlated_data,
    args = c(
      list("cluster_size" = cluster_size),
      list(...)))

  # Select the first instance for feature 2.
  data@data$feature_2 <- data@data$feature_2[1]

  return(data)
}



test_create_synthetic_correlated_one_sample_data <- function(...) {
  # Create test data.
  data <- do.call(
    test_create_synthetic_correlated_data,
    args = list(...))

  # Select the first instance
  data@data <- head(data@data, n = 1L)

  return(data)
}



test_create_synthetic_correlated_one_outcome_data <- function(
    ...,
    outcome_type) {
  
  # Create test data.
  data <- do.call(
    test_create_synthetic_correlated_data,
    args = c(
      list("outcome_type" = outcome_type),
      list(...)))

  if (outcome_type %in% c("binomial", "multinomial")) {
    data@data[, "outcome" := "0"]
  } else if (outcome_type %in% c("count", "continuous")) {
    data@data[, "outcome" := 1]
  } else if (outcome_type == "survival") {
    data@data[, ":="("outcome_time" = 1.25, "outcome_event" = 1)]
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  return(data)
}



test_create_synthetic_correlated_bad_outcome_data <- function(
    ...,
    outcome_type) {
  # Create test data.
  data <- do.call(
    test_create_synthetic_correlated_data,
    args = c(
      list("outcome_type" = outcome_type),
      list(...)))

  if (outcome_type %in% c("binomial", "multinomial")) {
    data@data[, "outcome" := "0"]
  } else if (outcome_type %in% c("count", "continuous")) {
    data@data[, "outcome" := 1]
  } else if (outcome_type == "survival") {
    data@data[, ":="("outcome_event" = 0)]
  } else {
    ..error_outcome_type_not_implemented(outcome_type)
  }

  return(data)
}



test_data_drop_rare_feature_levels <- function(data) {
  # Replace rare levels in feature data.

  # Prevent NOTE by non-standard use in data.table.
  n <- NULL

  for (ii in get_feature_columns(data)) {
    # Skip numeric data.
    if (is.numeric(data@data[[ii]])) next

    # Find the rarest level of the categorical feature, and set replacement
    # level.
    level_data <- data@data[, list("n" = .N), by = c(ii)][order(n)]
    rare_level <- head(level_data[[ii]], n = 1L)
    replacement_level <- tail(level_data[[ii]], n = 1L)
    data@data[get(ii) == rare_level, (ii) := replacement_level]
  }

  return(data)
}

Try the familiar package in your browser

Any scripts or data that you put into this service are public.

familiar documentation built on Sept. 30, 2024, 9:18 a.m.