공개자료를 모을 때 출처·기준일·단위를 함께 기록하기
자료명과 URL만 모으는 대신 기준일·게시일·확인일·단위·이용조건을 한 행에 기록합니다. 코드 없이 쓰는 CSV 템플릿과 체크리스트를 제공합니다.
분석 전에 데이터셋의 각 열이 무엇을 의미하는지, 어떤 단위와 자료형을 사용하는지, 어떤 값을 허용하는지 문서화합니다. 작은 합성 시험 데이터로 같은 데이터 사전을 간단한 자동 검증에도 사용하는 방법을 확인합니다.
이런 분께다른 연구자도 읽고 검토할 수 있도록 데이터셋의 열 정의를 명확하게 정리하려는 연구자를 위한 안내입니다.
열 이름만으로는 연구 데이터를 충분히 설명하기 어렵습니다. speed는 차량 속도일 수도 있고 휠 속도나 각속도일 수도 있습니다. accel도 종방향, 횡방향, 전체 가속도 중 무엇인지 알 수 없습니다. 데이터 사전은 이후 분석자가 의미를 추측하지 않도록 의도한 정의를 기록합니다.
이 글에서는 각 열에 대해 column name, meaning, unit, data type, allowed values, example의 여섯 가지 정보를 기록합니다. allowed values에는 범주, 수치 범위, 고유성 규칙, 간단한 문자열 패턴 등을 적을 수 있습니다.
아래 데이터는 이 글을 위해 작성한 합성 데이터입니다. test_runs.csv로 저장하세요. 처음 네 행은 의도한 규칙을 따르고, 다섯 번째 행에는 데이터 사전을 이용해 검출할 수 있는 문제 네 개를 의도적으로 넣었습니다.
run_id,speed_kmh,accel_m_s2,road_surface,test_status
R001,30,0.5,dry,complete
R002,50,-1.2,wet,complete
R003,70,0.0,dry,review
R004,40,-0.8,wet,complete
R005,135,fast,snow,done
행은 5개이고 열은 5개입니다. R005에는 허용 범위를 초과한 속도, 숫자가 아닌 가속도, 허용되지 않은 노면 범주, 허용되지 않은 시험 상태가 들어 있습니다. run_id 자체는 정상입니다.
데이터 사전은 분석 코드를 열어보지 않아도 이해할 수 있어야 합니다. 비슷한 측정값과 구분할 수 있을 정도로 meaning을 구체적으로 작성하세요.
| column_name | meaning | unit | type | allowed_values | example |
|---|---|---|---|---|---|
| run_id | 한 번의 시험 run을 구분하는 고유 식별자 | string | R 다음에 정확히 숫자 3개; 고유값 | R001 | |
| speed_kmh | 해당 레코드 시작 시 차량 속도 | km/h | integer | 0 이상 120 이하 | 50 |
| accel_m_s2 | 종방향 가속도 | m/s^2 | float | -5.0 이상 5.0 이하 | -1.2 |
| road_surface | 시험에 사용한 노면 조건 | category | dry;wet | wet | |
| test_status | 시험 레코드의 검토 상태 | category | complete;review | complete |
식별자와 범주형 라벨은 물리적 측정량이 아니므로 unit을 비워 두는 것이 의도된 상태입니다. type은 스프레드시트가 자동으로 추정한 형식이 아니라 데이터가 의도한 표현 방식을 의미합니다.
다음 스크립트를 data_dictionary_check.py로 저장하세요. 열 정의는 SPEC에 한 번만 작성합니다. 스크립트는 이 정의를 data_dictionary.csv로 내보내고 동일한 규칙으로 각 데이터 행을 검사합니다. 원본 test_runs.csv는 읽기만 합니다.
import csv
import re
from pathlib import Path
SOURCE = Path("test_runs.csv")
OUTPUT_DIR = Path("outputs") / "data_dictionary_result"
DICTIONARY = OUTPUT_DIR / "data_dictionary.csv"
ISSUES = OUTPUT_DIR / "validation_issues.csv"
SPEC = [
{
"column_name": "run_id",
"meaning": "Unique identifier for one test run",
"unit": "",
"type": "string",
"allowed_values": "R followed by exactly 3 digits; unique",
"example": "R001",
},
{
"column_name": "speed_kmh",
"meaning": "Vehicle speed at the start of the record",
"unit": "km/h",
"type": "integer",
"allowed_values": "0 to 120 inclusive",
"example": "50",
},
{
"column_name": "accel_m_s2",
"meaning": "Longitudinal acceleration",
"unit": "m/s^2",
"type": "float",
"allowed_values": "-5.0 to 5.0 inclusive",
"example": "-1.2",
},
{
"column_name": "road_surface",
"meaning": "Road-surface condition used for the test",
"unit": "",
"type": "category",
"allowed_values": "dry;wet",
"example": "wet",
},
{
"column_name": "test_status",
"meaning": "Review state of the test record",
"unit": "",
"type": "category",
"allowed_values": "complete;review",
"example": "complete",
},
]
def issue(row_number, column, value, problem):
return {
"row_number": row_number,
"column_name": column,
"value": value,
"issue": problem,
}
def main() -> None:
if not SOURCE.is_file():
raise FileNotFoundError(f"Source CSV not found: {SOURCE}")
if OUTPUT_DIR.exists():
raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")
expected_columns = [item["column_name"] for item in SPEC]
issues = []
seen_run_ids = set()
row_count = 0
with SOURCE.open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream)
if reader.fieldnames != expected_columns:
raise ValueError(
f"Header mismatch. Expected {expected_columns}, got {reader.fieldnames}."
)
for row_number, row in enumerate(reader, start=2):
row_count += 1
run_id = row["run_id"].strip()
if re.fullmatch(r"R[0-9]{3}", run_id) is None:
issues.append(issue(row_number, "run_id", run_id, "PATTERN_ERROR"))
elif run_id in seen_run_ids:
issues.append(issue(row_number, "run_id", run_id, "DUPLICATE_VALUE"))
seen_run_ids.add(run_id)
try:
speed = int(row["speed_kmh"])
except ValueError:
issues.append(issue(row_number, "speed_kmh", row["speed_kmh"], "TYPE_ERROR"))
else:
if not 0 <= speed <= 120:
issues.append(issue(row_number, "speed_kmh", row["speed_kmh"], "OUT_OF_RANGE"))
try:
accel = float(row["accel_m_s2"])
except ValueError:
issues.append(issue(row_number, "accel_m_s2", row["accel_m_s2"], "TYPE_ERROR"))
else:
if not -5.0 <= accel <= 5.0:
issues.append(issue(row_number, "accel_m_s2", row["accel_m_s2"], "OUT_OF_RANGE"))
if row["road_surface"] not in {"dry", "wet"}:
issues.append(issue(row_number, "road_surface", row["road_surface"], "ALLOWED_VALUE_ERROR"))
if row["test_status"] not in {"complete", "review"}:
issues.append(issue(row_number, "test_status", row["test_status"], "ALLOWED_VALUE_ERROR"))
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir()
dictionary_fields = [
"column_name", "meaning", "unit", "type", "allowed_values", "example"
]
with DICTIONARY.open("x", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=dictionary_fields)
writer.writeheader()
writer.writerows(SPEC)
issue_fields = ["row_number", "column_name", "value", "issue"]
with ISSUES.open("x", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=issue_fields)
writer.writeheader()
writer.writerows(issues)
print(f"Rows checked: {row_count}.")
print(f"Dictionary columns: {len(SPEC)}.")
print(f"Validation issues: {len(issues)}.")
print(f"Output folder: {OUTPUT_DIR.as_posix()}")
if __name__ == "__main__":
main()
문제는 R005에서만 발생해야 합니다. CSV 헤더가 1행이므로 R005는 실제 CSV 6행입니다. 검증 문제는 총 4개가 기록되어야 합니다.
row_number,column_name,value,issue
6,speed_kmh,135,OUT_OF_RANGE
6,accel_m_s2,fast,TYPE_ERROR
6,road_surface,snow,ALLOWED_VALUE_ERROR
6,test_status,done,ALLOWED_VALUE_ERROR
아래 예상 콘솔 출력은 합성 데이터셋과 스크립트를 바탕으로 손으로 도출한 결과이며 실제 실행 로그가 아닙니다.
Rows checked: 5.
Dictionary columns: 5.
Validation issues: 4.
Output folder: outputs/data_dictionary_result| 문제 | 확인할 사항 |
|---|---|
| 새 데이터 열에 데이터 사전 항목이 없음 | 분석 전에 데이터 사전을 업데이트해 의미와 단위를 기록하세요. |
| 같은 개념이 서로 다른 단위로 존재 | 열을 분리하거나 단위를 명시적으로 통일하세요. 기억에 의존하지 마세요. |
| 시간이 지나며 범주 라벨이 바뀜 | 허용값을 정의하고 새로운 노면 조건 같은 의도된 추가 항목을 문서화하세요. |
| 스프레드시트가 정수형 ID로 바꿈 | 앞자리 0이나 문자열 패턴이 중요하면 식별자를 string으로 관리하세요. |
| 검증은 통과하지만 의미가 틀림 | 구조 검사는 잘못된 과학적 정의나 잘못 붙은 센서 채널 이름을 자동으로 찾지 못합니다. |
실제 연구 데이터 사전에는 여기서 사용한 여섯 필드보다 더 많은 정보가 필요할 수 있습니다. 프로젝트에 따라 source system, sensor location, coordinate direction, sampling rate, nullable 여부, 정밀도, 계산 방법, valid range, missing-value code, 담당자 등을 추가하세요.
허용값은 현재 파일에서 우연히 관찰된 최솟값과 최댓값이 아니라 과학적 의미를 기준으로 정의해야 합니다. 한 파일의 speed가 30에서 70 km/h 사이에만 있다고 해서 자동으로 30에서 70이 공학적 유효 범위가 되는 것은 아닙니다.
데이터 사전은 데이터셋이나 분석 코드와 함께 버전을 관리하세요. 열 정의, 단위, 부호 규약, 범주가 바뀌면 언제 변경되었는지 기록해야 합니다. 데이터 사전은 실제 분석 중인 데이터 버전을 정확히 설명할 때 가장 유용합니다.
ChatGPT Python tool · Python 3.13.5 · standard library: csv, pathlib, re
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.