SIENTIAPDE-1316

SIENTIAPDE-1084: Refactor apply_aggregation method in Gates class for improved performance and clarity. Changed parameter name from 'group' to 'values', optimized aggregation logic, and enhanced error handling for invalid functions. Streamlined data processing by reducing unnecessary operations and added batch debug logging.
This commit is contained in:
vitor-aignosi
2025-10-16 13:06:44 -03:00
parent 205284d9f4
commit 8bdbf049b8

View File

@@ -44,7 +44,7 @@ class Gates(BaseActivity):
BaseActivity.__init__( BaseActivity.__init__(
self, logger, notification_handler, set_error_counter=True) self, logger, notification_handler, set_error_counter=True)
def apply_aggregation(self, group: DataFrame, aggr_function: str, def apply_aggregation(self, values: DataFrame, aggr_function: str,
metadata: dict[str, Any]) -> float | None | str: metadata: dict[str, Any]) -> float | None | str:
""" """
Apply aggregation function to a group of time-series data. Apply aggregation function to a group of time-series data.
@@ -54,7 +54,7 @@ class Gates(BaseActivity):
reporting for invalid aggregation functions. reporting for invalid aggregation functions.
Args: Args:
group (DataFrame): Group of data points to aggregate values (DataFrame): Group of data points to aggregate (pre-sorted by timestamp)
aggr_function (str): Aggregation function to apply. aggr_function (str): Aggregation function to apply.
Supported functions: 'lts' (latest), 'avg' (average), 'mdn' (median), Supported functions: 'lts' (latest), 'avg' (average), 'mdn' (median),
'max' (maximum), 'min' (minimum) 'max' (maximum), 'min' (minimum)
@@ -66,36 +66,39 @@ class Gates(BaseActivity):
Raises: Raises:
NotificationError: If invalid aggregation function is specified NotificationError: If invalid aggregation function is specified
""" """
if len(group) == 1: # Fast path for single value
return group['value'].item() if len(values) == 1:
return values['value'].iloc[0]
# Apply aggregation function to value
if aggr_function == 'lts': if aggr_function == 'lts':
return group['value'].iloc[-1] return values['value'].iloc[-1]
# Remove NaN values without inplace operation
clean_values = values['value'].dropna()
if clean_values.empty:
return None
# Use dictionary lookup for aggregation functions (faster than if-elif chain)
aggregation_map = {
'avg': lambda x: x.mean(),
'mdn': lambda x: x.median(),
'max': lambda x: x.max(),
'min': lambda x: x.min()
}
if aggr_function in aggregation_map:
return aggregation_map[aggr_function](clean_values)
else: else:
group.dropna(inplace=True, subset=['value']) self.send_notification(
metadata=metadata,
if group.empty: notification_id="AGGREGATION_ISSUES",
return None message=f"Invalid aggregation function: {aggr_function}",
block="aggregate_data",
if aggr_function == 'avg': level=NotificationLevel.ERROR,
return group['value'].mean() attachment_content=traceback.format_exc()
elif aggr_function == 'mdn': )
return group['value'].median() return 'continue'
elif aggr_function == 'max':
return group['value'].max()
elif aggr_function == 'min':
return group['value'].min()
else:
self.send_notification(
metadata=metadata,
notification_id="AGGREGATION_ISSUES",
message=f"Invalid aggregation function: {aggr_function}",
block="aggregate_data",
level=NotificationLevel.ERROR,
attachment_content=traceback.format_exc()
)
return 'continue'
@activity.defn(name="aggregate_data") @activity.defn(name="aggregate_data")
async def aggregate_data(self, input_data: dict[str, Any]) -> dict[str, Any]: async def aggregate_data(self, input_data: dict[str, Any]) -> dict[str, Any]:
@@ -130,21 +133,25 @@ class Gates(BaseActivity):
metadata=metadata metadata=metadata
) )
# Initialize result dictionary # Sort once by timestamp for all data (more efficient than sorting each group)
result = {} df = df.sort_values(['tag', 'name', 'timestamp'])
# Group by tag and name # Group by tag and name
grouped = df.groupby(['tag', 'name']) # sort=False since we already sorted
grouped = df.groupby(['tag', 'name'], sort=False)
# Prepare aggregation functions mapping
model_tags = input_data['model_tags']
# Process groups efficiently
results = []
for (tag, name), group in grouped: for (tag, name), group in grouped:
# Get the aggregation function from model_tags # Get the aggregation function from model_tags
aggr_function = input_data['model_tags'].get( aggr_function = model_tags.get(
name, {}).get('aggr_func', 'lts') name, {}).get('aggr_func', 'lts')
group.sort_values(by='timestamp', inplace=True) # Get the latest timestamp (last row since data is sorted)
latest_timestamp = group['timestamp'].iloc[-1]
# Get the latest timestamp
latest_timestamp = group['timestamp'].max()
aggr_value = self.apply_aggregation( aggr_value = self.apply_aggregation(
group, aggr_function, metadata) group, aggr_function, metadata)
@@ -152,48 +159,42 @@ class Gates(BaseActivity):
if aggr_value == 'continue': if aggr_value == 'continue':
continue continue
self.debug( # Batch debug logging to reduce overhead
f"Aggregated data: {aggr_value}", if self.logger.level <= 10: # DEBUG level
metadata=metadata self.debug(
) f"Processed {tag}_{name}: value={aggr_value}, "
self.debug( f"timestamp={latest_timestamp}, func={aggr_function}",
f"Latest timestamp: {latest_timestamp}", metadata=metadata
metadata=metadata )
)
self.debug(
f"Groups: {group.to_string()}",
metadata=metadata
)
self.debug(
f"group name: {name}",
metadata=metadata
)
self.debug(
f"group tag: {tag}",
metadata=metadata
)
# Store the result # Store the result directly in list for better performance
result[f"{tag}_{name}"] = { results.append({
'tag': tag, 'tag': tag,
'name': name, 'name': name,
'value': aggr_value, 'value': aggr_value,
'timestamp': latest_timestamp, 'timestamp': latest_timestamp,
'aggregation_function': aggr_function 'aggregation_function': aggr_function
} })
result_df = DataFrame(list(result.values()))
self.info( self.info(
f"Aggregated data has {len(result_df)} rows", f"Aggregated data has {len(results)} rows",
metadata=metadata metadata=metadata
) )
self.debug( # Convert to DataFrame only once at the end if we have results
f"Aggregated data: {result_df.to_string()}", if results:
metadata=metadata result_df = DataFrame(results)
)
return result_df.to_dict() if self.logger.level <= 10: # DEBUG level
self.debug(
f"Final aggregated data:\n{result_df.to_string()}",
metadata=metadata
)
return result_df.to_dict()
else:
# Return empty DataFrame dict structure
return DataFrame().to_dict()
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()