no image
[Python] 선형회귀 실습 _ 보스톤 주택 가격에 대한 선형 회귀
- 데이터: http://lib.stat.cmu.edu/datasets/boston - 데이터에 대한 설명 CRIM per capita crime rate by town ZN proportion of residential land zoned for lots over 25,000 sq.ft. INDUS proportion of non-retail business acres per town CHAS Charles River dummy variable (= 1 if tract bounds river; 0 otherwise) NOX nitric oxides concentration (parts per 10 million) RM average number of rooms per dwelling AGE propor..
2024.03.18
no image
[Python] 연관분석 실습 _ 아이템 기반 추천 시스템
- movies.csv: 영화 정보 데이터 - ratings.csv: 평점정보 데이터 1. 데이터 읽어오기 movies = pd.read_csv('./python_machine_learning-main/data/movielens/movies.csv') ratings = pd.read_csv('./python_machine_learning-main/data/movielens/ratings.csv') print(movies.shape) print(ratings.shape) (9742, 3) (100836, 4) # ratings에서 timestamp 제거 ratings = ratings[['userId', 'movieId', 'rating']] ratings_matrix.head(3) - 추천 시스템 데이터..
2024.03.18
no image
[Python] 연관분석 실습 _ 영화 콘텐츠 기반 필터링 추천 시스템
데이터 https://www.kaggle.com/tmdb/tmdb-movie-metadata 1. 데이터 읽어오기 movies =pd.read_csv('./python_machine_learning-main/data/tmdb_5000_movies.csv') print(movies.shape) movies.head() # 데이터 생성 movies_df = movies[['id','title', 'genres', 'vote_average', 'vote_count', 'popularity', 'keywords', 'overview']] # pd.set_option('max_colwidth', 100) movies_df.head(1) genres와 keywords는 하나의 문자열이 아니라 list다. csv 파..
2024.03.15
no image
[Python] Pandas 실습 _ 카카오 검색 API 데이터 가져오기
- 카카오 검색 API developers.kakao.com - 애플리케이션 생성 후 REST API 키 복사 a89507c93f15074167c0700239d4b1d0 블로그 검색 메서드URL인증 방식 GET https://dapi.kakao.com/v2/search/blog REST API 키 헤더 Authorization Authorization: KakaoAK ${REST_API_KEY} 인증 방식, 서비스 앱에서 REST API 키로 인증 요청 필수 파라미터 query String 검색을 원하는 질의어 특정 블로그 글만 검색하고 싶은 경우, 블로그 url과 검색어를 공백(' ') 구분자로 넣을 수 있음 필수 sort String 결과 문서 정렬 방식, accuracy(정확도순) 또는 recenc..
2024.03.15
no image
[Python] 연관분석 실습 _ 손흥민
설치 !pip install apyori 1. 데이터 읽어오기 df = pd.read_csv("./python_machine_learning-main/data/tweet_temp.csv") df.head() #한글 정제 함수 import re # 한글만 추출 def text_cleaning(text): hangul = re.compile('[^ ㄱ-ㅣ가-힣]+') # 한글의 정규표현식 result = hangul.sub('', text) return result #적용 # ‘tweet_text’ 피처에 적용 df['ko_text'] = df['tweet_text'].apply(lambda x: text_cleaning(x)) df.head() 2. 불용어 처리 - https://www.ranks.nl/s..
2024.03.15

- 데이터: http://lib.stat.cmu.edu/datasets/boston
- 데이터에 대한 설명

  • CRIM     per capita crime rate by town
  • ZN       proportion of residential land zoned for lots over 25,000 sq.ft.
  • INDUS    proportion of non-retail business acres per town
  • CHAS     Charles River dummy variable (= 1 if tract bounds river; 0 otherwise)
  • NOX      nitric oxides concentration (parts per 10 million)
  • RM       average number of rooms per dwelling
  • AGE      proportion of owner-occupied units built prior to 1940
  • DIS      weighted distances to five Boston employment centres
  • RAD      index of accessibility to radial highways
  • TAX      full-value property-tax rate per $10,000
  • PTRATIO  pupil-teacher ratio by town
  • B        1000(Bk - 0.63)^2 where Bk is the proportion of blacks by town
  • LSTAT    % lower status of the population
  • MEDV     Median value of owner-occupied homes in $1000's

 MEDV 컬럼이 타겟이고 나머지가 피처

 

 

1. 데이터 가져오기

data_url = "http://lib.stat.cmu.edu/datasets/boston"

raw_df = pd.read_csv(data_url, sep="\s+", skiprows=22, header=None)
print(raw_df.head())
          0      1      2    3      4      5     6       7    8      9     10
0    0.00632  18.00   2.31  0.0  0.538  6.575  65.2  4.0900  1.0  296.0  15.3
1  396.90000   4.98  24.00  NaN    NaN    NaN   NaN     NaN  NaN    NaN   NaN
2    0.02731   0.00   7.07  0.0  0.469  6.421  78.9  4.9671  2.0  242.0  17.8
3  396.90000   9.14  21.60  NaN    NaN    NaN   NaN     NaN  NaN    NaN   NaN
4    0.02729   0.00   7.07  0.0  0.469  7.185  61.1  4.9671  2.0  242.0  17.8
  • 공백 문자 단위로 데이터를 분할해서 읽을 공백 문자가 \s 이고
    +를 추가한 이유는 맨 앞에 공백이 있으면 제거하기 위해서
    뒤의 3개의 데이터도 하나의 행에 포함이 되어야 하는데 뒷줄로 넘어감

 

# 홀수 행에 짝수 행에서 앞의 2개의 열의 데이터를 붙이고 타겟 만들기

#피처 만들기
data = np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]])

#타겟 만들기
target = raw_df.values[1::2, 2]

...

 

 

 

2. DataFrame 생성

  • 피처의 이름을 출력하거나 데이터를 전처리할 때 피처의 이름을 사용하기 위함
bostonDF = pd.DataFrame(data, columns=["CRIM", "ZN", "INDUS", "CHAS", "NOX",
                                      "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO",
                                      "B", "LSTAT"])
bostonDF["PRICE"] = target
bostonDF.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 506 entries, 0 to 505
Data columns (total 14 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   CRIM     506 non-null    float64
 1   ZN       506 non-null    float64
 2   INDUS    506 non-null    float64
 3   CHAS     506 non-null    float64
 4   NOX      506 non-null    float64
 5   RM       506 non-null    float64
 6   AGE      506 non-null    float64
 7   DIS      506 non-null    float64
 8   RAD      506 non-null    float64
 9   TAX      506 non-null    float64
 10  PTRATIO  506 non-null    float64
 11  B        506 non-null    float64
 12  LSTAT    506 non-null    float64
 13  PRICE    506 non-null    float64
dtypes: float64(14)
memory usage: 55.5 KB

 

 

 

3. 타겟과 피처 사이의 선형 관계 파악

  • 산포도를 이용(여러 개의 컬럼 사이의 산포도를 만들 때는 seaborn 의 pairplot 이나 regplot을 이용)
    • 예전에는 숫자가 아닌 컬럼이 있으면 제외하고 출력을 했는데
    • 최신 API에서는 숫자 컬럼에 대해서만 동작하기 때문에 숫자가 아닌 컬럼이 있으면 에러
    • 피처가 많은 경우 불필요한 산포도가 너무 많이 그려져서 알아보기가 어려움
sns.pairplot(bostonDF, height=2.5)
plt.tight_layout()
plt.show()

 

 

#타겟 과의 관계를 파악하고자 하는 feature 리스트를 생성

lm_features = ["RM", "ZN", "INDUS", "NOX", "AGE", "PTRATIO", "LSTAT", "RAD"]

#한 줄에 4개씩 8개의 그래프 영역을 생성
fig, axe = plt.subplots(figsize=(16, 8), ncols=4, nrows=2)
#python에서 list를 순회할 때 인덱스를 같이 사용하는 방법
for i, feature in enumerate(lm_features):
    row = int(i / 4)
    col = i % 4
    sns.regplot(x=feature, y="PRICE", data=bostonDF, ax=axe[row][col])

 

 

# 타겟과 피처의 상관 계수 확인

#상관 계수 구하기
cols = lm_features.copy()
cols.append("PRICE")
cm = np.corrcoef(bostonDF[cols].values.T)
print(cm)
[[ 1.          0.31199059 -0.39167585 -0.30218819 -0.24026493 -0.35550149
  -0.61380827 -0.20984667  0.69535995]
 [ 0.31199059  1.         -0.53382819 -0.51660371 -0.56953734 -0.39167855
  -0.41299457 -0.31194783  0.36044534]
 [-0.39167585 -0.53382819  1.          0.76365145  0.64477851  0.38324756
   0.60379972  0.59512927 -0.48372516]
 [-0.30218819 -0.51660371  0.76365145  1.          0.7314701   0.18893268
   0.59087892  0.61144056 -0.42732077]
 [-0.24026493 -0.56953734  0.64477851  0.7314701   1.          0.26151501
   0.60233853  0.45602245 -0.37695457]
 [-0.35550149 -0.39167855  0.38324756  0.18893268  0.26151501  1.
   0.37404432  0.46474118 -0.50778669]
 [-0.61380827 -0.41299457  0.60379972  0.59087892  0.60233853  0.37404432
   1.          0.48867633 -0.73766273]
 [-0.20984667 -0.31194783  0.59512927  0.61144056  0.45602245  0.46474118
   0.48867633  1.         -0.38162623]
 [ 0.69535995  0.36044534 -0.48372516 -0.42732077 -0.37695457 -0.50778669
  -0.73766273 -0.38162623  1.        ]]
plt.figure(figsize=(15, 7))
sns.set(font_scale=1.5) #화면 크기에 따라 변경 - 폰트 크기

hm = sns.heatmap(cm, cbar=True, annot=True, square=True, fmt='.2f',
                annot_kws={'size':15}, yticklabels=cols, xticklabels=cols)
plt.show()
  • ticklabels는 보여질 문자열
  • annot_kws 는 글자 크기
  • fmt는 포맷으로 숫자의 표시 형식
  • annot는 값 출력 여부

 

 

- 데이터 탐색 결과

  • RM 과 LSTAT 의 PRICE 영향도가 높음
  • RM은 양의 상관 관계를 가지고 LSTAT는 음의 상관 관계를 가짐
  • 단변량(피처가 1개) 선형 회귀를 수행한다면 2개의 피처 중 하나를 이용하는 것이 효율적

 

 

scipy 를 이용한 RM 과 PRICE에 대한 단변량 선형 회귀

from scipy import stats

result = stats.linregress(bostonDF['RM'], bostonDF['PRICE'])

#하나의 속성 출력
print(result.pvalue)

#예측 - 방이 4개 인 경우
print("방이 4개인 경우:", result.slope * 4 + result.intercept)
2.487228871008377e-74
방이 4개인 경우: 1.7378151482826851
  • scipy는 머신러닝을 수행하지만 머신러닝만을 위한 패키지는 아님
  • 피처를 설정할 때 2차원 배열이 아니어도 됨
  • scipy 나 seaborn 의 데이터는 pandas의 자료구조가 가능

 

 

sklearn을 이용한 단변량 선형 회귀

from sklearn.linear_model import LinearRegression

#모델 생성 - 행렬 분해를 이용하는 선형 회귀 모델
slr = LinearRegression()

#타겟과 피처 생성
X = bostonDF[['LSTAT']].values #sklearn 에서는 피처가 2차원 배열
y = bostonDF['PRICE'].values #타겟은 일반적으로 1차원 배열

#훈련
slr.fit(X, y)
  • 통계 패키지는 훈련의 결과를 리턴
  • 머신러닝 패키지는 훈련의 결과를 내부에 저장
  • 다음 데이터를 예측할 때 통계 패키지는 수식을 직접 만들어야 함
  • 머신러닝 패키지는 수식을 만들지 않고 새로운 데이터만 predict에게 제공
  • API를 확인할 때 통계 패키지는 리턴하는 데이터를 확인해야 하고
  • 머신러닝 패키지는 모델을 확인해야 함
#기울기 와 절편 출력
print("기울기:", slr.coef_[0])
print("절편:", slr.intercept_)

#예측
print(slr.coef_[0] * 4 + slr.intercept_)
print(slr.predict([[4]]))
기울기: -0.9500493537579905
절편: 34.5538408793831
30.75364346435114
[30.75364346]

 

 

 

 

 

 

 

 

 

 

- movies.csv: 영화 정보 데이터

- ratings.csv: 평점정보 데이터

 

1. 데이터 읽어오기

movies = pd.read_csv('./python_machine_learning-main/data/movielens/movies.csv')
ratings = pd.read_csv('./python_machine_learning-main/data/movielens/ratings.csv')
print(movies.shape)
print(ratings.shape)
(9742, 3)
(100836, 4)



 

 

# ratings에서 timestamp 제거

ratings = ratings[['userId', 'movieId', 'rating']]
ratings_matrix.head(3)

 

 

 

- 추천 시스템 데이터 형태

  • user id별로 상품 id가 펼쳐지거나, 상품 id별로 user id가 펼쳐져야 한다.
  • 둘중 하나를 인덱스로 보내야 함 

# user id를 인덱스로 해서 movie id 별로 평점을 확인할 수 있도록 변경

ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')
  • 컬럼에 영화 제목이 아니라 movie id가 출력됨
  • movie id를 영화 제목으로 변경

 

# movies와 ratings를 movieid 컬럼을 기준으로 join(merge)

# title 컬럼을 얻기 이해 movies 와 조인 수행
rating_movies = pd.merge(ratings, movies, on='movieId')
rating_movies.head(3)

 

 

# pivot

# columns='title' 로 title 컬럼으로 pivot 수행. 
ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')

# NaN 값을 모두 0 으로 변환
ratings_matrix = ratings_matrix.fillna(0)
ratings_matrix.head(3)
  • 행렬을 펼쳐낼 때 유저별로 유사도를 계산할지, 영화별로 유사도를 계산할지에 따라서 펼쳐내는 방향이 달라진다.
  • 유사도를 계산할 항목이 인덱스로 존재하면 됨

 

 

 

2. 영화간 유사도 산출

# 아이템 기반으로 유사도 측정. 아이템인 영화제목이 index가 되도록 설정

ratings_matrix_T = ratings_matrix.transpose()
ratings_matrix_T.head(3)

 

 

# 코사인 유사도

#영화를 기준으로 코사인 유사도 산출
from sklearn.metrics.pairwise import cosine_similarity

item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)
print(item_sim)
[[1.         0.         0.         ... 0.32732684 0.         0.        ]
 [0.         1.         0.70710678 ... 0.         0.         0.        ]
 [0.         0.70710678 1.         ... 0.         0.         0.        ]
 ...
 [0.32732684 0.         0.         ... 1.         0.         0.        ]
 [0.         0.         0.         ... 0.         1.         0.        ]
 [0.         0.         0.         ... 0.         0.         1.        ]]

 

 

# 영화 제목 붙이기

# cosine_similarity() 로 반환된 넘파이 행렬을 영화명을 매핑하여 DataFrame으로 변환
item_sim_df = pd.DataFrame(data=item_sim, index=ratings_matrix.columns,
                          columns=ratings_matrix.columns)
print(item_sim_df.shape)
item_sim_df.head(3)

 

 

# Godfather, The(1972)와 가장 유사한 영화 5개 추출

item_sim_df["Godfather, The (1972)"].sort_values(ascending=False)[:6]
title
Godfather, The (1972)                        1.000000
Godfather: Part II, The (1974)               0.821773
Goodfellas (1990)                            0.664841
One Flew Over the Cuckoo's Nest (1975)       0.620536
Star Wars: Episode IV - A New Hope (1977)    0.595317
Fargo (1996)                                 0.588614
Name: Godfather, The (1972), dtype: float64

 

 

 

 

3. 아이템 기반 최근접 이웃 협업 필터링으로 개인화된 영화 추천

  • 아이템 기반의 영화 유사도 데이터는 모든 사용자의 평점을 기준으로 영화의 유사도를 생성했기 때문에 영화를 추천할 수는 있지만 개인의 취향을 전혀 반영하지 않음
  • 개인화된 영화 추천은 유저가 아직 관람하지 않은 영화를 추천해야 함
  • 아직 관람하지 않은 영화에 대해서 아이템 유사도와 기존에 관람한 영화의 평점데이터를 기반으로 모든 영화의 평점을 예측하고 그중에서 높은 예측 평점을 가진 영화를 추천

- 계산식: 사용자가 본 영화에 대한 실제 평점과 다른 모든 영화와의 코사인 유사도를 내적 곱 하고 그 값을 전체 합으로 나눔

 

 

# 사용자 별로 평점을 예측해주는 함수

def predict_rating(ratings_arr, item_sim_arr ):
    ratings_pred = ratings_arr.dot(item_sim_arr)/ np.array([np.abs(item_sim_arr).sum(axis=1)])
    return ratings_pred

 

 

#개인화된 예측 평점 확인

ratings_pred = predict_rating(ratings_matrix.values , item_sim_df.values)
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index= ratings_matrix.index,
                                   columns = ratings_matrix.columns)
ratings_pred_matrix.head(3)

 

 

# 평가

from sklearn.metrics import mean_squared_error

# 사용자가 평점을 부여한 (0이 아닌) 영화에 대해서만 예측 성능 평가 MSE 를 구함. 
def get_mse(pred, actual):
    pred = pred[actual.nonzero()].flatten()
    actual = actual[actual.nonzero()].flatten()
    return mean_squared_error(pred, actual)

print('아이템 기반 모든 인접 이웃 MSE: ', get_mse(ratings_pred, ratings_matrix.values ))
아이템 기반 모든 인접 이웃 MSE:  9.895354759094706
  • MSE가 너무 높다. 
  • 평점이 5.0인데, 제곱근인 3정도 차이가 난다는 건 추천 시스템으로 사용하기 어렵다는 뜻

 

 

 

4. 추천 수정

  • 현재는 모든 영화와의 유사도를 이용해서 평점을 예측했는데 모든 영화보다는 유저가 본 영화 중 유사도가 가장 높은 영화 몇개를 이용해서 예측하는 것이 더 나을 가능성이 높다.
# 유사도를 계산할 영화의 개수를 배개변수로 추가
def predict_rating_topsim(ratings_arr, item_sim_arr, n=20):
    # 사용자-아이템 평점 행렬 크기만큼 0으로 채운 예측 행렬 초기화
    pred = np.zeros(ratings_arr.shape)

    # 사용자-아이템 평점 행렬의 열 크기만큼 Loop 수행 
    for col in range(ratings_arr.shape[1]):
        # 유사도 행렬에서 유사도가 큰 순으로 n개 데이터 행렬의 index 반환
        top_n_items = [np.argsort(item_sim_arr[:, col])[:-n-1:-1]]
        # 개인화된 예측 평점을 계산
        for row in range(ratings_arr.shape[0]):
            pred[row, col] = item_sim_arr[col, :][top_n_items].dot(ratings_arr[row, :][top_n_items].T) 
            pred[row, col] /= np.sum(np.abs(item_sim_arr[col, :][top_n_items]))        
    return pred

 

# 수정 후 성능

ratings_pred = predict_rating_topsim(ratings_matrix.values , item_sim_df.values, n=20)
print('아이템 기반 인접 TOP-20 이웃 MSE: ', get_mse(ratings_pred, ratings_matrix.values ))


# 계산된 예측 평점 데이터는 DataFrame으로 재생성
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index= ratings_matrix.index,
                                   columns = ratings_matrix.columns)
아이템 기반 인접 TOP-20 이웃 MSE:  3.695009387428144
  • 아이템이나 유저를 가지고 유사도를 측정할 때 모든 데이터를 사용하는 것보다는 유사도가 높은 데이터 몇개를 이용해서 예측을 하는 것이 성능이 좋은 경우가 많다.
#9번째 유저에 영화 추천
user_rating_id = ratings_matrix.loc[9, :]
user_rating_id[ user_rating_id > 0].sort_values(ascending=False)[:10]
title
Adaptation (2002)                                                                 5.0
Citizen Kane (1941)                                                               5.0
Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981)    5.0
Producers, The (1968)                                                             5.0
Lord of the Rings: The Two Towers, The (2002)                      5.0
Lord of the Rings: The Fellowship of the Ring, The (2001)       5.0
Back to the Future (1985)                                                         5.0
Austin Powers in Goldmember (2002)                                   5.0
Minority Report (2002)                                                            4.0
Witness (1985)                                                                    4.0
Name: 9, dtype: float64

 

 

# 유저가 보지 않은 영화 목록을 리턴하는 함수

def get_unseen_movies(ratings_matrix, userId):
    # userId로 입력받은 사용자의 모든 영화정보 추출하여 Series로 반환함. 
    # 반환된 user_rating 은 영화명(title)을 index로 가지는 Series 객체임. 
    user_rating = ratings_matrix.loc[userId,:]
    
    # user_rating이 0보다 크면 기존에 관람한 영화임. 대상 index를 추출하여 list 객체로 만듬
    already_seen = user_rating[ user_rating > 0].index.tolist()
    
    # 모든 영화명을 list 객체로 만듬. 
    movies_list = ratings_matrix.columns.tolist()
    
    # list comprehension으로 already_seen에 해당하는 movie는 movies_list에서 제외함. 
    unseen_list = [ movie for movie in movies_list if movie not in already_seen]
    
    return unseen_list

 

 

# 유저가 보지 않은 영화 목록에서 예측 평점이 높은 영화 제목을 리턴하는 함수

def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
    # 예측 평점 DataFrame에서 사용자id index와 unseen_list로 들어온 영화명 컬럼을 추출하여
    # 가장 예측 평점이 높은 순으로 정렬함. 
    recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
    return recomm_movies

 

# 유저가 보지 않은 영화 목록 추출

# 사용자가 관람하지 않는 영화명 추출   
unseen_list = get_unseen_movies(ratings_matrix, 9)

# 아이템 기반의 인접 이웃 협업 필터링으로 영화 추천 
recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list, top_n=10)

# 평점 데이타를 DataFrame으로 생성. 
recomm_movies = pd.DataFrame(data=recomm_movies.values,index=recomm_movies.index,columns=['pred_score'])
recomm_movies

 

 

 

 

5. 작업 과정 정리

- 각 영화 간의 유사도 측정

 

- 유저가 본 영화의 평점을 기반으로 해서 유사도가 높은 20개의 영화를 추출하고, 그 영화들의 평점으로 않은 영화의 평점을 예측

 

 

데이터

https://www.kaggle.com/tmdb/tmdb-movie-metadata

 

1. 데이터 읽어오기

movies =pd.read_csv('./python_machine_learning-main/data/tmdb_5000_movies.csv')
print(movies.shape)
movies.head()


 

 

# 데이터 생성

movies_df = movies[['id','title', 'genres', 'vote_average', 'vote_count',
                 'popularity', 'keywords', 'overview']]

# pd.set_option('max_colwidth', 100)
movies_df.head(1)
  • genres와 keywords는 하나의 문자열이 아니라 list다.
    • csv 파일을 만드려고 list를 문자열로 만들기 위해 " "로 감싸는 형태로 만들었다. 
  • 파이썬이나 자바스크립트는 json 포맷의 문자열로 데이터를 표현한다. 
    + 문자열로 만들어진 데이터를 원래의 자료형으로 되돌리는 API가 제공된다. 
    • python에서는 ast 모듈의 literal_eval 함수 이용 (직접 파싱할 필요 X)
    • "[apple, pear, watermelon]"  ->  literal_eval(데이터)  ->  [apple, pear, watermelon]
print(movies_df[['genres', 'keywords']][:1])
                                              genres  \
0  [{"id": 28, "name": "Action"}, {"id": 12, "nam...   

                                            keywords  
0  [{"id": 1463, "name": "culture clash"}, {"id":...  

 

 

#데이터 변환

from ast import literal_eval

#문자열을 파이썬 객체로 변환
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)

#디셔너리에서 데이터만 추출
movies_df['genres'] = movies_df['genres'].apply(lambda x : [ y['name'] for y in x])
movies_df['keywords'] = movies_df['keywords'].apply(lambda x : [ y['name'] for y in x])

#데이터 확인
movies_df[['genres', 'keywords']][:1]

 

 

 

 

 

2. 장르 유사도 측정

  • 문자열을 피처로 만드는 방법은 CountVectorizer,  TfidfVectorizer 
  • 장르 리스트 안에서 같은 단어가 나올 수 없으니 등장 횟수만을 기반으로 하는 CountVectorizer 사용

 

 

#피처 벡터화

from sklearn.feature_extraction.text import CountVectorizer

# CountVectorizer를 적용하기 위해 공백문자로 word 단위가 구분되는 문자열로 변환. 
movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))
print(movies_df['genres_literal'])
0       [ { " i d " :   2 8 ,   " n a m e " :   " A c ...
1       [ { " i d " :   1 2 ,   " n a m e " :   " A d ...
2       [ { " i d " :   2 8 ,   " n a m e " :   " A c ...
3       [ { " i d " :   2 8 ,   " n a m e " :   " A c ...
4       [ { " i d " :   2 8 ,   " n a m e " :   " A c ...

...
count_vect = CountVectorizer(min_df=0.0, ngram_range=(1,2))
genre_mat = count_vect.fit_transform(movies_df['genres_literal'])
print(genre_mat.shape)
(4803, 276)

 

 

# 장르의 코사인 유사도 측정 

from sklearn.metrics.pairwise import cosine_similarity

genre_sim = cosine_similarity(genre_mat, genre_mat)
print(genre_sim.shape)
print(genre_sim[:2])
(4803, 4803)

[[1.         0.59628479 0.4472136  ... 0.         0.         0.        ]
 [0.59628479 1.         0.4        ... 0.         0.         0.        ]]

 

 

# 유사도 측정

genre_sim_sorted_ind = genre_sim.argsort()[:, ::-1]  #전부
print(genre_sim_sorted_ind[:1])
[[   0 3494  813 ... 3038 3037 2401]]

 

 

# 영화 정보 데이터프레임과 영화 정보 인덱스와 영화 제목과 추천할 영화 개수를 입력하면

  영화 제목과 평점을 리턴해주는 함수

def find_sim_movie(df, sorted_ind, title_name, top_n=10):
    
    # 인자로 입력된 movies_df DataFrame에서 'title' 컬럼이 입력된 title_name 값인 DataFrame추출
    title_movie = df[df['title'] == title_name]
    
    # title_named을 가진 DataFrame의 index 객체를 ndarray로 반환하고 
    # sorted_ind 인자로 입력된 genre_sim_sorted_ind 객체에서 유사도 순으로 top_n 개의 index 추출
    title_index = title_movie.index.values
    similar_indexes = sorted_ind[title_index, :(top_n)]   #자기 영화 제외하려면 1:(top_m)
    
    # 추출된 top_n index들 출력. top_n index는 2차원 데이터  
    #dataframe에서 index로 사용하기 위해서 1차원 array로 변경
    print(similar_indexes)
    
    similar_indexes = similar_indexes.reshape(-1)
    
    return df.iloc[similar_indexes]
similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Dark Knight Rises',10)
similar_movies[['title', 'vote_average', 'vote_count']].sort_values('vote_average', ascending=False)

 

 

 

 

 

3. 가중 평점

  • 평점을 많이 매기지 않은 데이터가 평점이 높게 나오는 경향이 있을 수 있다.
  • 데이터의 개수가 작으면 극단치의 영향을 많이 받기 때문이다.
  • 데이터 개수에 따른 보정을 해줄 필요가 있다. 
  • IMDB에서 권장
가중 평점(Weighted Rating) =(v/(v+m)) * R+ (m/(v+m)) * C
  • v: 개별 영화에 평점을 투표한 횟수
  • m: 평점을 부여하기 위한 최소 투표 횟수
  • R: 개별 영화에 평균 평점
  • C: 전체 영화에 대한 평균 평점

 

# 가중 평점 부여

C = movies_df['vote_average'].mean()
m = movies_df['vote_count'].quantile(0.6)
print('C:',round(C,3), 'm:',round(m,3))
C: 6.092    m: 370.2
def weighted_vote_average(record):
    v = record['vote_count']
    R = record['vote_average']
    
    return ( (v/(v+m)) * R ) + ( (m/(m+v)) * C )   

movies_df['weighted_vote'] = movies_df.apply(weighted_vote_average, axis=1)
movies_df['weighted_vote'] = movies_df.apply(weighted_vote_average, axis=1)
movies_df[['title', 'vote_average', 'weighted_vote', 'vote_count']][:10]

 

# 가중치 조정

def find_sim_movie(df, sorted_ind, title_name, top_n=10):
    title_movie = df[df['title'] == title_name]
    title_index = title_movie.index.values
    
    # top_n의 2배에 해당하는 쟝르 유사성이 높은 index 추출 
    similar_indexes = sorted_ind[title_index, :(top_n*2)]
    similar_indexes = similar_indexes.reshape(-1)
# 기준 영화 index는 제외
    similar_indexes = similar_indexes[similar_indexes != title_index]
    
    # top_n의 2배에 해당하는 후보군에서 weighted_vote 높은 순으로 top_n 만큼 추출 
    return df.iloc[similar_indexes].sort_values('weighted_vote', ascending=False)[:top_n]

similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather',10)
similar_movies[['title', 'vote_average', 'weighted_vote']]

 

 

 

 

 

 

지금은 장르기반으로만 추천했지만, 가장 최근에 끝난 것에도 가중치 부여할 수 있다.

  • ex. 범죄도시 4 개봉한다고 하면, 범죄도시 2-3를 보여주는 것처럼!

- 카카오 검색 API

developers.kakao.com

 

- 애플리케이션 생성 후 REST API 키 복사

a89507c93f15074167c0700239d4b1d0

 

 

블로그 검색    메서드URL인증 방식

GET https://dapi.kakao.com/v2/search/blog REST API 키

 

 

헤더

Authorization Authorization: KakaoAK ${REST_API_KEY}
인증 방식, 서비스 앱에서 REST API 키로 인증 요청
필수

 

 

파라미터

query String 검색을 원하는 질의어
특정 블로그 글만 검색하고 싶은 경우, 블로그 url과 검색어를 공백(' ') 구분자로 넣을 수 있음
필수
sort String 결과 문서 정렬 방식, accuracy(정확도순) 또는 recency(최신순), 기본 값 accuracy 선택
page Integer 결과 페이지 번호, 1~50 사이의 값, 기본 값 1 선택
size Integer 한 페이지에 보여질 문서 수, 1~50 사이의 값, 기본 값 10 선택

query = "막대그래프"

 

import requests
import json

query = "막대그래프"
url = ("https://dapi.kakao.com/v2/search/blog?query=" + query)
headers = {'Authorization': 'KakaoAK {}'.format('a89507c93f15074167c0700239d4b1d0')}
data = requests.post(url, headers=headers)
print(data.text)

 

#JSON 문자열을 Python의 자료구조로 변경
result = json.loads(data.text)
print(type(result))
<class 'dict'>

 

 

documents = result['documents']

for doc in documents:
    print(doc['blogname'], doc['title'], doc['datetime'], )
야채's Data Tableau - <b>막대</b> <b>그래프</b> 2024-02-01T14:22:10.000+09:00
...

 

 

- MySQL 연동

!pip install pyMySQL

 

 

- 데이터베이스 접속 확인

import pymysql

con = pymysql.connect(host='데이터베이스 위치', 
                        port = 포트번호, 
                        user = '아이디', 
                        password = '비밀번호', 
                        db = '데이터베이스 이름', 
                        charset = '인코딩 방식')
con.close()

 

 

- SELECT를 제외한 구문 실행

#연결 객체를 가지고 cursor()를 호출해서 커서 객체 생성
cursor = con.cursor()
#sql 실행
cursor.execute("SQL 구문(%s를 대입할 데이터 개수만큼 생성)", (%s에 대입할 데이터를 나열))
#commint
con.commit()
  • SELECT문이 아니면 항상 커밋!

 

- SELECT 구문은 execute , 커서를 이용해서 fetch_one이나 fetchall()을 호출해서 튜플을 가져와 읽기

#SQL을 실행하기 위한 객체 생성
cursor = con.cursor()

#테이블 생성 구문 실행
cursor.execute("create table phamacy(placename varchar(30), addressname varchar(200)")

 

# 파싱한 데이터 순회

for doc in documents:
    cursor.execute("insert into blog(blogname, title) values(%s, %s)",
                   (doc["blogname"], doc["title"]))
con.commit()

 

 

설치

!pip install apyori

 

1. 데이터 읽어오기

df = pd.read_csv("./python_machine_learning-main/data/tweet_temp.csv")
df.head()

 

#한글 정제 함수

import re

# 한글만 추출
def text_cleaning(text):
    hangul = re.compile('[^ ㄱ-ㅣ가-힣]+') # 한글의 정규표현식
    result = hangul.sub('', text)
    return result

 

#적용

# ‘tweet_text’ 피처에 적용
df['ko_text'] = df['tweet_text'].apply(lambda x: text_cleaning(x))
df.head()

 

 

 

2. 불용어 처리

- https://www.ranks.nl/stopwords/korean

from collections import Counter

# 한국어 약식 불용어사전 예시 파일 - https://www.ranks.nl/stopwords/korean
korean_stopwords_path = "./python_machine_learning-main/korean_stopwords.txt"
with open(korean_stopwords_path, encoding='utf8') as f:
    stopwords = f.readlines()
#앞뒤 공백 제거
stopwords = [x.strip() for x in stopwords]
print(stopwords)
['아', '휴', '아이구', '아이쿠', '아이고', '어', '나', '우리', '저희', '따라', '의해', '을', '를', '에', '의', '가', '으로', '로', '에게', '뿐이다', '의거하여', '근거하여', '입각하여', '기준으로', '예하면', '예를 들면', '예를 들자면', '저', '소인', '소생', '저희', '지말고', '하지마', '하지마라', '다른', '물론', '또한', '그리고',  ...
# 문자열 대입받아서 명사만 추출, 1글자는 제거하고 불용어도 제거해서 리턴

from konlpy.tag import Okt

def get_nouns(x):
    nouns_tagger = Okt()
    nouns = nouns_tagger.nouns(x)
    
    # 한글자 키워드를 제거
    nouns = [noun for noun in nouns if len(noun) > 1]
    
    # 불용어를 제거
    nouns = [noun for noun in nouns if noun not in stopwords]
    
    return nouns

.

# ‘ko_text’ 피처에 적용
df['nouns'] = df['ko_text'].apply(lambda x: get_nouns(x))
print(df.shape)
df.head()
(525, 4)

  • 연관 규칙 분석을 할 떄는 데이터가 리스트 형태로 만들어지면 됨
  • 상품 거래 연관 규칙 분석을 할 때는 구매한 상품 목록을 list로 만들면 됨    ex. ['사과', '배', '한라봉']

 

 

 

3. 연관 규칙 분석

from apyori import apriori

# 장바구니 형태의 데이터(트랜잭션 데이터)를 생성
transactions = [
    ['손흥민', '시소코'],
    ['손흥민', '케인'],
    ['손흥민', '케인', '포체티노']
]

# 연관 분석을 수행
results = list(apriori(transactions))
for result in results:
    print(result)
RelationRecord(items=frozenset({'손흥민'}), support=1.0, ordered_statistics=[OrderedStatistic(items_base=frozenset(), items_add=frozenset({'손흥민'}), confidence=1.0, lift=1.0)])
RelationRecord(items=frozenset({'시소코'}), support=0.3333333333333333, ordered_statistics=[OrderedStatistic(items_base=frozenset(), items_add=frozenset({'시소코'}), confidence=0.3333333333333333, lift=1.0)])
...
RelationRecord(items=frozenset({'포체티노', '손흥민'}), support=0.3333333333333333, ordered_statistics=[OrderedStatistic(items_base=frozenset(), items_add=frozenset({'포체티노', '손흥민'}), confidence=0.3333333333333333, lift=1.0), OrderedStatistic(items_base=frozenset({'손흥민'}), items_add=frozenset({'포체티노'}), confidence=0.3333333333333333, lift=1.0), OrderedStatistic(items_base=frozenset({'포체티노'}), items_add=frozenset({'손흥민'}), confidence=1.0, lift=1.0)])
...
# 트랜잭션 데이터를 추출합니다.
transactions = df['nouns'].tolist()
transactions = [transaction for transaction in transactions if transaction] # 공백 문자열 방지
print(transactions)
[['트릴', '리온', '축구', '국가대표', '손흥민', '선수', '샴푸', '모델', '기용', '출처', '한국', '경제', '네이버', '뉴스'], ['손흥민', '말씀'], ['손흥민'], ['경남', '도민', '일보', '프로축구', '연맹', '경기장', '선거운동', '손흥민', '영국', '관중', '인종차별', '행위', '보고', '축구장', '선거운동', '규정', '위반', '이야기'], ['선택', '손흥민', '축구'], ['토트넘', '골수팬', '승부사', '제일', '선수', '손흥민', '입다'], ['계정', '지기', '실수', '삭제', '다시', '하리보', '손흥민', '홍보', '모델', '발탁', '기념', '해당', '추첨', '통해', '하리보', '골드바', '기간'], ['안녕하십니까', '손흥민', '트위터', '매우', '오늘', '경기도', '관심'], 
...

 

# 연관분석

# 연관 분석을 수행
results = list(apriori(transactions,
                       min_support=0.1,
                       min_confidence=0.2,
                       min_lift=5,
                       max_length=2))
for result in results:
    print(result)
[RelationRecord(items=frozenset({'국가대표팀', '게임'}), support=0.14285714285714285, ordered_statistics=[OrderedStatistic(items_base=frozenset({'게임'}), items_add=frozenset({'국가대표팀'}), confidence=1.0, lift=7.0), OrderedStatistic(items_base=frozenset({'국가대표팀'}), items_add=frozenset({'게임'}), confidence=1.0, lift=7.0)]), 

RelationRecord(items=frozenset({'게임', '금메달'}), support=0.14285714285714285, ordered_statistics=[OrderedStatistic(items_base=frozenset({'게임'}), items_add=frozenset({'금메달'}), confidence=1.0, lift=7.0), OrderedStatistic(items_base=frozenset({'금메달'}), items_add=frozenset({'게임'}), confidence=1.0, lift=7.0)]), 

RelationRecord(items=frozenset({'모습', '게임'}), support=0.14285714285714285, ordered_statistics=[OrderedStatistic(items_base=frozenset({'게임'}), items_add=frozenset({'모습'}), confidence=1.0, lift=7.0), OrderedStatistic(items_base=frozenset({'모습'}), items_add=frozenset({'게임'}), confidence=1.0, lift=7.0)]), 

RelationRecord(items=frozenset({'아시아', '게임'}), support=0.14285714285714285, ordered_statistics=[OrderedStatistic(items_base=frozenset({'게임'}), items_add=frozenset({'아시아'}), confidence=1.0, lift=7.0), OrderedStatistic(items_base=frozenset({'아시아'}), items_add=frozenset({'게임'}), confidence=1.0, lift=7.0)]), 

...
  • 하나의 항목은 제거

 

# 데이터 프레임 형태로 정리

# 데이터 프레임 형태로 정리
columns = ['source', 'target', 'support']
network_df = pd.DataFrame(columns=columns)

# 규칙의 조건절을 source, 결과절을 target, 지지도를 support 라는 데이터 프레임의 피처로 변환
for result in results:
        items = [x for x in result.items]
        row = [items[0], items[1], result.support]
        series = pd.Series(row, index=network_df.columns)
        network_df.loc[len(network_df)] = series

network_df.head()

 

 

 

 

4. 키워드 빈도 추출

from konlpy.tag import Okt
from collections import Counter

# 명사 키워드를 추출
nouns_tagger = Okt()
nouns = nouns_tagger.nouns(tweet_corpus)
count = Counter(nouns)

# 한글자 키워드를 제거
remove_char_counter = Counter({x : count[x] for x in count if len(x) > 1})
print(remove_char_counter)
Counter({'손흥민': 560, '하리보': 245, '축구': 140, '선수': 140, '조현우': 140, '황의조': 140, '금메달': 140, '모델': 105, '한국': 105, '신제품': 105, '선거운동': 70, '보고': 70, '계정': 70, '지기': 70, '실수': 70, '삭제': 70, '다시': 70, '홍보': 70, '발탁': 70, '기념': 70, '해당': 70, '추첨': 70, '통해': 70, '골드바': 70, '기간': 70, '출국': 70, '국가대표팀': 70, '아시아': 70, '게임': 70, '모습': 70, '의조': 70, '진짜': 70, '트릴': 35, '리온': 35, '국가대표': 35, '샴푸': 35, '기용': 35, '출처': 35, '경제': 35, '네이버': 35, '뉴스': 35, '말씀': 35, '경남': 35, '도민': 35, '일보': 35, '프로축구': 35, '연맹': 35, '경기장': 35, '영국': 35, '관중': 35, '인종차별': 35, '행위': 35, '축구장': 35, '규정': 35, '위반': 35, '이야기': 35, '선택': 35, '토트넘': 35, '골수팬': 35, '승부사': 35, '제일': 35, '입다': 35, '안녕하십니까': 35, '트위터': 35, '매우': 35, '오늘': 35, '경기도': 35, '관심': 35, '긴급': 35, '속보': 35, '개시': 35, '샤인': 35, '가장': 35, '먼저': 35, '팔로우': 35, '알림': 35, '설정': 35, '두기': 35})

 

# 단어와 등장횟수를 가지고 DataFrame 생성

# 키워드와 키워드 빈도 점수를 ‘node’, ‘nodesize’ 라는 데이터 프레임의 피처로 생성
node_df = pd.DataFrame(remove_char_counter.items(), columns=['node', 'nodesize'])
node_df = node_df[node_df['nodesize'] >= 50] # 시각화의 편의를 위해 ‘nodesize’ 50 이하는 제거합니다.
node_df.head()

 

# networkx

import networkx as nx
plt.figure(figsize=(12,12))

# networkx 그래프 객체
G = nx.Graph()

# node_df의 키워드 빈도수를 데이터로 하여, 네트워크 그래프의 ‘노드’ 역할을 하는 원을 생성
for index, row in node_df.iterrows():
    G.add_node(row['node'], nodesize=row['nodesize'])
    
# network_df의 연관 분석 데이터를 기반으로, 네트워크 그래프의 ‘관계’ 역할을 하는 선을 생성
for index, row in network_df.iterrows():
    G.add_weighted_edges_from([(row['source'], row['target'], row['support'])])
    
# 그래프 디자인과 관련된 파라미터를 설정
pos = nx.spring_layout(G, k=0.6, iterations=50)
sizes = [G.nodes[node]['nodesize']*25 for node in G]
nx.draw(G, pos=pos, node_size=sizes, node_color='yellow')

#레이블 출력 (한글폰트)
nx.draw_networkx_labels(G, pos=pos, font_family='Malgun Gothic', font_size=25)

# 그래프 출력
ax = plt.gca()
plt.show()