Estadística descriptiva, mesures de centralització, dispersió, outliers, correlació entre variables i corbes distribució. Amb gràfics en Seaborn per il·lustrar tots els conceptes.

Introducció a l'Estadística

Història de l'Estadística

L'estadística va sorgir a Europa al 1663, inicialment enfocada en la "ciència de l'estat" per prendre decisions polítiques basades en dades econòmiques i demogràfiques. Avui en dia, té aplicacions molt més àmplies.

Branques de l'Estadística

La variabilitat de les DADES. Els processos aleatoris que generen aquestes dades, és a dir, la PROBABILITAT.

  • Estadística Descriptiva (se centra amb les dades)

Resum de les dades observades.

  • Estadística Inferencial (se centra amb les probabilitats)

Extrapolar una informació que tinc d’unes mostres a tota la població.

Definicions Clau

  • Dada: Unitat d'informació rellevant.
  • Probabilitat: Mesura de la certesa d'un esdeveniment, sempre entre 0 i 1. 0 vol dir que el succés és impossible que passi, i 1 que segur que passa.
  • Aleatorietat: Falta aparent de predictibilitat en esdeveniments; ja que el resultat d'un succés

Relació entre Dades i Probabilitat

  • Si tenim dades observades, podem deduir probabilitats.
  • Si tenim probabilitats, podem predir esdeveniments futurs.

Llibreries útils per a la estadística.

  • Statistics llibreria inclosa al nucli de Python per obtenir estadístiques de llistes. Si uses Pandas potser ja no et caldrà.

  • Numpy Té moltes útilitats: generar números i mostres aleatoris, tractament valors NaN i arrays.

  • Pandas Ens ve molt bé organitzar les dades tabulars en DataFrames o Series i proporciona mètodes per obtenir estadístiques totals i per calcular estadístiques de dades seleccionades ràpidament.

  • Polars Tot i no ser tant coneguda, ofereix millor rendiment i la mateixa compatibilitat en gràfics per a tractar Dataframes.

  • Matplotlib Llibreria de baix nivell per dibuixar trames i generar tot tipus de gràfics. Funciona molt bé amb estructures de Python, arrays, series, dataFrames... Requereix més codi que Seaborn, però ofereix el màxim nivell de personalització.

  • Seaborn Llibreria d'alt nivell. Ideal per gent amb deadlines ajustats. Amb menys codi que Matplotlib i un rendiment no gaire inferior, s'obtenen gràfics molt bons.

Estadística Descriptiva

Definicions i Conceptes

  • Població: Conjunt d'elements o esdeveniments d'interès.
  • Mostra: Subconjunt de la població que es pot observar.
  • Variable: Característica mesurable de la mostra.

Si llenço un dau 10 cops només, no tinc suficient informació per saber si el dau està trucat.

Però si el llenço 10.000 cops, i cada cara no surt aproximadament el mateix número de cops, llavors sí que puc dubtar del dau.

Per exemple, si vull saber la satisfacció dels alumnes dels cicles formatius, per a què la mostra sigui significativa n'escolliré de diverses families professionals (Salut, Mecànica, ...), de diversos nivells (PFI, CFGM, CFGS), a alumnes de diverses edats, que no hi hagi un excés d'homes i dones, etc...

Conclusió: El tamany de la mostra IMPORTA.

Tipus de Variables

  • Quantitatives:

    • Continues: Exemples com pes, la edat o alçada. En alguns estudis ens interessa molt agrupar-les per a què siguin discretes, per exemple, en grups d'edat (grup50_60,grup60_70 ...) per tal de poder representar gràfics o estudiar per separat cada franja.

    • Discretes: Exemples com nombre de fills.

  • Qualitatives (o de categories):

    • Ordinals: Amb un ordre implícit (Excel·lent, Notable ...)

    • Nominals: Sense ordre explícit, com classes de malalties (Sida,Càncer,Grip...)

El valor d'una variable també es pot anomenar "freqüència"

  • Freqüència absoluta: El valor absolut.

  • Freqüència relativa: El valor dividit per la mida de la mostra.

És molt important calcular aquestes freqüències per fer gràfics com els diagrama de barres, de línies, de punts, mapes de calor...

Estadística descriptiva: Mesures de Tendència Central

En estadística descriptiva es fan servir diferents mesures per intentar descriure la tendència central de les nostres dades.

  • Mitjana: La mitjana aritmètica o mitjana és el valor obtingut en sumar totes les dades i dividir el resultat entre el nombre total elements. En anglès es diu MEAN (Pandas, Numpy) o AVERAGE (LO Calc).

  • Mitjana Ponderada: La mitjana ponderada ens interessa quan algunes de les dades tenen més pes dins del valor de la mitjana que altres. Per exemple, en calcular les notes finals d'un mòdul on la Pràctica Pt1 compta un 70%, i l'examen compta el 30% restant. En anglès es diu WEIGHTED MEAN.

  • Moda: La moda és el valor (o valors si hi ha un empat) que té/nee més freqüència absoluta. Per calcular-ho hem de comptar el número de vegades que apareix cada valor d'una variable (les freqüències) o usar una funció que ens ho calculi automàticament. Si tots els elements apareixen un sol cop la variable no té moda. També pot passar que 2 o més valors empatin en màxim número d'aparicions, en aquest cas diem que tenim una variable multimodal.

Per exemple, suposem tenim una llista amb les notes de 10 persones i ens demanen la mitjana aritmètica i la moda.

llistaNotes = [8, 5, 6, 3, 10, 8, 6, 4, 8, 7]

El resultat que esperem és:

Mitjana = 6,5

L'obtenim sumant tots els elements 8 + 5 + ... + 7 = 65; pel número d'elements. 65 / 10 = 6,5

Moda = 8

És la nota que més apareix de totes, 3 vegades.

[8, 5, 6, 3, 10, 8, 6, 4, 8, 7]

8 -> 3 vegades 6 -> 2 vegades 5-> 1 vegada ... 3 -> 1 vegada

Podem automatitzar aquests càlculs amb Python i la classe Counter de la llibreria collections.

Aquesta classe permet calcular les freqüèncis absolutes de cada valor, i obtenir el que en té més (la moda).

from collections import Counter

llistaNotes : list[int] = [8, 5, 6, 3, 10, 8, 6, 4, 8, 7]

mitjanaNotes : float = sum(llistaNotes)/len(llistaNotes)
# print("Mitjana ", mitjanaNotes)

dictFreqNotes : dict[int,int] = Counter(llistaNotes)
# print(dictFreqNotes)
# Un cop tenim generades les freqüències, podem obtenir
# la moda amb la funció most_common(num)
modaNotes = dictFreqNotes.most_common(1)[0][0]
numVegadesModa = dictFreqNotes.most_common(1)[0][1]
# print("Moda ", modaNotes)

# Provem que obtenim els resultats esperats.
assert(mitjanaNotes == 6.5)
assert(modaNotes == 8)

Exemple

Tenim les notes de 4 alumnes (4 files) en 3 examens (3 columnes) en un array de Numpy i en un DataFrame.

Volem calcular-ne la mitjana aritmètica i la mitjana ponderada tenint en compte els pesos 0.2, 0.3 i 0.5 per cada examen.

També necessitem la moda de tots els resultats (els 12 examens). Tingues en compte que pot haver més d'una moda.

import numpy as np
from statistics import multimode

# Definim l'array bidimensional x amb les notes dels alumnes
x = np.array([
    [7, 6, 10],
    [4, 8, 7],
    [9, 5, 9],
    [7, 9, 8]
])

# Recordem, és la mitjana de cada alumne (files)
mean = np.mean(x, axis=1)

print("Mitjana aritmètica per a cada alumne:")
for i in range(len(mean)):
    print(f"Alumne {i+1} : {mean[i]:.2f}")

# Definim l'array de pesos w per a cada nota
w = np.array([0.2, 0.3, 0.5])
wmean = np.sum(w * x, axis=1) / np.sum(w)

print("Mitjana ponderada per a cada alumne:")
for i, wmean in enumerate(wmean):
    print(f"Alumne {i+1}: {wmean:.2f}")

# Aplanar l'array en una sola dimensió
x_flat = x.flatten()
# Trobar totes les modes
modes = multimode(x_flat)

print(f"Les modes de les notes són: {modes}")

Resultat:

Mitjana aritmètica per a cada alumne:
Alumne 1 : 7.67
Alumne 2 : 6.33
Alumne 3 : 7.67
Alumne 4 : 8.00
Mitjana ponderada per a cada alumne:
Alumne 1: 8.20
Alumne 2: 6.70
Alumne 3: 7.80
Alumne 4: 8.10
Les modes de les notes són: [7, 9]

Si en canvim tenim aquestes notes en un dataFrame, on hi podem tenir més informació; els resultats els podem obtenir d'aquesta manera; on al final creem una nova columna Mitjana_Ponderada per cada alumne.

import pandas as pd
import numpy as np

# DataFrame amb les dades dels alumnes (columnes) i les seves notes
data = {
    'Alumne': ['Anna', 'Bernat', 'Carla', 'David'],
    'Examen1': [7, 4, 10, 6],
    'Examen2': [6, 8, 7, 9],
    'Examen3': [8, 7, 9, 8]
}
df = pd.DataFrame(data)

# Calculem la mitjana aritmètica d'un examen (així el professor sap si l'ha fet molt difícil)
print("Mitjana aritmètica examen 1 = ",df['Examen1'].mean(axis=0))

# Pesos per a la mitjana ponderada (exemple)
pesos = [0.2, 0.3, 0.5]

# Calculem la mitjana ponderada per a cada alumne
df['Mitjana_Ponderada'] = np.average(df[['Examen1', 'Examen2', 'Examen3']], weights=pesos, axis=1)

print("Mitjana ponderada final examens.")
print(df['Mitjana_Ponderada'])

# Generem el diagrama de barres per les notes dels alumnes
fig, ax = plt.subplots(figsize=(10, 6))

num_alumnes = len(df['Alumne'])
num_exams = 3

# Creem les barres per cada examen
positions = np.arange(num_alumnes)
bar_width = 0.2

for i, examen in enumerate(['Examen1', 'Examen2', 'Examen3']):
    ax.bar(positions + i * bar_width, df[examen], width=bar_width, label=examen)

# Configuració del gràfic
ax.set_xlabel('Alumnes')
ax.set_ylabel('Notes')
ax.set_title('Notes parcials examens')
ax.set_xticks(positions + bar_width)
ax.set_xticklabels(df['Alumne'])
ax.legend()

# Ajustar la visualització
plt.tight_layout()
plt.show()

fig, ax = plt.plot(figsize=(8, 5))
# Generem el diagrama de barres per les mitjanes ponderades
plt.figure(figsize=(6, 6))

# Creem les barres per les mitjanes ponderades
plt.bar(df['Alumne'], df['Mitjana_Ponderada'], color='skyblue', label=df['Mitjana_Ponderada'])

plt.xlabel('Alumnes')
plt.ylabel('Mitjana Ponderada')
plt.title('Mitjanes Ponderades dels Alumnes')

# Afegim els números de cada barra
for i, v in enumerate(df['Mitjana_Ponderada']):
    plt.text(i, v + 0.1, str(round(v, 1)), ha='center', va='bottom', fontsize=10)

# Ajustar la visualització
plt.tight_layout()
plt.show()

Hem aprofitat per a crear dos gràfics dels valors del DataFrame amb Matplotlib; un amb les notes dels 4 alumnes i 3 examens i un altre amb la mitjana ponderada de cada alumne en els examens.

Aquí podem veure el primer gràfic:

Estadística descriptiva: Mesures de Variabilitat o dispersió

Per complementar les mesures de tendència central, es fan servir les mesures de variabilitat, que descriuen com varien les dades respecte al seu centre.

Mediana: La mediana és el valor que ocupa el lloc central de totes les dades quan aquestes estan ordenades de menor a major. Recomanable fer la gràfica de totes les observacions ordenades. En anglès es diu median.

Rang: El rang és la diferència entre l'observació més alta (màxim) i la més baixa (mínim).

Rang = | Màx - Mín |

Desviació respecte a la mitjana: La desviació respecte a la mitjana és la diferència en valor absolut entre cada valor de la variable estadística i la mitjana aritmètica. Aquest càlcul es fa per a calcular el pendent de les rectes de regressió.

Variança: La variància és la mitjana aritmètica del quadrat de les desviacions respecte a la mitjana d'una distribució estadística. La variància intenta descriure la dispersió de les dades. En resum, la variància seria la mitjana de les desviacions al quadrat. Es representa com σ2 (sigma minúscula al quadrat).

Desviació típica: La desviació típica és l'arrel quadrada de la variància. Es representa amb la lletra grega σ. En anglès es conèix com std = Standard Desviation.

Quantil: Els quantils són punts presos a intervals regulars de la funció de distribució d’una variable aleatòria. Les mostres s'ordenen segons els valors de menor a major, i el quantil és el valor en aquest punt.

Si el punt es pren en tant per cent de les observacions, s'anomenen percentil. Si les observacions es divideixen en quatre quarts, les tres divisions es diuen quartils (Q1=25%, Q2=50%, Q3=75%). Q2 és la Mediana. També hi ha decils (D1=10%, … D9=90%) si volem filar més prim.

L'aplicació pràctica que haureu vist dels quantils és ordenar les publicacions científiques per H-Index, on les del Q1 són les més recomanades.

Rang interquartílic:

És la diferència (resta) entre el primer i el tercer quartil.

IQR = Q3 − Q1.

Com mostrar estadístiques descriptives ?

Molt senzill! Pandas proporciona una funció que ja hem vist des del primer dia: describe.

Les que aquesta funció no proporcioni són senzilles de calcular.

Provem-ho amb el dataset dels examens dels alumnes:

data = {
    'Alumne': ['Wally', 'Xavi', 'Yaiza', 'Zulema'],
    'Examen1': [7, 4, 10, 6],
    'Examen2': [6, 8, 7, 9],
    'Examen3': [8, 7, 9, 8]
}
df = pd.DataFrame(data)
print(df.describe())
print(df['Examen1'].describe())

Resultat:

       Examen1   Examen2   Examen3
count     4.00  4.000000  4.000000
mean      6.75  7.500000  8.000000
std       2.50  1.290994  0.816497
min       4.00  6.000000  7.000000
25%       5.50  6.750000  7.750000
50%       6.50  7.500000  8.000000
75%       7.75  8.250000  8.250000
max      10.00  9.000000  9.000000

count     4.00
mean      6.75
std       2.50
min       4.00
25%       5.50
50%       6.50
75%       7.75
max      10.00
Name: Examen1, dtype: float64

Fixeu-vos totes les estadístiques que hem aconseguit:

  • count: Recompte de files/elements
  • Mean: Mitjana aritmètica
  • std: Desviació típica
  • max i min: Màxim i mínim
  • 50%: Mediana
  • 25% i 75%: Quartil1 i Quartil3

Estadístiques tant generals, com d'un examen concret (d'una columna del pandes).

Ara enteneu perquè us hem fet calcular la moda i la mitjana ponderada :)

Per calcular l'IQR, també ho tenim molt fàcil:

iqr = df['Examen1'].quantile(0.75) - df['Examen1'].quantile(0.25)
print(iqr)

El resultat és 7.75 - 5.5 = 2.25

Valors atípics (outlier).

Un valor atípic (outlier) és una observació que s'allunya massa de la moda; aquesta molt lluny de la tendència principal de la resta de dades.

Concretament, un valor outlier serà:

  • Un valor inferior al quartil1 - 1,5 * IQR

  • Un valor superior al quartil3 + 1,5 * IQR

Recordm que l'IQR és el rang interquartílic: q3 - q1.

Sempre cal considerar-ne la causa. Si són causats per errors en la lectura de les dades o mesures inusuals s'esborren, però si no cal considerar-les.

Pandas és una molt bona llibreria, però encara no ha previst com calcular els outliers.

De tota manera, amb aquest senzill codi podrem trobar-los i eliminar-los.

Si només volem trobar-los, ens oblidem del loc.

def remove_outliers(df_in, col_name):
    q1 = df_in[col_name].quantile(0.25)
    q3 = df_in[col_name].quantile(0.75)
    iqr = q3-q1 #Interquartile range
    fence_low  = q1-1.5*iqr
    fence_high = q3+1.5*iqr
    df_out = df_in.loc[(df_in[col_name] > fence_low) & (df_in[col_name] < fence_high)]
   return df_out
  • Boxplot: Un boxplot és una gràfica que mostra diversos descriptors alhora d'una o més variables: Els tres quartils, el "mínim" i "màxim" calculats i els outliers. Són usats en molts àmbits.

Les llibreries com Seaborn o Matplotlib els generen molt fàcilment.

Exemple 1. Estadístiques de dispersió i outliers

Ens han donat un dataframe amb la edat, altura, pressió sistòlica i diastòlica de diversos pacients.

La pressió arterial normal, en el cas de la majoria dels adults, es defineix com una pressió sistòlica de menys de 120 i una pressió diastòlica de menys de 80.

Doncs bé, ens demanen mostrar estadístiques centrals i de dispersió de totes les variables, calcular l'amplitud interquartilica de les pressions i crear un nou dataframe sense els outliers de la pressió (sigui sis o dia).

Codi de partida:

df_pacients = pd.DataFrame({
    'edat' : [50,43,22,61,64,54,38,98],
    'altura' : [1.75,1.57,1.6,1.66,1.63,1.79,1.70,1.61],
    'pr_sis' : [125,105,150,800,130,114,105,119],
    'pr_dia' : [81,74,84,76,80,78,69,81]
})

# Tractament dels outliers

Solució esperada:

 edat  altura  pr_sis  pr_dia
0    50    1.75     125      81
1    43    1.57     105      74
2    22    1.60     150      84
4    64    1.63     130      80
5    54    1.79     114      78
6    38    1.70     105      69
7    98    1.61     119      81
            edat    altura      pr_sis     pr_dia
count   7.000000  7.000000    7.000000   7.000000
mean   52.714286  1.664286  121.142857  78.142857
std    23.949351  0.083238   15.826440   5.080307
min    22.000000  1.570000  105.000000  69.000000
25%    40.500000  1.605000  109.500000  76.000000
50%    50.000000  1.630000  119.000000  80.000000
75%    59.000000  1.725000  127.500000  81.000000
max    98.000000  1.790000  150.000000  84.000000

def iqr(df_in, col_name):
    '''
      Calcula l'amplitud interquartilica (-1,5*iqr,+1,5*iqr)
      i retorna els valors inferior i superior.
    '''
    q1 = df_in[col_name].quantile(0.25)
    q3 = df_in[col_name].quantile(0.75)
    iqr = q3-q1 #Interquartile range
    fence_low  = q1-1.5*iqr
    fence_high = q3+1.5*iqr
    # Si, es poden retornar 2 valors.
    return fence_low, fence_high

def remove_outliers(df_in, col_name):
    '''
      Esborra valors outliers (-1,5*iqr,+1,5*iqr)
      d'una columna d'un dataframe.
    '''
    fence_low, fence_high = iqr(df_in,col_name)
    df_out = df_in.loc[(df_in[col_name] > fence_low) & (df_in[col_name] < fence_high)]
    return df_out

df_pacients = pd.DataFrame({
    'edat' : [50,43,22,61,64,54,38,98],
    'altura' : [1.75,1.57,1.6,1.66,1.63,1.79,1.70,1.61],
    'pr_sis' : [125,105,150,800,130,114,105,119],
    'pr_dia' : [81,74,84,76,80,78,69,81]
})

print('IQR pr_sis',iqr(df_pacients,'pr_sis'))
print('IQR pr_dia',iqr(df_pacients,'pr_dia'))
# la edat també té un iqr, però no l'esborrem perquè no passa res per 
# tenir pacients grans.
# print('IQR edat',iqr(df_pacients,'edat'))

df_pacients_clean = remove_outliers(df_pacients,'pr_sis')
print(df_pacients_clean)
print(df_pacients_clean.describe())

Creació de boxplot per estudiar els quartils de variables amb Seaborn.

Anem a crear gràfics de boxplot sobre l'exemple anterior dels pacients, concretament per a la pressió sistòlica.

Ho podem fer amb Matplotlib, però serà més senzill usar la llibreria de gràfics Seaborn. Cal importar-la amb la comanda:

pip install seaborn

En el codi que hem usat abans, hem d'afegir la creació de 2 boxplot, un amb els outliers i l'altre sense els outliers (que podem esborrar amb la funció remove_outliers)

import pandas as pd
import numpy as np
import seaborn as sns

def iqr(df_in, col_name):
    '''
      Calcula l'amplitud interquartilica (-1,5*iqr,+1,5*iqr)
      i retorna els valors inferior i superior.
    '''
    q1 = df_in[col_name].quantile(0.25)
    q3 = df_in[col_name].quantile(0.75)
    iqr = q3-q1 #Interquartile range
    fence_low  = q1-1.5*iqr
    fence_high = q3+1.5*iqr
    # Si, es poden retornar 2 valors.
    return fence_low, fence_high

def remove_outliers(df_in, col_name):
    '''
      Esborra valors outliers (-1,5*iqr,+1,5*iqr)
      d'una columna d'un dataframe.
    '''
    fence_low, fence_high = iqr(df_in,col_name)
    df_out = df_in.loc[(df_in[col_name] > fence_low) & (df_in[col_name] < fence_high)]
    return df_out

df_pacients = pd.DataFrame({
    'edat' : [50,43,22,61,64,54,38,98],
    'altura' : [1.75,1.57,1.6,1.66,1.63,1.79,1.70,1.61],
    'pr_sis' : [125,105,150,800,130,114,105,119],
    'pr_dia' : [81,74,84,76,80,78,69,81]
})

#print(df_pacients.describe())
print('IQR pr_sis',iqr(df_pacients,'pr_sis'))
print('IQR pr_dia',iqr(df_pacients,'pr_dia'))
# la edat també té un iqr, però no l'esborrem perquè no passa res per 
# tenir pacients grans.
# print('IQR edat',iqr(df_pacients,'edat'))

plt.figure(figsize=(8, 4))
plt.title('Pressió sistolica dels pacients (amb outliers).')
sns.boxplot(data=df_pacients, x="pr_sis")
plt.show()

df_pacients_clean = remove_outliers(df_pacients,'pr_sis')
print(df_pacients_clean)
print(df_pacients_clean.describe())

plt.figure(figsize=(8, 4))
plt.title('Pressió sistolica dels pacients (sense outliers).')
sns.boxplot(data=df_pacients_clean, x="pr_sis")
plt.show()

Fixeu-vos amb la sintaxis bàsica del gràfic boxplot de Seaborn:

  1. data Admet DataFrames, Series, dict, array, or list of arrays.
  2. x Variable que ens interessa (si a data hi ha DataFrame)
  3. y Si volem crear diversos plotbox classificats per un camp categòric com el gènere (si a data hi ha DataFrame)
  4. hue Si volem classificar encara més els boxplots.

Per tant, podem generar diversos plotbox d'una mateixa variable, classificats en camps categòrics (per exemple, un plotbox per homes i un altre per dones).

Ho podreu veure amb un exemple, que no serà l'anterior sinó amb un dels datasets més coneguts del món, el dels passatgers del Titanic, que té un munt de carecterístiques de cadascun

No ens caldrà descarregar-lo perquè Seaborn ja incorpora uns quants dataSets descarregats que podem importar fàcilment a un dataFrame.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

titanic_df = sns.load_dataset("titanic")
print(titanic_df.info())

sns.boxplot(data=titanic_df, x="age", y="class")
plt.title("Distribució edats passatgers/es del Titanic en funció del tipus de bitllet")
plt.show()

Fixeu-vos! Amb només dues linies noves ho hem aconseguit :)

Recompte de variables agrupat per categories amb Seaborn.

Partint del mateix exemple dels passatgers del Titanic i Seaborn; imaginem-nos que volem saber el número de passatgers homes i dones de cada classe.

Com ho fem ? Amb el gràfic de Seaborn catplot, que genera diagrames de barres classificables per diversos criteris (la x i el hue) i que permet fer operacions automàtiques dins del grup amb el paràmetre kind.

# Now let separate the gender by classes passing 'Sex' to the 'hue' parameter
sns.catplot(x='class', data=titanic_df, hue='sex', kind='count')

Teniu més d'exemples d'ús de catplot a la web de Seaborn:

https://seaborn.pydata.org/generated/seaborn.catplot.html

Exercici.Crea un gràfic que mostri quants superivents hi ha hagut de cada classe de bitllet (First,Second,Third)

sns.catplot(x='alive', data=titanic_df, hue='class', kind='count')

Estadística descriptiva: Mesures de correlació entre dues variables.

En moltes ocasions, ens pot interessar analitzar si ha una relació directa o inversa entre 2 variables d'una mostra.

Per exemple entre el temps i les temperatures, entre el pes i l'alçada de persones, l'edat i el nivell de sucre o el pes i el nivell de sucre.

Fins i tot, podem mostrar un mapa de correlacions, entre una variable i les altres variables (sempre i quan siguin quantitatives i del mateix tipus).

Anem a veure-ho amb un exemple senzill, el dataset tips; que conté dades de propines proporcionades per clients d'un bar, on tenim les dades de: preu del menjar(bill), gènere(sex), diners propina (tips), si és dinar o sopar(time), el número de persones(size), etc...

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# Carregar el dataset de propines
tips = sns.load_dataset("tips")

# Seleccionar només les variables numèriques
numeric_tips = tips.select_dtypes(include='number')

# Calcular la matriu de correlació
corr_matrix = numeric_tips.corr()

# Crear el heatmap de correlació
plt.figure(figsize=(10, 6))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', linewidths=0.5)
plt.title('Heatmap de Correlació de Propines')
plt.show()

# Crear el scatter plot
plt.figure(figsize=(10, 6))
sns.scatterplot(data=tips, x='total_bill', y='tip', hue='time')
plt.title('Scatter Plot de Total Bill vs Tip')
plt.xlabel('Total Bill')
plt.ylabel('Tip')
plt.legend(title='Time')
plt.show()

Fixeu-vos amb el mapa; com correlaciona totes les variables mumèriques: total_bill, tips, size:

Podem observar una correlació directa:

  • 0,68 entre el preu del menjar (total_bill) i els diners de propina (tips)

Diem que 2 variables estan fortament correlacionades directament si aquesta correlació és de 0,7 o superior, on el màxim és 1.

Per tant, en el nostre cas obtenim la informació que sovint (no sempre) es compleix com més val el que s'ha menjat més s'agraeix al servei en diners de propina superiors.

Finalment, comentar que hem dibuixat un altre gràfic de punts (scatterplot) que mira si hi ha correlació entre les propines i el preu del menjar, agrupat entre el temps de dinar i de sopar; perquè sempre va bé observar aquests gràfics per fer-nos una idea de la correlació de 2 variables a simple vista.

Seaborn també proporciona un altre tipus de gràfic molt potent per analitzar la distribució i correlació entre les variables, i fins i tot agrupar-les per una variabla categòrica (com poden ser 'Time', 'Sex' o 'Smoker').

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset("tips")

# Crear el pairplot amb línies de millor ajustament
sns.pairplot(tips, kind='reg', hue='time', diag_kind='kde', markers=["o", "s"])
plt.suptitle('Pairplot de Propines amb Línies de Millor Ajustament', y=1.02)
plt.show()

Exercici. Agafa el dataset dels pingüins de Palmer (també inclòs a Seaborn) i dibuixa la matriu de correlació entre totes les 4 variables numèriques. Comenta si has vist una forta correlació entre algún parell de variables.

Completa el codi per aconseguir-ho:

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
df = sns.load_dataset('penguins')

numeric_peng = df.select_dtypes(include='number')
matrix = numeric_peng.corr()
sns.heatmap(matrix, annot=True, vmax=1, vmin=-1, center=0, cmap='vlag')
plt.show()

Hi ha una forta correlació entre la longitud de les aletes i el pes dels pingüins (de 0,87 sobre 1).

Cas pràctic: Les temperatures han pujat a Barcelona degut al canvi climàtic ?

⚠ Aquest exemple encara no ha estat convertit a l’any 2023, és del 2022. Les dades no són les més recents. ⚠

En aquest blog han verificat, creant un gràfic amb una recta de regressió, que les temperatures a Londres han pujat força; mitjançant Pandas i Matplotlib i un fitxer CSV extret d'un portal oficial d'Opendata.

Podeu provar el codi font en el fitxer exemple-templondres.py

# https://readmedium.com/regression-plots-with-pandas-and-numpy-faf2edbfad4f

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

weather = pd.read_csv('https://raw.githubusercontent.com/alanjones2/dataviz/master/londonweather.csv')

#print(weather)
# Each line represents the temperature every month, 7 is july.
july = weather.query('Month == 7')

#Insert new column to make the plot
july.insert(0,'Yr',range(0,len(july)))
#print(july)

#Create basic plot, with max temperatures.
july.plot(y='Tmax',x='Yr',label = "{f}")

#Required to show the plot on screen.
# plt.show()

#In the graph we can't see trend, 
# temperatures do seem to be rising a little, over time.

#we'll show the linear regression to make sure if
#temperatures are rising every year.
#the third parameter is 1, if we want polinomical reg.
#should be 2, 3...

d = np.polyfit(july['Yr'],july['Tmax'],1)
f = np.poly1d(d)

#inserting that into a new column called Treg.
july.insert(6,'Treg',f(july['Yr']))
print(july)

# Next, we create a line plot of Yr against Tmax 
# (the wiggly plot we saw above) and 
# another of Yr against Treg which will be our straight 
# line regression plot. 
# We combine the two plot by assigning the first plot 
# to the variable ax and then passing that to the second plot 
# as an additional axis.

ax = july.plot(x = 'Yr',y='Tmax')
july.plot(x='Yr', y='Treg',color='Red',ax=ax)
plt.show()

Anàlisi dels resultats a Londres

És una barbaritat veure com ha pujat la temperatura el juliol Londres en des de fa 50 anys.

Exercici. Regressió lineal temperatures Barcelona

L'exercici consisteix en que creeu una recta de regressió similar usant una altra font de dades de temperatures. Us proposem el de les temperatures, de l'Ajuntament de Barcelona, actualitzada al 2023.

En aquest cas, la regressió lineal encaixa bastant.

En altres distribucions de dades encaixen les polinòmiques.

Recursos auxiliars:

Observacions: Aquestes mostres segurament s'han tret de l'observatori Fabra i Puig, que té un clima una més suau que al centre. Per consultar la info de qualsevol estació metereològica de Catalunya cal anar a la web oficial portal transparència meteo.cat i fer un tractament previ que és molt entretingut.

Resultats gràfics aproximats.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

weatherBcn = pd.read_csv('./temperaturesbarcelonadesde1780.csv')

## Volem fer l'estudi dels últims 15 anys només.
weatherBcnXXI = weatherBcn.tail(15)
print(weatherBcnXXI)

#Cada columna presenta la Temp_Mitjana. 
#Per a l'estudi, seleccionarem les del julol.
#Fixem-nos com podem seleccionar fàcilment les columnes per la etiqueta de la capçalera.
weatherBcnXXIJune = weatherBcnXXI[['Any','Temp_Mitjana_Juny']]
weatherBcnXXIJuly = weatherBcnXXI[['Any','Temp_Mitjana_Juliol']]
weatherBcnXXIAugust = weatherBcnXXI[['Any','Temp_Mitjana_Agost']]

## Opcional, afegir manualment la temperatura del juiol del 2022.
## insert-> per a columnes, append-> per a files.
## https://datagy.io/pandas-add-row/
## https://www.ccma.cat/el-temps/barcelona-registra-el-segon-juliol-mes-caloros-des-de-1914/noticia/3178369/

# Mètode correcte, però obsolet.
#weatherBcnXXIJuly = weatherBcnXXIJuly.append({'Any':'2022', 'Temp_Mitjana_Juliol':26.8}, ignore_index=True)
print(weatherBcnXXIJuly)

#Ara, creem el gràfic bàsic. 
weatherBcnXXIJuly.plot.scatter(y='Temp_Mitjana_Juliol',x='Any',label = "Temperatures")
plt.show()

#Ara, calculem la recta de regressió lineal.

g = np.polyfit(weatherBcnXXIJuly['Any'],weatherBcnXXIJuly['Temp_Mitjana_Juliol'],1)
#poly1d -> reg.lineal
f = np.poly1d(g)

#Afegirem una columna que ens ajuda a calcular la regressió lineal.
weatherBcnXXIJuly.insert(2,'Treg',f(weatherBcnXXIJuly['Any']))

ax = weatherBcnXXIJuly.plot(x = 'Any',y='Temp_Mitjana_Juliol')
weatherBcnXXIJuly.plot(x='Any', y='Treg', color='Red', ax=ax)
plt.show()

https://github.com/miquelamorosaldev/dawbio2-m14-bioinformatica-curs2022-2023-uf1-uf2/blob/main/Sessi%C3%B315_Estadistica/Estadistica_Rectes_Regressi%C3%B3/exercicis-metereologia.ipynb

Anàlisi resultats temperatures Barcelona.

Com podem veure, té un pendent considerable. La temperatura, en un observatori poc construït, ha pujat 1 grau i unes dècimes en 15 anys.

I si comptessim els resultats d'aquest estiu 2022, encara seria més alt el pendent de la recta.


Histogrames i corbes de distribució normal

Una de les tasques més comuns en estadística (tant descriptiva = dades, com inferencial = probabilitats) és generar gràfics per veure quina distribució presenten les dades un cop sabem la freqüència de cada ocurrència d'una variable; i gràcies a Numpy i Matplotlib podem generar aquest gràfic molt fàcilment.

Una de les distribucions teòriques més utilitzada a la pràctica és la distribució normal, també anomenada distribució gaussiana en honor al matemàtic Carl Friedrich Gauss.

La seva importància es deu fonamentalment a la freqüència amb què diferents variables associades a fenòmens naturals i quotidians segueixen, aproximadament, aquesta distribució; i a la facilitat que aporta per estudiar subconjunts de la població.

Caràcters morfològics (com la talla o el pes) i/o genètics, així com controls de qualitat són exemples de variables de les quals freqüentment s'assumeix que segueixen una distribució normal.

Forma i propietats distribució normal.

  • La mitjana, el mode i la mediana són tots iguals.
  • L'àrea total sota la corba és igual a 1.
  • La corba és simètrica al voltant de la mitjana.
  • És ideal per a representar variables numèriques contínues (com la alçada, la edat de persones...)

D'aquesta corba podem deduïr els següents intèrvals de confiança (IC):

  • El 68% aprox. de les dades es troben dins d'una desviació estàndard de la mitjana.
  • El 95% aprox. de les dades es troben dins de dues desviacions estàndard de la mitjana.
  • El 99,7% aprox. de les dades es troben dins de tres desviacions estàndard de la mitjana.

I per a què ens serveixen aquestes propietats ?

Si volem estudiar probabilitats, ens va bé per deduïr que hi ha un 2,38% de probabilitats (aprox) que un esportista tingui una alçada superior a la mitjana + desviació típica multiplicada per 2.

Ho pots entendre millor veient la corba de distribució:

Si tens curiositat com hem creat la corba, aquí tens el codi:

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# Càlcul del percentatge dins de +2 sigma
mu = 0
sigma = 1
percent_within_2sigma = norm.cdf(mu + 2*sigma, mu, sigma) * 100
print(f"Percentatge des de l'inici fins al +2σ: {percent_within_2sigma:.2f}%")

# Dibuixar una corba de distribució normal estàndard
x = np.linspace(-4, 4, 1000)  # Valors de x des de -4 fins a 4
y = norm.pdf(x, 0, 1)  # Funció de densitat de probabilitat per a una normal estàndard

# Àrea sota la corba des de l'inici fins a 2 sigma
x_fill = np.linspace(-4, 2, 1000)
y_fill = norm.pdf(x_fill, 0, 1)

plt.figure(figsize=(10, 6))
plt.plot(x, y, 'b-', label='Distribució Normal')
plt.fill_between(x_fill, y_fill, 0, alpha=0.3, color='b', label='Àrea fins a 2σ')

plt.title('Corba de Distribució Normal i Àrea fins a 2σ')
plt.xlabel('x')
plt.ylabel('Densitat de probabilitat')
plt.legend()

plt.grid(True)
plt.show()

La segona distribució més comuna és la distribució binomial; que sol donar-se en obtenir mostres del clima (temperatures, pluges...) i també en esdeveniments on intervé l'atzar (l'aletorietat): probabilitat d'obtenir una cara d'un dau, una moneda...

Aprofundir en els tipus de distribucions en estadística queda fora de l'abast d'aquest tutorial de Matplotlib; però si teniu curiositat aquest article aborda les més comuns, i com convertir a distribució normal (normalitzar) mostres, així com representar-ne gràfics amb Python:

  • [https://relopezbriega.github.io/blog/2016/06/29/distribuciones-de-probabilidad-con-python/]

Exemple distribució normal.

Anem a veure com dibuixar un histograma i una corba d'una distribució Normal. que servirà per a comparar si la nostra mostra té una forma semblant a la corba normal.

Per aquest exemple usarem una funció molt útil de Numpy que genera dades que segueixin una distribució normal.

Veiem-ho amb un exemple fictici del nivell de colesterol a la sang de 300 pacients.

import numpy as np
import matplotlib.pyplot as plt

# Paràmetres per a la distribució normal
mean = 200  # mitjana del colesterol (mg/dL)
std_dev = 30  # desviació estàndard (mg/dL)
n_samples = 300  # nombre de mostres

# Generar dades aleatòries amb una distribució normal
cholesterol_levels = np.random.normal(mean, std_dev, n_samples)

# Crear l'histograma
plt.hist(cholesterol_levels, bins=20, density=True, alpha=0.6, color='g', edgecolor='black')

# Crear la corba de distribució normal
xmin, xmax = plt.xlim()
x = np.linspace(xmin, xmax, 100)
p = np.exp(-0.5*((x-mean)/std_dev)**2) / (std_dev * np.sqrt(2 * np.pi))
plt.plot(x, p, 'k', linewidth=2)

# Afegir títol i etiquetes
plt.title('Distribució dels Nivells de Colesterol')
plt.xlabel('Nivells de Colesterol (mg/dL)')
plt.ylabel('Freqüència')

# Afegir línia vertical a la mitjana
plt.axvline(mean, color='b', linestyle='dashed', linewidth=2, label='Mitjana')

# Afegir línies verticals als intervals de confiança (±1 desviació estàndard)
plt.axvline(mean - std_dev, color='r', linestyle='dotted', linewidth=2, label='-1 Desviació estàndard')
plt.axvline(mean + std_dev, color='r', linestyle='dotted', linewidth=2, label='+1 Desviació estàndard')

# Afegir llegenda
plt.legend()

# Mostrar el gràfic
plt.show()

La sintaxis de la funció np.random.normal és:

  1. loc Mitjana aritmètica (mean) de la distribució.
  2. scale Desviació típica (std_dev) de la distribució. Ha de ser no negatiu.
  3. size Tamany (número d'elements) de la mostra

En quant a la sintaxi de plt.hist() remarquem el següent:

  1. data El més important, on li passem les dades.
  2. bins Matplotlib divideix els valors de la variable contínua en 20 intèrvals discrets, en 20 barres (bins=20).
  3. density Les àrees de les barres es normalitzen perquè la suma total sigui 1 (density=True).
  4. color Lletra per representar el color 'g' -> green.
  5. alpha Transparència del color del 60%.
  6. edgecolor Color de les vores.

Us animem a provar aquest diagrama, i a cercar altres exemples. És molt satisfactori realitzar aquest diagrama tan complet amb només 3 llibreries de Python i la comprensió de conceptes estadístics bàsics :)


Distribució Normal Estàndard

El terme distribució normal estàndard, que matemàticament es representa amb aquesta notació N(0,1), es refereix a una distribució normal amb una mitjana de 0 i una desviació estàndard de 1.

Aquesta distribució s'utilitza freqüentment com a referència en estadística, i de fet és comú en diverses aplicacions científiques agafar una mostra de dades i normalitzar-les amb aquesta distribució.

Quan apliques l'estandardització a un conjunt de dades, estàs transformant les dades de manera que tinguin una mitjana de 0 i una desviació estàndard de 1, igual que la distribució normal estàndard. Això es fa mitjançant la fórmula:

𝑍 = (𝑋 − 𝜇) / 𝜎

​On 𝑋 és el valor original, 𝜇 és la mitjana de les dades i 𝜎 és la desviació estàndard.

Per tant, tenint en compte el funcionament de la distribució normal, podem afirmar que el 68% de les dades tindran valors entre -1 i -1, o que el 95% tindran valors entre -2 i +2.

L'estandardització o normalització de z-score és crucial en el camp del machine learning per tal de garantir la equitat en les carecterístiques (features).

Això és degut a que molts algorismes de machine learning, especialment aquells que es basen en la distància, com K-Nearest Neighbors (KNN) i els models de regressió, són sensibles a l'escala de les característiques.

Les característiques amb valors més grans poden dominar aquelles amb valors més petits, conduint a biaixos no desitjats. Per tant, si normalitzem les dades evitem o almenys minimitzem aquests biaixos.

Normalització de dades amb Python i sklearn

I com normalitzem les dades ? Ho veiem amb un exemple molt senzill de dades d'alçades i pesos de diverses persones.

La forma més senzilla d'aplicar l'algorisme per normalitzar (n'incorpora diversos) és ajudar-nos de la llibreria de Machine Learning sklearn.

Aquesta et permet aplicar diferents mètodes per normalitzar (escalar en anglès) les dades. Nosaltres usem StandardScaler que aplica el mètode de normalització basat en la campana de Gauss, que hem vist anteriorment.

Un altre mètode habitual, i que és més simple, és l'escalat lineal (LinealScaler). Per usar-lo hem d'importar LinealScaler.

L'escalament lineal es una bona opció quan es compleixen aquestes condicions:

  • LLímits inferior y superior canvien poc amb el temps.
  • L'atribut conté pocs valors atípics o cap.
  • No hi ha cap o gairebé cap valor extrem (outliers)

Altrament, utilitzarem l'escalament normal (o Z-Score).

Per exemple, per a l'edat ens aniria bé l'escalament lineal (ja que en molts experiments no canvien) i el pes encaixarà millor amb l'escalament normal, ja que podem trobar-nos canvis, algún valor extrem, i encaixa molt bé amb la distribució normal.

import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler

data = {
    'alçada_cm': [165, 170, 165, 180, 195, 175, 160],
    'pes_kg': [70, 80, 65, 80, 75, 75, 60]
}
df = pd.DataFrame(data)

# Mostrar el dataframe original
print("Dades originals:")
print(df)

# Inicialitzar l'escalador
scaler = StandardScaler()

# Ajustar i transformar les dades
df_scaled = scaler.fit_transform(df)

# Convertir les dades escalades a un dataframe
df_scaled = pd.DataFrame(df_scaled, columns=['alçada_cm', 'pes_kg'])

# Mostrar les dades escalades
print("\nDades estandarditzades:")
print(df_scaled)
Dades originals:
   alçada_cm  pes_kg
0        165      70
1        170      80
2        165      65
3        180      80
4        195      75
5        175      75
6        160      60

Dades estandarditzades:
   alçada_cm    pes_kg
0  -0.716039 -0.306186
1  -0.260378  1.122683
2  -0.716039 -1.020621
3   0.650945  1.122683
4   2.017928  0.408248
5   0.195283  0.408248
6  -1.171700 -1.735055

Val, però i si no necessitem/volem importar sklearn ? Doncs és més senzill del que sembla gràcies a Pandas.

Simplement, hem de reemplaçar el codi de l'scaler per aquest:

means = df.mean()
stds = df.std()
# Aplicar l'estandardització manualment
df_scaled = (df - means) / stds

Exercici lliure: Prova d'afegir altres dades numèriques al dataframe com l'edat o la pressió arterial i analitza els resultats.