【问题标题】:Changing proportion of agreeing values in numpy arrays改变 numpy 数组中一致值的比例
【发布时间】:2021-05-25 20:54:40
【问题描述】:

我有一个问题一直在努力思考。假设我有一个看起来像这样的 numpy 数组(在实际实现中,len(array) 将在 4500 左右):

array = np.repeat([0, 1, 2], 2)
array >> [0, 0, 1, 1, 2, 2]

据此,我正在尝试生成一个新的(打乱的)数组,其中随机与array 一致的值的比例是特定比例p。所以让我们说p = .5。然后,一个示例新数组将类似于

    array = [0, 0, 1, 1, 2, 2]
new_array = [0, 1, 2, 1, 0, 2]

您可以看到,new_array 中的值恰好有 50% 与 array 中的值一致。输出数组的要求是:

np.count_nonzero(array - new_array) / len(array) = p,和
set(np.unique(array)) == set(np.unique(new_array))

“同意”是指array[i] == new_array[i] 同意索引inew_array 中的所有值都应该与array 相同,只是打乱了。

我确信有一种优雅的方式可以做到这一点——有人能想到什么吗?

谢谢!

【问题讨论】:

  • 根据索引同意?
  • 你说的同意是什么意思?这有点模糊。
  • @wwii “同意”,我的意思是 array[i] == new_array[i]
  • @Polkaguy6000 是的,没错。
  • 这很有趣,但我发现了一个问题,如果你有数组 [1,1,1,2] 并且所需的匹配率是 0.25,那么你的输出不可能是 [2,1,1,1](a .5 匹配)或[1,1,1,2](1.0 匹配)。

标签: python arrays numpy


【解决方案1】:

已编辑以包含不同且更准确的方法)

应该注意,并不是所有的混洗分数p(混洗元素的数量除以元素的总数)的值都是可访问的。 p 的可能值取决于输入的大小和重复元素的数量。

也就是说,我可以提出两种可能的方法:

  1. 将您的输入分成正确大小的 pinnedunpinned 索引,然后打乱 unpinned 索引。
import numpy as np


def partial_shuffle(arr, p=1.0):
    n = arr.size
    k = round(n * p)
    shuffling = np.arange(n)
    shuffled = np.random.choice(n, k, replace=False)
    shuffling[shuffled] = np.sort(shuffled)
    return arr[shuffling]

方法 (1) 的主要优点是可以使用np.random.choice() 和高级索引以矢量化形式轻松实现。 另一方面,只要您愿意接受某些洗牌可能会因为重复值或仅仅因为洗牌索引与未洗牌的索引意外重合而返回一些未洗牌的元素,则此方法效果很好。 这会导致p请求 值通常大于观察到的实际 值。 如果需要一个相对更准确的 p 值,可以尝试对 p 参数执行搜索,在输出中给出所需的值,或者通过反复试验。

  1. 实现Fisher-Yates shuffle 的变体,您可以:(a) 拒绝交换值相同的头寸,(b) 只选择尚未访问过的随机头寸进行交换。
def partial_shuffle_eff(arr, p=1.0, inplace=False, tries=2.0):
    if not inplace:
        arr = arr.copy()
    n = arr.size
    k = round(n * p)
    tries = round(n * tries)
    seen = set()
    i = l = t = 0
    while i < n and l < k:
        seen.add(i)
        j = np.random.randint(i, n)
        while j in seen and t < tries:
            j = np.random.randint(i, n)
            t += 1
        if arr[i] != arr[j]:
            arr[i], arr[j] = arr[j], arr[i]
            l += 2
            seen.add(j)
        while i in seen:
            i += 1
    return arr

虽然这种方法可以得到更准确的值p,但它仍然受到目标交换次数必须是偶数的限制。 此外,对于具有大量唯一性的输入,第二个 while (while j in seen ...) 可能是一个无限循环,因此应设置尝试次数上限。 最后,您需要使用显式循环,这会导致执行速度慢得多,除非您可以使用 Numba 的 JIT 编译,这会显着加快您的执行速度。

import numba as nb


partial_shuffle_eff_nb = nb.njit(partial_shuffle_eff)
partial_shuffle_eff_nb.__name__ = 'partial_shuffle_eff_nb'

为了测试部分改组的准确性,我们可以使用(百分比)Hamming distance

def hamming_distance(a, b):
    assert(a.shape == b.shape)
    return np.count_nonzero(a == b)


def percent_hamming_distance(a, b):
    return hamming_distance(a, b) / len(a)


def shuffling_fraction(a, b):
    return 1 - percent_hamming_distance(a, b)

我们可能会观察到类似这样的行为:

funcs = (
    partial_shuffle,
    partial_shuffle_eff,
    partial_shuffle_eff_nb
)

n = 12
m = 3
arrs = (
    np.zeros(n, dtype=int),
    np.arange(n),
    np.repeat(np.arange(m), n // m),
    np.repeat(np.arange(3), 2),
    np.repeat(np.arange(3), 3),
)

np.random.seed(0)
for arr in arrs:
    print(" " * 24, arr)
    for func in funcs:
        shuffled = func(arr, 0.5)
        print(f"{func.__name__:>24s}", shuffled, shuffling_fraction(arr, shuffled))
#                          [0 0 0 0 0 0 0 0 0 0 0 0]
#          partial_shuffle [0 0 0 0 0 0 0 0 0 0 0 0] 0.0
#      partial_shuffle_eff [0 0 0 0 0 0 0 0 0 0 0 0] 0.0
#   partial_shuffle_eff_nb [0 0 0 0 0 0 0 0 0 0 0 0] 0.0
#                          [ 0  1  2  3  4  5  6  7  8  9 10 11]
#          partial_shuffle [ 0  8  2  3  6  5  7  4  9  1 10 11] 0.5
#      partial_shuffle_eff [ 3  8 11  0  4  5  6  7  1  9 10  2] 0.5
#   partial_shuffle_eff_nb [ 9 10 11  3  4  5  6  7  8  0  1  2] 0.5
#                          [0 0 0 0 1 1 1 1 2 2 2 2]
#          partial_shuffle [0 0 2 0 1 2 1 1 2 2 1 0] 0.33333333333333337
#      partial_shuffle_eff [1 1 1 0 0 1 0 0 2 2 2 2] 0.5
#   partial_shuffle_eff_nb [1 2 1 0 1 0 0 1 0 2 2 2] 0.5
#                          [0 0 1 1 2 2]
#          partial_shuffle [0 0 1 1 2 2] 0.0
#      partial_shuffle_eff [1 1 0 0 2 2] 0.6666666666666667
#   partial_shuffle_eff_nb [1 2 0 1 0 2] 0.6666666666666667
#                          [0 0 0 1 1 1 2 2 2]
#          partial_shuffle [0 0 1 1 0 1 2 2 2] 0.2222222222222222
#      partial_shuffle_eff [0 1 2 1 0 1 2 2 0] 0.4444444444444444
#   partial_shuffle_eff_nb [0 0 1 0 2 1 2 1 2] 0.4444444444444444

或者,对于更接近您的用例的输入:

n = 4500
m = 3
arr = np.repeat(np.arange(m), n // m)

np.random.seed(0)
for func in funcs:
    shuffled = func(arr, 0.5)
    print(f"{func.__name__:>24s}", shuffling_fraction(arr, shuffled))
#          partial_shuffle 0.33777777777777773
#      partial_shuffle_eff 0.5
#   partial_shuffle_eff_nb 0.5

最后是一些小的基准测试:

n = 4500
m = 3
arr = np.repeat(np.arange(m), n // m)

np.random.seed(0)
for func in funcs:
    print(f"{func.__name__:>24s}", end=" ")
    %timeit func(arr, 0.5)
#          partial_shuffle  213 µs ± 6.36 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
#      partial_shuffle_eff  10.9 ms ± 194 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
#   partial_shuffle_eff_nb 172 µs ± 1.79 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

【讨论】:

    【解决方案2】:

    这是一个可能更容易理解的版本:

    import math, random
    
    # generate array of random values
    a = np.random.rand(4500)
    
    # make a utility list of every position in that array, and shuffle it
    indices = [i for i in range(0, len(a))]
    random.shuffle(indices)
    
    # set the proportion you want to keep the same
    proportion = 0.5
    
    # make two lists of indices, the ones that stay the same and the ones that get shuffled
    anchors = indices[0:math.floor(len(a)*proportion)]
    not_anchors = indices[math.floor(len(a)*proportion):]
    
    # get values of non-anchor indices, and shuffle them
    not_anchor_values = [a[i] for i in not_anchors]
    random.shuffle(not_anchor_values)
    
    # loop original array, if an anchor position, keep original value
    # if not an anchor, draw value from shuffle non-anchor value list and increment the count 
    final_list = []
    count = 0
    for e,i in enumerate(a):
        if e in not_anchors:
            final_list.append(i)
        else:
            final_list.append(not_anchor_values[count])
            count +=1
    
    # test proportion of matches and non-matches in output
    
    match = []
    not_match = []
    for e,i in enumerate(a):
        if i == final_list[e]:
            match.append(True)
        else:
            not_match.append(True)
    len(match)/(len(match)+len(not_match))
    

    代码中的注释解释了该方法。

    【讨论】:

    • 最终的测试可以更简洁地写成:match = [x == y for x, y in zip(a, b)]; sum(match) / len(a) 其中ab 分别是初始数组和部分洗牌的数组。
    【解决方案3】:

    你可以试试

    import random
    
    p = 0.5
    
    arr = np.array([0, 0, 1, 1, 2, 2])
    
    # number of similar elements required
    num_sim_element = round(len(arr)*p)
    
    # creating indeices of similar element
    hp = {}
    for i,e in enumerate(arr):
        if(e in hp):
            hp[e].append(i)
        else:
            hp[e] = [i]
    #print(hp)
    
    out_map = []
    
    k = list(hp.keys())
    v = list(hp.values())
    index = 0
    
    while(len(out_map) != num_sim_element):
        if(len(v[index]) > 0):
            k_ = k[index]
            random.shuffle(v[index])
            v_ = v[index].pop()
    
            out_map.append((k_,v_))
    
            index += 1
            index %= len(k)
    
    #print(out_map)
    
    out_unique = set([i[0] for i in out_map])
    out_indices = [i[-1] for i in out_map]
    
    out_arr = arr.copy()
    #for i in out_map:
    #    out_arr[i[-1]] = i[0]
    
    for i in set(range(len(arr))).difference(out_indices):
        out_arr[i] = random.choice(list(out_unique.difference([out_arr[i]])))
    
    
    print(arr)
    print(out_arr)
    
    assert 1 - (np.count_nonzero(arr - out_arr) / len(arr)) == p
    assert set(np.unique(arr)) == set(np.unique(out_arr))
    
    
    [0 0 1 1 2 2]
    [1 0 1 0 0 2]
    

    【讨论】:

    • 抱歉,有一件事我没有说明:我们正在改组arr,所以set(np.unique(arr)) == set(np.unique(out))。不要以为我对此很清楚。
    • set(np.unique(arr)) == set(np.unique(out)) 是必需的吗?
    • 是的,很抱歉最初没有指定!
    • @AlexL 你现在能查一下吗
    • 你的输入数组是什么?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-20
    • 1970-01-01
    相关资源
    最近更新 更多