Помоги мне, пожалуйста, со следующим заданием - генерацией синтетического потока данных. Сейчас объясню, какие у меня требования:
Нужна генерация по 2 типа наборов данных для каждого из распределений (Равномерное распределение, Нормальное распределение, Распределение Ципфа): без удаления ключей, которые были в потоке и с удалением добавленных ранее значений.
Количество элементов в потоке должно быть порядка 10^5, значения должны быть в диапазоне от 1 до 10^6.
Сами функции для распределений у меня уже есть:
def linear_congruential_generator(a, c, m, seed, size, lower_bound, upper_bound):
    numbers = []
    current_seed = seed
    
    for _ in range(size):
        current_seed = (a * current_seed + c) % m # [0...m)
        random_number = current_seed / m # [0...1)
        # to range [lower_bound, upper_bound]
        scaled_number = lower_bound + random_number * (upper_bound - lower_bound)
        numbers.append((scaled_number))
        
    return numbers

def generate_uniform_random_numbers(size=100000, lower_bound=1, upper_bound=1000000):
    # linear congruential generator parameters
    a = 1664525    # 48271
    c = 1013904223 # 0
    m = 2**32
    
    seed = 12345  
    
    return linear_congruential_generator(a, c, m, seed, size, lower_bound, upper_bound)

def generate_normal_random_numbers(size, mean, std_dev):
    uniform_random_pairs = generate_uniform_random_numbers(size * 2, lower_bound=0, upper_bound=1)
    random_numbers = []

    for i in range(0, size * 2, 2):
        x = uniform_random_pairs[i]  # First uniform random number
        y = uniform_random_pairs[i + 1]  # Second uniform random number

        # Applying the Box-Muller transform
        z0 = np.cos(2 * np.pi * x) * np.sqrt(-2 * np.log(y))
        z1 = np.sin(2 * np.pi * x) * np.sqrt(-2 * np.log(y))

        # Transform z0 and z1 to match the normal distribution
        random_numbers.append(mean + std_dev * z0)
        random_numbers.append(mean + std_dev * z1)

    return random_numbers[:size]

def generate_zipf_random_numbers(size, N, s):
    # Calculate H(N, s)
    H_N_s = sum(1 / (n ** s) for n in range(1, N + 1))

    # Compute cumulative probabilities
    cumulative_probabilities = np.zeros(N)
    cumulative_probabilities[0] = 1 / (1 ** s) / H_N_s  # f(1)
    
    for k in range(2, N + 1):
        cumulative_probabilities[k - 1] = cumulative_probabilities[k - 2] + (1 / (k ** s)) / H_N_s

    # Generate uniform random numbers between 0 and 1
    uniform_randoms = generate_uniform_random_numbers(size=size, lower_bound=0, upper_bound=1)
    #uniform_randoms = np.random.uniform(0, 1, size)

    # convert uniformly distributed random numbers into samples that follow the Zipf distribution
    samples = np.searchsorted(cumulative_probabilities, uniform_randoms) + 1

    return samples

Каким образом нужно делать удаление? Я, честно говоря, сам не до конца понимаю, но у меня есть некоторые пояснения от моего знакомого. Сейчас я тебе их предоставлю в виде диалога:
A: "А как при генерации наборов данных вы делали удаление ключей? Как выбирали, какой ключ удалять?"
B: "Я выбирал равновероятно ключ из тех, где на данный момент больше 0 элементов"
A: "А с какой частотой удалял?"
B: "Как параметр задавал вероятность удаления. Например, 0.01. Нам поступают числа, мы хотим в какой-то момент удаление осуществить. Как это сделать? Можно считать сколько раз значение появилось в потоке (в виде словаря, например), это значение поступает, умноженное на -1 и мы уменьшаем счётчик в словаре. Другими словами, сгенерированный поток одним из распределений даёт нам числа, с какой-то частотой мы можем брать и умножать значения на -1, проверять, было ли оно вообще в потоке, и если было, то уменьшать счётчик"
Надеюсь, приведённый диалог поможет тебе понять как именно нужно делать это задание, потому что я не очень понимаю