Download analysis.py from DAM20/Etmam: direct link, hf CLI and curl.
- Browser
- Download file 8.01 kB
-
https://huggingface.co/spaces/DAM20/Etmam/resolve/main/analysis.py
- Command line
-
hf download hf://spaces/DAM20/Etmam/analysis.py
-
curl -L -o analysis.py https://huggingface.co/spaces/DAM20/Etmam/resolve/main/analysis.py
8.01 kB
| # -*- coding: utf-8 -*- | |
| from __future__ import annotations | |
| import re | |
| import importlib.util | |
| import tempfile | |
| from io import BytesIO | |
| import numpy as np | |
| import pandas as pd | |
| COL_ID = "رقم البلاغ" | |
| COL_ADMIN = "الإدارة" | |
| COL_STATUS = "حالة البلاغ في النظام" | |
| COL_SOURCE = "مصدر البلاغ" | |
| COL_CREATED = "تاريخ الانشاء" | |
| COL_CLOSED = "تاريخ الاغلاق" | |
| COL_ELAPSED = "الوقت المنقضي" | |
| COL_HOURS = "الساعات" | |
| COL_SLA = "التوصيف" | |
| STATUS_CANON = [ | |
| "انتظار الاستجابة - مقاول", | |
| "انتظار الاستجابة - مراقب", | |
| "انتظار الاستجابة - مشرف", | |
| "قيد التنفيذ - مراقب", | |
| "قيد التنفيذ - مقاول", | |
| "معلق - اعادة فتح", | |
| ] | |
| SLA_ORDER = ["لم تتجاوز", "قارب على تجاوز SLA", "تجاوز SLA"] | |
| ALLOWED_SOURCES = {"Urbi", "تطبيق بلدي", "توكلنا", "مراكز الاتصال"} | |
| def _norm_ar(s: str) -> str: | |
| if not isinstance(s, str): | |
| return "" | |
| s = s.strip() | |
| s = ( | |
| s.replace("أ", "ا") | |
| .replace("إ", "ا") | |
| .replace("آ", "ا") | |
| .replace("ى", "ي") | |
| .replace("ة", "ه") | |
| ) | |
| s = re.sub(r"\s+", " ", s) | |
| s = s.replace(" -", "-").replace("- ", "-") | |
| return s | |
| def canon_status(raw: str) -> str: | |
| s = _norm_ar(raw) | |
| if "انتظار" in s and "استجابه" in s: | |
| if "مقاول" in s: | |
| return "انتظار الاستجابة - مقاول" | |
| if "مراقب" in s: | |
| return "انتظار الاستجابة - مراقب" | |
| if "مشرف" in s: | |
| return "انتظار الاستجابة - مشرف" | |
| if ("قيد" in s and "التنفيذ" in s) or ("التنفيذ" in s and ("قيد" in s or "جاري" in s)): | |
| if "مراقب" in s: | |
| return "قيد التنفيذ - مراقب" | |
| if "مقاول" in s: | |
| return "قيد التنفيذ - مقاول" | |
| if "معلق" in s and ("اعاده فتح" in s or "اعادة فتح" in s or "فتح" in s): | |
| return "معلق - اعادة فتح" | |
| return raw | |
| def canon_source(x: str) -> str: | |
| s = _norm_ar(x) | |
| if "urbi" in s.lower(): | |
| return "Urbi" | |
| if "بلدي" in s: | |
| return "تطبيق بلدي" | |
| if "توكلنا" in s: | |
| return "توكلنا" | |
| if ("مراكز" in s and "اتصال" in s) or ("مركز" in s and "اتصال" in s): | |
| return "مراكز الاتصال" | |
| return x | |
| def read_excel_safe(uploaded): | |
| data = uploaded.read() | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp: | |
| tmp.write(data) | |
| path = tmp.name | |
| engines = [] | |
| if importlib.util.find_spec("calamine") or importlib.util.find_spec("python_calamine"): | |
| engines.append("calamine") | |
| engines += [None, "openpyxl"] | |
| last = None | |
| for eng in engines: | |
| try: | |
| return pd.read_excel(path, sheet_name=0, engine=eng) | |
| except Exception as e: | |
| last = e | |
| continue | |
| raise RuntimeError(f"تعذر قراءة الملف: {last}") | |
| def parse_created_col(s): | |
| a = pd.to_datetime(s, errors="coerce", dayfirst=False) | |
| b = pd.to_datetime(s, errors="coerce", dayfirst=True) | |
| if b.notna().sum() > a.notna().sum(): | |
| return b | |
| return a | |
| def preprocess(df: pd.DataFrame, closed_ts: pd.Timestamp): | |
| idx_created = df.columns.get_loc(COL_CREATED) | |
| drop_after = [ | |
| df.columns[i] | |
| for i in range(idx_created + 1, min(idx_created + 4, len(df.columns))) | |
| ] | |
| df = df.drop(columns=drop_after, errors="ignore").copy() | |
| sidx = df.columns.get_loc(COL_SOURCE) | |
| right_cols = list(df.columns[sidx + 1:]) | |
| if right_cols: | |
| df.drop(columns=right_cols, inplace=True) | |
| NEW_CLASS_COL = "التصنيف الجديد" | |
| BAD_CLASS = "السيارات التالفة" | |
| rows_before = len(df) | |
| if NEW_CLASS_COL in df.columns: | |
| df = df[ | |
| df[NEW_CLASS_COL] | |
| .astype(str) | |
| .map(_norm_ar) | |
| != _norm_ar(BAD_CLASS) | |
| ].copy() | |
| rows_after = len(df) | |
| deleted = rows_before - rows_after | |
| df["_deleted_bad_class"] = deleted | |
| df["_rows_before_filter"] = rows_before | |
| df["_rows_after_filter"] = rows_after | |
| if COL_CLOSED not in df.columns: | |
| df.insert(idx_created + 1, COL_CLOSED, pd.NaT) | |
| if COL_ELAPSED not in df.columns: | |
| df.insert(idx_created + 2, COL_ELAPSED, pd.NaT) | |
| if COL_HOURS not in df.columns: | |
| df.insert(idx_created + 3, COL_HOURS, pd.NA) | |
| df[COL_CREATED] = parse_created_col(df[COL_CREATED]) | |
| df[COL_CLOSED] = closed_ts | |
| df[COL_ELAPSED] = df[COL_CLOSED] - df[COL_CREATED] | |
| total_seconds = df[COL_ELAPSED].dt.total_seconds() | |
| hours_floor = np.floor_divide(total_seconds.fillna(0).astype("int64"), 3600) | |
| df[COL_HOURS] = pd.Series(hours_floor, index=df.index).astype("Int64") | |
| df["_status_canon"] = df[COL_STATUS].astype(str).map(canon_status) | |
| df = df[df["_status_canon"].isin(STATUS_CANON)].copy() | |
| df["_status_canon"] = pd.Categorical( | |
| df["_status_canon"], categories=STATUS_CANON, ordered=True | |
| ) | |
| df[COL_SOURCE] = df[COL_SOURCE].astype(str).map(canon_source) | |
| df = df[df[COL_SOURCE].isin(ALLOWED_SOURCES)].copy() | |
| h = df[COL_HOURS].astype("float") | |
| conds = [h < 72, (h >= 72) & (h <= 95)] | |
| choices = ["لم تتجاوز", "قارب على تجاوز SLA"] | |
| df[COL_SLA] = np.select(conds, choices, default="تجاوز SLA") | |
| df[COL_SLA] = pd.Categorical(df[COL_SLA], categories=SLA_ORDER, ordered=True) | |
| return df | |
| def pivots(df_proc: pd.DataFrame): | |
| p_open = ( | |
| df_proc.groupby([COL_ADMIN, "_status_canon"])[COL_ID] | |
| .count() | |
| .unstack("_status_canon", fill_value=0) | |
| .reindex(columns=STATUS_CANON, fill_value=0) | |
| ) | |
| p_open.loc["الإجمالي الكلي"] = p_open.sum(axis=0) | |
| p_open.columns.name = None | |
| p_sla = ( | |
| df_proc.groupby([COL_ADMIN, COL_SLA])[COL_ID] | |
| .count() | |
| .unstack(COL_SLA, fill_value=0) | |
| .reindex(columns=SLA_ORDER, fill_value=0) | |
| ) | |
| p_sla.loc["الإجمالي الكلي"] = p_sla.sum(axis=0) | |
| p_sla.columns.name = None | |
| return p_open, p_sla | |
| def build(xls: pd.DataFrame, closed_ts: pd.Timestamp) -> BytesIO: | |
| df_all = preprocess(xls.copy(), closed_ts) | |
| df_ar = df_all[df_all[COL_SOURCE] != "Urbi"].copy() | |
| p_open_ar, p_sla_ar = pivots(df_ar) | |
| df_urbi = df_all[df_all[COL_SOURCE] == "Urbi"].copy() | |
| if df_urbi.empty: | |
| p_urbi = pd.DataFrame({"ملاحظة": ["لا توجد بلاغات Urbi"]}) | |
| urbi_index = False | |
| else: | |
| p_urbi = ( | |
| df_urbi.groupby([COL_ADMIN, COL_SOURCE])[COL_ID] | |
| .count() | |
| .unstack(COL_SOURCE, fill_value=0) | |
| ) | |
| p_urbi.loc["الإجمالي الكلي"] = p_urbi.sum(axis=0) | |
| p_urbi.columns.name = None | |
| urbi_index = True | |
| out = BytesIO() | |
| with pd.ExcelWriter(out, engine="openpyxl") as w: | |
| p_open_ar.to_excel(w, sheet_name="١- المفتوحة والمعاد فتحها") | |
| p_sla_ar.to_excel(w, sheet_name="٢- التوصيف") | |
| p_urbi.to_excel(w, sheet_name="٣- مصادر أخرى", index=urbi_index) | |
| out.seek(0) | |
| return out | |
| def get_pivots_for_ppt(xls: pd.DataFrame, closed_ts: pd.Timestamp): | |
| df_all = preprocess(xls.copy(), closed_ts) | |
| df_ar = df_all[df_all[COL_SOURCE] != "Urbi"].copy() | |
| p_open_ar, p_sla_ar = pivots(df_ar) | |
| df_urbi = df_all[df_all[COL_SOURCE] == "Urbi"].copy() | |
| if df_urbi.empty: | |
| p_urbi = pd.DataFrame() | |
| else: | |
| p_urbi = ( | |
| df_urbi.groupby([COL_ADMIN, COL_SOURCE])[COL_ID] | |
| .count() | |
| .unstack(COL_SOURCE, fill_value=0) | |
| ) | |
| p_urbi.loc["الإجمالي الكلي"] = p_urbi.sum(axis=0) | |
| p_urbi.columns.name = None | |
| return p_open_ar, p_sla_ar, p_urbi | |