데이터분석/Python

[streamlit] 튜토리얼 > fragment 내부에서 전체 스크립트 재실행 트리거하기

psystat 2024. 9. 17. 20:23

[출처] https://docs.streamlit.io/develop/tutorials/execution-flow/trigger-a-full-script-rerun-from-a-fragment

 

Streamlit Docs

Join the community Streamlit is more than just a way to make data apps, it's also a community of creators that share their apps and ideas and help each other make their work better. Please come join us on the community forum. We love to hear your questions

docs.streamlit.io


1. Intro

Streamlit을 사용하면 함수를 fragments로 전환하여 전체 스크립트와 독립적으로 재실행할 수 있습니다. 사용자가 조각 내부의 위젯과 상호작용하면 해당 fragment만 재실행됩니다. 때로는 fragment 내부에서 전체 스크립트 재실행을 트리거하고 싶을 수 있습니다. 이렇게 하려면 fragment 내에서 st.rerun을 호출합니다.

Applied concepts

  • fragment를 사용하여 사용자 입력에 따라 앱의 일부 또는 전체를 다시 실행할 수 있습니다.

Prerequisites

  • 파이썬 환경에는 다음이 설치되어 있어야 합니다: streamlit>=1.37.0
  • your-repository 라는 깨끗한 작업 디렉터리가 있어야 합니다.
  • fragments와 st.rerun에 대한 기본적인 이해가 있어야 합니다.

Summary

이 예제에서는 판매 데이터를 표시하는 앱을 빌드합니다. 앱에는 날짜 선택에 따라 달라지는 두 가지 요소 세트가 있습니다. 한 요소 세트는 선택한 날짜에 대한 정보를 표시합니다. 다른 요소 세트는 관련 월에 대한 정보를 표시합니다. 사용자가 한 달 내에 요일을 변경하는 경우 Streamlit은 첫 번째 요소 세트만 업데이트하면 됩니다. 사용자가 다른 달의 날짜를 선택하면 스트림릿은 모든 요소를 업데이트해야 합니다.

사용자가 같은 달 내에 요일을 변경할 때 전체 앱을 다시 실행하지 않도록 날짜별 요소를 fragment로 수집합니다. fragment 함수 정의를 바로 확인하려면 아래의  Build a function to show daily sales data을 참조하세요.

import streamlit as st
import pandas as pd
import numpy as np
from datetime import date, timedelta
import string
import time


@st.cache_data
def get_data():
    """Generate random sales data for Widget A through Widget Z"""

    product_names = ["Widget " + letter for letter in string.ascii_uppercase]
    average_daily_sales = np.random.normal(1_000, 300, len(product_names))
    products = dict(zip(product_names, average_daily_sales))

    data = pd.DataFrame({})
    sales_dates = np.arange(date(2023, 1, 1), date(2024, 1, 1), timedelta(days=1))
    for product, sales in products.items():
        data[product] = np.random.normal(sales, 300, len(sales_dates)).round(2)
    data.index = sales_dates
    data.index = data.index.date
    return data


@st.fragment
def show_daily_sales(data):
    time.sleep(1)
    with st.container(height=100):
        selected_date = st.date_input(
            "Pick a day ",
            value=date(2023, 1, 1),
            min_value=date(2023, 1, 1),
            max_value=date(2023, 12, 31),
            key="selected_date",
        )

    if "previous_date" not in st.session_state:
        st.session_state.previous_date = selected_date
    previous_date = st.session_state.previous_date
    st.session_state.previous_date = selected_date
    is_new_month = selected_date.replace(day=1) != previous_date.replace(day=1)
    if is_new_month:
        st.rerun()

    with st.container(height=510):
        st.header(f"Best sellers, {selected_date:%m/%d/%y}")
        top_ten = data.loc[selected_date].sort_values(ascending=False)[0:10]
        cols = st.columns([1, 4])
        cols[0].dataframe(top_ten)
        cols[1].bar_chart(top_ten)

    with st.container(height=510):
        st.header(f"Worst sellers, {selected_date:%m/%d/%y}")
        bottom_ten = data.loc[selected_date].sort_values()[0:10]
        cols = st.columns([1, 4])
        cols[0].dataframe(bottom_ten)
        cols[1].bar_chart(bottom_ten)


def show_monthly_sales(data):
    time.sleep(1)
    selected_date = st.session_state.selected_date
    this_month = selected_date.replace(day=1)
    next_month = (selected_date.replace(day=28) + timedelta(days=4)).replace(day=1)

    st.container(height=100, border=False)
    with st.container(height=510):
        st.header(f"Daily sales for all products, {this_month:%B %Y}")
        monthly_sales = data[(data.index < next_month) & (data.index >= this_month)]
        st.write(monthly_sales)
    with st.container(height=510):
        st.header(f"Total sales for all products, {this_month:%B %Y}")
        st.bar_chart(monthly_sales.sum())


st.set_page_config(layout="wide")

st.title("Daily vs monthly sales, by product")
st.markdown("This app shows the 2023 daily sales for Widget A through Widget Z.")

data = get_data()
daily, monthly = st.columns(2)
with daily:
    show_daily_sales(data)
with monthly:
    show_monthly_sales(data)


2. Build the example

2.1. Initialize your app

1. your_repository에서 app.py라는 파일을 만듭니다.

2. 터미널에서 디렉터리를 your_repository로 변경하고 앱을 시작합니다.

streamlit run app.py

아직 코드를 추가해야 하므로 앱이 비어 있습니다.

3. app.py에 다음을 작성합니다:

import streamlit as st
import pandas as pd
import numpy as np
from datetime import date, timedelta
import string
import time

이러한 라이브러리는 다음과 같이 사용하게 됩니다:

  • pandas.DataFrame에서 판매 데이터로 작업하게 됩니다.
  • numpy로 무작위 판매 데이터를 생성합니다.
  • 데이터에는 datetime.date 인덱스 값이 있습니다.
  • 판매되는 제품은 '위젯 A'부터 '위젯 Z'까지이므로 알파벳순으로 쉽게 접근할 수 있도록 string을 사용합니다.
  • (선택 사항) 마지막에 강조를 더하기 위해 time.sleep()을 사용하여 속도를 늦추고 fragment 작동하는 것을 확인할 수 있습니다.

4. app.py 파일을 저장하고 실행 중인 앱을 확인합니다.

5. 'Always rerun'을 클릭하거나 실행 중인 앱에서 'A' 키를 누르세요.
app.py에 변경 사항을 저장하면 실행 중인 미리 보기가 자동으로 업데이트됩니다. 미리보기는 여전히 비어 있습니다. 코드로 돌아갑니다.

2.2. 무작위 판매 데이터를 생성하는 함수 구축

먼저 일부 판매 데이터를 무작위로 생성하는 함수를 정의합니다. 함수를 복사만 하려는 경우 이 섹션을 건너뛰어도 됩니다.

@st.cache_data
def get_data():
    """Generate random sales data for Widget A through Widget Z"""

    product_names = ["Widget " + letter for letter in string.ascii_uppercase]
    average_daily_sales = np.random.normal(1_000, 300, len(product_names))
    products = dict(zip(product_names, average_daily_sales))

    data = pd.DataFrame({})
    sales_dates = np.arange(date(2023, 1, 1), date(2024, 1, 1), timedelta(days=1))
    for product, sales in products.items():
        data[product] = np.random.normal(sales, 300, len(sales_dates)).round(2)
    data.index = sales_dates
    data.index = data.index.date
    return data

1. st.cache_data 데코레이터를 사용하여 함수 정의를 시작하세요.

@st.cache_data
def get_data():
    """Generate random sales data for Widget A through Widget Z"""

데이터를 계속 무작위로 다시 생성할 필요가 없으므로 캐싱 데코레이터가 데이터를 한 번 무작위로 생성하여 스트림릿의 캐시에 저장합니다. 앱이 재실행되면 새 데이터를 다시 계산하는 대신 캐시된 값을 사용합니다.

2. 제품 이름 목록을 정의하고 각 제품 이름에 일일 평균 판매액을 할당합니다.

    product_names = ["Widget " + letter for letter in string.ascii_uppercase]
    average_daily_sales = np.random.normal(1_000, 300, len(product_names))
    products = dict(zip(product_names, average_daily_sales))

- 파이썬에서 1_000 은 숫자 1000 과 동일하게 취급된다.

3. 각 제품에 대해 일일 평균 판매량을 사용하여 1년 동안의 일일 판매량을 무작위로 생성합니다.

    data = pd.DataFrame({})
    sales_dates = np.arange(date(2023, 1, 1), date(2024, 1, 1), timedelta(days=1))
    for product, sales in products.items():
        data[product] = np.random.normal(sales, 300, len(sales_dates)).round(2)
    data.index = sales_dates
    data.index = data.index.date

마지막 줄에서 data.index.date는 타임스탬프를 제거하여 인덱스에 깨끗한 날짜가 표시되도록 합니다.

4. 무작위 판매 데이터를 반환합니다.

    return data

5. (선택 사항) 함수를 호출하고 데이터를 표시하여 함수를 테스트합니다.

data = get_data()
data

app.py 파일을 저장하여 미리보기를 확인합니다. 이 두 줄을 삭제하거나 계속 진행하면서 업데이트할 수 있도록 앱의 마지막에 남겨 두세요.

2.3. 일일 판매 데이터를 표시하는 함수 구축

일일 판매 데이터는 새로운 날짜를 선택할 때마다 업데이트되므로 이 기능을 fragment로 전환할 수 있습니다. fragment는 앱의 나머지 부분과 독립적으로 다시 실행할 수 있습니다. 이 fragment 안에 st.date_input 위젯을 포함시키고 월을 변경하는 날짜 선택을 수행합니다. fragment가 선택한 월의 변경을 감지하면 전체 앱 재실행을 트리거하여 모든 것이 업데이트될 수 있도록 합니다.

@st.fragment
def show_daily_sales(data):
    time.sleep(1)
    selected_date = st.date_input(
        "Pick a day ",
        value=date(2023, 1, 1),
        min_value=date(2023, 1, 1),
        max_value=date(2023, 12, 31),
        key="selected_date",
    )

    if "previous_date" not in st.session_state:
        st.session_state.previous_date = selected_date
    previous_date = st.session_state.previous_date
    st.session_state.previous_date = selected_date
    is_new_month = selected_date.replace(day=1) != previous_date.replace(day=1)
    if is_new_month:
        st.rerun()

    st.header(f"Best sellers, {selected_date:%m/%d/%y}")
    top_ten = data.loc[selected_date].sort_values(ascending=False)[0:10]
    cols = st.columns([1, 4])
    cols[0].dataframe(top_ten)
    cols[1].bar_chart(top_ten)

    st.header(f"Worst sellers, {selected_date:%m/%d/%y}")
    bottom_ten = data.loc[selected_date].sort_values()[0:10]
    cols = st.columns([1, 4])
    cols[0].dataframe(bottom_ten)
    cols[1].bar_chart(bottom_ten)

1. st.fragment 데코레이터를 사용하여 함수 정의를 시작하세요.

@st.fragment
def show_daily_sales(data):

fragment 재실행 중에는 데이터가 변경되지 않으므로 데이터를 fragment에 인수로 전달할 수 있습니다.

2. (선택 사항) time.sleep(1)을 추가하여 함수 속도를 늦추고 fragment가 어떻게 작동하는지 보여줄 수 있습니다.

    time.sleep(1)

3. st.date_input 위젯을 추가합니다.

    selected_date = st.date_input(
        "Pick a day ",
        value=date(2023, 1, 1),
        min_value=date(2023, 1, 1),
        max_value=date(2023, 12, 31),
        key="selected_date",
    )

무작위 데이터는 2023년에 대한 것이므로 최소 및 최대 날짜를 일치하도록(=2023/01/01, 2023/12/31로) 설정합니다. fragment 외부의 요소에는 이 날짜 값이 필요하므로 위젯에 키를 사용하세요. fragment로 작업할 때는 session state를 사용하여 fragment 안팎으로 정보를 전달하는 것이 가장 좋습니다.

4. session state의 "previous_date"를 초기화하여 각 선택 날짜를 비교합니다.

    if "previous_date" not in st.session_state:
        st.session_state.previous_date = selected_date

5. 이전 날짜 선택을 새 변수에 저장하고 session state의 "previous_date"를 업데이트합니다.

    previous_date = st.session_state.previous_date
    st.session_state.previous_date = selected_date

6. 월이 변경된 경우 st.rerun()을 호출합니다.

    is_new_month = selected_date.replace(day=1) != previous_date.replace(day=1)
    if is_new_month:
        st.rerun()

7. 선택한 날짜의 베스트셀러를 표시합니다.

    st.header(f"Best sellers, {selected_date:%m/%d/%y}")
    top_ten = data.loc[selected_date].sort_values(ascending=False)[0:10]
    cols = st.columns([1, 4])
    cols[0].dataframe(top_ten)
    cols[1].bar_chart(top_ten)

8. 선택한 날짜의 워스트셀러를 표시합니다.

    st.header(f"Worst sellers, {selected_date:%m/%d/%y}")
    bottom_ten = data.loc[selected_date].sort_values()[0:10]
    cols = st.columns([1, 4])
    cols[0].dataframe(bottom_ten)
    cols[1].bar_chart(bottom_ten)

9. (선택 사항) 함수를 호출하고 데이터를 표시하여 함수를 테스트합니다.

data = get_data()
show_daily_sales(data)

app.py 파일을 저장하여 미리보기를 확인합니다. 이 두 줄을 삭제하거나 계속 진행하면서 업데이트할 수 있도록 앱의 마지막에 남겨 두세요.

2.4. 월별 판매 데이터를 표시하는 함수 구축

마지막으로 월별 판매 데이터를 표시하는 함수를 작성해 보겠습니다. 이 함수는 show_daily_sales 함수와 유사하지만 조각화할 필요는 없습니다. 전체 앱이 재실행될 때만 이 함수를 재실행하면 됩니다.

def show_monthly_sales(data):
    time.sleep(1)
    selected_date = st.session_state.selected_date
    this_month = selected_date.replace(day=1)
    next_month = (selected_date.replace(day=28) + timedelta(days=4)).replace(day=1)

    st.header(f"Daily sales for all products, {this_month:%B %Y}")
    monthly_sales = data[(data.index < next_month) & (data.index >= this_month)]
    st.write(monthly_sales)

    st.header(f"Total sales for all products, {this_month:%B %Y}")
    st.bar_chart(monthly_sales.sum())

1. 함수 정의를 시작하세요.

def show_monthly_sales(data):

2. (선택 사항) time.sleep(1)을 추가하여 함수 속도를 늦추고 조각이 어떻게 작동하는지 보여줄 수 있습니다.

    time.sleep(1)

3. 세션 상태에서 선택한 날짜를 가져와서 이번 달과 다음 달의 첫날을 계산합니다.

    selected_date = st.session_state.selected_date
    this_month = selected_date.replace(day=1)
    next_month = (selected_date.replace(day=28) + timedelta(days=4)).replace(day=1)

4. 선택한 월의 모든 제품에 대한 일일 판매량을 표시합니다.

    st.header(f"Daily sales for all products, {this_month:%B %Y}")
    monthly_sales = data[(data.index < next_month) & (data.index >= this_month)]
    st.write(monthly_sales)

5. 선택한 월의 각 제품의 총 판매량을 표시합니다.

    st.header(f"Total sales for all products, {this_month:%B %Y}")
    st.bar_chart(monthly_sales.sum())

6. (선택 사항) 함수를 호출하고 데이터를 표시하여 함수를 테스트합니다.

data = get_data()
show_daily_sales(data)
show_monthly_sales(data)

app.py 파일을 저장하여 미리 보기를 확인합니다. 완료되면 이 세 줄을 삭제합니다.

2.5. 기능을 조합하여 앱 만들기

이러한 요소를 나란히 표시해 보겠습니다. 왼쪽에는 일별 데이터가 표시되고 오른쪽에는 월별 데이터가 표시됩니다.

1. 함수를 테스트하기 위해 코드 끝에 옵션 줄을 추가했다면 지금 지우세요.

2. 앱에 넓은 레이아웃을 제공하세요.

st.set_page_config(layout="wide")

3. 데이터를 가져옵니다.

data = get_data()

4. 앱의 제목과 설명을 추가합니다.

st.title("Daily vs monthly sales, by product")
st.markdown("This app shows the 2023 daily sales for Widget A through Widget Z.")

5. 열을 만들고 함수를 호출하여 데이터를 표시합니다.

daily, monthly = st.columns(2)
with daily:
    show_daily_sales(data)
with monthly:
    show_monthly_sales(data)

2.6. 예쁘게 만들기

이제 fragment를 사용하여 월별 데이터를 불필요하게 다시 그리는 것을 방지하는 앱이 작동합니다. 하지만 페이지에 정렬되지 않았으므로 컨테이너 몇 개를 삽입하여 예쁘게 만들 수 있습니다. 각 표시 기능에 컨테이너를 3개씩 추가합니다.

1. 컨테이너 3개를 추가하여 show_daily_sales 함수에서 요소의 높이를 고정합니다.

2. show_monthly_sales 함수에서 요소의 높이를 고정하기 위해 컨테이너 3개를 추가합니다.

첫 번째 컨테이너는 show_daily_sales 함수에서 입력 위젯과 조정할 공간을 만듭니다.

import streamlit as st
import pandas as pd
import numpy as np
from datetime import date, timedelta
import string
import time

@st.cache_data
def get_data():
    """Generate random sales data for Widget A through Widget Z"""

    product_names = ["Widget " + letter for letter in string.ascii_uppercase]
    average_daily_sales = np.random.normal(1_000, 300, len(product_names))
    products = dict(zip(product_names, average_daily_sales))

    data = pd.DataFrame({})
    sales_dates = np.arange(date(2023, 1, 1), date(2024, 1, 1), timedelta(days=1))
    for product, sales in products.items():
        data[product] = np.random.normal(sales, 300, len(sales_dates)).round(2)
    data.index = sales_dates
    data.index = data.index.date
    return data

@st.fragment
def show_daily_sales(data):
    time.sleep(1)
    with st.container(height=100): ### ADD CONTAINER ###
        selected_date = st.date_input(
            "Pick a day ",
            value=date(2023, 1, 1),
            min_value=date(2023, 1, 1),
            max_value=date(2023, 12, 31),
            key="selected_date",
        )

    if "previous_date" not in st.session_state:
        st.session_state.previous_date = selected_date

    previous_date = st.session_state.previous_date
    st.session_state.previous_date = selected_date

    is_new_month = selected_date.replace(day=1) != previous_date.replace(day=1)
    if is_new_month:
        st.rerun()

    with st.container(height=510): ### ADD CONTAINER ###
        st.header(f"Best sellers, {selected_date:%m/%d/%y}")
        top_ten = data.loc[selected_date].sort_values(ascending=False)[0:10]
        cols = st.columns([1, 4])
        cols[0].dataframe(top_ten)
        cols[1].bar_chart(top_ten)

    with st.container(height=510): ### ADD CONTAINER ###
        st.header(f"Worst sellers, {selected_date:%m/%d/%y}")
        bottom_ten = data.loc[selected_date].sort_values()[0:10]
        cols = st.columns([1, 4])
        cols[0].dataframe(bottom_ten)
        cols[1].bar_chart(bottom_ten)

def show_monthly_sales(data):
    time.sleep(1)
    selected_date = st.session_state.selected_date
    this_month = selected_date.replace(day=1)
    next_month = (selected_date.replace(day=28) + timedelta(days=4)).replace(day=1)

    st.container(height=100, border=False) ### ADD CONTAINER ###

    with st.container(height=510): ### ADD CONTAINER ###
        st.header(f"Daily sales for all products, {this_month:%B %Y}")
        monthly_sales = data[(data.index < next_month) & (data.index >= this_month)]
        st.write(monthly_sales)

    with st.container(height=510): ### ADD CONTAINER ###
        st.header(f"Total sales for all products, {this_month:%B %Y}")
        st.bar_chart(monthly_sales.sum())

st.set_page_config(layout="wide")
data = get_data()

st.title("Daily vs monthly sales, by product")
st.markdown("This app shows the 2023 daily sales for Widget A through Widget Z.")

daily, monthly = st.columns(2)
with daily:
    show_daily_sales(data)
with monthly:
    show_monthly_sales(data)

2.7. 다음 단계

예제를 계속 아름답게 꾸미세요. st.plotly_chart 또는 st.altair_chart를 사용하여 차트에 레이블을 추가하고 높이를 조정해 보세요.