← 메인 화면프로젝트 자료·코드
← 자료 목록

common.py

프로그램·코드 · 8.1 KB원본 다운로드

원본 내용을 읽기 전용으로 표시합니다.

  1. # 공통 분석 모듈1. 이건 왠만하면 바꾸지 마세용ㅇㅇ
  2. import os
  3. import warnings
  4. import numpy as np
  5. import pandas as pd
  6. import matplotlib
  7. matplotlib.use("Agg") # 그림을 창으로 안 띄우고 파일로만 저장
  8. import matplotlib.pyplot as plt
  9. from matplotlib import font_manager
  10. import folium
  11. warnings.filterwarnings("ignore") # 경고 메시지 너무 많이 떠서 끔
  12. # 폴더 경로 주의!
  13. 폴더 = os.path.dirname(os.path.abspath(__file__)) # 파일이 있는 폴더
  14. 데이터폴더 = os.path.join(폴더, "data")
  15. 결과폴더 = os.path.join(폴더, "output")
  16. os.makedirs(결과폴더, exist_ok=True) # output 폴더 없으면 만들기!
  17. SEED = 42 # 랜덤 값 고정(돌릴 때마다 결과 같게)
  18. TARGET = "조치원읍" # 타겟 지역
  19. GRID_M = 250 # 격자 크기 (보통 250m)
  20. MIN_STORES = 8 # 격자 안에 업소가 8개보다 적으면 상권으로 안 봄
  21. # 한글 폰트 설정 (안 하면 깨짐)
  22. 폰트목록 = [f.name for f in font_manager.fontManager.ttflist]
  23. if "Noto Sans CJK KR" in 폰트목록:
  24. plt.rcParams["font.family"] = "Noto Sans CJK KR"
  25. elif "NanumGothic" in 폰트목록:
  26. plt.rcParams["font.family"] = "NanumGothic"
  27. elif "Malgun Gothic" in 폰트목록:
  28. plt.rcParams["font.family"] = "Malgun Gothic" # 윈도우
  29. plt.rcParams["axes.unicode_minus"] = False # 마이너스 기호 깨짐 방지(?)
  30. # 그래프 색깔 (업종, 군집 구분할 때 순서대로 씀. 색깔은 내맘대로 함ㅇㅇ)
  31. COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728",
  32. "#9467bd", "#8c564b", "#e377c2", "#17becf"]
  33. # 결과를 화면에 출력하고 나중에 txt 파일로도 저장하려고 리스트에 모아둠 참고하셔요
  34. 결과기록 = []
  35. # 공통 함수
  36. # log: 화면 출력 + 결과기록에 저장(파일로 나와요!)
  37. # 그림저장: 그래프를 output에 저장
  38. # 지도만들기: 배경지도가 깔린 빈 folium 지도
  39. # 다양성: 업종 다양성 지수 (행정동, 법정동은 쓰면 클남)
  40. # 최빈값: 격자의 대표 행정동 정할 때 씀(행!정!동!!!! 제발 법정동 쓰지마)
  41. def log(내용=""):
  42. print(내용)
  43. 결과기록.append(str(내용))
  44. def 그림저장(파일이름):
  45. plt.tight_layout()
  46. plt.savefig(os.path.join(결과폴더, 파일이름), bbox_inches="tight", dpi=120)
  47. plt.close()
  48. def 지도만들기(위치, 확대):
  49. # 기본 지도(OSM)가 html 파일로 열면 안 나와서 다른 주소(OSM HOT)를 씀. 왜 이러는지 모르겠음 ## 나도 모름
  50. m = folium.Map(location=위치, zoom_start=확대, tiles=None)
  51. folium.TileLayer(
  52. tiles="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
  53. attr="© OpenStreetMap contributors, Tiles style by HOT, hosted by OpenStreetMap France",
  54. name="배경지도(OpenStreetMap HOT)", max_zoom=19).add_to(m)
  55. return m
  56. def 다양성(s):
  57. # 샤논 엔트로피 (업종이 여러 개 골고루 있으면 값이 커짐)
  58. p = s.value_counts(normalize=True)
  59. return float(-(p * np.log(p)).sum())
  60. def 최빈값(s):
  61. # 제일 많이 나온 값
  62. return s.mode().iloc[0]
  63. # 위도, 경도를 미터로 바꿀 때 기준점 (세종 가운데 중심)
  64. LAT0 = 36.50
  65. LON0 = 127.27
  66. # 1단계 : 데이터 불러오기, 전처리
  67. def 데이터준비():
  68. # 하는 일- 두 시점 csv 를 읽어서 정리하고, 뒤 단계에서 계속 쓰는 컬럼 제작
  69. # 돌려주는 값
  70. # now: 2026 업소 표
  71. # prev: 2025 업소 표
  72. # CATS: 업종 대분류 10개 이름 (업소 많은 순). 그래프, 군집 변수 순서를 이걸로 통일
  73. log("=" * 70)
  74. log("1. 데이터 불러오기 및 전처리")
  75. log("=" * 70)
  76. # 원자료는 컬럼이 39개인데 주소/코드 컬럼은 거의 안 써서 필요한 11개만 읽음.
  77. # 업종명 공백 문제는 01.py 에서 발견함 (안 지우면 '비알코올'이 두 업종으로 갈라짐. 힘들다아앙)
  78. 컬럼 = ["상가업소번호", "상호명", "지점명", "상권업종대분류명", "상권업종중분류명",
  79. "상권업종소분류명", "행정동명", "도로명", "층정보", "경도", "위도"]
  80. now = pd.read_csv(os.path.join(데이터폴더, "sejong_store_202606.csv"), usecols=컬럼, low_memory=False)
  81. prev = pd.read_csv(os.path.join(데이터폴더, "sejong_store_202506.csv"), usecols=컬럼, low_memory=False)
  82. log(f"2026-06 업소 수: {len(now):,}개 / 2025-06 업소 수: {len(prev):,}개")
  83. # 업종 이름 뒤에 공백이 붙은 게 있음 ('비알코올 ') => 이거 없에는거
  84. for c in ["상권업종대분류명", "상권업종중분류명", "상권업종소분류명", "행정동명"]:
  85. now[c] = now[c].astype(str).str.strip()
  86. prev[c] = prev[c].astype(str).str.strip()
  87. # 결측치, 중복 확인
  88. 결측수 = now[["경도", "위도"]].isna().any(axis=1).sum()
  89. 중복수 = now["상가업소번호"].duplicated().sum()
  90. log(f"좌표 결측: {결측수}건, 업소번호 중복: {중복수}건")
  91. now = now.dropna(subset=["경도", "위도"])
  92. now = now.drop_duplicates("상가업소번호")
  93. # 분석용 컬럼 만들기
  94. # 도로,권역,1층여부,체인은 원본에 없어서 직접 만듬
  95. # 이 컬럼들이 이따 골목vs대로 비교, 원도심vs신도심 비교애 쓰임
  96. # 도로명에서 마지막 단어만 쓰기 (예: '세종특별자치시 세종특별자치시 충현로' -> '충현로')
  97. now["도로"] = now["도로명"].astype(str).str.split().str[-1]
  98. prev["도로"] = prev["도로명"].astype(str).str.split().str[-1]
  99. # 읍,면이면 원도심/동이면 신도심(예외는 아마 없을듯)
  100. 권역목록 = []
  101. for 동 in now["행정동명"]:
  102. if 동.endswith("읍") or 동.endswith("면"):
  103. 권역목록.append("읍면(원도심)")
  104. else:
  105. 권역목록.append("동(신도심)")
  106. now["권역"] = 권역목록
  107. # 1층이면 1, 다른 층이면 0, 층정보가 비어 있으면 -1(이래야 나중에 변환 쉬움)
  108. now["층"] = now["층정보"].astype(str).str.strip()
  109. def 일층확인(층):
  110. if 층 == "1":
  111. return 1
  112. elif 층 == "nan":
  113. return -1
  114. else:
  115. return 0
  116. now["1층여부"] = now["층"].apply(일층확인)
  117. # 체인점 여부(따로 데이터가 없어서 지점명이 있거나 같은 이름 가게가 3개 이상이면 체인으로 보기로 함. 근데 체인점이 2개 이하면 체인으로 안볼테니까 문제가 있을듯)
  118. 이름개수 = now["상호명"].value_counts()
  119. now["체인"] = ((now["지점명"].notna()) | (now["상호명"].map(이름개수) >= 3)).astype(int)
  120. # 1년 전 비교
  121. # 같은 가게는 두 시점에서 상가업소번호가 같다는 점을 이용
  122. # 주의!!!!!!!!!!!!!!: 번호만 바뀐 가게도 일부 있어서(약 285건, 탐색과정/02) 소멸이 쪼끔? 크게 잡힐 수 있음
  123. # 작년에 없던 업소번호 -> 신규 / 작년에 있었는데 올해 없는 업소번호 -> 소멸(폐업이나 이전)
  124. 작년번호 = set(prev["상가업소번호"])
  125. 올해번호 = set(now["상가업소번호"])
  126. now["신규"] = (~now["상가업소번호"].isin(작년번호)).astype(int)
  127. prev["소멸"] = (~prev["상가업소번호"].isin(올해번호)).astype(int)
  128. log(f"1년간 유지 {len(작년번호 & 올해번호):,} / 소멸 {prev['소멸'].sum():,} / 신규 {now['신규'].sum():,}")
  129. log(f"전체 1년 소멸률 {prev['소멸'].mean():.1%}, 신규 비율(현재 업소 중) {now['신규'].mean():.1%}")
  130. # 좌표 변환
  131. # 250m 격자, DBSCAN 60m, 주변 100m 업소 수 같은 걸 계산하려면 미터 단위가 필요함
  132. # 세종 중심(LAT0, LON0 은 위쪽 설정 부분) 기준으로 대충 계산
  133. now["x"] = (now["경도"] - LON0) * 111320 * np.cos(np.radians(LAT0))
  134. now["y"] = (now["위도"] - LAT0) * 110540
  135. prev["x"] = (prev["경도"] - LON0) * 111320 * np.cos(np.radians(LAT0))
  136. prev["y"] = (prev["위도"] - LAT0) * 110540
  137. # 업종 대분류 10개 (많은 순서)
  138. CATS = now["상권업종대분류명"].value_counts().index.tolist()
  139. #끝! 모듈 한참 남음 ㅠㅠ
  140. return now, prev, CATS
파일 확인 정보

SHA-256: f0fd6b8b238fc925b2ce346a1c60fa3f0424234d65b59c448ac8fbbafa162ea0