数据可视化毕业设计

3,966 阅读20分钟

1、前言

很近没有认真写文章了,都是遇到一些问题简单记录一下才被迫发表博客,这次准备把之前做的几套可视化分析大屏系统总结并分享出来,但暂时不开源(因为都是一些毕业设计,有这毕业设计方面的需求可以联系我86377220,其中包括了论文以及各种资料)。

这次介绍的系统主题是基于恶意短信的可视分析系统,数据来源是ChinaVIS 2017年挑战赛一,数据背景如下介绍:

以上是这次系统设计的基调和背景,围绕这一背景,利用所学知识,将官方所给的数据转换为交互式的可视化图形,在系统运行过程中发现数据之间的联系以及找到题目所要求答案。这是我本科期间的毕业设计,基于此设计所拽写的毕业论文也被评为优秀论文,有需要的可私聊。

后面会对为什么选择这个主题作为毕业设计,开发中遇到的困难,系统采用到的技术重难点分析,通过系统发现的规律,以及论文的一些写作技巧(相同的语句如何降低重复率)进行说明。

先来一张系统效果图,有个概念:

image-20210816162213802.png

image-20210816162243989.png 总共分为两个大的模块,一个是全部垃圾短信的宏观分布规律,另外一个是分类垃圾短信的分布规律。这两个界面完成了题目所要求85%的答案,如果17年能有这个水平参加可视化大赛,估计也能拿个优秀奖(每年的参赛队伍将近100支,获奖比例才30%左右,含金量可谓是比较重了,2020年参加的疫情可视化很幸运的拿到了优秀奖,后期会对这套系统进行介绍)吧?

2、选题背景

废话不多说,直接奔向正题!

为什么会选择这个主题作为毕业论文呢?回答这个问题之前,还得把大学的经历先简单的介绍一番,我学的是电子类专业,就业方向走软件类也可以走硬件类方向,对电路懵逼的我毫不犹豫的选择了软件,然后在众多语言中稀里糊涂的选择了前端(别问为什么,问就是不知道!(╹▽╹))。

想必大家在大学期间可能都参加过很多比赛,也完成过很多的项目,大学生创新创业训练计划项目应该都听说过吧,含金量虽然没多重,但能很好的锻炼的自己的动手和学习能力,更重要的是,学校会拨经费下来作为对项目的孵化,这样大学期间不就省了很大一笔学习资料费吗?(插播一句,有想了解大学生创新创业训练计划项目的,欢迎骚扰!两次国家级立项和一次校级立项的经验足以帮助你从众多申请项目中脱颖而出)。

也就是通过参加比赛,选择了前端,然后碰到一个很好的指导老师(可视化方向的),就一直跟着她从事可视化分析,然后带领学生团队参加各类比赛。也就是说选数据可视化作为毕业设计方向已经有了将近三年的基础。

那为什么会选择ChinaVIS作为数据来源和选题呢?起初从指导老师那里得知这个领域含金量比较重的比赛包括ChinaVIS和阿里的天池大数据竞赛等,因为老师对ChinaVIS比较了解,所以就没有选择其他的参赛平台。

2019年参加了第一次ChinaVIS,四个人组成一个团队,什么都不会,连简单的HTML语法都不知道,一群懵懂的小白就此开启了填坑之路。因为比赛只有两个月时间,结果可想而知,做出了一堆自己都不想去看的乱七八糟图形。还天天呆在办公室,基本上没有休息时间,到最后,连自己都不想去看这个代码了,就团队的每个人都很烦躁,因为失去了方向,做出来的东西和预想的天壤之别。

然后接下来的一年,就开始学各类技术,但2020年疫情爆发了,本以为组委会不会举办今年的比赛,后面又开放了开放式选题,疫情可视化,分析疫情对我国社会所产生的影响。然后皇天不负有心人,获得了优秀奖,但参赛队伍90%以上都是研究生队伍,所以取得这个成绩还是很满意。

完成项目后,就基本上确定了毕业论文选题的方向,数据来源从以往ChinaVIS中选,最后发现伪基站和垃圾短信数据比较贴近现实,做起来比较有意义,一拍即合,就这样完成了自己的选题之路!(PS:很多学校都是老师给课题,然后学生从这几个课题里面去选,因为论文指导老师和之前的指导老师都是同一个,就自己选然后告诉她就行,这样就可以做自己想要做的事情)。后面项目组的学弟根据这套系统和论文也申请了大学生创新创业训练计划项目,不出意外的又获得了国家级立项,连续三年,同一个指导老师和项目组,都是国家级项目,这在学校中的二级学院里面还是第一次!

3、环境依赖

1、Node.js(前端Vue和后端node都依赖该环境)

2、开发工具:Git,vscode,Hbuilder,pycharm

3、开发语言:Python,HTML+CSS+JavaScript

4、重点依赖库:echarts,bootstrap,jQuery

4、可视化分析

以下可视化分析直接从复制来源于本人的毕业论文。

本次垃圾短信的记录数大约有330万条,时间跨度为63天,平均每天的垃圾短信为5万2千条,但受到节假日和两会召开的影响,每天的分布显得极不均匀。如图4.1所示,不同日期的垃圾短信数据有大于25万条的,也有将近1万条的记录。根据平均值计算,当天垃圾短信大于5万条且小于10万条的则可以推断出较为活跃;数量大于10万条的则为非常活跃。但受到外部因素的影响,3月6号到15号的两会召开期间不活跃,4月2号到5号受假期影响也表现为不活跃,假期恢复之后则迅速回到平均活跃水平。

wps1.jpg

​ 图4.1 不同日期垃圾短信活跃趋势图

在图4.2中,将垃圾短信按照每小时进行统计,可以看出从早6点开始,一直持续到晚9点,是每天垃圾短信分布最为密集的时间段。而从晚9点到次日早六点期间,垃圾短信的数量分布非常少。再结合4.3的a、b、c三幅极坐标柱形图也可以分析出垃圾短信的时间段基本上和上述分析的一致。

wps2.jpg 图4.2 垃圾短信频繁分布时间段图

wps3.png

​ 图4.3 极坐标图

利用多图交互式可视化的方法将时间维度和空间维度进行结合分析,有利于探索更加深层次的隐蔽信息。如图4.4交互式图形所示,点击折线图可以更新地图和极坐标柱形图的数据,地图显示当前日期的垃圾短信分布位置数据,极坐标显示当前日期不同时间段的垃圾短信数量;然后再次点击极坐标柱形图,可以得到如图4.5所示的每小时垃圾短信分布图。图4.5的四幅小图分别展示的是10、13、17和22时的数据,经过对比,可以发现基本上所有的垃圾短信都集中分布在北京四环以内,并且主要分布在朝阳区和东城区两个位置。从时间上对比,白天其他地方的垃圾短信相对较多,但到了晚上,垃圾短信几乎值分布在市区东部,其他地方基本为0。

wps4.jpg

​ 图4.4 多维交互式图形

wps5.png

​ 图4.5 每小时垃圾短信分布

可视分析大概就分析这两个点,篇幅写太多了也不好!

5、部分代码分析

5.1 、后端代码

之前对Java后端开发并不是很熟悉,而且是在毕业设计期间,又外出实习,就更没有时间用Java来写了。所以采用JavaScript语法的node.js作为后端,记得刚开始入门node的时候,完全不知道怎么实现,网上看了很多的资料,非常的零散,连最基本的demo都没有搭建出来,找了半天模板,也没用直接可以使用的,小白完成很懵逼!

然后经过几天的探索,根据零散的资料一步一步完成了《Node后端模板代码(附源码)》,根据这套代码,可以直接拿来开发使用,更多详细的点可以通过博文进行学习。当然,这是一套很基本的模板代码,只是实现了整个流程的最基本点,适合刚入门node的人作为参考!

其实后端的做的事情并不多,仅仅相当于一个前端和数据库连接的一个桥梁,基本上所有的数据处理都放在了前端完成,这导致目前系统运行起来有一点点的卡,后续打算用Java去实现,并将数据处理部分都放到后端,前端只做数据的渲染。

这里简单对后端的模板代码进行分析:

  • 封装处理请求和和SQL查询以及响应的模板代码,有了这个模板代码,后端写的就非常非常简单了,基本上两三行代码就是一个接口,不过后面经过验证,方便是方便,但是扩展性不高,只能做一些简单的查询,如果后端想进行数据处理,就有点麻烦了。

    /*
     * @Description  : 封装操作数据库的代码,因为一些代码高度相似,其他文件要使用这些模块的时候,直接用require导入即可
     */
    
    // 导入url模块
    var url = require('url');
    // 导入mysql模块 
    var mysql = require('mysql');
    // 导入数据库配置信息
    var dbconfig = require('@/config/DBConfig');
    // 使用DBConfig.js的配置信息创建一个MySQL连接池。
    var pool = mysql.createPool(dbconfig.mysql);
    var logger = require('log4js').getLogger("index");
    
    /**
     * 封装对数据库的增删改查代码,返回的结果code为0表示失败,为1表示成功
     * @param {*} res 服务器的响应,也就是服务器把查询到的数据返回给前端
     * @param {String} sql SQL语句
     * @param {String} querySuccessMsg 操作数据库成功时的提示信息
     * @param {String} queryErrMsg 操作数据库失败时的提示信息
     * @returns {*} 失败就直接返回,成功就返回数据
     */
    var baseApi = function (res, sql, querySuccessMsg, queryErrMsg) {
        // 对传入的各类信息进行比较,当没有传入的时候,就默认为'',防止出现出错
        var querySuccessMsg_ = querySuccessMsg || '';
        var queryErrMsg_ = queryErrMsg || '';
    
        pool.getConnection(function (err, connection) {
            /**
             * 这个错误是数据库连接时抛出的错误,比如MySQL服务未开启、主机错误、密码错误、端口错误、数据库不存在等等,
             * 如果连接的时候出错了,就会返回错误的信息,最好写上,到时候可以排查错误
             * 如果出现错误,那么err这个回调函数就不为null,如果连接正常,就为null
             */
            if (err !== null) {
                logger.error({
                    code: 0,
                    err: err,
                    msg: "数据库连接错误!请检查数据库服务是否开启、用户名和密码是否正确、主机地址和端口是否正确、数据库存不存在!"
                });
                res.send({
                    code: 0,
                    err: err,
                    msg: "数据库连接错误!请检查数据库服务是否开启、用户名和密码是否正确、主机地址和端口是否正确、数据库存不存在!"
                });
                return;
            }
    
            // 执行数据库操作语句
            connection.query(sql, function (err, results) {
                /**
                 * 这个错误是执行MySQL语句时可能会出现的错误,和上面的道理一样,还是要写上,方便检查错误
                 */
                logger.info(sql)
                if (err !== null) {
                    logger.error(err)
                    res.send({
                        code: 0,
                        err: err,
                        msg: "请检查SQL语句是否正确!"
                    });
                    return;
                }
    
                if (results != [] && results != "") {
                    logger.info('查询成功')
                    res.send({
                        code: 1,
                        data:  results,
                        msg: querySuccessMsg_
                    });
                } else {
                    logger.error({
                        code: 0,
                        data: {
                            results: results,
                            err: err
                        },
                        msg: queryErrMsg_
                    })
                    res.send({
                        code: 0,
                        data: {
                            results: results,
                            err: err
                        },
                        msg: queryErrMsg_
                    })
                }
            });
            connection.release(); //释放这个连接池
        })
    };
    
    module.exports = {
        baseApi
    };
    
  • 关于这套node模板代码还有其他的知识点,可以移步到《Node后端模板代码(附源码)》进行查看,有更加详细的模块说明和使用。

5.2、前端代码

前端用了Vue框架,但没有用到Vue的精华,只是比较肤浅的使用了Vue。这套系统也稍微算有一点点大,组件之间的通信很频繁,但居然没有使用Vue的状态过滤器vuex;其二,在前面介绍重点依赖库的时候可以发现,用到了jQuery,Vue最核心的部分是双向数据绑定,数据变化就会自动重新渲染视图,不需要我们自己去操纵DOM节点,但很遗憾,这套系统中绝大多数都是用jQuery在操纵DOM,这也使得本就卡的系统更加卡了。

当然,这也是之前不懂事,以为自己会使用了,结果是刚刚入门都还差点意思。后续也有可能对前端代码进行优化,尽可能的提升系统响应速度和渲染速度。

既然都写了,那也不怕拿出来丢丑了!

  • AJAX;前后端交互技术,基于jQuery的Ajax,并对其进一步进行封装,至于好坏,自行把握,我觉得用起来还是比较舒服的。

    /*
     * @Description  : 封装请求的代码,不需要每次请求都时候都写上一长串的代码,直接调用这个函数就可以了
     */
    
    /**
     * @param {*} url 发送请求的地址
     * @param {*} type 请求方式("POST" 或 "GET"), 默认为 "GET"
     * @param {*} data 发送到服务器的数据,数组存储,如:{"date": new Date().getTime(), "state": 1}
     * @param {*} async 默认值: true。默认设置下,所有请求均为异步请求。如果需要发送同步请求,请将此选项设置为 false。注意,同步请求将锁住浏览器,用户其它操作必须等待请求完成才可以执行。
     * @param {*} dataType 预期服务器返回的数据类型,常用的如:xml、html、json、text
     * 
     * @returns 返回一个异步对象promise,这个对象有两个状态,分别为成功和失败,用法如下:
     *          //------------先声明一个变量来接收这个函数的返回值------------------
     *          var promise = ajax(url, type, data, async, dataType);
     *          //------------如果失败的话,就会进入fail里面,执行失败的代码---------
     *          promise.fail(function (jqXHR, textStatus, err) {
                    console.log("错误码"+jqXHR.status+":"+err)
                });
                //------------如果成功的话,就会执行done代码,res会返回的结果---------
                promise.done(function (res) {
                    console.log(res)
    		        });
     */
    
    function ajax(url, type, data, async, dataType) {
      var baseURL = process.env.VUE_APP_BASE_API;
      var url_ = baseURL + url;
      async = (async==null || async=="" || typeof(async)=="undefined")? "true" : async;
      type = (type==null || type=="" || typeof(type)=="undefined")? "get" : type;
      dataType = (dataType==null || dataType=="" || typeof(dataType)=="undefined")? "json" : dataType;
      data = (data==null || data=="" || typeof(data)=="undefined")? '' : data;
      var promise = $.ajax({
          type: type,
          async: async,
          data: data,
          url: url_,
          dataType: dataType,
          error: function (XMLHttpRequest, textStatus, errorThrown) {
            console.error("ERROR - URLManager.ajax() - Unable to retrieve URL: " + url_ + ", [" + XMLHttpRequest.status + "]" + errorThrown);
        }
      });
      return promise;
    };
    
    export default ajax
    
  • 封装栅格;自适应的栅格,基于bootstrap,不需要每次去化边框了,直接调用这个Vue组件,然后传入宽高就可以实现代码复用。

    <!--
     * @Description  : 边框组件,把边框封装成一个组件,要用边框的时候直接调用这个组件就可以了,不需要每次都写边框
    -->
    <template>
    <div class="card border-0 p-0" :class="getBorderStyle()">
        <div class="main_left fl">
            <!--左上边框-->
            <div class="t_line_box">
                <i class="t_l_line"></i>
                <i class="l_t_line"></i>
            </div>
            <!--右上边框-->
            <div class="t_line_box">
                <i class="t_r_line"></i>
                <i class="r_t_line"></i>
            </div>
            <!--左下边框-->
            <div class="t_line_box">
                <i class="l_b_line"></i>
                <i class="b_l_line"></i>
            </div>
            <!--右下边框-->
            <div class="t_line_box">
                <i class="r_b_line"></i>
                <i class="b_r_line"></i>
            </div>
            <!-- <div class="main_title">
                <i class="fa" :class="getTitleIcon()"></i>
                {{title}}
            </div> -->
            <!-- slot vue中的插槽,意思是预留一个位置,到时候父组件调用的时候就可以显示在这个位置了 -->
            <slot></slot>
        </div>
    </div>
    </template>
    
    <script>
    export default {
        name: 'border',
        props: {
            item: String,
            // title: String, //标题文字
            titleIcon: String //标题图标的属性值
        },
        methods: {
            /**
             * 标题图标的属性
             */
            getTitleIcon() {
                return setBorderStyle(this.titleIcon)
            },
            /**
             * 边框的属性
             */
            getBorderStyle() {
                return setBorderStyle(this.item)
            }
        },
    }
    
    function setBorderStyle(param) {
        if (param !== undefined) {
            var itemArr = (param).split(' ');
            var style = [];
            for (let i = 0; i < itemArr.length; i++) {
                style.push(eval("(" + "{'" + itemArr[i] + "': true}" + ")"));
            }
            return style
        } else {
            return;
        }
    }
    </script>
    
    <style>
    @import '../../assets/css/bootstrap_plugins.css';
    
    [class*="col"] {
        padding: 0px !important;
    }
    
    .main_left {
        background: url('../../assets/img/background.png') no-repeat;
        background-size: 110% 110%;
        box-sizing: border-box;
        border: 2px solid rgba(100, 75, 245, 0.8);
        position: relative;
        box-shadow: 0 0 10px rgba(100, 75, 245, 0.8);
    }
    
    .fl {
        float: left;
    }
    
    .main_title {
        width: 180px;
        height: 35px;
        line-height: 33px;
        background-color: #2C58A6;
        border-radius: 18px;
        position: absolute;
        top: -17px;
        left: 50%;
        margin-left: -90px;
        color: #fff;
        font-size: 18px;
        font-weight: 600;
        box-sizing: border-box;
        padding-left: 45px;
        z-index: 1000;
    }
    
    .main_title i {
        position: absolute;
        top: 8px;
        left: 10px;
    }
    
    .l_t_line {
        width: 3px;
        height: 40px;
        left: -3px;
        top: -3px;
    }
    
    .t_l_line {
        height: 3px;
        width: 120px;
        left: -3px;
        top: -3px;
    }
    
    .t_line_box {
        position: absolute;
        width: 100%;
        height: 100%;
    }
    
    .t_line_box i {
        background-color: rgb(3, 245, 213);
        box-shadow: 0px 0px 10px rgb(3, 245, 213);
        position: absolute;
    }
    
    .t_r_line {
        height: 3px;
        width: 26px;
        right: -3px;
        top: -3px;
    }
    
    .r_t_line {
        width: 3px;
        height: 24px;
        right: -3px;
        top: -3px;
    }
    
    .l_b_line {
        width: 3px;
        height: 24px;
        left: -3px;
        bottom: -3px;
    }
    
    .b_l_line {
        height: 3px;
        width: 26px;
        left: -3px;
        bottom: -3px;
    }
    
    .r_b_line {
        width: 3px;
        height: 40px;
        right: -3px;
        bottom: -3px;
    }
    
    .b_r_line {
        height: 3px;
        width: 120px;
        right: -3px;
        bottom: -3px;
    }
    </style>
    
    

上面几段代码是比较常用和核心点的模块,其他的代码就不一一列举了,有需要的欢迎骚扰。

6、开发中遇到的困难

一个人把整套系统开发出来还是需要蛮长的时间,因为项目组有很多的新人,为了锻炼他们,就分模块的交给他们实现,虽然一些图形达到了要求,但是代码质量来说还是需要提高,前期主要是边和他们说明代码的问题,另一方面又自己改善他们的代码,并根据他们的思路,引入最恰当的图形,然后再提交到git上面去。

困难遇到的不算很多,因为没有继续用很新的技术,为了保守起见,先完成系统设计再说,很多的坑都是踩过了,遇到问题也能很快的解决。真正遇到瓶颈的是算法方面,这才是整套系统中遇到最大的难题,前面也提到只完成了85%左右,因为还有一部分是这边做动态路径实时预览跟踪伪基站的移动规律,但这部分的数据如何从原始数据中提取出来,转换成什么格式,都不是很清楚,即使到最后完成了一点点demo,但也没有达到预期,就砍掉了这部分。这需要算法去计算伪基站的移动路线经纬度,还需要和地图上的道路经纬度想匹配,才可以直观的展示在图形上,这部分因为时间和难度,完完全全就放弃了。

预期想做出类似的效果如下图所示,这是当年其他参赛选手完成的图形。

image-20210824091456483.png

对于大多数工科类的学生来说,毕业设计可能问题不是很大,真正的困难可能是系统完成后如何去写毕业论文,如果降低重复率。一些人毕业设计唰唰唰几下子就完成了,并且也实现的很好,但是在写论文上面却拖了很久,因为平常没有训练过,不知道如何下笔,思考半个小时都不知道如何写起,最后实在迫不得已把别人文章里面的段落复制过来,自己去改。但又发现,别人写的实在是太好了,都不知道如何改起,只能稍微改几个字或者词,这样基本上是不能通过论文查重的。

之前有看过如果降低别人文章句子的重复率,如何根据这种方法,即使是同一个意思,所组成的字词都不一样,重复率自然而然就降低了,很幸运第一次查重就在2%以下(这里不是自夸哈,只是想分享一下如何掌握写作的技巧,降低重复率)。

7、毕业论文降重写作技巧

关于上述所说的如何降低论文的重复率,下面以自身写论文的经验进行分享一下。其中这套系统的毕业论文,知网第一次查重1.7%;另外还写了一篇基于JavaEE的智能人事管理系统(OA办公系统)毕业论文,维普第一次查重6.5%左右。

怎么写的才能使得重复率这么低呢?首先,我觉得工科类的论文最好写了,尤其是有毕业设计做支撑的,基本上很难超过学校的查重率标准线。相反,个人觉得如果写金融等文科类的毕业论文,是比较难写的。

为什么这么说呢,因为论文里面有一个大的模块,是系统设计与实现,光这一块,就可以写上全部论文的三分之一篇章了,自己按照系统的每一个功能模块截图,然后说明其使用方法以及达到的效果,然后把数据库设计以及数据表以表格的形式插入到文章中,那些套话部分就差不多只剩一半的篇幅了。

image-20210824091712484.png

重点不是介绍如何写系统设计与实现这部分,因为这部分如果系统是自己做的,然后按照自己的思路阐述,必要时还可以贴上代码,完成不需要这部分的查重率不会过。重点要讲的是如何降低选题背景,国内外研究现状以及研究意义等套话部分,这部分的内容必须要借鉴参考文献,但是看论文是一件极其枯燥的事情,不可能把硕士论文全部看完一遍,然后提取一个中心思想写在这里,这对大部分的本科生来说还是有一点困难的,都是读了一段往了一段,基本上记不住讲的是什么。有人说可以通过摘要来提取核心思想啊,但都不看论文,光摘要能理解透吗。

一个投机取巧的办法是在选取文献的时候,可以先选择几篇质量较好的文章(为了区分,称为A),然后通过A文章的国内外研究现状部分引入的文献,把这些适合自己论文主题的文献都收集下来,再去知网收集下载具体看某篇文章,根据这些人总结的核心思想,在自己阅读完论文的前提下去改造语句,先按照自己对文章和语句的理解,对A文章中总结的核心思想按照自己想表达的意思转化出来,估计大部分都会觉得语句不通顺,可以尝试着使用有道翻译,先把中文翻译成英文,然后把翻译好的英文再翻译为中文,可以发现,很多地方虽然是一个意思,但是使用到的字不一样了,这也是降低重复率的严重方法。

好了,如何降低论文写作的重复率写分享这两个方法,第一是参考别人写好的核心思想,第二是有道翻译。后期有需要的话可以分享更多降重小技巧。

对了,有很多人喜欢查重之前去淘宝或者其他的渠道买查重的机会,无非就是两种可能,一是查重率很高,然后不断的修改;二是查重率低于学校要求标准线,以为自己没事了,可等学校一查,凉凉!这些是不可信的,因为他们用的查重软件和学校用的不一样,论文数据库也不一致,根本没有参考左右,相反,如果写的比较好的文章,还非常有可能被别人窃取,然后卖出去,等自己再查论文的时候,发现全篇都是抄的,这就很尴尬。

8、结语

以上就是分享的全部内容了。祝愿各位写论文的大佬都能顺利完成查重,也祝愿各位大佬写的系统没有bug(找bug是一件比较痛苦的过程)。

如果有大学生创新创业训练计划项目不懂的可以骚扰我哈!当然,如果是想要这套系统源码的,也可以联系,不过需要付出一点点!

或者需要定制可视化系统的、Java、前端等等都欢迎骚扰,去年帮助很多人码了很多套可视化大屏系统,相比于定制来说,直接buy成品会省很多!