{"metadata":{"kernelspec":{"name":"ir","display_name":"R","language":"R"},"language_info":{"name":"R","codemirror_mode":"r","pygments_lexer":"r","mimetype":"text/x-r-source","file_extension":".r","version":"4.0.5"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Appendix for Player Target Vision (PlayerTV):\n\n## Contents Table:\n\n* [Offensive / Defensive Usage](#section-3)\n* [PlayerTV](#comp-subsection-5)\n    - [Players](#comp-subsection-5)\n        - [Andre Roberts](#PTV_NYJ_19)\n        - [Charone Peake](#PTV_NYJ_17)\n        - [Terrence Brooks](#PTV_NYJ_23)\n        - [Darryl Roberts](#PTV_NYJ_27)\n        - [Trenton Cannon](#PTV_NYJ_40)\n        - [Parry Nickerson](#PTV_NYJ_43)\n        - [Neville Hewitt](#PTV_NYJ_46)\n        - [Frankie Luvu](#PTV_NYJ_50)\n        - [Brandon Copeland](#PTV_NYJ_51)\n        - [Jeremiah Attaochu](#PTV_NYJ_55)\n        - [Christopher Herndon](#PTV_NYJ_89)\n        - [Sam Martin](#PTV_DET_06)\n        - [Bradley Marquez](#PTV_DET_12)\n        - [Tavon Wilson](#PTV_DET_32)\n        - [Miles Killebrew](#PTV_DET_35)\n        - [Nick Bellore](#PTV_DET_43)\n        - [Jalen Reeves-Maybin](#PTV_DET_44)\n        - [Charles Washington](#PTV_DET_45)\n        - [Tracy Walker](#PTV_DET_47)\n        - [Don Muhlbach](#PTV_DET_48)\n        - [Christian Jones](#PTV_DET_52)\n        - [Marquis Flowers](#PTV_DET_59)\n* [Previous Discussions after publishing PlayerTV on Twitter:](#section-8)\n    - [GoArmyEdge](#AD_1)\n    - [Animations](#AD_2)\n    - [VR Capabilities](#AD_3)\n    - [Standardisation](#AD_4)\n    - [Sensors](#AD_5)\n    - [Private Camera Angles](#AD_6)\n    - [Resources Required](#AD_7)\n    - [Commericalisation](#AD_8)\n    - [Game Pass](#AD_9)\n* [Code Review](#section-9)\n    - [Installing and Loading Packages](#code_review_1)\n    - [Loading Competition Data (and Cleansing)](#code_review_2)\n    - [TV Guide](#code_review_3)\n    - [User Inputs](#code_review_4)\n    - [Filtered TV Guide](#code_review_5)\n    - [Save File Naming Function](#code_review_6)\n    - [Targeted Player Tracking Data](#code_review_7)\n    - [Other 21 Players Tracking Data](#code_review_8)\n    - [Building the Stadium](#code_review_9)\n    - [One Dataframe](#code_review_10)\n    - [Adding Fun Stats](#code_review_11)\n    - [Radar, aka the Dots](#code_review_radar)\n    - [Screenshot Function](#code_review_12)\n    - [Animation Function](#code_review_13)\n* [Final Words](#section-10)\n\n<a id=\"usage_2\"></a>\n## Offensive / Defensive Usage:\n\nDue to the manner of the mission statement for the #BigDataBowl 2022 being \"Help evaluate special teams performance\", I am going to talk about helping with offence and defence in the future usage section - even though it is available now. \n\nStarting with the offence, the player(s) who would most benefit from this product would be the QBs. The QBs are the ones who need to learn opponent coverage schemes, blitz packages and, even their own team's playbook. Being able to simulate snaps using VR would give the QB the immersion like he is on the field without the risk of being hit. The QB would be able to simulate everything inside the pocket (from moving around pressure, making reads/progressions, making the throw or even starting the process of scrambling). A further idea would be that (using NGS) you could simulate the percentage chance the WR catches the ball, or an interception takes place. Essentially, a QB is just playing a video game with real-life data fully replicated (an idea that will be explored later).\nThe Oline can use it to identify blitz packages (like the QB) and practice taking the snap and reacting to the plays. This is the main other position where I think the VR simulation would be fully immersive. Training the Oline how to snap the ball in some of the loudest stadiums requires practice, and that can be fully simulated using the speakers within the VR headset. \nThe rest of the positions can still utilise the simulation, but the immersion is less. WRs could line up for the snap and identify how the opposition CB lines up (inside, outside or head-on)? In press coverage, how do they use their hands? \nRBs could use this product to help them identify gaps in real-time. By giving the RB historical plays (or made up plays) and seeing their reaction time to identify the gap. Yards gained is all about identifying gaps (either pure rushes or yards after the catch), so providing training software to help improve their onfield intelligence would significantly improve the team's performance.\n\nOn the defensive side; I think LBs would be the position that would most benefit from this product. In the modern era where play-action is becoming more and more common, the LBs role of identifying play-action vs handoff is critical. \nThat idea could also be used to identify screen passes for defensive linemen. It normally only takes one player to correctly identify the screen for the offence not to work as well as hoped for, but if all the pass rushers don't react, it can go for a big play. Therefore the VR simulation could be a great place to look for the signs that it could be a screen.\nFor CBs, the VR simulations could be used to help track the WR's route. While most routes are the same, each player will have a tendency to show if it is a comeback, slug-go or go route. The hope is that using the VR simulation, the CB can learn which is which. The other part of the CB VR simulation would be being able to track where the ball is and the player. In an era where DPI seems to more important than ever, the onfield rule seems to be if the player doesn't turn his head and gets in the way its a DPI. Therefore, it is on the CB to learn how to track the ball and player simultaneously to be able to turn his head at the right point. My hope is that this VR simulation would be able to help provide that training. \n\nBroadcast teams are probably the most important people in communicating the complex game into simple terms for the fans. The fans are becoming more intelligent (starting to acknowledge EPA and other advanced metrics), but there is still a knowledge gap of what it is to experience being on the field. The Jalen Hurts example where Dallas Goedert was wide open for a Touchdown and the camera angle showed it clearly led to a lot of active discussions on social media. It took for Jalen Hurts and the Eagles to say in detail about the reads and progressions on that play for the casual fan to understand. With PlayerTV, the broadcast team would have instantly shown what Jalen Hurts was looking at and why he never threw the ball to the open WR.\n\nAs much as it is the broadcast team's job, other media is used to provide extra analysis of each game to the fans. This analysis can go into additional details that take more time to research. PlayerTV would be able to give the media an additional way to show what each player did on the play and why they did it. An article on any player could showcase all these plays differently rather than just using all-22.\n\nThere is an additional future usage; I mentioned how the QB is basically a video game, so why not make it a video game? There would be an active market to sell the game into. This would not only drive revenue for the NFL, but also educate the fans in the process. ","metadata":{"_kg_hide-input":false}},{"cell_type":"markdown","source":"<a id=\"comp-subsection-5\"></a>\n## PlayerTV:\nWe have now looked at all the publicly available methods of analysing special team play before the Big Data Bowl to evaluate the play. We can now look at what PlayerTV sees when looking at this play. To do this I'll look at the Kick Returner first and then each player on the return team, followed by all the players on the kicking team. \n\nBefore we analyse the play using PlayerTV, I want to list the features:\n* Perceived Height\n* Perceived Width\n* Radar Chart\n* Name\n* Speed (mph)\n* Direction Player is Facing\n\nThese additional features help the coach/scout/analyst to understand the play easier. Using a combination of `Facing` (Direction of travel with 0 being directly towards the endzone) and `Speed` (mph), you can identify when the player makes a \"cut\" move. \n\nPerceived Height and Width immerses you into the play with the other players moving closer (therefore larger) or further away from you and getting smaller.\n\nUsing the radar chart (the dots), you can identify where the chosen player is in context to other players and the field.\n\n<a id=\"PTV_NYJ_19\"></a>\n#### Andre Roberts:","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Andre%20Roberts.gif)\n\n* Team: New York Jets \n* Number: #19\n* Role: Kick Returner\n\n\n\nPlayerTV identified that Roberts runs backwards to the left in order to catch the ball before running right of the first player (using the dots, we identify him as Lions #12) and the second opponent (Lions #52). After taking evasive action which forced Roberts to run 45 degrees to the right, Roberts, then, cuts in 135 degrees to the left. Now the sideline, Roberts, then, used his speed and acceleration to go from around 12mph to 19mph. This speed meant he could just outrun everyone to score the Touchdown. \n\nYou can see the available space before he cuts back in, which is really good to see. Also, he almost cuts into two opponents, causing a missed tackle attempt by one of them. The punter (Sam Martin) also has a missed tackle, which is really fun to see.","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_17\"></a>\n#### Charone Peake","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Charone%20Peake.gif)\n\n* Team: New York Jets\n* Number: #17\n* Role: Vice","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_23\"></a>\n#### Terrence Brooks","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Terrence%20Brooks.gif)\n\n* Team: New York Jets\n* Number: #23\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_27\"></a>\n#### Darryl Roberts","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Darryl%20Roberts.gif)\n\n* Team: New York Jets\n* Number: #27\n* Role: Special Teams Safety","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_40\"></a>\n#### Trenton Cannon","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Trenton%20Cannon.gif)\n\n* Team: New York Jets\n* Number: #40\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_43\"></a>\n#### Parry Nickerson","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Parry%20Nickerson.gif)\n\n* Team: New York Jets\n* Number: #43\n* Role: Vise","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_46\"></a>\n#### Neville Hewitt","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Neville%20Hewitt.gif)\n\n* Team: New York Jets\n* Number: #46\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_50\"></a>\n#### Frankie Luvu","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Frankie%20Luvu.gif)\n\n* Team: New York Jets\n* Number: #50\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_51\"></a>\n#### Brandon Copeland","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Brandon%20Copeland.gif)\n\n* Team: New York Jets\n* Number: #51\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_55\"></a>\n#### Jeremiah Attaochu","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Jeremiah%20Attaochu.gif)\n\n* Team: New York Jets\n* Number: #55\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_NYJ_89\"></a>\n#### Christopher Herndon","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Christopher%20Herndon.gif)\n\n* Team: New York Jets\n* Number: #89\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_06\"></a>\n#### Sam Martin","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Sam%20Martin.gif)\n\n* Team: Detroit Lions\n* Number: #06\n* Role: Punter","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_12\"></a>\n#### Bradley Marquez","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Bradley%20Marquez.gif)\n\n* Team: Detroit Lions\n* Number: #12\n* Role: Gunner","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_32\"></a>\n#### Tavon Wilson","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Tavon%20Wilson.gif)\n\n* Team: Detroit Lions\n* Number: #32\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_35\"></a>\n#### Miles Killebrew","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Miles%20Killebrew.gif)\n\n* Team: Detroit Lions\n* Number: #35\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_43\"></a>\n#### Nick Bellore","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Nick%20Bellore.gif)\n\n* Team: Detroit Lions\n* Number: #43\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_44\"></a>\n#### Jalen Reeves-Maybin","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Jalen%20Reeves-Maybin.gif)\n\n* Team: Detroit Lions\n* Number: #44\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_45\"></a>\n#### Charles Washington","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Charles%20Washington.gif)\n\n* Team: Detroit Lions\n* Number: #45\n* Role: Gunner","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_47\"></a>\n#### Tracy Walker","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Tracy%20Walker.gif)\n\n* Team: Detroit Lions\n* Number: #47\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_48\"></a>\n#### Don Muhlbach","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Don%20Muhlbach.gif)\n\n* Team: Detroit Lions\n* Number: #48\n* Role: Unidentified","metadata":{"_kg_hide-input":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_52\"></a>\n#### Christian Jones\n","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Christian%20Jones.gif)\n\n* Team: Detroit Lions\n* Number: #52\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"PTV_DET_59\"></a>\n#### Marquis Flowers","metadata":{}},{"cell_type":"markdown","source":"![image](https://raw.githubusercontent.com/rogers1000/bdb2022/main/2018_1_NYJ_DET_2626_Marquis%20Flowers.gif)\n\n* Team: Detroit Lions\n* Number: #59\n* Role: Unidentified","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"section-8\"></a>\n## Previous Discussions after publishing PlayerTV on Twitter:\n\n<a id=\"AD_1\"></a>\n#### Point 1 - \"Great work makes me think of @GoArmyEdge with tracking data. There are certainly some NFL teams incorporate such technology.\" by @903124S","metadata":{}},{"cell_type":"markdown","source":"\"I didn't know this was a thing. I did try and have a quick look but nothing came up. I suspect that quite a few teams would utilise it as understanding what is happening from individuals perspectives is next level film analysis. I need to have a better look at this tonight.\"\n\n\"I did some research and actually got reached out by @GoArmyEdge themselves for me to have a look at their product on a NFL/NCAA level. The graphics and ease of use is clear to see. It is mindblowing how good the product is, but there is some room for improvement as with all things. \n\nGoArmy Edge does not advertise its product online and uses word of mouth as its primary marketing strategy. Their target audience is NFL/NCAA teams, not the public and, therefore, the public does not know about this product (including myself before being reached out to). \n\nSeeing what they do and who they do it for means that I feel confident that NFL teams would be interested in my product PlayerTV in some capacity (or all of it). If the NFL were to produce their own version, it would have to match or outspend the Army, which has spent a lot of money making GoArmy Edge by seeing their product quality.\n\nThe NFL would have a couple of advantages if it were to elect to create its own app, including but not limited to the graphics engine, automated access with coordinate data for instant replays and enforce additional equipment on teams to expand the product's capability related to motion capture to just name three. \n\nThat said, GoArmy Edge looks like a complete product, which teams can simulate plays and do almost everything that I have thought about.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_2\"></a>\n#### Point 2 - \"It's very rudimentary right now but that looks amazing!! Not an expert by any means but smoothing out the \"animation,\" adding some shaders, and improving the graphics a little bit would take this thing to the next level.\" by @MatthewS_Pod","metadata":{}},{"cell_type":"markdown","source":"\"Thank you. I 100% agree the graphics need a lot of work before it can be a professional product. With it being a competition for the NFL, they either have the rights to use the madden graphics engine already or are about to start contract talks with 2k / EA soon. What that means is that I couldn't ever animate anything close to the capabilities that the NFL could produce if they took on this idea. As a result, I focused my time on other parts of the submission because if a graphics engine can be used, nothing I make comes close to that. If this competition wasn't for the NFL and was a generic data competition, I would have spent a lot more time on the animation because I know that there would be less chance of this idea coming to life and having resources to do so. Therefore there is a sunken cost association.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_3\"></a>\n#### Point 3: VR Capability","metadata":{}},{"cell_type":"markdown","source":"\"If the NFL followed up and put it into a madden engine, VR capability would be up there on my wish list. Such a different perspective. One of the biggest things with time and resources, especially in the NFL, is the forced time limits for contact drills. This is certainly one way of countering the limits by re-enacting the plays virtually. Getting the rookies up to the speed of the NFL compared to college is one of the most vital things a coach can do. I believe this could be a training tool to teach players how quickly the game is played. \n\nYou could have it where the user selects a play and replays that play identical to what happened on the field. An example for the QB would be the user has to throw the ball and PlayerTV using motion tracking hardware works out if you passed the ball to where the WR could catch it. You could even have it where the simulation reacts to the user so if the user steps out the pocket with the simulation adapting to counter that. This could not just be a training tool but a potentially commercially viable product.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_4\"></a>\n#### Point 4: Really curious what you considered the standard for eye-line and a standard players field of view? - @All22_Addict","metadata":{}},{"cell_type":"markdown","source":"\"PlayerTV uses 180-degree vision (90 on either side) because I don't have access to an NFL helmet. It also means I can include potential glances. You are looking from someone 6ft tall (my height, but also because it's a general height of people). I would love to make it reactive in the future. Width is all one size (0.5 yards / 18inches) because its around guess of how big someone is side to side. There are no general measurements of NFL players like there is for height. In the future perhaps, but at the moment, width is standard throughout.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_5\"></a>\n#### Point 5: This is super cool! Would just caution that player body trajectory is not head direction and not gaze direction. - @mbarbaranius","metadata":{}},{"cell_type":"markdown","source":"\"I acknowledge that and understand that this is the first step in the idea. If the NFL was to be fully on board with the idea, then a sensor in the helmet would be my method of choice for capturing the data.\nI also used 180 degrees to counter the gazes. I've never worn an NFL helmet (I know the data guy never played the game), so I don't know how much you can see in them sideways, but part of the reason for 180 degrees was that it covers quick glances in either direction.\n\nOne thing to note is that I am surprised that the NFL doesn't have a sensor on the helmet to track concussion-related activity such as G-Force.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_6\"></a>\n#### Point 6: Private Camera Angles - by @DweckJoshua ","metadata":{}},{"cell_type":"markdown","source":"\"I acknowledge that this idea's benefit is much more significant at a consumer level than a professional level scout/analyst. The presumption is that every team has their own cameras all over their stadiums to track every angle possible. The understanding that the NFL All-22 footage, which is mandated at the two angles discussed above, means that the NFL teams will not publish any other angle. There are also broadcast angles that aren't publicly available being recorded. \n\nI still think that there would be a benefit of providing this service to professional teams to see what the player sees from their POV, especially as mentioned above when looking at passing plays where someone is wide open on the camera angle, but the player is looking in a totally different direction.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_7\"></a>\n#### Point 7: Resources Required","metadata":{}},{"cell_type":"markdown","source":"\"Software Development - Depending on the license agreement with EA (Madden) or 2k or any other game developer, you may have to build the graphics from scratch. Of course, that would take time and capital, even if the NFL had the license to use a pre-built graphics engine. \n\nAutomated Data Collection - You would need to have sensors on the ball to track height of the ball, you would need more sensors on the players to track head movement and how high their head is relative to others, and you would need to track the referees. \n\nManual Data Collection - You would need someone to collect all the data for what each individual did. The style of handoff (playaction or no playaction), was the player running with two hands or one hand on the ball? If one hand, which hand? Did he do a spin move or hand-off an opponent to get past them? How did he run through them? All this data would need to be collected and likely by human interaction.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_8\"></a>\n#### Point 8: Consumer Usage","metadata":{}},{"cell_type":"markdown","source":"\"This has been brought up quite a bit, but you could easily commercialise this product. People already spend 100 dollars each year on NFL GamePass, and I'm sure people would be willing to spend a lot more money on getting access to \"All-22+\". This is not only for the potential VR capabilities or the film review but also the data that comes with it.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"AD_9\"></a>\n#### Point 9: \"Put this guy in charge of Game Pass. Stat.\" - @NFL_DougFarrar","metadata":{}},{"cell_type":"markdown","source":"\"I would love to work on Game Pass and bring interaction between fans and the NFL closer together. I also acknowledge that the NFL does not want the GPS data available to the public, and therefore, my idea is unlikely ever to be viewable to the public. On that note, I would be more than happy to work for any team or organisation, it doesn't have to relate to football operations, but I would still love access to the dots.\"","metadata":{"_kg_hide-input":true,"_kg_hide-output":true}},{"cell_type":"markdown","source":"<a id=\"section-11\"></a>\n## Code Review:\n\nIn this section, I will go through all parts of my code. My code is written in Rstudio, using the programming language of R.\n\nI have split my code into 13 sections.\n- [Installing and Loading Packages](#code_review_1)\n- [Loading Competition Data (and Cleansing)](#code_review_2)\n- [TV Guide](#code_review_3)\n- [User Inputs](#code_review_4)\n- [Filtered TV Guide](#code_review_5)\n- [Save File Naming Function](#code_review_6)\n- [Targeted Player Tracking Data](#code_review_7)\n- [Other 21 Players Tracking Data](#code_review_8)\n- [Building the Stadium](#code_review_9)\n- [One Dataframe](#code_review_10)\n- [Adding Fun Stats](#code_review_11)\n- [Radar, aka the dots](#code_review_radar)\n- [Screenshot Function](#code_review_12)\n- [Animation Function](#code_review_13)\n\n<a id=\"code_review_1\"></a>\n#### Installing and Loading Packages:\nKaggle has installed all the main packages as default, but they don't have the NFL ones installed. Therefore I needed to install both `nflreadr` and `nflfastR`. \n\nI loaded all the normal packages for any occasion: `tidyverse`, `ggrepel`, `ggimage`, `ggpubr` and `stringr`. The domain specialist packages of `nflfastR` and `nflreadr` are both included too. The difference this time is that when using live graphs, you need specialist packages such as `lme4`, `repr`, `gganimate`, `cowplot`, `ggridges` and `gifski`. \n\nI have also put a `scipen` to make large numbers into decimal places and turned off warnings (for presentation purposes).","metadata":{}},{"cell_type":"code","source":"install.packages(\"nflfastR\")\ninstall.packages(\"nflreadr\")\n\noptions(scipen = 9999)\noptions(warn=-1)\nlibrary(nflfastR)\nlibrary(tidyverse)\nlibrary(nflreadr)\nlibrary(ggrepel)\nlibrary(ggimage)\nlibrary(ggpubr)\nlibrary(lme4)\nlibrary(repr)\nlibrary(gganimate)\nlibrary(cowplot)\nlibrary(ggridges)\nlibrary(stringr)\nlibrary(gifski)","metadata":{"_kg_hide-output":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-06T13:06:02.234539Z","iopub.execute_input":"2022-01-06T13:06:02.237175Z","iopub.status.idle":"2022-01-06T13:06:49.635271Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_2\"></a>\n#### Loading Competition Data (and Cleansing):\nThis is fairly simple, but I took the data provided as part of the competition and loaded it into the Kaggle environment. \n\nBecause of how my project works, I loaded the `games`, `PFF_Scouting`, `Players`, `Plays` and `tracking 2018` data. Because of how the data filenames are saved on the Kaggle website, if I wanted to change from 2018 to either 2019 or 2020, I just have to do a find-and-replace swapping 2018 to the chosen year.\n\nI loaded the TV Guide and PlayerTV animations separately because I am looking into different parts of the data depending on the purpose. \n\nThe final thing that I did relating to loading data is load `nflreadr`'s play-by-play data. This gives me extra information such as yardage for a Touchdown, a binary measurement if there was a Touchdown, the week the game was played in, the quarter the game was in for that play and the time remaining. \n\nThe yardage and touchdown data is used in creating a new play description. I found that the current one provides details that I don't think are necessary or in the most precise manner. \n\nGame week, quarter and time remaining are valuable when researching the play on NFL Game Pass to validate the outcomes. ","metadata":{}},{"cell_type":"code","source":"games <- read_csv(\"../input/nfl-big-data-bowl-2022/games.csv\") \n\nPFF_Scouting <- read_csv(\"../input/nfl-big-data-bowl-2022/PFFScoutingData.csv\") %>%\n  mutate(tracker_join = paste0(gameId,\"_\",playId)) \n\nplayers <- read_csv(\"../input/nfl-big-data-bowl-2022/players.csv\") \n\nplayers <- players %>%\n  filter(str_detect(players$height,\"-\")) %>%\n  separate(col = height, c(\"feet\",\"inches\"), remove = FALSE, convert = TRUE) %>%\n  mutate(height_inches = 12*feet + inches) %>%\n  mutate(height_yards = height_inches / 36) \n\nplays <- read_csv(\"../input/nfl-big-data-bowl-2022/plays.csv\") \n\nTV_guide_tracking_df <- read_csv(\"../input/nfl-big-data-bowl-2022/tracking2018.csv\") %>%\n  ### Getting Tracking Data Info\n  select(gameId,playId,nflId,jerseyNumber,team) %>%\n  mutate(tracker_join = paste0(gameId,\"_\",playId)) %>%\n  select(tracker_join,nflId,jerseyNumber,team) %>%\n  unique()\n\ngc()","metadata":{"_kg_hide-output":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-06T13:06:59.741685Z","iopub.execute_input":"2022-01-06T13:06:59.777740Z","iopub.status.idle":"2022-01-06T13:09:04.589242Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_3\"></a>\n#### TV Guide:\nWhile all the attention (rightly so) is on the live animations that I have built, the TV guide plays the most valuable role in saving me time when researching individual plays. \n\nThe TV Guide gives us the possibility to search for individual games, plays, players, teams, provides us with details of what happened on each play, the names of each player, their team and jersey number, if a touchdown was scored, what week/quarter the game took place in and the time remaining in that quarter. \n\nEverything you need to know when looking up a specific play. As long as the requestee tells us which player, game and time remaining; the TV Guide will help you find that play in seconds. \n\nThe code seems complex, but most of it is creating `ifelse()` statements that provide the information we are looking for. The other part of it is creating our new `Play_details` column from all the other information that is provided. ","metadata":{}},{"cell_type":"code","source":"nflreadr_df <- nflreadr::load_pbp(seasons = 2018) %>%\n  ### Getting Yardline and Touchdown data\n  select(old_game_id,play_id,yardline_100,touchdown,week,qtr,time) %>%\n  mutate(tracker_join = paste0(old_game_id,\"_\",play_id)) %>%\n  select(tracker_join,yardline_100,touchdown,qtr,time) \n\ngc()\n\nTV_guide <- games %>%\n  select(gameId,season,week,homeTeamAbbr,visitorTeamAbbr) %>%\n  left_join(plays, by = \"gameId\") %>%\n  select(yardlineNumber,kickLength,kickReturnYardage,playDescription,gameId,playId,season,week,homeTeamAbbr,visitorTeamAbbr,specialTeamsPlayType,specialTeamsResult,kickerId,returnerId) %>%\n  mutate(tracker_join = paste0(gameId,\"_\",playId)) %>%\n  left_join(TV_guide_tracking_df, by = \"tracker_join\") %>%\n  mutate(plays_for = if_else(team == \"away\", visitorTeamAbbr,homeTeamAbbr)) %>%\n  left_join(players, by = \"nflId\") %>%\n  select(tracker_join,playDescription,gameId,playId,season,week,homeTeamAbbr,visitorTeamAbbr,specialTeamsPlayType,specialTeamsResult,kickerId,returnerId,nflId,jerseyNumber,plays_for,displayName,yardlineNumber,kickLength,kickReturnYardage) %>%\n  left_join(players, by = c(\"kickerId\" = \"nflId\")) %>%\n  select(yardlineNumber,kickLength,kickReturnYardage,tracker_join,playDescription,gameId,playId,season,week,homeTeamAbbr,visitorTeamAbbr,specialTeamsPlayType,specialTeamsResult,kickerId,returnerId,nflId,jerseyNumber,plays_for,displayName.x,displayName.y) %>%\n  rename(player = displayName.x) %>%\n  rename(kicker = displayName.y) %>%\n  mutate(returner_id = as.double(returnerId)) %>%\n  mutate(returner_id1 = if_else(is.na(returner_id),0,returner_id)) %>%\n  left_join(players, by = c(\"returner_id1\" = \"nflId\")) %>%\n  rename(returner = displayName) %>%\n  select(yardlineNumber,kickLength,kickReturnYardage,tracker_join,playDescription,gameId,playId,season,week,homeTeamAbbr,visitorTeamAbbr,specialTeamsPlayType,specialTeamsResult,kickerId,returner_id1,nflId,jerseyNumber,plays_for,player,kicker,returner) %>%\n  mutate(team_number = paste0(plays_for,\" \",jerseyNumber)) %>%\n  left_join(PFF_Scouting, by = \"tracker_join\") %>%\n  select(yardlineNumber,kickLength,kickReturnYardage,tracker_join,playDescription,gameId.x,playId.x,season,week,homeTeamAbbr,visitorTeamAbbr,specialTeamsPlayType,specialTeamsResult,kickerId,returner_id1,nflId,jerseyNumber,plays_for,player,kicker,returner,gunners,puntRushers,specialTeamsSafeties,vises) %>%\n  mutate(team_jersey = ifelse(jerseyNumber < 10, paste0(plays_for,\" 0\",jerseyNumber),paste0(plays_for,\" \",jerseyNumber))) %>%\n  mutate(role_gunners = str_detect(gunners,team_jersey)) %>%\n  mutate(role_gunners1 = if_else(role_gunners == \"TRUE\",1,0)) %>%\n  mutate(role_gunners2 = if_else(is.na(role_gunners),0,role_gunners1)) %>%\n  mutate(role_puntRushers = str_detect(puntRushers,team_jersey)) %>%\n  mutate(role_puntRushers1 = if_else(role_puntRushers == \"TRUE\",1,0)) %>%\n  mutate(role_puntRushers2 = if_else(is.na(role_puntRushers),0,role_puntRushers1)) %>%\n  mutate(role_specialTeamsSafeties = str_detect(specialTeamsSafeties,team_jersey)) %>%\n  mutate(role_specialTeamsSafeties1 = if_else(role_specialTeamsSafeties == \"TRUE\",1,0)) %>%\n  mutate(role_specialTeamsSafeties2 = if_else(is.na(role_specialTeamsSafeties),0,role_specialTeamsSafeties1)) %>%\n  mutate(role_vises = str_detect(vises,team_jersey)) %>%\n  mutate(role_vises1 = if_else(role_vises == \"TRUE\",1,0)) %>%\n  mutate(role_vises2 = if_else(is.na(role_vises),0,role_vises1)) %>%\n  mutate(Role1 = if_else(role_gunners2 == 1,\"Gunner\",\"\")) %>%\n  mutate(Role2 = if_else(role_puntRushers2 == 1,\"Punt Rusher\",Role1)) %>%\n  mutate(Role3 = if_else(role_specialTeamsSafeties2 == 1,\"Special Teams Safeties\",Role2)) %>%\n  mutate(Role4 = if_else(role_vises2 == 1,\"Vise\",Role3)) %>%\n  mutate(Role5 = if_else(kickerId == nflId, \"Kicker\",Role4)) %>%\n  mutate(Role6 = if_else(returner_id1 == nflId, \"Returner\",Role5)) %>%\n  mutate(Role = Role6) %>%\n  select(tracker_join,playDescription,gameId.x,playId.x,season,week,homeTeamAbbr,visitorTeamAbbr,specialTeamsPlayType,specialTeamsResult,kickerId,returner_id1,nflId,team_jersey,plays_for,player,Role,yardlineNumber,kickLength,kickReturnYardage) %>%\n  rename(gameId = gameId.x) %>%\n  rename(playId = playId.x) %>%\n  mutate(count = 1) %>%\n  filter(!is.na(nflId)) %>%\n  left_join(nflreadr_df, by = \"tracker_join\") %>%\n  mutate(game = paste0(visitorTeamAbbr,\" @ \",homeTeamAbbr)) %>%\n  mutate(specialTeamsResult1 = if_else(specialTeamsResult == \"Return\" & touchdown == 1, paste0(\"Returned \", kickReturnYardage),specialTeamsResult)) %>%\n  mutate(specialTeamsResult3 = if_else(touchdown == 1, paste0(specialTeamsResult1, \" for a TOUCHDOWN\"),specialTeamsResult1)) %>%\n  mutate(play_details = paste0(game,\"; \", specialTeamsPlayType, \"; \",specialTeamsResult3)) %>%\n  select(gameId,playId,play_details,nflId,player,team_jersey,Role,touchdown,week,qtr,time) %>%\n  mutate(Vision = if_else(Role == \"Returner\",\"Target\",\"\")) %>%\n  mutate(game_play = paste0(gameId,\"_\",playId)) %>%\n  mutate(game_play_player = paste0(gameId,\"_\",playId,\"_\",nflId)) %>%\n  select(game_play,game_play_player,gameId,playId,nflId,play_details,player,team_jersey,Role,touchdown,week,qtr,time)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:09:28.202215Z","iopub.execute_input":"2022-01-06T13:09:28.203953Z","iopub.status.idle":"2022-01-06T13:09:36.160918Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_4\"></a>\n#### User Inputs:\nIn the current form of PlayerTV, anyone can load the code and press play. In order for the user to change which play they are seeing, they need to go to the TV Guide and copy the gameId, playId and nflId of their chosen play and player. After that they highlight all the remaining code below and click 'run', wait a couple of minutes for the code to load and then they can watch the play.\n\nIt is a straightforward and practical approach meaning that anyone (with or without coding experience) can use PlayerTV. ","metadata":{}},{"cell_type":"code","source":"### USER INPUT\n\nPlayerTV_game <- 2018091000\nPlayerTV_play <- 2626\nPlayerTV <- 35527\n\n### END OF USER INPUT","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:10:32.885184Z","iopub.execute_input":"2022-01-06T13:10:32.886939Z","iopub.status.idle":"2022-01-06T13:10:32.905635Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_5\"></a>\n#### Filtered TV Guide\nWhile this function could be redundant depending on how you search the play of your choosing, this gives non-coding people easy access to see only the players in the play of their choosing. \nThe code is simply using three filter functions associated with the User Inputs.","metadata":{}},{"cell_type":"code","source":"TV_guide_filtered <- TV_guide %>%\n  filter(gameId == PlayerTV_game, playId == PlayerTV_play)\n\ngc()\n\nTV_guide_filtered","metadata":{"_kg_hide-output":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-06T13:10:35.017311Z","iopub.execute_input":"2022-01-06T13:10:35.019166Z","iopub.status.idle":"2022-01-06T13:10:35.892956Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_6\"></a>\n#### SaveFile Naming Function\nThis is another ease of use function. This means that you can easily identify the animation and come back to it at a later time. The savefile is the season, week, both teams, playId and the PlayerTV POV player name.\n\nThis means that it can be easily identified by a person or computer if looking to recall the file.","metadata":{}},{"cell_type":"code","source":"players_name <- players %>%\n  filter(nflId == PlayerTV) %>%\n  select(nflId,displayName)\n\nPlayerTV_name <- dplyr::pull(players_name,displayName)\n\nsavefile1 <- nflreadr::load_pbp(seasons = 2018) %>%\n  select(season,week,old_game_id,away_team,home_team,play_id) %>%\n  filter(old_game_id == PlayerTV_game) %>%\n  filter(play_id == PlayerTV_play) %>%\n  #mutate(game_play == )\n  mutate(filename = paste0(season,\"_\",week,\"_\",away_team,\"_\",home_team,\"_\",play_id,\"_\",PlayerTV_name))\n\n\nsavefile <- dplyr::pull(savefile1, filename)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:10:38.942551Z","iopub.execute_input":"2022-01-06T13:10:38.944345Z","iopub.status.idle":"2022-01-06T13:10:39.097743Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_7\"></a>\n#### Targeted Player Tracking Data:\nThis is more about getting the code ready for the rest of the players on the field. It renames the variables to include `_target`, so when I do a `left_join()`, it is easily identifiable whose data is whose.","metadata":{}},{"cell_type":"code","source":"gc()\n\ntracking_df <- read_csv(\"../input/nfl-big-data-bowl-2022/tracking2018.csv\") %>%\n  mutate(game_play = paste0(gameId,\"_\",playId)) %>%\n  filter(gameId == PlayerTV_game, playId == PlayerTV_play)\n\ntracking_df_target <- tracking_df %>%\n  filter(nflId == PlayerTV) %>%\n  select(frameId,x,y,dir,o,team) %>%\n  rename(x_target = x) %>%\n  rename(y_target = y) %>%\n  rename(dir_target = dir) %>%\n  rename(o_target = o) %>%\n  rename(team_target = team)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:10:45.607479Z","iopub.execute_input":"2022-01-06T13:10:45.609737Z","iopub.status.idle":"2022-01-06T13:11:41.857619Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_8\"></a>\n\n#### Other 21 Players Tracking Data:\nThis is where it can get a bit complex, but I'll try my best to simplify it. There are three essential parts to my animation: Height, Width and POV angle. \n\nI also did some fun things such as change the strength of the colour (`alpha`) depending on the closeness to the target player.","metadata":{}},{"cell_type":"code","source":"tracking_df_target_distance <- tracking_df %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  ### Pythagoras Theorem\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  left_join(players, by = \"nflId\") %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  filter(nflId != PlayerTV) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  #select(nflId,frameId,x_difference_real,y_difference_real,xpos_ypos_deg,xpos_yneg_deg,xneg_yneg_deg,xneg_ypos_deg) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  #mutate(colour_graph = if_else(team == team_target, \"blue\",\"red\")) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(width_yards = 0.5) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  mutate(x1 = observer_angle - width_perception/2) %>%\n  mutate(x2 = observer_angle + width_perception/2) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,grass_gap,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,height_inches,0)) %>%\n  mutate(colour_graph = if_else(team == team_target, \"blue\",\"red\")) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1 - yards_away_decimals) %>%\n  mutate(height_perception1 = if_else(nflId == PlayerTV, -1,height_perception)) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"players\") %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) ","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:28.896083Z","iopub.execute_input":"2022-01-06T13:12:28.898111Z","iopub.status.idle":"2022-01-06T13:12:29.125632Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##### Data Collection \nTo start, I took the default tracking dataframe (from `Tracking2018.csv`) and joined that data with the Targeted Player Tracking Data (`tracking_df_target_distance`).\n","metadata":{}},{"cell_type":"markdown","source":"##### Creating the Y-Axis.\n\nI focused on height first. Height in our animation is more complex than just the player's height as it is all about an object perceived height from the Target Player. More information on perceived height can be found [here](https://www.sciencedirect.com/science/article/pii/S0042698906002112). \n\n![Image of Perceived Height (Inches) over Distance (Yards)](https://raw.githubusercontent.com/rogers1000/bdb2022/main/Perceived_height_inches_over_distance_yards.png)\n\nIn short, the formula is `Tan-1(height_yards/yards_away)`.\n\nI started with `yards_away` as that is the more straightforward process. To do this, I used Pythagoras Theorem.\nPythagoras Theorem uses one side of a triangle squared times the other side squared before square rooting the answer to work out the Hypotenuse length. This is possible because everyone is connected to the coordinates `x` and `y` meaning you can make a triangle to everyone.\n\nNow we calcuated the yards_away; we needed to get the player's height. As distance away is in yards, the height must also be in yards. I did this earlier in my Competition Data Loading (and Cleansing); I cleaned all the height data as it was a mix of inches combined with Feet/Inches, converting it all into inches. Because my distance away variable is in yards, I had to convert all height units into yards. That is a simple process of dividing inches by 36.\n\nRstudio by default puts angles in Radian, so I had to convert that into degrees (the conversion formula being 1Radian = 180/Pi). That gives us our height perception of the player in inches. \n\nHowever, we don't just want the perceived height because otherwise, that would give us a bar chart for perceived height. We need to work out the whereabouts in relation to the field. I called this 'grass_gap' because it is the amount of grass you see in front of you before seeing the player. It is a simple formula of height in inches minus perceived height.\n\nAs the players are rectangles, they need two data points for each axis. The Y-axis highest measurement is `height_inches` and lowest measurement is `grass_gap`.","metadata":{}},{"cell_type":"markdown","source":"##### Creating the X-Axis.\n\nThe width formula is almost identical to the height formula. The main difference is that there is no data for players' width, so I decided that everyone would be around 0.5 yards wide (18 inches) shoulder to shoulder as a simple approximation.\n\nWith perceived height and width sorted, I only needed to figure out where to place the player on the X-Axis as the size was sorted. I called the formula `Observer Angle`. \n\n`Observer Angle` uses `Tan-1` to work out the angle of the player relative to the chosen camera angle (PlayerTV POV). Depending on the other player's location on the `x` or `y` coordinates selected what `Tan-1` formula to use. This gives us the player angle on a 360-degree compass. I called this `compass360` as it puts everyone on the field on a 360-degree point.\n\nThe final step was to change the focus on the compass from point West to the PlayerTV's `Orientation` (datapoint from `tracking2018.csv`) to see if they are in the player's eye-line or not. To do this I did `Orientation` of Chosen Player minus `compass360` and I called this `Observer Angle`. \n\nNow that I had the angle from the eye line of the chosen player, I could plot the two data points on the x-axis to make the rectangle. The two points named `x1` and `x2`. `x1` is equal `Observer Angle` minus `perceived width` divided by 2 and `x2` is equal `Observer Angle` plus `perceived width` divided by 2. \n\nAnyone that is not within -90 degrees or 90 degrees is not shown in the graph as they are not visible.","metadata":{}},{"cell_type":"markdown","source":"<a id=\"code_review_9\"></a>\n#### Building the Stadium\n\nI tried to keep it simple when building the stadium and only added the goalposts, the field, and the stadium's background. There are no white lines to indicate the field at this time, but it would be something I would like to develop in the future. \n\nA key point to understand is that merging the dataframes for the graph requires all the column names to be identical. That means that the `stadium_field` and `stadium_background` have columns that are not required. ","metadata":{}},{"cell_type":"markdown","source":"##### Stadium Field\n\nThe only datapoint that is required is the colour of the pitch (\"forestgreen\"). The field is designed to cover the whole vision across the the `x-axis` and the height is designed to cover the majority of the `y-axis`. Any future work to build a proper stadium with stands, lines, fans or anything else would be placed on top of the Field and therefore cover the grass from view.","metadata":{}},{"cell_type":"code","source":"nFrames <- max(tracking_df$frameId)\n\nstadium_field <- data.frame(frameId = c(1:nFrames),height = 3.33,x =0, y = 53.3/2, o = \"NA\", width = 2.13,width_perception = \"unknown\", colour_graph = \"forestgreen\", alpha_value = 1) %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = 5000) %>%\n  mutate(perception_height_rad = atan(height/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_inches = height*36) %>%\n  mutate(x1 = -90, x2 = 90, y1 = 0, y2 = height_inches) %>%\n  mutate(observer_angle = 0) %>%\n  mutate(width_perception = 0) %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(description = \"stadium_field\") %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) ","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:34.695886Z","iopub.execute_input":"2022-01-06T13:12:34.697681Z","iopub.status.idle":"2022-01-06T13:12:34.774974Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##### Stadium Background\nWhat is not covered by the `Stadium_Field` will be covered by the `Stadium_Background`. I elected for a grey background (hex code: \"#808080\") that covers the background behind the goalposts to make them more clear. The background in theory could be adapted into a stand at a future date. ","metadata":{}},{"cell_type":"code","source":"stadium_background <- data.frame(frameId = c(1:nFrames), x = 125, y = 53.3/2, height_inches = 0,height_perception = 0, width_perception = 0, yards_away = 10000, colour_graph = \"#808080\", alpha_value = 1, o_target = 0) %>%\n  mutate(x1 = -90, x2 = 90, y1 = 0, y2 = 150) %>%\n  mutate(observer_angle = 0) %>%\n  mutate(width_perception = 0) %>%\n  mutate(description = \"stadium_background\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description)\n\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:36.595942Z","iopub.execute_input":"2022-01-06T13:12:36.597674Z","iopub.status.idle":"2022-01-06T13:12:36.644007Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##### Goalpost - Groundpost. \n\nThere are two Groundposts in the stadium, one for either goalpost. The only difference between the two is the `x` coordinate. According to the NFL [rulebook](http://static.nfl.com/static/content/public/image/rulebook/pdfs/4_Rule1_The_Field.pdf), the height of each groundpost is 120inches (3.33 yards or 10 feet) high with a width of 5 inches (0.14 yards or 0.41 foot) wide.\n\nThe formulas for working out perceived height and width are the same as the players - as is the Observer Angle. ","metadata":{}},{"cell_type":"code","source":"goalpost_groundpost_x0 <- data.frame(frameId = c(1:nFrames),x = 0, y = 53.3/2, height_inches = 120, height_yards = 3.33, width_inches = 5, width_yards = 0.14,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  mutate(x1 = observer_angle - width_perception/2) %>%\n  mutate(x2 = observer_angle + width_perception/2) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,grass_gap,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,height_inches,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_groundpost_x0\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) \n\n\ngoalpost_groundpost_x120 <- data.frame(frameId = c(1:nFrames),x = 120, y = 53.3/2, height_inches = 120, height_yards = 3.33, width_inches = 5, width_yards = 0.14,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  mutate(x1 = observer_angle - width_perception/2) %>%\n  mutate(x2 = observer_angle + width_perception/2) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,grass_gap,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,height_inches,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_groundpost_x120\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) ","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:37.413899Z","iopub.execute_input":"2022-01-06T13:12:37.415750Z","iopub.status.idle":"2022-01-06T13:12:37.670095Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##### Goalpost - Crossbar.\n\nLike the groundposts, there are two Crossbars are either eide of the stadium - the only difference being the `x` coordinate. According to NFL [rulebook](http://static.nfl.com/static/content/public/image/rulebook/pdfs/4_Rule1_The_Field.pdf), the height of the bar is 5 inches and the width of the bar is 222.12 inches (6.17 yards or 18 feet 6 inches) wide.\n\nThe formulas for working out `perceived width` is the same as the players - as is the `Observer Angle`. However, because the crossbar is 10 feet in the air, the `perceived height` calculation differs. Because we already have the max height of the Crossbar from the groundpost, we can use the Groundpost `y2` as the Crossbar `y2` and then minus the `perceived height` to get our Crossbar `y1`.","metadata":{}},{"cell_type":"code","source":"\ngoalpost_groundpost_x120_data <- goalpost_groundpost_x120 %>%\n  select(frameId,gp_x120_y2 = y2, gp_x120_observer_angle = observer_angle) \n\n### cross bar height = 5inches / 0.14 yards\n\ngoalpost_crossbar_x120 <- data.frame(frameId = c(1:nFrames),x = 120, y = 53.3/2, height_inches = 5, height_yards = 0.14, width_inches = 222.12, width_yards = 6.17,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  left_join(goalpost_groundpost_x120_data, by = \"frameId\") %>%\n  mutate(x1 = gp_x120_observer_angle - width_perception/2) %>%\n  mutate(x2 = gp_x120_observer_angle + width_perception/2) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,gp_x120_y2-height_perception,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,gp_x120_y2,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_crossbar_x120\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) \n\ngoalpost_groundpost_x0_data <- goalpost_groundpost_x0 %>%\n  select(frameId,gp_x0_y2 = y2, gp_x0_y1 = y1, gp_x0_observer_angle = observer_angle) \n\n### cross bar height = 5inches / 0.14 yards\n\ngoalpost_crossbar_x0 <- data.frame(frameId = c(1:nFrames),x = 0, y = 53.3/2, height_inches = 5, height_yards = 0.14, width_inches = 222.12, width_yards = 6.17,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  left_join(goalpost_groundpost_x0_data, by = \"frameId\") %>%\n  mutate(x1 = gp_x0_observer_angle - width_perception/2) %>%\n  mutate(x2 = gp_x0_observer_angle + width_perception/2) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,gp_x0_y2-height_perception,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,gp_x0_y2,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_crossbar_x0\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) ","metadata":{"_kg_hide-output":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:38.639737Z","iopub.execute_input":"2022-01-06T13:12:38.641500Z","iopub.status.idle":"2022-01-06T13:12:38.927624Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##### Goalpost - Uprights.\n\nUnlike the other bars, there are 4 uprights on the field (2 on either goalpost) - this means that not only do the `x` coordinates change, but so do the `y` coordinates.  \n\nHowever, they mirror meaning that there are 2 uprights on the same `x` coordinate and 2 uprights on the same `y` coordinate. According to NFL [rulebook](http://static.nfl.com/static/content/public/image/rulebook/pdfs/4_Rule1_The_Field.pdf), the height of the bar is 360 inches (12 foot or 10 yards) tall and the width of the bar is 4 inches (0.11 yards or 0.3 feet) wide.\n\nFor all the uprights, I took the data from the crossbar's `y2` and put that as the upright `y1`, while for the `y2` I took that data and then added the `perceived height`.\n\nThe difference for the right upright and left upright is the x-axis. For the right upright, I used `x1` as a base for the `x1` and for the `x2` I took the crossbar `x1` and added the `perceived width` of the upright. \n\nFor the left upright, I took the crossbar `x2` and used that for the upright  `x2` with the upright `x1` being the crossbar `x2` minus the upright's `perceived width`.\n\nThe only difference between the 2 goalposts as mentioned are the `x` coordinates. ","metadata":{}},{"cell_type":"code","source":"\ngoalpost_crossbar_x0_data <- goalpost_crossbar_x0 %>%\n  select(frameId,y2_crossbar_x0 = y2,x1_crossbar_x0 = x1, x2_crossbar_x0 = x2) \n\ngoalpost_right_upright_x0 <- data.frame(frameId = c(1:nFrames),x = 0, y = (53.3/2) + (6.17/2), height_inches = 360, height_yards = 10, width_inches = 4, width_yards = 0.11,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  left_join(goalpost_crossbar_x0_data, by = \"frameId\") %>%\n  mutate(x1 = x1_crossbar_x0) %>%\n  mutate(x2 = x1_crossbar_x0 + width_perception) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x0,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x0 + height_perception,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_right_upright_x0\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) \n\ngoalpost_left_upright_x0 <- data.frame(frameId = c(1:nFrames),x = 0, y = (53.3/2) + (6.17/2), height_inches = 360, height_yards = 10, width_inches = 4, width_yards = 0.11,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  left_join(goalpost_crossbar_x0_data, by = \"frameId\") %>%\n  mutate(x1 = x2_crossbar_x0 - width_perception) %>%\n  mutate(x2 = x2_crossbar_x0) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x0,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x0 + height_perception,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_left_upright_x0\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) \n\n\ngoalpost_crossbar_x120_data <- goalpost_crossbar_x120 %>%\n  select(frameId,y2_crossbar_x120 = y2,x1_crossbar_x120 = x1, x2_crossbar_x120 = x2) \n\n\ngoalpost_right_upright_x120 <- data.frame(frameId = c(1:nFrames),x = 120, y = (53.3/2) + (6.17/2), height_inches = 360, height_yards = 10, width_inches = 4, width_yards = 0.11,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  left_join(goalpost_crossbar_x120_data, by = \"frameId\") %>%\n  mutate(x1 = x1_crossbar_x120) %>%\n  mutate(x2 = x1_crossbar_x120 + width_perception) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x120,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x120 + height_perception,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_right_upright_x120\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description)\n\ngoalpost_left_upright_x120 <- data.frame(frameId = c(1:nFrames),x = 120, y = (53.3/2) + (6.17/2), height_inches = 360, height_yards = 10, width_inches = 4, width_yards = 0.11,colour_graph = \"#E8DE35\") %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  mutate(a_squared = (x - x_target)*(x - x_target)) %>%\n  mutate(b_squared = (y - y_target)*(y - y_target)) %>%\n  mutate(X_difference_positive = sqrt(a_squared)) %>%\n  mutate(Y_difference_positive = sqrt(b_squared)) %>%\n  mutate(x_difference_real = x - x_target) %>%\n  mutate(y_difference_real = y - y_target) %>%\n  mutate(yards_away = sqrt(a_squared + b_squared)) %>%\n  ### People Height Perception\n  mutate(perception_height_rad = atan(height_yards/yards_away)) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(height_perception = perception_height_rad * 180/3.14) %>%\n  mutate(grass_gap = height_inches - height_perception) %>%\n  mutate(Observer_Angle_xpos_ypos_rad = ifelse(x_difference_real >= 0 & y_difference_real >= 0, atan((x - x_target)/(y - y_target)),1000)) %>%\n  mutate(xpos_ypos_deg = Observer_Angle_xpos_ypos_rad*180/3.14) %>%\n  mutate(Observer_Angle_xpos_yneg_rad = ifelse(x_difference_real >= 0 & y_difference_real <= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xpos_yneg_deg = Observer_Angle_xpos_yneg_rad*180/3.14 + 90) %>%\n  mutate(Observer_Angle_xneg_yneg_rad = ifelse(x_difference_real <= 0 & y_difference_real <= 0, atan((x_target - x)/(y_target - y)),1000)) %>%\n  mutate(xneg_yneg_deg = Observer_Angle_xneg_yneg_rad*180/3.141593 + 180) %>%\n  mutate(Observer_Angle_xneg_ypos_rad = ifelse(x_difference_real <= 0 & y_difference_real >= 0, atan((y_target - y)/(x - x_target)),1000)) %>%\n  mutate(xneg_ypos_deg = Observer_Angle_xneg_ypos_rad*180/3.141593+270) %>%\n  mutate(Quad1 = ifelse(xpos_ypos_deg < 360, xpos_ypos_deg,xpos_yneg_deg)) %>%\n  mutate(Quad2 = ifelse(Quad1 < 360, Quad1, xneg_yneg_deg)) %>%\n  mutate(compass360 = ifelse(Quad2 < 360, Quad2, xneg_ypos_deg)) %>%\n  mutate(observer_angle = o_target - compass360) %>%\n  mutate(perception_width_rad = atan(width_yards/yards_away)) %>%\n  mutate(width_perception = perception_width_rad * 180/3.14) %>%\n  left_join(goalpost_crossbar_x120_data, by = \"frameId\") %>%\n  mutate(x1 = x2_crossbar_x120 - width_perception) %>%\n  mutate(x2 = x2_crossbar_x120) %>%\n  mutate(y1 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x120,-1)) %>%\n  mutate(y2 = if_else(observer_angle > -90 & observer_angle < 90,y2_crossbar_x120 + height_perception,0)) %>%\n  arrange(yards_away) %>%\n  mutate(yards_away_decimals = yards_away / 100) %>%\n  mutate(alpha_value = 1) %>%\n  arrange(frameId, -yards_away) %>%\n  mutate(description = \"goalpost_left_upright_x120\") %>%\n  select(frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description) %>%\n  filter(!is.na(height_perception)) %>%\n  mutate(nflId = 0) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1,x2,colour_graph,alpha_value,o_target,description)\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:39.681155Z","iopub.execute_input":"2022-01-06T13:12:39.683141Z","iopub.status.idle":"2022-01-06T13:12:40.237486Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_10\"></a>\n#### Merging the Data\n\nAs mentioned above, I needed all the column names to be identical to merge the data using `rbind()` function. I also made some slight adjustments to my dataframe, such as changing `colour` to `fill` and creating a new `colour` (outline) variable. I also wanted to ensure that the players weren't being overlapped by other parts of the stadium so made a priority column for later use. \nThe `arrange()` function means that the background is the background and the foreground is the foreground. \nI, also, made adjustments to the `x-axis` so that players who were on the verge of being out of sight, but were still just insight were being seen. ","metadata":{}},{"cell_type":"code","source":"graph_data1 <- rbind(tracking_df_target_distance,stadium_field,stadium_background,goalpost_groundpost_x0,goalpost_crossbar_x0,goalpost_right_upright_x0,goalpost_left_upright_x0,goalpost_groundpost_x120,goalpost_crossbar_x120,goalpost_right_upright_x120,goalpost_left_upright_x120) %>%\n  rename(fill_graph = colour_graph) %>%\n  mutate(colour_graph = ifelse(fill_graph == \"#E8DE35\" | fill_graph == \"white\",fill_graph,\"black\")) %>%\n  mutate(player_priority = ifelse(description == \"players\",1,0)) %>%\n  arrange(frameId, player_priority,-yards_away) %>%\n  mutate(x1_real = ifelse(x1 < -90 & x2 > -90, -89.9,x1)) %>%\n  mutate(x2_real = ifelse(x2 > 90 & x1 < 90, 89.9, x2)) %>%\n  select(nflId,frameId,height_perception,width_perception,yards_away,observer_angle,y1,y2,x1_real,x2_real,colour_graph,alpha_value,o_target,description,player_priority,fill_graph) %>%\n  rename(x1 = x1_real) %>%\n  rename(x2 = x2_real) ","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:40.660679Z","iopub.execute_input":"2022-01-06T13:12:40.662402Z","iopub.status.idle":"2022-01-06T13:12:40.715734Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_11\"></a>\n#### Adding Fun Stats\nI thought I would add some fun stats to the animation such as the chosen Player's name, speed (mph) they are running at and their direction of travel (with 0 directly facing towards the opposition endzone). The speed has been converted to mph (from the original yards) for greater understanding. \n\nI joined this table to the merged dataframe so that it all ran smoothly. ","metadata":{}},{"cell_type":"code","source":"returner <- TV_guide_filtered %>%\n  filter(Role == \"Returner\") %>%\n  select(game_play,returner_id = nflId) \n\ntracking_df_target_extrainfo <- tracking_df %>%\n  filter(nflId == PlayerTV) %>%\n  left_join(returner, by = \"game_play\") %>%\n  select(frameId,speed_yards_per_second = s,displayName,returner_id,playDirection) %>%\n  mutate(speed_mph = (speed_yards_per_second*2.045)) %>%\n  mutate(speed_mph_round = round(speed_yards_per_second*2.045,digits = 2)) %>%\n  mutate(colour = \"white\") \n\n\none_table <- graph_data1 %>%\n  left_join(tracking_df_target_extrainfo, by = \"frameId\")  %>%\n  mutate(fill_graph1 = ifelse(returner_id == nflId, \"white\",fill_graph)) %>%\n  mutate(fill_graph = ifelse(is.na(returner_id),fill_graph,fill_graph1)) %>%\n  mutate(x1 = x1*-1) %>%\n  mutate(x2 = x2*-1) %>%\n  mutate(facing = ifelse(playDirection == \"right\", o_target - 270,o_target - 90)) %>%\n  mutate(facing = round(facing,2))","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:46.428000Z","iopub.execute_input":"2022-01-06T13:12:46.429782Z","iopub.status.idle":"2022-01-06T13:12:46.508925Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_radar\"></a>\n#### Radar (aka the Dots)\n\nIn addition to the fun stats, I added a radar to PlayerTV. The radar gives the user an idea of where the player is on the field relative to the sideline, the other players and the ball. \n\nThese (like the fun stats) are add-ons to improve the product for the user. \n\nWhen looking in detail at the code, we first of all have to decide the scale of the pitch. The dots are a complementary add-on and therefore needs to be small enough not to be the main focus, but big enough that anyone can see it. I decided to have it as a width of 22.5 on my scale of -90 to 90.\n\nTo scale the coordinate data down, I needed to find the ratio for the side length because I decided on the width of the pitch, not the height. To get the ratio answer, I did 53.3 (width of a field) divided by 22.5 (width on PlayerTV). I, then, worked out the width for the `y_axis` by dividing 120 (size of field) by the ratio.\n\nNow, I have the ratios, I converted the dots (players) to the scale. During the debugging phase, I realised that the radar (dots) are reversed so I use the trick of `160/3 - y` to reverse the axis. I also put columns for `colours` and `fills` for making the dots look fancy (same as the PlayerTV colour scheme). \n\nNow that the dots are scaled to the size needed, they need to be placed in the correct location, to do this I moved the `y_axis` using `y_axis - 90`. \n\nTo help with PlayerTV's chosen player visability, I recreated the same process but filtered to only the chosen player. I, then, added a colour column (white) so it could be used for the outline. \n\nI, also, needed to do this with the ball so people can see the ball on the radar. \n","metadata":{}},{"cell_type":"code","source":"\ndots_x_ratio <- 53.3/22.5\ndots_ratio <- 120/dots_x_ratio\n\ndots_coordinates_players <- tracking_df %>%\n  #select(frameId,x,y,team) %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  left_join(returner, by = \"game_play\") %>%\n  #mutate(x = ifelse(playDirection == \"right\", 120-x, x)) %>%\n  mutate(y = ifelse(playDirection == \"right\", 160/3 - y, y)) %>%\n  mutate(y = ifelse(playDirection == \"left\", 160/3 - y, y)) %>%\n  mutate(colour_graph = if_else(team == team_target, \"blue\",\"red\")) %>%\n  mutate(colour_graph1 = if_else(nflId == returner_id, \"white\",colour_graph)) %>%\n  mutate(colour_graph = ifelse(is.na(returner_id),colour_graph,colour_graph1)) %>%\n  mutate(fill_graph = ifelse(nflId == PlayerTV,\"white\",colour_graph)) %>%\n  mutate(y_left = y - 90) %>%\n  mutate(x_ratio = x/dots_x_ratio) %>%\n  mutate(y_ratio = y/dots_x_ratio) %>%\n  mutate(y_ratio_left = y_ratio - 90) %>%\n  filter(team != \"football\")\n\ndots_coordinates_playerTV <- tracking_df %>%\n  #select(frameId,x,y,team) %>%\n  left_join(tracking_df_target, by = \"frameId\") %>%\n  left_join(returner, by = \"game_play\") %>%\n  #mutate(x = ifelse(playDirection == \"right\", 120-x, x)) %>%\n  mutate(y = ifelse(playDirection == \"right\", 160/3 - y, y)) %>%\n  mutate(y = ifelse(playDirection == \"left\", 160/3 - y, y)) %>%\n  mutate(colour_graph = if_else(team == team_target, \"blue\",\"red\")) %>%\n  mutate(colour_graph = if_else(nflId == returner_id, \"white\",colour_graph)) %>%\n  mutate(fill_graph = ifelse(nflId == PlayerTV,\"white\",colour_graph)) %>%\n  mutate(y_left = y - 90) %>%\n  mutate(x_ratio = x/dots_x_ratio) %>%\n  mutate(y_ratio = y/dots_x_ratio) %>%\n  mutate(y_ratio_left = y_ratio - 90) %>%\n  filter(nflId == PlayerTV)\n\n\n#view(dots_coordinates_playerTV)\n\ndots_coordinates_football <- tracking_df %>%\n  filter(team == \"football\") %>%\n  mutate(y = ifelse(playDirection == \"right\", 160/3 - y, y)) %>%\n  mutate(y = ifelse(playDirection == \"left\", 160/3 - y, y)) %>%\n  mutate(x_ratio = x/dots_x_ratio) %>%\n  mutate(y_ratio = y/dots_x_ratio) %>%\n  mutate(y_ratio_left = y_ratio - 90) ","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:47.262492Z","iopub.execute_input":"2022-01-06T13:12:47.264441Z","iopub.status.idle":"2022-01-06T13:12:47.440793Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_12\"></a>\n#### Screenshot Function\n\nIn order to add next level analysis, I thought that giving coaches/scouts/analysts the opportunity to pause the animation at a given moment would be a good thing to do. This does all the functions that the animation does but for a still image. It does not include the add-ons to provide a full-screen view.\n\nI will describe all the parts that make the screenshot special in the Animation section of the Code Review. ","metadata":{}},{"cell_type":"code","source":"### USER INPUT\n\nscreenshot_frame <- 65\n\n### END OF USER INPUT\n\none_table_screenshot <- one_table %>%\n  filter(frameId == screenshot_frame) %>%\n  filter(y2 > 0) \n\none_table_screenshot %>%\n  ggplot(aes(x = observer_angle, y = height_perception, col = colour_graph, fill = colour_graph, alpha = alpha_value)) +\n  geom_rect(data = one_table_screenshot, mapping = aes(xmin = x1, xmax = x2, ymin = y1, ymax = y2), fill = one_table_screenshot$fill_graph, colour = one_table_screenshot$colour_graph,alpha = one_table_screenshot$alpha_value) +\n  scale_colour_identity() +\n  scale_fill_identity() +\n  xlim(-90,90) +\n  theme_void() +\n  theme(axis.title.x=element_blank(),\n        axis.text.x=element_blank(),\n        axis.ticks.x=element_blank(),\n        axis.title.y=element_blank(),\n        axis.text.y=element_blank(),\n        axis.ticks.y=element_blank(),\n        legend.key = element_blank(),\n        legend.text = element_blank(),\n        legend.position = \"none\") +\n  labs(subtitle = \"Frame: {frame_time}\",\n       caption = \"ZacRogers | ZacRogers.co.uk | @TheRogersUK\")\n\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:48.022230Z","iopub.execute_input":"2022-01-06T13:12:48.024372Z","iopub.status.idle":"2022-01-06T13:12:48.499101Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"code_review_13\"></a>\n#### Animation Code Function\nThis is where the magic happens. \n\nI use `geom_rect()` to make a rectangle using the `one_table` data which is the dataframe which contains all the players and the stadium data (`height perception` and `Observer Angle`). That is what makes the players and stadium appear.\n\nI, then, created a small white box in the bottom right of the plot which will have the fun stats on it. These fun stats are added by using `geom_label()` and a `paste0()` function to add a description before the data. \n\nTo place the radar into PlayerTV, I created an outline of the field using `geom_rect()` using the ratio from above. I, also, added the endzones to give extra clarity about if the player was in the endzone or not. I decided not to fill the field in green so that it doesn't take away from the potential players behind it. I, then, added the players (as dots) using `geom_point()` and added an extra circle for the chosen PlayerTV player so that it was more obvious where the player is you want to focus on is on the field.\n\nIn order to make the `colours` and `fill` work properly and inline with the colours set, I use `scale_colour_identity()` and `scale_fill_identity()` before doing the final things to make the plot look less like a standard graph and more like a camera.\n\nTo do this I used the `theme_void()` theme which removes almost everything and then removed anything associated with the axis scale (title, ticks and text). After using `scale_colour_identity()`, a legend appears but I can remove that using `legend.position = \"none\"`). \n\nThe final parts before making it a live plot is adding a subtitle which says which frame you are watching (to help provide the frame for the screenshot function) and a caption to say I built it, where to contact me and the dataset. \n\nNow for the real magic, how to animate the graph. I used `transition_time()` function along with the time series data point (frameId) and put `ease_aes('linear')` to say that time difference is linear.\n\nThe final part is the `anim_save` which lets me customise how I want to save the live plot. ","metadata":{}},{"cell_type":"code","source":"PlayerTV_animation <- ggplot() +\n  xlim(-90,90) +\n  geom_rect(data = one_table, mapping = aes(xmin = x1, xmax = x2, ymin = y1, ymax = y2), fill = one_table$fill_graph, colour = one_table$colour_graph, alpha= one_table$alpha_value) +\n  geom_rect(aes(xmin = 50, xmax = 90, ymin = 0, ymax = 25, fill = \"white\", col = \"black\")) +\n  geom_label(data = one_table, mapping = aes(x = 70, y = 20, label = paste0(\"PlayerTV: \",displayName),col = \"black\", fill = \"white\"), label.size = NA) +\n  geom_label(data = one_table, mapping = aes(x = 70, y = 5, label = paste0(\"Facing: \",facing),col = \"black\", fill = \"white\"), label.size = NA) +\n  geom_label(data = one_table, mapping = aes(x = 70, y = 12.5, label = paste0(\"Speed: \",speed_mph_round, \" mph\"),col = \"black\",fill = \"white\"), label.size = NA) +\n  ### RADAR CHART\n  # Field\n  geom_rect(aes(xmin = -90, xmax = -67.5, ymin = 0, ymax = dots_ratio), colour = \"white\", alpha = 0.0) +\n  geom_rect(aes(xmin = -90, xmax = -67.5, ymin = 0+(dots_ratio/10), ymax = dots_ratio-(dots_ratio/10)), colour = \"white\", alpha = 0.0) +\n  # Players\n  geom_point(data = dots_coordinates_players, mapping = aes(x = y_ratio_left, y = x_ratio, col = colour_graph)) +\n  # Ball\n  geom_point(data = dots_coordinates_football, mapping = aes(x = y_ratio_left, y = x_ratio, col = \"black\"), alpha = 1) +\n  # PlayerTV\n  geom_point(data = dots_coordinates_playerTV, mapping = aes(x = y_ratio_left, y = x_ratio, col = fill_graph), size = 2, shape = 1, alpha = 1) +\n  scale_colour_identity() +\n  scale_fill_identity() +\n  theme_void() +\n  #labs(title = plot_title) +\n  theme(axis.title.x=element_blank(),\n        axis.text.x=element_blank(),\n        axis.ticks.x=element_blank(),\n        axis.title.y=element_blank(),\n        axis.text.y=element_blank(),\n        axis.ticks.y=element_blank(),\n        legend.position = \"none\") +\n  labs(subtitle = \"Frame: {frame_time}\",\n       caption = \"ZacRogers | ZacRogers.co.uk | Big Data Bowl 2022\") +\n  transition_time(frameId)  +\n  ease_aes('linear') + \n  NULL \n\nanim_save(paste0(savefile,\".gif\"),\n          animate(PlayerTV_animation, width = 720, height = 440,\n                  fps = 10, nframe = nFrames))\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-01-06T13:12:50.234107Z","iopub.execute_input":"2022-01-06T13:12:50.236120Z","iopub.status.idle":"2022-01-06T13:14:37.811566Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id=\"section-10\"></a>\n## Final Words:\n\nThis rounds off an amazing year of coding and analysis for me; a big thank you to [Ben Baldwin](https://twitter.com/benbbaldwin), [Tan Ho](https://twitter.com/_TanHo), [Tej Seth](https://twitter.com/tejfbanalytics), [Arjun Menon](https://twitter.com/arjunmenon100) and [Anthony Reinhard](https://twitter.com/reinhurdler) for all the inspiration and help this year. I wouldn't have imagined I could produce anything like this of my own doing even six months ago. \n\nYou can find more of my analysis at [BlueChipScouting.com](https://www.bluechipscouting.com/nfl-articles-1?author=5ecbebb89996b06f4fbad2f8) when work is less busy. You can also check my portfolio out at: https://www.zacrogers.co.uk/portfolio, LinkedIn at: https://www.linkedin.com/in/zacrogers/ and Twitter at https://twitter.com/TheRogersUK. \n\nI have written this up as a Twitter [thread](https://twitter.com/TheRogersUK/status/1476617379455062020?s=20) and will continue to add footage from PlayerTV on request.","metadata":{}},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}