241212 데이터 분석 (단순 선형 회기분석)

241212 에이콘 아카데미 수업에 기반하여 작성되었음을 알립니다.


단순 선형 회기분석 실습

w : 슬로프, 기울기 /  b : 바이어스, 편향

tf2.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- TensorFlow.js 로드 -->

    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis/dist/tfjs-vis.umd.min.js"></script>

    <!-- 통계관련 기타 (상관계수 등) 라이브러리 로드 -->
    <script src="https://cdn.jsdelivr.net/npm/simple-statistics@7.7.0/dist/simple-statistics.min.js"></script>
    <title>Document</title>
    <!-- Main script -->
     <script type="module">
        import {func} from "./tf2script.mjs";
        window.func = func;
     </script>
</head>
<body>
    <h3>회귀분석 모델 기본 이해</h3>
    새로운 값 입력: <input type="number" value="1" id="inputValue" />
    <button onclick="func()">결과보기</button>
    <hr>
    <div id="showResult"></div>
    <div id="r2"></div> <!-- 모델 성능 확인용 결정계수 표시-->
    <div id="chart-container" style="margin-top:20px"></div>
</body>
</html>

tf2script.mjs

function calcCorrelation(){
    // 상관 관계 확인
    const x = [1,2,3,4,5,6,7,8,9,10];
    const y = [1,0,5,4,6,8,4,8,9,12];
    const correlation = ss.sampleCorrelation(x, y);
    console.log(`피어슨 상관계수 : ${correlation.toFixed(3)}`); // 0.892 매우 강한 양의 상관관계
    // 인과 관계? 있다라는 판단이 들면 회귀분석을 시작
}

let model; // 회기분석 모델
let xs, ys; // 학습 데이터

export function func(){
    calcCorrelation();
    if(!model){
        initModel().then(() => { // 모델 생성
            predictDisplay();
            calculateR2(); // 회기분석 모델 성능 확인
        });
    } else {
        predictDisplay();
    }
}

async function initModel(){
    // 모델 초기화
    model = tf.sequential();
    model.add(tf.layers.dense({units:1, inputShape:[1]})); // 입력값이 1차원으로 1개의 유닛에 들어온다.
    model.compile({loss:'meanSquaredError', optimizer:'sgd'});
    // loss는 코스트를 말한다. 코스트가 작을수록 0에 가까울수록 좋다.
    // optimizer에는 sgd, adam 등등 이 있다. 보통 adam이 좋다! 코스트를 minimize하는 방법

    // 학습 데이터 : 표본으로 학습
    xs = tf.tensor2d([1,2,3,4,5,6,7,8,9,10], [10, 1]); // 첫번째 변수 : 독립변수, x, 영향을 주는 변수
    ys = tf.tensor1d([1,0,5,4,6,8,4,8,9,12]); // 두번째 변수 : 종속변수, y, 영향을 받는 변수

    await model.fit(xs, ys); // 학습 실행 후 최적의 모델 생성 y = wx + b를 만들어줌, tensorFlow가 w, b를 만들어준다.

    // 학습된 모델의 가중치(w)와 편향(b) 확인
    const weights = model.getWeights();
    const w = weights[0].dataSync();
    const b = weights[1].dataSync();
    console.log(`학습된 파라미터 : w=${w}, b=${b}`); // w=0.7995800971984863, b=0.11707036942243576
    // y = 0.7995800971984863 * newx + 0.11707036942243576
    const newx = 13.5435
    console.log('미지의 새로운 x에 대한 예측 결과 : ', 0.7995800971984863 * newx + 0.11707036942243576);
    // 미지의 새로운 x에 대한 예측 결과 :  10.946183415830136
}

 

학습된 파라미터 w, b 확인

미지의 x (13.5435)를 가정하여 예측 결과 출력

미지의 값을 입력받고 예측값 출력

function predictDisplay() {
    const inputValue = parseFloat(document.getElementById('inputValue').value);

    if(isNaN(inputValue)){
        document.getElementById('showResult').innerText = '숫자 입력!';
        return;
    }

    // 모델을 사용해서 예측
    const pred = model.predict(tf.tensor2d([inputValue], [1,1])); // 학습할 때의 모양과 똑같게 넣어줘야한다.
    const predValue = pred.dataSync()[0]; // dataSync : 텐서를 js 배열로 변환

    // 예측 결과 출력
    document.getElementById('showResult').innerText = `입력값 : ${inputValue}, 예측값 : ${predValue.toFixed(2)}`;
}

결정계수 계산 (회기분석모델의 성능 파악)

// 결정계수 계산
function calculateR2() {
    // 예측값 계산
    const predictedYs = model.predict(xs).dataSync();

    // 실제 값과 예측값을 바탕으로 R² 계산
    // 결정계수는 회귀 모델이 종속변수를 얼마나 잘 설명하는지를 나타내는 통계적 척도!
    const actualYs = ys.dataSync();
    // reduce 함수는 배열의 각 요소를 순회하면서 누적 결과를 계산하는 데 사용된다. 
    // reduce 함수는 배열을 한 개의 값으로 줄이는 데 활용
    // actualYs(실제값) 배열의 평균값을 계산
    const meanY = actualYs.reduce((sum, val) => sum + val, 0) / actualYs.length;

    // 실제값과 평균값 간의 차이를 제곱하여 모두 더한 값으로, 데이터 전체 변동성을 나타낸다.
    const ssTotal = actualYs.reduce((sum, val) => sum + Math.pow(val - meanY, 2), 0);
    // 잔차 제곱합을 계산
    const ssResidual = actualYs.reduce((sum, val, index) => 
    sum + Math.pow(val - predictedYs[index], 2), 0);

    // 회귀 모델의 설명력 R²를 계산하는 식. 
    // 모델이 종속변수의 총 변동성을 얼마나 설명하는지를 나타내는 통계적 척도.
    const rSquared = 1 - ssResidual / ssTotal;

    // R² 결과 표시
    document.getElementById("r2").innerText = `rSquared:${rSquared.toFixed(3)}, 모델 설명력 (R²): ${(rSquared * 100).toFixed(1)}%`;
}

값이 매번 달라진다. 독립변수가 종속변수를 설명하는 비율값, 회기분석에서는 설명력이라고 부른다.

25%이상이면 잘나온 것, 높은 설명력은 보통 잘 안나온다.

https://cafe.daum.net/flowlife/RM66/26
https://datalabbit.tistory.com/54
https://aliencoder.tistory.com/40

차트

// 차트 관련  ---------------------
function getData() {
  // xs와 ys라는 두 개의 텐서에서 데이터를 가져와서 이를 가공한 후, 
  // 차트에 사용하기 위한 데이터 형식으로 변환하는 역할
  // dataSync()는 텐서의 모든 값을 동기적으로 가져와 자바스크립트 배열로 변환
  const dataX = xs.dataSync();
  const dataY = ys.dataSync();

  // dataX 배열을 기반으로 새로운 배열을 만든다. 
  // 이 배열은 map() 메서드를 통해 각 요소가 변환
  // map() 메서드는 dataX 배열의 각 요소에 대해 함수를 호출하고, 그 결과를 새 배열로 반환한다. 
  // 여기서 각 요소는 객체 { index: value, value: dataY[index] }로 변환된다.
  return Array.from(dataX).map((value, index) => {
    return { index: value, value: dataY[index] };
  });
}

function chart() {
  const data = getData(); // 학습 데이터를 가져옴

  // HTML에 고정된 차트를 렌더링할 컨테이너 선택
  const container = document.getElementById('chart-container');

  // 산점도 데이터 준비
  const scatterData = data.map(point => ({
      x: point.index, // x축 (독립변수)
      y: point.value  // y축 (종속변수)
  }));

  // 추세선 데이터 계산
  const trendlineData = calcTrendline(data);

  // 산점도와 추세선 함께 그리기
  tfvis.render.scatterplot(
      container, // 기존 visor가 아닌 고정된 div에 렌더링
      { 
          values: [scatterData, trendlineData], 
          series: ['Data', 'Trendline'] // 산점도와 추세선 이름 지정
      },
      {
          xLabel: '독립변수(X)',
          yLabel: '종속변수(Y)',
          height: 300,
          width: 500,
          seriesColors: ['blue', 'red'], // 데이터 점: 파란색, 추세선: 빨간색
          lineSeries: ['Trendline'], // 추세선을 직선으로 표시
          style: { lineWidth: 1 }
      }
  );
}

function calcTrendline(data) {
  // 데이터의 X와 Y 값을 분리
  const xValues = data.map(point => point.index);
  const yValues = data.map(point => point.value);

  // 평균 계산
  const meanX = xValues.reduce((sum, x) => sum + x, 0) / xValues.length;
  const meanY = yValues.reduce((sum, y) => sum + y, 0) / yValues.length;

  // 선형 회귀 계수 계산 (y = mx + b에서 m)
  const bunja = xValues.reduce((sum, x, i) => sum + (x - meanX) * (yValues[i] - meanY), 0);
  const bunmo = xValues.reduce((sum, x) => sum + Math.pow(x - meanX, 2), 0);
  
  const slope = bunja / bunmo; // 기울기 m
  const intercept = meanY - slope * meanX; // 절편 계산 (b)

  // 추세선 데이터를 X 범위에서 여러 점으로 생성
  // Math.min(...xValues) : 배열 xValues에서 가장 작은 값을 찾아 minX에 저장. 
  // Math.min은 숫자 중 최소값 반환하는 함수며, 
  // ... 연산자로 배열을 개별 요소로 펼쳐 Math.min에 전달한다.
  const minX = Math.min(...xValues);
  const maxX = Math.max(...xValues);
  const step = (maxX - minX) / 100;   // 100개의 점 생성

  const linePoints = [];
  for (let x = minX; x <= maxX; x += step) {
      linePoints.push({ x, y: slope * x + intercept });
  }

  return linePoints;
}


단순 선형 회기분석 실습 (보스턴 데이터)

통계 연습 데이터

BostonHousing.csv 파일 : https://raw.githubusercontent.com/selva86/datasets/master/BostonHousing.csv

https://cafe.daum.net/flowlife/RM66/23

tf3boston.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #result-container{
            display: none;
        }

        #scatter-plot {
            position: absolute;
            right: 10px;
            top: 10px;
            width: 40%;
            height: 400px;
            padding: 10px;
        }
    </style>
</head>
<body>
    <h2>보스턴 지역 평균 집값 : 단순선형회수(x:방 갯수, rm, y:medv)</h2>
    <button id="showButton">분석 결과 차트 보기</button> 예측결과가 나오는데 약간의 시간 필요
    <div id="result-container">
        <div id="input-container">
            방 갯수 입력 :
            <input type="number" id="roomsInput" min="1" />
            <button id="predictButton">예측 가격 확인</button>
        </div>
        <div id="single-predict-container">
            <h3>궁금한 방 갯수에 대한 집 예측 가격 : </h3>
            <p id="singlePrediction"></p>
        </div>
        <div id="predictions-container">
            <h3>실제값에 방 갯수에 따른 집 예측 가격 : </h3>
            <p id="predictions-list"></p>
        </div>
        <div id="chart-container">
            <div id="scatter-plot"></div>
        </div>
    </div>

    <script type="module">
        import {runAnalysis, predictPrice} from './tf3.mjs';
        // 전역 객체
        window.runAnalysis = runAnalysis;
        window.predictPrice = predictPrice;
    </script>
</body>
</html>

 

tf3.mjs

데이터 읽기

async function fetchHousingData(){
    console.log("데이터 로드 중");
    try{
        // csv 파일 읽기
        const response = await fetch('https://raw.githubusercontent.com/selva86/datasets/master/BostonHousing.csv');
        if(!response.ok){
            throw new Error(`데이터 읽기 실패 : ${response.statusText}`);
        }

        const data = await response.text();
        //console.log(data);
        const rows = data.split('\n').slice(1).filter(row => row.length > 0);
        //console.log(rows);

        const parseData = rows.map(row => {
            const cols = row.split(",");
            return {
                rm:parseFloat(cols[5]), // rm : 방갯수(독립변수 x)
                medv:parseFloat(cols[13]) // medv : 집값(종속변수 y)
            };
        });
        //console.log(parseData);
        return parseData;

    }catch(error){
        console.log(error);
        return [];
    }
}

데이터 읽기 확인

필요한 데이터만 추출

상관계수

// 상관계수 
function calcCorrelation(x, y){
    const correlation = ss.sampleCorrelation(x, y);
    console.log(`피어슨 상관계수 : ${correlation.toFixed(3)}`);
}

export async function runAnalysis(){
    console.log("분석 시작");
    
    const data = await fetchHousingData();
    if(data.length === 0){
        console.error('데이터가 없어!');
        return;
    }

    const dataX = data.map(d => d.rm); // rm 열 추출
    const dataY = data.map(d => d.medv); // medv 열 추출
    //console.log(dataX);
    calcCorrelation(dataX, dataY); // 상관관계 확인
}

상관관계 확인, 0.695 매우높다!

예측값

차트

function createScatterPlot(dataX, dataY, predictions){
    // 실제 데이터 (X, Y) 값을 객체 배열로 변환
    const actualValues = dataX.map(( x, i ) => ( { x: x, y: dataY[i] } ) );
    // 예측 데이터 (X, 예측 값) 값을 객체 배열로 변환
    const predictValues = dataX.map(( x, i ) => ( { x: x, y: predictions[i] } ) );
    
    // 'scatter-plot' div 선택
    const container = document.getElementById('scatter-plot');

    // TensorFlow.js의 시각화 라이브러리를 사용하여 산포도 그리기
    tfvis.render.scatterplot(container, {values: [actualValues, predictValues]}, 
        {
          xLabel: '숙소당 방 수 (RM)',
          yLabel: '주택의 중간 값 (MEDV)',
          height: 300,        // 그래프 높이 설정
          series: ['Actual', 'Predicted']   // 데이터 시리즈 레이블 설정 (실제값, 예측값)
    });
}

학습 결과가 좋지않으면 횟수를 늘려줌                                      epochs:1000으로 늘려줌, 그다지 변하지는 않았다.

실제값, 예측값 출력

function displayPredictions(actualYvalues, predictions){
    const predictList = document.getElementById('predictions-list');
    predictList.innerHTML = '';

    predictions.forEach((pred, index) => {
        const listitem = document.createElement('li');
        
        listitem.textContent = `실제값 : ${actualYvalues[index].toFixed(2)}, 예측값 : ${pred.toFixed(2)}`; 
        
        predictList.appendChild(listitem);
    })
}

궁금한 방 갯수에 대한 집 예측 가격

export async function predictPrice(){
    // 입력된 미지의 궁금한 방 갯수의 집값 예측
    const roomsInput = document.getElementById('roomsInput').value;

    if(roomsInput && model) {
        const inputTensor = tf.tensor2d([parseFloat(roomsInput)], [1, 1]);

        // 학습 모델을 이용해서 가격 예측 수행
        let prediction = model.predict(inputTensor).dataSync()[0];

        if(prediction < 0) {
            prediction = 0; // 예측된 가격이 음수일 경우 실제값의 최소값을 입력. 0을 준다.
        }

        // 예측 결과를 화면에 출력
        document.getElementById('singlePrediction').textContent = `예측 집 가격 : ${prediction.toFixed(2)}`;
    } else {
        document.getElementById('roomsInput').textContent = '방 갯수 입력해라!';
    }
}

 

느낀점

✨ 수업 느낀점
단순 선형 회기분석 모델을 통한 미지의 값에 대한 예측을 해보았다!
이러한 데이터 분석을 우리의 프로젝트에 적용할 수 있을까? 추가로 공부를 더 해보아야겠다!