데이터 분석

[250528] 지표 변동의 원인 파악 프로세스, API 실습, 정적 웹 크롤링 실습

경 민 2025. 5. 28. 19:44
👩🏻‍💻  Point of Today I LEARNED 
📌 선택학습반 (Product Data Science)
↗ 3회차

📌 API 활용 실습과제

↗ 네이버 검색어 트렌드 - 검색량 수집 및 line graph 시각화 실습
↗ 카카오맵 - 할리스 카페 위치 정보 파악 및 folium 시각화 실습 (위도/경도)

📌 웹 크롤링 
↗ 정적 크롤링 - AI 기사 추출 실습

📌 최종 프로젝트 주제 탐색

 

 


1. Product Data Scientist

 

< 갑작스러운 지표 변동 이해하기 >

지표 변동의 원인을 파악하고 이에 대해 체계적으로 분석해야 한다.

1단계 변동의 심각성 판단
  • 계절성이 있는 주기적인 변동인가?
  • 데이터 로깅 방식의 변경 여부
  • 제품/서비스 출시 여부
2단계 Funnel 분석으로 문제가 발생한 정확한 지점 식별
  • ex) 문제 상황 : 갑자기 카카오톡 메시지 총 전송량이 급감했다.
    • 퍼널
      1. 어플 실행
      2. 메시지 작성 인터페이스 (대화창)
      3. 메시지 전송 시도
      4. 메시지 전송 성공
3단계 세그멘테이션에서 원인을 좀 더 구체화

→ 특정 그룹에게만 영향이 있었는지? 아니면 모든 사용자들이 같은 행동 양상을 띄는가?

여러 가설을 세워서 파헤치기

  • 기기 유형 : Android, iOS, Web, mWeb
  • 앱 버전 : 최근 업데이트 여부
  • 국가/지역
  • 사용자 유형 : 신규/기존
4단계 해결 방안 도출 및 관계자와 커뮤니케이션
  • 문제 해결을 위한 액셜 플랜 설계
  • 제품 팀과 커뮤니케이션
  • 단기적 조치 ~ 장기적 개선 방안 모색

2. API 강의 복습 및 과제

2-1. 네이버 검색 API를 활용한 검색량 조회

# 1. API 기본 URL
base_url = 'https://openapi.naver.com/v1/datalab/search'

⬇︎

# 2. API 요청 헤더 설정
client_id = " "
client_secret = " "

headers = {
    'X-Naver-Client-Id': client_id,
    'X-Naver-Client-Secret': client_secret,
    'Content-Type': 'application/json'
}

⬇︎

# 3. API 요청 파라미터 설정
params = {
    'startDate': '2024-05-01',
    'endDate': '2025-04-30',    # 지난 1년간의 데이터를
    'timeUnit': 'week', # 주간단위로 가져옴
    'keywordGroups': [
        {
            'groupName': '패션의류',
            'keywords': ['지그재그', '무신사', '29cm']
        },
        {
            'groupName': '전자제품',
            'keywords': ['애플', '삼성', 'lg']
        },
        {
            'groupName': '화장품뷰티',
            'keywords': ['뷰티컬리', '올리브영', '시코르']
        },
        {
            'groupName': '식품구매',
            'keywords': ['마켓컬리', '쿠팡', '오아시스마켓']
        }
    ]
}

파라미터 설정

각 그룹별 대표하는 브랜드 3가지를 키워드로 지정해줬다.

파라미터 각 항목이 어떤 의미인지 헷갈려서 지피티에게 물어봄 . . .

keywordGroups ✅ 필수 여러 검색어 묶음을 비교하려고 만들 때 사용하는 배열 전체를 감싸는 큰 리스트
groupName ✅ 필수 해당 검색어 묶음의 대표 이름(라벨) 
사용자가 구별하기 쉬우려고 임의로 만든 그룹제목
"대선"
keywords ✅ 필수 실제로 검색량을 조사할 구체적인 단어들
"후보", "단일화" 등

⬇︎

# 4. 응답 결과 저장
response = requests.post(base_url, headers=headers, json=params) # 네이버 검색어는 post 메서드를 사용

if response.status_code == 200:
    result = response.json() # json 형식으로 파싱
    for item in result['results']:
        print(f'제목: {item['title']}')
        print(f'검색어: {item['keywords']}')
        for data in item['data']:
            print(f"  기간: {data['period']}, 검색량 비율: {data['ratio']}")
        print('-' * 50)
else:
    print(f'Error Code: {response.status_code}')
    print(f'Error Message: {response.text}')

 

응답결과목록 설정

⬇︎

# 응답 결과 저장
response = requests.post(base_url, headers=headers, json=params)

# 결과 처리 및 시각화
if response.status_code == 200:
    result = response.json()
    
    # 빈 DataFrame 만들기
    df = pd.DataFrame()

    # 각 그룹별로 데이터를 넣어주기
    for item in result['results']:
        group = item['title']
        keyword = item['keywords']
        temp_df = pd.DataFrame(item['data'])
        temp_df['group'] = group
        df = pd.concat([df, temp_df], ignore_index=True)

    # 피벗 테이블로 시계열 형태로 변환 후 시각화
    pivot_df = df.pivot(index='period', columns='group', values='ratio')

    plt.figure(figsize=(14, 6))
    plt.rc('font', family='AppleGothic')
    for col in pivot_df.columns:
        plt.plot(pivot_df.index, pivot_df[col], label=col)

    plt.title('네이버 검색 트렌드 (주간, 그룹별)', fontsize=16)
    plt.xlabel('기간', fontsize=12)
    plt.ylabel('검색량 비율', fontsize=12)
    plt.xticks(rotation=45)
    plt.legend()
    plt.tight_layout()
    plt.grid(True)
    plt.show()

else:
    print(f'Error Code: {response.status_code}')
    print(f'Error Message: {response.text}')

  • '마켓컬리', '쿠팡', '오아시스마켓' 검색량이 압도적으로 많다.
  • 사실 쿠팡이 식품만 판매하는 게 아니라서 keyword 설정을 잘못한 거 같기도 하다.

 

재밌어서 다른 키워드로 또 검색해봤다.

이번엔 배달 플랫폼으로. keyword는 그룹당 하나씩만.

params = {
    'startDate': '2024-05-01',
    'endDate': '2025-04-30',
    'timeUnit': 'week',
    'keywordGroups': [
        {
            'groupName': '배달의민족',
            'keywords': ['배달의민족']
        },
        {
            'groupName': '쿠팡이츠',
            'keywords': ['쿠팡이츠']
        },
        {
            'groupName': '요기요',
            'keywords': ['요기요']
        },
        {
            'groupName': '땡겨요',
            'keywords': ['땡겨요']
        }
    ]
}

  • 오.. 예상했던대로 1년간 배민이 1등이고, 다음으로 쿠팡이츠 > 요기요 > 땡겨요 순으로 검색량이 많다.
  • 2025년 3월 24일 주간에 딱 한 번 쿠팡이츠 검색량이 배민을 이겼다.
  • 찾아보니 3월 25일에 쿠팡이츠에서 포장수수료 관련한 정책을 내놓음.
  • 요기요는.. 2024년 5월~8월까지 검색량 들쭉달쭉하다가 결국 하향 후 그대로 유지 중. 심지어 땡겨요에 역전당하기도 함.

2-2. 카카오맵 API를 활용한 할리스 카페 위도/경도 구하기

# 0. 할리스 카페 기본 정보 들어있는 csv 파일 불러오기 (주소정보 필수)
df = pd.read_csv('/Users/t2024-m0164/Desktop/Data/6. API : 웹 크롤링 실습/1. 데이터 수집 API 실습 파일/hollys_stores.csv')

⬇︎

# 1. API 기본 URL
url = "https://dapi.kakao.com/v2/local/search/keyword.json"

⬇︎

# 2. 헤더 정보 입력
headers = {"Authorization": 'KakaoAK {REST_API_KEY}'}

카카오맵 인증 설정은 네이버랑 달랐다.

Authorization 을 사용함

* 발급받은 api key를 입력해서 실행시켰더니 
{'errorType': 'NotAuthorizedError', 
'message': 'App(Data) disabled OPEN_MAP_AND_LOCAL service.'}

# '앱은 존재하지만 해당 API 기능은 꺼져 있어서 접근할 수 없다' 는 의미

 

이런 에러가 났다.

카카오맵의 로컬 API 사용 권한을 활성화해줘야 하는 문제였다.
[ 해결 방법 ]
1. https://developers.kakao.com 접속
2. 내 애플리케이션
3. 등록한 애플리케이션 클릭
4. 좌측 메뉴에서 '설정 > 앱 권한신청 > 앱 권한'
5. 카카오맵 선택 후 체크박스 활성화 ON

⬇︎

# 3. 위도/경도 저장할 열 준비
df['위도'] = None
df['경도'] = None

⬇︎

# 4. 반복문으로 주소 변환 요청
for i, row in df.iterrows():
    query = row['address']
    try:
        res = requests.get(url, 
                           headers=headers, 
                           params={'query': query})
        
        if res.status_code == 200:
            result = res.json()

            if result['documents']:
                doc = result['documents'][0]
                df.at[i, '위도'] = doc['y']
                df.at[i, '경도'] = doc['x']
            else:
                print(f"[결과 없음] {query}")
        else:
            print(f"[오류] status: {res.status_code}, 주소: {query}")
        time.sleep(2)  # 너무 빠르면 차단당할 수 있음
    except Exception as e:
        print(f"[예외 발생] {query} → {e}")
        
# 5. csv 파일로 저장
df.to_csv('할리스매장_위경도.csv', index=False, encoding='utf-8-sig')
print("✅ 변환 완료! 파일 저장됨.")

원래 phone 컬럼까지만 있었는데 위도, 경도가 추가됨.

⬇︎

# 6. 최종 시각화 
import pandas as pd
import folium

# 1) CSV 파일 불러오기
df = pd.read_csv('할리스매장_위경도.csv')

# 2) 중심 좌표 계산 (평균값 또는 특정 지역 중심)
center_lat = df['위도'].astype(float).mean()
center_lon = df['경도'].astype(float).mean()

# 3) 지도 객체 생성
map_hollys = folium.Map(location=[center_lat, center_lon], zoom_start=12)

# 4) 각 매장 마커 추가
for i, row in df.iterrows():
    try:
        folium.Marker(
            location=[float(row['위도']), float(row['경도'])],
            popup=f"{row['name']}<br>{row['address']}",
            icon=folium.Icon(color='red', icon='coffee', prefix='fa')
        ).add_to(map_hollys)
    except:
        continue  # 좌표 없는 경우 생략

# 5) 지도 저장
map_hollys.save('hollys_map.html')
print("✅ hollys_map.html 파일로 저장 완료!")


3. 웹 크롤링

3-1. 정적 크롤링

1) HTML 파싱 기초

<html>
    <body>
        <div class="container">
            <h1 id="title">웹 크롤링 기초 학습하기</h1>
            <p class="content">BeautifulSoup을 이용한 HTML 파싱 방법을 배워봅시다.</p>
            <p class="content">CSS 선택자를 활용하면 원하는 요소를 쉽게 찾을 수 있습니다.</p>
            <ul class="items">
                <li class="item">HTML 구조 이해하기</li>
                <li class="item second">CSS 선택자 활용하기</li>
                <li class="item">데이터 추출 및 저장하기</li>
            </ul>
            <a href="https://example.com">더 알아보기</a>
        </div>
    </body>
</html>

select가 계층적으로 찾기 때문에 보다 정확하고 강력하다.

 

 


🔸 간단 실습 🔸

두 개 모두 클래스가 content인 태그 조회하는 코드

 

** select는 클래스로 조회할 때 앞에 . 붙여야 함 (id로 조회할 땐 #)



 

2) AI타임즈 뉴스 수집 실습 과제

import requests
from bs4 import BeautifulSoup
import pandas as pd
import json
from typing import List, Dict

⬇︎

# 1. 웹페이지 요청
def get_news_list(page: int) -> str:
   
    url = "https://www.aitimes.com/news/articleList.html?view_type=sm"
    params = {"page": page}
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'
    }
    
    try:
        response = requests.get(url, params=params, headers=headers)
        response.raise_for_status()  # 오류가 있으면 예외를 발생시킴
        return response.text
    except requests.exceptions.RequestException as e:
        print(f"페이지 {page} 요청 중 에러 발생: {e}")
        return ""

# 테스트: 첫 페이지 가져오기
html = get_news_list(1)
print("HTML 일부:", html[:500])  # 처음 500자만 출력

⬇︎

# 2. HTML 파싱
def parse_news_info(html: str) -> List[Dict]:
 
    news_list = []
    soup = BeautifulSoup(html, 'html.parser')

    articles = soup.select('ul.type2 > li')

    if not articles:
        return news_list
        
    for li in articles:
        title_tag = li.select_one('h4.titles')
        summary_tag = li.select_one('p.lead.line-6x2')
        byline_tags = li.select('span.byline > em')
        link_tag = li.select_one('a[href]')
        
        if not (title_tag and summary_tag and len(byline_tags) >= 2 and link_tag):
            continue
            
        news = {
            '제목': title_tag.get_text(strip=True),
            '요약': summary_tag.get_text(strip=True),
            '기자': byline_tags[0].get_text(strip=True),
            '날짜': byline_tags[1].get_text(strip=True),
            '링크': 'https://www.aitimes.com' + link_tag['href']
        }
        news_list.append(news)
        
    return news_list

# 테스트: 첫 페이지 파싱하기
news_list = parse_news_info(html)
print(f"첫 페이지에서 찾은 뉴스 수: {len(news_list)}")
print("\n첫 번째 뉴스 정보:")
print(json.dumps(news_list[0], ensure_ascii=False, indent=2))

⬇︎

# 3. 데이터 저장

def save_to_files(news_list: List[Dict], base_path: str = "/Users/t2024-m0164/Desktop/"):
   """
   매장 정보를 CSV와 JSON 파일로 저장하는 함수
   """
   # DataFrame 생성
   df = pd.DataFrame(news_list)
   
   # CSV 파일로 저장
   csv_path = f"{base_path}news_list.csv"
   df.to_csv(csv_path, encoding='utf-8', index=False)
   
   # JSON 파일로 저장 (DataFrame 활용)
   json_path = f"{base_path}news_list.json"
   df.to_json(json_path, orient='records', force_ascii=False, indent=4)
   
   print(f"CSV 파일 저장 완료: {csv_path}")
   print(f"JSON 파일 저장 완료: {json_path}")
   
   # 데이터 미리보기
   print("\n데이터 미리보기:")
   print(df.head())

# 테스트: 첫 페이지 데이터 저장하기
save_to_files(news_list)

⬇︎

# 4. 전체 반복 (페이지 별로)
def main():
    news_list = []
    
    # 매장 정보 수집
    print("AI 뉴스정보 수집 중...")
    for page in range(1, 5):  # 1~4 페이지만 수집 (테스트용)
        html = get_news_list(page)
        if html:
            page_news = parse_news_info(html)
            news_list.extend(page_news)
            # stores += page_stores
            print(f"페이지 {page}: {len(page_news)}개의 뉴스 정보 수집 완료")
    
    if not news_list:
        print("뉴스 정보를 가져오는데 실패했습니다.")
        return
        
    print(f"\n총 {len(news_list)}개의 뉴스 정보를 수집했습니다.")

    # 데이터 저장
    try:
        save_to_files(news_list)
    except Exception as e:
        print(f"데이터 저장 중 에러 발생: {e}")

# 전체 과정 실행
main()

 

여기까지 크롤링 완료...

 

csv 파일 불러온 후 날짜 컬럼을 datetime으로 형변환해준 후 dt.day로 YYYY-MM-DD 형식의 컬럼을 다시 만들어줬다.

이후 일별 기사 개수 카운팅하는 것까지 분석해보았다.