## [mlcourse.ai](https://mlcourse.ai) - Open Machine Learning Course

Auteur: [Egor Polusmak](https://www.linkedin.com/in/egor-polusmak/).
Traduit et édité par [Yuanyuan Pao](https://www.linkedin.com/in/yuanyuanpao/) et [Ousmane Cissé](https://fr.linkedin.com/in/ousmane-cisse).
Ce matériel est soumis aux termes et conditions de la licence [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/).
L'utilisation gratuite est autorisée à des fins non commerciales.

# Topic 9. Analyse des séries temporelles en Python
## Partie 2. Prédire l'avenir avec Facebook Prophet

La prévision de séries chronologiques trouve une large application dans l'analyse de données. Ce ne sont que quelques-unes des prévisions imaginables des tendances futures qui pourraient être utiles:
- Le nombre de serveurs dont un service en ligne aura besoin l'année prochaine.
- La demande d'un produit d'épicerie dans un supermarché un jour donné.
- Le cours de clôture de demain d'un actif financier négociable.

Pour un autre exemple, nous pouvons faire une prédiction des performances d'une équipe, puis l'utiliser comme référence: d'abord pour fixer des objectifs pour l'équipe, puis pour mesurer les performances réelles de l'équipe par rapport à la référence.

Il existe plusieurs méthodes différentes pour prédire les tendances futures, par exemple, [ARIMA](https://en.wikipedia.org/wiki/Autoregressive_integrated_moving_average), [ARCH](https://en.wikipedia.org/wiki/Autoregressive_conditional_heteroskedasticity), [modèles régressifs](https://en.wikipedia.org/wiki/Autoregressive_model), [réseaux de neurones](https://medium.com/machine-learning-world/neural-networks-for-algorithmic-trading-1-2-correct-time-series-forecasting-backtesting-9776bfd9e589).

Dans cet article, nous examinerons [Prophet](https://facebook.github.io/prophet/), une bibliothèque de prévisions de séries chronologiques publiée par Facebook et open source, le 23 février 2017. Nous l'essayerons également dans le problème de prédiction du nombre quotidien de publications sur Medium.

## Plan de l'article

1. Introduction
2. Le modèle de prévision de Prophet
3. Entraînez-vous avec le Prophet
    * 3.1 Installation en Python
    * 3.2 Ensemble de données
    * 3.3 Analyse visuelle exploratoire
    * 3.4 Faire une prévision
    * 3.5 Évaluation de la qualité des prévisions
    * 3.6 Visualisation
4. Transformation Box-Cox
5. Résumé
6. Références

## 1. Introduction

Selon [l'article](https://research.fb.com/prophet-forecasting-at-scale/) sur Facebook Research, Prophet a été initialement développé dans le but de créer des prévisions commerciales de haute qualité. Cette bibliothèque tente de résoudre les difficultés suivantes, communes à de nombreuses séries chronologiques commerciales:
- Effets saisonniers causés par le comportement humain: cycles hebdomadaires, mensuels et annuels, creux et pics les jours fériés.
- Changements de tendance dus aux nouveaux produits et aux événements du marché.
- Valeurs aberrantes.

Les auteurs affirment que, même avec les paramètres par défaut, dans de nombreux cas, leur bibliothèque produit des prévisions aussi précises que celles fournies par des analystes expérimentés.

De plus, Prophet dispose d'un certain nombre de personnalisations intuitives et facilement interprétables qui permettent d'améliorer progressivement la qualité du modèle de prévision. Ce qui est particulièrement important, ces paramètres sont tout à fait compréhensibles même pour les non-experts en analyse de séries chronologiques, qui est un domaine de la science des données nécessitant certaines compétences et expérience.

Soit dit en passant, l'article d'origine s'intitule «Prévisions à grande échelle», mais il ne s'agit pas de l'échelle au sens «habituel», qui traite des problèmes de calcul et d'infrastructure d'un grand nombre de programmes de travail. Selon les auteurs, Prophet devrait bien évoluer dans les 3 domaines suivants:
- Accessibilité à un large public d'analystes, éventuellement sans expertise approfondie des séries chronologiques.
- Applicabilité à un large éventail de problèmes de prévision distincts.
- Estimation automatisée des performances d'un grand nombre de prévisions, y compris la signalisation des problèmes potentiels pour leur inspection ultérieure par l'analyste.

## 2. Le modèle de prévision Prophet

Maintenant, regardons de plus près comment fonctionne Prophet. Dans son essence, cette bibliothèque utilise le [modèle de régression additive](https://en.wikipedia.org/wiki/Additive_model) $y(t)$ comprenant les composants suivants:

$$y(t) = g(t) + s(t) + h(t) + \epsilon_{t},$$

où:
* La tendance $g(t)$ modélise les changements non périodiques.
* La saisonnalité $s(t)$ représente des changements périodiques.
* La composante vacances $h(t)$ fournit des informations sur les vacances et les événements.

Ci-dessous, nous considérerons quelques propriétés importantes de ces composants de modèle.

### Tendance

La bibliothèque Prophet implémente deux modèles de tendance possibles pour $g(t)$.

Le premier est appelé *Croissance saturée non linéaire*. Il est représenté sous la forme du [modèle de croissance logistique](https://en.wikipedia.org/wiki/Fonction_logistique):

$$g(t) = \frac{C}{1+e^{-k(t - m)}},$$

où:

* $C$ est la capacité de charge (c'est-à-dire la valeur maximale de la courbe).

* $k$ est le taux de croissance (qui représente "la pente" de la courbe).

* $m$ est un paramètre de décalage.

Cette équation logistique permet de modéliser la croissance non linéaire avec saturation, c'est-à-dire lorsque le taux de croissance d'une valeur diminue avec sa croissance. Un des exemples typiques serait de représenter la croissance de l'audience d'une application ou d'un site Web.

En fait, $C$ et $k$ ne sont pas nécessairement des constantes et peuvent varier dans le temps. Prophet prend en charge le réglage automatique et manuel de leur variabilité. La bibliothèque peut elle-même choisir des points optimaux de changements de tendance en ajustant les données historiques fournies.

En outre, Prophet permet aux analystes de définir manuellement des points de changement du taux de croissance et des valeurs de capacité à différents moments. Par exemple, les analystes peuvent avoir des informations sur les dates des versions précédentes qui ont influencé de manière importante certains indicateurs clés de produit.

Le deuxième modèle de tendance est un simple *modèle linéaire par morceaux* (Piecewise Linear Model) avec un taux de croissance constant.
Il est le mieux adapté aux problèmes sans saturation de la croissance.

### Saisonnalité

La composante saisonnière $s(t)$ fournit un modèle flexible de changements périodiques dus à la saisonnalité hebdomadaire et annuelle.

Les données saisonnières hebdomadaires sont modélisées avec des variables factices. Six nouvelles variables sont ajoutées: «lundi», «mardi», «mercredi», «jeudi», «vendredi», «samedi», qui prennent des valeurs 0 ou 1 selon le jour de la semaine. La caractéristique «dimanche» n'est pas ajoutée car ce serait une combinaison linéaire des autres jours de la semaine, et ce fait aurait un effet négatif sur le modèle.

Le modèle de saisonnalité annuelle dans Prophet repose sur la série de Fourier.

Depuis la version 0.2, vous pouvez également utiliser des séries chronologiques infra-journalières et faire des prévisions infra-journalières, ainsi qu'utiliser la nouvelle caractéristique de saisonnalité quotidienne.

### Vacances et événements

La composante $h(t)$ représente les jours anormaux prévisibles de l'année, y compris ceux dont les horaires sont irréguliers, par exemple les Black Fridays.

Pour utiliser cette caractéristique, l'analyste doit fournir une liste personnalisée d'événements.

### Erreur

Le terme d'erreur $\epsilon(t)$ représente des informations qui n'étaient pas reflétées dans le modèle. Habituellement, il est modélisé comme un bruit normalement distribué.

### Analyse comparative (benchmark) de Prophet

Pour une description détaillée du modèle et des algorithmes derrière Prophet, reportez-vous à l'article ["Forecasting at scale"](https://peerj.com/preprints/3190/) de Sean J. Taylor et Benjamin Letham.

Les auteurs ont également comparé leur bibliothèque avec plusieurs autres méthodes de prévision de séries chronologiques. Ils ont utilisé l'[Erreur absolue moyenne en pourcentage (MAPE)](https://en.wikipedia.org/wiki/Mean _absolue_ pourcentage_erreur) comme mesure de la précision de la prédiction. Dans cette analyse, Prophet a montré une erreur de prévision considérablement plus faible que les autres modèles.

Regardons de plus près comment la qualité de la prévision a été mesurée dans l'article. Pour ce faire, nous aurons besoin de la formule d'erreur moyenne absolue en pourcentage.

Soit $y_{i}$ la *valeur réelle (historique)* et $\hat{y}_{i}$ la *valeur prévue* donnée par notre modèle.

$e_{i} = y_{i} - \hat{y}_{i}$ est alors *l'erreur de prévision* et $p_{i} =\frac{\displaystyle e_{i}}{\displaystyle y_{i}}$ est *l'erreur de prévision relative*.

Nous définissons

$$MAPE = mean\big(\left |p_{i} \right |\big)$$

MAPE est largement utilisé comme mesure de la précision des prédictions car il exprime l'erreur en pourcentage et peut donc être utilisé dans les évaluations de modèles sur différents ensembles de données.

De plus, lors de l'évaluation d'un algorithme de prévision, il peut s'avérer utile de calculer [MAE (Mean Absolute Error)](https://en.wikipedia.org/wiki/Mean _error_ absolue) afin d'avoir une image des erreurs en nombres absolus. En utilisant des composants précédemment définis, son équation sera

$$MAE = mean\big(\left |e_{i}\right |\big)$$

Quelques mots sur les algorithmes avec lesquels Prophet a été comparé. La plupart d'entre eux sont assez simples et sont souvent utilisés comme référence pour d'autres modèles:
* `naive` est une approche de prévision simpliste dans laquelle nous prédisons toutes les valeurs futures en nous appuyant uniquement sur l'observation au dernier moment disponible.
* `snaive` (saisonnier naïf) est un modèle qui fait des prédictions constantes en tenant compte des informations sur la saisonnalité. Par exemple, dans le cas de données saisonnières hebdomadaires pour chaque futur lundi, nous prédirions la valeur du dernier lundi et pour tous les futurs mardis, nous utiliserions la valeur du dernier mardi, etc.
* `mean` utilise la valeur moyenne des données comme prévision.
* `arima` signifie *Autoregressive Integrated Moving Average*, voir [Wikipedia](https://en.wikipedia.org/wiki/Autoregressive_integrated_moving_average) pour plus de détails.
* `ets` signifie *Lissage exponentiel*, voir [Wikipedia](https://en.wikipedia.org/wiki/Exponential_smoothing) pour plus d'informations.

## 3. Entraînez-vous avec Facebook Prophet

### 3.1 Installation en Python

Tout d'abord, vous devez installer la bibliothèque. Prophet est disponible pour Python et R. Le choix dépendra de vos préférences personnelles et des exigences du projet. Plus loin dans cet article, nous utiliserons Python.

En Python, vous pouvez installer Prophet à l'aide de PyPI:
```
$ pip install fbprophet
```

Dans R, vous trouverez le package CRAN correspondant. Reportez-vous à la [documentation](https://facebookincubator.github.io/prophet/docs/installation.html) pour plus de détails.

Importons les modules dont nous aurons besoin et initialisons notre environnement:

```python
import warnings

warnings.filterwarnings("ignore")

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import statsmodels.api as sm
from scipy import stats

%matplotlib inline
```

### 3.2 Jeu de données

Nous prédirons le nombre quotidien de publications publiées sur [Medium](https://medium.com/).

Tout d'abord, nous chargeons notre jeu de données.

```python
df = pd.read_csv("../../data/medium_posts.csv.zip", sep="\t")
```

Ensuite, nous omettons toutes les colonnes à l'exception de `published` et `url`. Le premier correspond à la dimension temporelle tandis que le second identifie de manière unique un message par son URL. Par la suite, nous nous débarrassons des doublons possibles et des valeurs manquantes dans les données:

```python
df = df[["published", "url"]].dropna().drop_duplicates()
```

Ensuite, nous devons convertir `published` au format datetime car par défaut `pandas` traite ce champ comme une chaîne.

```python
df["published"] = pd.to_datetime(df["published"])
```

Trions la trame de données par date et jetons un œil à ce que nous avons:

```python
df.sort_values(by=["published"]).head(n=3)
```

La date de sortie publique de Medium était le 15 août 2012. Mais, comme vous pouvez le voir sur les données ci-dessus, il existe au moins plusieurs lignes avec des dates de publication beaucoup plus anciennes. Ils sont apparus d'une manière ou d'une autre dans notre ensemble de données, mais ils ne sont guère légitimes. Nous allons simplement couper notre série chronologique pour ne conserver que les lignes qui tombent sur la période du 15 août 2012 au 25 juin 2017:

```python
df = df[
    (df["published"] > "2012-08-15") & (df["published"] < "2017-06-26")
].sort_values(by=["published"])
df.head(n=3)
```

```python
df.tail(n=3)
```

Comme nous allons prédire le nombre de publications, nous allons agréger et compter les publications uniques à chaque moment donné. Nous nommerons la nouvelle colonne correspondante `posts`:

```python
aggr_df = df.groupby("published")[["url"]].count()
aggr_df.columns = ["posts"]
```

Dans cette pratique, nous sommes intéressés par le nombre de messages **par jour**. Mais en ce moment, toutes nos données sont divisées en intervalles de temps irréguliers qui sont inférieurs à une journée. C'est ce qu'on appelle une série chronologique infra-journalière (*sub-daily time series*). Pour le voir, affichons les 3 premières lignes:

```python
aggr_df.head(n=3)
```

Pour résoudre ce problème, nous devons agréger le nombre de messages par "bins" d'une taille de date. Dans l'analyse des séries chronologiques, ce processus est appelé *rééchantillonnage* (*resampling*). Et si l'on *réduit* le taux d'échantillonnage des données, il est souvent appelé *sous-échantillonnage* (*downsampling*).

Heureusement, `pandas` a une fonctionnalité intégrée pour cette tâche. Nous allons rééchantillonner notre indice de date jusqu'à des "bins" d'un jour:

```python
daily_df = aggr_df.resample("D").apply(sum)
daily_df.head(n=3)
```

### 3.3 Analyse visuelle exploratoire

Comme toujours, il peut être utile et instructif de regarder une représentation graphique de vos données.

Nous allons créer un tracé de série chronologique pour toute la plage de temps. L'affichage de données sur une période aussi longue peut donner des indices sur la saisonnalité et les écarts anormaux visibles.

Tout d'abord, nous importons et initialisons la bibliothèque `Plotly`, qui permet de créer de superbes graphes interactifs:

```python
from plotly import graph_objs as go
from plotly.offline import init_notebook_mode, iplot

# Initialize plotly
init_notebook_mode(connected=True)
```

Nous définissons également une fonction d'aide, qui tracera nos trames de données tout au long de l'article:

```python
def plotly_df(df, title=""):
    """Visualize all the dataframe columns as line plots."""
    common_kw = dict(x=df.index, mode="lines")
    data = [go.Scatter(y=df[c], name=c, **common_kw) for c in df.columns]
    layout = dict(title=title)
    fig = dict(data=data, layout=layout)
    iplot(fig, show_link=False)
```

Essayons de tracer notre jeu de données *tel quel*:

```python
plotly_df(daily_df, title="Posts on Medium (daily)")
```

Les données à haute fréquence peuvent être assez difficiles à analyser. Même avec la possibilité de zoomer fournie par `Plotly`, il est difficile d'inférer quoi que ce soit de significatif à partir de ce graphique, à l'exception de la tendance à la hausse et à l'accélération.

Pour réduire le bruit, nous allons rééchantillonner le compte à rebours des postes jusqu'à la semaine. Outre le *binning*, d'autres techniques possibles de réduction du bruit incluent [Moving-Average Smoothing](https://en.wikipedia.org/wiki/Moving_average) et [Exponential Smoothing](https://en.wikipedia.org/wiki/Exponential_smoothing), entre autres.

Nous sauvegardons notre dataframe sous-échantillonné dans une variable distincte, car dans cette pratique, nous ne travaillerons qu'avec des séries journalières:

```python
weekly_df = daily_df.resample("W").apply(sum)
```

Enfin, nous traçons le résultat:

```python
plotly_df(weekly_df, title="Posts on Medium (weekly)")
```

Ce graphique sous-échantillonné s'avère un peu meilleur pour la perception d'un analyste.

L'une des fonctions les plus utiles fournies par `Plotly` est la possibilité de plonger rapidement dans différentes périodes de la chronologie afin de mieux comprendre les données et trouver des indices visuels sur les tendances possibles, les effets périodiques et irréguliers.

Par exemple, un zoom avant sur quelques années consécutives nous montre des points temporels correspondant aux vacances de Noël, qui influencent grandement les comportements humains.

Maintenant, nous allons omettre les premières années d'observations, jusqu'en 2015. Premièrement, elles ne contribueront pas beaucoup à la qualité des prévisions en 2017. Deuxièmement, ces premières années, ayant un nombre très faible de messages par jour, sont susceptible d'augmenter le bruit dans nos prévisions, car le modèle serait obligé d'ajuster ces données historiques anormales avec des données plus pertinentes et indicatives des dernières années.

```python
daily_df = daily_df.loc[daily_df.index >= "2015-01-01"]
daily_df.head(n=3)
```

Pour résumer, à partir de l'analyse visuelle, nous pouvons voir que notre ensemble de données n'est pas stationnaire avec une tendance croissante importante. Il montre également une saisonnalité hebdomadaire et annuelle et un certain nombre de jours anormaux chaque année.

### 3.4 Faire une prévision

L'API de Prophet est très similaire à celle que vous pouvez trouver dans `sklearn`. Nous créons d'abord un modèle, puis appelons la méthode `fit` et, enfin, faisons une prévision. L'entrée de la méthode `fit` est un` DataFrame` avec deux colonnes:
* `ds` (datestamp ou horodatage) doit être de type` date` ou `datetime`.
* `y` est une valeur numérique que nous voulons prédire.

Pour commencer, nous allons importer la bibliothèque et éliminer les messages de diagnostic sans importance:

```python
import logging

from fbprophet import Prophet

logging.getLogger().setLevel(logging.ERROR)
```

Convertissons notre dataframe de données au format requis par Prophet:

```python
df = daily_df.reset_index()
df.columns = ["ds", "y"]
df.tail(n=3)
```

Les auteurs de la bibliothèque conseillent généralement de faire des prédictions basées sur au moins plusieurs mois, idéalement, plus d'un an de données historiques. Heureusement, dans notre cas, nous avons plus de quelques années de données pour s'adapter au modèle.

Pour mesurer la qualité de nos prévisions, nous devons diviser notre ensemble de données en une *partie historique*, qui est la première et la plus grande tranche de nos données, et une *partie prédiction*, qui sera située à la fin de la chronologie. Nous allons supprimer le dernier mois de l'ensemble de données afin de l'utiliser plus tard comme cible de prédiction:

```python
prediction_size = 30
train_df = df[:-prediction_size]
train_df.tail(n=3)
```

Maintenant, nous devons créer un nouvel objet `Prophet`. Ici, nous pouvons passer les paramètres du modèle dans le constructeur. Mais dans cet article, nous utiliserons les valeurs par défaut. Ensuite, nous formons notre modèle en invoquant sa méthode `fit` sur notre jeu de données de formation:

```python
m = Prophet()
m.fit(train_df);
```

En utilisant la méthode `Prophet.make_future_dataframe`, nous créons un dataframe qui contiendra toutes les dates de l'historique et s'étendra également dans le futur pour les 30 jours que nous avons omis auparavant.

```python
future = m.make_future_dataframe(periods=prediction_size)
future.tail(n=3)
```

Nous prédisons les valeurs avec `Prophet` en passant les dates pour lesquelles nous voulons créer une prévision. Si nous fournissons également les dates historiques (comme dans notre cas), en plus de la prédiction, nous obtiendrons un ajustement dans l'échantillon pour l'historique. Appelons la méthode `predict` du modèle avec notre dataframe `future` en entrée:

```python
forecast = m.predict(future)
forecast.tail(n=3)
```

Dans le dataframe résultant, vous pouvez voir de nombreuses colonnes caractérisant la prédiction, y compris les composants de tendance et de saisonnalité ainsi que leurs intervalles de confiance. La prévision elle-même est stockée dans la colonne `yhat`.

La bibliothèque Prophet possède ses propres outils de visualisation intégrés qui nous permettent d'évaluer rapidement le résultat.

Tout d'abord, il existe une méthode appelée `Prophet.plot` qui trace tous les points de la prévision:

```python
m.plot(forecast);
```

Ce graphique n'a pas l'air très informatif. La seule conclusion définitive que nous pouvons tirer ici est que le modèle a traité de nombreux points de données comme des valeurs aberrantes.

La deuxième fonction `Prophet.plot_components` pourrait être beaucoup plus utile dans notre cas. Il nous permet d'observer différentes composantes du modèle séparément: tendance, saisonnalité annuelle et hebdomadaire. De plus, si vous fournissez des informations sur les vacances et les événements à votre modèle, elles seront également affichées dans ce graphique.

Essayons-le:

```python
m.plot_components(forecast);
```

Comme vous pouvez le voir sur le graphique des tendances, Prophet a fait du bon travail en adaptant la croissance accélérée des nouveaux messages à la fin de 2016. Le graphique de la saisonnalité hebdomadaire conduit à la conclusion qu'il y a généralement moins de nouveaux messages le samedi et le dimanche que le les autres jours de la semaine. Dans le graphique de saisonnalité annuelle, il y a une baisse importante le jour de Noël.

### 3.5 Évaluation de la qualité des prévisions

Évaluons la qualité de l'algorithme en calculant les mesures d'erreur pour les 30 derniers jours que nous avons prédits. Pour cela, nous aurons besoin des observations $y_i$ et des valeurs prédites correspondantes $\hat{y}_i$.

Examinons l'objet `forecast` que la bibliothèque a créé pour nous:

```python
print(", ".join(forecast.columns))
```

Nous pouvons voir que cette base de données contient toutes les informations dont nous avons besoin, à l'exception des valeurs historiques. Nous devons joindre l'objet `forecast` avec les valeurs réelles `y` de l'ensemble de données d'origine `df`. Pour cela nous allons définir une helper fonction que nous réutiliserons plus tard:

```python
def make_comparison_dataframe(historical, forecast):
    """Join the history with the forecast.

       The resulting dataset will contain columns 'yhat', 'yhat_lower', 'yhat_upper' and 'y'.
    """
    return forecast.set_index("ds")[["yhat", "yhat_lower", "yhat_upper"]].join(
        historical.set_index("ds")
    )
```

Appliquons cette fonction à notre dernière prévision:

```python
cmp_df = make_comparison_dataframe(df, forecast)
cmp_df.tail(n=3)
```

Nous allons également définir une helper fonction que nous utiliserons pour évaluer la qualité de nos prévisions avec les mesures d'erreur MAPE et MAE:

```python
def calculate_forecast_errors(df, prediction_size):
    """Calculate MAPE and MAE of the forecast.

       Args:
           df: joined dataset with 'y' and 'yhat' columns.
           prediction_size: number of days at the end to predict.
    """

    # Make a copy
    df = df.copy()

    # Now we calculate the values of e_i and p_i according to the formulas given in the article above.
    df["e"] = df["y"] - df["yhat"]
    df["p"] = 100 * df["e"] / df["y"]

    # Recall that we held out the values of the last `prediction_size` days
    # in order to predict them and measure the quality of the model.

    # Now cut out the part of the data which we made our prediction for.
    predicted_part = df[-prediction_size:]

    # Define the function that averages absolute error values over the predicted part.
    error_mean = lambda error_name: np.mean(np.abs(predicted_part[error_name]))

    # Now we can calculate MAPE and MAE and return the resulting dictionary of errors.
    return {"MAPE": error_mean("p"), "MAE": error_mean("e")}
```

Utilisons notre fonction:

```python
for err_name, err_value in calculate_forecast_errors(cmp_df, prediction_size).items():
    print(err_name, err_value)
```

En conséquence, l'erreur relative de notre prévision (MAPE) est d'environ 22,72%, et en moyenne notre modèle est erroné de 70,45 posts (MAE).

### 3.6 Visualisation

Créons notre propre visualisation du modèle construit par Prophet. Il comprendra les valeurs réelles, les prévisions et les intervalles de confiance.

Premièrement, nous allons tracer les données sur une période de temps plus courte pour rendre les points de données plus faciles à distinguer. Deuxièmement, nous ne montrerons les performances du modèle que pour la période que nous avons prévue, c'est-à-dire les 30 derniers jours. Il semble que ces deux mesures devraient nous donner un graphique plus lisible.

Troisièmement, nous utiliserons `Plotly` pour rendre notre graphique interactif, ce qui est idéal pour l'exploration.

Nous définirons notre propre helper fonction `show_forecast` et l'appellerons (pour en savoir plus sur son fonctionnement, veuillez vous référer aux commentaires dans le code et la [documentation](https://plot.ly/python/)):

```python
def show_forecast(cmp_df, num_predictions, num_values, title):
    """Visualize the forecast."""

    def create_go(name, column, num, **kwargs):
        points = cmp_df.tail(num)
        args = dict(name=name, x=points.index, y=points[column], mode="lines")
        args.update(kwargs)
        return go.Scatter(**args)

    lower_bound = create_go(
        "Lower Bound",
        "yhat_lower",
        num_predictions,
        line=dict(width=0),
        marker=dict(color="444"),
    )
    upper_bound = create_go(
        "Upper Bound",
        "yhat_upper",
        num_predictions,
        line=dict(width=0),
        marker=dict(color="444"),
        fillcolor="rgba(68, 68, 68, 0.3)",
        fill="tonexty",
    )
    forecast = create_go(
        "Forecast", "yhat", num_predictions, line=dict(color="rgb(31, 119, 180)")
    )
    actual = create_go("Actual", "y", num_values, marker=dict(color="red"))

    # In this case the order of the series is important because of the filling
    data = [lower_bound, upper_bound, forecast, actual]

    layout = go.Layout(yaxis=dict(title="Posts"), title=title, showlegend=False)
    fig = go.Figure(data=data, layout=layout)
    iplot(fig, show_link=False)

show_forecast(cmp_df, prediction_size, 100, "New posts on Medium")
```

À première vue, la prédiction des valeurs moyennes par notre modèle semble raisonnable. La valeur élevée de MAPE que nous avons obtenue ci-dessus peut s'expliquer par le fait que le modèle n'a pas réussi à saisir l'amplitude croissante de pic-à-pic (peak-to-peak) d'une faible saisonnalité.

En outre, nous pouvons conclure du graphique ci-dessus que de nombreuses valeurs réelles se trouvent en dehors de l'intervalle de confiance. Prophet peut ne pas convenir aux séries chronologiques avec une variance instable, du moins lorsque les paramètres par défaut sont utilisés. Nous allons essayer de résoudre ce problème en appliquant une transformation à nos données.

## 4. Transformation Box-Cox

Jusqu'à présent, nous avons utilisé Prophet avec les paramètres par défaut et les données d'origine. Nous laisserons les paramètres du modèle seuls. Mais malgré cela, nous avons encore des progrès à faire. Dans cette section, nous appliquerons la [Box–Cox transformation](http://onlinestatbook.com/2/transformations/box-cox.html) à notre série originale. Voyons où cela nous mènera.

Quelques mots sur cette transformation. Il s'agit d'une transformation de données monotone qui peut être utilisée pour stabiliser la variance. Nous utiliserons la transformation Box-Cox à un paramètre, qui est définie par l'expression suivante:

$$
\begin{equation}
  boxcox^{(\lambda)}(y_{i}) = \begin{cases}
    \frac{\displaystyle y_{i}^{\lambda} - 1}{\displaystyle \lambda} &, \text{if $\lambda \neq 0$}.\\
    ln(y_{i}) &, \text{if $\lambda = 0$}.
  \end{cases}
\end{equation}
$$

Nous devrons implémenter l'inverse de cette fonction afin de pouvoir restaurer l'échelle de données d'origine. Il est facile de voir que l'inverse est défini comme:

$$
\begin{equation}
  invboxcox^{(\lambda)}(y_{i}) = \begin{cases}
    e^{\left (\frac{\displaystyle ln(\lambda y_{i} + 1)}{\displaystyle \lambda} \right )} &, \text{if $\lambda \neq 0$}.\\
    e^{y_{i}} &, \text{if $\lambda = 0$}.
  \end{cases}
\end{equation}
$$

La fonction correspondante en Python est implémentée comme suit:

```python
def inverse_boxcox(y, lambda_):
    return np.exp(y) if lambda_ == 0 else np.exp(np.log(lambda_ * y + 1) / lambda_)
```

Tout d'abord, nous préparons notre jeu de données en définissant son index:

```python
train_df2 = train_df.copy().set_index("ds")
```

Ensuite, nous appliquons la fonction `stats.boxcox` de` Scipy`, qui applique la transformation Box – Cox. Dans notre cas, il renverra deux valeurs. La première est la série transformée et la seconde est la valeur trouvée de $\lambda$ qui est optimale en termes de maximum de log-vraisemblance (maximum log-likelihood):

```python
train_df2["y"], lambda_prophet = stats.boxcox(train_df2["y"])
train_df2.reset_index(inplace=True)
```

Nous créons un nouveau modèle `Prophet` et répétons le cycle d'ajustement de prévision que nous avons déjà fait ci-dessus:

```python
m2 = Prophet()
m2.fit(train_df2)
future2 = m2.make_future_dataframe(periods=prediction_size)
forecast2 = m2.predict(future2)
```

À ce stade, nous devons inverser la transformation de Box – Cox avec notre fonction inverse et la valeur connue de $\lambda$:

```python
for column in ["yhat", "yhat_lower", "yhat_upper"]:
    forecast2[column] = inverse_boxcox(forecast2[column], lambda_prophet)
```

Ici, nous allons réutiliser nos outils pour faire le dataframe de comparaison et calculer les erreurs:

```python
cmp_df2 = make_comparison_dataframe(df, forecast2)
for err_name, err_value in calculate_forecast_errors(cmp_df2, prediction_size).items():
    print(err_name, err_value)
```

On peut donc affirmer avec certitude que la qualité du modèle s'est améliorée.

Enfin, tracons nos performances précédentes avec les derniers résultats côte à côte. Notez que nous utilisons `prediction_size` pour le troisième paramètre afin de zoomer sur l'intervalle prévu:

```python
show_forecast(cmp_df, prediction_size, 100, "No transformations")
show_forecast(cmp_df2, prediction_size, 100, "Box–Cox transformation")
```

Nous voyons que la prévision des changements hebdomadaires dans le deuxième graphique est beaucoup plus proche des valeurs réelles maintenant.

## 5. Résumé

Nous avons jeté un coup d'œil à *Prophet*, une bibliothèque de prévisions open source spécifiquement destinée aux séries chronologiques commerciales. Nous avons également effectué des exercices pratiques de prévision des séries chronologiques.

Comme nous l'avons vu, la bibliothèque Prophet ne fait pas de merveilles et ses prédictions prêtes à l'emploi ne sont pas [idéales](https://en.wikipedia.org/wiki/No_free_lunch_in_search_and_optimization). Il appartient toujours au data scientist d'explorer les résultats des prévisions, d'ajuster les paramètres du modèle et de transformer les données si nécessaire.

Toutefois, cette bibliothèque est conviviale et facilement personnalisable. La seule possibilité de prendre en compte les jours anormaux connus de l'analyste à l'avance peut faire la différence dans certains cas

Dans l'ensemble, la bibliothèque Prophet vaut la peine de faire partie de votre boîte à outils analytiques.

## 6. Références

- Official [Prophet repository](https://github.com/facebookincubator/prophet) on GitHub.
- Official [Prophet documentation](https://facebookincubator.github.io/prophet/docs/quick_start.html).
- Sean J. Taylor, Benjamin Letham ["Forecasting at scale"](https://facebookincubator.github.io/prophet/static/prophet_paper_20170113.pdf) — scientific paper explaining the algorithm which lays the foundation of `Prophet`.
- [Forecasting Website Traffic Using Facebook’s Prophet Library](http://pbpython.com/prophet-overview.html) — `Prophet` overview with an example of website traffic forecasting.
- Rob J. Hyndman, George Athanasopoulos ["Forecasting: principles and practice"](https://www.otexts.org/fpp) – a very good online book about time series forecasting.


Before you turn in your homework, make sure everything runs as expected.

Make sure you execute every single code cell, in order, filling with your solutions in any place that says `# YOUR CODE HERE`, and always DELETE the line that says:

```python
raise NotImplementedError()
```

The purpose of this line is to tell you if you forgot to answer a question (it will throw an error if the line is there)

**IMPORTANT:**

* **DO NOT DELETE ANY CELL** and do not change the title of the Notebook.

* Use the same variable names as the ones written in the questions; otherwise, the tests will fail.

* Before you turn in your homework, make sure everything runs as expected: restart the kernel (in the menubar, select Kernel $\rightarrow$ Restart) and then run all cells (in the menubar, select Cell $\rightarrow$ Run All).

Fill your name below:

```python
name = "Yinfeng Ding"
```

# Sod's test problems

Sod's test problems are standard benchmarks used to assess the accuracy of numerical solvers. The tests use a classic example of one-dimensional compressible flow: the shock-tube problem. Sod (1978) chose initial conditions and numerical discretization parameters for the shock-tube problem and used these to test several schemes, including Lax-Wendroff and MacCormack's. Since then, many others have followed Sod's example and used the same tests on new numerical methods.

The shock-tube problem is so useful for testing numerical methods because it is one of the few problems that allows an exact solution of the Euler equations for compressible flow.

This notebook complements the previous lessons of the course module [_"Riding the wave: convection problems"_](https://github.com/numerical-mooc/numerical-mooc/tree/master/lessons/03_wave) with Sod's test problems as an independent coding exercise. We'll lay out the problem for you, but leave important bits of code for you to write on your own. Good luck!

## What's a shock tube?

A shock tube is an idealized device that generates a one-dimensional shock wave in a compressible gas. The setting allows an analytical solution of the Euler equations, which is very useful for comparing with the numerical results to assess their accuracy.

Picture a tube with two regions containing gas at different pressures, separated by an infinitely-thin, rigid diaphragm. The gas is initially at rest, and the left region is at a higher pressure than the region to the right of the diaphragm. At time $t = 0.0 s$, the diaphragm is ruptured instantaneously.

What happens?

You get a shock wave.  The gas at high pressure, no longer constrained by the diaphragm, rushes into the lower-pressure area and a one-dimensional unsteady flow is established, consisting of:

* a shock wave traveling to the right
* an expansion wave traveling to the left
* a moving contact discontinuity

The shock-tube problem is an example of a *Riemann problem* and it has an analytical solution, as we said. The situation is illustrated in Figure 1.

<center> Figure 1: The shock-tube problem. </center>

## The Euler equations

The Euler equations govern the motion of an inviscid fluid (no viscosity). They consist of the conservation laws of mass and momentum, and often we also need to work with the energy equation.

Let's consider a 1D flow with velocity $u$ in the $x$-direction. The Euler equations for a fluid with density $\rho$ and pressure $p$ are:

$$
\begin{cases}
    &\frac{\partial \rho}{\partial t} + \frac{\partial}{\partial x}(\rho u) = 0 \\
    &\frac{\partial}{\partial t}(\rho u) + \frac{\partial}{\partial x} (\rho u^2 + p)=0
\end{cases}
$$

... plus the energy equation, which we can write in this form:

$$
\begin{equation}
\frac{\partial}{\partial t}(\rho e_T) + \frac{\partial}{\partial x} (\rho u e_T +p u)=0
\end{equation}
$$

where $e_T=e+u^2/2$ is the total energy per unit mass, equal to the internal energy plus the kinetic energy (per unit mass).

Written in vector form, you can see that the Euler equations bear a strong resemblance to the traffic-density equation that has been the focus of this course module so far. Here is the vector representation of the Euler equation:

$$
\begin{equation}
\frac{\partial }{\partial t} \underline{\mathbf{u}} + \frac{\partial }{\partial x} \underline{\mathbf{f}} = 0
\end{equation}
$$

The big difference with our previous work is that the variables $\underline{\mathbf{u}}$ and $\underline{\mathbf{f}}$ are *vectors*.  If you review the [Phugoid Full Model](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/01_phugoid/01_03_PhugoidFullModel.ipynb) lesson, you will recall that we can solve for several values at once using the vector form of an equation.  In the Phugoid Module, it was an ODE—now we apply the same procedure to a PDE.

Let's take a look at what $\underline{\mathbf{u}}$ and $\underline{\mathbf{f}}$ consist of.

## The conservative form

Many works in the early days of computational fluid dynamics in the 1960s showed that using the conservation form of the Euler equations is more accurate for situations with shock waves.  And as you already saw, the shock-tube solutions do contain shocks.

The conserved variables $\underline{\mathbf{u}}$ for Euler's equations are

$$
\begin{equation}
\underline{\mathbf{u}} = \left[
\begin{array}{c}
\rho \\
\rho u \\
\rho e_T \\
\end{array}
\right]
\end{equation}
$$

where $\rho$ is the density of the fluid, $u$ is the velocity of the fluid and $e_T = e + \frac{u^2}{2}$ is the specific total energy; $\underline{\mathbf{f}}$ is the flux vector:

$$
\begin{equation}
\underline{\mathbf{f}} = \left[
\begin{array}{c}
\rho u \\
\rho u^2 + p \\
(\rho e_T + p) u \\
\end{array}
\right]
\end{equation}
$$

where $p$ is the pressure of the fluid.

If we put together the conserved variables and the flux vector into our PDE, we get the following set of equations:

$$
\begin{equation}
    \frac{\partial}{\partial t}
    \left[
        \begin{array}{c}
            \rho \\
            \rho u \\
            \rho e_T \\
        \end{array}
    \right] +
    \frac{\partial}{\partial x}
    \left[
        \begin{array}{c}
            \rho u \\
            \rho u^2 + p \\
            (\rho e_T + p) u \\
        \end{array}
    \right] =
    0
\end{equation}
$$

There's one major problem there.  We have 3 equations and 4 unknowns.  But there is a solution!  We can use an equation of state to calculate the pressure—in this case, we'll use the ideal gas law.

## Calculating the pressure

For an ideal gas, the equation of state is

$$
e = e(\rho, p) = \frac{p}{(\gamma -1) \rho}
$$

where $\gamma = 1.4$ is a reasonable value to model air,

$$
\therefore p = (\gamma -1)\rho e
$$

Recall from above that

$$
e_T = e+\frac{1}{2} u^2
$$

$$
\therefore e = e_T - \frac{1}{2}u^2
$$

Putting it all together, we arrive at an equation for the pressure

$$
p = (\gamma -1)\left(\rho e_T - \frac{\rho u^2}{2}\right)
$$

## Flux in terms of $\underline{\mathbf{u}}$

With the traffic model, the flux was a function of traffic density.  For the Euler equations, the three equations we have are coupled and the flux *vector* is a function of $\underline{\mathbf{u}}$, the vector of conserved variables:

$$
\underline{\mathbf{f}} = f(\underline{\mathbf{u}})
$$

In order to get everything squared away, we need to represent $\underline{\mathbf{f}}$ in terms of $\underline{\mathbf{u}}$.
We can introduce a little shorthand for the $\underline{\mathbf{u}}$ and $\underline{\mathbf{f}}$ vectors and define:

$$
\underline{\mathbf{u}} =
\left[
    \begin{array}{c}
        u_1 \\
        u_2 \\
        u_3 \\
    \end{array}
\right] =
\left[
    \begin{array}{c}
        \rho \\
        \rho u \\
        \rho e_T \\
    \end{array}
\right]
$$

$$
\underline{\mathbf{f}} =
\left[
    \begin{array}{c}
        f_1 \\
        f_2 \\
        f_3 \\
    \end{array}
\right] =
\left[
    \begin{array}{c}
        \rho u \\
        \rho u^2 + p \\
        (\rho e_T + p) u \\
    \end{array}
\right]
$$

With a little algebraic trickery, we can represent the pressure vector using quantities from the $\underline{\mathbf{u}}$ vector.

$$
p = (\gamma -1)\left(u_3 - \frac{1}{2} \frac{u^2_2}{u_1} \right)
$$

Now that pressure can be represented in terms of $\underline{\mathbf{u}}$, the rest of $\underline{\mathbf{f}}$ isn't too difficult to resolve:

$$\underline{\mathbf{f}} = \left[ \begin{array}{c}
f_1 \\
f_2 \\
f_3 \\ \end{array} \right] =
\left[ \begin{array}{c}
u_2\\
\frac{u^2_2}{u_1} + (\gamma -1)\left(u_3 - \frac{1}{2} \frac{u^2_2}{u_1} \right) \\
\left(u_3 + (\gamma -1)\left(u_3 - \frac{1}{2} \frac{u^2_2}{u_1}\right) \right) \frac{u_2}{u_1}\\ \end{array}
\right]$$

## Test conditions

The first test proposed by Sod in his 1978 paper is as follows.

In a tube spanning from $x = -10 \text{m}$ to $x = 10 \text{m}$ with the rigid membrane at $x = 0 \text{m}$, we have the following initial gas states:

$$
\underline{IC}_L =
\left[
    \begin{array}{c}
        \rho_L \\
        u_L \\
        p_L \\
    \end{array}
\right] =
\left[
    \begin{array}{c}
        1.0 \, kg/m^3 \\
        0 \, m/s \\
        100 \, kN/m^2 \\
    \end{array}
\right]
$$

$$
\underline{IC}_R =
\left[
    \begin{array}{c}
        \rho_R \\
        u_R \\
        p_R \\
    \end{array}
\right] =
\left[
    \begin{array}{c}
        0.125 \, kg/m^3 \\
        0 \, m/s \\
        10 \, kN/m^2 \\
    \end{array}
\right]
$$

where $\underline{IC}_L$ are the initial density, velocity and pressure on the left side of the tube membrane and $\underline{IC}_R$ are the initial density, velocity and pressure on the right side of the tube membrane.

The analytical solution to this test for the velocity, pressure and density, looks like the plots in Figure 2.

<center> Figure 2. Analytical solution for Sod's first test. </center>

## The Richtmyer method

For this exercise, you will use the **Lax-Friedrichs** scheme that we implemented in [lesson 2](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/03_wave/03_02_convectionSchemes.ipynb).
But, we will also be using a new scheme called the **Richtmyer** method.
Like the MacCormack method, Richtmyer is a *two-step method*, given by:

$$
\begin{align}
\underline{\mathbf{u}}^{n+\frac{1}{2}}_{i+\frac{1}{2}} &= \frac{1}{2} \left( \underline{\mathbf{u}}^n_{i+1} + \underline{\mathbf{u}}^n_i \right) -
\frac{\Delta t}{2 \Delta x} \left( \underline{\mathbf{f}}^n_{i+1} - \underline{\mathbf{f}}^n_i\right) \\
\underline{\mathbf{u}}^{n+1}_i &= \underline{\mathbf{u}}^n_i - \frac{\Delta t}{\Delta x} \left(\underline{\mathbf{f}}^{n+\frac{1}{2}}_{i+\frac{1}{2}} - \underline{\mathbf{f}}^{n+\frac{1}{2}}_{i-\frac{1}{2}} \right)
\end{align}
$$

The flux vectors used in the second step are obtained by evaluating the flux functions on the output of the first step:

$$
\underline{\mathbf{f}}^{n+\frac{1}{2}}_{i+\frac{1}{2}} = \underline{\mathbf{f}}\left(\underline{\mathbf{u}}^{n+\frac{1}{2}}_{i+\frac{1}{2}}\right)
$$

The first step is like a *predictor* of the solution: if you look closely, you'll see that we are applying a Lax-Friedrichs scheme here. The second step is a *corrector* that applies a leapfrog update. Figure 3 gives a sketch of the stencil for Richtmyer method, where the "intermediate time" $n+1/2$ will require a temporary variable in your code, just like we had in the MacCormack scheme.

<center> Figure 3. Stencil of Richtmyer scheme. </center>

## Implement your solution (40 points)

---

Your mission, should you wish to accept it, is to calculate the pressure, density and velocity along the shock tube at time $t = 0.01 s$ using the Richtmyer method **and** the Lax-Friedrichs method. Good luck!

Code parameters to use:

* Number of discrete points along the 1D domain: `nx = 81` (which gives `dx = 0.25` for a domain of length 20).
* Time-step size: `dt = 0.0002`.
* Heat capacity ratio: `gamma = 1.4`.

Implement your solution in this section.
You can use as many code cells as you want.

```python
# YOUR CODE HERE
import numpy
import sympy
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
sympy.init_printing()
```

```python
# Set parameters.
nx = 81
dx = 0.25
dt = 0.0002
gamma = 1.4
t = 0.01
nt = int(t/dt)+1
```

```python
# Get the grid point coordinates.
x = numpy.linspace(-10,10,num = nx)

# Set the initial conditions.
rho0 = numpy.ones(nx)
mask = numpy.where(x >= 0)
rho0[mask] = 0.125
p0 = 100000*numpy.ones(nx)
p0[mask] = 10000
v0 = numpy.zeros(nx)
e0 = p0 / ((gamma-1) * rho0)
eT0 = e0 + 0.5 * v0**2

u0 = numpy.array([rho0,
                 rho0*v0,
                 rho0*eT0])
f0 = numpy.array([u0[1],
                 u0[1]**2 / u0[0] + (gamma-1)*(u0[2] - 0.5*u0[1]**2 / u0[0]),
                 (u0[2] + (gamma - 1) * (u0[2] - 0.5*u0[1]**2 / u0[0])) * u0[1] / u0[0]])
```

```python
# Richtmyer scheme, two step method, R1, R2
u_R2 = u0.copy()
u_R1 = u_R2.copy()
f_R2 = f0.copy()

for i in range(1, nt):
    u_R1 = 0.5 * (u_R2[:,1:] + u_R2[:,:-1]) - dt / (2 * dx) * (f_R2[:,1:] - f_R2[:,:-1])# first step is like a predictor of the solution
    f_R1 = numpy.array([u_R1[1],
                        u_R1[1]**2 / u_R1[0] + (gamma - 1) * (u_R1[2] - 0.5 * u_R1[1]**2 / u_R1[0]),
                        (u_R1[2] + (gamma -1) * (u_R1[2] - 0.5 * u_R1[1]**2 / u_R1[0])) * u_R1[1] / u_R1[0]])
    u_R2[:,1:-1] = u_R2[:,1:-1] - dt / dx * (f_R1[:,1:] - f_R1[:,:-1])# corrector that applies a leapfrog update, advance in time
    f_R2 = numpy.array([u_R2[1],
                        u_R2[1]**2 / u_R2[0] + (gamma - 1) * (u_R2[2] - 0.5 * u_R2[1]**2 / u_R2[0]),
                       (u_R2[2] + (gamma -1) * (u_R2[2] - 0.5 * u_R2[1]**2 / u_R2[0])) * u_R2[1] / u_R2[0]])

rho_Richtmyer = u_R2[0]
v_Richtmyer = u_R2[1] / u_R2[0]
p_Richtmyer = (gamma -1) * (u_R2[2] - 0.5 * u_R2[1]**2 / u_R2[0])
```

```python
# Lax-Friedrichs scheme
u_L = u0.copy()
f_L = f0.copy()
for n in range(1, nt):
    # Advance in time using Lax-Friedrichs scheme.
    u_L[:,1:-1] = 0.5*(u_L[:,:-2] + u_L[:,2:]) - 0.5*dt/dx * (f_L[:,2:] - f_L[:,:-2])
    f_L = numpy.array([u_L[1],
                      u_L[1]**2 / u_L[0] + (gamma - 1) * (u_L[2] - 0.5 * u_L[1]**2 / u_L[0]),
                     (u_L[2] + (gamma -1) * (u_L[2] - 0.5 * u_L[1]**2 / u_L[0])) * u_L[1] / u_L[0]])
rho_Lax = u_L[0]
v_Lax = u_L[1] / u_L[0]
p_Lax = (gamma -1) * (u_L[2] - 0.5 * u_L[1]**2 / u_L[0])
```

## Assessment (80 points)

---

Answer questions in this section.

Do not try to delete or modify empty code cells that are already present.
For each question, provide your answer in the cell **just above** the empty cell.
(This empty cell contains hidden tests to assert the correctness of your answer and cannot be deleted.)
Pay attention to the name of the variables we ask you to create to store computed values; if the name of the variable is misspelled, the test will fail.

```python
try:
    import mooc37 as mooc
except:
    import mooc36 as mooc
```

* **Q1 (10 points):** Plot the numerical solution of the density, velocity, and pressure at time $t = 0.01 s$ obtained with the Richtmyer scheme **and** with the Lax-Friedrichs scheme.

You should also plot the analytical solution.
The analytical solution can be obtained using the function `analytical_solution` from the Python file `sod.py` (located in the same folder than the Jupyter Notebook).
To import the function in your Notebook, use `from sod import analytical_solution`.
You can use `help(analytical_solution)` to see how you should call the function.

Create one figure per variable and make sure to label your axes.
(For example, the first figure should contain the numerical solution of the density using both schemes, as well as the analytical solution for the density.)
Make sure to add a legend to your plots.

```python
# YOUR CODE HERE
from sod import analytical_solution
help(analytical_solution)
```

    Help on function analytical_solution in module sod:

    analytical_solution(t, x, left_state, right_state, diaphragm=0.0, gamma=1.4)
        Compute the analytical solution of the Sod's test at a given time.

        Parameters
        ----------
        t : float
            The time.
        x : numpy.ndarray
            Coordinates along the tube (as a 1D array of floats).
        left_state : tuple or list
            Initial density, velocity, and pressure values
            on left side of the diaphragm.
            The argument should be a tuple or list with 3 floats.
        right_state : tuple or list
            Initial density, velocity, and pressure values
            on right side of the diaphragm.
            The argument should be a tuple or list with 3 floats.
        diaphragm : float, optional
            Location of the diaphgram (membrane), by default 0.0.
        gamma : float, optional
            Heat capacity ratio, by default 1.4.

        Returns
        -------
        tuple of numpy.ndarray objects
            The density, velocity, and pressure along the tube at the given time.
            This is a tuple with 3 elements: (density, velocity, pressure).
            Each element is a 1D NumPy array of floats.

```python
# Analytical solution
# Set the initial conditions.
left_state = [1.0, 0.0, 100000.0]
right_state = [0.125, 0.0, 10000.0]

# Analytical solution at t = 0.01
A = analytical_solution(t, x, left_state, right_state, diaphragm=0.0, gamma=1.4)
rho_analytical = A[0]
v_analytical = A[1]
p_analytical = A[2]
```

```python
# Plot rho
pyplot.figure(figsize=(6.0, 6.0))
pyplot.title('Density at time 0.01s')
pyplot.xlabel('x')
pyplot.ylabel('rho')
pyplot.grid()
pyplot.plot(x, rho_Richtmyer, label='Richtmyer', color='C0', linestyle='-', linewidth=2)
pyplot.plot(x, rho_Lax, label='Lax-Friedrich', color='C1', linestyle='-', linewidth=2)
pyplot.plot(x, rho_analytical, label='Analytical', color='C2', linestyle='-', linewidth=2)
pyplot.legend()
pyplot.xlim(-10.0, 10.0)
pyplot.ylim(0.0, 1.1)
```

```python
# Plot velocity
pyplot.figure(figsize=(6.0, 6.0))
pyplot.title('Velocity at time 0.01s')
pyplot.xlabel('x')
pyplot.ylabel('velocity')
pyplot.grid()
pyplot.plot(x, v_Richtmyer, label='Richtmyer', color='C0', linestyle='-', linewidth=2)
pyplot.plot(x, v_Lax, label='Lax-Friedrich', color='C1', linestyle='-', linewidth=2)
pyplot.plot(x, v_analytical, label='Analytical', color='C2', linestyle='-', linewidth=2)
pyplot.legend()
pyplot.xlim(-10.0, 10.0)
pyplot.ylim(0.0, 400.0)
```

```python
# Plot pressure
pyplot.figure(figsize=(6.0, 6.0))
pyplot.title('Pressure at time 0.01s')
pyplot.xlabel('x')
pyplot.ylabel('pressure')
pyplot.grid()
pyplot.plot(x, p_Richtmyer, label='Richtmyer', color='C0', linestyle='-', linewidth=2)
pyplot.plot(x, p_Lax, label='Lax-Friedrich', color='C1', linestyle='-', linewidth=2)
pyplot.plot(x, p_analytical, label='analytical', color='C2', linestyle='-', linewidth=2)
pyplot.legend()
pyplot.xlim(-10.0, 10.0)
pyplot.ylim(0.0, 110000.0)
```

* **Q2 (10 points):** At $t = 0.01 s$, what type of numerical errors to you observe in the numerical solution obtained with the Richtmyer scheme and with the Lax-Friedrichs scheme? (Diffusion errors? Dispersion errors? Explain why.)

You should write your answer in the following Markdown cell.

YOUR ANSWER HERE

The Richtmyer scheme has dispersion errors. Observing the curve, we can find that the richtmyer scheme curve is closer to the analytical curve, and the curve oscillates, which is achieved through second-order accuracy. Numerical dispersion occurs when a higher order discretisation scheme is used to improve accuracy of the result. Numerical dispersion often takes the form of so-called 'spurious oscillations'. This is due to the truncation error of the discretisation. This is due to the truncation error of the discretisation. A second order upwind method, the leading truncation error is odd. And odd order derivatives contribute to numerical dispersion.

The Lax-Friedrichs scheme has diffusion errors. substituting  𝜌𝑛𝑖  by the average of its neighbors introduces a first-order error. Numerical diffusion occurs when 1st order discretisation are used. This is due to the truncation error of the discretisation. The truncation is an odd-order method, the leading truncation error is even. Even order derivatives in the truncation error contribute to numerical diffusion.

* **Q3 (5 points):** At $t = 0.01 s$, what's the $L_2$-norm of the difference between the density obtained with the Richtmyer scheme and the analytical solution?

Store your result in the variable `l2_norm1`; you can check your answer by calling the function `mooc.check('hw3_l2_norm1', l2_norm1)`.

**WARNING:** the variable name `l2_norm1` is spelled with the number `1`, **not** the letter `l`.

```python
# YOUR CODE HERE
Diff = rho_Richtmyer - rho_analytical
help(numpy.linalg.norm)
l2_norm1 = numpy.linalg.norm(Diff, ord=2, axis=0)
print(l2_norm1)
mooc.check('hw3_l2_norm1', l2_norm1)
```

    Help on function norm in module numpy.linalg:

    norm(x, ord=None, axis=None, keepdims=False)
        Matrix or vector norm.

        This function is able to return one of eight different matrix norms,
        or one of an infinite number of vector norms (described below), depending
        on the value of the ``ord`` parameter.

        Parameters
        ----------
        x : array_like
            Input array.  If `axis` is None, `x` must be 1-D or 2-D, unless `ord`
            is None. If both `axis` and `ord` are None, the 2-norm of
            ``x.ravel`` will be returned.
        ord : {non-zero int, inf, -inf, 'fro', 'nuc'}, optional
            Order of the norm (see table under ``Notes``). inf means numpy's
            `inf` object. The default is None.
        axis : {None, int, 2-tuple of ints}, optional.
            If `axis` is an integer, it specifies the axis of `x` along which to
            compute the vector norms.  If `axis` is a 2-tuple, it specifies the
            axes that hold 2-D matrices, and the matrix norms of these matrices
            are computed.  If `axis` is None then either a vector norm (when `x`
            is 1-D) or a matrix norm (when `x` is 2-D) is returned. The default
            is None.

            .. versionadded:: 1.8.0

        keepdims : bool, optional
            If this is set to True, the axes which are normed over are left in the
            result as dimensions with size one.  With this option the result will
            broadcast correctly against the original `x`.

            .. versionadded:: 1.10.0

        Returns
        -------
        n : float or ndarray
            Norm of the matrix or vector(s).

        See Also
        --------
        scipy.linalg.norm : Similar function in SciPy.

        Notes
        -----
        For values of ``ord < 1``, the result is, strictly speaking, not a
        mathematical 'norm', but it may still be useful for various numerical
        purposes.

        The following norms can be calculated:

        =====  ============================  ==========================
        ord    norm for matrices             norm for vectors
        =====  ============================  ==========================
        None   Frobenius norm                2-norm
        'fro'  Frobenius norm                --
        'nuc'  nuclear norm                  --
        inf    max(sum(abs(x), axis=1))      max(abs(x))
        -inf   min(sum(abs(x), axis=1))      min(abs(x))
        0      --                            sum(x != 0)
        1      max(sum(abs(x), axis=0))      as below
        -1     min(sum(abs(x), axis=0))      as below
        2      2-norm (largest sing. value)  as below
        -2     smallest singular value       as below
        other  --                            sum(abs(x)**ord)**(1./ord)
        =====  ============================  ==========================

        The Frobenius norm is given by [1]_:

            :math:`||A||_F = [\sum_{i,j} abs(a_{i,j})^2]^{1/2}`

        The nuclear norm is the sum of the singular values.

        Both the Frobenius and nuclear norm orders are only defined for
        matrices and raise a ValueError when ``x.ndim != 2``.

        References
        ----------
        .. [1] G. H. Golub and C. F. Van Loan, *Matrix Computations*,
               Baltimore, MD, Johns Hopkins University Press, 1985, pg. 15

        Examples
        --------
        >>> from numpy import linalg as LA
        >>> a = np.arange(9) - 4
        >>> a
        array([-4, -3, -2, ...,  2,  3,  4])
        >>> b = a.reshape((3, 3))
        >>> b
        array([[-4, -3, -2],
               [-1,  0,  1],
               [ 2,  3,  4]])

        >>> LA.norm(a)
        7.745966692414834
        >>> LA.norm(b)
        7.745966692414834
        >>> LA.norm(b, 'fro')
        7.745966692414834
        >>> LA.norm(a, np.inf)
        4.0
        >>> LA.norm(b, np.inf)
        9.0
        >>> LA.norm(a, -np.inf)
        0.0
        >>> LA.norm(b, -np.inf)
        2.0

        >>> LA.norm(a, 1)
        20.0
        >>> LA.norm(b, 1)
        7.0
        >>> LA.norm(a, -1)
        -4.6566128774142013e-010
        >>> LA.norm(b, -1)
        6.0
        >>> LA.norm(a, 2)
        7.745966692414834
        >>> LA.norm(b, 2)
        7.3484692283495345

        >>> LA.norm(a, -2)
        0.0
        >>> LA.norm(b, -2)
        1.8570331885190563e-016 # may vary
        >>> LA.norm(a, 3)
        5.8480354764257312 # may vary
        >>> LA.norm(a, -3)
        0.0

        Using the `axis` argument to compute vector norms:

        >>> c = np.array([[ 1, 2, 3],
        ...               [-1, 1, 4]])
        >>> LA.norm(c, axis=0)
        array([ 1.41421356,  2.23606798,  5.        ])
        >>> LA.norm(c, axis=1)
        array([ 3.74165739,  4.24264069])
        >>> LA.norm(c, ord=1, axis=1)
        array([ 6.,  6.])

        Using the `axis` argument to compute matrix norms:

        >>> m = np.arange(8).reshape(2,2,2)
        >>> LA.norm(m, axis=(1,2))
        array([  3.74165739,  11.22497216])
        >>> LA.norm(m[0, :, :]), LA.norm(m[1, :, :])
        (3.7416573867739413, 11.224972160321824)

    0.2497209782456826
    [hw3_l2_norm1] Good job!

```python

```

* **Q4 (5 points):** At $t = 0.01 s$, what's the $L_2$-norm of the difference between the density obtained with the Lax-Friedrichs scheme and the analytical solution?

Store your result in the variable `l2_norm2`; you can check your answer by calling the function `mooc.check('hw3_l2_norm2', l2_norm2)`.

```python
# YOUR CODE HERE
Diff_2 = rho_Lax - rho_analytical
l2_norm2 = numpy.linalg.norm(Diff_2, ord=2, axis=0)
print(l2_norm2)
mooc.check('hw3_l2_norm2', l2_norm2)
```

    0.4610293528265613
    [hw3_l2_norm2] Good job!

```python

```

* **Q5 (5 points):** At $t = 0.01 s$, what's the value of the density, obtained with Richtmyer scheme, at location $x = 2.5 m$ (in $kg/m^3$)?

Store your result in the variable `rho1`; you can check your answer by calling the function `mooc.check('hw3_rho1', rho1)`.

**WARNING**: the variable name `rho1` is spelled with the number `1`, **not** the letter `l`.

```python
# YOUR CODE HERE
rho1 = rho_Richtmyer[int((2.5+10)/dx)]
print(rho1)
mooc.check('hw3_rho1', rho1)
```

    0.3746914026476011
    [hw3_rho1] Good job!

```python

```

* **Q6 (5 points):** At $t = 0.01 s$, what's the value of the velocity, obtained with Lax-Friedrichs scheme, at location $x = 2.5 m$ (in $m/s$)?

Store your result in the variable `v2`; you can check your answer by calling the function `mooc.check('hw3_v2', v2)`.

```python
# YOUR CODE HERE
v2 = v_Lax[int((2.5+10)/dx)]
print(v2)
mooc.check('hw3_v2', v2)
```

    281.8563023522752
    [hw3_v2] Good job!

```python

```

* **Q7 (5 points):** At $t = 0.01 s$, what's the absolute difference in the pressure, between the analytical solution and the Richtmyer solution, at location $x = 2.5 m$ (in $N/m^2$)?

Store your result in the variable `p_diff`; you can check your answer by calling the function `mooc.check('hw3_p_diff', p_diff)`.

```python
# YOUR CODE HERE
p_R = p_Richtmyer[int((2.5+10)/dx)]
p_A = p_analytical[int((2.5+10)/dx)]
p_diff = abs(p_R - p_A)
print(p_diff)
mooc.check('hw3_p_diff', p_diff)
```

    64.17847424907086
    [hw3_p_diff] Good job!

```python

```

* **Q8 (5 points):** At $t = 0.01 s$, what's the value of the entropy, obtained with Richtmyer scheme, at location $x = -1.5 m$ (in $J/kg/K$)?

The entropy $s$ is defined as:

$$
s = \frac{p}{\rho^\gamma}
$$

Store your result in the variable `s1`; you can check your answer by calling the function `mooc.check('hw3_s1', s1)`.

**WARNING**: the variable name `s1` is spelled with the number `1`, **not** the letter `l`.

```python
# YOUR CODE HERE
rho_Rs = rho_Richtmyer[int((10-1.5)/dx)]
p_Rs = p_Richtmyer[int((10-1.5)/dx)]
s1 = p_Rs / rho_Rs**gamma
print(s1)
mooc.check('hw3_s1', s1)
```

    100697.043028669
    [hw3_s1] Good job!

```python

```

* **Q9 (5 points):** At $t = 0.01 s$, what's the value of the speed of sound, obtained with Lax-Friedrichs scheme, at location $x = -1.5 m$ (in $m/s$)?

The speed of sound $a$ is defined as:

$$
a = \sqrt{\frac{\gamma p}{\rho}}
$$

Store your result in the variable `a2`; you can check your answer by calling the function `mooc.check('hw3_a2', a2)`.

```python
# YOUR CODE HERE
rho_La = rho_Lax[int((10-1.5)/dx)]
p_La = p_Lax[int((10-1.5)/dx)]
a2 = (gamma * p_La / rho_La)**0.5
print(a2)
mooc.check('hw3_a2', a2)
```

    349.455377505974
    [hw3_a2] Good job!

```python

```

* **Q10 (5 points):** At $t = 0.01 s$, what's the value of the Mach number, obtained with Richtmyer scheme, at location $x = -1.5 m$?

**Hint:** the Mach number is the ratio between the velocity and the speed of sound.

Store your result in the variable `M1`; you can check your answer by calling the function `mooc.check('hw3_M1', M1)`.

**WARNING**: the variable name `M1` is spelled with the number `1`, **not** the letter `l`.

```python
# YOUR CODE HERE
# Mach number = velocity / speed of sound
rho_Ra = rho_Richtmyer[int((10-1.5)/dx)]
p_Ra = p_Richtmyer[int((10-1.5)/dx)]
aR = (gamma * p_Ra / rho_Ra)**0.5
v_Ra = v_Richtmyer[int((10-1.5)/dx)]
M1 = v_Ra/aR
print(M1)
mooc.check('hw3_M1', M1)
```

    0.5483352954050432
    [hw3_M1] Good job!

```python

```

## Reference

---

* Sod, Gary A. (1978), "A survey of several finite difference methods for systems of nonlinear hyperbolic conservation laws," *J. Comput. Phys.*, Vol. 27, pp. 1–31 DOI: [10.1016/0021-9991(78)90023-2](http://dx.doi.org/10.1016%2F0021-9991%2878%2990023-2) // [PDF from unicamp.br](http://www.fem.unicamp.br/~phoenics/EM974/TG%20PHOENICS/BRUNO%20GALETTI%20TG%202013/a%20survey%20of%20several%20finite%20difference%20methods%20for%20systems%20of%20nonlinear%20hyperbolic%20conservation%20laws%20Sod%201978.pdf), checked Oct. 28, 2014.