폴더: 전처리
교통사고건수.csv => 원본
교통사고건수_clear.csv => 전처리된 데이터
교통사고건수_dirty.csv => 전처리된 데이터에 결측치, 이상치 추가
data.py => (결측치, 이상치 생성 + 전처리 + 저장) 전체 tkinter 코드
ㄴ파일 불러오기에 clear.csv로
결측치 5 => 0 (1%미만)
이상치 76 => 0 (10% 수준)
이상치는 의도적으로 남겨두는 것과 제거하는 것을 구분
df_raw : 파일에서 불러온 원본 데이터 (절대 수정하지 않음)
df_clean : 전처리 대상 데이터 (모든 전처리는 여기서만 수행)
df_view : 현재 화면(Treeview)에 보여주는 데이터
Dirty 데이터 생성
결측치 주입
각 컬럼의 10% 행을 무작위 선택
NaN으로 설정
이상치 주입
각 컬럼별 IQR 계산
Q3 + 6 × IQR 값을 이상치로 강제 주입
=> IQR 기준으로 반드시 탐지되는 이상치
결측치 현황 확인
처리 대상 컬럼만 결측치 개수 출력
사고유형별, 차종별 결측치는 의도적으로 제외
ㄴ 처리하기로 한 데이터가 얼마나 남았는지(전체 결측치 X)
이상치 현황 확인
탐지 기준: IQR (Q1 - 1.5 * IQR), (Q3 + 1.5 * IQR)
대도시/지역 차이로 생긴 극단값 전체를 문제 삼지 않음
분석에 직접 쓰이는 핵심 지표만 점검
결측치 처리
각 컬럼의 중앙값으로 대체
사고 유형별 / 자동차 종류별 결측치는 처리X
ㄴ 사건이 없었다는 정보 보존
이상치 처리
Winsorizing(하한: Q1 - 1.5 × IQR, 상한: Q3 + 1.5 × IQR)
상,하한을 벗어난 값은 경계값으로 치환
ㄴ 이상치 삭제X, 극단값의 영향만 완화, 데이터 수 유지
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import pandas as pd
import numpy as np
# 전역 데이터
df_raw = None # 원본
df_clean = None # 전처리 대상
df_view = None # 현재 화면 표시용
column_widths = None
# 테이블 미리보기
def update_preview(df):
global column_widths, df_view
df_view = df
tree.delete(*tree.get_children())
tree["columns"] = list(df.columns)
tree["show"] = "headings"
if column_widths is None:
column_widths = {}
for col in df.columns:
sample = str(df[col].iloc[0]) if len(df) > 0 else ""
column_widths[col] = min(max(len(col), len(sample)) * 10, 300)
for col in df.columns:
anchor = "e" if pd.api.types.is_numeric_dtype(df[col]) else "center"
tree.heading(col, text=col)
tree.column(col, width=column_widths[col], anchor=anchor, stretch=False)
for _, row in df.head(20).iterrows():
tree.insert("", "end", values=list(row))
# 파일 불러오기
def load_file():
global df_raw, df_clean, column_widths
column_widths = None
path = filedialog.askopenfilename(
filetypes=[("CSV", "*.csv"), ("Excel", "*.xlsx *.xls")]
)
if not path:
return
try:
if path.endswith(".csv"):
df_raw = pd.read_csv(path)
else:
df_raw = pd.read_excel(path)
df_clean = df_raw.copy(deep=True)
update_preview(df_raw)
messagebox.showinfo(
"로드 완료",
f"행: {len(df_raw)} / 열: {len(df_raw.columns)}"
)
except Exception as e:
messagebox.showerror("에러", str(e))
# dirty 데이터 만들기 (결측 + 이상치)
def make_dirty():
global df_clean
if df_clean is None:
return
rng = np.random.default_rng(42)
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)",
"주민등록인구수(등록외국인포함) (명)"
]
for col in TARGET_NUM_COLS:
if col not in df_clean.columns:
continue
# 결측치
idx = rng.choice(
df_clean.index,
int(len(df_clean) * 0.1),
replace=False
)
df_clean.loc[idx, col] = np.nan
# 이상치 (탐지 보장)
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
extreme_value = Q3 + 6 * IQR
idx = rng.choice(
df_clean.index,
int(len(df_clean) * 0.1),
replace=False
)
df_clean.loc[idx, col] = extreme_value
update_preview(df_clean)
# 결측치 확인
def check_missing():
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)",
"주민등록인구수(등록외국인포함) (명)"
]
result = df_clean[TARGET_NUM_COLS].isna().sum()
messagebox.showinfo("결측치 현황 (처리 대상)", result.to_string())
# 이상치 확인 (IQR)
def check_outliers():
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)"
]
result = {}
for col in TARGET_NUM_COLS:
if col not in df_clean.columns:
continue
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
result[col] = ((df_clean[col] < lower) | (df_clean[col] > upper)).sum()
messagebox.showinfo("이상치 현황 (처리 대상)", pd.Series(result).to_string())
# 결측치 처리
def handle_missing():
global df_clean
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)",
"주민등록인구수(등록외국인포함) (명)"
]
valid_cols = [c for c in TARGET_NUM_COLS if c in df_clean.columns]
df_clean[valid_cols] = df_clean[valid_cols].fillna(
df_clean[valid_cols].median()
)
update_preview(df_clean)
# 이상치 처리 (winsorizing)
def handle_outliers():
global df_clean
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)"
]
for col in TARGET_NUM_COLS:
if col not in df_clean.columns:
continue
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
df_clean[col] = np.where(
df_clean[col] < lower, lower,
np.where(df_clean[col] > upper, upper, df_clean[col])
)
update_preview(df_clean)
# 원본 / 전처리본 전환
def show_raw():
if df_raw is not None:
update_preview(df_raw)
def show_clean():
if df_clean is not None:
update_preview(df_clean)
# 저장
def save_file():
if df_clean is None:
return
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv")]
)
if path:
df_clean.to_csv(path, index=False, encoding="utf-8-sig")
messagebox.showinfo("저장 완료", "파일 저장 완료")
# Tk UI
root = tk.Tk()
root.title("교통사고 데이터 전처리 모듈")
root.geometry("1300x650")
style = ttk.Style(root)
style.configure("Treeview", font=("Malgun Gothic", 10), rowheight=28)
style.configure("Treeview.Heading", font=("Malgun Gothic", 10, "bold"))
button_frame = tk.Frame(root)
button_frame.pack(fill="x", padx=10, pady=5)
buttons = [
("파일 불러오기", load_file),
("Dirty 데이터 생성", make_dirty),
("결측치 확인", check_missing),
("이상치 확인", check_outliers),
("결측치 처리", handle_missing),
("이상치 처리", handle_outliers),
("원본 보기", show_raw),
("전처리본 보기", show_clean),
("저장", save_file)
]
for i, (text, cmd) in enumerate(buttons):
tk.Button(button_frame, text=text, command=cmd)\
.grid(row=0, column=i, padx=3, pady=2)
button_frame.grid_columnconfigure(i, weight=1)
table_frame = tk.Frame(root)
table_frame.pack(fill="both", expand=True)
tree = ttk.Treeview(table_frame)
tree.pack(side="left", fill="both", expand=True)
scroll_y = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
scroll_y.pack(side="right", fill="y")
scroll_x = ttk.Scrollbar(root, orient="horizontal", command=tree.xview)
scroll_x.pack(fill="x")
tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
root.mainloop()

모듈화(동작, 기능은 동일)
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import pandas as pd
import numpy as np
# 전역 데이터 변수
# df_raw : 사용자가 불러온 "원본" 데이터(절대 수정하지 않음)
# df_clean : 전처리/dirty 생성 등 모든 변경 작업을 적용하는 "복사본" 데이터
# df_view : 현재 화면(Treeview)에 표시 중인 데이터(원본/전처리본 전환용)
# column_widths : 컬럼별 고정 폭 캐시(한 번 계산되면 이후 미리보기에서도 동일 폭 유지)
df_raw = None
df_clean = None
df_view = None
column_widths = None
# Tkinter 위젯 참조를 전역으로 관리(함수들에서 접근 필요)
# root : Tk 루트 윈도우
# tree : ttk.Treeview(데이터 테이블)
root = None
tree = None
# 테이블 미리보기
def update_preview(df):
global column_widths, df_view, tree
# 현재 화면에 표시 중인 DataFrame을 갱신(원본/전처리본 전환 기능에서 사용)
df_view = df
# 기존 Treeview 내용 초기화
tree.delete(*tree.get_children())
# 컬럼 설정 및 헤더 표시 모드 지정
tree["columns"] = list(df.columns)
tree["show"] = "headings"
# 컬럼 폭은 최초 렌더링 시 1회 계산(이후 고정)
if column_widths is None:
column_widths = {}
for col in df.columns:
sample = str(df[col].iloc[0]) if len(df) > 0 else ""
column_widths[col] = min(max(len(col), len(sample)) * 10, 300)
# 컬럼 헤더/폭/정렬 설정
for col in df.columns:
# dtype이 숫자형이면 오른쪽 정렬
anchor = "e" if pd.api.types.is_numeric_dtype(df[col]) else "center"
tree.heading(col, text=col)
tree.column(col, width=column_widths[col], anchor=anchor, stretch=False)
# 상단 20행만 삽입
for _, row in df.head(20).iterrows():
tree.insert("", "end", values=list(row))
# 파일 불러오기
def load_file():
# 파일 선택 대화상자에서 파일을 선택해 로드
# df_raw에 원본 저장
# df_clean에 원본의 deep copy 저장(전처리 대상)
# 화면에는 원본(df_raw) 먼저 표시
global df_raw, df_clean, column_widths
# 새 파일 로드 시 컬럼 폭을 다시 계산하도록 캐시 초기화
column_widths = None
# 파일 선택 창(확장자 필터 적용)
path = filedialog.askopenfilename(
filetypes=[("CSV", "*.csv"), ("Excel", "*.xlsx *.xls")]
)
if not path:
return
try:
# 로드
if path.endswith(".csv"):
df_raw = pd.read_csv(path)
else:
df_raw = pd.read_excel(path)
# 원본 보존: 전처리/dirty 작업은 df_clean(복사본)에만 적용
df_clean = df_raw.copy(deep=True)
# 로드 직후 원본을 먼저 보여줌
update_preview(df_raw)
# 로드 결과 안내(행/열 수)
messagebox.showinfo(
"로드 완료",
f"행: {len(df_raw)} / 열: {len(df_raw.columns)}"
)
except Exception as e:
# 파일 인코딩/형식 오류 등 예외 발생 시 사용자에게 메시지 표시
messagebox.showerror("에러", str(e))
# Dirty 데이터 생성(결측치 + 이상치 주입)
def make_dirty():
# df_clean(복사본)에 결측치/이상치를 인위적으로 주입해 dirty 데이터 생성
# 대상 컬럼(TARGET_NUM_COLS)만 처리
# 각 컬럼별로 전체 행의 약 10%를 무작위 선택
# 1 결측치 주입: NaN
# 2 이상치 주입: Q3 + 6*IQR (IQR 기준으로 '탐지 보장'되는 큰 값)
global df_clean
if df_clean is None:
return
# 재현 가능한 결과를 위해 고정 시드 사용
rng = np.random.default_rng(42)
# dirty 작업을 적용할 수치 컬럼(핵심 지표만)
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)",
"주민등록인구수(등록외국인포함) (명)"
]
for col in TARGET_NUM_COLS:
# 데이터에 해당 컬럼이 없으면 스킵
if col not in df_clean.columns:
continue
# 1결측치 주입: 전체 행의 10%를 랜덤 선택해 NaN으로 설정
idx = rng.choice(df_clean.index, int(len(df_clean) * 0.1), replace=False)
df_clean.loc[idx, col] = np.nan
# 2이상치 주입 값 계산(IQR 기반)
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
# 같은 idx에 이상치를 주입(의도적으로 결측치/이상치가 겹칠 수 있음)
# Q3 + 6*IQR는 일반 IQR 탐지(1.5*IQR) 기준을 훨씬 넘어가기 때문에 탐지 가능성이 높음
df_clean.loc[idx, col] = Q3 + 6 * IQR
# dirty 생성 후 전처리본(df_clean)을 화면에 표시
update_preview(df_clean)
# 결측치 확인(처리 대상 컬럼만)
def check_missing():
# df_clean의 결측치 현황을 팝업으로 출력.
# 처리 대상 컬럼(TARGET_NUM_COLS)에 대해서만 NaN 개수 집계
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)",
"주민등록인구수(등록외국인포함) (명)"
]
# 각 컬럼별 NaN 개수
result = df_clean[TARGET_NUM_COLS].isna().sum()
messagebox.showinfo("결측치 현황 (처리 대상)", result.to_string())
# 이상치 확인(IQR 기준, 처리 대상 컬럼만)
def check_outliers():
# df_clean의 이상치 개수를 팝업으로 출력.
# IQR 방식(1.5*IQR)으로 lower/upper 경계를 계산
# 처리 대상 컬럼(TARGET_NUM_COLS)만 검사
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)"
]
result = {}
for col in TARGET_NUM_COLS:
if col not in df_clean.columns:
continue
# IQR 경계 계산
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
# 경계 밖 값(이상치) 개수 집계
result[col] = ((df_clean[col] < lower) | (df_clean[col] > upper)).sum()
messagebox.showinfo("이상치 현황 (처리 대상)", pd.Series(result).to_string())
# 결측치 처리(중앙값 대체)
def handle_missing():
# df_clean의 결측치를 중앙값(median)으로 대체
# 처리 대상 컬럼(TARGET_NUM_COLS)만 적용
# 데이터에 실제 존재하는 컬럼만(valid_cols) 추려서 안전하게 처리
global df_clean
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)",
"주민등록인구수(등록외국인포함) (명)"
]
# 파일마다 컬럼명이 다를 수 있으므로 존재하는 컬럼만 처리
valid_cols = [c for c in TARGET_NUM_COLS if c in df_clean.columns]
# 중앙값으로 결측치 대체(평균보다 이상치 영향이 적어 안정적)
df_clean[valid_cols] = df_clean[valid_cols].fillna(
df_clean[valid_cols].median()
)
update_preview(df_clean)
# 이상치 처리(Winsorizing: 경계값으로 절단)
def handle_outliers():
# df_clean의 이상치를 winsorizing 방식으로 처리
# IQR 기반 lower/upper 경계를 계산
# lower 미만은 lower로, upper 초과는 upper로 치환(삭제하지 않고 영향만 완화)
# 처리 대상 컬럼(TARGET_NUM_COLS)만 적용
global df_clean
if df_clean is None:
return
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)"
]
for col in TARGET_NUM_COLS:
if col not in df_clean.columns:
continue
# IQR 경계 계산
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
# 경계 밖 값은 경계값으로 치환
df_clean[col] = np.where(
df_clean[col] < lower, lower,
np.where(df_clean[col] > upper, upper, df_clean[col])
)
update_preview(df_clean)
# 원본/전처리본 보기(화면 전환)
def show_raw():
# 원본(df_raw)을 Treeview에 표시(데이터는 수정하지 않음)
if df_raw is not None:
update_preview(df_raw)
def show_clean():
# 전처리본(df_clean)을 Treeview에 표시
if df_clean is not None:
update_preview(df_clean)
# 저장(df_clean만 저장)
def save_file():
# 전처리본(df_clean)을 CSV로 저장
if df_clean is None:
return
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv")]
)
if path:
df_clean.to_csv(path, index=False, encoding="utf-8-sig")
messagebox.showinfo("저장 완료", "파일 저장 완료")
# 실행 함수(메인에서 import 후 호출 가능)
def run_preprocessing_app():
global root, tree
# 루트 윈도우 생성
root = tk.Tk()
root.title("교통사고 데이터 전처리 모듈")
root.geometry("1300x650")
# Treeview 스타일 지정(폰트/행높이/헤더 폰트)
style = ttk.Style(root)
style.configure("Treeview", font=("Malgun Gothic", 10), rowheight=28)
style.configure("Treeview.Heading", font=("Malgun Gothic", 10, "bold"))
# 상단 버튼 영역(Frame)
button_frame = tk.Frame(root)
button_frame.pack(fill="x", padx=10, pady=5)
# 버튼 목록(텍스트, 실행 함수) 각 버튼은 해당 함수로 이벤트 연결
buttons = [
("파일 불러오기", load_file),
("Dirty 데이터 생성", make_dirty),
("결측치 확인", check_missing),
("이상치 확인", check_outliers),
("결측치 처리", handle_missing),
("이상치 처리", handle_outliers),
("원본 보기", show_raw),
("전처리본 보기", show_clean),
("저장", save_file)
]
# 버튼 배치(grid) 한 줄로 쭉 배치되도록 column weight 부여
for i, (text, cmd) in enumerate(buttons):
tk.Button(button_frame, text=text, command=cmd)\
.grid(row=0, column=i, padx=3, pady=2)
button_frame.grid_columnconfigure(i, weight=1)
# 중앙 테이블 영역(Frame)
table_frame = tk.Frame(root)
table_frame.pack(fill="both", expand=True)
# Treeview 생성(데이터 테이블)
tree = ttk.Treeview(table_frame)
tree.pack(side="left", fill="both", expand=True)
# 세로 스크롤바(Treeview yview에 연결)
scroll_y = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
scroll_y.pack(side="right", fill="y")
# 가로 스크롤바(Treeview xview에 연결)
scroll_x = ttk.Scrollbar(root, orient="horizontal", command=tree.xview)
scroll_x.pack(fill="x")
# Treeview가 스크롤 상태를 스크롤바에 반영하도록 연결
tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
# 이벤트 루프 시작(창이 닫힐 때까지 유지)
root.mainloop()
# main guard
# main.py에서 import 할 경우에는 자동 실행X
if __name__ == "__main__":
run_preprocessing_app()
data.py -> model.py로 기능 옮김
+ 검색 기능 메인에 추가
main.py
import tkinter as tk
from model import Model
from view import View
from controller import Controller
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title('CSV/Excel 데이터 뷰어')
self.geometry('1000x720')
model = Model()
view = View(self)
controller = Controller(model, view)
view.set_controller(controller)
if __name__ == '__main__':
app = App()
app.mainloop()
model.py
import pandas as pd
import numpy as np
class Model:
def __init__(self):
self.df = None
self.copy_df = None # 복사본
def load_csv(self, path):
try:
self.df = pd.read_csv(path, encoding='utf-8-sig')
except UnicodeDecodeError:
self.df = pd.read_csv(path, encoding='cp949')
self.copy_df = self.df.copy()
self.df.columns = [str(col).strip() for col in self.df.columns]
self.copy_df.columns = [str(col).strip() for col in self.df.columns]
def save_csv(self, path):
if self.df is not None:
self.df.to_csv(path, index=False, encoding='utf-8-sig')
def return_copy(self):
return self.copy_df
def return_df(self):
return self.df
def handle_missing(self):
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)",
"주민등록인구수(등록외국인포함) (명)"
]
valid_cols = [c for c in TARGET_NUM_COLS if c in self.copy_df.columns]
self.copy_df[valid_cols] = self.copy_df[valid_cols].fillna(
self.copy_df[valid_cols].median()
)
def handle_outliers(self):
TARGET_NUM_COLS = [
"발생건수 (건)",
"사망자수 (명)",
"부상자수 (명)"
]
for col in TARGET_NUM_COLS:
if col not in self.copy_df.columns:
continue
Q1 = self.copy_df[col].quantile(0.25)
Q3 = self.copy_df[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
self.copy_df[col] = np.where(
self.copy_df[col] < lower, lower,
np.where(self.copy_df[col] > upper, upper, self.copy_df[col])
)
# 검색 메서드
def search_by_region(self, keyword):
if self.copy_df is None:
return None
if not keyword:
return self.copy_df
target_cols = [
"행정구역별(1)",
"행정구역별(2)"
]
valid_cols = [c for c in target_cols if c in self.copy_df.columns]
if not valid_cols:
return self.copy_df
mask = False
for col in valid_cols:
mask |= self.copy_df[col].astype(str).str.contains(
keyword, case=False, na=False
)
return self.copy_df[mask]
view.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
class MainMenu(tk.Menu):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
# 파일 메뉴
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="파일", menu=file_menu)
file_menu.add_command(label="열기", command=self.on_open)
file_menu.add_command(label="저장", command=self.on_save)
# 기능 메뉴
func_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="기능", menu=func_menu)
func_menu.add_command(label="결측치 처리", command=self.controller.handle_missing_)
func_menu.add_command(label="이상치 처리", command=self.controller.handle_outliers_)
# 🔹 검색 메뉴 (추가)
search_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="검색", menu=search_menu)
search_menu.add_command(label="지역 검색", command=self.on_search)
def on_open(self):
path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
if path:
self.controller.load_file(path)
def on_save(self):
path = filedialog.asksaveasfilename(defaultextension=".csv")
if path:
self.controller.save_file(path)
def on_search(self):
keyword = simple_input(self.master, "지역 검색", "지역명을 입력하세요")
if keyword is not None:
self.controller.search_region(keyword)
def simple_input(parent, title, msg):
win = tk.Toplevel(parent)
win.title(title)
win.grab_set()
tk.Label(win, text=msg).pack(padx=10, pady=5)
entry = tk.Entry(win)
entry.pack(padx=10, pady=5)
result = {"value": None}
def submit():
result["value"] = entry.get().strip()
win.destroy()
tk.Button(win, text="확인", command=submit).pack(pady=5)
parent.wait_window(win)
return result["value"]
class DataTable(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.tree = ttk.Treeview(self, show="headings")
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
hsb = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self.tree.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
def update_data(self, df, limit=100):
self.tree.delete(*self.tree.get_children())
if df is None:
return
cols = list(df.columns)
self.tree["columns"] = cols
for col in cols:
self.tree.heading(col, text=col)
self.tree.column(col, width=150, stretch=False)
for _, row in df.head(limit).iterrows():
self.tree.insert("", "end", values=list(row))
class View(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.pack(expand=True, fill="both")
self.data_table = DataTable(self)
self.data_table.pack(expand=True, fill="both")
def set_controller(self, controller):
menubar = MainMenu(self.master, controller)
self.master.config(menu=menubar)
def display_data(self, df):
self.data_table.update_data(df)
def show_error(self, msg):
messagebox.showerror("에러", msg)
controller.py
class Controller:
def __init__(self, model, view):
self.model = model
self.view = view
def load_file(self, path):
self.model.load_csv(path)
self.view.display_data(self.model.return_copy())
def save_file(self, path):
self.model.save_csv(path)
def handle_missing_(self):
self.model.handle_missing()
self.view.display_data(self.model.return_copy())
def handle_outliers_(self):
self.model.handle_outliers()
self.view.display_data(self.model.return_copy())
def search_region(self, keyword):
df = self.model.search_by_region(keyword)
self.view.display_data(df)
view 검색 클래스화 + 검색창 크기 확대
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
class SearchDialog(tk.Toplevel):
def __init__(self, parent, title="지역 검색", msg="지역명을 입력하세요"):
super().__init__(parent)
self.title(title)
self.geometry("320x140")
self.resizable(False, False)
self.grab_set()
self.result = None
tk.Label(self, text=msg).pack(padx=10, pady=(15, 5))
self.entry = tk.Entry(self, width=30)
self.entry.pack(padx=10, pady=5)
self.entry.focus_set()
tk.Button(self, text="확인", width=10, command=self.submit).pack(pady=10)
# Enter 키로도 검색 가능
self.bind("<Return>", lambda e: self.submit())
# 창이 닫힐 때까지 대기
self.wait_window(self)
def submit(self):
self.result = self.entry.get().strip()
self.destroy()
class MainMenu(tk.Menu):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
# 파일 메뉴
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="파일", menu=file_menu)
file_menu.add_command(label="열기", command=self.on_open)
file_menu.add_command(label="저장", command=self.on_save)
# 기능 메뉴
func_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="기능", menu=func_menu)
func_menu.add_command(label="결측치 처리", command=self.controller.handle_missing_)
func_menu.add_command(label="이상치 처리", command=self.controller.handle_outliers_)
# 검색 메뉴
search_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="검색", menu=search_menu)
search_menu.add_command(label="지역 검색", command=self.on_search)
def on_open(self):
path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
if path:
self.controller.load_file(path)
def on_save(self):
path = filedialog.asksaveasfilename(defaultextension=".csv")
if path:
self.controller.save_file(path)
def on_search(self):
dialog = SearchDialog(self.master)
if dialog.result is not None:
self.controller.search_region(dialog.result)
class DataTable(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.tree = ttk.Treeview(self, show="headings")
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
hsb = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self.tree.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
def update_data(self, df, limit=100):
self.tree.delete(*self.tree.get_children())
if df is None:
return
cols = list(df.columns)
self.tree["columns"] = cols
for col in cols:
self.tree.heading(col, text=col)
self.tree.column(col, width=150, stretch=False)
for _, row in df.head(limit).iterrows():
self.tree.insert("", "end", values=list(row))
class View(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.pack(expand=True, fill="both")
self.data_table = DataTable(self)
self.data_table.pack(expand=True, fill="both")
def set_controller(self, controller):
menubar = MainMenu(self.master, controller)
self.master.config(menu=menubar)
def display_data(self, df):
self.data_table.update_data(df)
def show_error(self, msg):
messagebox.showerror("에러", msg)
검색 창 상단메뉴로 변경
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
# Popup 전담 클래스
class Popup:
@staticmethod
def info(title, msg):
messagebox.showinfo(title, msg)
@staticmethod
def error(msg):
messagebox.showerror("에러", msg)
# DataTable 전담 클래스
class DataTable(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.tree = ttk.Treeview(self, show="headings", selectmode="browse")
self.vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
self.hsb = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview)
self.tree.configure(
yscrollcommand=self.vsb.set,
xscrollcommand=self.hsb.set
)
self.tree.grid(row=0, column=0, sticky="nsew")
self.vsb.grid(row=0, column=1, sticky="ns")
self.hsb.grid(row=1, column=0, sticky="ew")
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
def update_data(self, df, limit=200):
self.tree.delete(*self.tree.get_children())
self.tree["columns"] = list(df.columns)
for col in df.columns:
self.tree.heading(col, text=col)
self.tree.column(col, width=140, anchor="w")
for _, row in df.head(limit).iterrows():
self.tree.insert("", "end", values=list(row))
# 메뉴바 전담 클래스
class MainMenu(tk.Menu):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
# 파일
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="파일", menu=file_menu)
file_menu.add_command(label="열기", command=self.open_file)
file_menu.add_command(label="저장", command=self.save_file)
# 기능
func_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="기능", menu=func_menu)
func_menu.add_command(label="결측치 처리", command=controller.handle_missing_)
func_menu.add_command(label="이상치 처리", command=controller.handle_outliers_)
# 보기
view_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="보기", menu=view_menu)
view_menu.add_command(label="원본", command=controller.show_origin)
view_menu.add_command(label="복사본", command=controller.show_copy)
# 검색 (토글 버튼)
self.add_command(
label="검색",
command=controller.toggle_region_bar
)
def open_file(self):
path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
if path:
self.controller.load_file(path)
def save_file(self):
path = filedialog.asksaveasfilename(defaultextension=".csv")
if path:
self.controller.save_file(path)
# 메인 View (컨테이너)
class View(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.controller = None
# 상단 컨트롤 바 (검색)
self.control_bar = ttk.Frame(parent)
self.control_bar_visible = False
ttk.Label(self.control_bar, text="행정 구역").pack(side="left", padx=4)
self.region_var = tk.StringVar()
self.region_combo = ttk.Combobox(
self.control_bar,
textvariable=self.region_var,
state="readonly",
width=18
)
self.region_combo.pack(side="left", padx=4)
ttk.Label(self.control_bar, text="정렬 기준").pack(side="left", padx=4)
self.sort_var = tk.StringVar()
ttk.Combobox(
self.control_bar,
textvariable=self.sort_var,
state="readonly",
width=26,
values=[
"총사고발생건수",
"사망자수",
"부상자수",
"주민등록인구수(등록외국인포함)"
]
).pack(side="left", padx=4)
ttk.Label(self.control_bar, text="정렬 방향").pack(side="left", padx=4)
self.order_var = tk.StringVar(value="오름차순")
ttk.Combobox(
self.control_bar,
textvariable=self.order_var,
state="readonly",
width=10,
values=["오름차순", "내림차순"]
).pack(side="left", padx=4)
ttk.Button(
self.control_bar,
text="적용",
command=self.apply
).pack(side="left", padx=8)
# View 본체
self.pack(expand=True, fill="both", padx=5, pady=5)
self.data_table = DataTable(self)
self.data_table.pack(expand=True, fill="both")
# ---------- Controller 연결 ----------
def set_controller(self, controller):
self.controller = controller
self.parent.config(menu=MainMenu(self.parent, controller))
# ---------- 검색 바 제어 ----------
def toggle_control_bar(self):
if self.control_bar_visible:
self.control_bar.pack_forget()
else:
self.control_bar.pack(
fill="x",
padx=8,
pady=4,
before=self
)
self.control_bar_visible = not self.control_bar_visible
def hide_control_bar(self):
if self.control_bar_visible:
self.control_bar.pack_forget()
self.control_bar_visible = False
def update_region_list(self, regions):
self.region_combo["values"] = regions
self.region_var.set("")
# ---------- 동작 ----------
def apply(self):
region = self.region_var.get().strip()
sort_col = self.sort_var.get().strip()
ascending = self.order_var.get() == "오름차순"
self.controller.apply_filter_sort(region, sort_col, ascending)
def display_data(self, df):
self.data_table.update_data(df)
# ---------- 메시지 ----------
def show_popup(self, title, msg):
Popup.info(title, msg)
def show_error(self, msg):
Popup.error(msg)

로그 기록 추가
view.py
view_menu.add_command(label="전처리 로그", command=controller.show_logs)
controller.py
from datetime import datetime
class Controller:
# 컨트롤러는 뷰와 모델을 어떻게 적절하게 활용할건지 담당하는 클래스이다. 결국엔 모델과 뷰를 연결해주는 역할이다.
def __init__(self, model, view):
self.model = model
self.view = view
self.logs = []
def load_file(self, path):
try:
self.model.load_csv(path)
self.view.update_region_list(self.model.get_regions())
self.view.display_data(self.model.return_copy())
self.view.show_popup("파일 읽기", "파일 읽기 성공")
except Exception as e:
self.view.show_popup("파일 읽기 에러", f"파일을 불러오는 중 오류가 발생했습니다:\n{e}")
def save_file(self, path):
try:
self.model.save_csv(path)
self.view.show_popup("저장 완료", "파일이 성공적으로 저장되었습니다.")
except Exception as e:
self.view.show_popup("저장 에러", f"파일 저장 중 오류가 발생했습니다:\n{e}")
def handle_missing_(self):
try:
self.model.handle_missing()
self.add_log(action="결측치 처리", target="수치 컬럼", detail="NaN → 중앙값 대체")
self.view.display_data(self.model.return_copy())
self.view.show_popup("전처리", "결측치 처리가 완료되었습니다.")
except Exception as e:
self.view.show_popup("전처리 에러", f"결측치 처리 중 오류 발생:\n{e}")
def handle_outliers_(self):
try:
self.model.handle_outliers()
self.add_log(action="이상치 처리", target="총사고발생건수/사망자수/부상자수", detail="IQR 기준 clipping")
self.view.display_data(self.model.return_copy())
self.view.show_popup("전처리", "이상치 처리가 완료되었습니다.")
except Exception as e:
self.view.show_popup("전처리 에러", f"이상치 처리 중 오류 발생:\n{e}")
def show_origin(self):
try:
self.view.hide_control_bar()
self.view.display_data(self.model.return_df())
except Exception as e:
self.view.show_popup("표시 에러", f"원본 데이터를 표시할 수 없습니다:\n{e}")
def show_copy(self):
try:
self.view.hide_control_bar()
self.view.display_data(self.model.return_copy())
except Exception as e:
self.view.show_popup("표시 에러", f"복사본 데이터를 표시할 수 없습니다:\n{e}")
def toggle_region_bar(self):
try:
self.view.toggle_control_bar()
except Exception as e:
# UI 토글은 치명적이지 않으므로 콘솔에 출력하거나 가볍게 처리
print(f"UI Toggle Error: {e}")
def apply_filter_sort(self, region, sort_col, ascending):
try:
df = self.model.filter_and_sort(region, sort_col, ascending)
self.view.display_data(df)
except Exception as e:
self.view.show_popup("필터/정렬 에러", f"데이터를 거르는 중 오류가 발생했습니다:\n{e}")
def update_cell(self, row_idx, col_name, value):
try:
before = self.model.copy_df.at[row_idx, col_name]
self.model.update_cell(row_idx, col_name, value)
after = self.model.copy_df.at[row_idx, col_name]
self.add_log(action="셀 수정", target=f"row={row_idx}, col={col_name}", detail=f"{before} → {after}")
self.view.display_data(self.model.return_copy())
except Exception as e:
self.view.show_popup("수정 에러", f"데이터 수정에 실패했습니다:\n{e}")
def show_logs(self):
if not self.logs:
self.view.show_popup("전처리 로그", "기록된 전처리 내역이 없습니다.")
return
text = "\n".join(
f"[{l['time']}] {l['action']} | {l['target']} | {l['detail']}"
for l in self.logs
)
self.view.show_popup("전처리 로그", text)
# 로그 기록용 공통 함수
def add_log(self, action, target="", detail=""):
time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.logs.append({
"time": time,
"action": action,
"target": target,
"detail": detail
})
파생변수 추가
위험지역 여부
사망자수가 5명 이상인 지역을 위험지역으로 분류
사망자수 ≥ 5이면 1, 미만이면 0으로 표시
인명 피해가 반복, 집중되는 지역을 빠르게 구분하기 위한 지표
인구 대비 사고율
해당 지역의 총사고발생건수 ÷ 주민등록인구수로 계산
지역 규모 차이를 보정해 사고 위험도를 비교
사고 건수만으로는 보이지 않는 상대적 위험을 드러냄
시각화 추가
view.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
matplotlib.rcParams['font.family'] = 'Malgun Gothic'
matplotlib.rcParams['axes.unicode_minus'] = False
# Popup 전담 클래스
class Popup:
@staticmethod
def info(title, msg):
messagebox.showinfo(title, msg)
@staticmethod
def error(msg):
messagebox.showerror("에러", msg)
# 검색 전담 클래스
class ControlBar(ttk.Frame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
ttk.Label(self, text="행정 구역").pack(side="left", padx=4)
self.region_var = tk.StringVar()
self.region_combo = ttk.Combobox(
self, textvariable=self.region_var,
state="readonly", width=18
)
self.region_combo.pack(side="left", padx=4)
ttk.Label(self, text="정렬 기준").pack(side="left", padx=4)
self.sort_var = tk.StringVar()
self.sort_combo = ttk.Combobox(
self, textvariable=self.sort_var,
state="readonly", width=26,
values=["총사고발생건수", "사망자수", "부상자수", "주민등록인구수(등록외국인포함)"]
)
self.sort_combo.pack(side="left", padx=4)
ttk.Label(self, text="정렬 방향").pack(side="left", padx=4)
self.order_var = tk.StringVar(value="오름차순")
ttk.Combobox(
self, textvariable=self.order_var,
state="readonly", width=10,
values=["오름차순", "내림차순"]
).pack(side="left", padx=4)
ttk.Button(self, text="적용", command=self.on_apply).pack(side="left", padx=8)
def update_regions(self, regions):
self.region_combo["values"] = regions
self.region_var.set("")
def on_apply(self):
region = self.region_var.get().strip()
sort_col = self.sort_var.get().strip()
ascending = (self.order_var.get() == "오름차순")
self.controller.apply_filter_sort(region, sort_col, ascending)
# DataTable 전담 클래스
class DataTable(ttk.Frame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
self.tree = ttk.Treeview(self, show="headings", selectmode="none")
self.tree.pack(expand=True, fill="both")
self.tree.bind("<Double-1>", self._on_double_click)
self._edit_entry = None
def update_data(self, df, limit=200):
self.df = df
self.tree.delete(*self.tree.get_children())
self.tree["columns"] = list(df.columns)
for col in df.columns:
self.tree.heading(col, text=col)
self.tree.column(col, width=140, anchor="w")
for idx, row in df.head(limit).iterrows():
self.tree.insert("", "end", iid=str(idx), values=list(row))
def _on_double_click(self, event):
region = self.tree.identify("region", event.x, event.y)
if region != "cell":
return
row_id = self.tree.identify_row(event.y)
col_id = self.tree.identify_column(event.x)
if not row_id or not col_id:
return
col_index = int(col_id.replace("#", "")) - 1
col_name = self.tree["columns"][col_index]
x, y, w, h = self.tree.bbox(row_id, col_id)
value = self.tree.set(row_id, col_name)
self._edit_entry = ttk.Entry(self.tree)
self._edit_entry.place(x=x, y=y, width=w, height=h)
self._edit_entry.insert(0, value)
self._edit_entry.focus()
def cleanup():
if self._edit_entry:
self._edit_entry.destroy()
self._edit_entry = None
self.tree.selection_remove(self.tree.selection())
self.tree.focus("")
def save_edit(event=None):
new_value = self._edit_entry.get()
cleanup()
self.controller.update_cell(
row_idx=int(row_id),
col_name=col_name,
value=new_value
)
def cancel_edit(event=None):
cleanup()
self._edit_entry.bind("<Return>", save_edit)
self._edit_entry.bind("<Escape>", cancel_edit)
self._edit_entry.bind("<FocusOut>", cancel_edit)
# 요약 통계 전담 클래스
class AnalysisTablePopup(tk.Toplevel):
def __init__(self, parent, df):
super().__init__(parent)
self.title("교통사고 종합 지표 분석 리포트")
self.geometry("1000x600")
header = ttk.Label(
self,
text="지역별 사고 치명률 및 특성 분석 결과",
font=("맑은 고딕", 12, "bold")
)
header.pack(pady=10)
container = ttk.Frame(self)
container.pack(expand=True, fill="both", padx=10, pady=10)
self.table = DataTable(container, controller=None)
self.table.pack(expand=True, fill="both")
self.table.update_data(df)
ttk.Button(self, text="닫기", command=self.destroy).pack(pady=10)
# 메뉴바 전담 클래스
class MainMenu(tk.Menu):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
file_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="파일", menu=file_menu)
file_menu.add_command(label="열기", command=self.open_file)
file_menu.add_command(label="저장", command=self.save_file)
func_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="기능", menu=func_menu)
func_menu.add_command(label="결측치 처리", command=controller.handle_missing_)
func_menu.add_command(label="이상치 처리", command=controller.handle_outliers_)
func_menu.add_command(label="파생변수생성", command=controller.open_derived_popup)
view_menu = tk.Menu(self, tearoff=0)
self.add_cascade(label="보기", menu=view_menu)
view_menu.add_command(label="원본", command=controller.show_origin)
view_menu.add_command(label="복사본", command=controller.show_copy)
view_menu.add_command(label="종합 통계표 보기", command=controller.handle_analysis_report)
view_menu.add_command(label="전처리 로그", command=controller.show_logs)
view_graph = tk.Menu(self, tearoff=0)
self.add_cascade(label="그래프", menu=view_graph)
view_graph.add_command(label="산점도", command=controller.show_scatter)
view_graph.add_command(label="막대그래프", command=controller.show_bar)
self.add_command(label="검색", command=controller.toggle_region_bar)
def open_file(self):
path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
if path:
self.controller.load_file(path)
def save_file(self):
path = filedialog.asksaveasfilename(defaultextension=".csv")
if path:
self.controller.save_file(path)
# 파생변수생성 전담 클래스
class DerivedVariablePopup(tk.Toplevel):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
self.title("파생변수 생성")
self.geometry("300x180")
self.resizable(False, False)
self.grab_set()
ttk.Label(self, text="생성할 파생변수 선택").pack(pady=12)
ttk.Button(
self,
text="위험지역 여부 (사망자수 기준)",
command=lambda: self.create("risk")
).pack(fill="x", padx=20, pady=5)
ttk.Button(
self,
text="인구 대비 사고율",
command=lambda: self.create("rate")
).pack(fill="x", padx=20, pady=5)
def create(self, var_type):
self.controller.create_derived_variable(var_type)
self.destroy()
# 메인 View
class View(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.controller = None
self.pack(expand=True, fill="both", padx=5, pady=5)
self.control_bar = None
self.control_bar_visible = False
self.data_table = DataTable(self, controller=None)
self.data_table.pack(expand=True, fill="both")
def set_controller(self, controller):
self.controller = controller
self.control_bar = ControlBar(self.parent, controller)
self.data_table.controller = controller
self.parent.config(menu=MainMenu(self.parent, controller))
def toggle_control_bar(self):
if self.control_bar_visible:
self.control_bar.pack_forget()
else:
self.control_bar.pack(fill="x", padx=8, pady=4, before=self)
self.control_bar_visible = not self.control_bar_visible
def hide_control_bar(self):
if self.control_bar_visible and self.control_bar:
self.control_bar.pack_forget()
self.control_bar_visible = False
def update_region_list(self, regions):
if self.control_bar:
self.control_bar.update_regions(regions)
def display_data(self, df):
self.data_table.update_data(df)
def show_popup(self, title, msg):
Popup.info(title, msg)
def show_error(self, msg):
Popup.error(msg)
def show_analysis_table(self, df):
AnalysisTablePopup(self.parent, df)
def open_derived_popup(self):
DerivedVariablePopup(self.parent, self.controller)
def show_graph(self, df, graph_type):
GraphPopup(self.parent, df, graph_type)
# 그래프 팝업
class GraphPopup(tk.Toplevel):
def __init__(self, parent, df, graph_type):
super().__init__(parent)
self.title(f"그래프 - {graph_type}")
self.geometry("900x600")
fig = plt.Figure(figsize=(9, 5))
ax = fig.add_subplot(111)
if graph_type == "scatter":
sns.scatterplot(
data=df,
x="주민등록인구수(등록외국인포함)",
y="총사고발생건수",
s=60,
alpha=0.7,
ax=ax
)
ax.set_title("인구수 vs 사고 발생 건수")
elif graph_type == "bar":
sns.barplot(
data=df,
x="행정구역별(도/특별시/광역시)",
y="총사고발생건수",
errorbar=None,
ax=ax
)
ax.set_title("지역별 사고 발생 건수")
ax.tick_params(axis='x', rotation=45)
fig.tight_layout()
canvas = FigureCanvasTkAgg(fig, self)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)
ttk.Button(self, text="닫기", command=self.destroy).pack(pady=8)
controller.py
from datetime import datetime
class Controller:
# 컨트롤러는 뷰와 모델을 어떻게 적절하게 활용할건지 담당하는 클래스이다. 결국엔 모델과 뷰를 연결해주는 역할이다.
def __init__(self, model, view):
self.model = model
self.view = view
self.logs = []
def load_file(self, path):
try:
self.model.load_csv(path)
self.view.update_region_list(self.model.get_regions())
self.view.display_data(self.model.return_copy())
self.view.show_popup("파일 읽기", "파일 읽기 성공")
except Exception as e:
self.view.show_popup("파일 읽기 에러", f"파일을 불러오는 중 오류가 발생했습니다:\n{e}")
def save_file(self, path):
try:
self.model.save_csv(path)
self.view.show_popup("저장 완료", "파일이 성공적으로 저장되었습니다.")
except Exception as e:
self.view.show_popup("저장 에러", f"파일 저장 중 오류가 발생했습니다:\n{e}")
def handle_missing_(self):
try:
self.model.handle_missing()
self.add_log(action="결측치 처리", target="수치 컬럼", detail="NaN → 중앙값 대체")
self.view.display_data(self.model.return_copy())
self.view.show_popup("전처리", "결측치 처리가 완료되었습니다.")
except Exception as e:
self.view.show_popup("전처리 에러", f"결측치 처리 중 오류 발생:\n{e}")
def handle_outliers_(self):
try:
self.model.handle_outliers()
self.add_log(
action="이상치 처리",
target="총사고발생건수/사망자수/부상자수",
detail="IQR 기준 clipping"
)
self.view.display_data(self.model.return_copy())
self.view.show_popup("전처리", "이상치 처리가 완료되었습니다.")
except Exception as e:
self.view.show_popup("전처리 에러", f"이상치 처리 중 오류 발생:\n{e}")
def show_origin(self):
try:
self.view.hide_control_bar()
self.view.display_data(self.model.return_df())
except Exception as e:
self.view.show_popup("표시 에러", f"원본 데이터를 표시할 수 없습니다:\n{e}")
def show_copy(self):
try:
self.view.hide_control_bar()
self.view.display_data(self.model.return_copy())
except Exception as e:
self.view.show_popup("표시 에러", f"복사본 데이터를 표시할 수 없습니다:\n{e}")
def toggle_region_bar(self):
try:
self.view.toggle_control_bar()
except Exception as e:
print(f"UI Toggle Error: {e}")
def apply_filter_sort(self, region, sort_col, ascending):
try:
df = self.model.filter_and_sort(region, sort_col, ascending)
self.view.display_data(df)
except Exception as e:
self.view.show_popup("필터/정렬 에러", f"데이터를 거르는 중 오류가 발생했습니다:\n{e}")
def update_cell(self, row_idx, col_name, value):
try:
before = self.model.copy_df.at[row_idx, col_name]
self.model.update_cell(row_idx, col_name, value)
after = self.model.copy_df.at[row_idx, col_name]
self.add_log(
action="셀 수정",
target=f"row={row_idx}, col={col_name}",
detail=f"{before} → {after}"
)
self.view.display_data(self.model.return_copy())
except Exception as e:
self.view.show_popup("수정 에러", f"데이터 수정에 실패했습니다:\n{e}")
def handle_analysis_report(self):
try:
analysis_df = self.model.get_analysis_df()
if analysis_df is not None:
self.view.show_analysis_table(analysis_df)
else:
self.view.show_error("먼저 CSV 파일을 열어주세요.")
except Exception as e:
self.view.show_popup("분석 에러", f"데이터 분석 중 오류가 발생했습니다:\n{e}")
def show_logs(self):
if not self.logs:
self.view.show_popup("전처리 로그", "기록된 전처리 내역이 없습니다.")
return
text = "\n".join(
f"[{l['time']}] {l['action']} | {l['target']} | {l['detail']}"
for l in self.logs
)
self.view.show_popup("전처리 로그", text)
# 로그 기록용 공통 함수
def add_log(self, action, target="", detail=""):
time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.logs.append({
"time": time,
"action": action,
"target": target,
"detail": detail
})
# 파생변수 실행 함수
def create_derived_variable(self, var_type):
try:
df = self.model.return_copy()
if var_type == "risk":
if "위험지역여부" in df.columns:
self.view.show_popup("파생변수", "이미 '위험지역여부' 컬럼이 존재합니다.")
return
self.model.add_risk_region()
self.add_log(
action="파생변수 생성",
target="위험지역여부",
detail="사망자수 >= 5 → 1/0"
)
elif var_type == "rate":
if "인구대비사고율" in df.columns:
self.view.show_popup("파생변수", "이미 '인구대비사고율' 컬럼이 존재합니다.")
return
self.model.add_accident_rate()
self.add_log(
action="파생변수 생성",
target="인구대비사고율",
detail="발생건수 / 주민등록인구수"
)
self.view.display_data(self.model.return_copy())
self.view.show_popup("파생변수", "파생변수가 생성되었습니다.")
except Exception as e:
self.view.show_popup("파생변수 에러", str(e))
def open_derived_popup(self):
self.view.open_derived_popup()
def show_scatter(self):
df = self.model.return_copy()
self.view.show_graph(df, "scatter")
def show_bar(self):
df = self.model.return_copy()
self.view.show_graph(df, "bar")
'로보테크AI' 카테고리의 다른 글
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/21 (1) | 2026.01.21 |
|---|---|
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/20[SQL] (1) | 2026.01.20 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/15 (0) | 2026.01.15 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/14 (0) | 2026.01.14 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/13[Tkinter] (1) | 2026.01.13 |