# Partial Derivatives in sympy

```python
import sympy
```

```python
x, u = sympy.symbols('x u', real=True)
```

```python
U = sympy.Function('U')(x,u)
```

```python
U
```

    U(x, u)

### The case of a(n arbitrary) point transformation

cf. Introduction to Differential Invariants, Chapter 2 Lie Transformations pp. 16

```python
x = sympy.Symbol('x',real=True)
```

```python
y = sympy.Function('y')(x)
```

```python
U = sympy.Function('U')(x,y)
X = sympy.Function('X')(x,y)
Y = sympy.Function('Y')(X)
```

```python
sympy.pprint(sympy.diff(U,x))
```

    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(U(x, ξ₂))⎟│        + ⎜───(U(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x

```python
sympy.pprint( sympy.diff(Y,x))
```

    ⎛d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│    ⎞ ⎛ d        ⎞│
    ⎜──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│    ⎟⋅⎜───(Y(ξ₁))⎟│
    ⎝dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x⎠ ⎝dξ₁       ⎠│ξ₁=X

    (x, y(x))

```python
sympy.pprint( sympy.diff(Y,x).args[0] )
```

    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x

```python
sympy.pprint( sympy.diff(U,x)/ sympy.diff(Y,x).args[0])
```

    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(U(x, ξ₂))⎟│        + ⎜───(U(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x
    ──────────────────────────────────────────────────────────
    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x

For $Y''(X)$,

```python
YprimeX = sympy.diff(U,x)/sympy.diff(Y,x).args[0]
sympy.pprint( sympy.diff(YprimeX,x).simplify() )
```

                                                                   ⎛⎛            2
      ⎛d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│    ⎞ ⎜⎜d          d
    - ⎜──(y(x))⋅⎜───(U(x, ξ₂))⎟│        + ⎜───(U(ξ₁, y(x)))⎟│    ⎟⋅⎜⎜──(y(x))⋅────
      ⎝dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x⎠ ⎜⎜dx
                                                                   ⎝⎝         dy(x
    ──────────────────────────────────────────────────────────────────────────────

                         2               ⎞                     ⎛   2            ⎞│
                        d                ⎟ d          d        ⎜  ∂             ⎟│
    ──(X(x, y(x))) + ────────(X(x, y(x)))⎟⋅──(y(x)) + ──(y(x))⋅⎜──────(X(x, ξ₃))⎟│
     2               dx dy(x)            ⎟ dx         dx       ⎝∂ξ₃ ∂x          ⎠│
    )                                    ⎠
    ──────────────────────────────────────────────────────────────────────────────

                2                 2                              ⎞
               d                 d        ⎛ ∂           ⎞│       ⎟   ⎛d        ⎛ ∂
            + ───(X(x, y(x))) + ───(y(x))⋅⎜───(X(x, ξ₂))⎟│       ⎟ + ⎜──(y(x))⋅⎜──
    ξ₃=y(x)     2                 2       ⎝∂ξ₂          ⎠│ξ₂=y(x)⎟   ⎝dx       ⎝∂ξ
              dx                dx                               ⎠
    ──────────────────────────────────────────────────────────────────────────────

                                    ⎛d        ⎛ ∂           ⎞│          ⎛ ∂
                                    ⎜──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁,
                                    ⎝dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁

                                                    ⎛⎛            2
               ⎞│          ⎛ ∂              ⎞│    ⎞ ⎜⎜d          d
    ─(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│    ⎟⋅⎜⎜──(y(x))⋅──────(U(x, y(x)))
    ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x⎠ ⎜⎜dx            2
                                                    ⎝⎝         dy(x)
    ──────────────────────────────────────────────────────────────────────────────
                  2
           ⎞│    ⎞
     y(x)))⎟│    ⎟
           ⎠│ξ₁=x⎠

          2               ⎞                     ⎛   2            ⎞│            2
         d                ⎟ d          d        ⎜  ∂             ⎟│           d
    + ────────(U(x, y(x)))⎟⋅──(y(x)) + ──(y(x))⋅⎜──────(U(x, ξ₃))⎟│        + ───(U
      dx dy(x)            ⎟ dx         dx       ⎝∂ξ₃ ∂x          ⎠│ξ₃=y(x)     2
                          ⎠                                                  dx
    ──────────────────────────────────────────────────────────────────────────────

                   2                              ⎞
                  d        ⎛ ∂           ⎞│       ⎟
    (x, y(x))) + ───(y(x))⋅⎜───(U(x, ξ₂))⎟│       ⎟
                   2       ⎝∂ξ₂          ⎠│ξ₂=y(x)⎟
                 dx                               ⎠
    ───────────────────────────────────────────────

```python
sympy.factor_list( sympy.diff(Y,x)) # EY 20160522 I don't know how to simply obtain the factors of an expression
# EY 20160522 update resolved: look at above and look at this page; it explains all:
# http://docs.sympy.org/dev/tutorial/manipulation.html
```

    (1,
     [(Subs(Derivative(Y(_xi_1), _xi_1), (_xi_1,), (X(x, y(x)),)), 1),
      (Derivative(y(x), x)*Subs(Derivative(X(x, _xi_2), _xi_2), (_xi_2,), (y(x),)) + Subs(Derivative(X(_xi_1, y(x)), _xi_1), (_xi_1,), (x,)),
       1)])

```python
t, x, u, u_1, x_t, u_t, u_1t = sympy.symbols('t x u u_1 x_t u_t u_1t', real=True)
X = -u_1
U = u - x*u_1
U_1 = x
```

cf. How to do total derivatives: http://robotfantastic.org/total-derivatives-in-sympy.html

```python
from sympy import Derivative, diff, expr
def difftotal(expr, diffby, diffmap):
    """Take the total derivative with respect to a variable.

    Example:

        theta, t, theta_dot = symbols("theta t theta_dot")
        difftotal(cos(theta), t, {theta: theta_dot})

    returns

        -theta_dot*sin(theta)
    """
    # Replace all symbols in the diffmap by a functional form
    fnexpr = expr.subs({s:s(diffby) for s in diffmap})
    # Do the differentiation
    diffexpr = diff(fnexpr, diffby)
    # Replace the Derivatives with the variables in diffmap
    derivmap = {Derivative(v(diffby), diffby):dv
                for v,dv in diffmap.iteritems()}
    finaldiff = diffexpr.subs(derivmap)
    # Replace the functional forms with their original form
    return finaldiff.subs({s(diffby):s for s in diffmap})
```

```python
difftotal( U,t,{x:x_t, u:u_t, u_1:u_1t}) + (-U_1)* (-u_1t)
```

    -u_1*x_t + u_t

This transformation is the Legendre transformation

cf. 4. Exercises Chapter 2 Lie Transformations Introduction to Differential Invariants.

Consider transformation $(x,u)=(x,u(x)) \to (X,U)=(X(x,u),U(x,u))=(u,x)$.  Let $Y=Y(X)$.  $Y(X) \in \Gamma(\mathbb{R}^1 \times \mathbb{R}^1)$, i.e. $Y(X)$ is a section. So $Y(X) = Y(X(x,u)) = U(x,u)$.  And so in this case,
$Y(X(x,u))=Y(u)=U(x,u) = x$

```python
x = sympy.Symbol('x',real=True)
u = sympy.Function('u')(x)
U = x
X = u
Y = sympy.Function('Y')(X)
```

```python
sympy.pprint( sympy.diff(Y,x))
```

    d        ⎛ d        ⎞│
    ──(u(x))⋅⎜───(Y(ξ₁))⎟│
    dx       ⎝dξ₁       ⎠│ξ₁=u(x)

```python
sympy.pprint(sympy.diff(U,x))
```

    1

And so $Y'(X)$ is

```python
sympy.pprint( 1/ sympy.diff(Y,x).args[0])
```

       1
    ────────
    d
    ──(u(x))
    dx

And so $Y''(X)$ is

```python
sympy.pprint( sympy.diff( 1/ sympy.diff(Y,x).args[0], x))
```

       2
      d
    -───(u(x))
       2
     dx
    ───────────
              2
    ⎛d       ⎞
    ⎜──(u(x))⎟
    ⎝dx      ⎠

cf. (2) from 4. Exercises, Chapter 2 Lie Transformations pp. 20

Recall an arbitrary point transformation:

```python
x = sympy.Symbol('x',real=True)
```

```python
y = sympy.Function('y')(x)
```

```python
U = sympy.Function('U')(x,y)
X = sympy.Function('X')(x,y)
Y = sympy.Function('Y')(X)
```

```python
sympy.pprint(sympy.diff(U,x))
```

    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(U(x, ξ₂))⎟│        + ⎜───(U(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x

```python
sympy.pprint( sympy.diff(Y,x))
```

    ⎛d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│    ⎞ ⎛ d        ⎞│
    ⎜──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│    ⎟⋅⎜───(Y(ξ₁))⎟│
    ⎝dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x⎠ ⎝dξ₁       ⎠│ξ₁=X

    (x, y(x))

```python
sympy.pprint( sympy.diff(Y,x).args[0] )
```

    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x

```python
sympy.pprint( sympy.diff(U,x)/ sympy.diff(Y,x).args[0])
```

    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(U(x, ξ₂))⎟│        + ⎜───(U(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x
    ──────────────────────────────────────────────────────────
    d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│
    ──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│
    dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x

For $Y''(X)$,

```python
YprimeX = sympy.diff(U,x)/sympy.diff(Y,x).args[0]
Yprime2X = sympy.diff(YprimeX,x)
sympy.pprint( Yprime2X.simplify() )
```

                                                                   ⎛⎛            2
      ⎛d        ⎛ ∂           ⎞│          ⎛ ∂              ⎞│    ⎞ ⎜⎜d          d
    - ⎜──(y(x))⋅⎜───(U(x, ξ₂))⎟│        + ⎜───(U(ξ₁, y(x)))⎟│    ⎟⋅⎜⎜──(y(x))⋅────
      ⎝dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x⎠ ⎜⎜dx
                                                                   ⎝⎝         dy(x
    ──────────────────────────────────────────────────────────────────────────────

                         2               ⎞                     ⎛   2            ⎞│
                        d                ⎟ d          d        ⎜  ∂             ⎟│
    ──(X(x, y(x))) + ────────(X(x, y(x)))⎟⋅──(y(x)) + ──(y(x))⋅⎜──────(X(x, ξ₃))⎟│
     2               dx dy(x)            ⎟ dx         dx       ⎝∂ξ₃ ∂x          ⎠│
    )                                    ⎠
    ──────────────────────────────────────────────────────────────────────────────

                2                 2                              ⎞
               d                 d        ⎛ ∂           ⎞│       ⎟   ⎛d        ⎛ ∂
            + ───(X(x, y(x))) + ───(y(x))⋅⎜───(X(x, ξ₂))⎟│       ⎟ + ⎜──(y(x))⋅⎜──
    ξ₃=y(x)     2                 2       ⎝∂ξ₂          ⎠│ξ₂=y(x)⎟   ⎝dx       ⎝∂ξ
              dx                dx                               ⎠
    ──────────────────────────────────────────────────────────────────────────────

                                    ⎛d        ⎛ ∂           ⎞│          ⎛ ∂
                                    ⎜──(y(x))⋅⎜───(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁,
                                    ⎝dx       ⎝∂ξ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁

                                                    ⎛⎛            2
               ⎞│          ⎛ ∂              ⎞│    ⎞ ⎜⎜d          d
    ─(X(x, ξ₂))⎟│        + ⎜───(X(ξ₁, y(x)))⎟│    ⎟⋅⎜⎜──(y(x))⋅──────(U(x, y(x)))
    ₂          ⎠│ξ₂=y(x)   ⎝∂ξ₁             ⎠│ξ₁=x⎠ ⎜⎜dx            2
                                                    ⎝⎝         dy(x)
    ──────────────────────────────────────────────────────────────────────────────
                  2
           ⎞│    ⎞
     y(x)))⎟│    ⎟
           ⎠│ξ₁=x⎠

          2               ⎞                     ⎛   2            ⎞│            2
         d                ⎟ d          d        ⎜  ∂             ⎟│           d
    + ────────(U(x, y(x)))⎟⋅──(y(x)) + ──(y(x))⋅⎜──────(U(x, ξ₃))⎟│        + ───(U
      dx dy(x)            ⎟ dx         dx       ⎝∂ξ₃ ∂x          ⎠│ξ₃=y(x)     2
                          ⎠                                                  dx
    ──────────────────────────────────────────────────────────────────────────────

                   2                              ⎞
                  d        ⎛ ∂           ⎞│       ⎟
    (x, y(x))) + ───(y(x))⋅⎜───(U(x, ξ₂))⎟│       ⎟
                   2       ⎝∂ξ₂          ⎠│ξ₂=y(x)⎟
                 dx                               ⎠
    ───────────────────────────────────────────────

```python

```


#
References:
  - [The best Forecast Techniques or how to Predit from Time Series Data](https://towardsdatascience.com/the-best-forecast-techniques-or-how-to-predict-from-time-series-data-967221811981)
  - [Holt-Winters Forecasting for Dummies (or Developers) - Part I](https://grisha.org/blog/2016/01/29/triple-exponential-smoothing-forecasting/)

## Smoothing sumary

###  Simple average

$$ \hat{y}_{k+1} = \frac{1}{k} \sum^{k}_{i=1}y_{i} $$

### Exponential

$$ \hat{y}_{k} = \alpha\,y_{k} + (1 - \alpha)\,\hat{y}_{k-1} $$

### Double Exponential (Holt)

$$
\begin{equation}
l_{x} = \alpha\,y_{x} + (1 - \alpha)(\hat{l}_{k-1} +b_{k-1}) \\
b_{x} = \beta(l_{x} - l_{x-1}) + (1 - \beta)\,b_{x-1} \\
\hat{y}_{x+1} = l_{x} + b_{x}
\end{equation}
$$

### Triple Exponential (Holt-Winters)
$$
\begin{equation}
l_{x} = \alpha(y_{x} - s_{x-L}) + (1+\alpha)(l_{x-1} +b_{x-1}) \\
b_{x} = \beta(l_{x} - l_{x-1}) + (1-\beta)\beta_{x-1} \\
s_{x} = \gamma(y_{x} - l_{x}) + (1-\gamma)s_{x-L} \\
\hat{y}_{x+m} = l_{x} + m\,b_{x} + s_{x-L+1+(m-1)modL}
\end{equation}
$$

# Using statsmodel
Reference:
-[Holt-Winters Exponential Smoothing](https://towardsdatascience.com/holt-winters-exponential-smoothing-d703072c0572)

```python
import pandas as pd
from matplotlib import pyplot as plt
from statsmodels.tsa.holtwinters import ExponentialSmoothing as HWES

df = pd.read_csv(
    'retail_sales_used_car_dealers_us_1992_2020.csv',
    header=0,
    index_col=[0]
)
display(df.index)
df.index = pd.to_datetime(df.index)
display(df.index)

df.plot(figsize=(20,10))
plt.show()
```

```python
df_train = df.iloc[:-12]
df_test = df.iloc[-12:]

model = HWES(df_train, seasonal_periods=12, trend='add', seasonal='mul')
fitted = model.fit()

print(fitted.summary())
```

                           ExponentialSmoothing Model Results
    ================================================================================
    Dep. Variable:             Retail_Sales   No. Observations:                  328
    Model:             ExponentialSmoothing   SSE                       23654407.257
    Optimized:                         True   AIC                           3701.023
    Trend:                         Additive   BIC                           3761.711
    Seasonal:                Multiplicative   AICC                          3703.237
    Seasonal Periods:                    12   Date:                 Sun, 14 Mar 2021
    Box-Cox:                          False   Time:                         14:16:06
    Box-Cox Coeff.:                    None
    =================================================================================
                              coeff                 code              optimized
    ---------------------------------------------------------------------------------
    smoothing_level               0.3939286                alpha                 True
    smoothing_trend               0.0001000                 beta                 True
    smoothing_seasonal            0.4132305                gamma                 True
    initial_level                 5273.5357                  l.0                 True
    initial_trend                 20.563879                  b.0                 True
    initial_seasons.0             0.3744884                  s.0                 True
    initial_seasons.1             0.4141265                  s.1                 True
    initial_seasons.2             0.4527300                  s.2                 True
    initial_seasons.3             0.4944905                  s.3                 True
    initial_seasons.4             0.4379290                  s.4                 True
    initial_seasons.5             0.4436068                  s.5                 True
    initial_seasons.6             0.4543218                  s.6                 True
    initial_seasons.7             0.4376623                  s.7                 True
    initial_seasons.8             0.4108370                  s.8                 True
    initial_seasons.9             0.4357143                  s.9                 True
    initial_seasons.10            0.3751736                 s.10                 True
    initial_seasons.11            0.3446630                 s.11                 True
    ---------------------------------------------------------------------------------

    /home/joaoantoniocardoso/workspace_TCC/env/lib/python3.8/site-packages/statsmodels/tsa/base/tsa_model.py:581: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
      warnings.warn('A date index has been provided, but it has no'
    /home/joaoantoniocardoso/workspace_TCC/env/lib/python3.8/site-packages/statsmodels/tsa/holtwinters/model.py:427: FutureWarning: After 0.13 initialization must be handled at model creation
      warnings.warn(

```python
sales_forecast = fitted.forecast(steps=12)

fig = plt.figure(figsize=(20,10))
fig.suptitle('Retail Sales of Used Cars in the US (1992-2020)')
past, = plt.plot(df_train.index, df_train, 'b.-', label='Sales History')
future, = plt.plot(df_test.index, df_test, 'r.-', label='Actual Sales')
predicted_future, = plt.plot(df_test.index, sales_forecast, 'g.-', label='Sales Forecast')
plt.legend(handles=[past, future, predicted_future])
plt.show()
```

```python

```


## PRODUZA CALCULOS A MÃO COM A BIBLIOTECA HANDCALCS

```python
from math  import sqrt
import sympy   as sp
import handcalcs.render
```

```python
%%render   # código mágico jupyter
#PARAMETROS
a = 2
b = -5
c = 3
```

\[
\begin{aligned}
a &= 2 \;
\\[10pt]
b &= -5 \;
\\[10pt]
c &= 3 \;
\end{aligned}
\]

## FORMULA DE BHASKARA

```python
%%render
x = (-b + sqrt(b**2 - 4*a*c) / (2*a))
```

\[
\begin{aligned}
x &= \left( \left( - b \right) + \frac{ \sqrt { \left( b \right) ^{ 2 } - 4 \cdot a \cdot c } }{ 2 \cdot a } \right)  = \left( \left( - -5 \right) + \frac{ \sqrt { \left( -5 \right) ^{ 2 } - 4 \cdot 2 \cdot 3 } }{ 2 \cdot 2 } \right) &= 5.25
\end{aligned}
\]

## CALCULAR INTEGRAIS

```python
a, b, c = sp.symbols('a b c')
```

```python
d = a**2 + b*a
f = sp.integrate (d,a)
f
```

$\displaystyle \frac{a^{3}}{3} + \frac{a^{2} b}{2}$

```python
%%render sympy
a = 5
b = 10
x = f
```

\[
\begin{aligned}
a &= 5 \;
\\[10pt]
b &= 10 \;
\\[10pt]
x &= \frac{ \left( a \right) ^{ 3 } }{ 3 } + \left( a \right) ^{ 2 } \cdot \frac{ b }{ 2 }  = \frac{ \left( 5 \right) ^{ 3 } }{ 3 } + \left( 5 \right) ^{ 2 } \cdot \frac{ 10 }{ 2 } &= 166.667
\end{aligned}
\]


```python
from sympy import *
from sympy.plotting import *
init_printing()
#Vamos a definir la funcion modulo:
def modulo(vec):
    aux = 0
    for i in vec:
        aux+=S(i)**2
    return sqrt(aux)
#Vamos a definir la funcion para derivar:
def derivar(vec,var):
    aux = []
    for i in vec:
        aux.append(diff(S(i),var))
    return aux
#Vamos a definir la funcion para integrar:
def integrar(vec, var):
    aux = []
    for i in vec:
        aux.append(integrate(S(i),var))
    return aux
#Vamos a definir la funcion para sumar vectores:
def sumarvectores(vec1,vec2):
    return [sum(x) for x in zip(*[vec1,vec2])]
#Vamos a definir una funcion para hacer producto punto de vectores:
def productopunto(vec1, vec2):
    return (vec1[0]*vec2[0])+(vec1[1]*vec2[1])
```

Pregunta 1

Una particula parte de la posicion r(0)=<1;-2;3>

determine los vectores a(t) y r(t)

si su velocidad es v(t)=<6t;6t^5-2t;7>

```python
##Primero definimos las variables iniciales:
t = symbols('t')
#Lo vamos a trabajar como lista "[]" en vez de vector: "()"
r0 = [1,-2,3]
vt = [6*t,6*t**5-2*t,7]
```

```python
#Para hallar la aceleración, derivamos la velocidad:
derivar(vt,t)
#a(t)=
```

```python
#Para hallar la posicion con respecto al tiempo, tenemos que integrar la velocidad y sumar la posicion incial:
rt= integrar(vt,t)
sumarvectores(rt, r0)
#r(t)=
```

```python
integrar(vt,t)
```

Pregunta 2:

Tenemos que plantear una integral que permita calcular la masa de un alambre delgado

Une los puntos: A(sqrt(5);2sqrt(5);0) y B(0;0;5)

y tiene la forma de curva de la interseccion del cilindro:
x^2+z-5=0 y el plano y=2x

sabiendo que su densidad lineal de masa es:
densidad(x;y;z)=sqrt(5-z)

```python
#Primero definimos variables:
x,y,z=symbols('x y z', real=True)
A = [sqrt(5),2*sqrt(5),0]
B = [0,0,5]
expr1 = x**2+z-5
expr2 = 2*x-y #Para esta ecuacion igualamos la segunda a 0
densidadxyz=sqrt(5-z)
```

```python
#Tenemos que armar el vector, para eso:
#Resolvemos la ecuacion 2 con respecto de y para que nos bote y con respecto de x
valory = solve(expr2,y)[0]
valory
```

```python
#Resolvemos la ec 1 con respecto de z para que nos bote un valor z con respecto de x
valorz = solve(expr1,z)[0]
valorz
```

```python
vectorinterseccion = [x,valory,valorz]
vectorinterseccion
#r(x) = ; donde 0<=x<=sqrt(5)
```

```python
#Funcion densidad lineal en términos de t:
densidadt = densidadxyz.subs(z,valorz)
densidadt
```

```python
#Masa:
#es la integral del modulo de la derivada del vector, multiplicado por la densidad
#Hallamos primero los extremos:
supx = max(A[0],B[0])
infx = min(A[0],B[0])
aux = derivar(vectorinterseccion,x)
Integral(modulo(aux)*x,(x,infx,supx))
```

Pregunta 3:

Dado el campo de fuerza F(x;y)=<xy;y^2> y la curva: C:y^2-x=1 que va desde A(-1,0) hasta B(8,3)

```python
#Valores iniciales:
curva = y**2-x-1
campo = [x*y,y**2]
puntoA = [-1,0]
puntoB = [8,3]
```

```python
#Parametrizar la curva C, usando y=t:
rt = [solve(curva, x)[0].subs(y,t) , t]
rt
#Vector rt =
```

```python
solve(curva, x)[0]
```

```python
frt= [campo[0].subs(x,rt[0]).subs(y,rt[1]),campo[1].subs(y,rt[1])]
frt
#vector f(r(t))=
```

```python
dr = derivar(rt,t)
dr
#r'(t)=
```

```python
#Planteamos la integral que permite calcular el trabajo realizar por el campo de fuerza F a lo largo de la curva C desde A hasta B
#Como y=t, los limites de y son:
limsuperior = max(puntoA[1],puntoB[1])
liminferior = min(puntoA[1],puntoB[1])
Integral(simplify(productopunto(frt,dr)), (t,liminferior,limsuperior))

```


Vi har et vektorfelt

$$
\vec{F} = y \mathbf{i} + x \mathbf{j} + z \mathbf{k}
$$

Er vektorfeltet $\vec{F}$ konservativt?

Vi ser på kurveintegralet

$$
\int_{C_i} \vec{F} \cdot d\vec{r}
$$

mellom punktene $A=(0, 0, 0)$ og $B=(1,1,2)$, langs to forskjellige baner gitt ved

$$
C_1 : \begin{cases}
   x(t) = t \\
   y(t) = t  \\
   z(t) = 2t^2
   \end{cases},
   \quad \quad
C_2 : \begin{cases}
   x(t) = t \\
   y(t) = t^2  \\
   z(t) = 2t
   \end{cases},
   \quad \quad t\in [0, 1].
$$

Dette tilsvarer posisjonsvektorene
$$
\vec{r_1} = t \mathbf{i} + t \mathbf{j} + 2t^2 \mathbf{k}, \quad \quad \vec{r_2} = t \mathbf{i} + t^2 \mathbf{j} + 2t \mathbf{k}, \quad \quad t\in [0, 1].
$$

Hvis $\int_{C_i} \vec{F} \cdot d\vec{r}$ er uavhengig av vei, så er vektorfeltet $\vec{F}$ konservativt.

```python
import numpy as np
import sympy as sp
from sympy.vector import CoordSys3D
t = sp.Symbol('t', real=True)
N = CoordSys3D('N')
```

```python
r1 = t*N.i + t*N.j + 2*t**2*N.k
r2 = t*N.i + t**2*N.j + 2*t*N.k
r1
```

$\displaystyle (t)\mathbf{\hat{i}_{N}} + (t)\mathbf{\hat{j}_{N}} + (2 t^{2})\mathbf{\hat{k}_{N}}$

```python
dr1dt = r1.diff(t, 1)
dr2dt = r2.diff(t, 1)
F = lambda r: r.dot(N.j)*N.i + r.dot(N.i)*N.j + r.dot(N.k)*N.k
```

```python
sp.Integral(F(r2).dot(dr2dt), (t, 1, 0)).doit()
```

$\displaystyle -3$

```python
%matplotlib notebook
from mpl_toolkits import mplot3d
import matplotlib.pyplot as plt
ax = plt.axes(projection='3d')
ti = np.linspace(0, 1, 100)
def plot(r, ti):
    ax.plot3D(sp.lambdify(t, r.dot(N.i))(ti),
              sp.lambdify(t, r.dot(N.j))(ti),
              sp.lambdify(t, r.dot(N.k))(ti))
plot(r1, ti)
plot(r2, ti)
```

    <IPython.core.display.Javascript object>

```python

```


# Counterexamples in Calculus

## At the Beginning
I tend to think that the demonstration of counterexamples should play an important role for teaching mathematics courses such as calculus, real analysis, functional analysis, etc. Among the goals of mathematics, the most important one may be to teach logical thinking. This can be thought via things like definition and theorems in mathematics. For this purpose, presentation of counterexamples is necessary. It explains why the hypotheses of various theorems are important, also simple counterexamples showing that the conclusion of a theorem may not hold once the hypotheses is violent. Further, counterexamples offer students the intuition and ability to recognize false propositions.

Above are the motivations of starting this notebook to collect good counterexamples I have countered during my studying and teaching.

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

## Differential Calculus
### If the derivative of a function $f(x)$ is positive at a point $x = x_0$, then there exists a neighborhood about $x = x_0$ where the function is increasing.

__Counterexample__ Considering function
\begin{equation}
    f(x) = \left\{\begin{array}{lc} x + 2x^2\sin\frac{1}{x}, & \text{if } x \neq 0 \\
    0,& \text{if } x = 0\end{array}\right.
\end{equation}
whose derivative is
\begin{equation}
    f'(x) = \left\{\begin{array}{lc} 1 + 4x\sin\frac{1}{x}-2\cos\frac{1}{x}, & \text{if } x \neq 0 \\
    0,& \text{if } x = 0\end{array}\right..
\end{equation}
Clearly, $f'(0) > 0$, but $f(x)$ takes postive and negative values in any neihborhood of the point $x = 0$ (due to the $2\cos\frac{1}{x}$ term).  In other words, the function $f(x)$ is not monotone in any neihborhood of the point $x = 0$.

```python
def fun(x):
    if (x == 0):
        return 0.
    else:
        return x + 2*x**2*np.sin(1/x)

def fun_x(x):
    if (x == 0):
        return 1.
    else:
        return 1 + 4*x*np.sin(1/x) - 2*np.cos(1/x)

x = np.linspace(-0.4, 0.4, 1001)
y = [fun(x[i]) for i in range(1001)]
fig, ax = plt.subplots(1,2)
ax[0].plot(x, y)
y = [fun_x(x[i]) for i in range(1001)]
ax[1].plot(x, y)
```

## Numerical Methods For Differential Equations
### The regularity of the domain plays an important role for the elliptic regularity for a Sobolve function
\begin{equation}
    \|u\|_{W^{2,p}(\Omega)} \leq \|\Delta u\|_{L^p(\Omega)}
\end{equation}
where $\Omega$ is $\mathcal{C}^{1,1}/\mathcal{C}^2$ or convex.  In other words, the Poisson equation $-\Delta u = f$ boundary value problem can get a regularity lifting of $2$ from the data $f$, i.e, the weak differentiability goes up by $2$.

__A counterexample in nonconvex domain:__ the regularity lifting is not true anymore. Consider in the Pacman domain $\Omega$ parametrized using polar coordinates:
\begin{equation}
    \Omega = \{(r,\theta): 0 < r < 1, 0 < \theta < \pi/\alpha\}
\end{equation}
with $1/2 < \alpha < 1$ then
\begin{equation}
    u = (1 - r^2)r^\alpha\sin(\alpha\theta)
\end{equation}
solves the homogeneous Dirichlet boundary problem:
\begin{align}
    -\Delta u & = (4\alpha + 4)r^\alpha\sin(\alpha\theta)\,\,\quad \text{in }\Omega \\
    u & = 0 \,\,\quad \text{on } \partial\Omega
\end{align}
The right hand side data is in $L^p$, but $u \notin W^{2,p}$ for its second derivative shows strong singular behavior near the origin.  If we extend the Pacman to the full disk, such solution won't be there anymore for the value
\begin{equation}
    \lim\limits_{\theta\rightarrow 0^{+}}u(r,\theta) = \lim\limits_{\theta\rightarrow 2\pi^{-}} u(r,\theta)
\end{equation}

```python

```


# ILI285/INF285 Computación Científica
## Scientific Computing Team
## Departamento de Informática
## Universidad Técnica Federico Santa María
### ¿Cómo se puede aproximar $x=\sqrt{2}$?
#### 09/04/2020 v0.1

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

```python
def plot(x, y, xi = None, yi = None):
    y = f(x)
    plt.plot(x, f(x), label=r"$f(x)$")
    if xi is not None and yi is not None:
        plt.plot(xi, yi, 'ro', label='sol', markersize=14)
    plt.grid(True)
    plt.xlabel(r"$x$", fontsize=16)
    plt.ylabel(r"$f(x)$", fontsize=16)
    plt.legend()
    plt.show()
```

```python
def errorIteraciones(val):
    plt.plot(np.abs(np.diff(val)), '.')
    plt.yscale('log')
    plt.grid(True)
    plt.ylabel(r"$|x_{i+1} - x_i|$", fontsize=16)
    plt.xlabel("Iteraciones", fontsize=16)
    plt.show()
```

Haciendo un manejo algebraico básico, podemos reescribir el problema de la siguiente manera:
\begin{equation}
    \begin{split}
        x & = \sqrt{2} \quad /()^2 \\
        x^2 & = 2 \\
        x^2 - 2 &= 0.
    \end{split}
\end{equation}

Definiendo $f(x) = x^2- 2$, el objetivo es encontrar un $x_r$ tal que $f(x_r)=0$.

```python
f = lambda x: x ** 2 - 2
```

```python
Nx = 100
x_ = np.linspace(0, 3, Nx)
```

## Gráfica de la función que nos ayudará a conseguir la aproximación

```python
plot(x_, f(x_))
```

## "Mi primer algoritmo"

Un algoritmo 'naive' sería probar distintos valores de $x$ hasta que $f(x)=0$. Podemos disminuir la cantidad de valores de prueba pensando el intervalo en el que tiene sentido nuestra búsqueda, de esta forma se puede restringir a $x\in ]1, 2[$.

```python
def naive1(a, b, f, TOL = 1e-3):
    dx = 0.01
    x = a
    sol = [x]
    flag = True
    while flag:
        y = f(x)

        if abs(y) <= TOL:
            flag = False
        elif x > b:
            x = a
            dx /= 10
        else:
            x += dx

        sol.append(x)

    return np.array(sol)
```

```python
xf1 = naive1(1, 2, f)
```

```python
len(xf1)
```

    517

Aproximadamente $517$ iteraciones para alcanzar la tolerancia definida. No es para nada eficiente. Si me alejo de la solución, simplemente estoy empezando la búsqueda de nuevo pero con una mayor resolución de puntos.

```python
plot(x_, f(x_), xf1[-1], f(xf1[-1]))
```

```python
errorIteraciones(xf1)
```

## "Naive" 2

Una versión un poquito más "inteligente". Notamos que hay un cambio de signo en $f(x)$ según si estamos a la derecha o a la izquierda de la solución. Con esto, evitamos realizar toda la búsqueda desde el principio.

```python
def naive2(a, b, f, n_iter=50):
    dx = 0.01
    x = np.zeros(n_iter + 1)
    x[0] = a

    # Para guardar el valor antiguo. Se inicia en -1 porque partimos
    # buscando desde la izquierda y f(x) será negativo en esa parte
    y_old = -1

    for i in range(n_iter):
        y = f(x[i])

        # Si detectamos un cambio de signo (pasamos a la derecha o izquierda de la solucion)
        if y_old * y < 0:
            dx = -dx/10 # Se cambia la dirección de busqueda y se reduce la distancia de busqueda

        y_old = y # Se guarda el valor anterior para revisar el cambio de signo

        x[i+1] = x[i] + dx # Siguiente numero a probar

    return x
```

```python
xf2 = naive2(1, 2, f, 100)
```

```python
plot(x_, f(x_), xf2[-1], f(xf2[-1]))
```

```python
errorIteraciones(xf2)
```

Se disminuye notablemente el número de interaciones en comparación a la primera versión.

## Algoritmos más eficientes que veremos en el curso

## Bisección

Se basa en la división del dominio de búsqueda de manera que en cada iteración este se reduce a la mitad.

```python
def biseccion(a, b, f, n_iter=50):
    x = np.zeros(n_iter)
    for i in range(n_iter):
        c = (a + b) / 2
        x[i] = c
        if f(c) == 0:
            break
        if f(a) * f(c) < 0:
            b = c
        else:
            a = c
    return x
```

```python
a, b = 1, 2
xb = biseccion(a, b, f)
```

```python
plot(x_, f(x_), xb[-1], f(xb[-1]))
```

## Error absoluto vs iteraciones

```python
errorIteraciones(xb)
```

Aproximadamente $45$ iteraciones para alcanzar a solución.

## Newton-Raphson

Basado en una expansión en Series de Taylor, este algoritmo requiere el cálculo de la derivada de la función con la que estamos trabajando.

```python
fd = lambda x: 2 * x # f'(x)
```

```python
def newton(f, fd, x0, n_iter=10):
    x = np.zeros(n_iter + 1)
    x[0] = x0
    for i in range(n_iter):
        x[i+1] = x[i] - f(x[i]) / fd(x[i])
    return x
```

```python
x0 = 1
xn = newton(f, fd, x0)
```

```python
plot(x_, f(x_), xn[-1], f(xn[-1]))
```

```python
errorIteraciones(xn)
```

Aproximadamente $6$ iteraciones para alcanzar a solución.

## Comparación de errores entre los métodos

```python
plt.figure(figsize=(10, 6))
plt.plot(np.abs(np.sqrt(2) - xf1[-100:]), 'rx', label="Naive 1") # Solo las ultimas 100 iteraciones
plt.plot(np.abs(np.sqrt(2) - xf2), 'yx', label="Naive 2")
plt.plot(np.abs(np.sqrt(2) - xb), 'bd', label="Bisección")
plt.plot(np.abs(np.sqrt(2) - xn), 'go', label="Newton-Raphson")
plt.yscale('log')
plt.grid(True)
plt.legend()
plt.xlabel("Nº de iteraciones", fontsize=16)
plt.ylabel(r"$|\sqrt{2} - x_{aprox}|$", fontsize=16)
plt.show()
```

# Referencias

* Apuntes del curso Computación Científica
* Raíces de ecuaciones en $1$D, https://github.com/tclaudioe/Scientific-Computing/blob/master/SC1/04_roots_of_1D_equations.ipynb
* *NumPy*, https://numpy.org/
* *Matplotlib*, https://matplotlib.org/


# IWI-131 Programación

## Introducción a Python 3.X

Python es lenguage de programación cuya ejecución es realizada a través de un interprete. Éste interprete lee código desde:
* La consola de Python.
* Archivos de texto (con extensión .py)

En esta clase nos centraremos mayoritariamente en la **consola de python**.

## Tipos de Datos

Python opera sobre datos de distintos tipos. Cada tipo de datos tiene reglas que establecen la forma en que se deben escribir los valores literales (constantes) de ese tipo. Además, cada tipo de datos cuentan con una serie de operadores y funciones que se pueden aplicar. En algunos casos es posible convertir un dato particular de un tipo a otro, ya sea de forma implícita o explícita.

### Números enteros
Tipo `int` (*integer*)

```python
1
```

    1

```python
+135
```

    135

```python
-124
```

    -124

### Números Reales
Tipo `float` (*floating point*)

```python
-0.36
```

    -0.36

```python
1.0
```

    1.0

```python
6.02e23
```

    6.02e+23

### Valores Lógicos
Tipo `bool`

```python
True
```

    True

```python
False
```

    False

### Texto
Tipo `str` (*strings*)

```python
"hola"
```

    'hola'

```python
'hola'
```

    'hola'

```python
"Let's Go!"
```

    "Let's Go!"

```python
'Ella dijo "Hola"'
```

    'Ella dijo "Hola"'

## Expresiones y operadores

**Expresión:** combinación de valores que pueden ser evaluados y entregan un resultado.
Pueden estar formados por:
- **Valores literales**
- **Variables**
- **Operadores**
- **Llamadas a funciones**

**Operador:** símbolo en una expresión que representa una operación aplicada a los valores sobre los que actúa.

### Operadores Aritméticos

Operan sobre valores numéricos y entregan un valor numérico como resultado.
Pueden ser:
- Operadores binarios
- Operadores unarios

#### Operadores binarios
- Suma (`+`)
- Resta (`-`)
- Multiplicación (`*`)
- División (`/`)
- División Entera (`//`)
- Módulo o resto de la división (`%`)
- Potencia (`**`)

```python
3+2
```

    5

```python
8-5
```

    3

```python
8-5.0
```

    3.0

```python
1/2
```

    0.5

```python
1//2
```

    0

```python
5%2
```

    1

```python
2**2
```

    4

#### Operadores unarios
- Positivo (`+`)
- Negativo (`-`)

```python
+3
```

    3

```python
-5.0
```

    -5.0

## Llamados a funciones y uso de biblioteca

Algunas funciones se encuentran incorporadas al núcleo del lenguaje Python y se pueden utilizar directamente (`round`, `abs`). En otros casos, las funciones están agrupadas en colecciones denominadas bibliotecas (`math`, `random`), y es necesario primero importarlas desde la biblioteca antes de poder utilizarlas.

Algunos ejemplos

**Valor absoluto** $|x|$

```python
abs(4-5)
```

    1

**Redondear**

```python
round(2.456)
```

    2

### Ejemplos de funciones de la biblioteca `math`

**Exponencial** $e^x$

```python
from math import exp
exp(1)
```

    2.718281828459045

**Raíz cuadrada** $\sqrt{x}$

```python
import math
math.sqrt(36)
```

    6.0

### Ejemplos de funciones de la biblioteca `random`

`randint(a,b)` entrega un entero aleatorio entre $[a,b]$

```python
from random import randint
randint(1,10)
```

    10

## Precedencia de Operadores

Las expresiones se evalúan siguiendo reglas de precedencia que resuelven las ambigüedades. La precedencia de operadores, de mayor a menor, es la siguiente:

1. `(, )` (parentesis)
* `abs()`, `sqrt()`, `randint()` (llamada a funciones)
* `**`
* `+x`, `-x` (unario)
* `*`, `/`, `//`, `%`
* `+`, `-`

### Asociatividad de Operadores

El operador `**` es asociativo por la derecha. Ejemplo:

```python
2**3**2
```

    512

Los operadores `*`, `/` y `//` lo son por la izquierda. Ejemplo:

```python
24/4/2
```

    3.0

## Conversión entre tipos de datos (*casting*)

```python
int(3.5)
```

    3

```python
float("1")
```

    1.0

```python
str(25)
```

    '25'

```python
bool(0.0)
```

    False

```python
int("hola")
```

## Asignación de variables
Una asignación de variables tiene la forma:

 <variable> = <expresion>

- Primero se evalúa la expresión a la derecha del signo igual.
- El resultado de la evaluación es asignado a la variable a la izquierda del signo igual.

¿Qué valor tienen las siguiente variables?

```python
a = 4 + 5
b = a + 4
a = 2
d = a - 3
e = e + 1
```

## Entrada de datos

```python
dato = input()
```

    a

```python
nombre = input("Ingrese su nombre: ")
```

    Ingrese su nombre: Juan

¿Qué tipo de dato es la variable `nombre`?

```python
nombre
```

    'Juan'

## Salida de datos

```python
print("Hola mundo")
```

    Hola mundo

```python
a = 6
x = a**2
```

```python
print(a, 'al cuadrado es', x)
```

    6 al cuadrado es 36

```python
print(a, x)
```

    6 36

```python
print(a)
print(x)
```

    6
    36

## Comentarios

- Son textos que serán ignorados por el intérprete de Python
- Se utilizan para explicar el código y hacerlo más fácil de entender
- Existen dos tipos de comentario:
    1. Los que se escriben a la derecha de un caracter `#`
        - Cualquier texto que aparezca a la derecha de un caracter `#` será ignorado
        - Terminan cuando termina la línea
    2. Comentarios de múltiples líneas
        - Se encierran entre tres comillas al inicio y final: `'''`
        - Pueden comprender varias líneas

```python
#El siguiente codigo muestra la suma de 2 + 2 en pantalla
print(2 + 2)
```

    4

## Ejemplos

### Ejemplo 1

Solicitar el nombre del usuario e imprimir el mensaje `"Yo soy nombre"`.

```python
nombre = input("Ingrese nombre: ")
print("Yo soy", nombre)
```

    Ingrese nombre: Juan
    Yo soy Juan

### Ejemplo 2

Desarrollar un programa que convierta temperatura de Farenheit a Celsius. La fórmula de conversión es la siguiente:

\begin{equation}
    C = \frac{5}{9}\,(F - 32)
\end{equation}

```python
f = float(input('Temp. en Farenheit: '))
c = (5/9) * (f-32)
print('El equivalente en Celsius es aproximadamente:', int(round(c)))
```

    Temp. en Farenheit: 32
    El equivalente en Celsius es aproximadamente: 0

## Algunos errores

### Error de ejecución (*runtime error*)

Un error de ejecución ocurre cuando el programa termina abruptamente por una condición que ocurre y que le impide continuar ejecutándose. Por ejemplo, cuando se produce un error aritmético al intentar hacer una división por cero.

```python
n = 8
m = 0
print('Listo')
print(n/m)
```

### Error de sintaxis (*syntax error*)

Un error de sintaxis ocurre cuando nos equivocamos en la forma (sintaxis) de escribir una instrucción de nuestro programa. Por ejemplo, cuando no cerramos la cantidad adecuada de paréntesis, olvidamos una coma, escribimos mal una instrucción de Python.

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

```python
n = 6
print(n)
n + 2 = 7
print(n)
```

### Errores de nombre (*name error*)

Un error de nombre ocurre cuando se intenta acceder al contenido de una variable que no ha sido inicializada y que por lo tanto no existe. También puede ocurrir al intentar utilizar una función que no ha sido definida o importada desde una biblioteca.

```python
x = 20
print(5 * x)
print(5 * y)
```

## Reglas para definir identificadores

Los identificadores son los nombres con los que nombramos variables y otros elementos de nuestros programas. En general, como una buena práctica de programación, queremos que nuestras variables y funciones tengan nombres representativos, que indique por sí mismos su propósito. Por ejemplo, una variable para guardar la edad de una persona debería llamarse `edad` y no `x`, a pesar de que el programa funcionará correctamente con cualquiera de las dos.

Python tiene reglas simples para definir identificadores:

1. Un identificador puede contener cualquier combinación de letras (mayúsculas o minúsculas), dígitos y caracteres de guión bajo.
2. El primer caracter debe ser una letra. Las letras mayúsculas y las minúsculas se consideran diferentes, por lo que edad, Edad, y EDAD, son todos identificadores distintos.

Como consecuencia de las reglas 1. y 2., no es posible utilizar espacios en blanco en un identificador. Si queremos tener un identificador compuesto por varias palabras utilizamos guiones bajos para separarlas o una combinación de minúsculas y mayúsculas para destacar las palabras. Por ejemplo: `nombre_cliente` o `nombreCliente`.

## Ejercicios

### Ejercicio 1

Escribir una expresión para sumar los dígitos de un número entero de $3$ dígitos, que se ingrese por pantalla:

```python
n = int(input('Ingrese un número de 3 dígitos: '))
suma = n%10 + (n//10)%10 + n//100
print(suma)
```

    Ingrese un número de 3 dígitos: 123
    6

### Ejercicio 2

Modifique el resultado anterior para que reciba un entero con $3$ dígitos iguales y retorne el resultado ```n//suma```.

```python
n = int(input('Ingresa un numero de 3 digitos iguales: '))
suma = n%10 + (n//10)%10 + n//100
resultado = n//suma
print(resultado)
```

    Ingresa un numero de 3 digitos iguales: 111
    37

### Ejercicio 3

Realice un programa que determine el área de un círculo a partir de su radio.

```python
pi = 3.1415
radio = float(input('Ingrese el radio de un círculo: '))
print("El área de la circunferencia es", pi*(radio**2))
```

    Ingrese el radio de un círculo: 4
    El área de la circunferencia es 50.264

### Ejercicio 4

**RUTEO**: Rutee el siguiente programa e indique que es lo que imprime. Cada vez que el valor de una variable cambie, ponga su valor en una nueva fila de la tabla. La tabla tiene filas de sobra:

```python
a = 94567
b = 28954
c = 36532
d = 11404
e = 40613
a = a//10000
b = (b//1000)%10
c = (c//100)%10
d = (d//10)%10
e = e%10
print (a,b,c,d,e)
```

**Solución ruteo**:

|  |  |  |  | |
|---|---|---|---|---|
| **a** | **b** | **c** | **d** | **e** |
| 94567  |   |   |   |   |
|   | 28954  |   |   |   |
|   |   | 36532  |   |   |
|   |   |   | 11404  |   |
|   |   |   |   | 40613  |
| 9  |   |   |   |   |
|   | 8  |   |   |   |
|   |   | 5  |   |   |
|   |   |   | 0  |   |
|   |   |   |   | 3  |

### Ejercicio 5

Realice un programa que calcule el área de un triángulo a partir de las longitudes de sus lados.

Para calcularlo puede utilizar la fórmula de Herón:

\begin{equation}
    A = \sqrt{s\,(s-a)(s-b)(s-c)},
\end{equation}

donde $a$, $b$ y $c$ son las longitudes de cada lado y $s=\dfrac{a+b+c}{2}$ es el semiperímetro.

```python
l1 = float(input("Ingrese longitud de lado 1: "))
l2 = float(input("Ingrese longitud de lado 2: "))
l3 = float(input("Ingrese longitud de lado 3: "))
s = (l1 + l2 + l3) / 2 # semiperímetro

d1 = s-l1 # diferencia1
d2 = s-l2 # diferencia2
d3 = s-l3 # diferencia3
prod = s*d1*d2*d3 # producto de diferencias y semiperimetro
area = prod ** (1 / 2) # raíz cuadrada
# ¿cómo se podría hacer lo mismo utilizando math.sqrt()?

print("El área del triángulo es", area)
```

    Ingrese longitud de lado 1: 3
    Ingrese longitud de lado 2: 4
    Ingrese longitud de lado 3: 5
    El área del triángulo es 6.0


We have shown that if $x_1, ..., x_n$ are an iid sample from $N(\theta, 1)$ then $\bar{x}$ is sufficient for $\theta$.

We want to show that the conditional distribution of $X_1, ... X_n$ given $T = t = \bar{x} = \frac{1}{n} \sum_{i=1}^n x_i$ is free of $\theta$. First we can show, for example using the [moment generating function technique](https://newonlinecourses.science.psu.edu/stat414/node/172/), that:

$$\sum_{i=1}^n x_i \equiv n \bar{x} \equiv nt \sim N(n \theta, n)$$

so $T \sim N(\theta, 1)$. Then, using the Bayes theorem we have

\begin{align}
P(X_1 = x_1, ..., X_n = x_n | T = t) & = \frac{P(X_1 = x_1, ..., X_n = x_n, T = t)}{P(T = t)} \\
& = \frac{P(X_1 = x_1, ..., X_n = x_n)}{P(T = t)} \\
& = \frac{\sim N(\theta, 1)}{\sim N(\theta, 1)} \\
& = 1
\end{align}

which is free of $\theta$.


# SEIR model example
\begin{align}
\dot{S} & = S_f - \alpha\beta\frac{SI}{N+k_I I+k_R R} + r_{R\_S} R\\
\dot{E} & = E_f + \alpha\beta\frac{SI}{N+k_I I+k_R R} - E\frac{1}{t_{E\_I}} \\
\dot{I} & = E\frac{1}{t_{E\_I}} - I\frac{1}{t_{I\_R}} \\
\dot{R} & = I\frac{1}{t_{I\_R}} - r_{I\_R} R\\
\end{align}
Where:
* $S:$ Susceptible
* $E:$ Exposed
* $I:$ Infectious
* $R:$ Removed
* $\alpha:$ Mobilty
* $\beta:$ Infection rate
* $N:$ Total population
* $t_{E\_I}:$ # Transition time between exposed and infectious
* $t_{I\_R}:$ # Transition time between infectious and recovered
* $r_{R\_S}:$ Immunity loss rate ($\frac{1}{t_{R\_S}}$)
* $S_f,E_f,I_f,R_f:$ External flux
* $k_I:$ Infected saturation
* $k_R:$ Immunity shield

```python
# Util libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
# Adding lib paths
import sys
sys.path.insert(1, '../src2/models/')

# cv19 libraries
from SEIR import SEIR
import toml
import copy
```

```python
# For pop-up plots execute this code (optional)
import platform
OS = platform.system()

if OS == 'Linux':
    %matplotlib tk
    print('Linux')
elif OS == 'Windows':
    %matplotlib qt
    print('Windows')
elif OS == 'Darwin':
    %matplotlib tk
    print('Mac (Funciona?)')
```

    Linux

```python

```

```python
# Input configuration file
config = 'cfg/SEIR.toml'
```

```python
cfg = toml.load(config)
```

# Beta

## Beginning

```python
var = [0.8,0.9,1,1.1,1.2]
simcfg = [copy.deepcopy(cfg) for i in var]
```

```python
for i in range(len(simcfg)):
    simcfg[i]['parameters']['dynamic']['beta']=cfg['parameters']['dynamic']['beta']*var[i]
```

```python
%%capture
model = SEIR(config=config)
model.integrate()
peak = max(model.I)
```

```python
# Build simulation object
sim = []
for i in simcfg:
    sim.append(SEIR(config = i))
```

    No external data added
    SEIR object created
    No external data added
    SEIR object created
    No external data added
    SEIR object created
    No external data added
    SEIR object created
    No external data added
    SEIR object created

```python
%%capture
# Simulate (solve ODE)
for i in sim:
    i.integrate()
```

```python
for i in range(len(sim)):
    plt.plot(sim[i].t,sim[i].I/peak,label='Beta: '+str(int(var[i]*100))+'%')
plt.xlim(0,300)
plt.legend(loc=0)
plt.title('Beta')
plt.ylabel('Infectious')
plt.xlabel('day')
plt.show()
```

## During Simulation

```python
var2 = [0.8,0.9,1,1.1,1.2]
simcfg2 = [copy.deepcopy(cfg) for i in var2]
```

```python
alpha = []
for i in var2:
    alpha.append('{"function":"Events","values":[1,'+str(i)+'],"days":[[0,30],[30,500]]}')
```

```python
for i in range(len(simcfg2)):
    simcfg2[i]['parameters']['dynamic']['alpha']=alpha[i]
```

```python
%%capture
# Build simulation object
sim2 = []
for i in simcfg2:
    sim2.append(SEIR(config = i))
```

```python
%%capture
# Simulate (solve ODE)
for i in sim2:
    i.integrate()
```

```python
for i in range(len(sim2)):
    plt.plot(sim2[i].t,sim2[i].I/peak,label='Beta: '+str(int(var2[i]*100))+'%')
plt.xlim(0,300)
plt.legend(loc=0)
plt.title('Beta')
plt.ylabel('Infectious')
plt.xlabel('day')
plt.axvline(20,linestyle='dashed',color='grey')
plt.show()
```

# Population

## Beginning

```python
population = cfg['initialconditions']['population']
```

```python
var3 = [0.8,0.9,1,1.1,1.2]
simcfg3 = [copy.deepcopy(cfg) for i in var3]
```

```python
for i in range(len(simcfg3)):
    simcfg3[i]['initialconditions']['population']=population*var3[i]
```

```python
%%capture
# Build simulation object
sim3 = []
for i in simcfg3:
    sim3.append(SEIR(config = i))
```

```python
%%capture
# Simulate (solve ODE)
for i in sim3:
    i.integrate()
```

```python
sim3
```

    [<SEIR.SEIR at 0x7f874cf8ae80>,
     <SEIR.SEIR at 0x7f874cffebb0>,
     <SEIR.SEIR at 0x7f874cffeee0>,
     <SEIR.SEIR at 0x7f874cffec10>,
     <SEIR.SEIR at 0x7f874cffe100>]

```python
for i in range(len(sim3)):
    plt.plot(sim3[i].t,sim3[i].I/peak,label='Population: '+str(int(var3[i]*100))+'%')
plt.xlim(0,300)
plt.legend(loc=0)
plt.title('Population')
plt.ylabel('Infectious')
plt.xlabel('day')
plt.show()
```

```python

```

## During Simulation

```python
var4 = [-0.03,-0.015,0,0.015,0.03]
```

```python
S_f = []
for i in var4:
    S_f.append('{"function":"Events","values":['+str(population*i)+'],"days":[[20,25]]}')
```

```python
simcfg4 = [copy.deepcopy(cfg) for i in var4]
for i in range(len(simcfg4)):
    simcfg4[i]['parameters']['dynamic']['S_f']=S_f[i]
```

```python
# Build simulation object
sim4 = []
for i in simcfg4:
    sim4.append(SEIR(config = i))
```

    Executing Events
    No external data added
    SEIR object created
    Executing Events
    No external data added
    SEIR object created
    Executing Events
    No external data added
    SEIR object created
    Executing Events
    No external data added
    SEIR object created
    Executing Events
    No external data added
    SEIR object created

```python
%%capture
# Simulate (solve ODE)
for i in sim4:
    i.integrate()
```

```python
for i in range(len(sim)):
    plt.plot(sim4[i].t,sim4[i].I/peak,label='Population: '+str(int(var4[i]*100*10))+'%',linestyle='dashed')
plt.xlim(0,300)
plt.legend(loc=0)
plt.title('Population')
plt.ylabel('Infectious')
plt.xlabel('day')
plt.axvline(20,linestyle='dashed',color='grey')
plt.show()
```

```python

```


# DATA 5600: Introduction to Regression and Machine Learning for Analytics

## __CLT Examples__

Author:      Tyler J. Brough

Updated: October 6, 2021

---

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy import stats
```

### __Probability Distribution, Mean, and Variance of a Poisson Random Variable__

---

$$
\begin{align}
p(x) &= \frac{\lambda^{x} e^{-\lambda}}{x!} \quad x = 0, 2, \ldots \\
     & \\
\mu  &= \lambda \\
     & \\
\sigma^{2} &= \lambda \\
\end{align}
$$

---

where

* $\lambda =$ mean number of events during given unit of time, area, volume, etc.

* $e = 2.71828...$

#### __Poisson Example__

Suppose the number, $x$, of a company's employees who are absent on Mondays has (approximately) a Poisson probability distribution. Further, assume that the average number of Monday absentees is 2.6.

* (a) Find the mean and standard deviation of $x$ the number of employees absent on Monay.

$$
\begin{align}
\mu &= \lambda = 2.6 \\
& \\
\sigma^{2} &= \lambda = 2.6
\end{align}
$$

Then the standard deviation is: $\sqrt{2.6} = 1.61$

```python
λ = 2.6
n = 52 # weeks
x = np.random.poisson(lam=λ, size=n)
```

```python
x
```

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

```python
xbar = np.mean(x)
xbar
```

    2.5384615384615383

```python
sbar = np.std(x, ddof=1)
sbar
serr = sbar / np.sqrt(n)
```

```python
z = (x - xbar) / sbar
```

```python
M = 500_000
n = 104

xbars = np.zeros(M)

for i in range(M):
    xbars[i] = np.mean(np.random.poisson(lam=λ, size=n))
```

```python
plt.hist(xbars, bins=25)
```

```python
zbars = (xbars - λ) / (np.sqrt(λ) / np.sqrt(n))
```

```python
plt.hist(zbars)
```

```python
np.mean(zbars)
```

    -0.000184020234609593

```python
np.std(zbars,ddof=1)
```

    1.000616162076678

```python

```


# Quantum phase estimation

Your task in this notebook is to implement the quantum Fourier transform on a set of 3 qubits in Qiskit, and then use it to estimate the eigenvalue of a simple Hamiltonian.

### Part A: Implementing the QFT

```python
import numpy as np

from qiskit import QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit
from qiskit import execute, BasicAer

import qiskit.tools.visualization as qvis

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
```

**Task 1.** In the lecture slides I showed the QFT in general; using this template, write out the circuit for the 3-qubit case. Don't forget the SWAP at the end!

** A. ** _Here's a graphic of the circuit._

**Task 2.** Recall that the rotations have the form
\begin{equation}
 R_k = \begin{pmatrix}
       1 & 0 \\ 0 & e^{2\pi i / 2^k}
       \end{pmatrix}
\end{equation}
Evaluate the unitary operations for the rotations present in Task 1. Do these gates look familiar?

** A. ** _These are the $S$ and $T$ gates that I showed in the slides!_

** Task 3.** Now it's time to implement the circuit. I've set up a 3-qubit quantum register, so all you have to do is implement the gates. For reference, [this page](https://qiskit.org/documentation/terra/summary_of_quantum_operations.html#controlled-operations-on-qubits) contains information on the gates you can implement natively in Qiskit. Hint: look at the `cu1` gates.

```python
# Empty register
q = QuantumRegister(3)

# This object will be our circuit
qft = QuantumCircuit(q)

# Apply gates to the first qubit
qft.h(q[0])
qft.cu1(np.pi/2, q[1], q[0])
qft.cu1(np.pi/4, q[2], q[0])

# Apply gates to the second qubit
qft.h(q[1])
qft.cu1(np.pi/2, q[2], q[1])

# Apply  remaining gates
qft.h(q[2])

qft.swap(q[0], q[2])
```

### Part B: Eigenvalue estimation

In this part, we're going to implement the phase estimation algorithm to get the eigenvalue of the following unitary:
\begin{equation}
 U = \begin{pmatrix}
       1 & 0 \\
       0 & e^{5\pi i / 4} \\
       \end{pmatrix}
\end{equation}

** Task 4. ** What is the eigenvalue $\varphi$ of this matrix as expressed in the phase estimation problem, i.e. $e^{2\pi i \varphi}$? What is its expansion in the notation $0.\varphi_1 \cdots \varphi_n$?

** A. ** The eigenvalue here is $\varphi = 0.625$. In decimal notation this is $0.101$, i.e. $1 \cdot (2^{-1}) + 0 \cdot 2^{-2} + 1 \cdot 2^{-3}$. This fits perfectly in a 3-qubit state, and we should expect to measure $|101\rangle$ as our output state after running the algorithm.

Now let's set up the two quantum registers: one for the 3 qubits to represent phase, and 2 for the eigenvector since it is a 4-dimensional vector. We'll also need 3 classical bits to hold the measurement outcomes of the phase register.

```python
ph_reg = QuantumRegister(3)
eig_reg = QuantumRegister(1)
meas_reg = ClassicalRegister(3)

qpe = QuantumCircuit(ph_reg, eig_reg, meas_reg)
```

** Task 5. ** Apply the phase estimation algorithm to your registers. Recall that you'll also need to initialize the eigenvector register into the proper eigenvector.

```python
# Initialize the eigenvector
qpe.x(eig_reg)

# Perform the phase estimation; we're applying a controlled U varying amounts of times
qpe.h(ph_reg)

U_phase = 2 * np.pi * 0.625

qpe.cu1(U_phase, ph_reg[2], eig_reg[0])

qpe.cu1(U_phase, ph_reg[1], eig_reg[0])
qpe.cu1(U_phase, ph_reg[1], eig_reg[0])

qpe.cu1(U_phase, ph_reg[0], eig_reg[0])
qpe.cu1(U_phase, ph_reg[0], eig_reg[0])
qpe.cu1(U_phase, ph_reg[0], eig_reg[0])
qpe.cu1(U_phase, ph_reg[0], eig_reg[0])
```

** Task 6. ** Apply the inverse QFT to your phase register.

Note: unfortunately, Qiskit does not yet support a simple circuit reversal operation; so you'll have to manually list the inverses of each gate. Don't forget to take the adjoint!

```python
qpe.swap(ph_reg[0], ph_reg[2])
qpe.h(ph_reg[2])
qpe.cu1(-np.pi/2, ph_reg[2], ph_reg[1])
qpe.h(ph_reg[1])
qpe.cu1(-np.pi/4, ph_reg[2], ph_reg[0])
qpe.cu1(-np.pi/2, ph_reg[1], ph_reg[0])
qpe.h(ph_reg[0])
```

** Final task. ** Let's simulate the circuit and measure to get the vector corresponding to our phase!

```python
qpe.measure(ph_reg, meas_reg)

backend = BasicAer.get_backend('qasm_simulator') # the device to run on
result = execute(qpe, backend, shots=1000).result()
counts = result.get_counts(qpe)

# Extract the counts and compute the value
eigenvalue = 0

# Reverse - in this case it is symmetric, but in general you'll have to be careful
binary_eigenvalue = list(counts.keys())[0]
binary_eigenvalue = binary_eigenvalue[::-1]

for idx, b in enumerate(list(binary_eigenvalue)):
    if b == '1':
        eigenvalue += 2 ** (-(idx + 1))

print(f"Phase estimation routine returned the eigenvalue {eigenvalue}.")
```


Construir o gráfico e encontrar o foco e uma equação da diretriz.

8. $2y^2 -9x = 0$

Arrumando a equação

$2y^2 = 9x$

$y^2 = \frac{9x}{2}$

$2p = \frac{9}{2}$, logo

$p = \frac{9}{4}$

Calculando o foco

$F = \frac{p}{2}$

$F = \frac{\frac{9}{4}}{2}$

$F = \frac{9}{4} \cdot \frac{1}{2}$

$F(\frac{9}{8}, 0)$

Calculando a diretriz

$D = -\frac{p}{2}$

$D = -\frac{9}{8}$

$V(0,0)$

$D : x = -\frac{9}{8}$

$F(\frac{9}{8}, 0)$

Gráfico da parábola

```python
from sympy import *
from sympy.plotting import plot_implicit
x, y = symbols("x y")
plot_implicit(Eq((y-0)**2, 4.5*(x+0)), (x,-10,10), (y,-10,10),
title=u'Gráfico da parábola', xlabel='x', ylabel='y');
```

Problemas Propostos. Pag. 172 - 175/08.ipynb


# PC lab 4: Logistic regression for classification

## Introduction

In a binary classification setting, we are interested in assigning an observation $\mathbf{x}$ to one of two possible classes, denoted by $y$. For example, maybe we would like to tell if a patient has a particular disease (y = 1) or not (y = 0), given certain symptoms $\mathbf{x}$. Generally speaking, we want to predict the probability that the class label $y = 1$, conditional on the data that we have observed, $\mathbf{x}$. This probability is also called the *class posterior* or the *class-membership probability*, which we can denote as follows:

\begin{equation}
Pr(Y=1|X) = P(X) = p(y= 1|\mathbf{x})
\end{equation}

The book uses the statistical notation on the left, but the notation with the feature vector $\mathbf{x}$ is more common in machine learning literature. In any case, both notations mean exactly the same. In this PC lab, we will cover one of the most popular classifiers: logistic regression.

Just like linear regression, logistic regression (LR) is a linear model. However, LR does not model the mean of a continuous outcome, but the logarithm of the [odds](https://en.wikipedia.org/wiki/Odds) of the probability $P(X)$:

\begin{equation}
log \frac{P(X)}{1-P(X)} = w_{0}x_{0} + w_{1}x_{1} + ... + w_{p}x_{p} = \mathbf{w^Tx}
\end{equation}

However, we are really interested in the probability $p$ and not in the odds of p. Therefore, it is common to apply the inverse log-odds transformation on both sides of the equation. This transformation is the **logistic function $\phi(z)$**, hence the name of logistic regression:

\begin{equation}
\phi(z) = \frac{1}{1 + e^{-z}} = \frac{e^{z}}{1+e^{z}}
\end{equation}

Verify for yourself that applying $\phi(z)$ on the log-odds yields $p$.

In other words, we can make predictions for $p$ with logistic regression as follows:

\begin{equation}
p(\mathbf{x}) = \phi(w_{0}x_{0} + w_{1}x_{1} + ... + w_{p}x_{p})
\end{equation}

If we want to classify a data point $\mathbf{x}$, we can calculate $p$ with LR and simply assign it to class 1 if $p$ exceeds a certain probability threshold. A typical threshold is 0.5.

EXERCISE: What would happen to our predictions when we would choose a lower threshold, let's say 0.2? How would this affect the accuracy of our predictions? Can you think of a situation where we would want to do this?

Let's stop for a moment to have a look at what the logistic transformation does:

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('seaborn-white')

x = np.arange(-8,8,0.01) # Generate a range of x values
y = 1/(1+np.exp(-x)) # Calculate the logistic transformation of these x'es

# Plot them
fig, ax = plt.subplots()
ax.scatter(x,y, marker='.');
ax.set_xlabel('x');
ax.set_ylabel('y');
```

As shown, $\phi$ monotonically maps any number from the real domain to a number in [0,1]. Indeed, this is a desirable property if we want to predict a probability!

## Training a LR model

### Loss function: the cross-entropy loss

Now that we have the logistic regression model to predict the probability of belonging to a certain class, all that remains is the question of how to find the weights of the model on a given set of training data. As always, this is the problem of minimizing a loss function to find an optimal set of weights. Where we used the mean squared error (MSE) for linear regression, we will use the **cross-entropy** loss function for LR. Minimizing the binary cross-entropy loss is equivalent to minimizing the negative log-likelihood of the data under a binomial distribution:

\begin{equation}
l_{log} = \frac{1}{n}\sum\limits_{i=1}^{n}-y_{i}log(p(\mathbf{x}_i))-(1-y_i)log(1-p(\mathbf{x}_i))
\end{equation}

Where $y_i$ is the class of data point $i$ and $p(\mathbf{x}_i)$ is the class-membership probability predicted by logistic regression for the observation $\mathbf{x}_i$. If we look at the cross-entropy loss **for a single data point** $l_{log}^{i}$, we can break it down in two parts:

\begin{equation}
l_{log}^{i} =
\begin{cases}
   -log(p(\mathbf{x}_i)) & \text{if} \ y_i = 1\\
   -log(1-p(\mathbf{x}_i)) & \text{if} \ y_i = 0
\end{cases}
\end{equation}

It should be clear that the cross-entropy loss will be larger for smaller values of $p(\mathbf{x}_i)$ if $y_i = 1$, and vice versa. Let's visualize the cross-entropy loss for these two cases:

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('seaborn-white')

p = np.arange(0.01,0.99,0.01) # Generate a range of predicted probabilities between zero and 1
l_0 = -np.log(p)  # cross-entropy loss if y = 1
l_1 = -np.log(1-p)

# Plot them
fig, ax = plt.subplots(figsize=(10,6))
ax.scatter(p,l_0, marker='.');
ax.scatter(p, l_1, marker='.');
ax.set_xlabel('Predicted class-membership probability $p$');
ax.set_ylabel('Cross-entropy loss');
ax.legend(['Cross-entropy loss when y is 1', 'Cross-entropy loss when y is 0']);
```

EXERCISE: Make sure you understand the cross-entropy loss. Verify that it correctly penalizes wrong predictions in both cases. Suppose that we have no information about the data at all, what would be the best guess for p to minimize the cross-entropy loss?

### Finding the weights with gradient descent

For linear regression, the solutions to the normal equations provide a convenient analytical solution to obtain the optimal set of model weights $\mathbf{w}$ on a set of training data. There is no such solution to find the optimal weights for a logistic regression model, so instead an optimization algorithm such as **gradient descent** is used to train a LR model.

Gradient descent is an iterative optimization algorithm that searches for the optimum of an objective function by making small changes to a set of optimization variables. Gradient descent (and more complex optimization algorithms, but we offer a separate course for that) are widely used in machine learning to find the optimal set of model weights that minimize a certain loss function. Especially when there is no analytical solution for the weights available like for linear regression.

Generally, gradient descent uses the **gradient** of the loss function with respect to the model weights to perform updates to those weights in each iteration. At iteration $k+1$, the algoritm computes the gradient of a loss function $J(\mathbf{w})$ evaluated in the training data. Then, it performs an update to the current parameter values that is relative to the gradient multiplied with the learning rate $\gamma$, which is a constant:

\begin{equation}
\mathbf{w}_{k+1} = \mathbf{w}_{k} - \gamma\nabla{J(\mathbf{w}_{k})}
\end{equation}

Initially, the weights are often initialized with random draws from some distribution. The algorithm continues to do updates, until it converges or until some stopping criterion is reached.

In order to perform gradient descent to find the weights of a logistic regression model, we need to compute the gradient of the loss function with respect to the model parameters. Recall that, for a single data point, the cross-entropy loss function was as follows:

\begin{equation}
l_{log}^{i}(\mathbf{w}) = -y_{i}log(p(\mathbf{x}_i))-(1-y_i)log(1-p(\mathbf{x}_i))
\end{equation}

Where $p(\mathbf{x}_i)$ is nothing else than the weighted sum of the inputs squashed through the sigmoid function:

\begin{equation}
p(\mathbf{x}_i) = \phi(w_{0}x_{0i} + w_{1}x_{1i} + ... + w_{p}x_{pi})
\end{equation}

Before going on, let's first calculate the partial derivative of the sigmoid function:

\begin{equation}
\frac{\partial}{\partial z} \phi(z) = \frac{\partial}{\partial z} \frac{1}{1+e^{-z}} = \frac{e^{-z}}{(1+e^{-z})^2}
\end{equation}

We can rewrite this as follows:

\begin{equation}
\frac{e^{-z}}{(1+e^{-z})^2} = \frac{1 +e^{-z} -1}{(1+e^{-z})^2} = \frac{1}{1+e^{-z}} \Big( 1 - \frac{1}{1+e^{-z}}\Big) = \phi(z)(1 - \phi(z))
\end{equation}

With this result and by applying the chain rule, we can compute the partial derivative of the loss function with respect to the weight $w_j$. We will use the symbol $z$ to denote the weighted sum of the features (i.e., the input for the logistic function) and drop the superscript $i$ for clarity:

\begin{equation}
\frac{\partial l_{log}(\mathbf{w})}{\partial w_j} = \frac{\partial}{\partial w_j} \Big(-ylog(\phi(z))-(1-y)log(1-\phi(z)) \Big) \\ = \Big(  \frac{-y}{\phi(z)} + \frac{1-y}{1-\phi(z)}  \Big)\frac{\partial}{\partial w_j}\phi(z) \\ = \Big(  \frac{-y}{\phi(z)} + \frac{1-y}{1-\phi(z)}  \Big) \phi(z)(1-\phi(z))\frac{\partial}{\partial w_j}z
\end{equation}

Since $z = w_{0}x_{0} + w_{1}x_{1} + ... + w_{p}x_{p}$, $\frac{\partial}{\partial w_j}z$ is nothing more than $x_j$, so we can rewrite the above as:

\begin{equation}
\frac{\partial l_{log}(\mathbf{w})}{\partial w_j} = \Big( -y(1-\phi(z) + (1-y)\phi(z))\Big)x_j \\ = \big(  -y + \phi(z) \big)x_j = \big( \phi(z) - y \big)x_j
\end{equation}

With this partial derivative of the loss w.r.t $w_j$, we can write the update rule of the gradient descent algorithm for the $j^{th}$ weight:

\begin{equation}
w_{j,k+1} = w_{j,k} - \gamma(\phi(z_k)-y)x_{j}
\end{equation}

In other words, the algorithm will each time perform an update to the weight $w_{j}$ that is in proportion to the difference between the predicted probability of class membership in the previous iteration and the actual class. Makes sense! The entire gradient is simply the vector that contains the partial derivatives with respect to the entire weight vector $\mathbf{w}$, and in reality gradient descent acts on $\mathbf{w}$ and not on an individual weight $w_j$. Also, the gradient is typically not calculated for one data point, but evaluated over the entire training data set.

In practice, software packages such as [scikit-learn](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegressionscikit-learn) do this optimization under the hood, so there is no need to implement it manually each time we want to use logistic regression.

## Application: predicting the status of a breast cancer tumor

In the first application of logistic regression, we will use the [Breast Cancer Wisconsin (Diagnostic) Data Set](https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+%28Diagnostic%29). The dataset contains information on the disease status of 569 breast cancer patients: they were either diagnosed with a malign (status M) or with a benign (status B) tumor.

For each patient, the dataset also contains 30 features that represent statistics of the cell nuclei present in images taken after [fine needle aspirate tissue samples](https://en.wikipedia.org/wiki/Fine-needle_aspiration). These 30 features are the mean, standard deviation and the maximum of 10 measurements on the cell nuclei:
- radius
- texture
- perimeter
- area
- smoothness
- compactness
- concavity
- concave points
- symmetry
- fractal dimension

**Based on these feature of the cell nuclei, we would like to predict whether a patient has a malign or a benign breast cancer tumor.** Let's read in the data:

```python
import pandas as pd
import numpy as np
data = pd.read_csv('./wdbc.data', header=None, index_col=0, names=['Patient ID', 'status'] + list(np.arange(1,31,1)))
status = data['status']
data.head()
```

      status
      1
      2
      3
      4
      5
      6
      7
      8
      9
      ...
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30

      Patient ID

      842302
      M
      17.99
      10.38
      122.80
      1001.0
      0.11840
      0.27760
      0.3001
      0.14710
      0.2419
      ...
      25.38
      17.33
      184.60
      2019.0
      0.1622
      0.6656
      0.7119
      0.2654
      0.4601
      0.11890

      842517
      M
      20.57
      17.77
      132.90
      1326.0
      0.08474
      0.07864
      0.0869
      0.07017
      0.1812
      ...
      24.99
      23.41
      158.80
      1956.0
      0.1238
      0.1866
      0.2416
      0.1860
      0.2750
      0.08902

      84300903
      M
      19.69
      21.25
      130.00
      1203.0
      0.10960
      0.15990
      0.1974
      0.12790
      0.2069
      ...
      23.57
      25.53
      152.50
      1709.0
      0.1444
      0.4245
      0.4504
      0.2430
      0.3613
      0.08758

      84348301
      M
      11.42
      20.38
      77.58
      386.1
      0.14250
      0.28390
      0.2414
      0.10520
      0.2597
      ...
      14.91
      26.50
      98.87
      567.7
      0.2098
      0.8663
      0.6869
      0.2575
      0.6638
      0.17300

      84358402
      M
      20.29
      14.34
      135.10
      1297.0
      0.10030
      0.13280
      0.1980
      0.10430
      0.1809
      ...
      22.54
      16.67
      152.20
      1575.0
      0.1374
      0.2050
      0.4000
      0.1625
      0.2364
      0.07678

5 rows × 31 columns

First, let's look at the distribution of the disease status:

```python
pd.value_counts(data['status']).plot(kind='bar');
```

There are about 350 benign cases and roughly 200 malign cases. This is a fairly balanced dataset.

EXERCISE: Suppose that the dataset was unbalanced, with 525 B cases and only 25 M cases. Can you think of any problems this could give if we would evaluate the accuracy of our logistic regression predicitions? We will come back to this problem in one of the next labs.

In order to perform LR, we will encode the disease status as a binary variable.

```python
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder().fit(status)
encoder.classes_  # 'B' will become class 0, 'M' will become class 1
```

    array(['B', 'M'], dtype=object)

```python
y = encoder.transform(status)
x = data.drop('status', axis=1).values # Drop the disease status from the dataframe, convert to numpy array
y
```

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

EXERCISE: Using scikit-learn, split the data in a 80% training and a 20% test set. Fit a logistic regression model and evaluate trainig and testing accuracy. You should be able to achieve a fairly high accuracy!

Use [this method](http://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) for train-test splitting and [this implementation](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) to perform logistic regression. You can use the [score method](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegression.score) to evaluate the accuracy of your model. This method computes the accuracy as follows:

\begin{equation}
score = \frac{\text{Number of correctly classified instances}}{\text{Total number of instances}}
\end{equation}

```python
# ** solution **
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.20, random_state=42)

LRmodel = LogisticRegression()
LRmodel.fit(X_train, y_train)

LRmodel.score(X_train, y_train)

```

    /home/gaeta/miniconda3/envs/SynBio/lib/python3.7/site-packages/sklearn/linear_model/_logistic.py:765: ConvergenceWarning: lbfgs failed to converge (status=1):
    STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

    Increase the number of iterations (max_iter) or scale the data as shown in:
        https://scikit-learn.org/stable/modules/preprocessing.html
    Please also refer to the documentation for alternative solver options:
        https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
      extra_warning_msg=_LOGISTIC_SOLVER_CONVERGENCE_MSG)

    0.9538461538461539

```python
LRmodel.score(X_test, y_test)
# ** solution **
```

    0.956140350877193

To get an idea of which features are considered important by the LR model, we can visualize the weights it has learned in a bar plot:

```python
fig, ax = plt.subplots(figsize=(10,5))
pd.Series(LRmodel.coef_.flatten()).plot(ax=ax, kind='bar')
```

Use your LR model to predict the class probabilities and the classes for the training data. Use the [```predict_proba()```](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegression.predict_proba) method to generate the predicted probabilities. Use the code below to plot the two against each other. Which data points are most likely to be misclassified?

```python
# ** solution **
predicted_class_probabilities = LRmodel.predict_proba(X_train)[:,1]
predicted_classes = LRmodel.predict(X_train)

#** solution **

misclassified = predicted_classes !=  y_train

colors = ['#b2182b' if wrong else '#2166ac' for wrong in misclassified ]
fig, ax = plt.subplots(figsize=(8,6))
ax.scatter(predicted_class_probabilities, predicted_classes, marker='.', s=100, color=colors)
ax.set_xlabel('Predicted class probabilies').set_fontsize(20)
ax.set_ylabel('Predicted classes').set_fontsize(20)
ax.legend(['Correctly classified'])
```

Clearly, the misclassified points are those points where the predicted probability of class membership is rather close to 0.5.

# Multiclass classification

## One-versus-one classification

One-versus-one classification is another approach to a multiclass classification problem. For a K-class problem, the strategy consists of training $\frac{K(K-1)}{2}$ classifiers. Each of these classifiers much learn to distinguish to classes. One the classifiers are trained, a voting scheme is applied to make a prediction for an unseen data point: each classifier has to decide between two possible classes. The final predicted class is that class that gets the largest number of votes.

## One-versus-all classification

In one-versus-all (OvA) classification, a single classifier is trained per class, with the samples of that class as positive samples and all other samples as negatives. The strategy proceeds as follows for a K-class classification problem:

**Inputs:**
* a classification algorithm L (learner)
* feature matrix $\mathbf{X}$
* label vector y where $y_i \in {1,...,K}$

**Procedure:**
for each k in {1,...,K}:
* construct a new label vector z where $z_i$ is 1 if $y_i$ = k and 0 otherwise
* train L on $\mathbf{X}$ to obtain a classifier $f_k$. The classifier should return class probabilities and not hard labels.

**Returns**
A list of trained classifiers $f_k$ for each  k in {1,...,K}

To make predictions for a new sample $\mathbf{x}$, the $k$ classifiers are applied to $\mathbf{x}$ and the final predicted label is the label that is predicted with the highest confidence (probability):

$\hat{y} = \underset{k \in {1,...,K}}{\mathrm{argmax}} \, f_k(\mathbf{x})$

Let's simulate a toy dataset with three classes and two features, and split it in training and test data:

```python
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split

X, y = make_blobs(n_samples=1000, centers= [[-2.5, 0], [0, 1], [3.5, -1]], random_state=42)

#train-test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
```

```python
# Make the plot
fig, ax = plt.subplots(figsize=(15,10))
colors=['#66c2a5', '#fc8d62', '#8da0cb']
for i, color in enumerate(colors):
    idx_train = np.where(y_train==i)
    idx_test = np.where(y_test==i)
    plt.scatter(X_train[idx_train,0], X_train[idx_train,1], c=color, edgecolor='black', s=30)
    plt.scatter(X_test[idx_test,0], X_test[idx_test, 1],c='white', edgecolor=color, s=70)

ax.legend(['Class 1 - train',
           'Class 1 - test',
           'Class 2 - train',
           'Class 2 - test',
           'Class 3 - train',
           'Class 3 - test']);

ax.set_xlabel('Feature 1');
ax.set_ylabel('Feature 2');
ax.set_title('Toy dataset for multiclass classification').set_fontsize(20);
```

```python
i = 0
```

```python
z_train = np.zeros(len(y_train))
z_train[np.where(y_train==i)] = 1
```

```python
z_train
```

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

Implement a one-versus-all loop to tackle this classification problem. Train a list of classifiers on the training data. Make predictions on the test data. You can use the code below to get started.

```python
# ***solution***

L1 = LogisticRegression()
L2 = LogisticRegression()
L3 = LogisticRegression()

L = [L1, L2, L3]

# Train the list of classifiers in one-v-all fashion
for i,l in enumerate(L):
    z_train = (y_train==i)
    l.fit(X_train, z_train)

# Make predictions on the test data

predictions = []

for l in L:
    predictions.append(l.predict_proba(X_test)[:,1])

predicted_classes = np.array([np.argmax([pred[i] for pred in predictions]) for i in range(len(X_test))])

# ***solution***
```

Run the code below to visualize your predictions.

```python
classification_accuracy=np.round(np.mean(y_test == predicted_classes)*100,2)
```

```python
# Visualize the predictions

fig, ax = plt.subplots(figsize=(15,10))
colors=['#66c2a5', '#fc8d62', '#8da0cb']

for i, color in enumerate(colors):
    idx_train = np.where(y_train==i)
    idx_test = np.where(y_test==i)

    plt.scatter(X_train[idx_train,0], X_train[idx_train,1], c=color, edgecolor='black', s=30)
    plt.scatter(X_test[idx_test,0], X_test[idx_test, 1],c='white', edgecolor=color, s=70)

ax.legend(['Class 1 - train',
           'Class 1 - test',
           'Class 2 - train',
           'Class 2 - test',
           'Class 3 - train',
           'Class 3 - test']);

# add predictions
for i, color in enumerate(colors):
    idx_predicted = np.where(predicted_classes==i)
    plt.scatter(X_test[idx_predicted,0], X_test[idx_predicted,1], c=color, marker='s', s=2)

ax.set_xlabel('Feature 1');
ax.set_ylabel('Feature 2');
ax.set_title('Toy dataset for multiclass classification - classification accuracy: {}%'.format(classification_accuracy)).set_fontsize(20);
```

```python

```

```python

```