Bài 43: Thiết kế và triển khai mô hình máy học dự báo giá nhà – Part 1

Ở bài học trước, Tui đã trình bày Tổng quan về tích hợp Mô hình Máy học vào hệ thống Kinh doanh – Quản lý, các bạn đã biết được chu trình cần thiết cho việc xây dựng một mô hình máy học rồi. Trong bài này chúng ta tiến hành coding, Tui sẽ dùng Hồi quy tuyến tính để tạo mô hình máy học dự báo giá nhà từ tập dữ liệu mẫu Kaggle (tải ở đây) được lưu dưới định dạng CSV, khoảng 5,000 dòng dữ liệu.

Lý thuyết về hồi quy tuyến tính được Anh Vũ Hữu Tiệp và Cộng Sự trình bày rất kỹ trên blog Machine Learning Cơ Bản nên các bạn đọc trực tiếp trên này, hoặc đọc từ Wikipedia, do đó Tui không có nhắc lại phần lý thuyết.

Part 1 này Tui đi trực tiếp vào ứng dụng giải thuật máy học được cung cấp từ một số thư viện trong Python để tạo mô hình máy học dự báo giá nhà cho tập dữ liệu Kaggle ở trên, sử dụng Pycharm (các bạn có thể dùng Jupyter bằng cách cài đặt Anaconda3) chạy dạng Console, Part 2 Tui sẽ hướng dẫn các bạn cách chạy trên giao diện tương tác người dùng.

Các bạn thực hiện theo các bước sau:

Bước 1: Tạo dự án tên “HousingPricePrediction” trong Pycharm

Trong Project tạo một thư mục dataset và tải dữ liệu rồi lưu vào thư mục này

https://tranduythanh.com/datasets/USA_Housing.csv

Tui mô tả sơ lược dataset này:

  • Avg. Area Income: Thu nhập trung bình tại khu vực ngôi nhà đã bán
  • Avg. Area House Age: Trung bình tuổi của một ngôi nhà đã bán (số năm sử dụng)
  • Avg. Area Number of Rooms: Trung bình diện tích các phòng
  • Avg. Area Number of Bedrooms: Trung bình diện tích phòng ngủ
  • Area Population: Dân số tại khu vực bán nhà
  • Price: Giá ngôi nhà đã bán
  • Address: Địa chỉ ngôi nhà bán

Trong bài này ta không sử dụng thuộc tính Address.

Nhiệm vụ của chúng ta là xây dựng 1 mô hình máy học dựa trên tập dữ liệu này, khi người dùng nhập vào Area Income, House Age, Number of Rooms, Number of Bedroooms, Area Population thì mô hình sẽ dự báo Giá của căn nhà này là bao nhiêu.

Bước 2: Tạo một file Python “HousingPriceSimpleModel.py”:

Bước 3: Bổ sung các thư viện ở đầu file “HousingPriceSimpleModel.py”

# 01.Library for dataset processing
import pandas as pd
import numpy as np
# 02. Library for train model
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
# 03. Library for evaluation model
from sklearn import metrics
# 04. Library for saving model
import pickle

Ở trên Tui khai báo 4 nhóm thư viện bao gồm:

  • Nhóm thư viện xử lý dataset (01) : Đọc dữ liệu, tiền xử lý loại bỏ cột dữ liệu không sử dụng, trích lọc các biến độc lập (các cột thuộc tính), biến phụ thuộc (cột price)… Chúng ta dùng pandas, numpy
  • Nhóm thư viện để train model máy học (02): Bao gồm thư viện tách lọc tập train và test, và bài này chúng ta dùng hồi quy tuyến tính (LinearRegression)
  • Nhóm thư viện đánh giá chất lượng mô hình (03): chúng ta dùng thư viện metrics
  • Nhóm thư viện để xuất mô hình máy học được train xuống ổ cứng (04): Ta dùng thư viện pickle

Bước 4: Tiếp tục bổ sung mã lệnh đọc dữ liệu, xem các thông tin cũng như tạo ma trận X và mảng y

#use pandas to read CSV dataset
df = pd.read_csv('dataset/USA_Housing.csv')
#call functions about get dataset information:
print(df.head())
print(df.info())
print(df.describe())
print(df.columns)

Thực hiện các Lệnh ở trên ta có kết quả:

   Avg. Area Income  ...                                            Address
0      79545.458574  ...  208 Michael Ferry Apt. 674\nLaurabury, NE 3701...
1      79248.642455  ...  188 Johnson Views Suite 079\nLake Kathleen, CA...
2      61287.067179  ...  9127 Elizabeth Stravenue\nDanieltown, WI 06482...
3      63345.240046  ...                          USS Barnett\nFPO AP 44820
4      59982.197226  ...                         USNS Raymond\nFPO AE 09386

[5 rows x 7 columns]
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5000 entries, 0 to 4999
Data columns (total 7 columns):
 #   Column                        Non-Null Count  Dtype  
---  ------                        --------------  -----  
 0   Avg. Area Income              5000 non-null   float64
 1   Avg. Area House Age           5000 non-null   float64
 2   Avg. Area Number of Rooms     5000 non-null   float64
 3   Avg. Area Number of Bedrooms  5000 non-null   float64
 4   Area Population               5000 non-null   float64
 5   Price                         5000 non-null   float64
 6   Address                       5000 non-null   object 
dtypes: float64(6), object(1)
memory usage: 273.6+ KB
None
       Avg. Area Income  Avg. Area House Age  ...  Area Population         Price
count       5000.000000          5000.000000  ...      5000.000000  5.000000e+03
mean       68583.108984             5.977222  ...     36163.516039  1.232073e+06
std        10657.991214             0.991456  ...      9925.650114  3.531176e+05
min        17796.631190             2.644304  ...       172.610686  1.593866e+04
25%        61480.562388             5.322283  ...     29403.928702  9.975771e+05
50%        68804.286404             5.970429  ...     36199.406689  1.232669e+06
75%        75783.338666             6.650808  ...     42861.290769  1.471210e+06
max       107701.748378             9.519088  ...     69621.713378  2.469066e+06

[8 rows x 6 columns]
Index(['Avg. Area Income', 'Avg. Area House Age', 'Avg. Area Number of Rooms',
       'Avg. Area Number of Bedrooms', 'Area Population', 'Price', 'Address'],
      dtype='object')

Process finished with exit code 0

Bước 5: Xây dựng và train mô hình dự báo giá nhà

Ta cần tách dữ liệu thành một ma trận X chứa các đặc trưng cần đào tạo (gọi là các biến độc lập) và một mảng y với biến mục tiêu (biến phụ thuộc), trong trường hợp này thì cột y chính là cột Giá(Price). Cột Địa chỉ (Address) sẽ được loại bỏ khỏi mô hình vì nó chỉ có thông tin văn bản mà mô hình hồi quy tuyến tính này không sử dụng. Điều đó có nghĩa:

#set X matrix
#df.columns[:5] meaning:
#['Avg. Area Income', 'Avg. Area House Age', 'Avg. Area Number of Rooms',
# 'Avg. Area Number of Bedrooms', 'Area Population']
X = df[df.columns[:5]]
y = df['Price']
# Printing for observation:
print(X)
print(y)

Thực hiện các Lệnh ở trên ta có kết quả:

      Avg. Area Income  ...  Area Population
0         79545.458574  ...     23086.800503
1         79248.642455  ...     40173.072174
2         61287.067179  ...     36882.159400
3         63345.240046  ...     34310.242831
4         59982.197226  ...     26354.109472
...                ...  ...              ...
4995      60567.944140  ...     22837.361035
4996      78491.275435  ...     25616.115489
4997      63390.686886  ...     33266.145490
4998      68001.331235  ...     42625.620156
4999      65510.581804  ...     46501.283803

[5000 rows x 5 columns]
0       1.059034e+06
1       1.505891e+06
2       1.058988e+06
3       1.260617e+06
4       6.309435e+05
            ...     
4995    1.060194e+06
4996    1.482618e+06
4997    1.030730e+06
4998    1.198657e+06
4999    1.298950e+06
Name: Price, Length: 5000, dtype: float64

Thông thường ta cần chia tập dữ liệu ra làm 2: train và set. Tỉ lệ tùy thuộc vào dữ liệu cũng như kinh nghiệm. Giả sử ở đây ta chi theo tỉ lệ 80% cho train và 20% cho test.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=101)

lm = LinearRegression()

lm.fit(X_train, y_train)

Mã lệnh ở trên sẽ tách tập dataset ban đầu thành 2 tập, tập train và tập test với tỉ lệ lần lượt là 80% và 20%, sau đó dùng LinearRegression() để train mô hình máy học.

Bước 6: Thử nghiệm mô hình

Chúng ta có thể đánh giá mô hình sau đó thử nghiệm, ở bước này Tui sẽ thử nghiệm mô hình sau đó sẽ đánh giá lại để quyết định xem có nên xuất ra ổ cứng để sử dụng sau này hay không.

  • Thử nghiệm lần thứ 1: Lấy ngẫu nhiên tập dữ liệu thứ 0 trong iloc của X_test
print("Input 1:")
print([X_test.iloc[0]])

pre1 = lm.predict([X_test.iloc[0]])
print("Housing Price prediction 1 =", pre1)

Thực thi lệnh ở trên ta có kết quả:

Input 1:
[Avg. Area Income                66774.995817
Avg. Area House Age                 5.717143
Avg. Area Number of Rooms           7.795215
Avg. Area Number of Bedrooms        4.320000
Area Population                 36788.980327
Name: 1718, dtype: float64]
Housing Price prediction 1 = [1257919.729097]

So sánh với dữ liệu gốc, với các thông số input ở trên thì ta có dữ liệu giá gốc là (bạn tra lại dữ liệu gốc trong file CSV):

1251688.616

Còn mô hình ta tính ra được:

1257919.729097

Như vậy giá dự báo cũng khá sát với giá gốc

  • Thử nghiệm lần thứ 2, các giá trị đầu vào Tui sẽ lấy y chóc như lần thử nghiệm thứ 1, tuy nhiên Tui không dùng iloc[0] của X_test, mà Tui sẽ nhập trực tiếp:
print("Input 2:")
input2=[66774.995817, 5.717143, 7.795215, 4.320000, 36788.980327]
pre2 = lm.predict([input2])
print("Housing Price prediction 2 =", pre2)

Thực thi lệnh ở trên ta có kết quả:

Input 2:
[66774.995817, 5.717143, 7.795215, 4.32, 36788.980327]
Housing Price prediction 2 = [1257919.71744578]

So sánh kết quả so với lần thử nghiêm thứ 1 ta thấy kết quả hầu như chính xác như nhau.

  • Thử nghiệm lần thứ 3, Tui sử dụng một mảng dữ liệu đầu vào khác:
print("Input 3:")
input3=[21.566696, 165453.0425, 120499.8391, 1999.785336, 15.340604]
print(input3)
pre3 = lm.predict([input3])
print("Housing Price prediction 3 =", pre3)

Thực thi lệnh ở trên ta có kết quả:

Input 3:
[21.566696, 165453.0425, 120499.8391, 1999.785336, 15.340604]
Housing Price prediction 3 = [4.18962799e+10]

Nếu bạn muốn kiểm tra toàn bộ dữ liệu prediction trên tập test thì có thể code như sau:

predictions = lm.predict(X_test)
print("Full Housing Price Predictions:")
print(predictions)

Thực thi lệnh ở trên ta có kết quả:

Full Housing Price Predictions:
[1257919.729097    822112.41884197 1740669.05865495  972452.12917494
  993422.26329315  644126.07414456 1073911.79101682  856584.00194133
 1445318.25518365 1204342.19071174 1455792.46212863 1298556.65696433
 1735924.33836095 1336925.77577789 1387637.43231922 1222403.77772069
  613786.28691982  963933.54416267 1221197.33050139 1198071.57563599
  505861.89531673 1769106.54723353 1853881.16842511 1200369.50507868
 1065129.1285072  1812033.73067312 1768686.47091262 1439920.83814323
 1387251.99649376 1541178.39224181  726418.80504278 1754497.60908688
 1462185.72653198 1025600.16081145 1284926.8685155   917454.59590853
 1187046.94956865  999330.91117487 1329536.63409595  782191.60441437
 1393272.03053841  578216.88394851  822643.37151048 1895533.11413226
 1672019.84892041  966926.45434583 1129674.55638146  792797.75914663
 1161057.18412143 1472396.71417164 1457656.70412313 1162939.334229
 1099453.68110992 1358107.44619413  841103.70380593  986322.30573616
 1123323.53002134 1253538.63235181 1428279.66304074  499103.20906177
 1462817.08350573 1108744.36856804  660588.51700889 1247031.59801316
 1342744.55515752 1357505.99080656  818858.41205464 1487168.7491193
 1396247.6559861   885078.37963981  857545.83060842 1212816.45100842
 1101069.27148301 1834591.82895367  924022.96601965  855346.8084446
  822070.44924512 1397829.2123058   775933.8643152  1638075.3740563
 1079223.32119183 1355542.10150295 1321447.90227179 1198773.49593884
  757960.00844907  649708.59768215 1058567.42526768 1384318.81434923
 1030349.32788272 1297790.45656826  550588.21509211 1706268.87594955
  840478.99085965 1627285.73221856 1051446.08561746  947163.26591115
 1187965.14618703 1556566.4890801   900538.67981881  995246.18312522
 1221693.84495786 1592557.49052096  977671.97252197 1428171.2008612
 1015834.61615161 1448110.17850637  877865.27098758  831194.85890213
 1022735.97232827 1844169.94971864 1925418.90852406 1405526.2235022
 1042730.58532361 1056912.41311355 1440167.01805755 1029466.31854915
 1188198.60368704 1407875.62844789 1266581.80632292 1562660.40296843
  767006.37537442 1394809.1474023  2083454.34315169 1468363.78038259
 1326116.1692144  1314342.13126912  920802.61547049  780407.5175075
 1678929.25641696 1173317.53334908  863938.9328098   831496.26628413
 1552893.33508711  814232.28006267  628932.80758343 1230219.74082397
 1009051.68059211 1134177.07317901 1759135.80092859 1185004.78325484
  589995.18477429 1392467.64100483 1381608.25483992 1726383.2023419
 1169019.22666496 1019912.31831202 1730374.72274501 1094660.75381921
 1641553.67845519 1425844.82715513 1372526.00488651  957370.57751744
 1758571.00815348 1215039.09536614 1228730.36130002  949454.36633531
 1240479.54491375 1259976.01494439 1275292.19649843 1125707.61597875
 1513693.61294972 1559271.12556507  798393.94186589 1546245.51538456
 1278490.24324258 1048130.23839099 1258823.84340798 1647111.76109968
 1110683.6841632   474642.39148037 1647293.82576375 1314423.54403662
 1667425.25325174 1673875.47188459 1691912.07367635 1809235.34507848
 1557560.5512079   872642.52777441 1383629.88700025 1588489.67118679
 1394366.66112741 1126784.35366251 1453577.32336133  891270.72483827
  929092.04974816 1124619.49617814 1189423.55209047 1093519.52228545
 1352603.44998751 1104585.87337573 1041445.01140685 1840823.20915414
 1269025.97772559 1195900.79478608 1982501.87427729 1172318.99196702
  664063.71770282 1516831.57884726 1208883.11787161 1122128.21428225
 1735594.69613153  865742.02321862  969966.3835537  1331559.38783137
  909257.9004405  1330515.26483805 1267149.95713822  971617.92004141
 1240075.55463063 1990811.03720136 1502950.35445567 1142677.03057216
 1889344.61144088 1542683.11071993 1255495.76569687 1266858.02867607
 1336567.39146564  250802.81939456 1128344.25997861 1251829.92396526
 1365458.36530684 1731381.40176637 1048930.57357235 1054510.18009109
 1426952.23586587  930090.24788653  866136.48384222 1648292.85950441
 1183126.48975361 1342085.93169395  818757.06161575 1563303.58722225
 1348611.35759588 1815034.40114857 1260025.43364773 1220361.24580727
  648792.09698542 1331385.0939024   393964.58716725 1283083.6608295
 1354368.39181222 1594675.47475904 1019367.99761248 1154895.92538237
 1477596.50350931 1606153.12418652 1336855.88411859 1787639.82065234
 1798694.76607084  945560.93733933 1240280.37820927 1131796.8979187
 1506418.6409881  1172398.896648   1575031.41932382 1620022.75906772
  328146.62358475  423405.29747082  808473.31516009 1808607.71767717
 1161437.6335028  1290105.28066273  488495.93102175 1355523.83914854
 1791747.74473713  530190.7597526   895829.94850127 1234872.22552441
 1205813.61309886 1142869.05130465 1544709.4939468  1397728.97367436
  809314.75267993  916515.5408125  1717159.66171621 1071915.80625235
 1265594.03975673 1563516.24575786 1150899.12933388 1087806.80720891
 1723353.35034214 1401272.11235719 1097379.04885339 1642119.10859682
 1062758.32253009 1729910.73155036 1038760.9997124  1360748.10902365
 1345562.75881699 1331901.19961839 1002669.41036228 1344417.49481131
 1130066.69205505 1559820.77379468 1706430.10202814 2038885.49719396
  833086.74048143 1045214.81139648 1134207.36502681  935557.862612
 1372030.49915441  815056.03547679 1247496.29555842 1363128.23823989
 1496484.74422308 2229545.19220177  777432.21374589 1191745.78488591
 1096163.09011392 1272909.82942116 1625931.48842005  918993.98721316
 1006465.36876058 1594755.7658865   846284.31587657  849305.27099749
  570666.9766973  1801007.09806189 1485857.7010122  1686344.68445166
 1231934.75994846 1356156.40905842 1097791.78972486 1193394.13367233
 1126904.82603302 1084596.34251133 1044677.128424   2012287.09940269
  749703.83822341 1125570.87522046 1353902.80662881 1582337.54477098
 1132363.84344702 1127593.01594927 1558175.09336577 1470124.64995527
 1170136.96370427 1057993.19774258 1727175.42480496 1099174.32885364
 1878927.95333323  953041.98641305  220148.95728994 1469299.4012682
 1351934.36007778 1965515.79787762  471073.53784835 1078285.07736102
 1398197.71162202 1034751.6214149   792832.03083203 1446399.20930511
 1103988.74382329 1691306.84800526 1107502.12092221 1078162.52791708
 1708451.3561126  1134799.50112101  872219.41731097  871276.6337552
  566610.74806608 1539564.17473228 1215068.56129604 1750435.57551414
  780818.77892039 1005278.8908274  1699864.56225465  613683.19049426
 1238072.22784607 1590149.31057521 1332467.65674073 1110842.05743917
 1279563.18720233  894290.71453523  812546.8148645  1551251.04056016
 1359009.91049348 1253121.43582776 1229760.03037545 1499246.99540704
  919454.72722129 1331679.12723297 1089870.75329734  958885.84241772
 1064647.10625584  941059.19510286  968260.24774319 1046073.30643464
 1645969.84910694 1166194.65703568 1312898.64523883 1624075.1183887
 1122680.60708451 1293518.53738823 1362243.66193653 1044593.86406878
 1045450.3409926  1446228.97176106 1314918.61125719 1054569.81836383
 1953570.35858134 1514052.36511509 1204362.0007214  1982821.65391243
  680652.28599645 1723833.1028912  1461941.72132161 1413218.72506781
 1074165.48822053 1048173.12975262 1205533.14320529  997083.01429422
 1087634.17215582  799234.75302648  972455.40342001  801856.53463554
  874552.70140583 1379123.09013505 1126134.74113496 1298148.81659145
 1428161.98519949 1355123.02342546 1553045.32879594 1870468.89055136
  941675.18754439  710776.75163677 1590857.45772608 1458290.81462446
  666201.26424408 1740887.20683925 1311991.94997945 1798637.13769294
  833944.9042026  1302260.14903338 1587128.52619347 1317721.78646457
 1668052.02029736  936367.43448359 1700849.45729715 1165066.71552696
 1555174.98797143 1203157.19294374 2182009.99760784 1050703.04898764
 1085255.43002699  687418.2413657  1225471.31016553 1236416.18898843
 1234673.47753594  930443.0490492  1501992.00715114 1273737.55032504
  950041.90286064 1222288.62260244 1196391.39810298 1126494.323256
 1430405.24832769  654124.8831567   895637.24071162  292231.65792553
 1320679.35025207  805569.98785657 1233937.60765139 1104581.67567423
  908260.05005094 1127651.17125846 1746143.30165373  550595.13591362
  887562.56379595 1318241.03673407 1104224.94782251 1268614.59782285
  773344.86318576 1279225.19247096 1409430.68554383 1438394.94609839
 1242233.55899958  976371.07650901  961172.77960729 1405311.34334234
 1231311.47373034 1416589.57175405 1039865.66760668  405050.76035601
 1831567.93596     994168.26437392 1601760.17451136 1894119.37065231
  838793.55050793  942753.23147991 2020611.40750524  892650.27370657
 1103392.89712649 1856735.83501372 1374941.34714029 1347030.9418317
  839567.56053583 1868960.81804402 1046950.66016825  837784.75799152
 1243870.52458918  945414.8784892  1003374.52464082 1290578.27919801
 1173331.32430923 1522296.87016011 1104182.96400372 1284736.34065318
  666922.2209623   673548.38060283  839842.87494546 1485091.87949437
 1758926.1001963   616983.40087472 1267547.14699049  899323.40602058
 1158839.68496783  754436.13286385 1150948.88850265 1255846.0700254
  771868.76930345 1323417.05382824  929827.54051279 1067842.89483073
 1110264.9618046  1540579.01469861  806070.2039711  1375018.45965384
 1583012.8637186  1215804.72853409 1846871.22490026 1719674.14920754
 1072371.6108295  1261854.33505624 1801461.10335271 1289458.68342156
 1371299.08117235  983760.56441569 1324124.02939088 1051323.27262374
 1007052.9443188  1401321.32709743 1571085.17286577 1229001.87119959
 1283186.61191567 1288554.56408168 1415412.18859016 1103447.66939766
 1650111.15596409 1351906.62870447  806922.81052761 1101879.75531369
 1087454.15325061 1407522.12276368 1458792.37938237 1472979.40100164
 1430788.15060835 1233623.36217147 1870533.17388317 1257158.33660787
 1600913.12822158 1356762.37639189 1319817.63503686 1236051.9108198
 1268181.38595477 1129570.75163033 1705669.67477113 1411102.91700132
 2273540.41800102 1207547.00425412  899781.3847036  1254140.99658748
 1542438.22564716  657936.46719116 1385268.91742449  878970.16758734
 1109196.43128029 1353391.17019487 1696115.98345093 1413282.72684313
 1827087.79734247 1120215.62692099 1356976.31422317 1223780.10870815
 1248092.82209771 1769843.13529577  949225.39612295 1477952.65052356
 1255282.59862256  426003.13583316  707530.77134158 1696903.01183506
 1163050.51049259 1323411.5033268  2075338.18777907 1574543.41531418
 1817164.81632782 1027880.42644512  984572.34397046 1347495.61247839
 1405652.70637065 1378100.57654898 1725502.2933008  1380099.65837807
 1577843.39344022 1624768.07507459  620250.96994802  678847.62293504
 1255463.63367927 1493178.26766321 1251118.57109733 1531225.73325
 1038742.17013484  158813.64470869  670803.74477208 1358866.25754787
 1116231.80776672  825567.42192957 1205988.87074548 1163767.67010631
 1795770.41249053 1037902.14643255 1066195.46094948 1082294.65493931
 1435062.76289096 1454466.64232999 1330792.13240183 1690582.50968939
 1069251.9487768   808110.53863472  625553.6464463   919490.05366638
 1353662.2546001  1184444.42780225 1539245.25567221 1208513.48652763
  837956.30959035 1659341.88709898 1392103.22796819 1589783.52479679
 1262963.45793841 1511554.88048253  883619.90417901 1022564.2478265
 2053151.68156221 1324454.22123298 1339389.73837403  916369.74594413
 1080360.83870116 1124354.20250161 1225725.51762075 1036709.01383128
 1234858.0860976  1055632.34784629 1517840.47571422 1327306.74681115
 1172875.46526553  663510.22702194  994880.08825416  659636.17716454
 1215833.3865498  1289298.54249165 1049066.68537325 1002058.65544564
 1479008.16442341 1133499.87344621 1343292.37992994 1240281.28609788
 1875671.59202051 1384525.66937356  900349.97821838 1491477.28606211
  291738.12188895 1146285.9434131  1367218.08956031 1289910.89226899
 1291184.80884029 1824408.57177525 1491241.2227773  1389476.45607064
 1321293.64559164 1284967.15702986  846503.99338371 1311564.60147658
 1451747.22228778 1884449.22275646 1289541.93048528  794205.10442291
 1192680.65890835 1126098.68560079 1509907.60069687 1913607.45329345
  829309.12969399  595859.28942419 1582813.3055729  1035614.81747863
 1456522.8591344   925110.32853942 1095178.12785018 1245801.01775937
 1464082.99859734 1554146.7720129   965107.42154312 1356373.22425334
 1673565.88692989 1158338.71531108 1281278.86098632  787926.23885863
 1408326.67504604 1269972.82557244  440821.15599144 1191806.26503558
  856260.95454576 1026274.59180668  915461.63191209 1813981.92931032
 1260397.95598962 1238979.10170183 1387410.70388008  804543.00233416
 1184273.71677171  895553.53733314  666753.31379016  494113.56401944
 1433820.74136242  920519.49342551 1552699.63507721 1314958.29825851
 1408137.11007595 1105205.03868602 1225374.9668051  1555967.60398297
  764356.87177392 1209380.67918476 1307446.07617411 1156888.49949617
 1477617.77388599 1570107.70957062  888604.61517341 1357192.46882878
 1122559.1071572  1611585.4588343  1055610.51785009 1553877.5957549
 1094835.14060777  925610.91139446 1443072.82447055  887534.1374447
  456214.10636656 1667261.18226071 1271904.57635987 1182679.40285231
 1649171.89032533 1423009.73879234 1484679.82766679 1033297.25906527
 1292529.61451445 1145394.95691419  978455.62438492 1249136.06087141
 1127225.37341876  892653.77412716 1511054.90056614  873016.64861266
  776802.43645933 1039353.56986007 1315157.46162109 1584289.80977678
  987849.3901897  1368365.93287136 1100557.38102402 1365918.94448344
  935645.93243935  324562.71318829  882741.4389428  1276731.80519713
 1198249.83684015 1049018.28659815  636396.7037124  1449632.62265719
 1383995.417274   1298088.72377639 1540539.70367801 1493896.99889277
 2343746.35475218 1872235.70486221 1439963.7293974   764711.28781408
  800048.08135589 1913553.84633185  788172.68793124 1569240.63551064
 1490513.58848105 1140794.46978976 1258627.64263355  711243.70457631
 1250422.00060581 1164795.5837308  1719556.05365748 1144266.71457958
 1494354.61963915 1483904.82826235 1384296.65337772 2120401.56572417
 1436436.14421377 1360879.56964684  948664.59412427 1531287.09616398
  891575.11288498 1323714.31616798  743700.71602049 1226590.09491137
  990596.20736163 1233992.23613085 1418498.89087371 1538321.56247525
  496671.07127634 1441826.36086485 1807086.36903315 1288631.96182057
 1067396.35435427 1234051.48903035 1510931.44172461 1238596.80367902
 1079587.86267727 1096446.24680233 1340780.37376665 1728349.44751938
 1182885.47597413 1396880.83572716  848540.01935768 1118181.60414617
 1450084.64438066 2095131.06695887 1432554.8369338  1097488.60717876
 1543895.07644596  589701.16866297 1632891.15370816 1462326.34718035
 1251619.86424647 1215142.06779053  887591.37509515 1012091.21646187
 1528992.42455722 1224454.45215524 1657636.49616451 1521618.14196433
 1395801.01427134 1559056.91331875 1474320.2159518  1266083.75708837
  598008.41675619  775371.04758185 1626814.6124692  1184704.01403565
 1378438.85088416 1999851.71462622  519552.57086123  939469.86749699
 1730275.64303092  787500.26420787 1266092.51639942 1334783.79526669
 1311501.65110258  790924.30378168 1701593.43687928 1031290.33368498
 1384964.63414445 1881154.94357247 1135281.19135162 1155858.13118106
  821938.51015299 1392569.03068382 1828630.6890108  1499599.51634769
 1768913.65916671 2140238.41934353 1312065.46959489 1297778.51810689
  954090.00438522 1164766.05704707 1762265.19883864 1272190.22341176
 1337046.93436166 1734432.16375048 1241997.26902784 1545171.11180707
 1410803.40672303  842583.16037094 1410431.15875628  843468.41686778
  963938.51049702 1359381.61691094 1086200.9207718  1724349.54285902
 1488902.61977152 1435137.84302696 2032773.75166519 1550293.84023723
 1117672.92731721 1228981.23802782 1024338.06123588 1152686.83818179
 1016242.46686364 1375666.28141413 1050507.27599655 1181790.30430949
 1525713.82292139 1681606.23746156 1205200.14071651 1473170.42751321
 1290698.64726292 1157017.91864223 1637827.88146071  591770.21795497
 1900211.94766033 1461556.14927682 1338718.53329421 1365231.39271031
  966650.94972922 1677602.58059847 1401419.04822992 2018052.80955535
 1183950.82427373 1450992.61747403 1177265.33458142  843749.10312845
 1497536.13019635 1529073.71346475  558964.87508147 1261583.2192759
 1753273.50402693 1500926.03391635 1059402.18036569 1709352.5707534
 1112889.9392433  1247534.54359931  853834.333056    706144.92549257
 1070761.99800867 1659159.91706308 1062693.4703681  1073545.82718401
 1036990.30062324 1052078.86312731 1568784.39882065 1309154.63561277
  858450.71935711  988956.07442349 1396520.63086133 1769711.99813365
 1283298.07540821 1515402.04421687 1256935.59508284 1320333.17632623
 1505221.09859151 1110347.82733454  771327.59692895 1189663.34806261
 1128591.58671394 1533502.4634133  2187616.08323863 1177106.83506023
 1490349.58869886 1410251.5440795  1461916.30494554 1548793.63000759
 2006242.92462349 1312408.85291948 1581878.60451192  631870.52706524
 2015104.50190244 1056903.78936517 1547405.20803519  875505.84365161
 1826751.87241242 1174567.82091767  844395.35409811 1682709.20725988
 1275129.63095662 1526887.72765502 1000089.93827973 1529941.81041016
  520249.23833003 1509899.51986588 1245221.05841925 1415065.50463792
 1664533.90291694 1299536.2446968  1475156.35270561 1587824.19159122
 1087189.99440689 1242310.46082559  932496.32274209 1457832.98702953
 1327013.30958709 1365624.93545962 1065213.12237315 1097280.88986157]

Bước 7: Đánh giá chất lượng mô hình máy học

Trước tiên ta xem sự tương quan hệ số coefficients interpret:

# print the intercept
print(lm.intercept_)
coeff_df = pd.DataFrame(lm.coef_, X.columns, columns=['Coefficient'])
print(coeff_df)

Thực thi lệnh trên ta có kết quả:

-2640441.3997814017
                                Coefficient
Avg. Area Income                  21.566696
Avg. Area House Age           165453.042478
Avg. Area Number of Rooms     120499.839093
Avg. Area Number of Bedrooms    1999.785336
Area Population                   15.340604

Coeffient cho ta thấy:

  • Giả sử các thuộc tính khác không đổi, nếu Area Income tăng D đơn vị thì giá của căn nhà sẽ được bổ sung thêm D*21.5666696
  • Tương tự cho các thuộc tính khác. Như vậy dựa vào Cofficient ta biết được mức độ ảnh hưởng của các biến độc lập lên biến phụ thuộc. Ví dụ trong trường hợp này thì rõ ràng là tuổi thọ của căn nhà có tầm ảnh hưởng về giá của căn nhà lớn nhất, và dân số đóng vai trò thấp nhất về sự ảnh hưởng giá.

Ta có một số cách để đánh giá mô hình dự báo chạy bằng Hồi quy như sau:

Mean Absolute Error (MAE): MAE là một phương pháp đo lường sự khác biệt (độ chênh lệch giá trị) giữa hai biến liên tục. Giả sử rằng X và Y là hai biến liên tục thể hiện kết quả dự đoán của mô hình và kết quả thực tế, công thức:

Mean Squared Error (MSE): là giá trị trung bình của bình phương sai số (Hàm mất mát), là sự khác biệt giữa các giá trị được mô hình dự đoán và gía trị thực. MSE  cũng được gọi là một hàm rủi ro, tương ứng với giá trị kỳ vọng của sự mất mát sai số bình phương hoặc mất mát bậc hai chỉ số này phổ biến hơn chỉ số MAE bên trên,  công thức:

Root Mean Squared Error (RMSE): là căn bậc hai của giá trị trung bình của các sai số bình phương (MSE). Thông thường, ta thường dùng chỉ số này để xác định giá trị chênh lệch trung bình giữa giá dự đoán và giá trị test ban đầu, công thức:

print('MAE:', metrics.mean_absolute_error(y_test, predictions))
print('MSE:', metrics.mean_squared_error(y_test, predictions))
print('RMSE:', np.sqrt(metrics.mean_squared_error(y_test, predictions)))

Thực thi lệnh trên ta có kết quả:

MAE: 81305.23300085647
MSE: 10100187858.864885
RMSE: 100499.69083964829

Tùy vào lựa chọn tỉ lệ train mà các giá trị đo lường này có thể khác nhau.

Bước 8: Kết xuất mô hình máy học ra ổ cứng (file zip) để tái sử dụng

Khi mô hình được đánh giá là chạy tốt, ta nên lưu lại mô hình để các lần sau sử dụng chỉ cần nạp lại mô hình thay vì phải train lại (công đoạn train thường là tiêu tốn nhiều thời gian và nguồn lực).

Lưu mô hình, mô hình ở trên được đặt tên biến là lm, ta cần lưu mô hình lm này xuống ổ cứng để sử dụng cho lần sau, sử dụng thư viện pickle:

modelname = "housingmodel.zip"

pickle.dump(lm, open(modelname, 'wb'))

Thư thi lệnh trên ta sẽ có mô hình được lưu xuống ổ cứng với tên “housingmodel.zip”:

Source code đầy đủ của “HousingPriceSimpleModel.py”:

# 01.Library for dataset processing
import pandas as pd
import numpy as np
# 02. Library for train model
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
# 03. Library for evaluation model
from sklearn import metrics
# 04. Library for saving model
import pickle

#use pandas to read CSV dataset
df = pd.read_csv('dataset/USA_Housing.csv')
#call functions about get dataset information:
print(df.head())
print(df.info())
print(df.describe())
print(df.columns)

#set X matrix
#df.columns[:5] meaning:
#['Avg. Area Income', 'Avg. Area House Age', 'Avg. Area Number of Rooms',
# 'Avg. Area Number of Bedrooms', 'Area Population']
X = df[df.columns[:5]]
y = df['Price']
# Printing for observation:
print(X)
print(y)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=101)

lm = LinearRegression()

lm.fit(X_train, y_train)

print("Input 1:")
print([X_test.iloc[0]])

pre1 = lm.predict([X_test.iloc[0]])
print("Housing Price prediction 1 =", pre1)

print("Input 2:")
input2=[66774.995817, 5.717143, 7.795215, 4.320000, 36788.980327]
print(input2)
pre2 = lm.predict([input2])
print("Housing Price prediction 2 =", pre2)

print("Input 3:")
input3=[21.566696, 165453.0425, 120499.8391, 1999.785336, 15.340604]
print(input3)
pre3 = lm.predict([input3])
print("Housing Price prediction 3 =", pre3)

predictions = lm.predict(X_test)
print("Full Housing Price Predictions:")
print(predictions)

# print the intercept
print(lm.intercept_)
coeff_df = pd.DataFrame(lm.coef_, X.columns, columns=['Coefficient'])
print(coeff_df)

print('MAE:', metrics.mean_absolute_error(y_test, predictions))
print('MSE:', metrics.mean_squared_error(y_test, predictions))
print('RMSE:', np.sqrt(metrics.mean_squared_error(y_test, predictions)))

modelname = "housingmodel.zip"

pickle.dump(lm, open(modelname, 'wb'))

Bước 9: Sử dụng/Tái sử dụng mô hình máy học, tạo thêm 1 file mã lệnh Python “UseHousingPriceSimpleModel.py”

Bổ sung mã lệnh cho “UseHousingPriceSimpleModel.py”

import pandas as pd
import pickle

from pandas import Index

modelname="housingmodel.zip"

# Load Trained Machine Learning Model
trainedmodel=pickle.load(open(modelname, 'rb'))

features=Index(['Avg. Area Income', 'Avg. Area House Age', 'Avg. Area Number of Rooms',
       'Avg. Area Number of Bedrooms', 'Area Population'],
      dtype='object')
# Check coeff again if you want (no need)
coeff_df = pd.DataFrame(trainedmodel.coef_,features,columns=['Coefficient'])
print(coeff_df)

print("Input 1:")
input1=[66774.995817,5.717143,7.795215,4.320000,36788.980327]
print(input1)
prediction1=trainedmodel.predict([input1])
print("Housing Price Prediction 1=",prediction1)

print("-"*50)
input2=[[66774.995817,5.717143,7.795215,4.320000,36788.980327],
        [80527.47208,8.093512681,5.0427468,4.1,47224.35984]]
print(input2)
prediction2=trainedmodel.predict(input2)
print("Housing Price Prediction 2=",prediction2)

Mã lệnh ở trên sẽ tải mô hình máy học đã được lưu trước đó “housingmodel.zip” và tiến hành tái sử dụng để predict giá nhà, chạy mã lệnh trên ta có kết quả:

                                Coefficient
Avg. Area Income                  21.566696
Avg. Area House Age           165453.042478
Avg. Area Number of Rooms     120499.839093
Avg. Area Number of Bedrooms    1999.785336
Area Population                   15.340604
Input 1:
[66774.995817, 5.717143, 7.795215, 4.32, 36788.980327]
Housing Price Prediction 1= [1257919.71744578]
--------------------------------------------------
[[66774.995817, 5.717143, 7.795215, 4.32, 36788.980327], [80527.47208, 8.093512681, 5.0427468, 4.1, 47224.35984]]
Housing Price Prediction 2= [1257919.71744578 1775665.87664153]

Như vậy tới đây Tui đã hướng dẫn xong các bạn cách thứ sử dụng thư viện Hồi quy tuyến tính để train một mô hình máy học đơn giản để dự báo giá nhà, các bạn biết cách/ôn tập pandas để đọc và tiền xử lý dữ liệu, biết cách dùng train_test_split, LinearRegression để train mô hình. Biết cách dùng metrics để đánh giá chất lượng mô hình cũng như dùng pickle để lưu mô hình và phục hồi lại mô hình để tái sử dụng trong tương lai.

Source code đầy đủ của bài này bạn tải ở đây:

https://www.mediafire.com/file/z0yp8561ztuiaz5/HousingPricePrediction.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn thiết kế màn hình tương tác người dùng để train mô hình cũng như sử dụng mô hình máy học trên giao diện. Các bạn chú ý theo dõi

Chúc các bạn thành công

Bài 42: Tổng quan về tích hợp Mô hình Máy học vào hệ thống Kinh doanh – Quản lý

Trong chuỗi các bài học này Tui sẽ hướng dẫn các bạn cách thức lập trình các mô hình máy học và tích hợp mô hình máy học vào hệ thống quản lý, kinh doanh. Trước tiên là các bạn cần hiểu sơ qua về mô hình tổng quan của máy học, hiểu được các bước cần thiết khi xây dựng mô hình máy học, biết cách tích hợp nó vào các hệ thống đã có sẵn hoặc chưa có sẵn tùy thuộc vào nhu cầu sử dụng cũng như tùy thuộc vào cách thức thiết kế cấu trúc cơ sở dữ liệu và loại dữ liệu.

Máy học có rất nhiều ứng dụng cho từng trường hợp cụ thể, và nó lệ thuộc vào kinh nghiệm cũng như tư tưởng của kỹ sư thiết kế mô hình máy học, mỗi người có thể đưa ra các quy trình khác nhau, tuy nhiên nó cũng có các điểm chung cơ bản nhất khi xây dựng mô hình máy học.

Ta có thể ứng dụng máy học vào các bài toán như: Phân loại, Phân cụm, hồi quy. Chúng ta có thể áp dụng mô hình máy học vào các lĩnh vực như: Sản xuất, Chăm sóc sức khỏe và khoa học đời sống, Các dịch vụ tài chính, Các hệ thống bán lẻ, Truyền thông và giải trí, Cùng các lĩnh vực khác…

01. Đối với Lĩnh vực Sản xuất:

Máy học có thể hỗ trợ bảo trì dự đoán, kiểm soát chất lượng và nghiên cứu đổi mới trong lĩnh vực sản xuất. Công nghệ máy học cũng giúp các công ty cải thiện giải pháp hậu cần, bao gồm quản lý tài sản, chuỗi cung ứng và kho hàng.

(source : Internet)

02. Đối với lĩnh vực Chăm sóc sức khỏe và khoa học đời sống:

Sự phát triển như vũ bão của cảm biến và thiết bị có thể đeo được đã tạo ra một lượng lớn dữ liệu về sức khỏe. Các chương trình máy học có thể phân tích thông tin này và hỗ trợ bác sĩ chẩn đoán và điều trị trong thời gian thực.

(source : Internet)

03. Đối với lĩnh vực Dịch vụ tài chính:

Các dự án máy học về tài chính giúp cải thiện khả năng phân tích rủi ro và quy định. Công nghệ máy học có thể giúp các nhà đầu tư xác định cơ hội mới bằng cách phân tích hoạt động của thị trường chứng khoán, đánh giá các quỹ phòng hộ hoặc hiệu chỉnh danh mục tài chính

(source : Internet)

04. Đối với lĩnh vực Bán lẻ:

Nhà bán lẻ có thể sử dụng máy học để cải thiện dịch vụ khách hàng, quản lý hàng tồn kho, bán hàng gia tăng và tiếp thị đa kênh, dự báo doanh thu, dự báo nhu cầu tiêu dùng, dự báo xu hướng…

(source : Internet)

05. Đối với lĩnh vực Truyền thông và giải trí:

Các công ty giải trí tìm đến máy học để hiểu rõ hơn đối tượng mục tiêu của họ đồng thời cung cấp nội dung chân thực, được cá nhân hóa và theo nhu cầu của khách hàng. Thuật toán máy học được triển khai để giúp thiết kế trailer và các dạng quảng cáo khác, từ đó đề xuất nội dung được cá nhân hóa cho người tiêu dùng và thậm chí là hợp lý hóa quy trình sản xuất.

(source : Internet)

Ngoài ra đôi khi chúng ta cũng hay phân vân, bối rối giữa khái niệm về Trí tuệ nhân tạo và Máy học, thì dưới đây thì hãng AWS có bảng phân biệt 2 khái niệm này, Tui tổng hợp lại để các bạn hiểu thêm:

Trí tuệ nhân tạoMáy học
Khái niệmAI là thuật ngữ rộng cho các ứng dụng dựa trên máy móc bắt chước trí thông minh của con người. Không phải tất cả các giải pháp AI đều là ML.ML là một phương pháp trí tuệ nhân tạo. Tất cả các giải pháp ML đều là các giải pháp AI.
Trường hợp sử dụng phù hợp nhấtAI là lựa chọn tốt nhất để hoàn thành một tác vụ phức tạp của con người một cách hiệu quả.ML là lựa chọn tốt nhất để xác định các mẫu hình trong các tập dữ liệu lớn để giải quyết các vấn đề cụ thể.
MethodsAI có thể sử dụng các phương pháp khác nhau, như dựa trên quy tắc, mạng nơ-ron, thị giác máy tính, v.v. Đối với ML, người ta tự chọn và trích xuất các tính năng từ dữ liệu thô và gán trọng số để đào tạo mô hình.
ImplementsViệc triển khai AI phụ thuộc vào tác vụ. AI thường được xây dựng sẵn và truy cập thông qua các API.Bạn đào tạo các mô hình ML mới hoặc hiện có cho trường hợp sử dụng cụ thể của bạn. Cũng có sẵn các API ML được xây dựng sẵn.
(source : AWS)

Và dưới đây là quan điểm cá nhân của Tui liên quan tới máy học hay trí tuệ nhân tạo, và nó có thể làm không ưng cái bụng của một số người:

Đừng bao giờ thần thánh hóa Trí tuệ nhân tạo hay Máy học. Chúng chỉ đóng góp một vai trò nhất định trong hệ thống, giúp người quản trị đưa ra các quyết định nhanh chóng và phù hợp hơn.

Muốn ứng dụng máy học thì trước tiên chúng ta phải có hệ thống quản trị tốt trước đã, và các dữ liệu nên được cấu trúc phù hợp.

Nỗi sợ trí tuệ nhân tạo, máy học hay Robot thông minh sẽ thay thế con người? Cướp công việc của con người?

Không bao giờ có điều đó xảy ra, mỗi một công nghệ mới xuất hiện thì nó sẽ tạo ra rất nhiều nhóm các ngành nghề mới, công việc mới cho Con người làm việc. Nó chỉ thay thế những STUPID JOB

“Lùa Gà” là một từ lóng mà chúng ta thường nghe, một số trung tâm, chuyên gia đào tạo… cố tình tạo nên nỗi sợ hoang đường để học viên đăng ký học, hay đăng ký mua sản phẩm liên quan.

Chúng ta học để hiểu biết, để ứng dụng trong thực tiễn vì kiến thức là vô cùng vô tận. Học để thích ứng với sự phát triển như vũ bão của công nghệ mới chứ không phải vì lo sợ nó CƯỚP công việc của mình.

Vì mỗi người mỗi quan điểm khác nhau, nhìn bài toán khách hàng gặp phải dưới các lăng kính khác nhau nên mỗi người sẽ có những quy trình xử lý khác nhau, nhưng suy cho cùng thì cũng cùng một mục đích là đem lại dịch vụ tốt nhất cho khách hàng. Dưới đây Tui đưa ra một quy trình chung cơ bản về thiết kế mô hình máy học và tích hợp vào hệ thống nói chung, các bạn có thể tham khảo (có thể hoàn toàn KHÔNG ĐỒNG Ý hoặc ĐỒNG Ý một PHẦN). Dĩ nhiên các chuỗi bài hướng dẫn xây dựng mô hình máy học và tích hợp vào hệ thống mà Tui hướng dẫn trên Blog này sẽ nhìn chung thực hiện theo các quy trình này. Tùy thuộc vào tình hình thực tế của bài toán mà các bạn đang giải quyết để có thể cải biên nó phù hợp hơn.

Và cũng cần lưu ý: Không có mô hình máy học nào sai, chỉ có mô hình máy học phù hợp hay chưa phù hợp. Mô hình máy học có thể chạy tốt với tập dữ liệu X nhưng chưa chắc chạy tốt với tập dữ liệu Y. Đó là lý do vì sao chúng ta cần cải tiến mô hình máy học thường xuyên khi nhu cầu hay dữ liệu bị thay đổi đáng kể.

Ta xem lại mô hình tổng quan:

Lưu đồ trên là việc tích hợp mô hình máy học vào một hệ thống đang vận hành. Dĩ nhiên có những trường hợp ta chưa có hệ thống (dữ liệu) nhưng ta có ý tưởng về mô hình máy học từ đó ta thiết kế hệ thống đáp ứng ý tưởng mô hình máy học . Và cũng có những trường hợp hệ thống có sẵn rồi, dữ liệu có sẵn rồi, bây giờ ta cần tích hợp mô hình máy học vào, hầu hết bài học Tui sẽ hướng dẫn trường hợp số 2 là hệ thống có sẵn.

  • Ở lưu đồ trên ta thấy, đa phần nó có khoảng 3 phiên chính trong xây dựng và tích hợp mô hình máy học: Phiên PREPARING DATA, Phiên BUILD MODEL và phiên USE MODEL.
  • PREPARING DATA: Là Bước chuẩn bị dữ liệu, tùy thuộc vào các bài toán khác nhau, hay dữ liệu khác nhau mà ta có các bước chuẩn bị dữ liệu chi tiết khác nhau, nhìn chung thì ta cần có các thao tác như “làm sạch và chuẩn hóa dữ liệu”, “Mô hình hóa hướng đối tượng cho dữ liệu” để phục vụ cho việc lập trình được dễ dàng hơn. Ta cần phải chuẩn bị dữ liệu thật tốt để Build Mô hình, vì Cơ sở dữ liệu lưu trữ không phải lúc nào cũng lưu trữ các dữ liệu tốt, tối ưu mà nó có thể lưu những dữ liệu không tốt, các dữ liệu không có ích trong quá trình Build mô hình. Mà chúng ta cần biết nguyên tắc “Garbage In – Garbage Out” nghĩ là dữ liệu không tốt thì cho mô hình không tốt.
    1. Khi khách hàng ở bước (0) có các hoạt động trên hệ thống (1), chẳng hạn như tương tác sản phẩm, commments, review, đặt đơn hàng… thì các dữ liệu này sẽ được lưu vào Database (2). Database này chúng ta cần tiền xử lý cho tốt, nó sẽ nằm trong phiên PREPARING DATA.
    2. Mỗi dữ liệu khác nhau, mỗi bài toán khác nhau mà bước (3) làm sạch và chuẩn hóa dữ liệu sẽ khác nhau, có thể thêm nhiều các bước con khác nhau, sử dụng các kỹ thuật khác nhau.
    3. Mô hình hóa hướng đối tượng (4) có thể được áp dụng để giúp cho việc xử lý dữ liệu, mô hình được tốt hơn.
  • BUILD MODEL: Là các bước từ số (5) tới số (9). Phiên này sẽ build mô hình máy học, các bước bao gồm lọc và tải dữ liệu, lựa chọn giải thuật và các tham số cho mô hình, train mô hình, đánh giá và cải thiện mô hình, cuối cùng là xuất mô hình ra ổ cứng để tái sử dụng, tích hợp vào hệ thống.
    1. Bước (5) “Filter and load data” có thể được xem là bước lọc và nạp dữ liệu để phục vụ cho train mô hình, ta cần chọn các thuộc tính/biến phù hợp cũng như tỉ lệ train/test khác nhau, nó ảnh hưởng tới chất lượng mô hình
    2. Bước (6) “Select Algorithms” Bước này ta lựa chọn các giải thuật máy học phù hợp với bài toán, cũng với việc lựa chọn các tham số đầu vào để train mô hình. Ví dụ như bài toán Classfification thì nên dùng giải thuật nào, bài toán Clustering thì nên dùng giải thuật nào, các bài toán liên quan Time Series Data thì nên dùng giải thuật nào….. Cái này ứng với bài toán cụ thể ta sẽ dùng.
    3. Bước (7) là bắt đầu train mô hình máy học, từ tập dữ liệu, giải thuật máy học, các tham số được lựa chọn phù hợp thì chương trình bày đầu train.
    4. Bước (8) sau khi train mô hình xong thì chúng ta cần đánh giá mô hình có chất lượng hay không, nếu mô hình chất lượng (ta cho rằng) thì qua Bước (9) để xuất mô hình xuống ổ cứng để tái sử dụng, để tích hợp vào hệ thống mà khách hàng yêu cầu. Nếu mô hình không chất lượng thì ta có nhiều giải pháp, ở trên thì Tui đưa ra 2 giải pháp: Hoặc là chúng ta quay lại bước (5) hoặc là chúng ta quay lại phiên PREPARING DATA. Vì cả 2 nơi này đều là nguyên dẫn có thể dẫn tới mô hình không đạt chất lượng mong muốn.
    5. Bước (9) là bước xuất mô hình ra ổ cứng (lưu), vì ta không thể khi chạy dịch vụ ví dụ như Khách hàng đang sử dụng hệ thống sàn thương mại điện tử, ta muốn gợi ý các sản phẩm mà khách hàng có khả năng mua cao thì ta lại chạy lại mô hình được (vì nó rất tốn thời gian, training mô hình rất lâu, khách hàng không thể ngồi chờ). do đó Bước (9) là lưu mô hình rồi thì các lần sau sử dụng ta chỉ cần tải lại mô hình này rồi tích hợp vào hệ thống thôi.
  • USE MODEL: Là các bước (10), (11). Các bước này nạp và tích hợp mô hình máy học vào hệ thống và sử dụng mô hình máy học.
    1. Bước (10) là nạp mô hình đã lưu từ bước (9), ví dụ khách hàng đăng nhập vào hệ thống sàn thương mại điện tử, ta muốn gợi ý các sản phẩm mà khách hàng có khả năng mua cao nhất thì ta sẽ đọc mô hình máy học ở Bước (9) để sử dụng, tích hợp nó vào hệ thống ở bước (11)
    2. Bước (11) Sau khi mô hình máy học được tích hợp vào hệ thống, chương trình sẽ triệu gọi mô hình máy học, gọi các hàm predict để đưa ra các kết quả phù hợp cho khách hàng. Và hiển nhiên Khách hàng có thể tiếp tục tương tác trên hệ thống ở bước (0) như vậy chúng ta có một vòng tròn khép kín, mỗi lần khách hàng tương tác thì hệ thống lại lưu lại cơ sở dữ liệu, và theo thời gian thì chúng ta biết rằng có thể sở thích khách hàng sẽ thay đổi theo thời gian (khả năng cao, ví dụ tháng 01 bạn thích ăn Dưa leo, nhưng qua tháng 02 bạn lại thích Dưa chuột) như vậy có thể dẫn tới mô hình máy học không còn chất lượng nữa, ta cần có cách đánh giá lại mô hình, có thể build lại. Cứ thế tiếp tục cải tiến mô hình để có được một dịch vụ tốt nhất cho khách hàng.

Trên đây là tổng quan về máy học, trí tuệ nhân tạo, cách áp dụng mô hình máy học vào các lĩnh vực khác nhau như: Sản xuất, Chăm sóc sức khỏe và khoa học đời sống, Các dịch vụ tài chính, Các hệ thống bán lẻ, Truyền thông và giải trí, Cùng các lĩnh vực khác…

Đặc biệt Tui đưa ra mô hình tổng quan chung cơ bản về xây dựng và tích hợp mô hình máy học vào hệ thống quản lý, giải thích ý nghĩa của từng bước. Và nó phụ thuộc vào bài toán của khách hàng, phụ thuộc vào dữ liệu, phụ thuộc vào lăng kính của kỹ sư máy học mà mỗi người có thể cải biên flow chart khác nhau, tuy nhiên về cơ bản thì các chức năng, các bước cốt lõi là như nhau.

Bài học sau Tui sẽ hướng dẫn cụ thể bài Dự báo giá nhà dùng hồi quy tuyến tính. Xây dựng mô hình máy học và chạy thử nghiệm trên giao diện Python PyQt6. Các bạn chú ý theo dõi

Chúc các bạn thành công

Bài 41: Lập trình Đa Tiến Trình trong Python-PyQt6- Part 3

Bài đa tiến trình này Tui sẽ trình bày kỹ thuật xử lý Đa tiến trình phức tạp hơn, và nó được ứng dụng thực tế cho các bài tải đồng thời nhiều trang web trên mạng về máy tính. Sẽ có nhiều Background Thread được chạy đồng thời, và chúng ta sẽ quản lý các Background Thread này thông qua id. Giao diện phần mềm như dưới đây:

  • Chương trình cung cấp giao diện cho người dùng chọn Protocol trong QCombobox (https và https) cùng với domain website muốn tải.
  • Khi người dùng nhấn nút “Add”, các URL sẽ được đưa vào QTableWidget, widget này có 3 cột: Cột # là số thứ tự cũng coi như là Id của từng Background Thread, Cột URL là link Website mà người dùng muốn tải, và cột Progress cho biết tiến độ đang tải dữ liệu được bao nhiêu %.
  • Lưu ý rằng mỗi một link website tải sẽ là một Background Thread, các thread này sẽ chạy đồng thời, các dữ liệu và tiến độ sẽ được cập nhật liên tục lên Main Thread (UI) tương ứng với từng background thread.
  • Khi tải xong thì biểu tượng Icon màu xanh ở cột # sẽ tự động xuất hiện, ô URL sẽ đổi qua nền vàng, và Progress sẽ là 100%
  • Nút “Clear URLs” dùng để xóa các URL mà người dùng đã nhập trên giao diện
  • Nút “Start Downloading” để bắt đầu thực hiện các Background Thread chạy đồng thời.

Bước 1: Tạo dự án “LearnMultithreadingPart3” có cấu trúc như dưới đây:

  • “ItemDownload.py” là file chứa lớp đối tượng ItemDownload để chương trình mỗi lần thực thi sẽ lấy Id, domain, data line và percent thực hiện trong tiểu trình rồi gửi về cho Main Thread cập nhật lên giao diện.
  • “WorkerSignals.py” là lớp kế thừa từ QObject, khai báo các Signal để thực hiện call back gửi dữ liệu trở về giao diện chính để cập nhật giao diện thời gian thực
  • “Worker.py” là lớp kế thừa từ QRunnable, nó dùng để tạo các đối tượng chạy đa tiến trình, trong quá trình xử lý nó sẽ thông qua WorkerSignals để gửi tín hiệu về cho màn hình chính cập nhật giao diện.
  • “ProgressDelegate.py” là file chứa lớp đối tượng ProgressDelegate kế thừa từ QStyledItemDelegate nhằm hỗ trợ việc vẽ Progress Bar tiến độ downloading cho mỗi URL trong QTableWidget.
  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.
  • Thư mục “download” dùng để lưu nội dung HTML tải được của các URL trên mạng
  • Thư mục “images” chứa các icon để trang trí cho giao diện được đẹp

Dưới đây là Flow Chart xử lý đa tiến trình trong các mã lệnh ở các Lớp trong Project:

  • Flow Chart ở trên Tui vẽ ra 5 bước tổng quan, Tui giải thích sơ lược để các bạn nắm:
    1. Step 1: Trong lớp WorkerSignals ta khai báo các Signal để làm nhiệm vụ bắn các tín hiệu từ Background Thread (chạy ngầm) qua Main Thread (UI)
    2. Step 2: Trong lớp Worker ta sẽ sử dụng đối tượng WorkerSignals ở bước 1, đối tượng này có 2 signal: biến runningSignal để bắn tín hiệu cập nhật dữ liệu cũng như tiến độ về cho MainThread để cập nhật giao diện thời gian thực, biến finishSignal sẽ bắn tín hiệu về MainThread để báo rằng tiến trình đã hoàn tất.
    3. Step 3: Trong Main Thread khai báo các đối tượng Worker, QThreadPool để thực thi đa tiến trình, nó cần báo cho Background Thread biết là khi bắn tín hiệu (khi gọi hàm emit) về cho Main Thread thì slot nào sẽ lắng nghe
    4. Step 4: Trong quá trình thực thi Background Thread, chương trình sẽ bắn tín hiệu về cho Main Thread thông qua hàm emit
    5. Step 5: Bất cứ khi nào Background Thread gọi hàm emit thì ngay lập tức slot được khai báo trong Main Thread sẽ nhận được tín hiệu từ Back ground Thread gửi về. Tùy thuộc vào WorkerSignals ta khai báo các đối số như thế nào thì ta truyền dữ liệu tương ứng. Ta chỉ có thể cập nhật giao diện ở Main Thread, không thể cập nhật giao diện ở Background Thread, đó là lý do vì sao ta phải bắn tín hiệu về cho Main Thread.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp. Và bạn đặt tên các Widget như hình.

  • QComboBox (comboBoxProtocol) dùng để lưu mặc định 2 Protocols là httpshttp
  • QLineEdit (lineEditURL) là ô nhập domain muốn tải
  • QPushButton (pushButtonAddURL): Người dùng nhấn vào nút lệnh này thì chương trình sẽ đưa dữ liệu URL vào giao diện QTableWidget
  • QPushButton (pushButtonClearURL): Xóa các dữ liệu trong QTableWidget
  • QPushButton (pushButtonStartDownloading): Khi nhấn vào nút lệnh này, chương trình sẽ tạo nhiều Background Thread để tải và cập nhật tiến độ giao diện thời gian thực
  • QTableWidget (tableWidgetURL): Lưu các URL mà người dùng muốn tải, đồng thời cập nhật tiến độ tỉ lệ % tải dữ liệu từ Internet cho mỗi Item.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(524, 394)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/ic_logo.jpg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        MainWindow.setWindowIcon(icon)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButtonStartDownloading = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonStartDownloading.setGeometry(QtCore.QRect(160, 310, 151, 41))
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/ic_download.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonStartDownloading.setIcon(icon1)
        self.pushButtonStartDownloading.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonStartDownloading.setObjectName("pushButtonStartDownloading")
        self.tableWidgetURL = QtWidgets.QTableWidget(parent=self.centralwidget)
        self.tableWidgetURL.setGeometry(QtCore.QRect(20, 50, 481, 251))
        self.tableWidgetURL.setObjectName("tableWidgetURL")
        self.tableWidgetURL.setColumnCount(3)
        self.tableWidgetURL.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetURL.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetURL.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetURL.setHorizontalHeaderItem(2, item)
        self.pushButtonClearURL = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClearURL.setGeometry(QtCore.QRect(20, 310, 121, 41))
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonClearURL.setIcon(icon2)
        self.pushButtonClearURL.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonClearURL.setObjectName("pushButtonClearURL")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(30, 10, 55, 16))
        self.label.setObjectName("label")
        self.lineEditURL = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditURL.setGeometry(QtCore.QRect(130, 10, 261, 22))
        self.lineEditURL.setObjectName("lineEditURL")
        self.pushButtonAddURL = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonAddURL.setGeometry(QtCore.QRect(400, 7, 93, 31))
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("images/ic_add.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonAddURL.setIcon(icon3)
        self.pushButtonAddURL.setIconSize(QtCore.QSize(24, 24))
        self.pushButtonAddURL.setObjectName("pushButtonAddURL")
        self.comboBoxProtocol = QtWidgets.QComboBox(parent=self.centralwidget)
        self.comboBoxProtocol.setGeometry(QtCore.QRect(60, 10, 61, 22))
        self.comboBoxProtocol.setObjectName("comboBoxProtocol")
        self.comboBoxProtocol.addItem("")
        self.comboBoxProtocol.addItem("")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 524, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - MultiThreading"))
        self.pushButtonStartDownloading.setText(_translate("MainWindow", "Start Downloading"))
        item = self.tableWidgetURL.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "#"))
        item = self.tableWidgetURL.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "URL"))
        item = self.tableWidgetURL.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Progress"))
        self.pushButtonClearURL.setText(_translate("MainWindow", "Clear URLs"))
        self.label.setText(_translate("MainWindow", "URL:"))
        self.pushButtonAddURL.setText(_translate("MainWindow", "Add"))
        self.comboBoxProtocol.setItemText(0, _translate("MainWindow", "https"))
        self.comboBoxProtocol.setItemText(1, _translate("MainWindow", "http"))

Bước 4: Viết mã lệnh cho “ItemDownload.py

class ItemDownload:
    def __init__(self,id,domain,data,percent):
        self.id=id
        self.domain=domain
        self.data=data
        self.percent = percent

Lớp ItemDownload được định nghĩa với 4 thuộc tính: id, domain, data, percent. Với constructor nhận vào 4 biến như trên.

  • Dựa vào thuộc tính id để biết được Background Thread nào đang gửi tín hiểu về Main Thread
  • domain là thuộc tính cho biết tên domain muốn tải, nó cũng dùng để lưu tên file tương ứng xuống ở cứng
  • data là từng dòng dữ liệu tại thời điểm tải
  • percent là thuộc tính cho biết gói tin này đang ở tiến độ bao nhiêu %

Bước 5: Viết mã lệnh cho “WorkerSignals.py

from PyQt6.QtCore import QObject, pyqtSignal

from ItemDownload import ItemDownload

class WorkerSignals(QObject):
    runningSignal = pyqtSignal(ItemDownload)
    finishSignal = pyqtSignal(int)

Lớp WorkerSignals được kế thừa từ QObject, lớp này Tui khai báo 2 biến đối tượng có kiểu pyqtSignal:

  • runningSignal là signal để truyền tín hiệu trong quá trình thực hiện cập nhật giao diện thời gian thực. Signal này truyền 1 đối số là đối tượng ItemDownload nó sẽ được bắn về Main Thread để cập nhật giao diện thời gian thực trên QTableWidget, vì ItemDownload Tui đã thiết kế có thuộc tính percent rồi nên không cần dùng đối số 2 ở đây như bài trước (dĩ nhiên bạn có thể đổi)
  • finishSignal là signal thông báo đã hoàn tất thực hiện chạy đa tiến trình

Bước 6: Viết mã lệnh cho “Worker.py

import os
import time
from urllib.parse import urlparse

import requests
from PyQt6.QtCore import pyqtSlot, QRunnable

from ItemDownload import ItemDownload
from WorkerSignals import WorkerSignals


class Worker(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param id: The id for this worker
    :param url: The url to retrieve
    """

    def __init__(self, id, url):
        super().__init__()
        self.id = id
        self.url = url
        self.domain = urlparse(self.url).netloc
        path="download/"+self.domain+".html"
        if os.path.isfile(path):
            os.remove(path)
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        r = requests.get(self.url)
        lists=r.text.splitlines()
        for i in range(len(lists)):
            data=lists[i]
            percent = int(100 * (i + 1) / len(lists))
            itemDownload=ItemDownload(self.id,self.domain,data,percent)
            self.signals.runningSignal.emit(itemDownload)
            time.sleep(0.01)
        self.signals.finishSignal.emit(self.id)

Lớp Worker được kế thừa từ lớp QRunnable, lớp này Tui định nghĩa 2 hàm:

  • Constructor nhận vào đối số id (là id để xác định Background Thread nào sẽ nắm giữ nó) và url (là link muốn download). Nó cũng khởi tạo đối tượng WorkerSignals để sử dụng cho việc truyền tin về màn hình chính (Main Thread), Chúng ta chỉ có thể cập nhật giao diện ở Main Thread
  • override hàm run, hàm này là chạy long time, nó xử lý cho 1 Background Thread để tải 1 URL tương ứng với id được truyền vào. Sau đó dùng biến singal runningSignal để truyền tín hiệu cùng với dữ liệu về cho Main Thread thông qua hàm emit. Mỗi lần lặp Tui cho nó nghỉ 0.01 giây.
  • Cuối cùng khi kết thúc vòng lặp Tui gọi finishSignal để truyền tín hiệu là kết thúc tiến trình

Như vậy rõ ràng trong Main Thread Tui sẽ có 1 vòng lặp để tạo ra nhiều đối tượng Worker này với Id và Url khác nhau.

Bước 7: Viết mã lệnh cho “ProgressDelegate.py”. Lớp này là lớp để làm Custom ProgressBar xuất hiện trong từng Ô của QTableWidget.

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QStyledItemDelegate, QStyleOptionProgressBar, QApplication, QStyle


class ProgressDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        try:
            progress = index.data(Qt.ItemDataRole.UserRole)

            opt = QStyleOptionProgressBar()
            opt.rect = option.rect
            opt.minimum = 0
            opt.maximum = 100
            opt.progress = progress
            opt.text = f"{progress}%"
            opt.textVisible = True
            opt.state |= QStyle.StateFlag.State_Horizontal
            style = (
                option.widget.style() if option.widget is not None else QApplication.style()
            )
            style.drawControl(
                QStyle.ControlElement.CE_ProgressBar, opt, painter, option.widget
            )
        except:
            pass

Bước 8: Viết mã lệnh cho “MainWindowEx.py”

Khởi tạo Constructor như bên dưới, và override hàm setupUi để thiết lập giao diện cũng như gọi signal cho các Button:

from urllib.parse import urlparse

from PyQt6.QtCore import Qt, QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem
from pyqtgraph import QtGui

from ItemDownload import ItemDownload
from MainWindow import Ui_MainWindow
from ProgressDelegate import ProgressDelegate
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.urls=[]
        self.delegate = ProgressDelegate(self.tableWidgetURL)
        self.tableWidgetURL.setItemDelegateForColumn(2, self.delegate)
        self.pushButtonAddURL.clicked.connect(self.processAddURL)
        self.pushButtonClearURL.clicked.connect(self.clearData)
        self.pushButtonStartDownloading.clicked.connect(self.processMultiThreading)

Dòng lệnh 20 Tui khởi tạo đối tượng ProgressDelegate (self.delegate)

Dòng lệnh 21 Tui thiết lập cột Delegate cho QTableWidget là cột số 2. Cột này chính là custom Progress Bar để hiển thị tiến độ thực hiện Background Thread tải dữ liệu

Hàm processAddURL() để thêm URL vào QTableWidget khi người dùng nhấn nút “Add“, đồng thời lưu url vào danh sách self.urls:

def processAddURL(self):
    i=self.tableWidgetURL.rowCount()
    self.tableWidgetURL.insertRow(self.tableWidgetURL.rowCount())
    it_index = QTableWidgetItem(str(i + 1))
    protocol=self.comboBoxProtocol.currentText()
    url=protocol+"://"+self.lineEditURL.text()
    self.urls.append(url)
    it_url = QTableWidgetItem(url)
    it_progress = QTableWidgetItem()
    it_progress.setData(Qt.ItemDataRole.UserRole, 0)
    self.tableWidgetURL.setItem(i, 0, it_index)
    self.tableWidgetURL.setItem(i, 1, it_url)
    self.tableWidgetURL.setItem(i, 2, it_progress)
    self.lineEditURL.setText("")

Khi người dùng nhấn nút lệnh “Add” thì các URL sẽ được hiển thị lên QTableWidget như hình dưới đây:

Hàm clearData() dùng để xóa dữ liệu trên QTableWidget khi người dùng nhấn vào nút lệnh “Clear URLs”:

def clearData(self):
    self.urls.clear()
    self.tableWidgetURL.setRowCount(0)

Hàm processMultiThreading() sẽ tạo đối tượng QThreadPool để kích hoạt tiến trình bằng hàm start(worker), tuy nhiên trong bài này là nhiều Workers:

def processMultiThreading(self):

    self.threadpool = QThreadPool()
    print(
        "Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
    )
    for n, url in enumerate(self.urls):
        worker = Worker(n, url)
        worker.signals.runningSignal.connect(self.downloadingHtml)
        worker.signals.finishSignal.connect(self.finishedDownloadHtml)

        # Execute
        self.threadpool.start(worker)

Vòng lặp for của hàm processMultiThreading() sẽ start nhiều Worker chạy Background Thread. Lưu ý rằng mỗi một ThreadPool nó có khả năng chạy tối đa bao nhiêu Threads, chứ không phải chạy bao nhiêu cũng được, nên trong trường hợp số Worker nhiều hơn maximum của ThreadPool thì cần bổ sung thêm kỹ thuật khác.

Đồng thời ta cần gán các signal: runningSignal, finishSignal thông qua các slot downloadingHtml và finishedDownloadHtml; Lúc này khi bên Worker thực hiện gọi các lệnh emit thì bên MainThread này sẽ tự động thực hiện chính xác các Slot này.

Hàm downloadingHtml () để hiển thị các ItemDownloading lên QTableWidget theo thời gian thực và cập nhật percent tiến độ:

def downloadingHtml(self, itemDownload):
    it_progress_update = self.tableWidgetURL.item(itemDownload.id, 2)
    it_progress_update.setData(Qt.ItemDataRole.UserRole, itemDownload.percent)
    fileName="download\\"+itemDownload.domain+".html"
    file = open(fileName, "a", encoding="utf-8")  # append mode
    line="%s\n"%itemDownload.data
    file.write(line)
    file.close()

Mỗi lần bên Worker thực hiện lệnh:

self.signals.runningSignal.emit(itemDownload)

Thì hàm downloadingHtml sẽ tự động được thực thi.

Khi hàm này chạy thì ta có giao diện tương tự:

Cột Progress sẽ hiển thị tỉ lệ % hoàn thành, và chương trình Tui để lưu append html text vào file. Nên khi hoàn thành thì bạn có thể vào thư mục download của dự án để mở HTML lên xem nội dung tải.

Hàm finishedDownloadHtml() để lắng nghe khi nào thì Worker truyền tín hiệu hoàn tất tiến trình:

def finishedDownloadHtml(self,id):
    it_progress_url = self.tableWidgetURL.item(id, 1)
    it_progress_url.setBackground(Qt.GlobalColor.yellow)

    it_progress_id = self.tableWidgetURL.item(id, 0)
    it_progress_id.setIcon(QtGui.QIcon('images/ic_done.png'))

Khi bên Worker thực thi lệnh:

self.signals.finishSignal.emit(self.id)

Thì hàm finishedDownloadHtml sẽ được thực hiện

Những ItemDownload nào được hoàn thành 100% thì nó được bổ sung Icon màu xanh đánh dấu là xong, đồng thời tô nền vàng cột URL, trong trường hợp tải xong toàn bộ 100% thì ta có giao diện như bên dưới:

Mã lệnh đầy đủ của MainWindowEx.py:

from urllib.parse import urlparse

from PyQt6.QtCore import Qt, QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem
from pyqtgraph import QtGui

from ItemDownload import ItemDownload
from MainWindow import Ui_MainWindow
from ProgressDelegate import ProgressDelegate
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.urls=[]
        self.delegate = ProgressDelegate(self.tableWidgetURL)
        self.tableWidgetURL.setItemDelegateForColumn(2, self.delegate)
        self.pushButtonAddURL.clicked.connect(self.processAddURL)
        self.pushButtonClearURL.clicked.connect(self.clearData)
        self.pushButtonStartDownloading.clicked.connect(self.processMultiThreading)

    def processAddURL(self):
        i=self.tableWidgetURL.rowCount()
        self.tableWidgetURL.insertRow(self.tableWidgetURL.rowCount())
        it_index = QTableWidgetItem(str(i + 1))
        protocol=self.comboBoxProtocol.currentText()
        url=protocol+"://"+self.lineEditURL.text()
        self.urls.append(url)
        it_url = QTableWidgetItem(url)
        it_progress = QTableWidgetItem()
        it_progress.setData(Qt.ItemDataRole.UserRole, 0)
        self.tableWidgetURL.setItem(i, 0, it_index)
        self.tableWidgetURL.setItem(i, 1, it_url)
        self.tableWidgetURL.setItem(i, 2, it_progress)
        self.lineEditURL.setText("")
    def clearData(self):
        self.urls.clear()
        self.tableWidgetURL.setRowCount(0)
    def processMultiThreading(self):

        self.threadpool = QThreadPool()
        print(
            "Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
        )
        for n, url in enumerate(self.urls):
            worker = Worker(n, url)
            worker.signals.runningSignal.connect(self.downloadingHtml)
            worker.signals.finishSignal.connect(self.finishedDownloadHtml)

            # Execute
            self.threadpool.start(worker)
    def downloadingHtml(self, itemDownload):
        it_progress_update = self.tableWidgetURL.item(itemDownload.id, 2)
        it_progress_update.setData(Qt.ItemDataRole.UserRole, itemDownload.percent)
        fileName="download\\"+itemDownload.domain+".html"
        file = open(fileName, "a", encoding="utf-8")  # append mode
        line="%s\n"%itemDownload.data
        file.write(line)
        file.close()
    def finishedDownloadHtml(self,id):
        it_progress_url = self.tableWidgetURL.item(id, 1)
        it_progress_url.setBackground(Qt.GlobalColor.yellow)

        it_progress_id = self.tableWidgetURL.item(id, 0)
        it_progress_id.setIcon(QtGui.QIcon('images/ic_done.png'))

    def show(self):
        self.MainWindow.show()

Bước 9: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myApp=MainWindowEx()
myApp.setupUi(QMainWindow())
myApp.show()
app.exec()

Thực thi MyApp.py ta có kết quả, Chạy phần mềm lên:

Như vậy chúng ta đã làm xong xử lý đa tiến trình để cập nhật giao diện thời gian thực, các bạn ôn tập được cách khai báo WorkerSignals, Worker cũng như ôn tập cách sử dụng QThreadPool để kích hoạt Worker và ứng dụng nó vào cập nhật giao diện thời gian thực trên QTableWidget. Đặc biệt bổ sung được custom Progress Bar cho QTableWidget, xử lý được nhiều Background Threading để cập nhật giao diện đồng thời cho nhiều tiểu trình.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/85bi70lmhj3ejo1/LearnMultithreadingPart3.rar/file

Bài tập dành cho độc giả: Hãy bổ sung Button Cancel cho từng Background Thread (đang tải chưa xong thì không muốn tải nữa).

Từ bài học sau Tui sẽ làm hàng loạt các bài minh họa liên quan tới Mô hình máy học, tích hợp nó vào giao diện tương tác người dùng, ví dụ như mô hình máy học dự báo giá nhà, mô hình máy học dự báo kinh doanh, mô hình máy học phân tích cảm xúc… Các bạn chú ý theo dõi

Chúc các bạn thành công

Bài 40: Lập trình Đa Tiến Trình trong Python-PyQt6- Part 2

Trong bài học 39 Tui đã minh họa cách xử lý đa tiến trình để vẽ giao diện và tương tác thời gian thực mà chương trình không bị treo. Trong bài học này Tui tiếp tục minh họa phần cập nhật giao diện QTableWidget thời gian thực bằng kỹ thuật xử lý đa tiến trình có giao diện tương tự như dưới đây:

  • Chương trình giả lập N số lượng Customer từ giao diện
  • Khi bấm nút “Create” chương trình sẽ sử dụng kỹ thuật đa tiến trình đã cập nhật giao diện QTableWidget, mỗi lần tiểu trình tạo một Customer ngẫu nhiên nó sẽ gửi về tiến trình chính (giao diện) hiển thị Customer lên giao diện, đồng thời cho biết tỉ lệ hoàn thành cập nhật giao diện
  • Chẳng hạn ở trên ta thấy 44% tiến độ đã hoàn thành. Và trong quá trình thực hiện cập nhật giao diện thì người sử dụng có thể thao tác trên giao diện mà không bị treo.
  • Bạn có thể áp dụng bài này trong việc tải dữ liệu từ các Restful API để nạp danh sách đối tượng lên giao diện theo thời gian thực. Ví dụ như nạp các danh sách Sản phẩm từ Restful API chẳng hạn.

Bước 1: Tạo dự án “LearnMultithreadingPart2” có cấu trúc như dưới đây:

  • “Customer.py” là file chứa lớp đối tượng Customer để chương trình mỗi lần thực thi sẽ tạo ra một Customer ngẫu nhiên trong tiểu trình rồi gửi về cho Main Thread cập nhật lên giao diện.
  • “WorkerSignals.py” là lớp kế thừa từ QObject, khai báo các Signal để thực hiện call back gửi dữ liệu trở về giao diện chính để cập nhật giao diện thời gian thực
  • “Worker.py” là lớp kế thừa từ QRunnable, nó dùng để tạo các đối tượng chạy đa tiến trình, trong quá trình xử lý nó sẽ thông qua WorkerSignals để gửi tín hiệu về cho màn hình chính cập nhật giao diện.
  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Dưới đây là Flow Chart xử lý đa tiến trình trong các mã lệnh ở các Lớp trong Project:

  • Flow Chart ở trên Tui vẽ ra 5 bước tổng quan, Tui giải thích sơ lược để các bạn nắm:
    1. Step 1: Trong lớp WorkerSignals ta khai báo các Signal để làm nhiệm vụ bắn các tín hiệu từ Background Thread (chạy ngầm) qua Main Thread (UI)
    2. Step 2: Trong lớp Worker ta sẽ sử dụng đối tượng WorkerSignals ở bước 1, đối tượng này có 2 signal: biến runningSignal để bắn tín hiệu cập nhật dữ liệu cũng như tiến độ về cho MainThread để cập nhật giao diện thời gian thực, biến finishSignal sẽ bắn tín hiệu về MainThread để báo rằng tiến trình đã hoàn tất.
    3. Step 3: Trong Main Thread khai báo các đối tượng Worker, QThreadPool để thực thi đa tiến trình, nó cần báo cho Background Thread biết là khi bắn tín hiệu (khi gọi hàm emit) về cho Main Thread thì slot nào sẽ lắng nghe
    4. Step 4: Trong quá trình thực thi Background Thread, chương trình sẽ bắn tín hiệu về cho Main Thread thông qua hàm emit
    5. Step 5: Bất cứ khi nào Background Thread gọi hàm emit thì ngay lập tức slot được khai báo trong Main Thread sẽ nhận được tín hiệu từ Back ground Thread gửi về. Tùy thuộc vào WorkerSignals ta khai báo các đối số như thế nào thì ta truyền dữ liệu tương ứng. Ta chỉ có thể cập nhật giao diện ở Main Thread, không thể cập nhật giao diện ở Background Thread, đó là lý do vì sao ta phải bắn tín hiệu về cho Main Thread.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp. Và bạn đặt tên các Widget như hình.

  • QLineEdit (lineEditN) là ô nhập liệu
  • QProgressBar (progressbarPercent) là thanh trạng thái đánh dấu quá trình xử lý được bao nhiêu %.
  • QPushButton (pushButtonCreate) là widget sẽ ra lệnh để thực thi đa tiến trình
  • QTableWidget (tableWidgetCustomer)

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(436, 378)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setGeometry(QtCore.QRect(40, 10, 131, 21))
        self.label.setObjectName("label")
        self.lineEditN = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditN.setGeometry(QtCore.QRect(180, 10, 113, 22))
        self.lineEditN.setObjectName("lineEditN")
        self.pushButtonCreate = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonCreate.setGeometry(QtCore.QRect(310, 10, 93, 28))
        self.pushButtonCreate.setObjectName("pushButtonCreate")
        self.progressBarPercent = QtWidgets.QProgressBar(parent=self.centralwidget)
        self.progressBarPercent.setGeometry(QtCore.QRect(50, 50, 371, 23))
        self.progressBarPercent.setProperty("value", 0)
        self.progressBarPercent.setObjectName("progressBarPercent")
        self.tableWidgetCustomer = QtWidgets.QTableWidget(parent=self.centralwidget)
        self.tableWidgetCustomer.setGeometry(QtCore.QRect(20, 90, 401, 231))
        self.tableWidgetCustomer.setObjectName("tableWidgetCustomer")
        self.tableWidgetCustomer.setColumnCount(3)
        self.tableWidgetCustomer.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(2, item)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 436, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh-Multi Threading"))
        self.label.setText(_translate("MainWindow", "Number of Customer:"))
        self.pushButtonCreate.setText(_translate("MainWindow", "Create"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "Id"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "Name"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Age"))

Bước 4: Viết mã lệnh cho “Customer.py

class Customer:
    def __init__(self,id=0,name=None,age=0):
        self.id=id
        self.name=name
        self.age=age

Lớp Customer được định nghĩa với 4 thuộc tính: id, name, age. Với constructor nhận vào 4 biến có giá trị mặc định như trên.

Bước 5: Viết mã lệnh cho “WorkerSignals.py

from PyQt6.QtCore import pyqtSignal, QObject

from Customer import Customer

class WorkerSignals(QObject):
    runningSignal=pyqtSignal(Customer,int)
    finishSignal =pyqtSignal()

Lớp WorkerSignals được kế thừa từ QObject, lớp này Tui khai báo 2 biến đối tượng có kiểu pyqtSignal:

  • runningSignal là signal để truyền tín hiệu trong quá trình thực hiện cập nhật giao diện thời gian thực. Signal này truyền 2 đối số, đối số 1 là đối tượng Customer hiển thị lên QTableWidget, đối số 2 là percent thực hiện quá trình cập nhật giao diện thời gian thực
  • finishSignal là signal thông báo đã hoàn tất thực hiện chạy đa tiến trình

Bước 6: Viết mã lệnh cho “Worker.py

import random
import time

from PyQt6.QtCore import QRunnable

from Customer import Customer
from WorkerSignals import WorkerSignals

class Worker(QRunnable):
    def __init__(self,n):
        super().__init__()
        self.n = n
        self.signals = WorkerSignals()
    def run(self) -> None:
        for i in range(self.n):
            percent=int(100*(i+1)/self.n)
            customer=Customer()
            customer.id=i
            customer.name="name "+str(i)
            customer.age=random.randint(18,60)
            self.signals.runningSignal.emit(customer,percent)
            time.sleep(0.01)
        self.signals.finishSignal.emit()

Lớp Worker được kế thừa từ lớp QRunnable, lớp này Tui định nghĩa 2 hàm:

  • Constructor nhận vào đối số n là số lượng Customer giả lập mà người dùng muốn hiển thị trên giao diện QTableWidget. Nó cũng khởi tạo đối tượng WorkerSignals để sử dụng cho việc truyền tin về màn hình chính (Main Thread), Chúng ta chỉ có thể cập nhật giao diện ở Main Thread
  • override hàm run, hàm này là chạy long time, Tui đang giả lập nó 1 vòng lặp để tạo ngẫu nhiên các đối tượng, và tính percent. Sau đó dùng biến singal runningSignal để truyền tín hiệu cùng với dữ liệu về cho Main Thread thông qua hàm emit. Mỗi lần lặp Tui cho nó nghỉ 0.01 giây.
  • Cuối cùng khi kết thúc vòng lặp Tui gọi finishSignal để truyền tín hiệu là kết thúc tiến trình

Bước 7: Viết mã lệnh cho “MainWindowEx.py”

Khởi tạo Constructor như bên dưới, và override hàm setupUi để thiết lập giao diện cũng như gọi signal cho Button create

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox

from MainWindow import Ui_MainWindow
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonCreate.clicked.connect(self.processUpdate)

Hàm processUpdate sẽ tạo đối tượng QThreadPool để kích hoạt tiến trình bằng hàm start(worker):

def processUpdate(self):
    self.tableWidgetCustomer.setRowCount(0)
    n=int(self.lineEditN.text())
    self.threadPool = QThreadPool()
    worker=Worker(n)
    worker.signals.runningSignal.connect(self.updateUI)
    worker.signals.finishSignal.connect(self.finishthread)
    self.threadPool.start(worker)

Trong hàm processUpdate này ta khai báo đối tượng worker, truyền n là số lượng Customer mà người giả lập để hiển thị lên QTableWidget.

Đồng thời ta cần gán các signal: runningSignal, finishSignal thông qua các slot updateUI và finishthred; Lúc này khi bên Worker thực hiện gọi các lệnh emit thì bên MainThread này sẽ tự động thực hiện chính xác các Slot.

Hàm updateUI() để hiển thị các Customer lên QTableWidget theo thời gian thực và cập nhật percent tiến độ:

def updateUI(self,customer,percent):
    row=self.tableWidgetCustomer.rowCount()
    self.tableWidgetCustomer.insertRow(row)
    self.tableWidgetCustomer.setItem(row, 0, QTableWidgetItem(str(customer.id)))
    self.tableWidgetCustomer.setItem(row, 1, QTableWidgetItem(customer.name))
    self.tableWidgetCustomer.setItem(row, 2, QTableWidgetItem(str(customer.age)))
    self.progressBarPercent.setValue(percent)

Mỗi lần bên Worker thực hiện lệnh:

self.signals.runningSignal.emit(customer,percent)

Thì hàm updateUI sẽ tự động được thực thi.

Hàm finishthread() để lắng nghe khi nào thì Worker truyền tín hiệu hoàn tất tiến trình:

def finishthread(self):
    msg=QMessageBox()
    msg.setText("finished!")
    msg.setWindowTitle("information")
    msg.exec()

Khi bên Worker thực thi lệnh:

self.signals.finishSignal.emit()

Thì hàm finishthread sẽ được thực hiện

Mã lệnh đầy đủ của MainWindowEx.py:

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox

from MainWindow import Ui_MainWindow
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonCreate.clicked.connect(self.processUpdate)

    def processUpdate(self):
        self.tableWidgetCustomer.setRowCount(0)
        n=int(self.lineEditN.text())
        self.threadPool = QThreadPool()
        worker=Worker(n)
        worker.signals.runningSignal.connect(self.updateUI)
        worker.signals.finishSignal.connect(self.finishthread)
        self.threadPool.start(worker)

    def updateUI(self,customer,percent):
        row=self.tableWidgetCustomer.rowCount()
        self.tableWidgetCustomer.insertRow(row)
        self.tableWidgetCustomer.setItem(row, 0, QTableWidgetItem(str(customer.id)))
        self.tableWidgetCustomer.setItem(row, 1, QTableWidgetItem(customer.name))
        self.tableWidgetCustomer.setItem(row, 2, QTableWidgetItem(str(customer.age)))
        self.progressBarPercent.setValue(percent)

    def finishthread(self):
        msg=QMessageBox()
        msg.setText("finished!")
        msg.setWindowTitle("information")
        msg.exec()

    def show(self):
        self.MainWindow.show()

Bước 8: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()

Thực thi MyApp.py ta có kết quả, Chạy phần mềm lên:

Như vậy chúng ta đã làm xong xử lý đa tiến trình để cập nhật giao diện thời gian thực, các bạn ôn tập được cách khai báo WorkerSignals, Worker cũng như ôn tập cách sử dụng QThreadPool để kích hoạt Worker và ứng dụng nó vào cập nhật giao diện thời gian thực trên QTableWidget.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/oudzu7kgjil8n3k/LearnMultithreadingPart2.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn xử lý đa tiến trình gồm nhiều Worker thực hiện đồng thời để tải dữ liệu Online nội dung các Website trên internet. Và cập nhật dữ liệu thời gian thực cho QProgressBar trong QTableWidget cùng với lưu nội dung tải được xuống ổ cứng. Các bạn chú ý theo dõi

Chúc các bạn thành công

Bài 39: Lập trình Đa Tiến Trình trong Python-PyQt6- Part 1

Trong quá trình triển khai phần mềm, đôi khi chúng ta muốn cập nhật giao diện thời gian thực, muốn kiểm soát tiến độ từng bước quá trình xử lý, đồng thời muốn giao diện luôn mượt mà, không bị treo khi chương trình cùng lúc thực hiện nhiều tác vụ đồng thời. Trường hợp này chính là cơ chế xử lý Đa tiến trình (Multi-Threading).

Trong phần Đa tiến trình này Tui sẽ hướng dẫn 3 ví dụ minh họa về cập nhật giao diện thời gian thực để bạn làm quen với cách thức sử dụng các thư viện hỗ trợ đa nhiệm trong Python- PyQt6.

Giao diện trên tui minh họa người dùng có thể vẽ các QPushButton thời gian thực, trong quá trình vẽ người sử dụng có thể vừa tương tác tới các QPushButton đó là nhấn vào nó thì đổi nền xanh và chữ đỏ, đồng thời cũng cho biết tiến độ thực hiện của quá trình xử lý, ví dụ ở trên là đang xử lý tác vụ được 62%. Lưu ý là chương trình vẫn cứ đang thực hiện tác vụ vẽ các QPushButton, người sử dụng cứ nhấn các Button thoải mái mà không bị treo phần mềm.

Vậy ta làm điều này như thế nào?

Bước 1: Tạo dự án “LearnMultithreadingPart1” có cấu trúc như dưới đây:

  • “WorkerSignals.py” là lớp kế thừa từ QObject, khai báo các Signal để thực hiện call back gửi dữ liệu trở về giao diện chính để cập nhật giao diện thời gian thực
  • “Worker.py” là lớp kế thừa từ QRunnable, nó dùng để tạo các đối tượng chạy đa tiến trình, trong quá trình xử lý nó sẽ thông qua WorkerSignals để gửi tín hiệu về cho màn hình chính cập nhật giao diện.
  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Dưới đây là Flow Chart xử lý đa tiến trình trong các mã lệnh ở các Lớp trong Project:

  • Flow Chart ở trên Tui vẽ ra 5 bước tổng quan, Tui giải thích sơ lược để các bạn nắm:
    1. Step 1: Trong lớp WorkerSignals ta khai báo các Signal để làm nhiệm vụ bắn các tín hiệu từ Background Thread (chạy ngầm) qua Main Thread (UI)
    2. Step 2: Trong lớp Worker ta sẽ sử dụng đối tượng WorkerSignals ở bước 1, đối tượng này có 2 signal: biến runningSignal để bắn tín hiệu cập nhật dữ liệu cũng như tiến độ về cho MainThread để cập nhật giao diện thời gian thực, biến finishSignal sẽ bắn tín hiệu về MainThread để báo rằng tiến trình đã hoàn tất.
    3. Step 3: Trong Main Thread khai báo các đối tượng Worker, QThreadPool để thực thi đa tiến trình, nó cần báo cho Background Thread biết là khi bắn tín hiệu (khi gọi hàm emit) về cho Main Thread thì slot nào sẽ lắng nghe
    4. Step 4: Trong quá trình thực thi Background Thread, chương trình sẽ bắn tín hiệu về cho Main Thread thông qua hàm emit
    5. Step 5: Bất cứ khi nào Background Thread gọi hàm emit thì ngay lập tức slot được khai báo trong Main Thread sẽ nhận được tín hiệu từ Back ground Thread gửi về. Tùy thuộc vào WorkerSignals ta khai báo các đối số như thế nào thì ta truyền dữ liệu tương ứng.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp. và đặt tên các Widget như hình.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(429, 419)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setObjectName("label")
        self.horizontalLayout.addWidget(self.label)
        self.lineEditNumberOfButton = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditNumberOfButton.setObjectName("lineEditNumberOfButton")
        self.horizontalLayout.addWidget(self.lineEditNumberOfButton)
        self.pushButtonDraw = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonDraw.setObjectName("pushButtonDraw")
        self.horizontalLayout.addWidget(self.pushButtonDraw)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.progressBarPercent = QtWidgets.QProgressBar(parent=self.centralwidget)
        self.progressBarPercent.setProperty("value", 0)
        self.progressBarPercent.setObjectName("progressBarPercent")
        self.verticalLayout.addWidget(self.progressBarPercent)
        self.scrollArea = QtWidgets.QScrollArea(parent=self.centralwidget)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        self.scrollAreaWidgetContents = QtWidgets.QWidget()
        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 405, 276))
        self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.verticalLayoutButton = QtWidgets.QVBoxLayout()
        self.verticalLayoutButton.setObjectName("verticalLayoutButton")
        self.verticalLayout_3.addLayout(self.verticalLayoutButton)
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self.verticalLayout.addWidget(self.scrollArea)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 429, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - MultiThreading"))
        self.label.setText(_translate("MainWindow", "Number of Button:"))
        self.pushButtonDraw.setText(_translate("MainWindow", "Draw"))

Bước 4: Viết mã lệnh cho “WorkerSignals.py

from PyQt6.QtCore import QObject, pyqtSignal

class WorkerSignals(QObject):
    runningSignal=pyqtSignal(str,int)
    finishSignal=pyqtSignal()

Lớp này Tui khai báo 2 biến đối tượng có kiểu pyqtSignal:

  • runningSignal là signal để truyền tín hiệu trong quá trình thực hiện cập nhật giao diện thời gian thực, nó truyền 2 đối số, đối số 1 là chuỗi giá trị hiển thị cho QPushButton, đối số 2 là percent thực hiện quá trình cập nhật giao diện thời gian thực
  • finishSignal là signal thông báo đã hoàn tất thực hiện chạy đa tiến trình

Bước 5: Viết mã lệnh cho “Worker.py

import random
import time

from PyQt6.QtCore import QRunnable

from WorkerSignals import WorkerSignals


class Worker(QRunnable):
    def __init__(self,n):
        super().__init__()
        self.n=n
        self.signals=WorkerSignals()

    def run(self) -> None:
        for i in range(self.n):
            x=random.randint(0,100)
            label="value-%s"%x
            percent=int(100*(i+1)/self.n)
            self.signals.runningSignal.emit(label,percent)
            time.sleep(0.01)
        self.signals.finishSignal.emit()

Lớp này Tui định nghĩa 2 hàm:

  • Constructor nhận vào đối số n là số lượng QPushButton mà người dùng muốn vẽ trên giao diện. Nó cũng khởi tạo đối tượng WorkerSignals để sử dụng cho việc truyền tin về màn hình chính (Main Thread), Chúng ta chỉ có thể cập nhật giao diện ở Main Thread
  • override hàm run, hàm này là chạy long time, Tui đang giả lập nó 1 vòng lặp để tạo ngẫu nhiên các nhãn cho QPushButton, và tính percent. Sau đó dùng biên singal runningSignal để truyền tín hiệu cùng với dữ liệu về cho Main Thread thông qua hàm emit. Mỗi lần lặp Tui cho nó nghỉ 0.01 giây. Cuối cùng khi kết thúc vòng lặp Tui gọi finishSignal để truyền tín hiệu là kết thúc tiến trình

Bước 6: Viết mã lệnh cho “MainWindowEx.py”

Khởi tạo Constructor như bên dưới, và override hàm setupUi để thiết lập giao diện cũng như gọi signal cho Button draw threading

from functools import partial

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QPushButton, QMessageBox

from MainWindow import Ui_MainWindow
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonDraw.clicked.connect(self.processDraw)

Hàm processDraw sẽ tạo đối tượng QThreadPool để kích hoạt tiến trình bằng hàm start(worker):

def processDraw(self):
    n=(int)(self.lineEditNumberOfButton.text())
    self.threadpool=QThreadPool()
    worker=Worker(n)
    worker.signals.runningSignal.connect(self.drawingThreading)
    worker.signals.finishSignal.connect(self.drawingFinished)
    self.threadpool.start(worker)

Trong hàm processDraw này ta khai báo đối tượng worker, truyền n là số lượng QPushButton mà người sử dụng muốn vẽ.

Đồng thời ta cần gán các signal: runningSignal, finishSignal thông qua các slot drawingThreading và drawingFinished; Lúc này khi bên Worker thực hiện gọi lệnh emit thì bên MainThread này sẽ tự động thực hiện chính xác các Slot.

Hàm drawingThreading() để vẽ các QPushButton thời gian thực và cập nhật percent tiến độ, cũng như cho gán Signal runtime cho phép người dùng đổi màu nền và màu chữ của QPushButton:

def drawingThreading(self,label,percent):
    self.progressBarPercent.setValue(percent)
    button=QPushButton(self.MainWindow)
    button.setText(label)
    button.clicked.connect(partial(self.changeColor,button))
    self.verticalLayoutButton.addWidget(button)

Hàm đổi màu nền và màu chữ của QPushButton:

def changeColor(self,button):
    button.setStyleSheet("background-color: cyan;color:red");

Hàm drawingFinished() để lắng nghe khi nào thì Worker truyền tín hiệu hoàn tất tiến trình:

def drawingFinished(self):
    msgBox=QMessageBox(self.MainWindow)
    msgBox.setText("Drawing multi threading finished")
    msgBox.exec()

Mã lệnh đầy đủ của MainWindowEx.py:

from functools import partial

from PyQt6.QtCore import QThreadPool
from PyQt6.QtWidgets import QPushButton, QMessageBox

from MainWindow import Ui_MainWindow
from Worker import Worker


class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.pushButtonDraw.clicked.connect(self.processDraw)
    def processDraw(self):
        n=(int)(self.lineEditNumberOfButton.text())
        self.threadpool=QThreadPool()
        worker=Worker(n)
        worker.signals.runningSignal.connect(self.drawingThreading)
        worker.signals.finishSignal.connect(self.drawingFinished)
        self.threadpool.start(worker)
    def drawingThreading(self,label,percent):
        self.progressBarPercent.setValue(percent)
        button=QPushButton(self.MainWindow)
        button.setText(label)
        button.clicked.connect(partial(self.changeColor,button))
        self.verticalLayoutButton.addWidget(button)
    def changeColor(self,button):
        button.setStyleSheet("background-color: cyan;color:red");
    def drawingFinished(self):
        msgBox=QMessageBox(self.MainWindow)
        msgBox.setText("Drawing multi threading finished")
        msgBox.exec()
    def show(self):
        self.MainWindow.show()

Bước 7: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myApp= MainWindowEx()
myApp.setupUi(QMainWindow())
myApp.show()
app.exec()

Thực thi MyApp.py ta có kết quả, Chạy phần mềm lên:

Như vậy chúng ta đã làm xong xử lý đa tiến trình để cập nhật giao diện thời gian thực, các bạn biết cách khai báo WorkerSignals, Worker cũng như sử dụng QThreadPool để kích hoạt Worker. Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/zq1tkdk1yqfyilh/LearnMultithreadingPart1.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn xử lý đa tiến trình mà cập nhật dữ liệu thời gian thực cho QTableWidget. Các bạn chú ý theo dõi

Chúc các bạn thành công

Bài 38: Trực quan hóa dữ liệu – PyQtGraph-Custom BarGraphItem-PyQt6 – Part 8

Đây là phần cuối cùng về PyQtGraph trong chuỗi bài hướng dẫn, nó vẫn còn nhiều loại chart khác nhau rất đẹp và phong phú, các bạn tự tìm hiểu thêm. Đặc biệt PyQtGraph hỗ trợ cả 3D tương tác rất đẹp và chuyên nghiệp, nhưng vì thời gian có hạn nên Tui không thể giới thiệu tiếp, mà các bài học khác Tui sẽ chuyển qua Lập trình đa tiến trình để cập nhật giao diện thời gian thực được mượt nhất.

Bài học này ta sẽ làm Custom BarGraphItem bằng cách tạo ra một lớp kế thừa từ BarGraphItem. Mục đích của việc này là cung cấp khả năng tương tự sự kiện trực tiếp trên từng BarItem để hỗ trợ cho việc thống kê cũng như xem chi tiết được tốt nhất.

Bài này xử lý y chang như bài 37, nó chỉ khác ở chỗ ta làm CustomBarGraphItem và xử lý sự kiện cho các Bar item này, khi nhấn vào Bar Item nó sẽ tô nền vàng, khi nhấn qua bar item khác thì nó được phục hồi lại màu cũ.

Ta làm theo các bước dưới đây.

Bước 1: Tạo dự án tên “LearnPyQtBarGraphPart8” có cấu trúc như dưới đây:

  • dataset-revenue.xlsx” là file Excel mẫu của dự án
  • “CustomBarGraphItem.py” là lớp kế thừa từ BarGraphItem để làm Custom Bar Item, xử lý người dùng tương tác trực tiếp trên Bar Item
  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp.

Bạn lưu ý trong bài này Tui bổ sung nhóm QHBoxLayout ở bên trên để chọn Dataset là file Excel.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(647, 518)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setObjectName("label")
        self.horizontalLayout_2.addWidget(self.label)
        self.lineEditDataset = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditDataset.setObjectName("lineEditDataset")
        self.horizontalLayout_2.addWidget(self.lineEditDataset)
        self.pushButtonPickDataset = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonPickDataset.setObjectName("pushButtonPickDataset")
        self.horizontalLayout_2.addWidget(self.pushButtonPickDataset)
        self.verticalLayout_2.addLayout(self.horizontalLayout_2)
        self.layoutGraph = QtWidgets.QVBoxLayout()
        self.layoutGraph.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
        self.layoutGraph.setObjectName("layoutGraph")
        self.verticalLayout_2.addLayout(self.layoutGraph)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.chkBackgroundGrid = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkBackgroundGrid.setChecked(True)
        self.chkBackgroundGrid.setObjectName("chkBackgroundGrid")
        self.horizontalLayout.addWidget(self.chkBackgroundGrid)
        self.chkLegend = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkLegend.setChecked(True)
        self.chkLegend.setObjectName("chkLegend")
        self.horizontalLayout.addWidget(self.chkLegend)
        self.pushButtonChangBackground = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangBackground.setObjectName("pushButtonChangBackground")
        self.horizontalLayout.addWidget(self.pushButtonChangBackground)
        self.cboBarItem = QtWidgets.QComboBox(parent=self.centralwidget)
        self.cboBarItem.setObjectName("cboBarItem")
        self.horizontalLayout.addWidget(self.cboBarItem)
        self.pushButtonChangeBarColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeBarColor.setObjectName("pushButtonChangeBarColor")
        self.horizontalLayout.addWidget(self.pushButtonChangeBarColor)
        self.pushButtonClose = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClose.setObjectName("pushButtonClose")
        self.horizontalLayout.addWidget(self.pushButtonClose)
        self.verticalLayout_2.addLayout(self.horizontalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 647, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.pushButtonClose.clicked.connect(MainWindow.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - Custom BarGraph"))
        self.label.setText(_translate("MainWindow", "Choose Dataset:"))
        self.pushButtonPickDataset.setText(_translate("MainWindow", "Pick Dataset"))
        self.chkBackgroundGrid.setText(_translate("MainWindow", "Background Grid"))
        self.chkLegend.setText(_translate("MainWindow", "Legend"))
        self.pushButtonChangBackground.setText(_translate("MainWindow", "Chart Background"))
        self.pushButtonChangeBarColor.setText(_translate("MainWindow", "Bar Color"))
        self.pushButtonClose.setText(_translate("MainWindow", "Close"))

Bước 4: Viết mã lệnh cho “CustomBarGraphItem.py”

import pyqtgraph as pg

previousBarGraph=None
previousBarGraphBrush=None
class CustomBarGraphItem(pg.BarGraphItem):
    def mouseClickEvent(self, event):
        global previousBarGraph
        global  previousBarGraphBrush
        if previousBarGraph!=None:
            previousBarGraph.opts["brush"]=previousBarGraphBrush
            previousBarGraph._updateColors(previousBarGraph.opts)
            previousBarGraph.update()
        previousBarGraph=self
        previousBarGraphBrush=self.opts["brush"]
        self.opts["brush"]='y'
        self._updateColors(self.opts)
        self.update()

Lớp CustomBarGraphItem kế thừa từ BarGraphItem.

Lớp này khai báo 2 global veriable:

  • previousBarGraph để lưu lại BarItem nào được click trước đó
  • previousBarGraphBrush để lưu lại màu của BarItem được click trước đó

2 biến này dùng để phục hồi lại trạng thái ban đầu khi người dùng chọn qua Bar item khác.

Và ta override phương thức mouseClickEvent, sự kiện này sẽ kiểm tra BarItem nào đang được click để đổi màu, ta dùng hàm _updateColors() để đổi màu

Bước 5: Viết mã lệnh cho “MainWindowEx.py”

Lưu ý lớp này mã lệnh y chang như bài 37, Tui chỉ thay thế chỗ sử dụng CustomBarrGraphItem mà thôi, vì vậy Tui không giải thích chi tiết từng thành phần nữa, mà Tui gửi đầy đủ coding ở đây, cuối coding Tui giải thích sơ lược cách dùng CustomBarGraphItem:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog, QFileDialog

from CustomBarGraphItem import CustomBarGraphItem
from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg
import pandas as pd
import random as nd

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.setupBarGraph()
        self.setupSignal()
    def setupBarGraph(self):
        self.graphWidget = pg.PlotWidget()
        self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
        self.graphWidget.setBackground('w')

        labelStyle = {"color": "green", "font-size": "18px"}
        labelBrandStyle = {"color": "purple", "font-size": "18px"}
        self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
        self.graphWidget.setLabel("bottom","Cities",**labelStyle)
        self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)

        self.graphWidget.showGrid(x=True, y=True)

        self.width = 0.2
        self.bargraphItems = []

        self.legend = self.graphWidget.addLegend()

        self.layoutGraph.addWidget(self.graphWidget)

    def setupSignal(self):
        self.pushButtonPickDataset.clicked.connect(self.processPickDataset)
        self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
        self.chkLegend.stateChanged.connect(self.processChangeLegend)
        self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
        self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)

    def processPickDataset(self):
        filters = "Dataset (*.xlsx);;All files(*)"
        filename, selected_filter = QFileDialog.getOpenFileName(
            self.MainWindow,
            filter=filters,
        )
        self.lineEditDataset.setText(filename)

        self.drawChart(filename)

    def drawChart(self,datasetPath):
        #read Excel file into DataFrame by Pandas
        dataframe = pd.read_excel(datasetPath)
        #Read columns, we omit first column (corner)
        #Hà Nội,Huế,TP.HCM,Cần Thơ,An Giang:
        self.xlab = dataframe.columns.values[1:]
        #create an array from 1 to number of column
        self.positions = np.arange(1, len(self.xlab) + 1)
        #remove existing BarGraph Item
        #(user can choose file many times)
        for barItem in self.bargraphItems:
            self.graphWidget.removeItem(barItem)
        self.bargraphItems = []
        self.cboBarItem.clear()
        i = 0
        #loop to read row data
        for row in range(len(dataframe.values)):
            #Get value in the first column for each row(quarter X)
            name = dataframe.values[row][0]
            #Get an Array data for each row
            revenues = dataframe.values[row][1:].tolist()
            #setup the position for each graph bar item
            x = self.positions + i * self.width
            #random color for each bar item
            brush = (nd.randint(0, 255), nd.randint(0, 255), nd.randint(0, 255))
            #create BarGraphItem:
            bargraphItem = CustomBarGraphItem(x=x, height=revenues, width=self.width, brush=brush, name=name)
            i = i + 1
            #store bargraphItem into bargraphItems
            self.bargraphItems.append(bargraphItem)
            #add the Bar Graph Item into the Chart
            self.graphWidget.addItem(bargraphItem)
            #Draw the name of BarItem into QComBoBox
            self.cboBarItem.addItem(bargraphItem.name(), bargraphItem)
        #setup tick
        self.autoTick(self.graphWidget, self.xlab)
        #setup label
        self.autolabel(self.bargraphItems)
    def processChangeGrid(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.graphWidget.showGrid(x=True, y=True)
        else:
            self.graphWidget.showGrid(x=False, y=False)
    def processChangeLegend(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.legend.show()
        else:
            self.legend.hide()
    def processChangeChartBackground(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.graphWidget.setBackground(color.name())
            del dialog
    def processChangeBarColor(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            bargraphItem=self.bargraphItems[self.cboBarItem.currentIndex()]
            bargraphItem.opts["brush"]=color.name()
            bargraphItem._updateColors(bargraphItem.opts)
            del dialog
    def autoTick(self,graphWidget, xlab):
        ticks = []
        for i, item in enumerate(xlab):
            ticks.append((i +1+ 2*self.width, item))
        ticks = [ticks]
        ax = graphWidget.getAxis('bottom')
        ax.setTicks(ticks)

    def autolabel(self,barItems):
        # attach some text labels
        for barItem in barItems:
            xs, heights = barItem.getData()
            for i in range(len(heights)):
                height = heights[i]
                x = xs[i]
                clr = barItem.opts["brush"]
                text = pg.TextItem(str(height), color=clr)
                text.setParentItem(barItem)
                text.setX(x)
                text.setY(height)
                text.setAnchor((QPointF(0.5, 0.75)))

    def show(self):
        self.MainWindow.show()

Chúng ta quan sát dòng lệnh 83 thôi, đây là chỗ thay thế pg.BarGraphItem bằng:

bargraphItem = CustomBarGraphItem(x=x, height=revenues, width=self.width, brush=brush, name=name).

Bước 6: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()

Thực thi MyApp.py ta có kết quả, Chạy phần mềm lên:

Khi nhấn BarItem nào thì nó tự động đổi qua nền vàng, bấm qua BarItem khác thì nó phục hồi lại màu như cũ.

Như vậy chúng ta đã làm xong CustomGraphBarItem, các bạn đã biết cách xử lý sự kiện riêng cho từng bar item, có thể áp dụng vào nhiều mục đích khác nhau.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/kdvzf1melwq37ty/LearnPyQtBarGraphPart8.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách lập trình Đa tiến trình để giúp cập nhật giao diện thời gian thực được mượt mà.

Chúc các bạn thành công

Bài 37: Trực quan hóa dữ liệu – PyQtGraph-Excel Data-PyQt6 – Part 7

Bài học này Tui sẽ nâng cấp bài 36, đó là dữ liệu thay vì hardcode thì Tui đọc tự động từ Excel:

  • Tự lấy tick trong cột file Excel (Hà Nội, Huế, TP.HCM, Cần Thơ, An Giang)
  • Tự lấy Legend cho Bar Item (Quarter 1, Quarter 2, Quarter 3, Quarter 4)
  • Màu các Bar Item được tạo ngẫu nhiên theo 3 thông số R, G, B
  • Tự động vẽ Chart mà không lệ thuộc vào tiêu đề Cột hay tiều đề dòng của dữ liệu trong File Excel
  • Các chức năng còn lại được kế thừa từ bài 36

Ta tiến hành từng bước như sau:

Bước 0: Cài thư viện “xlrd

pip install xlrd

Bước 1: Tạo dự án tên “LearnPyQtBarGraphPart7” có cấu trúc như dưới đây:

  • dataset-revenue.xlsx” là file Excel mẫu của dự án
  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp.

Bạn lưu ý trong bài này Tui bổ sung nhóm QHBoxLayout ở bên trên để chọn Dataset là file Excel.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(647, 518)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        self.label.setObjectName("label")
        self.horizontalLayout_2.addWidget(self.label)
        self.lineEditDataset = QtWidgets.QLineEdit(parent=self.centralwidget)
        self.lineEditDataset.setObjectName("lineEditDataset")
        self.horizontalLayout_2.addWidget(self.lineEditDataset)
        self.pushButtonPickDataset = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonPickDataset.setObjectName("pushButtonPickDataset")
        self.horizontalLayout_2.addWidget(self.pushButtonPickDataset)
        self.verticalLayout_2.addLayout(self.horizontalLayout_2)
        self.layoutGraph = QtWidgets.QVBoxLayout()
        self.layoutGraph.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
        self.layoutGraph.setObjectName("layoutGraph")
        self.verticalLayout_2.addLayout(self.layoutGraph)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.chkBackgroundGrid = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkBackgroundGrid.setChecked(True)
        self.chkBackgroundGrid.setObjectName("chkBackgroundGrid")
        self.horizontalLayout.addWidget(self.chkBackgroundGrid)
        self.chkLegend = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkLegend.setChecked(True)
        self.chkLegend.setObjectName("chkLegend")
        self.horizontalLayout.addWidget(self.chkLegend)
        self.pushButtonChangBackground = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangBackground.setObjectName("pushButtonChangBackground")
        self.horizontalLayout.addWidget(self.pushButtonChangBackground)
        self.cboBarItem = QtWidgets.QComboBox(parent=self.centralwidget)
        self.cboBarItem.setObjectName("cboBarItem")
        self.horizontalLayout.addWidget(self.cboBarItem)
        self.pushButtonChangeBarColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeBarColor.setObjectName("pushButtonChangeBarColor")
        self.horizontalLayout.addWidget(self.pushButtonChangeBarColor)
        self.pushButtonClose = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClose.setObjectName("pushButtonClose")
        self.horizontalLayout.addWidget(self.pushButtonClose)
        self.verticalLayout_2.addLayout(self.horizontalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 647, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.pushButtonClose.clicked.connect(MainWindow.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - BarGraph"))
        self.label.setText(_translate("MainWindow", "Choose Dataset:"))
        self.pushButtonPickDataset.setText(_translate("MainWindow", "Pick Dataset"))
        self.chkBackgroundGrid.setText(_translate("MainWindow", "Background Grid"))
        self.chkLegend.setText(_translate("MainWindow", "Legend"))
        self.pushButtonChangBackground.setText(_translate("MainWindow", "Chart Background"))
        self.pushButtonChangeBarColor.setText(_translate("MainWindow", "Bar Color"))
        self.pushButtonClose.setText(_translate("MainWindow", "Close"))

Bước 4: Viết mã lệnh cho “MainWindowEx.py”

Khai báo lớp MainWindowEx kế thừa từ Generate Python code ở bước 3, dưới đây là hàm override setupUi, Tui chỉnh sửa chút xíu so với bài học trước, đó là trong hàm này nó sẽ gọi 2 hạm setupBarGraph() để cấu hình Graph trống ban đầu và setupSignal() để xủ lý sự kiến:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog, QFileDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg
import pandas as pd
import random as nd

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.setupBarGraph()
        self.setupSignal()

Hàm setupBarGraph() được viết theo mã lệnh như dưới đây:

def setupBarGraph(self):
    self.graphWidget = pg.PlotWidget()
    self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
    self.graphWidget.setBackground('w')

    labelStyle = {"color": "green", "font-size": "18px"}
    labelBrandStyle = {"color": "purple", "font-size": "18px"}
    self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
    self.graphWidget.setLabel("bottom","Cities",**labelStyle)
    self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)

    self.graphWidget.showGrid(x=True, y=True)

    self.width = 0.2
    self.bargraphItems = []

    self.legend = self.graphWidget.addLegend()

    self.layoutGraph.addWidget(self.graphWidget)

Hàm setupBarGraph() ở trên thiết kế Graph trống như hình dưới đây (các mã lệnh Tui đã giải thích chi tiết ở các bài học trước, nếu chưa hiểu thì các bạn xem lại):

Hàm setupSignal() để gán các Signal để triệu gọi các hàm, slot:

def setupSignal(self):
    self.pushButtonPickDataset.clicked.connect(self.processPickDataset)
    self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
    self.chkLegend.stateChanged.connect(self.processChangeLegend)
    self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
    self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)

Hàm setupSignal() Tui có bổ sung slot “processPickDataset”, slot này sẽ cung cấp chức năng lựa chọn file Excel cho người dùng và vẽ lên Graph.

def processPickDataset(self):
    filters = "Dataset (*.xlsx);;All files(*)"
    filename, selected_filter = QFileDialog.getOpenFileName(
        self.MainWindow,
        filter=filters,
    )
    self.lineEditDataset.setText(filename)

    self.drawChart(filename)

Hàm trên, Tui dùng QFileDialog cho người dùng chọn File được filter .xlsx mặc định.

Chương trình sẽ hiển thị tên file và đường dẫn lên trên QLineEdit

Và gọi hàm drawChart() để vẽ các BarGraph Item.

def drawChart(self,datasetPath):
    #read Excel file into DataFrame by Pandas
    dataframe = pd.read_excel(datasetPath)
    #Read columns, we omit first column (corner)
    #Hà Nội,Huế,TP.HCM,Cần Thơ,An Giang:
    self.xlab = dataframe.columns.values[1:]
    #create an array from 1 to number of column
    self.positions = np.arange(1, len(self.xlab) + 1)
    #remove existing BarGraph Item
    #(user can choose file many times)
    for barItem in self.bargraphItems:
        self.graphWidget.removeItem(barItem)
    self.bargraphItems = []
    self.cboBarItem.clear()
    i = 0
    #loop to read row data
    for row in range(len(dataframe.values)):
        #Get value in the first column for each row(quarter X)
        name = dataframe.values[row][0]
        #Get an Array data for each row
        revenues = dataframe.values[row][1:].tolist()
        #setup the position for each graph bar item
        x = self.positions + i * self.width
        #random color for each bar item
        brush = (nd.randint(0, 255), nd.randint(0, 255), nd.randint(0, 255))
        #create BarGraphItem:
        bargraphItem = pg.BarGraphItem(x=x, height=revenues, width=self.width, brush=brush, name=name)
        i = i + 1
        #store bargraphItem into bargraphItems
        self.bargraphItems.append(bargraphItem)
        #add the Bar Graph Item into the Chart
        self.graphWidget.addItem(bargraphItem)
        #Draw the name of BarItem into QComBoBox
        self.cboBarItem.addItem(bargraphItem.name(), bargraphItem)
    #setup tick
    self.autoTick(self.graphWidget, self.xlab)
    #setup label
    self.autolabel(self.bargraphItems)

Hàm ẩn hiển thị lưới processChangeGrid():

def processChangeGrid(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.graphWidget.showGrid(x=True, y=True)
    else:
        self.graphWidget.showGrid(x=False, y=False)

Hàm ẩn hiển thị legend processChangeLegend():

def processChangeLegend(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.legend.show()
    else:
        self.legend.hide()

Hàm thay đổi màu nền của Chart processChangeChartBackground():

def processChangeChartBackground(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        self.graphWidget.setBackground(color.name())
        del dialog

Hàm thay đổi màu của từng Baritem, lưu ý là người sử dụng sẽ chọn Bar item trong QComboBox trước, sau đó nhấn vào button “bar color” để đổi màu:

def processChangeBarColor(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        bargraphItem=self.bargraphItems[self.cboBarItem.currentIndex()]
        bargraphItem.opts["brush"]=color.name()
        bargraphItem._updateColors(bargraphItem.opts)
        del dialog

Hàm autoTick() sẽ thiết lập label cho các thành phố ở bottom Cities:

def autoTick(self,graphWidget, xlab):
    ticks = []
    for i, item in enumerate(xlab):
        ticks.append((i +1+ 2*self.width, item))
    ticks = [ticks]
    ax = graphWidget.getAxis('bottom')
    ax.setTicks(ticks)

Hàm autolabel() sẽ thiết lập label cho các Bar Item:

def autolabel(self,barItems):
    # attach some text labels
    for barItem in barItems:
        xs, heights = barItem.getData()
        for i in range(len(heights)):
            height = heights[i]
            x = xs[i]
            clr = barItem.opts["brush"]
            text = pg.TextItem(str(height), color=clr)
            text.setParentItem(barItem)
            text.setX(x)
            text.setY(height)
            text.setAnchor((QPointF(0.5, 0.75)))

Label cho từng Bar item giúp người sử dụng dễ dàng quan sát và so sánh cũng như đánh giá dữ liệu.

Dưới đây là coding đầy đủ của MainWindowEx.py:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog, QFileDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg
import pandas as pd
import random as nd

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.setupBarGraph()
        self.setupSignal()
    def setupBarGraph(self):
        self.graphWidget = pg.PlotWidget()
        self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
        self.graphWidget.setBackground('w')

        labelStyle = {"color": "green", "font-size": "18px"}
        labelBrandStyle = {"color": "purple", "font-size": "18px"}
        self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
        self.graphWidget.setLabel("bottom","Cities",**labelStyle)
        self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)

        self.graphWidget.showGrid(x=True, y=True)

        self.width = 0.2
        self.bargraphItems = []

        self.legend = self.graphWidget.addLegend()

        self.layoutGraph.addWidget(self.graphWidget)

    def setupSignal(self):
        self.pushButtonPickDataset.clicked.connect(self.processPickDataset)
        self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
        self.chkLegend.stateChanged.connect(self.processChangeLegend)
        self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
        self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)

    def processPickDataset(self):
        filters = "Dataset (*.xlsx);;All files(*)"
        filename, selected_filter = QFileDialog.getOpenFileName(
            self.MainWindow,
            filter=filters,
        )
        self.lineEditDataset.setText(filename)

        self.drawChart(filename)

    def drawChart(self,datasetPath):
        #read Excel file into DataFrame by Pandas
        dataframe = pd.read_excel(datasetPath)
        #Read columns, we omit first column (corner)
        #Hà Nội,Huế,TP.HCM,Cần Thơ,An Giang:
        self.xlab = dataframe.columns.values[1:]
        #create an array from 1 to number of column
        self.positions = np.arange(1, len(self.xlab) + 1)
        #remove existing BarGraph Item
        #(user can choose file many times)
        for barItem in self.bargraphItems:
            self.graphWidget.removeItem(barItem)
        self.bargraphItems = []
        self.cboBarItem.clear()
        i = 0
        #loop to read row data
        for row in range(len(dataframe.values)):
            #Get value in the first column for each row(quarter X)
            name = dataframe.values[row][0]
            #Get an Array data for each row
            revenues = dataframe.values[row][1:].tolist()
            #setup the position for each graph bar item
            x = self.positions + i * self.width
            #random color for each bar item
            brush = (nd.randint(0, 255), nd.randint(0, 255), nd.randint(0, 255))
            #create BarGraphItem:
            bargraphItem = pg.BarGraphItem(x=x, height=revenues, width=self.width, brush=brush, name=name)
            i = i + 1
            #store bargraphItem into bargraphItems
            self.bargraphItems.append(bargraphItem)
            #add the Bar Graph Item into the Chart
            self.graphWidget.addItem(bargraphItem)
            #Draw the name of BarItem into QComBoBox
            self.cboBarItem.addItem(bargraphItem.name(), bargraphItem)
        #setup tick
        self.autoTick(self.graphWidget, self.xlab)
        #setup label
        self.autolabel(self.bargraphItems)
    def processChangeGrid(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.graphWidget.showGrid(x=True, y=True)
        else:
            self.graphWidget.showGrid(x=False, y=False)
    def processChangeLegend(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.legend.show()
        else:
            self.legend.hide()
    def processChangeChartBackground(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.graphWidget.setBackground(color.name())
            del dialog
    def processChangeBarColor(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            bargraphItem=self.bargraphItems[self.cboBarItem.currentIndex()]
            bargraphItem.opts["brush"]=color.name()
            bargraphItem._updateColors(bargraphItem.opts)
            del dialog
    def autoTick(self,graphWidget, xlab):
        ticks = []
        for i, item in enumerate(xlab):
            ticks.append((i +1+ 2*self.width, item))
        ticks = [ticks]
        ax = graphWidget.getAxis('bottom')
        ax.setTicks(ticks)

    def autolabel(self,barItems):
        # attach some text labels
        for barItem in barItems:
            xs, heights = barItem.getData()
            for i in range(len(heights)):
                height = heights[i]
                x = xs[i]
                clr = barItem.opts["brush"]
                text = pg.TextItem(str(height), color=clr)
                text.setParentItem(barItem)
                text.setX(x)
                text.setY(height)
                text.setAnchor((QPointF(0.5, 0.75)))

    def show(self):
        self.MainWindow.show()

Bước 5: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()

Thực thi MyApp.py ta có kết quả, Chạy phần mềm lên:

Ta nhấn vào nút “Pick Dataset” rồi trỏ tới file Excel data mẫu:

Chọn File Excel rồi nhấn Open, Sau khi nhấn Open ta có kết quả:

Chương trình sẽ tự động nạp tên vào QComBoBox như bài trước để ta có thể chọn và đổi màu cho từng bar item:

Ví dụ như ta chọn “Quarter 3” Bar Item rồi chọn Bar Color:

Ta thử chọn màu đỏ rồi bấm OK:

Màu Bar Item cũng đổi như ta mong muốn, tương tự như bài trước.

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với Multiple BarGraph đọc dữ liệu từ EXCEL. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget.

Ôn tập cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Ôn tập cách lập trình để Auto Tick, và Auto Label cho Bar. Nó có ý nghĩa quan trọng để giúp Chart được rõ nghĩa, đọc dễ hiểu hơn.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/89e33vfnoiob6dl/LearnPyQtBarGraphPart7.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách kế thừa BarGraphItem để lắng nghe và xử lý sự kiện click chuột ngay trên từng Baritem

Chúc các bạn thành công

Bài 36: Trực quan hóa dữ liệu – PyQtGraph-Multiple BarGraph-PyQt6 – Part 6

Bài này chúng ta sẽ trực quan hóa dạng Multiple Bar Graph và nó được mở rộng từ bài trước.

Giả sử chúng ta có doanh số trung bình của công ty Lucy ở các chi nhánh Hà Nội, Huế, TP.HCM, Cần Thơ và An Giang theo 4 quý như sau:

Hà NộiHuếTP.HCMCần ThơAn Giang
Quarter 1100200250190220
Quarter 21207015080160
Quarter 380270180200250
Quarter 472230106210180

Sau khi viết các mã lệnh trực quan hóa thì ta có giao diện như dưới đây:

Sự khác biệt lớn của bài này là ở 2 chỗ:

  • Có nhiều Bar Item ở mỗi nhóm trực quan
  • Khi đổi Bar Color thì người dùng có thể lực chọn từng Bar Item để đổi màu, các Bar Item này được hiển thị trong QComboBox.

Chúng ta thực hiện các bước như sau:

Bước 1: Tạo dự án tên “LearnPyQtBarGraphPart6” có cấu trúc như dưới đây:

  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(647, 518)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
        self.label.setSizePolicy(sizePolicy)
        self.label.setMinimumSize(QtCore.QSize(0, 25))
        self.label.setMaximumSize(QtCore.QSize(16777215, 20))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.label.setFont(font)
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.verticalLayout_2.addWidget(self.label)
        self.layoutGraph = QtWidgets.QVBoxLayout()
        self.layoutGraph.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
        self.layoutGraph.setObjectName("layoutGraph")
        self.verticalLayout_2.addLayout(self.layoutGraph)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.chkBackgroundGrid = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkBackgroundGrid.setChecked(True)
        self.chkBackgroundGrid.setObjectName("chkBackgroundGrid")
        self.horizontalLayout.addWidget(self.chkBackgroundGrid)
        self.chkLegend = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkLegend.setChecked(True)
        self.chkLegend.setObjectName("chkLegend")
        self.horizontalLayout.addWidget(self.chkLegend)
        self.pushButtonChangBackground = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangBackground.setObjectName("pushButtonChangBackground")
        self.horizontalLayout.addWidget(self.pushButtonChangBackground)
        self.cboBarItem = QtWidgets.QComboBox(parent=self.centralwidget)
        self.cboBarItem.setObjectName("cboBarItem")
        self.horizontalLayout.addWidget(self.cboBarItem)
        self.pushButtonChangeBarColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeBarColor.setObjectName("pushButtonChangeBarColor")
        self.horizontalLayout.addWidget(self.pushButtonChangeBarColor)
        self.pushButtonClose = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClose.setObjectName("pushButtonClose")
        self.horizontalLayout.addWidget(self.pushButtonClose)
        self.verticalLayout_2.addLayout(self.horizontalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 647, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.pushButtonClose.clicked.connect(MainWindow.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - BarGraph"))
        self.label.setText(_translate("MainWindow", "Lucy Company"))
        self.chkBackgroundGrid.setText(_translate("MainWindow", "Background Grid"))
        self.chkLegend.setText(_translate("MainWindow", "Legend"))
        self.pushButtonChangBackground.setText(_translate("MainWindow", "Chart Background"))
        self.pushButtonChangeBarColor.setText(_translate("MainWindow", "Bar Color"))
        self.pushButtonClose.setText(_translate("MainWindow", "Close"))

Bước 4: Viết mã lệnh cho “MainWindowEx.py”

Khai báo lớp MainWindowEx kết thừa từ Generate Python code ở bước 3, dưới đây là hàm override setupUi để định nghĩa các thành phần cho Bar Chart:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.width = 0.2
        self.xlab = ["Hà Nội", "Huế", "TP.HCM", "Cần Thơ", "An Giang"]
        self.titleLegends=["Quarter 1","Quarter 2","Quarter 3","Quarter 4"]
        self.positions = np.array([1, 2, 3, 4, 5])
        self.revenues = [[100, 200, 250, 190, 220],
                         [120, 70, 150, 80, 160],
                         [80, 270, 180, 200, 250],
                         [72, 230, 106, 210, 180]
                         ]
        self.brushes=["r","g","b","c"]
        self.bargraphItems=[]

        self.setupBarGraph()

        self.autoTick(self.graphWidget,self.xlab)

        self.autolabel(self.bargraphItems)

Mã lệnh ở trên Tui định nghĩa các tiêu đề cho 5 thành phố (Hà Nội, Huế, TP.HCM, Cần Thơ, An Giang) và các doanh thu tương ứng theo các quý.

Đồng thời khai báo mảng các màu brushes (r, g, b, c) cho các Bar Item.

Trong setupUi() có gọi các hàm setupBarGraph(), autoTick(), và autoLabel()

Dưới đây là hàm setupBarGraph để vẽ Multiple Bar Graph:

def setupBarGraph(self):
    self.graphWidget = pg.PlotWidget()
    self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
    self.graphWidget.setBackground('w')

    labelStyle = {"color": "green", "font-size": "18px"}
    labelBrandStyle = {"color": "purple", "font-size": "18px"}
    self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
    self.graphWidget.setLabel("bottom","Cities",**labelStyle)
    self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)
    self.graphWidget.showGrid(x=True, y=True)

    self.legend = self.graphWidget.addLegend()
    self.bargraphItems.clear()
    self.cboBarItem.clear()
    for i in range(len(self.revenues)):
        bargraphItem = pg.BarGraphItem(
            x=self.positions+i*self.width,
            height=self.revenues[i],
            width=self.width,
            brush=self.brushes[i],
            name=self.titleLegends[i])
        self.bargraphItems.append(bargraphItem)
        self.graphWidget.addItem(bargraphItem)
        self.cboBarItem.addItem(bargraphItem.name(),bargraphItem)

    self.layoutGraph.addWidget( self.graphWidget)

    self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
    self.chkLegend.stateChanged.connect(self.processChangeLegend)
    self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
    self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)

Các mã lệnh ở trên đã được giải thích chi tiết ở những bài học trước do đó các bạn cần xem lại. Chỉ có chỗ vòng lặp là Tui dùng để duyệt qua từng phần tử trong ma trận revenues, mỗi lần duyệt sẽ tạo ra một BarGraphItem và tiến hành định dạng cho nó.

Trong vòng lặp ngoài việc vẽ các Bar Item thì nó cũng đưa tên và bar item object vào bên trong QCombox để cho người sử dụng có thể lựa chọn từng Bar item trên giao diện để đổi màu theo mong muốn của họ.

Ở cuối hàm setupUi() cũng gọi các signal như hiển thị/ẩn lưới của chart, ẩn hiện Legend, đổi màu nền chart, đổi bar item color.

Hàm ẩn hiển thị lưới processChangeGrid():

def processChangeGrid(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.graphWidget.showGrid(x=True, y=True)
    else:
        self.graphWidget.showGrid(x=False, y=False)

Hàm ẩn hiển thị legend processChangeLegend():

def processChangeLegend(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.legend.show()
    else:
        self.legend.hide()

Hàm thay đổi màu nền của Chart processChangeChartBackground():

def processChangeChartBackground(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        self.graphWidget.setBackground(color.name())
        del dialog

Hàm thay đổi màu của từng Baritem, lưu ý là người sử dụng sẽ chọn Bar item trong QComboBox trước, sau đó nhấn vào button “bar color” để đổi màu:

def processChangeBarColor(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        bargraphItem=self.bargraphItems[self.cboBarItem.currentIndex()]
        bargraphItem.opts["brush"]=color.name()
        bargraphItem._updateColors(bargraphItem.opts)
        del dialog

Ví dụ chọn Quarter 2 trên giao diện:

Sau đó chọn Bar Color button:

Sau khi nhấn OK ta có màu vàng được thiết lập cho Bar Item Quarter 2:

Hàm autoTick() sẽ thiết lập label cho các thành phố ở bottom Cities:

def autoTick(self,graphWidget, xlab):
    ticks = []
    for i, item in enumerate(xlab):
        ticks.append((i +1+ 2*self.width, item))
    ticks = [ticks]
    ax = graphWidget.getAxis('bottom')
    ax.setTicks(ticks)

Hàm autolabel() sẽ thiết lập label cho các Bar Item:

def autolabel(self,barItems):
    # attach some text labels
    for barItem in barItems:
        xs, heights = barItem.getData()
        for i in range(len(heights)):
            height = heights[i]
            x = xs[i]
            clr = barItem.opts["brush"]
            text = pg.TextItem(str(height), color=clr)
            text.setParentItem(barItem)
            text.setX(x)
            text.setY(height)
            text.setAnchor((QPointF(0.5, 0.75)))

Label cho từng Bar item giúp người sử dụng dễ dàng quan sát và so sánh cũng như đánh giá dữ liệu.

Dưới đây là coding đầy đủ của MainWindowEx.py:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow

        self.width = 0.2
        self.xlab = ["Hà Nội", "Huế", "TP.HCM", "Cần Thơ", "An Giang"]
        self.titleLegends=["Quarter 1","Quarter 2","Quarter 3","Quarter 4"]
        self.positions = np.array([1, 2, 3, 4, 5])
        self.revenues = [[100, 200, 250, 190, 220],
                         [120, 70, 150, 80, 160],
                         [80, 270, 180, 200, 250],
                         [72, 230, 106, 210, 180]
                         ]
        self.brushes=["r","g","b","c"]
        self.bargraphItems=[]

        self.setupBarGraph()

        self.autoTick(self.graphWidget,self.xlab)

        self.autolabel(self.bargraphItems)
    def setupBarGraph(self):
        self.graphWidget = pg.PlotWidget()
        self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
        self.graphWidget.setBackground('w')

        labelStyle = {"color": "green", "font-size": "18px"}
        labelBrandStyle = {"color": "purple", "font-size": "18px"}
        self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
        self.graphWidget.setLabel("bottom","Cities",**labelStyle)
        self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)
        self.graphWidget.showGrid(x=True, y=True)

        self.legend = self.graphWidget.addLegend()
        self.bargraphItems.clear()
        self.cboBarItem.clear()
        for i in range(len(self.revenues)):
            bargraphItem = pg.BarGraphItem(
                x=self.positions+i*self.width,
                height=self.revenues[i],
                width=self.width,
                brush=self.brushes[i],
                name=self.titleLegends[i])
            self.bargraphItems.append(bargraphItem)
            self.graphWidget.addItem(bargraphItem)
            self.cboBarItem.addItem(bargraphItem.name(),bargraphItem)

        self.layoutGraph.addWidget( self.graphWidget)

        self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
        self.chkLegend.stateChanged.connect(self.processChangeLegend)
        self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
        self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)
    def processChangeGrid(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.graphWidget.showGrid(x=True, y=True)
        else:
            self.graphWidget.showGrid(x=False, y=False)
    def processChangeLegend(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.legend.show()
        else:
            self.legend.hide()
    def processChangeChartBackground(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.graphWidget.setBackground(color.name())
            del dialog
    def processChangeBarColor(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            bargraphItem=self.bargraphItems[self.cboBarItem.currentIndex()]
            bargraphItem.opts["brush"]=color.name()
            bargraphItem._updateColors(bargraphItem.opts)
            del dialog
    def autoTick(self,graphWidget, xlab):
        ticks = []
        for i, item in enumerate(xlab):
            ticks.append((i +1+ 2*self.width, item))
        ticks = [ticks]
        ax = graphWidget.getAxis('bottom')
        ax.setTicks(ticks)

    def autolabel(self,barItems):
        # attach some text labels
        for barItem in barItems:
            xs, heights = barItem.getData()
            for i in range(len(heights)):
                height = heights[i]
                x = xs[i]
                clr = barItem.opts["brush"]
                text = pg.TextItem(str(height), color=clr)
                text.setParentItem(barItem)
                text.setX(x)
                text.setY(height)
                text.setAnchor((QPointF(0.5, 0.75)))

    def show(self):
        self.MainWindow.show()

Bước 5: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()

Thực thi MyApp.py ta có kết quả:

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với Multiple BarGraph. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget.

Ôn tập cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Ôn tập cách lập trình để Auto Tick, và Auto Label cho Bar. Nó có ý nghĩa quan trọng để giúp Chart được rõ nghĩa, đọc dễ hiểu hơn.

Đặc biệt trong bài này chúng ta biết cách vẽ Multiple Bar Item lên 1 Chart, nạp được bar item vào QComboBox để người dùng có thể tùy ý chọn màu cho từng Bar item. Đây là bài học ứng dụng thực tế rất cao. Các bạn chú ý làm lại nhiều lần để thuần thục về kỹ thuật, cũng như biết cách áp dụng vào các bài multiple chart tương tự

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/gyv9hgo9ezgro0e/LearnPyQtBarGraphPart6.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách nạp dữ liệu từ Excel vào phần mềm Python và vẽ lên Bar chart. Nó cũng là một bài áp dụng trong thức tế, Các bạn chú ý theo dõi

Chúc các bạn thành công

Bài 35: Trực quan hóa dữ liệu – PyQtGraph-BarGraph-PyQt6 – Part 5

Chúng ta đã biết cách sử dụng BarGraph để trực quan hóa dữ liệu ở các bài học trước, Lập trình xử lý sự kiện được trên BarGraph như ẩn hiện Background Grid, ẩn hiện Legend, thay đổi màu nền của Chart, thay đổi màu nền của Bar bằng QColorDialog. Với các bài minh họa trước thì các bạn đã có thể ứng dụng vào việc trực quan hóa dữ liệu dạng Bar ở một số trường hợp trong thực tế.

Trong bài học này Tui bổ sung thêm 2 kỹ thuật: Viết mã lệnh để tự động gán label cho các Bar Item, và viết mã lệnh để tự động tạo các Tick cho trục X của Chart nhằm làm rõ nghĩa cho các cột này thay vì chỉ các con số.

Đặc biệt Tui sẽ lập trình để 2 chức năng này có thể tự động hiểu được các Bar Chart theo dữ liệu khác nhau, có thể hiển thị chính xác theo Bar Item đơn hay nhóm các Bar Item, và các label có khả năng khớp màu với màu của mỗi Bar Item tương ứng.

  • Ở trên ta thấy Trục X thay vì chỉ hiển thị các con số 1, 2, 3, 4 mà nó được hiển thị bằng chuỗi “Quarter 1”, “Quarter 2”, “Quarter 3”, và “Quarter 4”
  • Đồng thời trên mỗi Bar item ta có các giá trị được hiển thị, ta gọi nó là label hoặc notation. Ví dụ ta thấy cột đầu tiên có giá trị 100, cột thứ 2 có giá trị 200, vân vân.

Rõ ràng việc hiển thị các Tick và Label sẽ giúp Chart được rõ nghĩa và dễ quan sát hơn.

Như vậy bài này sẽ có giao diện giống như bài trước, và các mã lệnh của một số chức năng cũng y chang nên Tui sẽ không giải thích lại các hàm này. Mà Tui chỉ nói tới 2 giải thuật về tự động tick và tự động label thôi.

Dưới đây là chi tiết từng bước thực hiện dự án:

Bước 1: Tạo dự án “LearnPyQtBarGraphPart5” trong Pycharm có cấu trúc như dưới đây:

  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.5.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(566, 436)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
        self.label.setSizePolicy(sizePolicy)
        self.label.setMinimumSize(QtCore.QSize(0, 25))
        self.label.setMaximumSize(QtCore.QSize(16777215, 20))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.label.setFont(font)
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.verticalLayout_2.addWidget(self.label)
        self.layoutGraph = QtWidgets.QVBoxLayout()
        self.layoutGraph.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
        self.layoutGraph.setObjectName("layoutGraph")
        self.verticalLayout_2.addLayout(self.layoutGraph)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.chkBackgroundGrid = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkBackgroundGrid.setChecked(True)
        self.chkBackgroundGrid.setObjectName("chkBackgroundGrid")
        self.horizontalLayout.addWidget(self.chkBackgroundGrid)
        self.chkLegend = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkLegend.setChecked(True)
        self.chkLegend.setObjectName("chkLegend")
        self.horizontalLayout.addWidget(self.chkLegend)
        self.pushButtonChangBackground = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangBackground.setObjectName("pushButtonChangBackground")
        self.horizontalLayout.addWidget(self.pushButtonChangBackground)
        self.pushButtonChangeBarColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeBarColor.setObjectName("pushButtonChangeBarColor")
        self.horizontalLayout.addWidget(self.pushButtonChangeBarColor)
        self.pushButtonClose = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClose.setObjectName("pushButtonClose")
        self.horizontalLayout.addWidget(self.pushButtonClose)
        self.verticalLayout_2.addLayout(self.horizontalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 566, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.pushButtonClose.clicked.connect(MainWindow.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - BarGraph"))
        self.label.setText(_translate("MainWindow", "Lucy Company"))
        self.chkBackgroundGrid.setText(_translate("MainWindow", "Background Grid"))
        self.chkLegend.setText(_translate("MainWindow", "Legend"))
        self.pushButtonChangBackground.setText(_translate("MainWindow", "Chart Background"))
        self.pushButtonChangeBarColor.setText(_translate("MainWindow", "Bar Color"))
        self.pushButtonClose.setText(_translate("MainWindow", "Close"))

Bước 4: Viết mã lệnh cho “MainWindowEx.py”

Chú ý rằng các mã lệnh nó y chang như bài 34, còn bài này Ta chỉ bổ sung thêm 2 giải thuật cho Auto Tick và Auto Label. Do đó Tui sẽ gửi code đầy đủ ở đây, và ở bên dưới Tui sẽ giải thích 2 giải thuật này:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.setupBarGraph()

        self.xlab = ["Quarter 1", "Quarter 2", "Quarter 3", "Quarter 4"]

        self.autoTick(self.graphWidget,self.xlab)

        self.autolabel([self.bargraphItem])

Trong hàm setupUi() ta khai báo và gọi 3 lệnh:

self.xlab = ["Quarter 1", "Quarter 2", "Quarter 3", "Quarter 4"]

Lệnh trên khai báo mảng chuỗi để hiển thị Tick ở bottom của Chart, nó sẽ thay thế cho các giá trị số, việc hiển thị chuỗi như này sẽ giúp cho Chart được rõ nghĩa hơn.

Lệnh:

self.autoTick(self.graphWidget,self.xlab)

Để gán các Tick

def autoTick(self,graphWidget, xlab):
    ticks = []
    for i, item in enumerate(xlab):
        ticks.append((i + 1, item))
    ticks = [ticks]
    ax = graphWidget.getAxis('bottom')
    ax.setTicks(ticks)

Hàm autoTick nhận vào 2 đối số:

-Đối số 1 là graphWidget (đối tượng PlotWidget)

– Đối số 2 là xlab là mảng chuỗi để hiển thị Tick

Vòng lặp mã lệnh bên trong autoTick ta sẽ đưa các chuỗi tick vào mảng ticks.

Lệnh graphWidget.getAxis(‘bottom’) để truy suất trục bottom,

Sau đó ta gọi lệnh setTicks(ticks) lúc này chương trình sẽ gán các Tick lên giao diện

Lệnh self.autolabel([self.bargraphItem]) để gán label cho mỗi Bar Item:

def autolabel(self,barItems):
    # attach some text labels
    for barItem in barItems:
        xs, heights = barItem.getData()
        for i in range(len(heights)):
            height = heights[i]
            x = xs[i]
            clr = barItem.opts["brush"]
            text = pg.TextItem(str(height), color=clr)
            text.setParentItem(barItem)
            text.setX(x)
            text.setY(height)
            text.setAnchor((QPointF(0.5, 0.75)))

Hàm autolabel sẽ nhận vào 1 mảng BarGraphItem, Tui thiết kế đối số là mảng để chương trình có thể tự động hiểu được tùy ý số nhóm và số bar item trong mỗi nhóm.

Hàm barItem.getData() sẽ trả về 2 dữ liệu: xs mảng trục bottom, heights mảng độ cao của Bar Item (mảng giá trị mà ta trực quan hóa). Dựa vào 2 dữ liệu này ta dễ dàng phân bổ các vị trí label phù hợp

Ta dùng lệnh: pg.TextItem() để tạo các TextItem tương ứng với từng nhóm Bar Item, và nó được vẽ chính xác vị trí thông qua hàm setX(), setY().

Lệnh setAnchor() giúp neo label theo tọa độ cho từng Bar Item.

Dưới đây là mã lệnh đầy đủ của “MainWindowEx.py“:

from PyQt6.QtCore import Qt, QPointF
from PyQt6.QtWidgets import QColorDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.setupBarGraph()

        self.xlab = ["Quarter 1", "Quarter 2", "Quarter 3", "Quarter 4"]

        self.autoTick(self.graphWidget,self.xlab)

        self.autolabel([self.bargraphItem])

    def setupBarGraph(self):
        self.graphWidget = pg.PlotWidget()
        self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
        self.graphWidget.setBackground('w')

        labelStyle = {"color": "green", "font-size": "18px"}
        labelBrandStyle = {"color": "purple", "font-size": "18px"}
        self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
        self.graphWidget.setLabel("bottom","Quarter (time)",**labelStyle)
        self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)
        self.graphWidget.showGrid(x=True, y=True)

        width = 0.3

        quarter = np.array([1, 2, 3, 4])
        revenue = [100, 200, 250, 190]

        self.bargraphItem = pg.BarGraphItem(x=quarter, height=revenue, width=width, brush='b', name="ABC Revenue")

        self.legend=self.graphWidget.addLegend()

        self.graphWidget.addItem(self.bargraphItem)
        self.layoutGraph.addWidget( self.graphWidget)

        self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
        self.chkLegend.stateChanged.connect(self.processChangeLegend)
        self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
        self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)
    def processChangeGrid(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.graphWidget.showGrid(x=True, y=True)
        else:
            self.graphWidget.showGrid(x=False, y=False)
    def processChangeLegend(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.legend.show()
        else:
            self.legend.hide()
    def processChangeChartBackground(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.graphWidget.setBackground(color.name())
            del dialog
    def processChangeBarColor(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.bargraphItem.opts["brush"]=color.name()
            self.bargraphItem._updateColors(self.bargraphItem.opts)
            del dialog
    def autoTick(self,graphWidget, xlab):
        ticks = []
        for i, item in enumerate(xlab):
            ticks.append((i + 1, item))
        ticks = [ticks]
        ax = graphWidget.getAxis('bottom')
        ax.setTicks(ticks)

    def autolabel(self,barItems):
        # attach some text labels
        for barItem in barItems:
            xs, heights = barItem.getData()
            for i in range(len(heights)):
                height = heights[i]
                x = xs[i]
                clr = barItem.opts["brush"]
                text = pg.TextItem(str(height), color=clr)
                text.setParentItem(barItem)
                text.setX(x)
                text.setY(height)
                text.setAnchor((QPointF(0.5, 0.75)))

    def show(self):
        self.MainWindow.show()

Bước 5: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()

Chạy “MyApp.py” ta có kết quả như mong muốn:

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với BarGraph. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget. Đặc biệt các bạn biết cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Đồng thời bài này Tui hướng dẫn các bạn cách lập trình để Auto Tick, và Auto Label cho Bar. Nó có ý nghĩa quan trọng để giúp Chart được rõ nghĩa, đọc dễ hiểu hơn.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/gypdzk2ajb61fni/LearnPyQtBarGraphPart5.rar/file

Bài học sau Tui sẽ hướng cách vẽ Multiple Bar Item trên cùng một Chart. Đây là tính năng rất quan trọng và phổ biến để trực quan hóa dữ liệu trong việc so sánh kết quả.

Các bạn chú ý theo dõi

Chúc các bạn thành công

Bài 34: Trực quan hóa dữ liệu – PyQtGraph-BarGraph-PyQt6 – Part 4

Bài học này chúng ta tiếp tục làm việc với BarGraph, Tui hướng dẫn các bạn cách bổ sung thiết kế giao diện trong Qt Designer để cung cấp các chức năng tương tác trong quá trình trực quan hóa dữ liệu với BarGraph:

  • Chức năng ẩn và hiện Background Grid
  • Chức năng ẩn và hiện Legend
  • Chức năng đổi màu nền của Chart dùng QColorDialog
  • Chức năng đổi màu nền của Bar dùng QColorDialog

Các chức năng này đều được thiết kế giao diện và cung cấp sự kiện tương tác người dùng. Dựa vào bài này mà các bạn có thể tùy chỉnh tương tác biểu đồ trong các trường hợp thực tế khác nhau.

Ngoài ra các bạn cũng được ôn tập lại các kiến thức liên quan tới QCheckBox, xử lý các sự kiện trên QCheckBox. Cũng như xử lý sự kiện trên QPushButton.

Bạn quan sát giao diện ở trên có 2 QCheckBox, 3 QPushButton được bổ sung vào giao diện để cho người dùng tương tác. Bài này mở rộng từ bài trước

Dưới đây là chi tiết từng bước thực hiện dự án:

Bước 1: Tạo dự án “LearnPyQtBarGraphPart4” trong Pycharm có cấu trúc như dưới đây:

  • “MainWindow.ui” là giao diện thiết kế tương tác người dùng bằng Qt Designer
  • “MainWindow.py” là Generate Python code cho giao diện “MainWindow.ui”
  • “MainWindowEx.py” là file mã lệnh kế thừa từ Generate Python Code để xử lý: Nạp giao diện, hiển thị chart, gán sự kiện và không bị lệ thuộc vào giao diện bị thay đổi sau này khi Generate lại code
  • “MyApp.py” là file mã lệnh thực thi chương trình.

Bước 2: Thiết kế giao diện “MainWindow.ui” và đặt tên cho Widget/layout như hình dưới đây:

Bạn lần lượt kéo thả các Widget vào giao diện như trên, lưu ý việc lựa chọn các Layout cho phù hợp.

Bước 3: Generate Python Code cho “MainWindow.ui”, lúc này mã lệnh “MainWindow.py” tự động được tạo ra:

# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.5.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(566, 436)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.label = QtWidgets.QLabel(parent=self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
        self.label.setSizePolicy(sizePolicy)
        self.label.setMinimumSize(QtCore.QSize(0, 25))
        self.label.setMaximumSize(QtCore.QSize(16777215, 20))
        font = QtGui.QFont()
        font.setPointSize(15)
        self.label.setFont(font)
        self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        self.label.setObjectName("label")
        self.verticalLayout_2.addWidget(self.label)
        self.layoutGraph = QtWidgets.QVBoxLayout()
        self.layoutGraph.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint)
        self.layoutGraph.setObjectName("layoutGraph")
        self.verticalLayout_2.addLayout(self.layoutGraph)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.chkBackgroundGrid = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkBackgroundGrid.setChecked(True)
        self.chkBackgroundGrid.setObjectName("chkBackgroundGrid")
        self.horizontalLayout.addWidget(self.chkBackgroundGrid)
        self.chkLegend = QtWidgets.QCheckBox(parent=self.centralwidget)
        self.chkLegend.setChecked(True)
        self.chkLegend.setObjectName("chkLegend")
        self.horizontalLayout.addWidget(self.chkLegend)
        self.pushButtonChangBackground = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangBackground.setObjectName("pushButtonChangBackground")
        self.horizontalLayout.addWidget(self.pushButtonChangBackground)
        self.pushButtonChangeBarColor = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonChangeBarColor.setObjectName("pushButtonChangeBarColor")
        self.horizontalLayout.addWidget(self.pushButtonChangeBarColor)
        self.pushButtonClose = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButtonClose.setObjectName("pushButtonClose")
        self.horizontalLayout.addWidget(self.pushButtonClose)
        self.verticalLayout_2.addLayout(self.horizontalLayout)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 566, 22))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.pushButtonClose.clicked.connect(MainWindow.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - BarGraph"))
        self.label.setText(_translate("MainWindow", "Lucy Company"))
        self.chkBackgroundGrid.setText(_translate("MainWindow", "Background Grid"))
        self.chkLegend.setText(_translate("MainWindow", "Legend"))
        self.pushButtonChangBackground.setText(_translate("MainWindow", "Chart Background"))
        self.pushButtonChangeBarColor.setText(_translate("MainWindow", "Bar Color"))
        self.pushButtonClose.setText(_translate("MainWindow", "Close"))

Bước 4: Viết mã lệnh cho “MainWindowEx.py”

Lớp MainWindowEx kế thừa lớp Ui_MainWindow (lớp được Generate Python code ở bước trước)

Tui định nghĩa 1 constructor __init__() gọi lại constructor ở lớp cha, tạm thời trong bài tập này chưa xử lý gì khác.

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QColorDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.setupBarGraph()

Hàm setupUi() được override để nạp giao diện, lưu lại biến MainWindow để sử dụng cho quá trình xử lý trong tương lai. Đồng thời nó cũng gọi hàm setupBarGraph() để hiển thị Chart, Hàm này ta viết như sau:

def setupBarGraph(self):
    self.graphWidget = pg.PlotWidget()
    self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
    self.graphWidget.setBackground('w')

    labelStyle = {"color": "green", "font-size": "18px"}
    labelBrandStyle = {"color": "purple", "font-size": "18px"}
    self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
    self.graphWidget.setLabel("bottom","Quarter (time)",**labelStyle)
    self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)
    self.graphWidget.showGrid(x=True, y=True)

    width = 0.3

    quarter = np.array([1, 2, 3, 4])
    revenue = [100, 200, 250, 190]

    self.bargraphItem = pg.BarGraphItem(x=quarter, height=revenue, width=width, brush='b', name="ABC Revenue")

    self.legend=self.graphWidget.addLegend()

    self.graphWidget.addItem(self.bargraphItem)
    self.layoutGraph.addWidget( self.graphWidget)

    self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
    self.chkLegend.stateChanged.connect(self.processChangeLegend)
    self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
    self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)
  • Tất cả các kỹ thuật liên quan tới PlotWidget và cách xử lý BarGraphItem Tui đã trình bày chi tiết và cụ thể ở các bài trước rồi, nên bài này Tui không nhắc lại. Mà chúng ta chỉ quan tâm xử lý Signal liên quan tới các QCheckBox và QPushButton để cung cấp chức năng tương tác Chart cho người dùng.
  • Ta xem Signal statechanged của QCheckBox background grid:
self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)

Slot(hàm) chi tiết xử lý khi người dùng Checked vào QCheckBox background grid được khai báo trong hàm processChangeGrid():

def processChangeGrid(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.graphWidget.showGrid(x=True, y=True)
    else:
        self.graphWidget.showGrid(x=False, y=False)

Khi chạy chức năng này thì ta thấy như sau: Nếu người sử dụng Unchecked QCheckBox background Grid này thì các lưới sẽ bị biến mất, còn Checked thì các lưới sẽ được hiển thị trở lại:

  • Tiếp theo Ta xem Signal statechanged của QCheckBox Legend:
self.chkLegend.stateChanged.connect(self.processChangeLegend)

Slot(hàm) chi tiết xử lý khi người dùng Checked vào QCheckBox legend được khai báo trong hàm processChangeLegend():

def processChangeLegend(self,value):
    state=Qt.CheckState(value)
    if state==Qt.CheckState.Checked:
        self.legend.show()
    else:
        self.legend.hide()

Khi chạy chức năng này thì ta thấy như sau: Nếu người sử dụng Unchecked QCheckBox Legend này thì Legend bị biến mất, còn Checked thì Legend sẽ được hiển thị trở lại:

  • Tiếp theo ta qua Signal clicked cho QPushButton thay đổi màu nền của Chart:
self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)

Slot(hàm) chi tiết xử lý khi người dùng nhấn vào QPushButton Chart Background được khai báo trong hàm processChangeChartBackground():

def processChangeChartBackground(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        self.graphWidget.setBackground(color.name())
        del dialog

Ta thấy, hàm trên Tui dùng QColorDialog() để hiển thị màn hình chọn màu, khi người dùng lựa chọn màu và nhần OK thì màu sẽ được lấy từ hàm currentColor(), sau đó ta truy suất tên của màu này để đổi màu cho Chart thông qua hàm: setBackground(color.name()).

Ví dụ người dùng chọn màu hồng rồi nhấn OK. Lúc này nền màu của Chart sẽ đổi thành màu hồng này:

  • Tiếp theo ta qua Signal clicked cho QPushButton thay đổi màu của Bar:
self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)

Slot(hàm) chi tiết xử lý khi người dùng nhấn vào QPushButton Chart Background được khai báo trong hàm processChangeBarColor():

def processChangeBarColor(self):
    dialog = QColorDialog()
    if dialog.exec():
        color = dialog.currentColor()
        self.bargraphItem.opts["brush"]=color.name()
        self.bargraphItem._updateColors(self.bargraphItem.opts)
        del dialog

Mã lệnh ở trên Tui cũng sử dụng QColorDialog để cho người dùng lựa chọn màu, sau khi lựa chọn màu thì tên màu sẽ được lưu với mảng opts[“brush”] của BarGraphItem.

sau đó nó sẽ được cập nhật màu thông qua hàm:

_updateColors(self.bargraphItem.opts) của BarGraphItem

Ví dụ người dùng chọn màu vàng rồi nhấn OK:

Dưới đây là mã lệnh đầy đủ của “MainWindowEx.py“:

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QColorDialog

from MainWindow import Ui_MainWindow
import numpy as np
import pyqtgraph as pg

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        super().__init__()
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.setupBarGraph()
    def setupBarGraph(self):
        self.graphWidget = pg.PlotWidget()
        self.graphWidget.setTitle("Revenue Report", color="r", size="15pt", bold=True, italic=True)
        self.graphWidget.setBackground('w')

        labelStyle = {"color": "green", "font-size": "18px"}
        labelBrandStyle = {"color": "purple", "font-size": "18px"}
        self.graphWidget.setLabel("left","Revenue (VNĐ)",**labelStyle)
        self.graphWidget.setLabel("bottom","Quarter (time)",**labelStyle)
        self.graphWidget.setLabel("right", "tranduythanh.com", **labelBrandStyle)
        self.graphWidget.showGrid(x=True, y=True)

        width = 0.3

        quarter = np.array([1, 2, 3, 4])
        revenue = [100, 200, 250, 190]

        self.bargraphItem = pg.BarGraphItem(x=quarter, height=revenue, width=width, brush='b', name="ABC Revenue")

        self.legend=self.graphWidget.addLegend()

        self.graphWidget.addItem(self.bargraphItem)
        self.layoutGraph.addWidget( self.graphWidget)

        self.chkBackgroundGrid.stateChanged.connect(self.processChangeGrid)
        self.chkLegend.stateChanged.connect(self.processChangeLegend)
        self.pushButtonChangBackground.clicked.connect(self.processChangeChartBackground)
        self.pushButtonChangeBarColor.clicked.connect(self.processChangeBarColor)
    def processChangeGrid(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.graphWidget.showGrid(x=True, y=True)
        else:
            self.graphWidget.showGrid(x=False, y=False)
    def processChangeLegend(self,value):
        state=Qt.CheckState(value)
        if state==Qt.CheckState.Checked:
            self.legend.show()
        else:
            self.legend.hide()
    def processChangeChartBackground(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.graphWidget.setBackground(color.name())
            del dialog
    def processChangeBarColor(self):
        dialog = QColorDialog()
        if dialog.exec():
            color = dialog.currentColor()
            self.bargraphItem.opts["brush"]=color.name()
            self.bargraphItem._updateColors(self.bargraphItem.opts)
            del dialog
    def show(self):
        self.MainWindow.show()

Bước 5: Viết mã lệnh “MyApp.py” để thực thi chương trình:

from PyQt6.QtWidgets import QApplication, QMainWindow

from MainWindowEx import MainWindowEx

app=QApplication([])
myWindow=MainWindowEx()
myWindow.setupUi(QMainWindow())
myWindow.show()
app.exec()

Chạy “MyApp.py” ta có kết quả như mong muốn, các biểu đồ cột, các nhãn của các cột, tiêu đề chart, các QCheckBox và QPushButton được hiển thị để người dùng tương tác:

Như vậy tới đây Tui đã trình bày xong cách lập trình xử lý trực quan hóa dữ liệu với BarGraph. Ôn tập lại cách khai báo các mảng dữ liệu tương ứng với các trục, Ôn tập cách gọi các hàm liên quan tới BarGraphItem để hiển thị Chart. Cũng như ôn tập lại được toàn bộ kiến thức liên quan tới PlotWidget. Đặc biệt các bạn biết cách tương tác Chart thông qua các QCheckBox và QPushButton để tương tác lưới của chart, tương tác legend, đổi màu chart và màu bar.

Soure code của bài này các bạn tải ở đây:

https://www.mediafire.com/file/mrb3yyoajnirgfr/LearnPyQtBarGraphPart4.rar/file

Bài học sau Tui sẽ hướng dẫn các bạn cách lập trình để hiển thị ticks và cách tự động gán notation cho từng Bar, các bạn chú ý theo dõi

Chúc các bạn thành công