Python3+Selenium爬虫实战:微博粉丝榜水分大揭秘

原文链接:http://www.jianshu.com/p/8632f65ee310

高能预警!分析到最后,我不得不感慨这个世界太真实了!

文中有大量代码,注重阅读体验的请在PC站打开!或者直接去我的个人博客(www.data-insights.cn)阅读!

一、微博粉丝榜:一潭深水

微博粉丝榜争夺战由来已久,每个明星在榜单上的位置似乎就象征着他(她)在粉丝心中、在娱乐圈中的地位。

但众所周知,微博粉丝榜是有着极大水分的。微博刷榜、刷关注等早已形成一套产业链。发展源自需求,有人提供服务就说明有人需要这种服务,明星们刷粉丝刷榜是可以将其转化为利益的,他们有充分的理由和动力去做这件事情。

但是我们相信,不可能所有明星都会通过这种手段来伪装自己的强大。那么,现在微博粉丝榜前列的大明星们,到底谁的粉丝最真实,谁的粉丝水分最大?

14318844-54e657b8024a8d27.jpg
image

14318844-16570726653ad6a2.jpg
image

我们不知道幕后真相,也没有人提供爆料,有的只是微博上公开的信息,那我们如何才能拨开迷雾,从这些公开的信息中抽丝剥茧,找到我们想要的答案呢?作为一个数据工作者,自然是要从数据入手了!

预警:前方会有不少技术相关的内容,对技术不感兴趣的同学可以跳着阅读,不会影响阅读体验!

二、准备工作

首先我们先观察一下我们可以获取到的信息。我们以人民日报为例,先在微博搜索“人民日报”。在结果页中,我们看到了粉丝数,也能看到热门微博的转发、评论、点赞数。

14318844-7dfaf896db789c9e.jpg
image

然后我们点进去人民日报官博的首页,可以看到它的更多微博,每条微博的转发、评论和点赞的数据都是可用的。

14318844-da43b0ec38b7f11e.jpg
image

接下来我们点进去粉丝页(从搜索页中点进去),现在微博仅开放最新的100个粉丝给我们看。在粉丝页,我们可以看到每个粉丝的一部分属性,比如关注数、粉丝数和微博数。

14318844-a37856501b7e1fc2.jpg
image

我们梳理下现在确认可以找到的信息:

  1. 微博表现:包括评论、转发、点赞
  2. 粉丝总数
  3. 每个粉丝的站内表现:关注数、粉丝数、微博数、性别、地址、关注来源

那么我们如何从这些数据里挖掘出谁的粉丝水分最大,谁的粉丝最真实呢?

我们定义几个指标:

  1. 微博人均评论/转发/点赞贡献量:用最近数条微博的平均评论数/转发数/点赞数除以粉丝量,得出一个平均每个粉丝对于微博热度的贡献量,这一指标可以衡量粉丝对于明星的支持度、忠诚度,如果是为了刷量买的粉丝,那这些粉丝大概率是不会有太多活动的。当然,可以刷粉丝量就可以刷转发、刷点赞、刷评论,但是这些刷出来的内容我们暂时不太容易去剔除。
  2. 合格粉比例:如何定义合格粉丝呢?我观察到有很多用户仅有一个粉丝(微博小助手,新用户默认),微博量为0,用户名的格式为“用户xxxxxxx”,也就是说,这些完全是新用户,且无法确定是否是僵尸用户。那我们本着宁可错杀绝不放过的原则,将其归类为不合格粉丝,剩下的就是合格粉丝。由于微博仅展示最近的100个粉丝,这个量级完全不够我们分析使用。所以我们采用滚动抓取去重的方式,来获得更多的粉丝数据。假设我们抓取了1000个粉丝样本,其中800个合格,那合格粉丝比例就是80%。
  3. 主动粉比例:我们观察关注来源,发现有以下几种:微博搜索、微博找人、手机型号、微博推荐。其中,微博推荐量级最为庞大,微博站内有诸多推荐位,这些位置应该是明码标价可以购买的,同时新用户注册完之后会有一些名人推荐,这一部分可能有冷启动的作用,同时也为明星增加了曝光。这些应该都属于微博推荐;微博搜索和微博找人很好理解,应该是用户主动找到这位明星并关注的,一般是真粉丝或者由热门事件带来;来源为手机型号的暂且不明,不清楚是不是微博与厂商的合作带来的App外流量。那么我们就将来自微博搜索和微博找人的粉丝定义为主动粉,主动粉比合格粉更能代表这一明星粉丝中忠诚粉的比例情况。

三、数据收集

数据收集是数据分析的关键步骤,毕竟巧妇难为无米之炊。那么我们如何获取这些数据呢?一条微博一条微博地点击然后手动录入?这不是我们技术派的风格。

我们准备使用Python3Selenium,通过页面抓取并解析来完成这一任务。这一部分的内容对非技术类同学来说简直是又臭又长,不感兴趣的可以直接跳过看分析部分。

1. 自动登录微博

现在想要绕过微博的反爬虫策略并成功抓取内容,必须要模拟登录。因此我们先看一下如何模拟浏览器行为,自动登录微博。

我们先打开微博首页,右键检查,观察页面元素。在我自己检验的过程中,使用无头(headless)模式抓取时,图中2标记的部分会有一定的失败概率;因此我们通过单击标记1所示的登录按钮来登录。

观察发现这是在一个<a>节点中,它有一些属性,比如class="S_txt1"node-type="loginBtn"等。那我们可以通过XPath的方式来定位,定位到之后要模拟浏览器的单击行为。

14318844-94a2a5a10df8642c.jpg
image

单击之后出现一个登录模块的浮层,在页面源码中我们发现了这么一个class="Bv6_layer"<div>节点,正是这个登录模块对应的源码。这个模块里有两个<input>节点,分别对应着用户名和密码的输入框,他们的name属性的取值分别为"username""password"。输入完用户名和密码,我们需要定位到登录按钮的元素并执行点击行为。

14318844-02f947191ec9c9f8.jpg
image

下面看下代码:

# ./../.venv/bin/python
# -*- coding:utf-8 -*-
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
import pandas as pd
import time


class WeiboFansCrawler:
    def __init__(self):
        options = webdriver.ChromeOptions()
        options.add_argument('headless')
        self.driver = webdriver.Chrome(options=options)
        self.wait = WebDriverWait(self.driver, 10)
        self.fan_data = []
        self.home_data = []
        self.fan_cnt = {}
        try:
            self.stars = eval(open('star_id.txt', 'r').read())
        except Exception as e:
            self.stars = {}
            print(e)

    def login(self):
        """
        登录微博
        :param driver: selenium webdriver
        :return: Nothing
        """
        self.driver.get('https://www.weibo.com')
        # 显式获取用户名输入框并输入用户名
        # print(self.driver.page_source)
        login_entrance = self.wait.until(lambda s: s.find_element_by_xpath('//li/a[@class="S_txt1" and @target="_top" and @node-type="loginBtn"]'))
        login_entrance.click()

        # 用户名
        username = self.wait.until(lambda s: s.find_element_by_xpath('//div[@class="Bv6_layer "]//input[@name="username"]'))
        username.send_keys('751718003@qq.com')

        # 获取密码框并输入密码
        password = self.wait.until(lambda s: s.find_element_by_xpath('//div[@class="Bv6_layer "]//input[@name="password"]'))
        password.send_keys('password')

        # 单击登录
        login_button = self.wait.until(lambda s: s.find_element_by_xpath('//div[@class="Bv6_layer "]//a[@node-type="submitBtn"]'))
        login_button.click()
        time.sleep(5)

__init__(self)函数中,我们定义了初始化的一些信息:比如我们启动了一个无头模式的chromedriver,设置了显式等待的方法,初始化了粉丝数据、主页微博数据、粉丝总数以及明星页面ID数据。

显示等待对应的方法是selenium.webdriver.support.ui.WebDriverWait,它用来应对页面加载较慢的情况。比如说你的网速不太好,页面没有加载完成,那程序很可能会因为定位不到元素而报错。但是使用time.sleep()的话,在网络条件比较好的情况下又会很浪费时间,因此使用显示等待就可以帮助我们在设置的超时范围内一直等待页面加载我们需要定位的元素,经济而实用。

明星与对应的页面ID的关系是绑定的,也就是说我们可以抓取一次并缓存到本地,之后就可以直接从本地读取对应关系。

接下来看登录模块,我们先控制浏览器打开微博首页,然后通过find_element_by_xpath方法,使用XPath表达式来定位到用户名输入框、密码输入框、登录按钮对应的元素,使用send_keys()方法将用户名和密码输入,使用click()方法单击登录提交。然后我们使用time.sleep(5)来休息五秒,这个地方不一定有必要,但是为了避免被当作爬虫封了IP,我们稳妥一点比较好。

需要注意的是,这里的用户名和密码要改成自己的用户名和密码才能使用。

2. 获取明星对应的ID

这个ID其实不是用户ID,但是是url地址中的一个参数,由用户ID加一个前缀构成。我们不清楚他的构成规则,但我们可以直接抓取到它。

首先,我们在顶部搜索框输入明星的姓名或者昵称,然后单击搜索按钮。

14318844-d475937fc5f4fe8e.jpg
image

然后,单击粉丝数对应的链接,进入粉丝页。

14318844-843fa9999accbee6.jpg
image

这时,会打开一个新的标签页,所以我们需要控制浏览器切换标签。然后我们单击第2页,观察url格式:

14318844-a2f74281042ebdc8.jpg
image

https://weibo.com/p/1003061192329374/follow?relate=fans&page=2#Pl_Official_HisRelation__59

通过观察,我们发现,follow?relate=fans&page=x是必要的参数,1003061192329374就是我们需要的ID。(后来我发现直接使用用户ID也就是默认粉丝页的ID也是可以的,不过这个不影响大局。)

获取ID之后,我们将其以字典格式保存到self.stars,并缓存到本地。看下这部分的代码:

    def get_page_id(self, search_word):
        # 显式等待获取搜索框并输入搜索词
        search_box = WebDriverWait(self.driver, 10).\
            until(lambda s: s.find_element_by_id('plc_top').find_element_by_tag_name('input'))
        search_box.send_keys(search_word)

        # 点击搜索
        self.driver.find_element_by_id('plc_top').\
            find_element_by_class_name('gn_search_v2').find_element_by_tag_name('a').click()

        # 点击进入粉丝页
        fans_page = WebDriverWait(self.driver, 10)\
            .until(lambda s: s.find_element_by_class_name('card-user-b')
                   .find_elements_by_tag_name('p')[2].find_elements_by_tag_name('a')[1])
        fans_page.click()

        self.driver.switch_to.window(self.driver.window_handles[1])
        fans_page2 = WebDriverWait(self.driver, 10)\
            .until(lambda s: s.find_element_by_class_name('W_pages').find_element_by_link_text('2'))
        fans_page2.click()
        time.sleep(1)

        ID = self.driver.current_url.split('/')[-2]
        self.driver.close()
        self.driver.switch_to.window(self.driver.window_handles[0])
        self.driver.back()
        time.sleep(0.5)

        self.stars[search_word] = ID
        print(self.stars)

3. 获取粉丝数据

我们刚才已经知道了粉丝页面url的格式,所以我们可以直接拼接并访问。

观察源码,我们发现粉丝昵称、用户ID、性别可以在一个<li>标签的action-data属性中找到,关注数、粉丝数、微博数也都能在下边轻易找到。那我们要做的就是把他们解析并保存下来。

14318844-976019e25d30aabf.jpg
image

看下代码:

    def get_fans_data(self):
        """
        获取粉丝数据
        :param driver:
        :param ID:
        :return:
        """
        for star_name, ID in self.stars.items():
            for cnt in range(1, 6):
                try:
                    url = 'https://weibo.com/p/{0}/follow?relate=fans&page={1}'.format(ID, cnt)
                    self.driver.get(url)

                    # 昵称/性别
                    name_elements = self.wait.until(lambda s: s.find_elements_by_class_name('info_name'))
                    names = [i.text for i in name_elements]
                    genders = [i.find_element_by_tag_name('i').get_attribute('class') for i in name_elements]
                    genders = [i.split(' ')[1].split('_')[1] for i in genders]

                    name_elements = self.wait.until(lambda s: s.find_elements_by_class_name('follow_item'))
                    ng_data = [i.get_attribute('action-data') for i in name_elements]
                    fan_ids = [i.split('&')[0].split('=')[1] for i in ng_data]
                    fan_names = [i.split('&')[1].split('=')[1] for i in ng_data]
                    fan_genders = [i.split('&')[2].split('=')[1] for i in ng_data]

                    # 关注/粉丝/微博
                    stat_elements = self.driver.find_elements_by_class_name('info_connect')
                    follows = [i.find_elements_by_tag_name('a')[0].text for i in stat_elements]
                    fans = [i.find_elements_by_tag_name('a')[1].text for i in stat_elements]
                    posts = [i.find_elements_by_tag_name('a')[2].text for i in stat_elements]

                    # 地址
                    addr_elements = self.driver.find_elements_by_class_name('info_add')
                    addrs = [i.find_element_by_tag_name('span').text for i in addr_elements]

                    # 订阅来源
                    from_elements = self.driver.find_elements_by_class_name('info_from')
                    froms = [i.find_element_by_tag_name('a').text for i in from_elements]

                    # 明星
                    stars = [star_name] * len(names)

                    info_list = list(zip(stars, fan_names, fan_ids, fan_genders, follows, fans, posts, addrs, froms))
                    self.fan_data.extend(info_list)
                    print(info_list)
                    time.sleep(1)

                except Exception as e:
                    print(e)
                    continue

这里我们加了一个try...except...模块,这是因为页面中某些字段会有缺失,会出现定位不到元素的情况,这种页面我们直接跳过即可。这样,我们就可以将粉丝数据保存到self.fan_data中了。

我们还需要将数据缓存到本地进行进一步分析(也可以考虑MySQL之类的数据库),所以我们定义一个函数来实现缓存功能:

    def dump_fans_data(self):
        df = pd.DataFrame(self.fan_data)
        df.columns = ['star_name', 'nick', 'id', 'gender', 'follow_cnt', 'fan_cnt', 'post_cnt', 'addr', 'from']
        df.to_csv('./fans.csv', mode='a', index=False, header=False)

4. 获取主页微博表现数据

我们还需要抓取他们主页展示的那些微博的表现情况。观察主页url的格式,我们可以发现可以跟刚才一样用ID去拼接。

url = 'https://weibo.com/p/{0}?is_all=1'.format(ID)

微博的具体内容我们不考虑,我们只考虑每条微博的表现数据。可以发现,这个数据隐藏得非常深,但还是难不倒我们。我们依次定位到转发、评论和点赞所在的元素并将文本解析出来。

14318844-3682319b804707eb.jpg
image

看下代码:

    def get_home_data(self):
        for star_name, ID in self.stars.items():
            url = 'https://weibo.com/p/{0}?is_all=1'.format(ID)
            self.driver.get(url)

            # 转发数据
            forward_elements = self.wait.until(lambda s: s.find_elements_by_xpath('//span[@node-type="forward_btn_text"]/span//em[2]'))
            forwards = [i.text for i in forward_elements]

            # 评论数据
            comment_elements = self.wait.until(lambda s: s.find_elements_by_xpath('//span[@node-type="comment_btn_text"]/span//em[2]'))
            comments = [i.text for i in comment_elements]

            # 点赞数据
            like_elements = self.wait.until(lambda s: s.find_elements_by_xpath('//span[@node-type="like_status"]//em[2]'))
            likes = [i.text for i in like_elements]

            # 明星
            stars = [star_name] * len(forwards)
            total = [self.fan_cnt[star_name]] * len(forward_elements)

            # index
            indexes = list(range(1, len(forwards) + 1))

            perform_list = list(zip(stars, total, indexes, forwards, comments, likes))
            self.home_data.extend(perform_list)
            print(perform_list)
            time.sleep(1)

接下来呢,我们同样要将主页数据缓存到本地。

    def dump_home_data(self):
        df = pd.DataFrame(self.home_data)
        df.columns = ['star_name', 'total_fans', 'post_index', 'forward', 'comment', 'like']
        df.to_csv('./home.csv', mode='w', index=False, header=True)

5. 执行抓取

我们挑选一些比较热门的明星来看下他们的粉丝情况,这里选了排行榜的前22位(2018-10-15数据),再加上陈羽凡这几天特别火,所以我们把他也加进来。

在抓取粉丝页时,由于每次抓取最多只能抓到100个用户,所以我们每次抓取后暂停100秒,继续下一轮抓取,做20个循环,这样就能积累较多的粉丝。(这些人太火了,所以粉丝增长非常快,100秒不够200秒也足够了,我为了效率每次只休息100秒)

if __name__ == '__main__':
    browser = WeiboFansCrawler()
    browser.login()

    # 明星列表
    star_lists = ['谢娜', '何炅', 'Angelababy', '杨幂', '陈坤', '赵薇', '姚晨',
             '林心如', '邓超', '郭德纲', '林志颖', '张小娴', '赵丽颖', '范冰冰',
             '贾乃亮', '唐嫣', '胡歌', '陈乔恩', '王力宏', '黄晓明', '文章同學',
             '刘亦菲', '陈羽凡']

    # 若本地缓存中没有,则在线获取ID
    if len(browser.stars) == 0:
        for star in star_lists:
            browser.get_page_id(star)

    # ID缓存到本地
    with open('star_id.txt', 'w') as f:
        f.write(str(browser.stars))

    # 获取主页数据
    browser.get_fan_cnt()
    browser.get_home_data()
    browser.dump_home_data()

    # 获取并缓存数据到本地
    loop = 0
    while True:
        browser.get_fans_data()
        browser.dump_fans_data()
        loop += 1
        if loop > 20:
            break
        time.sleep(100)
    
    print(browser.stars)
    print(browser.data)

大功告成!鼓掌!撒花!

四、数据分析

数据到手,接下来我们就要开始从数据中发掘信息了!

import pandas as pd

df = pd.read_csv('fans.csv')
df = df.drop_duplicates(['star_name', 'id'], keep='last')
df['addr'] = df['addr'].map(lambda x: x.split(' ')[0])
df.head()
14318844-be4d444ac970fc87.jpg
image

先展示下粉丝数据,看起来一切正常。这里我们需要对数据执行一下清洗:

  1. 去重:这是因为我们是滚动抓取的,用户可能有重复。但是同一个用户作为两个明星的粉丝的情况我们认为是正常的,所以以明星、粉丝用户ID两个key一起来去重。
  2. 地址:省级的地址就够用了,事实上这样还是太过分散,我们不会太关注地址。
df2 = pd.read_csv('./home.csv')
df2['forward'] = df2['forward'].map(lambda x: 0 if x =="转发" else x)
df2['comment'] = df2['comment'].map(lambda x: 0 if x =="评论" else x)
df2['like'] = df2['like'].map(lambda x: 0 if x == "赞" else x)
df2[['total_fans', 'post_index', 'forward', 'comment', 'like']] = df2[['total_fans', 'post_index', 'forward', 'comment', 'like']].applymap(int)
df2.head()
14318844-c0e50f4407c53afe.jpg
image

然后看下主页微博表现数据,这里没有展示出来的情况是,当一条微博转发、评论或者点赞的数字为0时,该数据对应着一个字符串"转发"、"评论"或者"赞",我们要将它们清洗为0,并全部转化为int格式。

1. 谁的粉丝增长最快?

接下来我们看下抓取到的每个明星的粉丝样本量是多少:

df_stats1 = df.groupby('star_name')['id'].count()
df_stats1.sort_values(ascending=False).plot(kind='bar', figsize=(18,9), fontsize=16)
14318844-ac1f9c329d3c81af.jpg
image

这个肯定不能代表总的粉丝量,但是这个数据能在一定程度上代表在我抓取数据的那段时间里,每个明星的粉丝增长量。为什么强调“一定程度上”呢,因为有的明星的粉丝的增长速度远远超过了我的抓取速度,所以这里出现了天花板效应。1900以上的明星在这段时间里实际增长的粉丝量都可能早已超过了2000,但我们前边提到过,我们一共20次循环,每次循环最多抓到100个,最后这两千个还要放一起去重。

那么这个榜单说明了什么呢?首先,虽然大家都是大腕儿,但是在粉丝增长速度上还是可以划分出梯队的,仅以这一段时间来说,林志颖、郭德纲、范冰冰、赵薇、刘亦菲、陈乔恩、黄晓明夫妇、胡歌明显是在第一梯队的。而赵丽颖、张小娴、贾乃亮、文章同学的粉丝增长速度则稍缓。陈羽凡总粉丝仅有500万,但是粉丝增长速度如此之快,明显不是一个正常状态,这种情况就是受到热点事件的影响了。

2. 谁的合格粉丝增长最快?

回顾之前的定义:合格粉丝是指满足粉丝数大于1、微博数大于0和用户名非默认三个条件任意一个的粉丝。

df['auto_name'] = df['nick'].map(lambda x: 1 if x.startswith('用户') else 0)
df_stats2 = df.query('(fan_cnt>1) or (post_cnt>0) or (auto_name==1)').groupby('star_name')['id'].count()
df_stats2.sort_values(ascending=False).plot(kind='bar', figsize=(18,9), fontsize=16)
14318844-eb56d2552bf131b8.jpg
image

林志颖郭德纲位置不变,陈羽凡直接冲到榜单第四。单纯看这一指标意义不大,但是如果跟前边的指标结合起来,看一个合格粉丝的比例,就有意义了。

3. 谁的粉丝合格比例最高?

这一指标已经有些尖锐了,他已经在某种程度上反映了不同明星的粉丝在微博上的活跃程度。活跃的用户各有各的活跃,而不活跃的用户就那么几种:一种是只看不说,也没有社交圈子没人关注;一种是新用户尚未开始发力;还有一种,就是僵尸了,具体是人造僵尸还是程序僵尸,我们不得而知。

df_tmp = pd.concat([df_stats1, df_stats2], axis=1)
df_tmp.columns = ['粉丝样本数', '合格粉丝数']
df_tmp['合格比例'] = df_tmp['合格粉丝数'] / df_tmp['粉丝样本数']
# df_tmp['合格比例'] = df_tmp['合格比例'].map(lambda x: '{0:.2f}%'.format(x*100))
df_tmp.index.name = '明星姓名'
df_tmp.sort_values('合格比例', ascending=False)['合格比例'].plot(kind='bar', figsize=(18,9), fontsize=16, grid=True)
14318844-858b3a7df599fc3e.jpg
image
14318844-346bfb92c0a5d88e.jpg
image

张小娴、文章的粉丝增长速度不算快,但是基本上都符合我们设定的合格标准。至于陈羽凡和贾乃亮……我只能说这个时间太过真实。全世界人民都爱吃瓜,这不是仅停留在嘴上说说而已的。吸毒队、出轨队每每有了新队员,当事人、相关人都会受到莫大的关注。仅有500万粉丝的陈羽凡,短时间内的粉丝增长速度直逼甚至超过近亿粉丝的大腕儿们。

张小娴的粉丝如此合格让我感到很欣慰,因为合格比例从某种程度上也可以说是非僵尸比例,这说明即使炒作已经遍及各个领域,但是至少有些文字工作者还是有底线的,不会单纯地为了数据而去做刷榜的事情。当然,这并不能排除她也有团队在做买粉等商业运作,只是我觉得这种商业运作是非常正常的。就像我们打广告广而告之一样,这是一种提高曝光度、提升身价的方法,并不算在数据上作假,我觉得无可厚非。

4. 谁的粉丝更活跃?

我们增加一个定义,那就是粉丝数大于1或者微博数大于0的,我们勉强算作活跃粉丝。至于仅进行了改名操作的用户,我们暂且算作不活跃。

df_stats20 = df.query('(fan_cnt>1) or (post_cnt>0)').groupby('star_name')['id'].count()
df_tmp = pd.concat([df_stats1, df_stats20], axis=1)
df_tmp.columns = ['粉丝样本数', '活跃粉丝数']
df_tmp['活跃比例'] = df_tmp['活跃粉丝数'] / df_tmp['粉丝样本数']
# df_tmp['活跃比例'] = df_tmp['活跃比例'].map(lambda x: '{0:.2f}%'.format(x*100))
df_tmp.index.name = '明星姓名'
df_tmp.sort_values('活跃比例', ascending=False)['活跃比例'].plot(kind='bar', figsize=(18,9), fontsize=16, grid=True)
14318844-8e55f052af3b5b0d.jpg
image
14318844-e1418482b151edec.jpg
image

榜单顺序与合格榜差异不算大,但是活跃比例就有些触目惊心了,最低的粉丝活跃比例能在20%以下。当然,这个比例并不能代表每个明星全部粉丝的表现。事实上,这些粉丝中,相当大的比例应该都是新用户,对于新用户来说,不活跃再正常不过了。

5. 谁的粉丝是主动关注的?

前边提到,通过搜索和找人功能关注某个明星的,我们称之为主动粉。主动粉一方面体现了该明星忠实粉丝的比例,另一方面也体现了该明星近期是否火热。

df['froms'] = df['from'].map(lambda x: x if x in ['微博推荐', '微博搜索', '微博找人'] else '其他')
df_stats3 = df.groupby(['star_name', 'froms'])['id'].count().unstack().fillna(0).applymap(int)
df_stats1.name = '粉丝样本数'
df_tmp = pd.concat([df_stats1, df_stats3], axis=1)
df_tmp['主动粉比例'] = df_tmp[['微博找人', '微博搜索']].sum(axis=1) / df_tmp['粉丝样本数']
df_tmp.index.name = '明星姓名'
df_tmp = df_tmp.sort_values('主动粉比例', ascending=False).reset_index()
df_tmp['主动粉比例'] = df_tmp['主动粉比例'].map(lambda x: '{0:.2f}%'.format(x*100))
df_tmp
14318844-c54639840da9dc9a.jpg
image
14318844-0c1420d9e22c2e61.jpg
image

嗯……同样是大腕儿,差距怎么就这么大呢?

文章同学的主动粉比例高达73%,看来他的粉丝是真爱啊。抛却出轨的问题无法接受外,我也挺喜欢文章的作品风格的。榜单前三的另外两位不做多说,出轨队(及相关人员)、吸毒队向来是吃瓜群众最关注的队伍。张小娴在榜单第四,看来知识输出带来的粉丝更真实、更主动。

至于榜单靠后的这些,也不是说他们的粉丝基础比较差。我们都知道,新用户注册完成之后,微博会罗列一大堆名人明星、媒体号等,给新用户选择去关注哪些。主动粉比例较低,说明这种通过推荐的方式得到的新增粉丝比例较大。也许他们的主动粉也不少,只是近期团队花了较多的精力在购买推荐位上(也可能是微博主动提供的,我不得而知)。

6. 谁的女粉比例最高?

我们都知道,微博是女同学的大本营。前一段王校长抽奖,让我们知道了微博上活跃的用户有112/113是女生。那么这些名人明星们,他们的新增粉丝中的男女比例是怎么样的呢?

df_stats4 = df.groupby(['star_name', 'gender'])['id'].count().unstack().fillna(0).applymap(int)
df_stats4.columns = ['女粉数', '男粉数']
df_tmp = pd.concat([df_stats1, df_stats4], axis=1)
df_tmp['女粉比例'] = df_tmp['女粉数'] / df_tmp['粉丝样本数']
df_tmp.index.name = '明星姓名'
df_tmp = df_tmp.sort_values('女粉比例', ascending=False).reset_index()
df_tmp['女粉比例'] = df_tmp['女粉比例'].map(lambda x: '{0:.2f}%'.format(x*100))
df_tmp
14318844-3a215d1bd7ff6c02.jpg
image
14318844-6279603faf3ed1a1.jpg
image

嗯……触目惊心啊!

首先,陈坤同学以他的盛世美颜俘虏了一大批妹子的芳心!结合微博男女失衡以及陈坤的个人属性,我们认为这个比例虽然有些高但还可以接受,毕竟后边几位名人的女粉比例跟他也没差多少。然后张小娴的粉丝多为女粉,这个就更好理解了,男生看言情内容的还是比较少的(虽然当年我很爱看……我确定我是直男!)。

榜单中间的我们不多聊,聊聊比较极端的。

唐嫣的粉丝中,高达82%的粉丝为男粉!!!而男粉丝并不活跃,所以这就是为什么她的活跃粉、合格粉、主动粉的比例这么低?

我产生了以下推测:

  1. 微博的推荐位是可以购买的
  2. 微博的推荐位可以个性化购买,比如选定性别、地域等
  3. 最近唐嫣的团队购买了大量男性新用户的推荐位或者微博很贴心地为她免费提供了大量男性新用户的推荐位
  4. 希望大家一起来帮忙想一下还有什么原因能产生如此极端的结果
  5. 想不起来也没关系,存在即是合理,我们尊重结果就是了

我们看到,上边推测下方有5条记录,这说明我们有5条推测可以说明这个结果是合理的,都散了吧。

7. 谁的粉丝对热度贡献更大?

这一指标能在一定程度上反应平均每个粉丝对于该明星流量、热度的贡献大小。但是呢,由于评论、转发、点赞的行为和是否是粉丝没有关系,所以刷转发、评论、点赞和刷粉丝榜是完全独立的。也就是说,假如两边的数据都掺杂了水分,那他们直接原有的相关性会被污染的数据所掩盖。

由于每条微博的转发、评论和点赞量不是完全在一个数量级,所以我们对三个贡献量做一个简单的0-1标准化,并以相同权重相加得到综合贡献量这一指标。

df2_stats = df2.groupby('star_name').mean()
df2_stats['转发贡献量'] = df2_stats['forward'] / df2_stats['total_fans']
df2_stats['评论贡献量'] = df2_stats['comment'] / df2_stats['total_fans']
df2_stats['点赞贡献量'] = df2_stats['like'] / df2_stats['total_fans']
df2_stats['avg_forward_normal'] = (df2_stats['转发贡献量'] - df2_stats['转发贡献量'].min()) / (df2_stats['转发贡献量'].max() - df2_stats['转发贡献量'].min())
df2_stats['avg_comment_normal'] = (df2_stats['评论贡献量'] - df2_stats['评论贡献量'].min()) / (df2_stats['评论贡献量'].max() - df2_stats['评论贡献量'].min())
df2_stats['avg_like_normal'] = (df2_stats['点赞贡献量'] - df2_stats['点赞贡献量'].min()) / (df2_stats['点赞贡献量'].max() - df2_stats['点赞贡献量'].min())
df2_stats['综合贡献量'] = df2_stats[['avg_forward_normal', 'avg_comment_normal', 'avg_like_normal']].sum(axis=1) / 3
df2_stats = df2_stats.sort_values('综合贡献量', ascending=False)
df2_stats['total_fans'] = df2_stats['total_fans'].map(int)
df2_stats = df2_stats[['total_fans', '转发贡献量', '评论贡献量', '点赞贡献量', '综合贡献量']]
df2_stats.columns = ['粉丝数', '转发贡献量', '评论贡献量', '点赞贡献量', '综合贡献量']
df2_stats.index.name = ''
df2_stats

作图:

df2_stats['转发贡献量'].sort_values(ascending=False).plot(kind='bar', figsize=(18,9), fontsize=16, grid=True)
df2_stats['评论贡献量'].sort_values(ascending=False).plot(kind='bar', figsize=(18,9), fontsize=16, grid=True)
df2_stats['点赞贡献量'].sort_values(ascending=False).plot(kind='bar', figsize=(18,9), fontsize=16, grid=True)
df2_stats['综合贡献量'].sort_values(ascending=False).plot(kind='bar', figsize=(18,9), fontsize=16, grid=True)
14318844-f3615f0937a175ae.jpg
image
14318844-ec19b358d3f14853.jpg
image
14318844-77c67ac507760cc7.jpg
image
14318844-b6a32ae52f75b335.jpg
image

这里我们看到了跟粉丝数据相悖的一些地方。比如说主动粉、活跃粉比例高的陈羽凡、文章等人在这里得到了较低的粉丝贡献量数据。

同时男粉比例较高的名人基本上得到了较低的粉丝贡献量数据,这也从侧面验证了之前微博老大所说的女用户更活跃是的确存在的现象。

不太一样的是虽然唐嫣的男粉比例异常地高,但她的粉丝贡献量数据排在榜单前三。也许是男粉对于男明星只是默默关注,但对于女明星就会比较爱表现吧……还有一种可能就是许多喜欢唐嫣的粉丝并没有关注她,但是每次都能及时出现在她的微博下并纷纷转发、评论和点赞。

啊,其实我觉得唐嫣挺漂亮的,我也挺喜欢她的,但是数据长这样真不能怪我啊!

五、结论

我不敢下结论,你们自己总结吧……

总得来说,这里的数据不能代表总体,这里的分析方法也存在很多问题,作者本人没有任何明显的倾向性,也不属于任何门派,也不是谁派来的黑子或者捧哏,大家看了权当一乐。如果有更好的分析思路,可以在下方留言,咱们共同探讨。

展开阅读全文

没有更多推荐了,返回首页