Proyecto full-stack en solitario · 2019–2020 · Angular · Firebase · ML

Makaw: previsión de finanzas personales

Una web en Angular + Firebase para gestionar las finanzas personales y prever los ahorros futuros, con un modelo Random Forest en Google Cloud que predice cuánto influye cada factor de tu vida en lo que ahorras.

Makaw: previsión de finanzas personales

Makaw es un proyecto de finanzas personales que desarrollé entre 2019 y 2020. Partió de una pregunta que casi todo el mundo se hace en algún momento: ¿cuánto podré ahorrar dentro de un año? Y si quiero ahorrar una cantidad concreta, ¿cuánto puedo permitirme gastar?

Las aplicaciones existentes resolvían solo la mitad. Las apps de los bancos y Fintonic son estupendas mostrando tu situación actual; Monefy lo es para registrar movimientos rápido a mano. Ninguna preveía el futuro, ni te daba un objetivo de ahorro concreto, ni dejaba a los usuarios hablar de finanzas entre ellos. Esos tres huecos son justo lo que Makaw quería cubrir, y, como ingeniero, lo interesante estaba en la arquitectura de debajo.

Qué puedes hacer en la app #s1

Empiezas registrando tus ingresos y gastos, incluidos los periódicos, como una nómina o una suscripción mensual. Donde la mayoría de apps se quedan en dibujar el pasado, Makaw proyecta esos movimientos periódicos hacia delante y traza la curva de a dónde va realmente tu saldo.

Alrededor de esa previsión se sitúan el resto de funcionalidades: una predicción con machine learning de cuánto suele ahorrar alguien con tu perfil, un cálculo de objetivos de ahorro que convierte «llegar a una cantidad para una fecha» en un gasto concreto por mes y por día, una puntuación financiera que te compara con gente como tú, y una pequeña capa social: un tablón estilo Twitter para consejos de finanzas personales y chat en tiempo real entre usuarios.

Está disponible en makaw-3a1bf.web.app y el código completo está en GitHub, ambos enlazados al final de esta página. Lo que sigue es la ingeniería de debajo, desde la infraestructura hasta el frontend.

Firebase como toda la infraestructura de backend #s2

En lugar de construir y mantener un backend tradicional, Makaw corre por completo sobre Firebase como Backend as a Service. Authentication gestiona el inicio de sesión (con proveedores OAuth disponibles de serie); Cloud Firestore, una base de datos NoSQL de documentos en tiempo real, es el corazón del sistema; las Cloud Functions alojan los endpoints serverless; Cloud Storage guarda el contenido generado por el usuario; y Firebase Hosting sirve la aplicación con despliegues de un solo comando y SSL.

La base de datos se modela como colecciones de documentos, no como tablas y filas. Usuarios, transacciones, chats y posts viven cada uno en su propia colección; un documento no es más que un conjunto de pares clave-valor (un objeto JSON), y el esquema puede variar de un documento a otro: justo la flexibilidad que da NoSQL cuando la forma de una entidad cambia con el tiempo.

De este mismo backend cuelga el pipeline de ML offline: una Cloud Function hace de puente entre la app de Angular y un modelo Random Forest servido en Google Cloud AI Platform, que detallo más abajo.

Makaw infrastructure An Angular single-page app in the browser talks to Firebase (Authentication, Cloud Firestore, Cloud Storage and a predictSavings Cloud Function). The Cloud Function calls a Random Forest model named user_savings on Google Cloud AI Platform. Below, an offline pipeline generates a synthetic dataset in Java, trains the model in a Cloud Datalab Jupyter notebook with scikit-learn, serialises it to forestmodel.joblib and deploys it to AI Platform. Browser Angular SPA served by Firebase Hosting HTTPS realtime Firebase Backend-as-a-Service Authentication sign-in · OAuth Cloud Storage user content Cloud Firestore NoSQL · realtime documents users · transactions · chats · posts Cloud Functions predictSavings europe-west3 · CORS Google Cloud AI Platform ML Engine user_savings Random Forest regressor googleapis service account · IAM Model training offline pipeline data-generator Java synthetic dataset dataset.csv 7 features → savings Cloud Datalab Jupyter notebook scikit-learn forestmodel.joblib trained model deploy pull user data
La infraestructura de Makaw: la SPA de Angular en el navegador habla por HTTPS con Firebase (Authentication, Cloud Firestore, Cloud Storage y la Cloud Function predictSavings en europe-west3); esa función llama al modelo Random Forest user_savings en Google Cloud AI Platform. Debajo, un pipeline offline genera un dataset sintético en Java, entrena el modelo en un notebook de Cloud Datalab con scikit-learn y despliega forestmodel.joblib en AI Platform.

Chat en tiempo real y archivos de usuario en Firestore #s3

El chat es la funcionalidad donde más se nota el carácter en tiempo real de Firestore. Cada conversación es un único documento en una colección chats, y el id del documento se deriva de forma determinista: se toman los uids de los dos participantes, se ordenan alfabéticamente y se concatenan. Como la misma pareja produce siempre el mismo id, no hay tabla de búsqueda ni riesgo de dos conversaciones en paralelo: el id es la dirección.

Firestore devuelve entonces ese documento como un observable «infinito»: un array de mensajes que se reemite entero cada vez que alguien escribe, de modo que las pantallas de ambos participantes se actualizan en vivo sin hacer polling. La contrapartida de un stream sin fin es que desuscribirse pasa a ser responsabilidad tuya cuando se cierra la conversación.

Los archivos de usuario (imágenes de perfil y vídeo) viven en Cloud Storage, con subidas y descargas seguras gestionadas por Firebase. Los bytes pesados se quedan en Storage y dentro del documento de Firestore solo se escribe la referencia ligera (la URL de descarga), el reparto habitual que mantiene los documentos pequeños y las lecturas baratas.

Real-time chat and media on Firestore Two users derive a deterministic conversation id by sorting their two uids alphabetically and concatenating them, so each pair maps to exactly one Firestore document under the chats collection with no lookup. The chats/{conversationId} document holds the messages; a message that carries a photo stores only a download URL that points to the bytes living in Cloud Storage. Firestore exposes the document as an "infinite" observable — an array of messages re-emitted on every write — which is pushed live to both subscribers. User A uid a7f1… User B uid c3d9… conversation id [uidA, uidB].sort().join('') deterministic — one document per pair chats/{conversationId} { from: a7f1, text: 'hola', ts } { from: c3d9, text: '¿ahorras?', ts } { from: a7f1, photoUrl, ts } Cloud Storage images · video secure up / download via Firebase url ref valueChanges() “infinite” observable Observable<Message[]> · re-emits on write pushed live to both subscribers
Dos usuarios derivan un id de conversación determinista ordenando sus uids y concatenándolos, direccionando exactamente un documento chats/{conversationId}. El documento guarda los mensajes; un mensaje con foto guarda solo una URL de descarga que apunta a los bytes en Cloud Storage. Firestore expone el documento como un observable «infinito» que reemite el array completo de mensajes en cada escritura, empujado en vivo a ambos suscriptores.

Previsión de ahorros con machine learning #s4

La funcionalidad estrella era la previsión. Prever a partir de datos históricos era la mitad fácil: proyectar las transacciones periódicas hacia delante y dibujar la curva del balance futuro. La mitad interesante era la US0009: predecir el ahorro a partir del perfil del usuario con un modelo.

Entrené un Random Forest para regresión (scikit-learn) en un notebook de Google Cloud Datalab, con todo el pipeline guardado en el directorio machine-learning del repo. Los árboles de decisión por sí solos sobreajustan; un bosque de árboles (aquí 1.200 árboles, profundidad 5, cada uno viendo una porción distinta de los datos y promediando sus salidas) generaliza mucho mejor y, de regalo, informa de la importancia de cada variable. Como la plataforma apenas tenía usuarios reales, un pequeño data-generator en Java sintetizaba el dataset, con una clase por variable: estudios, ciudad, edad, hijos, estado civil, género y un campo «entidad bancaria» deliberadamente aleatorio como control, para demostrar que el modelo encuentra la señal oculta dentro del ruido.

El resultado: un error absoluto medio de unos 135 € con el modelo frente a unos 518 € usando solo la media, es decir, unas 5 veces más preciso. Los pesos de las variables salieron así: ciudad de residencia 30 %, nivel de estudios 22 %, edad 19 %, género 15 %, hijos 12 %, y estado civil y entidad bancaria ~0 % (el control aleatorio pesó cero, como debía). El bosque entrenado se serializó a un forestmodel.joblib y se entregó a Google Cloud para servirlo.

Modal «Artificial Intelligence Predictions» de Makaw: desplegables de estudios, ciudad, edad, número de hijos, estado civil, género y entidad bancaria, con un botón Request.
Modal «Artificial Intelligence Predictions» de Makaw: desplegables de estudios, ciudad, edad, número de hijos, estado civil, género y entidad bancaria, con un botón Request.

Del notebook a producción: el puente serverless #s5

Sacar el modelo del notebook fue el verdadero reto de ingeniería, y el repositorio reparte ese trabajo en piezas separadas. El Random Forest entrenado se subió a Google Cloud y se publicó en AI Platform (ML Engine) como un modelo versionado llamado user_savings.

Una Cloud Function ligera de Firebase (predictSavings, desplegada en europe-west3 y alojada en el directorio firebase-functions) es el puente entre la app de Angular y ese modelo. Gestiona CORS (incluido el preflight OPTIONS), se autentica contra Google Cloud con una cuenta de servicio con scope cloud-platform, reenvía las instances de variables de la petición a ml.projects.predict mediante el cliente googleapis y devuelve la predicción. Casi todo el dolor aquí fue de autorización IAM, no del modelo en sí.

Un directorio aparte, cloud-functions, contiene un script experimental con el Admin SDK que dejé como andamiaje para una futura funcionalidad de servidor: su commit está marcado con sinceridad como «ignora esto, es solo un ejemplo para la próxima feature». La puntuación financiera, en cambio, se quedó en el cliente, en Angular, con solo un calculateScore esbozado y comentado para el día en que se moviera al backend.

Una arquitectura por capas alrededor del patrón fachada #s6

Antes de escribir funcionalidades dediqué una etapa inicial de «echar raíces» al diseño del que colgaría todo lo demás. La aplicación se divide en tres capas: una capa core (estado y acceso a la API), una capa de abstracción (las fachadas) y una capa de presentación (los componentes).

Cada dominio (transacciones, usuarios, chat) expone una fachada: un servicio inyectable que encapsula lo que los componentes pueden ver y hacer. Los componentes nunca tocan la API ni el estado directamente; solo hablan con la fachada. Esa única indirección es lo que mantuvo el código mantenible a medida que crecía.

Makaw layered architecture around the facade pattern Three stacked layers. The presentation layer holds the smart Container component plus the dumb Table and Chart components. The abstraction layer holds the injectable TransactionsFacade, the single indirection every component must go through. The core layer holds the transactions state (a BehaviourSubject exposed as an Observable) and the transactions API (Firestore access). Components talk only to the facade; they never reach the core directly, shown by a crossed-out red path. Presentation layer components Container smart component subscribes · dispatches Table dumb component renders rows Chart components line · bar · pie each derives its own observable components talk only to the facade Abstraction layer the single indirection TransactionsFacade injectable · guards what components may see + do next() · observable fetch · persist Core layer state + API access Transactions state BehaviourSubject<Transaction[]> exposed only as Observable Transactions API Firestore access load · create · update no direct access
Tres capas apiladas. La capa de presentación contiene el Container (smart) más los componentes dumb de tabla y gráficas (línea/barras/tarta). La capa de abstracción contiene la TransactionsFacade inyectable, la única indirección por la que pasa todo componente. La capa core contiene el estado de transacciones (un BehaviourSubject expuesto solo como Observable) y la API de transacciones (acceso a Firestore). Los componentes solo hablan con la fachada; un camino rojo tachado marca que nunca acceden al core directamente.

Estado reactivo con RxJS y flujo unidireccional #s7

El estado vive tras la fachada como un BehaviourSubject: un array de transacciones que empieza vacío y se expone a los componentes solo como Observable. Un componente smart (el contenedor) pide a la fachada que cargue los datos, se suscribe a transactions$ y obtiene respuesta inmediata gracias al valor inicial; cuando más tarde responde la API, la fachada hace next(transactions) y la suscripción se actualiza.

A partir de ahí todo es estrictamente unidireccional: el componente smart pasa el observable hacia abajo a los componentes dumb (las gráficas lineal, de barras y circular) como @Input. Cada hijo se suscribe y deriva su propio observable interno, de modo que sus transformaciones nunca alteran el stream del padre. Los cambios fluyen hacia abajo, nunca hacia arriba; las ediciones vuelven a través de eventos @Output que piden a la fachada actualizar el estado. Esta misma forma se reutilizó tal cual para usuarios y chat.

Modal «Create transaction» de Makaw sobre la tabla del tablero, alternando entre ingreso y gasto con descripción, categoría, importe y fecha.
Modal «Create transaction» de Makaw sobre la tabla del tablero, alternando entre ingreso y gasto con descripción, categoría, importe y fecha.

Puntuación, objetivos de ahorro, tablón y chat #s8

Sobre la previsión se asentaba la capa social y de objetivos. La puntuación financiera compara tus ahorros actuales con la media de los usuarios que comparten tu perfil (currentUserSavings / peopleAverageSavings): por debajo de uno se muestra en rojo con el porcentaje que te falta, por encima de uno en verde, y a partir de 2× cambia del porcentaje a un «X veces la media».

Los objetivos de ahorro te dejan elegir una fecha futura; la app calcula lo máximo que podrías ahorrar para entonces a partir de tus transacciones periódicas, con un slider (cableado con RxJS suscribiéndose a ambos inputs del formulario) que recalcula el gasto por mes y por día en vivo mientras lo arrastras.

El tablón es un muro estilo Twitter para consejos de finanzas personales: una colección posts donde cada post lleva subcolecciones anidadas de comments y likes, mantenidas en sincronía por los mismos observables en tiempo real de Firestore, asumiendo de buena gana la redundancia de datos que NoSQL cambia por lecturas más simples y rápidas. El chat, tratado más arriba, es la capa de mensajería en tiempo real entre usuarios.

Tarjetas del tablero de Makaw: ahorro actual de 3.450 € con «ahorras un 66 % más que una persona con tus mismas características», más tarjetas de acción en degradado para añadir transacciones, ver transacciones futuras y predicciones de IA.
Tarjetas del tablero de Makaw: ahorro actual de 3.450 € con «ahorras un 66 % más que una persona con tus mismas características», más tarjetas de acción en degradado para añadir transacciones, ver transacciones futuras y predicciones de IA.

Qué me llevo #s9

Makaw fue donde noté por primera vez la recompensa de invertir en arquitectura antes que en funcionalidades. La base de fachada más RxJS hizo que la gestión del estado fuera de verdad mantenible, y la separación entre componente y servicio hizo que la librería de cálculo de transacciones y la UI reutilizable (tabla, navbar, tarjetas) vinieran gratis en cada pantalla nueva.

También me enseñó a estimar sin experiencia previa, a documentar el trabajo según avanzaba (un rastro disciplinado de Trello y git), y que lo más difícil de poner ML en producción rara vez es el modelo: son los permisos y la fontanería que rodean su despliegue. Años después, trabajando con Angular profesionalmente, veo muchas cosas que refactorizaría hoy, lo cual tiene su propia satisfacción.

Stack

Angular TypeScript RxJS Firebase Cloud Firestore Python · scikit-learn

Enlaces