I'm a Data Analyst at STARS Advanced Radiology Services, where I build R Shiny dashboards, automate reporting pipelines, and use SQL to drive insights that have led to a 60% reduction in unread worklists and an 85% reduction in aged A/R. I hold a BS in Economics with a Minor in Quantitative Data Analytics from Michigan State University.
Before my current role, I spent three summers as a Data Science Intern at Byrne Electrical Specialists, building IoT data pipelines, OEE dashboards, and statistical process control models for manufacturing equipment.
Outside of work, I channel my passion for data science into sports analytics projects — building Elo rating systems, Monte Carlo tournament simulations, and computer vision pipelines. My goal is to transition into machine learning and AI engineering, and the projects below reflect that trajectory.
View ResumeA comprehensive analytics platform for VCT (Valorant Champions Tour) professional esports. Features a custom map-specific Elo rating system where each team maintains separate ratings across all competitive maps, enabling nuanced matchup analysis.
Includes match win probability predictions, Monte Carlo tournament simulations (Swiss, GSL, double elimination formats), interactive Elo history charts, player ratings, pick/ban analysis, and a record book — all backed by a PostgreSQL database with automated data ingestion via web scraping.
// Optimal map selection: each team bans their worst map
// then picks their best from the remaining pool
const team1Ban = Object.values(team1Probabilities)
.filter(p => remainingMaps.has(p.map))
.sort((a, b) => a.probability - b.probability)[0]?.map;
// After bans, teams pick maps where they have
// the highest win probability (game theory optimal)
const team1Pick = Object.values(team1Probabilities)
.filter(p => remainingMaps.has(p.map))
.sort((a, b) => b.probability - a.probability)[0]?.map; An interactive FIFA World Cup 2026 draw simulator that accurately models the official tournament draw procedures with all FIFA constraints — confederation limits, host placement rules, and side-pairing requirements (e.g., Spain/Argentina must be on opposite bracket sides).
Uses recursive backtracking with constraint propagation to validate every placement in real-time. Includes 1 million pre-computed Monte Carlo simulations using bitmask-optimized constraint checking for instant probability lookups — group probabilities, opponent matchups, and stadium assignments.
// Bitmask-encoded confederations for O(1) constraint checks
// in the Monte Carlo simulation loop (1M iterations)
const CONF_BITS = {
AFC: 1, CAF: 2, CONCACAF: 4,
CONMEBOL: 8, OFC: 16, UEFA: 32
};
// Greedy placement with bitmask validation
function canPlace(groupMask: number, teamConf: number): boolean {
if (teamConf === CONF_BITS.UEFA) {
return uefaCount[group] < 2;
}
return (groupMask & teamConf) === 0;
} Int32Array typed arrays and bitmask encoding to run all 1M iterations in ~200ms. An NCAA basketball tournament analysis platform that integrates four major ranking systems (NET, KenPom, EvanMiya, Torvik) for consensus team evaluation. Features interactive bracket building with click-to-advance winner selection, radar chart team comparisons across shooting, defense, and possession metrics, and travel logistics visualization using Leaflet maps with Haversine distance calculations.
Includes a dynamic rating normalization system that maps diverse metrics to a 0-100% color-coded scale (red → yellow → green), enabling fair visual comparison across different statistical domains.
nextGameId and nextSlot. When you change a winner, the engine automatically propagates via syncWinnerToNext(), then runs clearInvalidWinnersDownstream() to recursively cascade validity checks — ensuring that changing a second-round pick correctly invalidates any dependent Sweet 16, Elite 8, or Final Four selections. A comparison overlay modal pops up for data-driven decision support. An interactive dashboard for exploring 75+ years of college basketball Elo ratings (20,000+ data points from the Cooper dataset, 1949–2026). Features a multi-team comparison view with draggable time-range selection on charts, enabling users to rank teams by average Elo within any custom window of seasons.
Includes a paginated, sortable data table with team search, dual-axis line charts (Elo + Net Rating trends), and a rankings page with 3D flip-card reveals showing the top 25 teams for any selected era. Team colors and logos are dynamically applied throughout.
// Compute average Elo for selected teams within a season range
export function getSeasonAverages(
rows: CooperRow[],
startYear: number,
endYear: number
): Map<string, { avgElo: number; seasons: number }> {
const filtered = rows.filter(
r => r.season >= startYear && r.season <= endYear
);
const grouped = groupBy(filtered, r => r.sb_name);
return new Map(
[...grouped].map(([team, data]) => [
team,
{
avgElo: data.reduce((s, r) => s + r.b_xelo_n, 0) / data.length,
seasons: data.length
}
])
);
} An interactive data science app that clusters 88 PGA Tour courses across a 64-dimensional feature space (strokes-gained demands, grass types, yardage, fairway profiles) using K-Means with silhouette-optimized cluster selection.
Matches ~435 active PGA players to course cluster types via a dot-product fit score that aligns player skill vectors with cluster demand profiles — predicting which players gain or lose strokes at each course archetype. Includes KNN course similarity search, hierarchical cluster dendrograms, and PCA variance analysis.
# Cluster labeling: dominant SG demand + yardage profile
for i in range(k):
mask = labels == i
cluster_courses = course_data[mask]
sg_cols = ['ott_sg', 'app_sg', 'arg_sg', 'putt_sg']
dominant = cluster_courses[sg_cols].mean().idxmax()
avg_yardage = cluster_courses['adj_yardage'].mean()
length = "Long" if avg_yardage > median_yardage else "Short" # Score every player against a cluster's SG demand profile
def compute_fit_scores(players_df, cluster_profile):
sg_components = ['sg_ott', 'sg_app', 'sg_arg', 'sg_putt']
demand = ['ott_sg', 'app_sg', 'arg_sg', 'putt_sg']
fit = sum(
players_df[sc] * cluster_profile[d]
for sc, d in zip(sg_components, demand)
)
return players_df['sg_total'] + fit A computer vision pipeline that processes basketball game video to automatically generate shot-by-shot statistics and player box scores. Uses Roboflow's RF-DETR model for multi-class detection (players, ball, rim, jersey numbers, shot types), with ByteTrack and Meta's SAM2 for player tracking across frames.
Includes homography transformation to map pixel coordinates to NBA court positions (in feet), enabling accurate shot location classification (2PT/3PT). Play recognition uses Dynamic Time Warping (DTW) with hierarchical clustering to identify similar offensive possessions from trajectory data.
def compute_trajectory_distance(traj_a, traj_b):
"""Compare two possessions using Dynamic Time Warping,
enabling similarity detection despite timing differences."""
cost_matrix = cdist(traj_a, traj_b, metric='euclidean')
dtw_dist, _ = dtw_path(cost_matrix)
return dtw_dist
def cluster_plays(possessions, threshold=15.0):
"""Group similar possessions using hierarchical clustering
on a DTW distance matrix."""
n = len(possessions)
dist_matrix = np.zeros((n, n))
for i, j in combinations(range(n), 2):
d = compute_trajectory_distance(
possessions[i].trajectories,
possessions[j].trajectories
)
dist_matrix[i, j] = dist_matrix[j, i] = d
linkage = hierarchy.linkage(
squareform(dist_matrix), method='ward'
)
labels = hierarchy.fcluster(linkage, threshold, criterion='distance')
return labels A subsidiarity-first liquid democracy prototype exploring how different voting mechanisms affect collective decision-making. The core is a fully-tested Python voting package implementing 6 electoral methods: Plurality, Approval, Instant Runoff (IRV), Borda Count, Condorcet with Ranked Pairs (Tideman), and Quadratic Voting — with 144+ passing tests.
The web layer (Next.js + Supabase) provides proposal creation with structured intake forms, ballot management, and domain/program-based topic organization. Designed to eventually support topic-scoped delegation graphs and AI-assisted voting recommendations.
def ranked_pairs(pairwise_matrix, candidates):
"""Tideman's Ranked Pairs: resolve Condorcet cycles
by locking victories in order of margin strength."""
victories = []
for i, j in combinations(candidates, 2):
margin = pairwise_matrix[i][j] - pairwise_matrix[j][i]
if margin > 0:
victories.append((i, j, margin))
elif margin < 0:
victories.append((j, i, abs(margin)))
# Sort by margin desc, then winning votes, then tiebreak
victories.sort(key=lambda v: v[2], reverse=True)
locked = defaultdict(set)
for winner, loser, _ in victories:
if not creates_cycle(locked, winner, loser):
locked[winner].add(loser)
# Winner: candidate with no incoming edges
return find_source(locked, candidates) Interested in working together on ML, data science, or analytics projects? Let's connect.
Email Me