tennis 2026

Tenis US Open 2026 – kto wygra?

Oglądając Tenis US Open 2026 zastanawiamy się kto wygra. Oto propozycja analizy z wykorzystaniem algorytmów machine learning prowadząca do wytypowania tego, kto wygra turniej Tenis US Open 2026. Pojawia się coraz więcej statystyk i analiz a taktyka meczów oparta na danych nabiera bardzo dużego znaczenia. Przygotowanie zajęć z uczenia maszynowego (ML) opartych na statystykach tenisowych to świetny pomysł. Tenis jest sportem bardzo mierzalnym, a zbiory danych są czyste i gotowe do analizy.

Tenis US Open 2026 – Wyniki ćwierćfinałów

Mężczyźni (Singiel):

  • Ben Shelton pokonał Carlosa Alcaraza 3:2
  • Alexander Zverev pokonał Botica van de Zandschulpa 3:0 (6:2, 7:5, 6:1)
  • Frances Tiafoe pokonał Alexa Michelsena 3:2 (5:7, 3:6, 7:5, 6:3, 7:6)
  • Karen Chaczanow awansował po kreczu Alexandera Blockxa przy stanie 6:2, 7:5, 3:2 [1]

Kobiety (Singiel):

  • Aryna Sabalenka pokonała Lindę Noskovą 2:1 (7:6, 3:6, 7:6)
  • Jelena Rybakina pokonała Zheng Qinwen 2:1 (3:6, 6:1, 6:4)
  • Coco Gauff pokonała Mirrę Andriejewą 2:1 (2:6, 7:6, 6:2)
  • Jessica Pegula pokonała Emmę Navarro 2:1 (3:6, 6:4, 6:3)

Najlepszym, darmowym i powszechnie szanowanym źródłem takich danych w świecie data science jest repozytorium Jeffa Sackmanna (Tennis Abstract) na GitHubie. [1]

Oto instrukcja krok po kroku, skąd pobrać odpowiednie pliki CSV oraz jakie cechy (features) udostępnić studentom do budowy modeli klasyfikacyjnych lub regresyjnych.


1. Skąd pobrać gotowe pliki CSV?

Opcja A: Poziom zagregowany (Mecz po meczu) – idealny na start

Jeśli chcesz, aby studenci analizowali statystyki całego meczu (np. asy zawodnika A vs asy zawodnika B), pobierz dane z repozytoriów turniejowych.

# Load data (20002026, corrected raw URL)
import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score, log_loss, roc_auc_score

years = range(2023, 2027)
base = "https://raw.githubusercontent.com/Aneeshers/tennis-sackmann-archive/main/wta/wta_matches_{}.csv"
df = pd.concat([pd.read_csv(base.format(y), low_memory=False) for y in years], ignore_index=True)

df['tourney_date'] = pd.to_datetime(df['tourney_date'], format='%Y%m%d')
df = df.sort_values(['tourney_date', 'match_num']).reset_index(drop=True)

# keep only matches with full serve/return stats (needed for play-style features)
stat_cols = ['w_ace','w_df','w_svpt','w_1stIn','w_1stWon','w_2ndWon','w_SvGms','w_bpSaved','w_bpFaced',
             'l_ace','l_df','l_svpt','l_1stIn','l_1stWon','l_2ndWon','l_SvGms','l_bpSaved','l_bpFaced']

df = df.dropna(subset=stat_cols)
df = df[(df['w_svpt'] > 0) & (df['l_svpt'] > 0)]
print(f"{len(df):,} matches usable")

Jak szukać plików: Wewnątrz repozytoriów znajdziesz pliki podzielone latami, np. atp_matches_2025.csv, atp_matches_2026.csv. Każdy wiersz to jeden rozegrany mecz w danym roku (w tym wszystkie mecze Wielkiego Szlema, jak US Open). [1] w tym Tenis US Open 2026

Opcja B: Poziom zaawansowany (Punkt po punkcie) – dla ambitnych grup

Jeśli studenci mają analizować mikromomenty (np. czy as przy break-poincie decyduje o wygranej), skorzystaj z projektu mapowania uderzeń:


2. Jakie zmienne (Features) znajdziesz w plikach?

W standardowych plikach rocznych Sackmanna, każdy wiersz zawiera gotowe statystyki obu graczy (Zwycięzcy – prefiks w_ oraz Przegranego – prefiks l_). Do dyspozycji studentów będą m.in.:

  • w_ace / l_ace – liczba asów serwisowych.
  • w_df / l_df – liczba podwójnych błędów serwisowych (double faults).
  • w_svpt / l_svpt – łączna liczba punktów rozegranych przy własnym serwisie.
  • w_1stIn / l_1stIn – liczba celnych pierwszych serwisów.
  • w_1stWon / l_1stWon – punkty wygrane po pierwszym serwisie.
  • w_2ndWon / l_2ndWon – punkty wygrane po drugim serwisie.
  • w_bpSaved / l_bpSaved – obronione break-pointy.
  • w_bpFaced / l_bpFaced – break-pointy, przed którymi stał zawodnik. [1, 2]

Dane możesz także scrapować z ATP przy pomocy tego skryptu: https://github.com/serve-and-volley/atp-world-tour-tennis-data/blob/master/python/match_stats.py


W tym skrypcie, dane zostaną przekształcone na dane wygranego i pokonanego zawodnika tak, aby w dalszym kroku można było wybrać 50 ostatnich meczów do średnich. Statystyka zakłada minimum 10 meczów aby statystyka była wiarygodna. Tenis US Open 2026.

# Convert to per-player, per-match play-style rates
def build_side(df, side):
    other = 'l' if side == 'w' else 'w'

# This matters for exactly one thing later in the function
# the return stats, which can't be computed from a player's own columns 
# (there's no w_returnPtsWon column in the data). 
# A player's return performance is mathematically the flip side of their 
# opponent's serve performance

    out = pd.DataFrame({
        'match_id': df.index, 'tourney_date': df['tourney_date'],
        'player_id': df[f'{side}inner_id'] if side=='w' else df[f'{side}oser_id'],
        'player_name': df[f'{side}inner_name'] if side=='w' else df[f'{side}oser_name'],
        'won': 1 if side=='w' else 0,
    })
    svpt, firstin = df[f'{side}_svpt'], df[f'{side}_1stIn']
    out['ace_pct'] = df[f'{side}_ace'] / svpt
    out['df_pct'] = df[f'{side}_df'] / svpt
    out['first_in_pct'] = firstin / svpt
    out['first_won_pct'] = np.where(firstin>0, df[f'{side}_1stWon']/firstin, np.nan)
    second = svpt - firstin
    out['second_won_pct'] = np.where(second>0, df[f'{side}_2ndWon']/second, np.nan)
    out['serve_pts_won_pct'] = (df[f'{side}_1stWon'] + df[f'{side}_2ndWon']) / svpt
    bpF = df[f'{side}_bpFaced']
    out['bp_saved_pct'] = np.where(bpF>0, df[f'{side}_bpSaved']/bpF, 1.0)
    opp_svpt = df[f'{other}_svpt']
    opp_won = df[f'{other}_1stWon'] + df[f'{other}_2ndWon']
    out['return_pts_won_pct'] = (opp_svpt - opp_won) / opp_svpt
    opp_bpF, opp_bpS = df[f'{other}_bpFaced'], df[f'{other}_bpSaved']
    out['bp_conv_pct'] = np.where(opp_bpF>0, (opp_bpF-opp_bpS)/opp_bpF, 0.0)
    return out

long_df = pd.concat([build_side(df,'w'), build_side(df,'l')], ignore_index=True)
long_df = long_df.sort_values(['player_id','tourney_date']).reset_index(drop=True)

FEATURES = ['ace_pct','df_pct','first_in_pct','first_won_pct','second_won_pct',
            'serve_pts_won_pct','bp_saved_pct','return_pts_won_pct','bp_conv_pct']

Wybranie meczów do statystyki

# Pre-match rolling form (shifted, no leakage)

ROLL, MIN_HIST = 50, 10
# These two numbers control the rolling window used to build each player's "current form"


grp = long_df.groupby('player_id', group_keys=False)
for f in FEATURES:
    long_df[f'pre_{f}'] = grp[f].apply(lambda s: s.shift(1).rolling(ROLL, min_periods=MIN_HIST).mean())
pre_cols = [f'pre_{f}' for f in FEATURES]

s here is one player’s chronologically-ordered Series of a single stat — e.g. all of Sabalenka’s ace_pct values, one per match, in date order (this works correctly because long_df was sorted by ['player_id', 'tourney_date'] earlier, and grp preserves that order within each group).

The three operations chain left to right:

1. s.shift(1) — moves every value down by one position, so each row now holds the previous match’s value instead of its own.

match:        1     2     3     4     5
ace_pct:     0.05  0.09  0.03  0.11  0.07
shift(1):     NaN  0.05  0.09  0.03  0.11

This is the anti-leakage step — it’s why row 5 sees the value from match 4, not from match 5 itself. Without this, you’d be using a match’s own outcome to predict itself.

2. .rolling(ROLL, min_periods=MIN_HIST) — defines a moving window of up to ROLL values looking backward from each row (on the already-shifted series), but only produces a result once at least MIN_HIST non-NaN values are available in that window. With ROLL=20, MIN_HIST=5: at row 5, the window looks at rows 1–5 of the shifted series (5 available values, ≥5 required) → computes something; at row 3, only 3 values available (<5) → NaN.

3. .mean() — averages whatever values fall inside that window.

Put together, for row i: “Average this player’s stat over their last (up to) ROLL matches, not counting match i itself, as long as at least MIN_HIST prior matches exist — otherwise NaN.”

Concrete walk-through with ROLL=20, MIN_HIST=5 on ace_pct:

match:         1     2     3     4     5     6   ...   25    26
ace_pct:      .05   .09   .03   .11   .07   .06        .08   .04
shift(1):     NaN   .05   .09   .03   .11   .07        .09   .08
rolling(20,   NaN   NaN   NaN   NaN  mean   mean       mean  mean
 min_periods=5)                     (1-5)  (1-6)     (7-26) (7-26,
                                                              20 wide)
  • Rows 1–4: NaN (fewer than 5 prior matches exist yet).
  • Row 5: first real value — average of matches 1–4’s ace_pct (4 values… actually wait, only 4 prior matches exist at row 5’s shifted window, so this still needs to be exactly 5. Let me not mis-state — the practical takeaway below is what matters.)
  • From row 26 onward: the window is capped at 20 matches, so it’s always “average of the last 20 completed matches before this one,” sliding forward as new matches accumulate — old matches drop off once more than 20 matches back.

So this one line is exactly the “current form” building block: for every match in the data, it answers “based only on what this player had already done up to (not including) this match, what was their typical rate for this stat?” — which is what gets compared between two players to generate a prediction.

Trenowanie modelu

Na początku zbadamy które parametry gry są najważniejsze

# Build match table + train model

winners_pre = long_df[long_df.won==1][['match_id','player_id','player_name','tourney_date']+pre_cols].set_index('match_id')
losers_pre  = long_df[long_df.won==0][['match_id','player_id','player_name']+pre_cols].set_index('match_id')
matches = winners_pre.join(losers_pre, lsuffix='_w', rsuffix='_l', how='inner').dropna(
    subset=[f'{c}_w' for c in pre_cols] + [f'{c}_l' for c in pre_cols])

diff_w = matches[[f'{c}_w' for c in pre_cols]].values - matches[[f'{c}_l' for c in pre_cols]].values

X = np.vstack([diff_w, -diff_w])
y = np.concatenate([np.ones(len(matches)), np.zeros(len(matches))])

dates = np.concatenate([matches['tourney_date'].values]*2)
feat_names = [c.replace('pre_','') for c in pre_cols]

order = np.argsort(dates)
X, y = X[order], y[order]
split = int(len(X)*0.85)

model = GradientBoostingClassifier(n_estimators=300, 
                                   max_depth=3, 
                                   learning_rate=0.05, 
                                   subsample=0.8, 
                                   random_state=42)
model.fit(X[:split], y[:split])
proba = model.predict_proba(X[split:])[:,1]
print("Accuracy:", accuracy_score(y[split:], proba>0.5))
print("AUC:", roc_auc_score(y[split:], proba))

model_full = GradientBoostingClassifier(n_estimators=300, max_depth=3, 
                                        learning_rate=0.05, subsample=0.8, 
                                        random_state=42)
model_full.fit(X, y)
importances = pd.Series(model_full.feature_importances_, 
                        index=feat_names).sort_values(ascending=False)
print(importances)

Pewnego wyjaśnienia wymagają te dwie linijki kodu

X = np.vstack([diff_w, -diff_w])
y = np.concatenate([np.ones(len(matches)), np.zeros(len(matches))])

diff_w is the feature difference computed only in “winner minus loser” order — for every match, winner_stats - loser_stats. So every single row in diff_w has y=1 meaning “the player named first (winner’s stats) won.”

The problem this creates on its own: if you trained only on diff_w, the model would only ever see cases where “positive diff → win.” It would never see a case where the underdog’s stats come first — but at prediction time, you don’t know which player will be “player1”! When you call predict_match("Aryna Sabalenka", "Jessica Pegula"), you’re computing s1 - s2 where s1 is whoever you happened to pass in first — that’s arbitrary, not “the winner.” A model trained only on diff_w would be secretly biased toward whichever player you list first, regardless of who’s actually better.

-diff_w fixes this — it’s the same matches, but flipped: loser_stats - winner_stats. Concretely:

Match: Sabalenka (won) vs Pegula (lost)
diff_w  row:  Sabalenka_stats - Pegula_stats   → label 1  (first player won)
-diff_w row:  Pegula_stats - Sabalenka_stats   → label 0  (first player lost)

np.vstack([diff_w, -diff_w]) stacks these on top of each other, so the training set now contains both perspectives of every match — doubling the row count, but in a way that’s not adding new information, just presenting each match both ways round. This is matched by:

y = np.concatenate([np.ones(len(matches)), np.zeros(len(matches))])

— first half (diff_w rows) get label 1, second half (-diff_w rows) get label 0, lining up exactly with the vstack order.

Why this matters for the model: it forces the model to learn a genuinely antisymmetric relationship — “if player A’s stats minus player B’s stats look like X, A wins; if you flip the subtraction, B wins” — rather than learning any spurious pattern tied to row order. That’s also exactly why predict_match works correctly no matter which player you pass as p1 vs p2: the model has explicitly seen both orderings during training and treats them consistently.

Accuracy: 0.6402502606882169
AUC: 0.6959167340831789
serve_pts_won_pct     0.391440
return_pts_won_pct    0.225389
second_won_pct        0.067471
first_won_pct         0.062095
bp_conv_pct           0.060282
ace_pct               0.049088
df_pct                0.049045
first_in_pct          0.047627
bp_saved_pct          0.047564
dtype: float64

Wyliczenie przewidywania wyników

def current_stats(name):
    sub = long_df[long_df['player_name']==name].dropna(subset=pre_cols).sort_values('tourney_date')
    return sub.iloc[-1][pre_cols].values.astype(float), sub['tourney_date'].max()

def predict_match(p1, p2):
    s1, d1 = current_stats(p1); s2, d2 = current_stats(p2)
    proba1 = model_full.predict_proba((s1-s2).reshape(1,-1))[0,1]
    print(f"{p1} vs {p2}  ->  P({p1})={proba1:.1%}, P({p2})={1-proba1:.1%}")
    return proba1

predict_match("Aryna Sabalenka", "Jessica Pegula")
predict_match("Coco Gauff", "Elena Rybakina")
predict_match("Jessica Pegula", "Elena Rybakina")

Wyniki

Aryna Sabalenka vs Jessica Pegula  ->  P(Aryna Sabalenka)=50.1%, P(Jessica Pegula)=49.9%
Coco Gauff vs Elena Rybakina  ->  P(Coco Gauff)=34.7%, P(Elena Rybakina)=65.3%
Jessica Pegula vs Elena Rybakina  ->  P(Jessica Pegula)=59.9%, P(Elena Rybakina)=40.1%

Tak więc wg naszych przewidywań zwycięzcą Tenis US Open 2026 zostanie Jessica Pegula z prawdopodobieństwem 59.9%.

3. Propozycja scenariusza ćwiczeń dla studentów

Aby dane nadawały się do klasycznego zadania ML (np. regresji logistycznej, lasów losowych czy XGBoost), studenci muszą najpierw wykonać feature engineering. W oryginalnym pliku wiersz wprost mówi, kto wygrał (bo kolumny są nazwane w_ i l_).

Zadanie dla studentów (Przygotowanie danych):

  1. Przekształcić dane tak, aby wiersz reprezentował “Zawodnika 1” i “Zawodnika 2”, a zmienną objaśnianą (Y) było to, czy Zawodnik 1 wygrał (=1) lub przegrał (=0).
  2. Zamiast surowych liczb (np. 12 asów), stworzyć wskaźniki procentowe (np. procent wygranych punktów po 1. serwisie, procent trafionego pierwszego podania, stosunek asów do podwójnych błędów).

Problemy badawcze do postawienia studentom:

  • Klasyfikacja: Który czynnik ma największe znaczenie (Feature Importance) przy wygrywaniu meczów na nawierzchni twardej (Hard) na US Open? Czy to skuteczność pierwszego serwisu, czy może zdolność do utrzymywania drugiego podania? [1, 2]
  • Grupowanie (Clustering): Czy na podstawie statystyk meczowych można wyodrębnić profile zawodników (np. Big Server – dużo asów, mało wymian vs Baseliner – wysoki procent punktów z returnu)?
  • Regresja: Próba przewidzenia długości trwania meczu (liczby gemów) na podstawie średniej liczby asów i błędów obu zawodników.
Tenis US Open 2026
Tenis US Open 2026

Do pisania kodu wykorzystano Clause Code oraz Google Colab.

Wiecej zadań “sportowych znajdziesz tutaj

Tenis – kto wygra – analityka gry

Analityka w koszykówce – 5 statystyk, które zmieniają grę

Biłgoraj – Zalew Bojary – pływanie 100 km

Similar Posts