SIENTIAPDE-1231

Enhance MLFlow and MLFlowRepository with improved data handling and logging

- Refactored MLFlow class to sort data by 'created_at' and drop duplicates for better input preparation.
- Updated MLFlowRepository methods to include detailed logging for artifact downloads and model predictions.
- Introduced LzmaPayloadCodec for efficient payload compression in the worker, optimizing data handling for large payloads.
- Enhanced timestamp handling in treated data to ensure compatibility with model expectations.
This commit is contained in:
vitor-aignosi
2025-10-06 13:32:36 -03:00
parent 9b71ad7556
commit 36af84f056
3 changed files with 123 additions and 29 deletions

View File

@@ -232,14 +232,23 @@ class MLFlow(BaseActivity):
timestamp = data['timestamp'].max() timestamp = data['timestamp'].max()
self.debug(f'Timestamp: {timestamp}', metadata) self.debug(f'Timestamp: {timestamp}', metadata)
# Sort by created_at in descending order and keep first occurrence of each variable/timestamp pair
data = data.sort_values('created_at', ascending=False).drop_duplicates(
subset=['variable', 'timestamp'], keep='first'
)
data.drop(columns=['model_id'], inplace=True, errors='ignore') data.drop(columns=['model_id'], inplace=True, errors='ignore')
data.drop(columns=['created_at'], inplace=True, errors='ignore') data.drop(columns=['created_at'], inplace=True, errors='ignore')
data = data.pivot(index='timestamp', columns='variable', # Pivot data for model input format
values='value') data = data.pivot(
data.sort_index(inplace=True) index='timestamp', columns='variable',
data.reset_index(inplace=True) values='value')
data.fillna(np.nan, inplace=True)
# data.reset_index(inplace=True)
data.columns.name = None
data['timestamp'] = data.index
data['timestamp'] = to_datetime( data['timestamp'] = to_datetime(
data['timestamp'], format=DATETIME_FORMAT_WITH_TZ).dt.strftime(DATETIME_FORMAT) data['timestamp'], format=DATETIME_FORMAT_WITH_TZ).dt.strftime(DATETIME_FORMAT)
data['timestamp'] = to_datetime( data['timestamp'] = to_datetime(

View File

@@ -235,6 +235,9 @@ class MLFlowRepository():
if not path.exists(output_dir): if not path.exists(output_dir):
makedirs(output_dir) makedirs(output_dir)
self.logger.info(
f"Downloading artifacts from {run_id} to {output_dir}")
return self.client.download_artifacts( return self.client.download_artifacts(
run_id, run_id,
artifact_path, artifact_path,
@@ -347,18 +350,13 @@ class MLFlowRepository():
Raises: Raises:
ValueError: If model cannot be loaded with any compression method ValueError: If model cannot be loaded with any compression method
""" """
code_path = path.join(
artifact_path, "code")
if type == "transformer": pickle_file = "training_transformer.pkl" if type == "transformer" else "stacking_model.pkl"
code_path = path.join(
artifact_path, "transformer_pyfunc", "code")
pickle_path = path.join(
artifact_path, "transformer_pyfunc", "artifacts", "training_transformer.pkl")
elif type == "prediction": pickle_path = path.join(
code_path = path.join( artifact_path, "artifacts", pickle_file)
artifact_path, "stacking_model", "code")
pickle_path = path.join(
artifact_path, "stacking_model", "artifacts", "stacking_model.pkl")
if code_path not in sys_path: if code_path not in sys_path:
sys_path.insert(0, code_path) sys_path.insert(0, code_path)
@@ -468,6 +466,9 @@ class MLFlowRepository():
f"{message}") f"{message}")
elif index_type == datetime or index_type == pd.Timestamp: elif index_type == datetime or index_type == pd.Timestamp:
if data.index.tz is None:
data.index = data.index.tz_localize('UTC')
data.index = data.index.strftime(DATETIME_FORMAT_WITH_TZ) data.index = data.index.strftime(DATETIME_FORMAT_WITH_TZ)
else: else:
raise ValueError( raise ValueError(
@@ -690,6 +691,9 @@ class MLFlowRepository():
self.logger.custom_debug( self.logger.custom_debug(
f"Fitting transformation model with training data (shape: {data.shape})", metadata) f"Fitting transformation model with training data (shape: {data.shape})", metadata)
data.to_csv(
f"tmp/retrain_data_{model_name}.csv", index=True)
data_model = data_model.fit(data) data_model = data_model.fit(data)
self.logger.custom_debug( self.logger.custom_debug(
@@ -697,6 +701,20 @@ class MLFlowRepository():
treated_data = data_model.predict(data) treated_data = data_model.predict(data)
# Stores current index as timestamp, Courier model expects a timestamp column
# with specific format
treated_data['timestamp'] = treated_data.index
# Parses timestamp column to datetime format to align with data
treated_data = self.detect_and_parse_datetime_index(
treated_data, metadata)
self.logger.custom_debug(
f"Treated data index: {treated_data.index}", metadata)
treated_data.to_csv(
f"tmp/retrain_treated_data_{model_name}.csv", index=True)
self.logger.custom_debug( self.logger.custom_debug(
f"Transformed data shape: {treated_data.shape}", metadata) f"Transformed data shape: {treated_data.shape}", metadata)
@@ -708,12 +726,24 @@ class MLFlowRepository():
self.logger.custom_debug( self.logger.custom_debug(
f"Using provided target variable: {target_name}", metadata) f"Using provided target variable: {target_name}", metadata)
# Check if treated_data contains target variable
if target_name not in treated_data.columns:
self.logger.custom_debug(
f"Target variable {target_name} not found in treated data, aligning data with treated data indexes", metadata)
# Aligns data with treated data indexes to get target variable
aligned_data = data.loc[treated_data.index]
else:
# Uses target variable from treated data
self.logger.custom_debug(
f"Target variable {target_name} found in treated data, using it", metadata)
aligned_data = treated_data
if fit_config.get('y_type', 'series').lower() == 'series': if fit_config.get('y_type', 'series').lower() == 'series':
y = data[target_name] y = aligned_data[target_name]
self.logger.custom_debug( self.logger.custom_debug(
f"Extracting target as series, shape: {y.shape}", metadata) f"Extracting target as series, shape: {y.shape}", metadata)
else: else:
y = data[[target_name]] y = aligned_data[[target_name]]
self.logger.custom_debug( self.logger.custom_debug(
f"Extracting target as dataframe, shape: {y.shape}", metadata) f"Extracting target as dataframe, shape: {y.shape}", metadata)
@@ -726,22 +756,25 @@ class MLFlowRepository():
self.logger.custom_debug( self.logger.custom_debug(
f"Combined data shape for prediction model fit: {treated_data.shape}", metadata) f"Combined data shape for prediction model fit: {treated_data.shape}", metadata)
treated_data.to_csv(
f"tmp/retrain_treated_data_combined_{model_name}.csv", index=True)
prediction_model = prediction_model.fit(treated_data) prediction_model = prediction_model.fit(treated_data)
self.logger.custom_debug( self.logger.custom_debug(
"Prediction model fitted with combined data", metadata) "Prediction model fitted with combined data", metadata)
else: else:
# Keep data separated # Keep data separated
fit_order = fit_config.get('split_fit_first', 'x').lower() fit_order = fit_config.get('split_fit_first', 'x').lower()
y.to_csv(
f"tmp/retrain_y_{model_name}.csv", index=True)
self.logger.custom_debug( self.logger.custom_debug(
f"Fitting prediction model with separated data, order: {fit_order}", metadata) f"Fitting prediction model with separated data, first arg: {fit_order}", metadata)
if fit_order == 'x': if fit_order == 'x':
prediction_model = prediction_model.fit(treated_data, y) prediction_model = prediction_model.fit(treated_data, y)
self.logger.custom_debug(
"Prediction model fitted with X, y order", metadata)
else: else:
prediction_model = prediction_model.fit(y, treated_data) prediction_model = prediction_model.fit(y, treated_data)
self.logger.custom_debug(
"Prediction model fitted with y, X order", metadata)
experiment = self.get_experiment_by_run_id(latest_production_id) experiment = self.get_experiment_by_run_id(latest_production_id)
mlflow.set_experiment(experiment) mlflow.set_experiment(experiment)
@@ -1012,30 +1045,32 @@ class MLFlowRepository():
data.to_csv( data.to_csv(
f"tmp/treated_data_{model_name}.csv", index=True) f"tmp/treated_data_{model_name}.csv", index=True)
data = self.get_cached_predict( predict_data = self.get_cached_predict(
model_name, data, model_retention, flavor) model_name, data, model_retention, flavor)
end_time = datetime.now() end_time = datetime.now()
if isinstance(data, pd.DataFrame): if isinstance(predict_data, pd.DataFrame):
self.logger.custom_debug( self.logger.custom_debug(
f"Data received from model prediction: {data.head(5).to_csv()}", metadata) f"Data received from model prediction: {data.head(5).to_csv()}", metadata)
data.to_csv( predict_data.to_csv(
f"tmp/predicted_data_{model_name}.csv", index=True) f"tmp/predicted_data_{model_name}.csv", index=True)
data.columns = ['prediction'] data.columns = ['prediction']
else: else:
data = pd.DataFrame(data, columns=['prediction']) predict_data = pd.DataFrame(
data.to_csv( predict_data, columns=['prediction'])
predict_data.to_csv(
f"tmp/predicted_data_{model_name}.csv", index=True) f"tmp/predicted_data_{model_name}.csv", index=True)
data.index = input_index predict_data.index = input_index
data['response_time'] = (end_time - start_time).total_seconds() predict_data['response_time'] = (
end_time - start_time).total_seconds()
return { return {
'success': True, 'success': True,
'content': data.to_dict() 'content': predict_data.to_dict()
} }
except Exception as e: except Exception as e:

View File

@@ -28,6 +28,8 @@ Environment Variables:
from temporalio import workflow, client from temporalio import workflow, client
from temporalio.worker import Worker, PollerBehaviorAutoscaling from temporalio.worker import Worker, PollerBehaviorAutoscaling
from temporalio.runtime import Runtime, TelemetryConfig, PrometheusConfig from temporalio.runtime import Runtime, TelemetryConfig, PrometheusConfig
from temporalio.converter import PayloadCodec, DataConverter
from temporalio.api.common.v1 import Payload
with workflow.unsafe.imports_passed_through(): with workflow.unsafe.imports_passed_through():
import os import os
@@ -49,9 +51,54 @@ with workflow.unsafe.imports_passed_through():
from sientia_do.observability.logger import get_logger from sientia_do.observability.logger import get_logger
from laborious import metrics from laborious import metrics
from prometheus_client import start_http_server from prometheus_client import start_http_server
import lzma
import dataclasses
POD_ID = os.getenv('POD_ID') POD_ID = os.getenv('POD_ID')
SDK_METRICS_PORT = int(os.getenv('HTTP_SDK_METRICS_PORT', "9091")) SDK_METRICS_PORT = int(os.getenv('HTTP_SDK_METRICS_PORT', "9091"))
LZMA_MIN_MB = float(os.getenv('LZMA_MIN_MB', "1.5"))
class LzmaPayloadCodec(PayloadCodec):
async def encode(self, payloads):
out = []
for p in payloads:
if p.data:
old_len = len(p.data) / 1000000
# Only compress payloads larger than 1.5 MB
if old_len > LZMA_MIN_MB:
compressed_data = lzma.compress(p.data)
new_len = len(compressed_data) / 1000000
ratio = new_len / old_len if old_len else 0
print(
f"[codec] encode lzma: {old_len} MB -> {new_len} MB ({ratio:.2f}x)")
meta = dict(p.metadata or {})
meta[b"codec"] = b"lzma"
out.append(Payload(metadata=meta, data=compressed_data))
else:
out.append(p)
else:
out.append(p)
return out
async def decode(self, payloads):
out = []
for p in payloads:
if p.data and (p.metadata or {}).get(b"codec") == b"lzma":
# comp_len = len(p.data)
decomp = lzma.decompress(p.data)
# decomp_len = len(decomp)
# ratio = (decomp_len / comp_len) if comp_len else 0
# print(
# f"[codec] decode lzma: {comp_len} B -> {decomp_len} B ({ratio:.2f}x)")
meta = dict(p.metadata or {})
meta.pop(b"codec", None)
out.append(Payload(metadata=meta, data=decomp))
else:
out.append(p)
return out
async def main(): async def main():
@@ -125,9 +172,12 @@ async def main():
logger.custom_info(f'Starting Temporal Client at {host}...', metadata) logger.custom_info(f'Starting Temporal Client at {host}...', metadata)
codec_dc = DataConverter(payload_codec=LzmaPayloadCodec())
temporal_client = await client.Client.connect( temporal_client = await client.Client.connect(
target_host=host, target_host=host,
namespace=os.getenv('TEMPORAL_NAMESPACE', 'laborious'), namespace=os.getenv('TEMPORAL_NAMESPACE', 'laborious'),
data_converter=codec_dc,
runtime=new_runtime runtime=new_runtime
) )