# Bab 5 Waffle dan Multivariate Regression

Pada bab sebelumnya sudah dijelaskan bangaimana membangun regresi menggunakan bayesian. Kasus pada bab sebelumnya adalah menggunakan berat badan sebagai variabel terobservasi dan menentukan tinggi dari berat badan. Sekarang akan dijelaskan kausalitas (apakah suatu variabel kontrol berhubungan dengan variabel terikat.

Tentuunya pada kasus tinggi badan, relasi dapat dlilihat dengan mudah dan cukup intuitif. Tentunya hal ini tidak berlaku untuk penelitan-penelitian lainya seperti hubungan antara biaya promosi dan jumlah sales atau efek obat yang baru dibuat terhadap suatu penyakit.

Dikarenakan dalam suatu penelitian seringkali didapati variabel observasi bermacam-macam, maka regresi menggunakan lebih dari satu jenis variabel akan sering dijumpai.

Pada bukunya, Pak Richard menejelaskan kenapa regresi multivariate ini penting.
1. adanya *confound* dalam statistik. *confound* ini dapat dijelaskan sebagai susatu sebab musabab yang menyesatkan. Seperti banyaknya rumah penyaji waffle sebagai penyebab perceraian di Amerika Setikat. Ini dapat menyembunyikan variabel yang benar-benar berpengaruh dan memunculkan variabel yang sebenarnya tidak memiliki pengaruh apapun tapi berdasarkan data terlihat berpengaruh.

2. Banyaknya fenomena yang disebabkan oleh berbagai kasus. Ini sangat mungkin terjadi dalam penelitian soosial. Mungkin saat variabel-variabel ini dipisahkan dan dibuat model sendiri tidak memiliki pengharuh, akan tetapi saat digabungkan, efeknya akan teramplifikasi. Begitu juga sebaliknya, mungkin ada variabel yang apabila sendiri sangant berpengatuh tetapi saat digabugnkan dengan variabel lainya akan saling mengatenuasi.

3. Interaksi antar varaibel juga berpengaruh seperti yang dijelaskan pada poin kedua.

Pada bab ini akan dijelaskan tentang *confound* dalam data.

### Asosiasi Palsu

```python
import pymc3 as pm
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as stats
%matplotlib inline

class LinearModelling:
    def __init__(self, dataframe):
        self.df = dataframe
        self.feature = []
        self.trace_df = None
        self.model = None

        self.fig = None
        self.ax = None

        self.valid_distribution = ['normal', 'uniform']

        self.config_validation_map = {
            "intercept": self.__validate_config_intercept,
            "gradient": self.__validate_config_gradient,
            "prior": self.__validate_config_prior,
            "equation": self.__validate_config_equation,
            "result": self.__validate_config_result
        }

        self.validate_distribution_map = {
            "normal": self.__validate_distribution_normal,
            "uniform": self.__validate_distribution_uniform
        }

        self.model_distribution_map = {
            "normal": self.__construct_distribution_normal,
            "uniform": self.__construct_distribution_uniform,
        }

        self.generate_distribution_map = {
            "normal": self.__generate_random_variable_normal
        }

        self.equation_support = ['linear']

    # Public
    def add_feature(self, column: str) -> 'void':
        self.feature.append(self.df[column])
        return

    def add_model(self, model: 'pymc3_model'):
        self.model = model

    def set_model_config(self, config):
        self.__validate_config(config)
        self.config = config

    def get_current_config(self):
        return self.config

    def construct_linear_model(self):
        self.model_buffer = {}
        with pm.Model() as model:
            for variable in self.config.keys():
                self.model_buffer[variable] = self.__convert_config_to_model(variable)

        self.model = model

    def get_sample(self, number_sample, tune=1000):
        if self.model is None:
            raise Exception("Model Not constructed yet; use construct_model method first")

        with self.model:
            trace_model = pm.sample(number_sample, tune=tune)
            self.trace_model = trace_model
            self.trace_df = pm.trace_to_dataframe(trace_model)

    def get_trace_dataframe(self):
        if self.trace_df is None:
            raise Exception("Trace is not available; use get_sample method first")
        return self.trace_df

    def plot_trace_information(self):
        with self.model:
            az.plot_trace(self.trace_model)

    def plot_full(self):
        self.__init_plot()
        gradient_variables = self.__find_gradient_variables()
        result_variables = self.__find_result_variables()
        if len(gradient_variables) > 1 or len(result_variables) > 1:
            raise Exception("Gradient Variable or Result variable is more than one; use specific_plot instead")
        self.__plot_scatter(gradient_variables[0], result_variables[0])
        self.__plot_mean_regression(gradient_variables[0])
        self.__plot_mean_hdi(gradient_variables[0])
        self.__plot_mean_actual_hdi(gradient_variables[0])

    # Private
    def __init_plot(self):
        fig, ax = plt.subplots()
        fig.set_figwidth(16)
        fig.set_figheight(12)

        self.fig = fig
        self.ax = ax

    def __plot_scatter(self, gradient_variable, result_variable):
        x_axis_var = self.config[gradient_variable]['feature']
        y_axis_var = self.config[result_variable]['observed']
        x_data = self.df[x_axis_var]
        y_data = self.df[y_axis_var]
        self.ax.scatter(x_data, y_data, alpha=.1)
        return

    def __plot_mean_regression(self, gradient_variable):
        feature = self.config[gradient_variable]['feature']
        x_axis = np.sort(self.df[feature])
        intercept_variable = self.__find_intercept_variables()[0]
        y_axis = self.trace_df[intercept_variable].mean() + self.trace_df[gradient_variable].mean() * x_axis
        self.ax.plot(x_axis, y_axis)

    def __plot_mean_hdi(self, gradient_variable):
        low_limit, high_limit = self.__get_hdi_mean_boundaries(gradient_variable)
        x_axis = np.sort(self.df[self.config[gradient_variable]['feature']])
        self.ax.fill_between(x_axis, low_limit, high_limit, alpha=.2, color='black')

    def __plot_mean_actual_hdi(self, gradient_variable):
        low_limit, high_limit = self.__get_hdi_actual_boundaries(gradient_variable)
        x_axis = np.sort(self.df[self.config[gradient_variable]['feature']])
        self.ax.fill_between(x_axis, low_limit, high_limit, alpha=.2, color='black')

    def __get_hdi_mean_boundaries(self, gradient_variable):
        intercept_variable = self.__find_intercept_variables()[0]
        feature = self.config[gradient_variable]['feature']
        lower_bound, upper_bound = [], []
        for i in np.sort(self.df[feature]):
            result = self.trace_df[intercept_variable] + self.trace_df[gradient_variable] * i
            [low_limit, high_limit] = az.hdi(np.array(result), hdi = .89)
            lower_bound.append(low_limit)
            upper_bound.append(high_limit)
        return lower_bound, upper_bound

    def __get_hdi_actual_boundaries(self, gradient_variable):
        intercept_variable = self.__find_intercept_variables()[0]
        feature = self.config[gradient_variable]['feature']
        lower_bound, upper_bound = [], []
        result_config = self.config[self.__find_result_variables()[0]]
        result_distribution_type = result_config['distribution']
        for i in np.sort(self.df[feature]):
            result = self.trace_df[intercept_variable] + self.trace_df[gradient_variable] * i
            dist_prop = {'loc': result, 'scale': self.trace_df[result_config['sd']]}
            likelihood = self.generate_distribution_map[result_distribution_type](**dist_prop)
            [low_limit, high_limit] = az.hdi(np.array(likelihood), hdi = .89)
            lower_bound.append(low_limit)
            upper_bound.append(high_limit)
        return lower_bound, upper_bound

    def __generate_random_variable_normal(self, **dist_prop):
        return stats.norm.rvs(**dist_prop)

    def __convert_config_to_model(self, variable_name):
        config = self.config[variable_name]
        print("BUILDING", config)
        if config['post'] == 'equation':
            return self.__construct_deterministic(variable_name)
        else:
            return self.model_distribution_map[config['distribution']](config, variable_name)

    def __construct_distribution_normal(self, config, variable_name):
        mu, sd = config['mu'], config['sd']
        if type(config['mu']) == str:
            mu = self.model_buffer[mu]
        if type(config['sd']) == str:
            sd = self.model_buffer[sd]
        print(variable_name, mu, sd)
        keyword = {
            "mu": mu,
            "sd": sd,
        }
        try:
            keyword['observed'] = self.df[config['observed']]
        except KeyError:
            pass
        return pm.Normal(variable_name, **keyword)

    def __construct_distribution_uniform(self, config, variable_name):
        lower, upper = config['lower'], config['upper']
        if lower == str:
            lower = self.model_buffer[lower]
        if upper == str:
            upper = self.model_buffer[uppper]
        return pm.Uniform(variable_name, lower = lower, upper = upper)

    def __construct_deterministic(self, variable_name):
        base_eq = self.__find_intercept_model()
        for gradient_variable in self.__find_gradient_variables():
            base_eq += self.model_buffer[gradient_variable] * self.df[self.config[gradient_variable]['feature']]

        try:
            is_deterministic = self.__get_options_in_variable(variable_name, 'is_deterministic')
            if is_deterministic:
                return pm.Deterministic(variable_name, base_eq)
            else:
                return base_eq
        except KeyError:
            return base_eq

    def __find_intercept_model(self):
        for variable_name in self.config.keys():
            if self.config[variable_name]['post'] == 'intercept':
                return self.model_buffer[variable_name]
        raise Exception("No intercept exist in config")

    def __find_gradient_variables(self):
        gradient_variables = []
        for variable_name in self.config.keys():
            if self.config[variable_name]['post'] == 'gradient':
                gradient_variables.append(variable_name)
        return gradient_variables

    def __find_intercept_variables(self):
        intercept_variables = []
        for variable_name in self.config.keys():
            if self.config[variable_name]['post'] == 'intercept':
                intercept_variables.append(variable_name)
        return intercept_variables

    def __find_result_variables(self):
        result_variables = []
        for variable_name in self.config.keys():
            if self.config[variable_name]['post'] == 'result':
                result_variables.append(variable_name)
        return result_variables

    def __get_options_in_variable(self, variable_name, option):
        options = self.config[variable_name]['options']
        return options[option]

    def __validate_distribution_normal(self, main_config, config):
        if "mu" not in config.keys():
            raise Exception("Normal distribtion require mu -- average")
        if "sd" not in config.keys():
            raise Exception("Normal distribtion require sd -- standard deviation")

        if type(config['mu']) == str:
            self.__validate_variable_existance(main_config, config['mu'])
        if type(config['sd']) == str:
            self.__validate_variable_existance(main_config, config['sd'])

    def __validate_distribution_uniform(self, main_config, config):
        if "lower" not in config.keys():
            raise Exception("Normal distribtion require lower boundaries")
        if "upper" not in config.keys():
            raise Exception("Normal distribtion require upper boundaries")

        if type(config['lower']) == str:
            self.__validate_variable_existance(main_config, config['lower'])
        if type(config['upper']) == str:
            self.__validate_variable_existance(main_config, config['upper'])

    def __validate_distribution_support(self, config):
        if config['distribution'] not in self.valid_distribution:
            raise Exeception("Wrong Config for intercept -- detected in {}".format(config.post))

    def __validate_variable_exist(self, variable):
        if variable not in self.df.columns:
            raise Exception("Variable does not exists in provided dataframe")

    def __validate_equation_support(self, equation):
        if equation not in self.equation_support:
            raise Exception("Equation {} not supported".format(equation))

    def __validate_equation_variable(self, main_config, variables):
        for variable in variables:
            if variable not in main_config.keys():
                raise Exception("Equation need {} but does not provided".format(variable))

    def __validate_variable_existance(self, main_config, variable_name):
        if variable_name not in main_config.keys():
            raise Exception("Result require non exist variable; want {} available {}".format(variable_name, main_config.keys()))

    def __validate_config(self, config):
        for key in config.keys():
            self.config_validation_map[config[key]['post']](config, config[key])

    def __validate_config_intercept(self, main_config, config):
        self.__validate_distribution_support(config)
        self.validate_distribution_map[config['distribution']](main_config, config)

    def __validate_config_gradient(self, main_config, config):
        self.__validate_distribution_support(config)
        self.validate_distribution_map[config['distribution']](main_config, config)
        self.__validate_variable_exist(config['feature'])

    def __validate_config_prior(self, main_config, config):
        self.__validate_distribution_support(config)
        self.validate_distribution_map[config['distribution']](main_config, config)

    def __validate_config_equation(self, main_config, config):
        self.__validate_equation_support(config['model'])
        self.__validate_equation_variable(main_config, config['variable'])

    def __validate_config_result(self, main_config, config):
        self.__validate_distribution_support(config)
        self.validate_distribution_map[config['distribution']](main_config, config)
```

    WARNING (theano.tensor.blas): Using NumPy C-API based implementation for BLAS functions.

```python
asd = {
        "alpha": {
            "post": "intercept",
            "distribution": "normal",
            "mu": 178,
            "sd": 10
        },
        "beta": {
            "post": "gradient",
            "distribution": "normal",
            "mu": 0,
            "sd": 10,
            "feature": "weight_c"
        },
        "sigma": {
            "post": "prior",
            "distribution": "uniform",
            "lower": 0,
            "upper": 50
        },
        "mu": {
            "post": "equation",
            "model": "linear",
            "variable": ["alpha", "beta"],
            "options": {
                "is_deterministic": False
            }
        },
        "final": {
            "post": "result",
            "distribution": "normal",
            "mu": "mu",
            "sd": "sigma",
            "observed": "height"
        }
    }
```

```python
import pandas as pd
data_url = "https://raw.githubusercontent.com/pymc-devs/resources/master/Rethinking/Data/Howell1.csv"
df_howling = pd.read_csv(data_url, sep=";")
df_howling = df_howling.assign(weight_c = df_howling['weight'] - df_howling['weight'].mean())
new_model = LinearModelling(df_howling)
```

```python
new_model.set_model_config(asd)
```

```python
new_model.get_current_config()
```

    {'alpha': {'post': 'intercept', 'distribution': 'normal', 'mu': 178, 'sd': 10},
     'beta': {'post': 'gradient',
      'distribution': 'normal',
      'mu': 0,
      'sd': 10,
      'feature': 'weight_c'},
     'sigma': {'post': 'prior',
      'distribution': 'uniform',
      'lower': 0,
      'upper': 50},
     'mu': {'post': 'equation',
      'model': 'linear',
      'variable': ['alpha', 'beta'],
      'options': {'is_deterministic': False}},
     'final': {'post': 'result',
      'distribution': 'normal',
      'mu': 'mu',
      'sd': 'sigma',
      'observed': 'height'}}

```python
new_model.construct_linear_model()
```

    BUILDING {'post': 'intercept', 'distribution': 'normal', 'mu': 178, 'sd': 10}
    alpha 178 10
    BUILDING {'post': 'gradient', 'distribution': 'normal', 'mu': 0, 'sd': 10, 'feature': 'weight_c'}
    beta 0 10
    BUILDING {'post': 'prior', 'distribution': 'uniform', 'lower': 0, 'upper': 50}
    BUILDING {'post': 'equation', 'model': 'linear', 'variable': ['alpha', 'beta'], 'options': {'is_deterministic': False}}
    BUILDING {'post': 'result', 'distribution': 'normal', 'mu': 'mu', 'sd': 'sigma', 'observed': 'height'}
    final Elemwise{add,no_inplace}.0 sigma

```python
new_model.get_sample(1000)
```

    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Sequential sampling (2 chains in 1 job)
    NUTS: [sigma, beta, alpha]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:03<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

    Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 6 seconds.

```python
new_model.get_trace_dataframe()
```

      alpha
      beta
      sigma

      0
      137.727385
      1.728999
      9.604748

      1
      139.137279
      1.791867
      9.276072

      2
      138.788249
      1.805892
      9.600603

      3
      139.197692
      1.804551
      9.504207

      4
      137.630086
      1.767867
      9.351337

      ...
      ...
      ...
      ...

      1995
      137.986899
      1.741564
      9.339632

      1996
      138.876213
      1.793811
      9.303820

      1997
      137.971784
      1.729186
      9.602431

      1998
      138.603704
      1.730262
      9.220242

      1999
      138.267855
      1.803380
      9.219039

2000 rows × 3 columns

```python
new_model.plot_trace_information()
```

```python
new_model.plot_full()
```

```python
import pandas as pd
```

```python
try:
    print(df.describe().T)
except NameError:
    raw_link = "https://raw.githubusercontent.com/pymc-devs/resources/master/Rethinking/Data/WaffleDivorce.csv"
    df = pd.read_csv(raw_link, sep=';')
    print(df.describe())

df['MedianAgeMarriage_s'] = (df['MedianAgeMarriage'] - df['MedianAgeMarriage'].mean()) / df['MedianAgeMarriage'].std()
df['Divorce_s'] = (df['Divorce'] - df['Divorce'].mean()) / df['Divorce'].std()
df['Marriage_s'] = (df['Marriage'] - df['Marriage'].mean()) / df['Marriage'].std()
```

           Population  MedianAgeMarriage   Marriage  Marriage SE    Divorce  \
    count   50.000000           50.00000  50.000000    50.000000  50.000000
    mean     6.119600           26.05400  20.114000     1.399400   9.688000
    std      6.876156            1.24363   3.797905     0.796975   1.820814
    min      0.560000           23.20000  13.500000     0.390000   6.100000
    25%      1.635000           25.32500  17.125000     0.810000   8.300000
    50%      4.435000           25.90000  19.700000     1.185000   9.750000
    75%      6.677500           26.75000  22.100000     1.767500  10.900000
    max     37.250000           29.70000  30.700000     3.920000  13.500000

           Divorce SE  WaffleHouses      South     Slaves1860  Population1860  \
    count   50.000000     50.000000  50.000000      50.000000    5.000000e+01
    mean     0.961800     32.340000   0.280000   79378.340000    6.287293e+05
    std      0.525367     65.789587   0.453557  149730.921149    7.813127e+05
    min      0.240000      0.000000   0.000000       0.000000    0.000000e+00
    25%      0.572500      0.000000   0.000000       0.000000    4.332100e+04
    50%      0.795000      1.000000   0.000000       0.000000    4.077220e+05
    75%      1.257500     39.750000   1.000000   80828.000000    9.209770e+05
    max      2.500000    381.000000   1.000000  490865.000000    3.880735e+06

           PropSlaves1860
    count       50.000000
    mean         0.094051
    std          0.174449
    min          0.000000
    25%          0.000000
    50%          0.000000
    75%          0.085000
    max          0.570000

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

import pymc3 as pm
import arviz as az
import numpy as np
import seaborn as sns
```

```python
def get_hdi_in_point(alpha, beta, x):
    result = alpha + beta * x
    [low_limit, high_limit] = az.hdi(np.array(result), hdi=.89)
    return low_limit, high_limit
```

```python
with pm.Model() as m510:
    alpha = pm.Normal('alpha', mu=0, sd= 0.2)
    beta = pm.Normal('beta', mu=0, sd= 0.5)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha + beta * df['Marriage_s'])
    result = pm.Normal('result', mu=mu, sigma=sigma, observed=df['Divorce_s'])

with m510:
    trace_m510 = pm.sample(1000, tune=1000)
    df_trace_m510 = pm.trace_to_dataframe(trace_m510)

with pm.Model() as m511:
    alpha = pm.Normal('alpha', mu=0, sd= 0.2)
    beta = pm.Normal('beta', mu=0, sd= 0.5)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha + beta * df['MedianAgeMarriage_s'])
    result = pm.Normal('result', mu=mu, sigma=sigma, observed=df['Divorce_s'])

with m511:
    trace_m511 = pm.sample(1000, tune=1000)
    df_trace_m511 = pm.trace_to_dataframe(trace_m511)
```

    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Sequential sampling (2 chains in 1 job)
    NUTS: [sigma, beta, alpha]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:02<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

    Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 5 seconds.
    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Sequential sampling (2 chains in 1 job)
    NUTS: [sigma, beta, alpha]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

    Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 4 seconds.

```python
fig, [ax1, ax2] = plt.subplots(nrows=1, ncols=2)
fig.set_figwidth(12)
fig.set_figheight(6)

ax1.scatter(df['Marriage_s'], df['Divorce_s'], marker='x')
ax1.set_xlabel('Marriage')
ax1.set_ylabel('Divorce')

ax2.scatter(df['MedianAgeMarriage_s'], df['Divorce_s'], marker='x')
ax2.set_xlabel('MedianAgeMarriage')
ax2.set_ylabel('Divorce')

x_axis = np.sort(df['Marriage_s'])
y_axis = df_trace_m510['alpha'].mean() + df_trace_m510['beta'].mean() * np.sort(df['Marriage_s'])
ax1.plot(x_axis, y_axis, color='black')

low_line = []
up_line = []
for i in x_axis:
    [low_limit, high_limit] = get_hdi_in_point(df_trace_m510['alpha'], df_trace_m510['beta'], i)
    low_line.append(low_limit)
    up_line.append(high_limit)

ax1.fill_between(x_axis, low_line, up_line, alpha=.1)
ax1.grid(b=True)

x_axis = np.sort(df['MedianAgeMarriage_s'])
y_axis = df_trace_m511['alpha'].mean() + df_trace_m511['beta'].mean() * np.sort(df['MedianAgeMarriage_s'])
ax2.plot(x_axis, y_axis, color='black')

low_line = []
up_line = []
for i in x_axis:
    [low_limit, high_limit] = get_hdi_in_point(df_trace_m511['alpha'], df_trace_m511['beta'], i)
    low_line.append(low_limit)
    up_line.append(high_limit)

ax2.fill_between(x_axis, low_line, up_line, alpha=.1)
ax2.grid(b=True)
```

Perubahan nilai variabel menggunakan standarisasi ($\mu=0$ dan $\sigma=1$) menyebabkan beberapa perubahan interpretasi. Nilai intercept $\alpha$ akan mendekati 0 karena adanya standarisasi. Apabila nilai $\beta=1$ berarti perubahan satu standar deviasi pada variabel x-axis (`Marriage` dan `MedianAgeMarriage`) adalah perubhan satu standar deviasi pada variabel y-axis (`Divorce`)

Membandingkan parameter dari dua regeresi tidak menentukan apakah suatu variabel dapat mejadi prediktor yang baik.

**Pikiran sebelum membangun regresi**
Disediakan tiga data $D$ sebagai jumlah perceraian, $M$ jumlah pernikahan, dan $A$ nilai tengah umur pernikahan. Untuk menjelaskan ketiga relasi antar variable, digunakan sesuatu yang dinamakan graph terutama graph asiklik dan terarah, Graph terarah ialah bentuk graph yang memiliki panah dari satu titik ke titik lain. Asiklik berarti setiap panah apabila ditelusuri tidak kembali ke titik itu berasal.

Graph akan membantu dalam menjelaskan suatu kausalitas suatu kejadian. Kausalitas yang dapat dibagun dari kasus diatas kurang lebih sebagai berikut $A \rightarrow M \rightarrow D \leftarrow A$

Ini akan membangun pertanyaan. Apabila $M$ terlihar berpengaruh terhadap $D$, apakah dikarenakan $A$ karena nilai $A$ berpengaruh langsung terhadap $D$?

Ini dapat dibedakan dengan membandingkan pengaruh $A \rightarrow D$ dan $M \rightarrow D$. Apabila pengaruh keduanya sama, maka mungkin? dapat disimpulkan bahwa nilai $M$ tidak berpengaruh terhadap $D$. Korelasi positif hanya dihasilkan  karena $A \rightarrow M$ saja.

**Menguji Implikasi**
Conditional Independecies adalah salah satu yang dapat diuji dalam mengukur implikasi suatu variabel terhadap hasil. Ada dua bagian dalam conditional independece, pertama adalah menentukan pernyataan apakah data dapat dimasukan kedalam model atau tidak. Kedua, menentukan apakah pernyataan apakah suatu variable tidak terasosiasi dengan variabel lainya.

Secara formal dapat ditulis sebagai
$$ Y \bot\bot X|Z $$

Ketiga variabel $A$, $M$, dan $D$ ini akan menjadi tema dalam bab ini.

### Regressi Multivariate

Regresi multivariate dapat dideskripsikan sebagai berikut

$$
\begin{align}
    D_i &\sim \text{Normal}(\mu_i, \sigma) &\text{[Probability of data target]} \\
    \mu_i &= \alpha + \beta_M M_i + \beta_A A_i &\text{[Linear Model]} \\
    \alpha &\sim \text{Normal}(0, 0.2) &\text{[Prior Intercept]} \\
    \beta_M &\sim \text{Normal}(0, 0.5) &\text{[Gradient untuk } M \text{]} \\
    \beta_A &\sim \text{Normal}(0, 0.5) &\text{[Gradient untuk } A \text{]} \\
    \sigma &\sim \text{Exponential}(1) &\text{[Prior untuk } \sigma \text{]} \\
    \end{align}
$$

betuk dari pemodelan multivariate ini hampir sama dengan pemodelan menggunakan model quadratic hanya saja suku terakhir dari sisi tangan kanan $\mu_i$ bukan lah nilai kuadrat tapi variabel lainya. Apakah implikasi menggunakan model linear yang terdiri dari lebih dari satu variabel prediktor?

Secara matematris, linear model dapat dibaca dengan
> Rearata perceraian pada satu negara bagian adalah hasil penjumlahan suatu nilai konstan $\alpha$, perkalian antara suatu nilai $\beta_M$ dan jumlah pernikahan pada negara bagian tersebut dan perkalian antara suatu nilai  $\beta_A$ dan nilai tengah umur pernikahan pada negara bagian tersebut.

Suatu nilai $\beta_M$ dan $\beta_A$ mengukur relasi antara variabel tersebut ($M$ dan $A$) dan target $D$.

```python
with pm.Model() as m53:
    alpha = pm.Normal('alpha', mu=0, sd= 0.2)
    beta_M = pm.Normal('beta_M', mu=0, sd= 0.5)
    beta_A = pm.Normal('beta_A', mu=0, sd=0.5)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha +
                          beta_M * df['Marriage_s'] +
                          beta_A * df['MedianAgeMarriage_s'])
    result = pm.Normal('result', mu=mu, sigma=sigma, observed=df['Divorce_s'])

with m53:
    trace_m53 = pm.sample(1000, tune=1000)
    df_trace_m53 = pm.trace_to_dataframe(trace_m53)
```

    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Sequential sampling (2 chains in 1 job)
    NUTS: [sigma, beta_A, beta_M, alpha]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:03<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:02<00:00 Sampling chain 1, 0 divergences]

    Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 6 seconds.

```python
var_array = ["alpha", "beta_M", "beta_A", "sigma"]
df_trace_m53[var_array].describe().round(2)
```

      alpha
      beta_M
      beta_A
      sigma

      count
      2000.00
      2000.00
      2000.00
      2000.00

      mean
      -0.00
      -0.06
      -0.60
      0.83

      std
      0.10
      0.15
      0.15
      0.09

      min
      -0.31
      -0.61
      -1.13
      0.61

      25%
      -0.07
      -0.16
      -0.70
      0.76

      50%
      -0.00
      -0.05
      -0.60
      0.82

      75%
      0.07
      0.04
      -0.50
      0.88

      max
      0.35
      0.49
      0.04
      1.21

```python
fig, [ax1, ax2] = plt.subplots(nrows=2, ncols=1)
fig.set_figwidth(12)
fig.set_figheight(12)

sns.distplot(df_trace_m510['beta'], label='Marriage Single',ax=ax1)
sns.distplot(df_trace_m53['beta_M'], label='Marriage Multi',ax=ax1)
ax1.legend()
ax1.grid(b=True)

sns.distplot(df_trace_m511['beta'], label='MedianMarriageAge Single', ax=ax2)
sns.distplot(df_trace_m53['beta_A'], label='MedianMarriageAge Multi', ax=ax2)
ax2.legend()
ax2.grid(b=True)
```

Plot diatas membandingkan antara regresi yang dilakukan saat single variable dan multivariate. Plot pertama membandingkan variabel jumlah pernikahan dan plot kedua membandingkan nilai tengah umur pernikahan. Ada perbedaan nilai rerata antara dua model pada plot pertama. Plot multivariate memiliki nilai rerata mendekati nol apabila dibandingkan dengan single variate. Ini menandakan bahwa pada model multivariate, nilai jumlah pernikahan $M$ tidak berpengaruh banyak.

Nilai tengah umur pernikahan $A$ memiliki rerata yang hampir sama. Perbedaan ada pada nilai standar deviasi. Standar deviasi pada single variate lebih kecil dibandingkan dengan multivariate. Ini menandakan bahwa regresi multivariate memiliki ketidakpastian lebih besar daripada regresi single variabel

## Plotting Multivariate Regression

Pak Richard mengajukan tiga jenis plot untuk memahami model multi variael. Ketiga plot tersebut adalah
1. *Predictor Residual Plot*. Memplot hasil dengan resildual dari variabel prediksi.
2. *Posterior Prediction Plot*. Membandingkan antara prediksi yang dihasilkan oleh model dan data yang tersedia
3. *Counterfactual Plots*. Menampilkan plot prediksi tentang sesuatu yang sebenarnya tidak terjadi berdasarkan model yang sudah dibangun

#### Predictor Residual Plot
Kasus bab ini memiliki dua variabel prediksi yaitu jumlah pernikahan $M$ dan nilai tengah umur pernikahan $A$. Salah satu variabel tersebut akan menjadi prediktor dan yang lainya akan menjadi terprediksi (feature dan label). Model ini akan menghasilkan sebuah regresi linear. Kedua variabel ini akan dibuat plotnya bersama regresi linear-nya. Kedua variabel ini dinormalisasi terlebih dahulu (menjadikan rerata pada masing-masing variabel adalah $0$).

Masing-masing variabel akan dibandingkan dengan jumlah perceraian $D$ yang menjadi variabel yang akan dicari. Dibuat model regresi dengan masing-masing variabel terhadap jumlah perceraian. Model yang akan dihasilkan akan sejumlah variabel yang terlibat. Pada kasus diatas, akan dibuat dua model.

```python
with pm.Model() as m540:
    alpha = pm.Normal('alpha', mu = 0, sd = 0.2)
    beta = pm.Normal('beta', mu = 0, sd = 0.5)
    sigma = pm.Exponential('sigma', lam = 1)
    mu = pm.Deterministic('mu', alpha + beta * df['Marriage_s'])
    result = pm.Normal('result', mu = mu, sigma = sigma, observed=df['MedianAgeMarriage_s'])

with m540:
    trace_m540 = pm.sample(1000, tune=1000)
    df_trace_m540 = pm.trace_to_dataframe(trace_m540)

with pm.Model() as m541:
    alpha = pm.Normal('alpha', mu = 0, sd = 0.2)
    beta = pm.Normal('beta', mu = 0, sd = 0.5)
    sigma = pm.Exponential('sigma', lam = 1)
    mu = pm.Deterministic('mu', alpha + beta * df['MedianAgeMarriage_s'])
    result = pm.Normal('result', mu = mu, sigma = sigma, observed=df['Marriage_s'])

with m541:
    trace_m541 = pm.sample(1000, tune=1000)
    df_trace_m541 = pm.trace_to_dataframe(trace_m541)
```

    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Sequential sampling (2 chains in 1 job)
    NUTS: [sigma, beta, alpha]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

    Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 4 seconds.
    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Sequential sampling (2 chains in 1 job)
    NUTS: [sigma, beta, alpha]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

    Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 4 seconds.

```python
df.head()
```

      Location
      Loc
      Population
      MedianAgeMarriage
      Marriage
      Marriage SE
      Divorce
      Divorce SE
      WaffleHouses
      South
      Slaves1860
      Population1860
      PropSlaves1860
      MedianAgeMarriage_s
      Divorce_s
      Marriage_s

      0
      Alabama
      AL
      4.78
      25.3
      20.2
      1.27
      12.7
      0.79
      128
      1
      435080
      964201
      0.45
      -0.606290
      1.654205
      0.022644

      1
      Alaska
      AK
      0.71
      25.2
      26.0
      2.93
      12.5
      2.05
      0
      0
      0
      0
      0.00
      -0.686699
      1.544364
      1.549802

      2
      Arizona
      AZ
      6.33
      25.8
      20.3
      0.98
      10.8
      0.74
      18
      0
      0
      0
      0.00
      -0.204241
      0.610716
      0.048974

      3
      Arkansas
      AR
      2.92
      24.3
      26.4
      1.70
      13.5
      1.22
      41
      1
      111115
      435450
      0.26
      -1.410387
      2.093569
      1.655123

      4
      California
      CA
      37.25
      26.8
      19.1
      0.39
      8.0
      0.24
      0
      0
      0
      379994
      0.00
      0.599857
      -0.927058
      -0.266989

```python
# How to shut pymc3 logging
import logging
logger = logging.getLogger('pymc3')
logger.setLevel(logging.ERROR)
```

```python
fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(nrows=2, ncols=2)
fig.set_figwidth(14)
fig.set_figheight(14)

ax1.scatter(df['Marriage_s'], df['MedianAgeMarriage_s'], marker='x')
x_axis = df.sort_values('Marriage_s')
p_residual = []
y_axis = []
for num, i in x_axis.iterrows():
    y_axis_result = df_trace_m540['alpha'].mean() + df_trace_m540['beta'].mean() * i['Marriage_s']
    y_axis.append(y_axis_result)
    ax1.vlines(i['Marriage_s'], y_axis_result, i['MedianAgeMarriage_s'], alpha=.7)
    ax1.annotate(i['Loc'], [i['Marriage_s'], i['MedianAgeMarriage_s']])
    p_residual.append(i['MedianAgeMarriage_s'] - y_axis_result)
ax1.set_ylabel('Age at marriage (std)')
ax1.set_xlabel('Marriage rate (std)')
ax1.set_title("Residual A vs M")
ax1.grid(b=True)
ax1.plot(x_axis['Marriage_s'], y_axis)

with pm.Model() as m542:
    alpha = pm.Normal('alpha', mu = 0, sd = 0.2)
    beta = pm.Normal('beta', mu = 0, sd = 0.5)
    sigma = pm.Exponential('sigma', lam = 1)
    mu = pm.Deterministic('mu', alpha + beta * p_residual)
    result = pm.Normal('result', mu = mu, sigma = sigma, observed=x_axis['Divorce_s'])
with m542:
    trace_m542 = pm.sample(1000, tune=1000, progressbar=False)
    df_trace_m542 = pm.trace_to_dataframe(trace_m542)

y_axis = []
low_line = []
up_line = []

for i in np.sort(p_residual):
    y_axis_result = df_trace_m542['alpha'].mean() + df_trace_m542['beta'].mean() * i
    [low_limit, high_limit] = get_hdi_in_point(df_trace_m542['alpha'], df_trace_m542['beta'], i)
    low_line.append(low_limit)
    up_line.append(high_limit)
    y_axis.append(y_axis_result)

ax3.scatter(p_residual, x_axis['Divorce_s'], marker='x')
ax3.set_xlabel('Residual Age at marriage (std)')
ax3.set_ylabel('Divorce Rate (std)')
ax3.plot(np.sort(p_residual), y_axis)
ax3.fill_between(np.sort(p_residual), low_line, up_line, alpha=.3)
ax3.set_title("Residual (A) vs Oberseved")
ax3.grid(b=True)

ax2.scatter(df['MedianAgeMarriage_s'], df['Marriage_s'], marker='x')
x_axis = df.sort_values('Marriage_s')
p_residual = []
y_axis = []
for num, i in x_axis.iterrows():
    y_axis_result = df_trace_m541['alpha'].mean() + df_trace_m541['beta'].mean() * i['MedianAgeMarriage_s']
    y_axis.append(y_axis_result)
    ax2.annotate(i['Loc'], [i['MedianAgeMarriage_s'], i['Marriage_s']])
    ax2.vlines(i['MedianAgeMarriage_s'], y_axis_result, i['Marriage_s'], alpha=.7)
    p_residual.append(i['Marriage_s'] - y_axis_result)
ax2.set_ylabel('Marriage Rate (std)')
ax2.set_xlabel('Age at marriage (std)')
ax2.plot(x_axis['MedianAgeMarriage_s'], y_axis)
ax2.set_title("Residual M vs A")
ax2.grid(b=True)

with pm.Model() as m543:
    alpha = pm.Normal('alpha', mu = 0, sd = 0.2)
    beta = pm.Normal('beta', mu = 0, sd = 0.5)
    sigma = pm.Exponential('sigma', lam = 1)
    mu = pm.Deterministic('mu', alpha + beta * p_residual)
    result = pm.Normal('result', mu = mu, sigma = sigma, observed=x_axis['Divorce_s'])
with m543:
    trace_m543 = pm.sample(1000, tune=1000, progressbar=False)
    df_trace_m543 = pm.trace_to_dataframe(trace_m543)

y_axis = []
low_line = []
up_line = []

for i in np.sort(p_residual):
    y_axis_result = df_trace_m543['alpha'].mean() + df_trace_m543['beta'].mean() * i
    [low_limit, high_limit] = get_hdi_in_point(df_trace_m543['alpha'], df_trace_m543['beta'], i)
    low_line.append(low_limit)
    up_line.append(high_limit)
    y_axis.append(y_axis_result)

ax4.scatter(p_residual, x_axis['Divorce_s'], marker='x')
ax4.set_xlabel('Resoidual Marriage Rate (std)')
ax4.set_ylabel('Divorce Rate (std)')
ax4.set_title("Residual (M) vs Observed")
ax4.plot(np.sort(p_residual), y_axis)
ax4.fill_between(np.sort(p_residual), low_line, up_line, alpha=.3)
ax4.grid(b=True)
```

```python
print("Low LHS Plot Gradient Average (A) Based On Residual {:.2f}".format(df_trace_m542['beta'].describe()['mean']))
print("Low RHS Plot Gradient Average (M) Based On Residual {:.2f}".format(df_trace_m543['beta'].describe()['mean']))
print("Multivariate model gradient (A) {:.2f}".format(df_trace_m53['beta_A'].describe()['mean']))
print("Multivariate model gradient (M) {:.2f}".format(df_trace_m53['beta_M'].describe()['mean']))
```

    Low LHS Plot Gradient Average (A) Based On Residual -0.62
    Low RHS Plot Gradient Average (M) Based On Residual -0.06
    Multivariate model gradient (A) -0.60
    Multivariate model gradient (M) -0.06

> Bagaimana cara mealukan residual plot apabila feature lebih dari satu?

Pada plot sisi tangan kiri atas, didapati plot antara nilai tengah umur pernikahan dan jumlah pernikahan. Plot ini dibuat dengan cara membuat memperlakukan keduanya sebagai model. Garis regresi adalah hasil dari pemodelan. Apabila suatu negara bagian memiliki posisi diatas garis regresi, maka negara bagian tersebut dapat dikatakan nilai umur tengah pernikahan lebih dari yang diharapkan pada jumlah pernikahan pada garis tersebut. Begitu juga sebaliknya untuk negara bagian yang ada pada bagian bawah garis regressi.

Nilai residual dapat dihasilkan dari selisih antara nilai $y$ sebenernya --dalam kasus ini adalah nilai tengah umur pernikahan--dan $\hat{y}$--dalam kasus ini adalah nilai tengah umur pernikahan yang dihasilkan dari regressi.

Nilai residual ini dibuatkan model dengan variabel terobservasi (*actual outcome of interest*) --jumlah perceraian. Hasil regresi dan HDI dapat dilihat pada plot sisi tangan kiri bawah. Pada residual plot ini dapat dilihat adanya kecendurungan naiknya nilai residual berbanding terbalik dengan jumlah perceraian. Perlakuan ini juga diterapkan pada jumlah pernikahan yang mengisi sisi tangan kanan dari plot.

Tujuan dari residual plot ini adalah menghilangkan pengaruh dari variabel lain. Model regresi mengukur asosiasi antara variabel prediktor dengan variabel terobservasi setelah mengetahui residual antara variabel. Ini dapat dilihat pada gradient yang dihasilkan antara residual plot dan model mutivariate cukup mendekati. Sayangnya model mutivariate berasumsi bahwa variabel terobservasi dihasilkan dari model penambahan yang disesuaikan sedemikian rupa sedangkan hubungan antara variabel prediktor tidak selalu sama antara satu dengan yang lain.

#### Posterior Prediction Plot
Pemilihan distribusi pada variabel prior bersifat subjektif. Maka diperlukan validasi agar permilihan variabel ini lebih wajar dan tidak melenceng jauh sehingga model memberikan informasi yang berguna. Hasil sampling dari model yang dibangun juga ada baiknya dibandignkan dengan data yang tersedia (cross validation).

Pak Richard membandingkan antara nilai dari data yang teredia dan nilai dari sampling model. Dibarikan juga batasan 89% dari prediksi yang dihasilkan.

Plot tersebut memberikan informasi seberapa melenceng model yang dibangun tanpa harus mengkuantifikasi secara matematik.

```python
def get_hdi_point(alpha, beta1, beta2, i1, i2):
    result = alpha + beta1 * i1 + beta2 * i2
    [low_limit, high_limit] = az.hdi(np.array(result), hdi=.89)
    return low_limit, high_limit
```

```python
fig, ax = plt.subplots(nrows=1, ncols=1)
fig.set_figwidth(14)
fig.set_figheight(14)

ax.set_xlabel("Actual Divorce Rate")
ax.set_ylabel("Predicted Divorce Rate")

x_axis = []
y_axis = []

for index, row in df.iterrows():
    pred_divorce = df_trace_m53['alpha'].mean() + \
        df_trace_m53['beta_A'].mean() * row['MedianAgeMarriage_s'] + \
        df_trace_m53['beta_M'].mean() * row['Marriage_s']
    [low_limit, high_limit] = get_hdi_point(
        df_trace_m53['alpha'],
        df_trace_m53['beta_A'],
        df_trace_m53['beta_M'],
        row['MedianAgeMarriage_s'],
        row['Marriage_s'])
    ax.plot([row['Divorce_s']] * 2, [low_limit, high_limit], color='k', alpha=.4)
    ax.annotate(row['Loc'], [row['Divorce_s'], pred_divorce])
    x_axis.append(row['Divorce_s'])
    y_axis.append(pred_divorce)
ax.scatter(x_axis, y_axis)
ax.grid(b=True)
ax.plot([-2, 2], [-2,2], linestyle='dashed', color='k')
```

Pada plot diatas, diberikan sebuah garis yang menandakan bahwa prediksi dan data yan sebenarnya adalah sama. Semakin banyaknya suatu titik dekat ke garis, maka model semakin baik dalam memprediksi. Titik merepresentasikan negara bagian. Apabila suatu titik memiliki posisi diatas garis maka negara bagian tersebut memiliki jumlah perceraian yang lebih sedikit dibandingkan dengan perceraian hasil prediksi model. Beigtu juga sebaliknya, apabila negara bagian ada dibawah garis maka jumlah perceraian sebenernya melebihi apa yang diprediksi.

#### Counterfactual Plots

Hal yang paling mudah didapatkan dari counterfactual adalah melihat bagaimana prediksi berubah saat variabel prediktor berubah. Pada dasarnya ini adalah extrapolasi dari data yang tersedia. Model mejalankan prediksi dengan masukan diluar data yang tersedia. Tentunnya langkah ini rentan untuk menyesatkan dan tidak masuk akal.

Langkah yang disarankan oleh Pak Richard untuk melakukan counterfactual plot adalah sebagai berikut
1. Pilih satu variable yang akan menjadi variabel intervensi
2. Tentukan himpunan dari variabel intervensi tersebut
3. Untuk masing-masing nilai variabel intervensi, dan sampel dari posterior, gunakan model kausal untuk memprediksi variable lainnya.

```python
with pm.Model() as m55:
    alpha = pm.Normal('alpha', mu=0, sd= 0.2)
    beta_M = pm.Normal('beta_M', mu=0, sd= 0.5)
    beta_A = pm.Normal('beta_A', mu=0, sd=0.5)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha +
                          beta_M * df['Marriage_s'] +
                          beta_A * df['MedianAgeMarriage_s'])
    result = pm.Normal('result', mu=mu, sigma=sigma, observed=df['Divorce_s'])

    sigma_M = pm.Exponential('sigma_M', lam=1)
    beta_AM = pm.Normal('beta_AM', mu=0, sd=.5)
    alpha_M = pm.Normal('alpha_M', mu=0, sd=.2)
    mu_m = pm.Deterministic('mu_M', alpha_M +
                            beta_AM * df['MedianAgeMarriage_s'])
    result_M = pm.Normal('result_M', mu=mu_m, sd=sigma, observed=df["Marriage_s"])

with m55:
    trace_m55 = pm.sample(1000, tune=1000)
    ppc_m55 = pm.sample_posterior_predictive(trace_m55, var_names=['alpha_M', 'beta_AM', 'sigma_M'])
    df_trace_m55 = pm.trace_to_dataframe(trace_m55)
```

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:03<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:03<00:00 Sampling chain 1, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:00<00:00]

```python
len(ppc_m55['alpha_M'])
len(ppc_m55['beta_AM'])
```

    2000

```python
from scipy.stats import norm
norm_form = norm.rvs(size=2000, loc=ppc_m55['alpha_M'] + ppc_m55['beta_AM'] * 0.1, scale=ppc_m55['sigma_M'])
```

```python
az.hdi(np.array(norm_form), hdi=.89)

x_axis = np.linspace(-2,2,30)
y_axis = []
low_lim = []
high_lim = []
for i in x_axis:
    norm_form = norm.rvs(size=2000, loc=ppc_m55['alpha_M'] + ppc_m55['beta_AM'] * i, scale=ppc_m55['sigma_M'])
    [low_limit, high_limit] = az.hdi(norm_form,hdi_prob=.8)
    y_axis.append(np.mean(norm_form))
    low_lim.append(low_limit)
    high_lim.append(high_limit)

fig, ax = plt.subplots(nrows=1, ncols=1)
ax.plot(x_axis, y_axis)
ax.fill_between(x_axis, low_lim, high_lim, color='k', alpha=.2)
ax.set_ylim([-2,2])
ax.set_xlabel("manipulated A")
ax.set_ylabel("counterfactual M")
ax.grid(b=True)
```

## Masked Variable

```python
raw_url = "https://raw.githubusercontent.com/pymc-devs/resources/master/Rethinking/Data/milk.csv"
df = pd.read_csv(raw_url, sep=';')
print(df.shape)
df.head()
```

    (29, 8)

      clade
      species
      kcal.per.g
      perc.fat
      perc.protein
      perc.lactose
      mass
      neocortex.perc

      0
      Strepsirrhine
      Eulemur fulvus
      0.49
      16.60
      15.42
      67.98
      1.95
      55.16

      1
      Strepsirrhine
      E macaco
      0.51
      19.27
      16.91
      63.82
      2.09
      NaN

      2
      Strepsirrhine
      E mongoz
      0.46
      14.11
      16.85
      69.04
      2.51
      NaN

      3
      Strepsirrhine
      E rubriventer
      0.48
      14.91
      13.18
      71.91
      1.62
      NaN

      4
      Strepsirrhine
      Lemur catta
      0.60
      27.28
      19.50
      53.22
      2.19
      NaN

```python
def standarize(df, cols):
    df[cols + "_s"] = (df[cols] - df[cols].mean()) / df[cols].std()

def log_transform(df, cols):
    df[cols + "_l"] = np.log(df[cols])

standarize(df, 'kcal.per.g')
standarize(df, 'neocortex.perc')
log_transform(df, 'mass')

df[["kcal.per.g_s", "neocortex.perc_s", "mass_l"]]
```

      kcal.per.g_s
      neocortex.perc_s
      mass_l

      0
      -0.940041
      -2.080196
      0.667829

      1
      -0.816126
      NaN
      0.737164

      2
      -1.125913
      NaN
      0.920283

      3
      -1.001998
      NaN
      0.482426

      4
      -0.258511
      NaN
      0.783902

      5
      -1.063955
      -0.508641
      1.658228

      6
      -0.506340
      -0.508641
      1.680828

      7
      1.538249
      0.010742
      0.920283

      8
      1.662163
      NaN
      -0.342490

      9
      1.724120
      0.213470
      -0.385662

      10
      0.980633
      -1.461962
      -2.120264

      11
      -1.125913
      -0.986139
      -0.755023

      12
      0.423018
      -1.215673
      -1.139434

      13
      0.423018
      NaN
      -0.510826

      14
      0.546933
      NaN
      1.244155

      15
      0.237147
      0.401118
      0.438255

      16
      0.484976
      NaN
      1.957274

      17
      2.033906
      0.474837
      1.175573

      18
      0.918676
      NaN
      2.071913

      19
      1.228462
      0.975791
      2.509599

      20
      -1.001998
      NaN
      2.026832

      21
      -0.134597
      -0.007687
      1.680828

      22
      -0.816126
      NaN
      2.372111

      23
      -0.630255
      0.617249
      3.568969

      24
      -0.940041
      0.841756
      4.374876

      25
      -0.692212
      NaN
      4.582106

      26
      -1.001998
      0.446355
      3.707210

      27
      -0.568297
      1.461666
      3.499835

      28
      0.423018
      1.325956
      4.006424

```python
new_df = df[df['neocortex.perc_s'].notnull()]
new_df.head()
```

      clade
      species
      kcal.per.g
      perc.fat
      perc.protein
      perc.lactose
      mass
      neocortex.perc
      kcal.per.g_s
      neocortex.perc_s
      mass_l

      0
      Strepsirrhine
      Eulemur fulvus
      0.49
      16.60
      15.42
      67.98
      1.95
      55.16
      -0.940041
      -2.080196
      0.667829

      5
      New World Monkey
      Alouatta seniculus
      0.47
      21.22
      23.58
      55.20
      5.25
      64.54
      -1.063955
      -0.508641
      1.658228

      6
      New World Monkey
      A palliata
      0.56
      29.66
      23.46
      46.88
      5.37
      64.54
      -0.506340
      -0.508641
      1.680828

      7
      New World Monkey
      Cebus apella
      0.89
      53.41
      15.80
      30.79
      2.51
      67.64
      1.538249
      0.010742
      0.920283

      9
      New World Monkey
      S sciureus
      0.92
      50.58
      22.33
      27.09
      0.68
      68.85
      1.724120
      0.213470
      -0.385662

```python
with pm.Model() as m56:
    alpha = pm.Normal('alpha', mu = 0, sd=1)
    beta = pm.Normal('beta', mu= 0, sd=1)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha + beta * new_df['neocortex.perc_s'])
    result = pm.Normal('result', mu = mu, sd=sigma, observed=new_df['kcal.per.g_s'])

with m56:
    trace_m56 = pm.sample(1000, tune=1000)
    prior_pred_m56 = pm.sample_prior_predictive(samples=50)

with pm.Model() as m561:
    alpha = pm.Normal('alpha', mu = 0, sd=.2)
    beta = pm.Normal('beta', mu= 0, sd=.5)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha + beta * new_df['neocortex.perc_s'])
    result = pm.Normal('result', mu = mu, sd=sigma, observed=new_df['kcal.per.g_s'])

with m561:
    trace_m561 = pm.sample(1000, tune=1000)
    prior_pred_m561 = pm.sample_prior_predictive(samples=50)
    df_trace_m561 = pm.trace_to_dataframe(trace_m561)

with pm.Model() as m562:
    alpha = pm.Normal('alpha', mu = 0, sd=.2)
    beta = pm.Normal('beta', mu= 0, sd=.5)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha + beta * new_df['mass_l'])
    result = pm.Normal('result', mu = mu, sd=sigma, observed=new_df['kcal.per.g_s'])

with m562:
    trace_m562 = pm.sample(1000, tune=1000)
    prior_pred_m562 = pm.sample_prior_predictive(samples=50)
    df_trace_m562 = pm.trace_to_dataframe(trace_m562)

with pm.Model() as m563:
    alpha = pm.Normal('alpha', mu=0, sd=.2)
    beta_N = pm.Normal('beta_N', mu=0, sd=.5)
    beta_M = pm.Normal('beta_M', mu=0, sd=.5)
    sigma = pm.Exponential('sigma', lam=1)
    mu = pm.Deterministic('mu', alpha + beta_N * new_df['neocortex.perc_s'] + beta_M * new_df['mass_l'])
    result = pm.Normal('result', mu = mu, sd = sigma, observed=new_df['kcal.per.g_s'])

with m563:
    trace_m563 = pm.sample(1000, tune=1000)
    df_trace_m563 = pm.trace_to_dataframe(trace_m563)
```

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:02<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:01<00:00 Sampling chain 1, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:02<00:00 Sampling chain 0, 0 divergences]

  <progress value='2000' class='' max='2000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [2000/2000 00:02<00:00 Sampling chain 1, 0 divergences]

```python
prior_pred.keys()
fig, [ax, ax1] = plt.subplots(nrows=1, ncols=2)
fig.set_figwidth(14)
fig.set_figheight(7)
for num, i in enumerate(prior_pred_m56['alpha']):
    x_axis = [-2, 2]
    y_axis = [
        prior_pred_m56['alpha'][num] + prior_pred_m56['beta'][num] * -2,
        prior_pred_m56['alpha'][num] + prior_pred_m56['beta'][num] * 2
    ]
    ax.plot(x_axis, y_axis, color='k', alpha=.3)

ax.set_ylim([-2,2])
ax.set_ylabel('kilogram per kcal')
ax.set_xlabel('neocortex')

for num, i in enumerate(prior_pred_m561['alpha']):
    x_axis = [-2, 2]
    y_axis = [
        prior_pred_m561['alpha'][num] + prior_pred_m561['beta'][num] * -2,
        prior_pred_m561['alpha'][num] + prior_pred_m561['beta'][num] * 2
    ]
    ax1.plot(x_axis, y_axis, color='k', alpha=.3)

ax1.set_ylim([-2,2])
ax1.set_ylabel('kilogram per kcal')
ax1.set_xlabel('neocortex')
```

```python
def get_hdi_in_point(alpha, beta, x):
    result = alpha + beta * x
    [low_limit, high_limit] = az.hdi(np.array(result), hdi=.89)
    return low_limit, high_limit
```

```python
fig, [ax1, ax2] = plt.subplots(nrows=1, ncols=2)
fig.set_figwidth(14)
fig.set_figheight(7)

ax1.scatter(new_df['neocortex.perc_s'], new_df['kcal.per.g_s'], marker='x')
ax2.scatter(new_df['mass_l'], new_df['kcal.per.g_s'], marker='x')

df_plot = new_df.sort_values('neocortex.perc_s')
y_axis = []
low_lim = []
high_lim = []
for i in df_plot['neocortex.perc_s']:
    y_axis.append(trace_m561['alpha'].mean() + trace_m561['beta'].mean() * i)
    [low_limit, high_limit] = get_hdi_in_point(trace_m561['alpha'], trace_m561['beta'], i)
    low_lim.append(low_limit)
    high_lim.append(high_limit)

ax1.fill_between(df_plot['neocortex.perc_s'], low_lim, high_lim, alpha=.2)
ax1.plot(df_plot['neocortex.perc_s'], y_axis)
ax1.set_xlabel("Neocortex (std)")
ax1.set_ylabel("kcal per gr (std)")

df_plot = new_df.sort_values('mass_l')
y_axis = []
low_lim = []
high_lim = []
for i in df_plot['mass_l']:
    y_axis.append(trace_m562['alpha'].mean() + trace_m562['beta'].mean() * i)
    [low_limit, high_limit] = get_hdi_in_point(trace_m562['alpha'], trace_m562['beta'], i)
    low_lim.append(low_limit)
    high_lim.append(high_limit)

ax2.fill_between(df_plot['mass_l'], low_lim, high_lim, alpha=.2)
ax2.plot(df_plot['mass_l'], y_axis)
ax2.set_xlabel("Mass (log)")
ax2.set_ylabel("kcal per gr (std)")
```

```python
print("Neocortex")
print(df_trace_m561[['alpha', 'beta']].describe())

print("Mass")
print(df_trace_m562[['alpha', 'beta']].describe())
```

    Neocortex
                 alpha         beta
    count  2000.000000  2000.000000
    mean      0.038430     0.127707
    std       0.160323     0.249925
    min      -0.582136    -0.810495
    25%      -0.068903    -0.024910
    50%       0.036940     0.128149
    75%       0.146374     0.284459
    max       0.598105     0.983690
    Mass
                 alpha         beta
    count  2000.000000  2000.000000
    mean      0.108971    -0.117091
    std       0.170245     0.119145
    min      -0.465813    -0.547228
    25%      -0.012954    -0.199723
    50%       0.105328    -0.115955
    75%       0.227831    -0.035968
    max       0.663978     0.285823

```python
print("Beta N mean {:.2f}".format(df_trace_m561["beta"].mean()))
print("Beta M mean {:.2f}".format(df_trace_m562["beta"].mean()))
print("\n")
print("Beta N mean {:.2f}".format(df_trace_m563["beta_N"].mean()))
print("Beta M mean {:.2f}".format(df_trace_m563["beta_M"].mean()))
```

    Beta N mean 0.13
    Beta M mean -0.11

    Beta N mean 0.42
    Beta M mean -0.24

Pada hasil prediski memperkirakan beta untuk masing-masing variable `Neocortex` dan `Mass` memiliki nilai gradient yang berbeda. Hasil nilai gradient pada model multivariate menghasilkan nilai gradient yang lebih besar jarak dari 0 dari model single variable. Jarak dari nol pada gradient menandakan pengaruh suatu prediktor terhadap variabel yang diobservasi. Semakin jauh jarak dari nol (nilai absolut) maka semakin berpengaruh variabel tersebut. Apabila negatif berarti berbanding terbalik dan apabila positif maka berbanding lurus.

Ini menghasilkan tiga buah dugaan kausalitas. Pertama adalah persen berat neocortex mempengaruhi massa dan energi dalam susu. Kedua adalah berat dari suatu primata mempengaruhi persenan neocortex dan energi dalam susu. Yang ketiga adalah adanya faktor extenal yang mempengaruhi nilai neocortex dan massa dalam primata. Tidak dapat diketahui dugaan yang benar dikareanakan adanya dugaan faktor external yang tidak diketahui nilainya. DAG dengan conditional independece yang sama dapat disebut sebagai **Markov Equivalence**

# Practice Question

5E1
(1) $\mu_i = \alpha + \beta x_i$
(2) $\mu_i = \beta_x x_i + \beta_z z_i$
(3) $\mu_i = \alpha + \beta(x_i - z_i)$
(4) $\mu_i = \alpha + \beta_x x_i + \beta_z z_i$

Persamaan pertama bukan bentuk dari multiple regresi linear karena hanya ada satu variabel yang berpengaruh terhadap gradient. Persamaan kedua ada dua buah yang bepengaruh terhadap gradient $\beta_x$ dan $\beta_z$ sehingga dapat dianggap menjadi regresi linear berganda. Persamaan ketiga bukan bentuk dari regresi linear berganda karena hanya memiliki satu gradient. Adanya dua variabel dapat dianggap menjadi satu $y_i = x_i - z_i$. Persamaan keempat adalah bentuk persamaan linear berganda karena memiliki dua buah gradient.


# Preamble

```python
%matplotlib notebook
import matplotlib.pyplot as plt
from numpy import *
from scipy.special import *
```

```python
from sympy import lambdify
```

# Functions

```python
G1 = 0.1
G2 = G1/2
def H(x):
    return 0.5*(1+G2/G1)+0.5*(1-G2/G1)*j0(2*x)
def ϕ(k,x):
    return 0.5/H(x)*j0(x)*jn(k,x)*G1/k
def I(k,xs):
    return array([ϕ(k,x)**2+ϕ(-k,x)**2 for x in xs])
```

```python
xs = linspace(0,20,10001)
```

# Plotting

```python

```

```python
ks = [1,3,5,7,9,11]
plt.figure(figsize=(6,3))
for k in ks:
    plt.semilogy(xs, I(k,xs))
plt.legend(ks)
plt.ylim((1e-8,2e-1))
plt.xlabel(r'$2\Omega/\delta\omega$')
plt.tight_layout()
plt.savefig('zeros.pdf', format='pdf', dpi=400)
```

    <IPython.core.display.Javascript object>

```python

```

```python

```

jupyter notebooks/Plotting dipole moment components under bichromatic field.ipynb


(ONAM)=

# 3.2 Algoritmos de descenso y búsqueda de línea para funciones convexas

```{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://drive.google.com/file/d/16-_PvWNaO0Zc9x04-SRsxCRdn5fxebf2/view).

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

* Aprenderá la importancia de resolver ecuaciones no lineales para encontrar mínimos locales de funciones.

* Comprenderá el uso de la información de primer y segundo orden para resolver problemas de minimización de funciones convexas mediante los algoritmos general de descenso y de búsqueda de línea por *backtracking*.

```

## Ejemplos de problemas de optimización *small scale*

En optimización la búsqueda del (o los) **óptimo(s)** involucran el cálculo de información de primer o segundo orden, ver {ref}`Definición de función, continuidad y derivada <FCD>`, de la función $f_o$ de acuerdo a lo revisado en los {ref}`resultados útiles de teoría de convexidad <RESUTTEOCONV>`. Tal información para problemas *small scale* es calculada utilizando todos los datos en un enfoque por *batch* o lote.

### Ejemplo

$$\displaystyle \min_{x \in \mathbb{R}^2} x_1^4+2x_1^2x_2+x_2^2$$

```python
import numpy as np
import sympy
from sympy.tensor.array import derive_by_array
import matplotlib.pyplot as plt
from scipy.optimize import fsolve
from scipy.optimize import fmin
import pandas as pd
import cvxpy as cp
```

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

Los candidatos a ser mínimos los encontramos al calcular el gradiente de $f_o$ que podemos calcular con *SymPy*

```python
x1, x2 = sympy.symbols("x1, x2")
```

```python
fo_sympy = x1**4+2*x1**2*x2+x2**2
```

```python
sympy.pprint(fo_sympy)
```

      4       2        2
    x₁  + 2⋅x₁ ⋅x₂ + x₂

```python
gf = derive_by_array(fo_sympy, (x1, x2))
```

```python
sympy.pprint(gf)
```

    ⎡    3                2       ⎤
    ⎣4⋅x₁  + 4⋅x₁⋅x₂  2⋅x₁  + 2⋅x₂⎦

y plantear:

$$
\nabla f_o(x) =
\left [
\begin{array}{c}
4x_1^3+4x_1x_2\\
2x_1^2+2x_2
\end{array}
\right ]=0
$$

la cual es una ecuación de dos variables y dos incógnitas **no lineal**. Resolviendo para $x_2$ se obtiene la relación: $x_2 = -x_1^2$. Entonces todos los puntos con coordenadas $x = (x_1, x_2)$ que satisfacen tal relación cumplen $\nabla f_o(x) = 0$. ¿Todos serán mínimos locales?

**Gráfica de la superficie $f_o$**

```python
def fo_numpy(x):
    return x[0]**4 + 2*x[0]**2*x[1] + x[1]**2
```

```python
x1_plot,x2_plot = np.meshgrid(np.linspace(-2,2,100), np.linspace(-2,2,100))
z_plot = x1_plot**4 + 2*x1_plot**2*x2_plot + x2_plot**2
```

```python
x1_np = 0
x2_np = 0
z_np = fo_numpy([x1_np, x2_np])
point = (x1_np, x2_np, z_np)
```

```python
print(point)
```

    (0, 0, 0)

```python
# Create the figure
fig = plt.figure()
# Add an axes
ax = fig.gca(projection='3d')
ax.plot_surface(x1_plot, x2_plot, z_plot, alpha=0.2)
ax.scatter(point[0], point[1], point[2], color='green')
plt.show()
```

**Gráfica de las curvas de nivel de $f_o$**

```python
x1_plot,x2_plot = np.meshgrid(np.linspace(-2,2,100), np.linspace(-4, 1,100))
z_plot = x1_plot**4 + 2*x1_plot**2*x2_plot + x2_plot**2
```

```python
plt.contour(x1_plot,x2_plot,z_plot)
plt.scatter(point[0], point[1], color="green")
plt.show()
```

Resolvamos con [scipy.optimize.fsolve](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fsolve.html#scipy.optimize.fsolve) la ecuación no lineal $\nabla f_o(x) = 0$

```python
def eqn(x):
    x1,x2=x
    return [4*x1**3+4*x1*x2, 2*x1**2+2*x2]
```

```{margin}

Elegimos diferentes puntos iniciales.

```

```python
root1 = fsolve(eqn, (1, 1))
```

```python
root2 = fsolve(eqn, (-1, 1))
```

```python
root3 = fsolve(eqn, (2, 0))
```

```python
dic_roots = {"root1": root1,
             "root2": root2,
             "root3": root3}
```

```{margin}

Obsérvese que los puntos `root1`, `root2`, `root3` satisfacen la relación $x_2 = -x_1^2$.

```

```python
print(pd.DataFrame(dic_roots))
```

       root1  root2  root3
    0    1.0   -1.0    2.0
    1   -1.0   -1.0   -4.0

Al evaluar el gradiente en cada punto obtenemos cero (o cercano a cero):

```python
gf_eval = lambda x: np.array([partial_derivative.subs({"x1": x[0],
                                                       "x2": x[1]}) for partial_derivative in gf],
                             dtype=float)
```

```python
dic = {"root1": gf_eval(root1),
       "root2": gf_eval(root2),
       "root3": gf_eval(root3)}
```

```{margin}

Los puntos `root1`, `root2` y `root3` resuelven la ecuación no lineal $\nabla f(x) = 0$ .

```

```python
print(pd.DataFrame(dic).round(3))
```

       root1  root2  root3
    0    0.0    0.0    0.0
    1    0.0   -0.0    0.0

**¿Cómo podemos identificar si son mínimos? ...** usamos la Hessiana de $f_o$

```python
Hf = derive_by_array(gf, (x1, x2))
```

```python
sympy.pprint(Hf)
```

    ⎡     2             ⎤
    ⎢12⋅x₁  + 4⋅x₂  4⋅x₁⎥
    ⎢                   ⎥
    ⎣    4⋅x₁        2  ⎦

y revisamos eigenvalores de la Hessiana evaluada en los puntos `root1`, `root2`, `root3`

```python
Hf_eval = lambda x: np.array([second_partial_derivative.subs({"x1": x[0],
                                                              "x2": x[1]}) for second_partial_derivative in Hf],
                             dtype=float)
```

```python
Hf_root1 = Hf_eval(root1)
Hf_root2 = Hf_eval(root2)
Hf_root3 = Hf_eval(root3)
```

```{margin}

La Hessiana en `root1` es semidefinida positiva por lo que no podemos concluir que sea mínimo local de $f$.

```

```python
print(np.linalg.eigvals(Hf_root1))
```

    [10.  0.]

```{margin}

La Hessiana en `root2` es semidefinida positiva por lo que no podemos concluir que sea mínimo local de $f$.

```

```python
print(np.linalg.eigvals(Hf_root2))
```

    [10. -0.]

```{margin}

La Hessiana en `root3` es semidefinida positiva por lo que no podemos concluir que sea mínimo local de $f$.

```

```python
print(np.linalg.eigvals(Hf_root3))
```

    [34.  0.]

Tenemos un criterio para $2$ dimensiones:

```{admonition} Comentario

Sea $f \in \mathcal{C}^2(\text{intdom}f)$, $\det(\nabla^2 f(x))$ determinante de la Hessiana y $x \in \mathbb{R}^2$ un punto crítico o estacionario de $f$:

* Si $\frac{\partial^2f(x)}{\partial x_1^2} < 0$ y $\det(\nabla^2 f(x)) >0$ entonces $f$ tiene un **máximo local** en $x$.

* Si $\frac{\partial^2f(x)}{\partial x_1^2} > 0$ y $\det(\nabla^2 f(x)) >0$ entonces $f$ tiene un **mínimo local** en $x$.

* Si $\det(\nabla^2 f(x)) < 0$ entonces $f$ tiene un **punto silla o [*saddle point*](https://en.wikipedia.org/wiki/Saddle_point)** en $x$.

* Si $\det(\nabla^2 f(x)) = 0$ no podemos concluir si $x$ es extremo.

```

```{admonition} Observaciones
:class: tip

* Al determinante de la Hessiana de $f$ se le nombra **Hessiano** de $f$.

* Lo anterior es un caso particular de los resultados descritos en {ref}`sobre puntos críticos <SPCRITICOS>`.

```

En el ejemplo el Hessiano es:

```python
sympy.pprint(sympy.Matrix(Hf).det())
```

        2
    8⋅x₁  + 8⋅x₂

```{margin}

Los tres puntos `root1`, `root2` y `root3` satisfacen $x_2 = -x_1^2$.

```

El cual se anula justo en los puntos que cumplen: $x_2 = -x_1^2$

$8x_1^2 + 8x_2 = 8 x_1^2 + 8(-x_1^2) = 0$

por lo que no podemos concluir...

Usemos una función de *SciPy* [scipy.optimize.fmin](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin.html)

```{margin}

Elegimos un punto inicial.

```

```python
res_fmin = fmin(fo_numpy, [1,1])
```

    Optimization terminated successfully.
             Current function value: 0.000000
             Iterations: 56
             Function evaluations: 110

```{margin}

El punto `res_fmin` satisface $\nabla f(x) = 0$ y la relación $x_2 = -x_1^2$.

```

```python
print(res_fmin)
```

    [-0.063 -0.004]

```python
print(gf_eval(res_fmin))
```

    [-0.  0.]

```python
Hf_fmin = Hf_eval(res_fmin)
```

```{margin}

La Hessiana en `res_fmin` es semidefinida positiva por lo que no podemos concluir que sea mínimo local de $f$.

```

```python
print(np.linalg.eigvals(Hf_fmin))
```

    [0.    2.032]

Grafiquemos los puntos que cumplen $x_2=-x_1^2$

```python
x1_plot,x2_plot = np.meshgrid(np.linspace(-2,2,100), np.linspace(-4,2,100))
z_plot = x1_plot**4 + 2*x1_plot**2*x2_plot + x2_plot**2
```

```python
point1 = (root1[0], root1[1], fo_numpy(root1))
point2 = (root2[0], root2[1], fo_numpy(root2))
point3 = (root3[0], root3[1], fo_numpy(root3))
#another point:
point4 = (-2, -4, 0)
```

```python
x1_plot2 = np.linspace(-2,2,100)
```

```python
# Create the figure
fig = plt.figure()
# Add an axes
ax = fig.gca(projection='3d')
ax.plot_surface(x1_plot, x2_plot, z_plot, alpha=0.2)
ax.scatter(point[0], point[1], point[2], color='green')
ax.scatter(point1[0], point1[1], point1[2], color='green')
ax.scatter(point2[0], point2[1], point2[2], color='green')
ax.scatter(point3[0], point3[1], point3[2], color='green')
ax.scatter(point4[0], point4[1], point4[2], color='green')
ax.plot(x1_plot2, -x1_plot2**2, color="red")
plt.show()
```

Evaluemos en un rango más amplio la función objetivo $f_o$ y realicemos una gráfica

```python
x1_plot,x2_plot = np.meshgrid(np.linspace(-100,100,100), np.linspace(-100,100,100))
z_plot = x1_plot**4 + 2*x1_plot**2*x2_plot + x2_plot**2
```

```python
# Create the figure
fig = plt.figure()
# Add an axes
ax = fig.gca(projection='3d')
ax.plot_surface(x1_plot, x2_plot, z_plot, alpha=0.2)
plt.show()
```

```python
np.sum(z_plot < 0)
```

    0

**El mínimo valor de $f$ es $0$ por lo que tenemos un conjunto de mínimos dados por la curva $x_2=-x_1^2$.**

### ¿Por qué fue un poco complicado determinar el conjunto de mínimos de $f$?

**$f(x)=x_1^4+2x_1^2x_2+x_2^2$ no es una función convexa en su $\text{dom}f$**, $\mathbb{R}^2$, esto es, no satisface la desigualdad:

$$f(\theta x + (1-\theta) y) \leq \theta f(x) + (1-\theta)f(y)$$

$\forall x,y$ en su dominio y $\forall \theta \in [0,1]$.

```python
pointx = np.array([-.5, -1.5])
pointy = np.array([.5, -1.5])
theta = 1/2
```

```python
point_convex_combination = theta*pointx + (1-theta)*pointy
```

```python
print(fo_numpy(point_convex_combination))
```

    2.25

```{margin}

Se tiene $f(\theta x + (1-\theta)y) > \theta f(x) + (1-\theta)f(y)$ con la elección $\theta=0.5$, $x=[-0.5, -1.5]^T$, $y=[0.5, -1.5]^T$ lo que indica que $f$ no es convexa sino cóncava para esta elección de puntos $x,y$.

```

```python
print(theta*fo_numpy(pointx) + (1-theta)*fo_numpy(pointy))
```

    1.5625

### Ejemplo importante

$$\displaystyle \min_{x \in \mathbb{R}^2} \frac{1}{2}x^TPx+q^Tx+r$$

donde: $P=\left [\begin{array}{cc} 5 & 4\\ 4 & 5 \end{array} \right ]$, $q=\left [\begin{array}{c} -1\\ 1 \end{array} \right]$, $r=3$.

Haciendo las multiplicaciones de matriz-vector y productos punto

```python
P = sympy.Matrix([[5, 4],
                  [4, 5]])
```

```python
x = sympy.Matrix(sympy.symbols("x1, x2"))
```

```python
q = sympy.Matrix([-1,1])

```

```python
fo_sympy = (1/2*x.T*P*x + q.T*x)[0] + 3
```

```python
sympy.pprint(fo_sympy.expand())
```

          2                          2
    2.5⋅x₁  + 4.0⋅x₁⋅x₂ - x₁ + 2.5⋅x₂  + x₂ + 3

se **reescribe** el problema de optimización como:

$$\displaystyle \min_{x \in \mathbb{R}^2} \frac{5}{2}x_1^2 + \frac{5}{2}x_2^2+4x_1x_2 -x_1 + x_2+3$$

La función objetivo es una **función estrictamente convexa** en $\mathbb{R}^2$ (de hecho **fuertemente convexa**) pues:

```{margin}

Los eigenvalores de $P$ son $1$ y $9$ de multiplicidad simple cada uno.

```

```python
sympy.pprint(P.eigenvals())
```

    {1: 1, 9: 1}

y la Hessiana de $f_o$ es:

```python
sympy.pprint(derive_by_array(derive_by_array(fo_sympy, (x1,x2)),
                             (x1,x2))
             )
```

    ⎡5.0  4.0⎤
    ⎢        ⎥
    ⎣4.0  5.0⎦

El gradiente de $f_o$ es:

```python
sympy.pprint(fo_sympy.diff(x))
```

    ⎡5.0⋅x₁ + 4.0⋅x₂ - 1⎤
    ⎢                   ⎥
    ⎣4.0⋅x₁ + 5.0⋅x₂ + 1⎦

El mínimo debe satisfacer la ecuación **lineal** con dos variables y dos ecuaciones

$$\nabla f_o(x) = Px +q =\left [ \begin{array}{cc}
5 & 4\\
4 & 5
\end{array}
\right ]
\left [ \begin{array}{c}
x_1\\
x_2
\end{array}
\right ]
+ \left [ \begin{array}{c}
-1\\
1
\end{array}
\right ]=
\left [ \begin{array}{cc}
5x_1+4x_2-1\\
4x_1+5x_2+1
\end{array}
\right ]
=0
$$

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

En algunos casos especiales es posible resolver la ecuación no lineal $\nabla f_o(x) = 0$ para $x$ de forma analítica o cerrada. Este es el caso de este ejemplo cuya solución está dada por $x^* = -P^{-1}q$.

```

```python
P=np.array([[5,4],[4,5]])
q=np.array([-1,1])
print(np.linalg.solve(P,-q))
```

    [ 1. -1.]

El problema anterior también lo podemos resolver con [cvxpy](https://github.com/cvxgrp/cvxpy) pues es un **problema convexo** sin restricciones.

```python
n = 2 #number of variables
x = cp.Variable(n) #optimization variable
fo_cvxpy = (1/2)*cp.quad_form(x, P) + q.T @ x #objective function
```

```python
opt_objective = cp.Minimize(fo_cvxpy) #optimization objective
```

```python
prob = cp.Problem(opt_objective) #optimization problem
```

```python
print(prob.solve())
```

    -0.9999999999999993

```python
print("status:", prob.status)
print("optimal value", prob.value)
print("optimal var", x.value)
```

    status: optimal
    optimal value -0.9999999999999993
    optimal var [ 1. -1.]

````{admonition} Observaciones
:class: tip

* El paquete cvxpy requiere que se especifique el problema de optimización a resolver siguiendo reglas establecidas en [Disciplined Convex Programming](https://dcp.stanford.edu/). En el ejemplo anterior resulta en un error si se ejecutan las líneas siguientes:

```python
x1 = cp.Variable()
x2 = cp.Variable()
fo = 2.5*x1**2 + 4*x1*x2 - x1 + 2.5*x2**2 + x2 + 3

obj = cp.Minimize(fo_cvxpy)

prob = cp.Problem(obj)

prob.solve()
```

La última línea produce

```
Problem does not follow DCP rules. Specifically: The objective is not DCP.
```

* En la liga de [ejemplos](https://www.cvxpy.org/examples/index.html) hay muchos problemas típicos en optimización convexa.

````

## Métodos de descenso para funciones convexas

En lo siguiente se asume que $f_o$ cumple $f_o \in \mathcal{C}^2(\text{dom}f_o)$ y es convexa en un conjunto convexo y cerrado que contiene a $x^*$. Ver {ref}`conjunto abierto, cerrado, cerradura e interior <CACCI>` para definición de conjunto cerrado.

### Secuencia de minimización

Una gran cantidad de problemas de optimización utilizan métodos iterativos para calcular una secuencia de puntos $x^{(0)}, x^{(1)}, \dots \in \text{dom}f_o$ nombrados **secuencia de minimización** con la característica $f_o(x^{(k)}) \rightarrow p^*$ si $k \rightarrow \infty$. Tales métodos se conocen con el nombre de **métodos de descenso**.

```{margin}

Recuérdese que si $f_o$ es fuertemente convexa en el conjunto $\mathcal{S}$ entonces $\nabla^2 f_o (x) \in \mathbb{S}_{++}$ y $\text{cond}(\nabla ^2 f_o(x))$ está acotado por arriba por una constante para $x \in \mathcal{S}$.

```

```{admonition} Comentarios

* Se les nombra métodos de descenso pues para la secuencia de minimización se cumple la desigualdad: $f_o(x^{(k+1)}) < f_o(x^{(k)})$, excepto para $x^{(k)}$ óptimo.

* Para las secuencias de minimización se asume:

    * Los puntos iniciales $x^{(0)}$ de los métodos iterativos están en $\text{dom}f_o$.
    * Que el conjunto $f_o(x^{(0)})$-subnivel sea cerrado pues así se garantiza que la secuencia de minimización está en el conjunto $f_o(x^{(0)})$-subnivel para todas las iteraciones y con la suposición que $f_o$ sea fuertemente convexa en el conjunto $f_o(x^{(0)})$-subnivel se tienen resultados revisados en {ref}`definición de problemas de optimización, conjuntos y funciones convexas <DPOCFC>`.

```

### Paso o dirección de búsqueda y tamaño o longitud de paso

La secuencia de minimización se obtiene con la fórmula: $x^{(k+1)} = x^{(k)} + t^{(k)}\Delta x^{(k)}$.

```{admonition} Definición

Al vector $\Delta x \in \mathbb{R}^n$ se le nombra **paso o dirección de búsqueda**. Al escalar $t^{(k)}$ se le nombra **tamaño o longitud de paso** y siempre es positivo salvo en el caso en que $x^{(k)}$ sea óptimo.
```

### Condición para que un paso o dirección de búsqueda sea de descenso

```{admonition} Definición

Si el paso o dirección de búsqueda satisface la condición: $\nabla f_o^T(x^{(k)})\Delta x^{(k)} < 0$ se le nombra **dirección de descenso**.

```

Geométricamente las direcciones de descenso forman un **ángulo agudo** con $-\nabla f_o(x^{(k)})$:

En el dibujo $f = f_o$.

### Algoritmo: método general de descenso para funciones convexas

```{margin}

Los pasos de un algoritmo representan una guía para la implementación, no implica que se tengan que implementar uno a continuación del otro como se describe. Si una implementación respeta la lógica y al mismo algoritmo, entonces pueden seguirse los pasos de una forma distinta.
```

> **Dado** un **punto inicial** $x$ en $\text{dom}f_o$
>
> **Repetir** el siguiente bloque para $k=0,1,2,...$
>>
>> 1. Determinar una dirección de descenso $\Delta x$.
>> 2. Búsqueda de línea. Elegir un tamaño de paso $t > 0$.
>> 3. Hacer la actualización: $x = x + t\Delta x$.
>
> **hasta** convergencia (satisfacer criterio de paro).

```{margin}

Un rayo es el conjunto definido por $\{x + \theta v : \theta \geq 0 , v \neq 0, v \in \mathbb{R}^n\}$ para $x \in \mathbb{R}^n$.

```

```{admonition} Comentarios.

* El algoritmo termina si $f_o(x^{(k)})-p^* \leq \epsilon$ con $\epsilon >0$ una tolerancia dada, esto es, $x^{(k)}$ es $\epsilon$-subóptimo.

* Ejemplos de direcciones de descenso se tienen:

    * $\Delta x = - \nabla f_o \left (x^{(k)} \right )$ que da lugar al método de descenso en gradiente para $x^{(k)}$ no óptimo.
    * $\Delta x = - \nabla^2 f_o \left (x^{(k)} \right )^{-1} \nabla f_o\left(x^{(k)} \right)$ que da lugar al método de descenso por Newton con $\nabla^2 f_o \left (x^{(k)} \right ) \in \mathbb{S}_{++}$ y $x^{(k)}$ no óptimo.

* El criterio de paro típicamente es de la forma $||\nabla f_o(x)|| \leq \text{TOL}$ donde: $\text{TOL}$ es una cantidad pequeña y positiva (menor o igual a $10^{-8}$). También se involucra el número máximo de iteraciones en el criterio de paro.

* El paso $2$ busca reducir $f_o$ lo **suficiente** o minimizarla aproximadamente a lo largo de un **rayo**. Uno de los métodos que permite de forma sencilla lo anterior es la **búsqueda de línea por [*backtracking*](https://en.wikipedia.org/wiki/Backtracking_line_search)**.

```

### Ejemplo de función objetivo convexa

$$\displaystyle \min_{x \in \mathbb{R}^4} (x_1-2)^2+(2-x_2)^2+x_3^2+x_4^4$$

```python
x1, x2, x3, x4 = sympy.symbols("x1, x2, x3, x4")
```

```python
fo_sympy = (x1-2)**2 + (2-x2)**2 + x3**2 + x4**4
```

```python
gf = derive_by_array(fo_sympy, (x1, x2, x3, x4))
```

```python
sympy.pprint(gf)
```

    ⎡                              3⎤
    ⎣2⋅x₁ - 4  2⋅x₂ - 4  2⋅x₃  4⋅x₄ ⎦

```python
Hf = derive_by_array(gf, (x1, x2, x3, x4))
```

```python
sympy.pprint(Hf)
```

    ⎡2  0  0    0   ⎤
    ⎢               ⎥
    ⎢0  2  0    0   ⎥
    ⎢               ⎥
    ⎢0  0  2    0   ⎥
    ⎢               ⎥
    ⎢              2⎥
    ⎣0  0  0  12⋅x₄ ⎦

Como $f_o$ es una función convexa (de hecho **estrictamente convexa**) en su dominio $\mathbb{R}^4$, se tiene que su óptimo se obtiene igualando y resolviendo la **ecuación no lineal** $\nabla f_o(x) = 0$ :

$$\nabla f_o(x) =
\left[ \begin{array}{c}
2x_1-4 \\
2x_2-4\\
2x_3\\
4x_4^3
\end{array}
\right]
= 0
$$

El óptimo $x^* \in \mathbb{R}^4$ está dado por:

$$x^*=
\left[ \begin{array}{c}
2\\
2\\
0\\
0
\end{array}
\right]
$$

**¿Cómo encontramos numéricamente el óptimo?**

**Opción descenso en gradiente**: usando el algoritmo con dirección de descenso el gradiente de $f_o$ sin búsqueda de línea se tiene:

$$x^{(k+1)} = x^{(k)} - \nabla f_o(x^{(k)})$$

Tomando $x^{(0)} = \left [ \begin{array}{c} 5 \\ 5 \\1 \\ 0 \\ \end{array} \right ]$ como punto inicial:

```python
x_0 = np.array([5,5,1,0])
```

```python
gf_eval = lambda x: np.array([partial_derivative.subs({"x1": x[0],
                                              "x2": x[1],
                                              "x3": x[2],
                                              "x4": x[3]}) for partial_derivative in gf],
                              dtype=float)
```

```python
x_1 = x_0 - gf_eval(x_0)
```

```python
print(x_1)
```

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

```python
x_2 = x_1 - gf_eval(x_1)
```

```python
print(x_2)
```

    [5. 5. 1. 0.]

```python
x_3 = x_2 - gf_eval(x_2)
```

```python
print(x_3)
```

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

```python
x_4 = x_3 - gf_eval(x_3)
```

```python
print(x_4)
```

    [5. 5. 1. 0.]

y aquí nos quedaremos ciclando hasta el infinito...

**Opción descenso por dirección de Newton:** usando el algoritmo con dirección de descenso de Newton de $f_o$ sin búsqueda de línea se tiene:

$$x^{(k+1)} = x^{(k)} - \nabla^2 f_o \left (x^{(k)} \right )^{-1} \nabla f_o\left(x^{(k)} \right)$$

Tomando $x^{(0)} = \left [ \begin{array}{c} 5 \\ 5 \\1 \\ 0 \\ \end{array} \right ]$ como punto inicial y **no calculando la inversa** de la Hessiana pues en su lugar resolvemos el sistema de ecuaciones lineales $\nabla ^2 f_o \left ( x^{(k)} \right ) \Delta x = - \nabla f_o \left(x^{(k)}\right )$ resulta

```python
x_0 = np.array([5,5,1,0])
```

```python
Hf_eval = lambda x: np.array([second_partial_derivative.subs({"x1": x[0],
                                                              "x2": x[1],
                                                              "x3": x[2],
                                                              "x4": x[3]}) for second_partial_derivative in Hf],
                             dtype=float)
```

```python
Hf_sympy_eval = sympy.Matrix(Hf_eval(x_0))
sympy.pprint(Hf_sympy_eval)
```

    ⎡2.0  0.0  0.0  0.0⎤
    ⎢                  ⎥
    ⎢0.0  2.0  0.0  0.0⎥
    ⎢                  ⎥
    ⎢0.0  0.0  2.0  0.0⎥
    ⎢                  ⎥
    ⎣0.0  0.0  0.0  0.0⎦

```python
gf_sympy_eval = sympy.Matrix(gf_eval(x_0))
sympy.pprint(gf_sympy_eval)
```

    ⎡6.0⎤
    ⎢   ⎥
    ⎢6.0⎥
    ⎢   ⎥
    ⎢2.0⎥
    ⎢   ⎥
    ⎣0.0⎦

```python
x = sympy.Matrix([x1, x2, x3, x4])
```

El sistema de **ecuaciones lineales** a resolver es:

$$
\left [
\begin{array}{cccc}
2 & 0 & 0 & 0 \\
0 & 2 & 0 & 0 \\
0 & 0 & 2 & 0 \\
0 & 0 & 0 & 0
\end{array}
\right ]
\Delta x =
-\left [
\begin{array}{c}
6 \\
6 \\
2 \\
0
\end{array}
\right ]
$$

**Obsérvese que la última ecuación es redundante**

```python
sympy.pprint(sympy.solve(Hf_sympy_eval*x - (-gf_sympy_eval), x))
```

    {x₁: -3.0, x₂: -3.0, x₃: -1.0}

$4a$ ecuación redundante y por tanto $x_4 = 0$

```python
x_1 = x_0 + np.array([-3, -3, -1, 0])
```

```python
print(x_1)
```

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

```{admonition} Comentarios

De acuerdo al ejemplo anterior:

* Utilizar información de primer o segundo orden nos ayuda a encontrar óptimo(s) de funciones.

* Encontrar al óptimo involucró un método iterativo.

* En términos coloquiales y de forma simplificada, una **dirección de descenso** es aquella que al moverse de un punto a otro en tal dirección, el valor de $f_o$ decrece (en lo que sigue $f = f_o$):

En el dibujo anterior $\hat{f}$ es un modelo cuadrático, $\Delta x_{nt}$ es dirección de descenso de Newton y $x^*$ es el óptimo de $f$. Del punto $(x,f(x))$ nos debemos mover al punto $(x+\Delta x_{nt}, f(x + \Delta x_{nt}))$ para llegar al óptimo y el valor de $f$ decrece: $f(x+\Delta x_{nt}) < f(x)$.

* Con la información de primer orden no alcanzamos al óptimo (de hecho se cicla el método iterativo propuesto) pero con la de segundo orden sí lo alcanzamos en una iteración y tuvimos que resolver un sistema de ecuaciones lineales.

**Opción descenso en gradiente:** usando el algoritmo con dirección de descenso el gradiente de $f_o$ con búsqueda de línea se tiene:

$$x^{(k+1)} = x^{(k)} - t^{(k)}\nabla f_o(x^{(k)})$$

para $t^{(k)} > 0$. Con $t^{(0)}=0.5$ llegamos al óptimo en una iteración:

```python
t_0=0.5
```

```python
x_1 = x_0 - t_0*gf_eval(x_0)
```

```python
print(x_1)
```

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

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

El gradiente involucra menos almacenamiento en memoria que el almacenamiento de la Hessiana: $\mathcal{O}(n)$ vs $\mathcal{O}(n^2)$.

```

**Opción usando cvxpy...**

```python
x1 = cp.Variable()
x2 = cp.Variable()
x3 = cp.Variable()
x4 = cp.Variable()
```

```python
fo_cvxpy = (x1 -2)**2 + (2-x2)**2 + x3**2 + x4**2

```

```python
obj = cp.Minimize(fo_cvxpy)

prob = cp.Problem(obj)

print(prob.solve())
```

    7.031418247836792e-48

```python
print("status:", prob.status)
print("optimal value", prob.value)
print("optimal var", x1.value, x2.value, x3.value, x4.value)
```

    status: optimal
    optimal value 7.031418247836792e-48
    optimal var 2.0 2.0 0.0 0.0

## Método de búsqueda de línea por *backtracking*

En esta sección consideramos $f = f_o$.

Para entender el método de búsqueda de línea por *backtracking* supóngase que $f$ tiene una forma siguiente:

Y considérese una función $g: \mathbb{R} \rightarrow \mathbb{R}$ igual a $f$ pero restringida al rayo $x + t\Delta x$, esto es: $g(t) = f(x+t \Delta x)$ con $t>0$, $\Delta x$ dirección de descenso. Lo anterior se visualiza como sigue:

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

Obsérvese en el dibujo que $\nabla f(x) \neq 0$.

```

```{margin}

Ver {ref}`ejemplo<EJRestriccionALinea>` de la nota {ref}`Definición de función, continuidad y derivada <FCD>` para expresión de la derivada $g'(t)$.

```

Y como $f$ es continua y diferenciable, $g$ también lo es y $g(0)=f(x)$, $g'(t) = \nabla f(x+t\Delta x)^T \Delta x$. Si graficamos $g$ se tendría:

En la búsqueda de línea se construyen dos rectas. Una recta es $g(0) + \alpha g'(0)(t-0)$ con $\alpha \in (0,\frac{1}{2})$. La otra recta es $g(0)+g'(0)(t-0)$. Ambas rectas tienen pendiente negativa. Esto se visualiza como sigue:

En la búsqueda de línea por backtracking se busca $t^{(k)}$ tal que $f$ decrezca suficientemente. Lo anterior se establece con la desigualdad $f(x+t \Delta x) < f(x) + \alpha t \nabla f(x)^T \Delta x$:

obsérvese en el dibujo anterior que la región en la que se elegirá $t^{(k)}$ está a la izquierda de la línea punteada vertical de color verde.

Y visualmente en $R^3$ se tiene:

El método depende de dos constantes $\alpha$ y $\beta$ con $\alpha \in (0,\frac{1}{2})$ y $\beta \in (0,1)$.

### Algoritmo: búsqueda de línea por *backtracking*

> **Dados** $\Delta x$ dirección de descenso para $f$ en $x \in \text{dom}f$, $\alpha \in (0,\frac{1}{2})$, $\beta \in (0,1)$.
>
> **Asignar** t=1.
>
> **Mientras** $f(x+t\Delta x) > f(x) + \alpha t \nabla f(x) ^T\Delta x$.
>>
>> 1. **Reducir** $t: t= \beta t$.

```{admonition} Comentarios

* El valor $\alpha$ típicamente se elige entre $.01$ y $.03$ que indica que se acepta un decrecimiento en el valor de $f$ entre el $1 \%$ y el $30 \%$. La constante $\beta$ comúnmente se elige entre $.1$ (que modifica fuertemente $t$) y $.8$ (que realiza una modificación menos drástica de $t$).

* Obsérvese que la multiplicación $\nabla f(x)^T \Delta x$ es una derivada direccional, ver {ref}`definición de función, continuidad y derivada <FCD>`.
```

## Función en Python para el método de *backtracking*

```python
def line_search_by_backtracking(f,dir_desc,x,
                                der_direct, alpha=.15, beta=.5):
    """
    Line search that sufficiently decreases f restricted to a ray in the direction dir_desc.
    Args:
        alpha (float): parameter in line search with backtracking, tipically .15
        beta (float): parameter in line search with backtracking, tipically .5
        f (lambda expression): definition of function f.
        dir_desc (array): descent direction.
        x (array): numpy array that holds values where line search will be performed.
        der_direct (float): directional derivative of f.
    Returns:
        t (float): positive number for stepsize along dir_desc that sufficiently decreases f.
    """
    t=1
    if alpha > 1/2:
        print("alpha must be less than or equal to 1/2")
        t=-1
    if beta>1:
        print("beta must be less than 1")
        t=-1;
    if t!=-1:
        eval1 = f(x+t*dir_desc)
        eval2 = f(x) + alpha*t*der_direct
        while eval1 > eval2:
            t=beta*t
            eval1=f(x+t*dir_desc)
            eval2=f(x)+alpha*t*der_direct
    return t
```

**Preguntas de comprehensión.**

1)

**Referencias:**

1. S. P. Boyd, L. Vandenberghe, Convex Optimization, Cambridge University Press, 2009.

2.