TEZv commited on
Commit
82c668e
·
1 Parent(s): ecf207c

Replace income categories with salary slider

Browse files
Files changed (3) hide show
  1. app.py +16 -14
  2. src/assumptions.py +12 -13
  3. 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
- INCOME_THRESHOLD_LABELS,
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
- income_level = st.selectbox(
126
- "Income threshold",
127
- ["any", "above_median", "top_25", "top_10"],
128
- format_func=income_label,
 
 
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
- "Top 25 and Top 10 are scenario thresholds, not official percentiles."
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
- income_level=income_level,
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"`Above median` currently means roughly {format_count(SALARY_ANCHORS_UAH['workua_current_average'])} UAH/month "
272
- "as a public job-market benchmark. Higher salary bands are scenario cutoffs until a validated percentile source is added."
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
- INCOME_THRESHOLD_LABELS = {
40
- "any": "Any income",
41
- "above_median": "Above median (about 28,600 UAH/month)",
42
- "top_25": "Top 25 scenario (about 45,000 UAH/month)",
43
- "top_10": "Top 10 scenario (about 70,000 UAH/month)",
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
- INCOME_FACTORS,
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
- income_level: str
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
- ("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]),
 
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]),