252 KiB
252 KiB
In [ ]:
%matplotlib inline
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
pd.set_option("display.width", 160); pd.set_option("display.max_columns", 30)
THU_MUC = "/opt/csai/data/dietpi_health"
d30 = pd.read_csv(f"{THU_MUC}/dietpi_health_30s.csv.gz", parse_dates=["ts"])
d5 = pd.read_csv(f"{THU_MUC}/dietpi_health_5min.csv", parse_dates=["ts"])
d1h = pd.read_csv(f"{THU_MUC}/dietpi_health_1h.csv", parse_dates=["ts"])
so_su_co = pd.read_csv(f"{THU_MUC}/incidents.csv", keep_default_na=False) # keep_default_na: giữ chữ "n/a" là chữ
# 📦 d30 = mỗi 30 giây 1 dòng · d5 = mỗi 5 phút · d1h = mỗi giờ · so_su_co = sổ sự cố thật
print(f"30 giây: {len(d30):,} dòng, từ {d30.ts.min()} đến {d30.ts.max()}")
print(f"5 phút : {len(d5):,} dòng | 1 giờ: {len(d1h):,} dòng")
print("Các cột:", ", ".join(d30.columns[1:]))
so_o_trong = d30.drop(columns=["ts"]).isna().sum().max() # số dòng trống của cột trống nhiều nhất
so_ngay_30 = (d30.ts.max() - d30.ts.min()).total_seconds() / 86400
print("Tỉ lệ ô trống lớn nhất của một cột:", f"{so_o_trong / len(d30):.3%}",
f"= {so_o_trong} lần đo × 30 giây ≈ {so_o_trong * 30 / 60:.1f} phút trên {so_ngay_30:.1f} ngày")
print()
print("Sổ sự cố — số dòng theo loại và mức chắc chắn:")
print(pd.crosstab(so_su_co.kind, so_su_co.status))In [ ]:
# Các đợt bất thường thật nằm TRONG 30 ngày này (dùng để chấm ở Phần 5)
su_co = so_su_co[(so_su_co.kind == "incident") & (so_su_co.in_window == "YES")].copy()
su_co["bat_dau"] = pd.to_datetime(su_co.start_ts); su_co["ket_thuc"] = pd.to_datetime(su_co.end_ts)
dinh_ky = so_su_co[so_su_co.kind == "recurring_normal"]
# 📦 su_co = các đợt E nằm trong 30 ngày · dinh_ky = các việc định kỳ R (cron, backup)
pd.set_option("display.max_colwidth", 70)
print(su_co[["id", "start_ts", "end_ts", "status", "title"]].to_string(index=False))
print()
print(dinh_ky[["id", "start_ts", "title"]].to_string(index=False))In [ ]:
# 1.1 Chỗ trống: các lần Prometheus không hỏi được máy chủ (scrape_ok = 0)
trong = d30[d30.scrape_ok == 0]
nhom = (trong.ts.diff() > pd.Timedelta("1min")).cumsum()
khoang_trong = trong.groupby(nhom).ts.agg(["min", "max", "count"])
khoang_trong["phút"] = khoang_trong["count"] * 0.5
print(f"Tổng số dòng trống: {len(trong)} / {len(d30):,} ({len(trong)/len(d30):.2%})")
print("Khoảng trống dài hơn 2 phút:")
print(khoang_trong[khoang_trong["phút"] > 2].to_string(index=False))
# Vẽ 2 giờ quanh E1 ở độ phân giải 30 giây
cua_so = d30[(d30.ts >= "2026-09-22 16:00+07:00") & (d30.ts <= "2026-09-22 18:45+07:00")]
fig, truc = plt.subplots(2, 1, figsize=(14, 5.5), sharex=True)
truc[0].plot(cua_so.ts, cua_so.load15, color="#1E3A5F"); truc[0].set_ylabel("load15")
truc[1].plot(cua_so.ts, cua_so.cpu_iowait_pct, color="#DC2626"); truc[1].set_ylabel("iowait %")
for t in truc:
t.axvspan(khoang_trong["min"].iloc[khoang_trong["phút"].argmax()], khoang_trong["max"].iloc[khoang_trong["phút"].argmax()],
color="#9CA3AF", alpha=0.35, label="Prometheus không hỏi được máy chủ")
t.grid(alpha=0.3); t.legend(loc="upper left")
truc[1].xaxis.set_major_formatter(mdates.DateFormatter("%H:%M", tz=cua_so.ts.dt.tz))
plt.suptitle("E1 — 22/09/2026: máy chủ quá tải, giám sát bị 'mù'"); plt.tight_layout(); plt.show()In [ ]:
# 1.2 Cái bẫy "lấy trung bình": cùng đợt E1, nhìn ở 3 độ phân giải
a, b = "2026-09-22 16:30+07:00", "2026-09-22 18:30+07:00"
cat = lambda d: d[(d.ts >= a) & (d.ts <= b)]
print(f"iowait lớn nhất trong E1 — đo mỗi 30 giây : {cat(d30).cpu_iowait_pct.max():.1f} %")
print(f"iowait lớn nhất trong E1 — trung bình mỗi 5 phút : {cat(d5).cpu_iowait_pct.max():.1f} %")
print(f"iowait lớn nhất trong E1 — trung bình mỗi 1 giờ : {cat(d1h).cpu_iowait_pct_mean.max():.1f} %")
print(f"iowait lớn nhất trong E1 — 'lớn nhất' của mỗi 1 giờ : {cat(d1h).cpu_iowait_pct_max.max():.1f} %")
fig, t = plt.subplots(figsize=(14, 3.6))
t.plot(cat(d30).ts, cat(d30).cpu_iowait_pct, label="mỗi 30 giây", alpha=0.6)
t.plot(cat(d5).ts, cat(d5).cpu_iowait_pct, label="trung bình 5 phút", lw=2)
t.step(cat(d1h).ts, cat(d1h).cpu_iowait_pct_mean, where="post", label="trung bình 1 giờ", lw=3)
t.set_ylabel("iowait %"); t.legend(); t.grid(alpha=0.3); t.set_title("Càng gộp thô, đỉnh sự cố càng 'biến mất'")
t.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M", tz=d30.ts.dt.tz)); plt.show()In [ ]:
# 2.1 Mỗi giờ trong ngày: iowait "thường ngày" (trung vị) và "ngày xấu" (bách phân vị 95)
d30["gio"] = d30.ts.dt.hour
d30["phut_trong_ngay"] = d30.ts.dt.hour * 60 + d30.ts.dt.minute
theo_gio = d30.groupby("gio").cpu_iowait_pct.agg(trung_vi="median", p95=lambda s: s.quantile(0.95))
# 📦 theo_gio = mỗi giờ 0-23 một dòng: trung vị và p95 của iowait
print(theo_gio.round(2).T.to_string())
fig, truc = plt.subplots(1, 2, figsize=(15, 3.8))
truc[0].bar(theo_gio.index, theo_gio.p95, color="#DC2626", label="p95 (5% lúc tệ nhất)")
truc[0].bar(theo_gio.index, theo_gio.trung_vi, color="#2563EB", label="trung vị (thường ngày)")
truc[0].set_xlabel("giờ trong ngày"); truc[0].set_ylabel("iowait %"); truc[0].legend(); truc[0].grid(alpha=0.3, axis="y")
truc[0].set_title("iowait theo giờ: 4 cột cao bất thường")
# Từng khung 5 phút: khung nào có trung vị cao nhất?
khung5 = d30.assign(k=(d30.phut_trong_ngay // 5) * 5).groupby("k").cpu_iowait_pct.median()
truc[1].plot(khung5.index / 60, khung5.values, color="#1E3A5F")
truc[1].set_xlabel("giờ trong ngày (mỗi điểm = 5 phút)"); truc[1].set_ylabel("iowait % (trung vị 30 ngày)"); truc[1].grid(alpha=0.3)
truc[1].set_title("Trung vị từng khung 5 phút")
plt.tight_layout(); plt.show()
print("5 khung 5 phút có trung vị iowait cao nhất (phút tính từ 00:00):", khung5.sort_values(ascending=False).head(5).round(2).to_dict())In [ ]:
# 3.1 Bản đồ nhiệt: thứ trong tuần × giờ — mạng gửi đi (p95)
d30["thu"] = d30.ts.dt.dayofweek # 0 = Thứ Hai ... 6 = Chủ nhật
ten_thu = ["T2", "T3", "T4", "T5", "T6", "T7", "CN"]
bang_nhiet = d30.pivot_table(index="thu", columns="gio", values="net_eth0_tx_mbps", aggfunc=lambda s: s.quantile(0.95))
fig, t = plt.subplots(figsize=(15, 3.6))
anh = t.imshow(bang_nhiet.values, aspect="auto", cmap="magma")
t.set_yticks(range(7)); t.set_yticklabels(ten_thu); t.set_xticks(range(24)); t.set_xlabel("giờ trong ngày")
plt.colorbar(anh, ax=t, label="mạng gửi đi, p95 (Mbps)"); t.set_title("Mạng gửi đi theo thứ × giờ (4 tuần)"); plt.show()
# 3.2 So con số: 03:30–03:50 Chủ nhật vs các ngày khác
khung = d30.phut_trong_ngay.between(3 * 60 + 30, 3 * 60 + 50)
cn, khac = d30[khung & (d30.thu == 6)], d30[khung & (d30.thu != 6)]
so = pd.DataFrame({c: [cn[c].quantile(0.95), khac[c].quantile(0.95)]
for c in ["net_eth0_tx_mbps", "disk_sda_read_mbps", "cpu_iowait_pct"]}, index=["Chủ nhật 03:30–03:50", "ngày khác 03:30–03:50"]).round(2)
# 📦 so = bảng p95 của 3 chỉ số, Chủ nhật so với các ngày khác cùng khung giờ
print(so.to_string())
print("Số tuần trong dữ liệu (tuần ISO):", sorted(d30.ts.dt.isocalendar().week.unique().tolist()))In [ ]:
# 4.1 Bản đồ nhiệt ngày × giờ: iowait lớn nhất mỗi giờ
d30["ngay"] = d30.ts.dt.date # ngày thật (sắp xếp đúng thứ tự thời gian)
nhiet_thang = d30.pivot_table(index="ngay", columns="gio", values="cpu_iowait_pct", aggfunc="max")
nhiet_thang = nhiet_thang.reindex(columns=range(24)) # cột luôn là giờ 0..23 theo đúng thứ tự
# 📦 nhiet_thang = bảng ngày × giờ (0-23), mỗi ô = iowait lớn nhất trong giờ đó
fig, t = plt.subplots(figsize=(15, 9))
anh = t.imshow(nhiet_thang.values, aspect="auto", cmap="inferno", vmin=0, vmax=100)
t.set_yticks(range(len(nhiet_thang))); t.set_yticklabels([n.strftime("%d/%m") for n in nhiet_thang.index], fontsize=8)
t.set_xticks(range(24)); t.set_xlabel("giờ trong ngày")
plt.colorbar(anh, ax=t, label="iowait lớn nhất trong giờ (%)")
for _, r in su_co.iterrows(): # khung xanh = đợt trong sổ sự cố
ngay = r.bat_dau.date()
if ngay in nhiet_thang.index:
y = list(nhiet_thang.index).index(ngay)
t.add_patch(plt.Rectangle((r.bat_dau.hour - 0.5, y - 0.5), max(1, r.ket_thuc.hour - r.bat_dau.hour + 1), 1, fill=False, ec="cyan", lw=2))
t.text(r.ket_thuc.hour + 0.7, y, r.id, color="cyan", fontsize=8, va="center")
t.set_title("iowait theo ngày × giờ (khung xanh = đợt trong sổ sự cố)"); plt.show()In [ ]:
# 4.2 Tóm tắt từng ngày + "bình thường" trước / sau khi chuyển honeypot (S3, 22–23/09)
theo_ngay = d30.set_index("ts").resample("1D").agg({"load15": "max", "cpu_iowait_pct": "max", "mem_used_pct": "mean", "temp_max_c": "max"})
fig, truc = plt.subplots(4, 1, figsize=(15, 9), sharex=True)
for t, c, nhan in zip(truc, theo_ngay.columns, ["load15 lớn nhất", "iowait lớn nhất %", "RAM dùng trung bình %", "nhiệt độ lớn nhất °C"]):
t.plot(theo_ngay.index, theo_ngay[c], marker="o", color="#1E3A5F"); t.set_ylabel(nhan, fontsize=9); t.grid(alpha=0.3)
t.axvspan(pd.Timestamp("2026-09-22", tz="Asia/Ho_Chi_Minh"), pd.Timestamp("2026-09-24", tz="Asia/Ho_Chi_Minh"), color="#D97706", alpha=0.25)
for _, r in su_co.iterrows():
t.axvline(r.bat_dau, color="#DC2626", alpha=0.4, lw=1)
truc[0].set_yscale("log"); truc[0].set_title("Từng ngày (vạch đỏ = đợt trong sổ; dải cam = chuyển honeypot S3)")
plt.tight_layout(); plt.show()
truoc = d30[d30.ts < "2026-09-22 18:15+07:00"]; sau = d30[d30.ts >= "2026-09-23 00:00+07:00"]
doi = pd.DataFrame({"trước 22/09": [truoc.cpu_iowait_pct.quantile(0.95), truoc.mem_used_pct.mean(), truoc.swap_used_gib.max()],
"sau 23/09": [sau.cpu_iowait_pct.quantile(0.95), sau.mem_used_pct.mean(), sau.swap_used_gib.max()]},
index=["iowait p95 (%)", "RAM dùng trung bình (%)", "swap lớn nhất (GiB)"]).round(2)
# 📦 doi = "bình thường" trước và sau khi chuyển honeypot
print(doi.to_string())In [ ]:
# 5.1 Chuẩn bị: 11 chỉ số, ô trống tạm lấp bằng số liền trước (chỉ để mô hình chạy được — đã ghi nhớ ở Phần 1)
CHI_SO = ["cpu_iowait_pct", "load15", "mem_used_pct", "cpu_user_pct", "cpu_system_pct", "swap_used_gib",
"psi_io_some_pct", "disk_sda_io_time_pct", "net_eth0_rx_mbps", "temp_max_c", "procs_blocked"]
X = d5[CHI_SO].ffill().bfill()
so_ngay = (d5.ts.max() - d5.ts.min()).total_seconds() / 86400
def gop_dot(co, toi_thieu=1):
# gộp các khung 5 phút bị đánh dấu (co = True) thành đợt; bỏ đợt ngắn hơn toi_thieu khung
t = d5.ts[co]
if t.empty: return []
nhom = (t.diff() > pd.Timedelta("15min")).cumsum()
return [(g.min(), g.max()) for _, g in t.groupby(nhom) if len(g) >= toi_thieu]
def cham(ten, dot):
cham_su_co = lambda s, e: [r.id for _, r in su_co.iterrows() if s <= r.ket_thuc and e >= r.bat_dau]
bat = sorted({i for s, e in dot for i in cham_su_co(s, e)})
co_giai_thich = sum(1 for s, e in dot if cham_su_co(s, e))
return {"bộ phát hiện": ten, "số đợt báo": len(dot), "đợt / ngày": round(len(dot) / so_ngay, 1),
"bắt được": f"{len(bat)}/{len(su_co)}", "sự cố bắt được": " ".join(bat),
"đợt chưa giải thích": len(dot) - co_giai_thich,
"đợt lúc 00:00–00:30": sum(1 for s, e in dot if s.hour == 0 and s.minute <= 30)}
print(f"{len(X):,} khung 5 phút × {len(CHI_SO)} chỉ số, {so_ngay:.1f} ngày; {len(su_co)} sự cố để chấm")In [ ]:
# 5.2 Bộ A — ngưỡng cố định trên iowait
nguong_A = X.cpu_iowait_pct.mean() + 4 * X.cpu_iowait_pct.std()
dot_A = gop_dot(X.cpu_iowait_pct > nguong_A)
print(f"Ngưỡng A = {nguong_A:.2f} % iowait")
# 5.3 Bộ B — nền theo giờ trong ngày
# ✏️ SỬA: K_MAD (bao nhiêu lần MAD thì coi là lạ) và TOI_THIEU (đợt phải dài ít nhất bao nhiêu khung 5 phút)
K_MAD = 20
TOI_THIEU = 3
NEN_TU = "2026-09-23 00:00+07:00" # chỉ học "bình thường" từ sau khi chuyển honeypot (Phần 4.2)
khung = d5.ts.dt.hour * 12 + d5.ts.dt.minute // 5 # 288 khung 5 phút trong một ngày
lech = pd.DataFrame(index=d5.index)
for c in ["cpu_iowait_pct", "load15", "mem_used_pct", "cpu_user_pct", "temp_max_c"]:
goc = X[c].where(d5.ts >= NEN_TU) # chỉ dùng dữ liệu "bình thường mới" để tính nền
trung_vi = goc.groupby(khung).transform("median")
mad = (goc - trung_vi).abs().groupby(khung).transform("median")
lech[c] = (X[c] - trung_vi) / (1.4826 * mad + 1e-6 + 0.05 * X[c].abs().median())
diem_B = lech.max(axis=1) # lệch nhiều nhất trong 5 chỉ số
dot_B = gop_dot(diem_B > K_MAD, TOI_THIEU)
# 📦 lech = mỗi khung 5 phút × 5 chỉ số: lệch khỏi "bình thường của khung giờ đó" bao nhiêu lần MAD
# 5.4 Bộ C — Isolation Forest trên 11 chỉ số
from sklearn.ensemble import IsolationForest
rung = IsolationForest(n_estimators=200, contamination=0.01, random_state=0).fit(X)
dot_C = gop_dot(pd.Series(rung.predict(X) == -1, index=d5.index))
bang_cham = pd.DataFrame([cham("A. ngưỡng cố định", dot_A), cham(f"B. nền theo giờ (k={K_MAD}, ≥{TOI_THIEU} khung)", dot_B),
cham("C. Isolation Forest (1%)", dot_C)])
print(bang_cham.to_string(index=False))In [ ]:
# 5.5 Nhìn cả 30 ngày: mỗi bộ báo ở đâu, sự cố thật nằm ở đâu
fig, t = plt.subplots(figsize=(15, 3.8))
for y, (ten, dot, mau) in enumerate([("A ngưỡng cố định", dot_A, "#6B7280"), ("B nền theo giờ", dot_B, "#2563EB"), ("C Isolation Forest", dot_C, "#7C3AED")]):
for s, e in dot:
t.plot([s, e + pd.Timedelta("5min")], [y, y], lw=8, color=mau, solid_capstyle="butt")
t.text(d5.ts.min(), y + 0.3, ten, fontsize=9, color=mau)
for _, r in su_co.iterrows():
t.axvspan(r.bat_dau, r.ket_thuc + pd.Timedelta("5min"), color="#DC2626", alpha=0.25)
t.text(r.bat_dau, 2.75, r.id, fontsize=8, color="#DC2626")
t.set_yticks([]); t.set_ylim(-0.6, 3.1); t.grid(alpha=0.3, axis="x")
t.xaxis.set_major_formatter(mdates.DateFormatter("%d/%m", tz=d5.ts.dt.tz))
t.set_title("Đợt báo động của 3 bộ (thanh màu) so với sự cố trong sổ (dải đỏ)"); plt.show()In [ ]:
MOC_S3 = pd.Timestamp("2026-09-23 00:00+07:00") # ✏️ SỬA: thử mốc khác để xem đoạn nào ồn
ngay_truoc = (MOC_S3 - d5.ts.min()).total_seconds() / 86400
ngay_sau = (d5.ts.max() - MOC_S3).total_seconds() / 86400
cham_dai_do = lambda s, e: any(s <= r.ket_thuc and e >= r.bat_dau for _, r in su_co.iterrows()) # đợt có chạm sự cố nào không
dong = []
for ten, dot in [("A ngưỡng cố định", dot_A), ("B nền theo giờ", dot_B), ("C Isolation Forest", dot_C)]:
truoc = sum(1 for s, e in dot if s < MOC_S3)
trung = sum(1 for s, e in dot if cham_dai_do(s, e))
dong.append({"bộ": ten, "đợt trước 23/09": truoc, "mỗi ngày (trước)": round(truoc / ngay_truoc, 1),
"đợt từ 23/09": len(dot) - truoc, "mỗi ngày (sau)": round((len(dot) - truoc) / ngay_sau, 1),
"đợt chạm dải đỏ": trung, "đợt xa mọi dải đỏ": len(dot) - trung})
bang_hinh = pd.DataFrame(dong)
# 📦 bang_hinh = mỗi bộ một hàng: số đợt trước / sau mốc S3 (và quy ra mỗi ngày), số đợt trùng / không trùng sự cố
print(f"Trước 23/09: {ngay_truoc:.1f} ngày | từ 23/09: {ngay_sau:.1f} ngày")
print(bang_hinh.to_string(index=False))In [ ]:
# 5.6 Cái bập bênh: đổi K_MAD và TOI_THIEU của bộ B — bắt nhiều hơn thì báo nhiều hơn
hang = []
for k in [10, 20, 30]:
for toi_thieu in [1, 2, 3]:
hang.append(cham(f"k={k}, ≥{toi_thieu} khung", gop_dot(diem_B > k, toi_thieu)))
bap_benh = pd.DataFrame(hang)
print(bap_benh[["bộ phát hiện", "số đợt báo", "đợt / ngày", "bắt được", "đợt chưa giải thích"]].to_string(index=False))
fig, t = plt.subplots(figsize=(7, 4.5))
t.scatter(bap_benh["đợt / ngày"], bap_benh["bắt được"].str.split("/").str[0].astype(int), s=80, color="#2563EB")
for _, r in bap_benh.iterrows():
t.annotate(r["bộ phát hiện"], (r["đợt / ngày"], int(r["bắt được"].split("/")[0])), fontsize=8, xytext=(4, 4), textcoords="offset points")
t.set_xlabel("số đợt báo động mỗi ngày (SOC phải đi xem)"); t.set_ylabel(f"số sự cố bắt được / {len(su_co)}")
t.grid(alpha=0.3); t.set_title("Bắt thêm sự cố = phải đọc thêm báo động"); plt.show()In [ ]:
# 6.1 Hàng xóm giống nhau cỡ nào? (tự tương quan: 1 = giống hệt, 0 = không liên quan)
print(f"Tự tương quan giữa 2 khung 5 phút liền nhau — iowait: {X.cpu_iowait_pct.autocorr(1):.2f} | load15: {X.load15.autocorr(1):.2f}")
# 6.2 Nhãn: khung 5 phút nằm trong một đợt của sổ sự cố
y = pd.Series(0, index=d5.index); ten_su_co = pd.Series("", index=d5.index)
for _, r in su_co.iterrows():
m = (d5.ts >= r.bat_dau) & (d5.ts <= r.ket_thuc); y[m] = 1; ten_su_co[m] = r.id
print(f"Khung 5 phút có nhãn 'sự cố': {int(y.sum())} / {len(y):,} ({y.mean():.1%})")
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_score, recall_score, f1_score
def do(y_that, du_doan):
return {"precision": round(precision_score(y_that, du_doan, zero_division=0), 3),
"recall": round(recall_score(y_that, du_doan, zero_division=0), 3), "F1": round(f1_score(y_that, du_doan, zero_division=0), 3)}
# Cách 1: chia NGẪU NHIÊN 70/30
X_hoc, X_thi, y_hoc, y_thi = train_test_split(X, y, test_size=0.3, random_state=0, shuffle=True, stratify=y)
rung_1 = RandomForestClassifier(n_estimators=200, random_state=0, n_jobs=2).fit(X_hoc, y_hoc)
kq_ngau_nhien = do(y_thi, rung_1.predict(X_thi))
# Cách 2: chia THEO THỜI GIAN — học trước ngày cắt, thi sau ngày cắt
# ✏️ SỬA: ngày cắt
NGAY_CAT = "2026-09-24 00:00+07:00"
hoc = d5.ts < NGAY_CAT
rung_2 = RandomForestClassifier(n_estimators=200, random_state=0, n_jobs=2).fit(X[hoc], y[hoc])
kq_thoi_gian = do(y[~hoc], rung_2.predict(X[~hoc]))
print(f"Sự cố trong phần học: {sorted(ten_su_co[hoc & (y == 1)].unique())} | trong phần thi: {sorted(ten_su_co[~hoc & (y == 1)].unique())}")
ket_qua = pd.DataFrame([kq_ngau_nhien, kq_thoi_gian], index=["chia NGẪU NHIÊN", f"chia THEO THỜI GIAN (cắt {NGAY_CAT[:10]})"])
print(ket_qua.to_string())
ket_qua.plot.barh(figsize=(10, 3), xlim=(0, 1), title="Cùng dữ liệu, cùng mô hình — chỉ khác cách chia", color=["#2563EB", "#059669", "#D97706"])
plt.grid(alpha=0.3, axis="x"); plt.show()In [ ]:
# 6.3 Thêm 3 cái bẫy trung thực trong chính bài này
# (a) Sổ sự cố: bao nhiêu đợt thật sự ĐÃ XÁC NHẬN?
print("Mức chắc chắn của các sự cố dùng để chấm:", su_co.status.value_counts().to_dict())
# (b) "Bình thường" đổi sau khi chuyển honeypot: Isolation Forest học TRƯỚC 22/09 rồi dùng SAU 23/09
truoc_doi = d5.ts < "2026-09-22 00:00+07:00"; sau_doi = d5.ts >= "2026-09-23 00:00+07:00"
rung_cu = IsolationForest(n_estimators=200, contamination=0.01, random_state=0).fit(X[truoc_doi])
co_cu = pd.Series(rung_cu.predict(X) == -1, index=d5.index)
print(f"Isolation Forest học trước 22/09 — tỉ lệ báo động: trước {co_cu[truoc_doi].mean():.1%} | sau 23/09 {co_cu[sau_doi].mean():.1%}")
# (c) Nền của bộ B được tính trên CHÍNH 30 ngày đang chấm (không có phần thi riêng)
print("Nền bộ B tính từ:", NEN_TU[:10], "đến hết dữ liệu — cùng khoảng thời gian dùng để chấm")In [ ]:
# 8.1 Nạp dữ liệu service
sv = pd.read_csv(f"{THU_MUC}/services/services_1min.csv.gz", parse_dates=["ts"])
lich = pd.read_csv(f"{THU_MUC}/services/schedule.csv", keep_default_na=False)
anh_chup = pd.read_csv(f"{THU_MUC}/services/process_snapshots.csv", parse_dates=["ts"])
COT = [c for c in sv.columns if "__" in c] # tên cột dạng "<service>__<chỉ số>"
DICH_VU = sorted({c.split("__")[0] for c in COT} - {"host"})
# 📦 sv = mỗi phút 1 dòng, mỗi service vài cột · lich = lịch cron/timer · anh_chup = tiến trình ghi đĩa nhiều khi iowait ≥ 15%
print(f"{len(sv):,} phút, {len(DICH_VU)} service (gồm '_host_outside_containers'): {', '.join(DICH_VU)}")
def vuot_muc(ma_su_co):
# mỗi service: trung bình trong sự cố − trung vị của cùng khung giờ ở các ngày khác
r = su_co[su_co.id == ma_su_co].iloc[0]
a, b = r.bat_dau, max(r.ket_thuc, r.bat_dau + pd.Timedelta("10min"))
trong = sv[(sv.ts >= a) & (sv.ts <= b)]
phut = sv.ts.dt.hour * 60 + sv.ts.dt.minute
cung_gio = (phut >= a.hour * 60 + a.minute) & (phut <= b.hour * 60 + b.minute)
ngay_khac = sv[cung_gio & (sv.ts.dt.date != a.date())]
binh_thuong = ngay_khac.groupby(ngay_khac.ts.dt.date)[COT].mean().median()
hang = []
for dv in DICH_VU:
h = {"service": dv}
for chi_so, ten in [("cpu_pct", "CPU vượt (% 1 lõi)"), ("mem_mib", "RAM vượt (MiB)")]:
c = f"{dv}__{chi_so}"
if c in sv.columns: h[ten] = trong[c].mean() - binh_thuong[c]
h["đĩa vượt (MB/s)"] = sum(trong[f"{dv}__{x}"].mean() - binh_thuong[f"{dv}__{x}"]
for x in ["io_read_mbs", "io_write_mbs"] if f"{dv}__{x}" in sv.columns)
hang.append(h)
return pd.DataFrame(hang).set_index("service").round(2), a, b
# ✏️ SỬA: chọn sự cố để truy (E1, E2, E3, E4, E5, E6b, E7)
SU_CO_TRUY = "E2"
bang_vuot, tu, den = vuot_muc(SU_CO_TRUY)
print(f"{SU_CO_TRUY}: {tu:%d/%m %H:%M} → {den:%H:%M} | sổ ghi: {su_co.set_index('id').loc[SU_CO_TRUY, 'title']}")
print(bang_vuot.sort_values("CPU vượt (% 1 lõi)", ascending=False).head(5).to_string())
fig, truc = plt.subplots(1, 3, figsize=(16, 4))
for t, cot, mau in zip(truc, bang_vuot.columns, ["#2563EB", "#7C3AED", "#D97706"]):
top = bang_vuot[cot].sort_values(ascending=False).head(6)[::-1]
t.barh(top.index, top.values, color=[("#DC2626" if v > 0 else "#9CA3AF") for v in top.values]); t.set_title(cot); t.grid(alpha=0.3, axis="x")
plt.suptitle(f"{SU_CO_TRUY}: service nào VƯỢT mức bình thường của chính nó?"); plt.tight_layout(); plt.show()In [ ]:
# 8.2 Nhìn tận mắt: CPU từng service quanh sự cố (cộng dồn)
khung_xem = sv[(sv.ts >= tu - pd.Timedelta("2h")) & (sv.ts <= den + pd.Timedelta("2h"))].set_index("ts")
cpu = khung_xem[[f"{dv}__cpu_pct" for dv in DICH_VU if f"{dv}__cpu_pct" in sv.columns]].clip(lower=0).fillna(0)
cpu.columns = [c.split("__")[0].replace("_host_outside_containers", "ngoài container (chạy thẳng trên máy)") for c in cpu.columns]
# (tên bắt đầu bằng "_" bị matplotlib giấu khỏi chú thích, nên đổi tên cho dễ đọc)
top6 = cpu.mean().sort_values(ascending=False).head(6).index
ve = cpu[top6].copy(); ve["(các service khác)"] = cpu.drop(columns=top6).sum(axis=1)
ve = ve.rolling(5, min_periods=1).mean() # làm mượt 5 phút cho dễ nhìn
fig, t = plt.subplots(figsize=(15, 4.5))
t.stackplot(ve.index, ve.T.values, labels=ve.columns, alpha=0.85)
t.axvspan(tu, den, color="#DC2626", alpha=0.12, label=f"{SU_CO_TRUY} trong sổ")
t.set_ylabel("CPU (% của 1 lõi, cộng dồn)"); t.legend(loc="upper left", fontsize=8); t.grid(alpha=0.3)
t.xaxis.set_major_formatter(mdates.DateFormatter("%d/%m %H:%M", tz=sv.ts.dt.tz)); t.set_title("Ai đang 'ăn' CPU trước, trong và sau sự cố?")
plt.show()In [ ]:
# 8.3 Lúc đó có lịch chạy gì? + tiến trình nào ghi đĩa nhiều (ảnh chụp khi iowait ≥ 15%)
import re
MAU_CRON = re.compile(r"^(\*|\d+(-\d+)?)(/\d+)?(,(\*|\d+(-\d+)?)(/\d+)?)*$") # một ô cron hợp lệ
THU = {"Mon": 0, "Tue": 1, "Wed": 2, "Thu": 3, "Fri": 4, "Sat": 5, "Sun": 6}
def khop(o, v, lo, hi):
# o là một ô lịch (vd "*/6", "0,12", "1-5"); v có nằm trong đó không?
if o == "*": return True
for phan in o.split(","):
buoc = 1
if "/" in phan: phan, buoc = phan.split("/"); buoc = int(buoc)
day = range(lo, hi + 1) if phan == "*" else (range(int(phan.split("-")[0]), int(phan.split("-")[1]) + 1) if "-" in phan else [int(phan)])
if v in list(day)[::buoc]: return True
return False
def doc_lich(bieu_thuc):
# trả về danh sách hàm t -> True/False; None nếu không đọc được (lịch "sau khi khởi động", hoặc viết sai)
luat = []
for e in bieu_thuc.split(";"):
f = e.split()
if len(f) == 5 and all(MAU_CRON.match(x) for x in f): # cron: phút giờ ngày tháng thứ
luat.append(lambda t, f=f: khop(f[0], t.minute, 0, 59) and khop(f[1], t.hour, 0, 23) and khop(f[2], t.day, 1, 31)
and khop(f[3], t.month, 1, 12) and khop(f[4], (t.dayofweek + 1) % 7, 0, 6))
elif len(f) >= 2 and f[-2].endswith("*-*-*") and f[-1].count(":") >= 1: # systemd: [Thứ] *-*-* giờ:phút
gio, phut = f[-1].split(":")[:2]; thu = THU.get(f[0])
if not (MAU_CRON.match(gio) and MAU_CRON.match(phut)): return None
luat.append(lambda t, gio=gio, phut=phut, thu=thu: (thu is None or t.dayofweek == thu) and khop(gio, t.hour, 0, 23) and khop(phut, t.minute, 0, 59))
else:
return None
return luat
def viec_trong(a, b):
ds = []
for _, j in lich[lich.active == "YES"].iterrows():
L = doc_lich(j.schedule_expr)
if L is None: continue
luc = next((t for t in pd.date_range(a.floor("min"), b, freq="1min") if any(f(t) for f in L)), None)
if luc is not None: ds.append({"giờ chạy": luc.strftime("%H:%M"), "việc": j.job, "làm gì": j.what_it_does})
return pd.DataFrame(ds).sort_values("giờ chạy") if ds else pd.DataFrame()
khong_doc_duoc = [(j.job, j.schedule_expr) for _, j in lich.iterrows() if doc_lich(j.schedule_expr) is None]
print(f"Lịch không đọc được ({len(khong_doc_duoc)}):", khong_doc_duoc)
viec = viec_trong(tu - pd.Timedelta("5min"), den)
print(f"{len(viec)} việc có lịch chạy trong {SU_CO_TRUY} (từ 5 phút trước):")
print(viec.head(12).to_string(index=False))
chup = anh_chup[(anh_chup.ts >= tu) & (anh_chup.ts <= den)]
if len(chup):
tien_trinh = (chup.assign(dia_kb_s=chup.kb_read_s.fillna(0) + chup.kb_write_s.fillna(0)).groupby("process")
.dia_kb_s.agg(["mean", "count"]).sort_values("mean", ascending=False).head(6).round(0))
print(f"\n{len(chup)} dòng ảnh chụp tiến trình trong {SU_CO_TRUY}; tiến trình ghi/đọc đĩa nhiều nhất (KB/s trung bình, số lần thấy):")
print(tien_trinh.to_string())
else:
print(f"\nKhông có ảnh chụp tiến trình nào trong {SU_CO_TRUY} (iowait chưa lên 15%, hoặc giám sát bị 'mù').")In [ ]:
# 8.4 Truy cả 7 sự cố một lượt: nghi phạm số 1 theo CPU, RAM, đĩa — đặt cạnh lời ghi trong sổ
tong = []
for ma in ["E1", "E2", "E3", "E4", "E5", "E6b", "E7"]:
b_, a_, z_ = vuot_muc(ma)
top = lambda c: f"{b_[c].idxmax()} (+{b_[c].max():.1f})" if b_[c].max() > 0.05 else "không service nào vượt"
tong.append({"sự cố": ma, "mức chắc": su_co.set_index("id").loc[ma, "status"], "CPU vượt nhất": top("CPU vượt (% 1 lõi)"),
"RAM vượt nhất": top("RAM vượt (MiB)"), "đĩa vượt nhất": top("đĩa vượt (MB/s)"),
"sổ ghi": su_co.set_index("id").loc[ma, "title"][:55]})
bang_tong_dv = pd.DataFrame(tong)
# 📦 bang_tong_dv = mỗi sự cố 1 dòng: service vượt mức nhất theo 3 chỉ số + lời ghi trong sổ
print(bang_tong_dv.to_string(index=False))In [ ]:
# 8.5a Điểm z "bền": mỗi service vượt bao nhiêu lần "độ dao động thường ngày" CỦA CHÍNH NÓ
CHI_SO_DV = {"cpu_pct": "CPU", "mem_mib": "RAM", "io_mbs": "đĩa"}
# ✏️ SỬA: SAN = độ dao động tối thiểu của từng chỉ số (tránh chia cho số gần 0 khi service gần như đứng yên)
SAN = {"cpu_pct": 0.5, "mem_mib": 10, "io_mbs": 0.05}
# ✏️ SỬA: NGUONG_Z = vượt bao nhiêu lần độ dao động thường ngày thì coi là "bất thường"
NGUONG_Z = 3
# gộp đọc + ghi đĩa thành 1 cột "<service>__io_mbs" (min_count=1: cả 2 cột trống thì kết quả vẫn là trống)
dia = {f"{dv}__io_mbs": sv[[f"{dv}__{x}" for x in ["io_read_mbs", "io_write_mbs"] if f"{dv}__{x}" in sv.columns]].sum(axis=1, min_count=1)
for dv in DICH_VU if f"{dv}__io_read_mbs" in sv.columns or f"{dv}__io_write_mbs" in sv.columns}
sv = pd.concat([sv.drop(columns=[c for c in dia if c in sv.columns]), pd.DataFrame(dia)], axis=1) # chạy lại ô này cũng không bị trùng cột
COT_Z = [f"{dv}__{cs}" for dv in DICH_VU for cs in CHI_SO_DV if f"{dv}__{cs}" in sv.columns]
SAN_COT = pd.Series({c: SAN[c.split("__")[1]] for c in COT_Z}) # sàn của từng cột, theo loại chỉ số
PHUT = sv.ts.dt.hour * 60 + sv.ts.dt.minute # phút trong ngày của từng dòng (0..1439)
NGAY = sv.ts.dt.date # ngày của từng dòng
# 📦 COT_Z = các cột "<service>__cpu_pct / mem_mib / io_mbs" · SAN_COT = sàn cho từng cột đó
print(f"{len(COT_Z)} cột để chấm z, ví dụ: {COT_Z[:3]}")
def diem_z(a, b):
# z = (trung bình trong [a, b] − trung vị các ngày khác cùng giờ) / max(1,4826 × MAD, sàn)
trong = sv.loc[(sv.ts >= a) & (sv.ts <= b), COT_Z].mean()
cung_gio = (PHUT >= a.hour * 60 + a.minute) & (PHUT <= b.hour * 60 + b.minute) & (NGAY != a.date())
moi_ngay = sv.loc[cung_gio, COT_Z].groupby(NGAY[cung_gio]).mean() # mỗi ngày khác: 1 dòng trung bình cùng khung giờ
trung_vi = moi_ngay.median()
mad = (moi_ngay - trung_vi).abs().median()
z = (trong - trung_vi) / np.maximum(1.4826 * mad, SAN_COT)
z[moi_ngay.notna().sum() < 5] = np.nan # ít hơn 5 ngày để so (vd container đã dừng) -> không chấm
bang = pd.DataFrame({ten: [z.get(f"{dv}__{cs}", np.nan) for dv in DICH_VU] for cs, ten in CHI_SO_DV.items()}, index=DICH_VU)
bang.index.name = "service"
bang["z lớn nhất"] = bang.max(axis=1) # chỉ số nào lệch nhiều nhất của service đó
return bang.round(1)
bang_z = diem_z(tu, den)
# hạng 1 = vượt nhiều nhất; so hạng theo số thô (8.1) với hạng theo z
bang_z["hạng theo số thô (CPU, 8.1)"] = bang_vuot["CPU vượt (% 1 lõi)"].rank(ascending=False, method="min").astype("Int64")
bang_z["hạng theo z"] = bang_z["z lớn nhất"].rank(ascending=False, method="min").astype("Int64")
# 📦 bang_z = mỗi service 1 dòng: z của CPU, RAM, đĩa, z lớn nhất, và 2 cách xếp hạng
print(f"{SU_CO_TRUY}: 8 service có z lớn nhất (z ≥ {NGUONG_Z} = bất thường so với chính nó)")
print(bang_z.sort_values("z lớn nhất", ascending=False).head(8).to_string())
print(f"Số service có z ≥ {NGUONG_Z}: {int((bang_z['z lớn nhất'] >= NGUONG_Z).sum())} / {len(bang_z)}")
ten_de_doc = lambda s: s.replace("_host_outside_containers", "ngoài container (chạy thẳng trên máy)")
fig, truc = plt.subplots(1, 2, figsize=(16, 4.2))
tho = bang_vuot["CPU vượt (% 1 lõi)"].sort_values(ascending=False).head(8)[::-1]
truc[0].barh([ten_de_doc(s) for s in tho.index], tho.values, color="#9CA3AF"); truc[0].set_title("Theo số thô: CPU vượt (% một lõi)")
zz = bang_z["z lớn nhất"].sort_values(ascending=False).head(8)[::-1]
truc[1].barh([ten_de_doc(s) for s in zz.index], zz.values, color=[("#DC2626" if v >= NGUONG_Z else "#2563EB") for v in zz.values])
truc[1].axvline(NGUONG_Z, color="#DC2626", ls="--"); truc[1].set_title("Theo z lớn nhất (đỏ = vượt NGUONG_Z)")
for t in truc: t.grid(alpha=0.3, axis="x")
plt.suptitle(f"{SU_CO_TRUY}: số thô và điểm z xếp hạng service có giống nhau không?"); plt.tight_layout(); plt.show()In [ ]:
# 8.5b Ai lên trước? Xét từng phút quanh sự cố
# ✏️ SỬA: TRUOC_PHUT = nhìn sớm bao nhiêu phút trước lúc sổ ghi · KEO_DAI = phải vượt liền bao nhiêu phút mới tính
TRUOC_PHUT = 30
KEO_DAI = 3
MUOT = sv[COT_Z].rolling(5, min_periods=3).mean() # trung bình trượt 5 phút cho từng cột
# 📦 MUOT = cùng hình dạng với sv[COT_Z] (mỗi phút 1 dòng), nhưng đã làm mượt
def ai_len_truoc(a, b):
t0 = a - pd.Timedelta(minutes=TRUOC_PHUT)
cung_gio = (PHUT >= t0.hour * 60 + t0.minute) & (PHUT <= b.hour * 60 + b.minute)
khac = MUOT[cung_gio & (NGAY != a.date())] # cùng khung giờ, các ngày khác
k = PHUT[khac.index]
trung_vi = khac.groupby(k).median() # nền của TỪNG phút trong ngày
mad = (khac - trung_vi.loc[k].values).abs().groupby(k).median()
m = (sv.ts >= t0) & (sv.ts <= b)
kt = PHUT[m]
z = (MUOT[m] - trung_vi.reindex(kt).values) / np.maximum(1.4826 * mad.reindex(kt).values, SAN_COT.values)
hang, vuot_dv = [], {}
for dv in DICH_VU:
zd = z[[c for c in COT_Z if c.startswith(dv + "__")]].max(axis=1) # z lớn nhất của service này, từng phút
vuot = zd >= NGUONG_Z
du_dai = vuot.rolling(KEO_DAI).sum() >= KEO_DAI # True khi đã vượt liền KEO_DAI phút
vuot_dv[dv] = vuot
if du_dai.any():
dau = vuot.index[vuot.index.get_loc(du_dai.idxmax()) - KEO_DAI + 1] # phút ĐẦU của chuỗi vượt đầu tiên
hang.append({"service": dv, "lúc": sv.ts[dau], "so với sổ (phút)": round((sv.ts[dau] - a).total_seconds() / 60),
"số phút vượt": int(vuot.sum())})
bang = pd.DataFrame(hang, columns=["service", "lúc", "so với sổ (phút)", "số phút vượt"]).sort_values("lúc")
return bang, pd.DataFrame(vuot_dv).set_index(pd.DatetimeIndex(sv.ts[m]))
len_truoc, vuot_tung_phut = ai_len_truoc(tu, den)
# 📦 len_truoc = mỗi service có vượt 1 dòng: lúc bắt đầu vượt, sớm/muộn bao nhiêu phút so với sổ, tổng số phút vượt
print(f"{SU_CO_TRUY}: sổ ghi bắt đầu {tu:%d/%m %H:%M}. Thứ tự service bắt đầu vượt (âm = sớm hơn sổ):")
print(len_truoc.assign(lúc=len_truoc["lúc"].dt.strftime("%d/%m %H:%M")).to_string(index=False))
fig, t = plt.subplots(figsize=(15, 0.6 * max(len(len_truoc), 2) + 1.5))
for y, (_, r) in enumerate(len_truoc.iloc[::-1].iterrows()):
phut_vuot = vuot_tung_phut.index[vuot_tung_phut[r.service].values]
t.scatter(phut_vuot, [y] * len(phut_vuot), marker="|", s=120, color="#2563EB")
t.scatter([r["lúc"]], [y], s=90, color="#DC2626", zorder=3)
t.set_yticks(range(len(len_truoc))); t.set_yticklabels([ten_de_doc(s) for s in len_truoc.service[::-1]])
t.axvspan(tu, den, color="#DC2626", alpha=0.12); t.axvline(tu, color="black", lw=1)
t.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M", tz=sv.ts.dt.tz)); t.grid(alpha=0.3)
t.set_title(f"{SU_CO_TRUY}: ai bắt đầu vượt trước? (chấm đỏ = lúc bắt đầu vượt)"); plt.tight_layout(); plt.show()In [ ]:
# 8.5c Bảng nghi phạm 3 bằng chứng
TEN_VIEC = set(lich.job)
def tien_trinh_thuoc(p):
# tên tiến trình -> service, CHỈ khi suy ra được từ chính dữ liệu
if p in TEN_VIEC: return "_host_outside_containers" # trùng tên một việc cron/timer của máy chủ -> chạy thẳng trên máy
if p in DICH_VU: return p # trùng đúng tên một container
return "không xác định"
ban_do = pd.Series({p: tien_trinh_thuoc(p) for p in anh_chup.process.unique()})
# 📦 ban_do = mỗi tên tiến trình (trong ảnh chụp) -> service, hoặc "không xác định"
print(f"{len(ban_do)} tên tiến trình -> service:", ban_do.value_counts().to_dict())
print("Ví dụ 'không xác định':", ", ".join(ban_do[ban_do == "không xác định"].index[:10]))
def bang_nghi_pham(a, b):
z = diem_z(a, b)["z lớn nhất"]
co_viec = len(viec_trong(a - pd.Timedelta("5min"), b)) > 0 # có việc cron/timer nào chạy không (8.3)
chup = anh_chup[(anh_chup.ts >= a) & (anh_chup.ts <= b)]
thay = chup.process.map(tien_trinh_thuoc).value_counts() # mỗi service bị chụp bao nhiêu lần
hang = []
for dv in DICH_VU:
b1 = bool(z.get(dv) >= NGUONG_Z)
b2 = co_viec if dv == "_host_outside_containers" else None # None = không xác định
b3 = int(thay.get(dv, 0))
so = int(b1) + int(bool(b2)) + int(b3 > 0)
if so:
hang.append({"service": dv, "(i) z lớn nhất": z.get(dv), "(ii) việc định kỳ": {True: "có", False: "không", None: "không xác định"}[b2],
"(iii) số lần bị chụp": b3, "số bằng chứng": so, "kết luận": "NGHI PHẠM" if so >= 2 else "chưa đủ"})
return pd.DataFrame(hang, columns=["service", "(i) z lớn nhất", "(ii) việc định kỳ", "(iii) số lần bị chụp", "số bằng chứng", "kết luận"]
).sort_values(["số bằng chứng", "(i) z lớn nhất"], ascending=False)
print()
print(f"{SU_CO_TRUY}: các service có ít nhất 1 bằng chứng")
print(bang_nghi_pham(tu, den).to_string(index=False))
# Cả 7 sự cố một lượt
tong_nghi = []
for ma in ["E1", "E2", "E3", "E4", "E5", "E6b", "E7"]:
r = su_co.set_index("id").loc[ma]
a_, b_ = r.bat_dau, max(r.ket_thuc, r.bat_dau + pd.Timedelta("10min"))
bn = bang_nghi_pham(a_, b_)
nghi = bn[bn["kết luận"] == "NGHI PHẠM"]
tong_nghi.append({"sự cố": ma, "z lớn nhất": diem_z(a_, b_)["z lớn nhất"].max(),
"nghi phạm (≥ 2/3)": ", ".join(f"{s} ({n}/3)" for s, n in zip(nghi.service, nghi["số bằng chứng"])) or "chưa đủ bằng chứng",
"sổ ghi": r.title[:50]})
bang_7 = pd.DataFrame(tong_nghi)
# 📦 bang_7 = mỗi sự cố 1 dòng: z lớn nhất trong mọi service, các nghi phạm đủ 2/3, lời ghi trong sổ
print()
print(bang_7.to_string(index=False))In [ ]:
# 8.5d (a) Chạy mù trên E2: chỉ dùng giờ, không đọc lời ghi
r = su_co.set_index("id").loc["E2"]
mu = bang_nghi_pham(r.bat_dau, r.ket_thuc)
print("E2 chạy mù — nghi phạm (≥ 2/3):", mu[mu["kết luận"] == "NGHI PHẠM"][["service", "(i) z lớn nhất", "số bằng chứng"]].to_dict("records"))
print("Sổ ghi (chỉ mở ra SAU khi chạy):", r.title)
# 8.5d (b) Giả dược: khoảng cùng độ dài, ở lúc bình thường
# ✏️ SỬA: SO_CUA_SO = số khoảng giả cho mỗi sự cố · HAT_GIONG = đổi số này để bốc thăm bộ khoảng khác
SO_CUA_SO = 5
HAT_GIONG = 0
ngau_nhien = np.random.default_rng(HAT_GIONG)
ban = [(x.bat_dau - pd.Timedelta("2h"), x.ket_thuc + pd.Timedelta("2h")) for _, x in su_co.iterrows()] # vùng cấm quanh sự cố
gia = []
for ma in bang_7["sự cố"]:
x = su_co.set_index("id").loc[ma]
dai = max(x.ket_thuc - x.bat_dau, pd.Timedelta("10min"))
so_phut = int((sv.ts.max() - sv.ts.min() - dai).total_seconds() // 60)
duoc = 0
while duoc < SO_CUA_SO:
s = (sv.ts.min() + pd.Timedelta(minutes=int(ngau_nhien.integers(0, so_phut)))).floor("min"); e = s + dai
if s.date() != e.date() or any(s <= y2 and e >= y1 for y1, y2 in ban): continue # không qua nửa đêm, không chạm sự cố
bn = bang_nghi_pham(s, e); z = diem_z(s, e)["z lớn nhất"]
nghi = bn[bn["kết luận"] == "NGHI PHẠM"]
gia.append({"dài như": ma, "bắt đầu": s.strftime("%d/%m %H:%M"), "z lớn nhất": z.max(), "service z lớn nhất": z.idxmax(),
"có z ≥ NGUONG_Z": bool(z.max() >= NGUONG_Z), "nghi phạm (≥ 2/3)": ", ".join(nghi.service) or "-"})
duoc += 1
bang_gia = pd.DataFrame(gia)
# 📦 bang_gia = mỗi khoảng giả dược 1 dòng: z lớn nhất, service đó, có bị gọi tên nghi phạm không
print()
print(bang_gia.to_string(index=False))
print()
print(f"Giả dược: {bang_gia['có z ≥ NGUONG_Z'].mean():.0%} số khoảng bình thường có service z ≥ {NGUONG_Z}; "
f"{(bang_gia['nghi phạm (≥ 2/3)'] != '-').mean():.0%} số khoảng bị gọi tên nghi phạm (≥ 2/3)")
print("Service bị nghi oan nhiều nhất:", bang_gia.loc[bang_gia["nghi phạm (≥ 2/3)"] != "-", "nghi phạm (≥ 2/3)"].value_counts().to_dict())
# 8.5d (c) Ngưỡng z: sự cố thật vs giả dược
nguong = pd.DataFrame([{"ngưỡng z": k, "sự cố thật có z ≥ ngưỡng": f"{(bang_7['z lớn nhất'] >= k).sum()}/{len(bang_7)}",
"giả dược có z ≥ ngưỡng": f"{(bang_gia['z lớn nhất'] >= k).sum()}/{len(bang_gia)}"} for k in [3, 5, 10]])
print()
print(nguong.to_string(index=False))
fig, t = plt.subplots(figsize=(12, 3.2))
t.scatter(bang_7["z lớn nhất"].clip(lower=0.1), [1] * len(bang_7), s=80, color="#DC2626", label="sự cố thật")
for _, x in bang_7.iterrows(): t.annotate(x["sự cố"], (max(x["z lớn nhất"], 0.1), 1), xytext=(0, 8), textcoords="offset points", fontsize=8, ha="center")
t.scatter(bang_gia["z lớn nhất"].clip(lower=0.1), [0] * len(bang_gia), s=50, color="#6B7280", alpha=0.7, label="giả dược (lúc bình thường)")
t.axvline(NGUONG_Z, color="#DC2626", ls="--", label=f"NGUONG_Z = {NGUONG_Z}")
t.set_xscale("log"); t.set_yticks([0, 1]); t.set_yticklabels(["giả dược", "sự cố thật"]); t.set_ylim(-0.6, 1.6)
t.set_xlabel("z lớn nhất trong khoảng (thang log)"); t.grid(alpha=0.3, axis="x"); t.legend(loc="lower right", fontsize=8)
t.set_title("Sự cố thật và lúc bình thường: ngưỡng nào tách được hai hàng?"); plt.tight_layout(); plt.show()In [ ]:
# 9.4 Tự chấm mức giám sát
# ✏️ SỬA: True = hệ thống của bạn CÓ điều này, False = chưa có (giá trị sẵn = máy chủ DietPi trong bài)
CO = {
"1a. Số liệu cả máy (CPU, RAM, đĩa, mạng), đo ≤ 30 giây": True,
"1b. Giữ số liệu ≥ 30 ngày": True,
"2. Số liệu theo từng container": True,
"3a. Số liệu LIÊN TỤC theo từng tiến trình / service systemd": False,
"3b. Ảnh chụp tiến trình theo cả CPU lẫn đĩa": False,
"4a. Lịch việc định kỳ + sổ thay đổi (ai làm gì, lúc nào)": True,
"4b. Log tập trung giữ ≥ 30 ngày": False,
"4c. Cảnh báo khi chính giám sát bị 'mù' (không hỏi được máy)": False,
}
# mức = bậc thang: phải đủ MỌI điều của các mức dưới
CAN = {1: ["1a", "1b"], 2: ["2."], 3: ["3a", "3b"], 4: ["4a", "4b", "4c"]}
DUOC = {0: "chưa nói được gì chắc chắn về sức khoẻ máy chủ",
1: "LÚC NÀO máy chủ bất thường (Phần 1–6)",
2: "lúc nào + CONTAINER nào vượt mức (như E2 → cowrie)",
3: "lúc nào + gọi tên cả chương trình chạy thẳng trên máy",
4: "lúc nào + ai + VÌ SAO — đủ để xác nhận nguyên nhân"}
muc = 0
for m in [1, 2, 3, 4]:
if all(gt for ten, gt in CO.items() if any(ten.startswith(ma) for ma in CAN[m])):
muc = m
else:
break
for ten, gt in CO.items():
print("✅" if gt else "❌", ten)
print()
print(f"→ Mức {muc}: bạn nói được: {DUOC[muc]}")
if muc < 4:
thieu = [ten for ten, gt in CO.items() if not gt and any(ten.startswith(ma) for ma in CAN[muc + 1])]
print(f"→ Muốn lên Mức {muc + 1}, cần thêm:", "; ".join(thieu))

