Tabular Classification
Scikit-learn
Joblib
phishing-detection
url-classification
binary-classification
phiusiil
knn
scikit-learn
Eval Results (legacy)
Instructions to use feti-ai/fetiai-v1-phiusiil-binclf-knn-skl-500k with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Scikit-learn
How to use feti-ai/fetiai-v1-phiusiil-binclf-knn-skl-500k with Scikit-learn:
from huggingface_hub import hf_hub_download import joblib model = joblib.load( hf_hub_download("feti-ai/fetiai-v1-phiusiil-binclf-knn-skl-500k", "sklearn_model.joblib") ) # only load pickle files from sources you trust # read more about it here https://skops.readthedocs.io/en/stable/persistence.html - Notebooks
- Google Colab
- Kaggle
File size: 16,228 Bytes
d555038 acf2e47 d555038 acf2e47 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 89d02c1 d555038 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | ---
license: mit
pipeline_tag: tabular-classification
library_name: sklearn
datasets:
- feti-ai/phiusiil-if3070-stei-itb-2024-2025-1
tags:
- phishing-detection
- url-classification
- tabular-classification
- binary-classification
- phiusiil
- knn
- scikit-learn
model-index:
- name: fetiai-v1-phiusiil-binclf-knn-skl-500k
results:
- task:
type: tabular-classification
name: Phishing URL Classification
dataset:
type: feti-ai/phiusiil-if3070-stei-itb-2024-2025-1
name: PhiUSIIL Phishing URL Dataset
metrics:
- type: accuracy
value: 0.9807343043338912
name: Accuracy
- type: precision
value: 0.9808955223880597
name: Phishing precision
- type: recall
value: 0.7634758364312267
name: Phishing recall
---
<div align="center">
# fetiai-v1-phiusiil-binclf-knn-skl-500k
**Group 16** · IF3070 Foundations of Artificial Intelligence · STEI ITB
<p>
<img src="https://img.shields.io/badge/course-IF3070-3b5bdb?style=flat-square" alt="IF3070" style="display:inline-block;margin:0;vertical-align:text-bottom" />
<img src="https://img.shields.io/badge/institution-STEI%20ITB-1f2937?style=flat-square" alt="STEI ITB" style="display:inline-block;margin:0;vertical-align:text-bottom" />
<img src="https://img.shields.io/badge/year-2024%2F2025--1-6b7280?style=flat-square" alt="2024/2025-1" style="display:inline-block;margin:0;vertical-align:text-bottom" />
<img src="https://img.shields.io/badge/group-16-3b5bdb?style=flat-square" alt="Group 16" style="display:inline-block;margin:0;vertical-align:text-bottom" />
</p>
<p>
<img src="https://img.shields.io/badge/task-tabular--classification-3b5bdb?style=flat-square" alt="tabular classification" style="display:inline-block;margin:0;vertical-align:text-bottom" />
<img src="https://img.shields.io/badge/parameters-500k-1f2937?style=flat-square" alt="500,002 parameters" style="display:inline-block;margin:0;vertical-align:text-bottom" />
<img src="https://img.shields.io/badge/dataset-PhiUSIIL-6b7280?style=flat-square" alt="PhiUSIIL" style="display:inline-block;margin:0;vertical-align:text-bottom" />
<img src="https://img.shields.io/badge/licence-MIT-3b5bdb?style=flat-square" alt="MIT" style="display:inline-block;margin:0;vertical-align:text-bottom" />
</p>
</div>
**KNN (scikit-learn)** for phishing URL classification, served over HTTP as a single-model API.
```console
$ curl -s localhost:8000/predict -H 'content-type: application/json' -d '{
"features": {"URLLength": 31, "DomainLength": 25, "IsHTTPS": 1, "...": "all 49"}
}'
{
"model": "knn_sklearn",
"label": 1,
"verdict": "legitimate",
"phishing_score": 0.0,
"n_provided": 33,
"n_imputed": 16,
"coverage_ratio": 0.6735,
"low_evidence": false
}
```
> **This is a coursework reimplementation, not a security product.** It is trained on a
> static 2023–24 dataset, has no threat intelligence, no blocklist, and no knowledge of any
> campaign newer than its training data. Do not use it to decide whether a link is safe.
**Algorithm**
`KNN (scikit-learn)` · `Trained on PhiUSIIL` · `SMOTE` · `Feature Engineering`
**Built with**
<p>
<img src=".github/assets/logos/python.svg" width="18" height="18" align="top" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" /> Python
<img src=".github/assets/logos/scikit-learn.svg" width="18" height="18" align="top" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" /> scikit-learn
<img src=".github/assets/logos/numpy-dark.svg" width="18" height="18" align="top" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" /> NumPy
<img src=".github/assets/logos/pandas-dark.svg" width="18" height="18" align="top" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" /> pandas
<img src=".github/assets/logos/scipy.svg" width="18" height="18" align="top" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" /> SciPy
<img src=".github/assets/logos/docker.svg" width="18" height="18" align="top" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" /> Docker
</p>
**Links** — [Full application](https://github.com/fetiai/phishing-url-classifier) ·
[Live demo](https://phiusiil.faizath.com) ·
[Dataset](https://archive.ics.uci.edu/dataset/967/phiusiil+phishing+url+dataset)
---
## What it does
It scores **one pre-extracted feature row** with one model and returns a verdict.
That is the whole scope, and the boundary is deliberate. This service has no page fetcher,
no URL feature extractor and no SSRF guard, because duplicating a network-facing security
control into four repositories is how the four copies drift apart. Turning a URL into the
49 features this API expects is the job of the
[full application](https://github.com/fetiai/phishing-url-classifier), which fetches the
page under a guard, extracts the features, and scores them against all four models at once.
So this is a **tabular** classifier, not a text one: the input is a 49-dimensional feature
vector, and the URL string never reaches the model.
| Route | Purpose |
|---|---|
| `POST /predict` | one row → verdict, score, and how much of the row was real |
| `POST /predict/batch` | up to 1000 rows |
| `GET /metadata` | the feature contract, the demoted features, and this model's metrics |
| `GET /healthz` | liveness |
| `GET /readyz` | ready once the golden-row self-test has passed |
## Quickstart
```bash
make install # venv + pinned dependencies
make selftest # prove the artifact reproduces its recorded prediction
make serve # http://127.0.0.1:8000 (docs at /docs)
```
No training step and no download step: **the model is committed** (2.6 MB under
`model/`), so a fresh clone can serve immediately.
### With Docker
```bash
make docker-build && docker run -p 8000:8000 fetiai-v1-phiusiil-binclf-knn-skl-500k:local
```
The image **bakes the model in** rather than mounting it, so the image tag is a complete
description of what the service will predict. The build fails if the artifact does not
reproduce its own golden row.
## The request contract
Send all 49 feature columns. `null` is allowed and is the expected value for a feature the
caller could not determine — 12 of the 49 are permanently null, having failed the
extraction agreement gate in the parent project.
```jsonc
{
"features": { "URLLength": 31, "DomainLength": 25, "IsHTTPS": 1, /* ...46 more */ },
"url": "https://example.com/login", // optional
"domain": "example.com", // optional
"tld": "com", // optional
"title": "Sign in" // optional
}
```
`GET /metadata` returns the exact 49 names in order. Three of them carry typos that are
preserved on purpose — `NoOfDegitsInURL`, `DegitRatioInURL`, `SpacialCharRatioInURL` —
because those names are what the training data means.
A **missing** feature is rejected by name; a **null** one is imputed. That distinction is
the point: an extractor that dropped a column and an extractor that honestly could not
determine a value are different faults with different fixes.
### Why the response reports coverage
Imputation fills a missing feature from the training distribution, and that distribution is
92.5% legitimate. A mostly-empty row therefore does not produce a *neutral* prediction — it
produces one biased toward **legitimate**, which is exactly the wrong direction for a
phishing detector.
So every response carries `n_provided`, `n_imputed`, `coverage_ratio` and `low_evidence`.
Without them a verdict drawn from six real values looks precisely as confident as one drawn
from all 49.
The optional `url`, `domain`, `tld` and `title` fields are worth sending when you have
them: 21 of the 49 features are derived from the URL string, and supplying it lets those be
**recomputed** rather than imputed.
## Results
Measured on a 28,081-row validation split. **The held-out file shipped with the dataset has
no labels, so there is no test score and none is claimed.**
| Model | Phishing recall | Phishing precision | Accuracy |
|---|---|---|---|
| **KNN (scikit-learn)** | **0.763** | **0.981** | **0.98073** |
**Read that accuracy against 0.9248.** The corpus is 92.48% legitimate, so answering
"legitimate" to everything scores 0.9248 while catching no phishing whatsoever. Accuracy
alone cannot tell a working detector from a constant; phishing recall can.
Class 0 is phishing and is the positive class throughout.
The scikit-learn and from-scratch implementations of this algorithm disagree on
**0.0356%** of the validation split. That number is the reason both exist: a
reimplementation with nothing to check it against is an assertion, not a result. The
counterpart lives in [`fetiai-v1-phiusiil-binclf-knn-scratch-500k`](../fetiai-v1-phiusiil-binclf-knn-scratch-500k).
`model/metrics.json` also carries the `legacy` profile, flagged `"leaky": true`. It
reconstructs the original notebook's configuration, which standardised each split by its
own mean and standard deviation — information no deployed model can have, since there is no
batch to average over when a single row arrives. It is kept as evidence of what the leak
was worth and is never presented as this model's result.
## What is in this repo
| Path | What it is |
|---|---|
| `model/knn_sklearn.joblib` | the trained model (500,002 stored values) |
| `model/fitted_stats.json` | **not optional** — the scaler, imputation values, clip bounds and mode tables |
| `model/manifest.json` | sha256 of every file above, verified at load |
| `model/golden_row.json` | one record with its expected vector and prediction |
| `phiusiil/` | the scoring path: schema, preprocessing, and this one model class |
| `server/` | loader, prediction, HTTP layer |
`fitted_stats.json` deserves the emphasis. The model alone cannot classify anything: it was
fitted on standardised inputs, and the numbers that produce that standardisation live in
that file. Publishing weights without it would be publishing something unusable.
It is plain JSON rather than a pickled transformer on purpose. A pickled estimator arrives
with a `fit` method attached, and the defect this whole pipeline exists to avoid is someone
calling it at serving time. Numbers that cannot be re-fitted cannot leak.
> **The model file is a pickle.** `joblib.load` executes code, so treat it as you would any
> executable, and note that it was produced by **scikit-learn 1.9.0** — loading it under a
> different version is unsupported. It holds a bare scikit-learn estimator rather than a
> wrapper class, so unpickling depends on scikit-learn alone and on nothing defined in this
> repository. If you want a model that loads without executing anything, the from-scratch
> counterpart in [`fetiai-v1-phiusiil-binclf-knn-scratch-500k`](../fetiai-v1-phiusiil-binclf-knn-scratch-500k) is plain a NumPy `.npz` of the reference matrix and its labels.
## Verification
```bash
make selftest # golden row, offline
make test # golden row + HTTP contract + naming
make namecheck # provenance hygiene
```
The check that carries the weight is the **golden row**: one real record, its expected
49-feature vector, and its expected prediction, pushed through the whole path and compared
**exactly**. The vector and the prediction are asserted separately, because a wrong vector
means the preprocessing drifted while a right vector with a wrong label means the model
artifact did — different faults, different fixes.
Equality is bitwise on float32, never a tolerance. A tolerance-based comparison would pass
while a fitted statistic quietly differed, which is the one thing the test exists to catch.
`origin.json` records where every copied file came from, including the parent bundle's own
hashes, so drift is detectable without the parent repository present.
## Licence and data
MIT, as in the parent project. See `LICENSE`.
This model was trained in part on the PhiUSIIL Phishing URL Dataset (Prasad & Chandra), available from the UCI Machine Learning Repository, licensed under CC BY 4.0.
The dataset is the UCI PhiUSIIL Phishing URL Dataset (ID 967).
**This artifact embeds training data.** k-nearest neighbours has no learned parameters —
fitting is memorising — so `model/knn_sklearn.joblib` *is* the 10,000-row scaled reference set that
the classifier searches at prediction time. The dataset's attribution therefore travels with
this model, not only with the dataset.
---
## Team
<div align="center">
<table>
<tr>
<td width="220" align="center" valign="top">
<a href="https://github.com/thalitazhrr">
<img src="https://github.com/thalitazhrr.png?size=140" width="120" height="120" alt="Thalita Zahra Sutejo" style="border-radius:50%" />
</a>
<br /><br />
<b>Thalita Zahra Sutejo</b><br />
18222023
<br /><br />
<a href="https://github.com/thalitazhrr">
<img src=".github/assets/github.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
thalitazhrr
</a>
<br />
<a href="https://www.linkedin.com/in/thalitazahras/">
<img src=".github/assets/linkedin.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
thalitazahras
</a>
</td>
<td width="220" align="center" valign="top">
<a href="https://github.com/IrfanMusthofa">
<img src="https://github.com/IrfanMusthofa.png?size=140" width="120" height="120" alt="Irfan Musthofa" style="border-radius:50%" />
</a>
<br /><br />
<b>Irfan Musthofa</b><br />
18222056
<br /><br />
<a href="https://github.com/IrfanMusthofa">
<img src=".github/assets/github.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
IrfanMusthofa
</a>
<br />
<a href="https://www.linkedin.com/in/irfanmusthofa/">
<img src=".github/assets/linkedin.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
irfanmusthofa
</a>
</td>
<td width="220" align="center" valign="top">
<a href="https://github.com/EleanorCordelia">
<img src="https://github.com/EleanorCordelia.png?size=140" width="120" height="120" alt="Eleanor Cordelia" style="border-radius:50%" />
</a>
<br /><br />
<b>Eleanor Cordelia</b><br />
18222059
<br /><br />
<a href="https://github.com/EleanorCordelia">
<img src=".github/assets/github.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
EleanorCordelia
</a>
<br />
<a href="https://www.linkedin.com/in/eleanorcordelia/">
<img src=".github/assets/linkedin.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
eleanorcordelia
</a>
</td>
<td width="220" align="center" valign="top">
<a href="https://github.com/faizath">
<img src="https://github.com/faizath.png?size=140" width="120" height="120" alt="Muhammad Faiz Atharrahman" style="border-radius:50%" />
</a>
<br /><br />
<b>Muhammad Faiz Atharrahman</b><br />
18222063
<br /><br />
<a href="https://github.com/faizath">
<img src=".github/assets/github.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
faizath
</a>
<br />
<a href="https://www.linkedin.com/in/faizath/">
<img src=".github/assets/linkedin.svg" width="14" height="14" alt="" style="display:inline-block;margin:0;vertical-align:text-bottom" />
faizath
</a>
</td>
</tr>
</table>
</div>
---
<div align="center">
IF3070 Foundations of Artificial Intelligence · STEI ITB · 2024/2025-1
More at **[fetiai.github.io](https://fetiai.github.io/)**
</div>
|