- 将大量自定义 CSS(主题色、卡片、徽章、alert 等)精简为仅保留字体设置 - sidebar 标题/副标题/分隔线改用 st.header、st.caption、st.divider - KPI 指标卡片改用 st.metric - 作物推荐排行改用 st.container + st.progress - 种植建议的 alert 框改用 st.warning / st.success / st.info - 删除自定义 HTML footer,改用 st.caption
359 lines
15 KiB
Python
359 lines
15 KiB
Python
"""
|
||
农业智能决策系统
|
||
基于多因子 Cobb-Douglas 产量模型的作物种植决策支持应用
|
||
"""
|
||
|
||
import streamlit as st
|
||
import numpy as np
|
||
import plotly.graph_objects as go
|
||
|
||
# ─── Page Config ────────────────────────────────────────────────────────────
|
||
st.set_page_config(
|
||
page_title="种植决策助手",
|
||
page_icon="🌾",
|
||
layout="wide",
|
||
initial_sidebar_state="expanded",
|
||
)
|
||
|
||
# ─── Minimal CSS ─────────────────────────────────────────────────────────────
|
||
st.markdown("""
|
||
<style>
|
||
html, body, [class*="css"] {
|
||
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
|
||
}
|
||
</style>
|
||
""", unsafe_allow_html=True)
|
||
|
||
|
||
# ─── Crop Database ───────────────────────────────────────────────────────────
|
||
CROPS = {
|
||
"水稻": {
|
||
"emoji": "🌾",
|
||
"optimal": {"ph": (6.0, 7.0), "N": (80, 120), "P": (30, 60), "K": (40, 80),
|
||
"rainfall": (150, 250), "temp": (22, 30)},
|
||
"base_yield": 7500, # kg/ha
|
||
"color": "#4a7c59"
|
||
},
|
||
"小麦": {
|
||
"emoji": "🌿",
|
||
"optimal": {"ph": (6.0, 7.5), "N": (60, 100), "P": (20, 50), "K": (30, 60),
|
||
"rainfall": (60, 120), "temp": (15, 22)},
|
||
"base_yield": 6000,
|
||
"color": "#c69c5d"
|
||
},
|
||
"玉米": {
|
||
"emoji": "🌽",
|
||
"optimal": {"ph": (5.8, 7.0), "N": (100, 150), "P": (40, 70), "K": (60, 100),
|
||
"rainfall": (100, 180), "temp": (20, 28)},
|
||
"base_yield": 8500,
|
||
"color": "#e8a93f"
|
||
},
|
||
"大豆": {
|
||
"emoji": "🫘",
|
||
"optimal": {"ph": (6.0, 7.0), "N": (20, 50), "P": (30, 60), "K": (40, 80),
|
||
"rainfall": (80, 150), "temp": (18, 26)},
|
||
"base_yield": 3500,
|
||
"color": "#8b7cb3"
|
||
},
|
||
"油菜": {
|
||
"emoji": "🌻",
|
||
"optimal": {"ph": (6.0, 7.5), "N": (80, 130), "P": (30, 60), "K": (50, 90),
|
||
"rainfall": (80, 130), "temp": (15, 20)},
|
||
"base_yield": 3000,
|
||
"color": "#d97836"
|
||
},
|
||
"棉花": {
|
||
"emoji": "☁️",
|
||
"optimal": {"ph": (6.0, 8.0), "N": (60, 100), "P": (20, 45), "K": (40, 70),
|
||
"rainfall": (70, 120), "temp": (25, 32)},
|
||
"base_yield": 4500,
|
||
"color": "#5a6b7c"
|
||
},
|
||
}
|
||
|
||
|
||
# ─── Yield Model ─────────────────────────────────────────────────────────────
|
||
def compute_factor(value, optimal_low, optimal_high, penalty=0.5):
|
||
"""Score 0-1: 1 if in optimal range, decays outside."""
|
||
mid = (optimal_low + optimal_high) / 2
|
||
width = (optimal_high - optimal_low) / 2 + 1e-9
|
||
if optimal_low <= value <= optimal_high:
|
||
return 1.0
|
||
dist = min(abs(value - optimal_low), abs(value - optimal_high))
|
||
return max(0.0, 1.0 - penalty * (dist / width))
|
||
|
||
|
||
def predict_yield(crop_name, ph, N, P, K, rainfall, temp, pesticide, area):
|
||
crop = CROPS[crop_name]
|
||
opt = crop["optimal"]
|
||
|
||
f_ph = compute_factor(ph, *opt["ph"], penalty=0.6)
|
||
f_N = compute_factor(N, *opt["N"], penalty=0.4)
|
||
f_P = compute_factor(P, *opt["P"], penalty=0.4)
|
||
f_K = compute_factor(K, *opt["K"], penalty=0.4)
|
||
f_rain = compute_factor(rainfall, *opt["rainfall"], penalty=0.5)
|
||
f_temp = compute_factor(temp, *opt["temp"], penalty=0.7)
|
||
f_pest = 0.5 + 0.5 * min(pesticide / 100, 1.0)
|
||
|
||
# Cobb-Douglas style yield function
|
||
nutrient_idx = (f_N * f_P * f_K) ** (1/3)
|
||
soil_idx = f_ph
|
||
climate_idx = (f_rain * f_temp) ** 0.5
|
||
|
||
total_factor = soil_idx ** 0.2 * nutrient_idx ** 0.4 * climate_idx ** 0.3 * f_pest ** 0.1
|
||
|
||
yield_per_ha = crop["base_yield"] * total_factor
|
||
total_yield = yield_per_ha * area
|
||
|
||
factors = {
|
||
"土壤pH": f_ph, "氮(N)": f_N, "磷(P)": f_P,
|
||
"钾(K)": f_K, "降雨量": f_rain, "温度": f_temp, "农药": f_pest
|
||
}
|
||
return yield_per_ha, total_yield, factors
|
||
|
||
|
||
def rank_crops(ph, N, P, K, rainfall, temp, pesticide, area):
|
||
results = []
|
||
for name in CROPS:
|
||
yph, ytotal, factors = predict_yield(name, ph, N, P, K, rainfall, temp, pesticide, area)
|
||
score = np.mean(list(factors.values()))
|
||
results.append({
|
||
"crop": name,
|
||
"emoji": CROPS[name]["emoji"],
|
||
"yield_ha": yph,
|
||
"total_yield": ytotal,
|
||
"score": score,
|
||
"color": CROPS[name]["color"],
|
||
"factors": factors
|
||
})
|
||
results.sort(key=lambda x: x["score"], reverse=True)
|
||
return results
|
||
|
||
|
||
# ─── Sidebar Inputs ──────────────────────────────────────────────────────────
|
||
with st.sidebar:
|
||
st.header("🌾 种植决策助手")
|
||
st.caption("根据土壤和气候,推荐适宜作物")
|
||
st.divider()
|
||
|
||
st.subheader("🧪 土壤参数")
|
||
col1, col2 = st.columns(2)
|
||
with col1:
|
||
ph = st.slider("pH 值", 4.0, 9.0, 6.5, 0.1)
|
||
N = st.slider("氮 N (mg/kg)", 0, 200, 90, 5)
|
||
with col2:
|
||
P = st.slider("磷 P (mg/kg)", 0, 100, 45, 5)
|
||
K = st.slider("钾 K (mg/kg)", 0, 150, 60, 5)
|
||
|
||
st.subheader("🌦 气象数据")
|
||
col3, col4 = st.columns(2)
|
||
with col3:
|
||
rainfall = st.slider("降雨量 (mm/月)", 0, 400, 120, 10)
|
||
with col4:
|
||
temp = st.slider("温度 (°C)", 0, 45, 22, 1)
|
||
|
||
st.subheader("🌱 种植参数")
|
||
area = st.number_input("种植面积 (公顷)", 0.1, 10000.0, 100.0, 10.0)
|
||
pesticide = st.slider("农药用量 (kg/ha)", 0, 200, 50, 5)
|
||
|
||
st.subheader("🎯 目标作物")
|
||
selected_crop = st.selectbox(
|
||
"选择分析作物",
|
||
list(CROPS.keys()),
|
||
format_func=lambda x: f"{CROPS[x]['emoji']} {x}"
|
||
)
|
||
|
||
|
||
# ─── Compute ──────────────────────────────────────────────────────────────────
|
||
yph, ytotal, factors = predict_yield(selected_crop, ph, N, P, K, rainfall, temp, pesticide, area)
|
||
rankings = rank_crops(ph, N, P, K, rainfall, temp, pesticide, area)
|
||
best_crop = rankings[0]
|
||
|
||
|
||
# ─── Main Layout ─────────────────────────────────────────────────────────────
|
||
st.title("🌾 种植决策助手")
|
||
st.caption("输入土壤与气象条件,获得作物产量预测与种植建议")
|
||
|
||
# KPI row
|
||
overall = np.mean(list(factors.values()))
|
||
k1, k2, k3, k4 = st.columns(4)
|
||
k1.metric(f"{CROPS[selected_crop]['emoji']} {selected_crop} 单产", f"{yph:,.0f} kg/ha")
|
||
k2.metric(f"📦 {area:.0f} 公顷总产", f"{ytotal/1000:,.1f} 吨")
|
||
k3.metric("🎯 环境匹配指数", f"{overall*100:.1f}%")
|
||
k4.metric("🏆 最优推荐作物", f"{best_crop['emoji']} {best_crop['crop']}", f"匹配度 {best_crop['score']*100:.0f}%")
|
||
|
||
# ─── Charts Row ──────────────────────────────────────────────────────────────
|
||
col_left, col_right = st.columns([3, 2])
|
||
|
||
with col_left:
|
||
st.subheader("📊 影响因子雷达图")
|
||
|
||
factor_names = list(factors.keys())
|
||
factor_vals = [round(v * 100, 1) for v in factors.values()]
|
||
factor_names_closed = factor_names + [factor_names[0]]
|
||
factor_vals_closed = factor_vals + [factor_vals[0]]
|
||
|
||
fig_radar = go.Figure()
|
||
fig_radar.add_trace(go.Scatterpolar(
|
||
r=factor_vals_closed,
|
||
theta=factor_names_closed,
|
||
fill='toself',
|
||
fillcolor='rgba(74, 124, 89, 0.15)',
|
||
line=dict(color='#4a7c59', width=2),
|
||
name=selected_crop,
|
||
))
|
||
fig_radar.add_trace(go.Scatterpolar(
|
||
r=[100]*len(factor_names_closed),
|
||
theta=factor_names_closed,
|
||
line=dict(color='rgba(0,0,0,0.15)', width=1, dash='dot'),
|
||
mode='lines',
|
||
name='理想值',
|
||
))
|
||
fig_radar.update_layout(
|
||
polar=dict(
|
||
bgcolor='rgba(0,0,0,0)',
|
||
radialaxis=dict(range=[0, 100], showticklabels=True,
|
||
tickfont=dict(color='#5a5a5a', size=9),
|
||
gridcolor='rgba(0,0,0,0.08)'),
|
||
angularaxis=dict(tickfont=dict(color='#2c2c2c', size=11),
|
||
gridcolor='rgba(0,0,0,0.1)'),
|
||
),
|
||
paper_bgcolor='rgba(0,0,0,0)',
|
||
plot_bgcolor='rgba(0,0,0,0)',
|
||
font=dict(color='#2c2c2c'),
|
||
legend=dict(orientation='h', y=-0.12, font=dict(size=10)),
|
||
margin=dict(t=20, b=40, l=40, r=40),
|
||
height=320,
|
||
)
|
||
st.plotly_chart(fig_radar, use_container_width=True)
|
||
|
||
with col_right:
|
||
st.subheader("🏅 作物推荐排行")
|
||
for i, r in enumerate(rankings[:4]):
|
||
rank_icons = ["🥇", "🥈", "🥉", "4️⃣"]
|
||
with st.container(border=True):
|
||
c1, c2 = st.columns([3, 1])
|
||
c1.markdown(f"**{rank_icons[i]} {r['emoji']} {r['crop']}**")
|
||
c2.markdown(f"<div style='text-align:right; color:#4a7c59; font-weight:600;'>{r['score']*100:.1f}%</div>", unsafe_allow_html=True)
|
||
st.progress(int(r['score'] * 100), text=f"{r['yield_ha']:,.0f} kg/ha · 总产 {r['total_yield']/1000:,.1f} 吨")
|
||
|
||
# ─── Sensitivity Analysis ─────────────────────────────────────────────────────
|
||
st.subheader("📈 产量敏感性分析")
|
||
|
||
sa_col1, sa_col2 = st.columns(2)
|
||
|
||
with sa_col1:
|
||
N_range = np.linspace(0, 200, 60)
|
||
y_N = [predict_yield(selected_crop, ph, n, P, K, rainfall, temp, pesticide, 1)[0] for n in N_range]
|
||
|
||
fig_N = go.Figure()
|
||
fig_N.add_trace(go.Scatter(
|
||
x=N_range, y=y_N,
|
||
mode='lines', line=dict(color='#4a7c59', width=2.5),
|
||
fill='tozeroy', fillcolor='rgba(74, 124, 89, 0.08)',
|
||
name='产量'
|
||
))
|
||
fig_N.add_vline(x=N, line=dict(color='#d4a574', width=1.5, dash='dot'),
|
||
annotation_text=f"当前 {N}", annotation_font_color='#7c5e42')
|
||
fig_N.update_layout(
|
||
title=dict(text="氮肥用量 vs 产量", font=dict(color='#5a5a5a', size=12)),
|
||
xaxis=dict(title="氮 N (mg/kg)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
|
||
yaxis=dict(title="产量 (kg/ha)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
|
||
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
||
font=dict(color='#2c2c2c', size=10),
|
||
margin=dict(t=36, b=36, l=50, r=20), height=220,
|
||
showlegend=False,
|
||
)
|
||
st.plotly_chart(fig_N, use_container_width=True)
|
||
|
||
with sa_col2:
|
||
rain_range = np.linspace(0, 400, 60)
|
||
y_rain = [predict_yield(selected_crop, ph, N, P, K, r, temp, pesticide, 1)[0] for r in rain_range]
|
||
|
||
fig_R = go.Figure()
|
||
fig_R.add_trace(go.Scatter(
|
||
x=rain_range, y=y_rain,
|
||
mode='lines', line=dict(color='#5a8f9e', width=2.5),
|
||
fill='tozeroy', fillcolor='rgba(90, 143, 158, 0.08)',
|
||
name='产量'
|
||
))
|
||
fig_R.add_vline(x=rainfall, line=dict(color='#d4a574', width=1.5, dash='dot'),
|
||
annotation_text=f"当前 {rainfall}mm", annotation_font_color='#7c5e42')
|
||
fig_R.update_layout(
|
||
title=dict(text="月降雨量 vs 产量", font=dict(color='#5a5a5a', size=12)),
|
||
xaxis=dict(title="降雨量 (mm/月)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
|
||
yaxis=dict(title="产量 (kg/ha)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
|
||
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
||
font=dict(color='#2c2c2c', size=10),
|
||
margin=dict(t=36, b=36, l=50, r=20), height=220,
|
||
showlegend=False,
|
||
)
|
||
st.plotly_chart(fig_R, use_container_width=True)
|
||
|
||
# ─── All Crops Comparison Bar Chart ───────────────────────────────────────────
|
||
st.subheader("🌐 全作物产量对比")
|
||
|
||
crop_names = [f"{r['emoji']} {r['crop']}" for r in rankings]
|
||
crop_yields = [r['yield_ha'] for r in rankings]
|
||
crop_colors = [r['color'] for r in rankings]
|
||
|
||
fig_bar = go.Figure()
|
||
fig_bar.add_trace(go.Bar(
|
||
x=crop_names, y=crop_yields,
|
||
marker=dict(color=crop_colors, opacity=0.85,
|
||
line=dict(color='rgba(0,0,0,0.08)', width=1)),
|
||
text=[f"{y:,.0f}" for y in crop_yields],
|
||
textposition='outside',
|
||
textfont=dict(color='#5a5a5a', size=10),
|
||
))
|
||
fig_bar.update_layout(
|
||
xaxis=dict(color='#5a5a5a', gridcolor='rgba(0,0,0,0.04)'),
|
||
yaxis=dict(title="预期产量 (kg/ha)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
|
||
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
||
font=dict(color='#2c2c2c', size=11),
|
||
margin=dict(t=20, b=30, l=60, r=20), height=260,
|
||
showlegend=False,
|
||
)
|
||
st.plotly_chart(fig_bar, use_container_width=True)
|
||
|
||
# ─── Advisory Panel ───────────────────────────────────────────────────────────
|
||
st.subheader("💡 种植建议")
|
||
adv1, adv2 = st.columns(2)
|
||
|
||
with adv1:
|
||
crop_opt = CROPS[selected_crop]["optimal"]
|
||
|
||
if not (crop_opt["ph"][0] <= ph <= crop_opt["ph"][1]):
|
||
st.warning(f"pH {ph} 偏离 {selected_crop} 适宜范围 {crop_opt['ph']},建议{'施石灰' if ph < crop_opt['ph'][0] else '施硫磺'}调节")
|
||
else:
|
||
st.success(f"土壤 pH {ph} 处于 {selected_crop} 适宜范围内")
|
||
|
||
if N < crop_opt["N"][0]:
|
||
st.warning(f"氮肥不足({N} vs 建议 {crop_opt['N'][0]}-{crop_opt['N'][1]} mg/kg),建议追施尿素")
|
||
elif N > crop_opt["N"][1]:
|
||
st.warning(f"氮肥过量({N} mg/kg),可能造成徒长,建议减施")
|
||
else:
|
||
st.success(f"氮肥水平 {N} mg/kg 适宜")
|
||
|
||
if rainfall < crop_opt["rainfall"][0]:
|
||
st.warning(f"降雨量不足,建议增加灌溉(缺水 {crop_opt['rainfall'][0]-rainfall} mm)")
|
||
elif rainfall > crop_opt["rainfall"][1]:
|
||
st.warning(f"降雨量偏多,注意防涝排水")
|
||
else:
|
||
st.success(f"降雨量 {rainfall}mm 适合 {selected_crop} 生长")
|
||
|
||
with adv2:
|
||
suitable = [r for r in rankings if r['score'] > 0.6]
|
||
if suitable:
|
||
st.info("当前环境参数下适宜种植:" + "、".join([f"{r['emoji']} {r['crop']} ({r['score']*100:.0f}%)" for r in suitable]))
|
||
else:
|
||
st.info("当前环境参数下暂无特别适宜的作物")
|
||
|
||
with st.container(border=True):
|
||
st.markdown(f"**最优方案:{best_crop['emoji']} {best_crop['crop']}**")
|
||
st.markdown(f"- 预期单产:**{best_crop['yield_ha']:,.0f} kg/ha**")
|
||
st.markdown(f"- {area:.0f} 公顷总产:**{best_crop['total_yield']/1000:,.1f} 吨**")
|
||
|
||
st.divider()
|
||
st.caption("种植决策助手 · 基于 Cobb-Douglas 多因子产量模型 · 仅供参考")
|