Мотивация
Для тех, кто еще не ощутил возможности Streamlit, эта универсальная библиотека Python отлично подходит для создания удобных веб-приложений для визуализации данных и манипулирования ими. Его ключевая сила заключается в бесшовной интерактивности не только между пользователем и панелью управления, но и с базовыми данными. Используя код Python в фоновом режиме и другие дополнительные библиотеки и инструменты, Streamlit позволяет пользователям легко отображать, визуализировать, манипулировать, исследовать, анализировать и динамически изменять данные в режиме реального времени.
Команда разработчиков Streamlit постоянно представляет новые функции и пользовательские интерфейсы, еще больше повышая его эффективность как инструмента, применимого в широком спектре областей, включая логистику, финансы, здравоохранение и многое другое. Независимо от того, являетесь ли вы специалистом по данным, бизнес-аналитиком или просто человеком, ищущим информацию на основе данных, расширяемый набор инструментов Streamlit позволяет вам создавать интуитивно понятные, интерактивные и эффективные веб-приложения, используя Python в качестве языка программирования.
В этой статье мы покажем, как создать приложение Streamlit, имитирующее менеджера запасов. Это приложение позволяет пользователям добавлять товары в «корзину покупок» перед внесением этих изменений в фактические данные CSV. В следующей статье мы углубимся в обновление данных в реальном времени с помощью Snowflake. Но сейчас давайте начнем с основ и создадим корзину покупок.
Обратите внимание, что фрагменты кода будут записаны в отдельных полях кода, но в конце кода вы также найдете объединенную версию фрагментов кода. Если вы просто хотите скопировать весь код и изучить его самостоятельно или использовать его напрямую, прокрутите вниз до последней части. Я объединил фрагменты, чтобы избежать потенциальных ошибок, вызванных неуместными отступами.
Чтобы испытать само веб-приложение, вы можете перейти непосредственно по этой ссылке.
Часть 1. Ввод значений с использованием функций Streamlit
В этом разделе мы сосредоточимся на первоначальной настройке нашего веб-приложения Streamlit, которое позволяет пользователям интерактивно вводить значения в приложение. В частности, мы начнем с реализации раскрывающегося меню (называемого selectbox
), которое позволит пользователям выбирать категории продуктов. Эта функция расширяет возможности пользователя находить конкретные продукты из огромного количества тысяч наименований.
Импорт библиотек
Чтобы начать наш путь разработки Streamlit, мы начнем с импорта необходимых библиотек Python. Эти библиотеки предоставят нам инструменты и функции, необходимые для создания интерактивного менеджера запасов.
# importing libraries import pandas as pd import streamlit as st
Настройка широкого макета страницы
Streamlit позволяет нам настраивать макет нашего веб-приложения. Мы рассмотрим, как настроить широкий макет страницы, чтобы обеспечить визуально привлекательный и удобный интерфейс.
# use whole page width st.set_page_config(page_title="My Streamlit App", layout="wide")
Чтение файла CSV
Прежде чем мы сможем начать работать с данными, нам нужно прочитать CSV-файл, содержащий информацию о наших запасах. Мы углубимся в процесс загрузки этих данных в наше приложение Streamlit, что позволит нам беспрепятственно получать к ним доступ и манипулировать ими.
# read csv file df = pd.read_csv("inventory.csv")
Подготовка столбца флажка
Мы хотим взаимодействовать с нашими данными, и одно из важнейших взаимодействий — возможность выбирать каждую строку индивидуально. Чтобы это сделать, нам нужны флажки, подготовленные в каждой строке.
# preparing empty check boxes for the dataframe df["selected"] = [False for i in range(df.shape[0])]
Категории товаров
Чтобы заполнить наш selectbox
, нам нужен список уникальных категорий продуктов. Используйте функции Pandas, такие как unique()
, чтобы извлечь эти категории из кадра данных.
# preparing sub-category names for the selectbox list_sub_category = df["sub-category"].sort_values().unique().tolist()
Использование состояния сеанса Streamlit
Если мы хотим, чтобы наш фрейм данных в потоке данных можно было изменять после определенных действий, нам нужно объявить его не как обычный df
anymore, а вместо этого использовать st.session_state.df
, где df
является именем вашего фрейма данных. Здесь я определил два динамических фрейма данных: df
и full
. Мы обсудим это подробнее позже в этой статье, но df
— это пустой фрейм данных, содержащий только метки столбцов, а full
— это полный фрейм данных инвентаризации, содержащий все строки и столбцы.
# defining dataframe we want to dynamically interact with and # make changes to within streamlit session. # it is to be declared once at the beginning, like this: if 'df' not in st.session_state: st.session_state.df = pd.DataFrame(columns = df.columns) if 'full' not in st.session_state: st.session_state.full = df.copy()
Чтобы создать столбцы страницы вstreamlit, мы будем использовать st.columns
и определим размер каждого столбца. Обратите внимание, что размеры — это скорее соотношение, чем фактический размер. Это означает, что если вы напишете [3,3,3], размер будет точно таким же, как если бы вы написали их как [1,1,1].
a1,a2,a3 = st.columns([.75,.25,2])
Компоненты столбца первой страницы
В столбце первой страницы нашего приложения Streamlit мы разработаем удобный интерфейс для добавления предметов в наш инвентарь. Вот ключевые компоненты, которые мы включим:
selectbox
для категории продукта: мы добавимselectbox
, который позволит пользователям выбирать категорию продукта, который они хотят обновить. Этот выбор поможет сузить доступную продукцию.selectbox
для названия продукта: еще одинselectbox
позволит пользователям выбрать конкретный продукт, который они хотят добавить в список. Он связан с выбранной категорией, что гарантирует отображение только соответствующих названий продуктов.- Ввод текста для количества: пользователи могут указать количество, которое они хотят добавить в запасы для выбранного продукта.
- Кнопка «Добавить в корзину». Чтобы завершить запись, мы добавим кнопку «Добавить в корзину». При нажатии на эту кнопку будут зафиксированы выборы пользователя и введенное количество, добавление элемента в подготовленный фрейм данных.
with a1: # writing title to give users a lead st.subheader("Add item to inventory") # selecting sub-category before scrolling through product names sub_category = st.selectbox("Select product category:", list_sub_category) # retrieving list of recorded items in inventory list_item = df["product_name"][df["sub-category"] == sub_category].sort_values().unique().tolist() # to give an option to search items within all category names: if st.toggle("Search in all category"): list_item = df["product_name"].sort_values().unique().tolist() st.write("Num. of products in this category:",len(list_item)) st.write(" ") st.write(" ") # setting up a variable to save a product name that will be chosen by user during the interaction name = "" name = st.selectbox("Product Name:", list_item) # option to put a new item in the list if the product is not yet recorded in inventory list not_in_list = st.checkbox("Item not in the list.") if not_in_list: name = st.text_input("Product Name:") name = name.title() product_id = st.text_input("Product ID:") category = st.text_input("Category:") sub_category = st.text_input("Sub-Category:") # setting up variables to fill in other columns for each entry (row) sel = False if not_in_list == False: product_id = df["product_id"][df["product_name"] == name].values[0] category = df["category"][df["product_name"] == name].values[0] sub_category = df["sub-category"][df["product_name"] == name].values[0] quantity = st.text_input("Quantity:") # check if the quantity is a number q_isnumber = False if quantity != "": try: quantity = float(quantity) q_isnumber = True except: st.error("Quantity must be a number.") # preparing a button that records/appends the user input to the list/dataframe if st.button("Add to cart") and q_isnumber == True: st.session_state.df = st.session_state.df.append({ "selected":sel, "product_id":product_id, "category":category, "sub-category":sub_category, "product_name":name, "quantity":quantity }, ignore_index = True) # pops a small notification on below-right st.toast("Added to list.")
При добавлении товара в корзину в нашем приложении Streamlit очень важно указать, какие значения соответствуют каждому столбцу в кадре данных. Это достигается путем создания словаря, в котором имена столбцов действуют как ключи, а переменные, содержащие вводимые пользователем данные, служат соответствующими значениями.
Первый столбец должен выглядеть так.
Часть 2. Отображение редактируемого фрейма данных и простых взаимодействий
В этом разделе мы сосредоточимся на отображении данных о наших запасах в редактируемом формате с помощью st.data_editor
. Эта функция преобразует наш фрейм данных в интерактивную таблицу, похожую на Excel, что позволяет пользователям с легкостью редактировать значения ячеек. Кроме того, мы представим новый столбец «Выбрано» с флажками для выбора строк и продемонстрируем, как удалить выбранные строки из корзины инвентаря. Вот что мы рассмотрим:
- Использование
st.data_editor
: мы рассмотрим, как отображать данные о наших запасах и взаимодействовать с ними с помощьюst.data_editor
. Этот инструмент позволяет пользователям редактировать значения ячеек, щелкая по ним, предлагая удобный интерфейс, подобный Excel. - Добавление столбца «Выбрано». Мы добавим новый столбец под названием «Выбрано» в нашу корзину товаров. Этот столбец будет содержать флажки для каждой строки, облегчающие выбор конкретных пунктов для дальнейших действий.
- Создание кнопки «Удалить строку». Мы реализуем кнопку «Удалить строку», которая позволит пользователям удалять выбранные строки из корзины инвентаря. Эта функция упрощает управление элементами корзины.
- Форматирование корзины инвентаря. Чтобы улучшить взаимодействие с пользователем, мы отформатируем корзину инвентаря как сгруппированную таблицу. Такое форматирование подходит для сценариев, когда пользователи добавляют один и тот же продукт несколько раз, обеспечивая правильное агрегирование количеств.
Обратите внимание, что Часть 1 записывается в столбце страницы a1
, а Часть 2 — в столбце страницы a3
. Столбец страницы a2
объявлен как пустой столбец только для того, чтобы добавить больше места между обоими столбцами a1
и a3
.
with a3: st.subheader("Items to be added") # not sure why session state is declared again here lol if 'df' not in st.session_state: st.session_state.df = df.copy() # toggle button if st.toggle("Show data"): # to display the number of item in the list st.write("Num. of entry:", st.session_state.df.shape[0]) # displaying dataframe with st.data_editor # and applying st.column_config inside data_editor # 1. creating df.groupby to merge items with identical product name # 2. adding checkbox inside column_config parameter with False (unchecked) as default value # 3. hiding dataframe index so it will appear a bit "cleaner" without index column st.session_state.df = st.data_editor( st.session_state.df.groupby(["selected","product_name","category","sub-category","product_id"]).agg({"quantity":"sum"}).reset_index(), column_config = { "selected": st.column_config.CheckboxColumn("selected", default = False) }, hide_index = True, use_container_width=True) b1,b2,b3 = st.columns([1,1.5,1.5]) with b1: # setting up button to delete selected rows if st.button("Delete selected"): st.session_state.df = st.session_state.df[st.session_state.df["selected"] == False] st.success("Data deleted.") with b2: # setting up button to empty the cart if st.button("Delete all"): cols = st.session_state.df.columns st.session_state.df = pd.DataFrame(columns = cols) st.success("Data deleted.") with b3: # setting up button to add the quantity to the # actual quantity of mentioned item in the inventory data. if st.button("Save changes to inventory data"): for i,j in list(zip(st.session_state.df["product_name"],st.session_state.df["quantity"])): st.session_state.full["quantity"][st.session_state.full["product_name"] == i] += j cols = st.session_state.df.columns st.session_state.df = pd.DataFrame(columns = cols) st.success("Inventory updated. Scroll down to see your inventory data.")
Взаимодействие с фреймом данных должно выглядеть следующим образом.
Последняя часть. Отображение исходного фрейма данных для просмотра изменений
После обсуждения того, как изменить фрейм данных посредством взаимодействия в Streamlit, важно убедиться, что наши изменения эффективны. Вначале мы определили фрейм данных под названием full
как копию исходных данных инвентаризации. Мы хотим отобразить этот фрейм данных. Для этого мы создадим пространство для отображения исходных данных инвентаризации, что позволит нам сравнить обновленные данные с оригиналом.
selectbox
для категорий. Мы добавимselectbox
, который позволит пользователям выбирать определенную категорию продуктов. Эта функция упрощает процесс, не позволяя пользователям прокручивать тысячи уникальных предметов в инвентаре.- Использование
st.dataframe
: Чтобы отобразить исходные данные, мы можем использовать знакомую функциюst.dataframe
. Мы должны убедиться, что это отображение находится в отдельном разделе или пространстве в нашем приложении Streamlit для удобства пользователя.
Последний код должен выглядеть так.
st.divider() b1,b2,b3 = st.columns([1,3,1]) with b2: search_sub_category = st.selectbox("Select category to display:", list_sub_category) if st.toggle("Display data of selected category"): st.dataframe(st.session_state.full[st.session_state.full["sub-category"] == search_sub_category])
Подведение итогов
Теперь, когда мы обсудили фрагменты кода отдельно, чтобы определить, какая часть выполняет какую задачу, нам все равно нужно быть осторожными с форматированием кода и отступами. Когда мы пишем полный сценарий кода, довольно часто случается, что отступы неправильные, особенно если у вас много сложенных for
и if
. Именно по этой причине я решил вставить код целиком, чтобы вы могли скопировать его или использовать в качестве ориентира.
# importing libraries import pandas as pd import streamlit as st # use whole page width st.set_page_config(page_title="My Streamlit App", layout="wide") # read csv file df = pd.read_csv("inventory.csv") # preparing check boxes for the dataframe df["selected"] = [False for i in range(df.shape[0])] list_sub_category = df["sub-category"].sort_values().unique().tolist() # le title st.header("Inventory Manager") st.divider() # defining dataframe we want to dynamically interact with and make changes to within streamlit session. # declared once at the beginning, like this: if 'df' not in st.session_state: st.session_state.df = pd.DataFrame(columns = df.columns) if 'full' not in st.session_state: st.session_state.full = df.copy() # create page columns a1,a2,a3 = st.columns([.75,.25,2]) # first column's content with a1: st.subheader("Add item to inventory") # selecting sub-category before scrolling through product names sub_category = st.selectbox("Select product category:", list_sub_category) # retrieving list of recorded items in inventory list_item = df["product_name"][df["sub-category"] == sub_category].sort_values().unique().tolist() # to give an option to search items within all category names: if st.toggle("Search in all category"): list_item = df["product_name"].sort_values().unique().tolist() st.write("Num. of products in this category:",len(list_item)) st.write(" ") st.write(" ") # setting up a variable to save a product name that will be chosen by user during the interaction name = "" name = st.selectbox("Product Name:", list_item) # option to put a new item in the list if the product is not yet recorded in inventory list if st.checkbox("Item not in the list."): name = st.text_input("Product Name:") name = name.title() # setting up variables to fill in other columns for each entry (row) sel = False product_id = df["product_id"][df["product_name"] == name].values[0] category = df["category"][df["product_name"] == name].values[0] sub_category = df["sub-category"][df["product_name"] == name].values[0] quantity = st.text_input("Quantity:") # check if the quantity is a number q_isnumber = False if quantity != "": try: quantity = float(quantity) q_isnumber = True except: st.error("Quantity must be a number.") # preparing a button that records/appends the user input to the list/dataframe if st.button("Add to cart") and q_isnumber == True: st.session_state.df = st.session_state.df.append({ "selected":sel, "product_id":product_id, "category":category, "sub-category":sub_category, "product_name":name, "quantity":quantity }, ignore_index = True) # pops a small notification on below-right st.toast("Added to list.") with a3: st.subheader("Items to be added") # not sure why session state is declared again here lol if 'df' not in st.session_state: st.session_state.df = df.copy() # toggle button if st.toggle("Show data"): # to display the number of item in the list st.write("Num. of entry:", st.session_state.df.shape[0]) st.session_state.df = st.data_editor(st.session_state.df.groupby(["selected","product_name","category","sub-category","product_id"]).agg({"quantity":"sum"}).reset_index(), column_config = { "selected": st.column_config.CheckboxColumn("selected", default = False) }, hide_index = True, use_container_width=True) b1,b2,b3 = st.columns([1,1.5,1.5]) with b1: if st.button("Delete selected"): st.session_state.df = st.session_state.df[st.session_state.df["selected"] == False] st.success("Data deleted.") with b2: if st.button("Delete all"): cols = st.session_state.df.columns st.session_state.df = pd.DataFrame(columns = cols) st.success("Data deleted.") with b3: if st.button("Save changes to inventory data"): for i,j in list(zip(st.session_state.df["product_name"],st.session_state.df["quantity"])): st.session_state.full["quantity"][st.session_state.full["product_name"] == i] += j cols = st.session_state.df.columns st.session_state.df = pd.DataFrame(columns = cols) st.success("Inventory updated. Scroll down to see your inventory data.") st.divider() b1,b2,b3 = st.columns([1,3,1]) with b2: search_sub_category = st.selectbox("Select category to display:", list_sub_category) if st.toggle("Display data of selected category"): st.dataframe(st.session_state.full[st.session_state.full["sub-category"] == search_sub_category])
Что дальше?
Из-за развертывания Streamlit на GitHub вносить изменения в файлы в интерфейсе приложения в реальном времени непросто. Чтобы обновить фрейм данных, необходимо обновить сам файл репозитория GitHub, а этот процесс невозможен через интерфейс Streamlit.
Более практичный и, возможно, самый разумный подход предполагает привязку ваших данных к Snowflake Cloud. Когда вы открываете веб-приложение, оно извлекает данные непосредственно из Snowflake, а не полагается на CSV-файл. Таким образом, вы гарантируете, что новейшие данные будут доступны пользователям. Кроме того, любые обновления, сделанные в приложении, отражаются в базе данных Snowflake, что обеспечивает плавную синхронизацию данных и взаимодействие в режиме реального времени.
Однако это будет тема для другого дня. Следите за нашими следующими статьями по этой теме, в которых мы углубимся в связывание ваших данных с Snowflake Cloud для еще более эффективного управления данными. А пока продолжайте изучать наши сеансы кодирования на Python для различных задач анализа данных. Спасибо за чтение!