Inhalt des Skriptums seit dem Sommersemester 2025
- Startseite
- Allgemeine Informationen zur Lehrveranstaltung
- Einfaches Python Setup und Wichtige Informationen
- 01 Einführung in algorithmisches Denken & LLMs
- 02 Algorithmenanalyse und Komplexitätstheorie
- 03 Vektoren, Matrizen und Vektorisierung in Python
- 04 Differenzieren und Integrieren in Python: Analytische und Numerische Methoden
- 05 Arbeiten mit Daten: Vorbereitung, Visualisierung, Analyse
- 06 Graphalgorithmen: Darstellungen, Traversierungen, Kürzeste Wege und Spannbäume
- 07 Gradient Descent, Stochastische Optimierung, Simulated Annealing, Genetische Algorithmen
- 08 Schwärme und Emergenz
- 09 Monte-Carlo-Methoden
- 10 Machine-Learning Grundlagen
Hier finden Sie die Kapitel aus den Vergangenen Jahren (legacy):
- 0. Python Einführung und Checkup
- 1. Einführung und einfache Algorithmen
- 2. Differenzieren und Integrieren
- 3. Vektoren, Matrizen und Vektorisierung in Python
- 4. Datenanalyse bzw. Datenauswertung
- 5. Grundlagen der Optimierung und Gradient Descent
- 6. Stochastische Optimierung und Genetische Algorithmen
- 7. Monte-Carlo-Methoden – Simulation und Integration
- 8. Monte-Carlo-Methoden, Teil 2 – Monte-Carlo-Integration, Teil 2 und Random Walk
- 9. Unsupervised Machine Learning: Clustering von Daten
- 10. Supervised Machine Learning: Grundlagen
- 11. Einführung in künstliche neuronale Netzwerke
Die Jupyter-Notebooks zur Legacy-Variante der Lehrveranstaltung finden Sie im zugehörigen GitHub-Repository.
9: Monte-Carlo Methoden¶
Monte-Carlo-Methoden gehören zu den leistungsstärksten und vielseitigsten Werkzeugen in der angewandten Mathematik und Informatik. Der Name stammt von dem berühmten Casino in Monaco und deutet auf den Kern dieser Methoden hin: die Verwendung von Zufallszahlen und Wahrscheinlichkeiten zur Lösung bzw. Approximation komplexer Probleme.
Nach dem Studium dieser Einheit sollten Sie zumindest folgende drei der wichtigsten Punkte hier verstehen:¶
- Sampling und wie es zu einer Monte-Carlo-Simulation beiträgt bzw. deren Basis bereitstellt.
- Die typischen Eigenschaften einer MC-Simulation und wie man damit Werte für Kenngrößen eines untersuchten Systems abschätzen kann.
- Monte-Carlo-Integration als konkrete Sampling-Anwendung.
9.1 Grundlagen einer Monte-Carlo (MC) Simulation¶
Monte-Carlo-Simulationen basieren auf einem einfachen, aber mächtigen Prinzip: Wir nutzen eine große Anzahl von Zufallsexperimenten, um Näherungslösungen für Probleme zu finden, die deterministisch schwer zu lösen sind. Die Grundidee dabei ist, dass man durch Mitschreiben der Ergebnisse für bestimmte Größen im System über viele viele “Runs” einer Simulation eine Statistik dieser Ergebnisse bekommt. Für diese beobachtete Statistik des Systems kann man dann idealerweise einen Mittelwert samt Fehlerabschätzung für die Gesuchte Variable berechnen. Während diese Idee intuitiv einleuchtend erscheint, ist es wichtig zu verstehen, wie und warum dieser Ansatz funktioniert, und wo seine Grenzen liegen.
9.1.1 Sampling, Sample-Größe und Anzahl der Samples¶
Der grundlegende Begriff für das Verstehen einer MC-Simulation ist Sampling. Sampling (oder Stichprobenentnahme) bezeichnet den Prozess, zufällige Werte aus einer bestimmten Verteilung zu ziehen. Wir sind Zufallszahlen aus einer Verteilung bereits im Rahmen der Stochastischen Optimierung begegnet. Für eine MC-Simulation erzeugen wir nun eine Anzahl solcher Samples um damit dann die Berechnungen der Simulation durchzuführen.
Die wichtigsten Begriffe dabei sind:
- Sample: Ein Satz vorgegebener Größe von Zufallszahlen aus einer Verteilung
- Sample-Größe: Die Anzahl der Elemente in einem Sample
- Anzahl der Samples: Wie oft wir den Prozess wiederholen, d.h. wie viele solcher Gruppen von Zufallszahlen wir nacheinander verwenden.
Ein einfaches Beispiel: Wenn wir 1000-mal einen fairen Würfel werfen, dann haben wir 1000 Samples der Größe 1. Wenn wir dagegen 100-mal jeweils 10 Würfel gleichzeitig werfen und die Menge der Augenzahlen eines Wurfes gemeinsam betrachten, dann haben wir 100 Samples der Größe 10.
Die Sample-Größe beeinflusst direkt die Genauigkeit unserer Schätzungen. Je größer unsere Stichprobe, desto genauer wird in der Regel unsere Approximation. Dies folgt aus dem Gesetz der großen Zahlen, einem fundamentalen Prinzip der Wahrscheinlichkeitstheorie: Wenn wir ein zufälliges Experiment oft genug wiederholen, nähert sich der Durchschnitt der Ergebnisse dem Erwartungswert an. Das leuchtet ein, ist aber nicht alles (dazu gleich noch mehr). Vorerst kann man dazu aber bereits sagen:
- Je präziser das Ergebnis sein soll, desto größere Samples werden insgesamt benötigt.
- Die verfügbare Rechenleistung berschränkt die praktikable Menge von Samples und Runs insgesamt.
- Die Dimensionalität des Problems spielt eine grundlegende Rolle beim Ziehen von Stichproben, wobei es in höheren Dimensionen unwahrscheinlicher ist, etwas Bestimmtes durch Sampling zu “sehen” oder zu “finden” (auch bekannt als Fluch der Dimensionalität).
Ein Hauptresultat gleich vorab: Die Genauigkeit einer Monte-Carlo-Schätzung verbessert sich typischerweise mit $\sqrt{N}$, wobei $N$ die Anzahl der Gesamtzahl der Runs ist. Das bedeutet, um die Genauigkeit zu verdoppeln, benötigen wir viermal so viele Runs, um den Fehler um eine Kommastelle zu drücken, bereits 100 mal so viele!
9.1.2 Sample-Means, Means of Sample-Means und Fehlerabschätzung¶
Was man bei Monte-Carlo-Simulationen eigentlich immer berechnet, sind arithmetische Mittelwerte für Größen, die in der Simulation interessant sind. Gemittelt wird dabei über die Runs innerhalb eines Samples, das Resultat nennt man daher einen Sample-Mean, also den Mittelwert im Sample. Davon kann es mehrere geben, für jede interessante Variable einen.
Diese Sample-Means sind allerdings selbst Zufallsvariablen, die um den jeweils tatsächlichen Wert $\mu$ in den Daten schwanken. Aus diesem Grund ist es hilfreich, mehrere gleich große Samples innerhalb einer MC-Simulation zu verwenden. Der Grund dafür ist, dass man zwar auf naive Art und Weise einen Mittelwert über ein Sample berechnen kann, aber eigentlich nicht von vornherein eine Standardabweichung zu so einem Sample-Mean, denn man weiß im Allgemeinen nichts über die zugrundeliegende Verteilung der Größe, die man untersucht.
Konkret ist das übliche Verständnis einer mit $\sigma$ angegebenen Standardabweichung das einer zugrundeliegenden Gauß-Verteilung, was aber im Allgemeinen nicht der Fall ist. Daher benützt man den zentralen Grenzwertsatz (auf Englisch “Central-Limit Theorem”) und mittelt die Mittelwerte einer Größe aus mehreren Samples nochmals zu einem Mean of Sample-Means. Der Zentrale Grenzwertsatz besagt dabei, dass für mehrere Zufallsvariablen, die der gleichen Verteilung folgen, die Verteilung der Sample-Means normalverteilt ist. Das bedeutet:
- Nimmt man nur ein Sample und berechnet damit einen Mittelwert, dann hat man zwar einen Anhaltspunkt für den Mittelwert der tatsächlichen zugrundeliegenden Verteilung, aber nicht mehr.
- Nimmt man die Sample-Means und berechnet deren Mean und Standardabweichung (das darf man dann, denn die Verteilung ist ja eine Normalverteilung), dann hat man sowohl eine bessere Näherung für den tatsächlichen Mittelwert, als auch gleichzeitig eine Abschätzung für den statistischen Fehler in der Bestimmung dieses Mittelwerts.
- Wenn man nun die Anzahl der Elemente eines Samples erhöht (also die Samplegröße), dann bekommt man grundsätzlich genauere Werte für den Mittelwert, d.h. auch kleinere Fehler.
- Wenn man allerdings auch mehrere Samples verwendet, dann bekommt man eine insgesamt bessere Einschätzung des Mittelwerts und dessen Fehler. In der Praxis empfiehlt es sich, die Anzahl der Samples groß genug zu wählen, damit die Gaußverteilung der Sample-Means gut genug abgebildet werden kann. Als guter Anfangs-Wert eignet sich hier etwa 20.
Diese Methode bietet dabei zwei wesentliche Vorteile:
- Sie ermöglicht also eine Abschätzung des Fehlers aus den Daten selbst
- Sie erlaubt eine effiziente Parallelisierung der Berechnung durch die mögliche Verteilung von Samples auf CPUs etc.
Die Konvergenzrate von Monte-Carlo-Simulationen ist unabhängig von der Dimensionalität des Problems und beträgt immer $O(1/\sqrt{N})$. Dies macht sie besonders wertvoll für hochdimensionale Probleme, bei denen andere numerische Methoden oft und sehr schnell an ihre Grenzen stoßen. Allerdings bedeutet diese relativ langsame Konvergenzrate auch, dass für sehr präzise Ergebnisse eine große Anzahl von Runs erforderlich sein kann. Dazu kommt außerdem, dass der Fehler anfähnglich, also für z.B. eine gesamte Anzahl von 1 000 000 Runs, sehr groß im Vergleich mit konventionellen Methoden sein kann. In der Praxis werden daher oft Techniken zur Varianzreduktion eingesetzt, um die Effizienz zu verbessern, wie zum Beispiel Importance Sampling oder Markov-Chain Monte Carlo (MCMC), die hier aber leider den Rahmen sprengen.
9.2 Random Walk¶
Nach dieser recht ausführlichen etwas trockenen Einleitung geht es jetzt endlich mit den ersten Simulationen los. Das Beispiel, das wir uns hier ansehen, nennt man Random Walk. Der Random Walk ist eines der fundamentalsten Konzepte in der Wahrscheinlichkeitstheorie und findet Anwendungen in zahlreichen wissenschaftlichen Disziplinen – von der Physik und Chemie bis hin zur Finanzwirtschaft und Informatik. Ein Random Walk ist im Wesentlichen eine mathematische Formalisierung einer Pfadentwicklung, bei der jeder Schritt nach bestimmten Regeln zufällig bestimmt wird.
9.2.1 Definition und Setup¶
In seiner einfachsten Form ist ein Random Walk eine Folge von Zufallsschritten in einem mathematischen Raum. Beginnen wir mit dem eindimensionalen Fall:
Ein eindimensionaler Random Walk auf den ganzen Zahlen der Zahlengeraden kann wie folgt definiert werden:
- Wir starten an einem Punkt, üblicherweise bei $x = 0$
- In jedem Zeitschritt bewegen wir uns entweder um eine Einheit nach rechts oder eine Einheit nach links
- Die Richtung jedes Schritts wird zufällig und unabhängig von vorherigen Schritten bestimmt
Mathematisch können wir dies als Summe von Zufallsvariablen ausdrücken:
$$X_n = \sum_{i=1}^{n} S_i$$
wobei:
- $X_n$ die Position nach $n$ Schritten ist
- $S_i$ die Zufallsvariable für den $i$-ten Schritt ist, mit $P(S_i = 1) = p$ und $P(S_i = -1) = 1-p$
Im Fall eines symmetrischen Random Walks haben wir $p = 0.5$, das heißt, Bewegungen nach rechts und links sind gleich wahrscheinlich. Die kumulative Summe der zufälligen Schritte gibt uns die Position zu jedem Zeitpunkt.
9.2.2 Varianten: Dimensionen, Bias und Diskretisierung¶
Random Walks lassen sich auf verschiedene Weise verallgemeinern und anpassen, um unterschiedliche Phänomene zu modellieren. Ein Random Walk muss natürlich nicht auf eine Dimension beschränkt sein. In zwei Dimensionen bewegt sich unser “Wanderer” auf einem Gitter und kann in jedem Schritt in eine von vier Richtungen gehen: oben, unten, links oder rechts. Analog können wir Random Walks in drei oder mehr Dimensionen definieren.
Hier als Beispiel ein Satz von 1000 Random Walks in zwei Dimensionen mit diskreter endlicher Schrittweite und eingeschränkt auf die Richtungen der Koordinatenachsen. Die Code Snippets für diese Einheit stammen wieder von Gemini 2.5 pro.
import numpy as np
import matplotlib.pyplot as plt
def generate_random_walks_2d_discrete(num_walks, num_steps):
"""
Generates a specified number of 2D discrete random walks.
Each walk starts at (0,0). Steps are of size 1 and can be in
one of four directions: right, left, up, or down.
Args:
num_walks (int): The number of random walks to generate.
num_steps (int): The number of steps each walk should take.
Returns:
list: A list of numpy arrays, where each array represents a
walk and contains (x, y) coordinates for each step.
"""
walks = []
# Define possible steps: right, left, up, down
# Each row is a [dx, dy]
step_directions = np.array([[1, 0], [-1, 0], [0, 1], [0, -1]])
for _ in range(num_walks):
# Initialize walk at (0,0)
# Shape: (num_steps + 1, 2) to include the starting point
path = np.zeros((num_steps + 1, 2))
for i in range(num_steps):
# Randomly choose one of the four directions
step = step_directions[np.random.randint(0, 4)]
# Update position
path[i+1] = path[i] + step
walks.append(path)
return walks
def plot_random_walks(walks, title="2D Discrete Random Walks"):
"""
Plots a collection of 2D random walks on a single Matplotlib plot.
Args:
walks (list): A list of numpy arrays, where each array represents a
walk (output from generate_random_walks_2d_discrete).
title (str): The title for the plot.
"""
if not walks:
print("No walks to plot.")
return
num_walks = len(walks)
# Create a colormap to assign a different color to each walk
colors = plt.cm.viridis(np.linspace(0, 1, num_walks))
plt.figure(figsize=(10, 10)) # Adjust figure size for better visibility
for i, walk_path in enumerate(walks):
# Plot x and y coordinates of the walk
plt.plot(walk_path[:, 0], walk_path[:, 1], color=colors[i], alpha=0.7, linewidth=0.8)
plt.title(title)
plt.xlabel("X position")
plt.ylabel("Y position")
plt.grid(True, linestyle='--', alpha=0.6) # Add a grid for readability
plt.axis('equal') # Ensure steps have equal visual length in x and y
plt.show()
# --- Main execution ---
if __name__ == '__main__':
# Parameters for the random walks
number_of_walks = 1000
steps_per_walk = 100 # You can adjust the number of steps per walk
# 1. Generate the random walks
all_walks = generate_random_walks_2d_discrete(number_of_walks, steps_per_walk)
# 2. Visualize the walks
plot_title = f"{number_of_walks} Two-Dimensional Discrete Random Walks ({steps_per_walk} steps each)"
plot_random_walks(all_walks, title=plot_title)

Ein “biased” oder verzerrter Random Walk ist einer, bei dem die Wahrscheinlichkeiten für verschiedene Richtungen nicht gleich sind. So könnte beispielsweise ein Teilchen in einer Flüssigkeit, das einem elektrischen Feld ausgesetzt ist, eine höhere Wahrscheinlichkeit haben, sich in Richtung einer der Elektroden zu bewegen. In unserem eindimensionalen Beispiel würde ein Bias bedeuten, dass $p \neq 0.5$. Wenn $p > 0.5$, besteht eine höhere Wahrscheinlichkeit, sich nach rechts zu bewegen, was zu einer durchschnittlichen Drift in diese Richtung führt.
Eine Andere Erweiterung betrifft die Diskretisierung des Raumes: Bisher haben wir diskrete Schritte in Raum und Zeit betrachtet, aber in vielen Anwendungen ist es sinnvoller, die Zeit und Raum als kontinuierliche Variable zu behandeln. Bei einem Continuous-Time Random Walk (CTRW) sind nicht nur die räumlichen Schritte, sondern auch die Zeitintervalle zwischen den Schritten zufällig.
Man lässt Random Walks also im Allgemeinen in beliebige Richtungen und verschieden weit pro Schritt gehen, und passt die Anzahl der Dimensionen des Raums and das konkrete Problem an. Zusätzlich zu einem generellen Bias in eine Richtung kann man auch noch einbauen, dass ein Random-Walk nicht in genau jene Richtung zurückgehen darf, aus der er gerade gekommen ist. Das ist bei diskreten Random Walks wie in unserem obigen Beispiel interessant, wo de facto bei jedem Schritt eine von vier Möglichkeiten ausgeschlossen wäre.
Sehen wir uns einen allgemeinen Random Walk an:
import numpy as np
import matplotlib.pyplot as plt
def generate_random_walks_2d_continuous(num_walks, num_steps):
"""
Generates a specified number of 2D continuous random walks.
Each walk starts at (0,0). Steps have a random direction (0 to 2*pi)
and a random length (uniformly distributed between 0 and 1).
Args:
num_walks (int): The number of random walks to generate.
num_steps (int): The number of steps each walk should take.
Returns:
list: A list of numpy arrays, where each array represents a
walk and contains (x, y) coordinates for each step.
"""
walks = []
for _ in range(num_walks):
# Initialize walk at (0,0)
# Shape: (num_steps + 1, 2) to include the starting point
path = np.zeros((num_steps + 1, 2))
for i in range(num_steps):
# 1. Generate a random angle (direction) between 0 and 2*pi
angle = np.random.uniform(0, 2 * np.pi)
# 2. Generate a random step size between 0 and 1
step_size = np.random.uniform(0, 1)
# 3. Calculate the change in x and y (dx, dy)
dx = step_size * np.cos(angle)
dy = step_size * np.sin(angle)
# 4. Update position
path[i+1] = path[i] + [dx, dy]
walks.append(path)
return walks
def plot_random_walks(walks, title="2D Random Walks"):
"""
Plots a collection of 2D random walks on a single Matplotlib plot.
Args:
walks (list): A list of numpy arrays, where each array represents a
walk (output from a generate_random_walks function).
title (str): The title for the plot.
"""
if not walks:
print("No walks to plot.")
return
num_walks = len(walks)
# Create a colormap to assign a different color to each walk
# Using plt.cm.get_cmap for newer Matplotlib versions if cm.viridis is an issue
try:
colors = plt.cm.viridis(np.linspace(0, 1, num_walks))
except AttributeError: # Fallback for older versions or specific environments
cmap = plt.get_cmap('magma')
colors = cmap(np.linspace(0, 1, num_walks))
plt.figure(figsize=(10, 10)) # Adjust figure size for better visibility
for i, walk_path in enumerate(walks):
# Plot x and y coordinates of the walk
plt.plot(walk_path[:, 0], walk_path[:, 1], color=colors[i], alpha=0.6, linewidth=0.5)
plt.title(title)
plt.xlabel("X position")
plt.ylabel("Y position")
plt.grid(True, linestyle='--', alpha=0.6) # Add a grid for readability
plt.axis('equal') # Ensure steps have equal visual length in x and y initially,
# though step sizes vary. This maintains aspect ratio.
plt.show()
# --- Main execution ---
if __name__ == '__main__':
# Parameters for the random walks
number_of_walks = 1000
steps_per_walk = 100 # You can adjust the number of steps per walk
# 1. Generate the continuous random walks
all_walks_continuous = generate_random_walks_2d_continuous(number_of_walks, steps_per_walk)
# 2. Visualize the walks
plot_title_continuous = (
f"{number_of_walks} Continuous 2D Random Walks\n"
f"({steps_per_walk} steps each, random direction & step size [0,1])"
)
plot_random_walks(all_walks_continuous, title=plot_title_continuous)

9.2.3 Eigenschaften und interessante Fragen zu Random Walks¶
Soweit, so gut, aber was kann man nun eigentlich mit so etwas anfangen? Um diese Frage zu beantworten, sehen wir uns einfach ein paar konkrete Eigenschaften an, die man bezüglich der Pfade und der Walks untersuchen kann.
Rückkehrwahrscheinlichkeit von Random Walks auf Diskreten Gittern¶
Eine grundlegende Frage ist: Mit welcher Wahrscheinlichkeit kehrt ein Random Walk jemals zum Ausgangspunkt zurück? Das erscheint vielleicht für unser gerade eben gesehenes Beispiel ziemlich sinnlos, weil unwahrscheinlich, aber für das zuerst erwähnte eindimensionale Problem mit Schritten $\pm 1$ ist es, auch unmittelbar einsichtigerweise, eine sehr relevante Frage.
Die Antwort für Random Walks auf diskreten Gittern (so wie oben) hängt im Allgemeinen von der Dimension ab (der Beweis dafür stammt von Pólya):
- In einer Dimension: Die Rückkehrwahrscheinlichkeit beträgt 1 (der Walk kehrt mit Sicherheit zurück)
- In zwei Dimensionen: Die Rückkehrwahrscheinlichkeit beträgt ebenfalls 1
- In drei oder mehr Dimensionen: Die Rückkehrwahrscheinlichkeit ist kleiner als 1 (es besteht eine positive Wahrscheinlichkeit, nie zurückzukehren)
Erwarteter Abstand vom Ausgangspunkt¶
Wie weit entfernt vom Startpunkt erwarten wir den Walk nach $n$ Schritten zu finden? Für einen unbiased Random Walk gilt: Der erwartete Abstand vom Ursprung nach $n$ Schritten ist proportional zu $\sqrt{n}$. Diese $\sqrt{n}$-Skalierung ist ein charakteristisches Merkmal von diffusiven Prozessen und unterscheidet Random Walks von gerichteteren Bewegungen.
First-Passage Time und Hitting Time¶
Weitere interessante statistische Größen bei Random Walks sind:
- First-Passage Time: Die Zeit, die benötigt wird, um einen bestimmten Punkt zum ersten Mal zu erreichen
- Hitting Time: Die Zeit, die benötigt wird, um eine bestimmte Menge von Punkten zu erreichen
Diese Konzepte sind in vielen Anwendungen wichtig, beispielsweise beim Modellieren von chemischen Reaktionen, bei denen Moleküle zusammentreffen müssen, oder in der Finanzwirtschaft, wenn ein Aktienkurs einen bestimmten Wert erreicht.
Self-Avoiding Random Walks¶
Ein Self-Avoiding Random Walk ist ein Pfad, der sich nicht selbst kreuzt. Dies ist ein komplexeres Modell, das beispielsweise verwendet wird, um die Konformationen von Polymeren zu modellieren, bei denen sich zwei Teile des Moleküls nicht am selben Ort befinden können. Im Gegensatz zum einfachen Random Walk gibt es für Self-Avoiding Walks keine exakten analytischen Lösungen, und ihre Untersuchung erfordert daher grundsätzlich numerische Methoden wie eben MC-Simulationen.
Praktische Anwendungen von Random Walks¶
Random Walks finden Anwendung in verschiedenen wissenschaftlichen und praktischen Bereichen:
- Physik: Brownsche Bewegung, Diffusion, thermisches Rauschen
- Chemie: Molekulare Diffusion, Reaktionskinetik
- Biologie: Bewegung von Zellen, Ausbreitung von Krankheiten
- Informatik: Suchstrategien, Netzwerkanalyse
- Finanzwirtschaft: Aktienkurse, Option Pricing (Black-Scholes-Modell)
- Künstliche Intelligenz: Exploration in Reinforcement Learning, Monte Carlo Tree Search
Das Konzept ist also sehr generell einsetzbar und kann auf vielfältigste Situationen anwendbar.
9.3 Monte-Carlo-Integration¶
Die Monte-Carlo-Integration ist eine der wichtigsten und weitverbreitetsten Anwendungen von Monte-Carlo-Methoden. MC-Integration ist auch einer der natürlichsten Berührungspunkte mit dem Gebiet der MC-Methoden allgemein. Sie bietet einen leistungsstarken Ansatz zur numerischen Berechnung von Integralen, insbesondere in hohen Dimensionen oder für Funktionen mit komplexen Geometrien, bei denen klassische numerische Integrationsmethoden wie die Trapezregel oder die Gauß-Quadratur an ihre Grenzen stoßen.
9.3.1 Das berühmte aber untypische Beispiel: Berechnung von Pi¶
Die Berechnung der Kreiszahl $\pi$ mittels MC-Methode ist ein klassisches Beispiel, das oft zur Einführung in dieses Thema verwendet wird. Obwohl es ein anschauliches Beispiel für MC-Methoden ist, ist es wichtig zu verstehen, dass es nicht repräsentativ für typische Anwendungen der Monte-Carlo-Integration ist. Warum, das werden Sie im Folgenden unterscheiden lernen.
Die Grundidee für das einfache MC-Berechnen von $\pi$ ist, geometrisches Sampling einzusetzen, und zwar so:
- Wir betrachten einen Einheitskreis, der in ein Quadrat mit der Seitenlänge 2 eingebettet ist.
- Die Fläche des Kreises beträgt $A_{Kreis} = \pi \cdot r^2 = \pi$, da $r = 1$.
- Die Fläche des Quadrats beträgt $A_{Quadrat} = 4$.
- Das Verhältnis der Flächen ist also $\frac{A_{Kreis}}{A_{Quadrat}} = \frac{\pi}{4}$.
Der Monte-Carlo-Ansatz besteht nun darin, zufällige Punkte innerhalb des Quadrats zu erzeugen und zu zählen, wie viele davon innerhalb des Kreises liegen. Für eine große Anzahl von Punkten sollte der Anteil der Punkte im Kreis dem Flächenverhältnis $\frac{\pi}{4}$ entsprechen. Somit können wir $\pi$ approximieren als:
$$\pi \approx 4 \cdot \frac{\text{Anzahl der Punkte im Kreis}}{\text{Gesamtanzahl der Punkte}}$$
Sehen wir uns das gleich einmal an: Zunächst setzen wir das geometrische Sampling um, aber nur im Viertelkreis, der im ersten Quadranten liegt. Auch dort ist das Verhältnis des Viertelkreises zum Quadrat mit der Seitenlänge 1 wieder $\frac{\pi}{4}$.
import numpy as np
import matplotlib.pyplot as plt
def estimate_pi_monte_carlo(num_points=1000):
"""
Estimates Pi using the Monte Carlo method by simulating points
in a unit square and checking if they fall within a quarter circle.
Args:
num_points (int): The total number of random points to generate.
Returns:
float: The estimated value of Pi.
numpy.ndarray: x-coordinates of points inside the quarter circle.
numpy.ndarray: y-coordinates of points inside the quarter circle.
numpy.ndarray: x-coordinates of points outside the quarter circle.
numpy.ndarray: y-coordinates of points outside the quarter circle.
int: Number of points inside the quarter circle.
"""
# Generate random points (x, y) within the unit square [0,1] x [0,1]
# Each point has x and y coordinates between 0 and 1.
x_coords = np.random.uniform(0, 1, num_points)
y_coords = np.random.uniform(0, 1, num_points)
# Calculate the distance of each point from the origin (0,0)
# A point is inside the quarter circle if x^2 + y^2 <= 1^2 (radius = 1)
distance_squared = x_coords**2 + y_coords**2
is_inside_circle = distance_squared <= 1.0
# Separate points for plotting and counting
points_inside_x = x_coords[is_inside_circle]
points_inside_y = y_coords[is_inside_circle]
points_outside_x = x_coords[~is_inside_circle]
points_outside_y = y_coords[~is_inside_circle]
# Count points inside the quarter circle
num_inside_circle = np.sum(is_inside_circle)
# Estimate Pi
# The ratio (num_inside_circle / num_points) approximates (Area of quarter circle / Area of square)
# Area of quarter circle = (pi * r^2) / 4. Here r=1. So, Area = pi / 4.
# Area of unit square = 1 * 1 = 1.
# So, (num_inside_circle / num_points) approx pi / 4.
# Therefore, pi approx 4 * (num_inside_circle / num_points).
pi_approximation = 4 * (num_inside_circle / num_points)
return (pi_approximation,
points_inside_x, points_inside_y,
points_outside_x, points_outside_y,
num_inside_circle)
def plot_pi_estimation(pi_estimate, num_total_points,
points_inside_x, points_inside_y,
points_outside_x, points_outside_y,
num_inside_circle):
"""
Plots the visualization for the Monte Carlo Pi estimation.
"""
num_outside_circle = num_total_points - num_inside_circle
fig, ax = plt.subplots(figsize=(7, 7)) # Create a figure and an axes
# Plot points inside the quarter circle (green)
ax.scatter(points_inside_x, points_inside_y, color='green',
label=f'Inside Circle ({num_inside_circle})', s=10, alpha=0.7)
# Plot points outside the quarter circle but inside the square (red)
ax.scatter(points_outside_x, points_outside_y, color='red',
label=f'Outside Circle ({num_outside_circle})', s=10, alpha=0.7)
# Draw the arc of the quarter circle (radius 1, from 0 to pi/2)
theta = np.linspace(0, np.pi/2, 200) # Angles for the arc
x_circle_arc = np.cos(theta) # x-coordinates of the arc
y_circle_arc = np.sin(theta) # y-coordinates of the arc
ax.plot(x_circle_arc, y_circle_arc, color='blue', linewidth=2, label='Quarter Circle (r=1)')
# Set plot properties
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_aspect('equal', adjustable='box') # Crucial for correct visual representation
ax.set_xlabel('X coordinate')
ax.set_ylabel('Y coordinate')
title_str = (
f'Monte Carlo Estimation of $\pi$\n'
f'{num_total_points} random points\n'
f'Estimated $\pi \\approx {pi_estimate:.4f}$' # Using LaTeX for Pi
)
ax.set_title(title_str)
ax.legend(loc='upper right')
ax.grid(True, linestyle='--', alpha=0.5)
plt.show()
# --- Main execution ---
if __name__ == '__main__':
# Parameters
number_of_points = 1000
# 1. Perform Monte Carlo estimation
(pi_value_estimate,
px_in, py_in,
px_out, py_out,
n_inside) = estimate_pi_monte_carlo(num_points=number_of_points)
# 2. Output the results
print(f"--- Monte Carlo Pi Estimation ---")
print(f"Total random points generated: {number_of_points}")
print(f"Points inside the quarter circle: {n_inside}")
print(f"Points outside the quarter circle (but in square): {number_of_points - n_inside}")
print(f"Estimated value of Pi: {pi_value_estimate:.5f}")
print(f"Value of math.pi (for comparison): {np.pi:.5f}")
print(f"Absolute error: {abs(np.pi - pi_value_estimate):.5f}")
# 3. Plot the visualization
plot_pi_estimation(pi_value_estimate, number_of_points,
px_in, py_in, px_out, py_out, n_inside)
--- Monte Carlo Pi Estimation --- Total random points generated: 1000 Points inside the quarter circle: 795 Points outside the quarter circle (but in square): 205 Estimated value of Pi: 3.18000 Value of math.pi (for comparison): 3.14159 Absolute error: 0.03841

Sehen wir uns noch kurz an, wie die Anzahlen der Samples sowie der Punkte in einem Sample den errechneten Mittelwert und dessen Standardabweichung beeinflussen.
import numpy as np
import matplotlib.pyplot as plt
# --- Part 1: Core Pi Estimation Function (simplified) ---
def get_pi_estimate_single_run(num_points):
"""
Estimates Pi using the Monte Carlo method for a single run.
This function does not produce any plots.
Args:
num_points (int): The total number of random points to generate for this run.
Returns:
float: The estimated value of Pi for this run.
"""
# Ensure num_points is positive to avoid division by zero
if num_points <= 0:
return 0.0 # Or raise an error, or return np.nan
# Generate random points (x, y) within the unit square [0,1] x [0,1]
# Each point has x and y coordinates between 0 and 1.
x_coords = np.random.uniform(0, 1, num_points)
y_coords = np.random.uniform(0, 1, num_points)
# Calculate the distance squared of each point from the origin (0,0)
# A point is inside the quarter circle if x^2 + y^2 <= 1^2 (radius = 1)
distance_squared = x_coords**2 + y_coords**2
is_inside_circle = distance_squared <= 1.0
# Count points inside the quarter circle
num_inside_circle = np.sum(is_inside_circle)
# Estimate Pi
# The ratio (num_inside_circle / num_points) approximates (Area of quarter circle / Area of square)
# Area of quarter circle = (pi * r^2) / 4. Here r=1. So, Area = pi / 4.
# Area of unit square = 1 * 1 = 1.
# So, (num_inside_circle / num_points) approx pi / 4.
# Therefore, pi approx 4 * (num_inside_circle / num_points).
pi_approximation = 4 * (num_inside_circle / num_points)
return pi_approximation
# --- Part 2: Function to run multiple samples and get stats ---
def run_pi_simulation_series(num_samples, num_points_per_sample):
"""
Runs a series of Pi estimation simulations and calculates statistics
on the collected Pi estimates.
Args:
num_samples (int): The number of individual Pi estimation runs (samples).
num_points_per_sample (int): The number of random points used in each sample.
Returns:
tuple: (mean_pi_estimate, std_dev_pi_estimate)
The mean and standard deviation of the Pi estimates from all samples.
"""
pi_estimates_for_series = []
for _ in range(num_samples):
estimate = get_pi_estimate_single_run(num_points_per_sample)
pi_estimates_for_series.append(estimate)
# Convert list to NumPy array for easier statistical calculations
pi_estimates_array = np.array(pi_estimates_for_series)
# Calculate the mean of the Pi estimates
mean_pi = np.mean(pi_estimates_array)
# Calculate the standard deviation of the Pi estimates
# This represents the spread of the Pi estimates obtained from the samples.
std_dev_pi = np.std(pi_estimates_array) # ddof=0 by default for np.std
return mean_pi, std_dev_pi
# --- Main execution block ---
if __name__ == '__main__':
# Define the different simulation configurations
# Each dictionary contains a label for plotting, number of samples, and points per sample.
configurations = [
{"label": "100 samples\n100 pts/sample", "samples": 100, "points": 100},
{"label": "1000 samples\n100 pts/sample", "samples": 1000, "points": 100},
{"label": "100 samples\n1000 pts/sample", "samples": 100, "points": 1000},
{"label": "1000 samples\n1000 pts/sample", "samples": 1000, "points": 1000},
{"label": "10000 samples\n1000 pts/sample", "samples": 10000, "points": 1000},
{"label": "10000 samples\n10000 pts/sample", "samples": 10000, "points": 10000},
{"label": "1000 samples\n10000 pts/sample", "samples": 1000, "points": 10000},
{"label": "100 samples\n100000 pts/sample", "samples": 100, "points": 100000},
]
results_means = [] # To store the mean Pi estimate for each configuration
results_std_devs = [] # To store the std dev of Pi estimates for each configuration
config_labels_for_plot = [] # To store labels for the plot's x-axis
print("--- Monte Carlo Pi Estimation: Series Results ---")
# Loop through each configuration, run simulations, and store results
for config in configurations:
mean_pi, std_dev_pi = run_pi_simulation_series(config["samples"], config["points"])
results_means.append(mean_pi)
results_std_devs.append(std_dev_pi)
config_labels_for_plot.append(config["label"])
# Print results for the current configuration
# Replacing newline in label for cleaner single-line console output
print(f"\nConfiguration: {config['label'].replace(chr(10), ' ')}")
print(f" Number of Samples: {config['samples']}")
print(f" Points per Sample: {config['points']}")
print(f" Mean of Pi Estimates (Mean of Sample Means): {mean_pi:.5f}")
print(f" Std Dev of Pi Estimates (Sigma of Sample Means): {std_dev_pi:.5f}")
# --- Part 3: Plotting the summary results ---
plt.figure(figsize=(12, 7)) # Adjusted figure size for better label display
x_positions = np.arange(len(config_labels_for_plot)) # X-coordinates for the bars/points
# Create the error bar plot
# fmt='o' plots points at the mean values
# capsize adds caps to the error bars
plt.errorbar(x_positions, results_means, yerr=results_std_devs,
fmt='o', color='dodgerblue', markersize=8,
capsize=5, capthick=1, elinewidth=1,
label='Mean Estimated $\pi$ (with Std Dev of Sample Means)')
# Add a horizontal dashed line for the actual value of Pi
plt.axhline(y=np.pi, color='red', linestyle='--', linewidth=1.5,
label=f'Actual $\pi \\approx {np.pi:.5f}$')
# Set plot properties
plt.xticks(x_positions, config_labels_for_plot, rotation=0, ha='center') # Center labels
plt.ylabel('Estimated Value of $\pi$')
plt.xlabel('Simulation Configuration (Samples x Points per Sample)')
plt.title('Monte Carlo $\pi$ Estimation: Impact of Sample Size and Points per Sample')
plt.legend(loc='best')
plt.grid(True, linestyle=':', alpha=0.6, axis='y') # Lighter grid on y-axis
plt.tight_layout() # Adjust layout to make sure everything fits without overlapping
plt.show()
--- Monte Carlo Pi Estimation: Series Results --- Configuration: 100 samples 100 pts/sample Number of Samples: 100 Points per Sample: 100 Mean of Pi Estimates (Mean of Sample Means): 3.13240 Std Dev of Pi Estimates (Sigma of Sample Means): 0.17127 Configuration: 1000 samples 100 pts/sample Number of Samples: 1000 Points per Sample: 100 Mean of Pi Estimates (Mean of Sample Means): 3.14164 Std Dev of Pi Estimates (Sigma of Sample Means): 0.16449 Configuration: 100 samples 1000 pts/sample Number of Samples: 100 Points per Sample: 1000 Mean of Pi Estimates (Mean of Sample Means): 3.14196 Std Dev of Pi Estimates (Sigma of Sample Means): 0.05382 Configuration: 1000 samples 1000 pts/sample Number of Samples: 1000 Points per Sample: 1000 Mean of Pi Estimates (Mean of Sample Means): 3.14055 Std Dev of Pi Estimates (Sigma of Sample Means): 0.05316 Configuration: 10000 samples 1000 pts/sample Number of Samples: 10000 Points per Sample: 1000 Mean of Pi Estimates (Mean of Sample Means): 3.14261 Std Dev of Pi Estimates (Sigma of Sample Means): 0.05142 Configuration: 10000 samples 10000 pts/sample Number of Samples: 10000 Points per Sample: 10000 Mean of Pi Estimates (Mean of Sample Means): 3.14159 Std Dev of Pi Estimates (Sigma of Sample Means): 0.01633 Configuration: 1000 samples 10000 pts/sample Number of Samples: 1000 Points per Sample: 10000 Mean of Pi Estimates (Mean of Sample Means): 3.14119 Std Dev of Pi Estimates (Sigma of Sample Means): 0.01656 Configuration: 100 samples 100000 pts/sample Number of Samples: 100 Points per Sample: 100000 Mean of Pi Estimates (Mean of Sample Means): 3.14131 Std Dev of Pi Estimates (Sigma of Sample Means): 0.00508

Obwohl diese Methode konzeptionell einfach ist, ist sie keine effiziente Methode zur Berechnung von $\pi$, nicht einmal im Rahmen von MC-Methoden. Vielmehr illustriert sie perfekt das Prinzip einer einfachen MC-Simulation. Die MC-Integration is jedoch in Ihrer effizienten Form auf folgende Grundlage gestellt:
9.3.2 Integration als gewichtete Summierung von Funktionswerten¶
Die allgemeine Idee der Monte-Carlo-Integration lässt sich wie folgt darstellen: Wir möchten ein bestimmtes Integral berechnen
$$I = \int_a^b f(x) \, dx$$
Der klassische Monte-Carlo-Ansatz besteht aus den folgenden Schritten:
- Ziehe $N$ zufällige Punkte $x_1, x_2, \ldots, x_N$ gleichverteilt aus dem Intervall $[a, b]$.
- Berechne den Funktionswert $f(x_i)$ für jeden dieser Punkte.
- Approximiere das Integral als gewichteten Durchschnitt dieser Funktionswerte:
$$I \approx \frac{b-a}{N} \sum_{i=1}^{N} f(x_i)$$
Die theoretische Grundlage für diese Approximation ist der Erwartungswert. Wenn $X$ eine auf $[a, b]$ gleichverteilte Zufallsvariable ist, dann gilt:
$$E\left[ (b-a) \cdot f(X) \right] = \int_a^b f(x) \, dx$$
Mit dem Gesetz der großen Zahlen konvergiert der empirische Mittelwert gegen den Erwartungswert und man darf so den Wert der Summe als Näherungswert des Integrals betrachten. Den zugehörigen Fehler kann man wieder mittels Means of Sample-Means bestimmen. Hier ein Beispiel für so eine Integration. Wir berechnen wieder näherungsweise die Zahl $\pi$ und zwar über den Erwartungswert der Funktion $f(x)=\sqrt{1-x^2}$. Für jeweils 100 Samples erhöhen wir die Samplegröße schrittweise um einen Faktor 10. Der Plot ganz unten zeigt die Abhängigkeit der Standardabweichung des Means of Sample-Means von der Samplegröße.
import numpy as np
import matplotlib.pyplot as plt
# --- Part 1: Core Pi Estimation Function using MC Integration ---
def estimate_pi_mc_integration_single_run(num_points_N):
"""
Estimates Pi using Monte Carlo integration of sqrt(1-x^2) from 0 to 1.
The integral of sqrt(1-x^2) dx from 0 to 1 is pi/4.
Args:
num_points_N (int): The number of random points (x_i values) to generate.
Returns:
float: The estimated value of Pi for this run.
"""
if num_points_N <= 0:
# Return NaN or raise error for invalid input
return np.nan
# Generate N random numbers uniformly distributed between 0 and 1
# x_values are in [0, 1), so 1 - x_values**2 is in (0, 1]
x_values = np.random.uniform(0, 1, num_points_N)
# Calculate f(x_i) = sqrt(1 - x_i^2) for each x_i
integrand_values = np.sqrt(1 - x_values**2)
# The mean of these values approximates the integral of f(x) from 0 to 1
mean_of_integrand = np.mean(integrand_values)
# Estimate of pi/4 is mean_of_integrand
# Estimate of pi is 4 * mean_of_integrand
pi_estimate = 4 * mean_of_integrand
return pi_estimate
# --- Part 2: Function to run multiple samples and get stats ---
def run_mc_integration_series(K_num_runs, N_points_per_run):
"""
Runs a series of Pi estimations using MC integration and calculates statistics.
Args:
K_num_runs (int): The number of individual Pi estimation runs (e.g., 20).
This is K in the error analysis.
N_points_per_run (int): The number of random points (N) used in each run.
Returns:
tuple: (mean_pi_estimate, std_dev_of_pi_estimates)
The mean of the K Pi estimates, and the standard deviation of these K Pi estimates.
"""
pi_estimates_from_runs = []
for _ in range(K_num_runs):
estimate = estimate_pi_mc_integration_single_run(N_points_per_run)
pi_estimates_from_runs.append(estimate)
pi_estimates_array = np.array(pi_estimates_from_runs)
# Filter out NaN values that might result from N_points_per_run <= 0, if not handled earlier
pi_estimates_array = pi_estimates_array[~np.isnan(pi_estimates_array)]
if len(pi_estimates_array) == 0:
return np.nan, np.nan
mean_pi = np.mean(pi_estimates_array)
# Std dev of the K individual Pi estimates (each derived from N points)
std_dev_pi = np.std(pi_estimates_array) # ddof=0 by default
return mean_pi, std_dev_pi
# --- Main execution block ---
if __name__ == '__main__':
K_samples_for_stats = 100 # Number of samples (runs) to average for each N
N_values = [100, 1000, 10000, 100000, 1000000, 10000000] # Points per Pi estimate
exact_pi = np.pi
# Store results for plotting
mean_pi_estimates_for_each_N = []
# std_dev_of_the_K_pi_estimates_for_each_N = [] # Standard deviation of the K estimates
absolute_errors_of_mean = []
print("--- Monte Carlo Integration for Pi: Error Convergence Analysis ---")
for N_val in N_values:
# For each N, get the mean of K Pi estimates and the std dev of those K estimates
mean_pi_of_K_runs, std_dev_of_K_runs = run_mc_integration_series(K_samples_for_stats, N_val)
if np.isnan(mean_pi_of_K_runs):
print(f"\nN = {N_val:10,d} points per estimate: Skipped due to invalid input or all NaN results.")
absolute_errors_of_mean.append(np.nan) # Placeholder for plotting
mean_pi_estimates_for_each_N.append(np.nan)
continue
mean_pi_estimates_for_each_N.append(mean_pi_of_K_runs)
# std_dev_of_the_K_pi_estimates_for_each_N.append(std_dev_of_K_runs)
# Absolute error of the mean of K estimates
error = np.abs(exact_pi - mean_pi_of_K_runs)
absolute_errors_of_mean.append(error)
print(f"\nN = {N_val:10,d} points per estimate:")
print(f" Number of samples (K) for stats: {K_samples_for_stats}")
print(f" Mean of {K_samples_for_stats} Pi Estimates: {mean_pi_of_K_runs:.8f}")
print(f" Std Dev of these {K_samples_for_stats} Pi Estimates: {std_dev_of_K_runs:.8f}")
print(f" Absolute Error (|Exact Pi - Mean of {K_samples_for_stats} Estimates|): {error:.3e}")
# --- Part 4: Plotting the error convergence ---
plt.figure(figsize=(10, 7))
# Plot actual absolute error vs N
plt.plot(N_values, absolute_errors_of_mean, marker='o', linestyle='-', color='dodgerblue',
label=f'Actual Error: |$\pi_{{exact}}$ - Mean of {K_samples_for_stats} Estimates|')
# Theoretical error scaling:
# The standard deviation of a single Pi estimate from N points (sigma_P_N) is (4 * sigma_f) / sqrt(N)
# where sigma_f = sqrt(Var(sqrt(1-x^2))) = sqrt( (2/3) - (pi/4)^2 ).
sigma_f_val = np.sqrt((2/3) - (np.pi/4)**2) # Approx 0.2231
# The error of the mean of K_samples_for_stats estimates is expected to be of the order of
# Standard Error of Mean (SEM) = sigma_P_N / sqrt(K_samples_for_stats)
# SEM = (4 * sigma_f_val) / (sqrt(N) * sqrt(K_samples_for_stats))
# So, Error ~ C_theory / sqrt(N), where C_theory = (4 * sigma_f_val) / sqrt(K_samples_for_stats)
C_theory = (4 * sigma_f_val) / np.sqrt(K_samples_for_stats) # Approx 0.1995 for K=20
# Generate theoretical error line
theoretical_error_line = [C_theory / np.sqrt(n_val) for n_val in N_values if n_val > 0]
valid_N_values_for_theory = [n_val for n_val in N_values if n_val > 0]
if theoretical_error_line: # Only plot if we have valid N values
plt.plot(valid_N_values_for_theory, theoretical_error_line, marker='.', linestyle='--', color='red',
label=f'Theoretical Error Trend ($C/\\sqrt{{N}}$)\n$C = (4 \sigma_f) / \sqrt{{{K_samples_for_stats}}} \\approx {C_theory:.4f}$')
plt.xscale('log')
plt.yscale('log') # Vertical log scale as requested
plt.xlabel('Number of Points per $\pi$ Estimate (N)')
plt.ylabel('Absolute Error (log scale)')
plt.title(f'Monte Carlo Integration for $\pi$: Error Convergence')
plt.legend(loc='best')
plt.grid(True, which="both", linestyle=':', alpha=0.7) # Grid for major and minor ticks
plt.tight_layout()
plt.show()
--- Monte Carlo Integration for Pi: Error Convergence Analysis --- N = 100 points per estimate: Number of samples (K) for stats: 100 Mean of 100 Pi Estimates: 3.13462908 Std Dev of these 100 Pi Estimates: 0.08759975 Absolute Error (|Exact Pi - Mean of 100 Estimates|): 6.964e-03 N = 1,000 points per estimate: Number of samples (K) for stats: 100 Mean of 100 Pi Estimates: 3.13981985 Std Dev of these 100 Pi Estimates: 0.02924496 Absolute Error (|Exact Pi - Mean of 100 Estimates|): 1.773e-03 N = 10,000 points per estimate: Number of samples (K) for stats: 100 Mean of 100 Pi Estimates: 3.14202810 Std Dev of these 100 Pi Estimates: 0.00955622 Absolute Error (|Exact Pi - Mean of 100 Estimates|): 4.354e-04 N = 100,000 points per estimate: Number of samples (K) for stats: 100 Mean of 100 Pi Estimates: 3.14134207 Std Dev of these 100 Pi Estimates: 0.00275560 Absolute Error (|Exact Pi - Mean of 100 Estimates|): 2.506e-04 N = 1,000,000 points per estimate: Number of samples (K) for stats: 100 Mean of 100 Pi Estimates: 3.14174607 Std Dev of these 100 Pi Estimates: 0.00086131 Absolute Error (|Exact Pi - Mean of 100 Estimates|): 1.534e-04 N = 10,000,000 points per estimate: Number of samples (K) for stats: 100 Mean of 100 Pi Estimates: 3.14162938 Std Dev of these 100 Pi Estimates: 0.00027357 Absolute Error (|Exact Pi - Mean of 100 Estimates|): 3.673e-05

9.3.3 Importance Sampling in der Monte-Carlo-Integration¶
Die einfache Monte-Carlo-Integration (mit gleichverteilten Integrationspunkten) ist zwar schon recht brauchbar, noch besser ist allerdings das “Importance Sampling”. Bei dieser Technik werden die Samples nicht gleichmäßig über den Integrationsbereich verteilt, sondern gemäß einer Wahrscheinlichkeitsdichte, die idealerweise der Form der zu integrierenden Funktion ähnelt. Die Grundidee ist einfach, mehr Samples in jenen Bereichen zu platzieren, in denen $f(x)$ “wichtiger” ist (z.B. wo $|f(x)|$ groß ist oder sich stark ändert), und weniger Samples dort, wo $f(x)$ nahe Null bzw. nahezu konstant ist. Mathematisch formuliert den Integranden so um, dass er in zwei Funktionen zerlegt wird. Eine davon ($g$) dient dabei als Basis für eine (hoffentlich) bessere Verteilung, aus der gesampelt wird:
$$I = \int_a^b f(x) \, dx = \int_a^b \frac{f(x)}{g(x)} \cdot g(x) \, dx = E_g\left[ \frac{f(X)}{g(X)} \right]$$
$g(x)$ ist dabei also eine Wahrscheinlichkeitsdichte und $E_g$ bezeichnet den Erwartungswert bezüglich dieser Dichte. Mit diesem Importance Sampling approximieren wir das Integral daher als:
$$I \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f(x_i)}{g(x_i)}$$
wobei die $x_i$ jetzt gemäß der Dichte $g(x)$ gezogen werden. Die Effizienz dieser Methode hängt stark von der Wahl einer geeigneten Funktion $g(x)$ ab, was in der Praxis eine Herausforderung darstellen kann, vor allem, wenn man die zu integrierende Funktion nicht in geschlossener Form kennt.
9.3.4 Höherdimensionale MC-Integration und Skalierung des Fehlers¶
Ein besonderer Vorteil der Monte-Carlo-Integration zeigt sich bei mehrdimensionalen Integralen:
$$I = \int_{D} f(\mathbf{x}) \, d\mathbf{x}$$
wobei $D$ ein mehrdimensionales Gebiet ist und $\mathbf{x} = (x_1, x_2, \ldots, x_d)$ ein $d$-dimensionaler Vektor. Klassische numerische Integrationsmethoden wie die uns bereits bekannte Trapezregel oder eine Gauß-Quadratur leiden in solchen Fällen besonders unter dem “Fluch der Dimensionalität”: Wenn wir beispielsweise $n$ Punkte pro Dimension verwenden, benötigen wir insgesamt $n^d$ Funktionsauswertungen in $d$ Dimensionen. Für ein dreidimensionales Integral mit nur 10 Punkten pro Dimension wären das bereits 1.000 Auswertungen. In 10 Dimensionen wären es 10 Milliarden! Ebenso wie die Anzahl der Integrationspunkte muss man hier auch die Fehlerrechnung der einzelnen Dimensionen in Kombination betrachten.
Bei der Monte-Carlo-Integration hingegen skaliert der Fehler mit $\frac{1}{\sqrt{N}}$ unabhängig von der Dimension des Problems. Dies macht die Monte-Carlo-Integration zu einer der wenigen praktikablen Methoden für hochdimensionale Integrale. Die Herausforderung liegt dann darin (eine Art abgeschwächte Form des Fluchs der Dimensionalität), das Sampling so reichhaltig oder gezielt zu steuern, dass genügend Punkte der Samples im Interessanten Bereich der Funktion liegen.
Die mehrdimensionale Monte-Carlo-Integration funktioniert analog zum eindimensionalen Fall:
- Erzeuge $N$ zufällige Punkte $\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{x}_N$ gleichverteilt im Gebiet $D$.
- Approximiere das Integral als:
$$I \approx \frac{V}{N} \sum_{i=1}^{N} f(\mathbf{x}_i)$$
wobei $V$ das Volumen von $D$ ist. Auch hier kann man mit Importance Sampling und weiteren fortgeschrittenen Methoden arbeiten, die die Treffsicherheit der Methode optimieren.
9.4 Übungsaufgabe: Abschätzung Fraktaler Dimension mit Monte-Carlo Methoden¶
Es klingt vielleicht etwas ungewöhnlich, aber in der Übungsaufgabe zu dieser Einheit möchte ich Ihnen die Möglichkeit zeigen, MC-Methoden zur Untersuchung fraktaler Strukturen einzusetzen. Konkret werden wir MC-Methoden anwenden, um eine faszinierende Eigenschaft geometrischer Objekte zu untersuchen: die fraktale Dimension. Dabei geht es im Prinzip darum, die Fraktale Struktur auf mehreren Skalen geometrisch zu untersuchen oder “abzutasten”. Wir werden das gleich im Detail angehen, doch zuvor ein paar Grundbegriffe zur Wiederholung bzw. Einführung.
9.4.1 Fraktale (Ultra-Crash-Course)¶
Fraktale sind geometrische Objekte, die besondere Eigenschaften aufweisen, darunter die Selbstähnlichkeit. Dies bedeutet, dass Teile des Objekts dem Ganzen ähnlich sind, unabhängig davon, auf welcher Skala man sie betrachtet. Einige bekannte Beispiele für Fraktale sind:
Die Koch-Kurve: Beginnend mit einer geraden Linie wird das mittlere Drittel durch zwei Seiten eines gleichseitigen Dreiecks ersetzt. Dieser Prozess wird dann rekursiv auf jede der resultierenden Linien angewandt.
Die Sierpinski-Dreiecke: Ausgehend von einem gleichseitigen Dreieck werden immer wieder die mittleren Dreiecke entfernt, wodurch eine selbstähnliche Struktur entsteht.
Die Mandelbrot-Menge: Ein komplexeres mathematisches Fraktal, das durch Iteration einer einfachen Gleichung in der komplexen Ebene entsteht.
Natürliche Fraktale: Viele Strukturen in der Natur wie Küstenlinien, Bäume, Blutgefäßsysteme oder Wolken weisen fraktale Eigenschaften auf. Solche Eigenschaften sind:
- Die schon erwähnte Selbstähnlichkeit: Teile des Objekts ähneln dem Ganzen
- Rekursive Definition: Viele Fraktale werden durch wiederholte Anwendung einfacher Regeln erzeugt
- Detailreichtum auf allen Skalen: Bei Vergrößerung erscheinen immer neue Details
- Nicht-ganzzahlige Dimension: Fraktale haben oft Dimensionen, die keine ganzen Zahlen sind
Die letzte Eigenschaft, die nicht-ganzzahlige oder “fraktale” Dimension, wird der Fokus unserer Übungsaufgabe sein.
9.4.2 Fraktale Dimension und wie man sie berechnet¶
Die fraktale Dimension ist ein Maß dafür, wie ein Objekt den Raum füllt, in dem es definiert ist. Sie verallgemeinert den intuitiven Begriff der Dimension (Linien sind eindimensional, Flächen zweidimensional usw.) auf Objekte mit komplexeren Strukturen. Stellen wir uns vor, wir haben ein Objekt mit der Dimension $D$. Wenn wir dieses Objekt um den Faktor $r$ in jede Richtung skalieren, wie verändert sich dann sein “Volumen” (oder allgemeiner: sein Maß)?
Für gewöhnliche Objekte gilt:
- Eine Linie ($D=1$): Verdopplung der Länge → Verdopplung des Maßes
- Ein Quadrat ($D=2$): Verdopplung der Seitenlänge → Vervierfachung des Maßes
- Ein Würfel ($D=3$): Verdopplung der Kantenlänge → Verachtfachung des Maßes
Allgemein: Wenn wir ein Objekt mit Dimension $D$ um den Faktor $r$ skalieren, verändert sich sein Maß um den Faktor $r^D$. Bei Fraktalen gibt es mehrere Methoden, um ihre Dimension zu bestimmen. Wir benutzen hier eine Methode, die sich dann zur Berechnung mittels MC-Methode eignet.
In dieser Übung sollen Sie die fraktale Dimension eines digital generierten Sierpinski-Teppichs mithilfe der Masse-Radius-Methode (auch als “Masse-in-einer-Box”-Methode bekannt) abschätzen. Das Fraktal ist dabei als png-Bild gespeichert, in dem es schwarze (Teppich) und weiße (Löcher) Punkte gibt, siehe unten. Sie werden Monte-Carlo-Techniken zur Auswahl von Mittelpunkten auf dem Fraktal verwenden und die Skalierung der Masse mit der Größe der Beobachtungsbox analysieren. Damit ist folgendes gemeint:
Man wählt einen Punkt (Pixel) aus, der auf dem Fraktal liegt (schwarz). Dann sieht man sich eine Box (also ein Quadrat) an, in dessen Mitte der ausgewählte Punkt liegt. Um das einfach zu machen, nehmen wir Quadrate mit einer Seitenlänge von $2L+1$ an. $L$ kann dabei Werte von 1 bis zu einer Größe annehmen, die noch in das ganze Bild passt. Die Masse $M$ in der Box ist dann die Anzahl der Pixel, die zum Fraktal gehören (also alle schwarzen Pixel). Der Weg zur Fraktalen Dimension geht man, indem man diese Masse für verschiedene Werte von $L$ berechnet, denn $L$ entspricht einer einer Meßskala. Wenn man beim Plotten der Masse in Abhängigkeit von der Skala ein Potenzgesetz findet, dann hat man: $$M(L)\propto L^D$$
wobei $D$ die fraktale Dimension ist. Durch Auftragen von $\log(M(L))$ gegen $\log(L)$ kann $D$ als Steigung der resultierenden Geraden bestimmt werden.
9.4.3 Monte-Carlo-Berechnung der fraktalen Dimension¶
Die Monte-Carlo-Herangehensweise hilft uns nun dabei, mit unserem üblichen System der Berechnung des Means of Sample-Means und der zugehörigen Standardabweichung für verschiedene Skalen ($L$) den Wert von $M$ zu ermitteln. Sampeln Sie daher mit einer Samplegröße von 100 Punkten (oder weniger) über 10-20 Samples die Größe $M$. Analysieren Sie den Zusammenhang von $\log(M(L))$ und $\log(L)$ und suchen Sie nach einer Power-Law-Abhängigkeit.
9.4.4 Aufgabenstellung¶
Die konkreten Schritte sind:
- Laden Sie das Bild (
sierpinski_carpet_N8_6561x6561.png) und verwandeln Sie es in ein numpy-Array mit schwarzen und weißen Pixel - Implementieren Sie die Massen-Berechnung
- Definieren Sie eine geeignete Folge von Skalen $L$
- Implementieren Sie die MC-Simulation
- Sammeln Sie die Daten über alle Skalen, plotten und analysieren Sie die Ergebnisse Viel Vergnügen!
9.4.5 Vorbereitung: Erzeugen eines Bildes des Sierpinski-Teppichs¶
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import pickle
import os # For checking file sizes
def generate_sierpinski_carpet(n_levels):
"""
Generates a Sierpinski carpet as a NumPy array.
0 represents the carpet (black), 1 represents holes (white).
Args:
n_levels (int): The number of recursive levels (N in 3^N x 3^N).
Returns:
numpy.ndarray: A 2D NumPy array representing the Sierpinski carpet.
"""
if not isinstance(n_levels, int) or n_levels < 0:
raise ValueError("n_levels (N) must be a non-negative integer.")
image_side_length = 3**n_levels
# Initialize the carpet: 0 for black (carpet material), 1 for white (holes)
# Start with a fully black carpet.
carpet_array = np.zeros((image_side_length, image_side_length), dtype=np.uint8)
# Recursive helper function to "punch holes"
def _create_holes_recursive(arr, x_start, y_start, current_side, current_level, max_level):
# Base case: if we've reached the desired number of iterations, stop
if current_level >= max_level:
return
# Size of the sub-squares (integer division)
sub_square_side = current_side // 3
# If sub_square_side is 0, it means current_side was < 3, so no more holes can be punched.
if sub_square_side == 0:
return
# Calculate coordinates for the central hole
hole_y_start_idx = y_start + sub_square_side
hole_y_end_idx = y_start + 2 * sub_square_side
hole_x_start_idx = x_start + sub_square_side
hole_x_end_idx = x_start + 2 * sub_square_side
# "Punch" the central hole by setting its pixels to 1 (white)
arr[hole_y_start_idx:hole_y_end_idx, hole_x_start_idx:hole_x_end_idx] = 1
# Recursively call for the 8 surrounding sub-squares
next_level = current_level + 1
for i in range(3):
for j in range(3):
# Skip the central square (which is now a hole)
if i == 1 and j == 1:
continue
new_x_start = x_start + i * sub_square_side
new_y_start = y_start + j * sub_square_side
_create_holes_recursive(arr, new_x_start, new_y_start, sub_square_side, next_level, max_level)
# Initial call to the recursive function
# Starts with level 0, up to n_levels (exclusive for current_level in recursive calls)
_create_holes_recursive(carpet_array, 0, 0, image_side_length, 0, n_levels)
return carpet_array
# --- Main execution ---
if __name__ == "__main__":
N = 5 # Number of levels for the Sierpinski carpet (size will be 3^N x 3^N)
# For N=7, size is 2187x2187 pixels.
# For N=6, size is 729x729 pixels.
# For N=5, size is 243x243 pixels.
image_dimension = 3**N
base_filename = f"sierpinski_carpet_N{N}_{image_dimension}x{image_dimension}"
print(f"Generating Sierpinski carpet for N={N} (size: {image_dimension}x{image_dimension})...")
try:
sierpinski_array = generate_sierpinski_carpet(N)
print("Sierpinski carpet array generated successfully.")
# Option 1: Save as a 1-bit PNG file using Pillow
png_filepath = base_filename + ".png"
try:
pil_image = Image.fromarray(255*sierpinski_array, mode='L')
pil_image.save(png_filepath, optimize=False) # optimize=True can reduce file size
except ImportError:
print("Pillow library (PIL) not found. Cannot save as PNG.")
print("Please install it using: pip install Pillow")
except Exception as e:
print(f"An error occurred while saving PNG: {e}")
except ValueError as ve:
print(f"Error in parameter: {ve}")
except Exception as general_e:
print(f"An unexpected error occurred: {general_e}")
print("Beispiel-Plot für N=5:")
plt.matshow(sierpinski_array, cmap='gray')
Generating Sierpinski carpet for N=5 (size: 243x243)... Sierpinski carpet array generated successfully. Beispiel-Plot für N=5:

Und nun eine Musterlösung der Übungsaufgabe:
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import random
import time
from scipy.stats import linregress # For fitting the line
# --- 1. Load Image and Prepare Data ---
def load_and_prepare_image(image_path):
"""
Loads a PNG image, converts it to a binary NumPy array,
and extracts coordinates of fractal pixels.
Assumes input image has 0 for black (fractal) and 255 for white (background).
Output binary_image has 1 for fractal, 0 for background.
"""
try:
print(f"Loading image from: {image_path}")
img_pil = Image.open(image_path).convert('L') # Ensure grayscale
img_array_loaded = np.array(img_pil)
# Convert to binary: 1 for fractal (original black), 0 for background
# Assuming black (carpet) pixels are 0 and white (holes) are 255 in the input image.
binary_image = (img_array_loaded == 0).astype(np.uint8)
fractal_pixel_coords = np.argwhere(binary_image == 1) # Get (row, col) or (y, x)
if fractal_pixel_coords.size == 0:
raise ValueError("No fractal pixels (value 1 after conversion) found. Check image loading and thresholding.")
print(f"Image loaded successfully: {binary_image.shape[0]}x{binary_image.shape[1]} pixels.")
print(f"Found {len(fractal_pixel_coords)} fractal pixels.")
return binary_image, fractal_pixel_coords
except FileNotFoundError:
print(f"Error: Image file not found at '{image_path}'. Please ensure the path is correct.")
return None, None
except Exception as e:
print(f"Error loading or processing image: {e}")
return None, None
# --- 2. Implement Mass Measurement Function ---
def measure_mass_in_square(binary_image_array, center_y, center_x, half_side_n):
"""
Measures the mass (number of fractal pixels, i.e., sum of 1s)
in a square of side length (2*half_side_n + 1) centered at (center_y, center_x).
Handles image boundaries by summing only within valid image coordinates.
"""
img_height, img_width = binary_image_array.shape
# Define the bounds of the square neighborhood
y_min = max(0, center_y - half_side_n)
y_max = min(img_height, center_y + half_side_n + 1) # Slice end is exclusive
x_min = max(0, center_x - half_side_n)
x_max = min(img_width, center_x + half_side_n + 1) # Slice end is exclusive
# Extract the neighborhood and sum the fractal pixels (where value is 1)
neighborhood = binary_image_array[y_min:y_max, x_min:x_max]
mass = np.sum(neighborhood)
return mass
# --- Main Analysis Function ---
def run_mass_radius_analysis(image_path, list_of_n_values,
num_centers_per_exp, num_experiments):
"""
Performs the mass-radius analysis by sampling points from the fractal.
Returns lists of L values, mean masses, and std devs of mean masses.
"""
binary_image, all_fractal_coords = load_and_prepare_image(image_path)
if binary_image is None or all_fractal_coords.size == 0:
print("Failed to load or process image data. Aborting analysis.")
return [], [], []
img_height, img_width = binary_image.shape
results_L_values = []
results_mean_of_means_mass = []
results_std_dev_of_means_mass = []
total_fractal_pixels = len(all_fractal_coords)
for n_val in list_of_n_values:
current_L_value = 2 * n_val + 1
print(f"\nProcessing scale L = {current_L_value} (n = {n_val})...")
# For this method, any fractal pixel can be a center.
# The measure_mass_in_square function handles boundaries.
# We don't need to filter centers unless L is very large relative to image size
# and we want to avoid too many truncated boxes, but averaging helps.
if total_fractal_pixels == 0:
print(" No fractal pixels to sample centers from. Skipping scale.")
continue
experiment_mean_masses_for_this_L = [] # Store the mean mass from each experiment
for exp_idx in range(num_experiments):
batch_masses_for_current_experiment = []
# Sample 'num_centers_per_exp' indices from all_fractal_coords
# Using np.random.choice for potentially large arrays.
# With replacement is fine and simpler if num_centers_per_exp can be > total_fractal_pixels (unlikely here).
# If sampling without replacement, ensure num_centers_per_exp <= total_fractal_pixels.
num_to_sample = min(num_centers_per_exp, total_fractal_pixels)
if num_to_sample == 0 : continue # Should not happen if total_fractal_pixels > 0
# replace=True allows sampling more than available if needed, but usually we want unique centers if possible.
# Let's stick to sampling 'num_to_sample' without replacement if possible, or with if needed.
use_replacement = num_centers_per_exp > total_fractal_pixels
sampled_indices = np.random.choice(total_fractal_pixels,
size=num_centers_per_exp,
replace=use_replacement)
centers_for_this_batch = all_fractal_coords[sampled_indices]
for center_y, center_x in centers_for_this_batch:
mass = measure_mass_in_square(binary_image, center_y, center_x, n_val)
batch_masses_for_current_experiment.append(mass)
if batch_masses_for_current_experiment:
mean_mass_this_experiment = np.mean(batch_masses_for_current_experiment)
experiment_mean_masses_for_this_L.append(mean_mass_this_experiment)
if not experiment_mean_masses_for_this_L:
print(f" No mass data collected for L={current_L_value}. Possible issue with sampling or small N. Skipping.")
continue
# Calculate the mean of these experiment means, and their standard deviation
final_mean_M_L = np.mean(experiment_mean_masses_for_this_L)
final_std_dev_M_L = np.std(experiment_mean_masses_for_this_L) # Std dev of the means
results_L_values.append(current_L_value)
results_mean_of_means_mass.append(final_mean_M_L)
results_std_dev_of_means_mass.append(final_std_dev_M_L)
print(f" L={current_L_value}: Mean of ({num_experiments}) Mean Masses = {final_mean_M_L:.2f}, "
f"StdDev of these Means = {final_std_dev_M_L:.3f}")
return results_L_values, results_mean_of_means_mass, results_std_dev_of_means_mass
# --- 6. Visualize Results and Fit for Power Law ---
def visualize_and_fit_results(L_values, mean_mass_values, std_dev_of_means):
if not L_values or not mean_mass_values:
print("No data available for visualization.")
return
# Filter out non-positive values before taking log, if any (e.g. if mass was 0)
valid_indices = [i for i, m in enumerate(mean_mass_values) if m > 0]
if not valid_indices:
print("No positive mean mass values to plot on log scale.")
return
L_values_log = np.log([L_values[i] for i in valid_indices])
mean_mass_log = np.log([mean_mass_values[i] for i in valid_indices])
# Std dev of means can be used for error bars, but interpretation on log plot needs care.
# For now, we'll plot points and fit.
plt.figure(figsize=(10, 7))
plt.plot(L_values_log, mean_mass_log, 'o', markersize=8, alpha=0.7, label='Log(Mean Mass) vs Log(L)')
# Attempt to fit a line to the log-log data to find the slope (Fractal Dimension)
# Students should be encouraged to inspect the plot and select an appropriate range for fitting.
# Here, we'll try to fit a central portion or all if few points.
if len(L_values_log) >= 2: # Need at least 2 points for a line
# Heuristic for fitting range (e.g., middle 50% of points if enough points)
num_pts_log = len(L_values_log)
fit_start_idx = num_pts_log // 4
fit_end_idx = (3 * num_pts_log // 4)
if fit_end_idx <= fit_start_idx : # if too few points, use all available for fit.
fit_start_idx = 0
fit_end_idx = num_pts_log
if fit_end_idx - fit_start_idx < 2 and num_pts_log >=2: # ensure at least 2 points
fit_start_idx = 0
fit_end_idx = num_pts_log
if fit_end_idx - fit_start_idx >= 2:
try:
slope, intercept, r_value, p_value, std_err = linregress(
L_values_log[fit_start_idx:fit_end_idx],
mean_mass_log[fit_start_idx:fit_end_idx]
)
print(f"\n--- Linear Regression Fit (Log-Log Scale) ---")
print(f"Fit Range (log_L indices): {fit_start_idx} to {fit_end_idx-1}")
print(f" Estimated Fractal Dimension (Slope D): {slope:.4f}")
print(f" Intercept: {intercept:.4f}")
print(f" R-squared: {r_value**2:.4f}")
print(f" Standard error of the slope: {std_err:.4f}")
# Plot the fitted line
L_fit_plot = np.array(L_values_log) # Use all L values for plotting the line extent
mass_fit_plot = slope * L_fit_plot + intercept
plt.plot(L_fit_plot, mass_fit_plot, 'r--', linewidth=2,
label=f'Fit: D = {slope:.3f} (R²={r_value**2:.3f})')
except Exception as e:
print(f"Could not perform linear regression: {e}")
else:
print("Not enough data points in selected range for a meaningful linear fit.")
else:
print("Not enough data points for linear regression.")
plt.xlabel('Log(L) (L = side length of square)')
plt.ylabel('Log(Mean Mass)')
plt.title(f'Mass-Radius Method (Squares) - Fractal Dimension Estimation')
plt.legend()
plt.grid(True, which="both", linestyle=':', alpha=0.7)
plt.show()
# --- Main Execution ---
if __name__ == '__main__':
# IMPORTANT: Students should replace this with the actual path to their generated image
# For N=8 (6561x6561), use: "sierpinski_carpet_N8_6561x6561_grayscale.png"
# For N=7 (2187x2187), use: "sierpinski_carpet_N7_2187x2187_grayscale.png"
# Ensure this file exists in the same directory as the script, or provide a full path.
IMAGE_FILENAME = "sierpinski_carpet_N8_6561x6561.png"
# Define scales 'n' (half_side_n for square of side 2n+1).
# L = 2n+1. Example: n=1 -> L=3; n=2 -> L=5; n=4 -> L=9, etc.
# These values should be chosen based on image size. Max L should be < image_size/2.
# For 2187x2187 (N=7), max n could be ~500 (L~1001).
# For 6561x6561 (N=8), max n could be ~1500 (L~3001).
# Using a geometric-like progression for n:
n_values_to_test = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233] # Fibonacci-like for L spread
# Max n=233 => L = 467. This should be fine for N=7 and N=8 images.
# Filter n_values if L gets too large for smaller images if necessary, but this range is likely okay.
# Monte Carlo parameters
num_centers_to_sample_per_experiment = 100 # Number of random centers to average mass over
num_experiments = 20 # Number of times to repeat the above to get stats on the mean mass
print("Starting Mass-Radius Analysis for Fractal Dimension...")
start_time = time.time()
L_results, mean_mass_results, std_dev_results = run_mass_radius_analysis(
IMAGE_FILENAME,
n_values_to_test,
num_centers_to_sample_per_experiment,
num_experiments
)
end_time = time.time()
total_time = end_time - start_time
print(f"\n--- Analysis Complete ---")
print(f"Total execution time: {total_time:.2f} seconds ({total_time/60:.2f} minutes).")
if L_results and mean_mass_results:
visualize_and_fit_results(L_results, mean_mass_results, std_dev_results)
else:
print("Analysis did not yield results to visualize. Please check for errors above.")
Starting Mass-Radius Analysis for Fractal Dimension... Loading image from: sierpinski_carpet_N8_6561x6561.png Image loaded successfully: 6561x6561 pixels. Found 16777216 fractal pixels. Processing scale L = 3 (n = 1)... L=3: Mean of (20) Mean Masses = 7.20, StdDev of these Means = 0.118 Processing scale L = 5 (n = 2)... L=5: Mean of (20) Mean Masses = 18.93, StdDev of these Means = 0.258 Processing scale L = 7 (n = 3)... L=7: Mean of (20) Mean Masses = 36.01, StdDev of these Means = 0.493 Processing scale L = 11 (n = 5)... L=11: Mean of (20) Mean Masses = 83.07, StdDev of these Means = 1.478 Processing scale L = 17 (n = 8)... L=17: Mean of (20) Mean Masses = 190.71, StdDev of these Means = 2.521 Processing scale L = 27 (n = 13)... L=27: Mean of (20) Mean Masses = 454.82, StdDev of these Means = 7.009 Processing scale L = 43 (n = 21)... L=43: Mean of (20) Mean Masses = 1108.88, StdDev of these Means = 19.697 Processing scale L = 69 (n = 34)... L=69: Mean of (20) Mean Masses = 2681.40, StdDev of these Means = 35.261 Processing scale L = 111 (n = 55)... L=111: Mean of (20) Mean Masses = 6606.84, StdDev of these Means = 75.998 Processing scale L = 179 (n = 89)... L=179: Mean of (20) Mean Masses = 16199.92, StdDev of these Means = 211.542 Processing scale L = 289 (n = 144)... L=289: Mean of (20) Mean Masses = 39526.37, StdDev of these Means = 636.025 Processing scale L = 467 (n = 233)... L=467: Mean of (20) Mean Masses = 96514.25, StdDev of these Means = 1557.088 --- Analysis Complete --- Total execution time: 220.30 seconds (3.67 minutes). --- Linear Regression Fit (Log-Log Scale) --- Fit Range (log_L indices): 3 to 8 Estimated Fractal Dimension (Slope D): 1.8921 Intercept: -0.1135 R-squared: 1.0000 Standard error of the slope: 0.0028

