CSV 참고문헌 목록에서 중복 DOI와 누락 DOI 확인하기
연구 참고문헌을 CSV로 관리하면서 DOI 문자열을 비교용으로 정규화하고, 중복 DOI와 DOI 누락 항목을 검토 파일로 작성합니다. 원본 CSV는 그대로 유지하며 DOI가 적혀 있다는 이유만으로 유효한 DOI라고 판단하지 않습니다.
분석 전에 데이터셋의 각 열이 무엇을 의미하는지, 어떤 단위와 자료형을 사용하는지, 어떤 값을 허용하는지 문서화합니다. 작은 합성 시험 데이터로 같은 데이터 사전을 간단한 자동 검증에도 사용하는 방법을 확인합니다.
이런 분께 맞아요다른 연구자도 읽고 검토할 수 있도록 데이터셋의 열 정의를 명확하게 정리하려는 연구자를 위한 안내입니다.
열 이름만으로는 연구 데이터를 충분히 설명하기 어렵습니다. 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이 공학적 유효 범위가 되는 것은 아닙니다.
데이터 사전은 데이터셋이나 분석 코드와 함께 버전을 관리하세요. 열 정의, 단위, 부호 규약, 범주가 바뀌면 언제 변경되었는지 기록해야 합니다. 데이터 사전은 실제 분석 중인 데이터 버전을 정확히 설명할 때 가장 유용합니다.
2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · 표준 라이브러리: csv, pathlib, re · 실행하지 않음
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.