Méthodes Monte Carlo avec Cloud Dataproc et Apache Spark

Cloud Dataproc et Apache Spark vous offrent l'infrastructure et la capacité dont vous avez besoin pour exécuter des simulations Monte-Carlo rédigées en langage Java, Python ou Scala.

Les méthodes de Monte-Carlo peuvent permettre de répondre à un large éventail de questions dans les domaines des affaires, de l'ingénierie, des sciences, des mathématiques, etc. En procédant par échantillonnage aléatoire répété afin de créer une distribution de probabilité pour une variable, une simulation Monte-Carlo permet de répondre à des questions qui seraient insolubles par tout autre moyen. Ainsi, dans le domaine financier, la fixation du prix d'une option sur titres nécessite d'analyser les milliers de façons dont le cours de ce titre pourrait varier sur une certaine période. Les méthodes de Monte-Carlo offrent un moyen de simuler ces variations de cours des actions sur différents résultats possibles, tout en gardant le contrôle sur le domaine des intrants possibles du problème.

Auparavant, l'exécution de milliers de simulations pouvait prendre un temps considérable et engendrer des coûts élevés. Cloud Dataproc vous offre la possibilité de provisionner la capacité à la demande en payant à la minute. Apache Spark vous permet d'utiliser des clusters de plusieurs dizaines, centaines ou milliers de serveurs pour exécuter des simulations de manière intuitive et évolutive en fonction de vos besoins. Résultat : vous pouvez exécuter plus de simulations en moins de temps, ce qui peut aider votre entreprise à innover plus rapidement et à mieux gérer les risques.

La sécurité est toujours importante lorsque vous travaillez sur des données financières. Cloud Dataproc s'exécute sur Google Cloud Platform (GCP), qui vous offre plusieurs moyens de gérer vos données de manière sécurisée et privée. Par exemple, toutes les données sont chiffrées en transit et au repos. En outre, GCP est conforme aux normes ISO 27001, SOC3 et PCI.

Objectifs

  • Créer un cluster Cloud Dataproc géré avec Apache Spark préinstallé
  • Exécuter une simulation Monte-Carlo à l'aide de Python pour estimer la croissance d'un portefeuille d'actions au fil du temps
  • Exécuter une simulation Monte-Carlo à l'aide de Scala pour simuler les gains d'un casino

Coûts

Ce tutoriel utilise les composants facturables de Google Cloud Platform répertoriés ici :

Vous pouvez vous servir du simulateur de coût pour générer une estimation des coûts en fonction de votre utilisation prévue. Les nouveaux utilisateurs de GCP peuvent bénéficier d'un essai gratuit.

Une fois que vous avez terminé ce tutoriel, vous pouvez éviter de continuer à payer des frais en supprimant les ressources que vous avez créées. Consultez la page Effectuer un nettoyage pour en savoir plus.

Avant de commencer

Créer un cluster Cloud Dataproc

Suivez les étapes permettant de créer un cluster Cloud Dataproc à partir de la console Google Cloud Platform. Les paramètres de cluster par défaut prévoient deux nœuds de calcul, ce qui suffit pour ce tutoriel.

Désactiver la journalisation des avertissements

Par défaut, Apache Spark imprime une journalisation détaillée dans la fenêtre de la console. Pour les besoins de ce tutoriel, modifiez le niveau de journalisation pour ne consigner que les erreurs. Procédez comme suit :

Utiliser ssh pour se connecter au nœud principal du cluster Cloud Dataproc

  1. Dans la console GCP, accédez à la page Instances de VM.

    Accéder à la page Instances de VM

  2. Dans la liste des instances de machine virtuelle, cliquez sur SSH sur la ligne de l'instance à laquelle vous souhaitez vous connecter.

    bouton SSH en regard du nom de l'instance.

Une fenêtre de navigateur s'ouvre dans votre répertoire de base sur le nœud principal.

Connected, host fingerprint: ssh-rsa 2048 ...
...
user@clusterName-m:~$

Redéfinir le paramètre de journalisation

  1. Dans le répertoire d'accueil du nœud principal, modifiez le fichier /etc/spark/conf/log4j.properties.

    sudo nano /etc/spark/conf/log4j.properties
    
  2. Définissez log4j.rootCategory sur ERROR.

    # Set only errors to be logged to the console
    log4j.rootCategory=ERROR, console
    log4j.appender.console=org.apache.log4j.ConsoleAppender
    log4j.appender.console.target=System.err
    log4j.appender.console.layout=org.apache.log4j.PatternLayout
    log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
    
  3. Enregistrez les modifications, puis quittez l'éditeur. Si vous souhaitez réactiver la journalisation détaillée, annulez la modification en rétablissant la valeur d'origine (INFO) de .rootCategory.

Langages de programmation Spark

Spark accepte les langages de programmation Python, Scala et Java pour les applications autonomes, et fournit des interpréteurs interactifs pour Python et Scala. Le choix de tel ou tel langage est une question de préférence personnelle. Dans ce tutoriel, nous utilisons les interpréteurs interactifs, car ils vous permettent de faire des tests en modifiant le code et en essayant différentes valeurs de saisie, puis en affichant les résultats.

Estimer la croissance d'un portefeuille

Dans le domaine de la finance, les méthodes de Monte-Carlo servent parfois à effectuer des simulations visant à prédire la performance d'un investissement. En produisant des échantillons aléatoires de résultats sur une plage de conditions de marché probables, une simulation Monte-Carlo peut répondre aux questions concernant la performance d'un portefeuille pour une situation moyenne ou dans le pire cas de figure.

Suivez ces étapes pour créer une simulation basée sur les méthodes de Monte-Carlo pour essayer d'estimer la rentabilité d'un investissement financier en fonction de quelques facteurs de marché courants.

  1. Démarrez l'interpréteur Python à partir du nœud principal Cloud Dataproc.

    pyspark
    

    Attendez l'invite Spark (>>>).

  2. Saisissez le code ci-dessous. Veillez à conserver les retraits dans la définition de la fonction.

    import random
    import time
    from operator import add
    
    def grow(seed):
        random.seed(seed)
        portfolio_value = INVESTMENT_INIT
        for i in range(TERM):
            growth = random.normalvariate(MKT_AVG_RETURN, MKT_STD_DEV)
            portfolio_value += portfolio_value * growth + INVESTMENT_ANN
        return portfolio_value
    
  3. Appuyez sur return jusqu'à ce que l'invite Spark réapparaisse.

    Le code qui précède définit une fonction qui modélise ce qui pourrait se produire pour un investisseur disposant d'un compte d'épargne-retraite investi en actions, sur lequel il effectue tous les ans un versement complémentaire. La fonction génère chaque année un retour sur investissement aléatoire exprimé en pourcentage pour une durée de terme spécifiée. La fonction reçoit pour paramètre une valeur initiale. Cette valeur permet de réinitialiser le générateur de nombres aléatoires, ce qui garantit que la fonction ne reçoit pas la même liste de nombres aléatoires à chaque exécution. La fonction random.normalvariate garantit que des valeurs aléatoires apparaissent dans une distribution normale pour l'écart moyen et l'écart-type spécifiés. La fonction augmente la valeur du portefeuille du montant de la croissance, qui peut être positif ou négatif, et ajoute une somme annuelle représentant un investissement supplémentaire.

    Vous définissez les constantes requises lors d'une prochaine étape.

  4. Créez de nombreuses valeurs initiales pour alimenter la fonction. À l'invite Spark, saisissez le code suivant, qui génère 10 000 valeurs initiales :

    seeds = sc.parallelize([time.time() + i for i in xrange(10000)])
    

    Le résultat de l'opération parallelize est un ensemble de données distribué résilient (RDD, Resilient Distributed Dataset), c'est-à-dire un ensemble d'éléments optimisés pour le traitement en parallèle. Dans notre exemple, ce RDD contient des valeurs initiales basées sur l'heure système actuelle.

    Lors de la création du RDD, Spark divise les données en fonction du nombre de nœuds de calcul et de cœurs disponibles. Ici, Spark choisit d'utiliser huit tranches, soit une tranche par cœur. Cette valeur convient pour la présente simulation, qui porte sur 10 000 éléments de données. Dans des simulations plus importantes, chaque tranche pourrait dépasser la limite par défaut. Dans ce cas, spécifier un second paramètre sur la valeur parallelize permet d'augmenter le nombre de tranches, ce qui peut aider à maintenir une taille gérable pour chaque tranche, sans empêcher Spark d'exploiter les huit cœurs.

  5. Transmettez les valeurs initiales contenues dans le RDD à la fonction grow.

    results = seeds.map(grow)
    

    La méthode map transmet chaque valeur initiale du RDD à la fonction grow et ajoute chaque résultat à un nouveau RDD, qui est stocké dans les résultats (results). Notez que cette opération, qui effectue une transformation, ne produit pas immédiatement ses résultats. Spark n'effectue ce travail qu'au moment où les résultats sont requis. Cette évaluation minimaliste explique pourquoi vous pouvez saisir du code sans avoir préalablement défini les constantes.

  6. Spécifiez certaines valeurs pour la fonction.

    INVESTMENT_INIT = 100000  # starting amount
    INVESTMENT_ANN = 10000  # yearly new investment
    TERM = 30  # number of years
    MKT_AVG_RETURN = 0.11 # percentage
    MKT_STD_DEV = 0.18  # standard deviation
    
  7. Appelez reduce pour agréger les valeurs du RDD. Saisissez le code suivant pour additionner les résultats stockés dans le RDD :

    sum = results.reduce(add)
    
  8. Estimez et affichez le rendement moyen :

    print sum / 10000.
    

    Veillez à saisir le point (.) final, qui signifie que vous utilisez l'arithmétique en virgule flottante.

  9. Vous allez maintenant modifier une hypothèse et découvrir comment cela influe sur les résultats. Par exemple, vous pouvez saisir une nouvelle valeur pour le rendement moyen du marché :

    MKT_AVG_RETURN = 0.07
    
  10. Lancez à nouveau la simulation.

    print sc.parallelize([time.time() + i for i in xrange(10000)]) \
            .map(grow).reduce(add)/10000.
    
  11. Lorsque vous avez terminé vos tests, appuyez sur CTRL+D pour quitter l'interpréteur Python.

Programmation d'une simulation Monte-Carlo en langage Scala

Comme chacun sait, Monte-Carlo est la destination de référence pour les jeux de hasard. Dans cette section, vous allez utiliser Scala afin de créer une simulation qui modélise l'avantage mathématique dont bénéficie un casino dans un jeu de hasard. La "marge maison" d'un véritable casino varie sensiblement d'un jeu à un autre. Par exemple, elle peut dépasser 20 % au Keno. Dans ce tutoriel, nous allons créer un jeu simple dans lequel la maison n'a qu'un avantage de 1 %. Voici comment ce jeu se déroule :

  • Le joueur place une mise consistant en un certain nombre de jetons provenant de sa cagnotte.
  • Le joueur lance un dé à 100 faces (amusant, non ?).
  • Si le résultat du lancer est un nombre compris entre 1 et 49, le joueur gagne.
  • En cas de résultat compris entre 50 et 100, le joueur perd sa mise.

À l'évidence, ce jeu crée un désavantage de 1 % pour le joueur : 51 des 100 résultats possibles pour chaque lancer lui font perdre sa mise.

Suivez ces étapes pour créer et exécuter le jeu :

  1. Démarrez l'interpréteur Scala à partir du nœud principal Cloud Dataproc.

    spark-shell
    
  2. Copiez et collez le code ci-après pour créer le jeu. Scala n'a pas les mêmes exigences que Python en matière de retrait : vous pouvez donc vous contenter de copier le code et de le coller lorsque l'invite scala> s'affiche.

    val STARTING_FUND = 10
    val STAKE = 1   // the amount of the bet
    val NUMBER_OF_GAMES = 25
    
    def rollDie: Int = {
        val r = scala.util.Random
        r.nextInt(99) + 1
    }
    
    def playGame(stake: Int): (Int) = {
        val faceValue = rollDie
        if (faceValue < 50)
            (2*stake)
        else
            (0)
    }
    
    // Function to play the game multiple times
    // Returns the final fund amount
    def playSession(
       startingFund: Int = STARTING_FUND,
       stake: Int = STAKE,
       numberOfGames: Int = NUMBER_OF_GAMES):
       (Int) = {
    
        // Initialize values
        var (currentFund, currentStake, currentGame) = (startingFund, 0, 1)
    
        // Keep playing until number of games is reached or funds run out
        while (currentGame <= numberOfGames && currentFund > 0) {
    
            // Set the current bet and deduct it from the fund
            currentStake = math.min(stake, currentFund)
            currentFund -= currentStake
    
            // Play the game
            val (winnings) = playGame(currentStake)
    
            // Add any winnings
            currentFund += winnings
    
            // Increment the loop counter
            currentGame += 1
        }
        (currentFund)
    }
    
  3. Appuyez sur return jusqu'à ce que l'invite scala> réapparaisse.

  4. Saisissez le code suivant pour jouer 25 fois au jeu. Il s'agit de la valeur par défaut du paramètre NUMBER_OF_GAMES.

    playSession()
    

    Votre cagnotte contenait 10 unités au départ. Et maintenant, a-t-elle augmenté ou diminué ?

  5. Simulez à présent 10 000 joueurs misant 100 jetons par partie. Faites 10 000 parties en une session. Cette simulation Monte-Carlo calcule la probabilité de perdre tout votre argent avant la fin de la session. Saisissez le code suivant :

    (sc.parallelize(1 to 10000, 500)
      .map(i => playSession(100000, 100, 250000))
      .map(i => if (i == 0) 1 else 0)
      .reduce(_+_)/10000.0)
    

    Notez la syntaxe .reduce(_+_) : il s'agit d'une notation abrégée en Scala pour désigner une agrégation à l'aide d'une fonction de sommation. Elle est fonctionnellement équivalente à la notation .reduce(add) que vous avez pu voir dans l'exemple Python.

    Le code ci-dessus effectue les étapes suivantes :

    • Créer un RDD contenant les résultats des parties jouées au cours de la session
    • Remplacer les résultats des joueurs ruinés par le nombre 1 et les résultats non nuls par le nombre 0
    • Calculer le nombre total de joueurs ruinés
    • Diviser ce nombre par le nombre de joueurs

    Voici le type de résultat que vous pouvez obtenir :

    0.998
    

    Ce résultat est la quasi-garantie de perdre tout votre argent, même si le casino n'avait qu'un avantage de 1 %.

Nettoyer

Pour éviter que les ressources utilisées dans ce tutoriel soient facturées sur votre compte Google Cloud Platform :

Supprimer le projet

  1. Dans la console GCP, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer .
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Étapes suivantes

  • Pour savoir comment soumettre des tâches Spark à Cloud Dataproc sans avoir à vous connecter en ssh au cluster, consultez la section Soumettre une tâche de la documentation Cloud Dataproc.

  • Testez d'autres fonctionnalités de Google Cloud Platform. Découvrez nos tutoriels.