Model card

Logistic Regression (L2, class-balanced) + domain-reputation layer over 34 standardised lexical features. Fitted on 70,917 URLs, evaluated on a held-out 17,730.

0.962
ROC-AUC
90.55%
Accuracy
85.57%
Precision
87.42%
Recall
0.865
F1

Confusion matrix

Held-out test set, threshold 0.50.

10,696
True negative
legit called legit
904
False positive
legit called phishing
771
False negative
phishing missed
5,359
True positive
phishing caught

Learned coefficients

Positive weights push toward phishing, negative toward legitimate. Values are on standardised inputs, so magnitudes are directly comparable.

  • Query string length+3.276
  • Path depth+2.934
  • @ symbols in URL+1.404
  • Query parameters-1.039
  • Filename length+0.922
  • Dots in path+0.816
  • Dots in URL-0.792
  • Ampersands in query-0.743
  • Percent-encoding-0.737
  • Path length-0.625
  • Total URL length+0.586
  • Equals in query+0.514
  • Domain length+0.472
  • TLD inside query+0.414
  • Hyphens in path-0.414
  • Dots in domain-0.401
  • Email address in URL-0.390
  • Known URL shortener+0.369
  • Percent-encoding in path+0.329
  • Underscores in URL-0.327
  • Slashes in URL+0.317
  • Hyphens in domain+0.302
  • Ampersands+0.296
  • Raw IP as host+0.261
  • Equals signs+0.142
  • TLD strings in URL+0.102
  • Hyphens in filename-0.085
  • Question marks+0.084
  • Vowels in domain+0.073
  • Hyphens in URL-0.061
  • Dots in filename+0.043
  • Underscores in domain+0.028
  • @ in domain+0.016
  • 'server'/'client' in domain-0.006

Intended use

  • · A fast first-pass triage signal for links in email, chat and SMS.
  • · Security-awareness training: the feature breakdown shows why a link looks wrong.
  • · Offline / privacy-sensitive contexts where sending URLs to an API is unacceptable.

Limitations

  • · Lexical only — a phishing page on a clean, short domain will score as safe.
  • · Trained on a 2023-era corpus; TLD and shortener fashions drift over time.
  • · Linear model: it cannot represent interactions between features.
  • · Not a replacement for a blocklist, email gateway, or your own judgement.

Training recipe

X = df[FEATURES].astype(float)
y = df["phishing"].astype(int)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42)

pipe = Pipeline([
    ("scale", StandardScaler()),
    ("clf", LogisticRegression(
        penalty="l2", C=1.0, max_iter=2000,
        class_weight="balanced")),
])
pipe.fit(X_train, y_train)

# exported: coef_, intercept_, scaler.mean_, scaler.scale_
#        -> src/data/phishing-model.json