(EIGVALEIGVEC)=

# 2.2 Eigenvalores y eigenvectores

```{admonition} Notas para contenedor de docker:

Comando de docker para ejecución de la nota de forma local:

nota: cambiar `<ruta a mi directorio>` por la ruta de directorio que se desea mapear a `/datos` dentro del contenedor de docker.

`docker run --rm -v <ruta a mi directorio>:/datos --name jupyterlab_optimizacion -p 8888:8888 -d palmoreck/jupyterlab_optimizacion:2.1.4`

password para jupyterlab: `qwerty`

Detener el contenedor de docker:

`docker stop jupyterlab_optimizacion`

Documentación de la imagen de docker `palmoreck/jupyterlab_optimizacion:2.1.4` en [liga](https://github.com/palmoreck/dockerfiles/tree/master/jupyterlab/optimizacion).

```

---

Nota generada a partir de [liga](https://www.dropbox.com/s/s4ch0ww1687pl76/3.2.2.Factorizaciones_matriciales_SVD_Cholesky_QR.pdf?dl=0).

```{admonition} Al final de esta nota el y la lectora:
:class: tip

* Aprenderá las definiciones más relevantes en el tema de eigenvalores y eigenvectores para su uso en el desarrollo de algoritmos en el análisis numérico en la resolución de problemas del álgebra lineal numérica. En específico las definiciones de: diagonalizable o *non defective* y similitud son muy importantes.

* Comprenderá el significado geométrico de calcular los eigenvalores y eigenvectores de una matriz simétrica para una forma cuadrática que define a una elipse.

* Aprenderá cuáles problemas en el cálculo de eigenvalores y eigenvectores de una matriz son bien y mal condicionados.

```

En esta nota **asumimos** que $A \in \mathbb{R}^{n \times n}$.

## Eigenvalor (valor propio o característico)

```{admonition} Definición

El número $\lambda$ (real o complejo) se denomina *eigenvalor* de A si existe $v \in \mathbb{C}^n - \{0\}$ tal que $Av = \lambda v$. El vector $v$ se nombra eigenvector (vector propio o característico) de $A$ correspondiente al eigenvalor $\lambda$.
```

```{admonition} Observación
:class: tip

Observa que si $Av=\lambda v$ y $v \in \mathbb{C}^n-\{0\}$ entonces la matriz $A-\lambda I_n$ es singular por lo que su determinante es cero.

```

```{admonition} Comentarios

* Una matriz con componentes reales puede tener eigenvalores y eigenvectores con valores en $\mathbb{C}$ o $\mathbb{C}^n$ respectivamente.
* El conjunto de eigenvalores se le nombra **espectro de una matriz** y se denota como:

$$\lambda(A) = \{ \lambda | \det(A-\lambda I_n) = 0\}.$$

* El polinomio

$$p(z) = \det(A-zI_n) = (-1)^nz^n + a_{n-1}z^{n-1}+ \dots + a_1z + a_0$$

se le nombra **polinomio característico asociado a $A$** y sus raíces o ceros son los eigenvalores de $A$.

* La multiplicación de $A$ por un eigenvector es un reescalamiento y posible cambio de dirección del eigenvector.
* Si consideramos que nuestros espacios vectoriales se definen sobre $\mathbb{C}$ entonces siempre podemos asegurar que $A$ tiene un eigenvalor con eigenvector asociado. En este caso $A$ tiene $n$ eigenvalores y pueden o no repetirse.

* Se puede probar que el determinante de $A$: $\det(A) = \displaystyle \prod_{i=1}^n \lambda_i$ y la traza de $A$: $tr(A) = \displaystyle \sum_{i=1}^n \lambda_i$.
```

### Ejemplo

```python
import numpy as np
```

```python
np.set_printoptions(precision=3, suppress=True)
```

```python
A=np.array([[10,-18],[6,-11]])
```

```python
print(A)
```

    array([[ 10, -18],
           [  6, -11]])

**En *NumPy* con el módulo [numpy.linalg.eig](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.eig.html) podemos obtener eigenvalores y eigenvectores**

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [ 1. -2.]
    eigenvectores:
    [[0.894 0.832]
     [0.447 0.555]]

```{margin}

$Av_1 = \lambda_1 v_1$.
```

```python
print('matriz * eigenvector:')
print(A@evector[:,0])
print('eigenvalor * eigenvector:')
print(evalue[0]*evector[:,0])
```

    matriz * eigenvector:
    [0.894 0.447]
    eigenvalor * eigenvector:
    [0.894 0.447]

```{margin}

$Av_2 = \lambda_2 v_2$.
```

```python
print('matriz * eigenvector:')
print(A@evector[:,1])
print('eigenvalor * eigenvector:')
print(evalue[1]*evector[:,1])
```

    matriz * eigenvector:
    [-1.664 -1.109]
    eigenvalor * eigenvector:
    [-1.664 -1.109]

### Ejemplo

Si $v$ es un eigenvector entonces $cv$ es eigenvector donde: $c$ es una constante distinta de cero.

```python
const = -2
const_evector = const*evector[:,0]
print(const_evector)
```

    [-1.789 -0.894]

```{margin}

$cv$ es un eigenvector con eigenvalor asociado $c\lambda$ pues $A(cv) = \lambda(cv)$ se satisface si $Av = \lambda v$ y $c \neq 0$.

```

```python
print('matriz * (constante * eigenvector):')
print(A@const_evector)
print('eigenvalor * (constante * eigenvector):')
print(evalue[0]*const_evector)
```

    matriz * (constante * eigenvector):
    [-1.789 -0.894]
    eigenvalor * (constante * eigenvector):
    [-1.789 -0.894]

### Ejemplo

Una matriz con entradas reales puede tener eigenvalores y eigenvectores complejos:

```python
A=np.array([[3,-5],[1,-1]])
```

```python
print(A)
```

    [[ 3 -5]
     [ 1 -1]]

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Para $A \in \mathbb{R}^{n \times n}$ se tiene: $\lambda \in \mathbb{C}$ si y sólo si $\bar{\lambda} \in \mathbb{C}$ con $\bar{\lambda}$ el conjugado de $\lambda$.

```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [1.+1.j 1.-1.j]
    eigenvectores:
    [[0.913+0.j    0.913-0.j   ]
     [0.365-0.183j 0.365+0.183j]]

### Ejemplo

Los eigenvalores de una matriz diagonal son iguales a su diagonal y sus eigenvectores son los vectores canónicos $e_1, e_2, \dots e_n$.

```python
A = np.diag([2, 2, 2, 2])
```

```python
print(A)
```

    [[2 0 0 0]
     [0 2 0 0]
     [0 0 2 0]
     [0 0 0 2]]

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [2. 2. 2. 2.]
    eigenvectores:
    [[1. 0. 0. 0.]
     [0. 1. 0. 0.]
     [0. 0. 1. 0.]
     [0. 0. 0. 1.]]

```{admonition} Definición

La **multiplicidad algebraica** de un eigenvalor es su multiplicidad considerado como raíz/cero del polinomio característico $p(z)$. Si no se repite entonces tal eigenvalor se le nombra de multiplicidad **simple**.

La **multiplicidad geométrica** de un eigenvalor es el número máximo de eigenvectores linealmente independientes asociados a éste.

```

```{admonition} Observación
:class: tip

En el ejemplo anterior cada eigenvalor tiene una multiplicidad simple y la multiplicidad geométrica de cada eigenvalor es $1$.

```

### Ejemplo

Los eigenvalores de una matriz triangular son iguales a su diagonal.

```python
A=np.array([[10,0, -1],
            [6,10, 10],
            [3, 4, 11.0]])
A = np.triu(A)
```

```python
print(A)
```

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

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Observa que el eigenvalor igual a $10$ está repetido dos veces (multiplicidad algebraica igual a $2$) y se tienen dos eigenvectores linealmente independientes asociados a éste (multiplicidad geométrica igual a $2$).
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [10. 10. 11.]
    eigenvectores:
    [[ 1.     0.    -0.099]
     [ 0.     1.     0.99 ]
     [ 0.     0.     0.099]]

**Otro ejemplo:**

```python
A=np.array([[10,18, -1],
            [6,10, 10],
            [3, 4, 11.0]])
A = np.triu(A)
```

```python
print(A)
```

    array([[10., 18., -1.],
           [ 0., 10., 10.],
           [ 0.,  0., 11.]])

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Observa que en este ejemplo el eigenvalor $10$ está repetido dos veces (multiplicidad algebraica es igual a $2$) y sus eigenvectores asociados son linealmente dependientes (multiplicidad geométrica es igual a $1$).
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [10. 10. 11.]
    eigenvectores:
    [[ 1.    -1.     0.998]
     [ 0.     0.     0.056]
     [ 0.     0.     0.006]]

### Ejemplo

Un eigenvalor puede estar repetido y tener un sólo eigenvector linealmente independiente:

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

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

Observa que en este ejemplo el eigenvalor $2$ está repetido tres veces (multiplicidad algebraica es igual a $3$) y sus eigenvectores asociados son linealmente dependientes (multiplicidad geométrica es igual a $1$).
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [2. 2. 2.]
    eigenvectores:
    [[ 1. -1.  1.]
     [ 0.  0. -0.]
     [ 0.  0.  0.]]

```{admonition} Definición

Si $(\lambda, v)$ es una pareja de eigenvalor-eigenvector de $A$ tales que $Av = \lambda v$ entonces $v$ se le nombra eigenvector derecho. Si $(\lambda, v)$ es una pareja de eigenvalor-eigenvector de $A^T$ tales que $A^Tv = \lambda v$ (que es equivalente a $v^TA=\lambda v^T$) entonces $v$ se le nombra eigenvector izquierdo.
```

```{admonition} Observaciones
:class: tip

* En todos los ejemplos anteriores se calcularon eigenvectores derechos.

* Los eigenvectores izquierdos y derechos para una matriz simétrica son iguales.

```

(DIAGONALIZABLE)=

## $A$ diagonalizable

```{admonition} Definición

Si $A$ tiene $n$ eigenvectores linealmente independientes entonces $A$ se nombra diagonalizable o *non defective*. En este caso si $x_1, x_2, \dots, x_n$ son eigenvectores de $A$ con $Ax_i = \lambda_i x_i$ para $i=1,\dots,n$ entonces la igualdad anterior se escribe en ecuación matricial como:

$$AX = X \Lambda$$

o bien:

$$A = X \Lambda X^{-1}$$

donde: $X$ tiene por columnas los eigenvectores de $A$ y $\Lambda$ tiene en su diagonal los eigenvalores de $A$.

A la descomposición anterior $A = X \Lambda X^{-1}$ para $A$ diagonalizable o *non defective* se le nombra ***eigen decomposition***.
```

```{admonition} Observación
:class: tip

* Si $A = X \Lambda X^{-1}$ entonces $X^{-1}A = \Lambda X^{-1}$ y los renglones de $X^{-1}$ (o equivalentemente las columnas de $X^{-T}$) son eigenvectores izquierdos.

* Si $A = X \Lambda X^{-1}$ y $b = Ax = (X \Lambda X^{-1}) x$ entonces:

$$\tilde{b} = X^{-1}b = X^{-1}  (Ax) = X^{-1} (X \Lambda X^{-1}) x = \Lambda X^{-1}x = \Lambda \tilde{x}.$$

Lo anterior indica que el producto matricial $Ax$ para $A$ diagonalizable es equivalente a multiplicar una matriz diagonal por un vector denotado como $\tilde{x}$ que contiene los coeficientes de la combinación lineal de las columnas de $X$ para el vector $x$ . El resultado de tal multiplicación es un vector denotado como $\tilde{b}$ que también contiene los coeficientes de la combinación lineal de las columnas de $X$ para el vector $b$. En resúmen, si $A$ es diagonalizable o *non defective* la multiplicación $Ax$ es equivalente a la multiplicación por una matriz diagonal $\Lambda \tilde{x}$ (salvo un cambio de bases, ver [Change of basis](https://en.wikipedia.org/wiki/Change_of_basis)).

* Si una matriz $A$ tiene eigenvalores distintos entonces es diagonalizable y más general: si $A$ tiene una multiplicidad geométrica igual a su multiplicidad algebraica de cada eigenvalor entonces es diagonalizable.

```

### Ejemplo

La matriz:

$$A = \left[
\begin{array}{ccc}
1 & -4 & -4\\
8 & -11 & -8\\
-8 & 8 & 5
\end{array}
\right]
$$

es diagonalizable.

```python
A = np.array([[1, -4, -4],
              [8, -11, -8],
              [-8, 8, 5.0]])
```

```python
print(A)
```

    array([[  1.,  -4.,  -4.],
           [  8., -11.,  -8.],
           [ -8.,   8.,   5.]])

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
```

    eigenvalores:
    [ 1. -3. -3.]

```{margin}

Se verifica que los eigenvectores de este ejemplo es un conjunto linealmente independiente por lo que $A=X\Lambda X^{-1}$.

```

```python
print('eigenvectores:')
print(evector)
```

    eigenvectores:
    [[ 0.333 -0.717 -0.241]
     [ 0.667 -0.02  -0.796]
     [-0.667 -0.697  0.555]]

```python
X = evector
Lambda = np.diag(evalue)
```

```{margin}

Observa que si $Z$ es desconocida y $X^T Z^T = \Lambda$ entonces $Z^T = X^{-T} \Lambda$ y por tanto $XZ =X\Lambda X^{-1}$.
```

```python
X@np.linalg.solve(X.T, Lambda).T
```

    array([[  1.,  -4.,  -4.],
           [  8., -11.,  -8.],
           [ -8.,   8.,   5.]])

```python
print(A)
```

    array([[  1.,  -4.,  -4.],
           [  8., -11.,  -8.],
           [ -8.,   8.,   5.]])

$A$ es diagonalizable pues: $X^{-1} A X = \Lambda$

```{margin}

Observa que si $Z$ es desconocida y $XZ = A$ entonces $Z = X^{-1}A$ y por tanto $ZX = X^{-1} A X$.
```

```python
print(np.linalg.solve(X, A)@X)
```

    [[ 1.  0. -0.]
     [-0. -3. -0.]
     [ 0. -0. -3.]]

```python
print(Lambda)
```

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

```{admonition} Observación
:class: tip

Observa que **no necesariamente** $X$ en la *eigen decomposition* es una matriz ortogonal.

```

```{margin}

Aquí se toma $X[1:3,1]$ como la primera columna de $X$ y se satisface $X[1:3,1]^TX[1:3,1] = 1$ en este ejemplo pero en general esto no se cumple.

```

```python
X[:,0].dot(X[:,0])
```

    1.0

```{margin}

$X[1:3,1]^TX[1:3,2] \neq 0$ por lo que la primera y segunda columna de $X$ no son ortogonales.

```

```python
X[:,0].dot(X[:,1])
```

    0.2117366284008178

**Eigenvectores derechos:**

```{margin}

`x_1` es la primer columna de $X$: $X[1:3, 1]$ y `lambda_1` el eigenvalor asociado.
```

```python
x_1 = X[:,0]
lambda_1 = Lambda[0,0]
```

```python
print(A@x_1)
```

    array([ 0.333,  0.667, -0.667])

```{margin}

$Ax_1 = \lambda_1 x_1$.
```

```python
print(lambda_1*x_1)
```

    array([ 0.333,  0.667, -0.667])

```{margin}

`x_2` es la segunda columna de $X$: $X[1:3, 2]$ y `lambda_2` el eigenvalor asociado.
```

```python
x_2 = X[:,1]
lambda_2 = Lambda[1,1]
```

```python
print(A@x_2)
```

    array([2.151, 0.061, 2.09 ])

```{margin}

$Ax_2 = \lambda_2 x_2$.
```

```python
print(lambda_2*x_2)
```

    array([2.151, 0.061, 2.09 ])

**Eigenvectores izquierdos:**

```{admonition} Observación
:class: tip

Para los eigenvectores izquierdos se deben tomar los renglones de $X^{-1}$ (o equivalentemente las columnas de $X^{-T}$) sin embargo no se utiliza el método [inv](https://numpy.org/doc/stable/reference/generated/numpy.linalg.inv.html) de *NumPy* pues es más costoso computacionalmente y amplifica los errores por redondeo. En su lugar se utiliza el método [solve](https://numpy.org/doc/stable/reference/generated/numpy.linalg.solve.html) y se resuelve el sistema: $X^{-T} z  = e_i$ para $e_i$ $i$-ésimo vector canónico.

```

```python
e1 = np.zeros((X.shape[0],1))
```

```python
e1[0] = 1
```

```python
print(e1)
```

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

```{margin}

`x_inv_1` es el primer renglón de $X^{-1}$: $X^{-1}[1, 1:3]$.
```

```python
x_inv_1 = np.linalg.solve(X.T, e1)
```

```python
print(x_inv_1)
```

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

```python
print(A.T@x_inv_1)
```

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

```{margin}

$A^TX^{-T}[1:3,1] = \lambda_1 X^{-T}[1:3,1]$, `lambda_1` el eigenvalor asociado a `x_inv_1`.
```

```python
print(lambda_1*x_inv_1)
```

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

```python
e2 = np.zeros((X.shape[0],1))
```

```python
e2[1] = 1
```

```{margin}

`x_inv_2` es el segundo renglón de $X^{-1}$: $X^{-1}[2, 1:3]$.
```

```python
x_inv_2 = np.linalg.solve(X.T, e2)
```

```python
print(x_inv_2)
```

    array([[-0.851],
           [-0.13 ],
           [-0.556]])

```python
print(A.T@x_inv_2)
```

    array([[2.552],
           [0.391],
           [1.667]])

```{margin}

$A^TX^{-T}[1:3,2] = \lambda_2 X^{-T}[1:3,2]$, `lambda_2` el eigenvalor asociado a `x_inv_2`.
```

```python
print(lambda_2*x_inv_2)
```

    array([[2.552],
           [0.391],
           [1.667]])

```{admonition} Ejercicio
:class: tip

¿Es la siguiente matriz diagonalizable?

$$A = \left [
\begin{array}{ccc}
-1 & -1 & -2\\
8 & -11 & -8\\
-10 & 11 & 7
\end{array}
\right]
$$

si es así encuentra su *eigen decomposition* y diagonaliza a $A$.
```

(DESCESP)=

### Resultado: $A$ simétrica

Si A es simétrica entonces tiene eigenvalores reales. Aún más: $A$ tiene eigenvectores reales linealmente independientes, forman un conjunto ortonormal y se escribe como un producto de tres matrices nombrado **descomposición espectral o *symmetric eigen decomposition***:

$$A = Q \Lambda Q^T$$

donde: $Q$ es una matriz ortogonal cuyas columnas son eigenvectores de $A$ y $\Lambda$ es una matriz diagonal con eigenvalores de $A$.

```{admonition} Comentarios

* Por lo anterior una matriz simétrica es **ortogonalmente diagonalizable**, ver {ref}`A diagonalizable <DIAGONALIZABLE>`.

* Los eigenvalores de $A$ simétrica se pueden ordenar:

$$\lambda_n(A) \leq \lambda_{n-1}(A) \leq \dots \leq \lambda_1(A)$$

con:

$\lambda_{max}(A) = \lambda_1(A)$, $\lambda_{min}(A) = \lambda_n(A)$.

* Se prueba para $A$ simétrica:

$$\lambda_{max}(A) = \displaystyle \max_{x \neq 0} \frac{x^TAx}{x^Tx}$$

$$\lambda_{min}(A) = \displaystyle \min_{x \neq 0} \frac{x^TAx}{x^Tx}.$$

por lo tanto:

$$\lambda_{min}(A) \leq \frac{x^TAx}{x^Tx} \leq \lambda_{max}(A) \forall x \neq 0.$$

* $||A||_2 = \displaystyle \max\{|\lambda_1(A)|, |\lambda_n(A)|\}$.

* $||A||_F = \left( \displaystyle \sum_{i=1}^n \lambda_i ^2 \right)^{1/2}$.

* Los valores singulares de $A$ son el conjunto $\{|\lambda_1(A)|, \dots, |\lambda_{n-1}(A)|, |\lambda_n(A)|\}$.
```

### Ejemplo

Matriz simétrica y descomposición espectral de la misma:

```python
A=np.array([[5,4,2],[4,5,2],[2,2,2]])
```

```python
print(A)
```

    [[5 4 2]
     [4 5 2]
     [2 2 2]]

```python
evalue, evector = np.linalg.eigh(A)
```

```{margin}

Como $A$ es simétrica sus eigenvalores son reales y sus eigenvectores forman un conjunto linealmente independiente. Por lo anterior $A$ tiene descomposción espectral.
```

```python
print('eigenvalores:')
print(evalue)
print('eigenvectores:')
print(evector)
```

    eigenvalores:
    [ 1.  1. 10.]
    eigenvectores:
    [[ 0.482  0.569  0.667]
     [-0.727 -0.166  0.667]
     [ 0.49  -0.806  0.333]]

```{margin}

$A = Q \Lambda Q^T$
```

```python
print('descomposición espectral:')
Lambda = np.diag(evalue)
Q = evector
print('QLambdaQ^T:')
print(Q@Lambda@Q.T)
print('A:')
print(A)
```

    descomposición espectral:
    QLambdaQ^T:
    [[5. 4. 2.]
     [4. 5. 2.]
     [2. 2. 2.]]
    A:
    [[5 4 2]
     [4 5 2]
     [2 2 2]]

A es diagonalizable pues: $Q^T A Q = \Lambda$

```python
print(Q.T@A@Q)
```

    [[ 1.  0.  0.]
     [-0.  1. -0.]
     [ 0.  0. 10.]]

```python
print(Lambda)
```

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

Ver [numpy.linalg.eigh](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.linalg.eigh.html).

## Condición del problema del cálculo de eigenvalores y eigenvectores

La condición del problema del cálculo de eigenvalores y eigenvectores de una matriz, es la sensibilidad de los mismos ante perturbaciones en la matriz, ver {ref}`Condición de un problema y estabilidad de un algoritmo <CPEA>`. Diferentes eigenvalores o eigenvectores de una matriz no necesariamente son igualmente sensibles a perturbaciones en la matriz.

```{admonition} Observación
:class: tip

La condición del problema del cálculo de eigenvalores y eigenvectores de una matriz **no** es igual a la condición del problema de resolver un sistema de ecuaciones lineales, ver {ref}`Número de condición de una matriz <NCM>`.

```

Se prueba que la condición de un eigenvalor **simple** de una matriz $A$ está dado por $\frac{1}{|y^Tx|}$ con $x$ eigenvector derecho, $y$ eigenvector izquierdo de $A$ ambos asociados al eigenvalor simple y normalizados esto es: $x^Tx = y^Ty=1$.

```{admonition} Comentarios

* Para los casos en que: $\lambda$ eigenvalor de $A$ sea simple, $A$ sea diagonalizable, existen eigenvectores izquierdos y derechos asociados a un eigenvalor de $A$ tales que $y^Tx \neq 0$. En tales casos, el análisis del condicionamiento del problema del cálculo de eigenvalores y eigenvectores es más sencillo de realizar que para matrices no diagonalizables o eigenvalores con multiplicidad algebraica mayor a $1$. En particular, los eigenvalores de una matriz simétrica están muy bien condicionados: las perturbaciones en $A$ únicamente perturban a los eigenvalores en una magnitud medida con la norma de las perturbaciones y no depende de otros factores, por ejemplo del número de condición de $A$.

* La sensibilidad de un eigenvector depende de la sensibilidad de su eigenvalor asociado y de la distancia de tal eigenvalor de otros eigenvalores.

* Los eigenvalores que son "cercanos" o aquellos de multiplicidad mayor a $1$ pueden ser mal condicionados y por lo tanto difíciles de calcularse de forma exacta y precisa en especial si la matriz es defectuosa (no diagonalizable). Puede mejorarse el número de condición si se escala el problema por una matriz diagonal y similar a $A$, ver {ref}`similitud <SIMILITUD>`.
```

(SIMILITUD)=

## Similitud

```{admonition} Definición

Si existe $X \in \mathbb{R}^{n \times n}$ tal que $B = XAX^{-1}$ con $A, B \in \mathbb{R}^{n \times n}$ entonces $A$ y $B$ se nombran similares.

```

```{admonition} Observación
:class: tip

Las matrices que son similares tienen el mismo espectro, de hecho: $Ax = \lambda x$ si y sólo si $By = \lambda y$ para $y=Xx$. Lo anterior quiere decir que los eigenvalores de una matriz son **invariantes** ante cambios de bases o representación en coordenadas distintas.

```

### Ejemplo

Dada la matriz

$$A=
\left [
\begin{array}{cccc}
-1 & -1 & -1 & -1\\
0 & -5 & -16 & -22\\
0 & 3 & 10 & 14\\
4 & 8 & 12 & 14
\end{array}
\right ]
$$

Definir matrices $B_1, B_2$ similares a $A$ a partir de las matrices:

$$
\begin{array}{l}
X_1 =
\left [
\begin{array}{cccc}
2 & -1 & 0 & 0\\
-1 & 2 & -1 & 0\\
0 & -1 & 2 & -1\\
0 & 0 & -1 & 1
\end{array}
\right ],
X_2 = \left [
\begin{array}{cccc}
2 & -1 & 1 & 0\\
-1 & 2 & 0 & 0\\
0 & -1 & 0 & 0\\
0 & 0 & 0 & 1
\end{array}
\right ]
\end{array}
$$

y verificar que los eigenvalores de $A$ son los mismos que los de $B_1, B_2$, esto es, tienen el mismo espectro.

```python
A = np.array([[-1, -1 , -1, -1],
              [0, -5, -16, -22],
              [0, 3, 10, 14],
              [4, 8, 12, 14.0]])
```

```python
X1 = np.array([[2, -1, 0, 0],
              [-1, 2, -1, 0],
              [0, -1, 2, -1],
              [0, 0, -1, 1.0]])
```

$B_1 = X_1^{-1}AX_1$:

```{margin}

Calculamos $B1$ explícitamente para revisar qué forma tiene pero no es necesario.

```

```python
B1 = np.linalg.solve(X1, A)@X1

```

```python
print(B1)
```

    [[ 1.  2. -0.  0.]
     [ 3.  4. -0.  0.]
     [ 0.  0.  5.  6.]
     [ 0.  0.  7.  8.]]

```python
X2 = np.array([[2, -1, 1, 0],
               [-1, 2, 0, 0],
               [0, -1, 0, 0],
               [0, 0, 0, 1.0]])
```

$B_2 = X_2^{-1}AX_2$:

```{margin}

Calculamos $B2$ explícitamente para revisar qué forma tiene pero no es necesario.

```

```python
B2 = np.linalg.solve(X2, A)@X2

```

```python
print(B2)
```

    [[  1.   2.   0.  -6.]
     [  3.   4.   0. -14.]
     [ -0.   0.  -1.  -3.]
     [  0.   0.   4.  14.]]

**$B1$ y $B2$ son similares a $A$ por tanto tienen los mismos eigenvalores:**

```python
evalue, evector = np.linalg.eig(A)
```

```{margin}

`evalue` son los eigenvalores de $A$.

```

```python
print(evalue)
```

    [13.152  5.372 -0.152 -0.372]

```python
evalue_B1, evector_B1 = np.linalg.eig(B1)
```

```{margin}

`evalue_B1` son los eigenvalores de $B_1$, obsérvese que son iguales a los de $A$ salvo el orden.

```

```python
print(evalue_B1)
```

    [-0.372  5.372 -0.152 13.152]

```python
evalue_B2, evector_B2 = np.linalg.eig(B2)
```

```{margin}

`evalue_B2` son los eigenvalores de $B_2$, obsérvese que son iguales a los de $A$ salvo el orden.

```

```python
print(evalue_B2)
```

    [ 5.372 -0.372 -0.152 13.152]

Los eigenvectores **no son los mismos** pero pueden obtenerse vía multiplicación de matrices:

```{margin}

Elegimos un eigenvalor de $A$.

```

```python
print(evalue[1])
```

    5.372281323269013

```{margin}

Y elegimos el mismo eigenvalor en el *array* `evalue_B1` para $B_1$ que para este ejemplo corresponde al índice $1$ (el mismo que en `evalue` pero podría haber sido otro).

```

```python
print(evalue_B1[1])
```

    5.3722813232690125

```{margin}

Su correspondiente eigenvector en el índice $1$ del *array* `evector_B1`.

```

```python
print(evector_B1[:,1])
```

    array([-0.416, -0.909,  0.   ,  0.   ])

**$X^{-1}x$ es eigenvector de $B_1$ para $x$ eigenvector de $A$**:

```{margin}

`evector[:,1]` es el eigenvector de $A$ correspondiente al eigenvalor `evalue[1]`. En esta celda se hace el producto $X_1^{-1}x$ y `evector[:,1]` representa a $x$.

```

```python
X1_inv_evector = np.linalg.solve(X1, evector[:,1])
```

```python
print(X1_inv_evector)
```

    array([ 0.249,  0.543, -0.   , -0.   ])

```python
print(B1@(X1_inv_evector))
```

    array([ 1.335,  2.919, -0.   , -0.   ])

```{margin}

Se verifica que $B1(X_1^{-1}x) = \lambda (X_1^{-1}x)$ con $\lambda$ igual al valor `evalue_B1[1]`.

```

```python
print(evalue_B1[1]*(X1_inv_evector))
```

    array([ 1.335,  2.919, -0.   , -0.   ])

```{admonition} Observación
:class: tip

Obsérvese que son los mismos eigenvectores salvo una constante distinta de cero.

```

```python
print(evector_B1)
```

    [[-0.825 -0.416  0.     0.   ]
     [ 0.566 -0.909 -0.     0.   ]
     [ 0.     0.    -0.759 -0.593]
     [ 0.     0.     0.651 -0.805]]

```{margin}

El valor `1.33532534` es la primera entrada de `X1_inv_evector` que es $X_1^{-1}x$ y $x$ eigenvector de $A$. El valor `2.91920903` es la segunda entrada de `X_1_inv_evector`. Las entradas restantes son cercanas a cero.
```

```python
print(1.33532534e+00/evector_B1[0,1])
```

    -3.2101207266138467

```python
print(2.91920903e+00/evector_B1[1,1])
```

    -3.2101207350977647

La constante es aproximadamente $-3.21$:

```{margin}

`evector_B1` fue calculado con la función `eig` pero en la siguiente celda se observa que no es necesario si se tiene un eigenvector de $A$.
```

```python
print(evector_B1[:,1]*(-3.21))
```

    array([ 1.335,  2.919, -0.   , -0.   ])

```{margin}

Recuerda que `X_1_inv_evector` es $X_1^{-1}x$ con $x$ eigenvector de $A$ que en este caso se utilizó `evector[:,1]`.

```

```python
print(B1@(X1_inv_evector))
```

    array([ 1.335,  2.919, -0.   , -0.   ])

```{margin}

Se comprueba que $X_1^{-1}x$ es eigenvector de $B$ si $x$ es eigenvector de $A$.
```

```python
print(evalue_B1[1]*(X1_inv_evector))
```

    array([ 1.335,  2.919, -0.   , -0.   ])

Como $A$ tiene eigenvalores distintos entonces es diagonalizable, esto es existen $X_3, \Lambda$ tales que $X_3^{-1} A X_3 = \Lambda$.

```python
X_3 = evector
Lambda = np.diag(evalue)
```

```python
print(A)
```

    array([[ -1.,  -1.,  -1.,  -1.],
           [  0.,  -5., -16., -22.],
           [  0.,   3.,  10.,  14.],
           [  4.,   8.,  12.,  14.]])

```python
print(np.linalg.solve(X_3, A)@X_3)
```

    [[13.152 -0.     0.    -0.   ]
     [ 0.     5.372 -0.     0.   ]
     [-0.     0.    -0.152  0.   ]
     [-0.     0.    -0.    -0.372]]

```python
print(Lambda)
```

    array([[13.152,  0.   ,  0.   ,  0.   ],
           [ 0.   ,  5.372,  0.   ,  0.   ],
           [ 0.   ,  0.   , -0.152,  0.   ],
           [ 0.   ,  0.   ,  0.   , -0.372]])

```{admonition} Comentario

**$X_1$ diagonaliza a $A$ por bloques, $X_2$ triangulariza a $A$ por bloques y $X_3$ diagonaliza a $A$.** Las tres matrices representan al mismo operador lineal (que es una transformación lineal del espacio vectorial sobre sí mismo) pero en **coordenadas diferentes**. Un aspecto muy **importante** en el álgebra lineal es representar a tal operador lineal en unas coordenadas lo más simple posible. En el ejemplo la matriz $X_3$, que en sus columnas están los eigenvectores de $A$, ayuda a representarlo de forma muy simple.

```

```{admonition} Observación
:class: tip

$X_3$ es una matriz que diagonaliza a $A$ y tiene en sus columnas a eigenvectores de $A$, si el objetivo es diagonalizar a una matriz **no es necesario** resolver un problema de cálculo de eigenvalores-eigenvectores pues cualquier matriz $X$ no singular puede hacer el trabajo. Una opción es considerar una factorización para $A$ simétrica del tipo $LDL^T$ (que tiene un costo computacional bajo para calcularse), la matriz $L$ no es ortogonal y la matriz $D$ tiene los pivotes que se calculan en la eliminación Gaussiana, ver {ref}` Operaciones y transformaciones básicas del Álgebra Lineal Numérica <OTBALN>`.

```

```{admonition} Ejercicio
:class: tip

Considera

$$A=
\left [
\begin{array}{cccc}
-2 & -1 & -5 & 2\\
-9 & 0 & -8 & -2\\
2 & 3 & 11 & 5\\
3 & -5 & -13 & -7
\end{array}
\right ]
$$

Define $X_1$ tal que $X_1^{-1}AX_1$ sea diagonal.
```

### Ejemplo

```python
import sympy
import matplotlib.pyplot as plt
```

Considérese la siguiente ecuación cuadrática:

$$\frac{\tilde{x}^2}{16} + \frac{\tilde{y}^2}{9} = 1.$$

Con Geometría Analítica sabemos que tal ecuación representa una elipse inclinada. Además si

```python
D = sympy.Matrix([[sympy.Rational(1,16), 0],
                  [0, sympy.Rational(1,9)]])
```

```python
print(D)
```

$\displaystyle \left[\begin{matrix}\frac{1}{16} & 0\\0 & \frac{1}{9}\end{matrix}\right]$

Entonces el producto

$$\left [ \begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ] ^TD
\left [
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
$$

es:

```python
x_tilde, y_tilde = sympy.symbols("x_tilde, y_tilde")
x_y_tilde = sympy.Matrix([x_tilde, y_tilde])
```

```python
print((x_y_tilde.T*D*x_y_tilde)[0])
```

$\displaystyle \frac{x_{tilde}^{2}}{16} + \frac{y_{tilde}^{2}}{9}$

Rotemos al [eje mayor de la elipse](https://en.wikipedia.org/wiki/Semi-major_and_semi-minor_axes) un ángulo de $\theta = \frac{\pi}{3}$ con una {ref}`transformación de rotación <TROT>` que genera la ecuación matricial:

$$\begin{array}{l}
\left[
\begin{array}{c}
x\\
y
\end{array}
\right ]
=
\left [
\begin{array}{cc}
\cos(\theta) & -\sin(\theta)\\
\sin(\theta) & \cos(\theta)
\end{array}
\right ]
\left[
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
=
\left [
\begin{array}{cc}
\frac{1}{2} & -\frac{\sqrt{3}}{2}\\
\frac{\sqrt{3}}{2} & \frac{1}{2}
\end{array}
\right ]
\left[
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
=
Q\left[
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ]
\end{array}
$$

donde: $Q$ es la matriz de rotación en sentido contrario a las manecillas del reloj por el ángulo $\theta$.

Esto es:

$$
\begin{eqnarray}
x =\frac{\tilde{x}}{2} - \frac{\tilde{y}\sqrt{3}}{2} \nonumber \\
y =\frac{\tilde{x}\sqrt{3}}{2} + \frac{\tilde{y}}{2} \nonumber
\end{eqnarray}
$$

Despejando $\tilde{x},\tilde{y}$ y sustituyendo en $\frac{\tilde{x}^2}{16} + \frac{\tilde{y}^2}{9} = 1$ resulta en la ecuación:

```{margin}

Esta es una ecuación de una elipse inclinada.

```

```python
theta = sympy.pi/3
Q = sympy.Matrix([[sympy.cos(theta), -sympy.sin(theta)],
                  [sympy.sin(theta), sympy.cos(theta)]])
x,y = sympy.symbols("x, y")
x_tilde = (Q.T*sympy.Matrix([x,y]))[0]
y_tilde = (Q.T*sympy.Matrix([x,y]))[1]

print((x_tilde**2/16 + y_tilde**2/9).expand()*576)

```

$\displaystyle 57 x^{2} - 14 \sqrt{3} x y + 43 y^{2}$

O equivalentemente el producto

$$\left [ \begin{array}{c}
x\\
y
\end{array}
\right ]^T A
\left [
\begin{array}{c}
x\\
y
\end{array}
\right ]
$$

```python
x_y = sympy.Matrix([x,y])
A = Q*D*Q.T
print(((x_y.T*A*x_y)[0]).expand()*576)
```

$\displaystyle 57 x^{2} - 14 \sqrt{3} x y + 43 y^{2}$

con $A$ matriz dada por $A=QDQ^T$:

```{margin}

Observa que $A$ es **simétrica**.

```

```python
print(A)
```

$\displaystyle \left[\begin{matrix}\frac{19}{192} & - \frac{7 \sqrt{3}}{576}\\- \frac{7 \sqrt{3}}{576} & \frac{43}{576}\end{matrix}\right]$

En este ejemplo la matriz $Q$ de rotación es la matriz que diagonaliza ortogonalmente a $A$ pues: $Q^TAQ = D.$

Para realizar la **gráfica** de la elipse con *NumPy* observar que:

```{margin}

Estas ecuaciones nos indican que la misma elipse se puede representar en diferentes coordenadas. El cambio de coordenadas se realiza con la matriz $Q$.

```

$$
\begin{eqnarray}
1&=&57x^2 - 14 \sqrt{3}xy + 43 y^2 \nonumber \\
&=& \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]^T A
\left [
\begin{array}{c}
x\\
y
\end{array}
\right ] \nonumber \\
&=& \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]^T QDQ^T \left [
\begin{array}{c}
x\\
y
\end{array}
\right ] \nonumber \\
&=& \left(Q^T \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]\right)^TD\left(Q^T \left [ \begin{array}{c}
x\\
y
\end{array}
\right ]\right) \nonumber \\
&=& \left [ \begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ] ^TD
\left [
\begin{array}{c}
\tilde{x}\\
\tilde{y}
\end{array}
\right ] \nonumber \\
&=& \frac{\tilde{x}^2}{16} + \frac{\tilde{y}^2}{9} \nonumber
\end{eqnarray}
$$

**Gráfica para eigenvalores ordenados de forma decreciente en la diagonal de la matriz $D$.**

```{margin}

Usamos [eig](https://numpy.org/doc/stable/reference/generated/numpy.linalg.eig.html) para el cálculo numérico de eigenvalores, eigenvectores de $A$.

```

```python
A_np = np.array(A.evalf(),dtype = float)
evalue, evector = np.linalg.eig(A_np)
Q_np = evector
D_np = np.diag([evalue[0], evalue[1]])
```

```python
print(Q_np)
```

    array([[ 0.866,  0.5  ],
           [-0.5  ,  0.866]])

```python
Q = sympy.Matrix([Q[1,:],-Q[0,:]])
print(Q)
```

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

```python
print(D_np)
```

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

```python
print(sympy.Matrix([[D[1,1], 0],
              [0, D[0,0]]]))
```

$\displaystyle \left[\begin{matrix}\frac{1}{9} & 0\\0 & \frac{1}{16}\end{matrix}\right]$

```python
d1_inv=1/4
d2_inv=1/3
density=1e-2
x=np.arange(-1/d1_inv,1/d1_inv,density)
y1=1/d2_inv*np.sqrt(1-(d1_inv*x)**2)
y2=-1/d2_inv*np.sqrt(1-(d1_inv*x)**2)
#transform
x_y1_hat = np.column_stack((x,y1))
x_y2_hat = np.column_stack((x,y2))
apply_Q = lambda vec : np.transpose(Q@np.transpose(vec))
Q_to_vector_1 = apply_Q(x_y1_hat)
Q_to_vector_2 = apply_Q(x_y2_hat)
fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(1,2,1)
ax2 = fig.add_subplot(1,2,2)
#first plot
ax1.plot(Q_to_vector_1[:,0],Q_to_vector_1[:,1],'g',
         Q_to_vector_2[:,0],Q_to_vector_2[:,1],'g')
ax1.set_title("57x^2-14(3^(1/2))xy+43y^2=1")
ax1.set_xlabel("Ejes coordenados típicos")
ax1.axhline(color='r')
ax1.axvline(color='r')
#second plot
evector_1 = 1/d1_inv*Q_np[:,0]
evector_2 = 1/d2_inv*Q_np[:,1]
Evector_1 = np.row_stack((np.zeros(2), evector_1))
Evector_2 = np.row_stack((np.zeros(2), evector_2))
ax2.plot(Q_to_vector_1[:,0],Q_to_vector_1[:,1],
         color='g', label = "Elipse")
ax2.plot(Q_to_vector_2[:,0],Q_to_vector_2[:,1],
         color='g', label = "_nolegend_")
ax2.plot(Evector_1[:,0], Evector_1[:,1],
         color='b', label = "Eigenvector Q[:,0], define al semieje mayor principal de la elipse")
ax2.plot(-Evector_1[:,0], -Evector_1[:,1],
         color='b', label = "_nolegend_")
ax2.plot(Evector_2[:,0], Evector_2[:,1],
         color='m', label = "Eigenvector Q[:,1],  define al semieje menor principal de la elipse")
ax2.plot(-Evector_2[:,0], -Evector_2[:,1],
         color='m', label = "_nolegend_")
ax2.set_title("x_tilde^2(1/16) + y_tilde^2(1/9)=1")
ax2.set_xlabel("Ejes coordenados rotados")
ax2.legend(bbox_to_anchor=(1, 1))
fig.suptitle("Puntos en el plano que cumplen x_y_tilde^TDx_y_tilde = 1 o  x_y^TAx_y =1")
ax2.grid()
plt.show()
```

```{margin}

Recuerda que $A = Q D Q^T$, $A$ es similar a $D$ matriz diagonal y $Q$ es ortogonal.

```

En la gráfica anterior se representa la rotación de los ejes coordenados definidos por los vectores canónicos $e_1, e_2$ y los rotados definidos por los eigenvectores de $A$. Los eigenvectores de $A$ están en las columnas de $Q$. La primera columna de $Q$ define al eje mayor principal de la elipse y la segunda columna al eje menor principal. La longitud de los semiejes están dados respectivamente por la raíz cuadrada de los recíprocos de los eigenvalores de $A$ que en este caso son: $\frac{1}{9}, \frac{1}{16}$, esto es: $4$ y $16$. Ver por ejemplo: [Principal_axis_theorem](https://en.wikipedia.org/wiki/Principal_axis_theorem), [Diagonalizable_matrix](https://en.wikipedia.org/wiki/Diagonalizable_matrix).

```{admonition} Ejercicio
:class: tip

Rotar los ejes coordenados $45^o$ la ecuación de la elipse:

$$13x^2+10xy+13y^2=72$$

para representar tal ecuación alineando los ejes mayor y menor de la elipse a sus eigenvectores. Encontrar las matrices $Q, D$ tales que $A=QDQ^T$ con $Q$ ortogonal y $D$ diagonal.

```

## Algunos algoritmos para calcular eigenvalores y eigenvectores

Dependiendo de las siguientes preguntas es el tipo de algoritmo que se utiliza:

* ¿Se requiere el cómputo de todos los eigenvalores o de sólo algunos?

* ¿Se requiere el cómputo de únicamente los eigenvalores o también de los eigenvectores?

* ¿$A$ tiene entradas reales o complejas?

* ¿$A$ es de dimensión pequeña y es densa o grande y rala?

* ¿$A$ tiene una estructura especial o es una matriz general?

Para la última pregunta a continuación se tiene una tabla que resume las estructuras en las matrices que son relevantes para problemas del cálculo de eigenvalores-eigenvectores:

|Estructura|Definición|
|:---:|:---:|
|Simétrica|$A=A^T$|
|Ortogonal|$A^TA=AA^T=I_n$|
|Normal|$A^TA = AA^T$|

Ver {ref}`Ejemplos de matrices normales <EJMN>`.

(EJMN)=

### Una opción (inestable numéricamente respecto al redondeo): encontrar raíces del polinomio característico...

Por definición, los eigenvalores de $A \in \mathbb{R}^{n \times n}$ son las raíces o ceros del polinomio característico $p(z)$ por lo que un método es calcularlas vía tal polinomio. Sin embargo, **no es un buen método** calcular tales raíces o ceros pues para una $n > 4$ [Abel](https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem) probó de forma teórica que las raíces en general no son posibles expresarlas por una fórmula cerrada que involucren los coeficientes, operaciones aritméticas y raíces $\sqrt[n]{\cdot}$ . Por lo anterior para calcular eigenvalores de matrices con dimensión $n>4$ requiere de un **método iterativo**.

```{margin}

Como ejemplo de este enunciado considérese:

$$A=\left[
\begin{array}{cc}
1 & \epsilon\\
\epsilon & 1\\
\end{array}
\right]
$$

cuyos eigenvalores son $1 + \epsilon$, $1 - \epsilon$ con $\epsilon$ menor que $\epsilon_{maq}$. Usando aritmética en el SPF se prueba que las raíces del polinomio característico es $1$ de multiplicidad $2$.
```

Además de lo anterior, en ciertas bases de polinomios, por ejemplo $\{1, x, x^2, \dots, x^n\}$, los coeficientes de los polinomios  numéricamente no están bien determinados por los errores por redondeo y las raíces de los polinomios son muy sensibles a perturbaciones de los coeficientes, esto es, es un problema mal condicionado, ver {ref}`condición de un problema y estabilidad de un algoritmo <CPEA>`. Ver [Wilkinson's polynomial](https://en.wikipedia.org/wiki/Wilkinson%27s_polynomial) para un ejemplo.

### Alternativas

Revisaremos en la nota {ref}`Algoritmos y aplicaciones de eigenvalores, eigenvectores de una matriz <AAEVALEVEC>` algunos algoritmos como:

* Método de la potencia y método de la potencia inversa o iteración inversa.

* Iteración por el cociente de Rayleigh.

* Algoritmo QR.

* Método de rotaciones de Jacobi.

---

## Ejemplos de matrices normales

```{sidebar} Descomposición espectral para matrices normales

Las matrices normales generalizan al caso de entradas en $\mathbb{C}$ la diagonalización ortogonal al ser **unitariamente diagonalizables**. $A \in \mathbb{C}^{n \times n}$ es normal si y sólo si $A = U \Lambda U^H$ con $U$ matriz unitaria (generalización de una matriz ortogonal a entradas $\mathbb{C}$), $U^H$ la conjugada transpuesta de $U$ y $\Lambda$ matriz diagonal. Para $A \in \mathbb{R}^{n \times n}$ lo anterior se escribe como: $A$ es simétrica si y sólo si es ortogonalmente diagonalizable: $A = Q \Lambda Q^T$ (ver {ref}`descomposición espectral <DESCESP>`).

```

$$\begin{array}{l}
\left[
\begin{array}{cc}
1 &-2 \\
2 &1
\end{array}
\right],
\left[
\begin{array}{ccc}
1 &2 & 0\\
0 & 1 & 2\\
2 & 0 & 1
\end{array}
\right]
\end{array}
$$

Otro ejemplo:

$$A =
\left[
\begin{array}{ccc}
1 &1 & 0\\
0 & 1 & 1\\
1 & 0 & 1
\end{array}
\right]
$$

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

```python
print(A.T@A)
```

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

```{margin}

Como $A$ es normal entonces se cumple que $AA^T=A^TA$.

```

```python
print(A@A.T)
```

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

```python
evalue, evector = np.linalg.eig(A)
```

```python
print('eigenvalores:')
print(evalue)
```

    eigenvalores:
    [0.5+0.866j 0.5-0.866j 2. +0.j   ]

```{margin}

Se verifica que los eigenvectores de este ejemplo forman un conjunto linealmente independiente pues $A$ es normal.

```

```python
print('eigenvectores:')
print(evector)
```

    eigenvectores:
    [[-0.289+0.5j -0.289-0.5j -0.577+0.j ]
     [-0.289-0.5j -0.289+0.5j -0.577+0.j ]
     [ 0.577+0.j   0.577-0.j  -0.577+0.j ]]

```{margin}

Para una matriz normal $A$ se cumple que es unitariamente diagonalizable y $A = Q \Lambda Q^H$ donde: $Q^H$ es la conjugada transpuesta de $Q$.

```

```python
print('descomposición espectral:')
Lambda = np.diag(evalue)
Q = evector

```

    descomposición espectral:

```python
print('QLambdaQ^H:')
print(Q@Lambda@Q.conjugate().T)

```

    QLambdaQ^H:
    [[ 1.+0.j  1.+0.j -0.+0.j]
     [ 0.+0.j  1.+0.j  1.+0.j]
     [ 1.+0.j -0.+0.j  1.+0.j]]

```python
print(A)
```

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

```{margin}

Observa que $Q^HQ=QQ^H = I_3$ donde: $Q^H$ es la conjugada transpuesta de $Q$.
```

```python
print(Q.conjugate().T@Q)
```

    [[1.+0.j 0.-0.j 0.+0.j]
     [0.+0.j 1.+0.j 0.-0.j]
     [0.-0.j 0.+0.j 1.+0.j]]

```{admonition} Observación
:class: tip

El problema del cálculo de eigenvalores para matrices normales es bien condicionado.

```

**Preguntas de comprehensión:**

1)¿Qué son los eigenvalores de una matriz y qué nombre recibe el conjunto de eigenvalores de una matriz?

2)¿Cuántos eigenvalores como máximo puede tener una matriz?

3)¿Qué característica geométrica tiene multiplicar una matriz por su eigenvector?

4)¿A qué se le nombra matriz diagonalizable o *non defective*?

5)¿Cuál es el número de condición del problema de cálculo de eigenvalores con multiplicidad simple para una matriz simétrica?

6)¿Verdadero o Falso?

a.Una matriz es diagonalizable entonces tiene eigenvalores distintos.

b.Una matriz con eigenvalores distintos es diagonalizable.

c.Si $A=XDX^{-1}$ con $X$ matriz invertible entonces en la diagonal de $D$ y en las columnas de $X$ encontramos eigenvalores y eigenvectores derechos de $A$ respectivamente.

7)Describe la descomposición espectral de una matriz simétrica.

8)¿Qué característica tienen las matrices similares?

**Referencias:**

1. M. T. Heath, Scientific Computing. An Introductory Survey, McGraw-Hill, 2002.

2.  G. H. Golub, C. F. Van Loan, Matrix Computations, John Hopkins University Press, 2013.

3. L. Trefethen, D. Bau, Numerical linear algebra, SIAM, 1997.

4. C. Meyer, Matrix Analysis and Applied Linear Algebra, SIAM, 2000.


# Fundamentos de Matemática Discreta com Python

## Matemática Discreta

A matemática é uma ciência fundamental para qualquer formação ligada à computação, à informática e, em nosso caso, à ciência e análise de dados. Porém, a característica distintiva da matemática realizada pelas máquinas é a finitude. Isto é, enquanto a matemática tradicional que aprendemos é capaz de lidar com quantidades infinitas e incontáveis, as máquinas possuem limitações em suas capacidades, seja em armazenamento, seja em memória. Embora tais capacidades sejam expansíveis de alguma forma, devemos compreender que os recursos computacionais são finitos.

Chamamos de *Matemática Discreta* (ou *Álgebra Abstrata*) a área da Matemática que lida com objetos discretos, a saber, conjuntos, sequencias, listas, coleções ou quaisquer entidades *contáveis*. Por exemplo, diz-se que o conjunto dos números reais é incontável, ou não enumerável, pelo fato de não conseguirmos obter um paralelo entre seus elementos e o conjunto dos números naturais. Em outras palavras, não podemos determinar o número de elementos do conjunto $\mathbb{R}$. Por outro lado, isto não acontece com uma variedade de outros conjuntos que podemos encontrar na vida real. Observemos os exemplos:

- O conjunto das vogais da língua portuguesa;
- O conjunto dos times de futebol brasileiros da série A em 2020;
- O conjunto de nomes das estações do ano;
- O conjunto das personagens que formam o quarteto principal do filme *Os Pinguins de Madagascar* e;
- O conjunto dos números pares positivos menores ou iguais a dez.

Cada conjunto desses possui um número finito de elementos. Isto quer dizer que são contáveis, ou enumeráveis. Podemos defini-los, em linguagem matemática, por meio da *extensão*, quando listamos seus elementos, ou por meio da *compreensão*, quando usamos uma propriedade que distingue seus elementos. Ao longo do ensino básico, você já deparou com isto. Vamos apenas relembrar.

Reescritos por extensão, esses conjuntos, em ordem, são lidos como:

- $\{ a, e, i, o, u \}$
- $\{ \text{Atlético-PR}, \ldots, \text{Bahia}, \text{Botafogo}, \ldots, \text{Coritiba}, \ldots, \text{Fortaleza}, \ldots, \text{Internacional}, \ldots, \text{São Paulo}, \text{Sport}, \text{Vasco} \}$
- $\{ \text{Primavera}, \text{Verão}, \text{Outono}, \text{Inverno}\}$
- $\{ \text{Capitão}, \text{Kowalski}, \text{Recruta}, \text{Rico}\}$
- $\{ 2, 4, 6, 8,10\}$

Já por compreensão, poderiam ser lidos como:

- $\{ c \in \mathbb{A} \, ; \, c \text{ é vogal} \}$
- $\{ t \in \mathbb{T} \, ; \, t \text{ é da Série A} \}$
- $\{ x  \, ; \, x \text{ é uma estação do ano} \}$
- $\{ p  \, ; \, p \text{ é personagem do quarteto principal do filme Os Pinguins de Madagascar} \}$
- $\{ e  \, ; \, e \text{ é estação do ano} \}$
- $\{ n \in \mathbb{Z} \, | \, n = 2k \wedge 2 \leq n \leq 10 \wedge k \in \mathbb{Z} \}$

Por livre conveniência, chamamos de $\mathbb{A}$ o conjunto de todas as letras de nosso alfabeto e de $\mathbb{T}$ o conjunto de todos os times de futebol do Brasil. Adicionalmente, vale ressaltar que poderíamos usar diferentes formas de denotá-los por compreensão além dessas. Tal liberdade de escolha, desde que coerente, transmite exatamente o caráter abstrato que a Matemática Discreta possui.

## Estruturas de dados em Python para lidar com objetos discretos

A linguagem oferece diversos objetos para operarmos com quantidades discretas em formas de sequencias, listas ou coleções. De forma genérica, você pode interpretá-las como "conjuntos" que contém zero ou mais elementos. Um conjunto com zero elementos é chamado de *vazio*. Em Python, também temos meios para representar o "vazio" também, como veremos adiante.

As principais estruturas que aprenderemos serão:

- `list`: estrutura cujo conteúdo é modificável e o tamanho variável. Listas são caracterizadas por *mutabilidade* e *variabilidade*. Objetos `list` são definidos por um par de colchetes e vírgulas que separam seus elementos: `[., ., ... ,.]`.
- `tuple`: estrutura cujo conteúdo não é modificável e o tamanho fixado. Tuplas são caracterizadas por *imutabilidade* e *invariabilidade*. Objetos `tuple` são definidos por um par de colchetes e vírgulas que separam seus elementos: `(., ., ... ,.)`.
- `dict`: estruturas contendo uma coleção de pares do tipo *chave-valor*. Dicionários são caracterizados por *arrays associativos* (*tabelas hash*). Objetos `dict` são definidos por um par de chaves e agrupamentos do tipo  `'chave':valor` (*key:value*), separados por vírgula: `{'chave1':valor1, 'chave2':valor2, ... ,'chaven':valorn}`. As chaves (*keys*) são do tipo `str`, ao passo que os valores podem ser de tipos arbitrários.
- `set`: estruturas similares a `dict`, porém não possuem chaves e contêm objetos únicos. Conjuntos são caracterizadas por *unicidade* de elementos. Objetos `set` são definidos por um par de chaves e vírgulas que separam seus elementos: `{., ., ... ,.}`.

## Listas

Estruturas `list` formam uma coleção de objetos arbitrários e podem ser criadas de modo sequenciado com operadores de pertencimento ou por expressões geradoras, visto que são estruturas iteráveis.

```python
vogais = ['a','e','i','o','u']
vogais
```

    ['a', 'e', 'i', 'o', 'u']

```python
times = ['Bahia', 'Sport', 'Fortaleza', 'Flamengo']
times
```

    ['Bahia', 'Sport', 'Fortaleza', 'Flamengo']

```python
pares10 = [2,4,6,8,10]
pares10
```

    [2, 4, 6, 8, 10]

```python
mix = ['Bahia',24,6.54,[1,2]] # vários objetos na lista
mix
```

    ['Bahia', 24, 6.54, [1, 2]]

### Listas por geração

**Exemplo**: crie uma lista dos primeiros 100 inteiros não-negativos.

```python
os_100 = range(100) # range é uma função geradora
print(list(os_100)) # casting com 'list'
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

**Exemplo**: crie o conjunto $\{ x \in \mathbb{Z} \, ; \, -20 \leq x < 10 \}$

```python
print(list(range(-20,10))) # print é usado para imprimir column-wise
```

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

**Exemplo**: crie o conjunto $\{ x \in \mathbb{Z} \, ; \, -20 \leq x \leq 10 \}$

```python
print(list(range(-20,11))) # para incluir 10, 11 deve ser o limite. Por quê?
```

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

## Adicionando e removendo elementos

Há vários métodos aplicáveis para adicionar e remover elementos em listas.

### Adição por apensamento

Adiciona elementos por concatenação no final da lista.

```python
times.append('Botafogo')
times
```

    ['Bahia', 'Sport', 'Fortaleza', 'Flamengo', 'Botafogo']

```python
times.append('Fluminense')
times
```

    ['Bahia', 'Sport', 'Fortaleza', 'Flamengo', 'Botafogo', 'Fluminense']

### Adição por extensão

Para incluir elementos através de um objeto iterável, sequenciável, usamos `extend`.

```python
times.extend(['Vasco, Atlético-MG']) # usa outra lista pra estender a lista
times
```

    ['Bahia',
     'Sport',
     'Fortaleza',
     'Flamengo',
     'Botafogo',
     'Fluminense',
     'Vasco, Atlético-MG']

#### Iteração e indexação

A *iteração* sobre uma lista é o processo de "passear" por seus elementos de modo sequenciado. Ao fornecermos o *índice* (posição) de seus elementos, podemos indexá-los.

Em Python, a indexação de listas começa a partir de `0` e termina em `n - 1`, onde `n` é o tamanho da lista.

Por exemplo, analise a seguinte correspondência:

$\text{posição} : \{p=0, p=1, \ldots, p={n-1}\}$

$\text{elementos na lista} : [x_1, x_2, \ldots, x_{n}]$

Quer dizer, o primeiro elemento, $x_1$ está na posição 0, $p = 0$, ao passo que o último elemento, $x_n$, está na posição $n - 1$, $p = {n-1}$.  Logo, se escolhermos uma variável chamada $p$ que assume o valor $0$, $1$, ..., $n-1$, mediante a posição (ordenada) do elemento na lista, diremos que $p$ é um *iterador*, os inteiros de $0$ a $n-1$ são os *índices* e $n$ é o *tamanho da lista*.

Esta mesma idéia é aplicável a qualquer coleção, sequencia ou objeto iterável.

### Remoção por índice

Suponha que tivéssemos criado a lista:

```python
pares = [0,2,5,6] # 5 não é par
pares
```

    [0, 2, 5, 6]

Como 5 não é par, não deveria estar na lista. Para excluírmos um elemento em uma posição específica, usamos `pop` passando o *índice* onde o elemento está.

```python
pares.pop(2) # o ímpar 5 está na posição 2 e NÃO 3!
pares
```

    [0, 2, 6]

### Adição por índice

Nesta lista, podemos pensar em incluir 4 entre 2 e 6. Para isto, usamos `insert(posicao,valor)`, para `valor` na `posicao` desejada.

```python
pares.insert(2,4) # 4 é inserido na posição de 6, que é deslocado
pares
```

    [0, 2, 4, 6]

### Apagar conteúdo da lista

Podemos apagar o conteúdo inteiro da lista com `clear`.

```python
times.clear()
times # lista está vazia
```

    []

Podemos contar o número de elementos da lista com `len`.

```python
len(times) # verifica que a lista está vazia
```

    0

```python
type([]) # a lista é vazia, mas continua sendo lista
```

    list

### Outros métodos de lista

Conte repetições de elementos na lista com `count`.

```python
numeros = [1,1,2,3,1,2,4,5,6,3,4,4,5,5]
print( numeros.count(1), numeros.count(3), numeros.count(7) )
```

    3 2 0

Localize a posição de um elemento com `index`.

```python
numeros.index(5) # retorna a posição da primeira aparição
```

    7

Remova a primeira aparição do elemento com `remove`.

```python
numeros.remove(1) # perde apenas o primeiro
numeros
```

    [1, 2, 3, 1, 2, 4, 5, 6, 3, 4, 4, 5, 5]

Faça uma reflexão ("flip") *in-place* (sem criar nova lista) da lista com `reverse`.

```python
numeros.reverse()
numeros
```

    [5, 5, 4, 4, 3, 6, 5, 4, 2, 1, 3, 2, 1]

Ordene a lista de maneira *in-place* (sem criar nova lista) com `sort`.

```python
numeros.sort()
numeros
```

    [1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5, 6]

## Concatenação de listas

Listas são concatenadas ("somadas") com `+`. Caso já possua listas definidas, use `extend`.

```python
['Flamengo', 'Botafogo'] + ['Fluminense']
```

    ['Flamengo', 'Botafogo', 'Fluminense']

```python
['Flamengo', 'Botafogo'] + 'Fluminense' # erro: 'Fluminense' não é list
```

```python
times_nordeste = ['Fortaleza','Sport']
times_sul = ['Coritiba','Atlético-PR']
times_nordeste + times_sul
```

    ['Fortaleza', 'Sport', 'Coritiba', 'Atlético-PR']

```python
times_nordeste.extend(times_sul) # mesma coisa
times_nordeste
```

    ['Fortaleza', 'Sport', 'Coritiba', 'Atlético-PR']

## Fatiamento de listas

O fatiamento ("slicing") permite que selecionemos partes da lista através do modelo `start:stop`, em que `start` é um índice incluído na iteração, e `stop` não.

```python
letras = ['a','b','c','d','e','f','g']
letras[0:2]
```

    ['a', 'b']

```python
letras[1:4]
```

    ['b', 'c', 'd']

```python
letras[5:6]
```

    ['f']

```python
letras[0:7] # toda a lista
```

    ['a', 'b', 'c', 'd', 'e', 'f', 'g']

### Omissão de `start` e `stop`

```python
letras[:3] # até 3, exclusive
```

    ['a', 'b', 'c']

```python
letras[:5] # até 5, exclusive
```

    ['a', 'b', 'c', 'd', 'e']

```python
letras[4:] # de 4 em diante
```

    ['e', 'f', 'g']

```python
letras[6:] # de 6 em diante
```

    ['g']

### Modo reverso

```python
letras[-1] # último índice
```

    'g'

```python
letras[-2:-1] # do penúltimo ao último, exclusive
```

    ['f']

```python
letras[-3:-1]
```

    ['e', 'f']

```python
letras[-4:-2]
```

    ['d', 'e']

```python
letras[-7:-1] # toda a lista
```

    ['a', 'b', 'c', 'd', 'e', 'f']

```python
letras[-5:]
```

    ['c', 'd', 'e', 'f', 'g']

```python
letras[:-3]
```

    ['a', 'b', 'c', 'd']

## Elementos alternados com `step`

Podemos usar um dois pontos duplo (`::`) para dar um "passo" de alternância.

```python
letras[::2] # salta 2-1 intermediários
```

    ['a', 'c', 'e', 'g']

```python
letras[::3] # salta 3-1 intermediários
```

    ['a', 'd', 'g']

```python
letras[::7] # salto de igual tamanho
```

    ['a']

```python
letras[::8] # salto além do tamanho
```

    ['a']

## Mutabilidade de listas

Podemos alterar o conteúdo de elementos diretamente por indexação.

```python
from sympy.abc import x,y

ops = [x+y,x-y,x*y,x/y]
ops2 = ops.copy() # cópia de ops
ops
```

    [x + y, x - y, x*y, x/y]

```python
ops[0] = x-y
ops
```

    [x - y, x - y, x*y, x/y]

```python
ops[2] = x/y
ops
```

    [x - y, x - y, x/y, x/y]

```python
ops[1], ops[3] = x + y, x*y # mutação por desempacotamento
ops
```

    [x - y, x + y, x/y, x*y]

```python
ops[1:3] = [False, False, True] # mutação por fatiamento
ops
```

    [x - y, False, False, True, x*y]

```python
ops = ops2 # recuperando ops
ops
```

    [x + y, x - y, x*y, x/y]

```python
ops2 is ops
```

    True

```python
ops3 = [] # lista vazia
ops3
```

    []

```python
ops2 = ops + ops3 # concatenação cria uma lista nova
ops2
```

    [x + y, x - y, x*y, x/y]

```python
ops2 is ops # agora, ops2 não é ops
```

    False

```python
print(id(ops), id(ops2)) # imprime local na memória de ambas
```

    140242443352016 140242447434608

```python
ops2 == ops # todos os elementos são iguais
```

    True

O teste de identidade é `False`, mas o teste de igualdade é `True`.

**Exemplo:** Escreva uma função que calcule a área, perímetro, comprimento da diagonal, raio, perímetro e área do círculo inscrito, e armazene os resultados em uma lista.

```python
# usaremos matemática simbólica
from sympy import symbols
from math import pi

# símbolos
B, H = symbols('B H',positive=True)

def propriedades_retangulo(B,H):
    '''
        A função assume que a base B
        é maior do que a altura H. Senão,
        as propriedades do círculo inscrito
        não serão determinadas.
    '''
    d = (B**2 + H**2)**(1/2) # comprimento da diagonal
    r = d/2 # raio do círculo inscrito
    return [B*H, 2*(B+H), d, d/2, 2*pi*r, pi*(r)**2]

# lista de objetos símbolos
propriedades_retangulo(B,H)
```

    [B*H,
     2*B + 2*H,
     (B**2 + H**2)**0.5,
     (B**2 + H**2)**0.5/2,
     3.14159265358979*(B**2 + H**2)**0.5,
     0.785398163397448*(B**2 + H**2)**1.0]

```python
# substituindo valores
B, H = 4.0, 2.5

# desempacotando
propriedades_retangulo(B,H)
```

    [10.0,
     13.0,
     4.716990566028302,
     2.358495283014151,
     14.818862909286873,
     17.47510913559322]

### Formatação de strings

Os valores na lista acima poderiam ser impressos de uma maneira mais legível. Até o momento, estivemos habituados em imprimir valores passando-s à função `print`. Entretanto, a Python nos oferece uma ampla gama de recursos para formatar strings. Veremos mais detalhes sobre *templating* e formatação de strings mais à frente no curso. Por enquanto, vamos ver como podemos imprimir melhor os `float` anteriores.

O *template* a seguir usa a função `format` para substituição de valores indexados.

```python
templ = '{0} {1} ... {n}'.format(arg0,arg1,...,argn)
```

**Nota:** Para ajuda plena sobre formatação, consultar:

```python
help('FORMATTING')
```

```python
# considere R: retângulo; C: círculo inscrito

res = propriedades_retangulo(B,H) # resultado

props = ['Área de R',
         'Perímetro de R',
         'Diagonal de R',
         'Raio de C',
         'Perímetro de C',
         'Área de C'
        ] # propriedades

# template
templ = '{0:s} = {1:.2f}\n\
{2:s} = {3:.3f}\n\
{4:s} = {5:.4f}\n\
{6:s} = {7:.5f}\n\
{8:s} = {9:.6f}\n\
{10:s} = {11:.7f}'.format(props[0],res[0],\
                          props[1],res[1],\
                          props[2],res[2],\
                          props[3],res[3],\
                          props[4],res[4],\
                          props[5],res[5])

# impressão formatada
print(templ)
```

    Área de R = 10.00
    Perímetro de R = 13.000
    Diagonal de R = 4.7170
    Raio de C = 2.35850
    Perímetro de C = 14.818863
    Área de C = 17.4751091

### Como interpretar o que fizemos?

- `{0:s}` formata o primeiro argumento de `format`, o qual é `props[0]`, como `str` (`s`).
- `{1:.2f}` formata o segundo argumento de `format`, o qual é `res[0]`, como `float` (`f`) com duas casas decimais (`.2`).
- `{3:.3f}` formata o quarto argumento de `format`, o qual é `res[1]`, como `float` (`f`) com três casas decimais (`.3`).

A partir daí, percebe-se que um template `{X:.Yf}` diz para formatar o argumento `X` como `float` com `Y` casas decimais, ao passo que o template `{X:s}` diz para formatar o argumento `X` como `str`.

Além disso, temos:

- `\n`, que significa "newline", isto é, uma quebra da linha.
- `\`, que é um *caracter de escape* para continuidade da instrução na linha seguinte. No exemplo em tela, o *template* criado é do tipo *multi-line*.

**Nota:** a contrabarra em `\n` também é um caracter de escape e não um caracter *literal*. Isto é, para imprimir uma contrabarra literalmente, é necessário fazer `\\`. Vejamos exemplos de literais a seguir.

#### Exemplos de impressão de caracteres literais

```python
print('\\') # imprime contrabarra literal
print('\\\\') # imprime duas contrabarras literais
print('\'') # imprime plica
print('\"') # imprime aspas
```

    \
    \\
    '
    "

#### f-strings

Temos uma maneira bastante interessante de criar templates usando f-strings, que foi introduzida a partir da versão Python 3.6. Com f-strings a substituição é imediata.

```python
print(f'{props[0]} = {res[0]}') # estilo f-string
```

    Área de R = 10.0

#### Estilos de formatação

Veja um comparativo de estilos:

```python
print('%s = %f ' % (props[0], res[0])) # Python 2
print('{} = {}'.format(props[0], res[0])) # Python 3
print('{0:s} = {1:.4f}'.format(props[0], res[0])) # Python 3 formatado
```

    Área de R = 10.000000
    Área de R = 10.0
    Área de R = 10.0000

**Exemplo:** Considere o conjunto: V = $\{ c \in \mathbb{A} \, ; \, c \text{ é vogal} \}$. Crie a concatenação de todos os elementos com f-string.

```python
V = ['a','e','i','o','u']
V
```

    ['a', 'e', 'i', 'o', 'u']

```python
f'{V[0]}{V[1]}{V[2]}{V[3]}{V[4]}' # pouco Pythônico
```

    'aeiou'

Veremos à frente meios mais elegantes de fazer coisas similares.

## Controle de fluxo: laço `for`

Em Python, podemos realizar iterar por uma coleção ou iterador usando *laços*. Introduziremos aqui o laço `for`. Em Python, o bloco padrão para este laço é dado por:

```python
for valor in sequencia:
    # faça algo com valor
```

Acima, `valor` é um iterador.

```python
for v in vogais: # itera sobre lista inteira
    print(v)
```

    a
    e
    i
    o
    u

```python
for v in vogais[0:3]: # itera parcialmente
    print(v + 'a')
```

    aa
    ea
    ia

```python
for v in vogais[-2:]:
    print(f'{v*10}')
```

    oooooooooo
    uuuuuuuuuu

## Compreensão de lista

Usando `for`, a criação de listas torna-se bastante facilitada.

**Exemplo:** crie a lista dos primeiros 10 quadrados perfeitos.

```python
Q = [q*q for q in range(1,11)]
Q
```

    [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

A operação acima equivale a:

```python
Q2 = []
for q in range(1,11):
    Q2.append(q*q)
Q2
```

    [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

**Exemplo:** crie a PA: $a_n = 3 + 6(n-1), \, 1 \leq n \leq 10$

```python
PA = [3 + 6*(n-1) for n in range(1,11) ]
PA
```

    [3, 9, 15, 21, 27, 33, 39, 45, 51, 57]

**Exemplo:** se $X = \{1,2,3\}$ e $Y=\{4,5,6\}$, cria a "soma" $X + Y$ elemento a elemento.

```python
X = [1,2,3]
Y = [4,5,6]

XsY = [ X[i] + Y[i] for i in range(len(X)) ]
XsY
```

    [5, 7, 9]

**Exemplo:** se $X = \{1,2,3\}$ e $Y=\{4,5,6\}$, cria o "produto" $X * Y$ elemento a elemento.

```python
XpY = [ X[i]*Y[i] for i in range(len(X)) ]
XpY
```

    [4, 10, 18]

## Tuplas

Tuplas são são sequencias imutáveis de tamanho fixo. Em Matemática, uma tupla é uma sequência ordenada de elementos. Em geral, o termo $n-$upla ("ênupla") é usado para se referir a uma tupla com $n$ elementos.

Por exemplo, tuplas de um único elemento são chamadas de "singleton" ou "mônada". Tuplas de dois elementos são os conhecidos "pares ordenados". Com três elementos, chamamos de "trio" ou "tripleta", e assim por diante.

Em Python, tuplas são criadas naturalmente sequenciando elementos.

```python
par = 1,2; par
```

    (1, 2)

```python
trio = (1,2,3); trio
```

    (1, 2, 3)

```python
quad = (1,2,3,4); quad
```

    (1, 2, 3, 4)

```python
nome = 'Nome'; tuple(nome) # casting
```

    ('N', 'o', 'm', 'e')

Tuplas são acessíveis por indexação.

```python
quad[1]
```

    2

```python
quad[1:4]
```

    (2, 3, 4)

```python
quad[3] = 5 # tuplas não são mutáveis
```

Se na tupla houver uma lista, a lista é modificável.

```python
super_trio = tuple([1,[2,3],4]) # casting
super_trio
```

    (1, [2, 3], 4)

```python
super_trio[1].extend([4,5])
super_trio
```

    (1, [2, 3, 4, 5], 4)

Tuplas também são concatenáveis com `+`.

```python
(2,3) + (4,3)
```

    (2, 3, 4, 3)

```python
('a',[1,2],(1,1))*2 # repetição
```

    ('a', [1, 2], (1, 1), 'a', [1, 2], (1, 1))

### Desempacotamento de tuplas

```python
a,b,c,d = (1,2,3,4)
```

```python
for i in [a,b,c,d]:
    print(i) # valor das variáveis
```

    1
    2
    3
    4

```python
a,b = (1,2)
a,b = b,a # troca de valores
a,b
```

    (2, 1)

### `enumerate`

Podemos controlar índice e valor ao iterar em uma sequencia.

```python
for i,x in enumerate(X): # (i,x) é uma tupla (índice,valor)
    print(f'{i} : {x}')
```

    0 : 1
    1 : 2
    2 : 3

**Exemplo:** Construa o produto cartesiano

$$A \times B = \{(a,b) \in \mathbb{Z} \times \mathbb{Z} \, ; \, -4 \leq a \leq 4 \wedge 3 \leq a \leq 7\}$$

```python
AB = ([(a,b) for a in range(-4,4) for b in range(3,7)])
print(AB)
```

    [(-4, 3), (-4, 4), (-4, 5), (-4, 6), (-3, 3), (-3, 4), (-3, 5), (-3, 6), (-2, 3), (-2, 4), (-2, 5), (-2, 6), (-1, 3), (-1, 4), (-1, 5), (-1, 6), (0, 3), (0, 4), (0, 5), (0, 6), (1, 3), (1, 4), (1, 5), (1, 6), (2, 3), (2, 4), (2, 5), (2, 6), (3, 3), (3, 4), (3, 5), (3, 6)]

## Dicionários

Dicionários, ou especificamente, objetos `dict`, possuem extrema versatilidade e são muito poderosos. Criamos um `dict` por diversas formas. A mais simples é usar chaves e pares explícitos.

```python
d = {} # dict vazio
d
```

    {}

```python
type(d)
```

    dict

Os pares chave-valor incorporam quaisquer tipos de dados.

```python
d = {'par': [0,2,4,6,8], 'ímpar': [1,3,5,7,9], 'nome':'Meu dict', 'teste': True}
d
```

    {'par': [0, 2, 4, 6, 8],
     'ímpar': [1, 3, 5, 7, 9],
     'nome': 'Meu dict',
     'teste': True}

### Acesso a conteúdo

Para acessar o conteúdo de uma chave, indexamos pelo seu nome.

```python
d['par']
```

    [0, 2, 4, 6, 8]

```python
d['nome']
```

    'Meu dict'

**Exemplo:** construindo soma e multiplicação especial.

```python
# dict
op = {'X' :[1,2,3], 'delta' : 0.1}

# função
def sp(op):
    s = [x + op['delta'] for x in op['X']]
    p = [x * op['delta'] for x in op['X']]

    return (s,p) # retorna tupla

soma, prod = sp(op) # desempacota

for i,s in enumerate(soma):
    print(f'pos({i}) | Soma = {s} | Prod = {prod[i]}')
```

    pos(0) | Soma = 1.1 | Prod = 0.1
    pos(1) | Soma = 2.1 | Prod = 0.2
    pos(2) | Soma = 3.1 | Prod = 0.30000000000000004

### Inserção de conteúdo

```python
# apensa variáveis
op[1] = 3
op['novo'] = (3,4,1)
op
```

    {'X': [1, 2, 3], 'delta': 0.1, 1: 3, 'novo': (3, 4, 1)}

### Alteração de conteúdo

```python
op['novo'] = [2,1,4] # sobrescreve
op
```

    {'X': [1, 2, 3], 'delta': 0.1, 1: 3, 'novo': [2, 1, 4]}

### Deleção de conteúdo com `del` e `pop`

```python
del op[1] # deleta chave
op
```

    {'X': [1, 2, 3], 'delta': 0.1, 'novo': [2, 1, 4]}

```python
novo = op.pop('novo') # retorna e simultaneamente deleta
novo
```

    [2, 1, 4]

```python
op
```

    {'X': [1, 2, 3], 'delta': 0.1}

### Listagem de chaves e valores

Usamos os métodos `keys()` e `values()` para listar chaves e valores.

```python
arit = {'soma': '+', 'subtr': '-', 'mult': '*', 'div': '/'} # dict

k = list(arit.keys())
print(k)
val = list(arit.values())
print(val)
for v in range(len(arit)):
    print(f'A operação \'{k[v]}\' de "arit" usa o símbolo \'{val[v]}\'.')
```

    ['soma', 'subtr', 'mult', 'div']
    ['+', '-', '*', '/']
    A operação 'soma' de "arit" usa o símbolo '+'.
    A operação 'subtr' de "arit" usa o símbolo '-'.
    A operação 'mult' de "arit" usa o símbolo '*'.
    A operação 'div' de "arit" usa o símbolo '/'.

### Combinando dicionários

Usamos `update` para combinar dicionários. Este método possui um resultado similar a `extend`, usado em listas.

```python
pot = {'pot': '**'}
arit.update(pot)
arit
```

    {'soma': '+', 'subtr': '-', 'mult': '*', 'div': '/', 'pot': '**'}

### Dicionários a partir de sequencias

Podemos criar dicionários a partir de sequencias existentes usando `zip`.

```python
arit = {'soma', 'subtr', 'mult', 'div', 'pot'}
ops = {'+', '-', '*', '/', '**'}

dict_novo = {}

for chave,valor in zip(arit,ops):
    dict_novo[chave] = valor

dict_novo
```

    {'soma': '-', 'div': '*', 'pot': '/', 'mult': '+', 'subtr': '**'}

Visto que um `dict` é composto de várias tuplas de 2, podemos criar um  de maneira ainda mais simples.

```python
dict_novo = dict(zip(arit,ops)) # visto que dicts
dict_novo
```

    {'soma': '-', 'div': '*', 'pot': '/', 'mult': '+', 'subtr': '**'}

## Controle de fluxo: condicionais `if`, `elif` e `else`

Em Python, como na maioria das linguagens, o operador `if` ("se") serve para tratar situações quando um bloco de instruções de código precisa ser executado apenas se uma dada condição estabelecida for avaliada como verdadeira. Um bloco condicional é escrito da seguinte forma:

```python
if condição:
    # faça algo
```

Este bloco diz basicamente o seguinte: "faça algo se a condição for verdadeira". Vejamos alguns exemplos.

```python
if 2 > 0: # a condição é 'True'
    print("2 é maior do que 0!")
```

    2 é maior do que 0!

```python
2 > 0 # esta é a condição que está sendo avaliada
```

    True

```python
if 2 < 1: # nada é impresso porque a condição é 'False'
    print("2 é maior do que 0!")
```

```python
2 < 1 # esta é a condição que está sendo avaliada
```

    False

A condição pode ser formada de diversas formas desde que possa ser avaliada como `True` ou `False`.

```python
x, y = 2, 4
if x < y:
    print(f'{x} < {y}')
```

    2 < 4

A estrutura condicional pode ser ampliada com um ou mais `elif` ("ou se") e com `else` (senão). Cada `elif`, uma redução de *else if*, irá testar uma condição adicional se a condição relativa a `if` for `False`. Se alguma delas for testada como `True`, o bloco de código correspondende será executado. Caso contrário, a decisão do interpretador será executar o bloco que acompanhará `else`.

**Exemplo:** teste da tricotomia. Verificar se um número é $>$, $<$ ou $= 0$.

```python
x = 4.1 # número para teste

if x < 0: # se
    print(f'{x} < 0')
elif x > 0: # ou se
    print(f'{x} > 0')
else: # senão
    print(f'{x} = 0')
```

    4.1 > 0

**Exemplo:** Considere o conjunto de classificações sanguíneas ABO (+/-)

$$S = \{\text{A+}, \text{A-}, \text{B+}, \text{B-}, \text{AB+}, \text{AB-}, \text{O+}, \text{O-}\}$$

Se em um experimento aleatório, $n$ pessoas ($n \geq 500$) diferentes entrassem por um hospital em um único dia, qual seria a probabilidade de $p$ entre as $n$ pessoas serem classificadas como um(a) doador(a) universal (sangue $\text{O-}$) naquele dia? Em seguida, estime a probabilidade das demais.

```python
# 'randint' gera inteiros aleatoriamente
from random import randint

# número de pessoas
n = 500

# associa inteiros 0-7 ao tipo sanguíneo
tipos = [i for i in range(0,8)]
sangue = dict(zip(tipos,['A+','A-','B+','B-','AB+','AB-','O+','O-']))

# primeira pessoa
i = randint(0,8)

# grupo sanguíneo
s = []

# repete n vezes
for _ in range(0,n):
    if i == 0:
        s.append(0)
    elif i == 1:
        s.append(1)
    elif i == 2:
        s.append(2)
    elif i == 3:
        s.append(3)
    elif i == 4:
        s.append(4)
    elif i == 5:
        s.append(5)
    elif i == 6:
        s.append(6)
    else:
        s.append(7)

    i = randint(0,7) # nova pessoa

# calcula a probabilidade do tipo p em %.
# Seria necessário definir uma lambda?
prob = lambda p: p/n*100

# armazena probabilidades no dict P
P = {}
for tipo in tipos:
    P[tipo] = prob(s.count(tipo))
    if sangue[tipo] == 'O-':
        print('A probabilidade de ser doador universal é de {0:.2f}%.'.format(P[tipo]))
    else:
        print('A probabilidade de ser {0:s} é de {1:.2f}%.'.format(sangue[tipo],P[tipo]))
```

    A probabilidade de ser A+ é de 10.80%.
    A probabilidade de ser A- é de 14.00%.
    A probabilidade de ser B+ é de 15.00%.
    A probabilidade de ser B- é de 12.80%.
    A probabilidade de ser AB+ é de 12.80%.
    A probabilidade de ser AB- é de 13.60%.
    A probabilidade de ser O+ é de 11.00%.
    A probabilidade de ser doador universal é de 10.00%.

## Conjuntos

As estruturas `set` (conjunto) são úteis para realizar operações com conjuntos.

```python
set(['a','b','c']) # criando por função
```

    {'a', 'b', 'c'}

```python
{'a','b','c'} # criando de modo literal
```

    {'a', 'b', 'c'}

```python
{1,2,2,3,3,4,4,4} # 'set' possui unicidade de elementos
```

    {1, 2, 3, 4}

### União de conjuntos

Considere os seguintes conjuntos.

```python
A = {1,2,3}
B = {3,4,5}
C = {6}
```

```python
A.union(B) # união
```

    {1, 2, 3, 4, 5}

```python
A | B # união com operador alternativo ('ou')
```

    {1, 2, 3, 4, 5}

### Atualização de conjuntos (união)

A união *in-place* de dois conjuntos pode ser feita com `update`.

```python
C
```

    {6}

```python
C.update(B) # C é atualizado com elementos de B
C
```

    {3, 4, 5, 6}

```python
C.union(A) # conjunto união com A
```

    {1, 2, 3, 4, 5, 6}

```python
C # os elementos de A não foram atualizados em C
```

    {3, 4, 5, 6}

A atualização da união possui a seguinte forma alternativa com `|=`.

```python
C |= A # elementos de A atualizados em C
C
```

    {1, 2, 3, 4, 5, 6}

### Interseção de conjuntos

```python
A.intersection(B) # interseção
```

    {3}

```python
A & B # interseção com operador alternativo ('e')
```

    {3}

### Atualização de conjuntos (interseção)

A interseção *in-place* de dois conjuntos pode ser feita com `intersection_update`.

```python
D = {1, 2, 3, 4}
E = {2, 3, 4, 5}
```

```python
D.intersection(E) # interseção com E
```

    {2, 3, 4}

```python
D # D inalterado
```

    {1, 2, 3, 4}

```python
D.intersection_update(E)
D # D alterado
```

    {2, 3, 4}

A atualização da interseção possui a seguinte forma alternativa com `&=`.

```python
D &= E
D
```

    {2, 3, 4}

### Diferença entre conjuntos

```python
A
```

    {1, 2, 3}

```python
D
```

    {2, 3, 4}

```python
A.difference(D) # apenas elementos de A
```

    {1}

```python
D.difference(A) # apenas elementos de D
```

    {4}

```python
A - D # operador alternativo
```

    {1}

```python
D - A
```

    {4}

### Atualização de conjuntos (diferença)

A interseção *in-place* de dois conjuntos pode ser feita com `difference_update`.

```python
D = {1, 2, 3, 4}
E = {1, 2, 3, 5}
```

```python
D
```

    {1, 2, 3, 4}

```python
D.difference(E)
D
```

    {1, 2, 3, 4}

```python
D.difference_update(E)
D
```

    {4}

A atualização da diferença possui a seguinte forma alternativa com `-=`.

```python
D -= E
D
```

    {4}

### Adição ou remoção de elementos

```python
A
```

    {1, 2, 3}

```python
A.add(4) # adiciona 4 a A
A
```

    {1, 2, 3, 4}

```python
B
```

    {3, 4, 5}

```python
B.remove(3) # remove 3 de B
B
```

    {4, 5}

### Reinicialização de um conjunto (vazio)

Podemos remover todos os elementos de um conjunto com `clear`, deixando-o em um estado vazio.

```python
A
```

    {1, 2, 3, 4}

```python
A.clear()
A # A é vazio
```

    set()

```python
len(A) # 0 elementos
```

    0

### Diferença simétrica

A diferença simétrica entre dois conjuntos $A$ e $B$ é dada pela união dos complementares relativos:

$$A \triangle B = A\backslash B \cup B\backslash A$$

Logo, em $A \triangle B$ estarão todos os elementos que pertencem a $A$ ou a $B$ mas não aqueles que são comuns a ambos.

**Nota:** os complementares relativos $A\backslash B$ e $B\backslash A$ aqui podem ser interpretados como $A-B$ e $B-A$. Os símbolos $\backslash$ e $-$ em conjuntos podem ter sentidos diferentes em alguns contextos.

```python
G = {1,2,3,4}
H = {3,4,5,6}
```

```python
G.symmetric_difference(H) # {3,4} ficam de fora, pois são interseção
```

    {1, 2, 5, 6}

```python
G ^ H # operador alternativo
```

    {1, 2, 5, 6}

### Atualização de conjuntos (diferença simétrica)

A diferença simétrica *in-place* de dois conjuntos pode ser feita com `symmetric_difference_update`.

```python
G
```

    {1, 2, 3, 4}

```python
G.symmetric_difference_update(H)
G # alterado
```

    {1, 2, 5, 6}

```python
G ^= H # operador alternativo
G
```

    {1, 2, 3, 4}

### Continência

Podemos verificar se um conjunto $A$ é subconjunto de (está contido em) outro conjunto $B$ ($A \subseteq B$) ou se $B$ é um superconjunto para (contém) $A$ ($B \supseteq A$) com `issubset` e  `issuperset`.

```python
B
```

    {4, 5}

```python
C
```

    {1, 2, 3, 4, 5, 6}

```python
B.issubset(C) # B está contido em C
```

    True

```python
C.issuperset(B) # C contém B
```

    True

## Subconjuntos e subconjuntos próprios

Podemos usar operadores de comparação entre conjuntos para verificar continência.

- $A \subseteq B$: $A$ é subconjunto de $B$
- $A \subset B$: $A$ é subconjunto próprio de $B$ ($A$ possui elementos que não estão em $B$)

```python
{1,2,3} <= {1,2,3} # subconjunto
```

    True

```python
{1,2} < {1,2,3} # subconjunto próprio
```

    True

```python
{1,2,3} > {1,2}
```

    True

```python
{1,2} >= {1,2,3}
```

    False

### Disjunção

Dois conjuntos são disjuntos se sua interseção é vazia. Podemos verificar a disjunção com `isdisjoint`

```python
E
```

    {1, 2, 3, 5}

```python
G
```

    {1, 2, 3, 4}

```python
E.isdisjoint(G) 1,2,5 são comuns
```

```python
D
```

    {4}

```python
E.isdisjoint(D)
```

    True

```python
A
```

    set()

```python
E.isdisjoint(A)
```

    True

### Igualdade entre conjuntos

Dois conjuntos são iguais se contém os mesmos elementos.

```python
H = {3,'a', 2}
I = {'a',2, 3}
J = {1,'a'}
```

```python
H == I
```

    True

```python
H == J
```

    False

```python
{1,2,2,3} == {3,3,3,2,1} # lembre-se da unicidade
```

    True

## Compreensão de conjunto

Podemos usar `for` para criar conjuntos de maneira esperta do mesmo modo que as compreensões de lista e de dicionários. Neste caso, o funcionamento é como `list`, porém, em vez de colchetes, usamos chaves.

```python
{e for e in range(0,10)}
```

    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

```python
{(i,v) for (i,v) in enumerate(range(0,4))}
```

    {(0, 0), (1, 1), (2, 2), (3, 3)}

## Sobrecarga de operadores

Em Python, podemos realizar alguns procedimentos úteis para laços de repetição.

```python
x = 2
x += 1 # x = 2 + 1 (incrementação)
x
```

    3

```python
y = 3
y -= 1 # y = 3 - 1 (decrementação)
y
```

    2

```python
z = 2
z *= 2 # z = 2*2
z
```

    4

```python
t = 3
t /= 3 # t = 3/3
t
```

    1.0

**Exemplo:** verifique se a soma das probabilidades no `dict` `P` do experimento aleatório é realmente 100%.

```python
s = 0
for p in P.values(): # itera sobre os valores de P
    s += p # soma cumulativa
print(f'A soma de P é {s}%')
```

    A soma de P é 100.0%

De modo mais Pythônico:

```python
sum(P.values()) == 100
```

    True

Ou ainda:

```python
if sum(P.values()) == 100:
    print(f'A soma de P é {s}%')
else:
    print(f'Há erro no cálculo!')
```

    A soma de P é 100.0%

## Controle de fluxo: laço `while`

O condicional `while` permite que um bloco de código seja repetidamente executado até que uma dada condição seja avaliada como `False`, ou o laço seja explicitamente terminado com a keyword `break`.
Em laços `while`, é muito comum usar uma linha de atualização da condição usando sobrecarga de operadores.

A instrução é como segue:

```python
while condicao:
    # faça isso
    # atualize condicao
```

```python
x = 10
boom = 0
while x > boom: # não leva em conta igualdade
    print(x)
    x -= 1 # atualizando por decrementação
print('Boom!')
```

    10
    9
    8
    7
    6
    5
    4
    3
    2
    1
    Boom!

```python
x = 5
boom = 10
while x <= boom: # leva em conta igualdade
    print(x)
    x += 0.5 # atualizando por incrementação
```

    5
    5.5
    6.0
    6.5
    7.0
    7.5
    8.0
    8.5
    9.0
    9.5
    10.0

```python
from math import sin,pi
x = 1.0
i = 1
while x**3 > 0:
    if i % 100 == 0: # imprime apenas a cada 1000 repetições
        print(f'Repeti {i} vezes e x = {x**3}. Contando...')
    x -= 1e-3  # atualiza o decremento
    i += 1 # contagem de repetição
print(f'x = {x**3}')
```

    Repeti 100 vezes e x = 0.7314327009999998. Contando...
    Repeti 200 vezes e x = 0.5139224009999996. Contando...
    Repeti 300 vezes e x = 0.3444721009999996. Contando...
    Repeti 400 vezes e x = 0.2170818009999996. Contando...
    Repeti 500 vezes e x = 0.12575150099999965. Contando...
    Repeti 600 vezes e x = 0.06448120099999974. Contando...
    Repeti 700 vezes e x = 0.02727090099999983. Contando...
    Repeti 800 vezes e x = 0.008120600999999913. Contando...
    Repeti 900 vezes e x = 0.0010303009999999755. Contando...
    Repeti 1000 vezes e x = 9.999999999973564e-10. Contando...
    x = -6.8435572439409775e-46

```python
from math import sin,pi
x = 1.0
i = 1
while x**3 > 0:
    if i % 100 == 0: # imprime apenas a cada 1000 repetições
        print(f'Repeti {i} vezes e x = {x**3}. Contando...')
    if i == 500:
        print(f'Repeti demais. Vou parar.')
        break # execução interrompida aqui
    x -= 1e-3  # atualiza o decremento
    i += 1 # contagem de repetição
print(f'x = {x**3}')
```

    Repeti 100 vezes e x = 0.7314327009999998. Contando...
    Repeti 200 vezes e x = 0.5139224009999996. Contando...
    Repeti 300 vezes e x = 0.3444721009999996. Contando...
    Repeti 400 vezes e x = 0.2170818009999996. Contando...
    Repeti 500 vezes e x = 0.12575150099999965. Contando...
    Repeti demais. Vou parar.
    x = 0.12575150099999965

**Exemplo:** construa seu próprio gerador de números aleatórios para o problema da entrada de pessoas no hospital.

```python
# exemplo simples
def meu_gerador():
    nums = []
    while True: # executa indefinidamente até se digitar ''
        entr = input() # entrada do usuário
        nums.append(entr) # armazena
        if entr == '': # pare se nada mais for inserido
            return list(map(int,nums[:-1])) # converte para int e remove '' da lista
```

```python
# execução:
# 2; shift+ENTER; para 2
# 3; shift+ENTER; para 3
# 4; shift+ENTER; para 4
# shift+ENTER; para nada
nums = meu_gerador()
nums
```

     2
     3
     4

    [2, 3, 4]

**Exemplo:** verifique se a soma das probabilidades no `dict` `P` do experimento aleatório é realmente 100%.

```python
sum(P.values())
```

    100.0

## `map`

A função `map` serve para construir uma função que será aplicada a todos os elementos de uma sequencia. Seu uso é da seguinte forma:

```python
map(funcao,sequencia)
```

No exemplo anterior, as entradas do usuário são armazenadas como `str`, isto é, '2', '3' e '4'. Para que elas sejam convertidas para `int`, nós executamos um *casting* em todos os elementos da sequencia usando `map`.

A interpretação é a seguinte: para todo `x` pertencente a `sequencia`, aplique `funcao(x)`. Porém, para se obter o resultado desejado, devemos ainda aplicar `list` sobre o `map`.

```python
nums = ['2','3','4']
nums
```

    ['2', '3', '4']

```python
m = map(int,nums) # aplica a função 'int' aos elementos de 'num'
m
```

    <map at 0x7f8cbbb32810>

Observe que a resposta de `map` não é *human-readable*. Para lermos o que queremos, fazemos:

```python
l = list(m) # aplica 'list' sobre 'map'
l
```

    [2, 3, 4]

Podemos substituir `funcao` por uma função anônima. Assim, suponha que você quisesse enviezar os valores de entrada somando 1 a cada número. Poderíamos fazer isso como:

```python
list(map(lambda x: x**2,l)) # eleva elementos ao quadrado
```

    [4, 9, 16]

## `filter`

Podemos aplicar também como uma espécie de "filtro" para valores usando a função `filter`. No caso anterior, digamos que  valores acima de 7 sejam inseridos erroneamente no gerador de números (lembre-se que no sistema sanguíneo ABO, consideramos um `dict` cujo valor das chaves é no máximo 7). Podemos, ainda assim, filtrar a lista para coletar apenas valores menores do que 7. Para tanto, definimos uma função `lambda` com este propósito.

```python
lista_erronea = [2,9,4,6,7,1,9,10,2,4,5,2,7,7,11,7,6]
lista_erronea
```

    [2, 9, 4, 6, 7, 1, 9, 10, 2, 4, 5, 2, 7, 7, 11, 7, 6]

```python
f = filter(lambda x: x <= 7, lista_erronea) # aplica filtro
f
```

    <filter at 0x7f8cbd4d7450>

```python
lista_corrigida = list(f) # valores > 7 excluídos
lista_corrigida
```

    [2, 4, 6, 7, 1, 2, 4, 5, 2, 7, 7, 7, 6]

## Exemplos com maior complexidade

**Exemplo:** Podemos escrever outro gerador de forma mais complexa. Estude este caso (pouco Pythônico).

```python
import random

la = random.sample(range(0,1000),1000) # escolhe 1000 números numa lista aleatória de 0 a 1000
teste = lambda x: -1 if x >= 8 else x # retorna x no intervalo [0,7], senão o próprio número
f = list(map(teste,la))
final = list(filter(lambda x: x != -1,f)) # remove > 8
final
```

    [0, 7, 4, 1, 5, 6, 2, 3]

**Exemplo:** Associando arbitrariamente o identificador de uma pessoa a um tipo sanguíneo com compreensão de `dict`.

```python
id_pessoas = {chave:x for chave,x in enumerate(f) if x > -1} # compreensão de dicionário com if
id_pessoas
```

    {77: 0, 100: 7, 191: 4, 313: 1, 357: 5, 669: 6, 854: 2, 919: 3}


# Transformada discreta de Fourier (DFT)

```python
%matplotlib notebook
import numpy as np
import matplotlib.pyplot as plt
```

## 1. Definindo a transformada

Dado um sinal representado por um array $f$. A DFT do sinal é calculada através da equação:

\begin{equation}
F_m = \sum_{n=0}^{M-1}f_ne^{-2\pi imn/M}
\end{equation}
onde $M$ é o tamanho do array, $n$ e $m$ são os índices dos arrays $f$ e $F$ (array resultante), e $f_n$ indica o elemento de índice $n$ do array $f$.

```python
a = np.zeros(6, dtype=np.complex)
print(a)
```

    [0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]

```python
def DFT(fn):
    '''Transformada discreta de Fourier'''
    M = len(fn)
    Fm = np.zeros(M, dtype=np.complex)
    # Para cada frequência
    for m in range(M):
        # Para cada valor do sinal (instante de tempo, posição espacial, etc)
        for n in range(M):
            Fm[m] += fn[n]*np.exp(-1j*2.*np.pi*m*n/M)

    return Fm
```

## 2. DFT de um sinal coseno

```python
signal_frequency = 3.
x = np.linspace(0, 1, 100)
y = np.cos(2*np.pi*signal_frequency*x)
plt.figure()
plt.plot(x, y)
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x10fd10a58>]

```python
# Calcula e imprime a transformada.
Fy = DFT(y)
print(Fy)
```

    [ 1.00000000e+00+0.00000000e+00j  1.12147087e+00+3.52436418e-02j
      1.76713595e+00+1.11178770e-01j  4.99542138e+01+4.72206349e+00j
     -1.33283751e+00-1.68376534e-01j -5.71034048e-01-9.04429082e-02j
     -3.34344165e-01-6.37795605e-02j -2.23229390e-01-4.98976805e-02j
     -1.60512490e-01-4.12126028e-02j -1.21109089e-01-3.51854430e-02j
     -9.45317559e-02-3.07152294e-02j -7.56696829e-02-2.72427622e-02j
     -6.17581967e-02-2.44517998e-02j -5.11833308e-02-2.21490051e-02j
     -4.29462704e-02-2.02089808e-02j -3.63996260e-02-1.85465358e-02j
     -3.11076806e-02-1.71015921e-02j -2.67678492e-02-1.58304619e-02j
     -2.31643592e-02-1.47005494e-02j -2.01398062e-02-1.36869981e-02j
     -1.75770776e-02-1.27704944e-02j -1.53875195e-02-1.19357836e-02j
     -1.35029878e-02-1.11706430e-02j -1.18703860e-02-1.04651530e-02j
     -1.04478356e-02-9.81117072e-03j -9.20194246e-03-9.20194246e-03j
     -8.10581254e-03-8.63181363e-03j -7.13759206e-03-8.09600900e-03j
     -6.27937764e-03-7.59046365e-03j -5.51639341e-03-7.11169153e-03j
     -4.83636266e-03-6.65668213e-03j -4.22902300e-03-6.22281838e-03j
     -3.68574887e-03-5.80781090e-03j -3.19925499e-03-5.40964475e-03j
     -2.76336161e-03-5.02653610e-03j -2.37280732e-03-4.65689657e-03j
     -2.02309877e-03-4.29930372e-03j -1.71038930e-03-3.95247646e-03j
     -1.43138046e-03-3.61525435e-03j -1.18324167e-03-3.28658018e-03j
     -9.63544492e-04-2.96548502e-03j -7.70208620e-04-2.65107546e-03j
     -6.01457560e-04-2.34252253e-03j -4.55782140e-04-2.03905208e-03j
     -3.31910588e-04-1.73993623e-03j -2.28784086e-04-1.44448587e-03j
     -1.45536979e-04-1.15204381e-03j -8.14809733e-05-8.61978660e-04j
     -3.60928285e-05-5.73679081e-04j -9.00514913e-06-2.86548493e-04j
     -7.10542736e-15+1.85912308e-13j -9.00514919e-06+2.86548493e-04j
     -3.60928284e-05+5.73679081e-04j -8.14809733e-05+8.61978660e-04j
     -1.45536979e-04+1.15204381e-03j -2.28784086e-04+1.44448587e-03j
     -3.31910588e-04+1.73993623e-03j -4.55782140e-04+2.03905208e-03j
     -6.01457560e-04+2.34252253e-03j -7.70208620e-04+2.65107546e-03j
     -9.63544492e-04+2.96548502e-03j -1.18324167e-03+3.28658018e-03j
     -1.43138046e-03+3.61525435e-03j -1.71038929e-03+3.95247646e-03j
     -2.02309877e-03+4.29930372e-03j -2.37280732e-03+4.65689657e-03j
     -2.76336161e-03+5.02653610e-03j -3.19925499e-03+5.40964475e-03j
     -3.68574887e-03+5.80781090e-03j -4.22902300e-03+6.22281838e-03j
     -4.83636266e-03+6.65668213e-03j -5.51639341e-03+7.11169153e-03j
     -6.27937764e-03+7.59046365e-03j -7.13759206e-03+8.09600900e-03j
     -8.10581254e-03+8.63181363e-03j -9.20194246e-03+9.20194246e-03j
     -1.04478356e-02+9.81117072e-03j -1.18703860e-02+1.04651530e-02j
     -1.35029878e-02+1.11706430e-02j -1.53875195e-02+1.19357836e-02j
     -1.75770776e-02+1.27704944e-02j -2.01398062e-02+1.36869981e-02j
     -2.31643592e-02+1.47005494e-02j -2.67678492e-02+1.58304619e-02j
     -3.11076806e-02+1.71015921e-02j -3.63996260e-02+1.85465358e-02j
     -4.29462704e-02+2.02089808e-02j -5.11833308e-02+2.21490051e-02j
     -6.17581967e-02+2.44517998e-02j -7.56696829e-02+2.72427622e-02j
     -9.45317559e-02+3.07152294e-02j -1.21109089e-01+3.51854430e-02j
     -1.60512490e-01+4.12126028e-02j -2.23229390e-01+4.98976805e-02j
     -3.34344165e-01+6.37795605e-02j -5.71034048e-01+9.04429082e-02j
     -1.33283751e+00+1.68376534e-01j  4.99542138e+01-4.72206349e+00j
      1.76713595e+00-1.11178770e-01j  1.12147087e+00-3.52436418e-02j]

```python
# plota a transformada. Note que os valores de Fy são complexos,
# então para plotar precisamos calcular o valor absoluto
plt.figure()
plt.plot(np.abs(Fy))
```

Note que Fy possui apenas as amplitudes das frequências do sinal. Isto é, ainda não conhecemos a frequência associada a cada amplitude. Tais frequências serão calculadas abaixo.

## 2. Calculando as frequências

O array Fy calculado acima possui as magnitudes da transformada de Fourier, mas não tem qualquer informação sobre as frequências associadas a cada magnitude. Para calcular as frequências, utilizamos o teorema de Nyquist, que revela que a maior frequência do sinal que podemos representar é dada por

\begin{equation}
f_{max}=\frac{1}{2\Delta x}
\end{equation}
onde $\Delta x$ é o intervalo de amostragem da função (por exemplo, um sinal amostrado a cada 0.1 segundo).
Como o domínio da frequência da transformada de fourier tem a parte negativa, o intervalo de frequências da transformada começa em $-f_{max}$ e termina em $f_{max}$. Portanto, o intervalo total a ser utilizado é $2f_{max}$. As frequências são então divididas em $M$ valores, onde $M$ é o tamanho do array do sinal de entrada. Ou seja, a frequência possui incrementos dados por $\Delta f=2f_{max}/M$.

Outro detalhe é que a saída da DFT possui uma ordenação peculiar das frequências. Supondo que as frequências que queremos representar são dadas por [-3, -2, -1, 0, 1, 2, 3], o resultado da DFT é um array onde cada elemento é relativo ás seguintes frequências: [0, 1, 2, 3, -3, -2, -1]. Se o tamanho do sinal for par, as frequências são [0, 1, 2, -3, -2, -1].

```python
def DFT_frequency(x):

    M = len(x)
    delta_x = x[1]-x[0]     # Intervalo de amostragem
    fMax = 1/(2*delta_x)    # Máxima frequência a ser representada (teorema de Nyquist)
    f_interval = 2*fMax     # Intervalo total de frequências a ser dividido
    delta_f = f_interval/M  # Diferença de frequências entre posições consecutivas do array

    if M%2==0:
        halfM = M//2
        negative_frequencies = np.arange(-halfM, 0)*delta_f
        positive_frequencies = np.arange(0, halfM)*delta_f
    else:
        halfM = (M-1)//2
        negative_frequencies = np.arange(-halfM, 0)*delta_f
        positive_frequencies = np.arange(0, halfM+1)*delta_f
    frequencies = np.concatenate((positive_frequencies, negative_frequencies))

    # O array frequencies possui as seguintes frequências:
    #frequencies = [0, 1, ...,   M/2-1,     -M/2, ..., -1] * delta_f   se M for par
    #frequencies = [0, 1, ..., (M-1)/2, -(M-1)/2, ..., -1] * delta_f   se M for ímpar

    return frequencies
```

```python
# Podemos agora calcular a DFT e plotá-la com as frequências
signal_frequency = 3.
x = np.linspace(0, 1, 100)
y = np.cos(2*np.pi*signal_frequency*x)
Fy = DFT(y)
freqs = DFT_frequency(x)
plt.figure()
plt.plot(freqs, np.abs(Fy))
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x110d50dd8>]

```python
# Em Python, podemos implementar uma DFT mais eficiente utilizando funções numpy
def DFT_faster(fn):

    M = len(fn)
    n_array = np.tile(range(M), reps=(M, 1))
    m_array = np.arange(M).reshape(M, 1)
    indices = n_array*m_array
    coef_matrix = np.exp(-1j*2.*np.pi*indices/M)

    Fm = np.dot(coef_matrix, fn)

    return Fm
```

```python
# Podemos tabém fazer as mesmas operações utilizando funções do scipy
from scipy.fftpack import fft, fftfreq

Fy_scipy = fft(y)
freqs_scipy = fftfreq(len(x), x[1]-x[0])
plt.figure()
plt.plot(freqs_scipy, np.abs(Fy_scipy))

```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x3171a8780>]

```python

```

Lecture 03 - Filtering in the frequency domain/Transformada discreta de Fourier (DFT).ipynb