Skip to content

Commit 10e92d5

Browse files
Merge branch 'master' of https://github.com/jortilles/EDA
2 parents b52e045 + af9af03 commit 10e92d5

File tree

28 files changed

+1070
-178
lines changed

28 files changed

+1070
-178
lines changed

eda/eda_api/lib/module/dashboard/dashboard.controller.ts

Lines changed: 151 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Group from '../admin/groups/model/group.model'
88
import formatDate from '../../services/date-format/date-format.service'
99
import { CachedQueryService } from '../../services/cache-service/cached-query.service'
1010
import { ArimaService } from '../../services/prediction/arima.service'
11+
import { TensorflowService } from '../../services/prediction/tensorflow.service'
1112
import { TimeFormatService } from '../../services/time-format/time-format.service'
1213
import { QueryOptions } from 'mongoose'
1314
import ServerLogService from '../../services/server-log/server-log.service'
@@ -483,7 +484,6 @@ export class DashboardController {
483484
// Obtener el dashboard
484485
const dashboard = await Dashboard.findById(req.params.id);
485486
if (!dashboard) {
486-
console.log('Dashboard not found with this id:' + req.params.id);
487487
return next(new HttpException(500, 'Dashboard not found with this id'));
488488
}
489489

@@ -499,7 +499,6 @@ export class DashboardController {
499499
dashboard.user.toString() !== user;
500500

501501
if (visibilityCheck && roleCheck) {
502-
console.log("You don't have permission " + user + ' for dashboard ' + req.params.id);
503502
return next(new HttpException(500, "You don't have permission"));
504503
}
505504

@@ -1797,8 +1796,10 @@ export class DashboardController {
17971796
// Suma acumulativa
17981797
DashboardController.cumulativeSum(output, req.body.query);
17991798

1800-
// ARIMA (si aplica)
1801-
DashboardController.applyArimaIfNeeded(output, req.body, myQuery );
1799+
// Revisar si hay predicción y el tipo
1800+
if(req.body.query?.prediction && req.body.query?.prediction !== 'None' && req.body.output?.config?.chartType === 'line'){
1801+
await DashboardController.setupPrediction(output, req.body, myQuery, connection, dataModelObject, req.user)
1802+
}
18021803

18031804
console.log(
18041805
'\x1b[32m%s\x1b[0m',
@@ -1814,55 +1815,180 @@ export class DashboardController {
18141815
}
18151816
}
18161817

1817-
// Formula de arima
1818-
static applyArimaIfNeeded(output: any, body: any, myQuery: any) {
1819-
// Si no tenemos arima o no estamos en linea, salimos
1820-
if (body.query?.prediction !== 'Arima' || body.output.config.chartType !== 'line' ) {
1821-
return;
1822-
}
18231818

1824-
// Número de fechas a predecir
1825-
const steps = 3;
1819+
static async setupPrediction(output: any, body: any, myQuery: any, connection?: any, dataModelObject?: any, user?: any){
1820+
// Leer configuración de predicción del body
1821+
const predictionConfig = body.query?.predictionConfig || {};
1822+
const steps = predictionConfig.steps || 3;
18261823

1827-
// Buscamos campo de la fecha, su formato y su último valor
1824+
// Buscamos campo de la fecha — sin él no podemos generar fechas futuras
18281825
const dateField = myQuery.fields.find(field => field.column_type === 'date');
18291826
if (!dateField){
18301827
return;
1831-
}
1832-
1828+
}
1829+
18331830
const timeFormat = dateField.format;
18341831
const lastDate = output[1][output[1].length - 1][0];
18351832

1836-
// A partir de los datos anteriores generamos las proximas fechas
1833+
// Generamos las próximas fechas en el mismo formato que las existentes (mes, semana, día...)
18371834
const nextLabels = TimeFormatService.nextInSequenceGeneric( timeFormat, lastDate, steps );
18381835

18391836
const rows = output[1];
18401837
const lastIndex = rows.length - 1;
1838+
// Dataset numérico: solo la columna de valores (índice 1), descartando nulos/infinitos
18411839
const originalDataset = rows.map(row => row[1]).filter(val => Number.isFinite(val));
18421840

18431841
let predictions: number[] = [];
1842+
const setup = {steps: steps, rows: rows, lastIndex: lastIndex, nextLabels: nextLabels}
1843+
1844+
switch(body.query?.prediction){
1845+
case 'Arima':
1846+
// arimaParams puede ser undefined (usará configs automáticas) o {p,d,q} manual
1847+
await DashboardController.applyArimaPredicction(
1848+
predictions, originalDataset, setup, predictionConfig.arimaParams);
1849+
break;
1850+
case 'Tensorflow':
1851+
// Construir datasets de referencia para predicción multivariante
1852+
// referenceColumns ahora es [{table_name, column_name, display_name}]
1853+
const referenceColumns: {table_name: string, column_name: string, display_name: string}[] =
1854+
predictionConfig.tensorflowParams?.referenceColumns || [];
1855+
let referenceDatasets: number[][] = [];
1856+
1857+
if (referenceColumns.length > 0 && connection && dataModelObject) {
1858+
try {
1859+
// Para cada columna de referencia lanzamos una query auxiliar
1860+
// alineada con el mismo campo de fecha
1861+
referenceDatasets = await DashboardController.fetchReferenceDatasets(
1862+
referenceColumns, dateField, myQuery, connection, dataModelObject, user, body.query?.queryLimit
1863+
);
1864+
} catch (err) {
1865+
console.warn('[Prediction] Error fetching reference datasets, falling back to univariate:', err.message);
1866+
referenceDatasets = [];
1867+
}
1868+
}
1869+
1870+
// tfParams puede ser undefined o {epochs, lookback, learningRate}
1871+
await DashboardController.applyTensorflowPredicction(
1872+
predictions, originalDataset, setup, predictionConfig.tensorflowParams, referenceDatasets);
1873+
break;
1874+
}
1875+
}
1876+
1877+
// encontrar los datasets de las columnas de referencia para la predicción multivariante con TensorFlow. Cada dataset es un array de números alineado con el campo de fecha.
1878+
static async fetchReferenceDatasets(referenceColumns: {table_name: string, column_name: string}[],
1879+
dateField: any, myQuery: any, connection: any, dataModelObject: any, user: any, queryLimit?: number ): Promise<number[][]> {
1880+
const referenceDatasets: number[][] = [];
1881+
1882+
for (const refCol of referenceColumns) {
1883+
// Encontrar tablas
1884+
const table = dataModelObject.ds?.model?.tables?.find(t => t.table_name === refCol.table_name);
1885+
if (!table) {
1886+
console.warn(`[Prediction] Reference table not found: ${refCol.table_name}`);
1887+
continue;
1888+
}
1889+
// Encontrar columnas
1890+
const colDef = table.columns?.find(c => c.column_name === refCol.column_name);
1891+
if (!colDef) {
1892+
console.warn(`[Prediction] Reference column not found: ${refCol.column_name} in ${refCol.table_name}`);
1893+
continue;
1894+
}
1895+
1896+
// Build a query with the same date field + this reference column
1897+
const refQuery = {
1898+
fields: [
1899+
{ ...dateField }, // Same date field as main query for alignment
1900+
{
1901+
column_name: colDef.column_name,
1902+
column_type: colDef.column_type || 'numeric',
1903+
table_id: refCol.table_name,
1904+
display_name: colDef.display_name?.default || colDef.column_name,
1905+
order: 0,
1906+
aggregation_type: 'sum',
1907+
}
1908+
],
1909+
filters: myQuery.filters || [],
1910+
queryMode: myQuery.queryMode || 'EDA',
1911+
simple: myQuery.simple,
1912+
joinType: myQuery.joinType || 'inner',
1913+
};
1914+
1915+
try {
1916+
const refQueryBuilt = await connection.getQueryBuilded(refQuery, dataModelObject, user, queryLimit);
1917+
connection.client = await connection.getclient();
1918+
const refResults = await connection.execQuery(refQueryBuilt);
1919+
1920+
// Extract numeric values from results (column index 1 = the reference column)
1921+
let dataset: number[];
1922+
if (Array.isArray(refResults) && refResults.length > 0) {
1923+
if (Array.isArray(refResults[0])) {
1924+
// MongoDB format: already arrays
1925+
dataset = refResults.map(row => row[1]).filter(val => Number.isFinite(val));
1926+
} else {
1927+
// SQL format: objects
1928+
const keys = Object.keys(refResults[0]);
1929+
const valueKey = keys[1]; // Second column is the numeric value
1930+
dataset = refResults.map(row => {
1931+
const val = parseFloat(row[valueKey]);
1932+
return isNaN(val) ? null : val;
1933+
}).filter(val => val !== null && Number.isFinite(val));
1934+
}
1935+
}
18441936

1937+
if (dataset && dataset.length > 0) {
1938+
referenceDatasets.push(dataset);
1939+
} else {
1940+
console.warn(`[Prediction] No data for reference column ${refCol.table_name}.${refCol.column_name}`);
1941+
}
1942+
} catch (err) {
1943+
console.warn(`[Prediction] Failed to fetch reference column ${refCol.table_name}.${refCol.column_name}:`, err.message);
1944+
}
1945+
}
1946+
1947+
return referenceDatasets;
1948+
}
1949+
1950+
// Formula de arima
1951+
static applyArimaPredicction(predictions: number[], originalDataset: any, setup: {steps,rows,lastIndex,nextLabels}, arimaParams?: {p: number, d: number, q: number}) {
18451952
try {
1846-
// Calculamos las predicciones a través del servicio ARIMA
1847-
predictions = ArimaService.forecast(originalDataset, steps);
1953+
predictions = ArimaService.forecast(originalDataset, setup.steps, arimaParams);
18481954
} catch (err) {
18491955
console.error('Error ARIMA:', err);
18501956
return;
18511957
}
18521958

1853-
// Añadir columna predictionet
1854-
rows.forEach((row, index) => {
1855-
row.push(index === lastIndex ? row[1] : null);
1959+
// Añadir columna predicción a las filas existentes
1960+
// Solo el último punto real tiene valor para "unir" visualmente ambas líneas
1961+
setup.rows.forEach((row, index) => {
1962+
row.push(index === setup.lastIndex ? row[1] : null);
18561963
});
18571964

1858-
// Añadir fila de fechas
1859-
nextLabels.forEach((label, index) => {
1860-
rows.push([label, null, predictions[index] ?? null]);
1965+
// Añadir filas de fechas futuras con los valores predichos
1966+
setup.nextLabels.forEach((label, index) => {
1967+
setup.rows.push([label, null, predictions[index] ?? null]);
18611968
});
18621969

1863-
console.log('Predicción ARIMA aplicada');
18641970
}
18651971

1972+
// Predicción con TensorFlow
1973+
static async applyTensorflowPredicction(predictions: number[], originalDataset: any, setup: {steps,rows,lastIndex,nextLabels}, tfParams?: any, referenceDatasets?: number[][]) {
1974+
try {
1975+
predictions = await TensorflowService.forecast(originalDataset, setup.steps, tfParams, referenceDatasets);
1976+
} catch (err) {
1977+
console.error('Error TensorFlow:', err);
1978+
return;
1979+
}
1980+
1981+
// Añadir columna predicción a las filas existentes
1982+
setup.rows.forEach((row, index) => {
1983+
row.push(index === setup.lastIndex ? row[1] : null);
1984+
});
1985+
1986+
// Añadir filas de fechas futuras con los valores predichos
1987+
setup.nextLabels.forEach((label, index) => {
1988+
setup.rows.push([label, null, predictions[index] ?? null]);
1989+
});
1990+
1991+
}
18661992

18671993
/**
18681994
* Executa una consulta SQL per un dashboard

eda/eda_api/lib/module/predictions/predictions.controller.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { NextFunction, Request, Response } from 'express';
22
import { HttpException } from '../global/model/index';
33
import { ArimaService as ArimaLogic} from '../../services/prediction/arima.service';
4+
import { TensorflowService } from '../../services/prediction/tensorflow.service';
45

5-
export class ArimaController {
6+
export class PredictionsController {
7+
// Listado de todas las llamadas a la API para las predicciones
68

7-
static async predict(req: Request, res: Response, next: NextFunction) {
9+
// LLamada a Arima
10+
static async predictArima(req: Request, res: Response, next: NextFunction) {
811
try {
912
const { dataset, steps } = req.body;
1013

@@ -21,9 +24,28 @@ export class ArimaController {
2124
});
2225

2326
} catch (err) {
24-
console.error(err);
2527
next(new HttpException(500, 'Error generando predicción ARIMA'));
2628
}
2729
}
2830

31+
// LLamada a TensorFlow
32+
static async predictTensorflow(req: Request, res: Response, next: NextFunction) {
33+
try {
34+
const { dataset, steps } = req.body;
35+
36+
if (!dataset || !Array.isArray(dataset)) {
37+
return res.status(400).json({ ok: false, message: 'Dataset inválido' });
38+
}
39+
40+
const predictions = await TensorflowService.forecast(dataset, steps || 1);
41+
42+
res.status(200).json({
43+
ok: true,
44+
predictions
45+
});
46+
47+
} catch (err) {
48+
next(new HttpException(500, 'Error generando predicción TensorFlow'));
49+
}
50+
}
2951
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as express from 'express';
2-
import { ArimaController } from './predictions.controller';
2+
import { PredictionsController } from './predictions.controller';
33
import { authGuard } from '../../guards/auth-guard'; // opcional
44

55
const router = express.Router();
66

7-
router.post('/predict', authGuard, ArimaController.predict);
7+
router.post('/predict', authGuard, PredictionsController.predictArima);
8+
router.post('/tensorflow/predict', authGuard, PredictionsController.predictTensorflow);
89

910
export default router;

0 commit comments

Comments
 (0)