CSAI2026 labs: Lab-2..Lab-6 (Practical + Solution, data), Module-2..Module-6 notebooks and datasets

This commit is contained in:
mikrotik
2026-10-12 19:16:14 +07:00
commit 2fefa615cc
184 changed files with 1016761 additions and 0 deletions
@@ -0,0 +1,271 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "cell-001",
"metadata": {},
"source": [
"# Module 5 — Lab 4: Security Metrics and Error Costs\n",
"\n",
"\n",
"**Topics:** false negatives, class weights, validation threshold, test discipline\n",
"\n",
"> **Solution version:** Run the cells in order, compare the outputs with your work, and read the suggested answers."
]
},
{
"cell_type": "markdown",
"id": "cell-002",
"metadata": {},
"source": [
"## Learning objectives\n",
"\n",
"After completing this lab, you will be able to:\n",
"\n",
"- quantify false-negative and false-positive costs;\n",
"- compare class-weight policies on validation data;\n",
"- select a probability threshold without inspecting the test set;\n",
"- evaluate the chosen policy once on held-out test data;\n",
"- describe the operational trade-off between missed malware and analyst workload."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-003",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"DATA_PATH = Path(\"data/malware_features.csv\")\n",
"\n",
"if not DATA_PATH.exists():\n",
" raise FileNotFoundError(\n",
" \"Missing data/malware_features.csv. Follow data/README.md, place the three \"\n",
" \"official UCI CSV files under data/raw/, and run: python data/prepare_dataset.py\"\n",
" )\n",
"\n",
"\n",
"from sklearn.dummy import DummyClassifier\n",
"from sklearn.impute import SimpleImputer\n",
"from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score, accuracy_score\n",
"from sklearn.model_selection import train_test_split\n",
"from sklearn.pipeline import Pipeline\n",
"from sklearn.tree import DecisionTreeClassifier\n",
"\n",
"\n",
"def security_metrics(y_true, y_pred):\n",
" tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n",
" return {\n",
" \"accuracy\": accuracy_score(y_true, y_pred),\n",
" \"malware_precision\": precision_score(y_true, y_pred, zero_division=0),\n",
" \"malware_recall\": recall_score(y_true, y_pred, zero_division=0),\n",
" \"malware_f1\": f1_score(y_true, y_pred, zero_division=0),\n",
" \"false_positives\": int(fp),\n",
" \"false_negatives\": int(fn),\n",
" \"false_positive_rate\": fp / (fp + tn) if fp + tn else 0.0,\n",
" }"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-004",
"metadata": {},
"outputs": [],
"source": [
"df = pd.read_csv(DATA_PATH, low_memory=False)\n",
"feature_cols = [column for column in df.columns if column not in {\"source\", \"label\"}]\n",
"X = df[feature_cols].apply(pd.to_numeric, errors=\"coerce\")\n",
"y = df[\"label\"].astype(int)\n",
"\n",
"X_train_full, X_test, y_train_full, y_test = train_test_split(\n",
" X, y, test_size=0.20, stratify=y, random_state=42\n",
")\n",
"X_train, X_valid, y_train, y_valid = train_test_split(\n",
" X_train_full, y_train_full, test_size=0.25, stratify=y_train_full, random_state=42\n",
")\n",
"print(\"Train/validation/test:\", len(X_train), len(X_valid), len(X_test))"
]
},
{
"cell_type": "markdown",
"id": "cell-005",
"metadata": {},
"source": [
"## 1. Compare class-weight policies on validation data\n",
"\n",
"Weighting malware errors more heavily may reduce false negatives, but it can increase false positives. The correct choice depends on the cost of missed malware and available analyst capacity."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-006",
"metadata": {},
"outputs": [],
"source": [
"weight_policies = {\n",
" \"unweighted\": None,\n",
" \"malware_x2\": {0: 1, 1: 2},\n",
" \"malware_x4\": {0: 1, 1: 4},\n",
"}\n",
"validation_rows = []\n",
"fitted_candidates = {}\n",
"\n",
"for name, weights in weight_policies.items():\n",
" model = Pipeline([\n",
" (\"imputer\", SimpleImputer(strategy=\"median\", add_indicator=True)),\n",
" (\"tree\", DecisionTreeClassifier(\n",
" max_depth=4, min_samples_leaf=10, class_weight=weights, random_state=42\n",
" )),\n",
" ])\n",
" model.fit(X_train, y_train)\n",
" predictions = model.predict(X_valid)\n",
" validation_rows.append({\"policy\": name, **security_metrics(y_valid, predictions)})\n",
" fitted_candidates[name] = model\n",
"\n",
"validation_results = pd.DataFrame(validation_rows).set_index(\"policy\")\n",
"display(validation_results.round(4))\n",
"selected_policy = validation_results.sort_values(\n",
" [\"malware_recall\", \"false_positive_rate\", \"malware_f1\"],\n",
" ascending=[False, True, False],\n",
").index[0]\n",
"selected_model = fitted_candidates[selected_policy]\n",
"print(\"Selected class-weight policy:\", selected_policy)"
]
},
{
"cell_type": "markdown",
"id": "cell-007",
"metadata": {},
"source": [
"## 2. Select a validation threshold\n",
"\n",
"Search thresholds on validation probabilities only. Prefer thresholds that achieve at least 98% validation malware recall; among them, choose the one with the lowest false-positive rate."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-008",
"metadata": {},
"outputs": [],
"source": [
"validation_probabilities = selected_model.predict_proba(X_valid)[:, 1]\n",
"threshold_rows = []\n",
"for threshold in np.arange(0.20, 0.81, 0.05):\n",
" predictions = (validation_probabilities >= threshold).astype(int)\n",
" threshold_rows.append({\"threshold\": round(float(threshold), 2), **security_metrics(y_valid, predictions)})\n",
"\n",
"threshold_results = pd.DataFrame(threshold_rows).set_index(\"threshold\")\n",
"eligible = threshold_results[threshold_results[\"malware_recall\"] >= 0.98]\n",
"pool = eligible if not eligible.empty else threshold_results.loc[[threshold_results[\"malware_recall\"].idxmax()]]\n",
"selected_threshold = float(pool.sort_values(\n",
" [\"false_positive_rate\", \"malware_f1\"], ascending=[True, False]\n",
").index[0])\n",
"display(threshold_results.round(4))\n",
"print(\"Selected threshold:\", selected_threshold)"
]
},
{
"cell_type": "markdown",
"id": "cell-009",
"metadata": {},
"source": [
"## 3. Refit on all training data and evaluate once on the test set\n",
"\n",
"The test set is used only after all policy decisions are fixed."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-010",
"metadata": {},
"outputs": [],
"source": [
"final_model = Pipeline([\n",
" (\"imputer\", SimpleImputer(strategy=\"median\", add_indicator=True)),\n",
" (\"tree\", DecisionTreeClassifier(\n",
" max_depth=4,\n",
" min_samples_leaf=10,\n",
" class_weight=weight_policies[selected_policy],\n",
" random_state=42,\n",
" )),\n",
"])\n",
"final_model.fit(X_train_full, y_train_full)\n",
"test_probabilities = final_model.predict_proba(X_test)[:, 1]\n",
"default_predictions = (test_probabilities >= 0.50).astype(int)\n",
"tuned_predictions = (test_probabilities >= selected_threshold).astype(int)\n",
"\n",
"test_results = pd.DataFrame([\n",
" security_metrics(y_test, default_predictions),\n",
" security_metrics(y_test, tuned_predictions),\n",
"], index=[\"threshold_0.50\", f\"threshold_{selected_threshold:.2f}\"])\n",
"display(test_results.round(4))"
]
},
{
"cell_type": "markdown",
"id": "cell-011",
"metadata": {},
"source": [
"## Self-check"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-012",
"metadata": {},
"outputs": [],
"source": [
"assert set(validation_results.index) == set(weight_policies)\n",
"assert 0.20 <= selected_threshold <= 0.80\n",
"assert len(test_probabilities) == len(y_test)\n",
"assert set(tuned_predictions).issubset({0, 1})\n",
"assert test_results.shape[0] == 2\n",
"assert final_model.named_steps[\"tree\"].get_depth() <= 4\n",
"print(\"✓ Lab 4 completed\")"
]
},
{
"cell_type": "markdown",
"id": "cell-013",
"metadata": {},
"source": [
"## Suggested answers\n",
"\n",
"1. **Why tune on validation data?** Repeatedly choosing settings from test results turns the test set into training information.\n",
"2. **Why prioritize malware recall?** A false negative allows a malicious file to pass undetected. However, maximizing recall without an FPR constraint can overwhelm analysts.\n",
"3. **Is the chosen threshold universal?** No. It depends on prevalence, feature drift, collection methods, and operational costs."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}