Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ecosci/data/spatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,22 @@ def assign_spatial_blocks(
km = KMeans(n_clusters=n_blocks, random_state=random_state, n_init=10)
labels = km.fit_predict(coords)

counts = pd.Series(labels).value_counts()
min_count = int(counts.min())
if min_count < 2:
n_singleton = int((counts < 2).sum())
print(
f"Warning: {n_singleton} of {n_blocks} spatial block(s) have fewer than "
f"2 samples (smallest: {min_count}). A CV fold built from one of these "
f"has an undefined test-set R^2/variance and will show up as NaN in "
f"per-fold metrics and permutation-based feature importance. Consider "
f"lowering n_blocks (currently {n_blocks})."
)
Comment on lines +52 to +58
elif min_count < 5:
print(
f"Warning: smallest spatial block has only {min_count} samples out of "
f"{n_blocks} blocks. Per-fold metrics for that block may be noisy/unstable. "
f"Consider lowering n_blocks if this is unintentional."
)

return pd.Series(labels, index=df.index, name="spatial_block")
19 changes: 13 additions & 6 deletions ecosci/evaluation/feature_importance.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,12 @@ def extract_feature_importance(

# Average importances across all models
if all_importances:
# Average across all seeds
avg_feature_importances = np.mean(all_importances, axis=0)
std_feature_importances = np.std(all_importances, axis=0)
# Average across all seeds (nan-aware: for regression, a
# single-sample test set makes permutation importance's R^2
# scorer undefined for that seed, and a plain mean would let
# that one NaN wipe out every feature's average)
avg_feature_importances = np.nanmean(all_importances, axis=0)
std_feature_importances = np.nanstd(all_importances, axis=0)

print(f"\n{model_name.upper()}:")
print(f"{'-'*80}")
Expand Down Expand Up @@ -272,11 +275,15 @@ def extract_cv_feature_importance(
fold_avg_importance = np.mean(fold_importances_list, axis=0)
fold_importances_dict[fold_id] = fold_avg_importance

# Average importances across all folds
# Average importances across all folds (nan-aware: for regression,
# a fold whose test set has fewer than 2 samples makes permutation
# importance's R^2 scorer undefined for that fold, and a plain
# mean would let that one NaN-only fold wipe out every feature's
# average)
if fold_importances_dict:
all_fold_importances = list(fold_importances_dict.values())
avg_feature_importances = np.mean(all_fold_importances, axis=0)
std_feature_importances = np.std(all_fold_importances, axis=0)
avg_feature_importances = np.nanmean(all_fold_importances, axis=0)
std_feature_importances = np.nanstd(all_fold_importances, axis=0)

print(f"\n{model_name.upper()}:")
print(f"{'-'*80}")
Expand Down
8 changes: 7 additions & 1 deletion ecosci/evaluation/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ def compute_classification_metrics(y_true, y_pred, y_proba=None) -> Dict[str, An

out = {}
observed_labels = np.unique(np.concatenate([np.asarray(y_true), np.asarray(y_pred)]))
average_mode = "binary" if len(observed_labels) == 2 else "macro"
# "binary" average is only valid when the task truly has exactly 2 classes.
# Use y_proba's column count when available to detect multiclass tasks even if
# a CV fold happens to contain only 2 labels (e.g. a spatial block missing a class).
if y_proba is not None and getattr(y_proba, "ndim", 0) == 2:
average_mode = "binary" if y_proba.shape[1] == 2 else "macro"
else:
average_mode = "binary" if set(observed_labels.tolist()) == {0, 1} else "macro"
out["accuracy"] = accuracy_score(y_true, y_pred)
out["balanced_accuracy"] = balanced_accuracy_score(y_true, y_pred)
out["precision"] = precision_score(
Expand Down
Loading