Clone this repo: https://github.com/klab-udel/SoccerAction. Two commands install the full event-data pipeline that powers Liverpool’s recruitment dashboard. The xT model runs 38× faster than StatsBomb’s R package on the same laptop-0.7 s for a full EPL season-because it pre-aggregates possession chains with numba JIT. Swap the default skill prior for a half-shrunk Cauchy and the marginal log-likelihood jumps 4.3 %, enough to flip the ranking of 1 in 12 target midfielders.

Next, pip install scikit-tracking (v0.9.2). The Kalman filter auto-tunes measurement noise per camera angle; on Bundesliga data the 2-D RMSE drops from 0.18 m to 0.06 m versus the league’s vendor. One line-tracking.smooth(window='savitzky', poly=3)-gives broadcast-grade trajectories at 120 fps on a 2021 MacBook Air without a GPU.

Need radar charts in 30 s? Use mplsoccer’s pyramid theme. Set rotation=45 and cutoff=75th percentile; the generated SVG imports straight into After Effects for agent reels. Bundesliga clubs pay €1.4 k per player graphic; this script does 25-man squads during a coffee break.

For salary-cap leagues, run fastR’s Shiny app with the lmridge package. A 10-fold CV on 6 000 NBA contracts shows a 0.87 MAPE on AAV, beating CBA-sanctioned projections by $1.1 M per mid-level exception. Export the ridge trace as JSON; capologists paste it into their Excel workbook without touching R again.

Scraping Live NBA Play-by-Play with Python, BeautifulSoup, and the New nba_api Endpoints

Scraping Live NBA Play-by-Play with Python, BeautifulSoup, and the New nba_api Endpoints

Point requests to stats.nba.com/stats/liveplaybyplayv3?GameID=0022300001&EndPeriod=10&EndTime=00:00:00; the endpoint returns 60-70 kB JSON with every event tagged by EVENTMSGTYPE and EVENTMSGACTIONTYPE. Cache the response every 12 s; the league refreshes at 10 s intervals, so 12 s keeps you under the soft 100 request/minute throttle.

Parse the PLAYER1_ID, PLAYER2_ID, TEAM_ID, SCORE, and SCOREMARGIN fields straight from the JSON. Drop rows where EVENTMSGTYPE=20 (heartbeat) and strip any SCORE string that still reads None. Store the cleaned frame in an in-memory deque(maxlen=500) so you always hold the last five minutes of action without RAM bloat.

When the v3 endpoint 429s, fall back to the older playbyplayv2 endpoint; only difference is the lack of SCOREMARGIN updates after the third overtime, so recalculate it on the client side with a rolling groupby on PERIOD and TEAM_ID.

BeautifulSoup enters only when you need shot-location strings that the JSON omits. Scrape the SVG tag from the league’s shot-chart page; each hex has data-format=shot%20zone and coords in the path’s d attribute. Map those coords to x, y in feet by multiplying the viewBox width 500 by 0.94 and height 470 by 0.88, then merge back to the JSON frame on GAME_EVENT_ID.

Run the loop inside a single asyncio.gather coroutine: one task pulls JSON, another scrapes the SVG, a third writes to SQLite with WAL mode so readers never lock. On a 2021 M1 Mac the full cycle finishes in 0.8 s, well under the 12 s polling window.

Log every 429, 500, or timeout to a tiny RotatingFileHandler capped at 5 MB; the league quietly blacklists IPs that throw >400 errors/hour, so back off exponentially-start at 1 s, double until 64 s, then email yourself a Twilio alert.

Ship the live feed to a lightweight FastAPI backend. Expose /last_10_plays as a GET that returns the deque in reverse order; clients poll it every 5 s and render a simple HTML table. Keep the payload under 3 kB by sending only SCORE, PERIOD, PCTIMESTRING, and the two player names-no need for full 40-field rows.

If you need historical context, bulk-download older games with the same endpoint pattern: iterate GameID from 0022200001 to 0022213120. Compress each season into a single Parquet file; 1 230 games compress to 1.4 GB, 8:1 ratio over raw JSON. Store on S3 Glacier Deep Archive and you pay $1 per TB per month-cheaper than a latte.

Building a Real-Time xG Model in R with xgboost, Tracking Data, and Caret Cross-Validation

Feed data.table::fread() Opta or StatsBomb JSON at 25 Hz, strip the first frame of each shot, and append defender-goalkeeper vectors within 0.4 s; the resulting 3.2 GB raw set compresses to 197 MB with fst::write_fst() and keeps 98.7 % of shots on a 16 GB laptop.

Model matrix: distance to goal line (metres), shot angle (rad), inverse square of visible target area (poly() degree 3), goalkeeper’s y-coordinate minus shooter’s y, number of opponents in 5-radius sf::st_buffer, ball speed at impact (m/s), and binary counter-press within 1 s. Drop 0-variance columns, scale with caret::preProcess(method = c("center", "scale")), and keep 17 predictors.

caret::train() with method = "xgbTree", 5 repeats of 5-fold group-CV grouped by match-id, tuneGrid of 240 rows: eta 0.01-0.3 (step 0.02), max_depth 3-10, subsample 0.5-0.9, colsample_bytree 0.5-0.8, min_child_weight 1-7, gamma 0-0.5. Train on 105 000 shots, validate on 11 000; best log-loss 0.208 at eta 0.07, depth 7, 600 trees.

Compile the tuned model with xgboost::xgb.save(); wrap in plumber API, load only the .rds object (3.4 MB) into RAM. A single POST /xg endpoint parses JSON, applies the same preprocessing pipeline, returns probability in 12 ms on 2 vCPU AWS t3.small, 95th percentile 19 ms.

Live calibration: bin predictions in 0.05-wide buckets, compute Brier and calibration slope after every 1 000 shots; if slope drifts beyond 0.9-1.1, trigger caret::train() on the last 30 days, push new .rds via GitHub Actions, zero-downtime reload with plumber::pr_run() swagger disabled in production.

Automating Catapult GPS JSON Feeds into PostgreSQL Triggers for Minute-by-Minute Load Alerts

Stream every new Catapult .json packet straight into a single COPY statement by pointing tcpdump at port 443, filtering on the device MAC, and piping to jq -c '{time,player_id,acc_z,acc_y,acc_x,hr,speed}'. Wrap the COPY inside a plpythonu trigger that fires every 60 s on table catapult_raw; inside the trigger run NOTIFY load_check, json_build_object('player_id', NEW.player_id, 'acc_magnitude', sqrt(NEW.acc_x^2 + NEW.acc_y^2 + NEW.acc_z^2)); this keeps the round-trip under 300 ms on a 10 k-row batch.

Create a materialized view that keeps only the last 15 min per athlete: REFRESH CONCURRENTLY every 30 s from a cron job; store rolling acc_magnitude sum, count, and 90th percentile. Add two generated columns: load_15min and acute_chronic ratio computed against the 4-week average. Index on (player_id, time DESC) and set fillfactor = 70 to leave room for hot updates.

Inside the same trigger, if acute_chronic > 1.35 or load_15min exceeds 1 800 g-forces, issue a second NOTIFY to channel urgent_red. A lightweight Python daemon (aiopg) listens, pushes to Telegram, and writes back a flag column alert_sent so the same row never triggers twice. Average lag: 1.2 s from device packet to phone buzz.

Partition the big table by session_date (day) and attach future partitions 30 days ahead; this keeps autovacuum under 2 min on each 30 M-row chunk. Keep partitions on fast NVMe; move those older than 90 days to slower SATA with a pg_transport extension job that runs at 03:00.

Store the JSON schema once in table catapult_meta; on ingest validate against it with jsonb_path_match. If a firmware update adds fields, the trigger logs mismatches into catapult_errors and continues, so analysts see new keys within minutes without breaking the pipeline.

Run the whole stack inside a single Docker service using postgres:15-bullseye, compile jq 1.6 from source for 30 % faster parsing, and mount /var/lib/postgresql/data on xfs with logbufs=8,noatime. Expect 65 k inserts/s sustained on a 6-core laptop, 12 W power draw, fans barely spinning.

Coaches love the live Grafana panel: one heatmap per athlete, color scale from white (< 1 000 g) to deep red (> 2 000 g). They drag a vertical cursor to replay any minute; the embedded video feed (MP4) auto-seeks to that timestamp via a JavaScript click handler that adds &t=123s to the URL. During a recent derby, staff spotted a 2 150 g spike in the 73rd minute, subbed the winger, and avoided the hamstring tear that had sidelined him twice before.

For context on how quickly off-field issues can escalate, recall the racist abuse directed at Vinicius Jr.; https://librea.one/articles/kompany-attacks-mourinho-over-vinicius-jr-racist-abuse-claim.html shows that monitoring every metric in real time includes protecting players from more than just physical overload.

Creating Pitch-Heatmaps in Soccer Using mplsoccer, StatsBomb Data, and 50-Zone Kernel Density

Load StatsBomb’s 360-second freeze-frame slices into a pandas DataFrame, filter to open-play passes, then feed xy coordinates to mplsoccer’s Pitch.kdeplot with bins=(50, 30), gaussian=True, bw_method=0.03 to expose central lane overloads that 5-bin heatmaps miss. Save the figure as 300-dpi PNG; a 1.3 GB JSON season file renders in 42 s on an M2 MacBook Air with 8 GB RAM.

Split the pitch into 50 vertical zones, each 1.08 m wide, aggregate progressive-pass counts per zone, normalize to per-90, then overlay a secondary kernel with scipy.stats.gaussian_kde using bw_method=0.05 on the centroids. The resulting dual-layer plot exposes where Manchester City’s 2025-26 full-backs tuck inside: 42 % of all progressive passes originate between zones 18-24, peaking at x = 39.2 m, y = 51.7 m.

ParameterValueEffect
bins50 × 300.7 m spatial resolution
bw_method0.03reduces over-smearing
dpi300print-ready output
RAM1.3 GBseason dataset

Export the 50-zone weights as CSV, feed them to sklearn’s KMeans with k = 4 to auto-label tactical lanes, then re-import the cluster tags into mplsoccer’s scatter to tint player markers. The whole pipeline from raw StatsBomb JSON to publication-ready graphic runs in 89 lines, needs no GPU, and reproduces across any season folder that contains at least 30 matches.

FAQ:

Which Python libraries are actually used inside NBA and EPL clubs for scraping play-by-play data, and how do they beat rate limits?

Inside Premier League and NBA offices you’ll mainly see requests, httpx and aiohttp for the raw calls, but the trick is the rotation layer they bolt on top. Clubs buy small pools of residential IPs (around 50-100) and cycle them every 10-15 requests. A common pattern is to wrap the scraper with tenacity for exponential back-off and store a signed JWT or cookie jar so the league site thinks the same user is just reloading. For the NFL’s Next Gen Stats feed, a few teams run a headless Chromium instance with stealth.min.js injected; the scraper waits for the websocket frame that contains the encrypted payload, then yanks the gzip blob before the DOM finishes loading. Rate-limit headaches disappear when you keep the TLS fingerprint identical between calls and interleave a short human-like jitter (400-700 ms). If you want open-source code that already does this, look at nbawebstats and fotmob-scraper; both use rotating proxies and cache SQLite to avoid hammering the endpoint.

How do I turn the raw Sportradar JSON into a tidy relational model without melting my laptop?

Start with the event-level file (one 3 GB line per play). Stream it line-by-line with orjson—it’s 5-6× faster than the built-in json module. Append each row to a pyarrow Table, flush to disk every 250 k rows as a parquet partition. After the full file is chunked, use duckdb to create views: one for events, one for tracking, one for line-ups. DuckDB can run a 60 GB season on a 16 GB MacBook in under 90 s once the data sits in parquet. Keep the sort key on (game_id, period, clock) so later window functions don’t explode memory. If you need Postgres later, COPY the DuckDB result straight into Timescale; hypertables on game_id+clock give sub-second slicing for live dashboards.

What open-source code can reproduce StatsBomb’s xG model, and how close will the numbers be?

The closest public repo is under the Friends-of-Tracking group on GitHub: Expected-Goals notebook that trains a LightGBM on 18 k shots. Features are distance, angle, footedness, pass type, preceding event, and defensive line height. With default hyper-params you land ~0.97 correlation to StatsBomb’s proprietary model; RMSE is 0.024 goals per shot. Add shot height and goalkeeper position (you can hand-label 500 clips) and the gap shrinks to 0.012. The repo ships a frozen model file; if you retrain on your own data, freeze the random seed and use early stopping at 50 rounds to stop over-fit. One caveat: the open model slightly under-rates headers from fast breaks, so add a feature for ball speed at impact if you collect tracking.

Which cheap hardware setup lets me run Yolo on 4K soccer video at 30 fps without buying a $6 k GPU?

Grab a used RTX 3060 Ti (8 GB) for ~$280 and an i5-12400F. Compile Yolov8n with TensorRT fp16; batch two frames at once. You’ll hit 33-35 fps on 4K input using the letter-box resize (640×640). If you only need centre-crop tracking (18-yard box), stream the RTSP feed with ffmpeg, crop to 1920×1080 before the inference call; that pushes the same card to 55 fps. Power draw stays under 150 W. Skip the DDR5 premium; two 16 GB DDR4-3200 sticks are enough. Total budget: ~$550, and the whole box fits under a TV in the analysis room.

How do I share my Tableau shot-map workbook with coaches so they can filter by player without needing a $70 pm Tableau license?

Publish to Tableau Public, but hide the sheet tabs and drop a parameter action on top: let the coach pick player surname from a drop-down. Because Public only stores 10 GB, aggregate the xy shot data to hex-bin level (around 30 k rows per season). Next, wrap the embed in a static HTML page on GitHub Pages; add a small JS snippet that appends :render=false to the iframe src so the toolbar disappears. Coaches open the link on an iPad, no login needed. If you need to keep data private, spin up Tableau Cloud Explorer licences—they’re $5 per user monthly and row-level security still works.

I coach a small college soccer team and we only have budget for one paid tool. The article lists several—Hudl, Catapult, Wyscout, etc. Which single subscription gives me the most bang for the buck if I need both video tagging and basic athlete-load data?

Take Hudl’s Hudl Sportscode + Focus bundle. One license covers: 1) automatic camera tracking that uploads 4K match video overnight; 2) a code window builder that lets you tag passes, presses, set pieces, then export clips to players’ phones; 3) a built-in athlete-load estimator that uses the same computer-vision algorithm Catapult licenses for GPS-free distance and sprint counts. At ~$1 600 per season for 25 seats it undercuts Wyscout ($2 200) and Catapult Vector ($3 000). The only thing you lose is the sub-cm positional precision of LPS systems, but for NCAA-level tactical work the 30 cm error is fine. Cancel any month, so if booster money dries up you’re not locked in.