run length and adapt vs. average risk adjustment¶
- how long does hawk/dove multi risk attitude take to converge with the new logic?
- how many do not converge ?
- what difference does it make if we use adapt or average risk adjustment strategy?
Most recent analysis based on data generated with this command:
./simulatingrisk/hawkdovemulti/batch_run.py --params risk_adjust
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.
In [218]:
import polars as pl
df = pl.read_csv("../../data/hawkdovemulti/riskadjust/dist-uniform/2025-08-05T153548_956082_model.csv")
In [219]:
total_runs = len(df)
print(f"Analyzing {total_runs} runs")
Analyzing 600 runs
simulation run length¶
They either finished very quickly (~50 steps) or never finished)
In [220]:
df["Step"].describe()
Out[220]:
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 |
In [221]:
df["Step"].plot.hist()
Out[221]:
In [222]:
# what about those that converged?
converged = df.filter(pl.col("status") == "converged")
In [223]:
converged["Step"].plot.hist()
Out[223]:
what % converged?¶
In [224]:
status_totals = df["status"].value_counts()
status_totals
Out[224]:
shape: (2, 2)
status | count |
---|---|
str | u32 |
"converged" | 481 |
"running" | 119 |
In [225]:
converg_total = status_totals.filter(status_totals["status"] == "converged")["count"][0]
print(f"{converg_total} runs out of {total_runs}; {converg_total/total_runs*100:.2f}% complete")
481 runs out of 600; 80.17% complete
risk adjustment (adopt / average)¶
hypothesis: adjustment strategy does not have a significant impact on the final result, only affects how long it takes to get there
In [226]:
from scipy import stats
df_riskadjust = converged.clone()
# TODO: make reusable functions for annotating data
for i in range(0, 10):
# calculate new series based on existing
pct_risk_category = df_riskadjust.select(pl.col(f"total_r{i}") / pl.col("total_agents"))
# add new column to the dataframe
df_riskadjust = df_riskadjust.with_columns(pl.Series(name=f"pct_r{i}", values=pct_risk_category))
df_riskadjust = df_riskadjust.with_columns(
pl.Series('pct_risk_inclined', values=df_riskadjust.select((pl.col("total_r0") + pl.col("total_r1") + pl.col("total_r2")) / pl.col("total_agents"))),
pl.Series('pct_risk_moderate', values=df_riskadjust.select((pl.col("total_r3") + pl.col("total_r4") + pl.col("total_r5") + pl.col("total_r6")) / pl.col("total_agents"))),
pl.Series('pct_risk_avoidant', values=df_riskadjust.select((pl.col("total_r7") + pl.col("total_r8") + pl.col("total_r9")) / pl.col("total_agents")))
)
df_riskadjust = df_riskadjust.with_columns(pl.Series('risk_attitude_mean', values=df_riskadjust.select(
(pl.col("total_r1") + pl.col("total_r2")*2 + pl.col("total_r3")*3 + pl.col("total_r4")*4 + pl.col("total_r5")*5 + pl.col("total_r6")*6 + pl.col("total_r7")*7 + pl.col("total_r8")*8 + pl.col("total_r9")*9)
/ pl.col("total_agents"))))
df_adopt = df_riskadjust.filter((pl.col("risk_adjustment") == "adopt"))
df_avg = df_riskadjust.filter((pl.col("risk_adjustment") == "average"))
print(f"adopt: {len(df_adopt):,} rows")
print(f"average: {len(df_avg):,} rows")
adopt: 251 rows average: 230 rows
In [246]:
maxlen = min(len(df_adopt), len(df_avg))
stats.ttest_rel(df_adopt.select("pct_risk_inclined")[:maxlen], df_avg.select("pct_risk_inclined")[:maxlen])
Out[246]:
TtestResult(statistic=array([0.30599364]), pvalue=array([0.75988749]), df=array([229]))
In [243]:
import altair as alt
alt.Chart(df_riskadjust).mark_boxplot().encode(
x=alt.X('pct_risk_inclined', title='% risk inclined'), y=alt.Y('risk_adjustment', title="Adjustment"))
Out[243]:
In [229]:
from simulatingrisk.hawkdovemulti import analysis_utils
# df_adopt, df_average
adopt_chart = analysis_utils.graph_population_risk_category(
analysis_utils.groupby_population_risk_category(df_adopt)
).properties(title="risk adjust: adopt")
average_chart = analysis_utils.graph_population_risk_category(
analysis_utils.groupby_population_risk_category(df_avg)
).properties(title="risk adjust: average")
(adopt_chart | average_chart).properties(title="distribution of population category by run").resolve_scale(y='shared')
Out[229]:
Simulations that converged¶
In [230]:
# filter to status = running
converged = converged.with_columns(
pct_agents_risk_changed=pl.col("num_agents_risk_changed").truediv(pl.col("total_agents")),
seven_pct_pop=pl.col("total_agents").mul(0.07)
)
alt.Chart(converged).mark_boxplot().encode(
x=alt.X('num_agents_risk_changed', title='# Agents that adjusted risk attitude'),
y=alt.Y('risk_adjustment', title='Adjustment')).facet('grid_size')
Out[230]:
In [231]:
alt.Chart(converged).mark_boxplot().encode(
x=alt.X('pct_agents_risk_changed', title='% of Agents that adjusted risk attitude'),
y=alt.Y('risk_adjustment', title='Adjustment')).facet('grid_size')
Out[231]:
In [232]:
converg_pop_boxplot = alt.Chart(converged).mark_boxplot().encode(
x=alt.X('sum_risk_level_changes', title='Total risk attitude changes'),
y=alt.Y('risk_adjustment', title='Adjustment'))
converg_pop_boxplot.facet('grid_size')
Out[232]:
In [233]:
converg_threshold = alt.Chart(converged).mark_point(color="orange").encode(
x="seven_pct_pop",
y=alt.Y('risk_adjustment', title='Adjustment')
)
(converg_pop_boxplot + converg_threshold).facet('grid_size')
Out[233]:
Simulations that did not converge¶
In [234]:
# filter to status = running
not_converged = df.filter(pl.col("status") == "running").with_columns(
pct_agents_risk_changed=pl.col("num_agents_risk_changed").truediv(pl.col("total_agents")),
seven_pct_pop=pl.col("total_agents").mul(0.07)
)
How many simulations with each adjustment type did not converge?
In [235]:
alt.Chart(not_converged).mark_bar().encode(
y=alt.Y('risk_adjustment', title="Adjustment"), x='count(RunId)')
Out[235]:
How many agents adjusted on the last adjustment round?
In [236]:
alt.Chart(not_converged).mark_boxplot().encode(
x=alt.X('num_agents_risk_changed', title='# Agents that adjusted risk attitude'),
y=alt.Y('risk_adjustment', title='Adjustment'))
Out[236]:
In [237]:
alt.Chart(not_converged).mark_boxplot().encode(
x=alt.X('pct_agents_risk_changed', title='% of Agents that adjusted risk attitude'),
y=alt.Y('risk_adjustment', title='Adjustment'))
Out[237]:
What about total population adjustments?
In [238]:
nonconverg_pop_boxplot = alt.Chart(not_converged).mark_boxplot().encode(
x=alt.X('sum_risk_level_changes', title='Total risk attitude changes'),
y=alt.Y('risk_adjustment', title='Adjustment'))
nonconverg_pop_boxplot
Out[238]:
Facet by grid size, and add a marker (orange circle) to indicate the threshold (based on 7% of total population size).
In [239]:
converg_threshold = alt.Chart(not_converged).mark_point(color="orange").encode(
x="seven_pct_pop",
y=alt.Y('risk_adjustment', title='Adjustment')
)
(nonconverg_pop_boxplot + converg_threshold).facet('grid_size')
Out[239]: