[Service] Streamlit


앞서 프로토타입 등을 빠르게 구현할 때 사용할 수 있는 라이브러리로 Streamlit 이 있다고 했다. 이에 대해 알아보자.

Streamlit

  • 다른 웹 서비스 구현 라이브러리인 Voila 는 노트북에서 쉽게 프로토타입을 만들 수 있지만, 대시보드처럼 레이아웃을 잡기 어렵다는 단점이 있다.
  • 또한 JavaScript, React, Vue 등을 사용해 프로토타입을 만드는 것은 시간 효율적이지 않다.
  • 처음부터 HTML/JavaScript + Flask/FastAPI 가 아닌 기존 코드를 조금만 수정해서 웹 서비스를 만드는 것이 목표일 때, Streamlit 을 사용할 수 있다.
  • 아래 이미지는 데이터 분석가(또는 데이터 사이언티스트)가 웹 서비스를 만드는 Flow 에 대해 나타내고 있다.

    Untitled

  • 데이터 Product 가 중요하다면 프론트엔드/PM 조직과 협업한다. 단 프론트엔드/PM 조직은 다른 웹도 만들고 있어 빠르게 이터레이션 하기 어렵다.

    UntitledStep 1 에서 requirements 는 라이브러리가 아니라 요구사항을 뜻한다.

  • Streamlit 은 다른 조직의 도움 없이 빠르게 웹 서비스를 만들 수 있는 장점이 있다.

    Untitled

  • 여기서 유념해야 할 것은, 모든 것은 프로젝트의 요구조건에 따라 다르고, 프로젝트 설계에 따라 다르다는 점이다.
  • Streamlit 의 대안으로는 아래와 같은 도구들이 있다.
    • R 의 Shiny
    • Flask, Fast API : 백엔드를 직접 구성 + 프론트엔드 작업도 진행
    • Dash : 제일 기능이 풍부한 Python 대시보드 라이브러리
    • Voila : jupyter notebook 을 바로 시각화할 수 있으나, UX 나 레이아웃을 잡기에는 까다롭다.

      Untitled

  • Streamlit 은 공식문서를 제공한다. Streamlit Docs에 들어가보면 확인할 수 있다.
  • Streamlit 의 장점은 아래와 같다.
    • 파이썬 스크립트 코드를 조금만 수정하면 웹을 띄울 수 있다.
    • 백엔드 개발이나 HTTP 요청을 구현하지 않아도 된다.
    • 다양한 Component 제공해 대시보드 UI 구성할 수 있다.
    • Streamlit Cloud 도 존재하기 때문에 쉽게 배포할 수 있다. 단, Community Plan 은 Public Repo 만 가능하다.
    • 화면 녹화 기능(Record)이 존재한다. 이를 github README.md 에 올릴 수 있다.
    • 굉장히 적은 코드로 프론트엔드 기능을 할 수 있다.
  • 아래와 같이 CLI 로 streamlit run streamlit-basic.py 명령어만 입력하면 바로 확인해볼 수 있다.

    Untitled

Streamlit Component

  • Streamlit API Document 에는 굉장히 많은 Component 가 존재한다. Streamlit cheat sheet를 참고하면 확인할 수 있다.
  • 아래는 Streamlit 에서 제공하는 대표적인 component 들이며, 코드와 웹에서 보이는 결과를 보여주는 이미지를 보자.

Title, Header, subheader, Write

CODE RESULT
  import streamlit as st
  import pandas as pd
  import numpy as np
  import time

  st.title('Title')
  st.header('Header')
  st.subheader('subheader')

  st.write('Write Something')

Untitled

버튼

CODE RESULT
  if st.button("버튼이 클릭되면1"):
      st.write("클릭 후 보이는 메세지1")

  if st.button("버튼이 클릭되면2"):
      st.write("클릭 후 보이는 메세지2")

Untitled

체크박스

  • value argument 에 인자를 넘겨주면 default 값을 설정할 수 있다.
CODE RESULT
  checkbox_btn = st.checkbox("체크박스 버튼")

  if checkbox_btn:
      st.write("체크박스 버튼 클릭!")

  checkbox_btn2 = st.checkbox("Checkbox Button2", value=True)

  if checkbox_btn2:
      st.write("Button2")

Untitled

Untitled

Pandas Dataframe, Markdown

  • st.write : 보여줄 수 있는 것이면 어떤 것이든 보여준다.
  • st.dataframe : Interactive 한 Dataframe, 컬럼 클릭이 가능하고 정렬도 가능하다.
  • st.table : Static 한 Dataframe
CODE RESULT
  df = pd.DataFrame([
      'first column':[1,2,3,4],
      'second column':[10,20,30,40]
  ])

  st.markdown('============')

  st.write(df)
  st.dataframe(df)
  st.table(df)

Untitled

강조

CODE RESULT
  df = pd.DataFrame([
      'first column':[1,2,3,4],
      'second column':[10,20,30,40]
  ])

  st.dataframe(df.style.highlight_max(axis=0))
  st.table(df.style.highlight_max(axis=0))

Untitled

Metric, Json

  • 지표 등을 작성할 수 있다.
CODE RESULT
  st.metric("My Metric", 42, 2)

  st.json(df.to_json())

Untitled

Line Chart

CODE RESULT
  chart_data = pd.DataFrame(
      np.random.randn(20, 3),
      columns=['a', 'b', 'c']
  )

  st.line_chart(chart_data)

Untitled

Map Chart

CODE RESULT
  map_data = pd.DataFrame(
      np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
      columns=['lat', 'lon']
  )

  st.map(map_data)

Untitled

  • 그 외에 plotly chart 등도 쉽게 사용할 수 있다. 자세한 것은 Streamlit Docs 의 Chart elements 를 확인해보자.

    Untitled

Radio Button, Select Box

CODE RESULT
  selected_item = st.radio("Radio Part", ("A", "B", "C"))

  if selected_item == "A":
      st.write("A!")
  elif selected_item == "B":
      st.write("B!")
  elif selected_item == "C":
      st.write("C!")

  option = st.selectbox('Please select in selectbox!',
                        ('kyle', 'elice', 'yepp'))

  st.write('You selected:' , option)

Untitled

Multi Select Box

CODE RESULT
  multi_select = st.multiselect('Please select somthings in multi selectbox!', 
                                ['A', 'B', 'C', 'D'])

  st.write('You selected:', multi_select)

Untitled

Slider

CODE RESULT
  values = st.slider('Select a range of values', 0.0, 100.0, (25.0, 75.0))
  st.write('Values:', values)

Untitled

input box, caption, code, latex

CODE RESULT
  text_input = st.text_input('텍스트를 입력해주세요')
  st.write(text_input)

  password_input = st.text_input('암호를 입력해주세요', type="password")

  number_input = st.number_input('숫자를 입력해주세요')
  st.write(number_input)

  st.date_input('날짜를 입력하세요')
  st.time_input('시간을 입력하세요')

  st.caption('This is caption')
  st.code('a=123')
  st.latex('\int a x^2 \,dx')

Untitled

  • st.sidebar.button(”hi”) 와 같이 사용한다. Sidebar 에는 파라미터를 지정하거나 암호를 설정할 수 있다.

    Untitled

columns

CODE RESULT
  col1, col2, col3, col4 = st.columns(4)
  
  col1.write("this is col1")
  col2.write("this is col2")
  col3.write("this is col3!!!")
  col4.write("this is col4~~~")

Untitled

Expander

CODE RESULT
  with st.expander("클릭하면 열려요"):
      st.write('content!')

Untitled

Spinner

CODE RESULT
  with st.spinner("Please wait..."):
      time.sleep(5)

Untitled

Ballons

  • st.balloons() 을 사용하면 웹 화면에 풍선이 나오는 이쁜(?) 효과를 발생시킨다.

Status Box

CODE RESULT
  st.success("Success")
  st.info("Info")
  st.warning("Warning")
  st.error("Error message")

Untitled

Form

CODE RESULT
  with st.form(key="입력 form"):
      username = st.text_input("Username")
      password = st.text_input("Password", type="password")
      st.form_submit_button("login")

Untitled

file uploader

CODE RESULT
  uploaded_file = st.file_uploader("Choose a file", type=['png', 'jpg', 'jpeg'])

Untitled

Streamlit Data Flow

  • Streamlit 의 화면에서 무언가 업데이트 되면, 전체 Streamlit 코드가 다시 실행되는 구조다.
    • 코드가 수정되는 경우
    • 사용자가 Streamlit 의 위젯과 상호작용 하는 경우(버튼 클릭, 입력 상자에 텍스트 입력 등)
  • streamlit 의 data flow 로 인해 매번 코드가 재실행되며 중복 이벤트를 할 수 없다. 따라서 Global Variable 처럼 서로 공유할 수 있는 변수가 필요하다.

Seesion State

  • streamlit 0.84 버전에서 이러한 역할을 하는 Session State 가 개발되었다.
  • st.session_state.{seesion_state_value} 와 같이 사용하며, session_state_value 에 저장해서 활용하는 방식이다. 이를 Global Variable 처럼 쓸 수 있다.
  • 만약 session state 가 없다면 아래와 같이 코드를 짤 수 있다. 아래 코드는 Increment 를 누르면 1 이 증가하고 Decrement 를 누르면 1 이 감소하는 예제다.

    import streamlit as st
    import pandas as pd
    import numpy as np
    import time
    
    st.title('Counter Example without session state')
      
    count_value = 0
    
    increment = st.button('Increment')
    if increment:
        count_value += 1
    
    decrement = st.buttom('Decrement')
    if decrement:
        count_value -= 1
    
    st.write('Count = ', count_value)
    
  • 그러나 increment 버튼을 아무리 눌러도 계속 결과는 1 이다.
  • 한 번 버튼을 누르면 재실행이 되어 count value 가 계속 0 으로 초기화 되기 때문이다. 즉 한 번 버튼을 누를 때마다 코드 전체가 재실행되는 것이다.
  • 이제 session state 가 있다면 아래와 같다.

    import streamlit as st
    import pandas as pd
    import numpy as np
    import time
    
    st.title('Counter Example without session state')
      
    # count session state 에 init
    if 'count' not in st.session_state:
        st.session_state.count = 0
    
    # increment 버튼이 클릭되면 session state 의 count 에 1 을 더함
    increment = st.button('Increment')
    if increment:
        st.session_state.count += 1
    
    # decrement 버튼이 클릭되면 session state 의 count 에 1 을 뺌
    decrement = st.buttom('Decrement')
    if decrement:
        st.session_state.count -= 1
    
    st.write('Count = ', st.session_state.count)
    
  • 이제는 코드가 재실행이 되어도 st.session_state 의 값은 변하지 않아, 버튼을 누른 만큼 count 가 됨을 확인할 수 있다.
  • 따라서 계속 반복해서 사용하고 싶은 변수가 있으면 session_state 에 저장해주는 것이 중요하다.
  • session_state 를 사용할 때는 처음에 default value 변수가 있는지 확인하고 없으면 init 해준다. 만약 위 코드에서 if 문 없이 st.session_state.count = 0 으로 바로 할당했다면 계속 재실행 되어 무조건 0 으로 남게 된다. 따라서 처음에 st.session_state 에 해당 변수가 없을 때만 0 으로 init 한 것이다.
  • 이후 특정 조건 충족 시 session_state 에 저장한 변수가 증가하도록 만들면 된다.
  • 이와 관련해서 Session State 에 대해 잘 정리한 Streamlit blog 글을 보자.

Callback

  • st.session_stateon_change 옵션을 같이 사용하여 Callback 함수를 사용할 수 있다.
  • 프로그래밍에서 callback 이란 특정한 조건을 만족하거나 이벤트 발생 시 특정 함수를 호출하는 패턴을 의미한다. 이는 트리거와 유사한 개념이다.
  • callback 함수는 GUI 프로그래밍이나 웹 애플리케이션에서 사용자의 입력(버튼 클릭, 키 입력, 마우스 이벤트 등)이나 시스템 이벤트 처리에 사용된다.

    st.title("가고 싶은 휴양지를 선택하세요")
    
    # 세션상태 초기화
    if 'lang' not in st.session_state:
        st.session_state['lang']='미국'
    
    # 콜백함수 정의
    def button_callback(selected_lang):
        st.session_state['lang'] = selected_lang
    
    # 라디오 버튼 생성
    radio_options=['미국', '영국', '프랑스', '독일']
    radio_selected = st.radio("어느 곳으로 떠나시겠습니까?", radio_options)
    
    # 기본 버튼: on_click 에 콜백 함수를 지정, args 에 콜백 함수에 대한 인수 지정
    clicked = st.button('선택', on_click=button_callback, args=[radio_selected])
    
    # 콜백함수를 실행한 결과 출력
    st.write(f"{st.session_state['lang']}에 가는 것을 선택했습니다.")
    
  • 만약 버튼 없이 radio_selected 를 고르자마자 st.session_state['lang'] 을 바꿔버리면, 사용자가 선택만 해도 값이 바뀌게 된다. 그러나 “진짜 바꿀지 말지” 는 버튼 클릭으로 결정해야 한다.
  • 따라서 버튼을 눌렀을 때만 st.session_state 를 바꾸도록 콜백 함수를 버튼에 연결하는 것이다.
  • 이처럼 콜백 함수 쓰는 이유는 “버튼을 눌렀을 때만 상태를 바꿔서, 사용자의 명확한 ‘확정’ 행동을 기다리기 위해” 사용하는 것이다.
  • 위 코드에서는 st.button 을 누를 때만 true 가 되고, 콜백(on_click)을 연결하면 누른 순간 자동으로 실행된다.
  • 이러한 “선택 후 확정” 은 웹앱에서 기본적이고 좋은 습관이다.
  • 간단한 앱에서는 버튼을 클릭했는지 직접 if 문으로 체크하는 if clicked: 로도 충분히 가능하지만, 복잡한 인터랙션(여러 버튼/액션)에서는 on_click=callback 을 쓰는 게 관리가 편하다.

Cache

  • Streamlit 은 매번 다시 실행하는 특성 때문에 데이터도 매번 다시 읽게 된다. 이런 경우 @st.cache 데코레이터를 사용해서 캐싱하면 좋다.
  • 아래 코드와 같이 Streamlit Cache 를 뜻하는 @st.cache 를 데코레이터로 함수 위에 붙여준다.

    DATE_COLUMN = 'data/time'
    DATA_URL = 'https://s3-us-west-2.amazonaws.com/streamlit-demo-data/uber-raw-data-sep14.csv.gz'
    
    @st.cache
    def load_data(nrows):
        data = pd.read_csv(DATA_URL, nrows=nrows)
        lowercase = lambda x: str(x).lower()
        data.rename(lowercase, axis='columns', inplace=True)
        data[DATA_COLUMN] = pd.to_datetime(data[DATE_COLUMN])
        return data
    
  • 캐싱은 성능을 위해 메모리 등에 저장하는 행위다. 데이터를 읽는 함수를 만들고 위처럼 @st.cache 데코레이터를 적용하게 되면, 다시 실행이 되더라도 캐싱이 되어 더 빠르게 실행된다.
맨 위로 이동 ↑

댓글 남기기