{ "cells": [ { "cell_type": "markdown", "id": "3067f2e0", "metadata": {}, "source": [ "# Optimization Methods of GerryChain\n", "\n", "\n", "\n", "In GerryChain, we provide a class known as the `SingleMetricOptimizer` as well as a\n", "`Gingelator` subclass that allow us to perform optimization runs.\n", "\n", "\n", "Currently, there are 3 different optimization methods available in GerryChain:\n", "\n", "- **Short Bursts**: This method chains together a series of neutral explorers. The main\n", " idea is to run the chain for a short period of time (short burst) and then continue\n", " the chain from the partition that maximizes the objective function within the most\n", " recent short burst. For more information, please refer to\n", " [this paper](https://arxiv.org/abs/2011.02288).\n", "\n", "- **Simulated Annealing**: This method varies the probablity of accepting a worse plan\n", " according to a temperature schedule which ranges from 0 to 1.\n", "\n", "- **Tilted Runs**: This method accepts a worse plan with a fixed probability $p$,\n", " and always accepts better plans.\n", "\n", "\n", "While sampling naively with GerryChain can give us an understanding of the neutral\n", "baseline for a state, there are often cases where we want to find plans with\n", "properties that are rare to encounter in a neutral run. Many states have\n", "laws/guidelines that state that plans should be as compact as feasibly possible, maximize\n", "preservation of political boundaries and/or communities of interest; some even look to\n", "minimize double bunking of incumbents or seek proportionality/competitiveness in\n", "contests. Heuristic optimization methods can be used to find example plans with these\n", "properties and to explore the trade-offs between them.\n", "\n", "\n", "
\n", " \n", " Download Example File\n", " \n", "
\n", "
\n", "\n", "\n", "For the first part of this documentation, we will be working with a simple example wherein\n", "we will try to minimize the number of cut edges in a partition. There are, of course\n", "more complex things that we can do here, but the point of this guide is to just get you\n", "used to the API for the `SingleMetricOptimizer` class. As per usual, we will begin\n", "with importing the necessary packages:" ] }, { "cell_type": "code", "execution_count": null, "id": "579ac694", "metadata": {}, "outputs": [], "source": [ "from gerrychain import (\n", " GeographicPartition,\n", " Partition,\n", " Graph,\n", " MarkovChain,\n", " proposals,\n", " updaters,\n", " constraints,\n", " accept,\n", " Election,\n", ")\n", "from gerrychain.optimization import SingleMetricOptimizer, Gingleator\n", "from gerrychain.partition import recursive_seed_part\n", "from gerrychain.proposals import build_recom_proposal_fn\n", "from functools import partial\n", "import pandas as pd\n", "import json\n", "from networkx.readwrite import json_graph\n", "import matplotlib.pyplot as plt\n", "from tqdm import tqdm\n", "import numpy as np\n", "import random\n", "\n", "rng = random.Random(2024)" ] }, { "cell_type": "markdown", "id": "dfe9b147", "metadata": {}, "source": [ "Since the `SingleMetricOptimizer` class uses ReCom under the hood, we will need to\n", "do a lot of the same setup that we did in [Running a chain with ReCom](./recom.ipynb)\n", "section:" ] }, { "cell_type": "code", "execution_count": null, "id": "07f94678", "metadata": {}, "outputs": [], "source": [ "graph = Graph.from_json(\"05_bg_census_consolidated.json\")\n", "\n", "POPCOL = \"tot_pop_20\"\n", "SEN_DISTS = 35\n", "EPS = 0.02\n", "TOTPOP = sum(graph.node_data(node_id)[POPCOL] for node_id in graph.node_indices)\n", "\n", "chain_updaters = {\n", " \"population\": updaters.Tally(POPCOL, alias=\"population\"),\n", "}\n", "\n", "initial_partition = Partition.from_random_assignment(\n", " graph=graph,\n", " n_parts=SEN_DISTS,\n", " epsilon=EPS,\n", " pop_col=POPCOL,\n", " updaters=chain_updaters,\n", " rng=rng,\n", ")\n", "\n", "proposal_fn = build_recom_proposal_fn(\n", " pop_col=POPCOL,\n", " pop_target=TOTPOP / SEN_DISTS,\n", " epsilon=EPS,\n", ")\n", "\n", "chain_constraints = constraints.within_percent_of_ideal_population(initial_partition, EPS)" ] }, { "cell_type": "markdown", "id": "e25e1ee6", "metadata": {}, "source": [ "## Using `SingleMetricOptimizer`\n", "\n", "`SingleMetricOptimizer` is a wrapper around our basic `MarkovChain`\n", "class; to set it up, we simply pass it a proposal function, some constraints, an initial\n", "state, and the objective function of interest:" ] }, { "cell_type": "code", "execution_count": null, "id": "19678d69", "metadata": {}, "outputs": [], "source": [ "def num_cut_edges(partition):\n", " return len(partition[\"cut_edges\"])\n", "\n", "\n", "optimizer = SingleMetricOptimizer(\n", " proposal_fn=proposal_fn,\n", " constraints=chain_constraints,\n", " initial_state=initial_partition,\n", " optimization_metric_fn=num_cut_edges,\n", " maximize=False,\n", " rng=rng,\n", ")" ] }, { "cell_type": "markdown", "id": "fa2173d8", "metadata": {}, "source": [ "An important thing to note here is that the objective function that we are passing takes as\n", "input a `Partition` object and returns a float or integer value. In our case, one of the\n", "default updaters for the `Partition` class, `cut_edges`, returns the cut edge set for\n", "the whole partition. With this, we can run each of the optimization methods and collect some data!\n", "\n", "> Note\n", ">\n", "> We collect data using the syntax `optimizer.best_score`. The `.best_score` property\n", "> of the `SingleMetricOptimizer` just returns the best score that has been observed\n", "> throughout the duration of the optimization process. To evaluate the score of a\n", "> particular partition, we can use the syntax `optimizer.score_fn(partition)`." ] }, { "cell_type": "code", "execution_count": null, "id": "fcaff067", "metadata": {}, "outputs": [], "source": [ "total_steps = 5000\n", "\n", "# Short Bursts\n", "min_scores_sb = np.zeros(total_steps)\n", "scores_sb = np.zeros(total_steps)\n", "for i, part in enumerate(optimizer.short_bursts(5, 1000, with_progress_bar=False)):\n", " min_scores_sb[i] = optimizer.best_score\n", " scores_sb[i] = optimizer.score_fn(part)\n", "\n", "# Simulated Annealing\n", "min_scores_anneal = np.zeros(total_steps)\n", "scores_anneal = np.zeros(total_steps)\n", "for i, part in enumerate(\n", " optimizer.simulated_annealing(\n", " total_steps,\n", " optimizer.jumpcycle_beta_function(200, 800),\n", " beta_magnitude=1,\n", " with_progress_bar=False,\n", " )\n", "):\n", " min_scores_anneal[i] = optimizer.best_score\n", " scores_anneal[i] = optimizer.score_fn(part)\n", "\n", "# Tilted Runs\n", "min_scores_tilt = np.zeros(total_steps)\n", "scores_tilt = np.zeros(total_steps)\n", "for i, part in enumerate(optimizer.tilted_run(total_steps, p=0.125, with_progress_bar=False)):\n", " min_scores_tilt[i] = optimizer.best_score\n", " scores_tilt[i] = optimizer.score_fn(part)" ] }, { "cell_type": "markdown", "id": "bc74580f", "metadata": {}, "source": [ "We can then plot the results to see how each method performed:" ] }, { "cell_type": "code", "execution_count": null, "id": "27b35ba8", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(12, 6))\n", "plt.plot(min_scores_sb, label=\"Short Bursts\")\n", "plt.plot(min_scores_anneal, label=\"Simulated Annealing\")\n", "plt.plot(min_scores_tilt, label=\"Tilted Run\")\n", "plt.xlabel(\"Steps\", fontsize=20)\n", "plt.ylabel(\"Min #CutEdges Observered\", fontsize=20)\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "90cc0024", "metadata": {}, "source": [ "Of course, the above trace is just shows the progression of the best score as the chain\n", "moves along. If we want to see how the optimization function performs at each step, we can\n", "plot the full trace:" ] }, { "cell_type": "code", "execution_count": null, "id": "cd3da236", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(12, 6))\n", "plt.plot(scores_sb, label=\"Short Bursts\")\n", "plt.plot(scores_anneal, label=\"Simulated Annealing\")\n", "plt.plot(scores_tilt, label=\"Tilted Run\")\n", "plt.xlabel(\"Steps\", fontsize=20)\n", "plt.ylabel(\"Cut Edges\", fontsize=20)\n", "plt.legend(fontsize=9)\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "86579059", "metadata": {}, "source": [ "Here we can see some of the quirks of each of the optimization methods. For example,\n", "our simulated annealing method is using a jumpcycle beta function and we can see when\n", "the acceptance rate is high represented by spikes in the trace plot. Short bursts\n", "is generally pretty noisy, but since each burst starts from the best partition in the\n", "previous burst, we see that it has a general downward trend. Lastly, tilted runs\n", "just accept worse partitions with a fixed probability, so the (relatively) uniform volatility\n", "of the cut edge score trace plot is expected.\n", "\n", "\n", "## Using `Gingleator`\n", "\n", "Named for the Supreme Court case *Thornburg v. Gingles*, **Gingles' Districts** are\n", "districts that are 50% + 1 of a minority population subgroup (more colloquially called\n", "majority-minority districts).\n", "\n", "`Gingleator` is a subclass of the `SingleMetricOptimizer`. Technically this means that\n", "everything in the `Gingleator` class can also be done using the `SingleMetricOptimizer`\n", "class, but the `Gingleator` class provides some quality-of-life improvements to the user\n", "experience.\n", "\n", "For the purposes of this tutorial, we will be using the `Gingleator` class to try and optimize\n", "the number of districts with\n", "\n", "1. A majority-minority population for black population (mainly as a warm-up)\n", "2. A majority-minority Voting Age Population (VAP) for black voters\n", "\n", "In the provided JSON file (linked in the blue button above), we have included the following\n", "columns aggregated from the 2020 Census data on block groups for the state of Arkansas:\n", "\n", "- `tot_pop_20`: Total Population for the block group according to the 2020 Census\n", "- `tot_vap_20`: Total Voting Age Population (18+) for the block group according to the 2020 Census\n", "- `bpop_20`: Total Black Population for the block group according to the 2020 Census\n", "- `bvap_20`: Total Black Voting Age Population (18+) for the block group according to the 2020 Census\n", "\n", "> Note\n", ">\n", "> Both the `bpop_20` and `bvap_20` columns are \"any part black\" aggregations acquired\n", "> from the 2020 Census P1 and P3 tables. That is to say, `bpop_20` is the total number of\n", "> people that included \"black\" as any part of their racial identity when reporting to the\n", "> Census Bureau.\n", "\n", "Since the `Gingleator` class is a subclass of `SingleMetricOptimizer`, a lot of the\n", "setup for the optimization process is the same. The main things that we need to be careful of\n", "are the parameters we pass to the `Gingleator` class. Specifically, we need to make updaters\n", "that keep track of the relevant statistics we are optimizing for. In example (1) this means\n", "that we need an updater for the total population (we will just call this `population`) and\n", "the total black population (we will call this `bpop`).\n", "\n", "\n", "### Majority-Minority Total Population" ] }, { "cell_type": "code", "execution_count": null, "id": "fbd3ce77", "metadata": {}, "outputs": [], "source": [ "graph = Graph.from_json(\"05_bg_census_consolidated.json\")\n", "\n", "POPCOL = \"tot_pop_20\"\n", "SEN_DISTS = 35\n", "EPS = 0.02\n", "TOTPOP = sum(graph.node_data(node_id)[POPCOL] for node_id in graph.node_indices)\n", "\n", "chain_updaters = {\n", " \"population\": updaters.Tally(POPCOL, alias=\"population\"),\n", " \"bpop\": updaters.Tally(\"bpop_20\", alias=\"bpop\"),\n", "}\n", "\n", "initial_partition = Partition.from_random_assignment(\n", " graph=graph,\n", " n_parts=SEN_DISTS,\n", " epsilon=EPS,\n", " pop_col=POPCOL,\n", " updaters=chain_updaters,\n", " rng=rng,\n", ")\n", "\n", "proposal_fn = build_recom_proposal_fn(\n", " pop_col=POPCOL,\n", " pop_target=TOTPOP / SEN_DISTS,\n", " epsilon=EPS,\n", ")\n", "\n", "chain_constraints = constraints.within_percent_of_ideal_population(initial_partition, EPS)" ] }, { "cell_type": "markdown", "id": "994e242f", "metadata": {}, "source": [ "There are several parameters that we need to pass to the `Gingleator` class. Most of them\n", "should be familiar at this point, but the following are the most important ones for\n", "understanding appropriate usage of the class:\n", "\n", "\n", "**Population Parameters of the** `Gingleator` **class**\n", "\n", "There are three main population parameters that we can to pass to the `Gingleator` class.\n", "However, depending on which are passed, either one or two of them will be unnecessary.\n", "\n", "- (`minority_pop_col`, `total_pop_col`): This pair is passed when the user would like for the `Gingleator` class to compute the percentage of the minority population from quotient of these two updaters. The `total_pop_col` is the name of the **UPDATER** that contains the total population for each partition, and the `minority_pop_col` is the name of the **UPDATER** that contains total population for the minority population of interest. In the case that this pair of parameters is passed, the initialization function will create an updater for `minority_perc_col` via the formula `minority_pop_col / total_pop_col` for each partition, and the optimization function will then be passed the decimal values to compute the resulting partition's score for each step in the Markov chain.\n", "\n", "- The `minority_perc_col` is the name of the **UPDATER** that contains the percentage of the minority population of interest. The updater should already have the score for each part in the partition formatted as a percentage, so the optimization function will process these values as they are passed.\n", "\n", "**Score Function of the** `Gingleator` **class**\n", "\n", "The `score_fn` parameter is a function $f:P \\to \\mathbb{R}$ that\n", "take in a gerrychain `Partition` object and returns a score for that partition. The\n", "`SingleMetricOptimizer` class also allows for the modification of score functions, but\n", "the `Gingleator` class comes with some nice built-in score functions that are meant to\n", "to be used as good starting points for exploring the space of possible plans.\n", "\n", "Let $t$ be the threshold for the score as determined by the user, and let $n$\n", "be the number of districts in a partition $P$ with a minority percentage over the\n", "threshold value $t$, so $n = \\sum_{p_i \\in P} \\mathbb{1}_{p_i\\geq t}$ where\n", "$p_i$ is the percentage of the minority population in district $i$.\n", "\n", "- `num_opportunity_dists`: Given a `Partition`, this function will return $n$.\n", "\n", "- `reward_partial_dist`: Given a `Partition`, this function will return $n + \\max(\\{p_i : p_i < t\\})$.\n", "\n", "- `reward_next_highest_close`: Given a `Partition`, let $p_k = \\max(\\{p_i : p_i < t\\})$. This function will return $n$ if $p_k + 0.1 < t$ and $n + 10(p_k - t + 0.1)$ otherwise.\n", "\n", "- `penalize_maximum_over`: Given a `Partition`, this function will return 0 if $n = 0$ and $n + \\frac{1-\\max(\\{p_i\\})}{1-t}$ otherwise.\n", "\n", "- `penalize_avg_over`: Given a `Partition`, this function will return 0 if $n = 0$ and $n + \\frac{1-avg(\\{p_i: p_i \\geq t\\})}{1-t}$ otherwise.\n", "\n", "\n", "We are now prepared to instantiate the `Gingleator` class:" ] }, { "cell_type": "code", "execution_count": null, "id": "cd6fd465", "metadata": {}, "outputs": [], "source": [ "gingles = Gingleator(\n", " proposal_fn,\n", " chain_constraints,\n", " initial_partition,\n", " minority_pop_col=\"bpop\",\n", " total_pop_col=\"population\",\n", " score_fn=Gingleator.reward_partial_dist,\n", " rng=rng,\n", ")" ] }, { "cell_type": "markdown", "id": "dda335db", "metadata": {}, "source": [ "Since the `Gingleator` class is a subclass of the `SingleMetricOptimizer` class, we can\n", "use the same optimization methods as before:" ] }, { "cell_type": "code", "execution_count": null, "id": "b9916bc7", "metadata": {}, "outputs": [], "source": [ "total_steps = 5000\n", "\n", "# Short Bursts\n", "max_scores_sb = np.zeros(total_steps)\n", "scores_sb = np.zeros(total_steps)\n", "for i, part in enumerate(gingles.short_bursts(10, 500, with_progress_bar=False)):\n", " max_scores_sb[i] = gingles.best_score\n", " scores_sb[i] = gingles.score_fn(part)\n", "\n", "# Simulated Annealing\n", "max_scores_anneal = np.zeros(total_steps)\n", "scores_anneal = np.zeros(total_steps)\n", "for i, part in enumerate(\n", " gingles.simulated_annealing(\n", " total_steps,\n", " gingles.jumpcycle_beta_function(1000, 4000),\n", " beta_magnitude=500,\n", " with_progress_bar=False,\n", " )\n", "):\n", " max_scores_anneal[i] = gingles.best_score\n", " scores_anneal[i] = gingles.score_fn(part)\n", "\n", "# Tilted Runs\n", "max_scores_tilt = np.zeros(total_steps)\n", "scores_tilt = np.zeros(total_steps)\n", "for i, part in enumerate(gingles.tilted_run(total_steps, 0.125, with_progress_bar=False)):\n", " max_scores_tilt[i] = gingles.best_score\n", " scores_tilt[i] = gingles.score_fn(part)" ] }, { "cell_type": "markdown", "id": "1f942baa", "metadata": {}, "source": [ "And we can plot the results again:" ] }, { "cell_type": "code", "execution_count": null, "id": "44cd96aa", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(12, 6))\n", "plt.plot(max_scores_sb, label=\"Short Bursts\")\n", "plt.plot(max_scores_anneal, label=\"Simulated Annealing\")\n", "plt.plot(max_scores_tilt, label=\"Tilted Run\")\n", "plt.xlabel(\"Steps\", fontsize=20)\n", "plt.ylabel(\"Max Score Observered\", fontsize=20)\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "5f108731", "metadata": {}, "source": [ "Great! We have successfully used the `Gingleator` class to optimize the number of districts\n", "with a majority-minority population for black population. Now, we can do the same thing for\n", "for the majority-minority Voting Age Population (VAP) for black voters.\n", "\n", "### Majority-Minority Voting Age Population\n", "\n", "The astute reader will probably notice pretty quickly that there is a very easy way to modify\n", "the code from the previous section to optimize for Majority-Minority VAP. Indeed, all we need\n", "to do is change our updaters to be" ] }, { "cell_type": "code", "execution_count": null, "id": "085ecdd1", "metadata": {}, "outputs": [], "source": [ "chain_updaters = {\n", " \"population\": updaters.Tally(POPCOL, alias=\"population\"),\n", " \"vap\": updaters.Tally(\"tot_vap_20\", alias=\"vap\"),\n", " \"bvap\": updaters.Tally(\"bvap_20\", alias=\"bvap\"),\n", "}\n", "\n", "initial_partition = Partition.from_random_assignment(\n", " graph=graph,\n", " n_parts=SEN_DISTS,\n", " epsilon=EPS,\n", " pop_col=POPCOL,\n", " updaters=chain_updaters,\n", " rng=rng,\n", ")\n", "\n", "chain_constraints = constraints.within_percent_of_ideal_population(initial_partition, EPS)" ] }, { "cell_type": "markdown", "id": "8be6e594", "metadata": {}, "source": [ "and then change our `Gingleator` class instantiation to be" ] }, { "cell_type": "code", "execution_count": null, "id": "cb1fc25d", "metadata": {}, "outputs": [], "source": [ "gingles = Gingleator(\n", " proposal_fn,\n", " chain_constraints,\n", " initial_partition,\n", " minority_pop_col=\"bvap\",\n", " total_pop_col=\"vap\",\n", " score_fn=Gingleator.reward_partial_dist,\n", " rng=rng,\n", ")" ] }, { "cell_type": "markdown", "id": "5df1a166", "metadata": {}, "source": [ "and we will be off to the races. In the interest of being thorough, however, let us see how to\n", "modify this code to make use of the `minority_perc_col` parameter of the `Gingleator` class.\n", "For this, we will just need to tweak our updaters a little bit:" ] }, { "cell_type": "code", "execution_count": null, "id": "0893824f", "metadata": {}, "outputs": [], "source": [ "graph = Graph.from_json(\"05_bg_census_consolidated.json\")\n", "\n", "POPCOL = \"tot_pop_20\"\n", "SEN_DISTS = 35\n", "EPS = 0.02\n", "TOTPOP = sum(graph.node_data(node_id)[POPCOL] for node_id in graph.node_indices)\n", "\n", "\n", "# Updaters take in partitions and then return some value. In this case, we\n", "# want to return a dictionary of mapping each part in the partition to the value BVAP/VAP\n", "def compute_bvap_pct(partition):\n", " percent_by_part = {}\n", " for part in partition.parts:\n", " # bvap and vap are dictionaries mapping each partition part to\n", " # it corresponding BVAP or VAP tally respectively\n", " percent_by_part[part] = partition[\"bvap\"][part] / partition[\"vap\"][part]\n", " return percent_by_part\n", "\n", "\n", "chain_updaters = {\n", " \"population\": updaters.Tally(POPCOL, alias=\"population\"),\n", " \"vap\": updaters.Tally(\"tot_vap_20\", alias=\"vap\"),\n", " \"bvap\": updaters.Tally(\"bvap_20\", alias=\"bvap\"),\n", " \"bvap_pct\": compute_bvap_pct,\n", "}\n", "\n", "initial_partition = Partition.from_random_assignment(\n", " graph=graph,\n", " n_parts=SEN_DISTS,\n", " epsilon=EPS,\n", " pop_col=POPCOL,\n", " updaters=chain_updaters,\n", " rng=rng,\n", ")\n", "\n", "proposal_fn = build_recom_proposal_fn(\n", " pop_col=POPCOL,\n", " pop_target=TOTPOP / SEN_DISTS,\n", " epsilon=EPS,\n", ")\n", "\n", "chain_constraints = constraints.within_percent_of_ideal_population(initial_partition, EPS)" ] }, { "cell_type": "code", "execution_count": null, "id": "1d6a4db3", "metadata": {}, "outputs": [], "source": [ "gingles = Gingleator(\n", " proposal_fn,\n", " chain_constraints,\n", " initial_partition,\n", " minority_perc_col=\"bvap_pct\",\n", " score_fn=Gingleator.reward_partial_dist,\n", " rng=rng,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "be1c3c29", "metadata": {}, "outputs": [], "source": [ "total_steps = 5000\n", "\n", "# Short Bursts\n", "max_scores_sb = np.zeros(total_steps)\n", "scores_sb = np.zeros(total_steps)\n", "for i, part in enumerate(gingles.short_bursts(10, 500, with_progress_bar=False)):\n", " max_scores_sb[i] = gingles.best_score\n", " scores_sb[i] = gingles.score_fn(part)\n", "\n", "# Simulated Annealing\n", "max_scores_anneal = np.zeros(total_steps)\n", "scores_anneal = np.zeros(total_steps)\n", "for i, part in enumerate(\n", " gingles.simulated_annealing(\n", " total_steps,\n", " gingles.jumpcycle_beta_function(1000, 4000),\n", " beta_magnitude=500,\n", " with_progress_bar=False,\n", " )\n", "):\n", " max_scores_anneal[i] = gingles.best_score\n", " scores_anneal[i] = gingles.score_fn(part)\n", "\n", "# Tilted Runs\n", "max_scores_tilt = np.zeros(total_steps)\n", "scores_tilt = np.zeros(total_steps)\n", "for i, part in enumerate(gingles.tilted_run(total_steps, 0.125, with_progress_bar=False)):\n", " max_scores_tilt[i] = gingles.best_score\n", " scores_tilt[i] = gingles.score_fn(part)" ] }, { "cell_type": "markdown", "id": "7db195a9", "metadata": {}, "source": [ "And now we plot the results again!" ] }, { "cell_type": "code", "execution_count": null, "id": "d1ab27b3", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(12, 6))\n", "plt.plot(max_scores_sb, label=\"Short Bursts\")\n", "plt.plot(max_scores_anneal, label=\"Simulated Annealing\")\n", "plt.plot(max_scores_tilt, label=\"Tilted Run\")\n", "plt.xlabel(\"Steps\", fontsize=20)\n", "plt.ylabel(\"Max Score Observered\", fontsize=20)\n", "plt.legend()\n", "plt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }