| name | skin-matching |
| description | 皮肤双胞胎匹配算法,找到相似皮肤用户并推荐其有效产品。实现社区匹配功能时使用此技能。 |
皮肤匹配技能
概述
通过皮肤指纹向量匹配相似用户,推荐他们验证有效的产品。
皮肤指纹设计
struct SkinFingerprint: Codable {
let skinType: SkinType
let ageRange: AgeRange
let gender: Gender?
let region: String?
let issueVector: [Double]
var vector: [Double] {
var v: [Double] = []
v.append(contentsOf: skinType.oneHot)
v.append(ageRange.normalized)
v.append(contentsOf: issueVector)
return v
}
}
enum SkinType: String, Codable, CaseIterable {
case dry, oily, combination, sensitive
var oneHot: [Double] {
Self.allCases.map { $0 == self ? 1.0 : 0.0 }
}
}
enum AgeRange: String, Codable {
case under20, age20to25, age25to30, age30to35, age35to40, over40
var normalized: Double {
switch self {
case .under20: return 0.1
case .age20to25: return 0.25
case .age25to30: return 0.4
case .age30to35: return 0.55
case .age35to40: return 0.7
case .over40: return 0.85
}
}
}
相似度计算
class SkinMatcher {
func cosineSimilarity(_ a: [Double], _ b: [Double]) -> Double {
guard a.count == b.count, !a.isEmpty else { return 0 }
let dotProduct = zip(a, b).map(*).reduce(0, +)
let magnitudeA = sqrt(a.map { $0 * $0 }.reduce(0, +))
let magnitudeB = sqrt(b.map { $0 * $0 }.reduce(0, +))
guard magnitudeA > 0, magnitudeB > 0 else { return 0 }
return dotProduct / (magnitudeA * magnitudeB)
}
func weightedSimilarity(user: SkinFingerprint, other: SkinFingerprint) -> Double {
let basicSimilarity = cosineSimilarity(user.vector, other.vector)
let skinTypeBonus = user.skinType == other.skinType ? 0.2 : -0.3
let ageDiff = abs(user.ageRange.normalized - other.ageRange.normalized)
let ageBonus = ageDiff < 0.2 ? 0.1 : 0
return min(1.0, max(0, basicSimilarity + skinTypeBonus + ageBonus))
}
func findSkinTwins(
for user: SkinFingerprint,
from pool: [UserProfile],
limit: Int = 10
) -> [SkinTwin] {
pool
.map { profile -> SkinTwin in
let similarity = weightedSimilarity(user: user, other: profile.fingerprint)
return SkinTwin(
userId: profile.id,
similarity: similarity,
skinProfile: profile.anonymized,
effectiveProducts: profile.effectiveProducts
)
}
.filter { $0.similarity >= 0.6 }
.sorted { $0.similarity > $1.similarity }
.prefix(limit)
.map { $0 }
}
}
匹配结果模型
struct SkinTwin: Identifiable {
let id = UUID()
let userId: String
let similarity: Double
let skinProfile: AnonymousProfile
let effectiveProducts: [EffectiveProduct]
var matchLevel: MatchLevel {
switch similarity {
case 0.9...: return .twin
case 0.8..<0.9: return .verySimilar
case 0.7..<0.8: return .similar
default: return .somewhatSimilar
}
}
}
enum MatchLevel: String {
case twin = "皮肤双胞胎 👯"
case verySimilar = "非常相似"
case similar = "相似"
case somewhatSimilar = "有点相似"
}
struct AnonymousProfile: Codable {
let skinType: SkinType
let ageRange: String
let mainConcerns: [SkinConcern]
let region: String?
}
struct EffectiveProduct: Identifiable {
let id: String
let product: Product
let usageDuration: Int
let improvementPercent: Double
let verifiedAt: Date
}
推荐分数计算
struct ProductRecommendationScore {
let product: Product
let score: Double
let reasons: [String]
static func calculate(
product: Product,
userFingerprint: SkinFingerprint,
skinTwins: [SkinTwin]
) -> ProductRecommendationScore {
var score: Double = 0
var reasons: [String] = []
let relevantTwins = skinTwins.filter { twin in
twin.effectiveProducts.contains { $0.product.id == product.id }
}
if !relevantTwins.isEmpty {
let weightedEffectiveness = relevantTwins.reduce(0.0) { sum, twin in
let effectiveness = twin.effectiveProducts
.first { $0.product.id == product.id }?
.improvementPercent ?? 0
return sum + twin.similarity * effectiveness
} / relevantTwins.reduce(0.0) { $0 + $1.similarity }
score += weightedEffectiveness * 0.4
reasons.append("\(relevantTwins.count)位相似用户验证有效")
}
let ingredientMatch = calculateIngredientMatch(product, userFingerprint)
score += ingredientMatch * 0.3
if ingredientMatch > 0.7 {
reasons.append("成分适合你的肤质")
}
let concernMatch = calculateConcernMatch(product, userFingerprint)
score += concernMatch * 0.2
if concernMatch > 0.7 {
reasons.append("针对你的皮肤问题")
}
let riskPenalty = calculateRiskPenalty(product, userFingerprint)
score -= riskPenalty * 0.1
if riskPenalty > 0.3 {
reasons.append("⚠️ 部分成分可能刺激")
}
return ProductRecommendationScore(
product: product,
score: min(1.0, max(0, score)),
reasons: reasons
)
}
}
数据贡献机制
struct DataContribution {
func contribute(
session: TrackingSession,
report: TrackingReport,
consentLevel: ConsentLevel
) {
switch consentLevel {
case .anonymous:
uploadAnonymousStats(report)
case .pseudonymous:
uploadForMatching(session, report)
case .public:
uploadPublic(session, report)
case .none:
break
}
}
enum ConsentLevel {
case anonymous
case pseudonymous
case `public`
case none
}
}
验证