Привет! Меня зовут Григорий Митраков, я BI-аналитик в рекламном агентстве.
В статье расскажу вам как создать приложение в Power BI по пожеланиям заказчиков.
Предыстория
После того, как я устроился работать в агентство ко мне обратилась руководитель HR отдела для разработки небольшого автоматизированного отчета в BI. До этого основные показатели (текучесть, вовлеченность и др.) рассчитывались в табличках Excel полу ручным способом.
Так выглядели отчеты в Excel файлах:
Причину автоматизации мне объяснили тем, что около 4-х часов в неделю сотрудник HR отдела тратил на расчет показателей в таблицах Excel. (Что хорошо – видно, что какими-то графиками и диаграммами уже пользовались).
Сбор требований к отчету
После первой встречи с HR отделом в лице руководителя, специалиста, занимавшегося сбором данных и оформлением отчетов в таблицах Excel, выяснилось, что источники с необходимыми данными находятся в регулярных выгрузках из 1С Предприятие, Google Sheets, Битрикс24, PostgreSQL, iSpring. По визуальному оформлению как таковых «жестких» требований не было, была лишь просьба, учесть наглядность представляемых данных и «желательно как в таблицах Excel».
Работа над дашбордом
Задача показалась мне интересной ввиду достаточно большого количества разнообразных источников данных и свободы в выборе визуализации данных.
Для прозрачности работы, контроля текущего статуса над задачей и внесение своевременных правок в оформление и расчет метрик решили собираться раз в неделю по четвергам (этот день назвали «День HR»).
Для всех источников данных решил использовать БД PostgreSQL (была развернута в компании). Для разработки ETL-процессов — Apache Airflow.
Ниже представлена схема ETL процесса:
Данные раз в сутки (по расписанию) загружаю:
-
с помощью API REST с опросников (google sheets),
-
с помощью API REST с платформы для обучения сотрудников iSpring,
-
с Битрикс24 веб-скрейпингом (у Битрикс24 есть за дополнительную плату расширение BI-Аналитика, но в компании бесплатная версия, поэтому получение данных организовано таким образом),
-
с сетевых папок, в которых хранятся регулярные выгрузки в формате .xlsx из 1С.
Пример DAG (таблица с курсами iSpring):
import json import time import datetime import requests import pandas as pd from airflow.decorators import dag, task from airflow.models import Variable from sqlalchemy import create_engine from airflow.providers.telegram.hooks.telegram import TelegramHook # Параметры для аутентификации и входа user = 'grigoriy' host = '101.100.9.43' db = 'internal_data' pwd = Variable.get('planning_datas_password') postgresql_url = f'postgresql+psycopg2://{user}:{pwd}@{host}/{db}' engine = create_engine(postgresql_url) # запрос к БД для получения id различных курсов query_db = '''SELECT t.module_id, t.content_item_id, t.course_id FROM ispring_course_modules_table AS t GROUP BY t.module_id, t.content_item_id, t.course_id''' URL_TOKEN = 'https://api-learn.ispringlearn.ru/api/v3/token' URL_CONTENT = 'https://api-learn.ispringlearn.ru/courses/modules' HEADER = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' } HEADERS_R = { 'X-Target-Locale': 'en-US', 'Accept': 'application/json' } CLIENT_SECRET = Variable.get('ispring_api_client_secret') DATA_URLENCODE = { 'client_id': 'xxbbaaxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'client_secret': CLIENT_SECRET, 'grant_type': 'client_credentials' } default_args = {'owner': 'xxxxxxx' } TELEGRAM_TOKEN = Variable.get('tg_bot_token') CHAT_ID = '0000000000' # Необходимые функции: def error_to_telegram(token: str, chat_id: str, e, name_dag: str): ''' Функция отправляет сообщения об ошибках в чат телеграмм :param: token: "xxx:xxx" API токен для telegram telegram_token :param: chat_id куда отправлять сообщение :param: e - ошибка :param: name_dag - название DAG ''' telegram_conn_id = 'telegram_default' telegram_hook = TelegramHook(telegram_conn_id, token, chat_id) t = datetime.datetime.now() date_and_time = str(t.date()) + ' ' + str(t.time()) message = f'? В *DAG {name_dag}* произошла ошибка {e}' message_text = f'{message}. Время ошибки: {date_and_time}' telegram_hook.send_message({ 'text': message_text, 'disable_notification': True }) @dag(default_args=default_args, schedule_interval='30 12 * * *', start_date=datetime.datetime(2023, 1, 1), catchup=False ) def get_data_course_modules_from_ispring(): @task def get_id_courses_db(query: str, engine) -> pd.DataFrame: """ Функция получает список различных id курсов из БД :param query: запрос к БД :return: список """ dis_id_db = pd.read_sql(query, engine) return dis_id_db @task def get_token_ispring(url_token: str, header: dict, data_urlcode: dict, telegram_token: str, id_chat: str) -> str: ''' Функция получает токен :param: url_token - url :param: header - headers :param: data_urlcode - params :return: token ''' res = requests.post(url=url_token, headers=header, data=data_urlcode) if res.status_code == 200: # Результаты получения токена: res_token = json.loads(res.text) # Токен для доступа: authorization_token = res_token['access_token'] return authorization_token else: error_to_telegram(token=telegram_token, chat_id=id_chat, e=str(res.status_code) + ' ' + str(res.text), name_dag='get_data_course_modules_from_ispring') @task def get_data_courses(url_content: str, header_r: dict, autho_token: str, id_courses_t: pd.DataFrame, telegram_token: str, id_chat: str) -> pd.DataFrame: # Присвоим токен к заголовку: header_r['Authorization'] = autho_token result = requests.get(url=url_content, headers=header_r) if result.status_code == 200: # Таблица со списком курсов: course_content = pd.DataFrame(json.loads(result.text)["modules"]) # Переименуем столбцы: course_content = course_content.rename(columns={'moduleId': 'module_id', 'contentItemId': 'content_item_id', 'courseId': 'course_id', 'authorId': 'author_id', 'addedDate': 'added_date', 'viewUrl': 'view_url'}) # Приведем в необходимый тип данные: course_content['added_date'] = pd.to_datetime(course_content['added_date'], utc=True).dt.tz_localize(None).astype('datetime64[ns]') course_content = course_content.astype({'module_id': str, 'content_item_id': str, 'course_id': str, 'title': str, 'description': str, 'author_id': str, 'added_date': 'datetime64[ns]', 'view_url': str}) # Выберем необходимые столбцы: cols = ['module_id', 'content_item_id', 'course_id', 'title', 'description', 'author_id', 'added_date', 'view_url'] course_content = course_content[cols] return course_content[~((course_content['module_id'].isin(id_courses_t['module_id'])) & (course_content['content_item_id'].isin(id_courses_t['content_item_id'])) & (course_content['course_id'].isin(id_courses_t['course_id'])))].reset_index(drop=True) else: error_to_telegram(token=telegram_token, chat_id=id_chat, e=str(result.status_code) + ' ' + str(result.text), name_dag='get_data_course_modules_from_ispring') @task def append_data_to_db(data: pd.DataFrame, table: str, engine): """ Функция добавляет данные из датафрейма в БД :param data: датафрейм :param table: имя таблицы :return: None """ for i in range(len(data) // 10000 + 1): data.iloc[i * 10000: (i + 1) * 10000].to_sql(con=engine, name=table, if_exists='append', index=False) time.sleep(3) # Загрузка таблицы со списком курсов в БД dis_id_db = get_id_courses_db(query=query_db, engine=engine) token_ispring = get_token_ispring(url_token=URL_TOKEN, header=HEADER, data_urlcode=DATA_URLENCODE, telegram_token=TELEGRAM_TOKEN, id_chat=CHAT_ID) df = get_data_courses(url_content=URL_CONTENT, header_r=HEADERS_R, autho_token=token_ispring, id_courses_t=dis_id_db, telegram_token=TELEGRAM_TOKEN, id_chat=CHAT_ID) append_data_to_db(data=df, table='ispring_course_modules_table', engine=engine) get_data_course_modules_from_ispring = get_data_course_modules_from_ispring()
С каждого Python модуля получаю алерты в Телеграм-чат, в случае, если загрузка прошла неудачно.
Пример такого алерта:
Из БД PostgreSQL получаю данные в дашборды, некоторые таблички получаю с вьюшек, вьюшки с материализованных представлений. Схема в БД:
Финальная обработка данных осуществляется в Power Query.
Разработку начали с важных метрик, двигаясь постепенно к второстепенным по мере выполнения отчетов.
Первый дашборд представлял метрику «Текучесть в компании» (ниже представлены первые 2 страницы):
Модель данных получилась такая:
Второй дашборд представлял метрику «Вовлеченность в компании» (ниже представлены первые 2 страницы):
Модель данных получилась такая:
Следующий дашборд по оценке «Внутреннего NPS» — считается по формуле: (кол-во положительных оценок – кол-во отрицательных оценок) / общее кол-во оценок. Этот дашборд показывает количество и оценку внутренних задач в компании, выполняемых отделами или сотрудниками и оцениваемых постановщиками задач.
Ниже представлены первые две страницы:
Модель данных:
Следующий дашборд по метрике «Изменения грейдов сотрудников».
Ниже представлены первые две страницы:
Модель данных:
Следующий дашборд по метрике «Адаптация сотрудников в компании».
Ниже представлены первые две страницы дашборда:
Модель данных:
Следующий дашборд показывает результаты прохождения сотрудниками компании курсов на iSpring:
Модель данных:
Итоговый дашборд включает объединение предыдущих дашбордов в один: «Карта здоровья».
Получился такой:

Данные в дашборд загружаются через Центр данных Одного озера подключением к семантическим моделям предыдущих дашбордов. Показатели на «Карте здоровья» подсвечиваются цветами, в зависимости от значений.
Общая схема источников данных:
Заключение
Все эти дашборды объединил в приложение, которым удобно пользоваться, так как основные показатели выведены в одном окне. При необходимости легко можно просмотреть более детально по каждому показателю.
ссылка на оригинал статьи https://habr.com/ru/articles/827042/
Добавить комментарий