| from prefect import flow, task
|
| from prefect.artifacts import create_table_artifact
|
| from prefect.task_runners import ThreadPoolTaskRunner
|
| import numpy as np
|
| import pandas as pd
|
| from pipelines.preprocessing_pipeline import save_to_file
|
| from prefect.logging import get_run_logger
|
| import yaml
|
| import pickle
|
|
|
| from typing import Literal
|
|
|
| with open('config/params.yml') as file:
|
| config = yaml.safe_load(file)
|
|
|
| with open('config/fieldsensor_irrigator_mapping_anonym.yaml') as file:
|
| fieldsensor_irrigator_mapping = yaml.safe_load(file)
|
|
|
|
|
| data_availability = config['data_availability']
|
|
|
| datetime_col = config['datetime_col']
|
| value_col = config['value_col']
|
| datastream_id_col = config['datastream_id_col']
|
| datastream_name_col = config['datastream_name_col']
|
| sensor_type_col = config['sensor_type_col']
|
| ground_offset_col = config['ground_offset_col']
|
|
|
| resampling_window = config['resampling_window']
|
| days_weather_forecast = config['days_weather_forecast']
|
|
|
| meteo_method = config['meteo_method']
|
|
|
|
|
|
|
|
|
| @task(task_run_name="get_consortia")
|
| def get_consortia():
|
| """
|
| Retrieves the list of consortia from the configuration.
|
|
|
| Returns:
|
| List[str]: List of consortia names.
|
| """
|
| logger = get_run_logger()
|
| consortia = config['consortia']
|
| logger.info(f"We work with the following consortia: {consortia}")
|
| return consortia
|
|
|
|
|
| @task(task_run_name="read_input_data_{consortium_name}")
|
| def read_input_data(consortium_name, has_weather_data=True, has_crop_data=True, has_soil_data=True, has_remote_sensing_data=True):
|
| field_sensor_data = pd.read_parquet(f'data//03_primary//field_sensor_data_{consortium_name}.parquet')
|
| irrigation_data = pd.read_parquet(f'data//03_primary//irrigation_data_{consortium_name}.parquet')
|
| if has_weather_data:
|
| weather_data = pd.read_parquet(f'data//03_primary//weather_data_{consortium_name}.parquet')
|
| else:
|
| weather_data = None
|
| historical_weather_data = pd.read_parquet(f'data//03_primary//historical_weather_data_{consortium_name}.parquet')
|
| forecasted_weather_data = pd.read_parquet(f'data//03_primary//forecasted_weather_data_{consortium_name}.parquet')
|
| if has_crop_data:
|
| with open(f'data//03_primary//crop_type_data_{consortium_name}.pickle', 'rb') as handle:
|
| crop_type_data = pickle.load(handle)
|
| if has_soil_data:
|
| soil_type_data = pd.read_parquet(f'data//03_primary//soil_type_data_{consortium_name}.parquet')
|
| else:
|
| soil_type_data = None
|
| locations_ids = pd.read_parquet(f'data//03_primary//locations_ids_{consortium_name}.parquet')
|
|
|
| if has_remote_sensing_data:
|
|
|
|
|
| remote_sensing_data = pd.read_parquet(f'data//03_primary//remote_sensing_data_final_{consortium_name}.parquet')
|
| else:
|
| remote_sensing_data = None
|
|
|
|
|
| info_artifact = [
|
| {'variable': 'Length of field sensors DataFrame', 'amount': len(field_sensor_data) },
|
| {'variable': 'Length of irrigation data DataFrame', 'amount': len(irrigation_data) },
|
| {'variable': 'Length of weather data DataFrame', 'amount': len(weather_data) if weather_data is not None else None},
|
| {'variable': 'Length of historical weather data DataFrame', 'amount': len(historical_weather_data) },
|
| {'variable': 'Length of non-NaN field sensors DataFrame', 'amount': len(field_sensor_data.dropna()) },
|
| {'variable': 'Length of non-NaN irrigation data DataFrame', 'amount': len(irrigation_data.dropna()) },
|
| {'variable': 'Length of non-NaN weather data DataFrame', 'amount': len(weather_data.dropna()) if weather_data is not None else None},
|
| {'variable': 'Length of non-NaN historical weather data DataFrame', 'amount': len(historical_weather_data.dropna()) },
|
| ]
|
| create_table_artifact(
|
| key=f"data-{consortium_name}-info",
|
| table=info_artifact,
|
| description= "# Data info!"
|
| )
|
|
|
| return field_sensor_data, irrigation_data, weather_data, historical_weather_data, forecasted_weather_data, crop_type_data, soil_type_data, locations_ids, remote_sensing_data
|
|
|
|
|
| def get_type(name):
|
| if 'ELMED' in name:
|
| return 'ELMED'
|
| elif 'TN' in name:
|
| return 'TN'
|
| elif '_tens_' in name:
|
| return 'TN'
|
|
|
|
|
| def get_sector(sensor_name):
|
| candidates = [y for y in sensor_name.split(' ')[0].split('_') if 'sector' in y]
|
| if len(candidates) == 1:
|
| return candidates[0]
|
| else:
|
| return None
|
|
|
|
|
| def get_management(sensor_name):
|
| if '_management2' in sensor_name.lower():
|
| return 'management2'
|
| elif '_management1' in sensor_name.lower():
|
| return 'management1'
|
| elif 'management0' in sensor_name.lower():
|
| return 'management1'
|
| else:
|
| return ''
|
|
|
|
|
| def get_associated_sensors(sensors):
|
| sensors['type'] = sensors[datastream_name_col].transform(get_type)
|
| sensors['sector'] = sensors[datastream_name_col].transform(lambda x: get_sector(x))
|
| sensors['management'] = sensors[datastream_name_col].transform(lambda x: get_management(x))
|
| sensors['associated_sensors'] = sensors.apply(lambda x: sensors[(sensors['type'] == x['type'])&(sensors['sector'] == x['sector'])&(sensors['management'] == x['management'])&(sensors[datastream_name_col] != x[datastream_name_col])][datastream_name_col].values, axis=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| return sensors
|
|
|
|
|
| @task(task_run_name = 'get_sensors_meteo_{consortium_name}')
|
| def get_sensors_meteo(
|
| consortium_name:str,
|
| meteo_df: pd.DataFrame,
|
| location_ids: pd.DataFrame,
|
| sensor_df: pd.DataFrame,
|
| method: Literal['closest', 'interpolated'] = 'closest',
|
| idw_power: float = 2.0
|
| ) -> pd.DataFrame:
|
| """
|
| Enrich field sensor locations with meteo data from weather sensors.
|
|
|
| Parameters:
|
| -----------
|
| consortium_name: str
|
| Name of the processing consortium
|
| meteo_df : pd.DataFrame
|
| Time-series meteo data with datetime index and columns like
|
| 'Air temperature F0057_mean', 'Precipitation F5108', etc.
|
| location_ids : pd.DataFrame
|
| Location data with columns: 'datastream_name', 'x' (lon), 'y' (lat)
|
| sensor_df : pd.DataFrame
|
| Field sensor data containing 'datastream_name' column. Unique values
|
| identify the target locations to enrich.
|
| method : str
|
| 'closest' for nearest sensor, 'interpolated' for IDW
|
| idw_power : float
|
| Power parameter for inverse distance weighting (default: 2.0)
|
|
|
| Returns:
|
| --------
|
| pd.DataFrame
|
| Enriched dataframe with columns: 'result_time', 'location_id',
|
| and sensor-agnostic meteo features (e.g., 'temperature_mean_local_meteo')
|
| """
|
|
|
|
|
| if datetime_col in meteo_df.columns:
|
| meteo_df = meteo_df.set_index(datetime_col)
|
|
|
|
|
| meteo_columns = {}
|
|
|
| for col in meteo_df.columns:
|
|
|
| matching = location_ids[
|
| location_ids[datastream_name_col].apply(lambda x: x in col if pd.notna(x) else False)
|
| ]
|
|
|
| if len(matching) > 0:
|
|
|
| datastream_name = matching.iloc[0][datastream_name_col]
|
|
|
|
|
| suffix = col.replace(datastream_name, '').strip('_')
|
|
|
|
|
| base_metric = datastream_name.rsplit('_', 1)[0].lower().replace(' ', '_')
|
|
|
|
|
| if suffix:
|
| metric_key = f"{base_metric}_{suffix}"
|
| else:
|
| metric_key = base_metric
|
|
|
| if metric_key not in meteo_columns:
|
| meteo_columns[metric_key] = []
|
|
|
| meteo_columns[metric_key].append({
|
| 'col_name': col,
|
| datastream_name_col: datastream_name,
|
| 'sensor_id': datastream_name.rsplit('_', 1)[1] if ' ' in datastream_name else datastream_name
|
| })
|
|
|
|
|
| meteo_sensors = {}
|
| for metric_data in meteo_columns.values():
|
| for sensor_info in metric_data:
|
| ds_name = sensor_info[datastream_name_col]
|
| if ds_name not in meteo_sensors:
|
| loc = location_ids[location_ids[datastream_name_col] == ds_name]
|
| if len(loc) > 0:
|
| meteo_sensors[ds_name] = (loc.iloc[0]['x'], loc.iloc[0]['y'])
|
|
|
|
|
| target_datastream_names = sensor_df[datastream_name_col].unique()
|
|
|
|
|
| target_locations = location_ids[
|
| location_ids[datastream_name_col].isin(target_datastream_names)
|
| ].copy()
|
|
|
|
|
| results = []
|
|
|
| for _, target_loc in target_locations.iterrows():
|
| target_name = target_loc[datastream_name_col]
|
| target_x, target_y = target_loc['x'], target_loc['y']
|
|
|
|
|
| for timestamp in meteo_df.index:
|
| row_data = {
|
| datetime_col: timestamp,
|
| datastream_name_col: target_name
|
| }
|
|
|
|
|
| for metric_key, sensor_list in meteo_columns.items():
|
|
|
| available_sensors = []
|
|
|
| for sensor_info in sensor_list:
|
| col_name = sensor_info['col_name']
|
| ds_name = sensor_info[datastream_name_col]
|
|
|
| if ds_name in meteo_sensors and pd.notna(meteo_df.loc[timestamp, col_name]):
|
| sensor_x, sensor_y = meteo_sensors[ds_name]
|
| value = meteo_df.loc[timestamp, col_name]
|
|
|
|
|
| distance = np.sqrt((target_x - sensor_x) ** 2 + (target_y - sensor_y) ** 2)
|
|
|
| available_sensors.append({
|
| 'value': value,
|
| 'distance': distance
|
| })
|
|
|
|
|
| if len(available_sensors) == 0:
|
| local_value = np.nan
|
| elif method == 'closest' or len(available_sensors) == 1:
|
|
|
| closest = min(available_sensors, key=lambda s: s['distance'])
|
| local_value = closest['value']
|
| else:
|
|
|
| total_weight = 0
|
| weighted_sum = 0
|
|
|
| for sensor in available_sensors:
|
| if sensor['distance'] == 0:
|
|
|
| local_value = sensor['value']
|
| break
|
| else:
|
| weight = 1 / (sensor['distance'] ** idw_power)
|
| weighted_sum += weight * sensor['value']
|
| total_weight += weight
|
| else:
|
|
|
| local_value = weighted_sum / total_weight if total_weight > 0 else np.nan
|
|
|
| row_data[f"{metric_key}_local_meteo"] = local_value
|
|
|
| results.append(row_data)
|
|
|
|
|
| result_df = pd.DataFrame(results)
|
|
|
|
|
| meteo_cols = [col for col in result_df.columns if col.endswith('_local_meteo')]
|
| result_df = result_df[[datetime_col, datastream_name_col] + meteo_cols]
|
|
|
| return result_df
|
|
|
| @task(task_run_name='merge_all_data_{consortium_name}')
|
| def merge_all_data(consortium_name, field_sensor_data, irrigation_data, weather_data, historical_weather_data, forecasted_weather_data, remote_sensing_data, location_ids):
|
| associated_irrigators = []
|
| for year in fieldsensor_irrigator_mapping[consortium_name]:
|
| tmp = pd.DataFrame(fieldsensor_irrigator_mapping[consortium_name][year].items(), columns=[datastream_name_col, 'irrigator'])
|
| tmp['year'] = year
|
| associated_irrigators.append(tmp)
|
| associated_irrigators = pd.concat(associated_irrigators)
|
|
|
| field_sensor_data['year'] = field_sensor_data.index.year
|
|
|
| full_table = field_sensor_data.drop(columns=[datastream_id_col]).reset_index().merge(
|
| associated_irrigators,
|
| on = [datastream_name_col, 'year'],
|
| how = 'left'
|
| ).drop(columns=['year']).merge(
|
| irrigation_data.drop(columns=[datastream_id_col]).reset_index().rename(columns={datastream_name_col: 'irrigator', value_col: 'irrigation'}),
|
| on = ['irrigator', datetime_col],
|
| how = 'left'
|
| ).merge(
|
| forecasted_weather_data,
|
| on = [datetime_col, datastream_name_col],
|
| how= 'left'
|
| ).merge(
|
| historical_weather_data.reset_index(),
|
| on = [datetime_col, datastream_name_col],
|
| how= 'left'
|
| )
|
|
|
| if weather_data is not None:
|
| full_table = full_table.merge(
|
| weather_data,
|
| on=[datetime_col, datastream_name_col],
|
| how='left'
|
| )
|
|
|
| if remote_sensing_data is not None:
|
|
|
| indices = [
|
| "ndvi", "grvi", "rvi", "rgi", "aci", "maci", "gndvi", "ngrdi", "ngbdi", "bgvi", "brvi",
|
| "wi", "varig", "gli", "g_perc", "ndmi", "ndwi", "reci", "ndre_lower_end",
|
| "ndre_upper_end", "msavi", "arvi", "sipi", "gci"
|
| ]
|
|
|
| full_table = full_table.merge(
|
| remote_sensing_data[[datastream_name_col ,datetime_col] + indices],
|
| on=[datastream_name_col, datetime_col],
|
| how='left'
|
| )
|
|
|
| full_table['irrigation'] = full_table['irrigation'].fillna(0)
|
|
|
| sensors = pd.DataFrame(full_table[datastream_name_col].drop_duplicates().reset_index(drop=True))
|
| sensors = get_associated_sensors(sensors)
|
|
|
| full_table = full_table.merge(
|
| sensors[[datastream_name_col, 'associated_sensors']],
|
| on = [datastream_name_col],
|
| how = 'left'
|
| )
|
|
|
| return full_table
|
|
|
|
|
| @task(task_run_name='add_irrigation_forecast')
|
| def add_irrigation_forecast(full_table):
|
| for n in range(days_weather_forecast * 24 // int(resampling_window.split('h')[0])):
|
| full_table[f'forecasted_irrigation_next_{n+1}period'] = full_table['irrigation'].shift(-n)
|
| full_table[f'forecasted_irrigation_next_{n+1}period'] = full_table[f'forecasted_irrigation_next_{n+1}period'].fillna(0)
|
|
|
| return full_table
|
|
|
|
|
| @flow(name='model_preparation_pipeline', retries=1, task_runner=ThreadPoolTaskRunner())
|
| def model_preparation_pipeline(override_consortia=None, override_data_availability=None) -> list[str]:
|
| logger = get_run_logger()
|
| logger.info(f'Starting model preparation pipeline!')
|
|
|
| if not override_consortia:
|
| consortia = get_consortia()
|
| else:
|
| consortia = override_consortia
|
|
|
| for consortium_name in consortia:
|
| if override_data_availability:
|
| data_availability_consortium = override_data_availability
|
| else:
|
| data_availability_consortium = data_availability[consortium_name]
|
|
|
|
|
| field_sensor_data, irrigation_data, weather_data, historical_weather_data, forecasted_weather_data, crop_type_data, soil_type_data, location_ids, remote_sensing_data = read_input_data(consortium_name, **data_availability_consortium)
|
|
|
|
|
|
|
| if data_availability_consortium['has_weather_data']:
|
| weather_data = get_sensors_meteo(
|
| consortium_name=consortium_name,
|
| meteo_df = weather_data,
|
| location_ids = location_ids,
|
| sensor_df = field_sensor_data,
|
| method = meteo_method)
|
|
|
|
|
| full_table = merge_all_data.submit(consortium_name, field_sensor_data, irrigation_data, weather_data, historical_weather_data, forecasted_weather_data, remote_sensing_data, location_ids).result()
|
| full_table = add_irrigation_forecast(full_table)
|
|
|
| save_to_file(
|
| df=full_table,
|
| output_file=f'data//04_model_input//full_table_{consortium_name}.parquet',
|
| )
|
|
|
| return
|
|
|
|
|
| if __name__ == "__main__":
|
| model_preparation_pipeline() |