搜索文章

输入关键词开始搜索

对数

思考#算法#未来的路

应用场景

最近有一个需求,要展示某人整个人生阶段中,每一年的支出数据。假设这个人的支出信息如下(为了简化理解和作图,只罗列5年数据):

年龄支出金额原因
2120000读书
2230000读书+毕业
2350000工作后生活(吃饭、租房等)
243050000(家里出钱)买房
2560000工作后生活(吃饭、租房等,物价上涨了一些)

如果按照普通的数值作图,得到的图形是这样的:

not_log

可以看到,因为24岁这一年买房的支出远大于其他年份的支出,导致其他年份的数据都被压到底部了,无法区分出来具体数值。

而实际上买房这种属于非常规消费,在人生阶段中出现的频率是很低的,不应该由于这少部分年份的数据,而影响了大部分年份的展示。

为了解决这类非常规数据,就需要引入对数了。

这里先展示下log做粗糙处理后的图形(没有考虑0-1的情况,后面会详细说明下这个区间的特殊处理):

with_log

可以看到,非常规数据的差异被缩小了,大部分数据的趋势都可以明显的看出来。

对数处理,用于解决大部分常规数据+小部分非常规数据(即数据不是同一个量级)混合展示的问题。

什么是对数

log

对数的定义,摘自百度百科:

如果 ,N=ax(a>0, a≠1),即a的x次方等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作x=logaN。其中,a叫做对数的底数,N叫做真数,x叫做“以a为底N的对数”。

  1. 特别地,我们称以10为底的对数叫做常用对数(common logarithm),并记为lg。
  2. 称以无理数e(e=2.71828…)为底的对数称为自然对数(natural logarithm),并记为ln。
  3. 零没有对数。
  4. 实数范围内,负数无对数。在虚数范围内,负数是有对数的。

实现思路

JavaScript中已经提供了现成静态函数了:Math.log()

比如求以a为底的N的对数,则可以这样计算:

let x = Math.log(N) / Math.log(a)

我们实际作图的时候,其实是先算出每个数值对应的以10为底的对数,然后用这些对数值作图的。

以上面这个图形为例:

// 原始数据
var raw = [
    ['21岁', 20000],
    ['22岁', 30000],
    ['23岁', 50000],
    ['24岁', 3050000],
    ['25岁', 60000]
];

// 实际画图用的数据
raw.forEach((d) => {
    d[1] = Math.log(d[1]) / Math.log(10)
})
/*
["21岁", 4.30102999566398],
["22岁", 4.477121254719662],
["23岁", 4.698970004336019],
["24岁", 6.484299839346785],
["25岁", 4.778151250383643]
/*

处理流程

如何解决0-1之间的真数,其对数为负数的问题

以10为底的对数

对数真数
-∞0
01
110
2100
31000

真数为0时,对数是负无穷,而我们实际作图的时候,需要将真数0对应到对数0上面,这样才能做出标准的0点。因此我们数据处理的时候,还需要先将真数做一个加一操作,使真数从1开始,也就是取得的对数从0开始。

假如我们将真正绘制图形的Y值叫做图值g,那么:

let g = Math.log(N + 1) / Math.log(a)

如何解决真数为负数的问题

由于只能对整数取对数,当真数存在负值时,就要做一些特殊处理了。

其实这个处理很简单,就是取其绝对值来计算对数,然后将算得的结果乘以-1,就是作图所需的图值了:

let g = (Math.log(Math.abs(N) + 1) / Math.log(a)) * -1

流程图

这里借用下同事ZHQ的流程图,画得很棒,清晰明了:

process

其他问题

如何解决刻度显示问题

在展示的时候通过提供的格式化回调函数,对数值进行幂计算(Math.pow()),还原为原始数值进行展示即可。

比如Y刻度的数值,可以这样处理:

label: {
    show: true,
    style: {
        fill: 'rgba(0, 0, 0, 1)',
            fontFamily: 'SFUIText-Regular'
    },
    formatter: (d) => {
        // 别忘了精度处理
        let value = Math.ceil(Math.pow(10, d))
        return value >= 10000 ? (value / 10000) + '万' : value
    }
},

由于JS计算出来的不是整数,因此别忘记了做精度处理。

完整代码

<head>
    <link rel="stylesheet" href="http://s.thsi.cn/js/d3chart/style/v0.0.9/index.css">
    <script src="http://s.thsi.cn/js/datav/charts/latest/d3_charts.js"></script>
</head>

<body>
    <div style="width:1000px;height:350px;background:#fff" id="chart"></div>

    <script>
        /**
         * 常规数值
         */
        var data2 = [
            ['21岁', 20000],
            ['22岁', 30000],
            ['23岁', 50000],
            ['24岁', 3050000],
            ['25岁', 60000]
        ];

        /**
         * 对数数值
         */
        data2.forEach((d) => {
            // 注意这里的+1操作,是为了解决0-1之间对数为负数的问题
            d[1] = Math.log(d[1] + 1) / Math.log(10)
        })

        var hasNegativeNumber = false;

        for (var i = data2.length; i--;) {
            if (data2[i][1] < 0 || data2[i][2] < 0) {
                hasNegativeNumber = true;
                break;
            }
        }

        var domainFormatter1 = function (domain) {
            if (hasNegativeNumber) {
                var max = Math.max(Math.abs(domain[0]), Math.abs(domain[1]));
                return [-max, max];
            } else {
                return [domain[0] * 0.8, domain[1] * 1.2];
            }
        };


        var domainFormatter2 = function (domain) {
            if (hasNegativeNumber) {
                var max = Math.max(Math.abs(domain[0]), Math.abs(domain[1]));
                return [-max, max];
            } else {
                return [domain[0] * 0.8, domain[1] * 1.4];
            }
        };

        option = {
            axis: [{
                    position: 'bottom',
                    $dataIndex: 0,
                    dataKey: 0,
                    line: {
                        show: false
                    },
                    tick: {
                        show: false,
                        style: {
                            fill: 'rgba(94,120,145,1)'
                        }
                    },
                    label: {
                        style: {
                            fill: 'rgba(0, 0, 0, 1)',
                            fontFamily: 'SFUIText-Regular',
                            rich: {
                                a: {
                                    fontSize: 14,
                                    textLineHeight: 18
                                }
                            }
                        },
                        formatter: function (text) {
                            return '{a|' + text + '}';
                        },
                        padding: 20,
                        // rotate: 45
                    },
                    splitLine: {
                        show: true
                    },
                    interval: 1
                },
                {
                    position: 'left',
                    type: 'linear',
                    xOrY: 'y',
                    line: {
                        show: false
                    },
                    tick: {
                        show: true,
                        style: {
                            fill: 'rgba(0, 0, 0, 0.3)',
                        }
                    },
                    label: {
                        show: true,
                        style: {
                            fill: 'rgba(0, 0, 0, 1)',
                            fontFamily: 'SFUIText-Regular'
                        },
                        formatter: (d) => {
                            let value = Math.pow(10, d)
                            return value >= 10000 ? (value / 10000) + '万' : value

                        },
                    },
                    splitLine: {
                        show: true,
                        "style": {
                            "color": "#e4e4e4",
                            "lineWidth": 1
                        }
                    },
                    domainFormatter: domainFormatter1,
                    nice: true
                }
            ],
            grid: [{
                bottom: '15%',
                right: '12%',
                left: '10%',
                top: '10%'
            }],
            series: [{
                type: 'bar',
                $dataIndex: 0,
                dataKey: 1,
                label: {
                    normal: {
                        show: true,
                        formatter: (d) => {
                            // 精度处理
                            let value = Math.ceil(Math.pow(10, d))
                            // 注意这里的-1,是为了去掉之前处理[0, 1]区间数据时加上的那个1
                            return value >= 10000 ? ((value - 1) / 10000) + '万' : (value - 1)

                        }
                    },
                    emphasis: {
                        show: true,
                        style: {
                            position: 'top',
                            fill: 'rgba(70,218,218,1)',
                            fontFamily: 'PingFang-SC-Semibold'
                        }
                    }

                },
                itemStyle: {
                    normal: {
                        fill: {
                            y2: 1,
                            x2: 0,
                            colorStops: [{
                                    offset: 0,
                                    color: 'rgba(70,228,228,1)'
                                },
                                {
                                    offset: 1,
                                    color: 'rgba(70,218,218,1)'
                                }
                            ]
                        }
                    }
                }
            }],
            data: [{
                originData: data2
            }]
        };


        var myChart = D3Charts.init("chart");
        myChart.setOption(option);
    </script>
</body>