Add complaint sites filtering to overview tables with dedicated tab, separate complaint-specific tables for multi-RAT summary and top anomalies, and include complaint data in Excel export with two additional sheets
Browse files
panel_app/kpi_health_check_panel.py
CHANGED
|
@@ -417,7 +417,6 @@ except Exception: # noqa: BLE001
|
|
| 417 |
rules_table.configuration = cfg
|
| 418 |
except Exception: # noqa: BLE001
|
| 419 |
pass
|
| 420 |
-
|
| 421 |
site_summary_table = pn.widgets.Tabulator(
|
| 422 |
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 423 |
)
|
|
@@ -426,10 +425,18 @@ multirat_summary_table = pn.widgets.Tabulator(
|
|
| 426 |
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 427 |
)
|
| 428 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
top_anomalies_table = pn.widgets.Tabulator(
|
| 430 |
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 431 |
)
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
site_select = pn.widgets.AutocompleteInput(
|
| 434 |
name="Select a site (Type to search)",
|
| 435 |
options={},
|
|
@@ -446,7 +453,6 @@ kpi_compare_norm = pn.widgets.Select(
|
|
| 446 |
name="Normalization", options=["None", "Min-Max", "Z-score"], value="None"
|
| 447 |
)
|
| 448 |
|
| 449 |
-
# NEW WIDGETS
|
| 450 |
kpi_group_select = pn.widgets.Select(
|
| 451 |
name="KPI Group", options=["All (selected KPIs)"], value="All (selected KPIs)"
|
| 452 |
)
|
|
@@ -535,6 +541,8 @@ except Exception: # noqa: BLE001
|
|
| 535 |
_set_tabulator_pagination(site_summary_table, page_size=50)
|
| 536 |
_set_tabulator_pagination(multirat_summary_table, page_size=50)
|
| 537 |
_set_tabulator_pagination(top_anomalies_table, page_size=50)
|
|
|
|
|
|
|
| 538 |
_set_tabulator_pagination(site_kpi_table, page_size=50)
|
| 539 |
trend_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
|
| 540 |
heatmap_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
|
|
@@ -780,7 +788,6 @@ def _update_kpi_options() -> None:
|
|
| 780 |
]
|
| 781 |
kpis = sorted([str(c) for c in kpis])
|
| 782 |
|
| 783 |
-
# Apply Grouping if needed
|
| 784 |
groups = get_kpis_by_group(kpis)
|
| 785 |
group_options = ["All (selected KPIs)"] + sorted(
|
| 786 |
[g for g in groups.keys() if g != "Other"]
|
|
@@ -794,7 +801,6 @@ def _update_kpi_options() -> None:
|
|
| 794 |
if kpi_group_select.value not in group_options:
|
| 795 |
kpi_group_select.value = group_options[0]
|
| 796 |
|
| 797 |
-
# Filter KPIs based on group
|
| 798 |
filtered_kpis = filter_kpis(
|
| 799 |
kpis, kpi_group_select.value, mode=kpi_group_mode.value
|
| 800 |
)
|
|
@@ -852,20 +858,14 @@ def _update_site_view(event=None) -> None:
|
|
| 852 |
except Exception: # noqa: BLE001
|
| 853 |
available_sites = set()
|
| 854 |
|
| 855 |
-
# Robustly resolve code_int from site_select.value
|
| 856 |
-
# AutocompleteInput might return the Label (str) or Value (int) depending on usage
|
| 857 |
code_int = None
|
| 858 |
if code is not None:
|
| 859 |
-
# 1. Try if code is already a known value (int)
|
| 860 |
if hasattr(site_select, "options") and isinstance(site_select.options, dict):
|
| 861 |
-
# Check if it matches a Key (Label)
|
| 862 |
if code in site_select.options:
|
| 863 |
code_int = site_select.options[code]
|
| 864 |
-
# Check if it is a Value in the dict
|
| 865 |
elif code in site_select.options.values():
|
| 866 |
code_int = code
|
| 867 |
|
| 868 |
-
# 2. If not checking opts or not found, try direct cast
|
| 869 |
if code_int is None:
|
| 870 |
try:
|
| 871 |
code_int = int(code)
|
|
@@ -932,36 +932,38 @@ def _update_site_view(event=None) -> None:
|
|
| 932 |
trend_plot_pane.object, heatmap_plot_pane.object, hist_plot_pane.object = cached
|
| 933 |
return
|
| 934 |
|
| 935 |
-
# Determine KPIs to plot based on group mode
|
| 936 |
kpis_to_plot = []
|
| 937 |
|
| 938 |
-
# 1. Start with explicitly selected 'Compare KPIs'
|
| 939 |
selected = [str(x) for x in (kpi_compare_select.value or []) if str(x)]
|
| 940 |
|
| 941 |
-
# 2. Add the primary selected KPI if not present
|
| 942 |
if kpi and str(kpi) not in selected:
|
| 943 |
selected = [str(kpi)] + selected
|
| 944 |
|
| 945 |
-
# 3. Handle Group Mode "Add top 12 KPIs"
|
| 946 |
-
# If mode is "Add top...", we fetch from group and append
|
| 947 |
if "Add top" in str(kpi_group_mode.value):
|
| 948 |
from_group = filter_kpis(
|
| 949 |
d.columns.tolist(), kpi_group_select.value, mode="Top-N", top_n=12
|
| 950 |
)
|
| 951 |
-
# Merge unique
|
| 952 |
for gk in from_group:
|
| 953 |
if gk not in selected:
|
| 954 |
selected.append(gk)
|
| 955 |
|
| 956 |
-
# Safeguard: Limit to 15 max to prevent browser crash
|
| 957 |
kpis_to_plot = selected[:15]
|
| 958 |
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
|
| 966 |
fig = build_drilldown_plot(
|
| 967 |
df=d[d["site_code"] == int(code_int)],
|
|
@@ -973,12 +975,6 @@ def _update_site_view(event=None) -> None:
|
|
| 973 |
rat=rat,
|
| 974 |
)
|
| 975 |
trend_plot_pane.object = fig
|
| 976 |
-
|
| 977 |
-
rules_df = (
|
| 978 |
-
rules_table.value
|
| 979 |
-
if isinstance(rules_table.value, pd.DataFrame)
|
| 980 |
-
else pd.DataFrame()
|
| 981 |
-
)
|
| 982 |
kpis_for_heatmap = []
|
| 983 |
if (
|
| 984 |
isinstance(site_df, pd.DataFrame)
|
|
@@ -1488,6 +1484,52 @@ def _refresh_filtered_results(event=None) -> None:
|
|
| 1488 |
current_top_anomalies_df = pd.DataFrame()
|
| 1489 |
top_anomalies_table.value = current_top_anomalies_df
|
| 1490 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1491 |
current_export_bytes = None
|
| 1492 |
|
| 1493 |
|
|
@@ -1833,6 +1875,8 @@ def load_datasets(event=None) -> None:
|
|
| 1833 |
site_summary_table.value = pd.DataFrame()
|
| 1834 |
multirat_summary_table.value = pd.DataFrame()
|
| 1835 |
top_anomalies_table.value = pd.DataFrame()
|
|
|
|
|
|
|
| 1836 |
site_kpi_table.value = pd.DataFrame()
|
| 1837 |
trend_plot_pane.object = None
|
| 1838 |
heatmap_plot_pane.object = None
|
|
@@ -1924,8 +1968,20 @@ def load_datasets(event=None) -> None:
|
|
| 1924 |
_loading_datasets = False
|
| 1925 |
try:
|
| 1926 |
_update_site_options()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1927 |
_update_kpi_options()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1928 |
_update_site_view()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1929 |
_refresh_validation_state()
|
| 1930 |
except Exception: # noqa: BLE001
|
| 1931 |
pass
|
|
@@ -2088,6 +2144,16 @@ def _build_export_bytes() -> bytes:
|
|
| 2088 |
if isinstance(current_top_anomalies_df, pd.DataFrame)
|
| 2089 |
else None
|
| 2090 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2091 |
)
|
| 2092 |
|
| 2093 |
|
|
@@ -2176,6 +2242,13 @@ try:
|
|
| 2176 |
except Exception: # noqa: BLE001
|
| 2177 |
pass
|
| 2178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2179 |
try:
|
| 2180 |
multirat_summary_table.on_click(
|
| 2181 |
lambda e: _handle_double_click("multirat", multirat_summary_table, e)
|
|
@@ -2183,6 +2256,13 @@ try:
|
|
| 2183 |
except Exception: # noqa: BLE001
|
| 2184 |
pass
|
| 2185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2186 |
min_criticality.param.watch(_refresh_filtered_results, "value")
|
| 2187 |
min_anomaly_score.param.watch(_refresh_filtered_results, "value")
|
| 2188 |
city_filter.param.watch(_refresh_filtered_results, "value")
|
|
@@ -2228,7 +2308,7 @@ def _build_drilldown_export_bytes() -> bytes:
|
|
| 2228 |
if kpi_select.value and str(kpi_select.value) not in selected_kpis:
|
| 2229 |
selected_kpis = [str(kpi_select.value)] + selected_kpis
|
| 2230 |
|
| 2231 |
-
selected_kpis = [k for k in selected_kpis if k in
|
| 2232 |
base_cols = ["date_only"]
|
| 2233 |
daily_cols = base_cols + selected_kpis
|
| 2234 |
daily_out = s[daily_cols].copy() if selected_kpis else s[["date_only"]].copy()
|
|
@@ -2243,6 +2323,7 @@ def _build_drilldown_export_bytes() -> bytes:
|
|
| 2243 |
summary_out = pd.DataFrame()
|
| 2244 |
else:
|
| 2245 |
baseline_start, baseline_end, recent_start, recent_end = windows
|
|
|
|
| 2246 |
rows = []
|
| 2247 |
for k in selected_kpis:
|
| 2248 |
rule = _infer_rule_row(rules_df, str(rat), str(k))
|
|
@@ -2275,31 +2356,42 @@ def _build_drilldown_export_bytes() -> bytes:
|
|
| 2275 |
|
| 2276 |
bad_flags = []
|
| 2277 |
recent_vals = sk.loc[recent_mask, ["date_only", k]].sort_values("date_only")
|
|
|
|
| 2278 |
for _, r in recent_vals.iterrows():
|
| 2279 |
v = r.get(k)
|
| 2280 |
-
|
| 2281 |
-
|
| 2282 |
-
|
| 2283 |
-
|
| 2284 |
-
|
| 2285 |
-
|
| 2286 |
-
|
| 2287 |
-
sla_val,
|
| 2288 |
-
)
|
| 2289 |
)
|
| 2290 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2291 |
|
| 2292 |
rows.append(
|
| 2293 |
{
|
| 2294 |
"RAT": str(rat),
|
| 2295 |
-
"site_code": int(
|
| 2296 |
"KPI": str(k),
|
| 2297 |
"direction": direction,
|
| 2298 |
"sla": sla_val,
|
| 2299 |
"baseline_median": baseline_med,
|
| 2300 |
"recent_median": recent_med,
|
| 2301 |
"bad_days_recent": int(sum(bad_flags)),
|
| 2302 |
-
"max_streak_recent": int(max_consecutive_days(
|
| 2303 |
}
|
| 2304 |
)
|
| 2305 |
summary_out = pd.DataFrame(rows)
|
|
@@ -2334,7 +2426,6 @@ def _drilldown_export_callback() -> io.BytesIO:
|
|
| 2334 |
drilldown_export_button.callback = _drilldown_export_callback
|
| 2335 |
|
| 2336 |
|
| 2337 |
-
# Page layout components (used by the multipage portal)
|
| 2338 |
sidebar = pn.Column(
|
| 2339 |
file_2g,
|
| 2340 |
file_3g,
|
|
@@ -2373,9 +2464,7 @@ sidebar = pn.Column(
|
|
| 2373 |
export_button,
|
| 2374 |
)
|
| 2375 |
|
| 2376 |
-
|
| 2377 |
-
status_pane,
|
| 2378 |
-
validation_pane,
|
| 2379 |
pn.pane.Markdown("## Datasets"),
|
| 2380 |
datasets_table,
|
| 2381 |
pn.pane.Markdown("## KPI Rules (editable)"),
|
|
@@ -2386,7 +2475,19 @@ main = pn.Column(
|
|
| 2386 |
multirat_summary_table,
|
| 2387 |
pn.pane.Markdown("## Top anomalies (cross-RAT)"),
|
| 2388 |
top_anomalies_table,
|
| 2389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2390 |
pn.pane.Markdown("## Drill-down"),
|
| 2391 |
pn.Row(site_select, rat_select),
|
| 2392 |
pn.Row(kpi_group_select, kpi_group_mode),
|
|
@@ -2395,6 +2496,21 @@ main = pn.Column(
|
|
| 2395 |
pn.Column(trend_plot_pane, sizing_mode="stretch_both", min_height=500),
|
| 2396 |
pn.Column(heatmap_plot_pane, sizing_mode="stretch_both", min_height=400),
|
| 2397 |
pn.Column(hist_plot_pane, sizing_mode="stretch_both", min_height=400),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2398 |
)
|
| 2399 |
|
| 2400 |
|
|
|
|
| 417 |
rules_table.configuration = cfg
|
| 418 |
except Exception: # noqa: BLE001
|
| 419 |
pass
|
|
|
|
| 420 |
site_summary_table = pn.widgets.Tabulator(
|
| 421 |
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 422 |
)
|
|
|
|
| 425 |
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 426 |
)
|
| 427 |
|
| 428 |
+
complaint_multirat_summary_table = pn.widgets.Tabulator(
|
| 429 |
+
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
top_anomalies_table = pn.widgets.Tabulator(
|
| 433 |
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 434 |
)
|
| 435 |
|
| 436 |
+
complaint_top_anomalies_table = pn.widgets.Tabulator(
|
| 437 |
+
height=260, sizing_mode="stretch_width", layout="fit_data_table"
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
site_select = pn.widgets.AutocompleteInput(
|
| 441 |
name="Select a site (Type to search)",
|
| 442 |
options={},
|
|
|
|
| 453 |
name="Normalization", options=["None", "Min-Max", "Z-score"], value="None"
|
| 454 |
)
|
| 455 |
|
|
|
|
| 456 |
kpi_group_select = pn.widgets.Select(
|
| 457 |
name="KPI Group", options=["All (selected KPIs)"], value="All (selected KPIs)"
|
| 458 |
)
|
|
|
|
| 541 |
_set_tabulator_pagination(site_summary_table, page_size=50)
|
| 542 |
_set_tabulator_pagination(multirat_summary_table, page_size=50)
|
| 543 |
_set_tabulator_pagination(top_anomalies_table, page_size=50)
|
| 544 |
+
_set_tabulator_pagination(complaint_multirat_summary_table, page_size=50)
|
| 545 |
+
_set_tabulator_pagination(complaint_top_anomalies_table, page_size=50)
|
| 546 |
_set_tabulator_pagination(site_kpi_table, page_size=50)
|
| 547 |
trend_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
|
| 548 |
heatmap_plot_pane = pn.pane.Plotly(sizing_mode="stretch_both", config=PLOTLY_CONFIG)
|
|
|
|
| 788 |
]
|
| 789 |
kpis = sorted([str(c) for c in kpis])
|
| 790 |
|
|
|
|
| 791 |
groups = get_kpis_by_group(kpis)
|
| 792 |
group_options = ["All (selected KPIs)"] + sorted(
|
| 793 |
[g for g in groups.keys() if g != "Other"]
|
|
|
|
| 801 |
if kpi_group_select.value not in group_options:
|
| 802 |
kpi_group_select.value = group_options[0]
|
| 803 |
|
|
|
|
| 804 |
filtered_kpis = filter_kpis(
|
| 805 |
kpis, kpi_group_select.value, mode=kpi_group_mode.value
|
| 806 |
)
|
|
|
|
| 858 |
except Exception: # noqa: BLE001
|
| 859 |
available_sites = set()
|
| 860 |
|
|
|
|
|
|
|
| 861 |
code_int = None
|
| 862 |
if code is not None:
|
|
|
|
| 863 |
if hasattr(site_select, "options") and isinstance(site_select.options, dict):
|
|
|
|
| 864 |
if code in site_select.options:
|
| 865 |
code_int = site_select.options[code]
|
|
|
|
| 866 |
elif code in site_select.options.values():
|
| 867 |
code_int = code
|
| 868 |
|
|
|
|
| 869 |
if code_int is None:
|
| 870 |
try:
|
| 871 |
code_int = int(code)
|
|
|
|
| 932 |
trend_plot_pane.object, heatmap_plot_pane.object, hist_plot_pane.object = cached
|
| 933 |
return
|
| 934 |
|
|
|
|
| 935 |
kpis_to_plot = []
|
| 936 |
|
|
|
|
| 937 |
selected = [str(x) for x in (kpi_compare_select.value or []) if str(x)]
|
| 938 |
|
|
|
|
| 939 |
if kpi and str(kpi) not in selected:
|
| 940 |
selected = [str(kpi)] + selected
|
| 941 |
|
|
|
|
|
|
|
| 942 |
if "Add top" in str(kpi_group_mode.value):
|
| 943 |
from_group = filter_kpis(
|
| 944 |
d.columns.tolist(), kpi_group_select.value, mode="Top-N", top_n=12
|
| 945 |
)
|
|
|
|
| 946 |
for gk in from_group:
|
| 947 |
if gk not in selected:
|
| 948 |
selected.append(gk)
|
| 949 |
|
|
|
|
| 950 |
kpis_to_plot = selected[:15]
|
| 951 |
|
| 952 |
+
rules_df = (
|
| 953 |
+
rules_table.value
|
| 954 |
+
if isinstance(rules_table.value, pd.DataFrame)
|
| 955 |
+
else pd.DataFrame()
|
| 956 |
+
)
|
| 957 |
+
relevant_rules = rules_df
|
| 958 |
+
try:
|
| 959 |
+
if (
|
| 960 |
+
isinstance(rules_df, pd.DataFrame)
|
| 961 |
+
and not rules_df.empty
|
| 962 |
+
and "RAT" in rules_df.columns
|
| 963 |
+
):
|
| 964 |
+
relevant_rules = rules_df[rules_df["RAT"] == rat].copy()
|
| 965 |
+
except Exception: # noqa: BLE001
|
| 966 |
+
relevant_rules = rules_df
|
| 967 |
|
| 968 |
fig = build_drilldown_plot(
|
| 969 |
df=d[d["site_code"] == int(code_int)],
|
|
|
|
| 975 |
rat=rat,
|
| 976 |
)
|
| 977 |
trend_plot_pane.object = fig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
kpis_for_heatmap = []
|
| 979 |
if (
|
| 980 |
isinstance(site_df, pd.DataFrame)
|
|
|
|
| 1484 |
current_top_anomalies_df = pd.DataFrame()
|
| 1485 |
top_anomalies_table.value = current_top_anomalies_df
|
| 1486 |
|
| 1487 |
+
if current_multirat_raw is not None and not current_multirat_raw.empty:
|
| 1488 |
+
cm = _apply_city_filter(current_multirat_raw)
|
| 1489 |
+
if "is_complaint_site" in cm.columns:
|
| 1490 |
+
cm = cm[cm["is_complaint_site"] == True] # noqa: E712
|
| 1491 |
+
else:
|
| 1492 |
+
cm = cm.iloc[0:0].copy()
|
| 1493 |
+
score_col = (
|
| 1494 |
+
"criticality_score_weighted"
|
| 1495 |
+
if "criticality_score_weighted" in cm.columns
|
| 1496 |
+
else "criticality_score"
|
| 1497 |
+
)
|
| 1498 |
+
if score_col in cm.columns:
|
| 1499 |
+
cm = cm[
|
| 1500 |
+
pd.to_numeric(cm[score_col], errors="coerce").fillna(0)
|
| 1501 |
+
>= int(min_criticality.value)
|
| 1502 |
+
]
|
| 1503 |
+
cm = cm.sort_values(by=[score_col], ascending=False)
|
| 1504 |
+
complaint_multirat_summary_table.value = cm
|
| 1505 |
+
else:
|
| 1506 |
+
complaint_multirat_summary_table.value = pd.DataFrame()
|
| 1507 |
+
|
| 1508 |
+
if current_top_anomalies_raw is not None and not current_top_anomalies_raw.empty:
|
| 1509 |
+
ct = _apply_city_filter(current_top_anomalies_raw)
|
| 1510 |
+
if "is_complaint_site" in ct.columns:
|
| 1511 |
+
ct = ct[ct["is_complaint_site"] == True] # noqa: E712
|
| 1512 |
+
else:
|
| 1513 |
+
ct = ct.iloc[0:0].copy()
|
| 1514 |
+
if top_rat_filter.value:
|
| 1515 |
+
ct = ct[ct["RAT"].isin(list(top_rat_filter.value))]
|
| 1516 |
+
if top_status_filter.value and "status" in ct.columns:
|
| 1517 |
+
ct = ct[ct["status"].isin(list(top_status_filter.value))]
|
| 1518 |
+
score_col = (
|
| 1519 |
+
"anomaly_score_weighted"
|
| 1520 |
+
if "anomaly_score_weighted" in ct.columns
|
| 1521 |
+
else "anomaly_score"
|
| 1522 |
+
)
|
| 1523 |
+
if score_col in ct.columns:
|
| 1524 |
+
ct = ct[
|
| 1525 |
+
pd.to_numeric(ct[score_col], errors="coerce").fillna(0)
|
| 1526 |
+
>= int(min_anomaly_score.value)
|
| 1527 |
+
]
|
| 1528 |
+
ct = ct.sort_values(by=[score_col], ascending=False)
|
| 1529 |
+
complaint_top_anomalies_table.value = ct
|
| 1530 |
+
else:
|
| 1531 |
+
complaint_top_anomalies_table.value = pd.DataFrame()
|
| 1532 |
+
|
| 1533 |
current_export_bytes = None
|
| 1534 |
|
| 1535 |
|
|
|
|
| 1875 |
site_summary_table.value = pd.DataFrame()
|
| 1876 |
multirat_summary_table.value = pd.DataFrame()
|
| 1877 |
top_anomalies_table.value = pd.DataFrame()
|
| 1878 |
+
complaint_multirat_summary_table.value = pd.DataFrame()
|
| 1879 |
+
complaint_top_anomalies_table.value = pd.DataFrame()
|
| 1880 |
site_kpi_table.value = pd.DataFrame()
|
| 1881 |
trend_plot_pane.object = None
|
| 1882 |
heatmap_plot_pane.object = None
|
|
|
|
| 1968 |
_loading_datasets = False
|
| 1969 |
try:
|
| 1970 |
_update_site_options()
|
| 1971 |
+
except Exception: # noqa: BLE001
|
| 1972 |
+
pass
|
| 1973 |
+
|
| 1974 |
+
try:
|
| 1975 |
_update_kpi_options()
|
| 1976 |
+
except Exception: # noqa: BLE001
|
| 1977 |
+
pass
|
| 1978 |
+
|
| 1979 |
+
try:
|
| 1980 |
_update_site_view()
|
| 1981 |
+
except Exception: # noqa: BLE001
|
| 1982 |
+
pass
|
| 1983 |
+
|
| 1984 |
+
try:
|
| 1985 |
_refresh_validation_state()
|
| 1986 |
except Exception: # noqa: BLE001
|
| 1987 |
pass
|
|
|
|
| 2144 |
if isinstance(current_top_anomalies_df, pd.DataFrame)
|
| 2145 |
else None
|
| 2146 |
),
|
| 2147 |
+
(
|
| 2148 |
+
complaint_multirat_summary_table.value
|
| 2149 |
+
if isinstance(complaint_multirat_summary_table.value, pd.DataFrame)
|
| 2150 |
+
else None
|
| 2151 |
+
),
|
| 2152 |
+
(
|
| 2153 |
+
complaint_top_anomalies_table.value
|
| 2154 |
+
if isinstance(complaint_top_anomalies_table.value, pd.DataFrame)
|
| 2155 |
+
else None
|
| 2156 |
+
),
|
| 2157 |
)
|
| 2158 |
|
| 2159 |
|
|
|
|
| 2242 |
except Exception: # noqa: BLE001
|
| 2243 |
pass
|
| 2244 |
|
| 2245 |
+
try:
|
| 2246 |
+
complaint_top_anomalies_table.on_click(
|
| 2247 |
+
lambda e: _handle_double_click("top", complaint_top_anomalies_table, e)
|
| 2248 |
+
)
|
| 2249 |
+
except Exception: # noqa: BLE001
|
| 2250 |
+
pass
|
| 2251 |
+
|
| 2252 |
try:
|
| 2253 |
multirat_summary_table.on_click(
|
| 2254 |
lambda e: _handle_double_click("multirat", multirat_summary_table, e)
|
|
|
|
| 2256 |
except Exception: # noqa: BLE001
|
| 2257 |
pass
|
| 2258 |
|
| 2259 |
+
try:
|
| 2260 |
+
complaint_multirat_summary_table.on_click(
|
| 2261 |
+
lambda e: _handle_double_click("multirat", complaint_multirat_summary_table, e)
|
| 2262 |
+
)
|
| 2263 |
+
except Exception: # noqa: BLE001
|
| 2264 |
+
pass
|
| 2265 |
+
|
| 2266 |
min_criticality.param.watch(_refresh_filtered_results, "value")
|
| 2267 |
min_anomaly_score.param.watch(_refresh_filtered_results, "value")
|
| 2268 |
city_filter.param.watch(_refresh_filtered_results, "value")
|
|
|
|
| 2308 |
if kpi_select.value and str(kpi_select.value) not in selected_kpis:
|
| 2309 |
selected_kpis = [str(kpi_select.value)] + selected_kpis
|
| 2310 |
|
| 2311 |
+
selected_kpis = [k for k in selected_kpis if k in d.columns]
|
| 2312 |
base_cols = ["date_only"]
|
| 2313 |
daily_cols = base_cols + selected_kpis
|
| 2314 |
daily_out = s[daily_cols].copy() if selected_kpis else s[["date_only"]].copy()
|
|
|
|
| 2323 |
summary_out = pd.DataFrame()
|
| 2324 |
else:
|
| 2325 |
baseline_start, baseline_end, recent_start, recent_end = windows
|
| 2326 |
+
|
| 2327 |
rows = []
|
| 2328 |
for k in selected_kpis:
|
| 2329 |
rule = _infer_rule_row(rules_df, str(rat), str(k))
|
|
|
|
| 2356 |
|
| 2357 |
bad_flags = []
|
| 2358 |
recent_vals = sk.loc[recent_mask, ["date_only", k]].sort_values("date_only")
|
| 2359 |
+
bad_dates: list[date] = []
|
| 2360 |
for _, r in recent_vals.iterrows():
|
| 2361 |
v = r.get(k)
|
| 2362 |
+
is_bad_day = bool(
|
| 2363 |
+
is_bad(
|
| 2364 |
+
float(v) if pd.notna(v) else None,
|
| 2365 |
+
baseline_med,
|
| 2366 |
+
direction,
|
| 2367 |
+
float(rel_threshold_pct.value),
|
| 2368 |
+
sla_val,
|
|
|
|
|
|
|
| 2369 |
)
|
| 2370 |
)
|
| 2371 |
+
bad_flags.append(is_bad_day)
|
| 2372 |
+
if is_bad_day:
|
| 2373 |
+
try:
|
| 2374 |
+
d0 = r.get("date_only")
|
| 2375 |
+
if d0 is not None:
|
| 2376 |
+
bad_dates.append(
|
| 2377 |
+
d0
|
| 2378 |
+
if isinstance(d0, date)
|
| 2379 |
+
else pd.to_datetime(d0).date()
|
| 2380 |
+
)
|
| 2381 |
+
except Exception: # noqa: BLE001
|
| 2382 |
+
pass
|
| 2383 |
|
| 2384 |
rows.append(
|
| 2385 |
{
|
| 2386 |
"RAT": str(rat),
|
| 2387 |
+
"site_code": int(code_int),
|
| 2388 |
"KPI": str(k),
|
| 2389 |
"direction": direction,
|
| 2390 |
"sla": sla_val,
|
| 2391 |
"baseline_median": baseline_med,
|
| 2392 |
"recent_median": recent_med,
|
| 2393 |
"bad_days_recent": int(sum(bad_flags)),
|
| 2394 |
+
"max_streak_recent": int(max_consecutive_days(bad_dates)),
|
| 2395 |
}
|
| 2396 |
)
|
| 2397 |
summary_out = pd.DataFrame(rows)
|
|
|
|
| 2426 |
drilldown_export_button.callback = _drilldown_export_callback
|
| 2427 |
|
| 2428 |
|
|
|
|
| 2429 |
sidebar = pn.Column(
|
| 2430 |
file_2g,
|
| 2431 |
file_3g,
|
|
|
|
| 2464 |
export_button,
|
| 2465 |
)
|
| 2466 |
|
| 2467 |
+
_tab_overview = pn.Column(
|
|
|
|
|
|
|
| 2468 |
pn.pane.Markdown("## Datasets"),
|
| 2469 |
datasets_table,
|
| 2470 |
pn.pane.Markdown("## KPI Rules (editable)"),
|
|
|
|
| 2475 |
multirat_summary_table,
|
| 2476 |
pn.pane.Markdown("## Top anomalies (cross-RAT)"),
|
| 2477 |
top_anomalies_table,
|
| 2478 |
+
sizing_mode="stretch_width",
|
| 2479 |
+
)
|
| 2480 |
+
|
| 2481 |
+
_tab_complaint = pn.Column(
|
| 2482 |
+
pn.pane.Markdown("## Complaint sites only"),
|
| 2483 |
+
pn.pane.Markdown("### Multi-RAT Summary (Complaint)"),
|
| 2484 |
+
complaint_multirat_summary_table,
|
| 2485 |
+
pn.pane.Markdown("### Top anomalies (Complaint)"),
|
| 2486 |
+
complaint_top_anomalies_table,
|
| 2487 |
+
sizing_mode="stretch_width",
|
| 2488 |
+
)
|
| 2489 |
+
|
| 2490 |
+
_tab_drilldown = pn.Column(
|
| 2491 |
pn.pane.Markdown("## Drill-down"),
|
| 2492 |
pn.Row(site_select, rat_select),
|
| 2493 |
pn.Row(kpi_group_select, kpi_group_mode),
|
|
|
|
| 2496 |
pn.Column(trend_plot_pane, sizing_mode="stretch_both", min_height=500),
|
| 2497 |
pn.Column(heatmap_plot_pane, sizing_mode="stretch_both", min_height=400),
|
| 2498 |
pn.Column(hist_plot_pane, sizing_mode="stretch_both", min_height=400),
|
| 2499 |
+
sizing_mode="stretch_both",
|
| 2500 |
+
)
|
| 2501 |
+
|
| 2502 |
+
_tabs_main = pn.Tabs(
|
| 2503 |
+
("Overview", _tab_overview),
|
| 2504 |
+
("Complaint sites only", _tab_complaint),
|
| 2505 |
+
("Drill-down", _tab_drilldown),
|
| 2506 |
+
dynamic=True,
|
| 2507 |
+
sizing_mode="stretch_both",
|
| 2508 |
+
)
|
| 2509 |
+
|
| 2510 |
+
main = pn.Column(
|
| 2511 |
+
status_pane,
|
| 2512 |
+
validation_pane,
|
| 2513 |
+
_tabs_main,
|
| 2514 |
)
|
| 2515 |
|
| 2516 |
|
process_kpi/kpi_health_check/export.py
CHANGED
|
@@ -10,6 +10,8 @@ def build_export_bytes(
|
|
| 10 |
status_df: pd.DataFrame | None,
|
| 11 |
multirat_summary_df: pd.DataFrame | None = None,
|
| 12 |
top_anomalies_df: pd.DataFrame | None = None,
|
|
|
|
|
|
|
| 13 |
) -> bytes:
|
| 14 |
dfs = [
|
| 15 |
datasets_df if isinstance(datasets_df, pd.DataFrame) else pd.DataFrame(),
|
|
@@ -26,6 +28,16 @@ def build_export_bytes(
|
|
| 26 |
if isinstance(top_anomalies_df, pd.DataFrame)
|
| 27 |
else pd.DataFrame()
|
| 28 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
]
|
| 30 |
sheet_names = [
|
| 31 |
"Datasets",
|
|
@@ -34,5 +46,7 @@ def build_export_bytes(
|
|
| 34 |
"Site_KPI_Status",
|
| 35 |
"MultiRAT_Summary",
|
| 36 |
"Top_Anomalies",
|
|
|
|
|
|
|
| 37 |
]
|
| 38 |
return write_dfs_to_excel(dfs, sheet_names, index=False)
|
|
|
|
| 10 |
status_df: pd.DataFrame | None,
|
| 11 |
multirat_summary_df: pd.DataFrame | None = None,
|
| 12 |
top_anomalies_df: pd.DataFrame | None = None,
|
| 13 |
+
complaint_multirat_df: pd.DataFrame | None = None,
|
| 14 |
+
complaint_top_anomalies_df: pd.DataFrame | None = None,
|
| 15 |
) -> bytes:
|
| 16 |
dfs = [
|
| 17 |
datasets_df if isinstance(datasets_df, pd.DataFrame) else pd.DataFrame(),
|
|
|
|
| 28 |
if isinstance(top_anomalies_df, pd.DataFrame)
|
| 29 |
else pd.DataFrame()
|
| 30 |
),
|
| 31 |
+
(
|
| 32 |
+
complaint_multirat_df
|
| 33 |
+
if isinstance(complaint_multirat_df, pd.DataFrame)
|
| 34 |
+
else pd.DataFrame()
|
| 35 |
+
),
|
| 36 |
+
(
|
| 37 |
+
complaint_top_anomalies_df
|
| 38 |
+
if isinstance(complaint_top_anomalies_df, pd.DataFrame)
|
| 39 |
+
else pd.DataFrame()
|
| 40 |
+
),
|
| 41 |
]
|
| 42 |
sheet_names = [
|
| 43 |
"Datasets",
|
|
|
|
| 46 |
"Site_KPI_Status",
|
| 47 |
"MultiRAT_Summary",
|
| 48 |
"Top_Anomalies",
|
| 49 |
+
"Complaint_MultiRAT",
|
| 50 |
+
"Complaint_Top_Anomalies",
|
| 51 |
]
|
| 52 |
return write_dfs_to_excel(dfs, sheet_names, index=False)
|