1078 lines
45 KiB
Python
1078 lines
45 KiB
Python
import pandas as pd
|
|
import numpy as np
|
|
import plotly.express as px
|
|
import plotly.graph_objects as go
|
|
from dash import Dash, dcc, html, Input, Output, State, callback
|
|
import tensorflow as tf
|
|
import joblib
|
|
from sklearn.preprocessing import StandardScaler
|
|
import dash_bootstrap_components as dbc
|
|
from datetime import datetime, timedelta
|
|
import re
|
|
|
|
@tf.keras.utils.register_keras_serializable()
|
|
class WarmUpLearningRateSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
|
|
def __init__(self, initial_learning_rate=1e-3, warmup_steps=1000, decay_steps=10000):
|
|
super().__init__()
|
|
self.initial_learning_rate = initial_learning_rate
|
|
self.warmup_steps = warmup_steps
|
|
self.decay_steps = decay_steps
|
|
|
|
def __call__(self, step):
|
|
warmup_pct = tf.cast(step, tf.float32) / self.warmup_steps
|
|
warmup_lr = self.initial_learning_rate * warmup_pct
|
|
decay_factor = tf.pow(0.1, tf.cast(step, tf.float32) / self.decay_steps)
|
|
decayed_lr = self.initial_learning_rate * decay_factor
|
|
final_lr = tf.where(step < self.warmup_steps, warmup_lr, decayed_lr)
|
|
return final_lr
|
|
|
|
def get_config(self):
|
|
return {
|
|
'initial_learning_rate': self.initial_learning_rate,
|
|
'warmup_steps': self.warmup_steps,
|
|
'decay_steps': self.decay_steps
|
|
}
|
|
|
|
# Definizione delle classi del modello
|
|
class PositionalEncoding(tf.keras.layers.Layer):
|
|
def __init__(self, position, d_model):
|
|
super(PositionalEncoding, self).__init__()
|
|
self.pos_encoding = self.positional_encoding(position, d_model)
|
|
|
|
def get_angles(self, position, i, d_model):
|
|
angles = 1 / tf.pow(10000, (2 * (i // 2)) / tf.cast(d_model, tf.float32))
|
|
return position * angles
|
|
|
|
def positional_encoding(self, position, d_model):
|
|
angle_rads = self.get_angles(
|
|
position=tf.range(position, dtype=tf.float32)[:, tf.newaxis],
|
|
i=tf.range(d_model, dtype=tf.float32)[tf.newaxis, :],
|
|
d_model=d_model)
|
|
|
|
sines = tf.math.sin(angle_rads[:, 0::2])
|
|
cosines = tf.math.cos(angle_rads[:, 1::2])
|
|
|
|
pos_encoding = tf.concat([sines, cosines], axis=-1)
|
|
pos_encoding = pos_encoding[tf.newaxis, ...]
|
|
return tf.cast(pos_encoding, tf.float32)
|
|
|
|
def call(self, inputs):
|
|
return inputs + self.pos_encoding[:, :tf.shape(inputs)[1], :]
|
|
|
|
class TemporalAugmentation(tf.keras.layers.Layer):
|
|
def __init__(self, noise_factor=0.03, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.noise_factor = noise_factor
|
|
|
|
def call(self, inputs, training=None):
|
|
if training:
|
|
noise = tf.random.normal(
|
|
shape=tf.shape(inputs),
|
|
mean=0.0,
|
|
stddev=self.noise_factor
|
|
)
|
|
return inputs + noise
|
|
return inputs
|
|
|
|
class EnhancedTransformerBlock(tf.keras.layers.Layer):
|
|
def __init__(self, d_model, num_heads, ff_dim, dropout=0.1):
|
|
super().__init__()
|
|
self.att = tf.keras.layers.MultiHeadAttention(
|
|
num_heads=num_heads,
|
|
key_dim=d_model // num_heads,
|
|
value_dim=d_model // num_heads
|
|
)
|
|
self.ffn = tf.keras.Sequential([
|
|
tf.keras.layers.Dense(ff_dim, activation="gelu"),
|
|
tf.keras.layers.Dropout(dropout),
|
|
tf.keras.layers.Dense(d_model)
|
|
])
|
|
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
|
|
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
|
|
self.dropout1 = tf.keras.layers.Dropout(dropout)
|
|
self.dropout2 = tf.keras.layers.Dropout(dropout)
|
|
self.residual_attention = tf.keras.layers.Dense(d_model, activation='sigmoid')
|
|
|
|
def call(self, inputs, training):
|
|
attn_output = self.att(inputs, inputs)
|
|
attn_output = self.dropout1(attn_output, training=training)
|
|
residual_weights = self.residual_attention(inputs)
|
|
out1 = self.layernorm1(inputs + residual_weights * attn_output)
|
|
|
|
ffn_output = self.ffn(out1)
|
|
ffn_output = self.dropout2(ffn_output, training=training)
|
|
return self.layernorm2(out1 + ffn_output)
|
|
|
|
class TemporalPoolingLayer(tf.keras.layers.Layer):
|
|
def __init__(self, num_heads, key_dim, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.attention_pooling = tf.keras.layers.MultiHeadAttention(
|
|
num_heads=num_heads,
|
|
key_dim=key_dim
|
|
)
|
|
self.temporal_pooling = tf.keras.layers.GlobalAveragePooling1D()
|
|
self.max_pooling = tf.keras.layers.GlobalMaxPooling1D()
|
|
self.concat = tf.keras.layers.Concatenate(axis=-1)
|
|
|
|
def call(self, inputs, training=None):
|
|
att_output = self.attention_pooling(inputs, inputs)
|
|
avg_output = self.temporal_pooling(inputs)
|
|
max_output = self.max_pooling(inputs)
|
|
att_output = tf.reduce_mean(att_output, axis=1)
|
|
return self.concat([att_output, avg_output, max_output])
|
|
|
|
@tf.keras.utils.register_keras_serializable()
|
|
class OliveOilTransformer(tf.keras.Model):
|
|
def __init__(self, temporal_shape=None, static_shape=None, num_outputs=None,
|
|
d_model=128, num_heads=8, ff_dim=256, num_transformer_blocks=4,
|
|
mlp_units=[256, 128, 64], dropout=0.2, **kwargs):
|
|
super(OliveOilTransformer, self).__init__(**kwargs)
|
|
|
|
self.temporal_shape = temporal_shape
|
|
self.static_shape = static_shape
|
|
self.num_outputs = num_outputs
|
|
self.d_model = d_model
|
|
self.num_heads = num_heads
|
|
self.ff_dim = ff_dim
|
|
self.num_transformer_blocks = num_transformer_blocks
|
|
self.mlp_units = mlp_units
|
|
self.dropout_rate = dropout
|
|
|
|
if temporal_shape is not None and static_shape is not None and num_outputs is not None:
|
|
self.build_model()
|
|
|
|
def build_model(self):
|
|
# Input layers
|
|
self.temporal_input = tf.keras.layers.Input(shape=self.temporal_shape, name='temporal_input')
|
|
self.static_input = tf.keras.layers.Input(shape=self.static_shape, name='static_input')
|
|
|
|
# Input normalization
|
|
self.temporal_normalization = tf.keras.layers.LayerNormalization(epsilon=1e-6)
|
|
self.static_normalization = tf.keras.layers.LayerNormalization(epsilon=1e-6)
|
|
|
|
# Data Augmentation
|
|
self.temporal_augmentation = TemporalAugmentation(noise_factor=0.03)
|
|
|
|
# Temporal path
|
|
self.temporal_projection = tf.keras.Sequential([
|
|
tf.keras.layers.Dense(self.d_model//2, activation='gelu',
|
|
kernel_regularizer=tf.keras.regularizers.l2(1e-5)),
|
|
tf.keras.layers.Dropout(self.dropout_rate),
|
|
tf.keras.layers.Dense(self.d_model, activation='gelu',
|
|
kernel_regularizer=tf.keras.regularizers.l2(1e-5))
|
|
])
|
|
|
|
self.pos_encoding = PositionalEncoding(position=self.temporal_shape[0], d_model=self.d_model)
|
|
|
|
# Transformer blocks
|
|
self.transformer_blocks = [
|
|
EnhancedTransformerBlock(self.d_model, self.num_heads, self.ff_dim, self.dropout_rate)
|
|
for _ in range(self.num_transformer_blocks)
|
|
]
|
|
|
|
# Temporal pooling
|
|
self.temporal_pooling = TemporalPoolingLayer(
|
|
num_heads=self.num_heads,
|
|
key_dim=self.d_model//4
|
|
)
|
|
|
|
# Static path
|
|
self.static_encoder = tf.keras.Sequential([
|
|
tf.keras.layers.Dense(256, activation='gelu',
|
|
kernel_regularizer=tf.keras.regularizers.l2(1e-5)),
|
|
tf.keras.layers.Dropout(self.dropout_rate),
|
|
tf.keras.layers.Dense(128, activation='gelu',
|
|
kernel_regularizer=tf.keras.regularizers.l2(1e-5)),
|
|
tf.keras.layers.Dropout(self.dropout_rate),
|
|
tf.keras.layers.Dense(64, activation='gelu',
|
|
kernel_regularizer=tf.keras.regularizers.l2(1e-5))
|
|
])
|
|
|
|
# Feature fusion
|
|
self.fusion_layer = tf.keras.layers.Concatenate()
|
|
|
|
# MLP head
|
|
self.mlp_layers = []
|
|
for units in self.mlp_units:
|
|
self.mlp_layers.extend([
|
|
tf.keras.layers.BatchNormalization(),
|
|
tf.keras.layers.Dense(units, activation="gelu",
|
|
kernel_regularizer=tf.keras.regularizers.l2(1e-5)),
|
|
tf.keras.layers.Dropout(self.dropout_rate)
|
|
])
|
|
|
|
# Output layer
|
|
self.final_layer = tf.keras.layers.Dense(
|
|
self.num_outputs,
|
|
activation='linear',
|
|
kernel_regularizer=tf.keras.regularizers.l2(1e-5)
|
|
)
|
|
|
|
# Build model
|
|
temporal_encoded = self.encode_temporal(self.temporal_input, training=True)
|
|
static_encoded = self.encode_static(self.static_input)
|
|
combined = self.fusion_layer([temporal_encoded, static_encoded])
|
|
|
|
x = combined
|
|
for layer in self.mlp_layers:
|
|
x = layer(x)
|
|
|
|
outputs = self.final_layer(x)
|
|
|
|
self._model = tf.keras.Model(
|
|
inputs={'temporal': self.temporal_input, 'static': self.static_input},
|
|
outputs=outputs
|
|
)
|
|
|
|
def encode_temporal(self, x, training=None):
|
|
x = self.temporal_normalization(x)
|
|
x = self.temporal_augmentation(x, training=training)
|
|
x = self.temporal_projection(x)
|
|
x = self.pos_encoding(x)
|
|
|
|
skip_connection = x
|
|
for transformer in self.transformer_blocks:
|
|
x = transformer(x, training=training)
|
|
x = tf.keras.layers.Add()([x, skip_connection])
|
|
|
|
return self.temporal_pooling(x)
|
|
|
|
def encode_static(self, x):
|
|
x = self.static_normalization(x)
|
|
return self.static_encoder(x)
|
|
|
|
def call(self, inputs, training=None):
|
|
temporal_input = inputs['temporal']
|
|
static_input = inputs['static']
|
|
|
|
temporal_encoded = self.encode_temporal(temporal_input, training)
|
|
static_encoded = self.encode_static(static_input)
|
|
|
|
combined = self.fusion_layer([temporal_encoded, static_encoded])
|
|
|
|
x = combined
|
|
for layer in self.mlp_layers:
|
|
x = layer(x, training=training)
|
|
|
|
return self.final_layer(x)
|
|
|
|
def model(self):
|
|
return self._model
|
|
|
|
def get_config(self):
|
|
config = super().get_config()
|
|
config.update({
|
|
"temporal_shape": self.temporal_shape,
|
|
"static_shape": self.static_shape,
|
|
"num_outputs": self.num_outputs,
|
|
"d_model": self.d_model,
|
|
"num_heads": self.num_heads,
|
|
"ff_dim": self.ff_dim,
|
|
"num_transformer_blocks": self.num_transformer_blocks,
|
|
"mlp_units": self.mlp_units,
|
|
"dropout": self.dropout_rate
|
|
})
|
|
return config
|
|
|
|
@classmethod
|
|
def from_config(cls, config):
|
|
return cls(**config)
|
|
|
|
# Caricamento dati e modello
|
|
print("Caricamento dati...")
|
|
simulated_data = pd.read_parquet("./data/simulated_data.parquet")
|
|
weather_data = pd.read_parquet("./data/weather_data_complete.parquet")
|
|
olive_varieties = pd.read_parquet("./data/olive_varieties.parquet")
|
|
|
|
print("Caricamento modello e scaler...")
|
|
model = tf.keras.models.load_model('./models/oli_transformer/olive_transformer.keras',
|
|
custom_objects={
|
|
'OliveOilTransformer': OliveOilTransformer,
|
|
'PositionalEncoding': PositionalEncoding,
|
|
'TemporalAugmentation': TemporalAugmentation,
|
|
'EnhancedTransformerBlock': EnhancedTransformerBlock,
|
|
'TemporalPoolingLayer': TemporalPoolingLayer,
|
|
'WarmUpLearningRateSchedule': WarmUpLearningRateSchedule
|
|
})
|
|
|
|
scaler_temporal = joblib.load('./models/oli_transformer/scaler_temporal.joblib')
|
|
scaler_static = joblib.load('./models/oli_transformer/scaler_static.joblib')
|
|
scaler_y = joblib.load('./models/oli_transformer/scaler_y.joblib')
|
|
|
|
def clean_column_name(name):
|
|
# Rimuove caratteri speciali e spazi, converte in snake_case e abbrevia
|
|
name = re.sub(r'[^a-zA-Z0-9\s]', '', name) # Rimuove caratteri speciali
|
|
name = name.lower().replace(' ', '_') # Converte in snake_case
|
|
|
|
# Abbreviazioni comuni
|
|
abbreviations = {
|
|
'production': 'prod',
|
|
'percentage': 'pct',
|
|
'hectare': 'ha',
|
|
'tonnes': 't',
|
|
'litres': 'l',
|
|
'minimum': 'min',
|
|
'maximum': 'max',
|
|
'average': 'avg'
|
|
}
|
|
|
|
for full, abbr in abbreviations.items():
|
|
name = name.replace(full, abbr)
|
|
|
|
return name
|
|
|
|
# Funzioni di supporto per la dashboard
|
|
def prepare_static_features_multiple(varieties_info, percentages, hectares, all_varieties):
|
|
"""
|
|
Prepara le feature statiche per multiple varietà seguendo la struttura esatta della simulazione.
|
|
|
|
Args:
|
|
varieties_info (list): Lista di dizionari contenenti le informazioni sulle varietà selezionate
|
|
percentages (list): Lista delle percentuali corrispondenti a ciascuna varietà selezionata
|
|
hectares (float): Numero di ettari totali
|
|
all_varieties (list): Lista di tutte le possibili varietà nel dataset originale
|
|
|
|
Returns:
|
|
np.array: Array numpy contenente tutte le feature statiche
|
|
"""
|
|
# Inizializza un dizionario per tutte le varietà possibili
|
|
variety_data = {variety: {
|
|
'pct': 0,
|
|
'prod_t_ha': 0,
|
|
'oil_prod_t_ha': 0,
|
|
'oil_prod_l_ha': 0,
|
|
'min_yield_pct': 0,
|
|
'max_yield_pct': 0,
|
|
'min_oil_prod_l_ha': 0,
|
|
'max_oil_prod_l_ha': 0,
|
|
'avg_oil_prod_l_ha': 0,
|
|
'l_per_t': 0,
|
|
'min_l_per_t': 0,
|
|
'max_l_per_t': 0,
|
|
'avg_l_per_t': 0,
|
|
'tech': ''
|
|
} for variety in all_varieties}
|
|
|
|
# Aggiorna i dati per le varietà selezionate
|
|
for variety_info, percentage in zip(varieties_info, percentages):
|
|
variety_name = clean_column_name(variety_info['Varietà di Olive'])
|
|
technique = clean_column_name(variety_info['Tecnica di Coltivazione'])
|
|
|
|
# Base production calculations
|
|
annual_prod = variety_info['Produzione (tonnellate/ettaro)'] * 1000 * percentage/100 * hectares
|
|
min_oil_prod = annual_prod * variety_info['Min Litri per Tonnellata'] / 1000
|
|
max_oil_prod = annual_prod * variety_info['Max Litri per Tonnellata'] / 1000
|
|
avg_oil_prod = annual_prod * variety_info['Media Litri per Tonnellata'] / 1000
|
|
|
|
# Water need calculation
|
|
base_water_need = (
|
|
variety_info['Fabbisogno Acqua Primavera (m³/ettaro)'] +
|
|
variety_info['Fabbisogno Acqua Estate (m³/ettaro)'] +
|
|
variety_info['Fabbisogno Acqua Autunno (m³/ettaro)'] +
|
|
variety_info['Fabbisogno Acqua Inverno (m³/ettaro)']
|
|
) / 4 * percentage/100 * hectares
|
|
|
|
variety_data[variety_name].update({
|
|
'pct': percentage/100,
|
|
'prod_t_ha': variety_info['Produzione (tonnellate/ettaro)'],
|
|
'oil_prod_t_ha': variety_info['Produzione Olio (tonnellate/ettaro)'],
|
|
'oil_prod_l_ha': variety_info['Produzione Olio (litri/ettaro)'],
|
|
'min_yield_pct': variety_info['Min % Resa'],
|
|
'max_yield_pct': variety_info['Max % Resa'],
|
|
'min_oil_prod_l_ha': variety_info['Min Produzione Olio (litri/ettaro)'],
|
|
'max_oil_prod_l_ha': variety_info['Max Produzione Olio (litri/ettaro)'],
|
|
'avg_oil_prod_l_ha': variety_info['Media Produzione Olio (litri/ettaro)'],
|
|
'l_per_t': variety_info['Litri per Tonnellata'],
|
|
'min_l_per_t': variety_info['Min Litri per Tonnellata'],
|
|
'max_l_per_t': variety_info['Max Litri per Tonnellata'],
|
|
'avg_l_per_t': variety_info['Media Litri per Tonnellata'],
|
|
'tech': technique
|
|
})
|
|
|
|
# Crea il vettore delle feature nell'ordine esatto
|
|
static_features = [hectares] # Inizia con gli ettari
|
|
|
|
# Lista delle feature per ogni varietà
|
|
variety_features = ['pct', 'prod_t_ha', 'oil_prod_t_ha', 'oil_prod_l_ha', 'min_yield_pct', 'max_yield_pct',
|
|
'min_oil_prod_l_ha', 'max_oil_prod_l_ha', 'avg_oil_prod_l_ha', 'l_per_t', 'min_l_per_t',
|
|
'max_l_per_t', 'avg_l_per_t']
|
|
|
|
# Appiattisci i dati delle varietà mantenendo l'ordine esatto
|
|
for variety in all_varieties:
|
|
# Feature esistenti
|
|
for feature in variety_features:
|
|
static_features.append(variety_data[variety][feature])
|
|
|
|
# Feature binarie per le tecniche di coltivazione
|
|
for technique in ['tradizionale', 'intensiva', 'superintensiva']:
|
|
static_features.append(1 if variety_data[variety]['tech'] == technique else 0)
|
|
|
|
print(f"lunghezza features {len(static_features)} ")
|
|
|
|
return np.array(static_features).reshape(1, -1)
|
|
|
|
def make_prediction(weather_data, varieties_info, percentages, hectares):
|
|
"""Effettua una predizione usando il modello."""
|
|
try:
|
|
print("Inizio della funzione make_prediction")
|
|
|
|
# Prepara i dati meteorologici mensili
|
|
monthly_stats = weather_data.groupby(['year', 'month']).agg({
|
|
'temp': 'mean',
|
|
'precip': 'sum',
|
|
'solarradiation': 'sum'
|
|
}).reset_index()
|
|
|
|
monthly_stats = monthly_stats.rename(columns={
|
|
'temp': 'temp_mean',
|
|
'precip': 'precip_sum',
|
|
'solarradiation': 'solar_energy_sum'
|
|
})
|
|
|
|
print(f"Shape dei dati meteorologici mensili: {monthly_stats.shape}")
|
|
|
|
# Definisci la dimensione della finestra temporale
|
|
window_size = 41
|
|
|
|
# Prendi gli ultimi window_size mesi di dati
|
|
if len(monthly_stats) >= window_size:
|
|
temporal_data = monthly_stats[['temp_mean', 'precip_sum', 'solar_energy_sum']].values[-window_size:]
|
|
else:
|
|
raise ValueError(f"Non ci sono abbastanza dati meteorologici. Necessari almeno {window_size} mesi.")
|
|
|
|
print(f"Shape dei dati temporali prima della trasformazione: {temporal_data.shape}")
|
|
|
|
temporal_data = scaler_temporal.transform(temporal_data)
|
|
print(f"Shape dei dati temporali dopo la trasformazione: {temporal_data.shape}")
|
|
|
|
temporal_data = np.expand_dims(temporal_data, axis=0)
|
|
print(f"Shape finale dei dati temporali: {temporal_data.shape}")
|
|
|
|
all_varieties = olive_varieties['Varietà di Olive'].unique()
|
|
varieties = [clean_column_name(variety) for variety in all_varieties]
|
|
|
|
# Prepara i dati statici
|
|
print("Preparazione dei dati statici")
|
|
static_data = prepare_static_features_multiple(varieties_info, percentages, hectares,varieties)
|
|
|
|
# Verifica che il numero di feature statiche sia corretto
|
|
if static_data.shape[1] != scaler_static.n_features_in_:
|
|
print("ATTENZIONE: Il numero di feature statiche non corrisponde a quello atteso dallo scaler!")
|
|
print(f"Feature generate: {static_data.shape[1]}, Feature attese: {scaler_static.n_features_in_}")
|
|
|
|
static_data = scaler_static.transform(static_data)
|
|
print(f"Shape dei dati statici dopo la trasformazione: {static_data.shape}")
|
|
|
|
# Effettua la predizione
|
|
print("Effettuazione della predizione")
|
|
prediction = model.predict({'temporal': temporal_data, 'static': static_data})
|
|
prediction = scaler_y.inverse_transform(prediction)[0]
|
|
|
|
# Calcola i dettagli per varietà
|
|
variety_details = []
|
|
for variety_info, percentage in zip(varieties_info, percentages):
|
|
# Calcoli specifici per varietà
|
|
prod_per_ha = variety_info['Produzione (tonnellate/ettaro)'] * 1000
|
|
oil_per_ha = variety_info['Produzione Olio (litri/ettaro)']
|
|
water_need = (
|
|
variety_info['Fabbisogno Acqua Primavera (m³/ettaro)'] +
|
|
variety_info['Fabbisogno Acqua Estate (m³/ettaro)'] +
|
|
variety_info['Fabbisogno Acqua Autunno (m³/ettaro)'] +
|
|
variety_info['Fabbisogno Acqua Inverno (m³/ettaro)']
|
|
) / 4
|
|
|
|
variety_details.append({
|
|
'variety': variety_info['Varietà di Olive'],
|
|
'percentage': percentage,
|
|
'production_per_ha': prod_per_ha,
|
|
'oil_per_ha': oil_per_ha,
|
|
'water_need': water_need
|
|
})
|
|
|
|
return {
|
|
'olive_production': prediction[0],
|
|
'min_oil_production': prediction[1],
|
|
'max_oil_production': prediction[2],
|
|
'avg_oil_production': prediction[3],
|
|
'water_need': prediction[4],
|
|
'variety_details': variety_details
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"Errore durante la preparazione dei dati o la predizione: {str(e)}")
|
|
print(f"Tipo di errore: {type(e).__name__}")
|
|
import traceback
|
|
print("Traceback completo:")
|
|
print(traceback.format_exc())
|
|
raise e
|
|
|
|
# Definizione del layout della dashboard
|
|
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
|
|
|
|
# Layout della dashboard
|
|
variety_options = [
|
|
{'label': v, 'value': v}
|
|
for v in sorted(olive_varieties['Varietà di Olive'].unique())
|
|
]
|
|
technique_options = [
|
|
{'label': t, 'value': t}
|
|
for t in sorted(olive_varieties['Tecnica di Coltivazione'].unique())
|
|
]
|
|
|
|
# Layout della dashboard aggiornato
|
|
app.layout = dbc.Container([
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.H1("Dashboard Produzione Olio d'Oliva", className="text-center mb-4")
|
|
])
|
|
]),
|
|
|
|
dbc.Row([
|
|
dbc.Col([
|
|
dbc.Card([
|
|
dbc.CardHeader("Composizione Oliveto"),
|
|
dbc.CardBody([
|
|
# Prima varietà (obbligatoria)
|
|
html.Div([
|
|
html.H6("Varietà 1"),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Label("Seleziona Varietà:"),
|
|
dcc.Dropdown(
|
|
id='variety-1-dropdown',
|
|
options=[{'label': v, 'value': v} for v in olive_varieties['Varietà di Olive'].unique()],
|
|
value=olive_varieties['Varietà di Olive'].iloc[0]
|
|
),
|
|
], width=6),
|
|
dbc.Col([
|
|
html.Label("Tecnica:"),
|
|
dcc.Dropdown(
|
|
id='technique-1-dropdown',
|
|
options=[
|
|
{'label': 'Tradizionale', 'value': 'Tradizionale'},
|
|
{'label': 'Intensiva', 'value': 'Intensiva'},
|
|
{'label': 'Superintensiva', 'value': 'Superintensiva'}
|
|
],
|
|
value='Tradizionale'
|
|
),
|
|
], width=6)
|
|
]),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Label("Percentuale (%):"),
|
|
dcc.Input(
|
|
id='percentage-1-input',
|
|
type='number',
|
|
min=1,
|
|
max=100,
|
|
value=100,
|
|
className="form-control"
|
|
)
|
|
], width=6)
|
|
], className="mt-2")
|
|
], className="mb-3"),
|
|
|
|
# Seconda varietà (opzionale)
|
|
html.Div([
|
|
html.H6("Varietà 2 (opzionale)"),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Label("Seleziona Varietà:"),
|
|
dcc.Dropdown(
|
|
id='variety-2-dropdown',
|
|
options=[{'label': v, 'value': v} for v in olive_varieties['Varietà di Olive'].unique()],
|
|
value=None
|
|
),
|
|
], width=6),
|
|
dbc.Col([
|
|
html.Label("Tecnica:"),
|
|
dcc.Dropdown(
|
|
id='technique-2-dropdown',
|
|
options=[
|
|
{'label': 'Tradizionale', 'value': 'Tradizionale'},
|
|
{'label': 'Intensiva', 'value': 'Intensiva'},
|
|
{'label': 'Superintensiva', 'value': 'Superintensiva'}
|
|
],
|
|
value=None,
|
|
disabled=True
|
|
),
|
|
], width=6)
|
|
]),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Label("Percentuale (%):"),
|
|
dcc.Input(
|
|
id='percentage-2-input',
|
|
type='number',
|
|
min=0,
|
|
max=99,
|
|
value=0,
|
|
disabled=True,
|
|
className="form-control"
|
|
)
|
|
], width=6)
|
|
], className="mt-2")
|
|
], className="mb-3"),
|
|
|
|
# Terza varietà (opzionale)
|
|
html.Div([
|
|
html.H6("Varietà 3 (opzionale)"),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Label("Seleziona Varietà:"),
|
|
dcc.Dropdown(
|
|
id='variety-3-dropdown',
|
|
options=[{'label': v, 'value': v} for v in olive_varieties['Varietà di Olive'].unique()],
|
|
value=None
|
|
),
|
|
], width=6),
|
|
dbc.Col([
|
|
html.Label("Tecnica:"),
|
|
dcc.Dropdown(
|
|
id='technique-3-dropdown',
|
|
options=[
|
|
{'label': 'Tradizionale', 'value': 'Tradizionale'},
|
|
{'label': 'Intensiva', 'value': 'Intensiva'},
|
|
{'label': 'Superintensiva', 'value': 'Superintensiva'}
|
|
],
|
|
value=None,
|
|
disabled=True
|
|
),
|
|
], width=6)
|
|
]),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Label("Percentuale (%):"),
|
|
dcc.Input(
|
|
id='percentage-3-input',
|
|
type='number',
|
|
min=0,
|
|
max=99,
|
|
value=0,
|
|
disabled=True,
|
|
className="form-control"
|
|
)
|
|
], width=6)
|
|
], className="mt-2")
|
|
], className="mb-3"),
|
|
|
|
html.Div(id='percentage-warning', className="text-danger"),
|
|
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Label("Ettari totali:"),
|
|
dcc.Input(
|
|
id='hectares-input',
|
|
type='number',
|
|
value=5,
|
|
min=1,
|
|
max=100,
|
|
className="form-control"
|
|
)
|
|
], width=6)
|
|
], className="mt-3")
|
|
])
|
|
], className="mb-4")
|
|
], width=4),
|
|
|
|
dbc.Col([
|
|
dbc.Card([
|
|
dbc.CardHeader("Previsioni di Produzione"),
|
|
dbc.CardBody([
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.H4("Produzione Olive", className="text-center"),
|
|
html.H2(id='olive-production', className="text-center text-primary")
|
|
], width=6),
|
|
dbc.Col([
|
|
html.H4("Produzione Olio", className="text-center"),
|
|
html.H2(id='oil-production', className="text-center text-success")
|
|
], width=6)
|
|
]),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.H4("Fabbisogno Idrico", className="text-center mt-4"),
|
|
html.H2(id='water-need', className="text-center text-info")
|
|
])
|
|
]),
|
|
dbc.Row([
|
|
dbc.Col([
|
|
html.Div(id='extra-info', className="text-center mt-4")
|
|
])
|
|
])
|
|
])
|
|
], className="mb-4")
|
|
], width=8)
|
|
]),
|
|
|
|
dbc.Row([
|
|
dbc.Col([
|
|
dbc.Card([
|
|
dbc.CardHeader("Dettagli Produzione"),
|
|
dbc.CardBody([
|
|
dcc.Graph(id='production-details')
|
|
])
|
|
])
|
|
], width=12, className="mb-4")
|
|
]),
|
|
|
|
dbc.Row([
|
|
dbc.Col([
|
|
dbc.Card([
|
|
dbc.CardHeader("Analisi Meteorologica"),
|
|
dbc.CardBody([
|
|
dcc.Graph(id='weather-impact')
|
|
])
|
|
])
|
|
], width=6),
|
|
dbc.Col([
|
|
dbc.Card([
|
|
dbc.CardHeader("Fabbisogno Idrico Mensile"),
|
|
dbc.CardBody([
|
|
dcc.Graph(id='water-needs')
|
|
])
|
|
])
|
|
], width=6)
|
|
])
|
|
], fluid=True)
|
|
|
|
# Callback per la gestione delle percentuali e abilitazione dei campi
|
|
@app.callback(
|
|
[Output('technique-2-dropdown', 'disabled'),
|
|
Output('percentage-2-input', 'disabled'),
|
|
Output('technique-3-dropdown', 'disabled'),
|
|
Output('percentage-3-input', 'disabled'),
|
|
Output('percentage-warning', 'children')],
|
|
[Input('variety-2-dropdown', 'value'),
|
|
Input('variety-3-dropdown', 'value'),
|
|
Input('percentage-1-input', 'value'),
|
|
Input('percentage-2-input', 'value'),
|
|
Input('percentage-3-input', 'value')]
|
|
)
|
|
def manage_percentages(variety2, variety3, perc1, perc2, perc3):
|
|
perc1 = perc1 or 0
|
|
perc2 = perc2 or 0
|
|
perc3 = perc3 or 0
|
|
total = perc1 + perc2 + perc3
|
|
|
|
# Abilita/disabilita campi basati sulle selezioni
|
|
disable_2 = variety2 is None
|
|
disable_3 = variety3 is None or variety2 is None
|
|
|
|
warning = ""
|
|
if total > 100:
|
|
warning = "La somma delle percentuali non può superare 100%"
|
|
elif total < 100:
|
|
warning = f"La somma delle percentuali è {total}% (dovrebbe essere 100%)"
|
|
|
|
return disable_2, disable_2, disable_3, disable_3, warning
|
|
|
|
# Aggiorna il callback principale per utilizzare multiple varietà
|
|
@app.callback(
|
|
[Output('olive-production', 'children'),
|
|
Output('oil-production', 'children'),
|
|
Output('water-need', 'children'),
|
|
Output('production-details', 'figure'),
|
|
Output('weather-impact', 'figure'),
|
|
Output('water-needs', 'figure'),
|
|
Output('extra-info', 'children')],
|
|
[Input('variety-1-dropdown', 'value'),
|
|
Input('technique-1-dropdown', 'value'),
|
|
Input('percentage-1-input', 'value'),
|
|
Input('variety-2-dropdown', 'value'),
|
|
Input('technique-2-dropdown', 'value'),
|
|
Input('percentage-2-input', 'value'),
|
|
Input('variety-3-dropdown', 'value'),
|
|
Input('technique-3-dropdown', 'value'),
|
|
Input('percentage-3-input', 'value'),
|
|
Input('hectares-input', 'value')]
|
|
)
|
|
def update_dashboard(variety1, tech1, perc1, variety2, tech2, perc2,
|
|
variety3, tech3, perc3, hectares):
|
|
# Verifica i dati di input
|
|
if not variety1 or not tech1 or perc1 is None or hectares is None:
|
|
return "N/A", "N/A", "N/A", {}, {}, {}, ""
|
|
|
|
# Raccogli le informazioni delle varietà
|
|
varieties_info = []
|
|
percentages = []
|
|
|
|
# Prima varietà
|
|
variety_data = olive_varieties[
|
|
(olive_varieties['Varietà di Olive'] == variety1) &
|
|
(olive_varieties['Tecnica di Coltivazione'] == tech1)
|
|
]
|
|
if not variety_data.empty:
|
|
varieties_info.append(variety_data.iloc[0])
|
|
percentages.append(perc1)
|
|
|
|
# Seconda varietà
|
|
if variety2 and tech2 and perc2:
|
|
variety_data = olive_varieties[
|
|
(olive_varieties['Varietà di Olive'] == variety2) &
|
|
(olive_varieties['Tecnica di Coltivazione'] == tech2)
|
|
]
|
|
if not variety_data.empty:
|
|
varieties_info.append(variety_data.iloc[0])
|
|
percentages.append(perc2)
|
|
|
|
# Terza varietà
|
|
if variety3 and tech3 and perc3:
|
|
variety_data = olive_varieties[
|
|
(olive_varieties['Varietà di Olive'] == variety3) &
|
|
(olive_varieties['Tecnica di Coltivazione'] == tech3)
|
|
]
|
|
if not variety_data.empty:
|
|
varieties_info.append(variety_data.iloc[0])
|
|
percentages.append(perc3)
|
|
|
|
try:
|
|
# Prepara i dati e fai la predizione
|
|
prediction = make_prediction(weather_data, varieties_info, percentages, hectares)
|
|
|
|
# Formatta output
|
|
olive_prod_text = f"{prediction['olive_production']:.0f} kg/ha"
|
|
oil_prod_text = f"{prediction['avg_oil_production']:.0f} L/ha"
|
|
water_need_text = f"{prediction['water_need']:.0f} m³/ha"
|
|
|
|
# Crea il grafico dei dettagli di produzione
|
|
details_data = []
|
|
|
|
# Aggiungi dati per ogni varietà
|
|
for detail in prediction['variety_details']:
|
|
details_data.extend([
|
|
{
|
|
'Varietà': f"{detail['variety']} ({detail['percentage']}%)",
|
|
'Tipo': 'Olive',
|
|
'Produzione': detail['production_per_ha'] * (detail['percentage']/100)
|
|
},
|
|
{
|
|
'Varietà': f"{detail['variety']} ({detail['percentage']}%)",
|
|
'Tipo': 'Olio',
|
|
'Produzione': detail['oil_per_ha'] * (detail['percentage']/100)
|
|
}
|
|
])
|
|
|
|
# Aggiungi totali
|
|
details_data.extend([
|
|
{
|
|
'Varietà': 'Totale',
|
|
'Tipo': 'Olive',
|
|
'Produzione': prediction['olive_production']
|
|
},
|
|
{
|
|
'Varietà': 'Totale',
|
|
'Tipo': 'Olio',
|
|
'Produzione': prediction['avg_oil_production']
|
|
}
|
|
])
|
|
|
|
# Crea il grafico dei dettagli
|
|
details_df = pd.DataFrame(details_data)
|
|
details_fig = px.bar(
|
|
details_df,
|
|
x='Varietà',
|
|
y='Produzione',
|
|
color='Tipo',
|
|
barmode='group',
|
|
title='Dettagli Produzione per Varietà',
|
|
labels={'Produzione': 'kg/ha o L/ha'},
|
|
color_discrete_map={'Olive': '#1f77b4', 'Olio': '#2ca02c'}
|
|
)
|
|
details_fig.update_layout(
|
|
legend_title_text='Prodotto',
|
|
xaxis_tickangle=-45
|
|
)
|
|
|
|
# Grafico impatto meteo
|
|
recent_weather = weather_data.tail(41).copy()
|
|
weather_impact = px.scatter(
|
|
recent_weather,
|
|
x='temp',
|
|
y='solarradiation',
|
|
size='precip',
|
|
title='Condizioni Meteorologiche',
|
|
labels={
|
|
'temp': 'Temperatura (°C)',
|
|
'solarradiation': 'Radiazione Solare (W/m²)',
|
|
'precip': 'Precipitazioni (mm)'
|
|
}
|
|
)
|
|
weather_impact.update_layout(
|
|
legend_title_text='Precipitazioni',
|
|
showlegend=True
|
|
)
|
|
|
|
# Grafico fabbisogno idrico
|
|
water_data = []
|
|
months = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu',
|
|
'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic']
|
|
|
|
# Calcola il fabbisogno idrico mensile per ogni varietà
|
|
for detail in prediction['variety_details']:
|
|
variety_info = olive_varieties[
|
|
olive_varieties['Varietà di Olive'] == detail['variety']
|
|
].iloc[0]
|
|
|
|
seasonal_water = {
|
|
'Inverno': variety_info['Fabbisogno Acqua Inverno (m³/ettaro)'],
|
|
'Primavera': variety_info['Fabbisogno Acqua Primavera (m³/ettaro)'],
|
|
'Estate': variety_info['Fabbisogno Acqua Estate (m³/ettaro)'],
|
|
'Autunno': variety_info['Fabbisogno Acqua Autunno (m³/ettaro)']
|
|
}
|
|
|
|
for month in months:
|
|
season = get_season_from_month(month)
|
|
water_data.append({
|
|
'Mese': month,
|
|
'Varietà': detail['variety'],
|
|
'Fabbisogno': seasonal_water[season] * (detail['percentage']/100)
|
|
})
|
|
|
|
# Crea il grafico del fabbisogno idrico
|
|
water_df = pd.DataFrame(water_data)
|
|
water_needs = px.bar(
|
|
water_df,
|
|
x='Mese',
|
|
y='Fabbisogno',
|
|
color='Varietà',
|
|
title='Fabbisogno Idrico Mensile per Varietà',
|
|
labels={'Fabbisogno': 'm³/ettaro'},
|
|
barmode='stack'
|
|
)
|
|
water_needs.update_layout(
|
|
legend_title_text='Varietà',
|
|
xaxis_tickangle=0
|
|
)
|
|
|
|
extra_info = html.Div([
|
|
html.H5("Dettagli per Varietà", className="mb-3"),
|
|
html.Div([
|
|
# Crea una card per ogni varietà
|
|
dbc.Row([
|
|
dbc.Col([
|
|
dbc.Card([
|
|
dbc.CardHeader(
|
|
f"{detail['variety']} - {detail['percentage']}%",
|
|
className="font-weight-bold"
|
|
),
|
|
dbc.CardBody([
|
|
# Trova i dettagli completi della varietà dal dataset originale
|
|
html.Div([
|
|
# Produzione
|
|
html.Div([
|
|
html.H6("Produzione Prevista:", className="mb-2"),
|
|
html.P([
|
|
html.Span("Olive: ", className="font-weight-bold"),
|
|
f"{detail['production_per_ha'] * (detail['percentage']/100):.0f} kg/ha"
|
|
]),
|
|
html.P([
|
|
html.Span("Olio: ", className="font-weight-bold"),
|
|
f"{detail['oil_per_ha'] * (detail['percentage']/100):.0f} L/ha"
|
|
]),
|
|
], className="mb-3"),
|
|
|
|
# Rese
|
|
html.Div([
|
|
html.H6("Rese:", className="mb-2"),
|
|
html.P([
|
|
html.Span("Resa in Olio: ", className="font-weight-bold"),
|
|
f"{variety_info['Min % Resa']:.1f}% - {variety_info['Max % Resa']:.1f}%"
|
|
]),
|
|
html.P([
|
|
html.Span("Litri per Tonnellata: ", className="font-weight-bold"),
|
|
f"{variety_info['Min Litri per Tonnellata']:.0f} - {variety_info['Max Litri per Tonnellata']:.0f} L/t"
|
|
])
|
|
], className="mb-3"),
|
|
|
|
# Caratteristiche
|
|
html.Div([
|
|
html.H6("Caratteristiche:", className="mb-2"),
|
|
html.P([
|
|
html.Span("Temperatura Ottimale: ", className="font-weight-bold"),
|
|
f"{variety_info['Temperatura Ottimale']}°C"
|
|
]),
|
|
html.P([
|
|
html.Span("Resistenza alla Siccità: ", className="font-weight-bold"),
|
|
f"{variety_info['Resistenza']}"
|
|
])
|
|
], className="mb-3"),
|
|
|
|
# Fabbisogno Idrico
|
|
html.Div([
|
|
html.H6("Fabbisogno Idrico Stagionale:", className="mb-2"),
|
|
html.P([
|
|
html.Span("Primavera: ", className="font-weight-bold"),
|
|
f"{variety_info['Fabbisogno Acqua Primavera (m³/ettaro)']:.0f} m³/ha"
|
|
]),
|
|
html.P([
|
|
html.Span("Estate: ", className="font-weight-bold"),
|
|
f"{variety_info['Fabbisogno Acqua Estate (m³/ettaro)']:.0f} m³/ha"
|
|
]),
|
|
html.P([
|
|
html.Span("Autunno: ", className="font-weight-bold"),
|
|
f"{variety_info['Fabbisogno Acqua Autunno (m³/ettaro)']:.0f} m³/ha"
|
|
]),
|
|
html.P([
|
|
html.Span("Inverno: ", className="font-weight-bold"),
|
|
f"{variety_info['Fabbisogno Acqua Inverno (m³/ettaro)']:.0f} m³/ha"
|
|
])
|
|
])
|
|
])
|
|
])
|
|
], className="h-100")
|
|
], width=12 if len(prediction['variety_details']) == 1 else
|
|
6 if len(prediction['variety_details']) == 2 else 4,
|
|
className="mb-3")
|
|
for detail in prediction['variety_details']
|
|
for variety_info in [olive_varieties[
|
|
olive_varieties['Varietà di Olive'] == detail['variety']
|
|
].iloc[0]]
|
|
], className="mb-4"),
|
|
|
|
# Sezione totali
|
|
dbc.Card([
|
|
dbc.CardHeader("Totali Previsti", className="font-weight-bold"),
|
|
dbc.CardBody([
|
|
html.Div([
|
|
html.P([
|
|
html.Span("Produzione Totale Olive: ", className="font-weight-bold"),
|
|
f"{prediction['olive_production']:.0f} kg/ha"
|
|
]),
|
|
html.P([
|
|
html.Span("Produzione Totale Olio: ", className="font-weight-bold"),
|
|
f"{prediction['avg_oil_production']:.0f} L/ha"
|
|
]),
|
|
html.P([
|
|
html.Span("Resa Media in Olio: ", className="font-weight-bold"),
|
|
f"{(prediction['avg_oil_production']/prediction['olive_production']*100):.1f}%"
|
|
]),
|
|
html.P([
|
|
html.Span("Fabbisogno Idrico Totale: ", className="font-weight-bold"),
|
|
f"{prediction['water_need']:.0f} m³/ha"
|
|
])
|
|
])
|
|
])
|
|
])
|
|
])
|
|
], className="mt-4")
|
|
|
|
return olive_prod_text, oil_prod_text, water_need_text, details_fig, weather_impact, water_needs, extra_info
|
|
|
|
except Exception as e:
|
|
print(f"Errore durante la predizione: {str(e)}")
|
|
return "Errore", "Errore", "Errore", {}, {}, {}, f"Errore: {str(e)}"
|
|
|
|
|
|
def get_season_from_month(month):
|
|
"""Helper function per determinare la stagione dal mese."""
|
|
seasons = {
|
|
'Gen': 'Inverno', 'Feb': 'Inverno', 'Mar': 'Primavera',
|
|
'Apr': 'Primavera', 'Mag': 'Primavera', 'Giu': 'Estate',
|
|
'Lug': 'Estate', 'Ago': 'Estate', 'Set': 'Autunno',
|
|
'Ott': 'Autunno', 'Nov': 'Autunno', 'Dic': 'Inverno'
|
|
}
|
|
return seasons[month]
|
|
|
|
if __name__ == '__main__':
|
|
app.run_server(debug=True) |