Habrastatystyka: badanie najczęściej i najmniej odwiedzanych sekcji serwisu

Hej Habr.

В poprzednia część Ruch na Habr został przeanalizowany według głównych parametrów – liczby artykułów, ich wyświetleń i ocen. Nierozpatrzona jednak pozostała kwestia popularności sekcji serwisu. Ciekawie było przyjrzeć się temu bardziej szczegółowo i znaleźć najpopularniejsze i najbardziej niepopularne huby. Na koniec przyjrzę się bliżej efektowi geektimes, kończąc na nowym wyborze najlepszych artykułów w oparciu o nowe rankingi.

Habrastatystyka: badanie najczęściej i najmniej odwiedzanych sekcji serwisu

Dla tych, którzy są zainteresowani tym, co się stało, kontynuacja jest obcięta.

Jeszcze raz przypominam, że statystyki i oceny nie są oficjalne, nie mam żadnych poufnych informacji. Nie ma też gwarancji, że gdzieś się nie pomyliłem lub czegoś nie przeoczyłem. Ale i tak uważam, że wyszło interesująco. Zaczniemy najpierw od kodu, ci, których to nie interesuje, mogą pominąć pierwsze sekcje.

Zbieranie danych

W pierwszej wersji parsera brana była pod uwagę jedynie liczba wyświetleń, komentarzy i ocen artykułów. To już jest dobre, ale nie pozwala na tworzenie bardziej złożonych zapytań. Czas przeanalizować sekcje tematyczne serwisu, pozwoli to na zrobienie całkiem ciekawego researchu, na przykład sprawdzenia, jak zmieniała się popularność sekcji „C++” na przestrzeni kilku lat.

Ulepszono parser artykułu, teraz zwraca on huby, do których należy artykuł, a także nick autora i jego ocenę (tu też można wiele ciekawych rzeczy zrobić, ale o tym później). Dane są zapisywane w pliku csv, który wygląda mniej więcej tak:

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
...

Otrzymamy listę głównych węzłów tematycznych serwisu.

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)

Funkcja find_between i klasa Str wybierają ciąg znaków pomiędzy dwoma tagami, ja ich użyłem wcześniej. Węzły tematyczne są oznaczone „*”, dzięki czemu można je łatwo wyróżnić. Można także odkomentować odpowiednie linie, aby uzyskać sekcje innych kategorii.

Wynikiem funkcji get_hubs jest dość imponująca lista, którą zapisujemy jako słownik. Specjalnie przedstawiam listę w całości, abyście mogli oszacować jej objętość.

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'}

Dla porównania sekcje geektimes wyglądają skromniej:

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'}

W ten sam sposób zachowano pozostałe piasty. Teraz łatwo jest napisać funkcję, która zwróci wynik niezależnie od tego, czy artykuł należy do geektimes, czy do centrum profili.

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

Podobne funkcje przygotowano dla pozostałych działów („rozwój”, „administracja” itp.).

Przetwarzanie

Analizę czas zacząć. Ładujemy zbiór danych i przetwarzamy dane z centrum.

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)

Teraz możemy grupować dane według dni i wyświetlać liczbę publikacji dla różnych hubów.

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()

Liczbę opublikowanych artykułów wyświetlamy za pomocą Matplotlib:

Habrastatystyka: badanie najczęściej i najmniej odwiedzanych sekcji serwisu

Artykuły „geektimes” i „tylko geektimes” podzieliłem na wykresie, ponieważ Artykuł może należeć do obu działów jednocześnie (np. „DIY” + „mikrokontrolery” + „C++”). Użyłem określenia „profil”, aby wyróżnić artykuły profilowe w witrynie, chociaż być może angielski termin „profil” nie jest do końca poprawny.

W poprzedniej części pytaliśmy o „efekt geektimes” związany ze zmianą od tego lata zasad płatności za artykuły w geektimes. Wyświetlmy artykuły geektimes osobno:

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()

Wynik jest interesujący. Przybliżony stosunek wyświetleń artykułów geektimes do całości wynosi około 1:5. Jednak choć całkowita liczba wyświetleń wyraźnie się wahała, oglądalność artykułów „rozrywkowych” pozostała na mniej więcej tym samym poziomie.

Habrastatystyka: badanie najczęściej i najmniej odwiedzanych sekcji serwisu

Można też zauważyć, że łączna liczba wyświetleń artykułów w sekcji „geektimes” po zmianie regulaminu nadal spadała, ale „na oko” o nie więcej niż 5% wartości ogółem.

Interesujące jest spojrzenie na średnią liczbę wyświetleń na artykuł:

Habrastatystyka: badanie najczęściej i najmniej odwiedzanych sekcji serwisu

W przypadku artykułów „rozrywkowych” jest to około 40% powyżej średniej. Prawdopodobnie nie jest to zaskakujące. Awaria na początku kwietnia jest dla mnie niejasna, może tak się stało, a może to jakiś błąd w analizie, a może któryś z autorów geektimes wyjechał na wakacje ;).

Nawiasem mówiąc, wykres pokazuje dwa bardziej zauważalne szczyty liczby wyświetleń artykułów - Nowy Rok i święta majowe.

Piasty

Przejdźmy do obiecanej analizy hubów. Wymieńmy 20 najlepszych ośrodków według liczby wyświetleń:

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()

Wynik:

Habrastatystyka: badanie najczęściej i najmniej odwiedzanych sekcji serwisu

Co zaskakujące, najpopularniejszym ośrodkiem pod względem poglądów było „Bezpieczeństwo informacji”, a w pierwszej piątce liderów znalazły się także „Programowanie” i „Nauka popularna”.

Antitop zajmuje Gtk i Cocoa.

Habrastatystyka: badanie najczęściej i najmniej odwiedzanych sekcji serwisu

Zdradzę Ci sekret, górne piasty też widać tutaj, chociaż nie jest tam pokazana liczba wyświetleń.

ocena

I na koniec obiecana ocena. Korzystając z danych analizy hubów, możemy wyświetlić najpopularniejsze artykuły dla najpopularniejszych hubów w tym roku 2019.

Bezpieczeństwo informacji

Programowanie

Popularna nauka

kariera

Legislacja w IT

tworzenie stron internetowych

GTK

I na koniec, żeby nikt się nie obraził, podam ocenę najrzadziej odwiedzanego hubu „gtk”. W ciągu roku została opublikowana одна Artykuł, który również „automatycznie” zajmuje pierwszą linię rankingu.

wniosek

Nie będzie żadnych wniosków. Miłego czytania wszystkim.

Źródło: www.habr.com

Dodaj komentarz