{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":4535,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":3327,"modelId":986},{"sourceId":88712,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":74443,"modelId":99217},{"sourceId":268942,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":230141,"modelId":251887},{"sourceId":4537,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":3329,"modelId":986}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":648.840635,"end_time":"2026-01-26T19:47:49.486146","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-26T19:37:00.645511","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"0ede59bf05564f969039cdbeb21b766f":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_d6a9570b3a6a40a1b98ddac813f8c98c","placeholder":"​","style":"IPY_MODEL_fd5ee838834844df9e9bfc2944410299","tabbable":null,"tooltip":null,"value":" 1/1 [00:01&lt;00:00,  1.14s/it]"}},"1122471b0bc748e1b15995c9a57791e4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_4411a00e2b544831a6f64a9e92d50c45","max":357,"min":0,"orientation":"horizontal","style":"IPY_MODEL_e3ebb342a12343a4b8a1e6ebf7bf0a6d","tabbable":null,"tooltip":null,"value":357}},"19744fa77a97402e9239e415231070e6":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"1a387c15bd0d479091931dc28405babb":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_50a82b3e629f4fdcaf66c5b83a80d253","IPY_MODEL_1122471b0bc748e1b15995c9a57791e4","IPY_MODEL_d24c092fb06346af92ae0b170b2d2566"],"layout":"IPY_MODEL_a401efd79a104609bd9457d5918a7dfa","tabbable":null,"tooltip":null}},"4411a00e2b544831a6f64a9e92d50c45":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"4e08dea14ead43d79dca161a4fd96f10":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"50a82b3e629f4fdcaf66c5b83a80d253":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_ac03b5c7eaab4c5ba82c3e5ed9b6282e","placeholder":"​","style":"IPY_MODEL_7f462f4f65c343928423bab822158863","tabbable":null,"tooltip":null,"value":"Extracting: 100%"}},"6b3dcda2a0ad47d39e63ade6eb915152":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"7f462f4f65c343928423bab822158863":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"88970338da8d4bc9a855242cb61404b9":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_e0ad01b55e8b4bc5a443fa279b8be886","IPY_MODEL_db96185aaaea4d4f972ea47981afff22","IPY_MODEL_0ede59bf05564f969039cdbeb21b766f"],"layout":"IPY_MODEL_b80b0e92695b4c81b80560b8a6227122","tabbable":null,"tooltip":null}},"8adc531c67be439b8b04b209792fa3ab":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"a401efd79a104609bd9457d5918a7dfa":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"ac03b5c7eaab4c5ba82c3e5ed9b6282e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"b80b0e92695b4c81b80560b8a6227122":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"cf40439de28648e79b646bbcd2f291ed":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"d24c092fb06346af92ae0b170b2d2566":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_6b3dcda2a0ad47d39e63ade6eb915152","placeholder":"​","style":"IPY_MODEL_8adc531c67be439b8b04b209792fa3ab","tabbable":null,"tooltip":null,"value":" 357/357 [06:38&lt;00:00,  1.11s/it]"}},"d6a9570b3a6a40a1b98ddac813f8c98c":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"db96185aaaea4d4f972ea47981afff22":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_e29bbc4e0dd040c6a76d35c9162a5fe0","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_4e08dea14ead43d79dca161a4fd96f10","tabbable":null,"tooltip":null,"value":1}},"e0ad01b55e8b4bc5a443fa279b8be886":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_cf40439de28648e79b646bbcd2f291ed","placeholder":"​","style":"IPY_MODEL_19744fa77a97402e9239e415231070e6","tabbable":null,"tooltip":null,"value":"Extracting: 100%"}},"e29bbc4e0dd040c6a76d35c9162a5fe0":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":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,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"e3ebb342a12343a4b8a1e6ebf7bf0a6d":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"fd5ee838834844df9e9bfc2944410299":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"07e0c017","cell_type":"markdown","source":"# CSIRO Image2Biomass \n\n","metadata":{"_cell_guid":"b2d51c58-9357-4019-9dd2-63aea89233e6","_uuid":"aabaaf1e-057b-44c4-bd8e-6c4f6f163e0c","collapsed":false,"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.006571,"end_time":"2026-01-26T19:37:04.093153","exception":false,"start_time":"2026-01-26T19:37:04.086582","status":"completed"},"tags":[]}},{"id":"5c7ec5d5","cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_cell_guid":"476f0366-5084-448f-b4b8-13cda52acda8","_uuid":"aa2fbf25-3496-4027-b3ce-8605acf78666","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:37:04.107463Z","iopub.status.busy":"2026-01-26T19:37:04.106564Z","iopub.status.idle":"2026-01-26T19:37:05.495198Z","shell.execute_reply":"2026-01-26T19:37:05.494279Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":1.398451,"end_time":"2026-01-26T19:37:05.498037","exception":false,"start_time":"2026-01-26T19:37:04.099586","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"766d6518","cell_type":"code","source":"# Imports\nimport os\nimport gc\nimport warnings\n\nimport numpy as np\nimport pandas as pd\nimport cv2\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport torch\nfrom sklearn.model_selection import StratifiedKFold  # ADDED - you use this later\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.decomposition import PCA\nfrom sklearn.cross_decomposition import PLSRegression\nfrom sklearn.mixture import GaussianMixture\nfrom sklearn.metrics import r2_score\nfrom sklearn.ensemble import HistGradientBoostingRegressor\n\nfrom lightgbm import LGBMRegressor\nfrom catboost import CatBoostRegressor\nfrom xgboost import XGBRegressor\n\nfrom transformers import (\n    AutoModel,\n    AutoImageProcessor,\n    AutoTokenizer\n)\n\nwarnings.filterwarnings('ignore')\nos.environ['TOKENIZERS_PARALLELISM'] = 'false'\n\nprint('✓ Imports successful')","metadata":{"_cell_guid":"1374e507-f4f8-407d-afe8-c561920335ed","_uuid":"aa3f0fb9-07c3-4963-9a96-0ecc2f81f2b2","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:37:05.512497Z","iopub.status.busy":"2026-01-26T19:37:05.512051Z","iopub.status.idle":"2026-01-26T19:37:50.488506Z","shell.execute_reply":"2026-01-26T19:37:50.487617Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":44.991861,"end_time":"2026-01-26T19:37:50.496689","exception":false,"start_time":"2026-01-26T19:37:05.504828","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f200d2e0","cell_type":"code","source":"# Configuration\nclass Config:\n    data_dir = '/kaggle/input/csiro-biomass'\n    train_dir = f'{data_dir}/train'\n    test_dir = f'{data_dir}/test'\n    \n    siglip_path = '/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1'\n    \n    seed = 42\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    \n    patch_size = 520\n    overlap = 16\n    n_splits = 5  # FIXED: Changed from 4 to 5 (matches your StratifiedKFold later)\n    \n    target_names = [\n        'Dry_Clover_g',\n        'Dry_Dead_g', \n        'Dry_Green_g',\n        'Dry_Total_g',\n        'GDM_g'\n    ]\n    \n    target_max = {\n        'Dry_Clover_g': 71.7865,\n        'Dry_Dead_g': 83.8407,\n        'Dry_Green_g': 157.9836,\n        'Dry_Total_g': 185.70,\n        'GDM_g': 157.9836,\n    }\n    \n    # ADDED: Competition evaluation weights (from arxiv paper)\n    target_weights = {\n        'Dry_Clover_g': 0.1,\n        'Dry_Dead_g': 0.1,\n        'Dry_Green_g': 0.1,\n        'GDM_g': 0.2,\n        'Dry_Total_g': 0.5\n    }\n\ncfg = Config()\n\nnp.random.seed(cfg.seed)\ntorch.manual_seed(cfg.seed)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(cfg.seed)\n\nprint(f'✓ Device: {cfg.device}')\nprint(f'✓ Using {cfg.n_splits} folds')","metadata":{"_cell_guid":"13209941-8363-49cb-b30d-a8a53634b6f9","_uuid":"e02bbb47-2742-4549-810e-fdc0c8606792","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:37:50.511239Z","iopub.status.busy":"2026-01-26T19:37:50.510335Z","iopub.status.idle":"2026-01-26T19:37:50.524529Z","shell.execute_reply":"2026-01-26T19:37:50.523579Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.023532,"end_time":"2026-01-26T19:37:50.526369","exception":false,"start_time":"2026-01-26T19:37:50.502837","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f7864321","cell_type":"code","source":"# Load data\ntrain_df = pd.read_csv(f'{cfg.data_dir}/train.csv')\ntest_df = pd.read_csv(f'{cfg.data_dir}/test.csv')\n\n# CRITICAL: sample_id should NOT be modified!\n# Just use it as-is from the CSV\ntrain_df['image_id'] = train_df['sample_id'].apply(lambda x: x.split('_')[0])\ntrain_df['image_path'] = train_df['image_id'].apply(\n    lambda x: f\"{cfg.train_dir}/{x}.jpg\"\n)\n\n# CRITICAL FIX: Don't recreate sample_id, just extract image_id\ntest_df['image_id'] = test_df['sample_id'].apply(lambda x: x.split('_')[0])\ntest_df['image_path'] = test_df['image_id'].apply(\n    lambda x: f\"{cfg.test_dir}/{x}.jpg\"\n)\n\n# Rest stays the same...\ntrain_wide = train_df.pivot_table(\n    index='image_path',\n    columns='target_name',\n    values='target',\n    aggfunc='first'\n).reset_index()\n\ntrain_meta = train_df.drop_duplicates('image_path')[[\n    'image_path', 'State', 'Pre_GSHH_NDVI', 'Height_Ave_cm'\n]]\ntrain_wide = train_wide.merge(train_meta, on='image_path')\n\ntest_images = test_df[['image_id', 'image_path']].drop_duplicates().reset_index(drop=True)\n\nprint(f'✓ Train images: {len(train_wide)}')\nprint(f'✓ Test images: {len(test_images)} unique')\nprint(f'✓ Test predictions needed: {len(test_df)} (5 per image)')","metadata":{"_cell_guid":"17fe17cf-0ff5-401a-b68c-9a94f8b0822a","_uuid":"bab104fd-7ad4-42f4-9746-9d33eeaccce1","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:37:50.540279Z","iopub.status.busy":"2026-01-26T19:37:50.539928Z","iopub.status.idle":"2026-01-26T19:37:50.630864Z","shell.execute_reply":"2026-01-26T19:37:50.629670Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.100434,"end_time":"2026-01-26T19:37:50.632890","exception":false,"start_time":"2026-01-26T19:37:50.532456","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f0217425","cell_type":"code","source":"# Create CV folds stratified by State\nstate_mapping = {state: i for i, state in enumerate(train_wide['State'].unique())}\ntrain_wide['state_encoded'] = train_wide['State'].map(state_mapping)\n\nskf = StratifiedKFold(n_splits=cfg.n_splits, shuffle=True, random_state=cfg.seed)\ntrain_wide['fold'] = -1\n\nfor fold, (_, val_idx) in enumerate(skf.split(train_wide, train_wide['state_encoded'])):\n    train_wide.loc[val_idx, 'fold'] = fold\n\n# Validate fold distribution\nprint('✓ CV folds created with StratifiedKFold')\nprint(f'\\nFold distribution by State:')\nfold_dist = train_wide.groupby(['fold', 'State']).size().unstack(fill_value=0)\nprint(fold_dist)\nprint(f'\\nSamples per fold:')\nprint(train_wide['fold'].value_counts().sort_index())","metadata":{"_cell_guid":"11d41389-cefd-45b7-bf5e-546463e4b3f1","_uuid":"80286155-22b5-4cb0-a4a2-4fccbe7e45a8","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:37:50.648827Z","iopub.status.busy":"2026-01-26T19:37:50.647740Z","iopub.status.idle":"2026-01-26T19:37:50.677908Z","shell.execute_reply":"2026-01-26T19:37:50.676734Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.040723,"end_time":"2026-01-26T19:37:50.680006","exception":false,"start_time":"2026-01-26T19:37:50.639283","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"9c959270","cell_type":"code","source":"# Patch extraction\ndef split_image(image, patch_size=520, overlap=16):\n    \"\"\"\n    Split image into overlapping patches.\n    Images are 2000x1000 after preprocessing (from arxiv paper).\n    \"\"\"\n    h, w, c = image.shape\n    stride = patch_size - overlap\n    patches = []\n    \n    for y in range(0, h, stride):\n        for x in range(0, w, stride):\n            y2 = min(y + patch_size, h)\n            x2 = min(x + patch_size, w)\n            y1 = max(0, y2 - patch_size)\n            x1 = max(0, x2 - patch_size)\n            patches.append(image[y1:y2, x1:x2])\n    \n    return patches\n\ndef extract_siglip_embeddings(model_path, df, device='cuda', batch_size=8):\n    \"\"\"\n    Extract SigLIP embeddings efficiently with error handling.\n    \"\"\"\n    print(f\"Extracting embeddings for {len(df)} images...\")\n    \n    # Load model ONCE and keep on GPU\n    model = AutoModel.from_pretrained(model_path).eval().to(device)\n    processor = AutoImageProcessor.from_pretrained(model_path)\n    \n    embeddings = []\n    failed_images = []\n    \n    for idx, row in tqdm(df.iterrows(), total=len(df), desc=\"Extracting\"):\n        try:\n            img = cv2.imread(row['image_path'])\n            if img is None:\n                raise ValueError(f\"Failed to load image: {row['image_path']}\")\n            \n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            \n            # Split into patches\n            patches = split_image(img, patch_size=cfg.patch_size, overlap=cfg.overlap)\n            \n            # Process all patches at once (batch processing)\n            images = [Image.fromarray(patch) for patch in patches]\n            inputs = processor(images=images, return_tensors='pt')\n            inputs = {k: v.to(device) for k, v in inputs.items()}\n            \n            with torch.no_grad():\n                features = model.get_image_features(**inputs)\n            \n            # Average patch embeddings\n            avg_embedding = features.mean(dim=0).cpu().numpy()\n            embeddings.append(avg_embedding)\n            \n            # Cleanup\n            del inputs, features\n            \n        except Exception as e:\n            print(f\"\\n⚠️  Error processing {row['image_path']}: {str(e)}\")\n            failed_images.append(row['image_path'])\n            # Add zero embedding as placeholder\n            embeddings.append(np.zeros(1152))  # SigLIP embedding size\n    \n    # Cleanup model\n    del model\n    torch.cuda.empty_cache()\n    \n    if failed_images:\n        print(f\"\\n⚠️  Failed to process {len(failed_images)} images\")\n    \n    return np.stack(embeddings)\n\nprint('✓ Extraction functions defined')","metadata":{"_cell_guid":"45ef31cf-f855-48a9-ba1b-eb2db7bfc598","_uuid":"ce31359c-d918-4242-a0d6-12a0e1bee26a","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:37:50.695160Z","iopub.status.busy":"2026-01-26T19:37:50.694280Z","iopub.status.idle":"2026-01-26T19:37:50.707627Z","shell.execute_reply":"2026-01-26T19:37:50.706520Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.022941,"end_time":"2026-01-26T19:37:50.709324","exception":false,"start_time":"2026-01-26T19:37:50.686383","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8d698958","cell_type":"code","source":"# Extract embeddings\nprint('\\n=== Extracting SigLIP Embeddings ===')\n\n# TRAIN: Extract embeddings (one per unique image)\ntrain_siglip_emb = extract_siglip_embeddings(\n    cfg.siglip_path, \n    train_wide, \n    cfg.device\n)\n\n# TEST: Extract from UNIQUE images only (CRITICAL FIX!)\ntest_siglip_emb = extract_siglip_embeddings(\n    cfg.siglip_path, \n    test_images,  # ← FIXED: was test_df (5 rows per image)\n    cfg.device\n)\n\nprint(f'✓ Train embeddings: {train_siglip_emb.shape}')\nprint(f'✓ Test embeddings: {test_siglip_emb.shape}')\nprint(f'✓ Expected train shape: ({len(train_wide)}, 1152)')\nprint(f'✓ Expected test shape: ({len(test_images)}, 1152)')\n\n# Verify shapes match\nassert train_siglip_emb.shape[0] == len(train_wide), \"Train embedding count mismatch!\"\nassert test_siglip_emb.shape[0] == len(test_images), \"Test embedding count mismatch!\"\n\nprint('✓ Shape validation passed')\n\ngc.collect()\ntorch.cuda.empty_cache()","metadata":{"_cell_guid":"89e4bda0-c2e6-4fd6-998f-db1aa620d819","_uuid":"df66def4-79ce-4024-9175-dd4d49b47da5","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:37:50.724331Z","iopub.status.busy":"2026-01-26T19:37:50.723478Z","iopub.status.idle":"2026-01-26T19:45:02.367318Z","shell.execute_reply":"2026-01-26T19:45:02.366172Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":431.653414,"end_time":"2026-01-26T19:45:02.369202","exception":false,"start_time":"2026-01-26T19:37:50.715788","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"3f2f1918","cell_type":"code","source":"# Semantic features\ndef generate_semantic_features(image_embeddings, model_path, device='cuda', cached_vectors=None):\n    \"\"\"\n    Generate semantic similarity features using text prompts.\n    Returns: (features_array, concept_vectors_cache)\n    \"\"\"\n    print(f'Generating semantic features for {len(image_embeddings)} images...')\n    \n    concept_groups = {\n        'bare': ['bare soil', 'dirt ground', 'sparse vegetation', 'exposed earth'],\n        'sparse': ['low density pasture', 'thin grass', 'short clipped grass', 'patchy ground'],\n        'medium': ['average pasture cover', 'medium height grass', 'grazed pasture'],\n        'dense': ['dense tall pasture', 'thick grassy volume', 'high biomass', 'lush vegetation'],\n        'green': ['lush green vibrant pasture', 'photosynthesizing leaves', 'healthy vegetation'],\n        'dead': ['dry brown dead grass', 'yellow straw', 'senesced material', 'withered plants'],\n        'clover': ['white clover', 'trifolium repens', 'broadleaf legume', 'clover patches'],\n        'grass': ['ryegrass', 'blade-like leaves', 'fescue', 'perennial grass'],\n        'height': ['tall vegetation', 'high canopy', 'short ground cover', 'plant height'],\n        'density': ['thick canopy', 'dense coverage', 'sparse ground', 'vegetation density']\n    }\n    \n    # Use cached vectors if available (for test set)\n    if cached_vectors is None:\n        print('  Computing concept vectors...')\n        model = AutoModel.from_pretrained(model_path).to(device)\n        tokenizer = AutoTokenizer.from_pretrained(model_path)\n        \n        concept_vectors = {}\n        with torch.no_grad():\n            for name, prompts in concept_groups.items():\n                inputs = tokenizer(\n                    prompts,\n                    padding='max_length',\n                    max_length=64,\n                    truncation=True,\n                    return_tensors='pt'\n                ).to(device)\n                \n                text_embeddings = model.get_text_features(**inputs)\n                # L2 normalize\n                text_embeddings = text_embeddings / text_embeddings.norm(p=2, dim=-1, keepdim=True)\n                concept_vectors[name] = text_embeddings.mean(dim=0).cpu().numpy()\n        \n        # Cleanup\n        del model, tokenizer\n        torch.cuda.empty_cache()\n    else:\n        print('  Using cached concept vectors...')\n        concept_vectors = cached_vectors\n    \n    # Normalize image embeddings\n    img_norm = image_embeddings / (np.linalg.norm(image_embeddings, axis=1, keepdims=True) + 1e-8)\n    \n    # Compute cosine similarities\n    semantic_features = {}\n    for name, text_vector in concept_vectors.items():\n        similarity = img_norm @ text_vector\n        semantic_features[name] = similarity\n    \n    features_array = pd.DataFrame(semantic_features).values\n    print(f'  ✓ Generated {features_array.shape[1]} semantic features')\n    \n    return features_array, concept_vectors\n\nprint('✓ Semantic function defined')","metadata":{"_cell_guid":"34f51dd9-f520-4e7d-b5a8-87fec83d1ebc","_uuid":"135374f4-1836-47ad-9189-41324377ffc4","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:02.384281Z","iopub.status.busy":"2026-01-26T19:45:02.383952Z","iopub.status.idle":"2026-01-26T19:45:02.396147Z","shell.execute_reply":"2026-01-26T19:45:02.394727Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.022488,"end_time":"2026-01-26T19:45:02.398412","exception":false,"start_time":"2026-01-26T19:45:02.375924","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"3a670d76","cell_type":"code","source":"# FIXED: Create tabular features with aligned State encoding\ndef create_tabular_features_aligned(train_df, test_df):\n    \"\"\"\n    Create aligned tabular features ensuring train/test have same columns.\n    \"\"\"\n    train_features = []\n    test_features = []\n    feature_names = []\n    \n    # 1. NDVI feature\n    if 'Pre_GSHH_NDVI' in train_df.columns:\n        train_ndvi = train_df['Pre_GSHH_NDVI'].fillna(train_df['Pre_GSHH_NDVI'].median()).values\n        # For test, use train median to fill (no data leakage - just a constant)\n        test_ndvi = np.zeros(len(test_df))  # Test doesn't have NDVI\n        \n        train_features.append(train_ndvi.reshape(-1, 1))\n        test_features.append(test_ndvi.reshape(-1, 1))\n        feature_names.append('NDVI')\n    \n    # 2. Height feature\n    if 'Height_Ave_cm' in train_df.columns:\n        train_height = train_df['Height_Ave_cm'].fillna(train_df['Height_Ave_cm'].median()).values\n        test_height = np.zeros(len(test_df))  # Test doesn't have Height\n        \n        train_features.append(train_height.reshape(-1, 1))\n        test_features.append(test_height.reshape(-1, 1))\n        feature_names.append('Height')\n    \n    # 3. State encoding (ALIGNED between train and test)\n    if 'State' in train_df.columns and 'State' in test_df.columns:\n        # Get all unique states from BOTH train and test\n        all_states = sorted(set(train_df['State'].unique()) | set(test_df['State'].unique()))\n        \n        # Create aligned one-hot encoding\n        train_state_dummies = pd.get_dummies(\n            train_df['State'], \n            prefix='State'\n        ).reindex(columns=[f'State_{s}' for s in all_states], fill_value=0)\n        \n        test_state_dummies = pd.get_dummies(\n            test_df['State'] if 'State' in test_df.columns else pd.Series(['NSW'] * len(test_df)),\n            prefix='State'\n        ).reindex(columns=[f'State_{s}' for s in all_states], fill_value=0)\n        \n        train_features.append(train_state_dummies.values)\n        test_features.append(test_state_dummies.values)\n        feature_names.extend(train_state_dummies.columns.tolist())\n        \n        print(f'  States in train: {sorted(train_df[\"State\"].unique())}')\n        print(f'  States in test: {sorted(test_df.get(\"State\", pd.Series([\"NSW\"])).unique())}')\n        print(f'  Aligned state columns: {all_states}')\n    \n    # Combine all features\n    train_tabular = np.hstack(train_features) if train_features else np.zeros((len(train_df), 0))\n    test_tabular = np.hstack(test_features) if test_features else np.zeros((len(test_df), 0))\n    \n    print(f'\\n✓ Created {train_tabular.shape[1]} aligned tabular features')\n    print(f'  Train shape: {train_tabular.shape}')\n    print(f'  Test shape:  {test_tabular.shape}')\n    print(f'  Features: {feature_names}')\n    \n    return train_tabular, test_tabular\n\nprint('✓ Aligned tabular feature function defined')","metadata":{"execution":{"iopub.execute_input":"2026-01-26T19:45:02.414077Z","iopub.status.busy":"2026-01-26T19:45:02.413749Z","iopub.status.idle":"2026-01-26T19:45:02.425471Z","shell.execute_reply":"2026-01-26T19:45:02.424551Z"},"papermill":{"duration":0.022009,"end_time":"2026-01-26T19:45:02.427298","exception":false,"start_time":"2026-01-26T19:45:02.405289","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"84dc7ef7","cell_type":"code","source":"# Generate semantic features\nprint('\\n=== Generating Semantic Features ===')\n\n# Train: Generate semantic features and cache concept vectors\ntrain_semantic, concept_cache = generate_semantic_features(\n    train_siglip_emb, \n    cfg.siglip_path, \n    cfg.device,\n    cached_vectors=None\n)\n\n# Test: Reuse cached concept vectors (FASTER!)\ntest_semantic, _ = generate_semantic_features(\n    test_siglip_emb, \n    cfg.siglip_path, \n    cfg.device,\n    cached_vectors=concept_cache\n)\n\nprint(f'✓ Train semantic: {train_semantic.shape}')\nprint(f'✓ Test semantic: {test_semantic.shape}')\n\n# FIXED: Create ALIGNED tabular features\nprint('\\n=== Creating Aligned Tabular Features ===')\ntrain_tabular, test_tabular = create_tabular_features_aligned(train_wide, test_images)\n\n# Verify all dimensions match\nassert train_semantic.shape[0] == len(train_wide), \"Train semantic length mismatch!\"\nassert test_semantic.shape[0] == len(test_images), \"Test semantic length mismatch!\"\nassert train_tabular.shape[0] == len(train_wide), \"Train tabular length mismatch!\"\nassert test_tabular.shape[0] == len(test_images), \"Test tabular length mismatch!\"\nassert train_tabular.shape[1] == test_tabular.shape[1], \"❌ Tabular feature dimension MISMATCH!\"\n\nprint(f'\\n✓ Feature dimension check: TRAIN={train_tabular.shape[1]} == TEST={test_tabular.shape[1]}')\n\ngc.collect()\ntorch.cuda.empty_cache()\n","metadata":{"_cell_guid":"f95edbd7-4974-42d4-93c8-92a5c9b4a750","_uuid":"dcd1f78e-5538-48ed-9f07-9182486e0b5c","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:02.442970Z","iopub.status.busy":"2026-01-26T19:45:02.442168Z","iopub.status.idle":"2026-01-26T19:45:05.735418Z","shell.execute_reply":"2026-01-26T19:45:05.734615Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":3.303253,"end_time":"2026-01-26T19:45:05.737368","exception":false,"start_time":"2026-01-26T19:45:02.434115","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"76b5655f","cell_type":"code","source":"# FIXED: Feature engine with proper fit/transform for ALL feature types\nclass SupervisedEmbeddingEngine:\n    \"\"\"\n    Feature engineering with NO data leakage.\n    Handles: embeddings, semantic features, and tabular features separately.\n    \"\"\"\n    \n    def __init__(self, n_pca=0.80, n_pls=8, n_gmm=6, random_state=42):\n        # Embedding transformers\n        self.emb_scaler = StandardScaler()\n        self.pca = PCA(n_components=n_pca, random_state=random_state)\n        self.pls = PLSRegression(n_components=n_pls, scale=False)\n        self.gmm = GaussianMixture(n_components=n_gmm, covariance_type='diag', random_state=random_state)\n        self.pls_fitted_ = False\n        \n        # Tabular feature scaler (separate from embeddings)\n        self.tab_scaler = StandardScaler()\n        self.tab_fitted_ = False\n        \n        # Semantic features are cosine similarities - no scaling needed\n        # Just store for consistency checking\n        self.sem_dim_ = None\n        \n        self.random_state = random_state\n    \n    def fit(self, X_emb, y=None, X_semantic=None, X_tabular=None):\n        \"\"\"\n        Fit on training fold only.\n        \n        Args:\n            X_emb: Image embeddings (N, 1152)\n            y: Targets for supervised methods like PLS (N, 5)\n            X_semantic: Semantic similarities (N, 10) - already in [-1, 1]\n            X_tabular: Metadata features (N, 6) - NDVI, Height, State\n        \"\"\"\n        # Fit embedding transformers\n        X_emb_scaled = self.emb_scaler.fit_transform(X_emb)\n        self.pca.fit(X_emb_scaled)\n        self.gmm.fit(X_emb_scaled)\n        \n        if y is not None:\n            self.pls.fit(X_emb_scaled, y)\n            self.pls_fitted_ = True\n        \n        # Fit tabular scaler if provided\n        if X_tabular is not None and X_tabular.shape[1] > 0:\n            self.tab_scaler.fit(X_tabular)\n            self.tab_fitted_ = True\n        \n        # Store semantic dimension for validation\n        if X_semantic is not None:\n            self.sem_dim_ = X_semantic.shape[1]\n        \n        return self\n    \n    def transform(self, X_emb, X_semantic=None, X_tabular=None):\n        \"\"\"\n        Transform using fitted parameters.\n        \n        Returns: Combined feature matrix\n        \"\"\"\n        features = []\n        \n        # 1. Embedding-derived features\n        X_emb_scaled = self.emb_scaler.transform(X_emb)\n        features.append(self.pca.transform(X_emb_scaled))  # ~100-200 features\n        \n        if self.pls_fitted_:\n            features.append(self.pls.transform(X_emb_scaled))  # 8 features\n        \n        features.append(self.gmm.predict_proba(X_emb_scaled))  # 6 features\n        \n        # 2. Semantic features (NO scaling - already cosine similarities)\n        if X_semantic is not None:\n            assert X_semantic.shape[1] == self.sem_dim_, \"Semantic dimension mismatch!\"\n            features.append(X_semantic)  # 10 features\n        \n        # 3. Tabular features (scaled separately)\n        if X_tabular is not None and self.tab_fitted_ and X_tabular.shape[1] > 0:\n            features.append(self.tab_scaler.transform(X_tabular))  # ~6 features\n        \n        return np.hstack(features)\n    \n    def get_feature_names(self):\n        \"\"\"Return feature names for debugging.\"\"\"\n        names = []\n        names.extend([f'PCA_{i}' for i in range(self.pca.n_components_)])\n        if self.pls_fitted_:\n            names.extend([f'PLS_{i}' for i in range(self.pls.n_components)])\n        names.extend([f'GMM_{i}' for i in range(self.gmm.n_components)])\n        if self.sem_dim_:\n            names.extend([f'Semantic_{i}' for i in range(self.sem_dim_)])\n        if self.tab_fitted_:\n            names.extend(['Tabular' for _ in range(self.tab_scaler.n_features_in_)])\n        return names\n\nprint('✓ Fixed feature engine defined (with tabular support)')\n\n\n","metadata":{"_cell_guid":"1b4446cc-f0a5-405c-90f2-a69e30573eac","_uuid":"eacccb90-df67-4db6-bac8-7f03315bd39b","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:05.753053Z","iopub.status.busy":"2026-01-26T19:45:05.752685Z","iopub.status.idle":"2026-01-26T19:45:05.767010Z","shell.execute_reply":"2026-01-26T19:45:05.765871Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.024274,"end_time":"2026-01-26T19:45:05.768668","exception":false,"start_time":"2026-01-26T19:45:05.744394","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"083f94ab","cell_type":"code","source":"# Competition evaluation metric (from arxiv paper + Kaggle rules)\ndef competition_score(y_true, y_pred, target_names=None):\n    \"\"\"\n    Official competition metric: Log-transformed weighted R².\n    \n    From paper: \"Model performance is evaluated using weighted R² scores,\n    with log-stabilizing transformation: y_trans = log(1+y)\"\n    \n    Weights:\n        Dry_Green_g:  0.1\n        Dry_Dead_g:   0.1\n        Dry_Clover_g: 0.1\n        GDM_g:        0.2\n        Dry_Total_g:  0.5\n    \"\"\"\n    if target_names is None:\n        target_names = cfg.target_names\n    \n    # Get weights in correct order\n    weights = np.array([cfg.target_weights[t] for t in target_names])\n    \n    # Log transform (as per competition)\n    y_true_log = np.log1p(y_true)\n    y_pred_log = np.log1p(np.clip(y_pred, 0, None))  # Clip negatives before log\n    \n    # Calculate R² for each target\n    r2_scores = []\n    for i, target in enumerate(target_names):\n        r2 = r2_score(y_true_log[:, i], y_pred_log[:, i])\n        r2_scores.append(r2)\n    \n    r2_scores = np.array(r2_scores)\n    \n    # Weighted average\n    weighted_r2 = np.sum(weights * r2_scores)\n    \n    return weighted_r2, r2_scores\n\n\ndef competition_score_detailed(y_true, y_pred, target_names=None):\n    \"\"\"\n    Detailed breakdown of competition score.\n    \"\"\"\n    if target_names is None:\n        target_names = cfg.target_names\n    \n    overall, individual = competition_score(y_true, y_pred, target_names)\n    \n    print(f'\\n{\"=\"*60}')\n    print(f'COMPETITION METRIC (Log-transformed Weighted R²)')\n    print(f'{\"=\"*60}')\n    \n    weights = [cfg.target_weights[t] for t in target_names]\n    \n    for target, r2, weight in zip(target_names, individual, weights):\n        print(f'{target:15s}: R²={r2:7.4f} (weight={weight:.1f}) → {r2*weight:.4f}')\n    \n    print(f'{\"=\"*60}')\n    print(f'Overall Score: {overall:.4f}')\n    print(f'{\"=\"*60}\\n')\n    \n    return overall\n\n\nprint('✓ Competition metric functions defined')\nprint('✓ Uses log(1+y) transform + weighted R²')","metadata":{"execution":{"iopub.execute_input":"2026-01-26T19:45:05.784347Z","iopub.status.busy":"2026-01-26T19:45:05.783992Z","iopub.status.idle":"2026-01-26T19:45:05.794260Z","shell.execute_reply":"2026-01-26T19:45:05.793233Z"},"papermill":{"duration":0.020316,"end_time":"2026-01-26T19:45:05.796087","exception":false,"start_time":"2026-01-26T19:45:05.775771","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"0a4f7551","cell_type":"code","source":"# Pre-tuned hyperparameters\nBEST_PARAMS = {\n    'lightgbm': {\n        'n_estimators': 807,\n        'learning_rate': 0.014,\n        'num_leaves': 48,\n        'min_child_samples': 19,\n        'subsample': 0.745,\n        'colsample_bytree': 0.745,\n        'reg_alpha': 0.21,\n        'reg_lambda': 3.78,\n        'random_state': cfg.seed,\n        'verbose': -1\n    },\n    \n    'catboost': {\n        'iterations': 1900,\n        'learning_rate': 0.045,\n        'depth': 4,\n        'l2_leaf_reg': 0.56,\n        'random_strength': 0.045,\n        'bagging_temperature': 0.98,\n        'bootstrap_type': 'Bayesian',\n        'random_state': cfg.seed,\n        'verbose': 0,\n        'allow_writing_files': False,\n        'task_type': 'CPU'\n    },\n    \n    'xgboost': {\n        'n_estimators': 1354,\n        'learning_rate': 0.010,\n        'max_depth': 3,\n        'subsample': 0.60,\n        'colsample_bytree': 0.80,\n        'reg_alpha': 0.1,\n        'reg_lambda': 1.0,\n        'random_state': cfg.seed,\n        'verbosity': 0,\n        'tree_method': 'hist'\n    },\n    \n    'hist_gb': {\n        'max_iter': 300,\n        'learning_rate': 0.05,\n        'max_depth': None,\n        'l2_regularization': 0.44,\n        'random_state': cfg.seed\n    }\n}\n\nprint('✓ Hyperparameters loaded')","metadata":{"_cell_guid":"6506c6e9-bd38-4d9f-89f1-307e4bf57727","_uuid":"c4d88bb0-36be-4b1a-bc61-22958d9bd041","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:05.812021Z","iopub.status.busy":"2026-01-26T19:45:05.811386Z","iopub.status.idle":"2026-01-26T19:45:05.819051Z","shell.execute_reply":"2026-01-26T19:45:05.818161Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.017788,"end_time":"2026-01-26T19:45:05.820785","exception":false,"start_time":"2026-01-26T19:45:05.802997","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"0fe4da88","cell_type":"code","source":"# FIXED: CV training with ALL features and CORRECT metric\ndef cross_validate_and_predict_FIXED(\n    model_class,\n    model_params,\n    X_emb_train,      # Raw embeddings (N, 1152)\n    X_sem_train,      # Raw semantic features (N, 10)\n    X_tab_train,      # Raw tabular features (N, ~6)\n    y_train_full,     # Targets (N, 5)\n    X_emb_test,       # Test embeddings\n    X_sem_test,       # Test semantic\n    X_tab_test,       # Test tabular\n    folds,\n    target_names,\n    target_max_dict\n):\n    \"\"\"\n    Cross-validation with proper feature engineering per fold.\n    Uses COMPETITION METRIC (log-transformed weighted R²).\n    \"\"\"\n    n_splits = folds.max() + 1\n    n_targets = len(target_names)\n    \n    oof_preds = np.zeros((len(X_emb_train), n_targets))\n    test_preds = np.zeros((len(X_emb_test), n_targets))\n    \n    target_max_arr = np.array([target_max_dict[t] for t in target_names])\n    \n    fold_scores = []\n    \n    for fold in range(n_splits):\n        print(f'\\n  Fold {fold + 1}/{n_splits}')\n        print(f'  {\"-\"*50}')\n        \n        train_idx = folds != fold\n        val_idx = folds == fold\n        \n        # Split ALL feature types\n        X_emb_tr = X_emb_train[train_idx]\n        X_sem_tr = X_sem_train[train_idx]\n        X_tab_tr = X_tab_train[train_idx]\n        y_tr = y_train_full[train_idx] / target_max_arr  # Normalize\n        \n        X_emb_val = X_emb_train[val_idx]\n        X_sem_val = X_sem_train[val_idx]\n        X_tab_val = X_tab_train[val_idx]\n        y_val = y_train_full[val_idx]  # Keep original scale for evaluation\n        \n        # Fit feature engine on THIS FOLD'S training data only\n        fold_engine = SupervisedEmbeddingEngine(\n            n_pca=0.80,\n            n_pls=8,\n            n_gmm=6,\n            random_state=cfg.seed\n        )\n        fold_engine.fit(\n            X_emb_tr, \n            y=y_tr, \n            X_semantic=X_sem_tr,\n            X_tabular=X_tab_tr  # ← ADDED\n        )\n        \n        # Transform using fold-specific engine\n        X_tr_eng = fold_engine.transform(X_emb_tr, X_semantic=X_sem_tr, X_tabular=X_tab_tr)\n        X_val_eng = fold_engine.transform(X_emb_val, X_semantic=X_sem_val, X_tabular=X_tab_val)\n        X_test_eng = fold_engine.transform(X_emb_test, X_semantic=X_sem_test, X_tabular=X_tab_test)\n        \n        fold_test_preds = np.zeros((len(X_emb_test), n_targets))\n        \n        # Train separate model for each target\n        for t_idx, target_name in enumerate(target_names):\n            model = model_class(**model_params)\n            model.fit(X_tr_eng, y_tr[:, t_idx])\n            \n            # Predict and denormalize\n            oof_preds[val_idx, t_idx] = model.predict(X_val_eng) * target_max_arr[t_idx]\n            fold_test_preds[:, t_idx] = model.predict(X_test_eng) * target_max_arr[t_idx]\n        \n        test_preds += fold_test_preds\n        \n        # CRITICAL: Use competition metric!\n        fold_score, fold_r2_individual = competition_score(\n            y_val, \n            oof_preds[val_idx],\n            target_names\n        )\n        fold_scores.append(fold_score)\n        \n        # Print per-target breakdown\n        print(f'  Per-target R² (log-transformed):')\n        for t_name, r2_val in zip(target_names, fold_r2_individual):\n            print(f'    {t_name:15s}: {r2_val:.4f}')\n        print(f'  {\"─\"*50}')\n        print(f'  Fold Score (weighted): {fold_score:.4f}')\n    \n    # Average test predictions across folds\n    test_preds /= n_splits\n    \n    # Final OOF score with competition metric\n    oof_score, oof_r2_individual = competition_score(\n        y_train_full, \n        oof_preds,\n        target_names\n    )\n    \n    print(f'\\n  {\"=\"*50}')\n    print(f'  OOF Competition Score: {oof_score:.4f}')\n    print(f'  Mean Fold Score: {np.mean(fold_scores):.4f} ± {np.std(fold_scores):.4f}')\n    print(f'  {\"=\"*50}')\n    \n    return oof_preds, test_preds, oof_score\n\nprint('✓ FIXED CV function defined')\nprint('✓ Now uses tabular features')\nprint('✓ Now uses competition metric')\nprint('✓ Removed clover zeroing bug')","metadata":{"_cell_guid":"8b0ecd86-66c6-455d-b754-c4e74d871415","_uuid":"c52481bd-508f-41b6-9cc8-7ed21de71c35","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:05.837572Z","iopub.status.busy":"2026-01-26T19:45:05.836738Z","iopub.status.idle":"2026-01-26T19:45:05.850661Z","shell.execute_reply":"2026-01-26T19:45:05.849763Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.024683,"end_time":"2026-01-26T19:45:05.852477","exception":false,"start_time":"2026-01-26T19:45:05.827794","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"dc44c1a4","cell_type":"code","source":"# Prepare for training\ny_train = train_wide[cfg.target_names].values\nfolds = train_wide['fold'].values\n\n# Validation checks\nassert not np.isnan(y_train).any(), \"❌ Found NaN values in targets!\"\nassert (folds >= 0).all(), \"❌ Found invalid fold assignments!\"\nassert len(y_train) == len(folds), \"❌ Length mismatch between targets and folds!\"\n\nprint('\\n' + '='*70)\nprint('TRAINING MODELS WITH FIXED CV')\nprint('='*70)\n\n# Show target statistics\nprint('\\nTarget Statistics:')\nprint(f'{\"Target\":<15s} {\"Mean\":>10s} {\"Std\":>10s} {\"Min\":>10s} {\"Max\":>10s}')\nprint('-'*70)\nfor i, target in enumerate(cfg.target_names):\n    print(f'{target:<15s} {y_train[:, i].mean():>10.2f} {y_train[:, i].std():>10.2f} '\n          f'{y_train[:, i].min():>10.2f} {y_train[:, i].max():>10.2f}')\nprint('-'*70)\n\n# Check for zeros in Clover (important for model training)\nclover_idx = cfg.target_names.index('Dry_Clover_g')\nn_zero_clover = (y_train[:, clover_idx] == 0).sum()\nprint(f'\\nClover notes:')\nprint(f'  Zero clover samples: {n_zero_clover}/{len(y_train)} ({100*n_zero_clover/len(y_train):.1f}%)')\nprint(f'  Non-zero clover mean: {y_train[y_train[:, clover_idx] > 0, clover_idx].mean():.2f}g')\n\n# Feature dimensions\nprint(f'\\nFeature dimensions:')\nprint(f'  Embeddings:  {train_siglip_emb.shape}')\nprint(f'  Semantic:    {train_semantic.shape}')\nprint(f'  Tabular:     {train_tabular.shape}')\n\nprint('\\n' + '='*70)\n\n# Storage for results\nall_oof_preds = {}\nall_test_preds = {}\nall_scores = {}\n\nprint('✓ Ready to train models')","metadata":{"_cell_guid":"f0645c88-49be-469d-848b-40aa8e0d120a","_uuid":"ae0ff462-1ba9-4abb-9ebd-6ee04c75417a","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:05.868681Z","iopub.status.busy":"2026-01-26T19:45:05.868052Z","iopub.status.idle":"2026-01-26T19:45:05.879995Z","shell.execute_reply":"2026-01-26T19:45:05.879103Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.022486,"end_time":"2026-01-26T19:45:05.882216","exception":false,"start_time":"2026-01-26T19:45:05.859730","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"25bf6b2f","cell_type":"code","source":"# LightGBM\nprint('\\n--- LightGBM ---')\noof_lgbm, test_lgbm, score_lgbm = cross_validate_and_predict_FIXED(\n    LGBMRegressor,\n    BEST_PARAMS['lightgbm'],\n    train_siglip_emb,\n    train_semantic,\n    train_tabular,      # ← ADDED (was missing!)\n    y_train,\n    test_siglip_emb,\n    test_semantic,\n    test_tabular,       # ← ADDED (was missing!)\n    folds,\n    cfg.target_names,\n    cfg.target_max\n)\n\nall_oof_preds['lgbm'] = oof_lgbm\nall_test_preds['lgbm'] = test_lgbm\nall_scores['lgbm'] = score_lgbm\n\nprint(f'\\n✓ LightGBM OOF Competition Score: {score_lgbm:.4f}')","metadata":{"_cell_guid":"65f29750-afdc-4755-bded-96326283b263","_uuid":"32d9e9d5-f956-48a5-8ffc-9fa69b4a32f0","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:05.898918Z","iopub.status.busy":"2026-01-26T19:45:05.898108Z","iopub.status.idle":"2026-01-26T19:45:15.741550Z","shell.execute_reply":"2026-01-26T19:45:15.740549Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":9.8545,"end_time":"2026-01-26T19:45:15.743933","exception":false,"start_time":"2026-01-26T19:45:05.889433","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f0e5c784","cell_type":"code","source":"# CatBoost\nprint('\\n--- CatBoost ---')\noof_cat, test_cat, score_cat = cross_validate_and_predict_FIXED(\n    CatBoostRegressor,\n    BEST_PARAMS['catboost'],\n    train_siglip_emb,\n    train_semantic,\n    train_tabular,      # ← ADDED (was missing!)\n    y_train,\n    test_siglip_emb,\n    test_semantic,\n    test_tabular,       # ← ADDED (was missing!)\n    folds,\n    cfg.target_names,\n    cfg.target_max\n)\n\nall_oof_preds['cat'] = oof_cat\nall_test_preds['cat'] = test_cat\nall_scores['cat'] = score_cat\n\nprint(f'\\n✓ CatBoost OOF Competition Score: {score_cat:.4f}')","metadata":{"_cell_guid":"0c4d0c8c-f09f-4699-9acb-9cc075677ad3","_uuid":"1867b4f2-4623-430d-a0f1-e46ac6908d7d","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:45:15.761907Z","iopub.status.busy":"2026-01-26T19:45:15.761541Z","iopub.status.idle":"2026-01-26T19:46:56.717502Z","shell.execute_reply":"2026-01-26T19:46:56.716344Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":100.967779,"end_time":"2026-01-26T19:46:56.719828","exception":false,"start_time":"2026-01-26T19:45:15.752049","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8f6e59d3","cell_type":"code","source":"# XGBoost\nprint('\\n--- XGBoost ---')\noof_xgb, test_xgb, score_xgb = cross_validate_and_predict_FIXED(\n    XGBRegressor,\n    BEST_PARAMS['xgboost'],\n    train_siglip_emb,\n    train_semantic,\n    train_tabular,      # ← ADDED\n    y_train,\n    test_siglip_emb,\n    test_semantic,\n    test_tabular,       # ← ADDED\n    folds,\n    cfg.target_names,\n    cfg.target_max\n)\n\nall_oof_preds['xgb'] = oof_xgb\nall_test_preds['xgb'] = test_xgb\nall_scores['xgb'] = score_xgb\n\nprint(f'\\n✓ XGBoost OOF Competition Score: {score_xgb:.4f}')","metadata":{"_cell_guid":"3249c193-f666-47dc-b2fe-74c2f6fed649","_uuid":"11b990d4-4e97-41ba-be10-97ae36a45156","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:46:56.737269Z","iopub.status.busy":"2026-01-26T19:46:56.736887Z","iopub.status.idle":"2026-01-26T19:47:31.522473Z","shell.execute_reply":"2026-01-26T19:47:31.521442Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":34.805287,"end_time":"2026-01-26T19:47:31.532909","exception":false,"start_time":"2026-01-26T19:46:56.727622","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"c1ec8c41","cell_type":"code","source":"# HistGradientBoosting\nprint('\\n--- HistGradientBoosting ---')\noof_hist, test_hist, score_hist = cross_validate_and_predict_FIXED(\n    HistGradientBoostingRegressor,\n    BEST_PARAMS['hist_gb'],\n    train_siglip_emb,\n    train_semantic,\n    train_tabular,      # ← ADDED\n    y_train,\n    test_siglip_emb,\n    test_semantic,\n    test_tabular,       # ← ADDED\n    folds,\n    cfg.target_names,\n    cfg.target_max\n)\n\nall_oof_preds['hist'] = oof_hist\nall_test_preds['hist'] = test_hist\nall_scores['hist'] = score_hist\n\nprint(f'\\n✓ HistGradientBoosting OOF Competition Score: {score_hist:.4f}')","metadata":{"_cell_guid":"50cd278e-eb33-45fd-afd6-4762dd52f1da","_uuid":"3637997e-5d12-4a88-bf02-1fd16c30887a","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:47:31.550481Z","iopub.status.busy":"2026-01-26T19:47:31.550170Z","iopub.status.idle":"2026-01-26T19:47:46.462954Z","shell.execute_reply":"2026-01-26T19:47:46.462009Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":14.924482,"end_time":"2026-01-26T19:47:46.465374","exception":false,"start_time":"2026-01-26T19:47:31.540892","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"86fd503a","cell_type":"code","source":"# Summary\nprint('\\n' + '='*70)\nprint('INDIVIDUAL MODEL SCORES')\nprint('='*70)\nfor model_name, score in all_scores.items():\n    print(f'{model_name.upper():15s}: {score:.4f}')\nprint('='*70)","metadata":{"_cell_guid":"dd6f44d4-3167-4559-81f7-227b4e062334","_uuid":"eb2a7369-7682-493d-b853-d8c40609e71a","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:47:46.486885Z","iopub.status.busy":"2026-01-26T19:47:46.486155Z","iopub.status.idle":"2026-01-26T19:47:46.491717Z","shell.execute_reply":"2026-01-26T19:47:46.490833Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.017479,"end_time":"2026-01-26T19:47:46.493625","exception":false,"start_time":"2026-01-26T19:47:46.476146","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"b93fe318","cell_type":"code","source":"# Ensemble\nprint('\\n' + '='*70)\nprint('CREATING ENSEMBLE')\nprint('='*70)\n\n# Simple average ensemble\ntest_preds_ensemble = (\n    all_test_preds['lgbm'] +\n    all_test_preds['cat'] +\n    all_test_preds['xgb'] +\n    all_test_preds['hist']\n) / 4.0\n\noof_ensemble = (\n    all_oof_preds['lgbm'] +\n    all_oof_preds['cat'] +\n    all_oof_preds['xgb'] +\n    all_oof_preds['hist']\n) / 4.0\n\n# FIXED: Use competition metric\nensemble_score = competition_score_detailed(y_train, oof_ensemble, cfg.target_names)\n\nprint(f'✓ Ensemble OOF Competition Score: {ensemble_score:.4f}')\n\n# Compare to best individual model\nbest_individual = max(all_scores.values())\nimprovement = ensemble_score - best_individual\nprint(f'\\nEnsemble vs Best Individual:')\nprint(f'  Best individual: {best_individual:.4f}')\nprint(f'  Ensemble:        {ensemble_score:.4f}')\nprint(f'  Improvement:     {improvement:+.4f}')","metadata":{"_cell_guid":"ee27c419-760c-446c-a6c0-ae14f7bef8c2","_uuid":"a410352d-1bdc-4af4-97f2-ca8591f232f1","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:47:46.512288Z","iopub.status.busy":"2026-01-26T19:47:46.511553Z","iopub.status.idle":"2026-01-26T19:47:46.523870Z","shell.execute_reply":"2026-01-26T19:47:46.522829Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.023676,"end_time":"2026-01-26T19:47:46.525780","exception":false,"start_time":"2026-01-26T19:47:46.502104","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"e4153e97","cell_type":"code","source":"# Post-processing (MINIMAL)\ndef post_process_biomass(predictions, enforce_mass_balance=False):\n    \"\"\"\n    Light post-processing: clip negatives, optionally enforce mass balance.\n    \n    Relationships:\n        GDM_g = Dry_Green_g + Dry_Clover_g\n        Dry_Total_g = Dry_Green_g + Dry_Dead_g + Dry_Clover_g\n    \"\"\"\n    preds = predictions.copy()\n    \n    # 1. Clip negative predictions (can't have negative biomass)\n    preds = np.clip(preds, 0, None)\n    \n    if enforce_mass_balance:\n        print('⚠️  Enforcing mass balance constraints...')\n        \n        idx_clover = cfg.target_names.index('Dry_Clover_g')\n        idx_green = cfg.target_names.index('Dry_Green_g')\n        idx_dead = cfg.target_names.index('Dry_Dead_g')\n        idx_gdm = cfg.target_names.index('GDM_g')\n        idx_total = cfg.target_names.index('Dry_Total_g')\n        \n        # CORRECT relationships (if enforcing)\n        # GDM = Green + Clover\n        preds[:, idx_gdm] = preds[:, idx_green] + preds[:, idx_clover]\n        \n        # Total = Green + Dead + Clover\n        preds[:, idx_total] = preds[:, idx_green] + preds[:, idx_dead] + preds[:, idx_clover]\n        \n        # Clip again after adjustment\n        preds = np.clip(preds, 0, None)\n    \n    return preds\n\n# RECOMMENDED: Don't enforce mass balance (models already learned relationships)\ntest_preds_final = post_process_biomass(test_preds_ensemble, enforce_mass_balance=False)\n\nprint('✓ Post-processing applied')\nprint('  - Clipped negatives')\nprint('  - Mass balance: NOT enforced (models predict independently)')","metadata":{"_cell_guid":"d143a154-11e5-43be-96d1-2345ad9d4681","_uuid":"94d1154d-8936-4acc-85e2-2d19fb7d6e5f","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:47:46.545389Z","iopub.status.busy":"2026-01-26T19:47:46.544934Z","iopub.status.idle":"2026-01-26T19:47:46.553308Z","shell.execute_reply":"2026-01-26T19:47:46.552388Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.020355,"end_time":"2026-01-26T19:47:46.555287","exception":false,"start_time":"2026-01-26T19:47:46.534932","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"48b981c3","cell_type":"code","source":"# Submission - FINAL\nprint('\\n' + '='*70)\nprint('CREATING SUBMISSION')\nprint('='*70)\n\n# Start with test_df structure (has correct sample_ids and target_names)\nsubmission = test_df[['sample_id', 'target_name']].copy()\n\n# Create mapping: image_id -> row index in test_images\nimage_id_to_idx = {img_id: idx for idx, img_id in enumerate(test_images['image_id'])}\n\n# Fill in predictions\npredictions = []\nfor _, row in submission.iterrows():\n    # Extract image_id from sample_id\n    img_id = row['sample_id'].split('_')[0]\n    target_name = row['target_name']\n    \n    # Get indices\n    img_idx = image_id_to_idx[img_id]\n    t_idx = cfg.target_names.index(target_name)\n    \n    # Get prediction\n    pred_value = test_preds_final[img_idx, t_idx]\n    predictions.append(pred_value)\n\nsubmission['target'] = predictions\n\n# Validation checks\nprint(f'✓ Submission shape: {submission.shape}')\nprint(f'✓ Expected: ({len(test_images) * 5}, 3)')\nprint(f'✓ Unique images: {submission[\"sample_id\"].apply(lambda x: x.split(\"_\")[0]).nunique()}')\nprint(f'✓ Predictions per image: {len(submission) // len(test_images)}')\n\n# Check for issues\nassert len(submission) == len(test_images) * 5, \"❌ Wrong number of predictions!\"\nassert not submission['target'].isna().any(), \"❌ NaN predictions found!\"\nassert (submission['target'] >= 0).all(), \"❌ Negative predictions found!\"\n\nprint('\\nPrediction Statistics:')\nfor target in cfg.target_names:\n    target_preds = submission[submission['target_name'] == target]['target']\n    print(f'  {target:15s}: mean={target_preds.mean():7.2f}, '\n          f'std={target_preds.std():7.2f}, '\n          f'min={target_preds.min():7.2f}, '\n          f'max={target_preds.max():7.2f}')\n\n# Save submission\nsubmission[['sample_id', 'target']].to_csv('submission.csv', index=False)\n\nprint('\\n✓ Submission saved to submission.csv')\nprint(f'✓ First few rows:')\nprint(submission[['sample_id', 'target']].head(10))\n\n# Final sanity check - no duplicates\nduplicates = submission['sample_id'].duplicated().sum()\nif duplicates > 0:\n    print(f'\\n❌ WARNING: {duplicates} duplicate sample_ids found!')\nelse:\n    print(f'\\n✓ No duplicate sample_ids - submission is valid!')","metadata":{"_cell_guid":"852cd705-f56d-4eef-8784-73c317f0acd7","_uuid":"20a89a3f-6bfa-44a1-9b70-826841045d1f","collapsed":false,"execution":{"iopub.execute_input":"2026-01-26T19:47:46.574525Z","iopub.status.busy":"2026-01-26T19:47:46.573903Z","iopub.status.idle":"2026-01-26T19:47:46.603746Z","shell.execute_reply":"2026-01-26T19:47:46.602405Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.041739,"end_time":"2026-01-26T19:47:46.605685","exception":false,"start_time":"2026-01-26T19:47:46.563946","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}