File size: 5,239 Bytes
3c0e2ec
 
 
 
 
 
177b9af
3c0e2ec
177b9af
3c0e2ec
177b9af
3c0e2ec
177b9af
82c668e
177b9af
 
 
3c0e2ec
 
177b9af
 
ac4e07f
3c0e2ec
 
 
 
 
 
ac4e07f
3c0e2ec
 
 
 
 
82c668e
3c0e2ec
177b9af
 
 
 
 
 
 
 
 
3c0e2ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82c668e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c0e2ec
 
ac4e07f
3c0e2ec
 
 
 
82c668e
3c0e2ec
177b9af
 
 
 
 
 
 
 
 
3c0e2ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecf207c
 
 
 
 
 
3c0e2ec
 
 
 
 
 
 
 
ecf207c
3c0e2ec
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
from __future__ import annotations

from dataclasses import dataclass

from .assumptions import (
    AGE_BAND_FACTORS,
    ALCOHOL_FACTORS,
    BASELINE,
    CHILDREN_STATUS_FACTORS,
    EDUCATION_FACTORS,
    FUTURE_CHILDREN_FACTORS,
    HEIGHT_FACTORS,
    HOUSING_FACTORS,
    INCOME_CURVE_POINTS_UAH,
    LANGUAGE_FACTORS,
    MILITARY_STATUS_FACTORS,
    PETS_FACTORS,
    REGION_FACTORS,
    RELATIONSHIP_STATUS_FACTORS,
    RELOCATION_FACTORS,
    SMOKING_FACTORS,
    TARGET_POPULATION_FACTORS,
)


@dataclass(frozen=True)
class Criteria:
    base_population: int
    target_population: str
    age_min: int
    age_max: int
    region_scope: str
    relationship_status: str
    min_height_cm: int
    income_min_uah: int
    education_level: str
    children_status: str
    future_children: str
    military_status: str
    relocation: str
    housing: str
    smoking: str
    alcohol: str
    language: str
    pets: str


@dataclass(frozen=True)
class PoolEstimate:
    conservative: float
    central: float
    optimistic: float


def age_factor(age_min: int, age_max: int) -> float:
    bands = {
        "18-24": (18, 24),
        "25-34": (25, 34),
        "35-44": (35, 44),
        "45-54": (45, 54),
        "55-70": (55, 70),
    }
    selected = 0.0
    for label, (band_min, band_max) in bands.items():
        overlap_min = max(age_min, band_min)
        overlap_max = min(age_max, band_max)
        if overlap_min <= overlap_max:
            band_width = band_max - band_min + 1
            overlap_width = overlap_max - overlap_min + 1
            selected += AGE_BAND_FACTORS[label] * (overlap_width / band_width)
    return max(0.01, min(selected, 1.0))


def height_factor(min_height_cm: int) -> float:
    thresholds = sorted(HEIGHT_FACTORS)
    if min_height_cm <= thresholds[0]:
        return HEIGHT_FACTORS[thresholds[0]]
    if min_height_cm >= thresholds[-1]:
        return HEIGHT_FACTORS[thresholds[-1]]

    lower = max(threshold for threshold in thresholds if threshold <= min_height_cm)
    upper = min(threshold for threshold in thresholds if threshold >= min_height_cm)
    if lower == upper:
        return HEIGHT_FACTORS[lower]

    ratio = (min_height_cm - lower) / (upper - lower)
    return HEIGHT_FACTORS[lower] + ratio * (HEIGHT_FACTORS[upper] - HEIGHT_FACTORS[lower])


def income_factor(income_min_uah: int) -> float:
    points = sorted(INCOME_CURVE_POINTS_UAH)
    if income_min_uah <= points[0][0]:
        return points[0][1]
    if income_min_uah >= points[-1][0]:
        return points[-1][1]

    lower = max(point for point in points if point[0] <= income_min_uah)
    upper = min(point for point in points if point[0] >= income_min_uah)
    if lower == upper:
        return lower[1]

    ratio = (income_min_uah - lower[0]) / (upper[0] - lower[0])
    return lower[1] + ratio * (upper[1] - lower[1])


def model_factors(criteria: Criteria) -> list[tuple[str, float]]:
    return [
        ("Target population", TARGET_POPULATION_FACTORS[criteria.target_population]),
        ("Age range", age_factor(criteria.age_min, criteria.age_max)),
        ("Region scope", REGION_FACTORS[criteria.region_scope]),
        ("Relationship status", RELATIONSHIP_STATUS_FACTORS[criteria.relationship_status]),
        ("Minimum height", height_factor(criteria.min_height_cm)),
        ("Minimum income", income_factor(criteria.income_min_uah)),
        ("Education filter", EDUCATION_FACTORS[criteria.education_level]),
        ("Children status", CHILDREN_STATUS_FACTORS[criteria.children_status]),
        ("Future children", FUTURE_CHILDREN_FACTORS[criteria.future_children]),
        ("Military status", MILITARY_STATUS_FACTORS[criteria.military_status]),
        ("Relocation", RELOCATION_FACTORS[criteria.relocation]),
        ("Housing", HOUSING_FACTORS[criteria.housing]),
        ("Smoking", SMOKING_FACTORS[criteria.smoking]),
        ("Alcohol", ALCOHOL_FACTORS[criteria.alcohol]),
        ("Language", LANGUAGE_FACTORS[criteria.language]),
        ("Pets", PETS_FACTORS[criteria.pets]),
    ]


def central_estimate(criteria: Criteria) -> float:
    value = float(criteria.base_population)
    for _, factor in model_factors(criteria):
        value *= factor
    return value


def estimate_pool(criteria: Criteria) -> PoolEstimate:
    central = central_estimate(criteria)
    return PoolEstimate(
        conservative=central * BASELINE.uncertainty_low,
        central=central,
        optimistic=central * BASELINE.uncertainty_high,
    )


def sensitivity_table(criteria: Criteria) -> list[dict[str, float | str]]:
    remaining = float(criteria.base_population)
    rows: list[dict[str, float | str]] = [
        {
            "factor": "Baseline",
            "coefficient": 1.0,
            "remaining": remaining,
            "percent_of_baseline": 100.0,
        }
    ]
    for label, coefficient in model_factors(criteria):
        remaining *= coefficient
        rows.append(
            {
                "factor": label,
                "coefficient": round(coefficient, 4),
                "remaining": round(remaining, 2),
                "percent_of_baseline": round((remaining / criteria.base_population) * 100, 6),
            }
        )
    return rows