{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.13"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":37333,"sourceType":"competition"}],"isGpuEnabled":false,"isInternetEnabled":true,"language":"python","sourceType":"notebook"},"papermill":{"default_parameters":{},"duration":1107.601383,"end_time":"2026-09-03T22:38:45.359179+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-09-03T22:20:17.757796+00:00","version":"2.7.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"88643283-77d1-4642-9288-5411f9627bee","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\n---\n\n## Situación problemática\n\nUn **accidente cerebrovascular (ACV) isquémico** ocurre cuando un coágulo bloquea el flujo sanguíneo hacia el cerebro.\nDos orígenes frecuentes del coágulo son:\n\n- **CE (Cardioembolic):** el coágulo se forma en el corazón y viaja hasta una arteria cerebral.\n- **LAA (Large Artery Atherosclerosis):** el coágulo se asocia a aterosclerosis de grandes arterias.\n\nDistinguir el origen importa clínicamente porque orienta el tratamiento (por ejemplo, anticoagulación vs control de\nfactores ateroscleróticos) y el pronóstico. Hoy esa clasificación suele depender de estudios clínicos costosos o\nincompletos. El conjunto **Mayo Clinic STRIP AI** aporta *whole-slide images* histopatológicas de coágulos extraídos\ny metadatos (paciente, centro, etiqueta). El problema de ciencia de datos consiste en **caracterizar esos datos**\n—balance de clases, sesgos por centro, estructura por paciente y propiedades de las imágenes— antes de plantear\ncualquier modelo de visión artificial.\n\n## Problema científico\n\n¿Qué patrones en los **metadatos** (etiología, centro médico, número de imágenes por paciente) y en las\n**propiedades observables de las WSI** (tamaño, dimensiones, proporción aparente de tejido) permiten describir y\ndiferenciar las clases **CE** y **LAA**, e identificar sesgos o problemas de calidad presentes en el conjunto?\n\n## Objetivos\n\n**Objetivo general**  \nRealizar un análisis exploratorio del dataset Mayo Clinic STRIP AI que describa la estructura de los datos tabulares\ne imágenes, el balance de la variable objetivo y los principales sesgos o limitaciones del conjunto.\n\n**Objetivos específicos**\n1. Describir variables, observaciones, tipos, faltantes y duplicados del conjunto de entrenamiento.\n2. Cuantificar el balance CE vs LAA y su cruce con centro médico e imágenes por paciente.\n3. Caracterizar, sobre una muestra reproducible de WSI, dimensiones, tamaño en disco y proporción estimada de tejido,\n   incluyendo heterogeneidad de tamaño y proporción aparente de tejido.\n","metadata":{}},{"id":"b9e02424","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 usarán PIL como respaldo.\")\n\nfrom PIL import Image\nfrom IPython.display import display\n\nsns.set_theme(style=\"whitegrid\")\npd.set_option(\"display.max_columns\", None)\nRANDOM_SEED = 42\nnp.random.seed(RANDOM_SEED)\n\n# Resolver ruta del dataset en Kaggle (competitions/ o input directo)\n_CANDIDATES = [\n    \"/kaggle/input/competitions/mayo-clinic-strip-ai\",\n    \"/kaggle/input/mayo-clinic-strip-ai\",\n]\nDATA_DIR = next((p for p in _CANDIDATES if os.path.isdir(p)), _CANDIDATES[0])\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train\")\nTEST_IMG_DIR = os.path.join(DATA_DIR, \"test\")\nOTHER_IMG_DIR = os.path.join(DATA_DIR, \"other\")\n\nprint(\"DATA_DIR:\", DATA_DIR)\nprint(\"contenido:\", os.listdir(DATA_DIR) if os.path.isdir(DATA_DIR) else \"NO ENCONTRADO\")\nprint(\"OpenSlide:\", OPENSLIDE_OK)\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:23:48.761032Z","iopub.status.busy":"2026-09-07T18:23:48.760581Z","iopub.status.idle":"2026-09-07T18:23:50.615612Z","shell.execute_reply":"2026-09-07T18:23:50.614773Z","shell.execute_reply.started":"2026-09-07T18:23:48.760986Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"86c40011","cell_type":"markdown","source":"## 1. Carga y descripción general de los datos\n(Actividad 4.a — número de variables, observaciones y tipos)\n\nSe cargan `train.csv` (etiquetas CE/LAA), `test.csv` (sin etiqueta pública) y, si existe, `other.csv`\n(etiologías distintas o desconocidas: no forman parte del target binario CE/LAA).\n","metadata":{}},{"id":"a890cd99","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:50.618Z","iopub.status.busy":"2026-09-07T18:23:50.617459Z","iopub.status.idle":"2026-09-07T18:23:50.687856Z","shell.execute_reply":"2026-09-07T18:23:50.686748Z","shell.execute_reply.started":"2026-09-07T18:23:50.617966Z"},"papermill":{"duration":0.081368,"end_time":"2026-09-03T22:20:25.595851+00:00","exception":false,"start_time":"2026-09-03T22:20:25.514483+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1e5a0073","cell_type":"code","source":"# Tipos de dato de cada variable\ntrain.dtypes.to_frame(\"tipo_de_dato\")\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:23:50.689953Z","iopub.status.busy":"2026-09-07T18:23:50.689669Z","iopub.status.idle":"2026-09-07T18:23:50.699292Z","shell.execute_reply":"2026-09-07T18:23:50.698067Z","shell.execute_reply.started":"2026-09-07T18:23:50.689927Z"},"papermill":{"duration":0.017077,"end_time":"2026-09-03T22:20:25.617179+00:00","exception":false,"start_time":"2026-09-03T22:20:25.600102+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5b145015","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:50.702358Z","iopub.status.busy":"2026-09-07T18:23:50.701774Z","iopub.status.idle":"2026-09-07T18:23:50.735235Z","shell.execute_reply":"2026-09-07T18:23:50.734175Z","shell.execute_reply.started":"2026-09-07T18:23:50.702138Z"},"papermill":{"duration":0.020586,"end_time":"2026-09-03T22:20:25.64184+00:00","exception":false,"start_time":"2026-09-03T22:20:25.621254+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"88b32f0b","cell_type":"markdown","source":"### Diccionario de variables (`train`)\n\n| Variable | Tipo | Rol | Valores únicos (train) | Faltantes | Descripción |\n|---|---|---|---:|---:|---|\n| `image_id` | categórica (ID) | identificador de imagen / archivo `.tif` | 754 | 0 | Une la fila tabular con el archivo en `train/` |\n| `center_id` | categórica (numérica codificada) | covariable / posible factor de sesgo | 11 | 0 | Centro médico que aportó el caso |\n| `patient_id` | categórica (ID) | agrupador (varios slides por paciente) | 632 | 0 | Clave para no filtrar train/test por paciente |\n| `image_num` | cuantitativa discreta | índice de slide dentro del paciente | — | 0 | Orden/número de imagen del mismo paciente |\n| `label` | categórica | **variable objetivo** | 2 (CE, LAA) | 0 | Origen del coágulo: Cardioembolic vs Large Artery Atherosclerosis |\n\n**Interpretación de la carga.** El entrenamiento tiene **754 imágenes** de **632 pacientes** en **11 centros**.\nHay más imágenes que pacientes: algunos casos aportan varios slides. `test` solo muestra la forma del submission\n(pocas filas en el entorno de demo de Kaggle). `other` agrupa coágulos fuera de CE/LAA y **no debe usarse como\nclase objetivo** de este reto binario, aunque puede servir para explorar calidad de imagen.\n","metadata":{}},{"id":"0921c193","cell_type":"markdown","source":"## 2. Variables categóricas: tablas de frecuencia y proporciones\n(Actividad 4.b)","metadata":{"papermill":{"duration":0.004303,"end_time":"2026-09-03T22:20:25.650492+00:00","exception":false,"start_time":"2026-09-03T22:20:25.646189+00:00","status":"completed"},"tags":[]}},{"id":"75c179b1","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:50.736973Z","iopub.status.busy":"2026-09-07T18:23:50.736642Z","iopub.status.idle":"2026-09-07T18:23:51.024818Z","shell.execute_reply":"2026-09-07T18:23:51.023847Z","shell.execute_reply.started":"2026-09-07T18:23:50.736942Z"},"papermill":{"duration":0.252,"end_time":"2026-09-03T22:20:25.906776+00:00","exception":false,"start_time":"2026-09-03T22:20:25.654776+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f6f5d7f9","cell_type":"markdown","source":"**Interpretación — variable objetivo.** En train hay **547 CE (72.5%)** y **207 LAA (27.5%)**. La clase CE\ndomina aproximadamente **2.6:1**. Ese desbalance debe documentarse porque afectará métricas y la estrategia de\nentrenamiento futura (p. ej. *class weights*, muestreo estratificado o pérdida ponderada). No se eliminan\nregistros de la clase mayoritaria en esta etapa exploratoria: el desbalance es una propiedad real del dataset.\n","metadata":{}},{"id":"01772110","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:51.026184Z","iopub.status.busy":"2026-09-07T18:23:51.025919Z","iopub.status.idle":"2026-09-07T18:23:51.300126Z","shell.execute_reply":"2026-09-07T18:23:51.298739Z","shell.execute_reply.started":"2026-09-07T18:23:51.02616Z"},"papermill":{"duration":0.294284,"end_time":"2026-09-03T22:20:26.205795+00:00","exception":false,"start_time":"2026-09-03T22:20:25.911511+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1f50abdb","cell_type":"markdown","source":"**Interpretación — centros.** La aportación es desigual: el **centro 11** concentra ~34% de las imágenes, mientras\nque los centros 8 y 9 aportan ~2% cada uno. Hay categorías poco representadas. Un modelo podría aprender\ncaracterísticas del centro (tinción, escáner, protocolo) en lugar del patrón histológico del origen del coágulo.\n","metadata":{}},{"id":"28aee8ef","cell_type":"markdown","source":"## 3. Variables numéricas: estadística descriptiva\n(Actividad 4.b)","metadata":{"papermill":{"duration":0.004907,"end_time":"2026-09-03T22:20:26.217019+00:00","exception":false,"start_time":"2026-09-03T22:20:26.212112+00:00","status":"completed"},"tags":[]}},{"id":"f755a59e","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:51.301885Z","iopub.status.busy":"2026-09-07T18:23:51.301586Z","iopub.status.idle":"2026-09-07T18:23:51.605921Z","shell.execute_reply":"2026-09-07T18:23:51.604877Z","shell.execute_reply.started":"2026-09-07T18:23:51.301857Z"},"papermill":{"duration":0.31398,"end_time":"2026-09-03T22:20:26.536104+00:00","exception":false,"start_time":"2026-09-03T22:20:26.222124+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"baa6cd50","cell_type":"code","source":"# Outliers de imágenes por paciente (método IQR)\nq1 = imgs_por_paciente.quantile(0.25)\nq3 = imgs_por_paciente.quantile(0.75)\niqr = q3 - q1\nlim_inf = q1 - 1.5 * iqr\nlim_sup = q3 + 1.5 * iqr\noutliers = imgs_por_paciente[(imgs_por_paciente < lim_inf) | (imgs_por_paciente > lim_sup)]\n\nprint(f\"Q1={q1}, Q3={q3}, IQR={iqr}\")\nprint(f\"Límites IQR: [{lim_inf}, {lim_sup}]\")\nprint(f\"Pacientes fuera de rango IQR: {len(outliers)} ({100*len(outliers)/len(imgs_por_paciente):.1f}%)\")\nprint(\"Distribución de n_imagenes en outliers:\")\nprint(outliers.value_counts().sort_index())\nprint(\"\\nEjemplos (hasta 10 patient_id):\", list(outliers.head(10).index))\n\n# Decisión: NO eliminar. Varios slides por paciente son válidos en WSI clínicas.\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:23:51.607847Z","iopub.status.busy":"2026-09-07T18:23:51.607332Z","iopub.status.idle":"2026-09-07T18:23:51.624525Z","shell.execute_reply":"2026-09-07T18:23:51.623458Z","shell.execute_reply.started":"2026-09-07T18:23:51.607815Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"6b4dc69b","cell_type":"markdown","source":"**Interpretación — numéricas y outliers.** La mayoría de pacientes tiene **1 imagen** (mediana = 1);\nmedia ≈ **1.19**, máximo **5**. Con IQR (Q1 = Q3 = 1), **89 pacientes (14.1%)** quedan como atípicos\npor tener 2–5 slides. **No se eliminan**: son casos clínicos válidos con múltiples WSI. La unidad de\nanálisis y el split deben respetar `patient_id`.\n","metadata":{}},{"id":"c15a35bd","cell_type":"markdown","source":"## 4. Valores faltantes y duplicados","metadata":{"papermill":{"duration":0.005329,"end_time":"2026-09-03T22:20:26.547397+00:00","exception":false,"start_time":"2026-09-03T22:20:26.542068+00:00","status":"completed"},"tags":[]}},{"id":"2a1320c6","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:51.625981Z","iopub.status.busy":"2026-09-07T18:23:51.625652Z","iopub.status.idle":"2026-09-07T18:23:51.664859Z","shell.execute_reply":"2026-09-07T18:23:51.663588Z","shell.execute_reply.started":"2026-09-07T18:23:51.625954Z"},"papermill":{"duration":0.019218,"end_time":"2026-09-03T22:20:26.571963+00:00","exception":false,"start_time":"2026-09-03T22:20:26.552745+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"e57a9396","cell_type":"markdown","source":"**Interpretación — faltantes y duplicados.** En `train` no hay valores faltantes ni filas/`image_id`\nduplicados. **Decisión:** no imputar ni eliminar registros tabulares. La integridad frente a archivos\n`.tif` se verificó en la sección 6 (emparejado completo).\n","metadata":{}},{"id":"f0013b8b","cell_type":"markdown","source":"## 5. Cruce de variables clave\n(Actividad 4.c)","metadata":{"papermill":{"duration":0.005461,"end_time":"2026-09-03T22:20:26.583113+00:00","exception":false,"start_time":"2026-09-03T22:20:26.577652+00:00","status":"completed"},"tags":[]}},{"id":"af437168","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:51.66787Z","iopub.status.busy":"2026-09-07T18:23:51.667557Z","iopub.status.idle":"2026-09-07T18:23:52.17789Z","shell.execute_reply":"2026-09-07T18:23:52.176792Z","shell.execute_reply.started":"2026-09-07T18:23:51.66784Z"},"papermill":{"duration":0.507708,"end_time":"2026-09-03T22:20:27.096266+00:00","exception":false,"start_time":"2026-09-03T22:20:26.588558+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c959874a","cell_type":"markdown","source":"**Interpretación — label × centro.** Casi todos los centros mantienen mayoría CE, pero el **centro 3** invierte\nla proporción (**LAA ≈ 55%**). Eso refuerza el riesgo de *confounding* por centro: si train/validación no\nestratifican o agrupan bien, el modelo puede aprovechar la firma del centro. Hallazgo exploratorio, no causal.\n","metadata":{}},{"id":"025dc1d1","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":{"execution":{"iopub.execute_input":"2026-09-07T18:23:52.179511Z","iopub.status.busy":"2026-09-07T18:23:52.179087Z","iopub.status.idle":"2026-09-07T18:23:52.376024Z","shell.execute_reply":"2026-09-07T18:23:52.374924Z","shell.execute_reply.started":"2026-09-07T18:23:52.17948Z"},"papermill":{"duration":0.202859,"end_time":"2026-09-03T22:20:27.305817+00:00","exception":false,"start_time":"2026-09-03T22:20:27.102958+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"4af6a3a8","cell_type":"markdown","source":"**Interpretación — imágenes por paciente × etiología.** Las medias de slides asociadas a CE y LAA son\nsimilares en el agregado tabular; no hay evidencia fuerte de que una etiología concentre muchos más\nslides por paciente. La evaluación del reto es a nivel de **paciente**, no de imagen suelta.\n\n### Correlaciones (variables tabulares)\n\nEn `train` no hay pares numéricos sustanciales para un heatmap clásico: `center_id` es un código\ncategórico, `image_num` un índice y los IDs no deben correlacionarse. Las correlaciones útiles se\ncalculan sobre features derivadas de imagen (sección 6.3). Correlación ≠ causalidad.\n","metadata":{}},{"id":"a4e93041","cell_type":"markdown","source":"### Riesgo de fuga de información y grupos repetidos\n\n- **`label`** es el target: no debe usarse como feature.\n- **`image_id` / rutas / nombres de archivo** identifican el caso; no son features biológicas.\n- **`patient_id`:** 632 pacientes → 754 imágenes; un split aleatorio por fila puede mezclar el mismo\n  paciente entre conjuntos.\n- **`center_id`:** no es el diagnóstico, pero puede actuar como proxy de protocolo/escáner.\n","metadata":{}},{"id":"23bb6936","cell_type":"markdown","source":"## 6. Exploración a nivel de imagen\n\nLas imágenes son *whole-slide images* (WSI) en formato `.tif` de altísima resolución (pueden superar\n50,000 × 50,000 px y varios GB). El análisis se organiza así:\n\n1. **Inventario completo** CSV ↔ archivos (existencia y tamaño en disco; no se abre cada WSI entera).\n2. **Muestra estratificada** CE/LAA (`RANDOM_SEED=42`) con thumbnails OpenSlide para dimensiones,\n   canales, intensidad y proporción de tejido.\n","metadata":{}},{"id":"195fc624","cell_type":"markdown","source":"### 6.1 Inventario: correspondencia metadatos ↔ archivos\n","metadata":{}},{"id":"7932cd46","cell_type":"code","source":"def listar_tifs(carpeta):\n    if not os.path.isdir(carpeta):\n        return set()\n    return {f[:-4] for f in os.listdir(carpeta) if f.lower().endswith(\".tif\")}\n\ndef inventario_split(nombre, df_meta, img_dir, id_col=\"image_id\"):\n    ids_csv = set(df_meta[id_col].astype(str))\n    ids_disk = listar_tifs(img_dir)\n    faltan_archivo = sorted(ids_csv - ids_disk)\n    huerfanos = sorted(ids_disk - ids_csv)\n\n    # Tamaños en disco solo para IDs del CSV que existen (rápido: no abre OpenSlide)\n    tamanos_mb = []\n    for image_id in ids_csv & ids_disk:\n        fpath = os.path.join(img_dir, f\"{image_id}.tif\")\n        tamanos_mb.append(os.path.getsize(fpath) / (1024 ** 2))\n\n    resumen = {\n        \"split\": nombre,\n        \"filas_csv\": len(df_meta),\n        \"ids_csv_unicos\": len(ids_csv),\n        \"archivos_tif\": len(ids_disk),\n        \"csv_sin_archivo\": len(faltan_archivo),\n        \"tif_sin_csv\": len(huerfanos),\n        \"mb_min\": round(min(tamanos_mb), 1) if tamanos_mb else np.nan,\n        \"mb_mediana\": round(float(np.median(tamanos_mb)), 1) if tamanos_mb else np.nan,\n        \"mb_max\": round(max(tamanos_mb), 1) if tamanos_mb else np.nan,\n        \"mb_suma_gb\": round(sum(tamanos_mb) / 1024, 2) if tamanos_mb else np.nan,\n    }\n    return resumen, faltan_archivo, huerfanos, tamanos_mb\n\nresumenes = []\ndetalle_faltantes = {}\n\nres, faltan, huerf, mb_train = inventario_split(\"train\", train, TRAIN_IMG_DIR)\nresumenes.append(res)\ndetalle_faltantes[\"train\"] = {\"csv_sin_archivo\": faltan[:20], \"tif_sin_csv\": huerf[:20]}\n\nres, faltan, huerf, _ = inventario_split(\"test\", test, TEST_IMG_DIR)\nresumenes.append(res)\ndetalle_faltantes[\"test\"] = {\"csv_sin_archivo\": faltan[:20], \"tif_sin_csv\": huerf[:20]}\n\nif other is not None:\n    res, faltan, huerf, _ = inventario_split(\"other\", other, OTHER_IMG_DIR)\n    resumenes.append(res)\n    detalle_faltantes[\"other\"] = {\"csv_sin_archivo\": faltan[:20], \"tif_sin_csv\": huerf[:20]}\n\ndf_inventario = pd.DataFrame(resumenes)\nprint(\"Inventario CSV ↔ .tif\")\ndisplay(df_inventario)\n\nfor split, det in detalle_faltantes.items():\n    print(f\"\\n[{split}] ejemplos csv_sin_archivo ({len(det['csv_sin_archivo'])} mostrados):\", det[\"csv_sin_archivo\"])\n    print(f\"[{split}] ejemplos tif_sin_csv ({len(det['tif_sin_csv'])} mostrados):\", det[\"tif_sin_csv\"])\n\nplt.figure(figsize=(6, 4))\nsns.histplot(mb_train, bins=30, color=\"steelblue\")\nplt.title(\"Distribución del tamaño en disco — train (todas las WSI)\")\nplt.xlabel(\"Tamaño (MB)\")\nplt.ylabel(\"Número de archivos\")\nplt.tight_layout()\nplt.show()\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:23:52.377832Z","iopub.status.busy":"2026-09-07T18:23:52.377221Z","iopub.status.idle":"2026-09-07T18:23:53.831285Z","shell.execute_reply":"2026-09-07T18:23:53.830031Z","shell.execute_reply.started":"2026-09-07T18:23:52.377777Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"c1a6accf","cell_type":"markdown","source":"**Interpretación — inventario.** La correspondencia CSV↔`.tif` es completa en train, test y other:\n**0** filas sin archivo y **0** `.tif` huérfanos. En train los pesos van de **~7 MB a ~2.7 GB**\n(mediana ≈ **216 MB**; total ≈ **242 GB**). Eso confirma heterogeneidad extrema y la necesidad de\ntrabajar con thumbnails/tiling; no hay problema de integridad de emparejado metadatos–imagen.\n","metadata":{}},{"id":"7e359b41","cell_type":"code","source":"# Vista rápida de other.csv (etiologías fuera de CE/LAA; no son el target binario)\nif other is not None:\n    print(\"other shape:\", other.shape)\n    print(\"columnas:\", list(other.columns))\n    display(other.head())\n    if \"label\" in other.columns:\n        print(\"\\nFrecuencias en other['label']:\")\n        display(other[\"label\"].value_counts(dropna=False).to_frame(\"n\"))\nelse:\n    print(\"other.csv no disponible en este entorno.\")\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:23:53.832959Z","iopub.status.busy":"2026-09-07T18:23:53.832604Z","iopub.status.idle":"2026-09-07T18:23:53.850848Z","shell.execute_reply":"2026-09-07T18:23:53.850045Z","shell.execute_reply.started":"2026-09-07T18:23:53.832917Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"df98a740","cell_type":"markdown","source":"### 6.2 Muestra estratificada CE/LAA + metadatos de WSI\n\nSe abren solo `N_POR_CLASE` imágenes por etiqueta para obtener dimensiones, niveles piramidales,\ncanales (vía thumbnail RGB), intensidad media y detección de lecturas fallidas.\n","metadata":{}},{"id":"b3aee8a8","cell_type":"code","source":"N_POR_CLASE = 6          # 6 CE + 6 LAA = 12 WSI (ajustable)\nN_CHECK_LECTURA = 40     # chequeo extra de apertura (estratificado), sin guardar dims de todas\n\ndef muestreo_estratificado(df, n_por_clase, seed=RANDOM_SEED):\n    partes = []\n    for label, g in df.groupby(\"label\"):\n        k = min(n_por_clase, len(g))\n        partes.append(g.sample(n=k, random_state=seed))\n    return pd.concat(partes, ignore_index=True).sample(frac=1, random_state=seed).reset_index(drop=True)\n\ndef abrir_slide_info(fpath):\n    \"\"\"Devuelve dict con metadatos o error. No carga el WSI completo.\"\"\"\n    info = {\"ok\": False, \"error\": None}\n    try:\n        if OPENSLIDE_OK:\n            slide = openslide.OpenSlide(fpath)\n            w, h = slide.dimensions\n            info.update({\n                \"ok\": True,\n                \"ancho_px\": int(w),\n                \"alto_px\": int(h),\n                \"n_niveles\": slide.level_count,\n                \"nivel0_downsample\": float(slide.level_downsamples[0]),\n            })\n            thumb = slide.get_thumbnail((256, 256)).convert(\"RGB\")\n            slide.close()\n        else:\n            with Image.open(fpath) as im:\n                w, h = im.size\n                thumb = im.copy()\n                thumb.thumbnail((256, 256))\n                thumb = thumb.convert(\"RGB\")\n            info.update({\n                \"ok\": True,\n                \"ancho_px\": int(w),\n                \"alto_px\": int(h),\n                \"n_niveles\": np.nan,\n                \"nivel0_downsample\": np.nan,\n            })\n        arr = np.asarray(thumb)\n        info[\"canales\"] = int(arr.shape[2]) if arr.ndim == 3 else 1\n        info[\"intensidad_media\"] = float(arr.mean())\n        info[\"intensidad_std\"] = float(arr.std())\n        fondo = np.all(arr > 220, axis=-1) if arr.ndim == 3 else (arr > 220)\n        info[\"prop_tejido\"] = float(1 - fondo.mean())\n        info[\"thumb\"] = thumb\n    except Exception as e:\n        info[\"error\"] = f\"{type(e).__name__}: {e}\"\n    return info\n\nmuestra = muestreo_estratificado(train, N_POR_CLASE, seed=RANDOM_SEED)\nprint(\"Composición de la muestra estratificada:\")\nprint(muestra[\"label\"].value_counts())\n\nregistros = []\nthumbs_map = {}\nfor _, row in muestra.iterrows():\n    fpath = os.path.join(TRAIN_IMG_DIR, f\"{row['image_id']}.tif\")\n    rec = {\n        \"image_id\": row[\"image_id\"],\n        \"label\": row[\"label\"],\n        \"patient_id\": row[\"patient_id\"],\n        \"center_id\": row[\"center_id\"],\n        \"existe_archivo\": os.path.exists(fpath),\n        \"tam_disco_mb\": round(os.path.getsize(fpath) / (1024 ** 2), 1) if os.path.exists(fpath) else np.nan,\n    }\n    if rec[\"existe_archivo\"]:\n        info = abrir_slide_info(fpath)\n        rec.update({k: info.get(k) for k in [\n            \"ok\", \"error\", \"ancho_px\", \"alto_px\", \"n_niveles\", \"canales\",\n            \"intensidad_media\", \"intensidad_std\", \"prop_tejido\"\n        ]})\n        if info.get(\"ok\"):\n            rec[\"aspect_ratio\"] = round(info[\"ancho_px\"] / info[\"alto_px\"], 2)\n            thumbs_map[row[\"image_id\"]] = info[\"thumb\"]\n    else:\n        rec.update({\"ok\": False, \"error\": \"archivo_ausente\"})\n    registros.append(rec)\n\ndf_imgs = pd.DataFrame(registros)\nprint(f\"\\nLecturas OK: {df_imgs['ok'].sum()} / {len(df_imgs)}\")\nif (~df_imgs[\"ok\"]).any():\n    print(\"Fallos:\")\n    display(df_imgs.loc[~df_imgs[\"ok\"], [\"image_id\", \"label\", \"error\"]])\n\ndisplay(df_imgs)\nprint(f\"Thumbnails guardados: {len(thumbs_map)}\")\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:23:53.852704Z","iopub.status.busy":"2026-09-07T18:23:53.852254Z","iopub.status.idle":"2026-09-07T18:36:16.706759Z","shell.execute_reply":"2026-09-07T18:36:16.705453Z","shell.execute_reply.started":"2026-09-07T18:23:53.85266Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"603818d5","cell_type":"code","source":"# Chequeo adicional de lectura en una muestra estratificada más amplia (solo open/close)\ncheck_df = muestreo_estratificado(train, n_por_clase=N_CHECK_LECTURA // 2, seed=RANDOM_SEED)\nerrores_lectura = []\nfor _, row in check_df.iterrows():\n    fpath = os.path.join(TRAIN_IMG_DIR, f\"{row['image_id']}.tif\")\n    if not os.path.exists(fpath):\n        errores_lectura.append({\"image_id\": row[\"image_id\"], \"label\": row[\"label\"], \"error\": \"archivo_ausente\"})\n        continue\n    try:\n        if OPENSLIDE_OK:\n            slide = openslide.OpenSlide(fpath)\n            _ = slide.dimensions\n            slide.close()\n        else:\n            with Image.open(fpath) as im:\n                _ = im.size\n    except Exception as e:\n        errores_lectura.append({\n            \"image_id\": row[\"image_id\"],\n            \"label\": row[\"label\"],\n            \"error\": f\"{type(e).__name__}: {e}\",\n        })\n\nprint(f\"Chequeo de lectura: {len(check_df)} archivos\")\nprint(f\"Errores: {len(errores_lectura)}\")\nif errores_lectura:\n    display(pd.DataFrame(errores_lectura))\nelse:\n    print(\"No se detectaron lecturas fallidas en la muestra de chequeo.\")\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:36:16.708342Z","iopub.status.busy":"2026-09-07T18:36:16.707979Z","iopub.status.idle":"2026-09-07T18:36:17.505898Z","shell.execute_reply":"2026-09-07T18:36:17.504523Z","shell.execute_reply.started":"2026-09-07T18:36:16.708301Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"60a77995","cell_type":"code","source":"cols_num = [\"ancho_px\", \"alto_px\", \"tam_disco_mb\", \"aspect_ratio\",\n            \"intensidad_media\", \"intensidad_std\", \"prop_tejido\", \"n_niveles\", \"canales\"]\ncols_num = [c for c in cols_num if c in df_imgs.columns]\nprint(\"Estadísticos de la muestra estratificada (lecturas OK):\")\ndisplay(df_imgs.loc[df_imgs[\"ok\"], cols_num].describe())\n\nprint(\"\\nResumen por etiología:\")\ndisplay(df_imgs.loc[df_imgs[\"ok\"]].groupby(\"label\")[cols_num].agg([\"mean\", \"median\", \"std\", \"min\", \"max\"]).round(3))\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:36:17.508293Z","iopub.status.busy":"2026-09-07T18:36:17.507279Z","iopub.status.idle":"2026-09-07T18:36:17.638806Z","shell.execute_reply":"2026-09-07T18:36:17.637044Z","shell.execute_reply.started":"2026-09-07T18:36:17.508247Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"7a4bde22","cell_type":"markdown","source":"**Interpretación — metadatos de muestra.** Con **6 CE + 6 LAA** (12/12 lecturas OK) las dimensiones\nsiguen siendo enormes (anchos ≈ 6k–47k px; altos ≈ 17k–68k px) y el peso en disco muy variable\n(≈ 40 MB–1.2 GB en la muestra). En esta muestra todas las WSI abiertas reportaron **3 canales** y\n**1 nivel** piramidal visible vía OpenSlide. Las medias de intensidad son similares entre CE y LAA\n(≈ 238 vs ≈ 236); **no hay separación clara por tamaño o brillo** en n=12. `prop_tejido` sí es muy\nheterogéneo (≈ 0.02–1.0), con láminas casi vacías de tejido aparente y otras casi llenas.\n","metadata":{}},{"id":"d88639bd","cell_type":"code","source":"ok = df_imgs[\"ok\"].fillna(False)\nfig, axes = plt.subplots(1, 3, figsize=(14, 4))\n\nsns.histplot(data=df_imgs.loc[ok], x=\"tam_disco_mb\", hue=\"label\", bins=8, ax=axes[0], element=\"step\", common_norm=False)\naxes[0].set_title(\"Tamaño en disco (MB) por clase\")\naxes[0].set_xlabel(\"Tamaño (MB)\")\naxes[0].set_ylabel(\"Frecuencia\")\n\nsns.scatterplot(data=df_imgs.loc[ok], x=\"ancho_px\", y=\"alto_px\", hue=\"label\", ax=axes[1], s=80)\naxes[1].set_title(\"Dimensiones (ancho vs alto)\")\naxes[1].set_xlabel(\"Ancho (px)\")\naxes[1].set_ylabel(\"Alto (px)\")\n\nsns.boxplot(data=df_imgs.loc[ok], x=\"label\", y=\"intensidad_media\", hue=\"label\", ax=axes[2], legend=False)\naxes[2].set_title(\"Intensidad media del thumbnail\")\naxes[2].set_xlabel(\"Etiología\")\naxes[2].set_ylabel(\"Intensidad media (0-255)\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:36:17.641377Z","iopub.status.busy":"2026-09-07T18:36:17.640552Z","iopub.status.idle":"2026-09-07T18:36:18.277463Z","shell.execute_reply":"2026-09-07T18:36:18.276456Z","shell.execute_reply.started":"2026-09-07T18:36:17.64121Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"ba4c355e","cell_type":"markdown","source":"**Interpretación — gráficos.** El histograma/scatter muestran solapamiento CE/LAA en tamaño y\nresolución: la etiología **no se lee** solo de ancho×alto o MB. El boxplot de intensidad media del\nthumbnail tampoco separa clases de forma útil en esta muestra; el valor analítico está en documentar\ndispersión y posibles extremos de adquisición, no en clasificar el origen del coágulo.\n","metadata":{}},{"id":"8f3dec90","cell_type":"code","source":"# Thumbnails balanceados: misma cantidad por clase (usa thumbs_map de la celda anterior)\nthumbs = df_imgs.loc[df_imgs[\"ok\"]].copy()\nthumbs = thumbs[thumbs[\"image_id\"].isin(thumbs_map)]\nthumbs[\"label\"] = pd.Categorical(thumbs[\"label\"], categories=[\"CE\", \"LAA\"], ordered=True)\nthumbs = thumbs.sort_values([\"label\", \"image_id\"]).reset_index(drop=True)\n\nn = len(thumbs)\nncols = 4\nnrows = int(np.ceil(n / ncols)) if n else 1\nfig, axes = plt.subplots(nrows, ncols, figsize=(14, 3.2 * max(nrows, 1)))\naxes = np.atleast_1d(axes).ravel()\n\nfor ax, (_, row) in zip(axes, thumbs.iterrows()):\n    ax.imshow(thumbs_map[row[\"image_id\"]])\n    ax.set_title(f\"{row['label']} | {row['image_id']}\", fontsize=9)\n    ax.axis(\"off\")\nfor ax in axes[n:]:\n    ax.axis(\"off\")\n\nplt.suptitle(f\"Muestra estratificada de coágulos (thumbnails) — {N_POR_CLASE} por clase\")\nplt.tight_layout()\nplt.show()\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:36:18.27937Z","iopub.status.busy":"2026-09-07T18:36:18.279041Z","iopub.status.idle":"2026-09-07T18:36:19.027444Z","shell.execute_reply":"2026-09-07T18:36:19.026422Z","shell.execute_reply.started":"2026-09-07T18:36:18.279342Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"bcacef42","cell_type":"markdown","source":"**Interpretación — thumbnails.** La muestra estratificada permite comparar CE y LAA en igualdad de\nconteo. Visualmente predominan fondos claros y morfologías distintas del coágulo; hay casos con casi\ntodo el recorte en fondo y otros con tejido dominante.\n","metadata":{}},{"id":"d10e538b","cell_type":"code","source":"plt.figure(figsize=(5, 4))\nsns.boxplot(\n    data=df_imgs.loc[df_imgs[\"ok\"]],\n    x=\"label\", y=\"prop_tejido\", hue=\"label\", palette=\"Set3\", legend=False\n)\nplt.title(\"Proporción estimada de tejido vs. fondo, por etiología\")\nplt.xlabel(\"Etiología\")\nplt.ylabel(\"Proporción de tejido (thumbnail)\")\nplt.tight_layout()\nplt.show()\n\nprint(df_imgs.loc[df_imgs[\"ok\"], [\"image_id\", \"label\", \"prop_tejido\", \"intensidad_media\", \"tam_disco_mb\"]])\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:36:19.029Z","iopub.status.busy":"2026-09-07T18:36:19.028629Z","iopub.status.idle":"2026-09-07T18:36:19.212248Z","shell.execute_reply":"2026-09-07T18:36:19.211285Z","shell.execute_reply.started":"2026-09-07T18:36:19.028962Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"37f26f16","cell_type":"markdown","source":"**Interpretación — tejido.** `prop_tejido` oscila de casi 0 a casi 1 dentro de la misma muestra\n(p. ej. LAA con 0.02 y 0.99). Hay láminas con predominio de fondo blanco. En n=12 **no se concluye**\nque CE o LAA tengan sistemáticamente más tejido; la proporción de fondo es un factor de calidad dominante.\n","metadata":{}},{"id":"cdc73a38","cell_type":"markdown","source":"### 6.3 Correlación entre propiedades derivadas de imagen\n\nSolo variables numéricas de la muestra estratificada (sin IDs). Correlación ≠ causalidad.\n","metadata":{}},{"id":"8ccda1a0","cell_type":"code","source":"cols_corr = [c for c in [\n    \"ancho_px\", \"alto_px\", \"tam_disco_mb\", \"aspect_ratio\",\n    \"intensidad_media\", \"intensidad_std\", \"prop_tejido\"\n] if c in df_imgs.columns]\n\ncorr = df_imgs.loc[df_imgs[\"ok\"], cols_corr].corr(numeric_only=True)\nprint(\"Matriz de correlación (muestra estratificada):\")\ndisplay(corr.round(3))\n\nplt.figure(figsize=(6, 5))\nsns.heatmap(corr, annot=True, cmap=\"coolwarm\", vmin=-1, vmax=1, square=True, fmt=\".2f\")\nplt.title(\"Correlación — propiedades de WSI (muestra)\")\nplt.tight_layout()\nplt.show()\n\n# Pares con |r| más alto (excluyendo diagonal)\npares = (\n    corr.where(np.triu(np.ones(corr.shape), k=1).astype(bool))\n    .stack()\n    .abs()\n    .sort_values(ascending=False)\n)\nprint(\"\\nPares con mayor |correlación|:\")\nprint(pares.head(5))\n","metadata":{"execution":{"iopub.execute_input":"2026-09-07T18:36:19.213736Z","iopub.status.busy":"2026-09-07T18:36:19.213374Z","iopub.status.idle":"2026-09-07T18:36:19.577763Z","shell.execute_reply":"2026-09-07T18:36:19.576657Z","shell.execute_reply.started":"2026-09-07T18:36:19.213697Z"},"trusted":true},"outputs":[],"execution_count":null},{"id":"32e9670e","cell_type":"markdown","source":"## 7. Hallazgos y conclusiones\n\n### Hallazgos\n\n1. **Desbalance de clases:** CE **72.5%** (547) vs LAA **27.5%** (207).\n2. **Sesgo por centro:** 11 centros con aporte desigual (el centro 11 concentra ~34%). El centro 3\n   invierte la proporción (LAA ≈ 55%), con riesgo de *confounding* por centro.\n3. **Estructura por paciente:** 632 pacientes / 754 imágenes. Por IQR, **89 pacientes (14.1%)** tienen\n   2–5 slides; son casos válidos, **no errores**. Varias imágenes pueden pertenecer al mismo `patient_id`.\n4. **Calidad tabular:** train sin valores faltantes ni `image_id` duplicados.\n5. **Integridad imagen–metadatos:** 754/754, 4/4 y 396/396 emparejados; sin huérfanos. Train suma ≈ **242 GB**\n   (≈ 7 MB–2.7 GB por archivo).\n6. **WSI heterogéneas:** resoluciones de miles a decenas de miles de píxeles. Lectura OK en la muestra\n   estratificada (12/12) y en el chequeo ampliado (40/40).\n7. **Fondo vs tejido:** `prop_tejido` ≈ 0.02–1.0 en la muestra; muchas láminas tienen mucho fondo blanco.\n8. **Correlaciones (muestra):** las más altas son ancho↔tamaño en disco (≈ **0.88**) e\n   intensidad media↔`prop_tejido` (≈ **−0.86**). Relaciones esperables; **correlación ≠ causalidad** y n=12\n   es orientativo.\n9. **`other`:** 396 casos con etiquetas `Unknown` (331) y `Other` (65); no forman el target binario CE/LAA.\n\n### Conclusiones\n\nEl conjunto Mayo Clinic STRIP AI está **tabularmente limpio y bien emparejado con las WSI**, pero presenta\n**desbalance CE/LAA**, **aporte desigual por centro** e **imágenes enormes y heterogéneas** con proporción de\ntejido muy variable. El EDA caracteriza esas propiedades; dimensiones o intensidad media **no separan** de\nforma clara el origen del coágulo en la muestra analizada.\n\n**Limitaciones.** Intensidad, tejido y correlaciones de imagen se estimaron sobre thumbnails de **12 WSI**\nestratificadas, no sobre las 754 a resolución completa. El `test` público en Kaggle es reducido.\n","metadata":{}}]}