엑셀 중복 행을 찾아 검토용 파일로 분리하기
네 열이 완전히 같은 행을 찾아 원본 행 번호와 함께 새 엑셀 파일로 모읍니다. 첫 행도 포함해 중복 묶음을 확인합니다.
CSV의 날짜와 작업 시간을 검증하고, 월별 기록 수와 합계 시간을 CSV와 막대그래프로 저장합니다.
이런 분께반복되는 월간 업무 집계를 간단한 파이썬 코드로 처리하고 싶은 사무 담당자
업무 기록 한 행을 하나의 작업 기록으로 보고, date가 속한 달에 hours를 더합니다. record_count는 행 수이며 사람 수나 근무 일수가 아닙니다. hours는 십진수 시간입니다. 1.50은 1시간 30분이며 1시간 50분이 아닙니다.
동봉 데이터는 2026년 1~3월의 가상 업무 기록 9개입니다. 작업 시간을 측정하거나 성과를 평가하는 도구가 아니라, 이미 입력된 숫자를 월별로 합치는 연습입니다.
| 열 | 형식 | 확인 기준 |
|---|---|---|
| record_id | R001 같은 텍스트 | 파일 안에서 고유해야 함 |
| date | 2026-01-06 | YYYY-MM-DD, 실제 존재하는 날짜 |
| task | 자료 정리 | 빈 값 불가 |
| hours | 2.50 | 0 이상 유한수, 소수 둘째 자리까지 |
제목 행, 합계 행, 쉼표가 들어간 숫자는 넣지 않습니다. 중복 ID를 발견하면 이중 집계를 막기 위해 중지합니다. 작업 내용이 같더라도 ID가 다르면 서로 다른 기록으로 합산합니다.
python --version
python -m pip install -r requirements.txt
python example.py설치는 인터넷 연결이 필요합니다. 코드의 BASE는 현재 터미널 위치가 아니라 example.py의 위치를 기준으로 입력과 결과 경로를 정합니다.
날짜는 date.fromisoformat으로 확인하고 YYYY-MM 형식의 월 키를 만듭니다. Decimal로 시간을 더한 뒤 표에 소수 둘째 자리까지 기록합니다. 그래프를 그릴 때만 일반 실수로 바꿉니다. Agg 모드를 사용하므로 그래프 창 대신 PNG가 저장됩니다.
"""Summarize fictional work-log records by calendar month."""
import csv
import math
from collections import defaultdict
from datetime import date
from decimal import Decimal, InvalidOperation
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
BASE = Path(__file__).resolve().parent
HEADERS = ["record_id", "date", "task", "hours"]
def main():
destination = BASE / "outputs"
if destination.exists():
raise ValueError("outputs already exists; rename it before running again.")
totals = defaultdict(lambda: {"records": 0, "hours": Decimal("0")})
seen = set()
with (BASE / "work_log.csv").open(encoding="utf-8-sig", newline="") as file:
reader = csv.DictReader(file)
if reader.fieldnames != HEADERS:
raise ValueError(f"Expected headers: {HEADERS}")
for line, row in enumerate(reader, start=2):
if None in row or any(value is None or not value.strip() for value in row.values()):
raise ValueError(f"Row {line}: missing or extra field.")
if row["record_id"] in seen:
raise ValueError(f"Row {line}: duplicate record_id.")
seen.add(row["record_id"])
parsed_date = date.fromisoformat(row["date"])
if parsed_date.isoformat() != row["date"]:
raise ValueError(f"Row {line}: use YYYY-MM-DD dates.")
try:
hours = Decimal(row["hours"])
except InvalidOperation as error:
raise ValueError(f"Row {line}: hours must be a number.") from error
if not hours.is_finite() or hours < 0 or hours.as_tuple().exponent < -2:
raise ValueError(f"Row {line}: hours must be finite, nonnegative, with at most 2 decimals.")
month = parsed_date.strftime("%Y-%m")
totals[month]["records"] += 1
totals[month]["hours"] += hours
if not totals:
raise ValueError("No work records were found.")
months = sorted(totals)
figure, axis = plt.subplots(figsize=(8, 4.6), layout="constrained")
values = [float(totals[month]["hours"]) for month in months]
if not all(math.isfinite(value) and math.isfinite(value * 1.2) for value in values):
plt.close(figure)
raise ValueError("Monthly total is too large to plot as a finite number.")
bars = axis.bar(months, values, color="#2B6578", width=0.55)
axis.bar_label(bars, labels=[f"{value:.2f}" for value in values], padding=4)
axis.set(title="Monthly work hours | Fictional data", xlabel="Calendar month", ylabel="Hours")
axis.set_ylim(0, max(values) * 1.2 if max(values) > 0 else 1)
axis.spines[["top", "right"]].set_visible(False)
axis.set_axisbelow(True)
axis.grid(axis="y", alpha=0.2)
destination.mkdir(exist_ok=False)
with (destination / "monthly_summary.csv").open("x", encoding="utf-8-sig", newline="") as file:
writer = csv.writer(file)
writer.writerow(["month", "record_count", "total_hours"])
for month in months:
writer.writerow([month, totals[month]["records"], f"{totals[month]['hours']:.2f}"])
figure.savefig(destination / "monthly_hours.png", dpi=160)
plt.close(figure)
total_hours = sum((values["hours"] for values in totals.values()), Decimal("0"))
print(f"Records: {len(seen)}; months: {len(months)}; hours: {total_hours:.2f}")
print("Created outputs/monthly_summary.csv and outputs/monthly_hours.png.")
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, csv.Error) as error:
raise SystemExit(f"Stopped: {error}") from error
outputs/monthly_summary.csv와 outputs/monthly_hours.png가 만들어집니다. 그래프는 입력이 있는 달만 연도·월 순서로 표시합니다. 영어 축은 별도 한글 글꼴 설치 없이 실행하기 위한 선택입니다.
| month | record_count | total_hours |
|---|---|---|
| 2026-01 | 3 | 6.50 |
| 2026-02 | 3 | 7.50 |
| 2026-03 | 3 | 9.50 |
| 전체 확인 합계 | 9 | 23.50 |

| 문제 | 수정 방법 |
|---|---|
| duplicate record_id | 같은 기록을 두 번 넣었는지 확인합니다. |
| day is out of range / Invalid isoformat | 2026-02-30 같은 불가능한 날짜나 날짜 형식을 확인합니다. |
| hours must be a number | 시간에는 2.5처럼 숫자만 넣습니다. 시간 단위나 천 단위 쉼표를 빼세요. |
| hours must be finite… | 음수·NaN·Infinity·소수 셋째 자리 이상을 확인합니다. |
| 한글이 깨짐 | 원본 CSV를 UTF-8로 다시 저장합니다. |
| outputs already exists | 이전 결과를 보관한 뒤 결과 폴더 이름을 바꿉니다. |
Windows 11 (10.0.26200), CPython 3.12.14 (64비트). openpyxl 3.1.5, Matplotlib 3.10.8. 예제별 사용 라이브러리는 requirements.txt에 고정했습니다.
코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.
예제 ZIP 다운로드직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.