Skip to main content

range-validity

Detect IQR outliers (1.5×IQR rule) in numeric variables of a QUIQ-format table and compute Range Validity (% of non-outlier values). SQL version available via DuckDB PERCENTILE_CONT. No API key required. Use for LYDUS data quality assessment of numeric value range plausibility.

Jump to install

Source facts

Repository
28sungmin/m4-add-skills
Last source activity
July 9, 2026 at 10:57
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
range-validity
description
Detect IQR outliers (1.5×IQR rule) in numeric variables of a QUIQ-format table and compute Range Validity (% of non-outlier values). SQL version available via DuckDB PERCENTILE_CONT. No API key required. Use for LYDUS data quality assessment of numeric value range plausibility.
tier
community
category
lydus
parameters
{"quiq_path":{"description":"Path to QUIQ-format CSV file (output of quiq skill).","type":"string"},"save_path":{"description":"Directory path to save output files.","type":"string"},"weight":{"description":"IQR multiplier for outlier bounds (default 1.5). Larger values = less sensitive.","type":"number"}}
# Range Validity Detects **IQR-based outliers** in numeric variables of a QUIQ-format table. A value is an outlier if it falls below `Q25 − weight×IQR` or above `Q75 + weight×IQR`. Range Validity (%) is the proportion of non-outlier values. ## When to Use This Skill - After QUIQ conversion, to assess whether numeric measurements fall within plausible ranges - To identify values that are statistically extreme relative to the variable's distribution - As part of LYDUS quality management assessment ## SQL Support **Available.** `scripts/duckdb.sql` uses `PERCENTILE_CONT` for IQR calculation — faster for large datasets. **Both** the SQL and Python paths produce the per-variable boxplot PNGs (the SQL path draws them with matplotlib from the pooled numeric values); the Python version (`scripts/range_validity.py`) additionally exports the full outlier CSV. ## Filtering Logic | Condition | Value | |-----------|-------| | `Variable_type` | contains `numeric` (case-insensitive) | | `Is_categorical` | = 0 | | `Value` | numeric (non-null, parseable as float) | | Minimum group size | > 1,000 rows per `Variable_name` | > **Note**: IQR is computed per `Variable_name` pooled across all tables (same behavior as original). Outliers are then attributed to each `(Original_table_name, Variable_name)` pair. ## Outlier Detection `IQR = Q75 − Q25` | Bound | Formula | |-------|---------| | Lower | `Q25 − 1.5 × IQR` | | Upper | `Q75 + 1.5 × IQR` | `Range Validity (%) = (Total − Outlier_total) / Total × 100` ## Output | File | Description | |------|-------------| | `range_validity_total.txt` | Overall Range Validity (%), Total Num, Outlier Num | | `range_validity_summary.csv` | Per-(table, variable): Total_num, under/upper/total outlier counts and proportions, Range Validity (%) | | `range_validity_outlier_total.csv` | All outlier rows with Direction ('under'/'upper') | | `range_validity_boxplots/` | Per-variable boxplot PNGs (**always produced** — both SQL and Python paths) | ## How to Run ### SQL 버전 (빠름, boxplot 포함) ```python import os import re import duckdb import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt skill_dir = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(skill_dir, "scripts/duckdb.sql")) as f: sql = f.read() quiq_csv = "/path/to/quiq_3patients.csv" sql = sql.replace("{quiq_csv}", quiq_csv) df = duckdb.sql(sql).df() total_num = df["Total_num"].sum() outlier_num = df["Outlier_total_num"].sum() range_validity = round((total_num - outlier_num) / total_num * 100, 2) print(f"Range Validity (%) = {range_validity}") save_path = "/path/to/output" os.makedirs(save_path, exist_ok=True) df.to_csv(f"{save_path}/range_validity_summary.csv", index=False, encoding="utf-8-sig") with open(f"{save_path}/range_validity_total.txt", "w") as f: f.write(f"Range Validity (%) = {range_validity}\n") f.write(f"Total Num = {total_num}\n") f.write(f"Outlier Num = {outlier_num}\n") # Boxplots — one PNG per (table, variable), same style as the Python version. # Values are the pooled numeric values per Variable_name (matches IQR pooling). box_dir = os.path.join(save_path, "range_validity_boxplots") os.makedirs(box_dir, exist_ok=True) flierprops = dict(marker="o", markerfacecolor="green", markersize=2, linestyle="none", alpha=0.2) for idx, row in df.reset_index(drop=True).iterrows(): table_name, var_name = row["Original_table_name"], row["Variable_name"] vals = duckdb.sql( "SELECT TRY_CAST(Value AS DOUBLE) AS v " f"FROM read_csv_auto('{quiq_csv}', all_varchar=true) " f"WHERE Variable_name = '{var_name}' " "AND Variable_type ILIKE '%numeric%' " "AND TRY_CAST(Is_categorical AS INTEGER) = 0 " "AND TRY_CAST(Value AS DOUBLE) IS NOT NULL" ).df()["v"].tolist() fig = plt.figure(figsize=(3, 6)) ax = fig.add_subplot(111) if vals: ax.boxplot(vals, flierprops=flierprops) ax.set_title(f"{table_name} - {var_name}", fontsize=10, fontweight="bold") else: ax.set_title(f"{table_name} - {var_name}\n(No data)", fontsize=14, color="gray") ax.set_xticks([]); ax.set_yticks([]) fig.set_tight_layout(True) safe_t = re.sub(r'[\\/:*?"<>|]', " ", str(table_name)) safe_v = re.sub(r'[\\/:*?"<>|]', " ", str(var_name)) fig.savefig(os.path.join(box_dir, f"{idx}_{safe_t}_{safe_v}.png")) plt.close(fig) print(f"Saved {len(df):,} rows + {len(df)} boxplots → {save_path}") ``` ### Python 버전 (boxplot + outlier 상세 포함) ```python import pandas as pd from scripts.range_validity import get_range_validity quiq = pd.read_csv("/path/to/quiq.csv", low_memory=False) outlier_df, summary_df, label_vs_boxplot = get_range_validity(quiq, weight=1.5) total_num = summary_df['Total_num'].sum() outlier_num = summary_df['Outlier_total_num'].sum() range_validity = round((total_num - outlier_num) / total_num * 100, 2) print(f"Range Validity (%) = {range_validity}") ``` ### As a script with config ```yaml # config.yaml quiq_path: /path/to/quiq.csv save_path: /path/to/output weight: 1.5 # optional, default 1.5 ``` ```bash python scripts/range_validity.py --config config.yaml ``` ## Critical Notes 1. **IQR pooling** — IQR is computed on all rows for a `Variable_name` regardless of `Original_table_name`. This matches the original LYDUS implementation but means the same IQR bounds apply to all tables containing that variable. 2. **1,000-row threshold** — same group-size rule as preciseness: the filter applies per `Variable_name` total count (pooled). A variable with 600 rows in Table A and 500 rows in Table B (1,100 total) passes. 3. **weight 파라미터** — 기본값 1.5 (표준 Tukey fence). 민감도를 낮추려면 3.0 사용. 4. **원본 코드 개선 사항**: - `dict_dynamic` + `inplace` chained 수정 → `pool` 변수로 정리 - bare `except:` → `except Exception` - `summary_df.loc[len(summary_df)] = list` → row dict 리스트로 수집 후 `pd.DataFrame` - `os.path.join` 사용 (문자열 연결 대신) - `weight` 파라미터 외부 노출 (원본 하드코딩 1.5) 5. **Dependencies** — `pandas`, `numpy`, `matplotlib` (Python); `duckdb` + `matplotlib` (SQL path — matplotlib is needed for the boxplots) ## References - LYDUS 품질관리 프로그램 활용 가이드라인 (비공개 내부 문서) - Original Python implementation: LYDUS_Range_Validity.py (이성민 작성) - Tukey, J.W. (1977). Exploratory Data Analysis.
View on GitHub