def anticipatory_deviation(
incumbent_menu: tuple[Contract, ...],
entrant: Contract,
pi_high: float,
p_low: float,
p_high: float,
tolerance: float = 1e-9,
) -> dict[str, object]:
"""Applique le point fixe fini de retrait à une déviation."""
active_incumbents = list(incumbent_menu)
withdrawn: list[Contract] = []
type_data = ((1.0 - pi_high, p_low), (pi_high, p_high))
initial_menu = tuple(active_incumbents) + (entrant,)
initial_choices = tuple(
chosen_contract(initial_menu, probability) for _, probability in type_data
)
trace: list[dict[str, object]] = []
while True:
available = tuple(active_incumbents) + (entrant,)
choices = [chosen_contract(available, probability) for _, probability in type_data]
newly_withdrawn = []
for contract in active_incumbents:
contract_profit = sum(
mass * expected_profit(contract, probability)
for (mass, probability), choice in zip(type_data, choices)
if choice == contract
)
if contract in choices and contract_profit < -tolerance:
newly_withdrawn.append(contract)
trace.append({
"tour": len(trace),
"menu_actif": available,
"choix": tuple(choices),
"retraits": tuple(newly_withdrawn),
})
if not newly_withdrawn:
break
withdrawn.extend(newly_withdrawn)
active_incumbents = [
contract for contract in active_incumbents if contract not in newly_withdrawn
]
final_menu = tuple(active_incumbents) + (entrant,)
final_choices = tuple(
chosen_contract(final_menu, probability) for _, probability in type_data
)
entrant_profit = sum(
mass * expected_profit(entrant, probability)
for (mass, probability), choice in zip(type_data, final_choices)
if choice == entrant
)
return {
"entrant": entrant,
"initial_menu": initial_menu,
"initial_choices": initial_choices,
"withdrawn": tuple(withdrawn),
"final_menu": final_menu,
"final_choices": final_choices,
"trace": tuple(trace),
"entrant_profit": entrant_profit,
"profitable": entrant_profit > tolerance,
}
def anticipatory_menus(
contracts: tuple[Contract, ...],
pi_high: float,
p_low: float,
p_high: float,
max_menu_size: int = 2,
tolerance: float = 1e-9,
) -> list[dict[str, object]]:
"""Énumère les menus stables sous la règle finie documentée."""
retained: list[dict[str, object]] = []
for size in range(1, max_menu_size + 1):
for menu in combinations(contracts, size):
outcome = menu_outcome(menu, pi_high, p_low, p_high)
if outcome["aggregate_profit"] < -tolerance:
continue
reactions = [
anticipatory_deviation(menu, entrant, pi_high, p_low, p_high, tolerance)
for entrant in contracts
if entrant not in menu
]
if not any(reaction["profitable"] for reaction in reactions):
retained.append({"menu": menu, "reactions": reactions, **outcome})
return retained
small_grid = tuple(
Contract(coverage=coverage, premium=premium)
for coverage in (0.25, 0.50, 0.75)
for premium in (0.4, 0.8, 1.2, 1.6)
)
menus = anticipatory_menus(small_grid, pi_high, p_low, p_high)
menu_summary = pd.DataFrame([
{
"menu": [(c.coverage, c.premium) for c in item["menu"]],
"choix_L": (item["low_choice"].coverage, item["low_choice"].premium),
"choix_H": (item["high_choice"].coverage, item["high_choice"].premium),
"profit_agrege": item["aggregate_profit"],
"subvention_croisee": item["cross_subsidy"],
}
for item in menus
])
print(
f"Menus anticipatifs retenus : {len(menu_summary)} "
f"sur une grille finie de {len(small_grid)} contrats"
)
reaction_pool = [
anticipatory_deviation(menu, entrant, pi_high, p_low, p_high)
for menu in combinations(small_grid, 2)
for entrant in small_grid
if entrant not in menu
]
withdrawal_examples = [reaction for reaction in reaction_pool if reaction["withdrawn"]]
example_reaction = max(
withdrawal_examples or reaction_pool,
key=lambda reaction: (len(reaction["withdrawn"]), reaction["entrant_profit"]),
)
trace_frame = pd.DataFrame([
{
"tour": step["tour"],
"contrats_offerts": len(step["menu_actif"]),
"choix_L": (step["choix"][0].coverage, step["choix"][0].premium),
"choix_H": (step["choix"][1].coverage, step["choix"][1].premium),
"retraits": [(c.coverage, c.premium) for c in step["retraits"]],
}
for step in example_reaction["trace"]
])
print(
f"Exemple avant/après : {len(example_reaction['withdrawn'])} retrait(s), "
f"profit final entrant={example_reaction['entrant_profit']:.3f}"
)
display(trace_frame)
fig, axes = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True)
axes[0].scatter(
menu_summary["profit_agrege"] if not menu_summary.empty else [],
menu_summary["subvention_croisee"].astype(int) if not menu_summary.empty else [],
s=90,
alpha=0.75,
color="tab:purple",
)
axes[0].set(
xlabel="Profit agrégé du menu",
ylabel="Subvention croisée (0/1)",
yticks=[0, 1],
title="Menus retenus par la règle finie",
)
for contract in example_reaction["initial_menu"]:
axes[1].scatter(contract.coverage, contract.premium, color="tab:gray", s=80)
for contract in example_reaction["final_menu"]:
axes[1].scatter(contract.coverage, contract.premium, color="tab:green", marker="s", s=100)
for contract in example_reaction["withdrawn"]:
axes[1].scatter(contract.coverage, contract.premium, color="tab:red", marker="x", s=130, linewidths=2)
entrant = example_reaction["entrant"]
axes[1].scatter(entrant.coverage, entrant.premium, color="black", marker="*", s=180)
for label, choice in zip(("L", "H"), example_reaction["final_choices"]):
axes[1].annotate(label, (choice.coverage, choice.premium), xytext=(5, 5), textcoords="offset points", weight="bold")
axes[1].scatter([], [], color="tab:gray", label="Avant réaction")
axes[1].scatter([], [], color="tab:green", marker="s", label="Menu final")
axes[1].scatter([], [], color="tab:red", marker="x", label="Retiré")
axes[1].scatter([], [], color="black", marker="*", label="Entrant")
axes[1].set(xlabel="Couverture", ylabel="Prime", title="Entrée, retraits et choix finaux", xlim=(0.15, 0.85), ylim=(0.2, 1.8))
axes[1].legend(fontsize=8)
plt.show()
menu_summary.head(10)