8.3 Project

Univariate Statistics

Note

Download the data for this project by clicking this link.

We will use this dataset form Kaggle.com that I have made available on my GitHub page: https://raw.githubusercontent.com/nkmwicz/data-for-students/refs/heads/main/soccer-players.csv

This data has information on players from transfermarkt.com. It specifically, has information on the birthdates of professional soccer players. There is a theory that exists that players born in the earlier months of the year are significantly more likely to play professional soccer than those born later. Theoretically, if all birth months have an equal chance at making the pros, there should be an even distribution of birth months. We will test this theory using the data.

First, let’s load the data and take a look at it.

import pandas as pd

# Load the data
url = "https://raw.githubusercontent.com/nkmwicz/data-for-students/refs/heads/main/soccer-players.csv"

players = pd.read_csv(url)
players.head()
player_id first_name last_name name last_season current_club_id player_code country_of_birth city_of_birth country_of_citizenship ... foot height_in_cm contract_expiration_date agent_name image_url url current_club_domestic_competition_id current_club_name market_value_in_eur highest_market_value_in_eur
0 10 Miroslav Klose Miroslav Klose 2015 398 miroslav-klose Poland Opole Germany ... right 184.0 NaN ASBW Sport Marketing https://img.a.transfermarkt.technology/portrai... https://www.transfermarkt.co.uk/miroslav-klose... IT1 Società Sportiva Lazio S.p.A. 1000000.0 30000000.0
1 26 Roman Weidenfeller Roman Weidenfeller 2017 16 roman-weidenfeller Germany Diez Germany ... left 190.0 NaN Neubauer 13 GmbH https://img.a.transfermarkt.technology/portrai... https://www.transfermarkt.co.uk/roman-weidenfe... L1 Borussia Dortmund 750000.0 8000000.0
2 65 Dimitar Berbatov Dimitar Berbatov 2015 1091 dimitar-berbatov Bulgaria Blagoevgrad Bulgaria ... NaN NaN NaN CSKA-AS-23 Ltd. https://img.a.transfermarkt.technology/portrai... https://www.transfermarkt.co.uk/dimitar-berbat... GR1 Panthessalonikios Athlitikos Omilos Konstantin... 1000000.0 34500000.0
3 77 NaN Lúcio Lúcio 2012 506 lucio Brazil Brasília Brazil ... NaN NaN NaN NaN https://img.a.transfermarkt.technology/portrai... https://www.transfermarkt.co.uk/lucio/profil/s... IT1 Juventus Football Club 200000.0 24500000.0
4 80 Tom Starke Tom Starke 2017 27 tom-starke East Germany (GDR) Freital Germany ... right 194.0 NaN IFM https://img.a.transfermarkt.technology/portrai... https://www.transfermarkt.co.uk/tom-starke/pro... L1 FC Bayern München 100000.0 3000000.0

5 rows × 23 columns

Now, there are a lot of columns in this dataset that are unnecessary, so lets drop them (such as first_name, last_name, contract_expiration_date, image, etc.)

# Drop unnecessary columns
players = players[["player_id", "name", "last_season", "country_of_birth", "city_of_birth", "country_of_citizenship", "date_of_birth", "position", "foot", "height_in_cm", "agent_name", "market_value_in_eur"]]
players.head()
player_id name last_season country_of_birth city_of_birth country_of_citizenship date_of_birth position foot height_in_cm agent_name market_value_in_eur
0 10 Miroslav Klose 2015 Poland Opole Germany 1978-06-09 00:00:00 Attack right 184.0 ASBW Sport Marketing 1000000.0
1 26 Roman Weidenfeller 2017 Germany Diez Germany 1980-08-06 00:00:00 Goalkeeper left 190.0 Neubauer 13 GmbH 750000.0
2 65 Dimitar Berbatov 2015 Bulgaria Blagoevgrad Bulgaria 1981-01-30 00:00:00 Attack NaN NaN CSKA-AS-23 Ltd. 1000000.0
3 77 Lúcio 2012 Brazil Brasília Brazil 1978-05-08 00:00:00 Defender NaN NaN NaN 200000.0
4 80 Tom Starke 2017 East Germany (GDR) Freital Germany 1981-03-18 00:00:00 Goalkeeper right 194.0 IFM 100000.0

Now, let’s clean the date of birth to get birth months. We can do this by using the datetime methods of Pandas.

from datetime import datetime

players.dropna(subset=["date_of_birth"], inplace=True)
players["birth_mth"] = pd.to_datetime(players["date_of_birth"]).dt.month
players.describe()
player_id last_season height_in_cm market_value_in_eur birth_mth
count 3.235600e+04 32356.000000 30023.000000 3.083700e+04 32356.00000
mean 3.446263e+05 2019.342533 182.291143 1.612785e+06 5.72648
std 2.806483e+05 3.965010 6.972046 6.365154e+06 3.42123
min 1.000000e+01 2012.000000 17.000000 1.000000e+04 1.00000
25% 1.069858e+05 2016.000000 178.000000 1.000000e+05 3.00000
50% 2.824215e+05 2020.000000 183.000000 2.500000e+05 5.00000
75% 5.241980e+05 2023.000000 187.000000 7.000000e+05 9.00000
max 1.309800e+06 2024.000000 207.000000 2.000000e+08 12.00000

We can also call many of these functions on their own just by using the mean(), median(), mode(), std(), min(), and max() functions. This time, let’s look at players’ heights.

print(f"mean: {players['height_in_cm'].mean()}")
print(f"std: {players['height_in_cm'].std()}")
print(f"min: {players['height_in_cm'].min()}")
print(f"max: {players['height_in_cm'].max()}")
print(f"median: {players['height_in_cm'].median()}")
print(f"mode: {players['height_in_cm'].mode()}")
mean: 182.29114345668322
std: 6.972045868385484
min: 17.0
max: 207.0
median: 183.0
mode: 0    180.0
Name: height_in_cm, dtype: float64

If we have many columns that are

We can also call many of these functions on their own just by using the mean(), median(), mode(), std(), min(), and max() functions. This time, let’s look at players’ heights.

print(f"mean: {players['height_in_cm'].mean()}")
print(f"std: {players['height_in_cm'].std()}")
print(f"min: {players['height_in_cm'].min()}")
print(f"max: {players['height_in_cm'].max()}")
print(f"median: {players['height_in_cm'].median()}")
print(f"mode: {players['height_in_cm'].mode()}")
mean: 182.29114345668322
std: 6.972045868385484
min: 17.0
max: 207.0
median: 183.0
mode: 0    180.0
Name: height_in_cm, dtype: float64

If we have many columns that are

Now let’s look at some other columns in the dataset that are categorical. We can loop over the columns checking of each on is numerical or not. To check if numerical we can use the pd.api.types.is_numeric_dtype() function.

for col in players.columns:
    if not pd.api.types.is_numeric_dtype(players[col]):
        print(f"{col}: {players[col].nunique()}")
name: 31663
country_of_birth: 185
city_of_birth: 8532
country_of_citizenship: 183
date_of_birth: 9272
position: 5
foot: 3
agent_name: 2833

Let’s find the different boundaries or thresholds of the data. For instance, what is the boundary separating the lower 25% and lower 30% from the rest of the data. We can use the quantile() function to find this.

# Find the 25th and 33rd percentiles
q25 = players["height_in_cm"].dropna().quantile(0.25) * 0.0328  # to get feet
q33 = players["birth_mth"].quantile(0.33)
print(q25)
print(q33)
5.838400000000001
3.0

We can also check the skew number of the data. This is a measure of the asymmetry of the data. A skew of 0 means the data is symmetrical. A positive skew means the data is skewed to the right, while a negative skew means the data is skewed to the left. While we are at it, let’s check the kurtosis as well.

from scipy.stats import skew, kurtosis

skewness = skew(players["birth_mth"].dropna())
kurt = kurtosis(players["birth_mth"].dropna())
print(f"Skewness: {skewness}")
print(f"Kurtosis: {kurt}")
Skewness: 0.26205852205376656
Kurtosis: -1.1306173317984487

The skewness output is 0.2620. This is a slightly positive skewness score which means the data is slightly skewed to the right.

The kurtosis score is -1.1306 which means the data is fairly flat. This means the data is slightly less peaked than a normal distribution.

import matplotlib.pyplot as plt

plt.hist(players["birth_mth"], bins=4, color="skyblue", edgecolor="black")
plt.xlabel("Birth Month")
plt.ylabel("Frequency")
plt.title("Birth Month Distribution")
plt.show()

The nature of birth months is that we expect an equal distribution of birth months. However, the data shows that there are more players born in the earlier months of the year. This is consistent with the theory that players born in the earlier months of the year are more likely to play professional soccer. In fact, is a dramatic difference in the number of players born in the earlier months compared to the later months.

for i, num in enumerate(players["birth_mth"].value_counts()):
    print(f"Month {i+1}: {num} or {num/players.shape[0]*100:.2f}%")
Month 1: 4066 or 12.57%
Month 2: 3376 or 10.43%
Month 3: 3256 or 10.06%
Month 4: 3001 or 9.27%
Month 5: 2915 or 9.01%
Month 6: 2595 or 8.02%
Month 7: 2534 or 7.83%
Month 8: 2400 or 7.42%
Month 9: 2294 or 7.09%
Month 10: 2104 or 6.50%
Month 11: 1973 or 6.10%
Month 12: 1842 or 5.69%
jan_num = players.loc[players["birth_mth"] == 1, "birth_mth"].count()
jan_perc = jan_num / len(players) * 100
print(jan_perc)
for i in range(1, 13):
    num = players.loc[players["birth_mth"] == i, "birth_mth"].count()
    mth_avg = (num / len(players)) * 100
    deficit = jan_perc - mth_avg
    # print(mth_avg)
    print(f"Month {i} deficit: {deficit:.2f}")
12.56644826307331
Month 1 deficit: 0.00
Month 2 deficit: 2.50
Month 3 deficit: 2.13
Month 4 deficit: 3.29
Month 5 deficit: 3.56
Month 6 deficit: 4.55
Month 7 deficit: 4.73
Month 8 deficit: 5.15
Month 9 deficit: 5.48
Month 10 deficit: 6.06
Month 11 deficit: 6.47
Month 12 deficit: 6.87