在互联网时代,各类的音乐网站提供了成千上万的需求,满足了人们对于音乐的需求,让我们在通勤出行或者闲暇之余可以听到各种不同类型的音乐。而通过分析挖掘海量的历史音乐欣赏记录和用户数据,我们得以窥见消费者选择音乐背后的动机,并可以揭示特定人群的“音乐DNA”。这能够启发强大的营销战略,能够给音乐运营商带来极富价值的数据。而数字音乐的迅速发展造成了音乐歌曲的过剩,面对海量的歌曲和人们艰难的抉择,音乐推荐系统的出现可以为用户推荐其可能喜欢的歌曲,这种推荐服务可以为用户提供良好的体验,带来商业利益,并使可能“沉睡蒙尘”的歌曲重新焕发它的生机。
关键词:音乐、分析、推荐、Spark、ALS
本文采用kaggle平台上KKBox举办的—KKBox’s Music Recommendation Challenge比赛的公开数据集,对相应的音乐数据进行相应的基础统计分析和音乐推荐算法的实现。
KKBox是亚洲领先的音乐流媒体服务商,在台湾音乐占着重要的地位。官方比赛数据都来自都来自原数据集的抽样,除了对相应的ID进行了加密处理,其余数据都是原始数据。该数据集包含几个小文件
各数据文件的关系如下
Sqoop 是传统数据库与 Hadoop 之间数据同步的工具,它是 Hadoop 发展到一定程度的必然产物,它主要解决的是传统数据库和Hadoop之间数据的迁移问题。Sqoop 的产生主要源于以下几种需求:
1、多数使用 Hadoop 技术处理大数据业务的企业,有大量的数据存储在传统的关系型数据库中;
2、由于缺乏工具的支持,对 Hadoop 和 传统数据库系统中的数据进行相互传输是一件十分困难的事情;
3、基于前两个方面的考虑,极需一个在关系型数据库与 Hadoop 之间进行数据传输的组件;
Sqoop 是连接传统关系型数据库和 Hadoop 的桥梁。Sqoop 的核心设计思想是利用 MapReduce 加快数据传输速度。也就是说 Sqoop 的导入和导出功能是通过 MapReduce 作业实现的。Sqoop功能包括以下两个方面:
1、 将关系型数据库的数据导入到 Hadoop 及其相关的系统中,如 Hive和HBase。
2、 将数据从 Hadoop 系统里抽取并导出到关系型数据库。
Spark简介
Spark是基于内存计算的大数据并行计算框架,可用于构建大型的、低延迟的数据分析应用程序。2013年,Spark加入Apache孵化器项目后,开始获得迅猛的发展,如今已成为Apache软件基金会最重要的三大分布式计算系统开源项目之一(即Hadoop、Spark、Storm)。
Spark具有如下几个主要特点:
运行速度快:基于内存的执行速度可比Hadoop MapReduce快上百倍,基于磁盘的执行速度也能快十倍;
容易使用:Spark支持使用Scala、Java、Python和R语言进行编程,简洁的API设计有助于用户轻松构建并行程序,并且可以通过Spark Shell进行交互式编程;
通用性:Spark提供了完整而强大的技术栈,包括SQL查询、流式计算、机器学习和图算法组件,这些组件可以无缝整合在同一个应用中,足以应对复杂的计算;
运行模式多样:Spark可运行于独立的集群模式中,或者运行于Hadoop中,并且可以访问HDFS、HBase、Hive等多种数据源。
Spark生态系统
spark为什么去构建一个整的生态系统,而不是单一的组件,这主要是因为,在实际的企业应用中,我们一般会面临三大业务场景:
复杂的批量数据处理:时间跨度通常在数十分钟到数小时之间;
基于历史数据的交互式查询:时间跨度通常在数十秒到数分钟之间;
基于实时数据流的数据处理:时间跨度通常在数百毫秒到数秒 之间。
目前已有很多相对成熟的开源软件用于处理以上三种情景,一些企业可能只会涉及其中部分应用场景,只需部署相应软件即可满足业务需求,但是,对于互联网公司而言,通常会同时存在以上三种场景,就需要同时部署三种不同的软件,这样做难免会带来一些问题:
不同应用软件之间输入输出数据无法做到无缝共享,通常需要进行数据格式的转换。
不同的软件需要不同的开发和维护团队,带来了较高的使用成本。
比较难以对同一个集群中的各个系统进行统一的资源协调和分配,这会造成系统资源利用不充分。
Spark的设计遵循“一个软件栈满足不同应用场景”的理念,逐渐形成了一套完整的生态系统,Spark所提供的生态系统足以应对上述三种场景,即同时支持批处理、交互式查询和流数据处理。并且,Spark生态系统可以很好地实现与Hadoop生态系统的兼容。
BDAS架构
Spark的生态系统主要包含了Spark Core、Spark SQL、Spark Streaming、MLLib和GraphX 等组件,各个组件的具体功能如下:
推荐算法的应用是我们日常使用App中非常常用的功能,面向没有明确需求的用户。帮助用户在海量数据中,根据用户的历史信息和行为,向用户推荐其感兴趣的内容。推荐算法的作用主要有二,其一:解决信息过载问题;其二,挖掘长尾数据,一定程度上爆光冷门商品。
协同过滤推荐算法是推荐算法中较为经典的一种。一般来说,协同过滤推荐算法分为三种类型。第一种是基于用户(user-based)的协同过滤,第二种是基于项目(item-based)的协同过滤,第三种是基于模型(model based)的协同过滤。
以基于用户(user-based)的协同过滤推荐算法为例:其算法利用用户所在群体的共同喜好来向用户进行推荐。协同过滤利用了用户的历史行为(偏好、习惯等)将用户聚类成簇,这种推荐通过计算相似用户,假设被其他相似用户喜好的物品当前用户也感兴趣。
所以协同过滤的推荐方法通常包括两个步骤:根据用户行为数据找到和目标用户兴趣相似的用户集合(或者找到和历史购买物品相似的物品集合);找到这个集合中用户喜欢的且目标用户没有购买过的物品推荐给目标用户。
在实际使用中,协同过滤推荐算法面临两大制约,一是冷启动问题,冷启动问题又分为三类:
二是数据稀疏问题,数据稀疏问题是推荐系统面临的主要问题,也是导致推荐系统质量下降的重要原因。在一些大型网站如亚马逊,用户评价过的项目质量相对网站中总项目数量可谓是冰山一角,这就导致了用户项目评分矩阵的数据极端稀疏,在计算用户或项目的最近邻时准确率就会比较低,从而使得推荐系统的推荐质量急剧下降。而ALS算法是解决这类数据稀疏问题的一个措施。ALS算法的核心就是将稀疏评分矩阵拆解解为用户特征向量矩阵和项目特征向量矩阵的乘积,交替使用最小二乘法逐步计算用户/项目特征向量,通过用户/项目特征向量的矩阵来预测某个用户对某个项目的评分,以此方式达到矩阵降维目的。
本系统选取kaggle平台上KKBox的公开数据集来做离线分析和推荐服务,并把各项服务串起来搭建出一个基于ALS的音乐分析及离线推荐系统。
系统搭建流程图如下
该系统主要功能集中在数据分析、热门推荐、个性化推荐、前台交互展示方面
采用pandas进行数据集的离线分析,并把分析后获取的数据存储到MySQL。
数据分析主要分为用户方面的数据分析和歌曲方面的数据分析。
有关数据分析数据获取的代码如下
import pandas as pd
import numpy as np
import pymysql
from sqlalchemy import create_engine
from sklearn.preprocessing import LabelEncoder
train= pd.read_csv(r"kkbox-music-dataset\train.csv")
song_extra_info = pd.read_csv(r"kkbox-music-dataset\song_extra_info.csv")
songs = pd.read_csv(r"kkbox-music-dataset\songs.csv")
members = pd.read_csv(r"kkbox-music-dataset\members.csv")
def get_songs_info(conn):
#歌曲时长从毫秒转为秒
language_songs_length = songs.groupby(by = ["language"])["song_length"].mean()/1000/60
pd.io.sql.to_sql(language_songs_length,'language_songs_length', con=conn, if_exists='replace', index=True)
#统计被重复听的歌曲前200,得到song_repeats
repeats=train[train.target==1]
song_repeats=repeats["song_id"].value_counts()
song_repeats = pd.DataFrame({
'song_id':song_repeats.index,'count':song_repeats.values})
#三表合并
song_repeats = pd.merge(song_repeats,song_extra_info,on='song_id')
song_repeats = pd.merge(song_repeats,songs,on = "song_id")
pd.io.sql.to_sql(song_repeats[:200],'hotsongs', con=conn, if_exists='replace', index=True)
#统计
artistWordFre = pd.Series(song_repeats[:200]["artist_name"].tolist()).value_counts()
composerWordFre = pd.Series(song_repeats[:200]["composer"].tolist()).value_counts()
lyricistWordFre = pd.Series(song_repeats[:200]["lyricist"].tolist()).value_counts()
#series转dataframe
artistWordFre = pd.DataFrame({
"artist":artistWordFre.index,"count":artistWordFre.values})
composerWordFre = pd.DataFrame({
"composer":composerWordFre.index,"count":composerWordFre.values})
lyricistWordFre = pd.DataFrame({
"lyricist":lyricistWordFre.index,"count":lyricistWordFre.values})
pd.io.sql.to_sql(artistWordFre,'artist_wordfre', con=conn, if_exists='replace', index=True)
pd.io.sql.to_sql(composerWordFre,'composer_wordfre', con=conn, if_exists='replace', index=True)
pd.io.sql.to_sql(lyricistWordFre,'lyricist_wordfre', con=conn, if_exists='replace', index=True)
def get_users_info(conn):
#转换时间
members["registration_init_time"] = pd.to_datetime(members['registration_init_time'],format = r"%Y%m%d")
members["expiration_date"] = pd.to_datetime(members['expiration_date'],format = r"%Y%m%d")
#获取注册年份
members["registration_year"] = members["registration_init_time"].dt.year
#获取年份-注册渠道的数量关系
year_registered_via = members.groupby(by = ["registration_year","registered_via"],as_index = False)["msno"].count()
#构建年份-注册渠道透视表
registered_via_list = year_registered_via["registered_via"].value_counts().index.tolist()
registered_year_list = year_registered_via["registration_year"].value_counts().index.tolist()
year_via_df = pd.DataFrame(0,index = registered_year_list,columns = registered_via_list)
for i in year_registered_via.index:
year_via_df.loc[year_registered_via.loc[i, 'registration_year'], year_registered_via.loc[i, 'registered_via']] = year_registered_via.loc[i, 'msno']
pd.io.sql.to_sql(year_via_df,"year_via_df",con=conn, if_exists='replace', index=True)
#用户注册的城市分布
city_df = members.groupby(by = ["city"])["msno"].count().sort_values(ascending = True)
pd.io.sql.to_sql(city_df,"city_df",con=conn, if_exists='replace', index=True)
#用户注册的年份分布
year_df = members.groupby(by = ["registration_year"])["msno"].count().sort_index()
year_df.plot(kind = "area",title="注册用户的年份分布")
pd.io.sql.to_sql(year_df,"year_df",con=conn, if_exists='replace', index=True)
#获取年份-城市的数量关系
year_city = members.groupby(by = ["registration_year","city"],as_index = False)["msno"].count()
#构建年份-城市透视表
city_list = year_city["city"].value_counts().index.tolist()
registered_year_list = year_city["registration_year"].value_counts().index.tolist()
year_city_df = pd.DataFrame(0,index = registered_year_list,columns = city_list).sort_index()
for i in year_city.index:
year_city_df.loc[year_city.loc[i, 'registration_year'],year_city.loc[i, 'city']] = year_city.loc[i, 'msno']
pd.io.sql.to_sql(year_city_df,"year_city_df",con=conn, if_exists='replace', index=True)
def main():
plt.rcParams['font.sans-serif']=['SimHei']
conn = create_engine("mysql+pymysql://root:root@localhost/kkbox_music")
get_songs_info(conn)
get_users_info(conn)
main()
运行代码后,相关数据分析的结果数据存储到MySQL,在指定的数据库生成相应的数据表
下面根据数据分析得到的数据进行可视化呈现后的图表进行分析,相关可视化的步骤这里不详说。
各城市的用户注册占比
用户注册的城市分布,city1独占鳌头(猜测是台北),注册量比其余名次之和还要多,从注册人数来看,KKBox的受众城市分布为
说明KKBox在非第一簇的城市中推广的潜质极大。
各年份的用户注册数量变化
十年间,KKBox注册用户的数量发展呈现出三个阶段的发展。第一阶段(2004—2009),KKBox的注册渠道只有PC端,而PC端听音乐不方便,不能随时随地地听,此时发展是缓慢而稳定的,第二阶段(2009-2015),移动端开始发展崛起,用户听音乐的途径被拓宽,此时发展有了明显的增长,第三阶段(2015-2016),随着移动端穿戴设备的兴起,KKBox似乎又发现了一片红海。
热门歌手
占据热门歌曲排行榜的歌手主要有邓紫棋、周杰伦、五月天、田馥甄、林俊杰等
热门作曲家
占据热门歌曲排行榜的作曲家主要有周杰伦、韦礼安、阿信等
热门作词家
占据热门歌曲排行榜的作词家主要有方文山、阿信、林夕等
各城市各年份的用户注册数量分布
对于各城市各年份的用户注册人数数量发展来说,具有明显发展态势的城市有city22、city4、city5、city13、city15、city1,其中city1的发展和其他城市的发展拉开较大的差距,其余城市的发展相对持平。city1的迅猛发展得益于KKBox在台北策划的数起营销活动,包括成为Facebook亚太地区第一个合作伙伴,台北小巨蛋举行数字音乐风云榜等。
2004-2017年间用户注册渠道数量分布
12年间KKBox用户注册渠道注册数量总体趋于多元化转变。在2009年以前,所有用户都是从渠道9注册的,因为当时智能手机并没有普及,PC端几乎是一切网络平台的渠道来源。
KKBox于2008年开始推出了Mac版本,延伸到更多PC平台;2009年开始开始全面进军移动端,iOS和android版本均于当年上线。越来越多的用户选择在移动端注册听歌,这种发展趋势在2016年达到高潮。在2015年开始,KKBox相继退出AppleWatch和AndriodWear智慧手表,又迎来了一批注册高峰。
各语种歌曲的时长分布
各语种歌曲的时长分布基本集中在3-5分钟内,平均为4分钟。其实我们纵观歌曲的发展史,无论音乐风格如何变化,歌曲的平均时长基本集中在3-5分钟,原因如下:
早期唱片在工业技术上的限制导致了歌曲时长的局限性,进而影响了歌曲的创作与制作,即使后面技术的发展解决了唱片只能记录3分钟的技术局限性问题,3分钟左右的这个标准已经在音乐产业中得到了广泛认可,而且,相关学者表示3分钟左右的歌曲长度刚好足够表达创作者的情感,刚好能使得听者能产生共鸣并不会感到厌烦。于是大多数歌曲的创作都围绕着“三分钟定律”来创作。
并且在现代生活中,3-5分钟相比一小时的歌曲,可适用的场景更多更灵活,符合人么利用生活闲暇时间来听歌的生活习惯。
对数据集中对重复收听的歌曲记录进行统计聚合得到热门歌曲推荐Top200,并把获取到的歌曲榜单存储到MySQL。
相关热门推荐榜单数据获取的代码如下
train= pd.read_csv(r"kkbox-music-dataset\train.csv")
#统计被重复听的歌曲前200,得到song_repeats
repeats=train[train.target==1]
song_repeats=repeats["song_id"].value_counts()
song_repeats = pd.DataFrame({
'song_id':song_repeats.index,'count':song_repeats.values})
#三表合并
song_repeats = pd.merge(song_repeats,song_extra_info,on='song_id')
song_repeats = pd.merge(song_repeats,songs,on = "song_id")
#榜单数据存储到MySQL
pd.io.sql.to_sql(song_repeats[:200],'hotsongs', con=conn, if_exists='replace', index=True)
把用户收听歌曲的记录进行数据预处理,把用户ID和歌曲ID的字符串ID进行独热编码转换为自然数ID,并获取target字段(是否重复收听)组成建模数据,并上传至HDFS,方便Spark的取用。
import pandas as pd
from sklearn.preprocessing import LabelEncoder
train= pd.read_csv(r"kkbox-music-dataset\train.csv")
def change_id(dataset):
product_tags = dataset
le = LabelEncoder() #实例化
le = le.fit(product_tags)
label = le.transform(product_tags)
return label
def main():
#id进行label编码 dataset为数据集 product_tags为需要编码的特征列(假设为第一列)
train_copy = train
train_copy.iloc[:, :1] = change_id(train.iloc[:,:1])
train_copy.iloc[:, 1:2] = change_id(train.iloc[:,1:2])
traindata = train_copy[["msno","song_id","target"]]
traindata.to_csv("traindata.csv")
后可采取Hadoop命令将建模数据集导入到HDFS中
hadoop dfs -put traindata.csv hdfs://Master:9000/kkbox_music
建模的数据如下
利用Spark Mlib的协同过滤推荐算法ALS进行推荐计算,编写Scala代码,运行程序,为每位用户推荐5首歌曲,并把得到的推荐结果(用户ID::歌曲ID::推荐度)存储到HDFS。
Scala代码如下
import org.apache.spark.{
SparkConf, SparkContext}
import org.apache.spark.mllib.recommendation.ALS
import org.apache.spark.mllib.recommendation.MatrixFactorizationModel
import org.apache.spark.mllib.recommendation.Rating
object MusicALSRmd{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("musicALSRmd").setMaster("spark://Master:7077")
val sc = new SparkContext(conf)
val data1 = sc.textFile("hdfs://Master:9000/kkbox_music/traindata.csv")
val header = data1.first()
val tmpdata = data1.filter(x => x != header)
val rawrating1 = tmpdata.map(x=>x.split(",").slice(1,4))
val ratings2 = rawrating1.map {
case Array(user,product, rating) => Rating(user.toInt, product.toInt, rating.toDouble) }
val rank = 5
val numIterations = 15
val model = ALS.train(ratings2, rank, numIterations, 0.01)
#保存模型
model.save(sc,"hdfs://Master:9000/kkbox_result/")
/*
推荐n件商品给所有用户
val predictProductsForUsers = model.recommendProductsForUsers(4)
predictProductsForUsers.foreach{
t =>
println("\r\n向id为:" + t._1 + "的用户推荐了以下四件商品:")
for(i <- 0 until t._2.length){
println("UID:" + t._2(i).user + ",PID:" + t._2(i).product + ",SCORE:" + t._2(i).rating)
}
}
*/
#保存推荐结果
val recommendResult = model.recommendProductsForUsers(5)
recommendResult.map{
t =>{
var str = ""
for(i <- 0 until t._2.length){
str += t._2(i).user + "::" + t._2(i).product + "::" + t._2(i).rating + "\n"
}
str
}
}.saveAsTextFile("hdfs://Master:9000/kkbox_result/rmdResult1")
}
}
获取到的HDFS上存储的推荐结果数据如下
可再利用Sqoop把HDFS上的推荐结果导出到MySQL
先在MySQL上创建rmd_songs表存储推荐结果
create table rmd_songs(
userID varchar(50),
songID varchar(50),
rmd_level varchar(50)
)
再运行Sqoop导出代码
sqoop export \
--connect 'jdbc:mysql://192.168.43.10:3306/kkbox_music' \
--username 'root' \
--password 'root' \
--table 'rmd_songs' \
--export-dir '/kkbox_result/rmdResult1/*' \
--columns 'userID,songID,rmd_level' \
--mapreduce-job-name 'hdfs to mysql' \
--input-fields-terminated-by '::' \
--input-lines-terminated-by '\n'
利用Flask、Bootstrap4、Echarts、MySQL搭建起一个Web应用程序,其功能如下:
构建该Web应用程序的首页;
把该系统的功能封装成一个个子页面,首页链接了去到各个页面的入口,可从首页去到本系统的任何一个页面。
首页的Flask代码如下,跳转到首页时,其路由逻辑会从comments表中提取出评论内容和评论的用户账号,利用jinja渲染到评论区,模板里的jinja代码也会判断用户名是否为空(用户是否登录),如已登录,会把登录用户的用户名渲染到导航栏。
app = Flask(__name__)
app.config['SECRET_KEY'] = 'atuo'
user_info = ""
@app.route("/",methods = ["GET","POST"])
def index():
db = Mysql()
comments,comment_users = db.get_comments()
return render_template("index.html",comments = comments,comment_users = comment_users,user_info = user_info)
model层获取评论内容和评论的用户账号的代码如下
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
from sqlalchemy import create_engine
import pymysql.cursors
class Mysql(object):
def __init__(self):
self.conn = create_engine("mysql+pymysql://root:root@localhost/kkbox_music")
"""以下代码省略"""
def get_comments(self):
comment_sql = "SELECT * FROM comments;"
result = pd.read_sql(comment_sql,con=self.conn)
comments = result["comment"].tolist()
users = result["user"].tolist()
return comments,users
首页的前端代码如下
<html lang="en">
<head>
<meta charset="UTF-8">
<title>KKBox音乐数据分析及推荐系统首页title>
<link rel="stylesheet" href="{
{ url_for('static', filename='css/bootstrap.css') }}">
<script src="{
{ url_for('static', filename='jquery-3.4.0.min.js') }}">script>
<script src="{
{ url_for('static', filename='bootstrap.min.js') }}">script>
<style>
.empty {
height: 30px;
}
.empty2 {
height: 10px;
}
.col-center-block {
float: none;
display: block;
margin-left: auto;
margin-right: auto;
}
body {
background-color: #f3f3f3!important;
}
.carousel-inner img {
width: 100%;
height: 100%;
}
#cards {
margin-left: 20px;
}
.w3copyright-agile {
margin: 2em 0 1em;
text-align: center;
}
style>
head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg bg-light">
<a class="navbar-brand" href="#">Atuo Musica>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon">span>
button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav ml-auto">
{%if user_info == ""%}
<li class="nav-item active">
<a class="nav-link" href="{
{url_for('login')}}">登录a>
li>
<li class="nav-item">
<a class="nav-link" href="{
{url_for('register')}}">注册a>
li>
{%else%}
<li class="nav-item active">
<a class="nav-link" href="#">{
{user_info}}<img src="/static/img/person.png" class="d-inline-block align-top" width="28" height="28">a>
li>
<li class="nav-item">
<a class="nav-link" href="/quit_login">退出登录a>
li>
{%endif%}
ul>
div>
nav>
<div class="empty2">div>
<div id="demo" class="carousel slide">
<ul class="carousel-indicators">
<li data-target="#demo" data-slide-to="0" class="active">li>
<li data-target="#demo" data-slide-to="1">li>
<li data-target="#demo" data-slide-to="2">li>
ul>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="/static/img/rol_music1.jpg">
<div class="carousel-caption">
<h1>Atuo Musich1>
<h3>基于ALS的音乐分析及离线推荐系统h2>
div>
div>
<div class="carousel-item">
<img src="/static/img/rol_music2.jpg">
div>
<div class="carousel-item">
<img src="/static/img/rol_music3.jpg">
div>
div>
<a class="carousel-control-prev" href="#demo" data-slide="prev">
<span class="carousel-control-prev-icon">span>
a>
<a class="carousel-control-next" href="#demo" data-slide="next">
<span class="carousel-control-next-icon">span>
a>
div>
<div class="empty">div>
<div class="row" id="cards">
<div class="col-lg-4">
<div class="card" style="width: 18rem;">
<img class="card-img-top" src="/static/img/music1.jpg">
<div class="card-body">
<h5 class="card-title">热门歌曲推荐h5>
<p class="card-text">对重复收听的歌曲记录进行统计聚合得到热门歌曲推荐p>
<a href="{
{ url_for('mysql') }}" class="btn btn-primary">showa>
div>
div>
div>
<div class="col-lg-4">
<div class="card" style="width: 18rem;">
<img class="card-img-top" src="/static/img/music2.jpg">
<div class="card-body">
<h5 class="card-title">数据分析h5>
<p class="card-text">对KKBox音乐数据进行基础性分析,并进行图表展示p>
<a href="{
{ url_for('huge') }}" class="btn btn-primary">showa>
div>
div>
div>
<div class="col-lg-4">
<div class="card" style="width: 18rem;">
<img class="card-img-top" src="/static/img/music3.jpg">
<div class="card-body">
<h5 class="card-title">个性化推荐h5>
<p class="card-text">输入用户ID,按照推荐度从高到低排序推荐5首歌曲p>
<a href="{
{ url_for('rmd_music') }}" class="btn btn-primary">showa>
div>
div>
div>
div>
<div class="empty">div>
<div class='col-center-block' style="width: 90%; height: auto;">
<h5>请发布您对该系统的看法h5>
<form method="POST" action="/comment">
<div class="form-container">
<div class="form-group">
<textarea name="content" rows="2" class="form-control" placeholder="请输入评论">textarea>
div>
<div class="form-group">
<button class="btn btn-success">发布button>
div>
div>
form>
div>
<div class="col-center-block" style="width: 90%; height: auto;">
{%for i in comments%}
<div class="card text-center">
<div class="card-header text-white bg-info">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link active" href="#">{
{loop.index}}楼a>
li>
ul>
div>
<div class="card-body">
<p class="card-text text-left">{
{i}}p>
<p class="card-text text-right">{
{comment_users[loop.index-1]}}p>
div>
div>
{%endfor%}
div>
<div class="empty">div>
<div class="w3copyright-agile">
<p>© 2021 小坨毕设p>
div>
div>
body>
html>
首页的前端效果如下
2. **把数据分析获取到的数据进行Echarts的前端展示**;
由于篇幅问题,这里不对全部代码作展示,仅以各年份的用户注册数量变化数据为例。Flask获取数据接口代码和渲染HTML模板代码如下
@app.route('/year_via',methods = ["GET","POST"])
def year_via():
db = Mysql()
data = db.get_yearVia_data()
return data
#可视化图表页面展示
@app.route("/huge",methods = ["GET","POST"])
def huge():
return render_template("imgview.html")
model层代码如下
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
from sqlalchemy import create_engine
import pymysql.cursors
class Mysql(object):
def __init__(self):
"""以上代码省略"""
self.conn = create_engine("mysql+pymysql://root:root@localhost/kkbox_music")
"""以下代码省略"""
def get_yearVia_data(self):
result = pd.read_sql("select * from year_df",con=self.conn)
year = result["registration_year"].values.tolist()
vias = result["msno"].values.tolist()
return {
"year":year,"vias":vias}
Echarts绘图代码如下(imgview.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>KKBox音乐数据分析</title>
<link rel="stylesheet" href="/static/css/bootstrap.css">
<script src="/static/bootstrap.min.js"></script>
<script src="/static/echarts.min.js"></script>
<script src="https://echarts-www.cdn.bcebos.com/zh/asset/theme/macarons.js"></script>
<script src="/static/echarts-wordcloud.min.js"></script>
<script src="/static/echarts-gl.min.js"></script>
<style>
body {
background-image: url("/static/bg.jpeg");
}
h1 {
color: #fff;
}
#box1,
#box2,
#box3,
#box4,
#box5,
#box6,
#box7,
#box8 {
background-color: azure!important;
display: inline-block;
}
.contain {
text-align: center;
}
.public {
width: 600px;
height: 500px;
padding: 10px;
border: 1px solid #ccc;
box-shadow: 0 0 8px #aaa inset;
}
.empty {
height: 30px;
}
</style>
</head>
<body>
<div class="container-fluid">
<nav class="navbar navbar-expand-lg bg-light">
<a class="navbar-brand" href="{
{ url_for('index') }}">返回首页</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</nav>
</div>
<div class="empty"></div>
<h1 align="center">KKBox音乐数据分析</h1>
<div class="empty"></div>
<div class="contain">
<div id="box1" class="public"></div>
<div id="box2" class="public"></div>
<div id="box3" class="public"></div>
<div id="box4" class="public"></div>
<div id="box5" class="public"></div>
<div id="box6" class="public"></div>
<div id="box7" class="public"></div>
<div id="box8" class="public"></div>
</div>
<script>
//---------------------------以上代码省略---------------------------
//年份-注册条形图
var yearVia_chart = echarts.init(document.getElementById("box2"), 'macarons');
$.get("/year_via", function(data) {
dataAxis = data.year;
dataValue = data.vias;
option = {
title: {
text: '各年份的用户注册数量变化',
x: 'left',
},
color: ['#3398DB'],
tooltip: {
trigger: 'axis',
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true
},
xAxis: [{
type: 'category',
data: dataAxis,
axisTick: {
show: true,
alignWithLabel: true,
interval: 0
},
axisLabel: {
interval: 0,
rotate: 45,
}
}],
yAxis: [{
type: 'value',
nameLocation: 'middle',
nameGap: 50
}],
series: [{
name: "年份-注册",
type: 'bar',
barWidth: '60%',
data: dataValue
}]
};
yearVia_chart.setOption(option);
})
//---------------------------以下代码省略---------------------------
绘图结果如下
Flask获取数据接口代码和渲染HTML模板代码如下
@app.route('/mysql')
def mysql():
page = request.args.get("page")
if not page or int(page) <= 0:
page = 1
db = Mysql()
keyword = request.args.get("keyword")
infos = db.get_info(int(page),keyword)
page_end = db.get_infos_number()
#这里做个if验证
if int(page)-3<= 0:
page_range = range(1, 8)
else:
page_range = range(int(page)-3,int(page)+4)
if int(page)+4 >= page_end:
page_range = range(int(page_end)-6, math.ceil(page_end)+1)
return render_template("hotsongs.html",infos = infos,page = int(page),page_range = page_range)
model层代码如下,包含了分页和按歌手名搜索歌曲的实现
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
from sqlalchemy import create_engine
import pymysql.cursors
class Mysql(object):
def __init__(self):
db = 'kkbox_music'
host = 'localhost'
port = 3306
user = 'root'
passwd = 'root'
self.db_conn = pymysql.connect(host=host, port=port, db=db, user=user, passwd=passwd, charset='utf8')
self.conn = create_engine("mysql+pymysql://root:root@localhost/kkbox_music")
self.db_cur = self.db_conn.cursor()
self.db_conn.autocommit(1)
def get_info(self,page,keyword):
sql = "select * from hotsongs "
if keyword:
sql = sql + "where artist_name like '%" + keyword + "%'"
start = (page-1)*10
sql = sql + "limit " + str(start) + ",10;"
num = self.db_cur.execute(sql)
infos = self.db_cur.fetchall()
return infos
def get_infos_number(self):
sum_sql = "select * from hotsongs"
infos_number = self.db_cur.execute(sum_sql)
return infos_number/10
def __del__(self):
self.db_conn.close
榜单的前端代码如下(hotsongs.html)
<html lang="en">
<head>
<meta charset="UTF-8">
<title>KKBox热门歌曲推荐Top200title>
<link rel="stylesheet" href="{
{ url_for('static', filename='css/bootstrap.css') }}">
<script src="{
{ url_for('static', filename='jquery-3.4.0.min.js') }}">script>
<script src="{
{ url_for('static', filename='bootstrap.min.js') }}">script>
<style>
body {
background-image: url("/static/bg.jpg");
}
h1 {
margin-bottom: 40px;
}
.main {
padding: 10px;
width: 1200px;
/*height: 750px;*/
margin: 0 auto;
}
.empty {
height: 30px;
}
.form-control,
.btn-default {
margin-top: 20px;
display: inline-block;
width: 50px;
}
.w3copyright-agile {
margin: 2em 0 1em;
text-align: center;
}
style>
head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg bg-light">
<a class="navbar-brand" href="{
{ url_for('index') }}">返回首页a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon">span>
button>
nav>
<div class="empty">div>
<h1 align="center">KKBox热门歌曲推荐Top200h1>
<table class="table table-dark">
<thead>
<tr>
<th>IDth>
<th>播放次数th>
<th>歌曲名th>
<th>音乐类型th>
<th>歌手th>
<th>作曲家th>
<th>作词家th>
<th>语言类型th>
tr>
thead>
<tbody>
{%for info in infos%}
<tr>
<td>{
{info[0]}}td>
<td>{
{info[2]}}td>
<td>{
{info[3]}}td>
<td>{
{info[6]}}td>
<td>{
{info[7]}}td>
<td>{
{info[8]}}td>
<td>{
{info[9]}}td>
<td>{
{info[10]}}td>
tr>
{%endfor%}
tbody>
table>
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item">
<a href="/mysql?page={
{ page-1 }}" aria-label="Previous" class="page-link">
<span aria-hidden="true">«span>
a>
li>
{% for pg in page_range %}
<li class="page-item"><a href="/mysql?page={
{ pg }}" class="page-link">{
{ pg }}a>li>
{% endfor %}
<li class="page-item">
<a href="/mysql?page={
{page + 1}}" aria-label="Next" class="page-link">
<span aria-hidden="true">»span>
a>
li>
ul>
nav>
<form action="/mysql">
<div class="row">
<div class="col-xl-8">
<div class="input-group">
<input type="text" class="form-control" name="keyword" placeholder="搜索你喜欢的歌手">
div>
div>
<div class="col-xl-4 align-self-end">
<button class="btn btn-success" type="submit">Go!button>
div>
div>
form>
<div class="w3copyright-agile">
<p>© 2021 小坨毕设p>
div>
div>
body>
html>
Flask代码如下
#个性化推荐
@app.route("/rmd_form",methods = ["GET","POST"])
def rmd_music():
if request.method == "GET":
return render_template("form.html")
user_info = request.form.to_dict()
user_id = user_info.get("user_id")
db = Mysql()
musics_id = db.get_rmd_music(user_id)
return render_template("form.html",infos = musics_id,tops = [0,1,2,3,4],user_id = user_id)
model层代码如下
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
from sqlalchemy import create_engine
import pymysql.cursors
class Mysql(object):
def __init__(self):
"""以上代码省略"""
self.conn = create_engine("mysql+pymysql://root:root@localhost/kkbox_music")
"""以下代码省略"""
def get_rmd_music(self,user_id):
sql = "SELECT * FROM rmd_songs where userID = " + user_id +";"
result = pd.read_sql(sql,con=self.conn)
rmd_musics = result["rmd_level"].tolist()
return rmd_musics
前端代码如下(form.html)
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/css/bootstrap.css">
<script src="/static/jquery-3.4.0.min.js">script>
<script src="/static/bootstrap.min.js">script>
<title>KKBox音乐个性化推荐title>
<style>
.empty {
height: 60px;
}
.col-center-block {
float: none;
display: block;
margin-left: auto;
margin-right: auto;
}
h1 {
margin-top: 20px;
}
body {
background-image: url("/static/bg.jpg");
}
.w3copyright-agile {
margin: 2em 0 1em;
text-align: center;
}
style>
head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg bg-light">
<a class="navbar-brand" href="{
{ url_for('index') }}">返回首页a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
button>
nav>
<div class="empty">div>
<h1 align="center">KKBox音乐个性化推荐h1>
<div class="empty">div>
<div class="col-center-block">
<form class="form-horizontal" role="form" action="/rmd_form" method="post">
<div class="form-group">
<div class="row">
<div class="col-xl-5 offset-xl-3">
<input type="text" class="form-control" name="user_id" placeholder="请输入用户ID">
div>
<div class="col-xl-4">
<button type="submit" class="btn btn-success">提交button>
div>
div>
div>
form>
div>
<p align="center">以下为给用户{
{user_id}}推荐的五首歌曲p>
<div class="col-center-block" style="width: 75%; height: auto;">
<table class="table table-dark">
<thead>
<tr>
<th>推荐度排名th>
<th>歌曲IDth>
tr>
thead>
<tbody>
{%for i in tops%}
<tr>
<td>{
{i+1}}td>
<td>{
{infos[i]}}td>
tr>
{%endfor%}
tbody>
table>
div>
<div class="w3copyright-agile">
<p>© 2021 小坨毕设p>
div>
div>
body>
html>
前端效果如下,比如查询给用户18624的个性化推荐歌曲
5. 用户可进行注册、登录,注册用户名不可重复,若登录时用户名或密码输错,会有相应的错误提示;
Flask代码如下
app = Flask(__name__)
app.config['SECRET_KEY'] = 'atuo'
user_info = ""
#----------------增加登录和注册功能------------------
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
db = Mysql()
username = request.form.get('username')
password = request.form.get('password')
user_password = db.get_user(username)
if user_password == password:
global user_info
user_info = username
return redirect(url_for("index"))
elif user_password == "用户不存在":
flash("该用户不存在",category='nouser_error')
return render_template('login.html')
else:
flash("密码错误",category='error')
return render_template('login.html')
@app.route("/register",methods = ["GET","POST"])
def register():
if request.method == 'GET':
return render_template("register.html")
else:
db = Mysql()
username = request.form.get('username')
password = request.form.get('password')
user_password = db.get_user(username)
if user_password != "用户不存在":
flash("该用户已存在",category='error')
return render_template('register.html')
else:
db.user_register(username = username,password = password)
return redirect(url_for('login'))
model层代码如下
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
from sqlalchemy import create_engine
import pymysql.cursors
class Mysql(object):
def __init__(self):
db = 'kkbox_music'
host = 'localhost'
port = 3306
user = 'root'
passwd = 'root'
self.db_conn = pymysql.connect(host=host, port=port, db=db, user=user, passwd=passwd, charset='utf8')
self.conn = create_engine("mysql+pymysql://root:root@localhost/kkbox_music")
self.db_cur = self.db_conn.cursor()
self.db_conn.autocommit(1)
def get_user(self,username):
login_sql = "select password from users where username='" + username+"';"
self.db_cur.execute(login_sql)
try:
user_password = self.db_cur.fetchall()[0][0]
return user_password
except:
return "用户不存在"
def user_register(self,username,password):
register_sql = "INSERT INTO users (username,password) VALUES('{}','{}');".format(username,password)
self.db_cur.execute(register_sql)
def __del__(self):
self.db_conn.close
登录页面前端代码(注册页面和登录页面的前端代码基本一致)
<html lang="en">
<head>
<title>登录页面title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="/static/css/login.css" rel="stylesheet" type="text/css" media="all" />
head>
<body>
<div class="main-w3layouts wrapper">
<div class="main-agileinfo">
<div class="agileits-top">
<form action="/login" method="post">
<input class="text" type="text" name="username" placeholder="用户名" required="">
<input class="text" type="password" name="password" placeholder="密码" required="">
<input type="submit" value="登录"> {% for msg in get_flashed_messages() %}
<p style="color: rgb(255, 67, 67);">{
{ msg }}p>
{% endfor %}
form>
<p>创建一个账号? <a href="/register"> 立即注册!a>p>
<p><a href="/">返回首页a>p>
div>
div>
<div class="w3copyright-agile">
<p>© 2021 小坨毕设p>
div>
<ul class="w3lsg-bubbles">
<li>li>
<li>li>
<li>li>
<li>li>
<li>li>
<li>li>
<li>li>
<li>li>
<li>li>
<li>li>
ul>
div>
body>
html>
注册登录效果如下,可在注册页面注册相应的用户名和密码
注册成功之后会自动跳转到登录页面,进行用户登录
登录之后跳转到首页,导航栏出现用户名
6. 用户可在首页下方的评论区发表评论;
在评论区点击“提交”按钮后,页面会跳转到“/comment”路由,执行下面Flask代码的逻辑,model层写入评论内容和用户账号到数据库的comments表,路由重定向到首页
@app.route("/comment",methods = ["GET","POST"])
def comment():
if request.method == "POST":
comment_info = request.form.to_dict().get("content")
db = Mysql()
db.insert_comment(comment_info,user_info)
return redirect(url_for('index'))
model层代码如下
# -*- coding: utf-8 -*-
import pymysql
import pandas as pd
from sqlalchemy import create_engine
import pymysql.cursors
class Mysql(object):
def __init__(self):
db = 'kkbox_music'
host = 'localhost'
port = 3306
user = 'root'
passwd = 'root'
self.db_conn = pymysql.connect(host=host, port=port, db=db, user=user, passwd=passwd, charset='utf8')
self.conn = create_engine("mysql+pymysql://root:root@localhost/kkbox_music")
self.db_cur = self.db_conn.cursor()
self.db_conn.autocommit(1)
def insert_comment(self,comment,user_info):
insert_sql = "INSERT INTO comments (comment,user) VALUES('{}','{}');".format(comment,user_info)
self.db_cur.execute(insert_sql)
评论区的前端代码如下
<div class='col-center-block' style="width: 90%; height: auto;">
<h5>请发布您对该系统的看法h5>
<form method="POST" action="/comment">
<div class="form-container">
<div class="form-group">
<textarea name="content" rows="2" class="form-control" placeholder="请输入评论">textarea>
div>
<div class="form-group">
<button class="btn btn-success">发布button>
div>
div>
form>
div>
<div class="col-center-block" style="width: 90%; height: auto;">
{%for i in comments%}
<div class="card text-center">
<div class="card-header text-white bg-info">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link active" href="#">{
{loop.index}}楼a>
li>
ul>
div>
<div class="card-body">
<p class="card-text text-left">{
{i}}p>
<p class="card-text text-right">{
{comment_users[loop.index-1]}}p>
div>
div>
{%endfor%}
div>
评论区的前端效果如下。评论内容的显示以类似贴吧楼层的形式展示,每一层评论的楼层的空白区域为评论内容,空白区域右下角显示该评论的用户的用户名。
音乐在日常生活中是非常重要的娱乐方式,新一次的信息革命也顺势带来了数字音乐的迅速传播,我们进入音乐大数据的时代,得以去倾听海量的歌曲,在音乐的海洋里遨游。但随着音乐大数据时代的到来所面临的挑战是,数字音乐难免会存在信息过载和存在长尾数据等问题,在这种情况下,对用户进行个性化的音乐推荐显得极为重要。而传统的协同过滤推荐算法存在冷启动和数据稀疏的问题,并受到可扩展性的制约,海量的数据难以得到有效的利用。基于上述的问题,本文采用kaggle平台上KKBox举办的—KKBox’s Music Recommendation Challenge比赛的公开数据集,借助了两个强大的工具——Spark和ALS算法,构建起一个音乐分析及离线推荐系统。
本文首先对Sqoop、Spark和协同过滤推荐算法等相关技术作了简要的概述,介绍了Sqoop的相关用处和Spark在大数据时代下所具备的优势,探讨了传统协同过滤推荐算法存在的冷启动和数据稀疏问题,提出了ALS算法是解决传统协同过滤推荐算法应用时所存在的数据稀疏问题的有效方法,基于这样的前景提要来介绍基于ALS的音乐分析及离线推荐系统的设计与实现
在分析方面,本系统采取Pandas对音乐数据进行分析,并利用Flask、MySQL和Echarts等架构对分析后的数据进行可视化呈现;
在推荐方面,本系统采取Spark Mlib中的ALS算法对音乐数据进行推荐。借助Spark强大的内存计算和并行化计算的能力,提高大数据处理的效率;
在前台交互展示方面,本系统采取Flask+MySQL+Bootstrap4构建起一个Web应用程序,具备如注册登录、发表评论等基本交互展示功能;