2026. 2. 20. 01:36ㆍData Analyst Project
학원명당 - 원스토어
학원명당: 데이터 기반 학원 입지 분석 서비스
m.onestore.co.kr
최근에 지인이 학원 개업을 생각하게 되면서, 어디가 학원입지로서 적합한지에 대해서 고민이라는 이야기를 듣게 되었습니다.
지인의 이런 고민을 데이터 분석을 통해서 해결할 수 있다고 생각하게 되었고, 이런 고민을 가지고 있는 학원 창업자들에게 도움이 되고자 학원입지 분석 어플리케이션을 개발하게 되었습니다.
서론
첫번째로는, 지인의 요구사항 파악과 최종적인 결과물의 형식과 대시보드에 나타낼 정보들을 정리해보았습니다.
- 요구사항 파악: 지인의 핵심 고민인 '유동 인구 대비 경쟁 강도'와 '임대료 수준'을 최우선 분석 지표로 설정했습니다.
- 지표 정의: 시장 구매력(EPI), 입지 가성비(ROI_GAP), 유소년 비율, 학원 밀도 등 4가지 핵심 지표를 도출했습니다.
- 결과물 형식: 행정동별 데이터를 지도상에 색상으로 표현하는 히트맵 기반의 웹 어플리케이션으로 형식을 확정했습니다.
- 정보 구성: 클러스터링을 통한 상권 유형 분류, 인구 통계, 학교 현황, 주변 아파트 시세 정보를 한 화면에 구성하여 종합적인 판단이 가능하도록 기획했습니다.
두번째로는, 앞서 정의한 요구사항을 실질적인 데이터 모델로 구현하기 위한 분석 프로세스를 설계했습니다.
- 지표의 구체화: '시장 구매력'을 단순히 인구수로 판단하지 않고, 해당 지역의 아파트 평균 실거래가와 학생 수를 결합한 EPI(Market Purchasing Power) 지수로 수식화했습니다. 또한, 상가 임대료 대비 학생 수 비중을 계산하여 초기 진입 비용 대비 효율성을 측정하는 ROI_GAP 지표를 설계하여 분석의 객관성을 확보했습니다.
- 상권 유형화 전략: 단순한 데이터 나열에서 벗어나 학원 창업자에게 직관적인 인사이트를 제공하기 위해 클러스터링 분석을 계획했습니다. 입지별 특성에 따라 '학원 밀집지', '고급 주거지', '블루오션' 등으로 그룹화하여 본인의 자본금과 타겟층에 맞는 지역을 빠르게 필터링할 수 있는 구조를 설계했습니다.
- 공간 단위 설정: 분석의 최소 단위를 법정 경계동인 '법정동'으로 설정했습니다. 이는 학원 선택 시 거주지와의 인접성이 중요하다는 점을 반영한 것이며, 향후 지도 시각화 단계에서 사용자에게 익숙한 지역 정보를 제공하기 위한 결정이었습니다.
- 시각화 시나리오 구성: 사용자가 특정 동을 선택했을 때 해당 지역의 인구 통계, 학교 리스트, 주변 시세가 연동되어 나타나는 상호작용 시나리오를 구성하여, 데이터 분석 결과가 실제 의사결정 도구로 기능할 수 있도록 기획했습니다.
수집
데이터 수집은 공공데이터포털, 경기데이터드림, 서울열린데이터광장, 국토교통부 등에서 공공데이터를 수집하였습니다.
수집을 할때에는 각 데이터들이 내가 원하는 데이터 칼럼이 존재하는지를 확실히 확인하고 데이터를 수집하였습니다.
여기서 중요한것은 "법정동" 기준으로 데이터들이 이루어져있나 입니다.
행정동으로 수집이 되어있다면, 전처리 과정중 병합부분에서 데이터가 꼬이며 결측치가 많아지기 때문에, 이부분에 특히 유의하였습니다.
공공데이터 포털
국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase
www.data.go.kr
전처리
1. 학교 데이터 전처리 (서울 + 경기도)
① 서울 데이터 로드 및 동 추출
# 서울 데이터 전처리
import pandas as pd
import numpy as np
import re
import os
base_path = '/content/drive/MyDrive/'
df_seoul = pd.read_csv(f'{base_path}서울시 학교, 학생 데이터.csv', encoding='cp949')
df_seoul['temp_addr'] = df_seoul['도로명상세주소'].fillna(df_seoul['도로명주소']).astype(str)
df_seoul['dong'] = df_seoul['temp_addr'].apply(lambda x: re.search(r'\(([가-힣]+[동읍면])', str(x)).group(1) if re.search(r'\(([가-힣]+[동읍면])', str(x)) else None)
df_seoul['시도'] = df_seoul['도로명주소'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
df_seoul = df_seoul.sort_values('공시연도', ascending=False).drop_duplicates('학교명')
도로명상세주소가 없는 경우 도로명주소로 대체합니다. 이후 주소의 괄호 () 안에서 동/읍/면으로 끝나는 행정동 이름을 정규식으로 추출합니다. 또한 공시연도 기준 내림차순 정렬 후 학교명 중복을 제거해 최신 데이터만 남깁니다.
② 서울 피벗 테이블 생성
# 서울 피벗 테이블 생성
# 시도동을 인덱스로, 학교급 코드별로 학생수 합산, 학교명 카운트
seoul_std = df_seoul.pivot_table(index=['시도', 'dong'], columns='학교급코드', values='학생수(계)', aggfunc='sum', fill_value=0).reset_index()
seoul_sch = df_seoul.pivot_table(index=['시도', 'dong'], columns='학교급코드', values='학교명', aggfunc='count', fill_value=0).reset_index()
# 각 칼럼을 재정의
seoul_std.rename(columns={2: '초등학생수', 3: '중학생수', 4: '고등학생수'}, inplace=True)
seoul_sch.rename(columns={2: '초등학교수', 3: '중학교수', 4: '고등학교수'}, inplace=True)
# 시도동을 기준으로 병합
seoul_merged = pd.merge(seoul_std, seoul_sch, on=['시도', 'dong'], how='outer').fillna(0)
학교급코드 (2=초등, 3=중, 4=고등) 기준으로 피벗 테이블을 두 개 생성합니다. 하나는 학생 수 합계, 다른 하나는 학교 수 카운트입니다. 두 테이블을 시도 + dong 기준으로 outer join 후 결측치를 0으로 채웁니다.
③ 경기도 데이터 로드 및 동 추출
# 경기 데이터 전처리
df_gg = pd.read_csv(f'{base_path}경기도경기부동산포털학교정보.csv', encoding='cp949')
# 학년이나 학생수가 칼럼에 있으면, 합산
df_gg['총학생수'] = df_gg[[c for c in df_gg.columns if '학년' in c and '학생수' in c]].sum(axis=1)
df_gg['dong'] = df_gg['학교주소'].apply(lambda x: re.search(r'\(([가-힣]+[동읍면])', str(x)).group(1) if re.search(r'\(([가-힣]+[동읍면])', str(x)) else None)
df_gg['시도'] = df_gg['학교주소'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
경기도 데이터는 학년별 학생 수가 컬럼으로 분리되어 있어, 학년 + 학생수가 동시에 포함된 컬럼들을 찾아 행 방향으로 합산해 총학생수를 생성합니다. 동 추출 방식은 서울과 동일합니다.
④ 경기도 피벗 테이블 생성
# 경기 피벗 테이블 생성
gg_std = df_gg.pivot_table(index=['시도', 'dong'], columns='학교구분', values='총학생수', aggfunc='sum', fill_value=0).reset_index()
gg_sch = df_gg.pivot_table(index=['시도', 'dong'], columns='학교구분', values='학교명', aggfunc='count', fill_value=0).reset_index()
gg_std.rename(columns={'초등학교': '초등학생수', '중학교': '중학생수', '고등학교': '고등학생수'}, inplace=True)
gg_sch.rename(columns={'초등학교': '초등학교수', '중학교': '중학교수', '고등학교': '고등학교수'}, inplace=True)
gg_merged = pd.merge(gg_std, gg_sch, on=['시도', 'dong'], how='outer').fillna(0)
서울과 동일한 구조이나, 경기도 원본의 학교구분 컬럼 값이 문자열('초등학교')이므로 컬럼명 변환 방식이 다릅니다.
⑤ 컬럼 통일 및 서울 + 경기 병합
# 컬럼 통일 및 병합
cols_std = ['초등학생수', '중학생수', '고등학생수']
cols_sch = ['초등학교수', '중학교수', '고등학교수']
for col in cols_std + cols_sch:
if col not in seoul_merged.columns: seoul_merged[col] = 0
if col not in gg_merged.columns: gg_merged[col] = 0
school_by_dong = pd.concat([seoul_merged, gg_merged]).groupby(['시도', 'dong'])[cols_std + cols_sch].sum().reset_index()
school_by_dong['총학생수'] = school_by_dong[cols_std].sum(axis=1)
school_by_dong['총학교수'] = school_by_dong[cols_sch].sum(axis=1)
두 데이터셋 간 컬럼 누락 가능성을 방어적으로 처리한 뒤, concat 후 groupby로 동별 최종 합산합니다. 초/중/고 학생 수와 학교 수를 합산한 총학생수, 총학교수 파생 컬럼도 추가합니다.
⑥ 이상 데이터 제거 및 저장
# 이상 데이터 제거
school_by_dong = school_by_dong[school_by_dong['시도'].notna() & (school_by_dong['시도'].astype(str) != '0')]
school_by_dong = school_by_dong[school_by_dong['dong'].notna()]
school_by_dong.to_csv(f'{base_path}school_by_dong.csv', index=False, encoding='utf-8-sig')
시도가 비어있거나 '0'으로 잘못 파싱된 행, dong이 추출되지 않은 행을 제거합니다. 최종 결과를 school_by_dong.csv로 저장합니다.
2. 아파트 실거래가 전처리 (서울 + 경기도)
① 서울 엑셀 로드 및 컬럼 재정의
df_apt_seoul = pd.read_excel(f'{base_path}아파트(매매)_실거래가_20260202002906.xlsx', skiprows=16)
seoul_data = pd.DataFrame({
'시군구동': df_apt_seoul.iloc[:, 1],
'아파트명': df_apt_seoul.iloc[:, 5],
'전용면적': df_apt_seoul.iloc[:, 6],
'거래금액': df_apt_seoul.iloc[:, 9]
})
국토부 실거래가 엑셀은 상단에 메타 정보 행이 포함되어 있어 skiprows=16으로 헤더 이전 행을 건너뜁니다. 원본 컬럼명이 복잡하거나 위치 기반으로만 식별 가능하므로 iloc으로 필요한 컬럼만 추출해 새 DataFrame을 구성합니다.
② 경기도 엑셀 로드 및 컬럼 재정의
df_apt_gg = pd.read_excel(f'{base_path}경기도 아파트(매매)_실거래가_20260202003251.xlsx', skiprows=12)
gg_data = pd.DataFrame({
'시군구동': df_apt_gg['시군구'],
'아파트명': df_apt_gg['단지명'],
'전용면적': df_apt_gg['전용면적(㎡)'],
'거래금액': df_apt_gg['거래금액(만원)']
})
경기도 파일은 헤더가 12행부터 시작하며, 서울과 달리 컬럼명이 명시되어 있어 컬럼명으로 직접 접근합니다. 두 지역의 컬럼 구조를 동일하게 맞추기 위해 시군구동, 아파트명, 전용면적, 거래금액으로 통일합니다.
③ 병합 및 평수·평당가격 계산
apt_data = pd.concat([seoul_data, gg_data], ignore_index=True)
apt_data['거래금액_숫자'] = apt_data['거래금액'].astype(str).str.replace(',', '').astype(float)
apt_data['전용면적_숫자'] = pd.to_numeric(apt_data['전용면적'], errors='coerce')
apt_data['평수'] = apt_data['전용면적_숫자'] / 3.3058
apt_data['평당가격'] = apt_data['거래금액_숫자'] / apt_data['평수']
서울과 경기도 데이터를 concat으로 합칩니다. 거래금액은 1,000 형태의 문자열로 저장되어 있어 콤마를 제거한 뒤 float으로 변환합니다. 전용면적(㎡)을 3.3058로 나눠 평수를 구하고, 거래금액을 평수로 나눠 평당가격을 산출합니다.
④ 시도 및 동 추출
apt_data['시도'] = apt_data['시군구동'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
apt_data['dong'] = apt_data['시군구동'].apply(
lambda x: re.search(r'([가-힣]+[동읍면])$', str(x).split()[-1]).group(1)
if len(str(x).split()) >= 3 and re.search(r'([가-힣]+[동읍면])$', str(x).split()[-1])
else None
)
시군구동 컬럼은 서울특별시 강남구 역삼동 형태입니다. 앞 두 단어를 정규식으로 추출해 시도를 만들고, 마지막 단어가 동/읍/면으로 끝나는 경우에만 dong으로 저장합니다. 주소 토큰이 3개 미만이면 None을 반환해 불완전한 데이터를 걸러냅니다.
⑤ 동별 집계 및 저장
apt_by_dong = apt_data.groupby(['시도', 'dong']).agg({
'거래금액_숫자': 'mean',
'평당가격': 'mean',
'아파트명': 'count'
}).reset_index()
apt_by_dong.columns = ['시도', 'dong', '평균아파트가격', '평당가격', '거래건수']
apt_by_dong = apt_by_dong[apt_by_dong['시도'].notna() & apt_by_dong['dong'].notna()]
apt_by_dong.to_csv(f'{base_path}apt_by_dong.csv', index=False, encoding='utf-8-sig')
동별로 거래금액과 평당가격의 평균을 집계하고, 아파트명 카운트로 거래 건수를 구합니다. 시도나 dong이 추출되지 않은 행을 제거한 뒤 apt_by_dong.csv로 저장합니다.
3. 학원 데이터 전처리 (서울 + 경기도)
① 서울 CSV 로드 및 동 추출
# 서울 학원 데이터 로드 및 행정동 추출
df_seoul_academy = pd.read_csv(f'{base_path}서울시 학원 교습소정보.csv', encoding='cp949')
df_seoul_academy['temp_addr'] = df_seoul_academy['도로명상세주소'].fillna(df_seoul_academy['도로명주소']).astype(str)
df_seoul_academy['dong'] = df_seoul_academy['temp_addr'].apply(lambda x: re.search(r'\(([가-힣]+[동읍면])', str(x)).group(1) if re.search(r'\(([가-힣]+[동읍면])', str(x)) else None)
df_seoul_academy['시도'] = df_seoul_academy['도로명주소'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
학교 데이터와 동일하게 도로명상세주소 결측 시 도로명주소로 대체하고, 괄호 안에서 동/읍/면으로 끝나는 행정동명을 정규식으로 추출합니다.
② 서울 피벗 테이블 생성
# 동별 과목별 학원 수 피벗 테이블 생성
seoul_pivot = df_seoul_academy.pivot_table(index=['시도', 'dong'], columns='교습과정명', values='학원명', aggfunc='count', fill_value=0).reset_index()
교습과정명을 컬럼으로 펼쳐 동별 × 과목별 학원 수 매트릭스를 만듭니다. 학원명 카운트를 값으로 사용하며, 해당 과목 학원이 없는 동은 0으로 채웁니다.
③ 경기도 멀티시트 엑셀 로드
# 경기도 학원 데이터 시트 병합
excel_file = pd.ExcelFile(f'{base_path}경기도교육청_경기도 학원 정보_20251231.xlsx')
df_gg_list = []
for sheet in excel_file.sheet_names:
df_sheet = pd.read_excel(excel_file, sheet_name=sheet, skiprows=4)
df_gg_list.append(df_sheet)
df_gg_academy = pd.concat(df_gg_list, ignore_index=True)
경기도 학원 데이터는 시트가 여러 개로 분리된 엑셀 파일입니다. pd.ExcelFile로 파일을 열어 시트 목록을 확인한 뒤, 반복문으로 모든 시트를 읽어 하나의 DataFrame으로 합칩니다. 각 시트 상단 4행은 메타 정보이므로 skiprows=4로 건너뜁니다.
④ 경기도 동 추출 및 피벗 테이블 생성
# 경기도 행정동 추출 및 피벗 테이블 생성
df_gg_academy['시도'] = df_gg_academy['주소'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
df_gg_academy['dong'] = df_gg_academy['주소'].apply(lambda x: re.search(r'\(([가-힣]+[동읍면])', str(x)).group(1) if re.search(r'\(([가-힣]+[동읍면])', str(x)) else None)
gg_pivot = df_gg_academy.pivot_table(index=['시도', 'dong'], columns='교습과정', values='학원명', aggfunc='count', fill_value=0).reset_index()
서울과 동 추출 방식은 동일하나, 컬럼명이 교습과정명 대신 교습과정으로 다릅니다. 피벗 테이블 구조 자체는 동일합니다.
⑤ 두 지역 컬럼 통일 및 병합
# 컬럼 통일 및 서울 경기 데이터 병합
all_courses = set(seoul_pivot.columns[2:]) | set(gg_pivot.columns[2:])
for course in all_courses:
if course not in seoul_pivot.columns: seoul_pivot[course] = 0
if course not in gg_pivot.columns: gg_pivot[course] = 0
academy_by_dong = pd.concat([seoul_pivot, gg_pivot]).groupby(['시도', 'dong']).sum().reset_index()
academy_by_dong = academy_by_dong[academy_by_dong['시도'].notna() & academy_by_dong['dong'].notna()]
서울과 경기도의 교습과정 종류가 완전히 일치하지 않을 수 있어, 합집합(|)으로 전체 과정 목록을 구한 뒤 한쪽에만 없는 컬럼을 0으로 채워 구조를 통일합니다. 이후 concat + groupby로 동별 최종 합산합니다.
⑥ 총학원수 파생 컬럼 추가 및 저장
# 총학원수 산출 및 최종 저장
academy_by_dong.to_csv(f'{base_path}academy_by_dong_detailed.csv', index=False, encoding='utf-8-sig')
academy_cols = [col for col in academy_by_dong.columns if col not in ['시도', 'dong']]
academy_by_dong['총학원수'] = academy_by_dong[academy_cols].sum(axis=1)
zero_academy = academy_by_dong[academy_by_dong['총학원수'] == 0]
print(f"학원이 없는 동: {len(zero_academy)}개")
과목별 학원 수를 행 방향으로 합산해 총학원수 컬럼을 추가합니다. 학원이 단 하나도 없는 동을 별도로 출력해 데이터 품질을 확인하고, 최종 결과를 academy_by_dong_detailed.csv로 저장합니다.
4. 상가 실거래가 전처리 (서울 + 경기도)
① 엑셀 로드 및 병합
# 서울과 경기 상가 실거래가 데이터 병합
df_sang_seoul = pd.read_excel(f'{base_path}서울시 상업업무용(매매)_실거래가_20260202003444.xlsx', skiprows=12)
df_sang_gg = pd.read_excel(f'{base_path}경기도 상업업무용(매매)_실거래가_20260202003459.xlsx', skiprows=12)
df_sang = pd.concat([df_sang_seoul, df_sang_gg])
아파트와 달리 서울/경기 파일 모두 skiprows=12로 동일합니다. 두 파일을 바로 concat으로 합쳐 이후 처리를 한 번에 진행합니다.
② 컬럼 재정의
# 필요한 컬럼 추출
sang_data = pd.DataFrame({'시군구동': df_sang.iloc[:, 1], '면적': df_sang.iloc[:, 8], '거래금액': df_sang.iloc[:, 9]})
아파트 데이터와 마찬가지로 원본 컬럼 위치 기반으로 필요한 컬럼만 추출합니다. 상가는 아파트명 대신 면적을 사용하며, 나중에 거래 건수 집계에 활용합니다.
③ 평수 및 평당가격 계산
# 평수 및 평당가격 계산
sang_data['거래금액_숫자'] = pd.to_numeric(sang_data['거래금액'].astype(str).str.replace(',', ''), errors='coerce')
sang_data['면적_숫자'] = pd.to_numeric(sang_data['면적'], errors='coerce')
sang_data['평수'] = sang_data['면적_숫자'] / 3.3058
sang_data['평당가격'] = sang_data['거래금액_숫자'] / sang_data['평수']
아파트와 동일한 방식으로 계산합니다. 다만 아파트는 astype(float)으로 변환했던 반면, 상가는 pd.to_numeric(..., errors='coerce')를 사용해 변환 불가 값을 NaN으로 처리하는 방어 로직이 추가되어 있습니다.
④ 시도 및 동 추출
# 행정동 추출
sang_data['시도'] = sang_data['시군구동'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
sang_data['dong'] = sang_data['시군구동'].apply(lambda x: re.search(r'([가-힣]+[동읍면])$', str(x).split()[-1]).group(1) if len(str(x).split()) >= 3 and re.search(r'([가-힣]+[동읍면])$', str(x).split()[-1]) else None)
아파트와 완전히 동일한 방식입니다. 주소 마지막 단어가 동/읍/면으로 끝나는 경우에만 dong으로 저장하고, 토큰이 3개 미만이면 None을 반환합니다.
⑤ 동별 집계 및 저장
# 평균 집계 및 저장
sang_by_dong = sang_data.groupby(['시도', 'dong']).agg({'거래금액_숫자': 'mean', '평당가격': 'mean', '면적': 'count'}).reset_index()
sang_by_dong.columns = ['시도', 'dong', '평균상가가격', '상가평당가격', '상가거래건수']
sang_by_dong = sang_by_dong[sang_by_dong['시도'].notna() & sang_by_dong['dong'].notna()]
sang_by_dong.to_csv(f'{base_path}sanga_by_dong.csv', index=False, encoding='utf-8-sig')
동별로 거래금액과 평당가격의 평균을 집계하고, 면적 카운트로 상가 거래 건수를 구합니다. 이상 데이터를 제거한 뒤 sanga_by_dong.csv로 저장합니다.
5. 인구 데이터 전처리
① CSV 로드 및 시도·동 추출
# 행정구역명에서 시도 및 행정동 추출
pop = pd.read_csv(f'{base_path}population_seoul_gyeonggi_dong.csv')
pop['시도'] = pop['행정구역명'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
pop['dong'] = pop['행정구역명'].apply(lambda x: re.sub(r'[0-9\.·]+[가]*동$', '동', str(x).split()[-1]) if str(x).split()[-1].endswith(('동', '읍', '면')) else None)
다른 데이터셋과 달리 주소가 아닌 행정구역명 컬럼에서 추출합니다. 시도는 앞 두 단어를 정규식으로 추출하는 방식으로 동일하지만, dong 추출 방식이 다릅니다. 가-힣 뿐만 아니라 숫자·점·가운뎃점(·)이 섞인 동명(예: 1.2동, 가·나동)을 re.sub으로 정규화해 동으로 통일합니다.
② 연령대 구간 합산
# 5세 단위 연령대를 10세 단위로 병합
pop['10세미만'] = pop['4세이하'] + pop['5세이상~9세이하']
pop['10대'] = pop['10세이상~14세이하'] + pop['15세이상~19세이하']
pop['20대'] = pop['20세이상~24세이하'] + pop['25세이상~29세이하']
pop['30대'] = pop['30세이상~34세이하'] + pop['35세이상~39세이하']
pop['40대'] = pop['40세이상~44세이하'] + pop['45세이상~49세이하']
pop['50대'] = pop['50세이상~54세이하'] + pop['55세이상~59세이하']
pop['60대'] = pop['60세이상~64세이하'] + pop['65세이상~69세이하']
pop['70대'] = pop['70세이상~74세이하'] + pop['75세이상~79세이하']
pop['80대'] = pop['80세이상~84세이하'] + pop['85세이상~89세이하']
pop['90대'] = pop['90세이상~94세이하'] + pop['95세이상~99세이하']
pop['100세이상'] = pop['100세이상']
원본 데이터는 5세 단위로 세분화된 컬럼 구조입니다. 이를 10세 단위 연령대로 묶어 분석에 용이한 형태로 변환합니다. 이 연령대 데이터는 이후 클러스터링에서 유소년 인구 비율 등의 피처로 활용됩니다.
③ 동별 집계 (연령대 + 기본 지표)
# 컬럼 성격에 따른 동별 데이터 집계
age_groups = ['10세미만', '10대', '20대', '30대', '40대', '50대', '60대', '70대', '80대', '90대', '100세이상']
base_cols = ['노령화지수', '인구밀도', '평균나이', '총인구', '총주택(거처)수', '아파트']
agg_dict = {}
for col in base_cols:
agg_dict[col] = 'mean' if col in ['노령화지수', '인구밀도', '평균나이'] else 'sum'
for col in age_groups:
agg_dict[col] = 'sum'
pop_by_dong = pop.groupby(['시도', 'dong']).agg(agg_dict).reset_index()
집계 방식을 컬럼 성격에 따라 구분합니다. 노령화지수, 인구밀도, 평균나이처럼 비율·평균 성격의 지표는 mean으로, 총인구, 총주택수, 연령대별 인구처럼 개수 성격의 지표는 sum으로 집계합니다. 이 구분이 중요한 이유는 동일한 groupby에서 잘못된 집계 함수를 사용하면 값이 과대 계산될 수 있기 때문입니다.
④ 주요 지표 재집계 및 저장
# 주요 지표 재추출 및 최종 병합용 집계
df_pop = pd.read_csv(f'{base_path}population_seoul_gyeonggi_dong.csv')
selected_cols = ['행정구역명', '노령화지수', '인구밀도', '평균나이', '총인구', '총주택(거처)수']
df_pop_selected = df_pop[selected_cols].copy()
df_pop_selected['시도'] = df_pop_selected['행정구역명'].astype(str).str.extract(r'^([가-힣]+\s+[가-힣]+)')[0]
df_pop_selected['dong'] = df_pop_selected['행정구역명'].apply(lambda x: re.sub(r'[0-9\.·]+[가]*동$', '동', str(x).split()[-1]) if str(x).split()[-1].endswith(('동', '읍', '면')) else None)
pop_by_dong = df_pop_selected.groupby(['시도', 'dong']).agg({'노령화지수': 'mean', '인구밀도': 'mean', '평균나이': 'mean', '총인구': 'sum', '총주택(거처)수': 'sum'}).reset_index()
연령대 구간 집계와 별도로 최종 병합에 사용할 핵심 지표만 추려 다시 집계합니다. 원본을 새로 로드해 필요한 컬럼만 selected_cols로 선택하고, 동일한 방식으로 동별 집계를 수행합니다. 이 결과가 이후 final_dong 병합 시 인구 정보로 사용됩니다.
6. 최종 통합
① 저장된 CSV 파일 로드
# 저장된 데이터 로드
school_by_dong = pd.read_csv(f'{base_path}school_by_dong.csv')
apt_by_dong = pd.read_csv(f'{base_path}apt_by_dong.csv')
academy_by_dong = pd.read_csv(f'{base_path}academy_by_dong.csv')
sanga_by_dong = pd.read_csv(f'{base_path}sanga_by_dong.csv')
앞서 각 단계에서 저장해둔 CSV 파일을 다시 불러옵니다. pop_by_dong은 파일로 저장하지 않고 메모리에 남아있는 상태를 그대로 사용합니다.
② 데이터 통합 (Left Join)
# 데이터 병합
final_dong = academy_by_dong.copy()
final_dong = final_dong.merge(apt_by_dong, on=['시도', 'dong'], how='left')
final_dong = final_dong.merge(school_by_dong, on=['시도', 'dong'], how='left')
final_dong = final_dong.merge(pop_by_dong, on=['시도', 'dong'], how='left')
final_dong = final_dong.merge(sanga_by_dong, on=['시도', 'dong'], how='left')
academy_by_dong을 베이스로 설정하는 이유는 학원 데이터가 분석의 핵심 지표이기 때문입니다. 나머지 데이터를 모두 left 방식으로 조인함으로써, 학원 데이터에 존재하는 동은 반드시 유지되고 다른 데이터에만 있는 동은 제외됩니다. 병합 기준은 시도 + dong 두 컬럼의 조합으로, 서울과 경기도에 동명이 겹치는 경우(예: 역삼동)를 시도로 구분합니다.
③ 결측치 처리 및 저장
# 결측치 처리 및 최종 저장
final_dong = final_dong.fillna(0)
final_dong.to_csv(f'{base_path}final_dong.csv', index=False, encoding='utf-8-sig')
print(f"통합 완료: {len(final_dong)}개 동")
print(f"총 컬럼: {len(final_dong.columns)}개")
left join 특성상 학원 데이터에는 있지만 아파트·학교·인구·상가 데이터에는 없는 동이 존재할 수 있습니다. 이런 경우 발생하는 NaN을 fillna(0)으로 일괄 처리합니다. 이는 해당 동에 거래 이력이나 학교가 없는 것으로 간주하는 방식으로, 클러스터링 알고리즘이 결측치 없이 모든 피처를 사용할 수 있도록 보장합니다.
여기까지 진행하면 데이터 전처리는 완료입니다. 주로 도로명주소나 시군 법정동 명의 이름을 통일시키는 텍스트 전처리가 주요 요인이었습니다. 이후에는 피벗테이블을 통해서 데이터프레임을 만들고 병합하는 과정을 가졌습니다. 다음 포스트에서는 세부적인 분석 프로세스에 대해서 작성하겠습니다.
'Data Analyst Project' 카테고리의 다른 글
| [어플리케이션 개발] 3. 학원 입지 분석 대시보드 앱 개발 - 아키텍처 및 결과물 (0) | 2026.02.21 |
|---|---|
| [어플리케이션 개발] 2. 학원 입지 분석 대시보드 앱 개발 - 분석지표 생성 및 클러스터링 (0) | 2026.02.21 |
| [이커머스] 가중치 RFM 기반 고객 세분화 및 Cohort 분석을 통한 리텐션 증대 전략 (0) | 2026.01.07 |
| [대구 빅데이터 분석 경진대회] 클라우드 데이터베이스 구축과 데이터 적재 (2) | 2025.08.04 |
| [대구 빅데이터 분석 경진대회] 프로젝트 계획 및 가설설정과 데이터 수집 (1) | 2025.08.03 |