少儿 Python 编程 _ 第十九讲:数据分析网站

本讲是一个综合实例,结合了数据分析和构建网站技术,提供用户通过浏览器上传文件,在服务端实现分析上传的数据,并生成动态统计表格,回传给用户端。其中用到表单上传文件、读取 Excel 数据表文件、统计图表、生成动态网页等技术。

19.1 上传文件

让用户上传文件,处理后再把结果返回给用户,是一个很常用的操作,比如用户上传一张相片,服务器端经过美颜或者换背景处理后显示在网页上;又如用户上传一个 Excel 数据表文件,数据统计分析后把统计结果显示给用户。开发者提供前端和后端服务。用户使用网络中任意一台计算机或者手机,只需要用浏览器即可实现需要的功能,无需安装任何软件。

上传文件功能也可通过表单实现。本例展示了上传文件的方法。为简化代码逻辑,将 HTML 模板也写入了 Python 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
01 from flask import Flask,request,redirect,url_for
02 import os
03  
04 UPLOAD_DIR = "files"
05  
06 app=Flask(__name__)
07  
08 @app.route("/upload.html")
09 def page1():
10     return """
11 <html>
12     <h1>请上传文件</h1>
13     <form action="show.html" method=post enctype=multipart/form-data>
14         <input type=file name=upload_file>
15         <input type=submit value='上传'>
16     </form>
17 <html>
18     """
19  
20 @app.route("/show.html", methods=["POST","GET"])
21 def page2():
22     if request.method=="POST":
23         file = request.files['upload_file']
24         if file:
25             if not os.path.exists(UPLOAD_DIR):
26                 os.mkdir(UPLOAD_DIR)
27             file.save(os.path.join(UPLOAD_DIR, file.filename))
28             return "上传成功"
29     return redirect(url_for('page1'))
30  
31 app.run(host="0.0.0.0", port="8088")

第 01 行引入 flask 模块中的几种方法:Flask 用于建立服务,request 用于接收用户传来的数据,redirect 用于网页跳转,url_for 用于查找函数名对应的路径。

第 04 行定义了存储上传文件的目录。

第 08-18 行定义了函数用于提供用户选择文件和上传文件的界面。

第 08-09 行关联了网址中的路径与函数,当用户在浏览器中打开“/upload.html”时调用函数 page1。

第 10-18 行描述了返回的 HTML 文件,使用三个双引号可定义带有回车的字符串。读者也可以将这段代码写成文件,放在 templates 目录下,在程序中用 render_template 函数加载。

第 13-15 行定义了表单,表单提交时以 POST 方式访问路径“show.html”。

第 14 行添加了选择上传文件的控件,并将该控件命名为 upload_file,以便程序读取。

第 20-29 行定义了用户上传文件后的响应界面。

第 22 行判断是否为“POST”请求。

第 23 行获取上传的文件,赋值给变量 file。

第 24 行判断 file 变量是否正常。

第 25-26 行判断保存文件的目录是否存在,如果不存在,则创建该目录。

第 27 行使用文件保存路径加文件名,构造文件在服务端的存储路径。

第 28 行向客户端返回“上传成功”字符串。

第 29 行用于处理在非“POST”请求的情况下,跳转到 page1 对应的地址 upload.html 继续显示上传文件界面。

程序运行结果如图 19.1 所示:

图19.1文件上传界面

19.2 PyEcharts

探索性数据分析简称EDA,它是通过图表方式探索数据的结构和规律的一种数据分析方法。常用的方法有直方图、箱线图、变量分析等等。

Echarts 是一个用 Javascript 实现的商业级数据图表工具,它可以流畅的运行在计算机和手机设备上,兼容当前绝大部分浏览器。Pyecharts 是 Python 版本的 Echarts,它的使用方法类似前面讲过的 matplotlib,虽然它主要用于网页显示,但可以在 Jupyter Notebook 中调试,并且生成 HTML 文件。调试的显示效果和 HTML 页面效果完全一样。与其它 EDA 工具相比,它使用更方便,配色方案也更加考究。

之前学习了使用 Matplotlib 绘制图表,Matplotlib 虽然能实现绝大多数的图表绘制,但默认字体和配色效果都不太美观,如果想做出高级的图表,需要设置大量的参数,虽然它也能将图表保存成图片,但只支持静态图片,且嵌入网页操作比较复杂。

PyEcharts 解决了以上问题,它可以生成与用户交互的动态网页,有完美的字体和配色方案,用简单方法绘制复杂图片,并且可以方便地与 Flask 框架配合使用。

本节将介绍 PyEcharts 的使用方法。

19.2.1 准备环境

1.安装软件

在使用 PyEcharts 之前需要用以下命令安装模块,PyEcharts 不同版本使用方法不同,本例中使用了当前的默认版本 1.4.0。在 Windows 中打开:开始 ->所有程序 ->Anaconda 3->Anaconda Prompt,在终端输入:

1
01 $ pip install pyecharts

2.数据准备

将表 19.1 中的数据存入 Excel 数据表 test.xlsx,作为数据分析的素材。表中共有 9 条记录,8 个字段,包括字符型(姓名)、类别型(性别、类型、年龄、出场频率),数值型(智力、体力、颜值)。

表19.1 待分析的数据表

19.2.2 绘制柱状图

柱状图非常实用,本例展示了在同一张图中显示多柱对比效果的方法,对比了智力和体力两个特征,将每个人的智力和体力用不同颜色的柱表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
01 import pandas as pd
02 from pyecharts import charts
03 from pyecharts import options as opts
04  
05 df = pd.read_excel('test.xlsx')
06  
07 bar = charts.Bar()
08 bar.add_xaxis(df['姓名'].tolist())
09 bar.add_yaxis("智力", df['智力'].tolist())
10 bar.add_yaxis("体力", df['体力'].tolist())
11 bar.set_global_opts(title_opts=opts.TitleOpts(title="喜羊羊与灰太狼"))
12 bar.render('test.html')
13 bar.render_notebook()

第 01 行引入了数据表支持模块 Pandas,并将其重命名为 pd。

第 02 行引入了绘图模块 PyEcharts 的子模块 charts 用于绘制图表。

第 03 行引入了绘图模块 PyEcharts 的子模块 options 并重命名为 opts 用于设置参数。

第 05 行读入数据表文件 test.xlsx,并将其内容赋值给变量 df,注意将数据文件放在与程序相同的目录中。

第 07 行创建了 Bar 对象用于绘制柱图。

第 08 行设置了柱图的横坐标数据为姓名字段的内容,设置前将数据格式转换为列表。

第 09 行设置了柱图的纵坐标数据,第一个参数为显示的文字,第二个参数为柱的高度,本行设置了智力字段的内容。

第 10 行设置了柱图的纵坐标数据为体力字段的内容,工具用不同颜色区分不同的数据。

第 11 行将标题设置为“喜羊羊与灰太狼”,其中用到了 opt 模块的标题参数工具 TitleOpts。

第 12 行将图表数据渲染后保存到 test.html 文件中,此时当前目录下产生了新文件 test.html,如果 test.html 已经存在,程序将替换文件内容。

第 13 行将图表内容显示在 Jupyter Notebook 窗口之中。

程序运行结果如图 19.2 所示:

图19.2 柱图效果

在 Jupyter Notebook 中,可以显示图表的动态效果:将鼠标放在柱上,可以反馈当前柱对应的数据。

19.2.3 绘制饼图

饼图使用的数据和其它图表不同,需要指定每个区域显示的文字以及对应的数量。

1
2
3
4
5
6
7
01 pie = charts.Pie()
02 m = len(df[df['性别']=='男'])
03 f = len(df[df['性别']=='女'])
04 data = [['男',m],['女',f]]
05 pie.add("", data, radius=[60, 150],)
06 pie.render('test.html')
07 pie.render_notebook()

第 01 行创建了 Pie 对象用于绘制柱图。

第 02 行统计了性别为男的实例数量,本例中为 6 个。

第 03 行统计了性别为女的实例数量,本例中为 3 个。

第 04 行构建了两层列表,外层对应饼图中划分的不同区域,内层的两个值分别是:显示的文字及对应的大小。

第 05 行将数据加入饼图,其中 radius 参数指定了饼图内圈和外圈半径。

程序运行结果如图 19.3 所示:

图19.3 饼图效果

19.2.4 绘制折线图

折线图的绘制方法类似柱图,本例中除了绘制基本的折线图,还加入了图中最大值、最小值、以及平均值虚线的显示。

1
2
3
4
5
6
7
8
9
10
11
01 bar = charts.Line()
02 bar.add_xaxis(df['姓名'].tolist())
03 bar.add_yaxis("智力", df['智力'].tolist(),
04          markline_opts=opts.MarkLineOpts(data=[opts.MarkLineItem(type_="average")]),
05          markpoint_opts=opts.MarkPointOpts(data=[opts.MarkPointItem(type_="max")]))
06 bar.add_yaxis("体力", df['体力'].tolist(),
07          markline_opts=opts.MarkLineOpts(data=[opts.MarkLineItem(type_="average")]),
08          markpoint_opts=opts.MarkPointOpts(data=[opts.MarkPointItem(type_="min")]))
09 bar.set_global_opts(title_opts=opts.TitleOpts(title="喜羊羊与灰太狼"))
10 bar.render('test.html')
11 bar.render_notebook()

第 01 行创建了 Pie 对象用于绘制柱图。

第 03-05 行设置了柱图的纵坐标数据智力,第一个参数为显示的文字,第二个参数为柱的高度,参数 markline_opts 用于设置标志线,这里用标志线显示了均值 average,参数 markline_opts 用于设置标志点,此处将显示最大值 max 点为标志点。

第 06-08 行设置了柱图的纵坐标数据体力,标签线设置为均值 average,标志点设置为最小值点。

程序运行结果如图 19.4 所示:

图19.4折线图效果

本节介绍了三种常用图表,Pyecharts 还支持更多的图表,不同版本的 Pyecharts 的函数调用方法也有差异,因此想自如地使用该模块,需要学会从三方模块自带的例程中学习使用方法,更多实例请参考:https://github.com/pyecharts/pyecharts 的 example 目录下的例程。

课后练习:(练习答案见本讲最后的小结部分)

练习一:从 git 的 pyecharts 示例代码中学习一种之前没学过的图表,在 Jupyter Notebook 中正常运行,并讲述每行程序的作用。

19.3 显示图表网页

以上几个实例中用 render 方法将动态网页保存成 HTML 格式的文件,只要在 Flask 框架中需要显示图表处返回该网页的内容就可以在客户端的浏览器中正常显示图表。

以下实例结合了本讲学习的上传文件和制作图表功能,根据用户上传的 Excel 文件生成图表,并将结果反馈给浏览器显示图表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
01 from flask import Flask,request,redirect,url_for,render_template
02 import os
03 import pandas as pd
04 from pyecharts import charts
05  
06 UPLOAD_DIR = "files"
07 app = Flask(__name__)
08  
09 @app.route("/upload.html")
10 def page1():
11     return """
12 <html>
13     <h1>请上传文件</h1>
14     <form action="show.html" method=post enctype=multipart/form-data>
15         <input type=file name=upload_file>
16         <input type=submit value='上传'>
17     </form>
18 <html>
19     """
20  
21 @app.route("/show.html", methods=["POST","GET"])
22 def page2():
23     if request.method=="POST":
24         file = request.files['upload_file']
25         if file:
26             if not os.path.exists(UPLOAD_DIR):
27                 os.mkdir(UPLOAD_DIR)
28             filename = os.path.join(UPLOAD_DIR, file.filename)
29             file.save(filename)
30             df = pd.read_excel(filename)
31             bar = charts.Bar()
32             bar.add_xaxis(df['姓名'].tolist())
33             bar.add_yaxis("智力", df['智力'].tolist())
34             bar.add_yaxis("体力", df['体力'].tolist())
35             bar.render('templates/test.html')
36             return render_template('test.html')           
37     return redirect(url_for('page1'))
38  
39 app.run(host="0.0.0.0", port="8088")

本例中的大部分代码来自前面例程,因此不再逐行讲解,只介绍特殊部分。

第 01-20 行代码引入头文件,并实现上传文件界面。

第 21-37 行对上传的文件数据分析做图,将其结果返回给客户端。

第 29 行将上传的文件存储在服务器端的文件之中。

第 30 行打开保存的数据表文件。

第 31-35 行绘制图表,并将图表存储在 'templates/test.html' 文件中。

第 36 行将 test.html 文件内容返回给客户端,需要注意的是第 35 和 36 行中的 test.html 是同一文件,由于调用方式不同,第 35 行用 pyecharts 保存时指定了全路径。而 36 默认从模板目录读取文件,因此不加入模板目录名。

课后练习:

练习二:用 Excel 数据表创建一个小学生从一年级到六年级 12 个学期语文、数学、英语各科成绩的数据表,使用浏览器上传到服务器,服务器用折线图绘制其各科成绩的曲线,并在图中标出语文的平均分、最高分、最低分。做饼图分析英语成绩为“优秀”的比例(大于等于 90 分认为优秀)。

19.4 思维训练

19.4.1 学方法和学知识

有一次,我让某位小朋友查一查“红果酱”怎么做,于是她去百度搜索到一个“一斤红果加一斤糖”的做法,我隐约觉得糖是不是放太多了?于是我也用类似方法搜索,找到三种排名靠前的三种不同做法,最终将红果和糖的比例确定为 5:3,并按照多数帖子中描述的步骤进行了操作。当面对同一个陌生的问题,不同人有不同的处理方法。

在这个过程中,成人的常识起到了一定作用,成人能估计出一斤糖的甜度。而对比多种教程则是一种通过“数据”训练“模型”的方法,把脆弱的单一方法,扩展成实现目标的多条路径,以便处理更多的突发状况。另外,还有一些小技巧,比如查看美食攻略的排名、点赞数等等。可以说,这不仅是儿童和成人的区别,而是学习方法的区别。

在这个信息爆炸的时代,学习一门技能不再需要报班、买书,网络上有大量唾手可得的教程、视频,水平也良莠不齐(培训班也存在同样问题)。学习对大多数人来说,学习已经不仅是努力、认真、听老师的话;更高阶的技能是辨别学习资料的品质、合理安排时间和强度,寻找最优的途径,以及能客观地认识自己和评价外界的信息。

时代不同了,我们拥有了更多选择,判断力也比以往更加重要。如何培养判断力?判断一个教程和买一件衣服的规则不同,而货比三家、以及强大的常识系统是所有判断的基础。常识系统来自于行千里路读万卷书,尝试更多新鲜事物,而学习和思维的方法则源于通过有目标的训练培养出的良好习惯。

来看看机器人是如何学习的。Toyota 正在开发一款厨房机器人,首先,让机器人拥有了视觉、触觉、运动,以及与人交流的基本能力,然后训练做具体工作的能力:比如通过多次训练机器人从架子上拿东西,以适应于不同环境,不同的架子,不同的光线,处理意外情况……同样也是先构建基本技能和学习方法,然后学习各种具体的技能。这种模仿式的学习,不再需要海量的数据训练,使机器人可以通过少量学习即可掌握各种家务技能,相比另一个只具有精准抓取技能的机器人,虽然没那么准确,但适用于更多场合。

人也是一样,把一项技能训练得再精准,也没有机器精准。从工厂的蓝领,到实验室的白领,谁又能保证你的技能有一天不会被机器人取代。学霸学方法,学渣学知识。而拥有学习的技能,长远看,可以用短时间培养出新技能,永不过时;近期看,也省了不少报补习班的费用。

19.4.2 规则与练习

除了数学或者神学这些人造领域,现实中基本没有用之四海皆准的方法,拥有的方法越多能力越强。最重要的两种学习方法是:在练习中领悟和直接学习规则,它们各有利弊。如果需要快速提高,学习规则肯定是捷径,使用同样的时间和精力,的确能让人暂时领先;从素质教育的角度看,如果有足够时间,建议多做练习,毕竟练习具体技术的过程也是对比、总结、提炼,泛化,建立内部框架的过程。

在反复练习一项技能,尤其在寻找一个问题多个解法的过程中,一些子模块会反复出现,并且可以重用。比如练字过程中,虽然常用字有几千个,但是基本笔划只有几种,每种笔划写法也有限,随着练习越来越多,即使没人告诉你“捺”的几种写法,也能训练成一种习惯。与此相对的是学习一些前人总结好的规则,比如永字八法,代入已有的规则,也能让人在短时间内快速提高水平。

规则往往是使用语言描述的链式结构,而通过练习得到的是交叉连结的网状结构。使用语言表达时,由于只能提取主干,看起来没什么区别。但是链式结构非常脆弱,只要其中一个环节断开,整个链条就会完全失效,而网状结构中通向终点的路径不只一条,具有足够的健壮性。

19.4.3 复习和复用

学习也没有捷径,再好的方法离不开反复训练。复习可以加强记忆,大多数情况下,只有被反复使用的技能才能记住;另外,在多次做同一件事的时候,内部便开启了“优化”过程:解决一个问题可能涉及很多因素,其中哪些重要,哪些只是偶然发生?如果只做一次,可能永远都无法了解。

另一个技巧是学习一些类似的技能,网状结构中很多区域都可以被复用。当你学习了素描、色彩、书法、漫画,共性的部分被一次次激发,相对于单一的链条,将更能找出其重点,并且融会贯通。尤其是对于无法用语言描述的技能,练习一段时间画画之后再练习书法,就感觉上手很快,练习书法的过程中,绘画水平也有所提高。

学习每一种技能都需要花很多的时间和精力,这并不容易,如果钢琴已经学有小成,就更愿意继续学习钢琴,而不是从头再学小提琴。然而大多数人学习钢琴的目标都不是成为音乐家,少年儿童成长阶段主要以培养能力为主,建议做更多尝试。

19.5 小结

19.5.1 单词

本讲需要掌握的英文单词如表 19.2 所示。

表19.2本讲需要掌握的英文单词

19.5.2 习题答案

1.练习一:从 git 的 pyecharts 示例代码中学习一种之前没学过的图表,在 Jupyter Notebook 中正常运行,并讲述每行程序的作用。

第一步:打开网址 https://github.com/pyecharts/pyecharts。

第二步:在网页下方的 Demo 中找一个感兴趣的图表,记住名字。

第三步:打开代码的 examples 目录,找到名字对应的示例代码。

第四步:将示例代码复制到自己的 Jupyter Notebook 文件中。

第五步:把程序中的 render 改成 render_notebook,然后运行程序。

第六步:分析程序时注意:创建需要的控件,用 add 函数加入待分析的数据,用 opt 设置需要显示的特征属性。

2. 练习二:用 Excel 数据表创建一个小学生从一年级到六年级 12 个学期语文、数学、英语各科成绩的数据表,使用浏览器上传到服务器,服务器用折线图绘制其各科成绩的曲线,并在图中标出语文的平均分、最高分、最低分。做饼图分析英语成绩为“优秀”的比例(大于等于 90 分认为优秀)。

设计数据如表 19.3 所示:

表19.3 学习成绩数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
01 from flask import Flask,request,redirect,url_for,render_template
02 import os
03 import pandas as pd
04 from pyecharts import charts
05 from pyecharts import options as opts
06  
07 UPLOAD_DIR = "files"
08    
09 app=Flask(__name__)
10    
11 @app.route("/upload.html")
12 def page1():
13     return """
14 <html>
15     <h1>请上传文件</h1>
16     <form action="show.html" method=post enctype=multipart/form-data>
17          <input type=file name=upload_file>
18          <input type=submit value='上传'>
19     </form>
20 </html>
21 """
22  
23 @app.route("/show.html", methods=["POST","GET"])
24 def page2():
25     if request.method=="POST":
26         file = request.files['upload_file']
27         if file:
28             if not os.path.exists(UPLOAD_DIR):
29                 os.mkdir(UPLOAD_DIR)
30             filename = os.path.join(UPLOAD_DIR, file.filename)
31             file.save(filename)
32             df = pd.read_excel(filename)
33             pie = charts.Pie()
34             m = len(df[df['英语']>=90])
35             f = len(df[df['英语']<=89])
36             data = [['90',m],['89',f]]
37             pie.add("", data, radius=[60, 150],)
38             pie.render('templates/happy.html')
39             a=render_template('happy.html')
40         
41             bar = charts.Line()
42             bar.add_xaxis(df['学期'].tolist())
43             bar.add_yaxis("语文", df['语文'].tolist(),
44 markline_opts=opts.MarkLineOpts(data=[opts.MarkLineItem(type_="average")]),
45    markpoint_opts=opts.MarkPointOpts(data=[opts.MarkPointItem(type_="max"),
46                                          opts.MarkPointItem(type_="min")]))
47             bar.add_yaxis("数学", df['数学'].tolist())
48             bar.add_yaxis("英语", df['英语'].tolist())
49             bar.render('templates/happy1.html')
50             b=render_template('happy1.html')
51             return a + b
52     return redirect(url_for('page1'))
53  
54 app.run(host="0.0.0.0", port="8088")