Published
Edited
Mar 31, 2021
Insert cell
Insert cell
samples = runMultiSample({
numShots: 20 ,
shootingPct: 0.50,
hotStreakQuota: 3,
numSamples: 5000
})
Insert cell
samples.results.filter(d => d.realHotStreakShootingPct).map(d => d.realHotStreakShootingPct)
Insert cell
vl.data(samples.results.filter(d => !isNaN(d.realHotStreakShootingPct)))
.layer(
vl.markPoint()
.data(samples.results.filter(d => !isNaN(d.realHotStreakShootingPct)).map((d, i) => ({ ...d, index: i})).slice(0, 200))
.encode(
vl.x().fieldQ("index"),
vl.y().fieldQ("realHotStreakShootingPct").scale({ domain: [0, 1] }).axis({ title: 'Hot Streak Shooting Pct' }),
vl.color().fieldQ('realHotStreakShootingPct').scale({ scheme: 'redblue', reverse: true })
// vl.color().fieldO('type').scale({ range: [ '#F64C72', '#242582' ] }),
),
// vl.markLine({ interpolate: 'cardinal' })
// .data(merged.filter(d => d.type === 'Average'))
// .encode(
// vl.x().fieldT("date"),
// vl.y().fieldQ("avgScore").scale({ domain: [60, 130] }),
// vl.color().fieldO('type').scale({ range: [ '#F64C72', '#242582' ] }),
// ),
)
.title('Average Scoring')
.width(500)
.config(baseVegaConfig)
.render()
Insert cell
vl.markBar({ cornerRadius: 2 })
.data(samples.results.filter(d => d.realHotStreakShootingPct))
.encode(
vl.y(),
vl.x().count().axis({ title: 'Number of Appearances' }),
vl.color().fieldO("deal.closed").scale({ domain: [true, false], range: ['#584Cf2', '#bbc'] }).legend({ title: 'Deal Closed' })
)
.config(baseVegaConfig)
.render()
Insert cell
Insert cell
Insert cell
shots = {
return d3.range(0, shotsTaken).map(d => Math.random() > 0.5)
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
runMultiSample = {
return ({ numShots, shootingPct, hotStreakQuota, numSamples }) => {
let results = []
for (let i = 0; i < numSamples; i++) {
const sampleResults = runSample({ numShots, shootingPct, hotStreakQuota })
results.push(sampleResults)
}
const avgShootingPct = d3.mean(results, d => d.realShootingPct)
const chunkedHotShootingPct = d3.mean(results, d => d.realHotStreakShootingPct)
const allShotsAfterHot = [].concat(...results.map(d => d.shotsAfterHotStreak))
const hotShootingPct = allShotsAfterHot.reduce((acc, s) => s.made ? 1 + acc : acc, 0) / allShotsAfterHot.length
return {
results,
avgShootingPct,
hotShootingPct,
chunkedHotShootingPct
}
}
}
Insert cell
runSample = {
return ({ numShots, shootingPct, hotStreakQuota }) => {
const shots = d3.range(0, numShots).map(d => Math.random() < shootingPct)
const shotsAfterHotStreak = shots.map((d, i) => ({ made: d, index: i })).filter((d, i) => {
if (i < streakRequirement) {
return false
}
for (let j = 1; j <= streakRequirement; j++) {
if (!shots[i - j]) {
return false
}
}
return true
})
const realHotStreakShootingPct = shotsAfterHotStreak.reduce((acc, s) => s.made ? 1 + acc : acc, 0) / shotsAfterHotStreak.length
const realShootingPct = shots.reduce((acc, s) => s ? 1 + acc : acc, 0) / shots.length
return {
realShootingPct,
realHotStreakShootingPct,
shots,
numShots,
shootingPct,
hotStreakQuota,
shotsAfterHotStreak,
}
}
}
Insert cell
Insert cell
timeStreaks = {
const playerShots = realShots.filter(d => d.distance >= 0)
const results = {}
const streakQuota = 3
for (let i = 1; i <= streakQuota; i++) {
results['all'] = { shots: [shots[0]] }
results[`make-${i}`] = { shots: [] }
results[`miss-${i}`] = { shots: [] }
}
for (let i = 1; i < playerShots.length; i++) {
results['all'].shots.push(playerShots[i])
const lookBackCount = Math.min(streakQuota, i)
const lastResult = playerShots[i - 1].made
const lastResultString = lastResult ? 'make' : 'miss'
let j = 1;
while (j <= lookBackCount && playerShots[i - j].made === lastResult && Math.abs(playerShots[i - j].secondsIntoGame - playerShots[i].secondsIntoGame) < j * 180 ) {
results[`${lastResultString}-${j}`].shots.push(playerShots[i])
j++
}
}
for (const [key, r] of Object.entries(results)) {
const avgDistance = d3.mean(r.shots, s => s.distance)
const fgm = r.shots.reduce((sum, s) => s.made ? sum + 1 : sum, 0)
r.attempts = r.shots.length;
r.fgm = fgm;
r.fgpct = fgm / r.shots.length;
r.avgDistance = avgDistance;
}
return results
}
Insert cell
streaks = {
const playerShots = realShots.filter(d => d.distance >= 0)
const results = {}
const streakQuota = 3
for (let i = 1; i <= streakQuota; i++) {
results['all'] = { shots: [shots[0]] }
results[`make-${i}`] = { shots: [] }
results[`miss-${i}`] = { shots: [] }
}
for (let i = 1; i < playerShots.length; i++) {
results['all'].shots.push(playerShots[i])
const lookBackCount = Math.min(streakQuota, i)
const lastResult = playerShots[i - 1].made
const lastResultString = lastResult ? 'make' : 'miss'
let j = 1;
while (j <= lookBackCount && playerShots[i - j].made === lastResult) {
results[`${lastResultString}-${j}`].shots.push(playerShots[i])
j++
}
}
for (const [key, r] of Object.entries(results)) {
const fgm = r.shots.reduce((sum, s) => s.made ? sum + 1 : sum, 0)
const avgDistance = d3.mean(r.shots, s => s.distance)
r.attempts = r.shots.length;
r.fgm = fgm;
r.fgpct = fgm / r.shots.length;
r.avgDistance = avgDistance
}
return results
}
Insert cell
realShots = {
const json = await FileAttachment("steph-shots.json").json({ typed: true})
return json.data.map(d => {
let secondsIntoGame
if (d.period <= 4) {
secondsIntoGame = (d.period - 1) * 12 * 60 + (12 - d.minutes_remaining) * 60 - d.seconds_remaining
} else {
secondsIntoGame = (d.period - 1) * 12 * 60 + (5 - d.minutes_remaining) * 60 - d.seconds_remaining
}
return {
made: !!d.shot_made_flag,
distance: d.shot_distance,
secondsIntoGame,
...d
}
})
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more