Add expanded scenario filters
Browse files- app.py +130 -59
- src/assumptions.py +71 -0
- src/model_pool.py +27 -0
app.py
CHANGED
|
@@ -39,64 +39,125 @@ st.info(
|
|
| 39 |
|
| 40 |
with st.sidebar:
|
| 41 |
st.header("Scenario")
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
criteria = Criteria(
|
| 102 |
base_population=base_population,
|
|
@@ -108,6 +169,15 @@ criteria = Criteria(
|
|
| 108 |
min_height_cm=min_height,
|
| 109 |
income_level=income_level,
|
| 110 |
education_level=education_level,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
)
|
| 112 |
|
| 113 |
estimate = estimate_pool(criteria)
|
|
@@ -149,5 +219,6 @@ for note in DATA_QUALITY_NOTES:
|
|
| 149 |
st.subheader("Interpretation guardrails")
|
| 150 |
st.write(
|
| 151 |
"This model estimates a demographic scenario, not compatibility, attraction, safety, or relationship success. "
|
| 152 |
-
"A stricter filter can make a pool smaller, but it does not define a person's real-life chances."
|
|
|
|
| 153 |
)
|
|
|
|
| 39 |
|
| 40 |
with st.sidebar:
|
| 41 |
st.header("Scenario")
|
| 42 |
+
with st.expander("Core demographics", expanded=True):
|
| 43 |
+
base_population_text = st.text_input(
|
| 44 |
+
"Baseline population",
|
| 45 |
+
value=format_count(BASELINE.total_reference_population),
|
| 46 |
+
help=f"Reference population before filters, formatted with commas. Demo default: {format_count(BASELINE.total_reference_population)}.",
|
| 47 |
+
)
|
| 48 |
+
try:
|
| 49 |
+
base_population = parse_count(base_population_text, BASELINE.total_reference_population)
|
| 50 |
+
except ValueError:
|
| 51 |
+
st.warning("Use digits with optional commas, for example 10,000,000.")
|
| 52 |
+
base_population = BASELINE.total_reference_population
|
| 53 |
+
if not 10_000 <= base_population <= 50_000_000:
|
| 54 |
+
st.warning("Baseline population should stay between 10,000 and 50,000,000 for this demo.")
|
| 55 |
+
base_population = max(10_000, min(base_population, 50_000_000))
|
| 56 |
+
|
| 57 |
+
target_population = st.selectbox(
|
| 58 |
+
"Target population",
|
| 59 |
+
["all_adults", "women", "men"],
|
| 60 |
+
format_func=title_label,
|
| 61 |
+
help="Applies a demo sex-share coefficient before the other filters. Women: 53%, Men: 47%, All adults: 100%.",
|
| 62 |
+
)
|
| 63 |
+
age_min, age_max = st.slider(
|
| 64 |
+
"Age range",
|
| 65 |
+
18,
|
| 66 |
+
70,
|
| 67 |
+
(28, 42),
|
| 68 |
+
help="Narrows the pool by the selected age-band overlap.",
|
| 69 |
+
)
|
| 70 |
+
region_scope = st.selectbox(
|
| 71 |
+
"Region scope",
|
| 72 |
+
["all_ukraine", "large_cities", "kyiv_region", "western_regions"],
|
| 73 |
+
format_func=title_label,
|
| 74 |
+
help="Applies the selected regional scope coefficient.",
|
| 75 |
+
)
|
| 76 |
+
relationship_status = st.selectbox(
|
| 77 |
+
"Relationship status",
|
| 78 |
+
["any", "not_married", "single_or_divorced"],
|
| 79 |
+
format_func=title_label,
|
| 80 |
+
help="Demo availability proxy. Official marital status is not the same as real availability.",
|
| 81 |
+
)
|
| 82 |
+
min_height = st.slider(
|
| 83 |
+
"Minimum height, cm",
|
| 84 |
+
150,
|
| 85 |
+
205,
|
| 86 |
+
175,
|
| 87 |
+
help="Interpolates a demo height-distribution coefficient.",
|
| 88 |
+
)
|
| 89 |
+
income_level = st.selectbox(
|
| 90 |
+
"Income threshold",
|
| 91 |
+
["any", "above_median", "top_25", "top_10"],
|
| 92 |
+
format_func=title_label,
|
| 93 |
+
help="Applies an estimated income threshold coefficient.",
|
| 94 |
+
)
|
| 95 |
+
education_level = st.selectbox(
|
| 96 |
+
"Education filter",
|
| 97 |
+
["any", "higher_education", "graduate_plus"],
|
| 98 |
+
format_func=title_label,
|
| 99 |
+
help="Applies an estimated education-level coefficient.",
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
with st.expander("Family context"):
|
| 103 |
+
children_status = st.selectbox(
|
| 104 |
+
"Children status",
|
| 105 |
+
["any", "no_children", "has_children", "co_parenting_ready"],
|
| 106 |
+
format_func=title_label,
|
| 107 |
+
help="Scenario preference around existing children. These are demo assumptions, not value judgments.",
|
| 108 |
+
)
|
| 109 |
+
future_children = st.selectbox(
|
| 110 |
+
"Future children",
|
| 111 |
+
["any", "wants_children", "does_not_want_children", "open_or_undecided"],
|
| 112 |
+
format_func=title_label,
|
| 113 |
+
help="Scenario preference around future children.",
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
with st.expander("War and mobility"):
|
| 117 |
+
military_status = st.selectbox(
|
| 118 |
+
"Military status",
|
| 119 |
+
["any", "civilian_or_not_serving", "active_service", "veteran_or_service_history"],
|
| 120 |
+
format_func=title_label,
|
| 121 |
+
help="War-related scenario filter. Active service and veteran/service-history shares are placeholders until sourced.",
|
| 122 |
+
)
|
| 123 |
+
relocation = st.selectbox(
|
| 124 |
+
"Relocation",
|
| 125 |
+
["any", "same_city_only", "open_to_relocation", "remote_or_long_distance_ok"],
|
| 126 |
+
format_func=title_label,
|
| 127 |
+
help="Mobility and distance preference filter.",
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
with st.expander("Lifestyle and compatibility"):
|
| 131 |
+
housing = st.selectbox(
|
| 132 |
+
"Housing",
|
| 133 |
+
["any", "independent_living", "own_or_stable_housing"],
|
| 134 |
+
format_func=title_label,
|
| 135 |
+
help="Scenario proxy for independent or stable living setup.",
|
| 136 |
+
)
|
| 137 |
+
smoking = st.selectbox(
|
| 138 |
+
"Smoking",
|
| 139 |
+
["any", "non_smoker", "ok_with_smoking"],
|
| 140 |
+
format_func=title_label,
|
| 141 |
+
help="Lifestyle preference around smoking.",
|
| 142 |
+
)
|
| 143 |
+
alcohol = st.selectbox(
|
| 144 |
+
"Alcohol",
|
| 145 |
+
["any", "rare_or_none", "moderate_ok"],
|
| 146 |
+
format_func=title_label,
|
| 147 |
+
help="Lifestyle preference around alcohol use.",
|
| 148 |
+
)
|
| 149 |
+
language = st.selectbox(
|
| 150 |
+
"Language comfort",
|
| 151 |
+
["any", "ukrainian_comfortable", "english_comfortable", "ukrainian_and_english"],
|
| 152 |
+
format_func=title_label,
|
| 153 |
+
help="Communication comfort filter.",
|
| 154 |
+
)
|
| 155 |
+
pets = st.selectbox(
|
| 156 |
+
"Pets",
|
| 157 |
+
["any", "pet_friendly", "no_pets_preferred"],
|
| 158 |
+
format_func=title_label,
|
| 159 |
+
help="Household compatibility preference around pets.",
|
| 160 |
+
)
|
| 161 |
|
| 162 |
criteria = Criteria(
|
| 163 |
base_population=base_population,
|
|
|
|
| 169 |
min_height_cm=min_height,
|
| 170 |
income_level=income_level,
|
| 171 |
education_level=education_level,
|
| 172 |
+
children_status=children_status,
|
| 173 |
+
future_children=future_children,
|
| 174 |
+
military_status=military_status,
|
| 175 |
+
relocation=relocation,
|
| 176 |
+
housing=housing,
|
| 177 |
+
smoking=smoking,
|
| 178 |
+
alcohol=alcohol,
|
| 179 |
+
language=language,
|
| 180 |
+
pets=pets,
|
| 181 |
)
|
| 182 |
|
| 183 |
estimate = estimate_pool(criteria)
|
|
|
|
| 219 |
st.subheader("Interpretation guardrails")
|
| 220 |
st.write(
|
| 221 |
"This model estimates a demographic scenario, not compatibility, attraction, safety, or relationship success. "
|
| 222 |
+
"A stricter filter can make a pool smaller, but it does not define a person's real-life chances. "
|
| 223 |
+
"War, children, housing, and lifestyle filters are sensitive context variables; treat them as transparent assumptions."
|
| 224 |
)
|
src/assumptions.py
CHANGED
|
@@ -62,6 +62,65 @@ EDUCATION_FACTORS = {
|
|
| 62 |
"graduate_plus": 0.16,
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
DATA_QUALITY_NOTES = [
|
| 66 |
{
|
| 67 |
"label": "Population",
|
|
@@ -79,4 +138,16 @@ DATA_QUALITY_NOTES = [
|
|
| 79 |
"label": "Height",
|
| 80 |
"note": "Height currently requires proxy distribution unless a Ukraine-specific source is validated.",
|
| 81 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
]
|
|
|
|
| 62 |
"graduate_plus": 0.16,
|
| 63 |
}
|
| 64 |
|
| 65 |
+
CHILDREN_STATUS_FACTORS = {
|
| 66 |
+
"any": 1.0,
|
| 67 |
+
"no_children": 0.62,
|
| 68 |
+
"has_children": 0.31,
|
| 69 |
+
"co_parenting_ready": 0.18,
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
FUTURE_CHILDREN_FACTORS = {
|
| 73 |
+
"any": 1.0,
|
| 74 |
+
"wants_children": 0.48,
|
| 75 |
+
"does_not_want_children": 0.22,
|
| 76 |
+
"open_or_undecided": 0.58,
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
MILITARY_STATUS_FACTORS = {
|
| 80 |
+
"any": 1.0,
|
| 81 |
+
"civilian_or_not_serving": 0.91,
|
| 82 |
+
"active_service": 0.07,
|
| 83 |
+
"veteran_or_service_history": 0.15,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
RELOCATION_FACTORS = {
|
| 87 |
+
"any": 1.0,
|
| 88 |
+
"same_city_only": 0.22,
|
| 89 |
+
"open_to_relocation": 0.36,
|
| 90 |
+
"remote_or_long_distance_ok": 0.44,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
HOUSING_FACTORS = {
|
| 94 |
+
"any": 1.0,
|
| 95 |
+
"independent_living": 0.48,
|
| 96 |
+
"own_or_stable_housing": 0.29,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
SMOKING_FACTORS = {
|
| 100 |
+
"any": 1.0,
|
| 101 |
+
"non_smoker": 0.72,
|
| 102 |
+
"ok_with_smoking": 1.0,
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
ALCOHOL_FACTORS = {
|
| 106 |
+
"any": 1.0,
|
| 107 |
+
"rare_or_none": 0.46,
|
| 108 |
+
"moderate_ok": 0.76,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
LANGUAGE_FACTORS = {
|
| 112 |
+
"any": 1.0,
|
| 113 |
+
"ukrainian_comfortable": 0.82,
|
| 114 |
+
"english_comfortable": 0.38,
|
| 115 |
+
"ukrainian_and_english": 0.31,
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
PETS_FACTORS = {
|
| 119 |
+
"any": 1.0,
|
| 120 |
+
"pet_friendly": 0.54,
|
| 121 |
+
"no_pets_preferred": 0.42,
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
DATA_QUALITY_NOTES = [
|
| 125 |
{
|
| 126 |
"label": "Population",
|
|
|
|
| 138 |
"label": "Height",
|
| 139 |
"note": "Height currently requires proxy distribution unless a Ukraine-specific source is validated.",
|
| 140 |
},
|
| 141 |
+
{
|
| 142 |
+
"label": "Military status",
|
| 143 |
+
"note": "War-related filters are sensitive, time-changing, and should stay scenario-only until sourced.",
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"label": "Children",
|
| 147 |
+
"note": "Children and co-parenting filters are preference-context assumptions, not value judgments.",
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"label": "Independence",
|
| 151 |
+
"note": "Multiplying many filters assumes independence; use results as a stress test, not a factual census.",
|
| 152 |
+
},
|
| 153 |
]
|
src/model_pool.py
CHANGED
|
@@ -4,12 +4,21 @@ from dataclasses import dataclass
|
|
| 4 |
|
| 5 |
from .assumptions import (
|
| 6 |
AGE_BAND_FACTORS,
|
|
|
|
| 7 |
BASELINE,
|
|
|
|
| 8 |
EDUCATION_FACTORS,
|
|
|
|
| 9 |
HEIGHT_FACTORS,
|
|
|
|
| 10 |
INCOME_FACTORS,
|
|
|
|
|
|
|
|
|
|
| 11 |
REGION_FACTORS,
|
| 12 |
RELATIONSHIP_STATUS_FACTORS,
|
|
|
|
|
|
|
| 13 |
TARGET_POPULATION_FACTORS,
|
| 14 |
)
|
| 15 |
|
|
@@ -25,6 +34,15 @@ class Criteria:
|
|
| 25 |
min_height_cm: int
|
| 26 |
income_level: str
|
| 27 |
education_level: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
@dataclass(frozen=True)
|
|
@@ -78,6 +96,15 @@ def model_factors(criteria: Criteria) -> list[tuple[str, float]]:
|
|
| 78 |
("Minimum height", height_factor(criteria.min_height_cm)),
|
| 79 |
("Income threshold", INCOME_FACTORS[criteria.income_level]),
|
| 80 |
("Education filter", EDUCATION_FACTORS[criteria.education_level]),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
]
|
| 82 |
|
| 83 |
|
|
|
|
| 4 |
|
| 5 |
from .assumptions import (
|
| 6 |
AGE_BAND_FACTORS,
|
| 7 |
+
ALCOHOL_FACTORS,
|
| 8 |
BASELINE,
|
| 9 |
+
CHILDREN_STATUS_FACTORS,
|
| 10 |
EDUCATION_FACTORS,
|
| 11 |
+
FUTURE_CHILDREN_FACTORS,
|
| 12 |
HEIGHT_FACTORS,
|
| 13 |
+
HOUSING_FACTORS,
|
| 14 |
INCOME_FACTORS,
|
| 15 |
+
LANGUAGE_FACTORS,
|
| 16 |
+
MILITARY_STATUS_FACTORS,
|
| 17 |
+
PETS_FACTORS,
|
| 18 |
REGION_FACTORS,
|
| 19 |
RELATIONSHIP_STATUS_FACTORS,
|
| 20 |
+
RELOCATION_FACTORS,
|
| 21 |
+
SMOKING_FACTORS,
|
| 22 |
TARGET_POPULATION_FACTORS,
|
| 23 |
)
|
| 24 |
|
|
|
|
| 34 |
min_height_cm: int
|
| 35 |
income_level: str
|
| 36 |
education_level: str
|
| 37 |
+
children_status: str
|
| 38 |
+
future_children: str
|
| 39 |
+
military_status: str
|
| 40 |
+
relocation: str
|
| 41 |
+
housing: str
|
| 42 |
+
smoking: str
|
| 43 |
+
alcohol: str
|
| 44 |
+
language: str
|
| 45 |
+
pets: str
|
| 46 |
|
| 47 |
|
| 48 |
@dataclass(frozen=True)
|
|
|
|
| 96 |
("Minimum height", height_factor(criteria.min_height_cm)),
|
| 97 |
("Income threshold", INCOME_FACTORS[criteria.income_level]),
|
| 98 |
("Education filter", EDUCATION_FACTORS[criteria.education_level]),
|
| 99 |
+
("Children status", CHILDREN_STATUS_FACTORS[criteria.children_status]),
|
| 100 |
+
("Future children", FUTURE_CHILDREN_FACTORS[criteria.future_children]),
|
| 101 |
+
("Military status", MILITARY_STATUS_FACTORS[criteria.military_status]),
|
| 102 |
+
("Relocation", RELOCATION_FACTORS[criteria.relocation]),
|
| 103 |
+
("Housing", HOUSING_FACTORS[criteria.housing]),
|
| 104 |
+
("Smoking", SMOKING_FACTORS[criteria.smoking]),
|
| 105 |
+
("Alcohol", ALCOHOL_FACTORS[criteria.alcohol]),
|
| 106 |
+
("Language", LANGUAGE_FACTORS[criteria.language]),
|
| 107 |
+
("Pets", PETS_FACTORS[criteria.pets]),
|
| 108 |
]
|
| 109 |
|
| 110 |
|