Skip to content

Commit 73f6c95

Browse files
committed
fix: correct citations
1 parent f52646b commit 73f6c95

3 files changed

Lines changed: 95 additions & 95 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ The recommendations in Resample Lab are based on peer-reviewed research. Below i
7979
| He & Garcia | 2009 | Learning from Imbalanced Data | IEEE TKDE, 21(9), 1263–1284 | Comprehensive imbalanced learning survey |
8080
| Blagus & Lusa | 2013 | SMOTE for high-dimensional class-imbalanced data | BMC Bioinformatics, 14(1), 106 | SMOTE degradation in high dimensions |
8181
| Barua et al. | 2014 | MWMOTE—Majority Weighted Minority Oversampling | IEEE TKDE, 26(2), 405–425 | Weighted oversampling for hard-to-learn instances |
82-
| Elhassan & Aljurf | 2016 | Class imbalance problem: A review of recent techniques | J. Applied Sciences, 16(8), 314–328 | Comprehensive resampling technique review |
82+
| Dal Pozzolo et al. | 2015 | Calibrating Probability with Undersampling | IEEE SSCI 2015 | Resampling distorts probability estimates |
8383
| García et al. | 2020 | Understanding the apparent superiority of over-sampling | Expert Systems with Applications, 158, 113026 | Oversampling increases safe minority samples |
84-
| Hasanin et al. | 2020 | Over- and Under-sampling Approach for EISM Data | Frontiers in Public Health, 8, 178 | HUSDOS-Boost; stratified bagging for EISM |
84+
| Fujiwara et al. | 2020 | Over- and Under-sampling Approach for EISM Data | Frontiers in Public Health, 8, 178 | HUSDOS-Boost; stratified bagging for EISM |
8585
| Yang et al. | 2024 | A review on over-sampling techniques for multi-class imbalanced datasets | Frontiers in Digital Health, 6, 1430245 | Multi-class oversampling review |
8686
| Zhao et al. | 2025 | A Survey on Small Sample Imbalance Problem | arXiv:2504.14800 | EISM definitions; hybrid ensemble recommendations |
8787

@@ -116,7 +116,7 @@ Based on the literature, Resample Lab identifies three critical regimes:
116116

117117
| Regime | Trigger Condition | Rationale | Primary Sources |
118118
|--------|-------------------|-----------|-----------------|
119-
| **🔴 Tiny Minority / EISM** | Minority < 50 samples OR EPV < 10 | Synthetic methods may generate unreliable samples; overfitting risk high | Zhao et al. 2025, Hasanin et al. 2020 |
119+
| **🔴 Tiny Minority / EISM** | Minority < 50 samples OR EPV < 10 | Synthetic methods may generate unreliable samples; overfitting risk high | Zhao et al. 2025, Fujiwara et al. 2020 |
120120
| **🟠 High-Dimensional** | Features > 100 | SMOTE fails in high-dimensional spaces due to sparse neighborhoods | Blagus & Lusa 2013 |
121121
| **🟡 Large-Scale** | Total samples > 50,000 | Undersampling preferred for computational efficiency | Drummond & Holte 2003, Van Hulse et al. 2007 |
122122

logic/recommendationEngine.ts

Lines changed: 82 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,14 @@ const sigmoid = (val: number, threshold: number, k: number = 12) => {
7676

7777
const getMinorityThreshold = (p: number): number => {
7878
if (p <= THRESHOLDS.DIM_LOW) return THRESHOLDS.MIN_SAMPLES_BASE;
79-
if (p <= THRESHOLDS.DIM_HIGH) return THRESHOLDS.MIN_SAMPLES_BASE + (p - THRESHOLDS.DIM_LOW) * 2.5;
80-
return 400 + 200 * Math.log10(p / 100);
79+
if (p <= THRESHOLDS.DIM_HIGH) return THRESHOLDS.MIN_SAMPLES_BASE + (p - THRESHOLDS.DIM_LOW) * 2.5;
80+
return 400 + 200 * Math.log10(p / 100);
8181
};
8282

8383
const getDistanceStability = (sparsity: number, homogeneity: number): number => {
8484
if (sparsity < 0.3) return 1.0;
8585
const baseInstability = Math.pow(sparsity, 2.5);
86-
const distributionImpact = 0.2 + (0.8 * homogeneity);
86+
const distributionImpact = 0.2 + (0.8 * homogeneity);
8787
const risk = baseInstability * distributionImpact;
8888
return Math.max(0, 1 - risk);
8989
};
@@ -116,26 +116,26 @@ interface WeightResult {
116116
wHybrid: number;
117117
wBaseline: number;
118118
stability: number;
119-
dominantOriginal: StrategyType | null;
119+
dominantOriginal: StrategyType | null;
120120
}
121121

122122
const calculateWeights = (
123-
p: number,
124-
nMin: number,
125-
nTotal: number,
126-
sparsity: number,
123+
p: number,
124+
nMin: number,
125+
nTotal: number,
126+
sparsity: number,
127127
homogeneity: number
128128
): WeightResult => {
129129
const threshold = getMinorityThreshold(p);
130-
130+
131131
// 1. Dimensionality Factors
132-
const isLowDim = 1 - sigmoid(p, THRESHOLDS.DIM_LOW, 12);
133-
const isHighDim = sigmoid(p, THRESHOLDS.DIM_HIGH, 12);
132+
const isLowDim = 1 - sigmoid(p, THRESHOLDS.DIM_LOW, 12);
133+
const isHighDim = sigmoid(p, THRESHOLDS.DIM_HIGH, 12);
134134
const isMedDim = 1 - isLowDim - isHighDim;
135135

136136
// 2. Count & Ratio Factors
137137
const isTinyMin = 1 - sigmoid(nMin, threshold, 10);
138-
138+
139139
// Efficiency Pressure: Driven by total size (cost) OR extreme imbalance
140140
const ratio = nTotal / Math.max(1, nMin);
141141
const isHighRatio = sigmoid(ratio, THRESHOLDS.EFFICIENCY_RATIO, 8);
@@ -148,21 +148,21 @@ const calculateWeights = (
148148
let wBaseline = 0;
149149

150150
// 3. Logic Distribution
151-
151+
152152
// Low Dim Regime
153153
if (isLowDim > 0.01) {
154-
wOversample += isLowDim * isTinyMin;
155-
const safeScore = isLowDim * (1 - isTinyMin);
156-
wUndersample += safeScore * efficiencyPressure;
157-
wBaseline += safeScore * (1 - efficiencyPressure);
154+
wOversample += isLowDim * isTinyMin;
155+
const safeScore = isLowDim * (1 - isTinyMin);
156+
wUndersample += safeScore * efficiencyPressure;
157+
wBaseline += safeScore * (1 - efficiencyPressure);
158158
}
159159

160160
// Med Dim Regime
161161
if (isMedDim > 0.01) {
162162
wHybrid += isMedDim * isTinyMin;
163163
const safeScore = isMedDim * (1 - isTinyMin);
164164
wUndersample += safeScore * efficiencyPressure;
165-
wBaseline += safeScore * (1 - efficiencyPressure);
165+
wBaseline += safeScore * (1 - efficiencyPressure);
166166
}
167167

168168
// High Dim Regime
@@ -178,7 +178,7 @@ const calculateWeights = (
178178
// Determine dominant strategy BEFORE sparsity penalties
179179
let dominantOriginal: StrategyType | null = null;
180180
const maxOrig = Math.max(wOversample, wUndersample, wHybrid, wBaseline);
181-
181+
182182
if (maxOrig === wOversample) dominantOriginal = StrategyType.OVERSAMPLE;
183183
else if (maxOrig === wHybrid) dominantOriginal = StrategyType.HYBRID;
184184
else if (maxOrig === wUndersample) dominantOriginal = StrategyType.UNDERSAMPLE;
@@ -190,7 +190,7 @@ const calculateWeights = (
190190

191191
if (penalty > 0) {
192192
wOversample -= wOversample * penalty;
193-
wBaseline += wOversample * penalty;
193+
wBaseline += wOversample * penalty;
194194

195195
wHybrid -= wHybrid * penalty;
196196
wBaseline += wHybrid * penalty;
@@ -202,8 +202,8 @@ const calculateWeights = (
202202
// --- Visual Generators ---
203203

204204
export const getSmoothStrategyRGB = (
205-
p: number,
206-
nMin: number,
205+
p: number,
206+
nMin: number,
207207
nTotal: number,
208208
sparsity: number,
209209
homogeneity: number
@@ -212,14 +212,14 @@ export const getSmoothStrategyRGB = (
212212
const totalWeight = wOversample + wUndersample + wHybrid + wBaseline || 1;
213213

214214
let r = 0, g = 0, b = 0;
215-
215+
216216
// Helper to accumulate weighted colors
217217
const accumulate = (weight: number, type: StrategyType) => {
218218
if (weight > 0) {
219219
const normW = weight / totalWeight;
220220
const c = STRATEGY_RGB[type];
221-
r += c.r * normW;
222-
g += c.g * normW;
221+
r += c.r * normW;
222+
g += c.g * normW;
223223
b += c.b * normW;
224224
}
225225
};
@@ -234,33 +234,33 @@ export const getSmoothStrategyRGB = (
234234

235235
export const getFoldStabilityRGB = (minority: number, folds: number): RGB => {
236236
const minPerFold = minority / Math.max(1, folds);
237-
237+
238238
if (minPerFold < 1) return { r: 20, g: 10, b: 10 }; // Void (Invalid)
239239

240240
let r, g, b;
241241
if (minPerFold < 5) {
242-
// Deep Red to Bright Red
243-
r = 220; g = 40; b = 40;
242+
// Deep Red to Bright Red
243+
r = 220; g = 40; b = 40;
244244
} else if (minPerFold < 30) {
245-
// Interpolate Red -> Yellow -> Green
246-
const t = (minPerFold - 5) / 25;
247-
r = 239 + (16 - 239) * t;
248-
g = 68 + (185 - 68) * t;
249-
b = 68 + (129 - 68) * t;
245+
// Interpolate Red -> Yellow -> Green
246+
const t = (minPerFold - 5) / 25;
247+
r = 239 + (16 - 239) * t;
248+
g = 68 + (185 - 68) * t;
249+
b = 68 + (129 - 68) * t;
250250
} else {
251-
// Stable Green
252-
r = 16; g = 185; b = 129;
251+
// Stable Green
252+
r = 16; g = 185; b = 129;
253253
}
254-
254+
255255
return { r, g, b };
256256
};
257257

258258
const analyzeFolds = (minority: number, folds: number, total: number): FoldAnalysis => {
259259
const effectiveFolds = Math.min(folds, total);
260260
const minPerFold = minority / effectiveFolds;
261261
const minInTraining = Math.floor(minority * ((effectiveFolds - 1) / effectiveFolds));
262-
263-
const viabilityScore = Math.min(100, (minPerFold / 30) * 100);
262+
263+
const viabilityScore = Math.min(100, (minPerFold / 30) * 100);
264264
const isLOOCV = effectiveFolds >= total || effectiveFolds >= 5000;
265265
const isStratificationImpossible = effectiveFolds > minority;
266266

@@ -271,15 +271,15 @@ const analyzeFolds = (minority: number, folds: number, total: number): FoldAnaly
271271
if (isLOOCV) {
272272
statusKey = 'LOO';
273273
validationRisk = "Validation reduces to binary (0/1) loss. Probability calibration is impossible.";
274-
trainingImpact = `Maximizes training data (N=${total-1}), but computationally expensive.`;
274+
trainingImpact = `Maximizes training data (N=${total - 1}), but computationally expensive.`;
275275
} else if (isStratificationImpossible) {
276276
statusKey = 'IMPOSSIBLE';
277277
validationRisk = `Folds (k=${effectiveFolds}) > Minority Samples (${minority}). Stratification is impossible.`;
278278
trainingImpact = "N/A";
279279
} else if (minPerFold < 1.5) {
280280
statusKey = 'LOPO';
281281
validationRisk = "Single positive sample per fold prevents variance estimation.";
282-
trainingImpact = `Maximized Training (~${minority-1} positives per round).`;
282+
trainingImpact = `Maximized Training (~${minority - 1} positives per round).`;
283283
} else if (minPerFold < 15) {
284284
statusKey = 'VARIANCE';
285285
validationRisk = `Low density (${minPerFold.toFixed(1)} samples/fold) results in noisy performance metrics.`;
@@ -289,27 +289,27 @@ const analyzeFolds = (minority: number, folds: number, total: number): FoldAnaly
289289
validationRisk = "Sufficient density for stable metric estimation (e.g., AUC-ROC, F1).";
290290
trainingImpact = `Standard Stratified K-Fold Split.`;
291291
}
292-
292+
293293
const config = FOLD_STATUS_CONFIG[statusKey];
294294
const displayMinPerFold = minPerFold < 1 ? minPerFold.toFixed(2) : Math.floor(minPerFold);
295295

296-
return {
297-
minPerFold: Number(displayMinPerFold),
298-
minInTraining,
299-
label: config.label,
300-
statusColor: config.color,
301-
statusBg: config.bg,
302-
viabilityScore: (isStratificationImpossible && !isLOOCV) ? 0 : viabilityScore,
303-
validationRisk,
304-
trainingImpact
296+
return {
297+
minPerFold: Number(displayMinPerFold),
298+
minInTraining,
299+
label: config.label,
300+
statusColor: config.color,
301+
statusBg: config.bg,
302+
viabilityScore: (isStratificationImpossible && !isLOOCV) ? 0 : viabilityScore,
303+
validationRisk,
304+
trainingImpact
305305
};
306306
};
307307

308308
// --- Main Recommendation Export ---
309309

310310
export const analyzeDataset = (params: DatasetParams): Recommendation => {
311311
const { features, minority, total, folds, sparsity, sparsityHomogeneity, requiresCalibratedProbabilities } = params;
312-
312+
313313
const weights = calculateWeights(features, minority, total, sparsity, sparsityHomogeneity);
314314
const { wOversample, wUndersample, wHybrid, wBaseline, stability, dominantOriginal } = weights;
315315

@@ -331,7 +331,7 @@ export const analyzeDataset = (params: DatasetParams): Recommendation => {
331331
// SMOTE fails in tiny minority regime - recommend hybrid/ensemble instead
332332
strategy = StrategyType.HYBRID;
333333
}
334-
334+
335335
if (inLargeScaleRegime && !inTinyMinorityRegime) {
336336
// Large scale: prefer undersampling + threshold tuning
337337
strategy = StrategyType.UNDERSAMPLE;
@@ -347,7 +347,7 @@ export const analyzeDataset = (params: DatasetParams): Recommendation => {
347347
const foldAnalysis = analyzeFolds(minority, folds, total);
348348
const threshold = getMinorityThreshold(features);
349349
const ratio = total / Math.max(1, minority);
350-
350+
351351
// Build regime warnings
352352
const regimeWarnings: RegimeWarning[] = [];
353353
const citations: string[] = [];
@@ -357,7 +357,7 @@ export const analyzeDataset = (params: DatasetParams): Recommendation => {
357357
regimeWarnings.push({
358358
type: 'tiny-minority',
359359
title: 'Tiny Minority Regime (EISM)',
360-
message: minority < THRESHOLDS.TINY_MINORITY
360+
message: minority < THRESHOLDS.TINY_MINORITY
361361
? `Minority class (N=${minority}) < 50 samples. Standard SMOTE interpolates between sparse noise rather than true structure. Recommend: Hybrid Ensembles (e.g., HUSDOS-Boost) or Stratified Bagging.`
362362
: `Events Per Variable (EPV=${epv.toFixed(1)}) < 10. Insufficient signal density for reliable synthetic generation.`,
363363
citationIds: ['zhao2025', 'chawla2002']
@@ -393,17 +393,17 @@ export const analyzeDataset = (params: DatasetParams): Recommendation => {
393393
type: 'calibration',
394394
title: 'Calibration Impact',
395395
message: 'All resampling methods distort probability estimates. A predicted 80% risk may actually represent 10% true probability. If calibrated probabilities are required, use Class Weights + Isotonic Calibration instead.',
396-
citationIds: ['elhassan2016']
396+
citationIds: ['dalpozzolo2015']
397397
});
398-
citations.push('elhassan2016');
398+
citations.push('dalpozzolo2015');
399399
}
400400

401401
// Fold Integrity Warning (always show as best practice)
402402
regimeWarnings.push({
403403
type: 'fold-integrity',
404404
title: 'Cross-Validation Best Practice',
405405
message: 'Apply Feature Selection and Resampling INSIDE the cross-validation loop, not before. Performing these steps on the full dataset causes data leakage and inflated performance estimates.',
406-
citationIds: ['blagus2013', 'elhassan2016']
406+
citationIds: ['blagus2013', 'he2009']
407407
});
408408

409409
// Generate Text Content
@@ -420,8 +420,8 @@ export const analyzeDataset = (params: DatasetParams): Recommendation => {
420420
regimeWarnings
421421
};
422422

423-
const transitionNote = (minority > threshold * 0.8 && minority < threshold * 1.2)
424-
? " You are in a complex transition zone. "
423+
const transitionNote = (minority > threshold * 0.8 && minority < threshold * 1.2)
424+
? " You are in a complex transition zone. "
425425
: "";
426426

427427
const wasForcedToBaseline = strategy === StrategyType.BASELINE && (dominantOriginal === StrategyType.OVERSAMPLE || dominantOriginal === StrategyType.HYBRID);
@@ -433,23 +433,23 @@ export const analyzeDataset = (params: DatasetParams): Recommendation => {
433433
}
434434

435435
if (wasForcedToBaseline) {
436-
if (requiresCalibratedProbabilities) {
437-
result.title = "Class Weights + Calibration";
438-
result.description = "Probability Calibration Required.";
439-
result.rationale = `Resampling would distort probability estimates. Class Weights maintain calibration while addressing imbalance. Apply Isotonic Calibration post-training for optimal Brier Score.`;
440-
} else if (sparsity > 0.5 && sparsityHomogeneity < 0.3) {
441-
result.title = `Specialized ${dominantOriginal}`;
442-
result.description = "Structured Sparsity Detected.";
443-
result.rationale = `High sparsity (${Math.round(sparsity*100)}%) but concentrated. Standard interpolation (SMOTE) is unreliable. Strategy: Process features separately—interpolate dense columns, impute sparse ones.`;
444-
result.sparsityWarning = "Recommendation: Utilize SMOTE-NC or feature-specific handling.";
445-
result.color = COLORS[dominantOriginal || StrategyType.HYBRID];
446-
} else {
447-
result.title = "No Resampling / Class Weights";
448-
result.description = "Uniform Sparsity Risks Interpolation.";
449-
result.rationale = `Normally ${dominantOriginal} is best, but ${Math.round(sparsity*100)}% uniform sparsity (Stability: ${stability.toFixed(2)}) renders synthetic neighborhood generation unreliable. Use Class Weights.`;
450-
result.sparsityWarning = "Critical Sparsity: Euclidean distance metrics are unstable.";
451-
}
452-
}
436+
if (requiresCalibratedProbabilities) {
437+
result.title = "Class Weights + Calibration";
438+
result.description = "Probability Calibration Required.";
439+
result.rationale = `Resampling would distort probability estimates. Class Weights maintain calibration while addressing imbalance. Apply Isotonic Calibration post-training for optimal Brier Score.`;
440+
} else if (sparsity > 0.5 && sparsityHomogeneity < 0.3) {
441+
result.title = `Specialized ${dominantOriginal}`;
442+
result.description = "Structured Sparsity Detected.";
443+
result.rationale = `High sparsity (${Math.round(sparsity * 100)}%) but concentrated. Standard interpolation (SMOTE) is unreliable. Strategy: Process features separately—interpolate dense columns, impute sparse ones.`;
444+
result.sparsityWarning = "Recommendation: Utilize SMOTE-NC or feature-specific handling.";
445+
result.color = COLORS[dominantOriginal || StrategyType.HYBRID];
446+
} else {
447+
result.title = "No Resampling / Class Weights";
448+
result.description = "Uniform Sparsity Risks Interpolation.";
449+
result.rationale = `Normally ${dominantOriginal} is best, but ${Math.round(sparsity * 100)}% uniform sparsity (Stability: ${stability.toFixed(2)}) renders synthetic neighborhood generation unreliable. Use Class Weights.`;
450+
result.sparsityWarning = "Critical Sparsity: Euclidean distance metrics are unstable.";
451+
}
452+
}
453453
else if (inTinyMinorityRegime && strategy === StrategyType.HYBRID) {
454454
result.title = "Hybrid Ensemble / Stratified Bagging";
455455
result.description = "Tiny Minority Regime Detected.";
@@ -496,16 +496,16 @@ export const analyzeDataset = (params: DatasetParams): Recommendation => {
496496
result.rationale = `Undersampling reduces computational load and noise without disrupting the manifold structure in high dimensions. Consider feature selection to improve distance metric reliability.`;
497497
}
498498
}
499-
499+
500500
if (!result.sparsityWarning && sparsity > THRESHOLDS.SPARSITY_CRITICAL) {
501-
result.sparsityWarning = sparsityHomogeneity < 0.4
502-
? "Structured Sparsity: Use Nominal/Continuous variants (e.g., SMOTE-NC)."
503-
: "Caution: High sparsity reduces distance metric reliability.";
501+
result.sparsityWarning = sparsityHomogeneity < 0.4
502+
? "Structured Sparsity: Use Nominal/Continuous variants (e.g., SMOTE-NC)."
503+
: "Caution: High sparsity reduces distance metric reliability.";
504504
}
505505

506506
if (foldAnalysis.label.includes("Leave-One-Out")) {
507-
result.foldTarget = "Leave-One-Out (LOOCV)";
508-
result.samplingMix = "N/A (All data utilized).";
507+
result.foldTarget = "Leave-One-Out (LOOCV)";
508+
result.samplingMix = "N/A (All data utilized).";
509509
} else if (folds > minority) {
510510
result.foldTarget = "INVALID CONFIGURATION";
511511
result.samplingMix = `Cannot stratify ${folds} folds with ${minority} samples.`;

0 commit comments

Comments
 (0)