支付宝蚂蚁森林模块最早从2016年推出,题主最开始从支付宝集福活动开始接触。期间懒懒散散收过一些能量,但是相比朋友圈动辄几十几百公斤的能量值,我的能量值只有20Kg,想种棵胡杨连零头都不够。所以,本着节流开源的想法,题目决定利用Python分析一下,平时自己的能量都去哪了。
1、获取蚂蚁森林的收取记录
由于蚂蚁森林的收取记录只在手机App上显示,所以想要获得还得想些办法。经过一番查找,决定使用AutoJs,这是一个使用JS来进行自动化测试的软件。截止现在,免费版的Android版本在酷安软件市场已经下载不到了,题主的是之前安装的版本4.0.1。
但是目前支付宝版本升级之后,现有代码基本失效,有能力的童鞋可以自行修改,运行之后会在目录下生成recordEnergy.txt
。我使用的是19年4月份抓取的数据记录,共抓取了49天的数据。
JS代码分析
收取记录的页面是一直可以往下翻的,加载时间约为0.5秒。所以写一个循环,一直翻页就好了。
auto()
toast("开始测试")
var isDown = true
for(var i=0; i<85; i++){
if(isDown){
sleep(random(3,6)*1000)
getInfo()
isDown = scrollDown()
log("动一下--》"+i)
}
else{
log("滑动失败")
sleep(100)
scrollUp()
sleep(100)
isDown = scrollDown()
sleep(random(3,6)*1000)
log("尝试滑动一下-----》"+i)
}
}
将获取到的记录信息写入文件:日期及时间、姓名、操作种类及能量数目。
function intoFile(information){
files.append("recordEnergy.txt", information+"\n")
}
主要获取信息的函数,遍历所有子控件,如果控件可见,就获取信息,也是为了避免重复抓取。
function getInfo(){
// 获取可见控件的信息
var first = className("android.webkit.WebView").findOnce()
var second = first.child(0).child(0).children()
second.forEach(
function(childPara){
if(childPara.visibleToUser() == true){
if(childPara.childCount() == 0){
var dayStr = childPara.contentDescription
log(dayStr)
}
else{
var timeStr = childPara.child(3).contentDescription
var nameStr = childPara.child(2).child(0).contentDescription
var numStr = childPara.child(2).child(1).contentDescription
var info = timeStr+"\t"+nameStr+"\t"+numStr+"\t"+dayStr
intoFile(info)
}
}
}
)
}
2、对原始数据进行清洗
对抓取到的.txt
文件进行简单处理,这里题主使用的Excel。文件的信息有4类,其中对于姓名一栏,抓取的是人物昵称,需要一一核对替换;操作使用的是Excel的分列操作,当然直接使用正则表达式进行提取也是可以的;数量一栏,是以克为单位,没什么好说的;日期一栏是直接抓取的格式,后面会进行处理。另存为蚂蚁森林收取记录.csv
文件即可。
姓名 | 操作 | 数量 | 日期 |
---|---|---|---|
果子 | 帮忙收取/浇水/收取 | 12 | 2019/4/19 19:06 |
3、Python绘图展示
这里是本次的重点,使用的Python
包为Numpy、Pandas、Matplotlib、Seaborn
,请自行安装。下面开始介绍代码主体。
首先导入此次的包文件及一些设置
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 显示中文和负号,务必确保安装了黑体的字体
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False
# 一些matplotlib画图的设置
large = 16; med = 12; small = 10
params = {'axes.titlesize': large,
'legend.fontsize': med,
'figure.figsize': (9, 6),
'axes.labelsize': large,
'axes.titlesize': med,
'xtick.labelsize': med,
'ytick.labelsize': med,
'figure.titlesize': large}
plt.rcParams.update(params)
# 设置使用漫画绘图模式,禁用可以返回普通模式
plt.xkcd()
a、数据格式处理
代码如下,都是一些pandas的基础操作,没什么好讲的。
origindData = pd.read_csv("蚂蚁森林收取记录.csv", encoding="gbk")
# 提取别人收取能量的记录
recordData = origindData[origindData["操作"]=="收取"]
# 重置索引
recordData.reset_index(drop=True, inplace=True)
# 转换日期格式
recordData['time'] = pd.to_datetime(recordData['时间'],infer_datetime_format=True)
对于人名,题主决定采用名称的缩写,保护一下别人的隐私。
# xpinyin这个python包
from xpinyin import Pinyin
p = Pinyin()
# 转换拼音
recordData["name"] = recordData["姓名"].apply(lambda x : p.get_initials(x, ""))
b、绘制收取能量的排行图
首先获取排行排名的数据
# 数据聚合
sumSeries = recordData.groupby("name")["数量"].sum()
# 将聚合的结果转换为dataframe
sumData = pd.DataFrame({"name": sumSeries.index, "count":sumSeries.values})
# 排序
sumData.sort_values("count", ascending=False, inplace=True)
# 重置索引
sumData.reset_index(drop=True, inplace=True)
然后对sumData
绘图
fig, ax = plt.subplots()
# 共画15个
for i in range(15):
# 制定颜色
color = plt.cm.tab10_r(sumData.loc[i, "count"]/550)
# 画排行的竖直线
ax.vlines(sumData.loc[i, "name"], 0, sumData.loc[i, "count"], color=color, linewidth=2)
# 画竖直线上面的球
ax.scatter(sumData.loc[i, "name"], sumData.loc[i, "count"], color=color, s=50)
# 标注文字
ax.text(sumData.loc[i, "name"], sumData.loc[i, "count"]+15, color=color, s=str(sumData.loc[i, "count"]), fontsize=11, horizontalalignment='center', verticalalignment='bottom')
# 设置x坐标范围
ax.set_ylim(0,600)
# 设置标签
ax.set_xlabel("Short Name")
ax.set_ylabel("Number of Energy/g")
# ax.set_title("蚂蚁森林能量收取排行榜前15", size=20, loc="center")
# 设置坐表轴隐藏
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
# 保存图像
plt.savefig("XKCD-能量收取排行前15.png", dpi=300)
结果
c、绘制每天各时段能量被收取的序列图
画主体图形
首先准备数据
# 将时间格式里面的小时和分钟,计算成分钟数
recordData["timeSeries"] = recordData["time"].apply(lambda x: x.hour*60+x.minute)
# 创建每半个小时的时间分箱数据
# 创建分箱的标签范围
def getTimeLabels(x=0,y=24,step=1):
timeLabels = []
for i in range(x,y,step):
for j in [":00", ":30"]:
timeLabels.append(str(i)+j)
return timeLabels
# 分箱的数值范围
timeBins = np.linspace(0, 1440, 49)
# 分箱的标签范围
timeLabels = getTimeLabels()
# 分箱操作
recordData["timeBins"] = pd.cut(recordData["timeSeries"], bins=timeBins, labels=timeLabels)
# 进行聚合操作
tempData = recordData.groupby("timeBins")["数量"].sum()
# 将结果转换为dataframe
recordBinsData = pd.DataFrame({"time":tempData.index, "number":tempData.values})
绘制图形
fig, ax = plt.subplots()
# 绘制所有时段的
for i in (recordBinsData.index):
# 产生颜色
color = plt.cm.tab10_r(recordBinsData.loc[i, "number"]/310)
# 画竖直线
ax.vlines(i, 0, recordBinsData.loc[i, "number"], color=color, linewidth=3)
# 画柱子上的球
ax.scatter(i, recordBinsData.loc[i, "number"], color=color, s=3)
# 设置坐标轴
ax.set_xlim(0, len(recordBinsData))
ax.set_ylim(0,1400)
# 设置Y轴刻度间隔
ax.yaxis.set_minor_locator(plt.MultipleLocator(100))
# 设置X轴间隔为1
ax.xaxis.set_minor_locator(plt.MultipleLocator(1))
# 设置X轴标签
plt.xticks( np.arange(0, 48, 5), recordBinsData[::5]["time"] )
# 设置坐表轴隐藏
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
# 设置网格线
ax.grid(alpha=0.1)
# 设置标签
ax.set_xlabel("Time")
ax.set_ylabel("Number of Energy/g")
添加子图操作
# 创建1分钟的分箱结果
# 创建分箱的标签范围
def getTimeLabels(x=7,step=1):
timeLabels = []
for j in range(0,60,step):
timeLabels.append(str(x)+":"+str(j))
return timeLabels
# 分箱的数值范围
timeSmallBins = np.arange(420, 481)
# 分箱的标签范围
timeSambllLabels = getTimeLabels()
# 分箱操作
recordData["timeSmallBins"] = pd.cut(recordData["timeSeries"], bins=timeSmallBins, labels=timeSambllLabels)
# 进行聚合操作
tempData = recordData.groupby("timeSmallBins")["数量"].sum()
recordSmallBinsData = pd.DataFrame({"time":tempData.index, "number":tempData.values})
绘制图形
# 添加子图
ax1 = fig.add_axes([0.5, 0.4, 0.4, 0.4])
# 添加图形
ax1.plot(recordSmallBinsData.index, recordSmallBinsData["number"], color="darkred", linewidth=1)
# 充填直线的颜色
plt.fill_between(np.arange(0,31), 0, 650, color="red", alpha=0.05)
plt.fill_between(np.arange(30,61), 0, 650, color="blue", alpha=0.05)
# 设置坐标轴
ax1.set_xlim(0,60)
ax1.set_ylim(0,250)
# 设置X轴间隔为1
ax1.xaxis.set_minor_locator(plt.MultipleLocator(2))
# 设置X轴标签
plt.xticks( np.arange(0, 61, 10), recordSmallBinsData[::10]["time"] )
# 设置标签
ax1.set_xlabel("Time", fontsize=med-1)
ax1.set_ylabel("Number of Energy/g", fontsize=med-1)
# 设置坐表轴隐藏
ax1.spines['right'].set_color('none')
ax1.spines['top'].set_color('none')
# 设置网格线
ax1.grid(alpha=0.1)
# ax.set_title("蚂蚁森林森林能量每天时段收取统计", size=20, loc="center")
# 保存图像
plt.savefig("XKCD-蚂蚁森林森林能量每天平均收取统计.png", dpi=300)
结果
d、绘制这49天来的每天被收取能量的记录
首先获取数据
# 处理得到日期
recordData["date"] = recordData["time"].apply(lambda x : x.strftime("%Y-%m-%d"))
# 聚合操作
tempData = recordData.groupby("date")["数量"].sum()
# 将结果整理成dataframe操作
dateData = pd.DataFrame({"date":tempData.index, "number":tempData.values})
绘制折线图
fig, ax = plt.subplots()
# 画折线图
ax.plot(dateData.index, dateData["number"], color="darkred" , alpha=0.7, linewidth=1.5)
# 填充颜色
plt.fill_between(np.arange(0,27), 0, 350, color="green", alpha=0.04)
# 设置X轴坐标
ax.set_xlim(0, len(dateData))
ax.set_ylim(0, 350)
# 设置X轴间隔为1
ax.xaxis.set_minor_locator(plt.MultipleLocator(2))
# 设置X轴刻度
plt.xticks(np.arange(len(dateData)+1)[::12], [dateData.loc[x, "date"] for x in range(0, len(dateData), 12)])
# 设置标签
ax.set_xlabel("Date")
ax.set_ylabel("Number of Energy/g")
# 设置坐表轴隐藏
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
# 设置网格
ax.grid(alpha=0.1)
# ax.set_title("蚂蚁森林森林能量每天收取统计", size=20, loc="center")
# 保存图像
plt.savefig("XKCD-蚂蚁森林森林能量每天收取统计.png", dpi=300)
结果
4、结果简单分析
a、从第一张排行图可以看出,能量收取最多的克数成阶梯状分布,以500、400、300、200左右具有台阶。这个分布并不是预想的顺滑下降;
b、从第2张每天的时段序列图可以看出,每天的7-8点、1-2点以及晚上的7-8点半是收取能量的高峰,基本都是吃完以后的休息时间啊。能量收取最多的时间段为早上的7点-8点,高峰开始的时间为7:28左右,别人都是早起收能量的啊;
c、最后一张日期分布图,左边阴影部分是假期时间,左边的峰值与低谷与右边的具有明显区别,看来假期和在校的日常活动不再同一个水平啊。
所以最后要想收集能量多,还是早点起床收能量吧,哈哈。
最后,转载请注明出处!!!