Ejercicio PCA - acciones

Ejercicio PCA - acciones#

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

Descargar precios de acciones con diferentes características.

acciones = [
    # 🟢 Acciones de bajo Beta (< 0.8) — defensivas
    'JNJ',   # Johnson & Johnson
    'PG',    # Procter & Gamble
    'KO',    # Coca-Cola
    'PEP',   # PepsiCo
    'WMT',   # Walmart

    # 🟡 Acciones de Beta cercano a 1 (≈ 0.9 - 1.1) — mercado promedio
    'AAPL',  # Apple
    'MSFT',  # Microsoft
    'V',     # Visa
    'MA',    # Mastercard
    'UNH',   # UnitedHealth

    # 🔴 Acciones de Beta alto (> 1.2) — más volátiles que el mercado
    'TSLA',  # Tesla
    'NVDA',  # Nvidia
    'META',  # Meta (Facebook)
    'AMZN',  # Amazon
    'NFLX',  # Netflix

    # 🔁 Adicionales mixtas para aumentar diversidad
    'GOOGL', # Alphabet (Google)
    'AMD',   # Advanced Micro Devices
    'CRM',   # Salesforce
    'BA',    # Boeing
    'NKE'    # Nike
]

indice = '^GSPC'  # S&P500

datos = yf.download(acciones + [indice], start='2020-07-01', end='2025-07-31', interval='1mo')['Close']
datos.dropna(inplace=True)

datos.describe()
/tmp/ipython-input-4028798923.py:33: FutureWarning: YF.download() has changed argument auto_adjust default to True
  datos = yf.download(acciones + [indice], start='2020-07-01', end='2025-07-31', interval='1mo')['Close']
[*******************100%*********************]  21 of 21 completed
Ticker AAPL AMD AMZN BA CRM GOOGL JNJ KO MA META ... NFLX NKE NVDA PEP PG TSLA UNH V WMT ^GSPC
count 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 ... 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000 61.000000
mean 168.308615 111.604426 157.696810 192.557214 234.247294 130.157316 149.466882 56.029760 398.977761 351.281430 ... 555.438689 106.611316 53.068235 149.308490 141.179571 246.167060 444.539696 241.869444 56.422426 4591.564901
std 37.929746 32.599924 35.406763 31.330118 48.640246 32.428267 10.006478 8.197990 82.631453 165.833288 ... 256.862147 26.270569 47.606693 17.191153 16.928583 67.619500 87.096891 50.515680 18.549609 813.091153
min 103.174973 60.060001 84.000000 121.080002 131.438278 72.843140 119.756897 40.592751 279.303253 92.651718 ... 174.869995 56.027664 10.579879 112.904655 110.428238 95.384003 249.559998 173.635727 38.834499 3269.959961
25% 138.524902 85.519997 133.089996 171.820007 201.018570 103.111603 144.014450 50.373863 346.991150 252.285919 ... 394.519989 88.638336 16.206352 134.108490 127.865417 201.880005 386.012970 206.614471 44.324261 4076.600098
50% 167.575714 102.900002 160.309998 194.190002 236.031769 131.928772 151.222504 56.363876 364.622620 316.861664 ... 517.570007 105.146385 27.729065 153.950912 140.274200 240.080002 472.730499 225.511032 47.488297 4395.259766
75% 191.829453 137.179993 176.759995 212.009995 267.514801 154.275345 155.431381 59.936260 447.335083 473.209625 ... 641.619995 125.442932 90.315811 161.462906 155.858002 282.160004 498.170715 269.519684 59.013657 5254.350098
max 249.534180 192.529999 237.679993 260.660004 340.623810 203.538910 164.740005 72.037811 584.808716 773.440002 ... 1339.130005 160.342422 177.869995 177.486206 175.867111 404.600006 601.015259 363.944122 98.252411 6339.390137

8 rows × 21 columns

datos.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 61 entries, 2020-07-01 to 2025-07-01
Data columns (total 21 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   AAPL    61 non-null     float64
 1   AMD     61 non-null     float64
 2   AMZN    61 non-null     float64
 3   BA      61 non-null     float64
 4   CRM     61 non-null     float64
 5   GOOGL   61 non-null     float64
 6   JNJ     61 non-null     float64
 7   KO      61 non-null     float64
 8   MA      61 non-null     float64
 9   META    61 non-null     float64
 10  MSFT    61 non-null     float64
 11  NFLX    61 non-null     float64
 12  NKE     61 non-null     float64
 13  NVDA    61 non-null     float64
 14  PEP     61 non-null     float64
 15  PG      61 non-null     float64
 16  TSLA    61 non-null     float64
 17  UNH     61 non-null     float64
 18  V       61 non-null     float64
 19  WMT     61 non-null     float64
 20  ^GSPC   61 non-null     float64
dtypes: float64(21)
memory usage: 10.5 KB

Variables:#

Se usaran indicadores financieros para agrupar a las acciones:

  • Rendimiento medio mensual.

  • Volatilidad mensual.

  • Asimetría (Skewness).

  • Curtosis.

  • Coeficiente Beta: mide la sensibilidad del rendimiento de una acción frente a los movimientos del mercado, indicando cuánto tiende a variar la acción en relación con el índice de referencia.

def calcular_indicadores(serie_accion, serie_indice):
    retornos = serie_accion.pct_change().dropna()
    beta = np.cov(retornos, serie_indice.pct_change().dropna())[0, 1] / np.var(serie_indice.pct_change().dropna())
    return {
        'Retorno': retornos.mean(),
        'Volatilidad': retornos.std(),
        'Skewness': retornos.skew(),
        'Kurtosis': retornos.kurt(),
        'Beta': beta
    }

caracteristicas = []
for accion in acciones:
    caracteristicas.append(calcular_indicadores(datos[accion], datos[indice]))

df_indicadores = pd.DataFrame(caracteristicas, index=acciones)

df_indicadores.describe()
Retorno Volatilidad Skewness Kurtosis Beta
count 20.000000 20.000000 20.000000 20.000000 20.000000
mean 0.016279 0.092425 0.051634 0.542927 1.153155
std 0.013544 0.041562 0.562000 1.361422 0.589540
min 0.001234 0.045147 -1.143828 -0.827875 0.392903
25% 0.008797 0.062848 -0.288277 -0.358962 0.615435
50% 0.012891 0.079984 0.123930 -0.230196 1.125789
75% 0.020063 0.119030 0.306699 1.171872 1.420758
max 0.058650 0.203409 1.035206 4.428854 2.369970

Matriz de correlación:

# Matriz de correlación entre las variables:
import seaborn as sns

plt.figure(figsize=(8, 6))
sns.heatmap(df_indicadores.corr(), annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
plt.title('Mapa de Calor de Correlaciones de Indicadores')
plt.show()
../../../_images/output_9_04.png

PCA:#

# Escalado de datos:
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(df_indicadores)
from sklearn.decomposition import PCA

# Aplicación de PCA estándar
pca = PCA()
pca.fit(X_scaled)

# Cálculo de las varianzas explicadas
explained_variance = pca.explained_variance_ratio_

print("Varianza explicada por cada componente principal:")
print(explained_variance)

# Cálculo de la varianza explicada acumulada
explained_variance_cum = np.cumsum(pca.explained_variance_ratio_)

# Visualización del gráfico de varianza explicada
plt.figure(figsize=(8, 6))
plt.plot(
    range(1, len(explained_variance_cum) + 1),
    explained_variance_cum,
    marker="o",
    linestyle="--",
)
plt.xlabel("Número de Componentes Principales")
plt.ylabel("Varianza Explicada Acumulada")
plt.title("Gráfico de Varianza Explicada Acumulada")
plt.grid()
plt.show()
Varianza explicada por cada componente principal:
[0.54110558 0.24154776 0.16562478 0.04064715 0.01107473]
../../../_images/output_12_13.png

Cargas:

El análisis de cargas factoriales muestra que la Componente 1 está fuertemente asociada con retorno, volatilidad y beta, lo que sugiere que esta dimensión captura principalmente la dinámica conjunta entre rendimiento y riesgo sistemático de los activos.

Por su parte, la Componente 2 presenta una elevada correlación con asimetría y curtosis, aunque en el caso de la curtosis la relación es de signo inverso. Esto indica que esta componente refleja patrones de distribución no normales en los retornos (asimetrías y concentraciones de probabilidad en las colas).

En conjunto, las dos primeras componentes explican aproximadamente el 78% de la varianza total de los datos, lo cual evidencia una representación adecuada de la estructura de las variables originales. De esta manera, las cinco variables se encuentran bien representadas en las dos primeras dimensiones.

Cabe resaltar que en la Componente 3, tanto la asimetría como la curtosis exhiben una correlación positiva de magnitud moderada, lo que sugiere la existencia de un factor adicional que captura ciertos rasgos residuales de la forma de la distribución.

loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
loadings_df = pd.DataFrame(
    loadings,
    columns=[f"PC{i+1}" for i in range(df_indicadores.shape[1])],
    index=df_indicadores.columns,
)
loadings_df
PC1 PC2 PC3 PC4 PC5
Retorno 0.856260 0.063447 -0.457865 0.324702 -0.018812
Volatilidad 0.987746 -0.052716 0.090212 -0.197655 -0.164328
Skewness 0.212222 0.805643 0.584004 0.131967 -0.007552
Kurtosis 0.324036 -0.780401 0.559344 0.159228 0.019692
Beta 0.994516 0.080084 -0.002253 -0.163293 0.174601

Componentes: 2#

# Aplicación de PCA estándar
num_components = 2
pca = PCA(n_components=num_components)
X_pca = pca.fit_transform(X_scaled)

# Varianza explicada:
explained_variance_ratio = pca.explained_variance_ratio_
cumulative_variance = np.cumsum(explained_variance_ratio)
cumulative_variance
array([0.54110558, 0.78265334])

Matríz de rotación:

rotation_matrix = pd.DataFrame(
    pca.components_.T,
    columns=[f"PC{i+1}" for i in range(num_components)],
    index=df_indicadores.columns,
)

print(rotation_matrix)
                  PC1       PC2
Retorno      0.507389  0.056272
Volatilidad  0.585304 -0.046753
Skewness     0.125755  0.714526
Kurtosis     0.192012 -0.692138
Beta         0.589315  0.071027

Cargas de las variables:

loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
loadings_df = pd.DataFrame(
    loadings,
    columns=[f"PC{i+1}" for i in range(num_components)],
    index=df_indicadores.columns,
)
loadings_df
PC1 PC2
Retorno 0.856260 0.063447
Volatilidad 0.987746 -0.052716
Skewness 0.212222 0.805643
Kurtosis 0.324036 -0.780401
Beta 0.994516 0.080084

Cálculo de la matriz de proyección:

projected_data = X_scaled @ pca.components_.T
projected_df = pd.DataFrame(
    projected_data,
    columns=[f"PC{i+1}" for i in range(num_components)],
    index=df_indicadores.index,
)
projected_df
PC1 PC2
JNJ -2.009927 0.571023
PG -1.862686 0.698475
KO -1.628735 -0.195235
PEP -1.904777 0.749386
WMT -1.077136 -0.514352
AAPL -0.340766 0.870143
MSFT -0.463062 0.726591
V -0.846577 0.742175
MA -0.636976 0.384738
UNH -1.443072 -2.101111
TSLA 4.017179 0.810890
NVDA 3.314539 0.545678
META 0.854471 -1.035090
AMZN -0.003152 0.022272
NFLX 1.473487 -3.536336
GOOGL -0.469659 -0.278302
AMD 1.848918 1.107993
CRM 0.695677 0.488226
BA 1.065754 -0.027128
NKE -0.583498 -0.030033
X_pca
array([[-2.00992661e+00,  5.71023056e-01],
       [-1.86268621e+00,  6.98474566e-01],
       [-1.62873530e+00, -1.95235456e-01],
       [-1.90477735e+00,  7.49385720e-01],
       [-1.07713606e+00, -5.14352008e-01],
       [-3.40766071e-01,  8.70143063e-01],
       [-4.63062470e-01,  7.26590620e-01],
       [-8.46576773e-01,  7.42174674e-01],
       [-6.36975870e-01,  3.84738442e-01],
       [-1.44307229e+00, -2.10111131e+00],
       [ 4.01717943e+00,  8.10889523e-01],
       [ 3.31453916e+00,  5.45678433e-01],
       [ 8.54470784e-01, -1.03509043e+00],
       [-3.15242929e-03,  2.22716935e-02],
       [ 1.47348699e+00, -3.53633606e+00],
       [-4.69659069e-01, -2.78301948e-01],
       [ 1.84891788e+00,  1.10799255e+00],
       [ 6.95676720e-01,  4.88225688e-01],
       [ 1.06575383e+00, -2.71275852e-02],
       [-5.83498308e-01, -3.00332323e-02]])
plt.figure(figsize=(10, 6))

# Gráfico de las observaciones (acciones)
plt.scatter(X_pca[:, 0], X_pca[:, 1], c='blue')

# Etiquetas de las observaciones (acciones)
for i, label in enumerate(df_indicadores.index):
    plt.annotate(label, (X_pca[i, 0], X_pca[i, 1]),
                 fontsize=8, alpha=0.7)

# Añadir las cargas (loadings) como flechas rojas
for i, var in enumerate(df_indicadores.columns):
    plt.arrow(0, 0, loadings[i, 0], loadings[i, 1],
              color='crimson', alpha=0.8,
              head_width=0.05, length_includes_head=True)
    plt.text(loadings[i, 0]*1.4, loadings[i, 1]*1.1, var,
             color='crimson', ha='center', va='center', fontsize=9)

# Configuración del gráfico
plt.title('PCA de Indicadores Financieros (Biplot)')
plt.xlabel(f'Componente Principal 1 ({explained_variance_ratio[0]*100:.1f}%)')
plt.ylabel(f'Componente Principal 2 ({explained_variance_ratio[1]*100:.1f}%)')
plt.axhline(0, color='gray', lw=1)
plt.axvline(0, color='gray', lw=1)
plt.grid(True)
plt.tight_layout()
plt.show()
../../../_images/output_24_0.png

Dato nuevo:#

# Nueva acción:
new_data = yf.download(["DIS"], start='2020-07-01', end='2025-07-31', interval='1mo')['Close']
new_data.dropna(inplace=True)
/tmp/ipython-input-1044502549.py:2: FutureWarning: YF.download() has changed argument auto_adjust default to True
  new_data = yf.download(["DIS"], start='2020-07-01', end='2025-07-31', interval='1mo')['Close']
[*******************100%*********************]  1 of 1 completed
# Calcular indicadores con la misma función:
indicadores_new = calcular_indicadores(new_data["DIS"], datos[indice])

# Convertir a DataFrame para que tenga el mismo formato:
df_new = pd.DataFrame([indicadores_new], index=["DIS"])
df_new
Retorno Volatilidad Skewness Kurtosis Beta
DIS 0.00573 0.104237 0.635961 -0.016636 1.59273
# Escalar con el scaler ya entrenado en tus datos originales:
X_new_scaled = scaler.transform(df_new)

# Proyectar en el espacio PCA ya entrenado:
X_new_pca = pca.transform(X_new_scaled)

print(f"Coordenadas de {"DIS"} en PC1 y PC2:", X_new_pca)
Coordenadas de DIS en PC1 y PC2: [[0.2691755  1.04981175]]
plt.figure(figsize=(10, 6))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c='blue', label='Acciones originales')

for i, label in enumerate(df_indicadores.index):
    plt.annotate(label, (X_pca[i, 0], X_pca[i, 1]), fontsize=8, alpha=0.7)

# Nueva acción
plt.scatter(X_new_pca[0, 0], X_new_pca[0, 1], c='darkgreen', marker='X', s=110, label=df_new.index[0])
plt.annotate(df_new.index[0], (X_new_pca[0, 0], X_new_pca[0, 1]), fontsize=9, color='red')

# Flechas de cargas (asumiendo 'loadings' ya calculado)
for i, var in enumerate(df_indicadores.columns):
    plt.arrow(0, 0, loadings[i, 0], loadings[i, 1],
              color='crimson', alpha=0.8, head_width=0.05, length_includes_head=True)
    plt.text(loadings[i, 0]*1.4, loadings[i, 1]*1.1, var,
             color='crimson', ha='center', va='center', fontsize=9)

plt.title('PCA de Indicadores Financieros (con nueva acción)')
plt.xlabel(f'PC1 ({explained_variance_ratio[0]*100:.1f}%)')
plt.ylabel(f'PC2 ({explained_variance_ratio[1]*100:.1f}%)')
plt.axhline(0, color='gray', lw=1); plt.axvline(0, color='gray', lw=1)
plt.legend(); plt.grid(True); plt.tight_layout(); plt.show()
../../../_images/output_29_02.png

Advertencia:

Al aplicar PCA con menos componentes que las variables originales, el inverse_transform no recupera los datos originales, sino una aproximación que conserva solo la varianza capturada por esas componentes.

K-Means:#

from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans, DBSCAN
from sklearn.metrics import silhouette_score, pairwise_distances_argmin_min
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
# Calcular WCSS para diferentes valores de K:
wcss = []
K = range(1, 10)
for k in K:
    kmeans = KMeans(n_clusters=k, random_state=34)
    kmeans.fit(X_pca)
    wcss.append(kmeans.inertia_)

# Visualizar el método del codo
plt.figure(figsize=(8, 4))
plt.plot(K, wcss, "bo-")
plt.xlabel("Número de clústeres (K)")
plt.ylabel("WCSS")
plt.title("Método del Codo para determinar el número óptimo de clústeres")
plt.show()
../../../_images/output_33_0.png
# Calcular la puntuación de la silueta para diferentes valores de K:
from sklearn.metrics import silhouette_score

silhouette_scores = []
K = range(2, 11)
for k in K:
    kmeans = KMeans(n_clusters=k, random_state=34)
    kmeans.fit(X_pca)
    labels = kmeans.labels_
    score = silhouette_score(X_scaled, labels)
    silhouette_scores.append(score)

# Visualizar la puntuación de la silueta
plt.figure(figsize=(8, 4))
plt.plot(K, silhouette_scores, "bo-")
plt.xlabel("Número de clústeres (K)")
plt.ylabel("Puntuación de la Silueta")
plt.title("Método de la Silueta para determinar el número óptimo de clústeres")
plt.show()
../../../_images/output_34_0.png

Clusters = 3

k_base = 3

kmeans = KMeans(n_clusters=k_base, random_state=34)
df_indicadores_copy = df_indicadores.copy()
df_indicadores_copy['Cluster_KMeans'] = kmeans.fit_predict(X_pca)

# Valores de Inercia y Silueta:
inercia = kmeans.inertia_
silhouette = silhouette_score(X_pca, df_indicadores_copy['Cluster_KMeans'])

print(f"Clusters: {k_base}")
print(f"Inercia: {inercia}")
print(f"Puntuación de la Silueta: {silhouette}")
Clusters: 3
Inercia: 25.062446862137296
Puntuación de la Silueta: 0.4597279703983844
def graficar_clusters(df, metodo, var_x='Volatilidad', var_y='Retorno'):
    plt.figure(figsize=(10, 6))
    sns.scatterplot(
        data=df,
        x=var_x,
        y=var_y,
        hue=f'Cluster_{metodo}',
        palette='Set1',
        s=120
    )
    for i in range(len(df)):
        plt.text(df[var_x].iloc[i] + 0.002, df[var_y].iloc[i], df.index[i], fontsize=9)

    plt.title(f'Clustering por {metodo}: {var_y} vs {var_x}')
    plt.xlabel(var_x)
    plt.ylabel(var_y)
    plt.legend(title='Cluster')
    plt.grid(True)
    plt.show()

# Graficar cada método
graficar_clusters(df_indicadores_copy, 'KMeans')
../../../_images/output_37_0.png
# Clustering y variables en escala estandarizada:
labels = kmeans.labels_
X_scaled_df = pd.DataFrame(X_scaled, columns=df_indicadores_copy.iloc[:,:-1].columns)
X_scaled_df['Cluster_KMeans'] = labels
import plotly.graph_objects as go

# Columnas a usar
cols = ['Retorno','Volatilidad','Skewness','Kurtosis','Beta']

# Preparar datos y calcular promedio por cluster
tmp = X_scaled_df.copy()
tmp[cols] = tmp[cols].apply(pd.to_numeric, errors='coerce')
agg = tmp.groupby('Cluster_KMeans')[cols].mean().sort_index()

# Construir radar combinado
cats = cols + [cols[0]]
fig = go.Figure()

for cl, row in agg.iterrows():
    vals = row.tolist()
    fig.add_trace(go.Scatterpolar(
        r=vals + [vals[0]],
        theta=cats,
        name=f'Cluster {cl}',
        fill='toself',
        opacity=0.30
    ))

fig.update_layout(
    title='Radar combinado por cluster',
    template='plotly_white',
    polar=dict(radialaxis=dict(showline=False, gridcolor='lightgray'))
)

fig.show()