InterviewHack.ai
Start free
Blog/Preguntas de entrevista de Machine Learning con código (40+)

Preguntas de entrevista de Machine Learning con código (40+)

16 de septiembre de 2026

machine-learningpython

Artículo completo de preguntas de entrevista de Machine Learning con 42 preguntas numeradas, respuestas detalladas y código Python real.

Preguntas de entrevista de Machine Learning con código (40+)

Si estás preparando una entrevista para un rol de Machine Learning Engineer, Data Scientist o AI Engineer, sabés que la teoría sola no alcanza. Los entrevistadores quieren ver si podés escribir código que funcione bajo presión, explicar por qué elegiste un algoritmo y detectar errores en implementaciones ajenas.

Esta guía tiene 42 preguntas reales —del nivel que caen en empresas tech de LATAM, startups US y Big Tech— con respuestas detalladas y código Python ejecutable. Organizadas por tema para que las repaases en orden o las uses como repaso selectivo.


1. Fundamentos de ML

Pregunta 1: ¿Cuál es la diferencia entre bias y varianza? ¿Cómo se manifiesta en código?

Bias alto = el modelo es demasiado simple para capturar el patrón. Varianza alta = el modelo memorizó el training set y no generaliza.

python
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
import numpy as np

# Bias alto: árbol muy superficial (underfitting)
tree_shallow = DecisionTreeClassifier(max_depth=1)
scores = cross_val_score(tree_shallow, X, y, cv=5, scoring='accuracy')
print(f"Bias alto — train≈test≈bajo: {scores.mean():.3f}")

# Varianza alta: árbol sin límite (overfitting)
tree_deep = DecisionTreeClassifier()
scores = cross_val_score(tree_deep, X, y, cv=5, scoring='accuracy')
print(f"Varianza alta — train>>test: {scores.mean():.3f}")

El tradeoff se resuelve con regularización, más datos o modelos más complejos (con cuidado).


Pregunta 2: Implementá una función de train/validation/test split sin usar sklearn.

python
import numpy as np

def split_dataset(X, y, train=0.7, val=0.15, test=0.15, seed=42):
    assert abs(train + val + test - 1.0) < 1e-6, "Las proporciones deben sumar 1"
    np.random.seed(seed)
    n = len(X)
    idx = np.random.permutation(n)
    t = int(n * train)
    v = int(n * (train + val))
    return (X[idx[:t]], y[idx[:t]],
            X[idx[t:v]], y[idx[t:v]],
            X[idx[v:]], y[idx[v:]])

X_tr, y_tr, X_val, y_val, X_test, y_test = split_dataset(X, y)

Lo que verifica el entrevistador: que uses permutation y no shuffle in-place, que el split sea estratificado si el dataset está desbalanceado (agregar stratify=y si usás sklearn).


Pregunta 3: ¿Qué es la regularización L1 vs L2? Implementá ambas como término de pérdida.

python
import torch
import torch.nn as nn

def l1_regularization(model, lambda_=1e-4):
    l1 = sum(p.abs().sum() for p in model.parameters())
    return lambda_ * l1

def l2_regularization(model, lambda_=1e-4):
    l2 = sum((p ** 2).sum() for p in model.parameters())
    return lambda_ * l2

# En el loop de entrenamiento:
loss = criterion(outputs, labels)
loss += l1_regularization(model)  # o l2
optimizer.zero_grad()
loss.backward()
optimizer.step()

L1 produce sparsity (pesos exactamente 0, útil para feature selection). L2 penaliza pesos grandes pero no los elimina. En la práctica, ElasticNet combina ambas.


Pregunta 4: Explicá el gradiente descendente estocástico (SGD) e implementalo desde cero.

python
import numpy as np

def sgd(X, y, lr=0.01, epochs=100, batch_size=32):
    n, d = X.shape
    w = np.zeros(d)
    b = 0.0

    for epoch in range(epochs):
        idx = np.random.permutation(n)
        X_s, y_s = X[idx], y[idx]

        for i in range(0, n, batch_size):
            xb = X_s[i:i+batch_size]
            yb = y_s[i:i+batch_size]

            # Gradiente para regresión lineal (MSE)
            preds = xb @ w + b
            error = preds - yb
            dw = (2 / len(xb)) * xb.T @ error
            db = (2 / len(xb)) * error.sum()

            w -= lr * dw
            b -= lr * db
    return w, b

La diferencia con batch GD: en cada paso solo ves un mini-batch, lo que hace que el gradiente sea ruidoso pero el entrenamiento mucho más rápido en datasets grandes.


Pregunta 5: ¿Cómo detectás y tratás outliers en features numéricas antes de entrenar?

python
import pandas as pd
import numpy as np

def remove_outliers_iqr(df, col, factor=1.5):
    Q1 = df[col].quantile(0.25)
    Q3 = df[col].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - factor * IQR
    upper = Q3 + factor * IQR
    return df[(df[col] >= lower) & (df[col] <= upper)]

# Z-score para distribuciones aproximadamente normales
def remove_outliers_zscore(df, col, threshold=3):
    z = (df[col] - df[col].mean()) / df[col].std()
    return df[z.abs() < threshold]

# Opción menos agresiva: clipping
df['feature'] = df['feature'].clip(lower=df['feature'].quantile(0.01),
                                    upper=df['feature'].quantile(0.99))

Siempre calculá los límites SOLO en el training set y aplicalos al val/test para no filtrar información del futuro.


2. Algoritmos Clásicos

Pregunta 6: Implementá regresión logística desde cero con numpy.

python
import numpy as np

class LogisticRegression:
    def __init__(self, lr=0.01, epochs=1000):
        self.lr = lr
        self.epochs = epochs

    def sigmoid(self, z):
        return 1 / (1 + np.exp(-np.clip(z, -500, 500)))

    def fit(self, X, y):
        n, d = X.shape
        self.w = np.zeros(d)
        self.b = 0.0
        for _ in range(self.epochs):
            z = X @ self.w + self.b
            p = self.sigmoid(z)
            dw = (1/n) * X.T @ (p - y)
            db = (1/n) * (p - y).sum()
            self.w -= self.lr * dw
            self.b -= self.lr * db
        return self

    def predict_proba(self, X):
        return self.sigmoid(X @ self.w + self.b)

    def predict(self, X, threshold=0.5):
        return (self.predict_proba(X) >= threshold).astype(int)

Ojo con np.clip en el sigmoid: sin eso, exp(-z) genera overflow para z muy negativos.


Pregunta 7: ¿Cómo funciona un árbol de decisión? Implementá el cálculo de impureza Gini.

python
import numpy as np

def gini_impurity(y):
    if len(y) == 0:
        return 0
    classes, counts = np.unique(y, return_counts=True)
    probs = counts / len(y)
    return 1 - np.sum(probs ** 2)

def gini_split(y_left, y_right):
    n = len(y_left) + len(y_right)
    return (len(y_left)/n) * gini_impurity(y_left) + \
           (len(y_right)/n) * gini_impurity(y_right)

# Encontrar el mejor split para una feature continua
def best_split(X_col, y):
    thresholds = np.unique(X_col)
    best_gini = float('inf')
    best_thresh = None
    for t in thresholds:
        left = y[X_col <= t]
        right = y[X_col > t]
        g = gini_split(left, right)
        if g < best_gini:
            best_gini = g
            best_thresh = t
    return best_thresh, best_gini

Pregunta 8: Explicá cómo funciona Random Forest y por qué reduce la varianza.

python
from sklearn.ensemble import RandomForestClassifier
import numpy as np

# Random Forest = bagging de árboles con feature randomness
rf = RandomForestClassifier(
    n_estimators=100,
    max_features='sqrt',   # solo sqrt(n_features) en cada split
    max_samples=0.8,       # 80% del training set por árbol (bagging)
    random_state=42
)
rf.fit(X_train, y_train)

# Feature importance: promedio de reducción de impureza
importance = pd.Series(rf.feature_importances_, index=feature_names)
importance.sort_values().plot.barh()

La magia: los árboles individuales tienen alta varianza pero están decorrelacionados (distintas muestras + distintas features). Al promediar, la varianza baja como 1/n mientras el bias queda igual.


Pregunta 9: ¿Cuándo usás SVM en lugar de un modelo más simple?

python
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# SVM es sensible a la escala: siempre escalar
svm_pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('svm', SVC(kernel='rbf', C=1.0, gamma='scale', probability=True))
])

svm_pipeline.fit(X_train, y_train)

Usás SVM cuando: (1) el dataset es chico pero de alta dimensión (texto, genes), (2) la clase boundary no es lineal pero tampoco querés un ensemble complejo, (3) necesitás un margen de separación robusto. Con >100k filas, la complejidad O(n²) lo hace impracticable.


Pregunta 10: Implementá K-Means desde cero.

python
import numpy as np

def kmeans(X, k, max_iter=300, tol=1e-4, seed=42):
    np.random.seed(seed)
    idx = np.random.choice(len(X), k, replace=False)
    centroids = X[idx].copy()

    for _ in range(max_iter):
        dists = np.linalg.norm(X[:, None] - centroids[None], axis=2)
        labels = np.argmin(dists, axis=1)

        new_centroids = np.array([X[labels == j].mean(axis=0)
                                   if (labels == j).any() else centroids[j]
                                   for j in range(k)])

        if np.linalg.norm(new_centroids - centroids) < tol:
            break
        centroids = new_centroids

    return labels, centroids

labels, centroids = kmeans(X, k=3)

El entrevistador espera que menciones la inicialización (aleatoria vs k-means++), el criterio de parada y que el algoritmo puede converger a mínimos locales.


3. Evaluación de Modelos

Pregunta 11: ¿Por qué accuracy no es suficiente para clasificación desbalanceada? Calculá F1, precision y recall desde cero.

python
import numpy as np

def confusion_matrix_manual(y_true, y_pred):
    TP = ((y_true == 1) & (y_pred == 1)).sum()
    TN = ((y_true == 0) & (y_pred == 0)).sum()
    FP = ((y_true == 0) & (y_pred == 1)).sum()
    FN = ((y_true == 1) & (y_pred == 0)).sum()
    return TP, TN, FP, FN

def metrics(y_true, y_pred):
    TP, TN, FP, FN = confusion_matrix_manual(y_true, y_pred)
    precision = TP / (TP + FP + 1e-9)
    recall    = TP / (TP + FN + 1e-9)
    f1        = 2 * precision * recall / (precision + recall + 1e-9)
    accuracy  = (TP + TN) / len(y_true)
    return {'precision': precision, 'recall': recall, 'f1': f1, 'accuracy': accuracy}

# Ejemplo: fraude (1% positivos)
y_true = np.array([0]*99 + [1])
y_pred = np.zeros(100, dtype=int)
print(metrics(y_true, y_pred))  # accuracy=0.99, f1=0.0

Un modelo que siempre dice "no fraude" tiene 99% de accuracy pero F1=0. Por eso en fraude, detección de enfermedades o moderación de contenido se usa F1, AUC-ROC o AUC-PR.


Pregunta 12: Calculá AUC-ROC sin sklearn.

python
import numpy as np

def roc_auc_manual(y_true, y_scores):
    thresholds = np.sort(np.unique(y_scores))[::-1]
    tprs, fprs = [0], [0]
    pos = (y_true == 1).sum()
    neg = (y_true == 0).sum()

    for t in thresholds:
        preds = (y_scores >= t).astype(int)
        tp = ((preds == 1) & (y_true == 1)).sum()
        fp = ((preds == 1) & (y_true == 0)).sum()
        tprs.append(tp / pos)
        fprs.append(fp / neg)

    tprs.append(1); fprs.append(1)
    auc = np.trapz(tprs, fprs)
    return abs(auc)

auc = roc_auc_manual(y_test, model.predict_proba(X_test)[:, 1])
print(f"AUC-ROC: {auc:.4f}")

Pregunta 13: ¿Qué es cross-validation y cuándo usás StratifiedKFold?

python
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.ensemble import GradientBoostingClassifier

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
model = GradientBoostingClassifier()

scores = cross_val_score(model, X, y, cv=skf, scoring='roc_auc')
print(f"AUC: {scores.mean():.3f} ± {scores.std():.3f}")

Usás StratifiedKFold siempre que la distribución de clases sea desigual (>60/40). Si el dataset tiene grupos (mismo paciente en múltiples filas), usás GroupKFold para no filtrar información entre folds.


Pregunta 14: Implementá early stopping manual en un loop de entrenamiento.

python
def train_with_early_stopping(model, X_tr, y_tr, X_val, y_val,
                               patience=10, metric_fn=None):
    best_val = float('inf')
    best_epoch = 0
    best_weights = None
    no_improve = 0

    for epoch in range(1000):
        model.partial_fit(X_tr, y_tr)
        val_loss = metric_fn(model, X_val, y_val)

        if val_loss < best_val - 1e-4:
            best_val = val_loss
            best_epoch = epoch
            best_weights = copy.deepcopy(model.__dict__)
            no_improve = 0
        else:
            no_improve += 1

        if no_improve >= patience:
            print(f"Early stop en epoch {epoch}, mejor: {best_epoch}")
            model.__dict__.update(best_weights)
            break

    return model

Early stopping es la forma más práctica de regularización en redes neuronales: cuando la pérdida de validación deja de mejorar, guardás los pesos del mejor epoch.


4. Feature Engineering y Preprocesamiento

Pregunta 15: ¿Cuándo usás StandardScaler vs MinMaxScaler vs RobustScaler?

python
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

# StandardScaler: media 0, std 1 — para algoritmos que asumen distribución normal
ss = StandardScaler()

# MinMaxScaler: [0,1] — para redes neuronales con sigmoid/tanh de output
mm = MinMaxScaler()

# RobustScaler: usa IQR — ideal cuando hay outliers que no querés eliminar
rs = RobustScaler()

# SIEMPRE: fit solo en train, transform en val/test
ss.fit(X_train)
X_train_s = ss.transform(X_train)
X_test_s  = ss.transform(X_test)

El error más común en entrevistas: hacer fit_transform(X) en todo el dataset antes de splitear. Eso es data leakage.


Pregunta 16: ¿Qué es data leakage y cómo lo evitás con un Pipeline?

python
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression

# MAL: leakage — los parámetros del scaler ven el test set
X_scaled = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y)

# BIEN: el pipeline aplica fit solo al training fold
pipe = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler()),
    ('clf', LogisticRegression())
])
pipe.fit(X_train, y_train)
pipe.score(X_test, y_test)

El Pipeline garantiza que en cada fold de CV, el preprocessor se re-fittea solo con los datos de ese fold.


Pregunta 17: Encodings para variables categóricas — ¿cuándo cada uno?

python
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder
from category_encoders import TargetEncoder

# One-Hot: para categóricas sin orden con cardinalidad baja (<20)
ohe = OneHotEncoder(sparse_output=False, handle_unknown='ignore')

# Ordinal: cuando HAY orden real (ej: bajo/medio/alto)
oe = OrdinalEncoder(categories=[['bajo', 'medio', 'alto']])

# Target Encoding: para alta cardinalidad (ciudad, ID de producto)
te = TargetEncoder(smoothing=10)

# Para cardinalidad altísima (>1000): feature hashing
from sklearn.feature_extraction import FeatureHasher
fh = FeatureHasher(n_features=64, input_type='string')

Target Encoding tiene riesgo de leakage: el smoother reduce el impacto de categorías con pocas muestras y siempre se fittea solo en el training fold.


Pregunta 18: Implementá imputación de valores faltantes con estrategia según el tipo de feature.

python
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer, KNNImputer

def smart_impute(df, strategy='auto'):
    df = df.copy()
    for col in df.columns:
        if df[col].isnull().sum() == 0:
            continue
        if df[col].dtype in ['float64', 'int64']:
            df[col] = SimpleImputer(strategy='median').fit_transform(
                df[[col]]).ravel()
        else:
            df[col] = df[col].fillna(df[col].mode()[0])
    return df

# Alternativa potente: KNN imputer para variables correlacionadas
knn_imp = KNNImputer(n_neighbors=5)
X_imputed = knn_imp.fit_transform(X_train)

5. Gradient Boosting y Ensembles

Pregunta 19: ¿Cuál es la diferencia entre Bagging y Boosting?

python
from sklearn.ensemble import BaggingClassifier, AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier

# Bagging: modelos paralelos, independientes, en subsets aleatorios
bagging = BaggingClassifier(
    estimator=DecisionTreeClassifier(max_depth=10),
    n_estimators=100,
    max_samples=0.8,
    bootstrap=True
)

# Boosting: modelos secuenciales, cada uno aprende de los errores del anterior
boosting = AdaBoostClassifier(
    estimator=DecisionTreeClassifier(max_depth=1),
    n_estimators=100,
    learning_rate=0.1
)

Regla práctica: si el modelo base sufre overfitting → Bagging. Si sufre underfitting → Boosting. XGBoost y LightGBM son Gradient Boosting optimizados.


Pregunta 20: Ajuste de hiperparámetros de XGBoost con Optuna.

python
import optuna
import xgboost as xgb
from sklearn.model_selection import cross_val_score

def objective(trial):
    params = {
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'learning_rate': trial.suggest_float('learning_rate', 1e-3, 0.3, log=True),
        'n_estimators': trial.suggest_int('n_estimators', 50, 500),
        'subsample': trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-5, 10, log=True),
        'eval_metric': 'logloss',
        'use_label_encoder': False
    }
    model = xgb.XGBClassifier(**params, random_state=42)
    return cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc').mean()

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(f"Mejores params: {study.best_params}")

6. Redes Neuronales

Pregunta 21: Implementá un MLP desde cero con numpy (forward + backward).

python
import numpy as np

class MLP:
    def __init__(self, layers):
        self.weights = []
        self.biases = []
        for i in range(len(layers)-1):
            w = np.random.randn(layers[i], layers[i+1]) * np.sqrt(2/layers[i])
            b = np.zeros((1, layers[i+1]))
            self.weights.append(w)
            self.biases.append(b)

    def relu(self, z): return np.maximum(0, z)
    def relu_grad(self, z): return (z > 0).astype(float)
    def sigmoid(self, z): return 1 / (1 + np.exp(-z))

    def forward(self, X):
        self.zs, self.as_ = [], [X]
        a = X
        for i, (w, b) in enumerate(zip(self.weights, self.biases)):
            z = a @ w + b
            self.zs.append(z)
            a = self.relu(z) if i < len(self.weights)-1 else self.sigmoid(z)
            self.as_.append(a)
        return a

    def backward(self, y, lr=0.01):
        n = len(y)
        delta = self.as_[-1] - y.reshape(-1, 1)
        for i in reversed(range(len(self.weights))):
            dw = self.as_[i].T @ delta / n
            db = delta.mean(axis=0, keepdims=True)
            self.weights[i] -= lr * dw
            self.biases[i] -= lr * db
            if i > 0:
                delta = (delta @ self.weights[i].T) * self.relu_grad(self.zs[i-1])

Pregunta 22: ¿Qué es batch normalization y por qué acelera el entrenamiento?

python
import torch
import torch.nn as nn

class MLPConBN(nn.Module):
    def __init__(self, in_dim, hidden, out_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden),
            nn.BatchNorm1d(hidden),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(hidden, hidden),
            nn.BatchNorm1d(hidden),
            nn.ReLU(),
            nn.Linear(hidden, out_dim)
        )

    def forward(self, x):
        return self.net(x)

BN normaliza las activaciones a media≈0, std≈1 dentro de cada batch. Beneficios: (1) permite learning rates más altos, (2) actúa como regularizador, (3) mitiga el covariate shift interno. Durante inference usa las estadísticas acumuladas del training.


Pregunta 23: Implementá un loop de entrenamiento completo en PyTorch.

python
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

def train_epoch(model, loader, optimizer, criterion, device):
    model.train()
    total_loss = 0
    for X_batch, y_batch in loader:
        X_batch, y_batch = X_batch.to(device), y_batch.to(device)
        optimizer.zero_grad()
        outputs = model(X_batch)
        loss = criterion(outputs, y_batch)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        total_loss += loss.item()
    return total_loss / len(loader)

def eval_epoch(model, loader, criterion, device):
    model.eval()
    total_loss = 0
    with torch.no_grad():
        for X_batch, y_batch in loader:
            X_batch, y_batch = X_batch.to(device), y_batch.to(device)
            outputs = model(X_batch)
            total_loss += criterion(outputs, y_batch).item()
    return total_loss / len(loader)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MLPConBN(X_train.shape[1], 128, 1).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
criterion = nn.BCEWithLogitsLoss()
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)

7. NLP y Embeddings

Pregunta 24: ¿Cuál es la diferencia entre TF-IDF y embeddings? ¿Cuándo usás cada uno?

python
from sklearn.feature_extraction.text import TfidfVectorizer
from sentence_transformers import SentenceTransformer

corpus = ["La reunión fue cancelada", "El equipo tuvo una junta", "El partido fue suspendido"]

# TF-IDF: bag of words ponderado — rápido, interpretable, no entiende semántica
tfidf = TfidfVectorizer(ngram_range=(1,2), max_features=10000)
X_tfidf = tfidf.fit_transform(corpus)

# Embeddings: representación densa — captura "reunión" ≈ "junta"
model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
X_emb = model.encode(corpus)

from sklearn.metrics.pairwise import cosine_similarity
sims = cosine_similarity(X_emb)
print(f"Sim(reunión, junta): {sims[0,1]:.3f}")  # ~0.85

Usá TF-IDF cuando el texto es corto, la taxonomía es fija y querés interpretabilidad. Embeddings cuando necesitás semántica, paráfrasis o búsqueda por significado.


Pregunta 25: Implementá búsqueda semántica con FAISS.

python
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer

encoder = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
docs = ["Python tutorial for beginners", "Machine learning crash course",
        "Deep learning with PyTorch", "Data structures and algorithms"]

embeddings = encoder.encode(docs, normalize_embeddings=True)
dim = embeddings.shape[1]

index = faiss.IndexFlatIP(dim)
index.add(embeddings.astype(np.float32))

query = "intro a ML para principiantes"
q_emb = encoder.encode([query], normalize_embeddings=True).astype(np.float32)
scores, indices = index.search(q_emb, k=3)
for i, (score, idx) in enumerate(zip(scores[0], indices[0])):
    print(f"{i+1}. [{score:.3f}] {docs[idx]}")

8. ML en Producción (MLOps)

Pregunta 26: ¿Cómo detectás data drift en producción?

python
from scipy.stats import ks_2samp, chi2_contingency
import numpy as np

def detect_drift_numeric(ref_data, prod_data, threshold=0.05):
    stat, p_value = ks_2samp(ref_data, prod_data)
    return {'statistic': stat, 'p_value': p_value, 'drift': p_value < threshold}

def detect_drift_categorical(ref_data, prod_data):
    ref_counts = pd.Series(ref_data).value_counts()
    prod_counts = pd.Series(prod_data).value_counts()
    all_cats = set(ref_counts.index) | set(prod_counts.index)
    ref_freq = [ref_counts.get(c, 0) for c in all_cats]
    prod_freq = [prod_counts.get(c, 0) for c in all_cats]
    _, p_value, _, _ = chi2_contingency([ref_freq, prod_freq])
    return {'p_value': p_value, 'drift': p_value < 0.05}

drift = detect_drift_numeric(train_data['age'], prod_data['age'])
if drift['drift']:
    print(f"DRIFT DETECTADO en 'age' (p={drift['p_value']:.4f}) — reentrenar")

Pregunta 27: Implementá un modelo con versionado usando MLflow.

python
import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score

mlflow.set_experiment("churn-prediction")

with mlflow.start_run(run_name="rf-v2"):
    params = {'n_estimators': 200, 'max_depth': 8, 'min_samples_leaf': 10}
    mlflow.log_params(params)

    model = RandomForestClassifier(**params, random_state=42)
    model.fit(X_train, y_train)

    train_auc = roc_auc_score(y_train, model.predict_proba(X_train)[:, 1])
    val_auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1])
    mlflow.log_metric("train_auc", train_auc)
    mlflow.log_metric("val_auc", val_auc)
    mlflow.log_metric("overfit_gap", train_auc - val_auc)

    mlflow.sklearn.log_model(model, "model",
                              registered_model_name="ChurnRF",
                              input_example=X_train[:5])

Pregunta 28: ¿Cómo implementás A/B testing para modelos ML?

python
import numpy as np
from scipy.stats import ttest_ind

class ModelABTest:
    def __init__(self, model_a, model_b, split=0.5):
        self.model_a = model_a
        self.model_b = model_b
        self.split = split
        self.results_a, self.results_b = [], []

    def route(self, user_id):
        return 'A' if hash(str(user_id)) % 100 < self.split * 100 else 'B'

    def predict(self, X, user_id):
        model = self.model_a if self.route(user_id) == 'A' else self.model_b
        return model.predict(X)

    def log_outcome(self, user_id, metric_value):
        if self.route(user_id) == 'A':
            self.results_a.append(metric_value)
        else:
            self.results_b.append(metric_value)

    def significance_test(self, alpha=0.05):
        stat, p = ttest_ind(self.results_a, self.results_b)
        winner = 'A' if np.mean(self.results_a) > np.mean(self.results_b) else 'B'
        significant = p < alpha
        return {'p_value': p, 'winner': winner if significant else 'no winner yet',
                'mean_a': np.mean(self.results_a), 'mean_b': np.mean(self.results_b)}

9. Algoritmos de Ranking y Recomendación

Pregunta 29: Implementá collaborative filtering con matrix factorization.

python
import numpy as np

class MatrixFactorization:
    def __init__(self, n_factors=20, lr=0.01, reg=0.1, epochs=50):
        self.k = n_factors
        self.lr = lr
        self.reg = reg
        self.epochs = epochs

    def fit(self, R):
        n_users, n_items = R.shape
        self.P = np.random.normal(0, 0.1, (n_users, self.k))
        self.Q = np.random.normal(0, 0.1, (n_items, self.k))
        mask = ~np.isnan(R)

        for epoch in range(self.epochs):
            for u, i in zip(*np.where(mask)):
                err = R[u, i] - self.P[u] @ self.Q[i]
                self.P[u] += self.lr * (err * self.Q[i] - self.reg * self.P[u])
                self.Q[i] += self.lr * (err * self.P[u] - self.reg * self.Q[i])

    def predict(self, user, item):
        return self.P[user] @ self.Q[item]

    def recommend(self, user, top_k=10, seen_items=None):
        scores = self.P[user] @ self.Q.T
        if seen_items:
            scores[seen_items] = -np.inf
        return np.argsort(scores)[::-1][:top_k]

Pregunta 30: ¿Cómo evaluás un sistema de recomendación?

python
import numpy as np

def precision_at_k(recommended, relevant, k):
    recs_k = set(recommended[:k])
    return len(recs_k & set(relevant)) / k

def recall_at_k(recommended, relevant, k):
    recs_k = set(recommended[:k])
    rel = set(relevant)
    return len(recs_k & rel) / len(rel) if rel else 0

def ndcg_at_k(recommended, relevant, k):
    def dcg(items, relevant_set, k):
        return sum(1/np.log2(i+2) for i, item in enumerate(items[:k])
                   if item in relevant_set)
    ideal = sorted(recommended[:k], key=lambda x: x in set(relevant), reverse=True)
    idcg = dcg(ideal, set(relevant), k)
    return dcg(recommended, set(relevant), k) / idcg if idcg > 0 else 0

recs = [3, 1, 5, 7, 2]
relevant = {1, 3, 6}
print(f"P@3: {precision_at_k(recs, relevant, 3):.3f}")
print(f"NDCG@5: {ndcg_at_k(recs, relevant, 5):.3f}")

10. Deep Learning Avanzado

Pregunta 31: Implementá atención multi-cabeza (Multi-Head Attention) desde cero.

python
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        assert d_model % n_heads == 0
        self.d_k = d_model // n_heads
        self.n_heads = n_heads
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def split_heads(self, x, batch):
        return x.view(batch, -1, self.n_heads, self.d_k).transpose(1, 2)

    def forward(self, Q, K, V, mask=None):
        batch = Q.size(0)
        Q = self.split_heads(self.W_q(Q), batch)
        K = self.split_heads(self.W_k(K), batch)
        V = self.split_heads(self.W_v(V), batch)

        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        attn = F.softmax(scores, dim=-1)

        out = (attn @ V).transpose(1, 2).contiguous()
        out = out.view(batch, -1, self.n_heads * self.d_k)
        return self.W_o(out), attn

Pregunta 32: ¿Qué es el problema del gradiente que desaparece y cómo se mitiga en LSTMs?

python
import torch.nn as nn

class SentimentLSTM(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, n_layers):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.lstm = nn.LSTM(embed_dim, hidden_dim,
                            num_layers=n_layers,
                            batch_first=True,
                            dropout=0.3,
                            bidirectional=True)
        self.fc = nn.Linear(hidden_dim * 2, 1)
        self.dropout = nn.Dropout(0.3)

    def forward(self, x):
        emb = self.dropout(self.embedding(x))
        out, (hidden, cell) = self.lstm(emb)
        hidden = torch.cat([hidden[-2], hidden[-1]], dim=1)
        return self.fc(self.dropout(hidden))

El gradiente que desaparece ocurre porque multiplicar muchos Jacobians <1 da un gradiente ~0 en las capas tempranas. LSTM lo resuelve con el cell state, que permite gradientes fluir sin multiplicarse repetidamente.


11. Preguntas de Diseño de Sistemas ML

Pregunta 33: Diseñá un sistema de detección de fraude en tiempo real.

python
class FraudDetectionSystem:
    def __init__(self):
        self.feature_store = FeatureStore()
        self.model = LightGBMModel.load('fraud_model_v3')
        self.rules = RuleEngine()

    def score_transaction(self, txn: dict) -> dict:
        features = self.feature_store.get_features(
            user_id=txn['user_id'],
            window=['1m', '1h', '24h']
        )

        rule_verdict = self.rules.evaluate(txn, features)
        if rule_verdict['action'] == 'block':
            return rule_verdict

        score = self.model.predict_proba(features)[0][1]
        threshold = self.get_threshold(txn['merchant_category'])
        action = 'block' if score > 0.9 else 'review' if score > threshold else 'approve'

        return {'action': action, 'score': score, 'features': features}

En producción: feature store (Feast o Redis), dos modelos (one fast + one slow para review async), feedback loop de chargebacks, monitoreo de drift en features de ventana temporal.


Pregunta 34: ¿Cómo estructurías el pipeline de training de un modelo que se reentrana semanalmente?

python
from prefect import flow, task

@task(retries=2)
def extract_training_data(start_date, end_date):
    return query_feature_store(start_date, end_date)

@task
def validate_data(df):
    assert df.shape[0] > MIN_ROWS, "Datos insuficientes"
    assert df['label'].mean() > 0.001, "Sin positivos suficientes"
    return df

@task
def train_model(df, best_params):
    model = xgb.XGBClassifier(**best_params)
    model.fit(df.drop('label', axis=1), df['label'])
    return model

@task
def evaluate_and_promote(model, holdout):
    auc = roc_auc_score(holdout['label'],
                        model.predict_proba(holdout.drop('label', axis=1))[:, 1])
    current_auc = get_production_model_metric('auc')
    if auc > current_auc - 0.005:
        promote_to_production(model)
        return True
    return False

@flow(name="weekly-retraining")
def retrain_pipeline():
    df = extract_training_data(weeks_back=8)
    df = validate_data(df)
    model = train_model(df, load_best_params())
    evaluate_and_promote(model, load_holdout())

12. Estadística y Probabilidad

Pregunta 35: Implementá bootstrap confidence intervals.

python
import numpy as np

def bootstrap_ci(data, statistic_fn, n_bootstrap=10000, ci=0.95, seed=42):
    np.random.seed(seed)
    n = len(data)
    bootstrap_stats = [statistic_fn(np.random.choice(data, size=n, replace=True))
                       for _ in range(n_bootstrap)]

    alpha = (1 - ci) / 2
    return {
        'estimate': statistic_fn(data),
        'ci_lower': np.percentile(bootstrap_stats, alpha * 100),
        'ci_upper': np.percentile(bootstrap_stats, (1 - alpha) * 100),
        'ci_level': ci
    }

Pregunta 36: ¿Qué es el teorema de Bayes y cómo se aplica en un clasificador Naive Bayes?

python
import numpy as np

class GaussianNaiveBayes:
    def fit(self, X, y):
        self.classes = np.unique(y)
        self.priors = {}
        self.means = {}
        self.stds = {}
        for c in self.classes:
            X_c = X[y == c]
            self.priors[c] = len(X_c) / len(X)
            self.means[c] = X_c.mean(axis=0)
            self.stds[c]  = X_c.std(axis=0) + 1e-9

    def gaussian_pdf(self, x, mean, std):
        return np.exp(-((x - mean) ** 2) / (2 * std ** 2)) / (np.sqrt(2 * np.pi) * std)

    def predict(self, X):
        preds = []
        for x in X:
            posteriors = {c: np.log(self.priors[c]) + np.sum(
                np.log(self.gaussian_pdf(x, self.means[c], self.stds[c])))
                for c in self.classes}
            preds.append(max(posteriors, key=posteriors.get))
        return np.array(preds)

13. Preguntas Trampa y de Debugging

Pregunta 37: ¿Qué está mal en este código de entrenamiento?

python
# CÓDIGO CON BUG
X_scaled = StandardScaler().fit_transform(X)  # BUG: fit en todo el dataset
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)

# CORRECCIÓN
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s  = scaler.transform(X_test)  # solo transform

El bug es fit_transform(X) antes del split: la media y std del scaler ya "conocen" el test set. En features de alta cardinalidad este leak puede inflarte el AUC 2-5 puntos.


Pregunta 38: Tu modelo tiene AUC=0.95 en validación pero 0.61 en producción. ¿Qué revisás primero?

python
# 1. Leakage de features temporales
df.sort_values('date', inplace=True)  # siempre splitear por tiempo

# 2. Train-test distribution shift
from scipy.stats import ks_2samp
for col in features:
    stat, p = ks_2samp(train[col].dropna(), prod[col].dropna())
    if p < 0.05:
        print(f"DRIFT: {col} (p={p:.4f})")

# 3. Label leak
corr_with_target = X_train.corrwith(pd.Series(y_train))
suspicious = corr_with_target[corr_with_target.abs() > 0.9]
print(f"Features sospechosas: {suspicious.index.tolist()}")

# 4. Concept drift
print(f"Train positive rate: {y_train.mean():.4f}")
print(f"Prod positive rate: {y_prod.mean():.4f}")

Pregunta 39: Implementá un sistema de logging de predicciones para monitoreo.

python
import json
import hashlib
from datetime import datetime

class PredictionLogger:
    def __init__(self, model_name, model_version, storage_backend):
        self.model_name = model_name
        self.model_version = model_version
        self.storage = storage_backend

    def log(self, input_features: dict, prediction: float,
            confidence: float, user_id: str = None):
        record = {
            'timestamp': datetime.utcnow().isoformat(),
            'model_name': self.model_name,
            'model_version': self.model_version,
            'prediction_id': hashlib.md5(
                f"{user_id}{datetime.utcnow()}".encode()).hexdigest()[:16],
            'user_id': user_id,
            'prediction': round(float(prediction), 6),
            'confidence': round(float(confidence), 6),
            'feature_hash': hashlib.sha256(
                json.dumps(input_features, sort_keys=True).encode()).hexdigest()[:16],
        }
        self.storage.write(record)
        return record['prediction_id']

14. Casos de Estudio Rápidos

Pregunta 40: Tenés un dataset de 1 millón de filas y 500 features. ¿Cómo empezás?

python
import pandas as pd
import numpy as np
from sklearn.feature_selection import mutual_info_classif, VarianceThreshold

# 1. Entender la estructura
print(df.info(memory_usage='deep'))
print(df.isnull().sum().sort_values(ascending=False).head(20))

# 2. Eliminar features con varianza cero
vt = VarianceThreshold(threshold=0.01)
X_filtered = vt.fit_transform(X)

# 3. Eliminar features altamente correlacionadas
corr_matrix = pd.DataFrame(X_filtered).corr().abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
to_drop = [col for col in upper.columns if any(upper[col] > 0.95)]
X_reduced = pd.DataFrame(X_filtered).drop(columns=to_drop)

# 4. Mutual information para feature importance
mi = mutual_info_classif(X_reduced, y, random_state=42)
top_features = np.argsort(mi)[::-1][:50]

Pregunta 41: Diseñá las features para un modelo de churn de usuarios SaaS.

python
def build_churn_features(user_id, events_df, billing_df, support_df):
    features = {}

    for window in [7, 30, 90]:
        recent = events_df[events_df['user_id'] == user_id].tail(window)
        features[f'logins_{window}d'] = recent[recent['event'] == 'login'].shape[0]
        features[f'core_actions_{window}d'] = recent[recent['core_feature'] == 1].shape[0]
        features[f'days_active_{window}d'] = recent['date'].nunique()

    features['engagement_trend'] = (
        features['logins_7d'] / (features['logins_30d'] / 4 + 1e-6)
    )

    b = billing_df[billing_df['user_id'] == user_id]
    features['days_since_last_payment'] = (pd.Timestamp.now() - b['paid_at'].max()).days
    features['failed_payments_last_3m'] = b[b['status'] == 'failed']['amount'].count()
    features['mrr'] = b['mrr'].iloc[-1] if len(b) else 0

    s = support_df[support_df['user_id'] == user_id]
    features['open_tickets'] = s[s['status'] == 'open'].shape[0]
    features['tickets_last_30d'] = s[s['created_at'] > pd.Timestamp.now() - pd.Timedelta('30d')].shape[0]
    features['account_age_days'] = (pd.Timestamp.now() - b['created_at'].min()).days

    return features

Pregunta 42: ¿Cómo calibrás las probabilidades de un clasificador que da scores mal calibrados?

python
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
import matplotlib.pyplot as plt

def plot_calibration(y_true, y_prob, model_name="Modelo"):
    frac_positives, mean_pred = calibration_curve(y_true, y_prob, n_bins=10)
    plt.plot(mean_pred, frac_positives, 's-', label=model_name)
    plt.plot([0,1], [0,1], 'k--', label='Perfecto')
    plt.xlabel('Probabilidad predicha')
    plt.ylabel('Fracción real de positivos')
    plt.legend()

uncalibrated = xgb.XGBClassifier().fit(X_train, y_train)

cal_platt = CalibratedClassifierCV(uncalibrated, method='sigmoid', cv='prefit')
cal_platt.fit(X_val, y_val)

cal_isotonic = CalibratedClassifierCV(uncalibrated, method='isotonic', cv='prefit')
cal_isotonic.fit(X_val, y_val)

# Platt Scaling: asume relación sigmoide — funciona con pocos datos de calibración
# Isotonic Regression: más flexible, necesita ~1000 ejemplos mínimo

Consejos para la entrevista en vivo

Antes de escribir una línea de código, repetí el problema en voz alta y confirmá los inputs/outputs esperados. Los entrevistadores valoran más que entendas el problema que que escribas código perfecto.

Cuando no sepas algo, decilo directamente: "no recuerdo el nombre exacto de esa función pero haría así..." — y mostrá el razonamiento. Improvisar con confianza sobre código incorrecto es peor que admitir que no recordás algo puntual.

Si el entrevistador te da un dataset desbalanceado, mencioná las opciones antes de elegir una: oversampling (SMOTE), undersampling, ajuste del umbral, o ponderar las clases con class_weight='balanced'. Eso muestra amplitud de criterio.

Y practicá en voz alta. Leer código en silencio es distinto a explicar lo que hacés mientras lo escribís. Tu cerebro necesita ese entrenamiento específico.

FAQ

¿Qué preguntas de Machine Learning caen siempre en una entrevista técnica?+

Las preguntas más frecuentes cubren: bias-varianza tradeoff, regularización L1/L2, diferencia entre Random Forest y Gradient Boosting, métricas de evaluación para datos desbalanceados (F1, AUC-ROC), data leakage, y al menos una implementación desde cero (regresión logística, K-Means o descenso por gradiente). Preparate para escribir código Python real, no solo explicar teoría.

¿Cómo explico bias-varianza en una entrevista de ML sin sonar genérico?+

En lugar de definirlo, mostrá el diagnóstico: si train AUC es 0.98 y val AUC es 0.71, eso es varianza alta (overfitting). Si ambos son 0.65, es bias alto (underfitting). Mencioná la solución concreta: para varianza alta, más regularización o más datos; para bias alto, modelo más complejo o features nuevas.

¿Qué es data leakage en Machine Learning y cómo se detecta?+

Data leakage ocurre cuando información del futuro o del test set contamina el entrenamiento. La señal más obvia: AUC muy alto en validación pero bajo en producción. El error clásico es hacer fit_transform() sobre todo el dataset antes de splitear. La solución es usar sklearn Pipeline, que garantiza que el preprocessor solo ve los datos de train en cada fold.

¿Cuál es la diferencia entre Random Forest y XGBoost para una entrevista?+

Random Forest entrena árboles en paralelo sobre subsets aleatorios (bagging), lo que reduce varianza. XGBoost entrena árboles en secuencia, donde cada árbol corrige los errores del anterior (boosting), lo que reduce bias. Random Forest es más robusto a overfitting; XGBoost generalmente tiene mejor performance pero requiere más tuning de hiperparámetros.

¿Cómo evalúo un modelo de clasificación cuando los datos están desbalanceados?+

No uses accuracy: un modelo que siempre predice la clase mayoritaria puede tener 99% de accuracy con F1=0. Usá F1-score (balance precision-recall), AUC-ROC (qué tan bien separa las clases independientemente del umbral) o AUC-PR (más informativo cuando los positivos son muy raros, como en fraude o enfermedades). Siempre mostrá la matriz de confusión.

¿Qué preguntas de MLOps hacen en entrevistas de Machine Learning Engineer?+

Las más comunes: cómo detectás data drift en producción (test KS para numéricas, chi-squared para categóricas), cómo estructurís un pipeline de reentrenamiento automático, cómo versionás modelos (MLflow, DVC), cómo implementás A/B testing entre modelos, y cómo monitoreás que el modelo no se degrada. Mencioná feature stores si el rol es senior.

¿Cómo implemento regresión logística desde cero en Python?+

Necesitás tres cosas: la función sigmoid para mapear predicciones a probabilidades, el cálculo del gradiente de la binary cross-entropy (el error multiplicado por X transpuesta), y el loop de actualización de pesos. El truco es usar np.clip en el sigmoid para evitar overflow numérico cuando z es muy negativo. La implementación completa está en la pregunta 6 de esta guía.

Related articles

Cómo usar el método STAR en entrevistas (con ejemplos reales)

Aprende a responder preguntas difíciles usando el método STAR en entrevistas. Consejos y ejemplos concretos para roles remotos tech de LATAM.

Cómo conseguir trabajo remoto en dólares desde LATAM: guía real

Descubre consejos concretos para conseguir trabajo remoto en dólares desde LATAM: estrategias de búsqueda, preparación y entrevista para roles tecnológicos.

Las mejores preguntas para hacerle al entrevistador al final

Descubre las mejores preguntas para hacerle al entrevistador al final, útiles para entrevistas tech remotas, diferenciándote y logrando roles en dólares.

Cómo preparar entrevistas de desarrollo sin experiencia previa

Consejos prácticos para enfrentar entrevistas de tu primer trabajo como desarrollador, incluso sin experiencia. Técnicas para destacar y convencer en cada etapa.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY