Volver a Blog Data Science

Mi Primer Proyecto Real de Análisis de Datos y Machine Learning

7 min read vistas 1 leyendo ahora
Índice del artículo

El comienzo: un trabajo práctico intimidante

Todo empezó con un trabajo de la universidad: analizar datasets públicos, limpiar datos, hacer visualizaciones y construir modelos de machine learning. Sonaba simple en teoría, pero cuando abrí los archivos Excel con 357 filas en uno y 226.631 en otro, supe que esto iba a ser un reto.

Los datasets eran de empleo y ventas empresariales en Argentina (2014), del sitio de datos abiertos del gobierno. Mi objetivo: predecir ventas basándome en empleo, región y tamaño de empresa.

Este artículo documenta mi proceso completo, los errores que cometí, y las lecciones que aprendí.


Primer error: cargar sin entender

Mi primer instinto fue hacer lo que vi en tutoriales:

import pandas as pd
df = pd.read_excel('datos.xlsx')
print(df.head())

Pero cuando vi los datos, no entendía nada:

  • ¿Qué significaba cada columna?
  • ¿Por qué había valores como -99?
  • ¿Las ventas estaban en pesos o millones?
  • ¿Cómo se relacionaban ambos datasets?

Lección #1: Antes de tocar código, lee la documentación del dataset. Entiende qué representa cada fila y columna.


Exploración: conocer los datos antes de analizarlos

Aprendí que la fase de Análisis Exploratorio de Datos (EDA) no es opcional, es fundamental. Empecé con lo básico:

# Dimensiones
print(f"Filas: {df.shape[0]}, Columnas: {df.shape[1]}")

# Tipos de datos
print(df.info())

# Valores nulos
print(df.isna().sum())

# Valores únicos en columnas categóricas
print(df['REGION'].value_counts())

Esto me reveló cosas importantes:

  • Había 3 tamaños de empresa: Grande, Mediana, Pequeña
  • Había 7 regiones: CABA y GBA, Pampeana, Cuyo, etc.
  • Los datos tenían formato “largo”: una fila por combinación región-tamaño-variable
  • Había valores -99 que representaban datos faltantes

Lección #2: Las primeras 30 líneas de código deben ser solo exploración, no transformación.


Limpieza: el 80% del trabajo

Aquí es donde pasé más tiempo. Los datos “reales” nunca vienen limpios.

Problema 1: Formato pivoteado

Los datos tenían este formato:

REGION        | TAM     | VARIABLE      | VALOR
CABA y GBA    | Grande  | Empleo total  | 257,561
CABA y GBA    | Grande  | VENTAS        | 536,347,266,473

Pero para modelar necesitaba:

REGION        | TAM     | Empleo_total | VENTAS
CABA y GBA    | Grande  | 257,561      | 536,347,266,473

La solución fue pivotear:

pivot_df = df.pivot_table(
    index=['ANIO', 'REGION', 'TAM'],
    columns='VARIABLE',
    values='VALOR',
    aggfunc='sum'
).reset_index()

Problema 2: Valores -99 como “faltantes”

# Reemplazar -99 por NaN
empleo.replace(-99, np.nan, inplace=True)

Problema 3: Nombres inconsistentes

# Normalizar nombres de columnas
df.columns = df.columns.str.strip().str.lower()
pivot_df.rename(columns={
    'empleo total': 'Empleo_total',
    'ventas': 'VENTAS'
}, inplace=True)

Lección #3: La limpieza de datos no es glamorosa, pero es donde se ganan o pierden los proyectos.


Visualización: contar historias con datos

Después de limpiar, quería ver patrones. Las visualizaciones me ayudaron a entender relaciones que las tablas no mostraban.

Correlación entre Empleo y Ventas

import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
sns.scatterplot(
    data=pivot_df,
    x='Empleo_total',
    y='VENTAS',
    hue='TAM',
    palette='viridis',
    s=100
)
plt.title('¿Más empleados = Más ventas?')
plt.show()

Resultado: Sí, correlación positiva fuerte (0.80). Las empresas con más empleados generan más ventas.

Matriz de correlación

corr_matrix = pivot_df[['Empleo_total', 'VENTAS', 'VENTAS_por_empleado']].corr()

sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0)
plt.title('Matriz de Correlación')
plt.show()

Lección #4: Una buena visualización vale más que 1000 filas de datos.


Integración: unir múltiples datasets

El verdadero desafío fue integrar dos datasets diferentes:

  1. Dataset 1: Empleo y ventas por región/tamaño
  2. Dataset 2: Empleo registrado por provincia

El problema: uno tenía regiones, el otro provincias.

Solución: Mapeo manual

mapa_regiones = {
    'BUENOS AIRES': 'Pampeana',
    'CABA': 'CABA y GBA',
    'MENDOZA': 'Cuyo',
    'CHACO': 'NEA',
    # ... 24 provincias en total
}

empleo_agrupado['REGION'] = empleo_agrupado['PROVINCIA'].map(mapa_regiones)

Merge de datasets

merged_df = pd.merge(
    pivot_df,
    empleo_region,
    on=['ANIO', 'REGION'],
    how='inner'
)

Lección #5: La integración de datos requiere conocimiento del dominio. No hay función mágica que sepa que “Mendoza pertenece a Cuyo”.


Outliers: el dilema de qué hacer con ellos

Cuando hice boxplots, vi valores extremos:

def detectar_outliers_iqr(df, columna):
    Q1 = df[columna].quantile(0.25)
    Q3 = df[columna].quantile(0.75)
    IQR = Q3 - Q1
    limite_inf = Q1 - 1.5 * IQR
    limite_sup = Q3 + 1.5 * IQR
    outliers = (df[columna] < limite_inf) | (df[columna] > limite_sup)
    return outliers

Resultado: 15% de outliers en empleo, 11% en ventas.

Aquí tuve que tomar una decisión:

  • Eliminarlos todos: perdería información valiosa
  • Mantenerlos todos: sesgarían el modelo
  • Crear dos versiones: dataset completo para EDA, dataset limpio para modelado

Lección #6: Los outliers no siempre son errores. A veces son las observaciones más interesantes.


Machine Learning: primer intento fallido

Mi primer modelo fue un desastre:

# Intento 1: Directo sin preparación
from sklearn.linear_model import LinearRegression

X = merged_df[['Empleo_total', 'REGION', 'TAM']]  # ERROR: region y tam son strings
y = merged_df['VENTAS']

model = LinearRegression()
model.fit(X, y)
# ValueError: could not convert string to float

Aprendí que necesitaba preprocesamiento:

Paso 1: Codificar variables categóricas

from sklearn.preprocessing import LabelEncoder

le_region = LabelEncoder()
le_tam = LabelEncoder()

merged_df['REGION_ENC'] = le_region.fit_transform(merged_df['REGION'])
merged_df['TAM_ENC'] = le_tam.fit_transform(merged_df['TAM'])

Paso 2: Separar train/test

from sklearn.model_selection import train_test_split

X = merged_df[['Empleo_total', 'REGION_ENC', 'TAM_ENC']]
y = merged_df['VENTAS']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

Paso 3: Escalar features

from sklearn.preprocessing import RobustScaler

scaler = RobustScaler()  # Más robusto a outliers que StandardScaler
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

Lección #7: Los modelos no leen strings ni entienden escalas. El preprocesamiento es obligatorio.


Comparación de modelos: ¿cuál funciona mejor?

En lugar de apostar por un solo modelo, probé varios:

from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor

modelos = {
    'Regresión Lineal': LinearRegression(),
    'Ridge': Ridge(alpha=10.0),
    'Random Forest': RandomForestRegressor(max_depth=5, n_estimators=100),
    'Gradient Boosting': GradientBoostingRegressor(max_depth=3)
}

resultados = {}
for nombre, modelo in modelos.items():
    modelo.fit(X_train_scaled, y_train)
    y_pred = modelo.predict(X_test_scaled)
    
    r2 = r2_score(y_test, y_pred)
    mae = mean_absolute_error(y_test, y_pred)
    
    resultados[nombre] = {'R²': r2, 'MAE': mae}

Resultados

ModeloR² TestMAE
Regresión Lineal0.8158 mil millones
Ridge0.7957 mil millones
Random Forest0.7855 mil millones
Gradient Boosting0.5262 mil millones

El modelo más simple de Regresión Lineal había ganado.

¿Por qué? Con solo 72 observaciones después de limpiar outliers, modelos complejos como Random Forest y Gradient Boosting sufrían de overfitting.

Lección #8: Más complejo ≠ mejor. Con datasets pequeños, la simplicidad gana.


Análisis de errores: ¿dónde falla el modelo?

No basta con obtener un R² alto. Necesitaba entender dónde y por qué el modelo se equivoca.

Residuos por región

test_results = pd.DataFrame({
    'Real': y_test,
    'Predicho': y_pred_test,
    'Error_Pct': np.abs((y_test - y_pred_test) / y_test) * 100,
    'REGION': merged_df.loc[y_test.index, 'REGION']
})

error_region = test_results.groupby('REGION')['Error_Pct'].mean()
print(error_region.sort_values())

Descubrí que el modelo predecía peor en regiones con pocos datos (Norte Grande, NEA). Esto tiene sentido: menos muestras = peor aprendizaje.

Lección #9: Analiza errores por segmento. Un modelo “bueno en promedio” puede ser malo para ciertos grupos.


Validación cruzada: ¿el modelo es consistente?

Un R² de 0.81 en test está bien, pero ¿fue suerte? Para saberlo, usé validación cruzada:

from sklearn.model_selection import cross_val_score

cv_scores = cross_val_score(
    modelo, 
    X_train_scaled, 
    y_train, 
    cv=5,  # 5 folds
    scoring='r2'
)

print(f"R² promedio: {cv_scores.mean():.3f} ± {cv_scores.std():.3f}")
# Output: R² promedio: 0.776 ± 0.082

Esto me confirmó que el modelo es robusto, no dependía de una división afortunada de train/test.

Lección #10: La validación cruzada es tu seguro contra resultados engañosos.


Problemas que enfrenté y cómo los resolví

Problema 1: R² negativo (-41.81 en train)

Esto ocurrió cuando usé transformación logarítmica mal aplicada:

# ❌ MAL
y_log = np.log1p(y_train)
modelo.fit(X_train, y_log)
y_pred = modelo.predict(X_test)
r2 = r2_score(y_test, y_pred)  # Compara log vs original → desastre

Solución: volver a escala original antes de calcular métricas:

# ✅ BIEN
y_pred_log = modelo.predict(X_test_scaled)
y_pred = np.expm1(y_pred_log)  # De log a original
r2 = r2_score(y_test, y_pred)

Problema 2: Dataset muy pequeño (72 filas)

Estrategias que probé:

  • Regularización (Ridge/Lasso) para evitar overfitting
  • Reducir complejidad de Random Forest (max_depth=5)
  • Validación cruzada para aprovechar todos los datos
  • ❌ Data augmentation: no tiene sentido con datos estructurados reales

Problema 3: Interpretabilidad vs Performance

Random Forest daba feature importances claras, pero Regresión Lineal predecía mejor. Elegí Regresión Lineal por interpretabilidad:

# Coeficientes del modelo
coef_df = pd.DataFrame({
    'Variable': ['Empleo_total', 'REGION', 'TAM'],
    'Coeficiente': modelo.coef_
})

# Interpretación: cada empleado adicional aumenta las ventas en X pesos

Herramientas y librerías que aprendí

Análisis de datos

  • Pandas: manipulación de dataframes (merge, pivot, groupby)
  • NumPy: operaciones numéricas eficientes

Visualización

  • Matplotlib: gráficos básicos personalizables
  • Seaborn: visualizaciones estadísticas hermosas

Machine Learning

  • Scikit-learn: modelos, métricas, preprocesamiento, validación
  • train_test_split: división train/test estratificada
  • StandardScaler / RobustScaler: normalización
  • LabelEncoder: codificación de categóricas

Flujo de trabajo

  • Google Colab: notebooks en la nube sin setup
  • Git: versionado de código y notebooks

Experimentaciones futuras

Este fue mi primer proyecto, pero identifiqué mejoras:

  1. Más datos: conseguir información de 2015-2024
  2. Feature engineering: crear variables como “crecimiento año a año”
  3. Variables externas: inflación, tipo de cambio, PBI
  4. Modelos por segmento: un modelo específico para cada región
  5. Series temporales: si tuviera datos de múltiples años
  6. Deep Learning: probar redes neuronales (aunque probablemente sea overkill)

Conclusión: de intimidado a confiado

Lo que empezó como un trabajo universitario intimidante se convirtió en un proyecto completo de data science. Aprendí que:

  1. La limpieza de datos es el 80% del trabajo
  2. La visualización revela insights que las tablas ocultan
  3. Probar múltiples modelos es mejor que apostar por uno
  4. Con datasets pequeños, la simplicidad gana
  5. Los errores son esperables, analizarlos es clave
  6. La validación rigurosa separa modelos buenos de flukes

Mi consejo para quien empieza: no busques el proyecto perfecto. Empieza con datos reales, por caóticos que sean. Los mejores aprendizajes vienen de enfrentar problemas reales.

¿Tu turno? Busca un dataset que te interese en datos.gob.ar, Kaggle o UCI y empieza a ensuciarte las manos.

Últimos artículos

... tip: teclea algo secreto (una pista... CAMILO)