Metadata-Version: 2.4
Name: dpkits
Version: 2.0.0
Summary: A small package for data processing
Author-email: Hung Dao <hung.daotuan.1991@gmail.com>
Project-URL: Homepage, https://github.com/HungDaoHD/packaging_dpkits
Project-URL: Bug Tracker, https://github.com/HungDaoHD/packaging_dpkits/issues
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: colorama
Requires-Dist: contourpy
Requires-Dist: cycler
Requires-Dist: et-xmlfile
Requires-Dist: fonttools
Requires-Dist: joblib
Requires-Dist: kiwisolver
Requires-Dist: lxml
Requires-Dist: matplotlib
Requires-Dist: numpy
Requires-Dist: openpyxl
Requires-Dist: packaging
Requires-Dist: pandas
Requires-Dist: pandas-flavor
Requires-Dist: patsy
Requires-Dist: pillow
Requires-Dist: pingouin
Requires-Dist: pyparsing
Requires-Dist: pyreadstat
Requires-Dist: python-dateutil
Requires-Dist: python-pptx
Requires-Dist: pytz
Requires-Dist: scikit-learn
Requires-Dist: scipy
Requires-Dist: seaborn
Requires-Dist: six
Requires-Dist: statsmodels
Requires-Dist: tabulate
Requires-Dist: threadpoolctl
Requires-Dist: typing_extensions
Requires-Dist: tzdata
Requires-Dist: xarray
Requires-Dist: XlsxWriter
Requires-Dist: pydantic
Requires-Dist: xlwings
Requires-Dist: openai
Requires-Dist: prince
Requires-Dist: kneed
Dynamic: license-file

# dpkits — Package xử lý dữ liệu khảo sát

**Phiên bản 2.0.0**

Package Python chuyên dụng cho xử lý, biến đổi, phân tích và xuất bảng crosstab (có kiểm định sig) cho dữ liệu khảo sát từ nền tảng QMe.

## Yêu cầu

- **Python ≥ 3.12** (code dùng cú pháp f-string của Python 3.12)
- Các thư viện phụ thuộc (pandas, numpy, scipy, openpyxl, pyreadstat, scikit-learn, pingouin, xlwings, …) được cài tự động theo `requirements.txt`
- `TableFormatter` điều khiển Microsoft Excel qua xlwings → máy phải cài **Excel** (Windows / macOS)

## Cài đặt

```bash
pip install --upgrade dpkits
```

---

## Có gì mới trong 2.0.0

### Thay đổi phá tương thích (cần sửa script cũ)

| Thay đổi | Cách sửa |
|----------|----------|
| Bỏ `DataTableGeneratorV2` và `TableFormatterV2` | Dùng `DataTableGenerator` và `TableFormatter` (đã có đủ các tính năng của bản V2, xem bên dưới) |
| Không còn nhận `lst_header_qres` | Đổi sang `dict_header_qres`: `{'hd_1': lst_header_qres}` |
| Yêu cầu Python ≥ 3.12 | Nâng cấp Python |
| `lst_sig_lvl` phải tăng dần | `[95, 90]` → `[90, 95]` (nhập giảm dần sẽ bị dừng xử lý) |
| `sig_cols` được kiểm tra trước khi chạy | Cấu hình không hợp lệ sẽ bị dừng kèm thông báo (xem mục Lưu ý) |

### Thay đổi làm số liệu / ký hiệu sig khác bản 1.6.x

Chạy lại dự án cũ bằng 2.0.0 có thể cho kết quả khác ở các trường hợp sau — đây là các lỗi đã được sửa:

| Trường hợp | 1.6.x | 2.0.0 |
|------------|-------|-------|
| `cats` của stub khai báo **thiếu mã** (ví dụ ẩn mã DK) | % và count **sai** (mã không khai báo bị đếm nhầm) | Đúng: mã không khai báo vẫn nằm trong base, không hiện thành dòng |
| Sig độc lập khi câu hỏi có người **không trả lời** (NaN, câu có lọc) | n tính cả người không trả lời → sig "dễ" có ý nghĩa hơn thực tế | n = số người trả lời trong cột |
| Sig cột **Total** với cột con (`sig_type: 'ind'`) | Total so với cột con (2 mẫu chồng lên nhau) | Total so với **phần còn lại**: A vs Total = A vs (Total trừ A). Dòng chú thích của bảng ghi rõ điều này |
| `lst_sig_lvl = [95, 90]` | Chữ hoa = 90%, không bao giờ có chữ thường | Bị chặn, phải nhập tăng dần |

### Tính năng mới

- **`is_sig_total`** (trong `sig_test_info`): chọn có sig cột Total hay không.
- Dòng **Standard Error** (`sem`): câu NUM có mặc định; câu SA khai báo `mean` ra 3 dòng Mean, Std, Standard Error.
- **`decimal`**: số chữ số thập phân của ô % (generator ghi, `TableFormatter` đọc và định dạng).
- **`is_validate_data=False`**: bỏ qua bước kiểm tra mã ngoài codelist & trùng giá trị MA.
- **`run_tables(lst_group_tables=...)`**: tên khác của `run_tables_by_js_files` (giống API bản V2 cũ).
- `tables_to_run: []` = chạy tất cả bảng; tên bảng (key) khác `tbl_name` không còn lỗi.
- Đang debug (PyCharm / VS Code) → bảng chạy trong tiến trình hiện tại, đặt breakpoint được.
- `TableFormatter`: định dạng dòng `sem`, xóa cột sig rỗng ở bảng có trọng số, mở file ở sheet Content, tùy chọn `is_visible`.
- Thông báo mã ngoài codelist hiện đúng ID gốc của respondent.

### Hiệu năng

Đo trên một bộ dữ liệu thật (1.016 respondent × 3.155 biến, máy 12 core); thời gian thực tế phụ thuộc dữ liệu và máy:

| | 1.6.x | 2.0.0 |
|---|---|---|
| `import dpkits` | 5,7 s | 2,0 s |
| Khởi tạo `DataTableGenerator` (kiểm tra data) | 13,0 s | 1,3–1,7 s |
| Tính 1 bảng | 24,3 s | ~2 s |
| Tính 3 bảng | 30,6 s | ~7 s |
| RAM đỉnh | 2,75 GB | 0,25 GB |

---

## Import

```python
from dpkits import (
    APDataConverter,
    DataProcessing,
    DataTranspose,
    CodeframeReader,
    LSMCalculation,
    DataAnalysis,
    DataTableGenerator,
    TableFormatter,
)
```

## Tổng quan luồng xử lý

| Bước | Class | Mô tả |
|------|-------|--------|
| 1 | `APDataConverter` | Đọc file Excel/ZIP (QMe) → df_data + df_info |
| 2 | `DataProcessing` | Thêm, xóa, cập nhật biến |
| 3 | `DataTranspose` | Stack/Unstack dữ liệu nhiều sản phẩm |
| 4 | `CodeframeReader` | Đọc bảng codeframe câu hỏi mở |
| 5 | `LSMCalculation` | Tính điểm LSM 6 cấp |
| 6 | `DataAnalysis` | Penalty, hồi quy, tương quan, KDA, CA, PSM, k-means |
| 7 | `APDataConverter.generate_multiple_data_files` | Xuất .sav / .xlsx |
| 8 | `DataTableGenerator` + `TableFormatter` | Sinh bảng crosstab + sig test, format Excel |

---

## Bước 1 — APDataConverter: Đọc và chuyển đổi dữ liệu

```python
str_file_name     = 'APDataTest'
str_tbl_file_name = f'{str_file_name}_Topline.xlsx'

converter = APDataConverter(file_name='APDataTesting.xlsx')

# Chuyển sang dạng MC (thường dùng nhất)
df_data, df_info = converter.convert_df_mc()

# Chuyển sang dạng MD (binary columns cho câu MA)
df_data, df_info = converter.convert_df_md()
```

> Câu MA ở dạng MC được **đánh lại mã theo vị trí cột** (cột thứ 4 → mã 4, kể cả khi mã gốc là 97). Recode lại nếu cần giữ mã gốc.

**Cấu trúc df_info:**

| Cột | Nội dung |
|-----|----------|
| `var_name` | Tên biến (ví dụ: Q1, Gender) |
| `var_lbl` | Nhãn câu hỏi |
| `var_type` | Loại biến: SA, MA, SA_mtr, MA_mtr, FT, NUM, RANKING |
| `val_lbl` | Dict mã → nhãn: `{'1': 'Nam', '2': 'Nữ'}` |

---

## Bước 2 — DataProcessing: Xử lý và biến đổi dữ liệu

```python
dp = DataProcessing(df_data=df_data, df_info=df_info)
```

### Thêm câu hỏi — add_qres()

```python
import numpy as np

# Cấu trúc: { 'TênBiến': ['Nhãn', 'LoạiBiến', {mã: nhãn}, giá_trị_mặc_định] }
# Câu MA: dùng 'TênBiến|N' với N = số cột
df_data, df_info = dp.add_qres({
    'AgeGroup':  ['Nhóm tuổi', 'SA', {1: '18-24', 2: '25-34', 3: '35-44', 4: '45+'}, np.nan],
    'Channel|4': ['Kênh mua hàng', 'MA', {1: 'Siêu thị', 2: 'Online', 3: 'Chợ', 4: 'Khác'}, np.nan],
    'Score':     ['Điểm đánh giá', 'NUM', {}, np.nan],
})
```

> Không trộn giá trị mặc định chuỗi (`''`) với `np.nan` trong cùng một lần gọi: các cột mới sẽ thành kiểu chuỗi.

### Xóa câu hỏi — remove_qres()

```python
df_data, df_info = dp.remove_qres(lst_col=['Q_Old', 'Q_Temp'])
```

### Cập nhật giá trị — update_qres_data()

| method | Hành động |
|--------|-----------|
| `'a'` (append) | Thêm giá trị vào các ô đang trống (câu MA) trong điều kiện lọc |
| `'r'` (remove) | Xóa (đặt NaN) các giá trị khớp |
| `'o'` (overlay) | Ghi đè toàn bộ giá trị trong vùng điều kiện |

```python
df_data = dp.update_qres_data(query_fil='Age >= 18 and Age <= 24', qre_name='AgeGroup', lst_val_update=[1], method='o')
```

### Các phương thức khác

```python
# Đếm số lựa chọn câu MA (cột Q_Channel_1, Q_Channel_2, ...) → thêm cột Q_Channel_Count
df_data, df_info = dp.count_ma_choice(lst_ma_qre=['Q_Channel'])

# Dồn giá trị MA về trái, loại bỏ NaN giữa ('Tên|N' với N = số cột)
df_data = dp.align_ma_values_to_left(qre_name='Q_Channel|5')

# Gộp nhiều câu thành 1 câu MA
# lst_merge = cột OUTPUT (phải có sẵn trong df_info, tạo trước bằng add_qres); lst_to_merge = cột INPUT
# dk_code bị loại nếu respondent có chọn mã khác
df_data = dp.merge_qres(
    lst_merge=['Q_Combined_1', 'Q_Combined_2', 'Q_Combined_3', 'Q_Combined_4'],
    lst_to_merge=['Q1_1', 'Q1_2', 'Q2_1', 'Q2_2'],
    dk_code=99,
)

# Chuyển cột % dạng chuỗi sang số (Price_1_1, Price_1_2, Price_1_3)
df_data, df_info = dp.convert_percentage(lst_qres=['Price_1|3'], is_check_sum=True)

# Điểm xếp hạng: prefix của các cột Q_Rank_1, Q_Rank_2, ... → thêm Q_ScoreOfAtt_<mã>
# Chỉ truyền 1 câu mỗi lần gọi (xem Hạn chế đã biết)
df_data, df_info = dp.calculate_ranking_score(lst_ranking_qre=['Q'])

# One-hot encoding: {prefix cột mới: regex các cột nguồn} → thêm Q_Usage_BIN_<mã>
df_data, df_info = dp.one_hot_encoding(dict_one_hot_regex={'Q_Usage_BIN': '^Q_Usage_[0-9]{1,2}$'})
```

---

## Bước 3 — DataTranspose: Stack / Unstack dữ liệu

Dùng cho nghiên cứu nhiều sản phẩm (Product Testing, Brand Tracking...).

### Wide → Long (to_stack)

```python
dict_stack_structure = {
    'id_col':  'ResID',
    'sp_col':  'Ma_SP',
    'lst_scr': ['Gender', 'Age', 'City', 'HHI'],   # Cột screener (copy sang tất cả hàng)
    'dict_sp': {
        1: {'Ma_SP1': 'Ma_SP', 'Q1_SP1': 'Q1', 'Q2_SP1': 'Q2', 'Q3_SP1': 'Q3'},
        2: {'Ma_SP2': 'Ma_SP', 'Q1_SP2': 'Q1', 'Q2_SP2': 'Q2', 'Q3_SP2': 'Q3'},
    },
    'lst_fc': ['Awareness1', 'Frequency', 'Awareness2', 'Perception']  # Cột chung
}

df_data_stack, df_info_stack = DataTranspose.to_stack(df_data, df_info, dict_stack_structure)
```

### Long → Wide (to_unstack)

```python
dict_unstack_structure = {
    'id_col':            'ResID',
    'sp_col':            'Ma_SP',
    'lst_col_part_head': ['Gender', 'Age', 'City', 'HHI'],
    'lst_col_part_body': ['Q1', 'Q2', 'Q3'],
    'lst_col_part_tail': ['Awareness1', 'Frequency', 'Awareness2', 'Perception']
}

df_data_unstack, df_info_unstack = DataTranspose.to_unstack(df_data_stack, df_info_stack, dict_unstack_structure)
```

---

## Bước 4 — CodeframeReader: Đọc bảng codeframe câu hỏi mở

```python
cf = CodeframeReader(cf_file_name='codeframe_Q15.xlsx')
cf.to_dataframe_file()
cf.read_dataframe_output_file()

# Tích hợp kết quả vào DataProcessing
df_data, df_info = dp.add_qres(cf.dict_add_new_qres_oe)
```

**Yêu cầu file input:** Sheet có hậu tố `-CODE` chứa cột RESPONDENTID, COLUMN_NAME, VERBATIM, CODING.

---

## Bước 5 — LSMCalculation: Tính điểm LSM

```python
df_data, df_info = LSMCalculation.cal_lsm_6(df_data, df_info)
# Thêm vào df_data: CC1_Score → CC6_Score, LSM_Score, LSM (0–19)
```

> **Yêu cầu:** df_data phải có sẵn các cột CC1, CC2_1...CC2_10, CC3_1...CC3_7, v.v. theo cấu trúc bảng hỏi LSM chuẩn.

---

## Bước 6 — DataAnalysis: Phân tích thống kê nâng cao

```python
da = DataAnalysis(df_data=df_data, df_info=df_info)
```

### Penalty Analysis (JAR)

```python
dict_pen = {
    'HCM': {
        'query':    'City == 1',
        'prod_pre': 'Brand',
        'ol_qre':   'Q_OverallLiking',
        'jar_qres': {
            'Sweetness': {
                'label': 'Độ ngọt',
                'jar':   {'code': [3]},
                'b2b':   {'code': [1, 2], 'label': 'Quá nhạt'},
                't2b':   {'code': [4, 5], 'label': 'Quá ngọt'},
            }
        }
    }
}

da.penalty_analysis(dict_define_pen=dict_pen, output_name='penalty')  # tự thêm đuôi .xlsx
```

### Hồi quy tuyến tính

```python
result = da.linear_regression(
    dict_define_linear={
        'All': {
            'str_query':        'City > 0',               # '' = không lọc
            'dependent_vars':   ['Q_Purchase'],           # nhiều biến → lấy trung bình
            'explanatory_vars': ['Q1', 'Q2', 'Q3', 'Q4'],
        }
    },
    output_name='regression'  # tự thêm đuôi .xlsx; None = không xuất file
)
# result['All']['df_linear'] — DataFrame hệ số hồi quy (đã chuẩn hóa)
```

### Tương quan

```python
da.correlation(
    dict_define_corr={
        'All': {
            'str_query':        'City > 0',
            'dependent_vars':   ['Q_Overall'],
            'explanatory_vars': ['Q1', 'Q2', 'Q3', 'Q4', 'Q5'],
        }
    },
    output_name='corr.xlsx'  # hàm này KHÔNG tự thêm đuôi, phải ghi .xlsx
)
```

### Key Driver Analysis

```python
da.key_driver_analysis(
    dict_kda={
        'All': {
            'str_query':               'City > 0',
            'axis_y_dependent_vars':   ['Q_Overall'],
            'axis_x_dependent_vars':   None,   # hoặc list biến cho trục x
            'axis_x_explanatory_vars': None,   # None = trục x là mean của explanatory_vars
            'explanatory_vars':        ['Q_Att1', 'Q_Att2', 'Q_Att3'],
        }
    },
    output_name='kda'  # tự thêm đuôi .xlsx
)
```

Ngoài ra: `correspondence_analysis` (cần thư viện `prince`), `price_sensitive_metric` (Van Westendorp), `k_mean_segmentation`.

---

## Bước 7 — Export dữ liệu (.sav / .xlsx)

```python
dict_dfs = {
    1: {'data': df_data,       'info': df_info,       'tail_name': 'ByCode',  'sheet_name': 'ByCode',  'is_recode_to_lbl': False},
    2: {'data': df_data,       'info': df_info,       'tail_name': 'ByLabel', 'sheet_name': 'ByLabel', 'is_recode_to_lbl': True},
    3: {'data': df_data_stack, 'info': df_info_stack, 'tail_name': 'Stack',   'sheet_name': 'Stack',   'is_recode_to_lbl': False},
}

converter.generate_multiple_data_files(
    dict_dfs=dict_dfs,
    is_export_sav=True,
    is_export_xlsx=True,
    is_zip=True   # Lưu ý: sau khi nén, các file .sav/.xlsx gốc bị xóa, chỉ còn file .zip
)
```

---

## Bước 8 — DataTableGenerator + TableFormatter: Tạo và format bảng

### Cấu hình lst_side_qres (câu hỏi hàng / stub)

```python
lst_side_qres = [
    # Câu SA
    {"qre_name": "Gender"},
    {"qre_name": "Age", "qre_lbl": "{lbl} (Nam)", "qre_filter": "Gender == 1"},   # đổi nhãn + lọc riêng câu này

    # Câu MA: thêm $ trước tên biến
    {"qre_name": "$CC3"},

    # Câu MA với net / combine
    {"qre_name": "$Q15", "cats": {
        'net_code': {
            '900001|combine|Group 1 + 2': {'1': 'Yellow/dull teeth', '3': 'Dental plaque', '2': 'Sensitive teeth', '4': 'Caries'},
            '900002|net|Group 1':         {'1': 'Yellow/dull teeth', '3': 'Dental plaque'},
            '900003|net|Group 2':         {'2': 'Sensitive teeth', '4': 'Caries'},
        },
        '8': 'Other (specify)',
        '9': 'No problem',
    }},

    # Câu scale: net + mean + dòng tính toán
    {
        "qre_name": "Perception",
        "cats": {
            '1': 'Totally disagree', '2': 'Disagree', '3': 'Neutral', '4': 'Agree', '5': 'Totally agree',
            'net_code': {
                '900001|combine|B2B': {'1': 'Totally disagree', '2': 'Disagree'},
                '900003|combine|T2B': {'4': 'Agree', '5': 'Totally agree'},
            }
        },
        "mean": {1: 1, 2: 2, 3: 3, 4: 4, 5: 5},          # thêm 3 dòng: Mean, Std, Standard Error
        "calculate": {"T2B - B2B": "[T2B] - [B2B]"},      # {nhãn dòng mới: công thức}, [..] = cat_lbl của dòng
    },

    # Câu NUM: mặc định Mean, Std, Min, Max, Quantile 25/50/75%, Standard Error
    {"qre_name": "Income"},
]
```

**Tùy chọn của mỗi câu trong stub:**

| Key | Ý nghĩa |
|-----|---------|
| `qre_name` | Tên biến. `$Tên` cho câu MA; `Tên#combine(Q1_1,Q1_2,Q2_1)` để gộp nhiều cột MA (viết liền, không có dấu cách) |
| `qre_lbl` | Nhãn hiển thị; `{lbl}` = nhãn gốc trong df_info |
| `qre_filter` | Pandas query lọc riêng câu này |
| `cats` | Danh sách mã hiển thị + `net_code`. Có thể khai báo thiếu mã |
| `mean` | `{mã: hệ số}` để tính Mean / Std / Standard Error |
| `calculate` | `{nhãn: công thức}` dùng `[cat_lbl]` của các dòng |
| `friedman` | `{mã: hệ số}` để thêm dòng p-value kiểm định Friedman |
| `sort` | **Hiện chưa có tác dụng** (code sắp xếp đang tắt) |

**Ký hiệu net_code:**

| Ký hiệu | Ý nghĩa |
|---------|---------|
| `900001\|net\|Tên` | Net: dòng tổng + liệt kê các mã con bên dưới |
| `900001\|combine\|Tên` | Combine: chỉ hiện dòng tổng |

### Cấu hình dict_header_qres (câu hỏi cột / banner)

`dict_header_qres` là dict các nhóm header: mỗi key là một nhóm, value là list các cấp (tối đa 5 cấp), mỗi cấp là list câu hỏi. Các nhóm phải có cùng số cấp. Trong mỗi nhóm, các cột được đánh ký tự A, B, C… để đọc kết quả sig.

```python
dict_header_qres = {
    # Nhóm header 1: 1 cấp
    'hd_1': [
        [
            {
                "qre_name": "Age",
                "qre_lbl":  "Age",
                "cats": {
                    'TOTAL': 'Total',   # cột Total (xem is_sig_total)
                    '2': '18 - 24',
                    '3': '25 - 30',
                    '4': '31 - 39',
                },
            },
            # Custom query dùng ký hiệu @
            {
                "qre_name": "@City2",
                "qre_lbl":  "Location",
                "cats": {
                    'City.isin([1, 5, 10, 11, 12])': 'All South',
                    'City.isin([2, 4, 16, 17, 18])': 'All North',
                },
            },
        ],
    ],

    # Nhóm header 2: câu MA dùng $, cats rỗng = lấy codelist trong df_info
    'hd_2': [
        [
            {"qre_name": "$Q_Channel", "qre_lbl": "Channel", "cats": {}},
        ],
    ],
}
```

### Chạy bảng

```python
lst_func_to_run = [
    {
        'tables_to_run': ['Tbl_1_Pct', 'Tbl_1_Count'],   # [] = chạy tất cả bảng
        'tables_format': {

            "Tbl_1_Pct": {
                "tbl_name":             "Table 1 - Pct",   # tên sheet
                "tbl_filter":           "City > 0",
                "is_count":             0,       # 0 = %, 1 = count
                "is_pct_sign":          1,       # Hiển thị dấu %
                "decimal":              0,       # Số chữ số thập phân của ô % (mặc định 0)
                "is_hide_oe_zero_cats": 1,
                "is_hide_zero_cols":    0,       # Giữ 0 (xem Hạn chế đã biết)
                "weight_var":           '',      # Biến trọng số (để trống nếu không dùng; không dùng cùng sig)
                "sig_test_info": {
                    "sig_type":     "ind",       # 'ind' = độc lập, 'rel' = phụ thuộc, '' = không sig
                    "sig_cols":     [],          # [] = tất cả cặp cột; hoặc [["A", "B"], ["B", "C"]] (xem Lưu ý)
                    "lst_sig_lvl":  [90, 95],    # Mức tin cậy, bắt buộc tăng dần (tối đa 2 mức)
                    "is_sig_total": True,        # True (mặc định): Total = A, có sig; False: Total không sig, cột đầu tiên còn lại = A
                },
                "lst_side_qres":    lst_side_qres,
                "dict_header_qres": dict_header_qres,
            },

            "Tbl_1_Count": {
                "tbl_name":             "Table 1 - Count",
                "tbl_filter":           "City > 0",
                "is_count":             1,
                "is_pct_sign":          0,
                "is_hide_oe_zero_cats": 1,
                "is_hide_zero_cols":    0,
                "weight_var":           '',
                "sig_test_info": {"sig_type": "", "sig_cols": [], "lst_sig_lvl": []},
                "lst_side_qres":    lst_side_qres,
                "dict_header_qres": dict_header_qres,
            },
        },
    },
]

# Toàn bộ script phải nằm trong `if __name__ == '__main__':` (có thể chạy nhiều tiến trình)
if __name__ == '__main__':

    dtg = DataTableGenerator(
        df_data=df_data,
        df_info=df_info,
        xlsx_name=str_tbl_file_name,
        is_validate_data=True,   # False = bỏ qua kiểm tra mã ngoài codelist & trùng giá trị MA
    )
    dtg.run_tables_by_js_files(lst_func_to_run)   # hoặc dtg.run_tables(lst_group_tables=lst_func_to_run)

    # Gộp bảng từ data khác (ví dụ data stack) vào CÙNG file: generator mới, cùng xlsx_name, is_append=True
    # dtg_stack = DataTableGenerator(df_data=df_data_stack, df_info=df_info_stack, xlsx_name=str_tbl_file_name)
    # dtg_stack.run_tables_by_js_files(lst_func_to_run_stack, is_append=True)

    # Format bảng (cần Excel) → lưu thành '<tên file> - <Mon dd>.xlsx'
    dtf = TableFormatter(xlsx_name=str_tbl_file_name)
    dtf.format_workbook()   # is_visible=True để xem Excel khi đang format
```

---

## Lưu ý quan trọng

| Chủ đề | Lưu ý |
|--------|-------|
| File input | File QMe phải có sheet `Question` và `Data`. ZIP phải chứa `Questions.csv` và `Data.csv`. |
| Câu MA trong bảng | Thêm `$` trước tên biến: `"$Q_Channel"`. Không dùng `$` cho câu SA. |
| Custom query | Thêm `@` trước tên biến và dùng pandas query trong `cats`. |
| `cats` của stub | Có thể khai báo một phần mã (ví dụ ẩn mã DK). Mã không khai báo vẫn nằm trong base nhưng không hiện thành dòng riêng. |
| Sig test được bỏ qua khi | Một trong hai cột có n < 30 (n = số người trả lời câu đó trong cột), một trong hai cột có tỷ lệ/mean = 0, hoặc cả hai = 1. |
| Sig với cột Total | Với `sig_type: 'ind'`, cột có mã `TOTAL` ở cấp header cuối được so với **phần còn lại**: A vs Total = A vs (Total trừ A). Dòng chú thích của bảng ghi: *Total (A) is tested vs the rest of Total*. |
| `is_sig_total` | `True` (mặc định): cột Total mang ký tự **A** (dù nằm ở vị trí nào), các cột khác B, C…, Total được sig với các cột khác. `False`: cột Total không có ký tự và không sig (vẫn tính số liệu), cột đầu tiên còn lại trong nhóm mang ký tự **A**. |
| Mức sig | `lst_sig_lvl` phải tăng dần, ví dụ `[90, 95]`: chữ IN HOA = mức cao (95%), chữ thường = mức thấp (90%). |
| `sig_cols` | Ký tự A, B, C… được đánh riêng trong từng nhóm header, và cùng một `sig_cols` áp dụng cho mọi nhóm. Chỉ dùng được khi: mỗi phần tử là cặp 2 cột (chữ IN HOA), mọi nhóm header có cùng số cột (không tính Total khi `is_sig_total=False`), và các cặp phủ đủ mọi cột. Sai → báo lỗi và dừng trước khi chạy. |
| Trọng số | Không chạy sig cùng trọng số. Standard Error có trọng số tính theo cỡ mẫu hiệu dụng (Σw)²/Σw². |
| `tables_to_run` | Để trống `[]` = chạy tất cả bảng trong `tables_format`. Tên sheet = `tbl_name`. |
| Đa tiến trình | Bảng đầu tiên chạy trong tiến trình hiện tại và được đo thời gian; các bảng còn lại chỉ chạy song song (min(số bảng, CPU cores − 2) tiến trình) khi ước tính chạy tuần tự > `dtg.min_seconds_for_parallel` giây (mặc định 8), vì mỗi tiến trình con tốn ~4 giây khởi động. Đang debug thì luôn chạy tuần tự. |
| DataProcessing | Gán lại kết quả trả về sau mỗi phương thức. Đa số trả về `df_data, df_info`; riêng `align_ma_values_to_left`, `merge_qres`, `update_qres_data` chỉ trả về `df_data`. |

---

## Hạn chế đã biết (2.0.0)

| Chức năng | Hạn chế |
|-----------|---------|
| `APDataConverter(file_name=[...])` | Chưa dùng được với list nhiều file → convert từng file rồi `pd.concat` df_data |
| Recode `CITY` khi convert | Khi biến `CITY` chưa có codelist, tên tỉnh được đổi sang mã theo danh sách 63 tỉnh cũ; tên tỉnh ngoài danh sách sẽ báo lỗi |
| `calculate_ranking_score` | Chỉ truyền 1 câu mỗi lần; lỗi nếu cột xếp hạng có ô trống |
| `one_hot_encoding` | Respondent không trả lời câu gốc nhận 0 thay vì NaN |
| `is_hide_zero_cols = 1` | Làm mất các cột giá trị → giữ 0 |
| `sort` trong stub | Chưa có tác dụng |
| `correspondence_analysis` | Cần `prince`; trên Python 3.14, `prince` (qua `altair`) có thể import lỗi → hàm báo `ImportError` |
| `k_mean_segmentation` | Gọi `plt.show()` → chờ đóng cửa sổ biểu đồ khi chạy |
| Header | Tối đa 26 cột có ký tự (A–Z) trong một nhóm header |
