열 의미, 단위, 자료형, 허용값을 포함한 데이터 사전 만들기
분석 전에 데이터셋의 각 열이 무엇을 의미하는지, 어떤 단위와 자료형을 사용하는지, 어떤 값을 허용하는지 문서화합니다. 작은 합성 시험 데이터로 같은 데이터 사전을 간단한 자동 검증에도 사용하는 방법을 확인합니다.
연구 참고문헌을 CSV로 관리하면서 DOI 문자열을 비교용으로 정규화하고, 중복 DOI와 DOI 누락 항목을 검토 파일로 작성합니다. 원본 CSV는 그대로 유지하며 DOI가 적혀 있다는 이유만으로 유효한 DOI라고 판단하지 않습니다.
이런 분께 맞아요스프레드시트나 CSV로 참고문헌을 관리하면서 중복 항목과 DOI 누락을 간단히 확인하려는 연구자를 위한 안내입니다.
이 작업은 두 가지 실무 문제를 확인합니다. 참고문헌 행에 DOI 값이 없거나, 단순 텍스트 정규화 후 두 행의 DOI가 같은 경우입니다. DOI 등록기관에 접속하지 않으므로 DOI가 실제로 존재하는지, 정상적으로 연결되는지, 해당 논문에 속하는지까지 증명하지는 않습니다.
스크립트는 https://doi.org/와 doi: 같은 흔한 접두사를 제거하고 앞뒤 공백을 없앤 뒤 비교를 위해 나머지 DOI 문자열을 소문자로 바꿉니다. 원래 DOI 문자열은 출력 파일에 정규화된 값과 함께 그대로 보존합니다.
아래 참고문헌 목록은 이 글을 위해 작성한 합성 데이터입니다. DOI 형태의 문자열도 예제용이며 실제 또는 연결 가능한 출판물이라고 제시하는 값이 아닙니다. references.csv로 저장하세요.
ref_id,title,year,doi
R001,Synthetic bracket study,2024,10.0000/demo.alpha
R002,Synthetic vibration study,2025,10.0000/demo.beta
R003,Synthetic bracket study copy,2024,https://doi.org/10.0000/demo.alpha
R004,Synthetic fatigue note,2023,
R005,Synthetic surrogate study,2026,doi:10.0000/DEMO.GAMMA
R006,Synthetic optimization study,2026,10.0000/demo.delta
참고문헌 행은 6개입니다. DOI 문자열이 있는 행은 5개이고 R004 하나는 DOI가 없습니다. R001과 R003은 표기 방식은 다르지만 정규화하면 같은 값이 됩니다. R005는 대소문자와 doi: 접두사도 비교 전에 정리할 수 있음을 보여줍니다.
| ref_id | 원본 DOI | 정규화 DOI | 예상 문제 |
|---|---|---|---|
| R001 | 10.0000/demo.alpha | 10.0000/demo.alpha | DUPLICATE_DOI |
| R002 | 10.0000/demo.beta | 10.0000/demo.beta | 없음 |
| R003 | https://doi.org/10.0000/demo.alpha | 10.0000/demo.alpha | DUPLICATE_DOI |
| R004 | MISSING_DOI | ||
| R005 | doi:10.0000/DEMO.GAMMA | 10.0000/demo.gamma | 없음 |
| R006 | 10.0000/demo.delta | 10.0000/demo.delta | 없음 |
예상 결과는 참고문헌 6개, DOI 문자열이 있는 행 5개, DOI 누락 행 1개, 두 행이 포함된 중복 DOI 그룹 1개입니다. 정규화 후 비어 있지 않은 DOI 5개에는 서로 다른 값이 4개 있습니다.
다음 스크립트를 reference_list_check.py로 저장하세요. 전체 참고문헌의 정규화 사본과 검토가 필요한 행만 들어 있는 issues.csv를 별도로 작성합니다. 원본 references.csv는 읽기만 합니다.
import csv
from pathlib import Path
SOURCE = Path("references.csv")
OUTPUT_DIR = Path("outputs") / "reference_list_result"
NORMALIZED = OUTPUT_DIR / "normalized_references.csv"
ISSUES = OUTPUT_DIR / "issues.csv"
REQUIRED = {"ref_id", "title", "year", "doi"}
def normalize_doi(value: str) -> str:
text = value.strip()
lowered = text.lower()
prefixes = (
"https://doi.org/",
"http://doi.org/",
"doi:",
)
for prefix in prefixes:
if lowered.startswith(prefix):
text = text[len(prefix):].strip()
break
return text.lower()
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}")
rows = []
with SOURCE.open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream)
if reader.fieldnames is None or not REQUIRED.issubset(reader.fieldnames):
raise ValueError("CSV is missing a required column.")
seen_ids = set()
for row in reader:
ref_id = row["ref_id"].strip()
if not ref_id:
raise ValueError("ref_id must not be blank.")
if ref_id in seen_ids:
raise ValueError(f"Duplicate ref_id: {ref_id}")
seen_ids.add(ref_id)
rows.append({
"ref_id": ref_id,
"title": row["title"],
"year": row["year"],
"doi": row["doi"],
"normalized_doi": normalize_doi(row["doi"]),
})
by_doi = {}
for row in rows:
doi = row["normalized_doi"]
if doi:
by_doi.setdefault(doi, []).append(row["ref_id"])
duplicate_dois = {
doi: ids for doi, ids in by_doi.items() if len(ids) > 1
}
issue_rows = []
for row in rows:
doi = row["normalized_doi"]
if not doi:
issue_rows.append({
"ref_id": row["ref_id"],
"issue": "MISSING_DOI",
"normalized_doi": "",
"related_ref_ids": "",
})
elif doi in duplicate_dois:
issue_rows.append({
"ref_id": row["ref_id"],
"issue": "DUPLICATE_DOI",
"normalized_doi": doi,
"related_ref_ids": ";".join(duplicate_dois[doi]),
})
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir()
normalized_fields = ["ref_id", "title", "year", "doi", "normalized_doi"]
with NORMALIZED.open("x", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=normalized_fields)
writer.writeheader()
writer.writerows(rows)
issue_fields = ["ref_id", "issue", "normalized_doi", "related_ref_ids"]
with ISSUES.open("x", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=issue_fields)
writer.writeheader()
writer.writerows(issue_rows)
missing_count = sum(row["issue"] == "MISSING_DOI" for row in issue_rows)
print(f"References checked: {len(rows)}.")
print(f"Missing DOI rows: {missing_count}.")
print(f"Duplicate DOI groups: {len(duplicate_dois)}.")
print(f"Issue rows: {len(issue_rows)}.")
print(f"Output folder: {OUTPUT_DIR.as_posix()}")
if __name__ == "__main__":
main()
issues.csv에는 데이터 행이 3개 있어야 합니다. 정규화된 DOI가 중복되는 R001과 R003, 그리고 DOI가 없는 R004입니다.
ref_id,issue,normalized_doi,related_ref_ids
R001,DUPLICATE_DOI,10.0000/demo.alpha,R001;R003
R003,DUPLICATE_DOI,10.0000/demo.alpha,R001;R003
R004,MISSING_DOI,,
아래 예상 콘솔 출력은 합성 목록과 스크립트를 바탕으로 손으로 도출한 결과이며 실제 실행 로그가 아닙니다.
References checked: 6.
Missing DOI rows: 1.
Duplicate DOI groups: 1.
Issue rows: 3.
Output folder: outputs/reference_list_result| 문제 | 확인할 사항 |
|---|---|
| 같은 논문이 서로 다른 DOI 표기로 입력됨 | 비교 전에 흔한 DOI URL과 doi: 접두사를 정규화하세요. |
| 빈 DOI가 중복으로 처리됨 | DOI 누락 검사와 DOI 중복 그룹화를 분리하세요. |
| 서로 다른 논문의 제목이 비슷함 | 제목 유사성만으로 중복 참고문헌이라고 판단하지 마세요. |
| DOI처럼 보이는 문자열이 있음 | 값이 존재한다는 사실만으로 유효성을 보장하지 않습니다. 중요한 참고문헌은 신뢰할 수 있는 메타데이터 출처에서 확인하세요. |
| 기존 검토 결과가 덮어써짐 | 이전 검토 기록을 유지할 수 있도록 새로운 출력 폴더를 사용하세요. |
DOI 메타데이터가 있는 경우 DOI 비교는 유용하지만 DOI가 없다고 해서 참고문헌이 잘못되었거나 불완전하다는 뜻은 아닙니다. 일부 출판물이나 문서 유형은 DOI가 없을 수 있으며, 오래된 자료는 다른 식별자나 수동 서지 확인이 필요할 수 있습니다.
이 스크립트는 문자열 정규화만 수행합니다. DOI 링크를 실제로 확인하거나 Crossref 같은 등록기관에 질의하지 않으며, 제목이나 저자 검증, 철회 여부 확인, 서로 다른 DOI가 같은 연구의 다른 버전인지 판정하는 작업도 하지 않습니다.
실제 연구 데이터베이스에서는 저자, 제목, 연도, 저널, 권호, 페이지, DOI, 자체 reference ID 등을 함께 관리하는 편이 좋습니다. 자동 중복 탐지는 참고문헌 삭제 명령이 아니라 검토 보조 수단으로 사용하세요.
2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · 표준 라이브러리: csv, pathlib · 실행하지 않음
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.