tourism-risk 0.1.1.dev18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite both the software and the article below."
3
+ type: software
4
+ title: "tourism-risk: Intelligent analytical platform for tourism travel safety risk assessment"
5
+ authors:
6
+ - family-names: Shafar
7
+ given-names: Andrii
8
+ affiliation: Uzhhorod National University
9
+ version: 0.1.0
10
+ date-released: "2026-06-10"
11
+ license: MIT
12
+ repository-code: "https://github.com/Reidond/travel-risk-platform"
13
+ keywords:
14
+ - tourism
15
+ - risk assessment
16
+ - fuzzy logic
17
+ - decision support
18
+ preferred-citation:
19
+ type: article
20
+ title: "Intellectual-analytical platform for assessing the security risk of tourist travel"
21
+ authors:
22
+ # "Andriy" is the article's published transliteration of the author's
23
+ # name (the software author uses "Andrii") — keep as published.
24
+ - family-names: Shafar
25
+ given-names: Andriy
26
+ affiliation: Uzhhorod National University
27
+ journal: "Scientific Journal of Ternopil National Technical University"
28
+ year: 2025
29
+ volume: 120
30
+ issue: "4"
31
+ start: 78
32
+ end: 89
33
+ doi: 10.33108/visnyk_tntu2025.04.078
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrii Shafar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,313 @@
1
+ Metadata-Version: 2.4
2
+ Name: tourism-risk
3
+ Version: 0.1.1.dev18
4
+ Summary: Computational core for tourism travel safety risk assessment (fuzzy multi-level model M_I -> M_R1 -> M_R2 -> M_R3)
5
+ Keywords: tourism,risk-assessment,fuzzy-logic,decision-support,membership-function
6
+ Author: Andrii Shafar
7
+ Author-email: Andrii Shafar <andreyshafar@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Project-URL: Homepage, https://github.com/Reidond/travel-risk-platform/tree/main/core
20
+ Project-URL: Repository, https://github.com/Reidond/travel-risk-platform
21
+ Project-URL: Issues, https://github.com/Reidond/travel-risk-platform/issues
22
+ Project-URL: Documentation, https://github.com/Reidond/travel-risk-platform/blob/main/core/README.md
23
+ Description-Content-Type: text/markdown
24
+
25
+ # tourism-risk
26
+
27
+ **Computational core for tourism travel safety risk assessment** — a pure-Python
28
+ (zero-dependency) implementation of the fuzzy multi-level risk model from the
29
+ intelligent analytical platform for assessing the safety risk of tourist travel
30
+ (PhD thesis chapter 4 / the 2025 article, eq. 4.2–4.9).
31
+
32
+ The library computes the quantitative ($\mu_R$) and linguistic ($T_R$) safety
33
+ risk assessment of a tourist region from questionnaire data, implementing the
34
+ operator
35
+
36
+ $$
37
+ T_5(R, E, \Xi, \Delta, M_I, M_{R1}, M_{R2}, M_{R3}) \rightarrow Y(f_5) = \{\mu_R, T_R\}
38
+ $$
39
+
40
+ where $R$ is the set of regions, $E$ the set of respondents (tourism
41
+ participants), $\Xi(R) \in [0,1]$ the predicted level of repeat visits,
42
+ $\Delta(R)$ the expert safety level of the regional tourism system, and
43
+ $M_I, M_{R1}, M_{R2}, M_{R3}$ the four computation modules described below.
44
+
45
+ It is the engine behind the web platform of the same project, but has **no
46
+ dependency on any web layer** — researchers and tourism managers can apply it
47
+ directly in their own computations.
48
+
49
+ ## The pipeline
50
+
51
+ ### M_I — information module (eq. 4.2)
52
+
53
+ Each respondent rates criteria on a five-point linguistic scale
54
+ $l_1$ "completely disagree" … $l_5$ "completely agree", mapped numerically as
55
+ $\tau(l_k) = k$. Criteria are organised into configurable groups (article
56
+ preset: $G_1$ infrastructure safety, 5 criteria $K_{11}..K_{15}$; $G_2$ social
57
+ and ecological safety, 7 criteria $K_{21}..K_{27}$; $G_3$ medical safety,
58
+ 5 criteria $K_{31}..K_{35}$). Per group:
59
+
60
+ $$\theta_g = \sum_{i=1}^{m_g} \tau_{gi}$$
61
+
62
+ ### M_R1 — individual level (eq. 4.3–4.4)
63
+
64
+ A characteristic function converts $\theta_g$ to a term level $T_1..T_5$ using
65
+ boundaries that are multiples of the group size $m_g$ (default multipliers
66
+ 1, 2, 3, 4):
67
+
68
+ $$
69
+ T_g = \begin{cases}
70
+ T_1 & \theta_g < m_g \\
71
+ T_2 & m_g \le \theta_g < 2m_g \\
72
+ T_3 & 2m_g \le \theta_g < 3m_g \\
73
+ T_4 & 3m_g \le \theta_g < 4m_g \\
74
+ T_5 & \theta_g \ge 4m_g
75
+ \end{cases}
76
+ $$
77
+
78
+ Group terms are aggregated into the individual risk term
79
+ $r^*(e) \in \{L, BA, A, AA, H\}$ (low → high risk) by ordered "If–Then" rules.
80
+ Each rule is a multiset of minimum term levels; it fires iff the group terms
81
+ can be assigned **injectively** to the slots, each term ≥ its slot level.
82
+ Rules are evaluated top-down, first match wins; the default output is $H$.
83
+ Article preset: $[T_5,T_4,T_4] \to L$; $[T_5,T_4,T_3] \to BA$;
84
+ $[T_4,T_3,T_2] \to A$; $[T_3,T_2,T_2] \to AA$; else $H$.
85
+
86
+ ### M_R2 — regional level (eq. 4.5–4.7)
87
+
88
+ Risk terms map to a percentage scale $\chi$ (default $L\to15$, $BA\to30$,
89
+ $A\to50$, $AA\to80$, $H\to100$) and average into the regional value
90
+
91
+ $$\delta(R) = \frac{1}{n}\sum_{j=1}^{n} \chi(e_j)$$
92
+
93
+ (optionally a weighted mean). A quadratic Z-spline with parameters $a=60$,
94
+ $b=100$ produces $\varphi(R)$:
95
+
96
+ $$
97
+ \varphi(\delta) = \begin{cases}
98
+ 1 & \delta \le a \\
99
+ 1 - \dfrac{(\delta - a)^2}{2\left(\frac{b-a}{2}\right)^2} & a < \delta \le \frac{a+b}{2} \\
100
+ \dfrac{(b - \delta)^2}{2\left(\frac{b-a}{2}\right)^2} & \frac{a+b}{2} < \delta < b \\
101
+ 0 & \delta \ge b
102
+ \end{cases}
103
+ $$
104
+
105
+ A cone-shaped membership function combines $\varphi(R)$ with the DM-entered
106
+ $\Xi(R)$ into the regional feeling-of-safety level:
107
+
108
+ $$m_S(R) = 1 - \frac{1}{2}\sqrt{(\varphi(R) - 1)^2 + (\Xi(R) - 1)^2}$$
109
+
110
+ ### M_R3 — national level (eq. 4.8–4.9)
111
+
112
+ With interval boundaries $a_1..a_6 = (0, 20, 40, 60, 80, 100)$ and the expert
113
+ level $\Delta(R) = \Delta_k$, fuzzification gives
114
+
115
+ $$\omega(R) = a_{k+1} \cdot m_S(R)$$
116
+
117
+ and an S-shaped membership function the final quantitative estimate:
118
+
119
+ $$
120
+ \mu_R(\omega) = \begin{cases}
121
+ 0 & \omega \le a_1 \\
122
+ 2\left(\dfrac{\omega - a_1}{a_6 - a_1}\right)^2 & a_1 < \omega \le \frac{a_1+a_6}{2} \\
123
+ 1 - 2\left(\dfrac{a_6 - \omega}{a_6 - a_1}\right)^2 & \frac{a_1+a_6}{2} < \omega < a_6 \\
124
+ 1 & \omega \ge a_6
125
+ \end{cases}
126
+ $$
127
+
128
+ $\mu_R$ is interpreted linguistically: $[0, 0.2) \to R_1$ very high risk,
129
+ $[0.2, 0.4) \to R_2$ high, $[0.4, 0.6) \to R_3$ medium, $[0.6, 0.8) \to R_4$
130
+ low, $[0.8, 1] \to R_5$ very low risk.
131
+
132
+ ## Installation
133
+
134
+ > PyPI publication is pending; once published:
135
+
136
+ ```bash
137
+ pip install tourism-risk
138
+ ```
139
+
140
+ or, with [uv](https://docs.astral.sh/uv/):
141
+
142
+ ```bash
143
+ uv add tourism-risk
144
+ ```
145
+
146
+ Until then, install from source (from the repository root):
147
+
148
+ ```bash
149
+ uv pip install ./core # or: pip install ./core
150
+ ```
151
+
152
+ Requires Python ≥ 3.12. No runtime dependencies.
153
+
154
+ ## Usage: reproducing the article's worked example
155
+
156
+ The article evaluates three control respondents and three regions
157
+ ($R_1$ Zakarpattia, $R_2$ Ivano-Frankivsk, $R_3$ Lviv oblast). Individual
158
+ level, end to end:
159
+
160
+ ```python
161
+ from tourism_risk import DEFAULT_CONFIG, evaluate_respondent
162
+
163
+ # Ratings in group order: K11..K15, K21..K27, K31..K35
164
+ e1 = [5, 5, 1, 1, 1] + [1] * 7 + [2, 2, 2, 1, 1]
165
+ e210 = [4, 2, 2, 2, 2] + [2, 2, 3, 2, 3, 2, 1] + [2, 3, 3, 4, 5]
166
+ e251 = [1, 1, 2, 1, 2] + [1, 1, 2, 1, 2, 3, 3] + [1, 2, 4, 2, 3]
167
+
168
+ for name, ratings in [("e1", e1), ("e210", e210), ("e251", e251)]:
169
+ result = evaluate_respondent(ratings, DEFAULT_CONFIG)
170
+ print(name, result.theta, result.group_terms, result.risk_term.name)
171
+ ```
172
+
173
+ Expected output (matches article Table 1):
174
+
175
+ ```
176
+ e1 (13, 7, 8) (3, 2, 2) AA
177
+ e210 (12, 15, 17) (3, 3, 4) A
178
+ e251 (7, 13, 12) (2, 2, 3) AA
179
+ ```
180
+
181
+ Regional and national level, feeding the article's published step values:
182
+
183
+ ```python
184
+ from tourism_risk import (
185
+ DEFAULT_CONFIG, cone_membership, omega, risk_class, s_membership, z_spline,
186
+ )
187
+
188
+ # R1 Zakarpattia: published delta = 79.54, Xi = 0.85, expert level Delta_5
189
+ phi = z_spline(79.54, a=60.0, b=100.0) # eq. 4.6 -> 0.5227355
190
+ m_s = cone_membership(phi=0.5, xi=0.85) # eq. 4.7 (article carries phi rounded to 0.5)
191
+ w = omega(0.74, 5, DEFAULT_CONFIG.fuzz_boundaries) # eq. 4.8
192
+ mu = s_membership(74.0, 0.0, 100.0) # eq. 4.9
193
+ t_r = risk_class(mu, DEFAULT_CONFIG.risk_thresholds)
194
+
195
+ print(f"m_S = {m_s:.2f}, omega = {w:.0f}, mu_R = {mu:.4f}, class = {t_r.name}")
196
+ ```
197
+
198
+ Expected output (matches the published chain $m_S=0.74$, $\omega=74$,
199
+ $\mu_R=0.86$, $R_5$ — very low risk):
200
+
201
+ ```
202
+ m_S = 0.74, omega = 74, mu_R = 0.8648, class = R5
203
+ ```
204
+
205
+ Or evaluate a whole region in one call, with every intermediate retained:
206
+
207
+ ```python
208
+ from tourism_risk import DEFAULT_CONFIG, evaluate_region
209
+
210
+ region = evaluate_region(
211
+ respondent_ratings=[e1, e210, e251],
212
+ xi=0.85, # predicted level of repeat visits (DM input)
213
+ delta_level=5, # expert safety level Delta_5 (DM input)
214
+ config=DEFAULT_CONFIG,
215
+ )
216
+ print(region.delta) # 70.0 (eq. 4.5)
217
+ print(region.phi) # 0.875 (eq. 4.6)
218
+ print(round(region.m_s, 4)) # 0.9024 (eq. 4.7)
219
+ print(round(region.omega, 2)) # 90.24 (eq. 4.8)
220
+ print(round(region.mu, 4)) # 0.9809 (eq. 4.9)
221
+ print(region.risk_class.name) # R5 (very low risk)
222
+ ```
223
+
224
+ ## Customising the model
225
+
226
+ Every constant of the model is a field of the frozen `ModelConfig` — criteria
227
+ groups, rule set, term boundaries, the $\chi$ scale, Z-spline parameters,
228
+ cone base/scale, interval boundaries $a_1..a_6$ and risk-class thresholds:
229
+
230
+ ```python
231
+ import dataclasses
232
+
233
+ from tourism_risk import (
234
+ DEFAULT_CONFIG, CriteriaGroup, ModelConfig, RiskTerm, Rule, RuleSet,
235
+ evaluate_region,
236
+ )
237
+
238
+ # A stricter rule set (the article's prose variant of rule 4: two groups at T3)
239
+ strict_rules = RuleSet(
240
+ rules=(
241
+ Rule(pattern=(5, 4, 4), output=RiskTerm.L),
242
+ Rule(pattern=(5, 4, 3), output=RiskTerm.BA),
243
+ Rule(pattern=(4, 3, 2), output=RiskTerm.A),
244
+ Rule(pattern=(3, 3, 2), output=RiskTerm.AA),
245
+ ),
246
+ default=RiskTerm.H,
247
+ )
248
+ strict_config = dataclasses.replace(DEFAULT_CONFIG, rules=strict_rules)
249
+
250
+ # Or a fully custom model: two criteria groups and a shifted chi scale
251
+ custom_config = ModelConfig(
252
+ groups=(
253
+ CriteriaGroup(id="G1", name="Transport", criteria=("C1", "C2", "C3")),
254
+ CriteriaGroup(id="G2", name="Health", criteria=("C4", "C5", "C6", "C7")),
255
+ ),
256
+ rules=RuleSet(
257
+ rules=(
258
+ Rule(pattern=(5, 4), output=RiskTerm.L),
259
+ Rule(pattern=(4, 3), output=RiskTerm.A),
260
+ ),
261
+ default=RiskTerm.H,
262
+ ),
263
+ chi_scale=(10.0, 30.0, 50.0, 70.0, 90.0),
264
+ zspline_a=50.0,
265
+ zspline_b=90.0,
266
+ )
267
+
268
+ result = evaluate_region(
269
+ respondent_ratings=[[5, 5, 4, 4, 4, 5, 4]],
270
+ xi=0.9,
271
+ delta_level=4,
272
+ config=custom_config,
273
+ )
274
+ print(result.risk_class.name)
275
+ ```
276
+
277
+ Inputs are validated with clear `ValueError`s: ratings must be integers 1–5
278
+ with the exact criteria count, $\Xi \in [0, 1]$, boundaries strictly
279
+ increasing, rule patterns non-empty with levels 1–5 and at most one slot per
280
+ criteria group.
281
+
282
+ ## Publishing (maintainers)
283
+
284
+ Build the sdist + wheel and upload to PyPI with uv (from the repo root):
285
+
286
+ ```bash
287
+ uv build --project core --out-dir core/dist
288
+ uv publish core/dist/*
289
+ ```
290
+
291
+ `uv publish` authenticates with a PyPI API token — set the
292
+ `UV_PUBLISH_TOKEN` environment variable or pass `--token`. Bump `version` in
293
+ `core/pyproject.toml` (and `CITATION.cff`) before building.
294
+
295
+ ## Citation
296
+
297
+ If you use this library in academic work, please cite the article:
298
+
299
+ > Shafar, A. (2025). Intellectual-analytical platform for assessing the
300
+ > security risk of tourist travel. *Scientific Journal of Ternopil National
301
+ > Technical University, 120*(4), 78–89.
302
+ > https://doi.org/10.33108/visnyk_tntu2025.04.078
303
+
304
+ and the software itself (see
305
+ [`CITATION.cff`](https://github.com/Reidond/travel-risk-platform/blob/main/core/CITATION.cff)):
306
+
307
+ > Shafar, A. (2026). *tourism-risk: Intelligent analytical platform for
308
+ > tourism travel safety risk assessment* (Version 0.1.0) [Computer software].
309
+
310
+ ## License
311
+
312
+ [MIT](https://github.com/Reidond/travel-risk-platform/blob/main/core/LICENSE)
313
+ © 2026 Andrii Shafar
@@ -0,0 +1,289 @@
1
+ # tourism-risk
2
+
3
+ **Computational core for tourism travel safety risk assessment** — a pure-Python
4
+ (zero-dependency) implementation of the fuzzy multi-level risk model from the
5
+ intelligent analytical platform for assessing the safety risk of tourist travel
6
+ (PhD thesis chapter 4 / the 2025 article, eq. 4.2–4.9).
7
+
8
+ The library computes the quantitative ($\mu_R$) and linguistic ($T_R$) safety
9
+ risk assessment of a tourist region from questionnaire data, implementing the
10
+ operator
11
+
12
+ $$
13
+ T_5(R, E, \Xi, \Delta, M_I, M_{R1}, M_{R2}, M_{R3}) \rightarrow Y(f_5) = \{\mu_R, T_R\}
14
+ $$
15
+
16
+ where $R$ is the set of regions, $E$ the set of respondents (tourism
17
+ participants), $\Xi(R) \in [0,1]$ the predicted level of repeat visits,
18
+ $\Delta(R)$ the expert safety level of the regional tourism system, and
19
+ $M_I, M_{R1}, M_{R2}, M_{R3}$ the four computation modules described below.
20
+
21
+ It is the engine behind the web platform of the same project, but has **no
22
+ dependency on any web layer** — researchers and tourism managers can apply it
23
+ directly in their own computations.
24
+
25
+ ## The pipeline
26
+
27
+ ### M_I — information module (eq. 4.2)
28
+
29
+ Each respondent rates criteria on a five-point linguistic scale
30
+ $l_1$ "completely disagree" … $l_5$ "completely agree", mapped numerically as
31
+ $\tau(l_k) = k$. Criteria are organised into configurable groups (article
32
+ preset: $G_1$ infrastructure safety, 5 criteria $K_{11}..K_{15}$; $G_2$ social
33
+ and ecological safety, 7 criteria $K_{21}..K_{27}$; $G_3$ medical safety,
34
+ 5 criteria $K_{31}..K_{35}$). Per group:
35
+
36
+ $$\theta_g = \sum_{i=1}^{m_g} \tau_{gi}$$
37
+
38
+ ### M_R1 — individual level (eq. 4.3–4.4)
39
+
40
+ A characteristic function converts $\theta_g$ to a term level $T_1..T_5$ using
41
+ boundaries that are multiples of the group size $m_g$ (default multipliers
42
+ 1, 2, 3, 4):
43
+
44
+ $$
45
+ T_g = \begin{cases}
46
+ T_1 & \theta_g < m_g \\
47
+ T_2 & m_g \le \theta_g < 2m_g \\
48
+ T_3 & 2m_g \le \theta_g < 3m_g \\
49
+ T_4 & 3m_g \le \theta_g < 4m_g \\
50
+ T_5 & \theta_g \ge 4m_g
51
+ \end{cases}
52
+ $$
53
+
54
+ Group terms are aggregated into the individual risk term
55
+ $r^*(e) \in \{L, BA, A, AA, H\}$ (low → high risk) by ordered "If–Then" rules.
56
+ Each rule is a multiset of minimum term levels; it fires iff the group terms
57
+ can be assigned **injectively** to the slots, each term ≥ its slot level.
58
+ Rules are evaluated top-down, first match wins; the default output is $H$.
59
+ Article preset: $[T_5,T_4,T_4] \to L$; $[T_5,T_4,T_3] \to BA$;
60
+ $[T_4,T_3,T_2] \to A$; $[T_3,T_2,T_2] \to AA$; else $H$.
61
+
62
+ ### M_R2 — regional level (eq. 4.5–4.7)
63
+
64
+ Risk terms map to a percentage scale $\chi$ (default $L\to15$, $BA\to30$,
65
+ $A\to50$, $AA\to80$, $H\to100$) and average into the regional value
66
+
67
+ $$\delta(R) = \frac{1}{n}\sum_{j=1}^{n} \chi(e_j)$$
68
+
69
+ (optionally a weighted mean). A quadratic Z-spline with parameters $a=60$,
70
+ $b=100$ produces $\varphi(R)$:
71
+
72
+ $$
73
+ \varphi(\delta) = \begin{cases}
74
+ 1 & \delta \le a \\
75
+ 1 - \dfrac{(\delta - a)^2}{2\left(\frac{b-a}{2}\right)^2} & a < \delta \le \frac{a+b}{2} \\
76
+ \dfrac{(b - \delta)^2}{2\left(\frac{b-a}{2}\right)^2} & \frac{a+b}{2} < \delta < b \\
77
+ 0 & \delta \ge b
78
+ \end{cases}
79
+ $$
80
+
81
+ A cone-shaped membership function combines $\varphi(R)$ with the DM-entered
82
+ $\Xi(R)$ into the regional feeling-of-safety level:
83
+
84
+ $$m_S(R) = 1 - \frac{1}{2}\sqrt{(\varphi(R) - 1)^2 + (\Xi(R) - 1)^2}$$
85
+
86
+ ### M_R3 — national level (eq. 4.8–4.9)
87
+
88
+ With interval boundaries $a_1..a_6 = (0, 20, 40, 60, 80, 100)$ and the expert
89
+ level $\Delta(R) = \Delta_k$, fuzzification gives
90
+
91
+ $$\omega(R) = a_{k+1} \cdot m_S(R)$$
92
+
93
+ and an S-shaped membership function the final quantitative estimate:
94
+
95
+ $$
96
+ \mu_R(\omega) = \begin{cases}
97
+ 0 & \omega \le a_1 \\
98
+ 2\left(\dfrac{\omega - a_1}{a_6 - a_1}\right)^2 & a_1 < \omega \le \frac{a_1+a_6}{2} \\
99
+ 1 - 2\left(\dfrac{a_6 - \omega}{a_6 - a_1}\right)^2 & \frac{a_1+a_6}{2} < \omega < a_6 \\
100
+ 1 & \omega \ge a_6
101
+ \end{cases}
102
+ $$
103
+
104
+ $\mu_R$ is interpreted linguistically: $[0, 0.2) \to R_1$ very high risk,
105
+ $[0.2, 0.4) \to R_2$ high, $[0.4, 0.6) \to R_3$ medium, $[0.6, 0.8) \to R_4$
106
+ low, $[0.8, 1] \to R_5$ very low risk.
107
+
108
+ ## Installation
109
+
110
+ > PyPI publication is pending; once published:
111
+
112
+ ```bash
113
+ pip install tourism-risk
114
+ ```
115
+
116
+ or, with [uv](https://docs.astral.sh/uv/):
117
+
118
+ ```bash
119
+ uv add tourism-risk
120
+ ```
121
+
122
+ Until then, install from source (from the repository root):
123
+
124
+ ```bash
125
+ uv pip install ./core # or: pip install ./core
126
+ ```
127
+
128
+ Requires Python ≥ 3.12. No runtime dependencies.
129
+
130
+ ## Usage: reproducing the article's worked example
131
+
132
+ The article evaluates three control respondents and three regions
133
+ ($R_1$ Zakarpattia, $R_2$ Ivano-Frankivsk, $R_3$ Lviv oblast). Individual
134
+ level, end to end:
135
+
136
+ ```python
137
+ from tourism_risk import DEFAULT_CONFIG, evaluate_respondent
138
+
139
+ # Ratings in group order: K11..K15, K21..K27, K31..K35
140
+ e1 = [5, 5, 1, 1, 1] + [1] * 7 + [2, 2, 2, 1, 1]
141
+ e210 = [4, 2, 2, 2, 2] + [2, 2, 3, 2, 3, 2, 1] + [2, 3, 3, 4, 5]
142
+ e251 = [1, 1, 2, 1, 2] + [1, 1, 2, 1, 2, 3, 3] + [1, 2, 4, 2, 3]
143
+
144
+ for name, ratings in [("e1", e1), ("e210", e210), ("e251", e251)]:
145
+ result = evaluate_respondent(ratings, DEFAULT_CONFIG)
146
+ print(name, result.theta, result.group_terms, result.risk_term.name)
147
+ ```
148
+
149
+ Expected output (matches article Table 1):
150
+
151
+ ```
152
+ e1 (13, 7, 8) (3, 2, 2) AA
153
+ e210 (12, 15, 17) (3, 3, 4) A
154
+ e251 (7, 13, 12) (2, 2, 3) AA
155
+ ```
156
+
157
+ Regional and national level, feeding the article's published step values:
158
+
159
+ ```python
160
+ from tourism_risk import (
161
+ DEFAULT_CONFIG, cone_membership, omega, risk_class, s_membership, z_spline,
162
+ )
163
+
164
+ # R1 Zakarpattia: published delta = 79.54, Xi = 0.85, expert level Delta_5
165
+ phi = z_spline(79.54, a=60.0, b=100.0) # eq. 4.6 -> 0.5227355
166
+ m_s = cone_membership(phi=0.5, xi=0.85) # eq. 4.7 (article carries phi rounded to 0.5)
167
+ w = omega(0.74, 5, DEFAULT_CONFIG.fuzz_boundaries) # eq. 4.8
168
+ mu = s_membership(74.0, 0.0, 100.0) # eq. 4.9
169
+ t_r = risk_class(mu, DEFAULT_CONFIG.risk_thresholds)
170
+
171
+ print(f"m_S = {m_s:.2f}, omega = {w:.0f}, mu_R = {mu:.4f}, class = {t_r.name}")
172
+ ```
173
+
174
+ Expected output (matches the published chain $m_S=0.74$, $\omega=74$,
175
+ $\mu_R=0.86$, $R_5$ — very low risk):
176
+
177
+ ```
178
+ m_S = 0.74, omega = 74, mu_R = 0.8648, class = R5
179
+ ```
180
+
181
+ Or evaluate a whole region in one call, with every intermediate retained:
182
+
183
+ ```python
184
+ from tourism_risk import DEFAULT_CONFIG, evaluate_region
185
+
186
+ region = evaluate_region(
187
+ respondent_ratings=[e1, e210, e251],
188
+ xi=0.85, # predicted level of repeat visits (DM input)
189
+ delta_level=5, # expert safety level Delta_5 (DM input)
190
+ config=DEFAULT_CONFIG,
191
+ )
192
+ print(region.delta) # 70.0 (eq. 4.5)
193
+ print(region.phi) # 0.875 (eq. 4.6)
194
+ print(round(region.m_s, 4)) # 0.9024 (eq. 4.7)
195
+ print(round(region.omega, 2)) # 90.24 (eq. 4.8)
196
+ print(round(region.mu, 4)) # 0.9809 (eq. 4.9)
197
+ print(region.risk_class.name) # R5 (very low risk)
198
+ ```
199
+
200
+ ## Customising the model
201
+
202
+ Every constant of the model is a field of the frozen `ModelConfig` — criteria
203
+ groups, rule set, term boundaries, the $\chi$ scale, Z-spline parameters,
204
+ cone base/scale, interval boundaries $a_1..a_6$ and risk-class thresholds:
205
+
206
+ ```python
207
+ import dataclasses
208
+
209
+ from tourism_risk import (
210
+ DEFAULT_CONFIG, CriteriaGroup, ModelConfig, RiskTerm, Rule, RuleSet,
211
+ evaluate_region,
212
+ )
213
+
214
+ # A stricter rule set (the article's prose variant of rule 4: two groups at T3)
215
+ strict_rules = RuleSet(
216
+ rules=(
217
+ Rule(pattern=(5, 4, 4), output=RiskTerm.L),
218
+ Rule(pattern=(5, 4, 3), output=RiskTerm.BA),
219
+ Rule(pattern=(4, 3, 2), output=RiskTerm.A),
220
+ Rule(pattern=(3, 3, 2), output=RiskTerm.AA),
221
+ ),
222
+ default=RiskTerm.H,
223
+ )
224
+ strict_config = dataclasses.replace(DEFAULT_CONFIG, rules=strict_rules)
225
+
226
+ # Or a fully custom model: two criteria groups and a shifted chi scale
227
+ custom_config = ModelConfig(
228
+ groups=(
229
+ CriteriaGroup(id="G1", name="Transport", criteria=("C1", "C2", "C3")),
230
+ CriteriaGroup(id="G2", name="Health", criteria=("C4", "C5", "C6", "C7")),
231
+ ),
232
+ rules=RuleSet(
233
+ rules=(
234
+ Rule(pattern=(5, 4), output=RiskTerm.L),
235
+ Rule(pattern=(4, 3), output=RiskTerm.A),
236
+ ),
237
+ default=RiskTerm.H,
238
+ ),
239
+ chi_scale=(10.0, 30.0, 50.0, 70.0, 90.0),
240
+ zspline_a=50.0,
241
+ zspline_b=90.0,
242
+ )
243
+
244
+ result = evaluate_region(
245
+ respondent_ratings=[[5, 5, 4, 4, 4, 5, 4]],
246
+ xi=0.9,
247
+ delta_level=4,
248
+ config=custom_config,
249
+ )
250
+ print(result.risk_class.name)
251
+ ```
252
+
253
+ Inputs are validated with clear `ValueError`s: ratings must be integers 1–5
254
+ with the exact criteria count, $\Xi \in [0, 1]$, boundaries strictly
255
+ increasing, rule patterns non-empty with levels 1–5 and at most one slot per
256
+ criteria group.
257
+
258
+ ## Publishing (maintainers)
259
+
260
+ Build the sdist + wheel and upload to PyPI with uv (from the repo root):
261
+
262
+ ```bash
263
+ uv build --project core --out-dir core/dist
264
+ uv publish core/dist/*
265
+ ```
266
+
267
+ `uv publish` authenticates with a PyPI API token — set the
268
+ `UV_PUBLISH_TOKEN` environment variable or pass `--token`. Bump `version` in
269
+ `core/pyproject.toml` (and `CITATION.cff`) before building.
270
+
271
+ ## Citation
272
+
273
+ If you use this library in academic work, please cite the article:
274
+
275
+ > Shafar, A. (2025). Intellectual-analytical platform for assessing the
276
+ > security risk of tourist travel. *Scientific Journal of Ternopil National
277
+ > Technical University, 120*(4), 78–89.
278
+ > https://doi.org/10.33108/visnyk_tntu2025.04.078
279
+
280
+ and the software itself (see
281
+ [`CITATION.cff`](https://github.com/Reidond/travel-risk-platform/blob/main/core/CITATION.cff)):
282
+
283
+ > Shafar, A. (2026). *tourism-risk: Intelligent analytical platform for
284
+ > tourism travel safety risk assessment* (Version 0.1.0) [Computer software].
285
+
286
+ ## License
287
+
288
+ [MIT](https://github.com/Reidond/travel-risk-platform/blob/main/core/LICENSE)
289
+ © 2026 Andrii Shafar