8-3. 회귀분석+Apps Script+구글시트(대시보드)

회귀분석 + Apps Script를 활용하여 전사에서 사용할 수 있는 Google Sheet 만들기

🧩 구글 시트 구성 개요
시트1: raw_data – 분석할 데이터셋 : Google Docs8-2.Advertising_Data_Regression
시트2: model_summary – 변수명, 회귀계수, 분석 리포트
시트3: optimizer_ui – 총 예산($) 입력하면
1.
최적의 마케팅 비용 집행 계획 수립 (마케팅 믹스 모델링-MMM)
캠페인별 추천 예산 ($) 제안
최적의 예상 판매량 제안
2.
전략 유형별 3가지 마케팅 비용 집행안 제언
ROI 극대화형, 리스크 분산형, 잠재력 실험형

📋 전체 시스템 흐름

1.
raw_data: 분석할 데이터세트
2.
Apps Script (Backend):
데이터를 읽어서 Gemini API로 전송합니다.
Gemini에게 "회귀분석 수행 + 회귀계수 추출 + 분석 리포트 작성"을 요청합니다.
3.
model_summary: Gemini가 응답한 계수(숫자)와 분석 보고서(텍스트)를 자동으로 기록합니다.
4.
optimizer_ui: model_summary의 계수를 참조하여 예산 배분 시뮬레이션을 돌립니다.

🛠️ 단계별 구현 가이드

1단계: 준비 사항 (Gemini API 키 발급)

가장 먼저 AI를 사용할 수 있는 '열쇠'가 필요합니다.
1.
Google AI Studio에 접속합니다.
2.
Get API Key를 클릭하여 키를 생성하고 복사해둡니다.

2단계: 시트 구조 잡기 (탭 3개 생성)

a.
파일 > 사본 만들기 > 각자 구글 드라이브에 저장
b.
8-2.Advertising_Data > raw_data로 이름 변경 (대소문자 정확히)
2.
model_summary: 빈 시트 추가하기
3.
optimizer_ui: 빈 시트 추가하기

3단계: Apps Script 작성

구글 시트 상단 메뉴 확장 프로그램 Apps Script를 열고, 기존 내용을 지운 뒤 아래 Code.gs 전문을 복사해서 붙여넣고 > 저장
Code.gs - 펼쳐보기
// ========================================== // 🚀 AI 마케팅 분석 시스템 (Gemini 2.5 Flash) // ========================================== // 1. 설정 변수 const GEMINI_MODEL_ID = 'gemini-2.5-flash'; // 최신 모델 const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL_ID}:generateContent`; // 2. Gemini API 호출 함수 (핵심 엔진) function callGemini(prompt) { // 스크립트 속성에서 API 키 가져오기 const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY'); if (!apiKey) { throw new Error("API 키가 없습니다. [설정 > 스크립트 속성]에 'GEMINI_API_KEY'를 추가하세요."); } if (!prompt || prompt.trim() === "") { throw new Error("프롬프트가 비어 있습니다."); } // API 요청 본문 (Payload) const payload = { contents: [ { role: "user", parts: [{ text: prompt }] } ] }; const options = { method: 'post', contentType: 'application/json', payload: JSON.stringify(payload), muteHttpExceptions: true }; // API 호출 const urlWithKey = `${GEMINI_API_URL}?key=${apiKey}`; const response = UrlFetchApp.fetch(urlWithKey, options); const responseCode = response.getResponseCode(); const responseText = response.getContentText(); // 에러 처리 if (responseCode !== 200) { throw new Error(`Gemini API 오류 (${responseCode}): ${responseText}`); } const json = JSON.parse(responseText); // 응답 추출 if (json.candidates && json.candidates.length > 0 && json.candidates[0].content) { return json.candidates[0].content.parts[0].text; } else { if (json.promptFeedback && json.promptFeedback.blockReason) { throw new Error(`AI 응답 차단됨 (사유: ${json.promptFeedback.blockReason})`); } throw new Error("AI 응답이 비어 있습니다."); } } // 3. 데이터 분석 실행 함수 (시트 연동) function runAIAnalysis() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const rawSheet = ss.getSheetByName('raw_data'); const summarySheet = ss.getSheetByName('model_summary'); if (!rawSheet || !summarySheet) { SpreadsheetApp.getUi().alert("필수 시트 누락: 'raw_data', 'model_summary' 시트가 필요합니다."); return; } // 데이터 가져오기 const data = rawSheet.getDataRange().getValues(); if (data.length <= 1) { SpreadsheetApp.getUi().alert("'raw_data' 시트에 데이터가 없습니다."); return; } // CSV 변환 const csvString = data.map(row => row.join(",")).join("\n"); // 프롬프트 작성 const prompt = ` 당신은 데이터 분석 전문가입니다. 아래 데이터를 분석하여 'Product_Sold' 판매량 예측을 위한 회귀분석 리포트를 작성하세요. [데이터] ${csvString} [요청사항] 1. 'Product_Sold'를 종속변수로 하는 다중회귀분석을 수행하여 각 채널의 계수(Coefficient)와 절편(Intercept)을 도출하세요. 2. 경영진을 위한 핵심 인사이트 리포트를 작성하세요. 3. 결과는 반드시 아래 JSON 포맷으로만 출력하세요. (마크다운 없음, 순수 JSON) { "coefficients": { "TV": 0.0, "Billboards": 0.0, "Google_Ads": 0.0, "Social_Media": 0.0, "Influencer_Marketing": 0.0, "Affiliate_Marketing": 0.0, "Intercept": 0.0 }, "report": "여기에 줄바꿈을 사용하여 리포트 내용을 작성하세요." } `; try { ss.toast("Gemini 2.5 Flash가 분석 중입니다...", "🚀 분석 시작", 10); const resultText = callGemini(prompt); // JSON 파싱 (마크다운 제거) const cleanJson = resultText.replace(/```json/g, "").replace(/```/g, "").trim(); let result; try { result = JSON.parse(cleanJson); } catch (e) { throw new Error("AI 응답 형식이 올바르지 않습니다. 원본: " + resultText.substring(0, 50) + "..."); } // 시트 초기화 summarySheet.clear(); // 헤더 작성 summarySheet.getRange("A1:C1").setValues([["변수명", "계수", "분석 리포트"]]); summarySheet.getRange("A1:C1").setFontWeight("bold").setBackground("#EFEFEF"); // 계수 데이터 채우기 let row = 2; if (result.coefficients) { for (const key in result.coefficients) { summarySheet.getRange(row, 1).setValue(key); summarySheet.getRange(row, 2).setValue(result.coefficients[key]); row++; } } // 리포트 채우기 if (result.report) { const reportCell = summarySheet.getRange("C2"); reportCell.setValue(result.report); reportCell.setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); reportCell.setVerticalAlignment("top"); } // 시트 디자인 정리 summarySheet.getRange(2, 3, 15, 6).merge(); summarySheet.setColumnWidth(1, 150); summarySheet.setColumnWidth(2, 100); summarySheet.setColumnWidth(3, 600); // 🔁 여기서 optimizer_ui 템플릿 자동 생성 setupOptimizerUISheet(); ss.toast("분석이 완료되었습니다!", "✅ 성공", 5); } catch (e) { SpreadsheetApp.getUi().alert("오류 발생:\n" + e.toString()); } } // 🔧 optimizer_ui 기본 UI 자동 생성 function setupOptimizerUISheet() { const ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName('optimizer_ui'); // 시트가 없으면 새로 만들고, 있으면 내용만 비우기 if (!sheet) { sheet = ss.insertSheet('optimizer_ui'); } else { // 🔹 시트 전체가 아니라 1~20행만 초기화 const maxCols = sheet.getMaxColumns(); // 시트 전체 컬럼 수 sheet.getRange(1, 1, 20, maxCols).clear(); // 또는 clearContent()만 써도 됨 } // ===== 1행: 총 예산 ===== sheet.getRange('A1').setValue('총 예산 ($)').setFontWeight('bold'); sheet.getRange('B1').setValue(0); sheet.getRange('C1').setValue('← 여기에 예산 금액 입력'); // ===== 3~5행: 채널 / 최소비중 / 최대비중 ===== // 채널 목록 (원하면 나중에 시트에서 직접 수정 가능) const channels = [ 'TV', 'Billboards', 'Google_Ads', 'Social_Media', 'Influencer_Marketing', 'Affiliate_Marketing' ]; // 3행: 채널명 헤더 + 각 채널 이름 sheet.getRange('A3').setValue('채널명').setFontWeight('bold'); sheet.getRange(3, 2, 1, channels.length).setValues([channels]).setFontWeight('bold'); // 4행: 최소 비중 (기본 5%) sheet.getRange('A4').setValue('최소 비중').setFontWeight('bold'); const minShares = [0.05, 0.05, 0.05, 0.05, 0.05, 0.05]; // 5% sheet.getRange(4, 2, 1, channels.length).setValues([minShares]); sheet.getRange(4, 2, 1, channels.length).setNumberFormat('0%'); // 5행: 최대 비중 (기본 50%) sheet.getRange('A5').setValue('최대 비중').setFontWeight('bold'); const maxShares = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5]; // 50% sheet.getRange(5, 2, 1, channels.length).setValues([maxShares]); sheet.getRange(5, 2, 1, channels.length).setNumberFormat('0%'); // ===== 7행: 최적화 결과 제목 + 수식 ===== sheet.getRange('A7').setValue('[최적화 결과]').setFontWeight('bold'); // B7: ArrayFormula로 OPTIMIZE_BUDGET 호출 // 총 예산: B1 // 채널 범위: B3 ~ 마지막 채널 // 최소/최대 비중: B4/B5 ~ 마지막 채널 const lastColLetter = sheet.getRange(3, 1 + channels.length).getA1Notation().replace(/[0-9]/g, ''); // 예: G const formula = `=ArrayFormula(OPTIMIZE_BUDGET(B1, B3:${lastColLetter}3, B4:${lastColLetter}4, B5:${lastColLetter}5))`; sheet.getRange('B7').setFormula(formula); // 보기 좋게 약간 포맷팅 sheet.setColumnWidths(1, 1, 140); // A열 sheet.setColumnWidths(2, channels.length, 120); // B~G열 sheet.getRange('A1:C1').setVerticalAlignment('middle'); sheet.getRange('A3:A5').setVerticalAlignment('middle'); // 안내 문구가 잘 보이도록 줄바꿈 sheet.getRange('C1').setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); ss.toast('optimizer_ui 기본 템플릿이 생성되었습니다.', '✅ 완료', 5); } // 💰 4. 예산 최적화 UI 함수 (범위 인식 오류 해결 버전) function OPTIMIZE_BUDGET(totalBudget, channelNames, minShares, maxShares) { // 1. 필수 입력값 체크 if (!totalBudget || !channelNames || !minShares || !maxShares) { return [["💡 안내: 수식을 완성해 주세요.\n사용법: =OPTIMIZE_BUDGET(총예산, 채널명범위, 최소비중범위, 최대비중범위)"]]; } // 2. model_summary 시트에서 계수 읽어오기 const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('model_summary'); if (!sheet) return [["Error: 'model_summary' 시트가 없습니다."]]; const data = sheet.getDataRange().getValues(); let coeffs = {}; let intercept = 0; // 계수 파싱 (데이터가 있는 곳까지 순회) for (let i = 1; i < data.length; i++) { // 빈 행이면 패스 if (!data[i][0]) continue; let key = data[i][0].toString().trim(); let val = data[i][1]; // 숫자가 아니면 패스 if (typeof val !== 'number') continue; if (key.toLowerCase().includes('intercept') || key === '상수항' || key === '절편') { intercept = val; } else { coeffs[key] = val; } } // 3. [핵심 수정] 입력 범위 평탄화 (Flatten) 처리 // 2차원 배열([[A,B,C]])이든 1차원이든 무조건 1줄로 폄 var channels = flattenInput(channelNames); var mins = flattenInput(minShares); var maxs = flattenInput(maxShares); // 길이 체크 (채널명 개수와 비중 개수가 다르면 경고) if (channels.length !== mins.length || channels.length !== maxs.length) { return [["Error: 채널명 개수와 비중(최소/최대) 입력 칸 수가 다릅니다. 범위를 똑같이 맞춰주세요."]]; } var allocation = {}; var currentSpend = 0; var positiveCoeffSum = 0; // 4. 최적화 로직 실행 for (var i = 0; i < channels.length; i++) { // 빈 셀은 건너뜀 if (channels[i] === "" || channels[i] === null) continue; var ch = channels[i].toString().trim(); var cf = coeffs[ch]; // model_summary에 없는 채널명이면 0 처리 (에러 방지) if (cf === undefined) { cf = 0; // 디버깅용: return [["Error: '" + ch + "' 채널의 계수를 찾을 수 없습니다. model_summary를 확인하세요."]]; } var minAmt = totalBudget * (Number(mins[i]) || 0); allocation[ch] = { budget: minAmt, coeff: cf, maxShare: (maxs[i] !== undefined && maxs[i] !== "") ? Number(maxs[i]) : 1.0 }; currentSpend += minAmt; if (cf > 0) positiveCoeffSum += cf; } // 잔여 예산 배분 var remain = totalBudget - currentSpend; if (remain > 0 && positiveCoeffSum > 0) { for (var key in allocation) { var item = allocation[key]; if (item.coeff > 0) { var extra = remain * (item.coeff / positiveCoeffSum); // Max Cap 체크 var maxLimit = totalBudget * item.maxShare; if ((item.budget + extra) > maxLimit) { extra = Math.max(0, maxLimit - item.budget); } item.budget += extra; } } } // 5. 결과 테이블 생성 var output = [["채널명", "추천 예산 ($)", "비중 (%)", "효율 계수"]]; var totalSales = intercept; var sumBudget = 0; for (var i = 0; i < channels.length; i++) { if (channels[i] === "" || channels[i] === null) continue; var ch = channels[i].toString().trim(); var item = allocation[ch]; if (item) { totalSales += item.budget * item.coeff; sumBudget += item.budget; output.push([ ch, Math.round(item.budget), (totalBudget > 0 ? (item.budget/totalBudget*100).toFixed(1) : 0) + "%", item.coeff.toFixed(4) ]); } } output.push(["합계", Math.round(sumBudget), "100%", "-"]); output.push(["📈 예상 판매량", Math.round(totalSales), "-", "-"]); return output; } // [도우미 함수] 2차원 배열을 1차원으로 펴주는 함수 (에러 해결사) function flattenInput(input) { if (!input) return []; if (Array.isArray(input)) { // 배열 안에 배열이 있으면(2D) 펴고, 아니면 그대로 return input.reduce(function(acc, val) { return acc.concat(Array.isArray(val) ? flattenInput(val) : val); }, []); } return [input]; // 단일 값이면 배열로 감싸서 리턴 } // 5. 메뉴 생성 (시트 열릴 때 자동 실행) function onOpen() { SpreadsheetApp.getUi() .createMenu('🚀 AI 분석 시스템') .addItem('데이터 분석 실행 (Gemini 2.5 Flash)', 'runAIAnalysis') .addToUi(); } // 🧪 5. AI 다중 시나리오 생성 함수 (Gemini가 3가지 전략 제안) // 🧪 5. AI 다중 시나리오 생성 함수 (예상 판매량 자동 계산 추가) function runScenarioAnalysis() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const summarySheet = ss.getSheetByName('model_summary'); const uiSheet = ss.getSheetByName('optimizer_ui'); // 1. 데이터 읽기 (총 예산, 계수, 절편) const totalBudget = uiSheet.getRange("B1").getValue(); const summaryData = summarySheet.getDataRange().getValues(); let coeffs = {}; let intercept = 0; let coeffInfo = ""; // 계수와 절편 분리해서 저장 for (let i = 1; i < summaryData.length; i++) { let key = summaryData[i][0].toString().trim(); let val = summaryData[i][1]; if (typeof val === 'number') { if (key.toLowerCase().includes('intercept') || key === '상수항' || key === '절편') { intercept = val; } else { coeffs[key] = val; coeffInfo += `- ${key}: ${val}\n`; } } } if (!coeffInfo) { SpreadsheetApp.getUi().alert("먼저 'model_summary' 시트에 계수 데이터가 있어야 합니다."); return; } // 2. 프롬프트 작성 const prompt = ` 당신은 마케팅 최고 전략가(CMO)입니다. 현재 우리 회사의 광고 채널별 효율(회귀계수)은 아래와 같습니다. [채널 효율 데이터] ${coeffInfo} 총 예산: $${totalBudget} 위 데이터를 바탕으로 **3가지 서로 다른 마케팅 예산 배분 시나리오**를 제안해주세요. 1. **ROI 극대화형**: 효율이 높은 채널에 예산을 집중 ("선택과 집중") 2. **리스크 분산형**: 효율이 낮아도 골고루 배분하여 안정성 확보 ("안전 제일") 3. **잠재력 실험형**: 효율이 중간인 채널 비중을 높여 성장 가능성 테스트 ("기회 탐색") 결과는 반드시 아래 **JSON 형식**으로만 출력하세요. 설명 없이 JSON만 주세요. { "scenarios": [ { "name": "1. ROI 극대화형", "description": "설명...", "allocation": { "TV": 0, "Billboards": 0, ... } }, { "name": "2. 리스크 분산형", "description": "설명...", "allocation": { "TV": 0, "Billboards": 0, ... } }, { "name": "3. 잠재력 실험형", "description": "설명...", "allocation": { "TV": 0, "Billboards": 0, ... } } ] } `; try { ss.toast("Gemini가 3가지 시나리오를 구상 중입니다...", "🤔 전략 수립 중", 10); const resultText = callGemini(prompt); // JSON 추출 및 파싱 let cleanJson = resultText.replace(/```json/g, "").replace(/```/g, "").trim(); const firstBrace = cleanJson.indexOf("{"); const lastBrace = cleanJson.lastIndexOf("}"); if (firstBrace !== -1 && lastBrace !== -1) { cleanJson = cleanJson.substring(firstBrace, lastBrace + 1); } let result; try { result = JSON.parse(cleanJson); } catch (e) { throw new Error("AI 응답을 JSON으로 읽을 수 없습니다. 원본:\n" + resultText); } // 4. optimizer_ui 시트에 결과 뿌리기 let startRow = 21; uiSheet.getRange(startRow, 1).setValue("🤖 AI 추천 시나리오 (3 Options)"); uiSheet.getRange(startRow, 1).setFontWeight("bold").setFontSize(12); let currentRow = startRow + 2; // 헤더 생성 (예상 판매량 컬럼 추가!) const channelKeys = Object.keys(result.scenarios[0].allocation); const headers = ["시나리오명", "전략 설명", "📈 예상 판매량", "채널별 배분($) -->", ...channelKeys]; // 헤더 쓰기 uiSheet.getRange(currentRow, 1, 1, headers.length).setValues([headers]).setBackground("#EFEFEF").setFontWeight("bold"); currentRow++; // 데이터 쓰기 (판매량 직접 계산) result.scenarios.forEach(scenario => { // 1. 예상 판매량 계산: (각 채널 예산 * 계수)의 합 + 절편 let predictedSales = intercept; channelKeys.forEach(ch => { let budget = scenario.allocation[ch] || 0; let coeff = coeffs[ch] || 0; predictedSales += budget * coeff; }); // 2. 행 데이터 구성 let rowData = [ scenario.name, scenario.description, Math.round(predictedSales), // 계산된 판매량 (반올림) "" // 구분용 빈칸 ]; // 채널별 예산 추가 for (let i = 4; i < headers.length; i++) { const chName = headers[i]; rowData.push(scenario.allocation[chName] || 0); } uiSheet.getRange(currentRow, 1, 1, rowData.length).setValues([rowData]); currentRow++; }); // 숫자 서식 적용 (판매량과 예산 열에 콤마 포맷) // C열(판매량)과 E열 이후(예산) 전체 uiSheet.getRange(startRow + 3, 3, 3, 1).setNumberFormat("#,##0"); // 판매량 uiSheet.getRange(startRow + 3, 5, 3, channelKeys.length).setNumberFormat("$#,##0"); // 예산 ss.toast("예상 판매량이 포함된 시나리오가 완성되었습니다!", "✅ 성공", 5); } catch (e) { SpreadsheetApp.getUi().alert("시나리오 생성 중 오류:\n" + e.toString()); } } // 메뉴에 함수 추가 function onOpen() { SpreadsheetApp.getUi() .createMenu('🚀 AI 분석 시스템') .addItem('데이터 분석 실행 (Gemini 2.5 Flash)', 'runAIAnalysis') .addSeparator() // 구분선 .addItem('💡 3가지 전략 시나리오 제안받기', 'runScenarioAnalysis') // 추가됨 .addToUi(); } // End of Code.gs
주의: 프로젝트 설정 > 스크립트 속성 에 개인별 발급받은 Gemini API 키를 우선 입력하고 저장!!!

4단계: 시스템 사용 시나리오

1.
데이터 입력: raw_data 시트에 데이터를 확인
2.
분석 실행: 구글 시트 상단 메뉴 🚀 AI 분석 시스템 데이터 분석 실행을 클릭합니다.
메뉴를 클릭하면 "승인 필요 (Authorization Required)" 팝업이 뜹니다. -> [계속] 클릭.
model_summary 결과: Gemini가 데이터를 읽고 회귀분석을 한 뒤, model_summary 시트에 계수보고서를 작성
optimizer_ui 결과: 총 예산 , 채널별 예산 최소 / 최대 비중을 입력하면, 최적 예산안 시나리오 알려주는 템플릿 완성
3.
최적화 시뮬레이션:
optimizer_ui 시트로 이동합니다.
예산안 입력하면, 방금 AI가 분석한 따끈따끈한 계수를 바탕으로 최적 예산안과 더불어 예상 판매량 까지 알려줌
4.
추가 시나리오 분석
3가지 전략 시나리오 제안받기 : 추가적인 마케팅 예산안 받기
하단에 3개 시나리오 추가 생성
1.
ROI 극대화형
2.
리스크 분산형
3.
잠재력 실험형

참고용. 라이언의 최초 시작 Prompt

Apps Script를 통해서 회귀분석을 통한 판매량 예측 시스템을 아래와 같이 구현하려고 해. 가능여부 우선 알려주고, 어떻게 진행하면 좋을 지 단계별로 하나씩 안내해줘. --- raw_data : 분석용 데이터 업로드하는 시트 model_summary : Gemini API를 통해서 raw_data 회귀분석 및 그 회사 보고용 결과 정리 optimizer_ui : model_summary 시트의 계수를 참조하고, 총 가용 예산, 채널별 최소/최대 비중을 입력하면 가장 효율적인 예산 배분표와 예측 판매량이 **자동으로 생성(Spill)**
ChatGPT, Gemini 등을 활용하여 티키타카
시나리오 추가 기능을 위한 프롬프트 예시
AI가 회귀계수를 보고 추천 예산을 여러 개 생성할 수 있도록 해줘. - "공격형", "안정형", "균형형"처럼 시나리오별 예산안 제안
Contact : azureguy@empal.com / azureguy@cau.ac.kr