收视率系统

本文介绍了电视收视率分析系统的项目背景、需求、功能、收视率计算方法、开发流程,涉及数据采集、MapReduce处理、Hive数据仓库及MySQL数据库的使用。
摘要由CSDN通过智能技术生成

一、项目背景

对《中国好声音》、《快乐男声》、《最美和声》、《中国梦之声》等各种音乐选节目收视率的一个调查。依托北330万高清交互数字电视双向用户,从中随机抽取25000户作为样本进行统计。

二、项目需求

这里展示从节目的维度,统计每个节目的平均收视人数、平均到达人数、收视率、到达率和市场份额。我们根据每天抽样用户的收视数据,统计出每个节目按天、按小时、按分钟的上述5个收视指标。

三、 系统功能(这里以一个维度为例)

主要包括收视概况浏览、收视率走势分析、收视指标对比、收视数据对比查看。

五、 收视指标定义

这里写图片描述

收视人数
总的:某天收视人数(S11):sum(distinct stbnum) WHERE 指定日期。

频道:某天收视人数(S21):sum(distinct stbnum) WHERE 指定日期 AND 指定频道。

节目(这1天内收看此节目的人数):某天收视人数(S31):sum(distinct stbnum) WHERE 指定日期 AND 指定节目。

平均收视人数
该指标为在选定期间内平均每分钟的用户ID数。

总的:

每分钟(X11): sum(distinct stbnum)

每分钟(X12):……

每分钟(X1n):……

平均收视人数:(X11 + X12 + … + X1n)/n

频道:

每分钟(X21): sum(distinct stbnum) where 指定频道名称 = 频道名

每分钟(X22):……

每分钟(X2n):……

平均收视人数:(X21 + X22 + … + X2n)/n

节目:

每分钟(X31): sum(distinct stbnum) where 指定节目名称 = 节目名

每分钟(X32):……

每分钟(X3n):……

平均收视人数:(X31 + X32 + … + X3n)/n

收视率
平均收视人数/系统总ID数。

CONSTANT系统总ID数 IDNUM = sum(distinct stbnum)。

总的,频道,节目:

每分钟收视率Y1:X1/IDNUM;

每分钟收视率Y2:……

每分钟收视率Yn:Xn/IDNUM

某一段时间的收视率:(Y1 + Y2… + Yn)/n

市场份额
对应频道平均收视人数/所有频道平均收视人数。

总体:

100%

频道:

每分钟(Z21):X21/ sum(distinct stbnum) where 时间

……

每分钟(Z2n):X2n/ sum(distinct stbnum) where 时间

市场份额:(Z21 + … + Z2n)/n

节目:

每分钟(Z31):X31/ sum(distinct stbnum) where 时间

……

每分钟(Z3n):X3n/ sum(distinct stbnum) where 时间

市场份额:(Z31 + … + Z3n)/n

平均到达人数
默认扣除在某个频道或整个系统停留时间小于60s的用户ID,不包括60s,跟平均收视人数的差别在于排除原始记录中停留时间小于60s的记录。

总的:

每分钟(U11): sum(distinct stbnum) WHERE ((a_e – a_s)>=60)

每分钟(U12):……

每分钟(U1n):……

平均到达人数:(U11 + U12 + … + U1n)/n

频道:

每分钟(U21): sum(distinct stbnum) where 指定频道名称 = 频道名 AND ((a_e – a_s)>=60)

每分钟(U22):……

每分钟(U2n):……

平均到达人数:(U21 + U22 + … + U2n)/n

节目:

每分钟(U31): sum(distinct stbnum) where 指定节目名称 = 节目名 AND ((a_e – a_s)>=60)

每分钟(U32):……

每分钟(U3n):……

平均到达人数:(U31 + U32 + … + U3n)/n

到达率
平均到达人数/系统总ID数。

CONSTANT系统总ID数 IDNUM = sum(distinct stbnum)。

总的,频道,节目:

每分钟(V1):U1/ IDNUM

……

每分钟(Vn):Un/ IDNUM

某一段时间的到达率:(V1 + V2 + … + Vn)/n

人均收视时长
所有频道 —— 每天所有用户ID的总时间/用户ID数;具体某个频道 —— 访问过该频道的所有用户ID每天总时间/该频道每天的用户ID数;具体某个栏目 —— 访问过每期节目的所有用户ID总时间/该栏目的用户ID数。

总的:

某天人均收视时长(W11):SUM(a_e – a_s)/S11

频道:

某天人均收视时长(W21):SUM(a_e – a_s)/S21

节目:

某天人均收视时长(W31):SUM(a_e – a_s)/S31

六、开发流程

1.通过flume收集工具将用户产生的原始数据收集到hdfs分布式文件系统。

2.编写MR程序对原始的收视数据进行解析、清洗、提取业务所需的有效字段。

3.利用hive工具将MR处理后的数据导入数据仓库,同时对该数据进行统计分析。

4.编写应用程序或者使用sqoop工具将hive分析的最终数据导入数据库,比如mysql数据库。

5.前端查询,实现数据的可视化。

七.源数据

这里写图片描述

这里写图片描述
这里写图片描述

利用hdfs的小文件合并MergeSmallFilesToHDFS.java将每天的小文件合并为大文件,具体参考http://blog.csdn.net/zoeyen_/article/details/78947676

这里写图片描述

八、将源数据上传到hdfs文件系统

这里使用flume采集工具,我将flume工具安装在主节点(pc1)上,仅使用了一层agent。

①启动集群

这里写图片描述

②修改flume的配置文件

[hadoop@pc1 conf]$ vi flume-conf.properties

agent1.channels = ch1
agent1.sinks = sink1

# Define and configure an Spool directory source(使用spooldir监控日志目录)
agent1.sources.source1.channels = ch1
agent1.sources.source1.type = spooldir
agent1.sources.source1.spoolDir = /home/hadoop/tvdata 
#前三项必须配置,具体参数可参考官方文档
agent1.sources.source1.ignorePattern = event(_\d{
  4}\-\d{
  2}\-\d{
  2}_\d{
  2}_\d{
  2})?\.log(\.COMPLETED)?
agent1.sources.source1.deserializer.maxLineLength = 10240 
#配置收集每行数据的最大长度

# Configure channel(channel 选择file,防止数据丢失)
agent1.channels.ch1.type = file 
#也可以配置内存
agent1.channels.ch1.checkpointDir = /home/hadoop/app/flume/checkpointDir
agent1.channels.ch1.dataDirs = /home/hadoop/app/flume/dataDirs
#在flume目录下创建以上两个路径
# Define and configure a hdfs sink(数据采集到hdfs)
agent1.sinks.sink1.channel = ch1
agent1.sinks.sink1.type = hdfs
agent1.sinks.sink1.hdfs.path =
hdfs://pc1:9000/home/app/tvdata/%Y%m%d
#如果是集群就配置对外提供服务的地址
agent1.sinks.sink1.hdfs.useLocalTimeStamp = true
agent1.sinks.sink1.hdfs.rollInterval = 300
agent1.sinks.sink1.hdfs.rollSize = 67108864
agent1.sinks.sink1.hdfs.rollCount = 0
#agent1.sinks.sink1.hdfs.codeC = snappy #没有做snappy压缩

③创建路径

这里写图片描述

这里写图片描述

④将源数据上传到tvtest目录下

这里写图片描述

这里写图片描述

⑤进入flume安装目录,执行运行命令

[hadoop@node2 flume]$bin/flume-ng agent -n agent1 -c conf -f conf/flume-conf.properties 

⑥查看

这里写图片描述

这里写图片描述

出现乱码
查看官方文档,hdfs.fileType默认为SequenceFil,改为datastream就可以按原样输出数据到hdfs。

这里写图片描述

这里写图片描述

删除已经采集到hdfs的数据,重新采集

这里写图片描述

这里写图片描述

九、 编写MR程序对原始的收视数据进行解析、清洗、提取业务需要的有效字段

①对源数据进行预处理,提取需要的数据。编写一个只有mapper的mapreduce程序,调用一个DataUtil接口,这个接口引用了jsoup的jar包,来解析源数据的每一行数据,将机顶盒号和日期作为输出key,其它作为输出value。其中日期的解析由TimeUtil这个类实现。

/*
 * 解析机顶盒用户原始数据
 */
public class ParseAndFilterLog extends Configured implements Tool {
   

    /*
     * 只需Mapper完成原始数据解析
     */
    public static class ExtractTVMsgLogMapper extends
            //Mapper<LongWritable, BytesWritable, Text, Text> {
   
        Mapper<LongWritable, Text, Text, Text> {
        //public void map(LongWritable key, BytesWritable value, Context context)
        public void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            // 原始数据
            //String data = new String(value.getBytes(), 0, value.getLength());
            String data = value.toString();
            // 调用接口直接解析出我们需要数据格式
            // stbNum + "@" + date + "@" + sn + "@" + p+ "@" + s + "@" + e + "@"
            // + duration
            DataUtil.transData(data, context);
        }

    }

    public int run(String[] args) throws Exception {
        // TODO Auto-generated method stub
        Configuration conf = new Configuration();
        String[] otherArgs = new GenericOptionsParser(conf, args)
                .getRemainingArgs();
        if (otherArgs.length < 2) {
            System.err.println("Usage: ParseAndFilterLog [<in>...] <out>");
            System.exit(2);
        }       

        Job job = Job.getInstance();

        // 设置输出key value分隔符
        job.getConfiguration().set("mapreduce.output.textoutputformat.separator", "@");

        job.setJarByClass(ParseAndFilterLog.class);
        job.setMapperClass(ExtractTVMsgLogMapper.class);

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);

        //job.setInputFormatClass(SequenceFileInputFormat.class);
        // 设置输入路径
        for (int i = 0; i < otherArgs.length - 1; ++i) {
            FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
        }

        // 设置输出路径
        FileOutputFormat.setOutputPath(job, new Path(
                otherArgs[otherArgs.length - 1]));
        return job.waitForCompletion(true) ? 0 : 1;
    }
    public static void main(String[] args) throws Exception {
        int ec = ToolRunner.run(new Configuration(),new ParseAndFilterLog(), args);
        System.exit(ec);
    }
}
/**
 * 
 * 解析机顶盒用户原始数据
 * <GHApp>
 * <WIC cardNum="174041665" stbNum="01050908200014994" 
 * date="2012-09-16" pageWidgetVersion="1.0">
 * <A e="23:56:45" s="23:51:45" n="133" t="2" pi="488" 
 * p="24%E5%B0%8F%E6%97%B6" sn="CCTV-13 新闻" />
 * </WIC>
 * </GHApp>
 *
 */
public class DataUtil {
   

    @SuppressWarnings("unchecked")
    public static void transData(String text,Context context) {
        try {
            //通过Jsoup解析每行数据
            Document doc = Jsoup.parse(text);

            //获取WIC标签内容,每行数据只有一个WIC标签
            Elements content = doc.getElementsByTag("WIC");

            //解析出机顶盒号
            String stbNum = content.get(0).attr("stbNum");
            if(stbNum == null||"".equals(stbNum)){
                return ;
            }

            //解析出日期
            String date = content.get(0).attr("date");

            if(date == null||"".equals(date)){
                return ;
            }

            //解析A标签
            Elements els = doc.getElementsByTag("A");

            for (Element el : els) {
                //解析结束时间
                String e = el.attr("e");
                if(e ==null||"".equals(e)){
                    break;
                }
                //解析起始时间
                String s = el.attr("s");
                if(s == null||"".equals(s)){
                    break;
                }

                //解析节目内容
                String p = el.attr("p");
                if(p == null||"".equals(p)){
                    break;
                }

                //解析频道
                String sn = el.attr("sn");

                if(sn ==null||"".equals(sn)){
                    break ;
                }

                //对节目解码
                p = URLDecoder.decode(p, "utf-8");

                //解析出统一的节目名称,比如:天龙八部(1),天龙八部(2),同属于一个节目
                int index = p.indexOf("(");

                if (index != -1) {
                    p = p.substring(0, index);
                } 

                //起始时间转换为秒
                int startS = TimeUtil.TimeToSecond(s);

                //结束时间转换为秒
                int startE = TimeUtil.TimeToSecond(e);

                if (startE < startS) {
                    startE = startE + 24 * 3600;
                }
                //每条记录的收看时长
                int duration = startE - startS;

                context.write(new Text(stbNum + "@" + date), new Text(sn + "@" + p+ "@" + s + "@" + e + "@" + duration));

            }
        } catch (Exception e) {
               e.printStackTrace();
        }
    }
}
import java.util.ArrayList;
import java.util.List;
/**
 * 
 * 时间工具
 *
 */
public class TimeUtil {
   
    /**
     * 将时间00:00:00转换为秒 int
     * 
     * @param time
     * @return
     */
    public static int TimeToSecond(String time) {
        if (time == null||time.equals("")) {
            return 0;
        }
        String[] my = time.split(":");
        int hour = Integer.parseInt(my[0]);
        int min = Integer.parseInt(my[1]);
        int sec = Integer.parseInt(my[2]);
        int totalSec = hour * 3600 + min * 60 + sec;

        return totalSec;
    }

    /**
     * 将时间00:00:00转换为秒 String
     * 
     * @param time
     * @return
     */
    public static String TimeToSecond2(String time) {
        if (time == null) {
            return "";
        }
        String[] my = time.split(":");
        int hour = Integer.parseInt(my[0]);
        int min = Integer.parseInt(my[1]);
        int sec = Integer.parseInt(my[2]);
        int totalSec = hour * 3600 + min * 60 + sec;

        return totalSec + "";
    }

    /**
     * 求两个时间的字符串差值
     * @param a_e
     * @param a_s
     * @return
     */
    public static String getDuration(String a_e, String a_s) {
        if (a_e == null || a_s == null) {
            return 0 + "";
        }
        int ae = Integer.parseInt(a_e);
        int as = Integer.parseInt(a_s);
        return (ae - as) + "";

    }

    /**
     * 将时间 00:00转换为秒 int
     * 
     * @param time
     * @return
     */
    public static int Time2ToSecond(String time) {
        if (time == null) {
            return 0;
        }
        String[] my = time.split(":");
        int hour = Integer.parseInt(my[0]);
        int min = Integer.parseInt(my[1]);
        int totalSec = hour * 3600 + min * 60;

        return totalSec;
    }

    /**
     * 提取start end 之间的分钟数
     * 
     * @param time
     * @return
     */
    public static List<String> getTimeSplit(String start, String end) {
        List<String> list = new ArrayList<String>();
        String[] s = start.split(":");
        int sh = Integer.parseInt(s[0]);
        int sm = Integer.parseInt(s[1]);
        String[] e = end.split(":");
        int eh = Integer.parseInt(e[0]);
        int em = Integer.parseInt(e[1]);
        if (eh < sh) {
            eh = 24;
        }
        if (sh == eh) {
            for (int m = sm; m <= em; m++) {
                int am = m + 1
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值