TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 머신러닝>

[파이썬] 프로젝트 : 웹 페이지 구축 - 11(ML 모델 구현)

by 기록자_Recordian 2025. 3. 25.
728x90
반응형
이전 내용
 

[파이썬] 프로젝트 : 대시보드 웹 페이지 구축하기 - 10

이전 내용 [파이썬] 프로젝트 : 대시보드 웹 페이지 구축하기 - 9이전 내용 [파이썬] 프로젝트 : 대시보드 웹 페이지 구축하기 - 8이전 내용 [파이썬] 프로젝트 : 대시보드 웹 페이지 구축하기 - 7이

puppy-foot-it.tistory.com


예측을 위한 데이터셋 생성하기

 

필자가 부족해서 기존의 데이터셋으로 머신러닝에 활용하기가 한계가 있다고 느껴 새로운 데이터셋을 생성하기로 결정했다. 회원 중 이벤트에 참여한 1000명의 임의의 회원정보를 추출하여 해당 정보로

  • 나이 별로 어떤 유입 경로로 회원 가입을 많이 했는지 ▶ 타겟의 연령별 마케팅 채널 추천
  • 어떤 이벤트 진행 후 서비스 가입이 많아졌는지 
  • 특정 정보를 입력했을 때 이 사람이 서비스 구독을 할지 여부를 예측

등등 보다 다양한 분석과 예측을 할 수 있을거라는 생각이 들었다.

 

먼저 이를 위해 데이터셋을 생성한다.

# 나이, 도시에 대한 범위 설정
ages = range(25, 66)  # 나이 25세부터 65세
cities = range(0, 17)  # 17개 도시
bools = [0, 1]  # 이진값 (예/아니오, 남자/여자 등)
events = range(0, 7)  # 이벤트 7개
channels = range(0, 12) # 유입경로 12개
weights = [0.7, 0.3] # 가중치

# 결과 저장할 리스트
raw_data = {
    "age": [],          # 나이
    "gender": [],       # 성별
    "marriage": [],     # 혼인여부
    "city": [],         # 도시
    "channel": [],        # 가입 경로
    "before_ev": [],    # 이벤트 참여전 구독 여부
    "part_ev": [],      # 참여 이벤트
    "after_ev": [],     # 참여후 구독 여부
}

# 함수 정의
def fake_register():
    for _ in range(1000):
        age = random.choice(ages)
        gender = random.choice(bools)
        marriage = random.choice(bools)
        city = random.choice(cities)
        channel = random.choice(channels)
        before_ev = random.choice(bools)
        part_ev = random.choice(events)  # 1개 이상 7개 이하의 이벤트 선택
        after_ev = random.choices(bools, weights=weights, k=1)[0] # 가중치 부여

        # 데이터 추가
        raw_data['age'].append(age)
        raw_data['gender'].append(gender)
        raw_data['marriage'].append(marriage)
        raw_data['city'].append(city)
        raw_data['channel'].append(channel)
        raw_data['before_ev'].append(before_ev)
        raw_data['part_ev'].append(part_ev)
        raw_data['after_ev'].append(after_ev)

    print("데이터가 생성되었습니다.")

# 데이터 생성 함수 호출
fake_register()

# DataFrame 생성
data = pd.DataFrame(raw_data)

# 데이터 미리 보기
data.head()

※ 임의의 값을 생성 또는 선택 시에 가중치를 부여하면 특정 값이 더 잘 선택되도록 할 수 있다.

가상의 데이터이므로, 이벤트 참여 후 서비스 가입률이 상향됐다는 가정 하에 가입 값(0)에 더 높은 가중치를 부여한다.

 

▶ CSV 파일로 잘 저장되었고, 이제 이를 Streamlit으로 띄워보는 작업을 진행한다. (멀티 페이지에 추가)


Streamlit에 데이터프레임 띄우기

 

pages 폴더 내에 새로운 파일을 만들고 숫자를 지정한 뒤

 

깃허브에 올려둔 csv raw 파일 경로를 복사한 뒤 st.dataframe으로 불러와 home.py 파일을 실행하면

import pandas as pd

# CSV 파일 경로 설정 (깃허브)
CSV_FILE_PATH = 'https://raw.githubusercontent.com/.../main/'

memeber_df = pd.read_csv(CSV_FILE_PATH + 'members_data.csv')

st.dataframe(memeber_df)

 

그러나, 출력 시에 모든 값이 숫자로 나오기 때문에 해당 값이 어떤 값을 의미하는지 모르겠으므로, 숫자와 값을 mapping하여 값이 문자로 나타나는 데이터프레임이 출력되도록 수정해주고, expander 버튼을 삽입하여 데이터프레임이 필요치 않을 때는 접히도록 한다.

print_df = memeber_df.rename(columns={
     "age": "나이",
     "gender": "성별",
     "marriage": "혼인여부",
     "city": "도시",
     "channel": "가입경로",
     "before_ev": "참여_전",
     "part_ev": "참여이벤트",
     "after_ev": "참여_후"
})

# 데이터값 변경
print_df['성별'] = print_df['성별'].map({0:'남자', 1:'여자'})
print_df['혼인여부'] = print_df['혼인여부'].map({0:'미혼', 1:'기혼'})
print_df['도시'] = print_df['도시'].map({0:'부산', 1:'대구', 2:'인천', 3:'대전', 4:'울산', 5:'광주', 6:'서울', 
    7:'경기', 8:'강원', 9:'충북', 10:'충남', 11:'전북', 12:'전남', 13:'경북', 14:'경남', 15:'세종', 16:'제주'})
print_df['가입경로'] = print_df['가입경로'].map({0:"직접 유입", 1:"키워드 검색", 2:"블로그", 3:"카페", 4:"이메일", 
        5:"카카오톡", 6:"메타", 7:"인스타그램", 8:"유튜브", 9:"배너 광고", 10:"트위터 X", 11:"기타 SNS"})
print_df['참여_전'] = print_df['참여_전'].map({0:'가입', 1:'미가입'})
print_df['참여이벤트'] = print_df['참여이벤트'].map({0:"워크숍 개최", 1:"재활용 품목 수집 이벤트", 2:"재활용 아트 전시",
          3:"게임 및 퀴즈", 4:"커뮤니티 청소 활동", 5:"업사이클링 마켓", 6:"홍보 부스 운영"})
print_df['참여_후'] = print_df['참여_후'].map({0:'가입', 1:'미가입'})

with st.expander('회원 데이터'):
    st.dataframe(print_df, use_container_width=True)


머신러닝 모델 구현
탭1. 서비스 가입 여부 예측

 

우선, 탭을 세 개 만들고 입력받은 정보를 토대로 세 가지의 기계학습을 실행하는 기능을 넣는다.

  • 서비스가입 여부 예측
  • 추천 캠페인
  • 추천 채널
data = memeber_df[['age', 'gender', 'marriage', 'after_ev']]

tab1, tab2, tab3 = st.tabs(['서비스가입 예측', '추천 캠페인', '추천 채널'])

 

먼저, 첫번째 탭에는 나이, 성별, 혼인여부를 선택하면 랜덤포레스트를 통해 기존 데이터를 학습하여 입력된 정보에 따른 서비스 가입 여부를 예측한다. 

from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, classification_report
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

with tab1:
    first_column, second_column, thrid_columns = st.columns([6, 2, 2])
    with first_column:
        st.write("서비스가입 예측 모델입니다. 아래의 조건을 선택해 주세요.")
        ages_1 = st.slider(
            "연령대를 선택해 주세요.",
            25, 65, (35, 45)
        )
        st.write(f"**선택 연령대: :red[{ages_1}]세**")
    
    with second_column:
        gender_1 = st.radio(
            "성별을 선택해 주세요.",
            ["남자", "여자"],
            index=1
        )
    
    with thrid_columns:
        marriage_1 = st.radio(
            "혼인여부를 선택해 주세요.",
            ["미혼", "기혼"],
            index=1
        )
    
     # 예측 모델 학습 및 평가 함수
    def service_predict(data):
        # 데이터 전처리 및 파이프라인 설정
        numeric_features = ['age']
        categorical_features = ['gender', 'marriage']

        preprocessor = ColumnTransformer(
            transformers=[
                ('num', StandardScaler(), numeric_features),
                ('cat', OneHotEncoder(categories='auto'), categorical_features)
            ]
        )

        # 랜덤 포레스트 모델
        model = Pipeline(steps=[
            ('preprocessor', preprocessor),
            ('classifier', RandomForestClassifier(random_state=42, n_jobs=-1))  # n_jobs=-1로 모든 코어 사용
        ])

        # 데이터 분할
        X = data.drop(columns=['after_ev'])
        y = data['after_ev']
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

        # 하이퍼파라미터 튜닝을 위한 그리드 서치
        param_grid = {
            'classifier__n_estimators': [100, 200],  # 늘리면 성능 향상 가능
            'classifier__max_depth': [None, 10, 20],
            'classifier__min_samples_split': [2, 5]
        }

        grid_search = GridSearchCV(estimator=model, param_grid=param_grid, cv=3, n_jobs=-1, verbose=2)
        grid_search.fit(X_train, y_train)

        # 최적의 모델로 예측 수행
        y_pred = grid_search.predict(X_test)

        # 성능 평가
        accuracy = accuracy_score(y_test, y_pred)
        st.write(f"이 모델의 테스트 정확도는 {accuracy * 100:.1f}% 입니다.")

        return grid_search.best_estimator_, grid_search.best_estimator_.named_steps['classifier'].feature_importances_

    # 사용자가 입력한 값을 새로운 데이터로 변환
    def pre_result(model, new_data):
        prediction = model.predict(new_data)
        st.write(f"**모델 예측 결과: :rainbow[{'가입' if prediction[0] == 0 else '미가입'}]**")

    # 특성 중요도 시각화
    def plot_feature_importance(importances, feature_names):
        indices = np.argsort(importances)[::-1]
        plt.figure(figsize=(2, 1))
        plt.title("특성 중요도")
        plt.barh(range(len(importances)), importances[indices], align="center")
        plt.yticks(range(len(importances)), [feature_names[i] for i in indices])
        plt.xlabel("중요도")
        st.pyplot(plt)

    # 예측하기 버튼 클릭에 따른 동작
    if st.button("예측하기"):
        # 기존 데이터로 모델 학습
        model, feature_importances = service_predict(data)

        # 입력된 값을 새로운 데이터 형식으로 변환
        new_data = pd.DataFrame({
            'age': [(ages_1[0] + ages_1[1]) / 2],  # 나이의 중앙값
            'gender': [1 if gender_1 == '여자' else 0],  # 성별 인코딩
            'marriage': [1 if marriage_1 == '기혼' else 0]  # 혼인 여부 인코딩
        })

        # 예측 수행
        pre_result(model, new_data)

        # 특성 중요도 시각화
        feature_names = ['age'] + list(model.named_steps['preprocessor'].transformers_[1][1].get_feature_names_out())
        plot_feature_importance(feature_importances, feature_names)

▶ 모델 정확도가 65.5% 로 높지 않은 편인데, 모델 정확도를 높이기 위해 모델을 여럿 추가했으나 예측하는 데 시간이 너무 오래 걸려 랜덤포레스트 하나만 사용하기로 했다.


머신러닝 모델 구현
탭2. 캠페인 추천

 

이번에는 사용자 정보를 입력받으면, 해당 정보를 토대로 서비스 가입 확률을 높일 수 있는 캠페인을 추천해주는 모델을 구현해 본다.

데이터를 입력받는 방식은 탭1과 같다. 그러나, 슬라이더를 사용할 경우 그냥 사용해 버리면 중복 때문에 에러를 발생하므로, 꼭 매개변수에 key 값을 지정해줘야 한다.

ages_2 = st.slider(
    "연령대를 선택해 주세요.",
    25, 65, (35, 45),
    key='slider_2'
)
st.write(f"**선택 연령대: :red[{ages_2}]세**")

 

현재 이벤트 변수(part_ev)는 모두 정수형 값으로 되어 있기 때문에, 추후 추천 서비스 추출 시 이벤트명이 출력될 수 있도록 각 숫자와 이름을 매핑해준다.

여기서도 마찬가지로 랜덤포레스트를 사용해 모델을 학습 및 예측한다.

그리고 효과적인 이벤트를 추천하는 버튼을 누르면 모델이 동작하며 가입 가능성이 가장 높은 이벤트를 반환하고 출력한다.

data_2 = memeber_df[['age', 'gender', 'marriage', 'part_ev', 'after_ev']]

# 참여 이벤트 매핑
event_mapping = {
    0: "워크숍 개최",
    1: "재활용 품목 수집 이벤트",
    2: "재활용 아트 전시",
    3: "게임 및 퀴즈",
    4: "커뮤니티 청소 활동",
    5: "업사이클링 마켓",
    6: "홍보 부스 운영"
}

with tab2: # 캠페인 추천 모델
    first_column, second_column, thrid_columns = st.columns([6, 2, 2])
    with first_column:
        st.write("캠페인 추천 모델입니다. 아래의 조건을 선택해 주세요.")
        ages_2 = st.slider(
            "연령대를 선택해 주세요.",
            25, 65, (35, 45),
            key='slider_2'
        )
        st.write(f"**선택 연령대: :red[{ages_2}]세**")
    
    with second_column:
        gender_2 = st.radio(
            "성별을 선택해 주세요.",
            ["남자", "여자"],
            index=1,
            key='radio2_1'
        )
    
    with thrid_columns:
        marriage_2 = st.radio(
            "혼인여부를 선택해 주세요.",
            ["미혼", "기혼"],
            index=1,
            key='radio2_2'
        )

    # 추천 모델 함수
    def recommend_event(data_2):
        # X, y 설정
        X = data_2[['age', 'gender', 'marriage', 'part_ev']]
        y = data_2['after_ev']

        # 더미 변수 생성하여 참여 이벤트 인코딩
        X = pd.get_dummies(X, columns=['part_ev'], drop_first=True)

        # 데이터 분할
        X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.2, random_state=42)

        # 렌덤 포레스트 모델 정의 및 학습
        model = RandomForestClassifier(random_state=42)
        model.fit(X_train, y_train)

        return model, X_train.columns  # 모델과 피쳐 이름을 반환

    # 사용자 정보 입력을 통한 추천 이벤트 평가
    if st.button("효과적인 이벤트 추천하기"):
        # 추천 모델 훈련
        model, feature_names = recommend_event(data_2)

        event_results = {}

        # 각 이벤트에 대한 추천 가능성 평가
        for event in range(7):  # part_ev가 0에서 6까지의 숫자이므로, 7개의 이벤트에 대해 반복
            # 새로운 사용자 정보 세팅
            new_user_data = pd.DataFrame({
                'age': [(ages_2[0] + ages_2[1]) / 2],  # 연령대의 중앙값
                'gender': [1 if gender_2 == '여자' else 0],  # 성별 인코딩
                'marriage': [1 if marriage_2 == '기혼' else 0],  # 혼인 여부 인코딩
                'part_ev': [event]  # 번호로 매핑된 이벤트
            })

            # 더미 변수 생성
            new_user_data = pd.get_dummies(new_user_data, columns=['part_ev'], drop_first=True)

            # 피쳐 정렬
            new_user_data = new_user_data.reindex(columns=feature_names, fill_value=0)

            # 예측 수행
            prediction = model.predict(new_user_data)
            event_results[event] = prediction[0]  # 가입 여부 저장 (0: 가입, 1: 미가입)

        # 가입(0) 가능성이 높은 이벤트 중 가장 높은 것
        possible_events = {event: result for event, result in event_results.items() if result == 0} 

        if possible_events:
            best_event = max(possible_events, key=possible_events.get)
            st.write(f"**추천 이벤트: :violet[{event_mapping[best_event]}] 👈 이벤트가 가장 효과적입니다!**")
        else:
            st.write("추천 이벤트: 가입 확률이 높지 않으므로 다른 캠페인을 고려해보세요.")


머신러닝 모델 구현
탭3. 온라인 마케팅 채널 추천

 

이번엔 같은 정보를 입력 받으면 해당 정보를 토대로 서비스 가입자수를 효과적으로 높일 수 있는 마케팅 채널을 추천해주는 모델을 만든다. 2번탭과 비슷한 맥락으로 진행하면 되는데, 여기서는 추천 채널을 3개로 늘리고, 직접 유입과 같은 일부 채널은 제외시키도록 한다. 

data_3 = memeber_df[['age', 'gender', 'marriage', 'channel', 'after_ev']]

# 가입 시 유입경로 매핑
register_channel = {
    0:"직접 유입",
    1:"키워드 검색",
    2:"블로그",
    3:"카페",
    4:"이메일",
    5:"카카오톡",
    6:"메타",
    7:"인스타그램",
    8:"유튜브", 
    9:"배너 광고", 
    10:"트위터 X", 
    11:"기타 SNS"
}

with tab3: # 마케팅 채널 추천 모델
    col1, col2, col3 = st.columns([6, 2, 2])
    with col1:
        st.write("마케팅 채널 추천 모델입니다. 아래의 조건을 선택해 주세요")
        ages_3 = st.slider(
            "연령대를 선택해 주세요.",
            25, 65, (35, 45),
            key='slider_3'
        )
        st.write(f"**선택 연령대: :red[{ages_3}]세**")
    
    with col2:
        gender_3 = st.radio(
            "성별을 선택해 주세요.",
            ["남자", "여자"],
            index=1,
            key='radio3_1'
        )
    
    with col3:
        marriage_3 = st.radio(
            "혼인여부를 선택해 주세요.",
            ["미혼", "기혼"],
            index=1,
            key='radio3_2'
        )

        # 추천 모델 함수
    def recommend_channel(data_3):
        # X, y 설정
        X = data_3[['age', 'gender', 'marriage', 'channel']]
        y = data_3['after_ev']

        # 더미 변수 생성하여 유입 채널 인코딩
        X = pd.get_dummies(X, columns=['channel'], drop_first=True)

        # 데이터 분할
        X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.2, random_state=42)

        # 렌덤 포레스트 모델 정의 및 학습
        model = RandomForestClassifier(random_state=42)
        model.fit(X_train, y_train)

        return model, X_train.columns  # 모델과 피쳐 이름을 반환

    # 사용자 정보 입력을 통한 추천 이벤트 평가
    if st.button("효과적인 마케팅 채널 추천받기"):
        # 추천 모델 훈련
        model, feature_names = recommend_channel(data_3)

        channel_results = {}

        # 각 이벤트에 대한 추천 가능성 평가
        for channel in range(12):  # part_ev가 0에서 12까지의 숫자이므로, 12개의 이벤트에 대해 반복
            if channel in (0,1): # 직접 유입과 키워드 검색 채널 제외
                continue

            # 새로운 사용자 정보 세팅
            new_user_data = pd.DataFrame({
                'age': [(ages_2[0] + ages_2[1]) / 2],  # 연령대의 중앙값
                'gender': [1 if gender_2 == '여자' else 0],  # 성별 인코딩
                'marriage': [1 if marriage_2 == '기혼' else 0],  # 혼인 여부 인코딩
                'channel': [channel]  # 번호로 매핑된 채널
            })

            # 더미 변수 생성
            new_user_data = pd.get_dummies(new_user_data, columns=['channel'], drop_first=True)

            # 피쳐 정렬
            new_user_data = new_user_data.reindex(columns=feature_names, fill_value=0)

            # 예측 수행
            prediction = model.predict(new_user_data)
            channel_results[channel] = prediction[0]  # 가입 여부 저장 (0: 가입, 1: 미가입)

        # 가입(0) 가능성이 높은 채널 중 가장 높은 것 3개
        possible_channels = {channel: result for channel, result in channel_results.items() if result == 0} 

        if possible_channels:
            best_channels = sorted(possible_channels.keys(), key=lambda x: possible_channels[x])[:3]  # 가장 좋은 3개 채널
            recommended_channels = [register_channel[ch] for ch in best_channels]
            st.write(f"**추천 마케팅 채널:** :violet[{', '.join(recommended_channels)}] 👈 이 채널들이 가장 효과적입니다!")
        else:
            st.write("추천 마케팅 채널: 가입 확률이 높지 않으므로 다른 채널을 고려해보세요.")

 

이제 깃허브에 들어가서 새로운 파일을 포함하여 전체 파일을 다시 한 번 업로드해 준다.

 

Streamlit에서 적용이 잘 된다.

 

이제 어제 해결하지 못한 Streamlit 클라우드에 DB 연동을 해결해 볼 차례다.

 

▶ 교수님이 알려주시기로는 Streamlit 클라우드에서 무료로 서버를 사용할 수 있다고 했는데, 잘못 알려주신 거 같다. AWS의 RDS를 이용하면 DB를 사용할 수 있으나, 해당 프로젝트는 제출용이므로 작업 시에는 파일을 DB로 불러오지 않고 CSV로 받아오는 형식으로 바꿔서 진행하기로 변경하였다.


다음 내용

 

[파이썬] 프로젝트 : 웹 페이지 구축 - 12(보완 및 재배포)

이전 내용 [파이썬] 프로젝트 : 웹 페이지 구축 - 11(ML 모델 구현)이전 내용 [파이썬] 프로젝트 : 대시보드 웹 페이지 구축하기 - 10이전 내용 [파이썬] 프로젝트 : 대시보드 웹 페이지 구축하기 - 9

puppy-foot-it.tistory.com

728x90
반응형