【问题标题】:How to reduce a data graph but keeping the extremes如何减少数据图但保持极端
【发布时间】:2017-01-20 11:19:18
【问题描述】:

我有一个数据库,它以 10 分钟的间隔收集了一个月的数据集。 (所以每 10 分钟一个数据集)

现在我想在三个图表上显示该数据:过去 24 小时、过去 7 天和过去 30 天。

数据如下:

{ "data" : 278, "date" : ISODate("2016-08-31T01:51:05.315Z") }
{ "data" : 627, "date" : ISODate("2016-08-31T01:51:06.361Z") }
{ "data" : 146, "date" : ISODate("2016-08-31T01:51:07.938Z") }
// etc

对于 24 小时图,我只是输出最近 24 小时的数据,这很简单。

对于其他图表,我细化了数据:

const data = {}; //data from database
let newData = [];
const interval = 7; //for 7 days the interval is 7, for 30 days it's 30

for( let i = 0; i < data.length; i += interval ) {
    newData.push( data[ i ] );
};

这很好用,但极端事件(data0 或与其他平均值有很大差异)可能会丢失,具体取决于您搜索数据的时间。但是,不稀疏数据将导致大量数据点通过管道发送并且必须在前端进行处理。我想避免这种情况。

现在我的问题

如何减少 7 天的数据,同时保持极端数据?这里最有效的方法是什么?

添加: 本质上,我认为我正在尝试简化图表以减少点但保持整体形状。 (如果你从纯图像的角度来看)

类似于Douglas–Peucker algorithm 在节点中的实现?

【问题讨论】:

标签: javascript node.js statistics outliers


【解决方案1】:

正如您在 cmets 中提到的,Ramer-Douglas-Peucker (RDP) 算法用于处理 2D 图形中的数据点,但您希望将其用于 X 值固定的图形数据。我修改了 M Oehm 提供的this Javascript implementation of the algorithm,在计算中只考虑垂直 (Y) 距离。

另一方面,通常建议使用数据平滑来减少图中数据点的数量(参见 csgillespie 的 this post)。

为了比较这两种方法,我做了一个小测试程序。重置按钮创建新的测试数据。可以选择并应用一种算法来获得减少的点数,由指定的间隔分隔。然而,在 RDP 算法的情况下,结果点不是均匀分布的。为了获得与指定间隔相同的点数,我迭代地运行计算,每次调整 espilon 值,直到达到正确的点数。

根据我的测试,RDP 算法提供了更好的结果。唯一的缺点是点之间的间距不同。我认为这是无法避免的,因为我们希望保留原始数据中分布不均的极值点。

这里是sn-p的代码,在整页模式下比较好看:

var svgns = 'http://www.w3.org/2000/svg';
var graph = document.getElementById('graph1');
var grpRawData = document.getElementById('grpRawData');
var grpCalculatedData = document.getElementById('grpCalculatedData');

var btnReset = document.getElementById('btnReset');
var cmbMethod = document.getElementById('cmbMethod');
var btnAddCalculated = document.getElementById('btnAddCalculated');
var btnClearCalculated = document.getElementById('btnClearCalculated');

var data = [];
var calculatedCount = 0;
var colors = ['black', 'red', 'green', 'blue', 'orange', 'purple'];

var getPeriod = function () {
    return parseInt(document.getElementById('txtPeriod').value, 10);
};

var clearGroup = function (grp) {
    while (grp.lastChild) {
        grp.removeChild(grp.lastChild);
    }
};

var showPoints = function (grp, pts, markerSize, color) {
    var i, point;
    for (i = 0; i < pts.length; i++) {
        point = pts[i];
        var marker = document.createElementNS(svgns, 'circle');
        marker.setAttributeNS(null, 'cx', point.x);
        marker.setAttributeNS(null, 'cy', point.y);
        marker.setAttributeNS(null, 'r', markerSize);
        marker.setAttributeNS(null, 'fill', color);
        grp.appendChild(marker);
    }
};

// Create and display test data
var showRawData = function () {
    var i, x, y;
    var r = 0;
    data = [];
    for (i = 1; i < 500; i++) {
        x = i;
        r += 15.0 * (Math.random() * Math.random() - 0.25);
        y = 150 + 30 * Math.sin(x / 200) * Math.sin((x - 37) / 61) + 2 * Math.sin((x - 7) / 11) + r;
        data.push({ x: x, y: y });
    }
    showPoints(grpRawData, data, 1, '#888');
};

// Gaussian kernel smoother
var createGaussianKernelData = function () {
    var i, x, y;
    var r = 0;
    var result = [];
    var period = getPeriod();
    for (i = Math.floor(period / 2) ; i < data.length; i += period) {
        x = data[i].x;
        y = gaussianKernel(i);
        result.push({ x: x, y: y });
    }
    return result;
};

var gaussianKernel = function (index) {
    var halfRange = Math.floor(getPeriod() / 2);
    var distance, factor;
    var totalValue = 0;
    var totalFactor = 0;
    for (i = index - halfRange; i <= index + halfRange; i++) {
        if (0 <= i && i < data.length) {
            distance = Math.abs(i - index);
            factor = Math.exp(-Math.pow(distance, 2));
            totalFactor += factor;
            totalValue += data[i].y * factor;
        }
    }
    return totalValue / totalFactor;
};

// Ramer-Douglas-Peucker algorithm
var ramerDouglasPeuckerRecursive = function (pts, first, last, eps) {
    if (first >= last - 1) {
        return [pts[first]];
    }

    var slope = (pts[last].y - pts[first].y) / (pts[last].x - pts[first].x);

    var x0 = pts[first].x;
    var y0 = pts[first].y;

    var iMax = first;
    var max = -1;
    var p, dy;

    // Calculate vertical distance
    for (var i = first + 1; i < last; i++) {
        p = pts[i];
        y = y0 + slope * (p.x - x0);
        dy = Math.abs(p.y - y);

        if (dy > max) {
            max = dy;
            iMax = i;
        }
    }

    if (max < eps) {
        return [pts[first]];
    }

    var p1 = ramerDouglasPeuckerRecursive(pts, first, iMax, eps);
    var p2 = ramerDouglasPeuckerRecursive(pts, iMax, last, eps);

    return p1.concat(p2);
}

var internalRamerDouglasPeucker = function (pts, eps) {
    var p = ramerDouglasPeuckerRecursive(data, 0, pts.length - 1, eps);
    return p.concat([pts[pts.length - 1]]);
}

var createRamerDouglasPeuckerData = function () {
    var finalPointCount = Math.round(data.length / getPeriod());
    var epsilon = getPeriod();
    var pts = internalRamerDouglasPeucker(data, epsilon);
    var iteration = 0;
    // Iterate until the correct number of points is obtained
    while (pts.length != finalPointCount && iteration++ < 20) {
        epsilon *= Math.sqrt(pts.length / finalPointCount);
        pts = internalRamerDouglasPeucker(data, epsilon);
    }
    return pts;
};

// Event handlers
btnReset.addEventListener('click', function () {
    calculatedCount = 0;
    clearGroup(grpRawData);
    clearGroup(grpCalculatedData);
    showRawData();
});

btnClearCalculated.addEventListener('click', function () {
    calculatedCount = 0;
    clearGroup(grpCalculatedData);
});

btnAddCalculated.addEventListener('click', function () {
    switch (cmbMethod.value) {
        case "Gaussian":
            showPoints(grpCalculatedData, createGaussianKernelData(), 2, colors[calculatedCount++]);
            break;
        case "RDP":
            showPoints(grpCalculatedData, createRamerDouglasPeuckerData(), 2, colors[calculatedCount++]);
            return;
    }
});

showRawData();
div
{
    margin-bottom: 6px;
}
<div>
    <button id="btnReset">Reset</button>&nbsp;
    <select id="cmbMethod">
        <option value="RDP">Ramer-Douglas-Peucker</option>
        <option value="Gaussian">Gaussian kernel</option>
    </select>&nbsp;
    <label for="txtPeriod">Interval: </label>
    <input id="txtPeriod" type="text" style="width: 36px;" value="7" />
</div>
<div>
    <button id="btnAddCalculated">Add calculated points</button>
    <button id="btnClearCalculated">Clear calculated points</button>
</div>
<svg id="svg1" width="765" height="450" viewBox="0 0 510 300">
    <g id="graph1" transform="translate(0,300) scale(1,-1)">
        <rect width="500" height="300" stroke="black" fill="#eee"></rect>
        <g id="grpRawData"></g>
        <g id="grpCalculatedData"></g>
    </g>
</svg>

【讨论】:

  • 这看起来很棒。谢谢你。给我一些时间来消化这个然后回来。
  • 这很好,仍然定义了一些细节,但我认为我可以安全地将其标记为答案:)
  • 这是一个有趣的问题。最后的细节祝你好运。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多