카테고리 보관물: 미분류

간단한 기여도 계산 함수

기여도

기여도란 어떤 성과지표에서 어떤 부분집합이 전체의 성과지표에 얼마나 형향을 주었는지 계산하는 것입니다. 리프트(lift)라고도 합니다. 리프트는 알고리즘이나 계산마다 각기 다르므로 일반적으로 말하는 리프트가 이것과 동일한 것은 아닙니다.

비율값이 지표인 경우에 총 지표를 만드는데 구성된 멤버 중 하나의 기여도가 얼마인지를 알아내는 간단한 함수입니다. 비율값이 아닌 지표에서의 기요도는 그냥 전체에서 해당 멤버가 차지하는 비중을 계산하면 되기 때문에 계산할 것이 없습니다.

비율값이 KPI인 경우를 예를 하나 들면 강고 배너 캠페인 5개를 운영하고 나서 전체 CTR과 각 배너별 CTR이 있을 때 배너별로 기여도를 계산하거나 하는 경우입니다.

비율값이 KPI인 경우에 기여도 계산이 어려운 이유

위의 표와 같은 5개의 캠페인이 있는데 클릭과 노출이 다 다릅니다. 이때 캠페인1과 캠페인2는 모두 CTR 0.5로 50%씩 동일합니다. 그런데 캠페인2는 캠페인1에 비해 노출과 클릭이 각각 모두 10배 많습니다.

상식적으로 생각할 때는 이런 경우 캠페인1이 기여도가 더 높아야 하는 것입니다.
기여도 계산에서 이것이 반영되야 합니다.

코드

코드는 R로 되어 있습니다. 하려고 했던 것은 calc_contrib_origin이라는 함수에서 계산할 때 분모가 0이 되는 문제로 기여도가 아예 계산이 안되는 것을 어떻게 해결할 것인가입니다.

이때 비율을 계산하는 것이기 때문에 분모가 0이 안되도록 하는 것이 중요한데 이 부분이 생각보다 까다롭습니다.

# 원래 함수
calc_contrib_origin <- function(total_z, total_c, z, c) {
  ((total_z / total_c) - ((total_z - z) / (total_c - c)))
}
 
# 멍청한 함수 v0
calc_contrib_mod1 <- function(total_z, total_c, z, c) {
  frac <- total_c / c
  ((total_z / total_c) - ((z*frac) / (c*frac)))
}
 
# 멍청한 함수 v1
calc_contrib_mod1 <- function(total_v1, total_v2, v1, v2) {
  frac1 <- total_v1 / total_v2
  frac2 <- total_v2 / total_v1
  if ((total_v1 == v1) && (total_v2 == v2)) {
    cat("case 1\n")
    (total_v1 / total_v2)
    right <- 0
  }
  else if ((total_v1 != v1) && (total_v2 == v2)) {
    cat("case 2\n")
    (total_v1 / total_v2) - ((total_v1 - (frac2 * v1)) / v2)
    right <- ((total_v1 - (frac2 * v1)) / v2)
  }
  else if ((total_v1 == v1) && (total_v2 != v2)) {
    print("case 3\n")
    (total_v1 / total_v2) - (v1 / (total_v2 - (v2 * frac1)))
    right <- (v1 / (total_v2 - (v2 * frac1)))
  } else {  #
    cat("case 4\n")
    (total_v1 / total_v2) - ((total_v1 - v1) / (total_v2 - v2))
    right <- ((total_v1 - v1) / (total_v2 - v2))
  }
  left <- (total_v1 / total_v2)
  cat(paste0(left, " - ", right, "\n"))
  return(left - right)
}
 
# 멍청한 함수 v2
calc_contrib_mod2 <- function(total_v1, total_v2, v1, v2) {
  # frac1 <- total_v1 / v1
  # frac2 <- total_v2 / v2
  frac1 <- total_v1 / total_v2
  frac2 <- total_v2 / total_v1
  # 기준값
  if ((total_v1 == v1) && (total_v2 == v2)) {
    cat("case 1\n")
    (total_v1 / total_v2)
    right <- 0
  }
  # 분모가 0인 케이스
  else if ((total_v1 != v1) && (total_v2 == v2)) {
    cat("case 2\n")
    #total_v1 / (total_v2 - v2)
    right <- (v1 / (total_v2 - (v2 * frac1)))
  }
  # 분자가 0인 케이스
  else if ((total_v1 == v1) && (total_v2 != v2)) {
    print("case 3\n")
    right <- total_v1 / (total_v2 - v2)
  } else {  #
    cat("case 4\n")
    (total_v1 / total_v2) - ((total_v1 - v1) / (total_v2 - v2))
    right <- ((total_v1 - v1) / (total_v2 - v2))
  }
  left <- (total_v1 / total_v2)
  cat(paste0(left, " - ", right, "\n"))
  return(left - right)
}
 
# 덜 멍청한 함수 v1
calc_contrib_mod1 <- function(total_z, total_c, z, c) {
  (((total_z + 1) / (total_c + 1)) - ((total_z - z + 1) / (total_c - c + 1)))
}
 
# 덜 멍청한 함수 v2
calc_contrib_mod2 <- function(total_z, total_c, z, c) {
  lb <- 1 - 1 / (total_c + 1)
  rb <- 1 - (total_z + 1) / 1
  print(c(lb, rb, rb - lb))
  ( (((total_z + 1) / (total_c + 1)) - ((total_z - z + 1) / (total_c - c + 1))) ) / (rb - lb)
}
 
calc_contrib_mod <- calc_contrib_mod1
 
# ※ 전체 비율에서 항목값들이 존재하지 않았을 경우를 계산하는 것이 전부
# ※ 따라서 전체 비율을 계산하고 대상 항목의 수치를 포함하지 않은 것의 비율을 계산해서 뺀다
 
# 간단한 테스트
calc_contrib_mod(50, 100, 49, 60)
calc_contrib_mod(50, 100, 50, 60)
calc_contrib_mod(50, 100, 50, 59)
 
# 계산검증1. (o)
calc_contrib_mod(200, 250, 202, 250) # 아래 보다는 높아야 함 (o)
calc_contrib_mod(200, 250, 201, 250) # 아래 보다는 높아야 함 (o)
calc_contrib_mod(200, 250, 200, 250) # 기여도 100% (기준 기여도와 동일) (o)
calc_contrib_mod(200, 250, 199, 250) # 위의 것 보다는 낮아야 함 (o)
calc_contrib_mod(200, 250, 198, 250) # 위의 것 보다는 낮아야 함 (o)
 
calc_contrib_mod1(200, 250, 200, 0)
 
# 계산검증1-1 (o)
calc_contrib_mod(200, 250, 101, 50) # 아래 것 보다는 높아야 함 (o)
calc_contrib_mod(200, 250, 100, 50) # 아래 것 보다는 높아야 함 (o)
calc_contrib_mod(200, 250, 100, 51) # 위의 것 보다 낮아야 함 (o)
 
# 계산검증2 (o)
calc_contrib_mod(250, 200, 251, 200) # 기준기여도 보다 높거나 같아야 함
calc_contrib_mod(250, 200, 250, 200) # 기여도 100% (기준 기여도와 동일)
calc_contrib_mod(250, 200, 249, 200) # 위의 것 보다 낮아야 함 (o)
calc_contrib_mod(250, 200, 248, 200) # 위의 것 보다 낮아야 함 (o)
calc_contrib_mod(250, 200, 247, 200) # 위의 것 보다 낮아야 함 (o)
 
# 계산검증3 (o) 점진적으로 높아지는
calc_contrib_mod(250, 200, 50, 100)
calc_contrib_mod(250, 200, 100, 100)
calc_contrib_mod(250, 200, 100, 50)
calc_contrib_mod(250, 200, 100, 2)
calc_contrib_mod(250, 200, 100, 1)
 
# 계산검증4 (o) 점진적으로 낮아지는
calc_contrib_mod(250, 200, 100, 1)
calc_contrib_mod(250, 200, 100, 2)
calc_contrib_mod(250, 200, 100, 99)
calc_contrib_mod(250, 200, 100, 100)
calc_contrib_mod(250, 200, 100, 101)
calc_contrib_mod(250, 200, 100, 102)
 
# 계산검증5 (o). 다운 방향으로 기여도가 떨어져야함
calc_contrib_mod(110, 110, 100, 1)
calc_contrib_mod(110, 110, 100, 2)
calc_contrib_mod(110, 110, 100, 99)
calc_contrib_mod(110, 110, 100, 100)
calc_contrib_mod(110, 110, 100, 101)
calc_contrib_mod(110, 110, 100, 102)
 
# 계산검증6 (오류)
calc_contrib_mod(110, 110, 110, 1) # 아래것 보다 좋음 (o)
calc_contrib_mod(110, 110, 110, 2) # 아래것 보다 좋음 (o)
calc_contrib_mod(110, 110, 110, 99) # 아래것 보다 좋음 (o)
calc_contrib_mod(110, 110, 110, 100) # 아래것 보다 좋음 (o)
calc_contrib_mod(110, 110, 110, 101) # 아래것 보다 좋음 (o)
calc_contrib_mod(110, 110, 110, 102) # 아래것 보다 좋음 (o)
calc_contrib_mod(110, 110, 110, 109) # 아래것 보다 좋음 (x)
calc_contrib_mod(110, 110, 110, 110) # 기준
calc_contrib_mod(110, 110, 109, 109)
calc_contrib_mod(110, 110, 108, 108)
 
calc_contrib_mod(110, 110, 110, 111) # ※ 입력이 잘못된 케이스.
calc_contrib_mod(110, 110, 110, 112) # ※ 입력이 잘못된 케이스.
 
# 계산검증7 (x)
# 항목값이 가분수가 되면 결과값이 마이너스가 되는데
# 이것들이 기준값과 진분수인값들과 차이가 생긴다.
calc_contrib_mod(210, 110, 110, 1) # 아래것 보다 좋음 (o)
calc_contrib_mod(210, 110, 110, 2) # 아래것 보다 좋음 (o)
calc_contrib_mod(210, 110, 110, 99) # 아래것 보다 좋음 (o)
calc_contrib_mod(210, 110, 110, 100) # 아래것 보다 좋음 (o)
calc_contrib_mod(210, 110, 110, 101) # 아래것 보다 좋음 (o)
calc_contrib_mod(210, 110, 110, 102) # 아래것 보다 좋음 (o)
calc_contrib_mod(210, 110, 110, 109) # 아래것 보다 좋음 (x)
calc_contrib_mod(210, 110, 110, 110)
calc_contrib_mod(210, 110, 100, 110) # 위 보다는 안좋음
calc_contrib_mod(210, 110, 1, 110) # 위 보다는 안좋음
 
# -- 비교 (origin함수) (x)
calc_contrib_origin(210, 110, 110, 1) # 아래것 보다 좋음 (o)
calc_contrib_origin(210, 110, 110, 2) # 아래것 보다 좋음 (o)
calc_contrib_origin(210, 110, 110, 99) # 아래것 보다 좋음 (o)
calc_contrib_origin(210, 110, 110, 100) # 아래것 보다 좋음 (o)
calc_contrib_origin(210, 110, 110, 101) # 아래것 보다 좋음 (o)
calc_contrib_origin(210, 110, 110, 102) # 아래것 보다 좋음 (o)
calc_contrib_origin(210, 110, 110, 109) # 아래것 보다 좋음 (x)
calc_contrib_origin(210, 110, 110, 110)
calc_contrib_origin(210, 110, 100, 110) # 위 보다는 안좋음
calc_contrib_origin(210, 110, 1, 110) # 위 보다는 안좋음
 
# 계산검증8. (o)
calc_contrib_mod(110, 210, 1, 110)
calc_contrib_mod(110, 210, 109, 110) # 위 보다는 좋음
calc_contrib_mod(110, 210, 110, 110) # 위 보다는 좋음
 
# origin 함수 확인
calc_contrib_origin(110, 210, 109, 110) # 위 보다는 좋음
calc_contrib_origin(110, 210, 110, 110) # 위 보다는 좋음
 
# 심1
calc_contrib_origin(110, 210, 109, 110)
calc_contrib_origin(210, 110, 110, 109)
 
# 산수 노트
at <- 50
bt <- 100
a <- 30
b <- 15
(at / bt) - ((at - a) / (bt - b))
(at / bt) - ((at - a) * (bt - b)^-1)
(2 - 3)^2
2^2 + -2*(2*3) + 3^2
(2 - 3)^-1
# a²-2ab+b² = (a-b)²

골 프로그래밍 – Goal Programming with Excel

골 프로그래밍은 제목만 봐서는 직감적으로 알기 어려울 수 있습니다.
최대한 목표에 가깜게 하는 조건을 찾아주는 선형최적화 방법입니다.
원래는 프로그래밍이나 별도의 최적화 도구를 사용하는데 최근에는 엑셀로도 많이 작업합니다.

첨부한 링크를 보시고 잘 따라해보면 어렵지 않습니다. 다른 포스트에서 자세한 설명을 하기로 하고 이 포스트에서는 간단한 개요만 설명드립니다.

이하 평어체로 씁니다.

개요

엑셀에는 해 찾기(solver, linear programming) 기능이 있다.
선형계획법(Linear Optimization)을 엑셀로 구현한 것이다.
하지만 단순 선형계획법은 단일 타겟에 대한 최적화만 할 수 있기 때문에 다중 타겟을 최적화하기 위해서는 목표계획법(goal programming)을 사용해야한다.

목표 계획법 (Goal Programming) 또는 MLOP (Multi Linear Optimization Programming)

difference 값을 추가 도입해서 여러 개의 목표함수를 하나로 만든다.
그 후 그 하나의 함수를 최소화하는 최적조합을 simplex 알고리즘으로 찾아낸다.

엑셀 및 코드

엑셀로도 goal programming을 할 수 있는 것으로 알려져 있다.
엑셀로 goal programming을 시도해서 각 조건에서 최적 선택이나 최적 점을 찾는데 사용한다.
이후 Python 또는 R로 코드로 포팅할 수 있다.

쓸모

  • “비융율과 전환율을 각각 최대로 맞출 수 있는 rqs 또는 cic의 연속 노출 지점을 찾으라” 같은 것
  • 내가 돌봐주는 광고주에 impression-capping을 하려고 한다. ci_c 값은 얼마가 최적값(optima)일까?
  • 새로 들어온 광고주의 일예산은 얼마가 적당할까?

참고자료

잘 설명된 슬라이드를 참고한다.

https://sceweb.sce.uhcl.edu/helm/SENG5332DecisionAlysys/my_files/TableContents/Module-13/ch07.ppt

프론트에서 활용하기

Excel로 할 수 있으면 R과 Python, Javascript를 이용해서도 “Goal programming”을 할 수 있다.

최적값 찾는 기능을 서비스에 넣거나 배치 프로세스에서 자동으로 설정하게 할 수 있다.

따라해보기

단순 “해 찾기” Linear programming

“해 찾기”를 먼저 해본다. (“Goal programming”이 아님)

이것은 매우 쉽다. (바보가 아니면 다 할 수 있다)

cost_rate와 roas를 7:3의 가중치로 최대화하는 rqs의 연속 선택 시퀀스를 30개 이내에서 찾으라. 단 선택을 한다면 rqs=0부터 시작해야 한다.

local target 계산은 가중조화평균(아무짝에도 쓸모 없는 것이지만 테스트해본다)

패널티 트릭 Penalty trick

골 프로그래밍의 핵심은 패널티를 주기 위해 선형식 디자인을 어떻게 하는 가 이다.

연습을 해보지 않으면 디자인하는 것이 매우 헷갈리기 때문에 디자이닝에 대한 훈련이 되어 있어야 하고 고민도 깊이 해야 한다.

CTR이 높고 CVR이 낮은 것과 CTR이 낮고 CVR이 높은 것 중 어느 것이 좋은가?

광고 캠페인을 운영하다보면 비슷한 또는 동일한 캠페인인데 매체 또는 DSP업체 성과가 다음과 같이 다른 경우가 있습니다.

  • CTR은 높지만 CVR이 낮은 것
  • CVR은 높지만 CTR이 낮은 것

CTR과 CVR은 단순하게는 DSP의 타겟팅 능력과 매체의 품질에 가장 큰 영향을 받습니다만
그럼에도 불구하고 둘 중 어느 것이 좋은가를 기술적으로 볼 때

  • 광고업체(매체와 DSP모두)의 입장에서는 1번이 좋습니다.
  • 광고주의 입장에서는 2번이 좋습니다.

아직도 광고비 과금을 CPC(클릭당 과금) 방식으로 많이하기 때문입니다. 클릭율이 높으면 과금율이 높아지므로 매체( CPC매체인 경우)와 광고회사에게 좋습니다.

하지만 여기에 생각할 부분이 더 있습니다.

CPC가 만약 가변인 경우입니다. 즉 몇몇 광고 업체처럼 가변 CPC로 KPI에 따라 목표치에 최대한 근접하게 광고를 운영한다고 하면 생각할 것이 많아집니다.

CTR이 낮아도 광고요금이 상대적으로 높을 수 있고 CTR이 높아도 광고요금이 낮을 수 있습니다. 즉 CTR만으로는 광고의 성과를 정확하게 알아낼 수 없습니다.

결국 cVR로 마찬가지가 됩니다. 클릭 후에 오디언스의 액션에 따라 전환되는 비율을 측정하는데 CVR이 조금 낮더라도 전환수가 많다면 CVR이 높아도 전환수가 많지 않은 것 보다는 좋습니다.

CPC가 만약 고정이라면

그래도 CTR이 높은 경우가 사실은 더 유리합니다.

CVR은 원래 CTR보다 높게 형성되지만 보통 물건의 품질, 가격경쟁력, 브랜드파워에 따라 달라집니다. 그럼에도 불구하고 cTR이 높아져서 전환수가 많아지는 것이 전환률이 높은 것보다는 광구주 측이의 매출이나 이윤이장에서는 유리합니다.

결론

많은 경우에 CTR이 높고 CVR이 낮은 것이 더 좋습니다. 그 반데의 케이스보다는 유리합니다. 하지만 항상 그런 것은 아닙니다.