Autor: Fernando

  • Entorno híbrido (Ollama + DeepSeek) con LangChain.

    Por qué un entorno RAG híbrido (local + API)

    Este entorno se ha diseñado como base de trabajo para realizar prácticas del curso IBM RAG and Agentic AI, pero desvinculando la implementación de la plataforma watsonx de IBM.

    El objetivo no es replicar la herramienta, sino replicar la arquitectura. En el curso, muchos de los ejercicios se apoyan en servicios gestionados en el ecosistema de IBM, lo que facilita el aprendizaje inicial, pero introduce una fuerte dependencia del proveedor. Para un aprendizaje más profundo y aplicable, es necesario trasladar esos mismos conceptos a un entorno abierto y controlado.

    Entorno: Conda + Python

    Se utiliza Anaconda como gestor de entornos por varias razones:

    • aislamiento de dependencias (evita conflictos entre librerías)
    • compatibilidad con librerías de data science (NumPy, Pandas, sklearn)
    • control de versiones reproducible
    • integración natural con notebooks y workflows analíticos

    Framework: LangChain

    Se utiliza LangChain como capa de orquestación:

    • Abstrae el uso del LLM
    • Permite cambiar de modelo sin cambiar el pipeline
    • Integra: loaders, chunking, embeddings, vector stores, chains

    Uso de IA local: Ollama

    Se utiliza Ollama para ejecutar modelos en local.

    VentajasLimitacionesRol en el sistema
    Independencia total de internet
    Coste cero
    Privacidad de datos
    Control completo del entorno
    Rendimiento limitado por hardware
    Modelos más pequeños
    Menor precisión en tareas complejas
    Desarrollo
    Testing
    Validación de arquitectura
    Entornos offline

    Uso de IA en API: DeepSeek

    Se utiliza DeepSeek como modelo en la nube.

    VentajasLimitacionesRol en el sistema
    Mayor calidad de respuesta
    Mejor razonamiento
    Contexto más amplio
    Coste bajo
    Dependencia de red
    Coste (aunque bajo)
    Menor control
    Validación de calidad
    Comparación con modelos locales
    Ejecución en escenarios reales

    Coste aproximado: Para el modelo por defecto que se va a usar (deepseek-chat):

    • Entrada: ~$0.14 – $0.28 por 1 millón de tokens
    • Salida: ~$0.28 – $0.42 por 1 millón de tokens

    Por qué un enfoque híbrido

    El uso combinado de ambos modelos permite:

    1. Separar arquitectura de proveedor. El sistema RAG se diseña una vez y el LLM se cambia según necesidad:
      • mismo pipeline → distinto modelo
    2. Optimizar coste vs rendimiento
      • local → coste 0
      • API → alta calidad cuando es necesario
    3. Comparación y evaluación. Permite evaluar:
      • calidad de respuestas
      • Impacto del modelo en RAG
      • Diferencias entre local y cloud

    Preparación del entorno RAG híbrido

    Creación del entorno (Anaconda)

    Se crea un entorno aislado para evitar conflictos de dependencias. En la consola de conda ejecuta:

    conda create -n rag_env python=3.10 -y
    conda activate rag_env

    Se utiliza Python 3.10 (máxima compatibilidad con LangChain ahora mismo).

    Instalación de dependencias

    En este entorno se combinan Conda y pip para la instalación de dependencias. Aunque mezclar ambos gestores puede generar conflictos si no se hace correctamente, se sigue un patrón controlado que evita problemas.

    Primero se utilizan paquetes instalados con Conda para la base científica (como NumPy, Pandas o Scikit-learn), ya que garantizan compatibilidad a nivel de sistema. A continuación, se emplea pip para instalar librerías más recientes del ecosistema de IA, como LangChain o herramientas de embeddings, que suelen estar más actualizadas fuera de Conda.

    La regla clave es mantener el orden: instalar primero con Conda y después con pip, evitando volver a usar Conda sobre el mismo entorno una vez que pip ha añadido dependencias. De esta forma, se consigue un entorno estable, reproducible y compatible con las necesidades del desarrollo de sistemas RAG.

    Base científica (Conda)

    conda install -c conda-forge numpy pandas scikit-learn -y

    Librerías de IA, Machine Learning, LangChain (pip)

    pip install langchain langchain-community langchain-core
    pip install -U langchain-ollama
    pip install langchain-openai
    pip install faiss-cpu
    pip install sentence-transformers
    pip install pypdf
    pip install python-dotenv
    pip install requests
    pip install ipykernel
    pip install BeautifullSoup4
    pip install chromadb

    Esta combinación evita problemas de compatibilidad.

    Crear proyecto

    1. Crea una carpeta del proyecto.
    2. En el IDE de preferencia crea el proyecto desde la carpeta creada.
    3. Selecciona el entorno creado en Anaconda
    4. Opcional y recomendable iniciar repositorio y conectar con github.
    5. Crear estructura base
    rag-local/
     ├── .env
     ├── main.py
     ├── data/
     └── notebooks/

    Configuración de IA local (Ollama)

    Descargar modelo:

    ollama pull qwen:4b

    Cuando termine la descarga del modelo, ejecuta:

    ollama run qwen:4b

    Si todo fue correcto, te aparece un prompt interactivo y puedes hacer cualquier pregunta para verificar que el modelo está instalado.

    Configuración de IA en API (DeepSeek)

    Visita https://platform.deepseek.com/ inicia sesión y crea una api_key.

    Crear archivo .env

    DEEPSEEK_API_KEY=tu_api_key

    Cargar las variables en main.py

    # Agrega al inicio
    from dotenv import load_dotenv
    import os
    
    # Cargar variables
    load_dotenv()
    api_key = os.getenv("DEEPSEEK_API_KEY")

    Configuración de LangChain

    Conexión a modelo local

    # Agrega al inicio
    from langchain_ollama import OllamaLLM
    
    
    # LLM local
    llm_local = OllamaLLM(model="qwen:4b")

    Conexión a modelo API

    # agregar al inicio
    from langchain_openai import ChatOpenAI
    
    
    # LLM API
    llm_api = ChatOpenAI(
        openai_api_key=api_key,
        openai_api_base="https://api.deepseek.com",
        model="deepseek-chat"
    )

    Selección de modelo

    Se define un selector para poder cambiar de modelo sin modificar el resto del sistema.

    # Selector de modelo
    usar_api = False
    
    # Elegir Modelo
    llm = llm_api if usar_api else llm_local

    Verificación del entorno

    Código mínimo de prueba al final de main.py:

    respuesta = llm.invoke("Explica qué es RAG en 2 líneas")
    print(respuesta)

    El modelo local responde con usar_api = False:

    RAG significa "Remo de Armas" en inglés.

    El modelo con API responde al usar_api = True

    content='RAG (Retrieval-Augmented Generation) es una técnica que combina la recuperación de información relevante desde una base de datos externa con un modelo de lenguaje, permitiendo generar respuestas más precisas y actualizadas sin necesidad de reentrenar el modelo.' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 57, 'prompt_tokens': 15, 'total_tokens': 72, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 15}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_058df29938_prod0820_fp8_kvcache_20260402', 'id': '27107c79-dc76-40ab-b461-32439139c445', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--019dcfd2-0298-7482-b39b-c069b9f5e30a-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 15, 'output_tokens': 57, 'total_tokens': 72, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}

    Las respuestas de un LLM no solo contienen el texto generado (content), sino también metadatos relevantes como el uso de tokens, el modelo utilizado y el estado de finalización. Esta información es fundamental para analizar costes, rendimiento y comportamiento del sistema, especialmente en arquitecturas RAG.

    Creación de un módulo reutilizable

    El objetivo de este módulo es centralizar la configuración de los modelos LLM, permitiendo utilizar indistintamente un modelo local (Ollama) o un modelo en API (DeepSeek), evitando así la duplicación de código en notebooks o scripts.

    En lugar de definir la configuración del modelo en cada punto del proyecto, se encapsula esta lógica en un único módulo reutilizable. Esto facilita el cambio de modelo en cualquier momento y mejora la organización del código.

    Este enfoque resulta especialmente útil en sistemas RAG, donde el LLM forma parte de múltiples componentes del pipeline. Al desacoplar su configuración, se consigue un entorno más flexible, mantenible y preparado para evolucionar sin necesidad de modificar cada parte del sistema.

    Aquí tienes una sección lista para integrar en tu artículo, con enfoque didáctico y alineada con tu entorno híbrido.

    Parámetros de generación en DeepSeek y Ollama

    Los modelos de lenguaje, son configurables desde la API con parametros de generación que determinan el estilo, longitud y comportamiento de las respuestas. En este entorno híbrido (Ollama + DeepSeek), vamos a unificar conceptos para poder experimentar sin fricciones.

    Visita la documentación de la API de cada LLM

    Parámetros comunes

    Estos parámetros existen en ambos sistemas:

    Temperature (temperatura)

    Controla el nivel de aleatoriedad del modelo.

    • 0.0 → 0.3 → respuestas deterministas (más exactas)
    • 0.5 → 0.7 → equilibrio
    • 0.8+ → creatividad alta (más variabilidad)
    Max tokens / longitud de salida

    Limita el tamaño de la respuesta. Los LLM que estamos trabajando utilizan denominaciones diferentes:

    • DeepSeek → max_tokens
    • Ollama → num_predict
    Top-p (nucleus sampling)

    Controla la diversidad de palabras considerando la probabilidad acumulada.

    • 0.1 → 0.3 → respuestas más conservadoras
    • 0.8 → 1.0 → mayor diversidad
    Top-k (solo en Ollama)

    Limita el número de posibles palabras candidatas. DeepSeek no soporta este parámetro.

    • Bajo → más determinista
    • Alto → más diversidad
    Parámetros de penalización

    Solo DeepSeek (API tipo OpenAI) permite controlar repetición:

    • frequency_penalty → evita repetir palabras
    • presence_penalty → fomenta introducir nuevos temas
    Modos de razonamiento (DeepSeek)

    DeepSeek introduce capacidades avanzadas que permiten separar razonamiento interno de respuesta final

    • deepseek-reasoner
    • modo thinking

    Parámetros claves resumidos

    ParámetroDeepSeekOllamaUso recomendado
    temperature✔️✔️siempre
    max_tokens✔️usar como estándar
    num_predict✔️interno (no usar directamente)
    top_p✔️✔️recomendado
    top_k✔️opcional (solo local)
    penalties✔️avanzado
    reasoning mode✔️avanzado

    Agregar los parámetros al módulo

    Para evitar complejidad innecesaria, usamos una interfaz común y luego traducimos internamente:

    • max_tokens → num_predict (Ollama)
    • top_k solo si usamos Ollama

    Implementacion

    Crea en la raíz del proyecto el fichero llm_config.py

    from dotenv import load_dotenv
    import os
    
    from langchain_ollama import OllamaLLM
    from langchain_openai import ChatOpenAI
    
    # cargar variables de entorno
    load_dotenv()
    
    def get_llm(llm="dsk" , params=None):
        """
        Devuelve un modelo LLM configurado.
        
        Parameters:
        - usar_api (bool): si True usa DeepSeek, si False usa Ollama
        
        Returns:
        - instancia de LLM
        """
        
        default_params = {
            "temperature": 1, # Aleatoriedad del modelo.
            "max_tokens": 50, # Longitud de la respuesta en DeepSeek
            "top_p": 1, # diversidad de palabras considerando la probabilidad acumulada 
            "top_k": 40, # Diversidad de palabras considerando la frecuencia 
            "frequency_penalty": 0, # Penalizacion de frecuencia - DeepSeek
            "presence_penalty": 0, # Penalizacion de presencia - DeepSeek
            "repeat_penalty": 1.1 # Penalizacion de repeticion - Ollama
        }
    
        if params:
            default_params.update(params)
        
        if llm == "dsk":
            # DeepSeek
            return ChatOpenAI(
                model="deepseek-chat", 
                openai_api_key=os.getenv("DEEPSEEK_API_KEY"),
                openai_api_base="https://api.deepseek.com",
                temperature=default_params["temperature"],
                max_tokens=default_params["max_tokens"],
                top_p=default_params["top_p"],
                frequency_penalty=default_params["frequency_penalty"],
                presence_penalty=default_params["presence_penalty"]
            )
    
        elif llm == "oll":
            # Ollama
            return OllamaLLM(
                model="qwen:4b",
                temperature=default_params["temperature"],
                num_predict=default_params["max_tokens"],
                top_p=default_params["top_p"],
                top_k=default_params["top_k"],
                repeat_penalty=default_params["repeat_penalty"]
            )

    Test del módulo en un cuaderno Jupyter

    En la carpeta notebooks crea un cuaderno de Jupyter rag_test.ipynb. Antes de poder trabajar en el entorno rag_env debes registrar el kernel en Jupyter desde la terminal.

    Selecciona el kernel del entorno y añade al cuaderno las rutas para añadir la raíz al path,

    import sys
    import os
    
    sys.path.append(os.path.abspath(".."))

    Carga las variables del entorno

    from dotenv import load_dotenv
    load_dotenv()

    Importa la configuración de LLM

    from llm_config import get_llm

    Elige entre modelos cambiando el valor de llm

    llm = get_llm(llm="dsk")

    Realiza el test

    respuesta = llm.invoke("Explica qué es RAG en IA en 2 líneas")
    print(respuesta)

    Con esta configuración se ha establecido un entorno de trabajo completo para el desarrollo de sistemas RAG híbridos, combinando modelos locales y modelos en API dentro de una misma arquitectura.

    A lo largo de este proceso se ha definido una base técnica que permite trabajar de forma independiente a plataformas cerradas, replicando los conceptos del curso IBM RAG and Agentic AI en un entorno abierto, controlado y extensible. La integración de herramientas como LangChain, junto con el uso de modelos locales mediante Ollama y modelos en la nube como DeepSeek, proporciona la flexibilidad necesaria para experimentar, comparar resultados y optimizar el sistema según las necesidades.

    Este entorno no solo permite realizar las prácticas del curso, sino que sienta las bases para desarrollar soluciones reales, donde es posible equilibrar coste, rendimiento y control de los datos.

    A partir de este punto, el siguiente paso consiste en implementar el pipeline RAG completo, donde todas las piezas configuradas comenzarán a trabajar de forma conjunta y se podrá observar el verdadero valor de esta arquitectura.

    Actualización de entorno: modelo de embeddings

    En el entorno actual ya contamos con modelos de lenguaje (LLM) como Qwen o DeepSeek, que están diseñados para generación de texto. Sin embargo, para trabajar con arquitecturas RAG es imprescindible incorporar un segundo tipo de modelo: los modelos de embeddings.

    Estos modelos no generan texto, sino que transforman fragmentos de información en vectores numéricos que representan su significado semántico. Gracias a esto, es posible realizar búsquedas inteligentes, identificar similitudes entre textos y recuperar información relevante de forma eficiente.

    A diferencia de los LLM, los modelos de embeddings suelen ser más ligeros, pero trabajan de forma intensiva con memoria, especialmente cuando se generan vectores de múltiples documentos. En tu caso, según los recursos disponibles (16 GB de RAM ), es importante elegir un modelo que mantenga un buen equilibrio entre rendimiento y consumo.

    Para este entorno, la opción más recomendable es:

    nomic-embed-text

    Este modelo destaca por:

    • Buen rendimiento semántico para RAG
    • Bajo consumo de recursos
    • Ejecución fluida en entornos locales
    • Integración directa con Ollama

    Como alternativa más potente (pero más exigente en recursos) está: mxbai-embed-large
    (ideal si más adelante amplías RAM o trabajas con menos carga simultánea)

    Instalación del modelo de embeddings en Ollama

    Ejecuta en tu terminal:

    ollama pull nomic-embed-text
    Verificación (opcional pero recomendable)
    ollama list

    Deberías ver algo como:

    NAME                       ID              SIZE      MODIFIED
    nomic-embed-text:latest    0a109f422b47    274 MB    2 minutes ago
    qwen:4b                    d53d04290064    2.3 GB    3 days ago

    Separación de responsabilidades en el código

    Siguiendo buenas prácticas, no es recomendable mezclar la configuración de embeddings con la de los modelos generativos. Por ello, se introduce un nuevo módulo específico dentro del proyecto:

    embeddings_config.py

    De esta forma, cada tipo de modelo queda encapsulado en su propia capa, facilitando mantenimiento, escalabilidad y pruebas.

    Implementación del módulo de embeddings

    Se crea el archivo embeddings_config.py con una función que permite instanciar el modelo de embeddings de forma centralizada:

    from langchain_community.embeddings import OllamaEmbeddings
    
    def get_embeddings(provider="oll", model=None):
        """
        Devuelve un modelo de embeddings configurado.
        """
    
        if provider == "oll":
            return OllamaEmbeddings(
                model=model or "nomic-embed-text"
            )
    
        else:
            raise ValueError(f"Proveedor de embeddings no soportado: {provider}")

    Este enfoque permite desacoplar completamente el sistema y facilita futuros cambios, como probar otros modelos de embeddings sin modificar el resto del código.

    Uso dentro del flujo de trabajo

    Una vez definido el módulo, el uso es directo desde cualquier parte del proyecto:

    from config.embeddings_config import get_embeddings
    
    embeddings = get_embeddings()
    
    vector = embeddings.embed_query("¿Qué es RAG?")

    Con esto, ya es posible generar representaciones vectoriales tanto de consultas como de documentos.

    Resultado en la arquitectura híbrida

    Tras esta actualización, el entorno queda estructurado de forma clara:

    Embeddings → Ollama (local)
    
    LLM → DeepSeek / Ollama

    Esta separación es fundamental y refleja cómo se diseñan los sistemas RAG en entornos reales de producción, donde cada componente cumple una función específica dentro del pipeline.

  • Matrices en Python con Numpy

    NumPy (Numerical Python) es un paquete de código abierto ampliamente utilizado en ciencia e ingeniería.

    Realiza una amplia variedad de operaciones matemáticas avanzadas con alta eficiencia. En este artículo se verán varias funciones clave de NumPy como la creación de arrays, segmentación (slicing), indexación, cambio de forma (reshape) y apilamiento.

    Arrays con Numpy

    Los arrays son una de las estructuras de datos fundamentales de la librería NumPy, esenciales para organizar tus datos. Puedes pensar en ellos como una cuadrícula de valores, todos del mismo tipo.

    Si has utilizado listas de Python anteriormente, recordarás que son convenientes, ya que puedes almacenar diferentes tipos de datos. Sin embargo, las listas de Python tienen funciones limitadas y ocupan más espacio y tiempo de procesamiento que los arreglos de NumPy.

    NumPy proporciona un objeto array que es mucho más rápido y compacto que las listas de Python. A través de su amplia integración de API, la librería ofrece muchas funciones incorporadas que facilitan mucho el cómputo con solo unas pocas líneas de código. Esto puede ser una ventaja enorme al realizar operaciones matemáticas en grandes conjuntos de datos.

    El objeto de array en NumPy se llama ndarray, que significa “array n-dimensional”. Un arreglo 1-D representa una lista estándar de valores en una dimensión. Recuerda que en NumPy, todos los elementos dentro de un array son del mismo tipo.

    Crear arrays

    Puedes crear un array de una dimensión utilizando simplemente la función array() la cual recibe una lista de valores como argumento.

    a = np.array([1, 2, 3])
    print(a)
    [1 2 3]

    Otra forma de implementar un array es utilizando np.arange(). Esta función devolverá un arreglo de valores espaciados uniformemente dentro de un intervalo dado.

    b = np.arange(3)
    print(b)
    [0 1 2]

    Otro ejemplo usando las características de np.arange():

    # Crear un array que comienza por 1, termina en 20, incrementando valores por 3.
    c = np.arange(1, 20, 3)
    print(c)
    [ 1  4  7 10 13 16 19]

    La función np.linspace() puede generar valores espaciados uniformemente en un intervalo. En este ejemplo el intervalo es entre 0 y 100 y el número de elementos en el array, 5.

    lin_spaced_arr = np.linspace(0, 100, 5)
    print(lin_spaced_arr)
    [  0.  25.  50.  75. 100.]

    Por defecto los valores devueltos por np.linspace son de punto flotante (np.float64). Puedes especificar fácilmente tu tipo de datos usando dtype.

    c_int = np.arange(1, 20, 3, dtype=int)
    print(c_int)
    [ 1  4  7 10 13 16 19]

    Más arrays de NumPy

    Puedes crear arreglos fácilmente con funciones integradas tales como: 

    • np.ones(shape) – Devuelve un nuevo arreglo estableciendo los valores en uno.
    • np.zeros(shape) – Devuelve un nuevo arreglo estableciendo los valores en cero.
    • np.empty(shape) – Devuelve un nuevo arreglo sin inicializar.
    • np.random.rand(shape) – Devuelve un nuevo arreglo con valores randoms. 

    Arrays multidimensionales

    Con NumPy también puedes crear arreglos con más de una dimensión. En los ejemplos anteriores, trabajaste con arreglos 1-D, donde puedes acceder a sus elementos utilizando un único índice. Un arreglo multidimensional tiene más de una columna. Piensa en un arreglo multidimensional como una hoja de Excel donde cada fila/columna representa una dimensión.

    # Crear array de dos dimensiones (2-D)
    two_dim_arr = np.array([[1,2,3], [4,5,6]])
    print(two_dim_arr)
    [[1 2 3]
     [4 5 6]]

    Una forma alternativa de crear un arreglo multidimensional es cambiando la forma del arreglo 1-D inicial. Utilizando np.reshape() puedes reorganizar los elementos del arreglo anterior en una nueva forma.

    # 1-D array 
    one_dim_arr = np.array([1, 2, 3, 4, 5, 6])
    
    # Multidimensional array using reshape()
    multi_dim_arr = np.reshape(
                    one_dim_arr, # the array to be reshaped
                   (2,3) # dimensions of the new array
                  )
    # Print the new 2-D array with two rows and three columns
    print(multi_dim_arr)
    [[1 2 3]
     [4 5 6]]

    Encontrar el tamaño, la forma y la dimensión.

    Necesitarás saber cómo encontrar el tamaño, la dimensión y la forma de un array. Todos estos son atributos de un ndarray y se puede acceder a ellos de la siguiente manera:

    • ndarray.ndim – Almacena el número de dimensiones del arreglo.
    • ndarray.shape – Almacena la forma del arreglo. Cada número en la tupla denota la longitud de cada dimensión correspondiente.
    • ndarray.size – Almacena el número de elementos en el arreglo.

    Operaciones matemáticas con arrays

    En esta sección, verás que NumPy te permite realizar rápidamente sumas, restas, multiplicaciones y divisiones elemento por elemento, tanto para arreglos 1-D como multidimensionales.

    Las operaciones se realizan utilizando el símbolo matemático correspondiente: ‘+’, ‘-‘ y ‘*’. Recuerda que la suma de listas de Python funciona de manera completamente diferente, ya que concatena las listas creando una lista más larga; además, la resta y la multiplicación de listas de Python no funcionan.

    arr_1 = np.array([2, 4, 6])
    arr_2 = np.array([1, 3, 5])
    
    # Adding two 1-D arrays
    addition = arr_1 + arr_2
    print(addition)
    
    # Subtracting two 1-D arrays
    subtraction = arr_1 - arr_2
    print(subtraction)
    
    # Multiplying two 1-D arrays elementwise
    multiplication = arr_1 * arr_2
    print(multiplication)
    [ 3  7 11]
    [1 1 1]
    [ 2 12 30]

    Multiplicación de un vector por un escalar (broadcasting)

    Supón que necesitas convertir millas a kilómetros. Para hacerlo, puedes usar las funciones de arreglos de NumPy que has aprendido hasta ahora. Puedes lograrlo realizando una operación entre un arreglo (millas) y un solo número (la tasa de conversión, que es un escalar). Dado que 1 milla = 1.6 km, NumPy calcula cada multiplicación dentro de cada celda. 

    Este concepto se llama broadcasting (transmisión), el cual te permite realizar operaciones específicamente en arreglos de diferentes formas.

    vector = np.array([1, 2])
    vector * 1.6
    array([1.6, 3.2])

    Indexación y segmentación (slicing)

    La indexación es muy útil ya que te permite seleccionar elementos específicos de un array. También te permite seleccionar filas, columnas o planos completos, como verás en futuras tareas para arreglos multidimensionales. 

    Indexación

    Seleccionemos elementos específicos de los arreglos dados. 

    # Seleccionar el tercer elemento del array.
    a = ([1, 2, 3, 4, 5])
    print(a[2])
    
    # Seleccionar el primer elemento.
    print(a[0])
    3
    1

    Para arrays multidimensionales de forma \(n\), para indexar un elemento específico, debes ingresar \(n\) índices, uno por cada dimensión.

    # Indexing on a 2-D array
    two_dim = np.array(([1, 2, 3],
              [4, 5, 6], 
              [7, 8, 9]))
    
    # Select element number 8 from the 2-D array using indices i, j.
    print(two_dim[2][1])
    8

    Segmentación (Slicing)

    La segmentación te devuelve una sublista de los elementos que especifiques del array. La notación de segmentación define un valor de inicio y uno de fin, y copia la lista desde el inicio hasta el final, pero sin incluir este último (el final es exclusivo).

    La sintaxis es:

    arreglo[inicio:fin:paso]

    Si no se pasa ningún valor al inicio, se asume que inicio = 0; si no se pasa ningún valor para el fin, se asume que fin = longitud del arreglo y si no se pasa ningún valor para el paso, se asume que paso = 1.

    # Slice the array a to get the array [2,3,4]
    sliced_arr = a[1:4]
    print(sliced_arr)
    [2, 3, 4]

    Apilamiento (Stacking)

    Finalmente, el apilamiento es una característica de NumPy que permite una mayor personalización de los arrays. Consiste en unir dos o más arreglos, ya sea de forma horizontal o vertical, lo que significa que se realiza a lo largo de un nuevo eje.

    • np.vstack() – apila verticalmente.
    • np.hstack() – apila horizontalmente.
    • np.hsplit() – divide un arreglo en varios arreglos más pequeños.
    a1 = np.array([[1,1], 
                   [2,2]])
    a2 = np.array([[3,3],
                  [4,4]])
    print(f'a1:\n{a1}')
    print(f'a2:\n{a2}')
    a1:
    [[1 1]
     [2 2]]
    a2:
    [[3 3]
     [4 4]]

    Apilar los arrays verticalmente

    # Stack the arrays vertically
    vert_stack = np.vstack((a1, a2))
    print(vert_stack)
    [[1 1]
     [2 2]
     [3 3]
     [4 4]]

    Apilar horizontalmente:

    # Stack the arrays horizontally
    horz_stack = np.hstack((a1, a2))
    print(horz_stack)
    [[1 1 3 3]
     [2 2 4 4]]

  • Singularidad, dependencia lineal y el determinante

    Uno de los mayores errores al aprender álgebra lineal es pensar que trata de números, fórmulas y cálculos. Esa es solo la superficie. En realidad, lo que estás aprendiendo es algo mucho más poderoso: cómo interpretar y combinar información.

    El lenguaje de las ecuaciones

    Para entenderlo, hay que hacer un pequeño cambio de perspectiva. Una ecuación no es una operación. Es una afirmación sobre la realidad. Es una sentencia, \( x+y=10 \), está diciendo: “hay dos cantidades cuya suma es 10”.

    Con base en esta afirmación, piensa en una ecuación como una oración que describe al mundo:

    • El perro es negro.
    • El gato es naranja.

    Ambas son sentencias que aportan información y al combinarlas tienes un conjunto de afirmaciones que combinadas te permiten deducir información al igual que un sistema de acuaciones.

    Cuando aparece el sistema: combinar información

    Imagina que estás resolviendo un problema como si fuera un pequeño caso de investigación. Tienes varias pistas y cada una aporta información parcial. Una pista puede decirte algo genérico, otra puede acotar un poco más, y juntas empiezan a dibujar una imagen más precisa. Eso es exactamente lo que hace un sistema de ecuaciones: combinar piezas de información para reducir la incertidumbre.

    Pero no todas las combinaciones de información funcionan igual. Y aquí es donde el álgebra deja de ser mecánica y empieza a ser lógica. Cuando juntas varias sentencias, solo pueden pasar tres cosas:

    1. Que cada nueva ecuación aporte algo distinto y coherente con lo anterior. En ese caso, el sistema converge hacia una única solución, se denomina sistema no singular. En términos matemáticos, el sistema es completo, consistente y determinado. En términos prácticos, tienes suficiente información bien estructurada.
    2. Las sentencias no se contradicen, pero tampoco aportan nada nuevo. Es como si alguien repitiera la misma pista con otras palabras; se denomina sistema singular. En matemáticas esto se traduce en ecuaciones dependientes, y el resultado es que no hay una única solución, sino infinitas.
    3. Las sentencias se contradicen. Una dice una cosa y otra dice lo contrario, se denomina también sistema singular. En ese momento, el sistema deja de tener sentido. Has construido una realidad imposible.

    Los tres comportamientos posibles

    Sistema completo, no singular

    Cada ecuación aporta información nueva. No se repiten, no se contradicen. Se refuerzan. En ese caso, el sistema converge hacia una única solución. Esto significa que has descrito la realidad con suficiente precisión como para identificar un único resultado. Geométricamente, si quieres visualizarlo, es el punto donde las líneas se cruzan.

    En un sistema de ecuaciones, por ejemplo, que simula una compra de peras y bananas, donde tenemos que descubrir el precio de cada una. El primer día se compran 1 pera y 1 banana por 10€; el día 2 se compran 1 pera y 2 bananas por 12€.

    $$ a + b = 10$$

    $$ a + 2b = 12$$

    Solución única: \( a = 8, b = 2 \)

    Sistema redundante, singular

    Aquí ocurre algo más sutil. Las ecuaciones no están mal. No hay contradicción. Pero una no aporta nada nuevo respecto a la otra. En ese caso, el sistema no falla, pero tampoco se define completamente. Tienes infinitas soluciones. No porque el sistema sea incorrecto, sino porque no tienes suficiente información independiente.

    En este caso la compra se expresa así:

    $$ a + b = 10$$

    $$ 2a + 2b = 20$$

    Infinitas soluciones, todos los conjuntos de dos números que sumen 10.

    Sistema contradictorio, singular

    Aquí el sistema se rompe. Una ecuación afirma algo, la otra afirma lo contrario. No existe ningún valor que pueda satisfacer ambas simultáneamente. Y esto no es un problema de cálculo. Es un problema lógico.

    En este caso la compra se expresa así:

    $$ a + b = 10$$

    $$ 2a + 2b = 24$$

    No hay soluciones posibles.

    Veamos este sistema de sentencias en un pequeño test:

    • Entre el perro, el gato y el ave, uno es rojo
    • Entre el perro, el gato, uno es naranja
    • El perro es negro

    Preguntas:

    • ¿De qué color es el ave?
    • ¿Es un sistema singular o no singular?

    Cada sentencia del sistema va descubriendo con la solución de cada sentencia. Ninguna de las sentencias por sí sola permite llegar a la solución. Es la combinación progresiva de información la que va acotando el problema hasta hacerlo determinable.

    Desde el punto de vista del álgebra lineal, este sistema es no singular. ¿Por qué? Porque cada sentencia aporta información nueva y no redundante. No hay contradicciones ni repeticiones. El sistema está bien definido y conduce a una única solución posible.

    En un dataset real, cada registro funciona como una sentencia. Cada variable añade contexto, y el modelo lo único que hace es combinar esas “afirmaciones” para encontrar patrones. Si las sentencias son coherentes y aportan información distinta, el modelo puede aprender. Si son redundantes, el modelo se vuelve ineficiente. Y si son contradictorias, el modelo simplemente no puede construir una representación fiable de la realidad.

    Los sistemas de ecuaciones representados gráficamente

    La singularidad solo depende de la matriz de coeficientes:

    Las constantes no importan a la hora de determinar la singularidad. Si los lleváramos todos a 0, mantendrían la singularidad.

    Ecuaciones dependientes e independientes

    La dependencia lineal está relacionada con lo que vimos anteriormente sobre si las ecuaciones aportan información nueva o están repitiendo lo mismo.

    Dependencia Lineal

    Un conjunto de ecuaciones (o una matriz) es linealmente dependiente si:

    • Una de ellas puede obtenerse a partir de las otras mediante multiplicaciones o sumas.

    Ejemplo básico:

    $$ x + y = 1$$

    $$2x + 2y = 2$$

    o lo que es lo mismo

    $$A = \begin {bmatrix} 1 & 1 \\ 2 & 3 \end{bmatrix}$$

    Revela que la segunda ecuación es simplemente: \( 2 \cdot (x + y)\)

    • No aporta información nueva
    • Es redundante

    Conclusión: dependencia lineal

    Caso más sutil (nivel siguiente)

    No siempre es un múltiplo directo:

    $$\text{Fila 2} = \frac{\text{Fila 1} + \text{Fila 3}}{2}$$

    • Sigue siendo dependencia lineal
    • Porque una fila se construye con otras

    Independencia lineal

    Un conjunto de ecuaciones (o una matriz) es linealmente independiente si:

    • Ninguna ecuación puede construirse a partir de las demás

    Ejemplo básico:

    $$x + y = 1$$

    $$x – y = 3$$

    en forma matricial:

    $$A = \begin {bmatrix} 1 & 1 \\ 1 & 1 \end{bmatrix}$$

    No existe ningún número que permita convertir una ecuación en la otra.

    • Cada una aporta información diferente
    • Conclusión: independencia lineal

    Relación con singularidad

    • Dependencia lineal → matriz singular
    • Independencia lineal → matriz no singular

    En ciencia de datos:

    • Dependencia → variables redundantes
    • Independencia → variables útiles

    El rango: cómo medir la información de un sistema

    Hasta ahora has visto algo muy importante: no todas las frases (o ecuaciones) aportan lo mismo. Algunas añaden información nueva y otras simplemente repiten lo que ya sabemos. Pero en matemáticas —y especialmente en machine learning— necesitamos medir esa información de forma precisa. Esa información nos la brinda el rango de una matriz

    Recuerda los sistemas de frases que vimos:

    • “El perro es negro”
    • “El gato es naranja”

    Este sistema aporta dos piezas de información independientes.

    Sin embargo:

    • “El perro es negro”
    • “El perro es negro”

    Aquí solo hay una pieza de información real.

    Y en el caso más extremo, otros sistemas no aportan ninguna información útil para el problema que queremos resolver.

    ¿Qué es el rango?

    El rango traduce exactamente esa idea al lenguaje matemático: El rango de un sistema (o de su matriz) es la cantidad de información independiente que contiene.

    • Cuenta cuántas ecuaciones realmente aportan algo nuevo.
    • Ignora las que son redundantes o no aportan información útil.

    Conexión con sistemas de ecuaciones

    RangoQué significaSoluciones
    AltoMucha informaciónMuy restringidas
    MedioAlgo de redundanciaInfinitas soluciones
    BajoPoca o ninguna informaciónMáxima libertad

    Interpretación geométrica (clave en machine learning)

    Este concepto no es solo algebraico, también es geométrico:

    • Rango máximo → las soluciones se reducen a un punto
    • Rango intermedio → las soluciones forman una línea
    • Rango bajo → las soluciones ocupan un plano o más dimensiones

    Esto es exactamente lo que ocurre cuando entrenas modelos:

    • Más información → modelo más definido
    • Menos información → más incertidumbre

    Cómo se calcula el rango en la práctica

    Para calcular el rango no necesitas analizar ecuación por ecuación.

    En su lugar, haces lo siguiente:

    1. Transformas la matriz usando operaciones de fila
    2. La llevas a una forma más simple (forma escalonada)
    3. Cuentas cuántas filas siguen siendo útiles

    En machine learning, este concepto aparece constantemente:

    • Compresión de imágenes
    • Reducción de dimensionalidad
    • Eliminación de ruido
    • Modelos más eficientes

    De hecho, muchas técnicas avanzadas se basan en reducir el rango sin perder la información esencial.

    El determinante

    Otra forma para saber si un sistema es singular es a través del determinante. Un número que se calcula a partir la matriz y que te dice inmediatamente:

    • Si es 0 → matriz singular
      • Una fila es combinación de otra
      • Hay dependencia lineal
      • El sistema no tiene solución única
    • Si es ≠ 0 → matriz no singular
      • Filas independientes
      • Solucion unica

    Calcular el determinante

    Dada una matriz 2×2:

    $$\begin{bmatrix} a & b \\ c & d \end{bmatrix}$$

    El determinante es: \( ad – bc \).

    Matrices mas grandes

    El proceso es similar, pero con más diagonales,

    $$\begin{bmatrix} a & b & c \\ d & e & f \\ g & h & i \end{bmatrix}$$

    Primer diagonal: \( a * e * i \).

    Segundo diagonal: \( b * f * g \).

    Tercer diagonal: \( c * d * h \).

    Luego se realiza la operación de sustracción a la inversa:

    $$\begin{bmatrix} a & b & c \\ d & e & f \\ g & h & i \end{bmatrix}$$

    $$Det = (a * e * i) + (b * f * g) + (c * d * h)$$

    $$- (c * e * g) – (f * h * a) – (b * d * i)$$

    Atajo para el de las matrices con ceros debajo de la diagonal.

    Cuando una matriz tiene solo ceros debajo de la diagonal central, hace que todos los cálculos de las diagonales restantes sean 0 excepto el de la diagonal principal y el determinante será el resultado de este.

    Ejemplo: el resultado del determinante en esta matriz es 6 (1 *2*3).

    ¿Cómo se ve esto en el mundo de los datos?

    Lenguaje NaturalÁlgebra LinealCiencia de Datos
    SentenciaEcuación (Fila)Registro / Observación
    SujetosVariables Características (Features)
    ConsistenciaDeterminante Datos limpios y útiles
    RedundanciaDependencia LinealMulticolinealidad (Ruido)

    Y cuando operas con matrices, lo que estás haciendo en realidad es manipular información estructurada. Si programas un modelo de Machine Learning sin entender esto, tendrás problemas:

    1. Datos Redundantes: Si le das a tu modelo el “Salario Mensual” y el “Salario Anual”, no está aprendiendo más. Estás desperdiciando potencia de cómputo en información repetida (Multicolinealidad).
    2. Datos Contradictorios: Si en tu base de datos el mismo cliente aparece con 20 años y con 70 años en la misma compra, el modelo se confunde y falla.
    3. El Rango de la Matriz: En álgebra, el “Rango” es simplemente el número de pistas reales y únicas que tienes. Si tienes 100 ecuaciones pero el rango es 2, en realidad solo tienes 2 pistas útiles y 98 ecos.

  • Anaconda: guía práctica para gestionar entornos, paquetes y flujos de trabajo

    Instalar Anaconda te da acceso a conda, Python y miles de otras herramientas populares. Instala automáticamente más de 300 paquetes listos para el trabajo en Data Science.

    Anaconda Navigator proporciona una interfaz gráfica de usuario: Anaconda Desktop para interactuar con conda, sin utilizar la línea de comandos. Si prefiere trabajar desde la línea de comandos, puede interactuar con conda directamente a través de Anaconda Prompt.

    Manejo de entornos

    El entorno (base) NO es para proyectos, úsalo solamente para la gestión de conda y tareas globales. El proceso de instalación de conda crea un entorno llamado base, donde se instala conda. Sin embargo, al comenzar un nuevo proyecto, se recomienda crear un nuevo entorno. Esto facilita el mantenimiento y la reproducibilidad de los entornos, además de mantener su estabilidad.

    Crear un nuevo entorno

    conda create --name <ENV_NAME> <PACKAGE>=<VERSION> <PACKAGE> <PACKAGE>

    Puedes añadir paquetes al entorno especificando o no la versión. Ejemplo:

    conda create --name hello-env python=3.14 pandas beautifulsoup4 

    Activar un entorno

    conda activate <ENV_NAME>

    Cambiar entre entornos y visualizar entornos existentes

    conda info --envs
    conda activate <ENV_NAME>

    Bloquear un entorno

    Bloquear un entorno crea un entorno completamente especificado, con todos los paquetes utilizados en el proyecto y sus dependencias configuradas a una versión específica. Para bloquear tu proyecto, necesitas conda-project instalado el paquete en el entorno que deseas bloquear. Instala el paquete ejecutando los siguientes comandos:

    conda activate <ENV>
    conda install conda-project

    Si tu proyecto no contiene un archivo, environment.yml créalo ejecutando el siguiente comando:

    conda-project init

    A continuación, puedes bloquear el entorno de tu proyecto ejecutando el siguiente comando:

    conda-project lock

    Al bloquear tu proyecto, se genera un archivo conda-lock.default.yml que puedes exportar para compartirlo con otros.

    Compartir un entorno

    Compartir tu entorno con otra persona le permite usar conda para recrear tu entorno en su máquina. Para compartir un entorno y sus paquetes de software, debe exportar las configuraciones de su entorno a un archivo .yml en un entorno que este activado con el comando.

    conda env export > environment.yml

    El archivo se guarda en su directorio de trabajo y se puede compartir.

    Desactivar un entorno

    Lo más recomendable es desactivar el entorno cuando hayas terminado de trabajar en él. Cuando desactivas un entorno, conda vuelve al entorno que estaba activado anteriormente. Para desactivar su entorno activo, ejecute el siguiente comando:

    conda deactivate

    Eliminar un entorno

    Para eliminar un entorno, ejecute el siguiente comando. Si, por cualquier motivo, necesita eliminar manualmente un directorio de entorno, no utilice el explorador de archivos . Si lo hace, eliminará el contenido del entorno y liberará espacio en su equipo, pero la ruta del entorno permanecerá en su archivo environments.txt.

    conda remove --name <ENV_NAME> --all

    Gestión de Canales

    En el ecosistema de Conda, los canales son repositorios remotos donde se alojan los paquetes. Los tipos de canales más comunes:

    1. Defaults: Es el canal oficial gestionado por Anaconda. Contiene paquetes probados y validados para ser estables y compatibles entre sí.
    2. Conda-Forge: Es un canal comunitario (el más grande y popular). Al ser mantenido por la comunidad, suele tener versiones más actualizadas de las librerías y una variedad mucho mayor de paquetes que el canal por defecto.
    3. Canales específicos: Existen repositorios especializados para nichos científicos o técnicos, como bioconda (para bioinformática).

    Conda permite tener varios canales activos a la vez, pero sigue un orden jerárquico. Si un mismo paquete (por ejemplo, pandas) está disponible en dos canales distintos, Conda lo descargará del que tenga mayor prioridad (el que aparezca primero en tu configuración).

    Visualización de los canales disponibles

    Para ver qué canales están configurados actualmente en conda ejecute el siguiente comando:

    conda config --show channels

    Configuración de canales

    Conda lee la configuración de sus canales desde el archivo .condarc. Para añadir, eliminar o reordenar canales, deberá editar este archivo. Para encontrarlo utilice el comando:

    conda config --show-sources

    Puedes añadir o eliminar canales de tu lista channels: usando comandos de conda en o editando manualmente tu el archivo .condarc.

    conda config <FLAG> channels <CHANNEL>
    FlagAcción
    --addAgrega un canal al principio de channels:.
    --prependAgrega un canal al principio de channels:.
    --appendAgrega un canal al final de channels:.
    --removeElimina un canal de channels:.

    Ejemplo:

    conda config --add channels conda-forge

    Configurar valores predeterminados

    La entrada defaults en tu lista de channels: es un alias especial. Cuando conda detecta busca en los canales listados default_channels: en orden descendente. Este  también se puede configurar utilizando comandos de conda o editando manualmente .condarc.

    conda config --add default_channels <CHANNEL>

    Reemplaza <CHANNEL> con la URL del canal que deseas agregar.

    Instalación de paquetes desde un canal específico

    El uso de la sintaxis de dos puntos dobles instala el paquete desde el canal especificado, pero instala las dependencias de ese paquete desde los canales que aparecen en el  .condarc siguiendo el orden de prioridad de los canales.

    conda install <CHANNEL>::<PACKAGE>

    Gestión de paquetes

    Buscando paquetes conda

    La función de búsqueda de Conda le permite buscar a través de canales para comprobar si un paquete específico está disponible, qué versiones existen en diferentes canales o qué ya está instalado en su entorno local.

    conda search <PACKAGE>

    Buscar un canal específico

    conda search <CHANNEL>::<PACKAGE>

    Búsqueda en entornos locales

    Utilice la bandera --envs para buscar un paquete en sus entornos locales:

    conda search --envs <PACKAGE>

    Instalar paquetes

    Para instalar un solo paquete, ejecute el siguiente comando:

    conda install <PACKAGE>

    Para instalar un paquete en un entorno que no sea su entorno activo actual, especifique el nombre del entorno:

    conda install <PACKAGE> --name <ENVIRONMENT>

    Especificar un canal:

    conda install <CHANNEL>::<PACKAGE>

    Especificación de versiones de paquetes

    Por defecto, al instalar paquetes desde la línea de comandos, conda recupera las versiones más recientes de los paquetes solicitados (y sus dependencias) que sean compatibles con el entorno actual.

    conda install <PACKAGE>=<VERSION>

    Actualización de paquetes

    Para actualizar un solo paquete, ejecute el siguiente comando:

    conda update <PACKAGE>

    Cuando actualizas un paquete, conda también puede actualizar otros paquetes del entorno para mantener la compatibilidad, o instalar nuevos paquetes necesarios para las dependencias actualizadas. Esto ayuda a evitar que tu entorno se dañe debido a cambios en las dependencias. Para evitar que conda actualice cualquier paquete que no sea el que especifiques, usa la bandera --no-update-deps

    Actualizar varios paquetes

    conda update <PACKAGE> <PACKAGE> <PACKAGE>

    Especificar un canal para las actualizaciones de paquetes

    conda update <PACKAGE> --override-channels --channel <CHANNEL>

    Actualizar todos los paquetes

    Es posible que la ejecución de este comando no actualice todos los paquetes de un entorno determinado a sus últimas versiones. Si la última versión de un paquete es incompatible con otros paquetes instalados en el entorno, conda solo actualizará ese paquete a la última versión compatible .

    conda update --all

    Usando pip en Conda

    La mayoría de los paquetes populares del repositorio PyPI están disponibles en el repositorio público de Anaconda , Anaconda.org o conda-forge . Sin embargo, es posible que necesite usar pip si un paquete o una versión específica no está disponible a través de conda. Instalar paquetes usando pip modifica tu entorno conda. Sin embargo, conda desconoce estas modificaciones. Como resultado, cuando conda intenta modificar el entorno posteriormente, existe una alta probabilidad de que surjan conflictos de dependencias entre los paquetes controlados por conda y los paquetes de pip no controlados, lo que puede provocar un entorno dañado.

    Comprender conda y pip

    Aunque algunas funcionalidades de conda y pip se solapan (en concreto, la capacidad de instalar paquetes de Python), fueron diseñadas y deben usarse con fines diferentes.

    • Pip es la herramienta recomendada por la Autoridad de Empaquetado de Python para instalar paquetes del Índice de Paquetes de Python (PyPI).
    • Conda, por otro lado, es un gestor de paquetes y entornos multiplataforma que instala y gestiona paquetes tanto del repositorio público de Anaconda como de Anaconda.org.

    Otras diferencias clave entre conda y pip incluyen:

    condapip
    Formato de distribución del paqueteBinariosWheels o fuente
    ¿Requiere compiladores?No
    Tipos de paquetesCualquiera (Python, R, C++, etc.)Solo Python
    ¿Creación de entorno?Sí, incorporadoNo, requiere virtualenv o venv.
    ¿Resolución de dependencias?No
    Fuentes de paquetesRepositorio de Anaconda, Anaconda.orgPyPI

    Crear un entorno conda que incluya paquetes pip

    Para crear un entorno estable que incluya paquetes pip, Anaconda recomienda escribir un archivo environment.yml y luego construir un entorno a partir de ese archivo. Aunque este método requiere más tiempo de configuración, ofrece varias ventajas:

    • Control sobre el orden de compilación, las versiones y los canales de los paquetes.
    • Actualizaciones de entorno sencillas
    • Mayor reproducibilidad y facilidad para compartir mediante un archivo .yml.

    Al escribir el archivo, asegúrese de agregar pip y sus dependencias al final, ya que conda crea los entornos en el orden en que aparecen. Ejemplo:

    name: myenv           # Nombre del entorno
    dependencies:         # Lista de los paquetes que se incluyen
        - python=3.12    
        - bokeh>=2.4.2
        - flask
        - pip             # Install pip en el entorno
        - pip:            # Incluir los paquetes pip al final
            - Flask-Testing

    La documentación oficial de conda incluye más información sobre cómo crear archivos de entorno manualmente , así como especificaciones de coincidencia de paquetes .

    Creación de un entorno a partir de un archivo environment.yml

    Para crear un entorno a partir de un archivo environment.yml, ejecute el siguiente comando desde el directorio que contiene el archivo:

    conda env create --file environment.yml

    Actualizar un entorno con un archivo environment.yml

    Si alguna vez necesita agregar paquetes a su entorno, realizar cambios en las versiones de los paquetes o eliminar paquetes, actualice el archivo environment.yml y luego vuelva a construir el entorno ejecutando el siguiente comando desde el directorio que contiene el archivo:

    conda env update --file environment.yml --prune

    --prune elimina cualquier paquete huérfano del entorno.

    Uso de pip install en un entorno conda

    Debido a que conda no reconoce las actualizaciones de entorno realizadas por pip, el uso de pip en su entorno debe ser la última acción que se realice al construir el entorno.

    conda install <PACKAGE> <PACKAGE> pip

    No ejecute pip install en su entorno base. Cree un entorno conda independiente para aislar los cambios.

    Solucionador de dependencias

    Una de las principales características de conda es su capacidad para gestionar paquetes de software y sus dependencias. La gestión de dependencias puede resultar compleja, sobre todo cuando un paquete tiene muchas dependencias. Conda utiliza un algoritmo llamado 
    solucionador de dependencias , que determina qué versiones de qué paquetes deben instalarse para satisfacer (o resolver) todas las dependencias sin conflictos de versiones.

    Gestión de errores del solucionador

    Aunque el solucionador de conda está diseñado para instalar paquetes y sus dependencias sin generar conflictos de versiones, estos pueden ocurrir. Este tutorial explicará los posibles escenarios de conflicto del solucionador y cómo solucionarlos.

    Gestionar Python en entornos conda

    Por defecto, al instalar o actualizar paquetes en un ambiente, conda recupera las versiones más recientes posibles del/de los paquete(s) solicitado(s) y sus dependencias que sean compatibles con el entorno actual.

    Algunos paquetes (o versiones de paquetes) solo son compatibles con ciertas versiones de Python. Si intentas instalar un paquete que no es compatible con la versión de Python que usas en tu entorno, conda no lo instalará.

    En este caso, suele ser mejor crear un nuevo entorno que use la versión de Python requerida, junto con los demás paquetes que necesitas.Para crear un nuevo entorno con una versión específica de Python y todos los paquetes que necesitas, ejecuta el siguiente comando:

    conda create --name <ENVIRONMENT> python=<VERSION> <PACKAGE> <PACKAGE> <PACKAGE>

    Integraciones de conda

    Consulta esta página para integrar conda con:

    • Docker
    • Authenticated Docker builds
    • Snowflare Snowpark
    • TensorFlow

    Más de Anaconda

    • Herramientas Anaconda: diseñadas para optimizar tus flujos de trabajo de ciencia de datos, aprendizaje automático e inteligencia artificial. 
    • Plataforma Anaconda (Nube): Anaconda Platform proporciona una plataforma segura y repositorio centralizado. Dónde puedes controlar el acceso de tu organización a paquetes software de código abierto y realizar un seguimiento de las vulnerabilidades del software.
    • Data Science & AI Workbench: Workbench es una plataforma de ciencia de datos escalable, segura y preparada para entornos empresariales que permite a los equipos gestionar los activos de ciencia de datos, colaborar e implementar sus proyectos de ciencia de datos.

    Flujo de trabajo

    Crea el nuevo entorno para el proyecto y actívalo

    conda create --name <ENV_NAME>
    conda activate <ENV_NAME>

    Instala los paquetes necesarios para el proyecto

    conda install pandas scipy numpy matplotlib seaborn scikit-learn 

    Trabajar con JupyterLab – conda

    Para que el entorno aparezca en la lista de opciones de tu cuaderno, ejecuta este comando y luego puedes seleccionarlo en la lista de kernels disponibles.

    python -m ipykernel install --user --name=<ENV_NAME> --display-name "NAME_ALIAS"
    

    Trabajar con Visual Studio Code – conda

    Presiona Ctrl+Mayús+P, busca Python: Select Interpreter y elige tu entorno de la lista. Puedes instalar la extensión nb_conda_kernels en tu entorno base (conda install nb_conda_kernels), la cual detectará automáticamente todos tus entornos de Conda que tengan ipykernel instalado. 

  • AdventureWorks Power BI Data Analyst

    AdventureWorks Power BI Data Analyst

    Proyecto de Dashboard 360° para AdventureWorks 2022. Se extrajeron y exploraron los datos con SQL, se limpiaron y transformaron usando Python y Pandas, y finalmente se visualizaron en Power BI. El proyecto abarca todo el proceso ETL: consultas y modelado de datos en SQL, manipulación y preparación de datos en Python, y construcción de visualizaciones interactivas en Power BI para análisis integral de finanzas, ventas, clientes y productos.

    El proyecto de curso original solo incluía elaborar un dashboard con datos provenientes de tablas de Excel aportadas por el curso.

    Tecnologías utilizadas

    • SQL Server (base transaccional)
    • SQL Server Management Studio + VS Code (extensión MSSQL)
    • Python (pandas, pyodbc, sqlalchemy, seaborn)
    • Power BI Desktop + Power BI Service

    Diagrama de ETL del proyecto

    1. Exploración profunda de la base de datos

    Lo primero y más importante: nunca empieces a extraer datos sin entenderlos al 100 %.

    Acciones realizadas:

    • Generé un script SQL individual de exploración para cada una de las 18 tablas relevantes. Cada script incluye:
      • Descripción de las variables
      • Rango de fechas
      • Valores únicos y ejemplos de las columnas clave
      • Identificación de claves primarias y foráneas reales (aunque no estén definidas en la BD)

    Como resultado se creó un Diccionario de Datos completo en Excel y un diagrama relacional.

    2. Diseño del Modelo Analítico

    Definición de un modelo dimensional robusto, siguiendo las mejores prácticas de arquitectura BI. Para garantizar un rendimiento óptimo en Power BI y un análisis consistente, se diseñó un modelo en estrella (Star Schema), manteniendo únicamente algunas jerarquías naturales con forma de snowflake cuando aportaban claridad sin afectar el rendimiento.

    Definición de la Granularidad del Hecho

    Establecer la granularidad exacta de cada tabla de hechos:

    • Ventas: transacción por línea de pedido.
    • Inventario: snapshot por producto y almacén.
    • Finanzas: nivel contable por periodo.
    • Encuestas: respuesta por cliente.

    Definición de Métricas y KPIs Clave

    Se seleccionaron los indicadores esenciales para la visión 360°:

    • Ventas Totales (Sales Amount)
    • Coste del Producto (Total Product Cost)
    • Beneficio Bruto (Profit)
    • Margen (%)
    • Tendencias temporales: variaciones y crecimiento frente a periodos anteriores (YOY/MOM)
    • Valor Medio del Pedido (Average Order Value / AOV)
    • Rotación de Inventario y Alertas de Bajo Stock
    • Presupuesto vs Real (indicadores financieros)
    • Efectividad de Promociones

    Entregables Generados

    Durante esta fase se produjeron los documentos técnicos clave del proyecto:

    • Modelo_Dimensional: esquema visual del modelo en estrella.
    • Documento de Requerimientos Funcionales (.pdf): base formal para validar las necesidades del negocio con cada área.

    3. Extracción desde SQL Server

    Preparación de datos mediante vistas SQL

    Se crearon vistas en SQL Server, segun el documento de Requerimientos, que estructuran y preparan los datos para su posterior procesamiento en Python y análisis en Power BI.

    Las vistas incluyen:

    • Claves y relaciones: Se conservaron solo las necesarias para unir hechos y dimensiones sin duplicar información.
    • Atributos descriptivos y demográficos: Se mantuvieron columnas relevantes para análisis y segmentación (edad, género, ingresos, educación, categoría de producto), mientras que la información sensible de clientes se anonimizó o eliminó.
    • Métricas y valores numéricos: Se seleccionaron columnas necesarias para calcular KPIs como ventas, margen, ticket promedio, inventario y cumplimiento de cuotas.

    Además, se aplicaron transformaciones básicas dentro de las vistas, como el cálculo de edad a partir de la fecha de nacimiento y la unificación de jerarquías de productos, simplificando la carga y limpieza de datos en Python.

    Resultado: Una capa de datos limpia, consistente y segura, lista para análisis avanzado en Python y visualización interactiva en Power BI, cumpliendo los objetivos del dashboard ejecutivo.

    4. Limpieza y Transformación de Datos

    En esta fase se consolidan los datos de SQL Server y se preparan para su análisis en Power BI, asegurando datasets limpios, consistentes y listos para modelado.

    Extracción de Datos

    Se crearon cuadernos de Jupyter para extraer vistas SQL a CSV:

    • Conexión a SQL Server mediante SQLAlchemy.
    • Exportación de cada vista a /data/raw, eliminando prefijos vw_ en los nombres.
    • Mensajes de seguimiento en consola para validar éxito o fallo de cada extracción.

    Carga y Exploración

    Los CSV se cargaron en DataFrames de Pandas para exploración:

    • Inspección de registros y tipos de datos.
    • Identificación de valores nulos e inconsistencias.
    • Funciones reutilizables (CheckData) para revisión rápida de los datasets.

    Limpieza y Transformación

    Se aplicaron transformaciones consistentes por tabla de hechos:

    • Conversión de tipos, fechas y valores categóricos.
    • Tratamiento de nulos mediante reglas específicas según columna y tipo.
    • Estilo de limpieza coherente con los notebooks iniciales, mostrando resúmenes y primeras filas.

    Automatización del ETL

    Se desarrollaron scripts para consolidar el flujo:

    1. config.py: define rutas de trabajo y conexión a SQL Server con SQLAlchemy.
    2. etl_pipeline.py: ejecuta automáticamente:
      • Carga consultas SQL.
      • Genera DataFrames.
      • Limpia los datos según reglas definidas.
      • Guarda datasets limpios en /data/processed.
      • Proporciona logs y seguimiento en consola.

    Este enfoque permite reproducir todo el proceso con un solo comando, manteniendo la trazabilidad y calidad de los datos para análisis en Power BI.

    5. Exportación de Datasets Limpios

    Tras la limpieza y transformación, los datasets se preparan para su consumo en Power BI:

    • Se exportan en formato Parquet (recomendado por eficiencia) o CSV comprimido para reducir espacio.
    • Cada tabla del modelo se guarda en un archivo independiente: dimCliente.parquet, factVentas.parquet, etc.
    • Se incluye la fecha de generación en el nombre del archivo o en una tabla de control.
    • Se mantiene un registro maestro de datasets (datasets_control.xlsx) que documenta: nombre del archivo, número de filas y columnas, fecha de exportación.
    • Todos los archivos se almacenan en /data/final/, listos para el análisis.

    Este procedimiento asegura que los datos sean consistentes, auditables y fácilmente reutilizables por cualquier miembro del equipo BI.

    6. Importación y Modelo en Power BI

    La fase final consiste en construir el modelo semántico para análisis y reporting:

    • Se importan todos los archivos Parquet al proyecto de Power BI.
    • Se define un modelo estrella con relaciones claras entre hechos y dimensiones.
    • Columnas técnicas o auxiliares se ocultan para mejorar la experiencia del usuario.
    • Se crean medidas DAX avanzadas para KPIs clave: ventas, margen, cuotas, inventario, etc.
    • Se aplican optimizaciones de rendimiento: agregaciones, filtrado y segmentación, para mejorar la velocidad y la eficiencia.

    El resultado es un archivo Proyecto_Ventas.pbix completamente funcional y un documento de Modelo de Datos (Modelo de Datos en Power BI.pdf) con capturas de relaciones, medidas y estructura general, que permite navegar, analizar y tomar decisiones basadas en datos de manera rápida y confiable.

  • Space launch optimization with Machine Learning

    Space launch optimization with Machine Learning

    Predicción de Aterrizaje de Cohetes (Capstone IBM Data Science Professional Certificate)

    ¿Es posible predecir si la primera etapa de un Falcon 9 aterrizará con éxito?

    Este proyecto final del certificado profesional de IBM (septiembre 2025) desarrolla un pipeline completo de Ciencia de Datos para determinar la viabilidad del aterrizaje de la primera etapa, factor clave para reducir los costes de lanzamiento de SpaceX.

    Puntos clave del flujo de trabajo:

    • Ingeniería de Datos: Extracción híbrida mediante la API oficial de SpaceX y Web Scraping con BeautifulSoup.
    • Análisis y Visualización: EDA profundo con Pandas, análisis geoespacial de sitios de lanzamiento con Folium y consultas complejas en SQLite.
    • Machine Learning: Optimización de hiperparámetros mediante GridSearchCV para comparar modelos de Regresión Logística, SVM, Árboles de Decisión y KNN.
    • Producto Final: Dashboard interactivo desarrollado en Plotly Dash para la monitorización de métricas de éxito.
    • Stack técnico: Python (Pandas, Scikit-Learn), SQL, Folium, Plotly Dash.

    App del Proyecto:

    La aplicación del proyecto realizada en Flask y Plotly incluye un simulador interactivo que permite predicciones con dos modelos diferentes de Machine Learning. Puedes visitar la aplicacion en mi VPS personal Project Portal en: https://projects.fernandorioseco.es

    Enlace al repositorio del proyecto:

    https://github.com/fer78/Space-Launch-Optimization-with-Machine-Learning.git

    Escenario y visión general del proyecto

    La era espacial comercial ya ha llegado. Las empresas están haciendo que los viajes espaciales sean asequibles para todos:

    • Vingin Galactic está ofreciendo vuelos espaciales suborbitales.
    • Rocket Lab es un proveedor de satélites pequeños.
    • Blue Origin fabrica cohetes reutilizables suborbitales y orbitales.
    • SpaceX es la más exitosa, envía naves tripuladas espaciales a la Estación Espacial Internacional, sus satélites Starlink proporcionan internet satelital en todo el mundo.

    Una razón por la cual SpaceX puede hacer estas operaciones es porque ha logrado que sus lanzamientos sean más económicos. Recientemente, anunció en su sitio web el lanzamiento del nuevo cohete Falcon 9 con un coste de 62 millones de dólares, mientras que otras empresas pueden hacerlo con costes de más de 150 millones.

    Gran parte de este ahorro es debido a que SpaceX puede reutilizar la primera etapa del lanzamiento. Con este dato, si podemos determinar si la primera etapa aterrizará, podemos determinar el coste de un lanzamiento.

    En la infografía se pueden apreciar las diferentes partes del cohete:

    • Carga útil: contiene el elemento que se desea llevar al espacio.
    • Segunda etapa: ayuda a llevar la carga útil a la órbita.
    • Primera etapa: Realiza la mayor parte del trabajo de llevar la carga útil a la órbita, es la parte más grande y más costosa.

    A diferencia de otros proveedores de cohetes, el Falcon 9 puede recuperar la primera etapa. En ocasiones no tiene éxito durante el aterrizaje y se destruye. En otras ocasiones, la propia empresa destruye la primera etapa debido a los parámetros de la misión como la carga útil. La orbita y el cliente.

    En este proyecto asumirás el papel de un científico de datos que trabaja para una nueva empresa de cohetes: SpaceY, que quiere competir con SpaceX.

    SpaceY fue fundada por el empresario industrial Allon Mask.

    Tu trabajo es determinar el éxito de cada lanzamiento, recopilando información sobre la competencia y determinando si se reutilizará la primera etapa.

    En lugar de usar la ciencia de cohetes para determinar si la primera etapa aterrizará con éxito. Entrenarás un modelo de aprendizaje automático y usarás información pública para predecir si SpaceY reutilizará la primera etapa.

    En la primera fase de este proyecto, se construyó un dataset completo y estructurado a partir de datos públicos de todos los lanzamientos históricos de SpaceX, utilizando exclusivamente su API REST oficial (v4).

    Parte 1: Extracción de datos mediante peticiones HTTP a la API de SpaceX

    Se realizó una petición GET al endpoint https://api.spacexdata.com/v4/launches/past para obtener el histórico completo de lanzamientos.

    Dado que la respuesta contiene únicamente identificadores (rocket_id, payload_id, launchpad_id, core_id), se implementó un proceso de data enrichment mediante llamadas secundarias a los endpoints específicos:

    • /v4/rockets/{id} → nombre del booster (Falcon 9, Falcon 1, etc.)
    • /v4/launchpads/{id} → nombre del sitio de lanzamiento, longitud y latitud
    • /v4/payloads/{id} → masa de la carga útil (kg) y órbita destino (LEO, GTO, ISS, etc.)
    • /v4/cores/{id} → información crítica del core: éxito del aterrizaje, tipo de aterrizaje (RTLS, ASDS, océano), uso de grid fins y landing legs, bloque del booster, número de reusos, serial del core, etc.

    Transformación y normalización de datos

    • Se utilizó pd.json_normalize() para aplanar la respuesta JSON anidada en un DataFrame plano.
    • Se filtraron lanzamientos con múltiples cores o payloads (casos excepcionales de Falcon Heavy o misiones con rideshare) para mantener consistencia en el análisis de la primera etapa.

    Construcción del dataset final de entrenamiento

    Se creó un diccionario estructurado con las siguientes variables enriquecidas:

    • FlightNumber, Date, BoosterVersion
    • PayloadMass, Orbit, LaunchSite, Longitude, Latitude
    • Outcome (éxito/fracaso del aterrizaje + tipo), Flights, GridFins, Reused, Legs, LandingPad
    • Block, ReusedCount, Serial

    Este diccionario se convirtió en un DataFrame final (launch_df).

    Limpieza y filtrado específico

    • Se eliminaron todos los lanzamientos de Falcon 1, conservando únicamente Falcon 9.
    • Se reindexó la columna FlightNumber de forma secuencial (1 a n).
    • Se realizó imputación de valores faltantes en PayloadMass utilizando la media global de la columna, manteniendo intencionalmente los valores None en LandingPad (indicando aterrizajes oceánicos sin plataforma).

    El resultado es un dataset limpio, estructurado y enriquecido (dataset_part_1.csv) con 90 lanzamientos de Falcon 9 hasta noviembre de 2020, listo para las siguientes fases de análisis exploratorio, feature engineering y modelado predictivo del éxito del aterrizaje de la primera etapa.

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 90 entries, 0 to 89
    Data columns (total 17 columns):
     #   Column          Non-Null Count  Dtype  
    ---  ------          --------------  -----  
     0   FlightNumber    90 non-null     int64  
     1   Date            90 non-null     object 
     2   BoosterVersion  90 non-null     object 
     3   PayloadMass     90 non-null     float64
     4   Orbit           90 non-null     object 
     5   LaunchSite      90 non-null     object 
     6   Outcome         90 non-null     object 
     7   Flights         90 non-null     int64  
     8   GridFins        90 non-null     bool   
     9   Reused          90 non-null     bool   
     10  Legs            90 non-null     bool   
     11  LandingPad      64 non-null     object 
     12  Block           90 non-null     float64
     13  ReusedCount     90 non-null     int64  
     14  Serial          90 non-null     object 
     15  Longitude       90 non-null     float64
     16  Latitude        90 non-null     float64
    dtypes: bool(3), float64(4), int64(3), object(7)
    memory usage: 10.2+ KB
    

    Parte 2: Web Scraping de registros históricos desde Wikipedia

    En esta segunda fase del proyecto se construyó un dataset alternativo y complementario mediante web scraping de la página de Wikipedia “List of Falcon 9 and Falcon Heavy launches”, con el objetivo de obtener una fuente independiente a la API oficial de SpaceX y permitir validación cruzada de los datos.

    Tecnologías y herramientas utilizadas

    • requests con cabecera User-Agent personalizada para evitar bloqueos.
    • BeautifulSoup4 como parser HTML.
    • Funciones auxiliares propias para la extracción robusta de datos en celdas con ruido típico de Wikipedia (referencias, superíndices, enlaces, formato inconsistente).

    Proceso de parsing implementado

    1. Identificación de tablas relevantes
    2. Extracción automática de nombres de columnas
    3. Parsing fila por fila con lógica robusta
    4. Construcción del dataset

    Se obtuvo un DataFrame completo con 121 lanzamientos de Falcon 9 y Falcon Heavy hasta junio de 2021, incluyendo información crítica para el modelo predictivo:
    Booster landing (variable objetivo), Payload mass, Orbit, Launch site, Version Booster, etc.

    El archivo final se exportó como spacex_web_scraped.csv, listo para su uso en análisis exploratorio, fusión con el dataset de la API y entrenamiento de modelos de clasificación.

    Parte 3: Data Wrangling y definición de la variable objetivo

    En esta fase del proyecto se realizó un análisis exploratorio inicial (EDA) y el data wrangling necesario para convertir el dataset crudo (obtenido vía API en la Parte 1) en un conjunto de datos listo para modelado supervisado de clasificación.

    Principales tareas realizadas

    1. Carga y diagnóstico inicial del dataset
    2. Análisis exploratorio univariante
      • Distribución de lanzamientos por sitio de lanzamiento (LaunchSite):
      • Distribución por tipo de órbita (Orbit), destacando la predominancia de LEO, ISS, GTO y SSO, y la presencia de órbitas menos frecuentes (HEO, MEO, ES-L1, etc.).
    3. Análisis profundo de la variable Outcome (resultado del aterrizaje de la primera etapa)
    4. Creación de la variable objetivo binaria (Class)
      • Se definió el conjunto de resultados fallidos: bad_outcomes = {'False ASDS', 'False RTLS', 'False Ocean', 'None ASDS', 'None None'}
      • Se generó la columna Class mediante list comprehension: python landing_class = [0 if outcome in bad_outcomes else 1 for outcome in df['Outcome']] df['Class'] = landing_class
      • Class = 1 → primera etapa recuperada con éxito
      • Class = 0 → no recuperada (fallo o misión expendable)
    5. Cálculo de la tasa de éxito global

    Se exportó el dataset enriquecido como dataset_part_2.csv, que incluye:

    • Todas las variables originales enriquecidas desde la API
    • La nueva variable binaria Class lista para ser usada como target en modelos de clasificación supervisada
    • Ningún valor faltante crítico (solo LandingPad mantiene None intencionadamente)

    Parte 4: Exploratory Data Analysis (EDA)

    En esta fase clave del proyecto se realizó un análisis exploratorio exhaustivo (EDA) y la ingeniería de variables (feature engineering) definitiva para preparar los datos de cara al modelado predictivo supervisado.

    Se crean funciones auxiliares propias para aplicar un estilo visual elegante, consistente y profesional (ejes limpios, rejilla vertical rosa, leyenda personalizada rojo/verde para fallos y éxitos)

    Análisis exploratorio realizado

    FlightNumber vs PayloadMass

    Scatter plot que muestra claramente la curva de aprendizaje de SpaceX: los primeros lanzamientos presentan más fallos, mientras que a partir del vuelo ~20–25 la tasa de éxito se vuelve muy alta, incluso con cargas útiles pesadas.

    FlightNumber vs LaunchSite

    • CCAFS SLC-40 es el sitio dominante en número de lanzamientos.
    • Los fallos están concentrados en los primeros vuelos de cada plataforma.
    • KSC LC-39A y VAFB SLC-4E muestran tasas de éxito cercanas al 100 % en lanzamientos recientes.

    PayloadMass vs LaunchSite

    Observación clave: desde VAFB SLC-4E nunca se han lanzado cargas pesadas (>10 000 kg), lo que explica su tasa de éxito casi perfecta.

    Success Rate by Orbit Type

    Órbitas principales:

    Bar chart revelador:

    • LEO e ISS → tasa de éxito ≈ 100 %
    • GTO → tasa más baja (misión más energética, menor margen para recuperación)
    • SSO y polar → éxito muy alto

    FlightNumber vs Orbit

    • En LEO el éxito crece claramente con el número de vuelo.
    • En GTO la relación no es tan evidente: incluso en vuelos recientes hay tanto éxitos como fallos, debido a la mayor dificultad energética.

    PayloadMass vs Orbit

    • Con cargas útiles pesadas, la tasa de aterrizaje exitoso o positivo es mayor para las órbitas Polar, LEO e ISS.
    • Sin embargo, para GTO, resulta difícil distinguir entre aterrizajes exitosos y fallidos, ya que ambos resultados se presentan de manera equilibrada.”

    Evolución anual de la tasa de éxito (2013–2020)

    Line chart que muestra una mejora sostenida y casi lineal desde ~50 % en 2013 hasta >95 % en 2020, reflejando la madurez tecnológica de la reutilización.

    Feature Engineering

    1. Selección de variables relevantes para el modelo:
    2. One-Hot Encoding aplicado a las variables categóricas:
      • Orbit (11 categorías)
      • LaunchSite (3)
      • LandingPad (5)
      • Serial (53 identificadores únicos de boosters)
    3. Conversión completa del dataset a tipo float64 para compatibilidad con algoritmos de Machine Learning.

    Se generó el dataset definitivo dataset_part_3.csv con 104 columnas numéricas y 90 observaciones, completamente limpio, codificado y listo para entrenamiento de modelos de clasificación.

    Parte 5: Análisis exploratorio con SQL

    Aunque el núcleo del proyecto se basa en Machine Learning con Python, esta fase complementaria demuestra una competencia adicional muy valorada en ciencia de datos: el dominio de consultas SQL para análisis exploratorio y extracción de insights directamente desde bases de datos relacionales.

    Objetivo

    Utilizar SQLite como motor de base de datos en entorno local para cargar el dataset completo de misiones SpaceX y resolver 10 consultas analíticas reales mediante lenguaje SQL puro.

    Infraestructura implementada

    • Creación de base de datos en memoria: my_data1.db
    • Carga del archivo CSV original mediante pandas.to_sql()
    • Limpieza inicial: eliminación de filas con fecha nula
    • Uso de la extensión mágica %sql para ejecutar consultas directamente desde Jupyter

    Consultas SQL realizadas y resultados clave obtenidos

    TaskConsulta realizadaInsight principal
    1DISTINCT Launch_Site4 sitios únicos: CCAFS SLC-40, KSC LC-39A, VAFB SLC-4E, CCAFS LC-40
    2Lanzamientos desde sitios que empiezan por ‘CCA’69 misiones (mayoría histórica)
    3Masa total de carga útil NASA (CRS)45 716 kg transportados en contratos CRS
    4Masa media de carga en versión F9 v1.12 534 kg (versión temprana con menor capacidad)
    5Primera fecha de aterrizaje exitoso en plataforma terrestre2015-12-22 (Flight 20 – hito histórico RTLS)
    6Boosters con éxito en dron ship y carga entre 4000–6000 kg9 casos (todos F9 FT Bxxxx)
    7Conteo total de éxitos y fallos de misión98 éxitos – 1 fallo (CRS-7 explosión)
    8Booster_Version con máxima carga útil13 600 kg → F9 B5 B1048.2 y B1049.2 (misiones Starlink)
    9Fallos en dron ship durante 2015Enero (CRS-7) y Marzo (SES-9) – dos intentos fallidos emblemáticos
    10Ranking de outcomes de aterrizaje (2010-06-04 → 2017-03-20)Success (drone ship): 14, Success (ground pad): 9, Failure: 7, etc.

    Parte 6: Visualización interactiva de sitios de lanzamiento con Folium

    En esta fase del proyecto se implementó un análisis geoespacial interactivo utilizando la librería Folium, con el objetivo de explorar visualmente la ubicación estratégica de los sitios de lanzamiento de SpaceX y su relación con el éxito del aterrizaje de la primera etapa.

    Tecnologías utilizadas

    • folium + plugins: MarkerCluster, MousePosition, DivIcon
    • Cálculo de distancias geodésicas mediante la fórmula del haversine
    • Visualización interactiva en mapa base OpenStreetMap

    Tareas realizadas

    Marcado de los 4 sitios de lanzamiento activos de Falcon 9.

    • CCAFS LC-40
    • CCAFS SLC-40
    • KSC LC-39A (Kennedy Space Center)
    • VAFB SLC-4E (Vandenberg, California)

    Cálculo y representación de distancias a infraestructuras críticas

    Visualización interactiva de éxitos y fallos por lanzamiento

    • Se creó un MarkerCluster que agrupa automáticamente los marcadores según el nivel de zoom.
    • Cada lanzamiento se representa con un marcador cuyo color indica el resultado:
      • Verde → Class = 1 (aterrizaje exitoso)
      • Rojo → Class = 0 (fallo o misión expendable)
    • Popup informativo con: sitio de lanzamiento y resultado.

    Insights geoespaciales obtenidos

    • Todos los sitios de lanzamiento están ubicados en la costa (Atlántico o Pacífico) → minimiza riesgo poblacional.
    • Distancia a ciudades siempre superior a 40–50 km → protocolo de seguridad estándar.
    • Proximidad a autopistas (<1 km) y vías férreas → optimización logística.
    • VAFB SLC-4E (California) presenta 100 % de éxito en aterrizajes, parcialmente explicado por cargas más ligeras y menor densidad de tráfico aéreo.

    Parte 7: Dashboard Interactivo con Plotly Dash

    En la fase final del proyecto se desarrolló un dashboard web interactivo utilizando Plotly Dash, permitiendo a cualquier usuario (técnico o no técnico) explorar de forma dinámica los datos históricos de lanzamientos de SpaceX y los factores que influyen en el éxito del aterrizaje de la primera etapa.

    Tecnologías utilizadas

    • Dash (by Plotly) – Framework Python para aplicaciones web analíticas
    • Plotly Express – Gráficos interactivos de alto nivel
    • Pandas – Manipulación de datos
    • Despliegue local en http://0.0.0.0:8050

    Funcionalidades implementadas

    1. Dropdown de selección de sitio de lanzamiento
    2. Gráfico de tarta dinámico (Pie Chart)
    3. Range Slider de masa de carga útil (0 – 10 000 kg)
    4. Gráfico de dispersión interactivo (Scatter Plot)

    Callbacks implementados

    • @app.callback para el gráfico de tarta → responde al site-dropdown
    • @app.callback doble entrada (site-dropdown + payload-slider) → actualiza el scatter en tiempo real

    Parte Final: Modelado Predictivo y Selección del Mejor Algoritmo

    En la fase culminante del proyecto se entrenaron y evaluaron cuatro algoritmos de clasificación supervisada para predecir si la primera etapa del Falcon 9 aterrizará con éxito (Class = 1) o no (Class = 0), utilizando el dataset completamente preprocesado (104 variables one-hot + numéricas estandarizadas).

    Pipeline de Machine Learning implementado

    1. Estandarización de todas las variables con StandardScaler
    2. División train/test (80 % train – 20 % test, random_state=2)
    3. Búsqueda exhaustiva de hiperparámetros mediante GridSearchCV con 10-fold cross-validation
    4. Evaluación final sobre el conjunto de prueba (18 muestras)

    Modelos entrenados y resultados

    ModeloMejores hiperparámetros (GridSearchCV)Accuracy Validación (CV)Accuracy TestObservaciones
    Logistic RegressionC=0.01, penalty='l2', solver='lbfgs'84.64 %83.33 %Muy estable, pocos falsos positivos
    SVMC=1.0, gamma=0.0316, kernel='sigmoid'84.82 %83.33 %Rendimiento prácticamente idéntico a Regresión Logística
    Decision Treecriterion='gini', max_depth=6, max_features='sqrt', min_samples_leaf=4, splitter='random'88.93 %83.33 %Mejor en validación (posible leve sobreajuste)
    K-Nearest Neighborsn_neighbors=10, algorithm='auto', p=1 (distancia Manhattan)84.82 %83.33 %Robusto y consistente

    Resultado clave: Los cuatro modelos alcanzan 83.33 % de accuracy en el conjunto de prueba (15 aciertos de 18 muestras), lo que representa un rendimiento excelente considerando el reducido tamaño del dataset (90 observaciones totales).

    Análisis de matrices de confusión

    • Todos los modelos cometen 3 falsos positivos (predicen éxito cuando en realidad falló).
    • Solo 0–1 falsos negativos → priorizan correctamente la seguridad (no decir que fallará cuando en realidad aterriza).
    • El Decision Tree es el que mejor generaliza en validación, pero en test empata con los demás.

    Conclusión final del proyecto

    Se ha construido con éxito un sistema predictivo robusto capaz de estimar, con más del 83 % de precisión, si la primera etapa del Falcon 9 será recuperada en una misión futura, basándose únicamente en parámetros públicos conocidos antes del lanzamiento (sitio, órbita, masa de carga, versión del booster, etc.).

    Este modelo tiene aplicaciones reales:

    • Estimación de costes de lanzamiento (reutilizable vs. expendable)
    • Optimización de contratos comerciales
    • Apoyo en la toma de decisiones de clientes frente a competidores

  • Random Variables

    Las variables aleatorias son uno de los pilares fundamentales de la probabilidad y la estadística, y entenderlas es clave para trabajar con datos, modelos matemáticos, inferencia estadística o machine learning. En este artículo veremos de forma clara qué son, cómo se clasifican, cómo funcionan sus funciones asociadas (PMF y CDF), y cómo se calculan y visualizan con Python.

    ¿Qué es una Variable Aleatoria?

    Una variable aleatoria es una función que asigna valores numéricos a los resultados de un experimento aleatorio.

    • Si lanzas un dado, la variable aleatoria puede ser “el número que sale”.
    • Si tiras una moneda, la variable aleatoria puede ser “1 si sale cara, 0 si sale cruz”.
    • Si cuentas cuántos clientes entran a un local por hora, esa cantidad diaria también es una variable aleatoria.

    Clasificación principal:

    1. Variables aleatorias discretas → toman valores contables: 0,1,2,…
    2. Variables aleatorias continuas → toman infinitos valores dentro de un intervalo: altura, tiempos, pesos…

    Variables aleatorias discretas

    Las variables aleatorias discretas son aquellas que toman un número finito o contablemente infinito de valores distintos. Cada uno de estos valores tiene una probabilidad asociada, y la suma de todas estas probabilidades debe ser igual a uno. Pueden adoptar valores que se pueden contar, como 0, 1, 2, …, n, o una lista de valores específicos como {-3, -1, 0, 1, 5}.

    Ejemplos comunes:

    • Número de caras al lanzar una moneda 10 veces.
    • Número de clientes en una hora.
    • Número de errores en un texto.

    Variable Aleatoria de Bernoulli

    Una variable aleatoria de Bernoulli es un tipo específico de variable aleatoria discreta que sólo toma dos posibles valores, típicamente 0 y 1, para representar los resultados de un único ensayo de Bernoulli.

    Una variable de Bernoulli X se define de la siguiente manera:

    • \(X=1\) con probabilidad \(p\) (éxito).
    • \(X=0\) con probabilidad \(1−p\) (fracaso).

    Simulación en Python

    Vamos a construir una función en Python que simule un experimento de Bernoulli, el cual podría representar, por ejemplo, el lanzamiento de una moneda:

    import numpy as np
    
    def bernoulli_trial(p=0.5):
        """Simula un experimento de Bernoulli.
            Args:
        p (float): Probabilidad de éxito (por defecto 0.5).
            Returns:
        int: 1 si el experimento resulta en éxito, 0 en caso contrario.
        """
        return 1 if np.random.rand() <= p else 0
    

    En esta función, np.random.rand() genera un número aleatorio entre 0 y 1, y compara este número con la probabilidad de éxito p. Si el número generado es menor o igual a p, el resultado es un éxito (1); de lo contrario, es un fracaso (0).

    Simulación y Análisis de Resultados

    Podemos usar esta función para realizar múltiples ensayos y observar la frecuencia de éxitos, lo que nos permite estimar la probabilidad de éxito de la moneda:

    def simulate_bernoulli_trials(n, p=0.5):
        """Simula n ensayos de Bernoulli y reporta la frecuencia de éxitos.
            Args:
        n (int): Número de ensayos.
        p (float): Probabilidad de éxito.
        
        Returns:
        float: Frecuencia de éxitos.
        """
        results = [bernoulli_trial(p) for _ <strong>in</strong> range(n)]
        return sum(results) / n
    
    # Simular 1000 lanzamientos de una moneda con p = 0.7
    n_trials = 1000
    success_prob = 0.612199
    frequency_of_success = simulate_bernoulli_trials(n_trials, success_prob)
    
    print(f"La frecuencia de éxito estimada es {frequency_of_success:.2f}")

    Output:

    La frecuencia de éxito estimada es 0.71

    Este código simula 1000 lanzamientos de una moneda donde la probabilidad de obtener cara (éxito) es del 70%. La función simulate_bernoulli_trials devuelve la frecuencia de éxitos, que debería acercarse a 0.7 a medida que el número de ensayos aumenta.

    Visualización de la Convergencia

    Para visualizar cómo la frecuencia de éxitos converge a la probabilidad real, podríamos realizar múltiples simulaciones aumentando progresivamente el número de ensayos y graficar los resultados:

    import matplotlib.pyplot as plt
    
    trial_counts = [10, 50, 100, 500, 1000, 5000, 10000]
    frequencies = [simulate_bernoulli_trials(count, success_prob) for count <strong>in</strong> trial_counts]
    
    plt.figure(figsize=(10, 5))
    plt.plot(trial_counts, frequencies, marker='o', linestyle='-')
    plt.axhline(y=success_prob, color='r', linestyle='--')
    plt.title('Convergencia de la Frecuencia de Éxitos a la Probabilidad Real')
    plt.xlabel('Número de Ensayos')
    plt.ylabel('Frecuencia de Éxitos')
    plt.xscale('log')
    plt.show()
    
    Visualización de la Convergencia

    Este gráfico mostrará cómo la frecuencia de éxitos se estabiliza y converge hacia la probabilidad real de éxito (0.612199 en este caso) a medida que aumenta el número de ensayos, ilustrando la ley de los grandes números.

    Función de Masa de Probabilidad (PMF)

    La Función de Masa de Probabilidad (PMF) es una función que describe la probabilidad de que una variable aleatoria discreta tome un valor específico. Es una función que devuelve la probabilidad de que una variable aleatoria discreta sea exactamente igual a algún valor.​ Es una función que asocia a cada punto de su espacio muestral X la probabilidad de que esta lo asuma. La función de probabilidad suele ser el medio principal para definir una distribución de probabilidad discreta, y tales funciones existen para variables aleatorias escalares o multivariantes, cuyo dominio es discreto.

    La función de masa de probabilidad de un dado. Todos los números tienen la misma probabilidad de aparecer cuando este es tirado.

    pmf_dado

    Por ejemplo, supongamos que lanzamos una moneda justa varias veces y contamos el número de caras. La función de masa de probabilidad que describe la probabilidad de cada resultado posible (p. ej., 0 caras, 1 cara, 2 caras, etc.) se denomina distribución binomial. Los parámetros para la distribución binomial son:

    • n para el número de intentos (por ejemplo, n=10 si lanzamos una moneda 10 veces)
    • p para la probabilidad de éxito en cada prueba (probabilidad de observar un resultado particular en cada prueba. En este ejemplo, p= 0,5 porque la probabilidad de observar caras en un lanzamiento de moneda justo es 0,5)

    Si lanzamos una moneda normal 10 veces, decimos que el número de caras observadas sigue una distribución Binomial(n=10, p=0.5). El siguiente gráfico muestra la función de masa de probabilidad para este experimento. Las alturas de las barras representan la probabilidad de observar cada resultado posible calculado por el PMF.

    Veamos cómo cambia la forma de la distribución binomial a medida que cambia el tamaño de la muestra.

    Utilice el control deslizante para cambiar el valor de x lanzamientos de moneda justos, entre uno y diez. Las alturas de las barras resultantes representan la probabilidad de observar diferentes valores de caras en x número de lanzamientos de moneda justos. Puede pasar el cursor sobre cada barra y ver el valor numérico real de la altura de la barra. Las barras más altas representan resultados más probables.

    Observe que a medida que x aumenta, las barras se hacen más pequeñas. Esto se debe a que la suma de las alturas de todas las barras siempre será igual a 1. Entonces, cuando x es mayor, el número de caras que podemos observar aumenta y la probabilidad debe dividirse entre más valores.

    Binomial Distribution: Calculating Probability of a Given Number of Heads

    Calcular probabilidades usando Python

    El método binom.pmf() de la biblioteca scipy.stats se puede utilizar para calcular el PMF de la distribución binomial en cualquier valor. Este método toma 3 valores:

    • x: el valor del interés
    • n: el número de ensayos
    • p: la probabilidad de éxito

    Por ejemplo, supongamos que lanzamos una moneda normal 10 veces y contamos el número de caras. Podemos usar la función binom.pmf() para calcular la probabilidad de observar 6 cabezas de la siguiente manera:

    # import necessary library
    import scipy.stats as stats
    
    # st.binom.pmf(x, n, p)
    print(stats.binom.pmf(6, 10, 0.5))

    Output

    0.205078

    Observe que dos de los tres valores que entran en el método stats.binomial.pmf() son los parámetros que definen la distribución binomial: n representa el número de intentos y p representa la probabilidad de éxito.

    Uso de la función de masa de probabilidad en un rango

    Hemos visto que podemos calcular la probabilidad de observar un valor específico usando una función de masa de probabilidad. ¿Qué pasa si queremos encontrar la probabilidad de observar un rango de valores para una variable aleatoria discreta? Una forma de hacer esto es sumando la probabilidad de cada valor.

    Por ejemplo, digamos que lanzamos una moneda justa 5 veces y queremos saber la probabilidad de obtener entre 1 y 3 caras. Podemos visualizar este escenario con la función de masa de probabilidad:

    Binomial Distribution: Calculating Probability of a Range

    Podemos calcular esto usando la siguiente ecuación donde P(x) es la probabilidad de observar el número x de éxitos (cara en este caso):

    P(1 to 3 heads) = P(1<= X <=3)
    P(1 to 3 heads) = P(X=1) + P(X=2) + P(X=3)
    P(1 to 3 heads) = 0.1562 + 0.3125 + 0.3125
    P(1 to 3 heads) = 0.7812

    Visualicemos lo que significa tomar la probabilidad de un rango. Utilice los controles deslizantes para seleccionar un rango de valores que representen el número de caras que podríamos observar en 10 lanzamientos de moneda justos.

    Pruebe diferentes rangos para ver cómo cambian las probabilidades para diferentes valores. Pase el cursor sobre una barra individual para ver la altura de la barra (que corresponde a la probabilidad de que ocurra el valor).

    Binomial Distribution: Calculating Probability of a Range

    Función de masa de probabilidad en un rango usando Python

    Podemos utilizar el mismo método binom.pmf() de la biblioteca scipy.stats para calcular la probabilidad de observar un rango de valores. Como se mencionó en un ejercicio anterior, el método binom.pmf toma 3 valores:

    • x: el valor del interés
    • n: el número de ensayos
    • p: la probabilidad de éxito

    Por ejemplo, podemos calcular la probabilidad de observar entre 2 y 4 caras en 10 lanzamientos de moneda de la siguiente manera:

    import scipy.stats as stats
    
    # calculating P(2-4 heads) = P(2 heads) + P(3 heads) + P(4 heads) for flipping a coin 10 times
    print(stats.binom.pmf(2, n=10, p=.5) 
        + stats.binom.pmf(3, n=10, p=.5) 
        + stats.binom.pmf(4, n=10, p=.5))

    Output:

    0.366211

    También podemos calcular la probabilidad de observar menos de un cierto valor, digamos 3 caras, sumando las probabilidades de los valores debajo de él:

    import scipy.stats as stats
    
    # calculating P(less than 3 heads) = P(0 heads) + P(1 head) + P(2 heads) for flipping a coin 10 times
    print(stats.binom.pmf(0, n=10, p=.5) 
        + stats.binom.pmf(1, n=10, p=.5) 
        + stats.binom.pmf(2, n=10, p=.5))

    Output

    0.0546875

    Tenga en cuenta que debido a que nuestro rango deseado es inferior a 3 cabezas, no incluimos ese valor en la suma.

    Cuando hay muchos valores de interés posibles, esta tarea de sumar probabilidades puede resultar difícil. Si queremos saber la probabilidad de observar 8 o menos caras en 10 lanzamientos de moneda, debemos sumar los valores del 0 al 8:

    import scipy.stats as stats
    
    var = stats.binom.pmf(0, n = 10, p = 0.5) 
        + stats.binom.pmf(1, n = 10, p = 0.5) 
        + stats.binom.pmf(2, n = 10, p = 0.5) 
        + stats.binom.pmf(3, n = 10, p = 0.5) 
        + stats.binom.pmf(4, n = 10, p = 0.5) 
        + stats.binom.pmf(5, n = 10, p = 0.5) 
        + stats.binom.pmf(6, n = 10, p = 0.5) 
        + stats.binom.pmf(7, n = 10, p = 0.5) 
        + stats.binom.pmf(8, n = 10, p = 0.5)

    Output

    0.98926

    Esto implica una gran cantidad de código repetitivo. En su lugar, también podemos utilizar el hecho de que la suma de las probabilidades de todos los valores posibles es igual a 1:

    P(0to8heads) + P(9to10heads) = P(0to10heads) = 1
    P(0to8heads) = 1 − P(9to10heads)

    Ahora, en lugar de sumar 9 valores para las probabilidades entre 0 y 8 caras, podemos hacer 1 menos la suma de dos valores y obtener el mismo resultado:

    import scipy.stats as stats
    # less than or equal to 8
    1 - (stats.binom.pmf(9, n=10, p=.5) + stats.binom.pmf(10, n=10, p=.5))

    Output

    0.98926

    Función de distribución acumulativa

    La función de distribución acumulativa para una variable aleatoria discreta se puede derivar de la función de masa de probabilidad. Sin embargo, en lugar de la probabilidad de observar un valor específico, la función de distribución acumulativa proporciona la probabilidad de observar un valor específico O MENOS.

    Como se analizó anteriormente, las probabilidades de todos los valores posibles en una distribución de probabilidad dada suman 1. El valor de una función de distribución acumulativa en un valor dado es igual a la suma de las probabilidades menores que él, con un valor de 1 para la mayor número posible.

    CDF de Ejemplo para Diferentes Distribuciones

    • Distribución Discreta: Si una variable aleatoria 𝑋X es discreta, la CDF tiene saltos en los puntos donde la variable tiene una probabilidad distinta de cero.
    • Distribución Continua: Si una variable aleatoria 𝑋X es continua, la CDF es una función continua. Ejemplo: para una distribución normal con media 𝜇𝜇 y desviación estándar 𝜎𝜎, la CDF se representa usando la función de error, la cual es una integral de la función de densidad de probabilidad (PDF).

    Mostramos cómo se puede utilizar la función de masa de probabilidad para calcular la probabilidad de observar menos de 3 caras en 10 lanzamientos de moneda sumando las probabilidades de observar 0, 1 y 2 caras. La función de distribución acumulativa produce la misma respuesta al evaluar la función en CDF(X=2). En este caso, utilizar el CDF es más sencillo que el PMF porque requiere un cálculo en lugar de tres.

    La animación del enlace muestra la relación entre la función de masa de probabilidad y la función de distribución acumulativa. El gráfico superior es el PMF, mientras que el gráfico inferior es el CDF correspondiente. Al observar la gráfica de una CDF, cada valor del eje y es la suma de las probabilidades menores o iguales que él en la PMF.

    Enlace a la animacion

    Podemos usar una función de distribución acumulativa para calcular la probabilidad de un rango específico tomando la diferencia entre dos valores de la función de distribución acumulativa. Por ejemplo, para encontrar la probabilidad de observar entre 3 y 6 caras, podemos tomar la probabilidad de observar 6 o menos cabezas y restar la probabilidad de observar 2 o menos caras. Esto deja un remanente de entre 3 y 6 cabezas.

    La imagen de la derecha demuestra cómo funciona esto. Es importante tener en cuenta que para incluir el límite inferior en el rango, el valor que se resta debe ser uno menos que el límite inferior. En este ejemplo, queríamos saber la probabilidad de 3 a 6, que incluye 3.

    Enlace a la animacion

    Usando la función de distribución acumulativa en Python

    Podemos utilizar el método binom.cdf() de la biblioteca scipy.stats para calcular la función de distribución acumulativa. Este método toma 3 valores:

    • x: el valor de interés, buscando la probabilidad de este valor o menos
    • n: el tamaño de la muestra
    • p: la probabilidad de éxito

    Calcular matemáticamente la probabilidad de observar 6 o menos caras en 10 lanzamientos de moneda justos (0 a 6 caras) se parece a lo siguiente:

    P(6 or fewer heads) = P(0 to 6 heads)
    

    El codigo en Python es:

    import scipy.stats as stats
    
    print(stats.binom.cdf(6, 10, 0.5))
    

    Output

    0.828125
    

    Se puede pensar que calcular la probabilidad de observar entre 4 y 8 caras en 10 lanzamientos de moneda justos es tomar la diferencia del valor de la función de distribución acumulativa en 8 de la función de distribución acumulativa en 3:

    P(4 to 8 Heads) = P(0 to 8 Heads) − P(0 to 3 Heads)
    

    En Python utilizamos el codigo:

    import scipy.stats as stats
    
    print(stats.binom.cdf(8, 10, 0.5) - stats.binom.cdf(3, 10, 0.5))
    

    Output

    0.81738
    

    Para calcular la probabilidad de observar más de 6 caras en 10 lanzamientos de moneda justos, restamos el valor de la función de distribución acumulativa en 6 de 1. Matemáticamente, esto se parece a lo siguiente:

    P(more than 6 Heads) = 1 - P(6 or fewer Heads)
    

    Tenga en cuenta que “más de 6 cabezas” no incluye 6. En Python, calcularíamos esta probabilidad usando el siguiente código:

    import scipy.stats as stats
    print(1 - stats.binom.cdf(6, 10, 0.5))
    

    Output

    0.171875
    

    Tipos de Variables Aleatorias Discretas

    Los tipos de variables aleatorias discretas se clasifican comúnmente según las distribuciones de probabilidad que describen cómo se comportan los datos asociados a estas variables. Aquí describo algunas de las distribuciones más comunes y utilizadas para variables aleatorias discretas. En Python, la librería scipy.stats proporciona implementaciones de las PMFs para muchas distribuciones discretas comunes, lo que facilita su cálculo en la práctica. Estas funciones son extremadamente útiles para simulaciones, modelado estadístico y análisis probabilístico en una amplia gama de aplicaciones.

    1. Distribución Binomial:
      Usada cuando se están observando el número de éxitos en un número fijo de ensayos independientes y cada ensayo tiene solo dos posibles resultados (éxito o fracaso).
      La PMF de una distribución binomial describe la probabilidad de obtener un número específico de éxitos en un número fijo de ensayos independientes, cada uno con dos posibles resultados y una misma probabilidad de éxito.
    2. from scipy.stats import binom import numpy # Parámetros n = 10 # número de ensayos p = 0.5 # probabilidad de éxito size = 100 # muestras a generar # Generar Variables numpy.random.binomial(n, p, size) # Calcular PMF para un valor específico k = 5 # número de éxitos prob = binom.pmf(k, n, p) print(f"Probabilidad de {k} éxitos en {n} ensayos: {prob:.4f}")
    3. Distribución de Poisson: Aplica para modelar el número de eventos en un intervalo de tiempo o espacio fijo cuando estos eventos ocurren con una tasa media conocida y de manera independiente entre sí. La PMF de una distribución de Poisson mide la probabilidad de un número determinado de eventos ocurriendo en un intervalo fijo, dado que estos eventos ocurren con una tasa media conocida y de manera independiente. from scipy.stats import poisson import numpy lambda_ = 3 # tasa media de eventos por intervalo zize = 10 # número de muestras a generar. # Generar variables numpy.random.poisson(lambda_, size) # Calcular PMF k = 4 # número de eventos prob = poisson.pmf(k, lambda_) print(f"Probabilidad de {k} eventos: {prob:.4f}")
    4. Distribución Geométrica: Describe el número de ensayos necesarios para obtener el primer éxito en una secuencia de ensayos independientes, cada uno con dos posibles resultados. La PMF de una distribución geométrica describe la probabilidad de que el primer éxito ocurra en el ensayo 𝑘k from scipy.stats import geom import numpy p = 0.2 # probabilidad de éxito en cada ensayo zize = 10 # número de muestras a generar. # generar variable numpy.random.geometric(p, size) # Calcular PMF k = 5 # ensayo en el que ocurre el primer éxito prob = geom.pmf(k, p) print(f"Probabilidad de primer éxito en el intento {k}: {prob:.4f}")
    5. Distribución Binomial Negativa: Generaliza la distribución geométrica para contar el número de ensayos requeridos para obtener un número fijo de éxitos. Esta distribución cuenta cuántos ensayos son necesarios para lograr un número especificado de éxitos. from scipy.stats import nbinom import numpy r = 5 # número de éxitos deseados p = 0.5 # probabilidad de éxito en cada ensayo zize = 10 # número de muestras a generar. # generar variable numpy.random.negative_binomial(n, p, size) # Calcular PMF k = 10 # total de ensayos prob = nbinom.pmf(k-r, r, p) print(f"Probabilidad de alcanzar {r} éxitos en {k} ensayos: {prob:.4f}")
    6. Distribución Hipergeométrica: Modela el número de éxitos en una muestra de tamaño fijo tomada sin reemplazo de una población que consta de dos tipos de objetos (éxitos y fracasos).
      La PMF de la distribución hipergeométrica describe la probabilidad de obtener un número determinado de éxitos en una muestra extraída sin reemplazo de una población finita que consta de dos tipos de objetos. from scipy.stats import hypergeom import numpy M = 20 # tamaño total de la población n = 7 # número de éxitos en la población N = 12 # tamaño de la muestra # Calcular PMF k = 3 # número de éxitos observados en la muestra prob = hypergeom.pmf(k, M, n, N) print(f"Probabilidad de {k} éxitos en una muestra de {N}: {prob:.4f}") # generar variable ngood # número de elementos "buenos" en la población. nbad # número de elementos "malos" en la población. nsample # número de elementos a muestrear (sin reemplazo). size # número de muestras a generar. numpy.random.hypergeometric(ngood, nbad, nsample, size=None)
    7. Distribución Uniforme Discreta:
      • Todos los valores posibles de la variable tienen la misma probabilidad de ocurrir.
      • Parámetros: el mínimo y máximo valor que puede tomar la variable.
    8. Distribución de Bernoulli:
      • Un caso especial de la distribución binomial con un solo ensayo n=1n=1.
      • Parámetro: probabilidad de éxito pp.
    9. Distribución Multinomial:
      • Extensión de la distribución binomial para situaciones en las que cada ensayo puede resultar en más de dos categorías.
      • Parámetros: número de ensayos nn y vector de probabilidades pp para cada categoría.

    Estas distribuciones permiten modelar una gran variedad de procesos y fenómenos en diversos campos como la biología, ingeniería, economía, ciencias sociales, y más. Cada tipo de distribución proporciona un modelo estadístico que se adapta a las características específicas de los datos y la naturaleza del experimento o la observación realizada.

    Variables aleatorias continuas

    Las variables aleatorias continuas son aquellas que pueden tomar cualquier valor numérico dentro de un intervalo o conjunto de intervalos, a diferencia de las variables aleatorias discretas que tienen valores contables y separados. Este tipo de variables es fundamental en estadística y probabilidad para modelar fenómenos que requieren una escala de medida infinita y continua.

    Características Principales

    1. Valores no Contables: Las variables aleatorias continuas pueden adoptar cualquier valor dentro de un rango específico, que puede ser finito o infinito.
    2. Función de Densidad de Probabilidad (PDF): A diferencia de las variables discretas que usan una función de probabilidad de masa, las continuas se describen mediante una función de densidad de probabilidad. Esta función no proporciona probabilidades directamente, sino que el área bajo la curva de la función entre dos puntos corresponde a la probabilidad de que la variable aleatoria caiga dentro de ese intervalo.

    Ejemplos de Variables Aleatorias Continuas

    • Altura de los estudiantes en una clase: La altura puede variar continuamente y puede ser cualquier valor dentro de un rango razonable, por ejemplo, entre 1.50 metros y 2.00 metros.
    • Tiempo necesario para completar una tarea: Este tiempo puede ser cualquier número no negativo, medido con precisión hasta fracciones de segundo.
    • Presión en un tanque de gas: La presión puede fluctuar y tomar cualquier valor dentro de los límites de seguridad del tanque.

    Funcion de densidad de probabilidad

    De manera similar a cómo las variables aleatorias discretas se relacionan con las funciones de masa de probabilidad, las variables aleatorias continuas se relacionan con las funciones de densidad de probabilidad. Definen las distribuciones de probabilidad de variables aleatorias continuas y abarcan todos los valores posibles que puede adoptar la variable aleatoria dada.

    Cuando se representa gráficamente, una función de densidad de probabilidad es una curva que atraviesa todos los valores posibles que puede tomar la variable aleatoria, y el área total bajo esta curva suma 1.

    La siguiente imagen muestra una función de densidad de probabilidad. El área resaltada representa la probabilidad de observar un valor dentro del rango resaltado.

    Probability Density

    En una función de densidad de probabilidad, no podemos calcular la probabilidad en un solo punto. Esto se debe a que el área de la curva debajo de un único punto es siempre cero. El siguiente gif muestra esto.

    Probability Density one point

    Como podemos ver en la imagen anterior, a medida que el intervalo se hace más pequeño, el ancho del área bajo la curva también se hace más pequeño. Al intentar evaluar el área bajo la curva en un punto específico, el ancho de esa área se vuelve 0 y, por lo tanto, la probabilidad es igual a 0.

    Podemos calcular el área bajo la curva usando la función de distribución acumulativa para la distribución de probabilidad dada.

    Por ejemplo, las alturas caen bajo un tipo de distribución de probabilidad llamada distribución normal. Los parámetros de la distribución normal son la media y la desviación estándar, y utilizamos la forma Normal(media, desviación estándar) como abreviatura.

    Sabemos que la altura de las mujeres tiene una media de 167,64 cm con una desviación estándar de 8 cm, lo que las sitúa bajo la distribución Normal(167,64,8).

    Digamos que queremos saber la probabilidad de que una mujer elegida al azar mida menos de 158 cm. Podemos usar la función de distribución acumulativa para calcular el área bajo la curva de la función de densidad de probabilidad de 0 a 158 para encontrar esa probabilidad.

    Area

    Podemos calcular el área de la región azul en Python usando el método norm.cdf() de la biblioteca scipy.stats. Este método toma 3 valores:

    • x: el valor del interés
    • loc: la media de la distribución de probabilidad
    • scale: la desviación estándar de la distribución de probabilidad
    import scipy.stats as stats
    
    # stats.norm.cdf(x, loc, scale)
    print(stats.norm.cdf(158, 167.64, 8))
    

    Output

    0.1141
    

    Funciones de densidad de probabilidad y función de distribución acumulativa

    Podemos tomar la diferencia entre dos rangos superpuestos para calcular la probabilidad de que una selección aleatoria esté dentro de un rango de valores para distribuciones continuas. Este es esencialmente el mismo proceso que calcular la probabilidad de un rango de valores para distribuciones discretas.

    Rangos superpuestos

    Digamos que queremos calcular la probabilidad de observar aleatoriamente a una mujer de entre 165 cm y 175 cm, suponiendo que las alturas todavía siguen la distribución Normal (167,74, 8). Podemos calcular la probabilidad de observar estos valores o menos. La diferencia entre estas dos probabilidades será la probabilidad de observar aleatoriamente a una mujer en este rango dado. Esto se puede hacer en Python usando el método norm.cdf() de la biblioteca scipy.stats. Como se mencionó anteriormente, este método adopta 3 valores:

    import scipy.stats as stats
    # P(165 < X < 175) = P(X < 175) - P(X < 165)
    # stats.norm.cdf(x, loc, scale) - stats.norm.cdf(x, loc, scale)
    print(stats.norm.cdf(175, 167.74, 8) - stats.norm.cdf(165, 167.74, 8))
    

    Output

    0.45194
    

    También podemos calcular la probabilidad de observar aleatoriamente un valor o mayor restando de 1 la probabilidad de observar menos que el valor dado. Esto es posible porque sabemos que el área total bajo la curva es 1, por lo que la probabilidad de observar algo mayor que un valor es 1 menos la probabilidad de observar algo menor que el valor dado.

    Digamos que queremos calcular la probabilidad de observar a una mujer que mide más de 172 centímetros, suponiendo que las alturas todavía siguen la distribución Normal (167,74, 8). Podemos pensar en esto como lo opuesto a observar a una mujer que mide menos de 172 centímetros. Podemos visualizarlo de esta manera:

    Grafica

    Podemos usar el siguiente código para calcular el área azul tomando 1 menos el área roja:

    import scipy.stats as stats
    
    # P(X > 172) = 1 - P(X < 172)
    # 1 - stats.norm.cdf(x, loc, scale)
    print(1 - stats.norm.cdf(172, 167.74, 8))
    

    Output

    0.45194
    

    Tipos de Variables Aleatorias Continuas

    1. Normal (Gaussiana):
      • loc: media de la distribución (mu).
      • scale: desviación estándar de la distribución (sigma).
      • size: número de muestras a generar.
      numpy.random.normal(loc=0.0, scale=1.0, size=None) Genera números a partir de una distribución normal con media loc y desviación estándar scale.
    2. Exponencial:
      • scale: inverso de la tasa (lambda), a veces llamado parámetro de escala.
      • size: número de muestras a generar.
      numpy.random.exponential(scale=1.0, size=None) Genera números a partir de una distribución exponencial con un parámetro de escala.
    3. Uniforme:
      • low: límite inferior del rango de los valores.
      • high: límite superior del rango de los valores.
      • size: número de muestras a generar.
      numpy.random.uniform(low=0.0, high=1.0, size=None) Genera números a partir de una distribución uniforme entre low y high.
    4. Beta:
      • a: parámetro de forma alpha.
      • b: parámetro de forma beta.
      • size: número de muestras a generar.
      numpy.random.beta(a, b, size=None) Genera números a partir de una distribución beta con parámetros a y b.
    5. Gamma:
      • shape: parámetro de forma (k).
      • scale: parámetro de escala (theta).
      • size: número de muestras a generar.
      numpy.random.gamma(shape, scale=1.0, size=None) Genera números a partir de una distribución gamma con un parámetro de forma y escala.

    Estos métodos permiten simular datos que siguen estas distribuciones, lo que es útil en simulaciones, pruebas de hipótesis, y para entender mejor el comportamiento estadístico de fenómenos modelados por estas distribuciones.

  • La Distribución Binomial: La Base Matemática para Contar Éxitos en Experimentos Repetidos

    La distribución binomial es una de las herramientas más importantes en estadística y ciencia de datos. Aparece siempre que repetimos un experimento con dos posibles resultados —éxito o fracaso, sí o no, 1 o 0 — y queremos conocer la probabilidad de obtener cierto número de éxitos en un conjunto de intentos.

    En esta clase veremos qué es, cómo se construye y por qué es tan útil para problemas reales como comprobar si una moneda está trucada, evaluar la precisión de un modelo o estimar tasas de éxito en marketing, medicina o industria.

    Del experimento Bernoulli a la distribución Binomial

    En el articulo anterior aborde la Distribución Bernoulli, que describe un experimento con dos resultados posibles:

    • Éxito → se representa con 1
    • Fracaso → se representa con 0

    Si la probabilidad de éxito es p, entonces la probabilidad de fracaso es 1 – p.

    La pregunta ahora es: ¿qué ocurre cuando repetimos este experimento varias veces?

    Por ejemplo:

    • Lanzar una moneda n veces
    • Enviar n anuncios y medir si cada usuario hace clic
    • Revisar n productos y ver si cada uno tiene defectos

    La distribución que describe el número total de éxitos obtenidos en n intentos independientes es la Distribución Binomial.

    Una aplicación real: ¿es justa una moneda? (Test de hipótesis)

    Supón que quieres determinar si una moneda es justa.

    Planteamos dos hipótesis:

    • H₀ (Hipótesis nula): la moneda es justa → \( p = 0.5 \)
    • H₁ (Hipótesis alternativa): la moneda no es justa → \( p \neq 0.5 \)

    Lanzas la moneda n veces y defines:

    $$X_i = \begin{cases}1, & \text{si sale cara} \\ 0, & \text{si sale cruz} \end{cases}$$

    El número total de caras es:

    $$S = X_1 + X_2 + \cdots + X_n$$

    La pregunta clave es:

    ¿cuál es la probabilidad de que ocurra un determinado valor de S si la moneda es justa?

    Responder a eso es exactamente el papel de la distribución binomial.

    Construyendo la distribución binomial paso a paso

    Caso S = 0 (todas cruces)

    Para obtener 0 caras en n lanzamientos, todas deben ser cruces:

    $$P(S=0) = (1 – p)^n$$

    Solo existe una secuencia posible: TTTTT…T (n veces).


    Caso S = 1 (una sola cara)

    La probabilidad de que una secuencia concreta sea “una cara + n-1 cruces” es:

    $$p(1-p)^{n-1}$$

    Pero hay n posiciones posibles para esa única cara:

    • Cara en el lanzamiento 1
    • Cara en el lanzamiento 2
    • Cara en el lanzamiento n

    Así que:

    $$P(S=1) = n \cdot p(1-p)^{n-1}$$

    Caso general: S = s

    Para obtener exactamente s caras en n lanzamientos:

    La probabilidad de una secuencia concreta es:

    $$p^{s}(1-p)^{n-s}$$

    El número de secuencias distintas que contienen exactamente s caras y n-s cruces es:

    $$\begin{pmatrix} n \\ s \end{pmatrix}$$

    que se lee “n elige s”.

    Entonces, la probabilidad total es:

    $$P(S=s) = \begin{pmatrix} n \\ s \end{pmatrix} p^{s}(1-p)^{n-s}$$

    Esta es la fórmula de la distribución binomial.

    ¿Qué es el “n elige s”? La intuición del coeficiente binomial

    El operador combinatorio:

    $$ \begin{pmatrix} n \\ s \end{pmatrix} = \frac{n!}{s!(n-s)!}$$

    cuenta cuántas formas hay de elegir s elementos dentro de un conjunto de n elementos, sin importar el orden.

    Ejemplo clásico:

    El número de manos posibles de 5 cartas tomadas de una baraja de 52 cartas es:

    $$\begin{pmatrix}52 \\ 5 \end{pmatrix}$$

    En nuestro contexto, representa cuántas secuencias distintas tienen s caras y n-s cruces.

    ¿Qué forma tiene la distribución binomial?

    La distribución depende de dos parámetros:

    • n → número de ensayos
    • p → probabilidad de éxito en cada ensayo

    Si aumentamos n (más repeticiones)

    • El número máximo posible de éxitos crece
    • La distribución se vuelve más “ancha” cuando se mira en términos de conteos

    Pero si en vez de mirar S, miramos la fracción de éxitos:

    $$\frac{S}{n}$$

    lo que ocurre es que la distribución se estrecha alrededor de p. Esto conecta con la Ley de los Grandes Números.

    Si cambiamos p (probabilidad de éxito)

    • Si p aumenta → el histograma se desplaza hacia la derecha
    • Si p disminuye → se desplaza hacia la izquierda

    Cuando p = 0.5, la distribución es simétrica (si n es grande).

    Propiedades importantes de la distribución binomial

    Valor esperado (media)

    $$E[S] = np$$

    Varianza

    $$Var(S) = np(1-p)$$

    Aproximación normal (cuando n es grande)

    Si (n) es suficientemente grande, la distribución binomial se aproxima a una normal:

    $$S \approx \mathcal{N}(np, np(1-p))$$

    Esto es extremadamente útil en estadística inferencial.

    ¿Para qué sirve la distribución binomial en ciencia de datos?

    • Test A/B y marketing digital: Medir clics, conversiones o aperturas de email.
    • Calidad industrial: Detectar la tasa de defectos.
    • Modelos de clasificación: Analizar el número de aciertos vs. errores.
    • Inferencia estadística: Construir intervalos de confianza para una proporción.
    • Simulaciones y análisis de riesgo: Modelar escenarios de éxito/fracaso repetidos.

    En Resumen

    La distribución binomial:

    • Surge al repetir un experimento Bernoulli n veces
    • Modela el número de éxitos S en esas repeticiones
    • Su fórmula combina:
      • Probabilidad de una secuencia → \( p^s (1-p)^{n-s} \)
      • Número de secuencias posibles → \( \begin{pmatrix} n \\ s \end{pmatrix} \)
    • Tiene una estructura simple pero extremadamente poderosa
    • Es fundamental en estadística, machine learning y análisis de datos aplicado

  • La Distribución de Bernoulli — La base de la probabilidad binaria

    En muchas situaciones del mundo real nos enfrentamos a experimentos que solo tienen dos posibles resultados. Por ejemplo:

    • Lanzar una moneda: cara o cruz.
    • Aprobar o suspender un examen.
    • Un cliente hace clic en un anuncio o no lo hace.
    • Una lámpara funciona o se funde.

    Cuando un experimento tiene solo dos resultados posibles, podemos modelarlo mediante la distribución de Bernoulli, una de las distribuciones más simples y fundamentales en la estadística y la teoría de la probabilidad.

    Esta distribución recibe su nombre del matemático suizo Jacob Bernoulli (1655–1705), y es la base de muchas distribuciones más complejas, como la binomial, la geométrica o la beta.

    Definición formal

    Una variable aleatoria Bernoulli \( X \) puede tomar solo dos valores:

    $$X = \begin{cases} 1 & \text{con probabilidad } p \\ 0 & \text{con probabilidad } (1 – p)
    \end{cases}$$

    Donde \( p \) es el parámetro de la distribución y representa la probabilidad de éxito (por ejemplo, obtener “cara” en una moneda justa).

    El parámetro \( p \) cumple que:

    $$0 \leq p \leq 1$$

    Función de masa de probabilidad (PMF)

    La función que describe la probabilidad de cada posible resultado se llama función de masa de probabilidad (PMF):

    $$P(X = x) = p^x (1 – p)^{1 – x}, \quad x \in {0, 1}$$

    Aunque parezca complicada, en realidad es muy sencilla:

    • Si ( x = 1 ): \( P(X=1) = p \)
    • Si ( x = 0 ): \( P(X=0) = 1 – p \)

    Por ejemplo, si ( p = 0.5 ), tenemos una moneda justa; si ( p = 0.8 ), una moneda sesgada hacia cara.

    Esperanza o valor esperado

    El valor esperado o esperanza matemática \(E[X] [latex] representa el promedio que obtendríamos si repitiésemos el experimento infinitas veces.

    Para una Bernoulli:

    $$E[X] = 0 \times (1 – p) + 1 \times p = p$$

    Es decir, la esperanza de una Bernoulli es igual al parámetro ( p ).

    Si una moneda tiene ( p = 0.7 ) de salir cara, el valor esperado de obtener cara es 0.7.

    Varianza

    La varianza mide cuánto se dispersan los valores posibles de la variable respecto a su media.
    En la Bernoulli, se calcula como:

    $$Var(X) = p (1 – p)$$

    Esto tiene una interpretación interesante:

    • Cuando [latex] p = 0 \) o \( p = 1 \), la varianza es 0, ya que siempre se obtiene el mismo resultado.
    • La varianza máxima se da cuando \( p = 0.5 \), es decir, cuando ambos resultados son igualmente probables.

    Implementación en Python

    Podemos representar la distribución de Bernoulli de varias formas en Python.
    A continuación se muestran ejemplos con scipy y también una simulación manual con numpy.

    import numpy as np
    import matplotlib.pyplot as plt
    from scipy.stats import bernoulli
    
    # Parámetro de la distribución
    p = 0.5
    
    # Posibles valores de X (0 o 1)
    x = [0, 1]
    
    # Función de masa de probabilidad (PMF)
    pmf = bernoulli.pmf(x, p)
    
    # Graficamos
    plt.bar(x, pmf, color='skyblue', edgecolor='black')
    plt.xticks([0, 1], ['Fallo (0)', 'Éxito (1)'])
    plt.title(f'Distribución de Bernoulli (p = {p})')
    plt.ylabel('Probabilidad')
    plt.show()

    Esto mostrará una gráfica con dos barras, una en 0 con altura 0.5 y otra en 1 con altura 0.5.


    Ejemplo: Esperanza y varianza

    # Cálculo teórico
    mean_theoretical = bernoulli.mean(p)
    var_theoretical = bernoulli.var(p)
    
    print(f"Esperanza (E[X]) = {mean_theoretical}")
    print(f"Varianza (Var[X]) = {var_theoretical}")

    Salida:

    Esperanza (E[X]) = 0.5
    Varianza (Var[X]) = 0.25

    Ejemplo: Simulación con numpy

    Vamos a simular 10,000 lanzamientos de una moneda con probabilidad p = 0.7 de salir cara.

    # Simulación de 10,000 lanzamientos
    n = 10_000
    p = 0.7
    data = np.random.binomial(1, p, size=n)  # Binomial con n=1 equivale a Bernoulli
    
    # Resultados empíricos
    mean_empirical = np.mean(data)
    var_empirical = np.var(data)
    
    print(f"Media observada: {mean_empirical:.3f}")
    print(f"Varianza observada: {var_empirical:.3f}")

    Salida:

    Media observada: 0.703
    Varianza observada: 0.209

    7. Interpretación visual

    La varianza \( Var(X) = p(1 – p) \) alcanza su máximo cuando \( p = 0.5 \).
    Podemos comprobarlo gráficamente:

    p_values = np.linspace(0, 1, 100)
    variance = p_values * (1 - p_values)
    
    plt.plot(p_values, variance, color='coral')
    plt.title("Varianza de la Distribución de Bernoulli")
    plt.xlabel("p")
    plt.ylabel("Varianza")
    plt.grid(True)
    plt.show()

    En resumen:

    La distribución de Bernoulli es la piedra angular de la probabilidad binaria.
    Su simplicidad la convierte en un modelo ideal para entender conceptos más avanzados, como:

    • Distribución binomial: suma de varios experimentos Bernoulli independientes.
    • Distribución beta: distribución continua conjugada para ( p ) en el contexto bayesiano.
    • Procesos de clasificación binaria en machine learning (éxito/fracaso, 1/0).

    En resumen:

    ConceptoFórmulaInterpretación
    PMF( P(X=x) = p^x (1-p)^{1-x} )Probabilidad de éxito o fallo
    Esperanza( E[X] = p )Promedio esperado
    Varianza( Var(X) = p(1-p) )Dispersión de los resultados
  • El Teorema de Bayes: Cómo Actualizar Nuestras Creencias con Nueva Evidencia

    El Teorema de Bayes es una de las ideas más poderosas y elegantes de la probabilidad. Nos permite calcular la probabilidad de que algo sea cierto cuando tenemos nueva información o evidencia.

    La Intuición: Probabilidades Condicionales

    Imagina un experimento con dos pasos:

    1. Lanzamos una moneda (con una probabilidad desconocida de salir cara).
    2. Si sale cara, tiramos un dado de seis caras.
      Si sale cruz, tiramos uno de veinte caras.

    Ahora, supón que el resultado del dado fue un 5. La pregunta es: ¿Cuál es la probabilidad de que la moneda haya salido cara, sabiendo que el dado dio 5?

    Este es un ejemplo clásico de probabilidad condicional inversa: queremos invertir el sentido del razonamiento, pasando de

    $$P(\text{dado}=5 | \text{cara})$$

    a

    $$P(\text{cara} | \text{dado}=5)$$

    Derivando el Teorema

    A partir de las definiciones básicas de probabilidad y usando la interpretación geométrica de áreas bajo la curva, se llega a la fórmula general:

    $$P(A|B) = \frac{P(B|A) P(A)}{P(B)}$$

    Donde:

    • \(P(A) \) es la probabilidad inicial o priori de que ocurra \( A \).
    • \( P(B|A) \) es la verosimilitud: qué tan probable es observar \( B\) si \( A \) fuera cierto.
    • \( P(B) \) es la probabilidad total de \( B \), considerando todos los casos posibles.

    Aplicación: Ejemplo del Test de COVID

    Supongamos un test que da positivo el 80 % de las veces en la población general. Sabemos que si una persona tiene COVID, la probabilidad de que el test dé positivo es 0.9. Antes de hacernos el test, creemos que hay un 70 % de probabilidad de estar infectados.

    Aplicando Bayes:

    $$P(\text{COVID}|\text{test positivo}) = \frac{0.9 \times 0.7}{0.8} = 0.7875$$

    Es decir, la probabilidad real de tener COVID aumenta a 78.75 % tras recibir el resultado positivo.

    Actualización Iterativa

    Lo más interesante es que Bayes nos permite actualizar las probabilidades cada vez que obtenemos nueva evidencia. Por ejemplo, si un compañero de piso también da positivo, podemos recalcular con esa información y la probabilidad aumentará (en este caso, hasta cerca del 88 %).

    Este proceso de revisión continua de nuestras creencias es la base de muchos algoritmos de aprendizaje automático, donde los modelos aprenden y se ajustan con cada nuevo dato.

    Inferencia Bayesiana con Python

    La idea central de la inferencia bayesiana

    En lugar de estimar un solo valor (como hace la estadística clásica), Bayes nos da una distribución completa sobre los posibles valores de los parámetros. Así podemos medir incertidumbre y ajustar nuestras creencias conforme llegan nuevos datos.

    $$\text{Posterior} = \frac{\text{Verosimilitud} \times \text{Prior}}{\text{Evidencia}}$$

    Ejemplo práctico: Clasificador Bayesiano simple

    Veamos un ejemplo básico con Naive Bayes, aplicado a correos electrónicos.
    No necesitamos datos reales todavía — solo entender el razonamiento.

    from sklearn.naive_bayes import MultinomialNB
    from sklearn.feature_extraction.text import CountVectorizer
    
    # Datos de ejemplo
    emails = [
        "Oferta exclusiva gana dinero rápido",    # spam
        "Reunión de trabajo a las 10",            # no spam
        "Compra ahora descuento especial",        # spam
        "Adjunto informe mensual del proyecto"    # no spam
    ]
    
    labels = [1, 0, 1, 0]  # 1 = spam, 0 = no spam
    
    # Convertimos texto a matriz de frecuencias
    vectorizer = CountVectorizer()
    X = vectorizer.fit_transform(emails)
    
    # Entrenamos el modelo bayesiano
    model = MultinomialNB()
    model.fit(X, labels)
    
    # Probamos con un nuevo mensaje
    nuevo_email = ["oferta de trabajo con descuento"]
    X_new = vectorizer.transform(nuevo_email)
    prob_spam = model.predict_proba(X_new)
    
    print("Probabilidad de SPAM:", prob_spam[0][1])
    

    Este modelo aplica el Teorema de Bayes a cada palabra del mensaje y combina los resultados suponiendo independencia entre ellas (por eso se llama Naive o “ingenuo”).

    Inferencia bayesiana “real” con PyMC o NumPyro

    Cuando queremos estimar parámetros desconocidos, usamos librerías especializadas como PyMC:

    import pymc as pm
    import arviz as az
    
    # Ejemplo: estimar la probabilidad de éxito de una moneda sesgada
    with pm.Model() as modelo:
        p = pm.Beta("p", alpha=2, beta=2)        # Prior: distribución beta
        observaciones = pm.Bernoulli("obs", p, observed=[1,0,1,1,0,1])  # Datos
        trazas = pm.sample(2000, tune=1000)
    
    az.plot_posterior(trazas)
    

    Aquí usamos una distribución Beta como priori, y tras observar los datos (caras y cruces) obtenemos la distribución posterior de p : nuestra creencia actualizada sobre cuán sesgada está la moneda.

    ¿Por qué es importante?

    La inferencia bayesiana permite:

    • Actualizar modelos dinámicamente (ej. diagnóstico médico con nueva información).
    • Expresar incertidumbre en vez de dar una sola respuesta.
    • Combinar conocimiento previo con datos (por ejemplo, en modelos predictivos con pocos datos).

    Por eso se usa en:

    • Machine Learning probabilístico
    • Sistemas de recomendación
    • Medicina y biología
    • Finanzas y predicción de riesgos

    En resumen

    ConceptoInterpretación Bayesiana
    PriorLo que creemos antes de ver los datos
    EvidenciaLos datos observados
    PosteriorLo que creemos después de ver los datos
    VerosimilitudQué tan probable es ver esos datos si la hipótesis fuera cierta