Add pitching stats

This commit is contained in:
Peter 2023-09-11 13:39:46 -04:00
parent da894b9669
commit 4fbb1f0284
8 changed files with 441 additions and 14 deletions

5
components.d.ts vendored
View File

@ -5,7 +5,7 @@
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
export { }
declare module '@vue/runtime-core' {
export interface GlobalComponents {
@ -15,10 +15,13 @@ declare module '@vue/runtime-core' {
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
LastFourGamesBattingTable: typeof import('./src/components/LastFourGamesBattingTable.vue')['default']
LastFourGamesPitchingTable: typeof import('./src/components/LastFourGamesPitchingTable.vue')['default']
NavBar: typeof import('./src/components/NavBar.vue')['default']
NewsPreview: typeof import('./src/components/NewsPreview.vue')['default']
PlayerBattingSummaryTable: typeof import('./src/components/PlayerBattingSummaryTable.vue')['default']
PlayerCareerBattingTable: typeof import('./src/components/PlayerCareerBattingTable.vue')['default']
PlayerCareerPitchingTable: typeof import('./src/components/PlayerCareerPitchingTable.vue')['default']
PlayerPitchingSummaryTable: typeof import('./src/components/PlayerPitchingSummaryTable.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StandingsTable: typeof import('./src/components/StandingsTable.vue')['default']

View File

@ -0,0 +1,73 @@
<template>
<div class="col-sm-12">
<h3>Last 4 Games</h3>
<div class="table-responsive-xl">
<table class="table table-sm table-striped">
<thead class="thead-dark">
<tr>
<th>Game</th>
<th>W</th>
<th>L</th>
<th>ERA</th>
<th>GS</th>
<th>SV</th>
<th>HD</th>
<th>IP</th>
<th>H</th>
<th>R</th>
<th>ER</th>
<th>HR</th>
<th>BB</th>
<th>SO</th>
<th>HBP</th>
<th>BK</th>
<th>WP</th>
</tr>
</thead>
<tbody id="last4-pitching">
<tr v-for="gameStat in last4GamesPitching">
<td>{{ makeWxGyFromGame(gameStat.game) }}</td>
<td>{{ gameStat.win }}</td>
<td>{{ gameStat.loss }}</td>
<td>{{ gameStat.era.toFixed(2) }}</td>
<td>{{ gameStat.gs }}</td>
<td>{{ gameStat.save }}</td>
<td>{{ gameStat.hold }}</td>
<td>{{ outsToInnings(gameStat) }}</td>
<td>{{ gameStat.hits }}</td>
<td>{{ gameStat.run }}</td>
<td>{{ gameStat.e_run }}</td>
<td>{{ gameStat.hr }}</td>
<td>{{ gameStat.bb }}</td>
<td>{{ gameStat.so }}</td>
<td>{{ gameStat.hbp }}</td>
<td>{{ gameStat.balk }}</td>
<td>{{ gameStat.wp }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script lang="ts">
import type { Game } from '@/services/apiResponseTypes'
import type { PitchingStat } from '@/services/pitchingStatsService'
import { outsToInnings } from '@/services/utilities'
export default {
name: "LastFourGamesPitchingTable",
props: {
last4GamesPitching: { type: Array<PitchingStat>, required: true }
},
methods: {
makeWxGyFromGame(game: Game | 'TOT'): string {
if (game === 'TOT') return 'TOT'
return `w${game.week}g${game.game_num}`
},
outsToInnings(stat: PitchingStat): string {
return outsToInnings(stat)
}
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="row" id="career-batting-row">
<div v-if="hasBattingStats" class="row" id="career-batting-row">
<div class="col-sm-12">
<h3>Batting Stats</h3>
<div class="table-responsive-xl">
@ -118,6 +118,9 @@ export default {
postSeasonBattingStats: { type: Array<BattingStat>, required: true }
},
computed: {
hasBattingStats(): boolean {
return !!(this.regularSeasonBattingStats.length + this.postSeasonBattingStats.length)
},
careerBattingStat(): BattingStat | undefined {
if (this.regularSeasonBattingStats.length > 0) {
// old site behavior just summed regular season stats for the career line total

View File

@ -0,0 +1,189 @@
<template>
<div v-if="hasPitchingStats" class="row" id="career-pitching-row">
<div class="col-sm-12">
<h3>Pitching Stats</h3>
<div class="table-responsive-xl">
<table class="table table-sm table-striped" id="career-pitching">
<thead class="thead-dark">
<tr>
<th>Season</th>
<th>W</th>
<th>L</th>
<th>W-L%</th>
<th>ERA</th>
<th>G</th>
<th>GS</th>
<th>SV</th>
<th>HD</th>
<th>BSV</th>
<th>IP</th>
<th>H</th>
<th>R</th>
<th>ER</th>
<th>HR</th>
<th>BB</th>
<th>SO</th>
<th>HBP</th>
<th>BK</th>
<th>WP</th>
<th>IR</th>
<th>IRS</th>
<th>WHIP</th>
<th>H/9</th>
<th>HR/9</th>
<th>BB/9</th>
<th>SO/9</th>
<th>SO/BB</th>
</tr>
</thead>
<tbody id="career-pitching-table">
<tr v-for="stat in sortedRegularAndPostSeasonPitching">
<td>S{{ stat.seasonNumber }}{{ stat.isRegularSeason ? '' : ' / Playoffs' }}</td>
<td>{{ stat.win }}</td>
<td>{{ stat.loss }}</td>
<td>{{ winPercentage(stat) }}</td>
<td>{{ stat.era.toFixed(2) }}</td>
<td>{{ stat.games }}</td>
<td>{{ stat.gs }}</td>
<td>{{ stat.save }}</td>
<td>{{ stat.hold }}</td>
<td>{{ stat.bsave }}</td>
<td>{{ outsToInnings(stat) }}</td>
<td>{{ stat.hits }}</td>
<td>{{ stat.run }}</td>
<td>{{ stat.e_run }}</td>
<td>{{ stat.hr }}</td>
<td>{{ stat.bb }}</td>
<td>{{ stat.so }}</td>
<td>{{ stat.hbp }}</td>
<td>{{ stat.balk }}</td>
<td>{{ stat.wp }}</td>
<td>{{ stat.ir }}</td>
<td>{{ stat.ir_sc }}</td>
<td>{{ stat.whip.toFixed(2) }}</td>
<td>{{ hitsPer9(stat) }}</td>
<td>{{ hrsPer9(stat) }}</td>
<td>{{ stat.bbPer9.toFixed(1) }}</td>
<td>{{ stat.kPer9.toFixed(1) }}</td>
<td>{{ stat.kPerBB.toFixed(1) }}</td>
</tr>
</tbody>
<tfoot>
<tr v-if="careerPitchingStat" id="career-pitching-footer">
<th>Career</th>
<th>{{ careerPitchingStat.win }}</th>
<th>{{ careerPitchingStat.loss }}</th>
<th>{{ winPercentage(careerPitchingStat) }}</th>
<th>{{ careerPitchingStat.era.toFixed(2) }}</th>
<th>{{ careerPitchingStat.games }}</th>
<th>{{ careerPitchingStat.gs }}</th>
<th>{{ careerPitchingStat.save }}</th>
<th>{{ careerPitchingStat.hold }}</th>
<th>{{ careerPitchingStat.bsave }}</th>
<th>{{ outsToInnings(careerPitchingStat) }}</th>
<th>{{ careerPitchingStat.hits }}</th>
<th>{{ careerPitchingStat.run }}</th>
<th>{{ careerPitchingStat.e_run }}</th>
<th>{{ careerPitchingStat.hr }}</th>
<th>{{ careerPitchingStat.bb }}</th>
<th>{{ careerPitchingStat.so }}</th>
<th>{{ careerPitchingStat.hbp }}</th>
<th>{{ careerPitchingStat.balk }}</th>
<th>{{ careerPitchingStat.wp }}</th>
<th>{{ careerPitchingStat.ir }}</th>
<th>{{ careerPitchingStat.ir_sc }}</th>
<th>{{ careerPitchingStat.whip.toFixed(2) }}</th>
<th>{{ hitsPer9(careerPitchingStat) }}</th>
<th>{{ hrsPer9(careerPitchingStat) }}</th>
<th>{{ careerPitchingStat.bbPer9.toFixed(1) }}</th>
<th>{{ careerPitchingStat.kPer9.toFixed(1) }}</th>
<th>{{ careerPitchingStat.kPerBB.toFixed(1) }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</template>
<script lang="ts">
import { aggregatePitchingStats, type PitchingStat } from '@/services/pitchingStatsService'
import { outsToInnings } from '@/services/utilities'
interface PitchingStatWithSeason extends PitchingStat {
seasonNumber: number
isRegularSeason: boolean
}
export default {
name: "PlayerCareerPitchingTable",
props: {
regularSeasonPitchingStats: { type: Array<PitchingStat>, required: true },
postSeasonPitchingStats: { type: Array<PitchingStat>, required: true }
},
computed: {
hasPitchingStats(): boolean {
return !!(this.regularSeasonPitchingStats.length + this.postSeasonPitchingStats.length)
},
careerPitchingStat(): PitchingStat | undefined {
if (this.regularSeasonPitchingStats.length > 0) {
// old site behavior just summed regular season stats for the career line total
return aggregatePitchingStats(this.regularSeasonPitchingStats)
}
return undefined
},
sortedRegularAndPostSeasonPitching(): PitchingStatWithSeason[] {
let seasonStats: PitchingStatWithSeason[] = []
if (this.regularSeasonPitchingStats?.length) {
seasonStats = seasonStats.concat(this.regularSeasonPitchingStats.map(stat => {
return {
...stat,
seasonNumber: stat.player.season,
isRegularSeason: true
}
}))
}
// TODO: here would be where you could filter out postseason stats if desired (like Josef requested)
if (this.postSeasonPitchingStats?.length) {
seasonStats = seasonStats.concat(this.postSeasonPitchingStats.map(stat => {
return {
...stat,
seasonNumber: stat.player.season,
isRegularSeason: false
}
}))
}
return seasonStats.sort((s1, s2) => {
return s1.seasonNumber - s2.seasonNumber === 0
? s1.isRegularSeason
? -1
: 1
: s1.seasonNumber - s2.seasonNumber
})
},
},
methods: {
outsToInnings(stat: PitchingStat): string {
return outsToInnings(stat)
},
winPercentage(stat: PitchingStat): string {
if (stat.win + stat.loss === 0) return '-'
return (stat.win / (stat.win + stat.loss)).toFixed(3)
},
hitsPer9(stat: PitchingStat): string {
if (stat.outs === 0) return '-'
return (stat.hits * 27 / stat.outs).toFixed(1)
},
hrsPer9(stat: PitchingStat): string {
if (stat.outs === 0) return '-'
return (stat.hr * 27 / stat.outs).toFixed(1)
}
}
}
</script>

View File

@ -0,0 +1,92 @@
<template>
<div class="col-sm-8">
<div class="table-responsive-xl" style="max-width:35rem">
<table class="table table-sm table-striped">
<thead class="thead-dark">
<tr>
<th>Summary</th>
<th>W</th>
<th>L</th>
<th>SV</th>
<th>ERA</th>
<th>G</th>
<th>GS</th>
<th>IP</th>
<th>SO</th>
<th>WHIP</th>
</tr>
</thead>
<tbody id="pitcher-summary-table">
<tr v-if="currentSeasonPitching">
<td>Season {{ currentSeasonPitching.player.season }}</td>
<td>{{ currentSeasonPitching.win }}</td>
<td>{{ currentSeasonPitching.loss }}</td>
<td>{{ currentSeasonPitching.save }}</td>
<td>{{ currentSeasonPitching.era.toFixed(2) }}</td>
<td>{{ currentSeasonPitching.games }}</td>
<td>{{ currentSeasonPitching.gs }}</td>
<td>{{ outsToInnings(currentSeasonPitching) }}</td>
<td>{{ currentSeasonPitching.so }}</td>
<td>{{ currentSeasonPitching.whip.toFixed(2) }}</td>
</tr>
<tr v-if="currentPostSeasonPitching">
<td>S{{ currentPostSeasonPitching.player.season }} / Playoffs</td>
<td>{{ currentPostSeasonPitching.win }}</td>
<td>{{ currentPostSeasonPitching.loss }}</td>
<td>{{ currentPostSeasonPitching.save }}</td>
<td>{{ currentPostSeasonPitching.era.toFixed(2) }}</td>
<td>{{ currentPostSeasonPitching.games }}</td>
<td>{{ currentPostSeasonPitching.gs }}</td>
<td>{{ outsToInnings(currentPostSeasonPitching) }}</td>
<td>{{ currentPostSeasonPitching.so }}</td>
<td>{{ currentPostSeasonPitching.whip.toFixed(2) }}</td>
</tr>
</tbody>
<tfoot id="pitcher-summary-footer">
<tr v-if="careerPitchingStat">
<th>Career</th>
<td>{{ careerPitchingStat.win }}</td>
<td>{{ careerPitchingStat.loss }}</td>
<td>{{ careerPitchingStat.save }}</td>
<td>{{ careerPitchingStat.era.toFixed(2) }}</td>
<td>{{ careerPitchingStat.games }}</td>
<td>{{ careerPitchingStat.gs }}</td>
<td>{{ outsToInnings(careerPitchingStat) }}</td>
<td>{{ careerPitchingStat.so }}</td>
<td>{{ careerPitchingStat.whip.toFixed(2) }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</template>
<script lang="ts">
import { aggregatePitchingStats, type PitchingStat } from '@/services/pitchingStatsService'
import { outsToInnings } from '@/services/utilities'
import type { PropType } from 'vue'
export default {
name: "PlayerPitchingSummaryTable",
props: {
currentSeasonPitching: { type: Object as PropType<PitchingStat>, required: false },
currentPostSeasonPitching: { type: Object as PropType<PitchingStat>, required: false },
regularSeasonPitchingStats: { type: Array<PitchingStat>, required: true }
},
computed: {
careerPitchingStat(): PitchingStat | undefined {
if (this.regularSeasonPitchingStats.length > 0) {
// old site behavior just summed regular season stats for the career line total
return aggregatePitchingStats(this.regularSeasonPitchingStats)
}
return undefined
},
},
methods: {
outsToInnings(stat: PitchingStat): string {
return outsToInnings(stat)
}
}
}
</script>

View File

@ -140,7 +140,7 @@ export async function fetchPitchingStatsBySeasonAndPlayerId(seasonNumber: number
const pitchingStatsResponse: {
count: number
stats: PitchingStat[]
stats: PitchingStatRaw[]
} = await response.json()
if (pitchingStatsResponse.count === 0) return undefined
@ -149,7 +149,7 @@ export async function fetchPitchingStatsBySeasonAndPlayerId(seasonNumber: number
throw new Error('pitchingStatsService.fetchPitchingStatsBySeasonAndPlayerId - Expected one stat line for season/player, return contained many')
}
return pitchingStatsResponse.stats[0]
return normalizePitchingStat(pitchingStatsResponse.stats[0])
}
async function fetchLegacyPitchingStatsBySeasonAndPlayerId(seasonNumber: number, playerId: number, isRegularSeason: boolean): Promise<PitchingStat | undefined> {
@ -174,14 +174,14 @@ export async function fetchPitchingStatsForLastFourGamesBySeasonAndPlayerId(seas
const pitchingStatsResponse: {
count: number
stats: PitchingStat[]
stats: PitchingStatRaw[]
} = await response.json()
if (pitchingStatsResponse.count > 4) {
throw new Error(`pitchingStatsService.fetchPitchingStatsForLastFourBySeasonAndPlayerId - Expected at most 4 games, return contained ${pitchingStatsResponse.count}`)
}
return pitchingStatsResponse.stats
return pitchingStatsResponse.stats.map(normalizePitchingStat)
}
export function aggregatePitchingStats(pitchingStats: PitchingStat[]): PitchingStat {
@ -277,6 +277,11 @@ export function aggregatePitchingStats(pitchingStats: PitchingStat[]): PitchingS
return {
...totalStat,
era: eraFromOuts(totalStat),
whip: whipFromOuts(totalStat),
kPer9: kPer9FromOuts(totalStat),
bbPer9: bbPer9FromOuts(totalStat),
kPerBB: kPerBB(totalStat),
avg: avg(totalStatWithHit),
obp: 0, //obp({pa: totalStat.}),
slg: slg(totalStatWithHit),
@ -399,24 +404,48 @@ function era(stat: { e_run: number, ip: number }): number {
return stat.e_run * 9 / stat.ip
}
function eraFromOuts(stat: { e_run: number, outs: number }): number {
if (stat.outs === 0) return stat.e_run > 0 ? NaN : 0
return stat.e_run * 27 / stat.outs
}
function whip(stat: { bb: number, hits: number, ip: number }): number {
if (stat.ip === 0) return 0
if (stat.ip === 0) return stat.bb + stat.hits > 0 ? NaN : 0
return (stat.bb + stat.hits) / stat.ip
}
function whipFromOuts(stat: { bb: number, hits: number, outs: number }): number {
if (stat.outs === 0) return stat.bb + stat.hits > 0 ? NaN : 0
return (stat.bb + stat.hits) * 3 / stat.outs
}
function kPer9(stat: { so: number, ip: number }): number {
if (stat.ip === 0) return 0
return stat.so * 9 / stat.ip
}
function kPer9FromOuts(stat: { so: number, outs: number }): number {
if (stat.outs === 0) return 0
return stat.so * 27 / stat.outs
}
function bbPer9(stat: { bb: number, ip: number }): number {
if (stat.ip === 0) return 0
if (stat.ip === 0) return stat.bb > 0 ? NaN : 0
return stat.bb * 9 / stat.ip
}
function bbPer9FromOuts(stat: { bb: number, outs: number }): number {
if (stat.outs === 0) return stat.bb > 0 ? NaN : 0
return stat.bb * 27 / stat.outs
}
function kPerBB(stat: { so: number, bb: number }): number {
if (stat.bb === 0) return 0

View File

@ -38,3 +38,7 @@ export function woba(stat: { bb: number, hbp: number, hit: number, double: numbe
return numerator / denominator
}
export function outsToInnings(stat: { outs: number }): string {
return (stat.outs / 3).toFixed(1)
}

View File

@ -37,9 +37,12 @@
<h3>Summary</h3>
</div>
<!-- Batting Summary -->
<PlayerBattingSummaryTable :current-season-batting="currentSeasonBatting"
<PlayerBattingSummaryTable v-if="isBatter" :current-season-batting="currentSeasonBatting"
:current-post-season-batting="currentPostSeasonBatting"
:regular-season-batting-stats="regularSeasonBattingStats" />
<PlayerPitchingSummaryTable v-else :current-season-pitching="currentSeasonPitching"
:current-post-season-pitching="currentPostSeasonPitching"
:regular-season-pitching-stats="regularSeasonPitchingStats" />
<div class="col-sm-4">
<div class="table-responsive-xl" style="max-width:20rem">
<table class="table table-sm table-striped">
@ -61,11 +64,16 @@
</div>
</div>
<!-- Last 4 Games -->
<LastFourGamesBattingTable :last4-games-batting="last4Games" />
<LastFourGamesBattingTable v-if="isBatter" :last4-games-batting="last4GamesBatting" />
<LastFourGamesPitchingTable v-else :last4-games-pitching="last4GamesPitching" />
</div>
<!-- Career Batting -->
<PlayerCareerBattingTable :regular-season-batting-stats="regularSeasonBattingStats"
<PlayerCareerBattingTable v-if="isBatter" :regular-season-batting-stats="regularSeasonBattingStats"
:post-season-batting-stats="postSeasonBattingStats" />
<PlayerCareerPitchingTable :regular-season-pitching-stats="regularSeasonPitchingStats"
:post-season-pitching-stats="postSeasonPitchingStats" />
<PlayerCareerBattingTable v-if="!isBatter" :regular-season-batting-stats="regularSeasonBattingStats"
:post-season-batting-stats="postSeasonBattingStats" />
</div>
</div>
@ -78,8 +86,12 @@ import { type Player, fetchPlayerByName } from '@/services/playersService'
import { fetchBattingStatsBySeasonAndPlayerId, fetchBattingStatsForLastFourGamesBySeasonAndPlayerId, type BattingStat } from '@/services/battingStatsService'
import { CURRENT_SEASON, isNotUndefined } from '@/services/utilities'
import LastFourGamesBattingTable from '@/components/LastFourGamesBattingTable.vue'
import LastFourGamesPitchingTable from '@/components/LastFourGamesPitchingTable.vue'
import PlayerCareerBattingTable from '@/components/PlayerCareerBattingTable.vue'
import PlayerCareerPitchingTable from '@/components/PlayerCareerPitchingTable.vue'
import PlayerBattingSummaryTable from '@/components/PlayerBattingSummaryTable.vue'
import PlayerPitchingSummaryTable from '@/components/PlayerPitchingSummaryTable.vue'
import { fetchPitchingStatsBySeasonAndPlayerId, fetchPitchingStatsForLastFourGamesBySeasonAndPlayerId, type PitchingStat } from '@/services/pitchingStatsService'
export default {
name: "PlayerView",
@ -88,15 +100,23 @@ export default {
isAuthenticated: false as Boolean,
player: undefined as Player | undefined,
last2Decisions: [] as Decision[],
// Batting stats
regularSeasonBattingStats: [] as BattingStat[],
postSeasonBattingStats: [] as BattingStat[],
last4Games: [] as BattingStat[]
last4GamesBatting: [] as BattingStat[],
// Pitching stats
regularSeasonPitchingStats: [] as PitchingStat[],
postSeasonPitchingStats: [] as PitchingStat[],
last4GamesPitching: [] as PitchingStat[],
}
},
components: {
PlayerBattingSummaryTable,
PlayerCareerBattingTable,
LastFourGamesBattingTable
LastFourGamesBattingTable,
PlayerPitchingSummaryTable,
PlayerCareerPitchingTable,
LastFourGamesPitchingTable
},
props: {
seasonNumber: { type: Number, required: true },
@ -156,6 +176,16 @@ export default {
return undefined
return this.postSeasonBattingStats.find(stat => stat.player.season === CURRENT_SEASON)
},
currentSeasonPitching(): PitchingStat | undefined {
if (!this.regularSeasonPitchingStats.length)
return undefined
return this.regularSeasonPitchingStats.find(stat => stat.player.season === CURRENT_SEASON)
},
currentPostSeasonPitching(): PitchingStat | undefined {
if (!this.postSeasonPitchingStats.length)
return undefined
return this.postSeasonPitchingStats.find(stat => stat.player.season === CURRENT_SEASON)
},
lastAppearance(): string | undefined {
if (!this.last2Decisions?.length)
return undefined
@ -191,12 +221,16 @@ export default {
if (!this.player)
return
this.last2Decisions = await fetchLast2DecisionsByPlayerId(this.seasonNumber, this.player.id)
this.last4Games = await fetchBattingStatsForLastFourGamesBySeasonAndPlayerId(this.seasonNumber, this.player.id)
this.last4GamesBatting = await fetchBattingStatsForLastFourGamesBySeasonAndPlayerId(this.seasonNumber, this.player.id)
this.last4GamesPitching = await fetchPitchingStatsForLastFourGamesBySeasonAndPlayerId(this.seasonNumber, this.player.id)
// TODO: this should change, either with an api that can take a player name for every season, a way
// to get multiple seasons stats at once, or a players ids across all seasons at once
const playerSeasons = await Promise.all(Array.from(Array(CURRENT_SEASON), (element, index) => index + 1).map(seasonNumber => fetchPlayerByName(seasonNumber, this.player!.name)))
this.regularSeasonBattingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchBattingStatsBySeasonAndPlayerId(player!.season, player!.id, true)))).filter(isNotUndefined)
this.postSeasonBattingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchBattingStatsBySeasonAndPlayerId(player!.season, player!.id, false)))).filter(isNotUndefined)
this.regularSeasonPitchingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchPitchingStatsBySeasonAndPlayerId(player!.season, player!.id, true)))).filter(isNotUndefined)
this.postSeasonPitchingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchPitchingStatsBySeasonAndPlayerId(player!.season, player!.id, false)))).filter(isNotUndefined)
},
async tryFetchPlayerByNameForAnySeason(seasonNumber: number, playerName: string): Promise<Player | undefined> {
do {