2 agosto, 2026

OBTENER EL ELECTROCARDIOGRAMA EMOCIONAL DE LA ORGANIZACIÓN

Como ya os había comentado, en mi suscripción de Azure tengo tanto modelos desplegados en Azure AI Foundry como funciones de Azure Functions.

Hay algunas ventajas muy importantes respecto a Foundry, y básicamente es el precio y el control de todo el proceso. Puedo desplegar funciones que no alucinen (o por lo menos no temer a que lo hagan) y el precio, por lo general es mucho más bajo.

Por ejemplo, pensé en la posibilidad de crear un proceso que hiciese lo siguiente:
tengo comentarios de personas de mi empresa imaginaria desde el 1 de enero hasta mayo. En total, 550 comentarios de 550 personas distintas, realizados en fechas diferentes.

Pues bien, quiero visualizar esa información como si se tratase de un electrocardiograma, una forma diferente de ver e interpretar los datos.

¿Qué os parece?
A mí me encanta: es como tomar el pulso al corazón de la empresa ❤️

Para ello utilicé Azure Functions.
Podría haber utilizado Sora, pero habría sido mucho más caro y, además, cada ejecución generaría una imagen diferente.

Y en este caso lo que buscaba era control total sobre lo que voy a generar, asegurando coherencia, reproducibilidad y estabilidad en el resultado.

Por lo tanto, vamos como siempre con la función que vamos a colocar en VSC antes de desplegarla en Azure:

import io
from datetime import datetime
from collections import defaultdict

import azure.functions as func
import matplotlib.pyplot as plt


def main(req: func.HttpRequest) -> func.HttpResponse:
    try:
        # Leemos el cuerpo de la petición
        body = req.get_json()
        items = body.get("items", [])

        if not items:
            return func.HttpResponse("No hay datos", status_code=400)

        ancho = body.get("ancho", 1200)
        alto = body.get("alto", 300)

        # Procesamos los datos y capturamos el dominio real del sentimiento
        agrupado = defaultdict(list)
        fechas = []
        sentimientos_reales = []

        for it in items:
            try:
                fecha = datetime.fromisoformat(it["FechaEvento"])
                sentimiento = float(it["SentimientoScore"])

                sentimientos_reales.append(sentimiento)

                semana = fecha.strftime("%Y-%U")
                agrupado[semana].append(sentimiento)
                fechas.append(fecha)
            except:
                continue

        if not sentimientos_reales:
            return func.HttpResponse("Datos inválidos", status_code=400)

        semanas = sorted(agrupado.keys())
        medias = []
        volumenes = []

        for s in semanas:
            valores = agrupado[s]
            medias.append(sum(valores) / len(valores))
            volumenes.append(len(valores))

        # Calculamos el rango temporal
        fecha_inicio = min(fechas).strftime("%d %b %Y")
        fecha_fin = max(fechas).strftime("%d %b %Y")

        # Construimos el pulso tipo ECG
        x = []
        y = []
        t = 0.0

        max_vol = max(volumenes)

        for i, media in enumerate(medias):
            intensidad = volumenes[i] / max_vol
            pico = media * (1 + intensidad)

            latido = [
                (0.00, media * 0.2),
                (0.08, media * 0.3),
                (0.12, media * -0.2),
                (0.15, pico),
                (0.18, media * -0.4),
                (0.28, media * 0.2),
                (0.45, media * 0.2),
            ]

            for dx, dy in latido:
                x.append(t + dx)
                y.append(dy)

            t += 0.6

        # Definimos el eje Y a partir del dominio real del modelo
        y_model_min = min(sentimientos_reales)
        y_model_max = max(sentimientos_reales)

        margen_sup = (y_model_max - y_model_min) * 0.10

        ylim_inf = y_model_min
        ylim_sup = y_model_max + margen_sup

        ticks_y = [
            round(y_model_min, 2),
            round((y_model_min + 0) / 2, 2),
            0.0,
            round(y_model_max / 2, 2),
            round(y_model_max, 2)
        ]

        # Dibujamos la gráfica
        fig, ax = plt.subplots(figsize=(ancho / 100, alto / 100))

        ax.set_facecolor("#fffafa")
        ax.minorticks_on()
        ax.grid(which="minor", color="#e6bcbc", linewidth=0.4)
        ax.grid(which="major", color="#c08080", linewidth=0.8)

        ax.plot(x, y, color="#b00000", linewidth=2)
        ax.axhline(0, linestyle="--", linewidth=0.6, alpha=0.6)

        ax.set_ylim(ylim_inf, ylim_sup)
        ax.set_yticks(ticks_y)
        ax.tick_params(axis="y", labelsize=8)

        ax.set_xticks([])

        fig.suptitle("Pulso emocional de la organización", fontsize=14, y=0.97)
        fig.text(0.01, 0.92, f"{fecha_inicio} → {fecha_fin}", fontsize=9)

        ax.text(0.01, 0.88, "↑ Más positivo", transform=ax.transAxes,
                fontsize=9, color="green")
        ax.text(0.01, 0.08, "↓ Más negativo", transform=ax.transAxes,
                fontsize=9, color="red")
        ax.text(0.99, 0.02, "← Pasado     Presente →",
                transform=ax.transAxes,
                fontsize=8, ha="right")

        ax.spines["top"].set_visible(False)
        ax.spines["right"].set_visible(False)
        ax.spines["bottom"].set_visible(False)

        # Exportamos PNG
        buffer = io.BytesIO()
        plt.savefig(buffer, format="png", dpi=100)
        plt.close()
        buffer.seek(0)

        return func.HttpResponse(
            body=buffer.getvalue(),
            mimetype="image/png",
            status_code=200
        )

    except Exception as e:
        return func.HttpResponse(
            f"Error interno: {str(e)}",
            status_code=500
        )

Desplegada ya Azure:

Luego cargamos los datos en nuestra lista de SharePoint (o el medio que sea):

Y el siguiente paso es crear un flujo para que lea los datos de SharePoint con las respuestas de los 550 empleados y lo pase a la función desplegada en Azure y recupere nuestro electro:

La función de salida es esta, debéis añadirla como una expresión:

concat(
  'data:image/png;base64,',
  base64(body('HTTP'))
)

y finalmente, tenemos que incluir en nuestro power apps, en un botón:

Set(
    imgPulso;
    FLUJOPULSO.Run().img
)

Y en el control de imagen, imgPulso

El resultado:

Comparte este post

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies