{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    },
    "widgets": {
      "application/vnd.jupyter.widget-state+json": {
        "version_major": 2,
        "version_minor": 0,
        "state": {
          "5a8b8c2fe93f4864a1c6c3eecd1b154e": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HBoxModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HBoxModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HBoxView",
              "box_style": "",
              "children": [
                "IPY_MODEL_2a60552d9ca24ebb8bde706caffb288b",
                "IPY_MODEL_1435bd36758d4edcbb4e3c8a85200330",
                "IPY_MODEL_ba3fe51b37f04a90bb61716515cc18cd"
              ],
              "layout": "IPY_MODEL_4d144546b6f545a496aaf49246b264a6"
            }
          },
          "2a60552d9ca24ebb8bde706caffb288b": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_d1bff2cc36d342ad95d7048fc1712737",
              "placeholder": "​",
              "style": "IPY_MODEL_bab0258e9beb47449b68e08756e88a0a",
              "value": "Descargando ASOS: 100%"
            }
          },
          "1435bd36758d4edcbb4e3c8a85200330": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "FloatProgressModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "FloatProgressModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "ProgressView",
              "bar_style": "success",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_ce5c4b346b124a93aa6e3ad7d2cbfb1c",
              "max": 16,
              "min": 0,
              "orientation": "horizontal",
              "style": "IPY_MODEL_01e0918b453b4f2992b8186f2ecd2db3",
              "value": 16
            }
          },
          "ba3fe51b37f04a90bb61716515cc18cd": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_3b4da1d71dba45d99550a0ff6d2d8947",
              "placeholder": "​",
              "style": "IPY_MODEL_1ad9b04b2db6464898c59e7f2987ada9",
              "value": " 16/16 [04:08&lt;00:00, 15.51s/it]"
            }
          },
          "4d144546b6f545a496aaf49246b264a6": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "d1bff2cc36d342ad95d7048fc1712737": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "bab0258e9beb47449b68e08756e88a0a": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          },
          "ce5c4b346b124a93aa6e3ad7d2cbfb1c": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "01e0918b453b4f2992b8186f2ecd2db3": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "ProgressStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "ProgressStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "bar_color": null,
              "description_width": ""
            }
          },
          "3b4da1d71dba45d99550a0ff6d2d8947": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "1ad9b04b2db6464898c59e7f2987ada9": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          },
          "d32fd123693540e482c737441ba1df14": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HBoxModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HBoxModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HBoxView",
              "box_style": "",
              "children": [
                "IPY_MODEL_e0c110fee938460c96fa70c838ec41d8",
                "IPY_MODEL_b4351a5aab914373ae3d328f081e7494",
                "IPY_MODEL_5f5838184c494963bbec7e176628e1d6"
              ],
              "layout": "IPY_MODEL_20996146f0794c019dd1b40a5e080393"
            }
          },
          "e0c110fee938460c96fa70c838ec41d8": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_90a1118af8a041218ed7196e949f9f4b",
              "placeholder": "​",
              "style": "IPY_MODEL_9daff681a7ca4be4892a4f1d9201a264",
              "value": "Procesando: 100%"
            }
          },
          "b4351a5aab914373ae3d328f081e7494": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "FloatProgressModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "FloatProgressModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "ProgressView",
              "bar_style": "success",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_a726158af3e54d458b3aed03f1df5cbe",
              "max": 104,
              "min": 0,
              "orientation": "horizontal",
              "style": "IPY_MODEL_df4ab8be122b454f8d4147a6dc5fd9e4",
              "value": 104
            }
          },
          "5f5838184c494963bbec7e176628e1d6": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_406a24fc92be42a49743e4c6c79cf8ed",
              "placeholder": "​",
              "style": "IPY_MODEL_661c9f05f3ae472ba0cc1096b41cce2f",
              "value": " 104/104 [00:01&lt;00:00, 66.48it/s]"
            }
          },
          "20996146f0794c019dd1b40a5e080393": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "90a1118af8a041218ed7196e949f9f4b": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "9daff681a7ca4be4892a4f1d9201a264": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          },
          "a726158af3e54d458b3aed03f1df5cbe": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "df4ab8be122b454f8d4147a6dc5fd9e4": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "ProgressStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "ProgressStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "bar_color": null,
              "description_width": ""
            }
          },
          "406a24fc92be42a49743e4c6c79cf8ed": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "661c9f05f3ae472ba0cc1096b41cce2f": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          },
          "2303adb59f8343e4bf7ba7e0bfec18b0": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HBoxModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HBoxModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HBoxView",
              "box_style": "",
              "children": [
                "IPY_MODEL_b8fb929fd45e4eed85a60b7b7aed8170",
                "IPY_MODEL_89fff66d849f43b9b37c3e9f2388a8b4",
                "IPY_MODEL_391048ad027a4e2ab1e9f7abc916b59e"
              ],
              "layout": "IPY_MODEL_503bbe389cd0481cbcadc4ccfdb06418"
            }
          },
          "b8fb929fd45e4eed85a60b7b7aed8170": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_2f85c358cb7d477b9dd28112c710fa6a",
              "placeholder": "​",
              "style": "IPY_MODEL_9033eec2233f4a8baa99fe4917626266",
              "value": "Radiación ERA5: 100%"
            }
          },
          "89fff66d849f43b9b37c3e9f2388a8b4": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "FloatProgressModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "FloatProgressModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "ProgressView",
              "bar_style": "success",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_dc0f41f027df4fdd9d57bb30a8b817a8",
              "max": 16,
              "min": 0,
              "orientation": "horizontal",
              "style": "IPY_MODEL_d00ba11cedc34305aa42f7fc55f94ea1",
              "value": 16
            }
          },
          "391048ad027a4e2ab1e9f7abc916b59e": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_a9ff0704d67a456d9960a9f89b1613f4",
              "placeholder": "​",
              "style": "IPY_MODEL_553cecb9b6e943b3a31d2b10174019cc",
              "value": " 16/16 [00:04&lt;00:00,  3.26it/s]"
            }
          },
          "503bbe389cd0481cbcadc4ccfdb06418": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "2f85c358cb7d477b9dd28112c710fa6a": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "9033eec2233f4a8baa99fe4917626266": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          },
          "dc0f41f027df4fdd9d57bb30a8b817a8": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "d00ba11cedc34305aa42f7fc55f94ea1": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "ProgressStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "ProgressStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "bar_color": null,
              "description_width": ""
            }
          },
          "a9ff0704d67a456d9960a9f89b1613f4": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "553cecb9b6e943b3a31d2b10174019cc": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          },
          "4a68404920cb48c099155bf3c233fafb": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HBoxModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HBoxModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HBoxView",
              "box_style": "",
              "children": [
                "IPY_MODEL_c1488996287d429495db8fda2fc92946",
                "IPY_MODEL_e827aebbb971422b9e24c324e7052805",
                "IPY_MODEL_ef89e7faaa22420a974ff1965b2bfc5c"
              ],
              "layout": "IPY_MODEL_156a1f501dc8464ba44d1da8416ec5d9"
            }
          },
          "c1488996287d429495db8fda2fc92946": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_7588c70956fe48c2b6d4ec7f18837097",
              "placeholder": "​",
              "style": "IPY_MODEL_7c7ab54c68e642deac402aff098e32c7",
              "value": "Recalculando con rad ERA5: 100%"
            }
          },
          "e827aebbb971422b9e24c324e7052805": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "FloatProgressModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "FloatProgressModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "ProgressView",
              "bar_style": "success",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_59ca34068ce845d29d8d0de02871e81e",
              "max": 104,
              "min": 0,
              "orientation": "horizontal",
              "style": "IPY_MODEL_6dd9a828863844baba24a44db749fbbd",
              "value": 104
            }
          },
          "ef89e7faaa22420a974ff1965b2bfc5c": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "HTMLModel",
            "model_module_version": "1.5.0",
            "state": {
              "_dom_classes": [],
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "HTMLModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/controls",
              "_view_module_version": "1.5.0",
              "_view_name": "HTMLView",
              "description": "",
              "description_tooltip": null,
              "layout": "IPY_MODEL_6d1f0cba076c4356996c16bf7ffcfe5a",
              "placeholder": "​",
              "style": "IPY_MODEL_8c0d2011e7754ff4a45c4ca3e2cc3d44",
              "value": " 104/104 [00:01&lt;00:00, 63.41it/s]"
            }
          },
          "156a1f501dc8464ba44d1da8416ec5d9": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "7588c70956fe48c2b6d4ec7f18837097": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "7c7ab54c68e642deac402aff098e32c7": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          },
          "59ca34068ce845d29d8d0de02871e81e": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "6dd9a828863844baba24a44db749fbbd": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "ProgressStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "ProgressStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "bar_color": null,
              "description_width": ""
            }
          },
          "6d1f0cba076c4356996c16bf7ffcfe5a": {
            "model_module": "@jupyter-widgets/base",
            "model_name": "LayoutModel",
            "model_module_version": "1.2.0",
            "state": {
              "_model_module": "@jupyter-widgets/base",
              "_model_module_version": "1.2.0",
              "_model_name": "LayoutModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "LayoutView",
              "align_content": null,
              "align_items": null,
              "align_self": null,
              "border": null,
              "bottom": null,
              "display": null,
              "flex": null,
              "flex_flow": null,
              "grid_area": null,
              "grid_auto_columns": null,
              "grid_auto_flow": null,
              "grid_auto_rows": null,
              "grid_column": null,
              "grid_gap": null,
              "grid_row": null,
              "grid_template_areas": null,
              "grid_template_columns": null,
              "grid_template_rows": null,
              "height": null,
              "justify_content": null,
              "justify_items": null,
              "left": null,
              "margin": null,
              "max_height": null,
              "max_width": null,
              "min_height": null,
              "min_width": null,
              "object_fit": null,
              "object_position": null,
              "order": null,
              "overflow": null,
              "overflow_x": null,
              "overflow_y": null,
              "padding": null,
              "right": null,
              "top": null,
              "visibility": null,
              "width": null
            }
          },
          "8c0d2011e7754ff4a45c4ca3e2cc3d44": {
            "model_module": "@jupyter-widgets/controls",
            "model_name": "DescriptionStyleModel",
            "model_module_version": "1.5.0",
            "state": {
              "_model_module": "@jupyter-widgets/controls",
              "_model_module_version": "1.5.0",
              "_model_name": "DescriptionStyleModel",
              "_view_count": null,
              "_view_module": "@jupyter-widgets/base",
              "_view_module_version": "1.2.0",
              "_view_name": "StyleView",
              "description_width": ""
            }
          }
        }
      }
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "282P1qbnhEtV"
      },
      "source": [
        "# Mundial 2026 — WBGT NOAA Observacional (15 sedes + KPHL)\n",
        "## Notebook DEFINITIVO para el artículo IJBM\n",
        "\n",
        "**Cambios respecto a la versión anterior:**\n",
        "1. Filadelfia: KTTN (30 km) → KPHL/PHL (7 km)\n",
        "2. Delays de 15s entre peticiones IEM para evitar HTTP 429\n",
        "3. Re-descarga de KC, SF, Toronto con reintentos\n",
        "4. sdh₂₈ corregido: usa integración horaria (B7_grados_hora × asistencia)\n",
        "5. Bland-Altman añadido\n",
        "6. Leave-one-venue-out ICC añadido\n",
        "7. Bootstrap de disparidad corregido (Houston − sede más fría disponible)\n",
        "8. Todas las celdas de análisis operan sobre el dataset completo post-pipeline\n"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "8JUeiLaYhEti",
        "outputId": "b0e09397-8c8b-4911-926e-83e7193f3ea0"
      },
      "source": [
        "# Celda 1 — Instalación e imports\n",
        "!pip install openpyxl pysolar tqdm statsmodels -q\n",
        "\n",
        "import pandas as pd\n",
        "import numpy as np\n",
        "from datetime import datetime, timedelta, timezone\n",
        "import pysolar.solar as solar\n",
        "import requests, io, time, warnings\n",
        "from tqdm.notebook import tqdm\n",
        "import statsmodels.api as sm\n",
        "import statsmodels.formula.api as smf\n",
        "from scipy import stats as sp_stats\n",
        "import matplotlib\n",
        "matplotlib.use('Agg')\n",
        "import matplotlib.pyplot as plt\n",
        "warnings.filterwarnings('ignore')\n",
        "print('✅ Imports OK')\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\u001b[?25l   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/46.9 kB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m46.9/46.9 kB\u001b[0m \u001b[31m1.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
            "\u001b[?25h✅ Imports OK\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-nwIBinChEtn",
        "outputId": "19b215a5-d5cd-44f3-ae32-15ffe9acbd33"
      },
      "source": [
        "# Celda 2 — 16 sedes con ICAO CORREGIDOS (Filadelfia = KPHL)\n",
        "sedes = {\n",
        "  1:  {'ciudad':'Ciudad de Mexico','icao':'MMMX','iem':'MMMX','lat':19.3029,'lon':-99.1505,'utc_offset':-6,'dist_km':10,'alt_m':2195},\n",
        "  2:  {'ciudad':'Guadalajara','icao':'MMGL','iem':'MMGL','lat':20.6810,'lon':-103.4625,'utc_offset':-6,'dist_km':25,'alt_m':1560},\n",
        "  3:  {'ciudad':'Monterrey','icao':'MMMY','iem':'MMMY','lat':25.6700,'lon':-100.2440,'utc_offset':-6,'dist_km':25,'alt_m':540},\n",
        "  4:  {'ciudad':'Atlanta','icao':'KATL','iem':'KATL','lat':33.7553,'lon':-84.4006,'utc_offset':-5,'dist_km':15,'alt_m':320},\n",
        "  5:  {'ciudad':'Dallas','icao':'KDFW','iem':'KDFW','lat':32.7473,'lon':-97.0945,'utc_offset':-6,'dist_km':20,'alt_m':180},\n",
        "  6:  {'ciudad':'Houston','icao':'KHOU','iem':'KHOU','lat':29.6847,'lon':-95.4107,'utc_offset':-6,'dist_km':15,'alt_m':15},\n",
        "  7:  {'ciudad':'Los Angeles','icao':'KLAX','iem':'KLAX','lat':33.9534,'lon':-118.3387,'utc_offset':-8,'dist_km':10,'alt_m':30},\n",
        "  8:  {'ciudad':'Kansas City','icao':'KMCI','iem':'KMCI','lat':39.0489,'lon':-94.4839,'utc_offset':-6,'dist_km':20,'alt_m':270},\n",
        "  9:  {'ciudad':'Miami','icao':'KMIA','iem':'KMIA','lat':25.9580,'lon':-80.2389,'utc_offset':-5,'dist_km':25,'alt_m':3},\n",
        "  10: {'ciudad':'Filadelfia','icao':'KPHL','iem':'PHL','lat':39.8744,'lon':-75.2424,'utc_offset':-5,'dist_km':7,'alt_m':10},\n",
        "  11: {'ciudad':'Boston','icao':'KBOS','iem':'KBOS','lat':42.0909,'lon':-71.2643,'utc_offset':-5,'dist_km':40,'alt_m':90},\n",
        "  12: {'ciudad':'Nueva York/NJ','icao':'KEWR','iem':'KEWR','lat':40.8128,'lon':-74.0742,'utc_offset':-5,'dist_km':10,'alt_m':5},\n",
        "  13: {'ciudad':'San Francisco','icao':'KSJC','iem':'KSJC','lat':37.4033,'lon':-121.9694,'utc_offset':-8,'dist_km':8,'alt_m':5},\n",
        "  14: {'ciudad':'Seattle','icao':'KSEA','iem':'KSEA','lat':47.5952,'lon':-122.3316,'utc_offset':-8,'dist_km':18,'alt_m':5},\n",
        "  15: {'ciudad':'Toronto','icao':'CYTZ','iem':'CYTZ','lat':43.6332,'lon':-79.4186,'utc_offset':-5,'dist_km':2,'alt_m':76},\n",
        "  16: {'ciudad':'Vancouver','icao':'CYVR','iem':'CYVR','lat':49.2768,'lon':-123.1118,'utc_offset':-8,'dist_km':15,'alt_m':0},\n",
        "}\n",
        "\n",
        "# Mapeo estadio → sede_id\n",
        "est_map = {\n",
        "    'Estadio Azteca':1,'Estadio Akron':2,'Estadio BBVA':3,'Mercedes-Benz Stadium':4,\n",
        "    'AT&T Stadium':5,'NRG Stadium':6,'SoFi Stadium':7,'Arrowhead Stadium':8,\n",
        "    'Hard Rock Stadium':9,'Lincoln Financial Field':10,'Gillette Stadium':11,\n",
        "    'MetLife Stadium':12,'Levis Stadium':13,'Lumen Field':14,'BMO Field':15,'BC Place':16\n",
        "}\n",
        "\n",
        "print(f'{len(sedes)} sedes definidas:')\n",
        "for eid, s in sedes.items():\n",
        "    print(f'  {s[\"ciudad\"]:20s}: {s[\"icao\"]} (IEM: {s[\"iem\"]}, {s[\"dist_km\"]} km, {s[\"alt_m\"]}m)')\n",
        "print(f'\\n⚠ Filadelfia ahora usa KPHL (PHL en IEM) a 7 km, no KTTN (30 km)')\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "16 sedes definidas:\n",
            "  Ciudad de Mexico    : MMMX (IEM: MMMX, 10 km, 2195m)\n",
            "  Guadalajara         : MMGL (IEM: MMGL, 25 km, 1560m)\n",
            "  Monterrey           : MMMY (IEM: MMMY, 25 km, 540m)\n",
            "  Atlanta             : KATL (IEM: KATL, 15 km, 320m)\n",
            "  Dallas              : KDFW (IEM: KDFW, 20 km, 180m)\n",
            "  Houston             : KHOU (IEM: KHOU, 15 km, 15m)\n",
            "  Los Angeles         : KLAX (IEM: KLAX, 10 km, 30m)\n",
            "  Kansas City         : KMCI (IEM: KMCI, 20 km, 270m)\n",
            "  Miami               : KMIA (IEM: KMIA, 25 km, 3m)\n",
            "  Filadelfia          : KPHL (IEM: PHL, 7 km, 10m)\n",
            "  Boston              : KBOS (IEM: KBOS, 40 km, 90m)\n",
            "  Nueva York/NJ       : KEWR (IEM: KEWR, 10 km, 5m)\n",
            "  San Francisco       : KSJC (IEM: KSJC, 8 km, 5m)\n",
            "  Seattle             : KSEA (IEM: KSEA, 18 km, 5m)\n",
            "  Toronto             : CYTZ (IEM: CYTZ, 2 km, 76m)\n",
            "  Vancouver           : CYVR (IEM: CYVR, 15 km, 0m)\n",
            "\n",
            "⚠ Filadelfia ahora usa KPHL (PHL en IEM) a 7 km, no KTTN (30 km)\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 413,
          "referenced_widgets": [
            "5a8b8c2fe93f4864a1c6c3eecd1b154e",
            "2a60552d9ca24ebb8bde706caffb288b",
            "1435bd36758d4edcbb4e3c8a85200330",
            "ba3fe51b37f04a90bb61716515cc18cd",
            "4d144546b6f545a496aaf49246b264a6",
            "d1bff2cc36d342ad95d7048fc1712737",
            "bab0258e9beb47449b68e08756e88a0a",
            "ce5c4b346b124a93aa6e3ad7d2cbfb1c",
            "01e0918b453b4f2992b8186f2ecd2db3",
            "3b4da1d71dba45d99550a0ff6d2d8947",
            "1ad9b04b2db6464898c59e7f2987ada9"
          ]
        },
        "id": "DnnnsJXFhEtq",
        "outputId": "21629dd5-4e05-4937-b3d9-60f1fee5fe12"
      },
      "source": [
        "# Celda 3 — Descarga ASOS de las 16 sedes (con delays para evitar HTTP 429)\n",
        "BASE_URL = 'https://mesonet.agron.iastate.edu/cgi-bin/request/asos.py'\n",
        "datos_obs = {}\n",
        "info_estaciones = {}\n",
        "DELAY = 15  # segundos entre peticiones\n",
        "\n",
        "for eid, s in tqdm(sedes.items(), desc='Descargando ASOS'):\n",
        "    try:\n",
        "        station_code = s['iem']\n",
        "        url = f'{BASE_URL}?station={station_code}'\n",
        "        for d in ['tmpf','dwpf','relh','sknt']:\n",
        "            url += f'&data={d}'\n",
        "        url += '&year1=2026&month1=6&day1=1&year2=2026&month2=7&day2=22'\n",
        "        url += '&tz=Etc/UTC&format=comma&latlon=yes&missing=M&direct=yes&report_type=3'\n",
        "\n",
        "        resp = requests.get(url, timeout=30)\n",
        "\n",
        "        # Reintento si rate limited\n",
        "        if resp.status_code == 429:\n",
        "            print(f'  ⏳ Rate limited {s[\"ciudad\"]}, esperando 60s...')\n",
        "            time.sleep(60)\n",
        "            resp = requests.get(url, timeout=30)\n",
        "\n",
        "        if resp.status_code != 200:\n",
        "            print(f'  ❌ {s[\"ciudad\"]} ({station_code}): HTTP {resp.status_code}')\n",
        "            datos_obs[eid] = pd.DataFrame()\n",
        "            time.sleep(DELAY)\n",
        "            continue\n",
        "\n",
        "        lines = resp.text.strip().split('\\n')\n",
        "        header_idx = next(i for i, line in enumerate(lines) if not line.startswith('#'))\n",
        "        csv_text = '\\n'.join(lines[header_idx:])\n",
        "        df = pd.read_csv(io.StringIO(csv_text), low_memory=False)\n",
        "\n",
        "        if df.empty or len(df) < 10:\n",
        "            print(f'  ❌ SIN DATOS {s[\"ciudad\"]} ({station_code}): {len(df)} registros')\n",
        "            datos_obs[eid] = pd.DataFrame()\n",
        "            time.sleep(DELAY)\n",
        "            continue\n",
        "\n",
        "        time_col = [c for c in df.columns if 'valid' in c.lower()]\n",
        "        if not time_col:\n",
        "            print(f'  ❌ {s[\"ciudad\"]}: sin columna de tiempo')\n",
        "            datos_obs[eid] = pd.DataFrame()\n",
        "            time.sleep(DELAY)\n",
        "            continue\n",
        "\n",
        "        df['time'] = pd.to_datetime(df[time_col[0]], utc=True)\n",
        "        df = df.set_index('time').sort_index()\n",
        "\n",
        "        for col in ['tmpf','dwpf','relh','sknt']:\n",
        "            if col in df.columns:\n",
        "                df[col] = pd.to_numeric(df[col], errors='coerce')\n",
        "\n",
        "        df['temp'] = (df['tmpf'] - 32) * 5/9\n",
        "        df['rhum'] = df['relh']\n",
        "        df['wspd'] = df['sknt'] * 0.5144\n",
        "\n",
        "        df_h = df[['temp','rhum','wspd']].resample('1h').mean()\n",
        "        datos_obs[eid] = df_h\n",
        "        cob = df_h['temp'].notna().mean() * 100\n",
        "        info_estaciones[eid] = {\n",
        "            'icao': s['icao'], 'iem': s['iem'], 'dist_km': s['dist_km'],\n",
        "            'registros': len(df_h), 'cobertura': f'{cob:.0f}%',\n",
        "            'temp_range': f\"{df_h['temp'].min():.1f} - {df_h['temp'].max():.1f} C\"\n",
        "        }\n",
        "        print(f'  ✅ {s[\"ciudad\"]} ({station_code}): {len(df_h)} h, cob {cob:.0f}%, T {df_h[\"temp\"].min():.1f}-{df_h[\"temp\"].max():.1f}°C')\n",
        "\n",
        "    except Exception as e:\n",
        "        print(f'  ❌ {s[\"ciudad\"]}: {e}')\n",
        "        datos_obs[eid] = pd.DataFrame()\n",
        "\n",
        "    time.sleep(DELAY)\n",
        "\n",
        "ok = sum(1 for v in datos_obs.values() if not v.empty)\n",
        "sin = [sedes[k]['ciudad'] for k, v in datos_obs.items() if v.empty]\n",
        "print(f'\\n{\"=\"*60}')\n",
        "print(f'Sedes con datos: {ok}/16')\n",
        "print(f'Sin datos: {sin}')\n",
        "if 'Guadalajara' in sin and len(sin) == 1:\n",
        "    print('✅ Solo falta Guadalajara (esperado: ASOS mexicanos no en IEM)')\n",
        "elif len(sin) > 1:\n",
        "    print('⚠ Faltan sedes además de Guadalajara. Re-ejecutar con más delay.')\n"
      ],
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Descargando ASOS:   0%|          | 0/16 [00:00<?, ?it/s]"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "5a8b8c2fe93f4864a1c6c3eecd1b154e"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "  ❌ Ciudad de Mexico (MMMX): HTTP 503\n",
            "  ❌ Guadalajara (MMGL): HTTP 503\n",
            "  ✅ Monterrey (MMMY): 1224 h, cob 100%, T 21.0-43.0°C\n",
            "  ✅ Atlanta (KATL): 1224 h, cob 100%, T 14.4-36.7°C\n",
            "  ✅ Dallas (KDFW): 1224 h, cob 100%, T 21.1-40.6°C\n",
            "  ✅ Houston (KHOU): 1224 h, cob 100%, T 22.8-36.7°C\n",
            "  ✅ Los Angeles (KLAX): 1224 h, cob 100%, T 14.4-30.0°C\n",
            "  ✅ Kansas City (KMCI): 1224 h, cob 100%, T 11.7-35.6°C\n",
            "  ✅ Miami (KMIA): 1224 h, cob 100%, T 22.8-35.6°C\n",
            "  ✅ Filadelfia (PHL): 1224 h, cob 100%, T 11.7-38.9°C\n",
            "  ✅ Boston (KBOS): 1223 h, cob 99%, T 9.4-37.8°C\n",
            "  ✅ Nueva York/NJ (KEWR): 1224 h, cob 100%, T 11.1-40.0°C\n",
            "  ✅ San Francisco (KSJC): 1224 h, cob 100%, T 10.6-36.1°C\n",
            "  ✅ Seattle (KSEA): 1224 h, cob 100%, T 8.3-33.3°C\n",
            "  ✅ Toronto (CYTZ): 1224 h, cob 100%, T 10.0-35.0°C\n",
            "  ✅ Vancouver (CYVR): 1224 h, cob 100%, T 9.0-27.0°C\n",
            "\n",
            "============================================================\n",
            "Sedes con datos: 14/16\n",
            "Sin datos: ['Ciudad de Mexico', 'Guadalajara']\n",
            "⚠ Faltan sedes además de Guadalajara. Re-ejecutar con más delay.\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "source": [
        "# FIX — Re-descargar MMMX (Ciudad de México)\n",
        "# Ejecutar si MMMX falló por rate limit en celda 3\n",
        "\n",
        "import time\n",
        "station = 'MMMX'\n",
        "eid_cdmx = 1\n",
        "s = sedes[eid_cdmx]\n",
        "\n",
        "print(f\"Esperando 30s antes de descargar {s['ciudad']}...\")\n",
        "time.sleep(30)\n",
        "\n",
        "url = f'{BASE_URL}?station={station}'\n",
        "for d in ['tmpf','dwpf','relh','sknt']:\n",
        "    url += f'&data={d}'\n",
        "url += '&year1=2026&month1=6&day1=1&year2=2026&month2=7&day2=22'\n",
        "url += '&tz=Etc/UTC&format=comma&latlon=yes&missing=M&direct=yes&report_type=3'\n",
        "\n",
        "resp = requests.get(url, timeout=30)\n",
        "if resp.status_code == 429:\n",
        "    print(\"Rate limited. Esperando 60s...\")\n",
        "    time.sleep(60)\n",
        "    resp = requests.get(url, timeout=30)\n",
        "\n",
        "if resp.status_code == 200:\n",
        "    lines = resp.text.strip().split('\\n')\n",
        "    header_idx = next(i for i, line in enumerate(lines) if not line.startswith('#'))\n",
        "    csv_text = '\\n'.join(lines[header_idx:])\n",
        "    df = pd.read_csv(io.StringIO(csv_text), low_memory=False)\n",
        "    time_col = [c for c in df.columns if 'valid' in c.lower()]\n",
        "    df['time'] = pd.to_datetime(df[time_col[0]], utc=True)\n",
        "    df = df.set_index('time').sort_index()\n",
        "    for col in ['tmpf','dwpf','relh','sknt']:\n",
        "        if col in df.columns:\n",
        "            df[col] = pd.to_numeric(df[col], errors='coerce')\n",
        "    df['temp'] = (df['tmpf'] - 32) * 5/9\n",
        "    df['rhum'] = df['relh']\n",
        "    df['wspd'] = df['sknt'] * 0.5144\n",
        "    df_h = df[['temp','rhum','wspd']].resample('1h').mean()\n",
        "    datos_obs[eid_cdmx] = df_h\n",
        "    cob = df_h['temp'].notna().mean() * 100\n",
        "    print(f\"✅ {s['ciudad']} ({station}): {len(df_h)} h, cob {cob:.0f}%, T {df_h['temp'].min():.1f}-{df_h['temp'].max():.1f}°C\")\n",
        "else:\n",
        "    print(f\"❌ HTTP {resp.status_code}\")\n",
        "\n",
        "# Verificar\n",
        "ok = sum(1 for v in datos_obs.values() if not v.empty)\n",
        "sin = [sedes[k]['ciudad'] for k, v in datos_obs.items() if v.empty]\n",
        "print(f\"\\nSedes con datos: {ok}/16, sin datos: {sin}\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "4IBWEpyVk06u",
        "outputId": "8f7f8f71-95e1-4444-ec3f-5aa54ffa8d32"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Esperando 30s antes de descargar Ciudad de Mexico...\n",
            "✅ Ciudad de Mexico (MMMX): 1224 h, cob 100%, T 12.0-28.0°C\n",
            "\n",
            "Sedes con datos: 15/16, sin datos: ['Guadalajara']\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "xRRd99trhEtu",
        "outputId": "6089adba-98d4-4772-d5b2-5daabb5208f3"
      },
      "source": [
        "# Celda 4 — Funciones WBGT Liljegren + auxiliares\n",
        "\n",
        "def calc_es(ta):\n",
        "    \"\"\"Presión de vapor de saturación (hPa)\"\"\"\n",
        "    return 6.112 * np.exp(17.67 * ta / (ta + 243.5))\n",
        "\n",
        "def calc_twb_natural(ta, rh, ws, rad):\n",
        "    \"\"\"Natural wet-bulb temperature (Liljegren iterative solver)\"\"\"\n",
        "    if pd.isna(ta) or pd.isna(rh): return np.nan\n",
        "    ws = max(ws, 0.5) if not pd.isna(ws) else 1.0\n",
        "    rad = rad if not pd.isna(rad) else 0.0\n",
        "    ea = calc_es(ta) * rh / 100.0\n",
        "    twb = ta - 2.0\n",
        "    for _ in range(50):\n",
        "        es_twb = calc_es(twb)\n",
        "        h_conv = 6.3 * ws**0.6\n",
        "        evap = h_conv * 0.000665 * (es_twb - ea)\n",
        "        rad_abs = 0.95 * rad * 0.0052\n",
        "        f = h_conv * (twb - ta) + 2501.0 * evap - rad_abs\n",
        "        des = calc_es(twb + 0.01)\n",
        "        df_val = h_conv + 2501.0 * h_conv * 0.000665 * (des - es_twb) / 0.01\n",
        "        if abs(df_val) < 1e-10: break\n",
        "        twb_new = twb - f / df_val\n",
        "        if abs(twb_new - twb) < 0.01: twb = twb_new; break\n",
        "        twb = twb_new\n",
        "    return twb\n",
        "\n",
        "def calc_tg(ta, rh, ws, rad):\n",
        "    \"\"\"Globe temperature (Liljegren iterative solver). Globe diam=150mm, emis=0.95\"\"\"\n",
        "    if pd.isna(ta) or pd.isna(rad): return ta if not pd.isna(ta) else np.nan\n",
        "    ws = max(ws, 0.5) if not pd.isna(ws) else 1.0\n",
        "    rad = max(rad, 0.0)\n",
        "    tg = ta + 2.0\n",
        "    emis_g = 0.95; diam = 0.15; stefan = 5.67e-8\n",
        "    for _ in range(50):\n",
        "        h = 6.3 * (ws / diam)**0.6\n",
        "        ta_k = ta + 273.15; tg_k = tg + 273.15\n",
        "        rad_in = 0.5 * (1.0 - 0.3) * rad + emis_g * stefan * ta_k**4\n",
        "        rad_out = emis_g * stefan * tg_k**4 + h * (tg - ta)\n",
        "        f = rad_in - rad_out\n",
        "        df_v = -4 * emis_g * stefan * tg_k**3 - h\n",
        "        if abs(df_v) < 1e-10: break\n",
        "        tg_new = tg - f / df_v\n",
        "        if abs(tg_new - tg) < 0.01: tg = tg_new; break\n",
        "        tg = tg_new\n",
        "    return tg\n",
        "\n",
        "def calc_wbgt_liljegren(ta, rh, ws, rad):\n",
        "    \"\"\"Outdoor WBGT = 0.7*Tnwb + 0.2*Tg + 0.1*Ta (Liljegren et al., 2008)\"\"\"\n",
        "    tnwb = calc_twb_natural(ta, rh, ws, rad)\n",
        "    tg = calc_tg(ta, rh, ws, rad)\n",
        "    if pd.isna(tnwb) or pd.isna(tg): return np.nan\n",
        "    return 0.7 * tnwb + 0.2 * tg + 0.1 * ta\n",
        "\n",
        "def calc_swbgt(ta, rh):\n",
        "    \"\"\"sWBGT simplificado (Stull, 2011)\"\"\"\n",
        "    if pd.isna(ta) or pd.isna(rh): return np.nan\n",
        "    tw = ta * np.arctan(0.151977*(rh+8.313659)**0.5) + \\\n",
        "         np.arctan(ta+rh) - np.arctan(rh-1.676331) + \\\n",
        "         0.00391838*rh**1.5*np.arctan(0.023101*rh) - 4.686035\n",
        "    return 0.7 * tw + 0.3 * ta\n",
        "\n",
        "def cat_riesgo(wbgt):\n",
        "    if pd.isna(wbgt): return None\n",
        "    if wbgt >= 32: return 'Extreme'\n",
        "    if wbgt >= 30: return 'High'\n",
        "    if wbgt >= 28: return 'Moderate'\n",
        "    return 'Low'\n",
        "\n",
        "# Test\n",
        "print(f'Test Liljegren (33°C, 70%, 2.5 m/s, 500 W/m²): {calc_wbgt_liljegren(33,70,2.5,500):.1f}°C')\n",
        "print(f'Test sWBGT    (33°C, 70%):                      {calc_swbgt(33,70):.1f}°C')\n",
        "print('✅ Funciones OK')\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Test Liljegren (33°C, 70%, 2.5 m/s, 500 W/m²): 30.5°C\n",
            "Test sWBGT    (33°C, 70%):                      29.8°C\n",
            "✅ Funciones OK\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 105
        },
        "id": "sf1-UC6ShEt3",
        "outputId": "d134f58f-a56a-4f29-fbce-cdf8e3e2f436"
      },
      "source": [
        "# Celda 5 — Cargar calendario + datos ERA5 previos\n",
        "from google.colab import files\n",
        "print('Sube tu Excel (Mundial2026_Datos_Termicos_v6_FINAL.xlsx):')\n",
        "uploaded = files.upload()\n",
        "fname = list(uploaded.keys())[0]\n",
        "df_era5 = pd.read_excel(fname)\n",
        "df_era5['estadio_id'] = df_era5['A6_Estadio'].map(est_map)\n",
        "print(f'✅ Cargados {len(df_era5)} partidos, IDs mapeados: {df_era5[\"estadio_id\"].notna().sum()}/104')\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Sube tu Excel (Mundial2026_Datos_Termicos_v6_FINAL.xlsx):\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.HTML object>"
            ],
            "text/html": [
              "\n",
              "     <input type=\"file\" id=\"files-4aea6580-9490-4da4-a349-2bd391d59499\" name=\"files[]\" multiple disabled\n",
              "        style=\"border:none\" />\n",
              "     <output id=\"result-4aea6580-9490-4da4-a349-2bd391d59499\">\n",
              "      Upload widget is only available when the cell has been executed in the\n",
              "      current browser session. Please rerun this cell to enable.\n",
              "      </output>\n",
              "      <script>// Copyright 2017 Google LLC\n",
              "//\n",
              "// Licensed under the Apache License, Version 2.0 (the \"License\");\n",
              "// you may not use this file except in compliance with the License.\n",
              "// You may obtain a copy of the License at\n",
              "//\n",
              "//      http://www.apache.org/licenses/LICENSE-2.0\n",
              "//\n",
              "// Unless required by applicable law or agreed to in writing, software\n",
              "// distributed under the License is distributed on an \"AS IS\" BASIS,\n",
              "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
              "// See the License for the specific language governing permissions and\n",
              "// limitations under the License.\n",
              "\n",
              "/**\n",
              " * @fileoverview Helpers for google.colab Python module.\n",
              " */\n",
              "(function(scope) {\n",
              "function span(text, styleAttributes = {}) {\n",
              "  const element = document.createElement('span');\n",
              "  element.textContent = text;\n",
              "  for (const key of Object.keys(styleAttributes)) {\n",
              "    element.style[key] = styleAttributes[key];\n",
              "  }\n",
              "  return element;\n",
              "}\n",
              "\n",
              "// Max number of bytes which will be uploaded at a time.\n",
              "const MAX_PAYLOAD_SIZE = 100 * 1024;\n",
              "\n",
              "function _uploadFiles(inputId, outputId) {\n",
              "  const steps = uploadFilesStep(inputId, outputId);\n",
              "  const outputElement = document.getElementById(outputId);\n",
              "  // Cache steps on the outputElement to make it available for the next call\n",
              "  // to uploadFilesContinue from Python.\n",
              "  outputElement.steps = steps;\n",
              "\n",
              "  return _uploadFilesContinue(outputId);\n",
              "}\n",
              "\n",
              "// This is roughly an async generator (not supported in the browser yet),\n",
              "// where there are multiple asynchronous steps and the Python side is going\n",
              "// to poll for completion of each step.\n",
              "// This uses a Promise to block the python side on completion of each step,\n",
              "// then passes the result of the previous step as the input to the next step.\n",
              "function _uploadFilesContinue(outputId) {\n",
              "  const outputElement = document.getElementById(outputId);\n",
              "  const steps = outputElement.steps;\n",
              "\n",
              "  const next = steps.next(outputElement.lastPromiseValue);\n",
              "  return Promise.resolve(next.value.promise).then((value) => {\n",
              "    // Cache the last promise value to make it available to the next\n",
              "    // step of the generator.\n",
              "    outputElement.lastPromiseValue = value;\n",
              "    return next.value.response;\n",
              "  });\n",
              "}\n",
              "\n",
              "/**\n",
              " * Generator function which is called between each async step of the upload\n",
              " * process.\n",
              " * @param {string} inputId Element ID of the input file picker element.\n",
              " * @param {string} outputId Element ID of the output display.\n",
              " * @return {!Iterable<!Object>} Iterable of next steps.\n",
              " */\n",
              "function* uploadFilesStep(inputId, outputId) {\n",
              "  const inputElement = document.getElementById(inputId);\n",
              "  inputElement.disabled = false;\n",
              "\n",
              "  const outputElement = document.getElementById(outputId);\n",
              "  outputElement.innerHTML = '';\n",
              "\n",
              "  const pickedPromise = new Promise((resolve) => {\n",
              "    inputElement.addEventListener('change', (e) => {\n",
              "      resolve(e.target.files);\n",
              "    });\n",
              "  });\n",
              "\n",
              "  const cancel = document.createElement('button');\n",
              "  inputElement.parentElement.appendChild(cancel);\n",
              "  cancel.textContent = 'Cancel upload';\n",
              "  const cancelPromise = new Promise((resolve) => {\n",
              "    cancel.onclick = () => {\n",
              "      resolve(null);\n",
              "    };\n",
              "  });\n",
              "\n",
              "  // Wait for the user to pick the files.\n",
              "  const files = yield {\n",
              "    promise: Promise.race([pickedPromise, cancelPromise]),\n",
              "    response: {\n",
              "      action: 'starting',\n",
              "    }\n",
              "  };\n",
              "\n",
              "  cancel.remove();\n",
              "\n",
              "  // Disable the input element since further picks are not allowed.\n",
              "  inputElement.disabled = true;\n",
              "\n",
              "  if (!files) {\n",
              "    return {\n",
              "      response: {\n",
              "        action: 'complete',\n",
              "      }\n",
              "    };\n",
              "  }\n",
              "\n",
              "  for (const file of files) {\n",
              "    const li = document.createElement('li');\n",
              "    li.append(span(file.name, {fontWeight: 'bold'}));\n",
              "    li.append(span(\n",
              "        `(${file.type || 'n/a'}) - ${file.size} bytes, ` +\n",
              "        `last modified: ${\n",
              "            file.lastModifiedDate ? file.lastModifiedDate.toLocaleDateString() :\n",
              "                                    'n/a'} - `));\n",
              "    const percent = span('0% done');\n",
              "    li.appendChild(percent);\n",
              "\n",
              "    outputElement.appendChild(li);\n",
              "\n",
              "    const fileDataPromise = new Promise((resolve) => {\n",
              "      const reader = new FileReader();\n",
              "      reader.onload = (e) => {\n",
              "        resolve(e.target.result);\n",
              "      };\n",
              "      reader.readAsArrayBuffer(file);\n",
              "    });\n",
              "    // Wait for the data to be ready.\n",
              "    let fileData = yield {\n",
              "      promise: fileDataPromise,\n",
              "      response: {\n",
              "        action: 'continue',\n",
              "      }\n",
              "    };\n",
              "\n",
              "    // Use a chunked sending to avoid message size limits. See b/62115660.\n",
              "    let position = 0;\n",
              "    do {\n",
              "      const length = Math.min(fileData.byteLength - position, MAX_PAYLOAD_SIZE);\n",
              "      const chunk = new Uint8Array(fileData, position, length);\n",
              "      position += length;\n",
              "\n",
              "      const base64 = btoa(String.fromCharCode.apply(null, chunk));\n",
              "      yield {\n",
              "        response: {\n",
              "          action: 'append',\n",
              "          file: file.name,\n",
              "          data: base64,\n",
              "        },\n",
              "      };\n",
              "\n",
              "      let percentDone = fileData.byteLength === 0 ?\n",
              "          100 :\n",
              "          Math.round((position / fileData.byteLength) * 100);\n",
              "      percent.textContent = `${percentDone}% done`;\n",
              "\n",
              "    } while (position < fileData.byteLength);\n",
              "  }\n",
              "\n",
              "  // All done.\n",
              "  yield {\n",
              "    response: {\n",
              "      action: 'complete',\n",
              "    }\n",
              "  };\n",
              "}\n",
              "\n",
              "scope.google = scope.google || {};\n",
              "scope.google.colab = scope.google.colab || {};\n",
              "scope.google.colab._files = {\n",
              "  _uploadFiles,\n",
              "  _uploadFilesContinue,\n",
              "};\n",
              "})(self);\n",
              "</script> "
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving Mundial2026_Datos_Termicos_v6_FINAL (1).xlsx to Mundial2026_Datos_Termicos_v6_FINAL (1) (1).xlsx\n",
            "✅ Cargados 104 partidos, IDs mapeados: 104/104\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 552,
          "referenced_widgets": [
            "d32fd123693540e482c737441ba1df14",
            "e0c110fee938460c96fa70c838ec41d8",
            "b4351a5aab914373ae3d328f081e7494",
            "5f5838184c494963bbec7e176628e1d6",
            "20996146f0794c019dd1b40a5e080393",
            "90a1118af8a041218ed7196e949f9f4b",
            "9daff681a7ca4be4892a4f1d9201a264",
            "a726158af3e54d458b3aed03f1df5cbe",
            "df4ab8be122b454f8d4147a6dc5fd9e4",
            "406a24fc92be42a49743e4c6c79cf8ed",
            "661c9f05f3ae472ba0cc1096b41cce2f"
          ]
        },
        "id": "2lqAWBZ5hEt6",
        "outputId": "30d9bca2-d683-4205-a909-0470182a7879"
      },
      "source": [
        "# Celda 6 — Pipeline: WBGT NOAA + sWBGT para los 104 partidos\n",
        "\n",
        "era5_col = 'B1_WBGT_max' if 'B1_WBGT_max' in df_era5.columns else 'B1_WBGT_OBS'\n",
        "era5_cat = 'B11_cat_riesgo' if 'B11_cat_riesgo' in df_era5.columns else 'B11_cat_riesgo_OBS'\n",
        "\n",
        "resultados = []\n",
        "\n",
        "for idx, p in tqdm(df_era5.iterrows(), total=len(df_era5), desc='Procesando'):\n",
        "    eid = p.get('estadio_id')\n",
        "    if pd.isna(eid):\n",
        "        continue\n",
        "    eid = int(eid)\n",
        "    s = sedes[eid]\n",
        "\n",
        "    df_obs = datos_obs.get(eid, pd.DataFrame())\n",
        "    if df_obs.empty:\n",
        "        resultados.append({\n",
        "            'A1_ID': int(p['A1_ID']), 'A2_Fecha': str(p['A2_Fecha'])[:10],\n",
        "            'A3_Hora_local': str(p['A3_Hora_local'])[:5],\n",
        "            'A5_Sede': s['ciudad'], 'A7_Equipos': p.get('A7_Equipos',''),\n",
        "            'B1_WBGT_NOAA': None, 'B1_sWBGT_NOAA': None,\n",
        "            'B1_WBGT_ERA5': p.get(era5_col),\n",
        "            'B1_WBGT_diff': None,\n",
        "            'B3_T_max_NOAA': None, 'B6_HR_media_NOAA': None,\n",
        "            'B7_grados_hora_28': None, 'B7_grados_hora_26': None, 'B7_grados_hora_30': None,\n",
        "            'B11_cat_NOAA': None, 'B11_cat_ERA5': p.get(era5_cat,''),\n",
        "            'Estacion': s['icao'], 'Dist_km': s['dist_km'],\n",
        "            'Horas': 0, 'C5_Pct_sombra': p.get('C5_Pct_sombra'),\n",
        "            'C7_Ocupacion': p.get('C7_Ocupacion'), 'Fuente': 'ERA5',\n",
        "        })\n",
        "        continue\n",
        "\n",
        "    # Ventana de exposición: kickoff -2h a +4h\n",
        "    try:\n",
        "        fecha_str = str(p['A2_Fecha'])[:10]\n",
        "        hora_str = str(p['A3_Hora_local'])[:5]\n",
        "        if ':' not in hora_str: hora_str += ':00'\n",
        "        hora_local = datetime.strptime(f'{fecha_str} {hora_str}', '%Y-%m-%d %H:%M')\n",
        "    except:\n",
        "        continue\n",
        "\n",
        "    inicio = (hora_local - timedelta(hours=2) - timedelta(hours=s['utc_offset'])).replace(tzinfo=timezone.utc)\n",
        "    fin = (hora_local + timedelta(hours=4) - timedelta(hours=s['utc_offset'])).replace(tzinfo=timezone.utc)\n",
        "    ventana = df_obs.loc[(df_obs.index >= inicio) & (df_obs.index <= fin)].copy()\n",
        "\n",
        "    if ventana.empty or ventana['temp'].isna().all():\n",
        "        continue\n",
        "\n",
        "    wbgt_vals = []; swbgt_vals = []; ta_vals = []; rh_vals = []\n",
        "\n",
        "    for ts, row in ventana.iterrows():\n",
        "        ta = row.get('temp', np.nan)\n",
        "        rh = row.get('rhum', np.nan)\n",
        "        ws = row.get('wspd', np.nan)\n",
        "        if pd.isna(ta) or pd.isna(rh): continue\n",
        "        if pd.isna(ws): ws = 1.0\n",
        "\n",
        "        # Radiación pysolar (clear-sky)\n",
        "        try:\n",
        "            elev = solar.get_altitude(s['lat'], s['lon'], ts.to_pydatetime())\n",
        "            rad = max(0, 1361 * np.sin(np.radians(elev)) * 0.70) if elev > 0 else 0.0\n",
        "        except:\n",
        "            rad = 0.0\n",
        "\n",
        "        # Wind speed 10m → 2m (log profile, z0=0.01m)\n",
        "        ws_2m = ws * (np.log(2/0.01) / np.log(10/0.01))  # factor ≈ 0.75\n",
        "\n",
        "        w = calc_wbgt_liljegren(ta, rh, ws_2m, rad)\n",
        "        sw = calc_swbgt(ta, rh)\n",
        "        if not pd.isna(w):\n",
        "            wbgt_vals.append(w)\n",
        "            ta_vals.append(ta)\n",
        "            rh_vals.append(rh)\n",
        "        if not pd.isna(sw):\n",
        "            swbgt_vals.append(sw)\n",
        "\n",
        "    if not wbgt_vals:\n",
        "        continue\n",
        "\n",
        "    wbgt_arr = np.array(wbgt_vals)\n",
        "    wbgt_max = float(np.max(wbgt_arr))\n",
        "    swbgt_max = float(np.max(swbgt_vals)) if swbgt_vals else np.nan\n",
        "    era5_val = p.get(era5_col, np.nan)\n",
        "\n",
        "    resultados.append({\n",
        "        'A1_ID': int(p['A1_ID']),\n",
        "        'A2_Fecha': fecha_str,\n",
        "        'A3_Hora_local': hora_str,\n",
        "        'A5_Sede': s['ciudad'],\n",
        "        'A7_Equipos': p.get('A7_Equipos', ''),\n",
        "        'B1_WBGT_NOAA': round(wbgt_max, 2),\n",
        "        'B1_sWBGT_NOAA': round(swbgt_max, 2) if not pd.isna(swbgt_max) else None,\n",
        "        'B1_WBGT_ERA5': round(era5_val, 2) if pd.notna(era5_val) else None,\n",
        "        'B1_WBGT_diff': round(wbgt_max - era5_val, 2) if pd.notna(era5_val) else None,\n",
        "        'B3_T_max_NOAA': round(max(ta_vals), 1),\n",
        "        'B6_HR_media_NOAA': round(np.mean(rh_vals), 1),\n",
        "        'B7_grados_hora_28': round(sum(max(w - 28, 0) for w in wbgt_arr), 2),\n",
        "        'B7_grados_hora_26': round(sum(max(w - 26, 0) for w in wbgt_arr), 2),\n",
        "        'B7_grados_hora_30': round(sum(max(w - 30, 0) for w in wbgt_arr), 2),\n",
        "        'B11_cat_NOAA': cat_riesgo(wbgt_max),\n",
        "        'B11_cat_ERA5': p.get(era5_cat, ''),\n",
        "        'Estacion': s['icao'],\n",
        "        'Dist_km': s['dist_km'],\n",
        "        'Horas': len(wbgt_arr),\n",
        "        'C5_Pct_sombra': p.get('C5_Pct_sombra'),\n",
        "        'C7_Ocupacion': p.get('C7_Ocupacion'),\n",
        "        'Fuente': 'NOAA',\n",
        "    })\n",
        "\n",
        "df_noaa = pd.DataFrame(resultados)\n",
        "dn = df_noaa[df_noaa['B1_WBGT_NOAA'].notna()].copy()\n",
        "\n",
        "print(f'\\n{\"=\"*60}')\n",
        "print(f'NOAA: {len(dn)} partidos en {dn[\"A5_Sede\"].nunique()} sedes')\n",
        "print(f'ERA5-only: {len(df_noaa) - len(dn)} partidos')\n",
        "print(f'\\nCategorías NOAA:')\n",
        "print(dn['B11_cat_NOAA'].value_counts().to_string())\n",
        "print(f'\\nRESUMEN POR SEDE:')\n",
        "sede = dn.groupby('A5_Sede').agg(\n",
        "    WBGT_mean=('B1_WBGT_NOAA','mean'), WBGT_max=('B1_WBGT_NOAA','max'),\n",
        "    n=('A1_ID','count'),\n",
        "    n_mod=('B11_cat_NOAA', lambda x: (x=='Moderate').sum()),\n",
        "    n_high=('B11_cat_NOAA', lambda x: (x=='High').sum()),\n",
        "    bias=('B1_WBGT_diff','mean'),\n",
        ").sort_values('WBGT_mean', ascending=False)\n",
        "print(sede.round(2).to_string())\n"
      ],
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Procesando:   0%|          | 0/104 [00:00<?, ?it/s]"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "d32fd123693540e482c737441ba1df14"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "============================================================\n",
            "NOAA: 100 partidos en 15 sedes\n",
            "ERA5-only: 4 partidos\n",
            "\n",
            "Categorías NOAA:\n",
            "B11_cat_NOAA\n",
            "Low         83\n",
            "Moderate    13\n",
            "High         4\n",
            "\n",
            "RESUMEN POR SEDE:\n",
            "                  WBGT_mean  WBGT_max  n  n_mod  n_high  bias\n",
            "A5_Sede                                                      \n",
            "Houston               28.78     31.67  7      3       2  0.95\n",
            "Dallas                27.98     30.23  9      4       1  0.66\n",
            "Miami                 27.50     28.94  7      3       0  0.49\n",
            "Atlanta               27.00     30.68  8      2       1  2.04\n",
            "Monterrey             24.70     24.92  4      0       0  0.13\n",
            "Filadelfia            24.28     29.05  6      1       0  0.66\n",
            "Kansas City           23.12     24.95  6      0       0 -0.82\n",
            "Nueva York/NJ         23.12     25.98  8      0       0  0.88\n",
            "Boston                21.35     26.71  7      0       0  0.43\n",
            "Toronto               20.42     27.05  6      0       0  0.49\n",
            "Seattle               19.28     24.22  6      0       0  0.60\n",
            "Los Angeles           18.63     20.64  8      0       0 -0.60\n",
            "Vancouver             18.28     21.75  7      0       0  1.43\n",
            "Ciudad de Mexico      16.48     21.39  5      0       0  0.17\n",
            "San Francisco         16.32     23.56  6      0       0 -0.36\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 567,
          "referenced_widgets": [
            "2303adb59f8343e4bf7ba7e0bfec18b0",
            "b8fb929fd45e4eed85a60b7b7aed8170",
            "89fff66d849f43b9b37c3e9f2388a8b4",
            "391048ad027a4e2ab1e9f7abc916b59e",
            "503bbe389cd0481cbcadc4ccfdb06418",
            "2f85c358cb7d477b9dd28112c710fa6a",
            "9033eec2233f4a8baa99fe4917626266",
            "dc0f41f027df4fdd9d57bb30a8b817a8",
            "d00ba11cedc34305aa42f7fc55f94ea1",
            "a9ff0704d67a456d9960a9f89b1613f4",
            "553cecb9b6e943b3a31d2b10174019cc",
            "4a68404920cb48c099155bf3c233fafb",
            "c1488996287d429495db8fda2fc92946",
            "e827aebbb971422b9e24c324e7052805",
            "ef89e7faaa22420a974ff1965b2bfc5c",
            "156a1f501dc8464ba44d1da8416ec5d9",
            "7588c70956fe48c2b6d4ec7f18837097",
            "7c7ab54c68e642deac402aff098e32c7",
            "59ca34068ce845d29d8d0de02871e81e",
            "6dd9a828863844baba24a44db749fbbd",
            "6d1f0cba076c4356996c16bf7ffcfe5a",
            "8c0d2011e7754ff4a45c4ca3e2cc3d44"
          ]
        },
        "id": "h8LOMI1QhEt-",
        "outputId": "6486d074-9720-47c0-9dcd-3cfed6f940ff"
      },
      "source": [
        "# Celda 7 — Descargar radiación ERA5 y análisis de sensibilidad\n",
        "!pip install openmeteo-requests requests-cache retry-requests -q\n",
        "import openmeteo_requests, requests_cache\n",
        "from retry_requests import retry\n",
        "\n",
        "cache_session = requests_cache.CachedSession('.cache_rad', expire_after=-1)\n",
        "retry_session = retry(cache_session, retries=5, backoff_factor=0.2)\n",
        "om = openmeteo_requests.Client(session=retry_session)\n",
        "\n",
        "print(\"Descargando radiación ERA5 para las 16 sedes...\")\n",
        "rad_era5 = {}\n",
        "for eid, s in tqdm(sedes.items(), desc='Radiación ERA5'):\n",
        "    try:\n",
        "        params = {\"latitude\": s[\"lat\"], \"longitude\": s[\"lon\"],\n",
        "                  \"start_date\": \"2026-06-01\", \"end_date\": \"2026-07-22\",\n",
        "                  \"hourly\": [\"shortwave_radiation\"]}\n",
        "        responses = om.weather_api(\"https://archive-api.open-meteo.com/v1/archive\", params=params)\n",
        "        r = responses[0]; hourly = r.Hourly()\n",
        "        t0 = pd.to_datetime(hourly.Time(), unit=\"s\", utc=True)\n",
        "        t_end = pd.to_datetime(hourly.TimeEnd(), unit=\"s\", utc=True)\n",
        "        times = pd.date_range(start=t0, end=t_end, freq=pd.Timedelta(seconds=hourly.Interval()), inclusive=\"left\")\n",
        "        df_rad = pd.DataFrame({\"rad_era5\": hourly.Variables(0).ValuesAsNumpy()}, index=times)\n",
        "        rad_era5[eid] = df_rad\n",
        "        print(f\"  ✅ {s['ciudad']}: {len(df_rad)} h, rad media {df_rad['rad_era5'].mean():.0f} W/m²\")\n",
        "    except Exception as e:\n",
        "        print(f\"  ❌ {s['ciudad']}: {e}\")\n",
        "        rad_era5[eid] = pd.DataFrame()\n",
        "    time.sleep(0.3)\n",
        "\n",
        "# Recalcular WBGT con radiación ERA5\n",
        "resultados_corr = []\n",
        "for idx, p in tqdm(df_era5.iterrows(), total=len(df_era5), desc='Recalculando con rad ERA5'):\n",
        "    eid = p.get('estadio_id')\n",
        "    if pd.isna(eid): continue\n",
        "    eid = int(eid); s = sedes[eid]\n",
        "    df_obs = datos_obs.get(eid, pd.DataFrame())\n",
        "    df_rad = rad_era5.get(eid, pd.DataFrame())\n",
        "    if df_obs.empty or df_rad.empty: continue\n",
        "    try:\n",
        "        fecha_str = str(p['A2_Fecha'])[:10]; hora_str = str(p['A3_Hora_local'])[:5]\n",
        "        if ':' not in hora_str: hora_str += ':00'\n",
        "        hora_local = datetime.strptime(f'{fecha_str} {hora_str}', '%Y-%m-%d %H:%M')\n",
        "    except: continue\n",
        "    inicio = (hora_local - timedelta(hours=2) - timedelta(hours=s['utc_offset'])).replace(tzinfo=timezone.utc)\n",
        "    fin = (hora_local + timedelta(hours=4) - timedelta(hours=s['utc_offset'])).replace(tzinfo=timezone.utc)\n",
        "    ventana_obs = df_obs.loc[(df_obs.index >= inicio) & (df_obs.index <= fin)]\n",
        "    ventana_rad = df_rad.loc[(df_rad.index >= inicio) & (df_rad.index <= fin)]\n",
        "    if ventana_obs.empty or ventana_obs['temp'].isna().all(): continue\n",
        "    wbgt_cs = []; wbgt_e5r = []\n",
        "    for ts, row in ventana_obs.iterrows():\n",
        "        ta = row.get('temp', np.nan); rh = row.get('rhum', np.nan); ws = row.get('wspd', np.nan)\n",
        "        if pd.isna(ta) or pd.isna(rh): continue\n",
        "        if pd.isna(ws): ws = 1.0\n",
        "        ws_2m = ws * (np.log(2/0.01) / np.log(10/0.01))\n",
        "        try:\n",
        "            elev = solar.get_altitude(s['lat'], s['lon'], ts.to_pydatetime())\n",
        "            rad_cs = max(0, 1361 * np.sin(np.radians(elev)) * 0.70) if elev > 0 else 0.0\n",
        "        except: rad_cs = 0.0\n",
        "        rad_e5 = 0.0\n",
        "        if not ventana_rad.empty:\n",
        "            idx_closest = ventana_rad.index.get_indexer([ts], method='nearest')[0]\n",
        "            if idx_closest >= 0 and idx_closest < len(ventana_rad):\n",
        "                rad_e5 = max(0, ventana_rad.iloc[idx_closest]['rad_era5'])\n",
        "        wbgt_cs.append(calc_wbgt_liljegren(ta, rh, ws_2m, rad_cs))\n",
        "        wbgt_e5r.append(calc_wbgt_liljegren(ta, rh, ws_2m, rad_e5))\n",
        "    cs_arr = [x for x in wbgt_cs if not pd.isna(x)]\n",
        "    e5_arr = [x for x in wbgt_e5r if not pd.isna(x)]\n",
        "    if not cs_arr or not e5_arr: continue\n",
        "    era5_val = p.get(era5_col, np.nan)\n",
        "    resultados_corr.append({\n",
        "        'A1_ID': int(p['A1_ID']), 'A5_Sede': s['ciudad'],\n",
        "        'WBGT_clearsky': round(max(cs_arr), 2), 'WBGT_ERA5rad': round(max(e5_arr), 2),\n",
        "        'WBGT_ERA5': round(era5_val, 2) if pd.notna(era5_val) else None,\n",
        "        'cat_cs': cat_riesgo(max(cs_arr)), 'cat_e5r': cat_riesgo(max(e5_arr)),\n",
        "        'cat_era5': cat_riesgo(era5_val) if pd.notna(era5_val) else None,\n",
        "    })\n",
        "df_corr = pd.DataFrame(resultados_corr)\n",
        "\n",
        "# Results\n",
        "bias_cs = (df_corr['WBGT_clearsky'] - df_corr['WBGT_ERA5']).mean()\n",
        "bias_e5r = (df_corr['WBGT_ERA5rad'] - df_corr['WBGT_ERA5']).mean()\n",
        "efecto_rad = (df_corr['WBGT_clearsky'] - df_corr['WBGT_ERA5rad']).mean()\n",
        "print(f'\\n{\"=\"*60}')\n",
        "print(f'SENSIBILIDAD A LA RADIACIÓN (n={len(df_corr)})')\n",
        "print(f'{\"=\"*60}')\n",
        "print(f'  Bias total (clearsky vs ERA5):     {bias_cs:+.2f}°C')\n",
        "print(f'  Componente radiación:              {efecto_rad:+.2f}°C ({abs(efecto_rad/bias_cs)*100:.0f}%)')\n",
        "print(f'  Componente genuino (datos):        {bias_e5r:+.2f}°C ({abs(bias_e5r/bias_cs)*100:.0f}%)')\n",
        "print(f'\\n  Categorías clearsky: {df_corr[\"cat_cs\"].value_counts().to_dict()}')\n",
        "print(f'  Categorías ERA5rad:  {df_corr[\"cat_e5r\"].value_counts().to_dict()}')\n",
        "print(f'  Categorías ERA5:     {df_corr[\"cat_era5\"].value_counts().to_dict()}')\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Descargando radiación ERA5 para las 16 sedes...\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Radiación ERA5:   0%|          | 0/16 [00:00<?, ?it/s]"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "2303adb59f8343e4bf7ba7e0bfec18b0"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "  ✅ Ciudad de Mexico: 1248 h, rad media 271 W/m²\n",
            "  ✅ Guadalajara: 1248 h, rad media 263 W/m²\n",
            "  ✅ Monterrey: 1248 h, rad media 278 W/m²\n",
            "  ✅ Atlanta: 1248 h, rad media 274 W/m²\n",
            "  ✅ Dallas: 1248 h, rad media 287 W/m²\n",
            "  ✅ Houston: 1248 h, rad media 255 W/m²\n",
            "  ✅ Los Angeles: 1248 h, rad media 315 W/m²\n",
            "  ✅ Kansas City: 1248 h, rad media 283 W/m²\n",
            "  ✅ Miami: 1248 h, rad media 281 W/m²\n",
            "  ✅ Filadelfia: 1248 h, rad media 277 W/m²\n",
            "  ✅ Boston: 1248 h, rad media 282 W/m²\n",
            "  ✅ Nueva York/NJ: 1248 h, rad media 278 W/m²\n",
            "  ✅ San Francisco: 1248 h, rad media 341 W/m²\n",
            "  ✅ Seattle: 1248 h, rad media 274 W/m²\n",
            "  ✅ Toronto: 1248 h, rad media 287 W/m²\n",
            "  ✅ Vancouver: 1248 h, rad media 280 W/m²\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Recalculando con rad ERA5:   0%|          | 0/104 [00:00<?, ?it/s]"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "4a68404920cb48c099155bf3c233fafb"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "============================================================\n",
            "SENSIBILIDAD A LA RADIACIÓN (n=100)\n",
            "============================================================\n",
            "  Bias total (clearsky vs ERA5):     +0.52°C\n",
            "  Componente radiación:              +0.10°C (18%)\n",
            "  Componente genuino (datos):        +0.43°C (82%)\n",
            "\n",
            "  Categorías clearsky: {'Low': 83, 'Moderate': 13, 'High': 4}\n",
            "  Categorías ERA5rad:  {'Low': 85, 'Moderate': 10, 'High': 5}\n",
            "  Categorías ERA5:     {'Low': 95, 'Moderate': 5}\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "FdLVbmANhEuB",
        "outputId": "30d6db07-5407-4399-93cc-87bdf0a5f5a4"
      },
      "source": [
        "# Celda 8 — Validación NOAA vs ERA5 + Bland-Altman + gráficos\n",
        "dn = df_noaa[df_noaa['B1_WBGT_NOAA'].notna() & df_noaa['B1_WBGT_ERA5'].notna()].copy()\n",
        "\n",
        "wn = dn['B1_WBGT_NOAA'].values; we = dn['B1_WBGT_ERA5'].values\n",
        "r, pval = sp_stats.pearsonr(we, wn)\n",
        "rmse = np.sqrt(((wn - we)**2).mean())\n",
        "bias = (wn - we).mean()\n",
        "\n",
        "print(f'NOAA vs ERA5 (n={len(dn)})')\n",
        "print(f'  R² = {r**2:.3f}, RMSE = {rmse:.2f}°C, bias = {bias:+.2f}°C')\n",
        "\n",
        "# Bland-Altman\n",
        "diff = wn - we; m = (wn + we) / 2\n",
        "md = np.mean(diff); sd = np.std(diff, ddof=1)\n",
        "uloa = md + 1.96 * sd; lloa = md - 1.96 * sd\n",
        "se_mean = sd / np.sqrt(len(diff))\n",
        "r_pb, p_pb = sp_stats.pearsonr(m, diff)\n",
        "outside = ((diff > uloa) | (diff < lloa)).sum()\n",
        "\n",
        "print(f'\\nBLAND-ALTMAN:')\n",
        "print(f'  Mean bias: {md:+.3f}°C')\n",
        "print(f'  SD: {sd:.3f}°C')\n",
        "print(f'  95% LoA: [{lloa:.2f}, {uloa:.2f}]°C')\n",
        "print(f'  95% CI of bias: [{md-1.96*se_mean:.2f}, {md+1.96*se_mean:.2f}]')\n",
        "print(f'  Proportional bias: r={r_pb:.3f}, p={p_pb:.4f}')\n",
        "print(f'  Outside LoA: {outside}/{len(diff)} ({100*outside/len(diff):.1f}%)')\n",
        "\n",
        "# Gráficos\n",
        "fig, axes = plt.subplots(1, 4, figsize=(24, 5))\n",
        "\n",
        "# 1. Scatter\n",
        "ax = axes[0]\n",
        "colors_map = {'Low':'steelblue','Moderate':'orange','High':'red'}\n",
        "for cat in ['Low','Moderate','High']:\n",
        "    mask = dn['B11_cat_NOAA']==cat\n",
        "    if mask.sum()>0:\n",
        "        ax.scatter(dn.loc[mask,'B1_WBGT_ERA5'], dn.loc[mask,'B1_WBGT_NOAA'],\n",
        "                   c=colors_map[cat], alpha=0.7, edgecolors='k', lw=0.5, s=50, label=f'{cat} ({mask.sum()})')\n",
        "ax.plot([12,33],[12,33],'k--',lw=1,alpha=0.5)\n",
        "ax.axhline(28,color='orange',ls=':',lw=1); ax.axhline(30,color='red',ls=':',lw=1)\n",
        "ax.set_xlabel('WBGT ERA5 (°C)'); ax.set_ylabel('WBGT NOAA (°C)')\n",
        "ax.set_title(f'NOAA vs ERA5 — R²={r**2:.3f}, RMSE={rmse:.1f}°C'); ax.legend(fontsize=8)\n",
        "ax.set_aspect('equal'); ax.set_xlim(12,33); ax.set_ylim(12,33)\n",
        "\n",
        "# 2. Bland-Altman\n",
        "ax = axes[1]\n",
        "for cat in ['Low','Moderate','High']:\n",
        "    mask = dn['B11_cat_NOAA']==cat\n",
        "    if mask.sum()>0:\n",
        "        idx_mask = dn.index[mask]\n",
        "        m_sub = (dn.loc[idx_mask,'B1_WBGT_NOAA'].values + dn.loc[idx_mask,'B1_WBGT_ERA5'].values)/2\n",
        "        d_sub = dn.loc[idx_mask,'B1_WBGT_NOAA'].values - dn.loc[idx_mask,'B1_WBGT_ERA5'].values\n",
        "        ax.scatter(m_sub, d_sub, c=colors_map[cat], alpha=0.7, edgecolors='k', lw=0.3, s=50, label=cat)\n",
        "ax.axhline(md, color='black', lw=1.5, label=f'Bias: {md:+.2f}°C')\n",
        "ax.axhline(uloa, color='gray', ls='--', lw=1, label=f'LoA: [{lloa:.1f}, {uloa:.1f}]')\n",
        "ax.axhline(lloa, color='gray', ls='--', lw=1)\n",
        "ax.axhline(0, color='lightgray', lw=0.5)\n",
        "ax.set_xlabel('Mean of NOAA and ERA5 WBGT (°C)'); ax.set_ylabel('NOAA − ERA5 (°C)')\n",
        "ax.set_title('Bland–Altman'); ax.legend(fontsize=7)\n",
        "\n",
        "# 3. Bias by venue\n",
        "ax = axes[2]\n",
        "sesgo = dn.groupby('A5_Sede')['B1_WBGT_diff'].mean().sort_values()\n",
        "colors_bar = ['crimson' if x>0 else 'steelblue' for x in sesgo.values]\n",
        "ax.barh(range(len(sesgo)), sesgo.values, color=colors_bar, edgecolor='black', lw=0.5)\n",
        "ax.set_yticks(range(len(sesgo))); ax.set_yticklabels(sesgo.index, fontsize=8)\n",
        "ax.axvline(0, color='k', lw=0.5); ax.set_xlabel('Bias NOAA − ERA5 (°C)'); ax.set_title('Bias by venue')\n",
        "\n",
        "# 4. Categories\n",
        "ax = axes[3]\n",
        "cats = ['Low','Moderate','High']\n",
        "e_c = [(dn['B11_cat_ERA5'].isin(['Bajo','Low']) if c=='Low' else dn['B11_cat_ERA5']==c).sum() for c in cats]\n",
        "n_c = [(dn['B11_cat_NOAA']==c).sum() for c in cats]\n",
        "x = np.arange(len(cats))\n",
        "ax.bar(x-0.2, e_c, 0.35, label='ERA5', color='lightblue', edgecolor='black', lw=0.5)\n",
        "ax.bar(x+0.2, n_c, 0.35, label='NOAA', color='salmon', edgecolor='black', lw=0.5)\n",
        "ax.set_xticks(x); ax.set_xticklabels(cats); ax.legend(); ax.set_title('Risk categories')\n",
        "\n",
        "plt.tight_layout()\n",
        "plt.savefig('NOAA_vs_ERA5_validation.png', dpi=200, bbox_inches='tight')\n",
        "plt.savefig('Bland_Altman.png', dpi=300, bbox_inches='tight')\n",
        "plt.show()\n",
        "print('✅ Figuras guardadas')\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "NOAA vs ERA5 (n=100)\n",
            "  R² = 0.933, RMSE = 1.35°C, bias = +0.52°C\n",
            "\n",
            "BLAND-ALTMAN:\n",
            "  Mean bias: +0.523°C\n",
            "  SD: 1.253°C\n",
            "  95% LoA: [-1.93, 2.98]°C\n",
            "  95% CI of bias: [0.28, 0.77]\n",
            "  Proportional bias: r=0.286, p=0.0039\n",
            "  Outside LoA: 6/100 (6.0%)\n",
            "✅ Figuras guardadas\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "wFKKtWcwhEuF",
        "outputId": "533ffd30-81e5-4488-e997-d98ea9f9a385"
      },
      "source": [
        "# Celda 9 — M5/M6: Todas las métricas para el artículo\n",
        "# ICC, Mixed model, sdh (INTEGRACIÓN HORARIA), Gini, Bootstrap, Leave-one-out\n",
        "\n",
        "dn = df_noaa[df_noaa['B1_WBGT_NOAA'].notna()].copy()\n",
        "n_matches = len(dn); n_venues = dn['A5_Sede'].nunique()\n",
        "\n",
        "# Añadir variables para modelo mixto\n",
        "for eid, s in sedes.items():\n",
        "    mask = dn['A5_Sede'] == s['ciudad']\n",
        "    dn.loc[mask, 'altitud'] = s['alt_m']\n",
        "    dn.loc[mask, 'lat'] = s['lat']\n",
        "dn['hora_num'] = pd.to_numeric(dn['A3_Hora_local'].str.split(':').str[0], errors='coerce')\n",
        "dn['ocupacion'] = pd.to_numeric(dn['C7_Ocupacion'], errors='coerce')\n",
        "\n",
        "print(\"=\" * 70)\n",
        "print(f\"ANÁLISIS COMPLETO — {n_matches} partidos, {n_venues} sedes\")\n",
        "print(\"=\" * 70)\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 1. ICC\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "groups = [g['B1_WBGT_NOAA'].values for _, g in dn.groupby('A5_Sede')]\n",
        "k = len(groups)\n",
        "ns = [len(g) for g in groups]; n_total = sum(ns)\n",
        "grand_mean = np.mean(np.concatenate(groups))\n",
        "SS_between = sum(len(g) * (np.mean(g) - grand_mean)**2 for g in groups)\n",
        "SS_within = sum(np.sum((g - np.mean(g))**2) for g in groups)\n",
        "MS_between = SS_between / (k - 1)\n",
        "MS_within = SS_within / (n_total - k)\n",
        "n0 = (n_total - sum(n**2 for n in ns) / n_total) / (k - 1)\n",
        "ICC = (MS_between - MS_within) / (MS_between + (n0 - 1) * MS_within)\n",
        "F_stat = MS_between / MS_within\n",
        "df1 = k - 1; df2 = n_total - k\n",
        "p_icc = 1 - sp_stats.f.cdf(F_stat, df1, df2)\n",
        "\n",
        "# CI for ICC (Shrout & Fleiss)\n",
        "FL = F_stat / sp_stats.f.ppf(0.975, df1, df2)\n",
        "FU = F_stat / sp_stats.f.ppf(0.025, df1, df2)\n",
        "ICC_lower = (FL - 1) / (FL + n0 - 1)\n",
        "ICC_upper = (FU - 1) / (FU + n0 - 1)\n",
        "\n",
        "print(f\"\\n1. ICC(1,1)\")\n",
        "print(f\"   ICC = {ICC:.3f} (95% CI: {ICC_lower:.3f} – {ICC_upper:.3f})\")\n",
        "print(f\"   F({df1},{df2}) = {F_stat:.1f}, p < {p_icc:.2e}\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 2. Leave-one-venue-out ICC\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "print(f\"\\n2. LEAVE-ONE-VENUE-OUT ICC\")\n",
        "loo_results = {}\n",
        "for venue_out in dn['A5_Sede'].unique():\n",
        "    dn_loo = dn[dn['A5_Sede'] != venue_out]\n",
        "    g_loo = [g['B1_WBGT_NOAA'].values for _, g in dn_loo.groupby('A5_Sede')]\n",
        "    k_l = len(g_loo); ns_l = [len(g) for g in g_loo]; n_l = sum(ns_l)\n",
        "    gm_l = np.mean(np.concatenate(g_loo))\n",
        "    ssb_l = sum(len(g) * (np.mean(g) - gm_l)**2 for g in g_loo)\n",
        "    ssw_l = sum(np.sum((g - np.mean(g))**2) for g in g_loo)\n",
        "    msb_l = ssb_l / (k_l - 1); msw_l = ssw_l / (n_l - k_l)\n",
        "    n0_l = (n_l - sum(n**2 for n in ns_l) / n_l) / (k_l - 1)\n",
        "    icc_l = (msb_l - msw_l) / (msb_l + (n0_l - 1) * msw_l)\n",
        "    loo_results[venue_out] = icc_l\n",
        "    print(f\"   Excl. {venue_out:20s}: ICC = {icc_l:.3f}\")\n",
        "print(f\"   Rango: {min(loo_results.values()):.3f} – {max(loo_results.values()):.3f}\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 3. Dispersion metrics\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "venue_means = dn.groupby('A5_Sede')['B1_WBGT_NOAA'].mean().sort_values(ascending=False)\n",
        "cv = (venue_means.std() / venue_means.mean()) * 100\n",
        "gini_vals = venue_means.values\n",
        "n_g = len(gini_vals)\n",
        "gini = np.sum(np.abs(np.subtract.outer(gini_vals, gini_vals))) / (2 * n_g**2 * np.mean(gini_vals))\n",
        "iqr = np.percentile(venue_means, 75) - np.percentile(venue_means, 25)\n",
        "p90_p10 = np.percentile(venue_means, 90) - np.percentile(venue_means, 10)\n",
        "\n",
        "print(f\"\\n3. DISPERSIÓN\")\n",
        "print(f\"   Media global: {venue_means.mean():.2f}°C\")\n",
        "print(f\"   SD inter-sede: {venue_means.std():.2f}°C\")\n",
        "print(f\"   CV: {cv:.1f}%\")\n",
        "print(f\"   Gini: {gini:.3f}\")\n",
        "print(f\"   IQR: {iqr:.2f}°C\")\n",
        "print(f\"   P90-P10: {p90_p10:.2f}°C\")\n",
        "print(f\"   Disparity: {venue_means.max():.2f} - {venue_means.min():.2f} = {venue_means.max()-venue_means.min():.2f}°C\")\n",
        "print(f\"   Hottest: {venue_means.idxmax()} ({venue_means.max():.2f}°C)\")\n",
        "print(f\"   Coolest: {venue_means.idxmin()} ({venue_means.min():.2f}°C)\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 4. Bootstrap CI for disparity\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "hot_venue = venue_means.idxmax(); cold_venue = venue_means.idxmin()\n",
        "hot_vals = dn[dn['A5_Sede']==hot_venue]['B1_WBGT_NOAA'].values\n",
        "cold_vals = dn[dn['A5_Sede']==cold_venue]['B1_WBGT_NOAA'].values\n",
        "obs_diff = hot_vals.mean() - cold_vals.mean()\n",
        "t_stat, t_pval = sp_stats.ttest_ind(hot_vals, cold_vals)\n",
        "np.random.seed(42)\n",
        "boot_diffs = [np.random.choice(hot_vals, len(hot_vals), replace=True).mean() -\n",
        "              np.random.choice(cold_vals, len(cold_vals), replace=True).mean() for _ in range(10000)]\n",
        "ci_lo = np.percentile(boot_diffs, 2.5); ci_hi = np.percentile(boot_diffs, 97.5)\n",
        "\n",
        "print(f\"\\n4. BOOTSTRAP DISPARITY\")\n",
        "print(f\"   {hot_venue} (n={len(hot_vals)}): {hot_vals.mean():.2f} ± {hot_vals.std():.2f}°C\")\n",
        "print(f\"   {cold_venue} (n={len(cold_vals)}): {cold_vals.mean():.2f} ± {cold_vals.std():.2f}°C\")\n",
        "print(f\"   Disparity: {obs_diff:.2f}°C [95% CI: {ci_lo:.1f} – {ci_hi:.1f}]\")\n",
        "print(f\"   t = {t_stat:.1f}, p = {t_pval:.2e}\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 5. Mixed model (sin shadow)\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "dn['wbgt'] = dn['B1_WBGT_NOAA']\n",
        "dn['sede'] = dn['A5_Sede']\n",
        "model = smf.mixedlm(\"wbgt ~ hora_num + altitud + lat\", dn, groups=dn[\"sede\"])\n",
        "result = model.fit(reml=True)\n",
        "print(f\"\\n5. MODELO MIXTO: WBGT ~ hora + altitud + lat, (1|sede)\")\n",
        "print(result.summary())\n",
        "var_sede = float(result.cov_re.iloc[0, 0])\n",
        "var_resid = float(result.scale)\n",
        "icc_model = var_sede / (var_sede + var_resid)\n",
        "print(f\"\\n   σ²(sede) = {var_sede:.3f}\")\n",
        "print(f\"   σ²(resid) = {var_resid:.3f}\")\n",
        "print(f\"   ICC_adj = {icc_model:.3f}\")\n",
        "\n",
        "# VIF\n",
        "from statsmodels.stats.outliers_influence import variance_inflation_factor\n",
        "X = dn[['hora_num', 'altitud', 'lat']].dropna()\n",
        "X_c = sm.add_constant(X)\n",
        "vifs = {X.columns[i]: variance_inflation_factor(X_c.values, i+1) for i in range(len(X.columns))}\n",
        "print(f\"   VIFs: {', '.join(f'{k}={v:.2f}' for k,v in vifs.items())}\")\n",
        "\n",
        "# Residual diagnostics\n",
        "resids = result.resid\n",
        "w_stat, w_pval = sp_stats.shapiro(resids)\n",
        "fitted = result.fittedvalues\n",
        "r_het, p_het = sp_stats.spearmanr(fitted, np.abs(resids))\n",
        "print(f\"   Shapiro-Wilk: W={w_stat:.3f}, p={w_pval:.3f}\")\n",
        "print(f\"   Heteroscedasticity: r={r_het:.3f}, p={p_het:.3f}\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 6. Spectator-degree-hours (INTEGRACIÓN HORARIA)\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "print(f\"\\n6. SPECTATOR-DEGREE-HOURS (integración horaria)\")\n",
        "for threshold in [26, 28, 30]:\n",
        "    col = f'B7_grados_hora_{threshold}'\n",
        "    if col in dn.columns:\n",
        "        dn[f'sdh_{threshold}'] = dn[col].fillna(0) * dn['ocupacion'].fillna(0)\n",
        "        total = dn[f'sdh_{threshold}'].sum()\n",
        "        print(f\"\\n   Umbral {threshold}°C: Total sdh = {total:,.0f}\")\n",
        "        by_venue = dn.groupby('A5_Sede')[f'sdh_{threshold}'].sum().sort_values(ascending=False)\n",
        "        for v, val in by_venue.items():\n",
        "            if val > 0:\n",
        "                print(f\"     {v:20s}: {val:>12,.0f} ({100*val/total:.0f}%)\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 7. Threshold sensitivity table\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "print(f\"\\n7. TABLA DE SENSIBILIDAD AL UMBRAL\")\n",
        "print(f\"{'Threshold':>10} | {'Source':>8} | {'Matches':>8} | {'%':>6} | {'Venues':>6} | {'sdh':>12} | {'Disparity':>10}\")\n",
        "print(\"-\" * 75)\n",
        "disp = venue_means.max() - venue_means.min()\n",
        "for u, src in [(26,'ACSM'), (28,'FIFPRO'), (30,'ACSM'), (32,'FIFA')]:\n",
        "    n_above = (dn['B1_WBGT_NOAA'] >= u).sum()\n",
        "    pct = 100 * n_above / n_matches\n",
        "    n_v = dn[dn['B1_WBGT_NOAA'] >= u]['A5_Sede'].nunique()\n",
        "    col = f'sdh_{u}'\n",
        "    sdh = dn[col].sum() if col in dn.columns else 0\n",
        "    print(f\"   {u}°C     | {src:>8} | {n_above:>8} | {pct:>5.1f}% | {n_v:>6} | {sdh:>12,.0f} | {disp:.2f}°C\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# 8. sWBGT comparison\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "if 'B1_sWBGT_NOAA' in dn.columns and dn['B1_sWBGT_NOAA'].notna().sum() > 0:\n",
        "    print(f\"\\n8. sWBGT vs LILJEGREN\")\n",
        "    sw = dn['B1_sWBGT_NOAA']; lj = dn['B1_WBGT_NOAA']\n",
        "    diff_sw = lj - sw\n",
        "    r_sw, _ = sp_stats.pearsonr(sw.dropna(), lj[sw.notna()])\n",
        "    pct_sw = 100 * (sw >= 18.4).sum() / len(sw)\n",
        "    pct_lj = 100 * (lj >= 18.4).sum() / len(lj)\n",
        "    disp_sw = dn.groupby('A5_Sede')['B1_sWBGT_NOAA'].mean().max() - dn.groupby('A5_Sede')['B1_sWBGT_NOAA'].mean().min()\n",
        "    print(f\"   Mean: sWBGT={sw.mean():.2f}, Liljegren={lj.mean():.2f}, diff={diff_sw.mean():+.2f}\")\n",
        "    print(f\"   ≥18.4%: sWBGT={pct_sw:.1f}%, Liljegren={pct_lj:.1f}% (Craig: 67%)\")\n",
        "    print(f\"   ≥28: sWBGT={(sw>=28).sum()}, Liljegren={(lj>=28).sum()}\")\n",
        "    print(f\"   ≥30: sWBGT={(sw>=30).sum()}, Liljegren={(lj>=30).sum()}\")\n",
        "    print(f\"   Disparity: sWBGT={disp_sw:.2f}, Liljegren={disp:.2f}\")\n",
        "    print(f\"   R²={r_sw**2:.3f}\")\n",
        "\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "# SUMMARY TABLE\n",
        "# ══════════════════════════════════════════════════════════════\n",
        "print(f\"\\n{'='*70}\")\n",
        "print(\"RESUMEN PARA EL MANUSCRITO\")\n",
        "print(f\"{'='*70}\")\n",
        "print(f\"  n = {n_matches} partidos, {n_venues} sedes\")\n",
        "print(f\"  ICC = {ICC:.3f} [{ICC_lower:.3f}–{ICC_upper:.3f}], F({df1},{df2})={F_stat:.1f}\")\n",
        "print(f\"  LOO ICC range: {min(loo_results.values()):.3f}–{max(loo_results.values()):.3f}\")\n",
        "print(f\"  Disparity = {obs_diff:.2f}°C [{ci_lo:.1f}–{ci_hi:.1f}], t={t_stat:.1f}, p={t_pval:.1e}\")\n",
        "print(f\"  Mixed model: hora={result.fe_params['hora_num']:.3f}, alt={result.fe_params['altitud']:.5f}, lat={result.fe_params['lat']:.3f}\")\n",
        "print(f\"  ICC_adj = {icc_model:.3f}\")\n",
        "print(f\"  Gini = {gini:.3f}, IQR = {iqr:.2f}, P90-P10 = {p90_p10:.2f}\")\n",
        "sdh28 = dn['sdh_28'].sum() if 'sdh_28' in dn.columns else 'N/A'\n",
        "print(f\"  sdh_28 (hourly integration) = {sdh28:,.0f}\" if isinstance(sdh28, (int,float)) else f\"  sdh_28 = {sdh28}\")\n",
        "print(f\"  NOAA-ERA5: R²={r**2:.3f}, RMSE={rmse:.2f}, bias={bias:+.2f}\")\n",
        "print(f\"  Bland-Altman: LoA=[{lloa:.2f}, {uloa:.2f}], prop.bias r={r_pb:.3f}\")\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "======================================================================\n",
            "ANÁLISIS COMPLETO — 100 partidos, 15 sedes\n",
            "======================================================================\n",
            "\n",
            "1. ICC(1,1)\n",
            "   ICC = 0.707 (95% CI: 0.528 – 0.866)\n",
            "   F(14,85) = 17.1, p < 1.11e-16\n",
            "\n",
            "2. LEAVE-ONE-VENUE-OUT ICC\n",
            "   Excl. Ciudad de Mexico    : ICC = 0.693\n",
            "   Excl. Toronto             : ICC = 0.733\n",
            "   Excl. Los Angeles         : ICC = 0.694\n",
            "   Excl. San Francisco       : ICC = 0.697\n",
            "   Excl. Nueva York/NJ       : ICC = 0.721\n",
            "   Excl. Boston              : ICC = 0.725\n",
            "   Excl. Vancouver           : ICC = 0.707\n",
            "   Excl. Houston             : ICC = 0.678\n",
            "   Excl. Dallas              : ICC = 0.675\n",
            "   Excl. Filadelfia          : ICC = 0.729\n",
            "   Excl. Monterrey           : ICC = 0.708\n",
            "   Excl. Atlanta             : ICC = 0.700\n",
            "   Excl. Seattle             : ICC = 0.740\n",
            "   Excl. Miami               : ICC = 0.687\n",
            "   Excl. Kansas City         : ICC = 0.714\n",
            "   Rango: 0.675 – 0.740\n",
            "\n",
            "3. DISPERSIÓN\n",
            "   Media global: 22.48°C\n",
            "   SD inter-sede: 4.20°C\n",
            "   CV: 18.7%\n",
            "   Gini: 0.103\n",
            "   IQR: 6.89°C\n",
            "   P90-P10: 10.59°C\n",
            "   Disparity: 28.78 - 16.32 = 12.46°C\n",
            "   Hottest: Houston (28.78°C)\n",
            "   Coolest: San Francisco (16.32°C)\n",
            "\n",
            "4. BOOTSTRAP DISPARITY\n",
            "   Houston (n=7): 28.78 ± 1.89°C\n",
            "   San Francisco (n=6): 16.32 ± 3.34°C\n",
            "   Disparity: 12.46°C [95% CI: 9.0 – 15.1]\n",
            "   t = 7.8, p = 8.82e-06\n",
            "\n",
            "5. MODELO MIXTO: WBGT ~ hora + altitud + lat, (1|sede)\n",
            "         Mixed Linear Model Regression Results\n",
            "=======================================================\n",
            "Model:            MixedLM Dependent Variable: wbgt     \n",
            "No. Observations: 100     Method:             REML     \n",
            "No. Groups:       15      Scale:              5.9256   \n",
            "Min. group size:  4       Log-Likelihood:     -254.8089\n",
            "Max. group size:  9       Converged:          Yes      \n",
            "Mean group size:  6.7                                  \n",
            "-------------------------------------------------------\n",
            "             Coef.  Std.Err.   z    P>|z| [0.025 0.975]\n",
            "-------------------------------------------------------\n",
            "Intercept    43.021    5.308  8.105 0.000 32.618 53.424\n",
            "hora_num     -0.328    0.080 -4.127 0.000 -0.484 -0.172\n",
            "altitud      -0.005    0.002 -2.613 0.009 -0.009 -0.001\n",
            "lat          -0.374    0.133 -2.819 0.005 -0.634 -0.114\n",
            "Group Var     9.670    1.889                           \n",
            "=======================================================\n",
            "\n",
            "\n",
            "   σ²(sede) = 9.670\n",
            "   σ²(resid) = 5.926\n",
            "   ICC_adj = 0.620\n",
            "   VIFs: hora_num=1.06, altitud=1.59, lat=1.54\n",
            "   Shapiro-Wilk: W=0.975, p=0.050\n",
            "   Heteroscedasticity: r=-0.076, p=0.454\n",
            "\n",
            "6. SPECTATOR-DEGREE-HOURS (integración horaria)\n",
            "\n",
            "   Umbral 26°C: Total sdh = 19,421,179\n",
            "     Houston             :    7,572,113 (39%)\n",
            "     Dallas              :    5,433,677 (28%)\n",
            "     Atlanta             :    3,515,043 (18%)\n",
            "     Miami               :    1,852,711 (10%)\n",
            "     Filadelfia          :      906,711 (5%)\n",
            "     Toronto             :       73,922 (0%)\n",
            "     Boston              :       67,002 (0%)\n",
            "\n",
            "   Umbral 28°C: Total sdh = 5,286,952\n",
            "     Houston             :    2,901,175 (55%)\n",
            "     Dallas              :    1,083,737 (20%)\n",
            "     Atlanta             :    1,014,714 (19%)\n",
            "     Filadelfia          :      185,698 (4%)\n",
            "     Miami               :      101,628 (2%)\n",
            "\n",
            "   Umbral 30°C: Total sdh = 517,543\n",
            "     Houston             :      383,240 (74%)\n",
            "     Atlanta             :      118,053 (23%)\n",
            "     Dallas              :       16,249 (3%)\n",
            "\n",
            "7. TABLA DE SENSIBILIDAD AL UMBRAL\n",
            " Threshold |   Source |  Matches |      % | Venues |          sdh |  Disparity\n",
            "---------------------------------------------------------------------------\n",
            "   26°C     |     ACSM |       31 |  31.0% |      7 |   19,421,179 | 12.46°C\n",
            "   28°C     |   FIFPRO |       17 |  17.0% |      5 |    5,286,952 | 12.46°C\n",
            "   30°C     |     ACSM |        5 |   5.0% |      3 |      517,543 | 12.46°C\n",
            "   32°C     |     FIFA |        0 |   0.0% |      0 |            0 | 12.46°C\n",
            "\n",
            "8. sWBGT vs LILJEGREN\n",
            "   Mean: sWBGT=22.07, Liljegren=22.75, diff=+0.68\n",
            "   ≥18.4%: sWBGT=74.0%, Liljegren=83.0% (Craig: 67%)\n",
            "   ≥28: sWBGT=9, Liljegren=17\n",
            "   ≥30: sWBGT=0, Liljegren=5\n",
            "   Disparity: sWBGT=11.78, Liljegren=12.46\n",
            "   R²=0.986\n",
            "\n",
            "======================================================================\n",
            "RESUMEN PARA EL MANUSCRITO\n",
            "======================================================================\n",
            "  n = 100 partidos, 15 sedes\n",
            "  ICC = 0.707 [0.528–0.866], F(14,85)=17.1\n",
            "  LOO ICC range: 0.675–0.740\n",
            "  Disparity = 12.46°C [9.0–15.1], t=7.8, p=8.8e-06\n",
            "  Mixed model: hora=-0.328, alt=-0.00527, lat=-0.374\n",
            "  ICC_adj = 0.620\n",
            "  Gini = 0.103, IQR = 6.89, P90-P10 = 10.59\n",
            "  sdh_28 (hourly integration) = 5,286,952\n",
            "  NOAA-ERA5: R²=0.933, RMSE=1.35, bias=+0.52\n",
            "  Bland-Altman: LoA=[-1.93, 2.98], prop.bias r=0.286\n"
          ]
        }
      ],
      "execution_count": null
    },
    {
      "cell_type": "code",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 52
        },
        "id": "2o5B40iXhEuJ",
        "outputId": "1f869961-2069-4c1b-e1fc-0697f85bd6c5"
      },
      "source": [
        "# Celda 10 — Exportar Excel definitivo\n",
        "output = 'Mundial2026_WBGT_NOAA_15sedes_DEFINITIVO.xlsx'\n",
        "\n",
        "dn_export = df_noaa[df_noaa['B1_WBGT_NOAA'].notna()].copy()\n",
        "if 'error' in dn_export.columns: dn_export = dn_export.drop(columns=['error'])\n",
        "\n",
        "sede_summary = dn_export.groupby('A5_Sede').agg(\n",
        "    ICAO=('Estacion', 'first'), Dist_km=('Dist_km', 'first'),\n",
        "    WBGT_mean=('B1_WBGT_NOAA','mean'), WBGT_max=('B1_WBGT_NOAA','max'),\n",
        "    n=('A1_ID','count'),\n",
        "    n_mod=('B11_cat_NOAA', lambda x: (x=='Moderate').sum()),\n",
        "    n_high=('B11_cat_NOAA', lambda x: (x=='High').sum()),\n",
        "    bias=('B1_WBGT_diff','mean'),\n",
        ").sort_values('WBGT_mean', ascending=False)\n",
        "\n",
        "with pd.ExcelWriter(output, engine='openpyxl') as writer:\n",
        "    dn_export.to_excel(writer, sheet_name='Datos por partido', index=False)\n",
        "    sede_summary.to_excel(writer, sheet_name='Resumen por sede')\n",
        "    era5_only = df_noaa[df_noaa['B1_WBGT_NOAA'].isna()]\n",
        "    if len(era5_only) > 0:\n",
        "        era5_only.to_excel(writer, sheet_name='ERA5 only', index=False)\n",
        "    if len(df_corr) > 0:\n",
        "        df_corr.to_excel(writer, sheet_name='Rad sensitivity', index=False)\n",
        "\n",
        "from google.colab import files\n",
        "files.download(output)\n",
        "files.download('NOAA_vs_ERA5_validation.png')\n",
        "files.download('Bland_Altman.png')\n",
        "print(f'✅ Guardado: {output}')\n",
        "print(f'   NOAA: {len(dn_export)} partidos, {dn_export[\"A5_Sede\"].nunique()} sedes')\n"
      ],
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Javascript object>"
            ],
            "application/javascript": [
              "\n",
              "    async function download(id, filename, size) {\n",
              "      if (!google.colab.kernel.accessAllowed) {\n",
              "        return;\n",
              "      }\n",
              "      const div = document.createElement('div');\n",
              "      const label = document.createElement('label');\n",
              "      label.textContent = `Downloading \"${filename}\": `;\n",
              "      div.appendChild(label);\n",
              "      const progress = document.createElement('progress');\n",
              "      progress.max = size;\n",
              "      div.appendChild(progress);\n",
              "      document.body.appendChild(div);\n",
              "\n",
              "      const buffers = [];\n",
              "      let downloaded = 0;\n",
              "\n",
              "      const channel = await google.colab.kernel.comms.open(id);\n",
              "      // Send a message to notify the kernel that we're ready.\n",
              "      channel.send({})\n",
              "\n",
              "      for await (const message of channel.messages) {\n",
              "        // Send a message to notify the kernel that we're ready.\n",
              "        channel.send({})\n",
              "        if (message.buffers) {\n",
              "          for (const buffer of message.buffers) {\n",
              "            buffers.push(buffer);\n",
              "            downloaded += buffer.byteLength;\n",
              "            progress.value = downloaded;\n",
              "          }\n",
              "        }\n",
              "      }\n",
              "      const blob = new Blob(buffers, {type: 'application/binary'});\n",
              "      const a = document.createElement('a');\n",
              "      a.href = window.URL.createObjectURL(blob);\n",
              "      a.download = filename;\n",
              "      div.appendChild(a);\n",
              "      a.click();\n",
              "      div.remove();\n",
              "    }\n",
              "  "
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Javascript object>"
            ],
            "application/javascript": [
              "download(\"download_8b7a7947-62e5-4076-809d-b5b9d3194330\", \"Mundial2026_WBGT_NOAA_15sedes_DEFINITIVO.xlsx\", 23907)"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Javascript object>"
            ],
            "application/javascript": [
              "\n",
              "    async function download(id, filename, size) {\n",
              "      if (!google.colab.kernel.accessAllowed) {\n",
              "        return;\n",
              "      }\n",
              "      const div = document.createElement('div');\n",
              "      const label = document.createElement('label');\n",
              "      label.textContent = `Downloading \"${filename}\": `;\n",
              "      div.appendChild(label);\n",
              "      const progress = document.createElement('progress');\n",
              "      progress.max = size;\n",
              "      div.appendChild(progress);\n",
              "      document.body.appendChild(div);\n",
              "\n",
              "      const buffers = [];\n",
              "      let downloaded = 0;\n",
              "\n",
              "      const channel = await google.colab.kernel.comms.open(id);\n",
              "      // Send a message to notify the kernel that we're ready.\n",
              "      channel.send({})\n",
              "\n",
              "      for await (const message of channel.messages) {\n",
              "        // Send a message to notify the kernel that we're ready.\n",
              "        channel.send({})\n",
              "        if (message.buffers) {\n",
              "          for (const buffer of message.buffers) {\n",
              "            buffers.push(buffer);\n",
              "            downloaded += buffer.byteLength;\n",
              "            progress.value = downloaded;\n",
              "          }\n",
              "        }\n",
              "      }\n",
              "      const blob = new Blob(buffers, {type: 'application/binary'});\n",
              "      const a = document.createElement('a');\n",
              "      a.href = window.URL.createObjectURL(blob);\n",
              "      a.download = filename;\n",
              "      div.appendChild(a);\n",
              "      a.click();\n",
              "      div.remove();\n",
              "    }\n",
              "  "
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Javascript object>"
            ],
            "application/javascript": [
              "download(\"download_09260754-2db1-44ca-aea5-83d5226c2d34\", \"NOAA_vs_ERA5_validation.png\", 322576)"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Javascript object>"
            ],
            "application/javascript": [
              "\n",
              "    async function download(id, filename, size) {\n",
              "      if (!google.colab.kernel.accessAllowed) {\n",
              "        return;\n",
              "      }\n",
              "      const div = document.createElement('div');\n",
              "      const label = document.createElement('label');\n",
              "      label.textContent = `Downloading \"${filename}\": `;\n",
              "      div.appendChild(label);\n",
              "      const progress = document.createElement('progress');\n",
              "      progress.max = size;\n",
              "      div.appendChild(progress);\n",
              "      document.body.appendChild(div);\n",
              "\n",
              "      const buffers = [];\n",
              "      let downloaded = 0;\n",
              "\n",
              "      const channel = await google.colab.kernel.comms.open(id);\n",
              "      // Send a message to notify the kernel that we're ready.\n",
              "      channel.send({})\n",
              "\n",
              "      for await (const message of channel.messages) {\n",
              "        // Send a message to notify the kernel that we're ready.\n",
              "        channel.send({})\n",
              "        if (message.buffers) {\n",
              "          for (const buffer of message.buffers) {\n",
              "            buffers.push(buffer);\n",
              "            downloaded += buffer.byteLength;\n",
              "            progress.value = downloaded;\n",
              "          }\n",
              "        }\n",
              "      }\n",
              "      const blob = new Blob(buffers, {type: 'application/binary'});\n",
              "      const a = document.createElement('a');\n",
              "      a.href = window.URL.createObjectURL(blob);\n",
              "      a.download = filename;\n",
              "      div.appendChild(a);\n",
              "      a.click();\n",
              "      div.remove();\n",
              "    }\n",
              "  "
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Javascript object>"
            ],
            "application/javascript": [
              "download(\"download_99c40729-b30b-4b88-b52f-15664c7b6e92\", \"Bland_Altman.png\", 562568)"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "✅ Guardado: Mundial2026_WBGT_NOAA_15sedes_DEFINITIVO.xlsx\n",
            "   NOAA: 100 partidos, 15 sedes\n"
          ]
        }
      ],
      "execution_count": null
    }
  ]
}