Học Qlik sense qua ví dụ: Xử lý dữ liệu dạng cây và đồ thị

Reload script trong Qlik có thể xem là một ngôn ngữ lập trình hoàn chỉnh với các chức năng căn bản: hàm, biến, vòng lặp, rẽ nhánh. Tuy vậy, Qlik script không cung cấp nhiều công cụ và các bộ thư viện sẵn có bằng các ngôn ngữ chuyên nghiệp như Python, C hoặc Java. Hạn chế này gây khó khăn đáng kể khi cần hiện thực các giải thuật và cấu trúc dữ liệu phức tạp hơn như hướng đối tượng, mảng (array), tập hợp (set), v.v.

Tuy nhiên, Qlik script lại rất mạnh về xử lý dữ liệu dạng bảng hoàn toàn trong bộ nhớ. Vì vậy, nếu thay đổi góc nhìn về bài toán theo hướng sử dụng bảng dữ liệu, bạn vẫn có thể giải quyết khá nhiều vấn đề tưởng như khó xử lý được trong Qlik. Ví dụ: Qlik không có kiểu dữ liệu array, tuy nhiên bảng với 1 cột dữ liệu có thể được dùng như một array.

Nhân dịp VietQlikies vừa có một thảo luận khá thú vị trên Qlik Community (xem post tại đây) với chủ đề xử lý dữ liệu dạng cây, bài viết sau đây sẽ trình bày lại ví dụ trong community post để minh họa quá trình phân tích để giải bài toán bằng Qlik script.

Mô tả bài toán

Câu hỏi ban đầu trong community post có thể tóm tắt như sau:

  • Qlik dashboard được xây dựng để theo dõi các biểu mẫu (form) trong một tổ chức
  • Mỗi form được đặc trưng bởi một FormID
  • Mỗi form có thể là form gốc (original) hoặc được copy từ một form khác.
  • Bảng dữ liệu được cho như sau. Dòng đầu tiên và cuối cùng có nghĩa “Form9 được copy từ Form10”“Form10 được copy từ Form2”.
  • Một form có thể được update/copy nhiều lần từ các form khác, kể cả từ các bản copy của chính form đó. Ví dụ, trong bảng trên có 2 dòng “8-7” và “7-8”

Yêu cầu bài toán là tìm ra form gốc ban đầu cho mỗi form trong cột FormID, biết form gốc luôn có ID nhỏ nhất trong một chuỗi những lần copy. Trong bảng trên, form gốc của Form10 Form2. Form gốc của tất cả các form còn lại là Form1.

Giải bài toán bằng cấu trúc cây

Xét riêng trường hợp FormID = 9. Để truy ra form gốc của Form9, bạn có thể hình dung bảng dữ liệu trên ở dạng cấu trúc cây như hình dưới. Trong đó 9 → 10 và 9 → 8 có nghĩa “Form9 được copy từ Form10 và Form8“, tương ứng với 2 dòng đầu tiên trong bảng. Như vậy, trong cây dữ liệu, nếu A được copy từ B thì A là node cha và B là node con.

Cây được xây dựng qua nhiều bước lặp. Ở bước đầu tiên, form cần tìm gốc (ở đây là Form9) được đặt vào node gốc. Ở các bước tiếp theo, ta duyệt bảng dữ liệu để thêm các node con vào cây. Ở mỗi bước, nếu 1 node con đã tồn tại trong cây, ta sẽ bỏ qua không thêm node đó vào cây nữa (các node đánh dấu đỏ trong hình).

Vì form gốc ban đầu luôn có ID nhỏ nhất, dễ thấy form ban đầu cần tìm cho node gốc chính là node có ID nhỏ nhất trong cây. Như vậy, với FormID = 9, form gốc cần tìm là Form1. Với mỗi form cần tìm gốc, ta có thể xây dựng một cây tương tự.

Hiện thực giải thuật với Qlik script

1. Chuẩn bị dữ liệu

Bạn có thể load dữ liệu từ data file hoặc 1 inline load đơn giản như sau:

// Sample data
Copies:
LOAD * Inline [
FormID, CopyID
9, 10
9, 8
8, 10
8, 7
7, 1
7, 8
10, 2
];

2. Xây dựng hàm tìm original form

Bây giờ chúng ta sẽ tìm cách sử dụng reload script của Qlik để hiện thực giải thuật xây dựng cây như trình bày ở trên. Bản chất các bước lặp là tuần tự thêm các node con vào cây, sau đó chọn ra node con có ID nhỏ nhất. Vì vậy, ta có thể dùng mảng (array) một chiều để lưu các ID được thêm vào cây qua mỗi bước lặp như hình minh họa bên dưới. Sau bước lặp cuối cùng, kết quả cần tìm chính là phần tử nhỏ nhất trong array.

Vì reload script trong Qlik không hỗ trợ kiểu dữ liệu array, ta có thể tạo 1 table với 1 cột duy nhất để thay thế array. Dưới đây là code của hàm tìm original form ID, trong đó sử dụng bảng Original với vai trò như một array:

// Hàm tìm original form
// Trả về kết quả trong biến vOriginalFormID
Sub FindOriginal(vSearchFormID)
	vPos = 0;
	vCurrentID = vSearchFormID;
	
	// Dùng table với 1 cột làm array
	// Lấy form cần tìm original làm node gốc
	Original:
	LOAD * INLINE [
	OriginalFormID_Temp
	$(vSearchFormID)];
	
	vOriginalLength = NoOfRows('Original');
	
	// Lặp đến khi tất cả FormID được duyệt hết
	Do While vPos < vOriginalLength
		vCurrentID = Peek('OriginalFormID_Temp',vPos);
		
		Copies_Temp:
		LOAD DISTINCT CopyID as CopyID_Temp Resident Copies Where FormID = $(vCurrentID);
		
		// Thêm các node con vào array
		Concatenate (Original)
		LOAD CopyID_Temp as OriginalFormID_Temp Resident Copies_Temp WHERE NOT Exists(OriginalFormID_Temp,CopyID_Temp);
			
		DROP TABLE Copies_Temp;
		
		vPos = vPos + 1;
		vOriginalLength = NoOfRows('Original');
	Loop;
	
	// Lấy giá trị nhỏ nhất trong bảng Original
	MinOriginal:
	LOAD
	     MIN(OriginalFormID_Temp) AS MinOriginalFormID
	RESIDENT Original;

	vOriginalFormID = Peek('MinOriginalFormID');
	DROP TABLES Original, MinOriginal;
End Sub;

3. Test thử hàm FindOriginal

Câu lệnh DROP trong đoạn code trên xóa bảng tạm Original (dùng làm array) sau khi tìm ra giá trị original form. Để hiểu hơn về cách thức hoạt động của đoạn code, ta tạm thời comment câu lệnh DROP:

//DROP TABLES Original, MinOriginal;

và chạy thử hàm với tham số đầu vào vSearchFormID = 9

vFormID=9;
Call FindOriginal(vFormID);

Sau khi reload script, mở data model viewer, ta thấy bảng Original chứa mảng giá trị [9, 10, 8, 2, 7, 1] như minh họa trong hình ở phần (2). Ta cũng thấy biến vOriginalFormID lưu giá trị cần tìm là 1 (form gốc của Form9 Form1).

Thử thêm một số ví dụ khác:

  • Cho vFormID=7, ta có array [7, 1, 8, 10, 2] và kết quả vOriginalFormID=1
  • Cho vFormID=10, ta có array [10, 2] và kết quả vOriginalFormID=2

4. Áp dụng hàm FindOriginal cho tất cả các FormID

Sau khi xác nhận hàm FindOriginal hoạt động đúng, ta sẽ áp dụng hàm này cho tất cả các FormID. Trước tiên, bỏ comment ở câu lệnh DROP và xóa các câu lệnh test trong phần (3). Sau đó duyệt qua các FormID với đoạn code sau:

// Lấy danh sách các FormID duy nhất
Copies_Distinct:
LOAD DISTINCT FormID RESIDENT Copies;

// Bảng kết quả lưu giá trị orginial form của mỗi FormID
Copies_Original:
LOAD * INLINE [FormID, OriginalFormID];

// Lặp qua mỗi FormID và áp dụng sub FindOriginal
For i=0 to NoOfRows('Copies_Distinct') 
	vFormID = Peek('FormID',i,'Copies_Distinct');
	Call FindOriginal(vFormID);
	
	Concatenate(Copies_Original)
	LOAD * INLINE [
		FormID, OriginalFormID
		$(vFormID), $(vOriginalFormID)
	];
Next;

DROP TABLE Copies_Distinct;

Kết quả thu được sau khi reload giống với dự kiến: Form2 là form gốc của Form10, Form1 là gốc của các form 7,8,9.

Biểu đồ trong hình minh họa trên được vẽ bằng extension “Directed Graph”, download từ Qlik Branch. Bạn có thể tham khảo bài viết Học Qlik Sense qua ví dụ: Trump nói gì trên Twitter?  để xem cách cài đặt extension trong Qlik Sense Desktop.

Mở rộng bài toán: Đồ thị vô hướng

Sau khi thảo luận thêm trong community post, yêu cầu của người post được làm rõ hơn: Thật ra người post muốn form gốc là form có ID nhỏ nhất trong 1 nhóm các form có quan hệ copy với nhau. Như trong tập dữ liệu trên, form gốc của Form10 phải là Form1 thay vì Form2, dù Form10 được copy từ Form2 và không có liên hệ nào giữa Form1 Form2 trong bảng dữ liệu ban đầu.

Với yêu cầu mới này, bài toán có thể giải theo hướng hình dung bảng dữ liệu như một đồ thị vô hướng thay vì dùng cây như phương án trên. Cụ thể, các bước cần làm với bảng dữ liệu bao gồm:

  1. Xây dựng một đồ thì vô hướng từ bảng dữ liệu. Trong đồ thị vô hướng, các sự kiện “A được copy từ B”“B được copy từ A” đều được thể hiện bằng một cạnh A–B không có hướng duy nhất giữa 2 node A và B.
  2. Xác định các “đảo” (island – là một nhóm node liên kết với nhau nhưng không liên kết với nhóm node khác) trong đồ thị
  3. Ở mỗi island, form gốc của tất cả các form trong island là form có ID nhỏ nhất trong island đó

Ví dụ, khi thêm 3 dòng (20, 22), (21, 20), (20, 21) vào bảng dữ liệu ban đầu

Copies:
LOAD * Inline [
FormID, CopyID
9, 10
9, 8
8, 10
8, 7
7, 1
7, 8
10, 2
20, 22
21, 20
20, 21
];

ta sẽ xây dựng được đồ thị vô hướng như hình dưới. Đồ thị có 2 island tách biệt nhau, với 1 và 20 là 2 node nhỏ nhất trong 2 island đó. Từ đồ thị này, ta có câu trả lời Form1 là gốc của các form 2,7,8,9,10 và Form20 là gốc của các form 21,22.

Với phương án dùng cây dữ liệu đã có sẵn ở trên, ta có thể xây dựng nhanh đồ thị vô hướng bằng cách chuyển các liên kết 1 chiều A → B thành liên kết 2 chiều A <–> B. Như vậy, ta có thể duyệt qua bảng dữ liệu, với mỗi dòng như (20,22), ta có thể thêm dòng (22,20) vào bảng nếu (22,20) chưa có sẵn trong bảng. Đoạn script để làm công việc này là như sau:

Copies_Undirected_Temp:
NoConcatenate LOAD Distinct * Resident Copies;

Concatenate(Copies_Undirected_Temp) 
LOAD CopyID AS FormID,
     FormID AS CopyID
Resident Copies_Undirected_Temp;

Copies_Undirected:
NoConcatenate LOAD Distinct * Resident Copies_Undirected_Temp;

DROP TABLE Copies_Undirected_Temp;

Trong phần còn lại của script, bao gồm cả hảm FindOriginal, bạn dùng dữ liệu từ bảng Copies_Undirected thay vì bảng Copies ban đầu. Đây chưa phải là lời giải tối ưu, vì phải duyệt qua từng FormID xây dựng 1 đồ thị vô hướng riêng cho mỗi form. Bạn thử tìm cách tối ưu hóa script (chỉ xây dựng đồ thị vô hướng 1 lần) xem sao nhé!

Bạn có thể tham khảo ứng dụng mẫu tại VietQlikies Public Share – Blog 38

Kết luận

Hẳn bạn từng nghe ngôn ngữ lập trình không quan trọng bằng tư duy lập trình. Ví dụ như khi làm việc với MapReduce trong Hadoop, mọi yêu cầu xử lý dữ liệu phải được thiết kế lại theo logic Map + Reduce. Tương tự như vậy, với reload script trong Qlik, bạn cần nhớ cơ chế chủ yếu của Qlik là xử lý từng hàng (line-by-line) trong mỗi bảng dữ liệu. Tận dụng được cơ chế này khi thiết kế logic xử lý dữ liệu sẽ giúp bạn giải quyết được khá nhiều vấn đề thực tế.

Ngoài việc sử dụng table để thay thế cho kiểu array/ list/ tuple trong các ngôn ngữ lập trình khác, bạn còn có thể dùng các loại bảng sau trong Qlik script để hiện thực các cấu trúc dữ liệu phổ biến:

  • Từ điển dữ liệu (Dictionary) → Dùng Mapping table (Mapping Load)
  • Ngăn xếp (Stack)/ Hàng đợi (Queue) → Dùng table bình thường, sau đó dùng hàm Peek() để lấy ra từng phần tử dữ liệu ở vị trí phù hợp
  • Tập hợp (Set) => Dùng câu lệnh LOAD DISTINCT để tạo bảng giá trị đơn nhất

Một suy nghĩ 1 thoughts on “Học Qlik sense qua ví dụ: Xử lý dữ liệu dạng cây và đồ thị

Bình luận về bài viết này