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)
statisticvalue
strf64
"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)
statuscount
stru32
"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]: