%%html
<head>
<link rel="stylesheet" href="css_jupyter.css">
</head>
Pensemos cómo hacen dinero las empresas que manejan los fondos de pensiones o de inversión.
En general, la gran mayoría carga una comisión ad valorem calculada sobre el porcentaje del valor de los activos que manejan.
Esto significa que los managers estarán más preocupados por aumentar el volumen del fondo que por su performance: cuanto más grande sea el fondo en global más dinero se llevan en comisiones.
En este escenario, los economistas académicos han intentado proponer mecanismos que permitan alinear el interés de los managers con el interés de los inversores.
Por ejemplo, podría ser interesante obligar a los managers a tener dinero propio en el fondo. En este caso, no solo estarían interesados en aumentar el volumen total de activos del fondo sino también su rendimiento, alineando el interés del manager al del inversionista.
Resulta entonces natural que cualquier persona -como usted, por ejemplo- esté interesado en analizar y caracterizar la rentabilidad de los fondos. Para ello necesita obtener datos relacionados con los fondos de pensiones.
Mostrar cómo es posible obtener, de manera sencilla, datos de la Web para trabajar en aquellos aspectos que
nos interesen.
En particular, los objetivos específicos son:
Lo que aquí se presenta, de manera resumida, son algunos de los conceptos que hemos introducido en el curso de Obtención de datos de la Web. Este curso tiene como objetivo presentar distintos módulos de Python, e.g., request-html o selenium, para extraer información de la Web.
# Importamos las librerias-paquetes que vamos a utilizar
from requests_html import HTML, HTMLSession
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme()
import warnings
warnings.filterwarnings('ignore')
# iniciamos session
session = HTMLSession()
# Leemos los datos de la web y los guardamos en un dataframe
table_df =[]
i = 1
while True:
url = f"Por proprietary data la url queda omitida"
source = session.get(url)
# Buscamos el tag que nos interesa
tabla = source.html.find("tbody", first = True)
fondos = tabla.find("tr")
# Quebramos cuando ya no haya mas hojas con fondos
if fondos == []:
break
for row in range(len(fondos)):
fondo = fondos[row].text.split("\n")
table_df.append(fondo)
i = i + 1 # cambiamos la página
time.sleep(5)
tabla_pensiones = pd.DataFrame(table_df, columns = ['plan', 'categoria', 'un', 'tres', 'cinco', 'diez'])
# Vemos lo que hemos leido
tabla_pensiones.head()
plan | categoria | un | tres | cinco | diez | |
---|---|---|---|---|---|---|
0 | BBVA PLAN TELECOMUNICACIONES | RVI GLOBAL | 3,35% | 70,73% | 102,59% | 317,90% |
1 | NARANJA STANDARD & POORS 500 | RVI USA | 6,40% | 45,62% | 71,61% | 260,66% |
2 | ACUEDUCTO RV NORTEAMERICA | RVI USA | 5,10% | 44,43% | 70,19% | 218,79% |
3 | ABANCA USA | RVI USA | 5,10% | 44,43% | 70,33% | 216,32% |
4 | CASER RV NORTEAMERICA GA | RVI USA | 5,10% | 44,44% | 69,95% | 213,43% |
# Limpiamos los datos para poder trabajar con ellos,
tabla_pensiones = tabla_pensiones.replace(chr(183), np.nan)
for col in tabla_pensiones.columns:
tabla_pensiones[col] = tabla_pensiones[col].str.replace('%', '')
tabla_pensiones[col] = tabla_pensiones[col].str.replace(',', '.')
for col in ['un', 'tres', 'cinco', 'diez']:
tabla_pensiones[col] = tabla_pensiones[col].astype(float)
# Vemos los datos con los cuales vamos a trabajar
tabla_pensiones.head()
plan | categoria | un | tres | cinco | diez | |
---|---|---|---|---|---|---|
0 | BBVA PLAN TELECOMUNICACIONES | RVI GLOBAL | 3.35 | 70.73 | 102.59 | 317.90 |
1 | NARANJA STANDARD & POORS 500 | RVI USA | 6.40 | 45.62 | 71.61 | 260.66 |
2 | ACUEDUCTO RV NORTEAMERICA | RVI USA | 5.10 | 44.43 | 70.19 | 218.79 |
3 | ABANCA USA | RVI USA | 5.10 | 44.43 | 70.33 | 216.32 |
4 | CASER RV NORTEAMERICA GA | RVI USA | 5.10 | 44.44 | 69.95 | 213.43 |
# Veamos las rentabilidades medianas por categoría,
# ordenadas en base a la rentabilidad mediana a diez años
(tabla_pensiones.groupby("categoria").agg(["median"])
.sort_values(by = [("diez", "median")], ascending = False))
un | tres | cinco | diez | |
---|---|---|---|---|
median | median | median | median | |
categoria | ||||
RVI USA | 6.400 | 44.435 | 70.070 | 213.430 |
RVI GLOBAL | -1.620 | 23.495 | 18.660 | 100.040 |
RV EURO | -3.020 | 10.990 | 9.020 | 97.190 |
RVI EUROPA | -0.110 | 17.820 | 15.200 | 91.060 |
RV ESPAÑA | -2.335 | -0.085 | -10.155 | 70.340 |
RVI ASIA-EMERGENTES | -12.000 | 10.185 | 6.585 | 61.960 |
RF GARANTIZADO | -4.410 | -4.090 | 0.520 | 56.270 |
MIXTO. AGRESIVO GLOBAL | -3.160 | 8.950 | 8.175 | 55.545 |
MIXTO. AGRESIVO EURO | -4.120 | 9.880 | 4.880 | 54.410 |
MIXTO. MODERADO EURO | -3.920 | 6.400 | 5.230 | 49.860 |
MIXTO. MODERADO GLOBAL | -3.530 | 4.315 | 4.430 | 47.500 |
MIXTO. FLEXIBLE | -3.810 | 4.800 | 7.680 | 38.400 |
MIXTO. CONSERVADOR GLOBAL | -3.940 | -1.770 | -2.475 | 21.015 |
RETORNO ABSOLUTO | -2.780 | -0.180 | -3.435 | 19.930 |
MIXTO. CONSERVADOR EURO | -4.220 | -1.340 | -2.635 | 19.030 |
RV GARANTIZADO | -6.435 | -0.290 | 2.920 | 17.650 |
RF EURO LARGO PLAZO | -4.780 | -3.480 | -3.935 | 14.145 |
RF INTERNACIONAL | -2.125 | -3.020 | -4.370 | 4.890 |
RF EURO CORTO PLAZO | -1.890 | -2.800 | -4.750 | 0.250 |
MONETARIO EURO | -1.670 | -3.220 | -5.075 | -4.270 |
OTROS | -3.555 | 9.470 | 4.970 | NaN |
# Veamos las rentabilidades máximas y mínimas por categoría a diez años
(tabla_pensiones.groupby("categoria").agg({"diez":["min", "max"]}).dropna().
sort_values(by=[("diez", "max")], ascending = False))
diez | ||
---|---|---|
min | max | |
categoria | ||
RVI GLOBAL | 25.83 | 317.90 |
RVI USA | 185.78 | 260.66 |
RV EURO | 48.36 | 139.94 |
RVI EUROPA | 35.72 | 130.01 |
MIXTO. FLEXIBLE | -0.54 | 120.36 |
MIXTO. AGRESIVO GLOBAL | 11.11 | 108.43 |
MIXTO. AGRESIVO EURO | -2.87 | 108.27 |
RF GARANTIZADO | 11.69 | 105.54 |
RV GARANTIZADO | 4.15 | 97.98 |
RV ESPAÑA | 7.22 | 86.13 |
RVI ASIA-EMERGENTES | 26.47 | 83.36 |
MIXTO. MODERADO EURO | 13.92 | 76.86 |
MIXTO. MODERADO GLOBAL | 27.18 | 69.29 |
MIXTO. CONSERVADOR GLOBAL | 8.44 | 60.91 |
RF EURO LARGO PLAZO | -0.43 | 46.06 |
MIXTO. CONSERVADOR EURO | -15.62 | 42.87 |
RF INTERNACIONAL | -7.83 | 33.33 |
RETORNO ABSOLUTO | -12.74 | 31.36 |
RF EURO CORTO PLAZO | -6.90 | 18.21 |
MONETARIO EURO | -6.31 | 1.03 |
# Veamos cuales son, dentro de cada categoría, los mejores planes medidos por la rentabilidad a 10 años
categoria_diez = tabla_pensiones.groupby("categoria")
(categoria_diez.apply(lambda x: x.sort_values(["diez"], ascending = False))
.reset_index(drop=True).groupby("categoria").head(1).
sort_values(by=["diez"], ascending = False)
)
plan | categoria | un | tres | cinco | diez | |
---|---|---|---|---|---|---|
827 | BBVA PLAN TELECOMUNICACIONES | RVI GLOBAL | 3.35 | 70.73 | 102.59 | 317.90 |
909 | NARANJA STANDARD & POORS 500 | RVI USA | 6.40 | 45.62 | 71.61 | 260.66 |
725 | SANTALUCIA VIDA EMPLEADOS RENTA VARIABLE | RV EURO | 1.55 | 13.52 | 10.93 | 139.94 |
803 | CASER GESTION VALOR | RVI EUROPA | 10.07 | 38.18 | 27.90 | 130.01 |
287 | MERCHBANC GLOBAL | MIXTO. FLEXIBLE | -12.64 | 34.44 | 51.56 | 120.36 |
48 | IBERCAJA DE PENSIONES GESTION AUDAZ | MIXTO. AGRESIVO GLOBAL | -2.58 | 22.31 | 19.92 | 108.43 |
0 | BK MIXTO 75 BOLSA | MIXTO. AGRESIVO EURO | -1.87 | 15.42 | 9.75 | 108.27 |
591 | BBVA PROTECCION 2030 | RF GARANTIZADO | -7.16 | -0.37 | 20.36 | 105.54 |
760 | BBVA PROTECCION 2035 | RV GARANTIZADO | -12.77 | -3.12 | 18.41 | 97.98 |
711 | BK VARIABLE ESPAÑA | RV ESPAÑA | -1.23 | 3.72 | -5.98 | 86.13 |
797 | CASER PREMIER 2021 | RVI ASIA-EMERGENTES | -14.75 | 7.50 | 6.06 | 83.36 |
363 | IBERCAJA DE PENSIONES SOSTENIBLE Y SOLIDARIO | MIXTO. MODERADO EURO | -3.23 | 21.31 | 28.29 | 76.86 |
414 | AHORRO PREVISION | MIXTO. MODERADO GLOBAL | -2.03 | 4.21 | -0.15 | 69.29 |
245 | CASERMED PROTECCION 13 | MIXTO. CONSERVADOR GLOBAL | -0.72 | -3.39 | -4.35 | 60.91 |
538 | IBERCAJA DE PENSIONES HORIZONTE 2028 | RF EURO LARGO PLAZO | -9.95 | 3.15 | 10.18 | 46.06 |
92 | DELEG | MIXTO. CONSERVADOR EURO | -2.97 | 3.93 | 6.13 | 42.87 |
685 | ARQUIA BANCA PLAN INVERSIÓN | RF INTERNACIONAL | -6.82 | 0.07 | 2.10 | 33.33 |
460 | CASER NUEVAS OPORTUNIDADES | RETORNO ABSOLUTO | -4.75 | -1.00 | -4.70 | 31.36 |
486 | BANCA PUEYO I | RF EURO CORTO PLAZO | -4.09 | NaN | -0.37 | 18.21 |
442 | BK INVERSION MONETARIO | MONETARIO EURO | -1.64 | -3.22 | -4.95 | 1.03 |
458 | BK JUBILACION 2030 | OTROS | -3.02 | 7.24 | 3.42 | NaN |
# Análisis de un plan en particular, e.g., plan Naranja: usampos regex en pandas
(
tabla_pensiones[tabla_pensiones["plan"].str.match(r'(^NAR.*)')==True]
.sort_values(by=["diez"], ascending = False)
)
plan | categoria | un | tres | cinco | diez | |
---|---|---|---|---|---|---|
1 | NARANJA STANDARD & POORS 500 | RVI USA | 6.40 | 45.62 | 71.61 | 260.66 |
38 | NARANJA 2040 | MIXTO. FLEXIBLE | -2.80 | 12.23 | 12.49 | 108.40 |
87 | NARANJA 2030 | MIXTO. FLEXIBLE | -3.74 | 4.65 | 4.59 | 78.19 |
102 | NARANJA IBEX 35 | RV ESPAÑA | -4.57 | -0.57 | -11.75 | 70.34 |
190 | NARANJA 2020 | MIXTO. FLEXIBLE | -5.50 | -2.96 | -3.69 | 38.40 |
356 | NARANJA RENTA FIJA CORTO PLAZO | RF EURO CORTO PLAZO | -5.14 | -3.03 | -3.42 | 10.34 |
380 | NARANJA RENTA FIJA EUROPEA | RF INTERNACIONAL | -7.02 | -5.99 | -6.12 | 7.31 |
771 | NARANJA 2025 | MIXTO. FLEXIBLE | -3.81 | 0.57 | NaN | NaN |
772 | NARANJA 2050 | MIXTO. FLEXIBLE | -2.29 | 14.18 | 14.84 | NaN |
773 | NARANJA EURO STOXX 50 | RV EURO | -10.01 | 10.99 | NaN | NaN |
#Extraer los 10 mejores RVI a diez años y ordenarlos por rentabilidad
rvi = (tabla_pensiones[tabla_pensiones["categoria"].str.match(r'(^RVI.*)')==True]
.sort_values(by=["diez"], ascending = False).head(10)
# Veamos estos mejores planes
rvi
plan | categoria | un | tres | cinco | diez | |
---|---|---|---|---|---|---|
0 | BBVA PLAN TELECOMUNICACIONES | RVI GLOBAL | 3.35 | 70.73 | 102.59 | 317.90 |
1 | NARANJA STANDARD & POORS 500 | RVI USA | 6.40 | 45.62 | 71.61 | 260.66 |
2 | ACUEDUCTO RV NORTEAMERICA | RVI USA | 5.10 | 44.43 | 70.19 | 218.79 |
3 | ABANCA USA | RVI USA | 5.10 | 44.43 | 70.33 | 216.32 |
4 | CASER RV NORTEAMERICA GA | RVI USA | 5.10 | 44.44 | 69.95 | 213.43 |
5 | IBERCAJA DE PENSIONES BOLSA USA | RVI USA | 10.49 | 49.81 | 69.52 | 212.17 |
6 | CABK RV INTERNACIONAL | RVI GLOBAL | 3.96 | 43.00 | 66.77 | 197.16 |
7 | BK VARIABLE AMERICA | RVI USA | -3.46 | 36.47 | 52.89 | 190.76 |
8 | MAPFRE AMERICA | RVI USA | 5.35 | 36.73 | 56.25 | 185.78 |
9 | GCO PENSIONES RENTA VARIABLE | RVI GLOBAL | 3.97 | 36.04 | 45.61 | 170.55 |
# Analisis de la distribución de las rentabilidades a 10 años
sns.set(style="whitegrid")
fig, ax = plt.subplots()
sns.ecdfplot(data=tabla_pensiones, x="diez")
# Agrego la mediana
ax.axvline(x = tabla_pensiones["diez"].median(), ymin = 0, ymax = 0.5, linestyle = "dotted", alpha = 0.75, color = "green" )
ax.axhline(y = 0.5, xmin = 0, xmax = 0.15, linestyle = "dotted", alpha = 0.75, color = "green" )
# Flecha y texto
ax.annotate(f'Rentabilidad mediana: {tabla_pensiones["diez"].median()} ', xycoords='data', xy=(30, .0), xytext=(-125, -.35),
arrowprops=dict(facecolor='black', arrowstyle = "->", color = "blue"),
bbox=dict(boxstyle="round", alpha=0.1),
)
ax.set_xlabel("Rentabilidad a 10 años: porcentaje")
# Agrego el rug
sns.rugplot(data=tabla_pensiones, x="diez", hue = "categoria", height = 0.15, linewidth = 2.5)
# Stackoverflow sobre legends
# https://stackoverflow.com/questions/66623746/no-access-to-legend-of-sns-histplot-used-with-data-normalisation
ax.legend(handles=ax.legend_.legendHandles, labels=[t.get_text() for t in ax.legend_.texts],
title=ax.legend_.get_title().get_text(),
bbox_to_anchor=(1.05, 1), loc='upper left', handleheight = .85, fontsize = 10)
# Ensancho las handels de la leggend
for i in range(0,len(ax.legend_.legendHandles)):
ax.legend_.legendHandles[i]._linewidth = 4
plt.title("Distribución acumulada de la Rentabilidad a 10 años")
plt.show()
# Relación entre las medidas temporales de rentabilidad
sns.pairplot(data=tabla_pensiones, hue = "categoria",height=2.5, diag_kind= "hist", corner = True)
plt.show()
# Analizar la relación entre fondos en renta variable y rentabilidad a 10 años
# importamos statmodels para hacer regresión
import statsmodels.api as sm
# Creamos una dummy para renta variable
tabla_pensiones["Dummy_RentaV"] = (tabla_pensiones["categoria"].str.match(r'(^RV.*)')==True).astype(int)
# Veamos que información contiene esta dummy
tabla_pensiones["Dummy_RentaV"].describe()
count 922.000000 mean 0.228850 std 0.420321 min 0.000000 25% 0.000000 50% 0.000000 75% 0.000000 max 1.000000 Name: Dummy_RentaV, dtype: float64
# El describe no es más que un dataframe
media = tabla_pensiones["Dummy_RentaV"].describe()["mean"]
print(f"Porcentaje de fondos en renta variable es de {media:.4f}")
Porcentaje de fondos en renta variable es de 0.2289
# Defino varibles y hago la regresión
tabla_pensiones['constante'] = 1
X = tabla_pensiones[["constante","Dummy_RentaV"]]
y = tabla_pensiones["diez"]
reg = sm.OLS(endog=y, exog= X, missing='drop')
results = reg.fit()
print(results.summary())
OLS Regression Results ============================================================================== Dep. Variable: diez R-squared: 0.472 Model: OLS Adj. R-squared: 0.471 Method: Least Squares F-statistic: 410.9 Date: Sun, 29 May 2022 Prob (F-statistic): 1.03e-65 Time: 13:16:13 Log-Likelihood: -2260.9 No. Observations: 461 AIC: 4526. Df Residuals: 459 BIC: 4534. Df Model: 1 Covariance Type: nonrobust ================================================================================ coef std err t P>|t| [0.025 0.975] -------------------------------------------------------------------------------- constante 25.7781 1.746 14.768 0.000 22.348 29.208 Dummy_RentaV 72.4359 3.573 20.270 0.000 65.414 79.458 ============================================================================== Omnibus: 168.174 Durbin-Watson: 0.664 Prob(Omnibus): 0.000 Jarque-Bera (JB): 1028.290 Skew: 1.444 Prob(JB): 5.12e-224 Kurtosis: 9.723 Cond. No. 2.51 ============================================================================== Notes: [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
# Obsevar que el valor del coeficiente de la renta variable es equivalente a la diferencia de medias
(
tabla_pensiones.groupby("Dummy_RentaV")["diez"].mean().iloc[1] # media rentabilidades variables
-
tabla_pensiones.groupby("Dummy_RentaV")["diez"].mean().iloc[0] # media rentabilidades otros tipo de fondos
)
72.43593732193723
# Relación rentabilidad a 5 y diez años
#Creación de nombres cortos por categoría
tabla_pensiones["label"] =tabla_pensiones["categoria"].str.split(" ", n = 1, expand = True)[0]
# Relación rentabilidades a cinco y diez años
from numpy.polynomial import polynomial as pol
from numpy.polynomial import Polynomial as Pol
tabla_pensiones = tabla_pensiones.dropna()
X = tabla_pensiones["cinco"]
y = tabla_pensiones["diez"]
labels = tabla_pensiones["label"]
# Obtener coeficientes
c = pol.polyfit(X,y, deg = 1)
# Alternativa: obener funcion
c1= Pol.fit(X,y, deg = 1)
# Para evaluar esta funcion polinómica c1(X)
# Vamos a sustituir los puntos por los labels
fig, ax = plt.subplots()
ax.scatter(X, y, marker = " ")
for i, label in enumerate(labels):
ax.annotate(label, (X.iloc[i], y.iloc[i]), fontsize = 10, color = "green", alpha = 0.5)
# Ajustamos una regresión lineal utilizando polinomios
ax.plot(np.unique(X),
np.poly1d(np.polyfit(X, y, 1))(np.unique(X)),
color='blue', alpha = 0.5, linewidth = 4)
ax.set_xlim([-50,150])
ax.set_ylim([-60,350])
ax.set_xlabel('Rentabilidad a 5 años')
ax.set_ylabel('Rentabilidad a 10 años')
ax.set_title('Relación entre la rentabilidad a 5 y 10 años')
plt.show()
Sería interesante repetir el mismo ejercicio con los fondos de inversión.