update
This commit is contained in:
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/anova/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,711 +0,0 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Hướng dẫn One Way ANOVA</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; max-width: 800px; margin: auto; }
|
||||
h1 { color: #0074D9; }
|
||||
hr { margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3><strong>Hướng dẫn tính toán cỡ mẫu cho kiểm định ANOVA (Phân tích phương
|
||||
sai)</strong></h3>
|
||||
<hr>
|
||||
<h3><strong>1. Kiểm định ANOVA là gì?</strong></h3>
|
||||
<p>Kiểm định ANOVA (Analysis of Variance) là một phương pháp thống kê dùng để
|
||||
kiểm tra xem có sự khác biệt có ý nghĩa thống kê giữa trung bình của nhiều
|
||||
nhóm độc lập hay không.</p>
|
||||
<hr>
|
||||
<h4><strong>Phạm vi ứng dụng:</strong></h4>
|
||||
<p>Kiểm định ANOVA được sử dụng rộng rãi trong các nghiên cứu y tế, khoa học xã
|
||||
hội và giáo dục, đặc biệt khi cần so sánh trung bình của hơn 2 nhóm độc
|
||||
lập.<br>Ví dụ:</p>
|
||||
<ul>
|
||||
<li>So sánh trung bình cân nặng giữa 3 nhóm trẻ em được bổ sung dinh dưỡng ở
|
||||
các mức độ khác nhau.</li>
|
||||
<li>So sánh điểm trung bình của sinh viên giữa 4 phương pháp giảng dạy.</li>
|
||||
<li>So sánh thời gian sống sót trung bình giữa 3 loại thuốc.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3><strong>2. Công thức tính cỡ mẫu cho kiểm định ANOVA</strong></h3>
|
||||
<p>Công thức tổng quát để tính cỡ mẫu cho kiểm định ANOVA dựa trên kích thước
|
||||
hiệu ứng chuẩn hóa (</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">f</span>), mức ý nghĩa (</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">\alpha</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">α</span>), và độ mạnh kiểm định (</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">1-\beta</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">1−β</span>):</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>n</mi>
|
||||
<mo>=</mo>
|
||||
<mfrac>
|
||||
<mrow>
|
||||
<mo stretchy="false">(</mo>
|
||||
<mi>η</mi>
|
||||
<mo>+</mo>
|
||||
<mn>1</mn>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mo stretchy="false">(</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<mo>+</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<msup>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mrow>
|
||||
<msup>
|
||||
<mi>f</mi>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mfrac>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n =
|
||||
\frac{{(\eta + 1)(Z_{1-\alpha} + Z_{1-\beta})^2}}{{f^2}}</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">n=f2(η+1)(Z1−α+Z1−β)2</span></p>
|
||||
<p><strong>Trong đó:</strong></p>
|
||||
<ul>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>n</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">n</span>: Số mẫu cần thiết cho mỗi nhóm.
|
||||
</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>η</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">\eta</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">η</span>: Bậc tự do giữa các nhóm (<math
|
||||
style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>η</mi>
|
||||
<mo>=</mo>
|
||||
<mi>k</mi>
|
||||
<mo>−</mo>
|
||||
<mn>1</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">\eta = k - 1</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">η=k−1</span>, với<math
|
||||
style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>k</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">k</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">k</span> là số nhóm).</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f</span>: Kích thước hiệu ứng chuẩn hóa:
|
||||
<ul>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
<mo>=</mo>
|
||||
<mn>0.1</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f = 0.1</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f=0.1</span>: Hiệu ứng nhỏ.</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
<mo>=</mo>
|
||||
<mn>0.25</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f = 0.25</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f=0.25</span>: Hiệu ứng trung bình.</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
<mo>=</mo>
|
||||
<mn>0.4</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f = 0.4</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f=0.4</span>: Hiệu ứng lớn.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">Z_{1-\alpha}</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">Z1−α</span>: Điểm tới hạn từ phân phối
|
||||
chuẩn theo mức ý nghĩa<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">\alpha</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">α</span>.</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">Z_{1-\beta}</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">Z1−β</span>: Điểm tới hạn từ phân phối
|
||||
chuẩn tương ứng với độ mạnh kiểm định.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3><strong>3. Đặc biệt khi chỉ có 2 nhóm</strong></h3>
|
||||
<p>Khi số nhóm</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>k</mi>
|
||||
<mo>=</mo>
|
||||
<mn>2</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">k = 2</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">k=2</span>, kiểm định ANOVA trở thành <strong>kiểm
|
||||
định t-test hai mẫu độc lập</strong>.<br>Trong trường hợp này, công thức
|
||||
trên được đơn giản hóa thành:</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>n</mi>
|
||||
<mo>=</mo>
|
||||
<mfrac>
|
||||
<mrow>
|
||||
<mn>2</mn>
|
||||
<mo stretchy="false">(</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<mo>+</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<msup>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mrow>
|
||||
<msup>
|
||||
<mi>f</mi>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mfrac>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n =
|
||||
\frac{{2(Z_{1-\alpha} + Z_{1-\beta})^2}}{{f^2}}</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">n=f22(Z1−α+Z1−β)2</span></p>
|
||||
<p><strong>Nhận xét:</strong></p>
|
||||
<ul>
|
||||
<li>Nếu nghiên cứu của bạn luôn chỉ so sánh 2 nhóm, bạn nên sử dụng
|
||||
<strong>kiểm định t-test hai mẫu độc lập</strong> để đơn giản hóa.</li>
|
||||
<li>Tuy nhiên, nếu có khả năng mở rộng số lượng nhóm (nhiều hơn 2), kiểm định
|
||||
ANOVA sẽ là phương pháp tổng quát hơn.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3><strong>4. Hướng dẫn tính toán</strong></h3>
|
||||
<h4><strong>Cách tính cỡ mẫu</strong></h4>
|
||||
<p>Để tính cỡ mẫu cho kiểm định ANOVA, bạn cần cung cấp:</p>
|
||||
<ol>
|
||||
<li><strong>Kích thước hiệu ứng (</strong><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f</annotation>
|
||||
</semantics>
|
||||
</math><strong><span aria-hidden="true">f</span>)</strong>:
|
||||
<ul>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
<mo>=</mo>
|
||||
<mn>0.1</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f = 0.1</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f=0.1</span>: Hiệu ứng nhỏ.</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
<mo>=</mo>
|
||||
<mn>0.25</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f = 0.25</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f=0.25</span>: Hiệu ứng trung bình.</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
<mo>=</mo>
|
||||
<mn>0.4</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f = 0.4</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f=0.4</span>: Hiệu ứng lớn.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Mức ý nghĩa (</strong><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">\alpha</annotation>
|
||||
</semantics>
|
||||
</math><strong><span aria-hidden="true">α</span>)</strong>: Thường là 0.05.
|
||||
</li>
|
||||
<li><strong>Độ mạnh kiểm định (</strong><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">1-\beta</annotation>
|
||||
</semantics>
|
||||
</math><strong><span aria-hidden="true">1−β</span>)</strong>: Thường là 0.8
|
||||
hoặc 0.9.</li>
|
||||
<li><strong>Số nhóm (</strong><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>k</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">k</annotation>
|
||||
</semantics>
|
||||
</math><strong><span aria-hidden="true">k</span>)</strong>: Nhập số nhóm cần
|
||||
so sánh.</li>
|
||||
</ol>
|
||||
<h4><strong>Tổng cỡ mẫu</strong></h4>
|
||||
<p>Tổng cỡ mẫu cho toàn bộ nghiên cứu được tính bằng:</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<msub>
|
||||
<mi>n</mi>
|
||||
<mrow>
|
||||
<mi>t</mi>
|
||||
<mi>o</mi>
|
||||
<mi>t</mi>
|
||||
<mi>a</mi>
|
||||
<mi>l</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<mo>=</mo>
|
||||
<mi>n</mi>
|
||||
<mo>×</mo>
|
||||
<mi>k</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n_{total} = n \times k</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">ntotal=n×k</span></p>
|
||||
<p>Với</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>n</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">n</span> là số mẫu cần thiết cho mỗi nhóm, và</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>k</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">k</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
<p><span aria-hidden="true">k</span> là số nhóm.</p>
|
||||
<hr>
|
||||
<h3><strong>5. Ví dụ minh họa</strong></h3>
|
||||
<h4><strong>Bài toán:</strong></h4>
|
||||
<p>Bạn muốn kiểm tra sự khác biệt trung bình cân nặng giữa 3 nhóm trẻ em được bổ
|
||||
sung dinh dưỡng ở mức độ khác nhau với:</p>
|
||||
<ul>
|
||||
<li>Kích thước hiệu ứng (<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>f</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">f</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">f</span>) = 0.25 (hiệu ứng trung bình).</li>
|
||||
<li>Mức ý nghĩa (<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">\alpha</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">α</span>) = 0.05.</li>
|
||||
<li>Độ mạnh kiểm định (<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">1-\beta</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">1−β</span>) = 0.8.</li>
|
||||
<li>Số nhóm (<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>k</mi>
|
||||
<mo>=</mo>
|
||||
<mn>3</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">k = 3</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">k=3</span>).</li>
|
||||
</ul>
|
||||
<h4><strong>Áp dụng công thức:</strong></h4>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Bậc tự do giữa các nhóm:</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>η</mi>
|
||||
<mo>=</mo>
|
||||
<mi>k</mi>
|
||||
<mo>−</mo>
|
||||
<mn>1</mn>
|
||||
<mo>=</mo>
|
||||
<mn>3</mn>
|
||||
<mo>−</mo>
|
||||
<mn>1</mn>
|
||||
<mo>=</mo>
|
||||
<mn>2</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">\eta = k - 1 = 3 - 1 = 2
|
||||
</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">η=k−1=3−1=2</span>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tra bảng phân phối chuẩn:</p>
|
||||
<ul>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<mo>=</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mn>0.95</mn>
|
||||
</msub>
|
||||
<mo>=</mo>
|
||||
<mn>1.96</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">Z_{1-\alpha} = Z_{0.95} =
|
||||
1.96</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">Z1−α=Z0.95=1.96</span>.</li>
|
||||
<li><math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<mo>=</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mn>0.8</mn>
|
||||
</msub>
|
||||
<mo>=</mo>
|
||||
<mn>0.84</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">Z_{1-\beta} = Z_{0.8} =
|
||||
0.84</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">Z1−β=Z0.8=0.84</span>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tính cỡ mẫu cho mỗi nhóm:</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>n</mi>
|
||||
<mo>=</mo>
|
||||
<mfrac>
|
||||
<mrow>
|
||||
<mo stretchy="false">(</mo>
|
||||
<mi>η</mi>
|
||||
<mo>+</mo>
|
||||
<mn>1</mn>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mo stretchy="false">(</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>α</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<mo>+</mo>
|
||||
<msub>
|
||||
<mi>Z</mi>
|
||||
<mrow>
|
||||
<mn>1</mn>
|
||||
<mo>−</mo>
|
||||
<mi>β</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<msup>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mrow>
|
||||
<msup>
|
||||
<mi>f</mi>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mfrac>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n =
|
||||
\frac{{(\eta + 1)(Z_{1-\alpha} + Z_{1-\beta})^2}}{{f^2}}</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">n=f2(η+1)(Z1−α+Z1−β)2</span><math
|
||||
style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>n</mi>
|
||||
<mo>=</mo>
|
||||
<mfrac>
|
||||
<mrow>
|
||||
<mo stretchy="false">(</mo>
|
||||
<mn>2</mn>
|
||||
<mo>+</mo>
|
||||
<mn>1</mn>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mo stretchy="false">(</mo>
|
||||
<mn>1.96</mn>
|
||||
<mo>+</mo>
|
||||
<mn>0.84</mn>
|
||||
<msup>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mrow>
|
||||
<mrow>
|
||||
<mn>0.2</mn>
|
||||
<msup>
|
||||
<mn>5</mn>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
</mrow>
|
||||
</mfrac>
|
||||
<mo>=</mo>
|
||||
<mfrac>
|
||||
<mrow>
|
||||
<mn>3</mn>
|
||||
<mo>×</mo>
|
||||
<mn>7.84</mn>
|
||||
</mrow>
|
||||
<mn>0.0625</mn>
|
||||
</mfrac>
|
||||
<mo>=</mo>
|
||||
<mfrac>
|
||||
<mn>23.52</mn>
|
||||
<mn>0.0625</mn>
|
||||
</mfrac>
|
||||
<mo>=</mo>
|
||||
<mn>376.32</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n =
|
||||
\frac{{(2 + 1)(1.96 + 0.84)^2}}{{0.25^2}} =
|
||||
\frac{{3 \times 7.84}}{{0.0625}} = \frac{{23.52}}{{0.0625}} = 376.32
|
||||
</annotation>
|
||||
</semantics>
|
||||
</math><span
|
||||
aria-hidden="true">n=0.252(2+1)(1.96+0.84)2=0.06253×7.84=0.062523.52=376.32</span>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tổng cỡ mẫu:</p>
|
||||
<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<msub>
|
||||
<mi>n</mi>
|
||||
<mrow>
|
||||
<mi>t</mi>
|
||||
<mi>o</mi>
|
||||
<mi>t</mi>
|
||||
<mi>a</mi>
|
||||
<mi>l</mi>
|
||||
</mrow>
|
||||
</msub>
|
||||
<mo>=</mo>
|
||||
<mi>n</mi>
|
||||
<mo>×</mo>
|
||||
<mi>k</mi>
|
||||
<mo>=</mo>
|
||||
<mn>376.32</mn>
|
||||
<mo>×</mo>
|
||||
<mn>3</mn>
|
||||
<mo>=</mo>
|
||||
<mn>1129</mn>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n_{total} = n \times k = 376.32
|
||||
\times 3 = 1129</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">ntotal=n×k=376.32×3=1129</span>
|
||||
</li>
|
||||
</ol>
|
||||
<h4><strong>Kết luận:</strong></h4>
|
||||
<p>Cần ít nhất <strong>376 mẫu cho mỗi nhóm</strong> và tổng cộng <strong>1129
|
||||
mẫu</strong> cho cả nghiên cứu.</p>
|
||||
<hr>
|
||||
<h3><strong>6. Công cụ tính toán bằng R</strong></h3>
|
||||
<p>Bạn có thể tính cỡ mẫu cho kiểm định ANOVA bằng hàm
|
||||
<code>pwr.anova.test</code> trong R:</p>
|
||||
<div>
|
||||
<div>R</div>
|
||||
<div>
|
||||
<div>
|
||||
<div><svg>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M7 5C7 3.34315 8.34315 2 10 2H19C20.6569 2 22 3.34315 22 5V14C22 15.6569 20.6569 17 19 17H17V19C17 20.6569 15.6569 22 14 22H5C3.34315 22 2 20.6569 2 19V10C2 8.34315 3.34315 7 5 7H7V5ZM9 7H14C15.6569 7 17 8.34315 17 10V15H19C19.5523 15 20 14.5523 20 14V5C20 4.44772 19.5523 4 19 4H10C9.44772 4 9 4.44772 9 5V7ZM5 9C4.44772 9 4 9.44772 4 10V19C4 19.5523 4.44772 20 5 20H14C14.5523 20 15 19.5523 15 19V10C15 9.44772 14.5523 9 14 9H5Z"
|
||||
fill="currentColor"></path>
|
||||
</svg><span data-state="closed"><button>Sao chép mã</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div dir="ltr"><code>library(pwr)
|
||||
pwr.anova.test(k = 3, f = 0.25, sig.level = 0.05, power = 0.8)
|
||||
</code></div>
|
||||
</div>
|
||||
<p>Kết quả trả về:</p>
|
||||
<ul>
|
||||
<li>Số mẫu cần thiết cho mỗi nhóm (<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>n</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">n</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">n</span>).</li>
|
||||
<li>Tổng số mẫu nếu bạn nhân với số nhóm (<math style="display: block">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mi>k</mi>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">k</annotation>
|
||||
</semantics>
|
||||
</math><span aria-hidden="true">k</span>).</li>
|
||||
</ul>
|
||||
<p dir="ltr" style="text-align: left;"></p>
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/RepeatAnova/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,154 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hướng dẫn tính cỡ mẫu Repeated Measures ANOVA</title>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script type="text/javascript" id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h3, h4 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
hr {
|
||||
border: 1px solid #ccc;
|
||||
margin: 2em 0;
|
||||
}
|
||||
code {
|
||||
background-color: #f9f9f9;
|
||||
padding: 2px 4px;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
pre {
|
||||
background: #f4f4f4;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Nội dung được đưa vào đây -->
|
||||
<h3> </h3>
|
||||
<h3><strong>Hướng dẫn tính toán cỡ mẫu cho kiểm định Repeated Measures ANOVA</strong></h3>
|
||||
<hr>
|
||||
<h3><strong>1. Repeated Measures ANOVA là gì?</strong></h3>
|
||||
<p><strong>Repeated Measures ANOVA</strong> (Phân tích phương sai với số đo lặp lại) là một phương pháp thống kê được sử dụng khi cùng một nhóm đối tượng được đo nhiều lần trên cùng một biến phụ thuộc trong các điều kiện hoặc thời điểm khác nhau.</p>
|
||||
<h4><strong>Phạm vi ứng dụng:</strong></h4>
|
||||
<p>Repeated Measures ANOVA được áp dụng rộng rãi trong các nghiên cứu y sinh và y tế công cộng, khi cần so sánh sự thay đổi của một biến qua thời gian hoặc qua các điều kiện khác nhau:</p>
|
||||
<ul>
|
||||
<li>Theo dõi hiệu quả điều trị tại nhiều thời điểm.</li>
|
||||
<li>So sánh đáp ứng sinh học trong các điều kiện hoặc liều lượng khác nhau.</li>
|
||||
<li>Đánh giá thay đổi hành vi hoặc thái độ qua các giai đoạn can thiệp.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h4><strong>Ví dụ trong nghiên cứu:</strong></h4>
|
||||
<ol>
|
||||
<li><strong>Nghiên cứu lâm sàng:</strong>
|
||||
<ul>
|
||||
<li>Theo dõi huyết áp của bệnh nhân tại 3 thời điểm: trước điều trị, sau 1 tháng, và sau 3 tháng.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Nghiên cứu dinh dưỡng:</strong>
|
||||
<ul>
|
||||
<li>So sánh mức đường huyết của một nhóm bệnh nhân sau khi sử dụng 3 chế độ ăn khác nhau.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Y tế công cộng:</strong>
|
||||
<ul>
|
||||
<li>Đánh giá tác động của chiến dịch truyền thông sức khỏe đối với kiến thức người dân qua các thời điểm khảo sát khác nhau.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h3><strong>2. Công thức tính cỡ mẫu cho Repeated Measures ANOVA</strong></h3>
|
||||
<p>Cỡ mẫu cho kiểm định Repeated Measures ANOVA được tính toán dựa trên các tham số chính:</p>
|
||||
<ul>
|
||||
<li><strong>Số lần đo lặp lại (\(p\))</strong>: Số thời điểm hoặc điều kiện trong nghiên cứu.</li>
|
||||
<li><strong>Kích thước hiệu ứng chuẩn hóa (\(f\))</strong>: Đo lường mức độ thay đổi của biến phụ thuộc qua các lần đo.</li>
|
||||
<li><strong>Hệ số tương quan giữa các lần đo (\(\rho\))</strong>: Đo mức độ tương quan giữa các lần đo trên cùng một đối tượng.</li>
|
||||
<li><strong>Mức ý nghĩa (\(\alpha\))</strong>: Xác suất chấp nhận sai lầm loại I.</li>
|
||||
<li><strong>Độ mạnh kiểm định (\(1 - \beta\))</strong>: Xác suất phát hiện khác biệt có ý nghĩa khi khác biệt tồn tại.</li>
|
||||
</ul>
|
||||
<h4><strong>Công thức tổng quát:</strong></h4>
|
||||
<p>
|
||||
\[
|
||||
n = \frac{(\eta + 1)(Z_{1-\alpha} + Z_{1-\beta})^2}{f^2 (1 - \rho)}
|
||||
\]
|
||||
</p>
|
||||
<h4><strong>Trong đó:</strong></h4>
|
||||
<ul>
|
||||
<li>\(n\): Số mẫu cần thiết cho mỗi nhóm.</li>
|
||||
<li>\(\eta = p - 1\): Bậc tự do của số lần đo lặp lại.</li>
|
||||
<li>\(f\): Kích thước hiệu ứng chuẩn hóa (nhỏ = 0.1, trung bình = 0.25, lớn = 0.4).</li>
|
||||
<li>\(\rho\): Hệ số tương quan giữa các lần đo.</li>
|
||||
<li>\(Z_{1-\alpha}\): Giá trị tới hạn của phân phối chuẩn tương ứng mức ý nghĩa.</li>
|
||||
<li>\(Z_{1-\beta}\): Giá trị tới hạn tương ứng với độ mạnh kiểm định.</li>
|
||||
</ul>
|
||||
<h4><strong>Tổng cỡ mẫu cho toàn bộ nghiên cứu:</strong></h4>
|
||||
<p>
|
||||
\[
|
||||
n_{\text{total}} = n \times \text{số nhóm}
|
||||
\]
|
||||
</p>
|
||||
<hr>
|
||||
<h3><strong>3. Ví dụ minh họa</strong></h3>
|
||||
<h4><strong>Bài toán:</strong></h4>
|
||||
<p>Bạn muốn đánh giá sự thay đổi điểm huyết áp của một nhóm bệnh nhân tại 3 thời điểm:</p>
|
||||
<ol>
|
||||
<li>Trước khi điều trị.</li>
|
||||
<li>Sau 1 tháng điều trị.</li>
|
||||
<li>Sau 3 tháng điều trị.</li>
|
||||
</ol>
|
||||
<p>Các tham số:</p>
|
||||
<ul>
|
||||
<li>\(p = 3\)</li>
|
||||
<li>\(f = 0.25\)</li>
|
||||
<li>\(\rho = 0.5\)</li>
|
||||
<li>\(\alpha = 0.05\)</li>
|
||||
<li>\(1 - \beta = 0.8\)</li>
|
||||
</ul>
|
||||
<p>Tính toán từng bước sẽ cho kết quả \(n \approx 753\)</p>
|
||||
<hr>
|
||||
<h3><strong>4. Hướng dẫn tính toán cỡ mẫu bằng R</strong></h3>
|
||||
<h4><strong>Mã R mẫu:</strong></h4>
|
||||
<pre><code>library(pwr)
|
||||
|
||||
# Hiệu ứng điều chỉnh với hệ số tương quan
|
||||
adjusted_effect_size <- 0.25 * sqrt(1 - 0.5)
|
||||
|
||||
# Tính cỡ mẫu
|
||||
result <- pwr.anova.test(
|
||||
k = 3, # Số thời điểm
|
||||
f = adjusted_effect_size,
|
||||
sig.level = 0.05,
|
||||
power = 0.8
|
||||
)
|
||||
|
||||
print(result)</code></pre>
|
||||
<h4><strong>Kết quả trả về:</strong></h4>
|
||||
<pre><code>Balanced one-way analysis of variance power calculation
|
||||
|
||||
k = 3
|
||||
n = 752.64
|
||||
f = 0.1767767
|
||||
sig.level = 0.05
|
||||
power = 0.8</code></pre>
|
||||
<h4><strong>Kết luận:</strong></h4>
|
||||
<ul>
|
||||
<li>Cỡ mẫu cần thiết cho mỗi nhóm: \(n = 753\)</li>
|
||||
<li>Tổng cỡ mẫu (với 1 nhóm): \(n_{\text{total}} = 753\)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3><strong>5. Kết luận</strong></h3>
|
||||
<p>Repeated Measures ANOVA là một công cụ mạnh mẽ để phân tích dữ liệu lặp lại trên cùng một đối tượng, giúp kiểm tra hiệu quả của các can thiệp qua thời gian hoặc trong các điều kiện khác nhau. Việc tính toán cỡ mẫu đúng cách giúp đảm bảo nghiên cứu có đủ sức mạnh thống kê để phát hiện sự khác biệt thực sự.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/TwowayANOVA/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,125 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Hướng dẫn tính cỡ mẫu Two-way ANOVA</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
h1, h2, h3, h4 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
hr {
|
||||
margin: 30px 0;
|
||||
}
|
||||
code {
|
||||
background-color: #f3f3f3;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h3><strong>Hướng dẫn tính toán cỡ mẫu cho kiểm định Two-way ANOVA</strong></h3>
|
||||
<hr>
|
||||
<h3><strong>1. Two-way ANOVA là gì?</strong></h3>
|
||||
<p><strong>Two-way ANOVA</strong> (Phân tích phương sai hai chiều) là một phương pháp thống kê dùng để kiểm tra:</p>
|
||||
<ol>
|
||||
<li><strong>Hiệu ứng chính (Main effects):</strong>
|
||||
<ul>
|
||||
<li>Xem liệu từng yếu tố độc lập (factor) có ảnh hưởng có ý nghĩa đến biến phụ thuộc hay không.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Hiệu ứng tương tác (Interaction effects):</strong>
|
||||
<ul>
|
||||
<li>Xem liệu hai yếu tố độc lập có ảnh hưởng tương tác đến biến phụ thuộc hay không.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h4><strong>Phạm vi ứng dụng:</strong></h4>
|
||||
<p>Two-way ANOVA được sử dụng khi bạn cần phân tích tác động của hai yếu tố độc lập (có nhiều mức) đến biến phụ thuộc. Phương pháp này phổ biến trong nghiên cứu y học, khoa học xã hội, và kỹ thuật.</p>
|
||||
<h4><strong>Ví dụ:</strong></h4>
|
||||
<ul>
|
||||
<li><strong>Nghiên cứu y tế</strong>: Loại thuốc và giới tính ảnh hưởng đến huyết áp.</li>
|
||||
<li><strong>Nghiên cứu giáo dục</strong>: Phương pháp giảng dạy và trình độ học sinh ảnh hưởng đến điểm thi.</li>
|
||||
<li><strong>Nghiên cứu kỹ thuật</strong>: Nhiệt độ và áp suất ảnh hưởng đến độ bền vật liệu.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3><strong>2. Công thức tính cỡ mẫu</strong></h3>
|
||||
<p>Cỡ mẫu trong Two-way ANOVA phụ thuộc vào:</p>
|
||||
<ul>
|
||||
<li>Hiệu ứng chính hoặc hiệu ứng tương tác.</li>
|
||||
<li>Kích thước hiệu ứng chuẩn hóa \( f \).</li>
|
||||
<li>Mức ý nghĩa \( \alpha \), power \( 1 - \beta \).</li>
|
||||
<li>Số mức của hai yếu tố: \( \eta_1, \eta_2 \).</li>
|
||||
</ul>
|
||||
<h4><strong>Công thức tổng quát:</strong></h4>
|
||||
<p>
|
||||
\[
|
||||
n = \frac{(\eta_1 \times \eta_2)(Z_{1-\alpha} + Z_{1-\beta})^2}{f^2}
|
||||
\]
|
||||
</p>
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( n \): cỡ mẫu cần thiết cho mỗi tổ hợp.</li>
|
||||
<li>\( \eta_1, \eta_2 \): số mức của hai yếu tố.</li>
|
||||
<li>\( f \): kích thước hiệu ứng (0.1 nhỏ, 0.25 trung bình, 0.4 lớn).</li>
|
||||
<li>\( Z_{1-\alpha}, Z_{1-\beta} \): điểm tới hạn từ phân phối chuẩn.</li>
|
||||
</ul>
|
||||
<h4><strong>Tổng số mẫu:</strong></h4>
|
||||
<p>
|
||||
\[
|
||||
n_{total} = n \times (\eta_1 \times \eta_2)
|
||||
\]
|
||||
</p>
|
||||
<hr>
|
||||
<h3><strong>3. Ví dụ minh họa</strong></h3>
|
||||
<p>Bài toán: Kiểm tra ảnh hưởng của 3 loại thuốc (\( \eta_1 = 3 \)) và 2 giới tính (\( \eta_2 = 2 \)) đến huyết áp.</p>
|
||||
<ul>
|
||||
<li>\( f = 0.25 \), \( \alpha = 0.05 \), \( 1 - \beta = 0.8 \)</li>
|
||||
<li>\( Z_{1-\alpha} = 1.96 \), \( Z_{1-\beta} = 0.84 \)</li>
|
||||
</ul>
|
||||
<p>
|
||||
\[
|
||||
n = \frac{6 \times (1.96 + 0.84)^2}{0.25^2} = \frac{6 \times 7.84}{0.0625} = \frac{47.04}{0.0625} = 752.64
|
||||
\]
|
||||
</p>
|
||||
<p>
|
||||
\[
|
||||
n_{total} = 752.64 \times 6 = 4515.84
|
||||
\]
|
||||
</p>
|
||||
<p><strong>Kết luận:</strong> Cỡ mẫu mỗi tổ hợp: 753, tổng toàn nghiên cứu: 4516 (làm tròn).</p>
|
||||
<hr>
|
||||
<h3><strong>4. Tính toán trong R</strong></h3>
|
||||
<pre><code>library(pwr)
|
||||
|
||||
# Tổng số tổ hợp (3 x 2 = 6)
|
||||
total_groups <- 3 * 2
|
||||
|
||||
result <- pwr.anova.test(
|
||||
k = total_groups,
|
||||
f = 0.25,
|
||||
sig.level = 0.05,
|
||||
power = 0.8
|
||||
)
|
||||
|
||||
print(result)</code></pre>
|
||||
<p>Kết quả:</p>
|
||||
<pre><code>Balanced one-way analysis of variance power calculation
|
||||
|
||||
k = 6
|
||||
n = 753.64
|
||||
f = 0.25
|
||||
sig.level = 0.05
|
||||
power = 0.8</code></pre>
|
||||
<hr>
|
||||
<h3><strong>5. Kết luận</strong></h3>
|
||||
<p>Two-way ANOVA giúp kiểm tra đồng thời ảnh hưởng của hai yếu tố và tương tác giữa chúng đến biến phụ thuộc. Việc tính đúng cỡ mẫu là điều kiện then chốt đảm bảo độ tin cậy cho kết quả phân tích.</p>
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/chisquaregoodfit/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,71 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Tính Cỡ Mẫu Kiểm định Chi-squared Goodness-of-Fit</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.formula {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #8e44ad;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Tính Cỡ Mẫu Kiểm định Chi-squared Goodness-of-Fit</h1>
|
||||
|
||||
<h2>Mục đích sử dụng</h2>
|
||||
<p>Kiểm định Chi-squared Goodness-of-Fit dùng để đánh giá xem dữ liệu quan sát có phù hợp với phân phối lý thuyết hay không.</p>
|
||||
|
||||
<h2>Công thức tính cỡ mẫu</h2>
|
||||
<p>Cỡ mẫu được tính dựa trên kích thước hiệu ứng Cohen's \( w \) và giá trị tới hạn Chi-squared:</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \frac{\chi^2_{1-\alpha, df} + \chi^2_{1-\beta, df}}{w^2}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( w \): kích thước hiệu ứng (Cohen's w), đo mức độ khác biệt kỳ vọng giữa phân phối quan sát và phân phối lý thuyết.</li>
|
||||
<li>\( \chi^2_{1-\alpha, df} \), \( \chi^2_{1-\beta, df} \): giá trị tới hạn của phân phối Chi-squared với bậc tự do \( df \) tương ứng mức ý nghĩa và sức mạnh kiểm định.</li>
|
||||
<li>\( df \): bậc tự do bằng số nhóm phân loại trừ đi 1, tức \( df = k - 1 \).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Ví dụ</h2>
|
||||
<p>Giả sử bạn muốn kiểm định goodness-of-fit với 4 nhóm phân loại, mức ý nghĩa \( \alpha = 0.05 \), power 0.80, và Cohen's \( w = 0.25 \) (hiệu ứng trung bình):</p>
|
||||
<ul>
|
||||
<li>\( df = 4 - 1 = 3 \)</li>
|
||||
<li>\( \chi^2_{0.95, 3} = 7.81 \)</li>
|
||||
<li>\( \chi^2_{0.80, 3} = 4.11 \) (tra bảng hoặc phần mềm)</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \frac{7.81 + 4.11}{0.25^2} = \frac{11.92}{0.0625} = 190.72
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Nên cần khoảng <strong>191 quan sát</strong> để kiểm định hiệu quả với các tham số này.</p>
|
||||
|
||||
<h2>Ứng dụng</h2>
|
||||
<p>Bạn có thể nhập các thông số vào app để tính nhanh cỡ mẫu phù hợp với nghiên cứu của mình.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/chisquare/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,66 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Hướng dẫn Tính Cỡ Mẫu Kiểm định Chi-squared Độc lập</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.formula {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #c0392b;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Tính Cỡ Mẫu Kiểm định Chi-squared Độc lập</h1>
|
||||
|
||||
<h2>Mục đích sử dụng</h2>
|
||||
<p>Tính cỡ mẫu giúp xác định số quan sát cần thiết để kiểm định sự độc lập giữa hai biến danh mục bằng kiểm định Chi-squared với mức ý nghĩa và sức mạnh kiểm định mong muốn.</p>
|
||||
|
||||
<h2>Công thức tính cỡ mẫu</h2>
|
||||
<p>Cỡ mẫu được tính theo công thức:</p>
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \frac{\chi^2_{1-\alpha, df} + \chi^2_{1-\beta, df}}{w^2}
|
||||
\]
|
||||
</div>
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( w \): kích thước hiệu ứng (Cohen's w), đo mức độ khác biệt kỳ vọng giữa các tần số thực tế và tần số dự kiến dưới giả thuyết độc lập.</li>
|
||||
<li>\( \chi^2_{1-\alpha, df} \), \( \chi^2_{1-\beta, df} \): giá trị tới hạn của phân phối Chi-squared với bậc tự do \( df \) tương ứng mức ý nghĩa và sức mạnh kiểm định.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Ví dụ</h2>
|
||||
<p>Giả sử kiểm định độc lập bảng 2x2, với mức ý nghĩa \( \alpha = 0.05 \), sức mạnh kiểm định (power) \( 1-\beta = 0.80 \), và kích thước hiệu ứng Cohen's \( w = 0.3 \) (mức trung bình).</p>
|
||||
<ul>
|
||||
<li>Bậc tự do: \( df = (2-1)(2-1) = 1 \)</li>
|
||||
<li>Giá trị tới hạn: \( \chi^2_{0.95,1} = 3.84 \)</li>
|
||||
<li>Giá trị power: \( \chi^2_{0.80,1} = 1.64 \) (tra bảng hoặc phần mềm)</li>
|
||||
</ul>
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \frac{3.84 + 1.64}{0.3^2} = \frac{5.48}{0.09} \approx 61
|
||||
\]
|
||||
</div>
|
||||
<p>Vậy bạn cần khoảng 61 quan sát để phát hiện hiệu ứng với độ tin cậy và sức mạnh này.</p>
|
||||
|
||||
<h2>Ứng dụng</h2>
|
||||
<p>Trong ứng dụng, bạn nhập kích thước hiệu ứng và các tham số để tính cỡ mẫu tự động.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/PearsonCorrelationTest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,103 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kiểm định Tương quan Pearson</title>
|
||||
<!-- Tải script MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0056b3;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.formula {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định Tương quan Pearson</h1>
|
||||
|
||||
<p>
|
||||
Kiểm định tương quan Pearson được sử dụng để đo mức độ và hướng của mối quan hệ <strong>tuyến tính</strong> giữa hai biến định lượng liên tục.
|
||||
</p>
|
||||
|
||||
<h2>1. Hệ số tương quan Pearson (\( r \))</h2>
|
||||
|
||||
<p>Hệ số tương quan Pearson có giá trị trong khoảng từ -1 đến 1:</p>
|
||||
<ul>
|
||||
<li><strong>\( r \approx 1 \)</strong>: Tương quan dương mạnh</li>
|
||||
<li><strong>\( r \approx -1 \)</strong>: Tương quan âm mạnh</li>
|
||||
<li><strong>\( r \approx 0 \)</strong>: Không có tương quan tuyến tính</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
r = \frac{\sum (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum (x_i - \bar{x})^2 \sum (y_i - \bar{y})^2}}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( x_i, y_i \): các giá trị quan sát của hai biến</li>
|
||||
<li>\( \bar{x}, \bar{y} \): trung bình của các biến tương ứng</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Công thức tính cỡ mẫu cần thiết</h2>
|
||||
|
||||
<p>
|
||||
Để xác định cỡ mẫu đủ lớn nhằm phát hiện một hệ số tương quan \( r \) với mức ý nghĩa \( \alpha \) và công suất \( 1 - \beta \), công thức gần đúng là:
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \left( \frac{Z_{1-\alpha/2} + Z_{1-\beta}}{0.5 \cdot \ln\left(\frac{1 + r}{1 - r}\right)} \right)^2 + 3
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( r \): hệ số tương quan mong muốn phát hiện</li>
|
||||
<li>\( Z_{1-\alpha/2} \): giá trị tới hạn của phân phối chuẩn (≈ 1.96 nếu \( \alpha = 0.05 \))</li>
|
||||
<li>\( Z_{1-\beta} \): điểm tới hạn tương ứng với công suất mong muốn (≈ 0.84 nếu power = 80%)</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Ứng dụng trong y tế công cộng</h2>
|
||||
|
||||
<p>
|
||||
Tương quan Pearson được ứng dụng rộng rãi để:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Đánh giá tương quan giữa nồng độ PM2.5 và tỷ lệ bệnh phổi.</li>
|
||||
<li>Phân tích mối quan hệ giữa BMI và huyết áp.</li>
|
||||
<li>Khám phá liên hệ giữa hoạt động thể chất và mức cholesterol máu.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Những phân tích này giúp định hướng các chiến lược can thiệp và dự phòng bệnh trong cộng đồng.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/SpearmanCorrelationTest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,99 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kiểm định Tương quan hạng Spearman</title>
|
||||
<!-- Tải script MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0056b3;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.formula {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background-color: #e7f3fe;
|
||||
border-color: #d0eaff;
|
||||
color: #0c5460;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định Tương quan hạng Spearman (\(\rho_s\))</h1>
|
||||
|
||||
<p>
|
||||
Kiểm định tương quan hạng Spearman là một phương pháp phi tham số được sử dụng để đo lường mức độ và hướng của mối quan hệ <strong>đơn điệu (monotonic)</strong> giữa hai biến.
|
||||
</p>
|
||||
|
||||
<h2>1. Hệ số tương quan hạng Spearman (\(r_s\))</h2>
|
||||
|
||||
<p>
|
||||
Không giống như Pearson, Spearman không đo lường mối quan hệ tuyến tính. Thay vào đó, nó đánh giá xem khi giá trị của một biến tăng lên, giá trị của biến kia có xu hướng tăng (hoặc giảm) một cách nhất quán hay không, ngay cả khi mối quan hệ đó không phải là một đường thẳng.
|
||||
</p>
|
||||
<p>Hệ số \(r_s\) được tính bằng cách áp dụng công thức Pearson trên dữ liệu đã được xếp hạng (rank). Công thức tính toán phổ biến là:</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
r_s = 1 - \frac{6 \sum d_i^2}{n(n^2 - 1)}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( d_i \): là sự khác biệt giữa hạng của hai biến tương ứng cho quan sát thứ \(i\).</li>
|
||||
<li>\( n \): là số lượng quan sát.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Khi nào nên sử dụng Tương quan Spearman?</h2>
|
||||
<div class="alert">
|
||||
<p>Spearman là một lựa chọn thay thế mạnh mẽ cho Pearson trong các trường hợp sau:</p>
|
||||
<ul>
|
||||
<li><strong>Mối quan hệ không tuyến tính:</strong> Khi mối quan hệ giữa hai biến có tính đơn điệu nhưng không phải là một đường thẳng.</li>
|
||||
<li><strong>Dữ liệu thứ hạng (Ordinal Data):</strong> Khi một hoặc cả hai biến là dữ liệu thứ hạng (ví dụ: thang điểm Likert, mức độ hài lòng).</li>
|
||||
<li><strong>Dữ liệu không tuân theo phân phối chuẩn:</strong> Spearman không yêu cầu giả định về phân phối chuẩn của dữ liệu.</li>
|
||||
<li><strong>Có các giá trị ngoại lai (Outliers):</strong> Vì Spearman sử dụng hạng thay vì giá trị gốc, nó ít bị ảnh hưởng bởi các giá trị ngoại lai.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>3. Ứng dụng trong y tế công cộng</h2>
|
||||
|
||||
<p>
|
||||
Tương quan Spearman rất hữu ích trong các nghiên cứu y tế công cộng, đặc biệt khi làm việc với các thang đo hoặc các biến không tuân theo phân phối chuẩn:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Phân tích mối liên hệ giữa thứ hạng kinh tế-xã hội của một khu vực và thứ hạng về tỷ lệ mắc bệnh mãn tính.</li>
|
||||
<li>Đánh giá mối quan hệ giữa mức độ giáo dục (ví dụ: tiểu học, trung học, đại học) và mức độ nhận thức về một vấn đề sức khỏe.</li>
|
||||
<li>Khám phá xem liệu thứ hạng về mức độ ô nhiễm không khí ở các thành phố có tương quan với thứ hạng về tỷ lệ nhập viện do bệnh hô hấp hay không.</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/F-test/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,126 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kiểm định F về Phương sai (F-test for Variance)</title>
|
||||
<!-- Tải script MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0056b3;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.formula {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
color: #856404;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định F để so sánh phương sai (F-test for Variance)</h1>
|
||||
|
||||
<p>
|
||||
Kiểm định F (F-test) được sử dụng để so sánh phương sai của hai quần thể, thường là để kiểm tra giả thuyết rằng hai biến có độ biến thiên bằng nhau. Đây là nền tảng cho các kiểm định như ANOVA, hoặc kiểm tra giả định về tính đồng nhất phương sai trước khi thực hiện các phân tích khác.
|
||||
</p>
|
||||
|
||||
<h2>1. Giả thuyết kiểm định</h2>
|
||||
|
||||
<p>
|
||||
Giả sử có hai mẫu độc lập với phương sai lần lượt là \( \sigma_1^2 \) và \( \sigma_2^2 \), ta muốn kiểm tra:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>\( H_0: \sigma_1^2 = \sigma_2^2 \)</li>
|
||||
<li>\( H_1: \sigma_1^2 \ne \sigma_2^2 \) (hai phía) hoặc \( \sigma_1^2 > \sigma_2^2 \), v.v.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Công thức tính thống kê kiểm định F</h2>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
F = \frac{S_1^2}{S_2^2}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( S_1^2 \): phương sai mẫu của nhóm 1</li>
|
||||
<li>\( S_2^2 \): phương sai mẫu của nhóm 2</li>
|
||||
<li>Theo quy ước, ta thường đặt phương sai lớn hơn ở tử số để F luôn ≥ 1.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Giá trị \( F \) tuân theo phân phối F với \( (n_1 - 1, n_2 - 1) \) bậc tự do.
|
||||
</p>
|
||||
|
||||
<h2>3. Công thức xấp xỉ tính cỡ mẫu</h2>
|
||||
|
||||
<p>
|
||||
Mặc dù không phổ biến như trong t-test hay chi-square, cỡ mẫu để phát hiện sự khác biệt về phương sai có thể ước tính bằng:
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \left( \frac{Z_{1-\alpha/2} + Z_{1-\beta}}{\ln(\sigma_1 / \sigma_2) \cdot \sqrt{2}} \right)^2 + 2
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( \sigma_1 / \sigma_2 \): tỉ lệ giữa hai độ lệch chuẩn mong muốn phân biệt</li>
|
||||
<li>\( Z_{1-\alpha/2} \): điểm tới hạn phân phối chuẩn (≈ 1.96 nếu \( \alpha = 0.05 \))</li>
|
||||
<li>\( Z_{1-\beta} \): điểm tới hạn công suất (≈ 0.84 nếu power = 80%)</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert">
|
||||
<strong>Lưu ý:</strong> Công thức trên chỉ là xấp xỉ. Để có kết quả chính xác, bạn nên dùng phần mềm chuyên dụng như G*Power, R (gói `pwr`), hoặc các công cụ tính toán trực tuyến dựa trên phân phối F chính xác.
|
||||
</div>
|
||||
|
||||
<h2>4. Ứng dụng trong y tế công cộng</h2>
|
||||
|
||||
<p>
|
||||
Kiểm định F được sử dụng để đánh giá sự khác biệt về độ biến thiên của một biến định lượng giữa hai nhóm, ví dụ:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>So sánh độ dao động huyết áp giữa nam và nữ.</li>
|
||||
<li>Kiểm tra tính đồng nhất của biến cholesterol trong hai khu dân cư khác nhau.</li>
|
||||
<li>Xác định xem thuốc mới có làm giảm sự dao động của đường huyết hơn thuốc cũ không.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Trong phân tích ANOVA, kiểm định F còn giúp đánh giá sự khác biệt trung bình giữa nhiều nhóm — một bước quan trọng trong phân tích y tế cộng đồng có nhiều can thiệp.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/Kruskal-Wallis-Test/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,114 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kiểm định Kruskal-Wallis</title>
|
||||
<!-- Tải script MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0056b3;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.formula {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background-color: #e7f3fe;
|
||||
border-color: #d0eaff;
|
||||
color: #0c5460;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định Kruskal-Wallis (Kruskal-Wallis Test)</h1>
|
||||
|
||||
<p>
|
||||
Kiểm định Kruskal-Wallis là một phương pháp phi tham số được sử dụng để xác định xem liệu có sự khác biệt đáng kể về mặt thống kê giữa hai hoặc nhiều nhóm độc lập hay không. Nó được coi là phiên bản thay thế cho <strong>Phân tích phương sai một yếu tố (One-Way ANOVA)</strong> khi các giả định của ANOVA không được đáp ứng.
|
||||
</p>
|
||||
|
||||
<h2>1. Giả thuyết kiểm định</h2>
|
||||
<p>
|
||||
Kiểm định này so sánh phân phối của các nhóm dựa trên thứ hạng của dữ liệu.
|
||||
</p>
|
||||
<ul>
|
||||
<li>\( H_0 \): Phân phối của tất cả các nhóm là như nhau.</li>
|
||||
<li>\( H_1 \): Phân phối của ít nhất một nhóm khác biệt so với các nhóm còn lại.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Khi nào nên sử dụng Kruskal-Wallis?</h2>
|
||||
<div class="alert">
|
||||
<p>Sử dụng kiểm định này thay cho One-Way ANOVA khi:</p>
|
||||
<ul>
|
||||
<li><strong>Dữ liệu không tuân theo phân phối chuẩn:</strong> Đây là lý do phổ biến nhất.</li>
|
||||
<li><strong>Phương sai không đồng nhất:</strong> Khi giả định về tính đồng nhất của phương sai bị vi phạm.</li>
|
||||
<li><strong>Dữ liệu ở dạng thứ hạng (Ordinal Data):</strong> Khi biến phụ thuộc của bạn là dữ liệu thứ hạng (ví dụ: mức độ hài lòng, thang điểm Likert).</li>
|
||||
<li><strong>Cỡ mẫu nhỏ và dữ liệu không chuẩn.</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>3. Thống kê kiểm định (H-statistic)</h2>
|
||||
|
||||
<p>
|
||||
Thống kê kiểm định H được tính toán dựa trên tổng hạng của mỗi nhóm.
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
H = \frac{12}{N(N+1)} \sum_{i=1}^{k} \frac{R_i^2}{n_i} - 3(N+1)
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Trong đó:
|
||||
<ul>
|
||||
<li>\( N \): là tổng số quan sát trong tất cả các nhóm.</li>
|
||||
<li>\( k \): là số lượng nhóm.</li>
|
||||
<li>\( R_i \): là tổng hạng của các quan sát trong nhóm \(i\).</li>
|
||||
<li>\( n_i \): là số lượng quan sát trong nhóm \(i\).</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
Khi cỡ mẫu đủ lớn, thống kê H xấp xỉ tuân theo phân phối Chi-bình phương với \(k-1\) bậc tự do.
|
||||
</p>
|
||||
|
||||
<h2>4. Tính toán cỡ mẫu</h2>
|
||||
<p>
|
||||
Không giống như các kiểm định tham số, không có công thức giải tích đơn giản để tính toán cỡ mẫu cho kiểm định Kruskal-Wallis. Việc này thường đòi hỏi phải sử dụng các phần mềm thống kê chuyên dụng (như R, SAS) để chạy <strong>mô phỏng Monte Carlo</strong>, dựa trên các giả định về hình dạng phân phối và độ lớn của hiệu ứng mong muốn.
|
||||
</p>
|
||||
|
||||
<h2>5. Ứng dụng trong y tế công cộng</h2>
|
||||
<ul>
|
||||
<li><strong>So sánh mức độ hài lòng:</strong> So sánh mức độ hài lòng của bệnh nhân (đo bằng thang điểm từ 1-5) đối với ba chương trình can thiệp sức khỏe cộng đồng khác nhau.</li>
|
||||
<li><strong>Đánh giá nhận thức:</strong> So sánh mức độ nhận thức về nguy cơ bệnh tật (thấp, trung bình, cao) giữa các nhóm có trình độ học vấn khác nhau.</li>
|
||||
<li><strong>Phân tích dữ liệu lâm sàng không chuẩn:</strong> So sánh thời gian nằm viện (thường bị lệch phải) giữa các nhóm bệnh nhân được điều trị bằng các phác đồ khác nhau.</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/LevenesTest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,126 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kiểm định Levene về phương sai</title>
|
||||
<!-- Tải script MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0056b3;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.formula {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background-color: #e7f3fe;
|
||||
border-color: #d0eaff;
|
||||
color: #0c5460;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định Levene (Levene’s Test)</h1>
|
||||
|
||||
<p>
|
||||
Kiểm định Levene được sử dụng để đánh giá <strong>sự bằng nhau của phương sai</strong> (homogeneity of variance) giữa hai hay nhiều nhóm. Đây là kiểm định phổ biến hơn F-test khi dữ liệu không tuân theo phân phối chuẩn, vì nó <strong>ít nhạy cảm với sự vi phạm giả định chuẩn</strong>.
|
||||
</p>
|
||||
|
||||
<h2>1. Giả thuyết kiểm định</h2>
|
||||
<ul>
|
||||
<li>\( H_0: \sigma_1^2 = \sigma_2^2 = \dots = \sigma_k^2 \) (phương sai của các nhóm là bằng nhau)</li>
|
||||
<li>\( H_1: \) Có ít nhất một cặp nhóm có phương sai khác nhau</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Ý tưởng phương pháp</h2>
|
||||
|
||||
<p>
|
||||
Thay vì dùng giá trị gốc \( Y_{ij} \), Levene’s Test hoạt động trên <strong>độ lệch tuyệt đối</strong> giữa mỗi quan sát và trung vị hoặc trung bình của nhóm đó:
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
Z_{ij} = | Y_{ij} - \tilde{Y}_{\cdot j} |
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Trong đó:
|
||||
<ul>
|
||||
<li>\( Y_{ij} \): giá trị của quan sát thứ \( i \) trong nhóm \( j \)</li>
|
||||
<li>\( \tilde{Y}_{\cdot j} \): trung vị (hoặc trung bình) của nhóm \( j \)</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Sau đó, một <strong>kiểm định ANOVA một yếu tố</strong> được thực hiện trên biến mới \( Z_{ij} \). Nếu có sự khác biệt đáng kể về trung bình của \(Z_{ij}\) giữa các nhóm, điều đó ngụ ý rằng phương sai của các giá trị gốc không đồng nhất.
|
||||
</p>
|
||||
|
||||
<h2>3. Thống kê kiểm định (W-statistic)</h2>
|
||||
|
||||
<p>
|
||||
Thống kê Levene (W) có công thức tương tự như thống kê F trong ANOVA:
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
W = \frac{(N - k)}{(k - 1)} \cdot \frac{\sum_{j=1}^k n_j (\bar{Z}_{\cdot j} - \bar{Z}_{\cdot\cdot})^2}{\sum_{j=1}^k \sum_{i=1}^{n_j} (Z_{ij} - \bar{Z}_{\cdot j})^2}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( N \): tổng số quan sát, \( k \): số nhóm</li>
|
||||
<li>\( \bar{Z}_{\cdot j} \): trung bình của \( Z_{ij} \) trong nhóm \( j \)</li>
|
||||
<li>\( \bar{Z}_{\cdot\cdot} \): trung bình của tất cả các giá trị \( Z_{ij} \)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Giá trị \( W \) được so sánh với phân phối \( F_{k-1, N-k} \). Nếu p-value nhỏ hơn mức ý nghĩa (thường là 0.05), chúng ta bác bỏ \( H_0 \).
|
||||
</p>
|
||||
|
||||
<h2>4. Ứng dụng trong y tế công cộng</h2>
|
||||
|
||||
<ul>
|
||||
<li>So sánh sự biến động của huyết áp giữa ba khu vực dân cư khác nhau.</li>
|
||||
<li>Kiểm tra độ ổn định của chỉ số đường huyết giữa các nhóm bệnh nhân dùng các loại thuốc khác nhau.</li>
|
||||
<li>Đánh giá tính đồng nhất phương sai trước khi thực hiện phân tích ANOVA, đặc biệt khi nghiên cứu hiệu quả của nhiều can thiệp.</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Ghi chú</h2>
|
||||
<div class="alert">
|
||||
<ul>
|
||||
<li>Levene’s Test có thể dùng trung vị hoặc trung bình trong công thức. Phiên bản dùng trung vị (thường được gọi là <strong>Brown–Forsythe test</strong>) được khuyến nghị khi dữ liệu có các giá trị ngoại lai (outliers).</li>
|
||||
<li>Đây là một kiểm định giả định <strong>rất quan trọng</strong> trước khi thực hiện ANOVA hoặc các mô hình hồi quy có biến phân loại.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/MannWhitneyUtest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,101 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Giới thiệu về Mann-Whitney U Test</title>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 900px; margin: 40px auto; line-height: 1.6; }
|
||||
h2, h3, h4 { color: #003366; }
|
||||
code { background: #eee; padding: 2px 4px; border-radius: 4px; }
|
||||
hr { margin: 2em 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2><strong>Giới thiệu về Mann-Whitney U Test</strong></h2>
|
||||
|
||||
<h3><strong>1. Mann-Whitney U Test là gì?</strong></h3>
|
||||
<p>Kiểm định Mann-Whitney U là một kiểm định phi tham số được sử dụng để so sánh trung vị giữa hai nhóm độc lập, thay thế cho t-test hai mẫu độc lập khi:</p>
|
||||
<ul>
|
||||
<li>Dữ liệu không tuân theo phân phối chuẩn.</li>
|
||||
<li>Dữ liệu dạng thứ tự hoặc liên tục nhưng không chuẩn.</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3><strong>2. Vai trò và Ứng dụng</strong></h3>
|
||||
<ul>
|
||||
<li><strong>Y sinh:</strong> So sánh sự thay đổi giữa hai nhóm bệnh nhân dùng hai thuốc.</li>
|
||||
<li><strong>Thử nghiệm lâm sàng:</strong> Đánh giá hiệu quả thuốc qua trung vị chỉ số sinh học.</li>
|
||||
<li><strong>Y tế công cộng:</strong> So sánh trung vị mức độ hài lòng giữa hai cộng đồng.</li>
|
||||
</ul>
|
||||
|
||||
<h4><strong>Ví dụ</strong></h4>
|
||||
<ul>
|
||||
<li>So sánh trung vị BMI giữa nam và nữ.</li>
|
||||
<li>So sánh mức độ đau (1–10) giữa hai nhóm thuốc.</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2><strong>Hướng dẫn Tính Cỡ Mẫu</strong></h2>
|
||||
|
||||
<h3><strong>1. Công thức cỡ mẫu</strong></h3>
|
||||
<p>Cỡ mẫu mỗi nhóm được tính theo:</p>
|
||||
|
||||
<p style="text-align:center;">
|
||||
$$ n = \frac{(Z_{1-\alpha/2} + Z_{1-\beta})^2}{r^2} $$
|
||||
</p>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( Z_{1-\alpha/2} \): Giá trị Z theo mức ý nghĩa \( \alpha \)</li>
|
||||
<li>\( Z_{1-\beta} \): Giá trị Z theo độ mạnh kiểm định (power)</li>
|
||||
<li>\( r \): Kích thước hiệu ứng (effect size), tính theo:
|
||||
$$ r = \frac{\mu_1 - \mu_2}{\sigma} $$
|
||||
với \( \mu_1, \mu_2 \): trung vị hai nhóm, và \( \sigma \): độ lệch chuẩn tổng hợp.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3><strong>2. Các bước tính toán</strong></h3>
|
||||
<ol>
|
||||
<li>Xác định tham số: \( \alpha, 1 - \beta, r \)</li>
|
||||
<li>Tính:
|
||||
<ul>
|
||||
<li>\( Z_{1-\alpha/2} = qnorm(1 - \alpha/2) \)</li>
|
||||
<li>\( Z_{1-\beta} = qnorm(1 - \beta) \)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Thay vào công thức để tính \( n \)</li>
|
||||
</ol>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3><strong>3. Ví dụ minh họa</strong></h3>
|
||||
|
||||
<p><strong>Giả sử:</strong></p>
|
||||
<ul>
|
||||
<li>\( \mu_1 = 5 \), \( \mu_2 = 7 \), \( \sigma = 2 \)</li>
|
||||
<li>\( \alpha = 0.05 \Rightarrow Z_{1-\alpha/2} = 1.96 \)</li>
|
||||
<li>\( 1 - \beta = 0.8 \Rightarrow Z_{1-\beta} = 0.84 \)</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Bước 1:</strong> Tính effect size:</p>
|
||||
<p style="text-align:center;">
|
||||
$$ r = \frac{5 - 7}{2} = -1 \Rightarrow |r| = 1 $$
|
||||
</p>
|
||||
|
||||
<p><strong>Bước 2:</strong> Thay vào công thức:</p>
|
||||
<p style="text-align:center;">
|
||||
$$ n = \frac{(1.96 + 0.84)^2}{1^2} = \frac{7.84}{1} = 7.84 $$
|
||||
</p>
|
||||
|
||||
<p><strong>Kết luận:</strong> Cỡ mẫu cần thiết mỗi nhóm: <strong>8</strong> đối tượng.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1,113 +0,0 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Phương pháp luận Xác định Cỡ mẫu Nghiên cứu</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.7;
|
||||
color: #333;
|
||||
background-color: #f9f9f9;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
padding: 30px 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
h2 {
|
||||
color: #34495e;
|
||||
font-size: 1.5em;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
font-size: 1em;
|
||||
text-align: justify;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
strong {
|
||||
color: #c0392b;
|
||||
}
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: #ecf0f1;
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.reason {
|
||||
background-color: #e8f6f3;
|
||||
border-left: 4px solid #1abc9c;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<h1>PHƯƠNG PHÁP LUẬN XÁC ĐỊNH CỠ MẪU NGHIÊN CỨU</h1>
|
||||
|
||||
<p>Xác định cỡ mẫu là một bước thiết yếu và mang tính nền tảng trong đề cương nghiên cứu khoa học. Một cỡ mẫu được tính toán hợp lý không chỉ đảm bảo tính đại diện của mẫu cho quần thể nghiên cứu mà còn giúp tối ưu hóa việc sử dụng nguồn lực, thời gian và chi phí. Quan trọng hơn, nó đảm bảo nghiên cứu có đủ <strong>công suất thống kê (statistical power)</strong> để phát hiện ra các mối liên hệ hoặc sự khác biệt có ý nghĩa, từ đó gia tăng độ tin cậy và giá trị khoa học của kết quả nghiên cứu.</p>
|
||||
|
||||
<p>Công cụ tính toán cỡ mẫu được sử dụng trong nghiên cứu này được phát triển trên nền tảng ngôn ngữ lập trình R, một công cụ chuẩn mực và mạnh mẽ trong phân tích thống kê, kết hợp với Shiny framework để xây dựng giao diện người dùng tương tác và trực quan.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Mối quan hệ giữa Cỡ mẫu Nghiên cứu và Cỡ mẫu cho Kiểm định Giả thuyết</h2>
|
||||
|
||||
<p>Về bản chất, <strong>việc tính toán cỡ mẫu cho một nghiên cứu chính là quá trình xác định số lượng quan sát tối thiểu cần thiết để thực hiện các kiểm định giả thuyết thống kê (statistical hypothesis testing) đã đề ra trong mục tiêu nghiên cứu.</strong></p>
|
||||
|
||||
<p>Mỗi nghiên cứu khoa học đều nhằm trả lời một hoặc nhiều câu hỏi nghiên cứu, và các câu hỏi này được cụ thể hóa thành các giả thuyết thống kê (ví dụ: giả thuyết H₀ và giả thuyết Hₐ). Để có thể đưa ra kết luận bác bỏ hay không bác bỏ một giả thuyết, chúng ta cần sử dụng các phép kiểm định thống kê tương ứng (ví dụ: kiểm định t, khi bình phương, ANOVA).</p>
|
||||
|
||||
<p>Do đó, cỡ mẫu của nghiên cứu phải đủ lớn để phép kiểm định thống kê có đủ "năng lực" hay <strong>công suất thống kê <code>(1-β)</code></strong> để phát hiện ra một ảnh hưởng (effect size) thực sự tồn tại trong quần thể ở một <strong>mức ý nghĩa <code>(α)</code></strong> đã định trước. Nói cách khác, không có sự tách biệt giữa "cỡ mẫu nghiên cứu" và "cỡ mẫu cho kiểm định", chúng là một. Cỡ mẫu nghiên cứu được quyết định bởi yêu cầu của các kiểm định giả thuyết chính.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Xử lý trường hợp Nghiên cứu sử dụng nhiều Kiểm định Giả thuyết</h2>
|
||||
|
||||
<p>Trong thực tế, một nghiên cứu thường có nhiều hơn một mục tiêu và do đó cần thực hiện nhiều phép kiểm định giả thuyết khác nhau. Ví dụ, một nghiên cứu có thể vừa so sánh tỷ lệ giữa hai nhóm, vừa so sánh giá trị trung bình giữa ba nhóm, và vừa xem xét một mối tương quan. Mỗi phép kiểm định này, với các tham số khác nhau (mức ý nghĩa α, công suất 1-β, độ lớn ảnh hưởng dự kiến), sẽ yêu cầu một cỡ mẫu tối thiểu khác nhau.</p>
|
||||
|
||||
<p>Trong trường hợp này, quy trình chuẩn mực cần thực hiện như sau:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Xác định các mục tiêu chính:</strong> Liệt kê tất cả các giả thuyết thống kê chính yếu mà nghiên cứu cần phải trả lời. Đây là những mục tiêu quan trọng nhất, quyết định sự thành công của nghiên cứu.</li>
|
||||
<li><strong>Tính toán cỡ mẫu riêng lẻ:</strong> Thực hiện tính toán cỡ mẫu cho <strong>từng giả thuyết chính một cách độc lập</strong>.</li>
|
||||
<li><strong>Lựa chọn cỡ mẫu cuối cùng:</strong> Cỡ mẫu cuối cùng cho toàn bộ nghiên cứu sẽ là cỡ mẫu <strong>lớn nhất</strong> trong số các cỡ mẫu đã tính toán ở bước 2.</li>
|
||||
</ol>
|
||||
|
||||
<div class="reason">
|
||||
<p><strong>Lý do:</strong> Việc lựa chọn cỡ mẫu lớn nhất đảm bảo rằng nghiên cứu có đủ công suất thống kê cho tất cả các phân tích quan trọng. Nếu chọn một cỡ mẫu nhỏ hơn, nghiên cứu có thể có đủ năng lực để trả lời một số câu hỏi, nhưng sẽ không đủ năng lực (underpowered) để đưa ra kết luận đáng tin cậy cho các câu hỏi nghiên cứu khác yêu cầu cỡ mẫu lớn hơn. Điều này giúp tránh được sai lầm loại II (type II error) – không phát hiện được sự khác biệt trong khi nó thực sự tồn tại.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='tableauPlaceholder' id='viz1753192925819' style='position: relative'><noscript><a href='#'><img alt=' Prevalence of HIV, total (% of population ages 15-49) ' src='https://public.tableau.com/static/images/HI/HIVinWorld/PrevalenceofHIVtotalofpopulationages15-49/1_rss.png' style='border: none' /></a></noscript><object class='tableauViz' style='display:none;'><param name='host_url' value='https%3A%2F%2Fpublic.tableau.com%2F' /> <param name='embed_code_version' value='3' /> <param name='path' value='views/HIVinWorld/PrevalenceofHIVtotalofpopulationages15-49?:language=en-US&:embed=true&:sid=&:redirect=auth' /> <param name='toolbar' value='yes' /><param name='static_image' value='https://public.tableau.com/static/images/HI/HIVinWorld/PrevalenceofHIVtotalofpopulationages15-49/1.png' /> <param name='animate_transition' value='yes' /><param name='display_static_image' value='yes' /><param name='display_spinner' value='yes' /><param name='display_overlay' value='yes' /><param name='display_count' value='yes' /><param name='language' value='en-US' /></object></div> <script type='text/javascript'> var divElement = document.getElementById('viz1753192925819'); var vizElement = divElement.getElementsByTagName('object')[0]; vizElement.style.width='100%';vizElement.style.height=(divElement.offsetWidth*0.75)+'px'; var scriptElement = document.createElement('script'); scriptElement.src = 'https://public.tableau.com/javascripts/api/viz_v1.js'; vizElement.parentNode.insertBefore(scriptElement, vizElement); </script>
|
||||
<div class='tableauPlaceholder' id='viz1753193177428' style='position: relative'><noscript><a href='#'><img alt='BIẾN ĐỘNG DÂN SỐ VIỆT NAM TRONG CÁC THỜI KỲ ' src='https://public.tableau.com/static/images/HT/HT885SK6Z/1_rss.png' style='border: none' /></a></noscript><object class='tableauViz' style='display:none;'><param name='host_url' value='https%3A%2F%2Fpublic.tableau.com%2F' /> <param name='embed_code_version' value='3' /> <param name='path' value='shared/HT885SK6Z' /> <param name='toolbar' value='yes' /><param name='static_image' value='https://public.tableau.com/static/images/HT/HT885SK6Z/1.png' /> <param name='animate_transition' value='yes' /><param name='display_static_image' value='yes' /><param name='display_spinner' value='yes' /><param name='display_overlay' value='yes' /><param name='display_count' value='yes' /><param name='language' value='en-US' /></object></div> <script type='text/javascript'> var divElement = document.getElementById('viz1753193177428'); var vizElement = divElement.getElementsByTagName('object')[0]; vizElement.style.width='1016px';vizElement.style.height='991px'; var scriptElement = document.createElement('script'); scriptElement.src = 'https://public.tableau.com/javascripts/api/viz_v1.js'; vizElement.parentNode.insertBefore(scriptElement, vizElement); </script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/1ProportionTest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hướng dẫn Proportion Test một mẫu</title>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: auto; }}
|
||||
h1 {{ color: #0074D9; }}
|
||||
code {{ background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hướng dẫn Proportion Test một mẫu</h1>
|
||||
<p>Đây là hướng dẫn ngắn gọn cho việc tính toán cỡ mẫu trong kiểm định <strong>Proportion Test một mẫu</strong>.</p>
|
||||
|
||||
<h2>Giả thuyết kiểm định</h2>
|
||||
<p>\( H_0: p = p_0 \quad \text{{vs}} \quad H_1: p \ne p_0 \)</p>
|
||||
|
||||
<h2>Công thức tính cỡ mẫu</h2>
|
||||
<p>Cỡ mẫu \( n \) có thể được ước lượng bằng công thức:</p>
|
||||
<p>\[ n = \frac{{Z_{{1 - \alpha/2}}^2 \cdot p_0(1 - p_0)}}{{(p - p_0)^2}} \]</p>
|
||||
|
||||
<h2>Tham số</h2>
|
||||
<ul>
|
||||
<li>\( p_0 \): tỷ lệ giả định trong giả thuyết không</li>
|
||||
<li>\( p \): tỷ lệ kỳ vọng</li>
|
||||
<li>\( \alpha \): mức ý nghĩa</li>
|
||||
<li>\( Z \): giá trị tới hạn của phân phối chuẩn</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/2ProportionTest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hướng dẫn Proportion Test hai mẫu</title>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: auto; }}
|
||||
h1 {{ color: #0074D9; }}
|
||||
code {{ background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hướng dẫn Proportion Test hai mẫu</h1>
|
||||
<p>Đây là hướng dẫn ngắn gọn cho việc tính toán cỡ mẫu trong kiểm định <strong>Proportion Test hai mẫu</strong>.</p>
|
||||
|
||||
<h2>Giả thuyết kiểm định</h2>
|
||||
<p>\( H_0: p = p_0 \quad \text{{vs}} \quad H_1: p \ne p_0 \)</p>
|
||||
|
||||
<h2>Công thức tính cỡ mẫu</h2>
|
||||
<p>Cỡ mẫu \( n \) có thể được ước lượng bằng công thức:</p>
|
||||
<p>\[ n = \frac{{Z_{{1 - \alpha/2}}^2 \cdot p_0(1 - p_0)}}{{(p - p_0)^2}} \]</p>
|
||||
|
||||
<h2>Tham số</h2>
|
||||
<ul>
|
||||
<li>\( p_0 \): tỷ lệ giả định trong giả thuyết không</li>
|
||||
<li>\( p \): tỷ lệ kỳ vọng</li>
|
||||
<li>\( \alpha \): mức ý nghĩa</li>
|
||||
<li>\( Z \): giá trị tới hạn của phân phối chuẩn</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/Shapiro-Wilk/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,102 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kiểm định Shapiro-Wilk</title>
|
||||
<!-- Tải script MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0056b3;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.formula {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background-color: #e7f3fe;
|
||||
border-color: #d0eaff;
|
||||
color: #0c5460;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định Shapiro-Wilk (Shapiro-Wilk Test)</h1>
|
||||
|
||||
<p>
|
||||
Kiểm định Shapiro-Wilk là một trong những kiểm định thống kê mạnh mẽ và phổ biến nhất được sử dụng để kiểm tra xem một mẫu dữ liệu có tuân theo <strong>phân phối chuẩn (normal distribution)</strong> hay không.
|
||||
</p>
|
||||
|
||||
<h2>1. Giả thuyết kiểm định</h2>
|
||||
<ul>
|
||||
<li>\( H_0 \): Dữ liệu của mẫu tuân theo phân phối chuẩn.</li>
|
||||
<li>\( H_1 \): Dữ liệu của mẫu không tuân theo phân phối chuẩn.</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert">
|
||||
<strong>Lưu ý quan trọng:</strong> Trong kiểm định này, chúng ta thường mong muốn có một p-value lớn (p > 0.05) để không bác bỏ giả thuyết \(H_0\), từ đó có thể kết luận rằng dữ liệu tuân theo phân phối chuẩn và đủ điều kiện để sử dụng các kiểm định tham số.
|
||||
</div>
|
||||
|
||||
<h2>2. Thống kê kiểm định (W-statistic)</h2>
|
||||
|
||||
<p>
|
||||
Thống kê kiểm định W được tính toán dựa trên sự so sánh giữa các giá trị đã được sắp xếp của mẫu với các giá trị kỳ vọng tương ứng từ một phân phối chuẩn.
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
W = \frac{(\sum_{i=1}^n a_i x_{(i)})^2}{\sum_{i=1}^n (x_i - \bar{x})^2}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Trong đó:
|
||||
<ul>
|
||||
<li>\( x_{(i)} \): là giá trị thống kê thứ tự thứ \(i\) (dữ liệu đã được sắp xếp từ nhỏ đến lớn).</li>
|
||||
<li>\( \bar{x} \): là trung bình mẫu.</li>
|
||||
<li>\( a_i \): là các hằng số được tính toán từ trung bình, phương sai và hiệp phương sai của các thống kê thứ tự từ một mẫu có phân phối chuẩn.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
Giá trị W nằm trong khoảng từ 0 đến 1. Giá trị W càng gần 1 thì dữ liệu càng gần với phân phối chuẩn.
|
||||
</p>
|
||||
|
||||
<h2>3. Ứng dụng trong y tế công cộng</h2>
|
||||
|
||||
<p>
|
||||
Kiểm tra giả định về tính chuẩn của dữ liệu là một bước cực kỳ quan trọng trước khi thực hiện nhiều phân tích thống kê tham số.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Trước khi thực hiện T-test hoặc ANOVA:</strong> Các kiểm định này đều yêu cầu dữ liệu (hoặc phần dư) phải tuân theo phân phối chuẩn. Kiểm định Shapiro-Wilk giúp xác nhận giả định này.</li>
|
||||
<li><strong>Đánh giá dữ liệu lâm sàng:</strong> Kiểm tra xem các chỉ số sinh học như huyết áp, cholesterol, BMI trong một mẫu dân số có tuân theo phân phối chuẩn hay không. Nếu không, các phương pháp phi tham số hoặc phép biến đổi dữ liệu có thể cần được xem xét.</li>
|
||||
<li><strong>Kiểm tra phần dư của mô hình hồi quy:</strong> Trong hồi quy tuyến tính, một giả định quan trọng là phần dư (residuals) của mô hình phải tuân theo phân phối chuẩn. Shapiro-Wilk là một công cụ hiệu quả để kiểm tra giả định này.</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/onesamplettest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,81 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hướng dẫn tính cỡ mẫu kiểm định t một mẫu</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3><strong>Hướng dẫn tính toán cỡ mẫu cho kiểm định t một mẫu</strong></h3>
|
||||
<hr>
|
||||
<h3><strong>1. Kiểm định t một mẫu là gì?</strong></h3>
|
||||
<p>Kiểm định t một mẫu (One-sample t-test) là một phương pháp thống kê dùng để kiểm tra xem giá trị trung bình của một mẫu có khác biệt so với một giá trị đã biết hoặc giả thuyết hay không.</p>
|
||||
<p>Ví dụ: kiểm tra xem chiều cao trung bình của một nhóm học sinh có khác biệt so với chiều cao chuẩn quốc gia (ví dụ 165 cm) không.</p>
|
||||
<p><strong>Công thức kiểm định:</strong></p>
|
||||
<p><code>t = (x̄ - μ₀) / (s / √n)</code></p>
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li><code>x̄</code>: Giá trị trung bình mẫu</li>
|
||||
<li><code>μ₀</code>: Giá trị trung bình giả thuyết</li>
|
||||
<li><code>s</code>: Độ lệch chuẩn mẫu</li>
|
||||
<li><code>n</code>: Kích thước mẫu</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
<h3><strong>2. Phạm vi ứng dụng trong y tế công cộng</strong></h3>
|
||||
<p>Trong y tế công cộng, kiểm định t một mẫu thường được dùng để:</p>
|
||||
<ul>
|
||||
<li>So sánh trung bình của biến sức khỏe (BMI, huyết áp...) với giá trị chuẩn.</li>
|
||||
<li>Kiểm tra sự khác biệt giữa dữ liệu khảo sát và tiêu chuẩn y tế cộng đồng.</li>
|
||||
</ul>
|
||||
<p>Ví dụ:</p>
|
||||
<ul>
|
||||
<li>Kiểm tra cân nặng trung bình của trẻ em so với chuẩn WHO.</li>
|
||||
<li>So sánh mức ô nhiễm trung bình với giới hạn khuyến cáo.</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
<h3><strong>3. Công thức tính cỡ mẫu</strong></h3>
|
||||
<p><strong>Công thức:</strong></p>
|
||||
<p><code>n = ((Z₁₋⍺/2 + Z₁₋β)² × s²) / (μ₁ - μ₀)²</code></p>
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li><code>Z₁₋⍺/2</code>: Giá trị Z theo mức ý nghĩa <code>⍺</code></li>
|
||||
<li><code>Z₁₋β</code>: Giá trị Z theo độ mạnh kiểm định (power)</li>
|
||||
<li><code>s</code>: Độ lệch chuẩn dự kiến</li>
|
||||
<li><code>μ₁</code>: Giá trị trung bình kỳ vọng</li>
|
||||
<li><code>μ₀</code>: Giá trị trung bình giả thuyết</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
<h3><strong>4. Hướng dẫn tính toán</strong></h3>
|
||||
<p>Bạn cần cung cấp:</p>
|
||||
<ol>
|
||||
<li>Mức ý nghĩa (<code>⍺</code>): thường là 0.05</li>
|
||||
<li>Độ mạnh kiểm định (<code>1 - β</code>): thường là 0.8 hoặc 0.9</li>
|
||||
<li>Độ lệch chuẩn (<code>s</code>): lấy từ nghiên cứu trước</li>
|
||||
<li>Giá trị kỳ vọng (<code>μ₁</code>) và giá trị giả thuyết (<code>μ₀</code>)</li>
|
||||
</ol>
|
||||
<p><strong>Ví dụ:</strong></p>
|
||||
<ul>
|
||||
<li><code>⍺ = 0.05</code></li>
|
||||
<li><code>1 - β = 0.8</code></li>
|
||||
<li><code>s = 2</code></li>
|
||||
<li><code>μ₁ = 16</code></li>
|
||||
<li><code>μ₀ = 15</code></li>
|
||||
</ul>
|
||||
<p><strong>Tính:</strong></p>
|
||||
<p>Tra bảng Z:</p>
|
||||
<ul>
|
||||
<li><code>Z₀.975 = 1.96</code></li>
|
||||
<li><code>Z₀.8 = 0.84</code></li>
|
||||
</ul>
|
||||
<p>Áp dụng công thức:</p>
|
||||
<p><code>n = ((1.96 + 0.84)² × 4) / (1)² = (7.84 × 4) = 31.36</code></p>
|
||||
<p><strong>⇒ Cần ít nhất 32 trẻ em.</strong></p>
|
||||
|
||||
<hr>
|
||||
<h3><strong>5. Công cụ tính bằng R hoặc Shiny</strong></h3>
|
||||
<p>Bạn có thể dùng ứng dụng Shiny để tự động tính cỡ mẫu kiểm định t một mẫu. Ứng dụng này giúp thực hiện các bước tính toán nhanh chóng và trực quan.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/pairsamplettest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hướng dẫn kiểm định t ghép cặp</title>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; line-height: 1.6; }
|
||||
h3 { margin-top: 2em; color: #003366; }
|
||||
code { background-color: #f5f5f5; padding: 2px 4px; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h3><strong>Hướng dẫn tính toán cỡ mẫu cho kiểm định t ghép cặp (Paired t-test)</strong></h3>
|
||||
<hr>
|
||||
|
||||
<h3><strong>1. Kiểm định t ghép cặp là gì?</strong></h3>
|
||||
<p>Kiểm định t ghép cặp (Paired t-test) là phương pháp thống kê dùng để kiểm tra sự khác biệt trung bình giữa hai điều kiện, được đo lặp lại trên cùng đối tượng.</p>
|
||||
<p>Khác với t-test hai mẫu độc lập, t ghép cặp tập trung vào sự khác biệt giữa hai giá trị trong mỗi cặp, giúp giảm sai số và cần cỡ mẫu nhỏ hơn.</p>
|
||||
|
||||
<h3><strong>2. Phạm vi ứng dụng trong y tế công cộng</strong></h3>
|
||||
<ul>
|
||||
<li>So sánh huyết áp trước và sau điều trị.</li>
|
||||
<li>Đo ô nhiễm không khí sáng vs chiều cùng địa điểm.</li>
|
||||
<li>So sánh cân nặng trẻ em trước và sau chương trình dinh dưỡng.</li>
|
||||
</ul>
|
||||
|
||||
<h3><strong>3. Công thức kiểm định t ghép cặp</strong></h3>
|
||||
<p>Giá trị thống kê t được tính như sau:</p>
|
||||
<p style="text-align:center">
|
||||
$$ t = \frac{\bar{d} - \mu_d}{s_d / \sqrt{n}} $$
|
||||
</p>
|
||||
|
||||
<p><strong>Trong đó:</strong></p>
|
||||
<ul>
|
||||
<li>\(\bar{d}\): Trung bình của các hiệu số giữa hai điều kiện</li>
|
||||
<li>\(\mu_d\): Giá trị kỳ vọng của hiệu số (thường là 0)</li>
|
||||
<li>\(s_d\): Độ lệch chuẩn của hiệu số</li>
|
||||
<li>\(n\): Số cặp quan sát</li>
|
||||
</ul>
|
||||
|
||||
<h3><strong>4. Công thức tính cỡ mẫu</strong></h3>
|
||||
<p>Cỡ mẫu \(n\) được tính theo công thức:</p>
|
||||
<p style="text-align:center">
|
||||
$$ n = \frac{(Z_{1-\alpha/2} + Z_{1-\beta})^2 \cdot s^2}{\mu_d^2} $$
|
||||
</p>
|
||||
|
||||
<p><strong>Trong đó:</strong></p>
|
||||
<ul>
|
||||
<li>\(Z_{1-\alpha/2}\): Giá trị tới hạn theo mức ý nghĩa \(\alpha\)</li>
|
||||
<li>\(Z_{1-\beta}\): Giá trị tới hạn theo độ mạnh kiểm định</li>
|
||||
<li>\(s\): Độ lệch chuẩn của hiệu số</li>
|
||||
<li>\(\mu_d\): Khác biệt mong muốn phát hiện</li>
|
||||
</ul>
|
||||
|
||||
<h3><strong>5. Ví dụ minh họa</strong></h3>
|
||||
<p>Giả sử:</p>
|
||||
<ul>
|
||||
<li>\(\alpha = 0.05 \Rightarrow Z_{1-\alpha/2} = 1.96\)</li>
|
||||
<li>\(1-\beta = 0.8 \Rightarrow Z_{1-\beta} = 0.84\)</li>
|
||||
<li>\(s = 5\), \(\mu_d = 3\)</li>
|
||||
</ul>
|
||||
|
||||
<p>Áp dụng công thức:</p>
|
||||
<p style="text-align:center">
|
||||
$$ n = \frac{(1.96 + 0.84)^2 \cdot 5^2}{3^2} = \frac{7.84 \cdot 25}{9} = \frac{196}{9} \approx 21.78 $$
|
||||
</p>
|
||||
<p><strong>Vậy cần ít nhất 22 cặp quan sát.</strong></p>
|
||||
|
||||
<h3><strong>6. Tính bằng R với gói pwr</strong></h3>
|
||||
<p>Dùng hàm:</p>
|
||||
<pre><code>library(pwr)
|
||||
pwr.t.test(d = 3/5, sig.level = 0.05, power = 0.8, type = "paired")</code></pre>
|
||||
|
||||
<p>Với \(d = \mu_d / s = 3 / 5 = 0.6\)</p>
|
||||
|
||||
<h3><strong>7. Kết luận</strong></h3>
|
||||
<p>Kiểm định t ghép cặp hữu ích khi phân tích dữ liệu đo lặp. Tính đúng cỡ mẫu đảm bảo đủ mạnh để phát hiện khác biệt thực sự.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/twosamplettest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,116 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hướng dẫn tính cỡ mẫu cho kiểm định t hai mẫu</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
max-width: 960px;
|
||||
margin: auto;
|
||||
}
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
hr {
|
||||
margin: 2em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3><strong>Hướng dẫn tính toán cỡ mẫu cho kiểm định t hai mẫu</strong></h3>
|
||||
<hr>
|
||||
<h3><strong>1. Kiểm định t hai mẫu là gì?</strong></h3>
|
||||
<p>Kiểm định t hai mẫu (Two-sample t-test) là một phương pháp thống kê được sử dụng để so sánh giá trị trung bình của hai nhóm độc lập, nhằm kiểm tra xem sự khác biệt giữa hai giá trị trung bình này có ý nghĩa thống kê hay không.</p>
|
||||
<p>Ví dụ: So sánh mức độ hài lòng trung bình giữa hai nhóm bệnh nhân được điều trị bằng hai phương pháp khác nhau.</p>
|
||||
<p><strong>Công thức kiểm định t hai mẫu:</strong></p>
|
||||
<p>
|
||||
<code>
|
||||
t = (x̄₁ − x̄₂) / √[s<sub>p</sub>² (1/n₁ + 1/n₂)]
|
||||
</code>
|
||||
</p>
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>x̄₁, x̄₂: Trung bình nhóm 1 và nhóm 2</li>
|
||||
<li>s<sub>p</sub>²: Phương sai tổng hợp, tính theo:
|
||||
<br>
|
||||
<code>
|
||||
s<sub>p</sub>² = [(n₁−1)s₁² + (n₂−1)s₂²] / (n₁ + n₂ − 2)
|
||||
</code>
|
||||
</li>
|
||||
<li>s₁², s₂²: Phương sai từng nhóm</li>
|
||||
<li>n₁, n₂: Kích thước mẫu nhóm 1 và 2</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3><strong>2. Phạm vi ứng dụng trong y tế công cộng</strong></h3>
|
||||
<ul>
|
||||
<li>So sánh hiệu quả hai phương pháp điều trị</li>
|
||||
<li>Đánh giá sự khác biệt giữa nhóm điều trị và nhóm đối chứng</li>
|
||||
<li>Phân tích chỉ số sức khỏe giữa các nhóm, ví dụ người hút thuốc và không hút thuốc</li>
|
||||
</ul>
|
||||
<p><strong>Ví dụ:</strong></p>
|
||||
<ul>
|
||||
<li>So sánh cân nặng trung bình trẻ em có và không được bổ sung vi chất</li>
|
||||
<li>So sánh thời gian sống sót trung bình giữa hai nhóm điều trị ung thư</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3><strong>3. Công thức tính cỡ mẫu</strong></h3>
|
||||
<p><code>
|
||||
n = [2(Z<sub>1−α/2</sub> + Z<sub>1−β</sub>)² * s²] / (μ₁ − μ₂)²
|
||||
</code></p>
|
||||
<ul>
|
||||
<li>Z<sub>1−α/2</sub>: điểm tới hạn cho mức ý nghĩa α</li>
|
||||
<li>Z<sub>1−β</sub>: điểm tới hạn cho độ mạnh kiểm định</li>
|
||||
<li>s: độ lệch chuẩn tổng hợp</li>
|
||||
<li>μ₁, μ₂: trung bình của hai nhóm</li>
|
||||
<li>n: cỡ mẫu cho mỗi nhóm</li>
|
||||
</ul>
|
||||
<p><strong>Lưu ý:</strong> Tổng cỡ mẫu hai nhóm là <code>2n</code>.</p>
|
||||
<hr>
|
||||
<h3><strong>4. Hướng dẫn tính toán cỡ mẫu</strong></h3>
|
||||
<p><strong>Thông số cần biết:</strong></p>
|
||||
<ol>
|
||||
<li>Mức ý nghĩa (α), thường là 0.05</li>
|
||||
<li>Độ mạnh kiểm định (1−β), thường là 0.8 hoặc 0.9</li>
|
||||
<li>Độ lệch chuẩn s</li>
|
||||
<li>Sự khác biệt mong muốn μ₁ − μ₂</li>
|
||||
</ol>
|
||||
<p><strong>Ví dụ:</strong></p>
|
||||
<ul>
|
||||
<li>α = 0.05</li>
|
||||
<li>1−β = 0.8</li>
|
||||
<li>s = 3 (kg)</li>
|
||||
<li>μ₁ = 20, μ₂ = 18</li>
|
||||
</ul>
|
||||
<p><strong>Tính toán:</strong></p>
|
||||
<p>
|
||||
<code>
|
||||
n = [2(1.96 + 0.84)² * 9] / 4 = (2 × 7.84 × 9) / 4 = 141.12 / 4 = 35.28
|
||||
</code><br>
|
||||
→ Cần ít nhất <strong>36 đối tượng</strong> mỗi nhóm
|
||||
</p>
|
||||
<hr>
|
||||
<h3><strong>5. Tính bằng R hoặc Shiny</strong></h3>
|
||||
<p>Sử dụng hàm <code>pwr.t.test</code> từ package <code>pwr</code> trong R:</p>
|
||||
<pre><code>library(pwr)
|
||||
pwr.t.test(d = 0.5, sig.level = 0.05, power = 0.8, type = "two.sample")
|
||||
</code></pre>
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li><code>d = (μ₁ − μ₂)/s</code>: hiệu ứng chuẩn hóa</li>
|
||||
<li><code>sig.level</code>: mức ý nghĩa α</li>
|
||||
<li><code>power</code>: độ mạnh kiểm định (1−β)</li>
|
||||
<li><code>type = "two.sample"</code>: kiểm định t hai mẫu</li>
|
||||
</ul>
|
||||
<p>Kết quả trả về là cỡ mẫu cần thiết mỗi nhóm.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/Wilcoxon-Signed-Rank/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,124 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kiểm định Wilcoxon Signed-Rank</title>
|
||||
<!-- Tải script MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #0056b3;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.formula {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background-color: #e7f3fe;
|
||||
border-color: #d0eaff;
|
||||
color: #0c5460;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định Dấu-Hạng Wilcoxon (Wilcoxon Signed-Rank Test)</h1>
|
||||
|
||||
<p>
|
||||
Kiểm định Dấu-Hạng Wilcoxon là một kiểm định phi tham số được sử dụng để so sánh trung vị của hai mẫu có liên quan — ví dụ như dữ liệu trước và sau điều trị trên cùng một nhóm đối tượng. Nó là phương án thay thế cho <strong>kiểm định t ghép cặp</strong> khi giả định về phân phối chuẩn không được đáp ứng.
|
||||
</p>
|
||||
|
||||
<h2>1. Giả thuyết kiểm định</h2>
|
||||
<ul>
|
||||
<li>\( H_0: \) Trung vị của sự khác biệt giữa các cặp bằng 0.</li>
|
||||
<li>\( H_1: \) Trung vị của sự khác biệt khác 0 (hai phía), hoặc lớn hơn/nhỏ hơn 0 (một phía).</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Cách tính thống kê kiểm định</h2>
|
||||
|
||||
<p>
|
||||
Với \( n \) cặp quan sát \( (X_i, Y_i) \), ta xét hiệu số:
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
D_i = X_i - Y_i
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Bỏ qua các \( D_i = 0 \), sắp xếp các giá trị còn lại theo thứ tự tăng dần của \( |D_i| \) và gán hạng \( R_i \). Khi đó, thống kê kiểm định \( W \) là tổng hạng của các hiệu số dương:</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
W = \sum_{i: D_i > 0} R_i
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Giá trị \( W \) được so sánh với bảng phân phối Wilcoxon hoặc dùng xấp xỉ phân phối chuẩn khi \( n \) đủ lớn.
|
||||
</p>
|
||||
|
||||
<h2>3. Công thức ước lượng cỡ mẫu</h2>
|
||||
|
||||
<p>
|
||||
Cỡ mẫu cần thiết để phát hiện một hiệu ứng khác biệt với mức ý nghĩa \( \alpha \) và công suất \( 1 - \beta \) có thể được ước tính xấp xỉ bằng:
|
||||
</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n \approx \left( \frac{Z_{1 - \alpha/2} + Z_{1 - \beta}}{ES \cdot \sqrt{3}/\pi} \right)^2
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Trong đó:</p>
|
||||
<ul>
|
||||
<li>\( ES \): effect size (hiệu ứng chuẩn hoá, ví dụ: \( \delta / \sigma_D \))</li>
|
||||
<li>\( Z_{1 - \alpha/2} \): điểm tới hạn cho mức ý nghĩa (≈ 1.96 nếu \( \alpha = 0.05 \))</li>
|
||||
<li>\( Z_{1 - \beta} \): điểm tới hạn cho công suất mong muốn (≈ 0.84 nếu power = 80%)</li>
|
||||
</ul>
|
||||
<p>
|
||||
Lưu ý rằng đây là công thức xấp xỉ và việc tính toán chính xác hơn thường đòi hỏi mô phỏng.
|
||||
</p>
|
||||
|
||||
<h2>4. Ứng dụng trong y tế công cộng</h2>
|
||||
|
||||
<div class="alert">
|
||||
<p>Kiểm định Wilcoxon Signed-Rank được sử dụng khi:</p>
|
||||
<ul>
|
||||
<li>So sánh huyết áp trước và sau một can thiệp trên cùng một nhóm bệnh nhân, đặc biệt khi sự thay đổi không tuân theo phân phối chuẩn.</li>
|
||||
<li>Đánh giá hiệu quả của một chương trình giáo dục sức khỏe bằng cách so sánh điểm kiến thức trước và sau chương trình.</li>
|
||||
<li>So sánh mức độ lo âu của cùng một nhóm người trước và sau một chương trình tư vấn tâm lý.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Ưu điểm lớn nhất của kiểm định này là không yêu cầu giả định về phân phối chuẩn của dữ liệu, điều này rất hữu ích trong thực tế thu thập dữ liệu y tế, nơi dữ liệu thường bị lệch hoặc có các giá trị ngoại lai.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/Onesampleztest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,101 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hướng dẫn Kiểm định z Một Mẫu</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.formula {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #e67e22;
|
||||
}
|
||||
code {
|
||||
background-color: #f2f2f2;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- MathJax để hiển thị công thức toán học -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định z Một Mẫu (One-sample z-test)</h1>
|
||||
|
||||
<h2>Mục đích sử dụng</h2>
|
||||
<p>Kiểm định z một mẫu được sử dụng để so sánh trung bình mẫu với một giá trị trung bình giả định trong tổng thể, khi **độ lệch chuẩn của tổng thể đã biết**.</p>
|
||||
|
||||
<h2>Giả định</h2>
|
||||
<ul>
|
||||
<li>Mẫu ngẫu nhiên và độc lập.</li>
|
||||
<li>Tổng thể có phân phối chuẩn, hoặc cỡ mẫu đủ lớn (\( n \ge 30 \)).</li>
|
||||
<li>Đã biết độ lệch chuẩn tổng thể \( \sigma \).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Công thức kiểm định</h2>
|
||||
<p>Với mẫu có kích thước \( n \), trung bình \( \bar{X} \), kiểm định giả thuyết:</p>
|
||||
<ul>
|
||||
<li>\( H_0: \mu = \mu_0 \)</li>
|
||||
<li>\( H_1: \mu \ne \mu_0 \) (hoặc \( <, > \) tuỳ mục tiêu)</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
z = \frac{\bar{X} - \mu_0}{\sigma / \sqrt{n}}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<p>Giá trị z được so sánh với z tới hạn từ bảng phân phối chuẩn.</p>
|
||||
|
||||
<h2>Cách tính cỡ mẫu</h2>
|
||||
<p>Để xác định cỡ mẫu cần thiết nhằm phát hiện một độ chênh trung bình kỳ vọng \( \Delta = |\mu - \mu_0| \), dùng công thức:</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \left( \frac{(Z_{1-\alpha/2} + Z_{1-\beta}) \cdot \sigma}{\Delta} \right)^2
|
||||
\]
|
||||
Trong đó:
|
||||
<ul>
|
||||
<li>\( Z_{1-\alpha/2} \): z-score ứng với mức ý nghĩa (ví dụ: 1.96 nếu \( \alpha = 0.05 \))</li>
|
||||
<li>\( Z_{1-\beta} \): z-score ứng với sức mạnh kiểm định (power), ví dụ: 0.84 nếu power = 0.80</li>
|
||||
<li>\( \sigma \): độ lệch chuẩn đã biết</li>
|
||||
<li>\( \Delta \): mức sai khác trung bình muốn phát hiện</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Ví dụ</h2>
|
||||
<p>Giả sử chiều cao trung bình chuẩn là 165 cm. Bạn muốn kiểm định xem một nhóm sinh viên có chiều cao trung bình khác biệt không, với:</p>
|
||||
<ul>
|
||||
<li>\( \sigma = 6 \) cm</li>
|
||||
<li>\( \Delta = 2 \) cm</li>
|
||||
<li>\( \alpha = 0.05 \) → \( Z_{1-\alpha/2} = 1.96 \)</li>
|
||||
<li>Power = 0.80 → \( Z_{1-\beta} = 0.84 \)</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \left( \frac{(1.96 + 0.84) \cdot 6}{2} \right)^2 = (8.4)^2 = 70.56
|
||||
\]
|
||||
⇒ Cần ít nhất <strong>71 người</strong>.
|
||||
</div>
|
||||
|
||||
<h2>Thực hành</h2>
|
||||
<p>Bạn có thể sử dụng Shiny hoặc phần mềm R để nhập giá trị và tính toán tự động.</p>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/pairedztest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,89 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Hướng dẫn Kiểm định z Ghép Cặp (Paired z-test)</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.formula {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #2980b9;
|
||||
}
|
||||
code {
|
||||
background-color: #f2f2f2;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- MathJax -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định z Ghép Cặp (Paired z-test)</h1>
|
||||
|
||||
<h2>Mục đích sử dụng</h2>
|
||||
<p>Kiểm định z ghép cặp dùng để so sánh trung bình hiệu (chênh lệch) giữa hai phép đo lặp trên cùng một đối tượng khi biết độ lệch chuẩn của hiệu số tổng thể.</p>
|
||||
|
||||
<h2>Giả định</h2>
|
||||
<ul>
|
||||
<li>Dữ liệu ghép cặp độc lập.</li>
|
||||
<li>Độ lệch chuẩn của hiệu \( \sigma_D \) đã biết.</li>
|
||||
<li>Hiệu số giữa các cặp tuân theo phân phối chuẩn.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Công thức kiểm định</h2>
|
||||
<p>Với \( n \) cặp, hiệu số trung bình \( \bar{D} \), kiểm định giả thuyết:</p>
|
||||
<ul>
|
||||
<li>\( H_0: \mu_D = 0 \) (không có sự khác biệt trung bình)</li>
|
||||
<li>\( H_1: \mu_D \neq 0 \)</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
z = \frac{\bar{D} - 0}{\sigma_D / \sqrt{n}} = \frac{\bar{D}}{\sigma_D / \sqrt{n}}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<h2>Cách tính cỡ mẫu</h2>
|
||||
<p>Để xác định cỡ mẫu cần thiết phát hiện một sai khác trung bình kỳ vọng \( \Delta \) với mức ý nghĩa \( \alpha \) và power \( 1-\beta \):</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \left( \frac{(Z_{1-\alpha/2} + Z_{1-\beta}) \cdot \sigma_D}{\Delta} \right)^2
|
||||
\]
|
||||
</div>
|
||||
|
||||
<h2>Ví dụ</h2>
|
||||
<p>Giả sử bạn muốn phát hiện sự thay đổi trung bình \( \Delta = 3 \) điểm, với:</p>
|
||||
<ul>
|
||||
<li>Độ lệch chuẩn hiệu \( \sigma_D = 6 \)</li>
|
||||
<li>\( \alpha = 0.05 \) → \( Z_{1-\alpha/2} = 1.96 \)</li>
|
||||
<li>Power = 0.80 → \( Z_{1-\beta} = 0.84 \)</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = \left( \frac{(1.96 + 0.84) \times 6}{3} \right)^2 = (5.6)^2 = 31.36
|
||||
\]
|
||||
⇒ Cần ít nhất <strong>32 cặp quan sát</strong>.
|
||||
</div>
|
||||
|
||||
<h2>Thực hành</h2>
|
||||
<p>Bạn có thể áp dụng kiểm định này trong các phân tích dữ liệu ghép cặp khi biết độ lệch chuẩn tổng thể của hiệu số.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1 +0,0 @@
|
||||
<iframe src="https://shiny.huph.edu.vn/sample-apps/samplesize/twosampleztest/" style="width:100%;height:100vh;border:none;"></iframe>
|
@@ -1,103 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hướng dẫn Kiểm định z Hai Mẫu</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.formula {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #27ae60;
|
||||
}
|
||||
code {
|
||||
background-color: #f2f2f2;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- MathJax để hiển thị công thức -->
|
||||
<script id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Kiểm định z Hai Mẫu (Two-sample z-test)</h1>
|
||||
|
||||
<h2>Mục đích sử dụng</h2>
|
||||
<p>Dùng để so sánh trung bình của hai tổng thể độc lập khi **biết trước độ lệch chuẩn tổng thể**. Ví dụ:</p>
|
||||
<ul>
|
||||
<li>So sánh điểm trung bình giữa nam và nữ khi biết trước phương sai.</li>
|
||||
<li>So sánh hai quy trình sản xuất khác nhau về năng suất.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Giả định</h2>
|
||||
<ul>
|
||||
<li>Hai mẫu độc lập.</li>
|
||||
<li>Phân phối tổng thể chuẩn hoặc cỡ mẫu đủ lớn (\( n_1, n_2 \ge 30 \)).</li>
|
||||
<li>Đã biết trước độ lệch chuẩn của hai tổng thể: \( \sigma_1, \sigma_2 \).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Công thức kiểm định</h2>
|
||||
<p>Với hai mẫu có kích thước \( n_1 \), \( n_2 \), trung bình mẫu \( \bar{X}_1 \), \( \bar{X}_2 \), độ lệch chuẩn tổng thể \( \sigma_1, \sigma_2 \), kiểm định giả thuyết:</p>
|
||||
<ul>
|
||||
<li>\( H_0: \mu_1 = \mu_2 \)</li>
|
||||
<li>\( H_1: \mu_1 \ne \mu_2 \)</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
z = \frac{\bar{X}_1 - \bar{X}_2}{\sqrt{ \frac{\sigma_1^2}{n_1} + \frac{\sigma_2^2}{n_2} }}
|
||||
\]
|
||||
</div>
|
||||
|
||||
<h2>Cách tính cỡ mẫu</h2>
|
||||
<p>Giả sử \( \sigma_1 = \sigma_2 = \sigma \) (phương sai hai nhóm bằng nhau), cỡ mẫu mỗi nhóm cần để phát hiện sai khác trung bình \( \Delta = |\mu_1 - \mu_2| \) với mức ý nghĩa và power cho trước:</p>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = 2 \times \left( \frac{(Z_{1-\alpha/2} + Z_{1-\beta}) \cdot \sigma}{\Delta} \right)^2
|
||||
\]
|
||||
Trong đó:
|
||||
<ul>
|
||||
<li>\( Z_{1-\alpha/2} \): điểm z cho mức ý nghĩa hai phía (ví dụ: 1.96 nếu \( \alpha = 0.05 \))</li>
|
||||
<li>\( Z_{1-\beta} \): điểm z cho power mong muốn (ví dụ: 0.84 nếu power = 0.80)</li>
|
||||
<li>\( \Delta \): chênh lệch trung bình mong muốn phát hiện</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Ví dụ</h2>
|
||||
<p>So sánh trung bình điểm thi giữa hai nhóm:</p>
|
||||
<ul>
|
||||
<li>\( \sigma = 10 \)</li>
|
||||
<li>\( \Delta = 5 \)</li>
|
||||
<li>\( \alpha = 0.05 \) → \( Z_{1-\alpha/2} = 1.96 \)</li>
|
||||
<li>Power = 0.80 → \( Z_{1-\beta} = 0.84 \)</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula">
|
||||
\[
|
||||
n = 2 \times \left( \frac{(1.96 + 0.84) \cdot 10}{5} \right)^2
|
||||
= 2 \times \left( \frac{28}{5} \right)^2
|
||||
= 2 \times (5.6)^2 = 2 \times 31.36 = 62.72
|
||||
\]
|
||||
⇒ Mỗi nhóm cần ít nhất <strong>63 người</strong>.
|
||||
</div>
|
||||
|
||||
<h2>Thực hành</h2>
|
||||
<p>Bạn có thể sử dụng form bên trên để nhập dữ liệu và tính toán kiểm định.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
216
samplesize/diagnostest/app.R
Normal file
216
samplesize/diagnostest/app.R
Normal file
@@ -0,0 +1,216 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO NGHIÊN CỨU GIÁ TRỊ CHẨN ĐOÁN
|
||||
# - Bao gồm tính toán cỡ mẫu và phân tích ảnh hưởng của tỷ lệ hiện mắc.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Nghiên Cứu Giá trị Chẩn đoán"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
|
||||
h5("Thông số Xét nghiệm Dự kiến"),
|
||||
sliderInput("sens", "Độ nhạy (Sensitivity) dự kiến:", value = 0.90, min = 0.5, max = 0.99, step = 0.01),
|
||||
sliderInput("spec", "Độ đặc hiệu (Specificity) dự kiến:", value = 0.98, min = 0.5, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
h5("Thông số Quần thể & Độ chính xác"),
|
||||
sliderInput("prev", "Tỷ lệ hiện mắc (Prevalence) của bệnh:", value = 0.02, min = 0.001, max = 0.99, step = 0.001),
|
||||
sliderInput("d", "Độ chính xác mong muốn (Sai số d):", value = 0.05, min = 0.01, max = 0.15, step = 0.005),
|
||||
sliderInput("conf_level", "Độ tin cậy:", value = 0.95, min = 0.80, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
actionButton("go_diag", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "diag_results_tabs",
|
||||
type = "pills",
|
||||
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("diag_result_text"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Phân tích & Đồ thị",
|
||||
withSpinner(plotOutput("diag_prevalence_plot"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giải thích Tham số",
|
||||
uiOutput("diag_params_ui")),
|
||||
|
||||
tabPanel("Công thức & Ví dụ",
|
||||
h4("Công thức tính toán"),
|
||||
p("Quá trình tính toán gồm 2 bước:"),
|
||||
p(strong("Bước 1: Tính số người có bệnh (\\(n_{dis}\\)) và không bệnh (\\(n_{hea}\\)) cần thiết")),
|
||||
withMathJax(HTML("Công thức chung: $$n = \\frac{Z^2_{1-\\alpha/2} \\cdot p(1-p)}{d^2}$$")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("Để tính \\(n_{dis}\\), thay \\(p\\) bằng Độ nhạy dự kiến (Se).")),
|
||||
withMathJax(tags$li("Để tính \\(n_{hea}\\), thay \\(p\\) bằng Độ đặc hiệu dự kiến (Sp)."))
|
||||
),
|
||||
|
||||
p(strong("Bước 2: Tính tổng cỡ mẫu (N) cần sàng lọc từ tỷ lệ hiện mắc (Prevalence)")),
|
||||
withMathJax(HTML("$$N_{sens} = \\frac{n_{dis}}{\\text{Prevalence}}$$")),
|
||||
withMathJax(HTML("$$N_{spec} = \\frac{n_{hea}}{1 - \\text{Prevalence}}$$")),
|
||||
withMathJax(HTML("Cỡ mẫu cuối cùng là giá trị lớn hơn: $$N = \\max(N_{sens}, N_{spec})$$")),
|
||||
|
||||
hr(),
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn đánh giá độ chính xác của một xét nghiệm nhanh mới để phát hiện bệnh lao (TB) tại cộng đồng."),
|
||||
tags$ul(
|
||||
tags$li(strong("Độ nhạy & đặc hiệu dự kiến:"), withMathJax(HTML(" Dựa trên y văn, họ kỳ vọng Se = 90% và Sp = 98%."))),
|
||||
tags$li(strong("Độ chính xác mong muốn (d):"), withMathJax(HTML(" Khoảng tin cậy 95% cho các ước tính này không được rộng hơn \\(\\pm 5\\)% (d=0.05)."))),
|
||||
tags$li(strong("Tỷ lệ hiện mắc (Prevalence):"), withMathJax(HTML(" Tỷ lệ mắc lao trong vùng nghiên cứu là 2% (Prevalence = 0.02).")))
|
||||
),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_diag <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_diag, {
|
||||
# Tính toán cơ bản
|
||||
z_alpha <- qnorm(1 - (1 - input$conf_level) / 2)
|
||||
|
||||
n_disease <- ceiling((z_alpha^2 * input$sens * (1 - input$sens)) / input$d^2)
|
||||
n_healthy <- ceiling((z_alpha^2 * input$spec * (1 - input$spec)) / input$d^2)
|
||||
|
||||
n_from_sens <- ceiling(n_disease / input$prev)
|
||||
n_from_spec <- ceiling(n_healthy / (1 - input$prev))
|
||||
|
||||
n_total <- max(n_from_sens, n_from_spec)
|
||||
|
||||
rv_diag$n_disease <- n_disease
|
||||
rv_diag$n_healthy <- n_healthy
|
||||
rv_diag$N <- n_total
|
||||
|
||||
# Dữ liệu cho biểu đồ phân tích độ nhạy
|
||||
prev_range <- seq(0.001, 0.99, length.out = 200)
|
||||
|
||||
plot_data <- tibble(
|
||||
prevalence = prev_range,
|
||||
N_from_sens = ceiling(n_disease / prevalence),
|
||||
N_from_spec = ceiling(n_healthy / (1 - prevalence))
|
||||
) %>%
|
||||
mutate(N_total = pmax(N_from_sens, N_from_spec))
|
||||
|
||||
rv_diag$plot_data <- plot_data
|
||||
})
|
||||
|
||||
output$diag_result_text <- renderUI({
|
||||
if (input$go_diag == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_diag$N)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán"),
|
||||
p("Dựa trên các tham số bạn đã nhập, để đạt được độ chính xác mong muốn, nghiên cứu của bạn cần:"),
|
||||
|
||||
fluidRow(
|
||||
column(6, tags$div(class="alert alert-warning", style="text-align: center;",
|
||||
h5("Số người CÓ BỆNH tối thiểu"),
|
||||
h3(rv_diag$n_disease)
|
||||
)),
|
||||
column(6, tags$div(class="alert alert-success", style="text-align: center;",
|
||||
h5("Số người KHÔNG BỆNH tối thiểu"),
|
||||
h3(rv_diag$n_healthy)
|
||||
))
|
||||
),
|
||||
|
||||
hr(),
|
||||
h4("Tổng số người cần sàng lọc"),
|
||||
p(paste0("Với tỷ lệ hiện mắc của bệnh là ", scales::percent(input$prev, accuracy = 0.1),
|
||||
", để tìm đủ số lượng người bệnh và không bệnh nói trên, bạn cần phải sàng lọc tổng cộng:")),
|
||||
|
||||
tags$h2(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_diag$N, " người")
|
||||
)
|
||||
})
|
||||
|
||||
output$diag_prevalence_plot <- renderPlot({
|
||||
req(rv_diag$plot_data)
|
||||
|
||||
# Giới hạn trục y để dễ nhìn hơn
|
||||
max_y <- min(quantile(rv_diag$plot_data$N_total, 0.99, na.rm=TRUE) * 1.2, max(rv_diag$plot_data$N_total, na.rm=TRUE))
|
||||
if(is.infinite(max_y)) max_y <- 5 * rv_diag$N
|
||||
|
||||
|
||||
ggplot(rv_diag$plot_data, aes(x = prevalence)) +
|
||||
geom_line(aes(y = N_from_sens, color = "Dựa trên Độ nhạy"), size = 1.2) +
|
||||
geom_line(aes(y = N_from_spec, color = "Dựa trên Độ đặc hiệu"), size = 1.2) +
|
||||
geom_vline(xintercept = input$prev, linetype = "dashed", color = "red", size = 1) +
|
||||
annotate("text", x = input$prev, y = max_y * 0.9,
|
||||
label = paste0("Tỷ lệ hiện mắc đã chọn\n(", scales::percent(input$prev, accuracy = 0.1), ")"),
|
||||
color = "red", hjust = if_else(input$prev > 0.5, 1.1, -0.1)) +
|
||||
scale_color_manual(values = c("Dựa trên Độ nhạy" = "orange", "Dựa trên Độ đặc hiệu" = "seagreen")) +
|
||||
scale_x_continuous(labels = scales::percent) +
|
||||
scale_y_log10(labels = scales::comma) + # Trục log giúp dễ nhìn hơn với các giá trị lớn
|
||||
coord_cartesian(ylim = c(10, max_y)) +
|
||||
labs(
|
||||
title = "Ảnh hưởng của Tỷ lệ hiện mắc lên Tổng Cỡ mẫu (N)",
|
||||
subtitle = "Đường cong cho thấy số người cần sàng lọc để đạt đủ số ca bệnh (cam) hoặc ca không bệnh (xanh)",
|
||||
x = "Tỷ lệ hiện mắc (Prevalence)",
|
||||
y = "Tổng Cỡ mẫu cần sàng lọc (trục Log)",
|
||||
color = "Cỡ mẫu yêu cầu:"
|
||||
) +
|
||||
theme_minimal(base_size = 14) +
|
||||
theme(legend.position = "bottom")
|
||||
})
|
||||
|
||||
output$diag_params_ui <- renderUI({
|
||||
tagList(
|
||||
h4("Giải thích các tham số chính"),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Độ nhạy (Sensitivity)"),
|
||||
p("Là khả năng của xét nghiệm phát hiện chính xác những người ", tags$b("thực sự có bệnh."), " Nó được tính bằng (Số ca dương tính thật) / (Tổng số người có bệnh). Một độ nhạy 90% nghĩa là xét nghiệm sẽ phát hiện được 90 trong số 100 người có bệnh.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Độ đặc hiệu (Specificity)"),
|
||||
p("Là khả năng của xét nghiệm xác định chính xác những người ", tags$b("thực sự không có bệnh."), " Nó được tính bằng (Số ca âm tính thật) / (Tổng số người không có bệnh). Một độ đặc hiệu 98% nghĩa là xét nghiệm sẽ cho kết quả âm tính đúng cho 98 trong số 100 người không có bệnh.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Tỷ lệ hiện mắc (Prevalence)"),
|
||||
p("Là tỷ lệ phần trăm người đang mắc bệnh trong một quần thể tại một thời điểm nhất định. Tham số này ", tags$strong("cực kỳ quan trọng"), " vì nó quyết định bạn phải sàng lọc bao nhiêu người để tìm đủ số ca bệnh cần thiết cho nghiên cứu, đặc biệt là với các bệnh hiếm.")
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
187
samplesize/equivalence/app.R
Normal file
187
samplesize/equivalence/app.R
Normal file
@@ -0,0 +1,187 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO NGHIÊN CỨU TƯƠNG ĐƯƠNG (EQUIVALENCE)
|
||||
# - So sánh hai giá trị trung bình bằng phương pháp Two One-Sided Tests (TOST).
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu: Nghiên cứu Tương đương (So sánh 2 Trung bình)"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
|
||||
h5("Tham số Hiệu ứng & Biến thiên"),
|
||||
numericInput("delta_eq", "Biên tương đương (±δ):", value = 5, min = 0),
|
||||
numericInput("sd_eq", "Độ lệch chuẩn chung (σ):", value = 15, min = 0),
|
||||
numericInput("diff_eq", "Khác biệt trung bình kỳ vọng (μT - μS):", value = 0),
|
||||
helpText("Giá trị 0 có nghĩa là giả định hai nhóm có hiệu quả chính xác như nhau."),
|
||||
|
||||
hr(),
|
||||
h5("Tham số Kiểm định"),
|
||||
sliderInput("alpha_eq", "Mức ý nghĩa (α, cho mỗi phía):", value = 0.05, min = 0.01, max = 0.1, step = 0.01),
|
||||
sliderInput("power_eq", "Công suất mong muốn (1 - β):", value = 0.8, min = 0.5, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
actionButton("go_eq", "Tính toán Cỡ mẫu", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "eq_results_tabs",
|
||||
type = "pills",
|
||||
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("eq_result_text"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Phân tích & Đồ thị",
|
||||
withSpinner(plotOutput("eq_margin_plot"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giải thích Tham số",
|
||||
uiOutput("eq_params_ui")),
|
||||
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê: Two One-Sided Tests (TOST)"),
|
||||
p("Để chứng minh tương đương, chúng ta phải bác bỏ đồng thời CẢ HAI giả thuyết không sau đây:"),
|
||||
p(strong("1. Giả thuyết H01: Can thiệp mới thua kém đáng kể.")),
|
||||
withMathJax(HTML("$$H_{01}: \\mu_T - \\mu_S \\le -\\delta$$")),
|
||||
p(strong("2. Giả thuyết H02: Can thiệp mới vượt trội đáng kể.")),
|
||||
withMathJax(HTML("$$H_{02}: \\mu_T - \\mu_S \\ge +\\delta$$")),
|
||||
p("Chỉ khi bác bỏ được cả hai, chúng ta mới có thể chấp nhận giả thuyết thay thế \\(H_a\\):"),
|
||||
withMathJax(HTML("$$H_a: -\\delta < \\mu_T - \\mu_S < +\\delta$$")),
|
||||
|
||||
hr(),
|
||||
h4("Công thức tính cỡ mẫu (mỗi nhóm)"),
|
||||
p("Công thức cho kiểm định tương đương, giả định khác biệt kỳ vọng là 0:"),
|
||||
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{\\delta^2}$$")),
|
||||
p("Công thức tổng quát hơn khi khác biệt kỳ vọng \\(|\\mu_T - \\mu_S| > 0\\):"),
|
||||
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{(\\delta - |\\mu_T - \\mu_S|)^2}$$")),
|
||||
|
||||
hr(),
|
||||
h4("Ví dụ Y tế công cộng (Nghiên cứu Tương đương sinh học)"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một công ty dược phẩm sản xuất một phiên bản thuốc generic (T) của một loại thuốc hạ huyết áp nổi tiếng (S). Họ cần chứng minh rằng thuốc generic có hiệu quả tương đương sinh học với thuốc gốc. Tiêu chí tương đương được định nghĩa là sự khác biệt trong mức giảm huyết áp trung bình phải nằm trong khoảng \\(\\pm 5\\) mmHg."),
|
||||
p(strong("Tham số:")),
|
||||
tags$ul(
|
||||
tags$li("Biên tương đương \\(\\delta = 5\\) mmHg."),
|
||||
tags$li("Độ lệch chuẩn \\(\\sigma = 15\\) mmHg."),
|
||||
tags$li("Kỳ vọng sự khác biệt là 0."),
|
||||
tags$li("Mức ý nghĩa \\(\\alpha = 0.05\\) và công suất 80%.")
|
||||
),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_eq <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_eq, {
|
||||
# Validate input: Khác biệt kỳ vọng phải nhỏ hơn biên tương đương
|
||||
validate(
|
||||
need(abs(input$diff_eq) < input$delta_eq,
|
||||
"Lỗi: Khác biệt trung bình kỳ vọng phải nằm trong biên tương đương ( |μT - μS| < δ ). Nếu không, không thể chứng minh tương đương.")
|
||||
)
|
||||
|
||||
# Tính toán
|
||||
z_alpha <- qnorm(1 - input$alpha_eq)
|
||||
z_beta <- qnorm(input$power_eq) # Với TOST, thường dùng Z_beta thay vì Z_{beta/2}
|
||||
|
||||
numerator <- 2 * (input$sd_eq^2) * (z_alpha + z_beta)^2
|
||||
denominator <- (input$delta_eq - abs(input$diff_eq))^2
|
||||
|
||||
n_per_group <- ceiling(numerator / denominator)
|
||||
|
||||
rv_eq$n <- n_per_group
|
||||
|
||||
# Dữ liệu cho biểu đồ
|
||||
margin_range <- seq(abs(input$diff_eq) + 0.1, input$delta_eq * 1.5, length.out = 100)
|
||||
|
||||
plot_data <- data.frame(
|
||||
margin = margin_range,
|
||||
n = ceiling(2 * (input$sd_eq^2) * (z_alpha + z_beta)^2 / (margin_range - abs(input$diff_eq))^2)
|
||||
)
|
||||
rv_eq$plot_data <- plot_data
|
||||
})
|
||||
|
||||
output$eq_result_text <- renderUI({
|
||||
if (input$go_eq == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán Cỡ mẫu' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_eq$n)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán cỡ mẫu"),
|
||||
p("Với các tham số đã cho, để chứng minh tính tương đương, nghiên cứu của bạn cần:"),
|
||||
|
||||
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_eq$n, " người tham gia MỖI NHÓM"),
|
||||
p(style = "text-align: center; font-size: 1.2em;",
|
||||
"Tổng cỡ mẫu cần thiết: ", tags$b(rv_eq$n * 2)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
output$eq_margin_plot <- renderPlot({
|
||||
req(rv_eq$plot_data)
|
||||
|
||||
ggplot(rv_eq$plot_data, aes(x = margin, y = n)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_vline(xintercept = input$delta_eq, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = "Ảnh hưởng của Biên tương đương (δ) lên Cỡ mẫu",
|
||||
subtitle = "Biên càng lớn (khoảng tương đương rộng hơn), cỡ mẫu yêu cầu càng nhỏ",
|
||||
x = "Biên tương đương (δ)",
|
||||
y = "Cỡ mẫu mỗi nhóm (n)"
|
||||
) +
|
||||
annotate("text", x = input$delta_eq * 1.05, y = max(rv_eq$plot_data$n) * 0.9,
|
||||
label = paste0("Biên đã chọn\nδ = ", input$delta_eq),
|
||||
color = "red", hjust = 0) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$eq_params_ui <- renderUI({
|
||||
tagList(
|
||||
h4("Giải thích các tham số chính"),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Biên tương đương (Equivalence Margin - ±δ)"),
|
||||
p("Đây là tham số định nghĩa 'vùng tương đương'. Nó xác định một khoảng chênh lệch (từ ", tags$b("-δ"), " đến ", tags$b("+δ"), ") mà trong đó, sự khác biệt giữa hai can thiệp được coi là không có ý nghĩa về mặt lâm sàng. Việc lựa chọn δ phải dựa trên cơ sở khoa học vững chắc.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Phương pháp Two One-Sided Tests (TOST)"),
|
||||
p("Đây là phương pháp thống kê tiêu chuẩn để chứng minh tương đương. Thay vì một kiểm định hai phía thông thường (nhằm chứng minh sự khác biệt), TOST thực hiện hai kiểm định một phía để chứng minh ", tags$strong("sự thiếu vắng một sự khác biệt có ý nghĩa."), " Bạn phải chứng minh rằng khoảng tin cậy 90% (tương ứng với hai kiểm định một phía tại α=0.05) của sự khác biệt nằm hoàn toàn bên trong biên tương đương [-\\(\\delta\\), +\\(\\delta\\)].")
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
185
samplesize/non-inferiority/app.R
Normal file
185
samplesize/non-inferiority/app.R
Normal file
@@ -0,0 +1,185 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO NGHIÊN CỨU KHÔNG THUA KÉM (NON-INFERIORITY)
|
||||
# - So sánh hai giá trị trung bình.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu: Nghiên cứu Không thua kém (So sánh 2 Trung bình)"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
|
||||
h5("Tham số Hiệu ứng & Biến thiên"),
|
||||
numericInput("delta", "Biên không thua kém (δ):", value = 5, min = 0),
|
||||
numericInput("sd", "Độ lệch chuẩn chung (σ):", value = 15, min = 0),
|
||||
numericInput("diff", "Khác biệt trung bình kỳ vọng (μT - μS):", value = 0),
|
||||
helpText("Giá trị 0 có nghĩa là giả định hai nhóm có hiệu quả tương đương."),
|
||||
|
||||
hr(),
|
||||
h5("Tham số Kiểm định"),
|
||||
sliderInput("alpha_ni", "Mức ý nghĩa (α, một phía):", value = 0.05, min = 0.01, max = 0.1, step = 0.01),
|
||||
sliderInput("power_ni", "Công suất mong muốn (1 - β):", value = 0.8, min = 0.5, max = 0.99, step = 0.01),
|
||||
|
||||
hr(),
|
||||
actionButton("go_ni", "Tính toán Cỡ mẫu", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "ni_results_tabs",
|
||||
type = "pills",
|
||||
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("ni_result_text"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Phân tích & Đồ thị",
|
||||
withSpinner(plotOutput("ni_margin_plot"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giải thích Tham số",
|
||||
uiOutput("ni_params_ui")),
|
||||
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
p("Mục tiêu là chứng minh can thiệp mới (Test) không thua kém can thiệp chuẩn (Standard) một cách đáng kể."),
|
||||
p(strong("Giả thuyết không (H0): Can thiệp mới THUA KÉM."), "Sự khác biệt hiệu quả lớn hơn hoặc bằng một ngưỡng \\(\\delta\\) đã định trước."),
|
||||
withMathJax(HTML("$$H_0: \\mu_S - \\mu_T \\ge \\delta$$")),
|
||||
p(strong("Giả thuyết thay thế (Ha): Can thiệp mới KHÔNG THUA KÉM.")),
|
||||
withMathJax(HTML("$$H_a: \\mu_S - \\mu_T < \\delta$$")),
|
||||
|
||||
hr(),
|
||||
h4("Công thức tính cỡ mẫu (mỗi nhóm)"),
|
||||
withMathJax(HTML("$$n = \\frac{2\\sigma^2(Z_{\\alpha} + Z_{\\beta})^2}{(\\mu_T - \\mu_S - \\delta)^2}$$")),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("\\(\\delta\\) là biên không thua kém (non-inferiority margin).")),
|
||||
withMathJax(tags$li("\\(\\sigma\\) là độ lệch chuẩn chung của kết quả.")),
|
||||
withMathJax(tags$li("\\(Z_{\\alpha}\\) là Z-score một phía (ví dụ: 1.645 cho \\(\\alpha=0.05\\)).")),
|
||||
withMathJax(tags$li("\\(Z_{\\beta}\\) là Z-score của công suất (ví dụ: 0.84 cho power=80%).")),
|
||||
withMathJax(tags$li("\\(\\mu_T - \\mu_S\\) là sự khác biệt trung bình thực sự kỳ vọng giữa hai nhóm."))
|
||||
),
|
||||
|
||||
hr(),
|
||||
h4("Ví dụ Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một thuốc hạ huyết áp mới (T) được kỳ vọng có hiệu quả tương tự thuốc chuẩn (S) nhưng dễ sử dụng hơn. Các nhà lâm sàng đồng ý rằng nếu thuốc T làm giảm huyết áp kém hơn không quá 5 mmHg so với thuốc S, nó vẫn được coi là chấp nhận được (không thua kém). Độ lệch chuẩn của thay đổi huyết áp là 15 mmHg."),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_ni <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_ni, {
|
||||
# Tính toán
|
||||
z_alpha <- qnorm(1 - input$alpha_ni)
|
||||
z_beta <- qnorm(input$power_ni)
|
||||
|
||||
numerator <- 2 * (input$sd^2) * (z_alpha + z_beta)^2
|
||||
denominator <- (input$diff - input$delta)^2
|
||||
|
||||
validate(
|
||||
need(denominator > 0, "Lỗi: Mẫu số của công thức bằng 0. Khác biệt kỳ vọng không thể bằng biên không thua kém.")
|
||||
)
|
||||
|
||||
n_per_group <- ceiling(numerator / denominator)
|
||||
|
||||
rv_ni$n <- n_per_group
|
||||
|
||||
# Dữ liệu cho biểu đồ
|
||||
margin_range <- seq(input$delta * 0.5, input$delta * 1.5, length.out = 100)
|
||||
|
||||
plot_data <- data.frame(
|
||||
margin = margin_range,
|
||||
n = ceiling(2 * (input$sd^2) * (z_alpha + z_beta)^2 / (input$diff - margin_range)^2)
|
||||
)
|
||||
rv_ni$plot_data <- plot_data
|
||||
})
|
||||
|
||||
output$ni_result_text <- renderUI({
|
||||
if (input$go_ni == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán Cỡ mẫu' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_ni$n)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán cỡ mẫu"),
|
||||
p("Với các tham số đã cho, để chứng minh tính không thua kém, nghiên cứu của bạn cần:"),
|
||||
|
||||
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", rv_ni$n, " người tham gia MỖI NHÓM"),
|
||||
p(style = "text-align: center; font-size: 1.2em;",
|
||||
"Tổng cỡ mẫu cần thiết: ", tags$b(rv_ni$n * 2)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
output$ni_margin_plot <- renderPlot({
|
||||
req(rv_ni$plot_data)
|
||||
|
||||
ggplot(rv_ni$plot_data, aes(x = margin, y = n)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_vline(xintercept = input$delta, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = "Ảnh hưởng của Biên không thua kém (δ) lên Cỡ mẫu",
|
||||
subtitle = "Biên càng lớn (dễ dãi hơn), cỡ mẫu yêu cầu càng nhỏ",
|
||||
x = "Biên không thua kém (δ)",
|
||||
y = "Cỡ mẫu mỗi nhóm (n)"
|
||||
) +
|
||||
annotate("text", x = input$delta * 1.05, y = max(rv_ni$plot_data$n) * 0.9,
|
||||
label = paste0("Biên đã chọn\nδ = ", input$delta),
|
||||
color = "red", hjust = 0) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$ni_params_ui <- renderUI({
|
||||
tagList(
|
||||
h4("Giải thích các tham số chính"),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Biên không thua kém (Non-inferiority Margin - δ)"),
|
||||
p("Đây là tham số ", tags$strong("quan trọng nhất và mang tính chủ quan nhất"), " trong thiết kế nghiên cứu không thua kém. Nó định nghĩa mức độ 'thua kém' tối đa có thể chấp nhận được về mặt lâm sàng. Việc lựa chọn δ phải được biện minh một cách chặt chẽ dựa trên bằng chứng y học và ý kiến chuyên gia, thường là một phần hiệu quả của thuốc chuẩn đã được chứng minh trước đây.")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Khác biệt trung bình kỳ vọng (μT - μS)"),
|
||||
p("Đây là sự khác biệt thực sự mà bạn dự đoán giữa hai phương pháp. Trong nhiều trường hợp, để đảm bảo an toàn và tính toán cỡ mẫu đủ lớn, các nhà nghiên cứu thường đặt giá trị này bằng 0 (giả định hai phương pháp có hiệu quả chính xác như nhau).")
|
||||
),
|
||||
tags$div(class="alert alert-light",
|
||||
h5("Mức ý nghĩa (α, một phía)"),
|
||||
p("Bởi vì giả thuyết của nghiên cứu không thua kém là một chiều (chỉ quan tâm đến việc có 'thua kém' hay không), chúng ta sử dụng mức ý nghĩa một phía. Giá trị α = 0.05 một phía tương ứng với Z-score là 1.645, khác với 1.96 trong kiểm định hai phía.")
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
280
samplesize/reg_linear/app.R
Normal file
280
samplesize/reg_linear/app.R
Normal file
@@ -0,0 +1,280 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO HỒI QUY LOGISTIC (PHIÊN BẢN ỔN ĐỊNH)
|
||||
# - Sửa lỗi hiển thị công thức bằng cách escape ký tự `\` trong R string.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính cỡ mẫu cho hồi quy logistic theo công thức xấp xỉ của Hsieh (1989).
|
||||
calc_n_logistic_hsieh <- function(beta, var_x, p_mean, power = 0.8, sig.level = 0.05) {
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
denom <- (beta^2) * var_x * p_mean * (1 - p_mean)
|
||||
if (denom <= 0) return(NA_real_)
|
||||
n <- (z_a + z_b)^2 / denom
|
||||
ceiling(n)
|
||||
}
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho hồi quy logistic bằng mô phỏng Monte Carlo.
|
||||
simulate_logistic_power <- function(n, beta, intercept = 0, x_dist = list(type = "normal", mean = 0, sd = 1),
|
||||
n_sim = 1000, alpha = 0.05) {
|
||||
sim_results <- replicate(n_sim, {
|
||||
x <- switch(x_dist$type,
|
||||
"normal" = rnorm(n, mean = x_dist$mean, sd = x_dist$sd),
|
||||
"binary" = rbinom(n, size = 1, prob = x_dist$prob),
|
||||
"uniform" = runif(n, min = x_dist$min, max = x_dist$max))
|
||||
|
||||
linpred <- intercept + beta * x
|
||||
p <- 1 / (1 + exp(-linpred))
|
||||
y <- rbinom(n, 1, p)
|
||||
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (inherits(fit, "try-error")) return(NA_real_)
|
||||
|
||||
coefs <- summary(fit)$coefficients
|
||||
if ("x" %in% rownames(coefs)) {
|
||||
as.numeric(coefs["x", "Pr(>|z|)"] < alpha)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Hồi Quy Logistic"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số mô hình"),
|
||||
sliderInput("beta", "Giá trị β (log-odds) kỳ vọng:", value = 0.693, min = -2, max = 2, step = 0.01),
|
||||
sliderInput("p_mean_event", "Tỉ lệ 'event' trung bình trong quần thể:", value = 0.25, min = 0.01, max = 0.99),
|
||||
selectInput("x_type", "Phân phối của biến độc lập X:",
|
||||
choices = c("Nhị phân (0/1)" = "binary", "Chuẩn (Normal)" = "normal", "Đồng nhất (Uniform)" = "uniform")),
|
||||
conditionalPanel("input.x_type == 'normal'", numericInput("x_sd", "Độ lệch chuẩn của X:", value = 1, min = 0.1)),
|
||||
conditionalPanel("input.x_type == 'binary'", sliderInput("x_prob", "Tỉ lệ P(X=1):", value = 0.3, min = 0.01, max = 0.99)),
|
||||
conditionalPanel("input.x_type == 'uniform'", numericInput("x_min", "Min của X:", value = -1), numericInput("x_max", "Max của X:", value = 1)),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định"),
|
||||
sliderInput("power_log", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_log", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
hr(),
|
||||
actionButton("go_log", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "log_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("log_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("log_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail", "Nhập cỡ mẫu (n) để mô phỏng:", value = 200, min = 10)),
|
||||
column(6, actionButton("run_sim_detail", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("log_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("log_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
# --- TAB CÔNG THỨC ĐƯỢC ĐỊNH NGHĨA TĨNH TRONG UI (SỬA LỖI) ---
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
HTML("Kiểm định trong hồi quy logistic thường tập trung vào việc xem hệ số β có khác 0 một cách có ý nghĩa thống kê hay không.<br>"),
|
||||
withMathJax(HTML("$$H_0: \\beta = 0$$")),
|
||||
em("Diễn giải: Biến độc lập X không có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio = 1)."),
|
||||
br(),
|
||||
withMathJax(HTML("$$H_a: \\beta \\neq 0$$")),
|
||||
em("Diễn giải: Biến độc lập X có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio \\neq 1)."),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Xấp xỉ Hsieh, 1989)"),
|
||||
withMathJax(HTML("$$n \\approx \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{\\beta^2 \\cdot \\text{Var}(X) \\cdot \\bar{p}(1-\\bar{p})}$$")),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("\\(Z_{1-\\alpha/2}\\) là giá trị Z-score ứng với mức ý nghĩa \\(\\alpha\\) (ví dụ: 1.96 cho \\(\\alpha=0.05\\).)")),
|
||||
withMathJax(tags$li("\\(Z_{\\text{power}}\\) là giá trị Z-score ứng với công suất mong muốn (ví dụ: 0.84 cho power=0.8).")),
|
||||
withMathJax(tags$li("\\(\\beta\\) là hệ số hồi quy log-odds kỳ vọng (effect size).")),
|
||||
withMathJax(tags$li("\\(\\text{Var}(X)\\) là phương sai của biến độc lập X.")),
|
||||
withMathJax(tags$li("\\(\\bar{p}\\) là tỉ lệ trung bình của kết quả (event) trong quần thể."))
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn tính cỡ mẫu để xem liệu việc ", strong("hút thuốc lá (biến X)"), " có phải là yếu tố nguy cơ cho ", strong("bệnh tăng huyết áp (biến Y)"), " hay không."),
|
||||
p(strong("Diễn giải các tham số:")),
|
||||
tags$ul(
|
||||
tags$li(strong("Kết quả (Event):"), " Bị tăng huyết áp (Y=1)."),
|
||||
tags$li(strong("Biến độc lập (X):"), " Hút thuốc lá (X=1) vs. không hút (X=0). Đây là biến nhị phân."),
|
||||
tags$li(strong("\\(\\beta\\) (Effect size):"), withMathJax(HTML(" Dựa trên y văn, nhà nghiên cứu kỳ vọng Odds Ratio (OR) của việc hút thuốc gây tăng huyết áp là 2.0. Do đó, effect size mong muốn là \\(\\beta = \\ln(OR) = \\ln(2.0) \\approx 0.693\\)"))),
|
||||
tags$li(strong("\\(\\bar{p}\\) (Tỉ lệ event TB):"), withMathJax(HTML(" Tỉ lệ mắc tăng huyết áp chung trong quần thể ước tính là \\(\\bar{p} = 0.25\\)"))),
|
||||
tags$li(strong("\\(\\text{Var}(X)\\):"), withMathJax(HTML(" Tỉ lệ người hút thuốc trong quần thể là 30% (P(X=1)=0.3). Phương sai của X là \\(\\text{Var}(X) = 0.3 \\times 0.7 = 0.21\\)"))),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), withMathJax(HTML(" Nghiên cứu được thiết kế để có 80% cơ hội phát hiện mối liên quan (power=0.8) với mức ý nghĩa 5% (\\(\\alpha=0.05\\))")))
|
||||
),
|
||||
p("Nhà nghiên cứu sẽ nhập các giá trị trên vào công cụ để ước tính cỡ mẫu cần thiết. Các giá trị này cũng đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_log, {
|
||||
varx <- switch(input$x_type,
|
||||
"normal" = input$x_sd^2,
|
||||
"binary" = input$x_prob * (1 - input$x_prob),
|
||||
"uniform" = (input$x_max - input$x_min)^2 / 12)
|
||||
n_hsieh <- calc_n_logistic_hsieh(input$beta, varx, input$p_mean_event, input$power_log, input$alpha_log)
|
||||
rv$log_n_hsieh <- n_hsieh
|
||||
rv$log_varx <- varx
|
||||
|
||||
if (!is.na(n_hsieh)) {
|
||||
n_range_log <- unique(round(seq(max(30, n_hsieh * 0.5), n_hsieh * 1.5, length.out = 15)))
|
||||
xdist_log <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
power_values_log <- map_dbl(n_range_log, ~simulate_logistic_power(n = .x, beta = input$beta, x_dist = xdist_log, n_sim = 500, alpha = input$alpha_log))
|
||||
rv$log_plot_data <- tibble(SampleSize = n_range_log, Power = power_values_log)
|
||||
} else {
|
||||
rv$log_plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$log_result_text <- renderUI({
|
||||
if (input$go_log == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv$log_n_hsieh)
|
||||
n <- rv$log_n_hsieh
|
||||
|
||||
if (is.na(n)) {
|
||||
return(tags$div(class = "alert alert-danger", "Không thể tính toán. Kiểm tra lại các tham số (ví dụ: mẫu số của công thức có thể bằng 0)."))
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Hsieh)"),
|
||||
p("Với Var(X) ≈", tags$b(round(rv$log_varx, 3)), ", để phát hiện một \\(\\beta\\) là", tags$b(input$beta), "với power", tags$b(input$power_log), "và \\(\\alpha\\) là", tags$b(input$alpha_log), ", cỡ mẫu ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", n, " quan sát"),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả xấp xỉ. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn.")
|
||||
)
|
||||
})
|
||||
|
||||
output$log_power_plot <- renderPlot({
|
||||
req(rv$log_plot_data, rv$log_n_hsieh)
|
||||
ggplot(rv$log_plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_log, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv$log_n_hsieh, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Power thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Cỡ mẫu (n)", y = "Power (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv$log_n_hsieh * 1.05, y = 0.1, label = paste("Hsieh Approx.\nn =", rv$log_n_hsieh), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$log_effect_size_ui <- renderUI({
|
||||
or <- exp(input$beta)
|
||||
interpretation <- if (or > 1) {
|
||||
paste0("một sự gia tăng ", round((or - 1) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
} else {
|
||||
paste0("một sự sụt giảm ", round((1-or) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số chênh (Odds Ratio - OR)"),
|
||||
p("Trong hồi quy logistic, effect size thường được biểu diễn bằng Tỷ số chênh (OR), được tính bằng công thức \\(OR = e^\\beta\\)."),
|
||||
p("Nó cho biết odds của một \"event\" (kết quả Y=1) thay đổi như thế nào khi biến độc lập X tăng lên một đơn vị."),
|
||||
hr(),
|
||||
p("Với giá trị \\(\\beta\\) bạn đã chọn là ", tags$b(input$beta), ", Tỷ số chênh tương ứng là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(or, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Khi biến X tăng lên một đơn vị, điều này tương ứng với ", tags$b(interpretation))
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail, {
|
||||
xdist_detail <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
pvals <- replicate(1000, {
|
||||
x <- switch(xdist_detail$type, "normal" = rnorm(input$n_for_sim_detail, 0, xdist_detail$sd), "binary" = rbinom(input$n_for_sim_detail, 1, xdist_detail$prob), "uniform" = runif(input$n_for_sim_detail, xdist_detail$min, xdist_detail$max))
|
||||
p <- 1 / (1 + exp(-(0 + input$beta * x)))
|
||||
y <- rbinom(input$n_for_sim_detail, 1, p)
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (!inherits(fit, "try-error") && "x" %in% rownames(summary(fit)$coefficients)) {
|
||||
summary(fit)$coefficients["x", "Pr(>|z|)"]
|
||||
} else {
|
||||
NA_real_
|
||||
}
|
||||
})
|
||||
rv$log_sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$log_sim_detail_plot <- renderPlot({
|
||||
req(rv$log_sim_detail_data)
|
||||
power_est <- mean(rv$log_sim_detail_data$p_value < input$alpha_log, na.rm=TRUE)
|
||||
|
||||
ggplot(rv$log_sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_log, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (n=", input$n_for_sim_detail, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,578 @@
|
||||
@use "sass:math";
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input.dragging {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||
visibility: visible !important;
|
||||
background: #f2f2f2 !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: 0 none !important;
|
||||
box-shadow: inset 0 0 12px 4px #fff;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||
content: "!";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header {
|
||||
position: relative;
|
||||
padding: 6px 0.75rem;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
background: RGBA(var(--bs-body-bg), 0.15);
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
opacity: 0.4;
|
||||
margin-top: -12px;
|
||||
line-height: 20px;
|
||||
font-size: 20px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-dropdown_header .selectize-dropdown-header-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .selectize-dropdown-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||
border-right: 1px solid #f2f2f2;
|
||||
border-top: 0 none;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-radius: 0 2px 2px 0;
|
||||
box-sizing: border-box;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item .remove:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .item.active .remove {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-remove_button .disabled .item .remove {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 25px;
|
||||
top: 0;
|
||||
right: calc(0.75rem - 5px);
|
||||
color: var(--bs-body-color, black);
|
||||
opacity: 0.4;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button .clear:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-clear_button.single .clear {
|
||||
right: calc(0.75rem - 5px + 1.5rem);
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid #d0d0d0;
|
||||
border-bottom: 0 none;
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -6px 12px rgba(var(--bs-body-color-rgb, 0, 0, 0), 0.18);
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active::before {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-input,
|
||||
.selectize-input input {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
font-smoothing: inherit;
|
||||
}
|
||||
|
||||
.selectize-input,
|
||||
.selectize-control.single .selectize-input.input-active {
|
||||
background: var(--bs-body-bg);
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
padding: 0.375rem 0.75rem;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.has-items {
|
||||
padding: calc( 0.375rem - 1px - 0px) 0.75rem calc( 0.375rem - 1px - 3px - 0px);
|
||||
}
|
||||
|
||||
.selectize-input.full {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-input.disabled, .selectize-input.disabled * {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.selectize-input > * {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
cursor: pointer;
|
||||
margin: 0 3px 3px 0;
|
||||
padding: 1px 5px;
|
||||
background: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.05);
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83);
|
||||
border: 0px solid #dee2e6;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div.active {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
border: 0px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active {
|
||||
color: #cdcdcd;
|
||||
background: #cdcdcd;
|
||||
border: 0px solid #cdcdcd;
|
||||
}
|
||||
|
||||
.selectize-input > input {
|
||||
display: inline-block !important;
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
text-indent: 0 !important;
|
||||
border: 0 none !important;
|
||||
background: none !important;
|
||||
line-height: inherit !important;
|
||||
user-select: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input > input:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.selectize-input > input[placeholder] {
|
||||
box-sizing: initial;
|
||||
}
|
||||
|
||||
.selectize-input.has-items > input {
|
||||
margin: 0 0px !important;
|
||||
}
|
||||
|
||||
.selectize-input::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(var(--bs-border-color), 0.8);
|
||||
height: 1px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
border: 1px solid #d0d0d0;
|
||||
background: var(--bs-body-bg);
|
||||
margin: -1px 0 0 0;
|
||||
border-top: 0 none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable] .highlight {
|
||||
background: rgba(255, 237, 40, 0.4);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown .optgroup-header,
|
||||
.selectize-dropdown .no-results,
|
||||
.selectize-dropdown .create {
|
||||
padding: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .option,
|
||||
.selectize-dropdown [data-disabled],
|
||||
.selectize-dropdown [data-disabled] [data-selectable].option {
|
||||
cursor: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown [data-selectable].option {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||
border-top: 0 none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
color: #868e96;
|
||||
background: var(--bs-body-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .active.create {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .selected {
|
||||
background-color: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.selectize-dropdown .active:not(.selected) {
|
||||
background: #2fa4e7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 3px 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #d0d0d0;
|
||||
border-color: #d0d0d0 transparent #d0d0d0 transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input,
|
||||
.selectize-control.single .selectize-input input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input.input-active, .selectize-control.single .selectize-input.input-active input:not(:read-only) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow):after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(0.75rem + 5px);
|
||||
margin-top: -3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.selectize-control.single .selectize-input:not(.no-arrow).dropdown-active:after {
|
||||
margin-top: -4px;
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-color: transparent transparent RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.83) transparent;
|
||||
}
|
||||
|
||||
.selectize-control.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selectize-control.rtl.single .selectize-input:after {
|
||||
left: calc(0.75rem + 5px);
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.selectize-control.rtl .selectize-input > input {
|
||||
margin: 0 4px 0 -2px !important;
|
||||
}
|
||||
|
||||
.selectize-control .selectize-input.disabled {
|
||||
opacity: 0.5;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
margin: 2px 0 0 0;
|
||||
z-index: 1000;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-dropdown .optgroup:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--bs-border-color-translucent);
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown .create {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.selectize-dropdown-content {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown-emptyoptionlabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.selectize-input {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active {
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.selectize-input.dropdown-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectize-input.focus {
|
||||
border-color: #97d2f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input {
|
||||
border-color: #c71c22;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.is-invalid .selectize-input:focus {
|
||||
border-color: #9a161a;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #e96065;
|
||||
}
|
||||
|
||||
.selectize-control.form-control-sm .selectize-input {
|
||||
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)) !important;
|
||||
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input {
|
||||
height: auto;
|
||||
padding-left: calc(0.75rem - 5px);
|
||||
padding-right: calc(0.75rem - 5px);
|
||||
}
|
||||
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
border-radius: calc(var(--bs-border-radius) - 1px);
|
||||
}
|
||||
|
||||
.form-select.selectize-control,
|
||||
.form-control.selectize-control {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn, .input-group > .form-control:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-prepend > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:last-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .selectize-control:not(:first-child) .selectize-input {
|
||||
overflow: unset;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.selectize-dropdown.plugin-auto_position.selectize-position-top {
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
}
|
||||
|
||||
.selectize-control.plugin-auto_position .selectize-input.selectize-position-top.dropdown-active {
|
||||
border-radius: var(--bs-border-radius) !important;
|
||||
border-top: 1px solid var(--bs-border-color) !important;
|
||||
}
|
@@ -0,0 +1,294 @@
|
||||
@charset "UTF-8";
|
||||
/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
© Posit, PBC, 2023
|
||||
© RStudio, Inc, 2014
|
||||
© Denis Ineshin, 2014 https://github.com/IonDen
|
||||
© guybowden, 2014 https://github.com/guybowden
|
||||
*/
|
||||
.irs {
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* https://github.com/rstudio/shiny/issues/3443 */
|
||||
/* https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.irs *, .irs *:before, .irs *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.irs-line {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.irs-bar {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.irs-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs-handle.type_last {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs-min, .irs-max {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.irs-min {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.irs-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.irs-from, .irs-to, .irs-single {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.irs-grid {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.irs-with-grid .irs-grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.irs-grid-pol {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.irs-grid-pol.small {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.irs-grid-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.irs-disable-mask {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.lt-ie9 .irs-disable-mask {
|
||||
background: #000;
|
||||
filter: alpha(opacity=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.irs-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.irs-hidden-input {
|
||||
position: absolute !important;
|
||||
display: block !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
outline: none !important;
|
||||
z-index: -9999 !important;
|
||||
background: none !important;
|
||||
border-style: solid !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.irs {
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.irs--shiny {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.irs--shiny.irs-with-grid {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
background: linear-gradient(to bottom, #dedede -50%, #fff 150%);
|
||||
background-color: #ededed;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: s-resize;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar {
|
||||
top: 25px;
|
||||
height: 8px;
|
||||
border-top: 1px solid #2fa4e7;
|
||||
border-bottom: 1px solid #2fa4e7;
|
||||
background: #2fa4e7;
|
||||
cursor: s-resize;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar--single {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-bar::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
top: -9px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-shadow {
|
||||
top: 38px;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-shadow {
|
||||
filter: alpha(opacity=30);
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle {
|
||||
top: 17px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #ababab;
|
||||
background-color: #dedede;
|
||||
box-shadow: 1px 1px 3px rgba(255, 255, 255, 0.3);
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-min,
|
||||
.irs--shiny .irs-max {
|
||||
top: 0;
|
||||
padding: 1px 3px;
|
||||
text-shadow: none;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-min,
|
||||
.irs--shiny .lt-ie9 .irs-max {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-from,
|
||||
.irs--shiny .irs-to,
|
||||
.irs--shiny .irs-single {
|
||||
color: #fff;
|
||||
text-shadow: none;
|
||||
padding: 1px 3px;
|
||||
background-color: #2fa4e7;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.irs--shiny .lt-ie9 .irs-from,
|
||||
.irs--shiny .lt-ie9 .irs-to,
|
||||
.irs--shiny .lt-ie9 .irs-single {
|
||||
background: #999999;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid {
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-text {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-grid-pol.small {
|
||||
background-color: #999999;
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
.shiny-panel-conditional,
|
||||
div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *),
|
||||
div:where(.shiny-html-output):has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
}
|
||||
|
||||
.shiny-panel-conditional:has(> *).recalculating > *,
|
||||
div:where(.shiny-html-output):has(> *).recalculating > * {
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
|
||||
/* This is necessary so that an empty verbatimTextOutput slot
|
||||
is the same height as a non-empty one (only important when
|
||||
* placeholder = TRUE) */
|
||||
pre.shiny-text-output:empty::before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
pre.shiny-text-output.noplaceholder:empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
|
||||
CSS. This changes the behavior to not wrap.
|
||||
*/
|
||||
pre.shiny-text-output {
|
||||
word-wrap: normal;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#shiny-disconnected-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.42);
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
z-index: 99998;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
opacity: 0;
|
||||
animation: fadeIn 250ms forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.table.shiny-table > thead > tr > th, .table.shiny-table > thead > tr > td, .table.shiny-table > tbody > tr > th, .table.shiny-table > tbody > tr > td, .table.shiny-table > tfoot > tr > th, .table.shiny-table > tfoot > tr > td {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-xs > thead > tr > th, .shiny-table.spacing-xs > thead > tr > td, .shiny-table.spacing-xs > tbody > tr > th, .shiny-table.spacing-xs > tbody > tr > td, .shiny-table.spacing-xs > tfoot > tr > th, .shiny-table.spacing-xs > tfoot > tr > td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-s > thead > tr > th, .shiny-table.spacing-s > thead > tr > td, .shiny-table.spacing-s > tbody > tr > th, .shiny-table.spacing-s > tbody > tr > td, .shiny-table.spacing-s > tfoot > tr > th, .shiny-table.spacing-s > tfoot > tr > td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-m > thead > tr > th, .shiny-table.spacing-m > thead > tr > td, .shiny-table.spacing-m > tbody > tr > th, .shiny-table.spacing-m > tbody > tr > td, .shiny-table.spacing-m > tfoot > tr > th, .shiny-table.spacing-m > tfoot > tr > td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.shiny-table.spacing-l > thead > tr > th, .shiny-table.spacing-l > thead > tr > td, .shiny-table.spacing-l > tbody > tr > th, .shiny-table.spacing-l > tbody > tr > td, .shiny-table.spacing-l > tfoot > tr > th, .shiny-table.spacing-l > tfoot > tr > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.shiny-table .NA {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.46);
|
||||
}
|
||||
|
||||
.shiny-output-error {
|
||||
color: var(--bs-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.shiny-output-error:before {
|
||||
content: 'Error: ';
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-output-error-validation {
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.5);
|
||||
}
|
||||
|
||||
.shiny-output-error-validation:before {
|
||||
content: '';
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Work around MS Edge transition bug (issue #1637) */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.shiny-bound-output {
|
||||
transition: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
.slider-animate-container {
|
||||
text-align: right;
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
.slider-animate-button {
|
||||
/* Ensure controls above slider line touch target */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slider-animate-button .pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button .play {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slider-animate-button.playing .play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.progress.shiny-file-input-progress .progress-bar.bar-danger {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Make sure the filename doesn't extend past the bounds of the container */
|
||||
.shiny-input-container input[type=file] {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Old-style progress */
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 240px;
|
||||
background-color: RGBA(var(--bs-primary-rgb, 47, 164, 231), 0.05);
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* New-style progress (uses notifications API) */
|
||||
.shiny-progress-notification .progress {
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-message {
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress-notification .progress-text .progress-detail {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.shiny-label-null {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crosshair {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.grabbable {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.grabbing {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.ns-resize {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ew-resize {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.nesw-resize {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.nwse-resize {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
/* Workaround for Qt, which doesn't use font fallbacks */
|
||||
.qt pre, .qt code {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
/* Workaround for Qt 5, which draws its own margins around checks and radios;
|
||||
overrides the top margin on these elements set by Bootstrap */
|
||||
.qt5 .radio input[type="radio"],
|
||||
.qt5 .checkbox input[type="checkbox"] {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* Workaround for radio buttons and checkboxes not showing on Qt on Mac.
|
||||
This occurs in the RStudio IDE on macOS 11.5.
|
||||
https://github.com/rstudio/shiny/issues/3484
|
||||
*/
|
||||
.qtmac input[type="radio"],
|
||||
.qtmac input[type="checkbox"] {
|
||||
zoom: 1.0000001;
|
||||
}
|
||||
|
||||
.shiny-frame {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.shiny-flow-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-right: 12px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.shiny-split-layout {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shiny-split-layout > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shiny-input-panel {
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.04);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* For checkbox groups and radio buttons, bring the options closer to label,
|
||||
if label is present. */
|
||||
.shiny-input-checkboxgroup label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup label ~ .shiny-options-group {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* Checkbox groups and radios that are inline need less negative margin to
|
||||
separate from label. */
|
||||
.shiny-input-checkboxgroup.shiny-input-container-inline label ~ .shiny-options-group,
|
||||
.shiny-input-radiogroup.shiny-input-container-inline label ~ .shiny-options-group {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Limit the width of inputs in the general case. */
|
||||
.shiny-input-container:not(.shiny-input-container-inline) {
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Don't limit the width of inputs in a sidebar. */
|
||||
.well .shiny-input-container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Width of non-selectize select inputs */
|
||||
.shiny-input-container > div > select:not(.selectized) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
padding: 2px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.shiny-notification {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
color: var(--bs-emphasis-color, #000);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 0.375rem;
|
||||
opacity: 0.85;
|
||||
padding: 10px 2rem 10px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.shiny-notification-message {
|
||||
color: var(--bs-info-text-emphasis);
|
||||
background-color: var(--bs-info-bg-subtle);
|
||||
border: 1px solid var(--bs-info-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-warning {
|
||||
color: var(--bs-warning-text-emphasis);
|
||||
background-color: var(--bs-warning-bg-subtle);
|
||||
border: 1px solid var(--bs-warning-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-error {
|
||||
color: var(--bs-danger-text-emphasis);
|
||||
background-color: var(--bs-danger-bg-subtle);
|
||||
border: 1px solid var(--bs-danger-border-subtle);
|
||||
}
|
||||
|
||||
.shiny-notification-close {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
font-size: 1.125em;
|
||||
padding: 0.25rem;
|
||||
color: RGBA(var(--bs-emphasis-color-rgb, 0, 0, 0), 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shiny-notification-close:hover {
|
||||
color: RGB(var(--bs-emphasis-color-rgb, 0, 0, 0));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-notification-content-action a {
|
||||
color: RGB(var(--bs-primary-rgb, 47, 164, 231));
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.shiny-file-input-active {
|
||||
box-shadow: 0 0 0 0.25rem rgba(47, 164, 231, 0.25);
|
||||
}
|
||||
|
||||
.shiny-file-input-over {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(76, 174, 76, 0.6);
|
||||
}
|
||||
|
||||
/* Overrides bootstrap-datepicker3.css styling for invalid date ranges.
|
||||
See https://github.com/rstudio/shiny/issues/2042 for details. */
|
||||
.datepicker table tbody tr td.disabled,
|
||||
.datepicker table tbody tr td.disabled:hover,
|
||||
.datepicker table tbody tr td span.disabled,
|
||||
.datepicker table tbody tr td span.disabled:hover {
|
||||
color: var(--bs-tertiary-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Hidden tabPanels */
|
||||
.nav-hidden {
|
||||
/* override anything bootstrap sets for `.nav` */
|
||||
display: none !important;
|
||||
}
|
278
samplesize/reg_logitstics/app.R
Normal file
278
samplesize/reg_logitstics/app.R
Normal file
@@ -0,0 +1,278 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO HỒI QUY LOGISTIC (PHIÊN BẢN ỔN ĐỊNH)
|
||||
# - Sửa lỗi hiển thị công thức bằng cách escape ký tự `\` trong R string.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính cỡ mẫu cho hồi quy logistic theo công thức xấp xỉ của Hsieh (1989).
|
||||
calc_n_logistic_hsieh <- function(beta, var_x, p_mean, power = 0.8, sig.level = 0.05) {
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
denom <- (beta^2) * var_x * p_mean * (1 - p_mean)
|
||||
if (denom <= 0) return(NA_real_)
|
||||
n <- (z_a + z_b)^2 / denom
|
||||
ceiling(n)
|
||||
}
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho hồi quy logistic bằng mô phỏng Monte Carlo.
|
||||
simulate_logistic_power <- function(n, beta, intercept = 0, x_dist = list(type = "normal", mean = 0, sd = 1),
|
||||
n_sim = 1000, alpha = 0.05) {
|
||||
sim_results <- replicate(n_sim, {
|
||||
x <- switch(x_dist$type,
|
||||
"normal" = rnorm(n, mean = x_dist$mean, sd = x_dist$sd),
|
||||
"binary" = rbinom(n, size = 1, prob = x_dist$prob),
|
||||
"uniform" = runif(n, min = x_dist$min, max = x_dist$max))
|
||||
|
||||
linpred <- intercept + beta * x
|
||||
p <- 1 / (1 + exp(-linpred))
|
||||
y <- rbinom(n, 1, p)
|
||||
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (inherits(fit, "try-error")) return(NA_real_)
|
||||
|
||||
coefs <- summary(fit)$coefficients
|
||||
if ("x" %in% rownames(coefs)) {
|
||||
as.numeric(coefs["x", "Pr(>|z|)"] < alpha)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Hồi Quy Logistic"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số mô hình"),
|
||||
sliderInput("beta", "Giá trị β (log-odds) kỳ vọng:", value = 0.693, min = -2, max = 2, step = 0.01),
|
||||
sliderInput("p_mean_event", "Tỉ lệ 'event' trung bình trong quần thể:", value = 0.25, min = 0.01, max = 0.99),
|
||||
selectInput("x_type", "Phân phối của biến độc lập X:",
|
||||
choices = c("Nhị phân (0/1)" = "binary", "Chuẩn (Normal)" = "normal", "Đồng nhất (Uniform)" = "uniform")),
|
||||
conditionalPanel("input.x_type == 'normal'", numericInput("x_sd", "Độ lệch chuẩn của X:", value = 1, min = 0.1)),
|
||||
conditionalPanel("input.x_type == 'binary'", sliderInput("x_prob", "Tỉ lệ P(X=1):", value = 0.3, min = 0.01, max = 0.99)),
|
||||
conditionalPanel("input.x_type == 'uniform'", numericInput("x_min", "Min của X:", value = -1), numericInput("x_max", "Max của X:", value = 1)),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định"),
|
||||
sliderInput("power_log", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_log", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
hr(),
|
||||
actionButton("go_log", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "log_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("log_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("log_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail", "Nhập cỡ mẫu (n) để mô phỏng:", value = 200, min = 10)),
|
||||
column(6, actionButton("run_sim_detail", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("log_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("log_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
# --- TAB CÔNG THỨC ĐƯỢC ĐỊNH NGHĨA TĨNH TRONG UI (SỬA LỖI) ---
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
p("Kiểm định trong hồi quy logistic thường tập trung vào việc xem hệ số β có khác 0 một cách có ý nghĩa thống kê hay không."),
|
||||
p("$$H_0: \\beta = 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X không có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio = 1).")),
|
||||
p("$$H_a: \\beta \\neq 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio ≠ 1).")),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Xấp xỉ Hsieh, 1989)"),
|
||||
p("$$n \\approx \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{\\beta^2 \\, \\text{Var}(X) \\, \\bar{p}(1-\\bar{p})}$$"),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
tags$li("\\(Z_{1-\\alpha/2}\\) là giá trị Z-score ứng với mức ý nghĩa \\(\\alpha\\) (ví dụ: 1.96 cho \\(\\alpha=0.05\\))."),
|
||||
tags$li("\\(Z_{\\text{power}}\\) là giá trị Z-score ứng với công suất mong muốn (ví dụ: 0.84 cho power=0.8)."),
|
||||
tags$li("\\(\\beta\\) là hệ số hồi quy log-odds kỳ vọng (effect size)."),
|
||||
tags$li("\\(\\text{Var}(X)\\) là phương sai của biến độc lập X."),
|
||||
tags$li("\\(\\bar{p}\\) là tỉ lệ trung bình của kết quả (event) trong quần thể.")
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn tính cỡ mẫu để xem liệu việc ", strong("hút thuốc lá (biến X)"), " có phải là yếu tố nguy cơ cho ", strong("bệnh tăng huyết áp (biến Y)"), " hay không."),
|
||||
p(strong("Diễn giải các tham số:")),
|
||||
tags$ul(
|
||||
tags$li(strong("Kết quả (Event):"), " Bị tăng huyết áp (Y=1)."),
|
||||
tags$li(strong("Biến độc lập (X):"), " Hút thuốc lá (X=1) vs. không hút (X=0). Đây là biến nhị phân."),
|
||||
tags$li(strong("\\(\\beta\\) (Effect size):"), " Dựa trên y văn, nhà nghiên cứu kỳ vọng Odds Ratio (OR) của việc hút thuốc gây tăng huyết áp là 2.0. Do đó, effect size mong muốn là \\(\\beta = \\ln(OR) = \\ln(2.0) \\approx 0.693\\)."),
|
||||
tags$li(strong("\\(\\bar{p}\\) (Tỉ lệ event TB):"), " Tỉ lệ mắc tăng huyết áp chung trong quần thể ước tính là 25% (\\(\\bar{p} = 0.25\\))."),
|
||||
tags$li(strong("\\(\\text{Var}(X)\\):"), " Tỉ lệ người hút thuốc trong quần thể là 30% (P(X=1)=0.3). Phương sai của X là \\(\\text{Var}(X) = P(X=1) \\times (1-P(X=1)) = 0.3 \\times 0.7 = 0.21\\)."),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), " Nghiên cứu được thiết kế để có 80% cơ hội phát hiện mối liên quan (power=0.8) với mức ý nghĩa 5% (\\(\\alpha=0.05\\)).")
|
||||
),
|
||||
p("Nhà nghiên cứu sẽ nhập các giá trị trên vào công cụ để ước tính cỡ mẫu cần thiết. Các giá trị này cũng đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_log, {
|
||||
varx <- switch(input$x_type,
|
||||
"normal" = input$x_sd^2,
|
||||
"binary" = input$x_prob * (1 - input$x_prob),
|
||||
"uniform" = (input$x_max - input$x_min)^2 / 12)
|
||||
n_hsieh <- calc_n_logistic_hsieh(input$beta, varx, input$p_mean_event, input$power_log, input$alpha_log)
|
||||
rv$log_n_hsieh <- n_hsieh
|
||||
rv$log_varx <- varx
|
||||
|
||||
if (!is.na(n_hsieh)) {
|
||||
n_range_log <- unique(round(seq(max(30, n_hsieh * 0.5), n_hsieh * 1.5, length.out = 15)))
|
||||
xdist_log <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
power_values_log <- map_dbl(n_range_log, ~simulate_logistic_power(n = .x, beta = input$beta, x_dist = xdist_log, n_sim = 500, alpha = input$alpha_log))
|
||||
rv$log_plot_data <- tibble(SampleSize = n_range_log, Power = power_values_log)
|
||||
} else {
|
||||
rv$log_plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$log_result_text <- renderUI({
|
||||
if (input$go_log == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv$log_n_hsieh)
|
||||
n <- rv$log_n_hsieh
|
||||
|
||||
if (is.na(n)) {
|
||||
return(tags$div(class = "alert alert-danger", "Không thể tính toán. Kiểm tra lại các tham số (ví dụ: mẫu số của công thức có thể bằng 0)."))
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Hsieh)"),
|
||||
p("Với Var(X) ≈", tags$b(round(rv$log_varx, 3)), ", để phát hiện một \\(\\beta\\) là", tags$b(input$beta), "với power", tags$b(input$power_log), "và \\(\\alpha\\) là", tags$b(input$alpha_log), ", cỡ mẫu ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", n, " quan sát"),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả xấp xỉ. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn.")
|
||||
)
|
||||
})
|
||||
|
||||
output$log_power_plot <- renderPlot({
|
||||
req(rv$log_plot_data, rv$log_n_hsieh)
|
||||
ggplot(rv$log_plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_log, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv$log_n_hsieh, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Power thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Cỡ mẫu (n)", y = "Power (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv$log_n_hsieh * 1.05, y = 0.1, label = paste("Hsieh Approx.\nn =", rv$log_n_hsieh), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$log_effect_size_ui <- renderUI({
|
||||
or <- exp(input$beta)
|
||||
interpretation <- if (or > 1) {
|
||||
paste0("một sự gia tăng ", round((or - 1) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
} else {
|
||||
paste0("một sự sụt giảm ", round((1-or) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số chênh (Odds Ratio - OR)"),
|
||||
p("Trong hồi quy logistic, effect size thường được biểu diễn bằng Tỷ số chênh (OR), được tính bằng công thức \\(OR = e^\\beta\\)."),
|
||||
p("Nó cho biết odds của một \"event\" (kết quả Y=1) thay đổi như thế nào khi biến độc lập X tăng lên một đơn vị."),
|
||||
hr(),
|
||||
p("Với giá trị \\(\\beta\\) bạn đã chọn là ", tags$b(input$beta), ", Tỷ số chênh tương ứng là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(or, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Khi biến X tăng lên một đơn vị, điều này tương ứng với ", tags$b(interpretation))
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail, {
|
||||
xdist_detail <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
pvals <- replicate(1000, {
|
||||
x <- switch(xdist_detail$type, "normal" = rnorm(input$n_for_sim_detail, 0, xdist_detail$sd), "binary" = rbinom(input$n_for_sim_detail, 1, xdist_detail$prob), "uniform" = runif(input$n_for_sim_detail, xdist_detail$min, xdist_detail$max))
|
||||
p <- 1 / (1 + exp(-(0 + input$beta * x)))
|
||||
y <- rbinom(input$n_for_sim_detail, 1, p)
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (!inherits(fit, "try-error") && "x" %in% rownames(summary(fit)$coefficients)) {
|
||||
summary(fit)$coefficients["x", "Pr(>|z|)"]
|
||||
} else {
|
||||
NA_real_
|
||||
}
|
||||
})
|
||||
rv$log_sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$log_sim_detail_plot <- renderPlot({
|
||||
req(rv$log_sim_detail_data)
|
||||
power_est <- mean(rv$log_sim_detail_data$p_value < input$alpha_log, na.rm=TRUE)
|
||||
|
||||
ggplot(rv$log_sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_log, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (n=", input$n_for_sim_detail, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
278
samplesize/reg_logitstics/app.R1
Normal file
278
samplesize/reg_logitstics/app.R1
Normal file
@@ -0,0 +1,278 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO HỒI QUY LOGISTIC (PHIÊN BẢN ỔN ĐỊNH)
|
||||
# - Sửa lỗi hiển thị công thức bằng cách escape ký tự `\` trong R string.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính cỡ mẫu cho hồi quy logistic theo công thức xấp xỉ của Hsieh (1989).
|
||||
calc_n_logistic_hsieh <- function(beta, var_x, p_mean, power = 0.8, sig.level = 0.05) {
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
denom <- (beta^2) * var_x * p_mean * (1 - p_mean)
|
||||
if (denom <= 0) return(NA_real_)
|
||||
n <- (z_a + z_b)^2 / denom
|
||||
ceiling(n)
|
||||
}
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho hồi quy logistic bằng mô phỏng Monte Carlo.
|
||||
simulate_logistic_power <- function(n, beta, intercept = 0, x_dist = list(type = "normal", mean = 0, sd = 1),
|
||||
n_sim = 1000, alpha = 0.05) {
|
||||
sim_results <- replicate(n_sim, {
|
||||
x <- switch(x_dist$type,
|
||||
"normal" = rnorm(n, mean = x_dist$mean, sd = x_dist$sd),
|
||||
"binary" = rbinom(n, size = 1, prob = x_dist$prob),
|
||||
"uniform" = runif(n, min = x_dist$min, max = x_dist$max))
|
||||
|
||||
linpred <- intercept + beta * x
|
||||
p <- 1 / (1 + exp(-linpred))
|
||||
y <- rbinom(n, 1, p)
|
||||
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (inherits(fit, "try-error")) return(NA_real_)
|
||||
|
||||
coefs <- summary(fit)$coefficients
|
||||
if ("x" %in% rownames(coefs)) {
|
||||
as.numeric(coefs["x", "Pr(>|z|)"] < alpha)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Hồi Quy Logistic"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số mô hình"),
|
||||
sliderInput("beta", "Giá trị β (log-odds) kỳ vọng:", value = 0.693, min = -2, max = 2, step = 0.01),
|
||||
sliderInput("p_mean_event", "Tỉ lệ 'event' trung bình trong quần thể:", value = 0.25, min = 0.01, max = 0.99),
|
||||
selectInput("x_type", "Phân phối của biến độc lập X:",
|
||||
choices = c("Nhị phân (0/1)" = "binary", "Chuẩn (Normal)" = "normal", "Đồng nhất (Uniform)" = "uniform")),
|
||||
conditionalPanel("input.x_type == 'normal'", numericInput("x_sd", "Độ lệch chuẩn của X:", value = 1, min = 0.1)),
|
||||
conditionalPanel("input.x_type == 'binary'", sliderInput("x_prob", "Tỉ lệ P(X=1):", value = 0.3, min = 0.01, max = 0.99)),
|
||||
conditionalPanel("input.x_type == 'uniform'", numericInput("x_min", "Min của X:", value = -1), numericInput("x_max", "Max của X:", value = 1)),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định"),
|
||||
sliderInput("power_log", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_log", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
hr(),
|
||||
actionButton("go_log", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "log_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("log_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("log_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail", "Nhập cỡ mẫu (n) để mô phỏng:", value = 200, min = 10)),
|
||||
column(6, actionButton("run_sim_detail", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("log_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("log_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
# --- TAB CÔNG THỨC ĐƯỢC ĐỊNH NGHĨA TĨNH TRONG UI (SỬA LỖI) ---
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
p("Kiểm định trong hồi quy logistic thường tập trung vào việc xem hệ số β có khác 0 một cách có ý nghĩa thống kê hay không."),
|
||||
p("$$H_0: \\beta = 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X không có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio = 1).")),
|
||||
p("$$H_a: \\beta \\neq 0$$"),
|
||||
p(em("Diễn giải: Biến độc lập X có ảnh hưởng đến log-odds của kết quả Y (tương đương Odds Ratio ≠ 1).")),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Xấp xỉ Hsieh, 1989)"),
|
||||
p("$$n \\approx \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{\\beta^2 \\, \\text{Var}(X) \\, \\bar{p}(1-\\bar{p})}$$"),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
tags$li("\\(Z_{1-\\alpha/2}\\) là giá trị Z-score ứng với mức ý nghĩa \\(\\alpha\\) (ví dụ: 1.96 cho \\(\\alpha=0.05\\))."),
|
||||
tags$li("\\(Z_{\\text{power}}\\) là giá trị Z-score ứng với công suất mong muốn (ví dụ: 0.84 cho power=0.8)."),
|
||||
tags$li("\\(\\beta\\) là hệ số hồi quy log-odds kỳ vọng (effect size)."),
|
||||
tags$li("\\(\\text{Var}(X)\\) là phương sai của biến độc lập X."),
|
||||
tags$li("\\(\\bar{p}\\) là tỉ lệ trung bình của kết quả (event) trong quần thể.")
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn tính cỡ mẫu để xem liệu việc ", strong("hút thuốc lá (biến X)"), " có phải là yếu tố nguy cơ cho ", strong("bệnh tăng huyết áp (biến Y)"), " hay không."),
|
||||
p(strong("Diễn giải các tham số:")),
|
||||
tags$ul(
|
||||
tags$li(strong("Kết quả (Event):"), " Bị tăng huyết áp (Y=1)."),
|
||||
tags$li(strong("Biến độc lập (X):"), " Hút thuốc lá (X=1) vs. không hút (X=0). Đây là biến nhị phân."),
|
||||
tags$li(strong("\\(\\beta\\) (Effect size):"), " Dựa trên y văn, nhà nghiên cứu kỳ vọng Odds Ratio (OR) của việc hút thuốc gây tăng huyết áp là 2.0. Do đó, effect size mong muốn là \\(\\beta = \\ln(OR) = \\ln(2.0) \\approx 0.693\\)."),
|
||||
tags$li(strong("\\(\\bar{p}\\) (Tỉ lệ event TB):"), " Tỉ lệ mắc tăng huyết áp chung trong quần thể ước tính là 25% (\\(\\bar{p} = 0.25\\))."),
|
||||
tags$li(strong("\\(\\text{Var}(X)\\):"), " Tỉ lệ người hút thuốc trong quần thể là 30% (P(X=1)=0.3). Phương sai của X là \\(\\text{Var}(X) = P(X=1) \\times (1-P(X=1)) = 0.3 \\times 0.7 = 0.21\\)."),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), " Nghiên cứu được thiết kế để có 80% cơ hội phát hiện mối liên quan (power=0.8) với mức ý nghĩa 5% (\\(\\alpha=0.05\\)).")
|
||||
),
|
||||
p("Nhà nghiên cứu sẽ nhập các giá trị trên vào công cụ để ước tính cỡ mẫu cần thiết. Các giá trị này cũng đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_log, {
|
||||
varx <- switch(input$x_type,
|
||||
"normal" = input$x_sd^2,
|
||||
"binary" = input$x_prob * (1 - input$x_prob),
|
||||
"uniform" = (input$x_max - input$x_min)^2 / 12)
|
||||
n_hsieh <- calc_n_logistic_hsieh(input$beta, varx, input$p_mean_event, input$power_log, input$alpha_log)
|
||||
rv$log_n_hsieh <- n_hsieh
|
||||
rv$log_varx <- varx
|
||||
|
||||
if (!is.na(n_hsieh)) {
|
||||
n_range_log <- unique(round(seq(max(30, n_hsieh * 0.5), n_hsieh * 1.5, length.out = 15)))
|
||||
xdist_log <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
power_values_log <- map_dbl(n_range_log, ~simulate_logistic_power(n = .x, beta = input$beta, x_dist = xdist_log, n_sim = 500, alpha = input$alpha_log))
|
||||
rv$log_plot_data <- tibble(SampleSize = n_range_log, Power = power_values_log)
|
||||
} else {
|
||||
rv$log_plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$log_result_text <- renderUI({
|
||||
if (input$go_log == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv$log_n_hsieh)
|
||||
n <- rv$log_n_hsieh
|
||||
|
||||
if (is.na(n)) {
|
||||
return(tags$div(class = "alert alert-danger", "Không thể tính toán. Kiểm tra lại các tham số (ví dụ: mẫu số của công thức có thể bằng 0)."))
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Hsieh)"),
|
||||
p("Với Var(X) ≈", tags$b(round(rv$log_varx, 3)), ", để phát hiện một \\(\\beta\\) là", tags$b(input$beta), "với power", tags$b(input$power_log), "và \\(\\alpha\\) là", tags$b(input$alpha_log), ", cỡ mẫu ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", n, " quan sát"),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả xấp xỉ. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn.")
|
||||
)
|
||||
})
|
||||
|
||||
output$log_power_plot <- renderPlot({
|
||||
req(rv$log_plot_data, rv$log_n_hsieh)
|
||||
ggplot(rv$log_plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_log, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv$log_n_hsieh, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Power thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Cỡ mẫu (n)", y = "Power (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv$log_n_hsieh * 1.05, y = 0.1, label = paste("Hsieh Approx.\nn =", rv$log_n_hsieh), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$log_effect_size_ui <- renderUI({
|
||||
or <- exp(input$beta)
|
||||
interpretation <- if (or > 1) {
|
||||
paste0("một sự gia tăng ", round((or - 1) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
} else {
|
||||
paste0("một sự sụt giảm ", round((1-or) * 100, 1), "% trong \"odds\" của kết quả (event) xảy ra.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số chênh (Odds Ratio - OR)"),
|
||||
p("Trong hồi quy logistic, effect size thường được biểu diễn bằng Tỷ số chênh (OR), được tính bằng công thức \\(OR = e^\\beta\\)."),
|
||||
p("Nó cho biết odds của một \"event\" (kết quả Y=1) thay đổi như thế nào khi biến độc lập X tăng lên một đơn vị."),
|
||||
hr(),
|
||||
p("Với giá trị \\(\\beta\\) bạn đã chọn là ", tags$b(input$beta), ", Tỷ số chênh tương ứng là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(or, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Khi biến X tăng lên một đơn vị, điều này tương ứng với ", tags$b(interpretation))
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail, {
|
||||
xdist_detail <- switch(input$x_type,
|
||||
"normal" = list(type = "normal", mean = 0, sd = input$x_sd),
|
||||
"binary" = list(type = "binary", prob = input$x_prob),
|
||||
"uniform" = list(type = "uniform", min = input$x_min, max = input$x_max))
|
||||
|
||||
pvals <- replicate(1000, {
|
||||
x <- switch(xdist_detail$type, "normal" = rnorm(input$n_for_sim_detail, 0, xdist_detail$sd), "binary" = rbinom(input$n_for_sim_detail, 1, xdist_detail$prob), "uniform" = runif(input$n_for_sim_detail, xdist_detail$min, xdist_detail$max))
|
||||
p <- 1 / (1 + exp(-(0 + input$beta * x)))
|
||||
y <- rbinom(input$n_for_sim_detail, 1, p)
|
||||
fit <- try(glm(y ~ x, family = binomial), silent = TRUE)
|
||||
if (!inherits(fit, "try-error") && "x" %in% rownames(summary(fit)$coefficients)) {
|
||||
summary(fit)$coefficients["x", "Pr(>|z|)"]
|
||||
} else {
|
||||
NA_real_
|
||||
}
|
||||
})
|
||||
rv$log_sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$log_sim_detail_plot <- renderPlot({
|
||||
req(rv$log_sim_detail_data)
|
||||
power_est <- mean(rv$log_sim_detail_data$p_value < input$alpha_log, na.rm=TRUE)
|
||||
|
||||
ggplot(rv$log_sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_log, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (n=", input$n_for_sim_detail, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
354
samplesize/survival/app.R
Normal file
354
samplesize/survival/app.R
Normal file
@@ -0,0 +1,354 @@
|
||||
# ==============================================================================
|
||||
# ỨNG DỤNG SHINY TÍNH CỠ MẪU CHO PHÂN TÍCH SỐNG CÒN (LOG-RANK TEST)
|
||||
# - Xây dựng dựa trên cấu trúc ứng dụng Hồi quy Logistic.
|
||||
# - Bao gồm tính toán, mô phỏng, và diễn giải chi tiết.
|
||||
# Author: Gemini & User Collaboration
|
||||
# Date: 2025-10-17
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# THIẾT LẬP: Tải các thư viện cần thiết
|
||||
# ------------------------------------------------------------------------------
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(ggplot2)
|
||||
library(shinycssloaders)
|
||||
library(dplyr)
|
||||
library(purrr)
|
||||
library(scales)
|
||||
library(survival)
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 1: CÁC HÀM HỖ TRỢ (HELPER FUNCTIONS)
|
||||
# ==============================================================================
|
||||
|
||||
#' Tính số biến cố (events) cần thiết cho kiểm định Log-rank (Schoenfeld, 1983).
|
||||
#'
|
||||
#' @param hr Tỷ số rủi ro (Hazard Ratio) kỳ vọng.
|
||||
#' @param power Công suất mong muốn (1 - beta).
|
||||
#' @param sig.level Mức ý nghĩa alpha.
|
||||
#' @param p_alloc Tỷ lệ phân bổ vào nhóm 1 (can thiệp).
|
||||
#' @return Số nguyên là tổng số biến cố cần thiết.
|
||||
calc_events_logrank <- function(hr, power = 0.8, sig.level = 0.05, p_alloc = 0.5) {
|
||||
if (hr <= 0) return(NA_real_)
|
||||
z_a <- qnorm(1 - sig.level / 2)
|
||||
z_b <- qnorm(power)
|
||||
|
||||
num <- (z_a + z_b)^2
|
||||
den <- (log(hr)^2) * p_alloc * (1 - p_alloc)
|
||||
|
||||
if (den <= 0) return(NA_real_)
|
||||
|
||||
ceiling(num / den)
|
||||
}
|
||||
|
||||
|
||||
#' Ước tính công suất thực nghiệm cho kiểm định Log-rank bằng mô phỏng Monte Carlo.
|
||||
#'
|
||||
#' @param n Tổng cỡ mẫu.
|
||||
#' @param hr Tỷ số rủi ro.
|
||||
#' @param surv_prob_control Tỷ lệ sống sót ở nhóm chứng sau một thời gian theo dõi.
|
||||
#' @param follow_up_time Thời gian theo dõi.
|
||||
#' @param alpha Mức ý nghĩa.
|
||||
#' @param p_alloc Tỷ lệ phân bổ vào nhóm 1.
|
||||
#' @param n_sim Số lần mô phỏng.
|
||||
#' @return Giá trị công suất thực nghiệm (từ 0 đến 1).
|
||||
simulate_logrank_power <- function(n, hr, surv_prob_control, follow_up_time = 1,
|
||||
alpha = 0.05, p_alloc = 0.5, n_sim = 500) {
|
||||
|
||||
# Từ tỷ lệ sống, tính rate lambda cho phân phối mũ
|
||||
rate_control <- -log(surv_prob_control) / follow_up_time
|
||||
rate_treatment <- rate_control * hr
|
||||
|
||||
n1 <- round(n * p_alloc)
|
||||
n2 <- n - n1
|
||||
|
||||
sim_results <- replicate(n_sim, {
|
||||
# Tạo dữ liệu thời gian sống cho 2 nhóm
|
||||
times1 <- rexp(n1, rate = rate_treatment)
|
||||
times2 <- rexp(n2, rate = rate_control)
|
||||
|
||||
# Tạo dữ liệu kiểm duyệt (censoring) tại thời điểm follow_up_time
|
||||
event1 <- ifelse(times1 < follow_up_time, 1, 0)
|
||||
event2 <- ifelse(times2 < follow_up_time, 1, 0)
|
||||
|
||||
time_obs1 <- pmin(times1, follow_up_time)
|
||||
time_obs2 <- pmin(times2, follow_up_time)
|
||||
|
||||
# Gộp dữ liệu
|
||||
all_times <- c(time_obs1, time_obs2)
|
||||
all_events <- c(event1, event2)
|
||||
group <- factor(c(rep("Treatment", n1), rep("Control", n2)))
|
||||
|
||||
# Thực hiện kiểm định log-rank
|
||||
sdf <- try(survdiff(Surv(all_times, all_events) ~ group), silent = TRUE)
|
||||
|
||||
if (inherits(sdf, "try-error")) return(NA)
|
||||
|
||||
# Lấy p-value
|
||||
p_val <- 1 - pchisq(sdf$chisq, df = 1)
|
||||
return(p_val < alpha)
|
||||
})
|
||||
|
||||
mean(sim_results, na.rm = TRUE)
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 2: GIAO DIỆN NGƯỜI DÙNG (USER INTERFACE - UI)
|
||||
# ==============================================================================
|
||||
|
||||
ui <- fluidPage(
|
||||
theme = bs_theme(version = 5, bootswatch = "cerulean"),
|
||||
withMathJax(),
|
||||
|
||||
titlePanel("Công Cụ Tính Cỡ Mẫu Cho Phân Tích Sống Còn (Log-Rank)"),
|
||||
|
||||
sidebarLayout(
|
||||
sidebarPanel(
|
||||
width = 4,
|
||||
h4("Nhập Tham Số"),
|
||||
br(),
|
||||
h5("Tham số hiệu ứng & Quần thể"),
|
||||
sliderInput("s_a", "Tỷ lệ sống còn kỳ vọng ở Nhóm 1 (Can thiệp):", value = 0.45, min = 0.01, max = 0.99),
|
||||
sliderInput("s_b", "Tỷ lệ sống còn kỳ vọng ở Nhóm 2 (Chứng):", value = 0.30, min = 0.01, max = 0.99),
|
||||
numericInput("follow_up", "Thời gian theo dõi (đơn vị tùy chọn, ví dụ: năm):", value=3, min=0.1),
|
||||
|
||||
hr(),
|
||||
h5("Tham số kiểm định & Thiết kế"),
|
||||
sliderInput("power_surv", "Công suất mong muốn (1 - \\(\\beta\\)):", value = 0.8, min = 0.5, max = 0.99),
|
||||
sliderInput("alpha_surv", "Mức ý nghĩa (\\(\\alpha\\)):", value = 0.05, min = 0.01, max = 0.1),
|
||||
sliderInput("p_alloc_surv", "Tỷ lệ phân bổ vào Nhóm 1:", value = 0.5, min = 0.1, max = 0.9),
|
||||
helpText("Giá trị 0.5 tương ứng tỷ lệ 1:1."),
|
||||
|
||||
hr(),
|
||||
actionButton("go_surv", "Tính toán & Phân tích", class = "btn-primary w-100", icon = icon("calculator"))
|
||||
),
|
||||
|
||||
mainPanel(
|
||||
width = 8,
|
||||
tabsetPanel(
|
||||
id = "surv_results_tabs",
|
||||
type = "pills",
|
||||
tabPanel("Kết quả & Diễn giải",
|
||||
withSpinner(uiOutput("surv_result_text"), type = 6, color = "#007bff")),
|
||||
tabPanel("Đồ thị Power & Mô phỏng",
|
||||
withSpinner(plotOutput("surv_power_plot"), type = 6, color = "#007bff"),
|
||||
hr(),
|
||||
h4("Phân tích Mô phỏng Chi tiết"),
|
||||
p("Chạy mô phỏng sâu hơn với một cỡ mẫu cụ thể để xem phân phối của p-value và ước tính power chính xác hơn."),
|
||||
fluidRow(
|
||||
column(6, numericInput("n_for_sim_detail_surv", "Nhập cỡ mẫu (N) để mô phỏng:", value = 300, min = 20)),
|
||||
column(6, actionButton("run_sim_detail_surv", "Chạy mô phỏng chi tiết", icon = icon("play-circle"), class="btn-success", style="margin-top: 25px;"))
|
||||
),
|
||||
withSpinner(plotOutput("surv_sim_detail_plot"), type = 6, color = "#007bff")),
|
||||
tabPanel("Phân tích Effect Size",
|
||||
withSpinner(uiOutput("surv_effect_size_ui"), type = 6, color = "#007bff")),
|
||||
|
||||
tabPanel("Giả thuyết, Công thức & Ví dụ",
|
||||
h4("Giả thuyết thống kê"),
|
||||
HTML("Kiểm định Log-rank so sánh toàn bộ đường cong sống còn giữa các nhóm. Giả thuyết không có tham số cụ thể như hồi quy, mà là:"),
|
||||
p(strong("$$H_0: S_1(t) = S_2(t) \\text{ for all } t$$")),
|
||||
em("Diễn giải: Không có sự khác biệt về hàm sống còn giữa nhóm 1 và nhóm 2 tại mọi thời điểm."),
|
||||
br(),
|
||||
p(strong("$$H_a: S_1(t) \\neq S_2(t) \\text{ for some } t$$")),
|
||||
em("Diễn giải: Có sự khác biệt về hàm sống còn giữa hai nhóm."),
|
||||
hr(),
|
||||
|
||||
h4("Công thức tính cỡ mẫu (Schoenfeld, 1983)"),
|
||||
p("Quá trình tính toán gồm 2 bước:"),
|
||||
p(strong("Bước 1: Tính tổng số biến cố (events) cần thiết (d)")),
|
||||
withMathJax(HTML("$$d = \\frac{(Z_{1-\\alpha/2} + Z_{\\text{power}})^2}{(\\ln(HR))^2 \\cdot p_1 \\cdot p_2}$$")),
|
||||
p(strong("Bước 2: Tính tổng cỡ mẫu (N) từ số biến cố")),
|
||||
withMathJax(HTML("$$N = \\frac{d}{P(\\text{event})}$$")),
|
||||
p(strong("Trong đó:")),
|
||||
tags$ul(
|
||||
withMathJax(tags$li("\\(HR\\) là Tỷ số rủi ro (Hazard Ratio) kỳ vọng. Có thể ước tính \\(HR \\approx \\frac{\\ln(S_1)}{\\ln(S_2)}\\).")),
|
||||
withMathJax(tags$li("\\(p_1, p_2\\) là tỷ lệ phân bổ vào mỗi nhóm (ví dụ: 0.5 và 0.5).")),
|
||||
withMathJax(tags$li("\\(P(\\text{event})\\) là xác suất trung bình một người tham gia sẽ có biến cố trong thời gian nghiên cứu. \\(P(\\text{event}) = 1 - (p_1 S_1 + p_2 S_2)\\)."))
|
||||
),
|
||||
hr(),
|
||||
|
||||
h4("Ví dụ trong Y tế công cộng"),
|
||||
p(strong("Bối cảnh nghiên cứu:")),
|
||||
p("Một nhà nghiên cứu muốn thử nghiệm một loại thuốc mới (Nhóm 1) để kéo dài thời gian sống không tái phát của bệnh nhân ung thư so với phác đồ chuẩn (Nhóm 2). Thời gian theo dõi là 3 năm."),
|
||||
tags$ul(
|
||||
tags$li(strong("Tỷ lệ sống còn Nhóm 2 (chuẩn):"), withMathJax(HTML(" Dựa trên dữ liệu trước, tỷ lệ sống không tái phát sau 3 năm của phác đồ chuẩn là 30% (\\(S_2=0.3\\))."))),
|
||||
tags$li(strong("Tỷ lệ sống còn Nhóm 1 (mới):"), withMathJax(HTML(" Nhà nghiên cứu kỳ vọng thuốc mới sẽ cải thiện tỷ lệ này lên 45% (\\(S_1=0.45\\))."))),
|
||||
tags$li(strong("Effect size (HR):"), withMathJax(HTML(" HR ước tính là \\(\\frac{\\ln(0.45)}{\\ln(0.30)} \\approx 0.66\\)."))),
|
||||
tags$li(strong("Xác suất biến cố (tái phát):"), withMathJax(HTML(" \\(P(\\text{event}) = 1 - (0.5 \\times 0.45 + 0.5 \\times 0.30) = 0.625\\)."))),
|
||||
tags$li(strong("Power và \\(\\alpha\\):"), " Nghiên cứu cần 80% công suất và mức ý nghĩa 5%."),
|
||||
tags$li(strong("Thiết kế:"), " Phân bổ đều bệnh nhân vào 2 nhóm (1:1).")
|
||||
),
|
||||
p("Các giá trị này đã được đặt làm mặc định trong ứng dụng để bạn dễ hình dung.")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 3: LOGIC MÁY CHỦ (SERVER)
|
||||
# ==============================================================================
|
||||
server <- function(input, output, session) {
|
||||
|
||||
rv_surv <- reactiveValues()
|
||||
|
||||
observeEvent(input$go_surv, {
|
||||
validate(
|
||||
need(input$s_a != input$s_b, "Tỷ lệ sống còn ở hai nhóm phải khác nhau.")
|
||||
)
|
||||
|
||||
# Tính toán
|
||||
hr_est <- log(input$s_a) / log(input$s_b)
|
||||
d_req <- calc_events_logrank(hr_est, input$power_surv, input$alpha_surv, input$p_alloc_surv)
|
||||
|
||||
prob_event_1 <- 1 - input$s_a
|
||||
prob_event_2 <- 1 - input$s_b
|
||||
avg_prob_event <- input$p_alloc_surv * prob_event_1 + (1 - input$p_alloc_surv) * prob_event_2
|
||||
|
||||
n_total <- ceiling(d_req / avg_prob_event)
|
||||
n1 <- ceiling(n_total * input$p_alloc_surv)
|
||||
n2 <- n_total - n1
|
||||
|
||||
rv_surv$hr <- hr_est
|
||||
rv_surv$d <- d_req
|
||||
rv_surv$N <- n_total
|
||||
rv_surv$n1 <- n1
|
||||
rv_surv$n2 <- n2
|
||||
|
||||
# Dữ liệu cho biểu đồ
|
||||
if (!is.na(n_total)) {
|
||||
n_range_surv <- unique(round(seq(max(30, n_total * 0.5), n_total * 1.5, length.out = 15)))
|
||||
|
||||
power_values_surv <- map_dbl(n_range_surv, ~simulate_logrank_power(
|
||||
n = .x,
|
||||
hr = hr_est,
|
||||
surv_prob_control = input$s_b,
|
||||
follow_up_time = input$follow_up,
|
||||
alpha = input$alpha_surv,
|
||||
p_alloc = input$p_alloc_surv,
|
||||
n_sim = 500
|
||||
))
|
||||
rv_surv$plot_data <- tibble(SampleSize = n_range_surv, Power = power_values_surv)
|
||||
} else {
|
||||
rv_surv$plot_data <- NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$surv_result_text <- renderUI({
|
||||
if (input$go_surv == 0) {
|
||||
return(tags$div(class="alert alert-info", "Nhập các tham số và nhấn 'Tính toán & Phân tích' để xem kết quả."))
|
||||
}
|
||||
|
||||
req(rv_surv$N)
|
||||
|
||||
tagList(
|
||||
h4("Kết quả tính toán (Xấp xỉ Schoenfeld)"),
|
||||
p("Với Hazard Ratio (HR) ước tính là", tags$b(round(rv_surv$hr, 3)), "và các tham số đã cho, kết quả như sau:"),
|
||||
tags$div(class="alert alert-light", style="text-align: center;",
|
||||
p(style="font-size: 1.2em;", "Tổng số biến cố (events) cần quan sát: ", tags$strong(style="color: #dc3545;", rv_surv$d))
|
||||
),
|
||||
h3(style = "color: #007bff; text-align: center; margin-top: 20px;", "Tổng Cỡ Mẫu (N) ≈ ", rv_surv$N),
|
||||
p(style = "text-align: center; font-size: 1.2em;",
|
||||
"Cỡ mẫu Nhóm 1 (Can thiệp): ", tags$b(rv_surv$n1), br(),
|
||||
"Cỡ mẫu Nhóm 2 (Chứng): ", tags$b(rv_surv$n2)
|
||||
),
|
||||
hr(),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Ghi chú:"), " Đây là kết quả dựa trên công thức. Biểu đồ trong tab 'Đồ thị Power & Mô phỏng' được tạo ra bằng mô phỏng và có thể cho kết quả chính xác hơn, đặc biệt với cỡ mẫu nhỏ.")
|
||||
)
|
||||
})
|
||||
|
||||
output$surv_power_plot <- renderPlot({
|
||||
req(rv_surv$plot_data, rv_surv$N)
|
||||
ggplot(rv_surv$plot_data, aes(x = SampleSize, y = Power)) +
|
||||
geom_line(color = "#007bff", size = 1.2) +
|
||||
geom_point(color = "#007bff", size = 3) +
|
||||
geom_hline(yintercept = input$power_surv, linetype = "dashed", color = "red") +
|
||||
geom_vline(xintercept = rv_surv$N, linetype = "dotted", color = "darkorange", size=1.2) +
|
||||
labs(title = "Công suất thực nghiệm (Mô phỏng) vs. Cỡ mẫu", x = "Tổng Cỡ mẫu (N)", y = "Công suất (1 - \\(\\beta\\))") +
|
||||
annotate("text", x = rv_surv$N * 1.05, y = 0.1, label = paste("Formula Approx.\nN =", rv_surv$N), color = "darkorange", hjust = 0) +
|
||||
scale_y_continuous(labels = percent, limits = c(0, 1)) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
output$surv_effect_size_ui <- renderUI({
|
||||
req(rv_surv$hr)
|
||||
hr <- rv_surv$hr
|
||||
|
||||
interpretation <- if (hr < 1) {
|
||||
paste0("rủi ro (hazard) xảy ra biến cố tại bất kỳ thời điểm nào ", tags$b("giảm đi ", round((1 - hr) * 100, 1), "%"), " so với nhóm chứng.")
|
||||
} else {
|
||||
paste0("rủi ro (hazard) xảy ra biến cố tại bất kỳ thời điểm nào ", tags$b("tăng lên ", round((hr - 1) * 100, 1), "%"), " so với nhóm chứng.")
|
||||
}
|
||||
|
||||
tagList(
|
||||
h4("Phân tích Effect Size: Tỷ số rủi ro (Hazard Ratio - HR)"),
|
||||
p("Trong phân tích sống còn, effect size chính là Tỷ số rủi ro (HR). Nó đo lường hiệu quả tức thời của một can thiệp lên xác suất xảy ra biến cố."),
|
||||
p("Nó được ước tính từ tỷ lệ sống còn của hai nhóm (giả định rằng tỷ lệ rủi ro là hằng số theo thời gian): \\(HR \\approx \\ln(S_1) / \\ln(S_2)\\)."),
|
||||
hr(),
|
||||
p("Với các tỷ lệ sống còn bạn đã chọn, Tỷ số rủi ro ước tính là:"),
|
||||
tags$h3(style = "color: #007bff; text-align: center;", round(hr, 3)),
|
||||
tags$div(class = "alert alert-light",
|
||||
tags$b("Diễn giải:"),
|
||||
p("Một HR bằng ", tags$b(round(hr, 3)), " có nghĩa là nhóm can thiệp có ", interpretation)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
observeEvent(input$run_sim_detail_surv, {
|
||||
pvals <- replicate(1000, {
|
||||
n <- input$n_for_sim_detail_surv
|
||||
hr <- log(input$s_a) / log(input$s_b)
|
||||
|
||||
rate_control <- -log(input$s_b) / input$follow_up
|
||||
rate_treatment <- rate_control * hr
|
||||
|
||||
n1 <- round(n * input$p_alloc_surv)
|
||||
n2 <- n - n1
|
||||
|
||||
times1 <- rexp(n1, rate = rate_treatment)
|
||||
times2 <- rexp(n2, rate = rate_control)
|
||||
|
||||
event1 <- ifelse(times1 < input$follow_up, 1, 0)
|
||||
event2 <- ifelse(times2 < input$follow_up, 1, 0)
|
||||
|
||||
time_obs1 <- pmin(times1, input$follow_up)
|
||||
time_obs2 <- pmin(times2, input$follow_up)
|
||||
|
||||
all_times <- c(time_obs1, time_obs2)
|
||||
all_events <- c(event1, event2)
|
||||
group <- factor(c(rep("Treatment", n1), rep("Control", n2)))
|
||||
|
||||
sdf <- try(survdiff(Surv(all_times, all_events) ~ group), silent = TRUE)
|
||||
if (inherits(sdf, "try-error")) return(NA_real_)
|
||||
|
||||
1 - pchisq(sdf$chisq, df = 1)
|
||||
})
|
||||
rv_surv$sim_detail_data <- tibble(p_value = pvals)
|
||||
})
|
||||
|
||||
output$surv_sim_detail_plot <- renderPlot({
|
||||
req(rv_surv$sim_detail_data)
|
||||
power_est <- mean(rv_surv$sim_detail_data$p_value < input$alpha_surv, na.rm=TRUE)
|
||||
|
||||
ggplot(rv_surv$sim_detail_data, aes(x = p_value)) +
|
||||
geom_histogram(bins = 30, fill = "#28a745", color = "black", boundary=0) +
|
||||
geom_vline(xintercept = input$alpha_surv, linetype = "dashed", color = "red", size = 1) +
|
||||
labs(
|
||||
title = paste0("Phân phối P-value từ mô phỏng (N=", input$n_for_sim_detail_surv, ")"),
|
||||
subtitle = paste0("Công suất thực nghiệm ước tính: ", percent(power_est, accuracy = 0.1)),
|
||||
x = "P-value (từ Kiểm định Log-rank)", y = "Tần suất"
|
||||
) +
|
||||
theme_minimal(base_size = 14)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# PHẦN 4: CHẠY ỨNG DỤNG
|
||||
# ==============================================================================
|
||||
shinyApp(ui, server)
|
Reference in New Issue
Block a user