TEZv's picture
Format counts and add target population
ac4e07f
raw
history blame
5.2 kB
from __future__ import annotations
import pandas as pd
import plotly.express as px
import streamlit as st
from src.assumptions import BASELINE, DATA_QUALITY_NOTES
from src.model_pool import Criteria, estimate_pool, sensitivity_table
def format_count(value: int | float) -> str:
return f"{value:,.0f}"
def parse_count(value: str, fallback: int) -> int:
cleaned = value.replace(",", "").replace(" ", "").strip()
if not cleaned:
return fallback
return int(cleaned)
def title_label(value: str) -> str:
return value.replace("_", " ").title()
st.set_page_config(
page_title="Partner Pool Assumption Simulator",
page_icon="S7",
layout="wide",
)
st.title("Partner Pool Assumption Simulator")
st.caption("S7-K · Personal Relationship · transparent demo model")
st.info(
"Prototype status: current numbers are demo assumptions. Use this app to test model logic, "
"not to claim a factual count of available partners."
)
with st.sidebar:
st.header("Scenario")
base_population_text = st.text_input(
"Baseline population",
value=format_count(BASELINE.total_reference_population),
help=f"Reference population before filters, formatted with commas. Demo default: {format_count(BASELINE.total_reference_population)}.",
)
try:
base_population = parse_count(base_population_text, BASELINE.total_reference_population)
except ValueError:
st.warning("Use digits with optional commas, for example 10,000,000.")
base_population = BASELINE.total_reference_population
if not 10_000 <= base_population <= 50_000_000:
st.warning("Baseline population should stay between 10,000 and 50,000,000 for this demo.")
base_population = max(10_000, min(base_population, 50_000_000))
target_population = st.selectbox(
"Target population",
["all_adults", "women", "men"],
format_func=title_label,
help="Applies a demo sex-share coefficient before the other filters. Women: 53%, Men: 47%, All adults: 100%.",
)
age_min, age_max = st.slider(
"Age range",
18,
70,
(28, 42),
help="Narrows the pool by the selected age-band overlap.",
)
region_scope = st.selectbox(
"Region scope",
["all_ukraine", "large_cities", "kyiv_region", "western_regions"],
format_func=title_label,
help="Applies the selected regional scope coefficient.",
)
relationship_status = st.selectbox(
"Relationship status",
["any", "not_married", "single_or_divorced"],
format_func=title_label,
help="Demo availability proxy. Official marital status is not the same as real availability.",
)
min_height = st.slider(
"Minimum height, cm",
150,
205,
175,
help="Interpolates a demo height-distribution coefficient.",
)
income_level = st.selectbox(
"Income threshold",
["any", "above_median", "top_25", "top_10"],
format_func=title_label,
help="Applies an estimated income threshold coefficient.",
)
education_level = st.selectbox(
"Education filter",
["any", "higher_education", "graduate_plus"],
format_func=title_label,
help="Applies an estimated education-level coefficient.",
)
criteria = Criteria(
base_population=base_population,
target_population=target_population,
age_min=age_min,
age_max=age_max,
region_scope=region_scope,
relationship_status=relationship_status,
min_height_cm=min_height,
income_level=income_level,
education_level=education_level,
)
estimate = estimate_pool(criteria)
steps = sensitivity_table(criteria)
col_a, col_b, col_c = st.columns(3)
col_a.metric("Conservative estimate", format_count(estimate.conservative))
col_b.metric("Central estimate", format_count(estimate.central))
col_c.metric("Optimistic estimate", format_count(estimate.optimistic))
st.subheader("What narrows the pool")
step_df = pd.DataFrame(steps)
display_df = step_df.assign(
coefficient=step_df["coefficient"].map("{:.4f}".format),
remaining=step_df["remaining"].map(format_count),
)
fig = px.bar(
step_df,
x="factor",
y="remaining",
text="remaining",
custom_data=["coefficient"],
title="Remaining estimated pool after each criterion",
)
fig.update_traces(texttemplate="%{text:,.0f}", textposition="outside")
fig.update_traces(
hovertemplate="<b>%{x}</b><br>Remaining: %{y:,.0f}<br>Coefficient: %{customdata[0]:.4f}<extra></extra>"
)
fig.update_layout(yaxis_title="Estimated remaining pool", xaxis_title="")
st.plotly_chart(fig, use_container_width=True)
st.subheader("Scenario details")
st.dataframe(display_df, use_container_width=True, hide_index=True)
st.subheader("Data quality notes")
for note in DATA_QUALITY_NOTES:
st.write(f"- **{note['label']}**: {note['note']}")
st.subheader("Interpretation guardrails")
st.write(
"This model estimates a demographic scenario, not compatibility, attraction, safety, or relationship success. "
"A stricter filter can make a pool smaller, but it does not define a person's real-life chances."
)