File size: 22,163 Bytes
8f074bc
 
 
 
 
 
 
 
 
 
119d3b4
8f074bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119d3b4
 
 
 
 
8f074bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119d3b4
 
 
 
 
 
 
8f074bc
119d3b4
8f074bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119d3b4
 
8f074bc
119d3b4
8f074bc
119d3b4
 
 
8f074bc
119d3b4
 
8f074bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119d3b4
8f074bc
 
119d3b4
8f074bc
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
import os
import numpy as np
import pandas as pd
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import QuantileTransformer
from scipy.stats import gamma
import json

class FrontDatasetHandler:
    def __init__(self, maestro: pd.DataFrame=None, precios_cierre: pd.DataFrame=None, app_dataset: pd.DataFrame=None, 
                 json_path: str = None, pickle_path: str = None, ignore_columns: list = None, numeric_columns: list = None):
        self.maestro = maestro
        self.app_dataset = app_dataset # Dataframe preprocesado para la app
        self.pickle_path = pickle_path
        # Extraemos los ficheros JSON para la creación del dataset de la app si no se ha pasado como argumento:
        if self.app_dataset is None and json_path is not None:
            with open(os.path.join(json_path, "ignore_columns.json"), "r") as f:
                self.ignore_columns = json.load(f)['ignore_columns']
            print(f"ignore_columns: {self.ignore_columns}")
            with open(os.path.join(json_path, "numeric_columns.json"), "r") as f:
                self.numeric_columns = json.load(f)['numeric_columns']
            print(f"numeric_columns: {self.numeric_columns}")
            with open(os.path.join(json_path, "app_column_config.json"), "rb") as f:
                self.app_dataset_cols  = json.load(f)['app_dataset_cols']
            print(f"app_dataset_cols: {self.app_dataset_cols}")

            with open(os.path.join(json_path, "cat_to_num_maps.json"), "r") as f:
                num_maps = json.load(f)
            self.sector_num_map = num_maps['sector_num_map']
            self.industry_num_map = num_maps['industry_num_map']
        else:
            self.ignore_columns = ignore_columns
            self.numeric_columns = numeric_columns
            print(f"ignore_columns: {self.ignore_columns}")
            print(f"numeric_columns: {self.numeric_columns}")

        self.norm_columns = None
        if maestro is not None:
            maestro.drop(columns=self.ignore_columns, inplace=True, errors='ignore')
        self.precios_cierre = precios_cierre # Sólo necesario cuando se requiere preprocesar el dataset para la app
        self.rend_diario_log = None
        self.precios_cierre_fh = None
        self.rendimientos_y_volatilidad = None
        self.mapeos_var_categoricas = None
        self.activos_descartados = []
        self.quantile_scaler = None

    def filtra_y_homogeneiza(self, n_dias=366, n_dias_descartar=1, min_dias=100):
        if self.precios_cierre.index.name != 'date':
            self.precios_cierre.set_index('date', inplace=True)
        self.precios_cierre.columns.name = 'ticker'
        end_date = self.precios_cierre.index.max()
        start_date = end_date - pd.Timedelta(days=n_dias)
        # If start_date is not in the index, find the nearest earlier date
        if start_date not in self.precios_cierre.index:
            earlier_dates = self.precios_cierre.index[self.precios_cierre.index < start_date]
            if len(earlier_dates) > 0:
                start_date = earlier_dates.max()
            else:
                start_date = self.precios_cierre.index.min()
        
        # Filtrar datos dentro del rango de fechas 
        precios_cierre_fh = self.precios_cierre.loc[start_date:end_date].copy()
        
        # Descartar los últimos n_dias_descartar
        if n_dias_descartar > 0:
            dates_to_drop = precios_cierre_fh.index.sort_values(ascending=False)[:n_dias_descartar]
            precios_cierre_fh.drop(dates_to_drop, inplace=True)
        
        precios_cierre_fh.ffill(axis=0, inplace=True) # Se rellenan los datos vacíos con el dato del día anterior
        self.activos_descartados = precios_cierre_fh.columns[precios_cierre_fh.notna().sum(axis=0) < min_dias].tolist()
        precios_cierre_fh.drop(columns=self.activos_descartados, inplace=True)
        self.precios_cierre = precios_cierre_fh
        return
    
    def calcula_rendimientos_y_volatilidad(self, n_dias=365, umbral_max=0.3, umbral_min=-0.3):
        end_date = self.precios_cierre.index.max()
        print(f"Última fecha: {end_date}")
        print("Primera fecha: ",self.precios_cierre.index.min())
        start_date = end_date - pd.Timedelta(days=n_dias)
        # Si no hay cotizaciones para la fecha de inicio calculada (ej. fin de semana), se cambia por la fecha más cercana
        if start_date not in self.precios_cierre.index:
            earlier_dates = self.precios_cierre.index[self.precios_cierre.index < start_date]
            if len(earlier_dates) > 0:
                start_date = earlier_dates.max()
            else:
                start_date = self.precios_cierre.index.min()
                
        _df_rend_y_vol = self.precios_cierre.loc[start_date:end_date].copy()
        
        _df_rend_y_vol.dropna(how='all', inplace=True) #####
        # Reemplazar valores cero y negativos (errores de formato) por el siguiente valor más pequeño positivo
        _df_rend_y_vol[_df_rend_y_vol <= 0] = np.nextafter(0, 1)
        if self.activos_descartados:
            _df_rend_y_vol = _df_rend_y_vol.drop(columns=[col for col in self.activos_descartados if col in _df_rend_y_vol.columns])   
        if len(_df_rend_y_vol) == 0:
            raise ValueError(f"No hay datos disponibles en el rango de {n_dias} días")
        
        
        _rend_diario_log = np.log(_df_rend_y_vol).diff()
        _rend_diario_log = _rend_diario_log.iloc[1:] # Eliminar la primera fila
        # _rend_diario_log.dropna(how='all', inplace=True)  
        print(f'Datos rentabilidad ({n_dias} días) con outliers: {_rend_diario_log.shape}')
        # Identificar activos a descartar (outliers)
        _activos_outliers = _rend_diario_log.columns[((_rend_diario_log > umbral_max) | (_rend_diario_log < umbral_min)).any()].tolist()
        self.activos_descartados.extend([asset for asset in _activos_outliers if asset not in self.activos_descartados])
        # Descartar activos con rentabilidades atípicas
        _rend_diario_log = _rend_diario_log.loc[:, ~((_rend_diario_log > umbral_max) | (_rend_diario_log < umbral_min)).any()]
        print(f'Datos rentabilidad sin outliers: {_rend_diario_log.shape}')
        
        self.rend_diario_log = _rend_diario_log.copy()
        
        # Inicializar rendimientos_y_volatilidad si no existe
        if self.rendimientos_y_volatilidad is None:
            self.rendimientos_y_volatilidad = pd.DataFrame(columns=_rend_diario_log.columns)
            # print(f'INIT: Tabla rendimientos {n_dias}: {self.rendimientos_y_volatilidad.shape}')
        else:
            # Mantener solo los activos que están en _rend_diario_log
            self.rendimientos_y_volatilidad = self.rendimientos_y_volatilidad.loc[:, _rend_diario_log.columns]
            # print(f'Tabla rendimientos {n_dias}: {self.rendimientos_y_volatilidad.shape}')
            
        # Añadir nuevas columnas para el n_dias actual
        self.rendimientos_y_volatilidad.loc[f'ret_log_{n_dias}'] = np.sum(_rend_diario_log, axis=0)
        self.rendimientos_y_volatilidad.loc[f'ret_{n_dias}'] = (_df_rend_y_vol.ffill().bfill().iloc[-1] / _df_rend_y_vol.ffill().bfill().iloc[0]) - 1
        self.rendimientos_y_volatilidad.loc[f'vol_{n_dias}'] = _rend_diario_log.var()**0.5
 
        return
    
    def cruza_maestro(self):
        _rets_y_vol_maestro = self.rendimientos_y_volatilidad.T.reset_index().copy()
        _columns_to_merge = [col for col in _rets_y_vol_maestro.columns if col not in self.maestro.columns]     
        if len(_columns_to_merge) > 0:
            _maestro_v2 = self.maestro.merge(_rets_y_vol_maestro, left_on='ticker', right_on='ticker')
            _maestro_v2 = _maestro_v2.replace([float('inf'), float('-inf')], np.nan)
            self.maestro = _maestro_v2
        else:
            raise ValueError("No hay nuevas columnas para cruzar con el dataframe maestro")
        return
    
    def _cat_to_num_(self, df, cat, pre_map=None):
        """
        Transforma una columna categórica en un DataFrame a valores numéricos asignando un número entero a cada categoría.
        Si no se proporciona un mapeo (`pre_map`), asigna 0 a la categoría más frecuente, 1 a la siguiente más frecuente, y así sucesivamente.
        Si se proporciona un mapeo (`pre_map`), utiliza ese mapeo para la conversión.
        Parámetros
        ----------
        df : pandas.DataFrame
            DataFrame que contiene la columna categórica a transformar.
        cat : str
            Nombre de la columna categórica a transformar.
        pre_map : dict, opcional
            Diccionario que mapea cada categoría a un valor numérico. Si no se proporciona, el mapeo se genera automáticamente.
        Devuelve
        --------
        pandas.DataFrame
            DataFrame con dos columnas: la columna categórica original y una columna con los valores numéricos asignados.
        """
        if not pre_map:
            pivot = pd.pivot_table(df, index=[cat], aggfunc='size')
            df_sorted = pivot.sort_values(ascending=False).reset_index(name='count')
            df_sorted[cat + '_num'] = range(len(df_sorted))
        else:
            df_sorted = pd.DataFrame({cat: list(pre_map.keys()), cat + '_num': list(pre_map.values())})
        return df_sorted
    
    def var_categorica_a_numerica(self, cat_cols):
        for col in cat_cols:
            if col == 'sectorDisp':
                globals()[f"pt_{col}"] = self._cat_to_num_(self.maestro, col, self.sector_num_map)
            elif col == 'industryDisp':
                globals()[f"pt_{col}"] = self._cat_to_num_(self.maestro, col, self.industry_num_map)
            else:
                globals()[f"pt_{col}"] = self._cat_to_num_(self.maestro, col)  # Creamos un dataframe con el mapeo de cada variable categórica por frecuencia
        self.mapeos_var_categoricas = [globals()[f"pt_{col}"] for col in cat_cols] # Lista de dataframes con los mapeos de cada una de las variables categóricas

        _maestro = self.maestro.copy()
        for col, pt in zip(cat_cols, self.mapeos_var_categoricas):
            _maestro[col] = _maestro[col].astype(str)
            pt[col] = pt[col].astype(str)
            # Creamos un diccionario con cada variable categórica y su equivalente numérico
            mapping_dict = dict(zip(pt[col], pt[col + '_num']))
            _maestro[col + '_num'] = _maestro[col].map(mapping_dict)
            _maestro[col + '_num'] = pd.to_numeric(_maestro[col + '_num'], errors='coerce')

        self.maestro = _maestro
        return
    
    def normaliza_por_cuantiles(self):
        maestro_copy = self.maestro.copy()
        numeric_columns = maestro_copy.select_dtypes(include=np.number).columns
        self.quantile_scaler = QuantileTransformer(output_distribution='uniform')
        variables_numericas = [col for col in numeric_columns if not col.endswith('_norm')]
        all_na_cols = [col for col in variables_numericas if maestro_copy[col].isna().all()]
        variables_numericas = [col for col in variables_numericas if col not in all_na_cols]
        self.norm_columns = ['{}_norm'.format(var) for var in variables_numericas]
        maestro_copy[self.norm_columns] = self.quantile_scaler.fit_transform(maestro_copy[variables_numericas])
        maestro_copy[self.norm_columns] = maestro_copy[self.norm_columns].clip(0, 1)
        self.maestro = maestro_copy
        return
    
    def var_estandar_z(self):
        maestro_copy = self.maestro.copy()
        numeric_columns = maestro_copy.select_dtypes(include=np.number).columns
        variables_numericas = [col for col in numeric_columns if not col.endswith('_std')]
        variables_num_std = ['{}_std'.format(var) for var in variables_numericas]

        def estandarizar(x): 
            # Estandariza el valor z, restando la media y dividiendo por la desviación estándar
            mean_val = x.mean()
            std_val = x.std()
            if pd.isna(std_val) or std_val == 0:
                return pd.Series(0.0, index=x.index, name=x.name)
            else:
                normalized_series = (x - mean_val) / std_val
                return normalized_series.fillna(0.0)

        normalized_data = maestro_copy[variables_numericas].apply(estandarizar, axis=0)
        maestro_copy[variables_num_std] = normalized_data
        self.maestro = maestro_copy
        return
    
    def configura_distr_prob(self, shape, loc, scale, max_dist, precision_cdf):
        x = np.linspace(0, max_dist, num=precision_cdf)
        y_pdf = gamma.pdf(x, shape, loc, scale )
        y_cdf = gamma.cdf(x, shape, loc, scale )
        return y_pdf, y_cdf
    
    def calculos_y_ajustes_dataset_activos(self):
        maestro_copy = self.maestro.copy()
        # Conversiones a formato numérico de columnas que dan problemas
        for column in self.numeric_columns:
            if column in maestro_copy.columns:
                maestro_copy[column] = pd.to_numeric(maestro_copy[column], errors='coerce') 
                # print(f"Columna {column} convertida a {maestro_copy[column].dtype}")
        # Estandarización de los diferentes tipos de NaN
        # maestro_copy = maestro_copy.replace([None, np.nan, np.inf, -np.inf], pd.NA)
        # Antigüedad del fondo en años:
        if self.precios_cierre is not None and not self.precios_cierre.index.empty:
            _most_recent_date = self.precios_cierre.index.max().date()
        else:
            _most_recent_date = pd.Timestamp.today().date()
        # maestro_copy['firstTradeDateMilliseconds'] = pd.to_datetime(maestro_copy['firstTradeDateMilliseconds']).dt.date
        maestro_copy['firstTradeDateMilliseconds'] = pd.to_datetime(maestro_copy['firstTradeDateMilliseconds'], unit='ms', errors='coerce').dt.date 
        maestro_copy['asset_age'] = maestro_copy['firstTradeDateMilliseconds'].apply(
            lambda x: ((_most_recent_date - x).days / 365) if pd.notnull(x) and hasattr(x, 'day') else 0
        ).astype(int)
        outlier_thresholds = {
            'beta': (-100, 100),
            'dividendYield': (-1,100),
            'fiveYearAvgDividendYield': (-1,100),
            'trailingAnnualDividendYield': (-1,100),
            'quickRatio': (-1, 500),
            'currentRatio': (-1, 500),
            'ebitda': (-1e12, 1e12),
            'grossProfits': (-1e12, 1e12),
        }
        for column, (lower_bound, upper_bound) in outlier_thresholds.items():
            maestro_copy.loc[(maestro_copy[column] < lower_bound) | (maestro_copy[column] > upper_bound), column] = pd.NA
        self.maestro = maestro_copy.copy()
        return
    
    def filtra_df_activos(self, df, isin_target, filtros, debug=False):
        '''
        LEGACY
        Devuelve un dataframe filtrado, sin alterar el orden, eliminando características no deseadas, para usar en aplicación de búsqueda de activos sustitutivos.
        Las características y valores a filtrar son las de un fondo objetivo dado por su isin. 
        Por ejemplo, si clean_share es False en filtros, el dataframe final no incluirá más activos con el mismo valor de clean_share que el ISIN objetivo
        Argumentos:
            df (pandas.core.frame.DataFrame): Dataframe maestro de activos
            isin_target (str): ISIN del fondo objetivo
            # caracteristicas (list): Lista de str con los nombres de las características
            filtros (dict): Diccionario donde las claves son las características y los valores son True si se quiere conservar
            debug (bool, optional): Muestra información de depuración. Por defecto False.
        Resultado:
            df_filt (pandas.core.frame.DataFrame): Dataframe filtrado
        '''
        # fondo_target = df[df['isin'] == isin_target].iloc[0]
        fondo_target = df[df['ticker'] == isin_target].iloc[0]
        if debug: print(f'Tamaño inicial: {df.shape}')
        
        car_numericas = ['ret_365', 'vol_365', 'marketCap', 'asset_age']
        
        # for feature in caracteristicas[2:]:
        for feature in list(filtros.keys()):
            value = fondo_target[feature]
            if debug: print(f'{feature} = {value}')
            
            # Verificar si esta característica debe ser filtrada
            if feature in filtros and not filtros[feature]:
                if debug: print(f'FILTRO: {feature} != {value}')
                df = df[df[feature] != value]
                
                # Aplicar filtros adicionales para variables numéricas
                if feature in car_numericas:
                    if feature == 'ret_365':
                        if debug: print(f'FILTRO NUMÉRICO: {feature} > {value}')
                        df = df[df[feature] > value]
                    elif feature == 'vol_365':
                        if debug: print(f'FILTRO NUMÉRICO: {feature} < {value}')
                        df = df[df[feature] < value]
                    elif feature == 'asset_age':
                        if debug: print(f'FILTRO NUMÉRICO: {feature} > {value}')
                        df = df[df[feature] > value]
                    elif feature == 'marketCap':
                        if debug: print(f'FILTRO NUMÉRICO: {feature} > {value}')
                        df = df[df[feature] < value]
        
        df_filt = df
        if debug: print(f'Tamaño final: {df_filt.shape}')
        return df_filt
    
    def calcula_ind_sust (self, dist, y_cdf, precision_cdf, max_dist):
        try:
            idx = int((precision_cdf / max_dist) * dist)
            idx = min(idx, len(y_cdf) - 1)
            norm_dist = y_cdf[idx]
            ind_sust = max(0.0, 1.0 - norm_dist)
        except IndexError:
            ind_sust = 0
        return ind_sust
    

    def vecinos_cercanos(self, df, variables_busq, caracteristicas, target_ticker, y_cdf, precision_cdf, max_dist, n_neighbors, filtros):
        if target_ticker not in df['ticker'].values:
            return f"Error: '{target_ticker}' no encontrado en la base de datos"
        target_row = df[df['ticker'] == target_ticker]
        if ~target_row.index.isin(df.index):
            df = pd.concat([df, target_row], ignore_index=True)
        # print(f'DF original: {df.shape}')
        X = df[variables_busq]
        model = NearestNeighbors(n_neighbors=n_neighbors) ##### probar con más y filtrar después #######
        model.fit(X)
        target_row = df[df['ticker'] == target_ticker][variables_busq]
        # model.kneighbors devuelve dos arrays bidimensionales con los vecinos más cercanos y sus distancias:
        distances, indices = model.kneighbors(target_row)
        # combined_columns = list(set(caracteristicas + variables_busq))
        neighbors_df = df.iloc[indices[0]][caracteristicas]
        neighbors_df['distance'] = distances[0]
        ind_sust = np.array([self.calcula_ind_sust(dist, y_cdf, precision_cdf, max_dist) for dist in distances[0]])
      
        neighbors_df['ind_sust'] = ind_sust
        neighbors_df = neighbors_df.sort_values(by='distance', ascending=True)  
        target_row = neighbors_df[neighbors_df['ticker'] == target_ticker]
        
        # Aplicamos los filtros de exclusión:
        ### Código pendiente de eliminar/modificar (legado de la aplicación de fondos)
        neighbors_df = self.filtra_df_activos (df = neighbors_df, isin_target = target_ticker, filtros = filtros)
        ####################

        # Recupera el activo seleccionado en caso de haber hecho filtros, devolviéndolo a la primera posición del dataframe:
        if ~target_row.index.isin(neighbors_df.index):
            neighbors_df = pd.concat([pd.DataFrame(target_row), neighbors_df], ignore_index=True)
        # print(f'DF filtrado: {neighbors_df.shape}')
        # Ponemos el ticker como índice:
        neighbors_df.set_index('ticker', inplace = True)
        return neighbors_df
    
    def format_large_number(self, n, decimals=2):
        if n >= 1e12:
            return f'{n / 1e12:.{decimals}f} T'
        elif n >= 1e9:
            return f'{n / 1e9:.{decimals}f} B'
        elif n >= 1e6:
            return f'{n / 1e6:.{decimals}f} M'
        else:
            return str(n)
        
    def trae_embeddings_desde_pkl(self, embeddings_df_file_name='df_with_embeddings.pkl', embeddings_col_name='embeddings'):
        embeddings_df = pd.read_pickle(os.path.join(self.pickle_path, embeddings_df_file_name))
        self.maestro = self.maestro.merge(
            embeddings_df[['ticker', embeddings_col_name]],
            on='ticker',
            how='left'
        )
        print(f"Agregados embeddings {self.maestro.shape}")
        return
        
    def procesa_app_dataset(self, periodo=366, n_dias_descartar=1, min_dias=250, umbrales_rend=(-0.3, +0.3), periodos_metricas=[60, 365],
                        cat_cols = ['industryDisp', 'sectorDisp', 'country', 'city', 'exchange', 'financialCurrency', 'quoteType'],
                        embeddings_df_file_name='df_with_embeddings.pkl', embeddings_col_name='embeddings'):
        if self.app_dataset is not None:
            print("app_dataset already exists, skipping processing")
            return

        self.filtra_y_homogeneiza(n_dias=periodo, n_dias_descartar=n_dias_descartar, min_dias=min_dias)
        
        for periodo_metricas in periodos_metricas: 
            self.calcula_rendimientos_y_volatilidad(n_dias=periodo_metricas, umbral_max=umbrales_rend[1], umbral_min=umbrales_rend[0])
        self.cruza_maestro()
        self.var_categorica_a_numerica(cat_cols)
        
        self.calculos_y_ajustes_dataset_activos()
        self.normaliza_por_cuantiles()
        self.trae_embeddings_desde_pkl(embeddings_df_file_name=embeddings_df_file_name, embeddings_col_name=embeddings_col_name)
        app_dataset = self.maestro.copy()
        app_dataset = app_dataset.fillna({col: 0.5 for col in self.norm_columns})
        # Filtrado final de columnas para reducir el dataset:
        self.app_dataset = app_dataset[self.app_dataset_cols].copy()
        print(f"app_dataset preparado: {self.app_dataset.shape}")
        return