-
Notifications
You must be signed in to change notification settings - Fork 45
Use tools-API in qualx predict #1838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -13,12 +13,15 @@ | |||||||||||||||
# limitations under the License. | ||||||||||||||||
|
||||||||||||||||
"""module that defines the app descriptor for the results loaded by the tools.""" | ||||||||||||||||
|
||||||||||||||||
import re | ||||||||||||||||
from dataclasses import dataclass, field | ||||||||||||||||
from functools import cached_property | ||||||||||||||||
from typing import Optional | ||||||||||||||||
from typing import Optional, List, Dict | ||||||||||||||||
|
||||||||||||||||
import pandas as pd | ||||||||||||||||
from pydantic.alias_generators import to_camel | ||||||||||||||||
|
||||||||||||||||
from spark_rapids_tools.utils import Utilities | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
@dataclass | ||||||||||||||||
|
@@ -32,6 +35,56 @@ class AppHandler(object): | |||||||||||||||
# this will be loaded from the core-status csv report | ||||||||||||||||
eventlog_path: Optional[str] = None | ||||||||||||||||
|
||||||||||||||||
@staticmethod | ||||||||||||||||
def get_pd_dtypes() -> Dict[str, str]: | ||||||||||||||||
""" | ||||||||||||||||
Get the pandas data types for the AppHandler attributes. | ||||||||||||||||
:return: Dictionary mapping attribute names to pandas data types. | ||||||||||||||||
""" | ||||||||||||||||
return { | ||||||||||||||||
'app_id': Utilities.scala_to_pandas_type('String'), | ||||||||||||||||
'attempt_id': Utilities.scala_to_pandas_type('Int'), | ||||||||||||||||
'app_name': Utilities.scala_to_pandas_type('String'), | ||||||||||||||||
'eventlog_path': Utilities.scala_to_pandas_type('String') | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
@staticmethod | ||||||||||||||||
def normalize_attribute(arg_value: str) -> str: | ||||||||||||||||
""" | ||||||||||||||||
Normalize the attribute name to a plain format. | ||||||||||||||||
It uses re.sub to replace any '-' or '_' with a space using the regexp 'r"(_|-)+"'. | ||||||||||||||||
Finally, it uses str.replace() to remove any spaces. | ||||||||||||||||
:param arg_value: the attribute name to normalize. | ||||||||||||||||
:return: the actual field name that is used in the AppHandler. | ||||||||||||||||
""" | ||||||||||||||||
processed_value = re.sub(r'([_\-])+', ' ', arg_value.strip().lower()).replace(' ', '') | ||||||||||||||||
lookup_map = { | ||||||||||||||||
'appname': 'app_name', | ||||||||||||||||
'appid': 'app_id', | ||||||||||||||||
'attemptid': 'attempt_id', | ||||||||||||||||
'eventlogpath': 'eventlog_path' | ||||||||||||||||
} | ||||||||||||||||
return lookup_map.get(processed_value, arg_value) | ||||||||||||||||
|
||||||||||||||||
@classmethod | ||||||||||||||||
def get_key_attributes(cls) -> List[str]: | ||||||||||||||||
""" | ||||||||||||||||
Get the key attributes that define an AppHandler. | ||||||||||||||||
:return: List of key attributes. | ||||||||||||||||
""" | ||||||||||||||||
return ['app_id'] | ||||||||||||||||
|
||||||||||||||||
@classmethod | ||||||||||||||||
def get_default_key_columns(cls) -> Dict[str, str]: | ||||||||||||||||
""" | ||||||||||||||||
Get the default key columns for the AppHandler. | ||||||||||||||||
:return: Dictionary mapping attribute names to column names. | ||||||||||||||||
""" | ||||||||||||||||
res = {} | ||||||||||||||||
for attr in cls.get_key_attributes(): | ||||||||||||||||
res[attr] = to_camel(attr) | ||||||||||||||||
return res | ||||||||||||||||
|
||||||||||||||||
def is_name_defined(self) -> bool: | ||||||||||||||||
""" | ||||||||||||||||
Check if the app name is defined. | ||||||||||||||||
|
@@ -57,17 +110,37 @@ def uuid(self) -> str: | |||||||||||||||
""" | ||||||||||||||||
return self._app_id | ||||||||||||||||
|
||||||||||||||||
def patch_into_df(self, df: pd.DataFrame) -> pd.DataFrame: | ||||||||||||||||
def patch_into_df(self, | ||||||||||||||||
df: pd.DataFrame, | ||||||||||||||||
col_names: Optional[List[str]] = None) -> pd.DataFrame: | ||||||||||||||||
""" | ||||||||||||||||
Given a dataframe, this method will stitch the app_id and app-name to the dataframe. | ||||||||||||||||
This can be useful in automatically adding the app-id/app-name to the data-frame | ||||||||||||||||
:param df: the dataframe that we want to modify. | ||||||||||||||||
:param col_names: optional list of column names that defines the app_id and app_name to the | ||||||||||||||||
dataframe. It is assumed that the list comes in the order it is inserted in | ||||||||||||||||
the column names. | ||||||||||||||||
:return: the resulting dataframe from adding the columns. | ||||||||||||||||
""" | ||||||||||||||||
# TODO: We should consider add UUID as well, and use that for the joins instead. | ||||||||||||||||
# append attempt_id to support multiple attempts | ||||||||||||||||
col_values = [self.app_id] | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels weird that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point!
|
||||||||||||||||
if col_names is None: | ||||||||||||||||
# append attemptId to support multi-attempts | ||||||||||||||||
col_names = ['appId'] | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The zip operation will only iterate over min(len(col_names), len(col_values)) items. Since col_values always has length 1 but col_names can have different lengths, this could skip columns or fail silently.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||
# Ensure col_values matches col_names in length | ||||||||||||||||
if len(col_values) == 1 and len(col_names) > 1: | ||||||||||||||||
col_values = col_values * len(col_names) | ||||||||||||||||
elif len(col_values) != len(col_names): | ||||||||||||||||
raise ValueError('Length of col_values must be 1 or match length of col_names') | ||||||||||||||||
if not df.empty: | ||||||||||||||||
# TODO: We should consider add UUID as well, and use that for the joins instead. | ||||||||||||||||
df.insert(0, 'attemptId', self._attempt_id) | ||||||||||||||||
df.insert(0, 'appId', self._app_id) | ||||||||||||||||
for col_k, col_v in zip(reversed(col_names), reversed(col_values)): | ||||||||||||||||
if col_k not in df.columns: | ||||||||||||||||
df.insert(0, col_k, col_v) | ||||||||||||||||
else: | ||||||||||||||||
# if the column already exists, we should not overwrite it | ||||||||||||||||
# this is useful when we want to patch the app_id/app_name to an existing dataframe | ||||||||||||||||
df[col_k] = col_v | ||||||||||||||||
return df | ||||||||||||||||
|
||||||||||||||||
@property | ||||||||||||||||
|
@@ -121,3 +194,72 @@ def merge(self, other: 'AppHandler') -> 'AppHandler': | |||||||||||||||
if self.eventlog_path is None and other.eventlog_path is not None: | ||||||||||||||||
self.eventlog_path = other.eventlog_path | ||||||||||||||||
return self | ||||||||||||||||
|
||||||||||||||||
################################ | ||||||||||||||||
# Public Methods | ||||||||||||||||
################################ | ||||||||||||||||
|
||||||||||||||||
def convert_to_df(self) -> pd.DataFrame: | ||||||||||||||||
""" | ||||||||||||||||
Convert the AppHandler attributes to a DataFrame. | ||||||||||||||||
:return: DataFrame with app_id, app_name, and attempt_id as columns. | ||||||||||||||||
""" | ||||||||||||||||
data = { | ||||||||||||||||
'app_id': [self.app_id], | ||||||||||||||||
'attempt_id': [self.attempt_id], | ||||||||||||||||
'app_name': [self.app_name], | ||||||||||||||||
'eventlog_path': [self.eventlog_path] | ||||||||||||||||
} | ||||||||||||||||
data_types = AppHandler.get_pd_dtypes() | ||||||||||||||||
return pd.DataFrame({ | ||||||||||||||||
col: pd.Series(data[col], dtype=dtype) for col, dtype in data_types.items() | ||||||||||||||||
}) | ||||||||||||||||
|
||||||||||||||||
def add_fields_to_dataframe(self, | ||||||||||||||||
df: pd.DataFrame, | ||||||||||||||||
field_to_col_map: Dict[str, str]) -> pd.DataFrame: | ||||||||||||||||
""" | ||||||||||||||||
Insert fields/properties from AppHandler into the DataFrame, with user-specified column names. | ||||||||||||||||
:param df: Existing DataFrame to append to. | ||||||||||||||||
:type df: pd.DataFrame | ||||||||||||||||
:param field_to_col_map: Dictionary mapping AppHandler attributes (keys) to DataFrame column names (values). | ||||||||||||||||
:type field_to_col_map: Dict[str, str] | ||||||||||||||||
default: Value to use if attribute/property not found (raises if None). | ||||||||||||||||
""" | ||||||||||||||||
converted_df = self.convert_to_df() | ||||||||||||||||
row_data = [] | ||||||||||||||||
for attr, col in field_to_col_map.items(): | ||||||||||||||||
# Normalize the attribute name | ||||||||||||||||
norm_attr = AppHandler.normalize_attribute(attr) | ||||||||||||||||
try: | ||||||||||||||||
value = getattr(self, norm_attr) | ||||||||||||||||
row_data.append((col, norm_attr, value)) | ||||||||||||||||
except AttributeError as exc: | ||||||||||||||||
raise AttributeError(f"Attribute '{attr}' not found in AppHandler.") from exc | ||||||||||||||||
for col, norm_attr, value in reversed(row_data): | ||||||||||||||||
# Check if the column already exists in the DataFrame | ||||||||||||||||
if col in df.columns: | ||||||||||||||||
# If it exists, we should not overwrite it, skip | ||||||||||||||||
continue | ||||||||||||||||
# create a new column with the correct type. We do this because we do not want to | ||||||||||||||||
# to add values to an empty dataframe. | ||||||||||||||||
df.insert(loc=0, column=col, value=pd.Series(dtype=converted_df[norm_attr].dtype)) | ||||||||||||||||
# set the values in case the dataframe was non-empty. | ||||||||||||||||
df[col] = pd.Series([value] * len(df), dtype=converted_df[norm_attr].dtype) | ||||||||||||||||
return df | ||||||||||||||||
|
||||||||||||||||
@classmethod | ||||||||||||||||
def inject_into_df(cls, | ||||||||||||||||
df: pd.DataFrame, | ||||||||||||||||
field_to_col_map: Dict[str, str], | ||||||||||||||||
app_h: Optional['AppHandler'] = None) -> pd.DataFrame: | ||||||||||||||||
""" | ||||||||||||||||
Inject AppHandler fields into a DataFrame using a mapping of field names to column names. | ||||||||||||||||
:param df: | ||||||||||||||||
:param field_to_col_map: | ||||||||||||||||
:param app_h: | ||||||||||||||||
:return: | ||||||||||||||||
""" | ||||||||||||||||
if app_h is None: | ||||||||||||||||
app_h = AppHandler(_app_id='UNKNOWN_APP', _app_name='UNKNOWN_APP', _attempt_id=1) | ||||||||||||||||
return app_h.add_fields_to_dataframe(df, field_to_col_map) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to eventually push the branching down into the core qualx APIs, just so all invocations to
predict()
can use the switch, but this is fine (and easier) for now.