问题的产生

我的health-care健康饮食管理系统在使用过程中逐渐产生了一个问题,那就是随着用户摄入食物项目的添加,食物数目会变得更多用户需要寻找很久才能找到匹配的食物选项。同时由于页面简约性的要求,以及对于移动端设备支持的友好性方面考虑我想应该可以用一个类似计算机cpu调度方面的简单算法来解决这个问题。为了实现这一目标,我设计了一个基于频次和时间间隔的排序算法,使得推荐系统不仅了解用户的即时需求,也能通过历史选择为用户提供合理的推荐。

算法的需求

  • 基于频次的考虑:如果你经常选择某个食物,系统会优先推荐它,因为频繁的选择暗示了你对这个食物的偏好。
  • 基于时间间隔的考虑:如果某个食物已经很久没有被你选中了,系统会降低它的推荐优先级,避免向你推荐那些你可能已经不感兴趣的食物。

于是可以设计一个这样的函数:

priority_score=frequency×(1+time_weight_factordays_since_last_selected+1)\text{priority\_score} = \text{frequency} \times \left(1 + \frac{\text{time\_weight\_factor}}{\text{days\_since\_last\_selected} + 1}\right)

公式的组成部分

frequency(频次):
这是用户选择某个食物的次数。假设你频繁选择某个食物,它的频次就会较高。频次越高,意味着这个食物更符合用户的长期偏好,因此它对最终优先级的贡献也会越大。

days_since_last_selected(自上次选择以来的天数):
这是当前日期与用户最后一次选择该食物的日期之间的天数差。这个数值越大,意味着食物越久未被选中。一般来说,如果一个食物长时间未被选择,我们可能希望它的优先级会降低(除非用户有意想要重新尝试的需求)。

time_weight_factor(时间权重因子):
这个值决定了时间因素在最终优先级中的影响力。它是一个调整系数,影响时间间隔对食物优先级的贡献。数值越大,表示时间的影响越大,即食物选择的时间间隔对优先级的影响会更强。

公式的逻辑:

1+time weight factordays since last selected+11 + \frac{\text{time\ weight\ factor}}{\text{days\ since\ last\ selected} + 1}

这一部分是计算时间间隔对优先级的影响。假设食物的选择频次是固定的,这一项会根据食物的上次选择时间进行调整。

days since last selected+1\text{days\ since\ last\ selected} + 1

确保即使食物的选择频次是近期的,也不会造成除零错误。time_weight_factor 放大了时间间隔的影响,使得长期未被选择的食物得分明显降低。乘以 frequency:最后,我们将频次与时间因子相乘,最终的优先级分数既考虑了食物的选择频次,也考虑了其历史选择的时间间隔。

设置合适的 time_weight_factor 取决于你的应用场景以及用户的行为特征。以下是不同情境下的建议:

  • 注重近期行为的场景
    如果你希望系统更重视用户近期的选择习惯,并且优先推荐用户最近选择的食物,那么可以选择较小的 time_weight_factor。例如:适合值:5-15 效果:时间间隔对优先级的影响较小,频次的影响占主导地位,推荐频繁选择的食物。用户的历史选择不久的食物将优先推荐,忽略较久未选择的食物。

  • 注重长期偏好的场景
    如果你希望推荐系统更注重用户的长期偏好(例如,帮助用户重新开始选择长期未选过的食物),可以将 time_weight_factor 设置得较大。这样,时间间隔的影响会显著地增加,较久未选择的食物将得到更多的优先推荐。适合值:20-50。效果:时间间隔对优先级的影响增大,推荐系统会更倾向于推荐久未选择的食物,即使频次较低。

  • 平衡近期和长期行为的场景
    如果你希望在推荐中平衡近期选择与长期偏好的影响,可以选择一个中等的 time_weight_factor,例如 10-20。这样既能考虑用户最近的选择行为,也不会忽略那些久未选择的食物。适合值:10-20。效果:推荐系统在近期行为与长期偏好之间找到一个平衡,频次和时间间隔的影响都得到合理的考虑。

例子:优先级得分计算

假设我们有两个食物项 A 和 B,我们将根据以下公式来计算它们的优先级得分:

食物 A:

  • frequency = 10
  • days_since_last_selected = 5
  • time_weight_factor = 10

计算公式:

priority_score_A=10×(1+105+1)=10×(1+106)=10×2.6667=26.67\text{priority\_score\_A} = 10 \times \left(1 + \frac{10}{5 + 1}\right) = 10 \times \left(1 + \frac{10}{6}\right) = 10 \times 2.6667 = 26.67

食物 B:

  • frequency = 5
  • days_since_last_selected = 30
  • time_weight_factor = 10

计算公式:

priority_score_B=5×(1+1030+1)=5×(1+1031)=5×1.3226=6.61\text{priority\_score\_B} = 5 \times \left(1 + \frac{10}{30 + 1}\right) = 5 \times \left(1 + \frac{10}{31}\right) = 5 \times 1.3226 = 6.61

结果分析:

  • 食物 A 的优先级得分为 26.67,食物 B 的优先级得分为 6.61。
  • 尽管食物 B 的选择时间间隔较长,但因为食物 A 的选择频次较高,所以它的优先级得分更大。

通过这个例子,我们可以看到,优先级得分不仅考虑了用户过去的选择频次,还考虑了时间因素,从而可以更好地平衡用户的长期偏好和即时需求。

总结

  • 用户偏好动态调整
    饮食偏好会随着时间变化,但用户对特定食物的频繁选择也反映了偏好。基于时间和频次的优先级算法可以结合用户的选择频次和时间衰减,更贴近他们当前的饮食习惯。在该算法下,用户最近选择的食物优先级更高,但不会完全忽视过去的偏好记录。

  • 避免频次累积带来的偏好失真
    简单的频次统计会导致一些被高频次选择过的食物始终在推荐前列,即使用户不再偏爱它们。时间加权可以很好地解决此问题,因为每次用户选择食物时,都会根据距离上次选择时间的远近动态调整优先级,从而更加灵活。

  • 兼顾用户的饮食多样性
    当用户尝试新食物或一段时间未选择某食物时,优先级会自动下调,让用户有机会接触到更多不同的食物。这种机制有助于引导用户保持均衡饮食。

  • 实现难度适中
    基于时间和频次的优先级算法实现起来相对简单,只需在数据库中多存储一个“上次选择日期”,并在推荐时加权计算。相比LRU缓存算法等实现逻辑更简单,对系统的性能要求也不会过高。

实现代码

# user_item_frequency表
CREATE TABLE IF NOT EXISTS user_item_frequency (
user_id INTEGER,
item_id INTEGER,
frequency INTEGER DEFAULT 1,
last_selected DATE,
PRIMARY KEY (user_id, item_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (item_id) REFERENCES intake_items(id))
# intake_items
CREATE TABLE IF NOT EXISTS intake_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
calories_per_unit REAL NOT NULL,
pending_name TEXT,
pending_calories_per_unit REAL,
is_changed INTEGER NOT NULL DEFAULT 0,
is_approved INTEGER NOT NULL DEFAULT 0)

# 排序算法获取食物列表
def get_priority_sorted_items(user_id, time_weight_factor=10):
conn = get_db_connection()
cursor = conn.cursor()

# 获取用户的所有食物频次记录
cursor.execute('''
SELECT item_id, frequency, last_selected
FROM user_item_frequency
WHERE user_id = ?
''', (user_id,))

items = cursor.fetchall()

priority_items = []
for item_id, frequency, last_selected in items:
# 将 last_selected 从字符串转换为 datetime.date 对象
last_selected_date = datetime.strptime(last_selected, "%Y-%m-%d").date()

days_since_last_selected = (datetime.now().date() - last_selected_date).days
priority_score = frequency * (1 + time_weight_factor / (days_since_last_selected + 1)) # 防止除零
priority_items.append((item_id, priority_score))

# 按优先级得分从高到低排序
priority_items.sort(key=lambda x: x[1], reverse=True)

# 提取排序后的 item_id 列表
sorted_item_ids = [item[0] for item in priority_items]

conn.close()
return sorted_item_ids