{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "b6d1e3ec",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Clasificación híbrida de conjuntos mejorada cuánticamente (flujo de trabajo de estabilidad de la red)\"\n",
        "description: \"Construya y analice un conjunto híbrido cuántico-clásico para la clasificación de la estabilidad de la red en QPU de IBM utilizando la función Singularity Qiskit de Multiverse Computing.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore QEEC interpretability hyperparameters overfitting sublinear preds prec */}\n",
        "\n",
        "<span id=\"hybrid-quantum-enhanced-ensemble-classification-grid-stability-workflow\" />\n",
        "\n",
        "# Clasificación híbrida de conjuntos mejorada cuánticamente (flujo de trabajo de estabilidad de la red)\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "a6f69b77",
      "metadata": {},
      "source": [
        "*Estimación de uso: 20 minutos en tiempo QPU para cada trabajo en un procesador Eagle r3. (NOTA: Esto es sólo una estimación. Su tiempo de ejecución puede variar)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8bf80006",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## En segundo plano\n",
        "\n",
        "Este tutorial muestra un flujo de trabajo híbrido cuántico-clásico que mejora un conjunto clásico mediante una etapa de optimización cuántica. Mediante el uso de «Singularity Machine Learning –Classification» de Multiverse Computing (una función de Qiskit), entrenamos un conjunto de modelos de aprendizaje convencionales (por ejemplo, árboles de decisión, k-NN o regresión logística) y, a continuación, refinamos dicho conjunto con una capa cuántica para mejorar la diversidad y la generalización. El objetivo es práctico: en una tarea real de predicción de la estabilidad de la red eléctrica, comparamos una referencia clásica sólida con una alternativa optimizada cuánticamente utilizando las mismas divisiones de datos, para que puedas ver en qué aspectos ayuda el enfoque cuántico y qué costes conlleva.\n",
        "\n",
        "Por qué es importante: seleccionar un buen subconjunto entre muchos aprendices débiles es un problema combinatorio que crece rápidamente con el tamaño del conjunto. Las heurísticas clásicas como boosting, bagging y stacking funcionan bien a escalas moderadas, pero pueden tener dificultades para explorar eficientemente bibliotecas de modelos grandes y redundantes. La función integra algoritmos cuánticos -concretamente QAOA (y opcionalmente VQE en otras configuraciones)- para buscar en ese espacio de forma más eficaz una vez entrenados los aprendices clásicos, lo que aumenta la probabilidad de encontrar un subconjunto compacto y diverso que generalice mejor.\n",
        "\n",
        "Lo más importante es que la escala de datos no está limitada por los qubits. El trabajo pesado con los datos -preprocesamiento, formación del conjunto de alumnos y evaluación- sigue siendo clásico y puede manejar millones de ejemplos. Los Qubits sólo determinan el tamaño del conjunto utilizado en el paso de selección cuántica. Este desacoplamiento es lo que hace que el enfoque sea viable en el hardware actual: se mantienen los flujos de trabajo familiares de scikit-learn para el entrenamiento de datos y modelos, mientras que se llama al paso cuántico a través de una interfaz de acción limpia en Qiskit Functions.\n",
        "\n",
        "En la práctica, aunque se pueden proporcionar diferentes tipos de aprendices al conjunto (por ejemplo, árboles de decisión, regresión logística o k-NN), los árboles de decisión tienden a funcionar mejor. El optimizador favorece sistemáticamente a los miembros más fuertes del conjunto: cuando se suministran aprendices heterogéneos, los modelos más débiles, como los regresores lineales, suelen ser eliminados en favor de los más expresivos, como los árboles de decisión.\n",
        "\n",
        "Lo que harás aquí: preparar y equilibrar el conjunto de datos de estabilidad de la red; establecer una línea de base clásica AdaBoost; ejecutar varias configuraciones cuánticas que varían el ancho del conjunto y la regularización; ejecutar en simuladores IBM® o QPUs a través de Qiskit Serverless; y comparar la exactitud, precisión, recuperación y F1 a través de todas las ejecuciones. Por el camino, utilizará el patrón de acción de la función (`create`, `fit`, `predict`, `fit_predict`, `create_fit_predict`) y los controles de teclas:\n",
        "\n",
        "* Tipos de regularización: `onsite` (λ) para la dispersión directa y `alpha` para una compensación basada en la proporción entre los términos de interacción y los in situ\n",
        "* Autorregularización: establecer `regularization=\"auto\"` con un ratio de selección objetivo para adaptar la dispersión automáticamente\n",
        "* Opciones del optimizador: simulador frente a QPU, repeticiones, optimizador clásico y sus opciones, profundidad de transpilación y configuración del muestreador o estimador de « Qiskit Runtime »\n",
        "\n",
        "Las pruebas de la documentación muestran que la precisión mejora a medida que aumenta el número de aprendices (qubits) en problemas difíciles, y que el clasificador cuántico iguala o supera a un conjunto clásico comparable. En este tutorial, reproducirá el flujo de trabajo de principio a fin y examinará si el aumento de la amplitud del conjunto o el cambio a la regularización adaptativa produce mejores resultados en F1 con un uso razonable de los recursos. El resultado es una visión fundamentada de cómo un paso de optimización cuántica puede complementar, en lugar de sustituir, el aprendizaje clásico por conjuntos en aplicaciones reales.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de comenzar este tutorial, asegúrese de tener instalados los siguientes paquetes en su entorno Python :\n",
        "\n",
        "* `qiskit[visualization]~=2.1.0`\n",
        "* `qiskit-serverless~=0.24.0`\n",
        "* `qiskit-ibm-runtime v0.40.1`\n",
        "* `qiskit-ibm-catalog~=0.8.0`\n",
        "* `scikit-learn==1.5.2`\n",
        "* `pandas>=2.0.0,<3.0.0`\n",
        "* `imbalanced-learn~=0.12.3`\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuración\n",
        "\n",
        "En esta sección, inicializamos el cliente Qiskit Serverless y cargamos la función Singularity Machine Learning - Classification proporcionada por Multiverse Computing.\n",
        "Con Qiskit Serverless, puede ejecutar flujos de trabajo híbridos cuántico-clásicos en la infraestructura de nube gestionada de IBM sin preocuparse de la gestión de recursos.\n",
        "Necesitará una clave API de IBM Quantum Platform y su nombre de recurso en la nube (CRN) para autenticarse y acceder a Qiskit Functions.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d4fe7ee1-21ce-445c-b151-598cd4cf9227",
      "metadata": {},
      "source": [
        "<span id=\"download-the-dataset\" />\n",
        "\n",
        "### Descargar el conjunto de datos\n",
        "\n",
        "Para ejecutar este tutorial, utilizamos un **conjunto de datos de clasificación de la estabilidad de la red** preprocesado que contiene lecturas de sensores del sistema eléctrico etiquetadas.\n",
        "La siguiente celda crea automáticamente la estructura de carpetas necesaria y descarga los archivos de formación y de prueba directamente en su entorno mediante `wget`.\n",
        "Si ya tiene estos archivos localmente, este paso los sobrescribirá de forma segura para garantizar la coherencia de la versión.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "7a32efb3-a425-4c02-804b-65029ecffb6f",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "data_tutorial/grid_ 100%[===================>] 612.94K  --.-KB/s    in 0.01s   \n",
            "data_tutorial/grid_ 100%[===================>] 108.19K  --.-KB/s    in 0.006s  \n",
            "Dataset files downloaded:\n",
            "-rw-r--r-- 1 coder coder 109K Nov  8 18:50 data_tutorial/grid_stability/test.csv\n",
            "-rw-r--r-- 1 coder coder 613K Nov  8 18:50 data_tutorial/grid_stability/train.csv\n"
          ]
        }
      ],
      "source": [
        "## Download dataset for Grid Stability Classification\n",
        "\n",
        "# Create data directory if it doesn't exist\n",
        "!mkdir -p data_tutorial/grid_stability\n",
        "\n",
        "# Download the training and test sets from the official Qiskit documentation repo\n",
        "base_url = (\n",
        "    \"https://raw.githubusercontent.com/Qiskit/documentation/\"\n",
        "    \"main/datasets/tutorials/grid_stability\"\n",
        ")\n",
        "!wget -q --show-progress -O data_tutorial/grid_stability/train.csv {base_url}/train.csv\n",
        "\n",
        "!wget -q --show-progress -O data_tutorial/grid_stability/test.csv {base_url}/test.csv\n",
        "\n",
        "# Check the files have been downloaded\n",
        "!echo \"Dataset files downloaded:\"\n",
        "!ls -lh data_tutorial/grid_stability/*.csv"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bb9aa84f-ab37-412c-b056-7043b73380fa",
      "metadata": {},
      "source": [
        "<span id=\"import-required-packages\" />\n",
        "\n",
        "### Importar los paquetes necesarios\n",
        "\n",
        "En esta sección, importamos todos los paquetes Python y módulos Qiskit utilizados a lo largo del tutorial.\n",
        "Entre ellas se incluyen bibliotecas científicas básicas para el tratamiento de datos y la evaluación de modelos -como `NumPy`, `pandas` y `scikit-learn` - junto con herramientas de visualización y componentes Qiskit para ejecutar el modelo cuántico mejorado.\n",
        "También importamos `QiskitRuntimeService` y `QiskitFunctionsCatalog` para conectar con los servicios IBM Quantum® y acceder a la función Singularity Machine Learning.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f8c654f5-8355-4f67-b79d-c2b1c29ccc62",
      "metadata": {},
      "outputs": [],
      "source": [
        "from pprint import pprint\n",
        "from typing import Tuple\n",
        "import warnings\n",
        "\n",
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "from imblearn.over_sampling import RandomOverSampler\n",
        "from qiskit_ibm_catalog import QiskitFunctionsCatalog\n",
        "from sklearn.ensemble import AdaBoostClassifier\n",
        "from sklearn.metrics import (\n",
        "    accuracy_score,\n",
        "    f1_score,\n",
        "    precision_score,\n",
        "    recall_score,\n",
        ")\n",
        "from sklearn.model_selection import train_test_split\n",
        "\n",
        "warnings.filterwarnings(\"ignore\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3b389b34-02e8-4a63-ae24-0bf348647b48",
      "metadata": {},
      "source": [
        "<span id=\"set-constant-variables\" />\n",
        "\n",
        "### Establecer variables constantes\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "a68bf7df-914c-4b2a-827f-657578503750",
      "metadata": {},
      "outputs": [],
      "source": [
        "IBM_TOKEN = \"\"\n",
        "IBM_INSTANCE_TEST = \"\"\n",
        "IBM_INSTANCE_QUANTUM = \"\"\n",
        "FUNCTION_NAME = \"multiverse/singularity\"\n",
        "RANDOM_STATE: int = 123\n",
        "TRAIN_PATH = \"data_tutorial/grid_stability/train.csv\"\n",
        "TEST_PATH = \"data_tutorial/grid_stability/test.csv\""
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4709dc1c-b380-49f1-95c7-89197aa5e147",
      "metadata": {},
      "source": [
        "<span id=\"connect-to-ibm-quantum-and-load-the-singularity-function\" />\n",
        "\n",
        "### Conéctese a IBM Quantum y cargue la función Singularity\n",
        "\n",
        "A continuación, nos autenticamos con los servicios de IBM Quantum y cargamos la función Singularity Machine Learning - Classification de Qiskit Functions Catalog.\n",
        "`QiskitRuntimeService` establece una conexión segura con IBM Quantum Platform utilizando su token de API y su CRN de instancia, lo que permite el acceso a los backends cuánticos.\n",
        "El `QiskitFunctionsCatalog` se utiliza entonces para recuperar la función Singularidad por su nombre (`\"multiverse/singularity\"`), lo que nos permite llamarla más tarde para el cálculo híbrido cuántico-clásico.\n",
        "Si la configuración se realiza correctamente, aparecerá un mensaje de confirmación indicando que la función se ha cargado correctamente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "bc380c46",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Successfully connected to IBM Qiskit Serverless and loaded the Singularity function.\n",
            "Catalog: <QiskitFunctionsCatalog>\n",
            "Singularity function: QiskitFunction(multiverse/singularity)\n"
          ]
        }
      ],
      "source": [
        "catalog = QiskitFunctionsCatalog(\n",
        "    token=IBM_TOKEN,\n",
        "    instance=IBM_INSTANCE_TEST,\n",
        "    channel=\"ibm_quantum_platform\",\n",
        ")\n",
        "backend = catalog.least_busy()\n",
        "\n",
        "singularity = catalog.load(FUNCTION_NAME)\n",
        "print(\n",
        "    \"Successfully connected to IBM Qiskit Serverless and \"\n",
        "    \"loaded the Singularity function.\"\n",
        ")\n",
        "print(\"Catalog:\", catalog)\n",
        "print(\"Singularity function:\", singularity)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "47d6a559-118a-4aa9-874d-9c009b5da60c",
      "metadata": {},
      "source": [
        "<span id=\"define-helper-functions\" />\n",
        "\n",
        "### Definir funciones auxiliares\n",
        "\n",
        "Antes de ejecutar los experimentos principales, definimos algunas pequeñas funciones de utilidad que agilizan la carga de datos y la evaluación de modelos.\n",
        "\n",
        "* `load_data()` lee los archivos CSV de entrada en matrices NumPy, dividiendo las características y las etiquetas para que sean compatibles con `scikit-learn` y los flujos de trabajo cuánticos.\n",
        "* `evaluate_predictions()` calcula las principales métricas de rendimiento (exactitud, precisión, recuperación y F1-score ) y, opcionalmente, informa del tiempo de ejecución si se proporciona información sobre los tiempos.\n",
        "\n",
        "Estas funciones de ayuda simplifican las operaciones repetidas más adelante en el cuaderno y garantizan un informe métrico coherente en los clasificadores clásicos y cuánticos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "46bc841e-7365-4508-b6bf-ae57db6050e1",
      "metadata": {},
      "outputs": [],
      "source": [
        "def load_data(data_path: str) -> Tuple[np.ndarray, np.ndarray]:\n",
        "    \"\"\"Load data from the given path to X and y arrays.\"\"\"\n",
        "    df: pd.DataFrame = pd.read_csv(data_path)\n",
        "    return df.iloc[:, :-1].values, df.iloc[:, -1].values\n",
        "\n",
        "\n",
        "def evaluate_predictions(predictions, y_true):\n",
        "    \"\"\"Compute and print accuracy, precision, recall, and F1 score.\"\"\"\n",
        "    accuracy = accuracy_score(y_true, predictions)\n",
        "    precision = precision_score(y_true, predictions)\n",
        "    recall = recall_score(y_true, predictions)\n",
        "    f1 = f1_score(y_true, predictions)\n",
        "    print(\"Accuracy:\", accuracy)\n",
        "    print(\"Precision:\", precision)\n",
        "    print(\"Recall:\", recall)\n",
        "    print(\"F1:\", f1)\n",
        "    return accuracy, precision, recall, f1"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "988ee237",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "## Paso 1: Asignar entradas clásicas a un problema cuántico\n",
        "\n",
        "Comenzamos preparando el conjunto de datos para la experimentación híbrida cuántica-clásica. El objetivo de este paso es convertir los datos brutos de estabilidad de la red en divisiones equilibradas de entrenamiento, validación y prueba que puedan ser utilizadas de forma coherente tanto por los flujos de trabajo clásicos como por los cuánticos. Mantener divisiones idénticas garantiza que las comparaciones de rendimiento posteriores sean justas y reproducibles.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1c084cde-a5cf-4661-a00c-aa243c8b0e44",
      "metadata": {},
      "source": [
        "<span id=\"data-loading-and-preprocessing\" />\n",
        "\n",
        "### Carga y preprocesamiento de datos\n",
        "\n",
        "Primero cargamos los archivos CSV de entrenamiento y prueba, creamos una división de validación y equilibramos el conjunto de datos mediante sobremuestreo aleatorio. El equilibrio evita el sesgo hacia la clase mayoritaria y proporciona una señal de aprendizaje más estable tanto para los modelos de conjuntos clásicos como para los cuánticos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "0db0e914-a8f2-4a04-bec7-c15bac8104b4",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Shapes:\n",
            "  X_train_bal: (5104, 12)\n",
            "  y_train_bal: (5104,)\n",
            "  X_val: (850, 12)\n",
            "  y_val: (850,)\n",
            "  X_test: (750, 12)\n",
            "  y_test: (750,)\n"
          ]
        }
      ],
      "source": [
        "# Load and upload the data\n",
        "X_train, y_train = load_data(TRAIN_PATH)\n",
        "X_test, y_test = load_data(TEST_PATH)\n",
        "X_train, X_val, y_train, y_val = train_test_split(\n",
        "    X_train, y_train, test_size=0.2, random_state=RANDOM_STATE\n",
        ")\n",
        "\n",
        "# Balance the dataset through over-sampling of the positive class\n",
        "ros = RandomOverSampler(random_state=RANDOM_STATE)\n",
        "X_train_bal, y_train_bal = ros.fit_resample(X_train, y_train)\n",
        "\n",
        "print(\"Shapes:\")\n",
        "print(\"  X_train_bal:\", X_train_bal.shape)\n",
        "print(\"  y_train_bal:\", y_train_bal.shape)\n",
        "print(\"  X_val:\", X_val.shape)\n",
        "print(\"  y_val:\", y_val.shape)\n",
        "print(\"  X_test:\", X_test.shape)\n",
        "print(\"  y_test:\", y_test.shape)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b8c214fd-09a8-4fa5-ab83-b0c71ad615d4",
      "metadata": {},
      "source": [
        "<span id=\"classical-baseline-adaboost-reference\" />\n",
        "\n",
        "### Referencia clásica: AdaBoost reference\n",
        "\n",
        "Antes de ejecutar cualquier optimización cuántica, entrenamos una línea de base clásica fuerte -un clasificador estándar AdaBoost - en los mismos datos equilibrados. Esto proporciona un punto de referencia reproducible para posteriores comparaciones, ayudando a cuantificar si la optimización cuántica mejora la generalización o la eficiencia más allá de un conjunto clásico bien afinado.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "38ecd608-35b4-4e58-8a16-85ef98bf024a",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Classical AdaBoost baseline:\n",
            "Accuracy: 0.7893333333333333\n",
            "Precision: 1.0\n",
            "Recall: 0.7893333333333333\n",
            "F1: 0.8822652757078987\n"
          ]
        }
      ],
      "source": [
        "# ----- Classical baseline: AdaBoost -----\n",
        "baseline = AdaBoostClassifier(n_estimators=60, random_state=RANDOM_STATE)\n",
        "baseline.fit(X_train_bal, y_train_bal)\n",
        "baseline_pred = baseline.predict(X_test)\n",
        "print(\"Classical AdaBoost baseline:\")\n",
        "_ = evaluate_predictions(baseline_pred, y_test)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ac6f36e3",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-hardware-execution\" />\n",
        "\n",
        "## Paso 2: Optimizar el problema para la ejecución en hardware cuántico\n",
        "\n",
        "La tarea de selección de conjuntos se plantea como un problema de optimización combinatoria en el que cada clasificador débil es una variable de decisión binaria, y el objetivo busca el equilibrio entre la precisión y la dispersión mediante un término de regularización. El sistema resuelve `QuantumEnhancedEnsembleClassifier` este problema mediante QAOA en hardware de tipo « IBM », sin dejar de permitir la exploración basada en simuladores. El control `optimizer_options` del bucle híbrido: `simulator=False` dirige los circuitos hacia la QPU seleccionada, `num_solutions` aumenta la amplitud de búsqueda y ( `classical_optimizer_options` en el caso del optimizador clásico interno) regula la convergencia; unos valores en torno a las 60 iteraciones ofrecen un buen equilibrio entre calidad y tiempo de ejecución. Las opciones de tiempo de ejecución —como una profundidad de circuito moderada (`reps`) y un esfuerzo de transpilación estándar— contribuyen a garantizar un rendimiento sólido en todos los dispositivos. La configuración que se muestra a continuación es el perfil «best-results» que utilizaremos para las ejecuciones en hardware; también puedes crear una variante puramente simulada activando la opción `simulator=True` para ejecutar el flujo de trabajo en modo de simulación sin consumir tiempo de la QPU.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "18c0d3f9-691b-449d-83ca-e6bfce2d6b99",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Configured hardware optimization profile:\n",
            "  simulator: False\n",
            "  num_solutions: 100000\n",
            "  reps: 3\n",
            "  optimization_level: 3\n",
            "  num_transpiler_runs: 30\n",
            "  classical_optimizer: COBYLA\n",
            "  classical_optimizer_options: {'maxiter': 60}\n",
            "  estimator_options: None\n",
            "  sampler_options: None\n"
          ]
        }
      ],
      "source": [
        "# QAOA / runtime configuration for best results on hardware\n",
        "optimizer_options = {\n",
        "    \"simulator\": False,  # set True to test locally without QPU\n",
        "    \"num_solutions\": 100_000,  # broaden search over candidate ensembles\n",
        "    \"reps\": 3,  # QAOA depth (circuit layers)\n",
        "    \"optimization_level\": 3,  # transpilation effort\n",
        "    \"num_transpiler_runs\": 30,  # explore multiple layouts\n",
        "    \"classical_optimizer\": \"COBYLA\",  # robust default for this landscape\n",
        "    \"classical_optimizer_options\": {\n",
        "        \"maxiter\": 60  # practical convergence budget\n",
        "    },\n",
        "    # You can pass backend-specific options;\n",
        "    # leaving None uses least-busy routing\n",
        "    \"estimator_options\": None,\n",
        "    \"sampler_options\": None,\n",
        "}\n",
        "\n",
        "print(\"Configured hardware optimization profile:\")\n",
        "for key, value in optimizer_options.items():\n",
        "    print(f\"  {key}: {value}\")"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "b4d480b3",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "## Paso 3: Ejecutar utilizando Qiskit primitives\n",
        "\n",
        "Ahora ejecutamos el flujo de trabajo completo utilizando la acción `create_fit_predict` de la función Singularity para entrenar, optimizar y evaluar el `QuantumEnhancedEnsembleClassifier` de extremo a extremo en la infraestructura IBM. La función construye el conjunto, aplica la optimización cuántica a través de Qiskit primitives, y devuelve tanto las predicciones como los metadatos del trabajo (incluido el tiempo de ejecución y el uso de recursos). La división de datos clásica del Paso 1 se reutiliza para la reproducibilidad, con los datos de validación pasados a través de `fit_params` para que la optimización pueda ajustar los hiperparámetros internamente mientras se mantiene intacto el conjunto de prueba retenido.\n",
        "\n",
        "En este paso, exploramos varias configuraciones del conjunto cuántico para comprender cómo afectan los parámetros clave -específicamente `num_learners` y `regularization` - tanto a la calidad de los resultados como al uso de la QPU.\n",
        "\n",
        "* `num_learners` determina la anchura del conjunto (e implícitamente, el número de qubits), lo que influye en la capacidad y el coste computacional del modelo.\n",
        "* `regularization` controla la dispersión y el sobreajuste, determinando cuántos aprendices permanecen activos tras la optimización.\n",
        "\n",
        "Variando estos parámetros, podemos ver cómo interactúan la anchura del conjunto y la regularización: aumentar la anchura suele mejorar F1 pero cuesta más tiempo de QPU, mientras que una regularización más fuerte o adaptativa puede mejorar la generalización con aproximadamente la misma huella de hardware. Las siguientes subsecciones recorren tres configuraciones representativas para ilustrar estos efectos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8da41d4d-f06d-4b67-a8ee-b07fc0289558",
      "metadata": {},
      "source": [
        "<span id=\"baseline\" />\n",
        "\n",
        "### Línea base\n",
        "\n",
        "Esta configuración utiliza `num_learners = 10` y `regularization = 7`.\n",
        "\n",
        "* `num_learners` controla la anchura del conjunto, es decir, el número de aprendices débiles combinados y, en el hardware cuántico, el **número de qubits necesarios**. Un valor mayor amplía el espacio de búsqueda combinatoria y puede mejorar la precisión y la recuperación, pero también aumenta la anchura del circuito, el tiempo de compilación y el uso general de la QPU.\n",
        "* `regularization` establece la intensidad de la penalización por incluir alumnos adicionales. Con la regularización \"in situ\" por defecto, los valores más altos imponen una mayor dispersión (se mantienen menos aprendices), mientras que los valores más bajos permiten conjuntos más complejos.\n",
        "\n",
        "Esta configuración proporciona una línea de base de bajo coste, mostrando cómo se comporta un conjunto pequeño antes de escalar la anchura o ajustar la dispersión.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "e702c986-fd7f-4ea1-a93c-8cd5ef0c4a0b",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Problem scale and regularization\n",
        "NUM_LEARNERS = 10\n",
        "REGULARIZATION = 7"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "06c261c1-a3c1-42c9-a522-63f3fc01a970",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "-- Submitting quantum-enhanced ensemble job --\n",
            "Action status: ok\n",
            "Action message: Classifier created, fitted, and predicted.\n",
            "Metadata:\n",
            "{'resource_usage': {'RUNNING: EXECUTING_QPU': {'QPU_TIME': 1550.1889700889587},\n",
            "                    'RUNNING: MAPPING': {'CPU_TIME': 267.05158376693726},\n",
            "                    'RUNNING: POST_PROCESSING': {'CPU_TIME': 152.4274561405182},\n",
            "                    'RUNNING: WAITING_QPU': {'CPU_TIME': 3336.8785166740417}}}\n",
            "Accuracy: 0.868\n",
            "Precision: 1.0\n",
            "Recall: 0.868\n",
            "F1: 0.9293361884368309\n"
          ]
        }
      ],
      "source": [
        "# ----- Quantum-enhanced ensemble on IBM hardware -----\n",
        "print(\"\\n-- Submitting quantum-enhanced ensemble job --\")\n",
        "job_1 = singularity.run(\n",
        "    action=\"create_fit_predict\",\n",
        "    name=\"grid_stability_qeec\",\n",
        "    quantum_classifier=\"QuantumEnhancedEnsembleClassifier\",\n",
        "    num_learners=NUM_LEARNERS,\n",
        "    regularization=REGULARIZATION,\n",
        "    optimizer_options=optimizer_options,  # from Step 2\n",
        "    backend_name=backend,  # least-busy compatible backend\n",
        "    instance=IBM_INSTANCE_QUANTUM,\n",
        "    random_state=RANDOM_STATE,\n",
        "    X_train=X_train_bal,\n",
        "    y_train=y_train_bal,\n",
        "    X_test=X_test,\n",
        "    fit_params={\"validation_data\": (X_val, y_val)},\n",
        "    options={\"save\": False},\n",
        ")\n",
        "result_1 = job_1.result()\n",
        "print(\"Action status:\", result_1.get(\"status\"))\n",
        "print(\"Action message:\", result_1.get(\"message\"))\n",
        "print(\"Metadata:\")\n",
        "pprint(result_1.get(\"metadata\"))\n",
        "qeec_pred_job_1 = np.array(result_1[\"data\"][\"predictions\"])\n",
        "_ = evaluate_predictions(qeec_pred_job_1, y_test)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "1bf0a3ae-b5b8-43d7-b72d-5a9cf6c61060",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "Quantum job status: DONE\n"
          ]
        }
      ],
      "source": [
        "status_1 = job_1.status()\n",
        "print(\"\\nQuantum job status:\", status_1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ae62b414-db77-45aa-bf2c-a8537fd7eba0",
      "metadata": {},
      "source": [
        "<span id=\"increase-the-number-of-learners\" />\n",
        "\n",
        "### Aumentar el número de alumnos\n",
        "\n",
        "Aquí aumentamos `num_learners` de 10 → 30 manteniendo `regularization = 7`.\n",
        "\n",
        "* Un mayor número de aprendices amplía el espacio de hipótesis, lo que permite al modelo captar patrones más sutiles, que pueden elevar modestamente F1.\n",
        "* En la mayoría de los casos, la diferencia de tiempo de ejecución entre 10 y 30 aprendices no es sustancial, lo que indica que la anchura de circuito añadida no aumenta significativamente el coste de ejecución.\n",
        "* La mejora de la calidad sigue una *curva de rendimiento decreciente* : las primeras ganancias aparecen a medida que crece el conjunto, pero se estabilizan a medida que los alumnos adicionales aportan menos información nueva.\n",
        "\n",
        "Este experimento pone de relieve la relación entre calidad y eficiencia: aumentar la anchura del conjunto puede ofrecer pequeñas mejoras de precisión sin una penalización importante en el tiempo de ejecución, dependiendo de las condiciones del backend y de la transpilación.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "e5fe64b0-2713-4b65-b768-10fa5d8dbf79",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Problem scale and regularization\n",
        "NUM_LEARNERS = 30\n",
        "REGULARIZATION = 7"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "18a22fed-41c2-4407-88d4-02fbd32f3320",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "-- Submitting quantum-enhanced ensemble job --\n",
            "Action status: ok\n",
            "Action message: Classifier created, fitted, and predicted.\n",
            "QPU Time:\n",
            "{'resource_usage': {'RUNNING: EXECUTING_QPU': {'QPU_TIME': 1095.822762966156},\n",
            "                    'RUNNING: MAPPING': {'CPU_TIME': 680.2116754055023},\n",
            "                    'RUNNING: POST_PROCESSING': {'CPU_TIME': 154.4466371536255},\n",
            "                    'RUNNING: WAITING_QPU': {'CPU_TIME': 80.80395102500916}}}\n",
            "Accuracy: 0.8946666666666667\n",
            "Precision: 1.0\n",
            "Recall: 0.8946666666666667\n",
            "F1: 0.944405348346235\n"
          ]
        }
      ],
      "source": [
        "# ----- Quantum-enhanced ensemble on IBM hardware -----\n",
        "print(\"\\n-- Submitting quantum-enhanced ensemble job --\")\n",
        "job_2 = singularity.run(\n",
        "    action=\"create_fit_predict\",\n",
        "    name=\"grid_stability_qeec\",\n",
        "    quantum_classifier=\"QuantumEnhancedEnsembleClassifier\",\n",
        "    num_learners=NUM_LEARNERS,\n",
        "    regularization=REGULARIZATION,\n",
        "    optimizer_options=optimizer_options,  # from Step 2\n",
        "    backend_name=backend,  # least-busy compatible backend\n",
        "    instance=IBM_INSTANCE_QUANTUM,\n",
        "    random_state=RANDOM_STATE,\n",
        "    X_train=X_train_bal,\n",
        "    y_train=y_train_bal,\n",
        "    X_test=X_test,\n",
        "    fit_params={\"validation_data\": (X_val, y_val)},\n",
        "    options={\"save\": False},\n",
        ")\n",
        "result_2 = job_2.result()\n",
        "print(\"Action status:\", result_2.get(\"status\"))\n",
        "print(\"Action message:\", result_2.get(\"message\"))\n",
        "print(\"QPU Time:\")\n",
        "pprint(result_2.get(\"metadata\"))\n",
        "qeec_pred_job_2 = np.array(result_2[\"data\"][\"predictions\"])\n",
        "_ = evaluate_predictions(qeec_pred_job_2, y_test)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "edd22b98-7ae6-4444-8fe5-7279e9233c57",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "Quantum job status: DONE\n"
          ]
        }
      ],
      "source": [
        "status_2 = job_2.status()\n",
        "print(\"\\nQuantum job status:\", status_2)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5e94784a-3599-4911-ade9-4792b50c31bb",
      "metadata": {},
      "source": [
        "<span id=\"regularization\" />\n",
        "\n",
        "### Regularización\n",
        "\n",
        "En esta configuración, aumentamos a `num_learners = 60` e introducimos la regularización adaptativa para gestionar la dispersión de forma más intuitiva.\n",
        "\n",
        "* Con `regularization = \"auto\"`, el optimizador encuentra automáticamente una fuerza de regularización adecuada que selecciona aproximadamente `regularization_ratio * num_learners` aprendices débiles para el conjunto final, en lugar de fijar la penalización manualmente. Esto proporciona una interfaz más cómoda para gestionar el equilibrio entre la dispersión y el tamaño del conjunto.\n",
        "* `regularization_type = \"alpha\"` define cómo se aplica la sanción. A diferencia de `onsite`, que no está acotado `[0, ∞]`, `alpha` está acotado entre `[0, 1]`, lo que facilita su ajuste e interpretación. El parámetro controla el equilibrio entre las penalizaciones individuales y por pareja, ofreciendo un rango de configuración más suave.\n",
        "* `regularization_desired_ratio ≈ 0.82` especifica la proporción objetivo de alumnos que se mantendrán activos después de la regularización; en este caso, se retiene alrededor del 82% de los alumnos, recortando automáticamente el 18% más débil.\n",
        "\n",
        "Aunque la regularización adaptativa simplifica la configuración y ayuda a mantener un conjunto equilibrado, no garantiza necesariamente un rendimiento mejor o más estable. La calidad real depende de la selección de un parámetro de regularización adecuado, y su ajuste mediante validación cruzada puede ser costoso desde el punto de vista informático. La principal ventaja reside en una mayor facilidad de uso e interpretación, más que en un aumento directo de la precisión.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "209eb51a-e44b-4f94-8975-269ec7e3d1ca",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Problem scale and regularization\n",
        "NUM_LEARNERS = 60\n",
        "REGULARIZATION = \"auto\"\n",
        "REGULARIZATION_TYPE = \"alpha\"\n",
        "REGULARIZATION_RATIO = 0.82"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "eb2aa9db-4427-48de-aae0-c5c1da1cb998",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "-- Submitting quantum-enhanced ensemble job --\n",
            "Action status: ok\n",
            "Action message: Classifier created, fitted, and predicted.\n",
            "Metadata:\n",
            "{'resource_usage': {'RUNNING: EXECUTING_QPU': {'QPU_TIME': 1146.5584812164307},\n",
            "                    'RUNNING: MAPPING': {'CPU_TIME': 1387.7451872825623},\n",
            "                    'RUNNING: POST_PROCESSING': {'CPU_TIME': 171.78878355026245},\n",
            "                    'RUNNING: WAITING_QPU': {'CPU_TIME': 95.41597843170166}}}\n",
            "Accuracy: 0.908\n",
            "Precision: 1.0\n",
            "Recall: 0.908\n",
            "F1: 0.9517819706498952\n"
          ]
        }
      ],
      "source": [
        "# ----- Quantum-enhanced ensemble on IBM hardware -----\n",
        "print(\"\\n-- Submitting quantum-enhanced ensemble job --\")\n",
        "job_3 = singularity.run(\n",
        "    action=\"create_fit_predict\",\n",
        "    name=\"grid_stability_qeec\",\n",
        "    quantum_classifier=\"QuantumEnhancedEnsembleClassifier\",\n",
        "    num_learners=NUM_LEARNERS,\n",
        "    regularization=REGULARIZATION,\n",
        "    regularization_type=REGULARIZATION_TYPE,\n",
        "    regularization_desired_ratio=REGULARIZATION_RATIO,\n",
        "    optimizer_options=optimizer_options,  # from Step 2\n",
        "    backend_name=backend,  # least-busy compatible backend\n",
        "    instance=IBM_INSTANCE_QUANTUM,\n",
        "    random_state=RANDOM_STATE,\n",
        "    X_train=X_train_bal,\n",
        "    y_train=y_train_bal,\n",
        "    X_test=X_test,\n",
        "    fit_params={\"validation_data\": (X_val, y_val)},\n",
        "    options={\"save\": False},\n",
        ")\n",
        "result_3 = job_3.result()\n",
        "print(\"Action status:\", result_3.get(\"status\"))\n",
        "print(\"Action message:\", result_3.get(\"message\"))\n",
        "print(\"Metadata:\")\n",
        "pprint(result_3.get(\"metadata\"))\n",
        "qeec_pred_job_3 = np.array(result_3[\"data\"][\"predictions\"])\n",
        "_ = evaluate_predictions(qeec_pred_job_3, y_test)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "299b36cb-0ed8-4af3-b431-a87daec04c93",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "Quantum job status: DONE\n"
          ]
        }
      ],
      "source": [
        "status_3 = job_3.status()\n",
        "print(\"\\nQuantum job status:\", status_3)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "50b94af2",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "## Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado\n",
        "\n",
        "A continuación, postprocesamos los resultados de las ejecuciones clásica y cuántica, convirtiéndolos a un formato coherente para su evaluación posterior. Este paso compara la calidad predictiva utilizando métricas estándar -exactitud, precisión, recuperación y F1 - y analiza cómo la amplitud del conjunto (`num_learners`) y el control de la dispersión (`regularization`) influyen tanto en el rendimiento como en el comportamiento computacional.\n",
        "\n",
        "La línea de base clásica AdaBoost proporciona una referencia compacta y estable para el aprendizaje a pequeña escala. Funciona bien con conjuntos limitados y una sobrecarga computacional insignificante, lo que refleja la fuerza del boosting tradicional cuando el espacio de hipótesis sigue siendo manejable. Las configuraciones cuánticas (`qeec_pred_job_1`, `qeec_pred_job_2`, y `qeec_pred_job_3`) amplían esta línea de base incrustando el proceso de selección de conjuntos dentro de un bucle de optimización cuántica variacional. Esto permite al sistema explorar simultáneamente subconjuntos exponencialmente grandes de alumnos en superposición, abordando la naturaleza combinatoria de la selección de conjuntos de forma más eficiente a medida que aumenta la escala.\n",
        "\n",
        "Los resultados muestran que aumentar `num_learners` de 10 a 30 mejora la recuperación y F1, lo que confirma que un conjunto más amplio capta interacciones más ricas entre aprendices débiles. La ganancia es sublineal en el hardware actual -cada aprendiz adicional produce incrementos de precisión más pequeños-, pero el comportamiento de escalado subyacente sigue siendo favorable porque el optimizador cuántico puede buscar espacios de configuración más amplios sin la explosión exponencial típica de la selección clásica de subconjuntos. La regularización introduce un matiz adicional: una λ=7 fija impone una dispersión consistente y estabiliza la convergencia, mientras que la regularización α adaptativa ajusta automáticamente la dispersión basándose en las correlaciones entre los alumnos. Esta poda dinámica a menudo consigue una F1 ligeramente superior para la misma anchura de qubit, equilibrando la complejidad del modelo y la generalización.\n",
        "\n",
        "Cuando se compara directamente con la línea de base AdaBoost, la configuración cuántica más pequeña ( L=10 ) reproduce una precisión similar, validando la corrección de la tubería híbrida. Con anchuras mayores, las variantes cuánticas -especialmente con autorregularización- empiezan a superar modestamente la línea de base clásica, mostrando una mejora de la recuperación y F1 sin un crecimiento lineal del coste computacional. Estas mejoras no indican una \"ventaja cuántica\" inmediata, sino más bien **una eficacia de escalado** : el optimizador cuántico mantiene un rendimiento manejable a medida que se amplía el conjunto, mientras que un enfoque clásico se enfrentaría a un crecimiento exponencial de la complejidad de la selección de subconjuntos.\n",
        "\n",
        "En la práctica:\n",
        "\n",
        "* Utilice la **línea de base clásica** para la validación rápida y la evaluación comparativa en pequeños conjuntos de datos.\n",
        "* Aplique **conjuntos cuánticos** cuando aumente la amplitud del modelo o la complejidad de las características: la búsqueda basada en QAOA se adapta mejor a esos regímenes.\n",
        "* Emplear **la α-regularización adaptativa** para mantener la dispersión y la generalización sin aumentar la anchura del circuito.\n",
        "* Supervise el tiempo y la profundidad de la QPU para equilibrar las mejoras de calidad con las limitaciones de hardware a corto plazo.\n",
        "\n",
        "En conjunto, estos experimentos demuestran que los conjuntos optimizados cuánticamente complementan a los métodos clásicos: reproducen la precisión de referencia a escalas pequeñas y, al mismo tiempo, ofrecen una vía hacia el escalado eficiente en problemas de aprendizaje combinatorio de mayor envergadura. A medida que mejore el hardware, se espera que estas ventajas de escalado se agraven, ampliando el tamaño y la profundidad factibles de los modelos basados en conjuntos más allá de lo que resulta práctico clásicamente.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1cf1d610-f2e2-482c-af05-a882bb380528",
      "metadata": {},
      "source": [
        "<span id=\"evaluate-metrics-for-each-configuration\" />\n",
        "\n",
        "### Evaluar las métricas para cada configuración\n",
        "\n",
        "A continuación, evaluamos todas las configuraciones (la base clásica AdaBoost y los tres conjuntos cuánticos) con la ayuda de `evaluate_predictions` para calcular la exactitud, la precisión, la recuperación y F1 en el mismo conjunto de pruebas. Esta comparación aclara cómo se escala la optimización cuántica en relación con el enfoque clásico: con anchuras pequeñas, ambos funcionan de forma similar; a medida que los conjuntos crecen, el método cuántico puede explorar espacios de hipótesis más grandes de forma más eficiente. El cuadro resultante recoge estas tendencias de forma coherente y cuantitativa.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "ee8cc821-8311-4c84-8f94-ffb2ec3facc3",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Accuracy: 0.7893333333333333\n",
            "Precision: 1.0\n",
            "Recall: 0.7893333333333333\n",
            "F1: 0.8822652757078987\n",
            "Accuracy: 0.868\n",
            "Precision: 1.0\n",
            "Recall: 0.868\n",
            "F1: 0.9293361884368309\n",
            "Accuracy: 0.8946666666666667\n",
            "Precision: 1.0\n",
            "Recall: 0.8946666666666667\n",
            "F1: 0.944405348346235\n",
            "Accuracy: 0.908\n",
            "Precision: 1.0\n",
            "Recall: 0.908\n",
            "F1: 0.9517819706498952\n"
          ]
        },
        {
          "data": {
            "text/html": [
              "<div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>Config</th>\n",
              "      <th>Accuracy</th>\n",
              "      <th>Precision</th>\n",
              "      <th>Recall</th>\n",
              "      <th>F1</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>AdaBoost (Classical)</td>\n",
              "      <td>0.789333</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.789333</td>\n",
              "      <td>0.882265</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>QEEC L=10, reg=7</td>\n",
              "      <td>0.868000</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.868000</td>\n",
              "      <td>0.929336</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>QEEC L=30, reg=7</td>\n",
              "      <td>0.894667</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.894667</td>\n",
              "      <td>0.944405</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>QEEC L=60, reg=auto (α=0.82)</td>\n",
              "      <td>0.908000</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.908000</td>\n",
              "      <td>0.951782</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>"
            ],
            "text/plain": [
              "                         Config  Accuracy  Precision    Recall        F1\n",
              "0          AdaBoost (Classical)  0.789333        1.0  0.789333  0.882265\n",
              "1              QEEC L=10, reg=7  0.868000        1.0  0.868000  0.929336\n",
              "2              QEEC L=30, reg=7  0.894667        1.0  0.894667  0.944405\n",
              "3  QEEC L=60, reg=auto (α=0.82)  0.908000        1.0  0.908000  0.951782"
            ]
          },
          "execution_count": 18,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "results = []\n",
        "\n",
        "# Classical baseline\n",
        "acc_b, prec_b, rec_b, f1_b = evaluate_predictions(baseline_pred, y_test)\n",
        "results.append(\n",
        "    {\n",
        "        \"Config\": \"AdaBoost (Classical)\",\n",
        "        \"Accuracy\": acc_b,\n",
        "        \"Precision\": prec_b,\n",
        "        \"Recall\": rec_b,\n",
        "        \"F1\": f1_b,\n",
        "    }\n",
        ")\n",
        "\n",
        "# Quantum runs\n",
        "for label, preds in [\n",
        "    (\"QEEC L=10, reg=7\", qeec_pred_job_1),\n",
        "    (\"QEEC L=30, reg=7\", qeec_pred_job_2),\n",
        "    (f\"QEEC L=60, reg=auto (α={REGULARIZATION_RATIO})\", qeec_pred_job_3),\n",
        "]:\n",
        "    acc, prec, rec, f1 = evaluate_predictions(preds, y_test)\n",
        "    results.append(\n",
        "        {\n",
        "            \"Config\": label,\n",
        "            \"Accuracy\": acc,\n",
        "            \"Precision\": prec,\n",
        "            \"Recall\": rec,\n",
        "            \"F1\": f1,\n",
        "        }\n",
        "    )\n",
        "\n",
        "\n",
        "df_results = pd.DataFrame(results)\n",
        "df_results"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "704cecf6-2c87-4356-b9be-26f4346c194f",
      "metadata": {},
      "source": [
        "<span id=\"visualize-quality-trends-across-configurations\" />\n",
        "\n",
        "### Visualice las tendencias de calidad en todas las configuraciones\n",
        "\n",
        "El gráfico de barras agrupadas que aparece a continuación compara **la precisión** y la **F1** entre la línea de base clásica y los conjuntos cuánticos (`L=10`, `L=30`, y `L=60 auto-α`). Ilustra cómo la precisión se estabiliza mientras que F1 mejora gradualmente a medida que aumenta la anchura del conjunto cuántico, lo que demuestra que el método híbrido mantiene el escalado del rendimiento sin el crecimiento exponencial de los costes típico de la selección clásica de subconjuntos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "0f15c5fb-2450-4671-9bc2-471043414df2",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/sml-classification/extracted-outputs/0f15c5fb-2450-4671-9bc2-471043414df2-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "x = np.arange(len(df_results))\n",
        "width = 0.35\n",
        "plt.figure(figsize=(7.6, 4.6))\n",
        "plt.bar(x - width / 2, df_results[\"Accuracy\"], width=width, label=\"Accuracy\")\n",
        "plt.bar(x + width / 2, df_results[\"F1\"], width=width, label=\"F1\")\n",
        "plt.xticks(x, df_results[\"Config\"], rotation=10)\n",
        "plt.ylabel(\"Score\")\n",
        "plt.title(\"Classical vs Quantum ensemble performance\")\n",
        "plt.legend()\n",
        "plt.ylim(0, 1.0)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "745cd406-5166-4eb9-8506-0eedd71e9b79",
      "metadata": {},
      "source": [
        "<span id=\"interpretation\" />\n",
        "\n",
        "### Interpretación\n",
        "\n",
        "El gráfico confirma el patrón de escala esperado. El método clásico AdaBoost funciona muy bien con conjuntos pequeños, pero su escala es cada vez más costosa a medida que aumenta el número de aprendices débiles, porque su problema de selección de subconjuntos se expande combinatoriamente. Los modelos mejorados cuánticamente replican la precisión clásica a anchuras bajas y comienzan a superarla a medida que aumenta el tamaño del conjunto, especialmente bajo α-regularización adaptativa. Esto refleja la capacidad del optimizador cuántico para muestrear y evaluar muchos subconjuntos candidatos en paralelo mediante superposición, manteniendo una búsqueda manejable incluso con anchuras mayores. Aunque la sobrecarga actual del hardware compensa algunas de las ganancias teóricas, la tendencia ilustra la ventaja de eficiencia de escalado de la formulación cuántica. En términos prácticos, el método clásico sigue siendo preferible para los puntos de referencia ligeros, mientras que los conjuntos mejorados cuánticamente se vuelven ventajosos a medida que la dimensionalidad del modelo y el tamaño del conjunto aumentan, ofreciendo mejores compensaciones entre precisión, generalización y crecimiento computacional.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a27afb68-f959-4f4a-910c-0aadafb7e18e",
      "metadata": {},
      "source": [
        "<span id=\"appendix-scaling-benefits-and-enhancements\" />\n",
        "\n",
        "## Apéndice: Beneficios y mejoras de escalabilidad\n",
        "\n",
        "La ventaja de la escalabilidad de `QuantumEnhancedEnsembleClassifier` se debe a cómo el proceso de selección de conjuntos se corresponde con la optimización cuántica.\n",
        "Los métodos clásicos de aprendizaje por conjuntos, como AdaBoost o los bosques aleatorios, resultan costosos desde el punto de vista informático a medida que aumenta el número de aprendices débiles, ya que la selección del subconjunto óptimo es un problema combinatorio que se escala exponencialmente.\n",
        "\n",
        "En cambio, la formulación cuántica -aplicada aquí mediante el Algoritmo de Optimización Cuántica Aproximada (QAOA)- puede explorar estos espacios de búsqueda exponencialmente grandes de forma más eficiente evaluando múltiples configuraciones en superposición.\n",
        "Como resultado, el tiempo de entrenamiento no crece significativamente con el número de aprendices, lo que permite que el modelo siga siendo eficiente aunque aumente la amplitud del conjunto.\n",
        "\n",
        "Aunque el hardware actual introduce algunas limitaciones de ruido y profundidad, este flujo de trabajo demuestra un enfoque híbrido a corto plazo en el que los componentes clásicos y cuánticos cooperan: el optimizador cuántico proporciona un mejor paisaje de inicialización para el bucle clásico, mejorando la convergencia y la calidad del modelo final.\n",
        "A medida que evolucionen los procesadores cuánticos, se espera que estas ventajas de escalabilidad se extiendan a conjuntos de datos más grandes, conjuntos más amplios y circuitos más profundos.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ee41a301",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Referencias\n",
        "\n",
        "1. [Introducción a la Qiskit Functions](/docs/guides/functions)\n",
        "2. [Singularidad informática multiversal Machine Learning](/docs/guides/multiverse-computing-singularity)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2cb5785c",
      "metadata": {},
      "source": [
        "<span id=\"tutorial-survey\" />\n",
        "\n",
        "## Encuesta tutorial\n",
        "\n",
        "Tómese un minuto para comentar este tutorial. Su opinión nos ayudará a mejorar nuestra oferta de contenidos y la experiencia de los usuarios.\n",
        "\n",
        "[Enlace a la encuesta](https://your.feedback.ibm.com/jfe/form/SV_3BLFkNVEuh0QBWm)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    },
    "hours": 2,
    "qpuSeconds": 1200
  },
  "nbformat": 4,
  "nbformat_minor": 5
}