From 7093b2272665b53efe09891e635c960e994ace26 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:37:20 -0700 Subject: [PATCH] Simulate NBA remaining regular season games from standings (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace static Basketball-Reference seed probability distributions (p_1..p_10) with dynamic simulation of each team's remaining games based on current standings from the DB. - Load regularSeasonStandings in parallel with participants query - Compute remainingGames = 82 - gamesPlayed per team - Pre-compute per-game win probability (Elo vs average opponent 1500) on TeamEntry at construction time — not inside the hot loop - Sort conference standings by projected wins to assign seeds, replacing the drawSeed() weighted-probability approach - Conference resolved from standings table, falls back to TEAMS_DATA - Strip all p_1..p_10 seed probability data from TEAMS_DATA - Move simulateProjectedWins to module scope (no closure captures) - Remove const N alias; use NUM_SIMULATIONS directly Co-authored-by: Claude Sonnet 4.6 --- app/services/simulations/nba-simulator.ts | 336 +++++++++------------- 1 file changed, 134 insertions(+), 202 deletions(-) diff --git a/app/services/simulations/nba-simulator.ts b/app/services/simulations/nba-simulator.ts index 89a658d..e068f96 100644 --- a/app/services/simulations/nba-simulator.ts +++ b/app/services/simulations/nba-simulator.ts @@ -6,11 +6,13 @@ * * Algorithm: * 1. Load all participants for the sports season from DB - * 2. Match participant names to hardcoded team data (Elo + seed probabilities) - * 3. For each simulation: - * a. Assign each team a seed based on its weighted probability distribution (p_1..p_10) - * Teams with no seed probabilities always miss the playoffs (seed = 11) - * b. Sort each conference by drawn seed + random tiebreaker + * 2. Load current regular season standings (wins, gamesPlayed, conference) + * 3. Match participant names to hardcoded team data (Elo ratings) + * 4. For each simulation: + * a. For each team, simulate remaining regular season games (82 - gamesPlayed) + * using Elo win probability vs. an average opponent (Elo 1500) + * → projectedWins = currentWins + simulatedRemainingWins + * b. Sort each conference by projected wins (desc) + random tiebreaker → seeds 1–10 * → Top 6 lock in directly; seeds 7–10 enter the Play-In tournament * c. Simulate Play-In (single game each): * - Game 1: seed 7 vs seed 8 → winner becomes 7th playoff seed @@ -21,12 +23,17 @@ * Round 2: Conference Semis (winners of 1v8/4v5, winners of 2v7/3v6) * Round 3: Conference Finals * NBA Finals: East champion vs West champion - * 4. Track placement counts per scoring tier - * 5. Convert counts to probability distributions + * 5. Track placement counts per scoring tier + * 6. Convert counts to probability distributions * * Win probability (Elo, PARITY_FACTOR = 400): * P(A beats B) = 1 / (1 + 10^((eloB - eloA) / 400)) * + * Regular season projection: + * Per-game win probability = eloWinProbability(teamElo, 1500) where 1500 = average opponent. + * If no standings exist in DB, defaults to 0 wins / 82 remaining games (seeding by Elo only). + * Conference is read from standings table; falls back to TEAMS_DATA if missing. + * * Placement tiers → SimulationProbabilities mapping: * probFirst = NBA champion (1 per sim) * probSecond = NBA Finals loser (1 per sim) @@ -35,9 +42,9 @@ * Round 1 losers → all 0 (score 0 points) * Missed playoffs → all 0 * - * Elo ratings and seed probabilities are hardcoded below (March 2026 data). - * Source: Basketball-Reference Playoff Probabilities + Neil Paine Substack estimates. - * Update at the start of each season. + * Elo ratings are hardcoded below (March 2026 data). + * Source: Neil Paine Substack playoff Elo estimates (last 110 games, no regression, + * postseason games 3× weight). Update at the start of each season. */ import { database } from "~/database/context"; @@ -45,6 +52,7 @@ import { eq } from "drizzle-orm"; import * as schema from "~/database/schema"; import type { Simulator, SimulationResult } from "./types"; import { normalizeTeamName } from "~/lib/normalize-team-name"; +import { getRegularSeasonStandings } from "~/models/regular-season-standings"; // ─── Simulation parameters ──────────────────────────────────────────────────── @@ -56,158 +64,59 @@ const NUM_SIMULATIONS = 50_000; */ const PARITY_FACTOR = 400; -/** Seed probability keys — ordered p_1..p_10 for the drawSeed() inner loop. */ -const SEED_KEYS = ["p_1", "p_2", "p_3", "p_4", "p_5", "p_6", "p_7", "p_8", "p_9", "p_10"] as const; +/** NBA regular season games per team. */ +const NBA_REGULAR_SEASON_GAMES = 82; -// ─── Team data (2025-26 season, as of March 6, 2026) ───────────────────────── +// ─── Team data (2025-26 season, as of March 2026) ───────────────────────────── // -// elo: Estimated Elo rating (higher = stronger). -// p_1 through p_10: Probability of finishing at each conference seed. -// Sum of all p_X for a team = probability of making the playoffs. -// Teams with no p_X entries always miss the playoffs in simulation. +// elo: Estimated Elo rating (higher = stronger). +// Source: Playoff rating (last 110 games, no regression to mean, postseason games 3× weight). +// This is the appropriate signal for simulating both regular season win probability +// (vs. average opponent) and playoff matchups. // -// Sources: -// - Elo: Playoff rating (last 110 games, no regression to mean, postseason games 3× weight). -// This is the appropriate signal for simulating playoff matchups. -// - Seed probs: Basketball-Reference Playoff Probabilities Report (March 16, 2026). -// -// Seed probability methodology: -// BBRef reports conditional seed probabilities (summing to ~100% per team). -// We scale each team's values by their Playoffs% to get unconditional probabilities. -// The drawSeed() function then treats the remainder (1 - sum) as "miss playoffs". -// Formula: p_X = (BBRef_conditional_X / 100) × (Playoffs% / 100) +// conference: Used as a fallback when the standings table has no conference data. interface NbaTeamData { conference: "Eastern" | "Western"; elo: number; - p_1?: number; - p_2?: number; - p_3?: number; - p_4?: number; - p_5?: number; - p_6?: number; - p_7?: number; - p_8?: number; - p_9?: number; - p_10?: number; } const TEAMS_DATA: Record = { // ── Eastern Conference ────────────────────────────────────────────────────── - // Elo = playoff rating (last 110 games, no regression, postseason games 3× weight) - // Seed probs = BBRef conditional × (Playoffs% / 100) - // Detroit (PO%=100%): locked as 1-seed - "Detroit Pistons": { conference: "Eastern", elo: 1558, - p_1: 0.982, p_2: 0.016, p_3: 0.001 }, - - // Boston (PO%=100%): clear 2/3 seed - "Boston Celtics": { conference: "Eastern", elo: 1699, - p_1: 0.014, p_2: 0.540, p_3: 0.389, p_4: 0.052, p_5: 0.004 }, - - // New York (PO%=100%) - "New York Knicks": { conference: "Eastern", elo: 1626, - p_1: 0.003, p_2: 0.423, p_3: 0.479, p_4: 0.086, p_5: 0.009, p_6: 0.001 }, - - // Cleveland (PO%=99.9%): mostly 4-seed, slight play-in risk - "Cleveland Cavaliers": { conference: "Eastern", elo: 1628, - p_2: 0.020, p_3: 0.117, p_4: 0.684, p_5: 0.137, p_6: 0.032, p_7: 0.009, p_8: 0.001 }, - - // Orlando (PO%=88.8%): 5/6-seed range, play-in risk - "Orlando Magic": { conference: "Eastern", elo: 1508, - p_3: 0.007, p_4: 0.075, p_5: 0.333, p_6: 0.182, p_7: 0.139, p_8: 0.091, p_9: 0.044, p_10: 0.018 }, - - // Miami (PO%=78.7%) - "Miami Heat": { conference: "Eastern", elo: 1530, - p_3: 0.004, p_4: 0.041, p_5: 0.212, p_6: 0.224, p_7: 0.255, p_8: 0.113, p_9: 0.037, p_10: 0.009 }, - - // Toronto (PO%=86.4%) - "Toronto Raptors": { conference: "Eastern", elo: 1467, - p_3: 0.001, p_4: 0.038, p_5: 0.165, p_6: 0.324, p_7: 0.192, p_8: 0.103, p_9: 0.041, p_10: 0.011 }, - - // Atlanta (PO%=46.7%): mostly play-in range - "Atlanta Hawks": { conference: "Eastern", elo: 1496, - p_4: 0.001, p_5: 0.011, p_6: 0.022, p_7: 0.058, p_8: 0.124, p_9: 0.134, p_10: 0.123 }, - - // Philadelphia (PO%=52.9%) - "Philadelphia 76ers": { conference: "Eastern", elo: 1471, - p_5: 0.011, p_6: 0.039, p_7: 0.079, p_8: 0.116, p_9: 0.134, p_10: 0.091 }, - - // Charlotte (PO%=47.1%): deep play-in territory - "Charlotte Hornets": { conference: "Eastern", elo: 1496, - p_5: 0.002, p_6: 0.004, p_7: 0.017, p_8: 0.058, p_9: 0.118, p_10: 0.201 }, - - // Milwaukee (PO%=0%) - "Milwaukee Bucks": { conference: "Eastern", elo: 1442 }, - - // Chicago (PO%=0%) - "Chicago Bulls": { conference: "Eastern", elo: 1381 }, - - // Brooklyn (PO%=0%) - "Brooklyn Nets": { conference: "Eastern", elo: 1334 }, - - // Indiana (PO%=0%) - "Indiana Pacers": { conference: "Eastern", elo: 1433 }, - - // Washington (PO%=0%) - "Washington Wizards": { conference: "Eastern", elo: 1255 }, + "Detroit Pistons": { conference: "Eastern", elo: 1558 }, + "Boston Celtics": { conference: "Eastern", elo: 1699 }, + "New York Knicks": { conference: "Eastern", elo: 1626 }, + "Cleveland Cavaliers": { conference: "Eastern", elo: 1628 }, + "Orlando Magic": { conference: "Eastern", elo: 1508 }, + "Miami Heat": { conference: "Eastern", elo: 1530 }, + "Toronto Raptors": { conference: "Eastern", elo: 1467 }, + "Atlanta Hawks": { conference: "Eastern", elo: 1496 }, + "Philadelphia 76ers": { conference: "Eastern", elo: 1471 }, + "Charlotte Hornets": { conference: "Eastern", elo: 1496 }, + "Milwaukee Bucks": { conference: "Eastern", elo: 1442 }, + "Chicago Bulls": { conference: "Eastern", elo: 1381 }, + "Brooklyn Nets": { conference: "Eastern", elo: 1334 }, + "Indiana Pacers": { conference: "Eastern", elo: 1433 }, + "Washington Wizards": { conference: "Eastern", elo: 1255 }, // ── Western Conference ────────────────────────────────────────────────────── - // OKC (PO%=100%): dominant 1-seed - "Oklahoma City Thunder": { conference: "Western", elo: 1731, - p_1: 0.920, p_2: 0.081 }, - - // San Antonio (PO%=100%): locked as 2-seed - "San Antonio Spurs": { conference: "Western", elo: 1599, - p_1: 0.081, p_2: 0.919 }, - - // Houston (PO%=99.8%): 3/4 seed range - "Houston Rockets": { conference: "Western", elo: 1564, - p_3: 0.467, p_4: 0.258, p_5: 0.169, p_6: 0.092, p_7: 0.014, p_8: 0.001 }, - - // Denver (PO%=99.8%) - "Denver Nuggets": { conference: "Western", elo: 1618, - p_3: 0.222, p_4: 0.270, p_5: 0.277, p_6: 0.181, p_7: 0.043, p_8: 0.001 }, - - // LA Lakers (PO%=99.7%) - "Los Angeles Lakers": { conference: "Western", elo: 1569, - p_3: 0.219, p_4: 0.267, p_5: 0.242, p_6: 0.172, p_7: 0.079, p_8: 0.003 }, - - // Minnesota (PO%=96.2%) - "Minnesota Timberwolves": { conference: "Western", elo: 1603, - p_3: 0.072, p_4: 0.154, p_5: 0.222, p_6: 0.340, p_7: 0.173, p_8: 0.007 }, - - // Phoenix (PO%=83.4%): mostly 7-seed play-in entry - "Phoenix Suns": { conference: "Western", elo: 1500, - p_3: 0.011, p_4: 0.032, p_5: 0.065, p_6: 0.165, p_7: 0.517, p_8: 0.051, p_9: 0.005 }, - - // LA Clippers (PO%=71.1%): heavy play-in range - "LA Clippers": { conference: "Western", elo: 1573, - p_6: 0.042, p_7: 0.468, p_8: 0.112, p_9: 0.027 }, - - // Golden State (PO%=30.3%): longshot play-in - "Golden State Warriors": { conference: "Western", elo: 1530, - p_6: 0.003, p_7: 0.053, p_8: 0.160, p_9: 0.147 }, - - // Portland (PO%=19.5%): deep longshot - "Portland Trail Blazers": { conference: "Western", elo: 1426, - p_7: 0.013, p_8: 0.077, p_9: 0.111 }, - - // Dallas (PO%=0%) - "Dallas Mavericks": { conference: "Western", elo: 1473 }, - - // Memphis (PO%=0%) - "Memphis Grizzlies": { conference: "Western", elo: 1417 }, - - // New Orleans (PO%=0%) - "New Orleans Pelicans": { conference: "Western", elo: 1380 }, - - // Sacramento (PO%=0%) - "Sacramento Kings": { conference: "Western", elo: 1352 }, - - // Utah (PO%=0%) - "Utah Jazz": { conference: "Western", elo: 1334 }, + "Oklahoma City Thunder": { conference: "Western", elo: 1731 }, + "San Antonio Spurs": { conference: "Western", elo: 1599 }, + "Houston Rockets": { conference: "Western", elo: 1564 }, + "Denver Nuggets": { conference: "Western", elo: 1618 }, + "LA Lakers": { conference: "Western", elo: 1569 }, + "Minnesota Timberwolves":{ conference: "Western", elo: 1603 }, + "Phoenix Suns": { conference: "Western", elo: 1500 }, + "LA Clippers": { conference: "Western", elo: 1573 }, + "Golden State Warriors": { conference: "Western", elo: 1530 }, + "Portland Trail Blazers":{ conference: "Western", elo: 1426 }, + "Dallas Mavericks": { conference: "Western", elo: 1473 }, + "Memphis Grizzlies": { conference: "Western", elo: 1417 }, + "New Orleans Pelicans": { conference: "Western", elo: 1380 }, + "Sacramento Kings": { conference: "Western", elo: 1352 }, + "Utah Jazz": { conference: "Western", elo: 1334 }, }; // ─── Public helpers (exported for unit testing) ─────────────────────────────── @@ -238,6 +147,13 @@ interface TeamEntry { id: string; name: string; data: NbaTeamData | undefined; + conference: "Eastern" | "Western"; + /** Actual wins from the standings table (0 if no standings loaded). */ + currentWins: number; + /** Remaining regular season games = 82 - gamesPlayed (0 if season is complete). */ + remainingGames: number; + /** Elo win probability vs. average opponent (1500) — constant per team. */ + winProb: number; } /** Get Elo for a team entry. @@ -246,17 +162,31 @@ function elo(entry: TeamEntry): number { return entry.data?.elo ?? 1400; } +/** Simulate remaining regular season games for a team. + * Uses the pre-computed per-team winProb (Elo vs. average opponent). + * Returns projected total wins for the season. */ +function simulateProjectedWins(entry: TeamEntry): number { + let extra = 0; + for (let g = 0; g < entry.remainingGames; g++) { + if (Math.random() < entry.winProb) extra++; + } + return entry.currentWins + extra; +} + // ─── Simulator ──────────────────────────────────────────────────────────────── export class NBASimulator implements Simulator { async simulate(sportsSeasonId: string): Promise { const db = database(); - // 1. Load all participants for this sports season. - const participantRows = await db - .select({ id: schema.participants.id, name: schema.participants.name }) - .from(schema.participants) - .where(eq(schema.participants.sportsSeasonId, sportsSeasonId)); + // 1. Load participants and standings in parallel. + const [participantRows, standings] = await Promise.all([ + db + .select({ id: schema.participants.id, name: schema.participants.name }) + .from(schema.participants) + .where(eq(schema.participants.sportsSeasonId, sportsSeasonId)), + getRegularSeasonStandings(sportsSeasonId), + ]); if (participantRows.length === 0) { throw new Error( @@ -265,19 +195,35 @@ export class NBASimulator implements Simulator { ); } - // 2. Match participant names to hardcoded team data. - // Teams with no match or no seed probabilities will always miss the playoffs. + // 2. Build standings lookup and construct team entries. + // Conference, currentWins, remainingGames, and per-game winProb are all + // resolved once here so nothing is recomputed inside the hot loop. + const standingsMap = new Map(standings.map((s) => [s.participantId, s])); const participantIds = participantRows.map((r) => r.id); - const teams: TeamEntry[] = participantRows.map((r) => ({ - id: r.id, - name: r.name, - data: getTeamData(r.name), - })); + const teams: TeamEntry[] = participantRows.map((r) => { + const standing = standingsMap.get(r.id); + const data = getTeamData(r.name); + const gamesPlayed = standing?.gamesPlayed ?? 0; + const conf = standing?.conference; + const conference: "Eastern" | "Western" = + conf === "Eastern" || conf === "Western" + ? conf + : (data?.conference ?? "Eastern"); + return { + id: r.id, + name: r.name, + data, + conference, + currentWins: standing?.wins ?? 0, + remainingGames: Math.max(0, NBA_REGULAR_SEASON_GAMES - gamesPlayed), + winProb: eloWinProbability(data?.elo ?? 1400, 1500), + }; + }); - // Separate by conference for simulation (fall back to Eastern if no data found). - const easternTeams = teams.filter((t) => (t.data?.conference ?? "Eastern") === "Eastern"); - const westernTeams = teams.filter((t) => t.data?.conference === "Western"); + // 3. Separate by conference for simulation. + const easternTeams = teams.filter((t) => t.conference === "Eastern"); + const westernTeams = teams.filter((t) => t.conference === "Western"); // Validate: each conference needs at least 10 teams to fill the bracket + play-in. if (easternTeams.length < 10 || westernTeams.length < 10) { @@ -289,20 +235,6 @@ export class NBASimulator implements Simulator { // ─── Helpers (defined once, outside the hot loop) ───────────────────────── - /** Draw a conference seed (1–10) based on a team's probability distribution. - * Returns 11 if the draw falls outside all p_X values (team misses playoffs). */ - const drawSeed = (entry: TeamEntry): number => { - const data = entry.data; - if (!data) return 11; // Unknown team — always misses playoffs - let r = Math.random(); - for (let i = 0; i < SEED_KEYS.length; i++) { - const prob = data[SEED_KEYS[i]] ?? 0; - r -= prob; - if (r <= 0) return i + 1; - } - return 11; // Missed playoffs - }; - /** Simulate a single playoff game. Returns the winner. */ const simGame = (a: TeamEntry, b: TeamEntry): TeamEntry => Math.random() < eloWinProbability(elo(a), elo(b)) ? a : b; @@ -332,17 +264,18 @@ export class NBASimulator implements Simulator { }; /** Build an 8-team conference bracket [s1..s8] for one simulation iteration. - * Seeds are drawn probabilistically; positions 7–10 go through the Play-In. */ + * Seeds are determined by simulated projected wins; positions 7–10 go through the Play-In. */ const buildConferenceBracket = (confTeams: TeamEntry[]): TeamEntry[] => { - const seeded = confTeams.map((t) => ({ + const projected = confTeams.map((t) => ({ team: t, - seed: drawSeed(t), + projectedWins: simulateProjectedWins(t), tiebreaker: Math.random(), })); - seeded.sort((a, b) => a.seed - b.seed || a.tiebreaker - b.tiebreaker); + // Higher projected wins = better seed (sort descending; random tiebreaker for ties). + projected.sort((a, b) => b.projectedWins - a.projectedWins || b.tiebreaker - a.tiebreaker); - const top6 = seeded.slice(0, 6).map((x) => x.team); - const playIn = seeded.slice(6, 10).map((x) => x.team) as + const top6 = projected.slice(0, 6).map((x) => x.team); + const playIn = projected.slice(6, 10).map((x) => x.team) as [TeamEntry, TeamEntry, TeamEntry, TeamEntry]; const [seed7, seed8] = simPlayIn(playIn); return [...top6, seed7, seed8]; @@ -363,13 +296,13 @@ export class NBASimulator implements Simulator { return { winners: [m1.winner, m2.winner], losers: [m1.loser, m2.loser] }; }; - // 3. Integer placement count maps — initialized to 0 for all participants. + // 4. Integer placement count maps — initialized to 0 for all participants. const championCounts = new Map(participantIds.map((id) => [id, 0])); const finalistCounts = new Map(participantIds.map((id) => [id, 0])); const confFinalLoserCounts = new Map(participantIds.map((id) => [id, 0])); const confSemiLoserCounts = new Map(participantIds.map((id) => [id, 0])); - // 4. Monte Carlo simulation loop. + // 5. Monte Carlo simulation loop. for (let s = 0; s < NUM_SIMULATIONS; s++) { // ── Build brackets ─────────────────────────────────────────────────────── const eastBracket = buildConferenceBracket(easternTeams); @@ -384,9 +317,9 @@ export class NBASimulator implements Simulator { const { winner: champion, loser: finalist } = simSeries(eastChamp, westChamp); - // ── Record counts (maps are pre-populated so .get() is always defined) ─── - championCounts.set(champion.id, (championCounts.get(champion.id) ?? 0) + 1); - finalistCounts.set(finalist.id, (finalistCounts.get(finalist.id) ?? 0) + 1); + // ── Record counts (maps are pre-populated so .get() always returns a number) ─── + championCounts.set(champion.id, (championCounts.get(champion.id) ?? 0) + 1); + finalistCounts.set(finalist.id, (finalistCounts.get(finalist.id) ?? 0) + 1); confFinalLoserCounts.set(eastCFLoser.id, (confFinalLoserCounts.get(eastCFLoser.id) ?? 0) + 1); confFinalLoserCounts.set(westCFLoser.id, (confFinalLoserCounts.get(westCFLoser.id) ?? 0) + 1); for (const loser of [...eastR2Losers, ...westR2Losers]) { @@ -395,12 +328,11 @@ export class NBASimulator implements Simulator { // Round 1 losers are not counted (0 points per scoring rules). } - // 5. Convert integer counts to probability distributions. + // 6. Convert integer counts to probability distributions. // Exact denominators guarantee column sums of 1.0 by construction: - // probFirst/Second → N total (1 per sim) - // probThird/Fourth → confFinalLoserCounts / (2*N) — 2 conf final losers per sim - // probFifth–Eighth → confSemiLoserCounts / (4*N) — 4 conf semi losers per sim - const N = NUM_SIMULATIONS; + // probFirst/Second → NUM_SIMULATIONS total (1 per sim) + // probThird/Fourth → confFinalLoserCounts / (2*N) — 2 conf final losers per sim + // probFifth–Eighth → confSemiLoserCounts / (4*N) — 4 conf semi losers per sim const results: SimulationResult[] = participantIds.map((participantId) => { const c = championCounts.get(participantId) ?? 0; const f = finalistCounts.get(participantId) ?? 0; @@ -409,21 +341,21 @@ export class NBASimulator implements Simulator { return { participantId, probabilities: { - probFirst: c / N, - probSecond: f / N, - probThird: cf / (2 * N), - probFourth: cf / (2 * N), - probFifth: cs / (4 * N), - probSixth: cs / (4 * N), - probSeventh: cs / (4 * N), - probEighth: cs / (4 * N), + probFirst: c / NUM_SIMULATIONS, + probSecond: f / NUM_SIMULATIONS, + probThird: cf / (2 * NUM_SIMULATIONS), + probFourth: cf / (2 * NUM_SIMULATIONS), + probFifth: cs / (4 * NUM_SIMULATIONS), + probSixth: cs / (4 * NUM_SIMULATIONS), + probSeventh: cs / (4 * NUM_SIMULATIONS), + probEighth: cs / (4 * NUM_SIMULATIONS), }, source: "nba_bracket_monte_carlo", }; }); - // 6. Per-position normalization — belt-and-suspenders guard against floating-point - // division residuals. Columns are already near-exactly 1.0 after step 5. + // 7. Per-position normalization — belt-and-suspenders guard against floating-point + // division residuals. Columns are already near-exactly 1.0 after step 6. const positionKeys: Array = [ "probFirst", "probSecond", "probThird", "probFourth", "probFifth", "probSixth", "probSeventh", "probEighth",