Spaces:
Running
on
Zero
Running
on
Zero
Update scoring_calculation_system.py
Browse files- scoring_calculation_system.py +405 -192
scoring_calculation_system.py
CHANGED
|
@@ -4,6 +4,7 @@ from breed_noise_info import breed_noise_info
|
|
| 4 |
|
| 5 |
@dataclass
|
| 6 |
class UserPreferences:
|
|
|
|
| 7 |
"""使用者偏好設定的資料結構"""
|
| 8 |
living_space: str # "apartment", "house_small", "house_large"
|
| 9 |
exercise_time: int # minutes per day
|
|
@@ -14,277 +15,489 @@ class UserPreferences:
|
|
| 14 |
space_for_play: bool
|
| 15 |
other_pets: bool
|
| 16 |
climate: str # "cold", "moderate", "hot"
|
| 17 |
-
health_sensitivity: str = "medium"
|
| 18 |
barking_acceptance: str = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
def __post_init__(self):
|
| 21 |
"""在初始化後運行,用於設置派生值"""
|
| 22 |
if self.barking_acceptance is None:
|
| 23 |
self.barking_acceptance = self.noise_tolerance
|
| 24 |
|
| 25 |
-
@staticmethod
|
| 26 |
-
def calculate_breed_bonus(breed_info: dict, user_prefs: 'UserPreferences') -> float:
|
| 27 |
-
"""計算品種額外加分"""
|
| 28 |
-
bonus = 0.0
|
| 29 |
-
|
| 30 |
-
# 壽命加分
|
| 31 |
-
try:
|
| 32 |
-
lifespan = breed_info.get('Lifespan', '10-12 years')
|
| 33 |
-
years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
|
| 34 |
-
longevity_bonus = min(0.05, (max(years) - 10) * 0.01)
|
| 35 |
-
bonus += longevity_bonus
|
| 36 |
-
except:
|
| 37 |
-
pass
|
| 38 |
-
|
| 39 |
-
# 性格特徵加分
|
| 40 |
-
temperament = breed_info.get('Temperament', '').lower()
|
| 41 |
-
if user_prefs.has_children:
|
| 42 |
-
if 'gentle' in temperament or 'patient' in temperament:
|
| 43 |
-
bonus += 0.03
|
| 44 |
-
|
| 45 |
-
# 適應性加分
|
| 46 |
-
if breed_info.get('Size') == "Small" and user_prefs.living_space == "apartment":
|
| 47 |
-
bonus += 0.02
|
| 48 |
-
|
| 49 |
-
return bonus
|
| 50 |
-
|
| 51 |
-
@staticmethod
|
| 52 |
-
def calculate_additional_factors(breed_info: dict, user_prefs: 'UserPreferences') -> dict:
|
| 53 |
-
"""計算額外的排序因素"""
|
| 54 |
-
factors = {
|
| 55 |
-
'versatility': 0.0,
|
| 56 |
-
'health_score': 0.0,
|
| 57 |
-
'adaptability': 0.0
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
# 計算多功能性分數
|
| 61 |
-
temperament = breed_info.get('Temperament', '').lower()
|
| 62 |
-
versatile_traits = ['intelligent', 'adaptable', 'versatile', 'trainable']
|
| 63 |
-
factors['versatility'] = sum(trait in temperament for trait in versatile_traits) / len(versatile_traits)
|
| 64 |
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
lifespan = breed_info.get('Lifespan', '10-12 years')
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
def calculate_compatibility_score(breed_info: dict, user_prefs: UserPreferences) -> dict:
|
| 86 |
-
"""
|
| 87 |
-
scores = {}
|
| 88 |
try:
|
| 89 |
-
|
| 90 |
-
|
|
|
|
| 91 |
base_scores = {
|
| 92 |
"Small": {"apartment": 0.95, "house_small": 1.0, "house_large": 0.90},
|
| 93 |
-
"Medium": {"apartment": 0.
|
| 94 |
-
"Large": {"apartment": 0.
|
| 95 |
"Giant": {"apartment": 0.15, "house_small": 0.55, "house_large": 1.0}
|
| 96 |
}
|
| 97 |
-
|
|
|
|
| 98 |
base_score = base_scores.get(size, base_scores["Medium"])[living_space]
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
if
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
adjustments += 0.05
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def calculate_exercise_score(breed_exercise_needs, user_exercise_time):
|
| 115 |
exercise_needs = {
|
| 116 |
-
'VERY HIGH': 120,
|
| 117 |
-
'HIGH': 90,
|
| 118 |
-
'MODERATE': 60,
|
| 119 |
-
'LOW': 30,
|
| 120 |
-
'VARIES': 60
|
| 121 |
}
|
| 122 |
-
|
| 123 |
-
breed_need = exercise_needs.get(
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
if
|
|
|
|
|
|
|
| 127 |
return 1.0
|
| 128 |
-
elif
|
| 129 |
-
return 0.
|
| 130 |
-
elif difference <= 0.4:
|
| 131 |
-
return 0.85
|
| 132 |
-
elif difference <= 0.6:
|
| 133 |
-
return 0.70
|
| 134 |
-
elif difference <= 0.8:
|
| 135 |
-
return 0.50
|
| 136 |
else:
|
| 137 |
-
return 0.
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
|
|
|
| 141 |
base_scores = {
|
| 142 |
"High": {"low": 0.3, "medium": 0.7, "high": 1.0},
|
| 143 |
"Moderate": {"low": 0.5, "medium": 0.9, "high": 1.0},
|
| 144 |
-
"Low": {"low": 1.0, "medium": 0.95, "high": 0.
|
| 145 |
}
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
return base_score
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
|
|
|
| 158 |
base_scores = {
|
| 159 |
-
"High": {"beginner": 0.
|
| 160 |
-
"Moderate": {"beginner": 0.
|
| 161 |
-
"Low": {"beginner": 0.
|
| 162 |
}
|
| 163 |
-
|
| 164 |
score = base_scores.get(care_level, base_scores["Moderate"])[user_experience]
|
| 165 |
-
|
|
|
|
| 166 |
temperament_lower = temperament.lower()
|
|
|
|
| 167 |
if user_experience == "beginner":
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
def calculate_health_score(breed_name: str) -> float:
|
|
|
|
| 176 |
if breed_name not in breed_health_info:
|
| 177 |
return 0.5
|
| 178 |
|
| 179 |
health_notes = breed_health_info[breed_name]['health_notes'].lower()
|
| 180 |
-
|
| 181 |
-
#
|
| 182 |
severe_conditions = [
|
| 183 |
-
'
|
| 184 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
]
|
| 186 |
-
|
| 187 |
-
#
|
| 188 |
moderate_conditions = [
|
| 189 |
-
'allergies',
|
| 190 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
]
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
severe_count = sum(1 for condition in severe_conditions if condition in health_notes)
|
| 194 |
moderate_count = sum(1 for condition in moderate_conditions if condition in health_notes)
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
health_score -= (severe_count * 0.
|
| 198 |
-
health_score -= (moderate_count * 0.
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
| 203 |
health_score *= 0.9
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
|
| 209 |
-
return max(0.
|
| 210 |
|
| 211 |
def calculate_noise_score(breed_name: str, user_noise_tolerance: str) -> float:
|
|
|
|
| 212 |
if breed_name not in breed_noise_info:
|
| 213 |
return 0.5
|
| 214 |
|
| 215 |
noise_info = breed_noise_info[breed_name]
|
| 216 |
noise_level = noise_info['noise_level'].lower()
|
| 217 |
-
|
| 218 |
|
| 219 |
# 基礎噪音分數矩陣
|
| 220 |
-
|
| 221 |
-
'low': {'low': 1.0, 'medium': 0.
|
| 222 |
-
'medium': {'low': 0.7, 'medium': 1.0, 'high': 0.
|
| 223 |
-
'high': {'low': 0.4, 'medium': 0.7, 'high': 1.0}
|
|
|
|
| 224 |
}
|
| 225 |
|
| 226 |
-
#
|
| 227 |
-
base_score =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
#
|
| 230 |
special_adjustments = 0
|
| 231 |
-
if
|
|
|
|
|
|
|
| 232 |
special_adjustments -= 0.1
|
| 233 |
-
if user_prefs.living_space == 'apartment':
|
| 234 |
-
if noise_level == 'high':
|
| 235 |
-
special_adjustments -= 0.15
|
| 236 |
-
elif noise_level == 'medium':
|
| 237 |
-
special_adjustments -= 0.05
|
| 238 |
|
| 239 |
-
final_score = base_score + special_adjustments
|
| 240 |
-
|
|
|
|
| 241 |
|
| 242 |
# 計算所有基礎分數
|
| 243 |
scores = {
|
| 244 |
-
'space': calculate_space_score(
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
'health': calculate_health_score(breed_info.get('Breed', '')),
|
| 249 |
'noise': calculate_noise_score(breed_info.get('Breed', ''), user_prefs.noise_tolerance)
|
| 250 |
}
|
| 251 |
|
| 252 |
-
#
|
| 253 |
weights = {
|
| 254 |
-
'space': 0.
|
| 255 |
-
'exercise': 0.
|
| 256 |
-
'grooming': 0.
|
| 257 |
-
'experience': 0.
|
| 258 |
-
'health': 0.
|
| 259 |
-
'noise': 0.
|
| 260 |
}
|
| 261 |
|
| 262 |
-
#
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
adjustments += 0.02
|
| 273 |
-
|
| 274 |
-
# 2. 氣候相容性
|
| 275 |
-
if user_prefs.climate in breed_info.get('Suitable Climate', '').split(','):
|
| 276 |
-
adjustments += 0.02
|
| 277 |
-
|
| 278 |
-
# 3. 其他寵物相容性
|
| 279 |
-
if user_prefs.other_pets and breed_info.get('Good with Other Pets') == 'Yes':
|
| 280 |
-
adjustments += 0.02
|
| 281 |
-
|
| 282 |
-
final_score = min(1.0, max(0, base_score + adjustments))
|
| 283 |
-
scores['overall'] = round(final_score, 4)
|
| 284 |
|
| 285 |
# 四捨五入所有分數
|
| 286 |
-
for
|
| 287 |
-
|
| 288 |
|
| 289 |
return scores
|
| 290 |
|
|
|
|
| 4 |
|
| 5 |
@dataclass
|
| 6 |
class UserPreferences:
|
| 7 |
+
|
| 8 |
"""使用者偏好設定的資料結構"""
|
| 9 |
living_space: str # "apartment", "house_small", "house_large"
|
| 10 |
exercise_time: int # minutes per day
|
|
|
|
| 15 |
space_for_play: bool
|
| 16 |
other_pets: bool
|
| 17 |
climate: str # "cold", "moderate", "hot"
|
| 18 |
+
health_sensitivity: str = "medium"
|
| 19 |
barking_acceptance: str = None
|
| 20 |
+
time_away: str = "moderate" # 離家時間,影響某些品種的適合度
|
| 21 |
+
physical_activity: str = "moderate" # 活動類型,影響運動需求匹配
|
| 22 |
+
training_commitment: str = "moderate" # 訓練投入,影響經驗需求
|
| 23 |
+
children_age: str = None
|
| 24 |
|
| 25 |
def __post_init__(self):
|
| 26 |
"""在初始化後運行,用於設置派生值"""
|
| 27 |
if self.barking_acceptance is None:
|
| 28 |
self.barking_acceptance = self.noise_tolerance
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
@staticmethod
|
| 32 |
+
def calculate_breed_bonus(breed_info: dict, user_prefs: 'UserPreferences') -> float:
|
| 33 |
+
"""計算品種額外加分"""
|
| 34 |
+
bonus = 0.0
|
| 35 |
+
temperament = breed_info.get('Temperament', '').lower()
|
| 36 |
+
|
| 37 |
+
# 1. 壽命加分(最高0.05)
|
| 38 |
+
try:
|
| 39 |
lifespan = breed_info.get('Lifespan', '10-12 years')
|
| 40 |
+
years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
|
| 41 |
+
longevity_bonus = min(0.05, (max(years) - 10) * 0.01)
|
| 42 |
+
bonus += longevity_bonus
|
| 43 |
+
except:
|
| 44 |
+
pass
|
| 45 |
+
|
| 46 |
+
# 2. 性格特徵加分(最高0.15)
|
| 47 |
+
positive_traits = {
|
| 48 |
+
'friendly': 0.05,
|
| 49 |
+
'gentle': 0.05,
|
| 50 |
+
'patient': 0.05,
|
| 51 |
+
'intelligent': 0.04,
|
| 52 |
+
'adaptable': 0.04,
|
| 53 |
+
'affectionate': 0.04,
|
| 54 |
+
'easy-going': 0.03,
|
| 55 |
+
'calm': 0.03
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
negative_traits = {
|
| 59 |
+
'aggressive': -0.08,
|
| 60 |
+
'stubborn': -0.06,
|
| 61 |
+
'dominant': -0.06,
|
| 62 |
+
'aloof': -0.04,
|
| 63 |
+
'nervous': -0.05,
|
| 64 |
+
'protective': -0.04
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
personality_score = sum(value for trait, value in positive_traits.items() if trait in temperament)
|
| 68 |
+
personality_score += sum(value for trait, value in negative_traits.items() if trait in temperament)
|
| 69 |
+
bonus += max(-0.15, min(0.15, personality_score))
|
| 70 |
+
|
| 71 |
+
# 3. 適應性加分(最高0.1)
|
| 72 |
+
adaptability_bonus = 0.0
|
| 73 |
+
if breed_info.get('Size') == "Small" and user_prefs.living_space == "apartment":
|
| 74 |
+
adaptability_bonus += 0.05
|
| 75 |
+
if 'adaptable' in temperament or 'versatile' in temperament:
|
| 76 |
+
adaptability_bonus += 0.05
|
| 77 |
+
bonus += min(0.1, adaptability_bonus)
|
| 78 |
+
|
| 79 |
+
# 4. 家庭相容性(最高0.1)
|
| 80 |
+
if user_prefs.has_children:
|
| 81 |
+
family_traits = {
|
| 82 |
+
'good with children': 0.06,
|
| 83 |
+
'patient': 0.05,
|
| 84 |
+
'gentle': 0.05,
|
| 85 |
+
'tolerant': 0.04,
|
| 86 |
+
'playful': 0.03
|
| 87 |
+
}
|
| 88 |
+
unfriendly_traits = {
|
| 89 |
+
'aggressive': -0.08,
|
| 90 |
+
'nervous': -0.07,
|
| 91 |
+
'protective': -0.06,
|
| 92 |
+
'territorial': -0.05
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# 年齡評估這樣能更細緻
|
| 96 |
+
age_adjustments = {
|
| 97 |
+
'toddler': {'bonus_mult': 0.7, 'penalty_mult': 1.3},
|
| 98 |
+
'school_age': {'bonus_mult': 1.0, 'penalty_mult': 1.0},
|
| 99 |
+
'teenager': {'bonus_mult': 1.2, 'penalty_mult': 0.8}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
adj = age_adjustments.get(user_prefs.children_age,
|
| 103 |
+
{'bonus_mult': 1.0, 'penalty_mult': 1.0})
|
| 104 |
+
|
| 105 |
+
family_bonus = sum(value for trait, value in family_traits.items()
|
| 106 |
+
if trait in temperament) * adj['bonus_mult']
|
| 107 |
+
family_penalty = sum(value for trait, value in unfriendly_traits.items()
|
| 108 |
+
if trait in temperament) * adj['penalty_mult']
|
| 109 |
+
|
| 110 |
+
bonus += min(0.15, max(-0.2, family_bonus + family_penalty))
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# 5. 專門技能加分(最高0.1)
|
| 114 |
+
skill_bonus = 0.0
|
| 115 |
+
special_abilities = {
|
| 116 |
+
'working': 0.03,
|
| 117 |
+
'herding': 0.03,
|
| 118 |
+
'hunting': 0.03,
|
| 119 |
+
'tracking': 0.03,
|
| 120 |
+
'agility': 0.02
|
| 121 |
+
}
|
| 122 |
+
for ability, value in special_abilities.items():
|
| 123 |
+
if ability in temperament.lower():
|
| 124 |
+
skill_bonus += value
|
| 125 |
+
bonus += min(0.1, skill_bonus)
|
| 126 |
+
|
| 127 |
+
return min(0.5, max(-0.25, bonus))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def calculate_additional_factors(breed_info: dict, user_prefs: 'UserPreferences') -> dict:
|
| 132 |
+
"""計算額外的評估因素"""
|
| 133 |
+
factors = {
|
| 134 |
+
'versatility': 0.0, # 多功能性
|
| 135 |
+
'trainability': 0.0, # 可訓練度
|
| 136 |
+
'energy_level': 0.0, # 能量水平
|
| 137 |
+
'grooming_needs': 0.0, # 美容需求
|
| 138 |
+
'social_needs': 0.0, # 社交需求
|
| 139 |
+
'weather_adaptability': 0.0 # 氣候適應性
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
temperament = breed_info.get('Temperament', '').lower()
|
| 143 |
+
size = breed_info.get('Size', 'Medium')
|
| 144 |
+
|
| 145 |
+
# 1. 多功能性評估
|
| 146 |
+
versatile_traits = ['intelligent', 'adaptable', 'trainable', 'athletic']
|
| 147 |
+
working_roles = ['working', 'herding', 'hunting', 'sporting', 'companion']
|
| 148 |
+
|
| 149 |
+
trait_score = sum(0.2 for trait in versatile_traits if trait in temperament)
|
| 150 |
+
role_score = sum(0.2 for role in working_roles if role in breed_info.get('Description', '').lower())
|
| 151 |
+
|
| 152 |
+
factors['versatility'] = min(1.0, trait_score + role_score)
|
| 153 |
+
|
| 154 |
+
# 2. 可訓練度評估
|
| 155 |
+
trainable_traits = {
|
| 156 |
+
'intelligent': 0.3,
|
| 157 |
+
'eager to please': 0.3,
|
| 158 |
+
'trainable': 0.2,
|
| 159 |
+
'quick learner': 0.2
|
| 160 |
+
}
|
| 161 |
+
factors['trainability'] = min(1.0, sum(value for trait, value in trainable_traits.items()
|
| 162 |
+
if trait in temperament))
|
| 163 |
+
|
| 164 |
+
# 3. 能量水平評估
|
| 165 |
+
exercise_needs = breed_info.get('Exercise Needs', 'MODERATE').upper()
|
| 166 |
+
energy_levels = {
|
| 167 |
+
'VERY HIGH': 1.0,
|
| 168 |
+
'HIGH': 0.8,
|
| 169 |
+
'MODERATE': 0.6,
|
| 170 |
+
'LOW': 0.4,
|
| 171 |
+
'VARIES': 0.6
|
| 172 |
+
}
|
| 173 |
+
factors['energy_level'] = energy_levels.get(exercise_needs, 0.6)
|
| 174 |
+
|
| 175 |
+
# 4. 美容需求評估
|
| 176 |
+
grooming_needs = breed_info.get('Grooming Needs', 'MODERATE').upper()
|
| 177 |
+
grooming_levels = {
|
| 178 |
+
'HIGH': 1.0,
|
| 179 |
+
'MODERATE': 0.6,
|
| 180 |
+
'LOW': 0.3
|
| 181 |
+
}
|
| 182 |
+
coat_penalty = 0.2 if any(term in breed_info.get('Description', '').lower()
|
| 183 |
+
for term in ['long coat', 'double coat']) else 0
|
| 184 |
+
factors['grooming_needs'] = min(1.0, grooming_levels.get(grooming_needs, 0.6) + coat_penalty)
|
| 185 |
+
|
| 186 |
+
# 5. 社交需求評估
|
| 187 |
+
social_traits = ['friendly', 'social', 'affectionate', 'people-oriented']
|
| 188 |
+
antisocial_traits = ['independent', 'aloof', 'reserved']
|
| 189 |
+
|
| 190 |
+
social_score = sum(0.25 for trait in social_traits if trait in temperament)
|
| 191 |
+
antisocial_score = sum(-0.2 for trait in antisocial_traits if trait in temperament)
|
| 192 |
+
factors['social_needs'] = min(1.0, max(0.0, social_score + antisocial_score))
|
| 193 |
+
|
| 194 |
+
# 6. 氣候適應性評估
|
| 195 |
+
climate_terms = {
|
| 196 |
+
'cold': ['thick coat', 'winter', 'cold climate'],
|
| 197 |
+
'hot': ['short coat', 'warm climate', 'heat tolerant'],
|
| 198 |
+
'moderate': ['adaptable', 'all climate']
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
climate_matches = sum(1 for term in climate_terms[user_prefs.climate]
|
| 202 |
+
if term in breed_info.get('Description', '').lower())
|
| 203 |
+
factors['weather_adaptability'] = min(1.0, climate_matches * 0.3 + 0.4) # 基礎分0.4
|
| 204 |
+
|
| 205 |
+
return factors
|
| 206 |
|
| 207 |
|
| 208 |
def calculate_compatibility_score(breed_info: dict, user_prefs: UserPreferences) -> dict:
|
| 209 |
+
"""計算品種與使用者條件的相容性分數的優化版本"""
|
|
|
|
| 210 |
try:
|
| 211 |
+
def calculate_space_score(size: str, living_space: str, has_yard: bool, exercise_needs: str) -> float:
|
| 212 |
+
"""空間分數計算"""
|
| 213 |
+
# 基礎空間需求矩陣
|
| 214 |
base_scores = {
|
| 215 |
"Small": {"apartment": 0.95, "house_small": 1.0, "house_large": 0.90},
|
| 216 |
+
"Medium": {"apartment": 0.60, "house_small": 0.90, "house_large": 1.0},
|
| 217 |
+
"Large": {"apartment": 0.30, "house_small": 0.75, "house_large": 1.0},
|
| 218 |
"Giant": {"apartment": 0.15, "house_small": 0.55, "house_large": 1.0}
|
| 219 |
}
|
| 220 |
+
|
| 221 |
+
# 取得基礎分數
|
| 222 |
base_score = base_scores.get(size, base_scores["Medium"])[living_space]
|
| 223 |
+
|
| 224 |
+
# 運動需求調整
|
| 225 |
+
exercise_adjustments = {
|
| 226 |
+
"Very High": -0.15 if living_space == "apartment" else 0,
|
| 227 |
+
"High": -0.10 if living_space == "apartment" else 0,
|
| 228 |
+
"Moderate": 0,
|
| 229 |
+
"Low": 0.05 if living_space == "apartment" else 0
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
adjustments = exercise_adjustments.get(exercise_needs.strip(), 0)
|
| 233 |
+
|
| 234 |
+
# 院子獎勵
|
| 235 |
+
if has_yard and size in ["Large", "Giant"]:
|
| 236 |
+
adjustments += 0.10
|
| 237 |
+
elif has_yard:
|
| 238 |
adjustments += 0.05
|
| 239 |
+
|
| 240 |
+
return min(1.0, max(0.1, base_score + adjustments))
|
| 241 |
|
| 242 |
+
def calculate_exercise_score(breed_needs: str, user_time: int) -> float:
|
| 243 |
+
"""運動需求計算"""
|
| 244 |
+
# 更精確的運動需求定義
|
|
|
|
| 245 |
exercise_needs = {
|
| 246 |
+
'VERY HIGH': {'min': 120, 'ideal': 150, 'max': 180},
|
| 247 |
+
'HIGH': {'min': 90, 'ideal': 120, 'max': 150},
|
| 248 |
+
'MODERATE': {'min': 45, 'ideal': 60, 'max': 90},
|
| 249 |
+
'LOW': {'min': 20, 'ideal': 30, 'max': 45},
|
| 250 |
+
'VARIES': {'min': 30, 'ideal': 60, 'max': 90}
|
| 251 |
}
|
| 252 |
+
|
| 253 |
+
breed_need = exercise_needs.get(breed_needs.strip().upper(), exercise_needs['MODERATE'])
|
| 254 |
+
|
| 255 |
+
# 計算匹配度
|
| 256 |
+
if user_time >= breed_need['ideal']:
|
| 257 |
+
if user_time > breed_need['max']:
|
| 258 |
+
return 0.9 # 稍微降分,因為可能過度運動
|
| 259 |
return 1.0
|
| 260 |
+
elif user_time >= breed_need['min']:
|
| 261 |
+
return 0.8 + (user_time - breed_need['min']) / (breed_need['ideal'] - breed_need['min']) * 0.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
else:
|
| 263 |
+
return max(0.3, 0.8 * (user_time / breed_need['min']))
|
| 264 |
|
| 265 |
+
def calculate_grooming_score(breed_needs: str, user_commitment: str, breed_size: str) -> float:
|
| 266 |
+
"""美容需求計算"""
|
| 267 |
+
# 基礎分數矩陣
|
| 268 |
base_scores = {
|
| 269 |
"High": {"low": 0.3, "medium": 0.7, "high": 1.0},
|
| 270 |
"Moderate": {"low": 0.5, "medium": 0.9, "high": 1.0},
|
| 271 |
+
"Low": {"low": 1.0, "medium": 0.95, "high": 0.8}
|
| 272 |
}
|
| 273 |
+
|
| 274 |
+
# 取得基礎分數
|
| 275 |
+
base_score = base_scores.get(breed_needs, base_scores["Moderate"])[user_commitment]
|
| 276 |
+
|
| 277 |
+
# 體型影響調整
|
| 278 |
+
size_adjustments = {
|
| 279 |
+
"Large": {"low": -0.2, "medium": -0.1, "high": 0},
|
| 280 |
+
"Giant": {"low": -0.3, "medium": -0.15, "high": 0},
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
if breed_size in size_adjustments:
|
| 284 |
+
adjustment = size_adjustments[breed_size].get(user_commitment, 0)
|
| 285 |
+
base_score = max(0.2, base_score + adjustment)
|
| 286 |
+
|
| 287 |
return base_score
|
| 288 |
|
| 289 |
+
def calculate_experience_score(care_level: str, user_experience: str, temperament: str) -> float:
|
| 290 |
+
"""飼養經驗需求計算"""
|
| 291 |
+
# 降低初學者的基礎分數
|
| 292 |
base_scores = {
|
| 293 |
+
"High": {"beginner": 0.15, "intermediate": 0.70, "advanced": 1.0},
|
| 294 |
+
"Moderate": {"beginner": 0.40, "intermediate": 0.85, "advanced": 1.0},
|
| 295 |
+
"Low": {"beginner": 0.75, "intermediate": 0.95, "advanced": 1.0}
|
| 296 |
}
|
| 297 |
+
|
| 298 |
score = base_scores.get(care_level, base_scores["Moderate"])[user_experience]
|
| 299 |
+
|
| 300 |
+
# 擴展性格特徵評估
|
| 301 |
temperament_lower = temperament.lower()
|
| 302 |
+
|
| 303 |
if user_experience == "beginner":
|
| 304 |
+
# 增加更多特徵評估
|
| 305 |
+
difficult_traits = {
|
| 306 |
+
'stubborn': -0.12,
|
| 307 |
+
'independent': -0.10,
|
| 308 |
+
'dominant': -0.10,
|
| 309 |
+
'strong-willed': -0.08,
|
| 310 |
+
'protective': -0.06,
|
| 311 |
+
'energetic': -0.05
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
easy_traits = {
|
| 315 |
+
'gentle': 0.06,
|
| 316 |
+
'friendly': 0.06,
|
| 317 |
+
'eager to please': 0.06,
|
| 318 |
+
'patient': 0.05,
|
| 319 |
+
'adaptable': 0.05,
|
| 320 |
+
'calm': 0.04
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
# 更精確的特徵影響計算
|
| 324 |
+
temperament_adjustments = sum(value for trait, value in easy_traits.items() if trait in temperament_lower)
|
| 325 |
+
temperament_adjustments += sum(value for trait, value in difficult_traits.items() if trait in temperament_lower)
|
| 326 |
+
|
| 327 |
+
# 品種特定調整
|
| 328 |
+
if "terrier" in breed_info['Description'].lower():
|
| 329 |
+
temperament_adjustments -= 0.1 # 梗類犬對新手不友善
|
| 330 |
+
|
| 331 |
+
final_score = max(0.2, min(1.0, score + temperament_adjustments))
|
| 332 |
+
return final_score
|
| 333 |
|
| 334 |
def calculate_health_score(breed_name: str) -> float:
|
| 335 |
+
"""計算品種健康分數"""
|
| 336 |
if breed_name not in breed_health_info:
|
| 337 |
return 0.5
|
| 338 |
|
| 339 |
health_notes = breed_health_info[breed_name]['health_notes'].lower()
|
| 340 |
+
|
| 341 |
+
# 嚴重健康問題(降低0.15分)
|
| 342 |
severe_conditions = [
|
| 343 |
+
'hip dysplasia',
|
| 344 |
+
'heart disease',
|
| 345 |
+
'progressive retinal atrophy',
|
| 346 |
+
'bloat',
|
| 347 |
+
'epilepsy',
|
| 348 |
+
'degenerative myelopathy',
|
| 349 |
+
'von willebrand disease'
|
| 350 |
]
|
| 351 |
+
|
| 352 |
+
# 中度健康問題(降低0.1分)
|
| 353 |
moderate_conditions = [
|
| 354 |
+
'allergies',
|
| 355 |
+
'eye problems',
|
| 356 |
+
'joint problems',
|
| 357 |
+
'hypothyroidism',
|
| 358 |
+
'ear infections',
|
| 359 |
+
'skin issues'
|
| 360 |
+
]
|
| 361 |
+
|
| 362 |
+
# 輕微健康問題(降低0.05分)
|
| 363 |
+
minor_conditions = [
|
| 364 |
+
'dental issues',
|
| 365 |
+
'weight gain tendency',
|
| 366 |
+
'minor allergies',
|
| 367 |
+
'seasonal allergies'
|
| 368 |
]
|
| 369 |
|
| 370 |
+
# 計算基礎健康分數
|
| 371 |
+
health_score = 1.0
|
| 372 |
+
|
| 373 |
+
# 根據問題嚴重程度扣分
|
| 374 |
severe_count = sum(1 for condition in severe_conditions if condition in health_notes)
|
| 375 |
moderate_count = sum(1 for condition in moderate_conditions if condition in health_notes)
|
| 376 |
+
minor_count = sum(1 for condition in minor_conditions if condition in health_notes)
|
| 377 |
+
|
| 378 |
+
health_score -= (severe_count * 0.15)
|
| 379 |
+
health_score -= (moderate_count * 0.1)
|
| 380 |
+
health_score -= (minor_count * 0.05)
|
| 381 |
+
|
| 382 |
+
# 壽命影響
|
| 383 |
+
try:
|
| 384 |
+
lifespan = breed_health_info[breed_name].get('average_lifespan', '10-12')
|
| 385 |
+
years = float(lifespan.split('-')[0])
|
| 386 |
+
if years < 8:
|
| 387 |
health_score *= 0.9
|
| 388 |
+
elif years > 13:
|
| 389 |
+
health_score *= 1.1
|
| 390 |
+
except:
|
| 391 |
+
pass
|
| 392 |
|
| 393 |
+
# 特殊健康優勢
|
| 394 |
+
if 'generally healthy' in health_notes or 'hardy breed' in health_notes:
|
| 395 |
+
health_score *= 1.1
|
| 396 |
|
| 397 |
+
return max(0.2, min(1.0, health_score))
|
| 398 |
|
| 399 |
def calculate_noise_score(breed_name: str, user_noise_tolerance: str) -> float:
|
| 400 |
+
"""計算品種噪音分數"""
|
| 401 |
if breed_name not in breed_noise_info:
|
| 402 |
return 0.5
|
| 403 |
|
| 404 |
noise_info = breed_noise_info[breed_name]
|
| 405 |
noise_level = noise_info['noise_level'].lower()
|
| 406 |
+
noise_notes = noise_info['noise_notes'].lower()
|
| 407 |
|
| 408 |
# 基礎噪音分數矩陣
|
| 409 |
+
base_scores = {
|
| 410 |
+
'low': {'low': 1.0, 'medium': 0.9, 'high': 0.8},
|
| 411 |
+
'medium': {'low': 0.7, 'medium': 1.0, 'high': 0.9},
|
| 412 |
+
'high': {'low': 0.4, 'medium': 0.7, 'high': 1.0},
|
| 413 |
+
'varies': {'low': 0.6, 'medium': 0.8, 'high': 0.9}
|
| 414 |
}
|
| 415 |
|
| 416 |
+
# 獲取基礎分數
|
| 417 |
+
base_score = base_scores.get(noise_level, {'low': 0.7, 'medium': 0.8, 'high': 0.6})[user_noise_tolerance]
|
| 418 |
+
|
| 419 |
+
# 吠叫原因評估
|
| 420 |
+
barking_reasons_penalty = 0
|
| 421 |
+
problematic_triggers = [
|
| 422 |
+
('separation anxiety', -0.15),
|
| 423 |
+
('excessive barking', -0.12),
|
| 424 |
+
('territorial', -0.08),
|
| 425 |
+
('alert barking', -0.05),
|
| 426 |
+
('attention seeking', -0.05)
|
| 427 |
+
]
|
| 428 |
+
|
| 429 |
+
for trigger, penalty in problematic_triggers:
|
| 430 |
+
if trigger in noise_notes:
|
| 431 |
+
barking_reasons_penalty += penalty
|
| 432 |
+
|
| 433 |
+
# 可訓練性補償
|
| 434 |
+
trainability_bonus = 0
|
| 435 |
+
if 'responds well to training' in noise_notes:
|
| 436 |
+
trainability_bonus = 0.1
|
| 437 |
+
elif 'can be trained' in noise_notes:
|
| 438 |
+
trainability_bonus = 0.05
|
| 439 |
|
| 440 |
+
# 特殊情況
|
| 441 |
special_adjustments = 0
|
| 442 |
+
if 'rarely barks' in noise_notes:
|
| 443 |
+
special_adjustments += 0.1
|
| 444 |
+
if 'howls' in noise_notes and user_noise_tolerance == 'low':
|
| 445 |
special_adjustments -= 0.1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
|
| 447 |
+
final_score = base_score + barking_reasons_penalty + trainability_bonus + special_adjustments
|
| 448 |
+
|
| 449 |
+
return max(0.2, min(1.0, final_score))
|
| 450 |
|
| 451 |
# 計算所有基礎分數
|
| 452 |
scores = {
|
| 453 |
+
'space': calculate_space_score(
|
| 454 |
+
breed_info['Size'],
|
| 455 |
+
user_prefs.living_space,
|
| 456 |
+
user_prefs.space_for_play,
|
| 457 |
+
breed_info.get('Exercise Needs', 'Moderate')
|
| 458 |
+
),
|
| 459 |
+
'exercise': calculate_exercise_score(
|
| 460 |
+
breed_info.get('Exercise Needs', 'Moderate'),
|
| 461 |
+
user_prefs.exercise_time
|
| 462 |
+
),
|
| 463 |
+
'grooming': calculate_grooming_score(
|
| 464 |
+
breed_info.get('Grooming Needs', 'Moderate'),
|
| 465 |
+
user_prefs.grooming_commitment.lower(),
|
| 466 |
+
breed_info['Size']
|
| 467 |
+
),
|
| 468 |
+
'experience': calculate_experience_score(
|
| 469 |
+
breed_info.get('Care Level', 'Moderate'),
|
| 470 |
+
user_prefs.experience_level,
|
| 471 |
+
breed_info.get('Temperament', '')
|
| 472 |
+
),
|
| 473 |
'health': calculate_health_score(breed_info.get('Breed', '')),
|
| 474 |
'noise': calculate_noise_score(breed_info.get('Breed', ''), user_prefs.noise_tolerance)
|
| 475 |
}
|
| 476 |
|
| 477 |
+
# 優化權重配置
|
| 478 |
weights = {
|
| 479 |
+
'space': 0.28,
|
| 480 |
+
'exercise': 0.18,
|
| 481 |
+
'grooming': 0.12,
|
| 482 |
+
'experience': 0.22,
|
| 483 |
+
'health': 0.12,
|
| 484 |
+
'noise': 0.08
|
| 485 |
}
|
| 486 |
|
| 487 |
+
# 計算加權總分
|
| 488 |
+
weighted_score = sum(score * weights[category] for category, score in scores.items())
|
| 489 |
+
|
| 490 |
+
# 擴大分數差異
|
| 491 |
+
def amplify_score(score):
|
| 492 |
+
# 使用指數函數擴大差異
|
| 493 |
+
amplified = pow((score - 0.5) * 2, 3) / 8 + score
|
| 494 |
+
return max(0.65, min(0.95, amplified)) # 限制在65%-95%範圍內
|
| 495 |
+
|
| 496 |
+
final_score = amplify_score(weighted_score)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
|
| 498 |
# 四捨五入所有分數
|
| 499 |
+
scores = {k: round(v, 4) for k, v in scores.items()}
|
| 500 |
+
scores['overall'] = round(final_score, 4)
|
| 501 |
|
| 502 |
return scores
|
| 503 |
|