Flask를 사용하여 Python 으로 작성한 구문을 통해 전처리한 데이터를 api로 전송하여 react에서 chart.js로 시각화하는 부분까지 구현해봤습니다.
○ 라이브러리 설치
사용하는 python 가상환경에
pip install Flask
pip install flask-cors
를 통해 라이브러리를 설치합니다
flask-cors는 CORS 관련 문제를 방지하고 클라이언트 측에서 서버로의 요청을 허용하도록 도와주는 역할을합니다.
○ Flask 파일 만들기
파일 구성은 nomore(전체파일) > app >__init__.py 와 routes.py
nomore > function_module.py 와 run.py
nomore >src > predict(예측) > Predict.jsx
nomore > src > data > Data.jsx 와 Data.module.css
로 구성합니다.
하나의 파일로 구성해도되지만 재사용및 편의성을 위해 분리하였습니다.
○ function_module.py
import pandas as pd
import plotly.express as px
from sqlalchemy import create_engine
import pymysql
import sqlalchemy
from sqlalchemy import types
import matplotlib.pyplot as plt
import seaborn as sns
plt.rc("font",family = "Malgun Gothic")
plt.rcParams["axes.unicode_minus"] = False
from flask import jsonify
from datetime import datetime,date
import joblib
# db 접속
def dbconnection():
# 접속정보
host = "35.87.206.219"
user = "nomore"
password = "nnoo1122"
db = "nomore"
charset = "utf8"
#조회시 컬럼명을 동시에 보여줄지 여부 설정
cursorclass = pymysql.cursors.DictCursor
autocommit = True
# DB접속하기
try:
conn = pymysql.connect(host=host,
user = user,
password = password,
db=db,
charset=charset,
autocommit=autocommit,
cursorclass = cursorclass)
print("DB접속 성공>>>",conn)
except:
print("DB Server Checking...")
cur = conn.cursor()
return cur,conn
# db에서 sql 구문을 통해 데이터 조회 및 데이터프레임에 저장
def sql_select(cur,sql):
rs_cnt= cur.execute(sql)
rows = cur.fetchall()
print(rs_cnt,"건이 조회되었습니다")
df = pd.DataFrame(rows)
return df
# db 커서와 접속정보 반납
def db_close(cur,conn):
try:
cur.close()
conn.close()
except:
print("이미 모든 커서와 접속정보가 반납되었습니다")
# 가격대 컬럼 생성
def price_level(df):
price_level = [
str(int(i) // 100000) + "0만원~" + str(int(i) // 100000 + 1) + "0만원"
if category == "전기밥솥"
else str(int(i) // 500000 *5) + "0만원~" + str(int(i) // 500000 *5 + 5) + "0만원"
for i, category in zip(df["prod_prc"], df["prod_ctgr_name"])]
price_level = [price_level.replace("00만원~50","0~50") for price_level in price_level]
price_level = [price_level.replace("00만원~10","0~10") for price_level in price_level]
df["price_level"] = price_level
# 연령대 컬럼 생성
def age_level(df):
age_list = []
# 날짜만 가져오기
current_datetime = datetime.now()
current_date = current_datetime.date()
for i in df["user_birth"]:
if isinstance(i, date): # 이미 datetime.date 타입인 경우
birth_date = i
elif isinstance(i, str): # 문자열인 경우에만 처리
birth_date = datetime.strptime(i, "%Y-%m-%d").date()
else:
# 그 외의 경우에는 처리하지 않거나 예외 처리를 수행
# 예를 들어, None 또는 다른 타입인 경우에 대한 처리 추가 가능
birth_date = None # 혹은 다른 기본값으로 설정
if birth_date: # birth_date가 None이 아닌 경우에만 계산
age = (current_date - birth_date).days // 365
age_list.append(age)
else:
age_list.append(None) # 예외 처리 등을 위해 None을 추가
age_level = [str(age // 10) + "0대" if age is not None else None for age in age_list]
df["age_level"] = age_level
# 그룹화 두개 df 생성
def df_group2(df,col1,col2):
df_group = df.groupby([col1,col2],as_index=False).count()
return df_group
# 그룹화 세개 df 생성
def df_group3(df,col1,col2,col3):
df_group = df.groupby([col1,col2,col3],as_index=False).count()
return df_group
# 그룹화 두개 plot 생성
def group2_plot(df,col1,col2,col3,choice1):
condition = df[col1] == choice1
values = df[condition][col3]
labels = df[condition][col2]
# 최대 5개와 나머지 합계를 기타로 만들기
values_sorted = values.sort_values(ascending=False)
top_5=values_sorted[:5].index
labels = [labels[i] for i in top_5]
labels.append("기타")
values_to_plot = [values[i] for i in top_5]
values_to_plot.append(values_sorted[5:].sum())
# DataFrame 생성
# chart_data_df = pd.DataFrame({'labels': labels, 'values': values_to_plot})
chartData = {'labels' : labels,
'datasets':[{
'data' : values_to_plot,
'backgroundColor' :['red', 'blue', 'green', 'yellow', 'purple', 'orange'],
}],}
return chartData
# 그룹화 세개 plot 생성
def group3_plot(df,col1,col2,col3,col4,choice1,choice2):
condition = (df[col2] == choice2) & (df[col1] == choice1)
# 값(개수) 가져오기 - 기준:prod_id/prod_name
values = df[condition][col4]
# 실제 그래프에서 범주로 쓰일 것
labels = df[condition][col3]
# 최대 5개와 나머지 합계를 기타로 만들기
values_sorted = values.sort_values(ascending=False)
top_5=values_sorted[:5].index
labels = [labels[i] for i in top_5]
labels.append("기타")
values_to_plot = [values[i] for i in top_5]
values_to_plot.append(values_sorted[5:].sum())
chartData = {'labels' : labels,
'datasets':[{
'data' : values_to_plot,
'backgroundColor' :['red', 'blue', 'green', 'yellow', 'purple', 'orange'],
}],}
return chartData
# JSON 변환시 int64, int32를 int로 변환
def convert_int_columns(df):
for column in df.columns:
if df[column].dtype == 'int64' or df[column].dtype == 'int32':
df[column] = df[column].astype(int)
return df
# DataFrame을 JSON으로 변환하는 함수
def dataframe_to_json(df):
return df.to_json(orient='records')
▶ python에서 데이터를 추출하기 위한 db 연결, sql문을 통해 조회한 데이터를 데이터 프레임으로 생성,
가격대, 연령대 컬럼 생성 및 데이터프레임 그룹화와 시각화 함수를 작성해놓은 파일입니다.
이전 글에서와 달라진 점은 plotly나 pyplot을 사용하여 시각화한 내용을 react로 api로 보내는 것이 불가능하고
차트 관련된 데이터를 json형식으로 보내 react에서 chart.js를 사용하여 시각화를 해야하기 때문에 시각화 함수에서 return 값으로 json 형식의 데이터를 두었습니다.
○ __init__.py
Python에서 패키지로 인식되도록 하는 초기화 파일입니다.
Flask 애플리케이션을 패키지로 만들기 위해 필요한 파일이며 여기에 Flask 애플리케이션 인스턴스를 생성하고 필요한 설정을 초기화하는 코드를 작성합니다.
from flask import Flask
# 브라우저는 기본적으로 같은 출처 이외의 도메인으로의 HTTP 요청을 차단하며,
# 이를 해결하기 위해 서버에서 허용 정책을 설정해야함
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
from app import routes
○ run.py
Flask 애플리케이션을 실행하는 역할을 합니다.
Flask 개발 서버를 시작하기 위한 코드를 포함하고 애플리케이션을 실행하는 데 사용됩니다.
여기서 app 은 프로젝트의 패키지 이름으로 이 파일을 실행하면 로컬 서버에서 Flask 애플리 케이션이 실행됩니다.
저의 경우에는 aws를 통한 웹 배포까지 하기 위해서 host='0.0.0.0' 을 작성하였지만 로컬에서 돌리는 거라면 필요하지 않습니다.
from app import app
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000,debug=True)
○ routes.py
Flask 애플리케이션의 라우트를 정의하는 역할을 합니다.
라우트는 특정 url 경로로 들어오는 요청에 대한 응답을 처리하는데 사용됩니다.
from flask import render_template
from app import app
from function_module import dbconnection,sql_select,db_close,price_level,age_level,df_group2,df_group3,group2_plot,group3_plot,convert_int_columns,dataframe_to_json,predict_gb,predict_company,predict_price,user_data
from flask import request
from flask import jsonify
from flask_cors import CORS
import json
# flask에서 제공하는 전역객체(global object) : 한 요청 내에서 여러 함수 간에 데이터를 공유하는데 사용됨
from flask import g
@app.route('/api-flask/chart',methods=['GET','POST'])
def test():
if request.method == 'POST':
# 여기에 POST 요청을 처리하는 코드 추가
# request.json을 통해 클라이언트에서 전송한 데이터에 접근할 수 있음
data = request.json
print(data)
cur,conn = dbconnection()
sql = "SELECT prod_id, buy_id,prod_name,prod_company,prod_ctcd,prod_ctgr_name, prod_prc,user_id, user_gender,user_birth,user_address,user_family_counts FROM buy_dtls JOIN prod JOIN buy JOIN user JOIN prod_ctgr WHERE buy_pd_nm = prod_id AND buy_id = buy_dtls_nm AND buy_mbr_id = user_id AND prod_ctcd = prod_ctgr_code"
df = sql_select(cur,sql)
db_close(cur,conn)
price_level(df)
age_level(df)
df_g1 = df_group2(df,data["Data1"]["col1"],data["Data1"]["col2"])
df_cic1 = convert_int_columns(df_g1)
chartData1 = group2_plot(df_cic1,data["Data1"]["col1"],data["Data1"]["col2"],data["Data1"]["col3"], data["Data1"]["choice"])
# 딕셔너리로 변환 후 JSON 변환
json_chart_data1 = json.dumps(chartData1, ensure_ascii=False, default=str)
df_g2 = df_group3(df,data["Data2"]["col1"],data["Data2"]["col2"],data["Data2"]["col3"])
df_cic2 = convert_int_columns(df_g2)
chartData2 = group3_plot(df_cic2,data["Data2"]["col1"],data["Data2"]["col2"],data["Data2"]["col3"], data["Data2"]["col4"],data["Data2"]["choice1"],data["Data2"]["choice2"])
# 딕셔너리로 변환 후 JSON 변환
json_chart_data2 = json.dumps(chartData2, ensure_ascii=False, default=str)
return jsonify({'chartData1': json_chart_data1 ,'chartData2': json_chart_data2 })
else:
#GET 요청에 대한 코드
# GET 요청 처리
prod_ctgr_name = request.args.get('prod_ctgr_name')
return jsonify({'message': 'GET request successful', 'prod_ctgr_name': prod_ctgr_name})
▶ 여기서 함수들을 호출하고 생성된 json 형식의 데이터를 return했습니다.
○ React 에서 데이터를 전송받아 chart.js로 표현
Data.jsx 파일
import React, { useState, useEffect,useRef } from 'react';
import axios from 'axios';
import Chart from 'chart.js/auto';
import "./Data.module.css";
const Data = () => {
const [chartData1, setChartData1] = useState(null);
const chartRef1 = useRef(null);
const [chartData2, setChartData2] = useState(null);
const chartRef2 = useRef(null);
const [genderOption, setGenderOption] = useState('M'); // 초기값으로 '남자' 설정
const [ageOption, setAgeOption] = useState('20대');
const [selectedGender, setSelectedGender] = useState('M');
const [selectedElement, setSelectedElement] = useState('user_gender'); // 초기값으로 '구매자 성별' 설정
const [selectedCategory, setSelectedCategory] = useState('냉장고'); // 초기값으로 '구매자 성별' 설정
const [selectedCategory2, setSelectedCategory2] = useState('냉장고');
const [selectedElement1, setSelectedElement1] = useState('user_gender');
const [selectedElement2, setSelectedElement2] = useState('prod_company');
// 드롭다운 박스 선택 옵션
const handleCondition1Change = (e) => {
setSelectedElement1(e.target.value);
};
const handleCondition2Change = (e) => {
setSelectedElement2(e.target.value);
};
const fetchData = async () => {
try {
// POST 요청
const response = await axios.post('http://localhost:5000/api-flask/chart', {
Data1 : {
"col1" : "prod_ctgr_name",
"col2" : selectedElement,
"col3" : "prod_id",
"choice" : selectedCategory
},
Data2 : {
"col1" : "prod_ctgr_name",
"col2" : selectedElement1,
"col3" : selectedElement2,
"col4" : "prod_id",
"choice1" : selectedCategory2,
"choice2" : selectedGender
}// 다른 데이터 추가
});
// JSON.parse를 사용하여 문자열로 받아온 데이터를 객체로 변환
const chartDataFromResponse1 = JSON.parse(response.data.chartData1);
const chartDataFromResponse2 = JSON.parse(response.data.chartData2);
// 이전에 생성된 차트 파괴
if (chartRef1.current) {
chartRef1.current.destroy();
}
if (chartRef2.current) {
chartRef2.current.destroy();
}
// 차트데이터 설정
setChartData1(chartDataFromResponse1);
setChartData2(chartDataFromResponse2);
} catch (error) {
console.error('Error fetching data:', error);
}
};
useEffect(() => {
// 초기화 시 서버에서 차트 데이터를 불러오도록 설정
fetchData();
}, [selectedGender,selectedElement,selectedCategory,selectedCategory2,selectedElement1,selectedElement2]);
const customLegend = (chart) => {
let ul = document.createElement('ul');
let color = chart.data.datasets[0].backgroundColor;
chart.data.labels.forEach((label, index) => {
ul.innerHTML += `<li><span style="background-color: ${color[index]}; display: inline-block; width: 30px; height: 10px;"></span> ${label}</li>`;
});
return ul.outerHTML;
};
const commonOptions = {
responsive : false,
maintainAspectRatio: true,
cutout: '60%', // hole 기능
legend: {
display: false,
position: 'right',
labels: {
usePointStyle: true,
},
},
plugins: {
beforeDraw: (chart) => {
const width = chart.width;
const height = chart.height;
const legendHeight = chart.legend.height;
// 범례 위치를 지정 (가운데 오른쪽)
const positionX = width - 50;
const positionY = (height - legendHeight) / 2;
chart.ctx.save();
chart.ctx.translate(positionX, positionY);
// 사용자 정의 범례 호출
const customLegendHTML = customLegend(chart);
chart.ctx.innerHTML = customLegendHTML;
chart.ctx.restore();
},
},
};
useEffect(() => {
// chartData가 변경될 때마다 차트를 그리도록 설정
if (chartData1) {
const ctx1 = document.getElementById('myChart1')?.getContext('2d');
if (ctx1) {
chartRef1.current = new Chart(ctx1, {
type: 'pie',
data: chartData1,
options : commonOptions,
});
}
}
if (chartData2) {
const ctx2 = document.getElementById('myChart2')?.getContext('2d');
if (ctx2) {
chartRef2.current = new Chart(ctx2, {
type: 'pie',
data: chartData2,
options : commonOptions,
});
}
}
}, [chartData1,chartData2]);
const genderOptions = selectedElement1 === 'user_gender' ? ['M','F'] : [];
const ageOptions = selectedElement1 === 'age_level' ? ["20대", "30대", "40대","50대","60대"] : [];
return (
<div class="chart-box" style = {{display:'flex', justifyContent : 'center',fontSize:'20px'}}>
<div class="first-chart"style = {{paddingRight:'10px'}}>
{/* 드롭다운을 통해 선택 */}
<label> 제품 선택
<select value={selectedCategory} onChange={(e) => setSelectedCategory(e.target.value)} style={{fontSize:'20px'}}>
<option value='냉장고'>냉장고</option>
<option value='TV'>TV</option>
<option value='세탁기'>세탁기</option>
<option value='에어컨'>에어컨</option>
<option value='전기밥솥'>전기밥솥</option>
</select>
</label>
<label> 조건 선택:
<select value={selectedElement} onChange={(e) => setSelectedElement(e.target.value)} style={{fontSize:'20px'}}>
<option value='prod_company'>제조회사</option>
<option value='age_level'>구매자 연령대</option>
<option value='user_address'>구매자 거주지역</option>
<option value='user_gender'>구매자 성별</option>
</select>
</label>
</div>
<div style={{display:'flex', justifyContent : 'center'}}>
<canvas id="myChart1" className='my-chart'></canvas>
</div>
<div class = "second-chart">
<label> 제품 선택
<select value={selectedCategory2} onChange={(e) => setSelectedCategory2(e.target.value)}>
<option value='냉장고'>냉장고</option>
<option value='TV'>TV</option>
<option value='세탁기'>세탁기</option>
<option value='에어컨'>에어컨</option>
<option value='전기밥솥'>전기밥솥</option>
</select>
</label>
<label> 조건 선택1:
<select value={selectedElement1} onChange={handleCondition1Change}>
<option value='age_level'>구매자 연령대</option>
<option value='user_gender'>구매자 성별</option>
</select>
</label>
{selectedElement1 === 'user_gender' && (
<div>
<label> 성별선택:
<select value={selectedGender} onChange={(e) => setSelectedGender(e.target.value)}>
{genderOptions.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</label>
</div>
)}
{selectedElement1 === 'age_level' && (
<label> 연령대 선택:
<select value={selectedGender} onChange={(e) => setSelectedGender(e.target.value)}>
{ageOptions.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</label>
)}
<label> 조건 선택2:
<select value={selectedElement2} onChange={handleCondition2Change}>
<option value='prod_company'>제조회사</option>
<option value='age_level'>구매자 연령대</option>
<option value='user_address'>구매자 거주지역</option>
<option value='user_gender'>구매자 성별</option>
<option value='price_level'>제품 가격대</option>
</select>
</label>
</div>
<div style={{display:'flex', justifyContent : 'center'}}>
<canvas id="myChart2" className='my-chart' ></canvas>
</div>
</div>
);
};
export default Data;
▶ post 방식으로
1. 드롭다운 박스로 구성되어있는 항목을 사용자가 선택하면 그 데이터를 포함한 Data1, Data2를 Flask에 전송
2. python으로 데이터 조회 및 전처리
3. API 응답을 통해 데이터 react 로 전송
4. chart.js를 통해 데이터 시각화
위와 같은 흐름으로 웹이 구현되는 것입니다.
○ 실행 방법 및 결과
가상환경의 프로젝트 위치에서 아래와 같은 코드로 Flask를 실행합니다.
(gj_env_02) C:\Users\user\gj202311\11_sprint\react-test>python run.py
React 는 npm run start를 통해 실행시킵니다.
React 에 chart.js와 axios 가 설치되어있어야합니다.
▶ 구현된 웹 페이지 입니다. ( localhost:3000)
왼쪽은 냉장고를 구매한 회원의 성별 비율 , 오른쪽은 냉장고를 구매한 남성 회원 중 구매품목의 제조회사 비율입니다.
드롭다운 박스를 통해 선택하면 그에 따라 차트가 생성됩니다.
▶ 위 사진과 같이 전기밥솥을 구매한 구매자들의 거주지역 비율, 에어컨을 구매한 30대의 제품 가격대 비율입니다.
드롭다운 박스는 조건 선택 1에서 구매자 성별을 선택하면 성별 선택이, 구매자 연령대를 선택하면 연령대 선택이
나오도록 하였습니다.
이를 웹 페이지와 합친 모습입니다.
부족한 시간과 익숙하지 않은 frontend react를 다루는 작업으로 인해 아직은 부족한 모습이지만 기능은 잘 구현되는 것을 확인할 수 있습니다.
이를통해 사용자에게도 본인이 속한 거주지, 연령, 성별 그룹에서 어떤 제품을 선호하는지 어떤 가격대를 선호하는지를 확인 할 수있고 관리자의 경우에는 이 데이터를 통해 어떤 제품을 상위에 배치할지 어떤 제품의 수량을 높여야할지를 확인할 수있습니다.
시연 영상입니다. 구매자 성별을 선택하면 조건 선택 2에서는 선택해도 차트가 변하지 않는 것을 확인 할 수 있습니다.
또한 다양한 항목별로 차트를 확인 할 수있습니다.
'Project' 카테고리의 다른 글
[전자제품쇼핑몰] Flask 사용하여 React에 제품 추천 서비스 구현하기 (2) | 2024.02.04 |
---|---|
[전자제품 쇼핑몰] 머신러닝&딥러닝 - 제품 예측(추천) 모델 (1) | 2024.02.04 |
[전자제품쇼핑몰] 로그데이터 분석 및 시각화 (1) | 2024.01.28 |
[전자제품 쇼핑몰] 데이터베이스에 데이터 저장하기 & 데이터 생성하기 (1) | 2024.01.28 |
[전자제품쇼핑몰] 데이터 전처리 - 웹크롤링 데이터 전처리 (3) | 2024.01.24 |