Хабрастатистика: досліджуємо найбільш і найменш відвідувані розділи сайту

Привіт Хабр.

В попередньої частини було проаналізовано відвідуваність Хабра за основними параметрами — кількістю статей, їх переглядами та рейтингами. Однак питання популярності розділів сайту залишилося не розглянутим. Стало цікаво розглянути це докладніше, і знайти найпопулярніші та найпопулярніші хаби. Нарешті, я розгляну «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». У ньому за рік було опубліковано одна стаття, вона ж "автоматом" займає перший рядок рейтингу.

Висновок

Висновків не буде. Усім приємне читання.

Джерело: habr.com

Додати коментар або відгук