common.py
원본 내용을 읽기 전용으로 표시합니다.
# 공통 분석 모듈1. 이건 왠만하면 바꾸지 마세용ㅇㅇimport osimport warningsimport numpy as npimport pandas as pdimport matplotlibmatplotlib.use("Agg") # 그림을 창으로 안 띄우고 파일로만 저장import matplotlib.pyplot as pltfrom matplotlib import font_managerimport foliumwarnings.filterwarnings("ignore") # 경고 메시지 너무 많이 떠서 끔# 폴더 경로 주의!폴더 = os.path.dirname(os.path.abspath(__file__)) # 파일이 있는 폴더데이터폴더 = os.path.join(폴더, "data")결과폴더 = os.path.join(폴더, "output")os.makedirs(결과폴더, exist_ok=True) # output 폴더 없으면 만들기!SEED = 42 # 랜덤 값 고정(돌릴 때마다 결과 같게)TARGET = "조치원읍" # 타겟 지역GRID_M = 250 # 격자 크기 (보통 250m)MIN_STORES = 8 # 격자 안에 업소가 8개보다 적으면 상권으로 안 봄# 한글 폰트 설정 (안 하면 깨짐)폰트목록 = [f.name for f in font_manager.fontManager.ttflist]if "Noto Sans CJK KR" in 폰트목록:plt.rcParams["font.family"] = "Noto Sans CJK KR"elif "NanumGothic" in 폰트목록:plt.rcParams["font.family"] = "NanumGothic"elif "Malgun Gothic" in 폰트목록:plt.rcParams["font.family"] = "Malgun Gothic" # 윈도우plt.rcParams["axes.unicode_minus"] = False # 마이너스 기호 깨짐 방지(?)# 그래프 색깔 (업종, 군집 구분할 때 순서대로 씀. 색깔은 내맘대로 함ㅇㅇ)COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728","#9467bd", "#8c564b", "#e377c2", "#17becf"]# 결과를 화면에 출력하고 나중에 txt 파일로도 저장하려고 리스트에 모아둠 참고하셔요결과기록 = []# 공통 함수# log: 화면 출력 + 결과기록에 저장(파일로 나와요!)# 그림저장: 그래프를 output에 저장# 지도만들기: 배경지도가 깔린 빈 folium 지도# 다양성: 업종 다양성 지수 (행정동, 법정동은 쓰면 클남)# 최빈값: 격자의 대표 행정동 정할 때 씀(행!정!동!!!! 제발 법정동 쓰지마)def log(내용=""):print(내용)결과기록.append(str(내용))def 그림저장(파일이름):plt.tight_layout()plt.savefig(os.path.join(결과폴더, 파일이름), bbox_inches="tight", dpi=120)plt.close()def 지도만들기(위치, 확대):# 기본 지도(OSM)가 html 파일로 열면 안 나와서 다른 주소(OSM HOT)를 씀. 왜 이러는지 모르겠음 ## 나도 모름m = folium.Map(location=위치, zoom_start=확대, tiles=None)folium.TileLayer(tiles="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",attr="© OpenStreetMap contributors, Tiles style by HOT, hosted by OpenStreetMap France",name="배경지도(OpenStreetMap HOT)", max_zoom=19).add_to(m)return mdef 다양성(s):# 샤논 엔트로피 (업종이 여러 개 골고루 있으면 값이 커짐)p = s.value_counts(normalize=True)return float(-(p * np.log(p)).sum())def 최빈값(s):# 제일 많이 나온 값return s.mode().iloc[0]# 위도, 경도를 미터로 바꿀 때 기준점 (세종 가운데 중심)LAT0 = 36.50LON0 = 127.27# 1단계 : 데이터 불러오기, 전처리def 데이터준비():# 하는 일- 두 시점 csv 를 읽어서 정리하고, 뒤 단계에서 계속 쓰는 컬럼 제작# 돌려주는 값# now: 2026 업소 표# prev: 2025 업소 표# CATS: 업종 대분류 10개 이름 (업소 많은 순). 그래프, 군집 변수 순서를 이걸로 통일log("=" * 70)log("1. 데이터 불러오기 및 전처리")log("=" * 70)# 원자료는 컬럼이 39개인데 주소/코드 컬럼은 거의 안 써서 필요한 11개만 읽음.# 업종명 공백 문제는 01.py 에서 발견함 (안 지우면 '비알코올'이 두 업종으로 갈라짐. 힘들다아앙)컬럼 = ["상가업소번호", "상호명", "지점명", "상권업종대분류명", "상권업종중분류명","상권업종소분류명", "행정동명", "도로명", "층정보", "경도", "위도"]now = pd.read_csv(os.path.join(데이터폴더, "sejong_store_202606.csv"), usecols=컬럼, low_memory=False)prev = pd.read_csv(os.path.join(데이터폴더, "sejong_store_202506.csv"), usecols=컬럼, low_memory=False)log(f"2026-06 업소 수: {len(now):,}개 / 2025-06 업소 수: {len(prev):,}개")# 업종 이름 뒤에 공백이 붙은 게 있음 ('비알코올 ') => 이거 없에는거for c in ["상권업종대분류명", "상권업종중분류명", "상권업종소분류명", "행정동명"]:now[c] = now[c].astype(str).str.strip()prev[c] = prev[c].astype(str).str.strip()# 결측치, 중복 확인결측수 = now[["경도", "위도"]].isna().any(axis=1).sum()중복수 = now["상가업소번호"].duplicated().sum()log(f"좌표 결측: {결측수}건, 업소번호 중복: {중복수}건")now = now.dropna(subset=["경도", "위도"])now = now.drop_duplicates("상가업소번호")# 분석용 컬럼 만들기# 도로,권역,1층여부,체인은 원본에 없어서 직접 만듬# 이 컬럼들이 이따 골목vs대로 비교, 원도심vs신도심 비교애 쓰임# 도로명에서 마지막 단어만 쓰기 (예: '세종특별자치시 세종특별자치시 충현로' -> '충현로')now["도로"] = now["도로명"].astype(str).str.split().str[-1]prev["도로"] = prev["도로명"].astype(str).str.split().str[-1]# 읍,면이면 원도심/동이면 신도심(예외는 아마 없을듯)권역목록 = []for 동 in now["행정동명"]:if 동.endswith("읍") or 동.endswith("면"):권역목록.append("읍면(원도심)")else:권역목록.append("동(신도심)")now["권역"] = 권역목록# 1층이면 1, 다른 층이면 0, 층정보가 비어 있으면 -1(이래야 나중에 변환 쉬움)now["층"] = now["층정보"].astype(str).str.strip()def 일층확인(층):if 층 == "1":return 1elif 층 == "nan":return -1else:return 0now["1층여부"] = now["층"].apply(일층확인)# 체인점 여부(따로 데이터가 없어서 지점명이 있거나 같은 이름 가게가 3개 이상이면 체인으로 보기로 함. 근데 체인점이 2개 이하면 체인으로 안볼테니까 문제가 있을듯)이름개수 = now["상호명"].value_counts()now["체인"] = ((now["지점명"].notna()) | (now["상호명"].map(이름개수) >= 3)).astype(int)# 1년 전 비교# 같은 가게는 두 시점에서 상가업소번호가 같다는 점을 이용# 주의!!!!!!!!!!!!!!: 번호만 바뀐 가게도 일부 있어서(약 285건, 탐색과정/02) 소멸이 쪼끔? 크게 잡힐 수 있음# 작년에 없던 업소번호 -> 신규 / 작년에 있었는데 올해 없는 업소번호 -> 소멸(폐업이나 이전)작년번호 = set(prev["상가업소번호"])올해번호 = set(now["상가업소번호"])now["신규"] = (~now["상가업소번호"].isin(작년번호)).astype(int)prev["소멸"] = (~prev["상가업소번호"].isin(올해번호)).astype(int)log(f"1년간 유지 {len(작년번호 & 올해번호):,} / 소멸 {prev['소멸'].sum():,} / 신규 {now['신규'].sum():,}")log(f"전체 1년 소멸률 {prev['소멸'].mean():.1%}, 신규 비율(현재 업소 중) {now['신규'].mean():.1%}")# 좌표 변환# 250m 격자, DBSCAN 60m, 주변 100m 업소 수 같은 걸 계산하려면 미터 단위가 필요함# 세종 중심(LAT0, LON0 은 위쪽 설정 부분) 기준으로 대충 계산now["x"] = (now["경도"] - LON0) * 111320 * np.cos(np.radians(LAT0))now["y"] = (now["위도"] - LAT0) * 110540prev["x"] = (prev["경도"] - LON0) * 111320 * np.cos(np.radians(LAT0))prev["y"] = (prev["위도"] - LAT0) * 110540# 업종 대분류 10개 (많은 순서)CATS = now["상권업종대분류명"].value_counts().index.tolist()#끝! 모듈 한참 남음 ㅠㅠreturn now, prev, CATS
파일 확인 정보
SHA-256: f0fd6b8b238fc925b2ce346a1c60fa3f0424234d65b59c448ac8fbbafa162ea0