/* global Office, Excel, console */ // ==================================================================== // CHRONOS2 FORECASTING ADD-IN // Office.js Task Pane Implementation // ==================================================================== // URL del API en HuggingFace Spaces const API_BASE_URL = 'https://ttzzs-chronos2-excel-forecasting-api.hf.space'; // Para desarrollo local, descomenta la siguiente línea: // const API_BASE_URL = 'https://localhost:8000'; // Inicializar cuando Office esté listo Office.onReady((info) => { if (info.host === Office.HostType.Excel) { console.log('Chronos2 Add-in loaded successfully'); checkServerStatus(); // Auto-check cada 30 segundos setInterval(checkServerStatus, 30000); } }); // ==================================================================== // UTILIDADES // ==================================================================== function log(message, type = 'info') { const resultsDiv = document.getElementById('results'); const timestamp = new Date().toLocaleTimeString(); const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'; const entry = document.createElement('div'); entry.className = `log-entry log-${type}`; entry.innerHTML = `${timestamp} ${icon} ${message}`; resultsDiv.insertBefore(entry, resultsDiv.firstChild); // Limitar a 20 entries while (resultsDiv.children.length > 20) { resultsDiv.removeChild(resultsDiv.lastChild); } } async function checkServerStatus() { try { const response = await fetch(`${API_BASE_URL}/health`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (response.ok) { updateServerStatus(true, `Connected - ${data.model_id}`); } else { updateServerStatus(false, 'Server error'); } } catch (error) { updateServerStatus(false, 'Server offline'); } } function updateServerStatus(isOnline, message) { const statusEl = document.getElementById('serverStatus'); const textEl = document.getElementById('statusText'); statusEl.className = `status-indicator ${isOnline ? 'online' : 'offline'}`; textEl.textContent = message; } function showTab(tabName) { // Ocultar todos los tabs const tabs = document.querySelectorAll('.tab-content'); tabs.forEach(tab => tab.classList.remove('active')); const buttons = document.querySelectorAll('.tab'); buttons.forEach(btn => btn.classList.remove('active')); // Mostrar el tab seleccionado document.getElementById(`tab-${tabName}`).classList.add('active'); event.target.classList.add('active'); } // ==================================================================== // FUNCIONES DE EXCEL (Office.js) // ==================================================================== async function getSelectedRange() { return Excel.run(async (context) => { const range = context.workbook.getSelectedRange(); range.load('values, address'); await context.sync(); return { values: range.values, address: range.address }; }); } async function writeToRange(data, startCell) { return Excel.run(async (context) => { try { console.log(`[writeToRange] Writing ${data?.length || 0} rows to ${startCell}`); console.log('[writeToRange] Data:', JSON.stringify(data).substring(0, 200)); if (!data || data.length === 0) { throw new Error('No data to write'); } if (!data[0] || data[0].length === 0) { throw new Error('Invalid data structure: empty first row'); } const sheet = context.workbook.worksheets.getActiveWorksheet(); const numRows = data.length; const numCols = data[0].length; console.log(`[writeToRange] Creating range: ${numRows} rows x ${numCols} cols from ${startCell}`); const range = sheet.getRange(startCell).getResizedRange(numRows - 1, numCols - 1); range.values = data; range.format.autofitColumns(); await context.sync(); console.log('[writeToRange] ✅ Data written successfully'); } catch (error) { console.error('[writeToRange] ❌ Error:', error); console.error('[writeToRange] Stack:', error.stack); throw error; } }); } async function writeForecastResults(timestamps, median, q10, q90, startRow) { return Excel.run(async (context) => { try { console.log('[writeForecastResults] Starting...'); console.log(`[writeForecastResults] timestamps: ${timestamps?.length || 0} items`); console.log(`[writeForecastResults] median: ${median?.length || 0} items`); console.log(`[writeForecastResults] q10: ${q10?.length || 0} items`); console.log(`[writeForecastResults] q90: ${q90?.length || 0} items`); console.log(`[writeForecastResults] startRow: ${startRow}`); // VALIDACIÓN if (!timestamps || !median) { throw new Error('Invalid data: timestamps or median is undefined'); } if (timestamps.length === 0) { throw new Error('No forecast data received (empty timestamps)'); } if (timestamps.length !== median.length) { throw new Error(`Data mismatch: ${timestamps.length} timestamps vs ${median.length} median values`); } const sheet = context.workbook.worksheets.getActiveWorksheet(); // Preparar datos const data = []; data.push(['Timestamp', 'Median', 'Q10', 'Q90']); // Headers for (let i = 0; i < timestamps.length; i++) { data.push([ timestamps[i], median[i], q10 ? q10[i] : '', q90 ? q90[i] : '' ]); } console.log(`[writeForecastResults] Prepared ${data.length} rows (including header)`); // Escribir en columnas D-G a partir de la fila especificada const startCell = `D${startRow}`; console.log(`[writeForecastResults] Writing to ${startCell}`); await writeToRange(data, startCell); // Aplicar formato const headerRange = sheet.getRange(`D${startRow}:G${startRow}`); headerRange.format.font.bold = true; headerRange.format.fill.color = '#4472C4'; headerRange.format.font.color = 'white'; await context.sync(); console.log('[writeForecastResults] ✅ Forecast results written successfully'); } catch (error) { console.error('[writeForecastResults] ❌ Error:', error); console.error('[writeForecastResults] Stack:', error.stack); throw error; } }); } // ==================================================================== // FUNCIÓN 1: PRONÓSTICO UNIVARIANTE // ==================================================================== async function forecastUnivariate() { log('Starting univariate forecast...'); try { // Leer rango seleccionado const selection = await getSelectedRange(); const values = selection.values.flat().filter(v => v !== '' && !isNaN(v)); if (values.length < 3) { log('Error: Select at least 3 data points', 'error'); return; } log(`Selected ${values.length} data points from ${selection.address}`); // Obtener parámetros const predictionLength = parseInt(document.getElementById('predictionLength').value); const frequency = document.getElementById('frequency').value; // Construir request const requestBody = { prediction_length: predictionLength, series: { values: values }, start_timestamp: new Date().toISOString().split('T')[0], freq: frequency, quantile_levels: [0.1, 0.5, 0.9] }; log('Sending request to API...'); // Llamar a la API const response = await fetch(`${API_BASE_URL}/forecast_univariate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API error: ${response.statusText}`); } const data = await response.json(); log(`Received forecast for ${data.timestamps.length} periods`, 'success'); // Escribir resultados await Excel.run(async (context) => { const selection = context.workbook.getSelectedRange(); selection.load('rowIndex, rowCount'); await context.sync(); const startRow = selection.rowIndex + selection.rowCount + 2; await writeForecastResults( data.timestamps, data.median, data.quantiles['0.1'], data.quantiles['0.9'], startRow ); }); log('✨ Forecast written to spreadsheet', 'success'); } catch (error) { log(`Error: ${error.message}`, 'error'); console.error(error); } } // ==================================================================== // FUNCIÓN 2: DETECCIÓN DE ANOMALÍAS // ==================================================================== async function detectAnomalies() { log('Starting anomaly detection...'); try { const selection = await getSelectedRange(); const values = selection.values.flat().filter(v => v !== '' && !isNaN(v)); const contextLength = parseInt(document.getElementById('contextLength').value); const recentPoints = parseInt(document.getElementById('recentPoints').value); if (values.length < contextLength + recentPoints) { log(`Error: Need at least ${contextLength + recentPoints} points`, 'error'); return; } const context = values.slice(0, contextLength); const recent = values.slice(contextLength, contextLength + recentPoints); const requestBody = { context: { values: context }, recent_observed: recent, prediction_length: recentPoints, quantile_low: 0.05, quantile_high: 0.95 }; log('Analyzing data...'); const response = await fetch(`${API_BASE_URL}/detect_anomalies`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API error: ${response.statusText}`); } const data = await response.json(); const anomalyCount = data.anomalies.filter(a => a.is_anomaly).length; if (anomalyCount > 0) { log(`⚠️ Found ${anomalyCount} anomalies!`, 'error'); } else { log('No anomalies detected ✓', 'success'); } // Escribir resultados en Excel await Excel.run(async (context) => { const selection = context.workbook.getSelectedRange(); selection.load('rowIndex, rowCount'); await context.sync(); const startRow = selection.rowIndex + selection.rowCount + 2; const sheet = context.workbook.worksheets.getActiveWorksheet(); // Preparar datos const tableData = [['Index', 'Value', 'Expected', 'Lower', 'Upper', 'Is Anomaly']]; data.anomalies.forEach(a => { tableData.push([ a.index, parseFloat(a.value.toFixed(2)), parseFloat(a.predicted_median.toFixed(2)), parseFloat(a.lower.toFixed(2)), parseFloat(a.upper.toFixed(2)), a.is_anomaly ? 'YES' : 'No' ]); }); const range = sheet.getRangeByIndexes(startRow, 0, tableData.length, 6); range.values = tableData; range.format.autofitColumns(); // Format header const headerRange = sheet.getRangeByIndexes(startRow, 0, 1, 6); headerRange.format.font.bold = true; headerRange.format.fill.color = '#4472C4'; headerRange.format.font.color = 'white'; // Highlight anomalies for (let i = 0; i < data.anomalies.length; i++) { if (data.anomalies[i].is_anomaly) { const anomalyRange = sheet.getRangeByIndexes(startRow + i + 1, 0, 1, 6); anomalyRange.format.fill.color = '#FFC7CE'; } } await context.sync(); }); log('✨ Anomaly results written to spreadsheet', 'success'); } catch (error) { log(`Error: ${error.message}`, 'error'); console.error(error); } } // ==================================================================== // FUNCIÓN 3: BACKTEST // ==================================================================== async function runBacktest() { log('Running backtest...'); try { const selection = await getSelectedRange(); const values = selection.values.flat().filter(v => v !== '' && !isNaN(v)); const testLength = parseInt(document.getElementById('testLength').value); if (values.length <= testLength) { log('Error: Series must be longer than test length', 'error'); return; } const requestBody = { series: { values: values }, prediction_length: testLength, test_length: testLength }; log('Evaluating model...'); const response = await fetch(`${API_BASE_URL}/backtest_simple`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API error: ${response.statusText}`); } const data = await response.json(); const metrics = data.metrics; log(`📊 Backtest Results: MAE=${metrics.mae.toFixed(2)}, MAPE=${metrics.mape.toFixed(2)}%`, 'success'); // Escribir resultados en Excel await Excel.run(async (context) => { const selection = context.workbook.getSelectedRange(); selection.load('rowIndex, rowCount'); await context.sync(); const startRow = selection.rowIndex + selection.rowCount + 2; const sheet = context.workbook.worksheets.getActiveWorksheet(); // Tabla de métricas const metricsData = [ ['Metric', 'Value'], ['MAE', parseFloat(metrics.mae.toFixed(2))], ['MAPE', metrics.mape.toFixed(2) + '%'], ['RMSE', parseFloat(metrics.rmse.toFixed(2))], ['WQL', parseFloat(metrics.wql.toFixed(3))] ]; const metricsRange = sheet.getRangeByIndexes(startRow, 0, metricsData.length, 2); metricsRange.values = metricsData; metricsRange.format.autofitColumns(); // Format header const headerRange = sheet.getRangeByIndexes(startRow, 0, 1, 2); headerRange.format.font.bold = true; headerRange.format.fill.color = '#70AD47'; headerRange.format.font.color = 'white'; // Forecast vs Actuals si están disponibles if (data.forecast_median && data.actuals) { const forecastData = [['Timestamp', 'Forecast', 'Actual', 'Error']]; for (let i = 0; i < data.forecast_median.length; i++) { const error = Math.abs(data.forecast_median[i] - data.actuals[i]); forecastData.push([ data.forecast_timestamps[i] || `t+${i+1}`, parseFloat(data.forecast_median[i].toFixed(2)), parseFloat(data.actuals[i].toFixed(2)), parseFloat(error.toFixed(2)) ]); } const forecastRange = sheet.getRangeByIndexes( startRow + metricsData.length + 2, 0, forecastData.length, 4 ); forecastRange.values = forecastData; forecastRange.format.autofitColumns(); const forecastHeaderRange = sheet.getRangeByIndexes( startRow + metricsData.length + 2, 0, 1, 4 ); forecastHeaderRange.format.font.bold = true; forecastHeaderRange.format.fill.color = '#4472C4'; forecastHeaderRange.format.font.color = 'white'; } await context.sync(); }); log('✨ Backtest results written to spreadsheet', 'success'); } catch (error) { log(`Error: ${error.message}`, 'error'); console.error(error); } } // ==================================================================== // FUNCIÓN 4: MULTI-SERIES // ==================================================================== async function forecastMultiSeries() { log('Starting multi-series forecast...'); try { const selection = await getSelectedRange(); const data = selection.values; // Agrupar por series_id (columna A) const seriesMap = {}; for (let i = 1; i < data.length; i++) { // Skip header const seriesId = data[i][0]; const value = data[i][2]; // Columna C if (seriesId && value !== '' && !isNaN(value)) { if (!seriesMap[seriesId]) { seriesMap[seriesId] = []; } seriesMap[seriesId].push(parseFloat(value)); } } const seriesList = Object.entries(seriesMap).map(([id, values]) => ({ series_id: id, values: values })); if (seriesList.length === 0) { log('Error: No valid series found', 'error'); return; } log(`Found ${seriesList.length} series`); const predictionLength = parseInt(document.getElementById('multiPredLength').value); const requestBody = { prediction_length: predictionLength, series_list: seriesList, start_timestamp: new Date().toISOString().split('T')[0], freq: 'D', quantile_levels: [0.1, 0.5, 0.9] }; log('Forecasting all series...'); const response = await fetch(`${API_BASE_URL}/forecast_multi_id`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API error: ${response.statusText}`); } const result = await response.json(); log(`✨ Generated forecasts for ${result.forecasts.length} series`, 'success'); // Escribir resultados en Excel await Excel.run(async (context) => { const selection = context.workbook.getSelectedRange(); selection.load('rowIndex, rowCount'); await context.sync(); const startRow = selection.rowIndex + selection.rowCount + 2; const sheet = context.workbook.worksheets.getActiveWorksheet(); let currentRow = startRow; // Escribir cada serie result.forecasts.forEach(forecast => { // Header de la serie const seriesHeaderRange = sheet.getRangeByIndexes(currentRow, 0, 1, 1); seriesHeaderRange.values = [[`Series: ${forecast.series_id}`]]; seriesHeaderRange.format.font.bold = true; seriesHeaderRange.format.fill.color = '#4472C4'; seriesHeaderRange.format.font.color = 'white'; currentRow++; // Datos de la serie const tableData = [['Timestamp', 'Median', 'Q10', 'Q90']]; for (let i = 0; i < forecast.timestamps.length; i++) { tableData.push([ forecast.timestamps[i], parseFloat(forecast.median[i].toFixed(2)), parseFloat(forecast.quantiles['0.1'][i].toFixed(2)), parseFloat(forecast.quantiles['0.9'][i].toFixed(2)) ]); } const dataRange = sheet.getRangeByIndexes( currentRow, 0, tableData.length, 4 ); dataRange.values = tableData; dataRange.format.autofitColumns(); // Format header const headerRange = sheet.getRangeByIndexes(currentRow, 0, 1, 4); headerRange.format.font.bold = true; headerRange.format.fill.color = '#D9E1F2'; currentRow += tableData.length + 1; // +1 para separación }); await context.sync(); }); log('✨ Multi-series forecasts written to spreadsheet', 'success'); } catch (error) { log(`Error: ${error.message}`, 'error'); console.error(error); } } // ==================================================================== // FUNCIÓN 5: COVARIABLES // ==================================================================== async function forecastWithCovariates() { log('Starting forecast with covariates...'); try { const selection = await getSelectedRange(); const data = selection.values; if (data.length < 3) { log('Error: Need at least 3 rows of data', 'error'); return; } // Obtener parámetros const predictionLength = parseInt(document.getElementById('covPredLength').value); const covariateNamesInput = document.getElementById('covariateNames').value; const covariateNames = covariateNamesInput.split(',').map(s => s.trim()); log(`Reading data with ${covariateNames.length} covariates: ${covariateNames.join(', ')}`); // Estructura esperada: // Col A: Date/Timestamp // Col B: Target value // Col C+: Covariates const context = []; const future = []; for (let i = 1; i < data.length; i++) { // Skip header const timestamp = data[i][0] ? data[i][0].toString() : null; const target = data[i][1]; // Leer covariables const covariates = {}; for (let j = 0; j < covariateNames.length && j < data[i].length - 2; j++) { const covValue = data[i][j + 2]; if (covValue !== '' && !isNaN(covValue)) { covariates[covariateNames[j]] = parseFloat(covValue); } } // Si tiene target, es contexto histórico if (target !== '' && !isNaN(target)) { context.push({ timestamp: timestamp, target: parseFloat(target), covariates: covariates }); } // Si no tiene target pero sí covariables, son valores futuros else if (Object.keys(covariates).length > 0) { future.push({ timestamp: timestamp, covariates: covariates }); } } if (context.length === 0) { log('Error: No historical data found', 'error'); return; } log(`Context: ${context.length} points, Future: ${future.length} points`); const requestBody = { context: context, future: future.length > 0 ? future : null, prediction_length: predictionLength, quantile_levels: [0.1, 0.5, 0.9] }; log('Calling API with covariates...'); const response = await fetch(`${API_BASE_URL}/forecast_with_covariates`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API error: ${response.statusText} - ${errorText}`); } const result = await response.json(); log(`✨ Forecast generated with ${result.pred_df.length} predictions`, 'success'); // Escribir resultados en una nueva ubicación await Excel.run(async (context) => { const selection = context.workbook.getSelectedRange(); selection.load('rowIndex, rowCount, columnCount'); await context.sync(); const startRow = selection.rowIndex + selection.rowCount + 2; const startCol = 0; // Crear tabla con los resultados const sheet = context.workbook.worksheets.getActiveWorksheet(); // Headers const headers = Object.keys(result.pred_df[0]); const tableData = [headers]; // Data rows result.pred_df.forEach(row => { const rowData = headers.map(h => row[h]); tableData.push(rowData); }); const outputRange = sheet.getRangeByIndexes( startRow, startCol, tableData.length, headers.length ); outputRange.values = tableData; outputRange.format.autofitColumns(); // Format header const headerRange = sheet.getRangeByIndexes(startRow, startCol, 1, headers.length); headerRange.format.font.bold = true; headerRange.format.fill.color = '#4472C4'; headerRange.format.font.color = 'white'; await context.sync(); }); log('✨ Results written to spreadsheet', 'success'); } catch (error) { log(`Error: ${error.message}`, 'error'); console.error(error); } } // ==================================================================== // FUNCIÓN 6: ESCENARIOS // ==================================================================== async function generateScenarios() { log('Starting scenario generation...'); try { const selection = await getSelectedRange(); const data = selection.values; if (data.length < 3) { log('Error: Need at least 3 rows of data', 'error'); return; } const numScenarios = parseInt(document.getElementById('numScenarios').value); // Estructura esperada similar a covariates: // Col A: Date, Col B: Target, Col C+: Covariates // Para escenarios, generaremos variaciones de las covariables const context = []; const covariateNames = []; // Detectar nombres de covariables del header for (let j = 2; j < data[0].length; j++) { if (data[0][j]) { covariateNames.push(data[0][j].toString()); } } log(`Detected covariates: ${covariateNames.join(', ')}`); // Leer contexto histórico for (let i = 1; i < data.length; i++) { const timestamp = data[i][0] ? data[i][0].toString() : null; const target = data[i][1]; if (target !== '' && !isNaN(target)) { const covariates = {}; for (let j = 0; j < covariateNames.length && j < data[i].length - 2; j++) { const covValue = data[i][j + 2]; if (covValue !== '' && !isNaN(covValue)) { covariates[covariateNames[j]] = parseFloat(covValue); } } context.push({ timestamp: timestamp, target: parseFloat(target), covariates: covariates }); } } if (context.length === 0) { log('Error: No historical data found', 'error'); return; } // Generar escenarios automáticamente const predictionLength = 7; const scenarios = []; // Calcular valores promedio de covariables para generar variaciones const avgCovariates = {}; covariateNames.forEach(name => { const values = context .map(p => p.covariates[name]) .filter(v => v !== undefined); avgCovariates[name] = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; }); // Escenario 1: Baseline (promedios) const baselineScenario = { name: 'Baseline', future_covariates: [] }; for (let i = 0; i < predictionLength; i++) { baselineScenario.future_covariates.push({ timestamp: `future_${i+1}`, covariates: {...avgCovariates} }); } scenarios.push(baselineScenario); // Escenario 2: Optimista (+20%) if (numScenarios >= 2) { const optimisticScenario = { name: 'Optimistic (+20%)', future_covariates: [] }; for (let i = 0; i < predictionLength; i++) { const covs = {}; covariateNames.forEach(name => { covs[name] = avgCovariates[name] * 1.2; }); optimisticScenario.future_covariates.push({ timestamp: `future_${i+1}`, covariates: covs }); } scenarios.push(optimisticScenario); } // Escenario 3: Pesimista (-20%) if (numScenarios >= 3) { const pessimisticScenario = { name: 'Pessimistic (-20%)', future_covariates: [] }; for (let i = 0; i < predictionLength; i++) { const covs = {}; covariateNames.forEach(name => { covs[name] = avgCovariates[name] * 0.8; }); pessimisticScenario.future_covariates.push({ timestamp: `future_${i+1}`, covariates: covs }); } scenarios.push(pessimisticScenario); } log(`Generated ${scenarios.length} scenarios`); const requestBody = { context: context, scenarios: scenarios, prediction_length: predictionLength, quantile_levels: [0.1, 0.5, 0.9] }; log('Calling scenarios API...'); const response = await fetch(`${API_BASE_URL}/forecast_scenarios`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API error: ${response.statusText} - ${errorText}`); } const result = await response.json(); log(`✨ Generated ${result.scenarios.length} scenario forecasts`, 'success'); // Escribir resultados await Excel.run(async (context) => { const selection = context.workbook.getSelectedRange(); selection.load('rowIndex, rowCount'); await context.sync(); const startRow = selection.rowIndex + selection.rowCount + 2; const sheet = context.workbook.worksheets.getActiveWorksheet(); let currentRow = startRow; // Escribir cada escenario result.scenarios.forEach(scenario => { // Header del escenario const scenarioHeaderRange = sheet.getRangeByIndexes(currentRow, 0, 1, 1); scenarioHeaderRange.values = [[`Scenario: ${scenario.name}`]]; scenarioHeaderRange.format.font.bold = true; scenarioHeaderRange.format.fill.color = '#70AD47'; scenarioHeaderRange.format.font.color = 'white'; currentRow++; // Datos del escenario if (scenario.pred_df && scenario.pred_df.length > 0) { const headers = Object.keys(scenario.pred_df[0]); const tableData = [headers]; scenario.pred_df.forEach(row => { tableData.push(headers.map(h => row[h])); }); const dataRange = sheet.getRangeByIndexes( currentRow, 0, tableData.length, headers.length ); dataRange.values = tableData; dataRange.format.autofitColumns(); currentRow += tableData.length + 1; // +1 para separación } }); await context.sync(); }); log('✨ Scenarios written to spreadsheet', 'success'); } catch (error) { log(`Error: ${error.message}`, 'error'); console.error(error); } } // ==================================================================== // FUNCIÓN 7: MULTIVARIANTE // ==================================================================== async function forecastMultivariate() { log('Starting multivariate forecast...'); try { const selection = await getSelectedRange(); const data = selection.values; if (data.length < 3) { log('Error: Need at least 3 rows of data', 'error'); return; } // Obtener parámetros const predictionLength = parseInt(document.getElementById('multivarPredLength').value); const targetColumnsInput = document.getElementById('targetColumns').value; const targetColumns = targetColumnsInput.split(',').map(s => s.trim()); log(`Forecasting ${targetColumns.length} target variables: ${targetColumns.join(', ')}`); // Estructura esperada: // Col A: Date/Timestamp // Col B+: Target variables (múltiples columnas que queremos predecir) const context = []; // Validar que hay suficientes columnas if (data[0].length < targetColumns.length + 1) { log(`Error: Expected ${targetColumns.length + 1} columns but found ${data[0].length}`, 'error'); return; } // Leer datos for (let i = 1; i < data.length; i++) { // Skip header const timestamp = data[i][0] ? data[i][0].toString() : null; // Leer todos los targets const targets = {}; let hasValidData = false; for (let j = 0; j < targetColumns.length && j < data[i].length - 1; j++) { const value = data[i][j + 1]; if (value !== '' && !isNaN(value)) { targets[targetColumns[j]] = parseFloat(value); hasValidData = true; } } if (hasValidData) { context.push({ timestamp: timestamp, targets: targets, covariates: {} // Sin covariables por ahora }); } } if (context.length === 0) { log('Error: No valid data found', 'error'); return; } log(`Read ${context.length} data points`); const requestBody = { context: context, target_columns: targetColumns, prediction_length: predictionLength, quantile_levels: [0.1, 0.5, 0.9] }; log('Calling multivariate forecast API...'); const response = await fetch(`${API_BASE_URL}/forecast_multivariate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API error: ${response.statusText} - ${errorText}`); } const result = await response.json(); log(`✨ Generated multivariate forecast with ${result.pred_df.length} predictions`, 'success'); // Escribir resultados await Excel.run(async (context) => { const selection = context.workbook.getSelectedRange(); selection.load('rowIndex, rowCount'); await context.sync(); const startRow = selection.rowIndex + selection.rowCount + 2; const sheet = context.workbook.worksheets.getActiveWorksheet(); // Crear tabla con resultados if (result.pred_df && result.pred_df.length > 0) { const headers = Object.keys(result.pred_df[0]); const tableData = [headers]; result.pred_df.forEach(row => { tableData.push(headers.map(h => row[h])); }); const outputRange = sheet.getRangeByIndexes( startRow, 0, tableData.length, headers.length ); outputRange.values = tableData; outputRange.format.autofitColumns(); // Format header const headerRange = sheet.getRangeByIndexes(startRow, 0, 1, headers.length); headerRange.format.font.bold = true; headerRange.format.fill.color = '#4472C4'; headerRange.format.font.color = 'white'; await context.sync(); } }); log('✨ Multivariate forecast written to spreadsheet', 'success'); } catch (error) { log(`Error: ${error.message}`, 'error'); console.error(error); } }