From 498244052b60b1448a24f98a45331c83524eea57 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Wed, 11 Feb 2026 14:49:39 +0700 Subject: [PATCH] feat: Implement reliability-based CM cost prediction using historical cost per failure and document the new prediction logic. --- src/modules/equipment/Prediksi.py | 66 +++++++++++++++++++++++++++---- src/modules/equipment/formula.py | 8 ++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index 564325c..3af234e 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -674,6 +674,39 @@ class Prediksi: print(f"HTTP error occurred: {e}") return {} + def __get_historical_cost_per_failure(self, assetnum): + connection = None + try: + connection = get_production_connection() + if connection is None: + return 0.0 + cursor = connection.cursor() + # Optimized single-pass query: counts and sums in one scan + query = """ + SELECT + SUM(a.actmatcost) / NULLIF(COUNT(CASE WHEN a.wonum NOT LIKE 'T%%' THEN 1 END), 0) as cost_failure + FROM wo_maximo a + WHERE (a.asset_unit = '3' OR a.asset_unit = '00') + AND a.status IN ('COMP', 'CLOSE') + AND a.asset_assetnum = %s + AND a.worktype IN ('CM', 'PROACTIVE', 'EM') + AND a.wojp8 != 'S1' + AND ( + a.description NOT ILIKE '%%U4%%' + OR (a.description ILIKE '%%U3%%' AND a.description ILIKE '%%U4%%') + ) + """ + cursor.execute(query, (assetnum,)) + result = cursor.fetchone() + cost_failure = float(result[0]) if result and result[0] is not None else 0.0 + return cost_failure + except Exception as e: + print(f"Error fetching historical cost per failure for {assetnum}: {e}") + return 0.0 + finally: + if connection: + connection.close() + def __get_man_hour_rate(self, staff_level: str = "Junior"): connection = None try: @@ -755,7 +788,8 @@ class Prediksi: rate, max_year = self.__get_rate_and_max_year(assetnum) man_hour_rate = self.__get_man_hour_rate() # Defaults to 'junior' - pmt = 0 + # Pre-fetch cost per failure once per asset to avoid redundant DB queries + avg_cost_per_failure = self.__get_historical_cost_per_failure(assetnum) # Prediksi untuk setiap kolom for column in df.columns: @@ -807,16 +841,32 @@ class Prediksi: preds_list.append(cost) preds = np.array(preds_list, dtype=float) - elif recent_vals.empty: - avg = 0.0 - preds = np.repeat(float(avg), n_future) else: - avg = pd.to_numeric(recent_vals, errors="coerce").fillna(0).mean() - avg = 0.0 if pd.isna(avg) else float(avg) - preds = np.repeat(float(avg), n_future) + # Use pre-fetched cost per failure + preds_list = [] + for yr in future_years: + failures_data = await self._fetch_api_data(assetnum, yr) + # Interval from predicted number of failures + interval = 0.0 + if isinstance(failures_data, dict): + data_list = failures_data.get("data") + if isinstance(data_list, list) and len(data_list) > 0: + first_item = data_list[0] + if isinstance(first_item, dict): + num_fail = first_item.get("num_fail") + if num_fail is not None: + try: + interval = float(num_fail) + except Exception: + interval = 0.0 + + # predicted_cost = predicted_failures * avg_cost_per_failure + cost = interval * avg_cost_per_failure + preds_list.append(cost) + preds = np.array(preds_list, dtype=float) else: - # Для kolom non-cm, gunakan nilai dari last actual year bila ada, + # kolom non-cm, gunakan nilai dari last actual year bila ada, # jika tidak ada gunakan last available non-NA value, jika tidak ada pakai 0.0 if "is_actual" in df.columns and not df[df["is_actual"] == 1].empty: last_actual_year_series = df[df["is_actual"] == 1]["year"] diff --git a/src/modules/equipment/formula.py b/src/modules/equipment/formula.py index 849d964..99b41b1 100644 --- a/src/modules/equipment/formula.py +++ b/src/modules/equipment/formula.py @@ -6,6 +6,14 @@ This file consolidates the core mathematical/financial formulas used across: - `insert_actual_data.py` (aggregation formulas, man-hour conversion) - `Prediksi.py` (future value / fv wrappers) +### Prediction Logic Summary +| Category | Logic Type | Formula Basis | +| :--- | :--- | :--- | +| **CM Labor** | **Reliability-Based** | `Failures x 3.0 x 1.0 x ManPowerRate` | +| **CM Other** | **Reliability-Based** | `Failures x CostPerFailure (from Production SQL)` | +| **PM / OH / PDM** | **Last Scenario** | `Value from Last Actual Year` (Carry Forward) | +| **Total Risk Cost** | **Aggregated** | `Sum of above + Asset Criticality Multiplier` | + Keep these functions pure and well-documented to make debugging and comparisons easier. """