TEZv commited on
Commit
3c0e2ec
·
1 Parent(s): 448a370

Publish partner pool simulator space

Browse files
Files changed (8) hide show
  1. Dockerfile +12 -0
  2. README.md +12 -4
  3. app.py +100 -0
  4. data/sources.yml +59 -0
  5. requirements.txt +4 -0
  6. src/__init__.py +1 -0
  7. src/assumptions.py +76 -0
  8. src/model_pool.py +111 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.headless=true"]
README.md CHANGED
@@ -1,10 +1,18 @@
1
  ---
2
- title: Partner-Pool-Simulator 05-2026
3
- emoji: 📚
4
  colorFrom: blue
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Partner Pool Simulator 05-2026
3
+ emoji: 📊
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # Partner Pool Assumption Simulator
11
+
12
+ Transparent Streamlit prototype for `S7-K · Personal Relationship`.
13
+
14
+ This Space is a public demo for estimating how relationship criteria narrow an assumed demographic pool. Current coefficients are demo assumptions and must not be interpreted as factual population counts.
15
+
16
+ Source study:
17
+
18
+ - [K-RnD-Lab / SPHERE-I-SCIENCE](https://github.com/K-RnD-Lab/SPHERE-I-SCIENCE/tree/main/S7%20%E2%80%94%20%F0%9F%93%9A%20K%20Life%20OS/S7-K%20%C2%B7%20%F0%9F%91%A5%20Personal%20Relationship/R1a-partner-pool-assumption-simulator)
app.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pandas as pd
4
+ import plotly.express as px
5
+ import streamlit as st
6
+
7
+ from src.assumptions import BASELINE, DATA_QUALITY_NOTES
8
+ from src.model_pool import Criteria, estimate_pool, sensitivity_table
9
+
10
+
11
+ st.set_page_config(
12
+ page_title="Partner Pool Assumption Simulator",
13
+ page_icon="S7",
14
+ layout="wide",
15
+ )
16
+
17
+ st.title("Partner Pool Assumption Simulator")
18
+ st.caption("S7-K · Personal Relationship · transparent demo model")
19
+
20
+ st.info(
21
+ "Prototype status: current numbers are demo assumptions. Use this app to test model logic, "
22
+ "not to claim a factual count of available partners."
23
+ )
24
+
25
+ with st.sidebar:
26
+ st.header("Scenario")
27
+ base_population = st.number_input(
28
+ "Baseline population",
29
+ min_value=10_000,
30
+ max_value=50_000_000,
31
+ value=BASELINE.total_reference_population,
32
+ step=50_000,
33
+ )
34
+ age_min, age_max = st.slider("Age range", 18, 70, (28, 42))
35
+ region_scope = st.selectbox(
36
+ "Region scope",
37
+ ["all_ukraine", "large_cities", "kyiv_region", "western_regions"],
38
+ format_func=lambda value: value.replace("_", " ").title(),
39
+ )
40
+ relationship_status = st.selectbox(
41
+ "Relationship status",
42
+ ["any", "not_married", "single_or_divorced"],
43
+ format_func=lambda value: value.replace("_", " ").title(),
44
+ )
45
+ min_height = st.slider("Minimum height, cm", 150, 205, 175)
46
+ income_level = st.selectbox(
47
+ "Income threshold",
48
+ ["any", "above_median", "top_25", "top_10"],
49
+ format_func=lambda value: value.replace("_", " ").title(),
50
+ )
51
+ education_level = st.selectbox(
52
+ "Education filter",
53
+ ["any", "higher_education", "graduate_plus"],
54
+ format_func=lambda value: value.replace("_", " ").title(),
55
+ )
56
+
57
+ criteria = Criteria(
58
+ base_population=base_population,
59
+ age_min=age_min,
60
+ age_max=age_max,
61
+ region_scope=region_scope,
62
+ relationship_status=relationship_status,
63
+ min_height_cm=min_height,
64
+ income_level=income_level,
65
+ education_level=education_level,
66
+ )
67
+
68
+ estimate = estimate_pool(criteria)
69
+ steps = sensitivity_table(criteria)
70
+
71
+ col_a, col_b, col_c = st.columns(3)
72
+ col_a.metric("Conservative estimate", f"{estimate.conservative:,.0f}")
73
+ col_b.metric("Central estimate", f"{estimate.central:,.0f}")
74
+ col_c.metric("Optimistic estimate", f"{estimate.optimistic:,.0f}")
75
+
76
+ st.subheader("What narrows the pool")
77
+ step_df = pd.DataFrame(steps)
78
+ fig = px.bar(
79
+ step_df,
80
+ x="factor",
81
+ y="remaining",
82
+ text="remaining",
83
+ title="Remaining estimated pool after each criterion",
84
+ )
85
+ fig.update_traces(texttemplate="%{text:,.0f}", textposition="outside")
86
+ fig.update_layout(yaxis_title="Estimated remaining pool", xaxis_title="")
87
+ st.plotly_chart(fig, use_container_width=True)
88
+
89
+ st.subheader("Scenario details")
90
+ st.dataframe(step_df, use_container_width=True, hide_index=True)
91
+
92
+ st.subheader("Data quality notes")
93
+ for note in DATA_QUALITY_NOTES:
94
+ st.write(f"- **{note['label']}**: {note['note']}")
95
+
96
+ st.subheader("Interpretation guardrails")
97
+ st.write(
98
+ "This model estimates a demographic scenario, not compatibility, attraction, safety, or relationship success. "
99
+ "A stricter filter can make a pool smaller, but it does not define a person's real-life chances."
100
+ )
data/sources.yml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ sources:
2
+ - id: ukraine_nowpop
3
+ name: Ukraine NowPop
4
+ url: https://nowpop.org/
5
+ use_for:
6
+ - population_by_age_sex_region
7
+ status: candidate
8
+ confidence: medium
9
+ notes: Current population estimates should be reviewed for license, geographic granularity, and update cadence.
10
+
11
+ - id: ukrstat
12
+ name: State Statistics Service of Ukraine
13
+ url: https://stat.gov.ua/
14
+ use_for:
15
+ - baseline_demography
16
+ - wages
17
+ - social_indicators
18
+ status: candidate
19
+ confidence: medium
20
+ notes: Official source, but some demographic series may lag wartime mobility.
21
+
22
+ - id: ministry_of_justice_marriage
23
+ name: Ministry of Justice marriage and divorce administrative data
24
+ url: https://minjust.gov.ua/
25
+ use_for:
26
+ - marriages
27
+ - divorces
28
+ status: candidate
29
+ confidence: medium
30
+ notes: Useful for flow indicators, not a direct measure of single or available population.
31
+
32
+ - id: opendatabot_marriage_divorce
33
+ name: Opendatabot marriage/divorce analytics
34
+ url: https://opendatabot.ua/
35
+ use_for:
36
+ - public_context
37
+ - administrative_data_summary
38
+ status: candidate
39
+ confidence: low_to_medium
40
+ notes: Aggregated public analytics; validate against primary Ministry of Justice releases before using in model coefficients.
41
+
42
+ - id: ncd_risc_height
43
+ name: NCD Risk Factor Collaboration anthropometric data
44
+ url: https://ncdrisc.org/
45
+ use_for:
46
+ - height_distribution_proxy
47
+ status: candidate_proxy
48
+ confidence: low
49
+ notes: Use only if Ukraine-specific height distribution is unavailable; label as proxy.
50
+
51
+ - id: world_bank_living_conditions
52
+ name: World Bank Ukraine living conditions updates
53
+ url: https://www.worldbank.org/en/country/ukraine
54
+ use_for:
55
+ - income_context
56
+ - living_conditions_context
57
+ status: candidate
58
+ confidence: medium
59
+ notes: Useful for context and uncertainty framing rather than direct demographic filtering.
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit>=1.35
2
+ pandas>=2.2
3
+ plotly>=5.22
4
+ PyYAML>=6.0
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
src/assumptions.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class BaselineAssumptions:
8
+ total_reference_population: int = 10_000_000
9
+ uncertainty_low: float = 0.72
10
+ uncertainty_high: float = 1.28
11
+
12
+
13
+ BASELINE = BaselineAssumptions()
14
+
15
+ AGE_BAND_FACTORS = {
16
+ "18-24": 0.12,
17
+ "25-34": 0.22,
18
+ "35-44": 0.20,
19
+ "45-54": 0.18,
20
+ "55-70": 0.28,
21
+ }
22
+
23
+ REGION_FACTORS = {
24
+ "all_ukraine": 1.0,
25
+ "large_cities": 0.34,
26
+ "kyiv_region": 0.13,
27
+ "western_regions": 0.24,
28
+ }
29
+
30
+ RELATIONSHIP_STATUS_FACTORS = {
31
+ "any": 1.0,
32
+ "not_married": 0.46,
33
+ "single_or_divorced": 0.32,
34
+ }
35
+
36
+ HEIGHT_FACTORS = {
37
+ 160: 0.92,
38
+ 165: 0.82,
39
+ 170: 0.67,
40
+ 175: 0.48,
41
+ 180: 0.28,
42
+ 185: 0.13,
43
+ 190: 0.04,
44
+ }
45
+
46
+ INCOME_FACTORS = {
47
+ "any": 1.0,
48
+ "above_median": 0.42,
49
+ "top_25": 0.25,
50
+ "top_10": 0.10,
51
+ }
52
+
53
+ EDUCATION_FACTORS = {
54
+ "any": 1.0,
55
+ "higher_education": 0.38,
56
+ "graduate_plus": 0.16,
57
+ }
58
+
59
+ DATA_QUALITY_NOTES = [
60
+ {
61
+ "label": "Population",
62
+ "note": "Replace demo baseline with current age-sex population estimates before publication.",
63
+ },
64
+ {
65
+ "label": "Relationship status",
66
+ "note": "Official marital status does not equal real availability; label as estimated.",
67
+ },
68
+ {
69
+ "label": "Income",
70
+ "note": "Income and salary filters are sensitive to self-employment and informal earnings.",
71
+ },
72
+ {
73
+ "label": "Height",
74
+ "note": "Height currently requires proxy distribution unless a Ukraine-specific source is validated.",
75
+ },
76
+ ]
src/model_pool.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ 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
+ )
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Criteria:
18
+ base_population: int
19
+ age_min: int
20
+ age_max: int
21
+ region_scope: str
22
+ relationship_status: str
23
+ min_height_cm: int
24
+ income_level: str
25
+ education_level: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class PoolEstimate:
30
+ conservative: float
31
+ central: float
32
+ optimistic: float
33
+
34
+
35
+ def age_factor(age_min: int, age_max: int) -> float:
36
+ bands = {
37
+ "18-24": (18, 24),
38
+ "25-34": (25, 34),
39
+ "35-44": (35, 44),
40
+ "45-54": (45, 54),
41
+ "55-70": (55, 70),
42
+ }
43
+ selected = 0.0
44
+ for label, (band_min, band_max) in bands.items():
45
+ overlap_min = max(age_min, band_min)
46
+ overlap_max = min(age_max, band_max)
47
+ if overlap_min <= overlap_max:
48
+ band_width = band_max - band_min + 1
49
+ overlap_width = overlap_max - overlap_min + 1
50
+ selected += AGE_BAND_FACTORS[label] * (overlap_width / band_width)
51
+ return max(0.01, min(selected, 1.0))
52
+
53
+
54
+ def height_factor(min_height_cm: int) -> float:
55
+ thresholds = sorted(HEIGHT_FACTORS)
56
+ if min_height_cm <= thresholds[0]:
57
+ return HEIGHT_FACTORS[thresholds[0]]
58
+ if min_height_cm >= thresholds[-1]:
59
+ return HEIGHT_FACTORS[thresholds[-1]]
60
+
61
+ lower = max(threshold for threshold in thresholds if threshold <= min_height_cm)
62
+ upper = min(threshold for threshold in thresholds if threshold >= min_height_cm)
63
+ if lower == upper:
64
+ return HEIGHT_FACTORS[lower]
65
+
66
+ ratio = (min_height_cm - lower) / (upper - lower)
67
+ return HEIGHT_FACTORS[lower] + ratio * (HEIGHT_FACTORS[upper] - HEIGHT_FACTORS[lower])
68
+
69
+
70
+ def model_factors(criteria: Criteria) -> list[tuple[str, float]]:
71
+ return [
72
+ ("Age range", age_factor(criteria.age_min, criteria.age_max)),
73
+ ("Region scope", REGION_FACTORS[criteria.region_scope]),
74
+ ("Relationship status", RELATIONSHIP_STATUS_FACTORS[criteria.relationship_status]),
75
+ ("Minimum height", height_factor(criteria.min_height_cm)),
76
+ ("Income threshold", INCOME_FACTORS[criteria.income_level]),
77
+ ("Education filter", EDUCATION_FACTORS[criteria.education_level]),
78
+ ]
79
+
80
+
81
+ def central_estimate(criteria: Criteria) -> float:
82
+ value = float(criteria.base_population)
83
+ for _, factor in model_factors(criteria):
84
+ value *= factor
85
+ return value
86
+
87
+
88
+ def estimate_pool(criteria: Criteria) -> PoolEstimate:
89
+ central = central_estimate(criteria)
90
+ return PoolEstimate(
91
+ conservative=central * BASELINE.uncertainty_low,
92
+ central=central,
93
+ optimistic=central * BASELINE.uncertainty_high,
94
+ )
95
+
96
+
97
+ def sensitivity_table(criteria: Criteria) -> list[dict[str, float | str]]:
98
+ remaining = float(criteria.base_population)
99
+ rows: list[dict[str, float | str]] = [
100
+ {"factor": "Baseline", "coefficient": 1.0, "remaining": remaining}
101
+ ]
102
+ for label, coefficient in model_factors(criteria):
103
+ remaining *= coefficient
104
+ rows.append(
105
+ {
106
+ "factor": label,
107
+ "coefficient": round(coefficient, 4),
108
+ "remaining": round(remaining, 2),
109
+ }
110
+ )
111
+ return rows