Categorías
Informática Universitario Videojuego

Ejercicios

Estos son algunos ejercicios que pueden realizarse para practicar lo aprendido en el tema de Minería de Datos, sobre el uso de las bibliotecas NumPy, Pandas, Matplotlib y Seaborn desde Python.

⚠️Salvo que se indique lo contrario, estos ejercicios pueden resolverse en condiciones de prueba final, esto es, examen individual en el laboratorio, sin acceso a Internet y sin usar IA generativa. Tan sólo hace falta la plantilla o el contexto que proporcione el profesor junto al enunciado, y un servidor de lenguaje como Pylance que viene por defecto en la extensión oficial de Python para Visual Studio Code y que permite explorar la documentación del API.

  1. Crea una matriz NumPy de 100 * 3 con coordenadas (x, y, z) generadas aleatoriamente para 100 enemigos en el rango [-50, 50] en X y Z, y Y=0. Aplica una transformación vectorizada sin bucles para calcular la distancia euclídea de cada enemigo al origen (0,0,0) y filtra los índices de los enemigos que están a un radio menor de 15 unidades.
import numpy as np

# Generación de matriz 100x3 en rango [-50, 50] para X y Z, Y=0
# np.random.seed(42)
# enemigos = np.zeros((100, 3))
# enemigos[:, 0] = np.random.uniform(-50, 50, 100)
# enemigos[:, 2] = np.random.uniform(-50, 50, 100)

# Distancia euclídea al origen (sqrt(x^2 + y^2 + z^2)) vectorizada sin bucles
# ...

# Filtrado de índices con máscara booleana
# Usa np.where
  1. Dado el siguiente código de Python, explica qué efecto ha tenido la operación de difusión (broadcasting) sobre la matriz original. Si introducimos un algoritmo de Aprendizaje Automático basado en distancias, ¿por qué es crítico aplicar este escalado previo entre 0 y 1 en vez de usar las estadísticas directamente?
import numpy as np

# Filas: Personajes | Cols: [Vida, Ataque, Defensa, Velocidad]
stats = np.array([
    [450, 12, 25, 3],
    [120, 48, 8,  9],
    [300, 30, 15, 6],
    [500, 10, 30, 2],
    [200, 40, 10, 8]
])

v_min = stats.min(axis=0)
v_max = stats.max(axis=0)
stats_norm = (stats - v_min) / (v_max - v_min)
print(stats_norm)
  1. Genera un vector 2D normalizado que represente la dirección hacia la que mira un jugador (ej. [0, 1]) y una matriz de 50 * 2 con las direcciones hacia 50 PNJs en el mapa. Utiliza el producto escalar vectorizado (np.dot ó @) para calcular el coseno del ángulo entre el jugador y cada PNJ, determinando cuáles están dentro de un cono de visión de 90º (su coseno es mayor o igual que el coseno de 45º).
import numpy as np

# Vector de mirada del jugador (normalizado) y matriz 50x2 de PNJs
# mirada_jugador = np.array([0.0, 1.0])
# pnjs_dir = np.random.uniform(-1, 1, (50, 2))
# Normaliza con linalg.norm

# Producto escalar vectorizado (coseno del ángulo)
# cos_angulos = pnjs_dir @ mirada_jugador

# Umbral para 45º (cos(45º) ≈ 0.7071)
# en_cono = cos_angulos >= np.cos(np.radians(45))
  1. Crea una rejilla (grid) de 10 * 10 que represente el mapa de calor del daño de una explosión. Asigna valores aleatorios de daño. Define una zona central de 4 * 4 usando corte (slicing) e incrementa su daño en un +50%. Finalmente, reemplaza con 0 cualquier celda de la rejilla donde el daño final sea inferior a un umbral de 10 puntos mediante una máscara booleana.
import numpy as np

# Rejilla 10x10 de daño
# grid = np.random.uniform(0, 25, (10, 10))

# Incremento del 50% en zona central 4x4 (filas 3:7, cols 3:7)
# ...

# Aplicar umbral: poner a 0 celdas con daño < 10
# ...
  1. Crea un DataFrame de Pandas ficticio con 20 ítems de un RPG (columnas: nombre, tipo [arma/armadura/pocion], rareza [comun/raro/legendario], precio_oro, nivel_minimo). Filtra y muestra solo las armas de rareza ‘legendario’ que requieran un nivel mínimo menor o igual a 15 y tengan un coste superior a 500 monedas.
import pandas as pd

# Creación de DataFrame de ejemplo
# df_items = pd.DataFrame(...)

# Filtrado con operadores lógicos en Pandas (& para AND, | para OR, usar paréntesis)
# resultado = df_items[(df_items['tipo'] == 'arma') # & ... 
  1. Dado el siguiente código, ¿por qué se decide eliminar los duplicados antes de calcular la mediana del ping_ms y no después? Si la ausencia de datos en ping_ms se debe a que ciertos jugadores perdieron la conexión por completo (un fallo técnico sistémico), ¿de qué tipo de valor ausente según la teoría estaríamos hablando y qué sesgo introduce imputar con la mediana?
import pandas as pd
import numpy as np

logs = pd.DataFrame({
    'player_id': [1, 2, 2, 3, 4, 5, 5],
    'ping_ms': [45.0, 120.0, 120.0, np.nan, 35.0, np.nan, np.nan],
    'loss_percentage': [0.0, 2.5, 2.5, 1.0, 0.0, 5.0, 5.0]
})

...
print(logs_clean)
  1. Crea un DataFrame de una sesión de shooter táctico con las columnas bajas, muertes, asistencias y dano_total para 15 jugadores. Crea dos columnas calculadas: la tasa de bajas, muertes y asistencias (Kills, Deaths, Assists (KDA) Ratio) que se calcula así: (bajas + asistencias) / max(1, muertes) y el daño medio por baja: (dano_total / max(1, bajas)).
import pandas as pd
import numpy as np

# DataFrame de shooter táctico
# df_shooter = pd.DataFrame(...)

# Tasa KDA evitando división por 0: usaremos np.maximum(1, df['muertes'])
# df_shooter['KDA'] = ...
# df_shooter['dano_medio_baja'] = ...
  1. Genera un DataFrame ficticio de telemetría de juego con 1000 filas (datos de FPS, uso de GPU y memoria RAM). Guárdalo en un archivo CSV local, vuelve a leerlo seleccionando únicamente las columnas de rendimiento técnico e imprime un resumen estadístico global utilizando describe.
import pandas as pd

# Guardado en CSV sin incluir el índice automático
# df_telemetria.to_csv("telemetria_fps.csv", index=False)

# Carga seleccionando solo columnas específicas (usecols) y resumen estadístico
# Usa read_csv
# print(df_rendimiento.describe())
  1. Define una función matemática para la experiencia requerida por nivel (XP = 100 * nivel^1.5) para niveles del 1 al 50. Utiliza plt.plot para representar la curva de progresión, añade líneas discontinuas para marcar los niveles «hito» (10, 25 y 50) y personaliza títulos, etiquetas de ejes y leyenda.
import matplotlib.pyplot as plt
import numpy as np

# Nivel de 1 a 50 y cálculo de XP
# niveles = np.arange(1, 51)
# xp_requerida = 100 * (niveles ** 1.5)

# Trazado de línea y líneas verticales para hitos (axvline o plt.axvline)
# plt.plot(niveles, xp_requerida, label="XP Requerida")
# Bucle para los niveles hito
# plt.xlabel("Nivel"); plt.ylabel("XP"); plt.title("Curva de Progresión"); plt.legend()
  1. Dado el siguiente código, en el boxplot, ¿cómo se representan visualmente los valores atípicos y qué criterio matemático (basado en el Rango Intercuartílico) se utiliza para dibujarlos fuera de los «bigotes»? Si quisiéramos recortar estos valores atípicos para no distorsionar un modelo futuro, ¿qué diferencia hay entre eliminarlos y aplicar alguna técnica que los «trunque»?
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
duraciones = np.random.normal(loc=20, scale=5, size=500)
# Introduce un par de valores atípicos
# ... 

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].hist(duraciones, bins=20, color='skyblue', edgecolor='black')
axes[0].set_title("Histograma de Duración")

# Haz también el boxplot 
plt.show()
  1. Crea un gráfico de barras horizontal que compare la tasa de victorias (Win Rate) media de 5 arquetipos de personaje (Tanque, Asesino, Mago, Soporte, Tirador). Pinta en verde las barras que superen el 50% y en rojo o gris las que estén por debajo, añadiendo el valor exacto como texto sobre cada barra.
import matplotlib.pyplot as plt

# Datos de arquetipos y Win Rate (%)
# arquetipos = ['Tanque', 'Asesino', 'Mago', 'Soporte', 'Tirador']
# win_rates = [52.5, 48.0, 54.1, 49.2, 51.0]

# Asignar colores según condición: verde si > 50, rojo si <= 50
# colores = ['green' if wr > 50 else 'red' for wr in win_rates]

# Dibujar barras horizontales y añadir etiquetas de texto
# Dibuja con plt.barh 
# for bar in bars:
#     plt.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height()/2, f"{bar.get_width()}%")
  1. Dado un DataFrame con datos de 200 partidas (columnas: modo_juego [Solo, Duo, Squad], duracion_seg, puntuacion, victoria), utiliza groupby(‘modo_juego’) junto con agg para calcular -en una sola llamada- el tiempo medio de partida, la puntuación máxima y el porcentaje global de victorias por cada modo.
import pandas as pd

# DataFrame de partidas
# df_partidas = pd.DataFrame(...)

# Agregación múltiple con groupby() y agg()
  1. Dado el siguiente código, explica por qué es necesario especificar el parámetro axis=1 al usar apply() en este contexto. Modifica la función para añadir una nueva categoría llamada 'Minnow' si el gasto está entre 1 y 20 euros, reservando 'Casual Payer' para gastos entre 21 y 500 euros.
import pandas as pd

df_usuarios = pd.DataFrame({
'horas_jugadas': [120, 15, 300, 5, 80],
'gasto_euros': [0.0, 50.0, 1200.0, 0.0, 15.0]
})

def categorizar_jugador(row):
if row['gasto_euros'] > 500:
return 'Whale'
elif row['gasto_euros'] > 0:
return 'Casual Payer'
elif row['horas_jugadas'] > 100:
return 'Hardcore F2P'
else:
return 'Free-to-Play'

# Usa apply
print(df_usuarios)
  1. Dado un DataFrame con marcas de tiempo (timestamp) de inicio de sesión de jugadores durante un mes, convierte la columna a datetime. Extrae el día de la semana y la hora del día usando el accesor .dt, y agrupa los datos para determinar cuáles son los días y franjas horarias con mayor afluencia de usuarios en el juego.
import pandas as pd

# Convertir marca de tiempo a datetime
# df_logins['fecha'] = pd.to_datetime(df_logins['timestamp'])

# Extracción de componentes temporales mediante el accesor .dt
# df_logins['dia_semana'] = df_logins['fecha'].dt.day_name()
# df_logins['hora'] = df_logins['fecha'].dt.hour

# Agregación (groupby) para encontrar picos de afluencia
  1. Toma la tabla dinámica de enfrentamientos del ejercicio 14 (o genera una matriz de 5 * 5 de tasa de victoria) y represéntala mediante un mapa de calor (sns.heatmap). Configura la paleta de colores divergente (coolwarm), fuerza el centro del mapa de calor en 0.5 (balance perfecto) y muestra los porcentajes exactos dentro de cada celda (annot=True).
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

# Matriz 5x5 de winrate relativo entre 5 personajes
# winrate_matrix = pd.DataFrame(...)

# Mapa de calor con paleta divergente centrada en 0.5 (equilibrio perfecto), usando sns.heatmap
plt.show()
  1. Dado el siguiente código, al examinar los gráficos de la diagonal principal del pairplot, ¿qué tipo de representación de datos se muestra y para qué sirve separar los tonos por la variable rango? Si observas que dos variables numéricas muestran una distribución de puntos alineada en diagonal ascendente, ¿qué tipo de correlación indica y qué método estadístico (Pearson vs. Spearman) usarías si hay presencia de valores atípicos?
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

# Generar métricas de 100 jugadores
np.random.seed(42)
df_players = pd.DataFrame({
    'kda': np.random.uniform(1.0, 5.0, 100),
    'oro_minuto': np.random.uniform(200, 600, 100),
    'vision_score': np.random.uniform(10, 80, 100),
    'rango': np.random.choice(['Alto', 'Bajo'], 100)
})

# Visualización de matriz de dispersión coloreada por rango (sns.pairplot)
plt.show()
  1. Simula datos de tiempo de juego diario para usuarios en tres plataformas (PC, Console, Mobile). Utiliza un gráfico de violín (sns.violinplot) o un gráfico de caja (sns.boxplot) con o sin bigotes con ajuste de tono (hue) por tipo de usuario (Nuevo vs Veterano) para comparar gráficamente cómo se distribuye el compromiso del jugador según la plataforma.
import seaborn as sns
import matplotlib.pyplot as plt

# DataFrame de tiempo de juego
# df_engagement = pd.DataFrame(...)

# Gráfico de violín comparando tiempo por plataforma y tipo de usuario (sns.violinplot)
plt.show()

Esta página está licenciada bajo CC BY-NC-SA 4.0 por Laboratorios Narratech.