Привет, Хабр.

В предыдущей части была проанализирована посещаемость Хабра по основным параметрам — количеству статей, их просмотрам и рейтингам. Однако вопрос популярности разделов сайта остался не рассмотренным. Стало интересно рассмотреть это более подробно, и найти самые популярные и самые непопулярные хабы. Наконец, я рассмотрю «geektimes-эффект» более подробно, и в завершении читатели получат новую подборку лучших статей по новым рейтингам.



Кому интересно что получилось, продолжение под катом.

Еще раз напомню, что статистика и рейтинг не являются официальными, никакой инсайдерской информации у меня нет. Также не гарантируется, что я где-то не ошибся или что-то не пропустил. Но все же, думаю, получилось интересно. Мы приступим сначала к коду, кому это неактуально, первые разделы могут пропустить.

Сбор данных


В первой версии парсера учитывались лишь число просмотров, комментариев и рейтинг статей. Это уже неплохо, но не позволяет делать более сложные запросы. Пора проанализировать тематические разделы сайта, это позволит делать достаточно интересные исследования, например, посмотреть как менялась популярность раздела «С++» за несколько лет.

Парсер статей был улучшен, теперь он возвращает хабы, к которым относится статья, а также ник автора и его рейтинг (тут тоже можно сделать много интересного, но это потом). Данные сохранены в csv-файле примерно такого вида:

2018-12-18T12:43Z,https://habr.com/ru/post/433550/,"Мессенджер Slack — причины выбора, косяки при внедрении и особенности сервиса, облегчающие жизнь",votes:7,votesplus:8,votesmin:1,bookmarks:32,
views:8300,comments:10,user:ReDisque,karma:5,subscribers:2,hubs:productpm+soft
...

Получим список основных тематических хабов сайта.

def get_as_str(link: str) -> Str:
    try:
        r = requests.get(link)
        return Str(r.text)
    except Exception as e:
        return Str("")

def get_hubs():
    hubs = []
    for p in range(1, 12):
        page_html = get_as_str("https://habr.com/ru/hubs/page%d/" % p)
        # page_html = get_as_str("https://habr.com/ru/hubs/geektimes/page%d/" % p)  # Geektimes
        # page_html = get_as_str("https://habr.com/ru/hubs/develop/page%d/" % p)  # Develop
        # page_html = get_as_str("https://habr.com/ru/hubs/admin/page%d" % p)  # Admin
        for hub in page_html.split("media-obj media-obj_hub"):
            info = Str(hub).find_between('"https://habr.com/ru/hub', 'list-snippet__tags') 
            if "*</span>" in info:
                hub_name = info.find_between('/', '/"')
                if len(hub_name) > 0 and len(hub_name) < 32:
                    hubs.append(hub_name)
    print(hubs)

Функция find_between и класс Str выделяют строку между двух тегов, я использовал их ранее. Тематические хабы отмечены "*", так что их легко выделить, можно также раскомментировать соответствующие строки, чтобы получить разделы других категорий.

На выходе функции get_hubs получаем достаточно внушительный список, который сохраняем как dictionary. Специально привожу список целиком, чтобы можно было оценить его объем.

hubs_profile = {'infosecurity', 'programming', 'webdev', 'python', 'sys_admin', 'it-infrastructure', 'devops', 'javascript', 'open_source', 'network_technologies', 'gamedev', 'cpp', 'machine_learning', 'pm', 'hr_management', 'linux', 'analysis_design', 'ui', 'net', 'hi', 'maths', 'mobile_dev', 'productpm', 'win_dev', 'it_testing', 'dev_management', 'algorithms', 'go', 'php', 'csharp', 'nix', 'data_visualization', 'web_testing', 's_admin', 'crazydev', 'data_mining', 'bigdata', 'c', 'java', 'usability', 'instant_messaging', 'gtd', 'system_programming', 'ios_dev', 'oop', 'nginx', 'kubernetes', 'sql', '3d_graphics', 'css', 'geo', 'image_processing', 'controllers', 'game_design', 'html5', 'community_management', 'electronics', 'android_dev', 'crypto', 'netdev', 'cisconetworks', 'db_admins', 'funcprog', 'wireless', 'dwh', 'linux_dev', 'assembler', 'reactjs', 'sales', 'microservices', 'search_technologies', 'compilers', 'virtualization', 'client_side_optimization', 'distributed_systems', 'api', 'media_management', 'complete_code', 'typescript', 'postgresql', 'rust', 'agile', 'refactoring', 'parallel_programming', 'mssql', 'game_promotion', 'robo_dev', 'reverse-engineering', 'web_analytics', 'unity', 'symfony', 'build_automation', 'swift', 'raspberrypi', 'web_design', 'kotlin', 'debug', 'pay_system', 'apps_design', 'git', 'shells', 'laravel', 'mobile_testing', 'openstreetmap', 'lua', 'vs', 'yii', 'sport_programming', 'service_desk', 'itstandarts', 'nodejs', 'data_warehouse', 'ctf', 'erp', 'video', 'mobileanalytics', 'ipv6', 'virus', 'crm', 'backup', 'mesh_networking', 'cad_cam', 'patents', 'cloud_computing', 'growthhacking', 'iot_dev', 'server_side_optimization', 'latex', 'natural_language_processing', 'scala', 'unreal_engine', 'mongodb', 'delphi',  'industrial_control_system', 'r', 'fpga', 'oracle', 'arduino', 'magento', 'ruby', 'nosql', 'flutter', 'xml', 'apache', 'sveltejs', 'devmail', 'ecommerce_development', 'opendata', 'Hadoop', 'yandex_api', 'game_monetization', 'ror', 'graph_design', 'scada', 'mobile_monetization', 'sqlite', 'accessibility', 'saas', 'helpdesk', 'matlab', 'julia', 'aws', 'data_recovery', 'erlang', 'angular', 'osx_dev', 'dns', 'dart', 'vector_graphics', 'asp', 'domains', 'cvs', 'asterisk', 'iis', 'it_monetization', 'localization', 'objectivec', 'IPFS', 'jquery', 'lisp', 'arvrdev', 'powershell', 'd', 'conversion', 'animation', 'webgl', 'wordpress', 'elm', 'qt_software', 'google_api', 'groovy_grails', 'Sailfish_dev', 'Atlassian', 'desktop_environment', 'game_testing', 'mysql', 'ecm', 'cms', 'Xamarin', 'haskell', 'prototyping', 'sw', 'django', 'gradle', 'billing', 'tdd', 'openshift', 'canvas', 'map_api', 'vuejs', 'data_compression', 'tizen_dev', 'iptv', 'mono', 'labview', 'perl', 'AJAX', 'ms_access', 'gpgpu', 'infolust', 'microformats', 'facebook_api', 'vba', 'twitter_api', 'twisted', 'phalcon', 'joomla', 'action_script', 'flex', 'gtk', 'meteorjs', 'iconoskaz', 'cobol', 'cocoa', 'fortran', 'uml', 'codeigniter', 'prolog', 'mercurial', 'drupal', 'wp_dev', 'smallbasic', 'webassembly', 'cubrid', 'fido', 'bada_dev', 'cgi', 'extjs', 'zend_framework', 'typography', 'UEFI', 'geo_systems', 'vim', 'creative_commons', 'modx', 'derbyjs', 'xcode', 'greasemonkey', 'i2p', 'flash_platform', 'coffeescript', 'fsharp', 'clojure', 'puppet', 'forth', 'processing_lang', 'firebird', 'javame_dev', 'cakephp', 'google_cloud_vision_api', 'kohanaphp', 'elixirphoenix', 'eclipse', 'xslt', 'smalltalk', 'googlecloud', 'gae', 'mootools', 'emacs', 'flask', 'gwt', 'web_monetization', 'circuit-design', 'office365dev', 'haxe', 'doctrine', 'typo3', 'regex', 'solidity', 'brainfuck', 'sphinx', 'san', 'vk_api', 'ecommerce'}

Для сравнения, разделы geektimes выглядят скромнее:

hubs_gt = {'popular_science', 'history', 'soft', 'lifehacks', 'health', 'finance', 'artificial_intelligence', 'itcompanies', 'DIY', 'energy', 'transport', 'gadgets', 'social_networks', 'space', 'futurenow', 'it_bigraphy', 'antikvariat', 'games', 'hardware', 'learning_languages', 'urban', 'brain', 'internet_of_things', 'easyelectronics', 'cellular', 'physics', 'cryptocurrency', 'interviews', 'biotech', 'network_hardware', 'autogadgets', 'lasers', 'sound', 'home_automation', 'smartphones', 'statistics', 'robot', 'cpu', 'video_tech', 'Ecology', 'presentation', 'desktops', 'wearable_electronics', 'quantum', 'notebooks', 'cyberpunk', 'Peripheral', 'demoscene', 'copyright', 'astronomy', 'arvr', 'medgadgets', '3d-printers', 'Chemistry', 'storages', 'sci-fi', 'logic_games', 'office', 'tablets', 'displays', 'video_conferencing', 'videocards', 'photo', 'multicopters', 'supercomputers', 'telemedicine', 'cybersport', 'nano', 'crowdsourcing', 'infographics'}

Аналогично были сохранены остальные хабы. Теперь несложно написать функцию, которая возвращает результат, относится статья к geektimes или к профильному хабу.

def is_geektimes(hubs: List) -> bool:
    return len(set(hubs) & hubs_gt) > 0

def is_geektimes_only(hubs: List) -> bool:
    return is_geektimes(hubs) is True and is_profile(hubs) is False

def is_profile(hubs: List) -> bool:
    return len(set(hubs) & hubs_profile) > 0

Аналогичные функции были сделаны для других разделов («разработка», «администрирование» и пр).

Обработка


Пора приступать к анализу. Загружаем датасет и обрабатываем данные хабов.

def to_list(s: str) -> List[str]:
    # "user:popular_science+astronomy" => [popular_science, astronomy]
    return s.split(':')[1].split('+')

def to_date(dt: datetime) -> datetime.date:
    return dt.date()

df = pd.read_csv("habr_2019.csv", sep=',', encoding='utf-8', error_bad_lines=True, quotechar='"', comment='#')
dates = pd.to_datetime(df['datetime'], format='%Y-%m-%dT%H:%MZ')
dates += datetime.timedelta(hours=3)
df['date'] = dates.map(to_date, na_action=None)
hubs = df["hubs"].map(to_list, na_action=None)
df['hubs'] = hubs
df['is_profile'] = hubs.map(is_profile, na_action=None)
df['is_geektimes'] = hubs.map(is_geektimes, na_action=None)
df['is_geektimes_only'] = hubs.map(is_geektimes_only, na_action=None)
df['is_admin'] = hubs.map(is_admin, na_action=None)
df['is_develop'] = hubs.map(is_develop, na_action=None)

Теперь мы можем сгруппировать данные по дням и вывести число публикаций по разным хабам.

g = df.groupby(['date'])
days_count = g.size().reset_index(name='counts')
year_days = days_count['date'].values
grouped = g.sum().reset_index()
profile_per_day_avg = grouped['is_profile'].rolling(window=20, min_periods=1).mean()
geektimes_per_day_avg = grouped['is_geektimes'].rolling(window=20, min_periods=1).mean()
geektimesonly_per_day_avg = grouped['is_geektimes_only'].rolling(window=20, min_periods=1).mean()
admin_per_day_avg = grouped['is_admin'].rolling(window=20, min_periods=1).mean()
develop_per_day_avg = grouped['is_develop'].rolling(window=20, min_periods=1).mean()

Выводим количество опубликованных статей с помощью Matplotlib:



Я разделил в графике статьи «geektimes» и «geektimes only», т.к. статья может принадлежать к обеим разделам одновременно (например «DIY» + «микроконтроллеры» + «С++»). Обозначением «profile» я выделил профильные статьи сайта, хотя возможно, английский термин profile для этого не совсем верный.

В предыдущей части спрашивали про «geektimes-эффект», связанный с изменением правил оплаты статей для geektimes с этого лета. Выведем отдельно статьи geektimes:

df_gt = df[(df['is_geektimes_only'] == True)]
group_gt = df_gt.groupby(['date'])
days_count_gt = group_gt.size().reset_index(name='counts')
grouped = group_gt.sum().reset_index()
year_days_gt = days_count_gt['date'].values
view_gt_per_day_avg = grouped['views'].rolling(window=20, min_periods=1).mean()

Результат интересный. Примерное соотношение просмотров статей geektimes к общему где-то 1:5. Но если общее число просмотров заметно колебалось, то просмотр «развлекательных» статей держался примерно на одном уровне.



Также можно заметить, что общее число просмотров статей раздела «geektimes» после изменения правил все же упало, но «на глаз», не больше чем на 5% от общих значений.

Интересно посмотреть среднее число просмотров на статью:



Для «развлекательных» статей оно примерно на 40% выше среднего. Наверно это неудивительно. Провал в начале апреля мне непонятен, может так и было, или это какая-то ошибка парсинга, а может кто-то из авторов geektimes ушел в отпуск ;).

Кстати, на графике видны еще два заметных пика числа просмотров статей — новогодние и майские праздники.

Хабы


Перейдем к обещанному анализу хабов. Выведем топ 20 хабов по числу просмотров:

hubs_info = []
for hub_name in hubs_all:
    mask = df['hubs'].apply(lambda x: hub_name in x)
    df_hub = df[mask]

    count, views = df_hub.shape[0], df_hub['views'].sum()
    hubs_info.append((hub_name, count, views))

# Draw hubs
hubs_top = sorted(hubs_info, key=lambda v: v[2], reverse=True)[:20]
top_views = list(map(lambda x: x[2], hubs_top))
top_names = list(map(lambda x: x[0], hubs_top))

plt.rcParams["figure.figsize"] = (8, 6)
plt.bar(range(0, len(top_views)), top_views)
plt.xticks(range(0, len(top_names)), top_names, rotation=90)
plt.ticklabel_format(style='plain', axis='y')
plt.tight_layout()
plt.show()

Результат:



На удивление, самым популярным по просмотрам оказался хаб «Информационная безопасность», также в топ-5 лидеров входят «Программирование» и «Popular science».

Антитоп занимает Gtk и Cocoa.



Скажу по секрету, топ хабов также можно увидеть и здесь, хотя число просмотров там не показано.

Рейтинг


И наконец, обещанный рейтинг. Используя данные анализа хабов, мы можем вывести самые популярные статьи по самым популярным хабам за этот 2019 год.

Информационная безопасность


Программирование


Научно-популярное


Карьера


Законодательство в IT


Веб-девелопмент


GTK

И наконец, чтобы никому не было обидно, приведу рейтинг самого малопосещаемого хаба «gtk». В нем за год была опубликована одна статья, она же «автоматом» занимает первую строчку рейтинга.


Заключение


Заключения не будет. Всем приятного чтения.

Комментарии (15)


  1. kabuto
    15.09.2019 05:25
    -2

    Хабр давно уже дохлый сайт.


    1. gasizdat
      15.09.2019 10:55

      Вполне себе бодрый. Куча читателей и статей. ИМХО, проблема не в уменьшении активности на сайте, а в смещении тематики сайта. Раньше хабр был интересен только программистам и гикам. Теперь значительно большему кругу лиц. Контент неизбежно становился более попсовым. Это приводит к вымыванию профильной аудитории, что делает контент еще попсовей. Система с положительной обратной связью.


      1. DmitrySpb79 Автор
        15.09.2019 14:46

        Раньше хабр был интересен только программистам и гикам. Теперь значительно большему кругу лиц.

        В смене правил здесь есть определенный плюс — теперь у авторов больше мотивации писать например, не только в разделы типа DIY, но и добавлять больше информации по разработке, фрагменты кода и пр, чтобы попасть в профильные хабы.

        Я сначала относился негативно к смене правил оплаты, а сейчас посмотрев статистику, думаю, что здравый смысл в этом есть. Хотя конечно разделы типа «космоса» или «химии» мы теряем, зато есть надежда что будет больше информации именно IT-содержания.

        А так да, нельзя объять необъятное. Уже сейчас 50 статей на сайте в день это больше, чем я как читатель могу прочитать, 80% информации просто пропускается.


        1. gasizdat
          16.09.2019 08:36

          "зато есть надежда что будет больше информации именно IT-содержания".
          Лично у меня, нет такой надежды. Сравните активность в профильных хабах, типа C++, C#, JS в году так 2013 и сейчас. Если есть желание и время, сравните по данным хабам число статей, их рейтинг, количество и средний рейтинг комментариев к статьям, число добавления в закладки. Я практически уверен, что станет грустно. Я хорошо помню какая в этих хабах была движуха в 2013. Сейчас такого нет даже отдаленно.


        1. Boomburum
          16.09.2019 14:17

          Я сначала относился негативно к смене правил оплаты, а сейчас посмотрев статистику, думаю, что здравый смысл в этом есть

          Смена правил оплаты действительно была связана только с желанием сделать больший акцент на технических статьях, как это изначально было на Хабре, до введения ППА (что не исключает того, что в «гиктаймс-хабах» может оказаться что-то техническое-претехническое).

          Уже сейчас 50 статей на сайте в день это больше, чем я как читатель могу прочитать

          Постараемся скоро немного «почистить» ленту (без ущерба для авторов статей и читателей).


          1. DmitrySpb79 Автор
            16.09.2019 17:56

            С лентой все сложнее имхо. Если просто сократить ленту, то статей в первую страницу будет попадать еще меньше, и средний рейтинг еще больше упадет. В идеале нужна какая-то «интеллектуальная» система, формирующая список статей для каждого читателя по его истории просмотров, примерно как в youtube делается. Тогда такую ленту будет более интересно читать.


            1. Boomburum
              16.09.2019 18:02

              Не, речь не про сокращение ленты — подумываем над тем, чтобы вынести все мероприятия в отдельный раздел (как это было с новостям). Впрочем, так давно было на Хабре :) Пример с новостями показывает, что это вполне хороший вариант — сейчас новости смотрят и обсуждают, но при этом они не вытесняют лонгриды в ленте.

              Если унести все события (митапы, хакатоны и прочее) в отдельный раздел, то ленте постов достанется ещё больше внимания читателей. При этом события будут удобно расположены в отдельном разделе, и у них не будет такого количества минусов.

              Похоже на win-win? :)

              А насчёт «интеллектуальности» — это давно напрашивается, но мы проводили опрос и пользователи проголосовали за «хронологический порядок», который, по всей видимости, им проще контролировать. Ведь если зайти в тот же вконтакт/фейсбук/ютуб, сразу непонятно, какой пласт контента ты упустил с момента последнего входа )


              1. DmitrySpb79 Автор
                16.09.2019 19:41

                Хронологический порядок это нормально, я другое имел в виду.

                У меня по умолчанию включен режим «Лучшие статьи», т.е. я и так вижу примерно 20% всех публикаций, так что 80% я и так упускаю по-любому (режим «все подряд» я не использую, т.к. там слишком много уже).

                Так вот, хотелось бы чтобы режим «лучшие» показывал результаты с учетом моей истории просмотра. Если например, мне неинтересна Java, то и статьи по ней я читать не буду даже если в ленте они есть, а статью про Python я с большей вероятностью открою.

                Не знаю, понятно ли объяснил. Понятно что можно вручную настроить подписки на хабы, но лень заморачиваться, лучше чтобы оно все «само» формировалось :)


                1. Boomburum
                  16.09.2019 20:02

                  Да, смысл понятен, интересно — передам коллегам для обсуждения.
                  cc karaboz


                1. gasizdat
                  17.09.2019 10:11

                  Есть подписки на хабы и авторов. Лично меня вполне устраивает. Все, кроме качества контента в этих подписках


                  1. DmitrySpb79 Автор
                    17.09.2019 12:54

                    Это же саморегулирующаяся система с обратной связью. Хабр имеет очевидную «науч.поп» направленность, какие-то серьезные узкоспециализированные статьи со сложным кодом здесь имхо просто «не идут», не набирают ни рейтинга, ни просмотров. Я сам после анализа последних рейтингов зарекся выкладывать сюда что-то сложное, смысла нет — работа впустую.


                    1. gasizdat
                      17.09.2019 14:23
                      +1

                      какие-то серьезные узкоспециализированные статьи со сложным кодом здесь имхо просто «не идут», не набирают ни рейтинга, ни просмотров
                      До первого разделения это было не так. ИМХО, оно убило тот хабр, который стимулировал меня на статью. «Гики» заходили сюда почитать научпоп и пописать технохардкор. Всем пользователям это было выгодно в плане разнообразия и качества контента. После разделения научпоп переехал. Гикам стало не интересно на хабре, т.к. они туда ходили за научпопом, а авторам качественного научпопа на гиктаймсе, из-за изменений в ППА и увеличения средней попсовости ресурса (трудозатраты на качественный научпоп перестали окупаться аудиторией). Дальнейшее объединение уже не могло вернуть ресурс на прежнюю точку локального равновесия.
                      выкладывать сюда что-то сложное, смысла нет — работа впустую
                      Соглашусь, это подтверждает мою гипотезу выше. Поэтому самое интересное, что встречается здесь последнее время, это хорошие технические переводы, а качественного уникального контента все меньше.


                      1. DmitrySpb79 Автор
                        17.09.2019 15:41
                        +1

                        Есть еще такой момент, что все новые и полезные статьи по программированию в основном на английском, вся аудитория соответственно, там же. Русскоязычные статьи по программированию актуальны больше для джуниоров имхо.


  1. kababok
    15.09.2019 11:46

    А еще в одной из пятничных прямых линий с ТМ (когда-то за последние полгода) даже приводился официальный график и явно говорилось, что посещение в летние месяцы (по вполне очевидным причинам :) в среднем несколько проседает. :)


    1. DmitrySpb79 Автор
      15.09.2019 14:42

      Да, интересно что в новогодние праздники строго наоборот — среднее число просмотров на статью растет. Хотя и статей в праздники публикуется меньше.