How I matched 150,000 football players
· Mason Walters
I wanted to know which college football programs turn their recruits into NFL players more often than the recruits’ ratings predicted. That analysis is on my personal site: Which college football programs develop players?
The score turned out to be the easy part. The hard part was knowing who’s who. A player shows up in four places: a recruiting record, a college roster, the draft, and the NFL’s records. They don’t share one ID, and names repeat. Get the matching wrong and a program gets credit for someone else’s player.
So before any ranking, I built a data lake that matches every record to one person. As of October 2026 it holds 154,586 players. Here’s each problem I hit, what I did about it, and what happened.
CollegeFootballData (CFBD) supplies the recruits, rosters and draft; nflverse supplies the NFL side.
Load everything before matching anything
Problem. I built the matching on four schools first. That’s a good way to start, but players transfer. A recruit who signed with one of my four schools and played somewhere else had nowhere to land.
Technique. I loaded every team from CFBD: 135,063 roster IDs, 77,522 recruits, 18,906 transfer portal entries and 5,113 draft picks. Then I matched all of it in one run, which took 26 minutes and 14.1 GB of memory.
Result. 157,917 players, before any research. 15,077 of them have roster seasons at two or more schools. I keep 150 pairs I’ve labelled by hand. On those, the run made no wrong matches and missed one.
The full load also found a bug of mine. Two of my transfer decisions had stopped working. The ID links tied those recruits to their first school before my decisions were ever applied. My decisions now apply first.
IDs first, scores second, and a high bar
Problem. CFBD links some recruits to their roster records by ID, but not all of them.
Technique. I take the IDs wherever they exist and the names agree. Where they don’t, I score each candidate pair on what the two records share: name, schools, timing, position, height, hometown and high school. A pair links automatically only when it clears a high line and clearly beats the next candidate. Pairs in the band just below, or tied, stay apart as “hard pairs”, along with ID links whose names don’t agree.
Result. 37,590 recruit-to-roster links by ID, and 35,586 recruit and draft links by score. Across every kind, 8,169 hard pairs were left for a closer look. One cap shows how high the bar is. Take a recruit who signed with a different school from the roster record’s, with no hometown or high school to back the match up. Before the cap, I estimate 100 to 200 pairs like that linked automatically. After it, one did.
Agent research on the pairs the scores couldn’t settle
Problem. 8,169 hard pairs is more than I can check by hand.
Technique. For each hard pair that starts from a recruit, an AI agent researches the two records on the web and answers same, different or can’t tell. Every answer had to quote a page the agent saved, and a script checks the quote is there; an answer that fails is recorded as can’t tell.
Result. 8,823 pairs answered (including pairs the reruns opened): 5,282 same, 2,205 different, 1,336 can’t tell. Hard pairs fell from 8,169 to 1,236, and the player count fell from 157,917 to 152,933 as split records merged. Later I ran a version of the same research, with its own prompt, on the 308 open NFL pairs. 42 are still open. That’s why the player count moves: 157,917 before any research, 152,933 once the research merged split records, then 154,586 after the NFL records joined, because about 1,900 NFL players have no college record in CFBD and the NFL research merged 235 more.
The check proves the quote is on the page, not that it settles the pair. Of 10 random “same” answers I read, eight rested on a high school named in a school bio or roster, and two were thinner.
Checking my own answers, blind
Problem. My decisions outrank the agents’, so the research never asked about a pair I’d already decided. I had no idea whether I’d been wrong.
Technique. I had the agents research all 166 of my decided pairs blind (130 of my hand labels, 16 match reviews and 20 cases from V1): each record alone, with no score and no hint of what I’d said. The answers went into a folder the matching never reads.
Result. The agents agreed with me on 139, couldn’t tell on 26 and disagreed on 1. On that one, they were right. I had merged A.J. Semeatu and Albert Semeatu, two Utah running backs. Utah’s 2015 roster lists both of them, with different hometowns. I changed my answer to “different”.
A research service that waits out usage limits
The research runs on my Claude plan, which has usage limits, so a small service runs it with 10, then 20, workers, waits out each limit and carries on until a pass queues nothing. That took 5 rounds. It also exposed a bug: the limit message changed to “You’ve hit your session limit · resets 1:20am”, my pattern didn’t match it, and every limit cost each worker one of a job’s three attempts. 188 pairs used up all three before I requeued them by hand and fixed the pattern.
Guards that keep two people apart
Problem. A wrong merge is worse than a missed one: one of two careers disappears.
Technique. The first two rules hold even when an agent says “same”; the third only stops an automatic link.
- Two recruit records with different first names stay apart, even when I’m the one who decided they’re the same. Twins can share a class and a high school, like D’Onta and Armanti Foreman.
- A person holds at most one NFL record and one draft pick, unless I decide otherwise.
- Some NFL pairs never link automatically: an NFL record that lists no college, a college player drafted in a different year, or an NFL rookie season while the player is still on a college roster.
Result. The first-name rule holds 94 agent “same” answers apart, AJ and Antoine Cobb among them. Some are probably nicknames. Relaxing the rule is my call, and I haven’t. The one-NFL-record rule went in after a review caught a risk before the NFL research ran: an agent “same” could have put two NFL players in one person.
College names, learned from the draft
Problem. nflverse and CFBD spell schools differently. Mississippi is Ole Miss in CFBD, and Texas-El Paso is UTEP.
Technique. A drafted player’s draft slot is a near-sure link, so each one shows how both sources spell the same school. Slot links still get a name check, and the 8 that failed it were never used. The aliases are learned from the rest. A pair only counts when the names agree and the player lists exactly one college CFBD doesn’t know. That keeps a junior college from being mapped onto a four-year school.
Result. 29 aliases, and I checked every one. Some spellings never get learned, like Grambling State and Nicholls State, and those players can’t link automatically until I add an alias by hand. Of 11,758 NFL players, 9,504 are now matched to a college player.
What the fix did to the ranking
The ranking has had three versions. The first two, V1 and V2, scored how high players were drafted. The current one asks only whether they made the NFL.
Problem. V1 joined records by CFBD IDs only. A rated recruit whose roster record didn’t carry his recruit ID became two people: a recruit with no college career, and an unrated walk-on with one. The same person counted twice.
Result. The lake joins them, and the unrated group fell from 33,000 players to 23,047. In V1, walk-ons looked drafted more often than 2-star recruits, 2.3% against 1.3%, because the walk-on group held split players whose picks were real. With the split players joined, walk-ons are drafted 0.8% of the time and 2-stars 1.9%.
That moved programs in both directions. Programs with a lot of walk-ons rose, because undrafted walk-ons no longer counted so heavily against them. Navy’s walk-on total went from −62.7 to −9.6, in V2’s draft-value points. Some big programs fell. In V1, some of their drafted recruits looked undrafted because the pick landed on the other record. Florida State’s Mario Edwards, a 5-star taken 35th overall, was one. With the picks back, more is expected of every highly rated recruit, and programs that signed many who really went undrafted fell.
These ranks are from V2, so the chart’s “draft value” is how high a program’s players were drafted against what their ratings predicted. The current ranking asks a different question: did they make the NFL at all. V2’s one other change gave undrafted NFL players a little credit, and switching that off moves no program more than 3 places, so nearly every move below comes from matching.
The expectation curve
Problem. A program’s score compares how many of its recruits made the NFL with how many their ratings predicted. The first build of the current version used each star tier’s rate. Star tiers are wide, so I moved to a rating curve fitted separately for each class year. Fitted alone, a year’s top-rated block, a handful of 5-stars, often came out at 100%. A 5-star who made the NFL had been expected to, so he earned his program nothing. 54 5-stars were scored that way. Most 5-stars aren’t NFL-ready when they arrive, and a program that gets them there should get some credit.
Technique. One curve per recruit type (high school, junior college) across all years, shifted for each class year so each year’s expected total matches how many from that year actually made the NFL. The shift keeps the youngest classes, who’ve had less time, from looking like failures. Then I tested the curve on years it never saw: for each class year, fit it on the other eleven and predict the twelfth. I scored the predictions with the Brier score, the average squared error of a probability, where lower is better.
Result. One national rate for everyone scores 0.1018. Each star tier’s rate scores 0.0923. The curve, with nothing from the year it predicts, scores 0.0904. That’s an 11% cut in error against 9% for star tiers. Most of a recruit’s NFL chances still can’t be predicted from his rating, which is why programs are compared on hundreds of recruits.
No rated recruit is expected with certainty now. The highest expectation is 93%. A 5-star who makes the NFL counts for +0.07 to +0.54 of a player, and one who doesn’t counts −0.46 to −0.92: a miss costs more than a make earns, because 5-stars are expected to make it. The order of the programs barely changed. The credit for those 5-stars did.
Reviewers on every change
Problem. The worst bugs in a pipeline don’t fail. The numbers move a little, or a sentence claims a little more than the data, and nobody notices.
Technique. Every change gets a review against the plan before it merges. Before I share results, a final reviewer reads the write-up as a skeptic, checks each claim against the data and tries other ways of computing it.
Result. V2 was meant to change two things, the matching and the credit for undrafted NFL players, and nothing else. A reviewer found that V2 was quietly counting draft picks from before 2012, which V1 had never counted. Nothing errored. I put V1’s window back and rebuilt V2.
The skeptic on the current write-up found four claims that said more than the data did, and I fixed all four before anyone else read it. One was a heading: “The one-off problem is gone.” It meant that no program’s score rests on a few players any more. The evidence: of all the credit a program’s recruits earned by beating their ratings, the share from its three best players had dropped from 53% to 11% for the typical program. But most of that drop comes from the design. When making the NFL counts as one, no player can be worth more than one. V2’s own data with a yes-or-no “drafted” outcome already gives about 21%. Another check was circular. I’d picked the top 25 on the all-time score, then shown that they did well in both halves of the data, but the all-time score is made of those halves. The honest version picks the top 25 on the 2009–2014 classes alone: 20 of them stay positive in the 2015–2020 classes.
What I’d tell someone matching records
Load everything before you trust a match. Take IDs where you have them, set a high bar for everything else, and let the uncertain pairs stay apart. An unmatched pair is a gap you can see. A wrong merge hides. Then have someone check your work, including your own decisions and your own write-up. My worst mistakes here weren’t errors. They were numbers that quietly moved.
Found this useful or want to push back on it? Say hi.