美文网首页Python数据挖掘分析
以人人都是产品经理网站3.6万篇文章为例阐述整个数据ETL和分析

以人人都是产品经理网站3.6万篇文章为例阐述整个数据ETL和分析

作者: 陈曾经 | 来源:发表于2018-04-22 08:58 被阅读56次

    1 数据获取

    主要利用python中的requests,BeautifulSoup库从人人都是产品经理网站爬取36000篇文章并存入MongoDB数据库。整个爬虫用到的库还有json——用于解析从网页获取的json格式字串,re——借助re库实现正则表达式功能,对于爬取的字串进行预处理,pymongo——与数据库建立连接,把爬取的数据存入MongoDB数据库,numpy、urllib和multiprocessing。
    整个爬虫用到的是函数式编程的思想,主体部分定义了若干爬取函数,主要有:

    1. 获取初始页HTML和解析初始页的函数,用于获取详情页的url列表
    2. 获取详情页HTML和解析详情页的函数,用于获取文章的信息,并返回一个文章信息的字典
    3. main函数,首先,调用获取详情的URL列表的函数,取得详情页url,然后,利用for循环对每个详情页url调用获取文章信息的函数,取得文章内容,最后把文章内容存入MongoDB数据库
    4. 开启多线程,提高爬虫效率,整个爬虫总共获取了36000条记录,耗时2小时30分钟,如果没有开启多线程,估计要爬一天

    整个爬虫应用的库和定义的函数详情如下表所示:

    主要用到的Python库或爬虫定义的函数 作用或实现的功能
    requests 模拟网页发出get请求取得页面HTML
    numpy 从请求头中随机获取一个头部信息
    urllib 解析字符串,构造请求头部信息
    BeautifulSoup 解析网页HTML代码,获取需要的信息,如URL、文章信息
    json 解析json格式的页面,当获取到的页面代码是json字串时,就需要用json库对其反序列化
    re 进行字符串的操作,对一些爬取下来的信息进行预处理
    pymongo 与MongoDB数据库建立连接,把请求下来的信息存入数据库
    multiprocessing 开启多线程,提高爬虫效率
    函数:get_index_page() 获取初始页HTML
    函数:parse_index_page() 解析初始页HTML,取得详情页URL
    函数:get_detail_page() 获取详情页HTML
    函数:parse_detail_page() 解析详情页HTML,获得文章内容,返回文章字典
    函数:main() 调用get_index_page()和parse_index_page()取得详情页URL列表,遍历URL列表调用get_detail_page()和parse_detail_page()取得文章内容字典,最后存入MongoDB数据库

    爬虫的源代码在product.py文件中

    2 数据清洗、转换

    2.1 从MongoDB数据库中获取数据,并转换成DataFrame格式

    # 引入pymongo和pandas
    from pymongo import MongoClient
    import pandas as pd
    

    与MongoDB建立连接,取得数据并转化成DataFrame格式

    client = MongoClient()
    db = client.product
    cursor = db.everyone_product_more.find()
    
    df = pd.DataFrame(list(cursor))
    

    大致看下数据的情况,总共有12个指标:'_id', 'article', 'author', 'comment', 'good', 'star', 'tag', 'time','title', 'total_article', 'total_watch', 'watch',其中第一列_id是MongoDB生成的每条记录的唯一标识,可以删除,其余的分别是:文章内容,文章作者,文章评论量,文章点赞量,文章收藏量,文章标签,文章发表时间,文章的标题,文章作者总共发表的文章数量,文章作者所有文章的浏览量,文章的浏览量,共11个关于文章信息的指标。目前,所有指标的数据类型均为object,后续需要转换。

    df.head()
    
    # 查看数据信息
    df.info()
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 36000 entries, 0 to 35999
    Data columns (total 12 columns):
    _id              36000 non-null object
    article          36000 non-null object
    author           36000 non-null object
    comment          36000 non-null object
    good             36000 non-null object
    star             36000 non-null object
    tag              36000 non-null object
    time             36000 non-null object
    title            36000 non-null object
    total_article    35600 non-null object
    total_watch      35600 non-null object
    watch            36000 non-null object
    dtypes: object(12)
    memory usage: 3.3+ MB
    
    # 删除_id
    df = df.drop('_id', axis=1)
    

    2.2 查看数据的缺失情况

    df.isnull().sum()
    
    article            0
    author             0
    comment            0
    good               0
    star               0
    tag                0
    time               0
    title              0
    total_article    400
    total_watch      400
    watch              0
    dtype: int64
    

    文章作者总共发表的文章数量和文章作者所有文章的浏览量分别有400条记录缺失,再查看一下缺失行的详细信息,缺失都比较严重,因此考虑把缺失行全部删除,而相对36000条数据量,删除400条缺失信息是可以接受的

    删除缺失值。再查看下数据信息,总共还有35600条记录。

    df = df.dropna()
    
    df.info()
    
    <class 'pandas.core.frame.DataFrame'>
    Int64Index: 35600 entries, 1 to 35999
    Data columns (total 11 columns):
    article          35600 non-null object
    author           35600 non-null object
    comment          35600 non-null object
    good             35600 non-null object
    star             35600 non-null object
    tag              35600 non-null object
    time             35600 non-null object
    title            35600 non-null object
    total_article    35600 non-null object
    total_watch      35600 non-null object
    watch            35600 non-null object
    dtypes: object(11)
    memory usage: 3.3+ MB
    

    2.3 依次清理各个字段

    • 文章作者字段在结尾处存在换行符,清理掉
    df['author'] = df['author'].str.strip()
    
    • 评论量字段存在[],表示列表的字符,应该是评论量为0,并且这个字段需要转化成数字类型。
    df['comment'].values
    
    array([12, 1, list([]), ..., list([]), list([]), list([])], dtype=object)
    

    利用map结合匿名函数清理comment字段

    df['comment'] = df['comment'].map(lambda e: 0 if type(e)==list else int(e))
    
    • good和star字段转化成数字类型即可
    df[['good', 'star']] = df[['good', 'star']].astype('int')
    

    用DataFrame的describe先看下三个数值型字段的统计描述,发现收藏量star的值有存在小于0的情况,不合理。

    df.describe()
    
    • 定位到star小于0的值,并改成0
    df.loc[df['star'] < 0, 'star'] = 0
    
    • time字段是时间格式,将其转换成datetime格式
    df['time'] = pd.to_datetime(df['time'], format = '%Y-%m-%d')
    
    df.info()
    
    <class 'pandas.core.frame.DataFrame'>
    Int64Index: 35600 entries, 1 to 35999
    Data columns (total 11 columns):
    article          35600 non-null object
    author           35600 non-null object
    comment          35600 non-null int64
    good             35600 non-null int32
    star             35600 non-null int32
    tag              35600 non-null object
    time             35600 non-null datetime64[ns]
    title            35600 non-null object
    total_article    35600 non-null object
    total_watch      35600 non-null object
    watch            35600 non-null object
    dtypes: datetime64[ns](1), int32(2), int64(1), object(7)
    memory usage: 3.0+ MB
    

    用正则表达式查看total_article字段带有字符串‘篇’的条数,发现每条记录都含有‘篇’字,需要去掉并转化成数字类型。

    df['total_article'].str.contains('篇').sum()
    
    35600
    
    • 利用map结合匿名函数去除total_article字段中‘篇’并转化成数字类型
    df['total_article'] = df['total_article'].map(lambda e: int(e.strip('篇')))
    

    对于total_watch的处理就麻烦些,因为它带有中文单位‘万’和m(百万),因此我们需要去除‘万’并乘以10000,去除m并乘以1000000

    带'万'字符和'm'字符的记录数分别有16516和17887条,总共34403条

    df['total_watch'].str.contains('万').sum()
    
    16516
    
    df['total_watch'].str.contains('m').sum()
    
    17887
    
    df['total_watch'].str.contains('[万m]').sum()
    
    34403
    
    • 当然,清理这个字段的方法不止一种,我觉得最简单的一种是定义一个清理函数,然后结合map方法进行清理,如下:
      但是这个方法有个问题,可能会超出notebook默认的数据处理能力,而无法进行,解决办法是需要到notebook配置文件重新设置这个值。
    def clean_total_watch(e):
        if '万' in e:
            e = float(e.strip('万'))*10000
        elif 'm' in e:
            e = float(e.strip('m'))*1000000
        else:
            e = float(e)
        return e
    
    df['total_watch'] = df['total_watch'].map(clean_total_watch)
    
    • 还有一种办法稍微复杂些,但是不会有数据处理能力的限制。利用正则表达式把数字和单位分开成两列,然后把数字和单位相乘。
    df[['author_watch', 'unit']] = df['total_watch'].str.extract('([\d.]+)(\D+)?', expand=False)
    
    df['author_watch'] = df['author_watch'].astype('float')
    
    df.loc[df['unit'] == '万', 'unit'] = 10000
    
    df.loc[df['unit'] == 'm', 'unit'] = 1000000
    
    df.loc[df['unit'].isnull(), 'unit'] = 1
    
    df['unit'] = df['unit'].astype('int')
    
    df['author_watch'] = df['author_watch'] * df['unit']
    
    • 清理浏览量watch字段,刚刚定义的清理函数就能派上用途了,因为watch字段和total_watc字段情况一样,能够应用上这个函数。
    df['watch'] = df['watch'].map(clean_total_watch)
    

    至此,整个数据的清理工作就基本完成了。再看看数据的基本情况,总共11个字段,其中6个是数值类型的数据,1个时间格式数据,还有4个字符串格式数据。

    df.info()
    
    <class 'pandas.core.frame.DataFrame'>
    Int64Index: 35600 entries, 1 to 35999
    Data columns (total 11 columns):
    article          35600 non-null object
    author           35600 non-null object
    comment          35600 non-null int64
    good             35600 non-null int32
    star             35600 non-null int32
    tag              35600 non-null object
    time             35600 non-null datetime64[ns]
    title            35600 non-null object
    total_article    35600 non-null int64
    total_watch      35600 non-null float64
    watch            35600 non-null float64
    dtypes: datetime64[ns](1), float64(2), int32(2), int64(2), object(4)
    memory usage: 3.0+ MB
    
    df.describe()
    
    • 最后,为了便于识别,'total_article'和'total_watch'字段命名为'author_total_article'和'author_total_watch',再排序字段,把清理后的干净数据导出到EXCEL和MongoDB数据库
    df[['author_total_article', 'author_total_watch']] = df[['total_article', 'total_watch']]
    
    df = df[['title', 'author', 'time', 'tag', 'watch', 'star', 'good', 'comment', 'author_total_article', 'author_total_watch', 'article']]
    
    df.to_excel('product.xlsx')
    
    for index,row in df.iterrows():
        db.everyone_product_clean.insert(row.to_dict())
    
    from pymongo import MongoClient
    import pandas as pd
    
    client = MongoClient()
    db = client.product
    cursor = db.everyone_product_clean.find()
    df = pd.DataFrame(list(cursor))
    
    df.drop('_id', inplace=True, axis=1)
    
    df = df[['title', 'author', 'time', 'tag', 'watch', 'star', 'good', 'comment', 'author_total_article', 'author_total_watch', 'article']]
    

    3 数据分析及可视化

    为了便于研究新增2个文章标题长度和文章长度的字段

    df['len_title'] = df['title'].str.len()
    df['len_article'] = df['article'].str.len()
    

    3.1 文章浏览量、收藏量、点赞量和评论量分析

    文章浏览量、收藏量、点赞量和评论量是从用户角度客观反映文章质量的指标

    df.describe()
    
    • 文章总体浏览量
      文章的浏览量的平均值达到了1.15万,还是相当高的。
      浏览量最高的一篇到达了二百五十万,是在2015年4月发表的一篇关于产品原型的文章,文章的标题为22个字,文章文字部分内容只有252个字,大部分内容是原型设计的图片,看来直观的图片的力量要大于文字的说服力。该文章的收藏量为2089,点赞量为1878,评论量为31。该文章的作者GaraC总共发表了16篇文章,所有文章的浏览量为3700000。
    df.loc[df['watch'] == 2500000]
    
    # 浏览量最高的文章
    df.loc[df['watch'] == 2500000]['article'].values
    
    array([ '一直想找机会写写关于原型的事情,由于原型作为关键的需求文档,非常需要进行保密,所以未成文。最近刚好有一个项目被撤了,之前做的原型藏着也浪费,或许可以偷偷拿出来分享下。这是某项目1.06版原型,因此针对上一个版本原型修改了哪些内容,我在第一页做了说明我用来说明产品页面结构和主要功能流程首先是结构:然后是流程关于全局设计、交互的统一说明这是界面这是模块功能的流程这是内容与操作说明在导航部分我用字母、数字分别说明了所在模块、页面编号、页面层级等信息。只是一种便于我自己寻找以及内部沟通的形式,看个意思就好。'], dtype=object)
    
    # 作者GaraC的文章总量
    (df['author'] == 'GaraC').sum()
    
    16
    
    # 作者GaraC所有文章的总浏览量
    df['author_total_watch'].groupby(df['author']).sum()['GaraC']
    
    59200000.0
    
    • 文章收藏量
      文章收藏量的平均值为61,标准差较大达到了124,75分位大小为66,证明不同文章间收藏量差距很大,属于长尾分布。
      最大的收藏量达到了4213,是发表于2015年9月的一篇关于产品文档的文章,文章标题长16字,文章长度为1722,同样该文章也用了大量的图片说明。该文章的总浏览量458000,收藏量4213,点赞量2160,评论量30。该文章的作者臻龙总共发表了5篇文章,作者所有文章的浏览量为691000。
    df.loc[df['star'] == df['star'].max()]
    
    (df['author'] == '臻龙').sum()
    
    5
    
    df['author_total_watch'].groupby(df['author']).sum()['臻龙']
    
    3455000.0
    
    • 文章点赞量
      文章点赞量的平均值为15.6,标准差为37,75分位值为16接近平均值,差距也比较大,也属于长尾型分布。
      点赞量最多的一篇文章达到了2160,和收藏量最多的文章是同一个作者的同一篇文章。
    df.loc[df['good'] == df['good'].max()]
    
    • 文章评论量
      文章评论量的平均值为3,还是相当小的,50分位数评论量为1,也就是说有一半以上的文章都没有评论。说明可能存在的两个问题:(1)该网站的注册用户表较少。(2)互联网文章很难获得评论。
      评论量最高的一篇文章达到99,是2015年10月份发表的一篇关于产品经理求职的文章,文章标题长度12字,文章内容1576字,文章浏览量为27000,作者只发表了一篇文章。
    df.loc[df['comment'] == df['comment'].max()]
    
    (df['author'] == '兔子爱榴莲').sum()
    
    1
    

    从4个指标的箱线图来看,尾巴都拉的很长,把“箱子”拉的很平几乎成了一条线,说明数据分布很不均匀

    # 引入作图函数
    %matplotlib inline
    from matplotlib import pyplot as plt
    from matplotlib.ticker import MultipleLocator, FormatStrFormatter
    plt.style.use('ggplot')
    plt.rcParams['font.family'] = ['sans-serif']
    plt.rcParams['font.sans-serif'] = 'SimHei'
    
    fig, axes = plt.subplots(2, 2, figsize=(9,7), dpi=350)
    df.boxplot(ax=axes[0,0], column='watch')
    df.boxplot(ax=axes[0,1], column='star')
    df.boxplot(ax=axes[1,0], column='good')
    df.boxplot(ax=axes[1,1], column='comment')
    
    <matplotlib.axes._subplots.AxesSubplot at 0x23971a9d9b0>
    

    再来看看文章浏览量、收藏量、点赞量和评论量分别排前10的文章都在讲什么?
    从各个前10的文章中发现,比较多是关于Axure,求职,面试,干货教程分享的文章。
    而这些文章中,前10浏览量的标题平均字数为21.9,文章平均字数为1650.3,前10收藏量的标题平均字数为18.4,文章平均字数为2028.4,前10点赞量的标题平均字数为20.2,文章平均字数为1646.5,前10评论量的标题平均字数为20.4,文章平均字数为4048.6,各个前10文章标题字数平均值为20.225,文章平均字数为2343.45。看来一篇文章要想获得网友的认可,一个好的标题很重要,毕竟对互联网来说标题党还是有很大优势的,其次,文章的字数不一定要太多,但是需要配以合适的图表说明才能更加吸引网友。

    • 浏览量前10文章
    df.sort_values(by='watch',ascending=False)[['title', 'watch', 'star', 'good', 'comment', 'len_title', 'len_article', 'author_total_article', 'author_total_watch']].iloc[:10]
    
    • 收藏量前10文章
    df.sort_values(by='star',ascending=False)[['title', 'watch', 'star', 'good', 'comment', 'len_title', 'len_article', 'author_total_article', 'author_total_watch']].iloc[:10]
    
    • 点赞量前10文章
    df.sort_values(by='good',ascending=False)[['title', 'watch', 'star', 'good', 'comment', 'len_title', 'len_article', 'author_total_article', 'author_total_watch']].iloc[:10]
    
    • 评论量前10文章
    df.sort_values(by='comment',ascending=False)[['title', 'watch', 'star', 'good', 'comment', 'len_title', 'len_article', 'author_total_article', 'author_total_watch']].iloc[:10]
    

    3.2 文章作者分析

    • 建立一个以作者为索引的数据透视表
      可以看到全部35600篇文章是3460个作者写的,平均每个作者将近写12篇文章,75分位数为5,而且有1478位作者只发表了1篇文章,说明大部分文章都是少数几个作者写的。文章写作数量前10的作者,发表文章数量都是500篇以上,最多的作者老曹居然发表了4907篇文章,绝对是职业写手啊。
    df.pivot_table(index='author').sort_values(by='author_total_article', ascending=False).head(10)
    
    df.pivot_table(index='author').describe()
    
    (df.pivot_table(index='author')['author_total_article'] == 1).sum()
    
    1478
    

    3.3 文章、标题长度和文章浏览量、收藏量、点赞量及评论量之间的关系分析

    fig, axes = plt.subplots(1, 2, figsize=(7,3),dpi=350)
    axes[0].set_title('文章长度')
    axes[0].hist(df['len_article'], normed=True)
    
    (array([  2.26347723e-04,   4.03698508e-05,   5.17778867e-06,
              1.15232686e-06,   2.76558445e-07,   1.07550506e-07,
              2.30465371e-08,   1.53643581e-08,   7.68217903e-09,
              7.68217903e-09]),
     array([     0. ,   3656.5,   7313. ,  10969.5,  14626. ,  18282.5,
             21939. ,  25595.5,  29252. ,  32908.5,  36565. ]),
     <a list of 10 Patch objects>)
    
    axes[1].set_title('标题长度',color='black')
    axes[1].hist(df['len_title'],color='green')
    
    (array([  1.78700000e+03,   1.02290000e+04,   1.36330000e+04,
              7.55800000e+03,   1.75300000e+03,   4.70000000e+02,
              1.25000000e+02,   3.00000000e+01,   9.00000000e+00,
              6.00000000e+00]),
     array([  4. ,  10.3,  16.6,  22.9,  29.2,  35.5,  41.8,  48.1,  54.4,
             60.7,  67. ]),
     <a list of 10 Patch objects>)
    
    fig
    

    大部分文章长度在3600字以下,标题在17字以下

    fig, axes = plt.subplots(2, 2, figsize=(13,9), dpi=200, sharex=True, sharey=True)
    axes[0,0].scatter(df['len_title'], df['len_article']/1000, df['comment'])
    axes[0,1].scatter(df['len_title'], df['len_article']/1000, df['good'])
    axes[1,0].scatter(df['len_title'], df['len_article']/1000, df['watch']/1000)
    axes[1,1].scatter(df['len_title'], df['len_article']/1000, df['star'])
    plt.subplots_adjust(wspace=0,hspace=0)
    

    3.4 时间序列数据分析

    • 各指标按年和季度划分的平均值
    df.pivot_table(index=[df['time'].dt.year, df['time'].dt.quarter])
    
    • 发文数量按年和季度划分的平均值
    df.pivot_table(index=[df['time'].dt.year, df['time'].dt.quarter], values='title', aggfunc='count').unstack()
    
    • 发文量按年和月份划分的平均值
    df.pivot_table(index=[df['time'].dt.year, df['time'].dt.month], values='title', aggfunc='count').unstack()
    
    • 发文量按季度的增长趋势
    f,a = plt.subplots(figsize=(7,5), dpi=400)
    df.pivot_table(index=[df['time'].dt.year, df['time'].dt.quarter], values='title', aggfunc='count').iloc[1:-1,].plot(ax=a)
    
    <matplotlib.axes._subplots.AxesSubplot at 0x23970c17da0>
    

    3.5 标签分析

    from collections import Counter
    
    tag_list = []
    for tag in df['tag']:
        tag_list.extend(tag)
    
    tag_dic = Counter(tag_list)
    

    35600篇文章总共有17185个标签

    len(tag_dic)
    
    17185
    
    • 排名前10的标签
    tag_dic[:10]
    
    [('初级', 1902),
     ('产品经理', 1867),
     ('3年', 1535),
     ('案例分析', 1410),
     ('2年', 1340),
     ('中级', 1240),
     ('产品设计', 1023),
     ('用户体验', 976),
     ('创业', 938),
     ('微信', 858)]
    
    from wordcloud import WordCloud
    
    wordcloud = WordCloud(font_path='images/NotoSansHans-Regular.otf', width=700, height=500, background_color='white')
    wordcloud.generate_from_frequencies(tag_dic)
    
    plt.imshow(wordcloud)
    plt.axis('off')
    plt.show()
    

    相关文章

      网友评论

        本文标题:以人人都是产品经理网站3.6万篇文章为例阐述整个数据ETL和分析

        本文链接:https://www.haomeiwen.com/subject/imcqlftx.html