Skip to content

Commit

Permalink
Merge pull request #25 from team4099/curie
Browse files Browse the repository at this point in the history
Latest code from Curie
  • Loading branch information
Shom770 authored Apr 30, 2024
2 parents 73a34e3 + 1a670ce commit 3d77d4d
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 61 deletions.
76 changes: 56 additions & 20 deletions src/page_managers/match_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,17 @@ def generate_match_prediction_graphs(
).sum()
for team in combined_teams
]
if structures != Queries.TELEOP_PASSING
else [
self.calculated_stats.stat_per_match(
team, structures
).sum()
for team in combined_teams
]
for structures in (
(Queries.AUTO_AMP, Queries.TELEOP_AMP),
(Queries.AUTO_SPEAKER, Queries.TELEOP_SPEAKER),
Queries.TELEOP_TRAP
Queries.TELEOP_PASSING
)
]

Expand All @@ -329,13 +336,13 @@ def generate_match_prediction_graphs(
combined_teams,
structure_breakdown,
"Teams",
["# of Amp Cycles", "# of Speaker Cycles", "# of Trap Cycles"],
["# of Amp Cycles", "# of Speaker Cycles", "# of Feeding Cycles"],
"Total Cycles Scored into Structures",
title="Structure Breakdown",
color_map={
"# of Amp Cycles": GeneralConstants.GOLD_GRADIENT[0],
"# of Speaker Cycles": GeneralConstants.GOLD_GRADIENT[1],
"# of Trap Cycles": GeneralConstants.GOLD_GRADIENT[2]
"# of Feeding Cycles": GeneralConstants.GOLD_GRADIENT[2]
},
).update_layout(xaxis={"categoryorder": "total descending"})
)
Expand Down Expand Up @@ -503,10 +510,10 @@ def generate_alliance_dashboard(self, team_numbers: list[int], color_gradient: l
)

def generate_autonomous_graphs(
self,
team_numbers: list[int],
type_of_graph: str,
color_gradient: list[str]
self,
team_numbers: list[int],
type_of_graph: str,
color_gradient: list[str]
) -> None:
"""Generates the autonomous graphs for the `Match` page.
Expand Down Expand Up @@ -534,7 +541,8 @@ def generate_autonomous_graphs(
best_autos_by_team = sorted(
[
(
team_number, self.calculated_stats.points_contributed_by_match(team_number, Queries.AUTO).max())
team_number,
self.calculated_stats.points_contributed_by_match(team_number, Queries.AUTO).max())
for team_number in team_numbers
],
key=lambda pair: pair[1],
Expand Down Expand Up @@ -589,7 +597,8 @@ def generate_autonomous_graphs(
("Total Auto Cycles" if display_cycle_contributions else "Total Auto Points"),
title="Auto Scoring Breakdown",
color_map={
("Avg. Speaker Cycles" if display_cycle_contributions else "Avg. Speaker Points"): color_gradient[1],
("Avg. Speaker Cycles" if display_cycle_contributions else "Avg. Speaker Points"):
color_gradient[1],
("Avg. Amp Cycles" if display_cycle_contributions else "Avg. Amp Points"): color_gradient[2]
}
).update_layout(xaxis={"categoryorder": "total descending"})
Expand Down Expand Up @@ -618,10 +627,10 @@ def generate_autonomous_graphs(
)

def generate_teleop_graphs(
self,
team_numbers: list[int],
type_of_graph: str,
color_gradient: list[str]
self,
team_numbers: list[int],
type_of_graph: str,
color_gradient: list[str]
) -> None:
"""Generates the teleop graphs for the `Match` page.
Expand All @@ -633,7 +642,12 @@ def generate_teleop_graphs(
teams_data = [scouting_data_for_team(team) for team in team_numbers]
display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS

st.write("## ⭕ Cycles")
speaker_cycles_over_time_col, amp_periods_over_time_col = st.columns(2, gap="large")
passing_shot_by_team_col, = st.columns(1)

st.divider()
st.write("## ⛓️ Endgame")
climb_breakdown_by_team_col, climb_speed_by_team = st.columns(2, gap="large")

short_gradient = [
Expand Down Expand Up @@ -699,9 +713,31 @@ def generate_teleop_graphs(
)
)

with passing_shot_by_team_col:
passing_shots_by_team = [
self.calculated_stats.passing_shots_by_match(team)
for team in team_numbers
]
best_teams = sorted(zip(team_numbers, passing_shots_by_team), key=lambda pair: pair[1].mean())
color_map = {
pair[0]: color
for pair, color in zip(best_teams, short_gradient)
}

plotly_chart(
multi_line_graph(
*populate_missing_data(passing_shots_by_team),
x_axis_label="Match Index",
y_axis_label=team_numbers,
y_axis_title="# of Cycles",
title="Passing Cycles by Alliance",
color_map=color_map
)
)

with climb_breakdown_by_team_col:
harmonized_climbs_by_team = [
team_data[Queries.HARMONIZED_ON_CHAIN].sum()
team_data[Queries.HARMONIZED_ON_CHAIN].sum()
for team_data in teams_data
]
normal_climbs_by_team = [
Expand All @@ -726,7 +762,7 @@ def generate_teleop_graphs(
(team_data[Queries.CLIMB_SPEED] == "Slow").sum()
for team_data in teams_data
]

fast_climbs = [
(team_data[Queries.CLIMB_SPEED] == "Fast").sum()
for team_data in teams_data
Expand All @@ -745,9 +781,9 @@ def generate_teleop_graphs(
)

def generate_qualitative_graphs(
self,
team_numbers: list[int],
color_gradient: list[str]
self,
team_numbers: list[int],
color_gradient: list[str]
):
"""Generates the qualitative graphs for the `Match` page.
Expand All @@ -773,7 +809,7 @@ def generate_qualitative_graphs(
color=color_gradient[0]
)
)

with defense_rating_by_team_col:
defense_rating_by_team = [
self.calculated_stats.average_defense_skill(team)
Expand Down Expand Up @@ -806,4 +842,4 @@ def generate_qualitative_graphs(
title="Disables by Team",
color=color_gradient[2]
)
)
)
4 changes: 4 additions & 0 deletions src/page_managers/picklist_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ def __init__(self):
self.calculated_stats.average_cycles_for_structure,
structure=(Queries.AUTO_AMP, Queries.TELEOP_AMP)
),
"Average Feeding Cycles": self.calculated_stats.average_feeding_cycles_without_full_field,
"Avg. Adjusted Teleop Cycles (w/ Feeding)": (
lambda team: self.calculated_stats.average_cycles(team, Queries.TELEOP) + self.calculated_stats.average_feeding_cycles_without_full_field(team) / 2
),
"Average Trap Cycles": partial(
self.calculated_stats.average_cycles_for_structure,
structure=Queries.TELEOP_TRAP
Expand Down
3 changes: 2 additions & 1 deletion src/page_managers/ranking_simulator_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def generate_simulated_rankings(self, to_match: int) -> None:
for team in teams:
_, rps, avg_coop, avg_match_score, matches_played = rankings[rankings["team"] == team].iloc[0]
total_matches_played = matches_played + simulated_rankings[team][3]
print(team, total_matches_played)
total_avg_rp = (rps * matches_played + sum(simulated_rankings[team][0])) / total_matches_played
total_avg_coop = (avg_coop * matches_played + sum(simulated_rankings[team][1])) / total_matches_played
total_avg_score = (avg_match_score * matches_played + sum(simulated_rankings[team][2])) / total_matches_played
Expand All @@ -129,4 +130,4 @@ def generate_simulated_rankings(self, to_match: int) -> None:
sorted(new_rankings, key=lambda ranking: ranking[1:], reverse=True),
columns=("Team", "Average Ranking Points", "Average Coopertition", "Average Match Score")
)
st.table(ranking_df.applymap(lambda value: f"{value:.2f}" if isinstance(value, float) else value))
st.table(ranking_df.applymap(lambda value: f"{value:.2f}" if isinstance(value, float) else value))
62 changes: 36 additions & 26 deletions src/page_managers/team_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def generate_input_section(self) -> int:
:return: The team number selected to create graphs for.
"""
queried_team = int(st.experimental_get_query_params().get("team_number", [0])[0])
queried_team = int(st.experimental_get_query_params().get("team_number", [0])[0]) or 4099
return st.selectbox(
"Team Number",
(team_list := retrieve_team_list()),
Expand All @@ -57,7 +57,7 @@ def generate_metrics(self, team_number: int) -> None:
:param team_number: The team number to calculate the metrics for.
"""
points_contributed_col, drivetrain_col, auto_cycle_col, teleop_cycle_col = st.columns(4)
points_contributed_col, auto_cycle_col, teleop_cycle_col, feeding_cycle_col = st.columns(4)
iqr_col, trap_ability_col, climb_breakdown_col, disables_col = st.columns(4)

# Metric for avg. points contributed
Expand All @@ -75,22 +75,6 @@ def generate_metrics(self, team_number: int) -> None:
threshold=points_contributed_for_percentile
)

# Metric for drivetrain
with drivetrain_col:
try:
drivetrain = self.pit_scouting_data[
self.pit_scouting_data["Team Number"] == team_number
].iloc[0]["Drivetrain"].split("/")[0] # The splitting at / is used to shorten the drivetrain type.
except (IndexError, TypeError):
drivetrain = "—"

colored_metric(
"Drivetrain Type",
drivetrain,
background_color="#052e16",
opacity=0.5
)

# Metric for average auto cycles
with auto_cycle_col:
average_auto_speaker_cycles = self.calculated_stats.average_cycles_for_structure(
Expand Down Expand Up @@ -147,6 +131,27 @@ def generate_metrics(self, team_number: int) -> None:
second_threshold=average_teleop_amp_cycles_for_percentile
)

# Metric for feeding cycles of a team
with feeding_cycle_col:
average_feeding_cycles = self.calculated_stats.cycles_by_structure_per_match(
team_number,
Queries.TELEOP_PASSING
).mean()
average_feeding_cycles_for_percentile = self.calculated_stats.quantile_stat(
0.5,
lambda self, team: self.cycles_by_structure_per_match(
team,
Queries.TELEOP_PASSING
).mean()
)

colored_metric(
"Average Feeding Cycles",
average_feeding_cycles,
threshold=average_feeding_cycles_for_percentile,
value_formatter=lambda value: f"{value:.2f}"
)

# Metric for IQR of points contributed (consistency)
with iqr_col:
team_dataset = self.calculated_stats.points_contributed_by_match(
Expand Down Expand Up @@ -318,11 +323,11 @@ def generate_teleop_graphs(
:param type_of_graph: The type of graph to use for the graphs on said page (cycle contribution / point contributions).
:return:
"""
speaker_amp_col, climb_speed_col = st.columns(2)
speaker_amp_feeding_col, climb_speed_col = st.columns(2)
using_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS

# Teleop Speaker/amp over time graph
with speaker_amp_col:
# Teleop Speaker/amp/feeding over time graph
with speaker_amp_feeding_col:
speaker_cycles_by_match = self.calculated_stats.cycles_by_structure_per_match(
team_number,
Queries.TELEOP_SPEAKER
Expand All @@ -331,19 +336,24 @@ def generate_teleop_graphs(
team_number,
Queries.TELEOP_AMP
) * (1 if using_cycle_contributions else 2)
feeding_cycles_by_match = self.calculated_stats.cycles_by_structure_per_match(
team_number,
Queries.TELEOP_PASSING
)
line_names = [
("# of Speaker Cycles" if using_cycle_contributions else "# of Speaker Points"),
("# of Amp Cycles" if using_cycle_contributions else "# of Amp Points")
("# of Amp Cycles" if using_cycle_contributions else "# of Amp Points"),
"# of Passing Cycles"
]

plotly_chart(
multi_line_graph(
stacked_bar_graph(
range(len(speaker_cycles_by_match)),
[speaker_cycles_by_match, amp_cycles_by_match],
x_axis_label="Match Index",
[speaker_cycles_by_match, amp_cycles_by_match, feeding_cycles_by_match],
x_axis_label="",
y_axis_label=line_names,
y_axis_title=f"# of Teleop {'Cycles' if using_cycle_contributions else 'Points'}",
title=f"Speaker/Amp {'Cycles' if using_cycle_contributions else 'Points'} During Teleop Over Time",
title=f"Speaker/Amp/Feeding {'Cycles' if using_cycle_contributions else 'Points'} During Teleop Over Time",
color_map=dict(zip(line_names, (GeneralConstants.GOLD_GRADIENT[0], GeneralConstants.GOLD_GRADIENT[-1])))
)
)
Expand Down
45 changes: 40 additions & 5 deletions src/utils/calculated_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ def average_cycles(self, team_number: int, mode: str = None) -> float:
else:
return (self.cycles_by_match(team_number, Queries.AUTO) + self.cycles_by_match(team_number, Queries.TELEOP)).mean()

@_convert_to_float_from_numpy_type
def average_passing_cycles(self, team_number) -> float:
"""Calculates the average passing cycles for a team
The following custom graphs are supported with this function:
- Bar graph
:param team_number: The team number to calculate the average cycles for.
:return: A float representing the average cycles for said team in the mode specified."""

return self.cycles_by_match(team_number, Queries.TELEOP_PASSING).mean()

@_convert_to_float_from_numpy_type
def average_cycles_for_structure(self, team_number: int, structure: str) -> float:
"""Calculates the average cycles for a team for a structure (wrapper around `cycles_by_match`).
Expand Down Expand Up @@ -154,6 +166,22 @@ def cycles_by_match(self, team_number: int, mode: str = None) -> Series:
+ team_data[Queries.TELEOP_SPEAKER] + team_data[Queries.TELEOP_AMP] + team_data[Queries.TELEOP_TRAP]
)

def passing_shots_by_match(self, team_number: int) -> Series:
"""Returns the cycles for a certain mode (autonomous/teleop) in a match
The following custom graphs are supported with this function:
- Line graph
- Box plot
- Multi line graph
:param team_number: The team number to calculate the cycles by match for.
:param mode: The mode to return cycles by match for (Auto/Teleop)
:return: A series containing the cycles per match for the mode specified.
"""
team_data = scouting_data_for_team(team_number, self.data)

return team_data[Queries.TELEOP_PASSING]

def cycles_by_structure_per_match(self, team_number: int, structure: str | tuple) -> Series:
"""Returns the cycles for a certain structure (auto speaker, auto amp, etc.) in a match
Expand Down Expand Up @@ -233,6 +261,13 @@ def average_driver_rating(self, team_number: int) -> float:
lambda driver_rating: Criteria.DRIVER_RATING_CRITERIA.get(driver_rating, float("nan"))
).mean()

@_convert_to_float_from_numpy_type
def average_feeding_cycles_without_full_field(self, team_number: int) -> float:
"""Returns the average feeding cycles without matches where they ran full field cycles."""
team_scouting_data = scouting_data_for_team(team_number, self.data)
passing_cycles = team_scouting_data[team_scouting_data[Queries.TELEOP_PASSING] != 0][Queries.TELEOP_PASSING]
return (passing_cycles.mean()) if not passing_cycles.empty else 0

@_convert_to_float_from_numpy_type
def average_defense_rating(self, team_number: int) -> float:
"""Returns a series of data representing the team's defense rating
Expand Down Expand Up @@ -372,11 +407,11 @@ def chance_of_bonuses(self, alliance: list[int]) -> tuple[float, float, float]:

# Melody RP calculations
possible_cycle_combos = self.cartesian_product(*cycles_for_alliance, reduce_with_sum=True)
chance_of_reaching_15_cycles = (
len([combo for combo in possible_cycle_combos if combo >= 15]) / len(possible_cycle_combos)
chance_of_reaching_21_cycles = (
len([combo for combo in possible_cycle_combos if combo >= 21]) / len(possible_cycle_combos)
)
chance_of_reaching_18_cycles = (
len([combo for combo in possible_cycle_combos if combo >= 18]) / len(possible_cycle_combos)
chance_of_reaching_25_cycles = (
len([combo for combo in possible_cycle_combos if combo >= 25]) / len(possible_cycle_combos)
)

# Ensemble RP calculations
Expand All @@ -395,7 +430,7 @@ def chance_of_bonuses(self, alliance: list[int]) -> tuple[float, float, float]:

return (
chance_of_coop,
chance_of_reaching_18_cycles * (1 - chance_of_coop) + chance_of_reaching_15_cycles * chance_of_coop,
chance_of_reaching_21_cycles * (1 - chance_of_coop) + chance_of_reaching_25_cycles * chance_of_coop,
chance_of_ensemble_rp
)

Expand Down
Loading

0 comments on commit 3d77d4d

Please sign in to comment.