3 agosto, 2026

EXPERIMENTANDO CON AZURE FUNCTIONS: TRES FORMAS DE VISUALIZAR TEXTO EN NUBES DE PALABRAS

Hola a todos,

Siguiendo con este post: GENERAR NUBE DE PALABRAS CON AZURE FUNCTIONS EN POWER APPS he decidido crear dos nuevos códigos para generar nubes de palabras en función de su visualización.

Nube más ejecutiva: En este caso he priorizado la claridad visual frente a la cantidad de términos mostrados, destacando únicamente los conceptos más relevantes para facilitar una lectura rápida y una mejor integración en dashboards, informes y aplicaciones desarrolladas con Power Apps. La generación de la imagen se realiza mediante una Azure Function en Python, lo que permite reutilizar el servicio desde distintos entornos.

import azure.functions as func
import logging
import json
import base64
import re
import random
from io import BytesIO
from collections import Counter
from wordcloud import WordCloud

STOPWORDS_ES = set([
    "un", "una", "unas", "unos", "uno", "sobre", "todo", "también", "tras",
    "otro", "algún", "alguno", "alguna", "algunos", "algunas", "ser", "es",
    "soy", "eres", "somos", "sois", "estoy", "esta", "estamos", "estais",
    "estan", "como", "en", "para", "atras", "porque", "por qué", "estado",
    "estaba", "ante", "antes", "siendo", "ambos", "pero", "por", "poder",
    "puede", "podria", "pueden", "puedo", "podríamos", "mientras", "sin",
    "al", "del", "la", "las", "lo", "los", "de", "el", "que", "se",
    "y", "a", "o", "u", "e", "ni",
    "yo", "tú", "usted", "él", "ella", "ello", "nosotros", "nosotras",
    "vosotros", "vosotras", "ellos", "ellas", "me", "te", "nos", "os",
    "mi", "mis", "tu", "tus", "su", "sus", "nuestro", "nuestra", "vuestro",
    "vuestra", "esto", "estos", "estas", "ese", "esa", "esos", "esas",
    "aquel", "aquella", "aquellos", "aquellas", "quien", "quienes", "cual", "cuales",
    "haber", "ha", "han", "he", "hemos", "haya", "había", "hubo",
    "fue", "fueron", "era", "eran", "sería", "serían", "está", "están",
    "estar", "tenido", "tenía", "tiene", "tenemos", "tienen", "tener",
    "hacer", "hacía", "hace", "hacen", "hizo", "hecho", "decir", "dicho",
    "debe", "deben", "debemos", "dar", "dado", "ver", "visto", "ir", "ido",
    "venir", "viene", "venimos", "quiere", "querer", "sabemos", "saber",
    "trabaja", "trabajar", "hablar", "siento", "creo", "son",
    "entonces", "pues", "aunque", "además", "así", "aquí", "allí", "ahora",
    "después", "hoy", "ayer", "mañana", "nunca", "siempre", "ya",
    "tan", "tanto", "donde", "cuando", "sin embargo", "por tanto",
    "por eso", "por lo tanto", "cada", "toda", "todos", "todas",
    "algo", "nada", "mucho", "mucha", "muchos", "muchas", "poco",
    "poca", "pocos", "pocas", "demasiado", "bastante", "ningún",
    "ninguna", "varios", "varias", "respecto", "mismo", "misma",
    "inclusive", "quizás", "tal", "casi", "aproximadamente", "etc",
    "según", "igual", "cerca", "lejos", "hay", "con", "más", "muy",
    "no", "entre", "desde", "eso",
    "empresa", "trabajo", "laboral", "oficina", "puesto", "área",
    "departamento", "departamentos", "organización", "cargo", "personal",
    "compañero", "compañeros", "colaborativo", "colaboración", "grupo",
    "dirección", "superior", "empleado", "empleados", "reunión",
    "reuniones", "equipo", "jefe", "liderazgo", "gerente", "coordinador",
    "miembro", "entorno", "ambiente", "clima", "áreas", "personas",
    "nuestras",
    "últimamente", "actualmente", "general", "genera", "forma", "nivel",
    "modo", "tipo", "caso", "situación", "momento", "parte", "veces",
    "último", "últimos", "última", "últimas", "época", "épocas",
    "tiempo", "días", "años", "meses", "día", "año", "mes", "hora",
    "horas", "minutos", "segundos", "espacio", "espacios", "aspecto",
    "aspectos", "contexto", "problema", "problemas", "cosa", "cosas",
    "tema", "temas", "asunto", "asuntos", "manera", "motivo", "motivos",
    "mejor", "extra", "bueno", "buen", "excelente", "positivo", "buena",
    "mejora", "mejorado"
])

PALETA_EJECUTIVA = [
    "#0F172A",
    "#1E3A8A",
    "#2563EB",
    "#0F766E",
    "#0891B2",
    "#64748B",
    "#EF6351"
]

def color_func_ejecutiva(word, font_size, position, orientation, random_state=None, **kwargs):
    random.seed(hash(word))
    return random.choice(PALETA_EJECUTIVA)

def limpiar_y_contar(texto, stopwords_adicionales=None):
    if stopwords_adicionales is None:
        stopwords_adicionales = []

    stopwords_finales = STOPWORDS_ES.union(set([w.lower().strip() for w in stopwords_adicionales]))

    texto = texto.lower()
    texto = re.sub(r"https?://\S+|www\.\S+", " ", texto)
    texto = re.sub(r"[\r\n\t]+", " ", texto)
    texto = re.sub(r"[_/\\|]", " ", texto)
    texto = re.sub(r"[^a-záéíóúüñ\s]", " ", texto)
    texto = re.sub(r"\s+", " ", texto).strip()

    tokens = re.findall(r"[a-záéíóúüñ]{3,}", texto, flags=re.IGNORECASE)

    frecuencias = Counter()
    for token in tokens:
        if token not in stopwords_finales:
            frecuencias[token] += 1

    return frecuencias

def generar_imagen_ejecutiva(frecuencias, max_palabras):
    wc = WordCloud(
        width=800,
        height=400,
        scale=2,
        background_color="white",
        max_words=max_palabras,
        prefer_horizontal=0.96,
        relative_scaling=0.28,
        collocations=False,
        margin=12,
        min_font_size=10,
        max_font_size=125,
        random_state=42,
        normalize_plurals=False
    ).generate_from_frequencies(frecuencias)

    wc.recolor(color_func=color_func_ejecutiva, random_state=42)
    return wc.to_image().convert("RGB")

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("🟢 NubeEjecutiva iniciada.")

    try:
        req_body = req.get_json()
        texto = req_body.get("texto")

        if not texto or not str(texto).strip():
            return func.HttpResponse(
                json.dumps({"error": "El campo 'texto' es obligatorio."}, ensure_ascii=False),
                mimetype="application/json",
                status_code=400
            )

        max_palabras = int(req_body.get("max_palabras", 40))
        excluir = req_body.get("excluir", [])

        frecuencias = limpiar_y_contar(texto, excluir)

        if not frecuencias:
            return func.HttpResponse(
                json.dumps({"error": "No hay palabras suficientes para generar la nube."}, ensure_ascii=False),
                mimetype="application/json",
                status_code=400
            )

        imagen = generar_imagen_ejecutiva(frecuencias, max_palabras)

        buffer = BytesIO()
        imagen.save(buffer, format="PNG", optimize=True)
        imagen_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")

        resultado = {
            "imagen": imagen_b64,
            "estilo": "ejecutivo"
        }

        return func.HttpResponse(
            json.dumps(resultado, ensure_ascii=False),
            mimetype="application/json",
            status_code=200
        )

    except Exception as e:
        logging.error(f"❌ Error en NubeEjecutiva: {str(e)}")
        return func.HttpResponse(
            json.dumps({"error": f"Error interno: {str(e)}"}, ensure_ascii=False),
            mimetype="application/json",
            status_code=500
        )

Nube Visual: En este caso he priorizado una mayor densidad de información y una distribución más equilibrada de los términos para aprovechar mejor el espacio disponible. El resultado es una representación más rica y llamativa, ideal para análisis exploratorios, presentaciones o cuadros de mando donde la componente visual tiene un papel importante. Generada mediante una Azure Function en Python para su reutilización desde distintos entornos.

import azure.functions as func
import logging
import json
import base64
import re
import random
from io import BytesIO
from collections import Counter
from wordcloud import WordCloud

STOPWORDS_ES = set([
    "un", "una", "unas", "unos", "uno", "sobre", "todo", "también", "tras",
    "otro", "algún", "alguno", "alguna", "algunos", "algunas", "ser", "es",
    "soy", "eres", "somos", "sois", "estoy", "esta", "estamos", "estais",
    "estan", "como", "en", "para", "atras", "porque", "por qué", "estado",
    "estaba", "ante", "antes", "siendo", "ambos", "pero", "por", "poder",
    "puede", "podria", "pueden", "puedo", "podríamos", "mientras", "sin",
    "al", "del", "la", "las", "lo", "los", "de", "el", "que", "se",
    "y", "a", "o", "u", "e", "ni",
    "yo", "tú", "usted", "él", "ella", "ello", "nosotros", "nosotras",
    "vosotros", "vosotras", "ellos", "ellas", "me", "te", "nos", "os",
    "mi", "mis", "tu", "tus", "su", "sus", "nuestro", "nuestra", "vuestro",
    "vuestra", "esto", "estos", "estas", "ese", "esa", "esos", "esas",
    "aquel", "aquella", "aquellos", "aquellas", "quien", "quienes", "cual", "cuales",
    "haber", "ha", "han", "he", "hemos", "haya", "había", "hubo",
    "fue", "fueron", "era", "eran", "sería", "serían", "está", "están",
    "estar", "tenido", "tenía", "tiene", "tenemos", "tienen", "tener",
    "hacer", "hacía", "hace", "hacen", "hizo", "hecho", "decir", "dicho",
    "debe", "deben", "debemos", "dar", "dado", "ver", "visto", "ir", "ido",
    "venir", "viene", "venimos", "quiere", "querer", "sabemos", "saber",
    "trabaja", "trabajar", "hablar", "siento", "creo", "son",
    "entonces", "pues", "aunque", "además", "así", "aquí", "allí", "ahora",
    "después", "hoy", "ayer", "mañana", "nunca", "siempre", "ya",
    "tan", "tanto", "donde", "cuando", "sin embargo", "por tanto",
    "por eso", "por lo tanto", "cada", "toda", "todos", "todas",
    "algo", "nada", "mucho", "mucha", "muchos", "muchas", "poco",
    "poca", "pocos", "pocas", "demasiado", "bastante", "ningún",
    "ninguna", "varios", "varias", "respecto", "mismo", "misma",
    "inclusive", "quizás", "tal", "casi", "aproximadamente", "etc",
    "según", "igual", "cerca", "lejos", "hay", "con", "más", "muy",
    "no", "entre", "desde", "eso",
    "empresa", "trabajo", "laboral", "oficina", "puesto", "área",
    "departamento", "departamentos", "organización", "cargo", "personal",
    "compañero", "compañeros", "colaborativo", "colaboración", "grupo",
    "dirección", "superior", "empleado", "empleados", "reunión",
    "reuniones", "equipo", "jefe", "liderazgo", "gerente", "coordinador",
    "miembro", "entorno", "ambiente", "clima", "áreas", "personas",
    "nuestras",
    "últimamente", "actualmente", "general", "genera", "forma", "nivel",
    "modo", "tipo", "caso", "situación", "momento", "parte", "veces",
    "último", "últimos", "última", "últimas", "época", "épocas",
    "tiempo", "días", "años", "meses", "día", "año", "mes", "hora",
    "horas", "minutos", "segundos", "espacio", "espacios", "aspecto",
    "aspectos", "contexto", "problema", "problemas", "cosa", "cosas",
    "tema", "temas", "asunto", "asuntos", "manera", "motivo", "motivos",
    "mejor", "extra", "bueno", "buen", "excelente", "positivo", "buena",
    "mejora", "mejorado"
])

PALETA_VISUAL = [
    "#5B21B6",
    "#7C3AED",
    "#DB2777",
    "#EF6351",
    "#F59E0B",
    "#0891B2",
    "#0F766E"
]

def color_func_visual(word, font_size, position, orientation, random_state=None, **kwargs):
    random.seed(hash(word))
    return random.choice(PALETA_VISUAL)

def limpiar_y_contar(texto, stopwords_adicionales=None):
    if stopwords_adicionales is None:
        stopwords_adicionales = []

    stopwords_finales = STOPWORDS_ES.union(set([w.lower().strip() for w in stopwords_adicionales]))

    texto = texto.lower()
    texto = re.sub(r"https?://\S+|www\.\S+", " ", texto)
    texto = re.sub(r"[\r\n\t]+", " ", texto)
    texto = re.sub(r"[_/\\|]", " ", texto)
    texto = re.sub(r"[^a-záéíóúüñ\s]", " ", texto)
    texto = re.sub(r"\s+", " ", texto).strip()

    tokens = re.findall(r"[a-záéíóúüñ]{3,}", texto, flags=re.IGNORECASE)

    frecuencias = Counter()
    for token in tokens:
        if token not in stopwords_finales:
            frecuencias[token] += 1

    return frecuencias

def generar_imagen_visual(frecuencias, max_palabras):
    wc = WordCloud(
        width=800,
        height=400,
        scale=2,
        background_color="white",
        max_words=max_palabras,
        prefer_horizontal=0.78,
        relative_scaling=0.38,
        collocations=False,
        margin=14,
        min_font_size=9,
        max_font_size=95,
        random_state=42,
        normalize_plurals=False
    ).generate_from_frequencies(frecuencias)

    wc.recolor(color_func=color_func_visual, random_state=42)
    return wc.to_image().convert("RGB")

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("🟢 NubeVisual iniciada.")

    try:
        req_body = req.get_json()
        texto = req_body.get("texto")

        if not texto or not str(texto).strip():
            return func.HttpResponse(
                json.dumps({"error": "El campo 'texto' es obligatorio."}, ensure_ascii=False),
                mimetype="application/json",
                status_code=400
            )

        max_palabras = int(req_body.get("max_palabras", 45))
        excluir = req_body.get("excluir", [])

        frecuencias = limpiar_y_contar(texto, excluir)

        if not frecuencias:
            return func.HttpResponse(
                json.dumps({"error": "No hay palabras suficientes para generar la nube."}, ensure_ascii=False),
                mimetype="application/json",
                status_code=400
            )

        imagen = generar_imagen_visual(frecuencias, max_palabras)

        buffer = BytesIO()
        imagen.save(buffer, format="PNG", optimize=True)
        imagen_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")

        resultado = {
            "imagen": imagen_b64,
            "estilo": "visual"
        }

        return func.HttpResponse(
            json.dumps(resultado, ensure_ascii=False),
            mimetype="application/json",
            status_code=200
        )

    except Exception as e:
        logging.error(f"❌ Error en NubeVisual: {str(e)}")
        return func.HttpResponse(
            json.dumps({"error": f"Error interno: {str(e)}"}, ensure_ascii=False),
            mimetype="application/json",
            status_code=500
        )

Con estos códigos, ya podríamos generar las Functions de Azure.

Que una vez desplegadas ya podríamos utilizar Automate y el conector Http para generar la información.

La primera imagen es la que ya tenía, y las otras son la ejecutiva (centro) y visual (derecha).

Creo que no puedo elegir una como la mejor, cualquiera es buena según el entorno.

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