Volver al índice de Big Data · Práctica 2: Apache Spark

Práctica 1: Limpieza de datos en Online Retail

Asignatura: C703 · Big Data / Big Data
Alumno: Eduardo Alonso Sánchez · Boleta: 2023630155
Grupo: 7CV4 · Docente: Tania Rodríguez Sarabia · Periodo: 20271

Descargar reporte PDF · Notebook ejecutado · Resumen de resultados JSON

1. Objetivo y datos de entrada

El objetivo fue construir un proceso reproducible de carga, validación, limpieza, transformación y exportación de datos transaccionales. Trabajé con Online Retail, que reúne operaciones de un comercio británico entre diciembre de 2010 y diciembre de 2011. La descripción de UCI identifica los folios que comienzan con la letra C como cancelaciones y expresa los precios en libras esterlinas.

Utilicé la copia CSV de Databricks. La carga produjo 541,909 filas y ocho columnas: InvoiceNo, StockCode, Description, Quantity, InvoiceDate, UnitPrice, CustomerID y Country.

El análisis se ejecutó sobre el DataFrame completo. Las 155 filas de df_visualizacion se usaron únicamente para mostrar una parte del conjunto. La huella SHA-256 del archivo original permite comprobar el insumo de una reejecución:

a2f79bbdd4463df6db8a3f5a50b9c980ae8f645a370bf5e2c0d6097f9e817b05

2. Configuración y exploración inicial

La personalización usa 0155 como cadena en nombres, etiquetas y búsquedas, y 155 como entero para paridad y módulos. Conservé el cero inicial en el identificador escrito. El tamaño de visualización fue 155 % 100 + 100 = 155.

boleta_completa = "2023630155"
identificador = boleta_completa[-4:]  # "0155"
digitos_boleta = int(identificador)   # 155

df = pd.read_csv(
    RAW_PATH,
    encoding="utf-8",
    dtype={"InvoiceNo": "string", "StockCode": "string"},
)
columnas_fuente = df.columns.tolist()
N = digitos_boleta % 100 + 100
df_visualizacion = df.head(N).copy()

La exploración incluyó las primeras filas, los tipos, las estadísticas descriptivas y el conteo de nulos. InvoiceNo y StockCode se conservaron como texto para no perder los códigos alfanuméricos.

3. Tratamiento de valores nulos

Encontré 135,080 valores faltantes en CustomerID y 1,454 en Description. No hubo folios de factura nulos.

ColumnaNulos inicialesPorcentaje
CustomerID135,08024.926694 %
Description1,4540.268311 %
InvoiceNo00 %

Como 155 es impar, imputé CustomerID con 10000 + 155 = 10155. Las descripciones faltantes recibieron NO_DESCRIPCION_0155.

customer_id_imputado = 10000 + digitos_boleta
df["CustomerID"] = df["CustomerID"].fillna(customer_id_imputado).astype("Int64")
df["Description"] = df["Description"].fillna(f"NO_DESCRIPCION_{identificador}")
df = df.dropna(subset=["InvoiceNo"]).copy()

El valor 10155 representa un cliente desconocido. El resultado tiene 4,373 identificadores, pero sólo 4,372 corresponden a clientes presentes en el archivo original. Las transacciones imputadas no permiten identificar a una persona común.

4. Duplicados y redundancia

Detecté 5,268 duplicados exactos. La eliminación usó la clave de negocio InvoiceNo, StockCode y CustomerID, conservando la primera aparición. Esta regla es distinta de comparar todas las columnas y retiró 10,684 filas (1.971549 %). Quedaron 531,225 registros.

df["Fila_Original"] = df.index.astype("int64") + 1
duplicados_exactos = int(
    df.duplicated(subset=columnas_fuente, keep="first").sum()
)
clave_negocio = ["InvoiceNo", "StockCode", "CustomerID"]
df = df.drop_duplicates(subset=clave_negocio, keep="first").copy()

Fila_Original conserva la posición ordinal en el CSV de entrada. La columna se excluyó de la definición de duplicado exacto para que la auditoría no alterara el conteo.

5. Tipos y reglas de negocio

Convertí InvoiceDate con el formato mes/día/año y hora. Las 531,225 fechas se reconocieron. También convertí cantidad y precio a tipos numéricos. Antes del ajuste de valores extremos guardé las cantidades, los precios y los importes originales.

df["InvoiceDate"] = pd.to_datetime(
    df["InvoiceDate"], format="%m/%d/%Y %H:%M", errors="coerce"
)
df["Quantity"] = pd.to_numeric(df["Quantity"], errors="coerce")
df["UnitPrice"] = pd.to_numeric(df["UnitPrice"], errors="coerce")
df["Quantity_Original"] = df["Quantity"]
df["UnitPrice_Original"] = df["UnitPrice"]
df["Total_Transaccion_Original"] = (
    df["Quantity_Original"] * df["UnitPrice_Original"]
)
df["Transaccion_Valida"] = (
    (df["Quantity_Original"] > 0) & (df["UnitPrice_Original"] > 0)
)
df["Es_Cancelacion"] = df["InvoiceNo"].str.upper().str.startswith("C", na=False)

La regla de cantidad y precio positivos produjo 519,582 transacciones válidas según los valores originales. La bandera de cancelación identificó 9,139 filas y se conservó después de las transformaciones.

6. Valores extremos y capping

Para detectar valores atípicos calculé Q1 = 1, Q3 = 10 e IQR = 9. Los límites del rango intercuartílico fueron −12.5 y 23.5; 58,337 registros quedaron fuera de ellos. Para corregir Quantity apliqué el recorte asignado a la terminación de boleta: percentiles 5 y 95, equivalentes a 1 y 30.

Q1 = df["Quantity_Original"].quantile(0.25)
Q3 = df["Quantity_Original"].quantile(0.75)
IQR = Q3 - Q1
mascara_outlier_iqr = ~df["Quantity_Original"].between(
    Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
)

p5 = df["Quantity_Original"].quantile(0.05)
p95 = df["Quantity_Original"].quantile(0.95)
df["Quantity"] = df["Quantity_Original"].clip(lower=p5, upper=p95)
df["Cantidad_Capeada"] = df["Quantity"].ne(df["Quantity_Original"])
df["Total_Transaccion"] = df["Quantity"] * df["UnitPrice"]

Distribución de cantidades antes del ajuste y después del capping entre 1 y 30.

Figura 1. Cantidad original y cantidad ajustada. Fuente: notebook de la práctica, elaborado con la copia CSV de Online Retail.

El ajuste cambió 36,222 cantidades. Como el límite inferior fue 1, las 10,475 cantidades negativas pasaron a ser positivas, incluidas las 9,139 cancelaciones. Después del ajuste hay 528,721 filas con cantidad y precio positivos; este conteo no sustituye la validación hecha sobre el dato original.

Los dos listados de importes mayores muestran el efecto de la transformación:

OrdenFolio originalTotal original (GBP)Folio ajustadoTotal ajustado (GBP)¿Cancelación ajustada?
1581483168,469.60C55644538,970.00Sí
254143177,183.6055644419,485.00No
355644438,970.00C58060517,836.46Sí
453763213,541.33C54011716,888.02Sí
5A56318511,062.06C54011816,453.71Sí

El mayor total ajustado corresponde a una cancelación. Estos importes describen el DataFrame transformado; no deben presentarse como ventas reales. Las columnas originales y Es_Cancelacion permiten revisar su significado.

7. Normalización de productos y países

Normalicé los países a mayúsculas, retiré espacios de los extremos y colapsé espacios repetidos. Antes de unificar las descripciones había 1,324 productos con más de una descripción. La regla personalizada se aplicó a los códigos que contienen 0155: el código 90155 aparece en siete filas. Usé su descripción modal y añadí [UNIFICADO_0155].

df["Country"] = (
    df["Country"].astype("string")
    .str.replace(r"\s+", " ", regex=True).str.strip().str.upper()
)
mascara = df["StockCode"].str.contains(identificador, regex=False, na=False)
if mascara.any():
    moda_por_stock = (
        df.loc[mascara].groupby("StockCode")["Description"]
        .agg(lambda serie: serie.mode().iloc[0])
    )
    df.loc[mascara, "Description"] = (
        df.loc[mascara, "StockCode"].map(moda_por_stock)
        + f" [UNIFICADO_{identificador}]"
    )

La categoría de producto se obtuvo de StockCode. Primero revisé si contenía gift, después si contenía cualquier letra y, en los demás casos, lo clasifiqué como producto normal.

Categoría de productoRegistros
REGALO_015534
PRODUCTO_ESPECIAL53,925
PRODUCTO_NORMAL477,266

8. Segmentación y resultados finales

El umbral fue 2023630155 % 50 + 10 = 15 libras. Clasifiqué cada registro según su total ajustado: menos de 15 como BAJO_VALOR, y 15 o más como ALTO_VALOR. Aunque la columna se llama Categoria_Cliente, la clasificación corresponde a transacciones, no a clientes únicos ni a su gasto acumulado.

umbral = int(boleta_completa) % 50 + 10
df["Total_Venta_Original"] = df["Total_Transaccion_Original"]
df["Total_Venta"] = df["Total_Transaccion"]
df["Categoria_Cliente"] = np.where(
    df["Total_Venta"] < umbral, "BAJO_VALOR", "ALTO_VALOR"
)

Conteo de registros: 341842 de bajo valor y 189383 de alto valor, con umbral de 15 libras.

Figura 2. Registros por categoría calculada con el importe ajustado. Fuente: notebook de la práctica.

MétricaResultado
Filas originales541,909
Filas finales531,225
Filas eliminadas por clave de negocio10,684
Transacciones válidas con valores originales519,582
Transacciones positivas después del ajuste528,721
Cancelaciones conservadas9,139
Clientes reales distintos4,372
Identificadores distintos, incluido 101554,373
Productos distintos4,070
Total positivo original10,587,015.733 GBP
Total positivo ajustado8,446,757.503 GBP
Total neto original9,683,331.923 GBP
Total neto ajustado8,424,633.383 GBP
Registros de bajo valor / alto valor341,842 / 189,383
Filas que contienen 0155134,566

La búsqueda de 0155 se hizo en InvoiceNo, StockCode, Description, CustomerID y Country. La mayoría de coincidencias procede de 10155, por lo que el conteo no equivale a clientes reales ni a productos distintos.

9. Exportación, calidad y reproducción

El CSV limpio conserva los datos transformados y las columnas de auditoría. Las comprobaciones programadas obtuvieron 100 % de completitud, unicidad de la clave, coherencia de la bandera de cancelación, cumplimiento del rango capeado y normalización de países. Son verificaciones de reglas concretas; no prueban que cada importe sea una venta económicamente válida.

El notebook descargable contiene las salidas de la ejecución guardada, sin celdas con errores. Para repetir el procedimiento en Google Colab se puede subir y ejecutar completo: descarga el CSV original cuando no lo encuentra en datos/raw/. La ejecución conservada se hizo localmente; no se afirma una ejecución remota en Colab.

Para ejecutarlo con Jupyter, instala las dependencias de la práctica, abre el notebook desde la carpeta de trabajo y ejecuta las celdas en orden:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/jupyter lab practica_01_limpieza_datos_0155.ipynb

Las exportaciones del notebook quedan en datos/processed/: el CSV limpio, el resumen estadístico y los dos listados de cinco importes mayores. Las gráficas se guardan en assets/. Las evidencias pequeñas se pueden consultar aquí:

10. Conclusiones

Completé la limpieza sobre el conjunto completo y conservé un notebook ejecutado que permite revisar la descarga, los criterios personalizados y las exportaciones. La clave de negocio quedó sin repeticiones, se resolvieron los nulos y todas las fechas se convirtieron al tipo esperado.

El capping fue la decisión con mayor efecto sobre la interpretación. Al fijar el límite inferior en 1, cambió devoluciones y cancelaciones a cantidades ajustadas positivas. Por eso separé los totales originales de los transformados y mantuve las banderas para auditar cada fila.

La imputación con un único identificador también limita un análisis posterior de clientes. Para estudiar su comportamiento habría que excluir 10155 o tratarlo como categoría desconocida. La completitud del archivo no recupera la identidad que falta en el dato de origen.

Fuentes

Volver al índice de Big Data · Continuar con la Práctica 2