| name | time-series-feature-engineering |
| description | Ingeniería de características para series temporales: rolling windows, lag/lead, rolling stats, Fourier features, holiday encoding, seasonal decomposition. Basado en patrones de scikit-learn TS, statsmodels y featuretools. |
| version | 1.0.0 |
| author | Mastermind |
| license | MIT |
| metadata | {"hermes":{"tags":["time-series","feature-engineering","rolling","lag","fourier","seasonal","decomposition"],"related_skills":["forecast-montecarlo-escenarios","conversion-unidades-api-externa","sistemaelectricofuturo"]}} |
Feature Engineering para Series Temporales
Patrones para extraer características significativas de datos temporales: rolling windows, lag features, Fourier features, encoding estacional y descomposición.
¿Qué es y por qué importa?
Las series temporales (precio eléctrico, demanda, generación renovable) tienen patrones que no son evidentes en los datos crudos:
- Tendencia: ¿la serie sube o baja a largo plazo?
- Estacionalidad: ¿hay patrones diarios/semanales/anuales?
- Ciclos: ¿hay oscilaciones de largo plazo?
- Ruido: variaciones aleatorias
El feature engineering transforma datos crudos en características que los modelos pueden usar:
Precio crudo (€/MWh) → Features:
├── lag_1h, lag_24h, lag_168h (precios pasados)
├── rolling_mean_24h, rolling_std_24h (tendencia local)
├── hour_of_day, day_of_week (ciclos)
├── fourier_sin_24h, fourier_cos_24h (estacionalidad suave)
├── is_weekend, is_holiday (eventos)
└── diff_1h, pct_change_1h (derivadas)
Datos reales: En competition Kaggle de forecasting, las features de series temporales suelen explicar el 60-80% del rendimiento del modelo. Las rolling windows y lags son las features más predictivas para precios energéticos.
Features básicas
1. Lag Features (rezagos)
function createLagFeatures(data, lags = [1, 2, 3, 6, 12, 24, 168]) {
const result = data.map((row, index) => {
const features = { ...row };
for (const lag of lags) {
if (index >= lag) {
features[`lag_${lag}`] = data[index - lag].value;
} else {
features[`lag_${lag}`] = null;
}
}
return features;
});
return result;
}
const hourlyLags = createLagFeatures(data, [1, 2, 3, 6, 12, 24, 168]);
2. Rolling Windows (ventanas móviles)
function createRollingFeatures(data, windowSize = 24, stats = ['mean', 'std', 'min', 'max']) {
const result = data.map((row, index) => {
const features = { ...row };
const start = Math.max(0, index - windowSize + 1);
const window = data.slice(start, index + 1).map(d => d.value);
const valid = window.filter(v => v !== null && !isNaN(v));
if (valid.length === 0) return features;
for (const stat of stats) {
switch (stat) {
case 'mean':
features[`rolling_mean_${windowSize}`] = valid.reduce((a, b) => a + b, 0) / valid.length;
break;
case 'std': {
const mean = features[`rolling_mean_${windowSize}`];
const variance = valid.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / valid.length;
features[`rolling_std_${windowSize}`] = Math.sqrt(variance);
break;
}
case 'min':
features[`rolling_min_${windowSize}`] = Math.min(...valid);
break;
case 'max':
features[`rolling_max_${windowSize}`] = Math.max(...valid);
break;
case 'median':
features[`rolling_median_${windowSize}`] = median(valid);
break;
case 'skew': {
const mean = features[`rolling_mean_${windowSize}`];
const std = features[`rolling_std_${windowSize}`];
if (std > 0) {
const n = valid.length;
const skewSum = valid.reduce((s, v) => s + Math.pow((v - mean) / std, 3), 0);
features[`rolling_skew_${windowSize}`] = (n / ((n - 1) * (n - 2))) * skewSum;
}
break;
}
}
}
return features;
});
return result;
}
function median(arr) {
const sorted = [...arr].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}
const withRolling = createRollingFeatures(data, 24, ['mean', 'std', 'min', 'max', 'skew']);
3. Derivadas y Cambios Porcentuales
function createDifferencingFeatures(data, lags = [1, 24, 168]) {
return data.map((row, index) => {
const features = { ...row };
for (const lag of lags) {
if (index >= lag && data[index - lag].value != null && row.value != null) {
const prev = data[index - lag].value;
const current = row.value;
features[`diff_${lag}`] = current - prev;
if (Math.abs(prev) > 0.001) {
features[`pct_change_${lag}`] = (current - prev) / Math.abs(prev);
}
if (current > 0 && prev > 0) {
features[`log_diff_${lag}`] = Math.log(current) - Math.log(prev);
}
}
}
return features;
});
}
Features avanzadas
4. Fourier Features (estacionalidad suave)
function createFourierFeatures(data, period = 24, K = 3) {
return data.map((row, index) => {
const features = { ...row };
for (let k = 1; k <= K; k++) {
features[`fourier_sin_${k}_${period}`] = Math.sin((2 * Math.PI * k * index) / period);
features[`fourier_cos_${k}_${period}`] = Math.cos((2 * Math.PI * k * index) / period);
}
return features;
});
}
5. Encoding Temporal (hora, día, mes)
function createTemporalFeatures(data) {
return data.map((row) => {
const date = new Date(row.timestamp);
const features = { ...row };
features.hour = date.getHours();
features.dayOfWeek = date.getDay();
features.dayOfMonth = date.getDate();
features.month = date.getMonth();
features.quarter = Math.floor(date.getMonth() / 3) + 1;
features.weekOfYear = getWeekNumber(date);
features.hour_sin = Math.sin((2 * Math.PI * features.hour) / 24);
features.hour_cos = Math.cos((2 * Math.PI * features.hour) / 24);
features.dow_sin = Math.sin((2 * Math.PI * features.dayOfWeek) / 7);
features.dow_cos = Math.cos((2 * Math.PI * features.dayOfWeek) / 7);
features.month_sin = Math.sin((2 * Math.PI * features.month) / 12);
features.month_cos = Math.cos((2 * Math.PI * features.month) / 12);
features.is_peak = isPeakHour(features.hour);
features.is_valley = isValleyHour(features.hour);
features.band = getHourBand(features.hour);
features.is_weekend = features.dayOfWeek === 0 || features.dayOfWeek === 6;
features.is_monday = features.dayOfWeek === 1;
features.is_friday = features.dayOfWeek === 5;
return features;
});
}
function isPeakHour(hour) {
return hour >= 18 && hour <= 22;
}
function isValleyHour(hour) {
return hour >= 2 && hour <= 6;
}
function getHourBand(hour) {
if (hour >= 6 && hour < 10) return 'morning';
if (hour >= 10 && hour < 14) return 'midday';
if (hour >= 14 && hour < 18) return 'afternoon';
if (hour >= 18 && hour < 22) return 'evening';
return 'night';
}
function getWeekNumber(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}
6. Holiday y Event Encoding
const NATIONAL_HOLIDAYS = {
'2024': ['2024-01-01', '2024-01-06', '2024-03-29', '2024-05-01', '2024-08-15',
'2024-10-12', '2024-11-06', '2024-11-07', '2024-12-06', '2024-12-08', '2024-12-25'],
'2025': ['2025-01-01', '2025-01-06', '2025-04-18', '2025-05-01', '2025-05-02',
'2025-08-15', '2025-10-13', '2025-11-05', '2025-11-06', '2025-12-08', '2025-12-12', '2025-12-25'],
};
function createHolidayFeatures(data, region = 'national') {
return data.map((row) => {
const date = new Date(row.timestamp);
const dateStr = date.toISOString().split('T')[0];
const year = date.getFullYear().toString();
const features = { ...row };
const holidays = NATIONAL_HOLIDAYS[year] || [];
features.is_national_holiday = holidays.includes(dateStr);
const madridHolidays = getMadridHolidays(year);
features.is_madrid_holiday = madridHolidays.includes(dateStr);
const prevDay = new Date(date);
prevDay.setDate(prevDay.getDate() - 1);