[Project] 예술의전당 좌석 그룹핑·가격 수립 (3)

새로운 '좌석 그룹핑' 모델 제시
Oct 04, 2023
[Project] 예술의전당 좌석 그룹핑·가격 수립 (3)
프로젝트
프로젝트명
전체 기간
2023.08.23 ~ 2023.09.27 (5주)
진행단계
단계명
(3) 새로운 '좌석 그룹핑' 모델 제시
수행 기간
2023.00.00 ~ 2023.00.00 (00일)
활용 데이터 (출처)
- 예술의전당 공연예매데이터 (빅콘테스트 2023 제공) - 음악당 좌석배치도 (예술의전당 홈페이지 ‘공간소개’) - 음악당 평면도 (나라장터 ‘음악당 콘서트홀 음향시설 개선공사’)
분석 방법
- 데이터 수집(라벨링) : AutoCAD, Excel - 데이터 전처리 : pandas, regex(정규표현식) - 파생변수 생성 : math, numpy를 활용한 벡터 계산 (UDF 생성) - 시각화 : plotly.express, matplotlib을 활용한 3d scatter plot - 군집분석 : KNN, K-means, kneed, silhouette, mse

1. 좌석별 공간환경 분석

주어진 데이터프레임에는 좌석별 공간환경에 대한 정보가 전혀 없다. 요구된 선택의 다양성 및 가격책정의 논리성을 위해서는 좌석별 위치에 따른 시야각, 시햐방해정도, 편안함 등을 반영해야 한다고 판단했다. 이를 위해 공간정보를 수집, 도면을 재구성, 좌석별 좌표를 추출하고 여러 공간환경 관련 변수를 표현했다.

공간 정보 수집

  • 예술의전당 홈페이지의 좌석배치도
    • 층, 블록, 열, 넘버 에 대한 대략적인 위치만 표시되어 있다.
    • 일부 좌석에서는 해당 좌석에서 무대를 바라봤을 경우의 시야에 대한 정보를 제공하기 위해 촬영된 사진이 제공된다. 해당 사진은 동일한 날짜와 조건에 촬영된 것으로 보인다 (무대의 의자 배치가 모두 같다).
  • 나라장터 공시된 [예술의전당 콘서트홀 음향 개선 공사] 설계도면
    • 세부적인 좌석 위치는 나와있지 않고, 좌석 열에 대한 전체적인 표시만 되어 있다.
      • notion image
 

도면 재구성 및 좌표 추출

  • 위의 두 정보를 종합하여 AutoCAD로 도면을 재구성했다.
  • 좌석 사이즈를 참고하여 좌석을 배치하고 각각의 중심의 좌표를 excel로 추출하여 ‘좌석별 좌표’ 데이터프레임을 생성했다.
    • 원점은 무대에서 지휘자가 위치하는 자리를 기준으로 하였다. 무대에서 정면으로 바라본 방향을 y축으로 정의했다.
    • 층 | 블록 | 열 | 넘버 | x좌표 | y좌표 | z좌표
 

공간환경 변수 생성

  • 무대-객석 거리
    • 각 객석별로 원점(0, 0)으로부터의 유클리드 거리를 계산하여 산출했다.
    • code (펼쳐 보기)
      # r['X']2 + r['Y']2 + r['Z']2 df['무대객석거리'] = df.apply( lambda r: np.sqrt(r['X']**2 + r['Y']**2 + r['Z']**2), axis=1).astype(int)
      notion image
  • 시선의 각도
    • 각 층-블록-열 마다 좌석의 배치가 달라진다. 특정 객석에 앉은 관객이 무대의 원점을 보기 위해서 정면으로부터 고개를 상하, 좌우로 얼마나 돌려야하는지 그 각도를 나타냈다.
    • 해당 열의 좌석 배치에 수직이 되도록 정면벡터를 구하고, 원점을 향하는 시선벡터와의 사이 각을 코사인법칙을 이용해서 산출했다.
    • code (펼쳐 보기)
      def get_stage_forward_degree(df): # 무대의 정면에서 바라봤을 때의 각도 df['무대정면과의각도'] = round(np.cos(np.arctan2(df['X'], df['Y'])), 3) return df def get_seat_forward_degree(df, origin=(0, 0)): # 각 층-블록-열 별 필요한 좌석번호, 좌표 구하기 df_temp = df.groupby(['층', '블록', '열'])['넘버'].agg([max, min]).reset_index() df_temp.columns = ['층', '블록', '열', 'max_seat', 'min_seat'] df_temp = df.merge(df_temp, on=['층', '블록', '열'], how='left') df_temp.loc[df_temp['블록'].str.contains('BOX'), 'max_seat'] = 2 # 정면벡터 구하기 df_temp = df_temp.merge(df[df['넘버'] == df_temp['max_seat']][['층', '블록', '열', 'X', 'Y']], on=['층', '블록', '열'], suffixes=('', '_max_seat'), how='left') df_temp = df_temp.merge(df[df['넘버'] == df_temp['min_seat']][['층', '블록', '열', 'X', 'Y']], on=['층', '블록', '열'], suffixes=('', '_min_seat'), how='left') df_temp['정면벡터_X'] = -(df_temp['Y_max_seat'] - df_temp['Y_min_seat']) df_temp['정면벡터_Y'] = (df_temp['X_max_seat'] - df_temp['X_min_seat']) # 시선벡터 구하기 df_temp['시선벡터_X'] = df_temp['X'] - origin[0] # 시선의 기준 (현재는 원점) df_temp['시선벡터_Y'] = df_temp['Y'] - origin[1] # 시선의 기준 (현재는 원점) # 정면벡터, 시선벡터 사이의 각도 구하기 df_temp['좌우시선각도'] = np.arccos((df_temp['정면벡터_X'] * df_temp['시선벡터_X'] + df_temp['정면벡터_Y'] * df_temp['시선벡터_Y']) / (np.sqrt(df_temp['정면벡터_X']**2 + df_temp['정면벡터_Y']**2) * np.sqrt(df_temp['시선벡터_X']**2 + df_temp['시선벡터_Y']**2))) df_temp['좌우시선각도'] = np.where(df_temp['좌우시선각도'] > np.pi / 2, np.pi - df_temp['좌우시선각도'], df_temp['좌우시선각도']) df_temp['좌우시선각도'] = np.degrees(df_temp['좌우시선각도']) df_temp = df_temp.drop(['max_seat', 'min_seat', 'X_max_seat', 'Y_max_seat', 'X_min_seat', 'Y_min_seat'], axis=1) df = df_temp.drop(['정면벡터_X', '정면벡터_Y', '시선벡터_X', '시선벡터_Y'], axis=1) # 상하시선각도 df['상하시선각도'] = abs(np.degrees(np.arctan(df['Z'] / np.sqrt(df['X']**2 + df['Y']**2)))) return df
      notion image
  • 시야 범위각
    • 객석에서 무대를 얼마나 넓은 시야로 조망할 수 있는지를 표현했다.
    • 객석에서 무대의 상하좌우 끝점 좌표를 잇는 벡터를 만들고 cond 함수를 이용하여 상하면적 시야각, 좌우면적 시야각을 산출했다.
    • code (펼쳐 보기)
      def get_stage_forward_degree(df): # 무대의 정면에서 바라봤을 때의 각도 df['무대정면과의각도'] = round(np.cos(np.arctan2(df['X'], df['Y'])), 3) return df def get_seat_forward_degree(df, origin=(0, 0)): # 각 층-블록-열 별 필요한 좌석번호, 좌표 구하기 df_temp = df.groupby(['층', '블록', '열'])['넘버'].agg([max, min]).reset_index() df_temp.columns = ['층', '블록', '열', 'max_seat', 'min_seat'] df_temp = df.merge(df_temp, on=['층', '블록', '열'], how='left') df_temp.loc[df_temp['블록'].str.contains('BOX'), 'max_seat'] = 2 # 정면벡터 구하기 df_temp = df_temp.merge(df[df['넘버'] == df_temp['max_seat']][['층', '블록', '열', 'X', 'Y']], on=['층', '블록', '열'], suffixes=('', '_max_seat'), how='left') df_temp = df_temp.merge(df[df['넘버'] == df_temp['min_seat']][['층', '블록', '열', 'X', 'Y']], on=['층', '블록', '열'], suffixes=('', '_min_seat'), how='left') df_temp['정면벡터_X'] = -(df_temp['Y_max_seat'] - df_temp['Y_min_seat']) df_temp['정면벡터_Y'] = (df_temp['X_max_seat'] - df_temp['X_min_seat']) # 시선벡터 구하기 df_temp['시선벡터_X'] = df_temp['X'] - origin[0] # 시선의 기준 (현재는 원점) df_temp['시선벡터_Y'] = df_temp['Y'] - origin[1] # 시선의 기준 (현재는 원점) # 정면벡터, 시선벡터 사이의 각도 구하기 df_temp['좌우시선각도'] = np.arccos((df_temp['정면벡터_X'] * df_temp['시선벡터_X'] + df_temp['정면벡터_Y'] * df_temp['시선벡터_Y']) / (np.sqrt(df_temp['정면벡터_X']**2 + df_temp['정면벡터_Y']**2) * np.sqrt(df_temp['시선벡터_X']**2 + df_temp['시선벡터_Y']**2))) df_temp['좌우시선각도'] = np.where(df_temp['좌우시선각도'] > np.pi / 2, np.pi - df_temp['좌우시선각도'], df_temp['좌우시선각도']) df_temp['좌우시선각도'] = np.degrees(df_temp['좌우시선각도']) df_temp = df_temp.drop(['max_seat', 'min_seat', 'X_max_seat', 'Y_max_seat', 'X_min_seat', 'Y_min_seat'], axis=1) df = df_temp.drop(['정면벡터_X', '정면벡터_Y', '시선벡터_X', '시선벡터_Y'], axis=1) # 상하시선각도 df['상하시선각도'] = abs(np.degrees(np.arctan(df['Z'] / np.sqrt(df['X']**2 + df['Y']**2)))) return df
      notion image
  • 시야방해도
    • 콘서트홀의 일부 객석은 박스석, 난간 등으로 인해 시야가 방해된다는 단점이 있다.
    • 예술의 전당 콘서트홀 좌석배치도 및 안내페이지에서 시야 사진을 이용하여, 각 좌석 별로 보이는 의자 개수를 계산하고 좌석 데이터에 맵핑했다. 해당 좌석에서 보이는 의자 개수(예시 사진 속에서는 78개)를 100점으로 환산하여 시야방해점수를 구했다.
    • knn 알고리즘을 이용하여 시야 사진이 없는 좌석들에 대해서도, 보일 것으로 예상되는 의자 개수를 맵핑했다.
    • code (펼쳐 보기)
      # https://www.sac.or.kr/site/main/content/concertHall # 의자의 총 개수 중에 몇개가 보였는지를 표현 X_train = df.loc[(~df['시야방해점수'].isna()) & (df['시야방해점수'] != 0), ['X', 'Y', 'Z']] y_train = df.loc[(~df['시야방해점수'].isna()) & (df['시야방해점수'] != 0), ['시야방해점수']] # # knn적용해서 비어있는 값 추정하기 X_test = df.loc[df['시야방해점수'].isna(), ['X', 'Y', 'Z']] model = KNeighborsRegressor(n_neighbors=3, weights='distance') model.fit(X_train, y_train) y_pred = model.predict(X_test) df.loc[df['시야방해점수'].isna(), '시야방해점수'] = y_pred df['시야방해점수'] = df['시야방해점수'].astype(int)
      notion image
  • 편의시설
    • 콘서트홀은 복도에 위치한 좌석의 앞 공간이 다른 좌석의 공간보다 비교적 넓다.
    • 넓은좌석공간 : 1층 1열, 1층 A블록 15열과 같이 앞 공간이 넓은 좌석을 표시했다.
    • 휠체어석 : 출입구와 가깝고 계단을 이용하지 않고 접근가능한 휠체어석 표시했다.
    • code (펼쳐 보기)
      # 편의시설 - 좌석공간이 넓은 자리 표시 : 1층 AE블록 15열, 모든 1열 df['좌석공간넓음'] = 0 df.loc[(df['층'] == '1층') & (df['열'] == '1'), '좌석공간넓음'] = 1 df.loc[(df['층'] == '1층') & (df['열'] == '15') & ((df['블록'] =='A블록') | (df['블록'] == 'E블록')), '좌석공간넓음'] = 1 # 편의시설 - 휠체어 좌석 df['휠체어석'] = 0 df.loc[(df['층'] == '1층') & (df['블록'] == 'C블록') & (df['열'] == '22'), '휠체어석'] = 1 df.loc[(df['층'] == '2층') & (df['블록'] == 'B블록') & (df['열'] == '8'), '휠체어석'] = 1 df.loc[(df['층'] == '2층') & (df['블록'] == 'D블록') & (df['열'] == '8'), '휠체어석'] = 1 df['1/거리**3'] = 1 / (df['무대까지의 거리'] ** 3)

2. 좌석별 고객선호도 분석

좌석별 예매율

  • 해당 공연예매데이터에는 실제로 판매된 데이터만이 나와있다.취소된 표를 제외하고, 실질적인 “좌석별예매율 = (좌석이 실제 판매된 횟수) / (좌석이 예매가능했던 공연의 수)” 로 구할 수 있다.
  • 단, 코로나로 인한 거리두기 기간에는 예매 가능한 좌석의 총 수가 2505개라고 할 수 없다. 거리두기 기간에 해당하는 공연은 구매된 좌석과 붙어있으면서 예매가 되지 않은 경우는 좌석이 불가능했던 좌석으로 정의하였다.
    • 이를 바탕으로 코로나 기간에는 오픈되지 않은 좌석을 반영한 실질적인 좌석별 예매율을 산출했다
    • notion image

빨리 예매된 좌석

  • 좌석별 관객의 선호도 파악을 위해 좌석별로 얼마나 빨리 거래되는지를 나타내는 변수를 생성했다. 예매오픈일, 선예매오픈일을 기준으로 실제 고객이 구매한 시점까지의 시간을 구하고, 이를 통해 등수를 부여하고 표준화하여 점수를 부여했다.
    • (선)예매일을 기준으로 고객의 실제 구매 시점까지의 시간을 구한다.
    • 공연별로 구매까지 걸린 시간을 기준으로 좌석에 등수를 부여한다. 거래가 일어나지 않은 경우는 최고등수 + 1 을 부여한다. 이때, 거리두기 기간으로 인접한 좌석이 구매되어 예매가 불가능했던 경우는 KNN회귀 모델을 이용하여 인접한 좌석의 등수를 통해 부여한다.
    • 공연별로 예매된 좌석의 총 개수가 달라서 등수 변수를 분석에 활용할 경우 왜곡이 발생한다. 표준화 등수 점수 = 1 - (현재 등수 - 1) / 마지막 등수 로 부여하여 모든 좌석이 0~1의 점수를 갖도록 한다. 빨리 예매된 좌석일 수록 1에 가깝게, 늦게 예매되거나 예매되지 않았을수록 0에 가까운 점수가 부여된다.
 

상관관계 분석

  • 분석 목적 : 기존의 변수 및 파생변수들에 대한 상관 관계 분석을 통해, 공연의 예매율, 좌석의 예매율과 상관 관계가 있는 변수를 파악하기 위해 상관 분석을 시행하였다. 이를 기반으로, 주요 변수를 식별하여 분석에 대한 방향성을 설정하고, 좌석 선호도에 변수들이 미치는 영향을 명확히함으로써, 군집화와 모델링을 용이하게 하고자 하였다.
  • 상관관계 분석
    • 공연예매율 상관관계 분석 : 예매까지 걸린 시간과 예매여부, 검색량과 시간변수, 검색량과 예매까지 걸린 시간 등이 양의 상관관계를 보였으나, 뚜렷한 상관 관계는 찾을 수 없었다.
    • 좌석 예매율 상관관계 분석: 예매율과 평균가격 변수간에, 거리관련 변수와 시야각도 관련 변수 간에 상관관계를 찾을 수 있었다. 해당 변수들은 추후 차원축소를 통해 변수를 통합하여 설명할 수 있을 경우, 차원축소된 주성분으로 학습을 시킬 수 있음을 확인했다.
    • notion image

3. 차원축소 (PCA)

지금까지 파악한 좌석별 환경적인 특성(시야, 편의시설 등), 선호도, 예매율, 기존에 평균적으로 부여되던 가격 등 다양한 변수를 활용하여 새로운 좌석을 그룹핑하여 제시한다. 여러 특성들을 유사한 유형끼리 묶어서 차원축소를 하여 변수의 개수를 줄이고, 그 결과로 얻는 주성분이 설명력이 있을 경우 해당 주성분을 이전 변수들 대신 사용할 수 있다.

모든 변수를 2차원으로 차원축소

  • 무대까지의거리, 시야각, 시야방해점수, 좌석공간넓음, 휠체어석, 1/거리**3, 무대정면과의각도, 시선각도 등을 2차원으로 축소할 경우 두 변수가 설명하는 분산량은 0.9996이었다. 이 두 변수가 데이터의 대부분을 설명함을 의미한다.
  • 군집화 시에는 모든 변수를 사용하는 것이 아닌 위와 같이 차원 축소된 주성분 하나 (설명되는 분산량 0.992)를 이용하여 군집화를 시행하였다.
    • notion image

변수를 카테고리별로 묶어서 차원축소

  • 관객들에게 선택의 다양성과 가격선정의 투명성을 위해 지금까지 분석한 다양한 변수들을 시야, 음향, 편의시설, 고객선호도 유형으로 분류하고, 각각을 차원축소 했다.
    • 시야 : 무대까지의 거리, 무대면적시야각, 시야방해점수, 시선각도
    • 음향 : 1/거리**3, 무대정면과의 각도, 좌석의평균가격
    • 편의시설 : 좌석공간넓음, 휠체어석여부
    • 고객선호도 : 좌석예매율, 표준화등수점수_mean
  • 좌석특성별 피쳐에 가중치(weight)를 쉽게 조정하여 공연특성에 따라 대응할 수 있다는 장점이 있지만, 설명력이 1)에 비해 다소 떨어졌다. 또, 음향 관련 데이터를 확보할 수 없어서 평균가격이나 거리 등의 변수로 대체했다는 단점도 있다.
    • 이후 좌석등급 모델에는 1)의 주성분을 사용하여 분석을 진행했다.

4. 등급 부여 (군집화)

객단가에 따른 공연 분류

  • 좌석 그룹핑 모델의 목적은 기획자의 가격 전략에 맞춰 좌석을 그룹핑 하는 것이다. 현재 주어진 정보는 모두 익명화되어 있어 세부적인 의도를 파악하기 어렵기 때문에, 일반적으로 손익분기점 및 제작비용을 반영하여 계산한다고 알려진 공연별 객단가(모든 좌석의 평균 가격)을 판단 기준으로 했다.
  • 공연의 제작비용에 따라 공연의 흥행도, 수준이 달라질 것이므로 그에 따라 가격 전략도 달라질 것이다. 이를 반영하기 위해 위의 공연 객단가의 4분위수를 기준으로 151개의 공연을 분류했다.
  • 분석 결과 4개의 공연 유형 모두 분포는 완벽한 정규 분포의 형태는 아니었으나 분포의 생김새가 비슷하고, 샘플의 개수가 비슷하다.
    • 하위 25% 이하
      하위 25% ~ 하위 50%
      하위 50% ~하위 75%
      하위 75% 이상
      기준 객단가
      54509 이하
      54509 ~ 70900
      70900 ~ 87494
      87494 이상
      공연 수 
      38
      37
      38
      38
      평균 가격
      39128
      62164
      79953
      120100
      표준편차
      12943
      4707
      5524
      29255
      notion image
      notion image

좌석 그룹핑에 사용할 변수

  • 4가지 공연마다 좌석 별 예매율, 좌석 별 표준화 된 등수 점수, 좌석의 지리적 정보를 담은 첫 번째 주성분 (설명되는 분산량 : 0.993)을 이용해 군집화를 시행하였다.
  • 군집화는 Kmeans 를 이용하여 시행하였으며, random state 는 42로 고정하였다. cluster 개수는 사전에 지정하거나, 지정하지 않을 경우 2개와 8개 사이의 클러스터 개수 중 가장 MSE를 낮게 만드는 cluster 개수를 찾도록 하였다. 3가지 변수를 min max scale 을 이용하여 스케일링 한 후 군집화를 시행하였다.
  • 4가지 공연 모두 클러스터 개수는 지정하지 않고 최적의 군집 개수를 찾았으나 4공연 모두 최적의 클러스터는 4개로 동일하였다.
  • 모델링 한 군집화에는 변수 별 중요하게 여기는 정도인 하이퍼파라미터인 변수 별 가중치를 부여하여, 그 값을 가중치가 반영된 군집화가 가능하게 했다.
    • notion image
      (위) 가중치가 모두 동일한 경우, (아래) 가중치를 100 : 10 : 1 로 변경한 경우 예시
      (위) 가중치가 모두 동일한 경우, (아래) 가중치를 100 : 10 : 1 로 변경한 경우 예시

군집 분석 결과 스무딩(smothing)

  • 군집분석 결과 좌석의 등급은 기준 경계가 자연스럽지 않다. 이런 불규칙한 경계는 기획자의 입장에서도 가격을 책정하기 어렵고, 고객의 입장에서도 이해하기 어려울 수 있다. 등급별 경계를 자연스럽게 처리할 필요가 있다.
  • 1차로 군집 분석된 결과를 부드러운 경계 기준을 설정하기 위해 KNN 분류 문제를 이용하여 (p = 1, weight = ‘uniform’) 경계를 조정했다.
  • 이 때 사용 할 이웃 개수 k 도 사용자가 설정해야하는 하이퍼파라미터로서 해당 분석에는 50개의 좌석을 참조하였다. 이웃 개수 k 는 높아질 수록 경계 기준이 부드럽게 완화 되며 낮아 질 수록 경계 기준이 기존 경계 기준에 가까워 지는 모습을 보인다.
    • notion image

실루엣 계수

  • 평균가가 1사분위수 이하인 가격들인 공연에 대해 스무딩한 결과는 다음과 같다. 스무딩 전에 비해 19%의 값들이 기존 값과 다른 값을 가지게 된 모습을 볼 수 있다.
  • 이러한 스무딩은 군집들의 평균 실루엣 단점이 존재한다. 군집화가 잘 된 데이터들을 인위적으로 군집의 값을 변경시키다 보니 실루엣 계수가 스무딩 전에 비해 내려가는 경향을 보인다.
    • notion image
  • 하지만 이러한 과정은 최적의 조건에 따라 좌석을 그룹핑하면서도 고객에게 수용 가능한 경계 기준을 제공한다는 점에서 의의가 있다고 판단돼서 좌석 그룹핑 모델에 적용하기로 했다.

Share article
RSSPowered by inblog