업무 기록을 월별 집계표와 그래프로 만들기
CSV의 날짜와 작업 시간을 검증하고, 월별 기록 수와 합계 시간을 CSV와 막대그래프로 저장합니다.
네 열이 완전히 같은 행을 찾아 원본 행 번호와 함께 새 엑셀 파일로 모읍니다. 첫 행도 포함해 중복 묶음을 확인합니다.
이런 분께엑셀 데이터를 취합한 뒤 중복 입력을 확인하는 사무 담당자
이 예제의 중복 기준은 order_id, customer, item, quantity 네 값이 모두 같은 행입니다. 주문 번호 하나만 같은 행은 중복으로 보지 않습니다. 문자열의 공백이나 대소문자도 자동으로 바꾸지 않습니다.
중복을 삭제하기 전에 같은 내용을 가진 모든 행을 Duplicates 시트에 모읍니다. 처음 등장한 행도 포함합니다. 한 번만 나온 행은 Unique 시트에 남겨 전체 8행을 빠짐없이 검토할 수 있게 합니다.
orders.xlsx의 시트 이름은 Orders이며 첫 행의 열 이름과 순서가 아래와 같아야 합니다. 열을 추가해도 헤더 오류가 나므로 실무 파일을 바로 덮어씌우기보다 필요한 네 열을 복사한 연습용 파일을 만드세요.
| 열 | 역할 | 예시 |
|---|---|---|
| order_id | 문자열 주문 번호 | O-1001 |
| customer | 문자열 고객명 | Alpha |
| item | 문자열 품목 | Sensor |
| quantity | 양의 정수 수량 | 2 |
완전히 빈 행은 건너뜁니다. 일부 값만 비어 있거나 수식이 들어 있으면 중지합니다. 주문 번호·고객명·품목은 비어 있지 않은 문자열이어야 합니다. 숫자형 주문 번호는 자동 변환하지 않으므로 입력 파일에서 텍스트로 준비하세요.
python --version
python -m pip install -r requirements.txt
python example.py설치는 인터넷 연결이 필요합니다. 코드의 BASE는 현재 터미널 위치가 아니라 example.py의 위치를 기준으로 입력과 결과 경로를 정합니다.
load_workbook의 읽기 전용 모드로 원본을 열고, Counter로 각 행의 등장 횟수를 셉니다. 원본 엑셀의 행 번호는 enumerate의 start=2로 기억합니다. 결과는 새 Workbook에 저장하므로 원본을 다시 저장하지 않습니다.
"""Find repeated business rows. Keep the source workbook unchanged."""
from collections import Counter
from pathlib import Path
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill
BASE = Path(__file__).resolve().parent
HEADERS = ("order_id", "customer", "item", "quantity")
def main():
destination = BASE / "outputs"
if destination.exists():
raise ValueError("outputs already exists; rename it before running again.")
source = load_workbook(BASE / "orders.xlsx", read_only=True, data_only=False)
try:
if "Orders" not in source.sheetnames:
raise ValueError("Orders sheet is missing.")
worksheet = source["Orders"]
rows = list(worksheet.iter_rows(values_only=True))
if not rows or tuple(rows[0]) != HEADERS:
raise ValueError(f"Expected headers: {HEADERS}")
records = []
for row_number, row in enumerate(rows[1:], start=2):
if all(value is None for value in row):
continue
if any(value is None for value in row):
raise ValueError(f"Row {row_number}: missing value.")
if any(isinstance(value, str) and value.startswith("=") for value in row):
raise ValueError(f"Row {row_number}: formulas are not supported.")
if not all(isinstance(value, str) and value.strip() for value in row[:3]):
raise ValueError(f"Row {row_number}: first three fields must be text.")
if type(row[3]) is not int or row[3] <= 0:
raise ValueError(f"Row {row_number}: quantity must be a positive integer.")
records.append((row_number, tuple(row)))
if not records:
raise ValueError("No data rows were found.")
finally:
source.close()
counts = Counter(row for _, row in records)
repeated = [(number, row) for number, row in records if counts[row] > 1]
unique = [(number, row) for number, row in records if counts[row] == 1]
book = Workbook()
book.remove(book.active)
for name, selected in (("Duplicates", repeated), ("Unique", unique)):
sheet = book.create_sheet(name)
sheet.append(("source_row", *HEADERS, "occurrences"))
for number, row in selected:
sheet.append((number, *row, counts[row]))
sheet.freeze_panes = "A2"
sheet.auto_filter.ref = sheet.dimensions
sheet.sheet_view.showGridLines = False
for cell in sheet[1]:
cell.font = Font(name="Arial", bold=True, color="FFFFFF")
cell.fill = PatternFill("solid", fgColor="243B53")
for column, width in zip("ABCDEF", (14, 16, 18, 20, 14, 16)):
sheet.column_dimensions[column].width = width
destination.mkdir(exist_ok=False)
book.save(destination / "duplicate_review.xlsx")
book.close()
groups = sum(count > 1 for count in counts.values())
print(f"Input rows: {len(records)}; duplicate groups: {groups}")
print(f"Duplicate rows: {len(repeated)}; unique rows: {len(unique)}")
print("Created outputs/duplicate_review.xlsx (source unchanged).")
if __name__ == "__main__":
try:
main()
except (OSError, ValueError) as error:
raise SystemExit(f"Stopped: {error}") from error
outputs/duplicate_review.xlsx에 Duplicates와 Unique 두 시트가 생깁니다. source_row는 원본의 엑셀 행 번호이며, occurrences는 같은 내용이 전체 입력에서 나타난 횟수입니다.
| 항목 | 동봉 데이터 결과 |
|---|---|
| 입력 데이터 행 | 8 |
| 중복 묶음 | 2 |
| Duplicates 행 | 4 (원본 2, 3, 5, 8행) |
| Unique 행 | 4 (원본 4, 6, 7, 9행) |
Duplicates의 4행은 제거할 행 4개라는 뜻이 아닙니다. 각 묶음에서 하나씩만 유지한다면 추가로 입력된 행은 2개지만, 무엇을 유지할지는 원본을 확인한 뒤 정해야 합니다.
| 메시지 또는 증상 | 확인할 것 |
|---|---|
| Orders sheet is missing | 원본 시트 이름을 Orders로 맞춥니다. |
| Expected headers | 열 이름·순서·추가 열을 확인합니다. |
| missing value / formulas are not supported | 표시된 원본 행을 확인하고 값으로 준비합니다. |
| quantity must be a positive integer | 수량은 1 이상 정수로 입력합니다. |
| outputs already exists | 이전 결과 폴더를 다른 이름으로 보관합니다. |
| ModuleNotFoundError | 실행에 쓰는 python으로 requirements.txt를 설치합니다. |
Windows 11 (10.0.26200), CPython 3.12.14 (64비트). openpyxl 3.1.5, Matplotlib 3.10.8. 예제별 사용 라이브러리는 requirements.txt에 고정했습니다.
코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.
예제 ZIP 다운로드직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.
설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.