Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

2. Preprocesamiento

Este capítulo corresponde a la sección 9.10.4.2 del proyecto. Aplica las decisiones del resumen ejecutivo del EDA (sección 1.6) de forma idéntica en scikit-learn y en PySpark, partiendo en ambos casos del CSV original completo:

  1. Se genera una única partición 80/20 estratificada y se guarda en disco. Los dos entornos la leen.

  2. Se ajustan los transformadores solo con el conjunto de entrenamiento y después se aplican al de prueba.

  3. Se verifica numéricamente que los dos entornos producen exactamente las mismas variables.

El código de las transformaciones está en lc_utils.py (SkPreprocessor y spark_preprocess), para que los capítulos de modelado usen exactamente el mismo preprocesamiento que se valida aquí.

2.1 Partición común de los datos

La prueba de DeLong compara los AUC calculados sobre las mismas observaciones, así que scikit-learn y PySpark deben evaluar exactamente el mismo conjunto de prueba. La partición se define una sola vez:

  • con train_test_split sobre el identificador id;

  • con test_size=0.2, stratify=default y random_state=42;

  • y se guarda en data/split.parquet con las columnas id y split.

PySpark no usa randomSplit: lee este archivo y separa los conjuntos filtrando por split.

Filas: 1,345,310 | lectura y construcción de variables: 31.1 s | partición: 4.9 s
Archivo: C:\Users\User\Desktop\GEOLOGÍA\MAESTRÍA\Semestre 2\MachineLearning\jbook_ml202330\data\split.parquet
ids únicos: True | intersección train/test: 0
Loading...

La tasa de default es prácticamente la misma en los dos conjuntos, como corresponde a una partición estratificada. El conjunto de prueba tiene 269.062 préstamos: es el tamaño de muestra sobre el que se calculan después DeLong, McNemar y el bootstrap pareado.

2.2 Preprocesamiento con scikit-learn

PasoVariablesTransformaciónParámetro aprendido en train
Selección16 numéricas y 7 categóricasse excluyen fico_range_low, installment, grade (redundantes), mths_since_last_delinq (50 % de nulos) e issue_d—
Indicadoreshas_delinq_history, emp_length_missing, mort_acc_missing1 si hay historial de mora o si el dato falta—
Agrupaciónhome_ownership, purposeANY/NONE/OTHER → OTHER; categorías con menos del 1 % → other—
Recortedti, revol_utilrecorte (clip) a los percentiles 0,1 y 99,9percentiles
Transformaciónannual_inc, revol_ballog1p—
Imputaciónemp_length, mort_acc, revol_util, pub_rec_bankruptcies, dti, inq_last_6mthsmedianamedianas
Escalado16 numéricas y 3 indicadoresStandardScalermedia y desviación
Codificación7 categóricasOneHotEncoder(handle_unknown="ignore"), una columna por categoríacategorías

Los percentiles y las medianas se calculan como un elemento de la muestra (method="inverted_cdf"), que es la misma definición que usa Spark con approxQuantile(..., relativeError=0). Así los dos entornos aprenden exactamente los mismos valores.

Las variables numéricas se escalan para todos los modelos. La regresión logística, LinearSVC y Naive Bayes gaussiano lo necesitan. Los árboles son invariantes a transformaciones monótonas, así que el escalado no les cambia nada. Usar una sola matriz para los seis modelos simplifica la comparación.

Nota sobre la validación cruzada. El enunciado exige GridSearchCV sin Pipeline. Por eso el preprocesamiento se ajusta con todo el conjunto de entrenamiento antes de la búsqueda, y los pliegues de validación comparten medianas, percentiles y medias con los de ajuste. El efecto es despreciable: son pocos parámetros estimados con más de un millón de filas. Además, afecta igual a los dos entornos, porque el CrossValidator de PySpark recibe también los datos ya transformados. El conjunto de prueba no participa en ningún ajuste.

X_train: (1076248, 91) | X_test: (269062, 91) | dtype: float32
Memoria de X_train: 392 MB | ajuste y transformación: 3.6 s
Loading...
Dummies por variable categórica: {'term': 2, 'home_ownership': 4, 'verification_status': 3, 'purpose': 8, 'addr_state': 51, 'initial_list_status': 2, 'application_type': 2} | total de variables: 91
Loading...

2.3 Preprocesamiento con PySpark

La SparkSession se crea con la configuración obligatoria del enunciado (U.spark_session). El flujo de U.spark_preprocess es:

  1. Leer el CSV completo con spark.read.csv(..., escape='"') (ver sección 1.1), filtrar Fully Paid/Charged Off y construir con funciones de Spark SQL las mismas variables que en pandas: años de empleo, antigüedad crediticia, indicadores y agrupaciones.

  2. Unir con la partición común (data/split.parquet) por id y separar train y test filtrando la columna split.

  3. Ajustar con train un Pipeline con estas etapas:

    • approxQuantile(…, 0.0) para los límites de recorte (el resultado son 4 números);

    • Imputer(strategy="median", relativeError=0.0);

    • StringIndexer(handleInvalid="keep") con OneHotEncoder(dropLast=True);

    • VectorAssembler y StandardScaler(withMean=True, withStd=True) sobre las numéricas;

    • un VectorAssembler final con las dummies.

  4. Transformar train y test y cachearlos con .persist(StorageLevel.MEMORY_AND_DISK) justo después del VectorAssembler, como exige el enunciado.

Durante todo el proceso los datos permanecen como DataFrame de Spark: no se usa .toPandas() ni .collect() sobre el dataset. Al driver solo llegan resultados agregados de tamaño fijo (cuantiles, medianas, conteos y el vector de medias de la sección 2.4).

Spark 3.5.5 | master: local[*]
{'spark.memory.fraction': '0.8', 'spark.default.parallelism': '400', 'spark.sql.shuffle.partitions': '400', 'spark.driver.memory': '8g', 'spark.executor.memory': '8g', 'spark.memory.storageFraction': '0.3'}
train: 1,076,248 | test: 269,062 | tiempo total (lectura + pipeline + caché): 158.1 s
Etapas del Pipeline: ['ImputerModel', 'StringIndexerModel', 'StringIndexerModel', 'StringIndexerModel', 'StringIndexerModel', 'StringIndexerModel', 'StringIndexerModel', 'StringIndexerModel', 'OneHotEncoderModel', 'VectorAssembler', 'StandardScalerModel', 'VectorAssembler']
train cacheado: True | Disk Memory Serialized 1x Replicated
test  cacheado: True | Disk Memory Serialized 1x Replicated
root
 |-- id: long (nullable = true)
 |-- default: integer (nullable = false)
 |-- features: vector (nullable = true)

+--------+-------+--------------------------------------------------------------------------------------------------------------+
|      id|default|                                                                                                      features|
+--------+-------+--------------------------------------------------------------------------------------------------------------+
|68407277|      0|(91,[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,21,25,30,74,88,89],[-1.2410524863584014,0.1568214644...|
|68341763|      0|(91,[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,21,25,31,50,88,90],[0.6398607239337363,-0.5159979584...|
|68426831|      0|(91,[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,24,26,30,46,88,89],[-0.2833924067279532,0.0415408779...|
+--------+-------+--------------------------------------------------------------------------------------------------------------+
only showing top 3 rows

Loading...

2.4 Verificación de la equivalencia entre entornos

Se comparan los parámetros aprendidos (límites de recorte, medianas, media y desviación del escalador) y la media de cada una de las 91 variables resultantes en el conjunto de entrenamiento. En Spark, la media se calcula con Summarizer, que devuelve un único vector agregado.

Loading...
Máxima diferencia relativa: 4.6457726208361726e-07

Los límites de recorte, las medianas y las medias coinciden exactamente. La única diferencia está en la desviación estándar del escalador y es del orden de 5×10−75 \times 10^{-7}. Se debe a que Spark usa la desviación muestral (n−1n-1) y scikit-learn la poblacional (nn): el cociente entre ambas es n/(n−1)≈1+4,6×10−7\sqrt{n/(n-1)} \approx 1 + 4{,}6 \times 10^{-7} con n=1.076.248n = 1.076.248. No tiene efecto práctico en los modelos.

Variables: scikit-learn 91 | PySpark 91 | mismos nombres: True
Máxima |Δ| en la media de las variables transformadas (train): 1.71e-08
Loading...

Los dos entornos producen las mismas 91 variables, con las mismas categorías, y medias idénticas hasta el orden de 10-8 (error de redondeo numérico). También coinciden el número de filas y la tasa de default de cada conjunto. Por lo tanto, cualquier diferencia en los resultados de los capítulos siguientes se deberá a los algoritmos de cada librería y no a los datos que reciben.

La tabla omite las 51 dummies de addr_state para abreviar; están incluidas en la comparación del máximo ∣Δ∣|\Delta|.

2.5 Tiempos de preprocesamiento y hardware

Loading...
Loading...

En PySpark las etapas no se miden por separado. Como la evaluación es perezosa, la lectura del CSV, la unión con la partición, el ajuste del Pipeline y el llenado del caché se ejecutan en los mismos trabajos. El total de Spark incluye varias pasadas completas sobre los datos:

  • los cuantiles exactos;

  • las medianas exactas del Imputer;

  • los siete StringIndexer;

  • la media y la desviación del StandardScaler;

  • la materialización del caché.

pandas, en cambio, resuelve cada paso con operaciones vectorizadas en memoria. Con 1,3 millones de filas, que caben en la RAM de un solo equipo, esa diferencia favorece a scikit-learn. Se retoma en la reflexión final.

Guardado: C:\Users\User\Desktop\GEOLOGÍA\MAESTRÍA\Semestre 2\MachineLearning\jbook_ml202330\data\timings.json