Afischbacher / Nhl.Api

The Official Unofficial .NET NHL API
MIT License
25 stars 3 forks source link

Enhance player statistics #42

Closed mlynarp closed 4 months ago

mlynarp commented 8 months ago

It would be cool to enhance player statistics to include faceoffs won (FOW) as it is used quite often in Yahoo Fantasy Leagues.

Currently as a workaround I need to iterate over all games and check the boxscore and calculate on my own.

Afischbacher commented 8 months ago

Hey @mlynarp

Thanks for giving me this great suggestion! I will try and put it in the back-log for myself and work on this when I have a chance

Afischbacher commented 6 months ago

Hey @mlynarp

After releasing v3.0.0, there is a way to enable you to have the ability to retrieve the face off winning percentage statistic per player.

Within the C# class model PlayerCareerTotals there is the ability to give you access to the players regular season and playoff season face off winning percentage.

You can access this endpoint via Task<PlayerProfile>GetPlayerInformationAsync(PlayerEnum player) method, here is the documentation, https://github.com/Afischbacher/Nhl.Api?tab=readme-ov-file#getplayerinformationasyncplayer-method

Here is a small sample of the JSON returned with the specific information you are looking for with Connor McDavid's profile

    "careerTotals": {
        "regularSeason": {
            "faceoffWinningPctg": 0.4744
        },
        "playoffs": {
            "faceoffWinningPctg": 0.4635
        }
    },

Let me know if this helps, I can then close this issue :)

Thanks

mlynarp commented 6 months ago

Hey @Afischbacher ,

actually my goal was to collect statistics about the faceoffs won not the percentage. So for example I need to know that McDavid has collected throughout the 2023/2024 season 187 faceoffs he won as this category is used in Yahoo Fantasy league.

Thanks for future improvements.

Afischbacher commented 6 months ago

Hey @mlynarp

Now I understand what you mean, and I have built something to accommodate this and other types of events (blocked shots, hits) so you can effectively count by the player and season, this should be live in 3.1.0

Thanks :)

mlynarp commented 6 months ago

Hei @Afischbacher,

I checked the source code and it seems you will have to integrate https://api.nhle.com/stats/rest/en endpoint for statistics data which is not part of the source code.

Do you need help with these or when do you think you will have API which will return statistics data per player per season?

Just to describe you in more detail what I need:

I have an app which loads all active players within active season (info about season and season dates would be great as well) and then loads statistics per each player. For skater it is GP, G, A, P, +-, PIM, Shots, FOW (int number), Hits, Blocks, and for goalies its GP, W, GAA, SV, SV%. Then I do some analysis and data visualization.

Therefore I can help you with the code if its too big for now. Currently I didn't find the way how to achieve it with current version.

Btw. I studied the API reference here https://github.com/Zmalski/NHL-API-Reference and not sure whether all data are available in API :-(

Thanks for you valuable work!

Afischbacher commented 6 months ago

Hey @mlynarp

So, I have just released the 3.1.0 version of the Nhl.Api and think it will help you towards your goal of creating this app, I hope. It may require you to do API calls within the NuGet with minor LINQ manipulation, but it could work.

If I understand exactly what you're looking for. Take a look at the GetPlayerInformationAsync method to get all the statistics such as Goals, Assists and Points and call the GetTotalPlayerStatisticValueByTypeAndSeasonAsync for the Face offs Won and any other statistics you may need or want. Now I understand this might not give you the full picture of what you are looking for, but I think it's a step in the right direction and a welcome improvement for individuals looking for different statistics.

Here is an example of the Player Landing Endpoint for Connor McDavid

 "featuredStats": {
        "season": 20232024,
        "regularSeason": {
            "subSeason": {
                "gamesPlayed": 33,
                "goals": 14,
                "assists": 39,
                "points": 53,
                "plusMinus": 9,
                "pim": 16,
                "gameWinningGoals": 0,
                "otGoals": 0,
                "shots": 96,
                "shootingPctg": 0.145833,
                "powerPlayGoals": 4,
                "powerPlayPoints": 19,
                "shorthandedPoints": 0,
                "shorthandedGoals": 0
            },

Again, it does not have all the pieces of the puzzle, but I think it's close once you include the new endpoints I have added.

Let's keep this issue open and find a full solution for you, but please let me know what you think about the new changes.

Thanks

P.S. Please share this app, I would love to see it!!!

mlynarp commented 6 months ago

Hey @Afischbacher thanks for new version. It looks good and I think I will be able to get all the data I need.

Just one note, I noticed the implementation of the total player statistics. This is perfectly fine for one player, however I need to collect it for all NHL players and the above mentioned fields. It looks to me that it would cause a performance issue so I will rather iterate over all games boxscores and extract the particular fields for players.

Maybe it would be good to do it in API at least per team to avoid plenty of API calls and iterations. Something like GetTotalPlayersStatistiscByTeamAndSeasonAsync which would return a collections of players and its full statistic values within a POCO object.

P.S. My app is currently in very rough state, its like alpha alpha version :-) I can share later on when it is really in a good shape.

mlynarp commented 6 months ago

I started with the implementation but found that I had no way to collect information about the team. There is a list of team enums, but I can't map it to a team abbreviation. This function is private through the NhlTeamService class. The only way to collect information such as team name, conference name, division name, team abbreviation is through GetLeagueStandingsByDateAsync. This function includes team stats, which is great, but unfortunately it doesn't include the team ID, so I don't have a way to connect to the team enum programmatically.

Can you improve it and include this information to LeagueStanding object or make NhlTeamService interface available to use?

Thanks.

Afischbacher commented 6 months ago

Hey @mlynarp

Thank you for the feedback. Let me see if I can open up the functionality of the service to be public and also see if I can get the statistics per player for all categories. I'll see if I can find time tonight to unblock you.

mlynarp commented 6 months ago

Hi @Afischbacher

any news regarding API extension?

Thanks

Afischbacher commented 6 months ago

Hey @mlynarp

Let me try and release something for this weekend, apologies on the delays

Thanks

mlynarp commented 5 months ago

Hi @Afischbacher,

do you have time to release something? Or what is your expected time frame to provide above mentioned features.

Thanks for info.

Afischbacher commented 5 months ago

Hey @mlynarp

I have local changes done. Just need to write tests and deploy.

My goal is to have this done for end of week. Other projects and priorities have been taking my time, sadly...

I'll keep you posted. But please keep bugging me.

Thanks

Andre

Afischbacher commented 5 months ago

Hey @mlynarp

Check out the latest release of v3.2.0 and let's see if this does the trick for your app, I think it's what you're looking for!.

The method is GetAllTotalPlayerStatisticValueBySeasonAsync

If it does, I can close the issue, apologies for this taking longer than expected!

Thanks for your patience!

Andre

mlynarp commented 5 months ago

Hey @Afischbacher,

thanks for you update. I checked the API and I think I should be able to fulfill my task. Nevertheless I started to work on it and found out that NHL API (the official one, not your nuget) is extremely slow. Hope its temporarily. Do you experience same issue?

One simple request (https://api-web.nhle.com/v1/schedule/2023-10-29) takes up to 40 seconds. So the nuget API calls timed out.

mlynarp commented 5 months ago

It looks like to be working normally again.

mlynarp commented 5 months ago

So during my implementation I discovered couple of troubles:

  1. Calling GetAllTotalPlayerStatisticValueBySeasonAsync(8478465, "20232024") throws exception: Object reference not set to an instance of an object
  2. Calling GetAllTotalPlayerStatisticValueBySeasonAsync(8482496, "20232024") returns wrong stats. It has 48 FOW, 13 Hits and 21 Blocked shots. However the stats on NHL or Yahoo says 44, 9, 14.
  3. Calling GetAllTotalPlayerStatisticValueBySeasonAsync(8481479, "20232024") throws exception: Unknown NHL team: Grand Rapids Griffins (Parameter 'teamName'). -> Even though this player is in Detroid Red Wings.
  4. Calling GetAllTotalPlayerStatisticValueBySeasonAsync(8477507, "20232024") throws exception: Sequence contains more than one matching element.
  5. Asking stats for all players in parallel (725 players cca) will take aprox. 5 minutes. It would be great to think in API change to improve performance. (Maybe team based player stats but it might be tricky in case of transfers)

All above mentioned will be valid for more players, I just mentioned the reproducible cases.

To fix 3: I think the condition for the team should be x.Season == int.Parse(seasonYear) && x.LeagueAbbrev == "NHL" on the line 385 (NhlStatisticsApi.cs) or make leagueAbbrev parameterized.

Afischbacher commented 5 months ago

Hey @mlynarp,

Let me peek at these cases and see if I can get these bugs fixed soon.

Thanks, Andre

Afischbacher commented 5 months ago

Hey @mlynarp ,

I have fixed most of the issues quickly, I think I can have a release out tonight.

Thanks

mlynarp commented 5 months ago

Hey @Afischbacher

do you plan to release your quick fixes or do you plan something bigger? :-)

Thanks for feedback.

Afischbacher commented 5 months ago

Hey @mlynarp

Haha, if I were planning something bigger, I would tell you ๐Ÿง‘โ€๐Ÿณ๐Ÿ˜๐Ÿ˜†

Let me try and get it out in the next 24 hours, otherwise it will be a couple of days.

Thanks again, Andre

Afischbacher commented 5 months ago

@mlynarp

Take a read here https://github.com/Afischbacher/Nhl.Api/pull/48/files

Make comments if you want!

If I have time I can merge and release the NuGet tonight ๐Ÿ‘

Afischbacher commented 5 months ago

@mlynarp

v3.2.1 is live, take a peak!

mlynarp commented 5 months ago

Great,

first test results:

  1. Calling GetAllTotalPlayerStatisticValuesBySeasonAsync(8481523, "20232024", Nhl.Api.Enumerations.Game.GameType.RegularSeason) throws exception: Unknown NHL team: Montrรฉal Canadiens (Parameter 'teamName')' happens for all Montreal players
  2. Still very slow. Any idea how to improve it? My idea would be to make a function to call total stats per team to reduce number of iterations.

Thanks for your work!

mlynarp commented 5 months ago

Number of blocks still not fit, e,g. Elias Pettersson

Afischbacher commented 5 months ago

@mlynarp

  1. Release, v3.2.2 is now live, this is for the Montreal fix due to the accent in the team's name, which should work now.
  2. As for the performance, because I am pull each play by play in the game, it is not super performant at times, but I usually see performance in the range of 400ms to 1.5s which is not idea but not terrible, how slow is it for you?
  3. Number of blocks for Elias Pettersson? Not sure what this means?

Thanks, Andre

mlynarp commented 5 months ago

Montreal issue is fixed, great.

I am loading all NHL players, so I call GetAllTotalPlayerStatisticValuesBySeasonAsync roughly 1200 times. I run it in parallel but still it takes aprox. 120 seconds and sometimes when running in parallel it fails. Running in sequence would be a death.

The number of blocked shots returned by GetAllTotalPlayerStatisticValuesBySeasonAsync is not correct and doesn't fit the official numbers from NHL. The Faceoff won and hits seem to be correct.

Afischbacher commented 5 months ago

Hey @mlynarp

Happy to see this works with the new fix

Yes well, you're making about 84 * 1200 =100,800 requests in the span in ~2 minutes, I am not surprised, I would consider looking into a caching/storing this data in a persisted state to reduce your dependency on the API and run this on an event based or timer-based model. Doing this on request is not the best idea because this data does not update until any game is live or active.

As for the blocked shots, I took a deep dive into Elias Pettersson blocks for this season, and it has 41 registered events for him blocking versus the 38, so I am not sure if it's a data issue, but go through each game and look to see if he blocks the puck (joking of course)

See my data below, there are 41 instances of him "blocking a shot" sample.json

I am ready to close this issue,

Thanks

mlynarp commented 5 months ago

Hey @Afischbacher

current version is pretty good and I can start to use software again and save my fall in Fantasy League :-)

Anyway I checked the json and you are right there are 41 blocked shots for Elias. This is really strange. Only thing that come to my mind is that they do a stats correction after the game and maybe its not fixed in the play by play data.

Performance: You do for every player a call for profile information (that must be done), then call for team schedule and then for every game (84) you call GetGameCenter and then the iterative math. This means 1200x team schedule, 1200x84 game center.

I believe you can call once per team its schedule and then for every game(84) call game center. Then iterate and fill all team players. This would mean 32x team schedule, 32x84 game center. Iteration for team members will be more complicated but much faster. In the end you end up with 2688 API calls.

I will appreciate if you can try this as it would be then complete. I do some caching but irrespectively of this I believe it deserves this improvement ;-)

Thanks for your support.

Afischbacher commented 5 months ago

Hey @mlynarp

I think I can create an entire endpoint dedicated to this, I think the difference here is instead of the entity being around a player, we can just focus it on the entire league and build a Dictionary of Dictionary<PlayerProfile, Stats> and I think it can be done.

Let me see what I can do, I won't close this just yet.

Thanks, Andre

mlynarp commented 4 months ago

Hey @Afischbacher,

I redesigned my application and as a part of it I collect stats in different way. See the pseudo code:

  1. I load team schedule for every team
  2. For each game I load boxscore
  3. From boxscore I iterate the stats of forwards, defense and goalies
  4. I sum up the totals of statistics field

As a result all of this takes aprox 10 seconds and all the statistics are correct and fits to what is shown on Yahoo. I think that this is much better attitude to collect stats.

Petr

Afischbacher commented 4 months ago

Hey @mlynarp

I do have a new release coming out that might still give you what you're looking for. I will try and push something out this week.

Thanks

Afischbacher commented 4 months ago

@mlynarp

Hey!

Look at v3.3.0 I have added an endpoint that is much more performant and gives all the statistics I believe you need!

The method is GetAllPlayersStatisticValuesBySeasonAsync

Let me know what you think and if this works, we can finally close this!

Thanks, Andre

mlynarp commented 4 months ago

Hey @Afischbacher

I checked new version and it is really a good progress, however I measured 21 seconds for whole league which is still worse than my iteration over boxscore (8seconds) and it still suffers from unprecise data. The hits given and blocked shots doesn't match the real statistics.

I think that game play by play is not the correct statistics. Rather use the boxscore where it seems to fit.

P.S. Do you think you might provide an API to get player ID from player name? The problem is that getting the team roster will return just the active players on the roster. It means when a player is sent to AHL or being long term injured he's not part of roster and then this player is missing in overall stats. Alternatively you might return all player IDs who played within current season that would be the best.

Thanks for your work!

Afischbacher commented 4 months ago

Hey @mlynarp

Interesting, during my testing I saw it return me results within ~10 seconds, which is not bad for me, but it could be due to caching or latency from the NHL endpoint? Not sure why all the endpoints you call are quite slow? It is odd.

As for this question: Do you think you might provide an API to get player ID from player name?

There are two endpoints that come to mind:

public async Task<List<PlayerSearchResult>> SearchAllPlayersAsync(string query, int limit = 25, CancellationToken cancellationToken = default)

public async Task<List<PlayerSearchResult>> SearchAllActivePlayersAsync(string query, int limit = 25, CancellationToken cancellationToken = default)

With these endpoints you can just search for any name and have information returned including the PlayerId

Also remember the PlayerEnum is a direct dynamic mapping of all player names to their identifiers. Look for it, it may help you!

Let me know if this is any help, if it is, I can close this issue!

P.S. Please send a link to this app when you get a chance!

Thanks

mlynarp commented 4 months ago

Hey @Afischbacher

thanks for the API, I think that should do the job for me.

From my perspective the API offers what I really need, I will keep my own implementation but your API is fine.

Feel free to close the issue but I would consider to replace play by play statistics by boxscore as it delivers precise data.

Afischbacher commented 4 months ago

Hey @mlynarp

Going to close this issue as it is resolved!

Thanks

P.S. Please do share the app you build!!