Varun Kulkarni
All projects

Tennis Point Leverage

LSTM model to quantify tennis point leverage, momentum, and clutch performance

  • AI Research
  • Sports Analytics
  • Machine Learning

The question

Tennis commentators talk about “big points” as if importance is self-evident. Some of it is: losing at 30–40 is obviously worse than losing at 0–0. What interested me was a harder question—how much does each point move the probability of holding serve, and where do real players depart from a purely mechanical model of the game?

I approached that as a comparison between two systems. The first is an exact probabilistic baseline where every point is an independent Bernoulli trial. The second is a recurrent model trained on real point sequences. The baseline tells me what should happen from the score alone; the LSTM captures what actually tends to happen in the data. The gap between them is where pressure, sequencing, and selection effects begin to show up.

170K
service games
43 pp
leverage at 30–30
+25.6 pp
first-point hold gap
+2.4 pp
observed momentum

Starting at the baseline (pun intended)

Let H(s,r)H(s,r) be the probability that the server holds from score state (s,r)(s,r), and let pp be the server’s probability of winning an individual point. Using the tour-level approximation p≈0.65p \approx 0.65, the game can be written as a recurrence:

H(s,r)={1,s≥4 and s−r≥20,r≥4 and r−s≥2pH(s+1,r)+(1−p)H(s,r+1),otherwiseH(s,r)= \begin{cases} 1, & s\ge 4 \text{ and } s-r\ge 2\\ 0, & r\ge 4 \text{ and } r-s\ge 2\\ pH(s+1,r)+(1-p)H(s,r+1), & \text{otherwise} \end{cases}
H(s,r)={1,s≥4,s−r≥20,r≥4,r−s≥2pH(s+1,r)+(1−p)H(s,r+1)otherwiseH(s,r)= \begin{cases} 1, & \begin{aligned} s&\ge 4,\\[-1pt] s-r&\ge 2 \end{aligned}\\[7pt] 0, & \begin{aligned} r&\ge 4,\\[-1pt] r-s&\ge 2 \end{aligned}\\[7pt] \begin{aligned} pH(s+1,r)\\[5pt] {}+(1-p)H(s,r+1) \end{aligned} & \text{otherwise} \end{cases}

Deuce is the only cyclic state. If I abbreviate HD=H(3,3)H_D=H(3,3), the recurrence passes through advantage-server and advantage-receiver:

HD=pH(4,3)+(1−p)H(3,4)H(4,3)=p+(1−p)HDH(3,4)=pHD\begin{aligned} H_D &= pH(4,3)+(1-p)H(3,4)\\ H(4,3) &= p+(1-p)H_D\\ H(3,4) &= pH_D \end{aligned}

Substituting those advantage states back into deuce produces a fixed-point equation:

HD=p[p+(1−p)HD]+(1−p)pHD=p2+2p(1−p)HD\begin{aligned} H_D &= p\left[p+(1-p)H_D\right]+(1-p)pH_D\\ &= p^2+2p(1-p)H_D \end{aligned}
HD=p[p+(1−p)HD]+(1−p)pHD=p2+2p(1−p)HD\begin{aligned} H_D &= p\left[p+(1-p)H_D\right]\\[7pt] &\quad +(1-p)pH_D\\[7pt] &= p^2+2p(1-p)H_D \end{aligned}

Solving for HDH_D gives the familiar two-points-in-a-row result:

Hdeuce=p2p2+(1−p)2=0.42250.4225+0.1225≈0.775H_{\mathrm{deuce}} =\frac{p^2}{p^2+(1-p)^2} =\frac{0.4225}{0.4225+0.1225} \approx 0.775
Hdeuce=p2p2+(1−p)2=0.42250.4225+0.1225≈0.775\begin{aligned} H_{\mathrm{deuce}} &=\frac{p^2}{p^2+(1-p)^2}\\[9pt] &=\frac{0.4225}{0.4225+0.1225}\\[9pt] &\approx 0.775 \end{aligned}

This baseline deliberately assumes away momentum, pressure, player identity, surface, and match context. That is its value: it gives me a clean counterfactual. Any residual has to come from something the independence model does not represent.

Learning the empirical game

I extracted roughly 170,000 non-tiebreak service games from Jeff Sackmann’s Match Charting Project. Each point becomes a three-value observation:

xt=[st/4rt/41[server wins point t]]\mathbf{x}_t= \begin{bmatrix} s_t/4\\ r_t/4\\ \mathbb{1}[\text{server wins point }t] \end{bmatrix}

The score features locate the point within the game; the binary outcome lets the model update its belief sequentially. Games are padded to 24 points, and a mask prevents padded timesteps from contributing to the loss.

Point sequencescore, opponent score, outcome
2-layer LSTM32-dimensional hidden state
Dense head32 → 16 → 1 with ReLU
Hold probabilitysigmoid output at every point
The model is intentionally small: roughly 6,000 parameters, with one probability emitted at every valid timestep.

At each timestep, the LSTM combines the current point with its previous hidden state. The forget, input, and output gates decide what to retain, what to write, and what to expose:

ft=σ(Wf[ht−1,xt]+bf)it=σ(Wi[ht−1,xt]+bi)ot=σ(Wo[ht−1,xt]+bo)c~t=tanh⁡(Wc[ht−1,xt]+bc)\begin{aligned} f_t &= \sigma(W_f[h_{t-1},x_t]+b_f)\\ i_t &= \sigma(W_i[h_{t-1},x_t]+b_i)\\ o_t &= \sigma(W_o[h_{t-1},x_t]+b_o)\\ \tilde{c}_t &= \tanh(W_c[h_{t-1},x_t]+b_c) \end{aligned}

Those gates update the cell state, then the hidden state becomes the input to a small prediction head:

ct=ft⊙ct−1+it⊙c~tht=ot⊙tanh⁡(ct)zt=ReLU⁡(W1ht+b1)y^t=σ(W2zt+b2)\begin{aligned} c_t &= f_t\odot c_{t-1}+i_t\odot\tilde{c}_t\\ h_t &= o_t\odot\tanh(c_t)\\ z_t &= \operatorname{ReLU}(W_1h_t+b_1)\\ \hat{y}_t &= \sigma(W_2z_t+b_2) \end{aligned}

I chose an LSTM over a feed-forward score lookup because two games can reach the same score through different sequences. A basic recurrent network would preserve order too, but it introduces a familiar training problem. For a vanilla RNN,

ht=ϕ(Whht−1+Wxxt+b)h_t=\phi(W_hh_{t-1}+W_xx_t+b)

the gradient reaching an early hidden state contains a product of one Jacobian per later timestep:

∂L∂h1=[∏t=1T−1Wh⊤diag⁡ ⁣(ϕ′(at))]∂L∂hT\frac{\partial\mathcal{L}}{\partial h_1} =\left[\prod_{t=1}^{T-1} W_h^\top\operatorname{diag}\!\left(\phi'(a_t)\right)\right] \frac{\partial\mathcal{L}}{\partial h_T}
∂L∂h1=[∏t=1T−1Wh⊤diag⁡ ⁣(ϕ′(at))]∂L∂hT\begin{aligned} \frac{\partial\mathcal{L}}{\partial h_1} &=\left[\prod_{t=1}^{T-1}W_h^\top\right.\\[7pt] &\qquad\left. \operatorname{diag}\!\left(\phi'(a_t)\right)\right] \frac{\partial\mathcal{L}}{\partial h_T} \end{aligned}

If the effective norm of those factors is below one, the gradient decays roughly exponentially with sequence length; above one, it can explode:

∥∂L∂h1∥≲(∥Wh∥ γ)T−1∥∂L∂hT∥,γ=max⁡t∥ϕ′(at)∥\left\| \frac{\partial\mathcal{L}}{\partial h_1} \right\| \lesssim \left(\|W_h\|\,\gamma\right)^{T-1} \left\| \frac{\partial\mathcal{L}}{\partial h_T} \right\|, \qquad \gamma=\max_t\|\phi'(a_t)\|
∥∂L∂h1∥≲(∥Wh∥ γ)T−1×∥∂L∂hT∥,γ=max⁡t∥ϕ′(at)∥\begin{aligned} \left\|\frac{\partial\mathcal{L}}{\partial h_1}\right\| &\lesssim \left(\|W_h\|\,\gamma\right)^{T-1}\\[7pt] &\quad {}\times\left\| \frac{\partial\mathcal{L}}{\partial h_T} \right\|,\\[7pt] \gamma&=\max_t\|\phi'(a_t)\| \end{aligned}

The LSTM’s additive cell-state update creates a more direct gradient path. Its local derivative depends on the learned forget gate rather than another multiplication by a fixed recurrent matrix:

∂ct∂ct−1=diag⁡(ft)\frac{\partial c_t}{\partial c_{t-1}} =\operatorname{diag}(f_t)

When history remains useful, the network can keep ftf_t near one. That is the practical reason it can carry information through a long deuce sequence more reliably than a basic RNN.

Backpropagation through time still has to combine two sources at every step: the loss emitted at that point and the influence of the hidden state on every future point:

∂L∂ht=∂ℓt∂ht+(∂ht+1∂ht)⊤∂L∂ht+1\frac{\partial\mathcal{L}}{\partial h_t} =\frac{\partial\ell_t}{\partial h_t} +\left(\frac{\partial h_{t+1}}{\partial h_t}\right)^\top \frac{\partial\mathcal{L}}{\partial h_{t+1}}
∂L∂ht=∂ℓt∂ht+(∂ht+1∂ht)⊤∂L∂ht+1\begin{aligned} \frac{\partial\mathcal{L}}{\partial h_t} &=\frac{\partial\ell_t}{\partial h_t}\\[7pt] &\quad+\left( \frac{\partial h_{t+1}}{\partial h_t} \right)^\top\\[7pt] &\qquad \frac{\partial\mathcal{L}}{\partial h_{t+1}} \end{aligned}

Written at the gate level, the cell gradient accumulates both the current output path and the future cell-state path:

cˉt=hˉt⊙ot⊙(1−tanh⁡2(ct))+cˉt+1⊙ft+1δt(o)=hˉt⊙tanh⁡(ct)⊙ot(1−ot)δt(f)=cˉt⊙ct−1⊙ft(1−ft)δt(i)=cˉt⊙c~t⊙it(1−it)δt(g)=cˉt⊙it⊙(1−c~t2)\begin{aligned} \bar{c}_t &=\bar{h}_t\odot o_t\odot \left(1-\tanh^2(c_t)\right) +\bar{c}_{t+1}\odot f_{t+1}\\ \delta_t^{(o)} &=\bar{h}_t\odot\tanh(c_t)\odot o_t(1-o_t)\\ \delta_t^{(f)} &=\bar{c}_t\odot c_{t-1}\odot f_t(1-f_t)\\ \delta_t^{(i)} &=\bar{c}_t\odot\tilde{c}_t\odot i_t(1-i_t)\\ \delta_t^{(g)} &=\bar{c}_t\odot i_t\odot(1-\tilde{c}_t^2) \end{aligned}
cˉt=hˉt⊙ot⊙(1−tanh⁡2(ct))+cˉt+1⊙ft+1δt(o)=hˉt⊙tanh⁡(ct)⊙ot(1−ot)δt(f)=cˉt⊙ct−1⊙ft(1−ft)δt(i)=cˉt⊙c~t⊙it(1−it)δt(g)=cˉt⊙it⊙(1−c~t2)\begin{aligned} \bar{c}_t &=\bar{h}_t\odot o_t\odot \left(1-\tanh^2(c_t)\right)\\[7pt] &\quad+\bar{c}_{t+1}\odot f_{t+1}\\[9pt] \delta_t^{(o)} &=\bar{h}_t\odot\tanh(c_t)\odot o_t(1-o_t)\\[7pt] \delta_t^{(f)} &=\bar{c}_t\odot c_{t-1}\odot f_t(1-f_t)\\[7pt] \delta_t^{(i)} &=\bar{c}_t\odot\tilde{c}_t\odot i_t(1-i_t)\\[7pt] \delta_t^{(g)} &=\bar{c}_t\odot i_t\odot(1-\tilde{c}_t^2) \end{aligned}

Parameter gradients then sum the contribution from every point in the unrolled game. For the forget gate, for example:

∂L∂Wf=∑t=1Tδt(f)[ht−1,xt]⊤\frac{\partial\mathcal{L}}{\partial W_f} =\sum_{t=1}^{T} \delta_t^{(f)} [h_{t-1},x_t]^\top

I did not implement those derivatives by hand in the project. PyTorch’s nn.LSTM returns the full B×T×32B\times T\times 32 hidden-state tensor, the prediction head maps every state to a probability, and the masked loss reduces those outputs to one scalar. Calling loss.backward() then traverses the entire unrolled graph. Because a service game is capped at only 24 points, I use full BPTT rather than truncating the sequence.

I trained with binary cross-entropy at every valid timestep—not only at the end of the game—so the network had to produce a useful hold probability throughout the sequence:

L=−1∑i,tmi,t∑i,tmi,t[yilog⁡y^i,t+(1−yi)log⁡(1−y^i,t)]\mathcal{L} =-\frac{1}{\sum_{i,t}m_{i,t}} \sum_{i,t}m_{i,t} \left[y_i\log \hat{y}_{i,t}+(1-y_i)\log(1-\hat{y}_{i,t})\right]
L=−1∑i,tmi,t∑i,tmi,t[yilog⁡y^i,t+(1−yi)log⁡(1−y^i,t)]\begin{aligned} \mathcal{L} &=-\frac{1}{\sum_{i,t}m_{i,t}} \sum_{i,t}m_{i,t}\\[7pt] &\quad\left[ y_i\log \hat{y}_{i,t}\right.\\[7pt] &\qquad\left. +(1-y_i)\log(1-\hat{y}_{i,t})\right] \end{aligned}

Here mi,tm_{i,t} is one only for real points, so padding contributes neither loss nor gradient. The final hold/break label yiy_i is expanded across every valid point in game ii. Each batch then follows four steps:

  1. Pad variable-length games to 24 points and construct the timestep mask.
  2. Run the sequence through the LSTM and emit one probability after every point.
  3. Apply masked binary cross-entropy against the eventual hold/break outcome.
  4. Backpropagate through all valid timesteps and update the weights with Adam.
  • 2 LSTM layers with a 32-dimensional hidden state
  • 32 → 16 → 1 prediction head with ReLU and sigmoid
  • 135,876 training games and 33,970 validation games
  • Adam optimizer, learning rate 0.001, batch size 64, 20 epochs

The repository reports 99.2% final-timestep validation accuracy. I treat that as a sanity check rather than the main result: by the final point, the score almost gives away the outcome. The useful object is the full probability path before the game is decided.

Defining leverage

Once the model estimates empirical hold probability H^(s,r)\hat{H}(s,r), leverage is the difference between the next state after winning and the next state after losing:

L(s,r)=H^(s+1,r)−H^(s,r+1)L(s,r)=\hat{H}(s+1,r)-\hat{H}(s,r+1)

It is a counterfactual swing measured in percentage points. At 30–30, winning moves the server toward game point while losing creates break point. The model estimates a 43-point gap between those branches, making 30–30 the highest-leverage non-terminal state. Earlier pressure states are close: 15–30 produces a 42-point swing and 0–30 a 41-point swing.

Bar chart ranking tennis score states by their effect on server hold probability
A saved break point at 30–40 has the largest total swing because losing ends the game immediately. Among non-terminal states, 30–30 is the pivot.

Reality minus expectation

To separate score mechanics from observed behavior, I defined performance deviation as the LSTM estimate minus the independent-point baseline:

Δ(s,r)=H^(s,r)−H(s,r)\Delta(s,r)=\hat{H}(s,r)-H(s,r)

Positive values mean servers hold more often than the baseline expects; negative values mean they hold less often. The result is not a simple “players are clutch” story. At 15–40, servers outperform by 2.8 percentage points, while at 30–40 they underperform by 3.1 points. At 0–0 the residual is −4.8 points, the largest absolute departure in the standard score grid.

Heatmap showing empirical server hold probability minus theoretical expectation at each tennis score
Green cells beat the independence baseline; orange cells fall below it. Pressure effects are small, state-dependent, and less dramatic than the usual clutch-or-choke framing.

I like this result because it resists the clean headline. Pressure is not a single scalar that switches on at break point. Different score states carry different tactical incentives, samples, and player populations. The residual map is evidence of structure beyond the baseline—not proof of a psychological mechanism.

Two secondary effects

The first point produced the largest simple conditional gap in the analysis:

P(hold∣won first)=88.4%P(hold∣lost first)=62.8%difference=25.6 pp\begin{aligned} P(\mathrm{hold}\mid \mathrm{won\ first}) &= 88.4\%\\ P(\mathrm{hold}\mid \mathrm{lost\ first}) &= 62.8\%\\ \mathrm{difference} &= 25.6\ \mathrm{pp} \end{aligned}
Bar chart comparing hold rates after winning and losing the first point of a service game
Winning the opening point is strongly associated with holding serve. The chart is descriptive: server quality and other selection effects can contribute to the gap.

I also tested short-range momentum by conditioning the current point on the previous point’s outcome across 696,622 transitions:

P(Wt∣Wt−1)=65.1%P(Wt∣¬Wt−1)=62.7%Δmomentum=+2.4 pp\begin{aligned} P(W_t\mid W_{t-1}) &= 65.1\%\\ P(W_t\mid \neg W_{t-1}) &= 62.7\%\\ \Delta_{\mathrm{momentum}} &= +2.4\ \mathrm{pp} \end{aligned}

With that sample size, the difference is statistically detectable, but it is still small in practical terms. More importantly, this conditional comparison is not causal: player quality, serve rotation, and score context can all create persistence without a psychological “hot hand.”

Extending from games to matches

The service-game model answers a deliberately local question. I also wanted to see what “importance” looked like across an entire match, where the same point score means something different depending on the set score, server, tiebreak state, and match format. For that, I built a separate causal Transformer.

Each point is encoded with nine normalized features:

xt=[ S1,t,S2,t,G1,t,G2,t,P1,t,P2,t,servert,tiebreakt,bestOft ]\mathbf{x}_t= [\,S_{1,t},S_{2,t},G_{1,t},G_{2,t},P_{1,t},P_{2,t}, \mathrm{server}_t,\mathrm{tiebreak}_t,\mathrm{bestOf}_t\,]
xt=[ S1,t,S2,t,G1,t,G2,t,P1,t,P2,t,servert,tiebreakt,bestOft ]\begin{aligned} \mathbf{x}_t=[\, &S_{1,t},S_{2,t},G_{1,t},G_{2,t},\\[5pt] &P_{1,t},P_{2,t},\mathrm{server}_t,\\[5pt] &\mathrm{tiebreak}_t,\mathrm{bestOf}_t\,] \end{aligned}

A learned linear projection maps that vector into dmodel=64d_{\mathrm{model}}=64 dimensions, then adds sinusoidal positional encoding:

et=Wxxt+bx+PE⁡(t)\mathbf{e}_t=W_x\mathbf{x}_t+b_x+\operatorname{PE}(t)

The encoder uses three layers, four attention heads, a 256-dimensional feed-forward sublayer, and 0.1 dropout. Within each head, scaled dot-product attention is:

Attention⁡(Q,K,V)=softmax⁡(QK⊤dk+M)V\operatorname{Attention}(Q,K,V) =\operatorname{softmax} \left(\frac{QK^\top}{\sqrt{d_k}}+M\right)V
Attention⁡(Q,K,V)=softmax⁡(QK⊤dk+M)×V\begin{aligned} \operatorname{Attention}(Q,K,V) &=\operatorname{softmax}\left( \frac{QK^\top}{\sqrt{d_k}}+M \right)\\[7pt] &\quad {}\times V \end{aligned}

The mask MM is zero on and below the diagonal and −∞-\infty above it. That makes the model causal: the prediction after point tt can attend to points ≤t\leq t, but never to the future. A padding mask separately removes unused positions from shorter matches.

Training follows the same timestep-supervision idea as the LSTM, but at match scale. I split matches chronologically into 80% training, 10% validation, and 10% test sets, padded sequences to 500 points, and optimized masked binary cross-entropy at every timestep. The run used:

  • AdamW with learning rate 0.001 and weight decay 0.01
  • Cosine learning-rate annealing over 30 epochs
  • Batch size 32 and gradient clipping at norm 1.0
  • A sigmoid prediction head producing match-win probability after every point

In the saved training history, validation accuracy peaks at 96.2% around epoch 14. After that, training loss continues to fall while validation loss rises—a visible sign of overfitting. The loop saves a checkpoint only when validation accuracy improves, so the analysis can use the best generalizing state rather than assuming the final epoch is the best one.

Once I have the probability path p^1,…,p^T\hat{p}_1,\ldots,\hat{p}_T, I define the observed importance of point tt as the size of the update it caused:

It=∣p^t−p^t−1∣,p^0=0.5I_t=\left|\hat{p}_t-\hat{p}_{t-1}\right|, \qquad \hat{p}_0=0.5

I call the final crossing of the 50% line the match’s “decision point.” Formally, it is the largest timestep where the predicted favorite changes:

t⋆=max⁡{t:(p^t−1−0.5)(p^t−0.5)≤0}t^\star=\max\left\{t: (\hat{p}_{t-1}-0.5)(\hat{p}_t-0.5)\leq 0\right\}
t⋆=max⁡{t: (p^t−1−0.5)(p^t−0.5)≤0}\begin{aligned} t^\star=\max\left\{t:\, \right.&(\hat{p}_{t-1}-0.5)\\[7pt] &\left.(\hat{p}_t-0.5)\leq 0\right\} \end{aligned}
Transformer win-probability timeline for Roger Federer versus Novak Djokovic in the 2019 Wimbledon final
Federer vs. Djokovic, 2019 Wimbledon final. The upper panel tracks predicted win probability; the lower panel shows absolute point-to-point movement. The green line marks the final change in predicted favorite.

This match is exactly why I wanted the sequence model. Federer spends long stretches above 50%, and several late points produce enormous local probability changes, but the favorite keeps flipping. The green line does not claim that one point caused the outcome; it identifies the last moment when the model’s balance of evidence crossed from one player to the other and never returned.

What I would improve next

The current model pools eras, surfaces, rounds, and players. Charted matches also skew toward prominent players and high-profile events. A stronger follow-up would use hierarchical player effects, surface-specific serve priors, chronological out-of-sample evaluation, and calibration metrics at intermediate score states.

I would also separate prediction from explanation more aggressively. The LSTM is useful because sequence order matters, but the independence baseline remains the most interpretable part of the analysis. The project became interesting when I stopped asking the network for a verdict and started using it as one side of a controlled comparison.