ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [LG Aimers] 영업성공률 예측 모델 개발기 -전처리를 중심으로
    전공&대외활동 이야기/2024 LG AIMERS 4기 활동 2024. 3. 6. 21:12

    이번에는 팀에서 어떤 방식으로 데이터들을 분석했고, 전처리를 했는지에 관해 작성해보려고한다. 

    핵심적인 내용을 정리하면 아래와 같다. 

    1. label data가 imbalance한지 확인하자: 대다수의 최근 모델들은 이에 대응하는 하이퍼파라미터가 존재한다
    2. Feature에 따라 모델을 잘 선정하자: 범주형이 많았기에 CatBoost 모델을 이용. XGBoost 이용시 보다 눈에 띄게 성능이 증가했다. 
    3. 데이터 전처리에 있어서 주어진 데이터에만 한정하지 말자: product 등 다양한 데이터들은 여러가지 방면으로 해석가능하다(eg. 회사 사이트의 제품 카테고리를 이용)
    4. 모델의 결과를 분석하자: 어떤 feature가 어느정도의 영향을 미쳤는지 확인하고, 주요도가 떨어지는 특성은 제거하거나, 주요도가 높은 feature는 해석에 더 주의해보자 

    데이터 분포 확인

    우선 현재 목표가 마케팅 전환이 되었는지 아닌지를 나타내는 지표 is_converted를 예측하는, 모델로는 분류하는 과제를 수행하는 것이었기에, 데이터의 분표현황 & null 데이터를 확인해보았다. 

    df['is_converted'].value_counts()
    is_converted
    False    54449
    True      4850
    Name: count, dtype: int64

     

    그 결과 false 데이터가 거의 10배 정도 많은 데이터 셋이었고, 즉, imbalanced한 데이터셋이라는 사실을 확인 가능했다. 

    (이는 차후 모델의 파라미터 중 일부에 활용되는 데이터로 사용)

     

    추가적으로 각 feature별로 null 데이터의 수를 확인한 결과 데이터의 대다수가 null인 데이터가 존재하는 것을 확인했다. 

    또한 데이터의 각 feature들이 대다수 범주형 데이터로 구성되어있으며, 숫자형, 문자열(상품명 등) 데이터가 있는 것을 확인할 수 있었다. 숫자형의 데이터의 경우 추가적인 조작을 가하지는 않았고, 범주형과 문자열에 집중하여 전처리를 진행했다. 또한 범주형 데이터 셋의 대다수이기에 여러가지 Boosting 모델 중 범주형에 특화된 catboost를 사용하였다. 

     

    특히 범주형 및 문자열 데이터의 경우 아래와 같은 과정을 거쳤다.

    0. Translate

    대다수의 문자형 데이터들은 영어로 작성되어있었으나, 몇몇은 불어, 아랍어 등 다양한 언어로 작성되어있었다. 

    이를 위해 google translate api를 이용하여 모두 영어로 번역하는 과정을 1차로 거쳤다. 

    주의할 점은 번역 api 호출횟수가 하루에 정해져있다는 것이다. 해당 호출횟수를 모두 이용하면 번역에서 error 메세지가 출력된다(이번 우리팀에서는 꼭 필요한 때에만 번역함수를 호출하여 호출횟수를 최소화하고, 제출 때 위주로 번역함수를 실행하였다)

    코드는 아래와 같으며 중간에 특수문자를 제거하고, 소문자로 통일하는 과정이 포함되어있다. 

    from googletrans import Translator
    #영어 번역함수 & 소문자로 통일, _ 공백처리 & 특수문자제거
    def translate_column(df, column_name):
        print("translate not working")
        translator = Translator()
        regex = re.compile('[^a-zA-Z ]')  # 특수문자 제거
        df[column_name] = df[column_name].str.lower().str.replace('_', ' ').apply(lambda x: regex.sub('', str(x)) if pd.notna(x) else x)
        df[column_name] = df[column_name].replace(r'^\s*$', np.nan, regex=True)  # 공백을 NaN으로 대체
        count = df[column_name].size
        trans = []
        translated_cache = {} #번역된 단어를 캐시할 dict
        count = df[column_name].size
        max_retries = 2  # 최대 재시도 횟수
        for i in range(count):
            before = df[column_name][i]
            if pd.isna(before):
                trans.append(np.nan)
                continue
            elif before[0].isnumeric():
                trans.append(before)
                continue
    
            # 캐시에 번역된 단어가 있는지 확인하고, 없으면 번역 수행
            if before in translated_cache:
                after_text = translated_cache[before]
            else:
                retry = 0
                while retry < max_retries:
                    try:
                        after = translator.translate(before, dest='en')
                        after_text = after.text
                        translated_cache[before] = after_text  # 번역 결과를 캐시에 저장
                        print(i, ":  ", before, '   --->   ', after_text)
                        break
                    except Exception as e:
                        print(f"Error occurred: {e}")
                        print(f"Retrying... Attempt {retry + 1}")
                        retry += 1
                        after_text = before  # 예외 발생 시 기본값 설정
    
            trans.append(after_text)
    
        df[column_name] = trans

     

    1. Data Normaize 

    자연어를 전처리하는 과정에서 보통 사용하는 용어로, 비슷한 데이터들을 묶는 과정이다. 예를 들어 먹다, 먹었다, 먹는다, 섭취하다, 식사를 하다 등 먹다라는 것에 해당하는 단어를 모두 eat라는 동일한 카테고리로 묶는 과정이다. 

    이때, 특정한 라이브러리를 이용할 수도 있으며, 이번의 경우 몇몇 자연어들은 라이브러리를 이용하였고, 대다수는 하나씩 카테고리를 일일히 지정하는 함수를 작성하여 적용했다. 

     

    2. Word Embedding

    이번 데이터 중 특정 범주로 지정하기에 매우 어려운, 다양한 단어들이 들어있는 데이터들이 존재하기도 했다. 이런 데이터를 처리하기 위해 일부 자연어 feature는 HuggingFace를 이용해 word Embedding vector로 치환한 후 다시 묶는 방법을 사용하려고 했지만, 성능이 좋지 않아 자연어 데이터를 어떻게 처리해야하는지는 여러 자료를 추가로 확인할 필요가 있을 것 같다. 

    (열심히 코드를 작성했지만, 결국 단순히 범주형으로 처리했다..😂)

    그래도 일부 함수를 첨부한다. 

    이전 자연어 처리방법에서와 같이 불용어를 제거하고, PorterStemmer를 이용하여 Normalize를 1차적으로 진행하였다. 이때 PorterStemmer는 같은 의미를 가지고 있는 명사, 동사, 형용사 등을 하나로 묶어주는 역할을 한다. 

    # 불용어 제거 & Normalize 1차(PorterStemmer이용)
    nltk.download('stopwords')
    stop_words = set(stopwords.words('english'))
    stemmer = PorterStemmer()
    
    def remove_stopwords(df, column_name):
        #str로 변경 egex에 해당하지 않는 문자는 공백으로 치환
        regex = re.compile('[^a-zA-Z ]')  # 특수문자 제거
        df[column_name] = df[column_name].str.lower().str.replace('_', ' ').apply(lambda x: regex.sub('', str(x)) if pd.notna(x) else x)
        df[column_name] = df[column_name].replace(r'^\s*$', np.nan, regex=True)  # 공백을 NaN으로 대체
        df[column_name] = df[column_name].apply(lambda x: ' '.join([stemmer.stem(word) for word in str(x).split() if pd.notna(x) and str(word).lower() not in stop_words]) if pd.notna(x) else x)
        return df
        
    def etc_norm(df, column_name):
      df.loc[df[column_name].str.contains('etc|other|others', case=False, na=False), column_name] = 'others'
      return df

     

    또한 Bert를 이용하여 토큰화 및 단어를 vector로 변형하는 코드는 아래와 같다. 

    #Embeeding using Pretrained 모델
    # DistilBERT 토크나이저와 모델 로드
    tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')
    bert = DistilBertModel.from_pretrained('distilbert-base-uncased')
    bert = bert.to(device)
    # 이미 인코딩된 결과를 저장할 딕셔너리
    encoded_cache = {}
    # 새로운 dataframe을 생성
    def embed_bert(df, column_name):
        # 인코딩된 결과를 저장할 딕셔너리
        encodings = {}
        new_df = pd.DataFrame()
        # 각 행에 대해 문장을 인코딩
        for i, sentence in enumerate(df[column_name]):
            # 이미 인코딩된 결과가 있는 경우 해당 결과를 사용
            if sentence in encoded_cache:
                sentence_encoding = encoded_cache[sentence]
            else:
                # NaN이면 문장을 모두 0으로 처리
                if pd.isna(sentence):
                    sentence_encoding = [0] * 768  # BERT의 hidden state 크기는 768입니다.
                else:
                    # 문장을 토큰화하고 특수 토큰 추가
                    sentence = str(sentence)
                    inputs = tokenizer(sentence, return_tensors="pt", padding=True, truncation=True).to(device)
    
                    # BERT 모델에 입력값을 전달하여 인코딩
                    with torch.no_grad():  # 그래디언트 추적 중지
                        outputs = bert(**inputs)
    
                    # [CLS] 토큰의 hidden state를 가져와서 문장을 인코딩
                    sentence_encoding = outputs.last_hidden_state[:, 0, :].squeeze().detach().cpu().numpy()
    
                # 인코딩된 결과를 캐시에 저장
                encoded_cache[sentence] = sentence_encoding
    
            # 인코딩된 결과를 딕셔너리에 저장
            for j, val in enumerate(sentence_encoding):
                col_name = f"{column_name}_{j}"
                if col_name not in encodings:
                    encodings[col_name] = []
                encodings[col_name].append(val)
    
        # 인코딩된 결과를 새로운 데이터프레임으로 변환하여 반환
        new_df = pd.DataFrame(encodings)
        return new_df

     

    추가적으로 vetor로 변환한 결과를 이용해 단어를 군집화하려고 시도했던 코드는 아래와 같다. 최종제출한 모델에서는 해당 부분의 코드 실행 시간이 매우 길고 효과가 좋지 않아 사용하지는 않았다. 

    from sklearn.cluster import AgglomerativeClustering
    
    def cluster_df(df, target_df, cluster_name, sample_num):
       # 벡터 데이터를 하나의 벡터로 결합합니다.
        X = df.values
        # 군집 모델
        clustering_model = AgglomerativeClustering(n_clusters=sample_num)
        cluster_labels = clustering_model.fit_predict(X)
    
        # 군집 결과를 데이트프레임에 추가합니다.
        target_df[cluster_name] = cluster_labels
    
    # 함수 사용 예시
    # df = cluster_dataframe(df, 'product_subcategory', 5)

     

    범주형 normalize하기 

    2 customer country나 product category 등 일부 범주형 데이터의 경우 일일히 전처리해주는 과정을 거쳤다.

    def preprocess_customer_country(df):
        # 주어진 지역 리스트를 국가로 매핑하는 딕셔너리
        region_to_country = {
            'united states': ['us', 'usa', 'new hampshire', 'ny', 'ee uu', 'ohio', 'ca', 'california', 'nevada', 'jacksonville florida', 'new york', 'los angeles', 'chicago', 'houston', 'phoenix', 'philadelphia', 'san antonio', 'san diego', 'dallas', 'austin', 'jacksonville', 'san jose', 'fort worth', 'columbus', 'charlotte', 'indianapolis', 'san francisco', 'seattle', 'denver', 'oklahoma city', 'nashville', 'el paso', 'washington', 'las vegas', 'boston', 'portland', 'louisville', 'memphis', 'detroit', 'baltimore', 'milwaukee', 'albuquerque', 'tucson', 'fresno', 'sacramento', 'mesa', 'kansas city', 'atlanta', 'colorado springs', 'omaha', 'raleigh', 'virginia beach', 'long beach', 'miami', 'oakland', 'minneapolis', 'tulsa', 'bakersfield', 'tampa', 'wichita', 'arlington', 'aurora', 'new orleans', 'cleveland', 'anaheim', 'honolulu', 'henderson', 'stockton', 'riverside', 'lexington', 'corpus christi', 'orlando', 'irvine', 'cincinnati', 'santa ana', 'newark', 'saint paul', 'mesa', 'buffalo', 'gilbert', 'reno', 'madison', 'fort wayne', 'toledo', 'lubbock', 'st. petersburg', 'laredo', 'irving', 'chesapeake', 'glendale', 'winston-salem', 'scottsdale', 'garland', 'boise', 'norfolk', 'port st. lucie', 'spokane', 'richmond', 'fremont', 'huntsville'],
            'india': ['anand vihar delhi', 'ahmedabad', 'ajmer', 'amravati', 'amreli', 'anand', 'ankleshwar', 'bahrain', 'ballia', 'banda', 'bangalore', 'bharuch', 'bhopal', 'bhubaneswar', 'bhilwara', 'bilaspur', 'bulandshahr', 'bundi', 'chandrapur', 'chennai', 'cooch behar', 'darbhanga', 'dewas', 'dhanbad', 'farrukhabad', 'fatehpur', 'firozabad', 'gandhinagar', 'gangtok', 'ghaziabad', 'godhra', 'gorakhpur', 'gurgaon', 'gujarat', 'guwahati', 'hajipur', 'hanumangarh', 'himachal', 'howrah', 'hyderabad', 'imphal', 'indore', 'itarsi', 'jabalpur', 'jagdalpur', 'jaipur', 'jaisalmer', 'jalandhar', 'jamnagar', 'jamshedpur', 'jhansi', 'jodhpur', 'kalyan-dombivali', 'kanpur', 'karaikal', 'kashipur', 'kekri', 'kerala', 'kerela', 'kishangarh', 'kolkata', 'kota', 'kuala lumpur', 'kurnool', 'lucknow', 'madurai', 'maharashtra', 'mangalore', 'mapusa', 'mumbai', 'muzaffarpur', 'mysuru', 'nagapattinam', 'nagaur', 'nagercoil', 'nagpur', 'nainital', 'nashik', 'navi mumbai', 'navsari', 'nd', 'noida', 'nurmahal', 'odisha', 'ongole', 'palakkad', 'patiala', 'patna', 'pune', 'raebareli', 'raipur', 'rajgarh', 'rajkot', 'rajpura', 'ranchi', 'ratlam', 'ratnagiri', 'rewa', 'rohtak', 'roorkee', 'shimla', 'siliguri', 'sinnar', 'siwan', 'solan', 'solapur', 'somnath', 'surat', 'surendranagar', 'tezpur', 'telangana', 'thane', 'thiruvananthapuram', 'tirupati', 'udaipur', 'ujjain', 'unjha', 'uttar pradesh', 'vadodara', 'varanasi', 'vellore', 'veraval', 'vidisha', 'vijayawada', 'visakhapatnam', 'warangal', 'wardha', 'yavatmal'],
            'australia': ['sydney', 'melbourne', 'brisbane', 'perth', 'adelaide', 'gold coast', 'canberra', 'newcastle', 'central coast', 'wollongong', 'hobart', 'geelong', 'townsville', 'cairns', 'darwin', 'toowoomba', 'ballarat', 'bendigo', 'albury', 'wodonga', 'launceston', 'mackay', 'rockhampton', 'bunbury', 'coffs harbour', 'bundaberg', 'wagga wagga', 'hervey bay', 'mildura', 'shepparton', 'port macquarie', 'gladstone', 'sunshine coast', 'geraldton', 'kalgoorlie', 'tweed heads', 'mount isa', 'lismore', 'whyalla', 'griffith', 'maryborough', 'echuca', 'moranbah', 'gympie', 'dalby', 'innisfail', 'deniliquin', 'katherine', 'gunnedah', 'bowen', 'stawell', 'port hedland', 'yamba', 'kingaroy', 'yeppoon', 'emerald', 'swan hill', 'ulladulla', 'horsham', 'charters towers', 'leeton', 'roebourne', 'narrabri', 'thargomindah', 'monto', 'windorah', 'barcaldine'],
            'colombia' : ['barranquilla', 'bucaramanga', 'foz de iguaçu ', 'cartagena'],
            'spain' : ['alicante', 'gran canarias playa del ingles', 'caceres', 'valencia', 'madrid'],
            'antigua and barbuda' : ['antigua'],
            'brazil' : ['rj', 'rio de janeiro', 'br', 'aparecida', 'belo horizonte', 'capão da canoa', 'pinheiros', 'dourados', 'joão pessoa', 'cuiabá', 'manaus', 'recife'],
            'vietnam' : ['ha noi'],
            'turkey' : ['türkiye'],
            'united kingdom' : ['british virgin islands', 'cayman islands', 'gibraltar'],
            'venezuela' : ['isla de margarita'],
            'philippines' : ['manila', 'makati']
        }
        df['customer_country'] = df['customer_country'].str.split('/').str[-1].str.strip().str.lower().replace('', np.nan)
        df['customer_country'] = np.where(df['customer_country'].str.contains('@'), np.nan, df['customer_country']) # '@'가 들어간 값은 모두 결측치로 변경
        df['customer_country'] = df['customer_country'].str.split(',').str[-1].str.strip().str.lower().replace('', np.nan)
        df['customer_country'] = df['customer_country'].str.replace('.', '')
        df.loc[df['customer_country'].str.contains('united states|usa|massachusetts|road', case=False, na=False), 'customer_country'] = 'united states' # 'united states'를 포함하는 행 찾아 'United States'로 바꾸기
        df.loc[df['customer_country'].str.contains('sao paulo|são paulo', case=False, na=False), 'customer_country'] = 'brazil'
        df.loc[df['customer_country'].str.match(r'^\d', na=False), 'customer_country'] = 'united states' # 숫자로 시작하는 행 찾아 'united states'로 바꾸기 (미국 주소)
        df.loc[df['customer_country'].str.match(r'.*\d{5}$', na=False), 'customer_country'] = 'united states' # 숫자로 끝나는 행 찾아 'united states'로 바꾸기 (미국 우편 주소)
        df.loc[df['customer_country'].str.startswith('us', na=False), 'customer_country'] = 'united states' # 'us'로 시작하는 행 찾아 통일 시키기
        df.loc[df['customer_country'].str.startswith('uae', na=False), 'customer_country'] = 'uae' # 'uae'로 시작하는 행 찾아 통일 시키기
        df.loc[df['customer_country'].str.startswith('colombia', na=False), 'customer_country'] = 'colombia'
        df['customer_country'] = df['customer_country'].replace(['a', 'country'], np.nan)
        df['customer_country'] = np.where(df['customer_country'].str.contains('\d'), np.nan, df['customer_country'])
    
        for country, regions in region_to_country.items():
            df.loc[df['customer_country'].isin(regions), 'customer_country'] = country

     

    특히 product catergory의 경우 임의로 변환하는 것이 아닌 이번 데이터가 LG의 B2B 관련 상품에 관한 데이터였던만큼, 제품의 카테고리가 적혀있는 LG의 사이트를 방문, 해당 사이트의 정보를 기준으로 대표적인 제품들을 선정 전처리해주었다. 

    def normalize_product(df):
        df['product_category'] = df['product_category'].apply(lambda x: ' '.join([word for word in str(x).split(' ') if word.lower() != 'lg']))
        df['product_category'] = np.where(df['product_category'].str.contains('vrf'), 'vrf', df['product_category'])
        df.loc[df['product_category'].str.startswith('chiller', na=False), 'product_category'] = 'chiller'
        df['product_category'] = np.where(df['product_category'].str.contains('signag'), 'signag', df['product_category'])
        df.loc[df['product_category'].str.endswith('tv', na=False), 'product_category'] = 'tv'
        df.loc[df['product_category'].str.startswith('commerci', na=False), 'product_category'] = 'commerci'
        df.loc[df['product_category'].str.startswith('moniormonitor', na=False), 'product_category'] = 'monitor'
        df.loc[df['product_category'].str.startswith('multi v', na=False), 'product_category'] = 'multi v'
        df.loc[df['product_category'].str.startswith('system a', na=False), 'product_category'] = 'system ac'
        df.loc[df['product_category'].str.startswith('solar', na=False), 'product_category'] = 'solar'
        df.loc[df['product_category'].str.startswith('multisplit', na=False), 'product_category'] = 'multi split'
        df.loc[df['product_category'].str.startswith('projector', na=False), 'product_category'] = 'projector'
        df.loc[df['product_category'].str.endswith('display', na=False), 'product_category'] = 'monitor'
        df.loc[df['product_category'].str.endswith('monitor', na=False), 'product_category'] = 'monitor'

    숫자형에서 범주형으로 변환

    고객의 요청의 길이가 데이터로 주어졌었는데, 해당 데이터의 경우 최댓값과 최솟값을 받아 길이에 따라 4개의 범주로 범주형 데이터로 변환하는 과정을 거쳤었다. 해당 코드는 아래와 같다. 

     # 최대값과 최소값 확인
    max_value = df_train['lead_desc_length'].max()
    min_value = df_train['lead_desc_length'].min()
    
    # 범주 간격 계산
    interval = (max_value - min_value) / 5
    
    # 범주별 구간화 함수 정의
    def categorize_length(length):
        if length <= min_value + interval:
            return 'Very Short'
        elif length <= min_value + interval * 2:
            return 'Short'
        elif length <= min_value + interval * 3:
            return 'Medium'
        elif length <= min_value + interval * 4:
            return 'Long'
        else:
            return 'Very Long'
    
    # 'lead_desc_length' 컬럼 구간화 적용
    df_train['lead_desc_length'] = df_train['lead_desc_length'].apply(categorize_length)
    df_test['lead_desc_length'] = df_test['lead_desc_length'].apply(categorize_length)

     

    모델학습 및 결과 확인

    CatBoost의 경우 범주형 데이터를 따로 모델 내에서 구분하여 처리하기 때무에 범주형 데이터에 해당하는 list를 생성하여 모델에 넘겨주어야한다. 이때 Catergory type이 아닌 object type으로 넘겨주어야 에러가 발생하지 않는다. 

    또한 na 데이터의 경우에도 nan이라는 특정 문자열로 변환하여 넘겨주었다. 

    #범주형 설정
    label_columns = ['customer_country', 'business_unit', 'customer_type',
           'enterprise', 'lead_desc_length', 'inquiry_type', 'product_category',
           'customer_position', 'response_corporate', 'expected_timeline',
           'business_area', 'business_subarea',
           'product_bigcategory', 'industry_category', 'customer_idx', 'lead_owner']
           
     for col in label_columns:
        df_train[col].fillna('nan', inplace=True)
        df_test[col].fillna('nan', inplace=True)

     

    이후 모델 학습을 진행한 후 모델에서 어떤 feature가 주요하게 작용했는지 확인했다. 

    이때 앞서 밝힌 것과 같이 imbalanced data를 다루기 위해 모델의 파라미터 중 scale_pose_weight를 설정해주었다(처음에는 10으로 설정했으나, 이후 조정하였다)

    x_train, x_val, y_train, y_val = train_test_split(
        df_train.drop("is_converted", axis=1),
        df_train["is_converted"],
        test_size=0.2,
        shuffle=True,
        random_state=400,
    )
    
    from catboost import CatBoostClassifier, Pool
    
    model = CatBoostClassifier(
        iterations=500,  # 반복 횟수를 100으로 설정합니다. 적절한 값으로 조정해야 합니다.
        learning_rate=0.06,
        loss_function='Logloss',  # 손실 함수 설정
        eval_metric='F1',  # 평가 메트릭 설정
        scale_pos_weight=7,  # Scale_Pos_Weight 설정
        depth=4,
        l2_leaf_reg=10,
        random_seed=42
    )
    model.fit(x_train.fillna(0), y_train, cat_features=label_columns, verbose=10, eval_set=(x_val, y_val))

     

    모델의 성능은 아래 함수를 통해 확인하였다. 

    def get_clf_eval(y_test, y_pred=None):
        confusion = confusion_matrix(y_test, y_pred, labels=[True, False])
        accuracy = accuracy_score(y_test, y_pred)
        precision = precision_score(y_test, y_pred, labels=[True, False])
        recall = recall_score(y_test, y_pred)
        F1 = f1_score(y_test, y_pred, labels=[True, False])
    
        print("오차행렬:\n", confusion)
        print("\n정확도: {:.4f}".format(accuracy))
        print("정밀도: {:.4f}".format(precision))
        print("재현율: {:.4f}".format(recall))
        print("F1: {:.4f}".format(F1))
        
    pred = model.predict(x_val.fillna(0))
    get_clf_eval(y_val, pred)

     

    모델의 featue의 주요도는 아래 코드를 통해 확인하였다.

    feature_importance = model.get_feature_importance()
    
    # 각 특성별 중요도 출력
    for i, feature_name in enumerate(x_train.columns):
        print(f"{feature_name}: {feature_importance[i]}")


    확인한 결과 Lead Owner와 Customer index 정보가 가장 큰 영향을 끼치는 것을 확인가능했다(보통 해당 값이 1을 넘어가면 영향을 매우 많이 끼친 것으로 판단한다고 한다)

    Lead Owner & Customer index 정보 처리

    위에서 밝힌 것 처럼 Lead Owner & Customer index, 즉 물건을 구매한 사람과, 영업 담당자가 가장 큰 영향을 끼치는 것을 확인할 수 있었는데, 이를 보고 해당 column에서 특정 사람이 물건을 구매한 횟수와 영업을 진행한 횟수를 새로운 feature로 뽑아보자라는 생각이 들었다. 

    따라서 해당 데이터를 추출하고 확인해보았다. 

    # 'lead_owner'의 값의 개수를 세어서 새로운 열 'count'에 추가
    df_train['count_lead_owner'] = df_train['lead_owner'].map(df_train['lead_owner'].value_counts())
    df_test['count_lead_owner'] = df_test['lead_owner'].map(df_test['lead_owner'].value_counts())

     

    해당 data를 확인한 결과 데이터의 분산이 매우 크다는 사실을 확인했고, robustsclaer로 정규화를 통해 해당 데이터를 한번 더 가공하였다. 

    scaler = RobustScaler()
    # 특정 열의 데이터 추출
    column_data = df_train['count_lead_owner'].values.reshape(-1, 1)  # Scaler에 입력하기 위해 2차원 배열로 변환
    
    # 데이터 표준화
    scaled_data = scaler.fit_transform(column_data)
    df_train['count_lead_owner'] = scaled_data

     

    이후에는 모델의 성능이 F1 score 기준으로 크게 증가한 것을 확인했고, 여러 하이퍼파라미터를 조정하여 성능을 더 높이는데 집중하였다. 

    결과

    그 결과 Train data기준 아래의 결과를 얻었다! 

    오차행렬: [[ 888 59] [ 416 10497]] 정확도: 0.9599 정밀도: 0.6810 재현율: 0.9377 F1: 0.7890

     

    https://drive.google.com/file/d/1V8cCT1095jBjqyYUMUJTYZOnONEXRDjH/view?usp=sharing

Designed by Tistory.