Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

우리카드 소비 패턴 기반 맞춤형 카드 추천 시스템

고객의 카드 이용 내역을 분석해 나와 소비 성향이 비슷한 고객들이 더 많이 이용하는 업종을 찾아내고, 해당 업종에 혜택이 있는 카드를 GPT-4o-mini로 자연어 추천하는 시스템입니다.


목차

  1. 기술 스택
  2. 시스템 구조
  3. 추천 파이프라인
  4. 디렉토리 구조
  5. 환경 설정
  6. 실행 방법
  7. RAG 설정 (선택)
  8. 성능 평가
  9. API 명세

기술 스택

레이어 기술
Frontend HTML5, jQuery 3.7, Bootstrap 5.3
Backend Python 3.11, FastAPI 0.115
ML scikit-learn (MiniBatchKMeans, cosine_similarity)
LLM OpenAI GPT-4o-mini
고객 DB MySQL 8.0 (읽기 전용)
벡터 DB ChromaDB 0.5
캐시 joblib pkl

시스템 구조

Browser (jQuery)
    │  GET /api/customer/{seq}
    │  POST /api/recommend
    ▼
FastAPI (uvicorn)
    │
    ├─ 서버 시작 시 (1회)
    │     MySQL → 청크 스트리밍(50,000행) → ThreadPool 병렬 전처리
    │     → MiniBatchKMeans 학습 → pkl 캐시 저장
    │     (재시작 시 pkl 로드로 수 초 내 완료)
    │
    └─ 추천 요청마다
          비율 벡터 생성
          → Stage 1: KMeans 클러스터링으로 후보군 압축 (~35,000명)
          → Stage 2: 코사인 유사도 Top-30 + 갭 분석 → 타겟 업종 추출
          → Stage 3: GPT-4o-mini 자연어 추천 생성
                      (RAG_ENABLED=true 면 ChromaDB 카드 문서 grounding)

추천 파이프라인

전처리 — 비율 벡터 생성

대분류 10개 업종 금액을 그대로 비교하면 고소비 고객끼리만 유사해지는 문제가 발생합니다. 이를 막기 위해 소비 성향(구성 비중) 으로 변환합니다.

① NULL → 0 대체  (미사용 업종)
② log1p 변환     (금액 왜도 완화)
③ L1 정규화      (합이 1인 10차원 비율 벡터)
   → 전 업종 미사용이면 균등값(0.1) 할당

Stage 1: Recall — KMeans 클러스터링

117만 명 전체와 유사도를 계산하면 요청마다 수십 초가 걸립니다. MiniBatchKMeans(K=100)로 전체를 클러스터링하고, 대상 고객이 속한 클러스터 + 인접 클러스터 2개로 후보군을 약 35,000명으로 압축합니다.

Stage 2: Ranking — 코사인 유사도 + 갭 분석

코사인 유사도로 후보군 내 Top-30명 선별
    ↓
업종별 갭 분석:
  Top-30 중 해당 업종 실제 이용자(ratio > 0)만 필터링
  → 그 사람들의 평균 vs 대상 고객의 비율 = gap
  → 활성 이용자가 5명 미만이면 클러스터 centroid로 폴백
  → gap >= 0.01 인 업종만 후보, 미이용 업종은 +0.05 가산점
  → 상위 3개 업종을 타겟으로 선정

해석: "나와 소비 성향이 비슷한 고객 중 이 업종을 실제로 쓰는 사람들은 평균 X%p 더 이용합니다"

Stage 3: LLM 추천 생성

RAG_ENABLED 값에 따라 두 경로로 분기합니다.

경로 조건 특징
RAG 경로 RAG_ENABLED=true ChromaDB에서 카드 문서 검색 후 GPT에 제공 → 카드명 포함 추천
인사이트 경로 RAG_ENABLED=false (기본값) 소비 패턴 분석 + 방향성만 제시 → 할루시네이션 없음

두 경로 모두 "소비를 늘리세요" 같은 지시적 표현을 금지하고, "비슷한 분들은 이 업종을 더 즐기시더라고요" 관점으로 자연스럽게 안내합니다.


디렉토리 구조

.
├── backend/
│   ├── main.py                 # FastAPI 앱, lifespan 초기화
│   ├── config.py               # 환경변수 로드 (pydantic BaseSettings)
│   ├── parameters.py           # 알고리즘 파라미터 전체 (하드코딩 상수)
│   ├── db/
│   │   ├── mysql.py            # SQLAlchemy 엔진 (읽기 전용)
│   │   └── chroma.py           # ChromaDB 클라이언트
│   ├── models/
│   │   ├── customer.py         # ORM 모델 (CUSTOMER_USAGE 테이블)
│   │   └── *.pkl               # 학습된 모델 캐시 (gitignore)
│   ├── routers/
│   │   ├── customer.py         # GET /api/customer/{seq}
│   │   └── recommendation.py   # POST /api/recommend
│   ├── services/
│   │   ├── preprocess.py       # 비율 벡터 생성
│   │   ├── recall.py           # Stage 1: KMeans 후보 생성
│   │   ├── ranking.py          # Stage 2: 유사도 + 갭 분석
│   │   └── llm.py              # Stage 3: LLM 추천 생성
│   └── utils/
│       ├── constants.py        # 업종 컬럼명, 레이블, 코드 매핑
│       └── cluster_store.py    # 청크 로딩, 병렬 전처리, pkl 캐싱, 싱글톤
├── frontend/
│   ├── index.html
│   ├── css/style.css
│   └── js/app.js
├── scripts/
│   ├── ingest_cards.py         # PDF → ChromaDB 적재 (사전 1회 실행)
│   ├── evaluate.py             # 추천 파이프라인 성능 평가
│   └── pdfs/                   # 카드 상품 안내 PDF 보관 위치
└── .env.example

환경 설정

1. 패키지 설치

pip install -r requirements.txt

2. 환경변수 설정

.env.example을 복사해 .env를 만들고 값을 채웁니다.

# MySQL
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=password
MYSQL_DB=card_rec

# OpenAI
OPENAI_API_KEY=sk-...

# ChromaDB
CHROMA_PERSIST_DIR=./chroma_db
CHROMA_COLLECTION=card_benefits

# RAG 활성화 (카드 PDF 적재 완료 후 true로 변경)
RAG_ENABLED=false

3. 알고리즘 파라미터 조정

backend/parameters.py에서 모든 튜닝 값을 관리합니다.

KMEANS_N_CLUSTERS      = 100    # 클러스터 수 (전체 회원 수 / 목표 클러스터 크기)
RECALL_N_NEIGHBOR_CLUSTERS = 2  # 인접 클러스터 탐색 수
RANKING_TOP_K          = 30     # 코사인 유사도 상위 몇 명
GAP_ACTIVE_MIN_COUNT   = 5      # 활성 사용자 최소 인원 (미달 시 centroid 폴백)
GAP_MIN_THRESHOLD      = 0.01   # 최소 유의미한 갭 크기
GAP_TOP_N              = 3      # 최종 추천 업종 수
GAP_NEW_SECTOR_BONUS   = 0.05   # 미소비 업종 우선순위 가산점

실행 방법

서버 시작

uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000

최초 실행 시 MySQL에서 전체 데이터를 청크 로딩해 KMeans 학습 후 pkl로 저장합니다. 이후 재시작부터는 pkl을 로드해 수 초 내 완료됩니다.

브라우저 접속

http://localhost:8000

pkl 재학습이 필요한 경우 (파라미터 변경, 데이터 갱신)

del backend\models\*.pkl
uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000

RAG 설정 (선택)

카드 상품 PDF를 ChromaDB에 적재하면 실제 카드명과 혜택을 포함한 추천이 가능합니다.

1. PDF 준비

scripts/pdfs/ 폴더에 우리카드 상품 안내장 PDF를 넣습니다. 파일명이 카드명으로 사용됩니다. (예: 우리WON트래블.pdf)

2. 적재 실행

python -m scripts.ingest_cards

500자 청킹 후 품질 필터링(80자 미만, 한글 비율 15% 미만, 보일러플레이트 제거)을 거쳐 ChromaDB에 저장됩니다.

3. RAG 활성화

.env에서 RAG_ENABLED=true로 변경 후 서버를 재시작합니다.


성능 평가

python -m scripts.evaluate

다음 세 가지 항목을 측정합니다.

항목 지표 기준
클러스터링 품질 Silhouette Score (cosine) >0.5 우수
클러스터링 품질 Davies-Bouldin Index <0.5 우수
갭 분석 품질 Coverage (추천 업종이 1개 이상인 고객 비율) ≥80% 우수
RAG 검색 품질 업종별 ChromaDB 검색 성공률 10/10 우수

API 명세

GET /api/customer/{seq}

고객 구분 코드로 최신 분기 기준 고객 정보와 업종별 소비 내역을 반환합니다.

Response 200

{
  "seq": "123456789",
  "bas_yh": "2024Q4",
  "age": "30",
  "mbr_rk_label": "플래티넘",
  "life_stage_label": "신혼",
  "tot_use_am": 850,
  "macro_usage": [
    {"column": "FSBZ_AM", "label": "요식업", "amount": 210},
    {"column": "TRVLEC_AM", "label": "여행/레저/문화", "amount": 0}
  ]
}

POST /api/recommend

멀티 스테이지 추천 파이프라인을 실행하고 자연어 추천 결과를 반환합니다.

Request

{"seq": "123456789"}

Response 200

{
  "seq": "123456789",
  "similar_user_count": 28,
  "target_sectors": [
    {"column": "TRVLEC_AM", "label": "여행/레저/문화", "gap": 0.18, "is_new": true}
  ],
  "recommendation": "비슷한 소비 성향을 가진 고객들은 여행/레저 업종을...",
  "rag_used": false
}

주의 사항

  • MySQL DB는 읽기 전용입니다. SELECT 외 모든 쿼리는 금지합니다.
  • backend/models/*.pkl.gitignore 처리되어 있습니다. 환경마다 서버 최초 실행 시 재학습됩니다.
  • RAG_ENABLED=false(기본값)일 때 LLM은 카드 상품명과 혜택 수치를 생성하지 않습니다.
  • 고객 SEQ는 가명처리된 코드이며 실명 등 개인 식별 정보는 포함되지 않습니다.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages