Картинка сгенерирована с помошью Midjourney по запросу «Pandas and AI chat bot analyze data together»
Картинка сгенерирована с помошью Midjourney по запросу «Pandas and AI chat bot analyze data together»

Привет, Хабр!

С вами Data Scientist и участник профессионального сообщества NTA Максим Алёшин.

Всего за несколько месяцев существования чат‑бот ChatGPT обрёл необычайную популярность. Пожалуй, новости, связанные с этой технологией, уже изрядно всех утомили и мало чем удивляют. Но стоит отдать должное, большое число приложений на основе языковых моделей действительно завораживает: чат‑боты стали использовать буквально во всём, от создания контента до написания программного кода.

Вот уже постепенно появляются библиотеки для Python, позволяющие использовать подобные чат‑боты прямо из Jupyter Notebooks или IDE. Сравнительно недавно вышел релиз библиотеки Sketch, которая предоставляет функции интеллектуального помощника по написанию кода для пользователей Pandas. Он, помимо прочего, понимает контекст данных, значительно повышая релевантность предложений. Библиотека доступна в PyPi и готова к использованию сразу после установки.

Навигация по посту

Что за Sketch?

Выражаясь простыми словами, библиотека Sketch — это чат‑бот для Pandas. При его использовании отсутствует необходимость подключения дополнительных плагинов. Авторы объясняют, что в библиотеке применяются эффективные алгоритмы аппроксимации для быстрого обобщения данных и подачи этой информации в языковые модели. Разработчики не говорят, какая именно модель применяется в библиотеке, но использование API OpenAI наводит на мысли о том, что это вероятнее всего ChatGPT.

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

Методы библиотеки

После импорта Sketch для любого Pandas‑датафрейма становится доступно расширение.sketch. Через него можно обратиться к одному из трёх методов: .sketch.ask, .sketch.howto и .sketch.apply.

  • ask – функция, работающая по принципу «вопрос-ответ», возвращает результат в виде текста, основанного на сводной статистике и описании данных. Используется для описания данных, получения советов по переименовыванию столбцов и по действию над ними.

  • howto – функция, дающая рекомендации по написанию кода. Она возвращает блок кода, который можно скопировать и использовать в качестве «отправной точки» (как выражаются сами разработчики) для написания необходимого алгоритма в ответ на вопрос по данным, переданный в функцию. Например, можно спросить, как заполнить пропуски, нормализовать данные, создать новые признаки, построить график или даже построить модели.

  • apply – более продвинутая система ввода вопросов, которая больше подходит для генерации данных. Может использоваться для парсинга полей, создания новых признаков и многого другого. Для её использования необходимо иметь бесплатный OpenAI аккаунт, поскольку она работает только с личным API ключом.

Первое, что стоит понимать при использовании библиотеки — она передаёт языковой модели не всю информацию о датасете, а обобщённые данные (скетчи), поэтому не стоит рассчитывать, что с её помощью получится точно вычислить какие‑либо числовые характеристики, получить результат агрегирования данных и иных операций. Кроме того, модель работает на удалённом сервере разработчиков, и из этого тоже вытекают некоторые ограничения. Далее я это продемонстрирую.

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

Иллюстрация работы методов библиотеки Sketch
Иллюстрация работы методов библиотеки Sketch

Здесь я рассмотрю две доступные функции:.ask и.howto. Все запросы буду писать по‑русски, чтобы оценить качество работы на родном языке. Далее продемонстрирую наиболее показательные запросы к боту. Больше результатов тестов, не вошедших в эту публикацию, можете найти в Jupyter‑ноутбуке по ссылке.

Импорт библиотек и загрузка данных

Сначала импортируем Sketch и Pandas.

import sketch
import pandas as pd

В качестве данных для экспериментов беру информацию о дорожно‑транспортных происшествиях из открытого ресурса «Карта ДТП». На ресурсе содержатся довольно подробные данные, включая информацию о состоянии дорожного покрытия, погодных условиях, водителях, пассажирах и прочее.

В датасете содержится более 1,2 миллиона записей о ДТП, произошедших в России с начала 2015 года по февраль 2023 года.

Загружаю предобработанный набор данных:

df = pd.read_parquet('dtp_ru.parquet')
df.sample(4)

После этого можно применять методы Sketch к датасету. Ниже информация о нём:

df.info()

Результат:

RangeIndex: 1281351 entries, 0 to 1281350
Data columns (total 15 columns):
 #   Column                  Non-Null Count    Dtype         
---  ------                  --------------    -----         
 0   datetime                1281351 non-null  datetime64[ns]
 1   lat                     1270607 non-null  float64       
 2   lon                     1270607 non-null  float64       
 3   parent_region           1281351 non-null  object        
 4   region                  1281351 non-null  object        
 5   address                 1223853 non-null  object        
 6   category                1281351 non-null  object        
 7   severity                1281351 non-null  object        
 8   vehicles                1281351 non-null  object        
 9   participant_categories  1281351 non-null  object        
 10  participants_count      1281351 non-null  int64         
 11  injured_count           1281351 non-null  int64         
 12  dead_count              1281351 non-null  int64         
 13  light                   1281351 non-null  object        
 14  weather                 1281351 non-null  object        
dtypes: datetime64[ns](1), float64(2), int64(3), object(9)
memory usage: 146.6+ MB

«.ask the question», или задаю вопросы

.ask

ask – функция, работающая по принципу «вопрос-ответ», возвращает результат в виде текста, основанного на сводной статистике и описании данных. Используется для описания данных, получения советов по переименовыванию столбцов и по действию над ними.

«Что по данным?» и замер скорости получения ответа

Прошу описать данные:

df.sketch.ask('Опиши датасет')

Результат:

Failed to use remote https://prompts.approx.dev.. 414 Client Error: Request-URI Too Large for url: https://prompts.approx.dev/prompt/ask_from_parts?dfname=df&column_names=&data_types=&extras=&question=%D0%9A%D0%B0%D0%BA%D0%B8%D0%B5+%D0%BA%D0%BE%D0%BB%D0%BE%D0%BD%D0%BA%D0%B8+%D0%B5%D1%81%D1%82%D1%8C+%D0%B2+%D0%B4%D0%B0%D1%82%D0%B0%D1%81%D0%B5%D1%82%D0%B5%3F.
Consider setting SKETCH_USE_REMOTE_LAMBDAPROMPT=False
and run with your own open-ai key

В результирующем выводе я заменил значения некоторых параметров http запроса на <base64>, чтобы сократить его. Sketch отправляет запрос c информацией о датасете в виде base64 строки на сторонний сервер. Передаваемые переменные: column_names (имена колонок), data_types (типы данных), extras (посчитанные статистики и обобщённые данные) — приводятся к base64 строке. Если данные чересчур разнообразные, длина строки extras становится слишком большой. Проще говоря, может оказаться так, что столбцов в датасете с учётом формируемых для них скетчей больше, чем может вместиться в передаваемый на сервер модели запрос. Так, в данном случае суммарная длина url составила около 10 000 знаков, и Sketch выдал ошибку. Чтобы не прибегать к использованию API ключа, можно применять команды библиотеки к части датасета. В данном случае достаточно просто обращаться к чат‑боту после исключения из датасета единственного столбца. Демонстрирую, каким будет результат выполнения после удаления адреса:

df.drop('address',axis=1).sketch.ask("Опиши датасет")

Результат:

Датасет содержит информацию о 1281351 ДТП, произошедших в России. Он содержит следующие столбцы: index, datetime, lat, lon, parent_region, region, category, severity, vehicles, participant_categories, participants_count, injured_count, dead_count, light и weather. Index - это уникальный идентификатор каждого ДТП. Datetime - дата и время события. Lat и lon - широта и долгота события. Parent_region - регион, в котором произошло событие. Region - город, в котором произошло событие. Category - тип события (например, столкновение). Severity - уровень тяжести (легкий / средний / тяжелый). Vehicles - список участвующих автомобилей. Participant_categories - участники (водител

Теперь команда была выполнена успешно. Поскольку у модели есть ограничение также и на количество символов в ответе, он оборвался — всё как у ChatGPT. В дальнейшем я буду иногда прибегать к такому приёму, оставляя для запроса только те столбцы, которые необходимы для ответа ботом на вопрос, но в тексте эти операции буду опускать для краткости.

Анализирую, как влияет размер выборки на скорость выполнения команды Sketch. Считаю общее время и процессорное. Код с подсчётом доступен в упомянутой тетрадке. Для этого выполняю тот же самый запрос на выборках разного размера случайных строк из датасета.

Время выполнения увеличивается с ростом размера датасета. При этом процессорное время растёт линейно, это связано с тем, что формирование скетчей (обобщение данных) производится на локальном компьютере, и эти алгоритмы имеют линейную сложность. Общее же время работы команды, помимо предобработки данных, включает в себя отправку запроса на сервер, работу языковой модели на нём, получение ответа. Очевидно, что время операции в таком случае сильно зависит от скорости соединения, и действительно, большую часть времени составляет не процессорное. Поэтому и наблюдаются колебания на графике зависимости общего времени выполнения, и тренд, совпадающий с ростом процессорного времени. Помимо этого, замечу, что количество строк, в отличие от количества столбцов, в датасете не влияет на длину url‑запроса, и как следствие, на возможность получения ответа.

Для дальнейшего анализа ограничусь записями только для четырёх регионов: Амурской области, Еврейской автономной области, Хабаровского края и Сахалинской области.

df.sketch.ask("Опиши датасет")

Результат:

Датасет содержит информацию о 30662 ДТП, произошедших в Хабаровском крае. Он содержит следующие столбцы: index, datetime, lat, lon, parent_region, region, category, severity, vehicles, participant_categories, participants_count, injured_count, dead_count, light и weather. Типы данных различны: int64, datetime64[ns], float64 и object. В столбцах index, parent_region, region, category, severity, participant_categories и light уникальные значения соответственно 30662, 4, 61, 16, 3, 21 и 6

Не стоит доверять боту безоговорочно: он решил, что в датасете информация только о ДТП в Хабаровском крае. Проверяю эту информацию с помощью Pandas:

df.parent_region.value_counts()

Результат:

Хабаровский край                14362
Амурская область                 9546
Сахалинская область              5017
Еврейская автономная область     1737
Name: parent_region, dtype: int64

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

«А вот отсюда поподробней!»: конкретизирую вопросы

Интересно, может ли Sketch выделить категорию данных по косвенным признакам:

df.sketch.ask("Какие географические данные есть в датасете?")

Результат:

В датасете есть географические данные, включая широту (lat), долготу (lon), родительский регион (parent_region) и регион (region).

Похоже, Sketch действительно понимает контекст данных.

Теперь проверяю, как Sketch справляется с задачами определения значений, вычисления агрегирующих значений. Для начала прошу вывести уникальные значения сразу двух колонок:

df.sketch.ask("Какие уникальные значения у столбцов category, severity?")

Результат:

Столбец category имеет 16 уникальных значений: Отбрасывание предмета, Столкновение, Наезд на препятствие, Наезд на пешехода, Наезд на животное, Опрокидывание, Протаранил, Вылет с дороги, Налёт, Противоречия свидетельских показаний, Другое, Налёт со стороны, Налёт сзади, Налёт спереди, Обгон. Столбец severity имеет 3 уникальных значения: Легкий, Средний, Тяжёлый.

Проверяю с помощью Pandas:

print(f'Уникальные значения category\n{df.category.unique()}')
print(f'Уникальные значения severity\n{df.severity.unique()}')

Результат:

Уникальные значения category

['Отбрасывание предмета' 'Столкновение' 'Наезд на стоящее ТС' 'Наезд на пешехода'
 'Опрокидывание' 'Наезд на велосипедиста' 'Съезд с дороги' 'Иной вид ДТП'
 'Наезд на препятствие' 'Наезд на внезапно возникшее препятствие' 'Падение пассажира' 'Наезд на животное' 'Падение груза' 'Наезд на лицо, не являющееся участником дорожного движения, осуществляющее производство работ' 'Наезд на лицо, не являющееся участником дорожного движения, осуществляющее несение службы'  'Наезд на лицо, не являющееся участником дорожного движения, осуществляющее какую-либо другую деятельность']

Уникальные значения severity

['Легкий' 'Тяжёлый' 'С погибшими']

Похоже, этот бот не всегда честен: количество уникальных значений определено верно, однако для колонки category вместо указанных 16 значений перечислено 15, а сами значения совпадают с реальными лишь в половине случаев. Такое различие в значениях очевидно не может быть связано только с трудностями перевода.

«Дай совет, по-братски», и что Sketch думает о названиях колонок

Попрошу дать советы по разным вопросам:

df.sketch.ask('Как заполнить пропуски в столбце адреса, восстановить улицу и номер дома?')

Результат:

Для заполнения пропусков в столбце адреса и восстановления улицы и номера дома, необходимо использовать сервисы геокодирования, такие как Google Maps API, Yandex Maps API, OpenStreetMap API. Эти сервисы предоставляют API, которые можно использовать для поиска улицы и номера дома, связанных с указанными широтой/долготой.

df.sketch.ask("Как визуализировать ДТП на карте?")

Результат:

Для визуализации ДТП на карте можно использовать библиотеку Folium. Она позволяет создавать интерактивные карты, на которых можно отмечать различные события. Для этого нужно создать словарь, содержащий данные из dataframe df, связанные с координатами (lat, lon). Затем этот словарь можно использовать для создания Folium-карты, на которой будут отмечены расположения различных событий.

df.sketch.ask("Предложи, как переименовать столбцы (результат в html формате)")

Результат:

Sketch даёт релевантные советы: может предложить способ решения задачи и инструмент. В целом, оставляет впечатление продвинутой версии функции «Мне повезёт!» в поисковике: нет необходимости перебирать разные результаты выдачи, так ещё и запрос составить намного проще, не приходится думать, как в поисковом запросе описать особенности датасета — бот сам их учитывает.

«Доставай калькулятор»: прошу Sketch посчитать

Прошу Sketch вычислить некоторую статистику:

df.sketch.ask('Какие минимальные и максимальные, средние и медианные значения у численных столбцов?')

Результат:

index: min=0.0, max=30661.0, mean=15330.5, median=15330.5 lat: min=46.0042, max=60.087426, mean=48.814077, median=48.971184 lon: min=120.166667, max=156.1431, mean=135.311845, median=135.078009 participants_count: min=1.0, max=58.0, mean=2.837862, median=2.0 injured_count: min=0.0, max=43.0, mean=1.097881, median=1.0 dead_count: min=0.0, max=15.0, mean=0.252041, median=0.0 ``

Вывод удобен разве что для копирования в отчёт, не очень читаем. Снова проверяю:

df.describe()

Результат:

Все значения, кроме средних, совпали, да и в средних значениях расхождения невелики. Неужели Sketch умеет вычислять медиану, но не умеет среднее? На самом деле это связано с тем, что квантили вычисляются библиотекой до обобщения данных, а средние же значения искажаются из‑за алгоритмов формирования скетчей.

Ещё некоторые примеры результатов выполнения команды:

df.sketch.ask("Сколько ДТП с участием пешеходов")

Результат:

Для получения ответа на данный вопрос необходимо проанализировать столбец `participant_categories` датафрейма `df`. В этом столбце указаны категории участников ДТП. Если в строке указано "Пешеходы", то это означает, что в этом ДТП участвовали пешеходы. Для подсчета количества таких ДТП можно использовать следующую строку: df[df['participant_categories'].str.contains('Пешеходы')].shape[0]

df.sketch.ask("Сколько ДТП произошло в тёмное время суток и в светлое время суток?")
df.light.value_counts()

Результат:

В тёмное время суток произошло 14,845 ДТП, а в светлое время суток - 15,817 ДТП.

Светлое время суток                            19465
В темное время суток, освещение включено        6362
В темное время суток, освещение отсутствует     3876
Сумерки                                          643
В темное время суток, освещение не включено      313
Не установлено                                     3
Name: light, dtype: int64
df.sketch.ask("Сколько ДТП с пострадавшими?")
df.sketch.ask("Сколько строк со значением `injured_count` больше 0?")
df.sketch.ask("How many rows with `injured_count` greater than 0?")
print('ДТП с пострадавшими:', (df.injured_count > 0).sum())

Результат:

В данном датасете всего 30662 ДТП, из которых 21 содержит пострадавших.

В данном датафрейме есть 21 строка со значением `injured_count` больше 0.

There are 14,845 rows with an injured_count greater than 0.

ДТП с пострадавшими: 28915

Как видно, иногда модель вместо ответа на вопрос даёт инструкцию о том, как его получить. И это более надёжно, чем её расчёты! Во всех остальных случаях она соврала. Что примечательно: в случае, если задавать вопрос на английском, родном для модели языке, ответ отличается, однако по‑прежнему неверный.

Впрочем, как и пишут разработчики, функцию.ask() лучше использовать для получения советов. Ввиду принципа работы алгоритма обобщения данных ответы на вопросы с вычислением каких‑либо значений на основе данных из датасета заведомо не могут быть верными, кроме конечно же некоторых статистик (минимальные и максимальные значения, квантили), ведь они вычисляются отдельно и передаются в модель. Хотя Sketch и не может сам рассчитать некоторые величины, он может подсказать, как это сделать. Его просто нужно правильно спросить, об этом и расскажу дальше.

«.howto write the code», или зачем пандам Stack Overflow, если есть Sketch?

.howto

howto – функция, дающая рекомендации по написанию кода. Она возвращает блок кода, который можно скопировать и использовать в качестве «отправной точки» (как выражаются сами разработчики) для написания необходимого алгоритма в ответ на вопрос по данным, переданный в функцию. Например, можно спросить, как заполнить пропуски, нормализовать данные, создать новые признаки, построить график или даже построить модели.

Всё же самая интересная и полезная функция Sketch – это подсказки по написанию кода для манипуляций с датасетами Pandas. При этом можно задавать вопросы не только по работе методов Pandas, но и других библиотек. В запросе можно конкретно указать, какой библиотекой пользоваться. Но особенно интересно, что Sketch может сам указать, какую библиотеку необходимо подключить, чтобы получить желаемый результат. При этом, как уже говорилось ранее, ответ этой функции чаще всего не является готовым алгоритмом, в некоторых случаях его придётся изменять, чтобы получить рабочий код. Ниже несколько примеров.

«Как фичей добавить?»: генерация новых признаков

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

df.sketch.howto('Сгенерируй новые признаки')

Результат:

# Create a new column with the day of the week
df['day_of_week'] = df['datetime'].dt.day_name()

 # Create a new column with the hour of the day
df['hour_of_day'] = df['datetime'].dt.hour

 # Create a new column with the month of the year
df['month_of_year'] = df['datetime'].dt.month_name()

Sketch предлагает самый очевидный вариант — создание новых признаков на основе поля даты. Если меня не устраивает, например, что вместо номера дня недели предлагается его название, я могу просто изменить код или же уточнить, какие именно признаки хочу получить:

df.sketch.howto('Сгенерируй новые признаки: номер месяца, номер дня недели, час, число месяца')

Результат:

# Create new features
df['month'] = df['datetime'].dt.month
df['day_of_week'] = df['datetime'].dt.dayofweek
df['hour'] = df['datetime'].dt.hour
df['day_of_month'] = df['datetime'].dt.day

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

df.datetime = df.datetime.astype(str)
df.sketch.howto('Сгенерируй новые признаки')

Результат:

# Create a new column with the number of participants per vehicle
df['participants_per_vehicle'] = df['participants_count'] / df['vehicles'].str.len()

# Create a new column with the ratio of injured to dead
df['injured_to_dead_ratio'] = df['injured_count'] / df['dead_count']

# Create a new column with the ratio of participants to injured
df['participants_to_injured_ratio'] = df['participants_count'] / df['injured_count']

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

df.sketch.howto('Сгенерируй новые признаки (поле datetime необходимо перевести из строки в datetime64)')

Результат:

# Convert the datetime column from string to datetime64
df['datetime'] = pd.to_datetime(df['datetime'])

# Create new features from the datetime column
df['year'] = df['datetime'].dt.year
df['month'] = df['datetime'].dt.month
df['day'] = df['datetime'].dt.day
df['hour'] = df['datetime'].dt.hour
df['minute'] = df['datetime'].dt.minute

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

print(df.weather.unique())

Результат:

['Ясно' 'Дождь' 'Пасмурно' 'Снегопад' 'Метель' 'Пасмурно, Дождь' 'Ясно, Туман' 'Пасмурно, Снегопад' 'Снегопад, Метель' 'Дождь, Снегопад' 'Туман' 'Дождь, Ураганный ветер' 'Туман, Температура ниже -30С' 'Пасмурно, Туман' 'Ясно, Температура ниже -30С' 'Дождь, Туман' 'Снегопад, Температура ниже -30С' 'Пасмурно, Температура ниже -30С' 'Ясно, Дождь' 'Ясно, Температура выше +30С' 'Пасмурно, Метель' 'Пасмурно, Ураганный ветер' Пасмурно, Температура выше +30С' 'Метель, Ураганный ветер']

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

df.sketch.howto('Сгенерируй новые логические признаки из всех возможных погодных условий в поле `weather`')

Результат:

# Create a new column for each weather condition
df['weather_clear'] = df['weather'].apply(lambda x: 1 if x == 'Ясно' else 0)
df['weather_rain'] = df['weather'].apply(lambda x: 1 if x == 'Дождь' else 0)
df['weather_snow'] = df['weather'].apply(lambda x: 1 if x == 'Снег' else 0)
df['weather_fog'] = df['weather'].apply(lambda x: 1 if x == 'Туман' else 0)
df['weather_wind'] = df['weather'].apply(lambda x: 1 if x == 'Ветер' else 0)
df.sketch.howto('Сгенерируй новые логические признаки из всех возможных погодных условий в поле `weather` (погодные условия перечисляются через запятую)')

Результат:

# Create a list of all possible weather conditions
weather_conditions = df['weather'].str.split(',').tolist()
weather_conditions = [item for sublist in weather_conditions for item in sublist]
weather_conditions = list(set(weather_conditions))

# Create new boolean columns for each weather condition
for condition in weather_conditions:
    df[condition] = df['weather'].str.contains(condition).astype(int)

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

# Create a list of all possible weather conditions
weather_conditions = df['weather'].str.split(', ').tolist()
weather_conditions = [item for sublist in weather_conditions for item in sublist]
weather_conditions = list(set(weather_conditions))

# Create new boolean columns for each weather condition
for condition in weather_conditions:
    df[condition] = df['weather'].str.contains(condition).astype(int)

df[weather_conditions].sample(5)

Результат:

Это то, что нужно! Вообще говоря, инструкции для чат‑бота могут быть ещё более подробными. И чем более конкретными они будут, тем более релевантный ожиданиям результат будет получен.

«А такое знаешь, как считать?»: манипуляции с данными

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

df.vehicles.sample(5)

Результат:

150                           [None None None]
29443                      [TOYOTA Vista 1990]
2725                         [HONDA Logo 1999]
25595    [NISSAN Juke 1994, NISSAN Tiida 2009]
6093      [TOYOTA Crown 2002, YAMAHA XVS 2003]
Name: vehicles, dtype: object
df.sketch.ask('Какой минимальный и максимальный годы выпуска автомобилей (колонка vehicles имеет тип списка, год может быть неизвестен)?')

Результат:

Минимальный год выпуска автомобилей: 1986 Максимальный год выпуска автомобилей: 2018

df.sketch.howto(Определи минимальный и максимальный годы выпуска автомобилей (колонка vehicles имеет тип списка, год может быть неизвестен)?')

Результат:

# Get list of all vehicles
vehicles_list = df['vehicles'].tolist()

# Create empty list to store years
years_list = []

# Iterate through each vehicle in the list and extract the year from the string
for vehicle in vehicles_list:
    # Split the string on space
    vehicle_split = vehicle.split(' ')
    # Get the last item in the list which is the year
    year = vehicle_split[-1]
    # Append the year to the years_list if it is a number
    if year.isdigit():
        years_list.append(int(year))
        
# Calculate min and max years from the years_list
min_year = min(years_list)
max_year = max(years_list)

print('Min year:', min_year)
print('Max year:', max_year)
Применим полученный код
# Get list of all vehicles
vehicles_list = df['vehicles'].tolist()

# Create empty list to store all years
years_list = []

# Iterate through the list of vehicles and extract the year from each vehicle
for vehicles in vehicles_list:
    for v in vehicles:
        # Split the string on space to get the year
        year = v.split(' ')[-1]
        # Append the year to the list of years
        if year.isdigit():
            years_list.append(int(year))
        
# Calculate min and max years from the list of years
min_year = min(years_list)
max_year = max(years_list)

print('Min Year:', min_year)
print('Max Year:', max_year)

Результат:

Min Year: 1

Max Year: 2022

Снова команда ask вернула ложный ответ. Инструкция же по реализации алгоритма оказалась почти верной. Модель только не учла, что в каждой строке столбца vehicles находятся списки из авто, поэтому в последней ячейке кода я добавил вложенный цикл, по элементам таких списков. Значение 1 в минимальном годе выпуска — это ошибка в данных, а не в работе алгоритма.

Попробую получить ответ на более изощрённый вопрос:

df.sketch.howto("""
    Определить количество ДТП с участием машин марки `TOYOTA` до 2000 года выпуска,
    поле `vehicles` имеет тип списка, год может быть неизвестен
""")

Результат:

# Count the number of accidents involving Toyota vehicles before 2000
toyota_before_2000 = df[df['vehicles'].apply(lambda x: any('TOYOTA' in v and int(v.split()[-1]) < 2000 for v in x))].shape[0]
print(f'Number of accidents involving Toyota vehicles before 2000: {toyota_before_2000}')

Используем код:

# Count the number of accidents involving Toyota vehicles before 2000
toyota_before_2000 = df[df['vehicles'].apply(lambda x: 
    any('TOYOTA' in v and 
        (int(v.split()[-1]) < 2000 if v.split()[-1].isdigit() else False) 
    for v in x))].shape[0]
print(f'Number of accidents involving Toyota vehicles before 2000: {toyota_before_2000}')

Результат:

Number of accidents involving Toyota vehicles before 2000: 8961

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

Замечу, что иногда ответ модели зависит от того, как именно мы задаём вопрос. Например, запросы, начинающиеся с «Количество ДТП …», «Определить количество ДТП …», «Посчитай количество ДТП …» могут давать на выходе разный код с разными способами реализации алгоритма, но модель всегда старается ответить на поставленный вопрос.

Приведу ещё пару примеров:

df.sketch.howto('Количество ДТП с участием мотоциклистов')

Результат:

# Count the number of accidents involving motorcyclists
motorcycle_accidents = df[df['participant_categories'].str.contains('Мотоциклисты')].shape[0]
print(f'Number of accidents involving motorcyclists: {motorcycle_accidents}')
df.sketch.howto('На каком расстоянии от Москвы произошло ДТП?')

Результат:

# import necessary libraries
import pandas as pd
import numpy as np
from geopy.distance import geodesic

# define coordinates of Moscow
moscow_lat = 55.755826
moscow_lon = 37.6173

# calculate distance from Moscow for each row in the dataframe
df['distance_from_moscow'] = df.apply(lambda row: geodesic((row['lat'], row['lon']), (moscow_lat, moscow_lon)).km, axis=1)

# print the min and max distances from Moscow
print('Minimum distance from Moscow:', df['distance_from_moscow'].min())
print('Maximum distance from Moscow:', df['distance_from_moscow'].max())

Код, который сгенерировала модель, корректный. И самое главное, модель сама ищет нужную информацию (координаты Москвы в примере), приводит строки с импортом необходимых библиотек. Результат выполнения кода, сгенерированного по моему запросу, будет следующим:

Number of accidents involving motorcyclists: 2221
Minimum distance from Moscow: 4737.977084020733
Maximum distance from Moscow: 7127.686149630483

При этом я могу задавать абсолютно разные вопросы. Sketch ответит даже на такие как «Сколько ДТП произошло в день рождения Ленина?», «Какая фаза луны была в день ДТП?». Единственное ограничение: они должны быть связаны с информацией в датасете.

«Накодь, чтобы красиво было»: построение графиков

Приведу примеры запросов для построения графиков. Например, попрошу построить гистограмму:

df.sketch.howto("""Построить гистограмму количества пострадавших, 
                установить количество корзин равным максимальному значению""")

Запускаю сгенерированный код и смотрю на результат:

import matplotlib.pyplot as plt

# Get the max value of injured_count
max_injured_count = df['injured_count'].max()

# Create a histogram of the injured_count column with max_injured_count bins
df['injured_count'].hist(bins=max_injured_count)

# Set the title and labels for the histogram
plt.title('Injured Count')
plt.xlabel('Number of Injuries')
plt.ylabel('Frequency')

# Show the plot
plt.show()

Результат:

Тут Sketch сам предложил добавить подписи к графику, но делает он так не всегда, если боту прямо не указать это в запросе. Построю диаграмму рассеяния из координат с помощью seaborn, попрошу установить размер графика и некоторые другие параметры:

df.sketch.howto("""Построй координаты на графике с помощью seaborn 
    Цветом обозначь разные `parent_region`, 
    размер графика 12 на 9, размер точек пропорционально `participants_count`,
    подпиши названия осей и графика, добавь на график сетку,
    прозрачность точек 30 процентов""")

Запускаю сгенерированный код и смотрю на результат:

import seaborn as sns

# Set figure size
plt.figure(figsize=(12, 9))

# Plot the data
sns.scatterplot(x="lon", y="lat", hue="parent_region", size="participants_count", data=df, alpha=0.3)

# Add labels and grid
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.title('Accidents by Location')
plt.grid(True)

Результат:

Обратите внимание: бот забыл указать на необходимость импорта matplotlib.pyplot, она была импортирована ранее, поэтому код выполняется без ошибок.

«А в ML умеешь?»: построение моделей машинного обучения

Sketch может даже предложить модель машинного обучения:

df.sketch.howto('Кластеризация точек')

Результат:

#import necessary libraries
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans

#create a dataframe with only the columns needed for clustering
df_cluster = df[['lat', 'lon']]

#instantiate the KMeans model with the desired number of clusters
kmeans = KMeans(n_clusters=5)

#fit the model to the dataframe
kmeans.fit(df_cluster)

#predict the clusters for each row in the dataframe
df['cluster'] = kmeans.predict(df_cluster)
df.sketch.howto("""Построить модель, 
    предсказывающую количество погибших в ДТП на основе численных признаков""")

Результат:

#import necessary libraries
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

#separate the dataframe into features and target
X = df[['lat', 'lon', 'participants_count', 'injured_count']]
y = df['dead_count']

#split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

#fit the model to the training data
model = LinearRegression()
model.fit(X_train, y_train)

#make predictions on the test data
y_pred = model.predict(X_test)

#evaluate the model's performance using metrics such as mean squared error and r2 score
mse = mean_squared_error(y_test, y_pred) 
r2 = r2_score(y_test, y_pred) 
print('Mean Squared Error:', mse) 
print('R2 Score:', r2)

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

Заключение

Чтобы извлечь пользу от использования какого‑либо инструмента, необходимо понимать принцип его работы и его предназначение. В случае со Sketch необходимо также быть знакомым с принципами процесса анализа данных. Без понимания структуры хранения данных в Pandas и методов библиотеки вряд ли получится составить корректный запрос. Пожалуй, Sketch стоит воспринимать как расширение к Pandas, добавляющее возможность быстрого поиска информации о способе реализации алгоритма, но ни в коем случае не как альтернативу встроенным методам Pandas.

Знакомство с библиотекой Sketch оставляет положительные впечатления. Она выглядит очень перспективной для интеграции возможностей искусственного интеллекта в привычную для DA‑ и DS‑специалистов среду разработки. Это является её несомненным преимуществом. Ведь вместо того, чтобы копаться на Stack Overflow в поисках решения по задаче, похожей на вашу, а затем переписывать код, можно просто написать запрос прямо в Jupyter Notebook! И ответ будет составлен под ваш конкретный датафрейм.

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

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

На этом всё и спасибо за прочтение! Полная версия ноутбука и файл данных для экспериментов размещены в репозитории GitHub. Там вы найдёте алгоритм предобработки данных с открытого ресурса «Карта ДТП» и ещё больше примеров использования библиотеки Sketch.

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


  1. holodoz
    24.05.2023 10:29

    После любого запроса нужно выполнить руками запрос, чтобы понять, не галлюцинация это. Такая себе перспектива. Может с более свежими llm ответы будут точнее, но и chatGPT 4 сейчас в длинных списках путается, такая уж его природа


    1. NewTechAudit Автор
      24.05.2023 10:29

      Добрый день!

      Спасибо за замечание. Согласен, что chatGPT не идеален. Но, что есть, то есть.