{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Análisis Exploratorio – Mayo Clinic STRIP AI\n### Clasificación del origen de coágulos en ACV isquémico (CE vs LAA)\n\n**Cómo usar este notebook**\n- Diseñado para correr en **Kaggle Notebooks** (con el dataset ya adjunto vía \"Add Data\") o en **Google Colab**\n  descargando los datos con la API de Kaggle.\n- Requiere `openslide-python` para leer las imágenes `.tif` (whole-slide images). En Kaggle Notebooks normalmente\n  ya está disponible; en Colab hay que instalarlo (ver celda de setup).\n- El notebook trabaja sobre una **muestra** de imágenes para las secciones de análisis a nivel de píxel, ya que\n  cada `.tif` puede pesar varios GB.\n","metadata":{}},{"cell_type":"code","source":"# ==== 0. Setup ====\n# Descomentar si se corre en Colab y aún no está instalado:\n# !pip install openslide-python --quiet\n# !apt-get install -y openslide-tools --quiet\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\ntry:\n    import openslide\n    OPENSLIDE_OK = True\nexcept ImportError:\n    OPENSLIDE_OK = False\n    print(\"openslide no disponible: las secciones de imagen se omitirán o usarán PIL como respaldo.\")\n\nfrom PIL import Image\n\nsns.set_theme(style=\"whitegrid\")\npd.set_option(\"display.max_columns\", None)\n\n# Ruta actualizada: Kaggle ahora monta los datasets de competencia bajo /kaggle/input/competitions/\nDATA_DIR = \"/kaggle/input/competitions/mayo-clinic-strip-ai\"\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train\")\nOTHER_IMG_DIR = os.path.join(DATA_DIR, \"other\")\n\n# Verificación rápida de la estructura (déjala por ahora, luego la puedes borrar)\nprint(os.listdir(DATA_DIR))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:55.790802Z","iopub.execute_input":"2026-09-03T21:40:55.791169Z","iopub.status.idle":"2026-09-03T21:40:55.79994Z","shell.execute_reply.started":"2026-09-03T21:40:55.791139Z","shell.execute_reply":"2026-09-03T21:40:55.798718Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Carga y descripción general de los datos\n(Actividad 4.a — número de variables, observaciones y tipos)","metadata":{}},{"cell_type":"code","source":"train = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\ntest = pd.read_csv(os.path.join(DATA_DIR, \"test.csv\"))\n\n# other.csv contiene coágulos con etiología desconocida u otra (no CE/LAA); es opcional/exploratorio\nother_path = os.path.join(DATA_DIR, \"other.csv\")\nother = pd.read_csv(other_path) if os.path.exists(other_path) else None\n\nprint(f\"train: {train.shape[0]} observaciones, {train.shape[1]} variables\")\nprint(f\"test:  {test.shape[0]} observaciones, {test.shape[1]} variables\")\nif other is not None:\n    print(f\"other: {other.shape[0]} observaciones, {other.shape[1]} variables\")\n\ntrain.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:55.801602Z","iopub.execute_input":"2026-09-03T21:40:55.802215Z","iopub.status.idle":"2026-09-03T21:40:55.835481Z","shell.execute_reply.started":"2026-09-03T21:40:55.802182Z","shell.execute_reply":"2026-09-03T21:40:55.834664Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tipos de dato de cada variable\ntrain.dtypes.to_frame(\"tipo_de_dato\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:55.836452Z","iopub.execute_input":"2026-09-03T21:40:55.836795Z","iopub.status.idle":"2026-09-03T21:40:55.851632Z","shell.execute_reply.started":"2026-09-03T21:40:55.836768Z","shell.execute_reply":"2026-09-03T21:40:55.85068Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cantidad de pacientes, centros e imágenes únicas en el set de entrenamiento\nprint(\"Pacientes únicos:\", train['patient_id'].nunique())\nprint(\"Centros médicos únicos:\", train['center_id'].nunique())\nprint(\"Imágenes (filas) totales:\", train['image_id'].nunique())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:55.852814Z","iopub.execute_input":"2026-09-03T21:40:55.853136Z","iopub.status.idle":"2026-09-03T21:40:55.871564Z","shell.execute_reply.started":"2026-09-03T21:40:55.853109Z","shell.execute_reply":"2026-09-03T21:40:55.870654Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Variables categóricas: tablas de frecuencia y proporciones\n(Actividad 4.b)","metadata":{}},{"cell_type":"code","source":"# Distribución de la variable objetivo (label): CE vs LAA\nfreq_label = train['label'].value_counts()\nprop_label = train['label'].value_counts(normalize=True).round(3)\n\ntabla_label = pd.DataFrame({\"frecuencia\": freq_label, \"proporcion\": prop_label})\nprint(tabla_label)\n\nplt.figure(figsize=(5,4))\nsns.barplot(x=freq_label.index, y=freq_label.values, hue=freq_label.index, palette=\"viridis\", legend=False)\nplt.title(\"Distribución de la etiología del coágulo (label)\")\nplt.ylabel(\"Número de imágenes\")\nplt.xlabel(\"Etiología\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:55.874206Z","iopub.execute_input":"2026-09-03T21:40:55.874492Z","iopub.status.idle":"2026-09-03T21:40:56.013951Z","shell.execute_reply.started":"2026-09-03T21:40:55.874459Z","shell.execute_reply":"2026-09-03T21:40:56.012882Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Distribución por centro médico (center_id)\nfreq_center = train['center_id'].value_counts()\nprop_center = train['center_id'].value_counts(normalize=True).round(3)\ntabla_center = pd.DataFrame({\"frecuencia\": freq_center, \"proporcion\": prop_center})\nprint(tabla_center)\n\nplt.figure(figsize=(7,4))\nsns.barplot(x=freq_center.index, y=freq_center.values, hue=freq_center.index, palette=\"magma\", legend=False)\nplt.title(\"Número de imágenes por centro médico\")\nplt.ylabel(\"Número de imágenes\")\nplt.xlabel(\"Centro médico (center_id)\")\nplt.xticks(rotation=45)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:56.01508Z","iopub.execute_input":"2026-09-03T21:40:56.015358Z","iopub.status.idle":"2026-09-03T21:40:56.248131Z","shell.execute_reply.started":"2026-09-03T21:40:56.015331Z","shell.execute_reply":"2026-09-03T21:40:56.247104Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Variables numéricas: estadística descriptiva\n(Actividad 4.b)","metadata":{}},{"cell_type":"code","source":"# Imágenes por paciente (variable derivada, numérica discreta)\nimgs_por_paciente = train.groupby('patient_id')['image_id'].count().rename('n_imagenes')\n\nprint(imgs_por_paciente.describe())\n\nplt.figure(figsize=(6,4))\nsns.histplot(imgs_por_paciente, bins=range(1, imgs_por_paciente.max()+2), discrete=True, color=\"steelblue\")\nplt.title(\"Distribución de imágenes por paciente\")\nplt.xlabel(\"Número de imágenes\")\nplt.ylabel(\"Número de pacientes\")\nplt.show()\n\nplt.figure(figsize=(4,5))\nsns.boxplot(y=imgs_por_paciente, color=\"lightseagreen\")\nplt.title(\"Boxplot: imágenes por paciente\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:56.249254Z","iopub.execute_input":"2026-09-03T21:40:56.249643Z","iopub.status.idle":"2026-09-03T21:40:56.51728Z","shell.execute_reply.started":"2026-09-03T21:40:56.249612Z","shell.execute_reply":"2026-09-03T21:40:56.51635Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Valores faltantes y duplicados","metadata":{}},{"cell_type":"code","source":"print(\"Valores faltantes por columna (train):\")\nprint(train.isnull().sum())\n\nprint(\"\\nFilas duplicadas (train):\", train.duplicated().sum())\nprint(\"image_id duplicados:\", train['image_id'].duplicated().sum())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:56.518415Z","iopub.execute_input":"2026-09-03T21:40:56.519067Z","iopub.status.idle":"2026-09-03T21:40:56.528056Z","shell.execute_reply.started":"2026-09-03T21:40:56.519026Z","shell.execute_reply":"2026-09-03T21:40:56.527326Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Cruce de variables clave\n(Actividad 4.c)","metadata":{}},{"cell_type":"code","source":"# Etiología (label) vs Centro médico (center_id)\ntabla_cruzada = pd.crosstab(train['center_id'], train['label'])\ntabla_cruzada_prop = pd.crosstab(train['center_id'], train['label'], normalize='index').round(3)\n\nprint(\"Frecuencias:\")\nprint(tabla_cruzada)\nprint(\"\\nProporciones por fila (dentro de cada centro):\")\nprint(tabla_cruzada_prop)\n\ntabla_cruzada.plot(kind='bar', stacked=True, figsize=(7,4), colormap=\"viridis\")\nplt.title(\"Etiología del coágulo por centro médico\")\nplt.ylabel(\"Número de imágenes\")\nplt.xlabel(\"Centro médico\")\nplt.legend(title=\"Etiología\")\nplt.xticks(rotation=45)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:56.529208Z","iopub.execute_input":"2026-09-03T21:40:56.529594Z","iopub.status.idle":"2026-09-03T21:40:56.765288Z","shell.execute_reply.started":"2026-09-03T21:40:56.529556Z","shell.execute_reply":"2026-09-03T21:40:56.764374Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ¿La cantidad de imágenes por paciente varía según la etiología?\ntrain_con_conteo = train.merge(imgs_por_paciente, on='patient_id')\nresumen_por_label = train_con_conteo.groupby('label')['n_imagenes'].describe()\nprint(resumen_por_label)\n\nplt.figure(figsize=(5,4))\nsns.boxplot(data=train_con_conteo.drop_duplicates('patient_id'), x='label', y='n_imagenes', hue='label', palette=\"Set2\", legend=False)\nplt.title(\"Imágenes por paciente según etiología\")\nplt.xlabel(\"Etiología\")\nplt.ylabel(\"Número de imágenes\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:56.766462Z","iopub.execute_input":"2026-09-03T21:40:56.766855Z","iopub.status.idle":"2026-09-03T21:40:56.930184Z","shell.execute_reply.started":"2026-09-03T21:40:56.766816Z","shell.execute_reply":"2026-09-03T21:40:56.929393Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Exploración a nivel de imagen\nLas imágenes son *whole-slide images* (WSI) en formato `.tif` de altísima resolución\n(pueden superar los 50,000 x 50,000 píxeles y varios GB por archivo). Por eso se trabaja\nsobre una **muestra aleatoria** para no saturar memoria/tiempo, usando los *thumbnails*\n(vista reducida embebida) que provee OpenSlide en vez de cargar la imagen completa.\n","metadata":{}},{"cell_type":"code","source":"SAMPLE_SIZE = 12\nmuestra = train.sample(n=min(SAMPLE_SIZE, len(train)), random_state=42).reset_index(drop=True)\n\nregistros = []\nfor _, row in muestra.iterrows():\n    fpath = os.path.join(TRAIN_IMG_DIR, f\"{row['image_id']}.tif\")\n    if not os.path.exists(fpath):\n        continue\n    tam_disco_mb = os.path.getsize(fpath) / (1024**2)\n    if OPENSLIDE_OK:\n        slide = openslide.OpenSlide(fpath)\n        w, h = slide.dimensions\n        slide.close()\n    else:\n        with Image.open(fpath) as im:\n            w, h = im.size\n    registros.append({\n        \"image_id\": row['image_id'],\n        \"label\": row['label'],\n        \"ancho_px\": w,\n        \"alto_px\": h,\n        \"tam_disco_mb\": round(tam_disco_mb, 1),\n        \"aspect_ratio\": round(w / h, 2)\n    })\n\ndf_imgs = pd.DataFrame(registros)\ndf_imgs\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:56.931197Z","iopub.execute_input":"2026-09-03T21:40:56.931481Z","iopub.status.idle":"2026-09-03T21:40:57.016307Z","shell.execute_reply.started":"2026-09-03T21:40:56.931454Z","shell.execute_reply":"2026-09-03T21:40:57.015512Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Estadística descriptiva de las dimensiones/tamaño de la muestra de imágenes\ndf_imgs[['ancho_px', 'alto_px', 'tam_disco_mb', 'aspect_ratio']].describe()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:57.01742Z","iopub.execute_input":"2026-09-03T21:40:57.017815Z","iopub.status.idle":"2026-09-03T21:40:57.035645Z","shell.execute_reply.started":"2026-09-03T21:40:57.017779Z","shell.execute_reply":"2026-09-03T21:40:57.034552Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(11,4))\nsns.histplot(df_imgs['tam_disco_mb'], bins=8, ax=axes[0], color=\"coral\")\naxes[0].set_title(\"Tamaño en disco de las imágenes (MB)\")\nsns.scatterplot(data=df_imgs, x='ancho_px', y='alto_px', hue='label', ax=axes[1])\naxes[1].set_title(\"Dimensiones de las imágenes (ancho vs alto)\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:57.036894Z","iopub.execute_input":"2026-09-03T21:40:57.037336Z","iopub.status.idle":"2026-09-03T21:40:57.372408Z","shell.execute_reply.started":"2026-09-03T21:40:57.037296Z","shell.execute_reply":"2026-09-03T21:40:57.37148Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualización de miniaturas de una muestra, por etiología\ndef get_thumbnail(fpath, size=(300,300)):\n    if OPENSLIDE_OK:\n        slide = openslide.OpenSlide(fpath)\n        thumb = slide.get_thumbnail(size)\n        slide.close()\n        return thumb\n    else:\n        with Image.open(fpath) as im:\n            im.thumbnail(size)\n            return im.copy()\n\nfig, axes = plt.subplots(2, 4, figsize=(14,7))\nfor ax, (_, row) in zip(axes.flatten(), muestra.iterrows()):\n    fpath = os.path.join(TRAIN_IMG_DIR, f\"{row['image_id']}.tif\")\n    if os.path.exists(fpath):\n        thumb = get_thumbnail(fpath)\n        ax.imshow(thumb)\n    ax.set_title(f\"{row['label']}\", fontsize=10)\n    ax.axis('off')\nplt.suptitle(\"Muestra de coágulos por etiología (thumbnails)\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:40:57.374862Z","iopub.execute_input":"2026-09-03T21:40:57.375172Z","iopub.status.idle":"2026-09-03T21:45:38.850091Z","shell.execute_reply.started":"2026-09-03T21:40:57.375142Z","shell.execute_reply":"2026-09-03T21:45:38.848814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Estimación aproximada de proporción de tejido vs. fondo blanco (sobre el thumbnail)\n# Regla simple: un píxel se considera \"fondo\" si está muy cerca del blanco en los 3 canales RGB.\ndef proporcion_tejido(thumb, umbral=220):\n    arr = np.array(thumb.convert(\"RGB\"))\n    fondo = np.all(arr > umbral, axis=-1)\n    return 1 - fondo.mean()\n\nresultados_tejido = []\nfor _, row in muestra.iterrows():\n    fpath = os.path.join(TRAIN_IMG_DIR, f\"{row['image_id']}.tif\")\n    if os.path.exists(fpath):\n        thumb = get_thumbnail(fpath, size=(400,400))\n        prop = proporcion_tejido(thumb)\n        resultados_tejido.append({\"image_id\": row['image_id'], \"label\": row['label'], \"prop_tejido\": round(prop,3)})\n\ndf_tejido = pd.DataFrame(resultados_tejido)\nprint(df_tejido)\n\nplt.figure(figsize=(5,4))\nsns.boxplot(data=df_tejido, x='label', y='prop_tejido', hue='label', palette=\"Set3\", legend=False)\nplt.title(\"Proporción estimada de tejido vs. fondo, por etiología\")\nplt.ylabel(\"Proporción de tejido\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-03T21:45:38.852065Z","iopub.execute_input":"2026-09-03T21:45:38.85252Z","iopub.status.idle":"2026-09-03T21:52:37.887897Z","shell.execute_reply.started":"2026-09-03T21:45:38.852478Z","shell.execute_reply":"2026-09-03T21:52:37.886969Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Hallazgos y conclusiones (plantilla)\n\nCompletar esta sección con los resultados reales obtenidos al correr el notebook sobre el\ndataset completo:\n\n- **Balance de clases:** ¿CE y LAA están balanceadas o hay que aplicar técnicas de balanceo\n  (class weights, oversampling) al modelar?\n- **Distribución por centro médico:** ¿hay centros con clara mayoría de una etiología? Esto\n  podría introducir sesgo (*confounding*) si el modelo aprende el centro en vez del patrón\n  histológico real — considerar validación *group k-fold por patient_id/center_id*.\n- **Imágenes por paciente:** si varía entre etiologías, definir si el modelado será a nivel\n  de imagen o se agregará a nivel de paciente (como pide la evaluación de la competencia).\n- **Tamaño y resolución de las imágenes:** justifica la necesidad de tiling (recorte en\n  parches) y de trabajar con miniaturas o resoluciones reducidas por restricciones de memoria.\n- **Proporción de tejido vs. fondo:** si es baja, aplicar un filtro de tejido antes de generar\n  parches de entrenamiento, para no desperdiciar cómputo en fondo blanco.\n- **Valores faltantes / duplicados:** indicar hallazgos y decisión tomada (eliminar, imputar).\n- **Próximos pasos:** pipeline de tiling, selección de arquitectura (p. ej. EfficientNet /\n  ResNet sobre parches, con agregación a nivel de paciente), estrategia de validación cruzada.\n","metadata":{}}]}