The problem in brief
The data is the history of Kaggle "Featured" competitions from 2010 to 2024: 230 580 rows, one per user and competition, with the final public and private ranks, the number of teams, the number of submissions and the medal. The test lists the users of 5 later competitions (1 900 rows, each with a team id) and asks for a score: how likely is each user to finish in the top 3 % of that competition, that is with ? Only users with at least 4 past competitions and 10 past submissions are in the test, and the history contains only competitions that started before the first test competition.
The data comes from the Kaggle competition archive. The test results are private and we never submitted anything, so all scores below come from our own time-based validation inside the history.
How you are scored
ROC AUC between the scores and the true 0/1 outcome, over all test rows:
for a random user who made the top 3 % and a random user who did not. Only the order of the scores matters.
What to aim for. The organisers' baseline ranks users by their number of past competitions. On the public leaderboard the best entry scores 0.916, and most of the top 10 are between 0.895 and 0.916. Our validation uses different competitions, so the numbers are comparable only roughly.
First look at the data
About 6 % of the rows are top-3 % finishes, more than 3 % because every member of a winning team gets the same rank. Members of one team appear as several rows with exactly the same private rank in the same competition, which lets us rebuild the teams of the history: identifies a team.
The key question is how to validate. The test is a "future" situation: features may only use competitions that started before. A random split of rows would leak the future (a user's 2023 result helps predict their 2021 result). So we copy the test exactly:
- Validation: the last 5 competitions of the history play the role of the test. Features are computed from all competitions that started before the first of them, users are filtered with the same rule (at least 4 competitions and 10 submissions before), and the label is their result.
- Training: we repeat the same construction for every earlier competition since 2014: users of with their features computed from what happened before , labelled with their result in . This gives 105 768 training rows, each one a small replay of the test situation.
A first baseline
| score used to rank users | AUC (last 5 competitions) |
|---|---|
| number of past competitions (organisers' baseline) | 0.542 |
| past top-3 % rate | 0.772 |
| recency-weighted mean of | 0.744 |
Experience alone says almost nothing: many users with dozens of competitions never reach the top. Past success says a lot: the share of past competitions finished in the top 3 % already gives 0.77.
The key idea
Two kinds of signal: what the user did before, and who the user is teaming with now.

History (left panel). The probability of a top-3 % finish climbs steadily with the number of past top-3 % finishes: 3 % for users who never did it, 9 % after one, over 30 % after nine. Skill in competitions is persistent. We describe each user by several views of the past (mean, median and best percentile, recent form over the last 5 competitions, a 2-year exponentially weighted log-percentile, counts of top 3 % and top 10 %, medals, submissions per competition, days since the last competition) and let gradient boosting combine them.
Team (middle panel). The test gives a team id, so we know which test users compete together. In the history, a user who is in a team with other experienced users (users who also pass the filter) ends in the top 3 % in 28 % of cases for a team of two and 64 % for teams of four or more, against 4.5 % for solo users. Strong Kagglers merge with other strong Kagglers when they are near the top of the leaderboard, so a team made of experienced members is itself evidence of a good position. We add the team size (counted among filtered users, exactly as in the test), the best past log-percentile in the team and the total number of past top-3 % finishes in the team. These features are worth 4 points of AUC (0.784 without them, 0.826 with them) and are the most used by the model.
We also add the user's rank within the field of the competition (how strong they are compared with the other filtered users of the same competition), since the top 3 % is a fixed number of places: being strong matters less than being stronger than the others.
The full solution
- Rebuild teams in the history from equal private ranks.
- For each competition since 2014 (training) and for the test, compute user features from competitions that started before , keep users with at least 4 competitions and 10 submissions, and add team and field features inside :
d['team_size'] = tg.UserId.transform('size') # filtered users sharing the team
d['team_best_logpct'] = tg.logpct_mean.transform('min') # best teammate's history
d['team_sum_top3'] = tg.n_top3.transform('sum') # teammates' past top-3 % finishes
d['rank_in_comp'] = d.logpct_ewm.rank(pct=True) # strength relative to this field
- Train LightGBM (400 trees, learning rate 0.02, 15 leaves, at least 100 rows per leaf, row and column subsampling, L2 = 5), average 3 seeds.
- For the test, the history is everything in
train.parquet, the team is the givenTeamId, and the reference date is the last start date in the history.
Results
ROC AUC on the users of the last 5 (and 10) competitions of the history, built exactly like the test. The official test labels are private.
| approach | last 5 competitions | last 10 competitions |
|---|---|---|
| number of past competitions (organisers' baseline) | 0.542 | 0.553 |
| past top-3 % rate | 0.772 | 0.776 |
| Elo rating computed over all competitions (Codeforces-style) | 0.707 | 0.723 |
| logistic regression, all features | 0.808 | 0.823 |
| LightGBM without team features | 0.784 | 0.796 |
| LightGBM + Elo rating features | 0.826 | 0.844 |
| LightGBM, 63 leaves / 800 trees / training since 2018 only | 0.827 / 0.826 / 0.828 | 0.842 / 0.842 / 0.843 |
| LightGBM, all features (final) | 0.826 | 0.842 |
The final model gets 0.826 on the last 5 competitions and 0.842 on the last 10. The best public-leaderboard entry is 0.916, but on different competitions: our two validation windows already differ by 1.6 points, so the competitions themselves change the difficulty a lot. We cannot tell how far we are from that score without the test labels.
Two ideas failed and are worth knowing. An Elo rating, the natural choice for a competition named after Codeforces, is worse than the simple past top-3 % rate (0.707): with ratings updated by rank among hundreds of players, one lucky or unlucky competition moves a rating a lot, while "how often did you reach the top" is exactly the quantity asked. Added to the other features it brings nothing. Tuning the trees changed the score by less than 0.2 points: the information is in the features, not in the model settings.
One more signal exists in the test but cannot be validated: the team id grows with the date a team was created, and in 4 of the 5 test competitions our scores are mildly higher for smaller team ids (Spearman correlation down to ). Strong users seem to join early. The history has no team ids, so we could not measure this effect and left it out.
What to remember
- Validate the way you will be tested. For a "predict the future" task, rebuild the exact test situation in the past (same cut-off, same filter) for both training and validation. A random split would leak future results into the features.
- Replay history to multiply training data. Each past competition is a separate snapshot with its own features and labels; together they form a large training set that matches the test distribution.
- The best predictor of a rare success is past success, not experience. Rate features ("how often in the top") beat count features ("how many times played").
- Look for relational signals. Who a user teams up with tells as much as their own history. Features about the group (team size, best teammate) gave the largest single gain.
- A famous method is not automatically the right one. Elo was elegant and weaker than a well-chosen ratio; always compare against a simple baseline.