Replace income categories with salary slider
Browse files- app.py +16 -14
- src/assumptions.py +12 -13
- src/model_pool.py +19 -3
app.py
CHANGED
|
@@ -8,7 +8,7 @@ from src.assumptions import (
|
|
| 8 |
BASELINE,
|
| 9 |
BASELINE_REFERENCE_OPTIONS,
|
| 10 |
DATA_QUALITY_NOTES,
|
| 11 |
-
|
| 12 |
SALARY_ANCHORS_UAH,
|
| 13 |
SOURCE_LINKS,
|
| 14 |
)
|
|
@@ -30,10 +30,6 @@ def title_label(value: str) -> str:
|
|
| 30 |
return value.replace("_", " ").title()
|
| 31 |
|
| 32 |
|
| 33 |
-
def income_label(value: str) -> str:
|
| 34 |
-
return INCOME_THRESHOLD_LABELS[value]
|
| 35 |
-
|
| 36 |
-
|
| 37 |
def format_percent(value: int | float) -> str:
|
| 38 |
if value == 0:
|
| 39 |
return "0%"
|
|
@@ -122,17 +118,23 @@ with st.sidebar:
|
|
| 122 |
175,
|
| 123 |
help="Interpolates a demo height-distribution coefficient.",
|
| 124 |
)
|
| 125 |
-
|
| 126 |
-
"
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
help=(
|
| 130 |
-
"Salary anchors: Work.ua current benchmark is about "
|
| 131 |
f"{format_count(SALARY_ANCHORS_UAH['workua_current_average'])} UAH/month; "
|
| 132 |
f"KSE cites Work.ua January 2026 median at {format_count(SALARY_ANCHORS_UAH['kse_workua_jan_2026_median'])} UAH/month. "
|
| 133 |
-
"
|
| 134 |
),
|
| 135 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
education_level = st.selectbox(
|
| 137 |
"Education filter",
|
| 138 |
["any", "higher_education", "graduate_plus"],
|
|
@@ -208,7 +210,7 @@ criteria = Criteria(
|
|
| 208 |
region_scope=region_scope,
|
| 209 |
relationship_status=relationship_status,
|
| 210 |
min_height_cm=min_height,
|
| 211 |
-
|
| 212 |
education_level=education_level,
|
| 213 |
children_status=children_status,
|
| 214 |
future_children=future_children,
|
|
@@ -268,8 +270,8 @@ st.write(
|
|
| 268 |
f"({format_count(BASELINE_REFERENCE_OPTIONS['sssu_jan_2022_total']['value'])})."
|
| 269 |
)
|
| 270 |
st.write(
|
| 271 |
-
f"
|
| 272 |
-
"
|
| 273 |
)
|
| 274 |
|
| 275 |
st.subheader("Data quality notes")
|
|
|
|
| 8 |
BASELINE,
|
| 9 |
BASELINE_REFERENCE_OPTIONS,
|
| 10 |
DATA_QUALITY_NOTES,
|
| 11 |
+
INCOME_SLIDER_MAX_UAH,
|
| 12 |
SALARY_ANCHORS_UAH,
|
| 13 |
SOURCE_LINKS,
|
| 14 |
)
|
|
|
|
| 30 |
return value.replace("_", " ").title()
|
| 31 |
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
def format_percent(value: int | float) -> str:
|
| 34 |
if value == 0:
|
| 35 |
return "0%"
|
|
|
|
| 118 |
175,
|
| 119 |
help="Interpolates a demo height-distribution coefficient.",
|
| 120 |
)
|
| 121 |
+
income_min_uah = st.slider(
|
| 122 |
+
"Minimum monthly income, UAH",
|
| 123 |
+
min_value=0,
|
| 124 |
+
max_value=INCOME_SLIDER_MAX_UAH,
|
| 125 |
+
value=30_000,
|
| 126 |
+
step=5_000,
|
| 127 |
help=(
|
| 128 |
+
"Scenario salary threshold. 0 means no income filter. Salary anchors: Work.ua current benchmark is about "
|
| 129 |
f"{format_count(SALARY_ANCHORS_UAH['workua_current_average'])} UAH/month; "
|
| 130 |
f"KSE cites Work.ua January 2026 median at {format_count(SALARY_ANCHORS_UAH['kse_workua_jan_2026_median'])} UAH/month. "
|
| 131 |
+
"Very high thresholds such as 200,000 or 500,000 UAH/month use demo tail assumptions, not official percentiles."
|
| 132 |
),
|
| 133 |
)
|
| 134 |
+
st.caption(
|
| 135 |
+
"Selected income threshold: "
|
| 136 |
+
f"{'Any income' if income_min_uah == 0 else format_count(income_min_uah) + ' UAH/month'}"
|
| 137 |
+
)
|
| 138 |
education_level = st.selectbox(
|
| 139 |
"Education filter",
|
| 140 |
["any", "higher_education", "graduate_plus"],
|
|
|
|
| 210 |
region_scope=region_scope,
|
| 211 |
relationship_status=relationship_status,
|
| 212 |
min_height_cm=min_height,
|
| 213 |
+
income_min_uah=income_min_uah,
|
| 214 |
education_level=education_level,
|
| 215 |
children_status=children_status,
|
| 216 |
future_children=future_children,
|
|
|
|
| 270 |
f"({format_count(BASELINE_REFERENCE_OPTIONS['sssu_jan_2022_total']['value'])})."
|
| 271 |
)
|
| 272 |
st.write(
|
| 273 |
+
f"The income slider uses {format_count(SALARY_ANCHORS_UAH['workua_current_average'])} UAH/month as the current public job-market benchmark. "
|
| 274 |
+
"High values such as 200,000 or 500,000 UAH/month are supported as scenario stress-test cutoffs, not official salary percentiles."
|
| 275 |
)
|
| 276 |
|
| 277 |
st.subheader("Data quality notes")
|
src/assumptions.py
CHANGED
|
@@ -36,12 +36,18 @@ SALARY_ANCHORS_UAH = {
|
|
| 36 |
"workua_current_average": 28_600,
|
| 37 |
}
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
"
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
SOURCE_LINKS = [
|
| 47 |
{
|
|
@@ -108,13 +114,6 @@ HEIGHT_FACTORS = {
|
|
| 108 |
190: 0.04,
|
| 109 |
}
|
| 110 |
|
| 111 |
-
INCOME_FACTORS = {
|
| 112 |
-
"any": 1.0,
|
| 113 |
-
"above_median": 0.42,
|
| 114 |
-
"top_25": 0.25,
|
| 115 |
-
"top_10": 0.10,
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
EDUCATION_FACTORS = {
|
| 119 |
"any": 1.0,
|
| 120 |
"higher_education": 0.38,
|
|
|
|
| 36 |
"workua_current_average": 28_600,
|
| 37 |
}
|
| 38 |
|
| 39 |
+
INCOME_SLIDER_MAX_UAH = 500_000
|
| 40 |
+
|
| 41 |
+
INCOME_CURVE_POINTS_UAH = [
|
| 42 |
+
(0, 1.0),
|
| 43 |
+
(SALARY_ANCHORS_UAH["official_pfu_2025_average"], 0.55),
|
| 44 |
+
(SALARY_ANCHORS_UAH["workua_current_average"], 0.42),
|
| 45 |
+
(45_000, 0.25),
|
| 46 |
+
(70_000, 0.10),
|
| 47 |
+
(100_000, 0.055),
|
| 48 |
+
(200_000, 0.015),
|
| 49 |
+
(500_000, 0.002),
|
| 50 |
+
]
|
| 51 |
|
| 52 |
SOURCE_LINKS = [
|
| 53 |
{
|
|
|
|
| 114 |
190: 0.04,
|
| 115 |
}
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
EDUCATION_FACTORS = {
|
| 118 |
"any": 1.0,
|
| 119 |
"higher_education": 0.38,
|
src/model_pool.py
CHANGED
|
@@ -11,7 +11,7 @@ from .assumptions import (
|
|
| 11 |
FUTURE_CHILDREN_FACTORS,
|
| 12 |
HEIGHT_FACTORS,
|
| 13 |
HOUSING_FACTORS,
|
| 14 |
-
|
| 15 |
LANGUAGE_FACTORS,
|
| 16 |
MILITARY_STATUS_FACTORS,
|
| 17 |
PETS_FACTORS,
|
|
@@ -32,7 +32,7 @@ class Criteria:
|
|
| 32 |
region_scope: str
|
| 33 |
relationship_status: str
|
| 34 |
min_height_cm: int
|
| 35 |
-
|
| 36 |
education_level: str
|
| 37 |
children_status: str
|
| 38 |
future_children: str
|
|
@@ -87,6 +87,22 @@ def height_factor(min_height_cm: int) -> float:
|
|
| 87 |
return HEIGHT_FACTORS[lower] + ratio * (HEIGHT_FACTORS[upper] - HEIGHT_FACTORS[lower])
|
| 88 |
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
def model_factors(criteria: Criteria) -> list[tuple[str, float]]:
|
| 91 |
return [
|
| 92 |
("Target population", TARGET_POPULATION_FACTORS[criteria.target_population]),
|
|
@@ -94,7 +110,7 @@ def model_factors(criteria: Criteria) -> list[tuple[str, float]]:
|
|
| 94 |
("Region scope", REGION_FACTORS[criteria.region_scope]),
|
| 95 |
("Relationship status", RELATIONSHIP_STATUS_FACTORS[criteria.relationship_status]),
|
| 96 |
("Minimum height", height_factor(criteria.min_height_cm)),
|
| 97 |
-
("
|
| 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]),
|
|
|
|
| 11 |
FUTURE_CHILDREN_FACTORS,
|
| 12 |
HEIGHT_FACTORS,
|
| 13 |
HOUSING_FACTORS,
|
| 14 |
+
INCOME_CURVE_POINTS_UAH,
|
| 15 |
LANGUAGE_FACTORS,
|
| 16 |
MILITARY_STATUS_FACTORS,
|
| 17 |
PETS_FACTORS,
|
|
|
|
| 32 |
region_scope: str
|
| 33 |
relationship_status: str
|
| 34 |
min_height_cm: int
|
| 35 |
+
income_min_uah: int
|
| 36 |
education_level: str
|
| 37 |
children_status: str
|
| 38 |
future_children: str
|
|
|
|
| 87 |
return HEIGHT_FACTORS[lower] + ratio * (HEIGHT_FACTORS[upper] - HEIGHT_FACTORS[lower])
|
| 88 |
|
| 89 |
|
| 90 |
+
def income_factor(income_min_uah: int) -> float:
|
| 91 |
+
points = sorted(INCOME_CURVE_POINTS_UAH)
|
| 92 |
+
if income_min_uah <= points[0][0]:
|
| 93 |
+
return points[0][1]
|
| 94 |
+
if income_min_uah >= points[-1][0]:
|
| 95 |
+
return points[-1][1]
|
| 96 |
+
|
| 97 |
+
lower = max(point for point in points if point[0] <= income_min_uah)
|
| 98 |
+
upper = min(point for point in points if point[0] >= income_min_uah)
|
| 99 |
+
if lower == upper:
|
| 100 |
+
return lower[1]
|
| 101 |
+
|
| 102 |
+
ratio = (income_min_uah - lower[0]) / (upper[0] - lower[0])
|
| 103 |
+
return lower[1] + ratio * (upper[1] - lower[1])
|
| 104 |
+
|
| 105 |
+
|
| 106 |
def model_factors(criteria: Criteria) -> list[tuple[str, float]]:
|
| 107 |
return [
|
| 108 |
("Target population", TARGET_POPULATION_FACTORS[criteria.target_population]),
|
|
|
|
| 110 |
("Region scope", REGION_FACTORS[criteria.region_scope]),
|
| 111 |
("Relationship status", RELATIONSHIP_STATUS_FACTORS[criteria.relationship_status]),
|
| 112 |
("Minimum height", height_factor(criteria.min_height_cm)),
|
| 113 |
+
("Minimum income", income_factor(criteria.income_min_uah)),
|
| 114 |
("Education filter", EDUCATION_FACTORS[criteria.education_level]),
|
| 115 |
("Children status", CHILDREN_STATUS_FACTORS[criteria.children_status]),
|
| 116 |
("Future children", FUTURE_CHILDREN_FACTORS[criteria.future_children]),
|