Comparing Feature Engineering Approaches

All combinations

We want to design a structure that incorporates all these features. compare_methods() function, then unpacks what the results mean.

We use the bundled steel_industry dataset: one full year of 15-minute energy measurements from a Korean steel plant, including reactive power, power factor, CO2 emissions, and time-of-day indicators.

library(cyclicwave)
data(steel_industry)

Preparing the data

Three preprocessing steps:

  1. Thinning: keep every 10th row to reduce samples. The clustering analysis becomes both faster and more meaningful.
  2. Select numeric columns: discard date and categorical columns.
  3. Z-score normalization: required for any distance-based method.
data_thin    <- thin_data(steel_industry, step = 10)
numeric_data <- select_numeric_columns(data_thin)
data_scaled  <- normalize_features(numeric_data, method = "zscore")

dim(data_scaled)
#> [1] 3504    7

Ground-truth labels

steel_industry doesn’t ship with explicit class labels, but Usage_kWh gives us natural ones: low, medium, and high consumption regimes, defined by tertile cutoffs. We use these as a yardstick for evaluating how meaningful each clustering result is.

true_labels <- label_by_quantile(data_thin$Usage_kWh,
                                 probs = c(1/3, 2/3))
table(true_labels)
#> true_labels
#>    1    2    3 
#> 1184 1152 1168

Each class has roughly N/3 observations.

Defining the feature methods

compare_methods() takes a named list of feature extractors. Each is just a function that takes the raw data and returns a numeric feature matrix.

feature_methods <- list(
  pca_only = function(d) {
    pca <- prcomp(d, center = FALSE, scale. = FALSE)
    pca$x[, 1:3]
  },
  pca_circular = function(d) {
    pca <- prcomp(d, center = FALSE, scale. = FALSE)
    phase <- compute_phase(d, axis = "feature")
    circ <- extract_circular_features(phase)
    cbind(pca$x[, 1:3], circ)
  }
)

Defining the clustering methods

We try DBSCAN with two different parameter settings: one with a larger neighborhood radius (loose) and one with a smaller one (tight). This is a parameter sweep disguised as a method comparison.

cluster_methods <- list(
  dbscan_loose = list(fn = run_dbscan, params = list(eps = 0.5, min_pts = 8)),
  dbscan_tight = list(fn = run_dbscan, params = list(eps = 0.3, min_pts = 5))
)

One call to rule them all

compare_methods() runs every combination, evaluates each with the requested metrics, and returns a single comparison table.

comparison <- compare_methods(
  data            = data_scaled,
  feature_methods = feature_methods,
  cluster_methods = cluster_methods,
  metrics         = c("dbi", "accuracy", "n_clusters", "n_noise"),
  true_labels     = true_labels,
  normalize       = NULL,
  verbose         = FALSE
)

print(comparison)
#>   feature_method cluster_method       dbi  accuracy n_clusters n_noise
#> 1       pca_only   dbscan_loose 0.9056772 0.5907534          4      15
#> 2       pca_only   dbscan_tight 0.6312147 0.7285959         13      56
#> 3   pca_circular   dbscan_loose 0.5957794 0.7619863         14     126
#> 4   pca_circular   dbscan_tight 0.8596812 0.7796804         38     254

Four rows, one per combination, four metrics each. Now to read it.