In [4]:
%matplotlib inline
import random
import numpy as np
import scipy as sc
import scipy.stats as st

import pandas as pd

import seaborn as sns
import matplotlib.pyplot as plt

Datos de oleaje y viento y normal 2D

Vamos a examinar datos de viento y oleaje de una boya cerca del puerto de Cádiz.

  • Hm0 : Altura significante Espectral (m)
  • DirM : Direccion Media de PROCEDENCIA del Oleaje (0=N,90=E)
  • VelV : Velocidad Media del Viento (m/s)
  • DirV : Direccion Media de PROCEDENCIA del Viento (0=N,90=E)

También tiene otros datos que vamos a ignorar porque no están presentes o no nos interesan.

In [6]:
simar=pd.read_csv('/home/OYE/SIMAR_6006052.csv')
simar.head()
Out[6]:
AA MM DD HH Hm0 Tm02 Tp DirM Hm0_V DirM_V Hm0_F1 Tm02_F1 DirM_F1 Hm0_F2 Tm02_F2 DirM_F2 VelV DirV
0 2005 12 13 15 0.3 10.9 12.1 242.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 2005 12 13 18 0.3 10.3 12.0 237.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 2005 12 13 21 0.3 10.3 11.8 238.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 2005 12 14 0 0.3 10.7 11.6 241.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4 2005 12 14 3 0.3 10.6 11.4 239.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
In [7]:
simar.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100196 entries, 0 to 100195
Data columns (total 18 columns):
AA         100196 non-null int64
MM         100196 non-null int64
DD         100196 non-null int64
HH         100196 non-null int64
Hm0        100196 non-null float64
Tm02       100196 non-null float64
Tp         100184 non-null float64
DirM       100196 non-null float64
Hm0_V      0 non-null float64
DirM_V     0 non-null float64
Hm0_F1     0 non-null float64
Tm02_F1    0 non-null float64
DirM_F1    0 non-null float64
Hm0_F2     0 non-null float64
Tm02_F2    0 non-null float64
DirM_F2    0 non-null float64
VelV       99977 non-null float64
DirV       99977 non-null float64
dtypes: float64(14), int64(4)
memory usage: 13.8 MB
In [8]:
simar.describe()
Out[8]:
AA MM DD HH Hm0 Tm02 Tp DirM Hm0_V DirM_V Hm0_F1 Tm02_F1 DirM_F1 Hm0_F2 Tm02_F2 DirM_F2 VelV DirV
count 100196.000000 100196.000000 100196.000000 100196.000000 100196.000000 100196.000000 100184.000000 100196.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 99977.000000 99977.000000
mean 2011.602908 6.546669 15.736287 11.482375 0.746351 6.160956 6.730709 237.458621 NaN NaN NaN NaN NaN NaN NaN NaN 5.258193 198.900567
std 3.515889 3.489739 8.798571 6.922825 0.509752 2.012358 3.277458 32.843891 NaN NaN NaN NaN NaN NaN NaN NaN 2.765232 101.159532
min 2005.000000 1.000000 1.000000 0.000000 0.000000 0.400000 2.100000 1.000000 NaN NaN NaN NaN NaN NaN NaN NaN 0.000000 0.000000
25% 2009.000000 4.000000 8.000000 6.000000 0.400000 4.600000 4.300000 221.000000 NaN NaN NaN NaN NaN NaN NaN NaN 3.200000 110.000000
50% 2011.000000 7.000000 16.000000 12.000000 0.600000 5.600000 5.200000 249.000000 NaN NaN NaN NaN NaN NaN NaN NaN 4.800000 221.000000
75% 2015.000000 10.000000 23.000000 18.000000 0.900000 7.400000 9.000000 261.000000 NaN NaN NaN NaN NaN NaN NaN NaN 6.900000 288.000000
max 2018.000000 12.000000 31.000000 23.000000 5.900000 16.800000 20.900000 342.000000 NaN NaN NaN NaN NaN NaN NaN NaN 20.600000 360.000000

Histogramas de altura de ola y velocidad del viento

In [10]:
plt.figure(figsize=(9,9))
plt.hist(simar['Hm0'], bins=200)
plt.xlabel('Hm0')
plt.title('altura media de ola Hm0')
plt.show()
In [11]:
#Tenemos que quitar primero los datos NA (Not Available)
notnas = simar['VelV'].notna()
velV_sin_na = simar['VelV'][notnas]
#en vez del histograma de la librería matplotlib,
#usamos el distplot de la librería "seaborn"
sns.distplot(velV_sin_na)
Out[11]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2efd36e048>

No parecen normales:

  • no son simétricas
  • sólo toman valores positivos

Distribución de la velocidad del viento

Dibujamos el histograma de velocidad del viento junto con la función de densidad de la Normal con la misma media y desviación típica.

Recordamos que la normal que mejor ajusta a los datos en el sentido de "máxima verosimilitud" es la $\operatorname{Normal}(\mu, \sigma^2)$ que tiene su parámetro $\mu$ igual a la media muestral: $$ \bar{x} = \frac{1}{n}\sum_{j=1}^n x_j $$ y su parámetro $\sigma^2$ igual a la varianza muestral: $$ \hat{\sigma}^2 = \frac{1}{n}\sum_{j=1}^n (x_j - \bar{x})^2 $$

In [12]:
VelV = simar['VelV'][simar['VelV'].notna()]
meanV, stdV = VelV.mean(), VelV.std()

plt.figure(figsize=(16,12))

plt.hist(VelV, bins=30, density=1,alpha=0.9)

xs = np.arange(meanV-3*stdV,meanV+3*stdV,0.01)
N = st.norm(loc=meanV, scale=stdV)
ys = N.pdf(xs)
lines = plt.plot(xs,ys, lw=3)
plt.xlabel('VelV')
plt.show()

La distribución de Rayleigh ajusta mucho mejor, si escogemos el parámetro adecuadamente. Para la distribución de Rayleigh, el valor de máxima verosimilitud para el parámetro de escala es:

$${\displaystyle {\widehat {\sigma }}\approx {\sqrt {{\frac {1}{2N}}\sum _{i=1}^{N}x_{i}^{2}}}}$$
In [13]:
plt.figure(figsize=(16,12))

plt.hist(VelV, bins=50, density=1,alpha=0.9, label='Velocidad del viento')

scale_data = np.sqrt((VelV**2).sum()/(2*len(VelV)))
R = st.rayleigh(scale=scale_data)

xs = np.arange(meanV-3*stdV,meanV+5*stdV,0.01)
ys = R.pdf(xs)
lines = plt.plot(xs,ys, lw=3, label='Rayleigh(%.4f)'%scale_data)
plt.legend()
plt.xlabel('VelV')
plt.show()

El resultado es igual de revelador usando la función de distribución empírica

In [17]:
def dibuja_distribucion_empirica(muestra, dist=None):
    mean, std = dist.mean(), dist.std()
    minx = min(min(muestra)-1, mean-3*std)
    maxx = max(max(muestra)+1, mean+3*std)
    xs = np.concatenate([[minx], sorted(muestra), [maxx]])
    pasoy = 1/len(muestra)
    ys = np.concatenate( [np.arange(0,1,pasoy), [1,1]])
    plt.step(xs, ys, where='post', label='cdf empírica')
    if dist:
        xs = np.arange(minx,maxx,std*0.01)
        ys = dist.cdf(xs)
        plt.plot(xs,ys, label = 'cdf')
    plt.legend()

Comparamos con la normal

In [19]:
data = simar['VelV'][simar['VelV'].notna()]
mean, std = data.mean(), data.std()
N = st.norm(loc=mean, scale=std)
dibuja_distribucion_empirica(data,N)
plt.xlabel('VelV')
plt.show()

Comparamos con la distribución de Rayleigh

In [21]:
data = simar['VelV'][simar['VelV'].notna()]
mean, std = data.mean(), data.std()
R = st.rayleigh(scale=scale_data)
dibuja_distribucion_empirica(data,R)
plt.xlabel('VelV')
plt.show()

Maximum likelihood con st.rayleigh.fit(data, floc=0)

En lugar de usar las fórmulas de la tabla (o de la wikipedia, o de cualquier libro de texto), podemos encontrar los valores de los parámetros que maximizan la verosimilitud numéricamente, usando la sintaxis:

loc0, scale0 = st.rayleigh.fit(VelV, floc=0)

Donde:

  • loc y scale son los dos parámetros que scipy.stats reconoce para scipy.stats.rayleigh
  • floc=0 indica que queremos fijar loc a 0, porque no queremos que la desplace.
  • VelV son los datos
In [22]:
#Obtenemos un valor muy similar al anterior
st.rayleigh.fit(VelV, floc=0)
Out[22]:
(0, 4.2009397077443325)
In [23]:
scale_data
Out[23]:
4.200894408120785

Distribución de la altura del oleaje

In [24]:
HM = simar['Hm0'][simar['Hm0'].notna()]
meanH, stdH = HM.mean(), HM.std()
In [25]:
plt.figure(figsize=(8,6))

plt.hist(HM, bins=50, density=1,alpha=0.9)

xs = np.arange(meanH-3*stdH,meanH+5*stdH,0.01)
loc0, scale0 = st.rayleigh.fit(HM, floc=0)
R = st.rayleigh(scale=scale0)
ys = R.pdf(xs)
lines = plt.plot(xs,ys, lw=3)

plt.xlabel('Hm0')
plt.show()
In [26]:
R = st.rayleigh(scale=meanH/np.sqrt(np.pi/2))
dibuja_distribucion_empirica(HM,R)

plt.xlabel('Hm0')
plt.show()

La distribución de Rayleigh ajusta mejor que una normal, pero no ajusta tan bien como lo hacía para la velocidad del viento. Se suele modelizar como una distribución de Weibull, que es algo más difícil de ajustar:

  • En la wikipedia puedes leer que no se conoce una expresión cerrada para el estimador de máxima verosimilitud.
  • El método st.weibull_min.fit(HM, floc=0) devuelve unos valores muy malos de los parámetros, porque el método fit necesita un punto de partida decente. Cualquier número c_ini mayor que uno es un buen punto de partida para st.weibull_min.fit(HM, c_ini, floc=0)
In [29]:
c_ini = 2
c0, loc0, scale0 = st.weibull_min.fit(HM, c_ini, floc=0)
print(c0, loc0, scale0)
1.6327028875202405 0 0.8414213874326455
In [30]:
W = st.weibull_min(c=c0, loc=loc0, scale=scale0)
dibuja_distribucion_empirica(HM,W)

plt.xlabel('Hm0')
plt.show()

En este caso concreto tampoco ajusta mucho mejor que una Rayleigh :-/

Relaciones entre variables

Una correlación interesante: ¿a mayor velocidad del viento mayor altura del oleaje?

In [31]:
simar['VelV'].mean(), simar['Hm0'].mean()
Out[31]:
(5.25819338447843, 0.7463511517425845)
In [32]:
simar.plot.scatter(y = 'Hm0', x = 'VelV', s=10, alpha=0.1)
Out[32]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2ed967c278>

Observamos correlación positiva entre las variables.

In [33]:
simar[['Hm0','VelV']].corr()
Out[33]:
Hm0 VelV
Hm0 1.000000 0.615358
VelV 0.615358 1.000000

Intentamos ajustar el vector (Altura significativa, velocidad del viento) por una normal multivariante...

In [34]:
percentiles_canonicos = [
    1-2*st.norm(loc=0,scale=1).cdf(-k) for k in (1,2,3)
]

def plotMN(MN, sample_size=1000, npoints=100):
    #Muestra aleatoria de la normal multivariable
    means, Sigma = MN.mean, MN.cov
    # Create grid coordinates for plotting
    std1, std2 = np.sqrt([Sigma[0,0], Sigma[1,1]])
    X1 = np.linspace(means[0] - 4*std1, means[0] + 4*std1, npoints)
    X2 = np.linspace(means[1] - 4*std2, means[1] + 4*std2, npoints)
    xx, yy = np.meshgrid(X1,X2, indexing='xy')
    Z = np.zeros((X1.size,X2.size))

    for i,x1i in enumerate(X1):
        for j, x2j in enumerate(X2):
            Z[j,i] = MN.pdf([x1i,x2j])

    #Queremos dibujar exactamente los contornos que capturan 
    # los percentiles habituales (~68%,95%,99%):
    #https://en.wikipedia.org/wiki/Multivariate_normal_distribution#Interval
    factor = 1/(2*np.pi*np.sqrt(np.linalg.det(Sigma)))
    E = st.expon(scale=2).ppf
    levels = [
        factor*np.exp(-0.5*E(p))
        for p in reversed(percentiles_canonicos)
    ]

    plt.contour(xx, yy, Z, cmap=plt.cm.Set2_r, levels=levels)
    plt.contourf(xx, yy, Z, cmap=plt.cm.Set2_r, alpha=0.4, levels=levels)

La normal multivariante que mejor ajusta a los datos es la que tiene parámetro $\mathbf{\mu}$ igual a la media muestral $\bar{\mathbf{x}}=\frac{1}{n}\sum_{j=1}^n \mathbf{x}_j$ y parámetro $\Sigma$ igual a la matriz de covarianzas de la muestra $\frac{1}{n}\sum_{j=1}^n (\mathbf{x}_j-\bar{\mathbf{x}})^t\cdot (\mathbf{x}_j-\bar{\mathbf{x}})$.

El conjunto de puntos (Altura significativa, velocidad del viento) no parece sacado de una normal multivariante.

In [35]:
data = simar[['Hm0','VelV']]
means = data.mean()
Sigma = data.cov()
print(means)
print(Sigma)
MN = st.multivariate_normal(mean=means, cov=Sigma)
fig = plt.figure(figsize=(9,9))
fig.suptitle('Altura significativa vs velocidad del viento', fontsize=20)
plotMN(MN, npoints=200)
plt.scatter(data[data.columns[0]], data[data.columns[1]], s=6, alpha=0.3)
plt.xlabel('Hm0')
plt.ylabel('VelV')
plt.show()
Hm0     0.746351
VelV    5.258193
dtype: float64
           Hm0      VelV
Hm0   0.259847  0.867027
VelV  0.867027  7.646506

Descomponemos el viento en componentes

Según la wikipedia:

One example where the Rayleigh distribution naturally arises is when wind velocity is analyzed into its orthogonal 2-dimensional vector components. Assuming that each component is uncorrelated, normally distributed with equal variance, and zero mean, then the overall wind speed (vector magnitude) will be characterized by a Rayleigh distribution.

Descomponemos el viento en sus componentes NS y EW, y vemos que la distribución se parece razonablemente a una normal multivariante, cuyos parámetros hemos encontrado igual que antes.

In [39]:
VNS_EW = pd.DataFrame({
        'VEW': np.sin(simar['DirV']*np.pi/180)*simar['VelV'],
        'VNS': np.cos(simar['DirV']*np.pi/180)*simar['VelV'],
    })
data = VNS_EW
means = data.mean()
Sigma = data.cov()
print(means)
print(Sigma)
MN = st.multivariate_normal(mean=means, cov=Sigma)
fig = plt.figure(figsize=(9,9))
fig.suptitle('Componentes NS-EW del viento', fontsize=20)
plotMN(MN, npoints=200)
plt.scatter(data['VEW'], data['VNS'], s=4, alpha=0.1)

plt.xlabel('VelV (E-W)')
plt.ylabel('VelV (N-S)')
plt.show()
VEW   -0.458743
VNS    0.006280
dtype: float64
           VEW        VNS
VEW  22.397501  -3.780273
VNS  -3.780273  12.687393

Sin embargo, para los datos de oleaje, para los que los datos de altura significativa de la ola no sigue una distribución de Rayleigh, la normal multivariante no es una buena aproximación para las componentes direccionales.

El mecanismo que genera este fenómeno es otro...

In [40]:
HNS_EW = pd.DataFrame({
        'HNS': np.cos(simar['DirM']*np.pi/180)*simar['Hm0'],
        'HEW': np.sin(simar['DirM']*np.pi/180)*simar['Hm0'],
    })
data = HNS_EW
means = data.mean()
Sigma = data.cov()
print(means)
print(Sigma)
MN = st.multivariate_normal(mean=means, cov=Sigma)
fig = plt.figure(figsize=(9,9))
fig.suptitle('Componentes NS-EW del oleaje', fontsize=20)
plotMN(MN, npoints=200)
plt.scatter(data['HEW'], data['HNS'], s=4, alpha=0.1)

plt.xlabel('Hm0 (E-W)')
plt.ylabel('Hm0 (N-S)')
plt.show()
HEW   -0.552425
HNS   -0.342356
dtype: float64
          HEW       HNS
HEW  0.242937  0.056041
HNS  0.056041  0.151571

Estudiamos las correlaciones entre las componentes del viento y el oleaje. Aunque la normal multivariante no es un buen modelo para el oleaje, hay correlaciones importantes entre las componentes NS del viento y el oleaje, y no las hay entre las componentes NS de una y EW de la otra.

In [46]:
data = pd.DataFrame({
        'HNS': np.cos(simar['DirM']*np.pi/180)*simar['Hm0'],
        'VNS': np.cos(simar['DirV']*np.pi/180)*simar['VelV'],
        'HEW': np.sin(simar['DirM']*np.pi/180)*simar['Hm0'],
        'VEW': np.sin(simar['DirV']*np.pi/180)*simar['VelV'],
    })
print(data.corr())
          HEW       HNS       VEW       VNS
HEW  1.000000  0.292045  0.656420 -0.101973
HNS  0.292045  1.000000 -0.216478  0.609942
VEW  0.656420 -0.216478  1.000000 -0.224252
VNS -0.101973  0.609942 -0.224252  1.000000

Es una pena que los datos de esta boya no traigan el oleaje desglosado en "mar de viento" y "mar de fondo". En fin, podríamos seguir afinando, pero lo vamos a dejar aquí...

Ejercicio

Ajusta una normal multivariante a los datos:

  • Tm02 : Periodo Medio Espectral Momentos 0 y 2 (s)
  • Tp : Periodo de pico espectral (s)

Preguntas

  • ¿Hay correlación, positiva o negativa, entre ambas variables?
  • ¿Es la normal multivariante un buen modelo?
In [ ]:
data = simar[['Tp', 'Tm02']]
data.mean()
In [ ]:
 
In [ ]:
 

Ejercicio

Ajusta una normal multivariante a los datos:

  • log(Tm02) : logaritmo del Periodo Medio Espectral Momentos 0 y 2 (s)
  • log(Tp) : logaritmo del Periodo de pico espectral (s)

Preguntas

  • ¿Hay correlación, positiva o negativa, entre ambas variables?
  • ¿Es la normal multivariante un buen modelo?
In [ ]:
logTpTm = pd.DataFrame({
        'logTp': np.log(simar['Tp']),
        'logTm': np.log(simar['Tm02']),
    })
logTpTm.mean()
In [ ]:
 
In [ ]: