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:
- Dataset 1: Empleo y ventas por región/tamaño
- 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
| Modelo | R² Test | MAE |
|---|---|---|
| Regresión Lineal | 0.81 | 58 mil millones |
| Ridge | 0.79 | 57 mil millones |
| Random Forest | 0.78 | 55 mil millones |
| Gradient Boosting | 0.52 | 62 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:
- Más datos: conseguir información de 2015-2024
- Feature engineering: crear variables como “crecimiento año a año”
- Variables externas: inflación, tipo de cambio, PBI
- Modelos por segmento: un modelo específico para cada región
- Series temporales: si tuviera datos de múltiples años
- 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:
- La limpieza de datos es el 80% del trabajo
- La visualización revela insights que las tablas ocultan
- Probar múltiples modelos es mejor que apostar por uno
- Con datasets pequeños, la simplicidad gana
- Los errores son esperables, analizarlos es clave
- 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.