Python - 根据时间信息(即时、间隔)计算项目之间的相似度

Python - Compute similarities between items based on temporal information (instant, interval)

我想根据时间信息计算项目 (0,1,2,3..) 之间的相似度。时间信息可以是时刻(开始日期)、时间间隔(开始日期、结束日期)或空值(NaT);请参阅下面的数据框示例 (df_for)。

  1. {instant,instant} : 如果等于 sim = 1,否则 sim =0
  2. {instant,null} 反之亦然,sim =0
  3. {instant, interval}: 如果在区间内有瞬间,sim =1 或者如果区间包含瞬间,sim = 1
  4. {interval,interval} :如果区间重叠,sim = 两个区间的交集/两个区间的并集
  5. {interval,interval} :如果一个区间包含另一个区间,则 sim = 1

下面的python代码从数据帧中获取时间信息并执行上述(1-5)的条件。代码很冗长,我想知道是否有一个聪明的 way/lib 使用 python.

来计算时间段和时间瞬间之间的相似性
m, k = df_for.shape
sim = np.zeros((m, m))
data = df_for.values
for i in range(m):
    for j in range(m):
        if i != j:
            st1 = data[i][0]
            ed1 = data[i][1]
            st2 = data[j][0]
            ed2 = data[j][1]
            #both items are null values
            if pd.isnull(st1) and pd.isnull(ed1) and pd.isnull(st2) and pd.isnull(ed2):
                sim[i][j] = 0.
            # {instant, instant} => equal, not equal
            if pd.notnull(st1) and pd.isnull(ed1) and pd.notnull(st2) and pd.isnull(ed2):
                if st1 == st2:
                    sim[i][j] = 1.
                else:
                    sim[i][j] = 0.
            # {instant, null} => sim is 0
            if pd.notnull(st1) and pd.isnull(ed1) and pd.isnull(st2) and pd.isnull(ed2):
                sim[i][j] = 0.
            # {instant, interval} => meets, during
            if pd.notnull(st1) and pd.isnull(ed1) and pd.notnull(st2) and pd.notnull(ed2):
                    if(st2 <= st1 <= ed2):
                        sim[i][j] = 1. #a time is between two other times
                    else:
                        sim[i][j] = 0.
            # {interval, instant} => meets, contains
            if pd.notnull(st1) and pd.notnull(ed1) and pd.notnull(st2) and pd.isnull(ed2):
                    if(st1 <= st2 <= ed1):
                        sim[i][j] = 1. #a time is between two other times
                    else:
                        sim[i][j] = 0.
            # {interval, interval} => equal, overlaps, not overlaps
            if pd.notnull(st1) and pd.notnull(ed1) and pd.notnull(st2) and pd.notnull(ed2): 
                if (st1 <= st2 <= ed1) or (st2 <= st1 <= ed2):
                    intersect = min(ed1,ed2)- max(st1,st2) # earliestend-lateststart
                    union = max(st1,st2,ed1,ed2) - min(ed1,ed2,st1,st2)
                    overlaps = intersect/union
                    #print(intersect/np.timedelta64(1, 'D'),union/np.timedelta64(1, 'D'))
                    if (st1 > st2 and ed1 < ed2) or (st1 < st2 and ed1 > ed2): # contains, during
                        overlaps = 1.0
                    sim[i][j]=overlaps  
                else:
                    sim[i][j] = 0.  
        else:
            sim[i][j] = 1.

这里不想给出完整的答案。 我会创建一个 class 来接收开始和结束时间,然后对构造函数执行所有 null not null 检查。 稍后 class 我将重载比较运算符。像 eq 代表 == 和 ne 代表 != 在重载时,您可以尝试返回整数值而不是 True 和 False。 有了所有这些设置,我可能会重写 即时(数据[i][0],数据[i][1])==即时(数据[j][0],数据[j][1])

并从那里得到你需要的东西

我可以看到几种简化代码的方法。

  1. 首先,我建议将比较代码移动到一个单独的函数中,该函数接受 st1ed1st2ed2作为参数。 (作为旁注,为什么 st (start time) 但 ed (end date)?一致的名称可能很好。)您将能够 return 您的结果到调用代码,调用代码将负责对结果数组进行赋值。

  2. 说到调用代码...它不需要为每一对时间范围调用比较函数。结果应该始终是对称的(例如,compare(data[i][0], data[i][1], data[j][0], data[j][1]) 将变为 return,与 compare(data[j][0], data[j][1], data[i][0], data[i][1]) 相同)。所以只需调用其中一个,并将结果分配给 sim[i][j]sim[j][i].

  3. 而不是很多 if some_test_expression: return 1; else: return 0 个块,只是 return 比较结果:return some_test_expression。 Python 的 bool 类型是 int 的子类,因此当您将它们分配给 numpy 数组时,它们应该自动转换为数字(您也可以显式转换为 float如果您希望函数始终 return 相同类型)。

  4. 处理所有顶部为空值的情况。它们是一个简单的案例,如果你先处理它们(和 return,所以函数的其余部分不会 运行),你不需要检查那么多的东西是否为 null之后。您不需要单独处理 null/null、null/instantand null/interval,如果任一起始值为 null,只需 return 零即可:if isnull(st1) or isnull(st2): return 0.

  5. Instant/interval 比较是对称的,所以只用一种方式编写逻辑并翻转参数,如果它们最初的顺序错误:if isnull(ed2): st1, ed1, st2, ed2 = st2, ed2, st1, ed1

  6. 我看不出有太多可以通过在瞬间和间隔之间或两个间隔之间进行比较的具体实现来改进(除了上面已经提到的一般事情)。但是,我想说你的重叠间隔公式在相似性上会有很大的不连续性,因为一个小间隔慢慢地与一个更大的间隔重叠,然后被完全包含。例如,在一小时间隔前一毫秒开始的一秒间隔将导致 0.0002770.999 / (3600 + 0.001))两者之间的相似值,但与较大的间隔确实会有 1.0 的相似性(很大的不同)。一种更连续变化的相似性度量方法是将重叠量除以较小间隔的大小(而不是并集的大小)。该公式不需要一个区间完全包含另一个区间的特殊情况,因为默认计算已经给出了 1.0 的相似度(重叠将是较小区间的大小,因此您将对其进行除法自己)。

因此,将所有这些放在一起,这就是我重写内容的方式:

def compare_intervals(st1, et1, st2, et2): # note I've renamed the ed's to et
    # all nulls
    if pd.isnull(st1) or pd.isnull(st2):
        return 0.

    # {instant, instant} => equal, not equal
    if pd.isnull(et1) and pd.isnull(et2):
        return float(st1 == st2)

    # {instant, interval} => flip params (to be handled in next block)
    if pd.isnull(et1):
        st1, et1, st2, et2 = st2, et2, st1, et1

    # {interval, instant} => meets, contains
    if pd.isnull(et2):
        return float(st1 <= st2 <= et1)

    # {interval, interval} => equal, overlaps, not overlaps
    if (st1 <= st2 <= et1) or (st2 <= st1 <= et2):
        intersect = min(et1, et2) - max(st1, st2)
        min_interval = min(et1 - st1, et2 - st2) # using minimum instead of union
        return intersect / min_interval

    return 0. # intervals didn't overlap

m, k = df_for.shape
sim = np.zeros((m, m))
data = df_for.values
for i in range(m):
    for j in range(i, m): # we only iterate on j values >= i
        if i == j:
            sim[i,j] = 1.
        else:
            sim[i,j] = sim[j,i] = compare_intervals(data[i][0], data[i][1],
                                                    data[j][0], data[j][1])

一些我无法放入代码注释中的注释:

  1. compare_interval 函数的最后一步不需要对值作为区间的测试,因为所有其他情况(涉及瞬间和空值)都已经处理。

  2. 我已经将对 sim 数组的赋值更改为使用元组索引,因为这比多维 numpy 数组的嵌套索引更自然(如果使用嵌套,则切片不起作用指数)。您也许可以对 data 查找执行相同的操作,但我没有更改它们,因为我对 pandas 的了解几乎不及我对 numpy 的了解。

  3. 说到我缺乏 pandas 知识,可能有一种方法可以将比较函数直接应用于数据帧本身的 "join"。我不知道该怎么做。如果它存在,它可能比我在调用代码中基本保持不变的嵌套循环更快(即使它没有优化对称情况)。