boq-api / utils /inference.py
gabcares's picture
Upload 80 files
72fdabd verified
Raw
History Blame Contribute Delete
4.44 kB
import pandas as pd
from .engines import QSRuleBasedFallbackEngine
from .model import QSSelectiveCalibratedModel
def predict_with_cascade(
df_raw: pd.DataFrame,
ml_model: QSSelectiveCalibratedModel,
fallback_engine: QSRuleBasedFallbackEngine,
) -> pd.DataFrame:
"""
Executes the ultimate MLOps Cascade Architecture.
Workflow:
1. Run the highly-precise ML Model.
2. Identify rejected items (routed to 'Others').
3. Pass ONLY the rejected items to the deterministic Rule Engine.
4. Merge the results, tag the provenance/source, and re-infer NRM hierarchies.
Parameters
----------
df_raw : pd.DataFrame
The raw incoming BoQ data.
ml_model : QSSelectiveCalibratedModel
The loaded, hermetic ML production object.
fallback_engine : QSRuleBasedFallbackEngine
The initialized rule-based regex matcher.
Returns
-------
pd.DataFrame
The final DataFrame with completely resolved predictions and provenance tagging.
"""
# -------------------------------------------------------------------------
# 1. Primary ML Inference
# -------------------------------------------------------------------------
df_results = ml_model.predict_full(df_raw)
# -------------------------------------------------------------------------
# 2. Identify the ML Rejects
# -------------------------------------------------------------------------
others_mask = df_results["Predicted_Category"] == ml_model.others_label
# Initialize the provenance column (Auditability requirement)
df_results["Prediction_Source"] = "ML_Model"
# If the ML model handled everything perfectly, return early
if not others_mask.any():
return df_results
ml_model._logger.info(f"Fallback triggered for {others_mask.sum()} items.")
# -------------------------------------------------------------------------
# 3. Execute Fallback Rules on Rejects
# -------------------------------------------------------------------------
# The production object normalizes descriptions to the 'description' column
rejected_descriptions = df_results.loc[others_mask, "description"]
rule_predictions = fallback_engine.predict(rejected_descriptions)
# -------------------------------------------------------------------------
# 4. Merge Results & Update Provenance
# -------------------------------------------------------------------------
# Overwrite 'Others' with the Rule Engine's decisions
df_results.loc[others_mask, "Predicted_Category"] = rule_predictions
df_results.loc[others_mask, "Prediction_Source"] = "Rule_Engine"
# Deterministic rules have 1.0 confidence. Failed items get 0.0 confidence.
df_results.loc[
others_mask & (rule_predictions != "Unclassified"), "Prediction_Confidence"
] = 1.0
df_results.loc[
others_mask & (rule_predictions == "Unclassified"), "Prediction_Confidence"
] = 0.0
# -------------------------------------------------------------------------
# 5. Re-infer NRM Hierarchy for Rescued Items
# -------------------------------------------------------------------------
# Find the rows that were 'Others' but successfully classified by rules
rescued_mask = others_mask & (df_results["Predicted_Category"] != "Unclassified")
if rescued_mask.any():
# Map the new categories back to the NRM hierarchy using the ML model's dictionary
rescued_hierarchies = df_results.loc[rescued_mask, "Predicted_Category"].apply(
lambda cat: ml_model.infer_nrm_hierarchy(cat)
)
rescued_df = pd.DataFrame(
list(rescued_hierarchies), index=rescued_hierarchies.index
)
# Dynamically overwrite the output columns (Handling both 'Predicted_ge' and 'ge' formats)
for level in ["ge", "group_element", "e", "element"]:
pred_col = f"Predicted_{level}"
# If the model created a 'Predicted_' column, update that
if pred_col in df_results.columns:
df_results.loc[rescued_mask, pred_col] = rescued_df[level]
# Otherwise, update the base column
elif level in df_results.columns:
df_results.loc[rescued_mask, level] = rescued_df[level]
return df_results