전체 코드
main.py
import tkinter as tk
from model import Model
from view import View
from controller import Controller
class Dataanal_program(tk.Tk):
def __init__(self):
super().__init__()
self.title('data analysis program')
window = self.geometry('1080x720')
model = Model()
view = View(self)
controller = Controller(model, view)
view.set_controller(controller)
if __name__ == '__main__':
app = Dataanal_program()
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.df.columns = [c.strip() for c in self.df.columns]
self.copy_df = self.df.copy()
def return_df(self):
return self.df
def return_copy(self):
return self.copy_df
def save_csv(self, path):
if self.df is not None:
self.df.to_csv(path, index=False, encoding="utf-8-sig")
def handle_missing(self):
cols = [
"총사고발생건수",
"사망자수",
"부상자수",
"주민등록인구수(등록외국인포함)"
]
valid = [c for c in cols if c in self.copy_df.columns]
self.copy_df[valid] = self.copy_df[valid].fillna(
self.copy_df[valid].median()
)
def handle_outliers(self):
cols = ["총사고발생건수", "사망자수", "부상자수"]
for col in 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
low = q1 - 1.5 * iqr
high = q3 + 1.5 * iqr
self.copy_df[col] = np.clip(self.copy_df[col], low, high)
# 행정 구역 목록
def get_regions(self):
col = "행정구역별(도/특별시/광역시)"
if col not in self.copy_df.columns:
return []
regions = (
self.copy_df[col]
.dropna()
.astype(str)
.str.strip()
.unique()
.tolist()
)
regions.sort()
return regions
# 필터 + 정렬
def filter_and_sort(self, region, sort_col, ascending):
df = self.copy_df
if region:
df = df[df["행정구역별(도/특별시/광역시)"] == region]
if sort_col:
key = pd.to_numeric(df[sort_col], errors="coerce")
df = df.assign(_k=key).sort_values("_k", ascending=ascending).drop(columns="_k")
return df
# 데이터 수정 기능
def update_cell(self, row_idx, col_name, value):
if self.copy_df is None:
return
# 빈 문자열 → NaN
if value == "":
value = np.nan
# 숫자 컬럼은 숫자로 변환 시도
try:
value = float(value)
if value.is_integer():
value = int(value)
except:
pass
self.copy_df.at[row_idx, col_name] = value
def get_analysis_df(self):
if self.copy_df is None:
return None
df = self.copy_df.copy()
col_sido = '행정구역별(도/특별시/광역시)'
# 1. 숫자 데이터 전처리 (결측치 및 '-' 처리)
numeric_cols = [
'총사고발생건수', '사망자수', '부상자수',
'주민등록인구수(등록외국인포함)', '총자동차수'
]
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0)
# 2. 시도별로 그룹화 (작성하신 시도별 랭킹의 핵심)
area_df = df.groupby(col_sido).agg({
'총사고발생건수': 'sum',
'사망자수': 'sum',
'부상자수': 'sum',
'주민등록인구수(등록외국인포함)': 'sum',
'총자동차수': 'sum'
}).reset_index()
# 3. 전국 총계 계산 (전국 대비 비율 계산용)
total_national_accidents = area_df['총사고발생건수'].sum()
# 4. 분석 지표 및 랭킹 관련 데이터 계산
# 전국 대비 비율 (%)
area_df['전국_대비_비율(%)'] = (area_df['총사고발생건수'] / total_national_accidents * 100).round(2)
# 치명률 (%)
area_df['사고_치명률(%)'] = (area_df['사망자수'] / area_df['총사고발생건수'] * 100).round(2)
# 인구 1만명당 사고율
area_df['인구대비_사고율'] = (area_df['총사고발생건수'] / area_df['주민등록인구수(등록외국인포함)'] * 10000).round(2)
# 5. [중요] 사고 발생건수 기준 내림차순 정렬 및 순위 부여
area_df = area_df.sort_values(by='총사고발생건수', ascending=False)
area_df.insert(0, '순위', range(1, len(area_df) + 1))
# 6. 최종 보여줄 컬럼 선택
result_df = area_df[[
'순위', col_sido, '총사고발생건수', '사망자수',
'전국_대비_비율(%)', '사고_치명률(%)', '인구대비_사고율'
]]
return result_df
# 파생변수
def add_risk_region(self):
# 위험지역 여부 (사망자수 >= 5)
self.copy_df["위험지역여부"] = np.where(
self.copy_df["사망자수"] >= 5,
1,
0
)
def add_accident_rate(self):
accident_col = "총사고발생건수"
pop_col = "주민등록인구수(등록외국인포함)"
if accident_col not in self.copy_df.columns:
raise KeyError(accident_col)
if pop_col not in self.copy_df.columns:
raise KeyError(pop_col)
# 0 또는 NaN 방어
safe_pop = self.copy_df[pop_col].replace(0, np.nan)
self.copy_df = self.copy_df.assign(
인구대비사고율=self.copy_df[accident_col] / safe_pop
)
def get_map_value_map(self):
# 지도시각화에 필요한 전체 지표 데이터를 REGION_KEY 기반으로 가공하여 반환
if self.copy_df is None: return {}
df = self.copy_df.copy()
target_cols = ["총사고발생건수", "사망자수", "부상자수"]
# 숫자 변환
for c in target_cols:
if c in df.columns:
df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0)
# REGION_KEY 생성 로직 (city_county_key_from_csv 반영)
def make_key(row):
metro = {"서울특별시", "부산광역시", "대구광역시", "인천광역시", "광주광역시", "대전광역시", "울산광역시", "세종특별자치시", "제주특별자치도"}
sido = str(row["행정구역별(도/특별시/광역시)"]).strip()
sgg = str(row["행정구역별(시/구)"]).strip()
norm_sido = sido.replace(" ", "")
if norm_sido in metro:
return norm_sido
if " " in sgg:
first = sgg.split()[0].strip()
if first.endswith("시"):
return first.replace(" ", "")
return sgg.replace(" ", "")
df["REGION_KEY"] = df.apply(make_key, axis=1)
# 그룹화 및 딕셔너리 변환
agg = df.groupby("REGION_KEY")[target_cols].sum().reset_index()
value_map = {row["REGION_KEY"]: {k: float(row[k]) for k in target_cols} for _, row in agg.iterrows()}
return value_map
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
# 1.행정 구역 필터
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)
# 2.정렬 기준 필터
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)
# 3.정렬 방향 필터
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)
# 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 # 현재 표시 중인 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)
# 기존 DataTable 클래스 재활용
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_menu.add_command(label="지도로 보기", command=controller.handle_map_visualization)
# 그래프
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)
self.btn_risk = ttk.Button(
self,
text="위험지역 여부 (사망자수 기준)",
command=lambda: self.create("risk")
)
self.btn_risk.pack(fill="x", padx=20, pady=5)
self.btn_rate = ttk.Button(
self,
text="인구 대비 사고율",
command=lambda: self.create("rate")
)
self.btn_rate.pack(fill="x", padx=20, pady=5)
def create(self, var_type):
self.btn_risk.config(state="disabled")
self.btn_rate.config(state="disabled")
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
# 1. 메인 레이아웃 (View 본체)
self.pack(expand=True, fill="both", padx=5, pady=5)
# 2. 검색 바 전담 클래스 생성 (초기엔 보이지 않음)
self.control_bar = None
self.control_bar_visible = False
# 3. 데이터 테이블 생성
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:
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 open_traffic_map(self, value_map):
TrafficMapPopup(self.parent, value_map)
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":
region_col = "행정구역별(도/특별시/광역시)"
value_col = "총사고발생건수"
# 문자열 공백/NaN 정리(지역명 깨짐/중복 방지)
tmp = df.copy()
tmp[region_col] = tmp[region_col].astype(str).str.strip()
# 지역별 합계
agg = (
tmp.groupby(region_col, as_index=False)[value_col]
.sum()
.sort_values(value_col, ascending=False)
)
sns.barplot(
data=agg,
x=region_col,
y=value_col,
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)
#지도 시각화
import json
from shapely.geometry import Polygon
from shapely.ops import unary_union
import pandas as pd
class TrafficMapPopup(tk.Toplevel):
def __init__(self, parent, value_map):
super().__init__(parent)
self.title("교통사고 지표 지도 분석")
self.geometry("800x650")
self.value_map = value_map
self.canvas_w, self.canvas_h = 700, 550
self.padding = 30
# 지표 선택 및 상태창
ctrl_frame = ttk.Frame(self)
ctrl_frame.pack(fill="x", pady=5)
self.status_var = tk.StringVar(value="지표를 선택하여 분석을 시작하세요")
tk.Label(ctrl_frame, textvariable=self.status_var, font=("Malgun Gothic", 12, "bold")).pack()
self.canvas = tk.Canvas(self, width=self.canvas_w, height=self.canvas_h, bg="white", highlightthickness=0)
self.canvas.pack()
btn_frame = ttk.Frame(self)
btn_frame.pack(fill="x", side="bottom", pady=10)
metrics = ["총사고발생건수", "사망자수", "부상자수"]
for m in metrics:
ttk.Button(btn_frame, text=m, command=lambda val=m: self.repaint_map(val)).pack(side="left", padx=15)
self.region_items = {}
self.render_base_map()
def render_base_map(self):
#GeoJSON을 읽어 지도의 기본 형태(시/군/도 경계)를 그림
try:
with open("map.geojson", "r", encoding="utf-8") as f:
gj = json.load(f)
except:
messagebox.showerror("에러", "map.geojson 파일을 찾을 수 없습니다.")
return
regions_rings, sido_rings = {}, {}
all_lon, all_lat = [], []
# 1.링(Ring) 데이터 수집 및 좌표 범위 계산
for feat in gj.get("features", []):
props = feat.get("properties", {})
geom = feat.get("geometry", {})
sidonm, sggnm = props.get("sidonm"), props.get("sggnm")
# key 생성 함수 호출 (내부 구현)
city_key = self._get_geo_key(sidonm, sggnm)
sido_key = str(sidonm).strip().replace(" ", "")
coords = geom.get("coordinates")
rings = coords[0] if geom.get("type") == "Polygon" else [p[0] for p in coords]
for ring in (rings if geom.get("type") == "MultiPolygon" else [rings]):
regions_rings.setdefault(city_key, []).append(ring)
sido_rings.setdefault(sido_key, []).append(ring)
for lon, lat in ring:
all_lon.append(lon);
all_lat.append(lat)
self.min_lon, self.max_lon = min(all_lon), max(all_lon)
self.min_lat, self.max_lat = min(all_lat), max(all_lat)
# 2.Shapely 병합 및 그리기
merged_city = self._merge_rings(regions_rings)
merged_sido = self._merge_rings(sido_rings)
for city_key, shape in merged_city.items():
items = []
geoms = [shape] if shape.geom_type == "Polygon" else list(shape.geoms)
for g in geoms:
pts = self._project_shape(g)
it = self.canvas.create_polygon(pts, fill="#EDEDED", outline="#777777")
items.append(it)
self.region_items[city_key] = items
# 도 경계선 굵게 추가
for _, shape in merged_sido.items():
geoms = [shape] if shape.geom_type == "Polygon" else list(shape.geoms)
for g in geoms:
pts = self._project_shape(g)
self.canvas.create_polygon(pts, fill="", outline="#444444", width=2)
def _project_shape(self, shape):
coords = list(shape.exterior.coords)
pts = []
for lon, lat in coords:
x = (lon - self.min_lon) / (self.max_lon - self.min_lon + 1e-9)
y = (self.max_lat - lat) / (self.max_lat - self.min_lat + 1e-9)
pts.extend([self.padding + x * (self.canvas_w - 2 * self.padding),
self.padding + y * (self.canvas_h - 2 * self.padding)])
return pts
def _merge_rings(self, rings_dict):
merged = {}
for key, rings in rings_dict.items():
polys = [Polygon(r if r[0] == r[-1] else r + [r[0]]) for r in rings]
u = unary_union([p.buffer(0) if not p.is_valid else p for p in polys])
if not u.is_empty: merged[key] = u
return merged
def _get_geo_key(self, sidonm, sggnm):
metro = {"서울특별시", "부산광역시", "대구광역시", "인천광역시", "광주광역시", "대전광역시", "울산광역시", "세종특별자치시", "제주특별자치도"}
sido = str(sidonm).strip().replace(" ", "")
sgg = str(sggnm).strip()
if sido in metro: return sido
if sgg.endswith("군"): return sgg.replace(" ", "")
if "시" in sgg: return sgg[:sgg.find("시") + 1].replace(" ", "")
return sgg.replace(" ", "")
def repaint_map(self, metric):
vals = [self.value_map.get(ck, {}).get(metric, 0.0) for ck in self.region_items.keys()]
s = pd.Series(vals)
q = s.quantile([0.5, 0.75, 0.9, 0.95, 0.99]).tolist() if not s.empty else [0] * 5
palette = ["#fff5f5", "#ffe3e3", "#ffbdbd", "#ff8a8a", "#ff5c5c", "#e03131", "#a51111"]
for city_key, items in self.region_items.items():
v = self.value_map.get(city_key, {}).get(metric, 0.0)
color = palette[0]
for i, threshold in enumerate(q):
if v > threshold: color = palette[i + 1]
for it in items:
self.canvas.itemconfig(it, fill=color)
self.status_var.set(f"분석 지표: {metric}")
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.model.update_cell(row_idx, col_name, value)
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:
# 모델에서 가공된 분석용 DF를 가져옴
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 handle_map_visualization(self):
try:
# 1.모델에서 지도용 가공 데이터 가져오기 (인자 제거)
map_data = self.model.get_map_value_map()
# 2.뷰에 지도 팝업 요청 (메서드 이름 일치 및 인자 수정)
if map_data:
self.view.open_traffic_map(map_data)
else:
self.view.show_error("표시할 데이터가 없습니다.")
except Exception as e:
self.view.show_popup("지도 에러", f"지도를 생성할 수 없습니다: {e}")
def show_scatter(self):
try:
df = self.model.return_copy()
self.view.show_graph(df, "scatter")
except Exception as e:
self.view.show_popup("표시 에러", f"먼저 csv파일을 열어야됩니다.:\n{e}")
def show_bar(self):
try:
df = self.model.return_copy()
self.view.show_graph(df, "bar")
except Exception as e:
self.view.show_popup("표시 에러", f"먼저 csv파일을 열어야됩니다.:\n{e}")
map.geojson 일부
{
"type": "FeatureCollection",
"name": "20250401",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "adm_nm": "서울특별시 종로구 사직동", "adm_cd2": "1111053000", "sgg": "11110", "sido": "11", "sidonm": "서울특별시", "sggnm": "종로구", "adm_cd": "11010530" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 126.976888842748167, 37.575650779448786 ], [ 126.977034498877501, 37.569194530054553 ], [ 126.975974728212492, 37.569336299425771 ], [ 126.975374709912543, 37.569315567021562 ], [ 126.974331935623255, 37.569261800517531 ], [ 126.969048370018541, 37.568194417708327 ], [ 126.968544936033837, 37.568427679612753 ], [ 126.966649959821197, 37.569491655206583 ], [ 126.966281750244846, 37.569700734798694 ], [ 126.966097327080405, 37.569856509723706 ], [ 126.965728529225771, 37.570183936115114 ], [ 126.965926998221278, 37.570318805686206 ], [ 126.96601094018429, 37.571548395577473 ], [ 126.963659220521961, 37.575174660660373 ], [ 126.963086004345101, 37.576485920015543 ], [ 126.962840990511978, 37.57666158609274 ], [ 126.962810410472628, 37.579448809656768 ], [ 126.967424315843317, 37.579601537124489 ], [ 126.967421763026508, 37.579263521441646 ], [ 126.967430060184597, 37.579192577998604 ], [ 126.967457090095607, 37.578975250585437 ], [ 126.968066046996256, 37.578246780467879 ], [ 126.968955116954774, 37.577935262340283 ], [ 126.969212842969057, 37.577935299309388 ], [ 126.969414538865792, 37.578121124142172 ], [ 126.969664426694706, 37.578531136682216 ], [ 126.969667219148718, 37.578736205134931 ], [ 126.969668773533087, 37.578992879009881 ], [ 126.969669499103631, 37.57911252674959 ], [ 126.969904573616262, 37.579301753628727 ], [ 126.97135197544759, 37.57951327793981 ], [ 126.973819257844539, 37.579372140302624 ], [ 126.973917363383421, 37.578487073041011 ], [ 126.973939619980882, 37.578240429978088 ], [ 126.974331538357575, 37.575749906299862 ], [ 126.975803789978045, 37.57564946882421 ], [ 126.976888842748167, 37.575650779448786 ] ] ] ] } },
{ "type": "Feature", "properties": { "adm_nm": "서울특별시 종로구 삼청동", "adm_cd2": "1111054000", "sgg": "11110", "sido": "11", "sidonm": "서울특별시", "sggnm": "종로구", "adm_cd": "11010540" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 126.98268938649305, 37.595065519422398 ], [ 126.983372584569992, 37.594351925516882 ], [ 126.983868097928024, 37.593850468126433 ], [ 126.984011583697296, 37.593755381897623 ], [ 126.984273496190852, 37.593581837353476 ], [ 126.984678767240084, 37.593331231471026 ], [ 126.985225359852748, 37.5930571567418 ], [ 126.985286808140827, 37.593026356335294 ], [ 126.985328725807221, 37.593005726214919 ], [ 126.985759277179199, 37.592794011604347 ], [ 126.986479840783815, 37.592506465665579 ], [ 126.987526228202512, 37.592246800190104 ], [ 126.988760809134106, 37.592104003075761 ], [ 126.989109450717322, 37.591953415500925 ], [ 126.989475792086679, 37.5917950211998 ], [ 126.989075048100133, 37.591394461628269 ], [ 126.987898495808849, 37.590556715205203 ], [ 126.986210027969463, 37.589015966651431 ], [ 126.986013874282506, 37.588201649920947 ], [ 126.986010874237252, 37.588020241414476 ], [ 126.985956964282408, 37.587418359612826 ], [ 126.985790128959266, 37.587075122228988 ], [ 126.985337100030677, 37.586145890121195 ], [ 126.984974362286536, 37.585633285496748 ], [ 126.984465332345323, 37.585373689951119 ], [ 126.983714217816214, 37.584970435538899 ], [ 126.982918386510292, 37.583458326946833 ], [ 126.982849315927382, 37.583227993147538 ], [ 126.98283620716704, 37.582854067043328 ], [ 126.982842104010956, 37.582717763620892 ], [ 126.982864819894829, 37.582606322658101 ], [ 126.982991787047098, 37.582057058640778 ], [ 126.984034695368365, 37.578433706719785 ], [ 126.984639624275033, 37.577342343203 ], [ 126.98553316382565, 37.576674025462012 ], [ 126.984948844182966, 37.576335880365896 ], [ 126.984344788680914, 37.576064525106403 ], [ 126.983146652391852, 37.575526362928613 ], [ 126.979839036050819, 37.575943353839534 ], [ 126.979592489109393, 37.575977274556337 ], [ 126.979605791284357, 37.576607041918628 ], [ 126.979617516158967, 37.577161447995685 ], [ 126.97968954034279, 37.578609551100712 ], [ 126.979741125299739, 37.579131431874536 ], [ 126.980028224961956, 37.580086995769001 ], [ 126.980303701624806, 37.580986173619735 ], [ 126.979417938903708, 37.585900230324711 ], [ 126.978973668856298, 37.587178178288674 ], [ 126.978879575772694, 37.587271277315928 ], [ 126.976566811817619, 37.589296953371573 ], [ 126.97615022517256, 37.589545227974334 ], [ 126.975693343513328, 37.589684034240094 ], [ 126.97363965976345, 37.592594118993887 ], [ 126.973587263553426, 37.593289023280704 ], [ 126.975851137756862, 37.596564222244091 ], [ 126.98105808767329, 37.595065101105888 ], [ 126.98268938649305, 37.595065519422398 ] ] ] ] } },
{ "type": "Feature", "properties": { "adm_nm": "서울특별시 종로구 부암동", "adm_cd2": "1111055000", "sgg": "11110", "sido": "11", "sidonm": "서울특별시", "sggnm": "종로구", "adm_cd": "11010550" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 126.975851137756862, 37.596564222244091 ], [ 126.973587263553426, 37.593289023280704 ], [ 126.971937526494074, 37.593885782225897 ], [ 126.97154760196382, 37.593701308790735 ], [ 126.96984441311325, 37.593475095196247 ], [ 126.968924843946255, 37.59336392580812 ], [ 126.968871892276141, 37.593364914263915 ], [ 126.968778983264841, 37.593366650344137 ], [ 126.96870727402127, 37.593418361167267 ], [ 126.968642352894207, 37.593465182979493 ], [ 126.968530598684126, 37.593504599365019 ], [ 126.968472013174861, 37.593525256602454 ], [ 126.968138652949719, 37.593611635751977 ], [ 126.968043924368189, 37.59359205532072 ], [ 126.966433749475101, 37.592932440729243 ], [ 126.964688145735281, 37.591120725925748 ], [ 126.96457451385055, 37.590972619268328 ], [ 126.964111613838028, 37.589981390492333 ], [ 126.964137605268419, 37.589635008911145 ], [ 126.964142042118567, 37.589580331969429 ], [ 126.964240021716719, 37.589343047586297 ], [ 126.964237244108816, 37.589137979484313 ], [ 126.964115909960768, 37.588978566995401 ], [ 126.963422487230375, 37.588141674545561 ], [ 126.963173883022861, 37.587863381171267 ], [ 126.962233005353454, 37.586985922190337 ], [ 126.962036396787525, 37.586868147316309 ], [ 126.961977143996322, 37.586832654561519 ], [ 126.961710667930234, 37.586678211291549 ], [ 126.960505975254293, 37.586263725012984 ], [ 126.959682146563551, 37.586044761977305 ], [ 126.959469298789557, 37.586012240450067 ], [ 126.959311270253522, 37.58606121583167 ], [ 126.95917912366734, 37.586102172264638 ], [ 126.958865149453985, 37.585957645050037 ], [ 126.958048426953269, 37.585211145245822 ], [ 126.957804671329043, 37.584988341358191 ], [ 126.957753869177395, 37.584868680012001 ], [ 126.957895324618931, 37.583811410465415 ], [ 126.956894000654685, 37.585420104684566 ], [ 126.957172233903208, 37.586467603189412 ], [ 126.957473530231795, 37.587601840353926 ], [ 126.957929061313536, 37.588793643489147 ], [ 126.957514600188262, 37.592672080333642 ], [ 126.957250168102632, 37.593144703750241 ], [ 126.956765760010683, 37.594010509819597 ], [ 126.956498706310668, 37.59470677017238 ], [ 126.956329141603035, 37.595149822476095 ], [ 126.956312584542601, 37.595477610117847 ], [ 126.956309289703952, 37.595542942204389 ], [ 126.956303458066444, 37.595662365631078 ], [ 126.956612654445721, 37.596536562148643 ], [ 126.95695401282785, 37.59762350150384 ], [ 126.956983392286148, 37.597717808816441 ], [ 126.957041885710893, 37.597905574933776 ], [ 126.957145636243482, 37.598242928466817 ], [ 126.957135378943633, 37.598387095876419 ], [ 126.957100419578978, 37.598458871108285 ], [ 126.957054880455601, 37.598552347020352 ], [ 126.956967121128756, 37.598613815825999 ], [ 126.956883362613723, 37.598632921916838 ], [ 126.956731208422582, 37.598664651887326 ], [ 126.95635103787437, 37.598660647958702 ], [ 126.956214054773838, 37.598595489881518 ], [ 126.956123843030781, 37.598552566951696 ], [ 126.956127098318859, 37.598516212838994 ], [ 126.956132314159888, 37.598457965171306 ], [ 126.955517572167537, 37.598483297645537 ], [ 126.954093263161255, 37.598551078811226 ], [ 126.953974153857814, 37.598666332110852 ], [ 126.953935671367489, 37.598703577745205 ], [ 126.953825857936593, 37.598810103852657 ], [ 126.953508539793177, 37.599118470914306 ], [ 126.952749308815697, 37.600406123595477 ], [ 126.952762711530923, 37.601281207225824 ], [ 126.952765245891229, 37.601444200277108 ], [ 126.952777186346921, 37.602206681529097 ], [ 126.952779638843808, 37.602258519073516 ], [ 126.952801984745633, 37.602694705582692 ], [ 126.952881956684848, 37.602789658500043 ], [ 126.953012144646152, 37.602868930049006 ], [ 126.953081452954351, 37.602911137444842 ], [ 126.953201084725507, 37.603011965893799 ], [ 126.953264830420252, 37.603202668461329 ], [ 126.953512662014631, 37.604265145333116 ], [ 126.953536478608697, 37.604493970672458 ], [ 126.953540434971941, 37.604574162292607 ], [ 126.953542644269902, 37.604619059259029 ], [ 126.953437334821999, 37.605174023292186 ], [ 126.953376321249763, 37.605366284050788 ], [ 126.953857584254791, 37.604930353680167 ], [ 126.954010054553052, 37.604801490634614 ], [ 126.954238003275776, 37.604712410991084 ], [ 126.954627051489794, 37.60457224932783 ], [ 126.958154426482579, 37.603531674782161 ], [ 126.958249230303466, 37.603542250992902 ], [ 126.95842358280305, 37.603617411804947 ], [ 126.958625217020028, 37.603737764532148 ], [ 126.958729658424872, 37.603805617881036 ], [ 126.95909449579969, 37.604738150752624 ], [ 126.960011878346435, 37.60585550704166 ], [ 126.961070552793657, 37.606968940614223 ], [ 126.961534787487153, 37.607058994444564 ], [ 126.961638070815837, 37.60677892266331 ], [ 126.961658697800061, 37.606713463196492 ], [ 126.961671405342997, 37.606673168167994 ], [ 126.961696031847083, 37.606596666592971 ], [ 126.961758658130833, 37.606453864494398 ], [ 126.961773310350566, 37.606423700371813 ], [ 126.961829095938725, 37.606308844630554 ], [ 126.961889417646788, 37.606191945474635 ], [ 126.961974146074382, 37.606028963872404 ], [ 126.962100387976022, 37.60580420428451 ], [ 126.962228445071602, 37.605622266485099 ], [ 126.962698383208107, 37.605064449288584 ], [ 126.963159197885886, 37.604185170623389 ], [ 126.963255484023165, 37.604002275910723 ], [ 126.964331734146839, 37.603312005955722 ], [ 126.965664035224989, 37.601370023492201 ], [ 126.966160303295325, 37.600884418442369 ], [ 126.966611244510943, 37.600598028145022 ], [ 126.967145864119942, 37.600292860429086 ], [ 126.973295489353873, 37.598820775254246 ], [ 126.974103060069027, 37.597971564711237 ], [ 126.974186893400912, 37.597609013814214 ], [ 126.973675150893342, 37.59710436897587 ], [ 126.973828945337146, 37.597029522256484 ], [ 126.974005510874818, 37.596943600178797 ], [ 126.975851137756862, 37.596564222244091 ] ] ] ] } },
실행 화면

MVC패턴
Model, View, Controller가 각자 맡은 일만 하도록 분리
ㄴ계산, 화면, 연결
Model
데이터와 계산 담당
CSV 불러오기
데이터 저장
전처리 (결측치, 이상치)
파생변수 생성
필터, 정렬, 분석 결과 생성
View
화면과 사용자에게 보이는 것 담당
Tkinter 화면
테이블 출력
팝업
그래프
메뉴
Controller
Model과 View 사이에서 중간 조정자
실제 로직 X
| MVC O | MVC X |
| 버튼 코드 안에 데이터 처리 섞임 | 데이터 바꿔도 화면 영향 X |
| 그래프 코드 안에 파일 저장 섞임 | 화면 바꿔도 데이터 로직 영향 X |
| 고치려면 전부 영향 | 기능 제거/추가가 안전 |
데이터베이스와 SQL
(MySQL 사용)
데이터베이스:데이터의 집합
DBMS: 데이터베이스를 관리하고 운영하는 소프트웨어
ㄴMySQL, MariaDB, PostgreSQL, Orafle, SQL Server, DB2, Access, SQLite ...
DBMS 분류
계층형
각 계층은 트리 형태를 가짐, 처음 구성을 완료한 후 변경하기 까다로움, 현재 사용 X
망형
하위 구성원 들끼리도 연결된 유연한 구조. 잘 활용하려면 프로그래머가 모든 구조 이해해야만 작성 가능
관계형
RDBMS
대부분의 DBMS가 RDBMS 형태
테이블이라는 최소 단위로 구성되며, 테이블은 열과 행을 가짐(2차원 구조)
객체지향형
객체관계형
SQL
데이터베이스를 구축, 관리하고 활용하기 위해 사용되는 언어
관계형 데이터베이스에서 사용되는 언어
데이터베이스를 조작하는 언어지만 일반적인 프로그래밍 언어와는 다름
SQL은 국제 표준
MySQL 설치
https://dev.mysql.com/downloads/installer/

No thanks 바로 누르기
혼자 공부하는 SQL 45p까지
'로보테크AI' 카테고리의 다른 글
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/22 (0) | 2026.01.22 |
|---|---|
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/21 (1) | 2026.01.21 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/19 (0) | 2026.01.19 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/15 (0) | 2026.01.15 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/14 (0) | 2026.01.14 |