diff --git a/docs/notebooks/hawkdovemulti-adjust/hdm_convergence_runlength.html b/docs/notebooks/hawkdovemulti-adjust/hdm_convergence_runlength.html
new file mode 100644
index 0000000..ea812d3
--- /dev/null
+++ b/docs/notebooks/hawkdovemulti-adjust/hdm_convergence_runlength.html
@@ -0,0 +1,9138 @@
+
+
+
+
📊Initial Risk Attitude
- Distributions & Population Outcomes
+ Distributions & Population Outcomes
Analysis of final population category and simulation run
@@ -177,7 +177,7 @@
📊 Available Notebook Snapshots
- ⏱️Convergence & run length
+ ⏱️Convergence & run length
Investigation of simulation run lengths and convergence.
@@ -187,6 +187,20 @@
📊 Available Notebook Snapshots
>
+
+
+ ⏱️Convergence & risk adjustment
+
+
+ Analysis of simulation run length, convergence, and risk adjustment strategy.
+
+
View Notebook
+
+
+
+
🔬Parameter Analysis
diff --git a/notebooks/new_convergence/hdm_convergence_runlength.ipynb b/notebooks/new_convergence/hdm_convergence_runlength.ipynb
index 0701bc6..448fb91 100644
--- a/notebooks/new_convergence/hdm_convergence_runlength.ipynb
+++ b/notebooks/new_convergence/hdm_convergence_runlength.ipynb
@@ -9,24 +9,34 @@
"\n",
"- how long does hawk/dove multi risk attitude take to converge with the new logic?\n",
" - how many do not converge ?\n",
- "- what difference does it make if we use adapt or average risk adjustment strategy?"
+ "- what difference does it make if we use adapt or average risk adjustment strategy?\n",
+ "\n",
+ "\n",
+ "--- \n",
+ "Most recent analysis based on data generated with this command:\n",
+ "\n",
+ "```sh\n",
+ "./simulatingrisk/hawkdovemulti/batch_run.py --params risk_adjust\n",
+ "```\n",
+ "\n",
+ "Ran some tests with more iterations; when 200 iterations were specified, the percent of simulations that converged were closer to 85%, but all batches tested were in the 80% range."
]
},
{
"cell_type": "code",
- "execution_count": 143,
+ "execution_count": 218,
"id": "4edfc5bc-ab4c-435b-8c0c-593d560a1445",
"metadata": {},
"outputs": [],
"source": [
"import polars as pl\n",
"\n",
- "df = pl.scan_csv(\"../../data/hawkdovemulti/riskadjust/dist-uniform/*.csv\").collect()"
+ "df = pl.read_csv(\"../../data/hawkdovemulti/riskadjust/dist-uniform/2025-08-05T153548_956082_model.csv\")"
]
},
{
"cell_type": "code",
- "execution_count": 144,
+ "execution_count": 219,
"id": "cc794dde-f8d5-4bc6-9eb5-059741196600",
"metadata": {},
"outputs": [
@@ -34,7 +44,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Analyzing 1000 runs\n"
+ "Analyzing 600 runs\n"
]
}
],
@@ -56,7 +66,7 @@
},
{
"cell_type": "code",
- "execution_count": 145,
+ "execution_count": 220,
"id": "eaa053a9-2567-41e7-9c8a-0bc76c4c05c2",
"metadata": {},
"outputs": [
@@ -70,28 +80,28 @@
" white-space: pre-wrap;\n",
"}\n",
"\n",
- "
shape: (9, 2)| statistic | value |
|---|
| str | f64 |
| "count" | 1000.0 |
| "null_count" | 0.0 |
| "mean" | 2904.7 |
| "std" | 2131.600767 |
| "min" | 60.0 |
| "25%" | 750.0 |
| "50%" | 3500.0 |
| "75%" | 5500.0 |
| "max" | 5500.0 |
"
+ "
shape: (9, 2)| statistic | value |
|---|
| str | f64 |
| "count" | 600.0 |
| "null_count" | 0.0 |
| "mean" | 278.398333 |
| "std" | 363.777872 |
| "min" | 50.0 |
| "25%" | 61.0 |
| "50%" | 101.0 |
| "75%" | 200.0 |
| "max" | 1000.0 |
"
],
"text/plain": [
"shape: (9, 2)\n",
- "┌────────────┬─────────────┐\n",
- "│ statistic ┆ value │\n",
- "│ --- ┆ --- │\n",
- "│ str ┆ f64 │\n",
- "╞════════════╪═════════════╡\n",
- "│ count ┆ 1000.0 │\n",
- "│ null_count ┆ 0.0 │\n",
- "│ mean ┆ 2904.7 │\n",
- "│ std ┆ 2131.600767 │\n",
- "│ min ┆ 60.0 │\n",
- "│ 25% ┆ 750.0 │\n",
- "│ 50% ┆ 3500.0 │\n",
- "│ 75% ┆ 5500.0 │\n",
- "│ max ┆ 5500.0 │\n",
- "└────────────┴─────────────┘"
+ "┌────────────┬────────────┐\n",
+ "│ statistic ┆ value │\n",
+ "│ --- ┆ --- │\n",
+ "│ str ┆ f64 │\n",
+ "╞════════════╪════════════╡\n",
+ "│ count ┆ 600.0 │\n",
+ "│ null_count ┆ 0.0 │\n",
+ "│ mean ┆ 278.398333 │\n",
+ "│ std ┆ 363.777872 │\n",
+ "│ min ┆ 50.0 │\n",
+ "│ 25% ┆ 61.0 │\n",
+ "│ 50% ┆ 101.0 │\n",
+ "│ 75% ┆ 200.0 │\n",
+ "│ max ┆ 1000.0 │\n",
+ "└────────────┴────────────┘"
]
},
- "execution_count": 145,
+ "execution_count": 220,
"metadata": {},
"output_type": "execute_result"
}
@@ -102,95 +112,88 @@
},
{
"cell_type": "code",
- "execution_count": 146,
+ "execution_count": 221,
"id": "31aa7cd7-a9ab-408a-9266-218238c79e01",
"metadata": {},
"outputs": [
- {
- "data": {},
- "metadata": {},
- "output_type": "display_data"
- },
{
"data": {
- "application/vnd.holoviews_exec.v0+json": "",
"text/html": [
- "
\n",
- ""
+ "\n",
+ "
\n",
+ ""
],
"text/plain": [
- ":Histogram [Step] (Step_count)"
+ "alt.Chart(...)"
]
},
- "execution_count": 146,
- "metadata": {
- "application/vnd.holoviews_exec.v0+json": {
- "id": "p2751"
- }
- },
+ "execution_count": 221,
+ "metadata": {},
"output_type": "execute_result"
}
],
@@ -200,7 +203,7 @@
},
{
"cell_type": "code",
- "execution_count": 147,
+ "execution_count": 222,
"id": "97a6df2a-d8dc-4f61-b356-a0e52571d93f",
"metadata": {},
"outputs": [],
@@ -212,95 +215,88 @@
},
{
"cell_type": "code",
- "execution_count": 148,
+ "execution_count": 223,
"id": "9371939e-2223-40b3-9989-4172a69741f6",
"metadata": {},
"outputs": [
- {
- "data": {},
- "metadata": {},
- "output_type": "display_data"
- },
{
"data": {
- "application/vnd.holoviews_exec.v0+json": "",
"text/html": [
- "
\n",
- ""
+ "\n",
+ "
\n",
+ ""
],
"text/plain": [
- ":Histogram [Step] (Step_count)"
+ "alt.Chart(...)"
]
},
- "execution_count": 148,
- "metadata": {
- "application/vnd.holoviews_exec.v0+json": {
- "id": "p2817"
- }
- },
+ "execution_count": 223,
+ "metadata": {},
"output_type": "execute_result"
}
],
@@ -318,7 +314,7 @@
},
{
"cell_type": "code",
- "execution_count": 149,
+ "execution_count": 224,
"id": "2a819ce2-eb8b-40bf-8a0c-7509989b4401",
"metadata": {},
"outputs": [
@@ -332,7 +328,7 @@
" white-space: pre-wrap;\n",
"}\n",
"\n",
- "
shape: (2, 2)| status | count |
|---|
| str | u32 |
| "running" | 858 |
| "converged" | 142 |
"
+ "
shape: (2, 2)| status | count |
|---|
| str | u32 |
| "converged" | 481 |
| "running" | 119 |
"
],
"text/plain": [
"shape: (2, 2)\n",
@@ -341,12 +337,12 @@
"│ --- ┆ --- │\n",
"│ str ┆ u32 │\n",
"╞═══════════╪═══════╡\n",
- "│ running ┆ 858 │\n",
- "│ converged ┆ 142 │\n",
+ "│ converged ┆ 481 │\n",
+ "│ running ┆ 119 │\n",
"└───────────┴───────┘"
]
},
- "execution_count": 149,
+ "execution_count": 224,
"metadata": {},
"output_type": "execute_result"
}
@@ -358,7 +354,7 @@
},
{
"cell_type": "code",
- "execution_count": 150,
+ "execution_count": 225,
"id": "0c121e06-1d76-4a26-8bd4-50ede7c558e3",
"metadata": {},
"outputs": [
@@ -366,7 +362,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "142 runs out of 1000; 14.20% complete\n"
+ "481 runs out of 600; 80.17% complete\n"
]
}
],
@@ -388,7 +384,7 @@
},
{
"cell_type": "code",
- "execution_count": 151,
+ "execution_count": 226,
"id": "1dfbf4b2-1525-4f00-9a83-93a49b72e085",
"metadata": {},
"outputs": [
@@ -396,8 +392,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "adopt: 140 rows\n",
- "average: 2 rows\n"
+ "adopt: 251 rows\n",
+ "average: 230 rows\n"
]
}
],
@@ -435,17 +431,17 @@
},
{
"cell_type": "code",
- "execution_count": 152,
+ "execution_count": 246,
"id": "484c7785-b067-4253-8a33-2c66c86a05ff",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "TtestResult(statistic=array([0.58333333]), pvalue=array([0.66381736]), df=array([1]))"
+ "TtestResult(statistic=array([0.30599364]), pvalue=array([0.75988749]), df=array([229]))"
]
},
- "execution_count": 152,
+ "execution_count": 246,
"metadata": {},
"output_type": "execute_result"
}
@@ -459,105 +455,101 @@
},
{
"cell_type": "code",
- "execution_count": 153,
+ "execution_count": 243,
"id": "3f420203-f683-40bf-8ffa-9767d6cbdc09",
"metadata": {},
"outputs": [
- {
- "data": {},
- "metadata": {},
- "output_type": "display_data"
- },
{
"data": {
- "application/vnd.holoviews_exec.v0+json": "",
"text/html": [
- "
\n",
- ""
+ "\n",
+ "
\n",
+ ""
],
"text/plain": [
- ":BoxWhisker [risk_adjustment] (Step)"
+ "alt.Chart(...)"
]
},
- "execution_count": 153,
- "metadata": {
- "application/vnd.holoviews_exec.v0+json": {
- "id": "p2883"
- }
- },
+ "execution_count": 243,
+ "metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "df_riskadjust.plot.box(\"Step\", by='risk_adjustment')"
+ "import altair as alt\n",
+ "\n",
+ "alt.Chart(df_riskadjust).mark_boxplot().encode(\n",
+ " x=alt.X('pct_risk_inclined', title='% risk inclined'), y=alt.Y('risk_adjustment', title=\"Adjustment\"))"
]
},
{
"cell_type": "code",
- "execution_count": 154,
+ "execution_count": 229,
"id": "2872f0c6-85aa-476a-8297-7a664266c42b",
"metadata": {},
"outputs": [
@@ -566,28 +558,29 @@
"text/html": [
"\n",
"\n",
- "
\n",
+ "
\n",
""
],
"text/plain": [
"alt.HConcatChart(...)"
]
},
- "execution_count": 154,
+ "execution_count": 229,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from simulatingrisk.hawkdovemulti import analysis_utils\n",
- "import importlib\n",
- "importlib.reload(analysis_utils)\n",
"\n",
"# df_adopt, df_average\n",
"\n",
@@ -659,169 +651,915 @@
]
},
{
- "cell_type": "code",
- "execution_count": 155,
- "id": "2242207f-0d44-492c-83ba-3c4f6beb892b",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "(858, 23)"
- ]
- },
- "execution_count": 155,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "not_converged = df.filter(pl.col(\"status\") == \"running\")\n",
- "not_converged.shape"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 158,
- "id": "c7937841-63b8-484d-9061-da3462d49c18",
+ "cell_type": "markdown",
+ "id": "3db0444d-cb97-452f-8170-cb3ed807d06a",
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "
\n",
- "
shape: (9, 2)| statistic | value |
|---|
| str | f64 |
| "count" | 858.0 |
| "null_count" | 0.0 |
| "mean" | 10.850816 |
| "std" | 7.569892 |
| "min" | 1.0 |
| "25%" | 4.0 |
| "50%" | 10.0 |
| "75%" | 16.0 |
| "max" | 35.0 |
"
- ],
- "text/plain": [
- "shape: (9, 2)\n",
- "┌────────────┬───────────┐\n",
- "│ statistic ┆ value │\n",
- "│ --- ┆ --- │\n",
- "│ str ┆ f64 │\n",
- "╞════════════╪═══════════╡\n",
- "│ count ┆ 858.0 │\n",
- "│ null_count ┆ 0.0 │\n",
- "│ mean ┆ 10.850816 │\n",
- "│ std ┆ 7.569892 │\n",
- "│ min ┆ 1.0 │\n",
- "│ 25% ┆ 4.0 │\n",
- "│ 50% ┆ 10.0 │\n",
- "│ 75% ┆ 16.0 │\n",
- "│ max ┆ 35.0 │\n",
- "└────────────┴───────────┘"
- ]
- },
- "execution_count": 158,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
"source": [
- "not_converged[\"num_agents_risk_changed\"].describe()"
+ "## Simulations that converged"
]
},
{
"cell_type": "code",
- "execution_count": 160,
- "id": "9dff6e0a-54f8-460a-bb7e-859d67017e04",
+ "execution_count": 230,
+ "id": "c2e98e75-6699-408b-a835-c127ad83c271",
"metadata": {},
"outputs": [
- {
- "data": {},
- "metadata": {},
- "output_type": "display_data"
- },
{
"data": {
- "application/vnd.holoviews_exec.v0+json": "",
"text/html": [
- "
\n",
- ""
- ],
- "text/plain": [
- ":BoxWhisker (num_agents_risk_changed)"
- ]
- },
- "execution_count": 160,
- "metadata": {
- "application/vnd.holoviews_exec.v0+json": {
- "id": "p3032"
- }
- },
- "output_type": "execute_result"
- }
- ],
- "source": [
- "not_converged.plot.box(\"num_agents_risk_changed\")"
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.FacetChart(...)"
+ ]
+ },
+ "execution_count": 230,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# filter to status = running\n",
+ "converged = converged.with_columns(\n",
+ " pct_agents_risk_changed=pl.col(\"num_agents_risk_changed\").truediv(pl.col(\"total_agents\")),\n",
+ " seven_pct_pop=pl.col(\"total_agents\").mul(0.07)\n",
+ ")\n",
+ "\n",
+ "alt.Chart(converged).mark_boxplot().encode(\n",
+ " x=alt.X('num_agents_risk_changed', title='# Agents that adjusted risk attitude'), \n",
+ " y=alt.Y('risk_adjustment', title='Adjustment')).facet('grid_size')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 231,
+ "id": "5e49a7c6-6e54-457e-a087-481364051f32",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.FacetChart(...)"
+ ]
+ },
+ "execution_count": 231,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "alt.Chart(converged).mark_boxplot().encode(\n",
+ " x=alt.X('pct_agents_risk_changed', title='% of Agents that adjusted risk attitude'), \n",
+ " y=alt.Y('risk_adjustment', title='Adjustment')).facet('grid_size')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 232,
+ "id": "b7d48b2f-1365-4132-9641-09c0998af4e3",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.FacetChart(...)"
+ ]
+ },
+ "execution_count": 232,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "converg_pop_boxplot = alt.Chart(converged).mark_boxplot().encode(\n",
+ " x=alt.X('sum_risk_level_changes', title='Total risk attitude changes'), \n",
+ " y=alt.Y('risk_adjustment', title='Adjustment'))\n",
+ "\n",
+ "converg_pop_boxplot.facet('grid_size')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 233,
+ "id": "d12da2d9-d673-416a-bdee-9a6c9ccfdf65",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.FacetChart(...)"
+ ]
+ },
+ "execution_count": 233,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "converg_threshold = alt.Chart(converged).mark_point(color=\"orange\").encode(\n",
+ " x=\"seven_pct_pop\",\n",
+ " y=alt.Y('risk_adjustment', title='Adjustment')\n",
+ ")\n",
+ "(converg_pop_boxplot + converg_threshold).facet('grid_size')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8e15e456-cc28-45d4-8c6e-b9ef41e9c9bb",
+ "metadata": {},
+ "source": [
+ "## Simulations that did not converge"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 234,
+ "id": "2242207f-0d44-492c-83ba-3c4f6beb892b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# filter to status = running\n",
+ "not_converged = df.filter(pl.col(\"status\") == \"running\").with_columns(\n",
+ " pct_agents_risk_changed=pl.col(\"num_agents_risk_changed\").truediv(pl.col(\"total_agents\")),\n",
+ " seven_pct_pop=pl.col(\"total_agents\").mul(0.07)\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1ce0aed4-4c32-454b-b0b8-6d3b02d034cb",
+ "metadata": {},
+ "source": [
+ "How many simulations with each adjustment type did not converge?\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 235,
+ "id": "1d2b6726-e1b6-45a9-ab09-24cba63365e5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.Chart(...)"
+ ]
+ },
+ "execution_count": 235,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "alt.Chart(not_converged).mark_bar().encode(\n",
+ " y=alt.Y('risk_adjustment', title=\"Adjustment\"), x='count(RunId)')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "381b999a-e8c7-46c9-8daf-2586f056d932",
+ "metadata": {},
+ "source": [
+ "How many agents adjusted on the last adjustment round?"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 236,
+ "id": "9dff6e0a-54f8-460a-bb7e-859d67017e04",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.Chart(...)"
+ ]
+ },
+ "execution_count": 236,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "alt.Chart(not_converged).mark_boxplot().encode(\n",
+ " x=alt.X('num_agents_risk_changed', title='# Agents that adjusted risk attitude'), \n",
+ " y=alt.Y('risk_adjustment', title='Adjustment'))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 237,
+ "id": "e5fb0fe1-90c8-4558-b647-d142ecb5b8d9",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.Chart(...)"
+ ]
+ },
+ "execution_count": 237,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "alt.Chart(not_converged).mark_boxplot().encode(\n",
+ " x=alt.X('pct_agents_risk_changed', title='% of Agents that adjusted risk attitude'), \n",
+ " y=alt.Y('risk_adjustment', title='Adjustment'))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "85be9ac6-0e11-4866-882a-bb60a1afe566",
+ "metadata": {},
+ "source": [
+ "What about total population adjustments?\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 238,
+ "id": "c1cb6d78-b5df-40a3-89e3-23376bff3e32",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.Chart(...)"
+ ]
+ },
+ "execution_count": 238,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "nonconverg_pop_boxplot = alt.Chart(not_converged).mark_boxplot().encode(\n",
+ " x=alt.X('sum_risk_level_changes', title='Total risk attitude changes'), \n",
+ " y=alt.Y('risk_adjustment', title='Adjustment'))\n",
+ "\n",
+ "nonconverg_pop_boxplot"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "200fee55-2d84-4fc9-aed9-d1599176e332",
+ "metadata": {},
+ "source": [
+ "Facet by grid size, and add a marker (orange circle) to indicate the threshold (based on 7% of total population size)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 239,
+ "id": "60667865-a6e1-46c5-8ed2-dcf58fcfd129",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ ""
+ ],
+ "text/plain": [
+ "alt.FacetChart(...)"
+ ]
+ },
+ "execution_count": 239,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "converg_threshold = alt.Chart(not_converged).mark_point(color=\"orange\").encode(\n",
+ " x=\"seven_pct_pop\",\n",
+ " y=alt.Y('risk_adjustment', title='Adjustment')\n",
+ ")\n",
+ "(nonconverg_pop_boxplot + converg_threshold).facet('grid_size')"
]
}
],
@@ -841,7 +1579,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.12.1"
+ "version": "3.12.7"
}
},
"nbformat": 4,
diff --git a/simulatingrisk/hawkdovemulti/README.md b/simulatingrisk/hawkdovemulti/README.md
index a6bac8f..4115c83 100644
--- a/simulatingrisk/hawkdovemulti/README.md
+++ b/simulatingrisk/hawkdovemulti/README.md
@@ -23,16 +23,31 @@ instead of choosing based on the rules of the game.
## Convergence
The model is configured to stop automatically when it has stabilized.
-Convergence is reached when an adjustment round occurs and zero agents
-adjust their risk attitude.
+
+Model and agent data collection reports on whether agents updated their
+risk level in the last adjustment round, and model data collection
+includes a status of "running" or "converged".
+
+### With Adjustment
+
+When adjustment is enabled (adopt / average), convergence is checked
+after the simulation has run for a minimum of 50 rounds. The simulation
+is considered stable when individual agents are no longer adjusting risk attitudes,
+or when the number of agents in each risk attitude are relatively stable
+(e.g., agents are swapping risk attitudes but the overall total for each
+category is stable).
+
+Convergence is reached when an adjustment round occurs and _either_:
+
+- _zero_ agents adjust their risk attitude
+- the total changes of agents per risk attitudes is less than 7% of the population size
+
+### Without Adjustment
If adjustment is not enabled, convergence logic falls back to the
-implementation of the hawk/dove single-risk attitude simulation, which is
-based on a stable rolling % average of agents playing hawk.
+implementation of the hawk/dove single-risk attitude simulation. In this case,
+convergence is based on a stable rolling % average of agents playing hawk.
-Model and agent data collection also includes reports on whether agents
-updated their risk level in the last adjustment round, and model data collection
-includes a status of "running" or "converged".
## Batch running
diff --git a/simulatingrisk/hawkdovemulti/analysis_utils.py b/simulatingrisk/hawkdovemulti/analysis_utils.py
index fafe12e..618be1e 100644
--- a/simulatingrisk/hawkdovemulti/analysis_utils.py
+++ b/simulatingrisk/hawkdovemulti/analysis_utils.py
@@ -1,6 +1,7 @@
"""
utility methods for analyzing data collected generated by this model
"""
+
import altair as alt
import polars as pl
@@ -25,7 +26,9 @@ def groupby_population_risk_category(df):
poprisk_grouped = poprisk_grouped.with_columns(
pl.Series(
name="type",
- values=poprisk_grouped["risk_category"].map_elements(RiskState.category),
+ values=poprisk_grouped["risk_category"].map_elements(
+ RiskState.category, return_type=pl.datatypes.String
+ ),
)
)
return poprisk_grouped
diff --git a/simulatingrisk/hawkdovemulti/batch_run.py b/simulatingrisk/hawkdovemulti/batch_run.py
index 6ae0a46..0082b29 100755
--- a/simulatingrisk/hawkdovemulti/batch_run.py
+++ b/simulatingrisk/hawkdovemulti/batch_run.py
@@ -2,17 +2,15 @@
import argparse
import csv
-from datetime import datetime
import multiprocessing
import os
+from datetime import datetime
+from mesa.batchrunner import _collect_data, _make_model_kwargs
from tqdm.auto import tqdm
-from mesa.batchrunner import _make_model_kwargs, _collect_data
-
from simulatingrisk.hawkdovemulti.model import HawkDoveMultipleRiskModel
-
neighborhood_sizes = list(HawkDoveMultipleRiskModel.neighborhood_sizes)
# NOTE: it's better to be explicit about even parameters
@@ -40,7 +38,7 @@
"risk_adjustment": ["adopt", "average"],
"risk_distribution": "uniform",
# use model defaults; grid size must be specified
- "grid_size": 10, # 25,
+ "grid_size": [5, 10, 25],
},
"payoff": {
"adjust_payoff": HawkDoveMultipleRiskModel.supported_adjust_payoffs,
diff --git a/simulatingrisk/hawkdovemulti/model.py b/simulatingrisk/hawkdovemulti/model.py
index 83ae5ec..3003704 100644
--- a/simulatingrisk/hawkdovemulti/model.py
+++ b/simulatingrisk/hawkdovemulti/model.py
@@ -3,8 +3,7 @@
from enum import IntEnum
from functools import cached_property
-
-from simulatingrisk.hawkdove.model import HawkDoveModel, HawkDoveAgent
+from simulatingrisk.hawkdove.model import HawkDoveAgent, HawkDoveModel
class HawkDoveMultipleRiskAgent(HawkDoveAgent):
@@ -193,7 +192,7 @@ def __init__(
if risk_distribution not in self.risk_distribution_options:
raise ValueError(
f"Unsupported risk distribution '{risk_distribution}'; "
- + f"must be one of { ', '.join(self.risk_distribution_options) }"
+ + f"must be one of {', '.join(self.risk_distribution_options)}"
)
# make sure risk adjustment is valid
@@ -350,10 +349,10 @@ def converged(self):
# this simulation typically takes around 1000 rounds to converge,
# so don't even bother checking until at least 50 rounds
- return (
- self.schedule.steps > max(self.adjust_round_n, 50)
- # TODO: determine value (% of population?) and/or make configurable
- and (self.num_agents_risk_changed == 0 or self.sum_risk_level_changes == 6)
+ return self.schedule.steps > max(self.adjust_round_n, 50) and (
+ self.num_agents_risk_changed == 0
+ # NOTE: could adjust the threshold here
+ or self.sum_risk_level_changes <= len(self.schedule.agents) * 0.07
)
@cached_property
@@ -376,8 +375,8 @@ def sum_risk_level_changes(self):
# for each risk level, calculate the absolute difference
for rlevel, total in a.items():
changes[rlevel] = abs(total - b[rlevel])
+
return sum([val for val in changes.values()])
- # return sum([abs(a[rlevel] - b[rlevel]) for rlevel in a.keys()])
def __getattr__(self, attr):
# support dynamic properties for data collection on total by risk level