11  실습: 데이터 선택과 필터링

전국 학원 교습소 정보 데이터셋은 대한민국 전역의 학원 및 교습소의 기초 정보(명칭, 주소, 교습분야, 정원 등)를 담고 있는 실제 행정 데이터입니다. 이 데이터를 활용해 데이터 선택(loc, iloc), Boolean Indexing, pd.col(), query(), 그리고 MultiIndex의 활용법을 차례로 실습합니다.

11.1 데이터 준비 및 탐색

먼저 필요한 라이브러리를 임포트하고 한글 폰트를 설정한 후 데이터를 읽어옵니다. 이 데이터는 한국어 인코딩(CP949)으로 저장되어 있으므로 encoding="cp949"를 지정합니다. 또한 C++ 기반 초고속 파싱과 메모리 효율성을 위해 engine="pyarrow"dtype_backend="pyarrow" 옵션을 활용해 PyArrow 백엔드로 데이터를 읽어옵니다.

import pandas as pd
import numpy as np
import platform
import matplotlib.pyplot as plt

# 운영체제별 한글 폰트 설정
if platform.system() == "Darwin":
    plt.rcParams["font.family"] = "Apple SD Gothic Neo"
elif platform.system() == "Windows":
    plt.rcParams["font.family"] = "Malgun Gothic"
else:
    plt.rcParams["font.family"] = "NanumGothic"

plt.rcParams["axes.unicode_minus"] = False

# 데이터 로드 (PyArrow 엔진 및 백엔드 적용)
df = pd.read_csv(
    "../data/학원교습소정보(20251231).csv",
    encoding="cp949",
    engine="pyarrow",
    dtype_backend="pyarrow",
)

# 데이터 크기 확인
print("데이터 크기:", df.shape)

# 데이터 컬럼과 상위 데이터 확인
print("컬럼 목록:", df.columns.tolist())

# PyArrow 데이터 타입 샘플 확인
print("데이터 타입 샘플:\n", df.dtypes.head())
데이터 크기: (137562, 25)
컬럼 목록: ['시도교육청코드', '시도교육청명', '행정구역명', '학원교습소명', '학원지정번호', '학원명', '개설일자', '등록일자', '등록상태명', '휴원시작일자', '휴원종료일자', '정원합계', '일시수용능력인원합계', '분야명', '교습계열명', '교습과정목록명', '교습과정명', '인당수강료', '수강료공개여부', '기숙사학원여부', '도로명주소', '도로명상세주소', '도로명우편번호', '전화번호', '수정일자']
데이터 타입 샘플:
 시도교육청코드    string[pyarrow]
시도교육청명     string[pyarrow]
행정구역명      string[pyarrow]
학원교습소명     string[pyarrow]
학원지정번호      int64[pyarrow]
dtype: object

11.2 loc와 iloc를 활용한 데이터 선택

loc는 레이블(이름) 기반의 데이터 선택법이며, iloc는 정수 위치 기반의 데이터 선택법입니다. 데이터프레임의 기본 상태에서는 인덱스가 0부터 시작하는 연속된 정수이므로 loc와 iloc의 행 선택이 비슷하게 작동하는 것처럼 보일 수 있습니다. 하지만 둘 사이에는 근본적인 차이가 있습니다.

11.2.1 기본 위치 기반 선택 (iloc)

정수 인덱스를 사용하여 특정 위치의 행과 열을 가져옵니다.

# 첫 번째 행의 모든 열 가져오기
first_row = df.iloc[0, :]
print(first_row.head(10))

# 처음 5개 행과 처음 3개 열 가져오기
# iloc의 슬라이싱은 끝점(5)을 제외하여 0부터 4까지 5개 행을 반환합니다.
subset_iloc = df.iloc[0:5, 0:3]
print(subset_iloc)
시도교육청코드         B10
시도교육청명     서울특별시교육청
행정구역명           강남구
             ...   
등록일자       19950804
등록상태명            개원
휴원시작일자             
Name: 0, Length: 10, dtype: object
  시도교육청코드    시도교육청명 행정구역명
0     B10  서울특별시교육청   강남구
1     B10  서울특별시교육청   강남구
2     B10  서울특별시교육청   강남구
3     B10  서울특별시교육청   강남구
4     B10  서울특별시교육청   강남구

11.2.2 이름 기반 선택 (loc)

데이터의 인덱스를 특정 의미 있는 열로 변경한 후, 레이블을 이용해 데이터를 선택해 봅니다. 여기서는 학원지정번호를 인덱스로 지정하여 실습해 봅니다.

# 학원지정번호를 인덱스로 설정
df_indexed = df.set_index("학원지정번호")

# 특정 학원지정번호를 가진 행의 학원명과 도로명주소 가져오기
# loc의 행 선택자로 학원지정번호(정수 형태 레이블)를 지정합니다.
academy_info = df_indexed.loc[
    2531, ["학원명", "도로명주소"]
]
print(academy_info)
                 학원명                   도로명주소
학원지정번호                                      
2531    (주)멘토르수학전문학원    서울특별시 강남구 남부순환로 2927
2531         유진컴퓨터학원  충청남도 천안시 서북구 두정중1길 6-9

11.2.3 loc와 iloc의 슬라이싱 차이 비교

loc와 iloc의 중요한 차이 중 하나는 슬라이싱 범위의 끝점 포함 여부입니다.

# iloc: 끝점 제외 (0부터 4까지 5개 행 선택)
print("iloc 슬라이싱 개수:", len(df.iloc[0:5]))

# loc: 끝점 포함 (인덱스 레이블 0부터 5까지 6개 행 선택)
print("loc 슬라이싱 개수:", len(df.loc[0:5]))
iloc 슬라이싱 개수: 5
loc 슬라이싱 개수: 6

11.3 Boolean Indexing을 사용한 필터링

특정 조건을 만족하는 행들만 선택하는 강력한 도구입니다.

11.3.1 단일 조건 필터링

정원합계가 300명 이상인 대형 학원만 필터링합니다.

# 정원합계가 300명 이상인지 여부를 담은 Boolean Series 생성
is_large = df["정원합계"] >= 300

# 필터링 적용 및 결과 행 수 확인
large_academies = df[is_large]
print("대형 학원 수:", large_academies.shape[0])
대형 학원 수: 13395

11.3.2 복합 조건 필터링

두 개 이상의 조건을 조합할 때는 AND(&), OR(|), NOT(~) 연산자를 사용합니다. 각 조건은 반드시 괄호로 감싸야 연산자 우선순위 오류를 방지할 수 있습니다. 행정구역명이 강남구이면서 분야명이 입시.검정 및 보습인 학원을 필터링합니다.

# 복합 조건 필터링
gangnam_ipsi = df[
    (df["행정구역명"] == "강남구")
    & (df["분야명"] == "입시.검정 및 보습")
]
print("강남구 입시 학원 수:", gangnam_ipsi.shape[0])
print(gangnam_ipsi[["학원명", "교습과정명"]].head())
강남구 입시 학원 수: 1917
                      학원명  교습과정명
0            (주)멘토르수학전문학원     보습
1     (주)산에듀김영준국어논술전문별관학원  보습·논술
2  (주)산에듀김영준국어논술전문프리미엄관학원  보습·논술
3       (주)산에듀김영준국어논술전문학원  보습·논술
8                 101프렙학원  보습·논술

11.3.3 isin()을 활용한 다중 매칭

여러 개의 OR 조건을 연결해야 할 때 isin() 메서드를 사용하면 코드가 한결 간결해집니다. 행정구역이 강남 3구(강남구, 서초구, 송파구)인 학원을 필터링합니다.

gangnam_3gu = df[
    df["행정구역명"].isin(["강남구", "서초구", "송파구"])
]
print("강남 3구 학원 수:", gangnam_3gu.shape[0])
print(gangnam_3gu["행정구역명"].value_counts())
강남 3구 학원 수: 7230
행정구역명
강남구    3453
송파구    1945
서초구    1832
Name: count, dtype: int64[pyarrow]

11.4 pd.col()을 활용한 선언형 필터링

pandas 3.x부터 도입된 pd.col()은 함수형 프로그래밍 스타일의 선언적 코드를 지원합니다. 람다 식이나 복잡한 데이터프레임 참조 없이 컬럼을 쉽게 지칭할 수 있습니다.

# pd.col()을 사용하여 정원합계가 200명 이상이면서 일시수용능력인원합계가 150명 이상인 학원 필터링
condition_col = (pd.col("정원합계") >= 200) & (
    pd.col("일시수용능력인원합계") >= 150
)
filtered_col = df[condition_col]
print("pd.col 필터링 결과 수:", filtered_col.shape[0])
pd.col 필터링 결과 수: 2886

11.5 query() 메서드를 활용한 데이터 조회

query() 메서드는 SQL의 WHERE 절처럼 문자열 형태로 직관적인 필터링 조건을 작성할 수 있게 해줍니다.

11.5.1 기본 query 사용법

# 행정구역명이 서초구이면서 정원합계가 100명 이상인 학원 조회
query_result = df.query(
    "행정구역명 == '서초구' and 정원합계 >= 100"
)
print("서초구 100명 이상 학원 수:", query_result.shape[0])
서초구 100명 이상 학원 수: 666

11.5.2 외부 변수 참조 (@ 기호)

query() 문자열 내부에서 Python 프로그램의 외부 변수를 참조할 때는 변수명 앞에 @ 기호를 붙입니다.

# 외부 변수 정의
limit_capacity = 250
target_area = "수원시"

# query 안에서 외부 변수 참조
suwon_large = df.query(
    "행정구역명 == @target_area and 정원합계 >= @limit_capacity"
)
print("수원시 대형 학원 수:", suwon_large.shape[0])
수원시 대형 학원 수: 595

11.5.3 문자열 검색 조건 활용

query() 내에서 문자열 메서드인 str.contains() 등을 직접적으로 결합하여 검색 조건을 다룰 수 있습니다. 학원명에 피아노가 포함되고 행정구역이 강남구인 학원을 찾아봅니다.

# query 안에서 문자열 메서드 활용 (engine="python" 지정 권장)
piano_gangnam = df.query(
    "행정구역명 == '강남구' and 학원명.str.contains('피아노')",
    engine="python",
)
print("강남구 피아노 학원 수:", piano_gangnam.shape[0])
print(piano_gangnam["학원명"].head(3))
강남구 피아노 학원 수: 77
5      (주)엠제이재즈피아노실용음악학원
21              CIM피아노학원
276            김윤진피아노교습소
Name: 학원명, dtype: string[pyarrow]

11.6 MultiIndex와 데이터 선택

여러 개의 열을 결합하여 계층적 구조를 가진 MultiIndex를 생성하고, 이에 맞게 데이터를 조회하는 방법입니다.

11.6.1 MultiIndex 생성 및 집계

시도교육청명과 행정구역명을 기준으로 그룹화하여 정원합계의 평균을 집계한 요약 테이블을 생성해 봅니다. 이 과정에서 MultiIndex가 자연스럽게 생성됩니다.

# 그룹화 및 평균 정원 집계
grouped_summary = df.groupby(
    ["시도교육청명", "행정구역명"]
)[["정원합계", "일시수용능력인원합계"]].mean()
print(grouped_summary.head(10))
                        정원합계  일시수용능력인원합계
시도교육청명     행정구역명                        
강원특별자치도교육청 강릉시    144.483389   39.147841
           고성군     78.294118   40.882353
           동해시    105.338983   40.079096
...                      ...         ...
           영월군          72.4       29.66
           원주시    415.607467   48.804238
           인제군     80.526316   19.447368

[10 rows x 2 columns]

11.6.2 xs() 메서드를 활용한 다층 데이터 선택

MultiIndex 구조에서 특정 계층(Level)의 특정 값을 기준으로 데이터를 선택할 때 xs() 메서드가 매우 직관적이고 강력합니다.

# 첫 번째 인덱스 레벨(시도교육청명)에서 '서울특별시교육청'에 해당하는 행만 선택
seoul_data = grouped_summary.xs(
    "서울특별시교육청", level="시도교육청명"
)
print(seoul_data.head())

# 두 번째 인덱스 레벨(행정구역명)에서 '서구'에 해당하는 행만 선택
seogu_data = grouped_summary.xs("서구", level="행정구역명")
print(seogu_data.head())
              정원합계  일시수용능력인원합계
행정구역명                         
강남구    1206.668404   87.546771
강동구     139.227566   49.394113
강북구     186.231771   43.455729
강서구      96.150875   36.809767
관악구     419.850275   62.864011
                정원합계  일시수용능력인원합계
시도교육청명                          
광주광역시교육청  151.236383   44.571895
대구광역시교육청  107.862205   31.547244
대전광역시교육청  164.035884   45.566012
부산광역시교육청   78.320346   50.402597
인천광역시교육청   331.79269   62.306111

11.6.3 reset_index()를 통한 평탄화

MultiIndex 구조는 시각화나 파일 저장 시 불편할 수 있으므로, reset_index()를 사용해 일반 데이터프레임 형태로 되돌리는 방법입니다.

# MultiIndex 평탄화하여 일반 컬럼으로 변환
flat_summary = grouped_summary.reset_index()
print(flat_summary.head())
       시도교육청명 행정구역명          정원합계   일시수용능력인원합계
0  강원특별자치도교육청   강릉시    144.483389    39.147841
1  강원특별자치도교육청   고성군     78.294118    40.882353
2  강원특별자치도교육청   동해시    105.338983    40.079096
3  강원특별자치도교육청   삼척시     86.517647    45.470588
4  강원특별자치도교육청   속초시  61431.852761  61406.04908