James McCool commited on
Commit
d62964a
·
1 Parent(s): 2867cfc

more exposure limiting work

Browse files
Files changed (2) hide show
  1. app.py +5 -4
  2. global_func/build_optimal_lineups.py +39 -39
app.py CHANGED
@@ -2755,8 +2755,8 @@ if selected_tab == 'Optimizer':
2755
 
2756
  with st.expander('Exposures'):
2757
  st.caption(
2758
- "After each lineup, players at or above the cap are removed from the pool "
2759
- "(usage = appearances ÷ lineups built so far)."
2760
  )
2761
  enforce_max_exposure = st.checkbox(
2762
  "Enforce max player exposure",
@@ -2902,9 +2902,10 @@ if selected_tab == 'Optimizer':
2902
  float(max_player_exposure_pct) if enforce_max_exposure else None
2903
  )
2904
  if enforce_max_exposure:
 
2905
  st.caption(
2906
- f"Enforcing max {max_player_exposure_pct:.0f}% exposure "
2907
- f"across {int(num_lineups)} lineups (pool refreshes after each lineup)."
2908
  )
2909
 
2910
  def _optimizer_build_progress(done: int, total: int) -> None:
 
2755
 
2756
  with st.expander('Exposures'):
2757
  st.caption(
2758
+ "Cap is based on your target lineup count (e.g. 70% with 20 lineups "
2759
+ " each player in at most 14 lineups). Pool resets before each build."
2760
  )
2761
  enforce_max_exposure = st.checkbox(
2762
  "Enforce max player exposure",
 
2902
  float(max_player_exposure_pct) if enforce_max_exposure else None
2903
  )
2904
  if enforce_max_exposure:
2905
+ max_apps = int(max_player_exposure_pct / 100.0 * int(num_lineups))
2906
  st.caption(
2907
+ f"Enforcing max {max_player_exposure_pct:.0f}% exposure: "
2908
+ f" {max_apps} of {int(num_lineups)} lineups per player."
2909
  )
2910
 
2911
  def _optimizer_build_progress(done: int, total: int) -> None:
global_func/build_optimal_lineups.py CHANGED
@@ -3,6 +3,7 @@ Build optimal DFS lineups from projections using OR-Tools (via optimize_lineup h
3
  """
4
  from __future__ import annotations
5
 
 
6
  from collections import Counter
7
  from typing import Any, Callable
8
 
@@ -407,41 +408,37 @@ def _init_usage_counts(pool: pd.DataFrame) -> dict[str, int]:
407
  return dict.fromkeys(pool["player_names"].unique(), 0)
408
 
409
 
 
 
 
 
 
 
 
 
 
410
  def _exposure_excluded_players(
411
  pool_player_names,
412
  usage_counts: dict[str, int],
413
- num_lineups_built: int,
414
- max_exposure_fraction: float,
415
  ) -> set[str]:
416
- """
417
- Players above the exposure cap on current usage (count / lineups built).
418
-
419
- Anyone at or below the cap remains eligible when the pool is reset.
420
- """
421
- if num_lineups_built <= 0:
422
- return set()
423
- excluded: set[str] = set()
424
- for pname in pool_player_names:
425
- count = usage_counts.get(pname, 0)
426
- if count / num_lineups_built > max_exposure_fraction + 1e-9:
427
- excluded.add(pname)
428
- return excluded
429
 
430
 
431
  def _lineup_within_exposure_cap(
432
  row: pd.Series,
433
  player_columns: list[str],
434
  usage_counts: dict[str, int],
435
- num_lineups_built: int,
436
- max_exposure_fraction: float,
437
  ) -> bool:
438
- """True if accepting this lineup keeps every player at or below the exposure cap."""
439
- if num_lineups_built <= 0:
440
- return True
441
- next_total = num_lineups_built + 1
442
  for pname in _lineup_player_names(row, player_columns):
443
- count = usage_counts.get(pname, 0)
444
- if (count + 1) / next_total > max_exposure_fraction + 1e-9:
445
  return False
446
  return True
447
 
@@ -449,25 +446,22 @@ def _lineup_within_exposure_cap(
449
  def _build_active_pool(
450
  pool: pd.DataFrame,
451
  usage_counts: dict[str, int],
452
- num_lineups_built: int,
453
- max_exposure_fraction: float | None,
454
  static_exclude: set[str],
455
  temp_exclude: set[str] | None = None,
456
  ) -> pd.DataFrame:
457
  """
458
  Reset the optimization pool from the full projections pool before each lineup.
459
 
460
- Includes every player at or below the exposure cap (0% by default); excludes
461
- user blocks, optional temp blocks, and anyone strictly over the cap.
462
  """
463
  pool_names = pool["player_names"].unique()
464
  exclude = set(static_exclude)
465
  if temp_exclude:
466
  exclude |= temp_exclude
467
- if max_exposure_fraction is not None and num_lineups_built > 0:
468
- exclude |= _exposure_excluded_players(
469
- pool_names, usage_counts, num_lineups_built, max_exposure_fraction
470
- )
471
  if not exclude:
472
  return pool.reset_index(drop=True)
473
  return pool[~pool["player_names"].isin(exclude)].reset_index(drop=True)
@@ -528,12 +522,13 @@ def generate_optimal_lineups(
528
  usage_counts: dict[str, int] = _init_usage_counts(pool)
529
 
530
  exposure_cap = None
 
531
  if max_player_exposure is not None and 0 < max_player_exposure < 100.0:
532
  exposure_cap = max(0.0, min(1.0, float(max_player_exposure) / 100.0))
 
533
 
534
  static_exclude: set[str] = set(exclude_players or [])
535
  stack_in_solver = bool(stack_config and stack_config.get("enabled") and stack_slot_columns)
536
- use_fresh_lineup_build = stack_in_solver or exposure_cap is not None
537
 
538
  def _notify_progress() -> None:
539
  if progress_callback is not None:
@@ -562,12 +557,11 @@ def generate_optimal_lineups(
562
  key = _lineup_key(row, player_columns)
563
  if key in used_keys:
564
  continue
565
- if exposure_cap is not None and not _lineup_within_exposure_cap(
566
  row,
567
  player_columns,
568
  usage_counts,
569
- len(rows),
570
- exposure_cap,
571
  ):
572
  continue
573
  used_keys.add(key)
@@ -586,7 +580,7 @@ def generate_optimal_lineups(
586
  type_var=type_var,
587
  optimize_by=optimize_by,
588
  )
589
- first_pool = _build_active_pool(pool, usage_counts, 0, exposure_cap, static_exclude)
590
  first_row = try_add_lineup(seed, first_pool)
591
  if first_row is None:
592
  return pd.DataFrame(columns=player_columns)
@@ -613,15 +607,14 @@ def generate_optimal_lineups(
613
  active_pool = _build_active_pool(
614
  pool,
615
  usage_counts,
616
- num_built,
617
- exposure_cap,
618
  static_exclude,
619
  temp_exclude,
620
  )
621
  if active_pool.empty:
622
  break
623
 
624
- if use_fresh_lineup_build:
625
  next_seed = empty_lineup_row(player_columns)
626
  candidate_row = try_add_lineup(next_seed, active_pool)
627
  if candidate_row is None:
@@ -652,6 +645,13 @@ def generate_optimal_lineups(
652
  candidate_key = _lineup_key(candidate_row, player_columns)
653
  if candidate_key in used_keys:
654
  continue
 
 
 
 
 
 
 
655
  used_keys.add(candidate_key)
656
  _record_lineup_usage(candidate_row, player_columns, usage_counts)
657
  rows.append(candidate_row)
 
3
  """
4
  from __future__ import annotations
5
 
6
+ import math
7
  from collections import Counter
8
  from typing import Any, Callable
9
 
 
408
  return dict.fromkeys(pool["player_names"].unique(), 0)
409
 
410
 
411
+ def _max_appearances_for_cap(max_exposure_fraction: float, target_lineups: int) -> int:
412
+ """Max lineup appearances allowed per player (e.g. 70% of 20 lineups → 14)."""
413
+ if target_lineups < 1:
414
+ return 0
415
+ if max_exposure_fraction >= 1.0:
416
+ return target_lineups
417
+ return max(1, math.floor(max_exposure_fraction * target_lineups + 1e-9))
418
+
419
+
420
  def _exposure_excluded_players(
421
  pool_player_names,
422
  usage_counts: dict[str, int],
423
+ max_appearances: int,
 
424
  ) -> set[str]:
425
+ """Players who cannot appear in another lineup (already at max appearances)."""
426
+ return {
427
+ pname
428
+ for pname in pool_player_names
429
+ if usage_counts.get(pname, 0) >= max_appearances
430
+ }
 
 
 
 
 
 
 
431
 
432
 
433
  def _lineup_within_exposure_cap(
434
  row: pd.Series,
435
  player_columns: list[str],
436
  usage_counts: dict[str, int],
437
+ max_appearances: int,
 
438
  ) -> bool:
439
+ """True if accepting this lineup keeps every player at or below max appearances."""
 
 
 
440
  for pname in _lineup_player_names(row, player_columns):
441
+ if usage_counts.get(pname, 0) + 1 > max_appearances:
 
442
  return False
443
  return True
444
 
 
446
  def _build_active_pool(
447
  pool: pd.DataFrame,
448
  usage_counts: dict[str, int],
449
+ max_appearances: int | None,
 
450
  static_exclude: set[str],
451
  temp_exclude: set[str] | None = None,
452
  ) -> pd.DataFrame:
453
  """
454
  Reset the optimization pool from the full projections pool before each lineup.
455
 
456
+ All projection players start at 0 appearances; exclude anyone who has reached
457
+ the max allowed for the target lineup count.
458
  """
459
  pool_names = pool["player_names"].unique()
460
  exclude = set(static_exclude)
461
  if temp_exclude:
462
  exclude |= temp_exclude
463
+ if max_appearances is not None:
464
+ exclude |= _exposure_excluded_players(pool_names, usage_counts, max_appearances)
 
 
465
  if not exclude:
466
  return pool.reset_index(drop=True)
467
  return pool[~pool["player_names"].isin(exclude)].reset_index(drop=True)
 
522
  usage_counts: dict[str, int] = _init_usage_counts(pool)
523
 
524
  exposure_cap = None
525
+ max_appearances: int | None = None
526
  if max_player_exposure is not None and 0 < max_player_exposure < 100.0:
527
  exposure_cap = max(0.0, min(1.0, float(max_player_exposure) / 100.0))
528
+ max_appearances = _max_appearances_for_cap(exposure_cap, num_lineups)
529
 
530
  static_exclude: set[str] = set(exclude_players or [])
531
  stack_in_solver = bool(stack_config and stack_config.get("enabled") and stack_slot_columns)
 
532
 
533
  def _notify_progress() -> None:
534
  if progress_callback is not None:
 
557
  key = _lineup_key(row, player_columns)
558
  if key in used_keys:
559
  continue
560
+ if max_appearances is not None and not _lineup_within_exposure_cap(
561
  row,
562
  player_columns,
563
  usage_counts,
564
+ max_appearances,
 
565
  ):
566
  continue
567
  used_keys.add(key)
 
580
  type_var=type_var,
581
  optimize_by=optimize_by,
582
  )
583
+ first_pool = _build_active_pool(pool, usage_counts, max_appearances, static_exclude)
584
  first_row = try_add_lineup(seed, first_pool)
585
  if first_row is None:
586
  return pd.DataFrame(columns=player_columns)
 
607
  active_pool = _build_active_pool(
608
  pool,
609
  usage_counts,
610
+ max_appearances,
 
611
  static_exclude,
612
  temp_exclude,
613
  )
614
  if active_pool.empty:
615
  break
616
 
617
+ if stack_in_solver:
618
  next_seed = empty_lineup_row(player_columns)
619
  candidate_row = try_add_lineup(next_seed, active_pool)
620
  if candidate_row is None:
 
645
  candidate_key = _lineup_key(candidate_row, player_columns)
646
  if candidate_key in used_keys:
647
  continue
648
+ if max_appearances is not None and not _lineup_within_exposure_cap(
649
+ candidate_row,
650
+ player_columns,
651
+ usage_counts,
652
+ max_appearances,
653
+ ):
654
+ continue
655
  used_keys.add(candidate_key)
656
  _record_lineup_usage(candidate_row, player_columns, usage_counts)
657
  rows.append(candidate_row)