Tính mỗi năm viết một bài blog thôi, nhưng mà mới đây mình có gặp một quả bug thế giới quá, sau một hồi mày mò và tìm hiểu, mình quyết định viết một cái write up ngắn gọn để chửi ngôn ngữ kiếm tiền cho mình - JavaScript.
Bài toán
Chả là hôm nọ mình research một vài vấn đề liên quan tới mấy cái feature sắp tới, bài toán là: “Làm sao để đánh giá chính xác một đối tượng có được rating cao hay không chỉ dựa vào việc người dùng đánh giá tích cực/tiêu cực về đối tượng đó”?
Bằng vào kỹ năng Google đỉnh cao của mình, mình tìm được bài viết How Not To Sort By Average Rating của bác Evan Miller. Bài này có chỉ ra về việc một số hệ thống đánh giá thời bấy giờ có độ đúng đắn khá thấp nếu chúng ta chỉ suy nghĩ theo cách đơn giản.
Có rất nhiều hệ thống đang sai lầm trong việc làm sao để listing được một cách chính xác nhất về điểm đánh giá đối tượng. Các bạn có thể đọc trong bài, cũng khá dễ hiểu. Tóm lại thì cách khả thi và có độ chính xác tương đối có thể chấp nhận là sử dụng công thức Wilson Lower Bound của nhà toán học Edwin Bidwell Wilson.
Vì mình cần làm một ví dụ nho nhỏ có làm một cái PoC nho nhỏ, convert nhanh thuật toán Ruby trong bài sang JavaScript, trông nó sẽ như thế này:
var pnormaldist = function(qn) {
var b = [1.570796288, 0.03706987906, -0.8364353589e-3,
-0.2250947176e-3, 0.6841218299e-5, 0.5824238515e-5,
-0.104527497e-5, 0.8360937017e-7, -0.3231081277e-8,
0.3657763036e-10, 0.6936233982e-12
],= qn,
w1 = -Math.log(4.0 * w1 * (1.0 - w1)),
w3 = 1;
i
if (qn < 0.0 || qn > 1.0) {
return 0.0;
}if (qn === 0.5) {
return 0.0;
}if (qn > 0.5) {
= 1.0 - w1;
w1
}
= b[0];
w1 for (i; i < 11; i++) {
+= b[i] * Math.pow(w3, i);
w1
}
if (qn > 0.5) {
return Math.sqrt(w1 * w3);
}
return -Math.sqrt(w1 * w3);
};
function ci_lower_bound(pos, n, confidence = 0.95) {
if (n === 0) {
return 0;
}var z = pnormaldist(1 - (1 - confidence) / 2);
var phat = 1.0 * pos / n;
return (phat + z * z / (2 * n) - z * Math.sqrt((phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n);
}
Vậy là bước một đã xong, tiếp theo, mình chuẩn bị thêm một ít dataset example để test thử:
const data = [{"up":15,"down":0},{"up":70,"down":2},{"up":14,"down":2},{"up":4,"down":2},{"up":2,"down":15},{"up":5,"down":2},{"up":8,"down":6},{"up":37,"down":5},{"up":21,"down":23},{"up":52,"down":8},{"up":28,"down":12},{"up":147,"down":2},{"up":61,"down":1},{"up":30,"down":1},{"up":23,"down":5},{"up":40,"down":1},{"up":37,"down":2},{"up":61,"down":6},{"up":54,"down":2},{"up":18,"down":0},{"up":12,"down":2},{"up":68,"down":2}]
Và tiếp theo dĩ nhiên là mình sẽ thử nghiệm với data này:
const result = data
map((record) => ({
....record,
: record.up - record.down,
wrong_1: Number((record.up / (record.up + record.down)).toFixed(5)),
wrong_2: Number(ci_lower_bound(record.up, record.up + record.down).toFixed(5))
score
}))sort((a, b) => b.score > a.score);
.
console.table(result)
Cuối cùng mình cần chạy thử, và để hoàn thành việc thử nghiệm nhanh chóng nhất, mình đã chạy trực tiếp bằng console trên browser cho lẹ, và browser mặc định của mình là Firefox:
Tới đây, sau khi nhìn kết quả mình đã tấm tắc khen hay.
Nhưng…
Cho tới khi mình thử lại với cùng đoạn code trên ở trên NodeJS thì…
”Quát đờ heo”, đó là những gì mình đã thốt lên sau khi nhìn những gì console nó in ra. Hai kết quả vẫn trả về đúng, tuy nhiên, thứ tự lại khác nhau rất nhiều.
Mình còn sợ mình copy thiếu hoặc làm sai bước nào, thế là mình phải ngồi khoảng 30p để tìm đủ cách xem thử 2 đoạn code có khác nhau chỗ nào không. Và tất nhiên là không rồi
Sau một hồi nhìn tới nhìn lui, mình nghĩ kết quả cho từng pair data vẫn đúng, nhưng thứ tự sai, thì tức là chỉ có thể phép sort có vấn đề.
Sorting in JS
Sau một hồi vật lộn với debugging thì mình đã tìm ra chỗ gây lỗi, đó là chỗ b.score > a.score
, cách fix tương đối đơn giản, mình sửa dấu greater than >
thành minus -
là xong:
- .sort((a, b) => b.score > a.score);
+ .sort((a, b) => b.score - a.score);
Tại sao ư? Lý do là đây:
Theo Properties of the Array Prototype Object trong ECMAScript specification1 mình tìm được:
This method sorts the elements of this array. The sort must be stable (that is, elements that compare equal must remain in their original order). If
comparefn
is not undefined, it should be a function that accepts two argumentsx
andy
and returns a negative Number ifx
<y
, a positive Number ifx
>y
, or a zero otherwise.
Tại sao nhỉ? Lúc mình debug, mình phát hiện ra một thứ nho nhỏ khá thú vị:
Hai argument ở hai engine này có thứ tự khác nhau, lý do tại sao thì mình cũng chả biết, nhưng để đoán thì có thể là do mỗi browser đều có một spec riêng, mặc dù cuối cùng vẫn sẽ follow theo một standard cuối cùng. Và có thể nó được implement từ phía engine của browser, là depend của một số chỗ khác nên rất khó để thay đổi thành một kiểu thống nhất.
Phán đoán trên dựa vào việc khi mình lướt qua spec:
If comparefn is not undefined, it should be a function that accepts two arguments x and y (…)
Đúng vậy, nó chỉ nhắc tới việc hai agrument x
và y
chứ không hề nói về thứ tự.
Vậy mà linter nó không báo lỗi kỳ ghê, đúng là “expect” chứ không phải “must”.
Maybe you don’t know:
- Engine JS của Firefox là SpiderMonkey, khác với đa số các runtime khác như Chromium, NodeJS là V8.
- Hàm sort mặc định của Firefox sử dụng Merge Sort2
- Hàm sort mặc định của V8 là Tim Sort3
Kết
Mặc dù biết JavaScript sucks nhưng mình không ngờ lâu lâu vẫn vấp phải mấy cái lỗi củ chuối như này Thôi dù sao nó cũng đang nuôi mình nên chửi thì chửi chứ vẫn nhắm mắt cho qua thôi. Nếu bạn còn đọc tới đây, cảm ơn bạn đã dành thời gian ra ngồi đọc cái bài blog chửi rủa bug write up này của mình.