{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Analytics: Gutsy Returners","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport joblib\nimport json\n\nfrom matplotlib import pyplot as plt\nfrom typing import Dict, List, Any, Callable, Optional\nfrom tqdm.notebook import tqdm","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:08.299525Z","iopub.execute_input":"2022-01-06T23:00:08.299815Z","iopub.status.idle":"2022-01-06T23:00:08.306015Z","shell.execute_reply.started":"2022-01-06T23:00:08.299786Z","shell.execute_reply":"2022-01-06T23:00:08.305174Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"\"\"\"\nHelpers for visualizing the field and plays.\n\"\"\"\n\n\nSIDE = 5\nENDZONE = 10\nFIELD_L = 120\nFIELD_W = 53.3\nFIELD_MID = 60\nFIELD_X = (0 - SIDE, FIELD_L + SIDE)\nFIELD_Y = (0 - SIDE, FIELD_W + SIDE)\nFIELD_XD = (FIELD_X[1] - FIELD_X[0]) / 10\nFIELD_YD = (FIELD_Y[1] - FIELD_Y[0]) / 10\nFIELD_RATIO = 1.25\nFIELD_DIM = (FIELD_RATIO * FIELD_XD, FIELD_RATIO * FIELD_YD)\n\n\ndef plot_field(plt):\n    style = {\n        \"c\": \"black\",\n        \"alpha\": 0.5,\n    }\n    plt.xlim(*FIELD_X)\n    plt.ylim(*FIELD_Y)\n    plt.plot([0, FIELD_L], [0, 0], **style)\n    plt.plot([0, FIELD_L], [FIELD_W, FIELD_W], **style)\n    plt.plot([0, 0], [0, FIELD_W], **style)\n    plt.plot([FIELD_L, FIELD_L], [0, FIELD_W], **style)\n    for x in range(ENDZONE, FIELD_L, 10):\n        yard = 50 - abs(x - 10 - 50)\n        no_dash = (x == FIELD_MID) or (x == ENDZONE) or (x == (FIELD_L - ENDZONE))\n        style = {\n            \"c\": \"black\",\n            \"alpha\": 0.5,\n            \"dashes\": [] if no_dash else [2, 2],\n        }\n        plt.plot([x, x], [0, FIELD_W], **style)\n        plt.text(s=yard, x=x, y=-2, ha=\"center\")\n    fig = plt.gcf()\n    fig.set_size_inches(*FIELD_DIM)\n    \n\ndef team_to_color(s: str) -> str:\n    return \"blue\" if s == \"home\" else \"red\"\n\n\ndef plot_play(\n    df_tracking,\n    event=\"ball_snap\",\n    frame=None,\n    show_numbers=False\n):\n    if len(df_tracking) == 0:\n        raise ValueError(\"No records in the tracking data.\")\n\n    df_track = df_tracking.copy()\n    \n    plot_field(plt)\n    \n    df_track[\"color\"] = df_track[\"team\"].apply(team_to_color)\n\n    df_ball = df_track.query(\"team == 'football'\")\n    plt.scatter(x=df_ball[\"x\"], y=df_ball[\"y\"], c=\"brown\", s=5)\n\n    df_path = df_track.query(\"team != 'football'\")\n    plt.scatter(x=df_path[\"x\"], y=df_path[\"y\"], c=df_path[\"color\"], s=5, alpha=0.15)\n\n    time_query = f\"event == '{event}'\" if frame is None else f\"frameId == {frame}\"\n    df_init = df_track.query(f\"{time_query} and team != 'football'\")\n    plt.scatter(x=df_init[\"x\"], y=df_init[\"y\"], c=df_init[\"color\"], s=50)\n    plt.title(f\"Frame {df_init['frameId'].min()} / {df_tracking['frameId'].max()}\")\n    \n    if show_numbers:\n        for p in df_init.to_dict(orient=\"records\"):\n            plt.text(s=int(p[\"jerseyNumber\"]), x=p[\"x\"] + 0.5, y=p[\"y\"])","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:08.309076Z","iopub.execute_input":"2022-01-06T23:00:08.309497Z","iopub.status.idle":"2022-01-06T23:00:08.329118Z","shell.execute_reply.started":"2022-01-06T23:00:08.309465Z","shell.execute_reply":"2022-01-06T23:00:08.328489Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"MAX_DIST = 200\nSIDELINE_MIN = 0\nSIDELINE_MAX = 53.0 + (1.0 / 3.0)\nRECEIVING_GOAL_LINE = 10\nKICKING_GOAL_LINE = 110\n\n\ndef distance(a: Dict, b: Dict) -> float:\n    return np.sqrt((a[\"y\"] - b[\"y\"])**2 + (a[\"x\"] - b[\"x\"])**2)\n\n\ndef closest_defender_distance(\n    ball_x: float,\n    ball_y: float,\n    players: List[Dict],\n    kickingTeam: str\n) -> float:\n    ball = { \"x\": ball_x, \"y\": ball_y }\n    min_dist = MAX_DIST\n    for p in players:\n        if p[\"teamCode\"] == kickingTeam:\n            d = distance(ball, p)\n            if d < min_dist:\n                min_dist = d\n    return min_dist\n\n\ndef defenders_within_radius(\n    ball_x: float,\n    ball_y: float,\n    players: List[Dict],\n    kickingTeam: str,\n    radius: int\n) -> int:\n    ball = { \"x\": ball_x, \"y\": ball_y }\n    count = 0\n    for p in players:\n        if p[\"teamCode\"] == kickingTeam:\n            d = distance(ball, p)\n            if d < radius:\n                count += 1\n    return count\n\n\ndef blockers_within_radius(\n    ball_x: float,\n    ball_y: float,\n    players: List[Dict],\n    kickingTeam: str,\n    radius: int\n) -> int:\n    ball = { \"x\": ball_x, \"y\": ball_y }\n    count = 0\n    for p in players:\n        if p[\"teamCode\"] != kickingTeam and p[\"x\"] >= ball_x:\n            d = distance(ball, p)\n            if d < radius:\n                count += 1\n    return count\n\n\ndef distance_to_sideline(ball_y: float) -> float:\n    d_bottom = ball_y - SIDELINE_MIN\n    d_top = SIDELINE_MAX - ball_y\n    # Out of bounds\n    if d_bottom <= 0 or d_top <= 0:\n        return 0\n    # Get distance to closest sideline\n    return min(d_bottom, d_top)\n\n\ndef speed_upfield(player: Dict) -> float:\n    if pd.isna(player):\n        return 0\n    angle_rads = np.deg2rad(player[\"dir\"])\n    speed = player[\"s\"]\n    return speed * np.sin(angle_rads)\n\n\ndef speed_lateral(player: Dict) -> float:\n    if pd.isna(player):\n        return 0\n    angle_rads = np.deg2rad(player[\"dir\"])\n    speed = player[\"s\"]\n    # Take absolute value to get lateral speed in either direction\n    return speed * np.abs(np.cos(angle_rads))\n\n\ndef closest_defender_speed_upfield(\n    ball_x: float,\n    ball_y: float,\n    players: List[Dict],\n    kickingTeam: str\n) -> float:\n    ball = { \"x\": ball_x, \"y\": ball_y }\n    min_dist = MAX_DIST\n    closest = None\n    for p in players:\n        if p[\"teamCode\"] == kickingTeam:\n            d = distance(ball, p)\n            if d < min_dist:\n                min_dist = d\n                closest = p\n    return speed_upfield(closest)\n\n\ndef closest_defender_speed_lateral(\n    ball_x: float,\n    ball_y: float,\n    players: List[Dict],\n    kickingTeam: str\n) -> float:\n    ball = { \"x\": ball_x, \"y\": ball_y }\n    min_dist = MAX_DIST\n    closest = None\n    for p in players:\n        if p[\"teamCode\"] == kickingTeam:\n            d = distance(ball, p)\n            if d < min_dist:\n                min_dist = d\n                closest = p\n    return speed_lateral(closest)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:08.330737Z","iopub.execute_input":"2022-01-06T23:00:08.331090Z","iopub.status.idle":"2022-01-06T23:00:08.353314Z","shell.execute_reply.started":"2022-01-06T23:00:08.331062Z","shell.execute_reply":"2022-01-06T23:00:08.352430Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"vec_closest_defender_distance = np.vectorize(closest_defender_distance)\nvec_defenders_within_radius = np.vectorize(defenders_within_radius)\nvec_blockers_within_radius = np.vectorize(blockers_within_radius)\nvec_distance_to_sideline = np.vectorize(distance_to_sideline)\nvec_closest_defender_speed_upfield = np.vectorize(closest_defender_speed_upfield)\nvec_closest_defender_speed_lateral = np.vectorize(closest_defender_speed_lateral)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:08.354484Z","iopub.execute_input":"2022-01-06T23:00:08.354711Z","iopub.status.idle":"2022-01-06T23:00:08.369749Z","shell.execute_reply.started":"2022-01-06T23:00:08.354683Z","shell.execute_reply":"2022-01-06T23:00:08.368900Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Load Data","metadata":{}},{"cell_type":"code","source":"DIR = \"../input/nfl-big-data-bowl-2022\"\nDIR_VT = \"../input/process-punt-return-decision-data\"\nDIR_PRED = \"../input/model-training-returns-for-loss\"\ndf_preds = pd.read_csv(f\"{DIR_PRED}/predictions.csv\")\ndf_preds[\"players\"] = df_preds[\"players\"].apply(lambda j: json.loads(j))\nprint(f\"Loaded {df_preds.shape[0]:,d} plays with predictions.\")\ndf_games = pd.read_csv(f\"{DIR}/games.csv\")\ndf_players = pd.read_csv(f\"{DIR}/players.csv\")\n# Get patched versions from our custom output\ndf_plays = pd.read_csv(f\"{DIR_VT}/plays_patched.csv\")\ndf_pff = pd.read_csv(f\"{DIR_VT}/pff_patched.csv\")","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:08.371228Z","iopub.execute_input":"2022-01-06T23:00:08.371682Z","iopub.status.idle":"2022-01-06T23:00:09.246764Z","shell.execute_reply.started":"2022-01-06T23:00:08.371650Z","shell.execute_reply":"2022-01-06T23:00:09.245900Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"PLAY_KEYS = [\"gameId\", \"playId\"]","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.249499Z","iopub.execute_input":"2022-01-06T23:00:09.249986Z","iopub.status.idle":"2022-01-06T23:00:09.254100Z","shell.execute_reply.started":"2022-01-06T23:00:09.249941Z","shell.execute_reply":"2022-01-06T23:00:09.253524Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"MODEL_NAME = \"lr_bal\"\nEXPECTED_LOSS = f\"loss_{MODEL_NAME}_binary\"\nEXPECTED_SCORE = f\"prob_{MODEL_NAME}_binary\"\nmodel_path = f\"../input/model-training-returns-for-loss/models/{MODEL_NAME}_binary.joblib\"\nmodel = joblib.load(model_path)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.255616Z","iopub.execute_input":"2022-01-06T23:00:09.256151Z","iopub.status.idle":"2022-01-06T23:00:09.268405Z","shell.execute_reply.started":"2022-01-06T23:00:09.256107Z","shell.execute_reply":"2022-01-06T23:00:09.267580Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def get_returner_team(row: pd.Series) -> str:\n    if row.possessionTeam == row.homeTeamAbbr:\n        return row.visitorTeamAbbr\n    return row.homeTeamAbbr\n\n\ndf_preds[\"returnerTeam\"] = df_preds.apply(get_returner_team, axis=\"columns\")\ndf_preds[\"returnerTeam\"] = df_preds[\"returnerTeam\"].apply(lambda t: \"LV\" if t == \"OAK\" else t)\nassert df_preds[\"returnerTeam\"].isna().sum() == 0, \"Returner team column has nulls.\"","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:46.323975Z","iopub.execute_input":"2022-01-06T23:44:46.324544Z","iopub.status.idle":"2022-01-06T23:44:46.529488Z","shell.execute_reply.started":"2022-01-06T23:44:46.324495Z","shell.execute_reply":"2022-01-06T23:44:46.528882Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_preds[\"isGain\"] = ~df_preds[\"isZeroOrLoss\"]","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:47.055150Z","iopub.execute_input":"2022-01-06T23:44:47.055617Z","iopub.status.idle":"2022-01-06T23:44:47.060813Z","shell.execute_reply.started":"2022-01-06T23:44:47.055566Z","shell.execute_reply":"2022-01-06T23:44:47.060107Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_preds.columns","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:47.256471Z","iopub.execute_input":"2022-01-06T23:44:47.256875Z","iopub.status.idle":"2022-01-06T23:44:47.263042Z","shell.execute_reply.started":"2022-01-06T23:44:47.256846Z","shell.execute_reply":"2022-01-06T23:44:47.262261Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_preds.head()","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:47.446707Z","iopub.execute_input":"2022-01-06T23:44:47.446992Z","iopub.status.idle":"2022-01-06T23:44:47.478524Z","shell.execute_reply.started":"2022-01-06T23:44:47.446960Z","shell.execute_reply":"2022-01-06T23:44:47.477738Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Muffs","metadata":{}},{"cell_type":"code","source":"(\n    df_preds\n        [\n            (df_preds.specialTeamsResult == \"Muffed\")\n            & (df_preds.firstReturnableEvent == \"punt_land\")\n            & (df_preds.season > 2018)\n        ]\n        .sort_values(by=[EXPECTED_SCORE], ascending=[False])\n        [[\n            *PLAY_KEYS,\n            \"split\",\n            EXPECTED_SCORE,\n            \"firstReturnableEvent\",\n            \"ballYardline\",\n            \"ballLandingYardline\",\n            \"returnYardsGained\",\n            \"closestDefenderDistance\"\n        ]]\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:48.314839Z","iopub.execute_input":"2022-01-06T23:44:48.315438Z","iopub.status.idle":"2022-01-06T23:44:48.340274Z","shell.execute_reply.started":"2022-01-06T23:44:48.315385Z","shell.execute_reply":"2022-01-06T23:44:48.339275Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"play_cols = [\n    \"quarter\",\n    \"playDescription\",\n]\n(\n    df_preds\n        .query(\"playId == 3010\")\n        .join(\n            df_plays.set_index(PLAY_KEYS)[play_cols],\n            on=PLAY_KEYS\n        )\n        [[\n            *PLAY_KEYS,\n            \"split\",\n            *play_cols,\n            \"specialTeamsResult\",\n            EXPECTED_SCORE,\n        ]]\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:48.501792Z","iopub.execute_input":"2022-01-06T23:44:48.502382Z","iopub.status.idle":"2022-01-06T23:44:48.552439Z","shell.execute_reply.started":"2022-01-06T23:44:48.502334Z","shell.execute_reply":"2022-01-06T23:44:48.551873Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Gunners","metadata":{}},{"cell_type":"code","source":"# df_pff[\"gunnerJerseys\"] = df_pff[\"gunners\"].apply(lambda s: s.split(\"; \") if pd.notna(s) else [])","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:48.842196Z","iopub.execute_input":"2022-01-06T23:44:48.842597Z","iopub.status.idle":"2022-01-06T23:44:48.845411Z","shell.execute_reply.started":"2022-01-06T23:44:48.842567Z","shell.execute_reply":"2022-01-06T23:44:48.844672Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Gutsy Returners","metadata":{}},{"cell_type":"code","source":"\ndf_gutsy = df_preds[\n    df_preds[EXPECTED_LOSS]\n][[\n    *PLAY_KEYS,\n    \"specialTeamsResult\",\n    \"returnerNflId\",\n    \"returnYardsGained\",\n    \"returnerTeam\",\n    \"returnYardsGained\",\n    \"isGain\",\n    \"isZeroOrLoss\",\n    EXPECTED_LOSS,\n    EXPECTED_SCORE,\n]]","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:49.417605Z","iopub.execute_input":"2022-01-06T23:44:49.418278Z","iopub.status.idle":"2022-01-06T23:44:49.426574Z","shell.execute_reply.started":"2022-01-06T23:44:49.418235Z","shell.execute_reply":"2022-01-06T23:44:49.426019Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_gutsy.specialTeamsResult.value_counts()","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:50.364058Z","iopub.execute_input":"2022-01-06T23:44:50.364374Z","iopub.status.idle":"2022-01-06T23:44:50.372908Z","shell.execute_reply.started":"2022-01-06T23:44:50.364340Z","shell.execute_reply":"2022-01-06T23:44:50.372141Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_return_teams = (\n    df_preds\n        .groupby(\"returnerNflId\")\n        [\"returnerTeam\"].unique()\n        .reset_index()\n)\ndf_return_teams[\"returnerTeam\"] = df_return_teams[\"returnerTeam\"].apply(lambda s: \", \".join(s))","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:50.566651Z","iopub.execute_input":"2022-01-06T23:44:50.567120Z","iopub.status.idle":"2022-01-06T23:44:50.593046Z","shell.execute_reply.started":"2022-01-06T23:44:50.567072Z","shell.execute_reply":"2022-01-06T23:44:50.592441Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_gutsy_returns = (\n    df_gutsy\n        [\n            (df_gutsy.specialTeamsResult == \"Return\")\n            | (df_gutsy.specialTeamsResult == \"Muffed\")\n        ]\n        .groupby(\"returnerNflId\")\n        [\"playId\"].count()\n        .reset_index()\n        .rename(columns={\"playId\": \"nGutsyReturns\"})\n)\ndf_gutsy_totals = (\n    df_gutsy\n        .groupby(\"returnerNflId\")\n        [\"playId\"].count()\n        .reset_index()\n        .rename(columns={\"playId\": \"nGutsy\"})\n)\ndf_gutsy_gain_rate = (\n    df_gutsy\n        .groupby(\"returnerNflId\")\n        [\"isGain\"].mean()\n        .reset_index()\n        .rename(columns={\"isGain\": \"gainRateGutsy\"})\n)\ndf_gutsy_avg_yards = (\n    df_gutsy\n        .groupby(\"returnerNflId\")\n        [\"returnYardsGained\"].mean()\n        .reset_index()\n        .rename(columns={\"returnYardsGained\": \"avgReturnYardsGainedOnGutsyReturns\"})\n)\ndf_gutsy_metrics = (\n    df_gutsy_returns\n        .join(df_gutsy_totals.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n        .join(df_gutsy_avg_yards.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n        .join(df_gutsy_gain_rate.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n)\ndf_gutsy_metrics[\"gutsyReturnRate\"] = df_gutsy_metrics[\"nGutsyReturns\"] / df_gutsy_metrics[\"nGutsy\"]\ndf_gutsy_players = (\n    df_gutsy_metrics\n        .join(df_players.set_index(\"nflId\"), on=\"returnerNflId\")\n        .rename(columns={\"Position\": \"position\"})\n        .join(df_return_teams.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n)\n(df_gutsy_players.nGutsy >= 15).sum()","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:50.771193Z","iopub.execute_input":"2022-01-06T23:44:50.771646Z","iopub.status.idle":"2022-01-06T23:44:50.814789Z","shell.execute_reply.started":"2022-01-06T23:44:50.771603Z","shell.execute_reply":"2022-01-06T23:44:50.813927Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"gutsy_show_cols = {\n    \"displayName\": \"Returner\",\n    \"position\": \"Pos\",\n    \"returnerTeam\": \"Teams\",\n    \"nGutsy\": \"Difficult Returnable Punts Faced\",\n    \"nGutsyReturns\": \"Difficult Punts Returned\",\n    \"gutsyReturnRate\": \"Difficult Punt Return Rate\",\n    \"gainRateGutsy\": \"Gain Rate on Difficult Returnable Punts\",\n    \"avgReturnYardsGainedOnGutsyReturns\": \"Average Return Yards on Difficult Returnable Punts\",\n}\ndf_gutsy_ranks = (df_gutsy_players\n    [df_gutsy_players.nGutsy >= 15]\n    .sort_values(\n        by=[\"gainRateGutsy\", \"nGutsy\"],\n        ascending=[False, False]\n    )\n    [gutsy_show_cols.keys()]\n     .rename(columns=gutsy_show_cols)\n     .reset_index(drop=True)\n)\ndf_gutsy_ranks[\"Rank\"] = (np.arange(len(df_gutsy_ranks)) + 1).astype(int)\n(\n    df_gutsy_ranks\n        [[\"Rank\"] + list(gutsy_show_cols.values())]\n        .T.drop_duplicates().T\n        .head(10)\n        .style.format({\n            \"Average Return Yards on Difficult Returnable Punts\": \"{:.1f}\",\n            \"Difficult Punt Return Rate\": \"{:.3f}\",\n            \"Gain Rate on Difficult Returnable Punts\": \"{:.3f}\",\n        })\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:50.966321Z","iopub.execute_input":"2022-01-06T23:44:50.966799Z","iopub.status.idle":"2022-01-06T23:44:51.000697Z","shell.execute_reply.started":"2022-01-06T23:44:50.966763Z","shell.execute_reply":"2022-01-06T23:44:50.999752Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"line_kws = dict(\n    color=\"black\",\n    alpha=0.5,\n    dashes=[4, 1]\n)\nsns.scatterplot(\n    data=df_gutsy_ranks,\n    x=\"Difficult Punt Return Rate\",\n    y=\"Gain Rate on Difficult Returnable Punts\"\n)\nfor i, t in df_gutsy_ranks.iterrows():\n    if i < 5:\n        plt.text(\n            s=t[\"Returner\"],\n            x=t[\"Difficult Punt Return Rate\"],\n            y=t[\"Gain Rate on Difficult Returnable Punts\"]\n        )\nplt.axvline(df_gutsy_ranks[\"Difficult Punt Return Rate\"].mean(), **line_kws)\nplt.axhline(df_gutsy_ranks[\"Gain Rate on Difficult Returnable Punts\"].mean(), **line_kws)\nplt.gcf().set_size_inches(16, 8)\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:46:49.613329Z","iopub.execute_input":"2022-01-06T23:46:49.614171Z","iopub.status.idle":"2022-01-06T23:46:49.884583Z","shell.execute_reply.started":"2022-01-06T23:46:49.614131Z","shell.execute_reply":"2022-01-06T23:46:49.883992Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_preds[\"gutsyYards\"] = df_preds[EXPECTED_SCORE] * df_preds[\"returnYardsGained\"]\ndf_gutsy_yards = (\n    df_preds\n        .groupby(\"returnerNflId\")\n        [\"gutsyYards\"].mean()\n        .reset_index()\n        .rename(columns={\"gutsyYards\": \"meanGutsyYards\"})\n        .join(\n            df_preds\n                .groupby(\"returnerNflId\")\n                [\"gutsyYards\"].sum()\n                .reset_index()\n                .rename(columns={\"gutsyYards\": \"sumGutsyYards\"})\n                .set_index(\"returnerNflId\"),\n            on=\"returnerNflId\"\n        )\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:51.161293Z","iopub.execute_input":"2022-01-06T23:44:51.161761Z","iopub.status.idle":"2022-01-06T23:44:51.173729Z","shell.execute_reply.started":"2022-01-06T23:44:51.161711Z","shell.execute_reply":"2022-01-06T23:44:51.173184Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"result_cols = df_preds.specialTeamsResult.unique()\nrate_cols = [f\"{col} Rate\" for col in result_cols]\n\ndf_return_results = pd.concat([\n    df_preds[PLAY_KEYS + [\"returnerNflId\"]],\n    pd.get_dummies(df_preds[\"specialTeamsResult\"])\n], axis=1)\ndf_return_totals = (\n    df_preds\n        .groupby(\"returnerNflId\")\n        [\"playId\"].count()\n        .reset_index()\n        .rename(columns={\"playId\": \"totalReturnable\"})\n)\ndf_return_counts = (\n    df_return_results\n        .groupby(\"returnerNflId\")\n        [result_cols].sum()\n        .reset_index()\n        .join(\n            df_return_totals.set_index(\"returnerNflId\"),\n            on=\"returnerNflId\"\n        )\n)\ndf_return_freq = (\n    df_return_counts\n        .join(\n            df_return_counts.set_index(\"returnerNflId\")[result_cols],\n            on=\"returnerNflId\",\n            rsuffix=\" Rate\"\n        )\n)\ndf_return_freq[rate_cols] = (\n    df_return_counts\n        [result_cols]\n        .div(df_return_counts.totalReturnable, axis=0)\n)\ndf_return_yards = (\n    df_preds\n        .groupby(\"returnerNflId\")\n        [\"returnYardsGained\"].mean()\n        .reset_index()\n        .rename(columns={\"returnYardsGained\": \"meanReturnYards\"})\n        .join(\n            df_gutsy\n                .groupby(\"returnerNflId\")\n                [\"returnYardsGained\"].sum()\n                .reset_index()\n                .rename(columns={\"returnYardsGained\": \"sumReturnYards\"})\n                .set_index(\"returnerNflId\"),\n            on=\"returnerNflId\"\n        )\n)\ndf_return_players = (\n    df_return_freq\n        .join(df_players.set_index(\"nflId\"), on=\"returnerNflId\")\n        .rename(columns={\"Position\": \"position\"})\n        .join(df_return_teams.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n        .join(df_gutsy_metrics.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n        .join(df_return_yards.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n        .join(df_gutsy_yards.set_index(\"returnerNflId\"), on=\"returnerNflId\")\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:51.349180Z","iopub.execute_input":"2022-01-06T23:44:51.349751Z","iopub.status.idle":"2022-01-06T23:44:51.398504Z","shell.execute_reply.started":"2022-01-06T23:44:51.349712Z","shell.execute_reply":"2022-01-06T23:44:51.397564Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"returner_show_cols = [\n    \"displayName\",\n    \"position\",\n    \"nGutsyReturns\",\n    \"nGutsy\",\n    \"totalReturnable\",\n    \"gutsyReturnRate\",\n    \"meanGutsyYards\",\n    \"sumGutsyYards\",\n    \"meanReturnYards\",\n    \"sumReturnYards\",\n]\n(df_return_players\n     .sort_values(\n         by=[\"sumGutsyYards\"],\n         ascending=[False]\n     )\n     [returner_show_cols]\n     .head())","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:51.548018Z","iopub.execute_input":"2022-01-06T23:44:51.548797Z","iopub.status.idle":"2022-01-06T23:44:51.569411Z","shell.execute_reply.started":"2022-01-06T23:44:51.548748Z","shell.execute_reply":"2022-01-06T23:44:51.568790Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_return_players[df_return_players.meanGutsyYards > df_return_players.meanReturnYards][returner_show_cols]","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:51.795238Z","iopub.execute_input":"2022-01-06T23:44:51.795830Z","iopub.status.idle":"2022-01-06T23:44:51.819807Z","shell.execute_reply.started":"2022-01-06T23:44:51.795786Z","shell.execute_reply":"2022-01-06T23:44:51.819264Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"df_return_players[df_return_players.returnerNflId == 45599].iloc[0]","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:52.602339Z","iopub.execute_input":"2022-01-06T23:44:52.602912Z","iopub.status.idle":"2022-01-06T23:44:52.616563Z","shell.execute_reply.started":"2022-01-06T23:44:52.602856Z","shell.execute_reply":"2022-01-06T23:44:52.615334Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"gutsy_show_cols = [\n    \"returnerNflId\",\n    \"displayName\",\n    \"position\",\n    \"nGutsyReturns\",\n    \"nGutsy\",\n    \"gutsyReturnRate\",\n    \"returnerTeam\",\n]\n(df_return_players\n    [df_return_players.nGutsy >= 15]\n    .sort_values(\n        by=[\"gutsyReturnRate\", \"nGutsy\"],\n        ascending=[False, False]\n    )\n    [gutsy_show_cols]\n    .head(10))","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:57.397855Z","iopub.execute_input":"2022-01-06T23:44:57.398384Z","iopub.status.idle":"2022-01-06T23:44:57.421937Z","shell.execute_reply.started":"2022-01-06T23:44:57.398351Z","shell.execute_reply":"2022-01-06T23:44:57.421175Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Teams","metadata":{}},{"cell_type":"code","source":"dfg_team_returns = (\n    df_gutsy\n        [\n            (df_gutsy.specialTeamsResult == \"Return\")\n            | (df_gutsy.specialTeamsResult == \"Muffed\")\n        ]\n        .groupby(\"returnerTeam\")\n        [\"playId\"].count()\n        .reset_index()\n        .rename(columns={\"playId\": \"nGutsyReturns\"})\n)\ndfg_team_totals = (\n    df_gutsy\n        .groupby(\"returnerTeam\")\n        [\"playId\"].count()\n        .reset_index()\n        .rename(columns={\"playId\": \"nGutsy\"})\n)\n\ndf_team_gain_rate = (\n    df_gutsy\n        .groupby(\"returnerTeam\")\n        [\"isGain\"].mean()\n        .reset_index()\n        .rename(columns={\"isGain\": \"gainRateGutsy\"})\n)\ndf_team_avg_yards = (\n    df_gutsy\n        .groupby(\"returnerTeam\")\n        [\"returnYardsGained\"].mean()\n        .reset_index()\n        .rename(columns={\"returnYardsGained\": \"avgReturnYardsGainedOnGutsyReturns\"})\n)\ndfg_team_metrics = (\n    dfg_team_returns\n        .join(dfg_team_totals.set_index(\"returnerTeam\"), on=\"returnerTeam\")\n        .join(df_team_gain_rate.set_index(\"returnerTeam\"), on=\"returnerTeam\")\n        .join(df_team_avg_yards.set_index(\"returnerTeam\"), on=\"returnerTeam\")\n)\ndfg_team_metrics[\"gutsyReturnRate\"] = dfg_team_metrics[\"nGutsyReturns\"] / dfg_team_metrics[\"nGutsy\"]","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:44:59.619491Z","iopub.execute_input":"2022-01-06T23:44:59.620224Z","iopub.status.idle":"2022-01-06T23:44:59.655448Z","shell.execute_reply.started":"2022-01-06T23:44:59.620160Z","shell.execute_reply":"2022-01-06T23:44:59.654745Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"team_show_cols = {\n    \"returnerTeam\": \"Team\",\n    \"nGutsy\": \"Difficult Returnable Punts Faced\",\n    \"nGutsyReturns\": \"Difficult Punts Returned\",\n    \"gutsyReturnRate\": \"Difficult Punt Return Rate\",\n    \"gainRateGutsy\": \"Gain Rate on Difficult Returnable Punts\",\n    \"avgReturnYardsGainedOnGutsyReturns\": \"Average Return Yards on Difficult Returnable Punts\",\n}\ndf_team_ranks = (dfg_team_metrics\n    .sort_values(\n        by=[\"gainRateGutsy\", \"nGutsy\"],\n        ascending=[False, False]\n    )\n    [team_show_cols.keys()]\n     .rename(columns=team_show_cols)\n     .reset_index(drop=True)\n)\ndf_team_ranks[\"Rank\"] = (np.arange(len(df_team_ranks)) + 1).astype(int)\n(\n    df_team_ranks\n        [[\"Rank\"] + list(team_show_cols.values())]\n)\n(df_team_ranks.T.drop_duplicates().T\n        .head(10)\n        .style.format({\n            \"Average Return Yards on Difficult Returnable Punts\": \"{:.1f}\",\n            \"Difficult Punt Return Rate\": \"{:.3f}\",\n            \"Gain Rate on Difficult Returnable Punts\": \"{:.3f}\",\n        }))","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:45:00.177575Z","iopub.execute_input":"2022-01-06T23:45:00.178232Z","iopub.status.idle":"2022-01-06T23:45:00.205828Z","shell.execute_reply.started":"2022-01-06T23:45:00.178179Z","shell.execute_reply":"2022-01-06T23:45:00.204931Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"line_kws = dict(\n    color=\"black\",\n    alpha=0.5,\n    dashes=[4, 1]\n)\nsns.scatterplot(\n    data=df_team_ranks,\n    x=\"Difficult Punt Return Rate\",\n    y=\"Gain Rate on Difficult Returnable Punts\"\n)\nfor _, t in df_team_ranks.iterrows():\n    plt.text(\n        s=t[\"Team\"],\n        x=t[\"Difficult Punt Return Rate\"],\n        y=t[\"Gain Rate on Difficult Returnable Punts\"]\n    )\nplt.axvline(df_team_ranks[\"Difficult Punt Return Rate\"].mean(), **line_kws)\nplt.axhline(df_team_ranks[\"Gain Rate on Difficult Returnable Punts\"].mean(), **line_kws)\nplt.gcf().set_size_inches(16, 8)\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:45:04.885918Z","iopub.execute_input":"2022-01-06T23:45:04.886429Z","iopub.status.idle":"2022-01-06T23:45:05.139948Z","shell.execute_reply.started":"2022-01-06T23:45:04.886383Z","shell.execute_reply":"2022-01-06T23:45:05.139102Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Unexpected Returns","metadata":{}},{"cell_type":"code","source":"show_cols = [\n    *PLAY_KEYS,\n    \"split\",\n    \"firstReturnableEvent\",\n    \"kickingYardline\",\n    \"receivingYardline\",\n    \"ballLandingYardline\",\n    EXPECTED_LOSS,\n    EXPECTED_SCORE,\n    \"returnYardsGained\",\n]\n(\n    df_preds\n        [\n            (df_preds[EXPECTED_LOSS])\n            & (df_preds.returnYardsGained > 20)\n        ]\n        [show_cols]\n        .sort_values(\n            by=[\"returnYardsGained\", EXPECTED_SCORE],\n            # by=[EXPECTED_SCORE, \"returnYardsGained\"],\n            ascending=[False, False]\n        )\n        .head(10)\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.820947Z","iopub.execute_input":"2022-01-06T23:00:09.821280Z","iopub.status.idle":"2022-01-06T23:00:09.848437Z","shell.execute_reply.started":"2022-01-06T23:00:09.821242Z","shell.execute_reply":"2022-01-06T23:00:09.847852Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"(\n    df_preds\n        [\n            (df_preds[EXPECTED_LOSS])\n            & (df_preds.returnYardsGained < 0)\n        ]\n        [show_cols]\n        .sort_values(\n            by=[\"returnYardsGained\", EXPECTED_SCORE],\n            # by=[EXPECTED_SCORE, \"returnYardsGained\"],\n            ascending=[True, False]\n        )\n        .head(10)\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.849451Z","iopub.execute_input":"2022-01-06T23:00:09.849802Z","iopub.status.idle":"2022-01-06T23:00:09.878059Z","shell.execute_reply.started":"2022-01-06T23:00:09.849775Z","shell.execute_reply":"2022-01-06T23:00:09.877258Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"(\n    df_preds\n        [\n            (~df_preds[EXPECTED_LOSS])\n            & (df_preds.returnYardsGained > 20)\n            & (df_preds.kickingYardline >= 40)\n        ]\n        [show_cols]\n        .sort_values(\n            #by=[\"returnYardsGained\", EXPECTED_SCORE],\n            by=[EXPECTED_SCORE, \"returnYardsGained\"],\n            ascending=[True, False]\n        )\n        .head(10)\n)","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.879236Z","iopub.execute_input":"2022-01-06T23:00:09.879464Z","iopub.status.idle":"2022-01-06T23:00:09.904220Z","shell.execute_reply.started":"2022-01-06T23:00:09.879436Z","shell.execute_reply":"2022-01-06T23:00:09.903413Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model Heat Map","metadata":{}},{"cell_type":"code","source":"INPUT_COLS = [\n    # Defender and blocker features\n    \"closestDefenderDistance\",\n    \"defendersWithinRadius\",\n    \"blockersWithinRadius\",\n    \"closestDefenderSpeedUpfield\",\n    \"closestDefenderSpeedLateral\",\n    # Field position features\n    \"ballYardline\",\n    \"distanceToSideline\",\n    \"isInsideOwnEndzone\",\n    \"isInsideOwn20\",\n    \"isInsideOwn10\",\n]","metadata":{"_kg_hide-output":false,"execution":{"iopub.status.busy":"2022-01-06T23:00:09.905428Z","iopub.execute_input":"2022-01-06T23:00:09.905662Z","iopub.status.idle":"2022-01-06T23:00:09.914390Z","shell.execute_reply.started":"2022-01-06T23:00:09.905635Z","shell.execute_reply":"2022-01-06T23:00:09.913646Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"ids = 2019100606, 3010\nGAME_ID, PLAY_ID = ids\ndf_return = df_preds[(df_preds.gameId == GAME_ID) * (df_preds.playId == PLAY_ID)]\nXs = df_return[INPUT_COLS + [\"possessionTeam\", \"players\"]]\nplay = df_return.iloc[0]","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.915446Z","iopub.execute_input":"2022-01-06T23:00:09.915696Z","iopub.status.idle":"2022-01-06T23:00:09.927523Z","shell.execute_reply.started":"2022-01-06T23:00:09.915658Z","shell.execute_reply":"2022-01-06T23:00:09.926756Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"play.returnerNflId","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.928708Z","iopub.execute_input":"2022-01-06T23:00:09.929055Z","iopub.status.idle":"2022-01-06T23:00:09.936369Z","shell.execute_reply.started":"2022-01-06T23:00:09.929023Z","shell.execute_reply":"2022-01-06T23:00:09.935721Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"X_YDS = 120\nY_YDS = 54\nHEAT_DIM = (Y_YDS, X_YDS)\nX = Xs.loc[Xs.index.repeat(X_YDS * Y_YDS)].reset_index(drop=True)\nrow_idxs, col_idxs = np.indices(dimensions=HEAT_DIM)\nX[\"ballX\"] = col_idxs.flatten()\nX[\"ballY\"] = row_idxs.flatten()","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.937264Z","iopub.execute_input":"2022-01-06T23:00:09.937850Z","iopub.status.idle":"2022-01-06T23:00:09.949499Z","shell.execute_reply.started":"2022-01-06T23:00:09.937819Z","shell.execute_reply":"2022-01-06T23:00:09.948726Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Main inputs\nbx = X[\"ballX\"]\nby = X[\"ballY\"]\np = X[\"players\"]\nkt = X[\"possessionTeam\"]\n# Correct ball yard line and receiving yard line\nRECEIVING_GOAL_LINE = 10\nX[\"ballYardline\"] = X[\"ballX\"] - RECEIVING_GOAL_LINE\n# Defender and blocker features\nX[\"closestDefenderDistance\"] = vec_closest_defender_distance(bx, by, p, kt)\nX[\"defendersWithinRadius\"] = vec_defenders_within_radius(bx, by, p, kt, 2)\nX[\"blockersWithinRadius\"] = vec_blockers_within_radius(bx, by, p, kt, 5)\n# Field position features\nX[\"distanceToSideline\"] = vec_distance_to_sideline(by)\nX[\"isInsideOwnEndzone\"] = X[\"ballYardline\"] <= 0\nX[\"isInsideOwn20\"] = X[\"ballYardline\"] <= 20\nX[\"isInsideOwn10\"] = X[\"ballYardline\"] <= 10\n# Returner features\nX[\"closestDefenderSpeedUpfield\"] = vec_closest_defender_speed_upfield(bx, by, p, kt)\nX[\"closestDefenderSpeedLateral\"] = vec_closest_defender_speed_lateral(bx, by, p, kt)\n# Remove columns used for feature engineering\nX = X[INPUT_COLS]\nX.shape","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:09.950769Z","iopub.execute_input":"2022-01-06T23:00:09.950967Z","iopub.status.idle":"2022-01-06T23:00:11.114098Z","shell.execute_reply.started":"2022-01-06T23:00:09.950941Z","shell.execute_reply":"2022-01-06T23:00:11.113251Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"Yp_mat = model.predict_proba(X)[:,1].reshape(*HEAT_DIM)\nYp_mat.shape","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:11.115122Z","iopub.execute_input":"2022-01-06T23:00:11.115411Z","iopub.status.idle":"2022-01-06T23:00:11.133155Z","shell.execute_reply.started":"2022-01-06T23:00:11.115381Z","shell.execute_reply":"2022-01-06T23:00:11.132356Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plt.imshow(Yp_mat, cmap=\"PiYG_r\")\nplt.colorbar()\nplot_field(plt)\nfor p in play[\"players\"]:\n    plt.scatter(p[\"x\"], p[\"y\"], color=team_to_color(p[\"team\"]))\nplt.scatter(play[\"ballX\"], play[\"ballY\"], color=\"brown\")\nplt.scatter(play[\"ballLandingX\"], play[\"ballLandingY\"], color=\"black\")\nplt.text(s=\"Decision\", x=play[\"ballX\"], y=play[\"ballY\"])\nplt.text(s=\"Landing\", x=play[\"ballLandingX\"], y=play[\"ballLandingY\"])\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2022-01-06T23:00:11.134552Z","iopub.execute_input":"2022-01-06T23:00:11.135219Z","iopub.status.idle":"2022-01-06T23:00:11.737922Z","shell.execute_reply.started":"2022-01-06T23:00:11.135157Z","shell.execute_reply":"2022-01-06T23:00:11.737114Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}