James McCool commited on
Commit
cc6112d
·
1 Parent(s): 6d510d7

more fixes towards exposure limiting

Browse files
Files changed (1) hide show
  1. global_func/build_optimal_lineups.py +25 -170
global_func/build_optimal_lineups.py CHANGED
@@ -402,25 +402,28 @@ def _record_lineup_usage(
402
  usage_counts[pname] = usage_counts.get(pname, 0) + 1
403
 
404
 
 
 
 
 
 
405
  def _exposure_excluded_players(
 
406
  usage_counts: dict[str, int],
407
  num_lineups_built: int,
408
  max_exposure_fraction: float,
409
  ) -> set[str]:
410
  """
411
- Players to remove from the pool after ``num_lineups_built`` lineups exist.
412
 
413
- Excludes anyone at or above the cap on current usage (count / built), and anyone
414
- who would exceed the cap if they appeared in one more lineup.
415
  """
416
  if num_lineups_built <= 0:
417
  return set()
418
  excluded: set[str] = set()
419
- next_total = num_lineups_built + 1
420
- for pname, count in usage_counts.items():
421
- if count / num_lineups_built >= max_exposure_fraction - 1e-9:
422
- excluded.add(pname)
423
- elif (count + 1) / next_total > max_exposure_fraction + 1e-9:
424
  excluded.add(pname)
425
  return excluded
426
 
@@ -451,79 +454,25 @@ def _build_active_pool(
451
  static_exclude: set[str],
452
  temp_exclude: set[str] | None = None,
453
  ) -> pd.DataFrame:
454
- """Player pool for the next lineup after applying exposure and user exclusions."""
 
 
 
 
 
 
455
  exclude = set(static_exclude)
456
  if temp_exclude:
457
  exclude |= temp_exclude
458
  if max_exposure_fraction is not None and num_lineups_built > 0:
459
  exclude |= _exposure_excluded_players(
460
- usage_counts, num_lineups_built, max_exposure_fraction
461
  )
462
  if not exclude:
463
- return pool
464
  return pool[~pool["player_names"].isin(exclude)].reset_index(drop=True)
465
 
466
 
467
- def _print_exposure_debug(
468
- *,
469
- label: str,
470
- num_lineups_built: int,
471
- num_lineups_target: int,
472
- usage_counts: dict[str, int],
473
- full_pool: pd.DataFrame,
474
- active_pool: pd.DataFrame,
475
- max_exposure_fraction: float | None,
476
- static_exclude: set[str],
477
- temp_exclude: set[str] | None = None,
478
- attempt: int | None = None,
479
- ) -> None:
480
- """Stdout debug: player usage rates and who is in the optimization pool."""
481
- cap_pct = max_exposure_fraction * 100 if max_exposure_fraction is not None else None
482
- attempt_str = f" attempt={attempt}" if attempt is not None else ""
483
- print(
484
- f"\n[optimizer exposure] {label} | built={num_lineups_built}/{num_lineups_target}"
485
- f"{attempt_str} | cap={cap_pct}%"
486
- )
487
- print(f" full_pool={len(full_pool)} players | active_pool={len(active_pool)} players")
488
-
489
- if usage_counts and num_lineups_built > 0:
490
- usage_rows = []
491
- for pname, count in sorted(usage_counts.items(), key=lambda x: (-x[1], x[0])):
492
- rate = count / num_lineups_built
493
- usage_rows.append(f" {pname}: {count}/{num_lineups_built} ({rate:.1%})")
494
- print(f" usage ({len(usage_counts)} players with appearances):")
495
- for line in usage_rows[:25]:
496
- print(line)
497
- if len(usage_rows) > 25:
498
- print(f" ... and {len(usage_rows) - 25} more")
499
- elif num_lineups_built == 0:
500
- print(" usage: (none yet — first lineup)")
501
-
502
- if max_exposure_fraction is not None and num_lineups_built > 0:
503
- exposure_blocked = _exposure_excluded_players(
504
- usage_counts, num_lineups_built, max_exposure_fraction
505
- )
506
- print(f" exposure_blocked={len(exposure_blocked)} players")
507
- if exposure_blocked:
508
- sample = sorted(exposure_blocked)[:15]
509
- print(f" sample: {', '.join(sample)}")
510
- if len(exposure_blocked) > 15:
511
- print(f" ... and {len(exposure_blocked) - 15} more")
512
-
513
- if static_exclude:
514
- print(f" user_excluded={len(static_exclude)} players")
515
- if temp_exclude:
516
- print(f" temp_excluded (diversity)={len(temp_exclude)}: {', '.join(sorted(temp_exclude))}")
517
-
518
- if not active_pool.empty:
519
- avail = active_pool["player_names"].tolist()
520
- print(f" pool_available ({len(avail)}): {', '.join(avail[:20])}")
521
- if len(avail) > 20:
522
- print(f" ... and {len(avail) - 20} more")
523
- else:
524
- print(" pool_available: EMPTY")
525
-
526
-
527
  def generate_optimal_lineups(
528
  player_pool: pd.DataFrame,
529
  player_columns: list[str],
@@ -576,19 +525,15 @@ def generate_optimal_lineups(
576
  lock_teams = lock_teams or []
577
  rows: list[pd.Series] = []
578
  used_keys: set[frozenset] = set()
579
- usage_counts: dict[str, int] = {}
580
 
581
  exposure_cap = None
582
  if max_player_exposure is not None and 0 < max_player_exposure < 100.0:
583
  exposure_cap = max(0.0, min(1.0, float(max_player_exposure) / 100.0))
584
- print(
585
- f"\n[optimizer exposure] START build | target_lineups={num_lineups} "
586
- f"| cap={max_player_exposure}% | full_pool={len(pool)}"
587
- )
588
 
589
  static_exclude: set[str] = set(exclude_players or [])
590
  stack_in_solver = bool(stack_config and stack_config.get("enabled") and stack_slot_columns)
591
- debug_exposure = exposure_cap is not None
592
 
593
  def _notify_progress() -> None:
594
  if progress_callback is not None:
@@ -596,11 +541,9 @@ def generate_optimal_lineups(
596
 
597
  def try_add_lineup(seed: pd.Series, active_pool: pd.DataFrame) -> pd.Series | None:
598
  if active_pool.empty:
599
- if debug_exposure:
600
- print(" [try_add_lineup] skipped — active_pool is empty")
601
  return None
602
  max_tries = 8 if exposure_cap is not None else (5 if stack_in_solver else 8)
603
- for try_idx in range(max_tries):
604
  row = _build_one_lineup(
605
  seed,
606
  player_columns,
@@ -615,13 +558,9 @@ def generate_optimal_lineups(
615
  stack_slot_columns,
616
  )
617
  if row is None:
618
- if debug_exposure:
619
- print(f" [try_add_lineup] try {try_idx + 1}/{max_tries}: solver returned None")
620
  continue
621
  key = _lineup_key(row, player_columns)
622
  if key in used_keys:
623
- if debug_exposure:
624
- print(f" [try_add_lineup] try {try_idx + 1}/{max_tries}: duplicate lineup")
625
  continue
626
  if exposure_cap is not None and not _lineup_within_exposure_cap(
627
  row,
@@ -630,23 +569,10 @@ def generate_optimal_lineups(
630
  len(rows),
631
  exposure_cap,
632
  ):
633
- if debug_exposure:
634
- over = [
635
- p
636
- for p in _lineup_player_names(row, player_columns)
637
- if (usage_counts.get(p, 0) + 1) / (len(rows) + 1)
638
- > exposure_cap + 1e-9
639
- ]
640
- print(
641
- f" [try_add_lineup] try {try_idx + 1}/{max_tries}: "
642
- f"over exposure cap — {over[:8]}"
643
- )
644
  continue
645
  used_keys.add(key)
646
  _record_lineup_usage(row, player_columns, usage_counts)
647
  return row
648
- if debug_exposure:
649
- print(f" [try_add_lineup] failed after {max_tries} tries")
650
  return None
651
 
652
  seed = seed_row_with_constraints(
@@ -661,35 +587,11 @@ def generate_optimal_lineups(
661
  optimize_by=optimize_by,
662
  )
663
  first_pool = _build_active_pool(pool, usage_counts, 0, exposure_cap, static_exclude)
664
- if debug_exposure:
665
- _print_exposure_debug(
666
- label="before lineup 1",
667
- num_lineups_built=0,
668
- num_lineups_target=num_lineups,
669
- usage_counts=usage_counts,
670
- full_pool=pool,
671
- active_pool=first_pool,
672
- max_exposure_fraction=exposure_cap,
673
- static_exclude=static_exclude,
674
- )
675
  first_row = try_add_lineup(seed, first_pool)
676
  if first_row is None:
677
- if debug_exposure:
678
- print("[optimizer exposure] FAILED — could not build first lineup")
679
  return pd.DataFrame(columns=player_columns)
680
  rows.append(first_row)
681
  _notify_progress()
682
- if debug_exposure:
683
- _print_exposure_debug(
684
- label="after lineup 1 accepted",
685
- num_lineups_built=len(rows),
686
- num_lineups_target=num_lineups,
687
- usage_counts=usage_counts,
688
- full_pool=pool,
689
- active_pool=first_pool,
690
- max_exposure_fraction=exposure_cap,
691
- static_exclude=static_exclude,
692
- )
693
 
694
  max_attempts = max(num_lineups * 50, 100) if exposure_cap is not None else max(num_lineups * 20, 40)
695
  attempts = 0
@@ -697,7 +599,7 @@ def generate_optimal_lineups(
697
  attempts += 1
698
  num_built = len(rows)
699
  temp_exclude: set[str] = set()
700
- if stack_in_solver:
701
  drop_name = _exclude_player_for_next_lineup(
702
  rows[-1],
703
  pool,
@@ -716,35 +618,13 @@ def generate_optimal_lineups(
716
  static_exclude,
717
  temp_exclude,
718
  )
719
- if debug_exposure and (attempts == 1 or attempts % 10 == 0):
720
- _print_exposure_debug(
721
- label=f"before lineup {num_built + 1}",
722
- num_lineups_built=num_built,
723
- num_lineups_target=num_lineups,
724
- usage_counts=usage_counts,
725
- full_pool=pool,
726
- active_pool=active_pool,
727
- max_exposure_fraction=exposure_cap,
728
- static_exclude=static_exclude,
729
- temp_exclude=temp_exclude,
730
- attempt=attempts,
731
- )
732
  if active_pool.empty:
733
- if debug_exposure:
734
- print(
735
- f"[optimizer exposure] STOP — active_pool empty at attempt {attempts} "
736
- f"(built {num_built}/{num_lineups})"
737
- )
738
  break
739
 
740
- if stack_in_solver:
741
  next_seed = empty_lineup_row(player_columns)
742
  candidate_row = try_add_lineup(next_seed, active_pool)
743
  if candidate_row is None:
744
- if debug_exposure and attempts <= 3:
745
- print(
746
- f" [stack path] attempt {attempts}: try_add_lineup returned None"
747
- )
748
  continue
749
  rows.append(candidate_row)
750
  _notify_progress()
@@ -764,44 +644,19 @@ def generate_optimal_lineups(
764
  used_keys,
765
  )
766
  if candidate_row is None:
767
- if debug_exposure and attempts <= 5:
768
- print(f" [swap path] attempt {attempts}: _one_swap_below returned None")
769
  continue
770
  if not lineup_satisfies_stack(
771
  candidate_row, stack_config, stack_slot_columns, map_dict["team_map"]
772
  ):
773
- if debug_exposure and attempts <= 5:
774
- print(f" [swap path] attempt {attempts}: stack check failed")
775
  continue
776
  candidate_key = _lineup_key(candidate_row, player_columns)
777
  if candidate_key in used_keys:
778
- if debug_exposure and attempts <= 5:
779
- print(
780
- f" [swap path] attempt {attempts}: duplicate lineup "
781
- f"(same as prior — swap found nothing new)"
782
- )
783
- continue
784
- if exposure_cap is not None and not _lineup_within_exposure_cap(
785
- candidate_row,
786
- player_columns,
787
- usage_counts,
788
- num_built,
789
- exposure_cap,
790
- ):
791
- if debug_exposure and attempts <= 5:
792
- print(f" [swap path] attempt {attempts}: rejected by exposure cap")
793
  continue
794
  used_keys.add(candidate_key)
795
  _record_lineup_usage(candidate_row, player_columns, usage_counts)
796
  rows.append(candidate_row)
797
  _notify_progress()
798
 
799
- if debug_exposure:
800
- print(
801
- f"\n[optimizer exposure] END build | built={len(rows)}/{num_lineups} "
802
- f"| total_attempts={attempts}"
803
- )
804
-
805
  result = pd.DataFrame(rows)
806
  objectives = [
807
  calculate_lineup_objective(result.iloc[i], player_columns, pool, metric_col)
 
402
  usage_counts[pname] = usage_counts.get(pname, 0) + 1
403
 
404
 
405
+ def _init_usage_counts(pool: pd.DataFrame) -> dict[str, int]:
406
+ """Every player in the projections pool starts at 0 appearances (0% exposure)."""
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
 
 
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)
474
 
475
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  def generate_optimal_lineups(
477
  player_pool: pd.DataFrame,
478
  player_columns: list[str],
 
525
  lock_teams = lock_teams or []
526
  rows: list[pd.Series] = []
527
  used_keys: set[frozenset] = set()
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:
 
541
 
542
  def try_add_lineup(seed: pd.Series, active_pool: pd.DataFrame) -> pd.Series | None:
543
  if active_pool.empty:
 
 
544
  return None
545
  max_tries = 8 if exposure_cap is not None else (5 if stack_in_solver else 8)
546
+ for _ in range(max_tries):
547
  row = _build_one_lineup(
548
  seed,
549
  player_columns,
 
558
  stack_slot_columns,
559
  )
560
  if row is None:
 
 
561
  continue
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,
 
569
  len(rows),
570
  exposure_cap,
571
  ):
 
 
 
 
 
 
 
 
 
 
 
572
  continue
573
  used_keys.add(key)
574
  _record_lineup_usage(row, player_columns, usage_counts)
575
  return row
 
 
576
  return None
577
 
578
  seed = seed_row_with_constraints(
 
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)
593
  rows.append(first_row)
594
  _notify_progress()
 
 
 
 
 
 
 
 
 
 
 
595
 
596
  max_attempts = max(num_lineups * 50, 100) if exposure_cap is not None else max(num_lineups * 20, 40)
597
  attempts = 0
 
599
  attempts += 1
600
  num_built = len(rows)
601
  temp_exclude: set[str] = set()
602
+ if stack_in_solver and exposure_cap is None:
603
  drop_name = _exclude_player_for_next_lineup(
604
  rows[-1],
605
  pool,
 
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:
 
 
 
 
628
  continue
629
  rows.append(candidate_row)
630
  _notify_progress()
 
644
  used_keys,
645
  )
646
  if candidate_row is None:
 
 
647
  continue
648
  if not lineup_satisfies_stack(
649
  candidate_row, stack_config, stack_slot_columns, map_dict["team_map"]
650
  ):
 
 
651
  continue
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)
658
  _notify_progress()
659
 
 
 
 
 
 
 
660
  result = pd.DataFrame(rows)
661
  objectives = [
662
  calculate_lineup_objective(result.iloc[i], player_columns, pool, metric_col)