FlyRank “AI n-able Yourself” internship · ML capstone (ML-11)

Which pages should you refresh first? An honest, validated answer.

Abstract

Question. Across a content team's queue of already-published pages, which ones are declining in search performance right now, and which of those are worth a refresh? The two ways of being wrong do not cost the same, and that asymmetry drives everything below.

Data. An anonymized FlyRank content-performance extract, 30,000 rows / 32 clients, filtered to 26,604 scorable pages (31 clients) where decline is arithmetically possible and a position is on record.

Method. A RandomForest ranking model, evaluated under a client-grouped split (never a random one — a random split lets the model learn which client a row belongs to instead of the actual signal), audited against five separate leakage failure modes, and compared against a hand-written CTR-shortfall rule on the same folds.

Headline result. The model reaches 0.645 mean ROC AUC against 0.532 for the rule — a real but modest lift. Under a careless random split the same model reads 0.767; the 0.122-point gap between the two is not free skill, it is measured client memorization.

What this is for. A ranked, reason-coded queue that tells a content editor what to open first — decision-support for a human reviewer, not an automated publishing pipeline, and not a causal claim that refreshing a flagged page will fix its decline.

Introduction

A content team with a few thousand published pages cannot review all of them by hand every sprint. Someone has to decide what gets opened first, and the two ways of being wrong do not cost the same. A page flagged wrongly wastes an editor's time. The mistake nobody notices only surfaces when the client asks — and that one is fatal.

The model can fake a good reading. Given a random split it learns the client roster instead of the signal, and the score looks strong right up until it meets an account it has never seen.

This project asked whether a model trained on the FlyRank content extract can rank that queue better than the obvious rule an editor would write without one — and, just as importantly, whether it can say so honestly: with the validation design that actually earns the number, and the limitations stated before a reader has to find them.

Data

Two data sources were used across this project, for two different jobs, and only one of them produced the numbers below.

Excluded from the scored population, and why: the label and its raw material (never used as features); 1,205 rows with no recorded search position; and 2,191 rows where prior-period impressions were zero, meaning decline is arithmetically impossible for that row rather than a real observation of stability. No client names, URLs, or raw search queries appear anywhere in this dataset — client_id/content_id are opaque hashes.

Methodology

Label and baseline

The label is a same-extract comparison, not a forecast — both halves of the 30-day-vs-prior window sit inside the same 90-day pull. The baseline a human would use without a model: a page is worth reviewing if it earns a smaller share of the clicks its position tier normally delivers, where "normally delivers" is measured per tier (weighted clicks/impressions), not assumed, and only scored once expected clicks clear a floor of 5 — below that floor a real zero-click page and a real underperformer are statistically indistinguishable.

Features and validation design

Two feature sets were built and reused everywhere: permissive (38 columns — everything that is not the label, an identifier, or a rule-derivation column) and strict (34 columns — permissive minus four columns that share a time window with the label's own impression comparison, even though they are not the label itself).

The population spans 31 clients, and one client alone is 26.2% of the rows — a random train/test split would let a model learn which client this is instead of the actual signal. Every result below uses a client-grouped 5-fold split (no client appears in both train and test within a fold) unless stated otherwise.

The leakage audit

Five checks, run against the shipped (permissive) feature set:

TestWhat it checksResult
A — no leaky columnsNone of the four label-derived columns reached the feature listPass
B — label's raw materialAdd the label's own arithmetic back as a feature0.645 → 1.000 (+0.355)
C — grouped vs. random splitDoes the split honesty actually matter here0.645 grouped vs. 0.767 random (+0.122 gap)
D — degenerate-label populationRows where decline is arithmetically impossible, re-included0.645 → 0.686 (+0.041 inflation)
E — window-overlap columnsValue carried by columns sharing the label's time window0.645 permissive vs. 0.611 strict (+0.034)

The number this page stands behind is 0.645 mean ROC AUC, on a client-grouped split, on the 26,604 rows where decline is arithmetically possible. Every other number in the table above is what that headline would look like if one of those controls were skipped.

Results

Same 5 client-grouped folds for every row in the table below:

Mean across 5 client-grouped folds. P@K = precision at the top K ranked rows.
ModelFeaturesROC AUCP@10P@50P@100P@500
Baseline (hand rule)0.5320.7600.8080.7820.746
Logistic Regressionpermissive0.6250.7400.7960.7600.740
Logistic Regressionstrict0.5860.7200.7240.7060.704
RandomForestpermissive0.6450.9200.8440.8420.787
RandomForeststrict0.6110.8400.8680.8120.771
Bar chart comparing mean ROC AUC across 5 client-grouped folds: baseline hand rule 0.532, Logistic Regression 0.625, RandomForest 0.645, against a chance line at 0.5.
RandomForest reaches 0.645 mean AUC across 5 client-grouped folds, versus 0.532 for the Week-4 hand rule — real but modest separation from a coin flip (0.5).

ROC AUC and precision-at-K do not always agree: RandomForest/strict reads slightly higher at the top of the queue (P@50 0.868) than permissive (0.844), even though its overall AUC is lower (0.611 vs 0.645). Both numbers are reported because picking whichever one looks better would be fitting the story to the metric, not the other way around.

Limitations

Reconciling this page against earlier published numbers

Two populations from the same 30,000-row extract have been published from this repo, under two filter orders. Both are correct. Neither previously stated which pipeline produced it, so the figures read as if they disagree.

Same extract, same base rate, two pipelines. Row counts verified against data/raw/content_refresh_anonymized.csv.
Failure-modes tableThis page
Scored population26,61226,604
Filterszero prior impressionsno recorded position, then zero prior impressions
Validationone runtime, single splitmean of 5 client-grouped folds
Feature setnot namedpermissive
RF ROC AUC0.5650.645
RF Precision@500.6000.844
Base rate0.6110.611

The row counts differ by 8 because the two filters overlap. 1,205 rows have no recorded position (avg_position is stored as 0, not as a missing value) and 3,388 have zero prior impressions; 1,197 rows are in both, so 3,396 rows fall to one filter or the other. Applying only the impression filter leaves 26,612; applying both leaves 26,604. The 2,191 figure quoted in the Data section above is the incremental removal after the position filter has already taken 1,197 of them.

The 0.600 comes from unnamed features on a single runtime. The 0.844 comes from five client-grouped folds on the permissive set. The model did not get better between the two — the single-runtime measurement is the weaker one.

Ranked recommendations

The same out-of-fold model scores and rule scores above sort every page into one of four tiers: rule and model in agreement rank highest; rows where the rule is silent but the model is confident are flagged for investigation rather than automatic action; rows the rule flags but the model doubts stay in the queue, because the rule and the model answer different questions (is this page under-clicking its position, versus is it trending down).

Bar chart of the 26,604-page action queue split into four tiers: 5,396 refresh now (20%), 11,276 investigate rule-silent (42%), 1,730 CTR gap only (7%), 8,202 monitor (31%).
42% of the queue is rule-silent rows the model still flags for investigation — the tier this model adds on top of the hand rule.
TierPagesMeaning
1Refresh now5,396Rule and model agree the page is underperforming.
2Investigate11,276Rule is silent; model flags decline (70.2% true-decline rate in this snapshot).
3CTR gap only1,730Rule found a real click gap; model isn't confident it's declining.
4Monitor8,202Neither signal flags a problem right now.

Read this as decision-support, not confirmation. The 70.2% figure is directional evidence the model adds signal the rule misses in this snapshot — not proof any specific refresh will work. This page ships a ranked list and reasons for a human to open; it does not publish or edit anything on its own, and it should never be the sole basis for an automated action. The full human-review checklist and no-go list live in the underlying notebook (linked below).

Reproducibility

To rerun: clone the repo, pip install -r requirements.txt, open work/notebooks/capstone.ipynb, Run All. Every number and chart on this page regenerates from data/raw/content_refresh_anonymized.csv with no external dependency or hidden state.


Acknowledgments & data credit

Built on the FlyRank ML Internship dataset, an anonymized content-performance extract provided for this internship. This page is the ML-11 capstone of AI n-able Yourself — the paper and the notebooks it mirrors, not a tutorial on method. Section 3's methodology critique in the underlying notebook responds constructively to FlyRank's published SEO research paper (March 2026, included in this repo under docs/).