본문 바로가기
Project/🚀 AI Finance 2026

#15 [모듈1-9] pytrends + BS4 — 대안 데이터

by 로띠 2026. 3. 13.
데이터 수집

pytrends + BeautifulSoup4 — 대안 데이터로 주가 예측 정확도 높이기

Google Trends와 커뮤니티 데이터 수집/분석

Stock Price Fundamental News Sentiment Phase 7 Alternative Data ML/DL Model

주가를 예측할 때 가격 차트와 재무제표만 보면 될까요? Alchemetric 프로젝트는 여기서 한 발 더 나아가 사람들의 관심도를 데이터로 수집합니다. Google에서 "삼성전자"를 얼마나 검색하는지, 네이버 종목토론방에서 얼마나 떠들고 있는지 — 이런 대안 데이터(Alternative Data)pytrendsBeautifulSoup4로 자동 수집하고, ML 모델이 학습할 수 있는 피처로 변환하는 과정을 초보자 눈높이에서 설명합니다.

1. 대안 데이터(Alternative Data)란?

주가를 예측할 때 보통 어떤 데이터를 사용할까요? 대부분 전통적인 금융 데이터를 떠올립니다.

분류 예시 한계
주가/거래량 시가, 종가, 거래량, 이동평균 과거 기록일 뿐, 미래 원인을 담지 않음
재무제표 PER, PBR, ROE, 부채비율 분기 1회 발표, 이미 주가에 반영
거시경제 환율, 금리, VIX, 유가 시장 전체 영향이라 종목 특화 신호 약함

이런 전통 데이터는 모든 투자자가 똑같이 볼 수 있기 때문에, 그것만으로는 남들보다 나은 예측이 어렵습니다. 그래서 등장한 것이 대안 데이터(Alternative Data)입니다.

대안 데이터란?
전통적 금융 데이터가 아닌, "비전통적 소스"에서 얻는 투자 인사이트를 말합니다.

실제 월가 사례:
• 위성 사진으로 월마트 주차장 이용률 파악 → 매장 매출 예측
• 신용카드 결제 데이터로 소비 트렌드 분석
• SNS/검색 트렌드로 대중의 관심도 측정

우리 프로젝트의 대안 데이터

Alchemetric에서는 두 가지 대안 데이터를 수집합니다.

소스 수집 도구 측정하는 것
Google Trends pytrends 검색량 트렌드 (대중의 관심도)
네이버 종목토론방 BeautifulSoup4 커뮤니티 활성도 (개인 투자자 관심)

왜 중요한가?

"사람들이 검색을 많이 하면, 곧 주가가 움직인다."

맛집을 비유로 들어보겠습니다. 어떤 식당이 SNS에서 갑자기 화제가 되면 어떻게 될까요?

맛집 비유

1. SNS에서 "OO식당" 검색량 급증 (= Google Trends 상승)
2. 블로그/커뮤니티에 후기 글 폭증 (= 네이버 토론방 게시글 증가)
3. 곧 줄이 길어짐 (= 주가 상승)

검색량이 급증한다는 것은, 아직 행동(매수)으로 옮기지 않았지만 관심이 뜨겁다는 신호입니다. 이 "관심"을 수치화하면 주가 변동을 미리 감지할 수 있습니다.

2. Google Trends + pytrends

Google Trends란?

Google Trends는 구글에서 특정 키워드가 얼마나 검색되는지를 0~100 스케일로 보여주는 서비스입니다. 100이 해당 기간 내 가장 많이 검색된 시점이고, 50이면 그 절반 정도입니다.

예를 들어 "삼성전자"를 검색하면, 최근 3개월간 주 단위로 검색량 변화를 그래프로 확인할 수 있습니다. 실적 발표나 대형 뉴스가 있으면 검색량이 확 뛰는 패턴을 볼 수 있죠.

pytrends: 파이썬에서 자동으로 조회하기

pytrends는 Google Trends를 파이썬 코드로 자동 조회할 수 있게 해주는 비공식 라이브러리입니다. 웹 브라우저에서 일일이 검색하지 않아도, 코드 한 줄로 데이터를 가져올 수 있습니다.

from pytrends.request import TrendReq

# 1. pytrends 객체 생성 (한국어, 한국 시간대)
pytrends = TrendReq(hl="ko", tz=540)

# 2. 검색할 키워드와 기간 설정
pytrends.build_payload(
    kw_list=["삼성전자"],         # 검색 키워드
    timeframe="2024-01-01 2024-03-31",  # 기간
    geo="KR"                       # 한국 지역
)

# 3. 검색량 데이터 가져오기
df = pytrends.interest_over_time()
# → 주간 데이터 (매주 일요일 기준, 0~100 스케일)

실제 Alchemetric 코드(google_trends_fetcher.py)에서는 종목명을 키워드로 사용합니다. 종목코드가 아닌 "삼성전자" 같은 한글 이름으로 검색하는 것이 핵심입니다.

주간 → 일간 보간 (Linear Interpolation)

여기서 한 가지 문제가 있습니다. Google Trends는 주간 데이터만 제공합니다. 하지만 주가 데이터는 매일(일별) 있죠. 주간 데이터를 일별로 바꿔야 합니다.

선형 보간이란?

두 점 사이를 직선으로 이어 중간값을 추정하는 방법입니다.

예시: 월요일의 검색량이 50, 다음 월요일이 70이라면?
• 화요일: 약 53
• 수요일: 약 57
• 목요일: 약 60
• 금요일: 약 63

완벽하진 않지만, "빈 자리를 합리적으로 채우는" 가장 단순한 방법입니다.

이렇게 변환하면 두 가지 값이 생깁니다.

컬럼명 설명 특징
google_trend_value 주간 원본값 (0~100) 일주일에 한 번만 값이 있고, 나머지는 NULL
google_trend_interpolated 일별 보간값 (0~100) 매일 값이 있음 (추정치 포함)

실제 코드를 보면, pandas의 interpolate(method="linear")를 사용합니다.

def _interpolate_to_daily(self, weekly_df, start_dt, end_dt):
    """주간 → 일별 선형 보간"""
    # 주간 데이터를 일별 인덱스로 확장
    daily_idx = pd.date_range(start=start_dt, end=end_dt, freq="D")
    daily_df = wdf.reindex(daily_idx)

    # 빈 날짜를 선형 보간으로 채우기
    daily_df["value"] = daily_df["value"].interpolate(method="linear")

    # 원본 주간 값은 google_trend_value에, 보간값은 google_trend_interpolated에
    result["google_trend_value"] = ...       # 주간 날짜만 값 있음
    result["google_trend_interpolated"] = ...  # 매일 값 있음

Rate Limiting: Google에게 예의 바르게 요청하기

Google Trends에 너무 빠르게 연속 요청하면 차단당합니다. 그래서 요청마다 2.0초 대기 시간을 둡니다. 또한 실패 시 3번까지 재시도(retry)하며, 재시도 간격도 점점 늘어납니다(3초 → 6초 → 12초).

# google_trends_fetcher.py
_RATE_LIMIT_DELAY = 2.0   # 요청 사이 2초 대기

# retry 데코레이터: 실패 시 3번 재시도 (3초 → 6초 → 12초)
@retry(max_attempts=3, delay=3.0, backoff=2.0)
def _fetch_trend(self, keyword, start_date, end_date):
    ...
    time.sleep(_RATE_LIMIT_DELAY)   # 2초 쉬기

3. 네이버 종목토론방 + BeautifulSoup

네이버 종목토론방이란?

네이버 종목토론방은 개인 투자자들이 종목별로 의견을 교환하는 게시판입니다. 주소를 보면 패턴이 있습니다.

# 삼성전자(005930)의 종목토론방 URL
https://finance.naver.com/item/board.naver?code=005930

주가가 급등하거나 급락하면 게시글과 댓글이 폭증하는 패턴이 있습니다. 이 활동량 자체가 "투자자들의 관심과 흥분 정도"를 보여주는 중요한 신호입니다.

BeautifulSoup4란?

BeautifulSoup4는 파이썬의 HTML 파싱 라이브러리입니다. 웹 페이지의 HTML 코드를 읽어서, 원하는 데이터만 쏙쏙 뽑아내는 도구죠.

비유: 신문에서 기사 오려내기

신문 전체(= HTML 페이지)에서 원하는 기사(= 게시글 정보)만 가위로 오려내는 것과 같습니다.
BeautifulSoup이 가위 역할을 해주는 거죠.

requests = 신문을 가져오는 배달부
BeautifulSoup = 필요한 부분만 오려내는 가위

사용법: 게시판 크롤링 기본 흐름

from bs4 import BeautifulSoup
import requests

# 1. 웹 페이지 가져오기 (배달부 역할)
url = "https://finance.naver.com/item/board.naver?code=005930"
html = requests.get(url, headers={"User-Agent": "..."}).text

# 2. HTML 파싱 (가위 역할)
soup = BeautifulSoup(html, "html.parser")

# 3. 게시판 테이블에서 행(row) 추출
rows = soup.select("table.type2 tbody tr")

for row in rows:
    date = cols[0].get_text(strip=True)        # 날짜
    title = cols[1].select_one("a").text       # 제목
    comment = cols[1].select_one("span.cmt")  # 댓글 수

soup.select("table.type2 tbody tr")CSS 선택자로 HTML에서 원하는 요소를 찾는 문법입니다. "type2 클래스의 테이블 안에 있는 모든 행(tr)을 가져와라"라는 뜻이죠.

수집 항목과 일별 집계

각 게시글에서 날짜댓글 수를 추출한 뒤, 날짜별로 집계합니다.

컬럼명 설명 집계 방식
community_post_count 일별 게시글 수 해당 날짜 게시글 개수 합산
community_comment_count 일별 댓글 수 해당 날짜 모든 게시글의 댓글 수 합산

실제 코드에서 일별 집계는 defaultdict을 사용합니다.

# naver_community_fetcher.py
# 일별 집계: 같은 날짜의 게시글 수 + 댓글 수를 합산
daily = defaultdict(lambda: {"post_count": 0, "comment_count": 0})

for post in all_posts:
    daily[post["date"]]["post_count"] += 1
    daily[post["date"]]["comment_count"] += post["comment_count"]

Rate Limiting + User-Agent 설정

네이버 서버에도 예의를 지켜야 합니다. 요청 간 0.4초 대기하고, User-Agent 헤더를 설정하여 일반 브라우저인 것처럼 요청합니다.

# naver_community_fetcher.py
_RATE_LIMIT_DELAY = 0.4    # 0.4초 대기 (초당 최대 2.5회)

_USER_AGENT = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/120.0.0.0 Safari/537.36"
)

# 세션에 User-Agent와 Referer 헤더 설정
self.session.headers.update({
    "User-Agent": _USER_AGENT,
    "Referer": "https://finance.naver.com/",
})

4. ML 피처로의 변환 (Phase 7)

수집한 원시 데이터를 그대로 ML 모델에 넣으면 안 됩니다. "검색량 73"이나 "댓글 수 542"라는 숫자는 모델이 바로 이해하기 어렵기 때문이죠. 피처 엔지니어링을 통해 ML이 학습하기 좋은 형태로 가공해야 합니다.

6개 대안 데이터 피처

피처 원본 데이터 변환 방식 의미
google_trend_score 검색량 (0~100) ÷ 100 (0~1 정규화) 검색 관심도
google_trend_momentum 현재 vs 5일 전 (현재-5일전) / 5일전 관심도 변화 방향
community_post_volume 게시글 수 log1p (로그 변환) 토론 활성도
community_comment_volume 댓글 수 log1p (로그 변환) 참여 깊이
community_engagement_ratio 댓글 / 게시글 비율 계산 게시글당 참여도
alternative_activity_index 트렌드 + 커뮤니티 0.5 × trend + 0.5 × post 통합 활성도 지수

log1p 변환이란?

log1p(x)log(1 + x)의 줄임말입니다. 왜 이런 변환이 필요할까요?

어떤 날은 댓글이 5개이고, 어떤 날은 10,000개일 수 있습니다. 이런 극단적인 차이를 그대로 두면 ML 모델이 학습하기 어렵습니다. 로그 변환은 이런 극단값을 완화해줍니다.

비유: 데시벨(dB)

소리의 크기를 그냥 숫자로 표현하면, 속삭임(0.001)과 제트기 엔진(1,000,000)의 차이가 너무 큽니다. 데시벨로 바꾸면 속삭임이 30dB, 제트기가 140dB로 — 비교가 훨씬 쉬워집니다.

log1p도 똑같은 원리입니다!
원본값 (댓글 수) log1p(x) 결과 해석
0 0 활동 없음
10 2.4 약간의 관심
100 4.6 활발한 토론
1,000 6.9 폭발적 관심

원본값 0~1,000의 천 배 차이가 → 0~6.9의 약 7배 차이로 완화됩니다. 모델이 훨씬 학습하기 좋아지죠.

실제 변환 코드

# feature_engineer.py — _merge_alternative_features()

# 1) google_trend_score: 0~100 → 0~1
alt_df["google_trend_score"] = alt_df["trend_interp"] / 100.0

# 2) google_trend_momentum: (현재 - 5일전) / max(5일전, 1)
shifted = alt_df["trend_interp"].shift(5)
alt_df["google_trend_momentum"] = (
    (alt_df["trend_interp"] - shifted) / shifted.clip(lower=1.0)
)

# 3) community_post_volume: log1p 변환
alt_df["community_post_volume"] = np.log1p(alt_df["post_count"])

# 4) community_comment_volume: log1p 변환
alt_df["community_comment_volume"] = np.log1p(alt_df["comment_count"])

# 5) community_engagement_ratio: 댓글 / 게시글
alt_df["community_engagement_ratio"] = (
    alt_df["comment_count"] / alt_df["post_count"]
)

# 6) alternative_activity_index: 통합 지수
alt_df["alternative_activity_index"] = (
    0.5 * trend_norm + 0.5 * post_norm
)

각 피처를 쉽게 이해하기

google_trend_score: "지금 사람들이 이 종목에 얼마나 관심 있는가?" (0=무관심, 1=최고 관심)

google_trend_momentum: "관심이 늘고 있는가, 줄고 있는가?" (양수=관심 증가, 음수=관심 감소)

community_post_volume: "오늘 토론방이 얼마나 뜨거운가?" (게시글이 많을수록 높은 값)

community_comment_volume: "사람들이 얼마나 깊이 참여하고 있는가?" (댓글이 많을수록 높은 값)

community_engagement_ratio: "게시글 하나당 평균 몇 개의 댓글이 달리는가?" (비율이 높으면 열띤 토론 중)

alternative_activity_index: "종합적으로 이 종목에 대한 관심도가 어느 정도인가?" (트렌드 50% + 커뮤니티 50%)

5. 웹 스크래핑 주의사항

웹 스크래핑(크롤링)은 남의 서버에서 데이터를 가져오는 행위입니다. "남의 집 방문"에 비유하면 지켜야 할 예절이 있습니다.

규칙 비유 Alchemetric 적용
robots.txt 확인 현관에 "방문 금지" 표시가 있는지 확인 크롤링 허용 범위 확인 후 수집
Rate Limiting 초인종을 1초마다 누르면 안 됨 — 기다리기 Google: 2.0초, 네이버: 0.4초 간격
User-Agent 설정 방문자 명찰 착용 (누구인지 밝히기) Chrome 브라우저 User-Agent 사용
에러 처리 문이 잠겨 있으면 돌아가기 (부수지 않기) 타임아웃 10초, 실패 시 3번 재시도 후 포기
HTML 구조 변경 대응 집 리모델링하면 가구 위치가 바뀜 파싱 실패 시 graceful하게 빈 리스트 반환
핵심 원칙: "예의 바른 크롤러"

서버에 부담을 주지 않도록 천천히 요청하고, 차단당하면 즉시 멈추고, 에러가 나면 깔끔하게 처리합니다. Alchemetric의 크롤러는 이 원칙을 코드 레벨에서 강제합니다 — time.sleep()@retry 데코레이터로요.

6. 전체 파이프라인 흐름

데이터 수집부터 ML 학습까지 전체 흐름을 정리해봅시다.

Google Trends API                Naver Finance
       |                                |
  pytrends                     BeautifulSoup
       |                                |
주간 검색량 (0~100)          일별 게시글/댓글 수
       |                                |
선형 보간 (일간)              일별 집계
       |                                |
       +--------+         +-------------+
                |         |
                v         v
     alternative_data 테이블 (통합 저장)
                |
                v
     Feature Engineering (Phase 7)
                |
                v
     6개 피처 (log1p, 정규화, 모멘텀 등)
                |
                v
     feature_storeML/DL 학습

DB 테이블 구조 (alternative_data)

Google Trends와 네이버 커뮤니티 데이터는 하나의 테이블에 통합 저장됩니다. market + code + date 조합이 유니크 키입니다.

# models/alternative.py
class AlternativeData(ModelBase):
    __tablename__ = "alternative_data"

    id     = Column(Integer, primary_key=True)
    date   = Column(Date, nullable=False)
    market = Column(String(10))    # "KOSPI", "KOSDAQ"
    code   = Column(String(20))    # "005930"

    # Google Trends 데이터
    google_trend_value        = Column(Numeric)  # 주간 원본 (0~100)
    google_trend_interpolated = Column(Numeric)  # 일별 보간값

    # 네이버 종목토론방 데이터
    community_post_count    = Column(Integer)  # 일별 게시글 수
    community_comment_count = Column(Integer)  # 일별 댓글 수

Upsert: 데이터 충돌 없이 저장하기

Google Trends와 커뮤니티 데이터는 각각 별도로 수집됩니다. 같은 날짜에 데이터를 저장할 때, 한쪽이 다른 쪽 데이터를 덮어쓰면 안 되겠죠? 이를 위해 Upsert(Insert or Update) 패턴을 사용합니다.

# alternative_repository.py
# Trends 저장 시: 커뮤니티 컬럼은 건드리지 않음
def upsert_trends_data(self, records):
    # google_trend_value, google_trend_interpolated만 업데이트
    # community_post_count, community_comment_count는 보존!

# 커뮤니티 저장 시: Trends 컬럼은 건드리지 않음
def upsert_community_data(self, records):
    # community_post_count, community_comment_count만 업데이트
    # google_trend_value, google_trend_interpolated는 보존!

7. 대안 데이터의 한계

대안 데이터가 만능은 아닙니다. 몇 가지 한계를 알고 사용해야 합니다.

소스 한계 영향
Google Trends 주간 데이터만 제공 → 보간 오차 발생 수~목 데이터는 추정치일 수 있음
검색량 절대치를 모름 (0~100 상대값) 다른 종목과의 직접 비교 불가
네이버 커뮤니티 스팸/광고 게시글 포함 실제 관심도보다 부풀려질 수 있음
HTML 구조 변경 리스크 네이버가 디자인을 바꾸면 크롤러 수정 필요
공통 인과관계가 아닌 상관관계 검색량 증가가 반드시 주가 상승을 의미하진 않음

그럼에도 쓰는 이유

대안 데이터는 "대중 관심도"라는 전통 데이터에 없는 차원을 추가합니다.

주가, 재무제표, 거시경제 지표 — 이 모든 데이터에는 "사람들이 지금 이 종목에 얼마나 열광하고 있는가?"라는 정보가 빠져 있습니다. Google Trends와 커뮤니티 활성도는 바로 이 빈자리를 채워줍니다.

비유하자면, 전통 데이터가 자동차의 속도계라면, 대안 데이터는 차 안에 타고 있는 사람들의 흥분 정도를 알려주는 것과 같습니다. 속도가 같더라도 운전자가 가속 페달을 밟을 의지가 있는지, 아니면 브레이크를 밟으려는지 — 그 차이를 감지하는 데 도움을 줍니다.

Alchemetric의 ML 모델은 현재 총 69개 이상의 피처를 사용합니다. 대안 데이터 6개 피처는 그중 일부이지만, 다른 피처들이 포착하지 못하는 고유한 신호를 제공하기 때문에 예측 정확도 향상에 기여합니다.

Phase 데이터 소스 피처 수
Phase 1 (가격/기술) 주가, 거래량, RSI, MACD, 볼린저 밴드 등 24
Phase 2 (재무) PER, PBR, ROE, 외국인 비율 등 +9
Phase 3 (거시) 환율, 금리, VIX, 유가, 금 등 +11
Phase 4 (뉴스) 뉴스 센티먼트, 뉴스 볼륨 +5
Phase 5 (공시/수급) 공시 건수, 공매도, 프로그램매매 +10
Phase 6 (섹터) 섹터 수익률, 상대강도, 뉴스 정제 +10
Phase 7 (대안 데이터) Google Trends, 네이버 커뮤니티 +6

정리하면, pytrends로 Google 검색 트렌드를 수집하고, BeautifulSoup4로 네이버 종목토론방을 크롤링하여, "사람들의 관심도"라는 전통 금융 데이터에는 없는 차원을 ML 모델에 제공합니다. 주간 데이터를 선형 보간하고, log1p로 극단값을 완화하고, 모멘텀과 참여비율 같은 파생 피처를 만드는 과정 — 이 모든 것이 Phase 7 Alternative Data Pipeline입니다.