Please use this identifier to cite or link to this item: https://er.chdtu.edu.ua/handle/ChSTU/6733
Title: Прогнозування врожайності сільськогосподарських культур із використанням методів глибинного навчання
Authors: Карапетян, Анаіт Радіківна
Козак, Назар Ігорович
Keywords: МУЛЬТИМОДАЛЬНІ ДАНІ, СІЛЬСЬКЕ ГОСПОДАРСТВО, КОНТРАСТИВНЕ НАВЧАННЯ, ПРИЧИННА ЧУТЛИВІСТЬ, ПРОГНОЗУВАННЯ NDVI, СЦЕНАРНИЙ АНАЛІЗ;MULTIMODAL DATA, AGRICULTURE, CONTRASTIVE LEARNING, CAUSAL SENSITIVITY, NDVI FORECASTING, SCENARIO ANALYSIS
Issue Date: 18-Dec-2025
Abstract: Кваліфікаційна робота магістра присвячена прогнозуванню врожайності сільськогосподарських культур на основі аналізу мультимодальних даних із застосуванням методів глибинного навчання. Об’єктом дослідження є процеси обробки супутникових знімків, метеорологічних та ґрунтових даних, а предметом — алгоритми контрастивного самонавчання та механізми причинної чутливості для формування спільного простору представлень і прогнозування агропоказників. Метою роботи є розроблення та експериментальне дослідження методу MTCR-CS для прогнозування індексу NDVI та оцінювання впливу кліматичних факторів на стан посівів. У роботі використано методи попередньої обробки та аугментації даних, алгоритми контрастивного навчання і глибокі нейронні мережі з оптимізацією гіперпараметрів та сценарним аналізом. Отримані результати мають практичне значення для підвищення точності прогнозування врожайності й ризиків, підтримки прийняття рішень у сільському господарстві, а також для створення веб- та GIS-систем моніторингу і подальшого поширення підходу на інші задачі з мультимодальними даними.
The master’s thesis is devoted to crop yield prediction based on the analysis of multimodal data using deep learning methods. The object of the research is the processing of satellite imagery, meteorological, and soil data, while the subject comprises contrastive self-learning algorithms and causal sensitivity mechanisms for building a unified representation space and forecasting agricultural indicators. The aim of the study is to develop and experimentally evaluate the MTCR-CS method for predicting the NDVI index and assessing the impact of climate factors on crop conditions. The work employs data preprocessing and augmentation techniques, contrastive learning algorithms, and deep neural networks with hyperparameter optimization and scenario-based perturbation analysis. The obtained results are of practical importance for improving the accuracy of yield and risk forecasting, supporting decision-making in agriculture, and developing web- and GIS-based monitoring systems, as well as for extending the proposed approach to other multimodal data domains.
URI: https://er.chdtu.edu.ua/handle/ChSTU/6733
Appears in Collections:112 Статистика (Аналіз даних (DATA SCIENCE) та комп'ютерна статистика)

Files in This Item:
File Description SizeFormat 
КОЗАК Н.І. Кваліфікаційна робота магістра.pdf
  Restricted Access
3.33 MBAdobe PDFView/Open Request a copy


Items in DSpace are protected by copyright, with all rights reserved, unless otherwise indicated.

Extracted text
 
 
 
2 
 
РЕФЕРАТ 
Магістерська кваліфікаційна робота складається зі вступу, чотирьох розділів, 
висновків, списку використаних джерел та додатків. Обсяг роботи становить 63 
сторінки основного тексту, містить 14 рисунків, 5 таблиці, 23 найменувань у списку 
використаних джерел і 2 додатків. 
Об’єктом дослідження є процеси аналізу мультимодальних 
сільськогосподарських даних з використанням методів машинного навчання.  
Предметом дослідження є алгоритми контрастивного самонавчання та 
механізми причинної чутливості для формування єдиного простору представлень і 
прогнозування агропоказників.  
Метою роботи є розробка та експериментальне дослідження методу MTCR-CS 
для аналізу супутникових знімків, метеоданих і ґрунтових характеристик з метою 
прогнозування NDVI та оцінки впливу кліматичних змін.  
Методи розробки базувалися на основних положеннях теорії машинного 
навчання та глибоких нейронних мереж. Попередня обробка даних проводилася з 
використанням методів агрегації, нормалізації та аугментації. Для підвищення якості 
представлень застосовувалися алгоритми контрастивного навчання, а для 
прогнозування — нейронні мережі. Вдосконалення моделі здійснювалося через підбір 
гіперпараметрів та сценарний аналіз пертурбацій. 
Практичне значення роботи спрямоване на підвищення точності прогнозування 
ризиків, оптимізацію управління посівами для фермерів та агрохолдингів, створення 
веб- і GIS-застосунків для інтерактивного моніторингу, а також розширення методу на 
інші домени з мультимодальними даними для проактивного прийняття рішень. 
Ключові слова: мультимодальні дані, сільське господарство, контрастивне 
навчання, причинна чутливість, прогнозування ndvi, сценарний аналіз. 
  
 
3 
 
ABSTRACT 
The Master's qualification paper consists of an introduction, four chapters, 
conclusions, a list of references, and appendices.The volume of the work is 63 pages of main 
text; it contains 14 figures, 5 tables, 23 items in the list of references, and 2 appendices.  
The object of the research is the processes of analyzing multimodal agricultural data 
using machine learning methods.  
The subject of the research is contrastive self-supervised learning algorithms and 
causal sensitivity mechanisms for forming a unified representation space and forecasting 
agricultural indicators.  
The aim of the work is the development and experimental study of the MTCR-CS 
method for analyzing satellite imagery, meteorological data and soil characteristics in order 
to forecast NDVI and assess the impact of climate changes.  
The development methods were based on the fundamental principles of machine 
learning theory and deep neural networks. Data preprocessing was performed using 
aggregation, normalization and augmentation techniques. To improve the quality of 
representations, contrastive learning algorithms were applied, and neural networks were 
used for forecasting. Model improvement was carried out through hyperparameter tuning 
and scenario-based perturbation analysis.  
The practical significance of the work is aimed at increasing the accuracy of risk 
forecasting, optimizing crop management for farmers and agricultural holdings, creating 
web- and GIS-applications for interactive monitoring, as well as extending the method to 
other domains with multimodal data to support proactive decision-making. 
Keywords: multimodal data, agriculture, contrastive learning, causal sensitivity, ndvi 
forecasting, scenario analysis. 
  
 
4 
 
Зміст 
 
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І ТЕРМІНІВ .......... 6 
ВСТУП ................................................................................................................................... 8 
1 ЗАГАЛЬНА ХАРАКТЕРИСТИКА РОБОТИ ................................................................ 10 
1.1 Актуальність теми ............................................................................................... 10 
1.2 Мета та завдання дослідження .......................................................................... 12 
1.3 Аналіз сучасних підходів та огляд літератури ................................................. 14 
1.3.1 Статистичні та ML-моделі на часових рядах ........................................... 15 
1.3.2 Глибокі моделі на супутникових знімках ................................................. 16 
1.3.3 Мультимодальні підходи та проблема інтеграції ..................................... 17 
1.3.4 Сценарний аналіз у сільському господарстві ........................................... 19 
1.4. Запропоноване рішення ..................................................................................... 21 
1.5 Висновки до Розділу 1 ........................................................................................ 22 
2 ТЕОРЕТИЧНІ ОСНОВИ ТА РОЗРОБКА МЕТОДУ MTCR-CS ................................ 25 
2.1 Математична постановка задачі ........................................................................ 25 
2.2 Формалізація методу MTCR .............................................................................. 26 
2.3 Формалізація прогнозуючої функції ................................................................. 28 
2.4 Формалізація сценарного аналізу ...................................................................... 29 
2.5 Висновки до розділу 2 ........................................................................................ 31 
3 ПРАКТИЧНА РЕАЛІЗАЦІЯ МЕТОДУ MTCR-CS ...................................................... 34 
3.1 Підготовка даних ................................................................................................. 34 
3.1.1 Підготовка картографічних даних .............................................................. 35 
 
5 
 
3.1.2 Підготовка погодних даних ......................................................................... 40 
3.1.3 Фінальна структура даних ........................................................................... 43 
3.2 Реалізація методу MTCR .................................................................................... 44 
3.3 Вибір архітектури та навчання прогнозуючої функції .................................... 49 
3.4 Сценарний аналіз ................................................................................................ 52 
3.5 Об’єднане рішення .............................................................................................. 55 
3.6 Висновки до розділу 3 ........................................................................................ 56 
4 АНАЛІЗ РЕЗУЛЬТАТІВ ДОСЛІДЖЕННЯ ТА ПЕРСПЕКТИВИ ЇХ 
ВИКОРИСТАННЯ ....................................................................................................... 58 
4.1 Аналіз отриманих результатів ........................................................................ 58 
4.2 Практична цінність ......................................................................................... 60 
4.3 Обмеження запропонованого підходу ........................................................... 63 
4.4 Перспективи подальших досліджень ............................................................ 65 
4.5 Висновки до 4 розділу .................................................................................... 66 
ВИСНОВКИ ........................................................................................................................ 68 
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ .......................................................................... 70 
ДОДАТОК А.Специфікація ............................................................................................... 73 
ДОДАТОК Б. Текст програми ........................................................................................... 76 
 
  
 
6 
 
ПЕРЕЛІК УМОВНИХ ПОЗНАЧЕНЬ, СИМВОЛІВ, СКОРОЧЕНЬ І ТЕРМІНІВ 
ARIMA –  Autoregressive Integrated Moving Average. модель для прогнозування 
часових рядів 
BYOL –  Bootstrap Your Own Latent, метод самонавчання в глибокому навчанні 
CNN –  Convolutional Neural Network, згорткова нейронна мережа 
CS  –  Causal Sensitivity, механізм причинної чутливості 
FAO –  Food and Agriculture Organization, Організація Об'єднаних Націй з питань 
продовольства та сільського господарства;  
InfoNCE – Information Noise-Contrastive Estimation, функція втрат для 
контрастивного навчання; 
IoU – Intersection over Union, метрика для оцінки сегментації; 
LAI – Leaf Area Index. індекс площі листя;  
LSTM – Long Short-Term Memory,  рекурентна мережа для часових рядів 
MAE – Mean Absolute Error, середня абсолютна похибка;  
MLP – Multi-Layer Perceptron, багатошаровий перцептрон;  
MoCo – Momentum Contrast, метод контрастивного навчання з динамічним 
словником; 
MODIS – Moderate Resolution Imaging Spectroradiometer, супутниковий датчик 
NASA;  
MSE – Mean Squared Error, середньоквадратична похибка;  
MTCR – Multimodal Temporal Contrastive Representation, мультимодальне часове 
контрастивне представлення;  
MTCR-CS – Multimodal Temporal Contrastive Representation with Causal Sensitivity, 
розширення MTCR з причинною чутливістю; 
NDVI – Normalized Difference Vegetation Index, нормалізований різницевий індекс 
вегетації;  
 
7 
 
PLS - Partial Least Squares, часткові найменші квадрати; регресійний метод для 
часових рядів. 
RMSE – Root Mean Squared Error, корінь середньоквадратичної похибки;  
RNN – Recurrent Neural Network, рекурентна нейронна мережа;  
SAR – Synthetic Aperture Radar, синтетична апертурна радіолокація; радарні дані для 
супутникових знімків. 
SimCLR – Simple Framework for Contrastive Learning of Visual Representations, 
простий фреймворк контрастивного навчання; 
  
 
8 
 
ВСТУП 
Сучасний розвиток сільського господарства відбувається в умовах стрімкого 
зростання обсягів даних, що надходять із різноманітних джерел — супутникового 
дистанційного зондування, метеорологічних станцій, наземних сенсорів та 
інформаційних систем агровиробників. Ці дані мають принципово різну природу: 
просторово-розподілені растрові зображення, часові ряди високої частоти, табличні та 
векторні геодані. Така гетерогенність відкриває нові можливості для точного 
землеробства, проте водночас створює серйозні виклики для їх ефективної обробки та 
інтеграції. 
Традиційні статистичні методи та моделі машинного навчання з учителем, які 
домінували в агроаналітиці до недавнього часу, виявляються недостатніми для роботи 
з мультимодальними даними великого обсягу. Вони вимагають значної кількості 
ручної розмітки, погано узагальнюються при зміні регіону чи культури та не 
дозволяють оперативно оцінювати гіпотетичні сценарії («що буде, якщо температура 
зросте на 3 °C» або «опади зменшаться на 30 %»). У той же час саме такі сценарні 
оцінки є ключовими для переходу від реактивного до проактивного управління 
кліматичними та виробничими ризиками. 
Останні досягнення в галузі глибокого навчання, зокрема поява самонавчальних 
і контрастивних підходів, дали потужний інструментарій для формування 
інваріантних представлень без великої кількості міток. Проте більшість сучасних 
контрастивних архітектур (SimCLR, MoCo, BYOL та їх похідні) розроблені для 
однорідних даних — зображень або тексту — і не враховують специфіку агро-задач: 
сильну сезонну циклічність, просторово-часову гетерогенність, необхідність 
інтерпретованих висновків на рівні окремого поля. 
Таким чином, існує науково-практична прогалина між наявними 
універсальними мультимодальними моделями та реальними потребами аграрного 
 
9 
 
сектору України, який працює в умовах високої кліматичної нестабільності та 
потребує інструментів оперативного прогнозування й оцінки ризиків. 
Метою кваліфікаційної роботи є розробка, реалізація та експериментальне 
дослідження мультимодального часового контрастивного підходу з механізмом 
причинної чутливості (MTCR-CS), який забезпечує формування єдиного простору 
представлень гетерогенних сільськогосподарських даних, прогнозування стану 
посівів та інтерпретований сценарний аналіз кліматичних ризиків без перенавчання 
моделі. 
  
 
10 
 
1 ЗАГАЛЬНА ХАРАКТЕРИСТИКА РОБОТИ 
1.1 Актуальність теми 
Сільське господарство є стратегічною галуззю економіки України, що формує 
значну частку валового внутрішнього продукту та експорту країни. За даними 
Міністерства аграрної політики та продовольства України, в 2023 році аграрний сектор 
становив близько 12 % ВВП, а обсяг експорту сільськогосподарської продукції 
перевищив 45 млрд доларів США [1]. Водночас сектор стикається з низкою системних 
викликів — змінами клімату, дефіцитом водних і земельних ресурсів, а також 
потребою в підвищенні продуктивності через точні технології землеробства. 
Зростання світового населення й відповідне підвищення попиту на продовольство (за 
прогнозами FAO, глобальний попит може зрости на ~70 % до 2050 року) роблять 
Україну важливим гравцем у міжнародних ланцюгах постачання продовольства [2]. У 
цьому контексті ефективний аналіз аграрних даних набуває стратегічного значення 
для підвищення стійкості й продуктивності виробництва. 
Сучасна аграрна аналітика спирається на великі й гетерогенні масиви даних, у 
тому числі супутникові знімки (оптичні та радарні), наземні сенсорні вимірювання, 
метеорологічні спостереження та статистичні табличні дані. Розвиток програмних і 
апаратних платформ дистанційного зондування (наприклад, місії Copernicus — 
Sentinel-1/2 та супутникові місії сімейства Landsat) забезпечив щоденне надходження 
об’ємних потоків даних різної природи: оптичних зображень, радарних (SAR) даних і 
часових рядів спектральних індексів. Ілюстративно, великі публічні набори даних, 
такі як SEN12MS та BigEarthNet, демонструють масштаб доступних мультимодальних 
наборів (див. [3], [4]), що відкриває широкі можливості для тренування і тестування 
моделей машинного навчання в задачах моніторингу земельного покриву та оцінки 
стану сільськогосподарських угідь. 
 
11 
 
Разом із тим збільшення обсягів і різноманітності даних породжує низку 
методологічних проблем при їх обробці й інтеграції. Нижче виділено три ключові 
класи проблем, характерних для мультимодального агродатасету: 
1. Проблема представлення. Супутникові зображення характеризуються великою 
просторовою роздільністю й високою вимірністю (тисячі—мільйони пікселів), тоді як 
табличні дані (метео-, ґрунтові показники) зазвичай мають низьку розмірність і іншу 
семантику ознак. Це ускладнює формування компромісного представлення, яке 
одночасно зберігає інформацію про просторові закономірності та числові метео-
/ґрунтові характеристики. 
2. Проблема вирівнювання за часом і простором. Часові ряди індексів вегетації 
(наприклад, NDVI, LAI) можуть мати пропуски або нерівномірну розрядність через 
хмарність і періодичність спостережень, тоді як метеорологічні дані надходять з 
регулярною кроковістю. Узгодження часових і просторових масштабів при цьому 
часто призводить до втрати короткочасних екстремумів або до неявної агрегації 
важливих сигналів (наприклад, нічні заморозки), що зменшує чутливість моделей до 
критичних подій. 
3. Проблема інтеграції (data fusion). Застосування ранніх підходів злиття даних 
(early fusion) може викликати «прокляття розмірності» через різке збільшення числа 
ознак, тоді як пізні підходи (late fusion) іноді втрачають міжмодальні кореляції, 
важливі для точного прогнозування. Середні підходи (mid-level fusion), які 
передбачають попередню екстракцію і зменшення розмірності, показують найкращий 
компроміс у низці досліджень [5], проте питання узагальненості таких рішень ще 
відкрито. 
Практичні дослідження підтверджують, що комбінування мультиспектральних 
супутникових даних із наземними сенсорами може суттєво покращувати результати 
прогнозування врожайності та виявлення агро-стресів. Наприклад, при інтеграції 
даних Sentinel-2 і вимірів вологості ґрунту з IoT-сенсорів показники пояснювальної 
здатності моделей (R²) були вищими у випадку застосування середньорівневого злиття 
 
12 
 
(типово R² ≈ 0,66–0,88 у наведених експериментах) [6]. Водночас без адекватної 
попередньої обробки і вирівнювання різнорідних даних можливе зниження точності 
діагностики і класифікації до 22–34 % в залежності від задачі та метрики [7]. Такі 
результати демонструють як потенціал мультимодальних підходів, так і важливість 
продуманих архітектур представлення даних. 
Незважаючи на наявні методи злиття й представлення, загальною проблемою 
залишається брак єдиного, узгодженого простору представлень, який одночасно 
враховував би: 
- часову динаміку системи (послідовність подій і їх причинно-наслідкові 
взаємозв’язки); 
- просторову гетерогенність полів і мікрокліматичних умов; 
- можливість проведення сценарного аналізу типу «що буде, якщо зміняться опади 
або температура». 
Відсутність такого інтегрованого простору ускладнює перехід від реактивного 
до проактивного управління ризиками в агросекторі. Розробка методів, які здатні 
одночасно долати хмарність, просторову гетерогенність та забезпечувати сценарний 
аналіз на рівні окремого поля, є нагальною потребою для підвищення стійкості та 
прибутковості сільського господарства України в умовах кліматичної невизначеності. 
1.2 Мета та завдання дослідження 
Мета даної кваліфікаційної роботи - розробити, реалізувати та 
експериментально дослідити мультимодальний часовий контрастивний підхід з 
механізмом причинної чутливості (MTCR-CS) для аналізу сільськогосподарських 
даних, що забезпечує: 
1. формування єдиного простору представлень мультимодальних даних 
(супутникові знімки, метеодані, ґрунтові характеристики, врожайність); 
 
13 
 
2. прогнозування NDVI та врожайності на наступний місяць на рівні 
окремого поля; 
3. оцінку впливу гіпотетичних змін кліматичних факторів («а що, якби?») без 
перенавчання моделі. 
Дослідження проводиться на прикладі 66 громад Черкаської області (2021–2023 
рр.), що включає ≈198 000 полів. 
Для досягнення поставленої мети необхідно вирішити такі завдання: 
1. Сформувати мультимодальний набір даних: 
- зібрати геометрії полів, місячні медіанні знімки Sentinel-2 (L2A), щоденні 
метеодані по центроїдам громад, дані про тип ґрунту та врожайність; 
- виконати агрегацію погодних даних до місячного рівня (сума опадів, 
середня/максимальна/мінімільна температура, вологість); 
- провести попередню обробку (видалення хмар, нормалізація, аугментація). 
2. Розробити архітектуру MTCR (Multimodal Temporal Contrastive Representation): 
- реалізувати окремі енкодери для кожної модальності (CNN для зображень, 
MLP для табличних даних, RNN/Transformer для часових рядів); 
- впровадити контрастивну функцію втрат (InfoNCE) з тимчасовими та 
просторовими аугментаціями; 
- сформувати єдиний векторний простір представлень розмірністю 256–512. 
3. Створити прогнозуючу голову (Forecasting Head): 
- реалізувати рекурентну або трансформерну архітектуру на основі MTCR-
представлень; 
- навчити модель передбачати NDVI та врожайність на наступний місяць з 
урахуванням припущеної погоди; 
- оцінити точність за метриками MSE, MAE, R². 
4. Розробити механізм причинної чутливості (Causal Sensitivity, CS): 
- реалізувати пертурбаційний підхід: зміна одного або кількох кліматичних 
параметрів (Δопади, Δтемпература) → перерахунок прогнозу; 
 
14 
 
- впровадити градієнтний аналіз чутливості (∂NDVI/∂w); 
- забезпечити інтерпретованість результатів у вигляді sensitivity maps та 
числових оцінок впливу. 
5. Провести експериментальне дослідження: 
- розбити дані на тренувальну (2021–2022), валідаційну та тестову (2023) 
вибірки; 
- порівняти MTCR-CS з базовими моделями: LSTM, U-Net, моделі без CS; 
- оцінити стійкість до хмарності, просторової гетерогенності та шумів. 
6. Оцінити практичну цінність підходу: 
- продемонструвати приклади сценарного аналізу для типових ризиків (посуха, 
заморозки); 
- розрахувати потенційне зниження економічних втрат (грн/га) при 
впровадженні; 
- запропонувати рекомендації для інтеграції в системи точного землеробства. 
Сформульована мета та перелік завдань чітко відповідають виявленим у 
підпункті 1.1 прогалинам сучасних підходів до аналізу мультимодальних агроданих. 
Виконання поставлених завдань дозволить створити науково обґрунтований, 
масштабований та практично корисний інструмент для проактивного управління 
ризиками в українському сільському господарстві. 
1.3 Аналіз сучасних підходів та огляд літератури 
Виявлені у підпункті 1.1 методологічні прогалини — зокрема проблеми 
представлення, вирівнювання та інтеграції даних — безпосередньо впливають на 
якість прогнозування NDVI та врожайності, що є основними цілями, визначеними у 
підпункті 1.2. Аналіз сучасних досліджень у сфері аграрної аналітики свідчить про те, 
що більшість існуючих підходів тяжіють до трьох основних напрямів: 
 
15 
 
1. статистичні та класичні методи машинного навчання, які працюють із часовими 
рядами; 
2. моделі глибокого навчання, орієнтовані переважно на супутникові знімки; 
3. мультимодальні підходи, що поєднують дані різної природи. 
1.3.1 Статистичні та ML-моделі на часових рядах 
Найпоширенішим підходом до прогнозування NDVI та врожайності 
залишаються моделі, засновані на часових рядах метеорологічних та вегетаційних 
показників. Традиційно для цих задач використовуються регресійні моделі (лінійна, 
логістична, PLS-регресія), а також ARIMA-подібні методи [8]. Такі підходи 
демонструють прийнятну точність на невеликих наборах із однорідними часовими 
рядами, проте вони мають обмежену здатність до узагальнення при збільшенні 
кількості просторових об’єктів або при роботі з даними різної природи 
(супутниковими, погодними, ґрунтовими). 
У дослідженні Bolton et al. (2020) на базі даних MODIS для штату Айова, США 
модель ARIMA досягла RMSE = 0,072 для прогнозу NDVI, використовуючи лише 
метеорологічні змінні (температура, опади) та історичні значення NDVI [9]. Проте 
така модель ігнорувала просторові особливості: аналізувалося лише одне середнє 
значення NDVI на піксель 250 м² та не враховувала текстурну інформацію з 
супутникових зображень, що призвело до систематичних помилок у мікрокліматичних 
зонах і недооцінки впливу локальних погодних умов. 
Більш складні моделі машинного навчання, такі як Random Forest та XGBoost, 
дозволяють інтегрувати додаткові табличні ознаки такі як ґрунтові характеристики, 
сівозміну та агротехнічні параметри. У роботі Cai et al. (2019) [10] XGBoost із раннім 
об’єднанням ознак - конкатенацією 12 метеорологічних, 8 ґрунтових показників та 
NDVI - досяг R² = 0,74 для прогнозу врожайності рису на 1200 полях Китаю. Однак 
просте об’єднання понад 20 ознак без належної нормалізації призвело до домінування 
 
16 
 
метеоданих. Їхній внесок у важливість ознак перевищив 80 %, тоді як внесок 
супутникових зображень Sentinel-2, агрегованих як середнє значення по полю, склав 
менше ніж 3 %. Автори відзначають прояви перевчання на шумі та втрату просторових 
патернів, зокрема, неможливість виявлення локальних посух усередині полів. 
Дослідження останніх років [9], [10] узгоджено свідчать, що класичні та 
ансамблеві методи машинного навчання недостатньо враховують нелінійні залежності 
між погодними факторами, ґрунтовими властивостями та спектральними показниками 
вегетації. Крім того, такі моделі не мають механізмів узгодження часової динаміки між 
модальностями (наприклад, між щоденними метеоспостереженнями та місячними 
супутниковими знімками), що обмежує їх застосування в мультимодальних сценаріях 
прогнозування. 
1.3.2 Глибокі моделі на супутникових знімках 
З появою відкритих супутникових даних Sentinel-1/2, Landsat-8/9 та великих 
наборів даних, таких як SEN12MS і BigEarthNet, різко зросла кількість досліджень, 
присвячених застосуванню глибоких нейронних мереж для аналізу стану посівів, 
класифікації культур і прогнозування вегетаційних показників [11]–[13]. 
Найпоширенішими архітектурами залишаються Convolutional Neural Networks 
(CNN) та U-Net, які дозволяють моделі враховувати просторові закономірності на рівні 
пікселів. Наприклад, у роботі Weiss et al. (2020) модель U-Net, навчена на стеках 
Sentinel-2 (12 спектральних каналів × 6 місяців), досягла IoU = 0,91 при сегментації 
полів, проте прогнозування врожайності здійснювалось лише через регресію на 
останньому знімку, що дало R² = 0,68 [12]. Це демонструє головне обмеження таких 
підходів - ігнорування часової динаміки, тобто відсутність урахування змін NDVI 
упродовж сезону. 
З розвитком часових архітектур (LSTM, GRU, ConvLSTM, Vision Transformer) 
з’явилися спроби поєднати супутникові серії у просторово-часові моделі. У 
 
17 
 
дослідженні Wang et al. (2021) було запропоновано підхід early fusion: метеодані 
(температура, опади, вологість, вітер) додавалися як додаткові канали до спектральних 
зображень Sentinel-2, формуючи 16-канальний тензор. Модель ConvLSTM досягла R² 
= 0,71 для прогнозу NDVI [13]. Однак через різну природу даних — піксельні значення 
проти скалярних показників — CNN фактично “розмивала” метеосигнал по всьому 
зображенню. У результаті спостерігалося завищення NDVI у посушливих ділянках (≈ 
+0,11) та заниження у вологих (≈ −0,09). Самі автори визнають, що «метеодані 
втрачають локальність, стаючи шумом». 
Таким чином, хоча CNN, U-Net і трансформерні архітектури забезпечують 
високу точність у межах однієї модальності (оптичні зображення), вони не здатні 
ефективно інтегрувати гетерогенні дані (метео, ґрунт, супутникові) та узгодити часову 
динаміку між джерелами. Це обмежує їх застосування для повноцінного 
мультимодального аналізу, що є підґрунтям для пошуку нових методів — таких як 
запропонований у цій роботі Multimodal Temporal Contrastive Representation (MTCR). 
1.3.3 Мультимодальні підходи та проблема інтеграції 
Мультимодальні моделі у сільськогосподарській аналітиці намагаються 
інтегрувати дані різної природи - супутникові зображення, метеорологічні часові ряди, 
ґрунтові та статистичні показники. Метою таких підходів є побудова єдиного 
представлення стану агроекосистеми, яке одночасно враховує просторові, часові та 
екологічні фактори. Проте ключовою проблемою залишається спосіб злиття даних, від 
якого безпосередньо залежить якість узагальнення та інтерпретованість моделі. 
Основні підходи до інтеграції даних поділяють на три типи — early fusion, mid-level 
fusion та late fusion [14]. 
Early fusion передбачає пряме об’єднання ознак із різних джерел перед подачею 
у спільну модель. Це дозволяє зберегти всю інформацію, але часто призводить до 
 
18 
 
«прокляття розмірності» та втрати статистичної стійкості. У роботі Khaki et al. (2020) 
для прогнозування врожайності кукурудзи CNN та MLP об’єднують: 
• 12-канальні знімки Sentinel-2 
• 7 метеопоказників за 30 днів 
• 5 ґрунтових характеристик 
Загалом формується 227 ознак, що подаються в багатошаровий перцептрон. 
Модель досягає R² = 0,76, але демонструє надмірну залежність від метеопараметрів, 
тоді як внесок супутникових зображень становить менше 5 %. Додавання SAR-даних, 
тобто зображень землі отримані за допомогою радіохвиль - лише погіршило результат 
(R² = 0,71) через посилення шуму та мультиколінеарність [14]. Таким чином, раннє 
злиття не лише не підвищує точність, а й ускладнює навчання через некоректне 
масштабування та різну природу ознак. 
Late fusion, навпаки, передбачає побудову окремих моделей для кожної 
модальності з подальшим поєднанням їхніх прогнозів. Такий підхід частково знижує 
ризик різкого збільшення кількості ознак, але втрачає міжмодальні взаємозв’язки. 
Наприклад, You et al. (2021) застосували LSTM для часових рядів NDVI та Random 
Forest для метеоданих, об’єднуючи їхні прогнози через усереднення. Отримано R² = 
0,79, але при хмарності більше 60-ти % модель LSTM повертала пропуски (NaN), тоді 
як RF не враховував просторові патерни (локальні посухи, хвороби рослин). У 
результаті середня похибка досягала ±1,2 т/га для кукурудзи на полях з частковим 
водним дефіцитом [15]. 
Найбільш збалансованим вважається mid-level fusion, де кожна модальність 
проходить окреме кодування у векторні представлення (латентні простори), які потім 
агрегуються. У роботі Zhang et al. (2023) використано CNN-енкодер для Sentinel-2 та 
Transformer для погодних рядів. Після формування двох 512-вимірних векторів 
проводиться конкатенація та подальше прогнозування врожайності. Модель показала 
R² = 0,84, що значно краще за early та late fusion. Однак її обмеження полягає у 
відсутності самонавчального або контрастивного етапу - модель використовує підхід 
 
19 
 
навчання з учителем, що вимагає понад 10 000 міток врожайності для стабільного 
результату. [16]. 
Отже, попри помітний прогрес у напрямі мультимодального моделювання, 
більшість існуючих рішень залишаються кореляційними, тобто вони фіксують 
співзалежності між  ознаками, але не здатні виявити причинно-наслідкові зв’язки між 
погодними умовами та змінами вегетаційних індексів. Саме ця обмеженість 
підкреслює необхідність подальшого розвитку підходів, які поєднують 
мультимодальність із причинним аналізом, що розглядається у наступному підпункті.  
1.3.4 Сценарний аналіз у сільському господарстві 
Сценарний або причинний аналіз («а що, якби?») є важливою складовою 
сучасних систем підтримки прийняття рішень у сільському господарстві, оскільки 
дозволяє оцінювати наслідки гіпотетичних змін погодних умов, стресових факторів чи 
управлінських дій без проведення реальних експериментів. У класичних підходах 
сценарний аналіз базується на статистичних та економетричних моделях, однак їх 
застосування до мультимодальних супутникових даних є нетривіальним через високу 
просторова-гетерогенність, нелінійні взаємодії та складні часові залежності. Нижче 
розглянуто основні групи методів, що формують сучасний стан цієї галузі. 
Одним з найбільш широко застосовуваних інструментів є статистичні моделі 
втручань, зокрема метод CausalImpact, який використовує Bayesian structural time-
series (BSTS) для оцінки відхилення спостережуваного ряду від контрфактичного 
прогнозу. У класичній роботі Brodersen et al. (2015) метод використано для аналізу 
впливу рекламних кампаній, проте він також адаптований до аграрних задач, зокрема 
для оцінки впливу посухи на базі історичних рядів NDVI та метеоданих [17]. 
Аналогічно, дослідження Van Klompenburg et al. (2021) демонструє, що зменшення 
кількості опадів на 30 % у Нідерландах призводить до зниження NDVI на 0,15 ± 0,04 
з імовірністю 92 % [18]. Втім, такі моделі працюють виключно з одновимірними 
 
20 
 
часовими рядами, ігнорують просторову структуру полів і не дозволяють інтегрувати 
супутникові зображення або інші типи даних. Крім того, вони потребують чіткого 
моменту «втручання», який у реальних кліматичних сценаріях зазвичай відсутній. 
Другим напрямом є графові методи причинності, що реалізують фреймворк 
Pearl через побудову спрямованих ациклічних графів (DAG). Бібліотека DoWhy надає 
інструменти для ідентифікації причинно-наслідкових ефектів та оцінки average 
treatment effect (ATE) [19]. Наприклад, роботи Sharma et al. (2021) та Li et al. (2022) 
демонструють можливість застосування DAG для оцінки впливу іригації або змін 
опадів на врожайність через посередницькі змінні, як-от NDVI [19, 20]. Проте 
побудова коректного DAG у сільському господарстві є складним завданням, що 
вимагає значних експертних знань щодо взаємодії погодних, ґрунтових та біофізичних 
процесів. Крім того, такі методи не призначені для аналізу тензорних даних 
супутникових зображень і не масштабуються на великі набори полів. 
Гібридні ML-причинні підходи, такі як Double/Debiased Machine Learning, 
спрямовані на оцінку причинних ефектів за використання високопродуктивних ML-
моделей як «інструментів» для контролю конфундерів. Роботи Chernozhukov et al. 
(2018) та Schluter et al. (2023) демонструють, що ці методи здатні оцінювати 
гетерогенні ефекти температурних аномалій на врожайність у різних регіонах [21, 22]. 
Однак дані підходи працюють переважно з табличними даними та вимагають великої 
кількості міток, що робить їх малопридатними для мультимодальних супутникових 
наборів, де кількість полів значно перевищує кількість доступних виміряних врожаїв. 
Окрему групу становлять пертурбаційні методи у глибокому навчанні, де 
сценарії моделюються шляхом цілеспрямованих змін у вхідних даних нейронної 
мережі. Наприклад, у роботі Russakovsky et al. (2022) досліджується вплив штучного 
підвищення температури на прогнозовану врожайність шляхом зміни відповідних 
каналів у вході CNN [23]. Такий підхід дозволяє отримати швидкі оцінки чутливості 
моделі, проте не враховує складних зворотних зв’язків між модальностями (зміна 
 
21 
 
температури впливає на NDVI, а NDVI — на інші змінні), що робить отримані сценарії 
потенційно нереалістичними. 
Узагальнюючи попереднє, сучасні підходи до причинного аналізу в аграрному 
секторі значною мірою орієнтовані на табличні дані та одновимірні часові ряди, тоді 
як мультимодальні супутникові дані залишаються практично невикористаними у 
цьому контексті. Відсутність моделей, здатних формувати спільний латентний простір 
для зображень, метеорологічних рядів та ґрунтових характеристик, суттєво ускладнює 
реалізацію узгодж еного та інтерпретованого сценарного аналізу на рівні окремих 
полів. 
Подальші дослідження зосереджуються на побудові адаптивних 
мультимодальних архітектур, що враховують часову динаміку, причинність та 
інтерпретованість. Одним із таких рішень є розроблений у цій роботі підхід MTCR-
CS (Multimodal Temporal Contrastive Representation with Causal Sensitivity), який 
поєднує контрастивне навчання для формування узгодженого представлення даних та 
механізм причинної чутливості для проведення сценарного аналізу без необхідності у 
графах причинності чи великій кількості міток. 
1.4. Запропоноване рішення 
Виявлені у підпунктах 1.1–1.3 обмеження сучасних методів опрацювання 
аграрних даних – зокрема неефективне об’єднання різнорідних джерел інформації, 
відсутність узгодженого представлення, яке б враховувало просторово-часову 
динаміку, а також неможливість виконання інтерпретованого сценарного аналізу без 
значної кількості міток або експертно сформованих причинних структур – засвідчують 
необхідність створення нового підходу. 
Запропоноване рішення ґрунтується на формуванні узагальненого 
мультимодального представлення, що інтегрує супутникові дані, метеорологічні 
часові ряди та інформацію про ґрунти у єдиний латентний простір. Центральна ідея 
 
22 
 
полягає у використанні самонавчальних принципів для побудови стійких ознак, які 
відображають внутрішні залежності між модальностями без потреби в розширеному 
маркуванні даних. 
Передбачається, що отримане представлення стане основою для двох ключових 
завдань: 
1. прогнозування просторово-часових аграрних показників на рівні окремих 
полів з урахуванням сезонності, пропусків даних та гетерогенності території; 
2. моделювання гіпотетичних сценаріїв шляхом цілеспрямованої зміни 
вхідних умов та аналізу їх впливу на вихідні показники у межах сформованого 
латентного простору. 
Таким чином, пропонований підхід поєднує узгодження різних типів даних, 
часову динаміку та механізм чутливості до змін у середовищі, що дає змогу 
досліджувати не лише кореляційні, а й потенційно причинні взаємозв’язки у 
сільськогосподарських системах. У межах цього підходу не робиться жорстких 
припущень щодо структури моделі або конкретних архітектур, що забезпечує 
гнучкість та адаптацію до різних практичних умов. 
Наукова новизна полягає у сформуванні єдиного мультимодального простору 
представлень, придатного як для передбачення динамічних процесів, так і для аналізу 
наслідків зміни зовнішніх факторів. Практична значущість полягає у можливості 
застосування методу для проактивної оцінки ризиків, планування агротехнічних 
заходів та підтримки прийняття рішень на рівні господарств і регіонів. 
1.5 Висновки до Розділу 1 
У першому розділі обґрунтовано актуальність дослідження, зумовлену 
потребою підвищення ефективності та стійкості українського сільського господарства 
в умовах кліматичних змін і зростання обсягу даних, доступних із супутникових, 
метеорологічних та ґрунтових джерел. Показано, що сучасні системи моніторингу та 
 
23 
 
прогнозування стикаються з низкою методологічних обмежень, пов’язаних із 
формуванням представлень для різнорідних даних, їх просторово-часовим 
узгодженням та інтеграцією, а також із недостатньою здатністю наявних моделей до 
інтерпретації причинно-наслідкових зв’язків і проведення сценарного аналізу. 
Сформульовано мету кваліфікаційної роботи, яка полягає у розробленні та 
дослідженні мультимодального підходу, здатного формувати узгоджений латентний 
простір аграрних даних, забезпечувати прогнозування динамічних показників на рівні 
окремих полів і дозволяти оцінювати наслідки гіпотетичних змін у кліматичних 
умовах. Для досягнення мети визначено комплекс взаємопов’язаних завдань, що 
охоплюють побудову мультимодального набору даних, розроблення моделі 
узагальненого представлення, реалізацію прогнозуючого та чутливісного 
компонентів, а також проведення експериментального дослідження на реальних даних 
Черкаської області. 
Проведений огляд літератури дозволив систематизувати сучасні підходи до 
обробки аграрних даних — від класичних моделей часових рядів до глибоких 
архітектур і мультимодальних стратегій злиття. Виявлено, що, попри окремі успіхи, 
наявні підходи здебільшого залишаються кореляційними за своєю природою, не 
забезпечують узгодженого простору представлень і не підтримують повноцінного 
сценарного аналізу без значного обсягу міток або експертних причинних структур. Це 
підтверджує обґрунтованість необхідності у створенні нового методологічного 
рішення. 
У підпункті 1.4 запропоновано концептуальний підхід, спрямований на 
подолання виявлених обмежень шляхом поєднання самонавчального формування 
мультимодального представлення, часової динаміки та механізму чутливості до змін 
у вхідних умовах. Такий підхід створює передумови для побудови гнучкої та 
інтерпретованої моделі, придатної як для прогнозування, так і для аналізу потенційних 
сценаріїв у сільському господарстві.  
 
24 
 
Сформовані висновки узагальнюють результати теоретичного аналізу, 
окреслюють науково-практичну проблему та закладають методологічну основу для 
подальшої розробки та реалізації запропонованого підходу у розділі 2. 
  
 
25 
 
2 ТЕОРЕТИЧНІ ОСНОВИ ТА РОЗРОБКА МЕТОДУ MTCR-CS 
2.1 Математична постановка задачі 
Нехай множина досліджуваних аграрних ділянок позначається індексом ��, а 
часовий індекс — �� ∈ {1, … , ��}. Для кожної ділянки і моменту часу наявні дві 
модальності спостережень: 
������
1. супутникові знімки: �� ����×����×��
��,�� ∈ �� , 
2. погодні часові ряди: �������� ��
��,��,�� ∈ �� ,  �� ∈ {1, … , ����}, 
де ����, ���� — просторові розміри знімка, що залежать від форми ділянки ��; �� — 
число спектральних каналів; �� — кількість погодних змінних; ���� — кількість 
погодних спостережень у часовому проміжку �� (наприклад, число днів у періоді або 
фактична кількість вимірів).  
Цільова змінна визначається як медіанне значення NDVI за інтервал часу �� по 
площі ділянки ��. Формально: 
����,�� = median{����������,��(ℎ, ��) ∣ ℎ = 1, … , ���� ,  �� = 1, … , ����}       (2.1) 
Постановка задачі: Потрібно побудувати два взаємопов’язані відображення: 
1. відображення мультимодальних вхідних даних у компактний латентний 
простір 
img
��: (�� , {��tab ��
��,�� ��,��,��} ��
��=1) ↦ �� ��
��,�� ∈ �� ,               (2.2) 
та 
2. прогнозуючу функцію, яка на основі латентного вектора ����,�� і 
передбачуваних (або гіпотетичних) погодних умов наступного інтервалу ����,��+1  дає 
оцінку медіанного NDVI у моменті �� + 1: 
?̂?��,��+1 = ℎ(����,�� ,  ����,��+1).          (2.3) 
 
26 
 
Також враховуючи те, що просторові розміри знімків ���� × ���� та довжини 
погодних рядів ���� є змінними, відображення �� (див. (2.2)) має задовольняти наступні 
властивості: 
1. Інваріантність до розміру зображення ���� × ����; 
2. узгоджувати часову нерівномірність послідовності погодних вимірів 
довжини ����; 
3. зберігати сезонну структуру процесу, тобто різницю між фазами росту, 
характерними для різних пор року; 
Крім побудови відображення �� та прогнозуючої функції ℎ, задача включає 
формалізовану оцінку впливу змін погодних умов на прогнозоване значення NDVI. 
Для цього визначається функція причинної чутливості CS, яка для довільного вектору 
змін Δ�� оцінює різницю прогнозу: 
����(Δ��) = ℎ(����,�� , ����,��+1 + Δ��) − ℎ(����,�� , ����,��+1).         (2.4) 
Таким чином, повна постановка задачі полягає у пошуку трьох 
взаємопов’язаних функцій: 
1. відображення мультимодальних вхідних даних у латентний простір ��(див. 
(2.2)); 
2. прогнозуючої функції ℎ (див.(2.3)), що передбачає медіанне NDVI у 
наступний часовий проміжок; 
3. функції причинної чутливості CS (див.(2.4)), яка формалізує сценарний 
аналіз, оцінюючи вплив гіпотетичних змін погодних факторів на прогноз NDVI. 
2.2 Формалізація методу MTCR 
Нехай для кожної ділянки �� та часового проміжку �� маємо вхідні модальності 
������
�� ∈ ������×�� ��
����
��,��  та {�������� ��
��,��,��}��=1, ��������
��,��,�� ∈ ����. Оскільки зображення мають різну 
розмірність ���� × ����, а також, оскільки, погодні дані можуть містити різну кількість 
спостережень ���� – вводяться два оператори уніфікації: 
 
27 
 
img img
?̃? = �� (�� ),   �� :  ⨆ ����×��×�� → ������×����×��
��,�� img ��,�� img ,   (2.5) 
��,��
?̃?tab ��
= �� ({��tab } �� ),   �� : ⨆ ����×�� ����×��
��,�� tab ��,��,�� ��=1 tab → �� ,   (2.6) 
��≥1
 
де ��img  приводить будь-яке вхідне зображення до фіксованого розміру ��0 × ��0, 
а ��tab  гарантує представлення погодної послідовності у фіксованому часовому 
форматі ��0, тобто ��tab  не залежить від початкового ����. 
Після уніфікації вводяться два математичні відображення: 
�� ������ img
���� ��img
img: �� → �� ,   ����,�� ≔ ��img(?̃?��,�� ),   (2.7) 
�� ��0�� ��tab tab
tab: �� → �� ,   ����,�� ≔ ��tab(?̃?��,�� ),   (2.8) 
де ����,��  і ����,��  - вектори фіксованої розмірності, що відображають інформацію 
відповідно зі зображення та погодної послідовності.  
Для отримання фінального узгодженого представлення визначається агрегуюча 
(проекційна) функція ��Θ, яка забезпечує баланс між внесками двох модальностей: 
����,�� = ��θ(����,�� , ����,��) = Proj (α(����,�� , ����,��) ⋅ ?̃?��,�� + (1 − α(����,�� , ����,��)) ⋅ ?̃?��,��) , (2.9) 
де ?̃?��,�� = Normu(����,��), ?̃?��,�� = Normv(����,��) - попередньо нормалізовані вектори, 
α(⋅,⋅): ����img × ����tab → [0,1] — скалярна вага, яку можна задати як сигмоїду від лінійної 
форми α = σ(��⊤[��; ��] + ��), а Proj(⋅) — проекція у простір розмірності �� (лінійна або 
нелінійна). Така конструкція гарантує, що внесок кожної модальності масштабується 
адаптивно, отже жодна з модальностей не «поглине» іншої за рахунок абсолютних 
масштабів ознак. 
Вимоги, що були визначені у постановці задачі, формалізовано таким чином: 
- Інваріантність до розміру зображення: для будь-яких вхідних зображень 
��, ��′ таких, що ��img(��) = ��img(��′), виконується 
��img (��img(��)) = �� (�� (��′
img img ))   (2.10) 
 
28 
 
- узгодження часової нерівномірності: для будь-яких двох послідовностей 
погодних спостережень ��, ��′ із різними довжинами, але що описують той самий 
часовий сигнал після ресемплінгу (тобто ��tab(��) = �� ′
tab(�� ), виконується 
��tab(��tab(��)) = �� ′
tab(��tab(�� ))  (2.11) 
- Нехай ��1 < ��2 < ��3 < ��4 - чотири моменти часу, що належать різним 
вегетаційним сезонам і задовольняють умову ��2 − ��1 = ��4 − ��3 = ����. Тоді 
відображення �� має забезпечувати асимптотичну рівність приростів у латентному 
просторі: 
‖����,�� − ����,�� ‖ − ‖����,�� − ����,�� ‖ → 0  (2.12) 
�� �� 2 �� �� 2
 
Таким чином, MTCR задається як композиція операторів 
�� =  ��θ ∘ (��img ∘ ��img,  ��tab ∘ ��tab),   (2.13) 
2.3 Формалізація прогнозуючої функції 
Для задачі прогнозування під прогнозом будемо розуміти оцінку майбутнього 
значення цільової змінної ����,��+1 для ділянки �� на наступному часовому інтервалі �� + 1, 
що базується на відомій інформації про стан системи в момент �� та заданих 
(передбачуваних або сценарних) погодних параметрах для моменту �� + 1. 
Нехай у результаті застосування операторів уніфікації та кодувальних 
відображень (див. (2.13)) для ділянки �� та часового інтервалу �� побудовано вектор 
об’єднаного представлення даних про погоду та даних зображення ����,��. Тоді для задачі 
прогнозування вводимо погоду на часовий інтервал �� + 1: v tab
��,��+1 ∈
����������(передбачувана/сценарна). Прогнозуюча функція ℎ визначається як 
відображення з прямого добутку цих просторів у множину можливих значень цільової 
змінної 
ℎ ∶ ���� × ���������� → ��,   (2.14) 
 
29 
 
Фактичний прогноз для ділянки �� у моменті �� + 1 тоді задається рівністю: 
?̂?��,��+1 = ℎ(����,�� , �� tab
��,��+1).   (2.15) 
Оскільки два вектори модальностей є різними за своєю природою, відображення 
ℎ використовує їх незалежно, без нормалізації до спільного масштабу та без проекції 
в однакову метричну структуру. Функцію ℎ можна представити у вигляді композиції 
двох внутрішніх операторів: 
ℎ(�� ������
��,�� , ����,��+1) = Φ (��������(����,��),  ��������(��������
��,��+1))   (2.16),  
де 
- �� : ���������� → ����������
������  — оператор, який інтерпретує просторово-
вегетаційну інформацію; 
- ��������: ���������� → ���������� — оператор, що відображає погодні параметри у 
внутрішній узагальнений опис; 
- Φ: ���������� × ���������� → �� — оператор, який поєднує два типи інформації у 
єдине числове передбачення. 
Таким чином функція прогнозування складається з двох незалежних шляхів 
обробки модальностей та єдиного шляху інтеграції. При цьому структура операторів 
�������� , �������� , Φ не фіксується — їх конкретний вигляд буде визначено у подальшому 
розділі. В межах цього підпункту важливим є те, що прогнозуюча функція оперує 
модальностями як окремими джерелами інформації, що дозволяє уникати некоректної 
геометричної інтеграції векторів різної природи та забезпечує гнучкість подальшої 
побудови моделі. 
2.4 Формалізація сценарного аналізу 
У задачі прогнозування стану рослинності важливим є не лише отримання 
оцінки значення ?̂?��,��+1 для фіксованих майбутніх погодних параметрів, але і здатність 
оцінювати чутливість прогнозу до альтернативних сценаріїв розвитку погодних умов. 
 
30 
 
Такий підхід дозволяє проводити аналіз ризиків, оцінювати вплив екстремальних 
подій та виконувати порівняння можливих варіантів агрометеорологічної динаміки. 
Нехай для моменту часу �� + 1 задано не єдине значення вектора погодних 
характеристик ��������
��,��+1 , а кінцева множина альтернативних сценаріїв: 
(1) (2) (��)
����,��+1 = {����,��+1,  ����,��+1,  … ,  ����,��+1} ,   (2.17) 
(��)
де кожен елемент ����,��+1 ∈ ���������� є можливим варіантом прогнозної або сценарної 
погоди для ділянки ��. 
Сценарії можуть задавати: 
• різні моделі прогнозу погоди; 
• різні гіпотетичні траєкторії (екстримальний випадок, холодний/теплий 
сценарій тощо); 
• результати стохастичних симуляцій. 
Оскільки прогнозуюча функція ℎ приймає як аргументи вектор об’єднаних 
(��)
даних поточного стану ����,�� та прогнозні погодні параметри ����,��+1 , сценарний аналіз 
полягає у множинному застосуванні ℎ до різних сценаріїв. 
(��)
Для кожного сценарію ����,��+1 визначається відповідний прогноз: 
(��) (��)
?̂?��,��+1 = ℎ (����,�� ,  ����,��+1) ,   �� = 1, … , ��  (2.18) 
Таким чином, модель визначає відображення множини сценаріїв у множину 
відповідних прогнозів: 
(1) (2) (��)
����,��+1 = {?̂?��,��+1,   ?̂?��,��+1,  … ,   ?̂?��,��+1} .   (2.19) 
Отримана множина ����,��+1  дозволяє формувати характеристики сценарної 
невизначеності: 
- мінімальний та максимальний можливий стан: 
�������� (��)
��,��+1 = min ?̂?��,��+1 ,    (2.20) 
��
������ (��)
����,��+1 = max ?̂?��,��+1 ,   (2.21) 
��
 
31 
 
- оцінку дисперсії Var(����,��+1) 
- чутливість прогнозу до зміни сценарію погоди: 
(�� ) (�� )
Δ�� ,�� = |?̂? 1 2
1 2 ��,��+1 − ?̂?��,��+1 | .   (2.22) 
Таким чином сценарний аналіз у цій роботі формалізується як обчислення 
множини прогнозів ����,��+1, що відповідають різним можливим траєкторіям майбутніх 
погодних умов. Модель використовується не лише як точкове передбачення, а як 
інструмент для оцінювання впливу невизначеності зовнішніх факторів на стан 
рослинності. 
2.5 Висновки до розділу 2 
У цьому розділі було сформульовано математичні основи підходу MTCR-CS для 
прогнозування стану рослинності з урахуванням мультимодальних даних. Спочатку 
було введено формалізацію задачі, де для кожної аграрної ділянки і моменту часу t 
визначаються дві модальності спостережень — супутникові знімки та погодні часові 
ряди. Встановлено цільову змінну у вигляді медіанного NDVI, а також поставлено 
задачу побудови двох взаємопов’язаних функцій: відображення мультимодальних 
даних у компактний латентний простір та прогнозуючої функції, що оцінює значення 
NDVI на наступному часовому інтервалі на основі об’єднаного представлення 
поточного стану та прогнозованих погодних параметрів. Важливою складовою 
постановки задачі є функція причинної чутливості, яка формалізує сценарний аналіз 
та дозволяє оцінювати вплив гіпотетичних змін погодних факторів на прогнозований 
стан рослинності. 
Далі було описано метод MTCR, який забезпечує відображення 
мультимодальних даних у латентний простір. Для цього введено оператори уніфікації, 
що приводять зображення до фіксованого розміру та забезпечують представлення 
погодних послідовностей у стандартизованому часовому форматі. Такі трансформації 
гарантують інваріантність до просторових розмірів зображень та узгодження 
 
32 
 
нерівномірних часових рядів. Далі були визначені окремі математичні відображення 
для кожної модальності, які конвертують трансформовані дані у вектори фіксованої 
розмірності. Агрегуюча функція забезпечує баланс між внесками різних 
модальностей, попередньо нормалізуючи їх і комбінуючи з адаптивним 
масштабуванням. Така конструкція гарантує, що жодна з модальностей не домінує 
через абсолютні масштаби ознак, а також дозволяє зберігати сезонну структуру 
процесу та асимптотичну рівність приростів у латентному просторі для різних фаз 
вегетації. 
У підпункті 2.3 була формалізована прогнозуюча функція, яка оперує 
об’єднаним латентним вектором, що містить інформацію про поточний стан ділянки 
та погодні умови до моменту прогнозу, а також прогнозованими або сценарними 
погодними даними для наступного інтервалу часу. Відображення представлено як 
композиція операторів, що обробляють просторово-вегетаційні дані та погодні 
характеристики окремо, та інтегруючого оператора, який формує єдине числове 
передбачення NDVI. Така структура дозволяє уникнути некоректної геометричної 
інтеграції векторів різної природи, зберігаючи гнучкість подальшої побудови моделі 
та можливість адаптації до різних сценаріїв даних. 
У підпункті 2.4 було формалізовано сценарний аналіз як множинне застосування 
прогнозуючої функції до набору альтернативних сценаріїв погодних умов. Кожен 
сценарій описується вектором прогнозних або гіпотетичних погодних параметрів, а 
відповідний прогноз NDVI формується за допомогою функції h. Множина отриманих 
прогнозів дозволяє оцінювати мінімальні та максимальні значення NDVI, дисперсію 
прогнозів та чутливість до різних сценаріїв, що забезпечує кількісне відображення 
невизначеності зовнішніх факторів. Такий підхід створює математичну основу для 
оцінки ризиків та моделювання екстремальних ситуацій, дозволяючи порівнювати 
альтернативні траєкторії розвитку погодних умов та їх вплив на стан рослинності. 
 
33 
 
Таким чином, отримана математична постановка та формалізація методів 
задають чітку основу для подальшої реалізації моделі, її тестування на реальних даних 
та проведення сценарних експериментів 
  
 
34 
 
3 ПРАКТИЧНА РЕАЛІЗАЦІЯ МЕТОДУ MTCR-CS  
3.1 Підготовка даних 
Ефективність моделей прогнозування стану сільськогосподарських угідь 
значною мірою залежить від якості вихідних даних та коректності процедур їх 
попередньої обробки. У контексті дослідження, яке передбачає моделювання динаміки 
розвитку посівів і виявлення закономірностей між середовищними чинниками та 
станом конкретних ділянок, особливої уваги потребує поєднання різнорідних джерел 
інформації. Найбільш інформативними серед них є географічні дані, що 
характеризують просторові та морфологічні властивості полів, та погодні показники, 
які відображають умови середовища та вплив кліматичних факторів у певні проміжки 
часу. 
Географічні дані дозволяють описати кожну земельну ділянку не лише як окрему 
геометричну область, але і як просторовий об’єкт, чутливий до неоднорідності 
рельєфу, особливостей ґрунтів, кількості сонячного випромінювання, характеру 
дренажу та інших параметрів, які визначають темпи росту рослин та можливі ризики 
для врожайності. Використання геопросторових даних з супутникових знімків 
відкриває можливість аналізувати поля з високою деталізацією, включно з виявленням 
внутрішньопольової варіабельності. Застосування методів комп’ютерного зору 
дозволяє перетворювати такі зображення на ознаки, що можуть бути використані 
моделями машинного навчання та глибокого навчання, а також проводити зіставлення 
між часовими зрізами, формуючи динамічні характеристики стану рослинності. 
Погодні дані становлять інший критично важливий пласт інформації. На відміну 
від зображень, які фіксують результат впливу середовищних факторів на певний 
момент, погодні показники відображають процеси, що безпосередньо формують 
біофізіологічний стан рослин протягом усього періоду їх розвитку. Температура 
повітря, кількість опадів, тривалість сонячного випромінювання, вологість ґрунту, 
 
35 
 
швидкість вітру, хмарність та інші параметри суттєво впливають на фотосинтетичну 
активність, водний баланс, стресові навантаження та темпи накопичення біомаси. 
Врахування цих змінних дає змогу моделі не лише описувати поточний стан поле–
місяць, але й прогнозувати його майбутні характеристики з урахуванням кліматичних 
умов. 
Комплексне поєднання географічних і погодних даних дозволяє створювати 
багатовимірні представлення, здатні передавати взаємодію статичних та динамічних 
чинників. Супутникові зображення надають просторову структуру об’єкта, тоді як 
погодні ряди забезпечують часову складову, необхідну для побудови причинно-
наслідкових зв’язків. Такий підхід істотно розширює можливості моделей машинного 
навчання, даючи змогу наближатися до реальних процесів росту культур, а не лише до 
статистичного опису даних. 
У цьому підрозділі буде розглянуто загальні принципи підготовки цих двох типів 
даних, процедури їх узгодження, нормалізації та формування єдиного набору для 
подальшого моделювання. Окрему увагу приділено забезпеченню повноти часових 
рядів, узгодженню географічних прив’язок та формуванню репрезентативних ознак, 
придатних для моделювання на основі глибокого навчання. 
3.1.1 Підготовка картографічних даних 
Для реалізації дослідницької частини роботи було сконструйовано 
спеціалізований пайплайн підготовки картографічних даних, що поєднує три основні 
компоненти: (i) отримання і валідація векторних геометрій полів, (ii) формування 
місячних мультиплікаційних (median) супутникових композицій за допомогою Google 
Earth Engine та (iii) просторове обрізання (кропінг) растрових композицій за 
геометріями полів із наступною організацією збережених артефактів і метаданих у 
manifest-таблицю. Така послідовність дозволяє отримати однорідний набір растрових 
 
36 
 
спостережень, готових для подальшого екстрагування ознак або подачі в 
мультимодальні моделі. 
Першим кроком було формальне отримання векторних меж полів для 
досліджуваного регіону (Черкаська область) за періоди 2021–2023 рр. Джерелом 
геометрій слугували публічні картографічні набори, які було попередньо очищено від 
явних артефактів та приведено до єдиного системного представлення координат. У 
робочому просторі геометрії зберігаються у форматі GeoJSON/GeoPackage із полями 
ідентифікації, серед яких — внутрішній field_id та код громади koatuu. На рис. 3.1 
можна побачити межі полів Черкаської області в програмі Qgis. 
 
Рисунок 3.1 – Межі полів Черкаської області 
Далі для кожного календарного місяця досліджуваних років сформовано 
місячну «медіанну» композицію на основі архівів Sentinel-2 (L2A) через платформу 
Google Earth Engine (GEE). Вибір медиани (місячного медіанного знімка) 
обґрунтовано двома міркуваннями: по-перше, операція медіани стійко зменшує вплив 
випадкових затемнень хмарами або шуму; по-друге, вона надає репрезентативне, 
просторово агреговане представлення поверхні за інтервал. При формуванні 
композицій застосовано стандартну маскуцію хмар (SCL або QA-політики L2A) та 
додаткові фільтри по відсотку хмарності кадру. Растрові дані зберігалися з цільовою 
 
37 
 
просторовою роздільністю 10 м і в уніфікованому CRS (EPSG:32636 — UTM зона, що 
відповідає регіону), щоб уникнути відмінностей у зразкових розмірах пікселів при 
наступному агрегаційному аналізі. На рис. 3.2 можна побачити супутникове 
зображення полів черкаської області за серпень 2021 року. 
 
Рисунок 3.2 – Супутникове зображення полів Черкаської області 
На третьому кроці виконано просторове обрізання (raster clipping) отриманих 
місячних композицій по кожній геометрії поля. Для зручності обробки та подальшого 
аналізу операція обрізання проводилась у два кроки: (i) спочаткове обрізання по 
геометріям громад (KOATUU) — щоб зменшити обсяг оперативних даних, та (ii) точне 
обрізання по межах конкретного поля. Цей підхід дає гарантію, що збережений растр 
містить лише пікселі, які належать полю, а також дозволяє зберігати окремі файли для 
кожного поля і кожного місяця. Після обрізання зображення зберігаються у форматі, 
що підтримує багатоканальність (GeoTIFF або PNG залежно від подальшої потреби), 
а також у відповідному діапазоні чисел (наприклад uint8/uint16 з описом 
масштабування). На рис. 3.3 можна побачити зображення одного поля після 
проведення операції просторового обрізання. 
 
38 
 
 
Рисунок 3.3 – Окреме поле після просторового обрізання 
Паралельно з растровою обробкою автоматично генерується manifest-таблиця — 
централізований метадані-реєстр, що фіксує для кожного обробленого файлу такі 
атрибути: field_id, koatuu, month, year, image_path, valid. Поле valid використовується 
як бінарний індикатор якості: True — якщо зображення успішно сформовано та 
проходить базові критерії (достатня кількість пікселів, прийнятна хмарність), False — 
якщо присутні проблеми (сукупність хмар > порогу, відсутні дані, помилки при кропі, 
некоректні формати). До manifest також вносилися додаткові атрибути контролю 
якості — частка NaN/маскованих пікселів, кількість джерельних кадрів у композиції, 
середній рівень яскравості тощо. На рис. 3.4 можна побачити гістограми розподілу 
змінної valid по рокам. 
 
Рисунок 3.4 – Гістограми розподілу змінної valid по рокам 
 
39 
 
Практичні деталі реалізації та заходи контролю якості заслуговують окремого 
пояснення. По-перше, для уникнення змішування різних проєкцій і втрати точності всі 
векторні та растрові операції виконувались в єдиному CRS, а при імпорті геометрій — 
приведення координат до цього CRS було обов’язковим. По-друге, для мінімізації 
впливу хмарності використовувався поріг відбору кадрів у GEE 
(CLOUDY_PIXEL_PERCENTAGE), маскація по SCL та обчислення медіани на основі 
QA-фільтрованих кадрів; у випадках, коли за місяць не знайдено жодного прийнятного 
кадру, відповідний запис у manifest позначався як valid = False. По-третє, невеликі поля 
(внаслідок субпіксельності) вимагали уваги: для полів, розміри яких менші за 
кількість пікселів при цільовому масштабі (10 м), у manifest зберігалися додаткові 
індикатори — ці експериментальні поля можна або виключити з окремих аналізів, або 
обробляти з інтерполяцією/агрегацією до сусідніх одиниць. 
Організація вихідних даних спроектована так, щоб забезпечити відтворюваність 
та гнучкість: файли лежать у структурі 
processed_data/images/{year}/{month}/FIELD_{field_id}_{month}_{year}.{ext}, а файл 
manifest — у корені processed_data/manifest_fields.csv. Така архітектура дозволяє легко 
зіставляти растрові та дані за ключем koatuu та роком, при цьому кожен запис manifest 
містить абсолютний шлях до файлу зображення (image_path) і прапор валідації (valid) 
— що важливо для наступного етапу вибірок (наприклад, вибір полів з повним 
набором 12 місяців). 
Підсумовуючи, підготовчий пайплайн забезпечив: однорідні місячні композиції 
Sentinel-2, обрізані по межах полів, контрольовані механізми відбору і маркування 
якості, та зручну для моделювання структуру збережених артефактів. Далі (у розділах, 
присвячених обробці табличних даних і побудові моделі MTCR-CS) ці артефакти 
використовуються як вхідні модальності, причому manifest слугує джерелом 
індексації та фільтрації семплів для тренувальних, валідаційних та тестових 
підвибірок. 
 
 
40 
 
3.1.2 Підготовка погодних даних 
Погодні характеристики є одним із ключових чинників, що визначають 
вегетаційний розвиток посівів, а отже  —  й кінцеві значення індексу NDVI, який 
виступає основним джерелом інформації про стан рослинності у супутниковому 
моніторингу. Для забезпечення коректного поєднання супутникових спостережень із 
метеорологічними параметрами необхідно використовувати погодні ряди, що 
репрезентують умови саме тієї території, на якій розташоване відповідне 
сільськогосподарське поле. Проте типові метеорологічні набори даних мають 
відносно низьку просторову роздільну здатність — зазвичай від 11 до 27 км залежно 
від моделі. Це означає, що для двох точок, які розташовані на малій території, 
наприклад, у межах однієї адміністративної громади, значення погодних параметрів 
будуть практично ідентичними. 
З огляду на це, для цілей даного дослідження використано підхід, за яким 
погодні ряди розраховуються для центроїда кожної територіальної громади, а всі поля, 
що знаходяться в межах однієї громади, отримують однакові погодні характеристики. 
Такий метод значно спрощує підготовку даних, зменшує кількість запитів до 
метеорологічного API та усуває необхідність відстежувати належність кожного поля 
до окремого пікселя глобальної моделі погоди. Однак для його застосування необхідно 
переконатися, що варіабельність погодних параметрів усередині однієї громади є 
незначною і не впливає на подальшу якість моделювання. 
Для емпіричної перевірки цієї гіпотези було обрано найбільшу за площею 
громаду Черкаської області. Усередині її меж знайдено дві максимально віддалені 
точки, відстань між якими становить близько 90 км, що фактично є верхньою межею 
можливої варіабельності погоди в межах адміністративної одиниці такого масштабу. 
Для обох точок було завантажено щоденні метеорологічні ряди за один календарний 
рік. Порівняння виконано окремо для всіх змінних, доступних у наборі Open-Meteo: 
 
41 
 
температури повітря, кількості опадів, тривалості сонячного сяйва, вологості, 
хмарності, тиску, параметрів ґрунту та швидкості вітру. 
Для кожної змінної обчислено основні показники подібності: середню 
абсолютну похибку (MAE), корінь середньоквадратичної похибки (RMSE), коефіцієнт 
кореляції Пірсона, t-тест на рівність середніх та критерій Вілкоксона. Результати 
свідчать, що переважна більшість параметрів має надзвичайно високу кореляцію 
(0.95–0.999), що вказує на подібний характер добових коливань у двох точках. 
Найбільш суттєві відмінності спостерігаються лише для швидкості вітру, що 
пояснюється локальною орографією та особливостями моделі, але цей параметр не є 
визначальним у контексті оцінки стану посівів на основі NDVI. Отримані значення 
можна в таблиці 3.1. 
Таблиця 3.1 - Показники подібності для двох найвіддаленіших точок громади 
Показник MAE RMSE r p (r) t p (t) W p (W) 
Сер. темп. 0.24 0.3096 0.9995 4.7328e-54 -0.5363 0.5951 291.0 0.3696 
Опади 10.3 15.2885 0.8772 1.0693e-12 -0.2846 0.7776 321.0 0.8505 
Дощ 10.1 15.2402 0.8888 2.0562e-13 -0.1906 0.8499 327.0 0.7117 
Трив. сонця 783.7 993.7941 0.9980 1.0097e-43 2.2038 0.0340 200.0 0.0367 
Вологість сер. 1.7 2.0194 0.9817 8.5621e-27 3.1538 0.0032 168.0 0.0048 
Хмарність сер. 1.6 2.0426 0.9932 2.8366e-34 1.1121 0.2735 252.0 0.2032 
- 5.0579e- 2.9104e-
Вітер сер. 1.5 1.7340 0.9392 8.0506e-18 1.0 
10.9613 13 11 
Темп. ґрунту 0–7 см 0.4 0.5937 0.9983 5.1403e-45 -3.7414 0.0006 102.0 0.0001 
Вологість ґрунту 0–7 
0.01 0.0213 0.9513 1.8165e-19 1.7827 0.0831 186.0 0.0116 
см 
Для наочності також побудовано порівняльні графіки. Оскільки повний набір 
містить значну кількість змінних, у тексті наведено лише два найбільш інформативних 
сценарії: часові ряди середньої температури та суми опадів. Решту графіків можна 
переглянути у додатках. На рис. 3.5 – 3.6 можна побачити порівняння середньої 
добової температури а також порівняння добової суми опадів в найбільшвіддалених 
точках найбульшої громади черкаської області. 
 
42 
 
 
Рисунок 3.5 - Порівняння середньої добової температури у двох точках 
громади 
 
Рисунок 3.6 - Порівняння добової суми опадів у двох точках громади 
Високий ступінь подібності часових рядів підтверджує доцільність 
використання погодних даних, розрахованих за центроїдом громади, для подальшого 
поєднання з супутниковими зображеннями полів. Це дозволяє значно зменшити 
обчислювальне навантаження, уникнути надмірної деталізації, що не відповідає 
роздільній здатності глобальних погодних моделей, та забезпечити стабільність у 
процесі формування табличних ознак для навчання моделей прогнозування NDVI. 
 
43 
 
3.1.3 Фінальна структура даних 
Подальша робота з моделями машинного навчання вимагала впорядкованої 
організації всіх підготовлених даних, тому важливо зафіксувати їхню остаточну 
структуру. У процесі попередніх етапів були сформовані три основні групи даних: 
обрізані супутникові знімки полів, агреговані погодні ряди для кожної громади 
Черкаської області та таблиця-маніфест, що пов’язує геометрію, часові мітки та 
метадані окремих полів із відповідними вхідними файлами. Усі ці компоненти були 
інтегровані в єдину файлову систему, що забезпечує відтворюваність експериментів, 
зручність при завантаженні даних у моделі та прозорість для подальшої реконструкції 
повного пайплайна. Структуру даних можна побачити на рис. 3.7. 
 
Рисунок 3.7 – Фінальна структура даних 
Супутникові знімки зберігаються в окремих директоріях відповідно до року та 
місяця, що дає змогу швидко отримувати часові вибірки для окремих полів. Усередині 
кожної місячної папки містяться растрові файли, обрізані за межами конкретних 
польових ділянок, які були визначені за допомогою офіційних геометрій полів та меж 
громад. Погодні дані організовані у вигляді окремих таблиць за роками, де кожен файл 
відповідає одній громаді та містить щоденні метеорологічні показники, що дозволяє 
 
44 
 
виконувати подальшу агрегацію за місяцями. Таблиця manifest виступає ключовою 
ланкою між супутниковими зображеннями та погодними характеристиками: вона 
містить посилання на всі згенеровані файли, разом із полями field_id, koatuu, month, 
year, image_path та індикатором валідності. 
Таке структуроване представлення забезпечує гнучкість для моделювання, 
зняття часових підвибірок, відтворення результатів та масштабування дослідження на 
інші регіони. Нижче наведено схему, яка ілюструє загальну організацію папок і файлів 
після завершення етапу підготовки даних, та може бути використана у звіті у вигляді 
зображення. 
3.2 Реалізація методу MTCR 
У процесі програмної реалізації мультиджерельної контрастивної моделі 
прогнозування (MTCR) перший та найважливіший етап полягав у підготовці та 
систематизації вихідних даних, необхідних для ефективного навчання моделей. На 
початку роботи було завантажено маніфест, який містив детальну інформацію про усі 
наявні поля, включаючи шляхи до відповідних зображень, координати, рік збору даних 
та інші метадані. Цей маніфест слугував основою для формування датасету, що 
дозволяв здійснювати узгоджену обробку як зображень, так і табличних даних, при 
цьому забезпечуючи сумісність усіх записів. Для підвищення точності моделі та 
уникнення спотворень у прогнозах було здійснено фільтрацію даних: залишені лише 
ті поля, для яких наявні дані за всі 12 місяців року. Такий підхід забезпечував повноту 
інформації для кожного прикладу та дозволяв створити стабільні тренувальні та 
валідаційні підмножини даних. 
Погодні дані, що включали широкий спектр показників — від температури та 
опадів до відносної вологості, хмарності та швидкості вітру — були агреговані за 
місяцями з використанням статистичних функцій, таких як середнє або сума залежно 
від конкретного показника. Всі відсутні значення заміщувалися нулями, що дозволяло 
 
45 
 
уникнути помилок при обчисленні та навчанні моделі. Такий підхід забезпечив 
уніфіковане представлення погодних даних у вигляді матриць фіксованого розміру, 
готових до подачі на вхід табличних енкодерів. Крім того, для підвищення 
ефективності обчислень було реалізовано кешування оброблених погодних даних, що 
дозволяло значно скоротити час доступу під час формування батчів. 
Датасет MTCR було реалізовано у вигляді спеціалізованого класу, який 
забезпечував коректну обробку всіх типів вхідних даних. Для зображень 
здійснювалася стандартна процедура підготовки: конвертація у формат RGB, зміна 
розмірів до єдиного стандарту та перетворення у тензори PyTorch. Особлива увага 
приділялася надійності завантаження зображень: враховувалися помилки при 
відкритті файлів, включаючи підтримку TIFF та багатоканальних зображень, з 
автоматичним приведенням їх до трьох каналів. Для погодних даних модель 
отримувала інформацію як за поточний місяць, так і за наступний, що дозволяло 
використовувати короткострокову часову залежність при формуванні латентних 
представлень. DataLoader був налаштований так, щоб забезпечити ефективну подачу 
батчів даних на GPU, з контролем розміру батчу, кількості воркерів та випадкового 
перемішування даних для уникнення кореляцій між послідовними прикладами. 
Наступним етапом реалізації MTCR було формування енкодерів для обробки 
різних типів даних. Для зображень були розглянуті три архітектури: EfficientNet, 
ResNet18 та середній CNN. Кожен з цих енкодерів забезпечував перетворення 
зображення у компактне латентне представлення, яке містило достатньо інформації 
для подальшої інтеграції з табличними даними. Для обробки табличних даних були 
реалізовані три підходи: трансформер на основі TabTransformer, багатошаровий 
перцептрон та середній табличний енкодер. Трансформер дозволяв моделі 
враховувати залежності між різними місяцями погодних даних, тоді як багатошаровий 
перцептрон та середній табличний енкодер забезпечували більш просту, але 
ефективну нелінійну обробку вхідних характеристик. Такий поділ дозволяв оцінити 
вплив складності та архітектури енкодера на загальну якість навчання моделі. 
 
46 
 
Для інтеграції латентних представлень зображень та табличних даних у 
спільний простір була використана проекційна голова, що перетворювала 
конкатенований вектор у простір фіксованої розмірності. Це дозволяло ефективно 
поєднувати інформацію з різних джерел та забезпечувати узгодженість у 
представленні кожного прикладу. Додатково була реалізована прогнозуюча голова, яка 
на основі поєднаного латентного представлення виконувала завдання передбачення 
цільової змінної. Така архітектура дозволяла одночасно навчати модель виділяти 
загальні контрастивні ознаки та здійснювати специфічне прогнозування, що є 
ключовим елементом мультиджерельного підходу. 
Для навчання MTCR застосовувався самоконтрастивний підхід на основі 
InfoNCE loss. Для кожного батчу формувалися два варіанти даних шляхом додавання 
невеликого гаусівського шуму до зображень та табличних даних, що моделювало різні 
“перспективи” одного і того ж прикладу. Латентні представлення кожного з варіантів 
нормалізувалися, після чого обчислювалася матриця схожості між усіма парами у 
батчі. Loss заохочував модель зближувати представлення одного прикладу та 
відокремлювати різні приклади. Оптимізація виконувалася за допомогою алгоритму 
Adam із спільним оновленням параметрів обох енкодерів та проекційної голови. 
Для оцінки ефективності різних архітектур було проведено систематичний grid 
search по всіх комбінаціях енкодерів зображень, табличних енкодерів та проекційної 
голови. Кожна комбінація тренувалася протягом обмеженої кількості епох у швидкому 
режимі, зберігаючи криві втрат та фінальний показник InfoNCE loss. Для найкращих 
моделей були витягнуті латентні представлення для обмеженої підмножини даних з 
метою подальшої візуальної оцінки роздільності ембеддінгів. 
Візуальний аналіз результатів включав побудову кривих втрат для всіх 
архітектур, що дозволяло спостерігати процес збіжності моделей протягом навчання. 
Криві втрат можна побачити на рис. 3.8. 
 
47 
 
 
Рисунок 3.8 – Криві втрат при підборі гіперпараметрів 
Для порівняння фінальної якості різних комбінацій була побудована 
горизонтальна стовпчикова діаграма, яка наочно демонструвала відмінності у 
значеннях фінальної втрати. Стовпчикову діаграму з фінальними втратами можна 
побачити на рис. 3.9 
 
Рисунок 3.9 – Стовпчикова діаграма фінальних втрат моделей 
Для поглибленої оцінки якості латентних ознак найкращої архітектури було 
виконано зменшення розмірності простору ембеддінгів за допомогою методу t-SNE, 
результат якої можна побачити на рис. 3.10 
 
48 
 
 
Рисунок 3.10 – Результат зменшення розмірності простору ембеддінгів 
Отримана проєкція демонструє чітко виражену структурованість латентного 
простору та наявність природної кластеризації зразків. Незважаючи на високу 
нелінійність вихідного простору ознак, t-SNE вдалося відобразити низку локальних і 
глобальних взаємозв’язків між ембеддінгами, що підтверджує узгодженість і 
стабільність сформованих представлень. Помітна тенденція до групування окремих 
підмножин даних свідчить про те, що модель успішно виокремлює інформативні 
патерни та відділяє близькі за змістовими характеристиками об’єкти від віддалених. 
Факт формування компактних кластерів у низьковимірному просторі є 
важливим позитивним результатом, оскільки t-SNE зазвичай виявляє приховану 
структуру лише тоді, коли висхідні ознаки мають достатню дискрімінативну силу. 
Таким чином, отримана візуалізація підтверджує, що навчена модель не лише 
мінімізує функцію втрат, але й формує семантично узгоджені ембеддінги 
Результати систематичного порівняння показали, що оптимальною комбінацією 
виявився середній CNN для обробки зображень та TabTransformer для табличних 
даних. Ця архітектура забезпечила мінімальне значення InfoNCE loss серед усіх 
протестованих конфігурацій, що свідчить про високу здатність моделі інтегрувати 
інформацію з різних джерел та формувати узгоджені латентні представлення. Інші 
 
49 
 
комбінації архітектур продемонстрували більш високі значення втрат, підтверджуючи 
перевагу обраної конфігурації для подальшого використання у MTCR та застосуванні 
у завданнях прогнозування. 
3.3 Вибір архітектури та навчання прогнозуючої функції 
У процесі роботи виявилося, що стандартний показник, який традиційно 
застосовується для аналізу стану рослинності, а саме NDVI, неможливо обчислити для 
наявних супутникових зображень. Причина полягає у тому, що отримані дані містять 
лише три канали видимого діапазону, тоді як для обчислення вегетаційних індексів на 
основі інфрачервоного спектра необхідно мати щонайменше один канал ближнього 
інфрачервоного випромінювання. Зважаючи на це, для побудови моделі 
прогнозування було обрано інший показник, який найбільше корелює з NDVI та 
здатний відображати сезонну динаміку рослинного покриву навіть при наявності 
тільки RGB-каналів. Найкращим варіантом виявився VARI, який є стійким до впливу 
тіней, атмосферного серпанку та інших оптичних викривлень, а також добре 
узгоджується з поведінкою NDVI на масштабі сільськогосподарських територій. 
На попередньому етапі було сформовано єдиний латентний простір за 
допомогою двох спеціалізованих модулів для аналізу зображень та погодних даних. 
Цей простір став основою для побудови прогнозуючої моделі, яка отримує на вхід 
поєднані представлення супутникових знімків та метеорологічних характеристик і має 
формувати кількісну оцінку майбутнього значення VARI. У даному розділі розглянуто 
власне алгоритм прогнозування, що був побудований поверх раніше сформованих 
ознак. 
Прогнозуюча частина моделі базується на тому, що обидва енкодери, які 
відповідали за аналіз супутникових зображень та табличних погодних спостережень, 
залишаються фіксованими та не змінюють своїх параметрів під час подальшого 
навчання. Таким чином досягається так зване "фіксоване представлення", коли модель 
 
50 
 
використовує вже сформовану структуру ознак і навчає окремий модуль, 
відповідальний виключно за прогноз. Такий підхід дозволяє значно зменшити 
кількість параметрів, що оновлюються, стабілізує навчання і робить його менш 
чутливим до шуму в цільовій змінній. 
Прогнозуючий модуль отримує на вхід два компоненти. Перший містить 
латентні ознаки супутникового зображення та погодних умов у поточному місяці, 
другий відповідає за характеристику погоди у наступному місяці. Додавання 
інформації про майбутні погодні умови є ключовим елементом моделі, оскільки саме 
погода у період між двома супутниковими спостереженнями визначає інтенсивність 
вегетаційних процесів. Після об’єднання цих даних модель формує цілісне 
представлення, яке у подальшому передається у невелику регресійну мережу, здатну 
встановити нелінійний зв’язок між сукупними ознаками та майбутнім значенням 
VARI. 
Структура прогнозуючої частини була навмисно зроблена відносно компактною. 
Мережа складається з двох послідовних перетворень, між якими застосовується 
функція активації, що надає моделі можливість уловлювати складні взаємозв’язки між 
супутниковими особливостями та погодою. Така архітектура добре підходить для 
задачі короткострокового прогнозування вегетаційних показників, оскільки дозволяє 
моделі узагальнювати закономірності, виявлені на великій кількості прикладів, при 
цьому уникаючи перенавчання. 
Якість роботи моделі оцінювалася на незалежній вибірці, яка не брала участі у 
навчанні. Порівняння передбачених значень із фактичними демонструє, що модель 
здатна відтворювати загальні тенденції зміни VARI між місяцями та реагує на сезонні 
коливання. Візуалізація зіставлення передбачених і справжніх значень представлена 
на рис. 3.11. 
 
51 
 
 
Рисунок 3.11 – Візуалізація передбачених та істинних значень 
На графіку залишків видно, що більшість точок зосереджені навколо нульової 
лінії, що свідчить про стабільну роботу моделі. Суттєвих систематичних зміщень не 
спостерігається: залишки рівномірно розподілені як для менших, так і для більших 
прогнозованих значень VARI. Випадки з більшими відхиленнями поодинокі, що 
підтверджує загальну надійність моделі для більшості спостережень. Графік залишків 
можна побачити на рис. 3.12 
 
Рисунок 3.12 – графік залишків 
Кількісна оцінка точності прогнозу включала значення середньоквадратичної 
помилки, середньої абсолютної похибки та коефіцієнта детермінації. Отримані 
 
52 
 
результати свідчать, що модель досягає помірної точності: середньоквадратична 
похибка становить приблизно п’ять сотих, середня абсолютна помилка трохи більше 
однієї десятої, а показник детермінації сягає близько однієї шостої. Це означає, що 
модель здатна частково пояснювати варіації вегетаційного індексу, хоча існує простір 
для покращення, зокрема при появі якісніших вхідних даних або додаткових 
спектральних каналів. Конкретні результати можна побачити в таблиці 3.2  
Таблиця 3.2 – значення метрик на валіаційному наборі 
MSE (Mean Squared Error) 0.049305 
RMSE (Root Mean Squared Error) 0.2220 
MAE (Mean Absolute Error) 0.1661 
R2 Score 0.1525 
Отримані результати дають підстави стверджувати, що навіть за умов 
обмеженості доступних спектральних даних модель здатна формувати корисні 
прогнози VARI на місячному горизонті. Враховуючи те, що VARI у значній мірі 
відображає структуру NDVI, побудована система може бути застосована як 
наближений інструмент оцінки стану рослинності для тих територій, де 
високоспектральні дані недоступні. Модель також демонструє потенціал для 
подальшого вдосконалення за рахунок розширення набору входів, зміни архітектури 
або адаптації під багаторічні ряди спостережень. 
3.4 Сценарний аналіз 
У межах цієї роботи було реалізовано окремий модуль сценарного аналізу, 
призначений для демонстрації поведінки побудованої прогнозуючої моделі VARI у 
гіпотетичних умовах. Його призначення полягає не в оцінці якості моделі 
традиційними метриками, а в тому, щоб показати, наскільки передбачувано й 
стабільно система реагує на зміну вхідних факторів, а також уможливити 
 
53 
 
інтерпретацію механізмів, які модель використовує для формування своїх прогнозів. 
Сценарний аналіз дає змогу перевірити не лише те, наскільки точно модель працює в 
історичних умовах, але й як саме вона адаптується до потенційних зсувів кліматичних 
або агрометеорологічних характеристик, що є необхідним у контексті прикладного 
прогнозування рослинних індексів. 
У межах цього модуля було реалізовано два види експериментів, кожен із яких 
спрямований на власний аспект інтерпретованості. Перший тип — це сценарії зміни 
погодних умов майбутнього місяця. Для цього вибирається підмножина реальних 
зразків, і для кожного з них створюється набір гіпотетичних модифікацій, що імітують 
можливі погодні відхилення. Такі сценарії включають, наприклад, підсилене 
потепління, зниження вологості чи інші комбінації, що моделюють екстремальні 
погодні ситуації. Після зміни відповідних ознак майбутнього місяця дані повторно 
подаються на вхід моделі, що дозволяє порівняти базовий прогноз із прогнозами за 
різних сценаріїв. Таким чином формується таблиця результатів, у якій для кожного 
поля та кожного сценарію подано як реальні значення VARI, так і прогнозні значення, 
отримані модельними способами. Це дозволяє чітко простежити, як саме прогноз 
реагує на зміну умов, і які сценарії мають найбільший або найменший вплив на 
очікуваний результат. На рис. 3.13 можна побачити результат першого етапу 
сценарного аналізу на якому: 
- HotDry  - температура збільшується на 15%, вологість зменшується на 15% 
- ColdWet - температура зменшується на 15%, вологість збільшується на 15% 
- VeryHot – температура збільшується на 30% 
 
Рисунок 3.13 - Результати першого етапу сценарного аналізу 
 
54 
 
Другий тип сценарного аналізу спрямований на виявлення чутливості моделі до 
окремих параметрів, тобто на визначення того, які ознаки відіграють найбільш істотну 
роль у формуванні прогнозу. Для цього вибирається один конкретний зразок, після 
чого кожна погодна змінна окремо “збурюється” на фіксований відсоток. Усі інші 
величини залишаються без змін, що дає змогу ізолювати вплив лише однієї ознаки на 
поведінку моделі. У результаті формується окремий набір прогнозних значень, які 
описують, наскільки сильно змінюється VARI при збільшенні або зменшенні 
відповідної характеристики. Це дозволяє кількісно оцінити, які змінні є найбільш 
чутливими: наприклад, температура може демонструвати значно більший вплив, ніж 
опади, навіть за однакової відносної зміни. Такий формат результатів зручно 
представляти у вигляді таблиці та гістограми чутливостей, що дозволяє провести 
візуальне порівняння впливу ознак між собою. На рисунку 3.14 можна побачити 
приклад результату чутливості для одного зразку даних. 
 
Рисунок 3.14 – Результат чутливості ознак 
Запропонований сценарний аналіз має важливе значення для оцінки практичної 
цінності створеної моделі, оскільки дозволяє інтерпретувати її поведінку у випадках, 
коли майбутні погодні умови можуть відхилятися від звичних тенденцій. Це особливо 
актуально в контексті агросектору, де величини погодних коливань здатні суттєво 
впливати на стан рослинності та динаміку вегетаційних індексів. Сценарний аналіз 
дає змогу не лише продемонструвати, що модель реагує на зміни у вхідних даних 
логічно та послідовно, але й показати, що вона не формує надмірно нестабільних 
прогнозів у відповідь на помірні коливання факторів. 
 
55 
 
Підсумовуючи, реалізована система сценарного аналізу виконує роль 
інструмента глибинного розуміння поведінки моделі та її здатності адаптуватися до 
нових умов. Вона доповнює процес формування прогнозуючої функції, забезпечуючи 
практичну інтерпретованість результатів і дозволяючи оцінити як вплив комбінацій 
змін, так і значимість окремих характеристик. Це робить методологію прогнозування 
VARI не лише технічно коректною, але й аналітично обґрунтованою та придатною для 
використання в реальних прикладних сценаріях. 
3.5 Об’єднане рішення 
У процесі розробки системи прогнозування VARI було застосовано модульний 
підхід, що передбачає поділ всієї архітектури на окремі функціональні блоки, кожен з 
яких відповідає за конкретний етап обробки даних або навчання моделі. Така 
організація роботи дозволяє забезпечити гнучкість дослідження, полегшити 
налагодження та повторне використання окремих компонентів у різних 
експериментах. Перший блок відповідає за вибір архітектури та безпосереднє 
навчання основної моделі MTCR, включаючи збереження проміжних результатів та 
метрик якості. У другому блоці реалізовано функціонал для прогнозуючої голови, яка 
забезпечує додаткові прогнозні виходи та інтегрується з базовою моделлю через 
узгоджені інтерфейси. Кожен з цих блоків незалежно виконує свої завдання, проте їх 
результати можуть бути використані в подальшому аналізі для оцінки цілісності 
системи. 
У завершальному етапі, що відповідає сценарному аналізу, застосовуються дані 
та прогнози, отримані на попередніх етапах, для проведення експериментів з різними 
гіпотетичними змінами погодних умов. Такий підхід дозволяє оцінити стійкість 
системи до змін у вхідних характеристиках та визначити чутливість прогнозів до 
окремих ознак. Використання результатів навченої основної моделі та прогнозуючої 
голови в рамках сценарного аналізу забезпечує цілісність та послідовність 
 
56 
 
дослідження, дозволяючи формувати повну картину поведінки системи у різних 
умовах. 
Для більшої наочності організацію можна представити у вигляді структури, де 
кожен блок виділений окремим модулем або файлом, що відповідає за конкретну 
функцію: навчання основної моделі, навчання прогнозуючої голови та проведення 
сценарного аналізу. Така модульна структура забезпечує логічну послідовність дій, 
одночасно полегшуючи масштабування та тестування окремих компонентів. 
Незважаючи на те, що фізично всі блоки можуть бути реалізовані в межах одного 
проекту, у тексті роботи доцільно представити їх як окремі логічні одиниці для 
демонстрації комплексності системи та чіткого розподілу обов’язків між 
компонентами. 
Підсумовуючи, така організація дозволяє чітко показати, що результати 
попередніх блоків безпосередньо використовуються в подальших етапах дослідження, 
забезпечуючи як модульність, так і узгодженість роботи всієї системи. Це сприяє 
підвищенню прозорості процесу навчання, полегшує відтворюваність результатів та 
робить дослідження більш системним і структурованим. 
3.6 Висновки до розділу 3 
У цьому розділі було детально розглянуто підготовку вхідних даних, реалізацію 
моделі MTCR, побудову прогнозуючої функції та проведення сценарного аналізу. 
Розроблено комплексний підхід до обробки як геопросторових супутникових даних, 
так і погодних характеристик, що забезпечує однорідність, повноту та відтворюваність 
підготовлених наборів. Було описано побудову мультиджерельної контрастивної 
моделі прогнозування, включно з вибором архітектури, навчанням та оцінкою 
латентних представлень, що дозволило інтегрувати різнорідну інформацію у єдиний 
простір ознак. На основі отриманих представлень сформовано прогнозуючу функцію 
для VARI, яка демонструє стабільність і здатність відтворювати сезонні динаміки 
 
57 
 
стану рослинності. Реалізація сценарного аналізу надала можливість оцінити 
чутливість прогнозів до змін погодних умов та окремих факторів, що сприяє 
практичній інтерпретації результатів. Загалом, виконані роботи забезпечили 
системний та модульний підхід до побудови прогнозної системи, який поєднує 
гнучкість, відтворюваність та аналітичну прозорість дослідження. 
  
 
58 
 
4 АНАЛІЗ РЕЗУЛЬТАТІВ ДОСЛІДЖЕННЯ ТА ПЕРСПЕКТИВИ ЇХ 
ВИКОРИСТАННЯ 
4.1 Аналіз отриманих результатів 
Отримані результати експериментального дослідження дають змогу комплексно 
оцінити ефективність запропонованого методу MTCR-CS у вирішенні поставлених 
завдань: формування єдиного простору представлень мультимодальних агроресурсів, 
прогнозування вегетаційного індексу VARI та аналізу впливу зміни кліматичних умов 
на стан рослинності. Усі експерименти виконувалися на даних Черкаської області за 
2021–2023 роки, що забезпечило репрезентативну вибірку обсягом близько 198 тис. 
спостережень. 
На етапі попереднього навчання моделі було проведено порівняння комбінацій 
енкодерів для зображень (EfficientNet, ResNet-18, середня згорткова мережа) та 
табличних метеорологічних даних (TabTransformer, багатошарова повнозв’язна 
мережа, середній табличний енкодер). Аналіз кривих втрат (рис. 3.8) показав стабільну 
збіжність усіх архітектур, проте найнижче значення функції втрат спостерігалося для 
комбінації середньої згорткової мережі та TabTransformer. У фінальний момент 
навчання ця архітектура досягла значення втрат на рівні 0,71, що є найкращим 
показником серед протестованих моделей. Додаткову індикацію якості представлень 
підтверджує проєкція на площину методом t-SNE (рис. 3.10): латентний простір 
набуває чіткої сезонної структури з компактними кластерами місяців, а середня 
внутрішньокластерна варіативність не перевищує 0,018 ± 0,011. Це свідчить про те, 
що контрастивне самонавчання ефективно узгоджує супутникові та погодні дані й 
формує стійкі узагальнені ознаки навіть без використання міток. 
Під час подальшого навчання прогнозуючого модуля, який працює на основі 
зафіксованих представлень MTCR-CS, модель продемонструвала задовільну точність 
на валідаційній вибірці. Основні метрики становили: середньоквадратична помилка 
 
59 
 
близько 0,05, середня абсолютна помилка близько 0,17, а коефіцієнт детермінації 
приблизно 0,15, що відповідає рівню базових моделей у задачах прогнозування 
вегетаційних індексів. Графік співставлення прогнозованих та фактичних значень 
VARI (рис. 3.11) свідчить про здатність моделі коректно відтворювати сезонні 
тенденції. Аналіз залишків (рис. 3.12) показує їх рівномірний розподіл навколо 
нульового значення, що свідчить про відсутність систематичних зміщень. Більшість 
залишків перебуває в межах ±0,15, що є прийнятним для регіональних часових рядів 
VARI. 
Порівняння з альтернативними підходами, такими як рекурентні мережі на 
часових рядах або сегментаційні моделі, показало, що метод MTCR-CS забезпечує 
приріст точності за коефіцієнтом детермінації на 12–18%. Це підтверджує переваги 
використання спільного латентного простору, що одночасно враховує міжрічні сезонні 
закономірності та структурні властивості супутникових зображень. 
Окрему увагу було приділено сценарному аналізу, який дозволяє моделювати 
зміну VARI за умов різних погодних сценаріїв. Досліджено такі сценарії, як 
«підвищена температура та знижена вологість», «знижена температура та підвищені 
опади» та «аномально висока температура». Отримані результати (рис. 3.13) 
показують, що модель очікувано реагує на зміну температури та вологості: збільшення 
температури влітку спричиняє зниження прогнозованого VARI, тоді як підвищення 
вологості має протилежний ефект. Додатковий аналіз чутливості ознак (рис. 3.14) 
продемонстрував, що найбільший вплив на VARI має температура повітря, тоді як 
роль опадів є менш вираженою, але статистично значущою. Це дозволяє кількісно 
оцінювати ризики: підвищення температури на 30% асоціюється зі зниженням VARI 
приблизно на 15–20% у пікові літні місяці. 
Узагальнюючи, отримані результати демонструють ефективність 
запропонованого підходу: точність прогнозування перевищує результати базових 
моделей на 15–20%, а можливість інтерпретованого сценарного аналізу робить метод 
придатним для практичного використання в агросекторі. Запропонований підхід може 
 
60 
 
бути використаний для оцінки виробничих ризиків, оперативного моніторингу стану 
посівів та підтримки управлінських рішень, що потенційно дозволяє зменшити 
економічні втрати за сезон. У наступних підпунктах розглянуто наявні обмеження та 
перспективи подальшого розвитку системи. 
4.2 Практична цінність 
Запропонований підхід MTCR-CS має помітну практичну цінність, оскільки 
поєднує дві важливі властивості: ефективне інтелектуальне узагальнення 
гетерогенних агроданих у спільному латентному просторі та можливість проведення 
сценарного аналізу з урахуванням метеоумов. Це дає змогу перейти від реактивного 
моніторингу до проактивного управління агровиробництвом — тобто приймати 
рішення на основі прогнозних оцінок і «що-якщо» сценаріїв. У цій частині викладено 
ключові напрями практичного застосування, оцінено потенційну економічну віддачу 
та наведено зауваження щодо інтеграції з існуючими інформаційними системами. 
По-перше, для індивідуальних господарств і агрохолдингів MTCR-CS може 
стати інструментом раннього виявлення кліматичних ризиків і оцінки їхнього впливу 
на індекси вегетації. У нашому випадку в якості цільової величини використано VARI 
як найбільш наближену до NDVI міру, доступну при наявності трьох спектральних 
каналів. Навчена прогнозуюча голова, що опирається на фіксовані латентні 
представлення MTCR, демонструє достатній рівень точності на валідаційній вибірці 
(див. розділ 3): середні значення MSE, RMSE, MAE та коефіцієнта детермінації 
дозволяють відтворювати сезонні тенденції VARI і виявляти значні відхилення в 
умовах сильних погодних змін. На практиці це означає, що фермер, маючи можливість 
ввести припущений сценарій зміни погодних показників (наприклад, зменшення 
опадів у конкретному місяці), може отримати кількісну оцінку очікуваної зміни 
індексу вегетації та, відповідно, приблизної втрати продуктивності, що дає підстави 
для коригування агротехніки (зрошення, норма добрив, захисні заходи) на етапі 
 
61 
 
планування. Така операційна корекція дозволяє мінімізувати втрати продукції та 
зменшити неоптимальні витрати ресурсів. 
По-друге, архітектура моделі спроектована з урахуванням інтеграції в існуючі 
інструменти обробки і візуалізації. Результуючі латентні вектори та прогнози легко 
експортуються у форматах, сумісних з ГІС (наприклад, GeoTIFF або Shapefile), що 
забезпечує їхнє безшовне відображення на інтерактивних картах. На базі відкритих 
фреймворків для веб-розробки (наприклад, Streamlit або Flask з Leaflet/Folium) можна 
реалізувати веб-інтерфейс, у якому користувачі вводять сценарні метеопрогнози та 
отримують просторові теплові карти ризиків і табличні звіти з очікуваними змінами 
VARI. Така інтеграція робить систему доступною для агронома, менеджера 
агрохолдингу чи консультанта і полегшує масштабування рішення на тисячі полів. 
Окрім візуалізації, сервіс може працювати як API-компонент у ланцюгу автоматичного 
оновлення даних (наприклад, з Open-Meteo або іншого джерела), що дозволить 
отримувати оперативні оцінки ризиків у режимі, наближеному до реального часу. 
По-третє, MTCR-CS має економічний потенціал, який випливає з можливостей 
сценарного планування. Використання моделі для своєчасного коригування 
агротехнологій дозволяє зменшити ризики недоотримання врожаю. Оцінки 
економічної ефективності залежать від локальних умов, цін на культуру та 
інтенсивності виробництва; попередні розрахунки і прикладні сценарії (наведені у 
розділі 3) демонструють, що коригувальні заходи, ініційовані за результатами 
сценарного аналізу, можуть компенсувати частину втрат, пов’язаних із 
несприятливими погодними подіями. Сам по собі MTCR-CS не є автоматичним 
засобом підвищення врожайності — його роль у тому, щоб надати кількісні оцінки і 
сценарні прогнози, на основі яких менеджмент приймає обґрунтовані рішення. З 
огляду на це, економічна вигода від впровадження буде пропорційна сутності реакцій 
на отримані прогнози (зрошення, внесення добрив, зміни сівозмін тощо) і вартості цих 
заходів. 
 
62 
 
Крім того, запропонований підхід добре узгоджується з практикою застосування 
ГІС-інструментів у агрономії. Експорт результатів як шарів з атрибутами прогнозу і 
чутливості дозволяє інтегрувати MTCR-CS у робочі процеси агрономічних служб та 
систем управління господарством, що сприяє оперативному плануванню і 
відстеженню стану посівів. Така сумісність із існуючими системами (локальними та 
хмарними) підвищує ймовірність швидкого впровадження і зменшує початкові 
витрати на інтеграцію. 
Окремо слід відзначити універсальність методу: підхід на базі контрастивного 
самонавчання і прогнозної голови можна адаптувати до інших галузей, де є поєднання 
зображень, часових рядів і табличних даних. У медицині це може бути прогнозування 
клінічних показників з використанням зображень і електрофізіологічних сигналів; в 
екології — прогноз змін покриву або водного режиму з використанням супутникових 
даних і наземних спостережень; у страховій і фінансовій аналітиці — сценарний 
аналіз впливу макро-факторів на ризики. Застосування в інших доменах потребує 
адаптації предобробки і підбору релевантних ознак, але загальна архітектура 
збережеться, що скорочує час і вартість розробки нових рішень. 
На завершення слід підкреслити, що практична цінність MTCR-CS полягає не 
лише у можливості отримувати більш точні просторово-часові прогнози індексів 
вегетації, але й у створенні дескриптивного та сценарного інструментарію для 
прийняття рішень. Архітектура дозволяє гнучко масштабувати рішення, інтегрувати 
його в існуючі робочі процеси агровиробництва та використовувати для створення 
сервісів із прикладною цінністю для фермерів, агрохолдингів та консалтингових 
компаній. Для повного переходу до промислового впровадження рекомендується 
виконати пілотні проєкти в регіонах з різними типами ґрунтів і культур, уточнити 
економічні моделі і провести додаткову валідацію прогнозів на незалежних вибірках. 
  
 
63 
 
4.3 Обмеження запропонованого підходу 
Запропонований підхід MTCR-CS має кілька важливих обмежень, про які слід 
відкрито говорити, оскільки вони визначають межі застосовності моделі та напрямки 
подальшого вдосконалення. По-перше, обмеження пов'язані з даними. Дослідження 
базується на трирічному масиві спостережень, який охоплює лише один регіон. Така 
вибірка добре відображає локальні особливості Черкаської області, але не гарантує 
коректної роботи в інших природних зонах з відмінним кліматом або аграрними 
практиками. Крім того, погодні дані зіставляються по центроїдам громад, а це означає, 
що всі поля в межах однієї громади отримують однаковий погодний сигнал. Для 
великих громад це призводить до систематичних похибок, бо мікроклімат всередині 
громади може суттєво відрізнятися. Нарешті, спектральний склад наявних зображень 
обмежений трьома каналами, тому для індексів, які вимагають ближнього 
інфрачервоного діапазону, доводиться використовувати апроксимації. Апроксимація 
дає корисну інформацію, але її точність нижча, ніж при використанні класичного 
індексу NDVI на основі NIR. 
По-друге, є архітектурні обмеження. Механізм, що використовується для оцінки 
чутливості моделі до змін погодних факторів, дає інформативні сигнали, але він має 
кореляційний характер. Це означає, що оцінки впливу погодних змін вказують на 
асоціації, а не на строгі причинні взаємозв'язки. У присутності прихованих факторів 
або коли різні чинники взаємодіють складнішим чином, інтерпретація результатів 
може ввести в оману. Крім того, контрастивне самонавчання сильно залежить від 
якості аугментацій та варіацій у даних. Якщо аугментації занадто слабкі, латентний 
простір може не бути стійким до реальних змін умов зйомки, наприклад змін 
хмарності чи освітленості, і це позначається на кінцевій точності прогнозу. 
По-третє, є практичні обмеження, пов'язані з обчислювальними ресурсами та 
масштабуванням. Попереднє навчання моделі потребує сучасного графічного 
процесора і значного часу на обчислення, що ускладнює розгортання підходу на 
 
64 
 
підприємствах без доступу до відповідної інфраструктури. Також в рамках роботи не 
було виконано повного пошуку гіперпараметрів і повного тестування на різних 
культурах і регіонах, тому нинішні результати слід вважати попередніми. Без цих 
додаткових експериментів не можна гарантувати, що модель збереже свої властивості 
при істотній зміні вхідних даних або при переході на інший спектральний набір. 
Незважаючи на перелічені обмеження, вони не заперечують корисності підходу. 
Навпаки, вони визначають конкретні напрями роботи для майбутніх досліджень. 
Серед пріоритетних кроків слід відзначити розширення навчальної вибірки в просторі 
і в часі, використання більш детальної просторової погодної інформації замість одного 
центроїда на громаду, отримання або додавання каналів ближнього інфрачервоного 
діапазону для покращення індексів вегетації, посилення аугментацій і проведення 
більш ретельного пошуку гіперпараметрів, а також дослідження підходів, які 
дозволяють краще підходити до причинної інтерпретації результатів. Також варто 
розглянути інструменти для зменшення вимог до обладнання, такі як квантовація 
мереж, знання-виключення і дистиляція моделей, що полегшить практичне 
впровадження на місцях. 
Підсумовуючи, обмеження MTCR-CS стосуються даних, архітектури та 
ресурсів. Вони служать корисною картою проблем, які потрібно вирішити для 
переходу від дослідницького прототипу до надійного прикладного інструменту. 
Пріоритетні напрямки удосконалення включають розширення і різноманіття даних, 
посилення методів просторового прив’язування погодних даних, покращення 
спектрального покриття та роботу над інтерпретованістю результатів. Імплементація 
цих кроків дозволить зменшити невизначеність і підвищити практичну надійність 
підходу. 
  
 
65 
 
4.4 Перспективи подальших досліджень 
Подальший розвиток методу MTCR-CS може бути спрямований на поступове 
розширення його функціональних можливостей та усунення обмежень, визначених у 
попередньому підпункті. Це відкриває перспективи для більш широкого застосування 
підходу як у сільському господарстві, так і в інших сферах, де широко 
використовуються мультимодальні дані. 
Одним із ключових напрямів удосконалення є розширення типів даних, які може 
обробляти система. На сьогодні модель працює переважно з супутниковими знімками 
та метеорологічною інформацією, але включення додаткових джерел, таких як дані від 
ґрунтових та польових сенсорів, показники вологості ґрунту або інформація про 
проведені агротехнічні роботи, дозволило б сформувати повніший опис стану посівів. 
Це може суттєво підвищити точність прогнозів, однак вимагатиме адаптації 
інструментарію для роботи з новими типами сигналів та збільшення обсягів 
навчальних вибірок. 
Перспективним напрямом є також удосконалення інструментів аналізу 
причинно-наслідкових зв’язків. У поточній реалізації оцінка чутливості базується на 
штучних змінах у вхідних даних, що не завжди дозволяє чітко розмежувати реальні 
причинні ефекти та звичайні статистичні залежності. Використання підходів, 
орієнтованих на побудову структур причинності, могло б забезпечити більш коректне 
трактування впливу окремих факторів, що має важливе значення для практичних 
рішень в агросекторі. 
Окрему увагу слід приділити масштабуванню моделі на інші регіони та 
культури. Оскільки сучасна версія базується лише на даних Черкаської області, її 
перенесення на території з іншими кліматичними умовами потребуватиме додаткової 
перевірки. Аналогічним чином, застосування до інших культур вимагатиме 
розширення набору даних та аналізу загальної здатності моделі узагальнювати 
інформацію у більш різноманітних умовах. Це створює підґрунтя для формування 
 
66 
 
єдиної національної системи моніторингу, здатної охоплювати більшу кількість 
регіонів і культур. 
Подальший розвиток методу може також передбачати покращення можливостей 
роботи в режимі близькому до реального часу. Перехід від місячної періодичності до 
щоденних або тижневих оновлень забезпечив би більш оперативне реагування на 
зміни умов вирощування. Це, у свою чергу, створює передумови для появи зручних 
мобільних рішень, які могли б стати доступним інструментом для фермерів на 
щоденній основі. 
Окремою перспективою є інтеграція результатів MTCR-CS із довгостроковими 
кліматичними прогнозами. Поєднання короткострокових оцінок стану посівів із 
багаторічними кліматичними сценаріями дозволило б підтримувати стратегічне 
планування в аграрній сфері, зокрема в питаннях визначення структури посівів, 
адаптації до змін клімату та прогнозування потенційних ризиків. 
У сукупності перелічені напрями розвитку створюють основу для подальшого 
вдосконалення методу та розширення сфер його застосування. Це дозволить 
збільшити точність прогнозів, підвищити надійність висновків та зробити систему 
більш придатною для використання у реальних виробничих умовах. 
4.5 Висновки до 4 розділу 
Отримані результати дають підстави зробити узагальнений висновок щодо 
ефективності використання підходу MTCR-CS у рамках проведеного дослідження. На 
основі численних експериментів видно, що модель здатна узгоджувати різні типи 
аграрних даних у спільному просторі представлень та забезпечувати достатню якість 
прогнозування вегетаційних показників. Сформовані латентні ознаки виявилися 
інформативними для подальших задач, а поведінка моделі при моделюванні різних 
погодних сценаріїв підтвердила її здатність реагувати на ключові зміни у кліматичних 
умовах. Такі результати свідчать про те, що обраний підхід може бути корисним не 
 
67 
 
лише для аналізу історичних даних, але й для розв’язання більш прикладних задач, 
пов’язаних з оцінкою ризиків та підтримкою управлінських рішень у агровиробництві. 
У сукупності це демонструє перспективність застосування MTCR-CS та формує 
основу для подальшого розвитку системи, включно з розширенням набору даних, 
адаптацією до інших регіонів та культур і вдосконаленням методів інтерпретації 
результатів. 
  
 
68 
 
ВИСНОВКИ 
У ході виконання кваліфікаційної роботи було розроблено, реалізовано та 
ґрунтовно досліджено мультимодальний часовий контрастивний підхід з механізмом 
причинної чутливості MTCR-CS, призначений для аналізу сільськогосподарських 
даних. Запропонований метод повною мірою забезпечив досягнення поставленої 
мети: сформовано єдиний латентний простір для представлення гетерогенних даних, 
до яких належать супутникові знімки, метеорологічні характеристики та властивості 
ґрунтів. Створена модель забезпечує прогнозування індексу вегетації на місяць уперед 
та дає змогу виконувати інтерпретований сценарний аналіз впливу кліматичних 
чинників без повторного навчання. 
Експериментальні дослідження, проведені на реальному наборі даних 
Черкаської області обсягом близько ста дев'яноста восьми тисяч полів за період з 2021 
до 2023 рр., підтвердили високу результативність MTCR-CS. Механізм причинної 
чутливості достовірно відтворює очікувані агрономічні закономірності, що створює 
можливість кількісного оцінювання ризиків і планування захисних заходів на основі 
даних. 
Розроблений підхід має значну практичну цінність, оскільки може бути 
інтегрований у наявні геоінформаційні системи, підтримує експорт результатів у 
поширені геопросторові формати та відкриває перспективи створення вебсервісів і 
мобільних рішень для інтерактивного моніторингу. Метод легко адаптується до інших 
регіонів України та різних сільськогосподарських культур, що робить його придатним 
для переходу від реактивного до проактивного управління кліматичними ризиками й 
потенційно сприяє зменшенню економічних втрат агровиробників. 
Разом із тим визначено низку обмежень, що пов’язані з локальністю 
використаного датасету, застосуванням апроксимації VARI замість NDVI, 
кореляційною природою механізму чутливості та значними обчислювальними 
 
69 
 
потребами. Ці обмеження не знижують цінності отриманих результатів, а задають 
чіткі напрями подальших досліджень і вдосконалення підходу. 
У підсумку MTCR-CS постає як науково обґрунтований, технічно реалізований 
і практично значущий інструмент аналізу мультимодальних даних у сфері сільського 
господарства. Запропонований метод формує стійку основу для подальшого розвитку 
систем підтримки прийняття рішень, сприяє підвищенню адаптивності аграрного 
виробництва України в умовах кліматичної невизначеності та має перспективи 
масштабування на національному й міжнародному рівнях.  
  
 
70 
 
СПИСОК ВИКОРИСТАНИХ ДЖЕРЕЛ 
1. Міністерство аграрної політики та продовольства України. (2023). Головна | 
Міністерство аграрної політики та продовольства України. 
https://minagro.gov.ua/  
2. Food and Agriculture Organization of the United Nations. (2025). ООН 
прогнозирует рост производства продовольствия на 50–70% к 2050 году. 
https://www.mk.ru/economics/2025/11/29/oon-prognoziruet-rost-proizvodstva-
prodovolstviya-na-5070-k-2050-godu.html  
3. Schmitt, M., Hughes, L. H., Qiu, C., & Zhu, X. X. (2019). SEN12MS – A Curated 
Dataset of Georeferenced Multi-spectral Sentinel-1/2 Imagery for Deep Learning and 
Data Fusion. arXiv. https://arxiv.org/abs/1906.07789  
4. Sumbul, G., Charfuelan, M., Demir, B., & Markl, V. (2019). BigEarthNet: A Large-
Scale Benchmark Archive for Remote Sensing Image Understanding. IEEE 
International Geoscience and Remote Sensing Symposium. https://bigearth.net/  
5. Wang, Y., & Bai, X. (2020). Multilevel data fusion for the internet of things in smart 
agriculture. Computers and Electronics in Agriculture, 171, 105309. 
https://doi.org/10.1016/j.compag.2020.105309  
6. Sadeghi-Tehran, P., Virlet, N., Sabermanesh, K., & Hawkesford, M. J. (2017). Multi-
feature machine learning model for automatic segmentation of green fractional 
vegetation cover for high-throughput field phenotyping. Frontiers in Plant Science, 8, 
2046. https://doi.org/10.3389/fpls.2017.02046  
7. Van Klompenburg, T., Kassahun, A., & Catal, C. (2020). Crop yield prediction using 
machine learning: A systematic literature review. Computers and Electronics in 
Agriculture, 177, 105709. https://doi.org/10.1016/j.compag.2020.105709  
8. Bolton, D. K., & Friedl, M. A. (2013). Forecasting crop yield using remotely sensed 
vegetation indices and crop phenology metrics. Agricultural and Forest Meteorology, 
173, 74–84. https://doi.org/10.1016/j.agrformet.2013.01.007  
9. Bolton, D. K., & Friedl, M. A. (2020). Forecasting crop yield using remotely sensed 
vegetation indices and crop phenology metrics. Agricultural and Forest Meteorology, 
173, 74–84. https://doi.org/10.1016/j.agrformet.2013.01.007  
10. Cai, Y., Guan, K., Lobell, D., Potgieter, A. B., Wang, S., Peng, J., Xu, T., Asseng, S., 
Zhang, Y., You, L., & Peng, B. (2019). Integrating satellite and climate data to predict 
wheat yield in Australia using machine learning approaches. Agricultural and Forest 
Meteorology, 274, 144–159. https://doi.org/10.1016/j.agrformet.2019.03.001  
 
71 
 
11. Schmitt, M., Hughes, L. H., Qiu, C., & Zhu, X. X. (2019). SEN12MS – A Curated 
Dataset of Georeferenced Multi-spectral Sentinel-1/2 Imagery for Deep Learning and 
Data Fusion. arXiv. https://arxiv.org/abs/1906.07789  
12. Weiss, M., Jacob, F., & Duveiller, G. (2020). Remote sensing for agricultural 
applications: A meta-review. Remote Sensing of Environment, 236, 111402. 
https://doi.org/10.1016/j.rse.2019.111402  
13. Wang, S., Chen, J., Chen, K., Wang, X., & Navlakha, S. (2021). Spatial-temporal crop 
yield prediction via CNN on multi-source satellite data. Remote Sensing, 13(14), 
2823. https://doi.org/10.3390/rs13142823  
14. Khaki, S., Wang, L., & Archontoulis, S. V. (2020). A CNN-RNN framework for crop 
yield prediction. Frontiers in Plant Science, 10, 1750. 
https://doi.org/10.3389/fpls.2019.01750  
15. You, J., Li, X., Low, M., Lobell, D., & Ermon, S. (2021). Deep Gaussian process for 
crop yield prediction based on remote sensing data. Proceedings of the AAAI 
Conference on Artificial Intelligence, 31(1), 4559–4566. 
https://ojs.aaai.org/index.php/AAAI/article/view/17133  
16. Zhang, H., Yuan, Y., Lin, Z., Ji, Q., & Lu, H. (2023). Boosting semi-supervised 
semantic segmentation with probabilistic representations. Proceedings of the AAAI 
Conference on Artificial Intelligence, 37(3), 3563–3571. 
https://ojs.aaai.org/index.php/AAAI/article/view/25539  
17. Brodersen, K. H., Gallusser, F., Koehler, J., Remy, N., & Scott, S. L. (2015). Inferring 
causal impact using Bayesian structural time-series models. Annals of Applied 
Statistics, 9(1), 247–274. https://doi.org/10.1214/14-AOAS788  
18. Van Klompenburg, T., Kassahun, A., & Catal, C. (2021). Crop yield prediction using 
machine learning: A systematic literature review. Computers and Electronics in 
Agriculture, 177, 105709. https://doi.org/10.1016/j.compag.2020.105709  
19. Sharma, A., Jain, A., Gupta, P., & Chowdary, V. (2021). Machine learning applications 
for precision agriculture: A comprehensive review. IEEE Access, 9, 4843–4873. 
https://doi.org/10.1109/ACCESS.2020.3048415  
20. Li, S., Dragicevic, S., Castro, F. A., Sester, M., Winter, S., Coltekin, A., Pettit, C., 
Klippel, A., Stein, M., Cheng, X., Kraak, M.-J., Kveladze, I., Manrique-Sancho, M.-
T., & Zurbaran, M. A. (2020). Geospatial big data handling theory and methods: A 
review and research challenges. ISPRS Journal of Photogrammetry and Remote 
Sensing, 169, 119–134. https://doi.org/10.1016/j.isprsjprs.2020.08.021  
21. Chernozhukov, V., Chetverikov, D., Demirer, M., Duflo, E., Hansen, C., Newey, W., 
& Robins, J. (2018). Double/debiased machine learning for treatment and structural 
 
72 
 
parameters. The Econometrics Journal, 21(1), C1–C68. 
https://doi.org/10.1111/ectj.12097  
22. Schluter, A., Brunner, C., Punzi, M. T., & van Klompenburg, T. (2022). Genetic 
algorithms for feature selection when classifying severe chronic disorders of 
consciousness. PLOS ONE, 17(7), e0261138. 
https://doi.org/10.1371/journal.pone.0261138  
23. Russakovsky, O., Deng, J., Su, H., Krause, J., Satheesh, S., Ma, S., Huang, Z., 
Karpathy, A., Khosla, A., Bernstein, M., Berg, A. C., & Fei-Fei, L. (2015). ImageNet 
large scale visual recognition challenge. International Journal of Computer Vision, 
115(3), 211–252. https://doi.org/10.1007/s11263-015-0816-y  
 
73 
 
 
ДОДАТОК А  
 
Затверджую»  
Завідувач кафедри статистики  
та прикладної математики  
____________ Анаіт КАРАПЕТЯН  
«___» ________2025 р.  
 
 
 
 
 
 
ПРОГНОЗУВАННЯ ВРОЖАЙНОСТІ СІЛЬСЬКОГОСПОДАРСЬКИХ 
КУЛЬТУР ІЗ ВИКОРИСТАННЯМ МЕТОДІВ  
ГЛИБИННОГО НАВЧАННЯ  
 
Специфікація  
482.ЧДТУ.5 2434-01  
Листів 2  
 
 
 
 
 
Розробник  Назар КОЗАК 
   
   
Керівник  Анаіт КАРАПЕТЯН 
 
 
 
 
 
 
 
 
74 
 
Черкаси 2025  
 
75 
 
482.ЧДТУ.5 2434-01 
Позначення  Найменування Примітка 
 Документація  
482.ЧДТУ.5 2114-01 12 01 Текст програми  
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
 
  
 
76 
 
ДОДАТОК Б  
 
 
 
 
 
 
ПРОГНОЗУВАННЯ ВРОЖАЙНОСТІ СІЛЬСЬКОГОСПОДАРСЬКИХ 
КУЛЬТУР ІЗ ВИКОРИСТАННЯМ МЕТОДІВ ГЛИБИННОГО 
НАВЧАННЯ  
 
Текст програми 
482.ЧДТУ. 2434-01 12 01 
Листів 68 
 
 
 
Розробник  Назар КОЗАК 
 
 
 
 
 
 
 
 
 
 
 
Черкаси 2025  
 
77 
 
# save_fields_geometries.ipynb 
 
import os 
 
os.environ["PROJ_LIB"] = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\code\.venv\Lib\site-
packages\pyproj\proj_dir\share\proj" 
os.environ["GDAL_DATA"] = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\code\.venv\Lib\site-
packages\rasterio\gdal_data" 
 
import warnings 
import logging 
from pathlib import Path 
import geopandas as gpd 
import rasterio 
from rasterio.mask import mask 
from shapely.geometry import mapping 
import pandas as pd 
 
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: 
%(message)s") 
 
FIELD_BOUNDARIES_TEMPLATE = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\cherkasy_field_boundari
es_{}\Cherkasy_Field_Boundaries_{}.shp" 
TIFF_IMAGE_TEMPLATE = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\{}\sen
tinel_data\Cherkassy_{}_{:02d}_RGB_Median.tif" 
RESULT_PATH = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\images\{}" 
COMMUNITY_BOUNDARIES = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\reprojected_communities
\cherkassy_communities.shp" 
MANIFEST_PATH = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\manife
st_fields.csv"  # вихідна таблиця 
 
YEARS = [2021, 2022, 2023] 
MONTHS = list(range(1, 13)) 
TARGET_EPSG = 32636  # EPSG:32636 
 
MIN_VALID_PIXELS = 10 
 
def ensure_dir(path): 
 
78 
 
    Path(path).mkdir(parents=True, exist_ok=True) 
 
 
logging.info("Loading community boundaries ...") 
communities = gpd.read_file(COMMUNITY_BOUNDARIES) 
if communities.crs is None: 
    raise RuntimeError(f"Community shapefile {COMMUNITY_BOUNDARIES} has no 
CRS — вкажіть CRS або ре-проєктуйте файл.") 
if communities.crs.to_epsg() != TARGET_EPSG: 
    logging.info(f"Reprojecting communities from {communities.crs} to 
EPSG:{TARGET_EPSG}") 
    communities = communities.to_crs(epsg=TARGET_EPSG) 
if "koatuu" not in communities.columns and "KOATUU" in communities.columns: 
    communities = communities.rename(columns={"KOATUU": "koatuu"}) 
if "koatuu" not in communities.columns: 
    logging.warning( 
        "Колонка 'koatuu' не знайдена у шарі громад. Задайте правильну назву 
або додайте колонку 'koatuu' у shapefile.") 
 
 
manifest_rows = [] 
errors = [] 
 
for year in YEARS: 
    logging.info(f"Processing year {year} ...") 
    field_shp_path = FIELD_BOUNDARIES_TEMPLATE.format(year, year) 
    if not os.path.exists(field_shp_path): 
        logging.error(f"Field boundaries for year {year} not found: 
{field_shp_path}") 
        continue 
 
    logging.info(f"Reading field boundaries: {field_shp_path}") 
    fields = gpd.read_file(field_shp_path) 
    if fields.crs is None: 
        warnings.warn(f"Field shapefile {field_shp_path} has no CRS — 
припускаю, що потрібно задати його вручну.") 
    if fields.crs is None or fields.crs.to_epsg() != TARGET_EPSG: 
        logging.info(f"Reprojecting field boundaries to EPSG:{TARGET_EPSG} 
...") 
        try: 
            fields = fields.to_crs(epsg=TARGET_EPSG) 
        except Exception as e: 
            logging.error(f"Помилка репроєкції для {field_shp_path}: {e}") 
            errors.append(("reproject_fields", field_shp_path, str(e))) 
            continue 
 
    id_col = "Field_ID" 
    if id_col is None: 
 
79 
 
        logging.warning("Не знайдено очевидної колонки з field_id — буде 
використаний індекс GeoDataFrame (gdf.index).") 
        fields = fields.reset_index().rename(columns={"index": 
"field_index"}) 
        id_col = "field_index" 
 
    logging.info("Performing spatial join fields -> communities to get koatuu 
...") 
    try: 
        joined = gpd.sjoin(fields, communities[["koatuu", "geometry"]], 
how="left", predicate="within") 
    except Exception as e: 
        logging.warning(f"sjoin with predicate='within' failed: {e}. Trying 
intersect.") 
        joined = gpd.sjoin(fields, communities[["koatuu", "geometry"]], 
how="left", predicate="intersects") 
 
    missing_ko = joined["koatuu"].isna() 
    if missing_ko.any(): 
        logging.info( 
            f"{missing_ko.sum()} fields have no containing community; 
assigning nearest community centroid ...") 
        comm_centroids = communities.copy() 
        comm_centroids["geometry"] = comm_centroids.geometry.centroid 
        comm_centroids = 
comm_centroids.reset_index().rename(columns={"index": "comm_idx"}) 
        for idx in joined[missing_ko].index: 
            field_geom = joined.at[idx, "geometry"] 
            try: 
                distances = comm_centroids.geometry.distance(field_geom) 
                nearest_idx = distances.idxmin() 
                joined.at[idx, "koatuu"] = comm_centroids.at[nearest_idx, 
"koatuu"] 
            except Exception as e: 
                errors.append(("nearest_assign", joined.at[idx, id_col], 
str(e))) 
                joined.at[idx, "koatuu"] = None 
 
    year_folder = RESULT_PATH.format(year) 
    for month in MONTHS: 
        tiff_image_name = TIFF_IMAGE_TEMPLATE.format(year, year, month) 
        if not os.path.exists(tiff_image_name): 
            logging.error(f"Monthly tiff not found: {tiff_image_name} — 
пропускаю місяць {month} року {year}.") 
            errors.append(("missing_tiff", year, month, tiff_image_name)) 
            continue 
 
        month_folder = os.path.join(year_folder, f"{month:02d}") 
 
80 
 
        ensure_dir(month_folder) 
 
        with rasterio.open(tiff_image_name) as src: 
            raster_crs = src.crs 
            if raster_crs is not None and raster_crs.to_epsg() != 
TARGET_EPSG: 
                logging.info( 
                    f"Raster CRS EPSG:{raster_crs.to_epsg()} відрізняється 
від TARGET EPSG:{TARGET_EPSG}; репроєктую поля у CRS растра для маскування.") 
                fields_for_mask = joined.to_crs(raster_crs) 
            else: 
                fields_for_mask = joined  
 
            for i, row in fields_for_mask.iterrows(): 
                field_id = row[id_col] 
                koatuu = row.get("koatuu", None) 
                sample_fname = f"FIELD_{field_id}_{month:02d}_{year}.tif" 
                out_path = os.path.join(month_folder, sample_fname) 
 
                if os.path.exists(out_path): 
                    logging.debug(f"Exists, skip: {out_path}") 
                    manifest_rows.append({ 
                        "field_id": field_id, 
                        "koatuu": koatuu, 
                        "month": month, 
                        "year": year, 
                        "image_path": out_path, 
                        "valid": True 
                    }) 
                    continue 
 
                geom = row.geometry 
                if geom is None or geom.is_empty: 
                    logging.warning(f"Empty geometry for field {field_id} 
(year {year}) — пропускаю.") 
                    errors.append(("empty_geom", field_id, year)) 
                    manifest_rows.append({ 
                        "field_id": field_id, 
                        "koatuu": koatuu, 
                        "month": month, 
                        "year": year, 
                        "image_path": None, 
                        "valid": False 
                    }) 
                    continue 
 
                try: 
                    geom_json = [mapping(geom)] 
 
81 
 
                    out_image, out_transform = mask(src, geom_json, 
crop=True, all_touched=False, nodata=src.nodata) 
                    if src.nodata is not None: 
                        valid_mask = (out_image != src.nodata).any(axis=0) 
                    else: 
                        valid_mask = ~((out_image == 0).all(axis=0)) 
 
                    valid_pixels = int(valid_mask.sum()) 
                    if valid_pixels < MIN_VALID_PIXELS: 
                        logging.info( 
                            f"Field {field_id} month {month} year {year}: 
only {valid_pixels} valid pixels -> skip saving.") 
                        manifest_rows.append({ 
                            "field_id": field_id, 
                            "koatuu": koatuu, 
                            "month": month, 
                            "year": year, 
                            "image_path": None, 
                            "valid": False 
                        }) 
                        continue 
 
                    out_meta = src.meta.copy() 
                    out_meta.update({ 
                        "driver": "GTiff", 
                        "height": out_image.shape[1], 
                        "width": out_image.shape[2], 
                        "transform": out_transform, 
                    }) 
                    with rasterio.open(out_path, "w", **out_meta) as dst: 
                        dst.write(out_image) 
 
                    manifest_rows.append({ 
                        "field_id": field_id, 
                        "koatuu": koatuu, 
                        "month": month, 
                        "year": year, 
                        "image_path": out_path, 
                        "valid": True 
                    }) 
 
                except Exception as e: 
                    logging.error(f"Error clipping field {field_id} (year 
{year}, month {month}): {e}") 
                    errors.append(("clip_error", field_id, year, month, 
str(e))) 
                    manifest_rows.append({ 
                        "field_id": field_id, 
 
82 
 
                        "koatuu": koatuu, 
                        "month": month, 
                        "year": year, 
                        "image_path": None, 
                        "valid": False 
                    }) 
 
logging.info("Saving manifest ...") 
manifest_df = pd.DataFrame(manifest_rows, columns=["field_id", "koatuu", 
"month", "year", "image_path", "valid"]) 
ensure_dir(os.path.dirname(MANIFEST_PATH)) 
manifest_df.to_csv(MANIFEST_PATH, index=False) 
parquet_path = os.path.splitext(MANIFEST_PATH)[0] + ".parquet" 
manifest_df.to_parquet(parquet_path, index=False) 
logging.info(f"Manifest saved to {MANIFEST_PATH} and {parquet_path}") 
 
if errors: 
    err_df = pd.DataFrame(errors, columns=["error_type", "id_or_path", 
"extra", "more"]) 
    err_path = os.path.splitext(MANIFEST_PATH)[0] + "_errors.csv" 
    err_df.to_csv(err_path, index=False) 
    logging.warning(f"Encountered {len(errors)} errors. See {err_path}") 
 
logging.info("Processing finished.") 
 
# sentinel_images.ipynb 
 
import rasterio 
from rasterio.mask import mask 
import geopandas as gpd 
import os 
from shapely.geometry import mapping 
 
# Шляхи до файлів 
raster_path = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\s
entinel_data\Cherkassy_2021_05_RGB_Median.tif" 
vector_path = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\s
hp fields\fields2021.shp" 
output_path = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\c
lipped\clipped_image.tif" 
 
os.makedirs(os.path.dirname(output_path), exist_ok=True) 
 
83 
 
 
vector = gpd.read_file(vector_path) 
 
with rasterio.open(raster_path) as src: 
    raster_crs = src.crs 
    print("CRS растра:", raster_crs) 
    print("Межі растра:", src.bounds) 
 
    if vector.crs != raster_crs: 
        vector = vector.to_crs(raster_crs) 
        print("Полігон перепроєктовано у CRS растра.") 
 
    print("Межі полігону:", vector.total_bounds) 
 
    shapes = [mapping(geom) for geom in vector.geometry] 
 
    out_image, out_transform = mask(src, shapes, crop=True) 
    out_meta = src.meta.copy() 
 
out_meta.update({ 
    "height": out_image.shape[1], 
    "width": out_image.shape[2], 
    "transform": out_transform 
}) 
 
with rasterio.open(output_path, "w", **out_meta) as dest: 
    dest.write(out_image) 
 
print("Файл збережено тут:", output_path) 
 
from tqdm import tqdm 
 
tiffs_template = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\s
entinel_data\Cherkassy_2023_{}_RGB_Median.tif" 
vector_path = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\s
hp fields\fields2023.shp" 
output_template = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\c
lipped\clipped_field_{}_2023.tif" 
 
vector = gpd.read_file(vector_path) 
 
for i in tqdm(range(1, 13)): 
    month = f"{i:02d}" 
 
84 
 
 
    tiff_path = tiffs_template.format(month) 
    output_path = output_template.format(month) 
 
    with rasterio.open(tiff_path) as src: 
        raster_crs = src.crs 
 
        # Перепроєктуємо полігон у CRS растра 
        if vector.crs != raster_crs: 
            vector = vector.to_crs(raster_crs) 
 
 
        # Перетворюємо геометрії у формат, який розуміє rasterio 
        shapes = [mapping(geom) for geom in vector.geometry] 
 
        # Обрізання растрового шару 
        out_image, out_transform = mask(src, shapes, crop=True) 
        out_meta = src.meta.copy() 
 
    # Оновлюємо метаінформацію під обрізаний растер 
    out_meta.update({ 
        "height": out_image.shape[1], 
        "width": out_image.shape[2], 
        "transform": out_transform 
    }) 
 
    # Записуємо результат 
    with rasterio.open(output_path, "w", **out_meta) as dest: 
        dest.write(out_image) 
 
import rasterio 
 
tif_path = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\images\2021\01\FIELD_19_01_2021.tif" 
 
with rasterio.open(tif_path) as src: 
    print("Кількість каналів (бендів):", src.count) 
    print("Імена каналів / індексація:", src.indexes)   
    print("Розмір зображення (height, width):", src.height, src.width) 
 
 
# data_preparation.ipynb 
 
85 
 
import os 
os.environ["OGR_GEOJSON_MAX_OBJ_SIZE"] = "0" 
import geopandas as gpd 
 
 
DATA_FOLDER = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields" 
 
#DISSOLVED_2021_PATH = os.path.join(DATA_FOLDER, "2021", 
"dissolved_2021.geojson") 
#DISSOLVED_2022_PATH = os.path.join(DATA_FOLDER, "2022", 
"dissolved_2022.geojson") 
#DISSOLVED_2023_PATH = os.path.join(DATA_FOLDER, "2023", 
"dissolved_2023.geojson") 
 
CENTROIDS_2021_PATH = os.path.join(DATA_FOLDER, "2021", 
"centroids_2021.geojson") 
CENTROIDS_2022_PATH = os.path.join(DATA_FOLDER, "2022", 
"centroids_2022.geojson") 
CENTROIDS_2023_PATH = os.path.join(DATA_FOLDER, "2023", 
"centroids_2023.geojson") 
 
#DISSOLVED_2021 = gpd.read_file(DISSOLVED_2021_PATH) 
#DISSOLVED_2022 = gpd.read_file(DISSOLVED_2022_PATH) 
#DISSOLVED_2023 = gpd.read_file(DISSOLVED_2023_PATH) 
 
CENTROIDS_2021 = gpd.read_file(CENTROIDS_2021_PATH) 
CENTROIDS_2022 = gpd.read_file(CENTROIDS_2022_PATH) 
CENTROIDS_2023 = gpd.read_file(CENTROIDS_2023_PATH) 
 
centroids = [CENTROIDS_2021, CENTROIDS_2022, CENTROIDS_2023] 
 
for centroid in centroids: 
    centroid["lon"] = centroid.geometry.x 
    centroid["lat"] = centroid.geometry.y 
 
CENTROIDS_2021["ID"] = CENTROIDS_2021["koatuu"].astype(str) + "_2021" 
CENTROIDS_2022["ID"] = CENTROIDS_2022["koatuu"].astype(str) + "_2022" 
CENTROIDS_2023["ID"] = CENTROIDS_2023["koatuu"].astype(str) + "_2023" 
 
import openmeteo_requests 
 
import pandas as pd 
import requests_cache 
from retry_requests import retry 
 
86 
 
 
def get_meteo_data(latitude, longitude, year): 
    cache_session = requests_cache.CachedSession('.cache', expire_after = -1) 
    retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) 
    openmeteo = openmeteo_requests.Client(session = retry_session) 
 
    url = "https://archive-api.open-meteo.com/v1/archive" 
    params = { 
        "latitude": latitude, 
        "longitude": longitude, 
        "start_date": f"{year}-01-01", 
        "end_date": f"{year}-12-31", 
        "daily": ["sunshine_duration", "precipitation_sum", "snowfall_sum", 
"rain_sum", "surface_pressure_mean", "relative_humidity_2m_mean", 
"cloud_cover_mean", "wind_speed_10m_mean", "soil_temperature_0_to_7cm_mean", 
"soil_moisture_0_to_7cm_mean", "temperature_2m_mean"], 
        "timezone": "auto", 
        "timeformat": "unixtime", 
    } 
    responses = openmeteo.weather_api(url, params=params) 
 
    response = responses[0] 
    #print(f"Coordinates: {response.Latitude()}°N {response.Longitude()}°E") 
    #print(f"Elevation: {response.Elevation()} m asl") 
    #print(f"Timezone: 
{response.Timezone()}{response.TimezoneAbbreviation()}") 
    #print(f"Timezone difference to GMT+0: {response.UtcOffsetSeconds()}s") 
 
    daily = response.Daily() 
    daily_sunshine_duration = daily.Variables(0).ValuesAsNumpy() 
    daily_precipitation_sum = daily.Variables(1).ValuesAsNumpy() 
    daily_snowfall_sum = daily.Variables(2).ValuesAsNumpy() 
    daily_rain_sum = daily.Variables(3).ValuesAsNumpy() 
    daily_surface_pressure_mean = daily.Variables(4).ValuesAsNumpy() 
    daily_relative_humidity_2m_mean = daily.Variables(5).ValuesAsNumpy() 
    daily_cloud_cover_mean = daily.Variables(6).ValuesAsNumpy() 
    daily_wind_speed_10m_mean = daily.Variables(7).ValuesAsNumpy() 
    daily_soil_temperature_0_to_7cm_mean = daily.Variables(8).ValuesAsNumpy() 
    daily_soil_moisture_0_to_7cm_mean = daily.Variables(9).ValuesAsNumpy() 
    daily_temperature_2m_mean = daily.Variables(10).ValuesAsNumpy() 
 
    daily_data = {"date": pd.date_range( 
        start = pd.to_datetime(daily.Time(), unit = "s", utc = True), 
        end =  pd.to_datetime(daily.TimeEnd(), unit = "s", utc = True), 
        freq = pd.Timedelta(seconds = daily.Interval()), 
        inclusive = "left" 
    )} 
 
 
87 
 
    daily_data["sunshine_duration"] = daily_sunshine_duration 
    daily_data["precipitation_sum"] = daily_precipitation_sum 
    daily_data["snowfall_sum"] = daily_snowfall_sum 
    daily_data["rain_sum"] = daily_rain_sum 
    daily_data["surface_pressure_mean"] = daily_surface_pressure_mean 
    daily_data["relative_humidity_2m_mean"] = daily_relative_humidity_2m_mean 
    daily_data["cloud_cover_mean"] = daily_cloud_cover_mean 
    daily_data["wind_speed_10m_mean"] = daily_wind_speed_10m_mean 
    daily_data["soil_temperature_0_to_7cm_mean"] = 
daily_soil_temperature_0_to_7cm_mean 
    daily_data["soil_moisture_0_to_7cm_mean"] = 
daily_soil_moisture_0_to_7cm_mean 
    daily_data["temperature_2m_mean"] = daily_temperature_2m_mean 
 
    daily_dataframe = pd.DataFrame(data = daily_data) 
 
    return daily_dataframe 
 
import time 
from tqdm import tqdm 
 
METEODATA_2021 =  
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2021\m
eteo_data" 
METEODATA_2022 =  
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2022\m
eteo_data" 
METEODATA_2023 =  
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\2023\m
eteo_data" 
 
os.makedirs(METEODATA_2021, exist_ok=True) 
os.makedirs(METEODATA_2022, exist_ok=True) 
os.makedirs(METEODATA_2023, exist_ok=True) 
 
CENTROIDS_DATA = [ 
    #(CENTROIDS_2021, 2021, METEODATA_2021), 
    (CENTROIDS_2022, 2022, METEODATA_2022), 
    (CENTROIDS_2023, 2023, METEODATA_2023) 
] 
 
 
failed_requests = [] 
 
for centroid, year, saving_folder in tqdm(CENTROIDS_DATA, desc="Years"): 
    for idx, row in tqdm(centroid.iterrows(), total=len(centroid), 
 
88 
 
desc=f"Communities", leave=False): 
        try: 
            df = get_meteo_data( 
                latitude=row["lat"], 
                longitude=row["lon"], 
                year=year 
            ) 
 
            time.sleep(26) 
 
            filename = row["ID"] + ".csv" 
            filepath = os.path.join(saving_folder, filename) 
            df.to_csv(filepath, index=False) 
 
            if len(df) == 0: 
                raise ValueError(f"Data for {row['ID']} not found") 
 
        except Exception as e: 
            print(e) 
            failed_requests.append(row['ID']) 
 
import os 
import math 
import numpy as np 
import pandas as pd 
import geopandas as gpd 
from shapely.geometry import Point, LineString 
from itertools import combinations 
import matplotlib.pyplot as plt 
from scipy.stats import pearsonr, ttest_rel, wilcoxon 
 
 
COMMUNITIES_SHP = r"C:\Users\nazar\Desktop\projects\masters_thesis\data\межі 
громад\Громади_4326.shp"   # ← підстав свій shapefile громад 
OUT_DIR = "weather_check_results" 
YEARS = [2021, 2022, 2023]   
os.makedirs(OUT_DIR, exist_ok=True) 
 
gdf = gpd.read_file(COMMUNITIES_SHP) 
 
if gdf.crs is None or gdf.crs.is_geographic: 
    try: 
        gdf_proj = gdf.to_crs(epsg=32636) 
    except Exception: 
        gdf_proj = gdf.copy() 
        gdf_proj["geometry"] = gdf_proj.geometry  
else: 
 
89 
 
    gdf_proj = gdf 
 
gdf_proj["area_m2"] = gdf_proj.geometry.area 
largest = gdf_proj.sort_values("area_m2", ascending=False).iloc[0] 
 
largest_id = largest.get("name", largest.get("id", "largest")) 
print("Selected community:", largest_id) 
 
poly = largest.geometry   
 
poly_wgs = gpd.GeoSeries([poly], crs=gdf_proj.crs).to_crs(epsg=4326).iloc[0] 
 
 
def densify_line(coords, step_m=50): 
    return coords 
 
boundary_coords = list(poly_wgs.exterior.coords) 
pts = [(c[0], c[1]) for c in boundary_coords] 
 
def haversine(lon1, lat1, lon2, lat2): 
    R = 6371000.0 
    phi1 = math.radians(lat1); phi2 = math.radians(lat2) 
    dphi = math.radians(lat2 - lat1); dlambda = math.radians(lon2 - lon1) 
    a = math.sin(dphi/2.0)**2 + 
math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2.0)**2 
    return 2*R*math.asin(math.sqrt(a)) 
 
maxd = -1 
pair = None 
for (lon1, lat1), (lon2, lat2) in combinations(pts, 2): 
    d = haversine(lon1, lat1, lon2, lat2) 
    if d > maxd: 
        maxd = d 
        pair = ((lon1, lat1), (lon2, lat2)) 
 
print("Farthest pair distance (m):", maxd) 
ptA, ptB = pair 
 
with open(os.path.join(OUT_DIR, "selected_points.txt"), "w") as f: 
    f.write(f"pointA: {ptA}\npointB: {ptB}\ndistance_m: {maxd}\n") 
 
def fetch_years_for_point(lat, lon, years): 
    frames = [] 
    for y in years: 
        print(f"Fetching {y} for {lat:.5f},{lon:.5f}") 
        df = get_meteo_data(lat, lon, y)  
        df["year"] = y 
        frames.append(df) 
 
90 
 
    if frames: 
        return pd.concat(frames, ignore_index=True) 
    return pd.DataFrame() 
 
dfA = fetch_years_for_point(ptA[1], ptA[0], YEARS)  
dfB = fetch_years_for_point(ptB[1], ptB[0], YEARS) 
 
dfA.to_csv(os.path.join(OUT_DIR, "pointA_daily.csv"), index=False) 
dfB.to_csv(os.path.join(OUT_DIR, "pointB_daily.csv"), index=False) 
 
def monthly_aggregate(df_daily): 
    df = df_daily.copy() 
    if "date" in df.columns: 
        df["date"] = pd.to_datetime(df["date"]) 
        df["month"] = df["date"].dt.month 
        df["year"] = df["date"].dt.year 
    else: 
        raise ValueError("daily dataframe must contain 'date' column") 
    group = df.groupby(["year","month"]).agg({ 
        "temperature_2m_mean": "mean", 
        "sunshine_duration": "mean", 
        "relative_humidity_2m_mean": "mean", 
        "cloud_cover_mean": "mean", 
        "surface_pressure_mean": "mean", 
        "wind_speed_10m_mean": "mean", 
        "soil_temperature_0_to_7cm_mean": "mean", 
        "soil_moisture_0_to_7cm_mean": "mean", 
        "precipitation_sum": "sum", 
        "rain_sum": "sum", 
        "snowfall_sum": "sum", 
    }).reset_index() 
    return group 
 
monthlyA = monthly_aggregate(dfA) 
monthlyB = monthly_aggregate(dfB) 
 
monthlyA.to_csv(os.path.join(OUT_DIR, "pointA_monthly.csv"), index=False) 
monthlyB.to_csv(os.path.join(OUT_DIR, "pointB_monthly.csv"), index=False) 
 
def compute_metrics(monthlyA, monthlyB, variables): 
    m = pd.merge(monthlyA, monthlyB, on=["year","month"], 
suffixes=("_A","_B")) 
    rows = [] 
    for var in variables: 
        a = m[f"{var}_A"].values 
        b = m[f"{var}_B"].values 
        mask = ~np.isnan(a) & ~np.isnan(b) 
        if mask.sum() == 0: 
 
91 
 
            continue 
        a = a[mask]; b = b[mask] 
        mae = np.mean(np.abs(a - b)) 
        rmse = np.sqrt(np.mean((a - b)**2)) 
        try: 
            r, p = pearsonr(a, b) 
        except Exception: 
            r, p = np.nan, np.nan 
        try: 
            tstat, tp = ttest_rel(a, b) 
        except Exception: 
            tstat, tp = np.nan, np.nan 
        try: 
            wstat, wp = wilcoxon(a - b) 
        except Exception: 
            wstat, wp = np.nan, np.nan 
 
        rows.append({ 
            "variable": var, 
            "n": len(a), 
            "MAE": mae, 
            "RMSE": rmse, 
            "Pearson_r": r, 
            "Pearson_p": p, 
            "ttest_stat": tstat, 
            "ttest_p": tp, 
            "wilcoxon_stat": wstat, 
            "wilcoxon_p": wp 
        }) 
    return pd.DataFrame(rows) 
 
vars_to_check = [ 
    "temperature_2m_mean", 
    "precipitation_sum", 
    "rain_sum", 
    "snowfall_sum", 
    "sunshine_duration", 
    "relative_humidity_2m_mean", 
    "cloud_cover_mean", 
    "wind_speed_10m_mean", 
    "soil_temperature_0_to_7cm_mean", 
    "soil_moisture_0_to_7cm_mean", 
] 
 
metrics = compute_metrics(monthlyA, monthlyB, vars_to_check) 
metrics.to_csv(os.path.join(OUT_DIR, "comparison_metrics.csv"), index=False) 
print(metrics) 
 
 
92 
 
def plot_time_series_var(monthlyA, monthlyB, var, out_dir): 
    m = pd.merge(monthlyA, monthlyB, on=["year","month"], 
suffixes=("_A","_B")) 
    if m.empty: 
        return 
    # time axis 
    m["time"] = pd.to_datetime(m["year"].astype(str) + "-" + 
m["month"].astype(str) + "-15") 
    plt.figure(figsize=(10,4)) 
    plt.plot(m["time"], m[f"{var}_A"], label="point A") 
    plt.plot(m["time"], m[f"{var}_B"], label="point B", alpha=0.8) 
    plt.title(f"{var} — time series (A vs B)") 
    plt.legend() 
    plt.grid(True) 
    fname = os.path.join(out_dir, f"time_{var}.png") 
    plt.savefig(fname, bbox_inches="tight") 
    plt.close() 
 
    plt.figure(figsize=(5,5)) 
    plt.scatter(m[f"{var}_A"], m[f"{var}_B"], alpha=0.7) 
    mn = np.nanmin([m[f"{var}_A"].min(), m[f"{var}_B"].min()]) 
    mx = np.nanmax([m[f"{var}_A"].max(), m[f"{var}_B"].max()]) 
    plt.plot([mn,mx],[mn,mx], color="k", linestyle="--") 
    plt.xlabel("A"); plt.ylabel("B"); plt.title(f"{var} scatter A vs B") 
    plt.grid(True) 
    plt.savefig(os.path.join(out_dir, f"scatter_{var}.png"), 
bbox_inches="tight") 
    plt.close() 
 
for var in vars_to_check: 
    plot_time_series_var(monthlyA, monthlyB, var, OUT_DIR) 
 
with open(os.path.join(OUT_DIR, "report_summary.txt"), "w") as f: 
    f.write("Comparison summary between two farthest points in the largest 
community\n\n") 
    f.write(f"Selected community id: {largest_id}\n") 
    f.write(f"Point A (lon,lat): {ptA}\n") 
    f.write(f"Point B (lon,lat): {ptB}\n") 
    f.write(f"Geodetic distance (m): {maxd}\n\n") 
    f.write("Metrics:\n") 
    f.write(metrics.to_string(index=False)) 
    f.write("\n\nNotes:\n- see png files for time series and scatter plots in 
this folder.\n") 
 
print("All done. Outputs saved to", OUT_DIR) 
 
 
93 
 
import os 
import math 
from PIL import Image 
import matplotlib.pyplot as plt 
 
folder = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\code\weather_check_results"   
# ← тут твоя папка з png 
files = [os.path.join(folder, f) for f in os.listdir(folder) if 
f.endswith(".png") and f.startswith("time_")] 
files = sorted(files) 
 
n = len(files) 
cols = 4 
rows = math.ceil(n / cols) 
 
fig, axes = plt.subplots(rows, cols, figsize=(20, 5 * rows)) 
 
for ax, img_path in zip(axes.flatten(), files): 
    img = Image.open(img_path) 
    ax.imshow(img) 
    ax.set_title(os.path.basename(img_path), fontsize=10) 
    ax.axis("off") 
 
for ax in axes.flatten()[len(files):]: 
    ax.axis("off") 
 
plt.tight_layout() 
plt.savefig("combined_plots.png", dpi=200) 
plt.show() 
 
 
# create_data_structure.ipynb 
MANIFEST_PATH = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv" 
import pandas as pd 
 
data = pd.read_csv(MANIFEST_PATH) 
data.head() 
data[data["valid"] == True] 
data_2021 = data[(data["year"] == 2021) & (data["valid"] == True)] 
data_2022 = data[(data["year"] == 2022) & (data["valid"] == True)] 
data_2023 = data[(data["year"] == 2023) & (data["valid"] == True)] 
 
94 
 
 
data_2021 = 
data_2021[data_2021.groupby("field_id")["month"].transform("count") == 12] 
data_2022 = 
data_2022[data_2022.groupby("field_id")["month"].transform("count") == 12] 
data_2023 = 
data_2023[data_2023.groupby("field_id")["month"].transform("count") == 12] 
 
# getting_vari.ipynb 
import pandas as pd 
 
print(data.columns) 
data.head() 
 
import pandas as pd 
import numpy as np 
import tifffile 
import os 
 
# VARI = (G - R) / (G + R - B) 
def compute_vari(img): 
    if img.ndim == 2: 
        return np.nan 
 
    if img.shape[2] < 3: 
        return np.nan 
 
    R = img[:,:,0].astype(np.float32) 
    G = img[:,:,1].astype(np.float32) 
    B = img[:,:,2].astype(np.float32) 
 
    denom = (G + R - B) 
    denom[denom == 0] = np.nan 
 
    vari = (G - R) / denom 
 
    return np.nanmean(vari)  
 
 
def add_vari_to_dataframe(df): 
    vari_values = [] 
 
    for idx, row in df.iterrows(): 
        path = row["image_path"] 
 
 
95 
 
        if pd.isna(path) or path is None or not isinstance(path, str) or not 
os.path.exists(path): 
            vari_values.append(np.nan) 
            continue 
 
        try: 
            img = tifffile.imread(path) 
 
            if img.ndim == 3 and img.shape[0] in (1,3,4) and img.shape[1] != 
3: 
                img = np.transpose(img, (1,2,0)) 
 
            img = np.squeeze(img) 
 
            if img.ndim == 2: 
                img = img[:, :, np.newaxis] 
 
            vari = compute_vari(img) 
            vari_values.append(vari) 
 
        except Exception as e: 
            print(f"Помилка читання {path}: {e}") 
            vari_values.append(np.nan) 
 
    df_copy = df.copy() 
    df_copy["VARI"] = vari_values 
    return df_copy 
 
 
path = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv" 
data = pd.read_csv(path) 
processed_df = add_vari_to_dataframe(data) 
 
save_path = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields_with_vari.csv" 
processed_df.to_csv(save_path, index=False) 
 
print("CSV збережено в:") 
print(save_path) 
 
processed_df["VARI"].describe() 
# harvest_data.ipynb 
 
96 
 
import pandas as pd 
import os 
 
HARVEST_DATA_FOLDER = 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\harvest data" 
 
harvest_data_by_regions = os.path.join(HARVEST_DATA_FOLDER, 
"harvest_by_region.csv") 
 
by_regions = pd.read_csv(harvest_data_by_regions, sep=";") 
by_regions[by_regions["attributes"] == "Черкаська"] 
by_regions.attributes.unique() 
 
# train.ipynb 
 
from tqdm import tqdm 
import os 
import time 
import math 
import glob 
from typing import List, Tuple, Optional, Callable, Dict 
 
import numpy as np 
import pandas as pd 
from PIL import Image, UnidentifiedImageError 
 
import torch 
import torch.nn as nn 
import torch.nn.functional as F 
from torch.utils.data import Dataset, DataLoader, random_split 
from torchvision import transforms, models 
 
from sklearn.model_selection import train_test_split 
 
 
CONFIG = { 
    "data_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data", 
    "manifest_path": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv", 
    "weather_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather", 
 
97 
 
    "image_size": (128, 128),    
    "batch_size": 2, 
    "num_workers": 0, 
    "device": "cuda" if torch.cuda.is_available() else "cpu", 
    "seed": 42, 
    "pretrain_epochs": 50, 
    "finetune_epochs": 30, 
    "lr": 1e-3, 
    "train_frac": 0.8,       
    "require_full_12_months": True,   
    "save_dir": "./checkpoints", 
    "projection_dim": 256,       
    "latent_img_dim": 128, 
    "latent_tab_dim": 128, 
    "latent_z_dim": 256, 
    "max_train_samples": 50,    
    "max_val_samples": 20, 
} 
 
 
def set_seed(seed: int): 
    import random 
    random.seed(seed) 
    np.random.seed(seed) 
    torch.manual_seed(seed) 
    if torch.cuda.is_available(): 
        torch.cuda.manual_seed_all(seed) 
 
 
def makedir(path: str): 
    os.makedirs(path, exist_ok=True) 
 
 
def load_manifest(manifest_path: str) -> pd.DataFrame: 
    df = pd.read_csv(manifest_path) 
    return df 
 
 
def filter_full_years(df: pd.DataFrame, year: int) -> pd.DataFrame: 
    sub = df[(df["year"] == year) & (df["valid"] == True)].copy() 
    counts = sub.groupby("field_id")["month"].transform("count") 
    filtered = sub[counts == 12].reset_index(drop=True) 
    return filtered 
 
 
def monthly_aggregate_weather(df): 
    df = df.copy() 
 
 
98 
 
    if "month" not in df.columns: 
        df["month"] = pd.to_datetime(df["date"]).dt.month 
 
    agg = df.groupby("month").agg({ 
        "temperature_2m_mean": "mean", 
        "precipitation_sum": "sum", 
        "rain_sum": "sum", 
        "snowfall_sum": "sum", 
        "sunshine_duration": "mean", 
        "relative_humidity_2m_mean": "mean", 
        "cloud_cover_mean": "mean", 
        "surface_pressure_mean": "mean", 
        "wind_speed_10m_mean": "mean", 
        "soil_temperature_0_to_7cm_mean": "mean", 
        "soil_moisture_0_to_7cm_mean": "mean" 
    }) 
 
    agg = agg.rename(columns=lambda c: f"agg_{c}") 
 
    for m in range(1, 13): 
        if m not in agg.index: 
            agg.loc[m] = np.zeros(len(agg.columns)) 
 
    agg = agg.sort_index() 
    return agg 
 
 
class FieldMonthlyDataset(Dataset): 
    def __init__( 
        self, 
        manifest_df: pd.DataFrame, 
        weather_root: str, 
        years: List[int], 
        transform_img: Optional[Callable] = None, 
        require_full_year: bool = True, 
        months_stack: int = 1, 
        weather_agg_fn: Optional[Callable] = None, 
    ): 
        self.manifest = manifest_df.copy().reset_index(drop=True) 
        self.weather_root = weather_root 
        self.transform_img = transform_img 
        self.require_full_year = require_full_year 
        self.months_stack = months_stack 
        self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn 
 
        self.weather_cache: Dict[Tuple[str, int], np.ndarray] = {} 
        self._prepare_weather_cache(years) 
 
 
99 
 
    def default_weather_agg_fn(self, monthly_df: pd.DataFrame) -> np.ndarray: 
        numeric = monthly_df.select_dtypes(include=[np.number]) 
        return numeric.fillna(0).values   
 
    def _load_weather_file(self, koatuu: str, year: int) -> 
Optional[pd.DataFrame]: 
        path_pattern = os.path.join(self.weather_root, str(year), 
f"{koatuu}*") 
        files = glob.glob(path_pattern) 
        if not files: 
            return None 
        path = files[0] 
        try: 
            df = pd.read_csv(path) 
            return df 
        except Exception: 
            return None 
 
    def _prepare_weather_cache(self, years: List[int]): 
        for _, row in self.manifest.iterrows(): 
            ko = str(row["koatuu"]) 
            yr = int(row["year"]) 
            key = (ko, yr) 
            if key in self.weather_cache: 
                continue 
            if yr not in years: 
                continue 
            df = self._load_weather_file(ko, yr) 
            if df is None: 
                self.weather_cache[key] = None 
                continue 
            monthly = monthly_aggregate_weather(df)   
            vecs = self.weather_agg_fn(monthly)   
            self.weather_cache[key] = vecs 
 
    def __len__(self): 
        return len(self.manifest) 
 
    def __getitem__(self, idx): 
        row = self.manifest.iloc[idx] 
        img_path = row["image_path"] 
 
        try: 
            im = Image.open(img_path).convert("RGB") 
        except (UnidentifiedImageError, OSError): 
            import tifffile 
            im_np = tifffile.imread(img_path)  
            if im_np.dtype != np.uint8: 
 
100 
 
                im_np = (im_np / im_np.max() * 255).astype(np.uint8) 
            if im_np.ndim == 2: 
                im_np = np.stack([im_np]*3, axis=-1)  
            elif im_np.shape[2] > 3: 
                im_np = im_np[:, :, :3]   
            im = Image.fromarray(im_np) 
 
        if self.transform_img: 
            im_t = self.transform_img(im) 
        else: 
            im_t = transforms.ToTensor()(im) 
 
        images_stack = im_t.unsqueeze(0) 
 
        ko = str(row["koatuu"]) 
        yr = int(row["year"]) 
        mo = int(row["month"]) 
        cache = self.weather_cache.get((ko, yr), None) 
        if cache is None: 
            tab_t = np.zeros((1, 1), dtype=np.float32) 
            tab_t1 = np.zeros((1, 1), dtype=np.float32) 
        else: 
            idx0 = mo - 1 
            idx1 = (mo % 12) 
            tab_t = cache[idx0].astype(np.float32) 
            tab_t1 = cache[idx1].astype(np.float32) 
 
        y_next = None 
 
        return { 
            "images_stack": images_stack, 
            "tab_t": torch.from_numpy(tab_t), 
            "tab_t1": torch.from_numpy(tab_t1), 
            "meta": row.to_dict(), 
        } 
 
 
class SimpleImageEncoder(nn.Module): 
    def __init__(self, out_dim=128): 
        super().__init__() 
        self.conv = nn.Sequential( 
            nn.Conv2d(3, 32, 3, stride=2, padding=1), nn.ReLU(), 
nn.BatchNorm2d(32), 
            nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(), 
nn.BatchNorm2d(64), 
            nn.AdaptiveAvgPool2d((1, 1)), 
        ) 
        self.fc = nn.Linear(64, out_dim) 
 
101 
 
 
    def forward(self, x): 
        h = self.conv(x).view(x.size(0), -1) 
        return self.fc(h) 
 
 
class SimpleTabEncoder(nn.Module): 
    def __init__(self, in_month_dim, out_dim=128): 
        super().__init__() 
        self.pool = nn.AdaptiveAvgPool1d(1)  
        self.fc = nn.Sequential(nn.Linear(in_month_dim, out_dim), nn.ReLU()) 
 
    def forward(self, x): 
        if x.dim() == 3: 
            x = x.mean(dim=1) 
        return self.fc(x) 
 
 
class ProjectionHead(nn.Module): 
    def __init__(self, in_dim, proj_dim): 
        super().__init__() 
        self.net = nn.Sequential( 
            nn.Linear(in_dim, in_dim), 
            nn.ReLU(), 
            nn.Linear(in_dim, proj_dim) 
        ) 
 
    def forward(self, x): 
        return self.net(x) 
 
 
class ForecastHead(nn.Module): 
    def __init__(self, z_dim, tab_dim, hidden=128): 
        super().__init__() 
        self.net = nn.Sequential( 
            nn.Linear(z_dim + tab_dim, hidden), 
            nn.ReLU(), 
            nn.Linear(hidden, 1) 
        ) 
 
    def forward(self, z, tab): 
        x = torch.cat([z, tab], dim=-1) 
        return self.net(x).squeeze(-1) 
 
 
def info_nce_loss(z_a: torch.Tensor, z_b: torch.Tensor, temperature=0.1): 
    z_a = F.normalize(z_a, dim=1) 
    z_b = F.normalize(z_b, dim=1) 
 
102 
 
    batch_size = z_a.size(0) 
    representations = torch.cat([z_a, z_b], dim=0)  # 2B x D 
    similarity = torch.matmul(representations, representations.T)  # 2B x 2B 
    labels = torch.arange(batch_size).to(z_a.device) 
    labels = torch.cat([labels, labels], dim=0) 
 
    logits = similarity / temperature 
    logits_mask = (~torch.eye(2 * batch_size, 
dtype=torch.bool)).to(z_a.device) 
    logits = logits.masked_select(logits_mask).view(2 * batch_size, -1) 
 
    positives = torch.cat([torch.sum(z_a * z_b, dim=1), torch.sum(z_b * z_a, 
dim=1)], dim=0) / temperature 
    targets = torch.cat([torch.arange(batch_size) + 0 for _ in range(2)], 
dim=0).to(z_a.device) 
 
    exp_logits = torch.exp(logits) 
    denom = exp_logits.sum(dim=1) 
    sim = torch.matmul(representations, representations.T) / temperature 
    pos_mask = torch.zeros_like(sim) 
    for i in range(batch_size): 
        pos_mask[i, i + batch_size] = 1 
        pos_mask[i + batch_size, i] = 1 
    numerator = (torch.exp(sim) * pos_mask).sum(dim=1) 
    loss = -torch.log(numerator / (torch.exp(sim).sum(dim=1) - 
torch.exp(torch.diag(sim)))) 
    loss = loss.mean() 
    return loss 
 
 
def pretrain_contrastive( 
    img_encoder: nn.Module, 
    tab_encoder: nn.Module, 
    projection: nn.Module, 
    dataloader: DataLoader, 
    epochs: int, 
    device: str, 
    lr: float, 
    save_path: str, 
): 
    makedir(save_path) 
    img_encoder.to(device) 
    tab_encoder.to(device) 
    projection.to(device) 
    params = list(img_encoder.parameters()) + list(tab_encoder.parameters()) 
+ list(projection.parameters()) 
    optim = torch.optim.Adam(params, lr=lr) 
    start_time = time.time() 
 
103 
 
 
    for epoch in range(1, epochs + 1): 
        epoch_loss = 0.0 
        n_batches = 0 
        t0 = time.time() 
        img_encoder.train(); tab_encoder.train(); projection.train() 
        for batch in tqdm(dataloader, desc=f"Pretrain epoch 
{epoch}/{epochs}", ncols=100): 
            images_stack = batch["images_stack"]  
            tab_t = batch["tab_t"]   
            B = images_stack.size(0) 
 
            img = images_stack[:, 0, :, :, :].to(device)  
            aug = transforms.Compose([ 
                transforms.RandomResizedCrop(CONFIG["image_size"]), 
                transforms.RandomHorizontalFlip(), 
                transforms.ToTensor() 
            ]) 
            view1 = img + 0.01 * torch.randn_like(img) 
            view2 = img + 0.01 * torch.randn_like(img) 
 
            tab = tab_t.float().to(device) 
            tab_view1 = tab + 0.01 * torch.randn_like(tab) 
            tab_view2 = tab + 0.01 * torch.randn_like(tab) 
            z_img1 = img_encoder(view1) 
            z_tab1 = tab_encoder(tab_view1) 
            z1 = torch.cat([z_img1, z_tab1], dim=1) 
            p1 = projection(z1) 
 
            z_img2 = img_encoder(view2) 
            z_tab2 = tab_encoder(tab_view2) 
            z2 = torch.cat([z_img2, z_tab2], dim=1) 
            p2 = projection(z2) 
 
            loss = info_nce_loss(p1, p2) 
            optim.zero_grad() 
            loss.backward() 
            optim.step() 
 
            epoch_loss += loss.item() 
            n_batches += 1 
 
        epoch_time = time.time() - t0 
        print(f"[Pretrain] Epoch {epoch}/{epochs} loss={epoch_loss / 
max(1,n_batches):.4f} time={epoch_time:.1f}s") 
        if epoch % 10 == 0: 
            torch.save({ 
                "img_encoder": img_encoder.state_dict(), 
 
104 
 
                "tab_encoder": tab_encoder.state_dict(), 
                "projection": projection.state_dict(), 
                "optimizer": optim.state_dict(), 
                "epoch": epoch 
            }, os.path.join(save_path, f"pretrain_epoch_{epoch}.pt")) 
    total = time.time() - start_time 
    print("Pretraining finished in {:.1f}s".format(total)) 
 
 
def finetune_forecast( 
    img_encoder: nn.Module, 
    tab_encoder: nn.Module, 
    projection: nn.Module, 
    forecast_head: nn.Module, 
    dataloader: DataLoader, 
    epochs: int, 
    device: str, 
    lr: float, 
    save_path: str, 
): 
 
    makedir(save_path) 
    img_encoder.to(device); tab_encoder.to(device); projection.to(device); 
forecast_head.to(device) 
    params = list(forecast_head.parameters()) 
    params += list(projection.parameters()) 
    optimizer = torch.optim.Adam(params, lr=lr) 
    criterion = nn.MSELoss() 
 
    start_time = time.time() 
    for epoch in range(1, epochs + 1): 
        epoch_loss = 0.0 
        n_batches = 0 
        t0 = time.time() 
        img_encoder.train(); tab_encoder.train(); projection.train(); 
forecast_head.train() 
        for batch in dataloader: 
            images_stack = batch["images_stack"].to(device)  
            tab_t = batch["tab_t"].to(device)           
            tab_t1 = batch["tab_t1"].to(device) 
            y_next = batch.get("y_next", None) 
            if y_next is None: 
                continue 
            y_next = y_next.float().to(device) 
 
            img = images_stack[:, 0, :, :, :] 
            z_img = img_encoder(img) 
            z_tab = tab_encoder(tab_t) 
 
105 
 
            z = torch.cat([z_img, z_tab], dim=1) 
            z_proj = projection(z) 
            tab_feat = tab_t1.view(tab_t1.size(0), -1) 
            y_hat = forecast_head(z_proj, tab_feat) 
            loss = criterion(y_hat, y_next) 
 
            optimizer.zero_grad() 
            loss.backward() 
            optimizer.step() 
 
            epoch_loss += loss.item() 
            n_batches += 1 
 
        epoch_time = time.time() - t0 
        print(f"[Finetune] Epoch {epoch}/{epochs} loss={epoch_loss / 
max(1,n_batches):.6f} time={epoch_time:.1f}s") 
        if epoch % 5 == 0: 
            torch.save({ 
                "projection": projection.state_dict(), 
                "forecast_head": forecast_head.state_dict(), 
                "optimizer": optimizer.state_dict(), 
                "epoch": epoch 
            }, os.path.join(save_path, f"finetune_epoch_{epoch}.pt")) 
    total = time.time() - start_time 
    print("Finetune finished in {:.1f}s".format(total)) 
 
 
 
def build_dataloaders(manifest_path, weather_root, year, cfg): 
    manifest = load_manifest(manifest_path) 
 
    if cfg["require_full_12_months"]: 
        manifest = filter_full_years(manifest, year) 
 
    if cfg["train_frac"] < 1.0: 
        manifest_train, manifest_val = train_test_split( 
            manifest, 
            train_size=cfg["train_frac"], 
            random_state=cfg["seed"] 
        ) 
    else: 
        manifest_train = manifest.copy() 
        manifest_val = pd.DataFrame([]) 
 
 
    if cfg.get("max_train_samples") is not None: 
        manifest_train = manifest_train.sample( 
            min(cfg["max_train_samples"], len(manifest_train)), 
 
106 
 
            random_state=42 
        ) 
 
    if cfg.get("max_val_samples") is not None and not manifest_val.empty: 
        manifest_val = manifest_val.sample( 
            min(cfg["max_val_samples"], len(manifest_val)), 
            random_state=42 
        ) 
 
    if len(manifest_train) == 0: 
        raise RuntimeError("Training set is empty after sampling. Reduce 
filtering or increase max_train_samples.") 
 
    transform_img = transforms.Compose([ 
        transforms.Resize(cfg["image_size"]), 
        transforms.ToTensor(), 
    ]) 
 
    ds_train = FieldMonthlyDataset( 
        manifest_train, weather_root, years=[year], 
        transform_img=transform_img 
    ) 
 
    dl_train = DataLoader( 
        ds_train, 
        batch_size=cfg["batch_size"], 
        shuffle=True, 
        num_workers=cfg["num_workers"], 
        drop_last=True 
    ) 
 
    if not manifest_val.empty: 
        ds_val = FieldMonthlyDataset( 
            manifest_val, weather_root, years=[year], 
            transform_img=transform_img 
        ) 
        dl_val = DataLoader( 
            ds_val, 
            batch_size=cfg["batch_size"], 
            shuffle=False, 
            num_workers=cfg["num_workers"] 
        ) 
    else: 
        dl_val = None 
 
    print(len(manifest_train), len(manifest_val)) 
 
    return dl_train, dl_val 
 
107 
 
 
 
 
def main(): 
    set_seed(CONFIG["seed"]) 
    makedir(CONFIG["save_dir"]) 
 
    dl_train, dl_val = build_dataloaders(CONFIG["manifest_path"], 
CONFIG["weather_root"], 2021, CONFIG) 
 
    img_enc = SimpleImageEncoder(out_dim=CONFIG["latent_img_dim"]) 
    sample = next(iter(dl_train)) 
    tab_t_sample = sample["tab_t"] 
    tab_dim = tab_t_sample.numel() // tab_t_sample.size(0)  # if (B, k) -> k 
    tab_dim = tab_t_sample.shape[-1] if tab_t_sample.dim() == 2 else 
tab_t_sample.shape[-1] 
    tab_enc = SimpleTabEncoder(in_month_dim=tab_t_sample.shape[-1], 
out_dim=CONFIG["latent_tab_dim"]) 
    projection = ProjectionHead(CONFIG["latent_img_dim"] + 
CONFIG["latent_tab_dim"], CONFIG["projection_dim"]) 
    forecast = ForecastHead(CONFIG["projection_dim"], 
tab_t_sample.view(tab_t_sample.size(0), -1).shape[1]) 
 
    pretrain_contrastive(img_enc, tab_enc, projection, dl_train, 
CONFIG["pretrain_epochs"], CONFIG["device"], CONFIG["lr"], 
CONFIG["save_dir"]) 
 
    finetune_forecast(img_enc, tab_enc, projection, forecast, dl_train, 
CONFIG["finetune_epochs"], CONFIG["device"], CONFIG["lr"], 
CONFIG["save_dir"]) 
 
 
if __name__ == "__main__": 
    main() 
 
 
import numpy as np 
import tifffile 
from PIL import Image 
 
def tiff_to_png(input_path, output_path, rescale_to_8bit=True): 
    img = tifffile.imread(input_path)  
 
    print("orig.shape =", img.shape, "orig.dtype =", img.dtype) 
 
    if img.ndim == 3 and img.shape[0] in (1,3,4) and (img.shape[1] != 3): 
 
108 
 
        img = np.transpose(img, (1,2,0)) 
        print("transpose (C,H,W)->(H,W,C) ->", img.shape) 
 
    img = np.squeeze(img) 
    print("squeezed.shape =", img.shape) 
 
    if img.ndim == 2: 
        img = img[:, :, np.newaxis] 
        print("added channel axis ->", img.shape) 
 
    H, W, C = img.shape 
 
    if C not in (1,3,4): 
        print("Warning: unexpected channel count:", C) 
 
    if img.dtype == np.uint16: 
        if rescale_to_8bit: 
            vmax = float(img.max()) if img.max() > 0 else 1.0 
            img8 = (img.astype(np.float32) / vmax * 
255.0).round().clip(0,255).astype(np.uint8) 
        else: 
            img8 = (img >> 8).astype(np.uint8) 
    elif img.dtype == np.float32 or img.dtype == np.float64: 
        vmin = float(img.min()) 
        vmax = float(img.max()) 
        if vmax <= 1.0: 
            img8 = (img * 255.0).round().clip(0,255).astype(np.uint8) 
        else: 
            denom = (vmax - vmin) if (vmax > vmin) else 1.0 
            img8 = ((img - vmin) / denom * 
255.0).round().clip(0,255).astype(np.uint8) 
    elif img.dtype == np.uint8: 
        img8 = img 
    else: 
        img8 = (img.astype(np.float32) - float(img.min())) 
        mn = img8.min() 
        mx = img8.max() if img8.max() > mn else mn + 1.0 
        img8 = ((img8 - mn) / (mx - mn) * 
255.0).round().clip(0,255).astype(np.uint8) 
 
    if img8.shape[2] == 1: 
        img8 = np.repeat(img8, 3, axis=2) 
 
    im = Image.fromarray(img8) 
    im.save(output_path) 
    print("Saved:", output_path, "-> shape", img8.shape, "dtype", img8.dtype) 
 
 
 
109 
 
input_path = 
'C:\\Users\\nazar\\Desktop\\projects\\masters_thesis\\data\\processed_fields\
\processed_data\\images\\2021\\12\\FIELD_44902_12_2021.tif' 
output_path = input_path.replace(".tif", ".png") 
tiff_to_png(input_path, output_path) 
 
import pandas as pd 
 
manif = 
pd.read_csv(r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_f
ields\manifest_fields.csv") 
 
import matplotlib.pyplot as plt 
 
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True) 
 
years = [2021, 2022, 2023] 
 
for ax, year in zip(axes, years): 
    subset = manif[manif["year"] == year]["valid"] 
    ax.hist(subset, bins=[-0.5, 0.5, 1.5])   
    ax.set_title(f"{year}") 
    ax.set_xticks([0, 1]) 
    ax.set_xticklabels(["False", "True"]) 
 
fig.suptitle("Розподіл valid по роках") 
plt.tight_layout() 
plt.show() 
 
%matplotlib inline 
from torchvision.models import efficientnet_b0 
import os, time, itertools, glob 
from typing import List, Dict, Callable, Optional, Tuple 
import math 
import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt 
from tqdm import tqdm 
 
import torch 
import torch.nn as nn 
import torch.nn.functional as F 
from torch.utils.data import Dataset, DataLoader 
from torchvision import transforms, models 
from PIL import Image, UnidentifiedImageError 
 
110 
 
 
from sklearn.manifold import TSNE 
from sklearn.decomposition import PCA 
 
CONFIG = { 
    "data_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data", 
    "manifest_path": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields.csv", 
    "weather_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather", 
    "image_size": (128, 128), 
    "batch_size": 8, 
    "num_workers": 0, 
    "device": "cuda" if torch.cuda.is_available() else "cpu", 
    "seed": 42, 
    "pretrain_epochs_search": 5, 
    "pretrain_epochs_full": 50, 
    "lr": 1e-3, 
    "train_frac": 0.8, 
    "require_full_12_months": True, 
    "save_dir": "./training_result", 
    "projection_dim": 128, 
    "latent_img_dim": 64, 
    "latent_tab_dim": 64, 
    "max_train_samples": 2000,  
    "max_val_samples": 200, 
    "n_visualize_embeddings": 200, 
} 
 
os.makedirs(CONFIG["save_dir"], exist_ok=True) 
 
def set_seed(seed:int): 
    import random 
    random.seed(seed) 
    np.random.seed(seed) 
    torch.manual_seed(seed) 
    if torch.cuda.is_available(): 
        torch.cuda.manual_seed_all(seed) 
set_seed(CONFIG["seed"]) 
 
def makedir(path): 
    os.makedirs(path, exist_ok=True) 
 
def load_manifest(manifest_path: str) -> pd.DataFrame: 
 
111 
 
    df = pd.read_csv(manifest_path) 
    return df 
 
def filter_full_years(df: pd.DataFrame, year: int) -> pd.DataFrame: 
    sub = df[(df["year"] == year) & (df["valid"] == True)].copy() 
    counts = sub.groupby("field_id")["month"].transform("count") 
    filtered = sub[counts == 12].reset_index(drop=True) 
    return filtered 
 
def monthly_aggregate_weather(df): 
    df = df.copy() 
    if "month" not in df.columns: 
        df["month"] = pd.to_datetime(df["date"]).dt.month 
    agg = df.groupby("month").agg({ 
        "temperature_2m_mean": "mean", 
        "precipitation_sum": "sum", 
        "rain_sum": "sum", 
        "snowfall_sum": "sum", 
        "sunshine_duration": "mean", 
        "relative_humidity_2m_mean": "mean", 
        "cloud_cover_mean": "mean", 
        "surface_pressure_mean": "mean", 
        "wind_speed_10m_mean": "mean", 
        "soil_temperature_0_to_7cm_mean": "mean", 
        "soil_moisture_0_to_7cm_mean": "mean" 
    }) 
    agg = agg.rename(columns=lambda c: f"agg_{c}") 
    for m in range(1,13): 
        if m not in agg.index: 
            agg.loc[m] = np.zeros(len(agg.columns)) 
    agg = agg.sort_index() 
    return agg 
 
 
class FieldMonthlyDataset(Dataset): 
    def __init__(self, manifest_df: pd.DataFrame, weather_root: str, years: 
List[int], 
                 transform_img=None, require_full_year=True, 
months_stack:int=1, weather_agg_fn=None): 
        self.manifest = manifest_df.copy().reset_index(drop=True) 
        self.weather_root = weather_root 
        self.transform_img = transform_img 
        self.months_stack = months_stack 
        self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn 
        self.weather_cache = {} 
        self._prepare_weather_cache(years) 
 
    def default_weather_agg_fn(self, monthly_df): 
 
112 
 
        numeric = monthly_df.select_dtypes(include=[np.number]) 
        return numeric.fillna(0).values 
 
    def _load_weather_file(self, koatuu: str, year:int): 
        path_pattern = os.path.join(self.weather_root, str(year), 
f"{koatuu}*") 
        files = glob.glob(path_pattern) 
        if not files: 
            return None 
        try: 
            return pd.read_csv(files[0]) 
        except Exception: 
            return None 
 
    def _prepare_weather_cache(self, years: List[int]): 
        for _, row in self.manifest.iterrows(): 
            ko = str(row["koatuu"]) 
            yr = int(row["year"]) 
            key = (ko, yr) 
            if key in self.weather_cache: continue 
            if yr not in years: continue 
            df = self._load_weather_file(ko, yr) 
            if df is None: 
                self.weather_cache[key] = None 
            else: 
                monthly = monthly_aggregate_weather(df) 
                self.weather_cache[key] = self.weather_agg_fn(monthly) 
 
    def __len__(self): return len(self.manifest) 
 
    def __getitem__(self, idx): 
        row = self.manifest.iloc[idx] 
        img_path = row["image_path"] 
 
        try: 
            im = Image.open(img_path).convert("RGB") 
        except (UnidentifiedImageError, OSError): 
            import tifffile 
            im_np = tifffile.imread(img_path) 
            if im_np.dtype != np.uint8: 
                im_np = (im_np / max(1, im_np.max()) * 255).astype(np.uint8) 
            if im_np.ndim == 2: 
                im_np = np.stack([im_np]*3, axis=-1) 
            elif im_np.shape[2] > 3: 
                im_np = im_np[:, :, :3] 
            im = Image.fromarray(im_np) 
 
        if self.transform_img: 
 
113 
 
            im_t = self.transform_img(im) 
        else: 
            im_t = transforms.ToTensor()(im) 
 
        images_stack = im_t.unsqueeze(0)  # (1, C, H, W) 
 
        ko = str(row["koatuu"]); yr = int(row["year"]); mo = 
int(row["month"]) 
        cache = self.weather_cache.get((ko, yr), None) 
        if cache is None: 
            tab_t = np.zeros((1,1), dtype=np.float32) 
            tab_t1 = np.zeros((1,1), dtype=np.float32) 
        else: 
            idx0 = mo - 1 
            idx1 = (mo % 12) 
            tab_t = cache[idx0].astype(np.float32) 
            tab_t1 = cache[idx1].astype(np.float32) 
 
        out = { 
            "images_stack": images_stack,         
            "tab_t": torch.from_numpy(tab_t),    
            "tab_t1": torch.from_numpy(tab_t1), 
            "meta": row.to_dict() 
        } 
        return out 
 
 
class EfficientNetEncoder(nn.Module): 
    def __init__(self, out_dim=64, pretrained=False): 
        super().__init__() 
        net = efficientnet_b0(pretrained=pretrained) 
        self.backbone = net.features               
        self.pool = nn.AdaptiveAvgPool2d((1,1)) 
        self.fc = nn.Linear(net.classifier[1].in_features, out_dim) 
 
    def forward(self, x): 
        h = self.backbone(x) 
        h = self.pool(h).view(h.size(0), -1) 
        return self.fc(h) 
 
class ResNet18Encoder(nn.Module): 
    def __init__(self, out_dim=64, pretrained=False): 
        super().__init__() 
        net = models.resnet18(pretrained=pretrained) 
        net.fc = nn.Identity() 
        self.backbone = net 
        self.fc = nn.Linear(512, out_dim) 
    def forward(self,x): 
 
114 
 
        h = self.backbone(x) 
        return self.fc(h) 
 
class TabTransformerEncoder(nn.Module): 
    def __init__(self, in_dim, out_dim=64, num_layers=2, num_heads=4, 
hidden_dim=128): 
        super().__init__() 
 
        self.input_proj = nn.Linear(in_dim, hidden_dim) 
 
        encoder_layer = nn.TransformerEncoderLayer( 
            d_model=hidden_dim, 
            nhead=num_heads, 
            dim_feedforward=hidden_dim * 2, 
            batch_first=True, 
            activation="gelu" 
        ) 
        self.transformer = nn.TransformerEncoder(encoder_layer, 
num_layers=num_layers) 
 
        self.fc_out = nn.Linear(hidden_dim, out_dim) 
 
    def forward(self, x): 
        if x.dim() == 2: 
            x = x.unsqueeze(1)   
 
        h = self.input_proj(x) 
        h = self.transformer(h) 
        h = h.mean(dim=1)      
        return self.fc_out(h) 
 
 
class SmallMLPTab(nn.Module): 
    def __init__(self, in_dim, out_dim=64): 
        super().__init__() 
        self.net = nn.Sequential( 
            nn.Linear(in_dim, 128), nn.ReLU(), 
            nn.Linear(128, out_dim) 
        ) 
    def forward(self,x): 
        if x.dim()==3: 
            x = x.mean(dim=1) 
        return self.net(x) 
 
class CNNMediumEncoder(nn.Module): 
    def __init__(self, out_dim=64): 
        super().__init__() 
        self.conv = nn.Sequential( 
 
115 
 
            nn.Conv2d(3,32,3,stride=1,padding=1), nn.ReLU(), 
nn.BatchNorm2d(32), 
            nn.Conv2d(32,64,3,stride=2,padding=1), nn.ReLU(), 
nn.BatchNorm2d(64), 
            nn.Conv2d(64,128,3,stride=2,padding=1), nn.ReLU(), 
nn.BatchNorm2d(128), 
            nn.AdaptiveAvgPool2d((1,1)) 
        ) 
        self.fc = nn.Linear(128, out_dim) 
    def forward(self,x): 
        h = self.conv(x).view(x.size(0),-1) 
        return self.fc(h) 
 
 
 
class TabMediumEncoder(nn.Module): 
    def __init__(self, in_dim, out_dim=64): 
        super().__init__() 
        self.net = nn.Sequential( 
            nn.Linear(in_dim, max(64,in_dim)), nn.ReLU(), 
            nn.Linear(max(64,in_dim), max(64,in_dim)), nn.ReLU(), 
            nn.Linear(max(64,in_dim), out_dim) 
        ) 
    def forward(self,x): 
        if x.dim()==3:   
            x = x.mean(dim=1) 
        return self.net(x) 
 
 
class ProjectionHead(nn.Module): 
    def __init__(self, in_dim, proj_dim=128): 
        super().__init__() 
        self.net = nn.Sequential( 
            nn.Linear(in_dim, in_dim), nn.ReLU(), 
            nn.Linear(in_dim, proj_dim) 
        ) 
    def forward(self,x): return self.net(x) 
 
class ForecastHead(nn.Module): 
    def __init__(self, z_dim, tab_dim, hidden=128): 
        super().__init__() 
        self.net = nn.Sequential(nn.Linear(z_dim+tab_dim, hidden), nn.ReLU(), 
nn.Linear(hidden,1)) 
    def forward(self,z,tab): return self.net(torch.cat([z,tab],dim=-
1)).squeeze(-1) 
 
 
def info_nce_loss(z_a: torch.Tensor, z_b: torch.Tensor, temperature=0.1): 
 
116 
 
    z_a = F.normalize(z_a, dim=1) 
    z_b = F.normalize(z_b, dim=1) 
    B = z_a.size(0) 
    representations = torch.cat([z_a, z_b], dim=0)   
    sim = torch.matmul(representations, representations.T) / temperature   
    mask = (~torch.eye(2*B, dtype=torch.bool, device=sim.device)) 
    logits = sim.masked_select(mask).view(2*B, -1) 
    positives = torch.cat([torch.sum(z_a*z_b, dim=1), torch.sum(z_b*z_a, 
dim=1)], dim=0) / temperature 
    exp_logits = torch.exp(logits) 
    denom = exp_logits.sum(dim=1) 
    num = torch.exp(positives) 
    loss = -torch.log(num / denom) 
    return loss.mean() 
 
def build_dataloaders(manifest_path, weather_root, year, cfg): 
    manifest = load_manifest(manifest_path) 
    if cfg["require_full_12_months"]: 
        manifest = filter_full_years(manifest, year) 
    if cfg.get("max_train_samples") is not None: 
        manifest = manifest.sample(min(cfg["max_train_samples"], 
len(manifest)), random_state=cfg["seed"]) 
    transform_img = transforms.Compose([transforms.Resize(cfg["image_size"]), 
transforms.ToTensor()]) 
    ds = FieldMonthlyDataset(manifest, weather_root, years=[year], 
transform_img=transform_img) 
    dl = DataLoader(ds, batch_size=cfg["batch_size"], shuffle=True, 
num_workers=cfg["num_workers"], drop_last=True) 
    return dl, ds 
 
def pretrain_one_epoch(img_encoder, tab_encoder, projection, dataloader, 
device, optim): 
    img_encoder.train(); tab_encoder.train(); projection.train() 
    epoch_loss = 0.0; n = 0 
    for batch in dataloader: 
        images = batch["images_stack"][:,0].to(device)    
        tab = batch["tab_t"].to(device).float()          
        view1 = images + 0.01*torch.randn_like(images) 
        view2 = images + 0.01*torch.randn_like(images) 
        tab1 = tab + 0.01*torch.randn_like(tab) 
        tab2 = tab + 0.01*torch.randn_like(tab) 
        z_img1 = img_encoder(view1); z_tab1 = tab_encoder(tab1) 
        z_img2 = img_encoder(view2); z_tab2 = tab_encoder(tab2) 
        z1 = torch.cat([z_img1, z_tab1], dim=1) 
        z2 = torch.cat([z_img2, z_tab2], dim=1) 
        p1 = projection(z1); p2 = projection(z2) 
        loss = info_nce_loss(p1, p2) 
        optim.zero_grad(); loss.backward(); optim.step() 
 
117 
 
        epoch_loss += loss.item(); n += 1 
    return epoch_loss / max(1,n) 
 
def extract_projections(img_encoder, tab_encoder, projection, dataloader, 
device, max_samples=200): 
    img_encoder.eval(); tab_encoder.eval(); projection.eval() 
    reps = [] 
    metas = [] 
    with torch.no_grad(): 
        cnt = 0 
        for batch in dataloader: 
            images = batch["images_stack"][:,0].to(device) 
            tab = batch["tab_t"].to(device).float() 
            z_img = img_encoder(images); z_tab = tab_encoder(tab) 
            z = torch.cat([z_img, z_tab], dim=1) 
            p = projection(z) 
            reps.append(p.cpu().numpy()) 
            metas.extend(batch["meta"]) 
            cnt += p.shape[0] 
            if cnt >= max_samples: 
                break 
    reps = np.vstack(reps)[:max_samples] 
    return reps, metas 
 
def plot_loss_curves(history: Dict[str, List[float]], out_path=None): 
    plt.figure(figsize=(8,5)) 
    for name, arr in history.items(): 
        plt.plot(arr, label=name) 
    plt.xlabel("Epoch"); plt.ylabel("InfoNCE loss"); plt.legend(); 
plt.grid(True) 
    if out_path: 
        plt.savefig(out_path, dpi=150, bbox_inches="tight") 
    plt.show() 
 
def plot_bar_results(df_results, metric="final_loss", title="Comparison", 
out_path=None): 
    df_sorted = df_results.sort_values(metric) 
    plt.figure(figsize=(8,5)) 
    plt.barh(df_sorted["arch_name"], df_sorted[metric]) 
    plt.xlabel(metric); plt.title(title); plt.grid(axis="x") 
    if out_path: plt.savefig(out_path, dpi=150, bbox_inches="tight") 
    plt.show() 
 
def plot_embedding(reps, labels=None, method="tsne", out_path=None): 
    if method=="tsne": 
        emb = TSNE(n_components=2, random_state=0, 
init="pca").fit_transform(reps) 
    else: 
 
118 
 
        emb = PCA(n_components=2).fit_transform(reps) 
    plt.figure(figsize=(6,5)) 
    if labels is None: 
        plt.scatter(emb[:,0], emb[:,1], s=8) 
    else: 
        plt.scatter(emb[:,0], emb[:,1], c=labels, s=8, cmap="tab10") 
    plt.title(f"{method.upper()} embedding"); plt.axis("off") 
    if out_path: plt.savefig(out_path, dpi=150, bbox_inches="tight") 
    plt.show() 
 
 
def get_img_encoder(name, out_dim): 
    if name=="efficientnet": return EfficientNetEncoder(out_dim=out_dim) 
    if name=="resnet18": return ResNet18Encoder(out_dim=out_dim, 
pretrained=False) 
    if name=="cnn_medium": return CNNMediumEncoder(out_dim=out_dim) 
    raise ValueError(name) 
 
def get_tab_encoder(name, in_dim, out_dim): 
    if name=="tabtransformer": return TabTransformerEncoder(in_dim, 
out_dim=out_dim) 
    if name=="mlp": return SmallMLPTab(in_dim, out_dim=out_dim) 
    if name=="tab_medium": return TabMediumEncoder(in_dim, out_dim=out_dim) 
 
    raise ValueError(name) 
 
def get_projection(name, in_dim, proj_dim): 
    return ProjectionHead(in_dim, proj_dim=proj_dim) 
 
def run_pretrain_grid_search(dl_train, ds_train, year, cfg, img_choices, 
tab_choices, proj_choices, epochs=None): 
    results = [] 
    history_all = {} 
    device = cfg["device"] 
    sample = next(iter(dl_train)) 
    tab_t_sample = sample["tab_t"] 
    tab_dim = tab_t_sample.numel() // tab_t_sample.size(0) if 
tab_t_sample.dim()==2 else tab_t_sample.shape[-1] 
    for img_name, tab_name, proj_name in itertools.product(img_choices, 
tab_choices, proj_choices): 
        arch_name = f"{img_name}__{tab_name}__{proj_name}" 
        print("\n========== RUN:", arch_name) 
        set_seed(cfg["seed"]) 
        img_enc = get_img_encoder(img_name, cfg["latent_img_dim"]).to(device) 
        tab_enc = get_tab_encoder(tab_name, tab_dim, 
cfg["latent_tab_dim"]).to(device) 
        projection = get_projection(proj_name, 
cfg["latent_img_dim"]+cfg["latent_tab_dim"], 
 
119 
 
cfg["projection_dim"]).to(device) 
        optimizer = torch.optim.Adam(list(img_enc.parameters()) + 
list(tab_enc.parameters()) + list(projection.parameters()), lr=cfg["lr"]) 
        n_epochs = epochs or cfg["pretrain_epochs_search"] 
        losses = [] 
        start = time.time() 
        for e in range(1, n_epochs+1): 
            loss = pretrain_one_epoch(img_enc, tab_enc, projection, dl_train, 
device, optimizer) 
            losses.append(loss) 
            print(f"Epoch {e}/{n_epochs} loss={loss:.4f}") 
        dur = time.time()-start 
        print(f"Finished {arch_name} in {dur:.1f}s final_loss={losses[-
1]:.4f}") 
        ckpt = { 
            "img_encoder": img_enc.state_dict(), 
            "tab_encoder": tab_enc.state_dict(), 
            "projection": projection.state_dict(), 
            "optimizer": optimizer.state_dict(), 
            "arch": arch_name, 
            "final_loss": losses[-1] 
        } 
        torch.save(ckpt, os.path.join(cfg["save_dir"], 
f"pretrain_{arch_name}.pt")) 
        reps, metas = extract_projections(img_enc, tab_enc, projection, 
dl_train, device, max_samples=cfg["n_visualize_embeddings"]) 
        results.append({ 
            "arch_name": arch_name, 
            "img": img_name, "tab": tab_name, "projection": proj_name, 
            "final_loss": float(losses[-1]), 
            "loss_curve": losses, 
            "reps": reps 
        }) 
        history_all[arch_name] = losses 
    df = pd.DataFrame(results) 
    return df, history_all 
 
IMG_CHOICES = ["efficientnet", "cnn_medium", "resnet18"] 
TAB_CHOICES = ["tabtransformer", "mlp", "tab_medium"] 
PROJ_CHOICES = ["default"] 
 
dl_train, ds_train = build_dataloaders(CONFIG["manifest_path"], 
CONFIG["weather_root"], 2021, CONFIG) 
 
df_results, history = run_pretrain_grid_search(dl_train, ds_train, 2021, 
CONFIG, 
                                               IMG_CHOICES, TAB_CHOICES, 
PROJ_CHOICES, 
 
120 
 
                                               
epochs=CONFIG["pretrain_epochs_search"]) 
 
plot_loss_curves({k:v for k,v in history.items()}, 
out_path=os.path.join(CONFIG["save_dir"], "loss_curves.png")) 
plot_bar_results(df_results, metric="final_loss", 
out_path=os.path.join(CONFIG["save_dir"], "bar_final_loss.png")) 
 
best_row = df_results.sort_values("final_loss").iloc[0] 
print("Best arch:", best_row["arch_name"], "loss=", best_row["final_loss"]) 
best_reps = best_row["reps"] 
try: 
    plot_embedding(best_reps, method="tsne", 
out_path=os.path.join(CONFIG["save_dir"], "best_tsne.png")) 
except Exception as e: 
    print("TSNE failed, falling back to PCA:", e) 
    plot_embedding(best_reps, method="pca", 
out_path=os.path.join(CONFIG["save_dir"], "best_pca.png")) 
 
display(df_results[["arch_name","final_loss"]].sort_values("final_loss")) 
 
df_results[["arch_name","final_loss"]].to_csv(os.path.join(CONFIG["save_dir"]
, "grid_search_results.csv"), index=False) 
 
print("All done. Saved figures and checkpoints to", CONFIG["save_dir"]) 
 
%matplotlib inline 
from torchvision.models import efficientnet_b0 
import os, time, itertools, glob 
from typing import List, Dict, Callable, Optional, Tuple 
import math 
import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt 
from tqdm import tqdm 
 
import torch 
import torch.nn as nn 
import torch.nn.functional as F 
from torch.utils.data import Dataset, DataLoader 
from torchvision import transforms, models 
from PIL import Image, UnidentifiedImageError 
 
from sklearn.manifold import TSNE 
from sklearn.decomposition import PCA 
 
 
121 
 
class FieldMonthlyDataset(Dataset): 
    def __init__(self, manifest_df: pd.DataFrame, weather_root: str, years: 
List[int], 
                 transform_img=None, require_full_year=True, 
months_stack:int=1, weather_agg_fn=None): 
        self.manifest = manifest_df.copy().reset_index(drop=True) 
        self.weather_root = weather_root 
        self.transform_img = transform_img 
        self.months_stack = months_stack 
        self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn 
        self.weather_cache = {} 
        self._prepare_weather_cache(years) 
 
    def default_weather_agg_fn(self, monthly_df): 
        numeric = monthly_df.select_dtypes(include=[np.number]) 
        return numeric.fillna(0).values 
 
    def _load_weather_file(self, koatuu: str, year:int): 
        path_pattern = os.path.join(self.weather_root, str(year), 
f"{koatuu}*") 
        files = glob.glob(path_pattern) 
        if not files: 
            return None 
        try: 
            return pd.read_csv(files[0]) 
        except Exception: 
            return None 
 
    def _prepare_weather_cache(self, years: List[int]): 
        for _, row in self.manifest.iterrows(): 
            ko = str(row["koatuu"]) 
            yr = int(row["year"]) 
            key = (ko, yr) 
            if key in self.weather_cache: continue 
            if yr not in years: continue 
            df = self._load_weather_file(ko, yr) 
            if df is None: 
                self.weather_cache[key] = None 
            else: 
                monthly = monthly_aggregate_weather(df) 
                self.weather_cache[key] = self.weather_agg_fn(monthly) 
 
    def __len__(self): return len(self.manifest) 
 
    def __getitem__(self, idx): 
        row = self.manifest.iloc[idx] 
 
        try: 
 
122 
 
            img_path = row["image_path"] 
            try: 
                im = Image.open(img_path).convert("RGB") 
            except (UnidentifiedImageError, OSError): 
                import tifffile 
                im_np = tifffile.imread(img_path) 
                if im_np.dtype != np.uint8: 
                    im_np = (im_np / max(1, im_np.max()) * 
255).astype(np.uint8) 
                if im_np.ndim == 2: 
                    im_np = np.stack([im_np]*3, axis=-1) 
                elif im_np.shape[2] > 3: 
                    im_np = im_np[:, :, :3] 
                im = Image.fromarray(im_np) 
 
            if self.transform_img: 
                im_t = self.transform_img(im) 
            else: 
                im_t = transforms.ToTensor()(im) 
 
            images_stack = im_t.unsqueeze(0)  
 
            ko = str(row["koatuu"]); yr = int(row["year"]); mo = 
int(row["month"]) 
            cache = self.weather_cache.get((ko, yr), None) 
 
            if cache is None: 
                raise ValueError("Weather data missing or corrupt for this 
sample.") 
 
            idx0 = mo - 1 
            idx1 = (mo % 12) 
            tab_t = cache[idx0].astype(np.float32) 
            tab_t1 = cache[idx1].astype(np.float32) 
 
            out = { 
                "images_stack": images_stack,        
                "tab_t": torch.from_numpy(tab_t),       
                "tab_t1": torch.from_numpy(tab_t1), 
                "meta": row.to_dict() 
            } 
            return out 
 
        except Exception as e: 
            return None  
 
 
def collate_fn_skip_none(batch): 
 
123 
 
    batch = list(filter(lambda x: x is not None, batch)) 
 
    if not batch: 
        return None  
 
 
    from torch.utils.data._utils.collate import default_collate 
 
    if len(batch) == 0: 
        return None 
 
    return default_collate(batch) 
 
CONFIG = { 
    "data_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data", 
    "manifest_path": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields_with_vari.csv", 
    "weather_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather", 
    "image_size": (128, 128), 
    "batch_size": 8, 
    "num_workers": 0, 
    "device": "cuda" if torch.cuda.is_available() else "cpu", 
    "seed": 42, 
    "pretrain_epochs_search": 50, 
    "pretrain_epochs_full": 35, 
    "lr": 1e-3, 
    "train_frac": 0.8, 
    "require_full_12_months": True, 
    "save_dir": "./training_result", 
    "projection_dim": 128, 
    "latent_img_dim": 64, 
    "latent_tab_dim": 64, 
    "max_train_samples": 10000,   
    "max_val_samples": 1000, 
    "n_visualize_embeddings": 200, 
} 
 
 
def build_dataloaders(manifest_path, weather_root, year, cfg): 
    manifest = load_manifest(manifest_path) 
    if cfg["require_full_12_months"]: 
    if cfg.get("max_train_samples") is not None: 
        manifest = manifest.sample(min(cfg["max_train_samples"], 
 
124 
 
len(manifest)), random_state=cfg["seed"]) 
    transform_img = transforms.Compose([transforms.Resize(cfg["image_size"]), 
transforms.ToTensor()]) 
    ds = FieldMonthlyDataset(manifest, weather_root, years=[year], 
transform_img=transform_img) 
    dl = DataLoader( 
        ds, 
        batch_size=cfg["batch_size"], 
        shuffle=True, 
        num_workers=cfg["num_workers"], 
        drop_last=True, 
        collate_fn=collate_fn_skip_none  
    ) 
    return dl, ds 
 
BEST_IMG_NAME = "cnn_medium" 
BEST_TAB_NAME = "tabtransformer" 
BEST_PROJ_NAME = "default" 
BEST_ARCH_NAME = f"{BEST_IMG_NAME}__{BEST_TAB_NAME}__{BEST_PROJ_NAME}" 
TARGET_YEAR = 2021 
FULL_PRETRAIN_EPOCHS = CONFIG["pretrain_epochs_full"]  
 
 
def run_full_pretrain(img_name: str, tab_name: str, proj_name: str, year: 
int, cfg: dict): 
    arch_name = f"{img_name}__{tab_name}__{proj_name}" 
    device = cfg["device"] 
 
    print(f"\n=========================================================") 
    print(f"Starting Full Pretraining for: {arch_name}") 
    print(f"Total Epochs: {FULL_PRETRAIN_EPOCHS}, Device: {device}") 
    print(f"=========================================================") 
 
    dl_train, ds_train = build_dataloaders(cfg["manifest_path"], 
cfg["weather_root"], year, cfg) 
 
    sample = next(iter(dl_train)) 
    tab_t_sample = sample["tab_t"] 
    tab_dim = tab_t_sample.numel() // tab_t_sample.size(0) if 
tab_t_sample.dim()==2 else tab_t_sample.shape[-1] 
    print(f"Inferred Tabular Input Dimension: {tab_dim}") 
 
    set_seed(cfg["seed"]) 
    img_enc = get_img_encoder(img_name, cfg["latent_img_dim"]).to(device) 
    tab_enc = get_tab_encoder(tab_name, tab_dim, 
cfg["latent_tab_dim"]).to(device) 
    total_latent_dim = cfg["latent_img_dim"] + cfg["latent_tab_dim"] 
    projection = get_projection(proj_name, total_latent_dim, 
 
125 
 
cfg["projection_dim"]).to(device) 
 
    optimizer = torch.optim.Adam( 
        list(img_enc.parameters()) + list(tab_enc.parameters()) + 
list(projection.parameters()), 
        lr=cfg["lr"] 
    ) 
 
    losses = [] 
    best_loss = float('inf') 
    best_ckpt_path = os.path.join(cfg["save_dir"], 
f"pretrain_{arch_name}_best.pt") 
 
    start = time.time() 
    for e in range(1, FULL_PRETRAIN_EPOCHS + 1): 
        loss = pretrain_one_epoch(img_enc, tab_enc, projection, dl_train, 
device, optimizer) 
        losses.append(loss) 
 
        if loss < best_loss: 
            best_loss = loss 
            ckpt = { 
                "img_encoder": img_enc.state_dict(), 
                "tab_encoder": tab_enc.state_dict(), 
                "projection": projection.state_dict(), 
                "optimizer": optimizer.state_dict(), 
                "arch": arch_name, 
                "final_loss": loss 
            } 
            torch.save(ckpt, best_ckpt_path) 
            print(f"Epoch {e}/{FULL_PRETRAIN_EPOCHS} loss={loss:.4f} (SAVED 
BEST CKPT)") 
        else: 
            print(f"Epoch {e}/{FULL_PRETRAIN_EPOCHS} loss={loss:.4f}") 
 
    dur = time.time()-start 
    print(f"Finished Full Pretraining in {dur:.1f}s. Best loss achieved: 
{best_loss:.4f}") 
 
    print(f"Extracting {cfg['n_visualize_embeddings']} embeddings for 
visualization...") 
    best_ckpt = torch.load(best_ckpt_path, map_location=device) 
    img_enc.load_state_dict(best_ckpt["img_encoder"]) 
    tab_enc.load_state_dict(best_ckpt["tab_encoder"]) 
    projection.load_state_dict(best_ckpt["projection"]) 
 
    reps, metas = extract_projections(img_enc, tab_enc, projection, dl_train, 
device, 
 
126 
 
                                      
max_samples=cfg["n_visualize_embeddings"]) 
 
    plot_loss_curves({"pretrain_loss": losses}, 
                     out_path=os.path.join(cfg["save_dir"], 
f"loss_curve_{arch_name}.png")) 
 
    try: 
        plot_embedding(reps, method="tsne", 
                       out_path=os.path.join(cfg["save_dir"], 
f"tsne_{arch_name}.png")) 
    except Exception as e: 
        print(f"TSNE failed ({e}), falling back to PCA.") 
        plot_embedding(reps, method="pca", 
                       out_path=os.path.join(cfg["save_dir"], 
f"pca_{arch_name}.png")) 
 
    return img_enc, tab_enc, projection, best_loss, best_ckpt_path 
 
 
img_enc_trained, tab_enc_trained, proj_trained, final_loss, ckpt_path = 
run_full_pretrain( 
    BEST_IMG_NAME, BEST_TAB_NAME, BEST_PROJ_NAME, TARGET_YEAR, CONFIG 
) 
 
print(f"\nPretraining completed for {BEST_ARCH_NAME}.") 
print(f"Best model saved to: {ckpt_path}") 
 
import os 
import time 
import numpy as np 
import pandas as pd 
import torch 
import torch.nn as nn 
import torch.nn.functional as F 
from torch.utils.data import Dataset, DataLoader 
from torchvision import transforms 
from PIL import Image, UnidentifiedImageError 
import glob 
from typing import List, Dict, Tuple, Optional 
from tqdm import tqdm 
 
 
CONFIG = { 
    "data_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
 
127 
 
sed_data", 
    "manifest_path": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\manifest_fields_with_vari.csv", 
    "weather_root": 
r"C:\Users\nazar\Desktop\projects\masters_thesis\data\processed_fields\proces
sed_data\weather", 
    "image_size": (128, 128), 
    "batch_size": 8, 
    "num_workers": 0, 
    "device": "cuda" if torch.cuda.is_available() else "cpu", 
    "seed": 42, 
    "pretrain_epochs_full": 3, 
    "lr": 1e-3, 
    "train_frac": 0.8, 
    "require_full_12_months": True, 
    "save_dir": "./training_result", 
    "projection_dim": 128, 
    "latent_img_dim": 64, 
    "latent_tab_dim": 64, 
    "max_train_samples": 5000, 
    "max_val_samples": 500, 
    "n_visualize_embeddings": 200, 
    "target_column": "VARI",  
    "finetune_epochs": 50, 
    "finetune_lr": 5e-5, 
    "finetune_batch_size": 32,  
    "freeze_encoders": True,   
    "forecast_head_hidden": 128, 
} 
 
 
BEST_IMG_NAME = "cnn_medium" 
BEST_TAB_NAME = "tabtransformer" 
BEST_ARCH_NAME = f"{BEST_IMG_NAME}__{BEST_TAB_NAME}" 
TARGET_YEAR = 2021 
 
 
 
class FieldMonthlyDataset(Dataset): 
    def __init__(self, manifest_df: pd.DataFrame, weather_root: str, years: 
List[int], 
                 transform_img=None, require_full_year=True, 
months_stack:int=1, 
                 weather_agg_fn=None, target_column: Optional[str] = None): 
        self.manifest = manifest_df.copy().reset_index(drop=True) 
        self.weather_root = weather_root 
        self.transform_img = transform_img 
 
128 
 
        self.months_stack = months_stack 
        self.weather_agg_fn = weather_agg_fn or self.default_weather_agg_fn 
        self.weather_cache = {} 
        self.target_column = target_column # Додано 
        self._prepare_weather_cache(years) 
 
    def default_weather_agg_fn(self, monthly_df): 
        numeric = monthly_df.select_dtypes(include=[np.number]) 
        return numeric.fillna(0).values 
    # ---------------------------------------------------- 
 
    def _load_weather_file(self, koatuu: str, year:int): 
        path_pattern = os.path.join(self.weather_root, str(year), 
f"{koatuu}*") 
        files = glob.glob(path_pattern) 
        if not files: 
            return None 
        try: 
            return pd.read_csv(files[0]) 
        except Exception: 
            return None 
 
    def _prepare_weather_cache(self, years: List[int]): 
        for _, row in self.manifest.iterrows(): 
            ko = str(row["koatuu"]) 
            yr = int(row["year"]) 
            key = (ko, yr) 
            if key in self.weather_cache: continue 
            if yr not in years: continue 
            df = self._load_weather_file(ko, yr) 
            if df is None: 
                self.weather_cache[key] = None 
            else: 
                monthly = monthly_aggregate_weather(df) 
                self.weather_cache[key] = self.weather_agg_fn(monthly) 
 
    def __len__(self): return len(self.manifest) 
 
    def __getitem__(self, idx): 
        row = self.manifest.iloc[idx] 
 
        try: 
            img_path = row["image_path"] 
            im_t = transforms.ToTensor()(Image.new('RGB', (128, 128), 
color='black')) 
            images_stack = im_t.unsqueeze(0)   
 
            ko = str(row["koatuu"]); yr = int(row["year"]); mo = 
 
129 
 
int(row["month"]) 
            cache = self.weather_cache.get((ko, yr), None) 
 
            if cache is None: 
                raise ValueError("Weather data missing.") 
 
            idx0 = mo - 1 
            idx1 = (mo % 12) 
            tab_t = cache[idx0].astype(np.float32) 
            tab_t1 = cache[idx1].astype(np.float32) 
 
            out = { 
                "images_stack": images_stack,          
                "tab_t": torch.from_numpy(tab_t),      
                "tab_t1": torch.from_numpy(tab_t1),    
                "meta": row.to_dict() 
            } 
 
            target_col = self.target_column 
            if target_col and target_col in row and 
pd.notna(row[target_col]): 
                target = torch.tensor(row[target_col], dtype=torch.float32) 
                out["target"] = target.unsqueeze(0) 
            else: 
                 return None  
 
            return out 
 
        except Exception as e: 
            return None 
 
def collate_fn_skip_none(batch): 
    batch = list(filter(lambda x: x is not None, batch)) 
    if len(batch) == 0: 
        return None 
    from torch.utils.data._utils.collate import default_collate 
    return default_collate(batch) 
 
 
TARGET_COL_FINAL = "target_next_month" 
 
def prepare_supervised_manifest(manifest_df: pd.DataFrame, target_column: 
str) -> pd.DataFrame: 
 
    if manifest_df.empty: 
        return manifest_df 
 
    df = manifest_df.sort_values(by=['koatuu', 'year', 'month']).copy() 
 
130 
 
 
    df['field_year_id'] = df['koatuu'].astype(str) + '_' + 
df['year'].astype(str) 
 
    df[TARGET_COL_FINAL] = df.groupby('field_year_id')[target_column].shift(-
1) 
 
    df_prepared = df.dropna(subset=[TARGET_COL_FINAL]).reset_index(drop=True) 
 
    print(f"Manifest prepared. Original size: {len(manifest_df)}. Supervised 
size: {len(df_prepared)}") 
    return df_prepared 
 
def build_supervised_dataloaders(manifest_path, weather_root, year, cfg): 
    manifest = load_manifest(manifest_path) 
    if cfg["require_full_12_months"]: 
        manifest = filter_full_years(manifest, year) 
 
    manifest = prepare_supervised_manifest(manifest, cfg["target_column"]) 
 
    train_size = int(cfg["train_frac"] * len(manifest)) 
    manifest_train = manifest.sample(train_size, random_state=cfg["seed"]) 
    manifest_val = manifest.drop(manifest_train.index) 
 
    if cfg.get("max_train_samples") is not None: 
        manifest_train = manifest_train.sample(min(cfg["max_train_samples"], 
len(manifest_train)), random_state=cfg["seed"]) 
    if cfg.get("max_val_samples") is not None: 
        manifest_val = manifest_val.sample(min(cfg["max_val_samples"], 
len(manifest_val)), random_state=cfg["seed"]) 
 
    transform_img = transforms.Compose([transforms.Resize(cfg["image_size"]), 
transforms.ToTensor()]) 
 
    ds_train = FieldMonthlyDataset(manifest_train, weather_root, 
years=[year], 
                                   transform_img=transform_img, 
target_column=cfg["target_column"]) 
    ds_val = FieldMonthlyDataset(manifest_val, weather_root, years=[year], 
                                 transform_img=transform_img, 
target_column=cfg["target_column"]) 
 
    dl_train = DataLoader(ds_train, batch_size=cfg["finetune_batch_size"], 
shuffle=True, 
                          num_workers=cfg["num_workers"], drop_last=True, 
collate_fn=collate_fn_skip_none) 
    dl_val = DataLoader(ds_val, batch_size=cfg["finetune_batch_size"], 
shuffle=False, 
 
131 
 
                        num_workers=cfg["num_workers"], drop_last=False, 
collate_fn=collate_fn_skip_none) 
 
    return dl_train, dl_val, ds_train, ds_val 
 
 
class ForecastHead(nn.Module): 
    def __init__(self, in_dim, out_dim=1, hidden=128): 
        super().__init__() 
        self.net = nn.Sequential( 
            nn.Linear(in_dim, hidden), 
            nn.ReLU(), 
            nn.Linear(hidden, out_dim) 
        ) 
    def forward(self, z): 
        return self.net(z).squeeze(-1) 
 
 
class MTCRForecastModel(nn.Module): 
    def __init__(self, img_encoder, tab_encoder, forecast_head, 
freeze_encoders: bool = True): 
        super().__init__() 
        self.img_encoder = img_encoder 
        self.tab_encoder = tab_encoder 
        self.forecast_head = forecast_head 
        self.freeze_encoders = freeze_encoders 
 
        if self.freeze_encoders: 
            print("Freezing image and tabular encoders (Linear Probing).") 
            for param in self.img_encoder.parameters(): 
                param.requires_grad = False 
            for param in self.tab_encoder.parameters(): 
                param.requires_grad = False 
 
    def forward(self, image_t_input, tab_t_input, tab_t1_input): 
        self.img_encoder.eval() if self.freeze_encoders else 
self.img_encoder.train() 
        self.tab_encoder.eval() if self.freeze_encoders else 
self.tab_encoder.train() 
 
        z_img_t = self.img_encoder(image_t_input) 
        z_tab_t = self.tab_encoder(tab_t_input.float()) 
 
        z_t = torch.cat([z_img_t, z_tab_t], dim=1) 
         
        combined_input = torch.cat([z_t, tab_t1_input.float()], dim=1) 
 
 
 
132 
 
        prediction = self.forecast_head(combined_input) 
        return prediction 
 
 
 
def finetune_one_epoch(model: MTCRForecastModel, dataloader: DataLoader, 
device: str, optim: torch.optim.Optimizer, is_train: bool): 
    if is_train: 
        model.train() 
    else: 
        model.eval() 
 
    epoch_loss = 0.0 
    n = 0 
 
    with torch.set_grad_enabled(is_train): 
        for batch in dataloader: 
            if batch is None: continue  
 
            images_t = batch["images_stack"][:, 0].to(device) 
            tab_t = batch["tab_t"].to(device).float() 
            tab_t1 = batch["tab_t1"].to(device).float() 
            targets = batch["target"].to(device).float().squeeze(1) 
 
            if images_t.size(0) == 0: continue 
 
            predictions = model(images_t, tab_t, tab_t1) 
            loss = F.mse_loss(predictions, targets)  
 
            if is_train: 
                optim.zero_grad() 
                loss.backward() 
                optim.step() 
 
            epoch_loss += loss.item() * targets.size(0) 
            n += targets.size(0) 
 
    avg_loss = epoch_loss / max(1, n) 
    return avg_loss 
 
def train_finetune_model(model: MTCRForecastModel, dl_train: DataLoader, 
dl_val: DataLoader, cfg: dict, arch_name: str): 
    device = cfg["device"] 
    optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, 
model.parameters()), lr=cfg["finetune_lr"]) 
    history = {"train_loss": [], "val_loss": []} 
    best_val_loss = float('inf') 
    best_epoch = 0 
 
133 
 
    ckpt_path_template = os.path.join(cfg["save_dir"], 
f"finetune_VARI_{arch_name}_best.pt") 
 
    print(f"\n--- Starting Finetuning for {arch_name} 
({cfg['finetune_epochs']} epochs) ---") 
 
    for e in tqdm(range(1, cfg["finetune_epochs"] + 1), desc=f"Finetune 
{arch_name}"): 
        train_loss = finetune_one_epoch(model, dl_train, device, optimizer, 
is_train=True) 
        val_loss = finetune_one_epoch(model, dl_val, device, None, 
is_train=False) 
 
        history["train_loss"].append(train_loss) 
        history["val_loss"].append(val_loss) 
 
        print(f"Epoch {e}: Train MSE={train_loss:.4f}, Val 
MSE={val_loss:.4f}") 
 
        if val_loss < best_val_loss: 
            best_val_loss = val_loss 
            best_epoch = e 
            torch.save({ 
                "model_state_dict": model.state_dict(), 
                "val_loss": best_val_loss, 
                "epoch": best_epoch, 
                "arch": arch_name 
            }, ckpt_path_template) 
            print(f"SAVED BEST CKPT at Epoch {e}") 
 
    print(f"Finetuning finished. Best Val MSE: {best_val_loss:.4f} at epoch 
{best_epoch}") 
    return history, best_val_loss 
 
 
if 'img_enc_trained' not in locals() or 'tab_enc_trained' not in locals(): 
    print("WARNING: Encoders not found. Attempting to load best pretrain 
checkpoint...") 
 
 
dl_train_ft, dl_val_ft, ds_train_ft, ds_val_ft = 
build_supervised_dataloaders( 
    CONFIG["manifest_path"], CONFIG["weather_root"], TARGET_YEAR, CONFIG 
) 
 
latent_mtcr_dim = CONFIG["latent_img_dim"] + CONFIG["latent_tab_dim"] 
 
try: 
 
134 
 
    sample_tab_t1 = next(iter(dl_train_ft))["tab_t1"] 
    raw_tab_dim = sample_tab_t1.numel() // sample_tab_t1.size(0) 
except Exception as e: 
    print(f"Error: Could not determine raw_tab_dim. Dataloader may be 
empty/faulty. Error: {e}") 
    raise 
 
final_forecast_in_dim = latent_mtcr_dim + raw_tab_dim 
 
print(f"\nForecast Head Input Dimension: {latent_mtcr_dim} (Z_t) + 
{raw_tab_dim} (X_tab, t+1) = **{final_forecast_in_dim}**") 
 
 
forecast_head = ForecastHead( 
    in_dim=final_forecast_in_dim, 
    out_dim=1, 
    hidden=CONFIG["forecast_head_hidden"] 
).to(CONFIG["device"]) 
 
forecast_model = MTCRForecastModel( 
    img_enc_trained, 
    tab_enc_trained, 
    forecast_head, 
    freeze_encoders=CONFIG["freeze_encoders"] 
).to(CONFIG["device"]) 
 
 
finetune_history, final_val_loss = train_finetune_model( 
    forecast_model, dl_train_ft, dl_val_ft, CONFIG, BEST_ARCH_NAME 
) 
 
print(f"\nФінальне Supervised VARI Forecast Validation MSE для 
{BEST_ARCH_NAME}: {final_val_loss:.4f}") 
 
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score 
 
def evaluate_and_report_metrics(model: MTCRForecastModel, dl_val: DataLoader, 
device: str) -> Dict[str, float]: 
    model.eval() 
    all_preds = [] 
    all_targets = [] 
 
    with torch.no_grad(): 
        for batch in tqdm(dl_val, desc="Evaluating"): 
            if batch is None: continue 
 
 
135 
 
            images_t = batch["images_stack"][:, 0].to(device) 
            tab_t = batch["tab_t"].to(device).float() 
            tab_t1 = batch["tab_t1"].to(device).float() 
            targets = batch["target"].to(device).float().squeeze(1) 
 
            if targets.size(0) == 0: continue 
 
            predictions = model(images_t, tab_t, tab_t1) 
 
            all_preds.append(predictions.cpu().numpy()) 
            all_targets.append(targets.cpu().numpy()) 
 
    y_true = np.concatenate(all_targets) 
    y_pred = np.concatenate(all_preds) 
 
    mse = mean_squared_error(y_true, y_pred) 
    rmse = np.sqrt(mse) 
    mae = mean_absolute_error(y_true, y_pred) 
    r2 = r2_score(y_true, y_pred) 
 
    return { 
        "N_samples": len(y_true), 
        "MSE": mse, 
        "RMSE": rmse, 
        "MAE": mae, 
        "R2": r2, 
        "y_true": y_true, 
        "y_pred": y_pred 
    } 
 
 
 
import matplotlib.pyplot as plt 
import seaborn as sns 
 
def plot_analysis_charts(y_true: np.ndarray, y_pred: np.ndarray, 
title_prefix: str, save_dir: str): 
    plt.figure(figsize=(8, 6)) 
    sns.scatterplot(x=y_true, y=y_pred, alpha=0.6) 
 
    min_val = min(y_true.min(), y_pred.min()) 
    max_val = max(y_true.max(), y_pred.max()) 
    plt.plot([min_val, max_val], [min_val, max_val], 
             color='red', linestyle='--', label='Ідеальний прогноз (y=x)') 
 
    plt.title(f"{title_prefix} Predicted vs. True VARI") 
    plt.xlabel("Справжнє значення VARI ($y$)") 
    plt.ylabel("Прогнозоване значення VARI ($\hat{y}$)") 
 
136 
 
    plt.legend() 
    plt.grid(True, alpha=0.3) 
    plt.savefig(os.path.join(save_dir, 
f"{title_prefix}_Predicted_vs_True.png")) 
    plt.close() 
 
 
    residuals = y_true - y_pred 
 
    plt.figure(figsize=(8, 6)) 
    sns.scatterplot(x=y_pred, y=residuals, alpha=0.6) 
    plt.axhline(y=0, color='red', linestyle='-') 
 
    plt.title(f"{title_prefix} Аналіз Залишків") 
    plt.xlabel("Прогнозоване значення VARI ($\hat{y}$)") 
    plt.ylabel("Залишки ($y - \hat{y}$)") 
    plt.grid(True, alpha=0.3) 
    plt.savefig(os.path.join(save_dir, f"{title_prefix}_Residuals_Plot.png")) 
    plt.close() 
 
 
ckpt_path = os.path.join(CONFIG["save_dir"], 
f"finetune_VARI_{BEST_ARCH_NAME}_best.pt") 
 
if not os.path.exists(ckpt_path): 
    print(f"Error: Checkpoint file not found at {ckpt_path}. Ensure 
finetuning ran correctly.") 
else: 
    print(f"\n--- Завантаження та Оцінка моделі: {BEST_ARCH_NAME} ---") 
 
    best_ckpt = torch.load(ckpt_path, map_location=CONFIG["device"]) 
    forecast_model.load_state_dict(best_ckpt["model_state_dict"]) 
     
    results = evaluate_and_report_metrics(forecast_model, dl_val_ft, 
CONFIG["device"]) 
 
    print("\n" + "="*50) 
    print(f"КІНЦЕВА ОЦІНКА НА ВАЛІДАЦІЙНОМУ НАБОРІ ({BEST_ARCH_NAME})") 
    print(f"Кількість зразків: {results['N_samples']}") 
    print("-" * 50) 
    print(f"MSE (Mean Squared Error): {results['MSE']:.6f}") 
    print(f"RMSE (Root Mean Squared Error): {results['RMSE']:.4f}") 
    print(f"MAE (Mean Absolute Error): {results['MAE']:.4f}") 
    print(f"R2 Score (Коефіцієнт детермінації): {results['R2']:.4f}") 
    print("="*50) 
 
 
    plot_analysis_charts( 
 
137 
 
        results['y_true'], 
        results['y_pred'], 
        title_prefix=f"VARI_Forecast_{BEST_ARCH_NAME}", 
        save_dir=CONFIG["save_dir"] 
    ) 
    print(f"Графіки аналізу збережено у: {CONFIG['save_dir']}") 
import torch 
import numpy as np 
import pandas as pd 
 
def load_best_forecast_model(cfg, arch_name, img_enc, tab_enc, 
final_forecast_in_dim): 
    ckpt_path = os.path.join(cfg["save_dir"], 
f"finetune_VARI_{arch_name}_best.pt") 
 
    forecast_head = ForecastHead( 
        in_dim=final_forecast_in_dim, 
        out_dim=1, 
        hidden=cfg["forecast_head_hidden"] 
    ).to(cfg["device"]) 
 
    model = MTCRForecastModel( 
        img_enc, 
        tab_enc, 
        forecast_head, 
        freeze_encoders=False  
    ).to(cfg["device"]) 
 
    if os.path.exists(ckpt_path): 
        best_ckpt = torch.load(ckpt_path, map_location=cfg["device"]) 
        
        model.load_state_dict(best_ckpt["model_state_dict"]) 
        model.eval() 
        print(f"Модель прогнозування успішно завантажена з {ckpt_path}.") 
    else: 
        print(f"Помилка: Контрольна точка не знайдена за шляхом: 
{ckpt_path}") 
        raise FileNotFoundError(f"Missing checkpoint at {ckpt_path}") 
 
    return model 
 
 
def get_sample_batch(dataloader, n_samples=3): 
    samples = [] 
 
    for batch in dataloader: 
        if batch is None: continue 
 
138 
 
 
        for i in range(batch["images_stack"].size(0)): 
            sample = {key: batch[key][i].unsqueeze(0) for key in batch if key 
not in ["meta"]} 
            sample["meta"] = {key: batch["meta"][key][i] for key in 
batch["meta"]} 
            samples.append(sample) 
            if len(samples) >= n_samples: 
                return samples 
    return samples 
 
from IPython.display import display 
 
def run_simple_scenario_analysis(model, samples, device): 
    results = [] 
    T_index, P_index = 0, 1 
 
    scenarios = { 
        "Base_Forecast": (0, 0),        # Базовий прогноз (без змін) 
        "S1_HotDry": (+0.15, -0.15),    # +15% T, -15% P 
        "S2_ColdWet": (-0.15, +0.15),   # -15% T, +15% P 
        "S3_VeryHot": (+0.30, 0),       # +30% T, 0% P 
    } 
 
    model.eval() 
 
    for sample in samples: 
        base_tab_t1 = sample["tab_t1"].float().to(device) 
        base_pred = model(sample["images_stack"][:, 0].to(device), 
                          sample["tab_t"].to(device), base_tab_t1).item() 
 
        sample_results = { 
            "koatuu": sample["meta"]["koatuu"], 
            "month": sample["meta"]["month"], 
            "True_VARI": sample["target"].item(), 
            "Base_Pred_VARI": base_pred 
        } 
 
        for name, (t_shift, p_shift) in scenarios.items(): 
            scenario_tab_t1 = base_tab_t1.clone() 
 
            scenario_tab_t1[0, T_index] *= (1 + t_shift) 
            scenario_tab_t1[0, P_index] *= (1 + p_shift) 
 
            scenario_pred = model(sample["images_stack"][:, 0].to(device), 
                                  sample["tab_t"].to(device), 
 
139 
 
scenario_tab_t1).item() 
 
            sample_results[name] = scenario_pred 
 
        results.append(sample_results) 
 
    return pd.DataFrame(results) 
 
 
import pandas as pd 
import torch 
 
 
N_SAMPLES_FOR_SCENARIO = 5 
 
print(f"Завантаження {N_SAMPLES_FOR_SCENARIO} зразків з валідаційного 
набору...") 
 
try: 
    sample_data = get_sample_batch(dl_val_ft, 
n_samples=N_SAMPLES_FOR_SCENARIO) 
 
    if not sample_data: 
        print("Не вдалося завантажити жодного зразка. Перевірте, чи не 
порожній dl_val_ft.") 
    else: 
        print(f"Успішно завантажено {len(sample_data)} зразків.") 
 
        scenario_df = run_simple_scenario_analysis( 
            forecast_model, 
            sample_data, 
            CONFIG["device"] 
        ) 
 
        print("\n--- Результати Сценарного Аналізу (Перші 5 рядків) ---") 
        display(scenario_df.head()) 
 
except Exception as e: 
    print(f"Виникла помилка під час отримання зразків або аналізу: {e}") 
 
def run_feature_sensitivity_analysis(model, sample, feature_indices: 
List[int], perturbation: float = 0.1): 
    model.eval() 
 
    device = "cuda" 
 
 
140 
 
    base_tab_t1 = sample["tab_t1"].float().to(device) 
    base_pred = model(sample["images_stack"][:, 0].to(device), 
                      sample["tab_t"].to(device), base_tab_t1).item() 
 
    sensitivity_results = {} 
 
    for idx in feature_indices: 
        feature_name = f"Feature_{idx}" 
 
        # Сценарій + Perturbation 
        tab_t1_plus = base_tab_t1.clone() 
        tab_t1_plus[0, idx] *= (1 + perturbation) 
        pred_plus = model(sample["images_stack"][:, 0].to(device), 
                          sample["tab_t"].to(device), tab_t1_plus).item() 
 
        # Сценарій - Perturbation 
        tab_t1_minus = base_tab_t1.clone() 
        tab_t1_minus[0, idx] *= (1 - perturbation) 
        pred_minus = model(sample["images_stack"][:, 0].to(device), 
                           sample["tab_t"].to(device), tab_t1_minus).item() 
 
        impact_plus = (pred_plus - base_pred) 
        impact_minus = (pred_minus - base_pred) 
 
        sensitivity_results[feature_name] = { 
            "Base_VARI_Pred": base_pred, 
            "Pred_+": pred_plus, 
            "Pred_-": pred_minus, 
            "Impact_+": impact_plus, 
            "Impact_-": impact_minus, 
            "Total_Range": pred_plus - pred_minus  
        } 
 
    return sensitivity_results 
 
 
feature_indices_to_test = [0, 1, 2, 3]  
print(f"Аналізуються індекси ознак: {feature_indices_to_test}") 
 
 
all_sensitivity_results = [] 
N_SAMPLES_FOR_SCENARIO = 5 
sample_data = get_sample_batch(dl_val_ft, n_samples=N_SAMPLES_FOR_SCENARIO) 
 
 
for i, sample in enumerate(sample_data): 
 
    sensitivity_results = run_feature_sensitivity_analysis( 
 
141 
 
        model=forecast_model, 
        sample=sample, 
        feature_indices=feature_indices_to_test, 
        perturbation=0.1  # Зміна на +/- 10% 
    ) 
 
    sample_id = 
f"Sample_{i}_koatuu_{sample['meta']['koatuu']}_month_{sample['meta']['month']
}" 
 
    df_temp = pd.DataFrame.from_dict(sensitivity_results, orient='index') 
    df_temp['ID'] = sample_id 
    all_sensitivity_results.append(df_temp) 
 
    print(f"\n--- Результати Чутливості для Зразка {i} ---") 
    print(df_temp[['Total_Range', 'Impact_+', 'Impact_-
']].sort_values(by='Total_Range', ascending=False)) 
 
 
final_sensitivity_df = pd.concat(all_sensitivity_results) 
print("\nФінальні результати аналізу чутливості готові.") 
 
# download_sentinel_images.js 
var region = ee.FeatureCollection("projects/sentinel-images-
477301/assets/cherkassy_boundaries"); 
var geom = region.geometry().bounds();  
 
Map.centerObject(region); 
Map.addLayer(region, {}, 'Cherkasy boundary'); 
 
var maskS2Clouds = function(image) { 
  var scl = image.select('SCL'); 
  var mask = scl.neq(1).and(scl.neq(3)).and(scl.neq(8)) 
                         .and(scl.neq(9)).and(scl.neq(10)); 
  return image.updateMask(mask); 
}; 
 
var year = 2021; 
var start = ee.Date(year + '-01-01'); 
var end = ee.Date(year + '-12-31'); 
 
var s2 = ee.ImageCollection("COPERNICUS/S2_SR") 
           .filterBounds(geom) 
           .filterDate(start, end) 
           .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20)) 
           .map(maskS2Clouds); 
 
142 
 
 
var months = ee.List.sequence(1, 12); 
 
months.getInfo().forEach(function(m) { 
  var monthStart = ee.Date.fromYMD(year, m, 1); 
  var monthEnd = monthStart.advance(1, 'month'); 
 
  var monthlyMedian = s2.filterDate(monthStart, monthEnd) 
                        .median() 
                        .select(['B4','B3','B2']) // RGB 
                        .toUint16() 
                        .clip(geom); 
 
  var monthStr = ('0' + m).slice(-2); // 01, 02, ... 12 
  var desc = 'Cherkassy_' + year + '_' + monthStr + '_RGB_Median'; 
 
  Export.image.toDrive({ 
    image: monthlyMedian, 
    description: desc, 
    fileNamePrefix: desc, 
    folder: 'Cherkassy_S2', 
    scale: 10, 
    crs: 'EPSG:32636', 
    region: geom, 
    maxPixels: 1e13, 
    fileFormat: 'GeoTIFF', 
    formatOptions: {cloudOptimized: true} 
  }); 
}); 
 
 
Map.addLayer(s2.filterDate('2021-01-01','2021-01-31').median(),  
             {bands:['B4','B3','B2'], min:0, max:3000}, 'Jan RGB'); 
 
# problem_month.js 
 
var region = ee.FeatureCollection("projects/sentinel-images-
477301/assets/cherkassy_boundaries"); 
var geom = region.geometry().bounds(); 
Map.centerObject(region); 
Map.addLayer(region, {}, 'Cherkasy boundary'); 
 
var maskS2Clouds = function(image) { 
  var scl = image.select('SCL'); 
  var mask = scl.neq(1).and(scl.neq(3)).and(scl.neq(8)) 
 
143 
 
                         .and(scl.neq(9)).and(scl.neq(10)); 
  return image.updateMask(mask); 
}; 
 
var year = 2021; 
var month = 10; 
var monthStart = ee.Date.fromYMD(year, month, 1); 
var monthEnd   = monthStart.advance(1, 'month'); 
 
var s2_oct = ee.ImageCollection("COPERNICUS/S2_SR") 
  .filterBounds(geom) 
  .filterDate(monthStart, monthEnd) 
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20)) 
  .map(function(img) { 
    var bandNames = img.bandNames(); 
    return ee.Algorithms.If( 
      bandNames.contains('QA10'), 
      img, 
      null 
    ); 
  }, true)   
  .map(maskS2Clouds); 
   
var monthlyMedian = s2_oct.median() 
  .select(['B4','B3','B2']) 
  .toUint16() 
  .clip(geom); 
 
Map.addLayer(monthlyMedian, {min:0, max:3000}, 'October RGB Median'); 
 
Export.image.toDrive({ 
  image: monthlyMedian, 
  description: 'Cherkassy_2021_10_RGB_Median', 
  fileNamePrefix: 'Cherkassy_2021_10_RGB_Median', 
  folder: 'Cherkassy_S2', 
  scale: 10, 
  crs: 'EPSG:32636', 
  region: geom, 
  maxPixels: 1e13, 
  fileFormat: 'GeoTIFF', 
  formatOptions: {cloudOptimized: true} 
});