Ejemplo n.º 1
0
def generate_stats_layout(positions_source, comparison_source,
                          constructor_results, year_id, driver_id):
    """
    Year summary div, including:
    - WDC place
    - Highest race finish
    - Number of races
    - Points
    - Points per race
    - Number of wins and where were they
    - Number of podiums and where were they
    - Teammates
    - Constructors
    - Mean gap to teammate in positions
    - Mean grid position
    - Mean finish position
    :param positions_source: Positions source
    :param comparison_source: Comparison source (from teammate comparison line plot)
    :param constructor_results: Constructor results (from results.csv)
    :param year_id: Year
    :param driver_id: Driver ID
    :return: Stats layout
    """
    logging.info("Generating year driver stats layout")
    if positions_source.shape[0] == 0:
        return Div(text="")
    wdc_final_standing = positions_source["wdc_final_standing"].mode()
    if wdc_final_standing.shape[0] > 0:
        wdc_final_standing_str = int_to_ordinal(wdc_final_standing.values[0])
    else:
        wdc_final_standing_str = ""
    highest_race_finish_idx = positions_source["finish_position_int"].idxmin()
    if np.isnan(highest_race_finish_idx):
        highest_race_finish_str = ""
    else:
        highest_race_finish = positions_source.loc[highest_race_finish_idx,
                                                   "finish_position_int"]
        round_name = positions_source.loc[highest_race_finish_idx, "roundName"]
        highest_race_finish_str = int_to_ordinal(
            highest_race_finish) + " at " + round_name
        highest_race_finish_str = highest_race_finish_str.strip()
    num_races = positions_source.shape[0]
    num_races_str = str(num_races)
    points = positions_source["points"].max()
    if np.isnan(points):
        points_str = ""
    elif points <= 0:
        points_str = str(points) + " (0 pts/race)"
    else:
        points_str = str(points) + " (" + str(round(points / num_races,
                                                    1)) + " pts/race)"
    wins_slice = positions_source[positions_source["finish_position_int"] == 1]
    num_wins = wins_slice.shape[0]
    if num_wins == 0:
        wins_str = str(num_wins)
    else:
        wins_str = str(num_wins) + " (" + ", ".join(
            wins_slice["roundName"]) + ")"
        if len(wins_str) > 95:
            split = wins_str.split(" ")
            split.insert(int(len(split) / 2), "<br>    " + "".ljust(20))
            wins_str = " ".join(split)
    podiums_slice = positions_source[
        positions_source["finish_position_int"] <= 3]
    num_podiums = podiums_slice.shape[0]
    if num_podiums == 0:
        podiums_str = str(num_podiums)
    else:
        podiums_str = str(num_podiums) + " (" + ", ".join(
            podiums_slice["roundName"]) + ")"
        if len(podiums_str) > 95:
            split = podiums_str.split(" ")
            split.insert(int(len(split) / 2), "<br>    " + "".ljust(20))
            podiums_str = " ".join(split)
    teammate_dids = set(constructor_results["driverId"].unique()) - {driver_id}
    teammate_names = []
    for did in teammate_dids:
        teammate_names.append(get_driver_name(did, include_flag=False))
    teammate_str = ", ".join(teammate_names)
    constructor_cids = set(constructor_results["constructorId"].unique())
    constructor_names = []
    for cid in constructor_cids:
        constructor_names.append(get_constructor_name(cid, include_flag=True))
    constructors_str = ", ".join(constructor_names)
    mean_grid_pos = positions_source["grid"].replace("", np.nan).mean()
    if np.isnan(mean_grid_pos):
        mean_grid_pos_str = ""
    else:
        mean_grid_pos_str = str(round(mean_grid_pos, 1))
    mean_finish_pos = positions_source["finish_position_int"].mean()
    if np.isnan(mean_finish_pos):
        mean_finish_pos_str = ""
    else:
        mean_finish_pos_str = str(round(mean_finish_pos, 1))
    mean_teammate_gap_pos = (comparison_source["driver_fp"] -
                             comparison_source["teammate_fp"]).mean()
    if np.isnan(mean_teammate_gap_pos):
        mean_teammate_gap_pos_str = ""
    else:
        mean_teammate_gap_pos_str = str(abs(round(mean_teammate_gap_pos, 1))) + " places " + \
                                    ("better" if mean_teammate_gap_pos < 0 else "worse") + " than teammate"

    # Construct the HTML
    header_template = """
    <h2 style="text-align: left;"><b>{}</b></h2>
    """
    template = """
    <pre><b>{}</b> {}<br></pre>
    """

    driver_name = get_driver_name(driver_id, include_flag=False)
    driver_stats = header_template.format(
        f"{driver_name}'s Stats for the {year_id} Season")
    driver_stats += template.format("WDC Final Pos.: ".ljust(20),
                                    wdc_final_standing_str)
    driver_stats += template.format("Num. Races: ".ljust(20), num_races_str)
    if num_wins == 0:
        driver_stats += template.format("Best Finish Pos.: ".ljust(20),
                                        highest_race_finish_str)
    driver_stats += template.format("Wins: ".ljust(20), wins_str)
    driver_stats += template.format("Podiums: ".ljust(20), podiums_str)
    driver_stats += template.format("Points: ".ljust(20), points_str)
    driver_stats += template.format("Constructor(s): ".ljust(20),
                                    constructors_str)
    driver_stats += template.format("Teammate(s): ".ljust(20), teammate_str)
    driver_stats += template.format("Avg. Grid Pos.: ".ljust(20),
                                    mean_grid_pos_str)
    driver_stats += template.format("Avg. Finish Pos.: ".ljust(20),
                                    mean_finish_pos_str)
    driver_stats += template.format("Avg. Gap to T.M.: ".ljust(20),
                                    mean_teammate_gap_pos_str)

    return Div(text=driver_stats)
Ejemplo n.º 2
0
def generate_stats_layout(ycd_results,
                          ycd_pit_stop_data,
                          ycd_fastest_lap_data,
                          year_driver_standings,
                          race_results,
                          quali_source,
                          race_id,
                          circuit_id,
                          driver_id,
                          download_image=True):
    """
    Stats div including:
    - Location
    - Date
    - Weather
    - Rating
    - Constructor
    - Qualifying position and time
    - Laps
    - Fastest lap time along with rank
    - Average lap time
    - Basic teammate info (i.e. teammate finish in 5th with an average lap time of 1:15.125)
    - Finish position
    - Finish time
    - Points scored
    - Num pit stops
    - WDC impact (WDC place before, WDC after)
    :param ycd_results: YCD results
    :param ycd_pit_stop_data: YCD pit stop data
    :param ycd_fastest_lap_data: YCD fastest lap data
    :param year_driver_standings: YCD driver standings
    :param race_results: Race results
    :param quali_source: Quali source
    :param race_id: Race ID
    :param circuit_id: Circuit ID
    :param driver_id: Driver ID
    :param download_image: Whether to actually download the image
    :return: Stats layout
    """
    logging.info("Generating race stats layout")
    # Track image
    if download_image:
        image_url = str(circuits.loc[circuit_id, "imgUrl"])
        image_view = plot_image_url(image_url)
        disclaimer = Div(
            text="The image is of the current configuration of the track.")
        image_view = column([image_view, disclaimer],
                            sizing_mode="stretch_both")
    else:
        image_view = Div()

    # Race info
    race = races.loc[race_id]
    round_num = race["round"]
    circuit = circuits.loc[circuit_id]
    date = race["datetime"].split(" ")
    if len(date) > 0:
        date_str = date[0]
    else:
        date_str = race["datetime"]
    location_str = circuit["location"] + ", " + circuit["country"]
    circuit_str = circuit["name"] + " (" + location_str + ")"
    weather = race["weather"]
    if weather is None or weather == "":
        weather_str = ""
    else:
        weather_str = str(weather).title()
    sc = race["SCLaps"]
    if np.isnan(sc):
        sc_str = ""
    else:
        sc_str = str(int(sc)) + " laps under safety car"
    rating = race["rating"]
    if np.isnan(rating):
        rating_str = ""
    else:
        rating_str = str(round(rating, 1)) + " / 10"
    ycd_driver_standings = year_driver_standings[
        (year_driver_standings["raceId"] == race_id)
        & (year_driver_standings["driverId"] == driver_id)]
    ycd_driver_standings_prev = year_driver_standings[
        (year_driver_standings["raceId"] == race_id - 1)
        & (year_driver_standings["driverId"] == driver_id)]
    if round_num == 1 and ycd_driver_standings.shape[0] > 0:
        wdc_impact_str = int_to_ordinal(
            ycd_driver_standings["positionText"].values[0])
    elif round_num > 1 and ycd_driver_standings.shape[
            0] > 0 and ycd_driver_standings_prev.shape[0] > 0:
        wdc_impact_str = "from " + int_to_ordinal(ycd_driver_standings_prev["position"].values[0]) + " to " + \
                         int_to_ordinal(ycd_driver_standings["position"].values[0])
    else:
        wdc_impact_str = ""
    if ycd_pit_stop_data.shape[0] > 0:
        num_pit_stops_str = str(ycd_pit_stop_data.shape[0])
    else:
        num_pit_stops_str = ""
    if ycd_results.shape[0] > 0:
        ycd_results_row = ycd_results.iloc[0]
        constructor_id = ycd_results_row["constructorId"]
        constructor_str = get_constructor_name(constructor_id)
        grid_str = int_to_ordinal(ycd_results_row["grid"]).strip()
        fp_str, _ = result_to_str(ycd_results_row["positionOrder"],
                                  ycd_results_row["statusId"])
        fp_str = fp_str.strip()
        laps_str = str(ycd_results_row["laps"])
        runtime_str = millis_to_str(ycd_results_row["milliseconds"]).strip()
        points = ycd_results_row["points"]
        if abs(int(points) - points) < 0.01:
            points = int(points)
        points_str = str(points)
        teammates = set(
            race_results[race_results["constructorId"] ==
                         constructor_id]["driverId"].values) - {driver_id}
        teammate_strs = []
        for teammate_did in teammates:
            teammate_result = race_results[race_results["driverId"] ==
                                           teammate_did]
            if teammate_result.shape[0] > 0:
                tm_result_row = teammate_result.iloc[0]
                tm_name = get_driver_name(teammate_did)
                tm_fp_str, _ = result_to_str(tm_result_row["positionOrder"],
                                             tm_result_row["statusId"])
                tm_time_str = millis_to_str(tm_result_row["milliseconds"])
                if "ret" in tm_fp_str.lower():
                    teammate_strs.append(tm_name + " " + tm_fp_str)
                else:
                    teammate_strs.append(tm_name + " finished " +
                                         tm_fp_str.strip() + " (" +
                                         tm_time_str.strip() + ")")
        teammate_str = ", ".join(teammate_strs)
    else:
        constructor_str = ""
        grid_str = ""
        fp_str = ""
        laps_str = ""
        runtime_str = ""
        points_str = ""
        teammate_str = ""
    ycd_quali_source = quali_source[quali_source["driver_id"] == driver_id]
    if ycd_quali_source.shape[0] > 0:
        ycd_quali_row = ycd_quali_source.iloc[0]
        quali_pos = ycd_quali_row["quali_position"]
        quali_pos_str = int_to_ordinal(quali_pos).strip()
        quali_time_str = ""
        if "q1" in ycd_quali_source.columns and ycd_quali_row["q1"] != "~":
            quali_time_str = ycd_quali_row["q1"]
        if "q2" in ycd_quali_source.columns and ycd_quali_row["q2"] != "~":
            quali_time_str = ycd_quali_row["q2"]
        if "q3" in ycd_quali_source.columns and ycd_quali_row["q3"] != "~":
            quali_time_str = ycd_quali_row["q3"]
        quali_time_str = quali_time_str.strip()
    else:
        quali_pos_str = ""
        quali_time_str = ""
    if ycd_fastest_lap_data.shape[0] > 0:
        ycd_fastest_lap_data_row = ycd_fastest_lap_data.iloc[0]
        if np.isnan(ycd_fastest_lap_data_row["fastest_lap_time_millis"]):
            fastest_lap_str = ""
        else:
            fastest_lap_str = ycd_fastest_lap_data_row["fastest_lap_time_str"] + " (" + \
                              int_to_ordinal(ycd_fastest_lap_data_row["rank"]).strip() + " fastest this race)"
        avg_lap_time_str = millis_to_str(
            ycd_fastest_lap_data_row["avg_lap_time_millis"])
    else:
        fastest_lap_str = ""
        avg_lap_time_str = ""

    header_template = """
    <h2 style="text-align: center;"><b>{}</b></h2>
    """

    template = """
    <pre><b>{}</b> {}<br></pre>
    """

    race_name = get_race_name(race_id, include_year=True)
    driver_name = get_driver_name(driver_id)
    ycd_stats = header_template.format(driver_name + " at the " + race_name)

    ycd_stats += template.format("Circuit Name: ".ljust(22), circuit_str)
    ycd_stats += template.format("Date: ".ljust(22), date_str)
    if weather_str != "" and weather_str.lower() != "nan":
        ycd_stats += template.format("Weather: ".ljust(22), weather_str)
    if not np.isnan(rating):
        ycd_stats += template.format("Rating: ".ljust(22), rating_str)
    if not np.isnan(sc):
        ycd_stats += template.format("Safety Car Laps: ".ljust(22), sc_str)
    if wdc_impact_str != "":
        if round_num == 1:
            ycd_stats += template.format("WDC Position: ".ljust(22),
                                         wdc_impact_str)
        else:
            ycd_stats += template.format("WDC Impact: ".ljust(22),
                                         wdc_impact_str)
    if ycd_pit_stop_data.shape[0] > 0:
        ycd_stats += template.format("Num Pit Stops: ".ljust(22),
                                     num_pit_stops_str)
    if quali_pos_str != "":
        ycd_stats += template.format("Qualifying Position: ".ljust(22),
                                     quali_pos_str)
    if quali_time_str != "":
        ycd_stats += template.format("Qualifying Time: ".ljust(22),
                                     quali_time_str)
    if ycd_results.shape[0] > 0:
        ycd_stats += template.format("Constructor: ".ljust(22),
                                     constructor_str)
        ycd_stats += template.format("Grid Position: ".ljust(22), grid_str)
        ycd_stats += template.format("Finish Position: ".ljust(22), fp_str)
        ycd_stats += template.format("Num Laps: ".ljust(22), laps_str)
        ycd_stats += template.format("Race Time: ".ljust(22), runtime_str)
        ycd_stats += template.format("Points Earned: ".ljust(22), points_str)
        ycd_stats += template.format("Teammate(s): ".ljust(22), teammate_str)
    if ycd_fastest_lap_data.shape[0] > 0:
        if fastest_lap_str != "":
            ycd_stats += template.format("Fastest Lap Time: ".ljust(22),
                                         fastest_lap_str)
        ycd_stats += template.format("Avg. Lap Time: ".ljust(22),
                                     avg_lap_time_str)

    divider = vdivider()
    return row([image_view, divider, Div(text=ycd_stats)],
               sizing_mode="stretch_both")
Ejemplo n.º 3
0
def generate_results_table(yd_results, yd_fastest_lap_data, year_results,
                           year_fastest_lap_data, driver_id):
    """
    Table of results at each race, including quali position, finish position (or reason for DNF), time, gap to leader,
    fastest lap time and gap to fastest lap (of all drivers), average lap time and gap to fastest average lap time
    (of all drivers)
    :param yd_results: YD results
    :param yd_fastest_lap_data: YD fastest lap data
    :param year_results: Year results
    :param year_fastest_lap_data: Year fastest lap data
    :param driver_id: Driver ID
    :return: Results table
    """
    logging.info("Generating results table")
    source = pd.DataFrame(columns=[
        "race_name", "constructor_name", "quali_pos_str", "finish_pos_str",
        "time_str", "fastest_lap_time_str", "avg_lap_time_str"
    ])
    for idx, results_row in yd_results.iterrows():
        rid = results_row["raceId"]
        race_results = year_results[year_results["raceId"] == rid]
        race_fastest_lap_data = year_fastest_lap_data[
            year_fastest_lap_data["raceId"] == rid]
        race_driver_fastest_lap_data = yd_fastest_lap_data[
            yd_fastest_lap_data["raceId"] == rid]
        race_name = get_race_name(rid)
        constructor_name = get_constructor_name(results_row["constructorId"])
        quali_pos_str = int_to_ordinal(results_row["grid"])
        finish_pos = str(results_row["positionOrder"])
        status_id = results_row["statusId"]
        finish_pos_str, finish_pos = result_to_str(finish_pos, status_id)
        classification = get_status_classification(status_id)
        time = results_row["milliseconds"]
        winner = race_results[race_results["positionOrder"] == 1]
        if winner.shape[0] > 0 and winner["driverId"].values[0] != driver_id \
                and not np.isnan(time) and not np.isnan(results_row["position"]):
            time_gap = millis_to_str(time - winner["milliseconds"].values[0])
            time_str = millis_to_str(time) + " (+" + time_gap + ")"
            if status_id != 1 and classification == "finished":
                time_str = millis_to_str(
                    time) + " (+" + time_gap + ", " + status.loc[
                        status_id, "status"] + ")"
        elif finish_pos == 1:
            time_str = millis_to_str(time)
        else:
            time_str = "~"
        if race_driver_fastest_lap_data.shape[0] > 0:
            fastest_lap_time = race_driver_fastest_lap_data[
                "fastest_lap_time_millis"].values[0]
            fastest_lap_time_str = millis_to_str(fastest_lap_time)
            if race_driver_fastest_lap_data["rank"].values[0] == " 1":
                fastest_lap_time_str = fastest_lap_time_str + " (Fastest)"
            else:
                fastest_time = race_fastest_lap_data[
                    race_fastest_lap_data["rank"] ==
                    " 1"]["fastest_lap_time_millis"]
                if fastest_time.shape[0] > 0 and not np.isnan(
                        fastest_lap_time):
                    fastest_time = fastest_time.values[0]
                    fastest_gap = millis_to_str(fastest_lap_time -
                                                fastest_time)
                    fastest_lap_time_str = millis_to_str(
                        fastest_lap_time) + " (+" + fastest_gap + ")"
            if fastest_lap_time_str == "":
                fastest_lap_time_str = "~"
            fastest_avg_idx = race_fastest_lap_data[
                "avg_lap_time_millis"].idxmin()
            avg_lap_time = race_driver_fastest_lap_data[
                "avg_lap_time_millis"].values[0]
            if np.isnan(avg_lap_time):
                avg_lap_time_str = "~"
            elif race_fastest_lap_data.loc[
                    fastest_avg_idx,
                    "driver_id"] == driver_id or np.isnan(avg_lap_time):
                avg_lap_time_str = millis_to_str(
                    avg_lap_time) + " (Fastest Avg.)"
            else:
                fastest_avg_time = race_fastest_lap_data.loc[
                    fastest_avg_idx, "avg_lap_time_millis"]
                avg_gap = millis_to_str(avg_lap_time - fastest_avg_time)
                avg_lap_time_str = millis_to_str(
                    avg_lap_time) + " (+" + avg_gap + ")"
        else:
            fastest_lap_time_str = "~"
            avg_lap_time_str = "~"
        source = source.append(
            {
                "race_name": race_name,
                "constructor_name": constructor_name,
                "quali_pos_str": quali_pos_str,
                "finish_pos_str": finish_pos_str,
                "time_str": time_str,
                "fastest_lap_time_str": fastest_lap_time_str,
                "avg_lap_time_str": avg_lap_time_str
            },
            ignore_index=True)

    results_columns = [
        TableColumn(field="race_name", title="Race Name", width=100),
        TableColumn(field="constructor_name", title="Constructor", width=100),
        TableColumn(field="quali_pos_str", title="Grid Pos.", width=75),
        TableColumn(field="finish_pos_str", title="Finish Pos.", width=75),
        TableColumn(field="time_str", title="Time", width=100),
        TableColumn(field="fastest_lap_time_str",
                    title="Fastest Lap Time",
                    width=75),
        TableColumn(field="avg_lap_time_str", title="Avg. Lap Time", width=75),
    ]
    results_table = DataTable(source=ColumnDataSource(data=source),
                              columns=results_columns,
                              index_position=None,
                              height=27 * yd_results.shape[0])
    title = Div(
        text=
        f"<h2><b>Results for each race</b></h2><br><i>The fastest lap time and average lap time gaps "
        f"shown are calculated based on the gap to the fastest of all drivers and fastest average of "
        f"all drivers in that race respectively.</i>")
    return column(
        [title, row([results_table], sizing_mode="stretch_width")],
        sizing_mode="stretch_width")
Ejemplo n.º 4
0
def generate_win_plot(positions_source, year_results):
    """
    Plot number of wins, number of podiums, number of points, win percent, podium percent, and points per race as a
    percentage of #1's points on one plot.
    Note, this is different from the regular win plot (no num races, instead points and pts per race).
    :param positions_source: Positions source
    :param year_results: Year results
    :return: Win plot layout
    """
    # TODO maybe even refactor this to use a version in driver, but this also has points and pts/race support
    # Inspired by driver.generate_win_plot
    logging.info("Generating win plot")
    if isinstance(positions_source, dict):
        return Div(text="")
    win_source = pd.DataFrame(columns=[
        "x", "win_pct", "wins", "win_pct_str", "podium_pct", "podiums",
        "podium_pct_str", "ppr_pct", "points", "ppr_pct_str"
        "constructor_name", "wdc_final_standing", "wdc_final_standing_str"
    ])
    wins = 0
    podiums = 0
    n_races = 0
    max_potential_points = 0
    max_potential_ppr = year_results[year_results["position"] ==
                                     1]["points"].mode().values[0]
    for idx, row in positions_source.sort_values(by="x").iterrows():
        x = row["x"]
        pos = row["finish_position_int"]
        if not np.isnan(pos):
            wins += 1 if pos == 1 else 0
            podiums += 1 if 3 >= pos > 0 else 0
        points = row["points"]
        max_potential_points += max_potential_ppr
        n_races += 1
        win_pct = wins / n_races
        podium_pct = podiums / n_races
        ppr_pct = points / max_potential_points
        win_source = win_source.append(
            {
                "x": x,
                "wins": wins,
                "win_pct": win_pct,
                "podiums": podiums,
                "podium_pct": podium_pct,
                "points": points,
                "ppr_pct": ppr_pct,
                "constructor_name": row["constructor_name"],
                "wdc_final_standing": row["wdc_final_standing"],
                "wdc_final_standing_str": int_to_ordinal(
                    row["wdc_final_standing"]),
                "win_pct_str": str(round(100 * win_pct, 1)) + "%",
                "podium_pct_str": str(round(100 * podium_pct, 1)) + "%",
                "ppr_pct_str": str(round(100 * ppr_pct, 1)) + "%"
            },
            ignore_index=True)

    max_podium = win_source["podiums"].max()
    y_max_range = max_podium if max_podium > 0 else 10
    win_plot = figure(title="Wins, Podiums, and Points",
                      y_axis_label="",
                      x_axis_label="Year",
                      tools="pan,xbox_zoom,reset,box_zoom,wheel_zoom,save",
                      y_range=Range1d(0, y_max_range, bounds=(-20, 30)))
    subtitle = "The points percent is calculated as a percentage of the maximum number of points that driver could " \
               "achieve (if he/she won every race)"
    win_plot.add_layout(Title(text=subtitle, text_font_style="italic"),
                        "above")

    max_podium_pct = win_source["podium_pct"].max()
    max_ppr_pct = win_source["ppr_pct"].max()
    max_points = win_source["points"].max()
    if max_ppr_pct > max_podium_pct and podiums == 0:
        k = 10 / max_ppr_pct
        max_y_pct = max_ppr_pct
    elif max_ppr_pct > max_podium_pct and max_ppr_pct > 0:
        k = max_podium / max_ppr_pct
        max_y_pct = max_ppr_pct
    elif max_podium_pct > 0:
        k = max_podium / max_podium_pct
        max_y_pct = max_podium_pct
    else:
        max_y_pct = 1
        k = 1
    win_source["podium_pct_scaled"] = k * win_source["podium_pct"]
    win_source["win_pct_scaled"] = k * win_source["win_pct"]
    win_source["ppr_pct_scaled"] = k * win_source["ppr_pct"]
    if max_points > 0 and max_podium == 0:
        k_pts = 10 / max_points
    elif max_points > 0:
        k_pts = max_podium / max_points
    else:
        k_pts = 1
        max_y_pct = 1
    win_source["points_scaled"] = k_pts * win_source["points"]

    if max_points == 0 and max_podium == 0:  # Theoretically this case could be handled, but not really useful
        return Div()

    # Override the x axis
    x_min = positions_source["x"].min() - 0.001
    x_max = positions_source["x"].max() + 0.001
    win_plot.x_range = Range1d(x_min, x_max, bounds=(x_min, x_max))
    win_plot.xaxis.ticker = FixedTicker(ticks=positions_source["x"])
    win_plot.xaxis.major_label_overrides = {
        row["x"]: row["roundName"]
        for idx, row in positions_source.iterrows()
    }
    win_plot.xaxis.major_label_orientation = 0.8 * math.pi / 2
    win_plot.xaxis.axis_label = ""

    # Other y axis (%)
    max_y = win_plot.y_range.end
    y_range = Range1d(start=0, end=max_y_pct, bounds=(-0.02, 1000))
    win_plot.extra_y_ranges = {"percent_range": y_range}
    axis = LinearAxis(y_range_name="percent_range")
    axis.formatter = NumeralTickFormatter(format="0.0%")
    win_plot.add_layout(axis, "right")

    # Third y axis (points)
    y_range2 = Range1d(start=0, end=max_y / k, bounds=(-0.02, 1000))
    win_plot.extra_y_ranges["points_range"] = y_range2
    axis2 = LinearAxis(y_range_name="points_range", axis_label="Points")
    win_plot.add_layout(axis2, "right")

    kwargs = {
        "x": "x",
        "line_width": 2,
        "line_alpha": 0.7,
        "source": win_source,
        "muted_alpha": 0.01
    }
    wins_line = win_plot.line(y="wins", color="green", **kwargs)
    win_pct_line = win_plot.line(y="win_pct_scaled",
                                 color="green",
                                 line_dash="dashed",
                                 **kwargs)
    podiums_line = win_plot.line(y="podiums", color="yellow", **kwargs)
    podium_pct_line = win_plot.line(y="podium_pct_scaled",
                                    color="yellow",
                                    line_dash="dashed",
                                    **kwargs)
    points_line = win_plot.line(y="points_scaled", color="white", **kwargs)
    ppr_pct_line = win_plot.line(y="ppr_pct_scaled",
                                 color="white",
                                 line_dash="dashed",
                                 **kwargs)

    legend = [
        LegendItem(label="Number of Wins", renderers=[wins_line]),
        LegendItem(label="Win Percentage", renderers=[win_pct_line]),
        LegendItem(label="Number of Podiums", renderers=[podiums_line]),
        LegendItem(label="Podium Percentage", renderers=[podium_pct_line]),
        LegendItem(label="Points", renderers=[points_line]),
        LegendItem(label="Points Percentage", renderers=[ppr_pct_line])
    ]

    legend = Legend(items=legend,
                    location="top_right",
                    glyph_height=15,
                    spacing=2,
                    inactive_fill_color="gray")
    win_plot.add_layout(legend, "right")
    win_plot.legend.click_policy = "mute"
    win_plot.legend.label_text_font_size = "12pt"

    # Hover tooltip
    win_plot.add_tools(
        HoverTool(show_arrow=False,
                  tooltips=[
                      ("Number of Wins", "@wins (@win_pct_str)"),
                      ("Number of Podiums", "@podiums (@podium_pct_str)"),
                      ("Points",
                       "@points (@ppr_pct_str of max pts. possible)"),
                      ("Constructor", "@constructor_name"),
                      ("Final Position this year", "@wdc_final_standing_str")
                  ]))

    # Crosshair tooltip
    win_plot.add_tools(CrosshairTool(line_color="white", line_alpha=0.6))

    return win_plot
Ejemplo n.º 5
0
def generate_career_mltr_position_scatter():
    """
    Generates a scatter plot of mean lap time rank vs WDC position (averaged across whole career).
    :return: Mean lap time rank vs position scatter layout
    """
    logging.info("Generating mean lap time rank vs WDC position scatter")

    dids = drivers.index.values
    source = pd.DataFrame(columns=["driver_name", "avg_mltr", "avg_position", "num_races", "color", "size"])
    color_gen = itertools.cycle(Turbo256[25:220])
    for did in dids:
        driver_name = get_driver_name(did)
        driver_results = results[(results["driverId"] == did) & (results["grid"] > 0)]
        avg_mltr = fastest_lap_data[fastest_lap_data["driver_id"] == did]["avg_lap_time_rank"].mean()
        avg_position = wdc_final_positions[wdc_final_positions["driverId"] == did]["position"].mean()

        num_races = driver_results.shape[0]
        size = math.pow(num_races, 0.3) + 2

        source = source.append({
            "driver_name": driver_name,
            "avg_mltr": avg_mltr,
            "avg_position": avg_position,
            "num_races": num_races,
            "color": color_gen.__next__(),
            "size": size
        }, ignore_index=True)

    mltr_position_scatter = figure(title=u"Average Lap Time Rank vs WDC Position \u2014 which drivers out-drove their "
                                   u"cars, a different perspective",
                                   x_axis_label="Career Avg. Lap Time Rank",
                                   y_axis_label="Career Avg. WDC Position",
                                   x_range=Range1d(0, 35, bounds=(0, 60)),
                                   y_range=Range1d(0, 35, bounds=(0, 200)))
    mltr_position_scatter.xaxis.ticker = FixedTicker(ticks=np.arange(5, 61, 5).tolist() + [1])
    mltr_position_scatter.yaxis.ticker = FixedTicker(ticks=np.arange(5, 201, 5).tolist() + [1])
    mltr_position_scatter.xaxis.major_label_overrides = {i: int_to_ordinal(i) for i in range(1, 70)}
    mltr_position_scatter.yaxis.major_label_overrides = {i: int_to_ordinal(i) for i in range(1, 200)}

    subtitle = "Average is taken across the driver's whole career. Dot size is calculated based on the number of " \
               "races the driver entered. DNFs are not included."
    mltr_position_scatter.add_layout(Title(text=subtitle, text_font_style="italic"), "above")

    mltr_position_scatter.scatter(x="avg_mltr", y="avg_position", source=source, color="color", size="size", alpha=0.7)
    mltr_position_scatter.line(x=[-60, 60], y=[-60, 60], color="white", line_alpha=0.6)

    label_kwargs = dict(render_mode="canvas",
                        text_color="white",
                        text_font_size="10pt",
                        border_line_color="white",
                        border_line_alpha=0.7)
    label1 = Label(x=10, y=1, text=" Driver tends to finish higher than expected ", **label_kwargs)
    label2 = Label(x=1, y=25, text=" Driver tends to finish lower than expected ", **label_kwargs)
    mltr_position_scatter.add_layout(label1)
    mltr_position_scatter.add_layout(label2)

    mltr_position_scatter.add_tools(HoverTool(show_arrow=False, tooltips=[
        ("Driver", "@driver_name"),
        ("Avg. Lap Time Rank", "@avg_mltr"),
        ("Avg. WDC Position.", "@avg_position"),
        ("Races Entered", "@num_races")
    ]))

    mltr_position_scatter.add_tools(CrosshairTool(dimensions="both", line_color="white", line_alpha=0.6))

    return mltr_position_scatter
Ejemplo n.º 6
0
def generate_dnf_fp_scatter():
    """
    Generates a scatter plot of DNF rate vs average finish position.
    :return: DNF vs win scatter plot layout
    """
    logging.info("Generating DNF rate vs average position scatter")

    dids = drivers.index.values
    source = pd.DataFrame(columns=["driver_name",
                                   "win_pct", "win_pct_str",
                                   "avg_fp",
                                   "num_races", "color", "size"])
    color_gen = itertools.cycle(Turbo256[25:220])
    for did in dids:
        driver_name = get_driver_name(did)
        driver_results = results[(results["driverId"] == did) & (results["grid"] > 0)]
        num_races = driver_results.shape[0]
        if num_races == 0:
            continue
        classifications = driver_results["statusId"].apply(get_status_classification)
        num_dnf = classifications[(classifications == "mechanical") | (classifications == "crash")].shape[0]

        avg_fp = driver_results["position"].mean()

        dnf_pct = num_dnf / num_races
        dnf_pct_str = str(round(100 * dnf_pct, 1)) + "%"
        dnf_pct_str += " (" + str(num_dnf) + " / " + str(num_races) + " races entered)"

        size = math.pow(num_races, 0.3) + 2

        source = source.append({
            "driver_name": driver_name,
            "avg_fp": avg_fp,
            "dnf_pct": dnf_pct,
            "dnf_pct_str": dnf_pct_str,
            "num_races": num_races,
            "color": color_gen.__next__(),
            "size": size
        }, ignore_index=True)

    dnf_fp_scatter = figure(title=u"DNF Percent vs. Average Finish Position",
                            x_axis_label="DNF Percent (of races entered)",
                            y_axis_label="Career Avg. Finish Position",
                            x_range=Range1d(0, 1, bounds=(0, 1)),
                            y_range=Range1d(0, 24, bounds=(0, 60)))
    dnf_fp_scatter.xaxis.formatter = NumeralTickFormatter(format="0.0%")
    dnf_fp_scatter.yaxis.ticker = FixedTicker(ticks=np.arange(5, 61, 5).tolist() + [1])
    dnf_fp_scatter.xaxis.major_label_overrides = {i: int_to_ordinal(i) for i in range(1, 60)}
    subtitle = "Percentages are taken across the driver's whole career. Dot size is calculated based on the number " \
               "of races the driver entered. DNFs are not considered for the average finish position."
    dnf_fp_scatter.add_layout(Title(text=subtitle, text_font_style="italic"), "above")

    dnf_fp_scatter.scatter(x="dnf_pct", y="avg_fp", source=source, color="color", size="size", alpha=0.7)

    dnf_fp_scatter.line(x=[.39] * 2, y=[-100, 100], color="white", line_width=2, alpha=0.4)
    dnf_fp_scatter.line(x=[.256] * 2, y=[-100, 100], color="white", line_width=2, alpha=0.4)
    dnf_fp_scatter.line(x=[.135] * 2, y=[-100, 100], color="white", line_width=2, alpha=0.4)

    label_kwargs = dict(render_mode="canvas",
                        text_color="white",
                        text_font_size="10pt",
                        border_line_color="white",
                        border_line_alpha=0.7)
    label1 = Label(x=0.1, y=1, text=" Often finishes well, rare DNFs ", **label_kwargs)
    label2 = Label(x=0.6, y=1, text=" Often finishes well, often DNFs ", **label_kwargs)
    label3 = Label(x=0.8, y=20, text=" Often finishes poorly, often DNFs ", **label_kwargs)
    label_kwargs["border_line_alpha"] = 0.0
    label4 = Label(x=0.39, y=22, text=" 1950 Avg. DNF % ", **label_kwargs)
    label5 = Label(x=0.256, y=22, text=" 2010 Avg. DNF % ", **label_kwargs)
    label6 = Label(x=0.135, y=22, text=" 2019 Avg. DNF % ", **label_kwargs)
    dnf_fp_scatter.add_layout(label1)
    dnf_fp_scatter.add_layout(label2)
    dnf_fp_scatter.add_layout(label3)
    dnf_fp_scatter.add_layout(label4)
    dnf_fp_scatter.add_layout(label5)
    dnf_fp_scatter.add_layout(label6)

    dnf_fp_scatter.add_tools(HoverTool(show_arrow=False, tooltips=[
        ("Driver", "@driver_name"),
        ("DNF Percent", "@dnf_pct_str"),
        ("Avg. Finish Position", "@avg_fp"),
        ("Races Entered", "@num_races")
    ]))

    dnf_fp_scatter.add_tools(CrosshairTool(dimensions="both", line_color="white", line_alpha=0.6))

    return dnf_fp_scatter
Ejemplo n.º 7
0
def generate_career_spvfp_scatter():
    """
    Generates average (across career) start position vs finish position scatter plot.
    :return: SP v FP scatter plot layout
    """
    logging.info("Generating start position vs finish position scatter")

    dids = drivers.index.values
    source = pd.DataFrame(columns=["driver_name", "avg_sp", "avg_fp", "num_races", "color", "size"])
    color_gen = itertools.cycle(Turbo256[25:220])
    for did in dids:
        driver_name = get_driver_name(did)
        driver_results = results[(results["driverId"] == did) & (results["grid"] > 0)]
        avg_sp = driver_results["grid"].mean()
        avg_fp = driver_results["position"].mean()
        num_races = driver_results.shape[0]
        size = math.pow(num_races, 0.3) + 2

        source = source.append({
            "driver_name": driver_name,
            "avg_sp": avg_sp,
            "avg_fp": avg_fp,
            "num_races": num_races,
            "color": color_gen.__next__(),
            "size": size
        }, ignore_index=True)

    spvfp_scatter = figure(title=u"Average Starting Position vs Finish Position \u2014 Saturday vs Sunday performance",
                           x_axis_label="Career Avg. Grid Position",
                           y_axis_label="Career Avg. Finishing Position (Official Classification)",
                           x_range=Range1d(0, 35, bounds=(0, 60)),
                           y_range=Range1d(0, 35, bounds=(0, 60)))
    spvfp_scatter.xaxis.ticker = FixedTicker(ticks=np.arange(5, 61, 5).tolist() + [1])
    spvfp_scatter.yaxis.ticker = FixedTicker(ticks=np.arange(5, 61, 5).tolist() + [1])
    spvfp_scatter.xaxis.major_label_overrides = {i: int_to_ordinal(i) for i in range(1, 60)}
    spvfp_scatter.yaxis.major_label_overrides = {i: int_to_ordinal(i) for i in range(1, 60)}

    subtitle = "Average is taken across the driver's whole career. Dot size is calculated based on the number of " \
               "races the driver entered. DNFs not considered in either calculation."
    spvfp_scatter.add_layout(Title(text=subtitle, text_font_style="italic"), "above")

    spvfp_scatter.scatter(x="avg_sp", y="avg_fp", source=source, color="color", size="size", alpha=0.7)
    spvfp_scatter.line(x=[-60, 60], y=[-60, 60], color="white", line_alpha=0.2)
    spvfp_scatter.line(x=[0, 60], y=[2.58240774, 29.96273108], color="white", line_alpha=0.6)  # Regression line

    label_kwargs = dict(render_mode="canvas",
                        text_color="white",
                        text_font_size="10pt",
                        border_line_color="white",
                        border_line_alpha=0.7)
    label1 = Label(x=32, y=16, text=" Regression Line ", **label_kwargs)
    label2 = Label(x=26, y=1, text=" Driver tends to make up many places ", **label_kwargs)
    label3 = Label(x=1, y=25, text=" Driver tends to lose many places ", **label_kwargs)
    spvfp_scatter.add_layout(label1)
    spvfp_scatter.add_layout(label2)
    spvfp_scatter.add_layout(label3)

    spvfp_scatter.add_tools(HoverTool(show_arrow=False, tooltips=[
        ("Driver", "@driver_name"),
        ("Avg. Starting Pos.", "@avg_sp"),
        ("Avg. Finish Pos.", "@avg_fp (DNFs not considered)"),
        ("Races Entered", "@num_races")
    ]))

    spvfp_scatter.add_tools(CrosshairTool(dimensions="both", line_color="white", line_alpha=0.6))

    return spvfp_scatter
Ejemplo n.º 8
0
def generate_sp_position_scatter():
    """
    Generates a scatter plot of average (across career) start position vs WDC position
    :return:
    """
    logging.info("Generating start position vs WDC finish position scatter")

    dids = drivers.index.values
    source = pd.DataFrame(columns=["driver_name", "avg_sp", "avg_wdc_position", "num_races", "color", "size"])
    color_gen = itertools.cycle(Turbo256[25:220])
    for did in dids:
        driver_name = get_driver_name(did)
        driver_results = results[(results["driverId"] == did) & (results["grid"] > 0)]
        avg_sp = driver_results["grid"].mean()
        avg_position = wdc_final_positions[wdc_final_positions["driverId"] == did]["position"].mean()

        num_races = driver_results.shape[0]
        size = math.pow(num_races, 0.3) + 2

        source = source.append({
            "driver_name": driver_name,
            "avg_sp": avg_sp,
            "avg_wdc_position": avg_position,
            "num_races": num_races,
            "color": color_gen.__next__(),
            "size": size
        }, ignore_index=True)

    sp_wdc_pos_scatter = figure(title=u"Average Starting Position vs WDC Finish Position \u2014 Saturday vs Sunday "
                                      u"performance, a less outlier-prone perspective",
                                x_axis_label="Career Avg. Grid Position",
                                y_axis_label="Career Avg. WDC Finish Position",
                                x_range=Range1d(0, 35, bounds=(0, 60)),
                                y_range=Range1d(0, 35, bounds=(0, 200)))
    sp_wdc_pos_scatter.xaxis.ticker = FixedTicker(ticks=np.arange(5, 61, 5).tolist() + [1])
    sp_wdc_pos_scatter.yaxis.ticker = FixedTicker(ticks=np.arange(5, 201, 5).tolist() + [1])
    sp_wdc_pos_scatter.xaxis.major_label_overrides = {i: int_to_ordinal(i) for i in range(1, 70)}
    sp_wdc_pos_scatter.yaxis.major_label_overrides = {i: int_to_ordinal(i) for i in range(1, 200)}

    subtitle = "Average is taken across the driver's whole career. Dot size is calculated based on the number of " \
               "races the driver entered."
    sp_wdc_pos_scatter.add_layout(Title(text=subtitle, text_font_style="italic"), "above")

    sp_wdc_pos_scatter.scatter(x="avg_sp", y="avg_wdc_position", source=source, color="color", size="size", alpha=0.7)
    sp_wdc_pos_scatter.line(x=[-60, 60], y=[-60, 60], color="white", line_alpha=0.2)

    # This is the correct regression line it just isn't very helpful on this plot
    # sp_wdc_pos_scatter.line(x=[0, 200], y=[7.65286499, 345.88496942], color="white", line_alpha=0.6)

    label_kwargs = dict(render_mode="canvas",
                        text_color="white",
                        text_font_size="10pt",
                        border_line_color="white",
                        border_line_alpha=0.7)
    # TODO are these really the right labels?
    label1 = Label(x=10, y=1, text=" Driver tends to make up many places ", **label_kwargs)
    label2 = Label(x=1, y=25, text=" Driver tends to lose many places ", **label_kwargs)
    sp_wdc_pos_scatter.add_layout(label1)
    sp_wdc_pos_scatter.add_layout(label2)

    sp_wdc_pos_scatter.add_tools(HoverTool(show_arrow=False, tooltips=[
        ("Driver", "@driver_name"),
        ("Avg. Starting Pos.", "@avg_sp"),
        ("Avg. WDC Pos.", "@avg_wdc_position"),
        ("Races Entered", "@num_races")
    ]))

    sp_wdc_pos_scatter.add_tools(CrosshairTool(dimensions="both", line_color="white", line_alpha=0.6))

    return sp_wdc_pos_scatter
Ejemplo n.º 9
0
def generate_stats_layout(cd_years,
                          cd_races,
                          cd_results,
                          cd_fastest_lap_data,
                          positions_source,
                          circuit_id,
                          driver_id,
                          constructor_id=None):
    """
    Stats div including:
    - Years
    - Num. races
    - Num. wins
    - Num. podiums
    - Best results
    - Average start position
    - Average finish position
    - Average lap time
    - Fastest lap time
    - Num mechanical DNFs and mechanical DNF rate
    - Num crash DNFs and crash DNF rate
    :param cd_years: CD years
    :param cd_races: CD races
    :param cd_results: CD results
    :param cd_fastest_lap_data: CD fastest lap data
    :param positions_source: Positions source
    :param driver_id: Driver ID
    :param circuit_id: Circuit ID
    :param constructor_id: If set to anything but None, will do constructor mode
    :return: Stats div layout
    """
    logging.info("Generating stats div")
    num_races = cd_results.shape[0]
    if num_races == 0:
        return Div()
    win_results = cd_results[cd_results["positionOrder"] == 1]
    num_wins = win_results.shape[0]
    if num_wins > 0:
        rids = win_results["raceId"]
        years = sorted(cd_races.loc[rids.values,
                                    "year"].astype(str).values.tolist(),
                       reverse=True)
        num_wins = str(num_wins) + " (" + ", ".join(years) + ")"
    else:
        num_wins = str(num_wins)
    podium_results = cd_results[cd_results["positionOrder"] <= 3]
    num_podiums = podium_results.shape[0]
    if num_podiums > 0:
        rids = podium_results["raceId"]
        years = list(set(cd_races.loc[rids.values, "year"].values.tolist()))
        years = rounds_to_str(years)
        num_podiums_str = str(num_podiums) + " (" + years + ")"
        if len(num_podiums_str) > 120:
            split = num_podiums_str.split(" ")
            split.insert(int(len(split) / 2), "<br>      " + "".ljust(20))
            num_podiums_str = " ".join(split)
    else:
        num_podiums_str = str(num_podiums)
    best_result = None
    if num_wins == 0:
        idxmin = cd_results["positionOrder"].idxmin()
        if not np.isnan(idxmin):
            rid = cd_results.loc[idxmin, "raceId"]
            year = cd_races.loc[rid, "year"]
            best_result = int_to_ordinal(
                int(cd_results.loc[idxmin, "positionOrder"])) + f" ({year})"
    mean_sp = round(cd_results["grid"].mean(), 1)
    mean_fp = round(cd_results["positionOrder"].mean(), 1)

    avg_lap_time = cd_fastest_lap_data["avg_lap_time_millis"].mean()
    fastest_lap_time = cd_fastest_lap_data["fastest_lap_time_millis"].min()

    classifications = cd_results["statusId"].apply(get_status_classification)
    num_mechanical_dnfs = classifications[classifications ==
                                          "mechanical"].shape[0]
    num_crash_dnfs = classifications[classifications == "crash"].shape[0]
    num_finishes = classifications[classifications == "finished"].shape[0]
    mechanical_dnfs_str = str(num_mechanical_dnfs)
    crash_dnfs_str = str(num_crash_dnfs)
    finishes_str = str(num_finishes)
    if num_races > 0:
        mechanical_dnfs_str += " (" + str(
            round(100 * num_mechanical_dnfs / num_races, 1)) + "%)"
        crash_dnfs_str += " (" + str(round(100 * num_crash_dnfs / num_races,
                                           1)) + "%)"
        finishes_str += " (" + str(round(100 * num_finishes / num_races,
                                         1)) + "%)"

    if positions_source.shape[0] > 0:
        avg_finish_pos_overall = positions_source["avg_finish_pos"].mean()
        avg_finish_pos_here = positions_source["finish_position_int"].mean()
        diff = avg_finish_pos_here - avg_finish_pos_overall
        avg_finish_pos_overall = round(avg_finish_pos_overall, 1)
        avg_finish_pos_here = round(avg_finish_pos_here, 1)
        w = "higher" if diff < 0 else "lower"
        finish_pos_diff_str = f"Finished on average {round(abs(diff), 1)} place(s) {w} than average " \
                              f"(pos. {avg_finish_pos_here} here vs pos. {avg_finish_pos_overall} average overall)"
    else:
        finish_pos_diff_str = ""

    header_template = """
    <h2 style="text-align: center;"><b>{}</b></h2>
    """

    template = """
    <pre><b>{}</b> {}<br></pre>
    """

    if constructor_id:
        name = get_constructor_name(constructor_id, include_flag=False)
    else:
        name = get_driver_name(driver_id, include_flag=False, just_last=True)
    cd_stats = header_template.format(
        f"{name} at {get_circuit_name(circuit_id, include_flag=False)} Stats")
    cd_stats += template.format("Years: ".ljust(22), rounds_to_str(cd_years))
    cd_stats += template.format("Num Races: ".ljust(22), str(num_races))
    cd_stats += template.format("Num Wins: ".ljust(22), str(num_wins))
    cd_stats += template.format("Num Podiums: ".ljust(22),
                                str(num_podiums_str))
    if best_result:
        cd_stats += template.format("Best Result: ".ljust(22),
                                    str(best_result))
    cd_stats += template.format("Avg. Start Pos.: ".ljust(22), mean_sp)
    cd_stats += template.format("Avg. Finish Pos.: ".ljust(22), mean_fp)

    if not np.isnan(avg_lap_time):
        cd_stats += template.format("Avg. Lap Time: ".ljust(22),
                                    millis_to_str(avg_lap_time))
        cd_stats += template.format("Fastest Lap Time: ".ljust(22),
                                    millis_to_str(fastest_lap_time))
    cd_stats += template.format("Num. Mechanical DNFs: ".ljust(22),
                                mechanical_dnfs_str)
    cd_stats += template.format("Num. Crash DNFs: ".ljust(22), crash_dnfs_str)
    cd_stats += template.format("Num Finishes".ljust(22), finishes_str)
    if positions_source.shape[0] > 0:
        cd_stats += template.format("Compared to Average: ".ljust(22),
                                    finish_pos_diff_str)

    return Div(text=cd_stats)
Ejemplo n.º 10
0
def generate_stats_layout(positions_source, yc_results, comparison_source, year_id, constructor_id):
    """
    Year summary div, including WCC place, highest race finish, number of races, points, points per race, number of
    wins, number of podiums, and everything else in constructor.generate_stats_layout and
    yeardriver.generate_stats_layout
    - WCC place
    - Highest race finish
    - Number of races
    - Points
    - Points per race
    - Number of wins and where were they
    - Number of podiums and where were they
    - Teammates
    - Constructors
    - Mean gap to teammate in positions
    - Mean grid position
    - Mean finish position
    - DNF info
    :param positions_source: Positions source
    :param yc_results: YC results
    :param comparison_source: Comparison source
    :param year_id: Year ID
    :param constructor_id: Constructor ID
    :return: Stats layout
    """
    logging.info("Generating year constructor stats layout")
    if positions_source.shape[0] == 0:
        return Div(text="")
    wcc_final_standing = positions_source["wcc_final_standing"].mode()
    if wcc_final_standing.shape[0] > 0:
        wcc_final_standing_str = int_to_ordinal(wcc_final_standing.values[0])
    else:
        wcc_final_standing_str = ""
    highest_race_finish_idx = yc_results["positionOrder"].idxmin()
    if np.isnan(highest_race_finish_idx):
        highest_race_finish_str = ""
    else:
        highest_race_finish = yc_results.loc[highest_race_finish_idx, "positionOrder"]
        round_name = get_race_name(yc_results.loc[highest_race_finish_idx, "raceId"])
        highest_race_finish_str = int_to_ordinal(highest_race_finish) + " at " + round_name
    num_races = positions_source["race_id"].unique().shape[0]
    num_races_str = str(num_races)
    points = positions_source["points"].max()
    if np.isnan(points):
        points_str = ""
    elif points <= 0:
        points_str = str(points) + " (0 pts/race)"
    else:
        points_str = str(points) + " (" + str(round(points / num_races, 1)) + " pts/race)"
    wins_slice = yc_results[yc_results["positionOrder"] == 1]
    num_wins = wins_slice.shape[0]
    if num_wins == 0:
        wins_str = str(num_wins)
    else:
        wins_str = str(num_wins) + " (" + ", ".join(wins_slice["raceId"].apply(get_race_name)) + ")"
        if len(wins_str) > 120:
            split = wins_str.split(" ")
            split.insert(int(len(split) / 2), "<br>    " + "".ljust(20))
            wins_str = " ".join(split)
    podiums_slice = yc_results[yc_results["positionOrder"] <= 3]
    num_podiums = podiums_slice.shape[0]
    if num_podiums == 0:
        podiums_str = str(num_podiums)
    else:
        race_names = ", ".join([get_race_name(rid) for rid in podiums_slice["raceId"].unique()])
        podiums_str = str(num_podiums) + " (" + race_names + ")"
        if len(podiums_str) > 120:
            split = podiums_str.split(" ")
            split.insert(int(len(split) / 2), "<br>    " + "".ljust(20))
            podiums_str = " ".join(split)
    driver_dids = yc_results["driverId"].unique()
    driver_names = []
    for did in driver_dids:
        driver_names.append(get_driver_name(did))
    driver_names = ", ".join(driver_names)
    mean_grid_pos = yc_results["grid"].replace("", np.nan).mean()
    if np.isnan(mean_grid_pos):
        mean_grid_pos_str = ""
    else:
        mean_grid_pos_str = str(round(mean_grid_pos, 1))
    mean_finish_pos = yc_results["positionOrder"].mean()
    if np.isnan(mean_finish_pos):
        mean_finish_pos_str = ""
    else:
        mean_finish_pos_str = str(round(mean_finish_pos, 1))
    classifications = yc_results["statusId"].apply(get_status_classification)
    num_mechanical_dnfs = classifications[classifications == "mechanical"].shape[0]
    num_crash_dnfs = classifications[classifications == "crash"].shape[0]
    if num_races > 0:
        num_mechanical_dnfs_str = str(num_mechanical_dnfs) + " (" + \
                                  str(round(100 * num_mechanical_dnfs / num_races, 1)) + "%)"
        num_crash_dnfs_str = str(num_crash_dnfs) + " (" + str(round(100 * num_crash_dnfs / num_races, 1)) + "%)"
    else:
        num_mechanical_dnfs_str = ""
        num_crash_dnfs_str = ""
    mean_teammate_gap_pos = (comparison_source["driver1_fp"] - comparison_source["driver2_fp"]).mean()
    if np.isnan(mean_teammate_gap_pos):
        mean_teammate_gap_pos_str = ""
    else:
        mean_teammate_gap_pos_str = "Driver {} finished {} places better than driver {} on average"
        mean_teammate_gap_pos_str = mean_teammate_gap_pos_str.format("1" if mean_teammate_gap_pos < 0 else "2",
                                                                     str(abs(round(mean_teammate_gap_pos, 1))),
                                                                     "2" if mean_teammate_gap_pos < 0 else "1")

    # Construct the HTML
    header_template = """
    <h2 style="text-align: left;"><b>{}</b></h2>
    """
    template = """
    <pre><b>{}</b> {}<br></pre>
    """

    constructor_name = get_constructor_name(constructor_id, include_flag=False)
    constructor_stats = header_template.format(f"{constructor_name}'s Stats for the {year_id} Season")
    constructor_stats += template.format("WCC Final Pos.: ".ljust(20), wcc_final_standing_str)
    constructor_stats += template.format("Num. Races: ".ljust(20), num_races_str)
    if num_wins == 0:
        constructor_stats += template.format("Best Finish Pos.: ".ljust(20), highest_race_finish_str)
    constructor_stats += template.format("Wins: ".ljust(20), wins_str)
    constructor_stats += template.format("Podiums: ".ljust(20), podiums_str)
    constructor_stats += template.format("Points: ".ljust(20), points_str)
    constructor_stats += template.format("Drivers(s): ".ljust(20), driver_names)
    constructor_stats += template.format("Avg. Grid Pos.: ".ljust(20), mean_grid_pos_str)
    constructor_stats += template.format("Avg. Finish Pos.: ".ljust(20), mean_finish_pos_str)
    constructor_stats += template.format("Mechanical DNFs: ".ljust(20), num_mechanical_dnfs_str)
    constructor_stats += template.format("Crash DNFs: ".ljust(20), num_crash_dnfs_str)
    constructor_stats += template.format("Avg. Driver Gap: ".ljust(20), mean_teammate_gap_pos_str)

    return Div(text=constructor_stats)
Ejemplo n.º 11
0
def generate_results_table(yc_results, yc_fastest_lap_data, year_results, year_fastest_lap_data, year_only=False,
                           height=None, include_driver_name=True, include_constructor_name=False):
    """
    Generates a table of results at each race, including quali position, finish position (or reason for DNF), time, gap
    to leader, fastest lap time and gap to fastest lap (of all drivers), average lap time and gap to fastest average lap
    time (of all drivers).
    :param yc_results: YC results
    :param yc_fastest_lap_data: YC fastest lap data
    :param year_results: Year results
    :param year_fastest_lap_data: Year fastest lap data
    :param year_only: Whether to set the race name row to just the year
    :param height: Plot height
    :param include_driver_name: If True, will include a driver name column
    :param include_constructor_name: If True, will include a constructor name column
    :return: Table layout, source
    """
    # TODO this might be able to be refactored with yeardriver or year, but it is kind of unique
    logging.info("Generating results table")
    source = pd.DataFrame(columns=["race_name", "driver_name", "driver_id ", "race_id", "year", "constructor_name",
                                   "quali_pos_str",
                                   "finish_pos_str",
                                   "time_str",
                                   "fastest_lap_time_str",
                                   "avg_lap_time_str"])
    for idx, results_row in yc_results.sort_values(by=["raceId", "driverId"]).iterrows():
        rid = results_row["raceId"]
        driver_id = results_row["driverId"]
        constructor_id = results_row["constructorId"]
        driver_name = get_driver_name(driver_id)
        constructor_name = get_constructor_name(constructor_id)
        race_results = year_results[year_results["raceId"] == rid]
        race_fastest_lap_data = year_fastest_lap_data[year_fastest_lap_data["raceId"] == rid]
        race_driver_fastest_lap_data = yc_fastest_lap_data[(yc_fastest_lap_data["raceId"] == rid) &
                                                           (yc_fastest_lap_data["driver_id"] == driver_id)]
        race_name = get_race_name(rid)
        grid = results_row["grid"]
        if grid == -1:
            quali_pos_str = "DNQ"
        else:
            quali_pos_str = int_to_ordinal(grid)
        status_id = results_row["statusId"]
        finish_pos_str, finish_pos = result_to_str(results_row["positionOrder"], status_id)
        time = results_row["milliseconds"]
        winner = race_results[race_results["positionOrder"] == 1]
        if winner.shape[0] > 0 and winner["driverId"].values[0] != driver_id \
                and not np.isnan(time) and not np.isnan(results_row["position"]):
            time_gap = millis_to_str(time - winner["milliseconds"].values[0])
            time_str = millis_to_str(time) + " (+" + time_gap + ")"
            if status_id != 1 and get_status_classification(status_id) == "finished":
                time_str = millis_to_str(time) + " (+" + time_gap + ", " + status.loc[status_id, "status"] + ")"
        elif finish_pos == 1:
            time_str = millis_to_str(time)
        else:
            time_str = "Not Set"
        if race_driver_fastest_lap_data.shape[0] > 0:
            fastest_lap_time = race_driver_fastest_lap_data["fastest_lap_time_millis"].values[0]
            fastest_lap_time_str = millis_to_str(fastest_lap_time)
            if race_driver_fastest_lap_data["rank"].values[0] == " 1":
                fastest_lap_time_str = fastest_lap_time_str + " (Fastest)"
            else:
                fastest_time = race_fastest_lap_data[race_fastest_lap_data["rank"] == " 1"]["fastest_lap_time_millis"]
                if fastest_time.shape[0] > 0 and not np.isnan(fastest_lap_time):
                    fastest_time = fastest_time.values[0]
                    fastest_gap = millis_to_str(fastest_lap_time - fastest_time)
                    fastest_lap_time_str = millis_to_str(fastest_lap_time) + " (+" + fastest_gap + ")"
            if fastest_lap_time_str == "":
                fastest_lap_time_str = "Not Set"
            fastest_avg_idx = race_fastest_lap_data["avg_lap_time_millis"].idxmin()
            avg_lap_time = race_driver_fastest_lap_data["avg_lap_time_millis"].values[0]
            if np.isnan(avg_lap_time):
                avg_lap_time_str = "Not Set"
            elif race_fastest_lap_data.loc[fastest_avg_idx, "driver_id"] == driver_id or np.isnan(avg_lap_time):
                avg_lap_time_str = millis_to_str(avg_lap_time) + " (Fastest Avg.)"
            else:
                fastest_avg_time = race_fastest_lap_data.loc[fastest_avg_idx, "avg_lap_time_millis"]
                avg_gap = millis_to_str(avg_lap_time - fastest_avg_time)
                avg_lap_time_str = millis_to_str(avg_lap_time) + " (+" + avg_gap + ")"
        else:
            fastest_lap_time_str = "Not Set"
            avg_lap_time_str = "Not Set"
        source = source.append({
            "race_name": race_name,
            "race_id": rid,
            "driver_name": driver_name,
            "driver_id": driver_id,
            "constructor_name": constructor_name,
            "year": races.loc[rid, "year"],
            "quali_pos_str": quali_pos_str,
            "finish_pos_str": finish_pos_str,
            "time_str": time_str,
            "fastest_lap_time_str": fastest_lap_time_str,
            "avg_lap_time_str": avg_lap_time_str
        }, ignore_index=True)

    source = source.sort_values(by="year", ascending=False)

    results_columns = [
        TableColumn(field="quali_pos_str", title="Grid Pos.", width=75),
        TableColumn(field="finish_pos_str", title="Finish Pos.", width=75),
        TableColumn(field="time_str", title="Time", width=100),
        TableColumn(field="fastest_lap_time_str", title="Fastest Lap Time", width=75),
        TableColumn(field="avg_lap_time_str", title="Avg. Lap Time", width=75),
    ]
    if include_driver_name:
        results_columns.insert(0, TableColumn(field="driver_name", title="Driver Name", width=100))
    if include_constructor_name:
        results_columns.insert(0, TableColumn(field="constructor_name", title="Constructor Name", width=100))
    if year_only:
        results_columns.insert(0, TableColumn(field="year", title="Year", width=50))
    else:
        results_columns.insert(0, TableColumn(field="race_name", title="Race Name", width=100))
    results_table = DataTable(source=ColumnDataSource(data=source), columns=results_columns, index_position=None,
                              height=28 * yc_results.shape[0] if height is None else height)
    title = Div(text=f"<h2><b>Results for each race</b></h2><br><i>The fastest lap time and average lap time gaps "
                     f"shown are calculated based on the gap to the fastest of all drivers and fastest average of "
                     f"all drivers in that race respectively.</i>")
    return column([title, row([results_table], sizing_mode="stretch_width")], sizing_mode="stretch_width"), source
Ejemplo n.º 12
0
def generate_spvfp_scatter():
    """
    Generates a scatter plot of starting position vs finish position.
    :return: SPvFP scatter plot layout
    """
    logging.info("Generating start pos vs finish pos scatter")

    def get_start_pos(cid):
        circuit_races = races[races["circuitId"] == cid]
        circuit_results = results[results["raceId"].isin(circuit_races.index)]
        return circuit_results["grid"].mean()

    def get_finish_pos(cid):
        circuit_races = races[races["circuitId"] == cid]
        circuit_results = results[results["raceId"].isin(circuit_races.index)]
        return circuit_results["positionOrder"].mean()

    source = pd.DataFrame(circuits.index.values, columns=["circuit_id"])
    source["start_pos"] = source["circuit_id"].apply(get_start_pos)
    source["finish_pos"] = source["circuit_id"].apply(get_finish_pos)
    source["circuit_name"] = source["circuit_id"].apply(
        get_circuit_name_custom)
    source["flag"] = source["circuit_name"].apply(lambda s: s[-2:])
    source["years"] = source["circuit_id"].apply(get_circuit_years)
    source["num_races"] = source["circuit_id"].apply(
        lambda cid: races[races["circuitId"] == cid].shape[0])
    source["size"] = source["num_races"].apply(get_circuit_size)

    palette = Turbo256
    n_circuits = source.shape[0]
    colors = []
    di = 180 / n_circuits
    i = 20
    for _ in range(n_circuits):
        colors.append(palette[int(i)])
        i += di
    source["color"] = colors

    spvfp_scatter = figure(
        title=
        u"Average Starting Position vs Average Finishing Position \u2014 includes DNFs",
        x_axis_label="Avg. Starting Position at this Circuit",
        y_axis_label="Avg. Finishing Position at this circuit",
        x_range=Range1d(7, 20, bounds=(0, 60)),
        y_range=Range1d(7, 20, bounds=(0, 60)),
        plot_height=650)
    spvfp_scatter.xaxis.ticker = FixedTicker(
        ticks=np.arange(5, 61, 5).tolist() + [1])
    spvfp_scatter.yaxis.ticker = FixedTicker(
        ticks=np.arange(5, 61, 5).tolist() + [1])
    spvfp_scatter.xaxis.major_label_overrides = {
        i: int_to_ordinal(i)
        for i in range(1, 60)
    }
    spvfp_scatter.yaxis.major_label_overrides = {
        i: int_to_ordinal(i)
        for i in range(1, 60)
    }
    subtitle = "Dot size is calculated based on the number of races held at that circuit"
    spvfp_scatter.add_layout(Title(text=subtitle, text_font_style="italic"),
                             "above")

    spvfp_scatter.scatter(x="start_pos",
                          y="finish_pos",
                          source=source,
                          color="color",
                          size="size")
    spvfp_scatter.line(x=[-60, 60], y=[-60, 60], color="white", line_alpha=0.5)

    label_kwargs = dict(render_mode="canvas",
                        text_color="white",
                        text_font_size="12pt")
    label1 = Label(x=12,
                   y=9,
                   text="Drivers tend to finish higher than started",
                   **label_kwargs)
    label2 = Label(x=8,
                   y=19,
                   text="Drivers tend to finish lower than started",
                   **label_kwargs)
    spvfp_scatter.add_layout(label1)
    spvfp_scatter.add_layout(label2)

    spvfp_scatter.add_tools(
        HoverTool(show_arrow=False,
                  tooltips=[("Circuit Name", "@circuit_name"),
                            ("Number of races", "@num_races"),
                            ("Average starting position", "@start_pos"),
                            ("Average finishing position", "@finish_pos")]))

    spvfp_scatter.add_tools(CrosshairTool(line_color="white", line_alpha=0.6))

    return spvfp_scatter
Ejemplo n.º 13
0
def generate_upset_scatter():
    """
    Generates a scatter plot showing how prone circuits are to upsets.
    :return: Upset plot layout
    """
    logging.info("Generating upset scatter plot")

    def get_winner_wdc_position(cid):
        # Gets the mean WDC position for the winner at this circuit
        circuit_races = races[races["circuitId"] == cid]
        circuit_results = results[results["raceId"].isin(circuit_races.index)]
        winner_dids = circuit_results[circuit_results["position"] == 1][[
            "raceId", "driverId"
        ]].values
        wdc_positions = []
        for rid, did in winner_dids:
            year = races.loc[rid, "year"]
            wdc_pos = wdc_final_positions[
                (wdc_final_positions["year"] == year)
                & (wdc_final_positions["driverId"] == did)]
            if wdc_pos.shape[0] > 0:
                wdc_positions.append(wdc_pos["position"].values[0])
        return np.mean(wdc_positions)

    def get_wdc_position(cid):
        # Gets the mean finishing position at this circuit for this WDC of the year
        circuit_races = races[races["circuitId"] == cid]
        circuit_results = results[results["raceId"].isin(circuit_races.index)]
        wdc_positions = []
        for year in circuit_races["year"].values:
            wdc_did = wdc_final_positions[
                (wdc_final_positions["year"] == year)
                & (wdc_final_positions["position"] == 1)]
            if wdc_did.shape[0] > 0:
                wdc_did = wdc_did["driverId"].values[0]
                wdc_position = circuit_results[circuit_results["driverId"] ==
                                               wdc_did]
                if wdc_position.shape[0] > 0:
                    wdc_positions.append(
                        wdc_position["positionOrder"].values[0])
        return np.mean(wdc_positions)

    source = pd.DataFrame(circuits.index.values, columns=["circuit_id"])
    source["winner_wdc_position"] = source["circuit_id"].apply(
        get_winner_wdc_position)
    source["wdc_position"] = source["circuit_id"].apply(get_wdc_position)
    source["circuit_name"] = source["circuit_id"].apply(
        get_circuit_name_custom)
    source["flag"] = source["circuit_name"].apply(lambda s: s[-2:])
    source["years"] = source["circuit_id"].apply(get_circuit_years)
    source["num_races"] = source["circuit_id"].apply(
        lambda cid: races[races["circuitId"] == cid].shape[0])
    source["size"] = source["num_races"].apply(get_circuit_size)

    palette = Turbo256
    n_circuits = source.shape[0]
    colors = []
    di = 180 / n_circuits
    i = 20
    for _ in range(n_circuits):
        colors.append(palette[int(i)])
        i += di
    source["color"] = colors

    upset_scatter = figure(
        title=u"Upset Plot",
        x_axis_label="Avg. WDC position for winner at this circuit",
        y_axis_label="Avg. Position for the WDC at this circuit",
        x_range=Range1d(0, 10, bounds=(0, 60)),
        y_range=Range1d(0, 20, bounds=(0, 60)),
        plot_height=650)
    upset_scatter.xaxis.ticker = FixedTicker(
        ticks=np.arange(5, 61, 5).tolist() + [1])
    upset_scatter.yaxis.ticker = FixedTicker(
        ticks=np.arange(5, 61, 5).tolist() + [1])
    upset_scatter.xaxis.major_label_overrides = {
        i: int_to_ordinal(i)
        for i in range(1, 60)
    }
    upset_scatter.yaxis.major_label_overrides = {
        i: int_to_ordinal(i)
        for i in range(1, 60)
    }

    upset_scatter.scatter(x="winner_wdc_position",
                          y="wdc_position",
                          source=source,
                          color="color",
                          size="size")
    upset_scatter.line(x=[-60, 60], y=[-60, 60], color="white", line_alpha=0.5)

    marker_label_kwargs = dict(x="winner_wdc_position",
                               y="wdc_position",
                               level="glyph",
                               x_offset=0,
                               y_offset=0,
                               source=ColumnDataSource(source),
                               render_mode="canvas",
                               text_color="white",
                               text_font_size="10pt")
    labels = LabelSet(text="flag", **marker_label_kwargs)
    upset_scatter.add_layout(labels)

    label_kwargs = dict(render_mode="canvas",
                        text_color="white",
                        text_font_size="12pt")
    label1 = Label(x=0.5,
                   y=19,
                   text="Winner here does well in WDC",
                   **label_kwargs)
    label2 = Label(x=0.5,
                   y=18.2,
                   text="Eventual WDC does poorly here",
                   **label_kwargs)
    label3 = Label(x=5.5,
                   y=1.5,
                   text="Winner here does poorly in WDC",
                   **label_kwargs)
    label4 = Label(x=5.5,
                   y=0.7,
                   text="Eventual WDC does well here",
                   **label_kwargs)
    upset_scatter.add_layout(label1)
    upset_scatter.add_layout(label2)
    upset_scatter.add_layout(label3)
    upset_scatter.add_layout(label4)

    upset_scatter.add_tools(
        HoverTool(
            show_arrow=False,
            tooltips=[
                ("Circuit Name", "@circuit_name"),
                ("Number of races", "@num_races"),
                ("The winner at this circuit on average finishes in position",
                 "@winner_wdc_position in the WDC"),
                ("The WDC finishes",
                 "position @wdc_position at this circuit on average")
            ]))

    upset_scatter.add_tools(CrosshairTool(line_color="white", line_alpha=0.6))

    explanation = """The x axis is calculated by, for every circuit, finding the average World Drivers' Championship 
    position for the winner at this circuit.<br>
    The y axis is calculated by, for every circuit, finding the average finishing position for the World Driver's 
    Champion for that year at that circuit.<br>
    This means that dots far away from the origin represent circuits where the winner tends to finish low in the 
    championship and the (eventual) champion tends to finish low at this circuit, thus indicating that there are often 
    upsets. Dots near the origin indicate the opposite. Dots far from the origin near the x axis represent circuits 
    whose winner tends to do poorly in the WDC but the eventual World Champion does well here. Dots far from the 
    origin but near the y axis represent the opposite, or that the eventual World Champion does poorly here while 
    the winner at this circuit tends to do well in the WDC.<br>
    Dot size is calculated based on the number of races held at that circuit."""
    explanation = Div(text=explanation)

    return column([upset_scatter, explanation], sizing_mode="stretch_width")