티스토리 뷰

아침 6시 6분부터 공부함! 4일차 뿌듯!


🚀 Streamlit 앱 배포 실습

배포에 필요한 파일 구조

  • app.py — 진입점 파일
  • pages/ — 멀티페이지 폴더
  • data/ — 데이터 폴더
  • models/ — 머신러닝 모델 폴더
  • requirements.txt — 패키지 목록
  • secrets.toml — API 키 (GitHub에 올리면 ❌, .gitignore에 추가)

requirements.txt 작성 방법

방법  명령어  특징
pip freeze (비권장) pip freeze > requirements.txt 현재 환경의 모든 패키지 포함 → OS 종속 패키지(pywin32 등) 포함되어 오류 가능
pipreqs (권장) pip install pipreqs → pipreqs . --force 실제 코드에서 사용한 패키지만 추출. 누락된 패키지는 수동으로 추가

GitHub 업로드 순서

  1. 다운로드 받은 폴더에서 .git 폴더 삭제 (기존 버전 관리 정보 제거)
  2. .gitignore 생성: F1 → Add gitignore → Python 선택
  3. secrets.toml을 .gitignore에 추가
  4. Git 명령어 순서:
git init
git remote add origin [레포지토리 주소]
git remote -v          # 연결 확인
git add .
git commit -m "커밋 메시지"
git push origin master

로컬 기본 브랜치는 master, GitHub 기본 브랜치는 main으로 이름이 다를 수 있음

Streamlit Cloud 배포 순서

  1. share.streamlit.io 접속 → GitHub 계정으로 로그인
  2. Create app → Deploy public app from GitHub
  3. Repository, Branch(main), Main file(app.py) 선택
  4. Advanced Settings:
    • Python 버전: 3.12
    • Secrets: secrets.toml 내용 붙여넣기 → Save
  5. Deploy 버튼 클릭

코드 수정 후에는 git commit & push만 하면 자동으로 배포에 반영됨 ✅


🐛 배포 실패 주요 원인 & 해결

에러 원인 해결
ModuleNotFoundError requirements.txt에 패키지 누락 패키지 추가 후 commit & push
FileNotFoundError 절대 경로 하드코딩 (C:\\Users\\...) 상대 경로 또는 현재 파일 기반 경로로 변경
KeyError API 키 미설정 Streamlit Cloud → App Settings → Secrets 탭에서 추가
메모리 초과 대용량 데이터를 한꺼번에 로드 아래 메모리 최적화 기법 적용
계속 로딩 중 코드 오류, 무한 루프 중간중간 디버깅 메시지 추가

💾 대용량 파일 처리 전략

GitHub 파일 크기 제한

  • 일반 파일: 100MB
  • Git LFS 사용 시: 최대 2GB
  • 2GB 초과: Google Drive + gdown 라이브러리로 다운로드
# Git LFS 사용법 (최초 1회)
git lfs install
git lfs track "*.csv"
git add .gitattributes
git commit & push

 

메모리 최적화 기법

  • 데이터 타입 최적화 — int64 → int8 (8바이트 → 1바이트), float64 → float32
    • 주의: int8은 0~255 범위만 표현 가능
  • Parquet 파일 사용 — CSV 대비 용량 50~90% 감소, 읽기 속도 향상
df.to_parquet('file.parquet', compression='gzip')
pd.read_parquet('file.parquet')

 

대용량 처리 라이브러리

도구 특징
Polars Pandas보다 빠르고 메모리 효율적. 필요한 부분만 로드하여 처리
DuckDB SQL로 대용량 CSV 직접 쿼리. Pandas 대비 수천 배 빠름 (3GB 기준 수십 초 → 0.02초)
Polars + DuckDB 조합 DuckDB로 필요한 부분만 추출 → Polars로 분석 → Pandas로 추가 가공
import duckdb
con = duckdb.connect()
df = con.sql("SELECT * FROM 'data.csv' WHERE year = 2024").df()

 

Streamlit Cloud 리소스 제한

  • 메모리: 최소 690MB ~ 최대 2.7GB
  • 디스크: 최대 50GB

머신러닝 모델 저장

  • pickle 대신 joblib 사용 → 압축 저장으로 용량 절감

최종 프로젝트(3)

📊 프로젝트 개요

  • 분석 대상: 2024년 텍사스 레인저스 시즌
  • 핵심 문제: 기대 승률 vs 실제 승률의 괴리 → 전반부 선전 후 후반부 급격한 하락
  • 분석 목표: 기대 성적 대비 실제 성적 차이 원인 규명 + 2026년 시즌 개선 방안 도출

 

🗂️ 데이터 수집 전략

주요 데이터 소스

  • FanGraphs – 세이버메트릭스 지표
  • Baseball Reference
  • Baseball Savant

수집 방법

  • CSV 다운로드 + API 활용 병행

 

🛠️ 오늘의 기술적 이슈 & 해결

💡FanGraphs 팀 페이지에서 네트워크 API로 데이터 수집이 안 됐던 이유
팀 페이지는 SSR(Server-Side Rendering) 방식 → 완성된 HTML을 서버에서 전송하기 때문에 API 엔드포인트가 노출되지 않음

 

해결 방법

  • BeautifulSoup 등을 활용한 정적 웹 크롤링 방식으로 전환
  • tbody, tr, td 태그 파싱으로 게임 로그 데이터 추출 성공 ✅
  • 선수별 페이지는 기존 API 방식으로 수집 가능 (팀 페이지만 SSR)

핵심 개념 정리

방식 설명
SSR 서버에서 완성된 HTML을 보내줌 → 정적 크롤링 필요
CSR 클라이언트에서 JS가 데이터를 요청 → API 엔드포인트 확인 가능

 

 

📐 고려 중인 분석 지표

  • OPS (출루율 + 장타율) – 득점과 상관관계 높음
  • xStats (기대 지표) vs 실제 지표 비교
    • 단, 기대값 산정 방식이 공개되지 않아 검증 어려움
  • WAR – fWAR(FanGraphs)와 bWAR(Baseball Reference) 산정 방식 차이 있음
  • 볼 트래킹 데이터

📅 분석 각도

  • 월별 / 분기별 성적 변화
  • 홈 / 원정 경기 비교
  • 이닝별 성적 (전반부 vs 후반부)
  • 접전(1점 차 경기) 분석
  • 선발 / 불펜 투수 분리 분석
  • 선수 부상 이력 & 나이 요인
  • 레딧 팬 커뮤니티 감정 분석 (Reddit API 승인 후 진행 예정)

QCC 3회차

Q1.

SELECT
    login_cnt AS unique_logins,
    COUNT(*) AS employee_count
FROM (
    SELECT
        employee_id,
        COUNT(*) AS login_cnt
    FROM logins
    WHERE login_result = 'SUCCESS'
      AND login_time >= '2023-07-01'
      AND login_time < '2023-10-01'
    GROUP BY employee_id
) t
GROUP BY login_cnt
ORDER BY unique_logins ASC;

# 내가 작성한 쿼리

 

WITH success_logins AS (
    SELECT employee_id, login_id
    FROM qcc.logins
    WHERE login_result = 'SUCCESS'
      AND login_time >= '2023-07-01'
      AND login_time < '2023-10-01'
),
login_counts AS (
    SELECT employee_id, COUNT(distinct login_id) AS unique_logins
    FROM success_logins
    GROUP BY employee_id
)
SELECT unique_logins, COUNT(1) AS employee_count
FROM login_counts
GROUP BY unique_logins
ORDER BY unique_logins

# 정답 쿼리

 


Q2.

SELECT
    employee_id,
    name,
    salary
FROM employee_salary
WHERE salary = (
    SELECT MIN(salary)
    FROM (
        SELECT DISTINCT salary
        FROM employee_salary
        ORDER BY salary DESC
        LIMIT 3
#        LIMIT 1 OFFSET 2
    ) t
)
ORDER BY employee_id ASC;

# 내가 작성한 쿼리

WITH salary_ranked AS (
    SELECT employee_id, name, salary,
           DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM qcc.employee_salary
)
SELECT employee_id, name, salary
FROM salary_ranked
WHERE rnk = 3
ORDER BY employee_id

# 정답 쿼리

 

Q3.

SELECT
    ROUND(SUM(CASE WHEN s.department <> r.department THEN 1 ELSE 0 END) * 100.0/ COUNT(*), 1) AS inter_department_msg_pct
FROM messages m
JOIN employees s
    ON m.sender_id = s.employee_id
JOIN employees r
    ON m.receiver_id = r.employee_id;

# 내가 작성한 쿼리

SELECT
  ROUND(100.0 * SUM(CASE WHEN e1.department != e2.department THEN 1 ELSE 0 END) / COUNT(*), 1) AS inter_department_msg_pct
FROM qcc.messages m
JOIN qcc.employees e1 ON m.sender_id = e1.employee_id
JOIN qcc.employees e2 ON m.receiver_id = e2.employee_id

# 정답 쿼리

 

Q4.

WITH converted_users AS (
    SELECT DISTINCT us.user_id
    FROM qcc.ad_attribution a
    JOIN qcc.user_sessions us ON a.session_id = us.session_id
    WHERE a.converted = TRUE
), first_sessions AS (
    SELECT us.user_id, a.session_id, us.created_at, a.channel,
           ROW_NUMBER() OVER (PARTITION BY us.user_id ORDER BY us.created_at) AS rn
    FROM qcc.user_sessions us
    JOIN converted_users cu ON us.user_id = cu.user_id
    JOIN qcc.ad_attribution a ON us.session_id = a.session_id
)
SELECT user_id, channel
FROM first_sessions
WHERE rn = 1
ORDER BY user_id

# 정답 쿼리

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함