JSON(JavaScript Object Natation)格式最初是为JavaScript开发的,但随后成了一种常见格式,被包括Python在内的众多语言采用。
Python模块json能够将简单的Python数据结构转存到文件中,并在程序再次运行时加载该文件中的数据。
我们来编写一个使用json.dump()和json.load()的简短程序。
import json
numbers1 = [2, 3, 5, 7, 11, 13]
filename = 'numbers.json'
with open(filename, 'w') as f_obj1:
json.dump(numbers1, f_obj1)
with open(filename) as f_obj2:
numbers2 = json.load(f_obj2)
print(numbers2)
最后打印的结果是:
[2, 3, 5, 7, 11, 13]
这表明列表numbers1跟列表numbers2的内容一样。通过JSON格式的文件,这是一种在程序之间共享数据的简单方式。
我们将下载JSON格式的交易收盘价数据,并使用模块json来处理它们。Pygal提供了一个适合初学者使用的绘图工具,我们将使用它来对收盘价数据进行可视化,以探索价格变化的周期性。
收盘价数据文件位于http://raw.githubusercontent.com/muxuezi/btc/master/btc_close_2017.json 。可以直接将文件btc_close_2017.json下载在本地的文件夹中,也可以用Python中模块urllib的函数urlopen()来做,还可以通过Python的第三方模块requests下载数据。
首先,我们直接下载btc_close_2017.json,看看如何着手处理这个文件中的数据:
[{
“date”: “2017-01-01”,
“month”: “01”,
“week”: “52”,
“weekday”: “Sunday”,
“close”: “6928.6492”
},
{
“date”: “2017-01-02”,
“month”: “01”,
“week”: “1”,
“weekday”: “Monday”,
“close”: “7070.2554”
},
…
{
“date”: “2017-12-12”,
“month”: “12”,
“week”: “50”,
“weekday”: “Tuesday”,
“close”: “113732.6745”
}]
这个文件实际上就是一个很长的Python列表,其中每个元素都是一个包含五个键的字典:统计日期、月份、周数、周几以及收盘价。由于2017年1月1日是周日,作为2017年的第一周实在太短,因此将其计入2016年的第52周。于是2017年的第一周是从2017年1月2日(周一)开始的。如果用函数urlopen()来下载数据,可以使用下面的代码:
from __future__ import (absolute_import, division, print_function, unicode_literals)
try:
# Python 2.x
from urllib2 import urlopen
except ImportError:
# Pythion 3.x
from urllib.request import urlopen
import json
json_url='http://raw.githubusercontent.com/muxuezi/btc/master/btc_close_2017.json'
response = urlopen(json_url)
# read data
req = response.read()
# write date into file
with open('btc_close_2017_urllib.json','wb') as f:
f.write(req)
# load json
file_urllib = json.loads(req)
print(file_urllib)
函数urlopen()的代码稍微复杂一些,第三方模块requests封装了许多常用的方法,让数据下载和读取方式变得非常简单:
import requests
json_url='http://raw.githubusercontent.com/muxuezi/btc/master/btc_close_2017.json'
req = requests.get(json_url)
# write date into file
with open('btc_close_2017_urllib.json','w') as f:
f.write(req.text)
file_requests = req.json()
print(file_requests)
requests通过get方法向GitHub服务器发送请求。GitHub服务器响应请求后,返回的结果存储在req变量中。req.text属性可以直接读取文件数据,返回格式是字符串,可以像之前一样保存为文件btc_2017_request.json,其内容与btc_close_2017_urllib.json是一样的。另外,直接用req.json就可以将btc_close_2017.json文件的数据转换成Python列表file_requests,与之前的file_urllib内容相同。
下面编写一个小程序来提取btc_close_2017.json文件中的相关信息:
import json
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
for btc_dict in btc_data:
date = btc_dict['date']
month = btc_dict['month']
week = btc_dict['week']
weekday = btc_dict['weekday']
close = btc_dict['close']
print('{} is month {} week {}, {}, the close price is {} RMB'.format(date, month, week, weekday, close))
首先倒入模块json,然后将数据存储在btc_data中,然后我们遍历btc_data中的每个元素。每个元素都是一个字典,包含五个键-值对, btc_dict就用来存储字典中的每个键-值对。之后就可以取出所有键的值,并将日期、月份、周数、周几和收盘价相关联的值分别存储到date、month、week、weekday与close中。接下来,打印每一天的日期、月份、周数、周几和收盘价。 输出结果如下:
2017-02-17 is month 02 week 7, Friday, the close price is 7229.5808 RMB
2017-02-18 is month 02 week 7, Saturday, the close price is 7267.5468 RMB
…
2017-12-11 is month 12 week 50, Monday, the close price is 110642.88 RMB
2017-12-12 is month 12 week 50, Tuesday, the close price is 113732.6745 RMB
现在,我们已经掌握了json读取数据的方法。下面,让我们将数据转换为Pygal能够处理的格式。
btc_close_2017.json中的每个键和值都是字符串。为了能在后面的内容中对交易数据进行计算,需要先将表示月份、周数和收盘价的数字字符串转换为数值。因此,我们使用函数int()。
import json
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
for btc_dict in btc_data:
date = btc_dict['date']
month = int(btc_dict['month'])
week = int(btc_dict['week'])
weekday = btc_dict['weekday']
close = int(float(btc_dict['close']))
print('{} is month {} week {}, {}, the close price is {} RMB'.format(date, month, week, weekday, close))
收盘价因为是浮点数,所以先使用函数float()转为浮点数,然后再用int()去掉小数部分,返回整数部分。打印出的收盘价信息如下:
2017-02-17 is month 2 week 7, Friday, the close price is 7229 RMB
2017-02-18 is month 2 week 7, Saturday, the close price is 7267 RMB
2017-02-19 is month 2 week 7, Sunday, the close price is 7220 RMB
…
有了这些数据之后,可以结合Pygal的可视化功能来探索一些有趣的信息。
我们已经使用Pygal绘制条形图(bar chart)的方法,也使用了matplotlib绘制折线图(line chart)的方法,下面我们用Pygal来实现收盘价的折线图。
绘制折线图之前,需要获取x轴与y轴数据,因此我们创建了几个列表来存储数据。遍历btc_data,将转换为适当格式的数据存储到对应的列表中。对前面的代码做一些简单的调整。
有了x轴与y轴的数据,就可以绘制折线图了。由于数据点比较多,x轴要显示346个日期,在有限的屏幕上会显得十分拥挤。因此,我们需要利用Pygal的配置参数,对图形进行适当的调整。
完整的代码如下:
import json
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
dates, months, weeks, weekdays, closes = [], [], [], [], []
for btc_dict in btc_data:
dates.append(btc_dict['date'])
months.append(int(btc_dict['month']))
weeks.append(int(btc_dict['week']))
weekdays.append(btc_dict['weekday'])
closes.append(int(float(btc_dict['close'])))
import pygal
line_chart = pygal.Line(x_label_rotation=20, show_minor_x_labels=False)
line_chart.title = '收盘价(¥)'
line_chart.x_labels = dates
N = 20
line_chart.x_labels_major = dates[::N]
line_chart.add('收盘价',closes)
line_chart.render_to_file('收盘价折线图(¥).svg')
首先导入模块pygal,然后在创建Line实例时,分别设置了x_label_rotation与show_minor_x_labels作为初始化参数。x_label_rotation=20让x轴上的日期标签顺时针旋转20度,show_minor_x_labels=False则告诉图形不用显示所有的x轴标签。设置了图形的标题和x轴标签之后,我们配置x_labels_major 属性,让x轴坐标每隔20天显示一次,这样 x轴就不会显得非常拥挤了。最终的效果如下图所示:
从图中可以看出,价格从2017年11月12日到2017年12月12日快速增长,平均每天增值约2500元人民币,图中折线简直就像火箭发射一般,垂直升空。下面对价格做一些简单的探索。
进行时间序列分析总是期望发现趋势(trend)、周期性(seasonality)和噪声(noise),从而能够描述事实、预测未来、做出决策。从收盘价的折线图可以看出,2017年的总体趋势是非线性的,而且增长幅度不断增大,似乎呈指数分布。但是,我们还发现,在每个季度末(3月、6月和9月)似乎有一些相似的波动。尽管这些波动被增长的趋势掩盖了,不过其中也许有周期性。为了验证周期性的假设,需要首先将非线性的趋势消除。对数(log transformation)是常用的处理方法之一。让我们用Python标准库的数学模块math来解决这个问题。math里有许多常用的数学函数,这里用以10为底的对数函数math.log10计算收盘价,日期仍然保持不变。这种方式称为半对数(semi-logarithmic)变换。代码如下:
import json
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
dates, months, weeks, weekdays, closes = [], [], [], [], []
for btc_dict in btc_data:
dates.append(btc_dict['date'])
months.append(int(btc_dict['month']))
weeks.append(int(btc_dict['week']))
weekdays.append(btc_dict['weekday'])
closes.append(int(float(btc_dict['close'])))
import pygal
import math
line_chart = pygal.Line(x_label_rotation=20, show_minor_x_labels=False)
line_chart.title = '收盘价对数变换(¥)'
line_chart.x_labels = dates
N = 20
line_chart.x_labels_major = dates[::N]
closes_log = [math.log10(cp) for cp in closes ]
line_chart.add('log10收盘价',closes_log)
line_chart.render_to_file('收盘价对数变换折线图(¥).svg')
现在,用对数变换剔除非线性趋势之后,整体上涨的趋势更接近线性增长。
从上图可以清晰地看出,收盘价在每个季度末似乎有显著的周期性–3月、6月和9月都出现了剧烈的波动。那么,12月是不是会再现这一场景呢?下面再看看收盘价的月日均值与周日均值得表现。
下面再利用btc_close_2017.json文件中的数据,绘制2017年前11个月的日均值、前49周(2017-01-02 ~2017-12-10)的日均值,以及每周中各天(Monday ~ Sunday)的日均值。虽然这些日均值的数值不同,但都是一段时间的均值,计算方法是一样的。因此,可以将之前的绘图代码封装成函数,以便重复使用。
import json
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
dates, months, weeks, weekdays, closes = [], [], [], [], []
for btc_dict in btc_data:
dates.append(btc_dict['date'])
months.append(int(btc_dict['month']))
weeks.append(int(btc_dict['week']))
weekdays.append(btc_dict['weekday'])
closes.append(int(float(btc_dict['close'])))
import pygal
import math
from itertools import groupby
def draw_line(x_data, y_data, title, y_legend):
xy_map = []
for x, y in groupby(sorted(zip(x_data, y_data)), key=lambda _ : _[0]):
y_list = [v for _, v in y]
xy_map.append([x, sum(y_list) / len(y_list)])
x_unique, y_mean = [*zip(*xy_map)]
line_chart = pygal.Line()
line_chart.title = title
line_chart.x_labels = x_unique
line_chart.add(y_legend, y_mean)
line_chart.render_to_file(title+'.svg')
return line_chart
idx_month = dates.index('2017-12-01')
line_chart_month = draw_line(months[:idx_month],closes[:idx_month], '收盘价月日均值(¥)', '月日均值')
line_chart_month
由于需要将数据按月份、周数、周几分组,再计算每组的均值,因此我们导入Python标准库中模块itertools的函数groupby。然后将x轴和y轴的数据合并、排序,再利用函数groupby分组。分组之后,求出每组的均值,存储到xy_map变量中。最后,将xy_map中存储的x轴和y轴数据分离,就可以像之前那样用Pygal画图了。下面我们画出收盘价月日均值。由于2017年12月的数据并不完整,我们只取2017年1月到11月的数据。通过dates查找2017-12-01的索引位置,确定周数和收盘价的取数范围。部分代码如下:
...
idx_month = dates.index('2017-12-01')
line_chart_month = draw_line(months[:idx_month],closes[:idx_month], '收盘价月日均值(¥)', '月日均值')
line_chart_month
...
从上图中可以看出,除了7月相比上个月有所下降,其它各月都是增长的。11月相比10月的增幅非常惊人,月日均增长了45%。
下面再来绘制前49周(2017-01-02 ~ 2017-12-10)的日均值。2017年1月1日是周日,归属为2016年第52周,因此2017年的第一周从2017年1月2日开始,取数时需要将第一天去掉。另外,2017年第49周周日是2017年12月10日,因此我们通过dates查找2017-12-11的索引位置,确定周数和收盘价的取数范围。部分代码如下:
...
idx_week = dates.index('2017-12-11')
line_chart_week = draw_line(weeks[1:idx_week],closes[1:idx_week], '收盘价周日均值(¥)', '周日均值')
line_chart_week
...
从上图中可以看出,价格与节假日无关。在2017年的各个节假日都没有出现价格低点,包括春节(第4周)、清明节(第14周)、劳动节(第18周)、端午节(第22周)、国庆节(第40周)。
最后,绘制每周中各天的均值。为了使用完整的时间段,还像前面那样取前49周(2017-01-02 ~ 2017-12-10)的数据,同样通过dates查找2017-12-11的索引位置,确定周数和收盘价的取值范围。但是,由于这里的周几是字符串,按周一到周日的顺序排列,而不是单词首字母的顺序,绘图时x轴标签的顺序会有问题。另外,原来的周几都是英文单词,还可以将其调整为中文。因此,需要对前面的程序做一些特殊处理,代码如下所示:
import json
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
dates, months, weeks, weekdays, closes = [], [], [], [], []
for btc_dict in btc_data:
dates.append(btc_dict['date'])
months.append(int(btc_dict['month']))
weeks.append(int(btc_dict['week']))
weekdays.append(btc_dict['weekday'])
closes.append(int(float(btc_dict['close'])))
import pygal
import math
from itertools import groupby
def draw_line(x_data, y_data, title, y_legend):
xy_map = []
for x, y in groupby(sorted(zip(x_data, y_data)), key=lambda _ : _[0]):
y_list = [v for _, v in y]
xy_map.append([x, sum(y_list) / len(y_list)])
x_unique, y_mean = [*zip(*xy_map)]
line_chart = pygal.Line()
line_chart.title = title
line_chart.x_labels = x_unique
line_chart.add(y_legend, y_mean)
line_chart.render_to_file(title+'.svg')
return line_chart
def draw_line_weekday(x_data, y_data, title, y_legend):
xy_map = []
for x, y in groupby(sorted(zip(x_data, y_data)), key=lambda _ : _[0]):
y_list = [v for _, v in y]
xy_map.append([x, sum(y_list) / len(y_list)])
x_unique, y_mean = [*zip(*xy_map)]
line_chart = pygal.Line()
line_chart.title = title
#line_chart.x_labels = x_unique
line_chart.x_labels = ['周一','周二','周三','周四','周五','周六','周日']
line_chart.add(y_legend, y_mean)
line_chart.render_to_file(title+'.svg')
return line_chart
idx_week = dates.index('2017-12-11')
wd = ['Monday', 'Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
weekdays_int = [wd.index(w) + 1 for w in weekdays[1:idx_week]]
line_chart_weekday = draw_line_weekday(weekdays_int, closes[1:idx_week], '收盘价周均值(¥)', '周均值')
line_chart_weekday
首先,我们列出一周七天的英文单词,然后将weekdays的内容替换成1~7的在整数。这样,函数draw_line_weekday()在处理数据时按周几的顺序排列,将会将周一放在列表的第一位,周日放在列表的第七位。图形生成之后,再将图形的x轴标签替换为中文,最终输出的图形如下:
从上图可以看出,比特币收盘价在周一最低,周日最高。周一到周四快速拉升,周四是拐点,周五和周四基本持平(其实略低于周四),之后增速放慢。
前面已经为交易收盘价绘制了五幅图,分别是收盘价对数变换、收盘价月日均值、收盘价周日均值、收盘价星期均值。每个SVG文件打开之后都是独立的页面。如果能够将他们整合在一起,就可以很方便地进行长期管理、监测和分析。另外,新的图表也可以十分方便地加入进来,这样就形成了一个数据仪表盘(dashboard)。下面将前面绘制的图整合起来,做一个收盘价数据仪表盘。代码如下:
def generate_dashboard():
with open('收盘价dashboard.html', 'w',encoding='utf8') as html_file:
html_file.write(' 收盘价Dashboard \n')
for svg in [ '收盘价折线图(¥).svg', '收盘价对数变换折线图(¥).svg','收盘价月日均值(¥).svg',
'收盘价周日均值(¥).svg','收盘价周均值(¥).svg' ]:
html_file.write(' \n'.format(svg))
html_file.write('')
和常见网络应用的数据仪表盘一样,我们的数据仪表盘也是一个完整的网页(HTML文件)。首先,需要创建一个名为’收盘价dashboard.html’的网页文件,然后将每幅图都添加到页面中。这里设置SVG图形的默认高度为500像素,由于SVG是矢量图,可以任意缩放且不失真,因此可以通过放大或缩小网页来调整视觉效果。最终的效果如图所示:
关于交易收盘价的分析就介绍到这里,作为Python处理JSON文件格式的示例使用。
完整的代码如下:
import json
import pygal
import math
from itertools import groupby
dates, months, weeks, weekdays, closes = [], [], [], [], []
def generate_data_lists():
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
for btc_dict in btc_data:
dates.append(btc_dict['date'])
months.append(int(btc_dict['month']))
weeks.append(int(btc_dict['week']))
weekdays.append(btc_dict['weekday'])
closes.append(int(float(btc_dict['close'])))
def draw_line_N20(x_data, y_data, title, y_legend):
line_chart = pygal.Line(x_label_rotation=20, show_minor_x_labels=False)
line_chart.title = title
line_chart.x_labels = y_data
N = 20
line_chart.x_labels_major = x_data[::N]
line_chart.add(y_legend, closes)
line_chart.render_to_file(title+'.svg')
return line_chart
def draw_line_N20_log10(x_data, y_data, title, y_legend):
line_chart = pygal.Line(x_label_rotation=20, show_minor_x_labels=False)
line_chart.title = title
line_chart.x_labels = y_data
N = 20
line_chart.x_labels_major = x_data[::N]
closes_log = [math.log10(cp) for cp in closes ]
line_chart.add(y_legend, closes_log)
line_chart.render_to_file(title+'.svg')
return line_chart
def draw_line(x_data, y_data, title, y_legend):
xy_map = []
for x, y in groupby(sorted(zip(x_data, y_data)), key=lambda _ : _[0]):
y_list = [v for _, v in y]
xy_map.append([x, sum(y_list) / len(y_list)])
x_unique, y_mean = [*zip(*xy_map)]
line_chart = pygal.Line()
line_chart.title = title
line_chart.x_labels = x_unique
line_chart.add(y_legend, y_mean)
line_chart.render_to_file(title+'.svg')
return line_chart
def draw_line_weekday(x_data, y_data, title, y_legend):
xy_map = []
for x, y in groupby(sorted(zip(x_data, y_data)), key=lambda _ : _[0]):
y_list = [v for _, v in y]
xy_map.append([x, sum(y_list) / len(y_list)])
x_unique, y_mean = [*zip(*xy_map)]
line_chart = pygal.Line()
line_chart.title = title
line_chart.x_labels = ['周一','周二','周三','周四','周五','周六','周日']
line_chart.add(y_legend, y_mean)
line_chart.render_to_file(title+'.svg')
return line_chart
def generate_SVG_files():
line_chart_N20 = draw_line_N20(dates,closes, '收盘价折线图(¥)', '收盘价')
line_chart_N20
line_chart_N20_log10 = draw_line_N20_log10(dates,closes,
'收盘价对数变换折线图(¥)', 'log10收盘价')
line_chart_N20_log10
idx_month = dates.index('2017-12-11')
line_chart_month = draw_line(months[:idx_month],closes[:idx_month],
'收盘价月日均值(¥)', '月日均值')
line_chart_month
idx_week = dates.index('2017-12-11')
line_chart_week = draw_line(weeks[1:idx_week],closes[1:idx_week],
'收盘价周日均值(¥)', '周日均值')
line_chart_week
idx_week = dates.index('2017-12-11')
wd = ['Monday', 'Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
weekdays_int = [wd.index(w) + 1 for w in weekdays[1:idx_week]]
line_chart_weekday = draw_line_weekday(weekdays_int, closes[1:idx_week],
'收盘价周均值(¥)', '周均值')
line_chart_weekday
def generate_dashboard():
with open('收盘价dashboard.html', 'w',encoding='utf8') as html_file:
html_file.write(' 收盘价Dashboard \n')
for svg in [ '收盘价折线图(¥).svg', '收盘价对数变换折线图(¥).svg','收盘价月日均值(¥).svg',
'收盘价周日均值(¥).svg','收盘价周均值(¥).svg' ]:
html_file.write(' \n'.format(svg))
html_file.write('')
def main():
generate_data_lists()
generate_SVG_files()
generate_dashboard()
if __name__ == '__main__':
main()
文章原文来自《Python编程 从入门到实践》 [美] Eric Matthes 袁国忠 译