공공데이터 수집부터 시각화까지, DuckDB로 노원구 아파트 대시보드 만들기

공공데이터를 자동 수집해 DuckDB 기반 부동산 대시보드 만들기 🏠

소개

공공데이터포털이나 각종 데이터 서비스를 살펴보면 활용할 수 있는 데이터가 정말 많습니다.

하지만 실제 분석을 시작하려고 하면 곧바로 몇 가지 현실적인 문제를 만나게 됩니다.

  • 데이터마다 제공 형식이 다릅니다.

  • 월별로 계속 새로운 데이터가 추가됩니다.

  • 같은 데이터를 다시 수집하면 중복이 생길 수 있습니다.

  • 수집은 성공했지만 일부 데이터가 누락될 수도 있습니다.

  • 데이터를 모으는 것과 분석 가능한 상태로 만드는 것은 별개의 작업입니다.

이번에는 이러한 문제를 해결하기 위해 국토교통부 아파트 실거래 데이터와 KB부동산 시장지표를 수집하고, DuckDB에 저장한 뒤 하나의 대시보드로 시각화해봤습니다.

분석 대상은 서울시 노원구 아파트로 정했습니다.

최종 대시보드에는 다음 데이터가 포함됐습니다.

  • 노원구 아파트 매매 실거래

  • 노원구 아파트 전월세 실거래

  • KB 매매가격지수

  • KB 전세가격지수

  • KB 전세가율

  • 평형대·지역·연식별 거래 현황

이번 작업의 핵심은 단순히 차트를 만드는 것이 아니었습니다.

공공데이터 수집부터 저장, 검증, 분석, 시각화까지 반복 실행할 수 있는 하나의 자동화 흐름을 만드는 것이 목표였습니다.

📊 최종 결과물: 노원구 아파트 실거래가·KB 시장지표 대시보드
실거래 데이터 기간은 2025년 1월부터 2026년 6월까지이며, 가격은 이상치의 영향을 줄이기 위해 평균보다 중앙값을 우선 사용했습니다.


진행 방법

1. 서로 다른 부동산 데이터를 한곳에 모으기

이번 분석에는 성격이 다른 두 종류의 데이터가 필요했습니다.

국토교통부 실거래 데이터

실제 계약을 기준으로 다음 정보를 확인할 수 있습니다.

  • 계약 연월

  • 아파트명

  • 법정동

  • 전용면적

  • 거래금액

  • 보증금과 월세

  • 건축 연도

  • 층수

KB부동산 시장지표

개별 거래가 아닌 전체 시장의 흐름을 확인하기 위해 활용했습니다.

  • 매매가격지수

  • 전세가격지수

  • 전세가율

  • 서울과 노원구의 장기 추이

  • 지역별 최신 시장지표

실거래 데이터만 보면 개별 거래의 가격과 건수는 알 수 있지만, 시장 전체가 어느 방향으로 움직이고 있는지는 파악하기 어렵습니다.

반대로 가격지수만 보면 실제로 어느 지역과 평형에서 거래가 활발했는지 확인하기 어렵습니다.

그래서 두 데이터를 함께 저장하고 비교하기로 했습니다.

국토교통부 실거래 데이터
        ↓
실제 거래량·거래금액 분석
        ↓
        DuckDB
        ↑
시장 흐름·가격지수 분석
        ↑
KB부동산 시장지표

2. 수집 과정을 자동화하기

데이터를 한 번 내려받아 분석하는 것은 어렵지 않습니다.

문제는 매달 새로운 데이터가 생길 때마다 다음 작업을 반복해야 한다는 점이었습니다.

  1. 조회 기간을 변경합니다.

  2. API를 다시 호출합니다.

  3. 여러 페이지를 반복 조회합니다.

  4. 응답 데이터를 파일로 저장합니다.

  5. 컬럼 형식을 통일합니다.

  6. 기존 데이터와 중복되지 않도록 적재합니다.

  7. 누락된 데이터가 없는지 확인합니다.

  8. 대시보드를 다시 생성합니다.

이 과정을 수작업으로 처리하면 시간이 많이 들고, 어느 단계에서 실수가 발생했는지 찾기도 어렵습니다.

그래서 전체 작업을 다음과 같은 자동화 파이프라인으로 구성했습니다.

수집 대상 기간 확인
        ↓
공공데이터 API 호출
        ↓
원본 응답 저장
        ↓
데이터 정제·타입 변환
        ↓
DuckDB 임시 테이블 적재
        ↓
중복 제거 후 본 테이블 반영
        ↓
건수·누락·이상값 검증
        ↓
분석용 SQL 실행
        ↓
HTML 대시보드 생성

수집 프로그램을 다시 실행하더라도 이미 처리한 데이터는 중복 저장되지 않도록 구성했습니다.


3. DuckDB에 분석하기 좋은 구조로 저장하기

DuckDB를 선택한 가장 큰 이유는 별도의 데이터베이스 서버를 운영하지 않고도 대량 데이터를 SQL로 다룰 수 있기 때문입니다.

하나의 파일에 데이터를 저장하면서도 다음과 같은 작업을 처리할 수 있었습니다.

  • CSV와 Parquet 파일 읽기

  • API 수집 결과 저장

  • 테이블 간 조인

  • 월별·지역별 집계

  • 중복 제거

  • 중앙값 계산

  • 데이터 품질 검증

  • 시각화용 데이터 추출

테이블은 크게 세 계층으로 구분했습니다.

원본 데이터 영역

API에서 받은 값을 가능한 한 그대로 저장했습니다.

CREATE TABLE IF NOT EXISTS raw_apt_trade (
    collected_at TIMESTAMP,
    request_year_month VARCHAR,
    page_no INTEGER,
    raw_data JSON
);

원본을 보관하면 정제 결과에 문제가 생겼을 때 API 응답까지 거슬러 올라가 원인을 확인할 수 있습니다.

정제 데이터 영역

분석에 필요한 컬럼을 추출하고 타입을 통일했습니다.

CREATE TABLE IF NOT EXISTS apt_trade (
    contract_date DATE,
    district_name VARCHAR,
    legal_dong VARCHAR,
    apartment_name VARCHAR,
    exclusive_area DOUBLE,
    deal_amount BIGINT,
    deposit_amount BIGINT,
    monthly_rent BIGINT,
    build_year INTEGER,
    floor INTEGER,
    trade_type VARCHAR,
    collected_at TIMESTAMP
);

시장지표 영역

KB 매매·전세지수와 전세가율은 실거래 데이터와 분리해 저장했습니다.

CREATE TABLE IF NOT EXISTS kb_market_index (
    base_month DATE,
    region_name VARCHAR,
    sale_price_index DOUBLE,
    jeonse_price_index DOUBLE,
    jeonse_ratio DOUBLE
);

이렇게 원본, 정제 데이터, 시장지표를 분리하니 데이터 수정과 검증이 쉬워졌습니다.


4. 같은 데이터를 다시 수집해도 안전하게 만들기

자동화 파이프라인에서는 같은 기간을 여러 번 실행할 수 있습니다.

예를 들어 API 오류로 일부 페이지만 저장됐거나, 이전 데이터가 정정되어 다시 제공될 수도 있습니다.

따라서 수집 프로그램은 재실행해도 결과가 달라지지 않는 구조가 필요했습니다.

INSERT INTO apt_trade
SELECT *
FROM apt_trade_staging
WHERE NOT EXISTS (
    SELECT 1
    FROM apt_trade AS saved
    WHERE saved.contract_date = apt_trade_staging.contract_date
      AND saved.legal_dong = apt_trade_staging.legal_dong
      AND saved.apartment_name = apt_trade_staging.apartment_name
      AND saved.exclusive_area = apt_trade_staging.exclusive_area
      AND saved.floor = apt_trade_staging.floor
      AND saved.deal_amount = apt_trade_staging.deal_amount
);

실제 데이터에서는 하나의 컬럼만으로 거래를 식별하기 어렵기 때문에 계약일, 지역, 아파트명, 면적, 층, 거래금액 등을 조합해 중복 여부를 확인했습니다.

취소 거래나 정정 거래가 포함된 경우에는 별도 상태값을 두거나 최신 수집 데이터를 기준으로 갱신하는 방식도 고려했습니다.


5. 저장된 데이터가 정상인지 검증하기

이번 작업에서 가장 신경 쓴 부분은 데이터 수집보다 수집 결과 검증이었습니다.

데이터가 DuckDB에 저장됐다는 사실만으로는 정상적으로 수집됐다고 볼 수 없기 때문입니다.

월별 수집 건수 확인

SELECT
    DATE_TRUNC('month', contract_date) AS contract_month,
    trade_type,
    COUNT(*) AS trade_count
FROM apt_trade
WHERE district_name = '노원구'
GROUP BY contract_month, trade_type
ORDER BY contract_month, trade_type;

특정 월의 거래량이 갑자기 0건이거나 지나치게 적다면 API 호출 누락이나 페이지 처리 오류를 의심할 수 있습니다.

중복 데이터 확인

SELECT
    contract_date,
    legal_dong,
    apartment_name,
    exclusive_area,
    floor,
    deal_amount,
    COUNT(*) AS duplicate_count
FROM apt_trade
GROUP BY
    contract_date,
    legal_dong,
    apartment_name,
    exclusive_area,
    floor,
    deal_amount
HAVING COUNT(*) > 1;

필수값 누락 확인

SELECT *
FROM apt_trade
WHERE contract_date IS NULL
   OR apartment_name IS NULL
   OR exclusive_area IS NULL
   OR trade_type IS NULL;

비정상 값 확인

SELECT *
FROM apt_trade
WHERE exclusive_area <= 0
   OR deal_amount < 0
   OR deposit_amount < 0
   OR monthly_rent < 0;

검증 결과는 별도의 로그 테이블에 남겼습니다.

CREATE TABLE IF NOT EXISTS collection_log (
    data_type VARCHAR,
    target_month DATE,
    collected_count BIGINT,
    stored_count BIGINT,
    duplicate_count BIGINT,
    invalid_count BIGINT,
    status VARCHAR,
    checked_at TIMESTAMP
);

덕분에 어떤 월의 데이터가 정상 처리됐고, 어느 시점에 오류가 발생했는지 쉽게 확인할 수 있었습니다.


6. 평균 대신 중앙값을 활용하기

부동산 실거래 데이터에는 다양한 면적과 가격대의 아파트가 함께 포함됩니다.

고가 아파트 몇 건이나 비정상적으로 낮은 거래가 포함되면 평균 가격이 크게 달라질 수 있습니다.

그래서 이번 대시보드에서는 평균보다 중앙값을 주요 지표로 사용했습니다.

SELECT
    MEDIAN(deal_amount) / 10000.0 AS median_price_100m
FROM apt_trade
WHERE district_name = '노원구'
  AND trade_type = '매매';

다만 전체 매매가 중앙값에는 서로 다른 전용면적의 거래가 모두 포함되어 있습니다.

따라서 대시보드에서도 전체 중앙값뿐 아니라 다음 기준을 함께 확인할 수 있도록 구성했습니다.

  • 월별

  • 평형대별

  • 읍면동별

  • 건축 연식별


7. DuckDB 조회 결과를 대시보드로 시각화하기

최종 결과물은 별도의 서버 없이 열어볼 수 있는 HTML 대시보드로 만들었습니다.

대시보드는 크게 세 부분으로 나눴습니다.

핵심 요약

  • 매매 거래 건수

  • 전월세 거래 건수

  • 데이터 수집 기간

  • 매매가격 중앙값

  • 전세보증금 중앙값

  • KB 매매가격지수

  • KB 전세가격지수

  • KB 전세가율

실거래 분석

  • 월별 매매·전월세 거래 건수

  • 월별 매매가격 중앙값

  • 평형대별 가격과 거래량

  • 읍면동별 거래량

  • 건축 연식별 거래량과 가격

KB 시장지표 분석

  • 서울과 노원구 매매가격지수 비교

  • 서울과 노원구 전세가격지수 비교

  • 서울과 노원구 전세가율 비교

  • 실거래 중앙값과 KB 지수 흐름 비교

  • 지역별 최신 가격지수 비교

import duckdb
import plotly.express as px

with duckdb.connect("apt_data.duckdb", read_only=True) as conn:
    monthly_trade = conn.execute("""
        SELECT
            DATE_TRUNC('month', contract_date) AS month,
            trade_type,
            COUNT(*) AS trade_count
        FROM apt_trade
        WHERE district_name = '노원구'
        GROUP BY month, trade_type
        ORDER BY month
    """).df()

fig = px.bar(
    monthly_trade,
    x="month",
    y="trade_count",
    color="trade_type",
    barmode="group",
    title="월별 매매·전월세 거래 건수"
)

fig.write_html("apt_kb_dashboard.html")

대시보드 조회 단계에서는 DuckDB를 읽기 전용으로 연결했습니다.

수집과 적재 작업이 끝난 데이터베이스를 시각화 과정에서 실수로 수정하지 않도록 하기 위해서였습니다.

🖼️ [노원구 아파트 실거래가·KB 시장지표 대시보드 전체 화면 ]

구글애널리틱스 대시보드 스크린샷

결과와 배운 점

1. 총 3만 7천 건 이상의 거래 데이터를 하나의 화면에서 확인할 수 있게 됐다

최종 대시보드에는 2025년 1월부터 2026년 6월까지 노원구의 실거래 데이터가 반영됐습니다.

  • 매매 거래: 10,481건

  • 전월세 거래: 27,091건

  • 전체 거래: 37,572건

  • 전체 매매가격 중앙값: 5.99억 원

  • 순수 전세보증금 중앙값: 2.80억 원

전세보증금 중앙값은 월세가 0인 순수 전세 거래만을 대상으로 계산했습니다.

처음에는 데이터를 빠짐없이 저장하는 것이 목표였지만, 최종적으로는 수만 건의 데이터를 월별·평형별·지역별로 바로 탐색할 수 있게 됐습니다.


2. 전월세 거래량이 매매보다 지속적으로 많았다

월별 거래 건수를 비교해보니 분석 기간 내내 전월세 거래량이 매매 거래량보다 많았습니다.

매매 거래는 2026년 4월 1,038건으로 가장 많았습니다.

단순 누적 건수만 봤을 때도 전월세 거래가 매매 거래의 두 배를 크게 넘었습니다.

이 결과를 통해 가격만 보는 것보다 거래량을 함께 봐야 시장의 움직임을 더 입체적으로 이해할 수 있다는 점을 확인했습니다.


3. 분석 기간 동안 매매가격 중앙값이 약 0.69억 원 상승했다

분석 기간 첫 달과 마지막 달의 매매가격 중앙값을 비교했을 때 약 0.69억 원 증가했습니다.

다만 이 결과에는 서로 다른 전용면적의 거래가 함께 포함되어 있습니다.

특정 월에 대형 평형 거래가 많아지면 전체 중앙값도 상승할 수 있기 때문에, 이것을 곧바로 동일 아파트의 가격 상승으로 해석해서는 안 됩니다.

그래서 전체 중앙값 추이와 평형대별 가격을 함께 제공했습니다.


4. 20평대의 거래가 가장 활발했다

평형대별로 거래량과 매매가격 중앙값을 함께 확인해보니 20평대의 거래량이 가장 많았습니다.

또한 평형대가 커질수록 매매가격 중앙값도 대체로 상승하는 모습을 보였습니다.

거래량만 표시하면 어느 평형이 활발한지는 알 수 있지만 가격 수준은 알기 어렵습니다.

반대로 가격만 표시하면 소수 거래의 결과가 대표값처럼 보일 수 있습니다.

그래서 막대그래프로 거래량을, 선그래프로 가격 중앙값을 함께 표현했습니다.


5. 노원구에서는 오래된 아파트의 거래 비중이 높았다

건축 연식별로 데이터를 구분했을 때 거래량이 가장 많은 구간은 30년 초과 아파트였습니다.

반면 매매가격 중앙값은 신축에 가까울수록 높아지는 경향을 보였습니다.

이는 노원구의 아파트 재고 구성과도 관련이 있을 수 있지만, 이번 대시보드만으로 원인을 단정하지는 않았습니다.

대시보드는 다음 질문을 발견하기 위한 출발점으로 활용했습니다.

  • 오래된 아파트 거래가 많은 동은 어디인가?

  • 재건축 기대 지역이 거래량에 영향을 주는가?

  • 신축 가격 프리미엄은 평형대별로 얼마나 다른가?

  • 같은 동에서도 건축 연도에 따라 가격 차이가 얼마나 발생하는가?


6. 노원구 전세지수는 서울 전체보다 높게 나타났다

2026년 6월 기준 KB 지표는 다음과 같습니다.

  • 노원구 매매가격지수: 106.2

  • 노원구 전세가격지수: 107.2

  • 서울 전세가격지수: 104.5

  • 노원구 전세가율: 56.2%

노원구의 최신 전세가격지수는 서울 전체 지수보다 높게 나타났습니다. 전세가율은 2026년 7월 기준입니다.
실거래 데이터만으로는 최근 거래된 아파트의 가격 변화는 알 수 있지만, 장기간의 시장 흐름과 서울 전체 대비 상대적인 움직임을 보기 어렵습니다.

KB 시장지표를 함께 사용하니 실거래 분석의 시간적 범위를 보완할 수 있었습니다.


7. 서로 다른 지표를 비교할 때는 기준을 통일해야 했다

실거래 가격은 억 원 단위이고, KB 가격지수는 기준시점이 100인 지수입니다.

두 값을 그대로 한 그래프에 표시하면 단위가 다르기 때문에 흐름을 비교하기 어렵습니다.

그래서 실거래 중앙값과 KB 지수를 각각 2025년 1월을 100으로 재지수화했습니다.

SELECT
    base_month,
    median_price
        / FIRST_VALUE(median_price) OVER (ORDER BY base_month)
        * 100 AS actual_price_index,
    kb_price_index
        / FIRST_VALUE(kb_price_index) OVER (ORDER BY base_month)
        * 100 AS kb_rebased_index
FROM monthly_price_comparison;

이를 통해 절대적인 가격 차이가 아니라 두 시계열의 변화 방향을 비교할 수 있었습니다.

다만 두 데이터는 표본과 산정 방식이 다르기 때문에, 하나의 지표가 다른 지표를 움직였다고 인과관계로 해석하지 않았습니다.


8. 수집 자동화의 완성은 시각화가 아니라 검증이었다

처음에는 데이터를 자동으로 내려받고 DuckDB에 넣으면 작업이 끝날 것이라고 생각했습니다.

하지만 진행하면서 자동화에서 가장 중요한 것은 정상적으로 수집됐다는 사실을 증명할 수 있는가라는 점을 알게 됐습니다.

다음 질문에 답할 수 있어야 대시보드를 신뢰할 수 있었습니다.

  • 모든 조회 페이지를 가져왔는가?

  • 같은 거래가 중복 저장되지 않았는가?

  • 일부 월의 데이터가 누락되지는 않았는가?

  • 매매와 전월세가 올바르게 구분됐는가?

  • 금액 단위가 잘못 변환되지는 않았는가?

  • 빈 문자열이 숫자 0으로 바뀌지는 않았는가?

  • 정정되거나 취소된 거래는 어떻게 처리됐는가?

  • KB 지표의 기준월과 실거래 계약월이 일치하는가?

자동화 프로그램에 검증 SQL과 수집 로그를 포함하자 오류를 훨씬 빠르게 찾을 수 있었습니다.

수집 성공 메시지보다 검증 결과가 더 중요했습니다.


9. DuckDB는 수집과 분석 사이를 연결하기에 적합했다

이번 작업에서 DuckDB는 단순한 저장소 이상의 역할을 했습니다.

API 수집
  → DuckDB 저장
  → SQL 검증
  → 월별·지역별 집계
  → Pandas DataFrame 변환
  → Plotly 시각화
  → HTML 대시보드 생성

서버형 데이터베이스를 먼저 구성하지 않아도 파일 하나로 데이터 수집과 분석을 시작할 수 있었습니다.

또한 Python에서 복잡한 반복문으로 집계하기보다 SQL을 이용해 다음 작업을 간단히 처리할 수 있었습니다.

  • 중앙값 계산

  • 월별 집계

  • 평형 구간 분류

  • 건축 연식 구간 분류

  • 지역별 순위

  • 시계열 재지수화

  • 중복 및 이상값 검사

작은 규모의 개인 프로젝트나 데이터 분석 프로토타입에서는 매우 효율적인 선택이었습니다.


앞으로 개선하고 싶은 부분

이번에는 노원구를 기준으로 대시보드를 만들었지만, 다음 단계에서는 자동화 범위를 더 확장해보려고 합니다.

  • 서울 전체 자치구 자동 수집

  • 거래 취소와 정정 데이터 반영

  • 아파트 단지별 가격 추이

  • 동일 면적 기준 반복매매 분석

  • 전용면적을 통제한 가격 변화 분석

  • 지하철역과의 거리 데이터 결합

  • 재건축 연한과 거래량 비교

  • 매일 또는 매월 스케줄 자동 실행

  • 수집 실패 시 메신저 알림

  • 검증 실패 데이터를 별도 테이블로 격리

  • 대시보드 지역 필터 추가

  • DuckDB 파일과 Parquet 파티션 병행 관리

최종적으로는 다음 명령 하나로 수집부터 대시보드 생성까지 실행되는 구조를 목표로 하고 있습니다.

python run_pipeline.py \
  --region "서울특별시 노원구" \
  --start-month 2025-01 \
  --end-month 2026-06 \
  --validate \
  --build-dashboard

이번 작업을 통해 공공데이터 활용에서 중요한 것은 데이터를 많이 모으는 것이 아니라는 점을 배웠습니다.

반복해서 수집할 수 있고, 중복 없이 저장할 수 있고, 결과를 검증할 수 있으며, 실제 의사결정에 사용할 수 있는 형태로 보여줄 수 있어야 데이터가 비로소 가치가 생깁니다.

처음에는 API 데이터 수집 자동화에서 시작했지만, 최종적으로는 국토교통부 실거래와 KB 시장지표를 함께 살펴볼 수 있는 노원구 아파트 대시보드까지 완성할 수 있었습니다.

DuckDB는 이 과정에서 수집과 분석, 검증과 시각화를 연결해주는 가벼우면서도 강력한 중심 도구였습니다.

도움 받은 글

  • 공공데이터포털 국토교통부 아파트 실거래 자료

  • KB부동산 월간 아파트 시장지표

  • DuckDB 공식 문서

  • Plotly Python 공식 문서

  • 완성된 노원구 아파트 실거래가·KB 시장지표 대시보드

완성된 HTML 대시보드 열기

지피터스 뉴스레터

이번 주 AI, 딱 쓸 것만

매주 월요일 아침,
읽고 바로 써먹을 AI 소식만