降采样算法:优化数据可视化的利器

1,377 阅读19分钟

前言

在处理和可视化大规模数据集时,降采样(Downsampling)算法可以显著提升性能和用户体验。本文将介绍几种常见的降采样算法,说明每种算法的适用场景,并提供相应的JavaScript实现。我们还将探讨每种算法的优缺点。

1. 常见的降采样算法

降采样算法旨在从大规模数据集中选择部分数据点,以减少数据量并提升可视化效率。常见的降采样算法包括:

  • 模式中位数分桶算法 (Mode Median Bucket Algorithm)
  • 最小标准误差分桶(Min-Std-Error-Bucket)
  • 最长线段分桶算法 (Longest Segment Bucket Algorithm)
  • 多边形逼近算法 (Polygon Approximation Algorithm)
  • Visvalingam–Whyatt Algorithm
  • 最大三角形单桶算法 (Largest-Triangle-One-Bucket Algorithm)
  • 最大三角形面积三桶算法(LTTB)
  • 动态最大三角形三桶算法 (Dynamic Largest-Triangle-Three-Buckets, DLTTB)
  • 随机采样(Random Sampling)
  • 均匀采样(Uniform Sampling)
  • 滑动窗口采样(Sliding Window Sampling)
  • 分层采样(Stratified Sampling)

2. 每种降采样算法的适用场景和实际例子

模式中位数分桶算法 (Mode Median Bucket Algorithm)

模式中位数分桶算法是一种降采样算法,用于减少大规模数据集的数量,同时保留数据的主要特征。该算法通过将数据分成多个桶,并在每个桶中选择具有代表性的点来实现数据简化。

主要步骤

  1. 分桶:将数据划分成固定数量的桶,每个桶包含相同数量的数据点。
  2. 计算模式:在每个桶中找到出现频率最高的值(模式)。
  3. 计算中位数:在每个桶中找到中位数值。
  4. 选择代表点:从每个桶中选择模式和中位数,或者根据具体需求选择一个最具代表性的点。

适用场景

该算法适用于需要减少数据量但仍需保留主要数据特征的场景,例如传感器数据展示、金融数据可视化等。

JavaScript实现

function modeMedianBucket(data, bucketSize) {
    if (data.length === 0) return [];

    const buckets = [];
    const sampledData = [];

    // 分桶
    for (let i = 0; i < data.length; i += bucketSize) {
        const bucket = data.slice(i, i + bucketSize);
        buckets.push(bucket);
    }

    // 处理每个桶
    buckets.forEach(bucket => {
        // 找到模式(出现频率最高的值)
        const frequencyMap = {};
        bucket.forEach(point => {
            const key = JSON.stringify(point);
            frequencyMap[key] = (frequencyMap[key] || 0) + 1;
        });

        let mode = null;
        let maxFrequency = -1;
        for (const key in frequencyMap) {
            if (frequencyMap[key] > maxFrequency) {
                maxFrequency = frequencyMap[key];
                mode = JSON.parse(key);
            }
        }

        // 找到中位数
        const sortedBucket = bucket.slice().sort((a, b) => a[1] - b[1]);
        const median = sortedBucket[Math.floor(sortedBucket.length / 2)];

        // 选择代表点(这里选择模式和中位数)
        sampledData.push(mode);
        sampledData.push(median);
    });

    return sampledData;
}

// 示例数据
const data = [
    [0, 10], [1, 20], [2, 15], [3, 30], [4, 25],
    [5, 20], [6, 15], [7, 30], [8, 25], [9, 20],
    [10, 30], [11, 25], [12, 20], [13, 35], [14, 30]
];

// 采样后的数据
const bucketSize = 5;
const sampledData = modeMedianBucket(data, bucketSize);
console.log(sampledData);

优缺点

优点:

  • 保留主要特征:通过选择模式和中位数,算法能够保留数据的主要特征。
  • 灵活性高:可以根据具体需求选择不同的代表点。

缺点:

  • 实现复杂:相对于简单的采样方法,实现较为复杂。
  • 计算成本较高:需要计算模式和中位数,计算成本较高。

最小标准误差分桶 (Min-Std-Error-Bucket)

最小标准误差分桶算法是一种降采样算法,用于在减少数据量的同时,尽可能保留数据的整体趋势和主要特征。该算法通过将数据划分成多个桶,并在每个桶中选择一个能使误差最小的点来代表整个桶的数据。

主要步骤:

  1. 分桶:将数据划分成固定数量的桶,每个桶包含相同数量的数据点。
  2. 计算误差:在每个桶中计算每个点与桶内其他点的标准误差。
  3. 选择最小误差的点:选择误差最小的点作为该桶的代表点。

适用场景

最小标准误差分桶算法适用于需要减少数据量并保留数据整体趋势的场景,例如时间序列数据可视化、传感器数据展示等。

JavaScript实现

function minStdErrorBucket(data, bucketSize) {
    if (data.length === 0) return [];

    const buckets = [];
    const sampledData = [];

    // 分桶
    for (let i = 0; i < data.length; i += bucketSize) {
        const bucket = data.slice(i, i + bucketSize);
        buckets.push(bucket);
    }

    // 处理每个桶
    buckets.forEach(bucket => {
        let minError = Infinity;
        let bestPoint;

        bucket.forEach(point => {
            // 计算误差
            const error = bucket.reduce((sum, p) => sum + Math.pow(p[1] - point[1], 2), 0) / bucket.length;
            
            // 找到误差最小的点
            if (error < minError) {
                minError = error;
                bestPoint = point;
            }
        });

        sampledData.push(bestPoint);
    });

    return sampledData;
}

// 示例数据
const data = [
    [0, 10], [1, 20], [2, 15], [3, 30], [4, 25],
    [5, 20], [6, 15], [7, 30], [8, 25], [9, 20],
    [10, 30], [11, 25], [12, 20], [13, 35], [14, 30]
];

// 采样后的数据
const bucketSize = 5;
const sampledData = minStdErrorBucket(data, bucketSize);
console.log(sampledData);

优缺点

优点

  • 保留主要特征:通过选择误差最小的点,能够有效保留数据的整体趋势。
  • 误差最小:相比于其他方法,该算法可以使采样后的数据误差最小。

缺点

  • 实现复杂:相对于简单的采样方法,实现较为复杂。
  • 计算成本高:需要计算每个点的误差,计算成本较高。

最长线段分桶算法 (Longest Segment Bucket Algorithm)

最长线段分桶算法是一种降采样算法,用于在减少数据量的同时,尽可能保留数据的整体趋势和主要特征。该算法通过将数据划分成多个桶,并在每个桶中选择一个点,使得所选点与前后点形成的线段最长,以此来保持数据的关键变化。

主要步骤:

  1. 分桶:将数据划分成固定数量的桶,每个桶包含相同数量的数据点。
  2. 计算线段长度:在每个桶中,计算每个点与前后点形成的线段长度。
  3. 选择最长线段的点:选择线段长度最大的点作为该桶的代表点。

适用场景

最长线段分桶算法适用于需要减少数据量并保留数据整体趋势的场景,例如时间序列数据可视化、传感器数据展示等。

JavaScript实现

function longestSegmentBucket(data, bucketSize) {
    if (data.length === 0) return [];

    const buckets = [];
    const sampledData = [];

    // 分桶
    for (let i = 0; i < data.length; i += bucketSize) {
        const bucket = data.slice(i, i + bucketSize);
        buckets.push(bucket);
    }

    // 处理每个桶
    for (let i = 0; i < buckets.length; i++) {
        const bucket = buckets[i];

        // 对于每个桶中的点,计算前后点形成的线段长度
        let maxSegmentLength = -1;
        let bestPoint = bucket[0];

        for (let j = 1; j < bucket.length - 1; j++) {
            const prevPoint = bucket[j - 1];
            const currPoint = bucket[j];
            const nextPoint = bucket[j + 1];

            // 计算线段长度
            const segmentLength = Math.sqrt(
                Math.pow(currPoint[0] - prevPoint[0], 2) + Math.pow(currPoint[1] - prevPoint[1], 2)
            ) + Math.sqrt(
                Math.pow(nextPoint[0] - currPoint[0], 2) + Math.pow(nextPoint[1] - currPoint[1], 2)
            );

            // 找到线段长度最大的点
            if (segmentLength > maxSegmentLength) {
                maxSegmentLength = segmentLength;
                bestPoint = currPoint;
            }
        }

        sampledData.push(bestPoint);
    }

    return sampledData;
}

// 示例数据
const data = [
    [0, 10], [1, 20], [2, 15], [3, 30], [4, 25],
    [5, 20], [6, 15], [7, 30], [8, 25], [9, 20],
    [10, 30], [11, 25], [12, 20], [13, 35], [14, 30]
];

// 采样后的数据
const bucketSize = 5;
const sampledData = longestSegmentBucket(data, bucketSize);
console.log(sampledData);

优缺点

优点

  • 保留主要特征:通过选择线段长度最大的点,能够有效保留数据的关键变化。
  • 较好地保留趋势:在减少数据量的同时,保留了数据的整体趋势和主要特征。

缺点

  • 实现复杂:相对于简单的采样方法,实现较为复杂。
  • 计算成本较高:需要计算每个点的线段长度,计算成本较高。

多边形逼近算法 (Polygon Approximation Algorithm)

多边形逼近算法是一种降采样算法,通过简化数据点来逼近原始数据的轮廓或形状。这种方法主要用于保留数据的几何形状和主要特征,通常用于二维图形数据的降采样。

主要步骤:

  1. 初始化:选择一对初始点(通常是数据集的第一个点和最后一个点),并将其加入采样结果中。
  2. 递归分割:在初始点之间找到距离原始数据最远的点:我们从起始点和结束点开始,并找到这段数据中距离这条直线(起始点到结束点的直线)最远的那个点。并将其加入采样结果中。
  3. 递归逼近:在每个子区间内重复上述步骤,直到满足预定的误差容限或达到最大点数为止。

适用场景

多边形逼近算法适用于需要保留数据几何形状的场景,例如地图边界简化、手写体轮廓简化等。

JavaScript实现

function getPerpendicularDistance(point, lineStart, lineEnd) {
    const [x, y] = point;
    const [x1, y1] = lineStart;
    const [x2, y2] = lineEnd;

    const area = Math.abs((x2 - x1) * (y1 - y) - (x1 - x) * (y2 - y1));
    const bottom = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    return area / bottom;
}

function polygonApproximation(data, epsilon) {
    if (data.length < 3) return data;

    let dmax = 0;
    let index = 0;
    const end = data.length - 1;

    for (let i = 1; i < end; i++) {
        const d = getPerpendicularDistance(data[i], data[0], data[end]);
        if (d > dmax) {
            index = i;
            dmax = d;
        }
    }

    let result = [];

    if (dmax >= epsilon) {
        const recResults1 = polygonApproximation(data.slice(0, index + 1), epsilon);
        const recResults2 = polygonApproximation(data.slice(index, end + 1), epsilon);

        result = recResults1.slice(0, recResults1.length - 1).concat(recResults2);
    } else {
        result = [data[0], data[end]];
    }

    return result;
}

// 示例数据
const data = [
    [0, 0], [1, 0.5], [2, 1], [3, 1.5], [4, 2],
    [5, 1.5], [6, 1], [7, 0.5], [8, 0], [9, -0.5],
    [10, -1], [11, -1.5], [12, -2], [13, -1.5], [14, -1]
];

// 采样后的数据
const epsilon = 1.0;
const sampledData = polygonApproximation(data, epsilon);
console.log(sampledData);

优缺点

优点

  • 保留几何特征:能够有效保留数据的几何形状和主要特征。
  • 适用范围广:适用于各种需要保留轮廓的二维数据集。

缺点

  • 实现较复杂:相对于简单的采样方法,实现较为复杂。
  • 计算成本高:需要计算每个点到直线的距离,计算成本较高。

Visvalingam–Whyatt Algorithm

Visvalingam–Whyatt算法是一种用于折线简化的降采样算法。该算法通过计算每个点与其相邻点形成的三角形面积来确定点的重要性。面积最小的点被视为最不重要的点,并被移除。该过程重复进行,直到达到所需的简化程度。

主要步骤

  1. 计算每个点的三角形面积:对于每个数据点,计算它与前后相邻点形成的三角形面积。
  2. 移除面积最小的点:找出三角形面积最小的点并将其移除。
  3. 更新三角形面积:移除一个点后,更新剩余点的三角形面积。
  4. 重复步骤2和3:重复上述步骤,直到达到所需的点数或简化程度。

适用场景

Visvalingam–Whyatt算法适用于需要保留数据几何形状的简化场景,例如地图简化、时间序列数据简化等。

JavaScript实现

// 计算三角形面积的函数
function calculateTriangleArea(pointA, pointB, pointC) {
    return Math.abs(
        (pointA[0] * (pointB[1] - pointC[1]) +
         pointB[0] * (pointC[1] - pointA[1]) +
         pointC[0] * (pointA[1] - pointB[1])) / 2.0
    );
}

// Visvalingam–Whyatt算法实现
function visvalingamWhyatt(data, threshold) {
    if (data.length <= 2) return data;

    // 初始化每个点的三角形面积
    let areas = [];
    for (let i = 1; i < data.length - 1; i++) {
        const area = calculateTriangleArea(data[i - 1], data[i], data[i + 1]);
        areas.push({ index: i, area: area });
    }

    // 递归简化
    while (areas.length > 0) {
        // 找到面积最小的三角形
        let minAreaIndex = 0;
        for (let i = 1; i < areas.length; i++) {
            if (areas[i].area < areas[minAreaIndex].area) {
                minAreaIndex = i;
            }
        }

        // 如果最小面积大于阈值,停止简化
        if (areas[minAreaIndex].area > threshold) {
            break;
        }

        // 移除面积最小的点
        const removeIndex = areas[minAreaIndex].index;
        data.splice(removeIndex, 1);

        // 更新受影响的三角形面积
        areas.splice(minAreaIndex, 1); // 移除当前点的面积记录
        if (minAreaIndex > 0) {
            // 更新前一个点的面积
            areas[minAreaIndex - 1].area = calculateTriangleArea(
                data[areas[minAreaIndex - 1].index - 1],
                data[areas[minAreaIndex - 1].index],
                data[areas[minAreaIndex - 1].index + 1]
            );
        }
        if (minAreaIndex < areas.length) {
            // 更新后一个点的面积
            areas[minAreaIndex].area = calculateTriangleArea(
                data[areas[minAreaIndex].index - 1],
                data[areas[minAreaIndex].index],
                data[areas[minAreaIndex].index + 1]
            );
        }
    }

    return data;
}

// 示例数据
const data = [
    [0, 0], [1, 0.5], [2, 1], [3, 1.5], [4, 2],
    [5, 1.5], [6, 1], [7, 0.5], [8, 0], [9, -0.5],
    [10, -1], [11, -1.5], [12, -2], [13, -1.5], [14, -1]
];

// 采样后的数据
const threshold = 0.5; // 设定阈值
const sampledData = visvalingamWhyatt(data, threshold);
console.log(sampledData);

优缺点

优点

  • 保留几何特征:通过计算三角形面积,能够有效保留数据的几何形状和主要特征。
  • 适用范围广:适用于各种需要保留轮廓的二维数据集。

缺点

  • 实现较复杂:相对于简单的采样方法,实现较为复杂。
  • 计算成本高:需要计算每个点的三角形面积,计算成本较高。

最大三角形单桶算法 (Largest-Triangle-One-Bucket Algorithm)

最大三角形单桶算法(Largest-Triangle-One-Bucket, LTOB)是一种降采样算法,旨在通过分桶选择数据点来保持数据的整体趋势。与最大三角形三桶算法 (Largest-Triangle-Three-Bucket, LTTB) 类似,LTOB通过选择每个桶内的一个代表点,使得这些点能够形成最大面积的三角形,从而保留数据的主要变化特征。LTOB算法相比原始的Whytt算法,确保了点分布的相对均匀。每个桶都有一个代表点来表示,从而连接成为一个全局的路由。

主要步骤

  1. 分桶:将数据划分成固定数量的桶,每个桶包含相同数量的数据点。
  2. 选择每个桶的代表点:在每个桶中选择一个代表点,这个点通过与前一个桶的代表点和后一个桶的代表点形成的三角形面积决定。选择使三角形面积最大的那个点。
  3. 生成简化数据集:将每个桶选择的代表点组成简化后的数据集。

适用场景

最大三角形单桶算法适用于需要减少数据量并保留数据整体趋势的场景,例如时间序列数据可视化、传感器数据展示等。

JavaScript实现

function largestTriangleOneBucket(data, threshold) {
    const dataLength = data.length;
    if (threshold >= dataLength || threshold === 0) {
        return data; // 如果阈值大于数据长度或为0,返回原始数据
    }

    const sampled = [];
    const bucketSize = (dataLength - 2) / (threshold - 2);

    let a = 0, maxArea, maxAreaPoint, area;
    sampled.push(data[a]); // 第一个点

    for (let i = 0; i < threshold - 2; i++) {
        const avgRangeStart = Math.floor((i + 1) * bucketSize) + 1;
        const avgRangeEnd = Math.floor((i + 2) * bucketSize) + 1;
        const avgRangeLength = avgRangeEnd - avgRangeStart;

        const avgX = data.slice(avgRangeStart, avgRangeEnd).reduce((sum, point) => sum + point[0], 0) / avgRangeLength;
        const avgY = data.slice(avgRangeStart, avgRangeEnd).reduce((sum, point) => sum + point[1], 0) / avgRangeLength;

        const rangeOffs = Math.floor(i * bucketSize) + 1;
        const rangeTo = Math.floor((i + 1) * bucketSize) + 1;

        maxArea = -1;

        for (let j = rangeOffs; j < rangeTo; j++) {
            area = Math.abs((data[a][0] - avgX) * (data[j][1] - data[a][1]) - (data[a][0] - data[j][0]) * (avgY - data[a][1])) * 0.5;
            if (area > maxArea) {
                maxArea = area;
                maxAreaPoint = data[j];
            }
        }

        sampled.push(maxAreaPoint);
        a = data.indexOf(maxAreaPoint);
    }

    sampled.push(data[dataLength - 1]); // 最后一个点

    return sampled;
}

// 示例数据
const data = [
    [0, 0], [1, 0.5], [2, 1], [3, 1.5], [4, 2],
    [5, 1.5], [6, 1], [7, 0.5], [8, 0], [9, -0.5],
    [10, -1], [11, -1.5], [12, -2], [13, -1.5], [14, -1]
];

// 采样后的数据
const threshold = 5; // 设定阈值
const sampledData = largestTriangleOneBucket(data, threshold);
console.log(sampledData);

优缺点

优点

  • 保留整体趋势:通过选择每个桶内最能代表变化的点,能够有效保留数据的整体趋势。
  • 减少数据量:能够在大幅减少数据量的情况下,保留数据的主要变化特征。

缺点

  • 实现复杂:相对于简单的采样方法,实现较为复杂。
  • 计算成本较高:需要计算每个桶内点的三角形面积,计算成本较高。

最大三角形面积三桶算法 (LTTB)

适用场景

LTTB算法适用于需要保留数据总体趋势和主要特征的场景。例如,在显示金融市场的历史价格数据时,需要保留价格走势的整体形态。相比于单桶的短视问题,将有效区域的计算延伸到前后两个桶。

主要步骤

  1. 分桶:将数据划分成固定数量的桶。
  2. 计算三角形面积:对于每个桶,计算前后数据点与当前点形成的三角形面积。
  3. 选择最大三角形面积的点:选择面积最大的点作为代表点。

JavaScript实现

function lttb(data, threshold) {
    const bucketSize = Math.ceil(data.length / threshold);
    const sampled = [data[0]];

    for (let i = 0; i < threshold - 2; i++) {
        const start = Math.floor(i * bucketSize);
        const end = Math.floor((i + 1) * bucketSize);
        const bucket = data.slice(start, end);

        let maxArea = -1;
        let maxAreaPoint;
        const avgX = bucket.reduce((sum, point) => sum + point[0], 0) / bucket.length;
        const avgY = bucket.reduce((sum, point) => sum + point[1], 0) / bucket.length;

        for (const point of bucket) {
            const area = Math.abs(
                (sampled[sampled.length - 1][0] - avgX) * (point[1] - sampled[sampled.length - 1][1]) -
                (sampled[sampled.length - 1][0] - point[0]) * (avgY - sampled[sampled.length - 1][1])
            );
            if (area > maxArea) {
                maxArea = area;
                maxAreaPoint = point;
            }
        }

        sampled.push(maxAreaPoint);
    }

    sampled.push(data[data.length - 1]);
    return sampled;
}

优缺点

  • 优点:能较好地保留数据整体趋势,减少数据量显著。
  • 缺点:计算复杂度较高,可能会忽略高频细节。

动态最大三角形算法 (Dynamic Largest-Triangle-Three-Buckets Algorithm)

动态最大三角形三桶算法 (Dynamic Largest-Triangle-Three-Buckets, DLTTB) 是最大三角形三桶算法 (LTTB) 的变种,旨在处理动态数据集中的降采样需求。与LTTB一样,DLTTB 通过选择数据点以保留数据的整体趋势,但它能够动态更新数据集。LTD动态最大三角形,正如名字所说的那样可以动态的决定桶中的数据点个数。在上面提到的所有分桶算法中,我们都使用了同样的分配算法,即首尾各占一个桶,其他均分。这种分配方法无疑是最简单的,并且大多数情况是有效的。但是当我们遇到一些特殊形状的图形,如数据分布不均匀,一部分时间数据变化很平缓,部分时间变化很陡峭,如下图所示。我们的分桶方式就会显得力不从心。

主要步骤

  1. 分桶:将数据划分成固定数量的桶,每个桶包含相同数量的数据点。
  2. 选择每个桶的代表点:在每个桶中选择一个代表点,这个点通过与前后两个桶的代表点形成的三角形面积决定。选择使三角形面积最大的那个点。
  3. 更新数据集:当数据集发生变化时,动态调整桶和代表点。

适用场景

动态最大三角形三桶算法适用于需要处理动态更新的数据集并保留数据整体趋势的场景,例如实时监控数据、流数据处理等。

JavaScript实现

function dynamicLTTB(data, threshold) {
    if (threshold >= data.length || threshold === 0) {
        return data; // 如果阈值大于数据长度或为0,返回原始数据
    }

    const sampled = [];
    const every = (data.length - 2) / (threshold - 2);

    let a = 0, maxArea, maxAreaPoint, area;
    sampled.push(data[a]); // 第一个点

    for (let i = 0; i < threshold - 2; i++) {
        const avgRangeStart = Math.floor((i + 1) * every) + 1;
        const avgRangeEnd = Math.floor((i + 2) * every) + 1;
        const avgRangeLength = avgRangeEnd - avgRangeStart;

        const avgX = data.slice(avgRangeStart, avgRangeEnd).reduce((sum, point) => sum + point[0], 0) / avgRangeLength;
        const avgY = data.slice(avgRangeStart, avgRangeEnd).reduce((sum, point) => sum + point[1], 0) / avgRangeLength;

        const rangeOffs = Math.floor(i * every) + 1;
        const rangeTo = Math.floor((i + 1) * every) + 1;

        maxArea = -1;

        for (let j = rangeOffs; j < rangeTo; j++) {
            area = Math.abs((data[a][0] - avgX) * (data[j][1] - data[a][1]) - (data[a][0] - data[j][0]) * (avgY - data[a][1])) * 0.5;
            if (area > maxArea) {
                maxArea = area;
                maxAreaPoint = data[j];
            }
        }

        sampled.push(maxAreaPoint);
        a = data.indexOf(maxAreaPoint);
    }

    sampled.push(data[data.length - 1]); // 最后一个点

    return sampled;
}

// 示例数据
const data = [
    [0, 0], [1, 0.5], [2, 1], [3, 1.5], [4, 2],
    [5, 1.5], [6, 1], [7, 0.5], [8, 0], [9, -0.5],
    [10, -1], [11, -1.5], [12, -2], [13, -1.5], [14, -1]
];

// 采样后的数据
const threshold = 5; // 设定阈值
const sampledData = dynamicLTTB(data, threshold);
console.log(sampledData);

优缺点

优点

  • 保留整体趋势:通过选择每个桶内最能代表变化的点,能够有效保留数据的整体趋势。
  • 动态更新:能够处理动态更新的数据集,适用于实时数据处理场景。

缺点

  • 实现复杂:相对于简单的采样方法,实现较为复杂。
  • 计算成本较高:需要计算每个桶内点的三角形面积,计算成本较高。

随机采样 (Random Sampling)

适用场景

随机采样适用于初步探索性分析或对数据精度要求不高的场景。例如,在对用户点击数据进行初步分析时,可以使用随机采样来快速获得一个大致的了解。

主要步骤

  1. 随机选择数据点:从数据集中随机选择指定数量的数据点。

JavaScript实现

function randomSampling(data, sampleSize) {
    const sampledData = [];
    const dataCopy = [...data];

    for (let i = 0; i < sampleSize; i++) {
        const index = Math.floor(Math.random() * dataCopy.length);
        sampledData.push(dataCopy.splice(index, 1)[0]);
    }

    return sampledData;
}

均匀采样 (Uniform Sampling)

主要步骤

  1. 按固定间隔选择数据点:按固定间隔从数据集中选择数据点。

适用场景

均匀采样适用于数据分布较均匀或变化较小的场景。例如,在环境传感器数据展示中,均匀采样可以有效减少数据量而不会丢失太多信息。

JavaScript实现

function uniformSampling(data, sampleSize) { 
  const step = Math.floor(data.length / sampleSize); 
  const sampledData = []; 
  for (let i = 0; i < data.length; i += step) { 
    sampledData.push(data[i]); 
  } 
  return sampledData; 
}

优缺点

  • 优点:实现简单,计算效率高。
  • 缺点:可能丢失局部细节,采样结果在数据变化剧烈时不具代表性。

滑动窗口采样 (Sliding Window Sampling)

主要步骤

  1. 分窗口:将数据分成固定大小的窗口。
  2. 选择窗口内代表点:在每个窗口内选择最大值和最小值或其他代表点。

适用场景

滑动窗口采样适用于需要保留局部变化和细节的场景。例如,在心电图数据展示中,需要保留每个窗口内的最大、最小或中位数等特征点。

JavaScript实现

function slidingWindowSampling(data, windowSize) {
    const sampledData = [];

    for (let i = 0; i < data.length; i += windowSize) {
        const window = data.slice(i, i + windowSize);
        const maxPoint = window.reduce((max, point) => point[1] > max[1] ? point : max, window[0]);
        const minPoint = window.reduce((min, point) => point[1] < min[1] ? point : min, window[0]);

        sampledData.push(maxPoint);
        sampledData.push(minPoint);
    }

    return sampledData;
}

优缺点

  • 优点:保留局部特征,灵活性高。
  • 缺点:计算复杂度较高,结果依赖于窗口大小。

分层采样 (Stratified Sampling)

主要步骤

  1. 分层:将数据按指定维度进行分层。
  2. 每层抽样:在每个层中按比例随机抽样。

适用场景

分层采样适用于数据分布不均匀,需要保证各层数据特征均有代表的场景。例如,在人口调查数据中,可以按不同年龄段进行分层采样,以确保各年龄段均有代表性。

JavaScript实现

function stratifiedSampling(data, strataKeys, sampleSize) {
    const strata = {};
    const sampledData = [];

    // 分层
    data.forEach(point => {
        const key = strataKeys.map(k => point[k]).join('-');
        if (!strata[key]) strata[key] = [];
        strata[key].push(point);
    });

    // 每层采样
    Object.keys(strata).forEach(key => {
        const stratum = strata[key];
        const stratumSampleSize = Math.ceil((stratum.length / data.length) * sampleSize);
        const stratumSample = randomSampling(stratum, stratumSampleSize);
        sampledData.push(...stratumSample);
    });

    return sampledData;
}

优缺点

  • 优点:保持整体结构,适用于不均匀分布的数据。
  • 缺点:实现复杂,计算成本高。

结论

不同的降采样算法在不同的应用场景中具有各自的优缺点。选择合适的算法需要根据具体的数据特征和应用需求来确定。在实际应用中,可以结合多种算法,优化数据采样效果,提升数据可视化的性能和准确性。

希望这篇文章能帮助你更好地理解和应用降采样算法。如果你有任何问题或建议,欢迎留言讨论!