# ASSIGMENT 1

```python
import sympy as sp
import numpy as np
import pandas as pd

from astropy import units as u
from astropy.coordinates import solar_system_ephemeris
from astropy.time import Time
from astropy import constants as const

solar_system_ephemeris.set("jpl")
import matplotlib.pyplot as plt

from sympy.utilities.lambdify import lambdify
from scipy.integrate import odeint

from matplotlib.collections import LineCollection
from matplotlib.colors import ListedColormap, BoundaryNorm
```

```python
from poliastro.bodies import Earth, Jupiter, Sun
from poliastro.twobody import Orbit
from poliastro.plotting import OrbitPlotter

plt.style.use("seaborn")

earth = Orbit.from_body_ephem(Earth)
jupiter = Orbit.from_body_ephem(Jupiter)
sun = Orbit.from_body_ephem(Sun)

# frame = OrbitPlotter()
# frame.plot(earth, label="Earth")
# frame.plot(jupiter, label="Jupiter")

EPOCH = Time.now()
EPOCH = Time(EPOCH, scale='tdb')

earth =   Orbit.from_body_ephem(Earth, EPOCH)
jupiter = Orbit.from_body_ephem(Jupiter, EPOCH)
sun =     Orbit.from_body_ephem(Sun, EPOCH)
```

## 2D n-body problem

Set up symbols to be used for function to set up n-body problem

### Symbol space

```python
r_i_x, r_i_y, r_j_x, r_j_y = sp.symbols('r_i_x, r_i_y, r_j_x, r_j_y', real=True)  # Positions
V_i_x, V_i_y, V_j_x, V_j_y = sp.symbols('V_i_x, V_i_y, V_j_x, V_j_y', real=True)  # Velocities

G = sp.symbols('G', real=True)

M, m_i, m_j = sp.symbols('M, m_i, m_j', real=True)

r_i_vec = sp.Matrix([r_i_x, r_i_y])
r_j_vec = sp.Matrix([r_j_x, r_j_y])
V_i_vec = sp.Matrix([V_i_x, V_i_y])
V_j_vec = sp.Matrix([V_j_x, V_j_y])
r_ij_vec = r_j_vec - r_i_vec

r_i_norm, r_j_norm, r_ij_norm = sp.symbols(['|r_i|', '|r_j|', '|r_ij|'])
r_i_sym, r_j_sym, r_ij_sym = sp.MatrixSymbol('r_i', 2, 1), sp.MatrixSymbol('r_j', 2, 1), sp.MatrixSymbol('r_ij', 2, 1)
```

### Equations of Motion: Barycentric form

```python
"""
The following symbolic equations are those defining the n-body
problem with respect to the barcenter of the system. The following
are the respective outputs of the expressions using sympy.pprint().
It should be noted that the folowing samples are only between two
bodies.
"""

eom_bc1_vec = - G * M / (r_i_norm ** 3) * r_i_sym
eom_bc2_vec =   G * m_j * (1/ (r_ij_norm **3) - 1/(r_i_norm**3)) * r_ij_sym

"""
----------------------
Vector representation.
----------------------

>>> from sympy import pprint
>>> pprint(eom_bc1_vec + eom_bc2_vec)

-G⋅M             ⎛    1         1   ⎞
──────⋅rᵢ +G⋅m_j⋅⎜- ────── + ───────⎟⋅r_ij
     3           ⎜       3         3⎟
|r_i|            ⎝  |r_i|    |r_ij| ⎠

"""

eom_bc1 = - G * M / (r_i_vec.norm() ** 3) * r_i_vec
eom_bc2 =   G * m_j * (1/ (r_ij_vec.norm() ** 3) - 1/(r_i_vec.norm() **3) ) * r_ij_vec

"""
------------------------
Component representation.
------------------------

>>> from sympy import pprint, latex
>>> print(latex(eom_bc1 + eom_bc2))

The image below shows the latex rendering of the above code output.
"""
pass
```

Using the previous general definition for the barycentric EOM between i and j, we can now create a function to create the system of equations given any list of bodies. This is what `_barycentric_eom(bodies, vector=False)` is purposed for.

```python
def _barycentric_eom(bodies, vector=False):
    """
    Returns the equations of motion for all bodies within the n-body barycentric reference frame.

    -G⋅M             ⎛    1         1   ⎞
    ──────⋅rᵢ +G⋅m_j⋅⎜- ────── + ───────⎟⋅r_ij
         3           ⎜       3         3⎟
    |r_i|            ⎝  |r_i|    |r_ij| ⎠

    """
    _system = []
    if vector is False:
        for body_i in bodies:
            _body_system = []
            # Subscript symbol of body_i
            sub_i = body_i.name[0]

            # Parameter symbols of body_i
            var_i = {
                m_i: sp.symbols("m_{}".format(sub_i)),
                r_i_x: sp.symbols("r_{}_x".format(sub_i)),
                r_i_y: sp.symbols("r_{}_y".format(sub_i)),
            }

            # Add two-body influence from EOM
            _body_system.append(eom_bc1.subs(var_i))

            for body_j in bodies:

                # Ensure that body_j is not body_i, else skip.
                if body_j != body_i:

                    # Subscript symbol of body_j
                    sub_j = body_j.name[0]

                    # Parameter symbols of body_j
                    var_j = {
                        m_j: sp.symbols("m_{}".format(sub_j)),
                        r_j_x: sp.symbols("r_{}_x".format(sub_j)),
                        r_j_y: sp.symbols("r_{}_y".format(sub_j)),
                    }

                    # Add body_j perturbations from EOM
                    _body_system.append(eom_bc2.subs({**var_j, **var_i}))

                # Skip if body_j == body_i
                else:
                    pass

            lhs = sp.Matrix([*sp.symbols(['a_{}_x'.format(sub_i), 'a_{}_y'.format(sub_i)])])
            rhs = sum(_body_system, sp.zeros(2,1))
            _system.append(sp.Eq(
                lhs[0], rhs[0]
            ))
            _system.append(sp.Eq(
                lhs[1], rhs[1]
            ))
    return _system

"""
------------------------
Component representation.
------------------------

    >>> bodies = [Earth, Sun]
    >>> print(latex(_barycentric_eom(bodies)))

The image below shows the latex rendering of the above code output.
# TODO: Output format changed from below to sets of equations.
"""
pass

```

Sample output is seen below in vector format. The `sympy` allows for ease of LaTeX with its integrated formatter, from which the following render was created.

```python
def _eq_linear_momentum(bodies):   # barycentre
    """
    returns Eq in vector format
    """
    _eq = []
    req = []
    for _body in bodies:
        sub_i = _body.name[0]
        _eq.append( (sp.symbols("m_{}".format(sub_i)) *
                    sp.Matrix(sp.symbols("V_{}_x V_{}_y".format(sub_i, sub_i)))

                   ))
    shape = _eq[0].shape
    start = sp.zeros(*shape)
    m = sum(_eq, sp.zeros(*shape))
    return [sp.Eq(0, m[0]), sp.Eq(0, m[1])]

pprint(_eq_linear_momentum(bodies))
```

    [0 = V_E_x⋅m_E + V_J_x⋅m_J + V_S_x⋅m_S, 0 = V_E_y⋅m_E + V_J_y⋅m_J + V_S_y⋅m_S]

```python
def _eq_angular_momentum(bodies):  # 2D
    _eq = []
    for _body in bodies:
        _n = _body.name
        _eq.append(sp.symbols("m_{}".format(_n)) *
                  (
                      sp.symbols("r_{}_x".format(_n)) * sp.symbols("V_{}_y".format(_n)) - sp.symbols("r_{}_y".format(_n)) * sp.symbols("V_{}_x".format(_n))
                  )
                  )
    return [sp.Eq(sp.symbols("H_z"), sum(_eq, 0))]
```

```python
from mpmath import power

def _eq_energy_conservation(bodies, vector=False):
    """
    Returns the equation for the n-body system defining the total energy of the system.
    """
    _eq = []
    E_k = 0.5 * m_i * V_i_vec.norm() ** 2
    E_p = - 0.5 * G * (m_i * m_j) / r_ij_vec.norm()
    for i in bodies:
        sub_i=i.name[0]
        var_i={
            m_i:i.mass.si.value,
            r_i_x:sp.symbols('r_{}_x'.format(sub_i)),
            r_i_y:sp.symbols('r_{}_y'.format(sub_i)),
            V_i_x:sp.symbols('V_{}_x'.format(sub_i)),
            V_i_y:sp.symbols('V_{}_y'.format(sub_i))
        }
        _eq.append(E_k.subs(var_i))
        for j in bodies:
            if i != j:
                sub_j=j.name[0]
                var_j={
                    m_j:j.mass.si.value,
                    r_j_x:sp.symbols('r_{}_x'.format(sub_j)),
                    r_j_y:sp.symbols('r_{}_y'.format(sub_j)),
                    V_j_x:sp.symbols('V_{}_x'.format(sub_j)),
                    V_j_y:sp.symbols('V_{}_y'.format(sub_j))
                }
                _eq.append(E_p.subs({**var_i, **var_j}))
            else:
                pass
    return sp.Eq(sp.symbols("C"), sum(_eq, 0))

```

```python
def _state_matrix(bodies):
    """
    Creates a symbolic vector of the state of the system given the bodies.
    """
    states = []
    for _body in bodies:
        sub_i = _body.name[0]
        for s in 'r_{}_x r_{}_y'.split(' '):

            states.append(sp.symbols(s.format(sub_i)))
    for _body in bodies:
        sub_i = _body.name[0]
        for s in 'V_{}_x V_{}_y'.split(' '):

            states.append(sp.symbols(s.format(sub_i)))
    return sp.Matrix(states)

def _derivative_matrix(bodies):
    """
    Create a symbolic vector for the state derivative of the system given the bodies.
    """
    states = []
    eom = _barycentric_eom(bodies)
    for _body in bodies:
        sub_i = _body.name[0]
        for s in 'V_{}_x V_{}_y'.split(' '):
            states.append(sp.symbols(s.format(sub_i)))

    for _body in bodies:
        sub_i = _body.name[0]
        for s in 'a_{}_x a_{}_y'.split(' '):
            states.append(sp.symbols(s.format(sub_i)))

    return sp.Matrix(states).subs([(eom[i].lhs, eom[i].rhs) for i in range(len(eom))])
```

```python
def var(bodies):
    """
    Function built to return all constant parameters for a function
    prior to a function being lambdified.
    """
    _var = {
        G: const.G.si.value,
        M: sum([b.mass.si.value for b in bodies])
    }
    for body in bodies:
        _sub_i = body.name[0]
        _var_b = {
            sp.symbols("m_{}".format(_sub_i)): body.mass.si.value,
        }
        _var = {**_var, **_var_b}
    return _var
```

```python
def S0(bodies):
    """
    Returns the initial state vector given the list of involved bodies.
    It must be noted that the calculations below for Jupiter are only
    valid when Jupiter is part of the input argument. Otherwise it is
    ignored in the calculation of the barcentre and velocity of bodies.

    Some important information:
    ===============================
    1) Imposed uncertainty      |||
    ===============================
    Name   = Gravitational constant
    Value  = 6.67408e-11
    Uncertainty  = 3.1e-15
    Unit  = m3 / (kg s2)
    Reference = CODATA 2014

    ===============================
    2) Parameters used          |||
    ===============================
    Jupiter -----------------------
    SMA: 5.2044 AU
    Earth -------------------------
    SMA: 1.0 AU
    """
    # Step 1: Assume two-body problem positioning in arbitrary frame on x-axis.
    _a_Earth = u.AU.to(u.m)
    _a_Jupiter = u.AU.to(u.m)* 5.2044

    ## Initialised positions for bodies
    _pos_x = {
        Earth: _a_Earth,
        Jupiter: _a_Jupiter,
        Sun: 0.0}

    # Step 2: Calculate circular velocity using the SMA
    _V_circ_Earth = np.sqrt(Sun.k.si.value/_a_Earth)
    _V_circ_Jupiter = np.sqrt(Sun.k.si.value/_a_Jupiter)

    # Step 3: Calculate the position of the Barycentre in perifocal frame.
    _num = sum([b.mass.si.value * _pos_x[b] for b in bodies])
    _M = sum([b.mass.si.value for b in bodies])
    _r_cm_x =  _num/_M

    # Step 4: Offset x_position of bodies by r_cm
    for b in bodies:
        _pos_x[b] += - _r_cm_x

    # Step 5: Calculate velocity of Sun for sum of linear momentum = 0
    _st = {
        sp.symbols('r_E_x'):_pos_x[Earth],
        sp.symbols('r_E_y'):0.0,
        sp.symbols('r_S_x'):_pos_x[Sun],
        sp.symbols('r_S_y'): 0.0,
        sp.symbols('r_J_x'):_pos_x[Jupiter],
        sp.symbols('r_J_y'):0.0,
        sp.symbols('V_E_x'):0.0,
        sp.symbols('V_E_y'):_V_circ_Earth,
        sp.symbols('V_J_x'):0.0,
        sp.symbols('V_J_y'):_V_circ_Jupiter,
        sp.symbols('m_E'):Earth.mass.si.value,
        sp.symbols('m_J'):Jupiter.mass.si.value,
        sp.symbols('m_S'):Sun.mass.si.value
    }

    ## Solving the set of linear equations for the entire system's linear momentum.
    linear_momentum_eqs = [_eq.subs(_st) for _eq in _eq_linear_momentum(bodies)]

    sol = sp.solve(linear_momentum_eqs, dict=True)
    _st[sp.symbols("V_S_x")] = sol[0][sp.symbols('V_S_x')]
    _st[sp.symbols("V_S_y")] = sol[0][sp.symbols('V_S_y')]

    ## Generate state vector depending on given bodies.
    _state = [_st[_s] for _s in np.array(S).flatten()]

    # Step 6: Return the state vector!
    return np.array(_state).flatten().astype(float)

```

## Prepare for propagation (Earth + Sun)

```python
# Define bodies for n-body system.
bodies = [Earth, Sun]

# Instantiate state-vector from bodies list.
S = _state_matrix(bodies=bodies)

# Instantiate state-vector derivative from bodies list.
F = _derivative_matrix(bodies)

# Energy equation for evaluation.
E = _eq_energy_conservation(bodies).subs(var(bodies))

# Lambdify for increased computation of energy.
E = lambdify((S), (E.rhs))

# Substitute constants to increase speed through propagation.
F = F.subs(var(bodies))
```

## Prepare for propagation (Earth + Sun + Jupiter)

```python
# Define bodies for n-body system.
bodies = [Earth, Sun, Jupiter]

# Instantiate state-vector from bodies list.
S = _state_matrix(bodies=bodies)

# Instantiate state-vector derivative from bodies list.
F = _derivative_matrix(bodies)

# Energy equation for evaluation.
E = _eq_energy_conservation(bodies).subs(var(bodies))

# Lambdify for increased computation of energy.
E = lambdify((S), (E.rhs))

# Substitute constants to increase speed through propagation.
F = F.subs(var(bodies))

bodies = [Earth, Sun, Jupiter]
from sympy import pprint, latex
print(latex(sp.Matrix(S0(bodies))))
```

    \left[\begin{matrix}148854763525.635\\0.0\\-743107174.364845\\0.0\\777824051096.715\\0.0\\0.0\\29784.6920652165\\0.0\\-12.5551530705683\\0.0\\13055.9290198896\end{matrix}\right]

```python
# Lambdify the symbolic expression for increased computational speed.
"""
The Lambdified equation for dS is used in the following way, and returns accordingly.

    >>> dS(*S)

    ::returns::
    [V_Earth_x V_Earth_y V_Sun_x V_Sun_y a_Earth_x a_Earth_y a_Sun_x a_Sun_y]

    ::type:: np.ndarray

"""
dS = lambdify((S), (F))

# Define function for the propagation procedure.
def dS_dt(_S, t):
    """
    Integration of the governing vector differential equation for
    the barcyentric form of the two-body problem.

    [example]
    S = [r_Earth_x r_Earth_y r_Sun_x r_Sun_y V_Earth_x V_Earth_y V_Sun_x V_Sun_y]
    F = [V_Earth_x V_Earth_y V_Sun_x V_Sun_y a_Earth_x a_Earth_y a_Sun_x a_Sun_y]

    """
    return np.array(dS(*_S)).flatten().astype(float)
```

```python
# Define the time-steps for the propagation.
t = np.arange(0.0, 365*24*60*60, 100)

# Calculate the results from the propagation.
S_l = odeint(dS_dt, S0(bodies), t)

"""
The following plot is for the energy throughout the time domain.
    - Setting the time-step to 0.0000001 for np.arange(0, 0.00001)
        shows that the jump in energy is not a result of integration error,
        but implementation error.
"""
# Plot graph of Energy throughout time domain of entire system.
# figure(figsize=(6,4), dpi=300, facecolor='w', edgecolor='k')
# plt.plot(t, [E(*S_l[i,:]) for i in range(len(t))])
# plt.axes().set_xlabel('t [s]')
# plt.axes().set_ylabel('C [J]')
# ax = plt.gca()
# ax.ticklabel_format(useOffset=False)
# plt.show()

# figure(figsize=(7,4), dpi=300, facecolor='w', edgecolor='k')
# plt.axes().set_aspect('equal')
# plt.axes().set_xlabel('x [m]')
# plt.axes().set_ylabel('y [m]')
# for idx, body in enumerate(bodies):
#     plt.plot(S_l[:,idx*2], S_l[:,idx*2+1], label=body.name)
# plt.show()

"""
Velocity plots for Earth and Sun respectively.
"""
# plt.plot(S_l[:,4], S_l[:,5])
# plt.plot(S_l[:,6], S_l[:,7])
# plt.show()
pass

```

```python
x = S_l[:,2]
y = S_l[:,3]
dx = S_l[:,8]
dy = S_l[:,9]
dydx = t

# Create a set of line segments so that we can color them individually
# This creates the points as a N x 1 x 2 array so that we can stack points
# together easily to get the segments. The segments array for line collection
# needs to be (numlines) x (points per line) x 2 (for x and y)

points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)

points2 = np.array([dx, dy]).T.reshape(-1, 1, 2)
segments2 = np.concatenate([points2[:-1], points2[1:]], axis=1)

fig, axs = plt.subplots(1,2, figsize=(10,5), dpi=300, facecolor='w', edgecolor='k')

axs[0].set_aspect('equal', 'datalim')
axs[0].set_xlim(np.min(x)*1.05, np.max(x)*1.05)
axs[0].set_ylim(np.min(y)*1.05, np.max(y)*1.05)
# Create a continuous norm to map from data points to colors
norm = plt.Normalize(dydx.min(), dydx.max())
lc = LineCollection(segments, cmap='viridis', norm=norm)
lc.set_array(dydx)
lc.set_linewidth(1)
line = axs[0].add_collection(lc)
fig.colorbar(line, ax=axs[0], label='Time [s]')
axs[0].set_ylabel('y [m]')
axs[0].set_xlabel('x [m]')

axs[1].set_aspect('equal', 'datalim')
axs[1].set_xlim(np.min(dx)*1.05, np.max(dx)*1.05)
axs[1].set_ylim(np.min(dy)*1.05, np.max(dy)*1.05)
# Create a continuous norm to map from data points to colors
norm = plt.Normalize(dydx.min(), dydx.max())
lc = LineCollection(segments2, cmap='viridis', norm=norm)
lc.set_array(dydx)
lc.set_linewidth(1)
line = axs[1].add_collection(lc)
# fig.colorbar(line, ax=axs[0], label='Time [s]')
plt.subplots_adjust(left=0.01,wspace=0.30)

# left  = 0.125  # the left side of the subplots of the figure
# right = 0.9    # the right side of the subplots of the figure
# bottom = 0.1   # the bottom of the subplots of the figure
# top = 0.9      # the top of the subplots of the figure
# wspace = 0.2   # the amount of width reserved for space between subplots,
#                # expressed as a fraction of the average axis width
# hspace = 0.2   # the amount of height reserved for space between subplots,
#                # expressed as a fraction of the average axis height

axs[1].set_ylabel('y [m/s]')
axs[1].set_xlabel('x [m/s]')

plt.show()
```

```python

```

```python

```

```python

```


```python
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

import sys
sys.path.append('../../pyutils')
import metrics
import utils
```

# When $p$ is much bigger than $N$

High variance and overfitting are a major concern in this setting.
Simple, highly regularized models are often used.

Let's suppose we are trying to predict a linear model.
With $p<<N$, we can identify as many coefficients as we want without shrinkage.
With $p=N$, we can identify some non-zero coefficients with moderate shrinkage.
With $p>>N$, even though they are many non-zero coefficients, we don't have a hope to find them, we need to shrink a lot.

# Diagonal LDA and Nearest Shrunken Centroids

The simplest form of regularization assumes that the features are independant within each class (the within class covariance matris is diagonal).
It greatly reduces the number of parameters and often result in an effective and interpretable classifier.

The discriminant score for class $k$ is:
$$\theta_k(x) = - \sum_{j=1}^p \frac{(x_j - \bar{x}_{kj})^2}{s_j^2} + 2 \log \pi_k$$
with $s_j$ the within-class standard deriavtion for feature $j$, and:
$$\bar{x}_{kj} = \frac{1}{N_k} \sum_{i \in C_k} x_{ij}$$

We call $\bar{x}_k$ the centroid of class $k$. Diagonal LDA can be seen as a nearest centroid classifier with appropriate standardization.

To regularize in order to drop out features, we can shrink the classwise mean toward the overall mean for each feature separately. This method is called Nearest Shrunken Procedure (NSC).

Let
$$d_{jk} = \frac{\bar{x}_{kj} - \bar{x}_j}{m_k(s_j + s_0)}$$

with $m_k^2 = 1/N_k - 1/N$ and $s_0$ a small positive constant.

We can shrink $d_{kj}$ toward zero using soft thresholding:
$$d'_{kj} = \text{sign}(d_{kj})(|d_{kj}| - \Delta)_+$$
with $\Delta$ a parameter to be determined.

The shruken centroids are obtained by:
$$\bar{x}'_{kj} = \bar{x}_j + m_k(s_j + s_0)d'_{kj}$$
We use the shrunken centroids $\bar{x}'_{kj}$ instead of the original $\bar{x}_{kj}$ in the discriminant score.

# Linear Classifiers with Quadratic Regularization

## Regularized Discriminant Analysis

LDA involves the inversion of a $p*p$ within-covariance matrix $\Sigma$. When $p > n$, the matrix is singular.
RDA solves the issue by shrinking $\Sigma$ towards its diagonal:
$$\hat{\Sigma}(\gamma) = \gamma \hat{\Sigma} + (1 - \gamma) \text{diag}(\hat{\Sigma})$$

## Logistic Regression with Quadratic Regularization

The multiclass logistic model is expressed as:
$$P(G=k|X=x) \frac{\exp (\beta_{k0} + x^T \beta_k)}{\sum_{l=1}^K \exp (\beta_{l0} + x^T \beta_l)}$$

This has $K$ coefficients vecors $\beta_k$. We regalurize the fitting by maximizing the penalized log-likelihhood:
$$\max_{ \{ \beta_{0k}, \beta_k \}_1^K} \left[ \sum_{i=1}^N \log P(g_i|x_i) - \frac{\lambda}{2} \sum_{k=1}^K ||\beta_k||_2^2 \right]$$

## The Support Vector Classifier

When $p > N$, the classes are perfectly separable, unless there are identical feature vectors in different classes.
Surprisingly, the unregularized SVC often works about as well as the best regularized version.

## Feature Selection

RDA, regularized logistic regression and SVC shrinks weights toward zero, but they keep all features.
Recursive feature elimination remove feature with small weights, and retrain the classifier.

All 3 approches can be modified using kernels, to increase model complixity. With $p > N$ overfitting is always a danger, and yet using kernel may sometimes give better results.

## Computational shorcuts when $p \gg N$

Instead of working with $X \in \mathbb{R}^{N*p}$ matrix, we can work with a matrix of size $N*N$, using the SVD:
$$
\begin{equation}
\begin{split}
X & = UDV^T \\
& = RV^T
\end{split}
\end{equation}
$$

with $R \in \mathbb{R}^{N*N}$:
$$R = UD$$

We can usually work with $R$ instead of $X$.
For example, let's consider the estimates from a ridge regression:
$$\hat{\beta} = (X^TX + \delta I)^{-1}X^ty$$

We can instead get $\hat{\theta}$ the ridge regression estimate using $(r_i, y_i)$. And then we get $\hat{\beta} = V \hat{\theta}$.
This idea can be generalized to any linear models with a quadratic penalty on the weights.

# Linear classifiers with $L_1$ Regularization

The lasso for linear regression is:
$$\min_{\beta} \frac{1}{2} \sum_{i=1}^N \left( y_i - \beta_0 - \sum_{j=1}^p x_{ij}\beta_j \right) ^2 + \lambda \sum_{j=1}^p |\beta_j|$$

$L_1$ penalty causes a subset of the $\hat{\beta}_j$ to be exactly zero for a sufficiently large value of $\lambda$, and hence performs feature selection.

When $p > N$, as $\lambda \to 0$ the model fits perfectly the dataset.
When $p > N$ the number of non-zero coefficients is at most $N$ for any values of $\lambda$.

Linear regression can be applied for two-class clasification using $\pm 1$ as labels, and using sign for the predictions.

A more natural approach is to use the lasso penalty on logistic regression. We can use a symmetric multinomial logistric regression model, and maximixe the penalized log-likelihood:
$$\max_{ \{ \beta_{0k}, \beta_k \}_1^K} \left[ \sum_{i=1}^N log P(g_i|x_i) - \lambda \sum_{k=1}^K \sum_{j=1}^p |\beta_{kj}|  \right]$$

The lasso tends to encourage a sparse solution, and ridge tends to shrink the oefficients of correlated variables toward each other.
The elastic net penalty is a compromise:
$$\sum_{j=1}^p (\alpha |\beta|_j + (1 - \alpha) \beta_j^2)$$
with $\alpha \in [0,1]$ parameter that determines the mix of the penalties.

The logistic regression problem above with the elastic net penalty becomes:
$$\max_{ \{ \beta_{0k}, \beta_k \}_1^K} \left[ \sum_{i=1}^N log P(g_i|x_i) - \lambda \sum_{k=1}^K \sum_{j=1}^p (\alpha|\beta_{kj}| + (1 - \alpha) \beta_{kj}^2)  \right]$$

## The Fused Lasso

The Fused Lasso is a method that tend to smooth the coefficients uniformly. We add a penalty to take into account the ordering of the features:

$$\min_{\beta} \sum_{i=1}^N \left( y_i - \beta_0 - \sum_{j=1}^p x_{ij}\beta_j \right) ^2 + \lambda_1 \sum_{j=1}^p |\beta_j| + \lambda_2 \sum_{j=1}^{p-1} |\beta_{j-1} - \beta_j|$$

# Classification When Features are Unavailable

Instead of working with features, we can instead work with an $N*N$ proximity matrix, and we can interpret the proximities as inner-products.

For example, it can be considerer as the matrix kernel $K$, and can be used with kernel methods, SVM.

## Classition and other methods using Kernels

They are a number of other classifier, besides SVM, that can be implemented using only inner-product matrices. This also implies they can be kernelized like the SVM.

For nearest-neigbor, we can transform inner-products to distances:
$$||x_i - x_{i'}||^2 = \langle x_i, x_i \rangle + \langle x_{i'}, x_{i'} \rangle - \langle x_i, x_{i'} \rangle$$

For nearest-centroid classification, with training pairs $(x_i, g_i)$, and class  centroids $\bar{x}_k$, we can compute the distance of the test point to each centroid:
$$||x_0 - \bar{x}_k||^2 = \langle x_0, x_0 \rangle - \frac{2}{N_k} \sum_{g_i=k} \langle x_0, x_i \rangle + \frac{1}{N_k^2} \sum_{g_i=k} \sum_{g_{i'}=k} \langle x_i, x_{i'} \rangle$$

We can also perform kernel PCA. Let $X$ centered data matrix, with SVD decomposition:
$$X=UDV^T$$
We get the matrix of principal components $Z$:
$$Z = UD$$
When $K=XX^T$, it follow that $K=UD^2U^T$, and hence we can compute $Z$ from the eigeindecomposition of $K$.
If $X$ is not centered, we need to use the double-centered kernel instead:
$$\tilde{K} = (I-M)K(I-M)$$
with $M = \frac{1}{N} 1 1^T$.

But they are some things that we cannot do with kernels:
- We cannot standardize the variables.
- We cannot assess directly the contribution of individual variables (i.e. we cannot use the lasso penalty)
- We cannot separate the good variables from the noise: they all get an equal say.

# High-Dimensional Regression: Supervised Principal Components

PCA is an effective method to find linear combinations of features that exhibit large variation in the data.
Supervised PCA find linear linear combination with both high variance and significant correlation with the outcome.

Supervised PCA can be related Latent-Variable modeling.
Suppose we hase a response variable $Y$ related to an underlying latent variable U by a linear model:
$$Y = \beta_0 + \beta_1 U + \sigma$$

We have measurements on a set of features $X_j$, $j \in \mathcal{P}$:
$$X_j = \alpha_{0j} + \alpha_{1j}U + \sigma_j, \space j \in \mathcal{P}$$
We also have many additional features $X_k$, $k \notin \mathcal{P}$, which are independent of $U$.

This is a 3 steps, proccess, similar to supervised PCA:
- Estimates the set $\mathcal{P}$
- Given $\hat{\mathcal{P}}$, estimates $U$
- Perform a regression fit to estimate $\beta$, $\beta_0$.

# Feature Assessment and the Multiple-Testing Problem

Feature Assessment asses the significance of each features, it's related to multiple hypothesis testing.
Let's suppose we have a dataset ok $N$ observations, each with $M$ features, separated into $K=2$ groups.

To identify which features are informative to guess the group, we construct a two-sample t-statistic for each feature:
$$t_j = \frac{\bar{x}_{2j} - \bar{x}_{1j}}{\text{se}_j}$$
where:
$$\bar{x}_{kj} = \frac{1}{N_k} \sum_{i \in C_k} x_{ij}$$

$\text{se}_j$ is the pooled within-group standard error for feature $j$:
$$\text{se}_j = \hat{\sigma}_j \sqrt{\frac{1}{N_1} + \frac{1}{N_2}}$$
$$\hat{\sigma}_j^2 = \frac{1}{N_1 + N_2 - 2} \left( \sum_{i \in C_1} (x_{ij} - \bar{x}_{1j})^2 + \sum_{i \in C_2} (x_{ij} - \bar{x}_{2j})^2 \right)$$

We could consider any value $> 2$ in absoluve value to be significantly large. However, with $M$ large, we would expect many largve values to occur by chance.
We can assess the result for all $M$ using the multiple testing problem.
We can compute the p-value for each feature $p_j$:
$$p_j = \frac{1}{K} \sum_{k=1}^K I(|t_j^k| > |t_j|)$$

where whe take $K$ random sample labels permutations $t_j^k$.

Using p-values, we can test the hypotheses:
$H_{0j} = $ label has no effect on feature $j$.
$H_{1j} = $ label has an effect on feature $j$.
We reject $H_{0j}$ at level $\alpha$ if $p_j < \alpha$.

Let $A_j$ the event that $H_{0j}$ if falsely rejected: $P(A_j) = \alpha$
The familiy-wise error rate (FWER) is the probability of at least one false rejection.

## The False Discovery Rate

Possible outcomes from $M$ hypotesis tests:

| |Called not significant|Called signification|Total|
|---|---|---|---|
|$H_0$ True| $U$ | $V$ | $M_0$|
|$H_0$ False|$T$|$S$|$M_1$|
|Total|$M-R$|$R$|$M$|

The false discovery rate is:
$$\text{FDR} = E(V/R)$$

The expectation is taken over the sampled data.

## Asymmetric Cutpoints and the SAM Procedure

In previous approaches, we used the absolute value of $t_j$, hence applying the same cutpoint to both positive and negative values.
Significance analysis of microrrays (SAM) derive separate cut-point for the two classes.


Álgebra Lineal

1  Matrices

1.1  Propiedades de la matriz y algunas matrices elementales

1.2  Exploración de matrices

1.3  Suma y resta

1.4  Ejemplo:

1.4.1  Solución:

1.5  Multiplicación por escalar

1.6  Ejemplo:

1.6.1  Solución:

1.7  Transpuesta

1.8  Multiplicación

1.9  Ejercicio

1.10  Análisis de la multiplicación en las aplicaciones

1.11  Matrices Inversas

2  Resolución de sistemas de ecuaciones lineales

2.1  Método de eliminación de Gauss Jordan

2.1.1  Operaciones elementales de fila

2.1.2  Algoritmo

2.1.3  Ejemplos:

2.2  Ahora con Python

3  Determinantes

3.1  ¿Cómo se calcula?

3.2  ¿Qué pasa si la matriz es de mayor orden?

3.2.1  Algoritmo

3.3  Ejercicios:

3.4  ¿Cómo lo hago en Python?

3.4.1  Ejercicio

3.5  ¿Cómo puedo aplicar los determinantes?

3.5.1  Ejemplo:

3.6  ¿Se puede hallar la solución del sistema utilizando determinantes?

3.6.1  Ejemplo:

3.6.2  Problema

3.6.3  Solución

```python

```

Álgebra Lineal - Fundamentos

Pensemos en la siguiente situación:

Cada uno de los vendedores entregarán tablas como las siguientes:

    Vendedor 1
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    30
    50
    40
    20
    50

     Producto2
    35
    25
    45
    15
    20

     Producto3
    30
    10
    25
    25
    15

    Vendedor 2
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    10
    70
    30
    60
    20

     Producto2
    35
    25
    75
    25
    45

     Producto3
    35
    15
    15
    15
    25

 ¿Cómo sintetiza la información entregada por los dos vendedores?

 ¿Obtendría una información similar?

 ¿Cómo expresaría la diferencia de ventas entre vendedores de acuerdo al día de la semana?

 ¿Cómo expresaría las ventas de la distribuidora en esa semana si quiere evaluar el desempeño diario de sus vendedores?

 Esperamos que tratando de responder las anteriores preguntas hayan surgido varias ideas, sin embargo, teniendo en cuenta que hablamos de productos diferentes en días distintos un ejercicio conveniente es presentar una tabla de las mismas dimensiones tanto para presentar el informe totalizado de ventas como para presentar el desempeño de cada vendedor y su diferencia. Es claro que la suma se hace componente a componente y la tabla se estructura como un elemento que se puede "sumar"  y  "restar". Este elemnto se abstrae con el concepto de matriz, una generalización de lo que conocemos como tablas con la posibilidad de dotarlas con operaciones algebraicas.

## Matrices

Una matriz es un arreglo bidimensional, en Python se representa como una lista de listas y es preferible usar *array* del paquete **numpy** para poder hacer todo tipo de operaciones matriciales.

Escribimos primero

*import numpy as np*,

con el fin de llamar todas las funciones de numpy con el prefijo np.*funcion*.

```python
import numpy as np
```

Veamos algunos ejemplos, dos matrices 3x3 A y B (observe que cada lista se define con corchetes "[", "]", para nuestro caso, como es una arreglo bidimensional usamos dos niveles de corchetes para lograr la lista de listas):

```python
A = np.array([[1, 3, 2],
              [1, 0, 0],
              [1, 2, 2]])

B = np.array([[1, 0, 5],
              [7, 5, 0],
              [2, 1, 1]])
```

```python
A
```

    array([[1, 3, 2],
           [1, 0, 0],
           [1, 2, 2]])

```python
B
```

    array([[1, 0, 5],
           [7, 5, 0],
           [2, 1, 1]])

```python
## Una matriz 4x5
C=np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15],[16,17,18,19,20]])
```

```python
C
```

    array([[ 1,  2,  3,  4,  5],
           [ 6,  7,  8,  9, 10],
           [11, 12, 13, 14, 15],
           [16, 17, 18, 19, 20]])

```python
C.dtype
```

    dtype('int64')

### Propiedades de la matriz y algunas matrices elementales

La cantidad de elementos y el tamaño son  algunas propiedades de cualquier matriz, en Python contamos con funciones para encontrar esos valores y además tenemos la oportunidad de crear algunas matrices elementales.

> Para las propiedades elementales escribimos el nombre de la matriz y luego la función:
> * **Dimensión:**     [*nombre de la matriz*].*shape*
> * **Cantidad de elementos:**     [*nombre de la matriz*].*size*

> Para las matrices elementales usamos el paqueter **numpy**:
> *  **Matriz de unos de mxn:** *np.ones([m,n])*
> *  **Matriz de ceros de mxn:** *np.zeros([m,n])*
> *  **Matriz idéntidad de nxn:** *np.identity(n)*

```python
C.shape
```

    (4, 5)

```python
A.shape
```

    (3, 3)

```python
## Una matriz de 3x5

V1=np.array([[30,50,40,20,50],[35,25,45,15,20],[30,10,25,25,15]])

```

```python
V1
```

    array([[30, 50, 40, 20, 50],
           [35, 25, 45, 15, 20],
           [30, 10, 25, 25, 15]])

```python
V1.shape
```

    (3, 5)

```python
V1.size
```

    15

```python
B.size
```

    9

```python
Matriz_de_unos=np.ones([4,5])
```

```python
Matriz_de_unos
```

    array([[1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.]])

```python
Matriz_de_ceros=np.zeros([3,2])
Matriz_de_ceros
```

    array([[0., 0.],
           [0., 0.],
           [0., 0.]])

```python
np.identity(5)
```

    array([[1., 0., 0., 0., 0.],
           [0., 1., 0., 0., 0.],
           [0., 0., 1., 0., 0.],
           [0., 0., 0., 1., 0.],
           [0., 0., 0., 0., 1.]])

**Algunas definiciones**

*Matriz cuadrada:* Es una matriz con la misma cantidad de filas y columnas.

*Matriz identidad:* Es una matriz cuadrada que en la diagonal (elementos que están ubicados en la misma fila y columna)  tiene unos y fuera de la diagonal tiene ceros.

**Ejemplo**

$$\begin{pmatrix}1&0&0\\ 0&1&0\\ 0&0&1\end{pmatrix}$$

### Exploración de matrices

En general, una matriz de mxn tiene m filas y n columnas, y nos referimos a ella de manera unívoca cuando conocemos todos sus elementos y sus posiciones.

En otras palabras, una matriz es igual a otra si tiene la misma dimensión, los mismo elementos y ellos ocupan los mismos lugares:

$$\begin{pmatrix}1&2&3\\ 0&1&0\\ 0&4&1\end{pmatrix}\neq\begin{pmatrix}1&2&3\\ 0&1&0\\ 0&1&4\end{pmatrix}$$

**Notación de los elementos de una matriz**

Sea $A$ una matriz mxn, el elemento $a_{i j}$ cooresponde al elemento de la fila i-ésima y la columna j-ésima de la matriz. En Python se maneja la misma idea pero contando desde ***cero***.

```python
## Una matriz 3x3

A=np.array([[1,2,3],[0,1,0],[0,4,1]])
B=np.array([[1,2,3],[0,1,0],[0,1,4]])

```

```python
A
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 4, 1]])

```python
B
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 1, 4]])

```python
A==B
```

    array([[ True,  True,  True],
           [ True,  True,  True],
           [ True, False, False]])

```python
A[3,2] #Genera error porque no cuenta desde cero
```

```python
A[2,1]
```

    4

```python
B[2,1]
```

    1

**Muy importante tener en cuenta que las posiciones en Python están afectadas por iniciar siempre desde cero**

### Suma y resta

La suma/resta de matrices se hace componente a componente, esto nos indica que las matrices deben tener las mimas dimensiones, a esto se le conoce en ser compatibles para la suma/resta. Por ejemplo, si tenemos dos matrices de tamaño $2\times2$

$$A=\begin{bmatrix}a&b\\c&d\end{bmatrix} \\  B=\begin{bmatrix}e&f\\ g&h \end{bmatrix}\\
A\pm B= \begin{bmatrix}a\pm e & b \pm f\\ c \pm g & d\pm h\end{bmatrix}$$
Si $C$ fuera una matriz de un tamaño diferente a $2\times2$ la suma con $A$ o $B$ sería imposible de calcular.

### Ejemplo:
Verifique la suma y la resta de las matrices definidas en la celda de código anterior:

#### Solución:

```python
A
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 4, 1]])

```python
B
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 1, 4]])

```python
A+B
```

    array([[2, 4, 6],
           [0, 2, 0],
           [0, 5, 5]])

```python
A-B
```

    array([[ 0,  0,  0],
           [ 0,  0,  0],
           [ 0,  3, -3]])

### Multiplicación por escalar
Como su nombre lo indica se está multiplicando una matriz por un número real cualquiera, el número real $r$ podrá ser positivo negativo o inluso cero, y la matriz $A$ podrá ser de cualquier tamaño, el resultado de $rA$ es simplemente multiplicar cada entrada de $A$ por $r$.

Si $A$ es una matriz de $n$ filas y $m$ columnas

$$A=\left[ \begin{array}{llll}
           a_{11}& a_{12}&\ldots& a_{1m}\\
           a_{21}& a_{22}&\ldots& a_{2m}\\
           \ \ \ \vdots& \ \ \ \vdots& \ \vdots& \ \ \ \vdots\\
           a_{n1}& a_{n2}&\ldots& a_{nm}
             \end{array}
   \right],\\
   rA=\left[ \begin{array}{llll}
           ra_{11}& ra_{12}&\ldots& ra_{1m}\\
           ra_{21}& ra_{22}&\ldots& ra_{2m}\\
           \ \ \ \vdots& \ \ \ \vdots& \ \vdots& \ \ \ \vdots\\
           ra_{n1}& ra_{n2}&\ldots& ra_{nm}
             \end{array}
   \right].$$

### Ejemplo:
Teniendo en cuenta las matrices definidas anteriormente,calcule $3A$, $-2A$ y $\pi B$.
#### Solución:

```python
A
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 4, 1]])

```python
3*A
```

    array([[ 3,  6,  9],
           [ 0,  3,  0],
           [ 0, 12,  3]])

```python
-2*A
```

    array([[-2, -4, -6],
           [ 0, -2,  0],
           [ 0, -8, -2]])

```python
B
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 1, 4]])

```python
np.pi*B ## Usamos un valor irracional muy conocido, el número PI
```

    array([[ 3.14159265,  6.28318531,  9.42477796],
           [ 0.        ,  3.14159265,  0.        ],
           [ 0.        ,  3.14159265, 12.56637061]])

**Otros Ejemplos**

Ingresemos la matrices de ventas expuesta en el inicio de este cuaderno:

    Vendedor 1
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    30
    50
    40
    20
    50

     Producto2
    35
    25
    45
    15
    20

     Producto3
    30
    10
    25
    25
    15

    Vendedor 2
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    10
    70
    30
    60
    20

     Producto2
    35
    25
    75
    25
    45

     Producto3
    35
    15
    15
    15
    25

```python
V1 = np.array ([[30, 50, 40, 20, 50],[35,25,45,15,20],[30,10,25,25,15]])
V2=np.array([[10, 70, 30, 60, 20],[35, 25, 75, 25, 45],[35, 15, 15, 15, 25]])
```

```python
print("Vendedor 1:")
print(V1)
print("\n\nVendedor 2:")
print(V2)
```

    Vendedor 1:
    [[30 50 40 20 50]
     [35 25 45 15 20]
     [30 10 25 25 15]]

    Vendedor 2:
    [[10 70 30 60 20]
     [35 25 75 25 45]
     [35 15 15 15 25]]

**¿Cómo interpretamos la siguiente operación?**

```python
V1+V2
```

    array([[ 40, 120,  70,  80,  70],
           [ 70,  50, 120,  40,  65],
           [ 65,  25,  40,  40,  40]])

> Obtenemos una matriz que expresa la venta realizada por los dos vendedores durante esa semana.

**Suponga que el vendedor 1 tuvo el mismo desempeño en ventas durante 4 semanas ¿Cómo expreso una tabla que me permita determinar el total de ventas que el vendedor 1 hizo cada día de la semana, por producto?**

> Usamos multiplicación por escalar, particularmente, 4*V1.

```python
4*V1
```

    array([[120, 200, 160,  80, 200],
           [140, 100, 180,  60,  80],
           [120,  40, 100, 100,  60]])

**Ayuda visual para entender la matriz**

```python
import seaborn as sns
ax=sns.heatmap(V1,annot=True)
```

### Transpuesta

La transpuesta es la operación sobre una matriz que cambia filas por columnas, para esta sección usaremos unas matrices rectangulares, observe que hacemos el arreglo bidimensional haciendo una lista de listas con la función **np.array**, cada lista interna representa una fila de la matriz:

```python
M=np.array([[1,2,3,4],[-5,-6,-7,-8],[9,10,11,12]])
```

```python
M
```

    array([[ 1,  2,  3,  4],
           [-5, -6, -7, -8],
           [ 9, 10, 11, 12]])

```python
np.transpose(M)
```

    array([[ 1, -5,  9],
           [ 2, -6, 10],
           [ 3, -7, 11],
           [ 4, -8, 12]])

## Multiplicación falsa (componente a componente) de Python

Lo que obtenemos al multiplicar dos matrices en Python con el símbolo *, es una multiplicación componente a componente que no tiene sentido en matemáticas:

```python
V1
```

    array([[30, 50, 40, 20, 50],
           [35, 25, 45, 15, 20],
           [30, 10, 25, 25, 15]])

```python
V2
```

    array([[10, 70, 30, 60, 20],
           [35, 25, 75, 25, 45],
           [35, 15, 15, 15, 25]])

```python
V1*V2
```

    array([[ 300, 3500, 1200, 1200, 1000],
           [1225,  625, 3375,  375,  900],
           [1050,  150,  375,  375,  375]])

### Multiplicación

A diferencia de las otras operaciones, la multiplicación no se hace componente a componente, se multiplican filas por columnas, y para que sean compatibles las matrices para el producto, la cantidad de columnas de la primera matriz debe coincidir con la cantidad de filas de la segunda, es decir

$$A_{nm}B_{ml}=C_{nl}$$

Debido a su definición tan estricta con el orden, se convierte en una de las primeras operaciones conocidas por nosotros que no es conmutativa.

Para que nos demos a una idea de la operación haremos un ejemplo:

$$A=\begin{bmatrix}1&2\\3&0\\5&-1\end{bmatrix},\ B=\begin{bmatrix}0&3&4&-1\\
1&2&-1&0\end{bmatrix}$$
Como la matriz $A$ es de tamaño $3\times2$ y la matriz B es $2\times4$ el resultado será una matriz $3\times4$
$$A\times B=\begin{bmatrix}1\cdot0+2\cdot1&1\cdot3+2\cdot2& 1\cdot4+2\cdot(-1)&1\cdot(-1)+2\cdot0\\ 3\cdot0+0\cdot1&3\cdot3+0\cdot2& 3\cdot4+0\cdot(-1)&3\cdot(-1)+0\cdot0\\ 5\cdot0+(-1)\cdot1&5\cdot3+(-1)\cdot2& 5\cdot4+(-1)\cdot(-1)&5\cdot(-1)+(-1)\cdot0\end{bmatrix}\\
=\begin{bmatrix} 2&7&2&-1\\0&9&12&-3\\-1&13&21&-5\end{bmatrix}$$

Notemos que el producto de $B$ por $A$ no se puede realizar ya que $4\neq3.$

En Python usamos el comando @ para hacer esta multiplicación.

```python
print(A) ## El comando print() permite visualizar un poco mejor la matriz.
print(M)

print("\n El resultado es:")
print(A@M)
```

    [[1 2 3]
     [0 1 0]
     [0 4 1]]
    [[ 1  2  3  4]
     [-5 -6 -7 -8]
     [ 9 10 11 12]]

     El resultado es:
    [[ 18  20  22  24]
     [ -5  -6  -7  -8]
     [-11 -14 -17 -20]]

```python
print(M.shape)
print(A.shape)
A@M
```

    (3, 4)
    (3, 3)

    array([[ 18,  20,  22,  24],
           [ -5,  -6,  -7,  -8],
           [-11, -14, -17, -20]])

Observe lo que ocurre cuando multiplicamos matrices con dimensiones no apropiadas
### Ejercicio
Verifique las operaciones realizadas en la explicación  con código de Python.

```python
V1
```

    array([[30, 50, 40, 20, 50],
           [35, 25, 45, 15, 20],
           [30, 10, 25, 25, 15]])

```python
P=np.array([[40],[60],[80],[80],[20]])
P
```

    array([[40],
           [60],
           [80],
           [80],
           [20]])

```python
Ventas_V1=V1@P
Ventas_V1
```

    array([[10000],
           [ 8100],
           [ 6100]])

```python
Ventas_V2=V2@P
Ventas_V2
```

    array([[12200],
           [11800],
           [ 5200]])

### Análisis de la multiplicación en las aplicaciones

El vendedor 1 del ejemplo anterior tenía estas ventas semanales:

    Vendedor 1
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    30
    50
    40
    20
    50

     Producto2
    35
    25
    45
    15
    20

     Producto3
    30
    10
    25
    25
    15

Suponga que vamos a darle una comisión según el producto a cada integrante del equipo de ventas, según la siguiente tabla:

||Producto 1|Producto 2|Producto 3|
|:---:|:---:|:---:|:---:|
|Comisión|\$20.000 |\$30.000|\$50.000|

```python
Comision=np.array([[20,30,50]])
```

```python
V1
```

    array([[30, 50, 40, 20, 50],
           [35, 25, 45, 15, 20],
           [30, 10, 25, 25, 15]])

```python
Comision_dia=Comision@V1
print(Comision_dia.sum())
print(Ventas_V1.sum())
```

    13250
    24200

**Observaciones profundas de este caso:**

```python
P=np.array([[40],[80],[80],[20],[60]])
P
```

    array([[40],
           [80],
           [80],
           [20],
           [60]])

```python
Ventas_V1_nueva=V1@P
Ventas_V1_nueva.sum()
```

    25700

```python
Ventas_V2_nueva=V2@P
Ventas_V2_nueva.sum()
```

    29000

## Utilidad del ejercicio de ventas

Calculamos la utilidad, aprovechando que ya sabemos como calcular las ventas y las comisiones. Es claro que:

> **U=V-C**

```python
Ventas_Totales=(V1+V2)@P
Ventas_Totales
```

    array([[22600],
           [21100],
           [11000]])

```python
Comision_Total=Comision@(V1+V2)
Comision_Total
```

    array([[6150, 5150, 7000, 4800, 5350]])

```python
Utilidad=Ventas_Totales.sum()-Comision_Total.sum()
Utilidad
```

    26250

```python
P=np.array([[40],[80],[80],[20],[60]])
Comision=np.array([[20,30,50]])

Ventas_Totales=(V1+V2)@P
Comision_Total=Comision@(V1+V2)
Utilidad=Ventas_Totales.sum()-Comision_Total.sum()

print('Total de ventas es:',Ventas_Totales.sum())
print('Comision_Total es: ',Comision_Total.sum())
print('Utilidad es: ',Utilidad)
```

    Total de ventas es: 54700
    Comision_Total es:  28450
    Utilidad es:  26250

```python
P=np.array([[60],[80],[80],[20],[40]])
Comision=np.array([[30,20,50]])

Ventas_Totales=(V1+V2)@P
Comision_Total=Comision@(V1+V2)
Utilidad=Ventas_Totales.sum()-Comision_Total.sum()

print('Total de ventas es:',Ventas_Totales.sum())
print('Comision_Total es: ',Comision_Total.sum())
print('Utilidad es: ',Utilidad)
```

    Total de ventas es: 54700
    Comision_Total es:  28800
    Utilidad es:  25900

La idea de unificar el proces es crear una función que me permita trabajar con más libertad:

```python
def Utilidaddelej(p1,p2,p3,p4,p5,c1,c2,c3):
    P=np.array([[p1],[p2],[p3],[p4],[p5]])
    Comision=np.array([[c1,c2,c3]])

    Ventas_Totales=(V1+V2)@P
    Comision_Total=Comision@(V1+V2)
    Utilidad=Ventas_Totales.sum()-Comision_Total.sum()

    print('Total de ventas es:',Ventas_Totales.sum())
    print('Comision_Total es: ',Comision_Total.sum())
    print('Utilidad es: ',Utilidad)
    return Utilidad
```

```python
Utilidaddelej(50,80,80,40,60,20,30,50)
```

    Total de ventas es: 59650
    Comision_Total es:  28450
    Utilidad es:  31200

    31200

```python
import ipywidgets as widgets
from ipywidgets import interact

interact(Utilidaddelej,p1=40,p2=80,p3=80,p4=20,p5=60,c1=20,c2=30,c3=50)
```

    interactive(children=(IntSlider(value=40, description='p1', max=120, min=-40), IntSlider(value=80, description…

    <function __main__.Utilidaddelej(p1, p2, p3, p4, p5, c1, c2, c3)>

```python
np.array([1,1,1])@(V1+V2)
```

```python
ax=sns.heatmap(V1+V2,annot=True,fmt=".0f")
```

## Otro problema aplicado

Trabajamos en una textilera que produce 3 tipos de telas producidas a partir de 5 materiales como sigue:

||Algodon|Poliester|Caucho|Lana|Hilo|
|:--:|:--:|:--:|:--:|:--:|:--:|
|Tela 1|400|400|50|75|25|
|Tela 2|500|400|0|50|50|
|Tela 3|500|300|150|20|30|

Los precios de los materiales se relacionan a continuación:

||Precio|
|:--:|:--:|
|Algodon|20.000|
|Poliester|15.000|
|Caucho|10.000|
|Lana|12.000|
|Hilo|7.000|

Hay un pedido que requiere que se entreguen la siguiente cantidad de tela:

|Tela|Cantidad|
|:--:|:--:|
|Tela 1|320 ton|
|Tela 2|240 ton|
|Tela 3|150 ton|

¿Cuanto tengo que invertir?

```python
Cant_Tela=np.array([320,240,150])
Precio_Material=np.array([20000,15000,10000,12000,7000])
Tela_Material=np.array([[400,400,50,75,25],
                       [500,400,0,50,50],
                       [500,300,150,20,30]])
```

```python
Cant_Tela@Tela_Material@np.transpose(Precio_Material)
```

### Matrices Inversas
Si una matriz cuadrada $A$ de $n\times n$ se multiplica por derecha e izquierda por una matriz $B$ y se obtiene la identidad en ambos casos, es decir
$$AB=BA=I_n$$
La matriz $B$ se conoce como el inverso multiplicativo de la matriz $A$ o más comunmente como la matriz inversa de $A$.

No todas las matrices cuadradas tienen inversa, la forma de determinarla empleando Python es meduante la función  **np.linalg.inv**

```python
N = np.array([[1, 0, 1], [2, -1, 3], [4, 3, 2]])
N
```

    array([[ 1,  0,  1],
           [ 2, -1,  3],
           [ 4,  3,  2]])

```python
Ninv=np.linalg.inv(N)
Ninv
```

    array([[ 11.,  -3.,  -1.],
           [ -8.,   2.,   1.],
           [-10.,   3.,   1.]])

Comprobamos este resultado:

```python
(Ninv@N).round()
```

    array([[ 1., -0., -0.],
           [ 0.,  1.,  0.],
           [ 0.,  0.,  1.]])

Observe que la anterior es la matriz idéntica.

## Resolución de sistemas de ecuaciones lineales

El método más común para solucionar sistemas de ecuaciones es el conocido como Método de eliminación de Gauss-Jordan, el cual consiste básicamente en aplicar operaciones elementales a la matriz de coeficientes para hallar la solución del sistema, también se emplea para hallar inversas de martices.

### Método de eliminación de Gauss Jordan

Si se tiene el sistema de ecuaciones

$$\begin{cases}2x-4y=6\\
-4x+6y=-14
\end{cases}$$

Se puede verificar de manera sencilla que su solución es $x=5$ y $y=1$.

Si se remplaza la segunda ecuación por la suma de las dos veces la primera  más la segunda, obtendremos un nuevo sistema, el cual tiene como solución la misma de antes

$$\begin{cases}2x-4y=6\\
-y=-1
\end{cases}$$

Si se divide la segunda ecuación del primer sistema entre dos, obtenemos
$$\begin{cases}2x-4y=6\\
-2x+3y=-7
\end{cases}$$
sistema el cual también posee la misma solución.

Y si cambiamos el orden en las ecuaciones
$$\begin{cases}-4x+6y=-14\\2x-4y=6
\end{cases}$$
el sistema evidentemente tendrá las mismas soluciones.

Estas operaciones que acabamos de realizar sobre el sistema de ecuaciones son las conocidas como operaciones elementales, y éstas nunca van a cambiar las soluciones del sistema de ecuaciones. Éstas también pueden hacerse en columnas pero no es tan utilizado.

__________________
#### Operaciones elementales de fila
1. Cambio de filas.
2. Multiplicar una fila por una constante.
3. Sumar a una fila un múltiplo de ota.
_________________
Con esto en mente vale la pena trabajar con ellas para hallar la solución de un sistema de ecuaciones.

Como vimos anteriormente un sistema de ecuaciones se puede representar de manera matricial, por ejemplo

$$\begin{cases}2x-4y=6\\
-4x+6y=-14
\end{cases}$$
se puede ver como
$$\begin{bmatrix} 2&-4\\-4&6\end{bmatrix}\begin{bmatrix} x\\y\end{bmatrix}=\begin{bmatrix} 6\\-14\end{bmatrix}$$
La idea fundamental de escribir el sistema de esta manera es aplicar las operaciones elementales, para obtener lo siguiente
$$\begin{bmatrix} 1&0\\0&1\end{bmatrix}\begin{bmatrix} x\\y\end{bmatrix}=\begin{bmatrix} 5\\1\end{bmatrix}$$
ya que al realizar las operaciones de matrices obtendremos que $x=5$ y que $y$=1.

Realizaremos el procedimiento para la matriz que se tiene, con el fin de verificar la respuesta
$$
\left[
\begin{array}{cc|c}
2&-4&6\\
-4&6&-14
\end{array}
\right]
$$
El primer paso será convertir el 2 que está en la diagonal principal en un 1, esto lo podemos hacer dividiendo toda la fila entre 2,

$$
\left[
\begin{array}{cc|c}
2&-4&6\\
-4&6&-14
\end{array}
\right]\xrightarrow{f_1=\frac{1}{2}f_1}\left[
\begin{array}{cc|c}
1&-2&3\\
-4&6&-14
\end{array}
\right]
$$
Ya que tenemos el 1 en la diagonal principal debemos transformar en cero las demás entradas de la columna, en este caso al -4, esto se logra fácilmente sumando 4 veces la fila 1 a la fila 2
$$\left[
\begin{array}{cc|c}
1&-2&3\\
-4&6&-14
\end{array}
\right]\xrightarrow{f_2=f_2+4f_1}\left[
\begin{array}{cc|c}
1&-2&3\\
0&-2&-2
\end{array}
\right]
$$
Como ya se han anulado todos los elementos que no están en la diagonal principal, procedemos a volver 1 el siguiente elemento de la diagonal principal, en este caso al -2 que está en la segunda fila segunda columna.

$$\left[
\begin{array}{cc|c}
1&-2&3\\
0&-2&-2
\end{array}
\right]\xrightarrow{f_2=\frac{-1}{2}}\left[
\begin{array}{cc|c}
1&-2&3\\
0&1&1
\end{array}
\right]
$$
Y finalmente anulamos todos los elementos que no estén sobre la diagonal principal
$$\left[
\begin{array}{cc|c}
1&-2&3\\
0&1&1
\end{array}
\right]\xrightarrow{f_1=f_1+2f_2}\left[
\begin{array}{cc|c}
1&0&5\\
0&1&1
\end{array}
\right]
$$
De modo que $x=5$ mientras que $y=1$.

_____
#### Algoritmo
1. Si la entrada $a_{11}$ de la matriz no es un 1, debe transformase utilizando lasoperaciones elementales.
2. Volver cero todas las entradas de esa columnas que no estén sobre la diagonal principal.
3. Si la entrada $a_{22}$ de la matriz no es un 1, debe transformase utilizando lasoperaciones elementales.
4. Volver cero todas las entradas de esa columnas que no estén sobre la diagonal principal.
5. Repetir el proceso hasta agotar todas las columnas de la matriz.
_____

####  Ejemplos:
1. Solucione el siguiente sistema de ecuaciones utilizando Gauss-Jordan
$$\begin{cases}3x-15y=9\\
-6x+30y=0
\end{cases}$$

$$\left[
\begin{array}{cc|c}
3&-15&9\\
-6&30&0
\end{array}
\right]\xrightarrow{f_1=\frac{1}{3}f_1}\left[
\begin{array}{cc|c}
1&-5&3\\
-6&30&0
\end{array}
\right] \xrightarrow{f_2=f_2+6f_1}\left[
\begin{array}{cc|c}
1&-5&3\\
0&0&18
\end{array}
\right]
$$
Como es imposible transformar la entrada de la diagonal principal en uno, el proceso termina con un nuevo sistema de ecuaciones
$$\begin{cases}x-5y=3\\
0=18\end{cases}$$

como la segunda ecuación representa algo falso, lo que se puede decir del sistema es que no tiene soluciones.

2. Emplee Gauss-Jordan para solucionar el sistema de ecuaciones
$$\begin{cases} 3x+6y-9z=18\\
-6x+8y-10z=4\\
-2x+6y-8z=8\end{cases}$$

$$\left[
\begin{array}{ccc|c}
3&6&-9&18\\
-6&8&-10&4\\
-2&6&-8&8
\end{array}
\right] \xrightarrow{f_1=\frac{1}{3}f_1}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
-6&8&-10&4\\
-2&6&-8&8
\end{array}
\right] \xrightarrow{f_2=f_2+6f_1}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&20&-28&40\\
-2&6&-8&8
\end{array}
\right]\xrightarrow{f_3=f_3+2f_1}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&20&-28&40\\
0&10&-14&20
\end{array}
\right] \xrightarrow{f_2=\frac{1}{20}f_2}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&1&-\frac{7}{5}&2\\
0&10&-14&20
\end{array}
\right] \xrightarrow{f_3=f_3-10f_2}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&1&-\frac{7}{5}&2\\
0&0&0&0
\end{array}
\right]
$$
Como los elementos de la última fila son todos nulos, podemos afirmar que el sistema tiene infinitas soluciones.

```python
135/5
```

### Ahora con Python

El método que utilizamos para encontrar la solución de un sistema de ecuaciones lineales fue el método de eliminación de Gauss Jordan, si bien con la tabla definida en clase es muy sencillo definir un algoritmo para dicho proceso, es útil conocer funciones que nos permitan hacer ese cálculo.

Para esta sección usaremos otro paquete bien conocido en Python, el paquete **sympy**, iniciamos importandolo:

```python
!pip install sympy
```

    Collecting sympy
      Using cached sympy-1.7.1-py3-none-any.whl (5.9 MB)
    Collecting mpmath>=0.19
      Using cached mpmath-1.2.1-py3-none-any.whl (532 kB)
    Installing collected packages: mpmath, sympy
    Successfully installed mpmath-1.2.1 sympy-1.7.1

```python
from sympy import *
init_printing()
```

La forma de importar este paquete nos permite usar todas las funciones de **sympy** sin necesidad del prefijo. *init_printing()* es una función que nos facilitara la lectura de las matrices que usamos.

el paquete sympy es un paquete de matemática simbólica, es decir, buscará ser lo más fiel al lenguaje usado en clase.

Iniciamos definiendo una matriz con la función *Matrix*.

```python
Matrix([[1, -1], [3, 4], [0, 2]])
```

$\displaystyle \left[\begin{matrix}1 & -1\\3 & 4\\0 & 2\end{matrix}\right]$

Con este paque se pueden redefinir las operaciones descritas en la sección anterior con numpy, tenemos por ejemplo:

```python
A=Matrix([[1,2,3],[0,1,2],[-1,1,3]])
B=Matrix([[1,-1,1],[0,1,1],[-1,10,31]])
A , B
```

$\displaystyle \left( \left[\begin{matrix}1 & 2 & 3\\0 & 1 & 2\\-1 & 1 & 3\end{matrix}\right], \  \left[\begin{matrix}1 & -1 & 1\\0 & 1 & 1\\-1 & 10 & 31\end{matrix}\right]\right)$

Suma

```python
A+B
```

$\displaystyle \left[\begin{matrix}2 & 1 & 4\\0 & 2 & 3\\-2 & 11 & 34\end{matrix}\right]$

Resta

```python
A-B
```

$\displaystyle \left[\begin{matrix}0 & 3 & 2\\0 & 0 & 1\\0 & -9 & -28\end{matrix}\right]$

Sumando como en el tablero, usando simbolos matriciales:

```python
3*A
```

$\displaystyle \left[\begin{matrix}3 & 6 & 9\\0 & 3 & 6\\-3 & 3 & 9\end{matrix}\right]$

```python
s=MatrixSymbol("+",3,3)
e=MatrixSymbol("=",3,3)
(A*s*B)*e*(A+B)
```

$\displaystyle \left[\begin{matrix}1 & 2 & 3\\0 & 1 & 2\\-1 & 1 & 3\end{matrix}\right] + \left[\begin{matrix}1 & -1 & 1\\0 & 1 & 1\\-1 & 10 & 31\end{matrix}\right] = \left[\begin{matrix}2 & 1 & 4\\0 & 2 & 3\\-2 & 11 & 34\end{matrix}\right]$

Producto

```python
A*B #En sympy ESTE SÍ es el producto de matrices
```

$\displaystyle \left[\begin{matrix}-2 & 31 & 96\\-2 & 21 & 63\\-4 & 32 & 93\end{matrix}\right]$

Transpuesta

```python
Transpose(A)##Escrita
```

$\displaystyle \left(\left[\begin{matrix}1 & 2 & 3\\0 & 1 & 2\\-1 & 1 & 3\end{matrix}\right]\right)^{T}$

```python
A.T ##Calculada
```

$\displaystyle \left[\begin{matrix}1 & 0 & -1\\2 & 1 & 1\\3 & 2 & 3\end{matrix}\right]$

Inversa

```python
B**(-1)
```

$\displaystyle \left[\begin{matrix}\frac{21}{23} & \frac{41}{23} & - \frac{2}{23}\\- \frac{1}{23} & \frac{32}{23} & - \frac{1}{23}\\\frac{1}{23} & - \frac{9}{23} & \frac{1}{23}\end{matrix}\right]$

## Método de Gauss Jordan

Reducción de matrices:

Vamos a resolver el siguiente sistema:

$$ \begin{cases}
x&&+z&=2\\
2x&-y&+3z&=3\\
4x&+3y&+2z&=12
\end{cases}
$$

Su matriz aumentada es:

```python
A=Matrix([[1,0,1,2],[2,-1,3,3],[4,3,2,12]])
A
```

$\displaystyle \left[\begin{matrix}1 & 0 & 1 & 2\\2 & -1 & 3 & 3\\4 & 3 & 2 & 12\end{matrix}\right]$

Ahora la reducimos:

```python
A.rref()
```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & 0 & 1\\0 & 1 & 0 & 2\\0 & 0 & 1 & 1\end{matrix}\right], \  \left( 0, \  1, \  2\right)\right)$

No obstante, lo que queremos es hacer la reducción de Gauss Jordan para las matrices que pueden asociarse o bien a un problema de sistemas o a un problema de inversas. Para esto usamos la función *rref*:

Resolveremos el siguiente sistema:

$$ \begin{cases}x+y+z=1\\ 2x+2y+4z=0 \\ x+z=0 \end{cases}$$

Iniciamos definiendo la matriz aumentada del sistema:

```python
S=Matrix([[1,1,1,1],[2,2,4,0],[1,0,1,0]])
S
```

$\displaystyle \left[\begin{matrix}1 & 1 & 1 & 1\\2 & 2 & 4 & 0\\1 & 0 & 1 & 0\end{matrix}\right]$

Ahora lo reducimos:

```python
S.rref()
```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & 0 & 1\\0 & 1 & 0 & 1\\0 & 0 & 1 & -1\end{matrix}\right], \  \left( 0, \  1, \  2\right)\right)$

La primera parte de la respuesta nos entrega la forma escalonada reducida de la matriz y la segunda indica el indice de los pivotes que se encontraron en dicha forma. De aquí es fácil notar que la solución del sistema es:

$$ \begin{cases} x=1\\ y=1 \\z=-1 \end{cases} $$

Otro ejemplo, para el sistema:

$$\begin{cases}x+z=3\\ 2x+3y+4z=7\\ -x-3y-3z=-4 \end{cases}$$

```python
S2=Matrix([[1, 0, 1, 3], [2, 3, 4, 7], [-1, -3, -3, -4]])
S2
```

$\displaystyle \left[\begin{matrix}1 & 0 & 1 & 3\\2 & 3 & 4 & 7\\-1 & -3 & -3 & -4\end{matrix}\right]$

```python
S2.rref()
```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & 1 & 3\\0 & 1 & \frac{2}{3} & \frac{1}{3}\\0 & 0 & 0 & 0\end{matrix}\right], \  \left( 0, \  1\right)\right)$

Escribiendo la reducción como el sistema:

$$
\begin{cases}
x+z=3\\
y+\frac{2}{3}z=\frac{1}{3}\\
0=0
\end{cases}
$$

La solución aquí es infinita, tenemos:

$$\begin{cases} x=3-t\\ y=\frac{1}{3}-\frac{2}{3}t \\ z=t \end{cases} $$

para todo $ t\in \mathbb{R}$.

```python
S3=Matrix([[1, 0, 1, 5], [2, 3, 4, 7], [-1, -3, -3, -4]])
S3
```

$\displaystyle \left[\begin{matrix}1 & 0 & 1 & 5\\2 & 3 & 4 & 7\\-1 & -3 & -3 & -4\end{matrix}\right]$

```python
S3.rref()
```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & 1 & 0\\0 & 1 & \frac{2}{3} & 0\\0 & 0 & 0 & 1\end{matrix}\right], \  \left( 0, \  1, \  3\right)\right)$

Si se convierte en ecuaciones al final tendré:

$0x+0y+0z=1$

Lo cual es absurdo, luego ese sistema no tiene solución.

**Otra forma de ver un sistema**

Sea el sistema:
$$\begin{cases}x+y-z=1\\x+2y-2z=1\\x+5y-z=5\end{cases}$$

La matriz aumentada es:

$\begin{pmatrix}1&1&-1&1\\1&2&-2&1\\1&5&-1&5\end{pmatrix}$

Expresión usando operaciones matriciales para el sistema:

$\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}\cdot \begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix} x+y-z\\x+2y-2z \\x+5y-z \end{pmatrix}=\begin{pmatrix}1\\1\\5\end{pmatrix}$

$\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}\cdot \begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1\\1\\5\end{pmatrix}$

$\begin{pmatrix}1&0&0\\0&1&0\\0&0&1\end{pmatrix}\cdot \begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1\\1\\5\end{pmatrix}$

$\begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1\\1\\5\end{pmatrix}$

```python
from sympy import *
A=Matrix([[1,1,-1],[1,2,-2],[1,5,-1]])
B=Matrix([[1],[1],[5]])
B
```

```python
Sol=A.inv()*B
Sol
```

```python
A1=Matrix([[1,2,3],[1,0,1],[0,2,2]])
A1
```

```python
A1.inv()
```

```python
A.det()
```

```python
A1.det()
```

## Determinantes

Muchas veces necesitamos conocer de antemano si el problema al que nos enfrentamos tiene una solución única, para ello basta realizar un cálculo sencillo sobre el cual hablaremos enseguida.

**¿Qué es un determinante?**

El determinante es un número real asociado a cada matriz cuadrada, el cual nos permitirá, primero que nada, saber la cantidad de soluciones que tiene un sistema y en segundo lugar el valor de la solución cuando ésta sea única. El determinante de una matriz puede ser cualquier número real.

Es conveniente entender primero como calcular determinantes de matrices $2\times 2$ para luego calcular los deteminantes de mayor orden.

**Notación:**
Para indicar que se va a calcular el determinante de la matriz $A$ es común encontrar las notaciones $det(A)$ o $|A|$.

**Ejemplo:**
Halle el determinante de la matriz $\begin{pmatrix}1&3\\ \:4&2\end{pmatrix}$.

Podemos escribir $det\begin{pmatrix}1&3\\ \:4&2\end{pmatrix}=\begin{vmatrix}1&3\\ \:4&2\end{vmatrix}$

### ¿Cómo se calcula?
En realidad es una cuenta bastante simple, es el producto de la diagonal menos el producto de la antidiagonal, es decir
$$\begin{vmatrix}a_{11}&a_{12}\\ \:a_{21}&a_{22}\end{vmatrix}=a_{11}a_{22}-a_{12}a_{21}$$

**Ejemplos**
1. $$\begin{vmatrix}1&3\\ \:4&2\end{vmatrix}=(1)(2)-(3)(4)=-10$$
2. $$\begin{vmatrix}-4&3\\ \:-1&-6\end{vmatrix}=(-4)(-6)-(-1)(3)=27$$
3. $$\begin{vmatrix}2&4\\ \:-4&-8\end{vmatrix}=(2)(-8)-(4)(-4)=0$$

```python
M1=Matrix([[1,3],[4,2]])
M1.det()
```

**Para una matriz de 3x3 el determinante se calcula usando Sarrus**

Sea $A=\begin{pmatrix}a_{11}&a_{12}&a_{13}\\
a_{21}&a_{22}&a_{23}\\
a_{31}&a_{32}&a_{33}\\
\end{pmatrix}$
Utiliza el siguiente arreglo auxiliar:

$\begin{bmatrix}{\color{green} a_{11}}&a_{12}&a_{13}&a_{11}&a_{12}\\
a_{21}&\color{green}  a_{22}&a_{23}&a_{21}&a_{22}\\
a_{31}&a_{32}& \color{green} a_{33}&a_{31}&a_{32}\\
\end{bmatrix} = \tiny  a_{11}a_{22}a_{33}+a_{12}a_{23}a_{31}+a_{13}a_{21}a_{32}-a_{13}a_{22}a_{31}-a_{11}a_{23}a_{32}-a_{12}a_{21}a_{33}$

### ¿Qué pasa si la matriz es de mayor orden?
Primero que nada se deben definir los **cofactores:** el cofactor asociado al elemento $a_{ij}$ de una matriz de tamaño $n\times n$ se define como
$$C_{ij}=(-1)^{i+j}M_{ij}$$
Siendo $M_{ij}$ la matriz obtenida al eliminar la fila $i$ y la columna $j$ de la matriz original.

**Ejemplo:**
Hallar el cofactor $C_{23}$ correspondiente a la matriz  $A=\begin{pmatrix}1&2&0\\ \:-1&1&1\\ \:2&-1&0\end{pmatrix}$.

Al eliminar la segunda fila y la tercera columna se obtiene la matriz

por lo cual
$$C_{23}=(-1)^{2+3}\begin{vmatrix}1&2\\ \:2&-1\end{vmatrix}=(-1)(-5)=5$$

#### Algoritmo
La idea será la siguiente:
1. Elegir una fila (o una columna) .
2. Hallar los cofactores correspondientes a cada uno de los elementos de la fila (o columna).
3. Multiplicar los valores de las entradas por sus respectivos cofactores .
4. Realizar la suma de los resultados obtenidos en el paso 3.

Vamos a hallar el determinante de la matriz anterior utilizando la primera fila, como también la tercera columna, esto con el fin de mostrar que no importa cual se elija, el resultado será el mismo y también para que siempre se elija la fila o columna con mayor cantidad de entradas nulas.

**Determinante seleccionando la primera fila:**

Notemos que como la entrada $A_{23}$ es cero, el sumando asociado a ésta no aporta nada para calcular el determinante.

**Determinante seleccionando la tercera columna:**

Siguiendo la observación anterior en la tercera columna se tienen 2 entradas cuyo valor es cero, por lo que su aporte al determinante es nulo, basta calcular únicamente el cofactor $C_{23}$ multiplicarlo por su respectiva entrada y obtendríamos el valor del determinante así

### Ejercicios:
1. Calcule:
    1. $\begin{vmatrix}0&1\\ \:5&2\end{vmatrix}$
    2. $\begin{vmatrix}4&4\\ \:-2&-2\end{vmatrix}$
    3. $\begin{vmatrix}-4&3\\ \:0&1\end{vmatrix}$
    4. $\begin{vmatrix}0&0&0\\ \:4&2&3\\ \:-1&3&9\end{vmatrix}$
    5. $\begin{vmatrix}0&0&0\\ \:4&2&3\\ \:-1&3&9\end{vmatrix}$
    6. Haga una función en Python que le permita calcular el cofactor indicado por el usuario en una matriz arbitraria.

### ¿Cómo lo hago en Python?
En realidad es bastante simple, ya que la librería *Sympy* tiene una función que premite hallar el determinante directamente.

`matriz.det()`

```python
from sympy import *
```

```python
init_printing(use_unicode=True)
```

```python
M=Matrix([[1,2,0], [1,1,1], [2,-1,0]])
M
```

```python
M.col(2)
```

```python
M.row(1)
```

```python
M.det()
```

```python
Matrix([[-3,2,4], [2,0,3], [0.5,1,2]]).det()
```

#### Ejercicio
1. Utilice la función `.det()` para corroborar los resultados obtenidos anteriormente.

### ¿Cómo puedo aplicar los determinantes?

Como vimos anteriormente los sistemas de ecuaciones se pueden representar de manera matricial de la siguiente manera:
$$Ax=b$$
Si el determinante de $A$ fuese nulo, el sistema puede tener infinitas o ninguna solución, mientras que si es diferente de cero tendrá una única solución.

#### Ejemplo:
¿El siguiente sistema tiene una única solución?
$$\begin{cases} 3x+y=0\\ 2x-6y=5\end{cases}$$

Para responder a la pregunta formulada basta hallar el determinante de la matriz de coeficientes del sistema
$$\begin{vmatrix}3&1\\ \:2&-6\end{vmatrix}=-20\neq0$$
por lo tanto el sistema posee una única solución.

### ¿Se puede hallar la solución del sistema utilizando determinantes?
A éste proceso se le conoce como la Regla de Cramer y consiste en lo siguiente:
1. Se debe calcular el determinante de la matriz de coeficientes.
2. Se remplaza la columna correspondiente a la primera variable por los elementos que conforman al vector $b$, obteniendo una nueva matriz.
3. El proceso anterior se repite con todas las variables que tenga el sistema, es decir, se obtendrán tantas matrices como variables tiene el sistema.
4. Se calculan los determinantes de las matrices anteriores.
5. El valor de cada variable será el obtenido al dividir el determinante de la matriz en la cual se remplazó en vector $b$ por tal variable, entre el determinante de la matriz $A$.

#### Ejemplo:
Para tener una guía de lo que se está realizando es sano escribir como sub-índice la variable que se está remplazando por el vector $b$, por ejemplo
$$|A_x|=\begin{vmatrix}0&1\\ \:5&-6\end{vmatrix}=-5.$$
$$|A_y|=\begin{vmatrix}3&0\\ \:2&5\end{vmatrix}=15.$$
Por lo tanto
$$x=\frac{|A_x|}{|A|}=\frac{-5}{-20}=\frac{1}{4}.$$
$$y=\frac{|A_y|}{|A|}=\frac{15}{-20}=-\frac{3}{4}.$$

#### Problema
Un carpintero fabrica sillas, mesas y repisas.Se necesitan 15 minutos para lijar una silla, 5 para pintarla y 10 para barnizarla. Se necesitan 30 minutos para lijar una mesa, 12 para pintarla y 20 para barnizarla. Se necesitan 20 minutos para lijar una repisa, 10 para pintarla y 15 para barnizarla. La mesa de lijado está disponible 235  horas al mes, la mesa de pintura 95 horas al mes y la mesa de barnizado 160 horas. ¿Cuántas unidades de cada mueble deben fabricarse por mes de modo que las mesas de trabajo se ocupen todo el tiempo disponible?

#### Solución
Dado que las unidades de fabricación vienen dada en minutos y la catidad de tiempo total viene dada en horas, procederemos a convertir las horas en minutos, así

$$\text{Conversión de horas a minutos, debemos multiplicar por 60 }\ \ \begin{cases} 235h\rightarrow14100m\\95h\rightarrow5700m\\160h\rightarrow9600m\end{cases}$$

Por lo tanto el sistema se puede expresar como
$$\begin{cases}
15s+30m+20r=14100\\
5s+12m+10r=5700\\
10s+20m+15r=9600
\end{cases}$$

```python
A=Matrix([[15,30,20],[5,12,10],[10,20,15]])
A
```

```python
A.det()
```

En vista que el determinante de la matriz de coeficientes es diferente de cero, sabemos que el sistema tendrá una única solución, aplicando la regla de Cramer tenemos lo siguiente

```python
As=Matrix([[14100,30,20],[5700,12,10],[9600,20,15]])
As
```

```python
Am=Matrix([[15,14100,20],[5,5700,10],[10,9600,15]])
Am
```

```python
Ar=Matrix([[15,30,14100],[5,12,5700],[10,20,9600]])
Ar
```

Por lo tanto la solución del sistema de ecuaciones viene dada por:

```python
s=As.det()/A.det()
s
```

```python
m=Am.det()/A.det()
m
```

```python
r=Ar.det()/A.det()
r
```

Se deben fabricar mensualmete 180 sillas, 300 mesas y 120 repisas.

```python
A=Matrix([[3,1,0],[2,-6,5]])
```

```python
A
```

```python
A_p=Matrix([[3,1],[2,-6]])
A_p
```

```python
A_x=Matrix([[0,1],[5,-6]])
A_x
```

```python
A_y=Matrix([[3,0],[2,5]])
A_y
```

```python
x=A_x.det()/A_p.det()
x
```

```python
y=A_y.det()/A_p.det()
y
```

$$\begin{cases} 3x+y=0\\ 2x-6y=5\end{cases}$$

```python
3*(1/4)+(-3/4)
```

```python
2*(1/4)-6*(-3/4)
```

```python

```


# 2021-09-17 Sparsity, solvers, and nonlinear intro

## Last time

* Advection and boundary layers
* The (cell) Péclet number and oscillations

## Today
* Effective use of sparse/structured matrices
* Intro to rootfinding and Newton's method

```julia
using Plots
using LinearAlgebra

function vander(x, k=nothing)
    if k === nothing
        k = length(x)
    end
    V = ones(length(x), k)
    for j = 2:k
        V[:, j] = V[:, j-1] .* x
    end
    V
end

function fdstencil(source, target, k)
    "kth derivative stencil from source to target"
    x = source .- target
    V = vander(x)
    rhs = zero(x)'
    rhs[k+1] = factorial(k)
    rhs / V
end
```

    fdstencil (generic function with 1 method)

# Advection-diffusion with conservative FD

$$(- \kappa u_x + wu)_x = f$$

```julia
function advdiff_conservative(n, kappa, wind, forcing)
    x = LinRange(-1, 1, n)
    xstag = (x[1:end-1] + x[2:end]) / 2
    L = zeros(n, n)
    rhs = forcing.(x)
    kappa_stag = kappa.(xstag)
    for i in 2:n-1
        flux_L = -kappa_stag[i-1] * fdstencil(x[i-1:i], xstag[i-1], 1) + wind * [.5 .5]
        flux_R = -kappa_stag[i] * fdstencil(x[i:i+1], xstag[i], 1) + wind * [.5 .5]
        weights = fdstencil(xstag[i-1:i], x[i], 1)
        L[i, i-1:i+1] = weights[1] * [flux_L..., 0] + weights[2] * [0, flux_R...]
    end
    L[1, 1] = 1
    rhs[1] = 0
    L[2:end, 1] .= 0
    L[end,end] = 1
    rhs[end] = 0
    L[1:end-1,end] .= 0
    x, L, rhs
end
```

    advdiff_conservative (generic function with 1 method)

# Experiments, The [Péclet number](https://en.wikipedia.org/wiki/P%C3%A9clet_number)

\begin{align}
(- \kappa u_x + wu)_x &= f & \mathrm{Pe}_h &= \frac{h \lvert w \rvert}{\kappa}
\end{align}

```julia
n = 40; h = 2/n
kappa = 1
wind = 100
x, L, rhs = advdiff_conservative(n, x -> kappa, wind, one)
@show minimum(diff(x))
plot(x, L \ rhs, marker=:auto, legend=:none, title="Pe_h $(wind*h/kappa)")
```

    minimum(diff(x)) = 0.0512820512820511

# Upwinded discretization

Idea: incoming **advective flux** should depend only on upwind value, outgoing should depend only on my value.

```julia
function advdiff_upwind(n, kappa, wind, forcing)
    x = LinRange(-1, 1, n)
    xstag = (x[1:end-1] + x[2:end]) / 2
    L = zeros(n, n)
    rhs = forcing.(x)
    kappa_stag = kappa.(xstag)
    for i in 2:n-1
        flux_L = -kappa_stag[i-1] * fdstencil(x[i-1:i], xstag[i-1], 1) +
            wind * (wind > 0 ? [.75 .25] : [0 1])  # <--- was [.5 .5]
        flux_R = -kappa_stag[i] * fdstencil(x[i:i+1], xstag[i], 1) +
            wind * (wind > 0 ? [.75 .25] : [0 1])  # <--- was [.5 .5]
        weights = fdstencil(xstag[i-1:i], x[i], 1)
        L[i, i-1:i+1] = weights[1] * [flux_L..., 0] + weights[2] * [0, flux_R...]
    end
    L[1, 1] = 1
    rhs[1] = 0
    L[2:end, 1] .= 0
    L[end,end] = 1
    rhs[end] = 0
    L[1:end-1,end] .= 0
    x, L, rhs
end
```

    advdiff_upwind (generic function with 1 method)

# Try it for robustness

```julia
n = 40; h = 2/n
kappa = 1
wind = 1000
x, L, rhs = advdiff_upwind(n, x -> kappa, wind, one)
@show minimum(diff(x))
plot(x, L \ rhs, marker=:auto, legend=:none, title="Pe_h $(wind*h/kappa)")
```

    minimum(diff(x)) = 0.0512820512820511

### What is the order of accuracy of this upwind discretization?

# How expensive is `\`?

```julia

x, L, rhs = advdiff_upwind(5000, x -> kappa, wind, one)
@timev L \ rhs;
```

      1.670357 seconds (6 allocations: 190.792 MiB)
    elapsed time (ns): 1670356501
    bytes allocated:   200060272
    pool allocs:       3
    malloc() calls:    3

```julia
function elapsed_solve(n)
    x, L, rhs = advdiff_upwind(n, x -> kappa, wind, one)
    @elapsed L \ rhs
end
ns = (1:6) * 1000
times = elapsed_solve.(ns)
```

    6-element Vector{Float64}:
     0.012454447
     0.083467243
     0.334782038
     0.534024244
     1.20161831
     1.406723185

```julia
scatter(ns, times, xscale=:log10, yscale=:log10)
plot!(ns, [(ns/3000).^2, (ns/3000).^3], xrange=(1e3, 1e4))
```

## How long would $n=10000$ take to solve?

## How much memory would $n=10000$ take?

# Sparsity

```julia
x, L, rhs = advdiff_upwind(20, one, 1, one)
spy(L, marker=(:square, 10), c=:bwr)
```

```julia
using SparseArrays
sparse(L)
```

    20×20 SparseMatrixCSC{Float64, Int64} with 54 stored entries:
    ⠱⣦⡀
    ⠀⠈⠻⣦⡀
    ⠀⠀⠀⠈⠻⣦⡀
    ⠀⠀⠀⠀⠀⠈⠻⣦⡀
    ⠀⠀⠀⠀⠀⠀⠀⠈⠻⢆

# How does solving with the sparse matrix scale?

```julia
function elapsed_solve(n)
    x, L, rhs = advdiff_upwind(n, x -> kappa, wind, one)
    L = sparse(L)
    @elapsed L \ x
end
ns = (1:10) * 1000
times = elapsed_solve.(ns)
scatter(ns, times, xscale=:log10, yscale=:log10)
plot!([n -> 1e-2*n/1000, n -> 1e-2*(n/1000)^2, n -> 1e-2*(n/1000)^3])
```

## How much memory is needed?

# [SparseArrays](https://docs.julialang.org/en/v1/stdlib/SparseArrays/) Compressed Sparse Column (CSC)

```julia
# starting with a dense matrix
A = sparse([1 0 0 -2; 0 3.14 0 .4; 5.1 0 0 .06])
```

    3×4 SparseMatrixCSC{Float64, Int64} with 6 stored entries:
     1.0   ⋅     ⋅   -2.0
      ⋅   3.14   ⋅    0.4
     5.1   ⋅     ⋅    0.06

```julia
@show A.colptr
@show A.rowval
A.nzval
```

    A.colptr = [1, 3, 4, 4, 7]
    A.rowval = [1, 3, 2, 1, 2, 3]

    6-element Vector{Float64}:
      1.0
      5.1
      3.14
     -2.0
      0.4
      0.06

* CSC (or, more commonly outside Matlab/Julia, row-based CSR) are good for matrix operations (multiplication, solve), but sometimes inconvenient to assemble.

# Matrix assembly using COO format

```julia
A
```

    3×4 SparseMatrixCSC{Float64, Int64} with 6 stored entries:
     1.0   ⋅     ⋅   -2.0
      ⋅   3.14   ⋅    0.4
     5.1   ⋅     ⋅    0.06

```julia
sparse([1, 1, 2, 2, 3, 3, 1],
       [1, 4, 2, 4, 1, 4, 1],
       [1, -2, 3.14, .4, 5.1, .06, 10])
```

    3×4 SparseMatrixCSC{Float64, Int64} with 6 stored entries:
     11.0   ⋅     ⋅   -2.0
       ⋅   3.14   ⋅    0.4
      5.1   ⋅     ⋅    0.06

# Upwind advection-diffusion solver using COO

```julia
function advdiff_sparse(n, kappa, wind, forcing)
    x = LinRange(-1, 1, n)
    xstag = (x[1:end-1] + x[2:end]) / 2
    rhs = forcing.(x)
    kappa_stag = kappa.(xstag)
    rows = [1, n]
    cols = [1, n]
    vals = [1., 1.] # diagonals entries (float)
    rhs[[1,n]] .= 0 # boundary condition
    for i in 2:n-1
        flux_L = -kappa_stag[i-1] * fdstencil(x[i-1:i], xstag[i-1], 1) +
            wind * (wind > 0 ? [1 0] : [0 1])
        flux_R = -kappa_stag[i] * fdstencil(x[i:i+1], xstag[i], 1) +
            wind * (wind > 0 ? [1 0] : [0 1])
        weights = fdstencil(xstag[i-1:i], x[i], 1)
        append!(rows, [i,i,i])
        append!(cols, i-1:i+1)
        append!(vals, weights[1] *  [flux_L..., 0] + weights[2] * [0, flux_R...])
    end
    L = sparse(rows, cols, vals)
    x, L, rhs
end
```

    advdiff_sparse (generic function with 1 method)

# Assembly cost

```julia
n = 400; h = 2/n
kappa = 1
wind = 100
x, L, rhs = advdiff_sparse(n, x -> kappa, wind, one)
@show minimum(diff(x))
plot(x, L \ rhs, legend=:none, title="Pe_h $(wind*h/kappa)")
```

    minimum(diff(x)) = 0.005012531328320691

```julia
n = 10000
@time advdiff_upwind(n, one, 1, one);
@time advdiff_sparse(n, one, 1, one);
```

      0.543603 seconds (708.34 k allocations: 803.497 MiB, 19.04% gc time)
      0.195921 seconds (698.92 k allocations: 44.198 MiB, 35.39% gc time)

```julia
A = spzeros(5, 5)
A[1,1] = 3
A
```

    5×5 SparseMatrixCSC{Float64, Int64} with 1 stored entry:
     3.0   ⋅    ⋅    ⋅    ⋅
      ⋅    ⋅    ⋅    ⋅    ⋅
      ⋅    ⋅    ⋅    ⋅    ⋅
      ⋅    ⋅    ⋅    ⋅    ⋅
      ⋅    ⋅    ⋅    ⋅    ⋅


---
title: "Prediction through Simulation"
summary: "Or, if you want to understand and predict something, make a toy model of it"
date: 2020-05-12
slug: "predict-simulate"
source: jupyter
---

> Click the button below to run an interactive version of this post in Google Colab.
> There are some nice interactive widgets here that run on Colab, but have been replaced by GIFs
> on this blog. If you're not playing with the widgets yourself, you're missing out.

[](https://colab.research.google.com/drive/1t4zWK_xfoNRxMtzS3b5qQ-8bavBGAnkT?usp=sharing)

Not too long ago, I interviewed for a job that I  was quite keen on getting.
It's now been $d$ days, and I haven't heard back.
Are they going to phone me?
Most people in this situation would try not to worry about it too much.
Unlike most people, however, I know *Monte Carlo methods*!
In this post,
I use a *Bayesian Monte Carlo* approach
to estimate the probability that they're going to phone and offer me a job.
Here goes.

## Monte Carlo Methods

In Monte Carlo methods, we use simulations to estimate quantities that are difficult to otherwise calculate.
I've written a little more about Monte Carlo methods [here](eointravers.com/post/montecarlo/).
In this case, we're going to simulate how companies hire people after an interview.
To do this, we're going to assume a particular model of how this works,
and we're going to make some assumptions about the parameters of this model.
For example, we're going to make assumptions about the likelihood that other candidates better than me,
the number of days it takes for a company to make a decision,
and the probability of someone turning down the job if offered it.
We want to combine this information with our single data point -
how many days have passed without me hearing from them -
and use this combined knowledge to make predictions about the world.

In Bayesian terms, we can say we're assuming
a particular model $\mathcal{M}$,
with prior estimates $P(\theta)$ for our model parameters $\theta$.
to make predictions about some other quantity,
$P(y | \theta, x)$.
If this isn't clear, don't stress,
as we'll be working through a concrete example.

## How do companies hire?

Let's imagine a simple model of how hiring works.
We'll assume

- Of the interview candidates, $n$ were better than me.
  If $n=0$, I'm the first choice. $n=1$ means I'm the second preference, and so on.
  We don't know the value of $n$, but we can make some weak assumptions.
  Since this is a Bayesian analysis, we can use a prior distribution to encode
  what values of $n$ we think are likely.
  For now, we'll call this prior distribution $P(n)$.
  We can flesh out these distributions later.
- It takes a company $\tau$ days to make a decision.
  We'll call our prior distribution for this $P(\tau)$.
- If someone who isn't me is offered the job,
  there probability that they'll accept is $\gamma$.
  We'll assume a single value of $\gamma$,
  but explore the effects of varying this  value.
- It takes someone $\omega$ days to decide
  whether or not to accept the job and to let the company know.
  Again, we'll assume a prior distribution $P(\omega)$.
- Finally, there is some probability, $\kappa$,
  that the company just never phones me back.
  I originally left this part of the model out,
  but without it the model produces some weird predictions
  after long intervals.

What do our prior distributions look like?
In this problem, the **[geometric distribution](https://en.wikipedia.org/wiki/Geometric_distribution)** is a nice option for all three of our priors:
$P(n)$, $P(\tau)$, and $P(\omega)$.
The easiest way to think of the geometric distribution
is that it describes the number of coin tosses
you would expect to come up tails before one comes up heads.
It has only one parameter, which we'll call $\lambda$,
which is the probability of each individual toss coming up heads.
With different values of $\lambda$, it looks like this:

```python
# Import stuff
import numpy as np
from scipy import stats
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import rcParams
import seaborn as sns
# Set up plots defaults
rcParams['figure.figsize'] = (6, 4)
rcParams['font.size'] = 18
rcParams['axes.spines.top'] = False
rcParams['axes.spines.right'] = False
```

```python
n_flips = np.arange(6)
lambdas = [.1, .5, .9]
for l in lambdas:
    # Calculate mass for n_flips + 1 to get
    # number of tails before first heads
    # See https://en.wikipedia.org/wiki/Geometric_distribution
    d = stats.geom.pmf(n_flips + 1, p=l)
    plt.plot(n_flips, d, '-o',
             label='$\lambda = %.2f$' % l)
plt.legend()
plt.xlabel('Number of flips before HEADS')
plt.ylabel('Probability')
plt.ylim(0, 1)
plt.title('The Geometric distribution');
```

Our prior distributions are

$$
\begin{align}
n      &\sim \text{Geom}(\lambda_n)\newline
\tau   &\sim \text{Geom}(\lambda_{\tau})\newline
\omega &\sim \text{Geom}(\lambda_{\omega})\newline
\end{align}
$$

$\lambda_{\tau}$ and $\lambda_{\omega}$ have simple interpretations
as the probability that the company will reach a decision ($\tau$)
and the probability that a candidate will decide whether to take the job ($\omega$)
with each day that does by.
$\lambda_n$ isn't as easy to interpret,
and we might wish to consider other parameterisations,
but this works well enough for a half-serious blog post.
Lower values of $\lambda_n$ reflect higher probabilities
than I'm the best candidate. With higher values, it becomes
increasingly likely that there will be several candidates better than me.

For our other two parameters, $\gamma$ and $\kappa$,
we're going to just assume a single value as our prior.
Recall that $\gamma$ is the probability that someone will take the job if offered it, while $\kappa$ is the probability that the company forgets to contact me if I don't get the job.

All of this leaves us with just four prior parameters we need to set:
$\theta = [\lambda_n, \lambda_{\tau}, \lambda_{\omega}, \gamma, \kappa]$.

> I'm referring to these quantitites as *priors*:
> Infomation we know in advance, before seeing the current data.
> In Bayesian inference, we often want to update our prior beliefs in light of new evidence,
> for example by expecting decisions to be made quickly
> at future interviews if a decision is made quickly here.
> In this case, we only want to make a prediction about the current scenario.

This model specifies what data we'll see under different circumstances.

- If I'm the best candidate ($n = 0$),
  I'll be offered the job after $\tau$ days,
  where $\tau \sim P(\tau)$
  (the decision time is drawn randomly from the prior distribution of likely decision times).
- If $n$ candidates are better than me,
  and the $m$th candidate accepts the job,
  I'll be told the bad news after $\tau + \omega_1 + \omega_2 + \dots + \omega_m$ days,
  where $\omega \sim P(\omega)$
  (the times taken for each candidate to make up their minds
  is drawn randomly from the prior distribution of likely times).
- If $n$ candidates are better than me,
  but they all turn down the job,
  I'll be offered the job after $\tau + \omega_1 + \omega_2 + \dots + \omega_n$ days,
  where $\omega \sim P(\omega)$.
- In simulations where I'm not offered a job, $\kappa$ is the probability
  that I'll never hear from them (which the model captures as an infinite waiting time).

## Simulating the world

To begin, let's write a function that runs a single simulation.
This code will be slow, but relatively easy to understand.

```python
def do_simulation(theta: list):
    '''Run one simulation of the hiring process.
    `theta` is a list/array of [lambda_n, lambda_tau, lambda_omega, gamma, kappa]
    Returns (n, tau, days_to_call, job_for_me)
    '''
    lambda_n, lambda_tau, lambda_omega, gamma, kappa = theta
    # Sample one value of n and tau from their priors
    n = stats.geom(lambda_n).rvs() - 1      # Candidates ahead of me
    tau = stats.geom(lambda_tau).rvs() - 1  # Days to make initial decision
    job_for_me = True     # Set to False if someone else gets it first
    total_omega = 0
    for m in range(n):
        omega = stats.geom(lambda_omega).rvs() - 1  # Time taken for them to respond
        total_omega += omega
        they_take_job = stats.binom(p=gamma, n=1).rvs()
        if they_take_job:
            job_for_me = False
            if stats.binom(p=kappa, n=1).rvs():
                # They never call
                total_omega = np.inf
            break
    days_to_call = tau + total_omega
    return n, tau, total_omega, days_to_call, job_for_me
```

```python
# Let's run it a few times to make sure it works.
theta = [.5, .5, .5, .5, .1]
for i in range(5):
    print(do_simulation(theta))
```

This is all very good, but our code is slow.
Let's try to run 1000 simulations.

```python
def do_slow_simulations(theta: list, nsims=1000):
    sims = [do_simulation(theta)
            for i in range(nsims)]
    return pd.DataFrame(sims, columns=['n', 'tau', 'total_omega', 'days_to_call', 'job_for_me'])

%time slow_sims = do_slow_simulations(theta, nsims=1000)
```

Since we'll want to run lots of simulations with different parameters,
it's worth rewriting our code in a way that's a little harder to follow,
but much faster to run.
Since we've written the clean, slow code,
we can always compare the results to make sure we've not doing anything stupid in our faster version.

```python
def do_simulations(theta: list, nsims=1000):
    '''Simulation hiring process `nsims` times.
    `theta` is a list/array of [lambda_n, lambda_tau, lambda_omega, gamma]
    Returns pd.DataFrame with columns ['job_for_me', 'days_before_call']
    '''
    lambda_n, lambda_tau, lambda_omega, gamma, kappa = theta
    # Days to make initial decision.
    tau = stats.geom(lambda_tau).rvs(nsims) - 1
    # Candidates ahead of me.
    n = stats.geom(lambda_n).rvs(nsims) - 1
    # N candidates who turn down the job before one accepts it.
    m = stats.geom(gamma).rvs(nsims) - 1
    n_called_before_me = np.where(n < m, n, m)
    # I get the job if everyone ahead of me turns it down.
    job_for_me = np.where(m >= n, 1, 0)
    # Vector of response times for every candidate
    _omega = stats.geom(lambda_omega).rvs(m.sum() * nsims) - 1
    # Total response times for all candidates ahead of me in each simulation
    total_omega = np.array([np.sum(_omega[i:i+_n])
                            for i, _n in enumerate(n_called_before_me)])
    # Some chance they'll never call
    never_call = stats.binom(p=kappa, n=1).rvs(nsims)
    # But only in cases where I don't get the job
    never_call = np.where(job_for_me, 0, never_call)
    days_to_call = np.where(never_call==0,
                            # Decision time plus response time of people ahead of me
                            tau + total_omega,
                            np.inf)
    result = pd.DataFrame([n, tau, total_omega, days_to_call, job_for_me],
                          index=['n', 'tau', 'total_omega', 'days_to_call', 'job_for_me']).T
    return result
```

```python
%time sims = do_simulations(theta, nsims=1000)
sims.head(5)
```

```python
# Let's take a look at our simulations
def plot_sims_hist(sims: pd.DataFrame):
    '''Plot simulated outcomes as a histogram'''
    days = [df['days_to_call'].values for days, df in sims.groupby('job_for_me')]
    fig, axes = plt.subplots(1, 2, figsize=(8, 4), sharey=True,
                             gridspec_kw={'width_ratios': [5, 1]})
    plt.sca(axes[0])
    bins = np.arange(0, 15, 1)
    plt.hist(days, label=["Not hired", "I'm Hired"], bins=bins)
    plt.legend(title='Outcome')
    plt.xlabel('Days since interview')
    plt.ylabel('Number of simulations')
    plt.sca(axes[1])
    plt.bar(0, np.sum(days[0]==np.inf), width=.25)
    plt.xlim(-1, 1)
    axes[1].spines['left'].set_visible(False)
    plt.xticks([0], ['Never call'])
    return fig, axes

plot_sims_hist(sims);
```

# Have I got the job?

With these simulation parameters, the estimated probability that I will be offered the job eventually
is just the proportion of simulations in which I'm offered the job.

```python
sims['job_for_me'].mean()
```

However, $d$ days have already passed without me hearing back.
What is the probability I'll be offered the job now?
To find out, we just have to check on what proportion of simulations
*where I haven't heard back by day $d$*
do I go on to be offered the job.

```python
d = 7  # Let's say I haven't heard back in 7 days.
sims[sims['days_to_call'] > d]['job_for_me'].mean()
```

We can repeat this over a range of days to see how the probability
changes with each day that goes by.

```python
def plot_curve(sims):
    '''Plot prob. I've got the job after d days'''
    days = np.arange(0, 15)
    conditional_outcomes = [sims[sims['days_to_call'] > d]['job_for_me'] for d in days]
    prob = np.array([x.mean() for x in conditional_outcomes])
    plt.plot(days, prob, '-o')
    plt.xlabel('Days without hearing back')
    plt.ylabel("P(I'll be hired)")
    plt.ylim(0, 1)
    plt.xticks(days[::2]);

plot_curve(sims)
```

The curve is a little noisy due to the random nature of the simulations,
but clearly shows that the chance of being hired drops over time.
We can see how noisy the results are by repeating the simulations a few times.
**This isn't the same thing as "sampling from the posterior" in Bayesian stats.
This variability only occurs because we're running a limited number of simulations.
With an infinite number of simulations, we would get the same curve every time.**

```python
def plot_repeated_curve(theta, nsims=1000, nreps=25):
    '''Plot repeated simulation curves.
    Note that input is parameter list theta,
    and not simulation results as in the other plotting functions.'''
    days = np.arange(0, 15)
    all_probs = []
    for i in range(nreps):
        sims = do_simulations(theta, nsims=nsims)
        conditional_outcomes = [sims[sims['days_to_call'] > d]['job_for_me'] for d in days]
        prob = np.array([x.mean() for x in conditional_outcomes])
        all_probs.append(prob)
        plt.plot(days, prob, alpha=.5, color='b')
    mean_prob = np.array(all_probs).mean(0)
    plt.plot(days, mean_prob, color='r', linewidth=3)
    plt.xlabel('Days without hearing back')
    plt.ylabel("P(I'll be hired)")
    plt.ylim(0, 1)
    plt.xticks(days[::2])

plot_repeated_curve(theta, nreps=25)
```

# Different Priors

Of course, these predictions depend on the prior parameters we enter.
Our next step is to see how our predictions change as we change these parameters.
We could do this manually, by changing the code above and rerunning it with various parameter settings.
Happily, there's an easier way, using the `ipywidgets` package.

`ipywidgets` let's us quickly create interactive widgets within Jupyter notebooks.
In this case, we're going to create widgets the plot the results of our simulations,
with sliders to control the value of each parameter.
If you're running this code on your own computer or on **Binder**,
you can try this for yourself.
If you're just reading the blog post,
you'll have to settle for the gifs I recorded if the widgets in action.

First, let's create the histogram of simulations with different parameters.

```python
from ipywidgets import interact, FloatSlider
def fs(value, min, max, step, description=None):
    '''Shorthand for ipywidgets.FloatSlider'''
    return FloatSlider(value=value, min=min,
                       max=max, step=step,
                       description=description,
                       continuous_update=False,
                       style={'description_width': '250px'},
                       layout={'width': '500px'})

def make_interactive_widget(func):
    '''Wrapper to ipywidgets.interact
    Arguments to `func` must be:
        lambda_n, lambda_tau, lambda_omega, gamma, kappa'''
    interact(func,
             lambda_n = fs(.5, .05, .95, .05, '$\lambda_n$ (My ability)'),
             lambda_tau = fs(.5, .05, .95, .05, '$\lambda_{\\tau}$ (Speed of decision)'),
             lambda_omega = fs(.5, .05, .95, .05, '$\lambda_{\omega}$(Speed of responses)'),
             gamma = fs(.5, .05, .95, .05, '$\gamma$ (P(Others accept))'),
             kappa = fs(.05, .05, .95, .05, '$\kappa$ (P(Never call))'));

def make_title(lambda_n, lambda_tau, lambda_omega, gamma, kappa):
    return ('$\lambda_n = %.2f, \lambda_{\\tau} = %.2f, \lambda_{\\omega} = %.2f, \gamma = %.2f, \kappa = %.2f$' %
            (lambda_n, lambda_tau, lambda_omega, gamma, kappa))

def interactive_hist(lambda_n, lambda_tau, lambda_omega, gamma, kappa):
    '''Create a histogram of simulations with these parameters'''
    theta = [lambda_n, lambda_tau, lambda_omega, gamma, kappa]
    sims = do_simulations(theta, nsims=1000)
    plot_sims_hist(sims)
    plt.title(make_title(lambda_n, lambda_tau, lambda_omega, gamma, kappa), fontdict={'size':14})
```

```python
make_interactive_widget(interactive_hist)
```

Then we can do the same for the probability curve.

```python
def interactive_curve(lambda_n, lambda_tau, lambda_omega, gamma, kappa):
    theta = [lambda_n, lambda_tau, lambda_omega, gamma, kappa]
    sims = do_simulations(theta, nsims=2000)
    plot_curve(sims)
    plt.title(make_title(lambda_n, lambda_tau, lambda_omega, gamma, kappa), fontdict={'size':14})
```

```python
make_interactive_widget(interactive_curve)
```

Finally, we can do the same with the repeated simulation curve.
This takes longer to run, so is less snappy when used interactively.

```python
def interactive_repeated_curve(lambda_n, lambda_tau,
                               lambda_omega, gamma, kappa):
    plot_repeated_curve([lambda_n, lambda_tau,
                         lambda_omega, gamma, kappa], nreps=25, nsims=500)
    plt.title(make_title(lambda_n, lambda_tau, lambda_omega, gamma, kappa), fontdict={'size':14})

make_interactive_widget(interactive_repeated_curve)
```

```python

```

```python

```