All projects

Research · Università del Foro Italico

Does Early Speed Last?

Does being fast at 16 mean being fast at 25? A longitudinal study on athletes born between 1989 and 1995, followed from U18 to senior, asking whether early sprint performance predicts career success.

Jun 2026 – ongoing

Study and publication in progress

Overview

Every young sprinter is told the same two things, usually by different people: that early results decide everything, and that they mean nothing at all. Almost nobody has checked.

This is a retrospective cohort study, run with Università di Roma Foro Italico on the database rebuilt from twenty years of FIDAL rankings — around 450,000 registered athletes, 2005 to 2025. Seven birth cohorts, 1989 to 1995, are followed from their Allievi (U18) debut to age 30 over 100 m and 200 m, with the FIDAL scoring tables turning times into one comparable number across events and eras.

The blunt finding so far: the exit door is at 17. The steepest attrition of the whole career is the step out of U18, and it hits every performance level. Being fast early helps — the top Allievi quintile is retained far better than the bottom one — but it is not protection: most of the fastest sixteen-year-olds are also gone before the senior years.

What we built

  • A cohort definition with per-athlete observation windows, so every career is compared over the same phases: Allievi, Juniores, Promesse, Seniores.
  • Retention and dropout measured at every transition, both cumulatively and conditionally, and split by Allievi performance quintile.
  • Real dropout separated from specific dropout: about 64.5% of the men who stop sprinting leave athletics altogether, while the rest turn up in the 400 m or the horizontal jumps.
  • Quintile mobility from U18 to senior as a transition matrix, with ANOVA at each phase: the U18 ranking never stops being significant, but its F-statistic collapses from 1140 to 99 as careers diverge.
  • Birth-quarter composition by quintile, where the relative age effect shows up: 65.1% of the top male quintile is born in the first half of the year.
  • K-means over the four-phase score matrix, which finds six career archetypes — early dropout, late bloomer, and the four in between — projected with PCA.

Inside the study

Read-only · outputs from studio_coorte_sprint_italiano_89_95.ipynb

Five cells from the cohort notebook: how the seven birth cohorts are framed in time, what retention looks like once athletes are split by their U18 level, and the six career archetypes k-means pulls out of the score matrix.

In [ ]:
COHORTS     = list(range(1989, 1996))   # seven birth years
DATA_START  = 2005
DATA_END    = 2025
AGE_SEN_MAX = 30                       # senior window capped at 30

def cat_windows(birth_year: int) -> dict:
    """Observable category windows, clipped to the years we actually have."""
    raw = {
        "Allievi":  (birth_year + 16, birth_year + 17),
        "Juniores": (birth_year + 18, birth_year + 19),
        "Promesse": (birth_year + 20, birth_year + 22),
        "Seniores": (birth_year + 23, birth_year + AGE_SEN_MAX),
    }
    return {c: (max(y0, DATA_START), min(y1, DATA_END))
            for c, (y0, y1) in raw.items() if max(y0, DATA_START) <= min(y1, DATA_END)}
In [ ]:
# Best Allievi score per athlete, then split each sex into quintiles
_al = (df_wide[df_wide["score_Allievi"].notna()]
       .groupby(["athlete_id", "gender"])["score_Allievi"].max()
       .reset_index().rename(columns={"score_Allievi": "allievi_score"}))

Q_LABELS = ["Q1 (lowest)", "Q2", "Q3", "Q4", "Q5 (highest)"]
_al["quintile"] = _al.groupby("gender")["allievi_score"].transform(
    lambda x: pd.qcut(x, q=5, labels=Q_LABELS)
)
…
plot_retention_by_quintile(_al, df_wide)
In [ ]:
# Dropout at every transition, as a share of the athletes still there
drop = (1 - retained / previous) * 100

for gender, label in [("M", "Males"), ("F", "Females")]:
    show(drop.loc[gender], caption=f"Dropout (cumulative) — {label}")
In [ ]:
km_final = KMeans(n_clusters=K_BEST, random_state=RANDOM_STATE, n_init=30)
feat["cluster"] = km_final.fit_predict(X_scaled)

sizes = feat["cluster"].value_counts().sort_index()
print("Cluster sizes:")
for c, n in sizes.items():
    print(f"  Cluster {c}: {n:>5,} athletes  ({n / len(feat) * 100:.1f}%)")
In [ ]:
traj = feat.groupby("cluster")[SCORE_COLS].mean()

for c in range(K_BEST):
    ax.plot(x, traj.loc[c].values, marker="o", linewidth=2.2,
            color=CLUSTER_PALETTE[c], label=f"Cluster {c}  (n={sizes[c]:,})")

ax.set_xticklabels(["Allievi", "Juniores", "Promesse", "Seniores"])
ax.set_ylabel("Mean FIDAL score")
plt.show()
In [ ]:
pca   = PCA(n_components=2, random_state=RANDOM_STATE)
X_pca = pca.fit_transform(X_scaled)
var   = pca.explained_variance_ratio_ * 100

print(f"Variance explained — PC1: {var[0]:.1f}%  |  PC2: {var[1]:.1f}%  |  Total: {var.sum():.1f}%")