본문 바로가기
DEV/Web 개발

Web개발 :: 데이터 처리, 변형, Pandas Dataframe, Pagenation _TIL72

by 올커 2022. 12. 15.

■ JITHub 개발일지 72일차

□  TIL(Today I Learned) ::

데이터 처리, 변형, Pandas Dataframe 

1. 머신러닝 Dataframe 관련 오류

 - 현재 진행하고 있는 프로젝트에서는 머신러닝시 각 조건에 따라 필요한 데이터를 아래와 같이 쿼리셋으로 가져온다.

# 유사한 유저 정보 조회 및 추천(기존 사용이력이 없는 사용자)
def rcm_place_new_user(place_id, category):
    places = pd.DataFrame(list(Place.objects.values()))
    ...
    
# 유사한 유저 정보 조회 및 추천(기존 유저)
def rcm_place_user(user_id, cate_id):
    places = pd.DataFrame(list(Place.objects.values()))
    ...

 - 쿼리셋으로 가져온 데이터에서 카테고리에 따라 아래와 같이 처리해준 후

cate_id = CHOICE_ONE.index(category)

if cate_id <= 12:       # Case1: choice food group
    places = places[places['category'].str.contains(category)]
else:                   # Case2: choice place location
    places = places[places['place_address'].str.contains(category)]

 - 머신러닝을 처리해주기 위해 판다스의 데이터프레임으로 생성해준다.

# Create dataframe
    reviews = pd.DataFrame(list(Review.objects.values()))

    가져온 데이터 쿼리셋 중 places라는 쿼리셋은 'id'라는 인덱스를 갖고 있는데 reviews에서는 같은 값들을 'place_id' 라는 이름의 인덱스로 갖고 있기 때문에 인덱스 이름(데이터프레임에서는 컬럼 헤더)을 변경해주어야 한다. 이 때 사용하는 함수는 rename이다.

ABC.rename(columns = {'a':'A','b':'B'})

  그리고 rename만 진행하면 변경 적용(저장)이 이루어지지 않기 때문에 파이썬의 inplace 옵션을 True(기본값은 False)로 지정해주어야 했다.

places.rename(columns={'id':'place_id'}, inplace=True)

  이제 가져온 places와 reviews 두 데이터프레임을 'place_id'라는 컬럼 헤더를 기준으로 merge해준 후, 'author_id'를 인덱스로 한 피봇테이블을 생성한다.

  - 두 데이터 프레임을 병합할 때에는 pd.merge를 사용하고 소괄호 안에 두 개의 데이터프레임 이름, 그리고 병합 기준을 'on=' 옵션으로 지정해주어야 한다. 

  - 피봇 테이블을 생성할 때에는 아래와 같이 .pivot_table을 활용하고, 소괄호 안에 데이터값, index, columns를 각각 지정해주어야 한다.

place_ratings = pd.merge(places, reviews, on='place_id')
review_user = place_ratings.pivot_table('rating_cnt', index='author_id', columns='place_id')

  - 이제 이 다음 순서의 머신러닝 작업에서 문제가 발생했다. 사실 이 문제를 찾는 데 까지 오래 걸렸는데, 그 이유는 아래와 같았다.

  1) 추천 기능을 실행할 때 사용자의 조건이 다양한데 이 부분을 고려하지 못했던 점

  2) 기존에는 잘 동작되다가 머신러닝을 진행하지 못하는 조건의 유저가 머신러닝을 실행시 프론트에서는 데이터를 불러오지 못하는 에러가 발생하고 백엔드에서는 Django 서버가 꺼져버리는 문제(사실 Django가 local에서 Get 요청을 해준 경우에 서버를 꺼뜨렸다는 점에 충격적이었다.)

  3) 딥러닝 기능을 실행하고 데이터를 로드할 때 기본적으로 시간이 오래걸리는 상황이었기 때문에 데이터를 너무 많이 불러와서 그런 것인지 혼동을 한 점

  - 위의 3가지 이유 이 외에도 다른 작업들을 동반하면서 문제점을 정확하게 인지하지 못하였던 것 같다.

  - 실제 문제는! 사용자의 리뷰를 기반으로 머신러닝이 동작되는데, 사용자가 선택한 카테고리에 한해서 쿼리셋을 가져온 후 머신러닝을 동작시키기 때문에 발생했다. 사용자가 리뷰를 하나라도 작성하면 사용자에게 경험 정보가 있다고 판단하여 새로운 레코드를 생성하지 않고 유저 기반 협업 추천 시스템을 사용하는데 사용자가 작성한 리뷰의 카테고리가 선택한 카테고리와 다를 경우 해당 유저는 경험 데이터가 없는 것이나 마찬가지였던 점이다.

  - 또, 신규 유저일 경우에는 경험 데이터가 없어서 새로운 레코드를 생성하는데, 아래와 같이 코드를 작성하면서 레코드를 반복적으로 생성하고 있었다.

review_user.loc[len(review_user)] = np.nan
review_user.loc[len(review_user), col] = 5

  - 예를 들면 위 코드의 첫번째 줄에 len(review_user)가 14였다면 두번째 줄에는 loc로 인해 len(review_user)가 15로 되어 최종적으로 불필요한 행이 계속해서 추가되었던 것이다.

  - 위의 두 문제들을 임시적으로 해결하기 위해서 코드를 아래와 같이 코드를 변경해주었다. 프로젝트 중간 시연 이후 프로젝트 로직을 다시 구성해주어야 할 듯 하다.

if (user_id in review_user.index):
        review_user = review_user.fillna(0)
    else:
        col = random.choice(review_user.columns.to_list())
        review_user.loc[user_id] = np.nan
        review_user.loc[user_id, col] = 5
        review_user = review_user.fillna(0)

  - 이후 코사인 유사도를 통한 머신러닝 진행, 가장 유사한 유저를 찾아내고 리뷰 데이터를 통한 장소를 받아오는 기능에는 다른 문제점이 없었다.

    # Analyze cosine similarity
    user_sim_np = cosine_similarity(review_user, review_user)
    user_sim_df = pd.DataFrame(user_sim_np, index=review_user.index, columns=review_user.index)

    # Find the most similar user
    picked_user = user_sim_df.sort_values(by=user_id, ascending=False).index[1]
    result = review_user.query(f"author_id == {picked_user}").sort_values(ascending=False, by=picked_user, axis=1)

    # Recommend the most similar user
    result_list = []
    for column in result:
        result_list.append(column)
    return result_list

2. 페이지네이션 관련 오류

  - Django에서 페이지네이션을 제공하고 있어 백엔드에서 페이지네이션으로 데이터를 넘겨주고 있다.

# places/views.py
from gaggamagga.pagination import PaginationHandlerMixin

...
class PlaceListPagination(PageNumberPagination):
    page_size = 10

...

##### 맛집(유저일 경우) #####
class UserPlaceListView(PaginationHandlerMixin, APIView):
    permission_classes = [IsAuthenticated]
    pagination_class = PlaceListPagination

    # 맛집 리스트 추천
    @swagger_auto_schema(operation_summary="맛집 리스트 추천(유저)",
                    responses={200 : '성공', 401 : '인증 에러', 500 : '서버 에러'})
    def get(self, request, cate_id):
        place_list = rcm_place_user(user_id = request.user.id, cate_id=cate_id)
        preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(place_list)])
        place = Place.objects.filter(id__in=place_list).order_by(preserved)
        page = self.paginate_queryset(place)
        serializer = self.get_paginated_response(PlaceSerializer(page, many=True).data)
        return Response(serializer.data, status=status.HTTP_200_OK)
# project_folder/pagination.py

from rest_framework.pagination import PageNumberPagination

class BasePagination(PageNumberPagination):
    page_size_query_param = "page_size"

class PaginationHandlerMixin(object):
    @property
    def paginator(self):
        if not hasattr(self, "_paginator"):
            if self.pagination_class is None:
                self._paginator = None
            else:
                self._paginator = self.pagination_class()
        else:
            pass
        return self._paginator

    def paginate_queryset(self, queryset):
        if self.paginator is None:
            return None
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

    def get_paginated_response(self, data):
        assert self.paginator is not None
        return self.paginator.get_paginated_response(data)

  - 가져온 데이터는 프론트에서 지정해준 데이터 수량 만큼 프론트엔드로 데이터를 전달해준다.

  - 그런데 페이지네이션은 생각보다 쉬운 부분이 아니었다. 예를 들면 페이지 수에 따라 다양한 경우들을 모두 대응해줄 수 있어야 한다. 이번에 직접 지정해주었던 페이지 조건은 아래와 같다.

  ex1) current_page = 1, last_page = 1

  ex2) current_page = 1, last_page = 2

  ex3) current_page = 2, last_page = 2

  ex4) current_page = 1, last_page = 3

  ex5) current_page = 2, last_page = 3

  ex6) current_page = 3, last_page = 3

  ex7) current_page = 1

  ex8) current_page = 2

  ex9) current_page = last_page

  ex10) current_page = last_page-1

  ex11) current_page = last_page-2

  ex12) 그 외

- 위의 조건으로 아래와 같이 pagenation이라는 함수를 만들어주었다.

function pagenation(page_no, last_page_no, cate_id) {
    if ((page_no==1)&(last_page_no == 1)){
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div class="current_page">${page_no}</div></a>
            >
        `)
    }else if ((page_no == 1)&(last_page_no == 2)) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+1})">${page_no+1}</div></a>
            >
        `)
    }else if ((page_no == 2)&(last_page_no == 2)) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            >
        `)
    } else if ((page_no == 1)&(last_page_no == 3)) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+1})">${page_no+1}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${last_page_no})">${last_page_no}</div></a>
            >
        `)
    } else if ((page_no == 2)&(last_page_no == 3)) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${last_page_no})">${last_page_no}</div></a>
            >
        `)
    } else if ((page_no == 3)&(last_page_no == 3)) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-2})">${page_no-2}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            >
        `)
    } else if (page_no == 1) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <div class="no_page"></div>
            <
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+1})">${page_no+1}</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${last_page_no})">${last_page_no}</div></a>
            >
        `)
    } else if (page_no == 2) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+1})">${page_no+1}</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${last_page_no})">${last_page_no}</div></a>
            >
        `)
    }else if (page_no == last_page_no) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, 1)">1</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            >
        `)
    }else if (page_no == last_page_no-1) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, 1)">1</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+1})">${page_no+1}</div></a>
            >
        `)
    }else if (page_no == last_page_no-2) {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, 1)">1</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+1})">${page_no+1}</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+2})">${page_no+2}</div></a>
            >
        `)
    } else {
        $('#pagenation').empty()
        $('#pagenation').append(
        `
            <
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, 1)">1</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no-1})">${page_no-1}</div></a>
            <a href="#"><div class="current_page">${page_no}</div></a>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${page_no+1})">${page_no+1}</div></a>
            <div>...</div>
            <a href="#"><div onclick="UserPlaceListView(${cate_id}, ${last_page_no})">${last_page_no}</div></a>
            >
        `
    )
    }
}

  - 그런데 이 것으로 끝이 아니었다. 한 페이지에 보여주고자 하는 데이터 수가 정확히 나누어 떨어지는 경우와 아닌 경우, 그리고 pagenation.next가 없는 경우에 따라서도 조건들을 모두 만들어주어야 해서 아래와 같이 지정해주었다.

// 페이지네이션
    if (response_json.next== null) {
        if (parseInt(response_json.count%10) !== 0){
            if (response_json.count <= 10) {
                const page_no = 1
                const last_page_no = 1
                pagenation_new(page_no, last_page_no, place_id, category)
            } else {
                const last_page_no = parseInt(response_json.count/10)+1
                const page_no = last_page_no
                pagenation_new(page_no, last_page_no, place_id, category)
            }
        } else {
            const page_no = parseInt(response_json.count/10)
            const last_page_no = parseInt(response_json.count/10)
            pagenation_new(page_no, last_page_no, place_id, category)
        }        
    } else {
        if (parseInt(response_json.count%10) !== 0){
            const page_no = response_json.next.split('=')[1].split('/')[0]-1
            const last_page_no = parseInt(response_json.count/10)+1
            pagenation_new(page_no, last_page_no, place_id, category)
        }else{
            const page_no = response_json.next.split('=')[1].split('/')[0]-1
            const last_page_no = parseInt(response_json.count/10)
            pagenation_new(page_no, last_page_no, place_id, category)
        }
        
    }

 - 이번 프로젝트는 여러 가지 조건들을 모두 생각해주어야 했기 때문에 나중에 오류를 하나하나 잡기가 어려웠다.

 - 하나의 기능을 개발할 때 여러가지 조건들에 대한 대응로직을 잘 짜주는 것이 중요하다는 사실을 이번 프로젝트를 통해 알 수 있었다.

 

반응형

댓글