用数据分析的手段,看2019年CSDN博客之星总评选


文章目录

  • 1. 前言
  • 2. 数据抓取——分享一个调度服务框架
  • 3. 数据处理——基于Numpy的预处理
    • 3.1 读取指定编号博主的投票数据
    • 3.2 取得指定时刻得票数量前n名的博主序号
  • 4. 数据分析——基于matplotlib绘图
    • 4.1 绘制指定编号博主的投票曲线
    • 4.2 绘制指定编号博主的10分钟投票增量曲线
    • 4.3 将多位博主得票数据绘制在一张图上
    • 4.4 绘制日增量柱状图,打印markdown格式的位次变化表
  • 5. 趋势预测——最小二乘法拟合多项式

1. 前言 万众瞩目的CSDN博客之星总评选投票活动渐入佳境,竞争趋于白热化。入选前200名的博主们火力全开,使出了浑身解数,通过各种渠道拉票。一时间,CSDN刷爆了各大自媒体。无论是吃瓜群众,还是摇旗呐喊、擂鼓助威的亲友团,无不赞叹:CSDN这一波广告创意,真高!
俗话说,外行看热闹,内行看门道。姑且让亲友团、粉丝团飞一会儿,我来给吃瓜群众介绍一下,如何用数据分析的手段,剖析今年的CSDN博客之星总评选,并用机器学习的方式预测后续的竞争结果。本文纯粹出于技术交流之目的,绝无恶意揣测诽谤他人之意图;所谓预测,亦出于游戏之心态,只为博同学们一笑耳。
从17日开始,关心CSDN博客之星总评选的朋友们,可以点击“2019年CSDN博客之星总评选投票走势”查看实时的数据统计结果了。该站点数据来源于本次活动的官方网站,未经任何修改,亦无任何观点倾向,仅供参考。
2. 数据抓取——分享一个调度服务框架 做数据分析,首先得有数据。看2019年CSDN博客之星总评选,就要从活动网站上抓取数据。抓数据不难,同学们都会,但要抓历史数据,就需要费功夫了。因为活动网站上没有提供历史数据下载服务,我们只能不断地以固定时间间隔访问网站,并将数据记录下来。这就需要一个长期工作的服务程序。通常,执行定时任务的服务程序,都是基于调度服务的框架。
APScheduler 是我最喜欢的一个用于调度服务的模块,其全称是 Advanced Python Scheduler。这是一个轻量级的 Python 定时任务调度框架,功能非常强大。单说这个模块的话,洋洋洒洒可以写万字以上,但本文的重点不是 APScheduler,所以这里直接分享一个APScheduler 的应用实例。虽然只有区区70余行代码,依然非常实用、健壮。APScheduler 的安装很简单:
python -m pip install apscheduler
下面的程序启动之后,每10分钟从2019年CSDN博客之星总评选活动网站抓取一次数据,以博主编号为文件名,保存在和服务程序同级的 data 文件夹下,文件格式如下:
天元浪子
2020-01-15 11:10:00,1,10512
2020-01-15 11:20:00,1,10517
2020-01-15 11:30:00,1,10527
… …
【用数据分析的手段,看2019年CSDN博客之星总评选】scheduler_catch.py
# coding:utf-8"""定时数据抓取服务"""import os, re, json, time from datetime import datetime import urllib.request from apscheduler.schedulers.blocking import BlockingSchedulerdef start_service(): """启动数据抓取调度服务"""scheduler = BlockingScheduler() scheduler.add_job(TimeJobHandler, args = (), trigger = "cron", second = "0", minute = "0/10", hour = "*", day = "*", month = "*", day_of_week = "*", year = "*", misfire_grace_time = 60 )scheduler.start()def TimeJobHandler(): url = "http://m234140.nofollow.ax.mvote.cn/action/viewvotewxorderlist.html?voteguid=43ced329-3a4b-0a5d-a13c-f088cf8eafef" res = urllib.request.urlopen(url) now = datetime.now() html = res.read().decode("utf-8") itemMatch(html, now)def itemMatch(content, now): cwd = os.getcwd() path = os.path.join(cwd, "data") if not os.path.isdir(path): os.makedirs(path)content = content.replace("\r\n", "").replace("\n", "").replace("\r", "").replace("\"", "'") p = re.compile("第(\d+)名") col0 = p.findall(content) # 第N名 p = re.compile("(.*?)", ) col1 = p.findall(content) # 当前名次 p = re.compile("(\d+)票") col2 = p.findall(content) # 当前票数newName = [] for i, name in enumerate(col1): if "(点此进入个人页)" in name: ns = name.split(u"(") name = ns[0] name = name.strip() ns = name.split(".") num = ns[0] truename = "".join(ns[1:]).strip() filename = os.path.join(path, num + ".txt")if os.path.isfile(filename): f = open(filename, 'a') else: f = open(filename, 'w') f.write(truename + "\n")f.write(now.strftime("%Y-%m-%d %X") + "," + col0[i] + "," + col2[i] + "\n") f.close()if __name__ == '__main__': start_service()

3. 数据处理——基于Numpy的预处理 数据处理过程中导入用到两个模块,先统一写在这里,后面讲解时就不再处处导入了:
import numpy as np from datetime import datetime, timedelta

3.1 读取指定编号博主的投票数据
def read_by_no(no, start_dt, end_dt): """读取指定编号博主的投票数据"""with open('data/%d.txt'%no, 'r', encoding='utf-8') as fp: lines = fp.readlines()name = lines[0].strip() dt_list = list() rank_list = list() votes_list = list() for line in lines[1:]: dt, rank, votes = line.strip().split(',') if (not start_dt or start_dt and dt >= start_dt) and (not end_dt or end_dt and dt <= end_dt): dt_list.append(datetime.strptime(dt, '%Y-%m-%d %H:%M:%S')) rank_list.append(int(rank)) votes_list.append(int(votes))return name, dt_list, rank_list, votes_list

3.2 取得指定时刻得票数量前n名的博主序号
def get_top(n, end_dt): """取得end_dt时刻前n名序号"""ranks = list() for i in range(1, 202): with open('data/%d.txt'%i, 'r', encoding='utf-8') as fp: lines = fp.readlines()for j in range(1, len(lines)): dt, rank, votes = lines[j].strip().split(',') if dt > end_dt: break dt, rank, votes = lines[j-1].strip().split(',') ranks.append((i, int(rank)))ranks.sort(key=lambda x:x[1]) return [item[0] for item in ranks][:n]

4. 数据分析——基于matplotlib绘图 绘图函数需要导入matplotlib模块,并设置中文字体。先统一写在这里,后面讲解时就不再处处导入了:
import matplotlib.pyplot as plt import matplotlib.dates as mdates import matplotlib.ticker as tickerplt.rcParams['font.sans-serif'] = ['FangSong']# 设置默认字体 plt.rcParams['axes.unicode_minus'] = False# 解决保存图像时'-'显示为方块的问题

4.1 绘制指定编号博主的投票曲线
def plot_votes(no, start_dt=None, end_dt=None): """绘制指定编号博主的投票曲线"""name, dt_list, rank_list, votes_list = read_by_no(no, start_dt, end_dt)plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 8)) plt.title('2019年CSDN博客之星总评选%s10分钟得票数量统计曲线'%name, fontsize=20) plt.grid(linestyle=':') # 辅助网格 plt.plot(dt_list, votes_list) # 绘制数据 plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) # 格式化时间轴标注 plt.gcf().autofmt_xdate() # 优化标注(自动倾斜) #plt.savefig('image/得票数量统计曲线.png') # 保存为文件 plt.show()plot_votes(168)

绘制效果如下:
用数据分析的手段,看2019年CSDN博客之星总评选
文章图片

4.2 绘制指定编号博主的10分钟投票增量曲线
def plot_delta(no, start_dt=None, end_dt=None): """绘制指定编号博主的10分钟投票增量曲线"""name, dt_list, rank_list, votes_list = read_by_no(no, start_dt, end_dt)plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 8)) plt.title('2019年CSDN博客之星总评选%s10分钟投票增量统计曲线'%name, fontsize=20) plt.grid(linestyle=':') # 辅助网格 votes_list.insert(0, votes_list[0]) votes_list = np.diff(np.array(votes_list)) plt.plot(dt_list, votes_list, color='g') # 绘制数据 plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) # 格式化时间轴标注 plt.gcf().autofmt_xdate() # 优化标注(自动倾斜) #plt.savefig('image/投票增量曲线.png') # 保存为文件 plt.show()sdt = '2020-01-13 00:00:00' edt = '2020-01-15 00:00:00' plot_delta(168, start_dt=sdt, end_dt=edt)

绘制效果如下:
用数据分析的手段,看2019年CSDN博客之星总评选
文章图片

4.3 将多位博主得票数据绘制在一张图上 函数 get_top(n, end_dt) 返回 end_dt 时刻得票数量排名前 n 位的博主的序号列表。遍历这个列表,我们可以把多位博主的投票曲线或者投票增量曲线,画在同一张图上。这里就不一一给出代码了,直接贴出效果图:
用数据分析的手段,看2019年CSDN博客之星总评选
文章图片

用数据分析的手段,看2019年CSDN博客之星总评选
文章图片

4.4 绘制日增量柱状图,打印markdown格式的位次变化表 接下来,我们再学习画柱状图,用以分析TOP20的博主们每天的投票增量。
def plot_votes_delta(n, dt): """绘制日增量柱状图,打印markdown格式的位次变化表"""last_dt = datetime.strptime(dt, '%Y-%m-%d %H:%M:%S') - timedelta(days=1) last_dt = last_dt.strftime('%Y-%m-%d %X') no_list = get_top(n, dt) names, dv = list(), list() table_str = '|编号|博主|位次|升降|总得票数|日增票数|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n' for i in range(n): no = no_list[i] name, dt_list, rank_list, votes_list = read_by_no(no, last_dt, dt) dvotes = votes_list[-1]-votes_list[0] drank = rank_list[0]-rank_list[-1]if drank > 0: drank_str = '↑%d'%drank elif drank < 0: drank_str = '↓%d'%abs(drank) else: drank_str = '-'table_str += '|%d|%s|%d|%s|%d|%d|\n'%(no, name, i+1, drank_str, votes_list[-1], dvotes) names.append(name) dv.append(dvotes)print(table_str)color=[(np.random.random(),np.random.random(),np.random.random()) for i in range(n)] plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 12)) plt.title('2019年CSDN博客之星总评选TOP%d(截至%s)日增投票数量柱状图'%(n, dt), fontsize=20) plt.barh(names[::-1], dv[::-1], align="center", height=0.5, alpha=1.0, color=color) plt.gcf().autofmt_xdate() plt.savefig('image/日增投票数量柱状图%s.png'%dt[:10]) plt.show()sdt = '2020-01-13 00:00:00' edt = '2020-01-15 00:00:00' plot_votes_delta(20, edt)

绘制效果如下:
用数据分析的手段,看2019年CSDN博客之星总评选
文章图片

markdown格式的位次变化表:
编号 博主 位次 升降 总得票数 日增票数
168 天元浪子 1 - 9770 1795
22 Eastmount 2 - 8366 1062
25 Programer Cat 3 - 7597 1357
23 _YourBatman 4 ↑3 7037 1806
176 小傅哥 5 ↓1 6994 1513
200 DrogoZhang 6 ↓1 6803 1436
127 Mike__Jiang 7 ↓1 6741 1503
57 lilongsy 8 ↑3 6166 1577
201 刘望舒 9 - 6158 1197
95 敖丶丙 10 - 6033 1234
21 程序猿DD 11 ↑1 5910 1883
79 沉默王二 12 ↓4 5882 683
76 唯有坚持不懈 13 ↑2 5037 1258
82 人工智能博士 14 - 4597 776
68 十步杀一人_千里不留行 15 ↓2 4349 387
20 段智华 16 - 4094 915
69 不脱发的程序猿 17 - 3748 1016
66 lynnlovemin 18 - 3528 862
103 Vam的金豆之路 19 - 3120 578
70 狂野小青年 20 ↑2 3072 741
5. 趋势预测——最小二乘法拟合多项式 关于最小二乘法实现多项式拟合,请参考我的另一篇博文:《从寻找谷神星的过程,谈最小二乘法实现多项式拟合》。下面我们以168号博主最近4天的投票数据为例,用最小二乘法拟合多项式,对天元浪子未来一天的投票结果做出趋势预测(纯属搞笑,切勿当真)。
def predict(no, start_dt, end_dt): """趋势预测"""name, dt_list, rank_list, votes_list = read_by_no(168, start_dt, end_dt) _y = votes_list = np.array(votes_list) _x = np.arange(_y.shape[0]) x = np.arange(_y.shape[0]+144) # 预测24小时后得票数量g3 = np.poly1d(np.polyfit(_x, _y, 3)) g4 = np.poly1d(np.polyfit(_x, _y, 4))loss3 = np.sum(np.square(g3(_x)-_y))/_y.shape[0] loss4 = np.sum(np.square(g4(_x)-_y))/_y.shape[0]plt.figure('2019年CSDN博客之星总评选统计图表', facecolor='#f4f4f4', figsize=(15, 8)) plt.title('2019年CSDN博客之星总评选%s24小时趋势预测'%name, fontsize=20) plt.plot(_x, _y, label='原始数据') plt.plot(x, g3(x), label='3次多项式,预测24小时后得票数量:%d'%int(g3(x)[-1])) plt.plot(x, g4(x), label='4次多项式,预测24小时后得票数量:%d'%int(g4(x)[-1])) plt.plot(_x, g3(_x), label='3次多项式,误差%0.4f'%loss3) plt.plot(_x, g4(_x), label='4次多项式,误差%0.4f'%loss4)plt.legend() plt.show()

经过比较,4次多项式拟合结果误差最小。预测结果如下:
用数据分析的手段,看2019年CSDN博客之星总评选
文章图片

既然都读到这里了,就给168号博主天元浪子投上5票吧,谢谢!
用数据分析的手段,看2019年CSDN博客之星总评选
文章图片

    推荐阅读