文章来源Python Django+SQL+Pandas+Pyecharts自建在线数据分析平台(一)
作者ccpic

感谢:感谢作者 ccpic 分享的优质内容,本网页主要用于学习知识的存档备份,欢迎点击原网页支持作者。

(一)需求分析&技术实现

(二)初步搭建Django环境

(三)页面布局&Django模板

(四)SQL+Pandas初步处理数据

(五)前端表单交互

(六)Ajax异步传参与加载

(七)前端数据格式的处理

(八)DataTables接管前端表格

(九)Pyecharts实现交互图表

(十)静态图表的展示

(十一)“导出数据至Excel”功能

(十二)添加和配置缓存

(十三)用户登录系统

(十四)部署Django至生产环境

在数据可视化的工作中,静态图表是一个非常容易被忽视的形式。但在信息化程度较低的传统行业中,静态图表却是必不可缺的存在,主要是因为:

让老板们自己操作BI是天方夜谭

好吧,即使抛开老板们的主观能动性和BI的操作难度,我们也要有觉悟——PPT还是大部分行业工作中汇报与沟通的最常用媒介。而以Matplotlib为代表的的静态图表在某些方面还是更加契合这个媒介:

  • Echarts, Highcharts等在线交互图表注定会因为“交互”的特性而不将一些信息置于表面(如鼠标hover才显示的tooltip),截图或保存图片无法呈现。
  • 在线交互图表靠截图或保存图片展示的DPI远低于手动绘制的图表。
  • Matplotlib以及衍生的Seaborn等静态图表库往往具有高度可定制化的特性,对细节的展示度更优。
  • Matplotlib以及衍生的Seaborn等静态图表库与统计学方法的展示结合更容易。

以下面Matplotlib绘制的气泡图为例,展示了治疗糖尿病的胰岛素市场所有产品的销售额规模和净增长,中间有一个线性回归拟合的趋势线及CI和PI(回归带上方的是净增长显著高于预期,下方则反之,落在带内的则是统计学上不显著)。其中还有一些定制化的细节呈现方式,如只显示top30产品的名字;文字标签相互之间不堆叠等。

现有的在线图表方案几乎很难呈现这样的效果,但这却是Matplotlib或者Seaborn的拿手好戏。

所以本章的目标是用我们在建的Django系统也能动态展示上方这样的Matplotlib静态图表。

在上章我们创建的charts.py中新建一个mpl_bubble方法,绘制气泡图:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib as mpl
from matplotlib.ticker import FuncFormatter
from adjustText import adjust_text
from io import BytesIO
import base64
import scipy.stats as stats

myfont = fm.FontProperties(fname='C:/Windows/Fonts/msyh.ttc')

def mpl_bubble(x, y, z, labels, title, x_title, y_title,
x_fmt='{:.0%}', y_fmt='{:+.0%}',
y_avg_line=False, y_avg_value=None, y_avg_label='',
x_avg_line=False, x_avg_value=None, x_avg_label='',
x_max=None, x_min=None, y_max=None, y_min=None,
show_label=True, label_limit=15,
z_scale=1, color_scheme='随机颜色方案', color_list=None):

z = [x * z_scale for x in z] # 气泡大小系数

fig, ax = plt.subplots() # 准备画布和轴
fig.set_size_inches(15, 10) # 画布尺寸

# 手动强制xy轴最小值/最大值
if x_min is not None and x_min > min(x):
ax.set_xlim(xmin=x_min)
if x_max is not None and x_max < max(x):
ax.set_xlim(xmax=x_max)
if y_min is not None and y_min > min(y):
ax.set_ylim(ymin=y_min)
if y_max is not None and y_max < max(y):
ax.set_ylim(ymax=y_max)

# 确定颜色方案
if color_scheme == '随机颜色方案' or color_scheme is None:
cmap = mpl.colors.ListedColormap(np.random.rand(256, 3))
colors = iter(cmap(np.linspace(0, 1, len(x))))
else:
if len(x) <= len(color_list):
colors = color_list[:len(x)]
else:
colors = []
for i in range(len(x)):
colors.append(color_list[i % len(color_list)])
colors = iter(colors)

# 绘制气泡
for i in range(len(x)):
ax.scatter(x[i], y[i], z[i], color=next(colors), alpha=0.6, edgecolors="black")

# 添加系列标签,用adjust_text包保证标签互不重叠
if show_label is True:
texts = [plt.text(x[i], y[i], labels[i],
ha='center', va='center', multialignment='center', fontproperties=myfont, fontsize=10) for
i
in range(len(labels[:label_limit]))]
adjust_text(texts, force_text=0.5, arrowprops=dict(arrowstyle='->', color='black'))

# 添加分隔线(均值,中位数,0等)
if y_avg_line is True:
ax.axhline(y_avg_value, linestyle='--', linewidth=1, color='grey')
plt.text(ax.get_xlim()[1], y_avg_value, y_avg_label, ha='left', va='center', color='r',
multialignment='center',
fontproperties=myfont, fontsize=10)
if x_avg_line is True:
ax.axvline(x_avg_value, linestyle='--', linewidth=1, color='grey')
plt.text(x_avg_value, ax.get_ylim()[1], x_avg_label, ha='left', va='top',
color='r', multialignment='center', fontproperties=myfont, fontsize=10)

# 设置轴标签格式
ax.xaxis.set_major_formatter(FuncFormatter(lambda y, _: x_fmt.format(y)))
ax.yaxis.set_major_formatter(FuncFormatter(lambda y, _: y_fmt.format(y)))

# 添加图表标题和轴标题
plt.title(title, fontproperties=myfont)
plt.xlabel(x_title, fontproperties=myfont, fontsize=12)
plt.ylabel(y_title, fontproperties=myfont, fontsize=12)

"""以下部分绘制回归拟合曲线及CI和PI
参考
http://nbviewer.ipython.org/github/demotu/BMC/blob/master/notebooks/CurveFitting.ipynb
https://stackoverflow.com/questions/27164114/show-confidence-limits-and-prediction-limits-in-scatter-plot
"""

n = y.size # 观察例数
if n > 2: # 数据点必须大于cov矩阵的scale
p, cov = np.polyfit(x, y, 1, cov=True) # 简单线性回归返回parameter和covariance
poly1d_fn = np.poly1d(p) # 拟合方程
y_model = poly1d_fn(x) # 拟合的y值
m = p.size # 参数个数

dof = n - m # degrees of freedom
t = stats.t.ppf(0.975, dof) # 显著性检验t值

# 拟合结果绘图
ax.plot(x, y_model, "-", color="0.1", linewidth=1.5, alpha=0.5, label="Fit")

# 误差估计
resid = y - y_model # 残差
s_err = np.sqrt(np.sum(resid ** 2) / dof) # 标准误差

# 拟合CI和PI
x2 = np.linspace(np.min(x), np.max(x), 100)
y2 = poly1d_fn(x2)

# CI计算和绘图
ci = t * s_err * np.sqrt(1 / n + (x2 - np.mean(x)) ** 2 / np.sum((x - np.mean(x)) ** 2))
ax.fill_between(x2, y2 + ci, y2 - ci, color="#b9cfe7", edgecolor="", alpha=0.5)

# Pi计算和绘图
pi = t * s_err * np.sqrt(1 + 1 / n + (x2 - np.mean(x)) ** 2 / np.sum((x - np.mean(x)) ** 2))
ax.fill_between(x2, y2 + pi, y2 - pi, color="None", linestyle="--")
ax.plot(x2, y2 - pi, "--", color="0.5", label="95% Prediction Limits")
ax.plot(x2, y2 + pi, "--", color="0.5")

# 保存到字符串
sio = BytesIO()
plt.savefig(sio, format='png', bbox_inches='tight', transparent=True, dpi=600)
data = base64.encodebytes(sio.getvalue()).decode() # 解码为base64编码的png图片数据
src = 'data:image/png;base64,' + str(data) # 增加Data URI scheme

# 关闭绘图进程
plt.clf()
plt.cla()
plt.close()

return src

上方代码比较复杂,不要被吓着了,其实绝大部分都是为了完成Matplotlib的绘图结果。本章主要侧重于Django展示图片的方法,Matplotlib的部分不做讲解,毕竟那是另外一个超级大坑,能又写一个系列教程。

所以只需要特别注意下面这个片段,其他部分可以替换成你自己的matplotlib代码:

from io import BytesIO
import base64
...

def mpl_bubble(x, y, z, labels, title, x_title, y_title,
...):

...

# 保存到字符串
sio = BytesIO()
plt.savefig(sio, format='png', bbox_inches='tight', transparent=True, dpi=600)
data = base64.encodebytes(sio.getvalue()).decode() # 解码为base64编码的png图片数据
src = 'data:image/png;base64,' + str(data) # 增加Data URI scheme

...

return src

这段代码是将上方大量代码绘制的Matplotlib图片**保存为base64编码的字符串,再在头部加上Data URI scheme。**这步可以说是浏览器展示静态图片的核心步骤。

我们再扩展上一章views.py中的prepare_chart方法,增加一类”bubble_performance”生成数据并传入到之前准备好的绘图方法中。注意和Pyehcarts不一样,这里直接取得返回的值就好了,不需要.dump_options():

D_TRANS = {
'MAT': '滚动年',
'QTR': '季度',
'Value': '金额',
'Volume': '盒数',
'Volume (Counting Unit)': '最小制剂单位数',
'滚动年': 'MAT',
'季度': 'QTR',
'金额': 'Value',
'盒数': 'Volume',
'最小制剂单位数': 'Volume (Counting Unit)'
}

def prepare_chart(df, # 输入经过pivoted方法透视过的df,不是原始df
chart_type, # 图表类型字符串,人为设置,根据图表类型不同做不同的Pandas数据处理,及生成不同的Pyechart对象
form_dict, # 前端表单字典,用来获得一些变量作为图表的标签如单位
):
label = D_TRANS[form_dict['PERIOD_select'][0]] + D_TRANS[form_dict['UNIT_select'][0]]

if chart_type == 'bar_total_trend':
df_abs = df.sum(axis=1) # Pandas列汇总,返回一个N行1列的series,每行是一个date的市场综合
df_abs.index = df_abs.index.strftime("%Y-%m") # 行索引日期数据变成2020-06的形式
df_abs = df_abs.to_frame() # series转换成df
df_abs.columns = [label] # 用一些设置变量为系列命名,准备作为图表标签
df_gr = df_abs.pct_change(periods=4) # 获取同比增长率
df_gr.dropna(how='all', inplace=True) # 删除没有同比增长率的行,也就是时间序列数据的最前面几行,他们没有同比
df_gr.replace([np.inf, -np.inf, np.nan], '-', inplace=True) # 所有分母为0或其他情况导致的inf和nan都转换为'-'
chart = echarts_stackbar(df=df_abs,
df_gr=df_gr
) # 调用stackbar方法生成Pyecharts图表对象
return chart.dump_options() # 用json格式返回Pyecharts图表对象的全局设置
elif chart_type == 'bubble_performance':
df_abs = df.iloc[-1,:] # 获取最新时间粒度的绝对值
df_share = df.transform(lambda x: x / x.sum(), axis=1).iloc[-1,:] # 获取份额
df_diff = df.diff(periods=4).iloc[-1,:] # 获取同比净增长

chart = mpl_bubble(x=df_abs, # x轴数据
y=df_diff, # y轴数据
z=df_share * 50000, # 气泡大小数据
labels=df.columns.str.split('|').str[0], # 标签数据
title='', # 图表标题
x_title=label, # x轴标题
y_title=label + '净增长', # y轴标题
x_fmt='{:,.0f}', # x轴格式
y_fmt='{:,.0f}', # y轴格式
y_avg_line=True, # 添加y轴分隔线
y_avg_value=0, # y轴分隔线为y=0
label_limit=30 # 只显示前30个项目的标签
)
return chart

再度扩展query方法,再增加一个context:

def query(request):
...

# Matplotlib静态图表
bubble_performance = prepare_chart(pivoted, 'bubble_performance', form_dict)
context = {
...
'bubble_performance': bubble_performance
}

return HttpResponse(json.dumps(context, ensure_ascii=False), content_type="application/json charset=utf-8") # 返回结果必须是json格式

再看看此时的query结果,下方多了大段字符串为静态图片的base64编码:

之后的思路都和上一章很像,只是实现方法不一样。在display.html模板中,添加一个展示气泡图的tab,重点是留一个空的标签:

<div class="ui pointing secondary menu">
...
<a class="item" data-tab="bubble_performance"><i class="braille icon"></i>规模 vs. 增长</a>
</div>
...
<div class="ui tab segment" data-tab="bubble_performance">
<h3 class="ui header">
<div class="content">
规模 versus 增长
<div class="sub header">带线性拟合的气泡图</div>
</div>
</h3>
<div class="ui divider"></div>
<div class="ui container">
<img id="bubble_performance" style="width: 100%" alt="" />
</div>
</div>

注意上方标签中的style=”width: 100%”,没有这句图片将展示原始尺寸。

最后再次追加filter.html中AJAX部分的回调函数,用jQuery的.attr语法修改之前预留中的src参数:

<script type="text/javascript">
$("#AJAX_get").click(function (event) {
...

$.ajax({
...
success: function (ret) { //成功执行
...
// 展示Matplotlib气泡图
$("#bubble_performance").attr('src', ret['bubble_performance']);
},
...
});
})
</script>

大功告成,我们现在可以动态生成任意一个领域的复杂统计图表了。看看我们一直用来测试的高血压ARB市场的产品: