data.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. from fastapi import APIRouter, Form, Depends, HTTPException, File, UploadFile,Request
  2. from fastapi.responses import FileResponse, StreamingResponse
  3. from fastapi_login.exceptions import InvalidCredentialsException
  4. from fastapi_login import LoginManager
  5. from fastapi.responses import HTMLResponse
  6. from typing import List,Optional
  7. import pymysql
  8. import numpy as np
  9. import math
  10. import re
  11. import pandas as pd
  12. import ast
  13. import csv
  14. import matplotlib.pyplot as plt
  15. from matplotlib.ticker import MaxNLocator
  16. import matplotlib
  17. import io
  18. from datetime import datetime,timedelta
  19. from oauth2client.service_account import ServiceAccountCredentials
  20. from googleapiclient.discovery import build
  21. matplotlib.rc('font', family='STSong')
  22. plt.rcParams['font.sans-serif']=['SimHei']
  23. plt.rcParams['axes.unicode_minus']=False
  24. pd.set_option('display.max_columns', None)
  25. data = APIRouter()
  26. #USER_INFO_FILE = '/var/www/ntcri_api/app/api/user_information_change.csv'
  27. #ATTEND_RECORD_FILE = '/var/www/ntcri_api/app/api/attend_record_syn.csv'
  28. #CLASS_DETAIL_FILE = '/var/www/ntcri_api/app/api/class_detail.csv'
  29. #CLASS_LIST_FILE = '/var/www/ntcri_api/app/api/class_list.csv'
  30. #CLASS_NAME_FILE = '/var/www/ntcri_api/app/api/class_name1.csv'
  31. #SCHOOL_FILE = '/var/www/ntcri_api/app/api/school.csv'
  32. def call_sql(sql_query):
  33. connection = pymysql.connect(
  34. host='db.ptt.cx',
  35. user='choozmo',
  36. password='pAssw0rd',
  37. database='test'
  38. )
  39. cursor = connection.cursor()
  40. cursor.execute(sql_query)
  41. results = cursor.fetchall()
  42. df = pd.DataFrame(results, columns=[i[0] for i in cursor.description])
  43. cursor.close()
  44. connection.close()
  45. return df
  46. def match():
  47. # based on 'registration'
  48. sql_query = """
  49. SELECT
  50. `user_information`.`id` AS `user_information_id`, `birthday`, `gender`, `position`,
  51. `registration`.`id` AS `registration_id`, `registration`.`user_id`, `registration`.`create_time`,
  52. `class_list`.`id` AS `class_list_id`,`class_list`.`start_time`, `class_list`.`end_time`, `fee_method`,
  53. `class_name`.`id` AS `class_name_id`, `class_name`.`name`, `school_id`, `category`,`group_id`,`group_sort`, `is_inner`
  54. FROM `registration`
  55. JOIN `user_information` ON `registration`.`user_id` = `user_information`.`user_id`
  56. JOIN `class_list` ON `registration`.`event_id` = `class_list`.`id`
  57. JOIN `class_name` ON `class_list`.`name_id` = `class_name`.`id`
  58. """
  59. # based on 'attend_record'
  60. '''
  61. sql_query = """
  62. SELECT
  63. `user_information`.`id` AS `user_information_id`,`birthday`, `gender`, `position`,
  64. `attend_record`.`id` AS `attend_record_id`, `class_detail_id`, `attend_record`.`user_id`, `is_attend`,
  65. `class_detail`.`start_time` ,`class_detail`.`end_time`, `sessions`, `hour`,
  66. `class_list`.`id` AS `class_list_id`, `event`, `fee_method`,
  67. `class_name`.`id` AS `class_name_id`, `class_name`.`name`, `school_id`, `category`,`group_id`,`group_sort`, `is_inner`
  68. FROM `attend_record`
  69. JOIN `user_information` ON `attend_record`.`user_id` = `user_information`.`user_id`
  70. JOIN `class_detail` ON `attend_record`.`class_detail_id` = `class_detail`.`id`
  71. JOIN `class_list` ON `class_detail`.`class_list_id` = `class_list`.`id`
  72. JOIN `class_name` ON `class_list`.`name_id` = `class_name`.`id`
  73. """
  74. '''
  75. match_data = call_sql(sql_query)
  76. return match_data
  77. def student_info():
  78. def fix_dict_format(s):
  79. if isinstance(s, str):
  80. return s.encode().decode('unicode_escape')
  81. pairs = re.findall(r"'([^']+)':(\d+)", s)
  82. fixed_dict = "{" + ", ".join([f"'{key}': {value}" for key, value in pairs]) + "}"
  83. return fixed_dict
  84. else:
  85. return s
  86. # 讀檔,把 position 轉成 dictionary 格式
  87. #user_information = pd.read_csv(file)
  88. sql_query = """
  89. SELECT * FROM `user_information`
  90. """
  91. user_information = call_sql(sql_query)
  92. user_information['position'] = user_information['position'].apply(fix_dict_format)
  93. user_information['position'].fillna('{}', inplace=True)
  94. user_information['position'] = user_information['position'].apply(lambda x: ast.literal_eval(x) if pd.notnull(x) else {})
  95. # only學員
  96. student_information = user_information[user_information['position'].apply(lambda x: x.get('學員', 0)==1)]
  97. #user_information[user_information['gender'].str.strip() != '']['gender']
  98. return student_information
  99. def count_target_number(df, target, order=None):
  100. item_counts = df[target].value_counts(dropna=True)
  101. result_df = item_counts.reset_index()
  102. result_df.columns = [target, 'Count']
  103. nan_count = df[target].isna().sum()
  104. if nan_count > 0:
  105. nan_df = pd.DataFrame({target: [np.nan], 'Count': [nan_count]})
  106. result_df = pd.concat([result_df, nan_df], ignore_index=True)
  107. if order:
  108. result_df[target] = pd.Categorical(result_df[target], categories=order, ordered=True)
  109. result_df = result_df.sort_values(by=target)
  110. item_list = result_df[target].replace('', '無類別名稱').astype(str).tolist()
  111. count_list = result_df['Count'].astype(str).tolist()
  112. search_results = {target: str(item_list), "Count": str(count_list)}
  113. return search_results
  114. def search_specific_items(df, target, search_items):
  115. if not search_items:
  116. return count_target_number(df, target)
  117. all_items = df[target].unique
  118. not_in_list = [name for name in search_items if name not in all_items]
  119. if not_in_list:
  120. return {target: str(not_in_list), "Error": "Not in list"}
  121. item_counts = df[df[target].isin(search_items)][target].value_counts()
  122. item_list = item_counts.index.astype(str).tolist()
  123. count_list = item_counts.values.tolist()
  124. search_results = {"Item": item_list, "Count": count_list}
  125. return search_results
  126. @data.post("/age_bar")
  127. async def age_bar(age_group_list: Optional[List[int]] = [18, 25, 35, 45, 55, 65]):
  128. def create_labels(age_group_list):
  129. labels = [f"{start}-{end-1}" if end != float('inf') else f"{start}+" for start, end in zip(age_group_list[1:-2], age_group_list[2:-1])]
  130. labels = [f'{str(age_group_list[1]-1)}-'] + labels + [f'{str(age_group_list[-2])}+']
  131. return labels
  132. user_information = student_info()
  133. # 生日
  134. birthday = pd.to_datetime(user_information['birthday'], format='%Y-%m-%d')
  135. current_date = pd.to_datetime('today')
  136. user_information['age'] = ((current_date - birthday).dt.days / 365).round(1).astype(int)
  137. bins = [0] + (age_group_list) + [float('inf')]
  138. labels = create_labels(bins)
  139. user_information['age_group'] = pd.cut(user_information['age'], bins=bins, labels=labels, right=False)
  140. age_group_counts = user_information['age_group'].value_counts(sort=False)
  141. age_group_label = labels
  142. count_list = age_group_counts.values.tolist()
  143. search_results = {"Age_group" : str(age_group_label), "Count": str(count_list)}
  144. # 按年齡分群
  145. # bins = [0, 18, 25, 35, 45, 55, 65, float('inf')]
  146. # labels = ['18-', '18-24', '25-34', '35-44', '45-54', '55-64', '65+']
  147. #user_information['age_group'] = pd.cut(user_information['age'], bins=bins, labels=labels, right=False)
  148. return search_results
  149. @data.post("/gender_bar")
  150. def gender_bar(gender_type: Optional[str] = ''):
  151. user_information = student_info()
  152. if gender_type.strip() in ["男", "女"]:
  153. user_information = user_information[user_information['gender'] == gender_type]
  154. gender_counts = (user_information[user_information['gender'].str.strip() != '']['gender']).value_counts()
  155. gender_list = gender_counts.index.astype(str).tolist()
  156. count_list = gender_counts.values.tolist()
  157. search_results = {"Gender" : str(gender_list), "Count": str(count_list)}
  158. return search_results
  159. @data.post("/reg_gender")
  160. async def reg_gender():
  161. target = 'gender'
  162. match_data = match()
  163. return count_target_number(match_data, 'gender')
  164. @data.post("/reg_age")
  165. async def reg_age(age_group_list: Optional[List[int]] = [18, 25, 35, 45, 55, 65]):
  166. def create_labels(age_group_list):
  167. labels = [f"{start}-{end-1}" if end != float('inf') else f"{start}+" for start, end in zip(age_group_list[1:-2], age_group_list[2:-1])]
  168. labels = [f'{str(age_group_list[1]-1)}-'] + labels + [f'{str(age_group_list[-2])}+']
  169. return labels
  170. match_data = match()
  171. # 生日
  172. birthday = pd.to_datetime(match_data['birthday'], format='%Y-%m-%d')
  173. current_date = pd.to_datetime('today')
  174. match_data['age'] = ((current_date - birthday).dt.days / 365).round(1).astype(int)
  175. bins = [0] + (age_group_list) + [float('inf')]
  176. labels = create_labels(bins)
  177. match_data['age_group'] = pd.cut(match_data['age'], bins=bins, labels=labels, right=False)
  178. age_group_counts = match_data['age_group'].value_counts(sort=False)
  179. age_group_label = labels
  180. count_list = age_group_counts.values.tolist()
  181. search_results = {"Age_group" : str(age_group_label), "Count": str(count_list)}
  182. # 按年齡分群
  183. # bins = [0, 18, 25, 35, 45, 55, 65, float('inf')]
  184. # labels = ['18-', '18-24', '25-34', '35-44', '45-54', '55-64', '65+']
  185. #user_information['age_group'] = pd.cut(user_information['age'], bins=bins, labels=labels, right=False)
  186. return search_results
  187. def time_analysis_search(df, target, search_items):
  188. def time_analysis(df, target, name):
  189. df = df[df[target] == name]
  190. df['start_time'] = pd.to_datetime(df['start_time'])
  191. df['month'] = df['start_time'].dt.to_period('M')
  192. monthly_counts = df['month'].value_counts().sort_index()
  193. dates_list = monthly_counts.index.astype(str).tolist()
  194. count_list = monthly_counts.values.tolist()
  195. return (dates_list, count_list)
  196. all_items = df[target].unique()
  197. search_results = []
  198. for name in search_items or all_items:
  199. if name not in all_items:
  200. search_results.append({target: name, "Error": "Not in list"})
  201. else:
  202. dates_list, count_list = time_analysis(df, target, name)
  203. search_results.append({target: name, "Month": str(dates_list), "Count": str(count_list)})
  204. return search_results
  205. @data.post("/category_line")
  206. async def category_line(category_name: Optional[str] = ''):
  207. target = 'category'
  208. search_items = category_name.split()
  209. match_data = match()
  210. search_results = time_analysis_search(match_data, target, search_items)
  211. return search_results
  212. #@data.post("/group_line")
  213. async def group_line(group_id: Optional[str] = ''):
  214. #group_name_id = {'未來工藝學群': 1, '技藝工藝學群': 2, '生活工藝學群': 3, '青年工藝學群': 4, '世代工藝學群': 5,
  215. # '修護工藝學群': 6, '跨域工藝學群': 7, '線上工藝學群': 8, '希望工程學群': 9}
  216. target = 'group_id'
  217. search_items = group_id.split()
  218. try:
  219. search_items = [int(item) for item in search_items]
  220. except ValueError as e:
  221. return {"msg": str(e)}
  222. match_data = match()
  223. search_results = time_analysis_search(match_data, target, search_items)
  224. return search_results
  225. @data.post("/category_bar")
  226. async def category_bar(category_name: Optional[str] = ''):
  227. target = 'category'
  228. search_items = category_name.split()
  229. match_data = match()
  230. search_results = search_specific_items(match_data, target, search_items)
  231. return search_results
  232. @data.post("/group_bar")
  233. async def group_bar(group_id: Optional[str] = ''):
  234. #group_name_id = {'未來工藝學群': 1, '技藝工藝學群': 2, '生活工藝學群': 3, '青年工藝學群': 4, '世代工藝學群': 5,
  235. # '修護工藝學群': 6, '跨域工藝學群': 7, '線上工藝學群': 8, '希望工程學群': 9}
  236. target = 'group_id'
  237. search_items = group_id.split()
  238. try:
  239. search_items = [int(item) for item in search_items]
  240. except ValueError as e:
  241. return {"msg": str(e)}
  242. match_data = match()
  243. search_results = search_specific_items(match_data, target, search_items)
  244. return search_results
  245. @data.post("/first_course_bar")
  246. async def first_course_bar(by_class_name: Optional[int] = 0, by_category: Optional[int] = 0):
  247. if by_class_name == by_category == 1:
  248. return 'Please select only one.'
  249. elif by_class_name == by_category == 0:
  250. return 'Please select one.'
  251. elif by_class_name not in [0, 1] or by_category not in [0, 1]:
  252. return 'Please input 0 or 1.'
  253. match_data = match()
  254. match_data['create_time'] = pd.to_datetime(match_data['create_time'])
  255. earliest_records = match_data.groupby('user_id').agg({'create_time': 'min', 'name': 'first', 'category': 'first'}).reset_index()
  256. target = 'name' if by_class_name else 'category'
  257. first_class_counts = earliest_records[target].value_counts()
  258. course_list = first_class_counts.index.tolist()
  259. count_list = first_class_counts.values.tolist()
  260. return {target: str(course_list), "Count": str(count_list)}
  261. def repeat_time_search(df, target):
  262. def repeat_time_analysis(df, target, name):
  263. df = df[df[target] == name]
  264. counts = df['user_id'].value_counts()
  265. repeat_times = counts.value_counts().sort_index()
  266. repeat_time_list = repeat_times.index.tolist()
  267. count_list = np.array(repeat_times.values).flatten().tolist()
  268. return (repeat_time_list, count_list)
  269. all_items = df[target].unique()
  270. search_results = []
  271. for name in all_items:
  272. repeat_time_list, count_list = repeat_time_analysis(df, target, name)
  273. search_results.append({target: name, 'repeat_times': str(repeat_time_list), 'count': str(count_list)})
  274. return search_results
  275. @data.post("/repeat_participation_bar")
  276. async def repeat_participation_bar(by_class_name: Optional[int] = 0, by_category: Optional[int] = 0):
  277. if by_class_name == by_category == 1:
  278. return 'Please select only one.'
  279. elif by_class_name == by_category == 0:
  280. return 'Please select one.'
  281. elif by_class_name not in [0, 1] or by_category not in [0, 1]:
  282. return 'Please input 0 or 1.'
  283. target = 'name' if by_class_name else 'category'
  284. #search_items = [name.strip("'\"") for name in class_name.split()]
  285. match_data = match()
  286. search_results = repeat_time_search(match_data, target)
  287. return search_results
  288. @data.post("/school_city_pie")
  289. async def school_city_pie():
  290. #school = pd.read_csv(SCHOOL_FILE)
  291. sql_query = """
  292. SELECT * FROM `schools`
  293. """
  294. school = call_sql(sql_query)
  295. def extract_county_city(address):
  296. pattern = r"(.*?[縣市])"
  297. if isinstance(address, str):
  298. address = address.replace("臺", "台")
  299. match = re.search(pattern, address)
  300. if match:
  301. return match.group(1)[-3:]
  302. return None
  303. school['county_city'] = school['address'].apply(extract_county_city)
  304. city_counts = (school[school['county_city'].str.strip() != '']['county_city']).value_counts()
  305. total_count = city_counts.sum()
  306. #city_percentages = city_counts / total_count * 100
  307. threshold_percent = 0.01
  308. low_county = city_counts[city_counts / total_count < threshold_percent]
  309. city_counts['其他'] = low_county.sum()
  310. city_counts = city_counts[city_counts / total_count >= threshold_percent]
  311. result = {'City':str(city_counts.index.tolist()), 'Number of school':str(city_counts.values.tolist())}
  312. return result
  313. @data.post("/class_name_category_pie")
  314. async def class_name_category_pie():
  315. sql_query = """
  316. SELECT * FROM `class_name`
  317. """
  318. class_name = call_sql(sql_query)
  319. results = count_target_number(class_name, 'category')
  320. return results
  321. #@data.post("/class_detail_category_pie")
  322. async def class_detail_category_pie():
  323. sql_query = """
  324. SELECT `class_detail`.`id` AS `class_detail_id`, `class_detail`.`class_list_id`, `class_detail`.`start_time`, `class_list`.`name_id`,
  325. `class_name`.`name`, `class_name`.`school_id`, `class_name`.`category`, `class_name`.`group_id`
  326. FROM `class_detail`
  327. JOIN `class_list` ON `class_detail`.`class_list_id` = `class_list`.`id`
  328. JOIN `class_name` ON `class_list`.`name_id` = `class_name`.`id`
  329. """
  330. match_data = call_sql(sql_query)
  331. results = count_target_number(match_data, 'category')
  332. return results
  333. #@data.post("/registration_list_name")
  334. async def registration_list_name(category: Optional[int] = 0, group_id: Optional[int] = 0):
  335. if category == group_id == 1:
  336. return 'Please select only one.'
  337. elif category == group_id == 0:
  338. return 'Please select one.'
  339. sql_query = """
  340. SELECT `registration`.`event_id`, `class_list`.`name_id`, `class_name`.`name`, `class_name`.`school_id`, `class_name`.`category`, `class_name`.`group_id`
  341. FROM `registration`
  342. JOIN `class_list` ON `registration`.`event_id` = `class_list`.`id`
  343. JOIN `class_name` ON `class_list`.`name_id` = `class_name`.`id`
  344. """
  345. user_event = call_sql(sql_query)
  346. if category:
  347. return count_target_number(user_event, 'category')
  348. if group_id:
  349. return count_target_number(user_event, 'group_id')
  350. @data.get("/ga4_data")
  351. async def ga4_data(start_day: str = datetime.now().date(),end_day:str = datetime.now().date() ,name:str = "screenPageViews",dimensions:str="",page_size: str = "10"):
  352. start_time = datetime.strptime(start_day, "%Y-%m-%d").date() #現在時間
  353. end_time = datetime.strptime(end_day, "%Y-%m-%d").date()
  354. SCOPES = ['https://www.googleapis.com/auth/analytics.readonly']
  355. KEY_FILE_LOCATION = "./app/api/ntcri-space-400206-38246577c2b2.json"
  356. property_id= 'properties/378680283'
  357. credentials = ServiceAccountCredentials.from_json_keyfile_name(KEY_FILE_LOCATION, SCOPES)
  358. analytics = build('analyticsdata', 'v1beta', credentials=credentials)
  359. if dimensions == "":
  360. body={
  361. "requests": [
  362. {
  363. "dateRanges": [
  364. {
  365. "startDate": str(start_time),
  366. "endDate": str(end_time)
  367. }
  368. ],
  369. "metrics": [
  370. {
  371. "name": name
  372. }
  373. ],
  374. "limit": page_size
  375. }
  376. ]
  377. }
  378. else:
  379. body={
  380. "requests": [
  381. {
  382. "dateRanges": [
  383. {
  384. "startDate": str(start_time),
  385. "endDate": str(end_time)
  386. }
  387. ],
  388. "metrics": [
  389. {
  390. "name": name
  391. }
  392. ],
  393. "dimensions": [ #input the dimensions you need
  394. {
  395. "name": dimensions
  396. }
  397. ],
  398. "limit": page_size
  399. }
  400. ]
  401. }
  402. response = analytics.properties().batchRunReports(property=property_id, body=body).execute()
  403. return response