Matemáticas con código: pendientes, promedios y azar

Tema 5 · Fase 1, Programar con datos · Toda la ruta

Los 22 bloques de código de la teoría, en el mismo orden. Cópialos y córrelos en tu Python o en Colab; en la ficha Teoría está la explicación de cada uno.

Funciones y pendientes

import numpy as np
import matplotlib.pyplot as plt

def f(x):
    """La función de ejemplo: f(x) = x²."""
    return x ** 2

def pendiente_secante(f, a, h):
    """Pendiente de la recta que une (a, f(a)) con (a + h, f(a + h))."""
    return (f(a + h) - f(a)) / h

for h in [1, 0.1, 0.01, 0.001]:
    print(f"h = {h:<6} pendiente de la secante = "
          f"{pendiente_secante(f, 1, h):.4f}")
a = 1
xs = np.linspace(-0.5, 3.2, 200)
plt.plot(xs, f(xs), color="C0", linewidth=3, label="f(x) = x²")
for h, estilo in [(2, ":"), (1, "--"), (0.5, "-.")]:
    m = pendiente_secante(f, a, h)
    plt.plot(xs, f(a) + m * (xs - a), color="C2", linestyle=estilo,
             label=f"Secante, h = {h}: pendiente {m:g}")
    plt.scatter([a + h], [f(a + h)], color="C2", zorder=3)
# La tangente: la recta por (1, 1) con pendiente 2
plt.plot(xs, f(a) + 2 * (xs - a), color="C1", linewidth=2.5,
         label="Tangente: pendiente 2")
plt.scatter([a], [f(a)], color="C0", s=60, zorder=4)
plt.ylim(-1, 15)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.title("Secantes que se acercan a la tangente")
plt.legend(fontsize=9, loc="upper left")
plt.show()
def derivada(f, x, h=1e-6):
    """Derivada numérica de f en x, con la diferencia centrada."""
    return (f(x + h) - f(x - h)) / (2 * h)

def cubo(x):
    return x ** 3

# La derivada exacta de x³ es 3x²: en x = 2 vale 12
for h in [0.1, 0.01, 0.001]:
    adelante = pendiente_secante(cubo, 2, h)
    centrada = derivada(cubo, 2, h)
    print(f"h = {h:<5}  hacia adelante: {adelante:.6f}   "
          f"centrada: {centrada:.6f}")
print(f"Centrada con h = 0.000001: {derivada(cubo, 2):.6f}")
def curva(x):
    """La curva de la actividad: x³/3 − x."""
    return x ** 3 / 3 - x

for x in [-2, -1, 0, 1, 2]:
    m = derivada(curva, x)
    if abs(m) < 1e-6:
        forma = "está plana"
    elif m > 0:
        forma = "sube"
    else:
        forma = "baja"
    print(f"x = {x:>2}: pendiente {round(m, 4):>5}  -> la curva {forma}")

La regla de la cadena: engranajes

def adentro(x):
    return 3 * x + 1

def afuera(u):
    return u ** 2

def compuesta(x):
    return afuera(adentro(x))

x0 = 1
u0 = adentro(x0)                      # u0 = 4
print("Engranaje de afuera, du² en u = 4:", round(derivada(afuera, u0), 4))
print("Engranaje de adentro, d(3x+1):    ", round(derivada(adentro, x0), 4))
print("Producto (regla de la cadena):    ",
      round(derivada(afuera, u0) * derivada(adentro, x0), 4))
print("Derivada numérica directa:        ", round(derivada(compuesta, x0), 4))

Dos variables: derivadas parciales y gradiente

def g(x, y):
    """Un cuenco alargado: g(x, y) = x² + 3y²."""
    return x ** 2 + 3 * y ** 2

def gradiente_numerico(f, x, y, h=1e-6):
    """Las dos derivadas parciales de f en (x, y), juntas en un vector."""
    df_dx = (f(x + h, y) - f(x - h, y)) / (2 * h)
    df_dy = (f(x, y + h) - f(x, y - h)) / (2 * h)
    return np.array([df_dx, df_dy])

print("Gradiente numérico en (2, 1):",
      np.round(gradiente_numerico(g, 2, 1), 4))
print("Gradiente exacto (2x, 6y):   ", np.array([2 * 2, 6 * 1]))
punto = np.array([2.0, 1.0])
grad = gradiente_numerico(g, *punto)
paso = 1e-4                           # un paso muy corto
angulos = np.arange(360)              # una dirección por grado
rad = np.deg2rad(angulos)
direcciones = np.column_stack([np.cos(rad), np.sin(rad)])
subidas = [g(*(punto + paso * d)) - g(*punto) for d in direcciones]

print("Dirección en que g sube más:", angulos[np.argmax(subidas)],
      "grados")
angulo_grad = np.degrees(np.arctan2(grad[1], grad[0]))
print(f"Dirección del gradiente:     {angulo_grad:.1f} grados")
print(f"Mayor subida dividida por el paso: {max(subidas) / paso:.3f}")
print(f"Largo del gradiente:               {np.linalg.norm(grad):.3f}")
xs = np.linspace(-3.5, 3.5, 200)
ys = np.linspace(-2.2, 2.2, 200)
XX, YY = np.meshgrid(xs, ys)
plt.contour(XX, YY, g(XX, YY), levels=[1, 3, 7, 12, 19], colors="C2")

# Flechas del gradiente en una cuadrícula de puntos
xq, yq = np.meshgrid(np.arange(-2.5, 3, 1), [-1.2, 0, 1.2])
gx, gy = gradiente_numerico(g, xq, yq)
plt.quiver(xq, yq, gx, gy, color="C0", angles="xy",
           scale_units="xy", scale=10, width=0.007)
# El gradiente en (2, 1), sobre la curva de nivel g = 7
plt.quiver([2], [1], [grad[0]], [grad[1]], color="C1", angles="xy",
           scale_units="xy", scale=10, width=0.009)
plt.scatter([0], [0], marker="+", s=120, color="C0")
plt.gca().set_aspect("equal")
plt.xlabel("x")
plt.ylabel("y")
plt.title("Curvas de nivel de g y su gradiente")
plt.show()

Promedios: media, varianza y desviación estándar

# Minutos de viaje al colegio durante cinco días
sofia = np.array([20, 22, 19, 21, 18])
mateo = np.array([10, 35, 15, 30, 10])
print("Sofía:", sofia.mean(), "minutos en promedio")
print("Mateo:", mateo.mean(), "minutos en promedio")
for nombre, t in [("Sofía", sofia), ("Mateo", mateo)]:
    media = t.mean()
    varianza = np.mean((t - media) ** 2)
    desviacion = np.sqrt(varianza)
    print(f"{nombre}: media {media:.1f}   varianza {varianza:.1f}   "
          f"desviación {desviacion:.2f}")

print("Con NumPy:", np.var(mateo), round(np.std(mateo), 2))
z_sofia = (23 - sofia.mean()) / sofia.std()
z_mateo = (40 - mateo.mean()) / mateo.std()
print(f"23 minutos para Sofía: z = {z_sofia:.2f}")
print(f"40 minutos para Mateo: z = {z_mateo:.2f}")
z = (mateo - mateo.mean()) / mateo.std()
print("Mateo estandarizado:", np.round(z, 2))
print(f"Media: {abs(z.mean()):.2f}   desviación: {z.std():.2f}")
# Cuatro estudiantes: estatura (cm) y peso (kg)
tabla = np.array([[150, 45], [160, 52], [170, 61], [180, 70]],
                 dtype=float)
tabla_est = (tabla - tabla.mean(axis=0)) / tabla.std(axis=0)

print("Medias por columna:", tabla.mean(axis=0))
print("Desviaciones:", np.round(tabla.std(axis=0), 2))
print(np.round(tabla_est, 2))

Azar: probabilidad como frecuencia

rng = np.random.default_rng(7)
tiros = rng.integers(1, 7, size=100_000)
print("Primeros diez tiros:", tiros[:10])

for n in [10, 100, 1_000, 10_000, 100_000]:
    fraccion = np.mean(tiros[:n] == 6)
    print(f"{n:>7} tiros: fracción de seises = {fraccion:.4f}")
print(f"Probabilidad: 1/6 = {1 / 6:.4f}")
n_tiros = np.arange(1, len(tiros) + 1)
# Fracción de seises acumulada después de cada tiro
frecuencia = np.cumsum(tiros == 6) / n_tiros
plt.plot(n_tiros, frecuencia, color="C0", label="Fracción de seises")
plt.axhline(1 / 6, color="C2", linestyle="--", label="1/6")
plt.xscale("log")
plt.ylim(0, 0.5)
plt.xlabel("Cantidad de tiros (escala logarítmica)")
plt.ylabel("Fracción de seises")
plt.title("La frecuencia se acerca a 1/6")
plt.legend()
plt.show()

Probabilidad condicional y la regla de Bayes

N = 1_000_000                         # personas simuladas
p_enfermedad = 0.01                   # 1 de cada 100
sensibilidad = 0.90                   # P(positivo | enferma)
falso_positivo = 0.05                 # P(positivo | sana)

rng = np.random.default_rng(1)
enferma = rng.random(N) < p_enfermedad
azar = rng.random(N)
# A cada persona le sale positivo con la probabilidad que le toca
positivo = np.where(enferma, azar < sensibilidad, azar < falso_positivo)

print("Personas con resultado positivo:", positivo.sum())
print(f"P(enferma | positivo) simulada: {enferma[positivo].mean():.4f}")
p_positivo = (sensibilidad * p_enfermedad
              + falso_positivo * (1 - p_enfermedad))
print(f"P(enferma | positivo) con Bayes: "
      f"{sensibilidad * p_enfermedad / p_positivo:.4f}")

Distribuciones: uniforme, Bernoulli y normal

rng = np.random.default_rng(5)
estaturas = rng.normal(165, 7, size=100_000)
print(f"Media: {estaturas.mean():.2f}   "
      f"desviación: {estaturas.std():.2f}")

z = (estaturas - estaturas.mean()) / estaturas.std()
for k in [1, 2, 3]:
    print(f"Fracción con |z| menor que {k}: {np.mean(np.abs(z) < k):.3f}")
def densidad_normal(x, mu, sigma):
    """La fórmula de la campana."""
    return (np.exp(-(x - mu) ** 2 / (2 * sigma ** 2))
            / (sigma * np.sqrt(2 * np.pi)))

rng = np.random.default_rng(3)
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8))
for eje, k in zip(ejes.flat, [1, 2, 5, 20]):
    # 100 000 tiradas de k dados, sumando cada tirada
    sumas = rng.integers(1, 7, size=(100_000, k)).sum(axis=1)
    valores, cuentas = np.unique(sumas, return_counts=True)
    eje.bar(valores, cuentas / len(sumas), width=0.8, color="C0")
    eje.set_title("1 dado" if k == 1 else f"Suma de {k} dados")
    eje.set_xticks([k, 6 * k])          # la menor y la mayor suma
    eje.set_yticks([])

# Un dado tiene media 3,5 y varianza 35/12; con k dados, k veces eso
xs = np.linspace(20, 120, 300)
eje.plot(xs, densidad_normal(xs, 3.5 * k, np.sqrt(35 / 12 * k)),
         color="C1", linewidth=2)
fig.suptitle("Sumar dados forma una campana")
plt.show()

Logaritmo y exponencial

print("e =", np.e)
print("exp(1) =", np.exp(1), "  log(e) =", np.log(np.e))
print("log(exp(5)) =", np.log(np.exp(5)))
print("exp(10) =", round(np.exp(10), 1), "  log(10) =",
      round(np.log(10), 4))
xs = np.linspace(-3, 3, 200)
plt.plot(xs, np.exp(xs), color="C0", label="exp(x)")
xs_pos = np.linspace(0.02, 9, 300)     # log solo acepta positivos
plt.plot(xs_pos, np.log(xs_pos), color="C0", linestyle="--",
         label="log(x)")
plt.plot([-3, 9], [-3, 9], color="C2", linestyle=":",
         label="y = x")
plt.xlim(-3, 9)
plt.ylim(-3, 9)
plt.gca().set_aspect("equal")
plt.xlabel("x")
plt.title("exp y log son inversas")
plt.legend(loc="center left", bbox_to_anchor=(1, 0.5))
plt.show()
print("log(6 · 8)      =", round(np.log(6 * 8), 6))
print("log(6) + log(8) =", round(np.log(6) + np.log(8), 6))

# La probabilidad de 1000 sucesos independientes, cada uno de 1 %
probabilidades = np.full(1000, 0.01)
print("Producto directo:", np.prod(probabilidades))
print("Suma de logaritmos:", round(np.sum(np.log(probabilidades)), 2))
for x in [0, 1, 2]:
    print(f"x = {x}: exp(x) = {np.exp(x):.4f}   "
          f"pendiente = {derivada(np.exp, x):.4f}")