特征工程中的分位数(Quantile)处理:大规模数据下的近似算法与实现

特征工程中的分位数(Quantile)处理:大规模数据下的近似算法与实现

大家好,今天我们来聊聊特征工程中一个非常重要且常见的技术:分位数处理,以及在大规模数据场景下的近似算法与实现。分位数在数据分析和特征工程中扮演着关键角色,能够帮助我们理解数据的分布情况,识别异常值,并构建更有价值的特征。但当数据量达到TB甚至PB级别时,精确计算分位数变得非常耗时,甚至不可行。因此,我们需要借助近似算法来高效地估计分位数。

1. 分位数的基本概念与应用

分位数是指将数据集分成相等大小的若干份的数值点。例如,四分位数将数据分成四等份,百分位数将数据分成一百等份。常用的分位数包括:

  • 最小值 (Minimum): 0% 分位数
  • 下四分位数 (Q1, First Quartile): 25% 分位数
  • 中位数 (Median, Q2, Second Quartile): 50% 分位数
  • 上四分位数 (Q3, Third Quartile): 75% 分位数
  • 最大值 (Maximum): 100% 分位数

分位数在特征工程中的应用非常广泛:

  • 异常值检测: 通过比较数据点与分位数之间的距离,可以识别潜在的异常值。例如,可以使用 IQR (Interquartile Range) 方法,将小于 Q1 – 1.5 IQR 或大于 Q3 + 1.5 IQR 的值视为异常值。
  • 数据分箱: 将连续变量根据分位数划分为离散的区间,例如,可以将年龄分成 "年轻"、"中年"、"老年" 三个年龄段,每个年龄段包含相同数量的人群。
  • 特征缩放: 使用分位数进行特征缩放,例如,可以使用 RobustScaler,它通过减去中位数并除以四分位距 (IQR) 来缩放数据,对异常值具有较好的鲁棒性。
  • 特征衍生: 基于分位数可以衍生出新的特征,例如,计算每个数据点与中位数的差值,或者计算数据点属于哪个分位数区间。

2. 精确计算分位数的局限性

计算精确分位数最常见的方法是:

  1. 对数据进行排序。
  2. 根据所需的分位数,找到对应位置的值。

例如,要计算一个包含 N 个元素的数组的 p% 分位数,需要找到排序后的数组中第 (p/100) * N 个元素。

这种方法的时间复杂度为 O(N log N),主要开销在于排序步骤。当数据量非常大时,排序所需的内存和计算资源将变得非常昂贵,甚至超过了单台机器的承受能力。此外,如果数据是流式的,需要不断地更新分位数,则每次更新都需要重新排序,效率非常低。

3. 大规模数据下的分位数近似算法

为了解决大规模数据下精确计算分位数的难题,我们需要使用近似算法。这些算法通常牺牲一定的精度,以换取更高的效率和更低的资源消耗。常见的近似算法包括:

  • 随机抽样 (Random Sampling): 从大数据集中随机抽取一小部分样本,然后基于这些样本计算分位数。这种方法简单易懂,但精度较低,尤其是在数据集分布不均匀时。

    import numpy as np
    
    def quantile_random_sampling(data, quantile, sample_size):
        """
        使用随机抽样近似计算分位数
    
        Args:
            data: 包含数据的列表或 NumPy 数组
            quantile: 要计算的分位数,例如 0.5 表示中位数
            sample_size: 随机抽取的样本大小
    
        Returns:
            近似的分位数
        """
        sample = np.random.choice(data, size=sample_size, replace=False)
        return np.quantile(sample, quantile)
    
    # 示例
    data = np.random.rand(1000000)
    quantile = 0.5
    sample_size = 10000
    approximate_median = quantile_random_sampling(data, quantile, sample_size)
    print(f"近似中位数:{approximate_median}")
  • 直方图 (Histogram): 将数据划分为若干个桶 (bucket),然后统计每个桶中的数据个数。基于直方图可以近似计算分位数。直方图的精度取决于桶的数量,桶的数量越多,精度越高,但内存消耗也越大。

    import numpy as np
    
    def quantile_histogram(data, quantile, num_buckets):
        """
        使用直方图近似计算分位数
    
        Args:
            data: 包含数据的列表或 NumPy 数组
            quantile: 要计算的分位数,例如 0.5 表示中位数
            num_buckets: 直方图的桶的数量
    
        Returns:
            近似的分位数
        """
        counts, bin_edges = np.histogram(data, bins=num_buckets)
        cumulative_counts = np.cumsum(counts)
        total_count = cumulative_counts[-1]
        target_count = quantile * total_count
    
        # 找到目标桶
        for i, count in enumerate(cumulative_counts):
            if count >= target_count:
                break
    
        # 在目标桶内进行线性插值
        if i == 0:
            lower_bound = bin_edges[0]
            upper_bound = bin_edges[1]
            proportion = target_count / counts[i]
        else:
            lower_bound = bin_edges[i]
            upper_bound = bin_edges[i+1]
            proportion = (target_count - cumulative_counts[i-1]) / counts[i]
    
        return lower_bound + proportion * (upper_bound - lower_bound)
    
    # 示例
    data = np.random.rand(1000000)
    quantile = 0.5
    num_buckets = 100
    approximate_median = quantile_histogram(data, quantile, num_buckets)
    print(f"近似中位数:{approximate_median}")
  • T-Digest (Tree Digest): T-Digest 是一种基于树结构的算法,能够以较小的内存消耗和较高的精度近似计算分位数。T-Digest 的核心思想是将数据压缩成一组簇 (cluster),每个簇包含若干个数据点,并记录簇的中心和权重。查询分位数时,通过遍历树结构,找到包含目标分位数的簇,然后进行插值计算。

    import tdigest
    import numpy as np
    
    def quantile_tdigest(data, quantile):
        """
        使用 T-Digest 近似计算分位数
    
        Args:
            data: 包含数据的列表或 NumPy 数组
            quantile: 要计算的分位数,例如 0.5 表示中位数
    
        Returns:
            近似的分位数
        """
        td = tdigest.TDigest()
        td.batch_update(data)
        return td.quantile(quantile)
    
    # 示例
    data = np.random.rand(1000000)
    quantile = 0.5
    approximate_median = quantile_tdigest(data, quantile)
    print(f"近似中位数:{approximate_median}")
  • QuantileDigest: 类似于 T-Digest, 也是维护数据概要,用于快速分位数估计。

  • Greenwald-Khanna Algorithm (GK Algorithm): GK 算法是一种流式算法,能够在有限的内存空间内维护一组数据概要,并随时更新分位数估计。GK 算法的核心思想是维护一组 ε-近似分位数,其中 ε 是用户指定的精度参数。

    import numpy as np
    
    class GKQuantiles:
        """
        Greenwald-Khanna (GK) Algorithm for approximate quantile computation.
        """
        def __init__(self, epsilon):
            self.epsilon = epsilon  # Accuracy parameter
            self.tuples = []        # Stores tuples (v_i, g_i, d_i)
            self.n = 0              # Total number of elements processed
    
        def update(self, value):
            """
            Updates the quantile summary with a new value.
            """
            self.n += 1
            if not self.tuples:
                self.tuples.append((value, 1, 0))
                return
    
            # Find the correct position to insert the new value
            i = 0
            while i < len(self.tuples) and self.tuples[i][0] < value:
                i += 1
    
            if i == 0:
                g = 1
                d = 0
            elif i == len(self.tuples):
                g = 1
                d = 0
            else:
                g = 1
                d = self.tuples[i-1][1] + self.tuples[i-1][2]
    
            self.tuples.insert(i, (value, g, d))
    
            self.compress()
    
        def compress(self):
            """
            Compresses the quantile summary by merging tuples when possible.
            """
            if len(self.tuples) < 2:
                return
    
            i = 0
            while i < len(self.tuples) - 1:
                if self.tuples[i][1] + self.tuples[i+1][1] + self.tuples[i+1][2] <= 2 * self.epsilon * self.n:
                    self.tuples[i] = (self.tuples[i][0], self.tuples[i][1] + self.tuples[i+1][1], self.tuples[i+1][2])
                    del self.tuples[i+1]
                else:
                    i += 1
    
        def query(self, quantile):
            """
            Queries the approximate value of the given quantile.
    
            Args:
                quantile: The quantile to query (e.g., 0.5 for the median).
    
            Returns:
                The approximate value of the quantile.
            """
            if not 0 <= quantile <= 1:
                raise ValueError("Quantile must be between 0 and 1.")
    
            if not self.tuples:
                return None  # No data yet
    
            rank = quantile * self.n
    
            min_rank = 0
            max_rank = 0
            value = None
    
            for i in range(len(self.tuples)):
                v_i, g_i, d_i = self.tuples[i]
                max_rank = min_rank + g_i
    
                if min_rank <= rank <= max_rank:
                    value = v_i
                    break
    
                min_rank += g_i + d_i
    
            if value is None:
                return self.tuples[-1][0] #return the last element
    
            return value
    
    # Example Usage:
    data = np.random.rand(10000)
    epsilon = 0.01  # Adjust for desired accuracy
    gk = GKQuantiles(epsilon)
    
    for value in data:
        gk.update(value)
    
    approx_median = gk.query(0.5)
    print(f"Approximate Median (GK): {approx_median}")

4. 算法选择与评估

选择哪种近似算法取决于具体的应用场景和需求。需要综合考虑以下因素:

  • 数据量: 数据量越大,越需要选择内存消耗低的算法,例如 T-Digest 或 GK 算法。
  • 数据分布: 数据分布不均匀时,随机抽样的精度会受到影响,可以考虑使用 T-Digest 或 GK 算法。
  • 精度要求: 对精度要求越高,需要选择精度更高的算法,并调整算法的参数。
  • 更新频率: 如果数据是流式的,需要不断地更新分位数,则需要选择流式算法,例如 GK 算法。
  • 计算资源: 计算资源有限时,需要选择计算复杂度低的算法。

为了评估近似算法的性能,可以使用以下指标:

  • 误差率: 计算近似分位数与精确分位数之间的误差。
  • 内存消耗: 测量算法所需的内存空间。
  • 计算时间: 测量算法的运行时间。

可以使用真实数据集或模拟数据集进行实验,比较不同算法的性能。

5. 分布式环境下的分位数计算

在大规模分布式环境下,可以将数据分成多个分区,并在每个分区上独立计算分位数。然后,将各个分区的分位数结果进行合并,得到全局的分位数估计。

常见的分布式分位数计算方法包括:

  • MapReduce: 使用 MapReduce 框架,将数据分成多个 Map 任务,每个 Map 任务计算局部直方图或局部 T-Digest。然后,使用 Reduce 任务将局部结果进行合并,得到全局直方图或全局 T-Digest。

  • Spark: 使用 Spark 框架,可以更方便地进行分布式数据处理。Spark 提供了 approxQuantile 函数,可以直接计算近似分位数。

    from pyspark.sql import SparkSession
    import numpy as np
    
    # 创建 SparkSession
    spark = SparkSession.builder.appName("QuantileExample").getOrCreate()
    
    # 创建 RDD
    data = np.random.rand(100000)
    rdd = spark.sparkContext.parallelize(data)
    
    # 计算近似分位数
    quantile = 0.5
    approximate_median = rdd.map(lambda x: float(x)).approxQuantile(quantile, 0.01) # 0.01 is relative error
    print(f"Spark 近似中位数:{approximate_median}")
    
    # 关闭 SparkSession
    spark.stop()
  • Flink: Flink 也是一个流行的分布式计算框架,可以用于流式和批处理。 可以使用 Flink 的 quantile aggregation function,但是需要自己实现 approximate 的逻辑,例如基于 histogram 或者 T-Digest。

6. 代码实现注意事项

在实际应用中,需要注意以下代码实现细节:

  • 数据类型: 确保数据类型正确,避免精度损失。
  • 参数调整: 根据数据量和精度要求,合理调整算法的参数。
  • 异常处理: 处理数据中的缺失值和异常值。
  • 代码优化: 使用高效的数据结构和算法,优化代码性能。
  • 并发安全: 在多线程或分布式环境下,确保代码的并发安全。

7. 总结:选择合适的算法,关注精度与效率的平衡

我们讨论了分位数的基本概念和应用,以及在大规模数据下近似计算分位数的各种算法。 选择合适的算法需要在精度和效率之间找到平衡点,并根据具体的应用场景和数据特征进行调整。此外,我们还介绍了分布式环境下的分位数计算方法,以及代码实现时需要注意的细节。 掌握这些知识,能够帮助我们更好地处理大规模数据,构建更有效的特征,并提升机器学习模型的性能。

8. 快速估计:近似算法的选择与权衡

不同的近似算法在精度、效率和内存消耗方面各有优劣。需要根据具体情况选择合适的算法,并在实际应用中进行评估和调整。

9. 分布式计算:应对海量数据的有效策略

分布式计算能够将大规模数据分解成多个小块,并行处理,从而加速分位数的计算过程。充分利用分布式框架,可以有效地应对海量数据的挑战。

10. 实践细节:代码优化与参数调整

代码实现中的细节问题,例如数据类型、参数调整和异常处理,都会影响算法的性能和精度。在实践中需要关注这些细节,并进行优化。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注