Trực quan hóa dữ liệu bằng Python Dash
Giới thiệu về Dash
Dash là một thư viện mã nguồn mở được phát hành theo giấy phép MIT. Được viết trên Plotly.js và React.js, Dash lý tưởng cho việc xây dựng và triển khai các ứng dụng dữ liệu với UI (giao diện người dùng) tùy chỉnh. Dash đủ đơn giản để bạn có thể liên kết UI với code của mình trong vòng chưa đầy 10 phút. Ứng dụng Dash được render lên trình duyệt web, vì vậy nó có thể chạy trên đa nền tảng và cả thiết bị di động.
Hướng dẫn sử dụng Dash
Cài đặt Dash
Yêu cầu máy cài sẵn Python 3.
Trong terminal chạy lệnh sau để cài đặt Dash:
pip install dash
Với lệnh trên thì ngoài dash, pip cũng sẽ cài đặt thư viện hỗ trợ vẽ đồ thị đó là Plotly.py.
Và cuối cùng chúng ta cần cài đặt thư viện Pandas bằng lệnh:
pip install pandas
Về Pandas
Pandas là một thư viện mã nguồn mở được phát hành theo giấy phép BSD. Pandas cung cấp các cấu trúc dữ liệu hiệu suất cao, dễ sử dụng và các công cụ phân tích dữ liệu cho ngôn ngữ lập trình Python.
Pandas cung cấp 2 cấu trúc dữ liệu chính là DataFrame và Series. DataFrame là một cấu trúc dữ liệu 2 chiều có thể lưu trữ dữ liệu thuộc các loại khác nhau (bao gồm ký tự, số nguyên, giá trị dấu phẩy động, dữ liệu phân loại và hơn thế nữa) trong các cột. Mỗi cột trong DataFrame là một Series.
Có ba quy ước phổ biến để lưu trữ dữ liệu dạng cột:
-
- Dữ liệu dạng dài có một hàng cho mỗi quan sát và một cột cho mỗi biến. Dạng này phù hợp để lưu trữ và hiển thị dữ liệu đa biến, tức là với số chiều lớn hơn 2.
-
- Dữ liệu dạng rộng có một hàng cho mỗi giá trị của một trong các biến đầu tiên và một cột cho mỗi giá trị của biến thứ hai. Dạng này phù hợp để lưu trữ và hiển thị dữ liệu 2D.
-
- Dữ liệu dạng hỗn hợp là kết hợp của dữ liệu dạng dài và dữ liệu dạng rộng.
Dash Layout
Ứng dụng Dash bao gồm hai phần. Phần đầu tiên là “layout” của ứng dụng và nó mô tả ứng dụng đó trông như thế nào. Phần thứ hai mô tả khả năng tương tác của ứng dụng, nó là “callbacks”.
“Layout” là 1 cây tập hợp các “components“.
Dash cung cấp rất nhiều loại component: Dash HTML Components, Dash Core Components, Dash DataTable, Dash DAQ, Dash Bootstrap Components,…
Trong khuôn khổ của bài viết chúng ta sẽ tìm hiểu Dash HTML Components và Dash Core Components.
Dash HTML Components
Là fuction cung cấp các component kiểu HTML, dùng để định nghĩa các HTML tag cho layout.
Để dùng Dash HTML Components chúng ta cần import vào file .py như sau: from dash import html
Ví dụ:
html_demo.py
import dash from dash import html app = dash.Dash(__name__) app.layout = html.Div(children=[ html.H1(children='Hello Dash'), ]) if __name__ == '__main__': app.run_server(debug=True)
html.H1(children='Hello Dash')
sẽ tạo ra <h1>Hello Dash</h1>
trên trình duyệt.
Cũng giống như các thẻ HTML tag, chúng ta hoàn toàn có thể thay đổi style của html_component bằng property “style“.
Ví dụ: html.H1('Hello Dash', style={'textAlign': 'center', 'color': '#7FDBFF'})
Đoạn mã trên được hiển thị dưới dạng <h1 style="text-align: center; color: #7FDBFF">Hello Dash</h1>
Có một số khác biệt quan trọng giữa Dash HTML Components và các thuộc tính HTML:
- Thuộc tính “style” trong HTML là một chuỗi được phân tách bằng dấu chấm phẩy. Trong Dash, bạn cần cung cấp một dictionary.
- Các key trong dictionary là dạng camelCased. Vì vậy, thay vì “text-align“, trong Dash là “textAlign“.
- Thuộc tính “class” trong HTML là “className” trong Dash.
- Con của thẻ HTML được chỉ định thông qua argument với từ khoá “children“.
Ngoài ra thay vì dùng style trực tiếp chúng ta có thể dùng file CSS để định nghĩa style cho layout, chi tiết tham khảo: https://dash.plotly.com/external-resources
Bạn có thể xem tất cả các component có sẵn trong Dash HTML Components Gallery: https://dash.plotly.com/dash-html-components
Dash Core Components
Bao gồm một tập hợp các thành phần cấp cao hơn như dropdown, checkbox, radio, graph, v.v. Bạn có thể xem tất cả các component có sẵn trong Dash Core Components Gallery: https://dash.plotly.com/dash-core-components
Để dùng Dash Core Components chúng ta cần import vào file .py như sau: from dash import dcc
Trong các core component thì “Graph” là component quan trọng đối với Trực quan hóa dữ liệu. “Graph” hiển thị trực quan hóa dữ liệu trên trình duyệt bằng cách sử dụng thư viện javascript vẽ đồ thị mã nguồn mở Plotly.js. Plotly.js hỗ trợ hơn 35 loại biểu đồ và hiển thị biểu đồ ở cả vector-quality SVG và high-performance WebGL. Một lưu ý nhỏ ở đây là Plotly.js chỉ dùng để render lên trình duyệt (do Dash thực hiện) còn khi code chúng ta sẽ dùng thư viện Plotly.py (được cung cấp sẵn khi cài đặt Dash) chứ không code trực tiếp bằng javascript.
Để biết cách sử dụng “Graph” component chúng ta hãy đến với ví dụ hiển thị data csv lên trình duyệt dưới dạng đồ thị đường gấp khúc:
csv/graph_sample.csv
DateTime,DATA 1,DATA 2,DATA 3,DATA 4 20211220 101010.000,30,100,124,197 20211220 101010.010,40,110,134,65 20211220 101010.020,50,140,214,149 20211220 101010.030,60,150,169,-98 20211220 101010.040,70,160,204,-173 20211220 101010.050,80,170,164,-108 20211220 101010.060,90,180,148,150 20211220 101010.070,100,190,180,92 20211220 101010.080,110,200,268,94 20211220 101010.090,120,210,164,-139 20211220 101010.100,130,220,254,-132
Đầu tiên chúng ta cần dùng pandas để load file csv
df = pd.read_csv('csv/graph_sample.csv')
In biến df ra console xem thử cấu trúc của nó
print(df)
Tới đây bạn có thấy hơi quen quen không? Chính xác, nó là Dữ liệu dạng rộng mà chúng ta đã đề cập ở phần tìm hiểu Pandas ở trên!
Bước tiếp theo chúng ta chuyển dữ liệu của cột DateTime từ string thành datetime để chart của chúng ta hiển thị chính xác ngày và giờ của dữ liệu
df['DateTime'] = pd.to_datetime(df['DateTime'], format='%Y%m%d %H:%M:%S.%f')
Bây giờ chúng ta tạo một line figure bằng plotly express
line_fig = px.line(df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4'])
Truyền figure vào Graph component
app.layout = html.Div(children=[ dcc.Graph(id='graph', figure=line_fig) ])
Code hoàn chỉnh
graph_demo.py
import dash import pandas as pd import plotly.express as px from dash import dcc from dash import html app = dash.Dash(__name__) df = pd.read_csv('csv/graph_sample.csv') print(df) df['DateTime'] = pd.to_datetime(df['DateTime'], format='%Y%m%d %H:%M:%S.%f') line_fig = px.line(df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4']) app.layout = html.Div(children=[ dcc.Graph(id='graph', figure=line_fig) ]) if __name__ == '__main__': app.run_server(debug=True)
Trong terminal chạy lệnh:
python graph_demo.py
Sau đó truy cập http://127.0.0.1:8050/ để xem kết quả
Trong ví dụ trên:
- Thư viện Pandas được dùng để xử lý data đầu vào (đọc csv, chuyển dữ liệu của cột DateTime từ string thành datetime).
- Plotly Express (nằm trong thư viện Plotly.py) chịu trách nhiệm quy định kiểu biểu đồ (đường gấp khúc, phân tán,…), x-axis, y-axis,… của Graph đầu ra.
Dash Callbacks
Callback functions: các hàm được Dash tự động gọi bất cứ khi nào thuộc tính của input component thay đổi, để cập nhật một số thuộc tính trong component khác (output).
Để hiểu về Callbacks chúng ta hãy đến với ví dụ về filter dữ liệu theo ngày, với input lấy từ component dcc.DatePickerRange:
csv/callbacks_sample.csv
DateTime,DATA 1,DATA 2,DATA 3,DATA 4 20211219 101010.010,10,200,178,90 20211219 111010.020,20,150,134,25 20211219 121010.030,5,130,210,11 20211219 131010.040,15,110,100,-97 20211219 141010.050,60,150,143,-17 20211219 151010.060,30,140,132,30 20211219 161010.070,20,180,167,45 20211219 171010.080,16,120,240,123 20211219 181010.090,75,190,153,40 20211219 191010.100,90,250,162,-10 20211220 001010.000,68,142,156,1 20211220 011010.010,40,110,134,65 20211220 021010.020,50,140,214,149 20211220 031010.030,60,150,169,-98 20211220 041010.040,70,160,204,-173 20211220 051010.050,80,170,164,-108 20211220 061010.060,90,180,148,150 20211220 071010.070,100,190,180,92 20211220 081010.080,110,200,268,94 20211220 091010.090,120,210,164,-139 20211220 101010.100,130,220,254,-132 20211221 001010.000,10,90,142,30 20211221 011010.010,30,100,162,55 20211221 021010.020,80,120,180,20 20211221 031010.030,70,110,176,-10 20211221 041010.040,50,130,194,-90 20211221 051010.050,60,140,202,-120 20211221 061010.060,90,150,164,100 20211221 071010.070,120,160,197,132 20211221 081010.080,110,170,186,40 20211221 091010.090,130,210,182,-130 20211221 101010.100,120,230,210,-100
callbacks_demo.py
from datetime import datetime, timedelta import dash import pandas as pd import plotly.express as px from dash import dcc, Output, Input from dash import html app = dash.Dash(__name__) df = pd.read_csv('csv/callbacks_sample.csv') df['DateTime'] = pd.to_datetime(df['DateTime'], format='%Y%m%d %H:%M:%S.%f') init_start_date = df['DateTime'].min().strftime('%Y-%m-%d') init_end_date = df['DateTime'].max().strftime('%Y-%m-%d') app.layout = html.Div(children=[ dcc.DatePickerRange( id='date-picker-range', start_date=init_start_date, end_date=init_end_date, minimum_nights=0, display_format='YYYY/MM/DD' ), dcc.Graph(id='scatter-graph'), ]) @app.callback( Output('scatter-graph', 'figure'), Input('date-picker-range', 'start_date'), Input('date-picker-range', 'end_date') ) def update_figure(start_date, end_date): if start_date is not None and end_date is not None: start_date = datetime.fromisoformat(start_date) end_date = datetime.fromisoformat(end_date) + timedelta(days=1) filtered_df = df[(start_date <= df['DateTime']) & (df['DateTime'] <= end_date)] scatter_fig = px.scatter(filtered_df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4']) return scatter_fig if __name__ == '__main__': app.run_server(debug=True)
Trong Dash, các input và output của ứng dụng của chúng ta chỉ đơn giản là các property của một component cụ thể. Trong ví dụ này, input của chúng ta là property “start_date” và “end_date” của component có ID “date-picker-range“. Output của chúng ta là property “figure” của component có ID “scatter-graph“.
Bất cứ khi nào input property thay đổi, function mà có khai báo decorator @callback sẽ được gọi tự động. Dash cung cấp cho callback function này giá trị mới của input property làm argument (trong ví dụ trên function update_figure có 2 argument là start_date, end_date), và Dash cập nhật property của output component với bất kỳ giá trị nào được function trả về (trong ví dụ trên function update_figure trả về scatter_fig).
Trong terminal chạy lệnh python callbacks_demo.py
và truy cập vào http://127.0.0.1:8050/ để xem kết quả.
Sau khi thay đổi end_date
Tối ưu hóa và thêm chức năng
Ở phần này chúng ta lấy code ở phần Callbacks để tối ưu hóa và thêm chức năng cho nó.
Đọc n DATA
Hiện tại chúng ta đang set cứng số lượng data đầu vào là 4.
scatter_fig = px.scatter(filtered_df, x='DateTime', y=['DATA 1', 'DATA 2', 'DATA 3', 'DATA 4'])
Giả sử chúng ta có số lượng data bất kỳ DATA 1, DATA 2,…, DATA n thì với đoạn code trên chúng ta chỉ có thể đọc và hiển thị được 4 data mà thôi.
Để đọc và hiển thị lên biểu đồ n DATA, chúng ta cần chỉnh sửa code lại một chút:
# get first columns name for x-axis x_col_name = df.columns[0] # get list column name except first column for y-axis y_col_name_list = df.columns[1:] filtered_df = df[(start_date <= df[x_col_name]) & (df[x_col_name] <= end_date)] scatter_fig = px.scatter(filtered_df, x=x_col_name, y=y_col_name_list)
Đọc config từ header của CSV
Xét header của CSV như sau
DateTime(yyyyMMdd HH:mm:ss.fff),DATA 1(minFilter=20;maxFilter=100),DATA 2(maxFilter=140),DATA 3,DATA 4,DATA 5
Chúng ta sẽ thêm chức năng đọc config từ header trên:
- Đọc config của cột DateTime để set format date time (hiện tại đang set cứng trong code).
- Đọc config minFilter, maxFilter của các cột DATA để lọc bỏ những data có giá trị nhỏ hơn minFilter và lớn hơn maxFilter của cột DATA đó.
Đầu tiên thêm file utils.py chứa các function common
import re _format_convertor = ( ('yyyy', '%Y'), ('yyy', '%Y'), ('yy', '%y'), ('y', '%y'), ('MMMM', '%B'), ('MMM', '%b'), ('MM', '%m'), ('M', '%m'), ('dddd', '%A'), ('ddd', '%a'), ('dd', '%d'), ('d', '%d'), ('HH', '%H'), ('H', '%H'), ('hh', '%I'), ('h', '%I'), ('mm', '%M'), ('m', '%M'), ('ss', '%S'), ('s', '%S'), ('tt', '%p'), ('t', '%p'), ('fff', '%f'), ('zzz', '%z'), ('zz', '%z'), ('z', '%z'), ) def convert_py_datetime_format(in_format): out_format = '' while in_format: if in_format[0] == "'": apos = in_format.find("'", 1) if apos == -1: apos = len(in_format) out_format += in_format[1:apos].replace('%', '%%') in_format = in_format[apos + 1:] elif in_format[0] == '\\': out_format += in_format[1:2].replace('%', '%%') in_format = in_format[2:] else: for intok, outtok in _format_convertor: if in_format.startswith(intok): out_format += outtok in_format = in_format[len(intok):] break else: out_format += in_format[0].replace('%', '%%') in_format = in_format[1:] return out_format def extract_csv_col_config(col_name: str): try: found = re.search('\\((.*)\\)', col_name) col_name = col_name.replace(found.group(0), '') config_string = found.group(1) config_list = config_string.split(';') configs = [] for config in config_list: key_value_list = config.split('=') key = key_value_list[0] value = key_value_list[1] if len(key_value_list) > 1 else None configs.append((key, value)) except AttributeError: configs = [] return col_name, configs
Ở đoạn code trên:
- Function convert_py_datetime_format dùng để chuyển đổi format kiểu yyyyMMdd HH:mm:ss.fff sang dạng format của Python.
- Function extract_csv_col_config sẽ nhận vào tên của cột chứa config và trả về tên cột đã cắt bỏ phần string config cùng với array chứa các config của cột đó. Ví dụ
DATA 1(minFilter=20;maxFilter=100)
sẽ trả vềDATA 1
và array[(minFilter, 20), (maxFilter, 100)]
Tiếp theo thêm function process_csv_variable vào app.py
from datetime import datetime, timedelta import dash import numpy as np import pandas as pd import plotly.express as px from dash import dcc, Output, Input from dash import html from utils import extract_csv_col_config, convert_py_datetime_format def process_csv_variable(df_param): # process x-axis csv variable old_x_col_name = df_param.columns[0] new_x_col_name, configs = extract_csv_col_config(old_x_col_name) datetime_format = configs[0][0] df_param = df_param.rename(columns={old_x_col_name: new_x_col_name}) df_param[new_x_col_name] = pd.to_datetime(df_param[new_x_col_name], format=convert_py_datetime_format(datetime_format)) # process y-axis csv variable y_col_name_list = df_param.columns[1:] for old_y_col_name in y_col_name_list: new_y_col_name, configs = extract_csv_col_config(old_y_col_name) df_param = df_param.rename(columns={old_y_col_name: new_y_col_name}) for config, value in configs: if config == 'minFilter': df_param.loc[df_param[new_y_col_name] < int(value), new_y_col_name] = np.nan elif config == 'maxFilter': df_param.loc[df_param[new_y_col_name] > int(value), new_y_col_name] = np.nan return df_param app = dash.Dash(__name__) app.layout = html.Div(id='container', children=[ dcc.DatePickerRange( id='date-picker-range', minimum_nights=0, display_format='YYYY/MM/DD' ), dcc.Graph(id='scatter-graph'), ]) @app.callback( Output('date-picker-range', 'start_date'), Output('date-picker-range', 'end_date'), Input('container', 'id') ) def update_date_picker(id): df = pd.read_csv('csv/app_sample.csv') df = process_csv_variable(df) x_col_name = df.columns[0] init_start_date = df[x_col_name].min().strftime('%Y-%m-%d') init_end_date = df[x_col_name].max().strftime('%Y-%m-%d') return init_start_date, init_end_date @app.callback( Output('scatter-graph', 'figure'), Input('date-picker-range', 'start_date'), Input('date-picker-range', 'end_date') ) def update_figure(start_date, end_date): df = pd.read_csv('csv/app_sample.csv') df = process_csv_variable(df) if start_date is not None and end_date is not None: start_date = datetime.fromisoformat(start_date) end_date = datetime.fromisoformat(end_date) + timedelta(days=1) # get first columns name for x-axis x_col_name = df.columns[0] # get list column name except first column for y-axis y_col_name_list = df.columns[1:] filtered_df = df[(start_date <= df[x_col_name]) & (df[x_col_name] <= end_date)] scatter_fig = px.scatter(filtered_df, x=x_col_name, y=y_col_name_list) return scatter_fig if __name__ == '__main__': app.run_server(debug=True)
Function process_csv_variable sẽ nhận vào DataFrame, đọc config từ tên cột, xử lý data dựa theo config và sẽ trả về DataFrame sau khi xử lý.
Bây giờ chúng ta thêm file csv/app_sample.csv để test
DateTime(yyyyMMdd HH:mm:ss.fff),DATA 1(minFilter=20;maxFilter=100),DATA 2(maxFilter=140),DATA 3,DATA 4,DATA 5 20211219 101010.010,10,200,178,90,110 20211219 111010.020,20,150,134,25,120 20211219 121010.030,5,130,210,11,90 20211219 131010.040,15,110,100,-97,80 20211219 141010.050,60,150,143,-17,130 20211219 151010.060,30,140,132,30,140 20211219 161010.070,20,180,167,45,150 20211219 171010.080,16,120,240,123,160 20211219 181010.090,75,190,153,40,150 20211219 191010.100,90,250,162,-10,170 20211220 001010.000,68,142,156,1,180 20211220 011010.010,40,110,134,65,130 20211220 021010.020,50,140,214,149,190 20211220 031010.030,60,150,169,-98,200 20211220 041010.040,70,160,204,-173,190 20211220 051010.050,80,170,164,-108,180 20211220 061010.060,90,180,148,150,170 20211220 071010.070,100,190,180,92,150 20211220 081010.080,110,200,268,94,160 20211220 091010.090,120,210,164,-139,140 20211220 101010.100,130,220,254,-132,130 20211221 001010.000,10,90,142,30,150 20211221 011010.010,30,100,162,55,160 20211221 021010.020,80,120,180,20,170 20211221 031010.030,70,110,176,-10,110 20211221 041010.040,50,130,194,-90,90 20211221 051010.050,60,140,202,-120,80 20211221 061010.060,90,150,164,100,70 20211221 071010.070,120,160,197,132,60 20211221 081010.080,110,170,186,40,50 20211221 091010.090,130,210,182,-130,40 20211221 101010.100,120,230,210,-100,30
Trong terminal chạy lệnh python app.py
và truy cập vào http://127.0.0.1:8050/ để xem kết quả.
Để dễ kiểm tra kết quả chúng ta ẩn các data khác chỉ hiển thị DATA 1
Chúng ta thấy các data có value nhỏ hơn 20 và lớn hơn 100 đã bị lọc bỏ.
Source code
https://gitlab.com/bwv-hp/python-dash-sample