Столкнувшись со шквалом задач разной степени важности, 3 года назад я принял решение начать записывать задачи в планер/to do list который было бы удобно вести и с телефона, и с ноутбука. Выбор пал на Notion, как на популярную межоперационную платформу. За время использования планера было выполнено множество разных задач, и стало интересно провести некоторый анализ того, как и на что уходило время.

1. Устройство Notion планера

Notion планер является одной большой страничкой Database с форматом просмотра By Status, наполненной задачами, реализованными как page. Именно Status является ключевой фичей, характеризующей то, в каком состояние находиться каждая задача.

Для удобства статусы у меня принимают разные значение, главные 6 из которых это:

  • Top Priority для короткой задачи требующей незамедлительного внимания

  • Goal как долгая задача/цель, требующая последовательного подхода, но приводящая к серьезному результату

и соответвующие им 4 скрытых статуса:

  • Succesfully accomplished / Unaccomplished

  • Achieved Projects / Unachieved Projects.

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

Общий вид планера. Как можно догадаться из названия, проект начинался как реализация GTD.
Общий вид планера. Как можно догадаться из названия, проект начинался как реализация GTD.

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

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

Некоторые задачи зацензурены в силу деловой этики
Некоторые задачи зацензурены в силу деловой этики

2. Наработка данных

Выгрузить данные из Notion можно разными способами. Первый, и наверное самый простой способ, это скачать их напрямую через сайт. Так можно наработать .csv файл со всеми основными фичами. Второй способ, это использовать официальный API Notion. Мне нравиться именно второй способ, т.к помимо базовых фич страничек в планере, он умеет парсить и два не менее важных показателя: created_time, last_edited_time.

Для парсинга использовался python + requests, за выгрузку в .csv отвечал pandas

Для взаимодействия с официальным API Notion необходимо создать специальный токен. Как это делается детально описанно в спойлере:

Создание секретного токена API Notion

Для получения уникального ключа для API необходимо создать интеграцию по ссылке

Вот тут вам нужно на Create new integration
Вот тут вам нужно на Create new integration

Создав интеграцию и получив код:

Его нужно подключить к страничке с планером. Перед этим рекомендую установить вашей интеграции какую-то узнаваемую картинку, так чтобы не спутать с чужой при добавлении. Для этого на странице с планером жмем на 3 точки, потом на Connect to, где в выпадающем списке ищем нашу интеграцию.

Вот так выглядит интеграция после добавления
Вот так выглядит интеграция после добавления

Получив токен, смело заполняем headers для requests:

headers = {
    "Authorization": "Bearer " + "YOUR TOKEN",
    "Content-Type": "application/json",
    "Notion-Version": "2022-02-22"
}

Стоит отметить, что "Notion-Version" должен быть строго определенный, если с ним ошибиться, с сайта вернеться с ошибкам с указанием на возможные верные версии.

Парсинг выполняется с помощью requests.request('POST', url, json=payload, headers=headers), где url это адресс на базу данных, а payload указывает с какой page нужно записывать. По умолчанию Notion выдает максимум 100 page, что можно решить простым циклом.

Полная функция парсера db
def readEntireDatabase(databaseID, headers):
    url = f"https://api.notion.com/v1/databases/{databaseID}/query"
    page_size = 100 
    payload = {"page_size": page_size}
    
    response = requests.request('POST', url, json=payload, headers=headers)
    data = response.json()
    results = data["results"]
    
    while data["has_more"]:
        payload = {"page_size": page_size, "start_cursor": data["next_cursor"]}
        response = requests.request("POST", url, json=payload, headers=headers)
        data = response.json()
        results.extend(data["results"])
    return results

Полученный файл содержит себе множество json'ов страничек, у каждой из которой в наличие следующие данные (ключи словаря):

['object', 'id', 'created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'cover', 'icon', 'parent', 'archived', 'in_trash', 'properties', 'url', 'public_url']

Из них особенный интерес для меня представяют created_time, last_edited_time, in_trash, а также основной словарь лежащий по ключу properties. Именно он содержит все основые фичи страничек, в том числе и название со статусом.

Из json основные фичи вытаскиваются следующим образом (try-except соотвествует случаю когда нужное свойство не было указано в Notion):

if not page['in_trash']:
    try:
        names.append(page['properties']['Name']['title'][0]['plain_text']) 
    except:
        names.append('')
        
    try:
        statuses.append(page['properties']['Status']['select']['name']) 
    except:
        statuses.append('')

    try:
        tags.append(page['properties']['Tags']['multi_select'])
    except:
        tags.append('')
        
    try:
        dates.append(page['properties']['Date']['date'])
    except:
        dates.append('')

Собрав из полученных листов pd.DataFrame и сохранив в .csv можем переходить к анализу.

3. Анализ данных и визуализация

Для анализа полученных данных воспользуемся datetime, nltk и pymorphy2. Для визуализации и построения графиков matplotlib и wordcloud.

3.1 Временной анализ

Для начала разберемся с датами полученными из created_by и last_edited_by. Записываются они в нечитабельном формате 2024-07-06T18:18:00.000Z. Обрежем ненужное количество значащих цифр для миллисекунд с правого конца и заменем символы T на - для consistency

def format_datetime(string : str):
    return string[:-5].replace('T', '-')

Такой формат даты уже можно считать с помощью datetime.

datetime.strptime(date_str, '%Y-%m-%d-%H:%M:%S')

Переписав в pd.DataFrame колонки CREATION_TIME и EDITION_TIME в читабельном формате и получив их datetime репрезентации можем немного поиграть с анализом данных, посмотрев на то, как выглядит распределение решенных/заваленных задач на масштабах в дни/месяцы/годы.

Построив распредление задач от месяца можно увидеть закономерный провал летом, и в принципе ожидаемый подъем в апреле и ноябре (эти два пика совпадают с максимумами учебной нагрзуки в институте). Подсчитав среднее время выполнения и провала (изменения статуса на Unaccomplished) задачи получились соотвественно 13 и 57 дней.

Распределение количеств задач созданных и оконченных (статус Succesfully accomplished) от месяца в году
Распределение количеств задач созданных и оконченных (статус Succesfully accomplished) от месяца в году
Код построения графика
creation_months = np.zeros(12)
completion_months = np.zeros(12)

for date in data[data['STATUS'] == 'Succesfully accomplished'].CREATION_TIME:
    creation_months[datetime.strptime(date, '%Y-%m-%d-%H:%M:%S').month - 1] += 1

for date in data[data['STATUS'] == 'Succesfully accomplished'].EDITION_TIME:
    completion_months[datetime.strptime(date, '%Y-%m-%d-%H:%M:%S').month - 1] += 1
  
plt.figure(figsize=(7, 5))
plt.style.use('dark_background')
plt.bar(x=np.arange(1, 13), height=creation_months, color='white', label='начато')
plt.bar(x=np.arange(1, 13), height=completion_months, color='lightgray', label='сделано', alpha=0.5)
plt.xlabel('Месяц',fontsize=24)
plt.xticks(np.arange(1, 13)[::2], fontsize=14)
plt.ylabel('Кол-во задач',fontsize=24)
plt.yticks(np.arange(0, 151, 30), fontsize=14)
plt.legend(fontsize=14)
plt.tight_layout()
plt.savefig('months_tasks.jpg')

На масштабе в 4 года, видна явная тендция роста количества заверешнных задач (и проектов) от года к году. За начало 2024 года выполнено в 2.9 раз больше задач и в 3.33 раза больше проектов чем за соответствующий период 2023-го.

Количество оконченных задач в году
Количество оконченных задач в году
Код построения графика
# data_years_i полученно как value_counts to_dict()
fig, ax = plt.subplots(1, 2, figsize=(20, 7))
plt.rcParams.update({'font.size': 20})

ax[0].barh(width=list(data_years_1.values()), y=list(data_years_1.keys()), color='white')
ax[0].set_yticks([key for key in sorted(data_years_1.keys())])#, fontsize=20)
ax[0].set_xticks(range(0, 451, 90))#, fontsize=20)
ax[0].set_ylabel('Год', fontsize=24)
ax[0].set_xlabel('Количество задач', fontsize=24)

ax[1].barh(width=list(data_years_2.values()), y=list(data_years_2.keys()), color='white', )
ax[1].set_yticks([key for key in sorted(data_years_2.keys())])#, fontsize=20)
ax[1].set_xticks(range(0, 11, 2))#, fontsize=20)
ax[1].set_ylabel('Год', fontsize=24)

Самым приятным глазу, конечно же, является карта активности за целый год, постоенная в маштабе дней (источником вдохновения послужил GitHub).

Код построения графика
data_for_map = sc_data[sc_data['year'] == 2024]

activity = np.zeros((7, 53))

for day in np.arange(7):
    for week in np.arange(53):
        activity[day][week] += len(data_for_map[data_for_map['week'] == week][data_for_map['weekday'] == day + 1])

fig, ax = plt.subplots(figsize=(14, 11))
ax.set_aspect("equal")

plt.style.use('dark_background')
orig_map         = plt.cm.get_cmap('Grays') # инверсия чтобы maximum совпадал с белым 
reversed_map = orig_map.reversed() 

plt.title('Карта активности 2024', fontsize=24)
plt.pcolormesh(activity, cmap=reversed_map, edgecolor="w")
plt.xticks(np.arange(0, 53 ,5), fontsize=16)
plt.yticks(np.arange(0, 7, 2), fontsize=16)
plt.ylabel('Номер дня', fontsize=20)
plt.xlabel('Номер недели', fontsize=20)

3.2 Текстовый анализ

Проведем первичный анализ текстов задач. Посчитаем: 1) среднее числов слов в задаче, 2) среднее число символов в задаче и 3) средню длину слова.

sc_data = data[data['STATUS'] == 'Succesfully accomplished']

average_number_words = sc_data.NAME.str.split().str.len().mean()
average_number_symbols = sc_data.NAME.str.replace(' ', '').str.len().mean() # пробелы не считаем

def avg_word_length(sentence : str):
    words = sentence.split()
    return (sum(len(str(word)) for word in words) / len(words))

average_word_length = sc_data.NAME.apply(lambda x: avg_word_length(str(x))).mean()

В решенных задачах в среднем 4.2 слова и 24 символа. Средняя длина слова 6.7 букв.

Для анализа текстов задач проведем следующий препроцессинг.

  1. Заменим все знаки препинания на пробелы

  2. Приведем все слова к нижнему регистру

  3. Разобьем предложение на списки, а все списки объединим в один

  4. Удалим все слова с длиной меньше 4

Код препроцессинга. Часть 1
def clear_punctuation(string : str):
    for punct in ['.', ':', ',', '!', '?', ';', '\\', '/', '-', ')', '(']:
        string = string.replace(punct, ' ')
    return string

def filter(text : str):
    filtered = [word for word in text if len(word) > 3]
    return filtered

text = sc_data.NAME.astype(str)
text = text.apply(lambda x: clear_punctuation(x))
text = text.str.lower()
text = ' '.join(text.to_list()).split() 
text = filter(text)

Полученный список слов можно поместить в pd.Series и используя value_counts() получить словарь с частотностью. Полученный словарь красиво представить в качестве wordcloud:

Wordcloud чашка с кофе для сделанных дел
Wordcloud чашка с кофе для сделанных дел
Код Wordcloud
mask = np.array(Image.open('coffee.jpg'))
wordcloud = WordCloud(mask=mask).generate(' '.join(text))

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 7))
plt.imshow(wordcloud.recolor(color_func=grey_color_func, random_state=3),
           interpolation="bilinear")
plt.axis("off")
plt.savefig('completed.png')

И из картинки, и из словаря частот видно, что самыми популярным словами являются глаголы, призывающие к незамедлительному действию. И вместо того, чтобы удалять самые частые слова, можно определить для каждого слова его часть речи (c помощью pymorphy2), и удалить все ненужные.

Итого, добавим в препроцессинг еще два этапа:

  1. Удаление стоп-слов

  2. Приведение к начальной форме и удаление глаголов

Код препроцессинга. Часть 2
from nltk.corpus import stopwords
import nltk
import pymorphy2

morph = pymorphy2.MorphAnalyzer()

# Create a set of stop words 
stop_words = set(stopwords.words('russian')) 

def remove_stop_words(sentence : list, filter_array=stop_words): 
  filtered_words = [word for word in sentence if word not in filter_array] 
  return filtered_words

def filter_verbs(text: list):
    filtered = [word for word in text if morph.parse(word)[0].tag.POS != 'INFN' and morph.parse(word)[0].tag.POS != 'VERB']
    return filtered

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

Wordcloud мозг сделанных существительных
Wordcloud мозг сделанных существительных

Самые популярные слова: неделя, билет, лекция, статья.

4. Планы на будущее

Продолжая темы, поднятые в этом мини-проекте, можно выделить два основных возможных направления развития идеи.

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

Второе - это работа с лингвистическими моделями для семантической классификации проделанных задач на категории, к примеру на науку (мои подкатегории были бы физика и ML) искусство (кино и литература), здоровье и т.д
С такой задачей справился бы слегка доубученный BERT, но я не нашел датасетов с задачами по типу to-do, а среди моих 1500 задач, размечено от силы 60 (и то ~20 это reading и ~40 Stuying), так что оставим это на будущее.

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


  1. digtatordigtatorov
    19.07.2024 22:16
    +2

    Obsidian? Почему не он? Вот тут есть простор полета фантазий. Разобраться в графах, вытянуть много интересного.


    1. mc2
      19.07.2024 22:16

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


      1. Ryav
        19.07.2024 22:16
        +2

        Syncthing для вас шутка какая-то?


        1. NibiruanChild
          19.07.2024 22:16

          Костыль. За неимением другого пользуюсь для обсидиан, но много раз подводил

          1. Из-за конфликта версий не синхронились отдельные заметки

          2. Из-за режимов экономии энергии на мобильных устройствах не синхронил. Дописал заметку, закрыл ноутбук и ушел, а синка не произошло. А именно последние заметки чаще всего нужны

          3. Вне дома когда устройства хоть и с интернетом, но не в одной сети, синхронизации нет.

          4. Если надо открыть на устройстве, на котором был не в сети на момент написания заметки. У меня есть походные ноутбук и планшет, которые беру в командировки например. Если забыл синкнуть перед поездкой, то все.


          1. Ryav
            19.07.2024 22:16

            У меня ни одной проблемы не было за всё время использования, правда у меня есть медиа-сервер, который 99% времени в сети и туда в любом случае всё долетает, ну а с него уже и другие устройства могут забрать.

            По 3 должно быть без разницы, хоть за NATом у вас устройства.

            А по 4 вообще никакого решения нет. Как вы без сети синхронизируете?


            1. NibiruanChild
              19.07.2024 22:16

              4 у того же ноушн решается загрузкой на их сервера. Потому и говорю что синкфин увы костыль, хоть многим и хватает


              1. rustavelli
                19.07.2024 22:16

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


                1. NibiruanChild
                  19.07.2024 22:16

                  Всего лишь :D

                  Я же и говорю что костыль. Я не сказал что костыли не могут работать, но это костыль.

                  Хотя идеология обсидиан вся такая. Из коробки им пользоваться невозможно и надо тратить много времени на допил под себя. Иногда вместо того чтобы пользоваться.

                  Увы, я признаю что обсидиан одно из лучших решений. Сам на него ушел с ноушена. Ок быстрый и локальный, ссылки и поиск очень удобные. Но я абсолютно не понимаю тех кто его идеализирует, потому что у него огромная куча недостатков и недоработок до идеального инструмента.


              1. Ryav
                19.07.2024 22:16

                Так если вы так и так синхронизируете (сеть есть), то и через Syncthing с другим устройством проблем не вижу. Если у вас устройства одновременно в сети не могут быть, то добавьте ещё одно, которое всегда будет в сети (та же VPSка, например, или старый телефон, который всегда дома включённый и подключённый к сети лежит).


      1. LeshaRB
        19.07.2024 22:16
        +2

        В платной версий там есть синхронизация


    1. NibiruanChild
      19.07.2024 22:16

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


  1. dyadyaSerezha
    19.07.2024 22:16
    +3

    1) Не понял смысл анализа. Ну выяснилось, что задачи выполняются за 13, а фейлятся за 57 дней - и что? Что дальше? Какой из этого полезный вывод? А какая польза от среднего кол-ва слов и прочего? Не понимаю. И все графики на мой взгляд абсолютно бесполезные, так как не ведут ни к какой корректировке себя или к новому осознанию. Ну разве что процент неудачных дел хоть что-то бы показал, точнее, его изменение во времени. Но даже это для меня лично было бесполезной инфой.

    2) вместо лингвистических моделей в миллион раз проще ставить тэг для каждой задачи.


    1. hK04 Автор
      19.07.2024 22:16
      +2

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

      К примеру, распределение слов только за 2024 (после лингвистического анализа) дало забавные результаты: наиболее популярное словосочетание это "Аналитическая механика" (и соответсвующее ему наибошьшее количество потраченного времени), за которую у меня наименьший результата в учебную сессию. Вполне себе полезное знание о том, как не надо распределять время.

      2) Не считаю, что ставить тэги проще. По крайней мере для задач, которые надо делать оперативно.


      1. dyadyaSerezha
        19.07.2024 22:16

        Если уж про прогу, то 4 похожих try-except подряд, это очень плохой стиль. В цикле гораздо лучше.

        А написать короткий тэг в дополнение к названию, приоритету и датам задачи, это очень просто, по-моему.


  1. Kahelman
    19.07.2024 22:16

    Больше графиков- богу графиков.

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

    Лет десять сидел на Evernote - premium последние лет пять переехал на Joplin. Основная проблема Evernote -его превратили в тормозной маркетинговый ужас.

    Joplin тоже периодически косячит - отваливаются attached документы. Но в общем жить можно.

    Только хардкор- только текст :)


  1. Aquahawk
    19.07.2024 22:16
    +1

    Obsidian, 2 с копейками года, 1643 заметки, 1670 ссылок. Синхронизирую syncthing. На телефоне начал подтормаживать при открытии obsidian. Но вообще штука мега крутая. Думал что страницы без ссылок это проблема, но пока вроде как нет. Я гораздо чаще ищу по базе по именам (и алиасам) или полнотекстовым поиском, чем перемещаюсь по линкам. Что интересно графовый вид тоже бесполезен, только вот такие картинки показывать.