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.
- tourism_risk-0.1.1.dev18/CITATION.cff +33 -0
- tourism_risk-0.1.1.dev18/LICENSE +21 -0
- tourism_risk-0.1.1.dev18/PKG-INFO +313 -0
- tourism_risk-0.1.1.dev18/README.md +289 -0
- tourism_risk-0.1.1.dev18/pyproject.toml +66 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/__init__.py +73 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/individual.py +116 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/models.py +317 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/national.py +69 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/pipeline.py +91 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/presets.py +54 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/py.typed +0 -0
- tourism_risk-0.1.1.dev18/src/tourism_risk/regional.py +101 -0
- tourism_risk-0.1.1.dev18/tests/__init__.py +0 -0
- tourism_risk-0.1.1.dev18/tests/conftest.py +16 -0
- tourism_risk-0.1.1.dev18/tests/test_dataset.py +98 -0
- tourism_risk-0.1.1.dev18/tests/test_individual.py +95 -0
- tourism_risk-0.1.1.dev18/tests/test_national.py +118 -0
- tourism_risk-0.1.1.dev18/tests/test_pipeline.py +125 -0
- tourism_risk-0.1.1.dev18/tests/test_regional.py +123 -0
- tourism_risk-0.1.1.dev18/tests/test_rules.py +111 -0
- tourism_risk-0.1.1.dev18/tests/test_validation.py +274 -0
|
@@ -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
|