Блог об аналитике, визуализации данных, data science и BI

    Valiotti Analytics — построение аналитики для мобильных и digital-стартапов

Собираем топ-10 аккаунтов Instagram по теме аналитики и машинного обучения

Время чтения текста – 11 минут

В некоторых телеграм-каналах (раз, два) уже говорилось про другие интересные паблики в телеграме, однако по Instagram такого топа пока не было. Вероятно, это не самая популярная сеть для контента в нашей индустрии, тем не менее, можно проверить эту гипотезу, используя Python и данные. В этом материале рассказываем, как собрать данные по аккаунтам Instagram без API.

Метод сбора данных
Instagram API не позволит вам просто так собирать данные о других пользователях, но есть и другой метод. Можно отправить такой request-запрос:

https://instagram.com/leftjoin/?__a=1

И получить в ответе JSON-объект со всей информацией о пользователе, которую можно посмотреть самому: имя аккаунта, количество постов, подписок и подписчиков, а также первые десять постов с информацией про них: количество лайков, комментарии и прочее. Именно на таких request-запросах устроена библиотека pyInstagram.

Схема данных
Будем собирать данные в три таблицы Clickhouse: пользователи, посты и комментарии. В таблицу пользователей собираем всю информацию о них: идентификатор, наименование аккаунта, имя и фамилия человека, описание профиля, количество подписок и подписчиков, количество постов, суммарное количество комментариев и лайков, наличие верификации, география пользователя и ссылки на аватарку и Facebook.

CREATE TABLE instagram.users
(
    `added_at` DateTime,
    `user_id` UInt64,
    `user_name` String,
    `full_name` String,
    `base_url` String,
    `biography` String,
    `followers_count` UInt64,
    `follows_count` UInt64,
    `media_count` UInt64,
    `total_comments` UInt64,
    `total_likes` UInt64,
    `is_verified` UInt8,
    `country_block` UInt8,
    `profile_pic_url` Nullable(String),
    `profile_pic_url_hd` Nullable(String),
    `fb_page` Nullable(String)
)
ENGINE = ReplacingMergeTree
ORDER BY added_at

В таблицу с постами сохраняем автора поста, идентификатор записи, текст, количество комментариев и прочее. is_ad, is_album и is_video — поля, проверяющие, является ли запись рекламной, «каруселью» изображений или видеозаписью.

CREATE TABLE instagram.posts
(
    `added_at` DateTime,
    `owner` String,
    `post_id` UInt64,
    `caption` Nullable(String),
    `code` String,
    `comments_count` UInt64,
    `comments_disabled` UInt8,
    `created_at` DateTime,
    `display_url` String,
    `is_ad` UInt8,
    `is_album` UInt8,
    `is_video` UInt8,
    `likes_count` UInt64,
    `location` Nullable(String),
    `recources` Array(String),
    `video_url` Nullable(String)
)
ENGINE = ReplacingMergeTree
ORDER BY added_at

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

CREATE TABLE instagram.comments
(
    `added_at` DateTime,
    `comment_id` UInt64,
    `post_id` UInt64,
    `comment_owner` String,
    `comment_text` String
)
ENGINE = ReplacingMergeTree
ORDER BY added_at

Скрипт
Из библиотеки pyInstagram нам понадобятся классы Account, Media, WebAgent и Comment.

from instagram import Account, Media, WebAgent, Comment
from datetime import datetime
from clickhouse_driver import Client
import requests
import pandas as pd

Создаем экземпляр класса WebAgent — он необходим для вызова некоторых методов и обновления аккаунтов. В начале нам нужно иметь хотя бы названия профилей пользователей, информацию о которых мы хотим собрать, поэтому отправим другой request-запрос для поиска пользователей по ключевым словам, их список ниже в фрагменте кода. В выдаче будут аккаунты, у которых название или описание профиля совпало с ключевым словом.

agent = WebAgent()
queries_list = ['machine learning', 'data science', 'data analytics', 'analytics', 'business intelligence',
                'data engineering', 'computer science', 'big data', 'artificial intelligence',
                'deep learning', 'data scientist','machine learning engineer', 'data engineer']
client = Client(host='54.227.137.142', user='default', password='', port='9000', database='instagram')
url = 'https://www.instagram.com/web/search/topsearch/?context=user&count=0'

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

response_list = []
for query in queries_list:
    response = requests.get(url, params={
        'query': query
    }).json()
    response_list.extend(response['users'])
instagram_pages_list = []
for item in response_list:
    instagram_pages_list.append(item['user']['username'])
instagram_pages_list = list(set(instagram_pages_list))

Теперь проходим по списку аккаунтов, и если аккаунта с таким наименованием ещё не было в базе, то получаем расширенную информацию о нём. Для этого пробуем создать экземпляр класса Account, передав username параметром. После при помощи объекта agent обновляем информацию об аккаунте. Будем собирать только первые 100 постов, чтобы сбор не задерживался. Создадим список media_list — он при помощи метода get_media будет хранить код каждого поста, который затем можно будет получить при помощи класса Media.


Сбор медиа аккаунта

all_posts_list = []
username_count = 0
for username in instagram_pages_list:
    if client.execute(f"SELECT count(1) FROM users WHERE user_name='{username}'")[0][0] == 0:
        print('username:', username_count, '/', len(instagram_pages_list))
        username_count += 1
        account_total_likes = 0
        account_total_comments = 0
        try:
            account = Account(username)
        except Exception as E:
            print(E)
            continue
        try:
            agent.update(account)
        except Exception as E:
            print(E)
            continue
        if account.media_count < 100:
            post_count = account.media_count
        else:
            post_count = 100
        print(account, post_count)
        media_list, _ = agent.get_media(account, count=post_count, delay=1)
        count = 0

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


Сбор постов пользователя

for media_code in media_list:
            if client.execute(f"SELECT count(1) FROM posts WHERE code='{media_code}'")[0][0] == 0:
                print('posts:', count, '/', len(media_list))
                count += 1

                post_insert_list = []
                post = Media(media_code)
                agent.update(post)
                post_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
                post_insert_list.append(str(post.owner))
                post_insert_list.append(post.id)
                if post.caption is not None:
                    post_insert_list.append(post.caption.replace("'","").replace('"', ''))
                else:
                    post_insert_list.append("")
                post_insert_list.append(post.code)
                post_insert_list.append(post.comments_count)
                post_insert_list.append(int(post.comments_disabled))
                post_insert_list.append(datetime.fromtimestamp(post.date).strftime('%Y-%m-%d %H:%M:%S'))
                post_insert_list.append(post.display_url)
                try:
                    post_insert_list.append(int(post.is_ad))
                except TypeError:
                    post_insert_list.append('cast(Null as Nullable(UInt8))')
                post_insert_list.append(int(post.is_album))
                post_insert_list.append(int(post.is_video))
                post_insert_list.append(post.likes_count)
                if post.location is not None:
                    post_insert_list.append(post.location)
                else:
                    post_insert_list.append('')
                post_insert_list.append(post.resources)
                if post.video_url is not None:
                    post_insert_list.append(post.video_url)
                else:
                    post_insert_list.append('')
                account_total_likes += post.likes_count
                account_total_comments += post.comments_count
                try:
                    client.execute(f'''
                        INSERT INTO posts VALUES {tuple(post_insert_list)}
                    ''')
                except Exception as E:
                    print('posts:')
                    print(E)
                    print(post_insert_list)

Чтобы собрать комментарии необходимо вызвать метод get_comments и передать параметром экземпляр класса Media.


Сбор комментариев из поста

comments = agent.get_comments(media=post)
                for comment_id in comments[0]:
                    comment_insert_list = []
                    comment = Comment(comment_id)
                    comment_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
                    comment_insert_list.append(comment.id)
                    comment_insert_list.append(post.id)
                    comment_insert_list.append(str(comment.owner))
                    comment_insert_list.append(comment.text.replace("'","").replace('"', ''))
                    try:
                        client.execute(f'''
                            INSERT INTO comments VALUES {tuple(comment_insert_list)}
                        ''')
                    except Exception as E:
                        print('comments:')
                        print(E)
                        print(comment_insert_list)


Наконец, когда все посты и комментарии пройдены, можем занести информацию о пользователе.

Сбор информации о пользователе

user_insert_list = []
        user_insert_list.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
        user_insert_list.append(account.id)
        user_insert_list.append(account.username)
        user_insert_list.append(account.full_name)
        user_insert_list.append(account.base_url)
        user_insert_list.append(account.biography)
        user_insert_list.append(account.followers_count)
        user_insert_list.append(account.follows_count)
        user_insert_list.append(account.media_count)
        user_insert_list.append(account_total_comments)
        user_insert_list.append(account_total_likes)
        user_insert_list.append(int(account.is_verified))
        user_insert_list.append(int(account.country_block))
        user_insert_list.append(account.profile_pic_url)
        user_insert_list.append(account.profile_pic_url_hd)
        if account.fb_page is not None:
            user_insert_list.append(account.fb_page)
        else:
            user_insert_list.append('')
        try:
            client.execute(f'''
                INSERT INTO users VALUES {tuple(user_insert_list)}
            ''')
        except Exception as E:
            print('users:')
            print(E)
            print(user_insert_list)

Результаты
Таким методом нам удалось собрать 500 пользователей, 20 тысяч постов и 40 тысяч комментариев. Теперь можем написать простой запрос к базе и получить топ-10 Instagram-аккаунтов по теме аналитики и машинного обучения за последнее время:

SELECT *
FROM users
ORDER BY followers_count DESC
LIMIT 10

А вот и приятный бонус, для тех, кто искал на какие аккаунты в Instagram подписаться по релевантной тематике:

  1. @ai_machine_learning
  2. @neuralnine
  3. @datascienceinfo
  4. @compscistuff
  5. @computersciencelife
  6. @welcome.ai
  7. @papa_programmer
  8. @data_science_learn
  9. @neuralnet.ai
  10. @techno_thinkers

Полный код проекта доступен на GitHub

Доклады онлайн-конференции FutureData

Время чтения текста – 14 минут

C 8 по 9 сентября состоялась онлайн-конференция FutureData, в которой я принял участие. Вчера организаторы опубликовали записи докладов. Хочу поделиться своими наблюдениями и докладами, которые заинтересовали меня. Постарался собрать максимально релевантные скриншоты, но сразу извиняюсь за их качество, выдирал прямо из видео.

Featured Keynote: Automating Analysis
Спикер: Pat Hanrahan
В докладе профессор Стэнфордского университета и сооснователь Tableau рассуждает об использовании AI в аналитике. Доклад получился монотонным, и большую часть времени Pat обсуждает где мы сейчас, как мы используем AI, однако секция вопросов и ответов получилась интересная.

The Modern Data Stack: Past, Present, and Future
Спикер: Tristan Handy
Автор знаменитой публикации о руководстве по аналитике для основателя стартапа и создатель dbt рассуждает о том, как менялся современный data-stack с 2012 по 2020 год. Для меня доклад оказался наиболее интересным, особенно учитывая, что Tristan делает предсказания о том, что будет расти и развиваться в data-stack в ближайшее время.

Making Enterprise Data Timelier and More Reliable with Lakehouse Technology
Спикер: Matei Zaharia
Доклад главного технолога DataBricks. К сожалению, в докладе большие проблемы с аудио, но Matei рассматривает проблемы современного Data Lake, а дальше продвигает технологию DataBricks — DeltaLake. Как по мне, доклад получился рекламным, но послушать интересно.

How to Close the Analytic Divide
Спикер: Alan Jacobson
Chief Data Officer из Alteryx рассуждает о профессии Data Scientist и приводит статистику по зарплатам, в которой средняя зарплата специалиста по данным существенно выше, чем у остальных аналитиков. К слову, наше недавнее исследование с Ромой Буниным это подтверждает. Далее Alan обсуждает выручку компаний, находящихся на разных стадиях аналитического развития. Более развитые — (сюрприз!) растут быстрее. Отдельная часть доклада посвящена изменениям в трансформации к подходу к работе с данными, а в конце небольшое рекламное интро Alteryx. Доклад смотрится легко.

Hot Analytics — Handle with Care
Спикер: Gian Merlino
Co-Founder и CTO Imply приводит сравнение hot & cold data (намек на Snowflake?). Затем — демонстрация некоторой BI от Imply с простеньким интерфейсом и реализованным drag-n-drop. Далее Gian рассказывает о возможных аналитических архитектурах и затрагивает тему Druid, на которой построен Imply.

Анализ рынка вакансий аналитики и BI: дашборд в Tableau

Время чтения текста – 16 минут

По данным рейтинга SimilarWeb, hh.ru — третий по популярности сайт о трудоустройстве в мире. В одном из разговоров с Ромой Буниным у нас появилась идея сделать совместный проект: собрать данные из открытого HeadHunter API и визуализировать их при помощи Tableau Public. Нам захотелось понять, как меняется зарплата в зависимости от указанных в вакансии навыков, наименования позиции и сравнить, как обстоят дела в Москве, Санкт-Петербурге и регионах.

Как мы собирали данные?

Схема данных основана на коротком представлении вакансии, которую возвращает метод GET /vacancies. Из представления собираются следующие поля: тип вакансии, идентификатор, премиальность вакансии, необходимость прохождения тестирования, адрес компании, информация о зарплате, график работы и другие. Соответствующий CREATE-запрос для таблицы:


Запрос создания таблицы vacancies_short

CREATE TABLE headhunter.vacancies_short
(
    `added_at` DateTime,
    `query_string` String,
    `type` String,
    `level` String,
    `direction` String,
    `vacancy_id` UInt64,
    `premium` UInt8,
    `has_test` UInt8,
    `response_url` String,
    `address_city` String,
    `address_street` String,
    `address_building` String,
    `address_description` String,
    `address_lat` String,
    `address_lng` String,
    `address_raw` String,
    `address_metro_stations` String,
    `alternate_url` String,
    `apply_alternate_url` String,
    `department_id` String,
    `department_name` String,
    `salary_from` Nullable(Float64),
    `salary_to` Nullable(Float64),
    `salary_currency` String,
    `salary_gross` Nullable(UInt8),
    `name` String,
    `insider_interview_id` Nullable(UInt64),
    `insider_interview_url` String,
    `area_url` String,
    `area_id` UInt64,
    `area_name` String,
    `url` String,
    `published_at` DateTime,
    `employer_url` String,
    `employer_alternate_url` String,
    `employer_logo_urls_90` String,
    `employer_logo_urls_240` String,
    `employer_logo_urls_original` String,
    `employer_name` String,
    `employer_id` UInt64,
    `response_letter_required` UInt8,
    `type_id` String,
    `type_name` String,
    `archived` UInt8,
    `schedule_id` Nullable(String)
)
ENGINE = ReplacingMergeTree
ORDER BY vacancy_id

Первый скрипт собирает данные с HeadHunter по API и отправляет их в Clickhouse. Он использует следующие библиотеки:

import requests
from clickhouse_driver import Client
from datetime import datetime
import pandas as pd
import re

Далее загружаем таблицу с запросами и подключаемся к CH:

queries = pd.read_csv('hh_data.csv')
client = Client(host='1.234.567.890', user='default', password='', port='9000', database='headhunter')

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

Не всегда вакансии в выдаче соответствуют ожиданиям: случайно в базу могут попасть повара, маркетологи и администраторы магазина. Чтобы этого не произошло, опишем функцию check_name(name) — она будет принимать наименование вакансии и возвращать True в случае, если вакансия не подошла по названию.

def check_name(name):
    bad_names = [r'курьер', r'грузчик', r'врач', r'менеджер по закупу',
           r'менеджер по продажам', r'оператор', r'повар', r'продавец',
          r'директор магазина', r'директор по продажам', r'директор по маркетингу',
          r'кабельщик', r'начальник отдела продаж', r'заместитель', r'администратор магазина', 
          r'категорийный', r'аудитор', r'юрист', r'контент', r'супервайзер', r'стажер-ученик', 
          r'су-шеф', r'маркетолог$', r'региональный', r'ревизор', r'экономист', r'ветеринар', 
          r'торговый', r'клиентский', r'начальник цеха', r'территориальный', r'переводчик', 
          r'маркетолог /', r'маркетолог по']
    for item in bad_names:
        if re.match(item, name):
            return True

Затем объявляем бесконечный цикл — мы собираем данные без перерыва. Идём по DataFrame queries и сразу забираем оттуда тип вакансии, уровень, направление и поисковый запрос в отдельные переменные. Сначала по ключевому слову отправляем один запрос к методу /GET vacancies и получаем количество страниц. После идём от нулевой до последней страницы, отправляем те же запросы и заполняем список vacancies_from_response с полученными в выдаче короткими представлениями всех вакансий. В параметрах указываем 10 вакансий на страницу — больше ограничения HH API получить не позволяют. Так как мы не указали параметр area, API возвращает вакансии по всему миру.

while True:
   for query_type, level, direction, query_string in zip(queries['Тип'], queries['Уровень'], queries['Направление'], queries['Ключевое слово']):
           print(f'ключевое слово: {query_string}')
           url = 'https://api.hh.ru/vacancies'
           par = {'text': query_string, 'per_page':'10', 'page':0}
           r = requests.get(url, params=par).json()
           added_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
           pages = r['pages']
           found = r['found']
           vacancies_from_response = []

           for i in range(0, pages + 1):
               par = {'text': query_string, 'per_page':'10', 'page':i}
               r = requests.get(url, params=par).json()
               try:
                   vacancies_from_response.append(r['items'])
               except Exception as E:
                   continue

Теперь проходим по каждой вакансии на каждой странице двойным итератором. Сперва отправим запрос к Clickhouse и проверим, нет ли уже в базе вакансии с таким идентификатором и таким поисковым запросом. Если проверка пройдена — проверяем название вакансии. В случае неудачи переходим к следующей.

for item in vacancies_from_response:
               for vacancy in item:
                   if client.execute(f"SELECT count(1) FROM vacancies_short WHERE vacancy_id={vacancy['id']} AND query_string='{query_string}'")[0][0] == 0:
                       name = vacancy['name'].replace("'","").replace('"','')
                       if check_name(name):
                           continue

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


Код для сбора данных о вакансии

vacancy_id = vacancy['id']
                       is_premium = int(vacancy['premium'])
                       has_test = int(vacancy['has_test'])
                       response_url = vacancy['response_url']
                       try:
                           address_city = vacancy['address']['city']
                           address_street = vacancy['address']['street']
                           address_building = vacancy['address']['building']
                           address_description = vacancy['address']['description']
                           address_lat = vacancy['address']['lat']
                           address_lng = vacancy['address']['lng']
                           address_raw = vacancy['address']['raw']
                           address_metro_stations = str(vacancy['address']['metro_stations']).replace("'",'"')
                       except TypeError:
                           address_city = ""
                           address_street = ""
                           address_building = ""
                           address_description = ""
                           address_lat = ""
                           address_lng = ""
                           address_raw = ""
                           address_metro_stations = ""
                       alternate_url = vacancy['alternate_url']
                       apply_alternate_url = vacancy['apply_alternate_url']
                       try:
                           department_id = vacancy['department']['id']
                       except TypeError as E:
                           department_id = ""
                       try:
                           department_name = vacancy['department']['name']
                       except TypeError as E:
                           department_name = ""
                       try:
                           salary_from = vacancy['salary']['from']
                       except TypeError as E:
                           salary_from = "cast(Null as Nullable(UInt64))"
                       try:
                           salary_to = vacancy['salary']['to']
                       except TypeError as E:
                           salary_to = "cast(Null as Nullable(UInt64))"
                       try:
                           salary_currency = vacancy['salary']['currency']
                       except TypeError as E:
                           salary_currency = ""
                       try:
                           salary_gross = int(vacancy['salary']['gross'])
                       except TypeError as E:
                           salary_gross = "cast(Null as Nullable(UInt8))"
                       try:
                           insider_interview_id = vacancy['insider_interview']['id']
                       except TypeError:
                           insider_interview_id = "cast(Null as Nullable(UInt64))"
                       try:
                           insider_interview_url = vacancy['insider_interview']['url']
                       except TypeError:
                           insider_interview_url = ""
                       area_url = vacancy['area']['url']
                       area_id = vacancy['area']['id']
                       area_name = vacancy['area']['name']
                       url = vacancy['url']
                       published_at = vacancy['published_at']
                       published_at = datetime.strptime(published_at,'%Y-%m-%dT%H:%M:%S%z').strftime('%Y-%m-%d %H:%M:%S')
                       try:
                           employer_url = vacancy['employer']['url']
                       except Exception as E:
                           print(E)
                           employer_url = ""
                       try:
                           employer_alternate_url = vacancy['employer']['alternate_url']
                       except Exception as E:
                           print(E)
                           employer_alternate_url = ""
                       try:
                           employer_logo_urls_90 = vacancy['employer']['logo_urls']['90']
                           employer_logo_urls_240 = vacancy['employer']['logo_urls']['240']
                           employer_logo_urls_original = vacancy['employer']['logo_urls']['original']
                       except Exception as E:
                           print(E)
                           employer_logo_urls_90 = ""
                           employer_logo_urls_240 = ""
                           employer_logo_urls_original = ""
                       employer_name = vacancy['employer']['name'].replace("'","").replace('"','')
                       try:
                           employer_id = vacancy['employer']['id']
                       except Exception as E:
                           print(E)
                       response_letter_required = int(vacancy['response_letter_required'])
                       type_id = vacancy['type']['id']
                       type_name = vacancy['type']['name']
                       is_archived = int(vacancy['archived'])

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

try:
    schedule = vacancy['schedule']['id']
except Exception as E:
    print(E)
    schedule = ''"
if schedule == 'flyInFlyOut':
    continue

Теперь формируем список из полученных переменных, заменяем в нём None-значения на пустые строки во избежании конфликтов с Clickhouse и вставляем строку в таблицу.

vacancies_short_list = [added_at, query_string, query_type, level, direction, vacancy_id, is_premium, has_test, response_url, address_city, address_street, address_building, address_description, address_lat, address_lng, address_raw, address_metro_stations, alternate_url, apply_alternate_url, department_id, department_name,
salary_from, salary_to, salary_currency, salary_gross, insider_interview_id, insider_interview_url, area_url, area_name, url, published_at, employer_url, employer_logo_urls_90, employer_logo_urls_240,  employer_name, employer_id, response_letter_required, type_id, type_name, is_archived, schedule]
for index, item in enumerate(vacancies_short_list):
    if item is None:
        vacancies_short_list[index] = ""
tuple_to_insert = tuple(vacancies_short_list)
print(tuple_to_insert)
client.execute(f'INSERT INTO vacancies_short VALUES {tuple_to_insert}')

Как подключили Tableau к данным?

Tableau Public не умеет работать с базами данных, поэтому мы написали коннектор Clickhouse к Google Sheets. Он использует библиотеки gspread и oauth2client для авторизации в Google Spreadsheets API и библиотеку schedule для ежедневной работы по графику.

Работа с Google Spreadseets API подробно разобрана в материале «Собираем данные по рекламным кампаниям ВКонтакте»

import schedule
from clickhouse_driver import Client
import gspread
import pandas as pd
from oauth2client.service_account import ServiceAccountCredentials
from datetime import datetime

scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
client = Client(host='54.227.137.142', user='default', password='', port='9000', database='headhunter')
creds = ServiceAccountCredentials.from_json_keyfile_name('credentials.json', scope)
gc = gspread.authorize(creds)

Опишем функцию update_sheet() — она будет брать все данные из Clickhouse и вставлять их в таблицу Google Docs.

def update_sheet():
   print('Updating cell at', datetime.now())
   columns = []
   for item in client.execute('describe table headhunter.vacancies_short'):
       columns.append(item[0])
   vacancies = client.execute('SELECT * FROM headhunter.vacancies_short')
   df_vacancies = pd.DataFrame(vacancies, columns=columns)
   df_vacancies.to_csv('vacancies_short.csv', index=False)
   content = open('vacancies_short.csv', 'r').read()
   gc.import_csv('1ZWS2kqraPa4i72hzp0noU02SrYVo0teD7KZ0c3hl-UI', content.encode('utf-8'))

Чтобы скрипт запускался в 16:00 по МСК каждый день используем библиотеку schedule:

schedule.every().day.at("13:00").do(update_sheet)
while True:
   schedule.run_pending()

А что в результате?

Рома построил на полученных данных дашборд.

И в youtube-ролике рассказывает о том, как эффективно использовать дашборд

Инсайты, которые можно извлечь из дашборда

  1. Аналитики с навыком бизнес-аналитики востребованы на рынке больше всего: по такому запросу нашлось больше всего вакансий. Тем не менее, средняя зарплата выше у продуктовых аналитиков и аналитиков BI.
  2. В Москве средние зарплаты выше на 10-30 тысяч рублей, чем в Санкт-Петербурге и на 30-40 тысячи рублей, чем в регионах. Там же работы нашлось больше всего в России.
  3. Самые высокооплачиваемые должности: руководитель отдела аналитики (в среднем, 110 тыс. руб. в месяц), инженер баз данных (138 тыс. руб. в месяц) и директор по машинному обучению (250 тыс. руб. в месяц).
  4. Самые полезные навыки на рынке — владение Python c библиотеками pandas и numpy, Tableau, Power BI, Etl и Spark. Вакансий с такими требованиями больше и зарплаты в них указаны выше прочих. Для Python-программистов знание matplotlib ценится на рынке выше, чем владение plotly.

Полный код проекта доступен на GitHub

Полное руководство по созданию таблиц

Время чтения текста – 23 минуты

Перевод статьи «The Ultimate Guide to Designing Data Tables»

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

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

Разработка стиля таблицы

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

Выберите лучшее оформление строк

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

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

Пример сетки (таблица из UI Prep)

Горизонтальные линии: Отображение только горизонтальных линий снижает визуальные помехи сетки. Этот стиль содержит много пустого пространства, тем самым, помогая пользователю не отвлекаться от чтения. Этот стиль является наиболее распространенным и подходит для любых размеров набора данных.

Пример горизонтальной линии (таблица из UI Prep)

Зебра: Чередование разного цвета фона для каждой строки — ещё один хороший способ помочь пользователям сосредоточиться на чтении. Этот стиль рекомендуется для больших наборов данных, где чередование будет четким и не вызовет путаницы из-за выделения определенной строки.

Пример зебры (таблица из UI Prep)

Свободная форма: Удаление всех разделителей создает минималистичный вид за счет максимального уменьшения визуальных помех. Этот стиль рекомендуется для небольших наборов данных, где пользователям не нужна помощь при чтении.

Пример произвольной формы (таблица из UI Prep)

Используйте заметный контраст

Определите иерархию, добавив контраст данных к вашей таблице. Это можно сделать с помощью разных стилей текста и фона.

Контраст заголовка: Отделите заголовок от текста столбца, изменив его жирность и цвет. Применение разных цветов фона заголовка может обеспечить дополнительный контраст, если это необходимо.

Наверху: контраст текста заголовка, Внизу: контраст фона заголовка (таблица из UI Prep)

Выделение столбцов: Столбцы также могут быть разных цветов и жирности для выделения определенных данных, таких как идентификатор строки (первый столбец) или главных данных в одной ячейке (например, данные ячейки: 1,234 34%).

Наверху: контраст первой строки, Внизу: Контраст данных (таблица из UI Prep)

Добавляйте визуальные подсказки

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

Фон строки: Поменяйте цвет всей строки, чтобы подчеркнуть разницу в типе данных (т. е. переключение между отдельными значениями и суммами или средними значениями).

Пример фона строки (таблица из UI Prep)

Фон ячейки: Измените цвет ячейки, чтобы выделить элемент данных (например, уменьшение и повышение значения элемента данных).

Пример фона ячейки (таблица из UI Prep)

Правильно выравнивайте столбцы

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

Правила, которым необходимо следовать:

  1. Выравнивайте текстовые данные по левому краю (например, имя)
  2. Выравнивайте числовые данные, не связанные с размером (например, дату, почтовый индекс, номер телефона)
  3. Выравнивайте числовые данные, относящиеся к размеру (например, количество, процент)
  4. Выравнивайте заголовки в соответствии с данными столбца
Пример выравнивания (таблица из  UI Prep)

Используйте табличные числа

При отображении чисел используйте табличный (или моноширинный) шрифт. Это означает, что вместо пропорционального интервала (т. е. символ «W» шире, символа «I»), каждая фигура имеет одинаковую ширину. Это упрощает просмотр столбцов с числовыми данными.
Для своей таблицы вы можете использовать моноширинный шрифт (например, Courier, Courier New, Lucida Console, Monaco и т. д.) Или моноширинный числовой набор, иногда включаемый в пропорциональные шрифты.

Пример пропорционального и табличного шрифта

Выберите подходящую высоту для строки

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

Высота рядов:

  1. Сжатая: 40px
  2. Стандартная: 48px
  3. Свободная: 56px
Примеры высоты строки (таблица из UI Prep)

Создавайте достаточно отступов

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

Пример отступа (таблица из UI Prep)

Используйте подтекст

Подтекст может предоставить актуальный контекст и полезные детали, не занимая места и не загромождая таблицу. Его также можно использовать для объединения столбцов.
Например, вместо того, чтобы иметь отдельные столбцы для пользователя и электронной почты, электронное письмо можно добавить как подтекст под именем пользователя.

Пример подтекста (таблица из UI Prep)

Функциональность таблицы

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

Сохранение контекста при прокрутке

Прикрепите контекстную информацию, чтобы помочь пользователям понять, какие данные они просматривают при прокрутке по таблице. Это функционально важно при создании таблиц с большими наборами данных или на экранах меньшего размера.

Фиксированный заголовок: Закрепите заголовок вверху таблицы, когда пользователь ее прокручивает. Это обеспечивает контекст, постоянно сохраняя метки столбцов в поле зрения.

Пример фиксированного заголовка (таблица из UI Prep)

Фиксированный столбец: Закрепите первый столбец (с идентифицирующей информацией) сбоку, когда пользователь горизонтально прокручивает таблицу. Это обеспечивает контекст, постоянно сохраняя идентификатор строки в поле зрения.

Пример фиксированного столбца (таблица из UI Prep)

Ставьте в приоритет стандартные действия

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

Действие при наведении: Представьте стандартные действия, когда пользователь наводит курсор на строку. Они уменьшают визуальные помехи, но у новых пользователей могут возникнуть проблемы с обнаружением действий.

Пример действия при наведении курсора (таблица из UI Prep)

Групповое действие: Позволяет пользователям выбирать и обновлять несколько элементов одновременно. После выбора строк обобщающие действия становятся доступными в верхней части таблицы. Эта функция может сэкономить пользователям много времени, объединяя повторяющиеся задачи.

Пример массового действия (таблица из UI Prep)

Управляйте данными с помощью фильтров

Позвольте пользователям управлять данными, представленными в таблице. Фильтрация данных помогает пользователям находить то, что они ищут, получать различную информацию и проводить сравнения.

Базовые фильтры: Разрешите пользователям применять предустановленные параметры к некоторым наборам данных. Эта функция универсальна и подходит для большинства таблиц данных.

Пример базового фильтра (таблица из UI Prep)

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

Пример комплексного фильтра (таблица из UI Prep)

Включайте нумерацию страниц

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

Пример нумерации страниц в таблице (таблица из UI Prep)

Сделайте данные столбца настраиваемыми

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

Пример настраиваемого столбца (таблица из UI Prep)

Сделайте параметры отображения страницы настраиваемыми

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

Сортировка столбцов: Позвольте пользователям организовывать строки по определенной категории. Любой столбец можно упорядочить в алфавитном или числовом порядке, щелкнув значок сортировки рядом с меткой столбца.

Пример сортировки столбца (таблица из UI Prep)

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

Столбец с изменяемым размером (таблица из UI Prep)

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

Пример плотности отображения (таблица из UI Prep)

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

Обзор библиотеки pandas-profiling на примере датасета Superstore Sales

Время чтения текста – 10 минут

Перед тем как работать с данными, необходимо составить представление, с чем мы имеем дело. В материале будем рассматривать датасет SuperStore Sales, а именно его лист Orders. В нём собраны данные о покупках клиентов канадского интернет-супермаркета: идентификаторы заказа, товаров, клиента, тип доставки, цены, категории и названия продуктов и прочее. Подробнее с датасетом можно ознакомиться на GitHub. Например, если мы создадим из датасета DataFrame, можем воспользоваться стандартным методом describe() библиотеки pandas для описания данных:

import pandas as pd

df = pd.read_csv('superstore_sales_orders.csv', decimal=',')
df.describe(include='all')

И во многих случаях получим такую кашу:

Код библиотеки доступен на GitHub

Если постараться и потратить время, можно извлечь полезную информацию. Например, можем узнать, что люди чаще выбирают «Regular air» в качестве доставки или что большинство заказов поступило из провинции Онтарио. Тем не менее, есть и другое решение, которое подробнее и качественнее описывает датасет — библиотека pandas-profiling. Вы отдаёте ей DataFrame, а она генерирует html-страницу с подробным описанием сета данных:

import pandas_profiling
profile = pandas_profiling.ProfileReport(df)
profile.to_file("output.html")

Всего Pandas Profiling возвращает 6 разделов: обзор датасета, переменные, отношения и корреляцию между ними, количество пропущенных значений и примеры из датасета.

Web-версия отчёта доступна по ссылке

Обзор данных

Рассмотрим первый подраздел — «Overview». Библиотека собрала следующую статистику: количество переменных, наблюдений, пропущенных ячеек, дубликатов и общий вес файла. В колонке Variable types описаны типы переменных: здесь 12 качественных и 9 числовых.

В подразделе «Reproduction» собрана техническая информация библиотеки: сколько времени занял анализ сета данных, версия библиотеки и прочее.

А подраздел «Warnings» сообщает о возможных проблемах в структуре датасета: сейчас он, например, предупреждает, что у поля «Order Date» — слишком большое количество уникальных значений.

Переменные

Двигаемся ниже. В этом разделе содержится подробное описание каждой переменной: сколько возможных уникальных значений она принимает, сколько значений пропущено, сколько памяти занимает поле. Справа от статистики присутствует гистограмма с распределением значений поля.

При нажатии на Toggle details откроется расширенная информация: квартили, медиана и прочая полезная описательная статистика. В остальных вкладках находятся гистограмма из основного экрана, топ-10 значений по частоте и экстремальные значения.

Отношения переменных

В этом разделе визуализированы отношения переменных при помощи hexbin plot: выглядит это не очень очевидно и понятно. Особенно усугубляет положение отсутствие легенды к графику.

Корреляция переменных

В этом разделе представлена по-разному посчитананя корреляция переменных: например, первым указано r-value Пирсона. Заметно, что переменная Profit положительно коррелирует с переменной Sales. При нажатии на Toggle correlation descriptions открывается подробное пояснение к каждому коэффициенту.

Пропущенные значения

Тут всё просто — bar chart, матрица и дендрограмма с количеством заполненных полей в каждой переменной. Заметно, что в колонке Product Base Margin отсутствуют три значения.

Примеры

И, наконец, последний раздел представляет первые и последние 10 значений в качестве примера кусков сета данных — аналог метода head() из pandas.

Что в итоге?

Библиотека уделяет больше внимания статистике, чем pandas: можно получить подробную описательную статистику по каждой переменной, посмотреть, как коррелируют между собой столбцы датасета. В совокупности с генерацией простого и удобного интерфейса библиотека строит полноценный отчёт по датасету, уже на основании которого можно делать выводы и сформировать представление о данных.
И всё же, у библиотеки есть и минусы. На генерацию отчётов к громадным датасетам может уйти много времени вплоть до нескольких часов. Это безусловно хороший инструмент для автоматического проектирования, но он не может сделать полноценный анализ за вас и добавить больше деталей в графики. Кроме того, если вы только начали практиковаться с анализом данных лучше будет начать с pandas — это закрепит ваши навыки и придаст уверенности при работе с данными.

Ранее Ctrl + ↓