report.py 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  1. import pandas as pd
  2. import numpy as np
  3. import time
  4. import public as pb
  5. import openpyxl
  6. import matplotlib.pyplot as plt
  7. from scipy.stats import spearmanr
  8. # import tkinter as tk
  9. # from tkinter import ttk
  10. from tkinter import filedialog
  11. from tkinter import messagebox
  12. from openpyxl import load_workbook
  13. from openpyxl.drawing.image import Image as OImage
  14. import os
  15. import re
  16. import ttkbootstrap as ttk
  17. from ttkbootstrap.constants import *
  18. from PIL import Image, ImageTk
  19. from ttkbootstrap.dialogs import Messagebox
  20. import plotly.graph_objects as go
  21. import plotly.io as pio
  22. from sklearn.linear_model import LinearRegression
  23. from openpyxl.worksheet.hyperlink import Hyperlink
  24. from scipy.ndimage import gaussian_filter1d
  25. from scipy.stats import norm
  26. from plotly.subplots import make_subplots
  27. import docx
  28. from docx import Document
  29. from docx.shared import Inches
  30. from docx.oxml import OxmlElement, ns
  31. from docx.shared import Pt, RGBColor
  32. from docx.oxml.ns import nsdecls, nsmap
  33. from docx.oxml import parse_xml
  34. from docx.enum.dml import MSO_THEME_COLOR_INDEX
  35. from docx import Document
  36. from docx.opc.constants import RELATIONSHIP_TYPE as RT
  37. from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
  38. from docx.oxml.ns import qn
  39. from docx.enum.text import WD_ALIGN_PARAGRAPH
  40. # 显示所有数据
  41. pd.set_option('display.width', 10000) # 设置字符显示宽度
  42. pd.set_option('display.max_rows', None) # 设置显示最大行
  43. pd.set_option('display.max_columns', None) # 设置显示最大列,None为显示所有列
  44. # 设置字体
  45. # 设置字体 微软雅黑 新罗马 加粗
  46. plt.rcParams['font.family'] = ['Times New Roman', 'Microsoft YaHei']
  47. # 设置字体加粗
  48. font = {'weight': 'bold'}
  49. plt.rc('font', **font) # 应用字体设置
  50. indexClassificationList = {
  51. '物理指标': ['pH', '土壤质地', '土壤容重1(g/cm³)', '土壤容重2(g/cm³)', '土壤容重3(g/cm³)', '土壤容重4(g/cm³)', '土壤容重平均值(g/cm³)',
  52. '2~0.2mm颗粒含量', '0.2~0.02mm颗粒含量', '0.02~0.002mm颗粒含量', '0.002mm以下颗粒含量', '水稳>5mm(%)', '水稳3mm~5mm(%)',
  53. '水稳2mm~3mm(%)', '水稳1mm~2mm(%)', '水稳0.5mm~1mm(%)', '水稳0.25mm~0.5mm(%)', '水稳性大团聚体总和(%)', '洗失量(吸管法需填)', '风干试样含水量(分析基)'],
  54. '常规养分指标': ['有机质', '全氮', '全磷', '全钾', '有效磷', '速效钾', '有效硫', '有效硼', '有效铁', '有效锰', '有效铜', '有效锌', '有效钼', '有效硅', '缓效钾'],
  55. '一般化学性指标': ['阳离子交换量', '交换性盐基总量', '交换性钙', '交换性镁', '交换性钠', '交换性钾', '全盐量', '电导率',
  56. '水溶性Na⁺含量', '水溶性K⁺含量', '水溶性Ca²⁺含量', '水溶性Mg²⁺含量', '水溶性Cl⁻含量', '水溶性CO₃²⁻含量','水溶性HCO₃⁻含量',
  57. '水溶性SO₄²⁻含量', '离子总量', '碳酸钙',
  58. '游离铁', '全硫', '全锰', '全锌', '全铜', '全钼', '全硼', '全硒', '全铝', '全硅', '全铁', '全钙', '全镁'],
  59. '重金属指标': ['总汞', '总砷', '总铅', '总镉', '总铬', '总镍']
  60. }
  61. # 可交互绘图函数
  62. def getInteractiveImg(x,y,label,x1,y1,label1,x2,y2,label2,url,name,xLabel,YLabel,numArr):
  63. # coef, p_value = spearmanr(x, y)
  64. # 绘制数据散点图
  65. fig = go.Figure(data=go.Scatter(
  66. x=x,
  67. y=y,
  68. text=numArr.to_numpy(),
  69. mode='markers', name=label,
  70. marker=dict(
  71. size=4, # 点的大小
  72. color='blue', # 点的颜色
  73. ))
  74. )
  75. # 设置图表布局
  76. fig.update_layout(
  77. title={
  78. 'text': f"{name}",
  79. 'xanchor': 'center', # 控制水平对齐,可选'left', 'center', 'right'
  80. 'yanchor': 'bottom', # 控制垂直对齐,可选'top', 'middle', 'bottom'
  81. 'x': 0.5, # 控制标题的水平位置,0.5代表中心,可以是小数(相对位置)或整数(像素位置)
  82. 'y': 0.9 # 控制标题的垂直位置,0.9代表底部,可以是小数或整数
  83. },
  84. xaxis_title=xLabel,
  85. yaxis_title=YLabel)
  86. if label == 'pH':
  87. ph_y_f = [7.5 for _ in x]
  88. ph_y_s = [8.0 for _ in x]
  89. fig.add_trace(go.Scatter(x=x, y=ph_y_f, mode='lines', name='pH = 7.5', line=dict(
  90. width=1.5,
  91. color='#339933',
  92. dash='dash' # 设置虚线样式
  93. )))
  94. fig.add_trace(go.Scatter(x=x, y=ph_y_s, mode='lines', name='pH = 8.0', line=dict(
  95. width=2.5,
  96. color='#339933',
  97. dash='dash' # 设置虚线样式
  98. )))
  99. if len(x1) > 0 and len(y1) > 0:
  100. fig.add_trace(go.Scatter(x=x1, y=y1, mode='markers', name=label1, text=numArr.to_numpy(),
  101. marker=dict(
  102. size=4, # 点的大小
  103. color='red', # 点的颜色
  104. symbol='hourglass'
  105. )))
  106. if len(x2) > 0 and len(y2) > 0:
  107. fig.add_trace(go.Scatter(x=x2, y=y2, mode='markers', name=label2, text=numArr.to_numpy(),
  108. marker=dict(
  109. size=4, # 点的大小
  110. color='green', # 点的颜色
  111. symbol='triangle-up' # 点的形状,这里设置为正方形
  112. )))
  113. # model = LinearRegression()
  114. # model.fit(x.to_numpy().reshape(-1, 1), y) # 用x的平方作为特征值
  115. # y_pred = model.predict(x.to_numpy().reshape(-1, 1))
  116. # fig.add_trace(go.Scatter(x=x, y=y_pred, mode='lines', name='拟合直线'))
  117. html_file_path = f"{url}/{name}.html"
  118. pio.write_html(fig, file=html_file_path, auto_open=False)
  119. # 同时保存一份图片
  120. try:
  121. print(pio.kaleido.scope.plotlyjs)
  122. print(f"图片将保存至 " + f"{url}/{name}.png")
  123. fig.write_image(f"{url}/{name}.png",scale=3, width=800, height=600)
  124. print(f"图片已成功保存至 "+f"{url}/{name}.png")
  125. except Exception as e:
  126. print(f"保存图片时出现错误: {str(e)}")
  127. # 在文档中中插入html
  128. # workbook = load_workbook(filename=fileUrl)
  129. # # 选择一个工作表
  130. # ws = workbook[sheetName]
  131. # # 将 HTML 内容作为富文本写入单元格
  132. # ws[loc] = '=HYPERLINK("file:///{0}","点击查看统计图")'.format(html_file_path)
  133. # workbook.save(fileUrl)
  134. # 循环确定数据属于哪一个类型
  135. def getDataType(data, list):
  136. for item in list:
  137. if str(str(data['原样品编号'])[6:10]) in list[item]:
  138. data['土地类型归类'] = item
  139. return data
  140. # 1. 统计样品数量 分类统计:耕地:0101 0102 0103 园地:0201 0202 0203 0204 林地:0301 0302 0303 0304 0305 0306 0307 草地:0401 0402 0403 0404
  141. def getSimpleNum(data):
  142. """
  143. :param simpleData: 样品数据
  144. :return:
  145. """
  146. typeList = {
  147. '耕地园地': ['0101', '0102', '0103', '0201', '0202', '0203', '0204'],
  148. '林地草地': ['0301', '0302', '0303', '0304', '0305', '0306', '0307', '0401', '0402', '0403', '0404'],
  149. '商服用地': ['0501', '0502', '0503', '0504', '0505', '0506', '0507'],
  150. '工矿仓储用地': ['0601', '0602', '0603', '0604'],
  151. '住宅用地': ['0701', '0702'],
  152. '公共管理与公共服务用地': ['0801', '0802', '0803', '0804', '0805', '0806', '0807', '0808', '0809', '0810'],
  153. '特殊用地': ['0901', '0902', '0903', '0904', '0905', '0906'],
  154. '交通运输用地': ['1001', '1002', '1003', '1004', '1005', '1006', '1007', '1008', '1009'],
  155. '水域及水利设施用地': ['1101', '1102', '1103', '1104', '1105', '1106', '1107', '1108', '1109', '1110'],
  156. '其他土地': ['1201', '1202', '1203', '1204', '1205', '1206', '1207']
  157. }
  158. # 根据耕地园地、林地草地分类数据,统计不同类型数据数量,生成表格
  159. newData = pd.DataFrame({})
  160. for index, row in data.iterrows():
  161. newRow = getDataType(row, typeList)
  162. newData =newData._append(newRow)
  163. # 按照土地类型归类 分类计数
  164. grouped_df = newData.groupby('土地类型归类')
  165. counts = grouped_df.size()
  166. resData = pd.DataFrame({})
  167. for group_name, group_data in grouped_df:
  168. # 附表:数量总体统计数据
  169. res = {
  170. '类型': group_name,
  171. '样品编号': group_data['样品编号'].to_list(),
  172. '数量': counts[group_name],
  173. '合计': counts.sum()
  174. }
  175. newRes = pd.DataFrame(res)
  176. resData = resData._append(newRes)
  177. return {
  178. 'sData': counts, # 统计数量
  179. 'allData': resData # 附表总数
  180. }
  181. # 2.小数修改:进行中
  182. def is_one_decimal_place(num): # 判断是否保留一位小数
  183. print('检测小数1', num, str(num).count('.') == 1 and len(str(num).split('.')[1]) == 1)
  184. return (str(num).count('.') == 1 and len(str(num).split('.')[1]) == 1)
  185. def has_two_decimals(num): # 判断是否保留了两位小数
  186. print('检测小数2', num, str(num).count('.') == 1 and len(str(num).split('.')[1]) == 2)
  187. return (str(num).count(".") == 1 and len(str(num).split(".")[1]) == 2)
  188. def three_decimal(num):
  189. # 不超过三位有效数字
  190. num = str(num)
  191. num = num.replace('.', '')
  192. numLen = 0
  193. stripped_s = num.lstrip('0')
  194. if stripped_s and stripped_s[0].isdigit():
  195. index = num.index(stripped_s[0]) # 加上去除的0的个数
  196. numLen = len(num[index:])
  197. # 字符串全为0或不包含数字时
  198. return numLen
  199. def is_less_than_three_decimals(number): # 不超过3位小数
  200. # 转换为字符串
  201. number_str = str(number)
  202. # 分离整数部分和小数部分
  203. integer_part, decimal_part = number_str.split('.') if '.' in number_str else (number_str, '')
  204. # 判断小数部分是否不超过三位
  205. return len(decimal_part) <= 3
  206. # 保留3位小数
  207. def is_three_decimal(number):
  208. number_str = str(number)
  209. # 分离整数部分和小数部分
  210. integer_part, decimal_part = number_str.split('.') if '.' in number_str else (number_str, '')
  211. # 判断小数部分是否不超过三位
  212. return len(decimal_part) == 3
  213. def highlight_condition(s):
  214. if s['数据审核结果'] != '' and not pd.isna(s['数据审核结果']):
  215. return ['background-color: #99CC99']*len(s)
  216. else:
  217. return ['']*len(s)
  218. def filter_number(arr):
  219. """
  220. :param arr:
  221. :return:
  222. """
  223. return pd.to_numeric(arr, errors='coerce')
  224. def getNum(data, url):
  225. # 读取数据 处理每一项小数保留问题
  226. oneData = ['2~0.2mm颗粒含量','0.2~0.02mm颗粒含量','0.02~0.002mm颗粒含量','0.002mm以下颗粒含量', '洗失量(吸管法需填)',
  227. '水稳>5mm','水稳3mm~5mm','水稳2mm~3mm','水稳1mm~2mm','水稳0.5mm~1mm','水稳0.25mm~0.5mm','水稳性大团聚体总和'
  228. ]
  229. twoData = ['土壤容重1(g/cm³)','土壤容重2(g/cm³)','土壤容重3','土壤容重4','土壤容重平均值','pH']
  230. threeData = ['电导率','水溶性钠离子','水溶性钾离子','水溶性钙离子','水溶性镁离子','水溶性碳酸根','水溶性碳酸氢根','水溶性硫酸根','水溶性氯根','离子总量'] # 3位有效数字
  231. twoDataToThree = ['有机质','全氮','全磷','全钾','全硫','全硼','全硒','全铁'] #保留2位小数,最多不超过3位有效数字
  232. threeDataToThree = ['有效钼', '总汞']
  233. changeDataList = ['阳离子交换量', '交换性盐基总量', '交换性钙', '交换性镁', '交换性钠', '交换性钾'] # <10 2位小数 >=10 3位有效数字
  234. cationDataList = ['全锰', '全铜', '全锌', '速效钾', '缓效钾', '有效硅', '总铅', '总镉', '总铬', '总镍'] # <1000 2位小数 不超过3位有效数字 >=1000 保留整数
  235. needData = data.iloc[:, 7:]
  236. if '土壤质地' in needData.columns:
  237. del needData['土壤质地']
  238. # needData = needData.apply(pd.to_numeric, errors='coerce')
  239. checkDataRes = []
  240. for index, row in needData.iterrows():
  241. str = ''
  242. for item in row.index:
  243. if item in oneData and (not pd.isna(row[item])) and not is_one_decimal_place(row[item]):
  244. str += f"{item}应保留一位小数。"
  245. if (item in twoData) and (not pd.isna(row[item])) and not has_two_decimals(row[item]):
  246. str += f"{item}应保留两位小数。"
  247. if item in threeData and (not pd.isna(row[item])) and three_decimal(row[item]) != 3:
  248. str += f"{item}应保留三位有效数字。"
  249. if item in twoDataToThree and (not pd.isna(row[item])) and (not has_two_decimals(row[item]) or three_decimal(row[item]) > 3):
  250. str += f"{item}应保留两位小数且不超过三位有效数字。"
  251. if item in threeDataToThree and (not pd.isna(row[item])) and (not is_less_than_three_decimals(row[item]) or three_decimal(row[item]) != 3 ):
  252. str += f"{item}应保留三位有效数字且不超过三位小数。"
  253. if item in changeDataList and (not pd.isna(row[item])) and (filter_number(row[item]) < 10) and not has_two_decimals(row[item]):
  254. str += f"{item}应保留两位小数。"
  255. if item in changeDataList and (not pd.isna(row[item])) and (filter_number(row[item]) >= 10) and three_decimal(row[item]) != 3:
  256. str += f"{item}应保留三位有效数字。"
  257. if item == '碳酸钙' and (not pd.isna(row[item])) and not isinstance(row[item], int):
  258. str += f"{item}应为整数。"
  259. if item == '全盐量' and (not pd.isna(row[item])) and not is_three_decimal(row[item]):
  260. str += f"{item}应保留3位小数。"
  261. if item in cationDataList and (not pd.isna(row[item])) and (filter_number(row[item]) < 1000) and (not has_two_decimals(row[item]) or three_decimal(row[item]) > 3):
  262. str += f"{item}保留2位小数且不超过三位有效数字。"
  263. if item in cationDataList and (not pd.isna(row[item])) and (filter_number(row[item]) >= 1000) and not isinstance(row[item], int):
  264. str += f"{item}应为整数。"
  265. checkDataRes.append(str)
  266. data['数据审核结果'] = checkDataRes
  267. # 对审核结果有问题的数据进行标绿
  268. resData = data.style.apply(highlight_condition, axis=1)
  269. # 数据写入表格
  270. with pd.ExcelWriter( f'{url}/数值修约审核.xlsx', engine='openpyxl') as writer:
  271. resData.to_excel(writer, index=False, sheet_name='数值修约')
  272. # getNum('')
  273. # 3.计算所有指标的频度信息 已完成
  274. def getFrequencyInformation(data, url):
  275. if '数据审核结果' in data.columns:
  276. del data['数据审核结果']
  277. if '母岩' in data.columns:
  278. del data['母岩']
  279. #统计样品数量 计算最大值 最小值 中位数 平均值 标准差
  280. needData = data.iloc[:, 7:]
  281. if '土壤质地' in needData.columns:
  282. del needData['土壤质地']
  283. needData = needData.apply(pd.to_numeric, errors="coerce")
  284. resData = pd.DataFrame({})
  285. for item in needData.columns:
  286. if item == '总砷':
  287. print(needData[item])
  288. min_value = needData[item].min() # 最大值
  289. max_value = needData[item].max() # 最小值
  290. median_value = needData[item].median() # 中位数
  291. mean_value = needData[item].mean() # 平均数
  292. std_value = needData[item].std() # 标准差
  293. resData[item] = [min_value, max_value, median_value, mean_value, std_value]
  294. index_value = ['最小值', '最大值', '中位数', '平均数', '标准差']
  295. # 汇总数据
  296. resData.index = index_value
  297. data_res = round(resData, 2)
  298. data_res = data_res.rename_axis('频度分析')
  299. # data_res = data_res.transpose()
  300. # data_res = data_res.reset_index()
  301. # data_res.columns = ['指标名称', '最小值', '最大值', '中位数', '平均数', '标准差']
  302. print('频度分析---', data_res)
  303. return data_res
  304. # 数据写入表格
  305. # with pd.ExcelWriter(f'{url}/频度信息.xlsx', engine='openpyxl') as writer:
  306. # data_res.to_excel(writer, sheet_name='频度信息')
  307. # getFrequencyInformation('')
  308. # 4. 绘制每个指标的累积频率图
  309. def getMap(data, title, unit, url):
  310. data = data.dropna()
  311. if len(data) != 0: # 指标无数据 不进行绘图
  312. min = data.min()
  313. max = data.max()
  314. # 计算直方图
  315. bins = np.linspace(min - 1, max + 1, 30, endpoint=True)
  316. hist, bin_edges = np.histogram(data, bins=bins)
  317. cumulative_hist = np.cumsum(hist)
  318. # 计算拟合曲线
  319. p1 = np.polyfit(bin_edges[1:], hist, 2)
  320. p2 = np.poly1d(p1)
  321. # 计算累计频率
  322. cumulative_freq = cumulative_hist / cumulative_hist.max()
  323. # fig, ax1 = plt.subplots()
  324. fig = make_subplots(specs=[[{"secondary_y": True}]])
  325. if title == 'pH':
  326. fig.add_trace(go.Histogram(x=data, name="频数", nbinsx = 17, xbins = dict(start=0, end=8.5, size=0.5)), secondary_y=True)
  327. else:
  328. fig.add_trace(go.Histogram(x=data, name="频数"), secondary_y=True)
  329. # 绘制直方图 color="#ffc798" #a4464b #b1a9ab
  330. # fig = go.Figure(data=[go.Histogram(x=data, name="频数")])
  331. # 设置标题和其他格式
  332. fig.update_layout(
  333. title={
  334. 'text': f"{title}统计图",
  335. 'xanchor': 'center', # 控制水平对齐,可选'left', 'center', 'right'
  336. 'yanchor': 'bottom', # 控制垂直对齐,可选'top', 'middle', 'bottom'
  337. 'x': 0.5, # 控制标题的水平位置,0.5代表中心,可以是小数(相对位置)或整数(像素位置)
  338. 'y': 0.9 # 控制标题的垂直位置,0.9代表底部,可以是小数或整数
  339. },
  340. xaxis_title=f"{title}{unit}",
  341. yaxis_title='频次',
  342. bargap=0.2, # 相邻位置坐标的钢筋之间的间隙
  343. bargroupgap=0.1
  344. )
  345. # ax1.hist(data, bins=bins, rwidth=0.8, zorder=1, label="频数")
  346. # ax2 = ax1.twinx()
  347. # ax2.plot(bin_edges[1:], cumulative_freq, zorder=10, color="#a4464b", label="累积频率 ")
  348. fig.add_trace(go.Scatter(x=bin_edges[1:], y=cumulative_freq *100, mode='lines', name='累积频率',line=dict(width=2), yaxis='y2',
  349. marker=dict(
  350. color='red', # 点的颜色
  351. symbol='hourglass'
  352. )), secondary_y=False)
  353. # 绘制正态分布曲线
  354. # 估计正态分布参数
  355. mu = data.mean()
  356. sigma = data.std()
  357. # 创建正态分布对象
  358. dist = norm(mu, sigma)
  359. # 计算要绘制的x值
  360. x = np.linspace(bin_edges.min(), bin_edges.max(), 100)
  361. p = norm.pdf(x, mu, sigma)
  362. # ax3 = ax1.twinx()
  363. # # ax3.plot(bin_edges[1:], y_smoothed, label='拟合曲线', color="#333")
  364. # #label = 'N({:.2f}, {:.2f}^2)'.format(mu, sigma)
  365. # ax3.plot(x, dist.pdf(x), color="#333", label='拟合曲线')
  366. fig.add_trace(go.Scatter(x=x, y=p*100, mode='lines', yaxis='y2', name='拟合曲线', line=dict(width=2)), secondary_y=False)
  367. fig.update_layout(
  368. title={
  369. 'text': '指标频度统计图',
  370. 'xanchor': 'center', # 控制水平对齐,可选'left', 'center', 'right'
  371. 'yanchor': 'bottom', # 控制垂直对齐,可选'top', 'middle', 'bottom'
  372. 'x': 0.5, # 控制标题的水平位置,0.5代表中心,可以是小数(相对位置)或整数(像素位置)
  373. 'y': 0.9 # 控制标题的垂直位置,0.9代表底部,可以是小数或整数
  374. },
  375. yaxis_title='累积频率(%)', # 设置Y1轴标签
  376. yaxis2_title='频数'
  377. )
  378. maxData = 110
  379. if p.max()*100 >100:
  380. maxData = p.max()*100
  381. fig.update_yaxes(range=[0, maxData], row=1, col=1, secondary_y=False)
  382. html_file_path = f"{url}/{title}频度统计图.html"
  383. html_file_path = html_file_path.replace('(g/cm³)','')
  384. html_file_path = html_file_path.replace('(%)', '')
  385. html_file_path = html_file_path.replace('>', '')
  386. pio.write_html(fig, file=html_file_path, auto_open=False)
  387. title = title.replace('(g/cm³)', '')
  388. title = title.replace('(%)', '')
  389. title = title.replace('>', '')
  390. pio.write_image(fig, f"{url}/{title}频度统计图.png")
  391. def getFrequencyImage(data, url):
  392. unitList = {
  393. 'g/cm3': ['土壤容重1(g/cm³)','土壤容重2(g/cm³)', '土壤容重3(g/cm³)', '土壤容重4(g/cm³)','土壤容重平均值(g/cm³)'],
  394. '%': ['土壤机械组成','全铝', '全硅', '全铁', '全钙', '全镁', '水稳>5mm(%)','水稳3mm~5mm(%)', '水稳2mm~3mm(%)', '水稳1mm~2mm(%)', '水稳0.5mm~1mm(%)', '水稳0.25mm~0.5mm(%)', '水稳性大团聚体总和(%)'],
  395. 'g/kg': ['有机质', '全氮', '全磷', '全钾', '水溶性盐总量', '离子总量', '碳酸钙', '游离铁', '全硫'],
  396. 'mg/kg': ['有效磷', '速效钾', '有效硫', '有效硼', '有效铁', '有效锰', '有效铜', '有效锌', '有效钼', '有效硅', '缓效钾', '全锰',
  397. '全锌', '全铜', '全钼', '全硼', '全硒', '总汞', '总砷', '总铅', '总镉', '总铬', '总镍'],
  398. 'cmol/kg': ['阳离子交换量', '交换性盐基总量', '交换性钙', '交换性镁', '交换性钠', '交换性钾']
  399. }
  400. # 绘制图形
  401. needData = data.iloc[:, 7:] #这里有可能需要修改
  402. if '土壤质地' in needData.columns:
  403. del needData['土壤质地']
  404. needData = needData.apply(pd.to_numeric, errors="coerce")
  405. for item in needData.columns:
  406. # newData = needData[item].dropna()
  407. if len(needData) > 0:
  408. label = ''
  409. for i in unitList:
  410. if item in unitList[i]:
  411. label = i
  412. getMap(needData[item], item, label, url)
  413. # 公共函数 判断指标完整性
  414. # def checkAllNames(data, list):
  415. # for item in list:
  416. # if data[item] != '未检测' and data[item] != '/': # 未检测的值可以忽略 空值
  417. # if pd.isna(data[item]): # 表示值缺失
  418. # 5. 检测指标完整性
  419. def getDataComplete(data):
  420. # 根据类型:耕地园地表层土壤样品,耕地园地剖面土壤样品,林地草地表层土壤样品,林地草地剖面土壤样品,水稳定性大团聚体样品
  421. # 耕地园地样品列表 有效硅(水田才有)
  422. cultivatedLandandfieldList = ['风干试样含水量(分析基)', '洗失量(吸管法需填)', '土壤质地', '土壤容重1(g/cm³)', '土壤容重2(g/cm³)', '土壤容重3(g/cm³)', '土壤容重4(g/cm³)', '土壤容重平均值(g/cm³)'
  423. , '2~0.2mm颗粒含量', '0.2~0.02mm颗粒含量', '0.02~0.002mm颗粒含量', '0.002mm以下颗粒含量'
  424. , '水稳>5mm(%)', '水稳3mm~5mm(%)','水稳2mm~3mm(%)', '水稳1mm~2mm(%)','水稳0.5mm~1mm(%)','水稳0.25mm~0.5mm(%)', '水稳性大团聚体总和(%)'
  425. , 'pH', '阳离子交换量', '交换性盐基总量', '交换性钙', '交换性镁', '交换性钠', '交换性钾',
  426. '全盐量', '电导率', '水溶性Na⁺含量', '水溶性K⁺含量', '水溶性Ca²⁺含量', '水溶性Mg²⁺含量', '水溶性Cl⁻含量', '水溶性CO₃²⁻含量',
  427. '水溶性HCO₃⁻含量', '水溶性SO₄²⁻含量', '离子总量', '有机质', '全氮', '全磷', '全钾', '有效磷', '速效钾', '缓效钾', '有效硫', '有效铁',
  428. '有效锰', '有效铜', '有效锌', '有效硼', '有效钼', '总汞', '总砷', '总铅', '总镉', '总铬', '总镍']
  429. # 林地草地样品列表
  430. woodlandandGrassList = ['风干试样含水量(分析基)', '洗失量(吸管法需填)', '土壤质地', '土壤容重1(g/cm³)', '土壤容重2(g/cm³)', '土壤容重3(g/cm³)', '土壤容重4(g/cm³)', '土壤容重平均值(g/cm³)', '2~0.2mm颗粒含量','0.2~0.02mm颗粒含量', '0.02~0.002mm颗粒含量', '0.002mm以下颗粒含量', 'pH', '阳离子交换量', '交换性盐基总量', '交换性钙', '交换性镁', '交换性钠', '交换性钾', '有机质',
  431. '全氮', '全磷', '全钾', '有效磷', '速效钾',]
  432. # 其他样品列表 剖面
  433. # '耕地': ['0101', '0102', '0103'],
  434. # '园地': ['0201', '0202', '0203', '0204'],
  435. # '林地': ['0301', '0302', '0303', '0304', '0305', '0306', '0307'],
  436. # '草地': ['0401', '0402', '0403', '0404'],
  437. # 根据土地利用类型 判断指标是否完整 不完整的数据提取出来 这里规则无法确定 先放着吧
  438. # 统计所有数据中 各指标的数量
  439. resData = getSimpleNum(data)
  440. yxg = 0
  441. for index, row in data.iterrows():
  442. if str(str(row['原样品编号'])[6:10]) == '0101':
  443. yxg += 1
  444. data = data.replace('未检测', np.nan)
  445. if '序号' in data.columns:
  446. del data['序号']
  447. if '原样品编号' in data.columns:
  448. del data['原样品编号']
  449. if '样品编号' in data.columns:
  450. del data['样品编号']
  451. if '地理位置' in data.columns:
  452. del data['地理位置']
  453. if '母质' in data.columns:
  454. del data['母质']
  455. if '土壤类型' in data.columns:
  456. del data['土壤类型']
  457. if '土地利用类型' in data.columns:
  458. del data['土地利用类型']
  459. # 根据指标排序
  460. counts = data.count()
  461. # 根据土地类型统计样本数
  462. countData = {
  463. '耕地园地': resData['sData']['耕地园地'] if '耕地园地' in resData['sData'].index else 0,
  464. '林地草地': resData['sData']['林地草地'] if '林地草地' in resData['sData'].index else 0,
  465. '其他': 0,
  466. '有效硅': yxg
  467. }
  468. needNumList = []
  469. for item in data.columns:
  470. # 判断指标是否属于耕地园地、林地草地数组
  471. if item == '有效硅':
  472. needNumList.append(countData['有效硅'])
  473. elif (item in cultivatedLandandfieldList) and (item in woodlandandGrassList):
  474. needNumList.append(countData['耕地园地'] + countData['林地草地'])
  475. elif (item in cultivatedLandandfieldList) and (item not in woodlandandGrassList):
  476. needNumList.append(countData['耕地园地'])
  477. elif (item not in cultivatedLandandfieldList) and (item in woodlandandGrassList):
  478. needNumList.append(countData['林地草地'])
  479. elif (item not in cultivatedLandandfieldList) and (item not in woodlandandGrassList):
  480. needNumList.append(0)
  481. else:
  482. needNumList.append(0)
  483. counts = counts.to_frame()
  484. counts['应测数量'] = needNumList
  485. return counts
  486. # 6.指标名称与实际检测样品数量统计表,统计样品数量和指标检测数量
  487. def getCheckNum():
  488. # 统计每个指标的有值的数量,水溶性盐分总量大于1g/kg的 统计八大离子的检测量
  489. resData = ''
  490. return resData
  491. # 过滤特殊字符
  492. def filter_special_characters(s):
  493. s = str(s)
  494. return re.sub(r'[^a-zA-Z\u4e00-\u9fa5\d]', '', s)
  495. # 7.检测方法
  496. def checkMethod(data, url):
  497. # 各指标的检测标准方法
  498. checkData = {
  499. '土壤容重' : '《土壤检测 第4部分:土壤容重的测定》(NY/T 1121.4—2006) 环刀法',
  500. '机械组成' : '《土壤分析技术规范》(第二版), 5.1 吸管法',
  501. '土壤水稳性大团聚体': '《土壤检测 第19部分:土壤水稳定大团聚体组成的测定》(NY/T 1121.19—2008)筛分法',
  502. 'pH检测方法': '《土壤检测 第2部分:土壤pH的测定》(NY/T 1121.2—2006) 电位法',
  503. '水溶性盐类(水溶性盐总量、电导率、水溶性钠离子、钾离子、钙离子、镁离子、碳酸根、碳酸氢根、硫酸根、氯根)': '《森林土壤水溶性盐分分析》(LY/T 1251—1999)(浸提液中钙、镁、钾、钠离子的测定采用等离子体发射光谱法,硫酸根和碳酸根的测定增加离子色谱法) 质量法',
  504. '全氮': '《土壤检测 第24部分:土壤全氮的测定 自动定氮仪法》(NY/T 1121.24—2012) 自动定氮仪法',
  505. '全磷': '《森林土壤磷的测定》(LY/T 1232—2015)(详见本规范培训教材) 酸消解—电感耦合等离子体发射光谱法',
  506. '全钾': '《森林土壤钾的测定》(LY/T 1234—2015) 酸消解一电感耦合等离子体发射光谱法',
  507. '全铁': '《固体废物22种金属元素的测定》(HJ 781—2016) 酸消解—电感耦合等离子体发射光谱法',
  508. '全锰': '《固体废物22种金属元素的测定》(HJ 781—2016) 酸消解—电感耦合等离子体发射光谱法',
  509. '全钼': '《固体废物 金属元素的测定 电感耦合等离子体质谱法》(HJ 766—2015) 酸消解—电感耦合等离子体质谱法',
  510. '全铝': '《固体废物 22种金属元素的测定 电感耦合等离子体发射光谱法》(HJ 781—2016) 酸消解-电感耦合等离子体发射光谱法',
  511. '全硅': '《土壤和沉积物 11种元素的测定 碱熔—电感耦合等离子体发射光谱法》(HJ 974—2018) 碱熔一电感耦合等离子体发射光谱法 ',
  512. '全钙': '《固体废物 22种金属元素的测定 电感耦合等离子体发射光谱法》(HJ 781—2016) 酸消解—电感耦合等离子体发射光谱法',
  513. '全镁': '《固体废物 22种金属元素的测定 电感耦合等离子体发射光谱法》(HJ 781—2016) 酸消解—电感耦合等离子体发射光谱法',
  514. '速效钾': '《土壤 速效钾和缓效钾含量的测定》(NY/T 889—2004) 乙酸铵浸提—火焰光度法',
  515. '缓效钾': '《土壤 速效钾和缓效钾含量的测定》(NY/T 889—2004) 热硝酸浸提—火焰光度法',
  516. '有效硅': '《土壤检测第15部分:土壤有效硅的测定》(NY/T 1121.15-2006) 柠檬酸浸提-硅钼蓝比色法',
  517. '有效硼': '土壤样品制备与检测技术规范培训教材 沸水提取-电感耦合等离子体发射光谱法',
  518. '有效钼': '《土壤检测 第9部分:土壤有效钼的测定》(NY/T 1121.9-2023) 草酸-草酸铵浸提-电感耦合等离子体质谱法',
  519. '游离铁': '《土壤分析技术规范》(第二版),19.1游离铁(Fed)的测定(DCB法) 连二亚硫酸钠-柠檬酸钠-重 碳酸提-邻菲罗啉比色法',
  520. '总砷': '《土壤质量 总汞、总砷、总铅的测定 原子荧光法第2部分:土壤中总砷的测定》(GB/T22105.2-2008) 原子荧光法',
  521. '总铅': '《固体废物 金属元素的测定电感耦合等离子体质谱法》(HJ766-2015) 酸消解-电感耦合等离子体质谱法',
  522. '总镉': '《固体废物 金属元素的测定电感耦合等离子体质谱法》(HJ766-2015) 酸消解-电感耦合等离子体质谱法',
  523. '总铬': '《固体废物 金属元素的测定电感耦合等离子体质谱法》(HJ766-2015) 酸消解-电感耦合等离子体质谱法',
  524. '总镍': '《固体废物 金属元素的测定电感耦合等离子体质谱法》(HJ766-2015) 酸消解-电感耦合等离子体质谱法'
  525. }
  526. checkDBData = {
  527. '有机质': ['《土壤检测 第6部分:土壤有机质的测定》(NY/T 1121.6—2006) 重铬酸钾氧化—容量法',
  528. '土壤中总碳和有机质的测定元素分析仪法(农业行业标准报批稿) 元素分析仪法'], # 两种方法都可以
  529. '全硫': ['《土壤检测 第2部分:土壤全硫的测定》(NY/T 1104—2006) 硝酸镁氧化-硫酸钡比浊法',
  530. '燃烧红外光谱法(本规范培训教材)'],
  531. '全硼': ['《土壤分析技术规范》(第二版),18.1土壤全硼的测定 碱熔-姜黄素-比色法',
  532. '《土壤分析技术规范》(第二版),18.1土壤全硼的测定 碱熔-等离子体发射光谱法'],
  533. '全铜': ['《固体废物 金属元素的测定电感耦合等离子体质谱法》(HJ766-2015) 酸消解-电感耦合等离子体质谱法',
  534. '《固体废物 22种金属元素的测定电感耦合等离子体发射光谱法》(HJ781-2016) 酸消解-电感耦合等离子体发射光谱法'],
  535. '全锌': ['《固体废物 金属元素的测定电感耦合等离子体质谱法》(HJ766-2015) 酸消解一电感耦合等离子体质谱法',
  536. '《固体废物 22种金属元素的测定 电感耦合等离子体发射光谱法》(HJ 781—2016) 酸消解—电感耦合等离子体发射光谱法'],
  537. '可交换酸度': '《土壤分析技术规范》(第二版), 11.2 土壤交换性酸的测定 氯化钾交换—中和滴定法', # ph<6
  538. '碳酸钙': '《土壤分析技术规范》(第二版), 15.1 土壤碳酸盐的测定 气量法', # ph>7
  539. '总汞': ['《土壤质量 总汞、总砷、总铅的测定原子荧光法 第1部分:土壤中总汞的测定》(GB/T22105.1-2008) 原子荧光法',
  540. '《土壤和沉积物总汞的测定 催化热解/冷原子吸收分光光度法》(HJ923-2017) 催化热解-冷原子吸收分光光度法'],
  541. '有效铁': [
  542. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA 浸提-原子吸收分光光度法',
  543. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA 浸提-电感耦合等离子体发射光谱法'],
  544. '有效锰': [
  545. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA 浸提-原子吸收分光光度法',
  546. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA浸提-电感耦合等离子体发射光谱法'],
  547. '有效铜': [
  548. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA 浸提-原子吸收分光光度法',
  549. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA 浸提-电感耦合等离子体发射光谱法'],
  550. '有效锌': [
  551. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA 浸提-原子吸收分光光度法',
  552. '《土壤有效态锌、锰、铁、铜含量的测定 二乙三胺五乙酸(DTPA)浸提法》(NY/T8902004) DTPA 浸提-电感耦合等离子体发射光谱法'],
  553. }
  554. checkPHData = {
  555. '阳离子交换量': ['《土壤分析技术规范》(第二版), 12.2 乙酸铵交换法',
  556. '《土壤分析技术规范》(第二版), 12.1 EDTA—乙酸铵盐交换法'], # ph<=7.5 ph>7.5
  557. '交换性盐基总量、交换性钾、交换性钠、交换性钙、交换性镁': [
  558. '《土壤分析技术规范》(第二版), 13.1 酸性和中性土壤交换性盐基组分的测定(乙酸铵交换法)(交换性钙、镁、钾、钠离子的测定增加等离子体发射光谱法) 乙酸铵交换法等',
  559. '《石灰性土壤交换性盐基及盐基总量的测定》(NY/T 1615-2008)(交换液中钾、钠、钙、镁离子的测定增加等离子体发射光谱法) 氯化铵-乙醇交换法等'],
  560. # ph<=7.5 ph>7.5
  561. '有效磷': ['《土壤检测 第7部分:土壤有效磷的测定》(NY/T 1121.7—2014) 氟化铵-盐酸溶液浸提一钼锑抗比色法',
  562. '《土壤检测 第7部分:土壤有效磷的测定》(NY/T 1121.7—2014) 碳酸氢钠溶液—钼锑抗比色法'], # ph<6.5 ph>=6.5
  563. '有效硫': ['《土壤检测第14部分:土壤有效的测定》NY/T 1121.14-2023) 磷酸盐-乙酸溶液浸提-电感耦合等离子体发射光谱法',
  564. '《土壤检测第14部分:土壤有效的测定》(NY/T 1121.14-2023) 氯化钙浸提一电感耦合等离子体发射光谱法'],
  565. # ph<7.5 ph>=7.5
  566. }
  567. checkDataKey = [key for key in checkData]
  568. checkDBDataKey = ['有机质', '全硫', '全硼', '全铜', '全锌', '可交换酸度', '碳酸钙', '总汞', '有效铁', '有效锰', '有效铜', '有效锌']
  569. # [key for key in checkDBData]
  570. checkPHDataKey = ['阳离子交换量', '交换性盐基总量、交换性钾、交换性钠、交换性钙、交换性镁', '有效磷', '有效硫']
  571. # [key for key in checkPHData]
  572. # print('list',checkDBDataKey)
  573. # print('list', checkPHDataKey)
  574. checkDataRes = []
  575. for index, row in data.iterrows():
  576. str = ''
  577. for item in row.index:
  578. if row[item] == '未检测':
  579. str = ''
  580. elif row[item] == '未填写' or pd.isna(row[item]):
  581. str += f"{item}未填写。"
  582. elif not pd.isna(row[item]) and (item in checkDataKey) and (not pd.isna(checkData[item])): # 指标不为空 且 在通用指标列表中
  583. if filter_special_characters(row[item]) != filter_special_characters(checkData[item]):
  584. str += f"{item}检测方法填报有误。"
  585. elif (not pd.isna(row[item]) and (item in checkDBDataKey)):
  586. if filter_special_characters(row[item]) != filter_special_characters(checkDBData[item][0]) and filter_special_characters(row[item]) != filter_special_characters(checkDBData[item][1]):
  587. str += f"{item}检测方法填报有误。"
  588. # 指标在两种方法指标列表中
  589. elif (not pd.isna(row[item]) and (item in checkPHDataKey)):
  590. # 指标在区分ph值列表中
  591. if item == '阳离子交换量' and row['pH'] <= 7.5:
  592. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][0]):
  593. str += f"{item}检测方法填写有误。"
  594. if item == '阳离子交换量' and row['pH'] > 7.5:
  595. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][1]):
  596. str += f"{item}检测方法填写有误。"
  597. if item == '交换性盐基总量、交换性钾、交换性钠、交换性钙、交换性镁' and row['pH'] <= 7.5:
  598. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][0]):
  599. str += f"{item}检测方法填写有误。"
  600. if item == '交换性盐基总量、交换性钾、交换性钠、交换性钙、交换性镁' and row['pH'] > 7.5:
  601. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][0]):
  602. str += f"{item}检测方法填写有误。"
  603. if item == '有效磷' and row['pH'] < 6.5:
  604. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][0]):
  605. str += f"{item}检测方法填写有误。"
  606. if item == '有效磷' and row['pH'] >= 6.5:
  607. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][0]):
  608. str += f"{item}检测方法填写有误。"
  609. if item == '有效硫' and row['pH'] < 7.5:
  610. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][0]):
  611. str += f"{item}检测方法填写有误。"
  612. if item == '有效硫' and row['pH'] >= 7.5:
  613. if filter_special_characters(row[item]) != filter_special_characters(checkPHData[item][0]):
  614. str += f"{item}检测方法填写有误。"
  615. checkDataRes.append(str)
  616. data['数据审核结果'] = checkDataRes
  617. resData = data.style.apply(highlight_condition, axis=1)
  618. # 数据写入表格
  619. with pd.ExcelWriter(f'{url}/检测方法审核结果.xlsx', engine='openpyxl') as writer:
  620. resData.to_excel(writer, index=False, sheet_name='检测方法审核')
  621. return data[data['检测方法审核结果'] !='']
  622. # 8.数据填报审核
  623. def dataReportResult(data, url):
  624. # 未检出项填报,空值填报,错误值处理
  625. # 检测值有*号、/、未检测、空值、数值 为合理情况,
  626. resData = []
  627. for index, row in data.iterrows():
  628. str = ''
  629. for item in row.index:
  630. if row[item] == '-' or row[item] == 0:
  631. str = f"{item}数据填报错误。"
  632. resData.append(str)
  633. data['数据审核结果'] = resData
  634. finData = data.style.apply(highlight_condition, axis=1)
  635. # 数据写入表格
  636. with pd.ExcelWriter(f'{url}/数据填报项审核结果.xlsx', engine='openpyxl') as writer:
  637. finData.to_excel(writer, index=False, sheet_name='数据填报审核')
  638. # 9.土壤质地类型判断 这个之前已有,将土壤质地类型不一致的样本数据提取出来
  639. # 使用resData数据,判断土壤类型和土壤类型判断是否一致,不一致提取出不一致数据写入表格
  640. # 10.土壤检测数据超阈值样品统计表,将之前统计的超阈值的数据提取出来,显示原因
  641. # def getOverLineData(data, url): # 所有阈值判断
  642. # 提取数据项,异常原因,增加一列外业保持空值
  643. # 提取每个表格审核结果不为空的数据,最后将所有数据合并
  644. # resData =
  645. # 数据写入表格
  646. # with pd.ExcelWriter(f'{url}/超阈值样品统计表.xlsx', engine='openpyxl') as writer:
  647. # resData.to_excel(writer, index=False, sheet_name='超阈值数据')
  648. # 11.ph值统计 频度计算、历年数据统计、ph异常数据提取
  649. def getPHData(data, url):
  650. resData = pd.DataFrame({})
  651. # 计算频度
  652. min_value = data['pH'].min() # 最大值
  653. max_value = data['pH'].max() # 最小值
  654. median_value = data['pH'].median() # 中位数
  655. mean_value = round(data['pH'].mean(),2) # 平均数
  656. std_value = round(data['pH'].std(), 2) # 标准差
  657. resData['PH'] = [min_value, max_value, median_value, mean_value, std_value]
  658. index_value = ['最小值', '最大值', '中位数', '平均数', '标准差']
  659. resData.index = index_value
  660. # 绘制分布图
  661. x = np.arange(0, len(data['pH']), 1) # 横坐标数据
  662. data = data.sort_values(by='pH', ascending=True)
  663. y = data['pH']
  664. getInteractiveImg(x, y, 'pH', [], [], '', [], [], '', url,
  665. 'pH值分布图', '样品序号', 'pH', data['原样品编号'])
  666. # 提取异常数据
  667. abnormalData = pd.DataFrame({})
  668. for index, row in data.iterrows():
  669. if not pd.isna(row['pH']) and (row['pH'] < mean_value-3*std_value or row['pH'] > mean_value+3*std_value):
  670. newRow = row[['原样品编号', '样品编号', '土地利用类型', 'pH']]
  671. soilType = ''
  672. if isinstance(row['土壤类型'], str):
  673. if len(row['土壤类型'].split('_')) > 1:
  674. soilType = row['土壤类型'].split('_')[1]
  675. newRow['土壤类型'] = soilType
  676. newRow['外业情况'] = ''
  677. abnormalData = abnormalData._append(newRow)
  678. return {
  679. '异常数据': abnormalData,
  680. '频度分析': resData
  681. }
  682. # resData写进文档表格
  683. # 异常数据写入表格
  684. # 生成也可以写入文档表格
  685. # with pd.ExcelWriter( './img/ph异常数据.xlsx', engine='openpyxl') as writer:
  686. # abnormalData.to_excel(writer, index=False, sheet_name='ph异常数据')
  687. # resData.to_excel(writer, sheet_name='频度分析')
  688. # 12.有机质和全氮 计算比值、绘制相关性图、提取异常数据形成表格
  689. def getNAndC(data, url):
  690. # 去掉nan的值
  691. data = data.dropna(subset=['有机质', '全氮'])
  692. # 绘制散点图 拟合直线 计算方差
  693. x = data['有机质']
  694. y = data['全氮']
  695. plt.scatter(x, y)
  696. fig = go.Figure(data=go.Scatter(
  697. x=x,
  698. y=y,
  699. text=data['原样品编号'].to_numpy(),
  700. mode='markers', name='有机质与全氮',
  701. marker=dict(
  702. size=4, # 点的大小
  703. color='blue', # 点的颜色
  704. ))
  705. )
  706. # 使用sklearn的LinearRegression进行最小二乘法拟合
  707. model = LinearRegression()
  708. model.fit(x.to_numpy().reshape(-1, 1), y)
  709. # 计算拟合直线的斜率和截距
  710. slope = model.coef_[0]
  711. intercept = model.intercept_
  712. r, _ = np.corrcoef(y, slope * x + intercept)
  713. # 绘制拟合直线
  714. fig.add_trace(go.Scatter(x=x, y=slope * x + intercept,mode='lines', name='拟合直线'))
  715. # plt.plot(x, slope * x + intercept, color='red', label='拟合直线', linewidth=2)
  716. # 设置图表布局
  717. fig.update_layout(
  718. title={
  719. 'text': f"有机质与全氮相关性散点图,y={round(slope,2)}x + {round(intercept,2)},R²={round(r[1], 2) ** 2},R={round(r[1],1)}",
  720. 'xanchor': 'center', # 控制水平对齐,可选'left', 'center', 'right'
  721. 'yanchor': 'bottom', # 控制垂直对齐,可选'top', 'middle', 'bottom'
  722. 'x': 0.5, # 控制标题的水平位置,0.5代表中心,可以是小数(相对位置)或整数(像素位置)
  723. 'y': 0.9 # 控制标题的垂直位置,0.9代表底部,可以是小数或整数
  724. },
  725. xaxis_title='有机质(g/kg)',
  726. yaxis_title='全氮(g/kg)')
  727. html_file_path = f"{url}/有机质与全氮相关性散点图.html"
  728. pio.write_html(fig, file=html_file_path, auto_open=False)
  729. # 同时保存一份图片
  730. pio.write_image(fig, f"{url}/有机质与全氮相关性散点图.png")
  731. # plt.savefig('./img/审核报告图形/' + '有机质与全氮相关性散点图.png', dpi=500, bbox_inches='tight')
  732. # plt.show()
  733. abnormalData = pd.DataFrame({})
  734. for index, row in data.iterrows():
  735. if not pd.isna(row['有机质']) and not pd.isna(row['全氮']):
  736. if row['有机质']/row['全氮'] <13 or row['有机质']/row['全氮'] > 20:
  737. newRow = row[['原样品编号', '样品编号', '土地利用类型', '有机质', '全氮']]
  738. soilType = ''
  739. resStr = ''
  740. if row['有机质']/row['全氮'] < 13:
  741. resStr = '偏低'
  742. if row['有机质']/row['全氮'] > 20:
  743. resStr = '偏高'
  744. if isinstance(row['土壤类型'], str):
  745. if len(row['土壤类型'].split('_')) > 1:
  746. soilType = row['土壤类型'].split('_')[1]
  747. newRow['土壤类型'] = soilType
  748. newRow['碳氮比'] = round(row['有机质']/row['全氮'], 2)
  749. newRow['审核结果'] = resStr
  750. newRow['外业情况'] = ''
  751. abnormalData = abnormalData._append(newRow)
  752. return abnormalData
  753. # with pd.ExcelWriter('./img/碳氮比异常数据.xlsx', engine='openpyxl') as writer:
  754. # abnormalData.to_excel(writer, index=False, sheet_name='碳氮比异常数据')
  755. def getImg(x,y,label):
  756. plt.scatter(x, y)
  757. plt.xlabel('样品数量')
  758. plt.ylabel(label)
  759. # plt.savefig('./img/' + label + '数据分布图.png', dpi=500, bbox_inches='tight')
  760. plt.show()
  761. # 13.全磷和有效磷,绘图,统计异常值,绘图绘制在同一个图中
  762. def getPData(data, url):
  763. # 提取异常数
  764. abnormalData = pd.DataFrame({})
  765. # abnormalData.columns = ['原样品编号', '样品编号', '土地利用类型', '全磷', '有效磷', '土壤类型', '有效磷比', '外业情况']
  766. for index, row in data.iterrows():
  767. if not pd.isna(row['有效磷']) and not pd.isna(row['全磷']):
  768. if row['有效磷'] /(1000 * row['全磷']) >= 0.15:
  769. newRow = row[['原样品编号', '样品编号', '土地利用类型', '全磷', '有效磷']]
  770. soilType = ''
  771. if isinstance(row['土壤类型'], str):
  772. if len(row['土壤类型'].split('_')) > 1:
  773. soilType = row['土壤类型'].split('_')[1]
  774. newRow['土壤类型'] = soilType
  775. newRow['有效磷比'] = round(row['有效磷'] / (row['全磷']*10), 2)
  776. newRow['外业情况'] = ''
  777. abnormalData = abnormalData._append(newRow)
  778. with pd.ExcelWriter(f'{url}/有效磷占全磷比异常数据.xlsx', engine='openpyxl') as writer:
  779. abnormalData.to_excel(writer, index=False, sheet_name='有效磷占全磷比比异常数据')
  780. if not data['全磷'].empty and not data['有效磷'].empty:
  781. x1 = np.arange(0, len(data['全磷']), 1)
  782. x2 = np.arange(0, len(data['有效磷']), 1)
  783. x3 = np.arange(0, len(data['有效磷']/data['全磷']), 1)
  784. #data1 = data.sort_values(by='全磷', ascending=True)
  785. #data2 = data.sort_values(by='有效磷', ascending=True)
  786. data['有效磷占比'] = data['有效磷']/10*data['全磷']
  787. #data3 = data.sort_values(by='有效磷占比', ascending=True)
  788. y1 = data['全磷']
  789. y2 = data['有效磷']
  790. y3 = data['有效磷占比']
  791. # getImg(x1, y1,'全磷(g/kg)')
  792. # getImg(x2, y2, '有效磷(mg/kg)')
  793. # getImg(x3, y3, '有效磷占全磷比(%)')
  794. getInteractiveImg(x1, y1,'全磷(g/kg)',[], [], '', [], [], '', url,
  795. '全磷分布图', '样品序号', '全磷(g/kg)', data['原样品编号'])
  796. getInteractiveImg(x2, y2, '有效磷(mg/kg)', [], [], '', [], [], '', url,
  797. '有效磷分布图', '样品序号', '有效磷(mg/kg)', data['原样品编号'])
  798. getInteractiveImg(x3, y3, '有效磷占全磷比(%)', [], [], '', [], [], '', url,
  799. '有效磷占全磷比分布图', '样品序号', '有效磷占全磷比(%)', data['原样品编号'])
  800. del data['有效磷占比']
  801. return abnormalData
  802. # 14. 全钾、速效钾和缓效钾,绘图
  803. def getKData(data, url):
  804. data = data.replace(np.nan,0)
  805. x1 = np.arange(0, len(data['全钾']), 1)
  806. y1 = data['全钾']
  807. x2 = np.arange(0, len(data['速效钾'] + data['缓效钾']), 1)
  808. y2 = data['速效钾']/1000 + data['缓效钾']/1000
  809. getInteractiveImg(x1, y1, '全钾', x2, y2,'速效钾与缓效钾之和', [],[],'', url,'全钾与速效钾缓效钾之和关系统计图','样品序号','g/kg', data['原样品编号'])
  810. x = np.arange(0, len(data['速效钾']), 1)
  811. y = data['速效钾']
  812. x_1 = np.arange(0, len(data['缓效钾']), 1)
  813. y_1 = data['缓效钾']
  814. getInteractiveImg(x, y, '速效钾', x_1, y_1, '缓效钾', [], [], '', url,
  815. '速效钾与缓效钾散点图', '样品序号', 'mg/kg', data['原样品编号'])
  816. # 15.重金属 已有 提取重金属异常数据即可
  817. def getMetal(simpleData):
  818. # resData_14 数据中提取重金属超标的数据,提取相应的指标形成表格
  819. resData = ''
  820. return resData
  821. # 16.阳离子交换量与交换性盐基总量
  822. def cationExchangeCapacity(data, url):
  823. # 绘图
  824. x1 = data['阳离子交换量']
  825. y1 = data['交换性盐基总量']
  826. fig = go.Figure(data=go.Scatter(
  827. x=x1,
  828. y=y1,
  829. text=data['原样品编号'].to_numpy(),
  830. mode='markers', name='阳离子交换量与交换性盐基总量相关性散点图',
  831. marker=dict(
  832. size=4, # 点的大小
  833. color='blue', # 点的颜色
  834. ))
  835. )
  836. print(3.43)
  837. # 使用sklearn的LinearRegression进行最小二乘法拟合
  838. model = LinearRegression()
  839. model.fit(x1.to_numpy().reshape(-1, 1), y1)
  840. # 计算拟合直线的斜率和截距
  841. slope = model.coef_[0]
  842. intercept = model.intercept_
  843. r, _ = np.corrcoef(y1, slope * x1 + intercept)
  844. # 绘制拟合直线
  845. fig.add_trace(go.Scatter(x=x1, y=slope * x1 + intercept, mode='lines', name='拟合直线'))
  846. # plt.plot(x, slope * x + intercept, color='red', label='拟合直线', linewidth=2)
  847. # 设置图表布局
  848. fig.update_layout(
  849. title={
  850. 'text': f"阳离子交换量与交换性盐基总量相关性散点图,y={round(slope, 2)}x + {round(intercept, 2)},R²={round(r[1], 2) ** 2},R={round(r[1],1)}",
  851. 'xanchor': 'center', # 控制水平对齐,可选'left', 'center', 'right'
  852. 'yanchor': 'bottom', # 控制垂直对齐,可选'top', 'middle', 'bottom'
  853. 'x': 0.5, # 控制标题的水平位置,0.5代表中心,可以是小数(相对位置)或整数(像素位置)
  854. 'y': 0.9 # 控制标题的垂直位置,0.9代表底部,可以是小数或整数
  855. },
  856. xaxis_title='阳离子交换量(g/kg)',
  857. yaxis_title='交换性盐基总量(mS/cm)')
  858. html_file_path = f"{url}/阳离子交换量与交换性盐基总量相关性散点图.html"
  859. pio.write_html(fig, file=html_file_path, auto_open=False)
  860. # 同时保存一份图片
  861. pio.write_image(fig, f"{url}/阳离子交换量与交换性盐基总量相关性散点图.png")
  862. # 17.交换性盐基:二者之差 交换性盐基总量cmol(+)/kg 交换性钙镁钠钾之和 区分ph>7.5 和ph值<7.5
  863. def changeCation(data,url):
  864. hightData = data[data['pH'] > 7.5]
  865. lowData = data[data['pH'] <= 7.5]
  866. hightData = hightData.apply(pd.to_numeric, errors="coerce")
  867. lowData = lowData.apply(pd.to_numeric, errors="coerce")
  868. x_h = np.arange(0, len(hightData['交换性盐基总量']), 1)
  869. y_h = hightData['交换性盐基总量']
  870. y1_h = (hightData['交换性钙'] + hightData['交换性镁'] + hightData['交换性钾'] + hightData['交换性钠'])
  871. y2_h = (y_h-y1_h)
  872. # 绘图
  873. getInteractiveImg(x_h, y_h, '交换性盐基总量', x_h, y1_h, '钙镁钾钠之和', x_h, y2_h, '交换性盐基总量与钙镁钾钠和之差', url,
  874. '交换性盐基总量与交换性盐相关关系(pH大于7.5)', '样品序号', 'cmol/kg', hightData['原样品编号'])
  875. x_l = np.arange(0, len(lowData['交换性盐基总量']), 1)
  876. y_l = lowData['交换性盐基总量']
  877. y1_l = (lowData['交换性钙'] + lowData['交换性镁'] + lowData['交换性钾'] + lowData['交换性钠'])
  878. y2_l = (y_l - y1_l)
  879. getInteractiveImg(x_l, y_l, '交换性盐基总量', x_l, y1_l, '钙镁钾钠之和', x_l, y2_l,
  880. '交换性盐基总量与钙镁钾钠和之差', url,
  881. '交换性盐基总量与交换性盐相关关系(pH小于等于7.5)', '样品序号', 'cmol/kg', lowData['原样品编号'])
  882. # 18.水溶性盐总量、电导率、离子总量全盐量分布图,全盐量和电导率相关性分析,水溶性盐与离子总量关系
  883. def manyTypes(data,url):
  884. # data = data.replace('未检测', np.nan)
  885. data = data.dropna(subset=['全盐量', '电导率'])
  886. print(3.41)
  887. # 全盐量分布图
  888. x = np.arange(0, len(data['全盐量']), 1)
  889. data_1 = data.sort_values(by='全盐量', ascending=True)
  890. y = data_1['全盐量']
  891. getInteractiveImg(x, y, '全盐量', [], [], '', [], [],
  892. '', url,
  893. '全盐量分布图', '样品序号', '全盐量(g/kg)', data_1['原样品编号'])
  894. # 电导率分布图
  895. # x1 = np.arange(0, len(data['电导率']), 1)
  896. # y1 = data['电导率'].sort_values()
  897. # getInteractiveImg(x1, y1, '电导率', [], [], '', [], [],
  898. # '', './img/审核报告图形',
  899. # '电导率分布图', '样品序号', '电导率(mS/cm)', data['原样品编号'], 'fileUrl',
  900. # 'loc')
  901. print(3.42)
  902. x1 = data['全盐量'].dropna()
  903. y1 = data['电导率'].dropna()
  904. print('电导率', y1)
  905. # plt.scatter(x, y)
  906. fig = go.Figure(data=go.Scatter(
  907. x=x1,
  908. y=y1,
  909. text=data['原样品编号'].to_numpy(),
  910. mode='markers', name='全盐量与电导率相关关系',
  911. marker=dict(
  912. size=4, # 点的大小
  913. color='blue', # 点的颜色
  914. ))
  915. )
  916. print(3.43)
  917. # 使用sklearn的LinearRegression进行最小二乘法拟合
  918. model = LinearRegression()
  919. model.fit(x1.to_numpy().reshape(-1, 1), y1)
  920. # 计算拟合直线的斜率和截距
  921. slope = model.coef_[0]
  922. intercept = model.intercept_
  923. r, _ = np.corrcoef(y1, slope * x1 + intercept)
  924. # 绘制拟合直线
  925. fig.add_trace(go.Scatter(x=x1, y=slope * x1 + intercept, mode='lines', name='拟合直线'))
  926. # plt.plot(x, slope * x + intercept, color='red', label='拟合直线', linewidth=2)
  927. # 设置图表布局
  928. fig.update_layout(
  929. title={
  930. 'text':f"全盐量与电导率相关性散点图,y={round(slope, 2)}x + {round(intercept, 2)},R²={round(r[1], 2) ** 2},R={round(r[1], 2)}",
  931. 'xanchor': 'center', # 控制水平对齐,可选'left', 'center', 'right'
  932. 'yanchor': 'bottom', # 控制垂直对齐,可选'top', 'middle', 'bottom'
  933. 'x': 0.5, # 控制标题的水平位置,0.5代表中心,可以是小数(相对位置)或整数(像素位置)
  934. 'y': 0.9 # 控制标题的垂直位置,0.9代表底部,可以是小数或整数
  935. },
  936. xaxis_title='全盐量(g/kg)',
  937. yaxis_title='电导率(mS/cm)')
  938. html_file_path = f"{url}/全盐量与电导率相关性散点图.html"
  939. pio.write_html(fig, file=html_file_path, auto_open=False)
  940. # 同时保存一份图片
  941. pio.write_image(fig, f"{url}/全盐量与电导率相关性散点图.png")
  942. print(3.44)
  943. # 离子总量 水溶性盐总量及差值
  944. filterData = data.dropna(subset=['全盐量', '离子总量'])
  945. x2 = np.arange(0, len(filterData['离子总量']), 1)
  946. print(x2)
  947. print(3.441)
  948. print(filterData['离子总量'])
  949. y2 = filterData['离子总量']
  950. print(y2)
  951. print(3.442)
  952. y3 = filterData['全盐量']
  953. print(y3)
  954. print(3.443)
  955. y4 = (y2-y3)
  956. if not filterData.empty:
  957. #要增加对指标值是否缺失进行判断,都不缺失绘图
  958. getInteractiveImg(x2, y2, '离子总量', x2, y3, '全盐量', x2, y4,
  959. '离子总量与全盐量之差', url,
  960. '全盐量与离子总量相关性散点图', '样品数量', '离子总量/全盐量(g/kg)', data['原样品编号'])